前端模塊化(一)nodeJS中的CommonJS規範

序言

模塊化,你們用vuereact等東西,都會接觸到像exportsmodule.exportsexportexport defaultrequiredefineimport等等字段,感受不少人對於這些東西仍是分不清,概念很是的模糊,便想着寫這麼一篇文章,一是幫助本身梳理知識點,二是跟你們一塊兒成長。其中有寫得不對的,請及時提出來 ,我及時更正。javascript

剛開始寫的時候有些無從下手,一是由於知識點太多,二是由於本身的經驗還不足以幫助你們從深層次剖析js的模塊化中的區別,以及其實現原理、思想。這是一篇本身的學習筆記整理,我只能帶你們瞭解前端模塊化,區分他們並正確的使用他們。html

先給你們扔出幾條知識:前端

  • CommonJSNodeJS模塊系統具體實現的基石。
  • AMD:異步模塊規範,是RequireJS在推廣過程當中對模塊定義的規範化產出的,推崇依賴前置;
  • UMD:兼容AMDcommonJS規範的同時,還兼容全局引用的方式;
  • CMD:是SeaJS 在推廣過程當中對模塊定義的規範化產出的,推崇依賴就近;
  • ES6:ES6模塊的設計思想是儘可能的靜態化,使得編譯時就能肯定模塊的依賴關係,以及輸入和輸出的變量;

CommonJS規範

CommonJS官網上寫道,它但願js不只僅能夠在瀏覽器上運行,而是能夠在任何地方運行,使其具有開發大型應用的能力。vue

javascript: not just for browsers any more!

CommonJS定義的模塊分爲:java

  1. 模塊引用(require)
  2. 模塊定義(exports)
  3. 模塊標識(module)

他能夠作到:node

  • 服務器端JavaScript應用程序
  • 命令行工具
  • 圖形界面應用程序
  • 混合應用程序(如,Titanium或Adobe AIR)

CommonJS模塊的特色以下react

  • 全部代碼都運行在模塊做用域,不會污染全局做用域。
  • 模塊能夠屢次加載,可是隻會在第一次加載時運行一次,而後運行結果就被緩存了,之後再加載,就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存。
  • 模塊加載的順序,按照其在代碼中出現的順序。

先談一談包的概念

前面給你們說過,node.js是基於CommonJS的規範實現的,NPM你們必定都很熟悉,它實踐了CommonJS的包規範。jquery

包規範

關於包規範,類比於git倉庫,咱們能夠這麼理解:git

  • git init在當前文件夾中生成了隱藏文件.git,咱們把它叫作git倉庫
  • npm init命令在當前文件夾中生成了配置文件package.json,它描述了當前這個包,咱們管這個文件叫作包(概念不許確,能夠這麼理解)。

包結構

嚴格按照CommonJS規範來的話,包的目錄應當包含如下文件或目錄。npm

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

package.json則是一個配置文件,它描述了包的相關信息。

NodeJS模塊

既然node.js是基於CommonJS實現的,那麼咱們先來簡單看看NodeJS的模塊原理。

最近參加了公司開展的一次培訓,結構性思惟培養。任何東西都可以進行分類,事物一旦進行分類,更利於你們對此事物的認知,也能方便你們記憶。因此咱們先來看看Node的模塊分類🐵。

一般分類

先給你們講講模塊的分類

  • 核心模塊

    • 核心模塊指的是那些被編譯進Node的二進制模塊
    • 預置在Node中,提供Node的基本功能,如fs、http、https等。
    • 核心模塊使用C/C++實現,外部使用JS封裝
  • 第三方模塊

    • Node使用NPM(Node Package Manager)安裝第三方模塊
    • NPM會將模塊安裝(能夠說是下載到)到應用根目錄下的node_modules文件夾中
    • 模塊加載時,node會先在覈心模塊文件夾中進行搜索,而後再到node_modules文件夾中進行搜索
  • 文件模塊

    • 文件可放在任何位置
    • 加載模塊文件時加上路徑便可
  • 文件夾模塊(後續的nodeJS的加載規則將會詳細介紹)

    • Node首先會在該文件夾中搜索package.json文件,

      • 存在,Node便嘗試解析它,並加載main屬性指定的模塊文件
      • 不存在(或者package.json沒有定義main屬性),Node默認加載該文件夾下的index.js文件(main屬性其實NodeJS的一個拓展,CommonJS標準定義中其實並不包括此字段)

估計你們對於文件夾模塊概念都比較模糊,它其實至關於一個自定義模塊,給你們舉一個栗子🤡:

在根目錄下的/main.js中,咱們須要使用一個自定義文件夾模塊。咱們將全部的自定義文件夾模塊存放在根目錄下的/module下,其中有一個/module/demo文件夾,是咱們須要引入的文件夾模塊;

|—— main.js
|—— module
    |—— demo
        |—— package.json
        |—— demo.js

package.json文件的信息以下:

{
    "name": "demo",
    "version": "1.0.0",
    "main": "./demo.js"
}

/main.js中:

let demo = require("./modules/demo");

此時,Node將會根據package.json中指定的main屬性,去加載./modules/demo/demo.js;

這就是一個最簡單的包,以一個文件夾做爲一個模塊。

nodeJS模塊與CommonJS

module屬性
  • module.id 模塊的識別符,一般是帶有絕對路徑的模塊文件名。
  • module.filename 模塊的文件名,帶有絕對路徑。
  • module.loaded 返回一個布爾值,表示模塊是否已經完成加載。
  • module.parent 返回一個對象,表示調用該模塊的模塊。
  • module.children 返回一個數組,表示該模塊要用到的其餘模塊。
  • module.exports 表示模塊對外輸出的值。

來作一個測試,看看module究竟是個什麼東西(寫的詳細些,水平高的自行濾過);

  1. 新建一個文件夾,名爲modulePractice
  2. 命令行進入cd modulePractive/文件夾
  3. npm init,輸入信息,此時咱們至關於創建了一個包
  4. npm install jquery,安裝jquery來作測試
  5. 新建modulePractice/test.js
|—— modulePractice
    |—— node_module
    |—— package.json
    |—— test.js
// test.js
var jquery = require('jquery');
exports.$ = jquery;
console.log(module);        //module就是當前模塊內部中的一個對象,表明當前對象

終端執行這個文件

node test.js

命令行會輸出以下信息:

Module {
  id: '.',
  exports: { '$': [Function] },
  parent: null,
  filename: '/Applications/practice/nodepractice/modulePratice/test.js',
  loaded: false,
  children:
   [ Module {
       id: '/Applications/practice/nodepractice/modulePratice/node_modules/jquery/dist/jquery.js',
       exports: [Function],
       parent: [Circular],
       filename: '/Applications/practice/nodepractice/modulePratice/node_modules/jquery/dist/jquery.js',
       loaded: true,
       children: [],
       paths: [Array] } ],
  paths:
   [ '/Applications/practice/nodepractice/modulePratice/node_modules',
     '/Applications/practice/nodepractice/node_modules',
     '/Applications/practice/node_modules',
     '/Applications/node_modules',
     '/node_modules' ] }

如今咱們能夠看到,當前這個模塊的parent屬性爲null,這證實當前這個模塊是一個入口腳本。

咱們來看看在test.js中引入別的文件模塊,module會輸出什麼

6.新建一個modulePractice/child.js

|—— modulePractice
    |—— node_module
    |—— package.json
    |—— test.js
    |—— child.js
//child.js
var str = "I'm child";
exports.str = str;
console.log(module);

再一次執行:

node test.js

咱們再來分別看看child.js中的moduletest.js中的module分別是什麼樣子

//這個是child.js中輸出的信息
Module {
  id: '/Applications/practice/nodepractice/modulePratice/child.js',
  exports: { str: 'I\'m child' },
  parent:
   Module {
     id: '.',
     exports: {},
     parent: null,
     filename: '/Applications/practice/nodepractice/modulePratice/test.js',
     loaded: false,
     children: [ [Circular] ],
     paths:
      [ '/Applications/practice/nodepractice/modulePratice/node_modules',
        '/Applications/practice/nodepractice/node_modules',
        '/Applications/practice/node_modules',
        '/Applications/node_modules',
        '/node_modules' ] },
  filename: '/Applications/practice/nodepractice/modulePratice/child.js',
  loaded: false,
  children: [],
  paths:
   [ '/Applications/practice/nodepractice/modulePratice/node_modules',
     '/Applications/practice/nodepractice/node_modules',
     '/Applications/practice/node_modules',
     '/Applications/node_modules',
     '/node_modules' ] }

//這個是test.js中輸出的module信息
Module {
  id: '.',
  exports: { '$': [Function] },
  parent: null,
  filename: '/Applications/practice/nodepractice/modulePratice/test.js',
  loaded: false,
  children:
   [ Module {
       id: '/Applications/practice/nodepractice/modulePratice/child.js',
       exports: [Object],
       parent: [Circular],
       filename: '/Applications/practice/nodepractice/modulePratice/child.js',
       loaded: true,
       children: [],
       paths: [Array] },
     Module {
       id: '/Applications/practice/nodepractice/modulePratice/node_modules/jquery/dist/jquery.js',
       exports: [Function],
       parent: [Circular],
       filename: '/Applications/practice/nodepractice/modulePratice/node_modules/jquery/dist/jquery.js',
       loaded: true,
       children: [],
       paths: [Array] } ],
  paths:
   [ '/Applications/practice/nodepractice/modulePratice/node_modules',
     '/Applications/practice/nodepractice/node_modules',
     '/Applications/practice/node_modules',
     '/Applications/node_modules',
     '/node_modules' ] }

你們能夠看到

  • child.js中的parent屬性輸出的是test.jsmodule信息,
  • test.js中的children屬性,包括了jquerychild.js兩個module信息
  • test.js中的parent屬性爲null

由此,咱們能夠以module.parent來判斷當前模塊是不是入口腳本

固然,也有別的辦法能夠判斷入口腳本,好比使用require.main

child.js修改以下:

//child.js
var str = "I'm child";
exports.str = str;
console.log(require.main);
node test.js
Module {
  id: '.',
  exports: {},
  parent: null,
  filename: '/Applications/practice/nodepractice/modulePratice/test.js',
  loaded: false,
  children:
   [ Module {
       id: '/Applications/practice/nodepractice/modulePratice/child.js',
       exports: [Object],
       parent: [Circular],
       filename: '/Applications/practice/nodepractice/modulePratice/child.js',
       loaded: false,
       children: [],
       paths: [Array] } ],
  paths:
   [ '/Applications/practice/nodepractice/modulePratice/node_modules',
     '/Applications/practice/nodepractice/node_modules',
     '/Applications/practice/node_modules',
     '/Applications/node_modules',
     '/node_modules' ] }

能夠看到,require.main直接輸出的是入口腳本,因爲咱們是在child.js中打印的require.main,因此咱們拿不到test.js這個入口腳本的exports,且只能看到當前入口腳本的children僅有child.js一個模塊;

換一種方式進行測試,咱們在test.js中打印require.main看一下會輸出什麼東西;

test.js修改以下:

var child = require("./child.js");
var jquery = require('jquery');
exports.$ = jquery;
console.log(require.main);

執行

node test.js

拿到以下信息:

Module {
  id: '.',
  exports: { '$': [Function] },
  parent: null,
  filename: '/Applications/practice/nodepractice/modulePratice/test.js',
  loaded: false,
  children:
   [ Module {
       id: '/Applications/practice/nodepractice/modulePratice/child.js',
       exports: [Object],
       parent: [Circular],
       filename: '/Applications/practice/nodepractice/modulePratice/child.js',
       loaded: true,
       children: [],
       paths: [Array] },
     Module {
       id: '/Applications/practice/nodepractice/modulePratice/node_modules/jquery/dist/jquery.js',
       exports: [Function],
       parent: [Circular],
       filename: '/Applications/practice/nodepractice/modulePratice/node_modules/jquery/dist/jquery.js',
       loaded: true,
       children: [],
       paths: [Array] } ],
  paths:
   [ '/Applications/practice/nodepractice/modulePratice/node_modules',
     '/Applications/practice/nodepractice/node_modules',
     '/Applications/practice/node_modules',
     '/Applications/node_modules',
     '/node_modules' ] }

也就是說,在真正的入口文件中,打印的require.main信息,纔是徹底的信息;

一樣也能夠用require.main輸出的module信息中的parent屬性,來判斷是不是入口腳本;

固然也能夠在當前模塊中判斷require.main === module,若爲真,則表明它是被直接執行的(node xxx.js

exports屬性

如今咱們瞭解了module屬性,那麼module.exportsexports都是什麼呢?

從以上的測試,咱們能夠看到,module中其實帶有的exports屬性,就是咱們對外的接口。也就是說,module.exports屬性表示當前模塊對外輸出的接口,其餘文件加載該模塊,實際上就是讀取module.exports變量。

exports變量,其實是nodeJS爲了方便,爲每一個模塊提供一個exports變量,指向module.exports。這等同在每一個模塊頭部,有一行這樣的命令。

var exports = module.exports;

所以,咱們能夠直接向exports對象添加方法

exports.area = function (r) {
  return Math.PI * r * r;
};

exports.circumference = function (r) {
  return 2 * Math.PI * r;
};

注意點:

  • 不能直接將exports變量指向一個,等於切斷了exportsmodule.exports的聯繫,他將再也不是一個接口,而僅僅當前模塊中的一個局部變量。此時你在當前模塊中寫的全部其餘的exports導出的接口,都將失效。而只有module.exports可以暴露出去當前模塊的對外接口。

其實說簡單點,nodeJS僅僅爲了方便,用了一個變量exports直接指向了module.exports了,你只要注意exports變量,正確指向module.exports屬性便可。最終咱們導出去的接口,就是module.exports屬性。

加載規則,require方法

require命令的基本功能是,讀入並執行一個JavaScript文件,而後返回該模塊的exports對象。若是沒有發現指定模塊,會報錯。

require命令是CommonJS規範之中,用來加載其餘模塊的命令。它其實不是一個全局命令,而是指向當前模塊的module.require命令,然後者又調用Node的內部命令Module._load

  • require(): 加載外部模塊
  • require.resolve():將模塊名解析到一個絕對路徑
  • require.main:指向主模塊
  • require.cache:指向全部緩存的模塊
  • require.extensions:根據文件的後綴名,調用不一樣的執行函數

require命令用於加載文件,後綴名默認爲.js。

var foo = require('foo');
//  等同於
var foo = require('foo.js');

而這種方式的引入(不是絕對路徑,且不是相對路徑),將會以以下規則進行搜索加載;

/usr/local/lib/node/foo.js
/home/user/projects/node_modules/foo.js
/home/user/node_modules/foo.js
/home/node_modules/foo.js
/node_modules/foo.js

也就是說,將會先搜索默認的核心模塊(node),再層級往上找node_modules中的當前模塊。這樣使得不一樣的模塊能夠將所依賴的模塊本地化。

而若是是一個:

require('example-module/path/to/file')
  1. 則將先找到example-module的位置,而後再以它爲參數,找到後續路徑。
  2. 查找是否有file文件夾

    • 若找到,則嘗試找package.json,並以其main屬性指定的目錄做爲入口文件,不然便以當前目錄下的index.js | index.node做爲入口文件
    • 若未找到,則Node會嘗試爲文件名添加.js.json.node後,再去搜索。.js件會以文本格式的JavaScript腳本文件解析,.json文件會以JSON格式的文本文件解析,.node文件會以編譯後的二進制文件解析。
  3. 若尚未發現發現,則報錯。

第一次加載某個模塊時,Node會緩存該模塊。之後再加載該模塊,就直接從緩存取出該模塊的module.exports屬性。

CommonJS模塊載入方式

CommonJS規範加載模塊是同步的,也就是說,只有加載完成,才能執行後面的操做。因此通常來講,CommonJS規範不適用於瀏覽器環境。然而,對服務器端不是一個問題,由於全部的模塊都存放在本地硬盤,能夠同步加載完成,等待時間就是硬盤的讀取時間。可是,對於瀏覽器,這倒是一個大問題,由於模塊都放在服務器端,等待時間取決於網速的快慢,可能要等很長時間,瀏覽器處於"假死"狀態。

所以,瀏覽器端的模塊,不能採用"同步加載"(synchronous),只能採用"異步加載"(asynchronous)。這就是AMD規範誕生的背景。

下一章將會給你們講一下AMD規範。

參考文章

相關文章
相關標籤/搜索