前端的發展日新月異,前端工程的複雜度也不可同日而語。原始的開發方式,随着項目複雜度提高,代碼量越來越多,所需加載的檔案也越來越多,這個時候就需要考慮如下幾個問題:
命名問題:所有檔案的方法都挂載到window/global上,會污染全局環境,并且需要考慮命名沖突問題
依賴問題:script是順序加載的,如果各個檔案檔案有依賴,就得考慮js檔案的加載順序
網絡問題:如果js檔案過多,所需請求次數就會增多,增加加載時間
Javascript子產品化程式設計,已經成為一個迫切的需求。理想情況下,開發者隻需要實作核心的業務邏輯,其他都可以加載别人已經寫好的子產品。
本文主要介紹Javascript子產品化的4種規範: CommonJS、AMD、UMD、ESM。
CommonJS
CommonJS是一個更偏向于伺服器端的規範。NodeJS采用了這個規範。CommonJS的一個子產品就是一個腳本檔案。require指令第一次加載該腳本時就會執行整個腳本,然後在記憶體中生成一個對象。
{
id: '...',
exports: { ... },
loaded: true,
...
}
id是子產品名,exports是該子產品導出的接口,loaded表示子產品是否加載完畢。
以後需要用到這個子產品時,就會到exports屬性上取值。即使再次執行require指令,也不會再次執行該子產品,而是到緩存中取值。
// utile.js
const util = {
name:'Clearlove'
sayHello:function () {
return 'Hello I am Clearlove';
// exports 是指向module.exports的一個快捷方式
module.exports = util
// 或者
exports.name = util.name;
exports.sayHello = util.sayHello;
const selfUtil = require('./util');
selfUtil.name;
selfUtil.sayHello();
CommonJS是同步導入子產品
CommonJS導入時,它會給你一個導入對象的副本
CommonJS子產品不能直接在浏覽器中運作,需要進行轉換、打包
由于CommonJS是同步加載子產品,這對于伺服器端不是一個問題,因為所有的子產品都放在本地硬碟。等待子產品時間就是硬碟讀取檔案時間,很小。但是,對于浏覽器而言,它需要從伺服器加載子產品,涉及到網速,代理等原因,一旦等待時間過長,浏覽器處于”假死”狀态。
是以在浏覽器端,不适合于CommonJS規範。是以在浏覽器端又出現了一個規範—-AMD。
AMD
AMD(Asynchronous Module Definition - 異步加載子產品定義)規範,一個單獨的檔案就是一個子產品。它采用異步方式加載子產品,子產品的加載不影響它後面語句的運作。
這裡異步指的是不堵塞浏覽器其他任務(www.taobaoosx.com,dom建構,css渲染等),而加載内部是同步的(加載完子產品後立即執行回調)。
AMD也采用require指令加載子產品,但是不同于CommonJS,它要求兩個參數:
require([module], callback);
第一個參數[module],是一個數組,裡面的成員是要加載的子產品,callback是加載完成後的回調函數,回調函數中參數對應數組中的成員(子產品)。
AMD的标準中,引入子產品需要用到方法require,由于window對象上沒定義require方法, 這裡就不得不提到一個庫,那就是RequireJS。
官網介紹RequireJS是一個js檔案和子產品的加載器,提供了加載和定義子產品的api,當在頁面中引入了RequireJS之後,我們便能夠在全局調用define和require。
define(id?, dependencies?, factory);
id:子產品的名字,如果沒有提供該參數,子產品的名字應該預設為子產品加載器請求的指定腳本的名字
dependencies:子產品的依賴,已被子產品定義的子產品辨別的數組字面量。依賴參數是可選的,如果忽略此參數,它應該預設為 ["require", "exports", "module"]。然而,如果工廠方法的長度屬性小于3,加載器會選擇以函數的長度屬性指定的參數個數調用工廠方法。
factory:子產品的工廠函數,子產品初始化要執行的函數或對象。如果為函數,它應該隻被執行一次。如果是對象,此對象應該為子產品的輸出值。
// 定義一個moduleA.js
define(function(){
const name = "module A";
return {
getName(){
return name
});
// 定義一個moduleB.js
define(["moduleA"], function(moduleA){
showFirstModuleName(){
console.log(moduleA.getName());
// 實作main.js
require(["moduleB"], function(moduleB){
moduleB.showFirstModuleName();
<html>
<!-- 此處省略head -->
<body>
<!--引入requirejs并且在這裡指定入口檔案的位址-->
<script data-main="js/main.js" src="js/require.js"></script>
</body>
</html>
要通過script引入requirejs,然後需要為标簽加一個屬性data-main來指定入口檔案。
前面介紹用define來定義一個子產品的時候,直接傳“子產品名”似乎就能找到對應的檔案,這一塊是在哪實作的呢?其實在使用RequireJS之前還需要為它做一個配置:
// main.js
require.config({
paths: {
// key為子產品名稱, value為子產品的路徑
"moduleA": "./moduleA",
"moduleB": "./moduleB"
這個配置中的屬性paths隻寫子產品名就能找到對應路徑,不過這裡有一項要注意的是,路徑後面不能跟.js檔案字尾名,更多的配置項請參考RequireJS官網。
UMD
UMD 代表通用子產品定義(Universal Module Definition)。所謂的通用,就是相容了CmmonJS和AMD規範,這意味着無論是在CmmonJS規範的項目中,還是AMD規範的項目中,都可以直接引用UMD規範的子產品使用。
原理其實就是在子產品中去判斷全局是否存在exports和define,如果存在exports,那麼以CommonJS的方式暴露子產品,如果存在define那麼以AMD的方式暴露子產品:
(function (root, factory) {
if (typeof define === "function" && define.amd) {
define(["jquery", "underscore"], factory);
} else if (typeof exports === "object") {
module.exports = factory(require("jquery"), require("underscore"));
} else {
root.Requester = factory(root.$, root.);
}(this, function ($, ) {
// this is where I defined my module implementation
const Requester = { // ... };
return Requester;
}));
這種模式,通常會在webpack打包的時候用到。output.libraryTarget将子產品以哪種規範的檔案輸出。
ESM
在ECMAScript 2015版本出來之後,确定了一種新的子產品加載方式,我們稱之為ES6 Module。它和前幾種方式有差別和相同點:
它因為是标準,是以未來很多浏覽器會支援,可以很友善的在浏覽器中使用
它同時相容在node環境下運作
子產品的導入導出,通過import和export來确定
可以和CommonJS子產品混合使用
CommonJS輸出的是一個值的拷貝。ES6子產品輸出的是值的引用,加載的時候會做靜态優化
CommonJS子產品是運作時加載确定輸出接口,ES6子產品是編譯時确定輸出接口
ES6子產品功能主要由兩個指令構成:import和export。import指令用于輸入其他子產品提供的功能。export指令用于規範子產品的對外接口。
export的幾種用法:
// 輸出變量
export const name = 'Clearlove';
export const year = '2021';
// 輸出一個對象(推薦)
const name = 'Clearlove';
const year = '2021';
export { name, year}
// 輸出函數或類
export function add(a, b) {
return a + b;
// export default 指令
export default function() {
console.log('foo')
import導入子產品:
// 正常指令
import { name, year } from './module.js';
// 如果遇到export default指令導出的子產品
import ed from './export-default.js';
子產品編輯好之後,它有兩種形式加載:
浏覽器加載
浏覽器加載ES6子產品,使用<script>标簽,但是要加入type="module"屬性。
外鍊js檔案:
<script type="module" src="index.js"></script>
内嵌在網頁中
<script type="module">
import utils from './utils.js';
// other code
</script>
對于加載外部子產品,需要注意:
代碼是在子產品作用域之中運作,而不是在全局作用域運作。子產品内部的頂層變量,外部不可見
子產品腳本自動采用嚴格模式,不管有沒有聲明use strict
子產品之中,可以使用import指令加載其他子產品(.js字尾不可省略,需要提供絕對 URL 或相對 URL),也可以使用export指令輸出對外接口
子產品之中,頂層的this關鍵字傳回undefined,而不是指向window。也就是說,在子產品頂層使用this關鍵字,是無意義的
同一個子產品如果加載多次,将隻執行一次
Node加載
Node要求 ES6 子產品采用.mjs字尾檔案名。也就是說,隻要腳本檔案裡面使用import或者export指令,就必須采用.mjs字尾名。Node.js 遇到.mjs檔案,就認為它是 ES6 子產品,預設啟用嚴格模式,不必在每個子產品檔案頂部指定use strict。
如果不希望将字尾名改成.mjs,可以在項目的package.json檔案中,指定type字段為
"type": "module"
一旦設定了以後,該目錄裡面的 JS 腳本,就被解釋用 ES6 子產品。
$ node my-app.js
如果這時還要使用 CommonJS 子產品,那麼需要将 CommonJS 腳本的字尾名都改成.cjs。如果沒有type字段,或者type字段為commonjs,則.js腳本會被解釋成 CommonJS 子產品。
總結為一句話:.mjs檔案總是以 ES6 子產品加載,.cjs檔案總是以 CommonJS 子產品加載,.js檔案的加載取決于package.json裡面type字段的設定。
注意,ES6 子產品與 CommonJS 子產品盡量不要混用。require指令不能加載.mjs檔案,會報錯,隻有import指令才可以加載.mjs檔案。反過來,.mjs檔案裡面也不能使用require指令,必須使用import。
Node的import指令隻支援異步加載本地子產品(file:協定),不支援加載遠端子產品。
總結
由于 ESM 具有簡單的文法,異步特性和可搖樹性,是以它是最好的子產品化方案
UMD 随處可見,通常在 ESM 不起作用的情況下用作備用
CommonJS 是同步的,适合後端
AMD 是異步的,适合前端