<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                <h2 id="12.1">Node.js 概述</h2> ## 簡介 Node是JavaScript語言的服務器運行環境。 所謂“運行環境”有兩層意思:首先,JavaScript語言通過Node在服務器運行,在這個意義上,Node有點像JavaScript虛擬機;其次,Node提供大量工具庫,使得JavaScript語言與操作系統互動(比如讀寫文件、新建子進程),在這個意義上,Node又是JavaScript的工具庫。 Node內部采用Google公司的V8引擎,作為JavaScript語言解釋器;通過自行開發的libuv庫,調用操作系統資源。 ### 安裝與更新 訪問官方網站[nodejs.org](http://nodejs.org)或者[github.com/nodesource/distributions](https://github.com/nodesource/distributions),查看Node的最新版本和安裝方法。 官方網站提供編譯好的二進制包,可以把它們解壓到`/usr/local`目錄下面。 ```bash $ tar -xf node-someversion.tgz ``` 然后,建立符號鏈接,把它們加到$PATH變量里面的路徑。 ```bash $ ln -s /usr/local/node/bin/node /usr/local/bin/node $ ln -s /usr/local/node/bin/npm /usr/local/bin/npm ``` 下面是Ubuntu和Debian下面安裝Deb軟件包的安裝方法。 ```bash $ curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash - $ sudo apt-get install -y nodejs $ apt-get install nodejs ``` 安裝完成以后,運行下面的命令,查看是否能正常運行。 ```bash $ node --version # 或者 $ node -v ``` 更新node.js版本,可以通過node.js的`n`模塊完成。 ```bash $ sudo npm install n -g $ sudo n stable ``` 上面代碼通過`n`模塊,將node.js更新為最新發布的穩定版。 `n`模塊也可以指定安裝特定版本的node。 ```bash $ sudo n 0.10.21 ``` ### 版本管理工具nvm 如果想在同一臺機器,同時安裝多個版本的node.js,就需要用到版本管理工具nvm。 ```bash $ git clone https://github.com/creationix/nvm.git ~/.nvm $ source ~/.nvm/nvm.sh ``` 安裝以后,nvm的執行腳本,每次使用前都要激活,建議將其加入~/.bashrc文件(假定使用Bash)。激活后,就可以安裝指定版本的Node。 ```bash # 安裝最新版本 $ nvm install node # 安裝指定版本 $ nvm install 0.12.1 # 使用已安裝的最新版本 $ nvm use node # 使用指定版本的node $ nvm use 0.12 ``` nvm也允許進入指定版本的REPL環境。 ```bash $ nvm run 0.12 ``` 如果在項目根目錄下新建一個.nvmrc文件,將版本號寫入其中,就只輸入`nvm use`命令即可,不再需要附加版本號。 下面是其他經常用到的命令。 ```bash # 查看本地安裝的所有版本 $ nvm ls # 查看服務器上所有可供安裝的版本。 $ nvm ls-remote # 退出已經激活的nvm,使用deactivate命令。 $ nvm deactivate ``` ### 基本用法 安裝完成后,運行node.js程序,就是使用node命令讀取JavaScript腳本。 當前目錄的`demo.js`腳本文件,可以這樣執行。 ```bash $ node demo # 或者 $ node demo.js ``` 使用`-e`參數,可以執行代碼字符串。 ```bash $ node -e 'console.log("Hello World")' Hello World ``` ### REPL環境 在命令行鍵入node命令,后面沒有文件名,就進入一個Node.js的REPL環境(Read–eval–print loop,"讀取-求值-輸出"循環),可以直接運行各種JavaScript命令。 ```bash $ node > 1+1 2 > ``` 如果使用參數 --use_strict,則REPL將在嚴格模式下運行。 ```bash $ node --use_strict ``` REPL是Node.js與用戶互動的shell,各種基本的shell功能都可以在里面使用,比如使用上下方向鍵遍歷曾經使用過的命令。 特殊變量下劃線(_)表示上一個命令的返回結果。 ```bash > 1 + 1 2 > _ + 1 3 ``` 在REPL中,如果運行一個表達式,會直接在命令行返回結果。如果運行一條語句,就不會有任何輸出,因為語句沒有返回值。 ```bash > x = 1 1 > var x = 1 ``` 上面代碼的第二條命令,沒有顯示任何結果。因為這是一條語句,不是表達式,所以沒有返回值。 ### 異步操作 Node采用V8引擎處理JavaScript腳本,最大特點就是單線程運行,一次只能運行一個任務。這導致Node大量采用異步操作(asynchronous opertion),即任務不是馬上執行,而是插在任務隊列的尾部,等到前面的任務運行完后再執行。 由于這種特性,某一個任務的后續操作,往往采用回調函數(callback)的形式進行定義。 ```javascript var isTrue = function(value, callback) { if (value === true) { callback(null, "Value was true."); } else { callback(new Error("Value is not true!")); } } ``` 上面代碼就把進一步的處理,交給回調函數callback。 Node約定,如果某個函數需要回調函數作為參數,則回調函數是最后一個參數。另外,回調函數本身的第一個參數,約定為上一步傳入的錯誤對象。 ```javascript var callback = function (error, value) { if (error) { return console.log(error); } console.log(value); } ``` 上面代碼中,callback的第一個參數是Error對象,第二個參數才是真正的數據參數。這是因為回調函數主要用于異步操作,當回調函數運行時,前期的操作早結束了,錯誤的執行棧早就不存在了,傳統的錯誤捕捉機制try...catch對于異步操作行不通,所以只能把錯誤交給回調函數處理。 ```javascript try { db.User.get(userId, function(err, user) { if(err) { throw err } // ... }) } catch(e) { console.log(‘Oh no!’); } ``` 上面代碼中,db.User.get方法是一個異步操作,等到拋出錯誤時,可能它所在的try...catch代碼塊早就運行結束了,這會導致錯誤無法被捕捉。所以,Node統一規定,一旦異步操作發生錯誤,就把錯誤對象傳遞到回調函數。 如果沒有發生錯誤,回調函數的第一個參數就傳入null。這種寫法有一個很大的好處,就是說只要判斷回調函數的第一個參數,就知道有沒有出錯,如果不是null,就肯定出錯了。另外,這樣還可以層層傳遞錯誤。 ```javascript if(err) { // 除了放過No Permission錯誤意外,其他錯誤傳給下一個回調函數 if(!err.noPermission) { return next(err); } } ``` ### 全局對象和全局變量 Node提供以下幾個全局對象,它們是所有模塊都可以調用的。 - **global**:表示Node所在的全局環境,類似于瀏覽器的window對象。需要注意的是,如果在瀏覽器中聲明一個全局變量,實際上是聲明了一個全局對象的屬性,比如`var x = 1`等同于設置`window.x = 1`,但是Node不是這樣,至少在模塊中不是這樣(REPL環境的行為與瀏覽器一致)。在模塊文件中,聲明`var x = 1`,該變量不是`global`對象的屬性,`global.x`等于undefined。這是因為模塊的全局變量都是該模塊私有的,其他模塊無法取到。 - **process**:該對象表示Node所處的當前進程,允許開發者與該進程互動。 - **console**:指向Node內置的console模塊,提供命令行環境中的標準輸入、標準輸出功能。 Node還提供一些全局函數。 - **setTimeout()**:用于在指定毫秒之后,運行回調函數。實際的調用間隔,還取決于系統因素。間隔的毫秒數在1毫秒到2,147,483,647毫秒(約24.8天)之間。如果超過這個范圍,會被自動改為1毫秒。該方法返回一個整數,代表這個新建定時器的編號。 - **clearTimeout()**:用于終止一個setTimeout方法新建的定時器。 - **setInterval()**:用于每隔一定毫秒調用回調函數。由于系統因素,可能無法保證每次調用之間正好間隔指定的毫秒數,但只會多于這個間隔,而不會少于它。指定的毫秒數必須是1到2,147,483,647(大約24.8天)之間的整數,如果超過這個范圍,會被自動改為1毫秒。該方法返回一個整數,代表這個新建定時器的編號。 - **clearInterval()**:終止一個用setInterval方法新建的定時器。 - **require()**:用于加載模塊。 - **Buffer()**:用于操作二進制數據。 Node提供兩個全局變量,都以兩個下劃線開頭。 - `__filename`:指向當前運行的腳本文件名。 - `__dirname`:指向當前運行的腳本所在的目錄。 除此之外,還有一些對象實際上是模塊內部的局部變量,指向的對象根據模塊不同而不同,但是所有模塊都適用,可以看作是偽全局變量,主要為module, module.exports, exports等。 ## 模塊化結構 ### 概述 Node.js采用模塊化結構,按照[CommonJS規范](http://wiki.commonjs.org/wiki/CommonJS)定義和使用模塊。模塊與文件是一一對應關系,即加載一個模塊,實際上就是加載對應的一個模塊文件。 require命令用于指定加載模塊,加載時可以省略腳本文件的后綴名。 ```javascript var circle = require('./circle.js'); // 或者 var circle = require('./circle'); ``` require方法的參數是模塊文件的名字。它分成兩種情況,第一種情況是參數中含有文件路徑(比如上例),這時路徑是相對于當前腳本所在的目錄,第二種情況是參數中不含有文件路徑,這時Node到模塊的安裝目錄,去尋找已安裝的模塊(比如下例)。 ```javascript var bar = require('bar'); ``` 有時候,一個模塊本身就是一個目錄,目錄中包含多個文件。這時候,Node在package.json文件中,尋找main屬性所指明的模塊入口文件。 ```javascript { "name" : "bar", "main" : "./lib/bar.js" } ``` 上面代碼中,模塊的啟動文件為lib子目錄下的bar.js。當使用`require('bar')`命令加載該模塊時,實際上加載的是`./node_modules/bar/lib/bar.js`文件。下面寫法會起到同樣效果。 ```javascript var bar = require('bar/lib/bar.js') ``` 如果模塊目錄中沒有package.json文件,node.js會嘗試在模塊目錄中尋找index.js或index.node文件進行加載。 模塊一旦被加載以后,就會被系統緩存。如果第二次還加載該模塊,則會返回緩存中的版本,這意味著模塊實際上只會執行一次。如果希望模塊執行多次,則可以讓模塊返回一個函數,然后多次調用該函數。 ### 核心模塊 如果只是在服務器運行JavaScript代碼,用處并不大,因為服務器腳本語言已經有很多種了。Node.js的用處在于,它本身還提供了一系列功能模塊,與操作系統互動。這些核心的功能模塊,不用安裝就可以使用,下面是它們的清單。 - **http**:提供HTTP服務器功能。 - **url**:解析URL。 - **fs**:與文件系統交互。 - **querystring**:解析URL的查詢字符串。 - **child_process**:新建子進程。 - **util**:提供一系列實用小工具。 - **path**:處理文件路徑。 - **crypto**:提供加密和解密功能,基本上是對OpenSSL的包裝。 上面這些核心模塊,源碼都在Node的lib子目錄中。為了提高運行速度,它們安裝時都會被編譯成二進制文件。 核心模塊總是最優先加載的。如果你自己寫了一個HTTP模塊,`require('http')`加載的還是核心模塊。 ### 自定義模塊 Node模塊采用CommonJS規范。只要符合這個規范,就可以自定義模塊。 下面是一個最簡單的模塊,假定新建一個foo.js文件,寫入以下內容。 ```javascript // foo.js module.exports = function(x) { console.log(x); }; ``` 上面代碼就是一個模塊,它通過module.exports變量,對外輸出一個方法。 這個模塊的使用方法如下。 ```javascript // index.js var m = require('./foo'); m("這是自定義模塊"); ``` 上面代碼通過require命令加載模塊文件foo.js(后綴名省略),將模塊的對外接口輸出到變量m,然后調用m。這時,在命令行下運行index.js,屏幕上就會輸出“這是自定義模塊”。 ```bash $ node index 這是自定義模塊 ``` module變量是整個模塊文件的頂層變量,它的exports屬性就是模塊向外輸出的接口。如果直接輸出一個函數(就像上面的foo.js),那么調用模塊就是調用一個函數。但是,模塊也可以輸出一個對象。下面對foo.js進行改寫。 ```javascript // foo.js var out = new Object(); function p(string) { console.log(string); } out.print = p; module.exports = out; ``` 上面的代碼表示模塊輸出out對象,該對象有一個print屬性,指向一個函數。下面是這個模塊的使用方法。 ```javascript // index.js var m = require('./foo'); m.print("這是自定義模塊"); ``` 上面代碼表示,由于具體的方法定義在模塊的print屬性上,所以必須顯式調用print屬性。 ## 異常處理 Node是單線程運行環境,一旦拋出的異常沒有被捕獲,就會引起整個進程的崩潰。所以,Node的異常處理對于保證系統的穩定運行非常重要。 一般來說,Node有三種方法,傳播一個錯誤。 - 使用throw語句拋出一個錯誤對象,即拋出異常。 - 將錯誤對象傳遞給回調函數,由回調函數負責發出錯誤。 - 通過EventEmitter接口,發出一個error事件。 ### try...catch結構 最常用的捕獲異常的方式,就是使用try...catch結構。但是,這個結構無法捕獲異步運行的代碼拋出的異常。 ```javascript try { process.nextTick(function () { throw new Error("error"); }); } catch (err) { //can not catch it console.log(err); } try { setTimeout(function(){ throw new Error("error"); },1) } catch (err) { //can not catch it console.log(err); } ``` 上面代碼分別用process.nextTick和setTimeout方法,在下一輪事件循環拋出兩個異常,代表異步操作拋出的錯誤。它們都無法被catch代碼塊捕獲,因此catch代碼塊所在的那部分已經運行結束了。 一種解決方法是將錯誤捕獲代碼,也放到異步執行。 ```javascript function async(cb, err) { setTimeout(function() { try { if (true) throw new Error("woops!"); else cb("done"); } catch(e) { err(e); } }, 2000) } async(function(res) { console.log("received:", res); }, function(err) { console.log("Error: async threw an exception:", err); }); // Error: async threw an exception: Error: woops! ``` 上面代碼中,async函數異步拋出的錯誤,可以同樣部署在異步的catch代碼塊捕獲。 這兩種處理方法都不太理想。一般來說,Node只在很少場合才用try/catch語句,比如使用`JSON.parse`解析JSON文本。 ### 回調函數 Node采用的方法,是將錯誤對象作為第一個參數,傳入回調函數。這樣就避免了捕獲代碼與發生錯誤的代碼不在同一個時間段的問題。 ```javascript fs.readFile('/foo.txt', function(err, data) { if (err !== null) throw err; console.log(data); }); ``` 上面代碼表示,讀取文件`foo.txt`是一個異步操作,它的回調函數有兩個參數,第一個是錯誤對象,第二個是讀取到的文件數據。如果第一個參數不是null,就意味著發生錯誤,后面代碼也就不再執行了。 下面是一個完整的例子。 ```javascript function async2(continuation) { setTimeout(function() { try { var res = 42; if (true) throw new Error("woops!"); else continuation(null, res); // pass 'null' for error } catch(e) { continuation(e, null); } }, 2000); } async2(function(err, res) { if (err) console.log("Error: (cps) failed:", err); else console.log("(cps) received:", res); }); // Error: (cps) failed: woops! ``` 上面代碼中,async2函數的回調函數的第一個參數就是一個錯誤對象,這是為了處理異步操作拋出的錯誤。 ### EventEmitter接口的error事件 發生錯誤的時候,也可以用EventEmitter接口拋出error事件。 ```javascript var EventEmitter = require('events').EventEmitter; var emitter = new EventEmitter(); emitter.emit('error', new Error('something bad happened')); ``` 使用上面的代碼必須小心,因為如果沒有對error事件部署監聽函數,會導致整個應用程序崩潰。所以,一般總是必須同時部署下面的代碼。 ```javascript emitter.on('error', function(err) { console.error('出錯:' + err.message); }); ``` ### uncaughtException事件 當一個異常未被捕獲,就會觸發uncaughtException事件,可以對這個事件注冊回調函數,從而捕獲異常。 ```javascript process.on('uncaughtException', function(err) { console.error('Error caught in uncaughtException event:', err); }); try { setTimeout(function(){ throw new Error("error"); },1) } catch (err) { //can not catch it console.log(err); } ``` 只要給uncaughtException配置了回調,Node進程不會異常退出,但異常發生的上下文已經丟失,無法給出異常發生的詳細信息。而且,異常可能導致Node不能正常進行內存回收,出現內存泄露。所以,當uncaughtException觸發后,最好記錄錯誤日志,然后結束Node進程。 ```javascript process.on('uncaughtException', function(err) { logger(err); process.exit(1); }); ``` ### unhandledRejection事件 iojs有一個unhandledRejection事件,用來監聽沒有捕獲的Promise對象的rejected狀態。 ```javascript var promise = new Promise(function(resolve, reject) { reject(new Error("Broken.")); }); promise.then(function(result) { console.log(result); }) ``` 上面代碼中,promise的狀態變為rejected,并且拋出一個錯誤。但是,不會有任何反應,因為沒有設置任何處理函數。 只要監聽unhandledRejection事件,就能解決這個問題。 ```javascript process.on('unhandledRejection', function (err, p) { console.error(err.stack); }) ``` 需要注意的是,unhandledRejection事件的監聽函數有兩個參數,第一個是錯誤對象,第二個是產生錯誤的promise對象。這可以提供很多有用的信息。 ```javascript var http = require('http'); http.createServer(function (req, res) { var promise = new Promise(function(resolve, reject) { reject(new Error("Broken.")) }) p.info = {url: req.url} }).listen(8080) process.on('unhandledRejection', function (err, p) { if (p.info && p.info.url) { console.log('Error in URL', p.info.url) } console.error(err.stack) }) ``` 上面代碼會在出錯時,輸出用戶請求的網址。 ```javascript Error in URL /testurl Error: Broken. at /Users/mikeal/tmp/test.js:9:14 at Server.<anonymous> (/Users/mikeal/tmp/test.js:4:17) at emitTwo (events.js:87:13) at Server.emit (events.js:169:7) at HTTPParser.parserOnIncoming [as onIncoming] (_http_server.js:471:12) at HTTPParser.parserOnHeadersComplete (_http_common.js:88:23) at Socket.socketOnData (_http_server.js:322:22) at emitOne (events.js:77:13) at Socket.emit (events.js:166:7) at readableAddChunk (_stream_readable.js:145:16) ``` ## 命令行腳本 node腳本可以作為命令行腳本使用。 ```bash $ node foo.js ``` 上面代碼執行了foo.js腳本文件。 foo.js文件的第一行,如果加入了解釋器的位置,就可以將其作為命令行工具直接調用。 ```bash #!/usr/bin/env node ``` 調用前,需更改文件的執行權限。 ```bash $ chmod u+x foo.js $ ./foo.js arg1 arg2 ... ``` 作為命令行腳本時,`console.log`用于輸出內容到標準輸出,`process.stdin`用于讀取標準輸入,`child_process.exec()`用于執行一個shell命令。 <h2 id="12.2">module</h2> ## 概述 Node程序由許多個模塊組成,每個模塊就是一個文件。Node模塊采用了CommonJS規范。 根據CommonJS規范,一個單獨的文件就是一個模塊。每一個模塊都是一個單獨的作用域,也就是說,在一個文件定義的變量(還包括函數和類),都是私有的,對其他文件是不可見的。 ```javascript // example.js var x = 5; var addX = function(value) { return value + x; }; ``` 上面代碼中,變量`x`和函數`addX`,是當前文件`example.js`私有的,其他文件不可見。 如果想在多個文件分享變量,必須定義為`global`對象的屬性。 ```javascript global.warning = true; ``` 上面代碼的`warning`變量,可以被所有文件讀取。當然,這樣寫法是不推薦的。 CommonJS規定,每個文件的對外接口是`module.exports`對象。這個對象的所有屬性和方法,都可以被其他文件導入。 ```javascript var x = 5; var addX = function(value) { return value + x; }; module.exports.x = x; module.exports.addX = addX; ``` 上面代碼通過`module.exports`對象,定義對外接口,輸出變量`x`和函數`addX`。`module.exports`對象是可以被其他文件導入的,它其實就是文件內部與外部通信的橋梁。 `require`方法用于在其他文件加載這個接口,具體用法參見《Require命令》的部分。 ```javascript var example = require('./example.js'); console.log(example.x); // 5 console.log(example.addX(1)); // 6 ``` CommonJS模塊的特點如下。 - 所有代碼都運行在模塊作用域,不會污染全局作用域。 - 模塊可以多次加載,但是只會在第一次加載時運行一次,然后運行結果就被緩存了,以后再加載,就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存。 - 模塊加載的順序,按照其在代碼中出現的順序。 ## module對象 Node內部提供一個`Module`構建函數。所有模塊都是`Module`的實例。 ```javascript function Module(id, parent) { this.id = id; this.exports = {}; this.parent = parent; // ... ``` 每個模塊內部,都有一個`module`對象,代表當前模塊。它有以下屬性。 - `module.id` 模塊的識別符,通常是帶有絕對路徑的模塊文件名。 - `module.filename` 模塊的文件名,帶有絕對路徑。 - `module.loaded` 返回一個布爾值,表示模塊是否已經完成加載。 - `module.parent` 返回一個對象,表示調用該模塊的模塊。 - `module.children` 返回一個數組,表示該模塊要用到的其他模塊。 - `module.exports` 表示模塊對外輸出的值。 下面是一個示例文件,最后一行輸出module變量。 ```javascript // example.js var jquery = require('jquery'); exports.$ = jquery; console.log(module); ``` 執行這個文件,命令行會輸出如下信息。 ```javascript { id: '.', exports: { '$': [Function] }, parent: null, filename: '/path/to/example.js', loaded: false, children: [ { id: '/path/to/node_modules/jquery/dist/jquery.js', exports: [Function], parent: [Circular], filename: '/path/to/node_modules/jquery/dist/jquery.js', loaded: true, children: [], paths: [Object] } ], paths: [ '/home/user/deleted/node_modules', '/home/user/node_modules', '/home/node_modules', '/node_modules' ] } ``` 如果在命令行下調用某個模塊,比如`node something.js`,那么`module.parent`就是`undefined`。如果是在腳本之中調用,比如`require('./something.js')`,那么`module.parent`就是調用它的模塊。利用這一點,可以判斷當前模塊是否為入口腳本。 ```javascript if (!module.parent) { // ran with `node something.js` app.listen(8088, function() { console.log('app listening on port 8088'); }) } else { // used with `require('/.something.js')` module.exports = app; } ``` ### module.exports屬性 `module.exports`屬性表示當前模塊對外輸出的接口,其他文件加載該模塊,實際上就是讀取`module.exports`變量。 ```javascript var EventEmitter = require('events').EventEmitter; module.exports = new EventEmitter(); setTimeout(function() { module.exports.emit('ready'); }, 1000); ``` 上面模塊會在加載后1秒后,發出ready事件。其他文件監聽該事件,可以寫成下面這樣。 ```javascript var a = require('./a'); a.on('ready', function() { console.log('module a is ready'); }); ``` ### exports變量 為了方便,Node為每個模塊提供一個exports變量,指向module.exports。這等同在每個模塊頭部,有一行這樣的命令。 ```javascript var exports = module.exports; ``` 造成的結果是,在對外輸出模塊接口時,可以向exports對象添加方法。 ```javascript exports.area = function (r) { return Math.PI * r * r; }; exports.circumference = function (r) { return 2 * Math.PI * r; }; ``` 注意,不能直接將exports變量指向一個值,因為這樣等于切斷了`exports`與`module.exports`的聯系。 ```javascript exports = function(x) {console.log(x)}; ``` 上面這樣的寫法是無效的,因為`exports`不再指向`module.exports`了。 下面的寫法也是無效的。 ```javascript exports.hello = function() { return 'hello'; }; module.exports = 'Hello world'; ``` 上面代碼中,`hello`函數是無法對外輸出的,因為`module.exports`被重新賦值了。 這意味著,如果一個模塊的對外接口,就是一個單一的值,不能使用`exports`輸出,只能使用`module.exports`輸出。 ```javascript module.exports = function (x){ console.log(x);}; ``` 如果你覺得,`exports`與`module.exports`之間的區別很難分清,一個簡單的處理方法,就是放棄使用`exports`,只使用`module.exports`。 ## AMD規范與CommonJS規范的兼容性 CommonJS規范加載模塊是同步的,也就是說,只有加載完成,才能執行后面的操作。AMD規范則是非同步加載模塊,允許指定回調函數。由于Node.js主要用于服務器編程,模塊文件一般都已經存在于本地硬盤,所以加載起來比較快,不用考慮非同步加載的方式,所以CommonJS規范比較適用。但是,如果是瀏覽器環境,要從服務器端加載模塊,這時就必須采用非同步模式,因此瀏覽器端一般采用AMD規范。 AMD規范使用define方法定義模塊,下面就是一個例子: ```javascript define(['package/lib'], function(lib){ function foo(){ lib.log('hello world!'); } return { foo: foo }; }); ``` AMD規范允許輸出的模塊兼容CommonJS規范,這時`define`方法需要寫成下面這樣: ```javascript define(function (require, exports, module){ var someModule = require("someModule"); var anotherModule = require("anotherModule"); someModule.doTehAwesome(); anotherModule.doMoarAwesome(); exports.asplode = function (){ someModule.doTehAwesome(); anotherModule.doMoarAwesome(); }; }); ``` ## require命令 ### 基本用法 Node使用CommonJS模塊規范,內置的`require`命令用于加載模塊文件。 `require`命令的基本功能是,讀入并執行一個JavaScript文件,然后返回該模塊的exports對象。如果沒有發現指定模塊,會報錯。 ```javascript // example.js var invisible = function () { console.log("invisible"); } exports.message = "hi"; exports.say = function () { console.log(message); } ``` 運行下面的命令,可以輸出exports對象。 ```javascript var example = require('./example.js'); example // { // message: "hi", // say: [Function] // } ``` 如果模塊輸出的是一個函數,那就不能定義在exports對象上面,而要定義在`module.exports`變量上面。 ```javascript module.exports = function () { console.log("hello world") } require('./example2.js')() ``` 上面代碼中,require命令調用自身,等于是執行`module.exports`,因此會輸出 hello world。 ### 加載規則 `require`命令用于加載文件,后綴名默認為`.js`。 ```javascript var foo = require('foo'); // 等同于 var foo = require('foo.js'); ``` 根據參數的不同格式,`require`命令去不同路徑尋找模塊文件。 (1)如果參數字符串以“/”開頭,則表示加載的是一個位于絕對路徑的模塊文件。比如,`require('/home/marco/foo.js')`將加載`/home/marco/foo.js`。 (2)如果參數字符串以“./”開頭,則表示加載的是一個位于相對路徑(跟當前執行腳本的位置相比)的模塊文件。比如,`require('./circle')`將加載當前腳本同一目錄的`circle.js`。 (3)如果參數字符串不以“./“或”/“開頭,則表示加載的是一個默認提供的核心模塊(位于Node的系統安裝目錄中),或者一個位于各級node_modules目錄的已安裝模塊(全局安裝或局部安裝)。 舉例來說,腳本`/home/user/projects/foo.js`執行了`require('bar.js')`命令,Node會依次搜索以下文件。 - /usr/local/lib/node/bar.js - /home/user/projects/node_modules/bar.js - /home/user/node_modules/bar.js - /home/node_modules/bar.js - /node_modules/bar.js 這樣設計的目的是,使得不同的模塊可以將所依賴的模塊本地化。 (4)如果參數字符串不以“./“或”/“開頭,而且是一個路徑,比如`require('example-module/path/to/file')`,則將先找到`example-module`的位置,然后再以它為參數,找到后續路徑。 (5)如果指定的模塊文件沒有發現,Node會嘗試為文件名添加`.js`、`.json`、`.node`后,再去搜索。`.js`件會以文本格式的JavaScript腳本文件解析,`.json`文件會以JSON格式的文本文件解析,`.node`文件會以編譯后的二進制文件解析。 (6)如果想得到`require`命令加載的確切文件名,使用`require.resolve()`方法。 ### 目錄的加載規則 通常,我們會把相關的文件會放在一個目錄里面,便于組織。這時,最好為該目錄設置一個入口文件,讓`require`方法可以通過這個入口文件,加載整個目錄。 在目錄中放置一個`package.json`文件,并且將入口文件寫入`main`字段。下面是一個例子。 ```javascript // package.json { "name" : "some-library", "main" : "./lib/some-library.js" } ``` `require`發現參數字符串指向一個目錄以后,會自動查看該目錄的`package.json`文件,然后加載`main`字段指定的入口文件。如果`package.json`文件沒有`main`字段,或者根本就沒有`package.json`文件,則會加載該目錄下的`index.js`文件或`index.node`文件。 ### 模塊的緩存 第一次加載某個模塊時,Node會緩存該模塊。以后再加載該模塊,就直接從緩存取出該模塊的`module.exports`屬性。 ```javascript require('./example.js'); require('./example.js').message = "hello"; require('./example.js').message // "hello" ``` 上面代碼中,連續三次使用`require`命令,加載同一個模塊。第二次加載的時候,為輸出的對象添加了一個`message`屬性。但是第三次加載的時候,這個message屬性依然存在,這就證明`require`命令并沒有重新加載模塊文件,而是輸出了緩存。 如果想要多次執行某個模塊,可以讓該模塊輸出一個函數,然后每次`require`這個模塊的時候,重新執行一下輸出的函數。 所有緩存的模塊保存在`require.cache`之中,如果想刪除模塊的緩存,可以像下面這樣寫。 ```javascript // 刪除指定模塊的緩存 delete require.cache[moduleName]; // 刪除所有模塊的緩存 Object.keys(require.cache).forEach(function(key) { delete require.cache[key]; }) ``` 注意,緩存是根據絕對路徑識別模塊的,如果同樣的模塊名,但是保存在不同的路徑,`require`命令還是會重新加載該模塊。 ### 環境變量NODE_PATH Node執行一個腳本時,會先查看環境變量`NODE_PATH`。它是一組以冒號分隔的絕對路徑。在其他位置找不到指定模塊時,Node會去這些路徑查找。 可以將NODE_PATH添加到`.bashrc`。 ```javascript export NODE_PATH="/usr/local/lib/node" ``` 所以,如果遇到復雜的相對路徑,比如下面這樣。 ```javascript var myModule = require('../../../../lib/myModule'); ``` 有兩種解決方法,一是將該文件加入`node_modules`目錄,二是修改`NODE_PATH`環境變量,`package.json`文件可以采用下面的寫法。 ```javascript { "name": "node_path", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "NODE_PATH=lib node index.js" }, "author": "", "license": "ISC" } ``` `NODE_PATH`是歷史遺留下來的一個路徑解決方案,通常不應該使用,而應該使用`node_modules`目錄機制。 ### 模塊的循環加載 如果發生模塊的循環加載,即A加載B,B又加載A,則B將加載A的不完整版本。 ```javascript // a.js exports.x = 'a1'; console.log('a.js ', require('./b.js').x); exports.x = 'a2'; // b.js exports.x = 'b1'; console.log('b.js ', require('./a.js').x); exports.x = 'b2'; // main.js console.log('main.js ', require('./a.js').x); console.log('main.js ', require('./b.js').x); ``` 上面代碼是三個JavaScript文件。其中,a.js加載了b.js,而b.js又加載a.js。這時,Node返回a.js的不完整版本,所以執行結果如下。 ```bash $ node main.js b.js a1 a.js b2 main.js a2 main.js b2 ``` 修改main.js,再次加載a.js和b.js。 ```javascript // main.js console.log('main.js ', require('./a.js').x); console.log('main.js ', require('./b.js').x); console.log('main.js ', require('./a.js').x); console.log('main.js ', require('./b.js').x); ``` 執行上面代碼,結果如下。 ```bash $ node main.js b.js a1 a.js b2 main.js a2 main.js b2 main.js a2 main.js b2 ``` 上面代碼中,第二次加載a.js和b.js時,會直接從緩存讀取exports屬性,所以a.js和b.js內部的console.log語句都不會執行了。 ### require.main `require`方法有一個`main`屬性,可以用來判斷模塊是直接執行,還是被調用執行。 直接執行的時候(`node module.js`),`require.main`屬性指向模塊本身。 ```javascript require.main === module // true ``` 調用執行的時候(通過`require`加載該腳本執行),上面的表達式返回false。 ## 模塊的加載機制 CommonJS模塊的加載機制是,輸入的是被輸出的值的拷貝。也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。請看下面這個例子。 下面是一個模塊文件`lib.js`。 ```javascript // lib.js var counter = 3; function incCounter() { counter++; } module.exports = { counter: counter, incCounter: incCounter, }; ``` 上面代碼輸出內部變量`counter`和改寫這個變量的內部方法`incCounter`。 然后,加載上面的模塊。 ```javascript // main.js var counter = require('./lib').counter; var incCounter = require('./lib').incCounter; console.log(counter); // 3 incCounter(); console.log(counter); // 3 ``` 上面代碼說明,`counter`輸出以后,`lib.js`模塊內部的變化就影響不到`counter`了。 ### require的內部處理流程 `require`命令是CommonJS規范之中,用來加載其他模塊的命令。它其實不是一個全局命令,而是指向當前模塊的`module.require`命令,而后者又調用Node的內部命令`Module._load`。 ```javascript Module._load = function(request, parent, isMain) { // 1. 檢查 Module._cache,是否緩存之中有指定模塊 // 2. 如果緩存之中沒有,就創建一個新的Module實例 // 3. 將它保存到緩存 // 4. 使用 module.load() 加載指定的模塊文件, // 讀取文件內容之后,使用 module.compile() 執行文件代碼 // 5. 如果加載/解析過程報錯,就從緩存刪除該模塊 // 6. 返回該模塊的 module.exports }; ``` 上面的第4步,采用`module.compile()`執行指定模塊的腳本,邏輯如下。 ```javascript Module.prototype._compile = function(content, filename) { // 1. 生成一個require函數,指向module.require // 2. 加載其他輔助方法到require // 3. 將文件內容放到一個函數之中,該函數可調用 require // 4. 執行該函數 }; ``` 上面的第1步和第2步,`require`函數及其輔助方法主要如下。 - `require()`: 加載外部模塊 - `require.resolve()`:將模塊名解析到一個絕對路徑 - `require.main`:指向主模塊 - `require.cache`:指向所有緩存的模塊 - `require.extensions`:根據文件的后綴名,調用不同的執行函數 一旦`require`函數準備完畢,整個所要加載的腳本內容,就被放到一個新的函數之中,這樣可以避免污染全局環境。該函數的參數包括`require`、`module`、`exports`,以及其他一些參數。 ```javascript (function (exports, require, module, __filename, __dirname) { // YOUR CODE INJECTED HERE! }); ``` `Module._compile`方法是同步執行的,所以`Module._load`要等它執行完成,才會向用戶返回`module.exports`的值。 <h2 id="12.3">package.json文件</h2> ## 概述 每個項目的根目錄下面,一般都有一個`package.json`文件,定義了這個項目所需要的各種模塊,以及項目的配置信息(比如名稱、版本、許可證等元數據)。`npm install`命令根據這個配置文件,自動下載所需的模塊,也就是配置項目所需的運行和開發環境。 下面是一個最簡單的package.json文件,只定義兩項元數據:項目名稱和項目版本。 ```javascript { "name" : "xxx", "version" : "0.0.0", } ``` 上面代碼說明,`package.json`文件內部就是一個JSON對象,該對象的每一個成員就是當前項目的一項設置。比如`name`就是項目名稱,`version`是版本(遵守“大版本.次要版本.小版本”的格式)。 下面是一個更完整的package.json文件。 ```javascript { "name": "Hello World", "version": "0.0.1", "author": "張三", "description": "第一個node.js程序", "keywords":["node.js","javascript"], "repository": { "type": "git", "url": "https://path/to/url" }, "license":"MIT", "engines": {"node": "0.10.x"}, "bugs":{"url":"http://path/to/bug","email":"bug@example.com"}, "contributors":[{"name":"李四","email":"lisi@example.com"}], "scripts": { "start": "node index.js" }, "dependencies": { "express": "latest", "mongoose": "~3.8.3", "handlebars-runtime": "~1.0.12", "express3-handlebars": "~0.5.0", "MD5": "~1.2.0" }, "devDependencies": { "bower": "~1.2.8", "grunt": "~0.4.1", "grunt-contrib-concat": "~0.3.0", "grunt-contrib-jshint": "~0.7.2", "grunt-contrib-uglify": "~0.2.7", "grunt-contrib-clean": "~0.5.0", "browserify": "2.36.1", "grunt-browserify": "~1.3.0", } } ``` 下面詳細解釋package.json文件的各個字段。 ## scripts字段 `scripts`指定了運行腳本命令的npm命令行縮寫,比如start指定了運行`npm run start`時,所要執行的命令。 下面的設置指定了`npm run preinstall`、`npm run postinstall`、`npm run start`、`npm run test`時,所要執行的命令。 ```javascript "scripts": { "preinstall": "echo here it comes!", "postinstall": "echo there it goes!", "start": "node index.js", "test": "tap test/*.js" } ``` ## dependencies字段,devDependencies字段 `dependencies`字段指定了項目運行所依賴的模塊,`devDependencies`指定項目開發所需要的模塊。 它們都指向一個對象。該對象的各個成員,分別由模塊名和對應的版本要求組成,表示依賴的模塊及其版本范圍。 ```javascript { "devDependencies": { "browserify": "~13.0.0", "karma-browserify": "~5.0.1" } } ``` 對應的版本可以加上各種限定,主要有以下幾種: - **指定版本**:比如`1.2.2`,遵循“大版本.次要版本.小版本”的格式規定,安裝時只安裝指定版本。 - **波浪號(tilde)+指定版本**:比如`~1.2.2`,表示安裝1.2.x的最新版本(不低于1.2.2),但是不安裝1.3.x,也就是說安裝時不改變大版本號和次要版本號。 - **插入號(caret)+指定版本**:比如&#710;1.2.2,表示安裝1.x.x的最新版本(不低于1.2.2),但是不安裝2.x.x,也就是說安裝時不改變大版本號。需要注意的是,如果大版本號為0,則插入號的行為與波浪號相同,這是因為此時處于開發階段,即使是次要版本號變動,也可能帶來程序的不兼容。 - **latest**:安裝最新版本。 package.json文件可以手工編寫,也可以使用`npm init`命令自動生成。 ```bash $ npm init ``` 這個命令采用互動方式,要求用戶回答一些問題,然后在當前目錄生成一個基本的package.json文件。所有問題之中,只有項目名稱(name)和項目版本(version)是必填的,其他都是選填的。 有了package.json文件,直接使用npm install命令,就會在當前目錄中安裝所需要的模塊。 ```bash $ npm install ``` 如果一個模塊不在`package.json`文件之中,可以單獨安裝這個模塊,并使用相應的參數,將其寫入`package.json`文件之中。 ```bash $ npm install express --save $ npm install express --save-dev ``` 上面代碼表示單獨安裝express模塊,`--save`參數表示將該模塊寫入`dependencies`屬性,`--save-dev`表示將該模塊寫入`devDependencies`屬性。 ## peerDependencies 有時,你的項目和所依賴的模塊,都會同時依賴另一個模塊,但是所依賴的版本不一樣。比如,你的項目依賴A模塊和B模塊的1.0版,而A模塊本身又依賴B模塊的2.0版。 大多數情況下,這不構成問題,B模塊的兩個版本可以并存,同時運行。但是,有一種情況,會出現問題,就是這種依賴關系將暴露給用戶。 最典型的場景就是插件,比如A模塊是B模塊的插件。用戶安裝的B模塊是1.0版本,但是A插件只能和2.0版本的B模塊一起使用。這時,用戶要是將1.0版本的B的實例傳給A,就會出現問題。因此,需要一種機制,在模板安裝的時候提醒用戶,如果A和B一起安裝,那么B必須是2.0模塊。 `peerDependencies`字段,就是用來供插件指定其所需要的主工具的版本。 ```javascript { "name": "chai-as-promised", "peerDependencies": { "chai": "1.x" } } ``` 上面代碼指定,安裝`chai-as-promised`模塊時,主程序`chai`必須一起安裝,而且`chai`的版本必須是`1.x`。如果你的項目指定的依賴是`chai`的2.0版本,就會報錯。 注意,從npm 3.0版開始,`peerDependencies`不再會默認安裝了。 ## bin字段 bin項用來指定各個內部命令對應的可執行文件的位置。 ```javascript "bin": { "someTool": "./bin/someTool.js" } ``` 上面代碼指定,someTool 命令對應的可執行文件為 bin 子目錄下的 someTool.js。Npm會尋找這個文件,在`node_modules/.bin/`目錄下建立符號鏈接。在上面的例子中,someTool.js會建立符號鏈接`npm_modules/.bin/someTool`。由于`node_modules/.bin/`目錄會在運行時加入系統的PATH變量,因此在運行npm時,就可以不帶路徑,直接通過命令來調用這些腳本。 因此,像下面這樣的寫法可以采用簡寫。 ```javascript scripts: { start: './node_modules/someTool/someTool.js build' } // 簡寫為 scripts: { start: 'someTool build' } ``` 所有`node_modules/.bin/`目錄下的命令,都可以用`npm run [命令]`的格式運行。在命令行下,鍵入`npm run`,然后按tab鍵,就會顯示所有可以使用的命令。 ## main字段 `main`字段指定了加載該模塊時的入門文件,默認是模塊根目錄下面的`index.js`。 ## config字段 config字段用于向環境變量輸出值。 下面是一個package.json文件。 ```javascript { "name" : "foo", "config" : { "port" : "8080" }, "scripts" : { "start" : "node server.js" } } ``` 然后,在`server.js`腳本就可以引用config字段的值。 ```javascript http.createServer(...).listen(process.env.npm_package_config_port) ``` 用戶可以改變這個值。 ```bash $ npm config set foo:port 80 ``` ## 其他 ### browser字段 browser指定該模板供瀏覽器使用的版本。Browserify這樣的瀏覽器打包工具,通過它就知道該打包那個文件。 ```javascript "browser": { "tipso": "./node_modules/tipso/src/tipso.js" }, ``` ### engines字段 engines指明了該項目所需要的node.js版本。 ### man字段 man用來指定當前模塊的man文檔的位置。 ```javascript "man" :[ "./doc/calc.1" ] ``` ### preferGlobal字段 preferGlobal的值是布爾值,表示當用戶不將該模塊安裝為全局模塊時(即不用--global參數),要不要顯示警告,表示該模塊的本意就是安裝為全局模塊。 ### style字段 style指定供瀏覽器使用時,樣式文件所在的位置。樣式文件打包工具parcelify,通過它知道樣式文件的打包位置。 ```javascript "style": [ "./node_modules/tipso/src/tipso.css" ] ``` <h2 id="12.4">npm模塊管理器</h2> ## 簡介 npm有兩層含義。一層含義是Node.js的開放式模塊登記和管理系統,網址為[http://npmjs.org](http://npmjs.org)。另一層含義是Node.js默認的模塊管理器,是一個命令行下的軟件,用來安裝和管理node模塊。 npm不需要單獨安裝。在安裝node的時候,會連帶一起安裝npm。但是,node附帶的npm可能不是最新版本,最好用下面的命令,更新到最新版本。 ```bash $ npm install npm@latest -g ``` 上面的命令之所以最后一個參數是npm,是因為npm本身也是Node.js的一個模塊。 Node安裝完成后,可以用下面的命令,查看一下npm的幫助文件。 ```bash # npm命令列表 $ npm help # 各個命令的簡單用法 $ npm -l ``` 下面的命令分別查看npm的版本和配置。 ```bash $ npm -v $ npm config list -l ``` ## npm init `npm init`用來初始化生成一個新的`package.json`文件。它會向用戶提問一系列問題,如果你覺得不用修改默認配置,一路回車就可以了。 如果使用了`-f`(代表force)、`-y`(代表yes),則跳過提問階段,直接生成一個新的`package.json`文件。 ```bash $ npm init -y ``` ## npm set `npm set`用來設置環境變量。 ```bash $ npm set init-author-name 'Your name' $ npm set init-author-email 'Your email' $ npm set init-author-url 'http://yourdomain.com' $ npm set init-license 'MIT' ``` 上面命令等于為`npm init`設置了默認值,以后執行`npm init`的時候,`package.json`的作者姓名、郵件、主頁、許可證字段就會自動寫入預設的值。這些信息會存放在用戶主目錄的` ~/.npmrc`文件,使得用戶不用每個項目都輸入。如果某個項目有不同的設置,可以針對該項目運行`npm config`。 ```bash $ npm set save-exact true ``` 上面命令設置加入模塊時,`package.json`將記錄模塊的確切版本,而不是一個可選的版本范圍。 ## npm info `npm info`命令可以查看每個模塊的具體信息。比如,查看underscore模塊的信息。 ```bash $ npm info underscore { name: 'underscore', description: 'JavaScript\'s functional programming helper library.', 'dist-tags': { latest: '1.5.2', stable: '1.5.2' }, repository: { type: 'git', url: 'git://github.com/jashkenas/underscore.git' }, homepage: 'http://underscorejs.org', main: 'underscore.js', version: '1.5.2', devDependencies: { phantomjs: '1.9.0-1' }, licenses: { type: 'MIT', url: 'https://raw.github.com/jashkenas/underscore/master/LICENSE' }, files: [ 'underscore.js', 'underscore-min.js', 'LICENSE' ], readmeFilename: 'README.md'} ``` 上面命令返回一個JavaScript對象,包含了underscore模塊的詳細信息。這個對象的每個成員,都可以直接從info命令查詢。 ```bash $ npm info underscore description JavaScript's functional programming helper library. $ npm info underscore homepage http://underscorejs.org $ npm info underscore version 1.5.2 ``` ## npm search `npm search`命令用于搜索npm倉庫,它后面可以跟字符串,也可以跟正則表達式。 ```bash $ npm search <搜索詞> ``` 下面是一個例子。 ```bash $ npm search node-gyp // NAME DESCRIPTION // autogypi Autogypi handles dependencies for node-gyp projects. // grunt-node-gyp Run node-gyp commands from Grunt. // gyp-io Temporary solution to let node-gyp run `rebuild` under… // ... ``` ## npm list `npm list`命令以樹型結構列出當前項目安裝的所有模塊,以及它們依賴的模塊。 ```bash $ npm list ``` 加上global參數,會列出全局安裝的模塊。 ```bash $ npm list -global ``` `npm list`命令也可以列出單個模塊。 ```bash $ npm list underscore ``` ## npm install ### 基本用法 Node模塊采用`npm install`命令安裝。 每個模塊可以“全局安裝”,也可以“本地安裝”。“全局安裝”指的是將一個模塊安裝到系統目錄中,各個項目都可以調用。一般來說,全局安裝只適用于工具模塊,比如npm和grunt。“本地安裝”指的是將一個模塊下載到當前項目的`node_modules`子目錄,然后只有在項目目錄之中,才能調用這個模塊。 ```bash # 本地安裝 $ npm install <package name> # 全局安裝 $ sudo npm install -global <package name> $ sudo npm install -g <package name> ``` `npm install`也支持直接輸入Github代碼庫地址。 ```bash $ npm install git://github.com/package/path.git $ npm install git://github.com/package/path.git#0.1.0 ``` 安裝之前,`npm install`會先檢查,`node_modules`目錄之中是否已經存在指定模塊。如果存在,就不再重新安裝了,即使遠程倉庫已經有了一個新版本,也是如此。 如果你希望,一個模塊不管是否安裝過,npm 都要強制重新安裝,可以使用`-f`或`--force`參數。 ```bash $ npm install <packageName> --force ``` 如果你希望,所有模塊都要強制重新安裝,那就刪除`node_modules`目錄,重新執行`npm install`。 ```bash $ rm -rf node_modules $ npm install ``` ### 安裝不同版本 install命令總是安裝模塊的最新版本,如果要安裝模塊的特定版本,可以在模塊名后面加上@和版本號。 ```bash $ npm install sax@latest $ npm install sax@0.1.1 $ npm install sax@">=0.1.0 <0.2.0" ``` 如果使用`--save-exact`參數,會在package.json文件指定安裝模塊的確切版本。 ```bash $ npm install readable-stream --save --save-exact ``` install命令可以使用不同參數,指定所安裝的模塊屬于哪一種性質的依賴關系,即出現在packages.json文件的哪一項中。 - --save:模塊名將被添加到dependencies,可以簡化為參數`-S`。 - --save-dev: 模塊名將被添加到devDependencies,可以簡化為參數`-D`。 ```bash $ npm install sax --save $ npm install node-tap --save-dev # 或者 $ npm install sax -S $ npm install node-tap -D ``` 如果要安裝beta版本的模塊,需要使用下面的命令。 ```bash # 安裝最新的beta版 $ npm install <module-name>@beta (latest beta) # 安裝指定的beta版 $ npm install <module-name>@1.3.1-beta.3 ``` `npm install`默認會安裝dependencies字段和devDependencies字段中的所有模塊,如果使用production參數,可以只安裝dependencies字段的模塊。 ```bash $ npm install --production # 或者 $ NODE_ENV=production npm install ``` 一旦安裝了某個模塊,就可以在代碼中用require命令調用這個模塊。 ```javascript var backbone = require('backbone') console.log(backbone.VERSION) ``` ## 避免系統權限 默認情況下,Npm全局模塊都安裝在系統目錄(比如`/usr/local/lib/`),普通用戶沒有寫入權限,需要用到`sudo`命令。這不是很方便,我們可以在沒有root權限的情況下,安裝全局模塊。 首先,在主目錄下新建配置文件`.npmrc`,然后在該文件中將`prefix`變量定義到主目錄下面。 ```bash prefix = /home/yourUsername/npm ``` 然后在主目錄下新建`npm`子目錄。 ```bash $ mkdir ~/npm ``` 此后,全局安裝的模塊都會安裝在這個子目錄中,npm也會到`~/npm/bin`目錄去尋找命令。 最后,將這個路徑在`.bash_profile`文件(或`.bashrc`文件)中加入PATH變量。 ```bash export PATH=~/npm/bin:$PATH ``` ## npm update,npm uninstall `npm update`命令可以更新本地安裝的模塊。 ```bash # 升級當前項目的指定模塊 $ npm update [package name] # 升級全局安裝的模塊 $ npm update -global [package name] ``` 它會先到遠程倉庫查詢最新版本,然后查詢本地版本。如果本地版本不存在,或者遠程版本較新,就會安裝。 使用`-S`或`--save`參數,可以在安裝的時候更新`package.json`里面模塊的版本號。 ```javascript // 更新之前的package.json dependencies: { dep1: "^1.1.1" } // 更新之后的package.json dependencies: { dep1: "^1.2.2" } ``` 注意,從npm v2.6.1 開始,`npm update`只更新頂層模塊,而不更新依賴的依賴,以前版本是遞歸更新的。如果想取到老版本的效果,要使用下面的命令。 ```bash $ npm --depth 9999 update ``` `npm uninstall`命令,卸載已安裝的模塊。 ```bash $ npm uninstall [package name] # 卸載全局模塊 $ npm uninstall [package name] -global ``` ## npm run npm不僅可以用于模塊管理,還可以用于執行腳本。`package.json`文件有一個`scripts`字段,可以用于指定腳本命令,供npm直接調用。 ```javascript { "name": "myproject", "devDependencies": { "jshint": "latest", "browserify": "latest", "mocha": "latest" }, "scripts": { "lint": "jshint **.js", "test": "mocha test/" } } ``` 上面代碼中,`scripts`字段指定了兩項命令`lint`和`test`。命令行輸入`npm run-script lint`或者`npm run lint`,就會執行`jshint **.js`,輸入`npm run-script test`或者`npm run test`,就會執行`mocha test/`。`npm run`是`npm run-script`的縮寫,一般都使用前者,但是后者可以更好地反應這個命令的本質。 `npm run`命令會自動在環境變量`$PATH`添加`node_modules/.bin`目錄,所以`scripts`字段里面調用命令時不用加上路徑,這就避免了全局安裝NPM模塊。 npm內置了兩個命令簡寫,`npm test`等同于執行`npm run test`,`npm start`等同于執行`npm run start`。 `npm run`會創建一個Shell,執行指定的命令,并臨時將`node_modules/.bin`加入PATH變量,這意味著本地模塊可以直接運行。 舉例來說,你執行ESLint的安裝命令。 ```bash $ npm i eslint --save-dev ``` 運行上面的命令以后,會產生兩個結果。首先,ESLint被安裝到當前目錄的`node_modules`子目錄;其次,`node_modules/.bin`目錄會生成一個符號鏈接`node_modules/.bin/eslint`,指向ESLint模塊的可執行腳本。 然后,你就可以在`package.json`的`script`屬性里面,不帶路徑的引用`eslint`這個腳本。 ```javascript { "name": "Test Project", "devDependencies": { "eslint": "^1.10.3" }, "scripts": { "lint": "eslint ." } } ``` 等到運行`npm run lint`的時候,它會自動執行`./node_modules/.bin/eslint .`。 如果直接運行`npm run`不給出任何參數,就會列出`scripts`屬性下所有命令。 ```bash $ npm run Available scripts in the user-service package: lint jshint **.js test mocha test/ ``` 下面是另一個`package.json`文件的例子。 ```javascript "scripts": { "watch": "watchify client/main.js -o public/app.js -v", "build": "browserify client/main.js -o public/app.js", "start": "npm run watch & nodemon server.js", "test": "node test/all.js" }, ``` 上面代碼在`scripts`項,定義了四個別名,每個別名都有對應的腳本命令。 ```bash $ npm run watch $ npm run build $ npm run start $ npm run test ``` 其中,`start`和`test`屬于特殊命令,可以省略`run`。 ```bash $ npm start $ npm test ``` 如果希望一個操作的輸出,是另一個操作的輸入,可以借用Linux系統的管道命令,將兩個操作連在一起。 ```javascript "build-js": "browserify browser/main.js | uglifyjs -mc > static/bundle.js" ``` 但是,更方便的寫法是引用其他`npm run`命令。 ```javascript "build": "npm run build-js && npm run build-css" ``` 上面的寫法是先運行`npm run build-js`,然后再運行`npm run build-css`,兩個命令中間用`&&`連接。如果希望兩個命令同時平行執行,它們中間可以用`&`連接。 下面是一個流操作的例子。 ```javascript "devDependencies": { "autoprefixer": "latest", "cssmin": "latest" }, "scripts": { "build:css": "autoprefixer -b 'last 2 versions' < assets/styles/main.css | cssmin > dist/main.css" } ``` 寫在`scripts`屬性中的命令,也可以在`node_modules/.bin`目錄中直接寫成bash腳本。下面是一個bash腳本。 ```javascript #!/bin/bash cd site/main browserify browser/main.js | uglifyjs -mc > static/bundle.js ``` 假定上面的腳本文件名為build.sh,并且權限為可執行,就可以在scripts屬性中引用該文件。 ```javascript "build-js": "bin/build.sh" ``` ### 參數 `npm run`命令還可以添加參數。 ```javascript "scripts": { "test": "mocha test/" } ``` 上面代碼指定`npm test`,實際運行`mocha test/`。如果要通過`npm test`命令,將參數傳到mocha,則參數之前要加上兩個連詞線。 ```bash $ npm run test -- anothertest.js # 等同于 $ mocha test/ anothertest.js ``` 上面命令表示,mocha要運行所有`test`子目錄的測試腳本,以及另外一個測試腳本`anothertest.js`。 `npm run`本身有一個參數`-s`,表示關閉npm本身的輸出,只輸出腳本產生的結果。 ```bash // 輸出npm命令頭 $ npm run test // 不輸出npm命令頭 $ npm run -s test ``` ### scripts腳本命令最佳實踐 `scripts`字段的腳本命令,有一些最佳實踐,可以方便開發。首先,安裝`npm-run-all`模塊。 ```bash $ npm install npm-run-all --save-dev ``` 這個模塊用于運行多個`scripts`腳本命令。 ```bash # 繼發執行 $ npm-run-all build:html build:js # 等同于 $ npm run build:html && npm run build:js # 并行執行 $ npm-run-all --parallel watch:html watch:js # 等同于 $ npm run watch:html & npm run watch:js # 混合執行 $ npm-run-all clean lint --parallel watch:html watch:js # 等同于 $ npm-run-all clean lint $ npm-run-all --parallel watch:html watch:js # 通配符 $ npm-run-all --parallel watch:* ``` (1)start腳本命令 `start`腳本命令,用于啟動應用程序。 ```javascript "start": "npm-run-all --parallel dev serve" ``` 上面命令并行執行`dev`腳本命令和`serve`腳本命令,等同于下面的形式。 ```bash $ npm run dev & npm run serve ``` 如果start腳本沒有配置,`npm start`命令默認執行下面的腳本,前提是模塊的根目錄存在一個server.js文件。 ```bash $ node server.js ``` (2)dev腳本命令 `dev`腳本命令,規定開發階段所要做的處理,比如構建網頁資源。 ```javascript "dev": "npm-run-all dev:*" ``` 上面命令用于繼發執行所有`dev`的子命令。 ```javascript "predev:sass": "node-sass --source-map src/css/hoodie.css.map --output-style nested src/sass/base.scss src/css/hoodie.css" ``` 上面命令將sass文件編譯為css文件,并生成source map文件。 ```javascript "dev:sass": "node-sass --source-map src/css/hoodie.css.map --watch --output-style nested src/sass/base.scss src/css/hoodie.css" ``` 上面命令會監視sass文件的變動,只要有變動,就自動將其編譯為css文件。 ```javascript "dev:autoprefix": "postcss --use autoprefixer --autoprefixer.browsers \"> 5%\" --output src/css/hoodie.css src/css/hoodie.css" ``` 上面命令為css文件加上瀏覽器前綴,限制條件是只考慮市場份額大于5%的瀏覽器。 (3)serve腳本命令 `serve`腳本命令用于啟動服務。 ```javascript "serve": "live-server dist/ --port=9090" ``` 上面命令啟動服務,用的是[live-server](http://npmjs.com/package/live-server)模塊,將服務啟動在9090端口,展示`dist`子目錄。 `live-server`模塊有三個功能。 - 啟動一個HTTP服務器,展示指定目錄的`index.html`文件,通過該文件加載各種網絡資源,這是`file://`協議做不到的。 - 添加自動刷新功能。只要指定目錄之中,文件有任何變化,它就會刷新頁面。 - `npm run serve`命令執行以后,自動打開瀏覽器。、 以前,上面三個功能需要三個模塊來完成:`http-server`、`live-reload`和`opener`,現在只要`live-server`一個模塊就夠了。 (4)test腳本命令 `test`腳本命令用于執行測試。 ```javascript "test": "npm-run-all test:*", "test:lint": "sass-lint --verbose --config .sass-lint.yml src/sass/*" ``` 上面命令規定,執行測試時,運行`lint`腳本,檢查腳本之中的語法錯誤。 (5)prod腳本命令 `prod`腳本命令,規定進入生產環境時需要做的處理。 ```javascript "prod": "npm-run-all prod:*", "prod:sass": "node-sass --output-style compressed src/sass/base.scss src/css/prod/hoodie.min.css", "prod:autoprefix": "postcss --use autoprefixer --autoprefixer.browsers "> 5%" --output src/css/prod/hoodie.min.css src/css/prod/hoodie.min.css" ``` 上面命令將sass文件轉為css文件,并加上瀏覽器前綴。 (6)help腳本命令 `help`腳本命令用于展示幫助信息。 ```javascript "help": "markdown-chalk --input DEVELOPMENT.md" ``` 上面命令之中,`markdown-chalk`模塊用于將指定的markdown文件,轉為彩色文本顯示在終端之中。 (7)docs腳本命令 `docs`腳本命令用于生成文檔。 ```javascript "docs": "kss-node --source src/sass --homepage ../../styleguide.md" ``` 上面命令使用`kss-node`模塊,提供源碼的注釋生成markdown格式的文檔。 ### pre- 和 post- 腳本 `npm run`為每條命令提供了`pre-`和`post-`兩個鉤子(hook)。以`npm run lint`為例,執行這條命令之前,npm會先查看有沒有定義prelint和postlint兩個鉤子,如果有的話,就會先執行`npm run prelint`,然后執行`npm run lint`,最后執行`npm run postlint`。 ```javascript { "name": "myproject", "devDependencies": { "eslint": "latest" "karma": "latest" }, "scripts": { "lint": "eslint --cache --ext .js --ext .jsx src", "test": "karma start --log-leve=error karma.config.js --single-run=true", "pretest": "npm run lint", "posttest": "echo 'Finished running tests'" } } ``` 上面代碼是一個`package.json`文件的例子。如果執行`npm test`,會按下面的順序執行相應的命令。 1. `pretest` 1. `test` 1. `posttest` 如果執行過程出錯,就不會執行排在后面的腳本,即如果prelint腳本執行出錯,就不會接著執行lint和postlint腳本。 下面是一個例子。 ```javascript { "test": "karma start", "test:lint": "eslint . --ext .js --ext .jsx", "pretest": "npm run test:lint" } ``` 上面代碼中,在運行`npm run test`之前,會自動檢查代碼,即運行`npm run test:lint`命令。 下面是一些常見的`pre-`和`post-`腳本。 - `prepublish`:發布一個模塊前執行。 - `postpublish`:發布一個模塊后執行。 - `preinstall`:用戶執行`npm install`命令時,先執行該腳本。 - `postinstall`:用戶執行`npm install`命令時,安裝結束后執行該腳本,通常用于將下載的源碼編譯成用戶需要的格式,比如有些模塊需要在用戶機器上跟本地的C++模塊一起編譯。 - `preuninstall`:卸載一個模塊前執行。 - `postuninstall`:卸載一個模塊后執行。 - `preversion`:更改模塊版本前執行。 - `postversion`:更改模塊版本后執行。 - `pretest`:運行`npm test`命令前執行。 - `posttest`:運行`npm test`命令后執行。 - `prestop`:運行`npm stop`命令前執行。 - `poststop`:運行`npm stop`命令后執行。 - `prestart`:運行`npm start`命令前執行。 - `poststart`:運行`npm start`命令后執行。 - `prerestart`:運行`npm restart`命令前執行。 - `postrestart`:運行`npm restart`命令后執行。 對于最后一個`npm restart`命令,如果沒有設置`restart`腳本,`prerestart`和`postrestart`會依次執行stop和start腳本。 另外,不能在`pre`腳本之前再加`pre`,即`prepretest`腳本不起作用。 注意,即使Npm可以自動運行`pre`和`post`腳本,也可以手動執行它們。 ```bash $ npm run prepublish ``` 下面是`post install`的例子。 ```javascript { "postinstall": "node lib/post_install.js" } ``` 上面的這個命令,主要用于處理從Git倉庫拉下來的源碼。比如,有些源碼是用TypeScript寫的,可能需要轉換一下。 下面是`publish`鉤子的一個例子。 ```javascript { "dist:modules": "babel ./src --out-dir ./dist-modules", "gh-pages": "webpack", "gh-pages:deploy": "gh-pages -d gh-pages", "prepublish": "npm run dist:modules", "postpublish": "npm run gh-pages && npm run gh-pages:deploy" } ``` 上面命令在運行`npm run publish`時,會先執行Babel編譯,然后調用Webpack構建,最后發到Github Pages上面。 以上都是npm相關操作的鉤子,如果安裝某些模塊,還能支持Git相關的鉤子。下面以[husky](https://github.com/typicode/husky)模塊為例。 ```bash $ npm install husky --save-dev ``` 安裝以后,就能在`package.json`添加`precommit`、`prepush`等鉤子。 ```javascript { "scripts": { "lint": "eslint yourJsFiles.js", "precommit": "npm run test && npm run lint", "prepush": "npm run test && npm run lint", "...": "..." } } ``` 類似作用的模塊還有`pre-commit`、`precommit-hook`等。 ### 內部變量 scripts字段可以使用一些內部變量,主要是package.json的各種字段。 比如,package.json的內容是`{"name":"foo", "version":"1.2.5"}`,那么變量`npm_package_name`的值是foo,變量`npm_package_version`的值是1.2.5。 ```javascript { "scripts":{ "bundle": "mkdir -p build/$npm_package_version/" } } ``` 運行`npm run bundle`以后,將會生成`build/1.2.5/`子目錄。 `config`字段也可以用于設置內部字段。 ```javascript "name": "fooproject", "config": { "reporter": "xunit" }, "scripts": { "test": "mocha test/ --reporter $npm_package_config_reporter" } ``` 上面代碼中,變量`npm_package_config_reporter`對應的就是reporter。 ### 通配符 npm的通配符的規則如下。 - `*` 匹配0個或多個字符 - `?` 匹配1個字符 - `[...]` 匹配某個范圍的字符。如果該范圍的第一個字符是`!`或`^`,則匹配不在該范圍的字符。 - `!(pattern|pattern|pattern)` 匹配任何不符合給定的模式 - `?(pattern|pattern|pattern)` 匹配0個或1個給定的模式 - `+(pattern|pattern|pattern)` 匹配1個或多個給定的模式 - `*(a|b|c)` 匹配0個或多個給定的模式 - `@(pattern|pat*|pat?erN)` 只匹配給定模式之一 - `**` 如果出現在路徑部分,表示0個或多個子目錄。 ## npm link 開發Npm模塊的時候,有時我們會希望,邊開發邊試用。但是,常規情況下,使用一個模塊,需要將其安裝到`node_modules`目錄之中,這對于開發中的模塊,顯然非常不方便。`npm link`就能起到這個作用,建立一個符號鏈接,在全局的`node_modules`目錄之中,生成一個符號鏈接,指向模塊的本地目錄。 為了理解`npm link`,請設想這樣一個場景。你開發了一個模塊`myModule`,目錄為`src/myModule`,你自己的項目`myProject`要用到這個模塊,項目目錄為`src/myProject`。每一次,你更新`myModul`e,就要用`npm publish`命令發布,然后切換到項目目錄,使用`npm update`更新模塊。這樣顯然很不方便,如果我們可以從項目目錄建立一個符號鏈接,直接連到模塊目錄,就省去了中間步驟,項目可以直接使用最新版的模塊。 首先,在模塊目錄(`src/myModule`)下運行`npm link`命令。 ```bash src/myModule$ npm link ``` 上面的命令會在Npm的全局模塊目錄內,生成一個符號鏈接文件,該文件的名字就是`package.json`文件中指定的文件名。 ```bash /path/to/global/node_modules/myModule -> src/myModule ``` 這個時候,已經可以全局調用`myModule`模塊了。但是,如果我們要讓這個模塊安裝在項目內,還要進行下面的步驟。 切換到項目目錄,再次運行`npm link`命令,并指定模塊名。 ```bash src/myProject$ npm link myModule ``` 上面命令等同于生成了本地模塊的符號鏈接。 ```bash src/myProject/node_modules/myModule -> /path/to/global/node_modules/myModule ``` 然后,就可以在你的項目中,加載該模塊了。 ```javascript var myModule = require('myModule'); ``` 這樣一來,`myModule`的任何變化,都可以直接反映在`myProject`項目之中。但是,這樣也出現了風險,任何在`myProject`目錄中對`myModule`的修改,都會反映到模塊的源碼中。 如果你的項目不再需要該模塊,可以在項目目錄內使用`npm unlink`命令,刪除符號鏈接。 ```bash src/myProject$ npm unlink myModule ``` ## npm bin `npm bin`命令顯示相對于當前目錄的,Node模塊的可執行腳本所在的目錄(即`.bin`目錄)。 ```bash # 項目根目錄下執行 $ npm bin ./node_modules/.bin ``` ## npm adduser `npm adduser`用于在npmjs.com注冊一個用戶。 ```bash $ npm adduser Username: YOUR_USER_NAME Password: YOUR_PASSWORD Email: YOUR_EMAIL@domain.com ``` ## npm publish `npm publish`用于將當前模塊發布到`npmjs.com`。執行之前,需要向`npmjs.com`申請用戶名。 ```bash $ npm adduser ``` 如果已經注冊過,就使用下面的命令登錄。 ```bash $ npm login ``` 登錄以后,就可以使用`npm publish`命令發布。 ```bash $ npm publish ``` 如果當前模塊是一個beta版,比如`1.3.1-beta.3`,那么發布的時候需要使用`tag`參數。 ```bash $ npm publish --tag beta ``` 如果發布私有模塊,模塊初始化的時候,需要加上`scope`參數。只有npm的付費用戶才能發布私有模塊。 ```bash $ npm init --scope=<yourscope> ``` 如果你的模塊是用ES6寫的,那么發布的時候,最好轉成ES5。首先,需要安裝Babel。 ```javascript $ npm install --save-dev babel-cli@6 babel-preset-es2015@6 ``` 然后,在`package.json`里面寫入`build`腳本。 ```javascript "scripts": { "build": "babel source --presets babel-preset-es2015 --out-dir distribution", "prepublish": "npm run build" } ``` 運行上面的腳本,會將`source`目錄里面的ES6源碼文件,轉為`distribution`目錄里面的ES5源碼文件。然后,在項目根目錄下面創建兩個文件`.npmignore`和`.gitignore`,分別寫入以下內容。 ```javascrip // .npmignore source // .gitignore node_modules distribution ``` ## npm deprecate 如果想廢棄某個版本的模塊,可以使用`npm deprecate`命令。 ```bash $ npm deprecate my-thing@"< 0.2.3" "critical bug fixed in v0.2.3" ``` 運行上面的命令以后,小于`0.2.3`版本的模塊的`package.json`都會寫入一行警告,用戶安裝這些版本時,這行警告就會在命令行顯示。 ## npm owner 模塊的維護者可以發布新版本。`npm owner`命令用于管理模塊的維護者。 ```bash # 列出指定模塊的維護者 $ npm owner ls <package name> # 新增維護者 $ npm owner add <user> <package name> # 刪除維護者 $ npm owner rm <user> <package name> ``` <h2 id="12.5">fs 模塊</h2> fs是filesystem的縮寫,該模塊提供本地文件的讀寫能力,基本上是POSIX文件操作命令的簡單包裝。但是,這個模塊幾乎對所有操作提供異步和同步兩種操作方式,供開發者選擇。 ## readFileSync() readFileSync方法用于同步讀取文件,返回一個字符串。 ```javascript var text = fs.readFileSync(fileName, "utf8"); // 將文件按行拆成數組 text.split(/\r?\n/).forEach(function (line) { // ... }); ``` 該方法的第一個參數是文件路徑,第二個參數是文本文件編碼,默認為utf8。 不同系統的行結尾字符不同,可以用下面的方法判斷。 ```javascript // 方法一,查詢現有的行結尾字符 var EOL = fileContents.indexOf("\r\n") >= 0 ? "\r\n" : "\n"; // 方法二,根據當前系統處理 var EOL = (process.platform === 'win32' ? '\r\n' : '\n') ``` ## writeFileSync() writeFileSync方法用于同步寫入文件。 ``` fs.writeFileSync(fileName, str, 'utf8'); ``` 它的第一個參數是文件路徑,第二個參數是寫入文件的字符串,第三個參數是文件編碼,默認為utf8。 ## exists(path, callback) exists方法用來判斷給定路徑是否存在,然后不管結果如何,都會調用回調函數。 ```javascript fs.exists('/path/to/file', function (exists) { util.debug(exists ? "it's there" : "no file!"); }); ``` 上面代碼表明,回調函數的參數是一個表示文件是否存在的布爾值。 需要注意的是,不要在open方法之前調用exists方法,open方法本身就能檢查文件是否存在。 下面的例子是如果給定目錄存在,就刪除它。 ```javascript if(fs.exists(outputFolder)) { console.log("Removing "+outputFolder); fs.rmdir(outputFolder); } ``` ## mkdir(),writeFile(),readfile() mkdir方法用于新建目錄。 ```javascript var fs = require('fs'); fs.mkdir('./helloDir',0777, function (err) { if (err) throw err; }); ``` mkdir接受三個參數,第一個是目錄名,第二個是權限值,第三個是回調函數。 writeFile方法用于寫入文件。 ```javascript var fs = require('fs'); fs.writeFile('./helloDir/message.txt', 'Hello Node', function (err) { if (err) throw err; console.log('文件寫入成功'); }); ``` readfile方法用于讀取文件內容。 ```javascript var fs = require('fs'); fs.readFile('./helloDir/message.txt','UTF-8' ,function (err, data) { if (err) throw err; console.log(data); }); ``` 上面代碼使用readFile方法讀取文件。readFile方法的第一個參數是文件名,第二個參數是文件編碼,第三個參數是回調函數。可用的文件編碼包括“ascii”、“utf8”和“base64”。如果沒有指定文件編碼,返回的是原始的緩存二進制數據,這時需要調用buffer對象的toString方法,將其轉為字符串。 ```javascript var fs = require('fs'); fs.readFile('example_log.txt', function (err, logData) { if (err) throw err; var text = logData.toString(); }); ``` readFile方法是異步操作,所以必須小心,不要同時發起多個readFile請求。 ```js for(var i = 1; i <= 1000; i++) { fs.readFile('./'+i+'.txt', function() { // do something with the file }); } ``` 上面代碼會同時發起1000個readFile異步請求,很快就會耗盡系統資源。 ## mkdirSync(),writeFileSync(),readFileSync() 這三個方法是建立目錄、寫入文件、讀取文件的同步版本。 ```javascript fs.mkdirSync('./helloDirSync',0777); fs.writeFileSync('./helloDirSync/message.txt', 'Hello Node'); var data = fs.readFileSync('./helloDirSync/message.txt','UTF-8'); console.log('file created with contents:'); console.log(data); ``` 對于流量較大的服務器,最好還是采用異步操作,因為同步操作時,只有前一個操作結束,才會開始后一個操作,如果某個操作特別耗時(常常發生在讀寫數據時),會導致整個程序停頓。 ## readdir() readdir方法用于讀取目錄,返回一個所包含的文件和子目錄的數組。 ```javascript fs.readdir(process.cwd(), function (err, files) { if (err) { console.log(err); return; } var count = files.length; var results = {}; files.forEach(function (filename) { fs.readFile(filename, function (data) { results[filename] = data; count--; if (count <= 0) { // 對所有文件進行處理 } }); }); }); ``` ## stat() stat方法的參數是一個文件或目錄,它產生一個對象,該對象包含了該文件或目錄的具體信息。我們往往通過該方法,判斷正在處理的到底是一個文件,還是一個目錄。 ```javascript var fs = require('fs'); fs.readdir('/etc/', function (err, files) { if (err) throw err; files.forEach( function (file) { fs.stat('/etc/' + file, function (err, stats) { if (err) throw err; if (stats.isFile()) { console.log("%s is file", file); } else if (stats.isDirectory ()) { console.log("%s is a directory", file); } console.log('stats: %s',JSON.stringify(stats)); }); }); }); ``` ## watchfile(),unwatchfile() watchfile方法監聽一個文件,如果該文件發生變化,就會自動觸發回調函數。 ```javascript var fs = require('fs'); fs.watchFile('./testFile.txt', function (curr, prev) { console.log('the current mtime is: ' + curr.mtime); console.log('the previous mtime was: ' + prev.mtime); }); fs.writeFile('./testFile.txt', "changed", function (err) { if (err) throw err; console.log("file write complete"); }); ``` unwatchfile方法用于解除對文件的監聽。 ## createReadStream() createReadStream方法往往用于打開大型的文本文件,創建一個讀取操作的數據流。所謂大型文本文件,指的是文本文件的體積很大,讀取操作的緩存裝不下,只能分成幾次發送,每次發送會觸發一個data事件,發送結束會觸發end事件。 ```javascript var fs = require('fs'); function readLines(input, func) { var remaining = ''; input.on('data', function(data) { remaining += data; var index = remaining.indexOf('\n'); var last = 0; while (index > -1) { var line = remaining.substring(last, index); last = index + 1; func(line); index = remaining.indexOf('\n', last); } remaining = remaining.substring(last); }); input.on('end', function() { if (remaining.length > 0) { func(remaining); } }); } function func(data) { console.log('Line: ' + data); } var input = fs.createReadStream('lines.txt'); readLines(input, func); ``` ## createWriteStream() createWriteStream方法創建一個寫入數據流對象,該對象的write方法用于寫入數據,end方法用于結束寫入操作。 ```javascript var out = fs.createWriteStream(fileName, { encoding: "utf8" }); out.write(str); out.end(); ``` <h2 id="12.6">Path模塊</h2> ## path.join() `path.join`方法用于連接路徑。該方法的主要用途在于,會正確使用當前系統的路徑分隔符,Unix系統是”/“,Windows系統是”\“。 ```javascript var path = require('path'); path.join(mydir, "foo"); ``` 上面代碼在Unix系統下,會返回路徑`mydir/foo`。 ## path.resolve() `path.resolve`方法用于將相對路徑轉為絕對路徑。 它可以接受多個參數,依次表示所要進入的路徑,直到將最后一個參數轉為絕對路徑。如果根據參數無法得到絕對路徑,就以當前所在路徑作為基準。除了根目錄,該方法的返回值都不帶尾部的斜杠。 ```javascript // 格式 path.resolve([from ...], to) // 實例 path.resolve('foo/bar', '/tmp/file/', '..', 'a/../subfile') ``` 上面代碼的實例,執行效果類似下面的命令。 ```bash $ cd foo/bar $ cd /tmp/file/ $ cd .. $ cd a/../subfile $ pwd ``` 更多例子。 ```javascript path.resolve('/foo/bar', './baz') // '/foo/bar/baz' path.resolve('/foo/bar', '/tmp/file/') // '/tmp/file' path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif') // 如果當前目錄是/home/myself/node,返回 // /home/myself/node/wwwroot/static_files/gif/image.gif ``` 該方法忽略非字符串的參數。 ## accessSync() `accessSync`方法用于同步讀取一個路徑。 下面的代碼可以用于判斷一個目錄是否存在。 ```javascript function exists(pth, mode) { try { fs.accessSync(pth, mode); return true; } catch (e) { return false; } } ``` ## path.relative `path.relative`方法接受兩個參數,這兩個參數都應該是絕對路徑。該方法返回第二個路徑想對于地一個路徑的系那個相對路徑。 ```javascript path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb') // '../../impl/bbb' ``` 上面代碼中,如果當前目錄是`/data/orandea/test/aaa`,進入`path.relative`返回的相對路徑,就會到達`/data/orandea/impl/bbb`。 如果`path.relative`方法的兩個參數相同,則返回一個空字符串。 <h2 id="12.7">process對象</h2> `process`對象是Node的一個全局對象,提供當前Node進程的信息。它可以在腳本的任意位置使用,不必通過`require`命令加載。該對象部署了`EventEmitter`接口。 ## 進程信息 通過`process`對象,可以獲知當前進程的很多信息。 ### 退出碼 進程退出時,會返回一個整數值,表示退出時的狀態。這個整數值就叫做退出碼。下面是常見的Node進程退出碼。 - 0,正常退出 - 1,發生未捕獲錯誤 - 5,V8執行錯誤 - 8,不正確的參數 - 128 + 信號值,如果Node接受到退出信號(比如SIGKILL或SIGHUP),它的退出碼就是128加上信號值。由于128的二進制形式是10000000, 所以退出碼的后七位就是信號值。 ## 屬性 process對象提供一系列屬性,用于返回系統信息。 - **process.argv**:返回當前進程的命令行參數數組。 - **process.env**:返回一個對象,成員為當前Shell的環境變量,比如`process.env.HOME`。 - **process.installPrefix**:node的安裝路徑的前綴,比如`/usr/local`,則node的執行文件目錄為`/usr/local/bin/node`。 - **process.pid**:當前進程的進程號。 - **process.platform**:當前系統平臺,比如Linux。 - **process.title**:默認值為“node”,可以自定義該值。 - **process.version**:Node的版本,比如v0.10.18。 下面是主要屬性的介紹。 ### stdout,stdin,stderr 以下屬性指向系統IO。 **(1)stdout** stdout屬性指向標準輸出(文件描述符1)。它的write方法等同于console.log,可用在標準輸出向用戶顯示內容。 ```javascript console.log = function(d) { process.stdout.write(d + '\n'); }; ``` 下面代碼表示將一個文件導向標準輸出。 ```javascript var fs = require('fs'); fs.createReadStream('wow.txt') .pipe(process.stdout); ``` 上面代碼中,由于process.stdout和process.stdin與其他進程的通信,都是流(stream)形式,所以必須通過pipe管道命令中介。 ```javascript var fs = require('fs'); var zlib = require('zlib'); fs.createReadStream('wow.txt') .pipe(zlib.createGzip()) .pipe(process.stdout); ``` 上面代碼通過pipe方法,先將文件數據壓縮,然后再導向標準輸出。 **(2)stdin** stdin代表標準輸入(文件描述符0)。 ```javascript process.stdin.pipe(process.stdout) ``` 上面代碼表示將標準輸入導向標準輸出。 由于stdin和stdout都部署了stream接口,所以可以使用stream接口的方法。 ```javascript process.stdin.setEncoding('utf8'); process.stdin.on('readable', function() { var chunk = process.stdin.read(); if (chunk !== null) { process.stdout.write('data: ' + chunk); } }); process.stdin.on('end', function() { process.stdout.write('end'); }); ``` **(3)stderr** stderr屬性指向標準錯誤(文件描述符2)。 ### argv,execPath,execArgv argv屬性返回一個數組,由命令行執行腳本時的各個參數組成。它的第一個成員總是node,第二個成員是腳本文件名,其余成員是腳本文件的參數。 請看下面的例子,新建一個腳本文件argv.js。 ```javascript // argv.js console.log("argv: ",process.argv); ``` 在命令行下調用這個腳本,會得到以下結果。 ```javascript $ node argv.js a b c [ 'node', '/path/to/argv.js', 'a', 'b', 'c' ] ``` 上面代碼表示,argv返回數組的成員依次是命令行的各個部分,真正的參數實際上是從`process.argv[2]`開始。要得到真正的參數部分,可以把argv.js改寫成下面這樣。 ```javascript // argv.js var myArgs = process.argv.slice(2); console.log(myArgs); ``` execPath屬性返回執行當前腳本的Node二進制文件的絕對路徑。 ```javascript > process.execPath '/usr/local/bin/node' > ``` execArgv屬性返回一個數組,成員是命令行下執行腳本時,在Node可執行文件與腳本文件之間的命令行參數。 ```bash # script.js的代碼為 # console.log(process.execArgv); $ node --harmony script.js --version ``` ### process.env `process.env`屬性返回一個對象,包含了當前Shell的所有環境變量。比如,`process.env.HOME`返回用戶的主目錄。 通常的做法是,新建一個環境變量`NODE_ENV`,用它確定當前所處的開發階段,生產階段設為`production`,開發階段設為`develop`或`staging`,然后在腳本中讀取`process.env.NODE_ENV`即可。 運行腳本時,改變環境變量,可以采用下面的寫法。 ```bash $ export NODE_ENV=production && node app.js # 或者 $ NODE_ENV=production node app.js ``` ## 方法 process對象提供以下方法: - **process.chdir()**:切換工作目錄到指定目錄。 - **process.cwd()**:返回運行當前腳本的工作目錄的路徑。 - **process.exit()**:退出當前進程。 - **process.getgid()**:返回當前進程的組ID(數值)。 - **process.getuid()**:返回當前進程的用戶ID(數值)。 - **process.nextTick()**:指定回調函數在當前執行棧的尾部、下一次Event Loop之前執行。 - **process.on()**:監聽事件。 - **process.setgid()**:指定當前進程的組,可以使用數字ID,也可以使用字符串ID。 - **process.setuid()**:指定當前進程的用戶,可以使用數字ID,也可以使用字符串ID。 ### process.cwd(),process.chdir() `cwd`方法返回進程的當前目錄(絕對路徑),`chdir`方法用來切換目錄。 ```bash > process.cwd() '/home/aaa' > process.chdir('/home/bbb') > process.cwd() '/home/bbb' ``` 注意,`process.cwd()`與`__dirname`的區別。前者進程發起時的位置,后者是腳本的位置,兩者可能是不一致的。比如,`node ./code/program.js`,對于`process.cwd()`來說,返回的是當前目錄(`.`);對于`__dirname`來說,返回是腳本所在目錄,即`./code/program.js`。 ## process.nextTick() `process.nextTick`將任務放到當前一輪事件循環(Event Loop)的尾部。 ```bash process.nextTick(function () { console.log('下一次Event Loop即將開始!'); }); ``` 上面代碼可以用`setTimeout(f,0)`改寫,效果接近,但是原理不同。 ```bash setTimeout(function () { console.log('已經到了下一輪Event Loop!'); }, 0) ``` `setTimeout(f,0)`是將任務放到下一輪事件循環的頭部,因此`nextTick`會比它先執行。另外,`nextTick`的效率更高,因為不用檢查是否到了指定時間。 根據Node的事件循環的實現,基本上,進入下一輪事件循環后的執行順序如下。 1. `setTimeout(f,0)` 1. 各種到期的回調函數 1. `process.nextTick` ### process.exit() `process.exit`方法用來退出當前進程,它可以接受一個數值參數。如果參數大于0,表示執行失敗;如果等于0表示執行成功。 ```bash if (err) { process.exit(1); } else { process.exit(0); } ``` `process.exit()`執行時,會觸發`exit`事件。 ### process.on() `process.on`方法用來監聽各種事件,并指定回調函數。 ```javascript process.on('uncaughtException', function(err){ console.log('got an error: %s', err.message); process.exit(1); }); setTimeout(function(){ throw new Error('fail'); }, 100); ``` 上面代碼是`process`監聽Node的一個全局性事件`uncaughtException`,只要有錯誤沒有捕獲,就會觸發這個事件。 process支持的事件有以下一些。 - data事件:數據輸出輸入時觸發 - SIGINT事件:接收到系統信號時觸發 ```javascript process.on('SIGINT', function () { console.log('Got a SIGINT. Goodbye cruel world'); process.exit(0); }); ``` 使用時,向該進程發出系統信號,就會導致進程退出。 ```bash $ kill -s SIGINT [process_id] ``` SIGTERM信號表示內核要求當前進程停止,進程可以自行停止,也可以忽略這個信號。 ```javascript var http = require('http'); var server = http.createServer(function (req, res) { }); process.on('SIGTERM', function () { server.close(function () { process.exit(0); }); }); ``` 上面代碼表示,進程接到SIGTERM信號之后,關閉服務器,然后退出進程。需要注意的是,這時進程不會馬上退出,而是要回應完最后一個請求,處理完所有回調函數,然后再退出。 ### process.kill() process.kill方法用來對指定ID的線程發送信號,默認為SIGINT信號。 ```javascript process.on('SIGTERM', function(){ console.log('terminating'); process.exit(1); }); setTimeout(function(){ console.log('sending SIGTERM to process %d', process.pid); process.kill(process.pid, 'SIGTERM'); }, 500); setTimeout(function(){ console.log('never called'); }, 1000); ``` 上面代碼中,500毫秒后向當前進程發送SIGTERM信號(終結進程),因此1000毫秒后的指定事件不會被觸發。 ## 事件 ### exit事件 當前進程退出時,會觸發`exit`事件,可以對該事件指定回調函數。 ```javascript process.on('exit', function () { fs.writeFileSync('/tmp/myfile', '需要保存到硬盤的信息'); }); ``` 下面是一個例子,進程退出時,顯示一段日志。 ```javascript process.on("exit", code => console.log("exiting with code: " + code)) ``` 注意,此時回調函數只能執行同步操作,不能包含異步操作,因為執行完回調函數,進程就會退出,無法監聽到回調函數的操作結果。 ```javascript process.on('exit', function(code) { // 不會執行 setTimeout(function() { console.log('This will not run'); }, 0); }); ``` 上面代碼在`exit`事件的回調函數里面,指定了一個下一輪事件循環,所要執行的操作。這是無效的,不會得到執行。 ### beforeExit事件 beforeExit事件在Node清空了Event Loop以后,再沒有任何待處理的任務時觸發。正常情況下,如果沒有任何待處理的任務,Node進程會自動退出,設置beforeExit事件的監聽函數以后,就可以提供一個機會,再部署一些任務,使得Node進程不退出。 beforeExit事件與exit事件的主要區別是,beforeExit的監聽函數可以部署異步任務,而exit不行。 此外,如果是顯式終止程序(比如調用process.exit()),或者因為發生未捕獲的錯誤,而導致進程退出,這些場合不會觸發beforeExit事件。因此,不能使用該事件替代exit事件。 ### uncaughtException事件 當前進程拋出一個沒有被捕捉的錯誤時,會觸發`uncaughtException`事件。 ```javascript process.on('uncaughtException', function (err) { console.error('An uncaught error occurred!'); console.error(err.stack); throw new Error('未捕獲錯誤'); }); ``` 部署`uncaughtException`事件的監聽函數,是免于Node進程終止的最后措施,否則Node就要執行`process.exit()`。出于除錯的目的,并不建議發生錯誤后,還保持進程運行。 拋出錯誤之前部署的異步操作,還是會繼續執行。只有完成以后,Node進程才會退出。 ```javascript process.on('uncaughtException', function(err) { console.log('Caught exception: ' + err); }); setTimeout(function() { console.log('本行依然執行'); }, 500); // 下面的表達式拋出錯誤 nonexistentFunc(); ``` 上面代碼中,拋出錯誤之后,此前setTimeout指定的回調函數亦然會執行。 ### 信號事件 操作系統內核向Node進程發出信號,會觸發信號事件。實際開發中,主要對SIGTERM和SIGINT信號部署監聽函數,這兩個信號在非Windows平臺會導致進程退出,但是只要部署了監聽函數,Node進程收到信號后就不會退出。 ```javascript // 讀取標準輸入,這主要是為了不讓當前進程退出 process.stdin.resume(); process.on('SIGINT', function() { console.log('SIGINT信號,按Control-D退出'); }); ``` 上面代碼部署了SIGINT信號的監聽函數,當用戶按下Ctrl-C后,會顯示提示文字。 <h2 id="12.8">Buffer對象</h2> ## 概述 Buffer對象是Node.js用來處理二進制數據的一個接口。JavaScript比較擅長處理Unicode數據,對于處理二進制格式的數據(比如TCP數據流),就不太擅長。Buffer對象就是為了解決這個問題而提供的。該對象也是一個構造函數,它的實例代表了V8引擎分配的一段內存,基本上是一個數組,成員都為整數值。 Buffer是Node原生提供的全局對象,可以直接使用,不需要`require('buffer')`。 Buffer對象與字符串的互相轉換,需要指定編碼格式。目前,Buffer對象支持以下編碼格式。 - ascii - utf8 - utf16le:UTF-16的小頭編碼,支持大于U+10000的四字節字符。 - ucs2:utf16le的別名。 - base64 - hex:將每個字節轉為兩個十六進制字符。 V8引擎將Buffer對象占用的內存,解釋為一個整數數組,而不是二進制數組。所以,`new Uint32Array(new Buffer([1, 2, 3, 4]))`,生成的`Uint32Array`數組是一個4個成員的`Uint32Array`數組,而不是只有單個成員(`[0x1020304]`或者`[0x4030201]`)。 注意,這時二進制數組所對應的內存是從Buffer對象拷貝的,而不是共享的。二進制數組的`buffer`屬性,保留指向原Buffer對象的指針。 二進制數組的操作,與Buffer對象的操作基本上是兼容的,只有輕微的差異。比如,二進制數組的`slice`方法返回原內存的拷貝,而Buffer對象的`slice`方法創造原內存的一個視圖(view)。 ## Buffer構造函數 Buffer作為構造函數,可以用`new`命令生成一個實例,它可以接受多種形式的參數。 ```javascript // 參數是整數,指定分配多少個字節內存 var hello = new Buffer(5); // 參數是數組,數組成員必須是整數值 var hello = new Buffer([0x48, 0x65, 0x6c, 0x6c, 0x6f]); hello.toString() // 'Hello' // 參數是字符串(默認為utf8編碼) var hello = new Buffer('Hello'); // 參數是字符串(不省略編碼) var hello = new Buffer('Hello', 'utf8'); // 參數是另一個Buffer實例,等同于拷貝后者 var hello1 = new Buffer('Hello'); var hello2 = new Buffer(hello1); ``` 下面是讀取用戶命令行輸入的例子。 ```javascript var fs = require('fs'); var buffer = new Buffer(1024); var readSize = fs.readSync(fs.openSync('/dev/tty', 'r'), buffer, 0, bufferSize); var chunk = buffer.toString('utf8', 0, readSize); console.log('INPUT: ' + chunk); ``` 運行上面的程序結果如下。 ```bash # 輸入任意內容,然后按回車鍵 foo INPUT: foo ``` ## 類的方法 ### Buffer.isEncoding() Buffer.isEncoding方法返回一個布爾值,表示Buffer實例是否為指定編碼。 ```javascript Buffer.isEncoding('utf8') // true ``` ### Buffer.isBuffer() Buffer.isBuffer方法接受一個對象作為參數,返回一個布爾值,表示該對象是否為Buffer實例。 ```javascript Buffer.isBuffer(Date) // false ``` ### Buffer.byteLength() Buffer.byteLength方法返回字符串實際占據的字節長度,默認編碼方式為utf8。 ```javascript Buffer.byteLength('Hello', 'utf8') // 5 ``` ### Buffer.concat() Buffer.concat方法將一組Buffer對象合并為一個Buffer對象。 ```javascript var i1 = new Buffer('Hello'); var i2 = new Buffer(' '); var i3 = new Buffer('World'); Buffer.concat([i1, i2, i3]).toString() // 'Hello World' ``` 需要注意的是,如果Buffer.concat的參數數組只有一個成員,就直接返回該成員。如果有多個成員,就返回一個多個成員合并的新Buffer對象。 Buffer.concat方法還可以接受第二個參數,指定合并后Buffer對象的總長度。 ```javascript var i1 = new Buffer('Hello'); var i2 = new Buffer(' '); var i3 = new Buffer('World'); Buffer.concat([i1, i2, i3], 10).toString() // 'Hello Worl' ``` 省略第二個參數時,Node內部會計算出這個值,然后再據此進行合并運算。因此,顯式提供這個參數,能提供運行速度。 ## 實例屬性 ### length length屬性返回Buffer對象所占據的內存長度。注意,這個值與Buffer對象的內容無關。 ```javascript buf = new Buffer(1234); buf.length // 1234 buf.write("some string", 0, "ascii"); buf.length // 1234 ``` 上面代碼中,不管寫入什么內容,length屬性總是返回Buffer對象的空間長度。如果想知道一個字符串所占據的字節長度,可以將其傳入Buffer.byteLength方法。 length屬性是可寫的,但是這會導致未定義的行為,不建議使用。如果想修改Buffer對象的長度,建議使用slice方法返回一個新的Buffer對象。 ## 實例方法 ### write() write方法可以向指定的Buffer對象寫入數據。它的第一個參數是所寫入的內容,第二個參數(可省略)是所寫入的起始位置(從0開始),第三個參數(可省略)是編碼方式,默認為utf8。 ```javascript var buf = new Buffer(5); buf.write('He'); buf.write('l', 2); buf.write('lo', 3); console.log(buf.toString()); // "Hello" ``` ### slice() slice方法返回一個按照指定位置、從原對象切割出來的Buffer實例。它的兩個參數分別為切割的起始位置和終止位置。 ```javascript var buf = new Buffer('just some data'); var chunk = buf.slice(5, 9); chunk.toString() // "some" ``` ### toString() toString方法將Buffer對象,按照指定編碼(默認為utf8)轉為字符串。 ```javascript var hello = new Buffer('Hello'); hello // <Buffer 48 65 6c 6c 6f> hello.toString() // "Hello" ``` `toString`方法可以只返回指定位置內存的內容,它的第二個參數表示起始位置,第三個參數表示終止位置,兩者都是從0開始計算。 ```javascript var buf = new Buffer('just some data'); console.log(buf.toString('ascii', 5, 9)); // "some" ``` ### toJSON() toJSON方法將Buffer實例轉為JSON對象。如果JSON.stringify方法調用Buffer實例,默認會先調用toJSON方法。 ```javascript var buf = new Buffer('test'); var json = JSON.stringify(buf); json // '[116,101,115,116]' var copy = new Buffer(JSON.parse(json)); copy // <Buffer 74 65 73 74> ``` <h2 id="12.9">Events模塊</h2> ## 概述 ### 基本用法 `Events`模塊是Node對“發布/訂閱”模式(publish/subscribe)的實現。一個對象通過這個模塊,向另一個對象傳遞消息。 該模塊通過`EventEmitter`屬性,提供了一個構造函數。該構造函數的實例具有on方法,可以用來監聽指定事件,并觸發回調函數。任意對象都可以發布指定事件,被`EventEmitter`實例的`on`方法監聽到。 ```javascript var EventEmitter = require('events').EventEmitter; var ee = new EventEmitter(); ee.on('someEvent', function () { console.log('event has occured'); }); function f() { console.log('start'); ee.emit('someEvent'); console.log('end'); } f() // start // event has occured // end ``` 上面代碼在加載`events`模塊后,通過`EventEmitter`屬性建立了一個`EventEmitter`對象實例,這個實例就是消息中心。然后,通過`on`方法為`someEvent`事件指定回調函數。最后,通過`emit`方法觸發`someEvent`事件。 上面代碼也表明,`EventEmitter`對象的事件觸發和監聽是同步的,即只有事件的回調函數執行以后,函數`f`才會繼續執行。 ### on方法 默認情況下,Node.js允許同一個事件最多可以指定10個回調函數。 ```javascript ee.on("someEvent", function () { console.log("event 1"); }); ee.on("someEvent", function () { console.log("event 2"); }); ee.on("someEvent", function () { console.log("event 3"); }); ``` 超過10個回調函數,會發出一個警告。這個門檻值可以通過setMaxListeners方法改變。 ```javascript ee.setMaxListeners(20); ``` ### emit方法 EventEmitter實例的emit方法,用來觸發事件。它的第一個參數是事件名稱,其余參數都會依次傳入回調函數。 ```javascript var EventEmitter = require('events').EventEmitter; var myEmitter = new EventEmitter; var connection = function(id){ console.log('client id: ' + id); }; myEmitter.on('connection', connection); myEmitter.emit('connection', 6); ``` ## EventEmitter接口的部署 Events模塊的作用,還在于其他模塊可以部署EventEmitter接口,從而也能夠訂閱和發布消息。 ```javascript var EventEmitter = require('events').EventEmitter; function Dog(name) { this.name = name; } Dog.prototype.__proto__ = EventEmitter.prototype; // 另一種寫法 // Dog.prototype = Object.create(EventEmitter.prototype); var simon = new Dog('simon'); simon.on('bark', function(){ console.log(this.name + ' barked'); }); setInterval(function(){ simon.emit('bark'); }, 500); ``` 上面代碼新建了一個構造函數Dog,然后讓其繼承EventEmitter,因此Dog就擁有了EventEmitter的接口。最后,為Dog的實例指定bark事件的監聽函數,再使用EventEmitter的emit方法,觸發bark事件。 Node內置模塊util的inherits方法,提供了另一種繼承EventEmitter的寫法。 ```javascript var util = require('util'); var EventEmitter = require('events').EventEmitter; var Radio = function(station) { var self = this; setTimeout(function() { self.emit('open', station); }, 0); setTimeout(function() { self.emit('close', station); }, 5000); this.on('newListener', function(listener) { console.log('Event Listener: ' + listener); }); }; util.inherits(Radio, EventEmitter); module.exports = Radio; ``` 上面代碼中,Radio是一個構造函數,它的實例繼承了EventEmitter接口。下面是使用這個模塊的例子。 ```javascript var Radio = require('./radio.js'); var station = { freq: '80.16', name: 'Rock N Roll Radio', }; var radio = new Radio(station); radio.on('open', function(station) { console.log('"%s" FM %s 打開', station.name, station.freq); console.log('? ??'); }); radio.on('close', function(station) { console.log('"%s" FM %s 關閉', station.name, station.freq); }); ``` ## 事件類型 Events模塊默認支持兩個事件。 - newListener事件:添加新的回調函數時觸發。 - removeListener事件:移除回調時觸發。 ```javascript ee.on("newListener", function (evtName){ console.log("New Listener: " + evtName); }); ee.on("removeListener", function (evtName){ console.log("Removed Listener: " + evtName); }); function foo (){} ee.on("save-user", foo); ee.removeListener("save-user", foo); // New Listener: removeListener // New Listener: save-user // Removed Listener: save-user ``` 上面代碼會觸發兩次newListener事件,以及一次removeListener事件。 ## EventEmitter實例的方法 ### once方法 該方法類似于on方法,但是回調函數只觸發一次。 ```javascript var EventEmitter = require('events').EventEmitter; var myEmitter = new EventEmitter; myEmitter.once('message', function(msg){ console.log('message: ' + msg); }); myEmitter.emit('message', 'this is the first message'); myEmitter.emit('message', 'this is the second message'); myEmitter.emit('message', 'welcome to nodejs'); ``` 上面代碼觸發了三次message事件,但是回調函數只會在第一次調用時運行。 下面代碼指定,一旦服務器連通,只調用一次的回調函數。 ```javascript server.once('connection', function (stream) { console.log('Ah, we have our first user!'); }); ``` 該方法返回一個EventEmitter對象,因此可以鏈式加載監聽函數。 ### removeListener方法 該方法用于移除回調函數。它接受兩個參數,第一個是事件名稱,第二個是回調函數名稱。這就是說,不能用于移除匿名函數。 ```javascript var EventEmitter = require('events').EventEmitter; var emitter = new EventEmitter; emitter.on('message', console.log); setInterval(function(){ emitter.emit('message', 'foo bar'); }, 300); setTimeout(function(){ emitter.removeListener('message', console.log); }, 1000); ``` 上面代碼每300毫秒觸發一次message事件,直到1000毫秒后取消監聽。 另一個例子是使用removeListener方法模擬once方法。 ```javascript var EventEmitter = require('events').EventEmitter; var emitter = new EventEmitter; function onlyOnce () { console.log("You'll never see this again"); emitter.removeListener("firstConnection", onlyOnce); } emitter.on("firstConnection", onlyOnce); ``` **(3)removeAllListeners方法** 該方法用于移除某個事件的所有回調函數。 ```javascript var EventEmitter = require('events').EventEmitter; var emitter = new EventEmitter; // some code here emitter.removeAllListeners("firstConnection"); ``` 如果不帶參數,則表示移除所有事件的所有回調函數。 ```javascript emitter.removeAllListeners(); ``` **(4)listener方法** 該方法接受一個事件名稱作為參數,返回該事件所有回調函數組成的數組。 ```javascript var EventEmitter = require('events').EventEmitter; var ee = new EventEmitter; function onlyOnce () { console.log(ee.listeners("firstConnection")); ee.removeListener("firstConnection", onlyOnce); console.log(ee.listeners("firstConnection")); } ee.on("firstConnection", onlyOnce) ee.emit("firstConnection"); ee.emit("firstConnection"); // [ [Function: onlyOnce] ] // [] ``` 上面代碼顯示兩次回調函數組成的數組,第一次只有一個回調函數onlyOnce,第二次是一個空數組,因為removeListener方法取消了回調函數。 <h2 id="12.10">stream接口</h2> ## Stream是什么? ”流“(stream)這個概念,最簡單的理解,就是在數據還沒有接收完成時,就開始處理它。 ```javascript var fs = require('fs'); fs.createReadStream('./data/customers.csv').pipe(process.stdout); ``` 上面代碼中,`fs.createReadStream`方法以”流“的方式讀取文件,這可以在文件還沒有讀取完的情況下,就輸出到標準輸出。這顯然對大文件的讀取非常有利。 Unix操作系統從很早以前,就有Stream(流)這個概念,它是不同進程之間傳遞數據的一種方式。管道命令Pipe就起到在不同命令之間,連接Stream的作用。 Stream把較大的數據,拆成很小的部分。只要命令部署了Stream接口,就可以把一個流的輸出接到另一個流的輸入。Node引入了這個概念,通過Stream為異步讀寫數據提供的統一接口。無論是硬盤數據、網絡數據,還是內存數據,都可以采用這個接口讀寫。 讀寫數據有兩種方式。一種方式是同步處理,即先將數據全部讀入內存,然后處理。它的優點是符合直覺,流程非常自然,缺點是如果遇到大文件,要花很長時間,可能要過很久才能進入數據處理的步驟。另一種方式就是Stream方式,它是系統讀取外部數據實際上的方式,即每次只讀入數據的一小塊,像“流水”一樣。所以,Stream方式就是每當系統讀入了一小塊數據,就會觸發一個事件,發出“新數據塊”的信號,只要監聽這個事件,就能掌握進展,做出相應處理,這樣就提高了程序的性能。 Stream接口最大特點就是通過事件通信,具有readable、writable、drain、data、end、close等事件,既可以讀取數據,也可以寫入數據。讀寫數據時,每讀入(或寫入)一段數據,就會觸發一次data事件,全部讀取(或寫入)完畢,觸發end事件。如果發生錯誤,則觸發error事件。 一個對象只要部署了Stream接口,就可以從讀取數據,或者寫入數據。Node內部很多涉及IO處理的對象,都部署了Stream接口,比如HTTP連接、文件讀寫、標準輸入輸出等。 ## 基本用法 Node的I/O操作都是異步的,所以與磁盤和網絡的交互,都要通過回調函數。一個典型的寫文件操作,可能像下面這樣。 ```javascript var http = require('http'); var fs = require('fs'); var server = http.createServer(function (req, res) { fs.readFile(__dirname + '/data.txt', function (err, data) { res.end(data); }); }); server.listen(8000); ``` 上面的代碼有一個問題,那就是它必須將整個data.txt文件讀入內存,然后再輸入。如果data.txt非常大,就會占用大量的內容。一旦有多個并發請求,操作就會變得非常緩慢,用戶不得不等很久,才能得到結果。 由于參數req和res都部署了Stream接口,可以使用`fs.createReadStream()`替代`fs.readFile()`,就能解決這個問題。 ```javascript var http = require('http'); var fs = require('fs'); var server = http.createServer(function (req, res) { var stream = fs.createReadStream(__dirname + '/data.txt'); stream.pipe(res); }); server.listen(8000); ``` Stream接口的最大特點,就是數據會發出node和data事件,內置的pipe方法會處理這兩個事件。 數據流通過pipe方法,可以方便地導向其他具有Stream接口的對象。 ```javascript var fs = require('fs'); var zlib = require('zlib'); fs.createReadStream('wow.txt') .pipe(zlib.createGzip()) .pipe(process.stdout); ``` 上面代碼先打開文本文件wow.txt,然后壓縮,再導向標準輸出。 ```javascript fs.createReadStream('wow.txt') .pipe(zlib.createGzip()) .pipe(fs.createWriteStream('wow.gz')); ``` 上面代碼壓縮文件wow.txt以后,又將其寫回壓縮文件。 下面代碼新建一個Stream實例,然后指定寫入事件和終止事件的回調函數,再將其接到標準輸入之上。 ```javascript var stream = require('stream'); var Stream = stream.Stream; var ws = new Stream; ws.writable = true; ws.write = function(data) { console.log("input=" + data); } ws.end = function(data) { console.log("bye"); } process.stdin.pipe(ws); ``` 調用上面的腳本,會產生以下結果。 ```bash $ node pipe_out.js hello input=hello ^d bye ``` 上面代碼調用腳本下,鍵入hello,會輸出`input=hello`。然后按下ctrl-d,會輸出bye。使用管道命令,可以看得更清楚。 ```bash $ echo hello | node pipe_out.js input=hello bye ``` Stream接口分成三類。 - 可讀數據流接口,用于讀取數據。 - 可寫數據流接口,用于寫入數據。 - 雙向數據流接口,用于讀取和寫入數據,比如Node的tcp sockets、zlib、crypto都部署了這個接口。 ## 可讀數據流 “可讀數據流”用來產生數據。它表示數據的來源,只要一個對象提供“可讀數據流”,就表示你可以從其中讀取數據。 ```javascript var Readable = require('stream').Readable; var rs = new Readable; rs.push('beep '); rs.push('boop\n'); rs.push(null); rs.pipe(process.stdout); ``` 上面代碼產生了一個可寫數據流,最后將其寫入標注輸出。可讀數據流的push方法,用來將數據輸入緩存。 `rs.push(null)`中的null,用來告訴rs,數據輸入完畢。 “可讀數據流”有兩種狀態:流動態和暫停態。處于流動態時,數據會盡快地從數據源導向用戶的程序;處于暫停態時,必須顯式調用`stream.read()`等指令,“可讀數據流”才會釋放數據。剛剛新建的時候,“可讀數據流”處于暫停態。 三種方法可以讓暫停態轉為流動態。 - 添加data事件的監聽函數 - 調用resume方法 - 調用pipe方法將數據送往一個可寫數據流 如果轉為流動態時,沒有data事件的監聽函數,也沒有pipe方法的目的地,那么數據將遺失。 以下兩種方法可以讓流動態轉為暫停態。 - 不存在pipe方法的目的地時,調用pause方法 - 存在pipe方法的目的地時,移除所有data事件的監聽函數,并且調用unpipe方法,移除所有pipe方法的目的地 注意,只移除data事件的監聽函數,并不會自動引發數據流進入“暫停態”。另外,存在pipe方法的目的地時,調用pause方法,并不能保證數據流總是處于暫停態,一旦那些目的地發出數據請求,數據流有可能會繼續提供數據。 每當系統有新的數據,該接口可以監聽到data事件,從而回調函數。 ```javascript var fs = require('fs'); var readableStream = fs.createReadStream('file.txt'); var data = ''; readableStream.setEncoding('utf8'); readableStream.on('data', function(chunk) { data+=chunk; }); readableStream.on('end', function() { console.log(data); }); ``` 上面代碼中,fs模塊的createReadStream方法,是部署了Stream接口的文件讀取方法。該方法對指定的文件,返回一個對象。該對象只要監聽data事件,回調函數就能讀到數據。 除了data事件,監聽readable事件,也可以讀到數據。 ```javascript var fs = require('fs'); var readableStream = fs.createReadStream('file.txt'); var data = ''; var chunk; readableStream.setEncoding('utf8'); readableStream.on('readable', function() { while ((chunk=readableStream.read()) !== null) { data += chunk; } }); readableStream.on('end', function() { console.log(data) }); ``` readable事件表示系統緩沖之中有可讀的數據,使用read方法去讀出數據。如果沒有數據可讀,read方法會返回null。 “可讀數據流”除了read方法,還有以下方法。 - Readable.pause() :暫停數據流。已經存在的數據,也不再觸發data事件,數據將保留在緩存之中,此時的數據流稱為靜態數據流。如果對靜態數據流再次調用pause方法,數據流將重新開始流動,但是緩存中現有的數據,不會再觸發data事件。 - Readable.resume():恢復暫停的數據流。 - readable.unpipe():從管道中移除目的地數據流。如果該方法使用時帶有參數,會阻止“可讀數據流”進入某個特定的目的地數據流。如果使用時不帶有參數,則會移除所有的目的地數據流。 ### read() read方法從系統緩存讀取并返回數據。如果讀不到數據,則返回null。 該方法可以接受一個整數作為參數,表示所要讀取數據的數量,然后會返回該數量的數據。如果讀不到足夠數量的數據,返回null。如果不提供這個參數,默認返回系統緩存之中的所有數據。 只在“暫停態”時,該方法才有必要手動調用。“流動態”時,該方法是自動調用的,直到系統緩存之中的數據被讀光。 ```javascript var readable = getReadableStreamSomehow(); readable.on('readable', function() { var chunk; while (null !== (chunk = readable.read())) { console.log('got %d bytes of data', chunk.length); } }); ``` 如果該方法返回一個數據塊,那么它就觸發了data事件。 ### _read() 可讀數據流的_read方法,可以將數據放入可讀數據流。 ```javascript var Readable = require('stream').Readable; var rs = Readable(); var c = 97; rs._read = function () { rs.push(String.fromCharCode(c++)); if (c > 'z'.charCodeAt(0)) rs.push(null); }; rs.pipe(process.stdout); ``` 運行結果如下。 ```bash $ node read1.js abcdefghijklmnopqrstuvwxyz ``` ### setEncoding() 調用該方法,會使得數據流返回指定編碼的字符串,而不是緩存之中的二進制對象。比如,調用`setEncoding('utf8')`,數據流會返回UTF-8字符串,調用`setEncoding('hex')`,數據流會返回16進制的字符串。 該方法會正確處理多字節的字符,而緩存的方法`buf.toString(encoding)`不會。所以如果想要從數據流讀取字符串,應該總是使用該方法。 ```javascript var readable = getReadableStreamSomehow(); readable.setEncoding('utf8'); readable.on('data', function(chunk) { assert.equal(typeof chunk, 'string'); console.log('got %d characters of string data', chunk.length); }); ``` ### resume() resume方法會使得“可讀數據流”繼續釋放data事件,即轉為流動態。 ```javascript var readable = getReadableStreamSomehow(); readable.resume(); readable.on('end', function(chunk) { console.log('數據流到達尾部,未讀取任務數據'); }); ``` 上面代碼中,調用resume方法使得數據流進入流動態,只定義end事件的監聽函數,不定義data事件的監聽函數,表示不從數據流讀取任何數據,只監聽數據流到達尾部。 ### pause() pause方法使得流動態的數據流,停止釋放data事件,轉而進入暫停態。任何此時已經可以讀到的數據,都將停留在系統緩存。 ```javascript var readable = getReadableStreamSomehow(); readable.on('data', function(chunk) { console.log('讀取%d字節的數據', chunk.length); readable.pause(); console.log('接下來的1秒內不讀取數據'); setTimeout(function() { console.log('數據恢復讀取'); readable.resume(); }, 1000); }); ``` ### isPaused() 該方法返回一個布爾值,表示“可讀數據流”被客戶端手動暫停(即調用了pause方法),目前還沒有調用resume方法。 ```javascript var readable = new stream.Readable readable.isPaused() // === false readable.pause() readable.isPaused() // === true readable.resume() readable.isPaused() // === false ``` ### pipe() pipe方法是自動傳送數據的機制,就像管道一樣。它從“可讀數據流”讀出所有數據,將其寫出指定的目的地。整個過程是自動的。 ```javascript src.pipe(dst) ``` pipe方法必須在可讀數據流上調用,它的參數必須是可寫數據流。 ```javascript var fs = require('fs'); var readableStream = fs.createReadStream('file1.txt'); var writableStream = fs.createWriteStream('file2.txt'); readableStream.pipe(writableStream); ``` 上面代碼使用pipe方法,將file1的內容寫入file2。整個過程由pipe方法管理,不用手動干預,所以可以將傳送數據寫得很簡潔。 pipe方法返回目的地的數據流,因此可以使用鏈式寫法,將多個數據流操作連在一起。 ```javascript a.pipe(b).pipe(c).pipe(d) // 等同于 a.pipe(b); b.pipe(c); c.pipe(d); ``` 下面是一個例子。 ```javascript var fs = require('fs'); var zlib = require('zlib'); fs.createReadStream('input.txt.gz') .pipe(zlib.createGunzip()) .pipe(fs.createWriteStream('output.txt')); ``` 上面代碼采用鏈式寫法,先讀取文件,然后進行壓縮,最后輸出。 下面的寫法模擬了Unix系統的cat命令,將標準輸出寫入標準輸入。 ```javascript process.stdin.pipe(process.stdout); ``` 當來源地的數據流讀取完成,默認會調用目的地的end方法,就不再能夠寫入。對pipe方法傳入第二個參數`{ end: false }`,可以讓目的地的數據流保持打開。 ```javascript reader.pipe(writer, { end: false }); reader.on('end', function() { writer.end('Goodbye\n'); }); ``` 上面代碼中,目的地數據流默認不會調用end方法,只能手動調用,因此“Goodbye”會被寫入。 ### unpipe() 該方法移除pipe方法指定的數據流目的地。如果沒有參數,則移除所有的pipe方法目的地。如果有參數,則移除該參數指定的目的地。如果沒有匹配參數的目的地,則不會產生任何效果。 ```javascript var readable = getReadableStreamSomehow(); var writable = fs.createWriteStream('file.txt'); readable.pipe(writable); setTimeout(function() { console.log('停止寫入file.txt'); readable.unpipe(writable); console.log('手動關閉file.txt的寫入數據流'); writable.end(); }, 1000); ``` 上面代碼寫入file.txt的時間,只有1秒鐘,然后就停止寫入。 ### 事件 (1)readable readable事件在數據流能夠向外提供數據時觸發。 ```javascript var readable = getReadableStreamSomehow(); readable.on('readable', function() { // there is some data to read now }); ``` 下面是一個例子。 ```javascript process.stdin.on('readable', function () { var buf = process.stdin.read(); console.dir(buf); }); ``` 上面代碼將標準輸入的數據讀出。 read方法接受一個整數作為參數,表示以多少個字節為單位進行讀取。 ```javascript process.stdin.on('readable', function () { var buf = process.stdin.read(3); console.dir(buf); }); ``` 上面代碼將以3個字節為單位進行輸出內容。 (2)data 對于那些沒有顯式暫停的數據流,添加data事件監聽函數,會將數據流切換到流動態,盡快向外提供數據。 ```javascript var readable = getReadableStreamSomehow(); readable.on('data', function(chunk) { console.log('got %d bytes of data', chunk.length); }); ``` (3)end 無法再讀取到數據時,會觸發end事件。也就是說,只有當前數據被完全讀取完,才會觸發end事件,比如不停地調用read方法。 ```javascript var readable = getReadableStreamSomehow(); readable.on('data', function(chunk) { console.log('got %d bytes of data', chunk.length); }); readable.on('end', function() { console.log('there will be no more data.'); }); ``` (4)close 數據源關閉時,close事件被觸發。并不是所有的數據流都支持這個事件。 (5)error 當讀取數據發生錯誤時,error事件被觸發。 ## 可寫數據流 “可寫數據流”允許你將數據寫入某個目的地。它是數據寫入的一種抽象,不同的數據目的地部署了這個接口以后,就可以用統一的方法寫入。 以下是部署了可寫數據流的一些場合。 - 客戶端的http requests - 服務器的http responses - fs write streams - zlib streams - crypto streams - tcp sockets - child process stdin - process.stdout, process.stderr 下面是fs模塊的可寫數據流的例子。 ```javascript var fs = require('fs'); var readableStream = fs.createReadStream('file1.txt'); var writableStream = fs.createWriteStream('file2.txt'); readableStream.setEncoding('utf8'); readableStream.on('data', function(chunk) { writableStream.write(chunk); }); ``` 上面代碼中,fs模塊的createWriteStream方法針對特定文件,創建了一個“可寫數據流”,本質上就是對寫入操作部署了Stream接口。然后,“可寫數據流”的write方法,可以將數據寫入文件。 ### write() write方法用于向“可寫數據流”寫入數據。它接受兩個參數,一個是寫入的內容,可以是字符串,也可以是一個stream對象(比如可讀數據流),另一個是寫入完成后的回調函數。 它返回一個布爾值,表示本次數據是否處理完成。如果返回true,就表示可以寫入新的數據了。如果等待寫入的數據被緩存了,就返回false。不過,在返回false的情況下,也可以繼續傳入新的數據等待寫入。只是這時,新的數據不會真的寫入,只會緩存在內存中。為了避免內存消耗,比較好的做法還是等待該方法返回true,然后再寫入。 ```javascript var fs = require('fs'); var ws = fs.createWriteStream('message.txt'); ws.write('beep '); setTimeout(function () { ws.end('boop\n'); }, 1000); ``` 上面代碼調用end方法,數據就不再寫入了。 ### cork(),uncork() cork方法可以強制等待寫入的數據進入緩存。當調用uncork方法或end方法時,緩存的數據就會吐出。 ### setDefaultEncoding() setDefaultEncoding方法用于將寫入的數據編碼成新的格式。它返回一個布爾值,表示編碼是否成功,如果返回false就表示編碼失敗。 ### end() end方法用于終止“可寫數據流”。該方法可以接受三個參數,全部都是可選參數。第一個參數是最后所要寫入的數據,可以是字符串,也可以是stream對象;第二個參數是寫入編碼;第三個參數是一個回調函數,finish事件觸發時,會調用這個回調函數。 ```javascript var file = fs.createWriteStream('example.txt'); file.write('hello, '); file.end('world!'); ``` 上面代碼會在數據寫入結束時,在尾部寫入“world!”。 調用end方法之后,再寫入數據會報錯。 ```javascript var file = fs.createWriteStream('example.txt'); file.end('world!'); file.write('hello, '); // 報錯 ``` ### 事件 (1)drain事件 `writable.write(chunk)`返回false以后,當緩存數據全部寫入完成,可以繼續寫入時,會觸發drain事件。 ```javascript function writeOneMillionTimes(writer, data, encoding, callback) { var i = 1000000; write(); function write() { var ok = true; do { i -= 1; if (i === 0) { writer.write(data, encoding, callback); } else { ok = writer.write(data, encoding); } } while (i > 0 && ok); if (i > 0) { writer.once('drain', write); } } } ``` 上面代碼是一個寫入100萬次的例子,通過drain事件得到可以繼續寫入的通知。 (2)finish事件 調用end方法時,所有緩存的數據釋放,觸發finish事件。該事件的回調函數沒有參數。 ```javascript var writer = getWritableStreamSomehow(); for (var i = 0; i < 100; i ++) { writer.write('hello, #' + i + '!\n'); } writer.end('this is the end\n'); writer.on('finish', function() { console.error('all writes are now complete.'); }); ``` (3)pipe事件 “可寫數據流”調用pipe方法,將數據流導向寫入目的地時,觸發該事件。 該事件的回調函數,接受發出該事件的“可讀數據流”對象作為參數。 ```javascript var writer = getWritableStreamSomehow(); var reader = getReadableStreamSomehow(); writer.on('pipe', function(src) { console.error('something is piping into the writer'); assert.equal(src, reader); }); reader.pipe(writer); ``` (4)unpipe事件 “可讀數據流”調用unpipe方法,將可寫數據流移出寫入目的地時,觸發該事件。 該事件的回調函數,接受發出該事件的“可讀數據流”對象作為參數。 ```javascript var writer = getWritableStreamSomehow(); var reader = getReadableStreamSomehow(); writer.on('unpipe', function(src) { console.error('something has stopped piping into the writer'); assert.equal(src, reader); }); reader.pipe(writer); reader.unpipe(writer); ``` (5)error事件 如果寫入數據或pipe數據時發生錯誤,就會觸發該事件。 該事件的回調函數,接受一個Error對象作為參數。 ## HTTP請求 HTTP對象使用Stream接口,實現網絡數據的讀寫。 ```javascript var http = require('http'); var server = http.createServer(function (req, res) { // req is an http.IncomingMessage, which is a Readable Stream // res is an http.ServerResponse, which is a Writable Stream var body = ''; // we want to get the data as utf8 strings // If you don't set an encoding, then you'll get Buffer objects req.setEncoding('utf8'); // Readable streams emit 'data' events once a listener is added req.on('data', function (chunk) { body += chunk; }); // the end event tells you that you have entire body req.on('end', function () { try { var data = JSON.parse(body); } catch (er) { // uh oh! bad json! res.statusCode = 400; return res.end('error: ' + er.message); } // write back something interesting to the user: res.write(typeof data); res.end(); }); }); server.listen(1337); // $ curl localhost:1337 -d '{}' // object // $ curl localhost:1337 -d '"foo"' // string // $ curl localhost:1337 -d 'not json' // error: Unexpected token o ``` data事件表示讀取或寫入了一塊數據。 ```javascript req.on('data', function(buf){ // Do something with the Buffer }); ``` 使用req.setEncoding方法,可以設定字符串編碼。 ```javascript req.setEncoding('utf8'); req.on('data', function(str){ // Do something with the String }); ``` end事件,表示讀取或寫入數據完畢。 ```javascript var http = require('http'); http.createServer(function(req, res){ res.writeHead(200); req.on('data', function(data){ res.write(data); }); req.on('end', function(){ res.end(); }); }).listen(3000); ``` 上面代碼相當于建立了“回聲”服務,將HTTP請求的數據體,用HTTP回應原樣發送回去。 system模塊提供了pump方法,有點像Linux系統的管道功能,可以將一個數據流,原封不動得轉給另一個數據流。所以,上面的例子也可以用pump方法實現。 ```javascript var http = require('http'), sys = require('sys'); http.createServer(function(req, res){ res.writeHead(200); sys.pump(req, res); }).listen(3000); ``` ## fs模塊 fs模塊的createReadStream方法用于新建讀取數據流,createWriteStream方法用于新建寫入數據流。使用這兩個方法,可以做出一個用于文件復制的腳本copy.js。 ```javascript // copy.js var fs = require('fs'); console.log(process.argv[2], '->', process.argv[3]); var readStream = fs.createReadStream(process.argv[2]); var writeStream = fs.createWriteStream(process.argv[3]); readStream.on('data', function (chunk) { writeStream.write(chunk); }); readStream.on('end', function () { writeStream.end(); }); readStream.on('error', function (err) { console.log("ERROR", err); }); writeStream.on('error', function (err) { console.log("ERROR", err); });d all your errors, you wouldn't need to use domains. ``` 上面代碼非常容易理解,使用的時候直接提供源文件路徑和目標文件路徑,就可以了。 ```bash node cp.js src.txt dest.txt ``` Streams對象都具有pipe方法,起到管道作用,將一個數據流輸入另一個數據流。所以,上面代碼可以重寫成下面這樣: ```javascript var fs = require('fs'); console.log(process.argv[2], '->', process.argv[3]); var readStream = fs.createReadStream(process.argv[2]); var writeStream = fs.createWriteStream(process.argv[3]); readStream.on('open', function () { readStream.pipe(writeStream); }); readStream.on('end', function () { writeStream.end(); }); ``` ## 錯誤處理 下面是壓縮后發送文件的代碼。 ```javascript http.createServer(function (req, res) { // set the content headers fs.createReadStream('filename.txt') .pipe(zlib.createGzip()) .pipe(res) }) ``` 上面的代碼沒有部署錯誤處理機制,一旦發生錯誤,就無法處理。所以,需要加上error事件的監聽函數。 ```javascript http.createServer(function (req, res) { // set the content headers fs.createReadStream('filename.txt') .on('error', onerror) .pipe(zlib.createGzip()) .on('error', onerror) .pipe(res) function onerror(err) { console.error(err.stack) } }) ``` 上面的代碼還是存在問題,如果客戶端中斷下載,寫入的數據流就會收不到close事件,一直處于等待狀態,從而造成內存泄漏。因此,需要使用[on-finished模塊](https://github.com/jshttp/on-finished)用來處理這種情況。 ```javascript http.createServer(function (req, res) { var stream = fs.createReadStream('filename.txt') // set the content headers stream .on('error', onerror) .pipe(zlib.createGzip()) .on('error', onerror) .pipe(res) onFinished(res, function () { // make sure the stream is always destroyed stream.destroy() }) }) ``` <h2 id="12.11">Child Process模塊</h2> child_process模塊用于新建子進程。子進程的運行結果儲存在系統緩存之中(最大200KB),等到子進程運行結束以后,主進程再用回調函數讀取子進程的運行結果。 ## exec() `exec`方法用于執行bash命令,它的參數是一個命令字符串。 ```javascript var exec = require('child_process').exec; var ls = exec('ls -l', function (error, stdout, stderr) { if (error) { console.log(error.stack); console.log('Error code: ' + error.code); } console.log('Child Process STDOUT: ' + stdout); }); ``` 上面代碼的`exec`方法用于新建一個子進程,然后緩存它的運行結果,運行結束后調用回調函數。 `exec`方法最多可以接受兩個參數,第一個參數是所要執行的shell命令,第二個參數是回調函數,該函數接受三個參數,分別是發生的錯誤、標準輸出的顯示結果、標準錯誤的顯示結果。 由于標準輸出和標準錯誤都是流對象(stream),可以監聽data事件,因此上面的代碼也可以寫成下面這樣。 ```javascript var exec = require('child_process').exec; var child = exec('ls -l'); child.stdout.on('data', function(data) { console.log('stdout: ' + data); }); child.stderr.on('data', function(data) { console.log('stdout: ' + data); }); child.on('close', function(code) { console.log('closing code: ' + code); }); ``` 上面的代碼還表明,子進程本身有`close`事件,可以設置回調函數。 上面的代碼還有一個好處。監聽data事件以后,可以實時輸出結果,否則只有等到子進程結束,才會輸出結果。所以,如果子進程運行時間較長,或者是持續運行,第二種寫法更好。 下面是另一個例子,假定有一個child.js文件。 ```javascript // child.js var exec = require('child_process').exec; exec('node -v', function(error, stdout, stderr) { console.log('stdout: ' + stdout); console.log('stderr: ' + stderr); if (error !== null) { console.log('exec error: ' + error); } }); ``` 運行后,該文件的輸出結果如下。 ```bash $ node child.js stdout: v0.11.14 stderr: ``` exec方法會直接調用bash(`/bin/sh`程序)來解釋命令,所以如果有用戶輸入的參數,exec方法是不安全的。 ```javascript var path = ";user input"; child_process.exec('ls -l ' + path, function (err, data) { console.log(data); }); ``` 上面代碼表示,在bash環境下,`ls -l; user input`會直接運行。如果用戶輸入惡意代碼,將會帶來安全風險。因此,在有用戶輸入的情況下,最好不使用`exec`方法,而是使用`execFile`方法。 ## execSync() `execSync`是`exec`的同步執行版本。 它可以接受兩個參數,第一個參數是所要執行的命令,第二個參數用來配置執行環境。 ```javascript var execSync = require("child_process").execSync; var SEPARATOR = process.platform === 'win32' ? ';' : ':'; var env = Object.assign({}, process.env); env.PATH = path.resolve('./node_modules/.bin') + SEPARATOR + env.PATH; function myExecSync(cmd) { var output = execSync(cmd, { cwd: process.cwd(), env: env }); console.log(output); } myExecSync('eslint .'); ``` 上面代碼中,`execSync`方法的第二個參數是一個對象。該對象的`cwd`屬性指定腳本的當前目錄,`env`屬性指定環境變量。上面代碼將`./node_modules/.bin`目錄,存入`$PATH`變量。這樣就可以不加路徑,引用項目內部的模塊命令了,比如`eslint`命令實際執行的是`./node_modules/.bin/eslint`。 ## execFile() execFile方法直接執行特定的程序,參數作為數組傳入,不會被bash解釋,因此具有較高的安全性。 ```javascript var child_process = require('child_process'); var path = "."; child_process.execFile('/bin/ls', ['-l', path], function (err, result) { console.log(result) }); ``` 上面代碼中,假定`path`來自用戶輸入,如果其中包含了分號或反引號,ls程序不理解它們的含義,因此也就得不到運行結果,安全性就得到了提高。 ## spawn() spawn方法創建一個子進程來執行特定命令,用法與execFile方法類似,但是沒有回調函數,只能通過監聽事件,來獲取運行結果。它屬于異步執行,適用于子進程長時間運行的情況。 ```javascript var child_process = require('child_process'); var path = '.'; var ls = child_process.spawn('/bin/ls', ['-l', path]); ls.stdout.on('data', function (data) { console.log('stdout: ' + data); }); ls.stderr.on('data', function (data) { console.log('stderr: ' + data); }); ls.on('close', function (code) { console.log('child process exited with code ' + code); }); ``` spawn方法接受兩個參數,第一個是可執行文件,第二個是參數數組。 spawn對象返回一個對象,代表子進程。該對象部署了EventEmitter接口,它的`data`事件可以監聽,從而得到子進程的輸出結果。 spawn方法與exec方法非常類似,只是使用格式略有區別。 ```javascript child_process.exec(command, [options], callback) child_process.spawn(command, [args], [options]) ``` ## fork() fork方法直接創建一個子進程,執行Node腳本,`fork('./child.js')` 相當于 `spawn('node', ['./child.js'])` 。與spawn方法不同的是,fork會在父進程與子進程之間,建立一個通信管道,用于進程之間的通信。 ```javascript var n = child_process.fork('./child.js'); n.on('message', function(m) { console.log('PARENT got message:', m); }); n.send({ hello: 'world' }); ``` 上面代碼中,fork方法返回一個代表進程間通信管道的對象,對該對象可以監聽message事件,用來獲取子進程返回的信息,也可以向子進程發送信息。 child.js腳本的內容如下。 ```javascript process.on('message', function(m) { console.log('CHILD got message:', m); }); process.send({ foo: 'bar' }); ``` 上面代碼中,子進程監聽message事件,并向父進程發送信息。 ## send() 使用 child_process.fork() 生成新進程之后,就可以用 child.send(message, [sendHandle]) 向新進程發送消息。新進程中通過監聽message事件,來獲取消息。 下面的例子是主進程的代碼。 ```javascript var cp = require('child_process'); var n = cp.fork(__dirname + '/sub.js'); n.on('message', function(m) { console.log('PARENT got message:', m); }); n.send({ hello: 'world' }); ``` 下面是子進程sub.js代碼。 ```javascript process.on('message', function(m) { console.log('CHILD got message:', m); }); process.send({ foo: 'bar' }); ``` <h2 id="12.12">Http模塊</h2> ## 基本用法 ### 處理GET請求 `http`模塊主要用于搭建HTTP服務。使用Node搭建HTTP服務器非常簡單。 ```javascript var http = require('http'); http.createServer(function (request, response){ response.writeHead(200, {'Content-Type': 'text/plain'}); response.end('Hello World\n'); }).listen(8080, '127.0.0.1'); console.log('Server running on port 8080.'); ``` 上面代碼第一行`var http = require("http")`,表示加載`http`模塊。然后,調用`http`模塊的`createServer`方法,創造一個服務器實例。 `ceateServer`方法接受一個函數作為參數,該函數的`request`參數是一個對象,表示客戶端的HTTP請求;`response`參數也是一個對象,表示服務器端的HTTP回應。`response.writeHead`方法用來寫入HTTP回應的頭信息;`response.end`方法用來寫入HTTP回應的具體內容,以及回應完成后關閉本次對話。最后的`listen(8080)`表示啟動服務器實例,監聽本機的8080端口。 將上面這幾行代碼保存成文件`app.js`,然后執行該腳本,服務器就開始運行了。 ```bash $ node app.js ``` 這時命令行窗口將顯示一行提示“Server running at port 8080.”。打開瀏覽器,訪問http://localhost:8080,網頁顯示“Hello world!”。 上面的例子是收到請求后生成網頁,也可以事前寫好網頁,存在文件中,然后利用`fs`模塊讀取網頁文件,將其返回。 ```javascript var http = require('http'); var fs = require('fs'); http.createServer(function (request, response){ fs.readFile('data.txt', function readData(err, data) { response.writeHead(200, {'Content-Type': 'text/plain'}); response.end(data); }); // 或者 fs.createReadStream(`${__dirname}/index.html`).pipe(response); }).listen(8080, '127.0.0.1'); console.log('Server running on port 8080.'); ``` 下面的修改則是根據不同網址的請求,顯示不同的內容,已經相當于做出一個網站的雛形了。 ```javascript var http = require("http"); http.createServer(function(req, res) { // 主頁 if (req.url == "/") { res.writeHead(200, { "Content-Type": "text/html" }); res.end("Welcome to the homepage!"); } // About頁面 else if (req.url == "/about") { res.writeHead(200, { "Content-Type": "text/html" }); res.end("Welcome to the about page!"); } // 404錯誤 else { res.writeHead(404, { "Content-Type": "text/plain" }); res.end("404 error! File not found."); } }).listen(8080, "localhost"); ``` 回調函數的req(request)對象,擁有以下屬性。 - url:發出請求的網址。 - method:HTTP請求的方法。 - headers:HTTP請求的所有HTTP頭信息。 ### 處理POST請求 當客戶端采用POST方法發送數據時,服務器端可以對data和end兩個事件,設立監聽函數。 ```javascript var http = require('http'); http.createServer(function (req, res) { var content = ""; req.on('data', function (chunk) { content += chunk; }); req.on('end', function () { res.writeHead(200, {"Content-Type": "text/plain"}); res.write("You've sent: " + content); res.end(); }); }).listen(8080); ``` data事件會在數據接收過程中,每收到一段數據就觸發一次,接收到的數據被傳入回調函數。end事件則是在所有數據接收完成后觸發。 對上面代碼稍加修改,就可以做出文件上傳的功能。 ```javascript "use strict"; var http = require('http'); var fs = require('fs'); var destinationFile, fileSize, uploadedBytes; http.createServer(function (request, response) { response.writeHead(200); destinationFile = fs.createWriteStream("destination.md"); request.pipe(destinationFile); fileSize = request.headers['content-length']; uploadedBytes = 0; request.on('data', function (d) { uploadedBytes += d.length; var p = (uploadedBytes / fileSize) * 100; response.write("Uploading " + parseInt(p, 0) + " %\n"); }); request.on('end', function () { response.end("File Upload Complete"); }); }).listen(3030, function () { console.log("server started"); }); ``` ## 發出請求 ### get() get方法用于發出get請求。 ```javascript function getTestPersonaLoginCredentials(callback) { return http.get({ host: 'personatestuser.org', path: '/email' }, function(response) { var body = ''; response.on('data', function(d) { body += d; }); response.on('end', function() { var parsed = JSON.parse(body); callback({ email: parsed.email, password: parsed.pass }); }); }); }, ``` ### request() request方法用于發出HTTP請求,它的使用格式如下。 ```javascript http.request(options[, callback]) ``` request方法的options參數,可以是一個對象,也可以是一個字符串。如果是字符串,就表示這是一個URL,Node內部就會自動調用`url.parse()`,處理這個參數。 options對象可以設置如下屬性。 - host:HTTP請求所發往的域名或者IP地址,默認是localhost。 - hostname:該屬性會被`url.parse()`解析,優先級高于host。 - port:遠程服務器的端口,默認是80。 - localAddress:本地網絡接口。 - socketPath:Unix網絡套接字,格式為host:port或者socketPath。 - method:指定HTTP請求的方法,格式為字符串,默認為GET。 - path:指定HTTP請求的路徑,默認為根路徑(/)。可以在這個屬性里面,指定查詢字符串,比如`/index.html?page=12`。如果這個屬性里面包含非法字符(比如空格),就會拋出一個錯誤。 - headers:一個對象,包含了HTTP請求的頭信息。 - auth:一個代表HTTP基本認證的字符串`user:password`。 - agent:控制緩存行為,如果HTTP請求使用了agent,則HTTP請求默認為`Connection: keep-alive`,它的可能值如下: - undefined(默認):對當前host和port,使用全局Agent。 - Agent:一個對象,會傳入agent屬性。 - false:不緩存連接,默認HTTP請求為`Connection: close`。 - keepAlive:一個布爾值,表示是否保留socket供未來其他請求使用,默認等于false。 - keepAliveMsecs:一個整數,當使用KeepAlive的時候,設置多久發送一個TCP KeepAlive包,使得連接不要被關閉。默認等于1000,只有keepAlive設為true的時候,該設置才有意義。 request方法的callback參數是可選的,在response事件發生時觸發,而且只觸發一次。 `http.request()`返回一個`http.ClientRequest`類的實例。它是一個可寫數據流,如果你想通過POST方法發送一個文件,可以將文件寫入這個ClientRequest對象。 下面是發送POST請求的一個例子。 ```javascript var postData = querystring.stringify({ 'msg' : 'Hello World!' }); var options = { hostname: 'www.google.com', port: 80, path: '/upload', method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': postData.length } }; var req = http.request(options, function(res) { console.log('STATUS: ' + res.statusCode); console.log('HEADERS: ' + JSON.stringify(res.headers)); res.setEncoding('utf8'); res.on('data', function (chunk) { console.log('BODY: ' + chunk); }); }); req.on('error', function(e) { console.log('problem with request: ' + e.message); }); // write data to request body req.write(postData); req.end(); ``` 注意,上面代碼中,`req.end()`必須被調用,即使沒有在請求體內寫入任何數據,也必須調用。因為這表示已經完成HTTP請求。 發送過程的任何錯誤(DNS錯誤、TCP錯誤、HTTP解析錯誤),都會在request對象上觸發error事件。 ## 搭建HTTPs服務器 搭建HTTPs服務器需要有SSL證書。對于向公眾提供服務的網站,SSL證書需要向證書頒發機構購買;對于自用的網站,可以自制。 自制SSL證書需要OpenSSL,具體命令如下。 ```bash $ openssl genrsa -out key.pem $ openssl req -new -key key.pem -out csr.pem $ openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem $ rm csr.pem ``` 上面的命令生成兩個文件:ert.pem(證書文件)和 key.pem(私鑰文件)。有了這兩個文件,就可以運行HTTPs服務器了。 Node內置Https支持。 ```javascript var server = https.createServer({ key: privateKey, cert: certificate, ca: certificateAuthorityCertificate }, app); ``` Node.js提供一個https模塊,專門用于處理加密訪問。 ```javascript var https = require('https'); var fs = require('fs'); var options = { key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') }; var a = https.createServer(options, function (req, res) { res.writeHead(200); res.end("hello world\n"); }).listen(8000); ``` 上面代碼顯示,HTTPs服務器與HTTP服務器的最大區別,就是createServer方法多了一個options參數。運行以后,就可以測試是否能夠正常訪問。 ```bash curl -k https://localhost:8000 ``` ## 模塊屬性 (1)HTTP請求的屬性 - headers:HTTP請求的頭信息。 - url:請求的路徑。 ## 模塊方法 (1)http模塊的方法 - createServer(callback):創造服務器實例。 (2)服務器實例的方法 - listen(port):啟動服務器監聽指定端口。 (3)HTTP回應的方法 - setHeader(key, value):指定HTTP頭信息。 - write(str):指定HTTP回應的內容。 - end():發送HTTP回應。 <h2 id="12.13">assert 模塊</h2> assert模塊是Node的內置模塊,主要用于斷言。如果表達式不符合預期,就拋出一個錯誤。該模塊提供11個方法,但只有少數幾個是常用的。 ## assert() assert方法接受兩個參數,當第一個參數對應的布爾值為true時,不會有任何提示,返回undefined。當第一個參數對應的布爾值為false時,會拋出一個錯誤,該錯誤的提示信息就是第二個參數設定的字符串。 ```javascript // 格式 assert(value, message) // 例子 var assert = require('assert'); function add (a, b) { return a + b; } var expected = add(1,2); assert( expected === 3, '預期1加2等于3'); ``` 上面代碼不會有任何輸出,因為assert方法的第一個參數是true。 ```javascript assert( expected === 4, '預期1加2等于3') // AssertionError: 預期1加2等于3 ``` 上面代碼會拋出一個錯誤,因為assert方法的第一個參數是false。 ## assert.ok() ok是assert方法的另一個名字,與assert方法完全一樣。 ## assert.equal() equal方法接受三個參數,第一個參數是實際值,第二個是預期值,第三個是錯誤的提示信息。 ```javascript // 格式 assert.equal(actual, expected, [message]) assert.equal(true, value, message); // 等同于 assert(value, message); // 例子 var assert = require('assert'); function add (a, b) { return a + b; } var expected = add(1,2); // 以下三句效果相同 assert(expected == 3, '預期1+2等于3'); assert.ok(expected == 3, '預期1+2等于3'); assert.equal(expected, 3, '預期1+2等于3'); ``` equal方法內部使用的是相等運算符(==),而不是嚴格運算符(===),進行比較運算。 ## assert.notEqual() notEqual方法的用法與equal方法類似,但只有在實際值等于預期值時,才會拋出錯誤。 ```javascript // 格式 assert.notEqual(actual, expected, [message]) // 用法 var assert = require('assert'); function add (a, b) { return a + b; } var expected = add(1,2); // 以下三種寫法效果相同 assert(expected != 4, '預期不等于4'); assert.ok(expected != 4, '預期不等于4'); assert.notEqual(expected, 4, '預期不等于4'); ``` notEqual方法內部使用不相等運算符(!=),而不是嚴格不相等運算符(!==),進行比較運算。 ## assert.deepEqual() deepEqual方法用來比較兩個對象。只要它們的屬性一一對應,且值都相等,就認為兩個對象相等,否則拋出一個錯誤。 ```javascript // 格式 assert.deepEqual(actual, expected, [message]) // 例子 var assert = require('assert'); var list1 = [1, 2, 3, 4, 5]; var list2 = [1, 2, 3, 4, 5]; assert.deepEqual(list1, list2, '預期兩個數組應該有相同的屬性'); var person1 = { "name":"john", "age":"21" }; var person2 = { "name":"john", "age":"21" }; assert.deepEqual(person1, person2, '預期兩個對象應該有相同的屬性'); ``` ## assert.notDeepEqual() notDeepEqual方法與deepEqual方法正好相反,用來斷言兩個對象是否不相等。 ```javascript // 格式 assert.notDeepEqual(actual, expected, [message]) // 例子 var assert = require('assert'); var list1 = [1, 2, ,3, 4, 5]; var list2 = [1, 2, 3, 4, 5]; assert.notDeepEqual(list1, list2, '預期兩個對象不相等'); var person1 = { "name":"john", "age":"21" }; var person2 = { "name":"jane", "age":"19" }; // deepEqual checks the elements in the objects are identical assert.notDeepEqual(person1, person2, '預期兩個對象不相等'); ``` ## assert.strictEqual() strictEqual方法使用嚴格相等運算符(===),比較兩個表達式。 ```javascript // 格式 assert.strictEqual(actual, expected, [message]) // 例子 var assert = require('assert'); assert.strictEqual(1, '1', '預期嚴格相等'); // AssertionError: 預期嚴格相等 ``` ## assert.notStrictEqual() assert.notStrictEqual方法使用嚴格不相等運算符(!==),比較兩個表達式。 ```javascript // 格式 assert.notStrictEqual(actual, expected, [message]) // 例子 var assert = require('assert'); assert.notStrictEqual(1, true, '預期嚴格不相等'); ``` ## assert.throws() throws方法預期某個代碼塊會拋出一個錯誤,且拋出的錯誤符合指定的條件。 ```javascript // 格式 assert.throws(block, [error], [message]) // 例一,拋出的錯誤符合某個構造函數 assert.throws( function() { throw new Error("Wrong value"); }, Error, '不符合預期的錯誤類型' ); // 例二、拋出錯誤的提示信息符合正則表達式 assert.throws( function() { throw new Error("Wrong value"); }, /value/, '不符合預期的錯誤類型' ); // 例三、拋出的錯誤符合自定義函數的校驗 assert.throws( function() { throw new Error("Wrong value"); }, function(err) { if ( (err instanceof Error) && /value/.test(err) ) { return true; } }, '不符合預期的錯誤類型' ); ``` ## assert.doesNotThrow() doesNotThrow方法與throws方法正好相反,預期某個代碼塊不拋出錯誤。 ```javascript // 格式 assert.doesNotThrow(block, [message]) // 用法 assert.doesNotThrow( function() { console.log("Nothing to see here"); }, '預期不拋出錯誤' ); ``` ## assert.ifError() ifError方法斷言某個表達式是否false,如果該表達式對應的布爾值等于true,就拋出一個錯誤。它對于驗證回調函數的第一個參數十分有用,如果該參數為true,就表示有錯誤。 ```javascript // 格式 assert.ifError(value) // 用法 function sayHello(name, callback) { var error = false; var str = "Hello "+name; callback(error, str); } // use the function sayHello('World', function(err, value){ assert.ifError(err); // ... }) ``` ## assert.fail() fail方法用于拋出一個錯誤。 ```javascript // 格式 assert.fail(actual, expected, message, operator) // 例子 var assert = require('assert'); assert.fail(21, 42, 'Test Failed', '###') // AssertionError: Test Failed assert.fail(21, 21, 'Test Failed', '###') // AssertionError: Test Failed assert.fail(21, 42, undefined, '###') // AssertionError: 21 ### 42 ``` 該方法共有四個參數,但是不管參數是什么值,它總是拋出一個錯誤。如果message參數對應的布爾值不為false,拋出的錯誤信息就是message,否則錯誤信息就是“實際值 + 分隔符 + 預期值”。 <h2 id="12.14">Cluster模塊</h2> ## 概述 ### 基本用法 Node.js默認單進程運行,對于32位系統最高可以使用512MB內存,對于64位最高可以使用1GB內存。對于多核CPU的計算機來說,這樣做效率很低,因為只有一個核在運行,其他核都在閑置。cluster模塊就是為了解決這個問題而提出的。 cluster模塊允許設立一個主進程和若干個worker進程,由主進程監控和協調worker進程的運行。worker之間采用進程間通信交換消息,cluster模塊內置一個負載均衡器,采用Round-robin算法協調各個worker進程之間的負載。運行時,所有新建立的鏈接都由主進程完成,然后主進程再把TCP連接分配給指定的worker進程。 ```javascript var cluster = require('cluster'); var os = require('os'); if (cluster.isMaster){ for (var i = 0, n = os.cpus().length; i < n; i += 1){ cluster.fork(); } } else { http.createServer(function(req, res) { res.writeHead(200); res.end("hello world\n"); }).listen(8000); } ``` 上面代碼先判斷當前進程是否為主進程(cluster.isMaster),如果是的,就按照CPU的核數,新建若干個worker進程;如果不是,說明當前進程是worker進程,則在該進程啟動一個服務器程序。 上面這段代碼有一個缺點,就是一旦work進程掛了,主進程無法知道。為了解決這個問題,可以在主進程部署online事件和exit事件的監聽函數。 ```javascript var cluster = require('cluster'); if(cluster.isMaster) { var numWorkers = require('os').cpus().length; console.log('Master cluster setting up ' + numWorkers + ' workers...'); for(var i = 0; i < numWorkers; i++) { cluster.fork(); } cluster.on('online', function(worker) { console.log('Worker ' + worker.process.pid + ' is online'); }); cluster.on('exit', function(worker, code, signal) { console.log('Worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal); console.log('Starting a new worker'); cluster.fork(); }); } ``` 上面代碼中,主進程一旦監聽到worker進程的exit事件,就會重啟一個worker進程。worker進程一旦啟動成功,可以正常運行了,就會發出online事件。 ### worker對象 worker對象是`cluster.fork()`的返回值,代表一個worker進程。 它的屬性和方法如下。 (1)worker.id worker.id返回當前worker的獨一無二的進程編號。這個編號也是cluster.workers中指向當前進程的索引值。 (2)worker.process 所有的worker進程都是用child_process.fork()生成的。child_process.fork()返回的對象,就被保存在worker.process之中。通過這個屬性,可以獲取worker所在的進程對象。 (3)worker.send() 該方法用于在主進程中,向子進程發送信息。 ```javascript if (cluster.isMaster) { var worker = cluster.fork(); worker.send('hi there'); } else if (cluster.isWorker) { process.on('message', function(msg) { process.send(msg); }); } ``` 上面代碼的作用是,worker進程對主進程發出的每個消息,都做回聲。 在worker進程中,要向主進程發送消息,使用`process.send(message)`;要監聽主進程發出的消息,使用下面的代碼。 ```javascript process.on('message', function(message) { console.log(message); }); ``` 發出的消息可以字符串,也可以是JSON對象。下面是一個發送JSON對象的例子。 ```javascript worker.send({ type: 'task 1', from: 'master', data: { // the data that you want to transfer } }); ``` ### cluster.workers對象 該對象只有主進程才有,包含了所有worker進程。每個成員的鍵值就是一個worker進程對象,鍵名就是該worker進程的worker.id屬性。 ```javascript function eachWorker(callback) { for (var id in cluster.workers) { callback(cluster.workers[id]); } } eachWorker(function(worker) { worker.send('big announcement to all workers'); }); ``` 上面代碼用來遍歷所有worker進程。 當前socket的data事件,也可以用id屬性識別worker進程。 ```javascript socket.on('data', function(id) { var worker = cluster.workers[id]; }); ``` ## cluster模塊的屬性與方法 ### isMaster,isWorker isMaster屬性返回一個布爾值,表示當前進程是否為主進程。這個屬性由process.env.NODE_UNIQUE_ID決定,如果process.env.NODE_UNIQUE_ID為未定義,就表示該進程是主進程。 isWorker屬性返回一個布爾值,表示當前進程是否為work進程。它與isMaster屬性的值正好相反。 ### fork() fork方法用于新建一個worker進程,上下文都復制主進程。只有主進程才能調用這個方法。 該方法返回一個worker對象。 ### kill() kill方法用于終止worker進程。它可以接受一個參數,表示系統信號。 如果當前是主進程,就會終止與worker.process的聯絡,然后將系統信號法發向worker進程。如果當前是worker進程,就會終止與主進程的通信,然后退出,返回0。 在以前的版本中,該方法也叫做 worker.destroy() 。 ### listening事件 worker進程調用listen方面以后,“listening”就傳向該進程的服務器,然后傳向主進程。 該事件的回調函數接受兩個參數,一個是當前worker對象,另一個是地址對象,包含網址、端口、地址類型(IPv4、IPv6、Unix socket、UDP)等信息。這對于那些服務多個網址的Node應用程序非常有用。 ```javascript cluster.on('listening', function(worker, address) { console.log("A worker is now connected to " + address.address + ":" + address.port); }); ``` ## 不中斷地重啟Node服務 ### 思路 重啟服務需要關閉后再啟動,利用cluster模塊,可以做到先啟動一個worker進程,再把原有的所有work進程關閉。這樣就能實現不中斷地重啟Node服務。 首先,主進程向worker進程發出重啟信號。 ```javascript workers[wid].send({type: 'shutdown', from: 'master'}); ``` worker進程監聽message事件,一旦發現內容是shutdown,就退出。 ```javascript process.on('message', function(message) { if(message.type === 'shutdown') { process.exit(0); } }); ``` 下面是一個關閉所有worker進程的函數。 ```javascript function restartWorkers() { var wid, workerIds = []; for(wid in cluster.workers) { workerIds.push(wid); } workerIds.forEach(function(wid) { cluster.workers[wid].send({ text: 'shutdown', from: 'master' }); setTimeout(function() { if(cluster.workers[wid]) { cluster.workers[wid].kill('SIGKILL'); } }, 5000); }); }; ``` ### 實例 下面是一個完整的實例,先是主進程的代碼master.js。 ```javascript var cluster = require('cluster'); console.log('started master with ' + process.pid); // 新建一個worker進程 cluster.fork(); process.on('SIGHUP', function () { console.log('Reloading...'); var new_worker = cluster.fork(); new_worker.once('listening', function () { // 關閉所有其他worker進程 for(var id in cluster.workers) { if (id === new_worker.id.toString()) continue; cluster.workers[id].kill('SIGTERM'); } }); }); ``` 上面代碼中,主進程監聽SIGHUP事件,如果發生該事件就關閉其他所有worker進程。之所以是SIGHUP事件,是因為nginx服務器監聽到這個信號,會創造一個新的worker進程,重新加載配置文件。另外,關閉worker進程時,主進程發送SIGTERM信號,這是因為Node允許多個worker進程監聽同一個端口。 下面是worker進程的代碼server.js。 ```javascript var cluster = require('cluster'); if (cluster.isMaster) { require('./master'); return; } var express = require('express'); var http = require('http'); var app = express(); app.get('/', function (req, res) { res.send('ha fsdgfds gfds gfd!'); }); http.createServer(app).listen(8080, function () { console.log('http://localhost:8080'); }); ``` 使用時代碼如下。 ```bash $ node server.js started master with 10538 http://localhost:8080 ``` 然后,向主進程連續發出兩次SIGHUP信號。 ```bash $ kill -SIGHUP 10538 $ kill -SIGHUP 10538 ``` 主進程會連續兩次新建一個worker進程,然后關閉所有其他worker進程,顯示如下。 ```bash Reloading... http://localhost:8080 Reloading... http://localhost:8080 ``` 最后,向主進程發出SIGTERM信號,關閉主進程。 ```bash $ kill 10538 ``` ## PM2模塊 PM2模塊是cluster模塊的一個包裝層。它的作用是盡量將cluster模塊抽象掉,讓用戶像使用單進程一樣,部署多進程Node應用。 ```javascript // app.js var http = require('http'); http.createServer(function(req, res) { res.writeHead(200); res.end("hello world"); }).listen(8080); ``` 上面代碼是標準的Node架設Web服務器的方式,然后用PM2從命令行啟動這段代碼。 ```javascript $ pm2 start app.js -i 4 ``` 上面代碼的i參數告訴PM2,這段代碼應該在cluster_mode啟動,且新建worker進程的數量是4個。如果i參數的值是0,那么當前機器有幾個CPU內核,PM2就會啟動幾個worker進程。 如果一個worker進程由于某種原因掛掉了,會立刻重啟該worker進程。 ```bash # 重啟所有worker進程 $ pm2 reload all ``` 每個worker進程都有一個id,可以用下面的命令查看單個worker進程的詳情。 ```bash $ pm2 show <worker id> ``` 正確情況下,PM2采用fork模式新建worker進程,即主進程fork自身,產生一個worker進程。`pm2 reload`命令則會用spawn方式啟動,即一個接一個啟動worker進程,一個新的worker啟動成功,再殺死一個舊的worker進程。采用這種方式,重新部署新版本時,服務器就不會中斷服務。 ```bash $ pm2 reload <腳本文件名> ``` 關閉worker進程的時候,可以部署下面的代碼,讓worker進程監聽shutdown消息。一旦收到這個消息,進行完畢收尾清理工作再關閉。 ```javascript process.on('message', function(msg) { if (msg === 'shutdown') { close_all_connections(); delete_logs(); server.close(); process.exit(0); } }); ``` <h2 id="12.15">OS模塊</h2> os模塊提供與操作系統相關的方法。 ## API ### os.tmpdir() `os.tmpdir`方法返回操作系統默認的臨時文件目錄。 ## Socket通信 下面例子列出當前系列的所有IP地址。 ```javascript var os = require('os'); var interfaces = os.networkInterfaces(); for (item in interfaces) { console.log('Network interface name: ' + item); for (att in interfaces[item]) { var address = interfaces[item][att]; console.log('Family: ' + address.family); console.log('IP Address: ' + address.address); console.log('Is Internal: ' + address.internal); console.log(''); } console.log('=================================='); } ``` <h2 id="12.16">Net模塊和DNS模塊</h2> net模塊用于底層的網絡通信。 ## 服務器端Socket接口 來看一個簡單的Telnet服務的[例子](https://gist.github.com/atdt/4037228)。 ```javascript var net = require('net'); var port = 1081; var logo = fs.readFileSync('logo.txt'); var ps1 = '\n\n>>> '; net.createServer( function ( socket ) { socket.write( logo ); socket.write( ps1 ); socket.on( 'data', recv.bind( null, socket ) ); } ).listen( port ); ``` 上面代碼,在1081端口架設了一個服務。可以用telnet訪問這個服務。 ```bash $ telnet localhost 1081 ``` 一旦telnet連入以后,就會顯示提示符`>>>`,輸入命令以后,就會調用回調函數`recv`。 ```javascript function recv( socket, data ) { if ( data === 'quit' ) { socket.end( 'Bye!\n' ); return; } request( { uri: baseUrl + data }, function ( error, response, body ) { if ( body && body.length ) { $ = cheerio.load( body ); socket.write( $( '#mw-content-text p' ).first().text() + '\n' ); } else { socket.write( 'Error: ' + response.statusCode ); } socket.write( ps1 ); } ); } ``` 上面代碼中,如果輸入的命令是`quit`,然后就退出telnet。如果是其他命令,就發起遠程請求讀取數據,并顯示在屏幕上。 下面代碼是另一個例子,用到了更多的接口。 ```javascript var serverPort = 9099; var net = require('net'); var server = net.createServer(function(client) { console.log('client connected'); console.log('client IP Address: ' + client.remoteAddress); console.log('is IPv6: ' + net.isIPv6(client.remoteAddress)); console.log('total server connections: ' + server.connections); // Waiting for data from the client. client.on('data', function(data) { console.log('received data: ' + data.toString()); // Write data to the client socket. client.write('hello from server'); }); // Closed socket event from the client. client.on('end', function() { console.log('client disconnected'); }); }); server.on('error',function(err){ console.log(err); server.close(); }); server.listen(serverPort, function() { console.log('server started on port ' + serverPort); }); ``` 上面代碼中,createServer方法建立了一個服務端,一旦收到客戶端發送的數據,就發出回應,同時還監聽客戶端是否中斷通信。最后,listen方法打開服務端。 ## 客戶端Socket接口 客戶端Socket接口用來向服務器發送數據。 ```javascript var serverPort = 9099; var server = 'localhost'; var net = require('net'); console.log('connecting to server...'); var client = net.connect({server:server,port:serverPort},function(){ console.log('client connected'); // send data console.log('send data to server'); client.write('greeting from client socket'); }); client.on('data', function(data) { console.log('received data: ' + data.toString()); client.end(); }); client.on('error',function(err){ console.log(err); }); client.on('end', function() { console.log('client disconnected'); }); ``` 上面代碼連接服務器之后,就向服務器發送數據,然后監聽服務器返回的數據。 ## DNS模塊 DNS模塊用于解析域名。resolve4方法用于IPv4環境,resolve6方法用于IPv6環境,lookup方法在以上兩種環境都可以使用,返回IP地址(address)和當前環境(IPv4或IPv6)。 ```javascript var dns = require('dns'); dns.resolve4('www.pecollege.net', function (err, addresses) { if (err) console.log(err); console.log('addresses: ' + JSON.stringify(addresses)); }); dns.lookup('www.pecollege.net', function (err, address, family) { if (err) console.log(err); console.log('addresses: ' + JSON.stringify(address)); console.log('family: ' + JSON.stringify(family)); }); ``` <h2 id="12.17">Express框架</h2> ## 概述 Express是目前最流行的基于Node.js的Web開發框架,可以快速地搭建一個完整功能的網站。 Express上手非常簡單,首先新建一個項目目錄,假定叫做hello-world。 ```bash $ mkdir hello-world ``` 進入該目錄,新建一個package.json文件,內容如下。 ```javascript { "name": "hello-world", "description": "hello world test app", "version": "0.0.1", "private": true, "dependencies": { "express": "4.x" } } ``` 上面代碼定義了項目的名稱、描述、版本等,并且指定需要4.0版本以上的Express。 然后,就可以安裝了。 ```bash $ npm install ``` 執行上面的命令以后,在項目根目錄下,新建一個啟動文件,假定叫做index.js。 ```javascript var express = require('express'); var app = express(); app.use(express.static(__dirname + '/public')); app.listen(8080); ``` 然后,運行上面的啟動腳本。 ```bash $ node index ``` 現在就可以訪問`http://localhost:8080`,它會在瀏覽器中打開當前目錄的public子目錄(嚴格來說,是打開public目錄的index.html文件)。如果public目錄之中有一個圖片文件`my_image.png`,那么可以用`http://localhost:8080/my_image.png`訪問該文件。 你也可以在index.js之中,生成動態網頁。 ```javascript // index.js var express = require('express'); var app = express(); app.get('/', function (req, res) { res.send('Hello world!'); }); app.listen(3000); ``` 然后,在命令行下運行啟動腳本,就可以在瀏覽器中訪問項目網站了。 ```bash $ node index ``` 上面代碼會在本機的3000端口啟動一個網站,網頁顯示Hello World。 啟動腳本index.js的`app.get`方法,用于指定不同的訪問路徑所對應的回調函數,這叫做“路由”(routing)。上面代碼只指定了根目錄的回調函數,因此只有一個路由記錄。實際應用中,可能有多個路由記錄。 ```javascript // index.js var express = require('express'); var app = express(); app.get('/', function (req, res) { res.send('Hello world!'); }); app.get('/customer', function(req, res){ res.send('customer page'); }); app.get('/admin', function(req, res){ res.send('admin page'); }); app.listen(3000); ``` 這時,最好就把路由放到一個單獨的文件中,比如新建一個routes子目錄。 ```javascript // routes/index.js module.exports = function (app) { app.get('/', function (req, res) { res.send('Hello world'); }); app.get('/customer', function(req, res){ res.send('customer page'); }); app.get('/admin', function(req, res){ res.send('admin page'); }); }; ``` 然后,原來的index.js就變成下面這樣。 ```javascript // index.js var express = require('express'); var app = express(); var routes = require('./routes')(app); app.listen(3000); ``` ## 運行原理 ### 底層:http模塊 Express框架建立在node.js內置的http模塊上。http模塊生成服務器的原始代碼如下。 ```javascript var http = require("http"); var app = http.createServer(function(request, response) { response.writeHead(200, {"Content-Type": "text/plain"}); response.end("Hello world!"); }); app.listen(3000, "localhost"); ``` 上面代碼的關鍵是http模塊的createServer方法,表示生成一個HTTP服務器實例。該方法接受一個回調函數,該回調函數的參數,分別為代表HTTP請求和HTTP回應的request對象和response對象。 Express框架的核心是對http模塊的再包裝。上面的代碼用Express改寫如下。 ```javascript var express = require('express'); var app = express(); app.get('/', function (req, res) { res.send('Hello world!'); }); app.listen(3000); ``` 比較兩段代碼,可以看到它們非常接近。原來是用`http.createServer`方法新建一個app實例,現在則是用Express的構造方法,生成一個Epress實例。兩者的回調函數都是相同的。Express框架等于在http模塊之上,加了一個中間層。 ### 什么是中間件 簡單說,中間件(middleware)就是處理HTTP請求的函數。它最大的特點就是,一個中間件處理完,再傳遞給下一個中間件。App實例在運行過程中,會調用一系列的中間件。 每個中間件可以從App實例,接收三個參數,依次為request對象(代表HTTP請求)、response對象(代表HTTP回應),next回調函數(代表下一個中間件)。每個中間件都可以對HTTP請求(request對象)進行加工,并且決定是否調用next方法,將request對象再傳給下一個中間件。 一個不進行任何操作、只傳遞request對象的中間件,就是下面這樣。 ```javascript function uselessMiddleware(req, res, next) { next(); } ``` 上面代碼的next就是下一個中間件。如果它帶有參數,則代表拋出一個錯誤,參數為錯誤文本。 ```javascript function uselessMiddleware(req, res, next) { next('出錯了!'); } ``` 拋出錯誤以后,后面的中間件將不再執行,直到發現一個錯誤處理函數為止。 ### use方法 use是express注冊中間件的方法,它返回一個函數。下面是一個連續調用兩個中間件的例子。 ```javascript var express = require("express"); var http = require("http"); var app = express(); app.use(function(request, response, next) { console.log("In comes a " + request.method + " to " + request.url); next(); }); app.use(function(request, response) { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Hello world!\n"); }); http.createServer(app).listen(1337); ``` 上面代碼使用`app.use`方法,注冊了兩個中間件。收到HTTP請求后,先調用第一個中間件,在控制臺輸出一行信息,然后通過`next`方法,將執行權傳給第二個中間件,輸出HTTP回應。由于第二個中間件沒有調用`next`方法,所以request對象就不再向后傳遞了。 `use`方法內部可以對訪問路徑進行判斷,據此就能實現簡單的路由,根據不同的請求網址,返回不同的網頁內容。 ```javascript var express = require("express"); var http = require("http"); var app = express(); app.use(function(request, response, next) { if (request.url == "/") { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Welcome to the homepage!\n"); } else { next(); } }); app.use(function(request, response, next) { if (request.url == "/about") { response.writeHead(200, { "Content-Type": "text/plain" }); } else { next(); } }); app.use(function(request, response) { response.writeHead(404, { "Content-Type": "text/plain" }); response.end("404 error!\n"); }); http.createServer(app).listen(1337); ``` 上面代碼通過`request.url`屬性,判斷請求的網址,從而返回不同的內容。注意,`app.use`方法一共登記了三個中間件,只要請求路徑匹配,就不會將執行權交給下一個中間件。因此,最后一個中間件會返回404錯誤,即前面的中間件都沒匹配請求路徑,找不到所要請求的資源。 除了在回調函數內部判斷請求的網址,use方法也允許將請求網址寫在第一個參數。這代表,只有請求路徑匹配這個參數,后面的中間件才會生效。無疑,這樣寫更加清晰和方便。 ```javascript app.use('/path', someMiddleware); ``` 上面代碼表示,只對根目錄的請求,調用某個中間件。 因此,上面的代碼可以寫成下面的樣子。 ```javascript var express = require("express"); var http = require("http"); var app = express(); app.use("/home", function(request, response, next) { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Welcome to the homepage!\n"); }); app.use("/about", function(request, response, next) { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Welcome to the about page!\n"); }); app.use(function(request, response) { response.writeHead(404, { "Content-Type": "text/plain" }); response.end("404 error!\n"); }); http.createServer(app).listen(1337); ``` ## Express的方法 ### all方法和HTTP動詞方法 針對不同的請求,Express提供了use方法的一些別名。比如,上面代碼也可以用別名的形式來寫。 ```javascript var express = require("express"); var http = require("http"); var app = express(); app.all("*", function(request, response, next) { response.writeHead(200, { "Content-Type": "text/plain" }); next(); }); app.get("/", function(request, response) { response.end("Welcome to the homepage!"); }); app.get("/about", function(request, response) { response.end("Welcome to the about page!"); }); app.get("*", function(request, response) { response.end("404!"); }); http.createServer(app).listen(1337); ``` 上面代碼的all方法表示,所有請求都必須通過該中間件,參數中的“*”表示對所有路徑有效。get方法則是只有GET動詞的HTTP請求通過該中間件,它的第一個參數是請求的路徑。由于get方法的回調函數沒有調用next方法,所以只要有一個中間件被調用了,后面的中間件就不會再被調用了。 除了get方法以外,Express還提供post、put、delete方法,即HTTP動詞都是Express的方法。 這些方法的第一個參數,都是請求的路徑。除了絕對匹配以外,Express允許模式匹配。 ```javascript app.get("/hello/:who", function(req, res) { res.end("Hello, " + req.params.who + "."); }); ``` 上面代碼將匹配“/hello/alice”網址,網址中的alice將被捕獲,作為req.params.who屬性的值。需要注意的是,捕獲后需要對網址進行檢查,過濾不安全字符,上面的寫法只是為了演示,生產中不應這樣直接使用用戶提供的值。 如果在模式參數后面加上問號,表示該參數可選。 ```javascript app.get('/hello/:who?',function(req,res) { if(req.params.id) { res.end("Hello, " + req.params.who + "."); } else { res.send("Hello, Guest."); } }); ``` 下面是一些更復雜的模式匹配的例子。 ```javascript app.get('/forum/:fid/thread/:tid', middleware) // 匹配/commits/71dbb9c // 或/commits/71dbb9c..4c084f9這樣的git格式的網址 app.get(/^\/commits\/(\w+)(?:\.\.(\w+))?$/, function(req, res){ var from = req.params[0]; var to = req.params[1] || 'HEAD'; res.send('commit range ' + from + '..' + to); }); ``` ### set方法 set方法用于指定變量的值。 ```javascript app.set("views", __dirname + "/views"); app.set("view engine", "jade"); ``` 上面代碼使用set方法,為系統變量“views”和“view engine”指定值。 ### response對象 **(1)response.redirect方法** response.redirect方法允許網址的重定向。 ```javascript response.redirect("/hello/anime"); response.redirect("http://www.example.com"); response.redirect(301, "http://www.example.com"); ``` **(2)response.sendFile方法** response.sendFile方法用于發送文件。 ```javascript response.sendFile("/path/to/anime.mp4"); ``` **(3)response.render方法** response.render方法用于渲染網頁模板。 ```javascript app.get("/", function(request, response) { response.render("index", { message: "Hello World" }); }); ``` 上面代碼使用render方法,將message變量傳入index模板,渲染成HTML網頁。 ### requst對象 **(1)request.ip** request.ip屬性用于獲得HTTP請求的IP地址。 **(2)request.files** request.files用于獲取上傳的文件。 ### 搭建HTTPs服務器 使用Express搭建HTTPs加密服務器,也很簡單。 ```javascript var fs = require('fs'); var options = { key: fs.readFileSync('E:/ssl/myserver.key'), cert: fs.readFileSync('E:/ssl/myserver.crt'), passphrase: '1234' }; var https = require('https'); var express = require('express'); var app = express(); app.get('/', function(req, res){ res.send('Hello World Expressjs'); }); var server = https.createServer(options, app); server.listen(8084); console.log('Server is running on port 8084'); ``` ## 項目開發實例 ### 編寫啟動腳本 上一節使用express命令自動建立項目,也可以不使用這個命令,手動新建所有文件。 先建立一個項目目錄(假定這個目錄叫做demo)。進入該目錄,新建一個package.json文件,寫入項目的配置信息。 ```javascript { "name": "demo", "description": "My First Express App", "version": "0.0.1", "dependencies": { "express": "3.x" } } ``` 在項目目錄中,新建文件app.js。項目的代碼就放在這個文件里面。 ```javascript var express = require('express'); var app = express(); ``` 上面代碼首先加載express模塊,賦給變量express。然后,生成express實例,賦給變量app。 接著,設定express實例的參數。 ```javascript // 設定port變量,意為訪問端口 app.set('port', process.env.PORT || 3000); // 設定views變量,意為視圖存放的目錄 app.set('views', path.join(__dirname, 'views')); // 設定view engine變量,意為網頁模板引擎 app.set('view engine', 'jade'); app.use(express.favicon()); app.use(express.logger('dev')); app.use(express.bodyParser()); app.use(express.methodOverride()); app.use(app.router); // 設定靜態文件目錄,比如本地文件 // 目錄為demo/public/images,訪問 // 網址則顯示為http://localhost:3000/images app.use(express.static(path.join(__dirname, 'public'))); ``` 上面代碼中的set方法用于設定內部變量,use方法用于調用express的中間件。 最后,調用實例方法listen,讓其監聽事先設定的端口(3000)。 ```javascript app.listen(app.get('port')); ``` 這時,運行下面的命令,就可以在瀏覽器訪問http://127.0.0.1:3000。 ```bash node app.js ``` 網頁提示“Cannot GET /”,表示沒有為網站的根路徑指定可以顯示的內容。所以,下一步就是配置路由。 ### 配置路由 所謂“路由”,就是指為不同的訪問路徑,指定不同的處理方法。 **(1)指定根路徑** 在app.js之中,先指定根路徑的處理方法。 ```javascript app.get('/', function(req, res) { res.send('Hello World'); }); ``` 上面代碼的get方法,表示處理客戶端發出的GET請求。相應的,還有app.post、app.put、app.del(delete是JavaScript保留字,所以改叫del)方法。 get方法的第一個參數是訪問路徑,正斜杠(/)就代表根路徑;第二個參數是回調函數,它的req參數表示客戶端發來的HTTP請求,res參數代表發向客戶端的HTTP回應,這兩個參數都是對象。在回調函數內部,使用HTTP回應的send方法,表示向瀏覽器發送一個字符串。然后,運行下面的命令。 ```bash node app.js ``` 此時,在瀏覽器中訪問http://127.0.0.1:3000,網頁就會顯示“Hello World”。 如果需要指定HTTP頭信息,回調函數就必須換一種寫法,要使用setHeader方法與end方法。 ```javascript app.get('/', function(req, res){ var body = 'Hello World'; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Length', body.length); res.end(body); }); ``` **(2)指定特定路徑** 上面是處理根目錄的情況,下面再舉一個例子。假定用戶訪問/api路徑,希望返回一個JSON字符串。這時,get可以這樣寫。 ```javascript app.get('/api', function(request, response) { response.send({name:"張三",age:40}); }); ``` 上面代碼表示,除了發送字符串,send方法還可以直接發送對象。重新啟動node以后,再訪問路徑/api,瀏覽器就會顯示一個JSON對象。 ```javascript { "name": "張三", "age": 40 } ``` 我們也可以把app.get的回調函數,封裝成模塊。先在routes目錄下面建立一個api.js文件。 ```javascript // routes/api.js exports.index = function (req, res){ res.json(200, {name:"張三",age:40}); } ``` 然后,在app.js中加載這個模塊。 ```javascript // app.js var api = require('./routes/api'); app.get('/api', api.index); ``` 現在訪問時,就會顯示與上一次同樣的結果。 如果只向瀏覽器發送簡單的文本信息,上面的方法已經夠用;但是如果要向瀏覽器發送復雜的內容,還是應該使用網頁模板。 ### 靜態網頁模板 在項目目錄之中,建立一個子目錄views,用于存放網頁模板。 假定這個項目有三個路徑:根路徑(/)、自我介紹(/about)和文章(/article)。那么,app.js可以這樣寫: ```javascript var express = require('express'); var app = express(); app.get('/', function(req, res) { res.sendfile('./views/index.html'); }); app.get('/about', function(req, res) { res.sendfile('./views/about.html'); }); app.get('/article', function(req, res) { res.sendfile('./views/article.html'); }); app.listen(3000); ``` 上面代碼表示,三個路徑分別對應views目錄中的三個模板:index.html、about.html和article.html。另外,向服務器發送信息的方法,從send變成了sendfile,后者專門用于發送文件。 假定index.html的內容如下: ```html <html> <head> <title>首頁</title> </head> <body> <h1>Express Demo</h1> <footer> <p> <a href="/">首頁</a> - <a href="/about">自我介紹</a> - <a href="/article">文章</a> </p> </footer> </body> </html> ``` 上面代碼是一個靜態網頁。如果想要展示動態內容,就必須使用動態網頁模板。 ## 動態網頁模板 網站真正的魅力在于動態網頁,下面我們來看看,如何制作一個動態網頁的網站。 ### 安裝模板引擎 Express支持多種模板引擎,這里采用Handlebars模板引擎的服務器端版本[hbs](https://github.com/donpark/hbs)模板引擎。 先安裝hbs。 ```html npm install hbs --save-dev ``` 上面代碼將hbs模塊,安裝在項目目錄的子目錄node_modules之中。save-dev參數表示,將依賴關系寫入package.json文件。安裝以后的package.json文件變成下面這樣: ```javascript // package.json文件 { "name": "demo", "description": "My First Express App", "version": "0.0.1", "dependencies": { "express": "3.x" }, "devDependencies": { "hbs": "~2.3.1" } } ``` 安裝模板引擎之后,就要改寫app.js。 ```javascript // app.js文件 var express = require('express'); var app = express(); // 加載hbs模塊 var hbs = require('hbs'); // 指定模板文件的后綴名為html app.set('view engine', 'html'); // 運行hbs模塊 app.engine('html', hbs.__express); app.get('/', function (req, res){ res.render('index'); }); app.get('/about', function(req, res) { res.render('about'); }); app.get('/article', function(req, res) { res.render('article'); }); ``` 上面代碼改用render方法,對網頁模板進行渲染。render方法的參數就是模板的文件名,默認放在子目錄views之中,后綴名已經在前面指定為html,這里可以省略。所以,res.render('index') 就是指,把子目錄views下面的index.html文件,交給模板引擎hbs渲染。 ### 新建數據腳本 渲染是指將數據代入模板的過程。實際運用中,數據都是保存在數據庫之中的,這里為了簡化問題,假定數據保存在一個腳本文件中。 在項目目錄中,新建一個文件blog.js,用于存放數據。blog.js的寫法符合CommonJS規范,使得它可以被require語句加載。 ```javascript // blog.js文件 var entries = [ {"id":1, "title":"第一篇", "body":"正文", "published":"6/2/2013"}, {"id":2, "title":"第二篇", "body":"正文", "published":"6/3/2013"}, {"id":3, "title":"第三篇", "body":"正文", "published":"6/4/2013"}, {"id":4, "title":"第四篇", "body":"正文", "published":"6/5/2013"}, {"id":5, "title":"第五篇", "body":"正文", "published":"6/10/2013"}, {"id":6, "title":"第六篇", "body":"正文", "published":"6/12/2013"} ]; exports.getBlogEntries = function (){ return entries; } exports.getBlogEntry = function (id){ for(var i=0; i < entries.length; i++){ if(entries[i].id == id) return entries[i]; } } ``` ### 新建網頁模板 接著,新建模板文件index.html。 ```html <!-- views/index.html文件 --> <h1>文章列表</h1> {{"{{"}}#each entries}} <p> <a href="/article/{{"{{"}}id}}">{{"{{"}}title}}</a><br/> Published: {{"{{"}}published}} </p> {{"{{"}}/each}} ``` 模板文件about.html。 ```html <!-- views/about.html文件 --> <h1>自我介紹</h1> <p>正文</p> ``` 模板文件article.html。 ```html <!-- views/article.html文件 --> <h1>{{"{{"}}blog.title}}</h1> Published: {{"{{"}}blog.published}} <p/> {{"{{"}}blog.body}} ``` 可以看到,上面三個模板文件都只有網頁主體。因為網頁布局是共享的,所以布局的部分可以單獨新建一個文件layout.html。 ```html <!-- views/layout.html文件 --> <html> <head> <title>{{"{{"}}title}}</title> </head> <body> {{"{{{"}}body}}} <footer> <p> <a href="/">首頁</a> - <a href="/about">自我介紹</a> </p> </footer> </body> </html> ``` ### 渲染模板 最后,改寫app.js文件。 ```javascript // app.js文件 var express = require('express'); var app = express(); var hbs = require('hbs'); // 加載數據模塊 var blogEngine = require('./blog'); app.set('view engine', 'html'); app.engine('html', hbs.__express); app.use(express.bodyParser()); app.get('/', function(req, res) { res.render('index',{title:"最近文章", entries:blogEngine.getBlogEntries()}); }); app.get('/about', function(req, res) { res.render('about', {title:"自我介紹"}); }); app.get('/article/:id', function(req, res) { var entry = blogEngine.getBlogEntry(req.params.id); res.render('article',{title:entry.title, blog:entry}); }); app.listen(3000); ``` 上面代碼中的render方法,現在加入了第二個參數,表示模板變量綁定的數據。 現在重啟node服務器,然后訪問http://127.0.0.1:3000。 ```bash node app.js ``` 可以看得,模板已經使用加載的數據渲染成功了。 ### 指定靜態文件目錄 模板文件默認存放在views子目錄。這時,如果要在網頁中加載靜態文件(比如樣式表、圖片等),就需要另外指定一個存放靜態文件的目錄。 ```javascript app.use(express.static('public')); ``` 上面代碼在文件app.js之中,指定靜態文件存放的目錄是public。于是,當瀏覽器發出非HTML文件請求時,服務器端就到public目錄尋找這個文件。比如,瀏覽器發出如下的樣式表請求: ```javascript <link href="/bootstrap/css/bootstrap.css" rel="stylesheet"> ``` 服務器端就到public/bootstrap/css/目錄中尋找bootstrap.css文件。 ## Express.Router用法 從Express 4.0開始,路由器功能成了一個單獨的組件`Express.Router`。它好像小型的express應用程序一樣,有自己的use、get、param和route方法。 ### 基本用法 首先,`Express.Router`是一個構造函數,調用后返回一個路由器實例。然后,使用該實例的HTTP動詞方法,為不同的訪問路徑,指定回調函數;最后,掛載到某個路徑。 ```javascript var router = express.Router(); router.get('/', function(req, res) { res.send('首頁'); }); router.get('/about', function(req, res) { res.send('關于'); }); app.use('/', router); ``` 上面代碼先定義了兩個訪問路徑,然后將它們掛載到根目錄。如果最后一行改為app.use('/app', router),則相當于為`/app`和`/app/about`這兩個路徑,指定了回調函數。 這種路由器可以自由掛載的做法,為程序帶來了更大的靈活性,既可以定義多個路由器實例,也可以為將同一個路由器實例掛載到多個路徑。 ### router.route方法 router實例對象的route方法,可以接受訪問路徑作為參數。 ```javascript var router = express.Router(); router.route('/api') .post(function(req, res) { // ... }) .get(function(req, res) { Bear.find(function(err, bears) { if (err) res.send(err); res.json(bears); }); }); app.use('/', router); ``` ### router中間件 use方法為router對象指定中間件,即在數據正式發給用戶之前,對數據進行處理。下面就是一個中間件的例子。 ```javascript router.use(function(req, res, next) { console.log(req.method, req.url); next(); }); ``` 上面代碼中,回調函數的next參數,表示接受其他中間件的調用。函數體中的next(),表示將數據傳遞給下一個中間件。 注意,中間件的放置順序很重要,等同于執行順序。而且,中間件必須放在HTTP動詞方法之前,否則不會執行。 ### 對路徑參數的處理 router對象的param方法用于路徑參數的處理,可以 ```javascript router.param('name', function(req, res, next, name) { // 對name進行驗證或其他處理…… console.log(name); req.name = name; next(); }); router.get('/hello/:name', function(req, res) { res.send('hello ' + req.name + '!'); }); ``` 上面代碼中,get方法為訪問路徑指定了name參數,param方法則是對name參數進行處理。注意,param方法必須放在HTTP動詞方法之前。 ### app.route 假定app是Express的實例對象,Express 4.0為該對象提供了一個route屬性。app.route實際上是express.Router()的縮寫形式,除了直接掛載到根路徑。因此,對同一個路徑指定get和post方法的回調函數,可以寫成鏈式形式。 ```javascript app.route('/login') .get(function(req, res) { res.send('this is the login form'); }) .post(function(req, res) { console.log('processing'); res.send('processing the login form!'); }); ``` 上面代碼的這種寫法,顯然非常簡潔清晰。 ## 上傳文件 首先,在網頁插入上傳文件的表單。 ```html <form action="/pictures/upload" method="POST" enctype="multipart/form-data"> Select an image to upload: <input type="file" name="image"> <input type="submit" value="Upload Image"> </form> ``` 然后,服務器腳本建立指向`/upload`目錄的路由。這時可以安裝multer模塊,它提供了上傳文件的許多功能。 ```javascript var express = require('express'); var router = express.Router(); var multer = require('multer'); var uploading = multer({ dest: __dirname + '../public/uploads/', // 設定限制,每次最多上傳1個文件,文件大小不超過1MB limits: {fileSize: 1000000, files:1}, }) router.post('/upload', uploading, function(req, res) { }) module.exports = router ``` 上面代碼是上傳文件到本地目錄。下面是上傳到Amazon S3的例子。 首先,在S3上面新增CORS配置文件。 ```xml <?xml version="1.0" encoding="UTF-8"?> <CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> <CORSRule> <AllowedOrigin>*</AllowedOrigin> <AllowedMethod>GET</AllowedMethod> <AllowedMethod>POST</AllowedMethod> <AllowedMethod>PUT</AllowedMethod> <AllowedHeader>*</AllowedHeader> </CORSRule> </CORSConfiguration> ``` 上面的配置允許任意電腦向你的bucket發送HTTP請求。 然后,安裝aws-sdk。 ```bash $ npm install aws-sdk --save ``` 下面是服務器腳本。 ```javascript var express = require('express'); var router = express.Router(); var aws = require('aws-sdk'); router.get('/', function(req, res) { res.render('index') }) var AWS_ACCESS_KEY = 'your_AWS_access_key' var AWS_SECRET_KEY = 'your_AWS_secret_key' var S3_BUCKET = 'images_upload' router.get('/sign', function(req, res) { aws.config.update({accessKeyId: AWS_ACCESS_KEY, secretAccessKey: AWS_SECRET_KEY}); var s3 = new aws.S3() var options = { Bucket: S3_BUCKET, Key: req.query.file_name, Expires: 60, ContentType: req.query.file_type, ACL: 'public-read' } s3.getSignedUrl('putObject', options, function(err, data){ if(err) return res.send('Error with S3') res.json({ signed_request: data, url: 'https://s3.amazonaws.com/' + S3_BUCKET + '/' + req.query.file_name }) }) }) module.exports = router ``` 上面代碼中,用戶訪問`/sign`路徑,正確登錄后,會收到一個JSON對象,里面是S3返回的數據和一個暫時用來接收上傳文件的URL,有效期只有60秒。 瀏覽器代碼如下。 ```javascript // HTML代碼為 // <br>Please select an image // <input type="file" id="image"> // <br> // <img id="preview"> document.getElementById("image").onchange = function() { var file = document.getElementById("image").files[0] if (!file) return sign_request(file, function(response) { upload(file, response.signed_request, response.url, function() { document.getElementById("preview").src = response.url }) }) } function sign_request(file, done) { var xhr = new XMLHttpRequest() xhr.open("GET", "/sign?file_name=" + file.name + "&file_type=" + file.type) xhr.onreadystatechange = function() { if(xhr.readyState === 4 && xhr.status === 200) { var response = JSON.parse(xhr.responseText) done(response) } } xhr.send() } function upload(file, signed_request, url, done) { var xhr = new XMLHttpRequest() xhr.open("PUT", signed_request) xhr.setRequestHeader('x-amz-acl', 'public-read') xhr.onload = function() { if (xhr.status === 200) { done() } } xhr.send(file) } ``` 上面代碼首先監聽file控件的change事件,一旦有變化,就先向服務器要求一個臨時的上傳URL,然后向該URL上傳文件。 <h2 id="12.18">Koa框架</h2> Koa是一個類似于Express的Web開發框架,創始人也是同一個人。它的主要特點是,使用了ES6的Generator函數,進行了架構的重新設計。也就是說,Koa的原理和內部結構很像Express,但是語法和內部結構進行了升級。 官方[faq](https://github.com/koajs/koa/blob/master/docs/faq.md#why-isnt-koa-just-express-40)有這樣一個問題:”為什么koa不是Express 4.0?“,回答是這樣的:”Koa與Express有很大差異,整個設計都是不同的,所以如果將Express 3.0按照這種寫法升級到4.0,就意味著重寫整個程序。所以,我們覺得創造一個新的庫,是更合適的做法。“ ## Koa應用 一個Koa應用就是一個對象,包含了一個middleware數組,這個數組由一組Generator函數組成。這些函數負責對HTTP請求進行各種加工,比如生成緩存、指定代理、請求重定向等等。 ```javascript var koa = require('koa'); var app = koa(); app.use(function *(){ this.body = 'Hello World'; }); app.listen(3000); ``` 上面代碼中,變量app就是一個Koa應用。它監聽3000端口,返回一個內容為Hello World的網頁。 app.use方法用于向middleware數組添加Generator函數。 listen方法指定監聽端口,并啟動當前應用。它實際上等同于下面的代碼。 ```javascript var http = require('http'); var koa = require('koa'); var app = koa(); http.createServer(app.callback()).listen(3000); ``` ## 中間件 Koa的中間件很像Express的中間件,也是對HTTP請求進行處理的函數,但是必須是一個Generator函數。而且,Koa的中間件是一個級聯式(Cascading)的結構,也就是說,屬于是層層調用,第一個中間件調用第二個中間件,第二個調用第三個,以此類推。上游的中間件必須等到下游的中間件返回結果,才會繼續執行,這點很像遞歸。 中間件通過當前應用的use方法注冊。 ```javascript app.use(function* (next){ var start = new Date; // (1) yield next; // (2) var ms = new Date - start; // (3) console.log('%s %s - %s', this.method, this.url, ms); // (4) }); ``` 上面代碼中,`app.use`方法的參數就是中間件,它是一個Generator函數,最大的特征就是function命令與參數之間,必須有一個星號。Generator函數的參數next,表示下一個中間件。 Generator函數內部使用yield命令,將程序的執行權轉交給下一個中間件,即`yield next`,要等到下一個中間件返回結果,才會繼續往下執行。上面代碼中,Generator函數體內部,第一行賦值語句首先執行,開始計時,第二行yield語句將執行權交給下一個中間件,當前中間件就暫停執行。等到后面的中間件全部執行完成,執行權就回到原來暫停的地方,繼續往下執行,這時才會執行第三行,計算這個過程一共花了多少時間,第四行將這個時間打印出來。 下面是一個兩個中間件級聯的例子。 ```javascript app.use(function *() { this.body = "header\n"; yield saveResults.call(this); this.body += "footer\n"; }); function *saveResults() { this.body += "Results Saved!\n"; } ``` 上面代碼中,第一個中間件調用第二個中間件saveResults,它們都向`this.body`寫入內容。最后,`this.body`的輸出如下。 ```javascript header Results Saved! footer ``` 只要有一個中間件缺少`yield next`語句,后面的中間件都不會執行,這一點要引起注意。 ```javascript app.use(function *(next){ console.log('>> one'); yield next; console.log('<< one'); }); app.use(function *(next){ console.log('>> two'); this.body = 'two'; console.log('<< two'); }); app.use(function *(next){ console.log('>> three'); yield next; console.log('<< three'); }); ``` 上面代碼中,因為第二個中間件少了`yield next`語句,第三個中間件并不會執行。 如果想跳過一個中間件,可以直接在該中間件的第一行語句寫上`return yield next`。 ```javascript app.use(function* (next) { if (skip) return yield next; }) ``` 由于Koa要求中間件唯一的參數就是next,導致如果要傳入其他參數,必須另外寫一個返回Generator函數的函數。 ```javascript function logger(format) { return function *(next){ var str = format .replace(':method', this.method) .replace(':url', this.url); console.log(str); yield next; } } app.use(logger(':method :url')); ``` 上面代碼中,真正的中間件是logger函數的返回值,而logger函數是可以接受參數的。 ### 多個中間件的合并 由于中間件的參數統一為next(意為下一個中間件),因此可以使用`.call(this, next)`,將多個中間件進行合并。 ```javascript function *random(next) { if ('/random' == this.path) { this.body = Math.floor(Math.random()*10); } else { yield next; } }; function *backwards(next) { if ('/backwards' == this.path) { this.body = 'sdrawkcab'; } else { yield next; } } function *pi(next) { if ('/pi' == this.path) { this.body = String(Math.PI); } else { yield next; } } function *all(next) { yield random.call(this, backwards.call(this, pi.call(this, next))); } app.use(all); ``` 上面代碼中,中間件all內部,就是依次調用random、backwards、pi,后一個中間件就是前一個中間件的參數。 Koa內部使用koa-compose模塊,進行同樣的操作,下面是它的源碼。 ```javascript function compose(middleware){ return function *(next){ if (!next) next = noop(); var i = middleware.length; while (i--) { next = middleware[i].call(this, next); } yield *next; } } function *noop(){} ``` 上面代碼中,middleware是中間件數組。前一個中間件的參數是后一個中間件,依次類推。如果最后一個中間件沒有next參數,則傳入一個空函數。 ## 路由 可以通過`this.path`屬性,判斷用戶請求的路徑,從而起到路由作用。 ```javascript app.use(function* (next) { if (this.path === '/') { this.body = 'we are at home!'; } else { yield next; } }) // 等同于 app.use(function* (next) { if (this.path !== '/') return yield next; this.body = 'we are at home!'; }) ``` 下面是多路徑的例子。 ```javascript let koa = require('koa') let app = koa() // normal route app.use(function* (next) { if (this.path !== '/') { return yield next } this.body = 'hello world' }); // /404 route app.use(function* (next) { if (this.path !== '/404') { return yield next; } this.body = 'page not found' }); // /500 route app.use(function* (next) { if (this.path !== '/500') { return yield next; } this.body = 'internal server error' }); app.listen(8080) ``` 上面代碼中,每一個中間件負責一個路徑,如果路徑不符合,就傳遞給下一個中間件。 復雜的路由需要安裝koa-router插件。 ```javascript var app = require('koa')(); var Router = require('koa-router'); var myRouter = new Router(); myRouter.get('/', function *(next) { this.response.body = 'Hello World!'; }); app.use(myRouter.routes()); app.listen(3000); ``` 上面代碼對根路徑設置路由。 Koa-router實例提供一系列動詞方法,即一種HTTP動詞對應一種方法。典型的動詞方法有以下五種。 - router.get() - router.post() - router.put() - router.del() - router.patch() 這些動詞方法可以接受兩個參數,第一個是路徑模式,第二個是對應的控制器方法(中間件),定義用戶請求該路徑時服務器行為。 ```javascript router.get('/', function *(next) { this.body = 'Hello World!'; }); ``` 上面代碼中,`router.get`方法的第一個參數是根路徑,第二個參數是對應的函數方法。 注意,路徑匹配的時候,不會把查詢字符串考慮在內。比如,`/index?param=xyz`匹配路徑`/index`。 有些路徑模式比較復雜,Koa-router允許為路徑模式起別名。起名時,別名要添加為動詞方法的第一個參數,這時動詞方法變成接受三個參數。 ```javascript router.get('user', '/users/:id', function *(next) { // ... }); ``` 上面代碼中,路徑模式`\users\:id`的名字就是`user`。路徑的名稱,可以用來引用對應的具體路徑,比如url方法可以根據路徑名稱,結合給定的參數,生成具體的路徑。 ```javascript router.url('user', 3); // => "/users/3" router.url('user', { id: 3 }); // => "/users/3" ``` 上面代碼中,user就是路徑模式的名稱,對應具體路徑`/users/:id`。url方法的第二個參數3,表示給定id的值是3,因此最后生成的路徑是`/users/3`。 Koa-router允許為路徑統一添加前綴。 ```javascript var router = new Router({ prefix: '/users' }); router.get('/', ...); // 等同于"/users" router.get('/:id', ...); // 等同于"/users/:id" ``` 路徑的參數通過`this.params`屬性獲取,該屬性返回一個對象,所有路徑參數都是該對象的成員。 ```javascript // 訪問 /programming/how-to-node router.get('/:category/:title', function *(next) { console.log(this.params); // => { category: 'programming', title: 'how-to-node' } }); ``` param方法可以針對命名參數,設置驗證條件。 ```javascript router .get('/users/:user', function *(next) { this.body = this.user; }) .param('user', function *(id, next) { var users = [ '0號用戶', '1號用戶', '2號用戶']; this.user = users[id]; if (!this.user) return this.status = 404; yield next; }) ``` 上面代碼中,如果`/users/:user`的參數user對應的不是有效用戶(比如訪問`/users/3`),param方法注冊的中間件會查到,就會返回404錯誤。 redirect方法會將某個路徑的請求,重定向到另一個路徑,并返回301狀態碼。 ```javascript router.redirect('/login', 'sign-in'); // 等同于 router.all('/login', function *() { this.redirect('/sign-in'); this.status = 301; }); ``` redirect方法的第一個參數是請求來源,第二個參數是目的地,兩者都可以用路徑模式的別名代替。 ## context對象 中間件當中的this表示上下文對象context,代表一次HTTP請求和回應,即一次訪問/回應的所有信息,都可以從上下文對象獲得。context對象封裝了request和response對象,并且提供了一些輔助方法。每次HTTP請求,就會創建一個新的context對象。 ```javascript app.use(function *(){ this; // is the Context this.request; // is a koa Request this.response; // is a koa Response }); ``` context對象的很多方法,其實是定義在ctx.request對象或ctx.response對象上面,比如,ctx.type和ctx.length對應于ctx.response.type和ctx.response.length,ctx.path和ctx.method對應于ctx.request.path和ctx.request.method。 context對象的全局屬性。 - request:指向Request對象 - response:指向Response對象 - req:指向Node的request對象 - res:指向Node的response對象 - app:指向App對象 - state:用于在中間件傳遞信息。 ```javascript this.state.user = yield User.find(id); ``` 上面代碼中,user屬性存放在`this.state`對象上面,可以被另一個中間件讀取。 context對象的全局方法。 - throw():拋出錯誤,直接決定了HTTP回應的狀態碼。 - assert():如果一個表達式為false,則拋出一個錯誤。 ```javascript this.throw(403); this.throw('name required', 400); this.throw('something exploded'); this.throw(400, 'name required'); // 等同于 var err = new Error('name required'); err.status = 400; throw err; ``` assert方法的例子。 ```javascript // 格式 ctx.assert(value, [msg], [status], [properties]) // 例子 this.assert(this.user, 401, 'User not found. Please login!'); ``` 以下模塊解析POST請求的數據。 - co-body - https://github.com/koajs/body-parser - https://github.com/koajs/body-parsers ```javascript var parse = require('co-body'); // in Koa handler var body = yield parse(this); ``` ## 錯誤處理機制 Koa提供內置的錯誤處理機制,任何中間件拋出的錯誤都會被捕捉到,引發向客戶端返回一個500錯誤,而不會導致進程停止,因此也就不需要forever這樣的模塊重啟進程。 ```javascript app.use(function *() { throw new Error(); }); ``` 上面代碼中,中間件內部拋出一個錯誤,并不會導致Koa應用掛掉。Koa內置的錯誤處理機制,會捕捉到這個錯誤。 當然,也可以額外部署自己的錯誤處理機制。 ```javascript app.use(function *() { try { yield saveResults(); } catch (err) { this.throw(400, '數據無效'); } }); ``` 上面代碼自行部署了try...catch代碼塊,一旦產生錯誤,就用`this.throw`方法拋出。該方法可以將指定的狀態碼和錯誤信息,返回給客戶端。 對于未捕獲錯誤,可以設置error事件的監聽函數。 ```javascript app.on('error', function(err){ log.error('server error', err); }); ``` error事件的監聽函數還可以接受上下文對象,作為第二個參數。 ```javascript app.on('error', function(err, ctx){ log.error('server error', err, ctx); }); ``` 如果一個錯誤沒有被捕獲,koa會向客戶端返回一個500錯誤“Internal Server Error”。 this.throw方法用于向客戶端拋出一個錯誤。 ```javascript this.throw(403); this.throw('name required', 400); this.throw(400, 'name required'); this.throw('something exploded'); this.throw('name required', 400) // 等同于 var err = new Error('name required'); err.status = 400; throw err; ``` `this.throw`方法的兩個參數,一個是錯誤碼,另一個是報錯信息。如果省略狀態碼,默認是500錯誤。 `this.assert`方法用于在中間件之中斷言,用法類似于Node的assert模塊。 ```javascript this.assert(this.user, 401, 'User not found. Please login!'); ``` 上面代碼中,如果this.user屬性不存在,會拋出一個401錯誤。 由于中間件是層級式調用,所以可以把`try { yield next }`當成第一個中間件。 ```javascript app.use(function *(next) { try { yield next; } catch (err) { this.status = err.status || 500; this.body = err.message; this.app.emit('error', err, this); } }); app.use(function *(next) { throw new Error('some error'); }) ``` ## cookie cookie的讀取和設置。 ```javascript this.cookies.get('view'); this.cookies.set('view', n); ``` get和set方法都可以接受第三個參數,表示配置參數。其中的signed參數,用于指定cookie是否加密。如果指定加密的話,必須用`app.keys`指定加密短語。 ```javascript app.keys = ['secret1', 'secret2']; this.cookies.set('name', '張三', { signed: true }); ``` this.cookie的配置對象的屬性如下。 - signed:cookie是否加密。 - expires:cookie何時過期 - path:cookie的路徑,默認是“/”。 - domain:cookie的域名。 - secure:cookie是否只有https請求下才發送。 - httpOnly:是否只有服務器可以取到cookie,默認為true。 ## session ```javascript var session = require('koa-session'); var koa = require('koa'); var app = koa(); app.keys = ['some secret hurr']; app.use(session(app)); app.use(function *(){ var n = this.session.views || 0; this.session.views = ++n; this.body = n + ' views'; }) app.listen(3000); console.log('listening on port 3000'); ``` ## Request對象 Request對象表示HTTP請求。 (1)this.request.header 返回一個對象,包含所有HTTP請求的頭信息。它也可以寫成`this.request.headers`。 (2)this.request.method 返回HTTP請求的方法,該屬性可讀寫。 (3)this.request.length 返回HTTP請求的Content-Length屬性,取不到值,則返回undefined。 (4)this.request.path 返回HTTP請求的路徑,該屬性可讀寫。 (5)this.request.href 返回HTTP請求的完整路徑,包括協議、端口和url。 ```javascript this.request.href // http://example.com/foo/bar?q=1 ``` (6)this.request.querystring 返回HTTP請求的查詢字符串,不含問號。該屬性可讀寫。 (7)this.request.search 返回HTTP請求的查詢字符串,含問號。該屬性可讀寫。 (8)this.request.host 返回HTTP請求的主機(含端口號)。 (9)this.request.hostname 返回HTTP的主機名(不含端口號)。 (10)this.request.type 返回HTTP請求的Content-Type屬性。 ```javascript var ct = this.request.type; // "image/png" ``` (11)this.request.charset 返回HTTP請求的字符集。 ```javascript this.request.charset // "utf-8" ``` (12)this.request.query 返回一個對象,包含了HTTP請求的查詢字符串。如果沒有查詢字符串,則返回一個空對象。該屬性可讀寫。 比如,查詢字符串`color=blue&size=small`,會得到以下的對象。 ```javascript { color: 'blue', size: 'small' } ``` (13)this.request.fresh 返回一個布爾值,表示緩存是否代表了最新內容。通常與If-None-Match、ETag、If-Modified-Since、Last-Modified等緩存頭,配合使用。 ```javascript this.response.set('ETag', '123'); // 檢查客戶端請求的內容是否有變化 if (this.request.fresh) { this.response.status = 304; return; } // 否則就表示客戶端的內容陳舊了, // 需要取出新內容 this.response.body = yield db.find('something'); ``` (14)this.request.stale 返回`this.request.fresh`的相反值。 (15)this.request.protocol 返回HTTP請求的協議,https或者http。 (16)this.request.secure 返回一個布爾值,表示當前協議是否為https。 (17)this.request.ip 返回發出HTTP請求的IP地址。 (18)this.request.subdomains 返回一個數組,表示HTTP請求的子域名。該屬性必須與app.subdomainOffset屬性搭配使用。app.subdomainOffset屬性默認為2,則域名“tobi.ferrets.example.com”返回["ferrets", "tobi"],如果app.subdomainOffset設為3,則返回["tobi"]。 (19)this.request.is(types...) 返回指定的類型字符串,表示HTTP請求的Content-Type屬性是否為指定類型。 ```javascript // Content-Type為 text/html; charset=utf-8 this.request.is('html'); // 'html' this.request.is('text/html'); // 'text/html' this.request.is('text/*', 'text/html'); // 'text/html' // Content-Type為 application/json this.request.is('json', 'urlencoded'); // 'json' this.request.is('application/json'); // 'application/json' this.request.is('html', 'application/*'); // 'application/json' ``` 如果不滿足條件,返回false;如果HTTP請求不含數據,則返回undefined。 ```javascript this.is('html'); // false ``` 它可以用于過濾HTTP請求,比如只允許請求下載圖片。 ```javascript if (this.is('image/*')) { // process } else { this.throw(415, 'images only!'); } ``` (20)this.request.accepts(types) 檢查HTTP請求的Accept屬性是否可接受,如果可接受,則返回指定的媒體類型,否則返回false。 ```javascript // Accept: text/html this.request.accepts('html'); // "html" // Accept: text/*, application/json this.request.accepts('html'); // "html" this.request.accepts('text/html'); // "text/html" this.request.accepts('json', 'text'); // => "json" this.request.accepts('application/json'); // => "application/json" // Accept: text/*, application/json this.request.accepts('image/png'); this.request.accepts('png'); // false // Accept: text/*;q=.5, application/json this.request.accepts(['html', 'json']); this.request.accepts('html', 'json'); // "json" // No Accept header this.request.accepts('html', 'json'); // "html" this.request.accepts('json', 'html'); // => "json" ``` 如果accepts方法沒有參數,則返回所有支持的類型(text/html,application/xhtml+xml,image/webp,application/xml,*/*)。 如果accepts方法的參數有多個參數,則返回最佳匹配。如果都不匹配則返回false,并向客戶端拋出一個406”Not Acceptable“錯誤。 如果HTTP請求沒有Accept字段,那么accepts方法返回它的第一個參數。 accepts方法可以根據不同Accept字段,向客戶端返回不同的字段。 ```javascript switch (this.request.accepts('json', 'html', 'text')) { case 'json': break; case 'html': break; case 'text': break; default: this.throw(406, 'json, html, or text only'); } ``` (21)this.request.acceptsEncodings(encodings) 該方法根據HTTP請求的Accept-Encoding字段,返回最佳匹配,如果沒有合適的匹配,則返回false。 ```javascript // Accept-Encoding: gzip this.request.acceptsEncodings('gzip', 'deflate', 'identity'); // "gzip" this.request.acceptsEncodings(['gzip', 'deflate', 'identity']); // "gzip" ``` 注意,acceptEncodings方法的參數必須包括identity(意為不編碼)。 如果HTTP請求沒有Accept-Encoding字段,acceptEncodings方法返回所有可以提供的編碼方法。 ```javascript // Accept-Encoding: gzip, deflate this.request.acceptsEncodings(); // ["gzip", "deflate", "identity"] ``` 如果都不匹配,acceptsEncodings方法返回false,并向客戶端拋出一個406“Not Acceptable”錯誤。 (22)this.request.acceptsCharsets(charsets) 該方法根據HTTP請求的Accept-Charset字段,返回最佳匹配,如果沒有合適的匹配,則返回false。 ```javascript // Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5 this.request.acceptsCharsets('utf-8', 'utf-7'); // => "utf-8" this.request.acceptsCharsets(['utf-7', 'utf-8']); // => "utf-8" ``` 如果acceptsCharsets方法沒有參數,則返回所有可接受的匹配。 ```javascript // Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5 this.request.acceptsCharsets(); // ["utf-8", "utf-7", "iso-8859-1"] ``` 如果都不匹配,acceptsCharsets方法返回false,并向客戶端拋出一個406“Not Acceptable”錯誤。 (23)this.request.acceptsLanguages(langs) 該方法根據HTTP請求的Accept-Language字段,返回最佳匹配,如果沒有合適的匹配,則返回false。 ```javascript // Accept-Language: en;q=0.8, es, pt this.request.acceptsLanguages('es', 'en'); // "es" this.request.acceptsLanguages(['en', 'es']); // "es" ``` 如果acceptsCharsets方法沒有參數,則返回所有可接受的匹配。 ```javascript // Accept-Language: en;q=0.8, es, pt this.request.acceptsLanguages(); // ["es", "pt", "en"] ``` 如果都不匹配,acceptsLanguages方法返回false,并向客戶端拋出一個406“Not Acceptable”錯誤。 (24)this.request.socket 返回HTTP請求的socket。 (25)this.request.get(field) 返回HTTP請求指定的字段。 ## Response對象 Response對象表示HTTP回應。 (1)this.response.header 返回HTTP回應的頭信息。 (2)this.response.socket 返回HTTP回應的socket。 (3)this.response.status 返回HTTP回應的狀態碼。默認情況下,該屬性沒有值。該屬性可讀寫,設置時等于一個整數。 (4)this.response.message 返回HTTP回應的狀態信息。該屬性與`this.response.message`是配對的。該屬性可讀寫。 (5)this.response.length 返回HTTP回應的Content-Length字段。該屬性可讀寫,如果沒有設置它的值,koa會自動從this.request.body推斷。 (6)this.response.body 返回HTTP回應的信息體。該屬性可讀寫,它的值可能有以下幾種類型。 - 字符串:Content-Type字段默認為text/html或text/plain,字符集默認為utf-8,Content-Length字段同時設定。 - 二進制Buffer:Content-Type字段默認為application/octet-stream,Content-Length字段同時設定。 - Stream:Content-Type字段默認為application/octet-stream。 - JSON對象:Content-Type字段默認為application/json。 - null(表示沒有信息體) 如果`this.response.status`沒設置,Koa會自動將其設為200或204。 (7)this.response.get(field) 返回HTTP回應的指定字段。 ```javascript var etag = this.get('ETag'); ``` 注意,get方法的參數是區分大小寫的。 (8)this.response.set() 設置HTTP回應的指定字段。 ```javascript this.set('Cache-Control', 'no-cache'); ``` set方法也可以接受一個對象作為參數,同時為多個字段指定值。 ```javascript this.set({ 'Etag': '1234', 'Last-Modified': date }); ``` (9)this.response.remove(field) 移除HTTP回應的指定字段。 (10)this.response.type 返回HTTP回應的Content-Type字段,不包括“charset”參數的部分。 ```javascript var ct = this.reponse.type; // "image/png" ``` 該屬性是可寫的。 ```javascript this.reponse.type = 'text/plain; charset=utf-8'; this.reponse.type = 'image/png'; this.reponse.type = '.png'; this.reponse.type = 'png'; ``` 設置type屬性的時候,如果沒有提供charset參數,Koa會判斷是否自動設置。如果`this.response.type`設為html,charset默認設為utf-8;但如果`this.response.type`設為text/html,就不會提供charset的默認值。 (10)this.response.is(types...) 該方法類似于`this.request.is()`,用于檢查HTTP回應的類型是否為支持的類型。 它可以在中間件中起到處理不同格式內容的作用。 ```javascript var minify = require('html-minifier'); app.use(function *minifyHTML(next){ yield next; if (!this.response.is('html')) return; var body = this.response.body; if (!body || body.pipe) return; if (Buffer.isBuffer(body)) body = body.toString(); this.response.body = minify(body); }); ``` 上面代碼是一個中間件,如果輸出的內容類型為HTML,就會進行最小化處理。 (11)this.response.redirect(url, [alt]) 該方法執行302跳轉到指定網址。 ```javascript this.redirect('back'); this.redirect('back', '/index.html'); this.redirect('/login'); this.redirect('http://google.com'); ``` 如果redirect方法的第一個參數是back,將重定向到HTTP請求的Referrer字段指定的網址,如果沒有該字段,則重定向到第二個參數或“/”網址。 如果想修改302狀態碼,或者修改body文字,可以采用下面的寫法。 ```javascript this.status = 301; this.redirect('/cart'); this.body = 'Redirecting to shopping cart'; ``` (12)this.response.attachment([filename]) 該方法將HTTP回應的Content-Disposition字段,設為“attachment”,提示瀏覽器下載指定文件。 (13)this.response.headerSent 該方法返回一個布爾值,檢查是否HTTP回應已經發出。 (14)this.response.lastModified 該屬性以Date對象的形式,返回HTTP回應的Last-Modified字段(如果該字段存在)。該屬性可寫。 ```javascript this.response.lastModified = new Date(); ``` (15)this.response.etag 該屬性設置HTTP回應的ETag字段。 ```javascript this.response.etag = crypto.createHash('md5').update(this.body).digest('hex'); ``` 注意,不能用該屬性讀取ETag字段。 (16)this.response.vary(field) 該方法將參數添加到HTTP回應的Vary字段。 ## CSRF攻擊 CSRF攻擊是指用戶的session被劫持,用來冒充用戶的攻擊。 koa-csrf插件用來防止CSRF攻擊。原理是在session之中寫入一個秘密的token,用戶每次使用POST方法提交數據的時候,必須含有這個token,否則就會拋出錯誤。 ```javascript var koa = require('koa'); var session = require('koa-session'); var csrf = require('koa-csrf'); var route = require('koa-route'); var app = module.exports = koa(); app.keys = ['session key', 'csrf example']; app.use(session(app)); app.use(csrf()); app.use(route.get('/token', token)); app.use(route.post('/post', post)); function* token () { this.body = this.csrf; } function* post() { this.body = {ok: true}; } app.listen(3000); ``` POST請求含有token,可以是以下幾種方式之一,koa-csrf插件就能獲得token。 - 表單的_csrf字段 - 查詢字符串的_csrf字段 - HTTP請求頭信息的x-csrf-token字段 - HTTP請求頭信息的x-xsrf-token字段 ## 數據壓縮 koa-compress模塊可以實現數據壓縮。 ```javascript app.use(require('koa-compress')()) app.use(function* () { this.type = 'text/plain' this.body = fs.createReadStream('filename.txt') }) ``` ## 源碼解讀 每一個網站就是一個app,它由`lib/application`定義。 ```javascript function Application() { if (!(this instanceof Application)) return new Application; this.env = process.env.NODE_ENV || 'development'; this.subdomainOffset = 2; this.middleware = []; this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); } var app = Application.prototype; exports = module.exports = Application; ``` `app.use()`用于注冊中間件,即將Generator函數放入中間件數組。 ```javascript app.use = function(fn){ if (!this.experimental) { // es7 async functions are allowed assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function'); } debug('use %s', fn._name || fn.name || '-'); this.middleware.push(fn); return this; }; ``` `app.listen()`就是`http.createServer(app.callback()).listen(...)`的縮寫。 ```javascript app.listen = function(){ debug('listen'); var server = http.createServer(this.callback()); return server.listen.apply(server, arguments); }; app.callback = function(){ var mw = [respond].concat(this.middleware); var fn = this.experimental ? compose_es7(mw) : co.wrap(compose(mw)); var self = this; if (!this.listeners('error').length) this.on('error', this.onerror); return function(req, res){ res.statusCode = 404; var ctx = self.createContext(req, res); onFinished(res, ctx.onerror); fn.call(ctx).catch(ctx.onerror); } }; ``` 上面代碼中,`app.callback()`會返回一個函數,用來處理HTTP請求。它的第一行`mw = [respond].concat(this.middleware)`,表示將respond函數(這也是一個Generator函數)放入`this.middleware`,現在mw就變成了`[respond, S1, S2, S3]`。 `compose(mw)`將中間件數組轉為一個層層調用的Generator函數。 ```javascript function compose(middleware){ return function *(next){ if (!next) next = noop(); var i = middleware.length; while (i--) { next = middleware[i].call(this, next); } yield *next; } } function *noop(){} ``` 上面代碼中,下一個generator函數總是上一個Generator函數的參數,從而保證了層層調用。 `var fn = co.wrap(gen)`則是將Generator函數包裝成一個自動執行的函數,并且返回一個Promise。 ```javascript //co package co.wrap = function (fn) { return function () { return co.call(this, fn.apply(this, arguments)); }; }; ``` 由于`co.wrap(compose(mw))`執行后,返回的是一個Promise,所以可以對其使用catch方法指定捕捉錯誤的回調函數`fn.call(ctx).catch(ctx.onerror)`。 將所有的上下文變量都放進context對象。 ```javascript app.createContext = function(req, res){ var context = Object.create(this.context); var request = context.request = Object.create(this.request); var response = context.response = Object.create(this.response); context.app = request.app = response.app = this; context.req = request.req = response.req = req; context.res = request.res = response.res = res; request.ctx = response.ctx = context; request.response = response; response.request = request; context.onerror = context.onerror.bind(context); context.originalUrl = request.originalUrl = req.url; context.cookies = new Cookies(req, res, this.keys); context.accept = request.accept = accepts(req); context.state = {}; return context; }; ``` 真正處理HTTP請求的是下面這個Generator函數。 ```javascript function *respond(next) { yield *next; // allow bypassing koa if (false === this.respond) return; var res = this.res; if (res.headersSent || !this.writable) return; var body = this.body; var code = this.status; // ignore body if (statuses.empty[code]) { // strip headers this.body = null; return res.end(); } if ('HEAD' == this.method) { if (isJSON(body)) this.length = Buffer.byteLength(JSON.stringify(body)); return res.end(); } // status body if (null == body) { this.type = 'text'; body = this.message || String(code); this.length = Buffer.byteLength(body); return res.end(body); } // responses if (Buffer.isBuffer(body)) return res.end(body); if ('string' == typeof body) return res.end(body); if (body instanceof Stream) return body.pipe(res); // body: json body = JSON.stringify(body); this.length = Buffer.byteLength(body); res.end(body); } ```
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看