原文連接:javascript
https://github.com/ljianshu/Blog/issues/48css
前言html
在 JavaScript 發展初期就是爲了實現簡單的頁面交互邏輯,寥寥數語便可;現在 CPU、瀏覽器性能獲得了極大的提高,不少頁面邏輯遷移到了客戶端(表單驗證等),隨着 web2.0 時代的到來,Ajax 技術獲得普遍應用,jQuery 等前端庫層出不窮,前端代碼日益膨脹,此時在 JS 方面就會考慮使用模塊化規範去管理。前端
本文內容主要有理解模塊化,爲何要模塊化,模塊化的優缺點以及模塊化規範, 而且介紹下開發中最流行的 CommonJS、AMD、 ES六、CMD 規範。本文試圖站在小白的角度,用通俗易懂的筆調介紹這些枯燥無味的概念,但願諸君閱讀後,對模塊化編程有個全新的認識和理解!java
建議下載本文源代碼,本身動手敲一遍,請猛戳 GitHub 我的博客:node
https://github.com/ljianshu/Blogjquery
1、模塊化的理解git
1. 什麼是模塊?es6
模塊是指將一個複雜的程序依據必定的規則 (規範) 封裝成幾個塊 (文件),並進行組合在一塊兒,塊的內部數據與實現是私有的, 只是向外部暴露一些接口 (方法) 與外部其它模塊通訊。github
2. 模塊化的進化過程
全局 function 模式:將不一樣的功能封裝成不一樣的全局函數;
編碼: 將不一樣的功能封裝成不一樣的全局函數;
問題: 污染全局命名空間,容易引發命名衝突或數據不安全,並且模塊成員之間看不出直接關係。
function m1(){
//...
}
function m2(){
//...
}
namespace 模式:簡單對象封裝
做用: 減小了全局變量,解決命名衝突
問題: 數據不安全 (外部能夠直接修改模塊內部的數據)
let myModule = {
data: 'www.baidu.com',
foo() {
console.log(`foo() ${this.data}`)
},
bar() {
console.log(`bar() ${this.data}`)
}
}
myModule.data = 'other data' // 能直接修改模塊內部的數據
myModule.foo() // foo() other data
這樣的寫法會暴露全部模塊成員,內部狀態能夠被外部改寫。
IIFE 模式:匿名函數自調用 (閉包)
做用: 數據是私有的, 外部只能經過暴露的方法操做;
編碼: 將數據和行爲封裝到一個函數內部, 經過給 window 添加屬性來向外暴露接口;
問題: 若是當前這個模塊依賴另外一個模塊怎麼辦?
// index.html 文件
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo()
myModule.bar()
console.log(myModule.data) //undefined 不能訪問模塊內部數據
myModule.data = 'xxxx' // 不是修改的模塊內部的 data
myModule.foo() // 沒有改變
</script>
// module.js 文件
(function(window) {
let data = 'www.baidu.com'
// 操做數據的函數
function foo() {
// 用於暴露有函數
console.log(`foo() ${data}`)
}
function bar() {
// 用於暴露有函數
console.log(`bar() ${data}`)
otherFun() // 內部調用
}
function otherFun() {
// 內部私有的函數
console.log('otherFun()')
}
// 暴露行爲
window.myModule = { foo, bar } //ES6 寫法
})(window)
最後獲得的結果:
IIFE 模式加強:引入依賴
這就是現代模塊實現的基石。
// module.js 文件
(function(window, $) {
let data = 'www.baidu.com'
// 操做數據的函數
function foo() {
// 用於暴露有函數
console.log(`foo() ${data}`)
$('body').css('background', 'red')
}
function bar() {
// 用於暴露有函數
console.log(`bar() ${data}`)
otherFun() // 內部調用
}
function otherFun() {
// 內部私有的函數
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 庫,就把這個庫看成參數傳入。這樣作除了保證模塊的獨立性,還使得模塊之間的依賴關係變得明顯。
3. 模塊化的好處
避免命名衝突 (減小命名空間污染)
更好的分離, 按需加載
更高複用性
高可維護性
4. 引入多個<script>
後出現出現問題
請求過多
首先咱們要依賴多個模塊,那樣就會發送多個請求,致使請求過多。
依賴模糊
咱們不知道他們的具體依賴關係是什麼,也就是說很容易由於不瞭解他們之間的依賴關係致使加載前後順序出錯。
難以維護
以上兩種緣由就致使了很難維護,極可能出現牽一髮而動全身的狀況致使項目出現嚴重的問題。模塊化當然有多個好處,然而一個頁面須要引入多個 js 文件,就會出現以上這些問題。而這些問題能夠經過模塊化規範來解決,下面介紹開發中最流行的 commonjs、AMD、ES六、CMD 規範。
2、模塊化規範1.CommonJS (1) 概述
Node 應用由模塊組成,採用 CommonJS 模塊規範。每一個文件就是一個模塊,有本身的做用域。在一個文件裏面定義的變量、函數、類,都是私有的,對其餘文件不可見。在服務器端,模塊的加載是運行時同步加載的;在瀏覽器端,模塊須要提早編譯打包處理。
(2) 特色
全部代碼都運行在模塊做用域,不會污染全局做用域。
模塊能夠屢次加載,可是隻會在第一次加載時運行一次,而後運行結果就被緩存了,之後再加載,就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存。
模塊加載的順序,按照其在代碼中出現的順序。
(3) 基本語法
暴露模塊:module.exports = value
或exports.xxx = value
;
引入模塊:require(xxx), 若是是第三方模塊,xxx 爲模塊名;若是是自定義模塊,xxx 爲模塊文件路徑。
此處咱們有個疑問:CommonJS 暴露的模塊究竟是什麼? CommonJS 規範規定,每一個模塊內部,module 變量表明當前模塊。這個變量是一個對象,它的 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 和函數 addX。
var example = require('./example.js');// 若是參數字符串以「./」開頭,則表示加載的是一個位於相對路徑
console.log(example.x); // 5
console.log(example.addX(1)); // 6
(4) 模塊的加載機制
CommonJS 模塊的加載機制是,輸入的是被輸出的值的拷貝。也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。這點與 ES6 模塊化有重大差別(下文會介紹),請看下面這個例子:
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
上面代碼輸出內部變量 counter 和改寫這個變量的內部方法 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 模塊內部的變化就影響不到 counter 了。這是由於 counter 是一個原始類型的值,會被緩存。除非寫成一個函數,才能獲得內部變更後的值。
(5) 服務器端實現
①下載安裝 node.js
②建立項目結構
注意:用 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 // 用於數組去重;
④定義模塊代碼
//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]
// 引入第三方庫,應該放置在最前面
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) 瀏覽器端實現 (藉助 Browserify)
①建立項目結構
|-js
|-dist // 打包生成文件的目錄
|-src // 源碼所在的目錄
|-module1.js
|-module2.js
|-module3.js
|-app.js // 應用主源文件
|-index.html // 運行於瀏覽器上
|-package.json
{
"name": "browserify-test",
"version": "1.0.0"
}
②下載 browserify
全局: npm install browserify -g
局部: npm install browserify --save-dev
③定義模塊代碼 (同服務器端)
注意: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 規範加載模塊是同步的,也就是說,只有加載完成,才能執行後面的操做。AMD 規範則是非同步加載模塊,容許指定回調函數。
因爲 Node.js 主要用於服務器編程,模塊文件通常都已經存在於本地硬盤,因此加載起來比較快,不用考慮非同步加載的方式,因此 CommonJS 規範比較適用。可是,若是是瀏覽器環境,要從服務器端加載模塊,這時就必須採用非同步模式,所以瀏覽器端通常採用 AMD 規範。此外 AMD 規範比 CommonJS 規範在瀏覽器端實現要來着早。
(1) AMD 規範基本語法
定義暴露模塊:
// 定義沒有依賴的模塊
define(function(){
return 模塊
})
// 定義有依賴的模塊
define(['module1', 'module2'], function(m1, m2){
return 模塊
})
引入使用模塊:
引入使用模塊:
require(['module1', 'module2'], function(m1, m2){
使用 m1/m2
})
(2) 未使用 AMD 規範與使用 require.js
經過比較二者的實現方法,來講明使用 AMD 規範的好處。
未使用 AMD 規範
// 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>
最後獲得以下結果:
這種方式缺點很明顯:首先會發送多個請求,其次引入的 js 文件順序不能搞錯,不然會報錯!
使用 require.js
RequireJS 是一個工具庫,主要用於客戶端的模塊管理。它的模塊管理遵照 AMD 規範,RequireJS 的基本思想是,經過 define 方法,將代碼定義爲模塊;經過 require 方法,實現代碼的模塊加載。
接下來介紹 AMD 規範在瀏覽器實現的步驟:
①下載 require.js,並引入
官網: http://www.requirejs.cn/
github : https://github.com/requirejs/requirejs
而後將 require.js 導入項目: js/libs/require.js
② 建立項目結構
|-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/', // 基本路徑 出發點在根目錄下
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>
此外在項目中如何引入第三方庫?只需在上面代碼的基礎稍做修改:
// 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/', // 基本路徑 出發點在根目錄下
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 文件也要有相應的路徑配置。
小結:經過二者的比較,能夠得出 AMD 模塊定義的方法很是清晰,不會污染全局環境,可以清楚地顯示依賴關係。AMD 模式能夠用於瀏覽器環境,而且容許非同步加載模塊,也能夠根據須要動態加載模塊。
3.CMD
CMD 規範專門用於瀏覽器端,模塊的加載是異步的,模塊使用時纔會加載執行。CMD 規範整合了 CommonJS 和 AMD 規範的特色。在 Sea.js 中,全部 JavaScript 模塊都遵循 CMD 模塊定義規範。
(1)CMD規範基本語法
定義暴露模塊:
// 定義沒有依賴的模塊
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, 並引入
官網: http://seajs.org/
github : https://github.com/seajs/seajs
而後將 sea.js 導入項目: js/libs/sea.js
② 建立項目結構
|-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) {
// 內部變量數據
var data = 'atguigu.com'
// 內部函數
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>
最後獲得結果以下:
4.ES6 模塊化
ES6 模塊的設計思想是儘可能的靜態化,使得編譯時就能肯定模塊的依賴關係,以及輸入和輸出的變量。CommonJS 和 AMD 模塊,都只能在運行時肯定這些東西。好比,CommonJS 模塊就是對象,輸入時必須查找對象屬性。
(1) ES6 模塊化語法
export 命令用於規定模塊的對外接口,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 命令的時候,用戶須要知道所要加載的變量名或函數名,不然沒法加載。爲了給用戶提供方便,讓他們不用閱讀文檔就能加載模塊,就要用到 export default 命令,爲模塊指定默認輸出。
// export-default.js
export default function () {
console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'
模塊默認輸出, 其餘模塊加載該模塊時,import 命令能夠爲該匿名函數指定任意名字。
(2) ES6 模塊與 CommonJS 模塊的差別
它們有兩個重大差別:
① CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
② CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。
第二個差別是由於 CommonJS 加載的是一個對象(即 module.exports 屬性),該對象只有在腳本運行完纔會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。
下面重點解釋第一個差別,咱們仍是舉上面那個 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); // 4
ES6 模塊的運行機制與 CommonJS 不同。ES6 模塊是動態引用,而且不會緩存值,模塊裏面的變量綁定其所在的模塊。
(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 預設 (將 es6 轉換成 es5 的全部插件打包)
③ 定義.babelrc 文件
{
"presets": ["es2015"]
}
④ 定義模塊代碼
//module1.js 文件
// 分別暴露
export function foo() {
console.log('foo() module1')
}
export function bar() {
console.log('bar() module1')
}
//module2.js 文件
// 統一暴露
function fun1() {
console.log('fun1() module2')
}
function fun2() {
console.log('fun2() module2')
}
export { fun1, fun2 }
//module3.js 文件
// 默認暴露 能夠暴露任意數據類項,暴露什麼數據,接收到就是什麼數據
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>
最後獲得以下結果:
此外第三方庫 (以 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')
3、總結
CommonJS 規範主要用於服務端編程,加載模塊是同步的,這並不適合在瀏覽器環境,由於同步意味着阻塞加載,瀏覽器資源是異步加載的,所以有了 AMD CMD 解決方案。
AMD 規範在瀏覽器環境中異步加載模塊,並且能夠並行加載多個模塊。不過,AMD 規範開發成本高,代碼的閱讀和書寫比較困難,模塊定義方式的語義不暢。
CMD 規範與 AMD 規範很類似,都用於瀏覽器編程,依賴就近,延遲執行,能夠很容易在 Node.js 中運行。不過,依賴 SPM 打包,模塊的加載邏輯偏重ES6 在語言標準的層面上,實現了模塊功能,並且實現得至關簡單,徹底能夠取代 CommonJS 和 AMD 規範,成爲瀏覽器和服務器通用的模塊解決方案。