前言在JavaScript發(fā)展初期就是為了實現(xiàn)簡單的頁面交互邏輯,寥寥數(shù)語即可;如今CPU、瀏覽器性能得到了極大的提升,很多頁面邏輯遷移到了客戶端(表單驗證等),隨著web2.0時代的到來,Ajax技術(shù)得到廣泛應(yīng)用,jQuery等前端庫層出不窮,前端代碼日益膨脹,此時在JS方面就會考慮使用模塊化規(guī)范去管理。 本文內(nèi)容主要有理解模塊化,為什么要模塊化,模塊化的優(yōu)缺點以及模塊化規(guī)范,并且介紹下開發(fā)中最流行的CommonJS, AMD, ES6、CMD規(guī)范。本文試圖站在小白的角度,用通俗易懂的筆調(diào)介紹這些枯燥無味的概念,希望諸君閱讀后,對模塊化編程有個全新的認識和理解!
 一、模塊化的理解1.什么是模塊?將一個復(fù)雜的程序依據(jù)一定的規(guī)則(規(guī)范)封裝成幾個塊(文件), 并進行組合在一起 塊的內(nèi)部數(shù)據(jù)與實現(xiàn)是私有的, 只是向外部暴露一些接口(方法)與外部其它模塊通信
2.模塊化的進化過程function m1(){
//...
}
function m2(){
//...
}let myModule = {
data: 'www.baidu.com',
foo() {
console.log(`foo() ${this.data}`)
},
bar() {
console.log(`bar() ${this.data}`)
}
}
myModule.data = 'other data' //能直接修改模塊內(nèi)部的數(shù)據(jù)
myModule.foo() // foo() other data這樣的寫法會暴露所有模塊成員,內(nèi)部狀態(tài)可以被外部改寫。 // index.html文件
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo()
myModule.bar()
console.log(myModule.data) //undefined 不能訪問模塊內(nèi)部數(shù)據(jù)
myModule.data = 'xxxx' //不是修改的模塊內(nèi)部的data
myModule.foo() //沒有改變
</script> // module.js文件
(function(window) {
let data = 'www.baidu.com'
//操作數(shù)據(jù)的函數(shù)
function foo() {
//用于暴露有函數(shù)
console.log(`foo() ${data}`)
}
function bar() {
//用于暴露有函數(shù)
console.log(`bar() ${data}`)
otherFun() //內(nèi)部調(diào)用
}
function otherFun() {
//內(nèi)部私有的函數(shù)
console.log('otherFun()')
}
//暴露行為
window.myModule = { foo, bar } //ES6寫法
})(window)最后得到的結(jié)果:
 這就是現(xiàn)代模塊實現(xiàn)的基石 // module.js文件
(function(window, $) {
let data = 'www.baidu.com'
//操作數(shù)據(jù)的函數(shù)
function foo() {
//用于暴露有函數(shù)
console.log(`foo() ${data}`)
$('body').css('background', 'red')
}
function bar() {
//用于暴露有函數(shù)
console.log(`bar() ${data}`)
otherFun() //內(nèi)部調(diào)用
}
function otherFun() {
//內(nèi)部私有的函數(shù)
console.log('otherFun()')
}
//暴露行為
window.myModule = { foo, bar }
})(window, jQuery) // index.html文件
<!-- 引入的js必須有一定順序 -->
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo()
</script> 上例子通過jquery方法將頁面的背景顏色改成紅色,所以必須先引入jQuery庫,就把這個庫當(dāng)作參數(shù)傳入。這樣做除了保證模塊的獨立性,還使得模塊之間的依賴關(guān)系變得明顯。 3. 模塊化的好處避免命名沖突(減少命名空間污染) 更好的分離, 按需加載 更高復(fù)用性 高可維護性
4. 引入多個<script>后出現(xiàn)出現(xiàn)問題首先我們要依賴多個模塊,那樣就會發(fā)送多個請求,導(dǎo)致請求過多 我們不知道他們的具體依賴關(guān)系是什么,也就是說很容易因為不了解他們之間的依賴關(guān)系導(dǎo)致加載先后順序出錯。 以上兩種原因就導(dǎo)致了很難維護,很可能出現(xiàn)牽一發(fā)而動全身的情況導(dǎo)致項目出現(xiàn)嚴重的問題。 模塊化固然有多個好處,然而一個頁面需要引入多個js文件,就會出現(xiàn)以上這些問題。而這些問題可以通過模塊化規(guī)范來解決,下面介紹開發(fā)中最流行的commonjs, AMD, ES6, CMD規(guī)范。 二、模塊化規(guī)范1.CommonJS(1)概述Node 應(yīng)用由模塊組成,采用 CommonJS 模塊規(guī)范。每個文件就是一個模塊,有自己的作用域。在一個文件里面定義的變量、函數(shù)、類,都是私有的,對其他文件不可見。在服務(wù)器端,模塊的加載是運行時同步加載的;在瀏覽器端,模塊需要提前編譯打包處理。 (2)特點(3)基本語法暴露模塊:module.exports = value或exports.xxx = value 引入模塊:require(xxx),如果是第三方模塊,xxx為模塊名;如果是自定義模塊,xxx為模塊文件路徑
此處我們有個疑問:CommonJS暴露的模塊到底是什么? CommonJS規(guī)范規(guī)定,每個模塊內(nèi)部,module變量代表當(dāng)前模塊。這個變量是一個對象,它的exports屬性(即module.exports)是對外的接口。加載某個模塊,其實是加載該模塊的module.exports屬性。 // example.js
var x = 5;
var addX = function (value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;上面代碼通過module.exports輸出變量x和函數(shù)addX。 var example = require('./example.js');//如果參數(shù)字符串以“./”開頭,則表示加載的是一個位于相對路徑
console.log(example.x); // 5
console.log(example.addX(1)); // 6require命令用于加載模塊文件。require命令的基本功能是,讀入并執(zhí)行一個JavaScript文件,然后返回該模塊的exports對象。如果沒有發(fā)現(xiàn)指定模塊,會報錯。 (4)模塊的加載機制CommonJS模塊的加載機制是,輸入的是被輸出的值的拷貝。也就是說,一旦輸出一個值,模塊內(nèi)部的變化就影響不到這個值。這點與ES6模塊化有重大差異(下文會介紹),請看下面這個例子: // lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};上面代碼輸出內(nèi)部變量counter和改寫這個變量的內(nèi)部方法incCounter。 // main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;
console.log(counter); // 3
incCounter();
console.log(counter); // 3上面代碼說明,counter輸出以后,lib.js模塊內(nèi)部的變化就影響不到counter了。這是因為counter是一個原始類型的值,會被緩存。除非寫成一個函數(shù),才能得到內(nèi)部變動后的值。 (5)服務(wù)器端實現(xiàn)①下載安裝node.js②創(chuàng)建項目結(jié)構(gòu)注意:用npm init 自動生成package.json時,package name(包名)不能有中文和大寫 |-modules
|-module1.js
|-module2.js
|-module3.js
|-app.js
|-package.json
{
"name": "commonJS-node",
"version": "1.0.0"
}③下載第三方模塊npm install uniq --save // 用于數(shù)組去重
④定義模塊代碼//module1.js
module.exports = {
msg: 'module1',
foo() {
console.log(this.msg)
}
}//module2.js
module.exports = function() {
console.log('module2')
}//module3.js
exports.foo = function() {
console.log('foo() module3')
}
exports.arr = [1, 2, 3, 3, 2]// app.js文件
// 引入第三方庫,應(yīng)該放置在最前面
let uniq = require('uniq')
let module1 = require('./modules/module1')
let module2 = require('./modules/module2')
let module3 = require('./modules/module3')
module1.foo() //module1
module2() //module2
module3.foo() //foo() module3
console.log(uniq(module3.arr)) //[ 1, 2, 3 ]⑤通過node運行app.js命令行輸入node app.js,運行JS文件 (6)瀏覽器端實現(xiàn)(借助Browserify)①創(chuàng)建項目結(jié)構(gòu)|-js
|-dist //打包生成文件的目錄
|-src //源碼所在的目錄
|-module1.js
|-module2.js
|-module3.js
|-app.js //應(yīng)用主源文件
|-index.html //運行于瀏覽器上
|-package.json
{
"name": "browserify-test",
"version": "1.0.0"
}②下載browserify③定義模塊代碼(同服務(wù)器端)注意:index.html文件要運行在瀏覽器上,需要借助browserify將app.js文件打包編譯,如果直接在index.html引入app.js就會報錯! ④打包處理js根目錄下運行browserify js/src/app.js -o js/dist/bundle.js ⑤頁面使用引入在index.html文件中引入<script type="text/javascript" src="js/dist/bundle.js"></script> 2.AMDCommonJS規(guī)范加載模塊是同步的,也就是說,只有加載完成,才能執(zhí)行后面的操作。AMD規(guī)范則是非同步加載模塊,允許指定回調(diào)函數(shù)。由于Node.js主要用于服務(wù)器編程,模塊文件一般都已經(jīng)存在于本地硬盤,所以加載起來比較快,不用考慮非同步加載的方式,所以CommonJS規(guī)范比較適用。但是,如果是瀏覽器環(huán)境,要從服務(wù)器端加載模塊,這時就必須采用非同步模式,因此瀏覽器端一般采用AMD規(guī)范。此外AMD規(guī)范比CommonJS規(guī)范在瀏覽器端實現(xiàn)要來著早。 (1)AMD規(guī)范基本語法定義暴露模塊: //定義沒有依賴的模塊
define(function(){
return 模塊
})//定義有依賴的模塊
define(['module1', 'module2'], function(m1, m2){
return 模塊
})引入使用模塊: require(['module1', 'module2'], function(m1, m2){
使用m1/m2
})(2)未使用AMD規(guī)范與使用require.js通過比較兩者的實現(xiàn)方法,來說明使用AMD規(guī)范的好處。 // dataService.js文件
(function (window) {
let msg = 'www.baidu.com'
function getMsg() {
return msg.toUpperCase()
}
window.dataService = {getMsg}
})(window)// alerter.js文件
(function (window, dataService) {
let name = 'Tom'
function showMsg() {
alert(dataService.getMsg() + ', ' + name)
}
window.alerter = {showMsg}
})(window, dataService)// main.js文件
(function (alerter) {
alerter.showMsg()
})(alerter)// index.html文件
<div><h1>Modular Demo 1: 未使用AMD(require.js)</h1></div>
<script type="text/javascript" src="js/modules/dataService.js"></script>
<script type="text/javascript" src="js/modules/alerter.js"></script>
<script type="text/javascript" src="js/main.js"></script> 最后得到如下結(jié)果:
 這種方式缺點很明顯:首先會發(fā)送多個請求,其次引入的js文件順序不能搞錯,否則會報錯! RequireJS是一個工具庫,主要用于客戶端的模塊管理。它的模塊管理遵守AMD規(guī)范,RequireJS的基本思想是,通過define方法,將代碼定義為模塊;通過require方法,實現(xiàn)代碼的模塊加載。 接下來介紹AMD規(guī)范在瀏覽器實現(xiàn)的步驟: ①下載require.js, 并引入然后將require.js導(dǎo)入項目: js/libs/require.js ②創(chuàng)建項目結(jié)構(gòu)|-js
|-libs
|-require.js
|-modules
|-alerter.js
|-dataService.js
|-main.js
|-index.html ③定義require.js的模塊代碼// dataService.js文件
// 定義沒有依賴的模塊
define(function() {
let msg = 'www.baidu.com'
function getMsg() {
return msg.toUpperCase()
}
return { getMsg } // 暴露模塊
})//alerter.js文件
// 定義有依賴的模塊
define(['dataService'], function(dataService) {
let name = 'Tom'
function showMsg() {
alert(dataService.getMsg() + ', ' + name)
}
// 暴露模塊
return { showMsg }
})// main.js文件
(function() {
require.config({
baseUrl: 'js/', //基本路徑 出發(fā)點在根目錄下
paths: {
//映射: 模塊標(biāo)識名: 路徑
alerter: './modules/alerter', //此處不能寫成alerter.js,會報錯
dataService: './modules/dataService'
}
})
require(['alerter'], function(alerter) {
alerter.showMsg()
})
})()// index.html文件
<!DOCTYPE html>
<html>
<head>
<title>Modular Demo</title>
</head>
<body>
<!-- 引入require.js并指定js主文件的入口 -->
<script data-main="js/main" src="js/libs/require.js"></script>
</body>
</html> ④頁面引入require.js模塊:在index.html引入 <script data-main="js/main" src="js/libs/require.js"></script> 此外在項目中如何引入第三方庫?只需在上面代碼的基礎(chǔ)稍作修改: // alerter.js文件
define(['dataService', 'jquery'], function(dataService, $) {
let name = 'Tom'
function showMsg() {
alert(dataService.getMsg() + ', ' + name)
}
$('body').css('background', 'green')
// 暴露模塊
return { showMsg }
})// main.js文件
(function() {
require.config({
baseUrl: 'js/', //基本路徑 出發(fā)點在根目錄下
paths: {
//自定義模塊
alerter: './modules/alerter', //此處不能寫成alerter.js,會報錯
dataService: './modules/dataService',
// 第三方庫模塊
jquery: './libs/jquery-1.10.1' //注意:寫成jQuery會報錯
}
})
require(['alerter'], function(alerter) {
alerter.showMsg()
})
})()上例是在alerter.js文件中引入jQuery第三方庫,main.js文件也要有相應(yīng)的路徑配置。 小結(jié):通過兩者的比較,可以得出AMD模塊定義的方法非常清晰,不會污染全局環(huán)境,能夠清楚地顯示依賴關(guān)系。AMD模式可以用于瀏覽器環(huán)境,并且允許非同步加載模塊,也可以根據(jù)需要動態(tài)加載模塊。 3.CMDCMD規(guī)范專門用于瀏覽器端,模塊的加載是異步的,模塊使用時才會加載執(zhí)行。CMD規(guī)范整合了CommonJS和AMD規(guī)范的特點。在 Sea.js 中,所有 JavaScript 模塊都遵循 CMD模塊定義規(guī)范。 (1)CMD規(guī)范基本語法定義暴露模塊: //定義沒有依賴的模塊
define(function(require, exports, module){
exports.xxx = value
module.exports = value
})//定義有依賴的模塊
define(function(require, exports, module){
//引入依賴模塊(同步)
var module2 = require('./module2')
//引入依賴模塊(異步)
require.async('./module3', function (m3) {
})
//暴露模塊
exports.xxx = value
})引入使用模塊: define(function (require) {
var m1 = require('./module1')
var m4 = require('./module4')
m1.show()
m4.show()
})(2)sea.js簡單使用教程①下載sea.js, 并引入然后將sea.js導(dǎo)入項目: js/libs/sea.js ②創(chuàng)建項目結(jié)構(gòu)|-js
|-libs
|-sea.js
|-modules
|-module1.js
|-module2.js
|-module3.js
|-module4.js
|-main.js
|-index.html ③定義sea.js的模塊代碼// module1.js文件
define(function (require, exports, module) {
//內(nèi)部變量數(shù)據(jù)
var data = 'atguigu.com'
//內(nèi)部函數(shù)
function show() {
console.log('module1 show() ' + data)
}
//向外暴露
exports.show = show
})// module2.js文件
define(function (require, exports, module) {
module.exports = {
msg: 'I Will Back'
}
})// module3.js文件
define(function(require, exports, module) {
const API_KEY = 'abc123'
exports.API_KEY = API_KEY
})// module4.js文件
define(function (require, exports, module) {
//引入依賴模塊(同步)
var module2 = require('./module2')
function show() {
console.log('module4 show() ' + module2.msg)
}
exports.show = show
//引入依賴模塊(異步)
require.async('./module3', function (m3) {
console.log('異步引入依賴模塊3 ' + m3.API_KEY)
})
})// main.js文件
define(function (require) {
var m1 = require('./module1')
var m4 = require('./module4')
m1.show()
m4.show()
})④在index.html中引入<script type="text/javascript" src="js/libs/sea.js"></script>
<script type="text/javascript">
seajs.use('./js/modules/main')
</script>最后得到結(jié)果如下:
 4.ES6模塊化ES6 模塊的設(shè)計思想是盡量的靜態(tài)化,使得編譯時就能確定模塊的依賴關(guān)系,以及輸入和輸出的變量。CommonJS 和 AMD 模塊,都只能在運行時確定這些東西。比如,CommonJS 模塊就是對象,輸入時必須查找對象屬性。 (1)ES6模塊化語法export命令用于規(guī)定模塊的對外接口,import命令用于輸入其他模塊提供的功能。 /** 定義模塊 math.js **/
var basicNum = 0;
var add = function (a, b) {
return a + b;
};
export { basicNum, add };
/** 引用模塊 **/
import { basicNum, add } from './math';
function test(ele) {
ele.textContent = add(99 + basicNum);
}如上例所示,使用import命令的時候,用戶需要知道所要加載的變量名或函數(shù)名,否則無法加載。為了給用戶提供方便,讓他們不用閱讀文檔就能加載模塊,就要用到export default命令,為模塊指定默認輸出。 // export-default.js
export default function () {
console.log('foo');
}// import-default.js
import customName from './export-default';
customName(); // 'foo' 模塊默認輸出, 其他模塊加載該模塊時,import命令可以為該匿名函數(shù)指定任意名字。 (2)ES6 模塊與 CommonJS 模塊的差異它們有兩個重大差異: ① CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。 ② CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。 第二個差異是因為 CommonJS 加載的是一個對象(即module.exports屬性),該對象只有在腳本運行完才會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態(tài)定義,在代碼靜態(tài)解析階段就會生成。 下面重點解釋第一個差異,我們還是舉上面那個CommonJS模塊的加載機制例子: // lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4ES6 模塊的運行機制與 CommonJS 不一樣。ES6 模塊是動態(tài)引用,并且不會緩存值,模塊里面的變量綁定其所在的模塊。 (3) ES6-Babel-Browserify使用教程簡單來說就一句話:使用Babel將ES6編譯為ES5代碼,使用Browserify編譯打包js。 ①定義package.json文件 {
"name" : "es6-babel-browserify",
"version" : "1.0.0"
}②安裝babel-cli, babel-preset-es2015和browserifynpm install babel-cli browserify -g npm install babel-preset-es2015 --save-dev preset 預(yù)設(shè)(將es6轉(zhuǎn)換成es5的所有插件打包)
③定義.babelrc文件 {
"presets": ["es2015"]
}④定義模塊代碼//module1.js文件
// 分別暴露
export function foo() {
console.log('foo() module1')
}
export function bar() {
console.log('bar() module1')
}//module2.js文件
// 統(tǒng)一暴露
function fun1() {
console.log('fun1() module2')
}
function fun2() {
console.log('fun2() module2')
}
export { fun1, fun2 }//module3.js文件
// 默認暴露 可以暴露任意數(shù)據(jù)類項,暴露什么數(shù)據(jù),接收到就是什么數(shù)據(jù)
export default () => {
console.log('默認暴露')
}// app.js文件
import { foo, bar } from './module1'
import { fun1, fun2 } from './module2'
import module3 from './module3'
foo()
bar()
fun1()
fun2()
module3()⑤ 編譯并在index.html中引入然后在index.html文件中引入 <script type="text/javascript" src="js/lib/bundle.js"></script> 最后得到如下結(jié)果:
 此外第三方庫(以jQuery為例)如何引入呢? 首先安裝依賴npm install jquery@1 然后在app.js文件中引入 //app.js文件
import { foo, bar } from './module1'
import { fun1, fun2 } from './module2'
import module3 from './module3'
import $ from 'jquery'
foo()
bar()
fun1()
fun2()
module3()
$('body').css('background', 'green')三、總結(jié)CommonJS規(guī)范主要用于服務(wù)端編程,加載模塊是同步的,這并不適合在瀏覽器環(huán)境,因為同步意味著阻塞加載,瀏覽器資源是異步加載的,因此有了AMD CMD解決方案。 AMD規(guī)范在瀏覽器環(huán)境中異步加載模塊,而且可以并行加載多個模塊。不過,AMD規(guī)范開發(fā)成本高,代碼的閱讀和書寫比較困難,模塊定義方式的語義不順暢。 CMD規(guī)范與AMD規(guī)范很相似,都用于瀏覽器編程,依賴就近,延遲執(zhí)行,可以很容易在Node.js中運行。不過,依賴SPM 打包,模塊的加載邏輯偏重 ES6 在語言標(biāo)準(zhǔn)的層面上,實現(xiàn)了模塊功能,而且實現(xiàn)得相當(dāng)簡單,完全可以取代 CommonJS 和 AMD 規(guī)范,成為瀏覽器和服務(wù)器通用的模塊解決方案。
作者:浪里行舟 鏈接:前端模塊化詳解(完整版) 來源:github 著作權(quán)歸作者所有。非商業(yè)轉(zhuǎn)載請注明出處。
|