摘要:即盡早地執行依賴模塊。阮一峰輸出值的引用模塊是動態關聯模塊中的值,輸出的是值得引用。的加載實現阮一峰運行時加載靜態編譯模塊是運行時加載,模塊是編譯時輸出接口。
模塊化開發 優點
模塊化開發中,通常一個文件就是一個模塊,有自己的作用域,只向外暴露特定的變量和函數,并且可以按需加載。
依賴自動加載,按需加載。
提高代碼復用率,方便進行代碼的管理,使得代碼管理更加清晰、規范。
減少了命名沖突,消除全局變量。
目前流行的js模塊化規范有CommonJS、AMD、CMD以及ES6的模塊系統
常見模塊化規范CommonJs (Node.js)
AMD (RequireJS)
CMD (SeaJS)
CommonJS(Node.js)CommonJS是服務器模塊的規范,Node.js采用了這個規范。
根據 CommonJS 規范,一個多帶帶的文件就是一個模塊,每一個模塊都是一個多帶帶的作用域,在一個文件定義的變量(還包括函數和類),都是私有的,對其他文件是不可見的。
CommonJS規范加載模塊是同步的,也就是說,只有加載完成,才能執行后面的操作。
CommonJS 中,加載模塊使用 require 方法。該方法讀取一個文件并執行,最后返回文件內部的 exports 對象。
Node.js 主要用于服務器編程,加載的模塊文件一般都已經存在本地硬盤,加載起來較快,不用考慮異步加載的方式,所以 CommonJS 的同步加載模塊規范是比較適用的。但如果是瀏覽器環境,要從服務器加載模塊,這是就必須采用異步模式。所以就有了 AMD,CMD 等解決方案。
var x = 5; var addX = function(value) { return value + x; }; module.exports.x = x; module.exports.addX = addX; // 也可以改寫為如下 module.exports = { x: x, addX: addX, };
let math = require("./math.js"); console.log("math.x",math.x); console.log("math.addX", math.addX(4));AMD (RequireJS) 異步模塊定義
AMD = Asynchronous Module Definition,即 異步模塊定義。
AMD 規范加載模塊是異步的,并允許函數回調,不必等到所有模塊都加載完成,后續操作可以正常執行。
AMD 中,使用 require 獲取依賴模塊,使用 exports 導出 API。
//規范 API define(id?, dependencies?, factory); define.amd = {}; // 定義無依賴的模塊 define({ add: function(x,y){ return x + y; } }); // 定義有依賴的模塊 define(["alpha"], function(alpha){ return { verb: function(){ return alpha.verb() + 1; } } });異步加載和回調
require([module], callback) 中 callback 為模塊加載完成后的回調函數
//加載 math模塊,完成之后執行回調函數 require(["math"], function(math) { math.add(2, 3); });RequireJS
RequireJS 是一個前端模塊化管理的工具庫,遵循 AMD 規范,RequireJS 是對 AMD 規范的闡述。
RequireJS 基本思想為,通過一個函數來將所有所需的或者所依賴的模塊裝載進來,然后返回一個新的函數(模塊)。后續所有的關于新模塊的業務代碼都在這個函數內部操作。
RequireJS 要求每個模塊均放在獨立的文件之中,并使用 define 定義模塊,使用 require 方法調用模塊。
按照是否有依賴其他模塊情況,可以分為 獨立模塊 和 非獨立模塊。
獨立模塊,不依賴其他模塊,直接定義
define({ method1: function(){}, method2: function(){} }); //等價于 define(function() { return { method1: function(){}, method2: function(){} } });
非獨立模塊,依賴其他模塊
define([ "module1", "module2" ], function(m1, m2) { ... }); //等價于 define(function(require) { var m1 = require("module1"); var m2 = require("module2"); ... });
require 方法調用模塊
require(["foo", "bar"], function(foo, bar) { foo.func(); bar.func(); });CMD (SeaJS)
CMD = Common Module Definition,即 通用模塊定義。CMD 是 SeaJS 在推廣過程中對模塊定義的規范化產出。
CMD規范和AMD類似,都主要運行于瀏覽器端,寫法上看起來也很類似。主要是區別在于 模塊初始化時機
AMD中只要模塊作為依賴時,就會加載并初始化
CMD中,模塊作為依賴且被引用時才會初始化,否則只會加載。
CMD 推崇依賴就近,AMD 推崇依賴前置。
AMD 的 API 默認是一個當多個用,CMD 嚴格的區分推崇職責單一。例如,AMD 里 require 分全局的和局部的。CMD里面沒有全局的 require,提供 seajs.use() 來實現模塊系統的加載啟動。CMD 里每個 API 都簡單純粹。
//AMD define(["./a","./b"], function (a, b) { //依賴一開始就寫好 a.test(); b.test(); }); //CMD define(function (requie, exports, module) { //依賴可以就近書寫 var a = require("./a"); a.test(); ... //軟依賴 if (status) { var b = requie("./b"); b.test(); } });Sea.js
Sea.js Github Page
SeaJS與RequireJS最大的區別
使用Sea.js,在書寫文件時,需要遵守CMD(Common Module Definition)模塊定義規范。一個文件就是一個模塊。
用法通過 exports 暴露接口。這意味著不需要命名空間了,更不需要全局變量。這是一種徹底的命名沖突解決方案。
通過 require 引入依賴。這可以讓依賴內置,開發者只需關心當前模塊的依賴,其他事情 Sea.js 都會自動處理好。對模塊開發者來說,這是一種很好的 關注度分離,能讓程序員更多地享受編碼的樂趣。
通過 define 定義模塊,更多詳情參考SeasJS | 極客學院。
示例例如,對于下述util.js代碼
var org = {}; org.CoolSite = {}; org.CoolSite.Utils = {}; org.CoolSite.Utils.each = function (arr) { // 實現代碼 }; org.CoolSite.Utils.log = function (str) { // 實現代碼 };
可以采用SeaJS重寫為
define(function(require, exports) { exports.each = function (arr) { // 實現代碼 }; exports.log = function (str) { // 實現代碼 }; });
通過 exports 就可以向外提供接口。通過 require("./util.js") 就可以拿到 util.js 中通過 exports 暴露的接口。這里的 require 可以認為是 Sea.js 給 JavaScript 語言增加的一個語法關鍵字,通過 require 可以獲取其他模塊提供的接口。
define(function(require, exports) { var util = require("./util.js"); exports.init = function() { // 實現代碼 }; });SeaJS與RequireJS區別
二者區別主要表現在模塊初始化時機
AMD(RequireJS)中只要模塊作為依賴時,就會加載并初始化。即盡早地執行(依賴)模塊。相當于所有的require都被提前了,而且模塊執行的順序也不一定100%就是require書寫順序。
CMD(SeaJS)中,模塊作為依賴且被引用時才會初始化,否則只會加載。即只會在模塊真正需要使用的時候才初始化。模塊加載的順序是嚴格按照require書寫的順序。
從規范上來說,AMD 更加簡單且嚴謹,適用性更廣,而在RequireJS強力的推動下,在國外幾乎成了事實上的異步模塊標準,各大類庫也相繼支持AMD規范。
但從SeaJS與CMD來說,也做了很多不錯東西:1、相對自然的依賴聲明風格 2、小而美的內部實現 3、貼心的外圍功能設計 4、更好的中文社區支持。
UMDUMD = Universal Module Definition,即通用模塊定義。UMD 是AMD 和 CommonJS的糅合。
AMD 模塊以瀏覽器第一的原則發展,異步加載模塊。
CommonJS 模塊以服務器第一原則發展,選擇同步加載。它的模塊無需包裝(unwrapped modules)。
這迫使人們又想出另一個更通用的模式 UMD(Universal Module Definition),實現跨平臺的解決方案。
UMD 先判斷是否支持 Node.js 的模塊(exports)是否存在,存在則使用 Node.js 模塊模式。再判斷是否支持 AMD(define 是否存在),存在則使用 AMD 方式加載模塊。
(function (window, factory) { if (typeof exports === "object") { module.exports = factory(); } else if (typeof define === "function" && define.amd) { define(factory); } else { window.eventUtil = factory(); } })(this, function () { //module ... });ES6 模塊 ES6模塊和CommonJS區別
ES6 模塊輸出的是值的引用,輸出接口動態綁定,而 CommonJS 輸出的是值的拷貝。
CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。
CommonJS 輸出值的拷貝CommonJS 模塊輸出的是值的拷貝(類比于基本類型和引用類型的賦值操作)。對于基本類型,一旦輸出,模塊內部的變化影響不到這個值。對于引用類型,效果同引用類型的賦值操作。
// lib.js var counter = 3; var obj = { name: "David" }; function changeValue() { counter++; obj.name = "Peter"; }; module.exports = { counter: counter, obj: obj, changeValue: changeValue, };
// main.js var mod = require("./lib"); console.log(mod.counter); // 3 console.log(mod.obj.name); // "David" mod.changeValue(); console.log(mod.counter); // 3 console.log(mod.obj.name); // "Peter" // Or console.log(require("./lib").counter); // 3 console.log(require("./lib").obj.name); // "Peter"
counter 是基本類型值,模塊內部值的變化不影響輸出的值變化。
obj 是引用類型值,模塊內部值的變化影響輸出的值變化。
上述兩點區別,類比于基本類型和引用類型的賦值操作。
也可以借助取值函數(getter),將 counter 轉為引用類型值,效果如下。
在類的內部,可以使用 get 和 set 關鍵字,對某個屬性設置存執函數和取值函數,攔截該屬性的存取行為。 —— class | 阮一峰
// lib.js var counter = 3; function incCounter() { counter++; } module.exports = { get counter() { return counter }, incCounter: incCounter, };
// main.js var mod = require("./lib"); console.log(mod.counter); // 3 mod.incCounter(); console.log(mod.counter); // 4ES6 輸出值的引用
ES6 模塊是動態關聯模塊中的值,輸出的是值得引用。原始值變了,import 加載的值也會跟著變。
ES6 模塊的運行機制與 CommonJS 不一樣。JS 引擎對腳本靜態分析時,遇到模塊加載命令 import,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊里面去取值。ES6 模塊中,原始值變了,import 加載的值也會跟著變。因此,ES6 模塊是動態引用,并且不會緩存值。 —— ES6 Module 的加載實現 | 阮一峰
// 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); // 4CommonJS 運行時加載 ES6靜態編譯
CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。
這是因為,CommonJS 加載的是一個對象(即 module.exports 屬性),該對象只有在腳本運行完才會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。
ES6 模塊是編譯時輸出接口,因此有如下2個特點
import 命令會被 JS 引擎靜態分析,優先于模塊內的其他內容執行
export 命令會有變量聲明提升的效果
在文件中的任何位置引入 import 模塊都會被提前到文件頂部
// a.js console.log("a.js") import { foo } from "./b"; // b.js export let foo = 1; console.log("b.js 先執行"); // 執行結果: // b.js 先執行 // a.js
雖然 a 模塊中 import 引入晚于 console.log("a"),但是它被 JS 引擎通過靜態分析,提到模塊執行的最前面,優于模塊中的其他部分的執行。
由于 import 和 export 是靜態執行,所以 import 和 export 具有變量提升效果。即 import 和 export 命令在模塊中的位置并不影響程序的輸出。
// a.js import { foo } from "./b"; console.log("a.js"); export const bar = 1; export const bar2 = () => { console.log("bar2"); } export function bar3() { console.log("bar3"); } // b.js export let foo = 1; import * as a from "./a"; console.log(a); // 執行結果: // { bar: undefined, bar2: undefined, bar3: [Function: bar3] } // a.js
a 模塊引用了 b 模塊,b 模塊也引用了 a 模塊,export 聲明的變量也是優于模塊其它內容的執行的。但具體對變量賦值需要等到執行到相應代碼的時候。
ES6模塊和CommonJS相同點 模塊不會重復執行重復引入某個相同的模塊時,模塊只會執行一次。
循環依賴 CommonJS 模塊循環依賴CommonJS 模塊的重要特性是加載時執行,即腳本代碼在 require 的時候,就會全部執行。一旦出現某個模塊被“循環加載”,就只輸出已經執行的部分,還未執行的部分不會輸出。
//a.js exports.done = false; var b = require("./b.js"); console.log("在 a.js 之中,b.done = %j", b.done); exports.done = true; console.log("a.js 執行完畢");
上面代碼之中,a.js 腳本先輸出一個 done 變量,然后加載另一個腳本文件 b.js。注意,此時 a.js 代碼就停在這里,等待 b.js 執行完畢,再往下執行。
再看 b.js 的代碼。
//b.js exports.done = false; var a = require("./a.js"); console.log("在 b.js 之中,a.done = %j", a.done); exports.done = true; console.log("b.js 執行完畢");
上面代碼之中,b.js 執行到第二行,就會去加載 a.js,這時,就發生了“循環加載”。系統會 a.js 模塊對應對象的 exports 屬性取值,可是因為 a.js 還沒有執行完,從 exports 屬性只能取回已經執行的部分,而不是最后的值。
a.js 已經執行的部分,只有一行。
exports.done = false;
因此,對于 b.js來說,它從 a.js 只輸入一個變量 done,值為 false。
然后,b.js 接著往下執行,等到全部執行完畢,再把執行權交還給 a.js。于是,a.js 接著往下執行,直到執行完畢。我們寫一個腳本 main.js,驗證這個過程。
var a = require("./a.js"); var b = require("./b.js"); console.log("在 main.js 之中, a.done=%j, b.done=%j", a.done, b.done);
執行 main.js,運行結果如下。
$ node main.js 在 b.js 之中,a.done = false b.js 執行完畢 在 a.js 之中,b.done = true a.js 執行完畢 在 main.js 之中, a.done=true, b.done=true
上面的代碼證明了2點
在 b.js 之中,a.js 沒有執行完畢,只執行了第一行
main.js 執行到第二行時,不會再次執行 b.js,而是輸出緩存的 b.js 的執行結果,即它的第四行。
exports.done = true;
總之,CommonJS 輸入的是被輸出值的拷貝,不是引用。
另外,由于 CommonJS 模塊遇到循環加載時,返回的是當前已經執行的部分的值,而不是代碼全部執行后的值,兩者可能會有差異。所以,輸入變量的時候,必須非常小心。
var a = require("a"); // 安全的寫法 導入整體,保證module已經執行完成 var foo = require("a").foo; // 危險的寫法 exports.good = function (arg) { return a.foo("good", arg); // 使用的是 a.foo 的最新值 }; exports.bad = function (arg) { return foo("bad", arg); // 使用的是一個部分加載時的值 };
上面代碼中,如果發生循環加載,require("a").foo 的值很可能后面會被改寫,改用 require("a") 會更保險一點。
// a.js console.log("a starting"); exports.done = false; const b = require("./b"); console.log("in a, b.done =", b.done); exports.done = true; console.log("a done"); // b.js console.log("b starting"); exports.done = false; const a = require("./a"); console.log("in b, a.done =", a.done); exports.done = true; console.log("b done"); // node a.js // 執行結果: // a starting // b starting // in b, a.done = false // b done // in a, b.done = true // a done
從上面的執行過程中,可以看到,在 CommonJS 規范中,當遇到 require() 語句時,會執行 require 模塊中的代碼,并緩存執行的結果,當下次再次加載時不會重復執行,而是直接取緩存的結果。正因為此,出現循環依賴時才不會出現無限循環調用的情況。
ES6 模塊循環依賴跟 CommonJS 模塊一樣,ES6 不會再去執行重復加載的模塊,又由于 ES6 動態輸出綁定的特性,能保證 ES6 在任何時候都能獲取其它模塊當前的最新值。
動態 import()ES6 模塊在編譯時就會靜態分析,優先于模塊內的其他內容執行,所以導致了我們無法寫出像下面這樣的代碼
if(some condition) { import a from "./a"; }else { import b from "./b"; } // or import a from (str + "b");
因為編譯時靜態分析,導致了我們無法在條件語句或者拼接字符串模塊,因為這些都是需要在運行時才能確定的結果在 ES6 模塊是不被允許的,所以 動態引入import() 應運而生。
import() 允許你在運行時動態地引入 ES6 模塊,想到這,你可能也想起了 require.ensure 這個語法,但是它們的用途卻截然不同的。
require.ensure 的出現是 webpack 的產物,它是因為瀏覽器需要一種異步的機制可以用來異步加載模塊,從而減少初始的加載文件的體積,所以如果在服務端的話, require.ensure 就無用武之地了,因為服務端不存在異步加載模塊的情況,模塊同步進行加載就可以滿足使用場景了。 CommonJS 模塊可以在運行時確認模塊加載。
而 import() 則不同,它主要是為了解決 ES6 模塊無法在運行時確定模塊的引用關系,所以需要引入 import()。
先來看下它的用法
動態的 import() 提供一個基于 Promise 的 API
動態的 import() 可以在腳本的任何地方使用 import() 接受字符串文字,可以根據需要構造說明符
// a.js const str = "./b"; const flag = true; if(flag) { import("./b").then(({foo}) => { console.log(foo); }) } import(str).then(({foo}) => { console.log(foo); }) // b.js export const foo = "foo"; // babel-node a.js // 執行結果 // foo // foo
當然,如果在瀏覽器端的 import() 的用途就會變得更廣泛,比如 按需異步加載模塊,那么就和 require.ensure 功能類似了。
因為是基于 Promise 的,所以如果你想要同時加載多個模塊的話,可以是 Promise.all 進行并行異步加載。
Promise.all([ import("./a.js"), import("./b.js"), import("./c.js"), ]).then(([a, {default: b}, {c}]) => { console.log("a.js is loaded dynamically"); console.log("b.js is loaded dynamically"); console.log("c.js is loaded dynamically"); });
還有 Promise.race 方法,它檢查哪個 Promise 被首先 resolved 或 reject。我們可以使用 import() 來檢查哪個 CDN 速度更快:
const CDNs = [ { name: "jQuery.com", url: "https://code.jquery.com/jquery-3.1.1.min.js" }, { name: "googleapis.com", url: "https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js" } ]; console.log(`------`); console.log(`jQuery is: ${window.jQuery}`); Promise.race([ import(CDNs[0].url).then(()=>console.log(CDNs[0].name, "loaded")), import(CDNs[1].url).then(()=>console.log(CDNs[1].name, "loaded")) ]).then(()=> { console.log(`jQuery version: ${window.jQuery.fn.jquery}`); });
當然,如果你覺得這樣寫還不夠優雅,也可以結合 async/await 語法糖來使用。
async function main() { const myModule = await import("./myModule.js"); const {export1, export2} = await import("./myModule.js"); const [module1, module2, module3] = await Promise.all([ import("./module1.js"), import("./module2.js"), import("./module3.js"), ]); }
動態 import() 為我們提供了以異步方式使用 ES 模塊的額外功能。
根據我們的需求動態或有條件地加載它們,這使我們能夠更快,更好地創建更多優勢應用程序。
webpack中加載3種模塊 | 語法Webpack允許使用不同的模塊類型,但是底層必須使用同一種實現。所有的模塊可以直接在盒外運行。
ES6 模塊
import MyModule from "./MyModule.js";
CommonJS(Require)
var MyModule = require("./MyModule.js");
AMD
define(["./MyModule.js"], function (MyModule) { });參考資料
AMD, CMD, CommonJS和UMD | Segmentfault
JS模塊化加載之CommonJS、AMD、CMD、ES6
ES6 module的加載和實現 | 阮一峰
前端模塊化開發方案小對比
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/109292.html
摘要:若不存在則模塊標識應該默認定義為在加載器中被請求腳本的標識。這也是目前很多插件頭部的寫法,就是用來兼容各種不同模塊化的寫法。語句輸出的值是動態綁定的,綁定其所在的模塊。 前言 歷史上,js沒有模塊化的概念,不能把一個大工程分解成很多小模塊。這對于多人開發大型,復雜的項目形成了巨大的障礙,明顯降低了開發效率,java,Python有import,甚至連css都有@import,但是令人費...
摘要:常見模塊化方案是由社區提出的模塊化方案中的一種,遵循了這套方案。是模塊化規范中的一種,遵循了這套規范。中的模塊化能力由兩個命令構成和,命令用于規定模塊的對外接口,命令用于輸入其他模塊提供的功能。 為什么需要模塊化 在ES6出現之前,JS語言本身并沒有提供模塊化能力,這為開發帶來了一些問題,其中最重要的兩個問題應當是全局污染和依賴管理混亂。 // file a.js var name =...
摘要:要想讓模塊再次運行,必須清除緩存。模塊加載會阻塞接下來代碼的執行,需要等到模塊加載完成才能繼續執行同步加載。環境服務器環境應用的模塊規范是參照實現的。這等同在每個模塊頭部,有一行這樣的命令。 commonJS 特點: 1、模塊可以多次加載,但是只會在第一次加載時運行一次,然后運行結果就被緩存了,以后再加載,就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存。2、模塊加載會阻塞接下來代...
摘要:參考資料前端模塊化詳解完整版入門近一萬字的語法知識點補充徹底搞清楚中的和和詳解 前言 前端的模塊化之路經歷了漫長的過程,想詳細了解的小伙伴可以看浪里行舟大神寫的前端模塊化詳解(完整版),這里根據幾位大佬們寫的文章,將模塊化規范部分做了匯總和整理,希望讀完的小伙伴能有些收獲,也希望覺得有用的小伙伴可以點個贊,筆芯。 什么是模塊 將一個復雜的程序依據一定的規則(規范)封裝成幾個塊(文件)...
摘要:依賴全部加載完成后,調用回調函數規范異步加載模塊規范和很相似,簡單,并與和的規范保持了很大的兼容性在規范中,一個模塊就是一個文件。 拋出問題: 在開發中在導入模塊時經常使用require和import; 導出模塊時使用module.exports/exports或者export/export default; 有時候為了引用一個模塊會使用require奇怪的是也可以使用import?...
閱讀 2983·2021-11-23 09:51
閱讀 2997·2021-11-02 14:46
閱讀 862·2021-11-02 14:45
閱讀 2738·2021-09-23 11:57
閱讀 2492·2021-09-23 11:22
閱讀 1923·2019-08-29 16:29
閱讀 740·2019-08-29 16:16
閱讀 937·2019-08-26 13:44