前言
在 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 模式:將不同的功能封裝成不同的全局函數(shù)榛斯;
編碼: 將不同的功能封裝成不同的全局函數(shù);
問題: 污染全局命名空間搂捧,容易引起命名沖突或數(shù)據(jù)不安全驮俗,而且模塊成員之間看不出直接關(guān)系。
function m1(){ //...}function m2(){ //...}
namespace 模式:簡單對象封裝
作用: 減少了全局變量允跑,解決命名沖突
問題: 數(shù)據(jù)不安全 (外部可以直接修改模塊內(nèi)部的數(shù)據(jù))
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)可以被外部改寫。
IIFE 模式:匿名函數(shù)自調(diào)用 (閉包)
作用: 數(shù)據(jù)是私有的聋丝, 外部只能通過暴露的方法操作索烹;
編碼: 將數(shù)據(jù)和行為封裝到一個函數(shù)內(nèi)部, 通過給 window 添加屬性來向外暴露接口;
問題: 如果當前這個模塊依賴另一個模塊怎么辦?
// 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é)果:
IIFE 模式增強:引入依賴
這就是現(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 庫,就把這個庫當作參數(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) 特點
所有代碼都運行在模塊作用域张峰,不會污染全局作用域词顾。
模塊可以多次加載粘衬,但是只會在第一次加載時運行一次科盛,然后運行結(jié)果就被緩存了甜孤,以后再加載橘券,就直接讀取緩存結(jié)果旁舰。要想讓模塊再次運行箭窜,必須清除緩存纳猫。
模塊加載的順序,按照其在代碼中出現(xiàn)的順序侵续。
(3) 基本語法
暴露模塊:
module.exports = value
或exports.xxx = value
;引入模塊:require(xxx), 如果是第三方模塊轧坎,xxx 為模塊名;如果是自定義模塊属百,xxx 為模塊文件路徑。
此處我們有個疑問:CommonJS 暴露的模塊到底是什么? CommonJS 規(guī)范規(guī)定渔呵,每個模塊內(nèi)部,module 變量代表當前模塊录豺。這個變量是一個對象双饥,它的 exports 屬性(即 module.exports)是對外的接口弟断。加載某個模塊阀趴,其實是加載該模塊的 module.exports 屬性舍咖。
// example.jsvar 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); // 5console.log(example.addX(1)); // 6
(4) 模塊的加載機制
CommonJS 模塊的加載機制是球订,輸入的是被輸出的值的拷貝瑰钮。也就是說浪谴,一旦輸出一個值,模塊內(nèi)部的變化就影響不到這個值篇恒。這點與 ES6 模塊化有重大差異(下文會介紹)胁艰,請看下面這個例子:
// lib.jsvar counter = 3;function incCounter() { counter++;}module.exports = { counter: counter, incCounter: incCounter,};
上面代碼輸出內(nèi)部變量 counter 和改寫這個變量的內(nèi)部方法 incCounter腾么。
// main.jsvar counter = require('./lib').counter;var incCounter = require('./lib').incCounter;console.log(counter); // 3incCounter();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.jsmodule.exports = { msg: 'module1', foo() { console.log(this.msg) }}
//module2.jsmodule.exports = function() { console.log('module2')}
//module3.jsexports.foo = function() { console.log('foo() module3')}exports.arr = [1, 2, 3, 3, 2]
// 引入第三方庫疤坝,應(yīng)該放置在最前面let uniq = require('uniq')let module1 = require('./modules/module1')let module2 = require('./modules/module2')let module3 = require('./modules/module3')module1.foo() //module1module2() //module2module3.foo() //foo() module3console.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
全局: npm install browserify -g
局部: npm install browserify --save-dev
③定義模塊代碼 (同服務(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">
2. AMD
CommonJS 規(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ī)范的好處娱俺。
未使用 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 文件順序不能搞錯烛愧,否則會報錯屑彻!
使用 require.js
RequireJS 是一個工具庫社牲,主要用于客戶端的模塊管理搏恤。它的模塊管理遵守 AMD 規(guī)范,RequireJS 的基本思想是藤巢,通過 define 方法掂咒,將代碼定義為模塊绍刮;通過 require 方法,實現(xiàn)代碼的模塊加載岁歉。
接下來介紹 AMD 規(guī)范在瀏覽器實現(xiàn)的步驟:
①下載 require.js锅移,并引入
官網(wǎng): http://www.requirejs.cn/
github : https://github.com/requirejs/requirejs
然后將 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: { // 映射: 模塊標識名: 路徑 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.CMD
CMD 規(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, 并引入
官網(wǎng): http://seajs.org/
github : https://github.com/seajs/seajs
然后將 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.jsexport default function () { console.log('foo');}
// import-default.jsimport 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.jsexport let counter = 3;export function incCounter() { counter++;}// main.jsimport { counter, incCounter } from './lib';console.log(counter); // 3incCounter();console.log(counter); // 4
ES6 模塊的運行機制與 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 和 browserify
npm 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 中引入
使用 Babel 將 ES6 編譯為 ES5 代碼 (但包含 CommonJS 語法) : babel js/src -d js/lib
使用 Browserify 編譯 js : browserify js/lib/app.js -o js/lib/bundle.js
然后在 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 在語言標準的層面上机蔗,實現(xiàn)了模塊功能,而且實現(xiàn)得相當簡單甘萧,完全可以取代 CommonJS 和 AMD 規(guī)范萝嘁,成為瀏覽器和服務(wù)器通用的模塊解決方案。