JS模塊化以及相關規(guī)范
1.模塊化概念
隨著前端應用日趨復雜,項目代碼也大量膨脹,模塊化就是一種最主流的代碼組織方式,一個模塊就是一個實現(xiàn)特定功能的文件,它通過把我們的復雜代碼按照功能的不同,劃分為不同的模塊單獨維護的這種方式,去提高我們的開發(fā)效率,降低維護成本。要用什么功能就加載什么模塊,模塊化開發(fā)是當下最重要的前端開發(fā)范式之一,其只是思想,不包含具體實現(xiàn)。
2.模塊化開發(fā)的優(yōu)點
- 避免變量污染、命名沖突等問題
- 提高代碼復用率
- 提高可維護性
- 能夠進行依賴關系的管理
3.模塊化演變過程
1.文件劃分方式
將每個功能和相關的一些狀態(tài)數(shù)據(jù)單獨存放在不同的文件當中,此時一個文件就是一個獨立的模塊。
然后將這個模塊引入頁面當中,直接調(diào)用模塊中的成員(變量/函數(shù)),一個script標簽就對應一個模塊,所有模塊都在全局范圍內(nèi)工作。
例:
html文件:
<script type='text/javascript' src='module1.js'></script> <!-- 模塊1 -->
<script type='text/javascript' src='module2.js'></script> <!-- 模塊2 -->
<script type='text/javascript' src='module3.js'></script> <!-- 模塊3 -->
<script type='text/javascript'>
foo();
bar();
msg='NBA'; //會污染全局變量
foo();
</script>
js文件:
/**
* 全局函數(shù)模式: 將不同的功能封裝成不同的全局函數(shù)
* 問題: Global被污染了, 很容易引起命名沖突
*/
let msg = 'modulel'
function foo() {
console.log('foo()', msg);
}
function bar() {
console.log('bar()', msg);
}
這種方式的缺點很明顯,即:
- 各模塊內(nèi)部的成員都處在全局作用域中,即任意位置均可進行訪問和修改,這樣就會污染全局作用域。
- 容易出現(xiàn)命名沖突。
- 無法很好地管理各模塊之間的依賴關系。
2.命名空間(namespace)方式
命名空間方式是指:在文件劃分方式的基礎上,約定每個模塊只暴露一個對象,并將該模塊中的所有成員封裝在該對象中。當需要使用的時候,就調(diào)用這個對象的屬性即可。
例如:
module_a.js
let moduleA = {
name: '一碗周',
handle() {
console.log(this.name)
},
}
module_b.js
let moduleB = {
name: '一碗粥',
handle() {
console.log(this.name)
},
}
html文件:
<body>
<script src="./component/module_a.js"></script>
<script src="./component/module_b.js"></script>
<script>
console.log(moduleA.name);
console.log(moduleB.name); //仍然可以訪問到模塊中的所有屬性
moduleA.handle()
moduleB.handle()
</script>
</body>
所以,這種方法實際上就是簡單的對象封裝。
這種方式減少了命名沖突的可能,但是各模塊中仍然沒有私有空間,而且也沒有解決管理模塊依賴關系的問題。
3.IIFE(立即執(zhí)行函數(shù))
所謂的IIFE模式就是使用立即執(zhí)行函數(shù)去創(chuàng)建閉包,這種方式為模塊提供了私有空間。
具體的做法就是:
將模塊中每一個成員都放在一個函數(shù)提供的私有作用域當中,對于需要暴露給外部的成員可以通過掛載到全局對象上的方式去實現(xiàn)。
這種方式實現(xiàn)了私有成員的概念,就是說模塊的私有成員只能在模塊內(nèi)部通過閉包的方式去訪問。而在外部,是沒有辦法去使用的。這樣就確保了私有成員的安全。
例:
module_a.js
(function () {
let name = '一碗周'
function handle() {
console.log(name)
}
window.moduleA = { handle } //向window暴露handle對象,從而形成閉包
})()
module_b.js
(function () {
let name = '一碗粥'
function handle() {
console.log(name)
}
window.moduleB = { handle }
})()
html文件如下:
<body>
<script src="./component/module_a.js"></script>
<script src="./component/module_b.js"></script>
<script>
console.log(moduleA.name) // undefined
console.log(moduleB.name) // undefined,說明無法訪問到這個屬性,即實現(xiàn)了私有變量的效果
moduleA.handle()
moduleB.handle()
</script>
</body>
在這一階段,實現(xiàn)了私有成員的概念,但仍未解決模塊間的依賴關系問題。
4.IIFE模式增強
這一階段,在 IIFE 模式的基礎上,通過為立即執(zhí)行函數(shù)添加參數(shù)的形式,實現(xiàn)模塊間的依賴。
例如:
module_a.js
(function () {
function printName(name) {
console.log(name)
}
// 暴露一個打印的方法
window.moduleA = { printName }
})()
module_b.js
(function (m) /* 形參 */ {
let name = '一碗周'
function sayName() {
// 使用其他模塊的成員
m.printName(name)
}
window.moduleB = { sayName }
})(moduleA) // 實參
html文件:
<body>
<script src="./component/module_a.js"></script>
<script src="./component/module_b.js"></script>
<script>
moduleB.sayName() // 一碗周,即實現(xiàn)了模塊之間的依賴
</script>
</body>
但這種方式仍然存在問題:
- 引入了過多
<script>
標簽,就需要發(fā)送多個請求,請求數(shù)量太多 - 依賴關系模糊
- 難以維護
下面來介紹兩種現(xiàn)在開發(fā)過程中常使用的模塊化規(guī)范:
4.常用模塊化規(guī)范 — CommonJS
CommonJS在Node.js中廣泛應用,Node.js是CommonJS的實踐者。
CommonJS規(guī)范指出一個單獨的文件就是一個模塊,它采用的是同步加載模塊,也就是說模塊加載的順序就是代碼中編寫的順序是一致的,而加載的文件資源大多數(shù)都存儲在服務器中,所以說加載速度沒有什么問題。
但是這種方案不適用與瀏覽器端,由于網(wǎng)絡原因,更合理的方案是采用異步加載(CMD、AMD和ESmodule)。
4.1 CommonJS的基本語法
暴露模塊使用module.exports
,或者直接使用exports
,引入模塊直接使用require()
方法,示例代碼如下:
module_c.js
let name = '一碗周'
module.exports = {
name,
getName() {
return name
},
setName(n) {
name = n
},
}
index.js
// 引入自定義的模塊
const person = require('./module_c')
// 引入 Node.js 提供的模塊
const fs = require('fs')
console.log(person.getName()) // 一碗周
person.setName('一碗粥')
console.log(person.name) // 一碗周
console.log(person.getName()) // 一碗粥
4.2 CommonJS的模塊加載機制
在上面的代碼中,首先通過 module.exports
導出一個對象,其中包含一個屬性兩個方法。然后在index.js
中引入該模塊,通過require()
方法引入模塊并定義一個變量來接收這個模塊。
但需要注意的是,CommonJS的模塊加載機制是被輸出值的拷貝 ,也就是說一旦輸出了某個值,即使模塊內(nèi)的數(shù)據(jù)變化,也不會影響這個值了!
上面的代碼中通過setName()
重新為name
進行賦值,在賦值后拿到的結(jié)果還是初始值,這是因為name
是一個原始類型的值,它的值會被緩存。
當我們通過getName()方法
來方法name
的值才可以獲取到?jīng)]有緩存的那個結(jié)果。
5.AMD和CMD
AMD是"Asynchronous Module Definition "的縮寫。AMD規(guī)范的最佳實踐者是require.js。
CMD規(guī)范是在sea.js推廣中形成的,與AMD類似,不同點在于:AMD 推崇依賴前置、提前執(zhí)行,CMD推崇依賴就近、延遲執(zhí)行。
這里不對這兩者做介紹…
6.ES Module
6.1 ES Module 的語法特性
如果想要在HTML中使用使用ES Module的話,需要為<script>
標簽添加一個type="module"
的屬性,然后就可以執(zhí)行其中的JS代碼。
ES Module有主要以下幾個特性:
- 自動全部采用嚴格模式,自動忽略
'use strict'
- 每個ES Module都會運行在單獨的私有作用域中
- ES Module是通過CORS的方式請求外部JavaScript模塊的
-
ES Module的
<script>
標簽會自動延遲執(zhí)行腳本,相當于加了defer
屬性,網(wǎng)頁對默認的<script>
標簽采用的是立即執(zhí)行的機制,頁面的渲染會等待這個腳本執(zhí)行完成才會往下渲染
6.2 ES Module 的導入和導出
導出成員可以通過export
導出具體成員,也可以通過export default
導出默認成員,示例代碼如下:
module_e.js
// 導出單個成員
export let name = '一碗周'
// 導出默認成員
export default function sayMe() {
console.log('一碗周')
}
// 批量導出成員
// export { name, sayMe }
值得注意的是,批量導出成員的寫法并不是導出為一個對象,而是固定的語法,導出得到的是多個成員,導出多個成員必須使用花括號包裹!
想要導出對象,可以使用默認語法,示例代碼如下:
export default { name, sayMe }
這樣獲得的就是一個對象,其中有兩個屬性。
要注意的是:使用ES Module導出成員,導出的是值的引用 ,也就是說如果模塊內(nèi)部的成員發(fā)生改變,所有引用該模塊的地方都會發(fā)生改變。
導入成員使用import
關鍵字導入,如下代碼展示了如何導入一個ES Module模塊,示例代碼如下:
// 導入默認成員
// import sayMe from './module_e.js'
// 或者通過 as 關鍵字對導入的默認成員進行重命名
// import { default as sayMe } from './module_e.js'
// 導入指定成員
// import { name } from './module_e.js'
// 也可以將上面兩行合并為1行,示例代碼如下:
// import { default as sayMe, name } from './module_e.js'
// 或者簡寫如下:
import sayMe, { name } from './module_e.js'
sayMe()
console.log(name)
但是,我們無法修改導入的成員的值,如果修改則會拋出異常!
import sayMe, { name } from './module_e.js'
name = '1'
異常信息為Uncaught TypeError: Assignment to constant variable.
如果我們只想要執(zhí)行某個模塊,并不需要模塊內(nèi)部的成員,可以直接通過import
關鍵字引入即可。
如果我們想要動態(tài)的引入某個成員,可以將import()
當做一個函數(shù)來使用,示例代碼如下:
import('./module.js').then(res=>{
// res 表示模塊的默認導出成員
})
我們可以將導入的模塊直接導出,示例代碼如下:文章來源:http://www.zghlxwxcb.cn/news/detail-790809.html
export { name } from './module.js'
總結(jié)
總的來說,如今模塊化已經(jīng)成為了前端開發(fā)者的必備技能了。文章來源地址http://www.zghlxwxcb.cn/news/detail-790809.html
到了這里,關于【前端模塊化】JS模塊化思想以及相關規(guī)范(CommonJS、ES module)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!