日志

  • 再访富国岛

    再访富国岛

    2011年,我参加工作的第五个年头,201二进宫的第二个年头,我和老婆去越南富国岛度蜜月。这趟旅程给我带来的远不止美好的回忆:第一次出国,各种新奇的经历,让我禁不住重新思考过去五年在201的种种经历,最终决定立刻离开。

    所以我现在经常建议年轻人出国转转,修正修正三观。

    言归正传。这次国庆本来计划去甲米或者别的什么没去过的敌方,后来订票订晚了,到处都很贵,于是想起来这个性价比极高的地方:上次曾经订下一个5年之约,虽然这还差半年,也算是还愿了。

    越南,富国岛,似乎没有太大的变化,Mango Bay有好吃的食物和开放的房间,Bai Sao依然美丽动人,Le Verenda的服务和环境也仍然让人心旷神怡。不过对于我们而言,最大的变化是这次带了姆二。亲子游最大的变化就是一切日程以孩子为准:早上起来吃早饭,然后找个地方玩一下;然后吃午饭,睡午觉;醒了再找个地方玩一会儿,接着就吃晚饭;然后回去洗澡准备睡觉。第二天重演这个过程。

    原本想躺在沙滩上发呆,或者坐在露台上看书,或者两个人一起做马杀鸡,结果全成了陪小朋友挖沙土或者游泳。而且感觉时间过得飞快,一睁一闭,就回来上班了!有了孩子就是这样,带了他要后悔,不带的话,可能更后悔。

    Anyway,仍然希望孩子慢慢长大,慢慢将我们的世界分享给他。

  • 初尝Gulp

    初尝Gulp

    Gulp自出世起,热度就很高。它利用了Node的Stream机制,速度很快;函数式的写法也很便于阅读。这两大优势让它一跃成为最受欢迎的批处理工具,几乎占据了半壁江山(2015前端工具普查)。

    我之前一直用Grunt,为了不落后于时代,前几天在一个业余项目中试用了一下Gulp,以下是我的感受。

    确实好读

    虽然无法得到证实,不过我相信Grunt的设计中有很多都是借鉴Ant得来的——在没有Grunt之前,我曾经用Ant脚本 + Google Closer Compiler + Google Closer Stylesheets 压缩代码——比如,它的配置是JSON格式的,跟XML非常相像。Grunt的问题也在这里:声明任务需要写JSON配置文件,不同的任务需要写在一起,然后通过registerTask('task', ['task1', 'task2:js', 'task3', ....])来组织。这样前后脱节,确实既不好写,也不好读。个把月回来完全看不懂也不算意外……

    而Gulp的设计则更加自由,它充分利用JavaScript的函数式写法,一个任务就处理一批文件,从上往下看,结构非常清晰:

        gulp.task('js', function () {
          gulp.src(['js/'])
            .pipe(concat())
            .pipe(replace('@version@', version))
            .pipe(uglify())
            .pipe(gulp.dest('dist/js/app.js'));
        });
    

    胜者:Gulp

    速度快,然无卵

    Gulp受推崇的另一大原因就是速度快,一般的任务都是毫秒级。

    不过,这对于我来说意义不大。作为一名人民币玩家,哥的惯用IDE是WebStorm和PHPStorm,均支持文件实时编译和自动刷新等常用功能。所以我只需要批处理工具帮我压缩文件和打包即可。

    这个回合:打平。

    Stream VS Native,异步 VS 同步

    前面提到,Gulp利用了Node的Stream机制,它的内部是处理文档流。这方面Grunt比较保守,仍然是把文件读进内存,然后进行字符串操作。二者的机制不同不好对比,不过作为从页面仔成长起来的前端工程师,不得不承认我对的理解并不深刻,对文件读取和二进制操作也不甚了了,所以在理解二者和开发业务插件的时候,我宁可选择Grunt。

    另外,Gulp的任务是异步的,所以在我的使用场景中——先clean目录,然后各种压缩,最后打包成zip——就很难搞(所以我觉得很多人用Gulp只是为了实时编译预处理文件)。后来用了sequence插件才搞定。

    这方面我觉得Grunt虽然保守,但是更接近前端开发者的知识结构,学习成本更低。

    胜者:Grunt

    杂项

    Gulp的团队应该是有代码洁癖的,比如,他们有一个官方插件仓库,这很正常;但是他们竟然还有一个Gulp插件黑名单……简直匪夷所思……

    所以Gulp团队显然不愿意重复发明轮子,比如简单的clean目录,他们就不愿意做,而且也不让别人做,因为已经有很多工具可以实现这个功能了……但是对于初学者比如我来说,就会在插件引擎中找半天,最后还是Google到相关指引才得到正确的做法。

    这方面Grunt团队做的很好。Grunt插件中有一组是官方提供的,以grunt-contrib为命名空间,基本上,使用这套组件就可以完成我所有的日常工作了。

    胜者:Grunt


    总结

    大趋势上,Gulp的确势头迅猛。但是从实际体验中,我觉得Grunt目前更成熟,也能满足工作所需。

    我并不想说Gulp不好,只是现在的我用起来不太顺手。如果哪天我有时间好好搞搞后端,搞搞Node开发,把Stream概念搞清楚,也许我也会转投Gulp。不过目前,还是继续用Grunt吧。

  • Handlebars 4

    Handlebars 4

    图文……好像有关。

    Handlebars自从升到2.0之后,就放开了,版本号蹭蹭涨,如今已经升到4.0版。

    这个版本最显著的变化在于层级结构,以往的版本中,所有的Block Helper都可以形成一个层级。对于{{#each}}{{#with}}这种还好说,层级关系比较明确,很好理解;但是{{#if}},也有可能形成一个层级,这就很奇怪了。尤其像我前文所述,它只是可能形成一个层级,就更诡异了。

    所以这个修改其实是件好事儿。

    接下来,代码时间。

    Before

        {{#each list}}
          {{#if is_true}}
            <p>这里需要向上两层才能找到值。{{../../value}}注意,这里是两层。</p>
          {{/if}}
        {{/each}}
    
        var data = {
            list: [
              // items..
            ],
            value: 'value'
          }
          , template = Handlebars.compile(hbs);
        template(data);
    
    

    After

        {{#each list}}
          {{#if is_true}}
            <p>这里需要向上两层才能找到值。{{../value}}那,这里只有一层了</p>
          {{/if}}
        {{/each}}
    

    JavaScript是一样的。

    总结

    为了写这篇文章又去看了一遍Handlebars的文档,发现新增了不少特性,尤其开始支持子表达式,看来有必要再好好看一遍。

  • 近期要学习实践的新技术

    近期要学习实践的新技术

    众所周知,程序员是一个必须随时更新知识技能的职业。这其中,前端程序员尤甚。虽然我自称全栈,多半也要依赖前端技术作为小无相功驱使内力,方能打出少林寺七十二绝技写出其它平台的产品。所以,主动学习,更新知识技能是我必须坚持的。

    同时,在前端这个充满变革的领域,我不能把各种新奇的东西都放到实际生产中去。所以,不定期的集中学习就不可避免。

    那么,近期要学习实践的新技术有哪些呢?

    1. Bootstrap 4
    2. gulp
    3. Angular 2 直接从2开始吧,计划做个Todo
    4. React + React Native 要不做鸡血君?
    5. Polymer
    6. Material Design Lite
    7. Phonegap 5 自然是迁移游戏泡泡
    8. Eletron 结合别的技术做个写博客的工具。
    9. socket.io
    10. browserify

    希望能在年底前搞定。

  • 大招募!2016年春节北海道滑雪之旅

    大招募!2016年春节北海道滑雪之旅

    这个人啊,真的跑不了的。有了孩子后,啥都想给他。今年去清迈耍了泼水节,回来后突然特别想带他去滑雪,当然也有吃货俱乐部北海道特辑的原因。加上以前就特别想跟老婆去日本,于是便开始各种YY。

    现在准备把YY落地,同时也向各位兄弟姐妹同学同事发出邀请(是的,我不仅好了伤疤忘了痛而且忘得厉害),明年我们一起去北海道打雪仗吧!

    (更多…)
  • 招募!2016清迈泼水节

    招募!2016清迈泼水节

    今年,我们全家一起去了清迈,并且参加了泼水节,非常好玩。所以我准备明年多组织点人,一起去玩!(是的,我已经从上次比较失败的岘港会安之行中好了伤疤忘了痛了)

    以下是一些照片:

    (直接把照片贴了,懒得写游记了)

    怎么样,很好玩吧!快来加入我吧!!


    接下来是详情。

    泰国 清迈 泼水节

    泰国是旅游圣地,就不多说了。清迈位于泰国北部,是泰北的政治经济文化中心。它历史悠久,兴建于七百多年前,号称兰纳(Lanna)泰王国。以前是泰国的附属国,后来被并入泰国。所以清迈的气质和泰南的大城市比如曼谷相差很大(其实我没去过,这段属于道听途说),清迈更加小清新,甚至我们连人妖都没见到。清迈被华人所熟知的另外一大因素便是邓丽君,据说她最喜欢清迈,经常来此疗养,最后不幸发生时也香消于此。至今梅坪酒店仍然保留着那个套房,供后人瞻仰。

    泼水节当地人称宋干节,起源于沐浴礼佛,尤其是用净水清洗佛像,后来演变成水枪大战。据我目测有以下玩法:

    1. 塔佩门广场是最大的一处战场,乱战
    2. 沿护城河一圈,大家各自守着各自的门口,对战路上的行人和车辆
    3. 当地人或外地人可以租皮卡,绕着护城河的道路转,与守路的人对战
    4. 城里的小路上也有很多人(以小孩子居多)各自为战
    5. 商家自然不会错过这个机会,会在各种路边搭起各种站台,请妹子在台上跳舞,和台下泼水互动

    参与的有当地人,有外国人,年女老少都有,随便泼,有些人很坏,用冰水,超刺激。一般来说,穿着花枝招展的美女,与扎眼的各种人,都是吸引炮火的对象。

    其它娱乐项目

    除了泼水和窝在酒店里,清迈作为著名旅游城市,其它娱乐项目也很多。比如照片中我们参加的大象营。还有各种丛林探险、速降、自行车越野等。对于女生来说,马杀鸡和SPA必不可少,这边各种档位的都有。有兴趣的还可以报名厨艺班,学习一下当地菜肴的做法。

    作为泰国丽江,就算再咖啡馆泡着也是可以的嘛!

    开销

    清迈的消费还是蛮低的,普通小店一顿饭10~15块就能搞定,大一点的店也就30、50块。当然,大餐也有,我们为给老妈过生日,定的Le Crystal,车接车送,大概一个人不到400吧。

    清迈水果丰富,到处都是冷饮店咖啡馆,一杯smoothie 10+,果汁3、5块。

    泼水节期间晚上都有夜市,基本上都是2、3块、5、6块,二十块可以吃很多东西了,还有30块/小时的大街马杀鸡。

    唯一贵的可能就是酒店。泼水节期间,古城(水战中心)的酒店都会涨价,平时200随便住,这时候大概要400~600。我们住的Rich Lanna,标间一晚(含早餐)要将近600,不过地段相当好。

    总之,做好“穷家富路上”准备的我们,无时无刻不觉得,好便宜啊……

    其它注意事项

    咳咳,丑话放到最后说。

    我觉得很好玩,所以力邀各位朋友一起来玩。但是我不是专业搞旅游的,而且我也属于生活半不能自理+选择性障碍+懒的人,所以我觉得我还是不带队好。如果大家一起来玩,我能提供的:

    1. 资讯,包括日程,购物等
    2. 机票酒店推荐
    3. 提醒大家必需物品

    我不能提供的:

    1. 管吃管喝管玩
    2. 订机票酒店

    我希望的,就是大家商量好,来年一块儿去,到清迈碰头一起玩,想一起玩就一起玩,想自己SPA就自己SPA,各自分享好吃的好玩的好看的,就OK了。

    报名啊报名

    1. 小囧表示了兴趣,所以蛋东也可能回来。如果大家都是201的,团名就叫“中X村不在线”好了,哦也
  • Mac OS上各种开发环境的配置(未完成)

    Mac OS上各种开发环境的配置(未完成)

    很多开发人员喜欢 Mac OS,因为它基于FreeBSD,有原生的命令行工具,配置各种开发环境都很方便。我个人偏爱Windows,我觉得各种可视化用起来更爽。

    这篇文章介绍如何在Mac OS 10.10 Yosemite上配置各种开发环境,范围以前端所需为主。

    Xcode

    无论你是否准备开发iOS或者Mac OS上的应用,Xcode最新版都是必须的。因为里面包含了系统必须的命令行工具和编译工具,没有它们支持,我们就无法安装后面那些东西。

    好在Xcode是免费的,虽然体积巨大(而且早年不支持断点续传,我第一遍Xcode装了半个月),但只要你有恒心有毅力,智商正常,就都能装上。

    方法:

    1. 打开App Store
    2. 搜索Xcode
    3. 安装
    4. 安装完成后,在命令行里运行xcode-select -p,如果显示/Applications/Xcode.app/Contents/Developer则表明Xcode 命令行工具已经安装成功,否则的话,执行xcode-select --install安装

    homebrew

    工欲善其事,必先利其器。在*nix环境下装东西,一个好的包管理工具是必须的。我使用的是Homebrew,它的安装非常简单:

    ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
    

    因为Mac OS默认包含并启动了Ruby环境,所以此时系统就开始执行homebrew的安装。关于brew的用法,可以看它的官网,或者运行brew help阅读帮助。

    nodejs

    有了homebrew,一切都好办了。

    brew install nodejs
    node -v
    

    不过我更喜欢标准版,在nodejs的官网下载dmg安装包,直接安装即可。

    ruby

    Mac OS默认包含ruby,不过我喜欢最新版,所以还是拜托给homebrew好了。

    brew install ruby
    

    homebrew会把包安装在/usr/local/Cellar目录下,然后通过软链接链接到/usr/local。有些系统默认包含的组件已经注册在/usr/local或者/usr/bin,创建链接可能会失败。不用担心,homebrew提供了详细的帮助信息,仔细阅读,按照提示一步一步做,就OK了。

    Python & httpie

    python我用的很少,耳闻3和2差别巨大,也就不敢追新。所以我用系统自带的Python,只是安装了 httpie 这个工具来调试远程数据。安装一样是通过homebrew:

    brew install httpie
    http httpie.org
    

    apache httpd

    Apache httpd是Mac OS的默认组件,直接运行sudo apachectl start即可开启apache服务器,然后访问 http://localhost/ 就能看到默认页面了。

    系统默认的文档地址为 /Library/WebServer/Documents,把静态的页面放进去,就能正常浏览,用来测试还是蛮方便的。不过我觉得不算太实用。因为一来埋得很深,通过Finder无法直接到达;二来作为公司配的开发机,一般来说我不建议在公共目录里做东西。

    不过配置起来也不复杂。打开 /etc/apache2/extra/httpd-vhost.conf,里面应该有两个没有真正起作用的范例配置。基本上按照那里即可。

    先放出去,回头再补充好了……

  • 漏装php55w-mbstring导致中文邮件乱码

    漏装php55w-mbstring导致中文邮件乱码

    朋友的WordPress发中文邮件总是乱码,喊我帮忙看看。很奇怪,后台、文章里的中文都能正常显示,看起来一切正常;我在我电脑上搭了一套,同样的代码,发邮件也没问题。

    后来打开phpmailer的debug模式,发现什么都对,就是中文内容都是问号。

    继续往上找到发邮件的函数,运气不错插件留了filter,遂修改模板,add_filter强制转换内容。

    转换前先检查,不是UTF-8再转:

        if (!mb_detect_encoding($content)) {
          $content = iconv('ASCII', 'UTF-8', $content);
        }
        return $content;
    

    结果代码传上去报错,说没有mb_detect_encoding,然后想起来yum安装php时确实默认不包含很多扩展,于是手动安装yum install php55w-mbstring。然后重启apache,发邮件测试,正常了。

    我又想是不是缺少多字节文本模块(Multibyte String)导致原先无法发送中文呢?去掉filter,仍然正常,确实如此。

    总结

    yum安装新版php需要增加源,而新版的源默认不包含很多常用的库,使用的时候最好都装上。WordPress的翻译机制面对多字节文本时,编码不对不会报错,也需要小心。

  • 使用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()