关于koa2,你不知道的事

1,600 阅读7分钟

引言

什么是 koa

koa 是一个基于 node 实现的一个新的 web 框架,它是由 express 框架的原班人马打造。特点是优雅、简洁、表达力强、自由度高。和 express 相比,它是一个更轻量的 node 框架,因为它所有的功能都通过插件来实现,这种插拔式的架构设计模式,很符合 unix 哲学。

本文从零开始,循序渐进的展示和详解上手 koa2 框架的几个最重要的概念,最后会串联讲解一下 koa2 的处理流程以及源码结构。看完本文以后,相信无论对于上手 koa2 还是深入了解 koa2 都会有不小的帮助。

快速开始

安装并启动(hello world)

按照正常逻辑,安装使用这种一般都会去官网看一下类似guide的入门指引,殊不知 koa 官网和 koa 本身一样简洁(手动狗头)。

如果一步步搭建环境的话可能会比较麻烦,还好有项目生成器koa-generator(出自狼叔-桑世龙)。

// 安装koa项目生成器koa-generator
$ npm i koa-generator -g

// 使用koa-generator生成koa2项目
$ koa2 hello_koa2

// 切到指定项目目录,并安装依赖
$ cd hello_koa2
$ npm install

// 启动项目
$ npm start

项目启动后,默认端口号是3000,在浏览器中运行可以得到下图的效果说明运行成功。

koa2 简析结构

项目已经启动起来了,下面让我们来简单看一下源码文件目录结构吧:

这个就是 koa2 源码的源文件结构,核心代码就是 lib 目录下的四个文件:

application.js

application.js是 koa 的入口文件,它向外导出了创建 class 实例的构造函数,继承自 node 自带的events,这样就会赋予框架事件监听和事件触发的能力。application 还暴露了一些常用的 api,比如listenuse等等。

listen的实现原理其实就是对http.createServer进行了一个封装,这个函数中传入的callback是核心,它里面包含了中间件的合并,上下文的处理,对 res 的特殊处理。

use 的作用主要是收集中间件,将多个中间件放入一个缓存队列中,然后通过koa-compose这个插件进行递归组合调用这一系列的中间件。

context.js

这部分就是 koa 的应用上下文 ctx,其实就一个简单的对象暴露,里面的重点在 delegate,这个就是代理,这个就是为了开发者方便而设计的,比如我们要访问 ctx.repsponse.status 但是我们通过 delegate,可以直接访问 ctx.status 访问到它。

request.js、response.js

这两部分就是对原生的resreq的一些操作了,大量使用 es6 的getset的一些语法,去取headers或者设置headers、还有设置body等等

路由(URL 处理)

原生路由实现

koa 是个极简的 web 框架,简单到连路由模块都没有配备,我们先来可以根据ctx.request.url或者ctx.request.path获取用户请求的路径,来实现简单的路由。

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

app.use( async ( ctx ) => {
  let url = ctx.request.url
  ctx.body = url
})
app.listen(3000)

访问 http://localhost:3000/hello/forest 页面会输出 /hello/forest,也就是说上下文的请求request对象中url就是当前访问的路径名称,可以根据ctx.request.url 通过一定的判断或者正则匹配就可以定制出所需要的路由。

koa-router 中间件

如果依靠ctx.request.url去手动处理路由,将会写很多处理代码,这时候就需要对应的路由的中间件对路由进行控制,这里介绍一个比较好用的路由中间件koa-router

安装 koa-router 中间件

// koa2 对应的版本是 7.x
$ npm install --save koa-router@7

使用

const Koa = require('koa');
const Router = require('koa-router');

const app = new Koa();
const router = new Router();

router.get('/', async (ctx) => {
  let html = `
      <ul>
        <li><a href="/hello">helloworld</a></li>
        <li><a href="/about">about</a></li>
      </ul>
    `
  ctx.body = html
}).get('/hello', async (ctx) => {
  ctx.body = 'hello forest'
}).get('/about', async (ctx) => {
  ctx.body = '前端森林'
})

app.use(router.routes(), router.allowedMethods())

app.listen(3000);

中间件

在上面说到路由时,我们用到了中间件(koa-router)。那中间件究竟是什么呢?

Koa 的最大特色,也是最重要的一个设计,就是中间件(middleware)。Koa 应用程序是一个包含一组中间件函数的对象,它是按照类似堆栈的方式组织和执行的。

Koa 中使用app.use()来加载中间件,基本上 Koa 所有的功能都是通过中间件实现的。

每个中间件默认接受两个参数,第一个参数是 Context 对象,第二个参数是next函数。只要调用 next 函数,就可以把执行权转交给下一个中间件。

下图为经典的 Koa 洋葱模型:

我们来看一下 koa 官网的这个例子:

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

// logger

app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.get('X-Response-Time');
  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});

// x-response-time

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

// response

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

app.listen(3000);

上面的执行顺序就是:请求 -> logger 中间件 -> x-response-time 中间件 -> 响应中间件 -> x-response-time 中间件 -> logger 中间件 -> 响应。

通过这个顺序我们可以发现这是个栈结构以"先进后出"(first-in-last-out)的顺序执行。

Koa 已经有了很多好用的中间件(https://github.com/koajs/koa/wiki#middleware)你需要的常用功能基本上上面都有。

请求数据获取

get

获取方法

在 koa 中,获取GET请求数据源使用 koa 中 request 对象中的query方法或querystring方法。

query返回是格式化好的参数对象,querystring返回的是请求字符串,由于 ctx 对 request 的 API 有直接引用的方式,所以获取 GET 请求数据有两个途径。

  • 1、从上下文中直接获取

    • 请求对象ctx.query,返回如 { name:'森林', age:23 }
    • 请求字符串 ctx.querystring,返回如 name=森林&age=23
  • 2、从上下文的 request 对象中获取

    • 请求对象ctx.request.query,返回如 { a:1, b:2 }
    • 请求字符串 ctx.request.querystring,返回如 a=1&b=2

示例

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

app.use( async ( ctx ) => {
  let url = ctx.url
  // 从上下文的request对象中获取
  let request = ctx.request
  let req_query = request.query
  let req_querystring = request.querystring

  // 从上下文中直接获取
  let ctx_query = ctx.query
  let ctx_querystring = ctx.querystring

  ctx.body = {
    url,
    req_query,
    req_querystring,
    ctx_query,
    ctx_querystring
  }
})

app.listen(3000, () => {
  console.log('[demo] request get is starting at port 3000')
})

post

对于POST请求的处理,koa2 没有封装获取参数的方法,需要通过自己解析上下文 context 中的原生 node.js 请求对象req,将 POST 表单数据解析成 querystring(例如:a=1&b=2&c=3),再将 querystring 解析成 JSON 格式(例如:{"a":"1", "b":"2", "c":"3"})。

我们来直接使用koa-bodyparser 中间件从 POST 请求的数据体里面提取键值对。

对于POST请求的处理,koa-bodyparser中间件可以把 koa2 上下文的formData数据解析到ctx.request.body中。

示例

首先安装koa-bodyparser

$ npm install --save koa-bodyparser@3

看一个简单的示例:

const Koa = require('koa')
const app = new Koa()
const bodyParser = require('koa-bodyparser')

// 使用koa-bodyparser中间件
app.use(bodyParser())

app.use(async (ctx) => {

  if (ctx.url === '/' && ctx.method === 'GET') {
    // 当GET请求时候返回表单页面
    let html = `
      <h1>koa2 request post demo</h1>
      <form method="POST" action="/">
        用户名:<input name="name" /><br/>
        年龄:<input name="age" /><br/>
        邮箱: <input name="email" /><br/>
        <button type="submit">submit</button>
      </form>
    `
    ctx.body = html
  } else if (ctx.url === '/' && ctx.method === 'POST') {
    // 当POST请求的时候,中间件koa-bodyparser解析POST表单里的数据,并展示到页面
    ctx.body = ctx.request.body
  } else {
    // 404
    ctx.body = '<h1>404 Not Found</h1>'
  }
})

app.listen(3000, () => {
  console.log('[demo] request post is starting at port 3000')
})

模版引擎

在实际项目开发中,返回给用户的网页往往都会被写成模板文件。 Koa 先读取模板文件,然后将这个模板返回给用户,这里我们就需要使用模板引擎了。

关于 Koa 的模版引擎,我们只需要安装 koa 模板使用中间件koa-views, 然后再下载你喜欢的模板引擎(支持列表)便可以愉快的使用了。

这里以使用ejs模版为例展开说明。

安装模版

// 安装koa模板使用中间件
$ npm install --save koa-views

// 安装ejs模板引擎
$ npm install --save ejs

使用模版引擎

文件目录

├── package.json
├── index.js
└── view
    └── index.ejs

./index.js 文件

const Koa = require('koa')
const views = require('koa-views')
const path = require('path')
const app = new Koa()

// 加载模板引擎
app.use(views(path.join(__dirname, './view'), {
  extension: 'ejs'
}))

app.use( async ( ctx ) => {
  let title = '森林带你学koa2'
  await ctx.render('index', {
    title,
  })
})

app.listen(3000)

./view/index.ejs 模板

<!DOCTYPE html>
<html>
<head>
    <title><%= title %></title>
</head>
<body>
    <h1><%= title %></h1>
    <p>EJS Welcome to <%= title %></p>
</body>
</html>

静态资源服务器

网站一般都提供静态资源(图片、字体、样式表、脚本……),我们可以自己实现一个静态资源服务器,但这没必要,koa-static模块封装了这部分功能。

安装

$ npm i --save koa-static

示例

const Koa = require('koa')
const path = require('path')
const static = require('koa-static')

const app = new Koa()

// 静态资源目录对于相对入口文件index.js的路径
const staticPath = './static'

app.use(static(
  path.join( __dirname,  staticPath)
))


app.use( async ( ctx ) => {
  ctx.body = 'hello world'
})

app.listen(3000, () => {
  console.log('[demo] static-use-middleware is starting at port 3000')
})

cookie/session

koa2 中使用 cookie

使用方法

koa 提供了从上下文直接读取、写入 cookie 的方法:

  • ctx.cookies.get(name, [options]) 读取上下文请求中的 cookie
  • ctx.cookies.set(name, value, [options]) 在上下文中写入 cookie koa2 中操作的 cookies 是使用了 npm 的cookies模块,源码在这里,所以在读写 cookie 时的使用参数与该模块的使用一致。

示例

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

app.use( async ( ctx ) => {

  if ( ctx.url === '/index' ) {
    ctx.cookies.set(
      'cid',
      'hello world',
      {
        domain: 'localhost',  // 写cookie所在的域名
        path: '/index',       // 写cookie所在的路径
        maxAge: 10 * 60 * 1000, // cookie有效时长
        expires: new Date('2017-02-15'),  // cookie失效时间
        httpOnly: false,  // 是否只用于http请求中获取
        overwrite: false  // 是否允许重写
      }
    )
    ctx.body = 'cookie is ok'
  } else {
    ctx.body = 'hello world'
  }

})

app.listen(3000, () => {
  console.log('[demo] cookie is starting at port 3000')
})

koa2 中实现 session

koa2 原生功能只提供了 cookie 的操作,但是没有提供session操作。session 就只能自己实现或者通过第三方中间件实现。

我这里给大家演示一下通过中间件koa-generic-session来在 koa2 中实现 session。

const Koa = require("koa");
const redisStore = require("koa-redis");
const session = require("koa-generic-session");

app.use(
  session({
    key: "forum.sid", // cookie name
    prefix: "forum:sess:", // redis key的前缀
    cookie: {
      path: "/",
      httpOnly: true,
      maxAge: 24 * 60 * 60 * 1000 // ms
    },
    ttl: 24 * 60 * 60 * 1000, // ms
    store: redisStore({
      all: `${REDIS_CONF.host}:${REDIS_CONF.port}`
    })
  })
);

koa2 处理流程

上面提到了很多 koa2 涉及到的一些概念,下面让我们梳理一下 koa2 完整的处理流程吧!

完整大致可以分为以下四部分:

  • 初始化应用
  • 请求到来-创建上下文
  • 请求到来-中间件执行
  • 返回 res-特殊处理

这里参考大佬的一张关于 koa2 的完整流程图,

初始化应用

在我们的app.js中,初始化的时候创建了 koa 实例(new koa()),然后是很多的use,最后是app.listen(3000)

use主要是把所有的函数(使用的中间件)收集到一个middleware数组中。

listen主要是对http.createServer进行了一个封装,这个函数中传入的callback是核心,它里面包含了中间件的合并,上下文的处理等。也就是http.createServer(app.callback()).listen(...)

创建上下文

一个请求过来时,可以拿到对应的 req、res,koa 拿到后就通过createContext来创建应用上下文,并进行属性代理delegate

中间件执行

请求过来时,通过use操作已经将多个中间件放入一个缓存队列中。使用koa-compose将传入的middleware组合起来,然后返回了一个 promise。

http.createServer((req, res) => {
 // ... 通过req,res创建上下文
 // fn是`koa-compose`返回的promise
 return fn(ctx).then(handleResponse).catch(onerror);
})

res 返回并进行特殊处理

在上面一部分,我们看到有一个handleResponse,它是什么呢?(其实到这里我们还没有res.end())。

const handleResponse = () => respond(ctx);

respond 到底做了什么呢,其实它就是判断你之前中间件写的 body 的类型,做一些处理,然后才使用res.end(body)

到这里就结束了,返回了页面。

参考

福利

到这里关于 koa2 的一些相关概念就分享结束了,不知道你有没有收获呢?

我这里有两个关于koa2的完整(何为完整,数据库、日志、模型等等等等,你想要的都有)的项目,可以供大家参考,当然感觉不错的话可以给个 star 支持一下!!

  • https://github.com/Jack-cool/rest_node_api
  • https://github.com/Jack-cool/forum_code

最后

同时你可以关注我的同名公众号【前端森林】,这里我会定期发一些大前端相关的前沿文章和日常开发过程中的实战总结。