在JavaScript中,子產品是把實作某個特定功能的代碼放在一起并包裝,實作解耦和複用。
在ES6之前,ECMA Script并不包含子產品的概念,自然也沒有子產品文法,但的确有一些通用的模式來建立子產品,比較流行的就是CommonJS,AMD以及CMD。在ES6中,引入了子產品的文法。
本文講按照時間順序,依次介紹上述幾種子產品。
最早的子產品模式
在沒有CommonJS、AMD之前,開發者也會想一些辦法,将工程中的代碼進行解耦和封裝。比如使用IIFE來封裝子產品,如下:
var user = function(){
var status = 'logout'; //私有變量
var name = 'zhang san'; //私有變量
function generateUserAvatar() { // 私有函數
// logic code
return 'avatar icon';
}
return { // 暴露給外部的函數或對象
getUserInfo: function() {
console.log("UserName is " + name + ";" + generateUserAvatar());
},
checkUserLoginStatus: function() {
return status;
}
}
}();
user.getUserInfo(); // UserName is zhangsan
user.checkUserLoginStatus(); // logout
如上面的代碼所示,IIFE可以建立一個封閉的作用域,形成一個子產品,内部的變量和函數都是私有的,外部不可通路,這樣就可以把子產品自身的一些邏輯封裝在IIFE内部。IIFE return的對象是暴露出來可以被其他子產品通路的部分,進而實作了子產品間一些邏輯的共享。
CommonJS
在SPA(單頁面應用)出現之前,一個web應用由很多單獨的頁面組成,每個頁面的邏輯都相對簡單,使用一些簡單的子產品建立方法就可以很好的建構這些頁面的邏輯了。直到JavaScript被應用到伺服器端,事情發生了變化。在伺服器端,邏輯就複雜多了,也不能像前端一樣各個頁面去分擔不同的功能,是以必須要有一種子產品化的方式來管理代碼。NodeJS選擇了CommonJS作為它的子產品化的規範。
CommonJS使用exports導出需要被其他子產品使用的對象;使用require導入需要使用的被其他子產品暴露出來的對象。
下面是一個CommonJS的示例:
var todoList= {
showList: function() {
var todoItem = require('todoItem'); // 同步的子產品加載方式
if (todoItem.needShowTime) {
var todoTime = require('todoTime');
todoTime.showTime();
}
console.log('showlist');
}
};
exports.todoList = todoList;
CommonJS是一種同步的子產品加載方式。在執行require(‘todoItem’)的時候,會一直等到todoItem被加載完,才會執行後面的代碼。這在伺服器端是可行的,因為需要require的資源在本地,是以擷取資源并不會有太大的時間消耗。但當開發者想把這種模式應用于前端的時候,發現同步加載不是一個好辦法。
在加載require(‘todoItem’)的時候,需要去遠端的伺服器上擷取子產品,在同步的情況下,擷取子產品的過程中,不能執行其他任何操作,就會造成頁面的假死,影響使用者體驗。
AMD
在CommonJS之後,為了建立适合前端的子產品化規範,就有了AMD (異步子產品定義)。AMD是一種可以進行異步加載的子產品化規範,因而它很适用于前端開發。
AMD使用define定義子產品;使用require加載依賴。下面的代碼實作了CommonJS的示例中的相同功能。
define('todoList', ['todoItem'], (todoItem) => {
var todoList= {
showList: function() {
if (todoItem.showtime) {
require(['todoTime'], (todoTime) => {
todoTime.showTime();
});
console.log('showlist');
}
}
};
return todoList;
})
AMD與CommonJS最大的不同展現在require上。在CommonJS中,require方法隻有一個參數,就是需要被require的module,而在AMD中,require方法有兩個參數,一個是被require的module,一個是callback函數。
AMD在require的子產品加載完成後,會調用callback方法。而在擷取require的子產品的過程中,是可以繼續執行後面的代碼的,如console.log(‘showlist’);,這樣頁面就可以繼續響應使用者的其他操作,這就是AMD異步的加載方式所帶來的好處。常用的RequireJS就是這樣的一種機制,而AMD是RequireJS在推廣過程中對子產品定義的規範化産出。
// CommonJS 同步加載子產品
var todoTime = require('todoTime');
todoTime.showTime();
// AMD 異步加載子產品
require(['todoTime'], (todoTime) => {
todoTime.showTime();
});
CMD
CMD是SeaJS(來自于淘寶前端團隊)在推廣過程中對規範化定義的産出。
CMD和AMD一樣,都是異步加載子產品的規範。當讨論到CMD和AMD的不同時,通常會說AMD是依賴前置,而CMD是依賴就近。下面代碼是來自玉伯(SeaJS的作者)在知乎上的解答
// AMD 預設推薦的是
define(['./a', './b'], function(a, b) { // 依賴必須一開始就寫好
a.doSomething()
// 此處略去 100 行
b.doSomething()
//...
})
// CMD
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
// 此處略去 100 行
var b = require('./b') // 依賴可以就近書寫
b.doSomething()
// ...
})
事實上,AMD在後來也實作了依賴就近,文中在講述AMD時所給的示例,就可以算是一種依賴就近,隻是AMD的官方還是比較推薦依賴前置這種寫法。
ES6中的子產品
ES6新增了子產品相關的文法,使得構模組化塊得到了原生的文法支援。它使用export關鍵字來導出子產品中需要暴露出來的變量或函數;使用import來導入子產品的依賴。
下面的示例簡單的示範了ES6的子產品文法。
// user.js
const old = 20; // 内部變量
export const name = 'Jeo Snow'; // 導出的變量
export function setName(newName) { // 導出的函數
name = newName;
}
// profile.js
import {name, setName} from './user.js' // 導入子產品
setName('li si');
console.log(name); // li si
ES6隻規定了子產品的文法,并沒有規定子產品的加載方式,它希望ES的不同宿主(伺服器或者浏覽器),可以根據自身的特點去設計加載方式。
Web浏覽器的子產品加載機制展現在HTML規範中。HTML通過script标簽,可以加載腳本檔案,或者一段腳本代碼,将script标簽的type屬性設定為module,浏覽器就會将這段腳本了解為是一個子產品。如下:
<script type="module" src="module1.js"></script>
<script type="module">
import {name, setName} from './user.js'
setName('li si');
export function dosomething() { // dosomething};
</script>
當script标簽被設定為type="module"時,将會自動應用defer屬性。具體來說,在浏覽器解析HTML的過程中,一旦遇到帶有src屬性的
<script type="mudule">
,就開始下載下傳。每個子產品在加載的時候,都必須要遞歸的加載完所有的依賴子產品,才算子產品加載完成。當所有文檔都解析完畢後,再順序執行各個子產品。
是以,上面的代碼的加載如下:
- 下載下傳并解析module1.js。
- 遞歸下載下傳并解析。
- 解析内聯子產品。
- 遞歸下載下傳并解析内聯子產品中導入的資源。
執行過程會等到整個文檔被解析完畢後,如下:
- 遞歸執行module1.js中導入的資源。
- 執行module1.js。
- 遞歸執行内聯子產品中導入的資源。
-
執行内聯子產品。
注:當
帶有async屬性的時候,加載完成後立即執行。<script type="mudule">
【完】