js模塊化規范

JavaScript發展初期是為了實現簡單的頁面交互邏輯,寥寥數語即可;如今CPU、瀏覽器性能得到了極大的提升,很多頁面邏輯遷移到了客戶端或者瀏覽器(表單驗證等),隨著web2.0時代的到來,Ajax技術的廣泛應用,jQuery、yui等前端庫層出不窮,實現的邏輯也越來越復雜,前端代碼隨之日益膨脹,此時就需要使用模塊化規范去管理前端項目的開發,本文主要從理解模塊化、為什么要模塊化?模塊化的有缺點、模塊化的發展和模塊化規范。實現流行的規范有CommonJS、AMD、ES6、CMD。

前端開發模式規范化導圖

理解模塊化

模塊化的開發方式可以提高代碼復用率,方便進行代碼的管理。通常一個文件就是一個模塊,有自己的作用域,只向外暴露特定的變量和函數。

什么是模塊化

將一個復雜的程序依據一定的規則(規范)封裝成幾個塊(文件), 并進行組合在一起。

程序塊的內部數據與實現是私有的, 只是向外部暴露一些接口(方法)與外部其它模塊通信。

模塊化的發展過程

全局function模式:將不同的功能封裝成不同的全局函數

  • 編碼:將不同的功能封裝成不同的全局函數。
  • 問題:污染全局命名空間,容易引起命名沖突或數據不安全,而且模塊成員之間看不出直接關系。

namespace模式:簡單對象封裝

  • 作用:減少了全局變量,解決命名沖突。
  • 問題:數據不安全,外部可以直接修改模塊內部的數據,并容易導致閉包。
const myModule = {
  data: "www.80sz.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文件



// module.js文件
(function(window) {
  const data = 'www.80sz.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, $) {
  const data = "www.80sz.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文件
  
  
  
  

上例子通過jquery將頁面的背景顏色改成紅色,先引入jQuery庫,就把這個庫當作參數傳入。這樣做除了保證模塊的獨立性,還使得模塊之間的依賴關系變得明顯

模塊化的好處

  • 避免命名沖突(減少命名空間污染)
  • 更好的分離,按需加載
  • 更高復用性
  • 高可維護性

引入多個

這種方式缺點很明顯:首先會發送多個請求,其次引入的js文件順序不能搞錯,否則會報錯!

使用require.js

RequireJS是一個工具庫,主要用于客戶端的模塊管理。它的模塊管理遵守AMD規范。

RequireJS的基本思想是,通過define方法,將代碼定義為模塊;通過require方法,實現代碼的模塊加載

接下來介紹AMD規范在瀏覽器實現的步驟:

 

1、下載require.js, 并引入

  • 官網: http://www.requirejs.cn/
  • github : https://github.com/requirejs/requirejs

然后將require.js導入項目: js/libs/require.js

2、創建項目結構

|-js
  |-libs
    |-require.js
  |-modules
    |-alerter.js
    |-dataService.js
  |-main.js
|-index.html

3、定義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文件


  
    Modular Demo
  
  
    
    
  

4、頁面引入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模式可以用于瀏覽器環境,并且允許非同步加載模塊,也可以根據需要動態加載模塊。

CMD

CMD規范專門用于瀏覽器端,模塊的加載是異步的,模塊使用時才會加載執行。CMD規范整合了CommonJS和AMD規范的特點。在 Sea.js 中,所有 JavaScript 模塊都遵循 CMD模塊定義規范。

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()
})

sea.js簡單使用教程

下載sea.js,并引入

然后將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) {
  //內部變量數據
  const 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>

ES6模塊化

ES6 模塊的設計思想是盡量的靜態化,使得編譯時就能確定模塊的依賴關系,以及輸入和輸出的變量。CommonJSAMD 模塊,都只能在運行時確定這些東西。比如,CommonJS 模塊就是對象,輸入時必須查找對象屬性。

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命令可以為該匿名函數指定任意名字。

ES6 模塊與 CommonJS 模塊的差異

CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用

  • CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。
  • ES6 模塊的運行機制與 CommonJS 不一樣。JS 引擎對腳本靜態分析的時候,遇到模塊加載命令import,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊里面去取值。換句話說,ES6 的import有點像 Unix 系統的“符號連接”,原始值變了,import加載的值也會跟著變。因此,ES6 模塊是動態引用,并且不會緩存值,模塊里面的變量綁定其所在的模塊。

CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口

  • 運行時加載: CommonJS 模塊就是對象;即在輸入時是先加載整個模塊,生成一個對象,然后再從這個對象上面讀取方法,這種加載稱為“運行時加載”。

  • 編譯時加載: ES6 模塊不是對象,而是通過 export 命令顯式指定輸出的代碼,import時采用靜態命令的形式。即在import時可以指定加載某個輸出值,而不是加載整個模塊,這種加載稱為“編譯時加載”。

第二個差異是因為 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 模塊是動態引用,并且不會緩存值,模塊里面的變量綁定其所在的模塊

ES6-Babel-Browserify使用教程

簡單來說就一句話:使用Babel將ES6編譯為ES5代碼,使用Browserify編譯打包js

1、定義package.json文件

 {
   "name" : "es6-babel-browserify",
   "version" : "1.0.0"
 }

2、安裝babel-cli, babel-preset-es2015browserify

  • npm install babel-cli browserify -g
  • npm install babel-preset-es2015 --save-dev
  • preset 預設(將es6轉換成es5的所有插件打包)

3、定義.babelrc文件

  {
    "presets": ["es2015"]
  }

4、定義模塊代碼

//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()

5、編譯并在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>

總結

  • CommonJS規范主要用于服務端編程,加載模塊是同步的,這并不適合在瀏覽器環境,因為同步意味著阻塞加載,瀏覽器資源是異步加載的,因此有了AMD CMD解決方案。
  • AMD規范在瀏覽器環境中異步加載模塊,而且可以并行加載多個模塊。不過,AMD規范開發成本高,代碼的閱讀和書寫比較困難,模塊定義方式的語義不順暢。
  • CMD規范與AMD規范很相似,都用于瀏覽器編程,依賴就近,延遲執行,可以很容易在Node.js中運行。不過,依賴SPM 打包,模塊的加載邏輯偏重
  • ES6 在語言標準的層面上,實現了模塊功能,而且實現得相當簡單,完全可以取代 CommonJSAMD 規范,成為瀏覽器和服務器通用的模塊解決方案

參考:https://juejin.im/post/5c17ad756fb9a049ff4e0a62