模塊化開發是當下最重要的前端開發范式之一
模塊化演變過程
-
Stage1 文件劃分方式
具體的做法就是每個功能及其相關狀態數據各自單獨放到不同的文件中,約定每個文件就是一個獨立的模塊,使用某個模塊就是將這個模塊引入到頁面中,然后直接調用模塊中的成員(變量/函數)
缺點也就十分明顯了:- 污染了全局作用域
- 命名沖突問題
-
無法管理模塊依賴關系
-
Stage2 命名空間方式
每個模塊只暴露一個全局對象,所有的模塊成員都掛載到這個對象中,具體的做法就是在第一階段基礎之上,通過將每個模塊包裹為一個全局對象的形式實現,有點類似于為模塊內的成員添加了命名空間的感覺
通過【命名空間】這一概念減少了命名沖突的可能,但是同樣的,沒有私有空間,所有的模塊成員都可以在模塊外部被訪問或者是被修改,而且沒有辦法管理模塊之間的依賴關系
-
Stage3 IIFE 立即執行函數表達式
將每個模塊成員都放在一個函數提供的私有作用域中,對于需要暴露給外部的成員,通過掛在到全局對象上的方式來實現,有了私有成員的概念,私有成員只能在模塊成員內部通過閉包的形式訪問
需要暴露給外部的成員就使用這種掛載到全局作用域上面去實現 - Stage4 模塊化演變
利用IIFE參數作用依賴聲明使用,具體做法就是在第三階段的基礎上,利用立即執行函數的參數傳遞模塊依賴項,使得每一個模塊之間的關系變得更加明顯
例如使用jQuery,就使用立即調用函數接受jQuery的參數
以上就是早期在沒有工具和規范的情況下,對模塊化的落地方式
模塊化規范的出現
需要的內容就是:
模塊化標準+模塊加載器
CommonJS規范(node.js中的規范)
- 一個文件就是一個模塊
- 每個模塊都有單獨的作用域
- 通過module.exports導出成員
- 通過require函數載入模塊
CommonJS是以同步模式加載模塊
在瀏覽器中必然會導致效率低下
AMD(Asynchronous Module Definition)
異步模塊定義規范
require.js
require.js實現了AMD規范,本身也是很強大的模塊加載器
目前絕大多數第三方庫都支持AMD規范
- AMD使用起來相對復雜
- 模塊JS文件請求頻繁,效率低下
Sea.js+CMD
這些以前的知識在目前來看也是很重要的一環
模塊化標準規范(模塊化的最佳實踐)
- 在node環境當中,會采用CommonJS規范
- 在瀏覽器環境中,會采用一個叫做ES Modules規范
現如今絕大多數瀏覽器都已經支持ES Modules,故而ES Modules的學習成為了重中之重
ES Modules
- 通過script 添加type = module 的屬性,就可以以ES Module的標磚執行其中的JS代碼
<script type="module">
console.log("this is ES modules")
</script>
- ESM 會自動采用嚴格模式,忽略use strict
(在非嚴格模式下,this指向的是window對象)
<script type="module">
console.log(this)
</script>
- 每個ES Module 都是運行在單獨的私有作用域當中(第二個打印的foo就會報錯undefined)
<script type="module">
var foo = 100
console.log(foo)
</script>
<script type="module">
console.log(foo)
</script>
- 在ESM中是通過CORS的方式請求外部JS模塊的
- ESM 的script標簽會延遲執行腳本
(延遲加載腳本,先渲染元素到頁面上,一般的script標簽就會等到腳本加載完成才會渲染元素)
這個小特點與script標簽的defer屬性是一樣的
<script type= "module" src="demo.js"></script>
<p>需要顯示的內容</p>
ES Modules導入和導出
- 可以導出變量,函數,類等等
export var name = 'foo module'
export function hello(){
console.log("foo hello")
}
export class Person{
}
- 也可以統一導出,比如:
export { name , hello , Person}
- 在另一個模塊js文件要導入
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
}
重命名之后導入時也要注意名字變化
import { fooHello, fooName } from './module.js'
console.log(fooName)
fooHello()
重命名特殊情況
將導出成員名稱設置為default,這個成員就會被設置為當前模塊的默認導出成員,在導入的時候就必須要進行重命名
export {
name as default,
hello as fooHello,
Person as fooPerson
}
重命名default才能調用
import { fooHello, default as fooName } from './module.js'
ESM 關于針對default的特殊處理
將name變量設置為默認導出
export default name;
在導入的時候可以通過直接import + 變量名的方式接受默認導出的成員,變量名稱隨意
// fooName這里是可以隨意取名的
import fooName from './module.js'
ESM 導入導出的注意事項
- export 后面跟上的花括弧包裹的不是字面量,是固定語法
- 導入時的那些成員是分享的內存空間,是完全相同的引用關系
- 導入的成員是只讀的
ESM import用法
- 導入時from關鍵字后面跟的是字符串,內部的內容路徑必須要完整的文件名稱,不能省略js后綴名,跟CommonJS完全相反
- 也可以使用完整的url加載模塊,也就是說可以使用CDN上面的模塊,完整的
- 如果說只執行某個模塊的功能,不去提取模塊中的成員的話,可以保持花括弧為空,或者直接import跟上字符串,這個特性在我們導入一些不需要外界控制的子功能模塊時就非常有用了
import {} from './module.js'
import './module.js'
- 需要導出的成員特別多,導入時都會用到他們,就可以用*全部提取出來,使用as關鍵字全部存在對象里面
import * as mod from './module.js'
console.log(mod)
- 動態導入
import('./module.js').then(function (module) {
console.log(module)
})
- 默認成員和明明成員同時導出
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 直接導出導入成員
- 具體的做法就是將import關鍵詞修改為export,所有的導入成員會作為當前模塊的導出成員,在當前作用域下,也就不再可以訪問這些成員了。
一般用于index文件,把散落的模塊通過這種方式組織到一起,導出,方便外部使用
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(導入):
import { Button, Avatar } from './components/index.js'
console.log(Button)
console.log(Avatar)
avatar和button都是暴露了組件,index.js則是將這兩個組件導入,并且導出,作為一個橋梁的作用
ESM in Browser(Ployfill兼容方案)
- 讓瀏覽器支持ESM 的絕大特性
- 模塊名字為Browser ESM Loader
針對NPM下的模塊可以通過upkg這個網站的CDN服務來拿到所有的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的瀏覽器不去加載標簽內資源的問題
ESM in Node.js
- 在Node當中直接使用ESM ,要做的有:
-
第一,將文件的擴展名由 .js 改為 .mjs;
-
第二,啟動時需要額外添加
--experimental-modules
參數;
-
- 也可以用ESM 載入原生模塊
// // 此時我們也可以通過 esm 加載內置模塊了
import fs from 'fs'
fs.writeFileSync('./foo.txt', 'es module working')
- 也可以直接提取模塊內的成員,內置模塊兼容了ESM的提取成員的方式
import { writeFileSync } from 'fs'
writeFileSync('./bar.txt', 'es module working')
- 對于第三方的NPM模塊也可以通過ESM加載
(比如第三方模塊lodash)
import _ from 'lodash'
_.camelCase('ES Module')
- 但是不能使用ESM的花括弧方式去載入第三方模塊的成員
// // 不支持,因為第三方模塊都是導出默認成員
import { camelCase } from 'lodash'
console.log(camelCase('ES Module'))
ESM in Node.js 與 CommonJS模塊的交互
- CommonJS模塊始終只會導出一個默認成員
- ESM 中是可以導入CommonJS模塊的
- 不能直接提取成員,import不是解構導出對象
-
在CommonJS中通過require載入ESM 也是不可以的
ESM in Node.js與CommonJS的差異
在這之前先推薦使用nodemon工具,可以監聽mjs文件的變化并且給出錯誤信息
先用npm 進行全局安裝,再使用
- ESM中沒有模塊全局成員了
- require,module,exports自然是可以通過import和export代替
- __filename 和 __dirname 通過 import 對象的 meta 屬性獲取
const currentUrl = import.meta.url
console.log(currentUrl)
- 通過 url 模塊的 fileURLToPath 方法轉換為路徑
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文件就可以默認以ESM支持了
- 如果需要在 type=module 的情況下繼續使用 CommonJS, 需要將文件擴展名修改為 .cjs
Babel兼容方案
- 早期的node版本,可以使用Babel進行ESM的兼容
- 主流的JavaScript編譯器,可以將新特性的代碼編譯成當前環境支持的代碼
需要安裝babel一系列依賴
yarn add @babel/node @babel/core @babel/core @babel/preset-env --dev
-
檢測babel命令:
- 安裝插件
yarn add @babel/plugin-transform-commonjs --dev
- 建立一個.babelrc文件
{
"plugins": [
"@babel/plugin-transform-modules-commonjs"
]
}
- 運行文件
yarn babel-node .\index.js