模塊機制

JavaScript自誕生以來,曾經沒有人拿它當作一門真正的編程語言,認爲它不過是一種網頁小腳本而已。直到Web 2.0時代,前端工程師利用它大大提高了網頁上的用戶體驗。在這個過程當中,B/S應用展示出比C/S應用優越的地方。至此,JavaScript才被普遍重視起來。javascript

經歷了長長的後天努力過程,JavaScript不斷被類聚和抽象,以更好地組織業務邏輯。從另外一個角度而言,它也道出了JavaScript先天就缺少的一項功能:模塊。前端

在其餘高級語言中,Java有類文件,Python有import機制,Ruby有require,PHP有include和require。而JavaScript經過<script>標籤引入代碼的方式顯得雜亂無章,語言自身毫無組織和約束能力,直到出現CommonJS規範。java

CommonJS規範

CommonJS規範爲JavaScript制定了一個美好的願景——但願JavaScript可以在任何地方運行。node

CommonJS的出發點

CommonJS規範的提出,主要是爲了彌補當前JavaScript沒有標準的缺陷,以達到像Python、Ruby和Java具有開發大型應用的基礎能力,而不是停留在小腳本程序的階段。他們指望那些用CommonJS API寫出的應用能夠具有跨宿主環境執行的能力,這樣不只能夠利用JavaScript開發富客戶端應用,並且還能夠編寫如下應用。webpack

  • 服務器端JavaScript應用程序。
  • 命令行工具。
  • 桌面圖形界面應用程序。
  • 混合應用(Titanium和Adobe AIR等形式的應用)。

現在,CommonJS中的大部分規範雖然依舊是草案,可是已經初顯成效,爲JavaScript開發大型應用程序指明瞭一條很是棒的道路。目前,它依舊在成長中,這些規範涵蓋了模塊、二進制、Buffer、字符集編碼、I/O流、進程環境、文件系統、套接字、單元測試、Web服務器網關接口、包管理等。c++

CommonJS的模塊規範

CommonJS對模塊的定義十分簡單,主要分爲模塊引用、模塊定義和模塊標識3個部分。web

  1. 模塊引用(require)
var fs = require('fs');
  1. 模塊定義(exports)
// math.js
exports.add = function () {
  var sum = 0,
    i = 0,
    args = arguments,
    l = args.length;
  while (i < l) {
    sum += args[i++];
  }
  return sum;
};
  1. 模塊標識
    模塊標識其實就是傳遞給require()方法的參數,它必須是符合小駝峯命名的字符串,或者以.、..開頭的相對路徑,或者絕對路徑。它能夠沒有文件名後綴.js。

模塊機制

CommonJS構建的這套模塊導出和引入機制使得用戶徹底沒必要考慮變量污染,命名空間等方案與之相比相形見絀。express

Node的模塊實現

Node在實現中並不是徹底按照規範實現,而是對模塊規範進行了必定的取捨,同時也增長了少量自身須要的特性。在Node中引入模塊,須要經歷以下3個步驟。npm

  1. 路徑分析
  2. 文件定位
  3. 編譯執行

在Node中,模塊分爲兩類:一類是Node提供的模塊,稱爲核心模塊;另外一類是用戶編寫的模塊,稱爲文件模塊。編程

  • 核心模塊部分在Node源代碼的編譯過程當中,編譯進了二進制執行文件。在Node進程啓動時,部分核心模塊就被直接加載進內存中,因此這部分核心模塊引入時,文件定位和編譯執行這兩個步驟能夠省略掉,而且在路徑分析中優先判斷,因此它的加載速度是最 快的。
  • 文件模塊則是在運行時動態加載,須要完整的路徑分析、文件定位、編譯執行過程,速度比核心模塊慢。

Node對引入過的模塊都會進行緩存,以減小二次引入時的開銷。Node緩存的是編譯和執行以後的對象。不管是核心模塊仍是文件模塊,require()方法對相同模塊的二次加載都一概採用緩存優先的方式,這是第一優先級的。不一樣之處在於核心模塊的緩存檢查先於文件模塊的緩存檢查。

CommonJS包規範

CommonJS的包規範的定義其實也十分簡單,它由包結構和包描述文件兩個部分組成,前者用於組織包中的各類文件,後者則用於描述包的相關信息,以供外部讀取分析。

包結構

包其實是一個存檔文件,即一個目錄直接打包爲.zip或tar.gz格式的文件,安裝後解壓還原爲目錄。徹底符合CommonJS規範的包目錄應該包含以下這些文件。

  • package.json:包描述文件。
  • bin:用於存放可執行二進制文件的目錄。
  • lib:用於存放JavaScript代碼的目錄。
  • doc:用於存放文檔的目錄。
  • test:用於存放單元測試用例的代碼。

能夠看到,CommonJS包規範從文檔、測試等方面都作過考慮。當一個包完成後向外公佈時,用戶看到單元測試和文檔的時候,會給他們一種踏實可靠的感受。

注意: 最好不要用CommonJS規範的文件名,存儲與其功能不對應的文件。

包描述文件與NPM

包描述文件用於表達非代碼相關的信息,它是一個JSON格式的文件——package.json,位於包的根目錄下,是包的重要組成部分。而NPM的全部行爲都與包描述文件的字段息息相關。因爲CommonJS包規範尚處於草案階段,NPM在實踐中作了必定的取捨,這裏就只介紹實踐相關主要字段了。

{
    "name": "包名。規範定義它須要由小寫的字母和數字組成,能夠包含.、_和-,但不容許出現空格。",
    "version": "版本號",
    "description": "包簡介",
    "keywords": "關鍵詞數組,NPM中主要用來作分類搜索。一個好的關鍵詞數組有利於用戶快速找到你編寫的包。",
    "repositories": "託管源代碼的位置列表,代表能夠經過哪些方式和地址訪問包的源代碼。",
    "author": "做者",
    "bin": "一些包做者但願包能夠做爲命令行工具使用。配置好bin字段後,經過npm install package_name -g命令能夠將腳本添加到執行路徑中,以後能夠在命令行中直接執行。前面的node-gyp便是這樣安裝的。經過-g命令安裝的模塊包稱爲全局模式。",
    "main": "入口文件",
    "scripts": "腳本說明對象。它主要被包管理器用來安裝、編譯、測試和卸載包。",
    "engines": "支持的JavaScript引擎列表,有效的引擎取值包括ejs、flusspferd、gpsee、jsc、spidermonkey、narwhal、node和v8。",
    "dependencies": "使用當前包所須要依賴的包列表。",
    "devDependencies": "一些模塊只在開發時須要依賴。配置這個屬性,能夠提示包的後續開發者安裝依賴包。",
    "licenses": "當前包所使用的許可證列表,表示這個包能夠在哪些許可證下使用。",
    "contributors": "貢獻者列表。在開源社區中,爲開源項目提供代碼是常常出現的事情,若是名字能出如今知名項目的contributors列表中,是一件比較有榮譽感的事。列表中的第一個貢獻應當是包的做者本人。它的格式與維護者列表相同。",
    "maintainers": "包維護者列表"
}

NPM經常使用功能

CommonJS包規範是理論,NPM是其中的一種實踐。NPM之於Node,至關於gem之於Ruby,pear之於PHP。對於Node而言,NPM幫助完成了第三方模塊的發佈、安裝和依賴等。藉助NPM,Node與第三方模塊之間造成了很好的一個生態系統。

藉助NPM,能夠幫助用戶快速安裝和管理依賴包。除此以外,NPM還有一些巧妙的用法,下面咱們詳細介紹一下。

1.查看幫助

在安裝Node以後,執行npm –v命令能夠查看當前NPM的版本:

$ npm -v
3.10.9

在不熟悉NPM的命令以前,能夠直接執行NPM查看到幫助引導說明:

$ npm

Usage: npm <command>

where <command> is one of:
    access, adduser, bin, bugs, c, cache, completion, config,
    ddp, dedupe, deprecate, dist-tag, docs, edit, explore, get,
    help, help-search, i, init, install, install-test, it, link,
    list, ln, login, logout, ls, outdated, owner, pack, ping,
    prefix, prune, publish, rb, rebuild, repo, restart, root,
    run, run-script, s, se, search, set, shrinkwrap, star,
    stars, start, stop, t, tag, team, test, tst, un, uninstall,
    unpublish, unstar, up, update, v, version, view, whoami

npm <cmd> -h     quick help on <cmd>
npm -l           display full usage info
npm help <term>  search for help on <term>
npm help npm     involved overview

Specify configs in the ini-formatted file:
    /Users/yangzhinian/.npmrc
or on the command line via: npm <command> --key value
Config info can be viewed via: npm help config

npm@3.10.9 /Users/yangzhinian/.nvm/versions/node/v6.9.2/lib/node_modules/npm

能夠看到,幫助中列出了全部的命令,其中npm help <command &gt能夠查看具體的命令說明。

2.安裝依賴包

安裝依賴包是NPM最多見的用法,它的執行語句是npm install express。執行該命令後,NPM會在當前目錄下建立node_modules目錄,而後在node_modules目錄下建立express目錄,接着將包解壓到這個目錄下。

安裝好依賴包後,直接在代碼中調用require('express');便可引入該包。require()方法在作路徑分析的時候會經過模塊路徑查找到express所在的位置。模塊引入和包的安裝這兩個步驟是相輔相承的。

全局模式安裝

若是包中含有命令行工具,那麼須要執行npm install express -g命令進行全局模式安裝。須要注意的是,全局模式並非將一個模塊包安裝爲一個全局包的意思,它並不意味着能夠從任何地方經過require()來引用到它。

全局模式這個稱謂其實並不精確,存在諸多誤導。實際上,-g是將一個包安裝爲全局可用的可執行命令。它根據包描述文件中的bin字段配置,將實際腳本連接到與Node可執行文件相同的路徑下

"bin": {
  "express": "./bin/express"
}

事實上,經過全局模式安裝的全部模塊包都被安裝進了一個統一的目錄下,這個目錄能夠經過以下方式推算出來:

path.resolve(process.execPath, '..', '..', 'lib', 'node_modules');

若是Node可執行文件的位置是/usr/local/bin/node,那麼模塊目錄就是/usr/local/lib/node_modules。最後,經過軟連接的方式將bin字段配置的可執行文件連接到Node的可執行目錄下。

本地安裝

對於一些沒有發佈到NPM上的包,或是由於網絡緣由致使沒法直接安裝的包,能夠經過將包下載到本地,而後以本地安裝。本地安裝只需爲NPM指明package.json文件所在的位置便可:它能夠是一個包含package.json的存檔文件,也能夠是一個URL地址,也能夠是一個目錄下有package.json文件的目錄位置。具體參數以下:

npm install <tarball file>
npm install <tarball url>
npm install <folder>
從非官方源安裝

若是不能經過官方源安裝,能夠經過鏡像源安裝。在執行命令時,添加 --registry=http://registry.url便可,示例以下:

npm install underscore --registry=http://registry.url

若是使用過程當中幾乎都採用鏡像源安裝,能夠執行如下命令指定默認源:

npm config set registry http://registry.url

3.NPM鉤子命令

"scripts": {
    "test": "make test",
    "start": "./node_modules/.bin/nodemon -L  index.js",
    "dev": "webpack-dev-server --config ./bin/build/webpack.dev.conf.js"
}

當在一個具體的包目錄下執行npm run test時,將會執行"make test"命令。

4.發佈包

爲了將整個NPM的流程串聯起來,這裏將演示如何編寫一個包,將其發佈到NPM倉庫中,並經過NPM安裝回本地。

編寫模塊

模塊的內容咱們儘可能保持簡單,這裏仍是以sayHello做爲例子,相關代碼以下:

exports.sayHello = function () {
  return 'Hello, world.';
};

將這段代碼保存爲hello.js便可。

初始化包描述文件

package.json文件的內容儘管相對較多,可是實際發佈一個包時並不須要一行一行編寫。NPM提供的npm init命令會幫助你生成package.json文件,具體以下所示:

$ npm init
註冊包倉庫帳號

爲了維護包,NPM必需要使用倉庫帳號才容許將包發佈到倉庫中。註冊帳號的命令是npm adduser。這也是一個提問式的交互過程,按順序進行便可:

$ npm adduser
Username: (jacksontian) 
Email: (shyvo1987@gmail.com)
上傳包

上傳包的命令是npm publish 。在剛剛建立的package.json文件所在的目錄下,執行npm publish .開始上傳包,相關代碼以下:

$ npm publish .

在這個過程當中,NPM會將目錄打包爲一個存檔文件,而後上傳到官方源倉庫中。

安裝包
$ npm install package_name
管理包權限

一般,一個包只有一我的擁有權限進行發佈。若是須要多人進行發佈,可使用npm owner命令幫助你管理包的全部者:

$ npm owner ls eventproxy
npm http GET https://registry.npmjs.org/eventproxy
npm http 200 https://registry.npmjs.org/eventproxy
jacksontian <shyvo1987@gmail.com>

使用這個命令,也能夠添加包的擁有者,刪除一個包的擁有者:

npm owner ls <package name>
npm owner add <user> <package name>
npm owner rm <user> <package name>

5.分析包

在使用NPM的過程當中,或許你不能確認當前目錄下可否經過require()順利引入想要的包,這時能夠執行npm ls分析包。

這個命令能夠爲你分析出當前路徑下可以經過模塊路徑找到的全部包,並生成依賴樹,以下:

$ npm ls
/Users/jacksontian
├─┬ connect@2.0.3 
│ ├── crc@0.1.0 
│ ├── debug@0.6.0 
│ ├── formidable@1.0.9 
│ ├── mime@1.2.4 
│ └── qs@0.4.2 
├── hello_test_jackson@0.0.1 
└── urllib@0.2.3

局域NPM

NPM自身是開源的,不管是它的服務器端和客戶端。經過源代碼搭建本身的倉庫並非什麼祕密。

對於企業內部而言,私有的可重用模塊能夠打包到局域NPM倉庫中,這樣能夠保持更新的中心化,不至於讓各個小項目各自維護相同功能的模塊,杜絕經過複製粘貼實現代碼共享的行爲。

NPM潛在問題

潛在的問題在於,在NPM平臺上,每一個人均可以分享包到平臺上,鑑於開發人員水平不一,上面的包的質量也參差不齊。另外一個問題則是,Node代碼能夠運行在服務器端,須要考慮安全問題。

對於包的使用者而言,包質量和安全問題須要做爲是否採納模塊的一個判斷條件。好的包大體具有如下幾種特徵:

  • 具有良好的測試。
  • 具有良好的文檔(README、API)。
  • 具有良好的測試覆蓋率。
  • 具有良好的編碼規範。
  • 更多條件。

先後端共用模塊

JavaScript在Node出現以後,比別的編程語言多了一項優點,那就是一些模塊能夠在先後端實現共用,這是由於不少API在各個宿主環境下都提供。可是在實際狀況中,先後端的環境是略有差異的。

模塊的側重點

縱觀Node的模塊引入過程,幾乎全都是同步的。儘管與Node強調異步的行爲有些相反,但它是合理的。可是若是前端模塊也採用同步的方式來引入,那將會在用戶體驗上形成很大的問題。UI在初始化過程當中須要花費不少時間來等待腳本加載完成。

鑑於網絡的緣由,CommonJS爲後端JavaScript制定的規範並不徹底適合前端的應用場景。通過一段爭執以後,AMD規範最終在前端應用場景中勝出。它的全稱是Asynchronous Module Definition,便是"異步模塊定義",除此以外,還有玉伯定義的CMD規範。

AMD與CMD的區別

CMD與AMD規範的主要區別在於定義模塊和依賴引入的部分。AMD須要在聲明模塊的時候指定全部的依賴,經過形參傳遞依賴到模塊內容中:

define(['dep1', 'dep2'], function (dep1, dep2) {
  return function () {};
});

與AMD模塊規範相比,CMD模塊更接近於Node對CommonJS規範的定義:

define(factory);

在依賴部分,CMD支持動態引入,示例以下:

define(function(require, exports, module) {
  // The module code goes here
});

require、exports和module經過形參傳遞給模塊,在須要依賴模塊時,隨時調用require()引入便可。

兼容多種模塊規範

爲了讓同一個模塊能夠運行在先後端,在寫做過程當中須要考慮兼容前端也實現了模塊規範的環境。爲了保持先後端的一致性,類庫開發者須要將類庫代碼包裝在一個閉包內。如下代碼演示如何將hello()方法定義到不一樣的運行環境中,它可以兼容Node、AMD、CMD以及常見的瀏覽器環境中

;(function (name, definition) {
  // 檢測上下文環境是否爲AMD或CMD
  var hasDefine = typeof define === 'function',
    // 檢查上下文環境是否爲Node
    hasExports = typeof module !== 'undefined' && module.exports;

  if (hasDefine) {
    // AMD環境或CMD環境
    define(definition);
  } else if (hasExports) {
    // 定義爲普通Node模塊
    module.exports = definition();
  } else {
    // 將模塊的執行結果掛在window變量中,在瀏覽器中this指向window對象
    this[name] = definition();
  }
})('hello', function () {
  var hello = function () {};
  return hello;
});
相關文章
相關標籤/搜索