僅需一篇,搞定前端模塊化
原創【51CTO.com原創稿件】前言
在JavaScript發展初期就是為了實現簡單的頁面交互邏輯,寥寥數語即可;如今CPU、瀏覽器性能得到了極大的提升,很多頁面邏輯遷移到了客戶端(表單驗證等),隨著web2.0時代的到來,Ajax技術得到廣泛應用,jQuery等前端庫層出不窮,前端代碼日益膨脹,此時在JS方面就會考慮使用模塊化規范去管理。
本文內容主要有理解模塊化,為什么要模塊化,模塊化的優缺點以及模塊化規范,并且介紹下開發中***的CommonJS, AMD, ES6、CMD規范。本文試圖站在小白的角度,用通俗易懂的筆調介紹這些枯燥無味的概念,希望諸君閱讀后,對模塊化編程有個全新的認識和理解!
如需本文的源代碼,請猛戳源代碼 。
一、模塊化的理解
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 = 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
require命令用于加載模塊文件。require命令的基本功能是,讀入并執行一個JavaScript文件,然后返回該模塊的exports對象。如果沒有發現指定模塊,會報錯。
(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]
- // app.js文件
- // 引入第三方庫,應該放置在最前面
- 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文件中引入
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規范
- // alerter.js文件
- (function (window) {
- let msg = 'www.baidu.com'
- function getMsg() {
- return msg.toUpperCase()
- }
- window.dataService = {getMsg}
- })(window)
- // dataService.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文件
- // 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引入
此外在項目中如何引入第三方庫?只需在上面代碼的基礎稍作修改:
- // 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 規范,成為瀏覽器和服務器通用的模塊解決方案。
參考文章
- 前端模塊化開發那點歷史
- CommonJS,AMD,CMD區別
- AMD 和 CMD 的區別有哪些?
- Javascript模塊化編程
- Javascript標準參考教程
- CMD 模塊定義規范
- 理解CommonJS、AMD、CMD三種規范
作者:浪里行舟,慕課網認證作者,前端愛好者,立志往全棧工程師發展,從事前端一年多,目前技術棧有vue全家桶、ES6以及less等,樂于分享,最近一年寫了五六十篇原創技術文章,得到諸多好評!
【51CTO原創稿件,合作站點轉載請注明原文作者和出處為51CTO.com】