koa2编写后端接口(一)

270 阅读5分钟

我正在参加「掘金·启航计划」

前言

我本身是一个前端,工作和平时学习中更多的是跟前端的框架打交道。但是随着技术的提升,要不断的往技术的深度和广度进行深挖。曾经多次学习过node以及他的技术栈express,koa,但是因为写的少,总是忘记,没有到达一个熟练掌握的状态。希望通过此文,能牢靠掌握koa2,并像全栈进发。

express和koa2区别,为什么我要选择koa2?

相同点:

  1. 建立在http模块基础上:底层都是建立在node.js内置的http模块上。http模块生成服务器的原始代码如下
var http = require("http");

var app = http.createServer(function(request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.end("Hello world!");
});

app.listen(3000, "localhost");

不同点

  1. 中间件的处理方式不同:express是一个中间件处理完,再传递给下一个中间件。而koa使用的是洋葱模型。
  2. koa更加轻量:express其自带 Router、路由规则、View 等功能。而koa把这些都交给中间件进行处理
  3. koa使用了async,await:express处理异步的方式主要是callback,其源码也是callback太多。而koa使用了async,await处理异步,代码更清晰易懂,维护更方便。

通过上面的对比发现,koa的特点是更加轻量,速度更快,代码更清晰易懂,自由度高,所以我毫不犹豫的选择koa

基本用法

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

app.use(ctx => { //处理请求的中间件
    ctx.response.body = "hello world";
})
app.listen(3000);

我们可以这么联想koa是一个继承了eventEmitter的类,这个类主要有两个函数,uselisten

为什么node中的fs,http,net等内置模块都继承了EventEmitter类呢?

  • 解决了“回调地狱”
  • 将多个模块进行了解耦,自己执行时,不需要知道另一个模块的存在,只需要关心发布出来的事件就行
  • 因为多个模块可以不知道对方的存在,自己关心的事件可能是一个很遥远的旮旯发布出来的,也不能通过代码跳转直接找到发布事件的地方,debug的时候可能会有点困难。

use函数的作用是什么?

use函数的作用很简单,就是存储中间件函数的,use中的函数就是中间件函数

listen函数的作用是什么?

listen函数也很简单,就是上面说的对http模块的一个封装

listen(...args) { 
    // 这里的this.callback也就是执行中间件的时候
    const server = http.createServer(this.callback);
    server.listen(...args); 
}

context的作用是什么?

我们注意一下中间件函数中的第一个参数ctx,也就是context

Koa 提供一个 Context 对象,表示一次对话的上下文(包括 HTTP 请求和 HTTP 回复)。通过加工这个对象,就可以控制返回给用户的内容

ctx.response代表 HTTP Response。同样地,ctx.request代表 HTTP Request

什么是中间件

中间件的本质就是一个函数,在收到请求和返回相应的过程中做一些我们想做的事情。Express文档中对它的作用是这么描述的:

"中间件"(middleware),它处在 HTTP Request 和 HTTP Response 中间,用来实现某种中间功能。app.use()用来加载中间件

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

常用的中间件包括路由级中间件,错误处理中间件,应用级中间件

洋葱模型是如何执行的

  • 最外层的中间件首先执行。
  • 调用next函数,把执行权交给下一个中间件。
  • ...
  • 最内层的中间件最后执行。
  • 执行结束后,把执行权交回上一层的中间件。
  • ...
  • 最外层的中间件收回执行权之后,执行next函数后面的代码

compose源码

// 异步递归遍历函数 
// 传入中间件数组,返回一个函数 
// 默认只执行第一个中间件,如果第一个中间件不执行next()方法的话,那么不会执行第二个中间件,依次类推 
// 注意要用promise.resolve包裹,这样每一个中间件的返回值都是一个promise了,所以可以使用await next()
compose(middleware) {
    return function (context) { 
        const dispatch = (i) => { 
            let fn = middleware[i]; 
            if (i === middleware.length) { 
                return Promise.resolve(); 
            } 
            return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); 
         }; 
    // 执行第一个中间件 
    return dispatch(0); 
   }; 
 }

异步中间件

const fs = require('fs.promised');
const Koa = require('koa');
const app = new Koa();

const main = async function (ctx, next) {
  ctx.response.type = 'html';
  ctx.response.body = await fs.readFile('./demos/template.html', 'utf8');
};

app.use(main);
app.listen(3000);

路由的使用

koa-route 模块 原生路由用起来不太方便,我们可以使用封装好的koa-route模块

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

const Router = require('koa-router')

let home = new Router()

// 子路由1
home.get('/', async ( ctx )=>{
  let html = `
    <ul>
      <li><a href="/page/helloworld">/page/helloworld</a></li>
      <li><a href="/page/404">/page/404</a></li>
    </ul>
  `
  ctx.body = html
})

// 子路由2
let page = new Router()
page.get('/404', async ( ctx )=>{
  ctx.body = '404 page!'
}).get('/helloworld', async ( ctx )=>{
  ctx.body = 'helloworld page!'
})

// 装载所有子路由
let router = new Router()
router.use('/', home.routes(), home.allowedMethods())
router.use('/page', page.routes(), page.allowedMethods())

// 加载路由中间件
app.use(router.routes()).use(router.allowedMethods())

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

静态资源

如果网站提供静态资源(图片、字体、样式表、脚本......),为它们一个个写路由就很麻烦,也没必要。koa-static模块封装了这部分的请求

// 访问 http://localhost:3000/test.json
const Koa = require("koa");
const app = new Koa();

const path = require('path');
const serve = require('koa-static');

const main = serve(path.join(__dirname, "../public/"));

app.use(main);
app.listen(3000);

有时候也可以在静态文件中设置html,这样会根据路由去找对应的index.html

返回html文件

home.get('/', async (ctx) => {
  const index = path.join(__dirname, './index.html');
  // 设置response的Content-Type:
  ctx.response.type = 'html';  // 关键是这里,设置返回类型
  ctx.response.body = fs.readFileSync(index);
})

参考