Javascript 模組化管理的來世今生

SmallW發表於2018-09-25

Javascript 模組化管理的來世今生

[TOC]

模組管理這個概念其實在前幾年前端度過了刀耕火種年代之後就一直被提起,那麼我們有思考過這個模組管理具體指的是什麼東西?什麼樣子的展現形式?歷史由來?現在是什麼樣的一個狀態?

直接回想起來的就是 cmd amd commonJS 這三大模組管理的印象。但是大家清楚 cmd amd commonJS 為什麼會出現麼?接下來,我們就一起來瞅瞅這具體是啥情況。

感覺自己在每一個階段,對於同一個技術的理解都不一樣。

一、什麼是模組化開發

為了讓一整個龐大的專案看起來整整齊齊,規規整整而出現的模組化管理,我們常見的三種模組化管理的庫: requireJS、seaJS、commonJS規範 ( 注意,這裡的commonJS不是一個庫,而是一種規範) 逐漸的在我們日常的開發中出現。 同時依賴於 這三類規範而出現的一些構建工具,但最後都敗給了 webpack 。這是一篇介紹 webpack 基本入門的文章,可以結合著這篇文章來進行解讀。 《前端之路》之 webpack 4.0+ 的應用構建

1-1、模組化第一階段

在這個階段中,我們常常會把非常多複雜的功能 封裝成一個個的函式:

function f1() {
	// todo
}

function f2() {
	// todo
}
.
.
.
複製程式碼

但是當 整個專案變大了以後,就會遇到很多問題,都是定義的全域性變數,形成了比較嚴重的汙染,還有可能會出現因為重新命名導致的各種問題。所以這些是需要進化的。所以就會進入到模組化的第二階段: 物件。

1-2、封裝到物件

到了第二個階段為了避免全域性變數的汙染,我們會將單個模組封裝到一個物件內部。如下:


const module = {
	_number: 10,
	f1: () => { 
		console.log(123)
	},
	f2: () => { 
		console.log(456)
	},
	.
	.
	.
}
複製程式碼

這樣我們就沒個模組定義一個物件,在需要的時候直接呼叫就好了,但是這樣也會存在一個問題 這樣寫的話會暴露全部的物件內部的屬性,內部狀態可以被外部改變. 例如:

module._number = 100
複製程式碼

如果我們支付相關模組這樣子來寫的話。我們隨意的來改變支付的金額,那樣就會出現比較危險的情況。

1-3、 物件的優化

後來,聰明的人類就想到了利用 立即執行函式 來達到 不暴露私有成員的目的

const module2 = (function() {
	let _money = 100
	const m1 = () => {
		console.log(123)
	}
	const m2 = () => {
		console.log(456)
	}
	return {
		f1: m1,
		f2: m2
	}
})()
複製程式碼

通過立即執行函式,讓外部根本沒有時間從外部去修改內部的屬性,從而達到一定的防禦作用。

以上就是模組化開發的基礎中的基礎。 沒有庫,沒有規範,一切的維護都是靠人力,一切的創新,都是來源於 解放生產力。

二、模組化管理的發展歷程

2-1、CommonJS

CommonJS 的出發點: JS 沒有完善的模組系統,標準庫較少,缺少包管理工具。(雖然這些東西,在後端語言中已經是 早就被玩的不想玩的東西了) 伴隨著 NodeJS 的興起,能讓 JS 可以在任何地方執行,特別是服務端,以達到 也具備 Java、C#、PHP這些後臺語言具備開發大型應用的能力,所以 CommonJS 應運而生。

2-1-1、 CommonJS常見規範

  • 一個檔案就是一個模組,擁有單獨的作用域
  • 普通方式定義的 變數、函式、物件都屬於該模組內
  • 通過 require 來載入模組
  • 通過 exportsmodule.exports 來暴露模組中的內容

我們通過編寫一個 Demo 來嘗試寫這個規範

Demo 1 : 通過 module.exports 來匯出模組

// module.js
module.exports = {
  name: "zhang",
  getName: function() {
    console.log(this.name);
  },
  changeName: function(n) {
    this.name = n;
  }
};

// index.js
const module = require("./module/index");
console.log(module)	//  {name: "zhang", getName: ƒ, changeName: ƒ} "commons"
複製程式碼

Demo 2 : 通過 exports 來匯出模組

// module1.js
const getParam = () => {
  console.log(a);
};
let a = 123;
let b = 456;

exports.a = a;
exports.b = b;
exports.getParam = getParam;

// index.js
const module1 = require("./module/index1");
consoel.log(module1, "commons1")	// {a: 123, b: 456, getParam: ƒ} "commons1"
複製程式碼

Demo 3 : 同時存在 exports 和 module.exports 來匯出模組

// module2.js
let a = 123;

const getSome = () => {
  console.log("yyy");
};

const getA = () => {
  console.log(a);
};

exports.getSome = getSome;
module.exports = getA;

// index.js
const module2 = require("./module/index2");
consoel.log(module2, "commons2")	// function getA() {...}
複製程式碼

總結 : 通過這樣的一個對比的例子就可以比較清晰的對比出 exports 和 module.exports 的區別: 1、當 exports 和 module.exports 同時存在的時候,module.exports 會蓋過 exports 2、當模組內部全部是 exports 的時候, 就等同於 module.exports 3、最後 我們就可以認定為 exports 其實就是 module.exports 的子集。

以上就是我們對於 CommonJS 的一個初級的介紹。 還有一個硬性的規範,這裡我們只是做一下列舉,就不做詳細的 Demo 演示了。

2-1-2、 CommonJS 規範 --- 載入、作用域

所有程式碼都執行在模組作用域,不會汙染全域性作用域;模組可以多次載入,但只會在第一次載入的時候執行一次,然後執行結果就被快取了,以後再載入,就直接讀取快取結果;模組的載入順序,按照程式碼的出現順序是同步載入的;

2-1-3、 CommonJS 規範 --- __dirname、__filename

__dirname代表當前模組檔案所在的資料夾路徑,__filename代表當前模組檔案所在的資料夾路徑+檔名;

以上就是關於 CommonJS 規範 相關的介紹,更下詳細的 文件,可以查閱 CommonJS 規範 官方文件。

2-2、CommonJS 與 ES6(ES2015) 的 import export

在 ES2015 標準為出來之前,最主要的是CommonJS和AMD規範。上文中我們已經介紹了 CommonJS 規範(主要是為了服務端 NodeJS 服務),那麼當 ES6標準的出現,為瀏覽器端 模組化做了一個非常好的補充。

這裡,我們還是比較詳細的介紹下 ES6 的 import export 的系列特性。

2-2-1、 ES6 的 export

export用於對外輸出本模組(一個檔案可以理解為一個模組)變數的介面

Demo 1 export { xxx }

// export/index.js
const a = "123";
const fn = () => window.location.href;

export { fn };

// show/index.js
const ex = require("./export/index");
import x from "./export/index";
import { fn } from "./export/index";
console.log(ex, "export1"); // {fn: ƒ, __esModule: true} "export1"
console.log(x, "export-x"); // undefined "export-x"
console.log(fn, "export-fn"); // function() { return window.location.href; } "export-x"
複製程式碼

Demo 2 export default xxx

// export/index1.js
const a = "123";
const fn = () => window.location.href;
export default fn;


// show/index1.js
const ex1 = require("./export/index1");
import x from "./export/index1";

console.log(ex1, "export1"); 
// {default: ƒ, __esModule: true} "export1"
console.log(x, "export2"); 
// ƒ fn() {return window.location.href;} "export2"
複製程式碼

通過 Demo 1 和 Demo 2 我們可以很好的 對比了下 export 和 export default 的區別

export 可以匯出的是一個物件中包含的多個 屬性,方法。 export default 只能匯出 一個 可以不具名的 物件。

import {fn} from './xxx/xxx' ( export 匯出方式的 引用方式 ) import fn from './xxx/xxx1' ( export default 匯出方式的 引用方式 )

同時,我們發現 可以直接使用 require 來引用

這個也能引用其實有點神奇的。但是功能和 import 一樣。(原因就是我這裡起了 webpack 的 server 相關)

2-2-2、 ES6 的 import

這裡就同上面 Demo 中的例子一樣了得出的結論就是

import {fn} from './xxx/xxx' ( export 匯出方式的 引用方式 ) import fn from './xxx/xxx1' ( export default 匯出方式的 引用方式 ,可以不用在意匯出模組的模組名)

總結: 之前對於 模組的匯出、引用 的概念都比較的魔幻,這次通過知識的梳理終於搞清楚了。?

Demo 例子傳送門: 模組化》》》

2-3、AMD 的 RequireJS

Asynchronous Module Definition,中文名是非同步模組。它是一個在瀏覽器端模組化開發的規範,由於不是js原生支援,使用AMD規範進行頁面開發需要用到對應的函式庫,也就是大名鼎鼎的RequireJS,實際上AMD是RequireJS在推廣過程中對模組定義的規範化的產出。


requireJS主要解決兩個問題:

  • 1、 多個js檔案可能有依賴關係,被依賴的檔案需要早於依賴它的檔案載入到瀏覽器。
  • 2、 js載入的時候瀏覽器會停止頁面渲染,載入檔案愈多,頁面失去響應的時間愈長。
  • 3、 非同步前置載入!(什麼意思? 後面我們在原理那個章節進行介紹)

我們通過一個 Demo 來介紹下 RequireJS 的語法:

// 定義模組
define(['myModule'],() => {
  var name = 'Byron';
  function printName(){
     console.log(name);
}
  return {
     printName:printName
   }
})

// 載入模組
require(['myModule'],function(my){
   my.printName();
})
複製程式碼

requireJS 語法:

define(id,dependencies,factory)

——id 可選引數,用來定義模組的標識,如果沒有提供該引數,指令碼檔名(去掉擴充名)

——dependencies 是一個當前模組用來的模組名稱陣列

——factory 工廠方法,模組初始化要執行的函式或物件,如果為函式,它應該只被執行一次,如果是物件,此物件應該為模組的輸出值。


在頁面上使用require函式載入模組; require([dependencies], function(){}); require()函式接受兩個引數: ——第一個引數是一個陣列,表示所依賴的模組; ——第二個引數是一個回撥函式,當前面指定的模組都載入成功後,它將被呼叫。載入的模組會以引數形式傳入該函式,從而在回撥函式內部就可以使用這些模組

2-4、CMD 的 SeaJS

define(id, deps, factory)

因為CMD推崇一個檔案一個模組,所以經常就用檔名作為模組id;
CMD推崇依賴就近,所以一般不在define的引數中寫依賴,而是在factory中寫。

factory有三個引數:
function(require, exports, module){}

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

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

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

demo
// 定義模組  myModule.js
define(function(require, exports, module) {
  var $ = require('jquery.js')
  $('div').addClass('active');
});

// 載入模組
seajs.use(['myModule.js'], function(my){

});
複製程式碼

2-5、AMD 的 RequireJS 和 CMD 的 SeaJS 的差異

AMD推崇依賴前置,在定義模組的時候就要宣告其依賴的模組. CMD推崇就近依賴,只有在用到某個模組的時候再去require.


大家說:

AMD和CMD最大的區別是對依賴模組的執行時機處理不同,注意不是載入的時機或者方式不同

很多人說requireJS是非同步載入模組,SeaJS是同步載入模組,這麼理解實際上是不準確的,其實載入模組都是非同步的,只不過AMD依賴前置,js可以方便知道依賴模組是誰,立即載入,而CMD就近依賴,需要使用把模組變為字串解析一遍才知道依賴了那些模組,這也是很多人詬病CMD的一點,犧牲效能來帶來開發的便利性,實際上解析模組用的時間短到可以忽略。

三、模組化框架原理

3-1、 實現原理

(function(global){
    var modules = {};
    var define = function (id,factory) {
        if(!modules[id]){
            modules[id] = {
                id : id,
                factory : factory
            };
        }
    };
    var require = function (id) {
        var module = modules[id];
        if(!module){
            return;
        }

        if(!module.exports){
            module.exports = {};
            module.factory.call(module.exports,require,module.exports,module);
        }

        return module.exports;
    }

    global.define = define;
    global.require = require;
})(this);
複製程式碼

使用例項:

define('Hello',function(require,exports,module){
    function sayHello() {
        console.log('hello modules');
    }
    module.exports = {
        sayHello : sayHello
    }
});

var Hello = require('Hello');
Hello.sayHello();
複製程式碼

四、總結

4-1、 為什麼會有這個東西?

方便組織你的程式碼,提高專案的可維護性。一個專案的可維護性高不高,也體現一個程式設計師的水平,在如今越來越複雜的前端專案,這一點尤為重要。

4-2、 為什麼不用requirejs,seajs等

它們功能強大,但是檔案體積是個問題,此外還有就是業務有時候可能沒那麼複雜。

4-3、 適用場景

移動端頁面,將js注入到html頁面,這樣就不用考慮模組載入的問題,從而節省了很多的程式碼,在實現上也更為的簡單。 如果是多檔案載入的話,需要手動執行檔案載入順序,那麼其實最好用庫來進行依賴管理會好一點。

4-4、 現實情況

webpack + commonJS + ES6 (import + export )

這樣來 實現模組管理,實現 較大專案的管理。 好了,模組化管理就先介紹到這裡了,歡迎一起探討


關於 前端 Javascript 模組化管理的來世今生 的文章就介紹到這裡了,歡迎一起來論道~

GitHub 地址:(歡迎 star 、歡迎推薦 : )

前端 Javascript 模組化管理的來世今生

相關文章