摘要:但是,由于天生存在著一點戲劇性據(jù)傳說是在飛機上幾天時間設(shè)計出來的,模塊系統(tǒng)作為一門語言最基本的屬性卻是所缺的。尤其是在多頁面的項目下,不同頁面的腳本都是根據(jù)依賴關(guān)系異步按需加載的,不用手動處理每個頁面加載腳本的情況。
轉(zhuǎn)原文
概述javaScript — 目錄最火熱的語言,到處發(fā)著光芒, html5, hybrid apps, node.js, full-stack 等等。javaScript 從一個僅僅在瀏覽器上面的一個玩具語言,一轉(zhuǎn)眼演變成無所不能神一般的存在。但是,由于天生存在著一點戲劇性(javaScript 據(jù)傳說是在飛機上幾天時間設(shè)計出來的),模塊系統(tǒng)作為一門語言最基本的屬性卻是javaScript所缺的。
讓我們回到過去,通過 標(biāo)簽來編寫管理 js 腳本的年代也歷歷在目,翻看現(xiàn)在的許多項目,還是能找到這樣子的痕跡,但是隨著項目規(guī)模的不斷增長,js文件越來越多,需求的不斷變更,讓維護的程序員們越來越力不從心,怎么破?
CommonJS2009 ~ 2010 年間,CommonJS 社區(qū)大牛云集,稍微了解點歷史的同學(xué)都清楚,在同時間出現(xiàn)了 nodejs,一下子讓 javaScript 搖身一變,有了新的用武之地,同時在nodejs推動下的 CommonJS 模塊系統(tǒng)也是逐漸深入人心。
通過 require 就可以引入一個 module,一個module通過 exports 來導(dǎo)出對外暴露的屬性接口,在一個module里面沒有通過 exports 暴露出來的變量都是相對于module私有的
module 的查找也有一定的策略,通過統(tǒng)一的 package.json 來進行 module 的依賴關(guān)系配置,require一個module只需要require package.json里面定義的name即可
同時,nodejs也定義了一些系統(tǒng)內(nèi)置的module方便進行開發(fā),比如簡單的http server
var http = require("http"); http.createServer(function (req, res) { res.writeHead(200, {"Content-Type": "text/plain"}); res.end("Hello World "); }).listen(1337, "127.0.0.1"); console.log("Server running at http://127.0.0.1:1337/");
CommonJS 在nodejs帶領(lǐng)下,風(fēng)聲水起,聲明大噪,CommonJS 社區(qū)大牛們也就逐漸思考能否把在nodejs的這一套推向瀏覽器?
理想很豐滿,但是現(xiàn)實卻是不盡如人意的
一個最大的問題就是在瀏覽器加載腳本天生不支持同步的加載,無法通過文件I/O同步的require加載一個js腳本
So what ? CommonJS 中逐漸分裂出了 AMD,這個在瀏覽器環(huán)境有很好支持的module規(guī)范,其中最有代表性的實現(xiàn)則是 requirejs
正如 AMD 介紹的那樣:
The Asynchronous Module Definition (AMD) API specifies a mechanism for defining modules such that the module and its dependencies can be asynchfanronously loaded. This is particularly well suited for the browser environment where synchronous loading of modules incurs performance, usability, debugging, and cross-domain access problems.
翻譯過來就是說:異步模塊規(guī)范 API 定義了一種模塊機制,這種機制下,模塊和它的依賴可以異步的加載。這個非常適合于瀏覽器環(huán)境,因為同步的加載模塊會對性能,可用性,debug調(diào)試,跨域訪問產(chǎn)生問題。
確實,在瀏覽器環(huán)境下,AMD有著自己獨特的優(yōu)勢:
由于源碼和瀏覽器加載的一致,所見即所得,代碼編寫和debug非常方便。尤其是在多頁面的web項目下,不同頁面的腳本js都是根據(jù)依賴關(guān)系異步按需加載的,不用手動處理每個頁面加載js腳本的情況。
但是,AMD 有一個不得不承認(rèn)的作為一個module system的不足之處
請問?在 AMD(requireJS)里面怎么使用一個第三方庫的?
一般都會經(jīng)歷這么幾個步驟:
使用的第三方庫不想成為 global 的,只有引用的地方才可見
需要的庫支不支持 AMD ?
不支持 AMD,我需要 fork 提個 patch 嗎?
支持AMD,我的項目根路徑在哪兒?庫在哪兒?
不想要使用庫的全部,要不要配置個 shim?
需不需要配置個 alias ?
一個庫就需要問這么些個問題,而且都是人工手動的操作
最最關(guān)鍵的問題是你辛辛苦苦搞定的配置項都是相對于你當(dāng)前項目的
當(dāng)你想用在其他項目或者是單元測試,那么OK,你還得修改一下
因為,你相對的是當(dāng)前項目的根路徑,一旦根路徑發(fā)生改變,一切都發(fā)生了變化
requireJS 使用之前必須配置,同時該配置很難重用
相比較于 CommonJS 里面如果要使用一個第三方庫的話,僅僅只需要在 package.json 里面配置一下 庫名和版本號,然后npm install一下之后就可以直接 require 使用的方式,AMD 的處理簡直弱爆了 !!!
對于 AMD 的這個不足之處,又有社區(qū)大神提出了可以在 browser 運行的 CommonJS 的方式,并且通過模塊定義配置文件,可以很好的進行模塊復(fù)用
比較知名的就有 substack 的 browserify, tj 曾主導(dǎo)的 component,還有后來的 duo,webpack,時代就轉(zhuǎn)眼進入了 browser 上的 CommonJS
由于 CommonJS 的 require 是同步的,在 require 處需要阻塞,這個在瀏覽器上并沒有很好的支持(瀏覽器只能異步加載腳本,并沒有同步的文件I/O),CommonJS 要在 browser 上直接使用則必須有一個 build 的過程,在這個 build 的過程里進行依賴關(guān)系的解析與做好映射。這里有一個典型的實現(xiàn)就是 substack 的 browserify。
browserifybrowserify 在 github 上的 README.md 解釋是:
require("modules") in the browser
Use a node-style require() to organize your browser code
and load modules installed by npm.
browserify will recursively analyze all the require() calls in your app in
order to build a bundle you can serve up to the browser in a single
tag.
在 browserify 里可以編寫 nodejs 一樣的代碼(即CommonJS以及使用package.json進行module管理),browserify 會遞歸的解析依賴關(guān)系,并把這些依賴的文件全部build成一個bundle文件,在browser端使用則直接用 tag 引入這個 bundle 文件即可
browserify 有幾個特性:
編寫和 nodejs 一樣的代碼
在瀏覽器直接使用 npm 上的 module
為了能讓browser直接使用nodejs上的module,browserify 內(nèi)置了一些 nodejs module 的 browser shim 版本
比如:assert,buffer,crypto,http,os,path等等,具體見browserify builtins
這樣子,browserify就解決了:
CommonJS在瀏覽器
前后端代碼復(fù)用
前端第三方庫使用
componentcomponent 通過 component.json 來進行依賴描述,它的庫管理是基于 github repo的形式,由于進行了顯示的配置依賴,它并不需要對源碼進行 require 關(guān)系解析,但是時刻需要編寫 component.json 也使得開發(fā)者非常的痛苦,開發(fā)者更希望 code over configuration 的形式
duo所以有了 duo,duo 官網(wǎng)上介紹的是:
Duo is a next-generation package manager that blends the best ideas from Component, Browserify and Go to make organizing and writing front-end code quick and painless.
Duo 有幾個特點:
直接使用 require 使用 github 上某個 repo 的庫
var uid = require("matthewmueller/uid");
var fmt = require("yields/fmt");
var msg = fmt("Your unique ID is %s!", uid());
window.alert(msg);
不需用配置文件進行描述,直接內(nèi)嵌在代碼里面
支持源碼transform,比如支持 Coffeescript 或者 Sass
webpackwebpack takes modules with dependencies and generates static assets representing those modules.
webpack 是一個 module bundler 即模塊打包工具,它支持 CommonJS,AMD的module形式,同時還支持 code splittling,css 等
最近 browserify 和 webpack 也有一定的比較,可以看看 substack 的文章 browserify for webpack users
小結(jié)這些 browser 上的 CommonJS 解決方案都有一個共同的問題,就是無法避免的需要一個 build 過程,這個過程雖然可以通過 watch task 來進行自動化,但是還是edit和debug還是非常不方便的
試想著,你在進行debug,你設(shè)置了一個debugger,然后單步調(diào)試,調(diào)試調(diào)試著跳到了另外一個文件中,然后由于是一個bundle大文件,你在瀏覽器開發(fā)者工具看到的永遠都是同一個文件,然后你發(fā)現(xiàn)了問題所在,回頭去改源碼,還得先找到當(dāng)前所在行與源碼的對應(yīng)關(guān)系!當(dāng)然這個可以通過 source map 技術(shù)來進行解決,但是相比較 AMD 那種所見即所得的開發(fā)模式還是有一定差距
同時,需要build的過程也給多頁面應(yīng)用開發(fā)帶來了很多麻煩,每個頁面都要配置 watch task,都要配置 source map 之類的,而且build過程如果一旦出現(xiàn)了build error,開發(fā)者還要去看看命令行里面的日志,除非使用 beefy 這種可以把命令行里面的日志輸出到瀏覽器console,否則不知道情況的開發(fā)者就會一臉迷茫
CommonJS vs AMD這永遠是一個話題,因為誰也無法很好的取代誰,尤其在瀏覽器環(huán)境里面,兩者都有自己的優(yōu)點和缺點
CommonJS
優(yōu)點:簡潔,更符合一個module system,同時 module 庫的管理也非常方便* 缺點:瀏覽器環(huán)境必須build才能使用,給開發(fā)過程帶來不便
AMD
優(yōu)點:天生異步,很好的與瀏覽器環(huán)境進行結(jié)合,開發(fā)過程所見即所得
缺點:不怎么簡潔的module使用方式,第三方庫的使用時的重復(fù)繁瑣配置
dependency injection前面提到的 javaScript 依賴管理的方式,其實都是實現(xiàn)了同一種設(shè)計模式,service locator 或者說是 dependency lookup:
通過顯示的調(diào)用 require(id) 來向 service locator 提供方請求依賴的 module
id 可以是路徑,url,特殊含義的字符串(duo 中的github repo)等等
相反,dependency injection 則并沒有顯示的調(diào)用,而僅僅通過一種與 container 的約定描述來表達需要某個依賴,然后由 container 自動完成依賴的注入,這樣,其實是完成了 IoC(Inversion of control 控制反轉(zhuǎn))
service locator 和 dependency injection 并沒有誰一定優(yōu)于誰一說,要看具體使用場景,尤其是 javaScript 這種天生動態(tài)且是first-class的語言里, 可以簡單的對比下:
service locator 非常直接,需要某個依賴,則直接通過 locator 提供的 api (比如 require)調(diào)用向 locator 獲取即可,不過這也帶來了必須與 locator 進行耦合的問題,比如CommonJS的require,AMD的define
相反,dependency injection 由于并沒有顯示的調(diào)用container某個api,而是通過與container之間的某個約定來進行描述依賴,container再自動完成注入,相比較 service locator 則會隱晦一點
service locator 由于可以自己控制,使用起來更加的靈活,所依賴的也可以多樣,不僅僅限于javaScript(還可以是json等,具體要看service locator實現(xiàn))
dependency injection 則沒有那么的靈活,一般的container實現(xiàn)都是基于某個特定的module,比如最簡單的class,注入的一般都是該module所約定好的,比如class的instance
service locator 中的id實現(xiàn)一般基于文件系統(tǒng)或者其它標(biāo)識,可以是相對路徑或者絕對路徑或者url,這個其實就帶來了一定的限制性,依賴方必須要在該id描述下一直有效,如果依賴方比如改了個名字或者移動了目錄結(jié)構(gòu),那么所有被依賴方則必須做出改動
dependency injection 中雖然也有id,但是該id是module的全局自定義唯一id,這個id與文件系統(tǒng)則并沒有直接的關(guān)系,無論外部環(huán)境如何變,由于module的id是硬編碼的,container都能很好的處理
service locator 由于靈活性,寫出來的代碼多樣化,module之間會存在一定耦合,當(dāng)然也可以實現(xiàn)松耦合的,但是需要一定的技巧或者規(guī)范
dependency injection 由于天生是基于id描述的形式,控制交由container來完成,松散耦合,當(dāng)應(yīng)用規(guī)模不斷增長的時候還能持續(xù)帶來不錯的維護性
service locator 目前在javaScript界有大量實現(xiàn),而且有大量的庫可以直接使用,比如基于CommonJS的npm,因此在使用庫方面 service locator 有著天然的優(yōu)勢
dependency injection 則實現(xiàn)不多,而且由于是與container之間的約定,不同container之間的實現(xiàn)不同,也無法共通
其實,比較來比較去,不如兩者結(jié)合起來使用,都有各自的優(yōu)缺點:
dependency injection containerdependency injection 來編寫松散耦合的應(yīng)用層邏輯,service locator來使用第三方庫
一個優(yōu)秀的dependency injection container需要有下面這些特性:
無侵入式,與container之間的描述不是顯示通過container api調(diào)用而是通過配置
code over configuration,配置最好是內(nèi)嵌于code的,自描述的
實現(xiàn)異步腳本加載,由于已經(jīng)描述了依賴關(guān)系,那么就無需蛋疼的再通過其它途徑來處理依賴的腳本加載
代碼可以前后端直接復(fù)用,可以直接引用,而不是說通過復(fù)制/粘貼而來的復(fù)用
在container之上實現(xiàn)其它,比如AOP,一致性配置,代碼hot reload
這其實就是 bearcat 所做的事兒
bearcat 并不是實現(xiàn)了 service locator 模式的module system,它實現(xiàn)了 dependency injection container,因此bearcat可以很好的與上面提到的各種CommonJS或者AMD結(jié)合使用,結(jié)合自己的優(yōu)勢來編寫彈性、持續(xù)可維護的系統(tǒng)(應(yīng)用)
bearcat 的一個理念可以用下面一句話來描述:
Magic, self-described javaScript objects build up elastic, maintainable front-backend javaScript applications
bearcat 所倡導(dǎo)的就是使用簡單、自描述的javaScript對象來構(gòu)建彈性、可維護的前后端javaScript應(yīng)用
當(dāng)然可能有人會說,javaScript里面不僅僅是對象,還可以函數(shù)式、元編程什么的,其實也是要看應(yīng)用場景的,bearcat更適合的場景是一個多人協(xié)作的、需要持續(xù)維護的系統(tǒng)(應(yīng)用),如果是快速開發(fā)的腳本、工具、庫,那么則該怎么簡單、怎么方便,就怎么來
假如有一個應(yīng)用,需要有一輛car,同時car必須要有engine才能發(fā)動,那么car就依賴了engine,在bearcat的 dependency injection container 下,僅僅如下編寫代碼即可:
car.js
var Car = function() { this.$id = "car"; this.$engine = null; } Car.prototype.run = function() { this.$engine.run(); console.log("run car..."); } bearcat.module(Car, typeof module !== "undefined" ? module : {});
engine.js
var Engine = function() { this.$id = "engine"; } Engine.prototype.run = function() { console.log("run engine..."); } bearcat.module(Engine, typeof module !== "undefined" ? module : {});
通過 this.$id 來定義該module在bearcat container里的全局唯一id
通過 $Id 屬性來描述依賴,在car里就描述了需要id為 engine的一個依賴
通過 bearcat.module(Function) 來把module注冊到bearcat container中去
typeof module !== "undefined" ? module : {}
這一段是為了與 CommonJS(nodejs) 下進行兼容,在nodejs里由于有同步require,則無需向在瀏覽器環(huán)境下進行異步加載
啟動bearcat容器,整體跑起來
瀏覽器環(huán)境