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依赖管理都不是完美的
应人而异,各取所需

参考