Node.js隨手筆記(一):node簡介與模塊系統

Node.js簡介

首先從名字提及,網上查閱資料的時候會發現關於node的寫法五花八門,到底哪種寫法最標準呢?遵循官方網站的說法,一直將項目稱之爲「Node」或者「Node.js」。html

簡單來講,Node就是運行在服務器端的JavaScript。

JavaScript是一門腳本語言(能夠用來編程的而且直接執行源代碼的語言,就是腳本語言),腳本語言都須要一個解析器才能運行。對於寫在html中的js,一般是由瀏覽器去解析執行。對於獨立執行的js代碼,則須要Node這個解析器解析執行。前端

每一種解析器就是一個運行環境,不但容許js定義各類數據結構,進行各類計算,還容許js使用運行環境提供的內置對象和方法作一些事情。例如運行在瀏覽器中的js的用途是操做DOM,瀏覽器提供了document之類的內置對象。而運行在node中的js的用途是操做磁盤文件或搭建HTTP服務器,node就相應提供了fs、http等內置對象。node

Node不是js應用,而是js的運行環境。

看到Node.js這個名字,可能會誤覺得這是一個JavaScript應用,事實上,node採用c++語言對Google V8引擎進行了封裝,是一個JavaScript運行環境。V8引擎執行JavaScript的速度很是快,性能也很是好。node是一個讓開發者能夠快速建立網絡應用的服務端JavaScript平臺,同時運用JavaScript進行前端與後端編程,從而開發者能夠更專一於系統的設計以及保持其一致性。c++

// 快速構建服務器
const http = require('http')
http.createServer((req,res)=>{
    res.writeHead(200, {'Content-Type': 'text/plain'})
    res.end('hello World!')
}).listen(8088)

$ node helloWorld.js
Node採用事件驅動、異步編程

node的設計思想以事件驅動爲核心,它提供的絕大多數API都是基於事件的、異步的風格。開發者須要根據本身的業務邏輯註冊相應的回調函數,這些回調函數都是異步執行的。這意味着雖然在代碼結構中,這些函數看似是依次註冊的,可是它們並不依賴自身出現的順序,而是等待相應的事件觸發。git

在服務器開發中,併發的請求處理是個大問題,阻塞式的函數會致使資源浪費和時間延遲。經過事件註冊、異步函數,開發者能夠充分利用系統資源,執行代碼無須阻塞等待,有限的資源能夠用於其餘的任務。web

Node以單進程、單線程模式運行

這點和JavaScript的運行方式一致,事件驅動機制是node經過內部單線程高效率地維護事件循環隊列來實現的,沒有多線程的資源佔用和上下文切換,這意味着面對大規模的http請求,node憑藉事件驅動搞定一切。由此咱們是否能夠推測這樣的設計會致使負載的壓力集中在CPU(事件循環處理?)而不是內存。淘寶共享數據平臺團隊對node的性能測試:數據庫

  • 物理機配置:RHEL 5.二、CPU 2.2GHz、內存4G
  • Node.js應用場景:MemCache代理,每次取100字節數據
  • 鏈接池大小:50
  • 併發用戶數:100
  • 測試結果(socket模式):內存(30M)、QPS(16700)、CPU(95%)

眼見爲實,雖然看不太懂這些測試數據,可是最終測試結果是:它的性能讓人信服。npm

Node.js模塊系統

爲了讓Node.js的文件能夠相互調用,Node.js提供了一個簡單的模塊系統。模塊系統是Node組織管理代碼的利器也是調用第三方代碼的途徑。

模塊是Node應用程序的基本組成部分,文件和模塊是一一對應的。換言之,一個 Node.js 文件就是一個模塊,這個文件多是JavaScript 代碼、JSON 或者編譯過的C/C++ 擴展。編程

理想狀況下,開發者只須要實現核心的業務邏輯,其餘均可以加載別人已經寫好的模塊。可是,json

  • JavaScript沒有模塊系統。沒有原生的支持密閉做用域或依賴管理。
  • JavaScript沒有標準庫。除了一些核心庫外,沒有文件系統的API,沒有IO流API等。
  • JavaScript沒有標準接口。沒有如Web Server或者數據庫的統一接口。
  • JavaScript沒有包管理系統。不能自動加載和安裝依賴。

要想實現模塊化編程首先須要解決的問題是,命名衝突以及文件依賴問題。

CommonJS規範

因而便有了CommonJS規範的出現,其目標是爲了構建JavaScript在包括web服務器,桌面,命令行工具,以及瀏覽器方面的生態系統。CommonJS制定瞭解決這些問題的一些規範,而node就是這些規範的一種實現。node自身實現了require方法做爲其引入模塊的方法,同時npm也基於CommonJS定義的包規範,實現了依賴管理和模塊自動安裝等功能。

Node中模塊分類

原生模塊

原生模塊即爲Node API提供的核心模塊(如:os、http、fs、buffer、path等模塊),原生模塊在node源代碼編譯的時候編譯進了二進制執行文件,加載的速度最快。

const http = require('http');
文件模塊

爲動態加載模塊,動態加載的模塊主要由原生模塊module來實現和完成。原生模塊在啓動時已經被加載,而文件模塊須要經過調用module的require方法來實現加載。

首先定義一個文件模塊,以計算圓形的面積和周長兩個方法爲例:

const PI = Math.PI;
exports.area = (r) => {
    return PI * r * r;
};
exports.circumference = (r) => {
    return 2 * PI * r;
};

將這個文件存爲circle.js,並新建一個app.js文件,並寫入如下代碼:

// 調用文件模塊必須指定路徑,不然會報錯
const circle = require('./circle.js');
console.log( 'The area of a circle of radius 4 is ' + circle.area(4));

在require了這個文件以後,定義在exports對象上的方法即可以隨意調用。

包管理

Node Packaged Modules 簡稱NPM,是隨同node一塊兒安裝的包管理工具。Node自己提供了一些基本API模塊,可是這些基本模塊難以知足開發者需求。Node須要經過使用NPM來管理開發者自我研發的一些模塊,並使其可以公用與其餘的開發者。

NPM創建了一個node生態圈,node開發者和用戶能夠在裏邊互通有無。當你須要下載第三方包時,首先要知道有哪些包可用 npmjs.com 提供了能夠根據包名來搜索的平臺。知道包名後就可使用命令去安裝了。

npm -v // 測試是否安裝成功。
npm的經常使用命令行代碼:
  1. npm install moduleNames

    npm install moduleNames -g  // 全局安裝
    
    npm install moduleNames@2.0.0   // 安裝特定版本依賴
    
    npm install moduleNames --save  // --save 可簡寫爲 -S
    // 會在package.json的dependencies屬性下添加moduleNames依賴 即生產依賴插件
    
    npm install moduleNames --save-dev  // --save-dev 可簡寫爲 -D
    // 會在package.json的devDependencies屬性下添加moduleNames依賴 即開發依賴插件
  2. 卸載模塊

    npm uninstall moduleNames
  3. 更新模塊

    npm update moduleNames
  4. 搜索模塊

    npm search moduleNames
  5. 切換模板倉庫源:

    npm config set registry https://registry.npm.taobao.org/
    
    npm config get registry // 執行驗證是否切換成功
在NPM服務器上發佈本身的包

第一次使用NPM發佈本身的包須要在 npmjs.com 註冊一個帳號。也可使用命令 npm adduser,提示輸入帳號,密碼和郵箱,而後將提示建立成功('Logged in as Username on https://registry.npmjs.org/.')。

輸入npm init命令,根據提示配置包的相關信息,生成相應的package.json。npm命令運行時會讀取當前目錄的 package.json 文件和解釋這個文件

經過npm publish發包,包的名稱和版本就是你項目裏package.json的name和vision。此處注意:

  • name不能和已有包的名字重名。
  • name不能有大寫字母/空格/下劃線。
  • 不想發佈到npm上的代碼文件將它寫入.gitignore或.npmignore中再上傳。
  • 更新包和發佈包的命令同樣,可是每次更新別忘記修改包的版本。

模塊初始化

一個模塊中的JavaScript代碼僅在模塊第一次被使用時執行一次,並在執行過程當中初始化模塊的導出對象。以後,緩存起來的導出對象被重複利用。其中原生模塊都被定義在lib這個目錄下面,文件模塊則不定性。

模塊加載的優先級

模塊加載的優先級:已經緩存模塊 > 原生模塊 > 文件模塊 > 從文件加載

儘管require方法很簡單,可是內部的加載倒是十分複雜的
,其加載優先級也各自不一樣。以下圖示:

image

模塊加載策略

從原生模塊加載

原生模塊的優先級僅次於文件模塊緩存的優先級。require方法在解析文件名以後,優先檢查模塊是否在原生模塊列表中。

原生模塊也有一個緩存區,一樣也是優先從緩存區加載。若是緩存區沒有被加載過,則調用原生模塊的加載方式進行加載和執行。

從文件加載

實際上,在文件模塊中又分爲三類模塊,之後綴爲區分,node會根據後綴名來決定加載方法。

  • .js 經過fs模塊同步讀取js文件並編譯執行。
  • .node 經過c/c++進行編寫的Addon。經過dlopen方法進行加載。
  • .json 讀取文件,調用JSON.parse解析加載。

當文件模塊緩存中不存在,並且也不是原生模塊的時候,node會解析require方法傳入的參數,並從文件系統中加載實際的文件。

加載文件模塊的工做主要有原生模塊module來實現和完成,該原生模塊在啓動時已經被加載,進程直接調用到runMain靜態方法。

Module.runMain = function () {
    Module._load(process.argv[1], null, true);
};

_load靜態方法在分析文件名以後執行

var module = new Module(id, parent);

並根據文件路徑緩存當前模塊對象,該模塊實例對象則根據文件名加載。

module.load(filename);

以.js後綴的文件爲例,node在編譯js文件的過程當中實際完成的步驟是對js文件內容進行頭尾包裝。例如剛纔的app.js,在包裝以後變成這個樣子:

(function (exports, require, module, __filename, __dirname) {
    var circle = require('./circle.js');
    console.log('The area of a circle of radius 4 is ' + circle.area(4));
});

這段代碼擁有明確的上下文,不污染全局,返回爲一個具體的function對象。最後傳入module對象的exports,require方法,module,文件名,目錄名做爲實參並執行。

這就是爲何require並有定義在app.js文件中,可是這個方法卻存在的緣由。在這個主文件中,能夠經過require方法去引入其他的模塊。而其實這個require方法實際調用的就是load方法。

load方法在載入、編譯、緩存了module後,返回module的exports對象。這就是circle.js文件中只有定義在exports對象上的方法才能被外部調用的緣由。

以上所描述的模塊載入機制均定義在module模塊之中。

文件模塊加載過程當中的路徑分析

require方法接受如下幾種參數的傳遞:

  • http、fs、path等,原生模塊。
  • ./mod或../mod,相對路徑的文件模塊。
  • /pathtomodule/mod, 絕對路徑的文件模塊。
  • mod,非原生模塊的文件模塊。

在進入路徑查找以前有必要描述如下module path這個node中的概念。對於每個被加載的文件模塊,建立這個模塊對象的時候,這個模塊便會有一個paths屬性,它的值根據當前文件的路徑計算獲得。

例:
咱們建立modulepath.js這樣一個文件,其內容爲:

console.log(module.paths);

執行node modulepath.js,將獲得如下的輸出結果:

[ '/Users/zhaoyunlong/Node/demo/node_modules',
  '/Users/zhaoyunlong/Node/node_modules',
  '/Users/zhaoyunlong/node_modules',
  '/Users/node_modules',
  '/node_modules' ]

Windows下:

[ 'E:\\Extra\\miniprogram\\gm-xcc-demo\\gm-demo\\node_modules',
  'E:\\Extra\\miniprogram\\gm-xcc-demo\\node_modules',
  'E:\\Extra\\miniprogram\\node_modules',
  'E:\\Extra\\node_modules',
  'E:\\node_modules' ]

能夠看出module path的生成規則爲:從當前文件目錄開始查找node_modules目錄;而後依次進入父目錄,查找父目錄的node_modules目錄;依次迭代,直到根目錄下的node_modules目錄。

除此以外還有一個全局module path,是當前node執行文件的相對目錄(../../lib/node)。若是在環境變量中設置了HOME目錄和NODE_PATH目錄的話,整個路徑還包含NODE_PATH和HOME目錄下的.node_libraries與.node_modules。其最終值大體以下:

[ NODE_PATH,HOME/.node_modules,HOME/.node_libraries,execPath/../../lib/node ]

image

簡單說就是,若是require絕對路徑的文件,查找時不會去遍歷每個node_modules目錄,其速度最快。其他流程以下:

  1. 從module path 數組中取出第一個目錄做爲查找基準。
  2. 直接從目錄中查找該文件,若是存在,則結束查找。若是不存在,則進行下一條查找。
  3. 嘗試添加.js、.json、.node後綴後查找,若是存在文件,則結束。若是不存在,則進行下一條。
  4. 嘗試將require的參數做爲一個包來進行查找,讀取目錄下的package.json文件,取得main參數指定的文件。
  5. 嘗試查找該文件,若是存在,則結束查找。若是不存在則進行第3條查找。
  6. 若是繼續失敗,則取出module path數組中的下一個目錄做爲基準查找,循環第1至5個步驟。
  7. 若是繼續失敗,循環第1至6個步驟,直到module path中的最後一個值。
  8. 若是仍然失敗,則拋出異常。

整個查找過程十分相似JavaScript原型鏈的查找和做用域的查找。不一樣的是node對路徑查找實現了緩存機制,不然每次判斷路徑都是同步阻塞式進行,會致使嚴重的性能消耗。

相關文章
相關標籤/搜索