天天看點

Javascript子產品化詳解解釋成 ES6 子產品

前端的發展日新月異,前端工程的複雜度也不可同日而語。原始的開發方式,随着項目複雜度提高,代碼量越來越多,所需加載的檔案也越來越多,這個時候就需要考慮如下幾個問題:

命名問題:所有檔案的方法都挂載到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 是異步的,适合前端

繼續閱讀