從CommonJS vs AMD中解讀JavaScript依賴管理
概述
javaScript -- 目錄最火熱的語言,到處發(fā)著光芒, html5, hybrid apps, node.js, full-stack 等等。javaScript 從一個僅僅在瀏覽器上面的一個玩具語言,一轉(zhuǎn)眼演變成無所不能神一般的存在。但是,由于天生存在著一點戲劇性(javaScript 據(jù)傳說是在飛機(jī)上幾天時間設(shè)計出來的),模塊系統(tǒng)作為一門語言最基本的屬性卻是javaScript所缺的。 讓我們回到過去,通過 <script> 標(biāo)簽來編寫管理 js 腳本的年代也歷歷在目,翻看現(xiàn)在的許多項目,還是能找到這樣子的痕跡,但是隨著項目規(guī)模的不斷增長,js文件越來越多,需求的不斷變更,讓維護(hù)的程序員們越來越力不從心,怎么破?
CommonJS
2009 ~ 2010 年間,CommonJS 社區(qū)大牛云集,稍微了解點歷史的同學(xué)都清楚,在同時間出現(xiàn)了 nodejs,一下子讓 javaScript 搖身一變,有了新的用武之地,同時在nodejs推動下的 CommonJS 模塊系統(tǒng)也是逐漸深入人心。 1:通過 require 就可以引入一個 module,一個module通過 exports 來導(dǎo)出對外暴露的屬性接口,在一個module里面沒有通過 exports 暴露出來的變量都是相對于module私有的
2:module 的查找也有一定的策略,通過統(tǒng)一的 package.json 來進(jìn)行 module 的依賴關(guān)系配置,require一個module只需要require package.json里面定義的name即可
同時,nodejs也定義了一些系統(tǒng)內(nèi)置的module方便進(jìn)行開發(fā),比如簡單的http server
var http = require('http'); http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Hello World\n'); }).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
正如 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 定義了一種模塊機(jī)制,這種機(jī)制下,模塊和它的依賴可以異步的加載。這個非常適合于瀏覽器環(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 的方式,并且通過模塊定義配置文件,可以很好的進(jìn)行模塊復(fù)用
比較知名的就有 substack 的 browserify, tj 曾主導(dǎo)的 component,還有后來的 duo,webpack,時代就轉(zhuǎn)眼進(jìn)入了 browser 上的 CommonJS
CommonJS in browser
由于 CommonJS 的 require 是同步的,在 require 處需要阻塞,這個在瀏覽器上并沒有很好的支持(瀏覽器只能異步加載腳本,并沒有同步的文件I/O),CommonJS 要在 browser 上直接使用則必須有一個 build 的過程,在這個 build 的過程里進(jìn)行依賴關(guān)系的解析與做好映射。這里有一個典型的實現(xiàn)就是 substack 的 browserify。
browserify
browserify 在 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 <script> tag.
在 browserify 里可以編寫 nodejs 一樣的代碼(即CommonJS以及使用package.json進(jìn)行module管理),browserify 會遞歸的解析依賴關(guān)系,并把這些依賴的文件全部build成一個bundle文件,在browser端使用則直接用 <script> 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ù)用 * 前端第三方庫使用
component
component 通過 component.json 來進(jìn)行依賴描述,它的庫管理是基于 github repo的形式,由于進(jìn)行了顯示的配置依賴,它并不需要對源碼進(jìn)行 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);
- 不需用配置文件進(jìn)行描述,直接內(nèi)嵌在代碼里面
- 支持源碼transform,比如支持 Coffeescript 或者 Sass
webpack
webpack 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 來進(jìn)行自動化,但是還是edit和debug還是非常不方便的
試想著,你在進(jìn)行debug,你設(shè)置了一個debugger,然后單步調(diào)試,調(diào)試調(diào)試著跳到了另外一個文件中,然后由于是一個bundle大文件,你在瀏覽器開發(fā)者工具看到的永遠(yuǎn)都是同一個文件,然后你發(fā)現(xiàn)了問題所在,回頭去改源碼,還得先找到當(dāng)前所在行與源碼的對應(yīng)關(guān)系!當(dāng)然這個可以通過 source map 技術(shù)來進(jìn)行解決,但是相比較 AMD 那種所見即所得的開發(fā)模式還是有一定差距
同時,需要build的過程也給多頁面應(yīng)用開發(fā)帶來了很多麻煩,每個頁面都要配置 watch task,都要配置 source map 之類的,而且build過程如果一旦出現(xiàn)了build error,開發(fā)者還要去看看命令行里面的日志,除非使用 beefy 這種可以把命令行里面的日志輸出到瀏覽器console,否則不知道情況的開發(fā)者就會一臉迷茫
CommonJS vs AMD
這永遠(yuǎn)是一個話題,因為誰也無法很好的取代誰,尤其在瀏覽器環(huán)境里面,兩者都有自己的優(yōu)點和缺點
CommonJS * 優(yōu)點:簡潔,更符合一個module system,同時 module 庫的管理也非常方便
* 缺點:瀏覽器環(huán)境必須build才能使用,給開發(fā)過程帶來不便
AMD * 優(yōu)點:天生異步,很好的與瀏覽器環(huán)境進(jì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 的約定描述來表達(dá)需要某個依賴,然后由 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 進(jìn)行耦合的問題,比如CommonJS的require,AMD的define
相反,dependency injection 由于并沒有顯示的調(diào)用container某個api,而是通過與container之間的某個約定來進(jìn)行描述依賴,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ù)帶來不錯的維護(hù)性
- service locator 目前在javaScript界有大量實現(xiàn),而且有大量的庫可以直接使用,比如基于CommonJS的npm,因此在使用庫方面 service locator 有著天然的優(yōu)勢
dependency injection 則實現(xiàn)不多,而且由于是與container之間的約定,不同container之間的實現(xiàn)不同,也無法共通
其實,比較來比較去,不如兩者結(jié)合起來使用,都有各自的優(yōu)缺點:
dependency injection 來編寫松散耦合的應(yīng)用層邏輯,service locator來使用第三方庫
dependency injection container
一個優(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ù)可維護(hù)的系統(tǒng)(應(yīng)用)
#p#
bearcat
bearcat 的一個理念可以用下面一句話來描述:
Magic, self-described javaScript objects build up elastic, maintainable front-backend javaScript applications
bearcat 所倡導(dǎo)的就是使用簡單、自描述的javaScript對象來構(gòu)建彈性、可維護(hù)的前后端javaScript應(yīng)用
當(dāng)然可能有人會說,javaScript里面不僅僅是對象,還可以函數(shù)式、元編程什么的,其實也是要看應(yīng)用場景的,bearcat更適合的場景是一個多人協(xié)作的、需要持續(xù)維護(hù)的系統(tǒng)(應(yīng)用),如果是快速開發(fā)的腳本、工具、庫,那么則該怎么簡單、怎么方便,就怎么來
bearcat 快速例子
假如有一個應(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) 下進(jìn)行兼容,在nodejs里由于有同步require,則無需向在瀏覽器環(huán)境下進(jìn)行異步加載
啟動bearcat容器,整體跑起來
瀏覽器環(huán)境
<script src="./lib/bearcat.js"></script> <script src="./bearcat-bootstrap.js"></script> <script type="text/javascript"> bearcat.createApp(); // create app to init bearcat.use(['car']); // javaScript objects needed to be used bearcat.start(function() { // when this callback invoked, everything is ready var car = bearcat.getBean('car'); car.run(); });
bearcat.use(['car']) 表面當(dāng)前頁面需要使用 car,bearcat然后就會加載car.js,然后解析car里面的依賴,知道需要engine,然后加載engine.js腳本,加載完之后,再把engine實例化注入到car中,最后調(diào)用bearcat.start的回調(diào)完成整個容器的啟動
nodejs 環(huán)境
var bearcat = require('bearcat'); var contextPath = require.resolve('./context.json'); global.bearcat = bearcat; // make bearcat global, for `bearcat.module()` bearcat.createApp([contextPath]); bearcat.start(function() { var car = bearcat.getBean('car'); // get car car.run(); // call the method });
nodejs 環(huán)境下啟動,則不需用bearcat.use了,直接把 context.json的路徑傳遞給bearcat即可,bearcat會掃描context.json里面配置著的掃描路徑,該路徑下的所有js文件都會被掃描,合理的module都會注冊到bearcat中,然后實例化,注入
完整源碼 10-secondes-example
bearcat + browserify
bearcat 的簡潔,異步加載的module,無需打包,所見即所得,在編寫應(yīng)用層代碼上有非常大的便利
browserify 可以直接復(fù)用 npm 上的 module,使用第三方庫非常的方便
bearcat + browserify 會是一個不錯的組合
一個例子,基于 bearcat + browserify 的 markdwon-editor
bearcat 與 browserify 之間通過一個requireUtil(比如)的module來進(jìn)行連接
在這個 requireUtil 可以使用 browserify 的 require,用這個 require 來引入第三方庫,比如marked庫
requireUtil.js
var RequireUtil = function() { this.$id = "requireUtil"; this.$init = "init"; this.brace = null; this.marked = null; } RequireUtil.prototype.init = function() { this.brace = require('brace'); this.marked = require('marked'); } bearcat.module(RequireUtil, typeof module !== 'undefined' ? module : {});
然后在你的業(yè)務(wù)層代碼上,注入這個 requireUtil來使用 browserify 引入的第三方庫
markDownController.js
var MarkDownController = function() { this.$id = "markDownController"; this.$requireUtil = null; // requireUtil is ready for you to use } MarkDownController.prototype.initBrace = function(md) { var ace = this.$requireUtil.brace; var editor = ace.edit('editor'); editor.getSession().setMode('ace/mode/markdown'); editor.setTheme('ace/theme/monokai'); editor.setValue(md); editor.clearSelection(); return editor; } bearcat.module(MarkDownController, typeof module !== 'undefined' ? module : {});
這樣子一來,編寫業(yè)務(wù)層代碼由于是bearcat管理的,javaScript依賴異步加載,代碼編寫和debug就和AMD一樣,所見即所得,設(shè)置斷點什么的,再也不用擔(dān)心找不到源文件(或者需要source map)
使用 browserify 僅僅是為了用它來引入第三方庫,且也僅僅當(dāng)引入一個新的第三方庫的時候才會執(zhí)行一下 browserify 的 build
bearcat 和 browserify 的優(yōu)勢就都發(fā)揮了出來,提高了開發(fā)的效率以及可維護(hù)性
bearcat-markdown-editor 官網(wǎng)例子地址 markdown-editor
總結(jié)
無論是CommonJS、AMD或者是dependency injection,單獨使用某一個,javaScript依賴管理都不是完美的。
應(yīng)人而異,各取所需
參考
- martin fowlter dependency injection
- asynchronous frontend dependency management without AMD
- browserify with asynchronous script loading
- 原文
原文地址: bearcatjs.org
原文鏈接:http://ourjs.com/detail/54a7521a5695544119000003