1. 程式人生 > >javascript 模組化開發

javascript 模組化開發

一、為什麼會有模組化


1. 當一個專案開發的越來越複雜的時候,會遇到一些問題,比如:

  • 命名衝突:當專案由團隊進行協作開發的時候,不同開發人員的變數和函式命名可能相同;即使是一個開發,當開發週期比較長的時候,也有可能會忘記之前使用了什麼變數,從而導致重複命名,導致命名衝突。

  • 檔案依賴:程式碼重用時,引入js檔案的數目可能少了,或者引入的順序不對,比如使用boostrap的時候,需要引入jQuery,並且jQuery的檔案必須要比boostrap的js檔案先引入。

2. 當使用模組化開發的時候可以避免以上的問題,並且讓開發的效率變高,以及方便後期的維護:

  • 提升開發效率:程式碼方便重用,別人開發的模組直接拿過來就可以使用,不需要重複開發法類似的功能。

  • 方便後期維護:程式碼方便重用,別人開發的模組直接拿過來就可以使用,不需要重複開發法類似的功能。

所以總結來說,在生產角度,模組化開發是一種生產方式,這種方式生產效率高,維護成本低。從軟體開發角度來說,模組化開發是一種開發模式,寫程式碼的一種方式,開發效率高,方便後期維護。

二、模組化開發的演變過程


1. 全域性函式

function add(a , b) {
    return parseFloat(a) + parseFloat(b); } function substract(a ,b) {} function multiply(a ,b) {} function divide(a ,b) {} 

在早期的開發過程中就是將重複的程式碼封裝到函式中,再將一系列的函式放到一個檔案中,這種情況下全域性函式的方式只能認為的認為它們屬於一個模組,但是程式並不能區分哪些函式是同一個模組,如果僅僅從程式碼的角度來說,這沒有任何模組的概念。

存在的問題:

  • 汙染了全域性變數,無法保證不與其他模組發生變數名衝突。
  • 模組成員之間看不出直接關係。

2. 物件封裝-名稱空間

var calculator = {
  add: function(a, b) { return parseFloat(a) + parseFloat(b); }, subtract: function(a, b) {}, multiply: function(a, b) {}, divide: function(a, b) {} }; 

通過新增名稱空間的形式從某種程度上解決了變數命名衝突的問題,但是並不能從根本上解決命名衝突。 不過此時從程式碼級別可以明顯區分出哪些函式屬於同一個模組。

存在的問題:

  • 暴露了所有的模組成員,內部狀態可以被外部改寫,不安全。
  • 名稱空間越來越長。

3. 私有公有成員分離

var calculator = (function () {
    // 這裡形成一個單獨的私有的空間 // 私有成員的作用: // 1、將一個成員私有化 // 2、抽象公共方法(其他成員中會用到的) // 私有的轉換邏輯 function convert(input){ return parseInt(input); } function add(a, b) { return convert(a) + convert(b); } function subtract(a, b) {} function multiply(a, b) {} function divide(a, b) {} return { add : add, subtract : subtract, multiply : multiply, divide : divide } })(); 
  1. 利用此種方式將函式包裝成一個獨立的作用域,私有空間的變數和函式不會影響到全域性作用域。
  2. 以返回值的方式得到模組的公共成員,公開公有方法,隱藏私有空間內部的屬性、元素,比如註冊方法中可能會記錄日誌。
  3. 可以有選擇的對外暴露自身成員。
  4. 從某種意義上來說,解決了變數命名衝突的問題。

4. 模組的擴充套件與維護

// 計算模組
(function (calculator) {
    function convert(input) { return parseInt(input); } calculator.add = function(a, b) { return convert(a) + convert(b); } window.calculator = calculator; })(window.calculator || {}); // 新增需求 (function (calculator) { calculator.remain = function (a , b) { return a % b; } window.calculator = calculator; })(window.calculator || {}); alert(calculator.remain(4,3)); 
  1. 利用此種方式,有利於對龐大的模組的子模組劃分。
  2. 實現了開閉原則:對新增開發,對修改關閉。對於已有檔案儘量不要修改,通過新增新檔案的方式新增新功能。

5. 第三方依賴的管理

(function (calculator , $) {
    // 依賴函式的引數,是屬於模組內部
    // console.log($); calculator.remain = function (a , b) { return a % b; } window.calculator = calculator; })(window.calculator || {} , jQuery); 

模組最好要保證模組的職責單一性,最好不要與程式的其他部分直接互動,通過向匿名函式注入依賴項的形式,除了保證模組的獨立性,還使模組之間的以來關係變得明顯。
  對於模組的依賴通過自執行函式的引數傳入,這樣做可以做到依賴抽象,本例中使用的jQuery,而當要使用zepto的時候,只要更換傳入的引數即可。
  原則:高內聚低耦合,模組內相關性高,模組間關聯低。

總結:在什麼場景下使用模組化開發

  • 業務複雜
  • 重用邏輯非常多
  • 擴充套件性要求較高

三、模組化規範


伺服器端規範主要是CommonJSnode.js用的就是CommonJS規範。
  客戶端規範主要有:AMD(非同步模組定義,推崇依賴前置)、CMD(通用模組定義,推崇依賴就近)。AMD規範的實現主要有RequireJSCMD規範的主要實現有SeaJSRequireJS在國外用的比較多,SeaJS在國內用的比較多,並且SeaJS的創始人為阿里的玉伯,所以SeaJS在阿里系用的非常廣泛,包括京東等大廠也在用SeaJS,我們詳細介紹的也是SeaJS。但是SeaJS已經停止維護了,因為在ES6中已經有了模組化的實現,隨著ES6的普及,第三方的模組化實現將會慢慢的淘汰(但是這個在國內可能還要很多年)。

四、SeaJs


1. SeaJs簡介

  • SeaJS是一個基於CMD規範實現的模組化開發解決方案。
  • 作者:Alibaba 玉伯
  • Alibaba 玉伯
  • 特性:
    • 簡單友好的模組化定義規範。
    • 自然直觀的程式碼組織方式。
  • 哲學:一切皆模組

2. 使用步驟

  • 引入sea.js庫
  • 定義模組

define(function(require, exports, module){ 模組程式碼 });

* 暴露介面
- exports
- module.exports
* 依賴模組

require(‘模組id’)

* 啟動模組系統

seajs.use(‘模組id’,function( 模組物件 ){ 業務程式碼 });


#####HelloWorld
* 01-convertor.js
```JavaScript
/**
 * 轉換模組,到處成員:convertToNumber
 */
define(function (require, exports, module) { exports.convertToNumber = function (input) { return parseFloat(input); } }); 
  • 01-calculator.js
define(function (require, exports, module) {
    // 此處是模組的私有空間,定義模組的私有成員
    // 載入01-convertor模組 var convertor = require('./01-convertor'); function add(a, b) { return convertor.convertToNumber(a) + convertor.convertToNumber(b); } exports.add = add; }); 
  • 01-helloworld.html
<!DOCTYPE html>
<html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script src="node_modules/seajs/dist/sea.js"></script> <script> seajs.use('./01-calculator.js', function (calculator) { alert(calculator.add(1,2)); }); </script> </head> <body> </body> </html> 

3. 定義模組define

  • 先有規範,後有實現

  • 在CMD規範中,一個模組就是一個js檔案

  • define是一個全域性函式,用來定義模組

  • define(factory)

    • 物件{}這種方式,外部會直接獲取到該物件
    • 字串''同上
    • 函式function( require, exports, module ){ // 模組程式碼 }
      • 注意:為了減少出錯,定義函式的時候直接把這三個引數寫上

factory為物件、字串時,表示模組的介面就是該物件、字串。比如可以如下定義一個 JSON 資料模組:

define({ "foo": "bar" });

也可以通過字串定義模板模組:

define('I am a template. My name is {{name}}.');

factory為函式時,表示是模組的構造方法。執行該構造方法,可以得到模組向外提供的介面。factory方法在執行時,預設會傳入三個引數:require、exports和 module:

define(function(require, exports, module) { // 模組程式碼});
define define(id?, deps?, factory)

define也可以接受兩個以上引數。字串 id表示模組標識,陣列 deps是模組依賴。比如:

define('hello', ['jquery'], function(require, exports, module) { // 模組程式碼}); 

id和 deps引數可以省略。省略時,可以通過構建工具自動生成。
  注意:帶 id和 deps引數的 define用法不屬於 CMD 規範,而屬於 Modules/Transport 規範。

define.cmd Object

一個空物件,可用來判定當前頁面是否有 CMD 模組載入器:

if (typeof define === "function" && define.cmd) { 
    // 有 Sea.js 等 CMD 模組載入器存在
}
require Function

require是 factory函式的第一個引數。

require require(id)

require是一個方法,接受 模組標識 作為唯一引數,用來獲取其他模組提供的介面。

define(function(require, exports) { 
  // 獲取模組 a 的介面 
  var a = require('./a'); // 呼叫模組 a 的方法 a.doSomething(); }); 

注意:在開發時,require的書寫需要遵循一些 簡單約定

require.async require.async(id, callback?)

require.async方法用來在模組內部非同步載入模組,並在載入完成後執行指定回撥。callback引數可選。

define(function(require, exports, module) {
  // 非同步載入一個模組,在載入完成時,執行回撥
  require.async('./b', function(b) { b.doSomething(); }); // 非同步載入多個模組,在載入完成時,執行回撥 require.async(['./c', './d'], function(c, d) { c.doSomething(); d.doSomething(); }); }); 

注意:require是同步往下執行,require.async則是非同步回撥執行。require.async 一般用來載入可延遲非同步載入的模組。

require.resolve require.resolve(id)

使用模組系統內部的路徑解析機制來解析並返回模組路徑。該函式不會載入模組,只返回解析後的絕對路徑。

define(function(require, exports) {
  console.log(require.resolve('./b')); // ==> http://example.com/path/to/b.js }); 

這可以用來獲取模組路徑,一般用在外掛環境或需動態拼接模組路徑的場景下。

4. exports 和 module.exports

  • 功能:通過給 exports或module.exports動態的掛載變數、函式或物件,外部會獲取到該介面
  • exports 等價於 module.exports
  • 可以通過多次給exports 掛載屬性向外暴露
  • 不能直接給 exports 賦值
  • 如果想暴露單個變數、函式或物件可以通過直接給module.exports 賦值 即可

5. exports Object

exports是一個物件,用來向外提供模組介面。

define(function(require, exports) {
  // 對外提供 foo 屬性
  exports.foo = 'bar'; // 對外提供 doSomething 方法 exports.doSomething = function() {}; }); 

除了給 exports物件增加成員,還可以使用 return直接向外提供介面。

define(function(require) {
  // 通過 return 直接提供介面
  return { foo: 'bar', doSomething: function() {} }; }); 

如果 return語句是模組中的唯一程式碼,還可簡化為:

define({
  foo: 'bar',
  doSomething: function() {}
});

上面這種格式特別適合定義 JSONP 模組。

特別注意:下面這種寫法是錯誤的!

define(function(require, exports) {
  // 錯誤用法!!!
  exports = {
    foo: 'bar', doSomething: function() {} }; }); 

正確的寫法是用 return或者給 module.exports賦值:

define(function(require, exports, module) {
  // 正確寫法
  module.exports = { foo: 'bar', doSomething: function() {} }; }); 

提示:exports 僅僅是 module.exports 的一個引用。在 factory 內部給 exports 重新賦值時,並不會改變 module.exports 的值。因此給 exports 賦值是無效的,不能用來更改模組介面。

6. module Object

module是一個物件,上面儲存了與當前模組相關聯的一些屬性和方法。

6.1 module.id String

模組的唯一標識。

define('id', [], function(require, exports, module) { 
  // 模組程式碼 }); 

上面程式碼中,define的第一個引數就是模組標識。

6.2 module.uri String

根據模組系統的路徑解析規則得到的模組絕對路徑。

define(function(require, exports, module) {
  console.log(module.uri); // ==> http://example.com/path/to/this/file.js }); 

一般情況下(沒有在 define 中手寫 id 引數時),module.id 的值就是 module.uri,兩者完全相同。

6.3 module.dependencies Array

dependencies是一個數組,表示當前模組的依賴。

6.4 module. exports Object

當前模組對外提供的介面。
  傳給 factory 構造方法的 exports 引數是 module.exports 物件的一個引用。只通過 exports 引數來提供介面,有時無法滿足開發者的所有需求。 比如當模組的介面是某個類的例項時,需要通過 module.exports 來實現:

define(function(require, exports, module) {
  // exports 是 module.exports 的一個引用
  console.log(module.exports === exports); // true // 重新給 module.exports 賦值 module.exports = new SomeClass(); // exports 不再等於 module.exports console.log(module.exports === exports); // false }); 

注意:對 module.exports 的賦值需要同步執行,不能放在回撥函式裡。下面這樣是不行的:

// x.js
define(function(require, exports, module) {
  // 錯誤用法 setTimeout(function() { module.exports = { a: "hello" }; }, 0); }); 

在 y.js 裡有呼叫到上面的 x.js:

// y.js
define(function(require, exports, module) {
  var x = require('./x'); // 無法立刻得到模組 x 的屬性 a console.log(x.a); // undefined }); 

7. 小結

這就是 CMD 模組定義規範的所有內容。經常使用的 API 只有 define, require, require.async, exports, module.exports 這五個。其他 API 有個印象就好,在需要時再來查文件,不用刻意去記。

與 RequireJS 的 AMD 規範相比,CMD 規範儘量保持簡單,並與 CommonJS 和 Node.js 的 Modules 規範保持了很大的相容性。通過 CMD 規範書寫的模組,可以很容易在 Node.js 中執行

五、CommonJs

其實學完了SeaJs規範之後,CommonJs規範也就差不多會用了,兩者非常的相似,並且在node.js中使用起來更簡單。

在node.js中直接使用require引包,直接使用exports和module.exports暴露公開成員,並且npm基於CommonJs實現了自動載入和安裝依賴。

同樣的CommonJs讓node.js變得:1、增加內聚性,有助分工協作,2、方便重構,3、提高程式碼質量

node.js中的實現為:

(function(exports,require,module,__filename,__dirname){ return module.exports; }); 

require

  • 載入模組後會快取,多次載入後得到同一物件 require('http')

  • 檢視模組快取console.log(require.cache);

  • 查詢模組絕對路徑 require.resolve('./test.js');

  • 檢視單個的模組快取 require.cache[require.resolve('./test.js')]

  • 刪除模組快取 delete require.cache[require.resolve('./test.js')];



作者:iceman_dev
連結:https://www.jianshu.com/p/3832c00a44a7
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。