第五章:node 模塊化(上)模塊化演進(jìn)

node 模塊化

JS 誕生的時(shí)候,僅僅是為了實(shí)現(xiàn)網(wǎng)頁表單的本地校驗(yàn)和簡(jiǎn)單的 dom 操作處理。所以并沒有模塊化的規(guī)范設(shè)計(jì)。

項(xiàng)目小的時(shí)候,我們可以通過命名空間、局部作用域、自執(zhí)行函數(shù)等手段實(shí)現(xiàn)變量不沖突。但是到了大一點(diǎn)的項(xiàng)目,各種組件,各種第三方插件和各種 js 腳步融合的時(shí)候,就會(huì)發(fā)現(xiàn)這些技巧遠(yuǎn)遠(yuǎn)不夠。

模塊化的演變

為什么要有 JS 模塊化呢?在瀏覽器中,頂層作用域的變量是全局的,所以項(xiàng)目稍微復(fù)雜點(diǎn),如果引用的 js 非常多的時(shí)候,很容易造成命名沖突,然后造成很大意想不到的結(jié)果。

為了避免全局污染,JS 前輩們想了很多辦法,也就是前端的模塊化的演變過程,可以參考我的視頻:前端模塊化演變

模塊化演變過程:

  • 對(duì)象封裝

    • 所有的方法和屬性封裝到一個(gè)對(duì)象中
    • 所有的訪問通過對(duì)象來訪問,只污染一個(gè)對(duì)象,盡量避免污染其他。
var module = {
 star : 0,
  f1 : function ()
     //...
  },
 f2 : function (){
    //...
  }
 };
module.f1();
module.star = 1;

  • 命名空間(對(duì)象封裝的變種或者叫做升級(jí))

    • 理論意義上減少了變量沖突
    • 缺點(diǎn) 1:暴露了模塊中所有的成員,內(nèi)部狀態(tài)可以被外部改寫,不安全
    • 缺點(diǎn) 2:命名空間會(huì)越來越長(zhǎng)
    var Shop = {}; // 頂層命名空間
    Shop.User = {}; // 電商的用戶模塊
    Shop.User.UserList = {}; //用戶列表頁面模塊。
    Shop.User.UserList.length = 19; // 用戶一共有19個(gè)。
    
    
  • 私有空間

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

    • 開閉原則
    • 可維護(hù)性好
   // 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
  // 動(dòng)畫組件
  (function(laoma, d1, d2) {
    laoma.animate = {};
  })(window.laoma || {}, depend1, depend2);

  // laoma.form.js
  // 表單組件
  (function(laoma, d1, d2) {
    laoma.form = {};
  })(window.laoma || {}, depend1, depend2);

  • 圍觀jQuery的結(jié)構(gòu)
(function(window, undefined) {
    var jQuery = function() {}
    // ...
    window.jQuery = window.$ = jQuery;
})(window);

后續(xù)的演變就是,出現(xiàn)了 AMD、CMD、CommonJS 等模塊化標(biāo)準(zhǔn),然后前端模塊化進(jìn)入大爆發(fā)時(shí)代。

什么是 JS 模塊化

JS 模塊化就是指 JS 代碼分成不同的模塊,模塊內(nèi)部定義變量作用域只屬于模塊內(nèi)部,模塊之間變量命名不會(huì)相互沖突。各個(gè)模塊相互獨(dú)立,而且又可以通過某種方式相互引用協(xié)作。

模塊化的標(biāo)準(zhǔn)

目前前端流行的幾個(gè)模塊化標(biāo)準(zhǔn):CommonJs標(biāo)準(zhǔn)(node 的方案)、AMD、CMD、ES6 模塊方案。

未來的趨勢(shì)肯定是 ES6 的標(biāo)準(zhǔn)方案會(huì)逐漸統(tǒng)一。但是 AMD、CMD 標(biāo)準(zhǔn)跟 CommonJs 的標(biāo)準(zhǔn)相差不大,需要我們都研究一下。

requirejs 入門

requirejs 的使用:

第一步:requirejs 下載

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

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

第三步: 設(shè)置當(dāng)前頁面的 js 入口文件

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

data-main 屬性的作用是,指定網(wǎng)頁程序的主模塊。意思是當(dāng)前整個(gè)網(wǎng)頁的入口代碼。那么其他需要引用的 JS 文件呢?

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

主模塊依賴于其他模塊,這時(shí)就要使用 AMD 規(guī)范定義的的 require()函數(shù)。

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

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

require()異步加載 moduleA,moduleB 和 moduleC,瀏覽器不會(huì)失去響應(yīng);它指定的回調(diào)函數(shù),只有前面的模塊都加載成功后,才會(huì)運(yùn)行,解決了依賴性的問題。

實(shí)際應(yīng)用例子:

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

如果依賴的 JS 文件跟我們的 require.js 不在相同的目錄,那么需要我們單獨(dú)設(shè)置一下路徑映射關(guān)系。

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

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

自定義的模塊還依賴其他模塊,那么 define()函數(shù)的第一個(gè)參數(shù),必須是一個(gè)數(shù)組,指明該模塊的依賴性

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

CMD 與 Sea.js

[Sea.js]在推廣過程中逐漸形成了 CMD 的模塊定義標(biāo)準(zhǔn)。具體詳情請(qǐng)參考。

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

CMD 推崇的是:依賴就近依賴,AMD 則默認(rèn)約束模塊一開始就聲明相關(guān)依賴。其他定義方式及模塊相關(guān)的變量都很相似。

由于 Sea.js 官方文檔很詳細(xì),在此就不再贅述。如何使用請(qǐng)參考官網(wǎng)。

Node 的模塊化

Node.js 有一個(gè)簡(jiǎn)單的模塊加載系統(tǒng),遵循的是 CommonJS 的規(guī)范。 在 Node.js 中,文件和模塊是一一對(duì)應(yīng)的(每個(gè)文件被視為一個(gè)獨(dú)立的模塊)。

Node 在加載 JS 文件的時(shí)候,自動(dòng)給 JS 文件包裝上定義模塊的頭部和尾部。

// nodejs 會(huì)自動(dòng)給我們的js文件添加頭部,見下行
(function(exports, require, module, __filename, __dirname) {
  // 這里是你自己寫的js代碼文件
}); // 自定添加上尾部

見 NodeJs 的源碼截圖:

image

Node會(huì)自動(dòng)給js文件模塊傳遞的5個(gè)參數(shù),每個(gè)模塊內(nèi)的代碼都可以直接用。而且您也看到了,我們的代碼都會(huì)被包裝到一個(gè)函數(shù)中,所以我們的代碼的作用域都是在這個(gè)包裝的函數(shù)內(nèi),這點(diǎn)跟瀏覽器的window全局作用域是不同的。

模塊內(nèi)的參數(shù)說明:

  • __dirname: 當(dāng)前模塊的文件夾名稱
  • __filename: 當(dāng)前模塊的文件名稱---解析后的絕對(duì)路徑。
  • module: 當(dāng)前模塊的引用,通過此對(duì)象可以控制當(dāng)前模塊對(duì)外的行為和屬性等。
  • require:是一個(gè)函數(shù),幫助引入其他模塊.
  • exports:這是一個(gè)對(duì)于 module.exports 的更簡(jiǎn)短的引用形式,也就是當(dāng)前模塊對(duì)外輸出的引用。

如何加載模塊

在模塊內(nèi),我們可以通過require函數(shù)(此函數(shù)由nodejs自動(dòng)傳入,在模塊內(nèi)可以直接用)來加載js文件模塊、node內(nèi)置模塊等。require函數(shù)需要傳入要加載的模塊的名字或者是文件名或者目錄。

/*
假設(shè)開發(fā)目錄下有文件:
.
├── circle.js
└── main.js
*/

// circle.js
exports.pi = 3.1415926;  // 其他模塊引用當(dāng)前模塊時(shí),可以直接通過模塊對(duì)象訪問到 pi屬性。

// 主文件main.js:
const circle = require('./circle.js'); // 加載circle.js文件的module.export 賦值給circle
console.log(circle.pi); // => 3.1415926

解釋:
require加載文件circle.js后,此文件被node拼裝成模塊的代碼,然后執(zhí)行文件里面的js代碼,并把模塊內(nèi)的module.exports做為模塊的對(duì)外接口返回給引用者。

// circle.js 包裝后的代碼就是
// nodejs 會(huì)自動(dòng)給我們的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. 模塊在第一次加載后會(huì)被緩存。 這也意味著如果每次調(diào)用 require('foo') 都解析到同一文件,則返回相同的對(duì)象。

  2. Node.js提供了一些底層的核心模塊,它們定義在 Node.js 源代碼的 lib/ 目錄下。這些原生模塊在Node.js源代碼編譯的時(shí)候編譯進(jìn)了二進(jìn)制執(zhí)行文件,加載的速度最快。開發(fā)人員自定義的js文件是動(dòng)態(tài)加載的,加載速度比原生模塊慢,這個(gè)只是在第一次加載有區(qū)別,模塊加載完后都會(huì)被緩存,后續(xù)使用就不會(huì)被再次加載。

  3. require() 總是會(huì)優(yōu)先加載核心模塊。 例如,require('http') 始終返回內(nèi)置的 HTTP 模塊,即使有同名文件。

文件模塊中,又分為3類模塊。這三類文件模塊以后綴來區(qū)分,Node.js會(huì)根據(jù)后綴名來決定加載方法。

  • .js。通過fs模塊同步讀取js文件并編譯執(zhí)行。
  • .node。通過C/C++進(jìn)行編寫的Addon。通過dlopen方法進(jìn)行加載。
  • .json。讀取文件,調(diào)用JSON.parse解析加載。

參考源碼:

image

模塊加載邏輯

require方法接受以下幾種參數(shù)的傳遞:

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

文件加載的邏輯還是比較復(fù)雜的,而且考慮很多種情況。

  • require加載文件模塊,直接找對(duì)應(yīng)完整文件名最快,如果不給文件后綴名,node會(huì)自動(dòng)嘗試添加 js\json\mod等后綴進(jìn)行嘗試。當(dāng)沒有以 '/'、'./' 或 '../' 開頭來表示文件時(shí),這個(gè)模塊必須是一個(gè)核心模塊或加載自 node_modules 目錄。如果給定的路徑不存在,則 require() 會(huì)拋出一個(gè) code 屬性為 'MODULE_NOT_FOUND' 的 Error。
  • 如果加載目錄,又分三種情況:
  1. 第一種方式是在根目錄下創(chuàng)建一個(gè) package.json 文件,并指定一個(gè) main 模塊。 例子,package.json 文件類似:
{ 
  "name" : "some-library",
  "main" : "./lib/some-library.js"
}

如果這是在 ./some-library 目錄中,則 require('./some-library') 會(huì)試圖加載 ./some-library/lib/some-library.js。不存在也會(huì)報(bào)錯(cuò)。

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

  1. 其他的情況,則從 node_modules 目錄加載。 Node.js 會(huì)從當(dāng)前模塊的父目錄開始,嘗試從它的 /node_modules 目錄里加載模塊。 Node.js 不會(huì)附加 node_modules 到一個(gè)已經(jīng)以 node_modules 結(jié)尾的路徑上。

如果還是沒有找到,則移動(dòng)到再上一層父目錄,直到文件系統(tǒng)的根目錄。

例子,如果在 '/home/ry/projects/foo.js' 文件里調(diào)用了 require('bar.js'),則 Node.js 會(huì)按以下順序查找:

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

這使得程序本地化它們的依賴,避免它們產(chǎn)生沖突。

可以通過module.paths打印當(dāng)前node尋找模塊要搜索的所有路徑。

綜上邏輯,看官網(wǎng)的加載邏輯偽代碼:

從 Y 路徑的模塊 require(X)
1\. 如果 X 是一個(gè)核心模塊,
   a. 返回核心模塊
   b. 結(jié)束
2\. 如果 X 是以 '/' 開頭
   a. 設(shè) Y 為文件系統(tǒng)根目錄
3\. 如果 X 是以 './' 或 '/' 或 '../' 開頭
   a. 加載文件(Y + X)
   b. 加載目錄(Y + X)
4\. 加載Node模塊(X, dirname(Y))
5\. 拋出 "未找到"

加載文件(X)
1\. 如果 X 是一個(gè)文件,加載 X 作為 JavaScript 文本。結(jié)束
2\. 如果 X.js 是一個(gè)文件,加載 X.js 作為 JavaScript 文本。結(jié)束
3\. 如果 X.json 是一個(gè)文件,解析 X.json 成一個(gè) JavaScript 對(duì)象。結(jié)束
4\. 如果 X.node 是一個(gè)文件,加載 X.node 作為二進(jìn)制插件。結(jié)束

加載目錄(X)
1\. 如果 X/package.json 是一個(gè)文件,
   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

總結(jié):

我們自己加載模塊的時(shí)候,盡量的寫全點(diǎn),盡量不要讓node去推斷,引用文件模塊直接把文件名寫全,文件

module 對(duì)象

如果想查看當(dāng)前模塊,可以直接使用console直接打印一下module對(duì)象。

console.dir(module);
// 打印結(jié)果:
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' ] }

在每個(gè)模塊中,module 的自由變量是一個(gè)指向表示當(dāng)前模塊的對(duì)象的引用。 為了方便,module.exports 也可以通過全局模塊的 exports 對(duì)象訪問。

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

// 模塊的構(gòu)造函數(shù)
function Module(id, parent) {
  this.id = id;
  this.exports = {};   // 模塊實(shí)例的exports屬性初始化?。。odule.exports === exports
  this.parent = parent;
  updateChildren(parent, this, false);
  this.filename = null;
  this.loaded = false;
  this.children = [];
}

注意:exportsmodule.exports 的一個(gè)引用,就好比在每一個(gè)模塊定義最開始的地方寫了這么一句代碼:var exports = module.exports要注意的一點(diǎn)就是: 最終模塊會(huì)把module.exports作為對(duì)外的接口。所以,module.exports的引用地址發(fā)生了改變,在改變之前通過exports屬性設(shè)置的都會(huì)被遺棄。

module的其他屬性:

屬性 類型 屬性說明
module.filename string 模塊的完全解析后的文件名
module.id string 模塊的標(biāo)識(shí)符。 通常是完全解析后的文件名。
module.loaded boolean 模塊是否已經(jīng)加載完成,或正在加載
module.parent object 最先引用該模塊的模塊。
module.paths string 模塊的搜索路徑。
module.children object 被該模塊引用的模塊對(duì)象。

詳情請(qǐng)參考:中文Node文檔

es6的模塊

es6的模塊引入和導(dǎo)出跟以上都有點(diǎn)區(qū)別。不過肯定是未來的統(tǒng)一的模型。node目前版本位置并沒有es6的模塊api支持的很好,只是在實(shí)驗(yàn)階段。不過我們可以借助babel來轉(zhuǎn)換我們的js代碼,可以放心的使用。

由于這塊內(nèi)容,請(qǐng)直接參考阮一峰老師的es6入門

總結(jié)

從客戶端到服務(wù)端我們都搞定了js的模塊化,也就是說讓js走向了工程化,大型應(yīng)用的基礎(chǔ)被奠定了。當(dāng)然,目前業(yè)界模塊化已經(jīng)走入深水區(qū),尤其是webpack已經(jīng)可以讓前端的大部分資源都模塊化使用。

我們已經(jīng)搞定了,自己書寫模塊,已經(jīng)引用核心模塊、自己寫的模塊,那么怎么引用第三方模塊,怎么使用package文件,好吧提前透露一下:npm解密(下一節(jié))

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容