模塊化是一種主流的代碼組織方式,是一種思想,它將代碼依據不同的功能分成不同的模塊來提高開發效率,降低維護成本。
模塊化的演變
- stage1-文件劃分方式
// 具體做法就是將每個功能及其相關狀態數據各自單獨放到不同的文件中,
// 約定每個文件就是一個獨立的模塊,
// 使用某個模塊就是將這個模塊引入到頁面中,然后直接調用模塊中的成員(變量 / 函數)
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
// 命名沖突
method1()
// 模塊成員可以被修改
name = 'foo'
</script>
// module-a.js
var name = 'module-a'
function method1 () {
console.log(name + '#method1')
}
function method2 () {
console.log(name + '#method2')
}
缺點十分明顯:
污染全局變量
容易產生命名沖突
無法管理模塊與模塊之間的依賴關系
完全依靠約定,項目一旦上了體量就不行了。
- stage2-命名空間方式
// 具體做法就是在第一階段的基礎上,通過將每個模塊「包裹」為一個全局對象的形式實現,
// 有點類似于為模塊內的成員添加了「命名空間」的感覺。
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
// 模塊成員可以被修改
moduleA.name = 'foo'
</script>
// module-a.js
var moduleA = {
name: 'module-a',
method1: function () {
console.log(this.name + '#method1')
},
method2: function () {
console.log(this.name + '#method2')
}
}
通過「命名空間」減小了命名沖突的可能,
但是同樣沒有私有空間,所有模塊成員也可以在模塊外部被訪問或者修改,而且也無法管理模塊之間的依賴關系。
- stage3-使用立即執行函數
// 使用立即執行函數表達式為模塊提供私有空間
// 具體做法就是將每個模塊成員都放在一個函數提供的私有作用域中,
// 對于需要暴露給外部的成員,通過掛在到全局對象上的方式實現
// 還利用立即執行函數的參數傳遞模塊依賴項。
<script src="https://unpkg.com/jquery"></script>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
// 模塊私有成員無法訪問
console.log(moduleA.name) // => undefined
</script>
// module-a.js
(function ($) {
var name = 'module-a'
function method1 () {
console.log(name + '#method1')
$('body').animate({ margin: '200px' })
}
function method2 () {
console.log(name + '#method2')
}
window.moduleA = {
method1: method1,
method2: method2
}
})(jQuery)
有了私有成員的概念,私有成員只能在模塊成員內通過閉包的形式訪問。
參數傳遞模塊依賴項,使得模塊之間的關系變得更加明顯
- stage4-Require.js 提供了 AMD 模塊化規范
上面的方式還有問題就是,需要手動的維護引入模塊
// 如果不需要module-a的方法了,還要記得刪掉src="module-a.js"
<script src="https://unpkg.com/jquery"></script>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
所以我們還需要一段公共的代碼,通過代碼,去自動加載模塊就更好了
模塊化規范的出現
模塊化標準:commonJS規范,它是Node提出的一套標準,在nodejs當中,所有的模塊必須要遵循commonJS規范。
commonJS約定了:
- 一個文件就是一個模塊
- 每個模塊都有單獨的作用域
- 通過module.exports導出成員
- 通過require函數載入模塊
commonJS是以同步方式加載模塊,啟動時全部引入,它是適用于Node的,Node的機制就是在啟動時加載模塊,執行過程是不需要去加載的,它只會使用到模塊。
但是在瀏覽器中并不適用,它必然導致效率低下,因為每一次頁面加載,都會有大量的同步任務出現,所以早期的前端模塊化并沒有選擇commonJS,而是專門為瀏覽器端,結合瀏覽器的特點,重新設計了一個規范,AMD(Asynchronous Module Definition)異步的模塊定義規范,然后出了一個很出名的庫,Require.js。
Require.js 還有加載模塊的功能,所以它就是「模塊化標準 + 模塊加載器」
// 定義一個模塊
// module1 模塊名字
// ['jquery', './module2'] 依賴模塊
// function 導出函數
define('module1', ['jquery', './module2'], function ($, module2) {
return {
start: function () {
$('body').animate({ margin: '200px' })
module2()
}
}
})
// 引入模塊并使用
require(['./modules/module1'], function (module1) {
module1.start()
})
// 原理也是創建script標簽,通過src引入
但是,AMD使用起來相對復雜,除了業務代碼還要寫大量的Require.js。
我覺得這些歷史對于在和平時期才接觸前端的我們很重要。
模塊化標準規范
現如今前端的模塊化已經基本統一了。
瀏覽器端:ES Modules(ES2015中定義的模塊系統)
Node端:CommonJS
ES Modules在剛出來時沒有任何瀏覽器支持,隨之webpack等打包工具的出現,它才隨之流行開來,目前來說ES Modules是最主流的前端模塊化方案了,相比AMD這種社區提出來的開發規范,ES Modules可以說是語言層面上實現了模塊化,現在已經有部分瀏覽器直接支持ES Modules了。
ES Modules
基本特性:
<body>
<!-- 通過給 script 添加 type = module 的屬性,就可以以 ES Module 的標準執行其中的 JS 代碼了 -->
<script type="module">
console.log('this is es module')
</script>
<!-- 1. ESM 自動采用嚴格模式,忽略 'use strict' -->
<script type="module">
console.log(this)
</script>
<!-- 2. 每個 ES Module 都是運行在單獨的私有作用域中 -->
<script type="module">
var foo = 100
console.log(foo)
</script>
<script type="module">
console.log(foo)
</script>
<!-- 3. ESM 是通過 CORS 的方式請求外部 JS 模塊的 -->
<!-- <script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script> -->
<!-- 4. ESM 的 script 標簽會延遲執行腳本,相當于defer屬性 -->
<script type="module" defer src="demo.js"></script>
<p>需要顯示的內容</p>
</body>
導入導出:
// a.js導出
var name = 'foo module'
class Person
export { name, Person }
// b.js導入
import { name, hello, Person } from './module.js'
// a.js導出
var name = 'foo module'
export default name
// b.js導入
import name from './module.js'
注意
1.導出是將值的引用關系導出,不是完全復制了一份。比如我將a.js中name在1s后又改成abc,b.js中1.5s后打印name也會變成abc。
2.導出的成員是一個只讀的,并不能在模塊的外部修改,比如在b.js中 name = 123會報錯
// import其他常用用法
import './module.js' // 只執行模塊,不用提取成員
// 動態導入
import ('./module.js').then((module)=>{}) // 執行完模塊后返回promise
// 支持cdn導入
import { name } from 'http://localhost:3000/04-import/module.js'
兼容性改善,Polyfill
ES Module in Node
node 8.5版本后,開始支持ES Module
// 第一,將文件的擴展名由 .js 改為 .mjs;
// 第二,啟動時需要額外添加 `--experimental-modules` 參數;
import { foo, bar } from './module.mjs'
console.log(foo, bar)
// 此時我們也可以通過 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 加載
import _ from 'lodash'
ES Module 與 commonJS
- ES Module 中可以導入 CommonJS 模塊
- 不能在 CommonJS 模塊中通過 require 載入 ES Module
- CommonJS始終只會導出一個默認成員
- 不能直接提取成員,注意 import 不是解構導出對象
有人說可以在CommonJS中require 載入 ES Module,是因為你的webpack幫你編譯了。
ES Module 與 commonJS的區別
// cjs.js
// 當前文件的絕對路徑
console.log(__filename)
// 當前文件所在目錄
console.log(__dirname)
// =>/Users/.../.cjs.js
// =>/Users/.../differences
// 都是可以輸出的,因為在commonJS中__filename相當于全局變量
// 其實是commonJS把這些包裝成函數后,通過參數提供的成員
// esm.mjs
console.log(__filename) // 報錯
// 解決
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
console.log(__filename)
console.log(__dirname)