javaScript 依賴管理

概述

javaScript -- 目錄最火熱的語言,處處發着光芒, html5, hybrid apps, node.js, full-stack 等等。javaScript 從一個僅僅在瀏覽器上面的一個玩具語言,一轉眼演變成無所不能神通常的存在。可是,因爲天生存在着一點戲劇性(javaScript 據傳說是在飛機上幾天時間設計出來的),模塊系統做爲一門語言最基本的屬性倒是javaScript所缺的。
讓咱們回到過去,經過 <script> 標籤來編寫管理 js 腳本的年代也歷歷在目,翻看如今的許多項目,仍是能找到這樣子的痕跡,可是隨着項目規模的不斷增加,js文件愈來愈多,需求的不斷變動,讓維護的程序員們愈來愈力不從心,怎麼破?javascript

CommonJS

2009 ~ 2010 年間,CommonJS 社區大牛雲集,稍微瞭解點歷史的同窗都清楚,在同時間出現了 nodejs,一會兒讓 javaScript 搖身一變,有了新的用武之地,同時在nodejs推進下的 CommonJS 模塊系統也是逐漸深刻人心。
1:經過 require 就能夠引入一個 module,一個module經過 exports 來導出對外暴露的屬性接口,在一個module裏面沒有經過 exports 暴露出來的變量都是相對於module私有的
2:module 的查找也有必定的策略,經過統一的 package.json 來進行 module 的依賴關係配置,require一個module只須要require package.json裏面定義的name便可css

同時,nodejs也定義了一些系統內置的module方便進行開發,好比簡單的http serverhtml

jsvar 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帶領下,風聲水起,聲明大噪,CommonJS 社區大牛們也就逐漸思考可否把在nodejs的這一套推向瀏覽器?
理想很豐滿,可是現實倒是不盡如人意的
一個最大的問題就是在瀏覽器加載腳本天生不支持同步的加載,沒法經過文件I/O同步的require加載一個js腳本
So what ? CommonJS 中逐漸分裂出了 AMD,這個在瀏覽器環境有很好支持的module規範,其中最有表明性的實現則是 requirejs前端

AMD

正如 AMD 介紹的那樣:html5

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.java

翻譯過來就是說:異步模塊規範 API 定義了一種模塊機制,這種機制下,模塊和它的依賴能夠異步的加載。這個很是適合於瀏覽器環境,由於同步的加載模塊會對性能,可用性,debug調試,跨域訪問產生問題。node

確實,在瀏覽器環境下,AMD有着本身獨特的優點:
因爲源碼和瀏覽器加載的一致,所見即所得,代碼編寫和debug很是方便。尤爲是在多頁面的web項目下,不一樣頁面的腳本js都是根據依賴關係異步按需加載的,不用手動處理每一個頁面加載js腳本的狀況。webpack

可是,AMD 有一個不得不認可的做爲一個module system的不足之處:
請問在 AMD(requireJS)裏面怎麼使用一個第三方庫的?git

通常都會經歷這麼幾個步驟:程序員

  • 使用的第三方庫不想成爲 global 的,只有引用的地方纔可見
  • 須要的庫支不支持 AMD ?
  • 不支持 AMD,我須要 fork 提個 patch 嗎?
  • 支持AMD,個人項目根路徑在哪兒?庫在哪兒?
  • 不想要使用庫的所有,要不要配置個 shim?
  • 需不須要配置個 alias ?

一個庫就須要問這麼些個問題,並且都是人工手動的操做
最最關鍵的問題是你辛辛苦苦搞定的配置項都是相對於你當前項目的
當你想用在其餘項目或者是單元測試,那麼OK,你還得修改一下
由於,你相對的是當前項目的根路徑,一旦根路徑發生改變,一切都發生了變化

requireJS 使用以前必須配置,同時該配置很難重用

相比較於 CommonJS 裏面若是要使用一個第三方庫的話,僅僅只須要在 package.json 裏面配置一下 庫名和版本號,而後npm install一下以後就能夠直接 require 使用的方式,AMD 的處理簡直弱爆了 !!!

對於 AMD 的這個不足之處,又有社區大神提出了能夠在 browser 運行的 CommonJS 的方式,而且經過模塊定義配置文件,能夠很好的進行模塊複用
比較知名的就有 substack 的 browserify, tj 曾主導的 component,還有後來的 duowebpack,時代就轉眼進入了 browser 上的 CommonJS

CommonJS in browser

因爲 CommonJS 的 require 是同步的,在 require 處須要阻塞,這個在瀏覽器上並無很好的支持(瀏覽器只能異步加載腳本,並無同步的文件I/O),CommonJS 要在 browser 上直接使用則必須有一個 build 的過程,在這個 build 的過程裏進行依賴關係的解析與作好映射。這裏有一個典型的實現就是 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進行module管理),browserify 會遞歸的解析依賴關係,並把這些依賴的文件所有build成一個bundle文件,在browser端使用則直接用 <script> tag 引入這個 bundle 文件便可

browserify 有幾個特性:

  • 編寫和 nodejs 同樣的代碼
  • 在瀏覽器直接使用 npm 上的 module

爲了能讓browser直接使用nodejs上的module,browserify 內置了一些 nodejs module 的 browser shim 版本
好比:assert,buffer,crypto,http,os,path等等,具體見browserify builtins

這樣子,browserify就解決了:

  • CommonJS在瀏覽器
  • 先後端代碼複用
  • 前端第三方庫使用

component

component 經過 component.json 來進行依賴描述,它的庫管理是基於 github repo的形式,因爲進行了顯示的配置依賴,它並不須要對源碼進行 require 關係解析,可是時刻須要編寫 component.json 也使得開發者很是的痛苦,開發者更但願 code over configuration 的形式

duo

因此有了 duo,duo 官網上介紹的是:

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 的庫

    jsvar uid = require('matthewmueller/uid');
    var fmt = require('yields/fmt');
    
    var msg = fmt('Your unique ID is %s!', uid());
    window.alert(msg);
  • 不需用配置文件進行描述,直接內嵌在代碼裏面

  • 支持源碼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

小結

這些 browser 上的 CommonJS 解決方案都有一個共同的問題,就是沒法避免的須要一個 build 過程,這個過程雖然能夠經過 watch task 來進行自動化,可是仍是edit和debug仍是很是不方便的

試想着,你在進行debug,你設置了一個debugger,而後單步調試,調試調試着跳到了另一個文件中,而後因爲是一個bundle大文件,你在瀏覽器開發者工具看到的永遠都是同一個文件,而後你發現了問題所在,回頭去改源碼,還得先找到當前所在行與源碼的對應關係!固然這個能夠經過 source map 技術來進行解決,可是相比較 AMD 那種所見即所得的開發模式仍是有必定差距

同時,須要build的過程也給多頁面應用開發帶來了不少麻煩,每一個頁面都要配置 watch task,都要配置 source map 之類的,並且build過程若是一旦出現了build error,開發者還要去看看命令行裏面的日誌,除非使用 beefy 這種能夠把命令行裏面的日誌輸出到瀏覽器console,不然不知道狀況的開發者就會一臉迷茫

CommonJS vs AMD

這永遠是一個話題,由於誰也沒法很好的取代誰,尤爲在瀏覽器環境裏面,二者都有本身的優勢和缺點

CommonJS

  • 優勢:簡潔,更符合一個module system,同時 module 庫的管理也很是方便
  • 缺點:瀏覽器環境必須build才能使用,給開發過程帶來不便

AMD

  • 優勢:天生異步,很好的與瀏覽器環境進行結合,開發過程所見即所得
  • 缺點:不怎麼簡潔的module使用方式,第三方庫的使用時的重複繁瑣配置

dependency injection

前面提到的 javaScript 依賴管理的方式,其實都是實現了同一種設計模式,service locator 或者說是 dependency lookup:
經過顯示的調用 require(id) 來向 service locator 提供方請求依賴的 module
id 能夠是路徑,url,特殊含義的字符串(duo 中的github repo)等等

相反,dependency injection 則並無顯示的調用,而僅僅經過一種與 container 的約定描述來表達須要某個依賴,而後由 container 自動完成依賴的注入,這樣,實際上是完成了 IoC(Inversion of control 控制反轉)

service locator 和 dependency injection 並無誰必定優於誰一說,要看具體使用場景,尤爲是 javaScript 這種天生動態且是first-class的語言裏, 能夠簡單的對比下:

  • service locator 很是直接,須要某個依賴,則直接經過 locator 提供的 api (好比 require)調用向 locator 獲取便可,不過這也帶來了必須與 locator 進行耦合的問題,好比CommonJS的require,AMD的define

相反,dependency injection 因爲並無顯示的調用container某個api,而是經過與container之間的某個約定來進行描述依賴,container再自動完成注入,相比較 service locator 則會隱晦一點

  • service locator 因爲能夠本身控制,使用起來更加的靈活,所依賴的也能夠多樣,不只僅限於javaScript(還能夠是json等,具體要看service locator實現)
    dependency injection 則沒有那麼的靈活,通常的container實現都是基於某個特定的module,好比最簡單的class,注入的通常都是該module所約定好的,好比class的instance

  • service locator 中的id實現通常基於文件系統或者其它標識,能夠是相對路徑或者絕對路徑或者url,這個其實就帶來了必定的限制性,依賴方必需要在該id描述下一直有效,若是依賴方好比改了個名字或者移動了目錄結構,那麼全部被依賴方則必須作出改動
    dependency injection 中雖然也有id,可是該id是module的全局自定義惟一id,這個id與文件系統則並無直接的關係,不管外部環境如何變,因爲module的id是硬編碼的,container都能很好的處理

  • service locator 因爲靈活性,寫出來的代碼多樣化,module之間會存在必定耦合,固然也能夠實現鬆耦合的,可是須要必定的技巧或者規範
    dependency injection 因爲天生是基於id描述的形式,控制交由container來完成,鬆散耦合,當應用規模不斷增加的時候還能持續帶來不錯的維護性

  • service locator 目前在javaScript界有大量實現,並且有大量的庫能夠直接使用,好比基於CommonJS的npm,所以在使用庫方面 service locator 有着自然的優點
    dependency injection 則實現很少,並且因爲是與container之間的約定,不一樣container之間的實現不一樣,也沒法共通

其實,比較來比較去,不如二者結合起來使用,都有各自的優缺點:
dependency injection 來編寫鬆散耦合的應用層邏輯,service locator來使用第三方庫

dependency injection container

一個優秀的dependency injection container須要有下面這些特性:

  • 無侵入式,與container之間的描述不是顯示經過container api調用而是經過配置
  • code over configuration,配置最好是內嵌於code的,自描述的
  • 實現異步腳本加載,因爲已經描述了依賴關係,那麼就無需蛋疼的再經過其它途徑來處理依賴的腳本加載
  • 代碼能夠先後端直接複用,能夠直接引用,而不是說經過複製/粘貼而來的複用
  • 在container之上實現其它,好比AOP,一致性配置,代碼hot reload

這其實就是 bearcat 所作的事兒
bearcat 並非實現了 service locator 模式的module system,它實現了 dependency injection container,所以bearcat能夠很好的與上面提到的各類CommonJS或者AMD結合使用,結合本身的優點來編寫彈性、持續可維護的系統(應用)

bearcat

bearcat 的一個理念能夠用下面一句話來描述:
Magic, self-described javaScript objects build up elastic, maintainable front-backend javaScript applications
bearcat 所倡導的就是使用簡單、自描述的javaScript對象來構建彈性、可維護的先後端javaScript應用

固然可能有人會說,javaScript裏面不只僅是對象,還能夠函數式、元編程什麼的,其實也是要看應用場景的,bearcat更適合的場景是一個多人協做的、須要持續維護的系統(應用),若是是快速開發的腳本、工具、庫,那麼則該怎麼簡單、怎麼方便,就怎麼來

bearcat 快速例子

假若有一個應用,須要有一輛car,同時car必需要有engine才能發動,那麼car就依賴了engine,在bearcat的 dependency injection container 下,僅僅以下編寫代碼便可:

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 : {});
  • 經過 this.$id 來定義該module在bearcat container裏的全局惟一id
  • 經過 $Id 屬性來描述依賴,在car裏就描述了須要id爲 engine的一個依賴
  • 經過 bearcat.module(Function) 來把module註冊到bearcat container中去
    typeof module !== 'undefined' ? module : {}

這一段是爲了與 CommonJS(nodejs) 下進行兼容,在nodejs裏因爲有同步require,則無需向在瀏覽器環境下進行異步加載

啓動bearcat容器,總體跑起來

瀏覽器環境

<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']) 表面當前頁面須要使用 car,bearcat而後就會加載car.js,而後解析car裏面的依賴,知道須要engine,而後加載engine.js腳本,加載完以後,再把engine實例化注入到car中,最後調用bearcat.start的回調完成整個容器的啓動

nodejs 環境

jsvar 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 環境下啓動,則不需用bearcat.use了,直接把 context.json的路徑傳遞給bearcat便可,bearcat會掃描context.json裏面配置着的掃描路徑,該路徑下的全部js文件都會被掃描,合理的module都會註冊到bearcat中,而後實例化,注入

完整源碼 10-secondes-example

bearcat + browserify

bearcat 的簡潔,異步加載的module,無需打包,所見即所得,在編寫應用層代碼上有很是大的便利
browserify 能夠直接複用 npm 上的 module,使用第三方庫很是的方便

bearcat + browserify 會是一個不錯的組合

一個例子,基於 bearcat + browserify 的 markdwon-editor

bearcat 與 browserify 之間經過一個requireUtil(好比)的module來進行鏈接

在這個 requireUtil 可使用 browserify 的 require,用這個 require 來引入第三方庫,好比marked庫

requireUtil.js

jsvar 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 : {});

而後在你的業務層代碼上,注入這個 requireUtil來使用 browserify 引入的第三方庫

markDownController.js

jsvar 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 : {});

這樣子一來,編寫業務層代碼因爲是bearcat管理的,javaScript依賴異步加載,代碼編寫和debug就和AMD同樣,所見即所得,設置斷點什麼的,不再用擔憂找不到源文件(或者須要source map)
使用 browserify 僅僅是爲了用它來引入第三方庫,且也僅僅當引入一個新的第三方庫的時候纔會執行一下 browserify 的 build

bearcat 和 browserify 的優點就都發揮了出來,提升了開發的效率以及可維護性

bearcat-markdown-editor 官網例子地址 markdown-editor

總結

不管是CommonJS、AMD或者是dependency injection,單獨使用某一個,javaScript依賴管理都不是完美的
應人而異,各取所需

參考

相關文章
相關標籤/搜索