深圳Web前端培訓學習:js中的模組化--【千鋒】
深圳 Web 前端培訓學習: js 中的模組化 --【千鋒】
0. 前言
我們知道最常見的模組化方案有CommonJS 、 AMD 、 CMD 、 ES6 , AMD 規範一般用於瀏覽器,非同步的,因為模組載入是非同步的, js 解釋是同步的,所以有時候導致依賴還沒載入完畢,同步的程式碼執行結束; CommonJS 規範一般用於服務端,同步的,因為在伺服器端所有檔案都儲存在本地的硬碟上,傳輸速率快而且穩定。
1.script 標籤引入
最開始的時候,多個script 標籤引入 js 檔案。但是,這種弊端也很明顯,很多個 js 檔案合併起來,也是相當於一個 script ,造成變數汙染。專案大了,不想變數汙染也是很難或者不容易做到,開發和維護成本高。 而且對於標籤的順序,也是需要考慮一陣,還有載入的時候同步,更加是一種災難,幸好後來有了渲染完執行的 defer 和下載完執行的 async ,進入新的時代了。
接著,就有各種各樣的動態建立script 標籤的方法,最終發展到了上面的幾種方案。
2.AMD 與 CMD
2.1AMD
非同步模組定義,提供定義模組及非同步載入該模組依賴的機制。AMD 遵循依賴前置,程式碼在一旦執行到需要依賴的地方,就馬上知道依賴是什麼。而無需遍歷整個函式體找到它的依賴,因此效能有所提升。但是開發者必須先前知道依賴具體有什麼,並且顯式指明依賴,使得開發工作量變大。而且,不能保證模組載入的時候的順序。 典型代表 requirejs 。 require.js 在宣告依賴的模組時會立刻載入並執行模組內的程式碼。 require 函式讓你能夠隨時去依賴一個模組,即取得模組的引用,從而即使模組沒有作為引數定義,也能夠被使用。他的風格是依賴注入,比如:
/api.js
define('myMoudle',['foo','bar'],function(foo,bar){
// 引入了 foo 和 bar ,利用 foo 、 bar 來做一些事情
return {
baz:function(){return 'api'}
}
});
require(['api'],function(api) {
console.log(api.baz())
})
複製程式碼
然後你可以在中間隨時引用模組,但是模組第一次初始化的時間比較長。這就像開始的時候很拼搏很辛苦,到最後是美滋滋。
2.2CMD
通用模組定義,提供模組定義及按需執行模組。遵循依賴就近,程式碼在執行時,最開始的時候是不知道依賴的,需要遍歷所有的require 關鍵字,找出後面的依賴。一個常見的做法是將 function toString 後,用正則匹配出 require 關鍵字後面的依賴。 CMD 裡,每個 API 都簡單純粹。可以讓瀏覽器的模組程式碼像 node 一樣,因為同步所以引入的順序是能控制的。 對於典型代表 seajs ,一般是這樣子:
define(function(require,exports,module){
//... 很多程式碼略過
var a = require('./a');
// 要用到 a ,於是引入了 a
// 做一些和模組 a 有關的事情
});
複製程式碼
對於b.js 依賴 a.js
//a.js
define(function(require, exports) {
exports.a = function(){// 也可以把他暴露出去
// 很多程式碼
};
});
//b.js
define(function(require,exports){
// 前面幹了很多事情,突然想要引用 a 了
var fun = require('./a');
console.log(fun.a()); // 就可以呼叫到及執行 a 函式了。
})
// 或者可以 use
seajs.use(['a.js'], function(a){
// 做一些事情
});
複製程式碼
AMD 和 CMD 對比: AMD 推崇依賴前置、提前執行, CMD 推崇依賴就近、延遲執行。
AMD 需要先列出清單,後面使用的時候隨便使用(依賴前置),非同步,特別適合瀏覽器環境下使用(底層其實就是動態建立 script 標籤)。而且 API 預設是一個當多個用。
CMD 不需要知道依賴是什麼,到了改需要的時候才引入,而且是同步的,就像臨時抱佛腳一樣。
對於客戶端的瀏覽器,一說到下載、載入,肯定就是和非同步脫不了關係了,註定瀏覽器一般用AMD 更好了。但是, CMD 的 api 都是有區分的,區域性的 require 和全域性的 require 不一樣。
3.CommonJS 與 ES6
3.1 ES6
ES6 模組的 script 標籤有點不同,需要加上 type='module'
<script src='./a.js' type='module'>...</script>
複製程式碼
對於這種標籤都是非同步載入,而且是相當於帶上defer 屬性的 script 標籤,不會阻塞頁面,渲染完執行。但是你也可以手動加上 defer 或者 async ,實現期望的效果。 ES6 模組的檔案字尾是 mjs ,透過 import 引入和 export 匯出。我們一般是這樣子:
//a.mjs
import b from 'b.js'
//b.mjs
export default b
複製程式碼
ES6 畢竟是 ES6 ,模組內自帶嚴格模式,而且只在自身作用域內執行。在 ES6 模組內引入其他模組就要用 import 引入,暴露也要用 export 暴露。另外,一個模組只會被執行一次。 import 是 ES6 新語法,可靜態分析,提前編譯。他最終會被 js 引擎編譯,也就是可以實現編譯後就引入了模組,所以 ES6 模組載入是靜態化的,可以在編譯的時候確定模組的依賴關係以及輸入輸出的變數。 ES6 可以做到編譯前分析,而 CMD 和 AMD 都只能在執行時確定具體依賴是什麼。
3.2CommonJS
一般服務端的檔案都在本地的硬碟上面。對於客戶,他們用的瀏覽器是要從這裡下載檔案的,在服務端一般讀取檔案非常快,所以同步是不會有太大的問題。require 的時候,馬上將 require 的檔案程式碼執行
代表就是nodejs 了。用得最多的,大概就是:
//app.js
var route = require('./route.js')// 讀取控制路由的 js 檔案
//route.js
var route = {......}
module.exports = route
複製程式碼
require 第一次載入指令碼就會馬上執行指令碼,生成一個物件
區別: CommonJS 執行時載入,輸出的是值的複製,是一個物件(都是由 module.export 暴露出去的),可以直接拿去用了,不用再回頭找。所以,當 module.export 的原始檔裡面一些原始型別值發生變化, require 這邊不會隨著這個變化而變化的,因為被快取了。但是有一種常規的操作,寫一個返回那個值的函式。就像 angular 裡面 $watch 陣列裡面的每一個物件,舊值是直接寫死,新值是寫一個返回新值的函式,這樣子就不會寫死。 module.export 輸出一個取值的函式,呼叫的時候就可以拿到變化的值。
ES6 是編譯時輸出介面,輸出的是值的引用,對外的介面只是一種靜態的概念,在靜態解釋後已經形成。當指令碼執行時,根據這個引用去原本的模組內取值。所以不存在快取的情況, import 的檔案變了,誰發出 import 的也是拿到這個變的值。模組裡面的變數繫結著他所在的模組。另外,透過 import 引入的這個變數是隻讀的,試圖進行對他賦值將會報錯。
4. 迴圈依賴
就是a 依賴 b , b 依賴 a ,對於不同的規範也有不同的結果。
4.1CommonJS
對於node ,每一個模組的 exports={done:false} 表示一個模組有沒有載入完畢,經過一系列的載入最後全部都會變為 true 。 同步,從上到下,只輸出已經執行的那部分程式碼 首先,我們寫兩個 js 用 node 跑一下:
//a.js
console.log('a.js')
var b = require('./b.js')
console.log(1)
//b.js
console.log('b.js')
var a = require('./a.js')
console.log(2)
// 根據他的特點, require 一個檔案的時候,馬上執行內部的程式碼,所以相當於
console.log('a.js')
console.log('b.js')
console.log(2)
console.log(1)
// 輸出是 a.js 、 b.js 、 2 、 1
複製程式碼
加上export 的時候:
//a.js
module.exports = {val:1}
var b = require('./b.js')
console.log(b.val)
module.exports = {val:2}
b.val = 3
console.log(b)
//b.js
module.exports = {val:1}
var a = require('./a.js')
console.log(a.val)
module.exports = {val:2}
a.val = 3
console.log(a)
//1. 在 a.js 暴露出去一個物件 module.exports = {val:1}
//2.require 了 b ,來到 b ,執行 b 指令碼
//3.b 的第一行,把 {val:1} 暴露出去,引入剛剛 a 暴露的 {val:1} ,列印 a.val 的結果肯定是 1
//4. 重新暴露一次,是 {val:2} ,然後做了一件多餘的事情,改 a.val 為 3 (反正是複製過的了怎麼改都不會影響 a.js ),毫無疑問列印出 { val: 3 }
//5. 回到 a ,繼續第三行,列印 b.val ,因為 b 暴露的值是 2 ,列印 2
//6. 繼續再做一件無意義的事情,列印 { val: 3 }
複製程式碼
解決辦法:程式碼合理拆分
4.2ES6 模組
ES6 模組是輸出值的引用,是動態引用,等到要用的時候才用,因此可以完美實現相互依賴,在相互依賴的 a.mjs 和 b.mjs ,執行 a 的時候,當發現 import 馬上進入 b 並執行 b 的程式碼。當在 b 發現了 a 的時候,已經知道從 a 輸入了介面來到 b 的,不會回到 a 。但是在使用的過程中需要注意,變數的順序。
如果是單純的暴露一個基本資料型別,當然會報錯not defined 。 因為函式宣告會變數提升,所以我們可以改成函式宣告(不能用函式表示式)
//a.mjs
import b from './b'
console.log(b())
function a(){return 'a'}
export default a
//b.mjs
import a from './a'
console.log(a())
function b(){return 'b'}
export default b
複製程式碼
4.3 require
我們一般使用的時候,都是依賴注入,如果是有迴圈依賴,那麼可以直接利用require 解決
define('a',['b'],function(b){
//dosomething
});
define('b',['a'],function(a){
//dosomething
});
// 為了解決迴圈依賴,在迴圈依賴發生的時候,引入 require :
define('a',['b','require'],function(b,require){
//dosomething
require('b')
});
複製程式碼
4.4 sea
迴圈依賴,一般就是這樣
//a.js
define(function(require, exports, module){
var b = require('./b.js');
//......
});
//b.js
define(function(require, exports, module){
var a = require('./a.js');
//......
});
複製程式碼
而實際上,並沒有問題,因為sea 自己解決了這個問題: 一個模組有幾種狀態:
'FETCHING': 模組正在下載中 'FETCHED': 模組已下載 'SAVED': 模組資訊已儲存 'READY': 模組的依賴項都已下載,等待編譯 'COMPILING': 模組正在編譯中 'COMPILED': 模組已編譯
步驟:
1. 模組 a 下載並且下載完成 FETCHED
2. 編譯 a 模組(執行回撥函式)
3. 遇到了依賴 b , b 和自身沒有迴圈依賴, a 變成 SAVED
4. 模組 b 下載並且下載完成 FETCHED
5.b 遇到了依賴 a , a 是 SAVED ,和自身有迴圈依賴, b 變成 READY ,編譯完成後變成 COMPILED
6. 繼續回到 a ,執行剩下的程式碼,如果有其他依賴繼續重複上面步驟,如果所有的依賴都是 READY , a 變成 READY
7. 繼續編譯,當 a 回撥函式部分所有的程式碼執行完畢, a 變成 COMPILED
對於所有的模組相互依賴的通用的辦法,將相互依賴的部分抽取出來,放在一箇中介軟體,利用釋出訂閱模式解決
5.webpack 是如何處理模組化的
假設我們定義兩個js : app.js 是主入口檔案, a.js 、 b.js 是 app 依賴檔案,用的是 COMMONJS 規範 webpack 首先會從入口模組 app.js 開始,根據引入方法 require 把所有的模組都讀取,然後寫在一個列表上:
var modules = {
'./b.js': generated_b,
'./a.js': generated_a,
'./app.js': generated_app
}
複製程式碼
'generated_'+name 是一個 IIFE ,每個模組的原始碼都在裡面,不會暴露內部的變數。比如對於沒有依賴其他模組的 a.js 一般是這樣,沒有變化:
function generated_a(module, exports, webpack_require) {
// ...a 的全部程式碼
}
複製程式碼
對於app.js 則不一樣了:
function generated_app(module, exports, webpack_require) {
var a_imported_module = __webpack_require__('./a.js');
var b_imported_module = __webpack_require__('./b.js');
a_imported_module['inc']();
b_imported_module['inc']();
}
複製程式碼
webpack_require 就是 require 、 exports 、 import 這些的具體實現,夠動態地載入模組 a 、 b ,並且將結果返回給 app
對於webpack_require ,大概是這樣的流程
var installedModules = {};// 儲存已經載入完成的模組
function webpack_require(moduleId) {
if (installedModules[moduleId]) {// 如果已經載入完成直接返回
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {// 如果是第一次載入,則記錄在表上
i: moduleId,
l: false,// 沒有下載完成
exports: {}
};
// 在模組清單上面讀取對應的路徑所對應的檔案,將模組函式的呼叫物件繫結為 module.exports ,並返回
modules[moduleId].call(module.exports, module, module.exports,__webpack_require__);
module.l = true;// 下載完成
return module.exports;
}
複製程式碼
對於webpack 打包後的檔案,是一個龐大的 IIFE ,他的內容大概是這樣子:
(function(modules) {
var installedModules = {};
function __webpack_require__(moduleId) { /*...*/}
__webpack_require__.m = modules;// 所有的檔案依賴列表
__webpack_require__.c = installedModules;// 已經下載完成的列表
__webpack_require__.d = function(exports, name, getter) {// 定義模組物件的 getter 函式
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
configurable: false,
enumerable: true,
get: getter
});
}
};
__webpack_require__.n = function(module) {// 當和 ES6 模組混用的時候的處理
var getter = module && module.__esModule ?// 如果是 ES6 模組用 module.default
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };// 是 COMMONJS 則繼續用 module
__webpack_require__.d(getter, 'a', getter);
return getter;
};
__webpack_require__.o = function(object, property) { // 判斷是否有某種屬性(如 exports )
return Object.prototype.hasOwnProperty.call(object, property);
};
__webpack_require__.p = "";// 預設路徑為當前
return __webpack_require__(__webpack_require__.s = 0);// 讀取第一個模組
})
/************************************************************************/
//IIFE 第二個括號部分
([
(function(module, exports, __webpack_require__) {
var a = __webpack_require__(1);
var b = __webpack_require__(2);
// 模組 app 程式碼
}),
(function(module, exports, __webpack_require__) {
// 模組 a 程式碼
module.exports = ...
}),
(function(module, exports, __webpack_require__) {
// 模組 b 程式碼
module.exports = ...
})
]);
複製程式碼
如果是ES6 模組,處理的方法也不一樣。還是假設我們定義兩個 js : app.js 是主入口檔案, a.js 、 b.js 是 app 依賴檔案。
(function(modules) {
// 前面這段是一樣的
})
([
(function(module, __webpack_exports__, __webpack_require__) {// 入口模組
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
var __WEBPACK_IMPORTED_MODULE_0__m__ = __webpack_require__(1);
var __WEBPACK_IMPORTED_MODULE_1__m__ = __webpack_require__(2);
Object(__WEBPACK_IMPORTED_MODULE_0__m__["a"])();// 用 object 包裹著,使得其他模組 export 的內容即使是基本資料型別,也要讓他變成一個引用型別
Object(__WEBPACK_IMPORTED_MODULE_1__m__["b"])();
}),
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_exports__["a"] = a;// 也就是 export xxx
//....
}),
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_exports__["b"] = b;
//....
})
]);
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69947096/viewspace-2661290/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 深圳Web前端學習:學 Web 前端開發,培訓還是自學靠譜?-千鋒Web前端
- 深圳Web前端培訓學習:5G對Web前端發展的影響--【千鋒】Web前端
- 深圳Java培訓學習:MyBatis Plus 介紹--【千鋒】JavaMyBatis
- 深圳雲端計算培訓學習:Apache 訪問控制--【千鋒】Apache
- 深圳Python培訓學習:Python3 簡介–[千鋒]Python
- 深圳軟體測試培訓學習:Java Random介紹--【千鋒】Javarandom
- 深圳java培訓:構建xml文件--【千鋒】JavaXML
- 深圳軟體測試培訓學習:Java連線MySQL--【千鋒】JavaMySql
- 深圳軟體測試培訓學習:Android常用自動化測試工具【千鋒】Android
- 深圳雲端計算培訓學習:部署網校系統 edusoho--【千鋒】
- 深圳Web前端學習:如何給網頁劃分合適的結構--【千鋒】Web前端網頁
- 深圳雲端計算培訓學習:雲端計算正在殺死運維嗎?–【千鋒】運維
- 深圳雲端計算培訓學習:女生做雲端計算運維容易嗎?–【千鋒】運維
- 深圳Web前端學習:前端工程師到底要不要學習演算法知識?--【千鋒】Web前端工程師演算法
- 深圳雲端計算培訓學習:構建企業級WIKI及工單系統 --【千鋒】
- web前端培訓學習技巧有哪些Web前端
- 長沙Web前端培訓分享:Web前端學習路線Web前端
- 初學者必看Web前端學習路線圖-千鋒Web前端教學出品Web前端
- 千鋒長沙前端培訓:Vue相關面試題前端Vue面試題
- 深圳大資料學習:泛型--【千鋒】大資料泛型
- web前端培訓分享node學習筆記Web前端筆記
- 深圳Java培訓:MyBatis為什麼在國內相當流行?【千鋒】JavaMyBatis
- 深圳大資料學習:方法的巢狀--【千鋒】大資料巢狀
- Web前端學習路線資料彙總,Web前端培訓學校Web前端
- web前端培訓要學多久Web前端
- 深圳大資料學習:高階函式--【千鋒】大資料函式
- 千鋒長沙前端培訓:Vue的雙向資料繫結原理前端Vue
- web前端培訓需要學多久呢Web前端
- 學習web前端培訓就業前景怎麼樣?Web前端就業
- web前端開發培訓有哪些學習階段Web前端
- 前端需要學習什麼?長沙web前端培訓班學費多少?前端Web
- 運維工程師怎樣才能更好的進階?-千鋒深圳雲端計算培訓運維工程師
- 深圳Java學習:小白速懂Https協議-千鋒JavaHTTP協議
- 長沙web前端培訓班學費多少?長沙培訓前端多少錢?Web前端
- 好程式設計師web前端培訓分享學習JavaScript程式設計師Web前端JavaScript
- 千鋒長沙前端培訓:VUE-router導航守衛講解前端Vue
- 大資料和雲端計算的關係是什麼?-千鋒深圳雲端計算培訓大資料
- 千鋒教育長沙Java培訓怎麼樣?Java