前言
跟好朋友打赌,我要来个技术文章日更。于是,我跑到到群里喊了句,我要日更。没想到,得到的是大家的支持与鼓励。感谢小伙伴们,逼着我上了梁山......
此文,就是我日更文章的开始,不知道自己能坚持多久。最开始打算将文章标题名定为《围绕node60天》,一个叫老张的网友看了一下,随口说了句短小搓...呵呵呵...短小搓是什么鬼?于是,我给文章起了个高大上的名字,《诺的那些事》......这次够高大上了吧,哈哈哈哈。本来想给浅谈我眼中的express、koa和koa2定为副标题的,可恨简书本着回归写作根本的理念,不存在副标题这个功能,饮恨了。
我是个node程序开发者。此前,我一直认为自己是一个坚定的java信徒,我感觉java才是最终改变世界的语言,甚至我出国旅游去的都是java island......在用了7年Java之后,我遇见了node。遇见node的那一瞬间,我好似看见了蜂腰肥臀的妙龄少女,一群群一片片的在我面前欢歌笑舞,我知道,是时候放弃java了,java就这样渐渐成了往事。不过,我跟java肯定还是藕断丝连的,毕竟hadoop这样的程序还是需要用java来写的,另外,7年的武功是说什么也忘不了了。到现在为止,我用了3年node,见证了node的成长,也见证了自己的成长。我准备好好说说node的那些事,跟大家分享,接受大家的意见和建议。
跟很多人一样,我是以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大神的头像,我都会想起一个词: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的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服务器模块 |
|
Request | 请求 | |
Response | 响应 | |
Router | 路由 |
express用Application、Request、Response、Router四个主要模块,模拟了一个完整的web服务器功能,对了,express还在相当长的一段时期中受到了Connect的影响。在使用express的过程中,你会发现express是一个极简的、灵活的 web 应用开发框架,它提供的这一系列强大的特性,可以帮助你快速创建各种 web 和移动设备应用。
二代英雄: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
上一节已经提到,目前的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。