大家好,我是若川。最近組織了源碼共讀活動,感興趣的可以加我微信?ruochuan12
早期的web應用非常簡單,可以直接加載js的形式去實現。隨著需求的越來越多,應用越做越大,需要模塊化去管理項目中的js、css、圖片等資源。這里有很多大家熟悉的模塊化標準, CJS、AMD、CMD、UMD 等等。模塊化提供了我們更好的方式來組織和維護函數以及變量。而在 npm 生態開發的背景下,CJS 模塊是開發過程中接觸最多也是無法避免的。但由于瀏覽器并不能直接執行基于 CJS 打包的模塊,因此類似 webpack 等打包工具便應運而生。隨著webpack 大有一統構建工具的趨勢下,JavaScript 官方的標準化模塊系統ESM完成了。本文主要介紹下模塊化標準間差異、基本加載原理。
模塊化發展
1、為何要模塊化
仔細想想,用 JavaScript 編碼就是管理變量。這一切都是關于為變量賦值,或為變量添加數字,或將兩個變量組合在一起并將它們放入另一個變量中。
如果代碼中僅有少量的變量,那么組織起來其實是很簡單的。一旦有很多的變量,我們會通過函數作用域去組織變量。因為函數作用域的緣故,一個函數無法訪問另一個函數中定義的變量。
如果只維護少量變量非常簡單。但是如果有很多的變量,我們就需要用一種方法來幫助做到這一點,叫做作用域。由于作用域在 JavaScript 中的工作方式,函數不能訪問在其他函數中定義的變量。
這種方式是很有效的。在寫一個函數的時候,只需要考慮當前函數,而不必擔心其它函數可能會改變當前函數的變量。如果想要函數之間共享變量要怎么辦呢?一種通用的做法是全局作用域。
在 jQuery 時代這種提升做法相當普遍。在我們加載任何 jQuery 插件之前,我們必須確保 jQuery 已經存在于全局作用域。
所有的 <script>
必須以正確的順序排列,必須保證被依賴的變量先加載。如果排列錯了,那么在運行過程中,應用將會拋出錯誤,并且停止繼續運行。
代碼之間的依賴是不透明的。這使得代碼維護變得困難。代碼變得充滿不確定性。任何函數都可能依賴全局作用域中的任何變量。
其次,由于變量存在于全局作用域,所以任何代碼都可以改變它。
2、模塊化的作用
模塊化為你提供了一種更好的方式來組織變量和函數。可以把相關的變量和函數放在一起組成一個模塊。這種實現形式可以把函數和變量放在模塊作用域中。模塊作用域還提供一種暴露變量給其他模塊使用的方式。模塊可以明確地指定哪些變量、類或函數對外暴露。
對外暴露的過程稱為導出。一旦導出,其他模塊就可以明確地聲稱它們依賴這些導出的變量、類或者函數。
因為這是一種明確的關系,所以你可以很簡單地辨別哪些代碼能移除,哪些不能移除。
擁有了在模塊之間導出和導入變量的能力之后,你就可以把代碼分割成更小的、可以獨立運行地代碼塊了。基于這些代碼塊,你就可以像搭樂高積木一樣,創建所有不同類型的應用。比較流程的規范有CommonJS,AMD,CMD,ES,UMD等
3、現有模塊標準
CJS
?是?CommonJS
?的縮寫。只適用于node端:
const _ = require('lodash'); module.exports = function doSomething(n) {}
AMD
?代表異步模塊定義。在瀏覽器端有效:
define(['dep1', 'dep2'], function (dep1, dep2) {return function () {};
});
UMD
?代表通用模塊定義(Universal Module Definition
):
(function (root, factory) {if (typeof define === 'function' && define.amd) {// AMD. Register as an anonymous module.define([], factory);} else if (typeof module === 'object' && module.exports) {// Node. Does not work with strict CommonJS, but// only CommonJS-like environments that support module.exports,// like Node.module.exports = factory();} else {// Browser globals (root is window)root.returnExports = factory();}
}(typeof self !== 'undefined' ? self : this, function () {// Just return a value to define the module export.// This example returns an object, but the module// can return a function as the exported value.return {};
}));
什么是 ESM
簡介
ESM是ES6提出的標準模塊系統,ECMAScript modules。JS自己的模塊體系
<script type="module">import { html, Component, render } from 'https://unpkg.com/htm/preact/standalone.module.js';class App extends Component {state = {count: 0}add = () => {this.setState({ count: this.state.count + 1 });}render() {return html`<div class="app"><div>count: ${this.state.count}</div><button onClick=${this.add}>Add Todo</button></div>`;}}render(html`<${App} page="All" />`, document.body);
</script>
思考:上述代碼和在webpack中開發有啥區別?
2、瀏覽器端技術實現
回顧下Webpack執行流程
本地模塊化解析(通過webpack或者babel,將import解析成cjs)
將各個庫打包成一個js boundle
開啟服務,托管資源
瀏覽器獲取資源
執行代碼
瀏覽器端ESM執行流程
開啟服務,托管資源(ES源碼)
加載入口文件,瀏覽器模塊化解析
構建
遍歷依賴樹,先解析文件,然后找出依賴,最后又定位并加載這些依賴,如此往復。(下載所有的js)
模塊映射
當加載器要從一個 URL 加載文件時,它會把 URL 記錄到模塊映射中,并把它標記為正在下載的文件。然后它會發出這個文件請求并繼續開始獲取下一個文件。
解析模塊
所有的模塊都按照嚴格模式來解析的。不同文件類型按照不同的解析方式稱。在瀏覽器中,通過type="module"
屬性告訴瀏覽器這個文件需要被解析為一個模塊。不過在 Node 中,我們并不使用 HTML 標簽,所以也沒辦法通過type
屬性來辨別。社區提出一種解決辦法是使用.mjs
拓展名。
運行
采用深度優先的后序遍歷方式,順著關系圖到達最底端沒有任何依賴的模塊,然后設置它們的導出。模塊映射會以 URL 為索引來緩存模塊,以確保每個模塊只有一個模塊記錄。這保證了每個模塊只會運行一次。
3、為什么火起來
ES語法基本確定
http2普及
新瀏覽器普及
開發與發布代碼一致
啟動快
全新加載模式
目前瀏覽器支持:
目前只有5%的瀏覽器不兼容es相關規范。
4、為什么還沒火起來
部分瀏覽器的兼容性
歷史包袱悠久
生態不完善
實戰
當我們在項目中使用需要考慮以下幾個問題點
1. 代碼開發需要基于es開發
let a = 1;
new Promise()
() => {}
...
2. 依賴庫加載
node_modules代碼服務化
兼容cjs
加載包內部es目錄
cjstoesm
CDN(network for npm)
https://unpkg.com/
https://www.skypack.dev/
3. 兼容不支持的瀏覽器
type="module"
實現
如果瀏覽器不支持,他只識別
type="text/javascript"
不識別type="module"
,故不下載js;如果支持,則會下載js如果瀏覽器不支持,則會忽略nomodule,下載js;如果支持,則不會下載js
<script type="module" src="app.js"></script>
<script nomodule src="app-bundle.js"></script>
systemjs實現https://github.com/systemjs/systemjs
<script src="system.js"></script>
<script type="systemjs-importmap">
{"imports": {"lodash": "https://unpkg.com/lodash@4.17.10/lodash.js"}
}
</script>
<script type="systemjs-module" src="/js/main.js"></script>
4. jsx支持
通過其他開源庫
<script type="module">import { html, Component, render } from 'https://unpkg.com/htm/preact/standalone.module.js';class App extends Component {render() {return html`<div class="app"></div>`;}}render(html`<${App} page="All" />`, document.body);
</script>
本地語法糖解析
<APP {...Props}/>
=>
React.createElement(App, {...props})
現有腳手架
1. snowpack
托管node_modules
支持圖片、css等資源
JSX 和 Typescript 編譯
HMR
...
2. vite
https://cn.vitejs.dev/guide/
目前snowpack的作者后續可能不再維護了,所以推薦大家使用vite
ESM 未來
2018 年 5 月 Firefox 60 發布后,所有的主流瀏覽器就都默認支持 ESM 了。Node 也正在添加 ESM 支持,為此還成立了工作小組來專門研究 CJS 和 ESM 之間的兼容性問題。所以,在未來你可以直接在 <script>
標簽中使用 type="module"
,并且在代碼中使用 import
和 export
。同時,更多的模塊功能也正在研究中。比如動態導入提案已經處于 Stage 3 狀態;import.meta
也被提出以便 Node.js 對 ESM 的支持;模塊定位提案 也致力于解決瀏覽器和 Node.js 之間的差異。
相信在不久的未來,跟模塊一起玩耍將會變成一件更加愉快的事!
node v10以上版本全部支持ESM https://kentcdodds.com/blog/super-simple-start-to-es-modules-in-node-js
相關參考
ECMAScript modules in browsers https://jakearchibald.com/2017/es-modules-in-browsers/
JavaScript 模塊現狀 https://zhuanlan.zhihu.com/p/26567790
基于esm、html、unpkg的前端開發模式:https://github.com/developit/htm
How I Build JavaScript Apps In 2021:https://timdaub.github.io/2021/01/16/web-principles/
Find out how much turning on modern JS could save. https://estimator.dev/
什么是amd、commonjs、umd、esm? https://zhuanlan.zhihu.com/p/96718777
ES modules: A cartoon deep-dive:https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/
import.map:https://github.com/WICG/import-maps
面對 ESM 的開發模式,webpack 還有還手之力嗎? https://topic.atatech.org/articles/202736