前言
餘為前端菜鳥,感姿勢水平匱乏,難觀前端之大局。遂決定循前端知識之脈絡,以興趣為引,輔以幾分堅持,望於己能解惑致知、於同道能助力一二,豈不美哉。
本系列程式碼及文件均在 此處
隨著前端專案體積不斷變大,模組化的問題變得尤為重要,如何以模組的形式組織好檔案,如何解決全域性汙染的問題,前人已有所應對。
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模組是一個單獨的檔案,程式碼執行於嚴格模式和模組作用域內,可以使用import
和export
關鍵字。
目的
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模組
雖發表於此,卻畢竟為一人之言,又是每日學有所得之筆記,內容未必詳實,看官老爺們還望海涵。