分类: js

有关 JavaScript 的技术文章和行业分析文章。

  • 使用Backbone的正确姿势

    使用Backbone的正确姿势

    2012年来到点乐之后,我开始投身Web应用开发。当时我选择Backbone作为主力框架,刚开始做难免会带着各种旧习惯,只有一边使用一边摸索。最近我终于搞明白使用Backbone的正确姿势,记叙于此,希望能让后来者少走些弯路。

    使用框架时,我的原则是:既然使用了这个框架,就应该按照这个框架的思路解决问题,一定要把它的功能都用上,要按照它的方式组织代码,这样才对得起学习使用的成本。所以下面的“正确姿势”,自然也是奔着全面使用Backbone的内建功能、尽量符合Backbone的设计思路,这样的目的总结的。

    本文假定读者熟悉JavaScript,对Backbone有一定程度的了解。

    View

    我比较同意《JavaScript设计模式》中的观点,Backbone的设计很难讲是更接近MVP还是更接近MVC,而是在Web前端这个大环境下,基于其技术特点,吸取各种设计模式的优点做出最合适的的实现。Web应用中,视图的实现自然应该交给HTML和CSS负责,而在数据变化时更新视图,以及响应用户操作的事情,就交给Backbone.View

    Backbone.View提供非常直观的events帮助我们注册事件,免受使用jQuery的.on.on.on之苦。事件被委托给$el,不仅节省资源,并且做列表类应用需要操作多个子节点时,无需再绑定事件。

    不过请注意,这里的事件委托仍然有赖冒泡机制,所以诸如loaderror等不冒泡的事件无法委托给$el处理(submit在早期IE下也不行,但是jQuery会代发冒泡版),只能手工侦听具体节点。另外,Backbone会把处理函数代理给View的实例,所以函数中的this指向是实例,而不再是触发事件的DOM元素。

    列表的前世今生

    接着,来点代码,看看最常用的UI范式——列表。Backbone中,搭配使用Backbone.CollectionBackbone.View可以方便的实现列表类组件,不过因为Backbone本身的限制很少,实现方法很多。早期我习惯于通过reset方法和reset事件来操作列表,后来慢慢体会到,接下来的做法更合适。

    HTML部分:

        // 这里假设我们要做一个todo列表
        <ul>
          <script type="text/x-handlebars-template">
          <li id="{{id}}">
            <input type="checkbox" name="todo" value="{{id}}">
            {{title}} <time datetime="{{create_time}}">{{create_time}}</time>
          </li>
          </script>
        </ul>
    

    JavaScript部分(使用Handlebars作为模板引擎):

        var ListView = Backbone.View.extend({
          fragment: '',
          events: {
    
          },
          initialize: function () {
            this.template = Handlebars.compile(this.$('script').remove().html().replace(/r|n|s{2,}/g, '');
    
            this.collection.on('add', this.collection_addHandler, this);
            this.collection.on('remove', this.collection_removeHandler, this);
            this.collection.on('sync', this.collection_syncHandler, this);
            this.collection.fetch();
          },
          collection_addHandler: function (model) {
            this.fragment += this.template(model.toJSON());
          },
          collection_removeHandler: function (model) {
            this.$('#' + model.id).remove();
          },
          collection_syncHandler: function () {
            if (this.fragment) {
              this.$el.append(this.fragment);
              this.fragment = '';
            }
          }
        });
    

    之所以建议这么做,因为Backbone.Collection有三个特点:

    1. model的事件会由collection向外转播(相当于冒泡)
    2. 取得数据后,collection会逐个创建model,每次都会广播add事件
    3. .fetch().set()时,新数据中不曾出现的对象(以id为标识)会被移除

    列表中还有一些技巧,将在后文呈现。

    钦差大臣options

    早先取数据时为了向服务器传递变量,或者使用特定的jQuery参数,我经常覆写.sync()方法,甚至直接使用$.ajax()。后来发现,各函数的options(除去少数几个return之外)都会传递到下一个函数,直至最后;期间每一步的参数都会合并进来向后传递。

    所以我们只需要在第一次调用函数时,将需要的值放在options里即可 。比如,要保存model里的数据,API服务器和当前服务器不在同域,就可以这样:

    // 关于xhrFields,可以参考jQuery文档:http://api.jquery.com/jQuery.ajax/
    model.save(null, {
      xhrFields: {
        withCredentials: true
      }
    });
    

    其实,options是个很巧妙的设计。Backbone作为框架,必须给其它库和业务逻辑留出足够的空间,使用options,随便其他开发者传什么值,最后都能传回业务逻辑中。另外,当参数个数比较多的时候,使用options也有助于阅读代码。

    options进阶

    基于“从头传到尾”这个性质,我们还可以发明一些特殊用法。比如上一节的例子,我希望给每个元素增加一个删除按钮,点击后移除元素。重点是:以渐隐动画来表现移除动作。在Backbone中,.destroy()方法会通知服务器删除对象(.remove()方法只是从当前集合中移除model,无法满足需要),并且触发destroy事件,我们可以在这里插入动画;但立刻就又会触发remove事件,所以只是在collection_destroyHandler的时候fadeOut是不够的,还要防止collection_removeHandler在动画结束前直接移除dom。

    这个时候,我们就可以利用optionsdestroy()支持参数{wait: true},可以等待服务器返回成功后才移除model。于是我们就能确保对象已经从服务器上清除后,再以视图体现;同时,只要检查options里是否包含这个属性,就可以知道当前触发collection_removeHandler的是.destroy()还是.remove(),再决定是否立刻移除节点就很容易了。(其实随便传个什么标记都可以,这里使用{wait: true}可以获得更好的体验。)

        // 一致的代码我就不写了
        events: {
          'click .delele-button': 'deleteButton_clickHandler'
        },
        initialize: function () {
          // 一致的代码
          // ....
          // 不再重写
          this.collection.on('destroy', this.collection_destroyHandler, this);
        },
        collection_destroyHandler: function (model) {
          this.$('#' + model.id).fadeOut(function () { $(this).remove(); });
        },
        collection_removeHandler: function (model, collection, options) {
          if (!options.wait) { // 没有wait
            this.$('#' + model.id).remove();
          }
        },
        deleteButton_clickHandler: function (event) {
          var id = $(event.target).closest('li').attr('id');
          this.collection.get(id).destroy({wait: true}); // 服务器返回确认才真正移除
        }
    

    利用options能达成的效果还有很多。比如,有些时候我们想往model里放一些特殊用途的数据,只在渲染时候用,不保存到服务器上。通过研究源码我们发现,同样调用.toJSON().save()会传入含有各种参数的options,而手动调用则不会。于是我们又能根据options里的属性返回不同的数据,满足不同的需要。

    这些实现本文不再一一详述,大家请自行琢磨,欢迎留言探讨。

    和服务器步调一致—— sync & fetch

    有些习惯延续自之前的项目,比如请求远程数据。早期我总想用$.ajax从服务器端把数据取来,然后再resetcollection或者model。直到最近开发新项目:tiger-prawn + lemon-grass(也即点乐后台V5),我开始开发RESTful的后端,配合专为RESTful设计的Backbone,贯彻“同步”思路,于是我终于醒悟,发出文章开头那句感慨——我终于明白使用Backbone的正确姿势了。

    “同步”是Backbone非常重要的设计思路,也是用户体验里非常重要的一环。我们经常要面对多端的环境,尤其是开发企业级应用,多人协作办公,必须保证数据在每个终端看起来一致。我一开始很难理解为什么.fetch()回来数据后,除非指定{remove: false},否则新数据中不再存在的对象会被移出collection。这个疑问后来在开发集体协作的todo列表时得到解答。

    还拿前文的todo列表做例子。想象列表面向一个工作组,组内成员均从列表当中接受工作,完成后勾上checkbox表示结案;别的地方还会有需求源源不断的塞入这个列表中。这个时候,数据同步就显得尤为重要。某甲勾掉一条任务,需要从其他人的列表中勾掉同一条任务。由于我们数据交互的主要手段仍然是Ajax,服务器无法向浏览器发出指令,只能由浏览器通过返回值进行操作,所以,将“同步”作为强制性要求,本地collection根据返回值的变化,该修改的修改,该移除的移除,就是最佳选择了。

    trigger: false未必都有用

    最后再说个小问题。有时候我希望改变路径,但不要刷新页面,比如创建文章/article/create/,.save()后得到文章id,跳转到/article/id。因为仍然处于编辑状态,所以不需要刷新页面。一开始我以为router.navigate(‘#/article/id’, {trigger: false});,加个参数{trigger: false}`就可以防止路由生效,后来发现不行。

    出于种种原因,比如服务器没做重定向,我一直没用pushState。此种状态下,Backbone使用setInterval检查地址栏变化,所以只要地址栏有修改,就会触发相应的路由。

    相关项目

    下面是我做过并且在维护的一些使用了Backbone的项目。这些项目未必都做到了以上几点,所以仅供参考。

    团队培训项目团队培训项目二期,其实从这两个项目中就能看出我思路的变化。

    游戏宝典 手机应用,虽然项目停摆了……找时间更新移植到phonegap上。

    总结

    Backbone是一个轻量级,入侵程度很低的框架。可以很方便的结合各种其他库来使用,对使用者的要求也很少,易于学习。不过如果能以正确的姿势操作,又能达到事半功倍的效果。这篇文章写得很费劲,有些东西总是感觉话到嘴边写不出来,反复修改多次对最后两段仍不满意。哎,以后再说吧,先发了。

  • HTML5跨域开发

    HTML5跨域开发

    HTML5中提供了跨域加载数据的方法,让我们得以从JSONP或者Flash中介等各种绕行方案中解脱出来,更加顺畅地与服务器交流。另一方面,因为PHP是最好的语言……所以在它与Node.js之间,我选择前者作为后端语言开发内容服务。

    这篇文章记录使用jQuery+PHP开发跨域应用时的小心得。

    身份验证

    做身份验证,最简单的办法就是使用PHP的SESSION保存用户信息,于是就要用到Cookie。默认情况下,跨域Ajax请求发起时候不包含Cookie,需要我们主动将XHRwithCredentials属性设为true才行。

    jQuery会把XHR封装成jqXHR,并且不暴露真正的XHR(说实话这点有点难以理解,尤其是在做上传进度条的时候)。然后它提供一个给真正XHR赋值的接口xhrField,所以写成代码就是这样事儿的:

    $.ajax(url, {
      xhrField: {
        withCredentials: true
      }
    }
    

    各种HTTP头

    如果不需要验证用户身份,直接在HTTP头中输出Access-Control-Allow-Origin: *即可。

    我的产品需要验证,那么首先,HTTP头中必须有Access-Control-Allow-Credentials: true;此时对域的限制也严格许多,不再允许像前面那样使用*放开给任何来源,必须指明哪个具体域可以接受。

    关于Access-Control-Allow-Origin的值,规范中的说明是“域名列表或null”,然则接下来的“注意”有点诡异:“实际生产中,‘列表或null’要求更严格。你可以认为它实际只允许单一域名或null,而非空格分隔的域名列表。”——既然如此你干脆写个“域名或null”不就完了……

    总之对于我们而言,返回的HTTP头中还要包含Access-Control-Allow-Origin: http://域名,指定允许作为来源的协议、域名、端口,并且只能有一个(组)。因为通常来说我们开发环境和生产环境不一样,所以这里的域名最好不要写在服务器配置里;使用PHP,通过$_SERVER['HTTP_ORIGIN']取出访问来源,与白名单比对,通过后再输出相应的头,更加合适。

    调试

    我选择JSON作为前后端交流的格式。为了方便浏览器解析(也是HTML5的要求),我还返回了Content-type: application/json头。

    使用PHP少不了使用Xdebug。出现错误时,Xdebug会返回完整的栈,有利排查。但是为了方便阅读,Xdebug还会给返回信息套上<table>结构,这时Chrome的Network工具就会把它解析成奇怪的格式,所以Content-type一定要最后和数据一起返回。

    与之相反的是前文说到的Access-Control-Allow-OriginAccess-Control-Allow-Credentials,这二位必须放在最前面。不然如果出现500错,响应头不包含这两个跨域标记,Chrome就会理所当然地不显示返回内容,也就无法看到错误描述,根本无法排查。

    参考资料

    1. Using CORS
    2. Cross-Origin Resource Sharing
    3. HTTP access control (CORS)
    4. jQuery.ajax()
  • Backbone.Collection.fetch小优化一则

    使用Backbone开发无限滚动应用的时候,必然会用到Collection.fetch()加载后面的内容。在Backbone的设计中,fetch之后,Collection会把取回来的内容“智能合并”到当前结果集中,该添加的添加、该修改的修改、该删除的删除,并且触发不同的事件。

    这样就会带来一个问题,因为我们知道,向DOM树当中添加元素的次数越少越好,因为每次添加都会导致重新计算布局和重绘;但是Backbone又是依次触发add事件的,怎么办呢?

    其实也容易,因为set完成后,Collection还会广播sync事件,所以我们可以在响应add事件时拼装jquery对象,响应sync事件时再把它们添加到DOM树中。代码如下:

    var InfinityScroll = Backbone.View.extend({
      initialize: function () {
        this.collection.on('add', this.collection_addHandler, this);
        this.collection.on('sync', this.collection_syncHandler, this);
      },
      collection_addHandler: function (model) {
        var html = this.template(model.toJSON());
        this.fragment = this.fragment ? this.fragment.add(html) : $(html);
      },
      collection_syncHandler: function () {
        if (this.fragment) {
          this.$el.append(this.fragment);
          this.fragment = null;
        }
      }
    });
    
  • Nervenet + Backbone

    介绍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版的测试功能,以后可能会有各种改动。

  • 只顾好看了,真蛋疼

    Morris.js的性能不行,原本用Google Chart很容易画出来的图表用Morris直接就把Chrome搞死了……

    偏偏我移植到最后才发现……蛋疼啊……只要继续混用了……啊……

  • jQuery append script在chrome下无法触发load事件

    尼玛折腾了一早上,居然这样写会导致事件无法触发:

    var script = document.createElement('script');
    script.src = 'somefile.js';
    script.onload = function () { console.log('xx'); };
    $('head').append(script);

    最后一句换成纯js就解决了:

    document.head.appendChild(script);

     

  • 导出 Table 数据并保存为 Excel

    导出 Table 数据并保存为 Excel

    最近接到这么个需求,要把 <table> 显示的数据导出成 Excel。类似的需求并不稀罕,过去我通常用 PHP 输出 .csv 文件。不过这次似乎不太合适:作为数据源的表格允许用户有一些筛选和排序的动作,与原始数据显示有区别,传递操作比较麻烦;另外 .csv 文件的功能受限严重,难以扩展。所以我准备尝试下别的做法。

    Google之,发现 HTML5 又成了一座分水岭。之前在IE浏览器下,用户可以利用 ActiveXObject 创建 Excel.application 对象来处理。后来 Excel 开放标准,可以导出 xml 格式的文件,dataURI 就有了用武之地,导出 <table> 数据并保存为 Excel 有了更好的选择。

    (以下内容与 StackOverflow中的答案有重合,那个3条赞同的我认为是最佳答案,可惜我没法顶他……)

    准备工作

    1. 创建一个空白的Excel文档
    2. 另存为“XML表格”,XML 格式
    3. 好了,模版搞定

    或者,直接复制下面一段(这一段我使用了Handlebars模版,以便将来填充数据)

    template = ‘<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40"><head><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>{{worksheet}}</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--></head><body>{{#each tables}}<table>{{{this}}}</table>{{/each}}</body></html>';

    复制表格数据

    复制数据比较简单了。如前面模版所示,这里我很野蛮的直接复制 <thead><tbody> 的全部代码,填充内容。当然为了体现用户操作,我只复制显示的 <tr>。这里需要注意的是,jQuery 判断一个dom 是否处于显示状体基于以下3点:

    1. display:none
    2. 表单元素,type="hidden"
    3. 宽高为0
    4. 父级以上节点不显示,自己也不会显示

    所以,不能先 .clone().find(':hidde').remove(),因为添加到主 Dom 树之前,节点宽高都是0,也就会被认为还没显示,这下就都干掉了。

    输出内容

    套用模版之后,我们就有了完整的表格数据。接下来,我们需要把其转换成 Base64 格式,以便套用 dataURI 输出。于是便要使用 btoa 这个函数(将二进制数据转换成 base64 格式的字符串),不过注意,这个函数不能直接转换普通 unicode 字符,不然大多数浏览器都会抛出异常。所以需要先经过两步转换:

    function base64(string) {
      return window.btoa(unescape(encodeURIComponent(string)));
    }

    (MDN 还推荐了 另外一种做法,通过 Typed Array 做中介,我没有实操,有兴趣的可以试下。)

    然后配上 data 头和 mimetype,就可以触发下载了:

    var uri = 'data:application/vnd.ms-excel;base64,';
    location.href = uri + base64(template(tables));

    提升体验

    貌似到这里就完成了,不过作为一名挂职产品总监的码农,我很难容忍下载的文件文件名是“下载”,而且还没有扩展名(Windows 8 下,Windows 7 和 Mac 下会有.xls的扩展名,我认为和装的软件注册 mime 类型有关)。

    这是个用在内部管理后台的需求,我之前曾要求大家必须使用 Chrome 访问后台;而且我知道,Chrome 已经支持 <a> 里的 download 属性。那么这就好办了,因为 onclick 事件会先于系统默认行为触发,所以我可以在这个事件的处理函数中将生成的 Base64 放在被点击按钮的 href 里,并将其 download 属性设为容易理解的“某年某月末日至某年某月某日广告数据分析.xls”。至此,此项功能宣告圆满。

    HTML部分(使用了Bootstrap和Handlebars):

    <a href="#" title="点击下载" class="btn btn-primary export-button" download="{{start}}至{{end}}广告数据分析.xls"><i class="icon-download-alt icon-white"></i> 导出</a>

    JavaScript 部分

    tableToExcel: function (tableList, name) {
      var tables = []
        , uri = 'data:application/vnd.ms-excel;base64,'
        , template = Handlebars.compile('<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40"><head><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>{{worksheet}}</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--></head><body>{{#each tables}}<table>{{{this}}}</table>{{/each}}</body></html>');
    
      for (var i = 0; i < tableList.length; i++) {
        tables.push(tableList[i].innerHTML);
      }
      var data = {
        worksheet: name || 'Worksheet',
        tables: tables
      };
      return uri + base64(template(data));
    },
    exportHandler: function (event) {
      var tables = this.$('table')
        , table = null;
      tables.each(function (i) {
        var t = $('<table><thead></thead><tbody></tobdy></table>');
        t.find('thead').html(this.tHead.innerHTML);
        t.find('tbody').append($(this.tBodies).children(':visible').clone());
        t.find('.not-print').remove(); // not-print 是@media print中不会打印的部分
        t.find('a').replaceWith(function (i) { // 表格中不再需要的超链接也移除了
          return this.innerHTML;
        });
        table = table ? table.add(t) : t;
      });
      event.currentTarget.href = Dianjoy.utils.tableToExcel(table, '广告数据');
    }

    尾声

    说是圆满,其实也不尽然,因为 URL 有 2M 的长度限制,遇到真正的大表仍然可能出问题(我没实测)。

    最后例行吐槽:老板(领导)想提升工作效率,必须考虑员工的日常软件:不许用乱七八糟的浏览器,统一 Chrome;360 一定禁用(最近遇到 N 起升级 Chrome Dev 30 版导致各种 bug 的问题);全部装 Windows 8(自带杀毒,几乎所有外设秒配)。能做到这几点,公司办公效率提升1倍不止。

  • soma.js, infuse.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版。目前市面上有一些做法很接近的框架,不过具体实现上还有差异,孰优孰劣也有待验证。我会尽量解决各种开发中的痛点。

  • HTML5梦工厂交流会

    在微博上看到 @HTML5梦工厂 要开一个小型的交流会, @司徒正美 要来分享他的Avalon框架,我算了算,今天正好没事儿,就报名参加了。

    自从在宠物派见识到架构给产品开发带来的提升后,我就开始关注各种框架,并且在加入点乐后开始使用Backbone。Backbone是个很好的框架,解决了很多JavaScript的先天不足;当然还有一些地方可以加强,这也是我最近在努力的地方。与Backbone所属的MVC不同,Avalon是一个MVVM框架,与近期开始流行的Angular、Ember.js使用了同一个模式。所以我一直想多了解下这个模式的特点。

    不得不说,正美大的普通话和音量有点影响效果。听下来,我的理解是这样:

    1. MVVM = Model + View + ViewModel
    2. 在前端开发方面,HTML + CSS承担了View的职责,所以我们只需要实现Model + ViewModel部分就可以了
    3. Model是数据结构,属于设计方面的工作,所以职责集中在ViewModel的实现
    4. ViewModel等同于业务逻辑
    5. Web开发使用MVVM的优势在于,我们可以只关注业务逻辑,也就是ViewModel的实现,这样就极大减少工作量和代码量

    不过我还是有一些疑问,比如性能、复用性、维护效率之类的。看来有必要用Angular框架做一个项目了(大厂产品应该更有保障些~)。

    会上还听了一个CodeJam项目《谁是卧底》的分享,坦白说那哥们对phonegap开发最佳实践的理解还不如我,有机会写篇文章。

    最后帮公司打了个广告,希望能收几份简历。回头Nervenet能拿出去见人的时候也去分享下吧。

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

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

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

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

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

    (更多…)