前端日拱一卒D8——模組化

DerekZ95發表於2018-07-27

前言

餘為前端菜鳥,感姿勢水平匱乏,難觀前端之大局。遂決定循前端知識之脈絡,以興趣為引,輔以幾分堅持,望於己能解惑致知、於同道能助力一二,豈不美哉。

本系列程式碼及文件均在 此處

隨著前端專案體積不斷變大,模組化的問題變得尤為重要,如何以模組的形式組織好檔案,如何解決全域性汙染的問題,前人已有所應對。

CommonJS

Node的模組載入

Node的模組分為兩類,原生模組和檔案模組,原生模組在node原始碼編譯時被編譯進了二進位制檔案,載入速度最快。檔案模組則是動態載入,速度較慢。node對於模組進行了快取,二次require時不會有重複開銷。

github 原始碼片段

  • node啟動時原生模組Module已載入,執行入口檔案時,呼叫Module._load方法

    Module.runMain = function () {
      // node index.js 主檔案模組載入
      Module._load(process.argv[1], null, true);
    };
    複製程式碼
  • _load分析檔名後,new一個module,並對入口檔案模組進行快取,呼叫load方法根據檔案字尾選擇不同的載入方式

    var module = new Module(id, parent)
    
    Module._cache[filename] = module
    
    module.load(filename);
    複製程式碼
  • 載入模組,呼叫_compile方法,對模組內容進行wrap

    // wrap做的事情
    Module.wrap = function(script) {
      return Module.wrapper[0] + script + Module.wrapper[1];
    };
    Module.wrapper = [
      '(function (exports, require, module, __filename, __dirname) { ',
      '\n});'
    ];
    複製程式碼
  • 包裹後返回的函式傳入module物件的require,exports方法和module及檔案、目錄引數執行(這就是為什麼我們可以隨意用require和module.exports)

    Module.prototype._compile = function(content, filename) {
      // content為檔案內容
      var wrapper = Module.wrap(content);
    
      // vm.runInThisContext類似於eval,但是有明確的上下文,返回一個function
      var compiledWrapper = vm.runInThisContext(wrapper, {
        filename: filename,
        lineOffset: 0,
        displayErrors: true
      });
      // 返回
      return compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname)
    }
    複製程式碼
  • 我們在入口檔案內用來引入別的檔案模組的require

    Module.prototype.require = function(id) {
      return Module._load(id, this, /* isMain */ false);
    };
    複製程式碼
  • 然後就回到第二步啦,繼續愉快地去載入別的模組吧~

require檔案查詢策略

  • if in 檔案模組快取區 end else goto 2
  • if 原生模組 goto 3 else goto 4
  • if in 原生模組快取區 end else goto 4
  • find 載入原生/檔案模組 goto 5
  • cache 模組 end

module path

  • module path指的是根據require時傳入的值計算出來的檔案模組的位置

    // const a = require(module_path)
    console.log(module.paths)
    [ '/Users/derekely/derek/dz/dz-fe/frontend/basic/html/node_modules',
    '/Users/derekely/derek/dz/dz-fe/frontend/basic/node_modules',
    '/Users/derekely/derek/dz/dz-fe/frontend/node_modules',
    '/Users/derekely/derek/dz/dz-fe/node_modules',
    '/Users/derekely/derek/dz/node_modules',
    '/Users/derekely/derek/node_modules',
    '/Users/derekely/node_modules',
    '/Users/node_modules',
    '/node_modules' ]
    複製程式碼

    簡而言之,如果是絕對路徑不會按照層級一個個查,否則要按照層級結合快取進行查詢(package.json裡的main欄位可以指定檔案,方便查詢)

    emmmm這裡的過程其實還不太簡單,但是沒興趣瞭解了,以後再說

包結構

  • 頂級目錄下的package.json
  • 二進位制檔案在bin目錄下,js檔案在lib目錄下
  • 文件在doc目錄下,測試在test目錄下

簡單評價

CommonJS規範算是較早的js模組化規範,且node的模組化實現得比較好,模組相互獨立不彼此影響,引入方式又簡單便捷,可以說是很厲害了。

AMD

AMD(Asynchronous Module Definition) 非同步模組定義

剛剛說完的CommonJS規範下的模組載入是同步的,對於Node來說檔案存在硬碟裡是足夠的,但對於前端賴以生存的瀏覽器來說,js通過標籤引入,通過請求獲取,天生非同步,用CommonJS規範是會有問題的。

RequireJS

RequireJS是實現了AMD規範的工具庫,是非同步的模組載入解決方案,主要用於客戶端的模組管理。

// data-main 指定主程式碼所在指令碼
<script data-main="scripts/main" src="scripts/require.js"></script>
複製程式碼

define(id, [deps], cb)定義模組

  • 獨立模組
    define(() => {
      // 返回不侷限
      return {
        method: function() {}
      }
    })
    複製程式碼
  • 非獨立模組
    define(['a', 'b'], (a, b) => {
      // 該函式引數為依賴的模組
      // 必須返回一個物件
      return {
        methodA: a.method,
        methodB: b.method
      }
    })
    // another 寫法
    define((require) => {
      var a = require('a')
      var b = require('b')
      return {
        methodA: a.method,
        methodB: b.method
      }
    })
    複製程式碼

require([moddule], cb, errcb)載入模組

  • 寫法和define很類似
    require(['a', 'b'], (a, b) => {
      return {
        method: a.method
      }
    })
    複製程式碼
  • 動態載入
    // 在定義模組時使用require
    define(( require ) => {
      const isReady = false
      // 載入完a以後改變isReady值
      require(['a'], (a) => {
          isReady = true
      })
      return {
        isReady: isReady,
        a: a
      }
    })
    複製程式碼

RequireJS配置

簡單評價

可以滿足瀏覽器模組載入的需要,不像很久以前先後依賴順序沒法保證,而且可以並行載入多個模組,但是需要在使用前提前載入所有模組,不是很優雅。

CMD

CMD(Common Module Definition) 通用模組定義 由阿里的大佬玉伯提出,對應的瀏覽器端實現庫為大佬的sea.js

sea.js

sea.js與requirejs要做的事情其實是一致的,但兩者在模組定義方式和載入時機上有所區別

define

相比於AMD在定義時將依賴寫在前面,由於回撥函式引數為依賴,可以理解為提前宣告的模組被提前載入了

CMD選擇就近宣告,在需要的時候再載入

// cmd定義模組
define((require, exports, module) => {
  const a = require('a')
  const b = a.method()
  // exports匯出
  exports.r = b;
})
複製程式碼

use

seajs.use(['a.js'], (my) => {
  console.log(my.r)
})
複製程式碼

簡單評價

與RequireJS相比,依賴就近,需要時再載入,看起來更優雅一些。

ES6 Module

每個ES6模組是一個單獨的檔案,程式碼執行於嚴格模式和模組作用域內,可以使用importexport關鍵字。

目的

ES6的模組化設計的目的之一是為了靜態化,能夠在編譯時確定模組的依賴關係,從而方便進行程式碼分析、tree shaking優化。

此外,ES6的Module,旨在提供一種有別於社群的能夠同時滿足客戶端和服務端js模組化需要的官方標準,目前確實已經比較方便了。

語法

  • export

    // 定義模組的對外介面
    const a = 1
    const m = 2
    export c = 3
    export { a as b, m as n }
    // 指定預設輸出
    // 等價於 export { default as a },所以default後不能為宣告語句
    export default a = 1
    // 等價於給default賦值4
    export default 4
    複製程式碼
  • import

    import在編譯時執行,會被提升到模組頂部

    import { b as c } from './a.js'
    // 整體載入
    import * from './a.js'
    // a.js預設輸出a時不需要大括號
    import a from './a.js'
    複製程式碼

    由於是編譯時執行,無法做到在執行時根據執行結果動態載入(有提案import()用於執行時載入,這個且略過)

ES6 VS CommonJS

  • 前者為編譯時輸出介面,後者為執行時載入
  • 前者輸出的只讀引用,指令碼執行時需要找到模組取值,而後者是在載入完後輸出一個值的拷貝,是一個實際的物件

Node載入ES6模組

雖發表於此,卻畢竟為一人之言,又是每日學有所得之筆記,內容未必詳實,看官老爺們還望海涵。

相關文章