模塊化開發(fā)是當(dāng)下最重要的前端開發(fā)范式之一
模塊化演變過程
-
Stage1 文件劃分方式
具體的做法就是每個(gè)功能及其相關(guān)狀態(tài)數(shù)據(jù)各自單獨(dú)放到不同的文件中,約定每個(gè)文件就是一個(gè)獨(dú)立的模塊,使用某個(gè)模塊就是將這個(gè)模塊引入到頁面中,然后直接調(diào)用模塊中的成員(變量/函數(shù))
缺點(diǎn)也就十分明顯了:- 污染了全局作用域
- 命名沖突問題
-
無法管理模塊依賴關(guān)系
-
Stage2 命名空間方式
每個(gè)模塊只暴露一個(gè)全局對(duì)象,所有的模塊成員都掛載到這個(gè)對(duì)象中,具體的做法就是在第一階段基礎(chǔ)之上,通過將每個(gè)模塊包裹為一個(gè)全局對(duì)象的形式實(shí)現(xiàn),有點(diǎn)類似于為模塊內(nèi)的成員添加了命名空間的感覺
通過【命名空間】這一概念減少了命名沖突的可能,但是同樣的,沒有私有空間,所有的模塊成員都可以在模塊外部被訪問或者是被修改,而且沒有辦法管理模塊之間的依賴關(guān)系

-
Stage3 IIFE 立即執(zhí)行函數(shù)表達(dá)式
將每個(gè)模塊成員都放在一個(gè)函數(shù)提供的私有作用域中,對(duì)于需要暴露給外部的成員,通過掛在到全局對(duì)象上的方式來實(shí)現(xiàn),有了私有成員的概念,私有成員只能在模塊成員內(nèi)部通過閉包的形式訪問
需要暴露給外部的成員就使用這種掛載到全局作用域上面去實(shí)現(xiàn) - Stage4 模塊化演變
利用IIFE參數(shù)作用依賴聲明使用,具體做法就是在第三階段的基礎(chǔ)上,利用立即執(zhí)行函數(shù)的參數(shù)傳遞模塊依賴項(xiàng),使得每一個(gè)模塊之間的關(guān)系變得更加明顯
例如使用jQuery,就使用立即調(diào)用函數(shù)接受jQuery的參數(shù)
以上就是早期在沒有工具和規(guī)范的情況下,對(duì)模塊化的落地方式
模塊化規(guī)范的出現(xiàn)
需要的內(nèi)容就是:
模塊化標(biāo)準(zhǔn)+模塊加載器
CommonJS規(guī)范(node.js中的規(guī)范)
- 一個(gè)文件就是一個(gè)模塊
- 每個(gè)模塊都有單獨(dú)的作用域
- 通過module.exports導(dǎo)出成員
- 通過require函數(shù)載入模塊
CommonJS是以同步模式加載模塊
在瀏覽器中必然會(huì)導(dǎo)致效率低下
AMD(Asynchronous Module Definition)
異步模塊定義規(guī)范
require.js
require.js實(shí)現(xiàn)了AMD規(guī)范,本身也是很強(qiáng)大的模塊加載器


目前絕大多數(shù)第三方庫都支持AMD規(guī)范
- AMD使用起來相對(duì)復(fù)雜
- 模塊JS文件請(qǐng)求頻繁,效率低下
Sea.js+CMD

這些以前的知識(shí)在目前來看也是很重要的一環(huán)
模塊化標(biāo)準(zhǔn)規(guī)范(模塊化的最佳實(shí)踐)
- 在node環(huán)境當(dāng)中,會(huì)采用CommonJS規(guī)范
- 在瀏覽器環(huán)境中,會(huì)采用一個(gè)叫做ES Modules規(guī)范
現(xiàn)如今絕大多數(shù)瀏覽器都已經(jīng)支持ES Modules,故而ES Modules的學(xué)習(xí)成為了重中之重
ES Modules
- 通過script 添加type = module 的屬性,就可以以ES Module的標(biāo)磚執(zhí)行其中的JS代碼
<script type="module">
console.log("this is ES modules")
</script>
- ESM 會(huì)自動(dòng)采用嚴(yán)格模式,忽略u(píng)se strict
(在非嚴(yán)格模式下,this指向的是window對(duì)象)
<script type="module">
console.log(this)
</script>
- 每個(gè)ES Module 都是運(yùn)行在單獨(dú)的私有作用域當(dāng)中(第二個(gè)打印的foo就會(huì)報(bào)錯(cuò)undefined)
<script type="module">
var foo = 100
console.log(foo)
</script>
<script type="module">
console.log(foo)
</script>
- 在ESM中是通過CORS的方式請(qǐng)求外部JS模塊的
- ESM 的script標(biāo)簽會(huì)延遲執(zhí)行腳本
(延遲加載腳本,先渲染元素到頁面上,一般的script標(biāo)簽就會(huì)等到腳本加載完成才會(huì)渲染元素)
這個(gè)小特點(diǎn)與script標(biāo)簽的defer屬性是一樣的
<script type= "module" src="demo.js"></script>
<p>需要顯示的內(nèi)容</p>
ES Modules導(dǎo)入和導(dǎo)出
- 可以導(dǎo)出變量,函數(shù),類等等
export var name = 'foo module'
export function hello(){
console.log("foo hello")
}
export class Person{
}
- 也可以統(tǒng)一導(dǎo)出,比如:
export { name , hello , Person}
- 在另一個(gè)模塊js文件要導(dǎo)入
import { hello, name } from './module.js'
console.log(name)
hello()
重命名
var name = 'foo module'
function hello(){
console.log("foo hello")
}
class Person{
}
export {
name as fooName,
hello as fooHello,
Person as fooPerson
}
重命名之后導(dǎo)入時(shí)也要注意名字變化
import { fooHello, fooName } from './module.js'
console.log(fooName)
fooHello()
重命名特殊情況
將導(dǎo)出成員名稱設(shè)置為default,這個(gè)成員就會(huì)被設(shè)置為當(dāng)前模塊的默認(rèn)導(dǎo)出成員,在導(dǎo)入的時(shí)候就必須要進(jìn)行重命名
export {
name as default,
hello as fooHello,
Person as fooPerson
}
重命名default才能調(diào)用
import { fooHello, default as fooName } from './module.js'
ESM 關(guān)于針對(duì)default的特殊處理
將name變量設(shè)置為默認(rèn)導(dǎo)出
export default name;
在導(dǎo)入的時(shí)候可以通過直接import + 變量名的方式接受默認(rèn)導(dǎo)出的成員,變量名稱隨意
// fooName這里是可以隨意取名的
import fooName from './module.js'
ESM 導(dǎo)入導(dǎo)出的注意事項(xiàng)
- export 后面跟上的花括弧包裹的不是字面量,是固定語法
- 導(dǎo)入時(shí)的那些成員是分享的內(nèi)存空間,是完全相同的引用關(guān)系
- 導(dǎo)入的成員是只讀的
ESM import用法
- 導(dǎo)入時(shí)from關(guān)鍵字后面跟的是字符串,內(nèi)部的內(nèi)容路徑必須要完整的文件名稱,不能省略js后綴名,跟CommonJS完全相反
- 也可以使用完整的url加載模塊,也就是說可以使用CDN上面的模塊,完整的
- 如果說只執(zhí)行某個(gè)模塊的功能,不去提取模塊中的成員的話,可以保持花括弧為空,或者直接import跟上字符串,這個(gè)特性在我們導(dǎo)入一些不需要外界控制的子功能模塊時(shí)就非常有用了
import {} from './module.js'
import './module.js'
- 需要導(dǎo)出的成員特別多,導(dǎo)入時(shí)都會(huì)用到他們,就可以用*全部提取出來,使用as關(guān)鍵字全部存在對(duì)象里面
import * as mod from './module.js'
console.log(mod)

- 動(dòng)態(tài)導(dǎo)入
import('./module.js').then(function (module) {
console.log(module)
})
- 默認(rèn)成員和明明成員同時(shí)導(dǎo)出
var name = 'jack'
var age = 18
export { name, age }
console.log('module action')
export default 'default export'
import abc, { name, age } from './module.js'
console.log(name, age, abc)
ESM 直接導(dǎo)出導(dǎo)入成員
- 具體的做法就是將import關(guān)鍵詞修改為export,所有的導(dǎo)入成員會(huì)作為當(dāng)前模塊的導(dǎo)出成員,在當(dāng)前作用域下,也就不再可以訪問這些成員了。
一般用于index文件,把散落的模塊通過這種方式組織到一起,導(dǎo)出,方便外部使用
avatar.js:
export var Avatar = 'Avatar Component'
button.js:
var Button = 'Button Component'
export default Button
index.js:
export { default as Button } from './button.js'
export { Avatar } from './avatar.js'
app.js(導(dǎo)入):
import { Button, Avatar } from './components/index.js'
console.log(Button)
console.log(Avatar)
avatar和button都是暴露了組件,index.js則是將這兩個(gè)組件導(dǎo)入,并且導(dǎo)出,作為一個(gè)橋梁的作用
ESM in Browser(Ployfill兼容方案)
- 讓瀏覽器支持ESM 的絕大特性
- 模塊名字為Browser ESM Loader
針對(duì)NPM下的模塊可以通過upkg這個(gè)網(wǎng)站的CDN服務(wù)來拿到所有的JS文件
https://unpkg.com/ + npm下的模塊名
比如
https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js
/dist/表示目錄下的文件

- 引入IE所需要的promise,ployfill
<script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script>
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
- nomodule屬性
解決了支持polyfill的瀏覽器不去加載標(biāo)簽內(nèi)資源的問題
ESM in Node.js
- 在Node當(dāng)中直接使用ESM ,要做的有:
-
第一,將文件的擴(kuò)展名由 .js 改為 .mjs;
-
第二,啟動(dòng)時(shí)需要額外添加
--experimental-modules參數(shù);
-
- 也可以用ESM 載入原生模塊
// // 此時(shí)我們也可以通過 esm 加載內(nèi)置模塊了
import fs from 'fs'
fs.writeFileSync('./foo.txt', 'es module working')
- 也可以直接提取模塊內(nèi)的成員,內(nèi)置模塊兼容了ESM的提取成員的方式
import { writeFileSync } from 'fs'
writeFileSync('./bar.txt', 'es module working')
- 對(duì)于第三方的NPM模塊也可以通過ESM加載
(比如第三方模塊lodash)
import _ from 'lodash'
_.camelCase('ES Module')
- 但是不能使用ESM的花括弧方式去載入第三方模塊的成員
// // 不支持,因?yàn)榈谌侥K都是導(dǎo)出默認(rèn)成員
import { camelCase } from 'lodash'
console.log(camelCase('ES Module'))
ESM in Node.js 與 CommonJS模塊的交互
- CommonJS模塊始終只會(huì)導(dǎo)出一個(gè)默認(rèn)成員
- ESM 中是可以導(dǎo)入CommonJS模塊的
- 不能直接提取成員,import不是解構(gòu)導(dǎo)出對(duì)象
-
在CommonJS中通過require載入ESM 也是不可以的
ESM in Node.js與CommonJS的差異
在這之前先推薦使用nodemon工具,可以監(jiān)聽mjs文件的變化并且給出錯(cuò)誤信息
先用npm 進(jìn)行全局安裝,再使用

- ESM中沒有模塊全局成員了
- require,module,exports自然是可以通過import和export代替
- __filename 和 __dirname 通過 import 對(duì)象的 meta 屬性獲取
const currentUrl = import.meta.url
console.log(currentUrl)
- 通過 url 模塊的 fileURLToPath 方法轉(zhuǎn)換為路徑
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
console.log(__filename)
console.log(__dirname)
Node的新版本更加支持ESM了
- 在新版本中的package.json添加type屬性表示module,所有的JS文件就可以默認(rèn)以ESM支持了
- 如果需要在 type=module 的情況下繼續(xù)使用 CommonJS, 需要將文件擴(kuò)展名修改為 .cjs
Babel兼容方案
- 早期的node版本,可以使用Babel進(jìn)行ESM的兼容
- 主流的JavaScript編譯器,可以將新特性的代碼編譯成當(dāng)前環(huán)境支持的代碼
需要安裝babel一系列依賴
yarn add @babel/node @babel/core @babel/core @babel/preset-env --dev
-
檢測(cè)babel命令:
- 安裝插件
yarn add @babel/plugin-transform-commonjs --dev
- 建立一個(gè).babelrc文件
{
"plugins": [
"@babel/plugin-transform-modules-commonjs"
]
}
- 運(yùn)行文件
yarn babel-node .\index.js










