Nervenet + Backbone

使用Backbone时,配合Nervenet能取得很好的效果。

介绍Nervenet起源时我提到,Nervenet一名很大程度上来自于Backbone。使用Backbone进行开发期间,我体会到很多方便之处,也发现仍有不少障碍横垣在前,于是总结之前的经验,取Robotlegs之长,做出了这个框架。当然,Nervenet并非专门针对Backbone开发,自然也不会依赖它,不过由于设计初衷和后期实现,结合Nervenet使用Backbone时着实有一些优势。

context.createInstance创建实例

我们知道,依赖注入的对象一般都是类的实例;类实例化时,可能会用到一些尚未注入的属性,所以通常需要在注入后调用实例的postConstruct方法,才能彻底完成构造。Backbone为了实现其独特的架构,将真正的构造函数construct隐藏了起来,开发者操作的initialize函数实际上是初始化函数,从执行顺序来讲与上文中的postConstruct基本一致。同时,Backbone的几大基础类都接受传入初始化对象,即new Backbone.View(options);中的optionsoptions中若包含elmodel等属性,将直接用来填充类自身的属性,甚至从页面当中找到dom结构操作。

若采用普通的依赖注入,则需要在每个类中再实现一个postConstruct方法,增加维护成本和迁移成本。使用Nervenet提供的context.createInstance方法,可以在实例化对象时,扫描参数对象,完成注入,只需要保留initialize函数,非常简单,而且可以直接沿用之前的代码。

var view = context.createIntance(Backbone.View, {
  model: '{{$model}}'
});

从代码可以看出,要完成上述功能,还必须实现一个特性:“注入指定类型的对象”。Javascript是弱类型语言,代码中不包含对象类型,所以之前的类库只能根据“属性名”注入。对于Backbone来说,modelcollection这些都是特有的关键字,要注入的也多是它们,而要注入model,就必须先map一个值到model这个key中——这意味着同一组map只能给单一Backbone.View对象注入依赖,在实际开发难以想象。Nervenet的优势在于可以由“属性值”指定注入的内容,这样不同的View,可以指定不同的model,问题迎刃而解。

// 给不同的view注入不同的model
context.createInstance(Backbone.View, {
  model: '{{$user-model}}'
});

context.createInstance(Backbone.View, {
  model: '{{$job-model}}'
});

infuse.js的解决方案是“子注入器”,通过创建子注入器,构造不同的map,就可以对不同对象注入不同的值了)

mediatorMap管理mediator

了解Backbone的人都知道,虽然名为View,但其实Backbone.View更接近一般意义上的Mediator,而通常意义上的View则由HTML+CSS负责实现。我通常使用Backbone.View开发组件,组件拼接起来就是完整功能的页面嘛。所以我就设计了一个功能来帮助我管理mediator,就是context.mediatorMap,它主要提供三个方法:

.map(selector, MediatorClass[, options]),通常和.check(dom)连用。前者将选择器和类关联,后者则检查某个DOM节点下是否有符合选择器的节点,并为它们自动创建mediator

// 先这么注册一下
context.mediatorMap.map('.checkbox', MyCheckbox, options);

// 然后就可以用了
context.mediatorMap.check(document.getElementById('my-form'));

.createMediator(dom, MediatorClass[, args]),直接为某DOM节点创建mediator

// 直接为DOM创建mediator
context.mediatorMap.createMediator(document.body, MyGUI);

创建mediator的过程基本相同,自动注入依赖,并把目标DOM节点会作为第一个参数,或者第一个参数对象的el属性传给mediator的构造函数——后者当然是为了兼容Backbone。

Backbone基于jQuery,所以操作一个DOM和操作一组DOM对它来说没有什么区别,留意到这点后,我在Nervenet中也作相应照顾,可以通过options设置isSingle=true;表示使用者希望使用一个实例来管理所有符合要求的节点。默认false,会针对每个DOM节点创建独立的mediator

可惜的是,HTML没有提供方便的手段获取DOM节点的变化,所以暂时只好使用手工检查了;另外,querySelectorAll这个方法也存在一定的兼容性问题,所以,目前mediatorMap仍然只是0.1.2版的测试功能,以后可能会有各种改动。

soma.js, infuse.js, Nervenet

分析与Nervenet比较相似的框架:soma.js与其的差异,介绍Nervenet中有哪些比较独到的地方。

上一篇日志中,我介绍了Nervenet的创作思路。虽然JavaScript有着各种各样的先天不足,但是,运气也是实力的一部分,所以广大开发者只有用各种手法去适应它、改良它。应该说大家干得很棒,我也想贡献自己的力量,于是创造了Nervenet,希望解决我在开发中遇到的各种问题。

就在我写完Nervenet初版的时候,偶然看到MVC框架soma.js的介绍,发现跟我的思路很相像(其中用到的IoC类库:infuse.js,也是他们开发的)。于是仔细研究了一番,学到不少东西。今天我准便拿Nervenet和它们分析对比一下。

soma.js

我自认是个不喜欢“重复发明轮子”的人,于是看到出发点和实现方式如此接近的框架,不免一惊,心说果然世界足够大,持同样想法的人非常多。不知道soma.js的作者有没有用过robotlegs,二者的API真的很像(也许是mvc框架的标配吧,我没看过相关介绍)。我最初也希望引入robotlegs的做法来改善JavaScript编程体验,不过在反复思考后,觉得并不需要全部移植,比如mediator。在Flash里,新的影片剪辑被添加到舞台上时会触发Event.ADDED事件,可以被robotlegs侦听;同时,所有mc都是Sprite的子类,可以使用类名作为索引来创建需要的mediator。而到了JavaScript方面,Dom节点发生变化并不会触发事件;添加的Dom节点也没有类的关系,所以这里的mediator只能我们自行创建,这样其实也就没什么实质性的好处了。

另一个不太需要移植的是Command类。在MVC框架中,它的功能基本就是响应全局事件,进行相应的处理,很多时候只要实现execute方法就好。ActionScript 3在面向方向上做的比较充分,代码都会封装成类,于是Command里还可以放一些helper类型的函数;到了JavaScript这儿就显得不太合适了,既没有强继承关系也没有类型检查,甚至连类的实现都不完整,helper也可以用闭包实现,如果一样搞成类来处理,只是凭空加重了对代码的限制,在我看来有点得不偿失了。

所以我在Nervenet中并没有把robotlegs的功能都移植,而是选取部分比较重要一定会用的实现了。(代码参看测试用例,这里不贴了)

infuse.js

接着再说infuse.js。我一开始准备直接给对象加上app或者context或者injector属性,但是一直觉得这样太过简单粗暴;看过源码发现他们比我略微温柔一点,先遍历对象的属性,如果map了同名属性,就注入进去——仔细想想这差不多是另一种粗暴吧,不由分说的注入同名属性,如果代码不是针对infuse.js写的,可能会产生更多问题。不过我还是学习了这种做法,并进行了一些改造:如果对象属性中有以“$”(可配置)开头的同名属性,就注入。有了这样的规则,新写代码有理可依,改动代码也会比较放心,阅读代码时也有利于识别本身属性和注入属性。

JavaScript没有类型检查,但是在日常开发中难免遇到多个类的实例适合同一个名字,比如model、remote之类的,如果在注入时能自动选择合适的类型,那自然是极好的。于是我想到利用变量声明时的初始值,把类名包括命名空间写进去,作为类型说明,就可以在注入时自动选择合适的类型了。

代码请看测试用例inject部分。

值得一提的是,infuse.js中每个函数都对参数进行了充分的验证,很值得学习,不过我目前还是偷懒只验证了很少一部分。

依赖管理和代码加载

依赖管理和代码加载也是我力图实现的功能,虽然看起来和架构无关,不过在操作层面上,还是比较合拍的。因为我们总要有一个入口函数,比如jQuery中的$(function () {});,通过分析入口函数,就能得到依赖关系,继而可以实现依赖管理和代码加载,这样丝毫不会影响代码架构。目前我也正是这么做的。

不过这种“自然”的代码书写方式也会给加载带来难度。无论AMD还是CMD,都会把代码以函数的形式封装起来,在依赖处理完成后执行;而这种自然的方式,就要求每段代码执行前依赖都已经加载了,所以只能用Ajax把代码以文本的形式加载下来,分析依赖,继续加载,直至全部完成;在按照依赖关系放入script标签执行。如此一来,执行的代码是不允许依赖关系嵌套的,那么,以闭包来实现私有属性和方法的做法就行不通了。这点我还在思考解决方案。

使用方式参看测试用例

总结

目前Nervenet已经初步完成,我正在编写入门文档,并将其应用到实际项目中进行测试。这些完成后将发布0.1版。目前市面上有一些做法很接近的框架,不过具体实现上还有差异,孰优孰劣也有待验证。我会尽量解决各种开发中的痛点。

NerveNet——俺也开始写框架了!

NerveNet(神经网)是一个JavaScript框架,帮助我们创造命名空间、生成事件总线,并进行依赖注入。未来它还会管理依赖,处理编译输出。

感谢姆二,如果不是你,这篇文章和文中的框架可以提前半个月面世。我爱你和你妈。

本文说明了我设计此框架的意图和实现的方式,框架本身还没写完。

NerveNet(神经网)是一个JavaScript框架,帮助我们创造命名空间、生成事件总线,并进行依赖注入。未来它还会管理依赖,处理编译输出。

我给框架命名时从Backbone那里获得了灵感,因为使用Backbone时发现各种欠缺,在逐步修补它们时,这个框架渐渐成形了。我希望,一,它能弥补Backbone欠缺的地方;二,不要依赖Backbone,以便在更多场景中发挥作用。

继续阅读“NerveNet——俺也开始写框架了!”