前端模塊化詳解(完整版)


原文鏈接:

https://github.com/ljianshu/Blog/issues/48

前言

在 JavaScript 發展初期就是為了實現簡單的頁面交互邏輯,寥寥數語即可;如今 CPU、瀏覽器性能得到了極大的提升,很多頁面邏輯遷移到了客戶端(表單驗證等),隨着 web2.0 時代的到來,Ajax 技術得到廣泛應用,jQuery 等前端庫層出不窮,前端代碼日益膨脹,此時在 JS 方面就會考慮使用模塊化規范去管理。

本文內容主要有理解模塊化,為什么要模塊化,模塊化的優缺點以及模塊化規范, 並且介紹下開發中最流行的 CommonJS、AMD、 ES6、CMD 規范。本文試圖站在小白的角度,用通俗易懂的筆調介紹這些枯燥無味的概念,希望諸君閱讀后,對模塊化編程有個全新的認識和理解!

建議下載本文源代碼,自己動手敲一遍,請猛戳 GitHub 個人博客:

https://github.com/ljianshu/Blog

 

一、模塊化的理解

1. 什么是模塊?

模塊是指將一個復雜的程序依據一定的規則 (規范) 封裝成幾個塊 (文件),並進行組合在一起,塊的內部數據與實現是私有的, 只是向外部暴露一些接口 (方法) 與外部其它模塊通信。

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、ES6、CMD 規范。

二、模塊化規范1.CommonJS (1) 概述

Node 應用由模塊組成,采用 CommonJS 模塊規范。每個文件就是一個模塊,有自己的作用域。在一個文件里面定義的變量、函數、類,都是私有的,對其他文件不可見。在服務器端,模塊的加載是運行時同步加載的;在瀏覽器端,模塊需要提前編譯打包處理。

 (2) 特點

所有代碼都運行在模塊作用域,不會污染全局作用域。

模塊可以多次加載,但是只會在第一次加載時運行一次,然后運行結果就被緩存了,以后再加載,就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存。

模塊加載的順序,按照其在代碼中出現的順序。

 (3) 基本語法

  • 暴露模塊:module.exports = valueexports.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')

三、總結

CommonJS 規范主要用於服務端編程,加載模塊是同步的,這並不適合在瀏覽器環境,因為同步意味着阻塞加載,瀏覽器資源是異步加載的,因此有了 AMD CMD 解決方案。

AMD 規范在瀏覽器環境中異步加載模塊,而且可以並行加載多個模塊。不過,AMD 規范開發成本高,代碼的閱讀和書寫比較困難,模塊定義方式的語義不順暢。

CMD 規范與 AMD 規范很相似,都用於瀏覽器編程,依賴就近,延遲執行,可以很容易在 Node.js 中運行。不過,依賴 SPM 打包,模塊的加載邏輯偏重ES6 在語言標准的層面上,實現了模塊功能,而且實現得相當簡單,完全可以取代 CommonJS 和 AMD 規范,成為瀏覽器和服務器通用的模塊解決方案。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM