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