我的技术和生活

  • 只顾好看了,真蛋疼

    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);

     

  • [教程]纯CSS实现多选组件

    [教程]纯CSS实现多选组件

    产品篇

    在我们的后台中,需要设置广告精准投放的区域,也就是要在全国31个省、自治区、直辖市中选择。那么,出现下面这幅景象也就理所应当了:
    1

    这样做有几个问题:

    1. 选项很多,没有规律,找起来很累
    2. 如果是一个已经选择了部分选项的广告,修改时仍然需要用肉眼寻找,无法一眼看出来投放到哪些省份
    3. 选完一个,再选下一个,还要从头找,甚至会被已经选过的影响

    于是我想,首先应该把所有选项分为“已选中”和“未选中”两批,解决第2个问题,减轻第3个问题;其次复选框本身的价值不大,可以被替换为其它样式;唯一可能引入的问题,就是点选时,用户的预期是看到复选框里出现一个小对勾,表示选中,如果我把它移开放到“已选中”组里,用户可能会迷惑,需要一些时间学习。

    于是我跟某产品经理朋友聊了聊这个想法,他表示确实可能造成用户迷惑,不过如果能加入动画效果,那么基本没问题。嗯,开始动手。

    技术实现篇

    近日flexbox规范定案,浏览器相继支持display:flex;,同时传来一条好消息,新实现比老实现display:box;快很多。这次我打算用flexbox来解决问题,因为里面有一个很重要的属性:order(之前叫box-ordinal-group),它可以改变布局中元素的排列顺序,配合CSS3新增的选择器,应该可以满足需要。

    第一步 分拆选中/未选中

    (关于flexbox的知识,可以通过Google了解,虽然搜到的多是上一个版本,不过和最终版差别不大,只是叫法不同。本文不再过多讲解,我就当大家都会了)

    <input type="checkbox">本身的样式不能修改,所以我们必须借助<label>的帮助;实现选中/未选中区分,那自然就要用到伪类:checked;选择器一定是从外到内、从前到后的,没法选择父级元素,所以不能用<label>去包<input>,那么最终布局就只能是:

    <div>
        <input type="checkbox" name="q[]" id="q1" />
        <label for="q1">小宝3225</label>
        <input type="checkbox" name="q[]" id="q2" />
        <label for="q2">王老白白白</label>
        <input type="checkbox" name="q[]" id="q3" />
        <label for="q3">空夫31</label>
        <input type="checkbox" name="q[]" id="q4" />
        <label for="q4">谷大白话</label>
        <input type="checkbox" name="q[]" id="q5" />
        <label for="q5">Meathill</label>
        <input type="checkbox" name="q[]" id="q6" />
        <label for="q6">一毛不拔大师</label>
    </div>

    很简单哈,不解释了。CSS3新增了“下一节点”选择器 +,用来选择某节点的下一个节点,结合:checked伪类就可以将选中的<input>和它临近的<label>通过改变order属性移到前面去:

    #container {
      display:flex;
      flex-direction:row;
      flex-wrap:wrap;
    }
    #container input,
    #container label {
      order: 2; //所有选项、label顺序为2
    }
    input[type=checkbox]:checked,
    input[type=checkbox]:checked + label {
      order: 0; // 越小越靠前
    }

    不过这样只是把选中的内容提前,视觉上没有真正的分割。所以我决定再加入一根分割线,上面是选中的,下面是未选的。这个时候我们需要用到 ~ 这个选择器,选择某节点后面的节点:

    hr {
      display:none; // 默认情况下,没选任何选项,分割线隐藏
      order: 1; // 分割线顺序为1
      width:100%; // 保证独霸一行
    }
    input[type=checkbox]:checked ~ hr {
      display:block; // 有选项被选中后才会显示分割线
    }

    Demo如下:

    这样基础功能实现了。不过视觉上,排版仍然不整齐,选中的选项和未选中的选项区分不算太明显,所以下一步我准备美化下checkbox。

    第二步,美化checkbox

    做法与前面类似,也要用到CSS3新增的选择器。前面为了实现<label>提前,没有用它包裹<input>,所以在选项很多很长导致换行的时候,可能出现复选框和标签脱离的尴尬状况。好在复选框的价值可以用别的样式取代,所以先把小方框隐藏起来,转而将<label>作为操作目标,再来点边框底色圆角(参考自Bootstrap 3),就可以了:

    input[type=checkbox] {
      display: none;
    }
    label {
      min-width: 120px;
      border: 1px solid #CCC;
      padding: 2px 8px;
      text-align: center;
      margin: 0 5px 5px 0;
      background: #FFF;
      color: #333;
      border-radius: 3px;
      box-sizing: border-box;
    }
    label:hover {
      border-color: #ADADAD;
      background: #EBEBEB;
      cursor: pointer;
    }
    input[type=checkbox]:checked + label {
      order: 0;
      background-color: #5cb85c;
      border-color: #4cae4c;
      color: #FFF;
    }
    input[type=checkbox]:checked + label:hover {
      background-color: #47a447;
      border-color: #398439;
    }

    这样看起来还有上升空间,如果加上几个图标响应用户操作,那么学习成本会更低,对操作后的预期也会更准确。于是引用CDN上的font-awesome,使用:before伪类加上小图标,就得到了最终效果:

    我无意中发现,这样批量添加删除时,鼠标可以常点不动,应该也是个意外的收获吧。

    第三步,加入动画教育用户(失败)

    至此功能基本做好了,不过由于修改了行为,可能导致用户迷惑,所以准备加个动画帮助用户理解这个交互。

    可惜作为一个新功能,浏览器的支持尚不完善,虽然规范中规定“animatable: yes”,但是实测在Chrome v.30也无法工作:http://jsfiddle.net/meathill/Ka66W/1/

    看来只有等新版浏览器发布后再去完善了。

    兼容性

    使用纯CSS做组件,几乎不用担心兼容性问题,因为浏览器本身就做了很好的向下兼容,代码最多不生效,一般不会错。

    具体到这个组件,因为只针对视觉效果,没有增删改任何浏览器行为,所以兼容性也没有任何问题。不过最终效果呢,只有支持flexbox和CSS3选择符的浏览器才能正常渲染。

    我的环境是Window 8 + Chrome v.30,以及小米2 + Chrome v.30,测试通过。

    后记

    如今CSS很强,纯CSS可以实现很多功能,希望今后能做出更多有价值的东西。分享这个组件的实现,希望对大家有用。

  • 推荐好用的JS CDN

    上周GFW又抽风,导致取自CDN的jQuery和Bootstrap经常404,后台各种罢工。

    开始想说干脆放弃CDN得了,结果自家服务器也不是很给力……本地路径的静态文件也经常加载失败,挠了半天头,再去找找国内的CDN吧。

    后来想起来前几天看到七牛搞了个免费的开源仓库CDN,通过Google找到,叫http://www.staticfile.org/。打开一看,首页只列出不多的几个库,版本也不是最新的。我以为又是个没人维护的烂尾工程,读了介绍才知道他们倡导大家都来提交库信息,共同建立全面的CDN资源。我本想把这次要到的库和可以更新的库提交上去,后来发现原来他们已经引入了cdnjs.com里所有的库,只不过没有写在首页……果然大家都喜欢写代码不喜欢写文档啊,差点就错过了。

    BTW,cdnjs.com居然还提供了animate.css,真好。

    好在他们做了命令行工具,可以装上查引用地址。比如我想知道能不能用underscore,就可以这样:

    // 安装
    npm install -g sfile
    
    // 查找underscore
    sfile search underscore
    
    // 得到链接,这里要用全名
    sfile get underscore.js

    最后看https://github.com/staticfile/static/issues里的内容才知道,他们会把国外成熟的库直接从cdnjs复制过来,提交新库应以国内的为主。嗯,将来把Nervenet弄完也提交进去。

    重复一下网站地址:http://www.staticfile.org/。最后感谢下这些好人,以及服务提供商七牛云存储

  • 悲催的Android Webview——记新版广告墙开发

    悲催的Android Webview——记新版广告墙开发

    前一阵很辛劳,所以荒废了博客。前几天终于完成了这项艰苦卓绝的工程:HTML5版广告墙,决定写篇文章,记录一下踩过的坑。

    项目介绍

    广告墙属于典型的列表式应用:打开后是无尽列表,通过滑动手指驱使列表滚动,上拉加载更多内容,下拉刷新列表(这次没做)。单击列表中的某项,打开详细页;单击上面的后退按钮,退回上一级页面。

    考虑到目标平台是Android系统,内嵌WebKit内核,我采用了时下流行的HTML5+CSS3技术,并且做好了向下兼容的准备。为了减少代码量,我放弃了jQuery,也没有考虑半调子Zepto,全部使用原生开发。操作上,开始想用Hammer.js封装手势,后来处理滚动效果处理得不好,最终选择了iScroll 5。模板方面,前端使用Handlebars.js,后端使用Mustache,所以编译时还写了段代码做转换。

    项目代码托管在github上,因为前端不涉及到业务逻辑,所以仓库是公开的,有兴趣的同学以clone下来看看。

    技术选型大概聊这些,接下来开始进入正题。

    性能vs兼容性

    实际上,随着硬件的发展,对于列表式这种非性能敏感性应用来说,性能早已不是制约其发展的首要因素了。另一方面,有一定专业精神的前端如我,平时也会注重积累各种性能优化的点,并不断在项目中实验、实施——所以我就很搞不明白,技术实力强大如jQuery,为啥会弄出个jQuery Mobile这种完全不能用的东西出来……

    相反,在这个项目中,由于平台分裂带来的兼容性问题成了最大的绊脚石。尤其是这些兼容性问题,极少找到现成的解决方案……下面举两个例子:

    Vivo X909 和z-index

    正常情况下,position:absolute;的dom节点在不显式设置z-index属性区分层级的时候,默认后面的节点会显示在前面的节点上方。大多数浏览器里都是这样,可是Vivo X909就不行。

    列表大家都知道,点击某项,详情会从右边滚动进来。由于没有设置-webkit-tap-highlight-color(现在想想应该做的,下周回去加上吧),所以这个bug当时的的表现是“单击没有反应”。这种情况很难调试,我开始以为JS有错,试了很久,后来配置好Weinre(见上篇博客),才找到症结所在。最后把几个图层分别加上z-index,问题解决。

    早期Android的animation-fill-mode

    我们仍有20%的用户在使用2.3.x版本的Android,所以这块市场必然不能放弃。在小米2上调是好的动画,在老HTC G10上,滑出的图层在动画结束后会消失。这次好在有了上次的经验,直接上Weinre;加上caniuse.com也有明确的描述,所以很快解决了。


    各种难以定位、缺少文档、找不到解决方案的兼容性问题层出不穷,让我不禁感慨:本以为买上了WebKit的康庄大道,谁知尼玛这豆腐渣工程下面全是坑啊……

    滚屏

    前面提到,开始时间比较富裕,我也比较自信,想借助Hammer.js,慢慢打磨出色的滚动体验。后来无尽的bug环伺左右,实在无法专心研究,便退而求其次,使用iScroll了。

    iScroll如今已经发展到5,其作者自信的表示:“我尽全力提升了其在Android平台上的表现,我相信现在它已经达到了极致。”实验效果确实很理想,无论是高配置的小米,还是老旧的HTC G10,都表现出色。将来有空的时候得好好研究下他是怎么做的。

    工程实践

    随着时代发展,Web开发这种脚本语言也开始分裂成开发、部署两个阶段,这是个好事情。核心代码需要为维护、测试留下足够的空间,势必在性能表现上有所不足,部署时根据应用场景的不同输出不同的结果正好能解决这个问题。这个项目中我使用Grunt,花费了不少精力在编写Gruntfile.js上。

    开发时,我将测试用的数据,按照设计好的数据结构写在define.js中,生产环境中用服务器生成真实数据来替代。不同的应用场景,也用其配置。如今,目标支持的6个场景都能稳定工作,效果不错。

    Windows下不好配ruby,使用SASS是个问题,不过后来找到了Prepros,问题迎刃而解。

    技术细节分享

    简单构造选择器

    jQuery功能确实强大,不过实在太重型了;备胎Zepto轻便一些,不过功能也弱一些,语法做不到全兼容,用起来不舒服。仔细考虑这个项目的需求,无非是:找到某个元素,给它添加事件侦听;或者判断它是否包含某个class,那么自己写一个直接到Dom节点的选择器就足够了。后来看到篇文章,里面详细测试了querySelectorgetElementById众的性能,发现还是后者大大领先——尤其在移动平台上,于是基本放弃了用前者的念头。

    这个选择器实现起来很容易:

        var $ = window.$ = function (selector, root) {
          root = root || document;
          var dom;
          switch (selector.charAt(0)) {
            case '#':
              selector = selector.substr(1);
              dom = document.getElementById(selector);
              break;
    
            case '.':
              selector = selector.substr(1);
              dom = root.getElementsByClassName(selector)[0];
              break;
    
            default :
              dom = root.getElementsByTagName(selector)[0];
              break;
          }
          return dom;
        };
    

    另外,$还可以用来当命名空间,保存一些需要全局使用的变量和方法。

    使用Handlebars.js生成预编译模板

    预编译模版可以降低运行时的运算量,对于移动开发这种能省则省的应用场景,实在是必备之物。另一方面,为了方便开发,把模版放在HTML里更好调整。好在有“编译输出”这个步骤,可以两全其美。

    Handlebars.js的预编译是把模版转换成根据数据生成HTML的JS代码,可以使用uglify压缩。编译之前最好过滤掉多余的空格和换行符,这样生成的JS文件也会小很多。

    当然,再预编译也不如直接渲染HTML来的快,所以在服务器端生成页面的时候,一定要包含实际内容,让用户尽快得到信息。而PHP没有现成的Handlebars实现(我也不打算写),好在有mustache.php,并且handlebars兼容mustache(其实不完全,也被坑了),所以我写了一小段代码将Handlebars模版转换成Mustache模版

    使用attr()实现CSS中的多语言

    除了传统的中文版,这次还要制作英文版。英文版与中文版的功能几乎完全一致,只是文字全部换成英文。最简单的做法就是搞个en.html——因为使用了模版,JS里不包含任何文字内容;但是“上拉加载更多”这个功能目前使用CSS的:after实现——初衷是不修改dom,不引起relayout——当然,反正我用着Sass,增加个语言包不难,不过我还是希望把修改集中在一个地方。

    后来查了下CSS表达式的兼容性,发现Android 2.1起就支持,那就好办了,先把各阶段文案放到HTML的属性里:

        // 中文
        <div id="list" data-normal="继续上拉,加载更多广告" data-more="松开加载" data-loading="加载中..." data-no-more="没有其它广告了,再看看吧" data-error="加载失败,请稍候重试" data-over=""></div>
        // 英文
        <div id="list" data-normal="Pull up to load" data-more="Release to load" data-loading="Loading..." data-no-more="No more ads" data-error="Load failed" data-over=""></div>
    

    然后在CSS里使用attr()访问即可

        #list.over
          &:after
            content: attr(data-over)
    
        #list.more
          &:after
            content: attr(data-more)
    
        #list.loading
          &:after
            content: attr(data-loading)
    
        #list.no-more
          &:after
            content: attr(data-no-more)
    
        #list.error
          &:after
            content: attr(data-error)
    

    WebKit中z-indextranslateZ(0)混用,导致click事件半失灵的解决

    1. 要在浏览器里触发下载,只要链接到一个浏览器打不开的文件就行。链接跳转是在click触发的浏览器内建行为,换言之,链接跳转依赖于click的正常触发。
    2. clicktouch event晚200~300ms,所以为了保证用户体验,我们通常用taptouchstart + touchend)来替代click响应用户操作
    3. 所以一个“点击下载按钮”的操作,就会先响应tap事件,弹出详情浮层,告知用户获得积分的最终条件,然后等待浏览器开始下载
    4. CSS动画最好使用translateZ(0)促使浏览器启用GPU加速
    5. 这个bug在以上条件下产生,影响这个过程

    之前我在PC版Chrome里也曾见识过这个bug(最新版没验证,大概20+的时候吧)。具体表现为,一个层,它的translateZ有赋值,它可能被移到某处(通过translateXtranslateY,或者动画过程中),不过还未到达那里,但是它仍然会拦截那里mousedown/mouseup事件,导致click事件无法正常触发。

    这个bug非常难排查,因为它并不影响tap;视觉上看不到那个层,会想当然的认为不应该跟那个层有关系;并且,WTF,如果按得时间稍长,比如半秒,就可以触发click……因为触发条件比较复杂,我甚至很长时间没找到规律。最后只好请出“小黄鸭调试法”,试图向索隆解释发生了什么,然后发现无法正常下载的按钮都是被详情浮层遮盖着,继而回忆起以前解决过类似问题。

    最后解决这个问题的方案我其实不很满意,不过确实能顺利工作:

    1. 层的当前位置不会覆盖按钮,先不给层增加动画
    2. tap后,给click留出足够的时间(我这里用400ms),再给层添加动画
    3. 确保click可以响应,通过setTimeout给层添加动画——本来300ms就测试通过,不过为了保险,我还是决定写成400

    仍然悬而未决的问题

    虽说项目已经快上线了,但仍有几个不解之谜……

    1. 某些手机仍然无法下载(启动<a>
    2. 快速拖动的时候可能把内容拖走(应该是触发了浏览器默认行为)

    总结

    做完项目回头看,系统分裂造成的兼容性问题比想象中多很多,原先以为最多是有些2.x版本不支持,做向下兼容就好,谁知还有各种实现细节问题。不过,在PC Chrome + 手机Chrome + Weinrealert之后,只要有耐心,大多数问题时可以解决的。

    说完开发效率,另一方面再来看看产品表现。我们对性能不需要太乐观,也不用太悲观,敏感型应用,老老实实用原生开发,或编译成原生应用;非敏感型,跨平台高兼容性(咦,这眼泪哪里来的)的优势很明显。通过我实际看到移动端的表现,我们这些页面仔应该更有信心,将来移动大市场里,Web App的份额绝对不比Native App要少。

    这项事业任重而道远,需要我们一起努力。

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

  • 本地部署weinre帮助移动开发

    本地部署weinre帮助移动开发

    weinre是个开源项目,用来在Web开发中做远程调试的工作,相当给力。后来也捐给了Apache基金会,并且从ruby移植到了JavaScript,现在可以直接通过npm安装。在最近的广告墙大重构中,这个工具帮了我很大的忙。

    安装weinre首先要装node.js,后者在Windows和Mac上直接下包就能装,并且自动配置环境,很方便;在Linux上需要自己编译一次,也不复杂,我之前有篇日志写了,现在还好使,可以看看。

    node.js环境搞定后,直接用npm就可以安装weinre了,直接装在全局中最好:

    sudo npm install weinre -g

    接下来启动weinre,我在这里卡了一阵,大概因为不太了解端口侦听的缘故,我以为直接启动就好,结果从外面连不上(手机连不上连点反应都没有,很难排查),因为侦听的是localhost的ip,也就是127.0.0.1。后来尝试绑定内网ip,才算解决问题:

    # 8081是想找一个不常用的端口,后面的ip是我在内网的ip
    weinre --httpPort 8081 --boundHost 192.168.10.54

    这次无论是本机还是手机都可以正常访问了。然后Mac这里可能还会遇到点小问题。虽然我关闭了防火墙,但是Unix自带的ipfw还在工作,会阻止从外面过来的访问(不知道为啥80没问题),所以要给8081端口专门的许可:

    sudo ipfw add 8081 allow from any to any

    之后就万事大吉了,在HTML里添加 <script src="http://192.168.10.54:8081/target/target-script-min.js#meathill"></script> (域名根据具体情况修改),然后打开 http://192.168.10.54:8081/client/#meathill 就可以查看了,如果有多台终端在调试的话,还可以点选目标机器。

    好了,享受下weinre带来的方便快捷的移动端调试吧。(不自己截图了,借用了weinre官网上的图片)

    weinre使用截图
    weinre使用截图

  • 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能拿出去见人的时候也去分享下吧。

  • 僵尸,快跑

    昨天晚上做了个梦,觉得有点意思,记下来。

    有一天,世界性僵尸病毒爆发了,全球一半以上的人口变成了僵尸。(这里的僵尸是最原始的那种,行动缓慢,吃新鲜的肉,击中头部就会死。)在幸存者的共同努力下,这次危机终于被成功控制住了,而且,人们发现,僵尸并不像想象中的那样完全无法控制:由于感染程度的不同,部分僵尸一定程度上保留着生前的技能,只不过因为极度嗜血而无法自控;在药物控制、食物训练之后,甚至可以从事一些高级工作(有点类似僵尸肖恩)。当然,指望他们完全恢复意识暂时是不可能的

    后来世界各国通过法案达成协议:

    1. 所有变成僵尸的人,认定为死亡,按照一般死亡处理后事
    2. 失踪的人,也按照死亡处理
    3. 因情况特殊,遗体集中销毁,大家都不再追究

    但是,此时世界人口减少一大半,尤其是原本的人口密集地区,劳动力大量短缺,就有人打起了那些“尚能工作”的僵尸的主意。很快,庞大的地下僵尸交易市场就形成了,尤其是生前技能保留的比较完整的,非常值钱。可想而知,即使被感染成了僵尸,音容笑貌,未曾改变,对于活着的人而言,是绝不肯看到自己曾经的亲人沦为货物的;再加上感染的危险存在,所以各国对这种“新奴隶主义”打击也非常重,抓到一定第一时间把僵尸焚毁。原本默许的,大家可以照管亲人变成的僵尸的权力,也被严令禁止了。

    就在这个时候,“我”苏醒了。我被病毒改变的只有死灰一样的外表和动静差别极大的代谢系统,静止的时候,与死尸无异;运动的时候,代谢系统又会快速将能量供给到全身。(就像灵光波动拳那样,发动的时候会重现年轻。)我这次苏醒是因为一个什么原因(忘了),当我发现社会秩序已经基本恢复后,就像赶紧回到家人的身边。

    于是,我需要在僵尸贩子、各国警察的层层抓捕下,东躲西藏;又需要对抗自己对血肉的渴望,对昏睡的渴望;还需要保持运动量,避免以僵尸的面目示人。在刚刚脱离危机自顾不暇个个自危的人中,寻找真正可以依赖可以托付的伙伴,让他们帮我回到家乡。