Share008資訊科技公司

我是資深的電腦資訊從業員,曾於 Motorola 及 Philips 等跨國大型公司管理層工作十多年,具各類ERP資源管理系統及其它應用系統經驗,如QAD之MFG/PRO、SAP、Ufida(用友)、Kingdee(金蝶)、Microsoft's Dynamic、Wonderware's In-Track (SFC)、Webplan (SCM)、Hyperion (business intelligence)、Informatics (Data Warehouse)...等等。另外,我精於廠房車間之電腦資訊運作,擁有 CISSP 及 ITIL 認證,能提供日常資訊運作之檢測及審查,以提高操作效率。 本人誠意為各類大中小型廠房提供資訊審計、支援及意見,歡迎聯絡,電郵為 au8788@gmail.com

「ERP資源管理系統」已是現今廠房管理必不可少的工具,提高它的效能,絕對能改善公司之盈利,請多多留意。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

提供香港股票價位歷史數據

我想很多人會對"香港股票價位的歷史數據"有興趣,我已下載成Microsoft Access database version 2000 的文檔,資料由2008/1/1至2009/12/2,zip壓縮後也有11M,若索取請留你的PM我 。

祝願各瀏覽者股壇威威!

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

2014年10月31日

IT人在工廠日記 – 逃婚故事喻香港真普選

國忠是一名狐獨老人,可是績極地支持學生的反普選運動,原因是補償二十年前的錯事。回想二十年前,國忠強迫兒子遵從爺爺安排的一場政治婚姻來幫助家業,提供幾位女子給兒子去迎娶,但是不讓兒子自由戀愛;雖然兒子曾經努力抗爭,但是國忠只顧慮爺爺的身家,按爺爺的意思去強迫兒子,最後兒子離家出走;之後,國忠艱苦經營爺爺留下的家業,因為後繼無人,祖業日漸息微,國忠現時很後悔當日對待兒子的做法。早知今日,悔不當初,唉……
*** 希望現時中國和香港的當權派為下一代的未來,三思三思 !!! ***
10329043_1001086806584917_2429572626423854103_n 10405642_10153179873884156_6718458170399488055_n  10734280_10152901710637448_6886067310481241138_n

2014年10月22日

NodeJS基礎教學&簡介


source:   http://www.slideshare.net/xdxie/nodejs-15251110

  • 1. NodeJS 新一代高效能網站開發框架From:http://blog.xdxie.net 1
  • 2. Outline ! NodeJS簡介 – What & Why ! NodeJS V.S JavaScript V.S JAVA !   NON-BLOCKING & BLOCKING ! NodeJS 總結&具體優點 ! NodeJS 開發環境建置 & 執行 !   Require & Module !   NPM(Node Package Manager) !   DEMO HTTP伺服器建立(基本、延伸、再延伸) !   DEMO Socket.IO聊天室 !   What is your need !   ReferenceFrom:http://blog.xdxie.net 2
  • 3. What is NodeJS ! Node.JS 是一個高效能、易擴充的網站應用程式開發框架 (Web Application Framework) 。它誕生的原因,是為了讓開發 者能夠更容易開發高延展性的網路服務,不需要經過太多 複雜的調校、效能調整及程式修改,就能滿足網路服務在 不同發展階段對效能的要求。 !   Ryan Dahl 是 NodeJS 的催生者,他開發 NodeJS 的目的,就是 希望能解決 Apache 在連線數量過高時,緩衝區 (buffer) 和系 統資源會很快被耗盡的問題,希望能建立一個新的開發框 架以解決這個問題。因此嘗試使用效能十分優秀的 V8 JavaScript Engine ,讓網站開發人員熟悉的 JavaScript 程式 語言,也能應用於後端服務程式的開發,並且具有出色的 執行效能。From:http://blog.xdxie.net 3
  • 4. Why use NodeJS !   以前要寫一個能夠同時容納上百人的上線的網路服務,需要花費 多大的苦工,可能10人多就需要經過一次程式調整,而NodeJS就 是為了解決這個困境, NodeJS 因此誕生,它是一種利用 V8 Javascript 編譯器,所開發的產品,利用V8 編譯器的高效能,與 Javascript 的程式開發特性所產生的網路程式。 !   以前的網路程式原理是將使用者每次的連線(connection)都開啟一個 執行緒(thread),當連線爆增的時候將會快速耗盡系統效能,並且 容易產生阻塞(block)的發生。 ! NodeJS對於資源的調校有所不同,當程式接收到一筆連線 (connection),會通知作業系統將連線保留,並且放入heap中配置, 先讓連線進入休眠(Sleep)狀態,當系統通知時才會觸發連線的 callback。這種處理連線方式只會佔用掉記憶體,並不會使用到CPU 資源。另外因為採用Javascript 語言的特性,每個request都會有一個 callback,如此可以避免發生Block的狀況發生。From:http://blog.xdxie.net 4
  • 5. NodeJS V.S JavaScript V.S JAVA !   JavaScript是網頁的前端語言。早期稱為LiveScript,由Netscape 與Sun合作,受當時的JAVA啟發改名為Javascript,目的之一為 看上去像JAVA,故結構上有部分相似處,但JavaScript與JAVA 並沒有任何關係。 ! NodeJS可以說是JavaScript的後端語言。From:http://blog.xdxie.net 5
  • 6. BLOCKING & NON-BLOCKING 是什麼?From:http://blog.xdxie.net 6
  • 7. BLOCKING // 開始泡泡麵 make_cup_noodles(); // 在等泡麵泡好的時候電話響了, 但是我還是必須等麵泡完了才能 接, 於是我便錯過了這通電話 answer_a_phone_call(); // 這時我突然想尿尿, 但我還是必須等面泡完,於是我便尿褲子了… go_to_toilet(); // 麵好了我穿著滿是尿的褲子吃麵 eat_the_noodles(); // 吃完了我穿著滿是尿的褲子丟掉空碗 throw_the_cup_to_trash_can();From:http://blog.xdxie.net 7
  • 8. NON-BLOCKING make_cup_noodles(function(完成的麵){ //開始泡麵,麵好後做function eat_the_noodles(function(空碗){ //吃泡麵,吃完以後做function throw_the_cup_to_trash_can(); //吃完了可以丟掉空碗 }); 確認事情發生後必須寫在callback內然而 }); 動作一多,程式將不方便閱讀也不好維護 answer_a_phone_call(function(響了的電話){ solution : events模組 //電話響了我才接電話,而不是一直在電話旁邊等電話 }); go_to_toilet(function(想上廁所的我){ //想上廁所的時候才去上廁所,而不是一直在廁所等尿意 }); EVENT-DRIVEN使整支程式可以在事件發生時才做事, 而不是BLOCK在一件事上後面的事情都要等 From:http://blog.xdxie.net 8
  • 9. 所以簡單說 NodeJS到底是什麼?From:http://blog.xdxie.net 9
  • 10. •  JavaScript的後端語言 •  使用V8 JavaScript編譯器所開發的產品 •  NON-BLOCKING (非阻塞特性) •  EVENT-DRIVEN (事件驅動) •  HIGH CONCURRENCY (高連線數量)From:http://blog.xdxie.net 10
  • 11. 那NodeJS的具體優點呢?From:http://blog.xdxie.net 11
  • 12. •  入手容易 •  無論你曾學過PHP、Ruby或是Python多少 對於JavaScript都有一些基本觀念,要入手 NodeJS可以迅速很多,甚至可以說會一種 語言前後端都通吃。 •  高性能低耗能的優秀性能表現 •  非同步I/O •  Event-Driven •  強大的社群支援 •  NPM ! •  Realtime應用的選擇 效 能 •  Socket.IO 高From:http://blog.xdxie.net 12
  • 13. 簡單介紹了NodeJSFrom:http://blog.xdxie.net 13
  • 14. NodeJS開發環境建置 !   官方網站下載安裝 !   http://nodejs.org/ !   於命令視窗執行指令: node –v !   出現版本號表示安裝成功From:http://blog.xdxie.net 14
  • 15. NodeJS執行程式方法 !   目前工作目錄為您.js檔案的位置 !   執行指令:node filename.jsFrom:http://blog.xdxie.net 15
  • 16. Require & Module !   require = load module NodeJS所看到的 var http = require(‘http’); //可以開始使用http的各種方法, 如 http.createServer() 前端Javascript所看到的 <script src=“jquery.js”> //可以開始使用jQueryFrom:http://blog.xdxie.net 16
  • 17. Require好簡單 但是Module從哪來?From:http://blog.xdxie.net 17
  • 18. Node Package Manager ! NodeJS 的套件(package)管理工具 !   開發者可以直接利用線上套件庫,加速軟體專案開發 ! NodeJS 在 0.6.3 版本開始內建 NPM 不用再另外安裝了! !  https://npmjs.org/From:http://blog.xdxie.net 18
  • 19. Node Package Manager ! npm -v //NPM版本訊息 ! npm list //列出目前工作目錄安裝的套件 ! npm install module_name //安裝於目前工作目錄 ! npm install -g module_name //安裝於系統目錄 ! npm update module_name //更新套件From:http://blog.xdxie.net 19
  • 20. Node Package Manager !   使用package.json管理套件版本 EXAMPLE: { "name": "application-name" , "version": "0.0.1" , "private": true , "dependencies": { "express": "2.5.5" , "coffee-script": "latest" , "mongoose": ">= 2.5.3" } }From:http://blog.xdxie.net 20
  • 21. 還想知道什麼? npm helpFrom:http://blog.xdxie.net 21
  • 22. 來點實作的東西吧!From:http://blog.xdxie.net 22
  • 23. DEMO - HTTP伺服器建立 var server, port=2012, http=require(http); server=http.createServer(function(req,res){ res.writeHead(200,{Content-type:text/plain}); res.end(Hello World); console.log(guest visted); }); server.listen(port); 出現兩次訊息, Why? console.log(Server is running); favorite.icoFrom:http://blog.xdxie.net 23
  • 24. DEMO – HTTP伺服器建立 延伸 var server, ….. url=require(url); server=http.createServer(function(req,res){ path=url.parse(req.url); res.writeHead(200,{Content-type:text/plain}); switch(path.pathname){ case /’: res.end(hello world);break; case /index’: res.end(index page);break; default: res.end(path.pathname=+path.pathname);break; } }).listen(port); console.log(Server is running.);From:http://blog.xdxie.net 24
  • 25. 這樣的HTTP伺服器似乎還是不怎麼樣From:http://blog.xdxie.net 25
  • 26. DEMO – HTTP伺服器建立 再延伸 var server, … fs=require(fs); server=http.createServer(function(req,res){ path=url.parse(req.url); fs.readFile(static+path.pathname,utf8,function(err,file){ if(err){ res.writeHead(404,{Content-type:text/plain}); res.end(); return; } res.writeHead(200,{Content-type:text/html}); 純文字輸出 res.end(file); }); }).listen(port); console.log(Server is running.); 以html方式輸出 From:http://blog.xdxie.net 26
  • 27. Socket.IO簡介 !   整合了websocket,long-polling,iframe polling等技術,讓不支援 websocket 的瀏覽器也能夠體驗websocket 的感受 !   實現Server-push,Real-Time服務,典型範例:聊天室 ! NodeJS最常使用的套件之一 !   跨各瀏覽器(包括IE..XD) 的噩夢! 開發 者 I E是From:http://blog.xdxie.net 27
  • 28. DEMO – Socket.IO聊天室 Real-time Server-side核心部分 var sio=io.listen(3333); sio.sockets.on(connection,function(socket){ socket.emit(reqname); socket.on(username,function(data){ console.log(data.username+ enter the room); var username=data.username; chat room}); sio.sockets.emit(msg,{msg:User +username+ enter the }) socket.on(msg,function(data){ sio.sockets.emit(msg,{msg:data.msg}); }); }); 對所有Socket廣播,包括自己 比較: socket.broadcast.emit 不包括自己From:http://blog.xdxie.net 28
  • 29. DEMO – Socket.IO聊天室 Real-time Client核心部分 <script src="http://localhost:3333/socket.io/socket.io.js"></script> var name; var socket=io.connect(http://localhost:3333); socket.on(reqname,function(data){ name=window.prompt("whats your name?"); socket.emit(username,{username:name}); }) socket.on(msg,function(data){ console.log(data); document.getElementById(chatbox).innerHTML+=<br />+data.msg; }); function sendmsg(){ var msg=document.getElementById(msg).value; document.getElementById(msg).value=; socket.emit(msg,{msg:name+ says +msg}); }From:http://blog.xdxie.net 29
  • 30. 講到這裡 你覺得NodeJS怎麼樣?From:http://blog.xdxie.net 30
  • 31. From:http://blog.xdxie.net 31
  • 32. 如果你對NodeJS有興趣 你還需要什麼?From:http://blog.xdxie.net 32
  • 33. What is your need ! Javascript !   如果未曾寫過javascript建議先從javascript學起 ! NodeJS 電子書 !   完整的教學 ! NodeJS API !   從官方API手冊了解可以使用的API,知道的越多對開發越有幫助 !   express ! NodeJS的Web開發框架,使開發http service變得更容易From:http://blog.xdxie.net 33
  • 34. 其實你最需要的是創意From:http://blog.xdxie.net 34
  • 35. 還有Google 身為一個開發者如果不會Google那跟鹹魚有什麼分別From:http://blog.xdxie.net 35
  • 36. Reference ! NodeJS官方網站 !   http://nodejs.org/ ! NodeJS API !   http://nodejs.org/docs/latest/api/ !   NODEJS台灣社群電子書 !   http://book.nodejs.tw/ ! Dreamerslab !   http://dreamerslab.com/ ! Socket.IO !   http://socket.io/! ! XDXie’s Blog !   http://blog.xdxie.net/ 我的Blog,或許可以來這邊挖筆記XD From:http://blog.xdxie.net 36
  • 37. Thanks for your attention.From:http://blog.xdxie.net 37

2014年10月20日

Exchange 2010 error 550 5.7.1 Client does not have permissions to send as this

錯誤550 5.7.1 Client does not have permissions to send as this引用回覆 編輯/刪除文章 搜尋由  發表的其他文章 回報給版主 IP 位置 回此頁最上方
透過建好的“接收連接器”來發送郵件時,卻出現了550 5.7.1的錯誤,
使用者沒有權限寄送郵件,這該如何處理呢?




Exchange 2010“接收連接器”的權限問題可利用 ADSIedit.msc (ADSI編輯器)來修正。

開啟ADSI編輯器,連線到AD主機的設定(Configuration)項目,
然後展到到以下路徑的SMTP Receive Connectors。
CN=Configuration\CN=Services\CN=Microsoft Exchange\CN=<Organization>
\CN=Administrative Groups\CN=Exchange Administrative Group (FYDIBOHF23SPDLT)
\CN=Servers\CN=<Server Name>\CN=Protocols\CN=SMTP Receive Connectors

展開SMTP Receive Connectors後,點選權限有問題的接收連接器,
例如本例的Shunze_test,來修改權限。



於權限有問題的接收連接器中,修改Authenticated Uses (已認證的使用者)權限,
增加Accept any Sender,Accept Authenticated Flag及Accept Authoritative Domain Sender這三個權限給Authenticated Uses。



回到接收連接器設定,檢查一下驗證及權限群組該勾的是否已勾選。





確定無誤後,再次發信測試,
你會發現原本出現的權限問題550 5.7.1 Client does not have permissions to send as this sender,
經過權限的適當修正後,已經順利排除了~

2014年10月17日

BYOD,帶自己的行動裝置來上班!

info source: http://www.managertoday.com.tw/?p=12359
由員工自攜行動裝置投入工作,公司可節省採購成本,並享受整體工作產能的提升。但另一方面,企業也要及早制定個人行動裝置的身分識別、網路存取等級和行動應用系統的管理措施,才能真正實現安全且高效率的虛擬企業。
各位企業管理人是否發現,這一兩年辦公室有種行動科技的革命悄悄在發生?越來越多同事把智慧型手機、平板電腦帶到工作場所,有些還連接到公司網路取用資料、處理公務。
這股風潮正在歐美大行其道,受到企業管理大師的討論和鼓吹,衍生出「帶自己的行動裝置來上班(Bring Your Own Device, BYOD)」的新名詞。甚至還因為有助於增進工作生產力,搭上雲端服務的列車,帶動行動裝置管理的配套措施和管理方案的發展。
BYOD現象對企業來說就像是兩面刃。由員工自攜行動裝置投入工作,好處是公司節省了採購硬體成本,整體工作產能也能因為行動效率而有提升。但另一方面,這些裝置開啟資安大門,讓企業的敏感資料得面對前所未見的使用風險。
不過也別因為恐慌而一昧圍堵。行動裝置本身不是問題,IT需要關注的是行動裝置取用企業資料的過程,尤其是裝置的身分識別、網路存取等級和行動應用系統的管理措施,越早部署、效果越好。
現在越來越多行動管理軟體能夠部署在既有的有線、無線網路或VPN環境,企業並不需要更動或改變現有資訊架構,就能把行動裝置納入管理,以享用正面的效益。而且這些行動裝置的使用政策管理和設備安全性檢查,完全自動化,IT人員並不用增加負擔,就能輕鬆控管。

支援員工行動裝置 實現虛擬企業

這種個人行動裝置大量湧入工作場所的風潮,也興起另一個新名詞「IT支援員工個人化(Consumerization of IT)」。這是指當企業組織默許或鼓勵個人科技用品演變成商業行為,為保護與推進這些科技產品而產生的新一代需求。這些需求可以概分為三類:效率的要求(隨時隨地處理緊急公務)、追求效益的企業管理層的需要,以及有影響力人員的期望(比如高階主管運用iPad移動辦公)。
BYOD的風行還可望實現虛擬企業的目標。傳統上,員工得到辦公室才能取用公司資源或資料,若是要在任何地方取用企業資源,也得使用公司配發或管理的科技裝置。但在雲端服務年代,員工渴望使用自己的科技工具,在任何地方、使用任何服務以取用資源,甚至有機會來到虛擬企業的境地──讓工作方式完全不受限地點、服務和裝置。
在這個風潮下,企業IT部門必須加快腳步研究行動應用這門基本功。假使企業能夠洞察雲端運算與員工IT消費者化之間連動趨勢,找出正確的方向與做法,就能建構優質的行動應用環境,從容應付雲端與IT消費者化世代的各項挑戰。
企業決策人看BYOD
  • 2011年中,不允許員工使用自有裝置的比例將近七成,但未來一年後,不允許的比例將降到37%,鼓勵員工BYOD的比例甚至從24%升高到46%。
  • 最被企業接受的行動裝置是智慧型手機,其次為平板電腦。
  • 有31%的企業認為在BYOD政策引進後,會降低硬體支出及相關支援人力;9%企業認為會降低硬體支出;5%企業認為會降低相關支援人力。

IP-guard功能介紹

IP-guard功能介紹
模組
子功能
功能詳細介紹
基本管理
基本資訊
統計用戶端電腦基本資訊,包括電腦名稱,網路位址,作業系統,登錄用戶,目前狀態等資訊。
基本控制
能夠在控制端對網內任意用戶端電腦進行鎖定、關閉、重新啟動、登出和發送通知資訊等。
基本日誌
對用戶端電腦的開機/關機,使用者登入/登出,撥號等的事件紀錄。
基本策略
設定用戶端電腦的本機系統的操作權限,包括控制台,電腦管理,系統管理,網路內容,元件管理等所有電腦內容。並能夠將用戶端電腦的IP與MAC地址進行鎖定。
基本策略
設定當用戶端電腦系統狀態變化時警報,包括硬體異動,儲存設備和通訊設備新增與移除,軟體安裝移除,系統資訊和網路配置變化等。
策略日誌
紀錄用戶端電腦在執行策略時的操作日誌,如禁止、警報、警告和鎖定電腦等。
應用程式管控
應用程式日誌
詳細記錄應用程式啟動、退出;紀錄視窗切換和標題變化,並可以按時間範圍,電腦範圍,應用程式名稱,應用程式路徑、視窗標題等多種查詢條件進行查詢。
應用程式統計
多種方式統計各種應用程式的使用時間百分比,並以列表和圖表兩種方式顯示。統計方式包括應用程式類別、名稱、明細統計以及按電腦分項統計。
應用程式控制
可在指定時間內限制指定電腦對指定程式的應用,並可在受限程式執行時向控制台發送警報。
網頁瀏覽管控
網頁瀏覽日誌
詳細記錄每台電腦(使用者)瀏覽網頁的網址和標題,並提供查詢功能
網頁瀏覽統計
多種方式統計網頁瀏覽情況,以列表和圖表兩種方式顯示統計結果。統計方式包括按網站類別、明細統計以及按電腦分項統計。
網站瀏覽控制
對指定的用戶端在指定的時間範圍內瀏覽指定的網站或網址進行管控。
網路流量管控
網路流量統計
統計用戶端在指定時間範圍內的網路通訊流量,包括通訊總流量和每種網路協定、位址的詳細流量。統計方式包括按位址、協定和連接埠的類別或明細統計以及依電腦分項統計。
網路流量控制
透過設定指定時間範圍,網路位址和連接埠範圍、發送和接收方向來限制電腦流量速度,保障網路頻寬資源的合理使用。
檔案操作管控
檔案操作日誌
紀錄本機上所有檔案操作資訊,包括在硬碟、移動存取、網路路徑、共享目錄上所有檔案的建立、讀取、修改、複製、移動、刪除、復原、重新命名等操作;並可按日誌紀錄的資訊進行查詢。
檔案操作控制
控制用戶端電腦在指定的磁碟類型或者網路上對指定的文件的讀取,修改和刪除操作的權限。
文件備份
為防止重要文件被誤刪或者簒改,可以在文件內容被改變或破壞前進行備份。
文件列印管控
列印操作日誌
詳細記錄所有列印操作的時間、電腦、使用者、應用程式、頁數、印表機類型和名稱,並能夠根據時間、檔案名稱、電腦等資訊進行查詢。
列印內容紀錄
完整記錄在所有類型印表機上的檔案列印影像,查看列印的原始內容。
列印控制
控制應用程式列印權限,阻止非法應用程式列印;控制電腦列印權限,限制電腦對指定類型印表機或指定印表機的使用。
螢幕監控
即時螢幕快照
即時查看用戶端的螢幕快照,支持對同時多個使用者登入的監控,支援對多顯示器的監控,可同時對一組電腦進行集中監控。
螢幕歷史紀錄
紀錄用戶端的歷史螢幕畫面,根據使用者不同操作方法,指定紀錄功能;配合日誌紀錄檢視當時的螢幕情況;支援將螢幕歷史轉存為通用錄影文件,被其他常用工具播放。
遠端維護
遠端維護
即時查看用戶端的執行資訊,遠端分析用戶端執行狀況和故障原因,並可以執行遠端操作,幫助解決遠端電腦使用問題。
遠端控制
遠端連接到用戶端電腦的桌面,直接操作用戶端,方便進行遠端協助或操作示範
遠端文件傳送
遠端打開指定用戶端的文件夾,傳送文件和蒐集故障樣本。
設備管控
設備控制
控制用戶端各種設備類型的使用權限:
儲存設備:軟碟機、光碟機、燒錄機、磁碟機、移動儲存設備等;
通訊設備:序列埠/並列埠、SCSI、1394、藍芽、紅外線、MODEM、 直線纜線傳輸等;
USB設備:USB鍵盤、滑鼠、MODEM、MO/ZIP、儲存、光碟機、硬碟和其他USB裝備;
網路設備:無線網卡、隨插即用網卡、虛擬網卡、PnP網卡等;
其他設備:聲音設備、虛擬光碟機;
可以禁止任何新增加的設備。
網路控制
網路通訊控制
透過對網路通訊方向、IP地址範圍、網路連接埠範圍的設定,管理電腦使用網路的權限;
控制指定用戶端的網路通訊;
控制指定協定和位址範圍的網路通訊;
控制與外來電腦的網路通訊。
入侵檢測
檢測網路內是否有非法電腦接入並發出警報,同時阻止非法電腦接入網路。
郵件管控
郵件日誌
紀錄標準協定(POP3、SMTP)郵件、Exchange郵件收發的收件人、發件人、內文及完整附件;紀錄網頁郵件,Lotus郵件發送的收件人、發件人、內文及完整附件。
郵件控制
透過郵件的寄件者、收件者、主旨、附件及郵件大小等條件限制,控制發送郵件的帳戶,阻止向不被允許的收件人發送郵件,阻止發送特定名稱或者超出規定大小的附件。
即時通訊管控
即時通訊日誌
完整紀錄MSN、QQ、TM、RTX、ICQ、Yahoo、Sina UC、PoPo、Skype、Lotus Sametime、阿里巴巴等主流即時通訊工具的對話時間、對話人、對話內容等。
即時通訊
傳輸文件控制
透過文件名稱和大小條件的設定,控制透過即時通訊工具向外來發送文件,並可以在傳輸的過程中備份文件。
資產管理
資產管理
自動掃描並完整記錄每台電腦軟硬體資產資訊,詳細記錄資產變更資訊,可自定資產內容進行輔助資訊管理;
通過自定資產類別對非IT資產進行管理。
系統更新管理
自動掃描用戶端的微軟產品更新安裝情況;
根據策略下載指定的更新並進行自動分發和安裝。
安全漏洞檢查
自動掃描用戶端的安全漏洞的情況,並提供分析報告和解決方案。
軟體派送
自動部屬和安裝軟體,執行程式或者派送文件,支援斷點續傳,支援後台安裝和傳統安裝。
移動儲存管理
移動儲存授權
管控移動儲存的使用,只有經過授權的移動儲存設備才可在用戶端電腦上使用。
移動儲存加密
對指定移動儲存設備上存取的文件自動進行透明加解密,其他未經授權的用戶端或外部的電腦無法讀取該移動儲存設備中的加密文件。

2014年10月9日

雲端上的POS系統

雲端軟件越來越流行,以下一款POS似乎也不錯。

一個雲端POS為你帶來什麼?

系統化管理店內商品

用「心算」計每一單交易的金額是每位老闆的專長,但這樣沒系統地記錄交易,存貨和商品銷售量也只能「估估下」。在POS的幫助下,存貨和銷售量均被精準地記錄下來,不用再「估估下」。而且POS能即時為顧客打印收據,令顧客對你的商店更有信心!

防止員工出錯或作弊

在沒有POS的情況下,員工可能會在收錢時出錯或自取店內商品使用。 POS可精確記錄交易及存貨, 減少員工收錢時出錯或自取店內商品使用的機會。

用數據了解商店經營狀況

在大數據時代,數據收集變成每間商店的必須動作。ShopPOS幫助商店收集日常交易數據、商品出入記錄,並以圖表清晰顯示銷售及存貨情況,幫助商店調整營運及宣傳策略,達至存貨最佳化及利潤極大化。

雲端上的POS系統 - 隨時隨地連接ShopPOS

無論在店中、在家中或在街上,只要有能連接上網的電腦或平板電腦,即可隨時隨地登入POS進行操作!

立即體驗ShopPOS!


專為小商店而設

以往POS的使用成本較高,因為需要一套專用的硬件作為配套,一套POS起碼需要2萬以上。故「一在線」研發了雲端POS系統 — ShopPOS,讓小商店能用普通電腦便能使用POS,不用再購買大量專用硬件。而且ShopPOS是一個極了廉價的方案,絕對適合中小企使用。

簡單易用的操作介面

ShopPOS由用戶體驗專家設計,並完全支援輕觸式摒幕,簡單易用,並附有中、英文語言介面。小商店從此不用再花時間研究怎樣使用POS了!

重點功能

真實案例

大埔蜂蜜糖果屋:
「現在我更了解我的生意!」


使用了ShopPOS後,記錄了每單生意的數據,每日生意額和成本一目了然,不用像以前每日「埋數」時估估下。ShopPOS內的圖表分析令我了解到原來一些看似毛利低的糖果,竟是帶來最多利潤的商品!需端的設計也帶來很大的方便,現在我在家中也能輕鬆地管理存貨及「埋數」,不必半夜還留在店內。

2個架設POS方案

我們提供了2個架設POS方案,以滿足不同經營者的需求!
軟件 + 安裝套餐
$5,800
低成本開始使用專業級POS系統
  • 購買即送5項禮品
  • 所有系統功能
  • 上門安裝服務
  • 90分鐘教學、24/7支援
  • 5個電子郵件
  • 50GB雲端儲存容量

最人性化的網上商店設計服務

市面上大多數的網上商店系統質素參差,而且大部分十分難用。因此,「一在線」所開發的網上商店系統 — ShoppingOne以最人性化的使用體驗為目標,務求為中小企提供一個功能最齊全,使用簡單的網上商店系統!

了解更多

Node.js 入門

資料來源:  http://www.nodebeginner.org/index-zh-tw.html

JavaScript與Node.js

JavaScript與你

拋開技術,我們先來聊聊你以及你和JavaScript的關系。本章的主要目的是想讓你看看,對你而言是否有必要繼續閱讀後續章節的內容。
如果你和我一樣,那麼你很早就開始利用HTML進行 "開發" ,正因如此,你接觸到了這個叫JavaScript有趣的東西,而對於JavaScript,你只會基本的操作——為web頁面增加互動。
而你真正想要的是 "實用的東西" ,你想要知道如何建構復雜的web站點 —— 於是,你學習了一種諸如PHP、Ruby、Java這樣的程式語言,並開始書寫 "後端" 代碼。
與此同時,你還始終關注著JavaScript,隨著透過一些對jQuery,Prototype之類技術的介紹,你慢慢了解到了很多JavaScript中的進階技能,同時也感受到了JavaScript絕非僅僅是window.open() 那麼簡單。 .
不過,這些畢竟都是前端技術,盡管當想要增強頁面的時候,使用jQuery總讓你覺得很爽,但到最後,你頂多是個JavaScript用戶,而非JavaScript開發者
然後,出現了Node.js,伺服器端的JavaScript,這有多酷啊?
於是,你覺得是時候該重新拾起既熟悉又陌生的JavaScript了。但是別急,寫Node.js應用是一件事情;理解為什麼它們要以它們書寫的這種方式來書寫則意味著——你要懂JavaScript。這次是玩真的了。
問題來了: 由於JavaScript真正意義上以兩種,甚至可以說是三種形態存在(從中世紀90年代的作為對DHTML進行增強的小玩具,到像jQuery那樣嚴格意義上的前端技術,一直到現在的伺服器端技術),因此,很難找到一個 "正確" 的方式來學習JavaScript,使得讓你書寫Node.js應用的時候感覺自己是在真正開發它而不僅僅是使用它。
因為這就是關鍵: 你本身已經是個有經驗的開發者,你不想透過到處尋找各種解決方案(其中可能還有不正確的)來學習新的技術,你要確保自己是透過正確的方式來學習這項技術。
當然了,外面不乏很優秀的學習JavaScript的文章。但是,有的時候光靠那些文章是遠遠不夠的。你需要的是指導。
本書的目標就是給你提供指導。

簡短申明

業界有非常優秀的JavaScript程式設計師。而我並非其中一員。
我就是上一節中描述的那個我。我熟悉如何開發後端web應用,但是對 "真正" 的JavaScript以及Node.js,我都只是新手。我也只是最近學習了一些JavaScript的進階概念,並沒有實踐經驗。
因此,本書並不是一本 "從入門到精通" 的書,更像是一本 "從初級入門到進階入門" 的書。
如果成功的話,那麼本書就是我當初開始學習Node.js最希望擁有的教學課程。

伺服器端JavaScript

JavaScript最早是運行在瀏覽器中,然而瀏覽器只是提供了一個上下文,它定義了使用JavaScript可以做什麼,但並沒有 "說" 太多關於JavaScript語言本身可以做什麼。事實上,JavaScript是一門 "完整" 的語言: 它可以使用在不同的上下文中,其能力與其他同類語言相比有過之而無不及。
Node.js事實上就是另外一種上下文,它允許在後端(脫離瀏覽器環境)運行JavaScript代碼。
要實現在後台運行JavaScript代碼,代碼需要先被解釋然後正確的執行。Node.js的原理正是如此,它使用了Google的V8虛擬機(Google的Chrome瀏覽器使用的JavaScript執行環境),來解釋和執行JavaScript代碼。
除此之外,伴隨著Node.js的還有許多有用的模組,它們可以簡化很多重復的勞作,比如向終端輸出字串。
因此,Node.js事實上既是一個運行時環境,同時又是一個函式庫。
要使用Node.js,首先需要進行安裝。關於如何安裝Node.js,這裡就不贅述了,可以直接參考官方的安裝指南。安裝完成後,繼續回來閱讀本書下面的內容。

"Hello World"

好了, "廢話" 不多說了,馬上開始我們第一個Node.js應用: "Hello World" 。
打開你最喜歡的編輯器,建立一個helloworld.js檔案。我們要做就是向STDOUT輸出 "Hello World" ,如下是實現該功能的代碼:
console.log("Hello World");
儲存該檔案,並透過Node.js來執行:
node helloworld.js
正常的話,就會在終端輸出Hello World 。
好吧,我承認這個應用是有點無趣,那麼下面我們就來點 "實用的東西" 。

一個完整的基於Node.js的web應用

使用案例

我們來把目標設定得簡單點,不過也要夠實際才行:
  • 用戶可以透過瀏覽器使用我們的應用。
  • 當用戶請求http://domain/start時,可以看到一個歡迎頁面,頁面上有一個檔案上傳的表單。
  • 用戶可以選擇一個圖片並送出表單,隨後檔案將被上傳到http://domain/upload,該頁面完成上傳後會把圖片顯示在頁面上。
差不多了,你現在也可以去Google一下,找點東西亂搞一下來完成功能。但是我們現在先不做這個。
更進一步地說,在完成這一目標的過程中,我們不僅僅需要基礎的代碼而不管代碼是否優雅。我們還要對此進行抽象,來尋找一種適合建構更為復雜的Node.js應用的方式。

應用不同模組分析

我們來分解一下這個應用,為了實現上文的使用案例,我們需要實現哪些部分呢?
  • 我們需要提供Web頁面,因此需要一個HTTP伺服器
  • 對於不同的請求,根據請求的URL,我們的伺服器需要給予不同的回應,因此我們需要一個路由,用於把請求對應到請求處理程序(request handler)
  • 當請求被伺服器接收並透過路由傳遞之後,需要可以對其進行處理,因此我們需要最終的請求處理程序
  • 路由還應該能處理POST資料,並且把資料封裝成更友好的格式傳遞給請求處理入程序,因此需要請求資料處理功能
  • 我們不僅僅要處理URL對應的請求,還要把內容顯示出來,這意味著我們需要一些視圖邏輯供請求處理程序使用,以便將內容發送給用戶的瀏覽器
  • 最後,用戶需要上傳圖片,所以我們需要上傳處理功能來處理這方面的細節
我們先來想想,使用PHP的話我們會怎麼建構這個結構。一般來說我們會用一個Apache HTTP伺服器並配上mod_php5模組。
從這個角度看,整個 "接收HTTP請求並提供Web頁面" 的需求根本不需要PHP來處理。
不過對Node.js來說,概念完全不一樣了。使用Node.js時,我們不僅僅在實現一個應用,同時還實現了整個HTTP伺服器。事實上,我們的Web應用以及對應的Web伺服器基本上是一樣的。
聽起來好像有一大堆活要做,但隨後我們會逐漸意識到,對Node.js來說這並不是什麼麻煩的事。
現在我們就來開始實現之路,先從第一個部分--HTTP伺服器著手。

建構應用的模組

一個基礎的HTTP伺服器

當我準備開始寫我的第一個 "真正的" Node.js應用的時候,我不但不知道怎麼寫Node.js代碼,也不知道怎麼組織這些代碼。
我應該把所有東西都放進一個檔案裡嗎?網上有很多教學課程都會教你把所有的邏輯都放進一個用Node.js寫的基礎HTTP伺服器裡。但是如果我想加入更多的內容,同時還想保持代碼的可讀性呢?
實際上,只要把不同功能的代碼放入不同的模組中,保持代碼分離還是相當簡單的。
這種方法允許你擁有一個乾淨的主檔案(main file),你可以用Node.js執行它;同時你可以擁有乾淨的模組,它們可以被主檔案和其他的模組執行。
那麼,現在我們來建立一個用於啟動我們的應用的主檔案,和一個儲存著我們的HTTP伺服器代碼的模組。
在我的印象裡,把主檔案叫做index.js或多或少是個標準格式。把伺服器模組放進叫server.js的檔案裡則很好理解。
讓我們先從伺服器模組開始。在你的項目的根目錄下建立一個叫server.js的檔案,並寫入以下代碼:
var http = require("http");

http.createServer(function(request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("Hello World");
  response.end();
}).listen(8888);
搞定!你剛剛完成了一個可以工作的HTTP伺服器。為了證明這一點,我們來運行並且測試這段代碼。首先,用Node.js執行你的腳本:
node server.js
接下來,打開瀏覽器存取http://localhost:8888/,你會看到一個寫著 "Hello World" 的網頁。
這很有趣,不是嗎?讓我們先來談談HTTP伺服器的問題,把如何組織項目的事情先放一邊吧,你覺得如何?我保證之後我們會解決那個問題的。

分析HTTP伺服器

那麼接下來,讓我們分析一下這個HTTP伺服器的構成。
第一行請求(require)Node.js自帶的 http 模組,並且把它賦值給 http 變數。
接下來我們執行http模組提供的函數: createServer 。這個函數會回傳一個物件,這個物件有一個叫做 listen 的方法,這個方法有一個數值參數,指定這個HTTP伺服器監聽的埠號號。
咱們暫時先不管 http.createServer 的括號裡的那個函數定義。
我們本來可以用這樣的代碼來啟動伺服器並偵聽8888埠號:
var http = require("http");
var server = http.createServer();
server.listen(8888);
這段代碼只會啟動一個偵聽8888埠號的伺服器,它不做任何別的事情,甚至連請求都不會應答。
最有趣(而且,如果你之前習慣使用一個更加保守的語言,比如PHP,它還很奇怪)的部分是 createSever() 的第一個參數,一個函數定義。
實際上,這個函數定義是 createServer() 的第一個也是唯一一個參數。因為在JavaScript中,函數和其他變數一樣都是可以被傳遞的。

進行函數傳遞

舉例來說,你可以這樣做:
function say(word) {
  console.log(word);
}
function execute(someFunction, value) {
  someFunction(value);
}

execute(say, "Hello");
請仔細閱讀這段代碼!在這裡,我們把 say 函數作為execute函數的第一個變數進行了傳遞。這裡回傳的不是 say 的回傳值,而是 say 本身!
這樣一來, say 就變成了execute 中的區域變數 someFunction ,execute可以透過執行 someFunction() (帶括號的形式)來使用 say 函數。
當然,因為 say 有一個變數, execute 在執行 someFunction 時可以傳遞這樣一個變數。
我們可以,就像剛才那樣,用它的名字把一個函數作為變數傳遞。但是我們不一定要繞這個 "先定義,再傳遞" 的圈子,我們可以直接在另一個函數的括號中定義和傳遞這個函數:
function execute(someFunction, value) {
  someFunction(value);
}

execute(function(word){ console.log(word) }, "Hello");
我們在 execute 接受第一個參數的地方直接定義了我們準備傳遞給 execute 的函數。
用這種方式,我們甚至不用給這個函數起名字,這也是為什麼它被叫做 匿名函數
這是我們和我所認為的 "進階" JavaScript的第一次親密接觸,不過我們還是得循序漸進。現在,我們先接受這一點:在JavaScript中,一個函數可以作為另一個函數接收一個參數。我們可以先定義一個函數,然後傳遞,也可以在傳遞參數的地方直接定義函數。

函數傳遞是如何讓HTTP伺服器工作的

帶著這些知識,我們再來看看我們簡約而不簡單的HTTP伺服器:
var http = require("http");

http.createServer(function(request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("Hello World");
  response.end();
}).listen(8888);
現在它看上去應該清晰了很多:我們向 createServer 函數傳遞了一個匿名函數。
用這樣的代碼也可以達到同樣的目的:
var http = require("http");
function onRequest(request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("Hello World");
  response.end();
}

http.createServer(onRequest).listen(8888);
也許現在我們該問這個問題了:我們為什麼要用這種方式呢?

基於事件驅動的回呼(callback)

這個問題可不好回答(至少對我來說),不過這是Node.js原生的工作方式。它是事件驅動的,這也是它為什麼這麼快的原因。
你也許會想花點時間讀一下Felix Geisendörfer的大作Understanding node.js,它介紹了一些背景知識。
這一切都歸結於 "Node.js是事件驅動的" 這一事實。好吧,其實我也不是特別確切的了解這句話的意思。不過我會試著解釋,為什麼它對我們用Node.js寫網絡應用(Web based application)是有意義的。
當我們使用 http.createServer 方法的時候,我們當然不只是想要一個偵聽某個埠號的伺服器,我們還想要它在伺服器收到一個HTTP請求的時候做點什麼。
問題是,這是非同步的:請求任何時候都可能到達,但是我們的伺服器卻跑在一個單程序中。
寫PHP應用的時候,我們一點也不為此擔心:任何時候當有請求進入的時候,網頁伺服器(通常是Apache)就為這一請求新建一個程序,並且開始從頭到尾執行相應的PHP腳本。
那麼在我們的Node.js程序中,當一個新的請求到達8888埠號的時候,我們怎麼控制流程呢?
嗯,這就是Node.js/JavaScript的事件驅動設計能夠真正幫上忙的地方了——雖然我們還得學一些新概念才能掌握它。讓我們來看看這些概念是怎麼應用在我們的伺服器程式碼裡的。
我們建立了伺服器,並且向建立它的方法傳遞了一個函數。無論何時我們的伺服器收到一個請求,這個函數就會被執行。
我們不知道這件事情什麼時候會發生,但是我們現在有了一個處理請求的地方:它就是我們傳遞過去的那個函數。至於它是被預先定義的函數還是匿名函數,就無關緊要了。
這個就是傳說中的 回呼(callback) 。我們給某個方法傳遞了一個函數,這個方法在有相應事件發生時執行這個函數來進行 回呼(callback) 。
至少對我來說,需要一些功夫才能弄懂它。你如果還是不太確定的話就再去讀讀Felix的部落格文章。
讓我們再來琢磨琢磨這個新概念。我們怎麼證明,在建立完伺服器之後,即使沒有HTTP請求進來、我們的回呼(callback)函數也沒有被執行的情況下,我們的代碼還繼續有效呢?我們試試這個:
var http = require("http");
function onRequest(request, response) {
  console.log("Request received.");
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("Hello World");
  response.end();
}

http.createServer(onRequest).listen(8888);

console.log("Server has started.");
注意:在 onRequest (我們的回呼(callback)函數)觸發的地方,我用 console.log輸出了一段文字。在HTTP伺服器開始工作之後,也輸出一段文字。
當我們與往常一樣,運行它node server.js時,它會馬上在命令行上輸出 "Server has started." 。當我們向伺服器發出請求(在瀏覽器存取http://localhost:8888/), "Request received." 這條消息就會在命令行中出現。
這就是事件驅動的非同步伺服器端JavaScript和它的回呼(callback)啦!
(請注意,當我們在伺服器存取網頁時,我們的伺服器可能會輸出兩次 "Request received." 。那是因為大部分伺服器都會在你存取 http://localhost:8888 /時嘗試讀取 http://localhost:8888/favicon.ico )

伺服器是如何處理請求的

好的,接下來我們簡單分析一下我們伺服器代碼中剩下的部分,也就是我們的回呼(callback)函數 onRequest() 的主體部分。
當回呼(callback)啟動,我們的 onRequest() 函數被觸發的時候,有兩個參數被傳入: request 和 response 。
它們是物件,你可以使用它們的方法來處理HTTP請求的細節,並且回應請求(比如向發出請求的瀏覽器發回一些東西)。
所以我們的代碼就是:當收到請求時,使用 response.writeHead() 函數發送一個HTTP狀態200和HTTP頭的內容類型(content-type),使用 response.write() 函數在HTTP相應主體中發送文字 "Hello World"。
最後,我們執行 response.end() 完成回應。
目前來說,我們對請求的細節並不在意,所以我們沒有使用 request 物件。

伺服器端的模組放在哪裡

OK,就像我保證過的那樣,我們現在可以回到我們如何組織應用這個問題上了。我們現在在 server.js 檔案中有一個非常基礎的HTTP伺服器代碼,而且我提到通常我們會有一個叫 index.js 的檔案去執行應用的其他模組(比如 server.js 中的HTTP伺服器模組)來引導和啟動應用。
我們現在就來談談怎麼把server.js變成一個真正的Node.js模組,使它可以被我們(還沒動工)的 index.js 主檔案使用。
也許你已經注意到,我們已經在代碼中使用了模組了。像這樣:
var http = require("http");
...

http.createServer(...);
Node.js中自帶了一個叫做 "http" 的模組,我們在我們的代碼中請求它並把回傳值賦給一個區域變數。
這把我們的區域變數變成了一個擁有所有 http 模組所提供的公共方法的物件。
給這種區域變數起一個和模組名稱一樣的名字是一種慣例,但是你也可以按照自己的喜好來:
var foo = require("http");
...

foo.createServer(...);
很好,怎麼使用Node.js內部模組已經很清楚了。我們怎麼建立自己的模組,又怎麼使用它呢?
等我們把 server.js 變成一個真正的模組,你就能搞明白了。
事實上,我們不用做太多的修改。把某段代碼變成模組意味著我們需要把我們希望提供其功能的部分 匯出 到請求這個模組的腳本。
目前,我們的HTTP伺服器需要匯出的功能非常簡單,因為請求伺服器模組的腳本僅僅是需要啟動伺服器而已。
我們把我們的伺服器腳本放到一個叫做 start 的函數裡,然後我們會匯出這個函數。
var http = require("http");
function start() {
  function onRequest(request, response) {
    console.log("Request received.");
    response.writeHead(200, {"Content-Type": "text/plain"});
    response.write("Hello World");
    response.end();
  }

  http.createServer(onRequest).listen(8888);
  console.log("Server has started.");
}

exports.start = start;
這樣,我們現在就可以建立我們的主檔案 index.js 並在其中啟動我們的HTTP了,雖然伺服器的代碼還在 server.js 中。
建立 index.js 檔案並寫入以下內容:
var server = require("./server");

server.start();
正如你所看到的,我們可以像使用任何其他的內置模組一樣使用server模組:請求這個檔案並把它指向一個變數,其中已匯出的函數就可以被我們使用了。
好了。我們現在就可以從我們的主要腳本啟動我們的的應用了,而它還是老樣子:
node index.js
非常好,我們現在可以把我們的應用的不同部分放入不同的檔案裡,並且透過建置模組的方式把它們連接到一起了。
我們仍然只擁有整個應用的最初部分:我們可以接收HTTP請求。但是我們得做點什麼——對於不同的URL請求,伺服器應該有不同的反應。
對於一個非常簡單的應用來說,你可以直接在回呼(callback)函數 onRequest() 中做這件事情。不過就像我說過的,我們應該加入一些抽象的元素,讓我們的例子變得更有趣一點兒。
處理不同的HTTP請求在我們的代碼中是一個不同的部分,叫做 "路由選擇" ——那麼,我們接下來就創造一個叫做 路由 的模組吧。

如何來進行請求的 "路由"

我們要為路由提供請求的URL和其他需要的GET及POST參數,隨後路由需要根據這些資料來執行相應的代碼(這裡 "代碼" 對應整個應用的第三部分:一系列在接收到請求時真正工作的處理程序)。
因此,我們需要查看HTTP請求,從中提取出請求的URL以及GET/POST參數。這一功能應當屬於路由還是伺服器(甚至作為一個模組自身的功能)確實值得探討,但這裡暫定其為我們的HTTP伺服器的功能。
我們需要的所有資料都會包含在request物件中,該物件作為onRequest()回呼(callback)函數的第一個參數傳遞。但是為了解析這些資料,我們需要額外的Node.JS模組,它們分別是urlquerystring模組。
                               url.parse(string).query
                                           |
           url.parse(string).pathname      |
                       |                   |
                       |                   |
                     ------ -------------------
http://localhost:8888/start?foo=bar&hello=world
                                ---       -----
                                 |          |
                                 |          |
              querystring(string)["foo"]    |
                                            |
                         querystring(string)["hello"]
當然我們也可以用querystring模組來解析POST請求體中的參數,稍後會有示範。
現在我們來給onRequest()函數加上一些邏輯,用來找出瀏覽器請求的URL路徑:
var http = require("http");
var url = require("url");
function start() {
  function onRequest(request, response) {
    var pathname = url.parse(request.url).pathname;
    console.log("Request for " + pathname + " received.");
    response.writeHead(200, {"Content-Type": "text/plain"});
    response.write("Hello World");
    response.end();
  }

  http.createServer(onRequest).listen(8888);
  console.log("Server has started.");
}

exports.start = start;
好了,我們的應用現在可以透過請求的URL路徑來區別不同請求了--這使我們得以使用路由(還未完成)來將請求以URL路徑為基準映射到處理程序上。
在我們所要建構的應用中,這意味著來自/start/upload的請求可以使用不同的代碼來處理。稍後我們將看到這些內容是如何整合到一起的。
現在我們可以來編寫路由了,建立一個名為router.js的檔案,增加以下內容:
function route(pathname) {
  console.log("About to route a request for " + pathname);
}

exports.route = route;
如你所見,這段代碼什麼也沒幹,不過對於現在來說這是應該的。在增加更多的邏輯以前,我們先來看看如何把路由和伺服器整合起來。
我們的伺服器應當知道路由的存在並加以有效利用。我們當然可以透過硬編碼的方式將這一依賴項綁定到伺服器上,但是其它語言的編程經驗告訴我們這會是一件非常痛苦的事,因此我們將使用依賴注入的方式較松散地增加路由模組(你可以讀讀Martin Fowlers關於依賴注入的大作來作為背景知識)。
首先,我們來擴充一下伺服器的start()函數,以便將路由函數作為參數傳遞過去:
var http = require("http");
var url = require("url");
function start(route) {
  function onRequest(request, response) {
    var pathname = url.parse(request.url).pathname;
    console.log("Request for " + pathname + " received.");

    route(pathname);

    response.writeHead(200, {"Content-Type": "text/plain"});
    response.write("Hello World");
    response.end();
  }

  http.createServer(onRequest).listen(8888);
  console.log("Server has started.");
}

exports.start = start;
同時,我們會相應擴充index.js,使得路由函數可以被注入到伺服器中:
var server = require("./server");
var router = require("./router");

server.start(router.route);
在這裡,我們傳遞的函數依舊什麼也沒做。
如果現在啟動應用(node index.js,始終記得這個命令行),隨後請求一個URL,你將會看到應用輸出相應的訊息,這表明我們的HTTP伺服器已經在使用路由模組了,並會將請求的路徑傳遞給路由:
bash$ node index.js
Request for /foo received.
About to route a request for /foo
(以上輸出已經去掉了比較煩人的/favicon.ico請求相關的部分)。

行為驅動執行

請允許我再次脫離主題,在這裡談一談函數編程。
將函數作為參數傳遞並不僅僅出於技術上的考量。對軟體設計來說,這其實是個哲學問題。想想這樣的場景:在index檔案中,我們可以將router物件傳遞進去,伺服器隨後可以執行這個物件的route函數。
就像這樣,我們傳遞一個東西,然後伺服器利用這個東西來完成一些事。嗨~那個叫路由的東西,能幫我把這個路由一下嗎?
但是伺服器其實不需要這樣的東西。它只需要把事情做完就行,其實為了把事情做完,你根本不需要東西,你需要的是動作。也就是說,你不需要名詞,你需要動詞
理解了這個概念裡最核心、最基本的思想轉換後,我自然而然地理解了函數編程。
我是在讀了Steve Yegge的大作名詞王國中的死刑之後理解函數編程。你也去讀一讀這本書吧,真的。這是曾給予我閱讀的快樂的關於軟體的書籍之一。

路由給真正的請求處理程序

回到正題,現在我們的HTTP伺服器和請求路由模組已經如我們的期望,可以相互交流了,就像一對親密無間的兄弟。
當然這還遠遠不夠,路由,顧名思義,是指我們要針對不同的URL有不同的處理方式。例如處理/start的 "業務邏輯" 就應該和處理/upload的不同。
在現在的實現下,路由過程會在路由模組中 "結束" ,並且路由模組並不是真正針對請求 "采取行動" 的模組,否則當我們的應用程式變得更為復雜時,將無法很好地擴充。
我們暫時把作為路由目標的函數稱為請求處理程序。現在我們不要急著來開發路由模組,因為如果請求處理程序沒有就緒的話,再怎麼完善路由模組也沒有多大意義。
應用程式需要新的部件,因此加入新的模組 -- 已經無需為此感到新奇了。我們來建立一個叫做requestHandlers的模組,並對於每一個請求處理程序,增加一個占位用函數,隨後將這些函數作為模組的方法匯出:
function start() {
  console.log("Request handler 'start' was called.");
}
function upload() {
  console.log("Request handler 'upload' was called.");
}

exports.start = start;
exports.upload = upload;
這樣我們就可以把請求處理程序和路由模組連接起來,讓路由 "有路可尋" 。
在這裡我們得做個決定:是將requestHandlers模組硬編碼到路由裡來使用,還是再增加一點依賴注入?雖然和其他模式一樣,依賴注入不應該僅僅為使用而使用,但在現在這個情況下,使用依賴注入可以讓路由和請求處理程序之間的耦合更加松散,也因此能讓路由的重用性更高。
這意味著我們得將請求處理程序從伺服器傳遞到路由中,但感覺上這麼做更離譜了,我們得一路把這堆請求處理程序從我們的主檔案傳遞到伺服器中,再將之從伺服器傳遞到路由。
那麼我們要怎麼傳遞這些請求處理程序呢?別看現在我們只有2個處理程序,在一個真實的應用中,請求處理程序的數量會不斷增加,我們當然不想每次有一個新的URL或請求處理程序時,都要為了在路由裡完成請求到處理程序的映射而反復折騰。除此之外,在路由裡有一大堆if request == x then call handler y也使得系統丑陋不堪。
仔細想想,有一大堆東西,每個都要映射到一個字串(就是請求的URL)上?似乎關聯陣列(associative array)能完美勝任。
不過結果有點令人失望,JavaScript沒提供關聯陣列 -- 也可以說它提供了?事實上,在JavaScript中,真正能提供此類功能的是它的物件。
在這方面,http://msdn.microsoft.com/en-us/magazine/cc163419.aspx有一個不錯的介紹,我在此摘錄一段:
在C++或C#中,當我們談到物件,指的是類別(Class)或者結構體(Struct)的實體。物件根據他們實體化的範本(就是所謂的類別),會擁有不同的屬性和方法。但在JavaScript裡物件不是這個概念。在JavaScript中,物件就是一個鍵/值對的集合 -- 你可以把JavaScript的物件想象成一個鍵為字串類型的字典。
但如果JavaScript的物件僅僅是鍵/值對的集合,它又怎麼會擁有方法呢?好吧,這裡的值可以是字串、數字或者……函數!
好了,最後再回到代碼上來。現在我們已經確定將一系列請求處理程序透過一個物件來傳遞,並且需要使用松耦合的方式將這個物件注入到route()函數中。
我們先將這個物件引入到主檔案index.js中:
var server = require("./server");
var router = require("./router");
var requestHandlers = require("./requestHandlers");
var handle = {}
handle["/"] = requestHandlers.start;
handle["/start"] = requestHandlers.start;
handle["/upload"] = requestHandlers.upload;

server.start(router.route, handle);
雖然handle並不僅僅是一個 "東西" (一些請求處理程序的集合),我還是建議以一個動詞作為其命名,這樣做可以讓我們在路由中使用更流暢的表達式,稍後會有說明。
正如所見,將不同的URL映射到相同的請求處理程序上是很容易的:只要在物件中增加一個鍵為"/"的屬性,對應requestHandlers.start即可,這樣我們就可以乾淨簡潔地配置/start/的請求都交由start這一處理程序處理。
在完成了物件的定義後,我們把它作為額外的參數傳遞給伺服器,為此將server.js修改如下:
var http = require("http");
var url = require("url");
function start(route, handle) {
  function onRequest(request, response) {
    var pathname = url.parse(request.url).pathname;
    console.log("Request for " + pathname + " received.");

    route(handle, pathname);

    response.writeHead(200, {"Content-Type": "text/plain"});
    response.write("Hello World");
    response.end();
  }

  http.createServer(onRequest).listen(8888);
  console.log("Server has started.");
}

exports.start = start;
這樣我們就在start()函數裡增加了handle參數,並且把handle物件作為第一個參數傳遞給了route()回呼(callback)函數。
然後我們相應地在route.js檔案中修改route()函數:
function route(handle, pathname) {
  console.log("About to route a request for " + pathname);
  if (typeof handle[pathname] === 'function') {
    handle[pathname]();
  } else {
    console.log("No request handler found for " + pathname);
  }
}

exports.route = route;
透過以上代碼,我們首先檢查給定的路徑對應的請求處理程序是否存在,如果存在的話直接執行相應的函數。我們可以用從關聯陣列中取得元素一樣的方式從傳遞的物件中取得請求處理函數,因此就有了簡潔流暢的形如handle[pathname]();的表達式,這個感覺就像在前方中提到的那樣: "嗨,請幫我處理了這個路徑" 。
有了這些,我們就把伺服器、路由和請求處理程序在一起了。現在我們啟動應用程式並在瀏覽器中存取http://localhost:8888/start,以下日志可以說明系統執行了正確的請求處理程序:
Server has started.
Request for /start received.
About to route a request for /start
Request handler 'start' was called.
並且在瀏覽器中打開http://localhost:8888/可以看到這個請求同樣被start請求處理程序處理了:
Request for / received.
About to route a request for /
Request handler 'start' was called.

讓請求處理程序作出回應

很好。不過現在要是請求處理程序能夠向瀏覽器回傳一些有意義的訊息而並非全是 "Hello World" ,那就更好了。
這裡要記住的是,瀏覽器發出請求後獲得並顯示的 "Hello World" 訊息仍是來自於我們server.js檔案中的onRequest函數。
其實 "處理請求" 說白了就是 "對請求作出回應" ,因此,我們需要讓請求處理程序能夠像onRequest函數那樣可以和瀏覽器進行 "對話" 。

不好的實現方式

對於我們這樣擁有PHP或者Ruby技術背景的開發者來說,最直截了當的實現方式事實上並不是非常靠譜: 看似有效,實則未必如此。
這裡我指的 "直截了當的實現方式" 意思是:讓請求處理程序透過onRequest函數直接回傳(return())他們要展示給用戶的訊息。
我們先就這樣去實現,然後再來看為什麼這不是一種很好的實現方式。
讓我們從讓請求處理程序回傳需要在瀏覽器中顯示的訊息開始。我們需要將requestHandler.js修改為如下形式:
function start() {
  console.log("Request handler 'start' was called.");
  return "Hello Start";
}
function upload() {
  console.log("Request handler 'upload' was called.");
  return "Hello Upload";
}

exports.start = start;
exports.upload = upload;
好的。同樣的,請求路由需要將請求處理程序回傳給它的訊息回傳給伺服器。因此,我們需要將router.js修改為如下形式:
function route(handle, pathname) {
  console.log("About to route a request for " + pathname);
  if (typeof handle[pathname] === 'function') {
    return handle[pathname]();
  } else {
    console.log("No request handler found for " + pathname);
    return "404 Not found";
  }
}

exports.route = route;
正如上述代碼所示,當請求無法路由的時候,我們也回傳了一些相關的錯誤訊息。
最後,我們需要對我們的server.js進行重構以使得它能夠將請求處理程序透過請求路由回傳的內容回應給瀏覽器,如下所示:
var http = require("http");
var url = require("url");
function start(route, handle) {
  function onRequest(request, response) {
    var pathname = url.parse(request.url).pathname;
    console.log("Request for " + pathname + " received.");

    response.writeHead(200, {"Content-Type": "text/plain"});
    var content = route(handle, pathname)
    response.write(content);
    response.end();
  }

  http.createServer(onRequest).listen(8888);
  console.log("Server has started.");
}

exports.start = start;
如果我們運行重構後的應用,一切都會工作的很好:請求http://localhost:8888/start,瀏覽器會輸出 "Hello Start" ,請求http://localhost:8888/upload會輸出 "Hello Upload" ,而請求http://localhost:8888/foo 會輸出 "404 Not found" 。
好,那麼問題在哪裡呢?簡單的說就是: 當未來有請求處理程序需要進行Non-Blocking的操作的時候,我們的應用就 "掛" 了。
沒理解?沒關系,下面就來詳細解釋下。

Blocking與Non-Blocking

正如此前所提到的,當在請求處理程序中包括Non-Blocking操作時就會出問題。但是,在說這之前,我們先來看看什麼是Blocking操作。
我不想去解釋 "Blocking" 和 "Non-Blocking" 的具體含義,我們直接來看,當在請求處理程序中加入Blocking操作時會發生什麼。
這裡,我們來修改下start請求處理程序,我們讓它等待10秒以後再回傳 "Hello Start" 。因為,JavaScript中沒有類似sleep()這樣的操作,所以這裡只能夠來點小Hack來模擬實現。
讓我們將requestHandlers.js修改成如下形式:
function start() {
  console.log("Request handler 'start' was called.");

  function sleep(milliSeconds) {
    var startTime = new Date().getTime();
    while (new Date().getTime() < startTime + milliSeconds);
  }

  sleep(10000);
  return "Hello Start";
}
function upload() {
  console.log("Request handler 'upload' was called.");
  return "Hello Upload";
}

exports.start = start;
exports.upload = upload;
上述代碼中,當函數start()被執行的時候,Node.js會先等待10秒,之後才會回傳 "Hello Start" 。當執行upload()的時候,會和此前一樣立即回傳。
(當然了,這裡只是模擬休眠10秒,實際場景中,這樣的Blocking操作有很多,比方說一些長時間的計算操作等。)
接下來就讓我們來看看,我們的改動帶來了哪些變化。
如往常一樣,我們先要重啟下伺服器。為了看到效果,我們要進行一些相對復雜的操作(跟著我一起做): 首先,打開兩個瀏覽器窗口或者標簽頁。在第一個瀏覽器窗口的地址欄中輸入http://localhost:8888/start, 但是先不要打開它!
在第二個瀏覽器窗口的地址欄中輸入http://localhost:8888/upload, 同樣的,先不要打開它!
接下來,做如下操作:在第一個窗口中( "/start" )按下 Enter,然後快速切換到第二個窗口中( "/upload" )按下 Enter。
注意,發生了什麼: /start URL加載花了10秒,這和我們預期的一樣。但是,/upload URL居然花了10秒,而它在對應的請求處理程序中並沒有類似於sleep()這樣的操作!
這到底是為什麼呢?原因就是start()包含了Blocking操作。形象的說就是 "它Blocking了所有其他的處理工作" 。
這顯然是個問題,因為Node一向是這樣來標榜自己的: "在node中除了代碼,所有一切都是並行執行的" 
這句話的意思是說,Node.js可以在不新增額外執行緒的情況下,依然可以對任務進行並行處理 —— Node.js是單執行緒的。它透過事件輪詢(event loop)來實現並行操作,對此,我們應該要充分利用這一點 —— 盡可能的避免Blocking操作,取而代之,多使用Non-Blocking操作。
然而,要用Non-Blocking操作,我們需要使用回呼(callback),透過將函數作為參數傳遞給其他需要花時間做處理的函數(比方說,休眠10秒,或者查詢資料庫,又或者是進行大量的計算)。
對於Node.js來說,它是這樣處理的: "嘿,probablyExpensiveFunction()(譯者注:這裡指的就是需要花時間處理的函數),你繼續處理你的事情,我(Node.js執行緒)先不等你了,我繼續去處理你後面的代碼,請你提供一個callbackFunction(),等你處理完之後我會去執行該回呼(callback)函數的,謝謝!"
(如果想要了解更多關於事件輪詢細節,可以閱讀Mixu的博文——理解node.js的事件輪詢。)
接下來,我們會介紹一種錯誤的使用Non-Blocking操作的方式。
和上次一樣,我們透過修改我們的應用來暴露問題。
這次我們還是拿start請求處理程序來 "開刀" 。將其修改成如下形式:
var exec = require("child_process").exec;
function start() {
  console.log("Request handler 'start' was called.");
  var content = "empty";

  exec("ls -lah", function (error, stdout, stderr) {
    content = stdout;
  });

  return content;
}
function upload() {
  console.log("Request handler 'upload' was called.");
  return "Hello Upload";
}

exports.start = start;
exports.upload = upload;
上述代碼中,我們引入了一個新的Node.js模組,child_process。之所以用它,是為了實現一個既簡單又實用的Non-Blocking操作:exec()
exec()做了什麼呢?它從Node.js來執行一個shell命令。在上述例子中,我們用它來取得目前目錄下所有的檔案( "ls -lah" ),然後,當/startURL請求的時候將檔案訊息輸出到瀏覽器中。
上述代碼是非常直觀的: 建立了一個新的變數content(初始值為 "empty" ),執行 "ls -lah" 命令,將結果賦值給content,最後將content回傳。
和往常一樣,我們啟動伺服器,然後存取 "http://localhost:8888/start" 。
之後會載入一個漂亮的web頁面,其內容為 "empty" 。怎麼回事?
這個時候,你可能大致已經猜到了,exec()在Non-Blocking這塊發揮了神奇的功效。它其實是個很好的東西,有了它,我們可以執行非常耗時的shell操作而無需迫使我們的應用停下來等待該操作。
(如果想要證明這一點,可以將 "ls -lah" 換成比如 "find /" 這樣更耗時的操作來效果)。
然而,針對瀏覽器顯示的結果來看,我們並不滿意我們的Non-Blocking操作,對吧?
好,接下來,我們來修正這個問題。在這過程中,讓我們先來看看為什麼目前的這種方式不起作用。
問題就在於,為了進行Non-Blocking工作,exec()使用了回呼(callback)函數。
在我們的例子中,該回呼(callback)函數就是作為第二個參數傳遞給exec()的匿名函數:
function (error, stdout, stderr) {
  content = stdout;
}
現在就到了問題根源所在了:我們的代碼是同步執行的,這就意味著在執行exec()之後,Node.js會立即執行 return content ;在這個時候,content仍然是 "empty" ,因為傳遞給exec()的回呼(callback)函數還未執行到——因為exec()的操作是非同步的。
我們這裡 "ls -lah" 的操作其實是非常快的(除非目前目錄下有上百萬個檔案)。這也是為什麼回呼(callback)函數也會很快的執行到 —— 不過,不管怎麼說它還是非同步的。
為了讓效果更加明顯,我們想象一個更耗時的命令: "find /" ,它在我機器上需要執行1分鐘左右的時間,然而,盡管在請求處理程序中,我把 "ls -lah" 換成 "find /" ,當打開/start URL的時候,依然能夠立即獲得HTTP回應 —— 很明顯,當exec()在後台執行的時候,Node.js自身會繼續執行後面的代碼。並且我們這裡假設傳遞給exec()的回呼(callback)函數,只會在 "find /" 命令執行完成之後才會被執行。
那究竟我們要如何才能實現將目前目錄下的檔案列表顯示給用戶呢?
好,了解了這種不好的實現方式之後,我們接下來來介紹如何以正確的方式讓請求處理程序對瀏覽器請求作出回應。

以Non-Blocking操作進行請求回應

我剛剛提到了這樣一個短語 —— "正確的方式" 。而事實上通常 "正確的方式" 一般都不簡單。
不過,用Node.js就有這樣一種實現方案: 函數傳遞。下面就讓我們來具體看看如何實現。
到目前為止,我們的應用已經可以透過應用各層之間傳遞值的方式(請求處理程序 -> 請求路由 -> 伺服器)將請求處理程序回傳的內容(請求處理程序最終要顯示給用戶的內容)傳遞給HTTP伺服器。
現在我們採用如下這種新的實現方式:相對採用將內容傳遞給伺服器的方式,我們這次採用將伺服器 "傳遞" 給內容的方式。 從實踐角度來說,就是將response物件(從伺服器的回呼(callback)函數onRequest()取得)透過請求路由傳遞給請求處理程序。 隨後,處理程序就可以採用該物件上的函數來對請求作出回應。
原理就是如此,接下來讓我們來一步步實現這種方案。
先從server.js開始:
var http = require("http");
var url = require("url");
function start(route, handle) {
  function onRequest(request, response) {
    var pathname = url.parse(request.url).pathname;
    console.log("Request for " + pathname + " received.");

    route(handle, pathname, response);
  }

  http.createServer(onRequest).listen(8888);
  console.log("Server has started.");
}

exports.start = start;
相對此前從route()函數取得回傳值的做法,這次我們將response物件作為第三個參數傳遞給route()函數,並且,我們將onRequest()處理程序中所有有關response的函數調都移除,因為我們希望這部分工作讓route()函數來完成。
下面就來看看我們的router.js:
function route(handle, pathname, response) {
  console.log("About to route a request for " + pathname);
  if (typeof handle[pathname] === 'function') {
    handle[pathname](response);
  } else {
    console.log("No request handler found for " + pathname);
    response.writeHead(404, {"Content-Type": "text/plain"});
    response.write("404 Not found");
    response.end();
  }
}

exports.route = route;
同樣的模式:相對此前從請求處理程序中取得回傳值,這次取而代之的是直接傳遞response物件。
如果沒有對應的請求處理器處理,我們就直接回傳 "404" 錯誤。
最後,我們將requestHandler.js修改為如下形式:
var exec = require("child_process").exec;
function start(response) {
  console.log("Request handler 'start' was called.");

  exec("ls -lah", function (error, stdout, stderr) {
    response.writeHead(200, {"Content-Type": "text/plain"});
    response.write(stdout);
    response.end();
  });
}
function upload(response) {
  console.log("Request handler 'upload' was called.");
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("Hello Upload");
  response.end();
}

exports.start = start;
exports.upload = upload;
我們的處理程序函數需要接收response參數,為了對請求作出直接的回應。
start處理程序在exec()的匿名回呼(callback)函數中做請求回應的操作,而upload處理程序仍然是簡單的回復 "Hello World" ,只是這次是使用response物件而已。
這時再次我們啟動應用(node index.js),一切都會工作的很好。
如果想要證明/start處理程序中耗時的操作不會Blocking對/upload請求作出立即回應的話,可以將requestHandlers.js修改為如下形式:
var exec = require("child_process").exec;
function start(response) {
  console.log("Request handler 'start' was called.");

  exec("find /",
    { timeout: 10000, maxBuffer: 20000*1024 },
    function (error, stdout, stderr) {
      response.writeHead(200, {"Content-Type": "text/plain"});
      response.write(stdout);
      response.end();
    });
}
function upload(response) {
  console.log("Request handler 'upload' was called.");
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("Hello Upload");
  response.end();
}

exports.start = start;
exports.upload = upload;
這樣一來,當請求http://localhost:8888/start的時候,會花10秒鐘的時間才載入,而當請求http://localhost:8888/upload的時候,會立即回應,縱然這個時候/start回應還在處理中。

更有用的場景

到目前為止,我們做的已經很好了,但是,我們的應用沒有實際用途。
伺服器,請求路由以及請求處理程序都已經完成了,下面讓我們按照此前的使用案例給網站增加互動:用戶選擇一個檔案,上傳該檔案,然後在瀏覽器中看到上傳的檔案。 為了保持簡單,我們假設用戶只會上傳圖片,然後我們應用將該圖片顯示到瀏覽器中。
好,下面就一步步來實現,鑒於此前已經對JavaScript原理性技術性的內容做過大量介紹了,這次我們加快點速度。
要實現該功能,分為如下兩步: 首先,讓我們來看看如何處理POST請求(非檔案上傳),之後,我們使用Node.js的一個用於檔案上傳的外部模組。之所以採用這種實現方式有兩個理由。
第一,盡管在Node.js中處理基礎的POST請求相對比較簡單,但在這過程中還是能學到很多。
第二,用Node.js來處理檔案上傳(multipart POST請求)是比較復雜的,它在本書的范疇,但,如何使用外部模組卻是在本書涉獵內容之內。

處理POST請求

考慮這樣一個簡單的例子:我們顯示一個文字區(textarea)供用戶輸入內容,然後透過POST請求送出給伺服器。最後,伺服器接受到請求,透過處理程序將輸入的內容展示到瀏覽器中。
/start請求處理程序用於建置帶文字區的表單,因此,我們將requestHandlers.js修改為如下形式:
function start(response) {
  console.log("Request handler 'start' was called.");

  var body = '<html>'+
    '<head>'+
    '<meta http-equiv="Content-Type" content="text/html; '+
    'charset=UTF-8" />'+
    '</head>'+
    '<body>'+
    '<form action="/upload" method="post">'+
    '<textarea name="text" rows="20" cols="60"></textarea>'+
    '<input type="submit" value="Submit text" />'+
    '</form>'+
    '</body>'+
    '</html>';

    response.writeHead(200, {"Content-Type": "text/html"});
    response.write(body);
    response.end();
}
function upload(response) {
  console.log("Request handler 'upload' was called.");
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("Hello Upload");
  response.end();
}

exports.start = start;
exports.upload = upload;
好了,現在我們的應用已經很完善了,都可以獲得威比獎(Webby Awards)了,哈哈。(譯者注:威比獎是由國際數字藝術與科學學院主辦的評選全球最佳網站的獎項,具體參見詳細說明)透過在瀏覽器中存取http://localhost:8888/start就可以看到簡單的表單了,要記得重啟伺服器哦!
你可能會說:這種直接將視覺元素放在請求處理程序中的方式太丑陋了。說的沒錯,但是,我並不想在本書中介紹諸如MVC之類的模式,因為這對於你了解JavaScript或者Node.js環境來說沒多大關系。
余下的篇幅,我們來探討一個更有趣的問題: 當用戶送出表單時,觸發/upload請求處理程序處理POST請求的問題。
現在,我們已經是新手中的專家了,很自然會想到採用非同步回呼(callback)來實現Non-Blocking地處理POST請求的資料。
這裡採用Non-Blocking方式處理是明智的,因為POST請求一般都比較 "重" —— 用戶可能會輸入大量的內容。用Blocking的方式處理大資料量的請求必然會導致用戶操作的Blocking。
為了使整個過程Non-Blocking,Node.js會將POST資料拆分成很多小的資料區塊,然後透過觸發特定的事件,將這些小資料區塊傳遞給回呼(callback)函數。這裡的特定的事件有data事件(表示新的小資料區塊到達了)以及end事件(表示所有的資料都已經接收完畢)。
我們需要告訴Node.js當這些事件觸發的時候,回呼(callback)哪些函數。怎麼告訴呢? 我們透過在request物件上註冊監聽器(listener) 來實現。這裡的request物件是每次接收到HTTP請求時候,都會把該物件傳遞給onRequest回呼(callback)函數。
如下所示:
request.addListener("data", function(chunk) {
  // called when a new chunk of data was received
});

request.addListener("end", function() {
  // called when all chunks of data have been received
});
問題來了,這部分邏輯寫在哪裡呢? 我們現在只是在伺服器中取得到了request物件 —— 我們並沒有像之前response物件那樣,把 request 物件傳遞給請求路由和請求處理程序。
在我看來,取得所有來自請求的資料,然後將這些資料給應用層處理,應該是HTTP伺服器要做的事情。因此,我建議,我們直接在伺服器中處理POST資料,然後將最終的資料傳遞給請求路由和請求處理器,讓他們來進行進一步的處理。
因此,實現思路就是: 將dataend事件的回呼(callback)函數直接放在伺服器中,在data事件回呼(callback)中收集所有的POST資料,當接收到所有資料,觸發end事件後,其回呼(callback)函數執行請求路由,並將資料傳遞給它,然後,請求路由再將該資料傳遞給請求處理程序。
還等什麼,馬上來實現。先從server.js開始:
var http = require("http");
var url = require("url");
function start(route, handle) {
  function onRequest(request, response) {
    var postData = "";
    var pathname = url.parse(request.url).pathname;
    console.log("Request for " + pathname + " received.");

    request.setEncoding("utf8");

    request.addListener("data", function(postDataChunk) {
      postData += postDataChunk;
      console.log("Received POST data chunk '"+
      postDataChunk + "'.");
    });

    request.addListener("end", function() {
      route(handle, pathname, response, postData);
    });

  }

  http.createServer(onRequest).listen(8888);
  console.log("Server has started.");
}

exports.start = start;
上述代碼做了三件事情: 首先,我們設定了接收資料的編碼格式為UTF-8,然後註冊了 "data" 事件的監聽器,用於收集每次接收到的新資料區塊,並將其賦值給postData 變數,最後,我們將請求路由的執行移到end事件處理程序中,以確保它只會當所有資料接收完畢後才觸發,並且只觸發一次。我們同時還把POST資料傳遞給請求路由,因為這些資料,請求處理程序會用到。
上述代碼在每個資料區塊到達的時候輸出了日志,這對於最終生產環境來說,是很不好的(資料量可能會很大,還記得吧?),但是,在開發階段是很有用的,有助於讓我們看到發生了什麼。
我建議可以嘗試下,嘗試著去輸入一小段文字,以及大段內容,當大段內容的時候,就會發現data事件會觸發多次。
再來點酷的。我們接下來在/upload頁面,展示用戶輸入的內容。要實現該功能,我們需要將postData傳遞給請求處理程序,修改router.js為如下形式:
function route(handle, pathname, response, postData) {
  console.log("About to route a request for " + pathname);
  if (typeof handle[pathname] === 'function') {
    handle[pathname](response, postData);
  } else {
    console.log("No request handler found for " + pathname);
    response.writeHead(404, {"Content-Type": "text/plain"});
    response.write("404 Not found");
    response.end();
  }
}

exports.route = route;
然後,在requestHandlers.js中,我們將資料包含在對upload請求的回應中:
function start(response, postData) {
  console.log("Request handler 'start' was called.");

  var body = '<html>'+
    '<head>'+
    '<meta http-equiv="Content-Type" content="text/html; '+
    'charset=UTF-8" />'+
    '</head>'+
    '<body>'+
    '<form action="/upload" method="post">'+
    '<textarea name="text" rows="20" cols="60"></textarea>'+
    '<input type="submit" value="Submit text" />'+
    '</form>'+
    '</body>'+
    '</html>';

    response.writeHead(200, {"Content-Type": "text/html"});
    response.write(body);
    response.end();
}
function upload(response, postData) {
  console.log("Request handler 'upload' was called.");
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("You've sent: " + postData);
  response.end();
}

exports.start = start;
exports.upload = upload;
好了,我們現在可以接收POST資料並在請求處理程序中處理該資料了。
我們最後要做的是: 目前我們是把POST請求的整個訊息內容傳遞給了請求路由和請求處理程序。我們應該只把POST資料中感興趣的部分傳遞給請求路由和請求處理程序。在我們這個例子中,我們感興趣的其實只是text欄位。
我們可以使用此前介紹過的querystring模組來實現:
var querystring = require("querystring");
function start(response, postData) {
  console.log("Request handler 'start' was called.");

  var body = '<html>'+
    '<head>'+
    '<meta http-equiv="Content-Type" content="text/html; '+
    'charset=UTF-8" />'+
    '</head>'+
    '<body>'+
    '<form action="/upload" method="post">'+
    '<textarea name="text" rows="20" cols="60"></textarea>'+
    '<input type="submit" value="Submit text" />'+
    '</form>'+
    '</body>'+
    '</html>';

    response.writeHead(200, {"Content-Type": "text/html"});
    response.write(body);
    response.end();
}
function upload(response, postData) {
  console.log("Request handler 'upload' was called.");
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("You've sent the text: "+
  querystring.parse(postData).text);
  response.end();
}

exports.start = start;
exports.upload = upload;
好了,以上就是關於處理POST資料的全部內容。

處理檔案上傳

最後,我們來實現我們最終的使用案例:允許用戶上傳圖片,並將該圖片在瀏覽器中顯示出來。
回到90年代,這個使用案例完全可以滿足用於IPO的商業模型了,如今,我們透過它能學到這樣兩件事情: 如何安裝外部Node.js模組,以及如何將它們應用到我們的應用中。
這裡我們要用到的外部模組是Felix Geisendörfer開發的node-formidable模組。它對解析上傳的檔案資料做了很好的抽象。 其實說白了,處理檔案上傳 "就是" 處理POST資料 —— 但是,麻煩的是在具體的處理細節,所以,這裡採用現成的方案更合適點。
使用該模組,首先需要安裝該模組。Node.js有它自己的包管理器,叫NPM。它可以讓安裝Node.js的外部模組變得非常方便。透過如下一條命令就可以完成該模組的安裝:
npm install formidable
如果終端輸出如下內容:
npm info build Success: formidable@1.0.9
npm ok
就說明模組已經安裝成功了。
現在我們就可以用formidable模組了——使用外部模組與內部模組類似,用require語句將其引入即可:
var formidable = require("formidable");
這裡該模組做的就是將透過HTTP POST請求送出的表單,在Node.js中可以被解析。我們要做的就是建立一個新的IncomingForm,它是對送出表單的抽象表示,之後,就可以用它解析request物件,取得表單中需要的資料欄位。
node-formidable官方的例子展示了這兩部分是如何融合在一起工作的:
var formidable = require('formidable'),
    http = require('http'),
    sys = require('sys');

http.createServer(function(req, res) {
  if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
    // parse a file upload
    var form = new formidable.IncomingForm();
    form.parse(req, function(err, fields, files) {
      res.writeHead(200, {'content-type': 'text/plain'});
      res.write('received upload:\n\n');
      res.end(sys.inspect({fields: fields, files: files}));
    });
    return;
  }

  // show a file upload form
  res.writeHead(200, {'content-type': 'text/html'});
  res.end(
    '<form action="/upload" enctype="multipart/form-data" '+
    'method="post">'+
    '<input type="text" name="title"><br>'+
    '<input type="file" name="upload" multiple="multiple"><br>'+
    '<input type="submit" value="Upload">'+
    '</form>'
  );
}).listen(8888);
如果我們將上述代碼,儲存到一個檔案中,並透過node來執行,就可以進行簡單的表單送出了,包括檔案上傳。然後,可以看到透過執行form.parse傳遞給回呼(callback)函數的files物件的內容,如下所示:
received upload:

{ fields: { title: 'Hello World' },
  files:
   { upload:
      { size: 1558,
        path: '/tmp/1c747974a27a6292743669e91f29350b',
        name: 'us-flag.png',
        type: 'image/png',
        lastModifiedDate: Tue, 21 Jun 2011 07:02:41 GMT,
        _writeStream: [Object],
        length: [Getter],
        filename: [Getter],
        mime: [Getter] } } }
為了實現我們的功能,我們需要將上述代碼應用到我們的應用中,另外,我們還要考慮如何將上傳檔案的內容(儲存在/tmp目錄中)顯示到瀏覽器中。
我們先來解決後面那個問題: 對於儲存在區域硬碟中的檔案,如何才能在瀏覽器中看到呢?
顯然,我們需要將該檔案讀取到我們的伺服器中,使用一個叫fs的模組。
我們來增加/showURL的請求處理程序,該處理程序直接硬編碼將檔案/tmp/test.png內容展示到瀏覽器中。當然了,首先需要將該圖片儲存到這個位置才行。
requestHandlers.js修改為如下形式:
var querystring = require("querystring"),
    fs = require("fs");
function start(response, postData) {
  console.log("Request handler 'start' was called.");

  var body = '<html>'+
    '<head>'+
    '<meta http-equiv="Content-Type" '+
    'content="text/html; charset=UTF-8" />'+
    '</head>'+
    '<body>'+
    '<form action="/upload" method="post">'+
    '<textarea name="text" rows="20" cols="60"></textarea>'+
    '<input type="submit" value="Submit text" />'+
    '</form>'+
    '</body>'+
    '</html>';

    response.writeHead(200, {"Content-Type": "text/html"});
    response.write(body);
    response.end();
}
function upload(response, postData) {
  console.log("Request handler 'upload' was called.");
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("You've sent the text: "+
  querystring.parse(postData).text);
  response.end();
}
function show(response, postData) {
  console.log("Request handler 'show' was called.");
  fs.readFile("/tmp/test.png", "binary", function(error, file) {
    if(error) {
      response.writeHead(500, {"Content-Type": "text/plain"});
      response.write(error + "\n");
      response.end();
    } else {
      response.writeHead(200, {"Content-Type": "image/png"});
      response.write(file, "binary");
      response.end();
    }
  });
}

exports.start = start;
exports.upload = upload;
exports.show = show;
我們還需要將這新的請求處理程序,增加到index.js中的路由映射表中:
var server = require("./server");
var router = require("./router");
var requestHandlers = require("./requestHandlers");
var handle = {}
handle["/"] = requestHandlers.start;
handle["/start"] = requestHandlers.start;
handle["/upload"] = requestHandlers.upload;
handle["/show"] = requestHandlers.show;

server.start(router.route, handle);
重啟伺服器之後,透過存取http://localhost:8888/show,就可以看到儲存在/tmp/test.png的圖片了。
好,最後我們要的就是:
  • /start表單中增加一個檔案上傳元素
  • 將node-formidable整合到我們的upload請求處理程序中,用於將上傳的圖片儲存到/tmp/test.png
  • 將上傳的圖片內嵌到/uploadURL輸出的HTML中
第一項很簡單。只需要在HTML表單中,增加一個multipart/form-data的編碼類型,移除此前的文字區,增加一個檔案上傳組件,並將送出按鈕的文案改為 "Upload file" 即可。 如下requestHandler.js所示:
var querystring = require("querystring"),
    fs = require("fs");
function start(response, postData) {
  console.log("Request handler 'start' was called.");

  var body = '<html>'+
    '<head>'+
    '<meta http-equiv="Content-Type" '+
    'content="text/html; charset=UTF-8" />'+
    '</head>'+
    '<body>'+
    '<form action="/upload" enctype="multipart/form-data" '+
    'method="post">'+
    '<input type="file" name="upload">'+
    '<input type="submit" value="Upload file" />'+
    '</form>'+
    '</body>'+
    '</html>';

    response.writeHead(200, {"Content-Type": "text/html"});
    response.write(body);
    response.end();
}
function upload(response, postData) {
  console.log("Request handler 'upload' was called.");
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("You've sent the text: "+
  querystring.parse(postData).text);
  response.end();
}
function show(response, postData) {
  console.log("Request handler 'show' was called.");
  fs.readFile("/tmp/test.png", "binary", function(error, file) {
    if(error) {
      response.writeHead(500, {"Content-Type": "text/plain"});
      response.write(error + "\n");
      response.end();
    } else {
      response.writeHead(200, {"Content-Type": "image/png"});
      response.write(file, "binary");
      response.end();
    }
  });
}

exports.start = start;
exports.upload = upload;
exports.show = show;
很好。下一步相對比較復雜。這裡有這樣一個問題: 我們需要在upload處理程序中對上傳的檔案進行處理,這樣的話,我們就需要將request物件傳遞給node-formidable的form.parse函數。
但是,我們有的只是response物件和postData陣列。看樣子,我們只能不得不將request物件從伺服器開始一路透過請求路由,再傳遞給請求處理程序。 或許還有更好的方案,但是,不管怎麼說,目前這樣做可以滿足我們的需求。
到這裡,我們可以將postData從伺服器以及請求處理程序中移除了 —— 一方面,對於我們處理檔案上傳來說已經不需要了,另外一方面,它甚至可能會引發這樣一個問題: 我們已經 "消耗" 了request物件中的資料,這意味著,對於form.parse來說,當它想要取得資料的時候就什麼也取得不到了。(因為Node.js不會對資料做快取)
我們從server.js開始 —— 移除對postData的處理以及request.setEncoding (這部分node-formidable自身會處理),轉而採用將request物件傳遞給請求路由的方式:
var http = require("http");
var url = require("url");
function start(route, handle) {
  function onRequest(request, response) {
    var pathname = url.parse(request.url).pathname;
    console.log("Request for " + pathname + " received.");
    route(handle, pathname, response, request);
  }

  http.createServer(onRequest).listen(8888);
  console.log("Server has started.");
}

exports.start = start;
接下來是 router.js —— 我們不再需要傳遞postData了,這次要傳遞request物件:
function route(handle, pathname, response, request) {
  console.log("About to route a request for " + pathname);
  if (typeof handle[pathname] === 'function') {
    handle[pathname](response, request);
  } else {
    console.log("No request handler found for " + pathname);
    response.writeHead(404, {"Content-Type": "text/html"});
    response.write("404 Not found");
    response.end();
  }
}

exports.route = route;
現在,request物件就可以在我們的upload請求處理程序中使用了。node-formidable會處理將上傳的檔案儲存到區域/tmp目錄中,而我們需要做的是確保該檔案儲存成/tmp/test.png。 沒錯,我們保持簡單,並假設只允許上傳PNG圖片。
這裡採用fs.renameSync(path1,path2)來實現。要注意的是,正如其名,該方法是同步執行的, 也就是說,如果該重命名的操作很耗時的話會Blocking。 這塊我們先不考慮。
接下來,我們把處理檔案上傳以及重命名的操作放到一起,如下requestHandlers.js所示:
var querystring = require("querystring"),
    fs = require("fs"),
    formidable = require("formidable");
function start(response) {
  console.log("Request handler 'start' was called.");

  var body = '<html>'+
    '<head>'+
    '<meta http-equiv="Content-Type" content="text/html; '+
    'charset=UTF-8" />'+
    '</head>'+
    '<body>'+
    '<form action="/upload" enctype="multipart/form-data" '+
    'method="post">'+
    '<input type="file" name="upload" multiple="multiple">'+
    '<input type="submit" value="Upload file" />'+
    '</form>'+
    '</body>'+
    '</html>';

    response.writeHead(200, {"Content-Type": "text/html"});
    response.write(body);
    response.end();
}
function upload(response, request) {
  console.log("Request handler 'upload' was called.");

  var form = new formidable.IncomingForm();
  console.log("about to parse");
  form.parse(request, function(error, fields, files) {
    console.log("parsing done");
    fs.renameSync(files.upload.path, "/tmp/test.png");
    response.writeHead(200, {"Content-Type": "text/html"});
    response.write("received image:<br/>");
    response.write("<img src='/show' />");
    response.end();
  });
}
function show(response) {
  console.log("Request handler 'show' was called.");
  fs.readFile("/tmp/test.png", "binary", function(error, file) {
    if(error) {
      response.writeHead(500, {"Content-Type": "text/plain"});
      response.write(error + "\n");
      response.end();
    } else {
      response.writeHead(200, {"Content-Type": "image/png"});
      response.write(file, "binary");
      response.end();
    }
  });
}

exports.start = start;
exports.upload = upload;
exports.show = show;
好了,重啟伺服器,我們應用所有的功能就可以用了。選擇一張區域圖片,將其上傳到伺服器,然後瀏覽器就會顯示該圖片。

總結與展望

恭喜,我們的任務已經完成了!我們開發完了一個Node.js的web應用,應用雖小,但卻 "五臟俱全" 。 期間,我們介紹了很多技術點:伺服器端JavaScript、函數編程、Blocking與Non-Blocking、回呼(callback)、事件、內部和外部模組等等。
當然了,還有許多本書沒有介紹到的: 如何操作資料庫、如何進行單元測試、如何開發Node.js的外部模組以及一些簡單的諸如如何取得GET請求之類的方法。
但本書畢竟只是一本給初學者的教學課程 —— 不可能覆蓋到所有的內容。

幸運的是,Node.js社區非常活躍(作個不恰當的比喻就是猶如一群有過動兒在一起,能不活躍嗎?), 這意味著,有許多關於Node.js的資源,有什麼問題都可以向社區尋求解答。 其中Node.js社區的wiki以及 NodeCloud就是最好的資源。