這是一篇關于js模塊化編程的總結記錄
隨著網站逐漸變成”互聯網應用程序”,嵌入網頁的javascript代碼越來越龐大,越來越復雜。
網頁越來越像桌面程序,需要一個團隊分工協作、進度管理、單元測試等等……開發者不得不使用軟件工程的方法,管理網頁的業務邏輯。
Javascript模塊化編程,已經成為一個迫切的需求。理想情況下,開發者只需要實現核心的業務邏輯,其他都可以加載別人已經寫好的模塊。
Javascript社區做了很多努力,在現有的運行環境中,實現”模塊”的效果。
這里的CommonJS規范指的是CommonJS Modules/1.0規范。
CommonJS是一個更偏向于服務器端的規范。NodeJS采用了這個規范。CommonJS的一個模塊就是一個腳本文件。require命令第一次加載該腳本時就會執行整個腳本,然后在內存中生成一個對象。
{ id: '...', exports: { ... }, loaded: true, ...}id是模塊名,exports是該模塊導出的接口,loaded表示模塊是否加載完畢。此外還有很多屬性,這里省略了。
以后需要用到這個模塊時,就會到exports屬性上取值。即使再次執行require命令,也不會再次執行該模塊,而是到緩存中取值。
// math.jsexports.add = function(a, b) { return a + b;}var math = require('math');math.add(2, 3); // 5由于CommonJS是同步加載模塊,這對于服務器端不是一個問題,因為所有的模塊都放在本地硬盤。等待模塊時間就是硬盤讀取文件時間,很小。但是,對于瀏覽器而言,它需要從服務器加載模塊,涉及到網速,代理等原因,一旦等待時間過長,瀏覽器處于”假死”狀態。
所以在瀏覽器端,不適合于CommonJS規范。所以在瀏覽器端又出現了一個規范—-AMD。
CommonJS解決了模塊化的問題,但這種同步加載方式并不適合于瀏覽器端。
AMD是”Asynchronous Module Definition”的縮寫,即”異步模塊定義”。它采用異步方式加載模塊,模塊的加載不影響它后面語句的運行。 這里異步指的是不堵塞瀏覽器其他任務(dom構建,CSS渲染等),而加載內部是同步的(加載完模塊后立即執行回調)。
AMD也采用require命令加載模塊,但是不同于CommonJS,它要求兩個參數:
require([module], callback);第一個參數[module],是一個數組,里面的成員是要加載的模塊,callback是加載完成后的回調函數。如果將上述的代碼改成AMD方式:
require(['math'], function(math) { math.add(2, 3);})其中,回調函數中參數對應數組中的成員(模塊)。
requireJS加載模塊,采用的是AMD規范。也就是說,模塊必須按照AMD規定的方式來寫。
具體來說,就是模塊書寫必須使用特定的define()函數來定義。如果一個模塊不依賴其他模塊,那么可以直接寫在define()函數之中。
define(id?, dependencies?, factory);id:模塊的名字,如果沒有提供該參數,模塊的名字應該默認為模塊加載器請求的指定腳本的名字;
dependencies:模塊的依賴,已被模塊定義的模塊標識的數組字面量。依賴參數是可選的,如果忽略此參數,它應該默認為 ["require", "exports", "module"]
。然而,如果工廠方法的長度屬性小于3,加載器會選擇以函數的長度屬性指定的參數個數調用工廠方法。
factory:模塊的工廠函數,模塊初始化要執行的函數或對象。如果為函數,它應該只被執行一次。如果是對象,此對象應該為模塊的輸出值。
假定現在有一個math.js文件,定義了一個math模塊。那么,math.js書寫方式如下:
// math.jsdefine(function() { var add = function(x, y) { return x + y; } return { add: add }})加載方法如下:
// main.jsrequire(['math'], function(math) { alert(math.add(1, 1));})如果math模塊還依賴其他模塊,寫法如下:
// math.jsdefine(['dependenceModule'], function(dependenceModule) { // ...})當require()函數加載math模塊的時候,就會先加載dependenceModule模塊。當有多個依賴時,就將所有的依賴都寫在define()函數第一個參數數組中,所以說AMD是依賴前置的。這不同于CMD規范,它是依賴就近的。
CMD推崇依賴就近,延遲執行??梢园涯愕囊蕾噷戇M代碼的任意一行,如下:
define(factory)factory
為函數時,表示是模塊的構造方法。執行該構造方法,可以得到模塊向外提供的接口。factory 方法在執行時,默認會傳入三個參數:require、exports 和 module.
如果使用AMD寫法,如下:
// AMDdefine(['a', 'b'], function(a, b) { a.doSomething(); b.doSomething();})這個規范實際上是為了Seajs的推廣然后搞出來的。那么看看SeaJS是怎么回事兒吧,基本就是知道這個規范了。
同樣Seajs也是預加載依賴js跟AMD的規范在預加載這一點上是相同的,明顯不同的地方是調用,和聲明依賴的地方。AMD和CMD都是用difine和require,但是CMD標準傾向于在使用過程中提出依賴,就是不管代碼寫到哪突然發現需要依賴另一個模塊,那就在當前代碼用require引入就可以了,規范會幫你搞定預加載,你隨便寫就可以了。但是AMD標準讓你必須提前在頭部依賴參數部分寫好(沒有寫好? 倒回去寫好咯)。這就是最明顯的區別。
sea.js通過sea.use()
來加載模塊。
由于CommonJS是服務器端的規范,跟AMD、CMD兩個標準實際不沖突。
當我們寫一個文件需要兼容不同的加載規范的時候怎么辦呢,看看下面的代碼。
(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD define(['jquery', 'underscore'], factory); } else if (typeof exports === 'object') { // Node, CommonJS之類的 module.exports = factory(require('jquery'), require('underscore')); } else { // 瀏覽器全局變量(root 即 window) root.returnExports = factory(root.jQuery, root._); }}(this, function ($, _) { // 方法 function a(){}; // 私有方法,因為它沒被返回 (見下面) function b(){}; // 公共方法,因為被返回了 function c(){}; // 公共方法,因為被返回了 // 暴露公共方法 return { b: b, c: c }}));這個代碼可以兼容各種加載規范了。
es6通過import
、export
實現模塊的輸入輸出。其中import命令用于輸入其他模塊提供的功能,export命令用于規定模塊的對外接口。
一個模塊就是一個獨立的文件。該文件內部的所有變量,外部無法獲取。如果希望外部文件能夠讀取該模塊的變量,就需要在這個模塊內使用export關鍵字導出變量。如:
// PRofile.jsexport var a = 1;export var b = 2;export var c = 3;下面的寫法是等價的,這種方式更加清晰(在底部一眼能看出導出了哪些變量):
var a = 1;var b = 2;var c = 3;export {a, b, c}export命令除了輸出變量,還可以導出函數或類。
導出函數export function foo(){}function foo(){}function bar(){}export {foo, bar as bar2}其中上面的as表示給導出的變量重命名。
要注意的是,export導出的變量只能位于文件的頂層,如果處于塊級作用域內,會報錯。如:
function foo() { export 'bar'; // SyntaxError}導出類export default class {} // 關于default下面會說export語句輸出的值是動態綁定,綁定其所在的模塊。
// foo.jsexport var foo = 'foo';setTimeout(function() { foo = 'foo2';}, 500);// main.jsimport * as m from './foo';console.log(m.foo); // foosetTimeout(() => console.log(m.foo), 500); // foo2import命令可以導入其他模塊通過export導出的部分。
// abc.jsvar a = 1;var b = 2;var c = 3;export {a, b, c}//main.jsimport {a, b, c} from './abc';console.log(a, b, c);如果想為導入的變量重新取一個名字,使用as關鍵字(也可以在導出中使用)。
import {a as aa, b, c};console.log(aa, b, c)如果想在一個模塊中先輸入后輸出一個模塊,import語句可以和export語句寫在一起。
import {a, b, c} form './abc';export {a, b, c}// 使用連寫, 可讀性不好,不建議export {a, b, c} from './abc';使用*關鍵字。
// abc.jsexport var a = 1;export var b = 2;export var c = 3;// main.jsimport * from as abc form './abc';console.log(abc.a, abc.b, abc.c);在export輸出內容時,如果同時輸出多個變量,需要使用大括號{}
,同時導入也需要大括號。使用export defalut
輸出時,不需要大括號,而輸入(import)export default
輸出的變量時,不需要大括號。
本質上,export default
輸出的是一個叫做default的變量或方法,輸入這個default變量時不需要大括號。
就到這里了吧。關于循環加載(模塊相互依賴)沒寫,CommonJS和ES6處理方式不一樣。
該如何理解AMD ,CMD,CommonJS規范–javascript模塊化加載學習總結
AMD/CMD與前端規范
前端模塊化之旅(二):CommonJS、AMD和CMD
研究一下javascript的模塊規范(CommonJs/AMD/CMD)
Javascript模塊化編程(一):模塊的寫法
Javascript模塊化編程(二):AMD規范
Javascript模塊化編程(三):require.js的用法
Module
新聞熱點
疑難解答