摘要:參考精讀模塊化發展模塊化七日談前端模塊化開發那點歷史本文先發布于我的個人博客模塊化開發的演進歷程,后續如有更新,可以查看原文。
Brendan Eich用了10天就創造了JavaScript,因為當時的需求定位,導致了在設計之初,在語言層就不包含很多高級語言的特性,其中就包括模塊這個特性,但是經過了這么多年的發展,如今對JavaScript的需求已經遠遠超出了Brendan Eich的預期,其中模塊化開發更是其中最大的需求之一。
尤其是2009年Node.js出現以后,CommonJS規范的落地極大的推動了整個社區的模塊化開發氛圍,并且隨之出現了AMD、CMD、UMD等等一系列可以在瀏覽器等終端實現的異步加載的模塊化方案。
此前,雖然自己也一直在推進模塊化開發,但是沒有深入了解過模塊化演進的歷史,直到最近看到了一篇文章《精讀JS模塊化發展》,文章總結了History of JavaScript這個開源項目中關于JavaScript模塊化演進的部分,細讀幾次之后,對于一些以前模棱兩可的東西,頓時清晰了不少,下面就以時間線總結一下自己的理解:
在1999年的時候,絕大部分工程師做JS開發的時候就直接將變量定義在全局,做的好一些的或許會做一些文件目錄規劃,將資源歸類整理,這種方式被稱為直接定義依賴,舉個例子:
// greeting.js var helloInLang = { en: "Hello world!", es: "?Hola mundo!", ru: "Привет мир!" }; function writeHello(lang) { document.write(helloInLang[lang]); } // third_party_script.js function writeHello() { document.write("The script is broken"); } // index.htmlBasic example
但是,即使有規范的目錄結構,也不能避免由此而產生的大量全局變量,這就導致了一不小心就會有變量沖突的問題,就好比上面這個例子中的writeHello。
于是在2002年左右,有人提出了命名空間模式的思路,用于解決遍地的全局變量,將需要定義的部分歸屬到一個對象的屬性上,簡單修改上面的例子,就能實現這種模式:
// greeting.js var app = {}; app.helloInLang = { en: "Hello world!", es: "?Hola mundo!", ru: "Привет мир!" }; app.writeHello = function (lang) { document.write(helloInLang[lang]); } // third_party_script.js function writeHello() { document.write("The script is broken"); }
不過這種方式,毫無隱私可言,本質上就是全局對象,誰都可以來訪問并且操作,一點都不安全。
所以在2003年左右就有人提出利用IIFE結合Closures特性,以此解決私有變量的問題,這種模式被稱為閉包模塊化模式:
// greeting.js var greeting = (function() { var module = {}; var helloInLang = { en: "Hello world!", es: "?Hola mundo!", ru: "Привет мир!", }; module.getHello = function(lang) { return helloInLang[lang]; }; module.writeHello = function(lang) { document.write(module.getHello(lang)); }; return module; })();
IIFE可以形成一個獨立的作用域,其中聲明的變量,僅在該作用域下,從而達到實現私有變量的目的,就如上面例子中的helloInLang,在該IIFE外是不能直接訪問和操作的,可以通過暴露一些方法來訪問和操作,比如說上面例子里面的getHello和writeHello2個方法,這就是所謂的Closures。
同時,不同模塊之間的引用也可以通過參數的形式來傳遞:
// x.js // @require greeting.js var x = (function(greeting) { var module = {}; module.writeHello = function(lang) { document.write(greeting.getHello(lang)); }; return module; })(greeting);
此外使用IIFE,還有2個好處:
提高性能:通過IIFE的參數傳遞常用全局對象window、document,在作用域內引用這些全局對象。JavaScript解釋器首先在作用域內查找屬性,然后一直沿著鏈向上查找,直到全局范圍,因此將全局對象放在IIFE作用域內可以提升js解釋器的查找速度和性能;
壓縮空間:通過參數傳遞全局對象,壓縮時可以將這些全局對象匿名為一個更加精簡的變量名;
在那個年代,除了這種解決思路以外,還有通過其它語言的協助來完成模塊化的解決思路,比如說模版依賴定義、注釋依賴定義、外部依賴定義等等,不過不常見,所以就不細說了,究其本源,它們想最終實現的方式都差不多。
不過,這些方案,雖然解決了依賴關系的問題,但是沒有解決如何管理這些模塊,或者說在使用時清晰描述出依賴關系,這點還是沒有被解決,可以說是少了一個管理者。
沒有管理者的時候,在實際項目中,得手動管理第三方的庫和項目封裝的模塊,就像下面這樣把所有需要的JS文件一個個按照依賴的順序加載進來:
如果頁面中使用的模塊數量越來越多,恐怕再有經驗的工程師也很難維護好它們之間的依賴關系了。
于是如LABjs之類的加載工具就橫空出世了,通過使用它的API,動態創建,從而達到控制JS文件加載以及執行順序的目的,在一定的程度上解決了依賴關系,例如:
$LAB.script("greeting.js").wait() .script("x.js") .script("y.js").wait() .script("run.js");
不過LABjs之類的加載工具是建立在以文件為單位的基礎之上的,但是JS中的模塊又不一定必須是文件,同一個文件中可以聲明多個模塊,YUI作為昔日前端領域的佼佼者,很好的糅合了命名空間模式及沙箱模式,下面來一睹它的風采:
// YUI - 編寫模塊 YUI.add("dom", function(Y) { Y.DOM = { ... } }) // YUI - 使用模塊 YUI().use("dom", function(Y) { Y.DOM.doSomeThing(); // use some methods DOM attach to Y }) // hello.js YUI.add("hello", function(Y){ Y.sayHello = function(msg){ Y.DOM.set(el, "innerHTML", "Hello!"); } },"3.0.0",{ requires:["dom"] }) // main.js YUI().use("hello", function(Y){ Y.sayHello("hey yui loader"); })
此外,YUI團隊還提供的一系列用于JS壓縮、混淆、請求合并(合并資源需要server端配合)等性能優化的工具,說其是現有JS模塊化的鼻祖一點都不過分。
不過,隨著Node.js的到來,CommonJS規范的落地以及各種前端工具、解決方案的出現,很快,YUI3就被湮沒在了歷史的長流里面,這樣成為了JS模塊化開發的一個分水嶺,引用一段描述:
從 1999 年開始,模塊化探索都是基于語言層面的優化,真正的革命從 2009 年 CommonJS 的引入開始,前端開始大量使用預編譯。
CommonJS是一套同步的方案,它考慮的是在服務端運行的Node.js,主要是通過require來加載依賴項,通過exports或者module.exports來暴露接口或者數據的方式,想了解更多,可以看一下《CommonJS規范》,下面舉個簡單的例子:
var math = require("math"); esports.result = math.add(2,3); // 5
由于服務器上通過require加載資源是直接讀取文件的,因此中間所需的時間可以忽略不計,但是在瀏覽器這種需要依賴HTTP獲取資源的就不行了,資源的獲取所需的時間不確定,這就導致必須使用異步機制,代表主要有2個:
基于 AMD 的RequireJS
基于 CMD 的SeaJS
它們分別在瀏覽器實現了define、require及module的核心功能,雖然兩者的目標是一致的,但是實現的方式或者說是思路,還是有些區別的,AMD偏向于依賴前置,CMD偏向于用到時才運行的思路,從而導致了依賴項的加載和運行時間點會不同,關于這2者的比較,網上有很多了,這里推薦幾篇僅供參考:
《SeaJS 和 RequireJS 的異同》
《再談 SeaJS 與 RequireJS 的差異》
本人就先接觸了SeaJS后轉到RequireJS,雖然感覺AMD的模式寫確實沒有CMD這么符合一慣的語義邏輯,但是寫了幾個模塊以后就習慣了,而且社區資源比較豐富的AMD陣營更加符合當時的項目需求(扯多了),下面分別寫個例子做下直觀的對比:
// CMD define(function (require) { var a = require("./a"); // <- 運行到此處才開始加載并運行模塊a var b = require("./b"); // <- 運行到此處才開始加載并運行模塊b // more code .. })
// AMD define( ["./a", "./b"], // <- 前置聲明,也就是在主體運行前就已經加載并運行了模塊a和模塊b function (a, b) { // more code .. } )
通過例子,你可以看到除了語法上面的區別,這2者主要的差異還是在于:
何時加載和運行依賴項?
這也是CommonJS社區中質疑AMD最主要原因之一,不少人認為它破壞了規范,反觀CMD模式,簡單的去除define的外包裝,這就是標準的CommonJS實現,所以說CMD是最貼近CommonJS的異步模塊化方案,不過孰優孰劣,這里就不扯了,需求決定一切。
此外同一時期還出現了一個UMD的方案,其實它就是AMD與CommonJS的集合體,通過IIFE的前置條件判斷,使一個模塊既可以在瀏覽器運行,也可以在Node.JS中運行,舉個例子:
// UMD (function(define) { define(function () { var helloInLang = { en: "Hello world!", es: "?Hola mundo!", ru: "Привет мир!" }; return { sayHello: function (lang) { return helloInLang[lang]; } }; }); }( typeof module === "object" && module.exports && typeof define !== "function" ? function (factory) { module.exports = factory(); } : define ));
個人覺得最少用到的就是這個UMD模式了。
2015年6月,ECMAScript2015也就是ES6發布了,JavaScript終于在語言標準的層面上,實現了模塊功能,使得在編譯時就能確定模塊的依賴關系,以及其輸入和輸出的變量,不像 CommonJS、AMD之類的需要在運行時才能確定(例如FIS這樣的工具只能預處理依賴關系,本質上還是運行時解析),成為瀏覽器和服務器通用的模塊解決方案。
// lib/greeting.js const helloInLang = { en: "Hello world!", es: "?Hola mundo!", ru: "Привет мир!" }; export const getHello = (lang) => ( helloInLang[lang]; ); export const sayHello = (lang) => { console.log(getHello(lang)); }; // hello.js import { sayHello } from "./lib/greeting"; sayHello("ru");
與CommonJS用require()方法加載模塊不同,在ES6中,import命令可以具體指定加載模塊中用export命令暴露的接口(不指定具體的接口,默認加載export default),沒有指定的是不會加載的,因此會在編譯時就完成模塊的加載,這種加載方式稱為編譯時加載或者靜態加載。
而CommonJS的require()方法是在運行時才加載的:
// lib/greeting.js const helloInLang = { en: "Hello world!", es: "?Hola mundo!", ru: "Привет мир!" }; const getHello = function (lang) { return helloInLang[lang]; }; exports.getHello = getHello; exports.sayHello = function (lang) { console.log(getHello(lang)) }; // hello.js const sayHello = require("./lib/greeting").sayHello; sayHello("ru");
可以看出,CommonJS中是將整個模塊作為一個對象引入,然后再獲取這個對象上的某個屬性。
因此ES6的編譯時加載,在效率上面會提高不少,此外,還會帶來一些其它的好處,比如引入宏(macro)和類型檢驗(type system)這些只能靠靜態分析實現的功能。
可惜的是,目前瀏覽器和Node.js的支持程度都并不理想,截止發稿,也就只有 Chrome61+ 與 Safari10.1+ 才做到了部分支持。
不過可以通過Babel這類工具配合相關的plugin(可以參考《Babel筆記》),轉換為ES5的語法,這樣就可以在Node.js運行起來了,如果想在瀏覽器上運行,可以添加Babel配置,為模塊文件添上AMD的define函數作為外層,再并配合RequireJS之類的加載器即可。
更多關于ES6 Modules的資料,可以看一下《ECMAScript 6 入門 - Module 的語法》。
參考精讀 js 模塊化發展
History of JavaScript
JavaScript 模塊化七日談
JavaScript Module Pattern: In-Depth
前端模塊化開發那點歷史
JavaScript Modules: A Beginner’s Guide
本文先發布于我的個人博客《JavaScript模塊化開發的演進歷程》,后續如有更新,可以查看原文。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/88457.html
摘要:服務提供方對外發布服務,服務需求方調用服務提供方所發布的服務。應用服務器通過統一數據訪問模塊訪問各種數據,減輕應用程序管理諸多數據源的麻煩。 原文地址:https://blog.coding.net/blog/General-architecture-for-Java-applications 當我們架設一個系統的時候通常需要考慮到如何與其他系統交互,所以我們首先需要知道各種系統之間是...
摘要:月號,杭州和聯合主辦的第八期技術分享會,在公司如期舉行。張偉林,宋小菜資深前端開發工程師,年,霹靂迷,已手殘的紙牌魔術師,喜歡神奇的東西,技術棧從上向下不斷橫向縱向貫穿,目前在尋找前后端大一統思想的路上越走越偏。 showImg(https://segmentfault.com/img/bVbkWN4?w=3000&h=1686); 12 月 9 號,杭州 NodeParty 和 Ro...
摘要:月日,杭州站圓滿收場。第二位嘉賓阿里巴巴移動安全專家何星宇,業界知名白帽子,本次的分享主題移動開發者所必須關注的安全那些事。杭州站分享已經結束,非常感謝大家的參與。 showImg(https://segmentfault.com/img/bVqWqG); 11 月 14 日,SegmentFault D-Day 杭州站圓滿收場。雖然這次『云』議題比較高深,但絲毫沒有影響到愛挑戰的小伙...
摘要:阿里巴巴的共享服務理念以及企業級互聯網架構建設的思路,給這些企業帶來了不少新的思路,這也是我最終決定寫這本書的最主要原因。盡在雙阿里巴巴技術演進與超越是迄今唯一由阿里巴巴集團官方出品全面闡述雙八年以來在技術和商業上演進和創新歷程的書籍。 showImg(https://segmentfault.com/img/remote/1460000015386860); 1、大型網站技術架構:核...
閱讀 996·2023-04-25 14:45
閱讀 2772·2021-09-30 09:59
閱讀 3114·2021-09-22 15:48
閱讀 2425·2019-08-30 15:55
閱讀 3467·2019-08-30 15:44
閱讀 535·2019-08-29 14:07
閱讀 3412·2019-08-26 13:45
閱讀 536·2019-08-26 11:31