Node入門教程(6)第五章:node 模塊化(上)模塊化演進

node 模塊化

JS 誕生的時候,僅僅是爲了實現網頁表單的本地校驗和簡單的 dom 操做處理。因此並無模塊化的規範設計。javascript

項目小的時候,咱們能夠經過命名空間、局部做用域、自執行函數等手段實現變量不衝突。可是到了大一點的項目,各類組件,各類第三方插件和各類 js 腳步融合的時候,就會發現這些技巧遠遠不夠。css

模塊化的演變

爲何要有 JS 模塊化呢?在瀏覽器中,頂層做用域的變量是全局的,因此項目稍微複雜點,若是引用的 js 很是多的時候,很容易形成命名衝突,而後形成很大意想不到的結果。html

爲了不全局污染,JS 前輩們想了不少辦法,也就是前端的模塊化的演變過程,能夠參考個人視頻:前端模塊化演變前端

模塊化演變過程:java

  • 對象封裝node

    • 全部的方法和屬性封裝到一個對象中
    • 全部的訪問經過對象來訪問,只污染一個對象,儘可能避免污染其餘。
var module = {  star : 0,   f1 : function ()     //...   },  f2 : function (){     //...   }  }; module.f1(); module.star = 1; 
  • 命名空間(對象封裝的變種或者叫作升級)jquery

    • 理論意義上減小了變量衝突
    • 缺點 1:暴露了模塊中全部的成員,內部狀態能夠被外部改寫,不安全
    • 缺點 2:命名空間會愈來愈長
    var Shop = {}; // 頂層命名空間 Shop.User = {}; // 電商的用戶模塊 Shop.User.UserList = {}; //用戶列表頁面模塊。 Shop.User.UserList.length = 19; // 用戶一共有19個。 
  • 私有空間webpack

    • 私有空間的變量和函數不會影響全局做用域
    • 公開公有方法,隱藏私有屬性
    // => 給單個文件裏面定義的局部變量都 變成 局部做用域裏面的變量。 // 第二個嘗試: // a.js (function() { var a = 9; })(); // b.js (function() { var a = 'ssss'; })(); 
  • 模塊的維護和擴展git

    • 開閉原則
    • 可維護性好
    • // laoma.core.js (function(laoma, d1, d2) { laoma.Btn = { getVal: function() { console.log('val'); }, setVal: function(str) { console.log('setvale'); } }; })(window.laoma || {}, depend1, depend2); // laoma.animate.js // 動畫組件 (function(laoma, d1, d2) { laoma.animate = {}; })(window.laoma || {}, depend1, depend2); // laoma.form.js // 表單組件 (function(laoma, d1, d2) { laoma.form = {}; })(window.laoma || {}, depend1, depend2); 
      • 圍觀jQuery的結構
      (function(window, undefined) { var jQuery = function() {} // ... window.jQuery = window.$ = jQuery; })(window);

後續的演變就是,出現了 AMD、CMD、CommonJS 等模塊化標準,而後前端模塊化進入大爆發時代。es6

什麼是 JS 模塊化

JS 模塊化就是指 JS 代碼分紅不一樣的模塊,模塊內部定義變量做用域只屬於模塊內部,模塊之間變量命名不會相互衝突。各個模塊相互獨立,並且又能夠經過某種方式相互引用協做。

模塊化的標準

目前前端流行的幾個模塊化標準:CommonJs標準(node 的方案)、AMDCMD、ES6 模塊方案。

將來的趨勢確定是 ES6 的標準方案會逐漸統一。可是 AMD、CMD 標準跟 CommonJs 的標準相差不大,須要咱們都研究一下。

requirejs 入門

requirejs 的使用:

第一步:requirejs 下載

第二步: 把 requirejs 直接引入到 html

<script src="js/require.js"></script>

第三步: 設置當前頁面的 js 入口文件

<script src="js/require.js" data-main="js/main"></script>

data-main 屬性的做用是,指定網頁程序的主模塊。意思是當前整個網頁的入口代碼。那麼其餘須要引用的 JS 文件呢?

第四步: 引用其餘模塊的文件

主模塊依賴於其餘模塊,這時就要使用 AMD 規範定義的的 require()函數。

// main.js require(['moduleA', 'moduleB', 'moduleC'], function(moduleA, moduleB, moduleC) { // some code here }); 

require()函數接受兩個參數。第一個參數是一個數組,表示所依賴的模塊,上例就是['moduleA', 'moduleB', 'moduleC'],即主模塊依賴這三個模塊;第二個參數是一個回調函數,當前面指定的模塊都加載成功後,它將被調用。加載的模塊會以參數形式傳入該函數,從而在回調函數內部就可使用這些模塊。

require()異步加載 moduleA,moduleB 和 moduleC,瀏覽器不會失去響應;它指定的回調函數,只有前面的模塊都加載成功後,纔會運行,解決了依賴性的問題。

實際應用例子:

require(['jquery', 'underscore', 'backbone'], function($, _, Backbone) { // some code here }); 

若是依賴的 JS 文件跟咱們的 require.js 不在相同的目錄,那麼須要咱們單獨設置一下路徑映射關係。

require.config({ paths: { underscore: 'lib/underscore.min', backbone: 'lib/backbone.min' } }); 

第五步:如何自定義 AMD 模塊(可選)

自定義的模塊還依賴其餘模塊,那麼 define()函數的第一個參數,必須是一個數組,指明該模塊的依賴性

define(['myLib'], function(myLib) { function foo() { myLib.doSomething(); } return { foo: foo }; }); 

CMD 與 Sea.js

[Sea.js]在推廣過程當中逐漸造成了 CMD 的模塊定義標準。具體詳情請參考

跟 AMD 比較相似,並且兼容 CommonJS 的模塊寫法。

CMD 推崇的是:依賴就近依賴,AMD 則默認約束模塊一開始就聲明相關依賴。其餘定義方式及模塊相關的變量都很類似。

因爲 Sea.js 官方文檔很詳細,在此就再也不贅述。如何使用請參考官網

Node 的模塊化

Node.js 有一個簡單的模塊加載系統,遵循的是 CommonJS 的規範。 在 Node.js 中,文件和模塊是一一對應的(每一個文件被視爲一個獨立的模塊)。

Node 在加載 JS 文件的時候,自動給 JS 文件包裝上定義模塊的頭部和尾部。

// nodejs 會自動給咱們的js文件添加頭部,見下行 (function(exports, require, module, __filename, __dirname) { // 這裏是你本身寫的js代碼文件 }); // 自定添加上尾部 

見 NodeJs 的源碼截圖:

 

 

Node會自動給js文件模塊傳遞的5個參數,每一個模塊內的代碼均可以直接用。並且您也看到了,咱們的代碼都會被包裝到一個函數中,因此咱們的代碼的做用域都是在這個包裝的函數內,這點跟瀏覽器的window全局做用域是不一樣的。

模塊內的參數說明:

  • __dirname: 當前模塊的文件夾名稱
  • __filename: 當前模塊的文件名稱---解析後的絕對路徑。
  • module: 當前模塊的引用,經過此對象能夠控制當前模塊對外的行爲和屬性等。
  • require:是一個函數,幫助引入其餘模塊.
  • exports:這是一個對於 module.exports 的更簡短的引用形式,也就是當前模塊對外輸出的引用。

如何加載模塊

在模塊內,咱們能夠經過require函數(此函數由nodejs自動傳入,在模塊內能夠直接用)來加載js文件模塊、node內置模塊等。require函數須要傳入要加載的模塊的名字或者是文件名或者目錄。

/* 假設開發目錄下有文件: . ├── circle.js └── main.js */ // circle.js exports.pi = 3.1415926; // 其餘模塊引用當前模塊時,能夠直接經過模塊對象訪問到 pi屬性。 // 主文件main.js: const circle = require('./circle.js'); // 加載circle.js文件的module.export 賦值給circle console.log(circle.pi); // => 3.1415926 

解釋:
require加載文件circle.js後,此文件被node拼裝成模塊的代碼,而後執行文件裏面的js代碼,並把模塊內的module.exports作爲模塊的對外接口返回給引用者。

// circle.js 包裝後的代碼就是 // nodejs 會自動給咱們的js文件添加頭部 (function(exports, require, module, __filename, __dirname) { exports.pi = 3.1415926; // exports === modeule.exports }); // 自定添加上尾部 // 主文件main.js: const circle = require('./circle.js'); circle => circle.js中的module.exports 

加載策略

Node.js的模塊分爲兩類,一類爲原生(核心)模塊,一類爲文件模塊。

  1. 模塊在第一次加載後會被緩存。 這也意味着若是每次調用 require('foo') 都解析到同一文件,則返回相同的對象。

  2. Node.js提供了一些底層的核心模塊,它們定義在 Node.js 源代碼的 lib/ 目錄下。這些原生模塊在Node.js源代碼編譯的時候編譯進了二進制執行文件,加載的速度最快。開發人員自定義的js文件是動態加載的,加載速度比原生模塊慢,這個只是在第一次加載有區別,模塊加載完後都會被緩存,後續使用就不會被再次加載。

  3. require() 老是會優先加載核心模塊。 例如,require('http') 始終返回內置的 HTTP 模塊,即便有同名文件。

文件模塊中,又分爲3類模塊。這三類文件模塊之後綴來區分,Node.js會根據後綴名來決定加載方法。

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

參考源碼:

 

 

模塊加載邏輯

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

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

文件加載的邏輯仍是比較複雜的,並且考慮不少種狀況。 require加載文件模塊,直接找對應完整文件名最快,若是不給文件後綴名,node會自動嘗試添加 js\json\mod等後綴進行嘗試。當沒有以 '/'、'./' 或 '../' 開頭來表示文件時,這個模塊必須是一個核心模塊或加載自 node_modules 目錄。若是給定的路徑不存在,則 require() 會拋出一個 code 屬性爲 'MODULE_NOT_FOUND' 的 Error。 若是加載目錄,又分三種狀況: 第一種方式是在根目錄下建立一個 package.json 文件,並指定一個 main 模塊。 例子,package.json 文件相似:

{ 
  "name" : "some-library", "main" : "./lib/some-library.js" } 

若是這是在 ./some-library 目錄中,則 require('./some-library') 會試圖加載 ./some-library/lib/some-library.js。不存在也會報錯。

若是目錄裏沒有 package.json 文件,則 Node.js 就會試圖加載目錄下的 index.js 或 index.node 文件。 例如,若是上面的例子中沒有 package.json 文件,則 require('./some-library') 會試圖加載:

./some-library/index.js
./some-library/index.node

其餘的狀況,則從 node_modules 目錄加載。 Node.js 會從當前模塊的父目錄開始,嘗試從它的 /node_modules 目錄里加載模塊。 Node.js 不會附加 node_modules 到一個已經以 node_modules 結尾的路徑上。

若是仍是沒有找到,則移動到再上一層父目錄,直到文件系統的根目錄。

例子,若是在 '/home/ry/projects/foo.js' 文件裏調用了 require('bar.js'),則 Node.js 會按如下順序查找:

/home/ry/projects/node_modules/bar.js
/home/ry/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js

這使得程序本地化它們的依賴,避免它們產生衝突。

能夠經過module.paths打印當前node尋找模塊要搜索的全部路徑。

綜上邏輯,看官網的加載邏輯僞代碼:

從 Y 路徑的模塊 require(X)
1\. 若是 X 是一個核心模塊,
   a. 返回核心模塊
   b. 結束
2\. 若是 X 是以 '/' 開頭 a. 設 Y 爲文件系統根目錄 3\. 若是 X 是以 './' 或 '/' 或 '../' 開頭 a. 加載文件(Y + X) b. 加載目錄(Y + X) 4\. 加載Node模塊(X, dirname(Y)) 5\. 拋出 "未找到" 加載文件(X) 1\. 若是 X 是一個文件,加載 X 做爲 JavaScript 文本。結束 2\. 若是 X.js 是一個文件,加載 X.js 做爲 JavaScript 文本。結束 3\. 若是 X.json 是一個文件,解析 X.json 成一個 JavaScript 對象。結束 4\. 若是 X.node 是一個文件,加載 X.node 做爲二進制插件。結束 加載索引(X) 1\. 若是 X/index.js 是一個文件,加載 X/index.js 做爲 JavaScript 文本。結束 3\. 若是 X/index.json 是一個文件,解析 X/index.json 成一個 JavaScript 對象。結束 4\. 若是 X/index.node 是一個文件,加載 X/index.node 做爲二進制插件。結束 加載目錄(X) 1\. 若是 X/package.json 是一個文件, a. 解析 X/package.json,查找 "main" 字段 b. let M = X + (json main 字段) c. 加載文件(M) d. 加載索引(M) 2\. 加載索引(X) 加載Node模塊(X, START) 1\. let DIRS=NODE_MODULES_PATHS(START) 2\. for each DIR in DIRS: a. 加載文件(DIR/X) b. 加載目錄(DIR/X) NODE_MODULES_PATHS(START) 1\. let PARTS = path split(START) 2\. let I = count of PARTS - 1 3\. let DIRS = [] 4\. while I >= 0, a. if PARTS[I] = "node_modules" CONTINUE b. DIR = path join(PARTS[0 .. I] + "node_modules") c. DIRS = DIRS + DIR d. let I = I - 1 5\. return DIRS 

總結:

咱們本身加載模塊的時候,儘可能的寫全點,儘可能不要讓node去推斷,引用文件模塊直接把文件名寫全,文件

module 對象

若是想查看當前模塊,能夠直接使用console直接打印一下module對象。

console.dir(module); // 打印結果: Module { id: '.', exports: {}, parent: null, filename: '/Users/flydragon/Desktop/work/gitdata/nodedemos/demos/02console.js', loaded: false, children: [], paths: [ '/Users/flydragon/Desktop/work/gitdata/nodedemos/demos/node_modules', '/Users/flydragon/Desktop/work/gitdata/nodedemos/node_modules', '/Users/flydragon/Desktop/work/gitdata/node_modules', '/Users/flydragon/Desktop/work/node_modules', '/Users/flydragon/Desktop/node_modules', '/Users/flydragon/node_modules', '/Users/node_modules', '/node_modules' ] } 

在每一個模塊中,module 的自由變量是一個指向表示當前模塊的對象的引用。 爲了方便,module.exports 也能夠經過全局模塊的 exports 對象訪問。

module.exports 與 exports區別,看Node中的源碼就知道了。

// 模塊的構造函數 function Module(id, parent) { this.id = id; this.exports = {}; // 模塊實例的exports屬性初始化!!!module.exports === exports this.parent = parent; updateChildren(parent, this, false); this.filename = null; this.loaded = false; this.children = []; } 

exports 是 module.exports 的一個引用,就比如在每個模塊定義最開始的地方寫了這麼一句代碼:var exports = module.exports

要注意的一點就是: 最終模塊會把module.exports做爲對外的接口。因此,module.exports的引用地址發生了改變,在改變以前經過exports屬性設置的都會被遺棄。

module的其餘屬性: 屬性|類型|屬性說明 ---|--- module.filename|string|模塊的徹底解析後的文件名 module.id|string|模塊的標識符。 一般是徹底解析後的文件名。 module.loaded| boolean |模塊是否已經加載完成,或正在加載中。 module.loaded| boolean |模塊是否已經加載完成,或正在加載中。 module.parent| object | 最早引用該模塊的模塊。 module.paths|string|模塊的搜索路徑。 module.children|object |被該模塊引用的模塊對象。

詳情請參考:中文Node文檔

es6的模塊

es6的模塊引入和導出跟以上都有點區別。不過確定是將來的統一的模型。node目前版本位置並無es6的模塊api支持的很好,只是在實驗階段。不過咱們能夠藉助babel來轉換咱們的js代碼,能夠放心的使用。

因爲這塊內容,請直接參考阮一峯老師的es6入門

總結

從客戶端到服務端咱們都搞定了js的模塊化,也就是說讓js走向了工程化,大型應用的基礎被奠基了。固然,目前業界模塊化已經走入深水區,尤爲是webpack已經可讓前端的大部分資源都模塊化使用。

咱們已經搞定了,本身書寫模塊,已經引用核心模塊、本身寫的模塊,那麼怎麼引用第三方模塊,怎麼使用package文件,好吧提早透露一下:npm解密(下一節)


參考:

  1. NodeJs 官網文檔
  2. MDN 文檔
  3. Javascript 模塊化編程(二):AMD 規範
  4. Javascript 模塊化編程(三):require.js 的用法
  5. CMD 模塊定義規範

老馬免費視頻教程

返回教程列表首頁

github地址:https://github.com/malun666/aicoder_node

相關文章
相關標籤/搜索