三英战豪强,思绪走四方。浅谈我眼中的express、koa和koa2

前言

跟好朋友打赌,我要来个技术文章日更。于是,我跑到到群里喊了句,我要日更。没想到,得到的是大家的支持与鼓励。感谢小伙伴们,逼着我上了梁山......
此文,就是我日更文章的开始,不知道自己能坚持多久。最开始打算将文章标题名定为《围绕node60天》,一个叫老张的网友看了一下,随口说了句短小搓...呵呵呵...短小搓是什么鬼?于是,我给文章起了个高大上的名字,《诺的那些事》......这次够高大上了吧,哈哈哈哈。本来想给浅谈我眼中的express、koa和koa2定为副标题的,可恨简书本着回归写作根本的理念,不存在副标题这个功能,饮恨了。

我是个node程序开发者。此前,我一直认为自己是一个坚定的java信徒,我感觉java才是最终改变世界的语言,甚至我出国旅游去的都是java island......在用了7年Java之后,我遇见了node。遇见node的那一瞬间,我好似看见了蜂腰肥臀的妙龄少女,一群群一片片的在我面前欢歌笑舞,我知道,是时候放弃java了,java就这样渐渐成了往事。不过,我跟java肯定还是藕断丝连的,毕竟hadoop这样的程序还是需要用java来写的,另外,7年的武功是说什么也忘不了了。到现在为止,我用了3年node,见证了node的成长,也见证了自己的成长。我准备好好说说node的那些事,跟大家分享,接受大家的意见和建议。

Nodejs-logo

跟很多人一样,我是以express开启自己的node之旅的。express是什么呢?它是一个封装了Connect的、并提供web服务的中间件,是开发web程序的利器。express是由TJ大神开发的,之后,TJ大神又开发了koa这款神器。但是,因为,es7的飞速发展,koa又迅速衍化出了koa2这个版本,时至今日,koa的github已经全面更换为了koa2版本的代码,当然,这一切,还真就是在我眼前发生的,作为历史的见证者,我要谈谈我对这几个框架的看法。

说个题外话,我的文章,不打算走传统技术文章的套路,我讲就讲一些不一样的,讲一些平常你看不到的东西,希望各位技术大牛多多给小弟指点,在下这厢有礼了。

酷事

单田芳老爷子,有几部超级好听的长篇评述,叫三侠五义,其中,有个重要的人物,白眉大侠徐良(刀是什么样的刀,金丝大环刀,剑是什么样的剑,闭月羞光剑......),白眉大侠徐良是一个早产儿,从小体弱多病,但是最终凭借着刻苦努力,成为了一代武术宗师。node的经历跟白眉大侠很像,node也是个早产儿。早期的node问题多多,那个时候,在win上安装node简直就是一件灾难。不过,随着时间的推移,node变得越来越好用,node的好基友npm也变得越来越庞大。我从node4.0开始入门,后来经历了node6.0版本的各种无奈和妥协,紧接着就迎来了node7.6版本的绝地反击,直到现在,感受到了node7.9的大彻大悟。

记得当初和一个技术猿聊天,他跟我抱怨,node里全是大坑。我一听,这位仁兄用的是express吧,这位仁兄告诉我,没有,他们自己实现了个轮子,跟express差不多,现在正在考虑着升级的事宜呢。呵呵呵了,造了个轮子,还跟express差不多。不过,这也从另外一个侧面看出了node当年不可回避的几个问题:

1.node早期就是个极客玩具,高手众多,产品野蛮生长。
2.使用node开发商业软件是一件正在发生的事情。(对了,阿里、腾讯、非死不可、领英这些大佬也都用node重构了一些适合的模块。)
3.那个时候,使用node的同志们不是傻,就是用鸡汤晃点了他们老板。

回调大坑

不过这个仁兄倒是也说出了node的一些痛点,其中,node存在的技术大坑可能大家都遇到过,这些坑是因为node的语言特性导致的。例如,回调大坑,就是这些坑中,最深的一个。

下边是一个回调大坑的代码示例,我们来感受一下:

module.exports = function (param, cb) {
  asyncFun1(param, function (er, data) {
    if (er) return cb(er);
    asyncFun2(data,function (er,data) {
      if (er) return cb(er);
      asyncFun3(data, function (er, data) {
        if (er) return cb(er);
        cb(data);
      })
    })
  })
}

node本身是异步回调的,通过高阶函数,偏函数实现回调函数。但是,回调函数嵌套过多,会使代码不可阅读、不可描述,那位仁兄说的大坑便是callback hell,翻译过来就是回调大坑,或者说是Pyramid of Doom,邪恶金字塔。(我朴素的认为这位仁兄只遇到了这一个大坑.....呵呵呵)

回调大坑怎么解决呢?es5可以利用一下第三方库,例如async库,或者单纯使用connect中间件提供的next功能来处理,还可以利用promise来处理回调大坑。当然,单纯使用promise可能给自己带来另外一个大坑,then大坑,或者叫pipe大坑,无数个then,想想也是够恐怖的。另外,还可以使用node自带的事件模块来处理回调问题,利用事件代理(我记得是backbone的一个模块)来简化代码书写。关于事件模块,我之后会写个小专题,来说说node的事件原理。不过,虽然提到了事件模块,但是,我不推荐用事件去处理回调嵌套,因为,需要写更多的代码,得不偿失。

这里说个题外话,在朴大人(朴灵)的一篇文章中,提到了wind库和step库,此处就不进行介绍了,因为,es6和es7会给我们更加好的使用体验。

es5讲完了,各种基于es5来处理回调的方法也讲完了。其实,我在使用这些方法来简化回调嵌套的时候,总感觉是脱了裤子放屁,讲真,有的时候,不仅没有简化代码,还会造成其他的代码阅读障碍,增加团队的学习成本。归根结底,我们的代码是写给人看的,机器只是顺便执行一下而已。

ES的官方组织,肯定认识到了这一点,于是,基于协程原理的规范也呼之欲出,终于在ES6中为大家带来了Generator函数。

Coroutine,协程,简单来说就是由用户通过特定的程序语言控制CPU切换和挂起进程(Process)或者线程(Thread),用同步的方式来模拟异步程序,我之后会单独讲一下进程、线程、协程,在此不做过多展开。既然提供了协程的功能,那么我们处理起回调也就迎刃而解了。

Generator函数和yield语句是一对好基友,如果没有yield语句的话,Generator函数只不过是暂缓执行的状态机而已。通过配合yield,Generator 函数就可以暂停执行和恢复执行,从而将其内部封装的异步函数变为同步执行。下面我们看看例子来感受一下:

function* gen(x){
  var y = yield x + 2;
  return y;
}

var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }

当然Generator函数还有些滥用之嫌,具体为什么,我会在后续的文章中做出解释。反正,ES官方组织他们对于Generator函数是不满意的。于是,ES官方组织马不停蹄,终于在ES7规范中,捣鼓出来了async/await这个目前为止,异步回调最佳的解决方案。

本质上讲,async/await规范是Generator函数+yield语句的语法糖。返回部分都是一个Promise对象。async/await规范比Generator函数+yield语句要更加好用,下面我们看看例子来感受一下:

var sleep = function (time) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve();
        }, time);
    })
};

var go= async function () {
    console.log('start');
    await sleep(3000);
    console.log('end');
};

go();

至此,回调大坑的问题算是得到了很好的解决,我们接下来,就说是express、koa和koa2这三个框架。

TJ大神的三大英雄

TJ大神

每每看到TJ大神的头像,我都会想起一个词:preconception。偏见,先入之见。许多人,对于程序员是存在很大的偏见的。大多数人的脑海中给程序员的定义都是这个样子:

图片来源于网络,加班的程序员们

一般人认为程序员:不够健康,不够整洁,不够潮流,人傻,钱多......TJ大神,这个顶级程序员,则给我们上了非常生动的一课。TJ是设计师出身,半路出家做了程序员,他一个人完成了express、koa、koa2设计和核心开发。TJ曾经说过,他之所以能做出这些NB的软件,是因为,他热爱阅读其他大牛的源码,他会把自己不明白的问题都弄懂。他在第一时间遇见了问题,处理了问题,保证自己深刻理解各种软件的核心原理和运行机制。于是,TJ就变成了一个npm包贡献极多的node大神,他的光辉和事迹最终会变为传说,再node圈里永久的流传下去......

三大英雄

node的早期,是荒芜的年代,正如之前我的那个哥们一样,那个时候,没有轮子。程序员自己制造了各种各样的轮子,真可谓是八仙过海,各显神通。那个时候node程序员一般这样开始写web应用:

var http = require('http');

http.createServer(function (request, response) {
    response.writeHead(200, {'Content-Type': 'text/plain'});
    response.end('Hello World\n');
}).listen(8888);

console.log('Server running at http://127.0.0.1:8888/');

终于,TJ打造了express、koa、koa2三大英雄,node的浪漫主义年代逐渐揭开了序幕。我们看下边的表:

英雄 说明 对应 经典
express web框架 es5 回调嵌套
koa web框架 es6 Generator函数+yield语句+Promise
koa2 web框架 es7 async/await+Promise

下面我就开始说一下这三个框架和他们之间千丝万缕的联系

初代英雄:express

初代英雄:express

express的入门非常简单,通过创建express的Application就构建了一个expressweb实例。下面我们看看例子来感受一下:

var express = require('express');
var app = express();

app.get('/', function (req, res) {
  res.send('Hello World!');
});

var server = app.listen(3000, function () {
  var host = server.address().address;
  var port = server.address().port;

  console.log('Example app listening at http://%s:%s', host, port);
});

express本身封装了路由模块,因此,可以利用express直接处理各种http路由请求。

在express用四个主要模块:

模块 说明 解释
Application web服务器模块
Application
抽象了web服务器的主要贡呢和接口,如监听、事件、加载中间件、get\post请求等
Request 请求
Request
Response 响应
Response
Router 路由
Router

express用Application、Request、Response、Router四个主要模块,模拟了一个完整的web服务器功能,对了,express还在相当长的一段时期中受到了Connect的影响。在使用express的过程中,你会发现express是一个极简的、灵活的 web 应用开发框架,它提供的这一系列强大的特性,可以帮助你快速创建各种 web 和移动设备应用。

二代英雄:koa

二代英雄:koa

目前的koa官方github已经全面的使用koa2版本的代码了,换句话说,koa和koa2现在只是版本上的区别了,koa是老版本,koa2用新的版本号。因此,koa1,我们需要查看老的代码版本。

koa 是由 express原班人马打造的(TJ),致力于构建更小、更富有表现力、更健壮的 web 框架。使用 koa 编写 web 应用,通过组合不同的 generator,可以免除重复繁琐的回调函数嵌套,并极大地提升错误处理的效率。koa 不在内核方法中绑定任何中间件,它仅仅提供了一个轻量优雅的函数库,使得编写 koa 应用变得得心应手。

Koa 包含了像 content-negotiation(内容协商)、cache freshness(缓存刷新)、proxy support(代理支持)和 redirection(重定向)等常用任务方法。 与提供庞大的函数支持不同,Koa只包含很小的一部分,因为Koa并不绑定任何中间件。

koa中也包含4个主要模块,Application、Request、Response、Context。此时,router已经被排除在内核之外了。其实,koa只是一个“中间架”,几乎所有的功能都需要由第三方中间件来协同完成。例如koa的router模块,就有20多个,优胜劣汰,自由选择......虽然有不规范之嫌,但是,koa是规范的这就足够了。使用koa,可以最大限度的发挥自己的想象力,利用koa,构建各种个性化的web与移动应用。下面我们看看例子来感受一下:

var koa = require('koa');
var app = koa();

app.use(function *(){
  this.body = 'Hello World';
});

app.listen(3000);

没错,就是这么简单,使用了Generator函数,这也是koa和express最大的不同,express是回调函数,koa是用Generator来作为响应器的。

另外,那个替代了router的context是怎样的呢?下面我们看看例子来感受一下:

app.use(function *(){
  this; // is the Context
  this.request; // is a koa Request
  this.response; // is a koa Response
});

另外,koa中还有co这个工具。co是一个“皮”,通过co来包装Generator和yeild,下面我们看看例子来感受一下:

var co = require('co');
 
co(function *(){
  // yield any promise 
  var result = yield Promise.resolve(true);
}).catch(onerror);
 
co(function *(){
  // resolve multiple promises in parallel 
  var a = Promise.resolve(1);
  var b = Promise.resolve(2);
  var c = Promise.resolve(3);
  var res = yield [a, b, c];
  console.log(res);
  // => [1, 2, 3] 
}).catch(onerror);
 
// errors can be try/catched 
co(function *(){
  try {
    yield Promise.reject(new Error('boom'));
  } catch (err) {
    console.error(err.message); // "boom" 
 }
}).catch(onerror);
 
function onerror(err) {
  // log any uncaught errors 
  // co will not throw any errors you do not handle!!! 
  // HANDLE ALL YOUR ERRORS!!! 
  console.error(err.stack);
}

虽然,前边说了很多express、koa的相关知识,但是,这两个都不重要了,随着koa2的扶正和node 7.6的发布,基于nodejs的程序开发,开启了新的篇章。

三代英雄:koa2

三代英雄:koa2

上一节已经提到,目前的koa官方github已经全面的使用koa2版本的代码了,并且有一句非常重要的提示Koa requires node v7.6.0 or higher for ES2015 and async function support.。意思是说,koa需要至少node v7.6.0版本和ES2015(es6+async)才能使用。这个提示,也是非常重要的一句话,从这个版本开始,我们可以抛弃Bable(当然,nodev7.6还是不能完全抛弃babel,因为到目前为止,node都还没有实现对import和export的支持,感谢深蓝wbe的提醒),快乐的使用async等新的语法了。(Babel 自带了一组 ES2015 语法转化器。这些转化器能让你现在就使用最新的 JavaScript 语法,而不用等待浏览器和node提供支持。)

目前,koa2结合了async/await已经成为了最好的web开发框架。上一节,已经讲了koa的主要模块和实现原理,此处,我只是简单说说koa2和koa不同之处,下面我们看看例子来感受一下:

const Koa = require('koa');
const app = new Koa();

app.use(ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

函数式编程,async/await功能,程序简单,好用,真可谓是居家旅行的不二之选呀。通过查看代码,koa2去除了co中间件,进一步的精简了内核,这一点也正好符合当下性冷淡风格的设计潮流......不禁想赞叹一句,TJ不愧是设计师出身呀......

小结

其实,很多人不关心框架的原理,他们关心的是如何从express升级到Koa,如何从koa升级到koa2。虽然koa
对于向前兼容的并不好,但是,我在这里想提出另外一种思路,就是利用express+async/await的形式来做升级。一方面,可以在不改变原有程序的基础上使用最新的语法特性,另一方面,可以用最小的代价获取最大的效益。下面我们看看例子来感受一下:

var PageQuery = async (page, pageSize, Model, populate, queryParams, sortParams) => {
    var start = (page - 1) * pageSize;
    var $page = {
        pageNumber: page
    };

    let TotleRow = await ModelCount(Model, queryParams);
    let records = await PageRecords(Model, queryParams, start, pageSize, populate, sortParams);

    $page.TotleRow = TotleRow;//(count - 1) / pageSize + 1;
    $page.PageCount=parseInt(TotleRow/pageSize)+1;
    $page.results = records;
    return $page;
};

var ModelCount = (Model, queryParams) => {
    return Model.count(queryParams).exec().then((count) => {
        return count;
    }).catch((err) => {
        console.log("err:" + err);
        return err;
    });
}

var PageRecords = (Model, queryParams, start, pageSize, populate, sortParams) => {
    return Model
        .find(queryParams)
        .skip(start)
        .limit(pageSize)
        .populate(populate)
        .sort(sortParams)
        .exec()
        .then((doc) => {
            return doc;
        }).catch((err) => {
            console.log("err:" + err);
            return err;
        });
}

//......
Customer.PageQuery(pageNum, pageSize, Customer.Model, "", {}, {}).then((pageResult) => {

        res.render('customersList', {
            layout: "admin",
            customersList: pageResult.results,
            totalPages: pageResult.PageCount,
            pno: pageNum,
        });
    }).catch((err) => {
        console.log("err:" + err);
        res.send("err");
    });

上边是我写的一个中间件,可以通过这种简单的方式,来处理express与es7的升级问题。(完整代码可以到我的github上去查看https://github.com/lxlhum/meet_quick ,大家帮我把星星点起来哈)

尾声

流行的web技术统计:node、ruby、python、php、java

现在,node的社区非常活跃,产品换代升级非常迅速和及时。使用node,既要求我们用扎实的功底,又要求我们与时俱进,不断学习。毕竟,不管用什么语言,在程序开发的道路上,只有不断学习,才能不断前进。

这部分关于node的基础知识,可以看我写的一个笔记:深入浅出NodeJS的读书笔记,保证您读过以后,会有豁然开朗之感。笔记中,记录了tcp/ip,http,socket/websocket的相关知识,全方位的介绍nodejs。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 框架提出的背景 ES6/7带来的变革 自ES6确定和ES7中async/await开始普及,Node的发展变得更加...
    宫若石阅读 8,477评论 1 14
  • 异步编程对JavaScript语言太重要。Javascript语言的执行环境是“单线程”的,如果没有异步编程,根本...
    呼呼哥阅读 7,296评论 5 22
  • 原文链接:http://www.jianshu.com/p/6b816c609669 前传 出于兴趣最近开始研究k...
    悬笔e绝阅读 7,197评论 1 11
  • 《我的前半生》终于收尾,而我中途就弃剧不看了。 前几集里,面对嫁的不好的小女儿,薛甄珠阿姨时时刻刻敲打责骂自己的孩...
    爱旅游的赵老师阅读 652评论 2 5
  • 从来没有过的如此精致的夜色 你会看见破碎的风,极有规律地排列事物 它们形象突出而逼真,此起彼伏的交错 每一块都带有...
    Prisio阅读 369评论 1 9