寫在前面
模塊化開發是我們日常作業潛移默化中用到的基本技能,發展至今非常地簡潔方便,但開發者們(指我自己)卻很少能清晰透徹地說出它的發展背景, 發展程序以及各個規范之間的區別,故筆者決定一探乾坤,深入淺出學習一下什么是前端模塊化,
通過本文,筆者希望各位能夠識訓到:
- 前端模塊化發展的大致歷史背景 ??
- 各個規范之間的基本特性和區別 ????
- 著重深入 ESM 和 CommonJs 的異同、優缺點 ??????
- 深耕 CommonJS 和 ESM 的特性 ????????
本文的重點會以大家熟知的 CommonJS 和 ESM 入手,深入淺出,結合示例 Demo 和一些小故事,希望給大家能夠帶到不一樣的體驗,
一、前端模塊化背景
某個技術的起源幾乎都是為了解決一些棘手的問題,模塊化也不例外,下面以一個簡單的例子來給大家講個故事,通過故事給大家講一講大致的發展史,故事并未涵蓋所有時間線上發生的事件,眾所周知在前端模塊化的長河里 AMD 和 CMD 一直打的不可開交,這里筆者挑選以 CMD 為支線向大家闡釋,
本故事的攥寫參考了部分 Sea.js 開源大佬發表在《程式員》雜志 2013 年 3 月刊的文章 (侵刪)
在線鏈接:前端模塊化開發的價值 ,本文推薦大家仔細閱讀,包括評論區,
故事開始! 在很久之前(可能就是2012年之前),JS 模塊化概念并未誕生的年代,前端開發們面臨諸多問題:Web 技術雖說日益成熟、JS 能實作的功能也愈發地多,但與此同時代碼量也是越來越大,那個年代往往會出現一個專案各個頁面公用同一個 JS 的情況,為了解決這個情況,JS 檔案出現了按功能拆分....
慢慢地,專案代碼變成了如下:
...
...
<script src="https://www.cnblogs.com/dtux/archive/2023/03/14/util/wxbridge.js"></script>
<script src="https://www.cnblogs.com/dtux/archive/2023/03/14/util/login.js"></script>
<script src="https://www.cnblogs.com/dtux/archive/2023/03/14/util/base.js"></script>
<script src="https://www.cnblogs.com/dtux/archive/2023/03/14/util/auth.js"></script>
<script src="https://www.cnblogs.com/dtux/archive/2023/03/14/util/logout.js"></script>
<script src="https://www.cnblogs.com/dtux/archive/2023/03/14/util/pay.js"></script>
...
拆分出來的代碼類似于如下:
function mapList(list) {
// 具體實作
}
function canBuyIt(goodId) {
// 具體實作
}
看似拆分很細,但卻有諸多的致命問題:
- 全域變數污染:各個檔案的變數都是掛載到window物件上,污染全域變數;
- 變數可能重名:不同檔案中的變數如果重名,后一份會覆寫前面的,造成錯誤;
- 檔案依賴順序:多個檔案之間存在依賴關系,需要保證一定加載順序問題嚴重......
拿上述 util 工具函式檔案舉例! 大家按規范模像樣地把這些函式統一放在 util.js 里,需要用到時,直接引入該檔案就好,非常方便,隨著團隊專案越來越大,問題隨之越來越多:
空山:我想定義 mapList 方法遍歷商品串列,但是已經有了,很煩,我的只能叫 mapGoodsList 了,
空河:我自定義了一個 canBuyIt 方法,為什么使用的時候,空山的代碼出問題了呢?
滿山:我明明都用了空山的方法,為什么結果還是不對呢?
經過團隊激烈討論,決定參照 Java 的方式,用 命名空間 來解決,于是乎代碼變成了如下:
// 這是新的 Utils.js 檔案
var userObj = {};
userObj.Auth = {};
userObj.Auth.Utils = {};
userObj.Auth.Utils.mapGoodsList = function (list) {
// 實作
};
userObj.Auth.Utils.canBuyIt = function (goodId) {
// 實作
};
現在通過命名空間的方式極大地解決了一部分沖突,但是仔細看上面的代碼,如果開發人員想要呼叫某一個簡單的方法,就需要他有強大的記憶力,個人負擔變得很重,(這里值得提一嘴的是,Yahoo 的前端團隊 YUI 采用了命名空間的解決方式,同時也通過利用沙箱機制很好的解決了命名空間過長的問題,有興趣的同學可以自行了解)
書接上回,大家現在可以基于 util.js 開發各自的 UI 層通用組件了,舉一個大佬寫的 dialog.js 組件
<script src="https://www.cnblogs.com/dtux/archive/2023/03/14/util.js"></script>
<script src="https://www.cnblogs.com/dtux/archive/2023/03/14/dialog.js"></script>
<script>
org.CoolSite.Dialog.init({ /* 傳入配置 */ });
</script>
可是無論大佬怎么寫檔案,以及多么鄭重地發郵件宣告,時不時總會有同事詢問為什么 dialog.js 有問題,通過一番排查,發現導致錯誤的原因經常是在 dialog.js 前沒有引入 util.js,這樣的問題和依賴依然還在可控范圍內,但是當專案越來越復雜,眾多檔案之間的依賴經常會讓人抓狂,下面這些問題,在當時每天都在真實地發生著:
- 通用組更新了前端基礎類別庫,卻很難推動全站升級,
- 業務組想用某個新的通用組件,但發現無法簡單通過幾行代碼搞定,
- 一個老產品要上新功能,最后評估只能基于老的類別庫繼續開發,
- 公司整合業務,某兩個產品線要合并,結果發現前端代碼沖突,
- ……
以上很多問題都是因為 檔案依賴 沒有很好的管理起來,在前端頁面里,大部分腳本的依賴目前依舊是通過人肉的方式保證,當團隊比較小時,這不會有什么問題,當團隊越來越大,公司業務越來越復雜后,依賴問題如果不解決,就會成為大問題,檔案的依賴,目前在絕大部分類別庫框架里,比如國外的 YUI3 框架、國內的 KISSY 等類別庫,目前是通過配置的方式來解決,拋一個例子,不深究,
YUI.add('my-module', function (Y) {
// ...
}, '0.0.1', {
requires: ['node', 'event']
});
上面的代碼,通過 requires 等方式來指定當前模塊的依賴,這很大程度上可以解決依賴問題,但不夠優雅,當模塊很多,依賴很復雜時,煩瑣的配置會帶來不少隱患,解決命名沖突和檔案依賴,是前端開發程序中的兩個經典問題,大佬們希望通過模塊化開發來解決這些問題,所以 Sea.js 營運而生,再往后,CMD 規范也就水到渠成地形成了,(準確說來是因為先有了優秀的 Sea.js,才在后續更替程序逐漸形成了我們后來人所學習到的 CMD 規范, )
故事講到這里要告一段落了,是時候給大伙來個評書總結了,JS 在設計上其實并沒有 模塊 的概念,為了讓 JS 變成一個功能強大的語言,業界大佬們各顯神通,定了一個名為 CommonJS 的規范,實作了一個名為模塊 的東西,但可惜當時環境下大多瀏覽器并不支持,只能用于 node.js,于是 CommonJS 開始分裂,變異了一個名為 AMD 規范的模塊,可以用于瀏覽器端,由于 AMD 與 CommonJS 規范相去甚遠,于是 AMD 自立門戶,并且推出了 require.js 這個框架,用于實作并推廣 AMD 規范,此時,CommonJS 的擁護者認為,瀏覽端也可以實作 CommonJS 的規范,于是稍作改動,推出了 sea.js 這個框架并形成了 CMD 規范,,
正在 AMD 與 CMD 打得火熱的時候,ECMAScript6 給 JS 本身定了一個模塊加載的功能,彎道超車:“你們倆別爭了,JS 模塊有原生的語法了”,
再后來,正因為 AMD 與CommonJS 如此不同,且用于不同的環境,為了能夠兼容兩個平臺,UMD 就應運而生了,不過它僅僅是一個 polyfill,以兼容兩個平臺而已,嚴格意義上來說不能成為一種標準規范,

至此,大致歷史背景已講述完畢,上文出現的各大規范名詞,接下來會跟大家見面,
二、模塊化規范介紹
大致了解背景之后,接下來認真地跟各位探討一下各大規范,
開始之前,想說明一下,針對于 AMD 和 CMD,筆者不打算帶各位做原始碼級別的深究,筆者希望大家只是做一個了解或回顧,隨后將重心放至第三、四章的 CommonJS 和 EMS 中,
老大哥 CommonJS
介紹
2009年,美國程式員 Ryan_Dahl 創造了 node.js 專案,將 JS 用于服務器端編程,這標志《 JS 模塊化編程》正式誕生,不同于純前端的服務器端,是一定要有模塊的概念的,它與作業系統或其他應用程式有著各種各樣的互動,否則編程會大受限制,甚至根本無法編程,
Node.js 后端編程中最重要的思想之一就是 “模塊” ,正是這個思想,讓 JavaScript 的大規模工程成為可能,也是基于此,隨后在瀏覽器端,require.js 和 sea.js 之類的工具包也出現了;在 ES module 被完全實作之前,CommonJs 統治了之前時代模塊化編程的大半江山,它的出現也彌補了當時 JS 對于模塊化沒有統一標準的缺陷,
簡單舉例 ??
在 CommonJS 中, 模塊通常使用 module.exports 和 exports,有一個全域性方法 require(),用于加載模塊,如下:(module.exports 和 exports 后文有做闡述,此處暫且不表)
// 匯出 a.js
module.exports = function sumIt(a,b){
return a + b
}
// 引入 main.js
const sumIt = require('./a.js');
console.log('sumIt===', sumIt(1,2));
AMD 自立門戶
簡介
AMD -- Asynchronous Module Definition(異步模塊定義),它誕生于 Dojo 在使用 XHR+eval 時的實踐經驗,其支持者希望未來的解決方案都可以免受由于過去方案的缺陷所帶來的麻煩,由于 CommonJS 奠定了服務器模塊規范,大家便開始考慮客戶端模塊,而且想兩者可以兼容,讓一個模塊可以同時在服務器和瀏覽器運行,
但是 CommonJS 是同步加載模塊,服務器所有模塊都存放在本地,硬碟讀取時間很快,但對于瀏覽器來說,等待時間則取決于網速的快慢,如果時間過長,瀏覽器可能會處于“假死”,例如剛剛 main.js 的代碼,當我們呼叫 sumIt(1,2) 的時候, 瀏覽器需要等待 a.js 加載完才能進行計算,所以瀏覽器端的模塊化使用同步加載是有缺陷的,需用異步加載取代之,這也就是 AMD 規范誕生的背景,
AMD 采用異步方式加載模塊,讓模塊的加載不影響它后面陳述句的運行,所有依賴這個模塊的陳述句,都定義在一個回呼函式中,等到加載完成之后,這個回呼函式才會運行,
AMD 規范詳覽看這里
AMD 模塊的設計模式請看這里
簡單舉例 ??
define(id?, dependencies?, factory)
// id: 字串,模塊名稱(可選)
// dependencies: 表示需要加載的依賴模塊(可選)
// factory: 工廠方法,回傳一個模塊函式,也可理解為加載成功后的回呼函式
//引入依賴 ,回呼函式通過形參傳入依賴
define(['Module1', ‘Module2’], function (Module1, Module2) {
function testIt () {
/// 業務代碼
Module1.test();
}
return testIt
});
require([module],callback())
define(function (require, exports, module) {
var yourModule = require("./yourModule");
yourModule.test();
exports.yourKey = function () {
//...
}
});
不難發現,AMD 的優點是適合在瀏覽器環境中異步加載模塊,可以并行加載多個模塊,
而缺點是提高了開發成本,并且不能按需加載,而是必須提前加載所有的依賴,
CMD -- 簡單純粹
簡介
Common Module Definition 背景有講,不多贅述,Sea.js 在推廣中對模塊定義的規范化產出,推崇依賴就近,延遲執行
簡單舉例 ??
//AMD
define(['./a','./b'], function (a, b) {
//依賴一開始就寫好
a.xxx();
b.xxx();
});
//CMD
define(id?, function (requie, exports, module) {
// 依賴可以就近書寫
var a = require('./a');
a.xxx();
// 軟依賴
if (status) {
var b = requie('./b');
b.xxx();
}
});
// require 是一個方法,用來獲取其他模塊提供的介面
// exports 是一個物件,用來向外提供模塊介面
// module 是一個物件,上面存盤了與當前模塊相關聯的一些屬性和方法
CMD 規范看這里
AMD 和 CMD 對比
- 對于依賴的模塊 AMD 是 提前執行,CMD 是 延遲執行,不過 Require.js 從2.0開始,也改成可以延遲執行(根據寫法不同,處理方式不通過),
- AMD 推崇 依賴前置(在定義模塊的時候就要宣告其依賴的模塊),CMD 推崇 依賴就近(只有在用到某個模塊的時候再去 require —— 按需加載),
- AMD 的 api 默認是一個當多個用,CMD 嚴格的區分推崇職責單一,例如:AMD 里 require 分全域的和區域的,CMD 里面沒有全域的 require, 提供 seajs.use() 來實作模塊系統的加載啟動,CMD 里每個API 都更簡單純粹,參考一下玉伯 2012 年的自評:

簡談下 -- UMD
網路上關于 UMD (Universal Module Definition) 通用模塊規范的說法五花八門,這里筆者不做任何評論,只做一個通用型認知的總結: UMD 像一種 polyfill,兼容支持多個模塊規范,
參考參考:點這里可以看一下娜娜關于 UMD 的解釋
UMD 理念、規范等官方資料: https://github.com/umdjs/umd
看一個簡單的例子:
output: {
path: path.join(__dirname),
filename: 'index.js',
libraryTarget: "umd",//此處是希望打包的插件型別
library: "Swiper",
}
看一下打包之后:
!function(root,callback){
"object"==typeof exports&&"object"==typeof module?//判斷是不是nodejs環境
module.exports=callback(require("react"),require("prop-types"))
:
"function"==typeof define&&define.amd?//判斷是不是requirejs的AMD環境
define("Swiper",["react","prop-types"],callback)
:"object"==typeof exports?//相當于連接到module.exports.Swiper
exports.Swiper=callback(require("react"),require("prop-types"))
:
root.Swiper=callback(root.React,root.PropTypes)//全域變數
}(window,callback)
新大哥 ESM
使用 Javascript 中一個標準模塊系統的方案,
在此之前的時期,社區在經歷了 AMD 和 CMD 洗禮后提出了一種想法:既然都是 JS 規范,Node.js 模塊能被瀏覽器環境下的 JS 代碼隨意參考嗎?能! 本著這個想法,ES6 (ECMAScript 6th Edition, 后來被命名為 ECMAScript 2015) 于 2015年6月17日 橫空出世,主要被人熟知的其中一個特性就是 es6 module, 下文簡稱為 ESM,具體深耕內容請詳見第四章,在此介紹章節不過多贅述,
import React from 'react';
import { a, b } from './myPath';
......
export default {
function1,
const1,
a,
b
}
- 在很多現代瀏覽器可以使用
- 它兼具兩方面的優點:具有 CJS 的簡單語法和 AMD 的異步
- 得益于 ES6 的靜態模塊結構,可以進行 Tree Shaking
- ESM 允許像 Rollup 這樣的打包器洗掉不必要的代碼,減少代碼包可以獲得更快的加載
- 可以在 HTML 中呼叫,如下
<script type="module">
...
import { test } from 'your-path';
test();
...
<script/>
三、CommonJS 的深耕
CJS 的簡單使用
先看一個簡單的 Demo:
let str = 'a檔案匯出'
module.exports = function logIt (){
return str
}
const logIt = require('./a.js')
module.exports = function say(){
return {
name: logIt(),
sex: 1
}
}
以上便是 CJS 最簡單的實作,那么現在我們要帶著問題了:
- module.exports,exports 的本質區別是什么???????
- require 的加載設計是怎樣的???????
- CJS 的優缺點和與 ESM 的異同是什么???????
CJS 的實作原理
每個模塊檔案上存在 module,exports,require 三個變數(在 nodejs 中還存在 __filename 和 __dirname 變數),然而這幾個變數是沒有被定義的,但是我們可以在 Commonjs 規范下每一個 JS 模塊上直接使用它們,
- module 記錄當前模塊資訊,
- require 引入模塊的方法,
- exports 當前模塊匯出的屬性
- __dirname 在 node 中表示被執行 js 檔案的絕對路徑
- __filename 在 node 中表示被執行 js 檔案的檔案名
在編譯程序中,Commonjs 會對 JS 的代碼塊進行包裝, 以上述的 b.js 為 ??,包裝之后如下:
(function(exports,require,module,__filename,__dirname){
const logIt = require('./a.js')
module.exports = function say(){
return {
name: logIt(),
sex: 1
}
}
})
如何執行包裝的呢? 讓我們來看看包裝函式的本質:
function wrapper (script) {
return '(function (exports, require, module, __filename, __dirname) {' +
script +
'\n})'
}
// 然后是包裝函式的執行
const modulefunction = wrapper(`
const logIt = require('./a.js')
module.exports = function say(){
return {
name: logIt(),
sex: 1
}
}
`)
script 為我們在 js 模塊中寫的內容,最后回傳的就是如上包裝之后的函式,當然這個函式暫且是一個字串,在模塊加載的時候,會通過 runInThisContext (可以理解成 eval ) 執行 modulefunction ,傳入require ,exports ,module 等引數,最終我們寫的 node.js 檔案就執行了,(真實的 runInThisContext 函式執行思路和上述一致,但實作細節不一樣)
runInThisContext(
modulefunction
)(module.exports, require, module, __filename, __dirname)
實作詳情請參照官方檔案: runInThisContext 的官方檔案和示例
到此,整個模塊執行的原理大致梳理完畢,????
require 的檔案加載流程
先以 node.js 為例,看一個簡單的代碼片段
const fs = require('fs');
const say = require('./b.js');
const moment = require('moment');
先對檔案模塊做一個簡單的分類:
- fs 為 nodejs 底層的核心模塊,其他常見的還有 path、http 模塊等;
- b.js 為我們撰寫的檔案模塊;
- ./ 和 ../ 作為 相對路徑 的檔案模塊, / 作為 絕對路徑 的檔案模塊,
- moment 為自定義模塊,其他常見的還有 crypto-js 等;像此類非路徑形式也非核心的模塊,將作為自定義模塊,
當 require 方法執行的時候,接收的唯一引數作為一個 識別符號
CJS 下對不同的識別符號處理流程不同,但是目的都是找到對應的模塊,
require 識別符號加載原則
此章節借鑒了 @我不是外星人 的優秀文章中的部分內容(侵刪)
在線鏈接:《深入淺出 Commonjs 和 Es Module》
筆者在巨人的肩膀上做了一些 Curd 潤色,供大家享用 ??
- 快取加載:已經被加載過一次的模塊,會被記錄放入快取中;
- 核心模塊:優先級僅次于 快取加載,在 Node 原始碼編譯中,已被編譯成二進制代碼,所以加載核心模塊速度最快;
- 路徑模塊:已 ./ ,../ 和 / 開始的識別符號,會被當作檔案模塊處理,require() 方法會將路徑轉換成真實路徑,并以真實路徑作為索引,將編譯后的結果放入快取,方便二次加載,
- 自定義塊:在當前目錄下的 node_modules 目錄查找,如果沒有,在父級目錄的 node_modules 查找...... 直到根目錄下的 node_modules 目錄為止,在查找程序中,會找 package.json 下 main 屬性指向的檔案,如果沒有 package.json ,在 node 環境下會以此查找 index.js ,index.json ,index.node,
- 從 Node.js 12+ 起,加載第三方模塊時,exports 欄位優先級比 main 欄位要高

require 模塊引入與處理
CommonJS 模塊同步加載并執行模塊檔案,CommonJS 模塊在執行階段分析模塊依賴
const logIt = require('./b');
console.log('我是 a 檔案');
exports.say = function(){
const message = logIt();
console.log(message);
}
const say = require('./a');
const obj = {
name:'b 檔案的 object 的 name',
author:'b 檔案的 object 的 author'
}
console.log('我是 b 檔案');
module.exports = function(){
return obj
}
const a = require('./a');
const b = require('./b');
console.log('我是 main 檔案');
運行一下:

????????? 問題:
- main.js 和 a.js 模塊都參考了 b.js 模塊,但是 b.js 模塊為什么只執行了一次?
- a.js 模塊 和 b.js 模塊互相參考,但是為什么沒有回圈參考報錯?
我們先引入一個上文并未提及的概念:Module 和 module
module :在 Node 中每一個 js 檔案都是一個 module ,module 上保存了 exports 等資訊之外,
還有一個 loaded ( boolean 型別)表示該模塊是否已經被加載過,
Module :以 nodejs 為例,整個系統運行之后,會用 Module 快取每一個模塊加載的資訊,
然后,在回答上述思考問題之前,一起來看一下阮一峰老師關于 require 的原始碼解讀:
// id 為路徑識別符號
function require(id) {
/* 查找 Module 上有沒有已經加載的 js 物件*/
const cachedModule = Module._cache[id]
/* 如果已經加載了那么直接取走快取的 exports 物件 */
if(cachedModule){
return cachedModule.exports
}
/* 創建當前模塊的 module */
const module = { exports: {} ,loaded: false , ...}
/* 將 module 快取到 Module 的快取屬性中,路徑識別符號作為 id */
Module._cache[id] = module
/* 加載檔案 */
runInThisContext(wrapper('module.exports = "123"'))
(module.exports, require, module, __filename, __dirname)
/* 加載完成 *//
module.loaded = true
/* 回傳值 */
return module.exports
}
代碼還是非常容易理解的,解讀總結如下:
require 會接收一個引數(檔案識別符號),然后分析定位檔案(上一小節已經講到),接下來從 Module 上查找有沒有快取,如果有快取,那么直接回傳快取的內容,
如果沒有快取,會創建一個 module 物件,快取到 Module 上,然后執行檔案;加載完檔案,將 loaded 屬性設定為 true ,然后回傳 module.exports 物件,
模塊匯出其實跟 a = b 賦值一樣:基本型別匯出的是值, 參考型別匯出的是參考地址,(exports 和 module.exports 持有相同參考,后文會專門解讀)
require 避免回圈參考
我們先來分析剛剛的例子,下面先用一幅圖來表示 a.js 的加載流程:

理解了這幅流程圖后,再來看完整的流程圖就不再吃力了:

此時我們需要注意一點:
當我們第一次執行 b.js 模塊的時候,a.js 還沒有匯出 say 方法,所以此時在 b.js 同步背景關系中,是獲取不到 say 的,那么如果想要獲取 say ,辦法有兩個:
異步加載
const say = require('./a');
const obj = {
name:'b 檔案的 object 的 name',
author:'b 檔案的 object 的 author'
}
console.log('我是 b 檔案');
setTimeout(()=>{
console.log('異步列印 a 模塊' , say)
},0)
module.exports = function(){
return obj
}
動態加載
console.log('我是 a 檔案');
exports.say = function(){
const logIt = require('./b');
const message = logIt();
console.log(message);
}
const a = require('./a');
a.say();
由此我們可見:
require 本質上就是一個函式,那么函式可以在任意背景關系中執行,自由地加載其他模塊的屬性方法,
require 避免重復加載
正如上述所言,加載之后的檔案的 module 會被快取到 Module 上,比如一個模塊已經 require 引入了 a 模塊,如果另外一個模塊再次參考 a ,那么會直接讀取快取值 module ,所以無需再次執行模塊,
對應 demo 片段中,首先 main.js 參考了 a.js ,a.js 中 require 了 b.js, 此時 b.js 的 module 放入快取 Module 中,接下來 main.js 再次參考 b.js ,那么直接走的快取邏輯,所以 b.js 只會執行一次,也就是在 a.js 引入的時候,由此就避免了重復加載,
???????????? 這里給大家拋一個思考問題:
// a.js
const b = require('./b');
console.log('我是 a 檔案',b);
const tets = Object.getPrototypeOf(b);
tets.aaa = 'new aaa test';
// b.js
console.log('我是 b 檔案');
module.exports = {
str: 'bbbb'
}
// main.js
require('./a');
const b = require('./b');
console.log('b===', b);
console.log('proto===', Object.getPrototypeOf(b));
?????? 看完這個事例,你有什么啟發嗎?是不是和第三方侵入式的工具庫很像呢?
exports 和 module.exports
module.exports 和 exports 在一開始都是一個空物件 { },但實際上,這兩個物件應當是指向同一塊記憶體的,在不去改變它們指向的記憶體地址的情況下,module.exports 和 exports 幾乎是等價的,
require 引入的物件本質上其實是 module.exports ,那么這就產生了一個問題,當 module.exports和 exports 指向的不是同一塊記憶體時,exports 的內容就會失效,
module.exports = { money: '20塊 ??' };
exports.money = '一伯萬!!!??';
這時候,require 真實得到的是 { money: '20塊 ??' } ,當他們二者 同時存在 的時候,會發生覆寫的情況,所以我們通常最好選擇 exports 和 module.exports 兩者之一,
- 思考問題1: 上述例子使用 exports = { money: '200' } 這種形式賦值物件可以嗎?
答:不可以,通過上述講解都知道 exports , module 和 require 作為形參的方式傳入到 js 模塊中,我們直接 exports = { } 修改 exports ,等于重新賦值了形參,但是不會在參考原來的形參,舉個例子:
function change(myName){
return myName.name = {
name: '老板'
}
}
let myName = {
name: '小打工人'
}
fix(myName);
console.log(myName);
- 簡單來說 module.exports 是給 module 里面的 exports 屬性賦值,值可以是任何型別;
- exports 是個物件,用它來暴露模塊資訊必須給它添加對應的屬性;
- 需要注意的是:module.exports 當匯出一些函式等非物件屬性的時候,也有一些風險,就比如回圈參考的情況下,物件會保留相同的記憶體地址,就算一些屬性是后系結的,也能通過異步形式訪問到,
四、ES Module 的深耕
匯入和匯出
// 匯出 a.js
const name = 'jiawen';
const game = 'lol';
const logIt = function (){
console.log('log it !!!')
}
export default {
name,
author,
logIt
}
// 引入 main.js
import { name , author , logIt } from './a.js'
// 對于引入默認匯出的模塊,可以自定義名稱,
import allInfo from './a.js'
對于 ESM 規范中混合匯出方式,日常使用,這里不再做舉例,
提一下 “重署名匯入和重定向匯出”:
import { name as newName , say, game as newGame } from '/a.js';
console.log( newName , newGame , say );
export * from 'module'; // 1
export { name, author, ..., say } from 'module'; // 2
export { name as newName , game as newGame , ..., say } from '/a.js'; // 3
只運行,不關心匯入:
import '/a.js'
動態匯入:
import asyncComponent from 'dt-common/src/utils/asyncLoad';
let lazy = (async, name) => {
return asyncComponent(
() => async.then((module: any) => module.default), { name }
)
}
const ApiManage = lazy(import('./views/dataService/apiManage'), 'apiManage');
- 動態匯入 import('xxx') 回傳一個 Promise. 使用時可能需要在 webpack 中做相應的配置處理,
ESM 的靜態語法
- ES6 module 的引入和匯出是靜態的,import 會自動提升到代碼的頂層,靜態的語法意味著可以在編譯時確定匯入和匯出,更加快速的查找依賴,可以使用 lint 工具對模塊依賴進行檢查,可以對匯入匯出加上型別資訊進行靜態的型別檢查,
- import , export 不能放在塊級作用域或條件陳述句中,(錯誤示范就不再舉例了)
- import 的匯入名不能為字串或在判斷陳述句中,不可以用模版字串拼接的方式,
ESM 的執行特性
- 使用 import 匯入的模塊運行在嚴格模式下
- 使用 import 匯入的變數是只讀的,(可以理解默認為 const 裝飾,無法被賦值)
- 使用 import 匯入的變數是與原變數系結/參考的,可以理解為 import 匯入的變數無論是否為基本型別都是參考傳遞,請看下面的例子:
// js中 基礎型別是值傳遞
let a = 1;
let b = a;
b = 2;
console.log(a, b) // 1 2
// js中 參考型別是參考傳遞
let a = { name: 'xxx' };
let b = obj
b.name = 'bbb'
console.log(a.name, b.name) // bbb bbb
// a.js
export let a = 1
export function add(){
a++
}
// main.js
import { a, add } from './a.js';
console.log(a); //1
add();
console.log(a); //2
ESM 的 import ()
剛剛已經舉過 import () 在 TagEngine 里實際應用的例子,其核心在于回傳一個 Promise 物件, 在回傳的 Promise 的 then 成功回呼中,可以獲取模塊的加載成功資訊,下面舉一些 import () 的社區常用:
- Vue 中的懶加載:
[
...
{
path: 'home',
name: '首頁',
component: ()=> import('./home') ,
},
...
]
- React 中的懶加載
const LazyComponent = React.lazy(() => import('./text'));
class index extends React.Component {
render() {
return (
<React.Suspense
fallback={
<div className="icon">
<SyncOutlinespin />
</div>
}
>
<LazyComponent />
</React.Suspense>
);
}
}
import() 這種加載效果,可以很輕松的實作代碼分割, 避免一次性加載大量 js 檔案,造成首次加載白
屏時間過長的情況,
ESM 的回圈參考
// f1.js
import { f2 } from './f2'
console.log(f2);
export let f1 = 'f1'
// f2.js
import { f1 } from './f1'
console.log(f1);
export let f2 = 'f2'
// main.js
import { f1 } from './f1'
console.log(bar)
此時會報錯 f1 未定義,我們可以采用函式宣告,因為函式宣告會提示到檔案頂部,所以就可以直接在 f2.js 呼叫還沒執行完畢的 f1.js的 f1 方法,但請不要在函式內使用外部變數 !!!!
// f1.js
import { f2 } from './f2'
console.log(f2());
export function f1(){
return 'f1'
}
// f2.js
import { f1 } from './f1'
console.log(f1());
export function f2(){
return 'f2'
}
// main.js
import { f1 } from './f1'
console.log(f1)
Tree Shaking 和 DCE
DCE: dead code elimination,簡稱 DCE,死代碼消除
Tree Shaking 在 Webpack 中的實作是用來盡可能的洗掉一些被 import 了但其實沒有被使用的代碼,
export let num = 1;
export const fn1 = ()=>{
num ++
}
export const fn2 = ()=>{
num --
}
import { fn1 } from './a'
fn1();
- 如上 a.js 中暴露兩個方法,fn1 和 fn2,但是在 main.js 中,只用到了 fn1,那么構建打包的時候,fn2將作為沒有參考的方法,不被打包進來,
- tree shaking 和 “死代碼剔除” 是有本質區別的,“做一個??蛋糕,死代碼剔除是扔一個雞蛋進去,做好蛋糕后把雞蛋殼拿出來;tree shaking 是先檢查并盡可能地剔除沒有用到的部分,比如雞蛋殼,再去做蛋糕,” 這二者還是有一些本質區別的,
五、ESM 與 CJS 的小結
CommonJS -- 小結
- CommonJS 模塊由 JS 運行時實作,
- CommonJs 是單個值匯出,本質上匯出的就是 exports 屬性,
- CommonJS 是可以動態加載的,對每一個加載都存在快取,可以有效的解決回圈參考問題,
- CommonJS 模塊同步加載并執行模塊檔案,
Es Module -- 小結
- ES6 Module 靜態的,代碼發生在編譯時,不能放在塊級作用域內,但可以動態匯入,
- ES6 Module 的值是動態系結的,可以通過匯出方法修改,可以直接訪問修改結果,
- ES6 Module 可以匯出多個屬性和方法,可以單個匯入匯出,混合匯入匯出,
- ES6 模塊提前加載并執行模塊檔案,匯入模塊在嚴格模式下,
- ES6 Module 的特性可以很容易實作 Tree Shaking 和 Code Splitting,
六、待探究的問題
此章節探討 ESM 與 CMJ 的互轉,??歡迎各位補充指正!!!??
特別鳴謝參考文章,排名不分先后:
https://github.com/amdjs/amdjs-api/wiki/AMD
https://github.com/seajs/seajs/issues/242
https://github.com/umdjs/umd
https://juejin.cn/post/6994224541312483336#heading-20
https://segmentfault.com/a/1190000017878394
?????? 完結 ??????
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/546871.html
標籤:其他
