早期的前端技術(shù)標(biāo)準(zhǔn)根本沒(méi)有預(yù)料到前端會(huì)有今天這樣的規(guī)模伐庭,所以很多設(shè)計(jì)會(huì)使我們?cè)陂_發(fā)過(guò)程中遇到很多模塊化的問(wèn)題望迎,雖然現(xiàn)如今基本上大部分都被我們后來(lái)所引用的各種標(biāo)準(zhǔn)所解決了,但是這個(gè)過(guò)程還是值得我們?nèi)ナ崂砹私庖幌碌摹?/p>
早期不使用工具和規(guī)范的情況下對(duì)模塊化的落地方式:
第一階段:文件劃分
我們?nèi)ゼs定每個(gè)文件代表一個(gè)獨(dú)立的模塊乏冀,并最終將其引入到html文件當(dāng)中劫哼,最后我們?cè)诤竺嬲{(diào)用模塊當(dāng)中的成員琉雳。
這種方式其實(shí)沒(méi)有從根本上解決模塊化想要解決的問(wèn)題,我們?cè)诙嗳碎_發(fā)過(guò)程中會(huì)遇到的全局變量污染友瘤,命名沖突等問(wèn)題并沒(méi)有得到解決翠肘,并且我們還是無(wú)法很好的去管理模塊與模塊間的依賴關(guān)系。早期引用模塊的數(shù)量少還可以辫秧,將來(lái)引用的模塊數(shù)量一多起來(lái)就很麻煩了束倍。
總而言之,這種方式還是需要主要依靠約定茶没。
第二階段:空間命名劃分
我們將每個(gè)模塊包裹為一個(gè)全局對(duì)象肌幽,相當(dāng)于我們?cè)诿總€(gè)模塊內(nèi)為每個(gè)模塊添加了命名空間
通過(guò)命名空間的方式可以一定程度上避免命名沖突和全局變量污染的問(wèn)題,但是問(wèn)題是我們每個(gè)模塊的內(nèi)部成員還是可以在外部直接訪問(wèn)或是修改抓半,這樣就會(huì)存在安全隱患崇裁,并且模塊間的依賴關(guān)系依然沒(méi)有得到解決亚皂。
第三階段:立即執(zhí)行函數(shù)
我們將每個(gè)模塊都放在函數(shù)提供的私有作用域當(dāng)中欢摄,對(duì)于需要暴露給外部的成員我們可以以掛在到全局對(duì)象上的方式去實(shí)現(xiàn)
這樣我們每個(gè)模塊的私有成員就都存在了閉包當(dāng)中扭粱,從外部無(wú)法直接訪問(wèn),這樣就保證了我們私有變量的安全探入。
并且我們還可以將依賴模塊作為立即執(zhí)行函數(shù)的參數(shù)來(lái)傳入狡孔,實(shí)現(xiàn)讓模塊間的依賴關(guān)系更加明確。
以上三個(gè)階段的方式都是以原始的模塊系統(tǒng)為基礎(chǔ)蜂嗽,通過(guò)約定化的方式去實(shí)現(xiàn)的代碼組織苗膝,這些方式在不同開發(fā)者去實(shí)現(xiàn)的過(guò)程中會(huì)有細(xì)微的差別。
為了統(tǒng)一不同開發(fā)者和不同項(xiàng)目之間的差異植旧,我們就需要一個(gè)標(biāo)準(zhǔn)去規(guī)范模塊化的實(shí)現(xiàn)方式辱揭。
并且所有的js文件和依賴都需要在html文件里統(tǒng)一以script標(biāo)簽的方式引入,在代碼量越來(lái)越大病附,需要的依賴越來(lái)越多的后期維護(hù)也是問(wèn)題问窃。
因此我們需要一個(gè)通用的模塊化標(biāo)準(zhǔn) 和 使用代碼來(lái)實(shí)現(xiàn)的模塊加載器。
模塊化規(guī)范
CommonJS規(guī)范
由Node.js提供的模塊化規(guī)范完沪,他規(guī)定了:
- 一個(gè)文件就是一個(gè)模塊
- 每個(gè)模塊都有單獨(dú)的作用域
- 通過(guò)module.exports導(dǎo)出成員
- 通過(guò)require函數(shù) 載入模塊成員
現(xiàn)如今的前端開發(fā)者應(yīng)該都非常熟悉這個(gè)規(guī)范域庇,但是這個(gè)規(guī)范適用于node端,在瀏覽器端直接使用是有問(wèn)題的覆积。
CommonJS約定是以同步的方式去加載模塊听皿,node的執(zhí)行機(jī)制是在啟動(dòng)時(shí)加載模塊,執(zhí)行過(guò)程中實(shí)用模塊而不需要去加載模塊宽档,因此在node端運(yùn)行是沒(méi)有問(wèn)題的写穴。
而在瀏覽器端,必然導(dǎo)致效率低下雌贱,每次頁(yè)面加載都會(huì)導(dǎo)致大量的同步請(qǐng)求出現(xiàn)啊送,因此早期的瀏覽器規(guī)范并沒(méi)有選擇CommonJS規(guī)范,而是專門設(shè)計(jì)了一個(gè)AMD規(guī)范欣孤。
AMD規(guī)范(Asynchronous Module Definition)
翻譯過(guò)來(lái)意思就是異步的模塊定義規(guī)范馋没,并且同期還推出了一個(gè)庫(kù),Require.js降传,它實(shí)現(xiàn)了AMD規(guī)范篷朵,它本身又是一個(gè)強(qiáng)大的模塊加載器。
舉個(gè)例子婆排,引用require.js使用AMD規(guī)范大概的文件結(jié)構(gòu)和過(guò)程如下:
main.js:
入口文件
require.js內(nèi)部提供require函數(shù)幫我們引入模塊声旺,用法和define類似。每次執(zhí)行require函數(shù)其實(shí)內(nèi)部就是幫我們創(chuàng)建一個(gè)script標(biāo)簽來(lái)幫我們執(zhí)行內(nèi)部的代碼段只。
require.config({
paths: {
// 因?yàn)?jQuery 中定義的是一個(gè)名為 jquery 的 AMD 模塊
// 所以使用時(shí)必須通過(guò) 'jquery' 這個(gè)名稱獲取這個(gè)模塊
// 但是 jQuery.js 并不一定在同級(jí)目錄下腮猖,所以需要指定路徑
jquery: './lib/jquery'
}
})
require(['./modules/module1'], function (module1) {
module1.start()
})
/modules/module1.js
定義模塊,內(nèi)部提供define函數(shù)赞枕,幫助我們定義模塊澈缺。我們定義模塊的時(shí)候接受三個(gè)參數(shù),分別是:模塊名炕婶,[依賴1姐赡,依賴2],function(依賴1形參柠掂,依賴2形參){模塊函數(shù)私有空間; return 對(duì)外暴露成員}
// 因?yàn)?jQuery 中定義的是一個(gè)名為 jquery 的 AMD 模塊
// 所以使用時(shí)必須通過(guò) 'jquery' 這個(gè)名稱獲取這個(gè)模塊
// 但是 jQuery.js 并不在同級(jí)目錄下项滑,所以需要指定路徑
define('module1', ['jquery', './module2'], function ($, module2) {
return {
start: function () {
$('body').animate({ margin: '200px' })
module2()
}
}
})
AMD社區(qū)生態(tài)可以說(shuō)相對(duì)完善了,但AMD使用過(guò)程中可能也存在一定的問(wèn)題:
- AMD使用起來(lái)相對(duì)復(fù)雜一點(diǎn)
- 模塊JS文件請(qǐng)求頻繁
總體來(lái)看涯贞,AMD規(guī)范算是前端模塊化的一個(gè)中間產(chǎn)物枪狂,并不能算是一個(gè)最終方案。更像是一個(gè)這種妥協(xié)的產(chǎn)物肩狂。除此之外還有同期出現(xiàn)的由taobao推出的Sea.js庫(kù)摘完,它實(shí)現(xiàn)的標(biāo)準(zhǔn)是由淘寶官方提出的CMD規(guī)范,全稱是Common Module Definition傻谁,他的規(guī)范類似于CommonJS孝治,在使用上類似于AMD規(guī)范,他想實(shí)現(xiàn)的目的是讓我們?cè)谑褂肅MD的同時(shí)寫出的代碼與CommonJS類似审磁,從而減輕開發(fā)者的學(xué)習(xí)成本谈飒,但是最后這種寫法后來(lái)被require.js也兼容了。
// 兼容 CMD 規(guī)范(類似 CommonJS 規(guī)范)
define(function (require, exports, module) {
// 通過(guò) require 引入依賴
var $ = require('jquery')
// 通過(guò) exports 或者 module.exports 對(duì)外暴露成員
module.exports = function () {
console.log('module 2~')
$('body').append('<p>module2</p>')
}
})
目前:ES Modules(瀏覽器) + CommonJS(node)
在這之前我們大概了解了前端模塊化發(fā)展的大致過(guò)程态蒂,盡管之前的標(biāo)準(zhǔn)基本都實(shí)現(xiàn)了模塊化杭措,但或多或少都會(huì)有一些讓開發(fā)者不太容易接受的問(wèn)題,隨著技術(shù)的發(fā)展钾恢,js的標(biāo)準(zhǔn)也在逐步完善∈炙兀現(xiàn)如今的模塊化可以說(shuō)已經(jīng)比較成熟了鸳址,目前開發(fā)者對(duì)于前段模塊化的實(shí)現(xiàn)方式也已經(jīng)基本統(tǒng)一。
- 在node環(huán)境中使用CommonJS規(guī)范
- 在瀏覽器環(huán)境中使用ES Modules規(guī)范
當(dāng)然泉懦,也會(huì)有部分其他情況出現(xiàn)稿黍,但是就主流而言,目前前端基本已經(jīng)統(tǒng)一成了這兩種規(guī)范崩哩,因此對(duì)于目前的前端開發(fā)者而言巡球,就模塊化規(guī)范來(lái)講,著重來(lái)掌握這兩種規(guī)范就好邓嘹。
CommonJS in Node.js
這里其實(shí)沒(méi)什么好說(shuō)的酣栈,CommonJS是node的內(nèi)置的模塊系統(tǒng),在node環(huán)境下執(zhí)行不存在任何環(huán)境問(wèn)題汹押,通過(guò)require去載入模塊矿筝,通過(guò)module.exports去導(dǎo)出模塊。
ES Modules in Browser
ES Modules是ECMAScript2015(ES6)定義的一個(gè)新的模塊系統(tǒng)鲸阻,也就是說(shuō)這是最近幾年才被定義的一個(gè)標(biāo)準(zhǔn)跋涣,他會(huì)存在很多兼容性的問(wèn)題。在這個(gè)標(biāo)準(zhǔn)剛出現(xiàn)的時(shí)候鸟悴,幾乎所有瀏覽器都不支持這個(gè)新特性陈辱。但后來(lái)隨著webpack等各種打包工具的流行,這個(gè)規(guī)范才逐漸開始普及细诸,截至目前沛贪。ES Modules可以說(shuō)是目前最主流的前端模塊化方案。
相比于AMD這種由社區(qū)提出的模塊化開發(fā)規(guī)范震贵,ES Modules相當(dāng)于是在語(yǔ)言層面實(shí)現(xiàn)了模塊化利赋,因此更為完善。并且現(xiàn)如今絕大多數(shù)瀏覽器已經(jīng)開始支持ES Modules特性了:
原生支持猩系,就意味著我們可以在開發(fā)中直接使用媚送,這就意味著將來(lái)會(huì)越來(lái)越普及。
原生是如何支持的:
引入時(shí)在script標(biāo)簽上添加type="module"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>ES Module - 模塊的特性</title>
</head>
<body>
<!-- 通過(guò)給 script 添加 type = module 的屬性寇甸,就可以以 ES Module 的標(biāo)準(zhǔn)執(zhí)行其中的 JS 代碼了 -->
<script type="module">
console.log('this is es module')
</script>
<!-- 1. ESM 自動(dòng)采用嚴(yán)格模式塘偎,忽略 'use strict' -->
<script type="module">
console.log(this)
</script>
<!-- 2. 每個(gè) ES Module 都是運(yùn)行在單獨(dú)的私有作用域中 -->
<script type="module">
var foo = 100
console.log(foo)
</script>
<script type="module">
console.log(foo)
</script>
<!-- 3. ESM 是通過(guò) CORS 的方式請(qǐng)求外部 JS 模塊的 -->
<!-- <script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script> -->
<!-- 4. ESM 的 script 標(biāo)簽會(huì)延遲執(zhí)行腳本 -->
<script defer src="demo.js"></script>
<p>需要顯示的內(nèi)容</p>
</body>
</html>
通過(guò)import導(dǎo)入模塊,export導(dǎo)出模塊
// import { default as fooName } from './module.js'
// console.log(fooName)
import { name, hello, Person } from './module.js'
console.log(name, hello, Person)
// export var name = 'foo module'
// export function hello () {
// console.log('hello')
// }
// export class Person {}
var name = 'foo module'
function hello () {
console.log('hello')
}
class Person {}
// export { name, hello, Person }
// export {
// // name as default,
// hello as fooHello
// }
// export default name
// var obj = { name, hello, Person }
export { name, hello, Person }
這里注意:
通過(guò)import引入的變量其實(shí)和引用文件export導(dǎo)出的變量是一個(gè)地址拿霉,而非復(fù)制吟秩,即修改了export導(dǎo)出的變量,另一邊通過(guò)import引入的變量也將法神改變绽淘。
-
import引入的變量一般情況下是只讀的涵防,我們不可以在引用后進(jìn)行手動(dòng)修改,這樣也避免了訪問(wèn)和改變其他模塊內(nèi)部變量的安全問(wèn)題沪铭。
ES Modules基本特性:
- 自動(dòng)采用嚴(yán)格模式壮池,忽略'use strict'
- 每個(gè)ESM都是單獨(dú)的私有作用域
- ESM是通過(guò)CORS去請(qǐng)求外部JS模塊的
- ESM的script標(biāo)簽都會(huì)延遲執(zhí)行腳本偏瓤,類似于defer屬性。