《前端之路》之 Javascript 模塊化管理的來世今生
目錄
- 第二章 - 04: Javascript 模塊化管理的來世今生
- 一、什麽是模塊化開發
- 1-1、模塊化第一階段
- 1-2、封裝到對象
- 1-3、 對象的優化
- 二、模塊化管理的發展歷程
- 2-1、CommonJS
- 2-2、CommonJS 與 ES6(ES2015) 的 import export
- 2-2-1、 ES6 的 export
- 2-2-2、 ES6 的 import
- 2-3、AMD 的 RequireJS
- 2-4、CMD 的 SaeJS
- 2-5、AMD 的 RequireJS 和 CMD 的 SaeJS 的差異
- 三、模塊化框架原理
- 3-1、 實現原理
- 四、總結
- 4-1、 為什麽會有這個東西?
- 4-2、 為什麽不用requirejs,seajs等
- 4-3、 適用場景
- 4-4、 現實情況
- 一、什麽是模塊化開發
第二章 - 04: Javascript 模塊化管理的來世今生
模塊管理這個概念其實在前幾年前端度過了刀耕火種年代之後就一直被提起,那麽我們有思考過這個模塊管理具體指的是什麽東西?什麽樣子的展現形式?歷史由來?現在是什麽樣的一個狀態?
直接回想起來的就是 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 = { let _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
來家在模塊 - 通過
exports
和module.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 的 SaeJS
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 的 SaeJS 的差異
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 模塊化管理的來世今生
《前端之路》之 Javascript 模塊化管理的來世今生