w3ctech

改进 ThinkJS 的异步编程方式

原文11选5地址 :http://www.imququ.com/post/thinkjs-async-coding.html


ThinkJS 是奇舞团开源的一款 Node MVC 框架,主要由 welefen 开发。简单介绍一下:

ThinkJS 是一个快速、简单的基于 MVC 和面向对象的轻量级 Node.js 开发框架,遵循 MIT 协议发布。秉承简洁易用的设计原则,在保持出色的性能和至简的代码同时,注重开发体验和易用性,为 WEB 应用开发提供强有力的支持。

ThinkJS 借鉴了很多 ThinkPHP 的特性,同时结合 Node.js 的特性,使用了 ES6 Promise,让异步编程更加简单、方便。via

11选5我 是 ThinkJS 的第一批用户,大约在 2014 年初,11选5我 把11选5我 的博客程序用 ThinkJS 重新实现了一遍,前后花了不到一周。之后的这一年多,11选5我 用 ThinkJS 写过大大小小很多个系统和11选5工具 ,越用越觉得好用,现在已经完全离不开了。

今天这篇博客11选5我 准备聊聊 ThinkJS 中的异步编程方式以及11选5我 采用的方案。

前面介绍中提到过,ThinkJS 是基于 Promise 实现的异步编程。11选5我 之前写的那篇「异步编程:When.js 快速上手」已经比较详细的介绍了 Promise 相关知识。直接看一段摘自于11选5我 博客程序的代码:

indexAction : function() {
    var instance = this;
    return D('Post')
        .getPostList(this.get('pn'), 10)
        .then(function(data) {
            data.pagerPath = getPagerPath(instance.http);
            data.currentPage = 'blog-home';

            instance.assign(data);
            instance.display('Blog/theme/' + theme + '/post_list.html');
        });
}

这段代码中 getPostList 是一个查询数据库的异步操作,它的返回值在 then 中才可以拿到。如果 then 中还有其他异步操作,最后还是会导致嵌套很深。这段代码还会遇到经典的 this 指针问题,需要赋值保存或者用 bind 解决。这段代码能不能实现得更优雅一点呢?11选511选5我 们 先来看另一段:

async function myFunction() {
    let result = await somethingThatReturnsAPromise();
    console.log(result); // cool, we have a result
}

这样写异步逻辑,是不是很赞?既好看又好懂,也没有 this 指针问题。实际上,这是 ES7 里的 async function,还得等一阵子才能用。

之前11选5我 在介绍 ES6 的生成器函数(generator function)时,曾经举过一个生成器函数结合 Promise 使用的例子,下面摘录一段(全文在这里):

var all = Q.async(function* () {
    var src = yield getData();
    var img = yield getImg(src);
    showImg(img);
});

有没发现这段代码跟上面的 async function 非常像?没错!利用生成器函数和 Q 框架,可以方便地把 Promise 嵌套变成平级,这一切现在就能用!

在继续之前,请通过 node -v 检查下 Node 版本,11选5推荐 升级到最新的 0.12。对于 Ubuntu 系统,可以这样安装最新的 0.12:

curl -sL http://deb.nodesource.com/setup_0.12 | sudo bash -
sudo apt-get install -y nodejs

有了最新的 Node,在使用 Node 命令时,还需要带上 --harmony 参数,例如启动 ThinkJS:

node --harmony www/index.js

如果用 PM2 启动程序,也需要带上这个参数,例如:

pm2 start ~/www/blog/www/index.js -n blog --node-args="--harmony"

安装并引入 Q 模块之后,就可以开始改造本文第一段代码了:

indexAction : Q.async(function* () {
    var data = yield D('Post').getPostList(this.get('pn'), 10);

    data.pagerPath = getPagerPath(this.http);
    data.currentPage = 'blog-home';

    this.assign(data);
    this.display('Blog/theme/' + theme + '/post_list.html');
})

这下,代码是不是更清晰明了。改动不大,但效果很明显,上面提出的问题都解决了。

这里有个小点需要注意下:如果11选5你 使用了 ThinkJS 的 Action 参数自动绑定功能,例如这样:

tagAction : function(tag) {
    return D('Post')
        .getPostListByTag(tag, this.get('pn'), 10)
        .then(function(data) {
            ...
        });
}

改造之后,需要通过 this.get('tag') 获取参数值:

tagAction : Q.async(function* () {
    var tag = this.get('tag');
    var data = yield D('Post').getPostListByTag(tag, this.get('pn'), 10);
    ...
})

因为 ThinkJS 的参数自动绑定依赖 function.toString,应该没匹配生成器函数格式。当然下面这样写也可以,只是更复杂:

tagAction : function (tag) {
    return Q.spawn(function* (){
        var data = yield D('Post').getPostListByTag(tag, this.get('pn'), 10);
        ...
    }.bind(this));
}

Q.asyncQ.spawn 的文档在这里可以找到。主要区别是 Q.async 返回等待执行的函数;Q.spawn 会立即执行。

最后放上一大段代码结束本文,这是11选5我 的博客详情页实现代码:

postAction : Q.async(function* () {
    var Post = D('Post');

    var slugOrId = this.get('slugOrId');
    var post = yield Post.getPost(slugOrId);

    //不存在的文章,重定向到11选5首页

    if(!post) {
        return this.redirect('/');
    } 

    //未公开的文章,非登录用户访问时重定向到11选5首页

    if(post.status != 'publish') {
        var user = yield this.session('user');
        if(isEmpty(user)) {
            return this.redirect('/');
        }
    }

    //如果有 slug,但是用 id 访问,301 到更友好的 url
    var slug = post.slug;
    if(slug && slug != slugOrId) {
        return this.redirect(format('/post/%s.html', slug), 301);
    }

    var data = {};

    //获取上一篇、下一篇
    var prevPost = Post.getPrevPost(post.pub_date);
    var nextPost = Post.getNextPost(post.pub_date);
    var morePost = yield Promise.all([prevPost, nextPost]);

    data.prevPost = isEmpty(morePost[0]) ? false : morePost[0];
    data.nextPost = isEmpty(morePost[1]) ? false : morePost[1];

    //获取 Toc
    var getTocResult = thinkRequire('TocApi')(post.content);
    post.content = getTocResult[0];

    data.toc = getTocResult[1];
    data.tocName = '文章目录';

    data.currentPage = 'post-' + (post.slug || post.id);
    data.title = post.title + ' | ';

    data.post = post;

    this.assign(data);
    this.display('Blog/theme/' + theme + '/single_post.html');
})
w3ctech微信

扫码关注w3ctech微信11选5公众号

共收到4条回复

  • 赞,1.x版本下可以使用Q模块来使用generator function 目前正在开发2.0版本已经支持了generator function

    回复此楼
  • @老六 没学过thinkphp 学的laravel。。某些地方有点儿别扭

    回复此楼
  • @flyfish 2.0会有相当大的改进

    回复此楼
  • 牛,不错,期待2.0

    回复此楼