用Node.js写一个web服务器,我前面已经写过两篇文章了:
第一篇是不使用任何框架也能搭建一个web服务器,主要是熟悉Node.js原生API的使用:使用Node.js原生API写一个web服务器
第二篇文章是看了Express的基本用法,更主要的是看了下他的源码:手写Express.js源码
Express的源码还是比较复杂的,自带了路由处理和静态资源支持等等功能,功能比较全面。与之相比,本文要讲的Koa就简洁多了,Koa虽然是Express的原班人马写的,但是设计思路却不一样。Express更多是偏向All in one的思想,各种功能都集成在一起,而Koa本身的库只有一个中间件内核,其他像路由处理和静态资源这些功能都没有,全部需要引入第三方中间件库才能实现。下面这张图可以直观的看到Express和koa在功能上的区别,此图来自于官方文档:
基于Koa的这种架构,我计划会分几篇文章来写,全部都是源码解析:
Koa的核心架构会写一篇文章,也就是本文。
对于一个web服务器来说,路由是必不可少的,所以@koa/router会写一篇文章。
另外可能会写一些常用中间件,静态文件支持或者bodyparser等等,具体还没定,可能会有一篇或多篇文章。
本文可运行迷你版Koa代码已经上传GitHub,拿下来,一边玩代码一边看文章效果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/KoaCore
简单示例我写源码解析,一般都遵循一个简单的套路:先引入库,写一个简单的例子,然后自己手写源码来替代这个库,并让我们的例子顺利运行。本文也是遵循这个套路,由于Koa的核心库只有中间件,所以我们写出的例子也比较简单,也只有中间件。
Hello World第一个例子是Hello World,随便请求一个路径都返回Hello World。
const Koa = require("koa"); const app = new Koa(); app.use((ctx) => { ctx.body = "Hello World"; }); const port = 3001; app.listen(port, () => { console.log(`Server is running on :${port}/`); }); logger然后再来一个logger吧,就是记录下处理当前请求花了多长时间:
app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); });注意这个中间件应该放到Hello World的前面。
从上面两个例子的代码来看,Koa跟Express有几个明显的区别:
ctx替代了req和res
可以使用JS的新API了,比如async和await
手写源码手写源码前我们看看用到了哪些API,这些就是我们手写的目标:
new Koa():首先肯定是Koa这个类了,因为他使用new进行实例化,所以我们认为他是一个类。
app.use:app是Koa的一个实例,app.use看起来是一个添加中间件的实例方法。
app.listen:启动服务器的实例方法
ctx:这个是Koa的上下文,看起来替代了以前的req和res
async和await:支持新的语法,而且能使用await next(),说明next()返回的很可能是一个promise。
本文的手写源码全部参照官方源码写成,文件名和函数名尽量保持一致,写到具体的方法时我也会贴上官方源码地址。Koa这个库代码并不多,主要都在这个文件夹里面:https://github.com/koajs/koa/tree/master/lib,下面我们开始吧。
Koa类从Koa项目的package.json里面的main这行代码可以看出,整个应用的入口是lib/application.js这个文件:
"main": "lib/application.js",lib/application.js这个文件就是我们经常用的Koa类,虽然我们经常叫他Koa类,但是在源码里面这个类叫做Application。我们先来写一下这个类的壳吧:
// application.js const Emitter = require("events"); // module.exports 直接导出Application类 module.exports = class Application extends Emitter { // 构造函数先运行下父类的构造函数 // 再进行一些初始化工作 constructor() { super(); // middleware实例属性初始化为一个空数组,用来存储后续可能的中间件 this.middleware = []; } };这段代码我们可以看出,Koa直接使用class关键字来申明类了,看过我之前Express源码解析的朋友可能还有印象,Express源码里面还是使用的老的prototype来实现面向对象的。所以Koa项目介绍里面的Expressive middleware for node.js using ES2017 async functions并不是一句虚言,它不仅支持ES2017新的API,而且在自己的源码里面里面也是用的新API。我想这也是Koa要求运行环境必须是node v7.6.0 or higher的原因吧。所以到这里我们其实已经可以看出Koa和Express的一个重大区别了,那就是:Express使用老的API,兼容性更强,可以在老的Node.js版本上运行;Koa因为使用了新API,只能在v7.6.0或者更高版本上运行了。