signed

QiShunwang

“诚信为本、客户至上”

Koa框架学习

2020/12/28 3:28:03   来源:

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

以上是Koa官网对Koa框架的描述。

1️⃣概述

1.1❀ Koa简介

  • Koa核心模块并未捆绑任何中间件(路由功能也需要引入别的中间件)【方便用户的拓展】
  • Koa使用了 Promise、async/await 语法来进行异步编程(Express 是基于事件和回调的)【避免地狱回调】
  • Koa增强了对错误的处理
  • Koa开发的web应用体积更小,功能更强大

可见, Koa框架和 Expres框架的主要差别在于异步编程和中间件方面,其他特性是相似的。

由于Koa进行异步调用时强制使用async/await,因此需要将异步回调方法转换为Promise,为了避免每个回调方法都需要自己包装,接下来将介绍这一问题目前最好的解决方案-Bluebird.

1.2❀ Bluebird

Bluebird 是Node.js最出名的 Promise 实现,除了实现标准的Promise规范之外,Bluebird还提供了包装方法,可以快速地将Node.js回调风格的函数包装为Promise。

安装:

npm install bluebird --save

将Node.js 回调风格的函数包装为Promise函数,该方法签名如下:

bluebird.promisifyAll(target[,options])

target 需要包装的对象。

  • target为普通对象,则包装后生成的异步API只有该对象持有
  • target为原型对象,则包装后生成的异步API被该原型所有实例持有。

options

  • suffix:设置异步API方法名后缀,默认为“Async”
  • multiArgs:是否允许多个回调参数,默认false。Promise的then()方法只接受一个resolve类参数(reject类也可以接受一个),但Node.js的回调函数function(err,...data)却可以接受多个参数。multiArgs为true时,bluebird将回调函数的所有参数组装成一个数组,然后传递给then,从而得到多个参数。

bluebird.promisifyAll()只会给目标对象添加新方法,原来的 Node.js 回调风格的方法不受影响。

包装之后的方法和包装之前的方法使用起来只有一个差别,那就是不要传递回调函数,通过 Promise 获取结果。


🌰🌰以下是包装fs对象的实例🌰🌰
const fs = require("fs");
const bluebird = require("bluebird");

bluebird.promisifyAll(fs);

//回调函数示例
fs.readFile("./package.json", { encoding: "utf-8" }, (err, data) => {
  if (err) {
    console.warn("读取异常", err);
    return;
  }
  console.log(data);
});

//Prmise示例
fs.readFileAsync("./package.json", { encoding: "utf-8" })
  .then((data) => {
    console.log(data);
  })
  .catch((err) => {
    console.warn("读取异常", err);
  });

2️⃣Hello Koa

1.初始化项目

npm init -y

2.模块安装

npm i webpack koa --save

3.编码

const Koa = require("koa");

const app = new Koa();//不同于express Koa是new实例

app.use(async (context) => {
  context.body = "Hello Koa";
});

app.listen(8080, () => {
  console.log("listen on 8080");
});

4.运行

node .\app.js   

在这里插入图片描述

Koa核心模块不绑定其它中间件,例子没有使用到路由,而是使用了中间件,无论任何对该服务的请求都只会返回Hello Koa

3️⃣Context

Koa Context包含:Koa Request、Koa Response和应用实例(app)等

Koa Request 对象是在 node 的 原生请求对象之上的抽象
Koa Response 对象是在 node 的原生响应对象之上的抽象

注意几个容易混淆API:

ctx.req:Node 的 request 对象.
ctx.res:Node 的 response 对象.

  • 绕过 Koa 的 response 处理是 不被支持的. 应避免使用以下 node 属性:
    res.statusCode
    res.writeHead()
    res.write()
    res.end()

ctx.request:koa 的 Request 对象.
ctx.response:koa 的 Response 对象.

3.1❀ Request 和 Response 的别名

一般不直接通过ctx.request.[propName]/ctx.response.[propName]调用Koa Request/Response对象属性,而是通过对应的别名来调用其属性,比如:ctx.headers就是ctx.request.headers的别名,就是简写一层调用。

那为什么ctx.headersctx.request.headers,而不是ctx.response.headers呢?

以下是我从官网目前版本截取的别名字段:👉官网Koa 别名
在这里插入图片描述
可以看到,ctx.[propName]在Request和Response别名里,没有重复的propName。说明Koa对Context维护了一个唯一键名指向对应Koa Request和Response对象属性名的数据结构。

ctx.headers==ctx.request.headers
ctx.body==ctx.response.body

假如想要Koa Response对象的headers怎么办?

那就不使用别名,直接读取即可 ctx.response.headers

使用别名,这不是很容易混淆吗?

我也是这么觉得的!估计初学者都是如此。但存在肯定有它的意义,别名归属(Request/Response)的划分,可能更倾向于该属性在服务器中的使用频度。以headers为例,虽然请求对象和响应对象都可以用到这个属性,但是对于服务器而言,人们可能更关注的是请求头的信息而非响应头的信息。但对于熟悉Koa的人来说,通过别名,简写也是一种效率的提升。这是我个人的一点看法。

3.2❀ Context常用方法和属性

只是罗列Conetxt常用的方法和属性,想看更全面的和具体用法👉官网Context

  • ctx.request:Koa的请求对象,一般不直接使用,通过别名引用来访问。

  • ctx.response:Koa的响应对象,一般不直接使用,通过别名引用来访问。

  • ctx.state:自定义数据存储,比如中间件需要往请求中挂载变量就可以存放在ctx.state中,后续中间件可以读取。
    前面的中间件 ctx.state.username=‘xx’ ,执行next()后,后面的中间件可以通过ctx.state.username获取到xx

  • ctx.throw():抛出 HTTP异常。

  • ctx.headers:请求报头,ctx.request.headers的别名。

  • ctx.method:请求方法,ctx.request.method的别名。

  • ctx.url:请求链接,ctx.request.url的别名。

  • ctx.path:请求路径,ctx.request.path的别名。

  • ctx.query:解析后的GET参数对象,ctx.request.query的别名。

  • ctx.host:当前域名,ctx.request.host的别名。

  • ctx.ip:客户端IP,ctx.request.ip的别名。

  • ctx.ips:反向代理环境下的客户端IP列表,ctx.request.ips的别名。

  • ctx.get():读取请求报头,ctx.request.get的别名。

  • ctx.body:响应内容,支持字符串、对象、Buffer,ctx.response.body的别名。

  • ctx.status:响应状态码,ctx.response.status的别名。

  • ctx.type:响应体类型,ctx.response.type 的别名。

  • ctx.redirect():重定向,ctx.response.redirect的别名。

  • ctx.set():设置响应报头,ctx.response.set的别名。


🌰🌰显示当前请求头信息并添加自定义报文头🌰🌰
const Koa = require("koa");

const app = new Koa();

app.use(async (ctx) => {
  ctx.set("customer-header", "nothing");
  ctx.body = {
    method: ctx.method,
    path: ctx.path,
    url: ctx.url,
    query: ctx.query,
    headers: ctx.headers,
    respHeaders: ctx.response.headers,
  };
});

app.listen(8080, () => {
  console.log("listen on 8080");
});

在这里插入图片描述

4️⃣Cookie

Cookie是Web应用维持少量数据的一种手段,常通过Cookie来维持服务端与客户端用户的身份认证。

4.1❀ Cookie 签名

由于 Cookie 存放在浏览器端,存在篡改风险,因此Web应用一般会在存放cookie数据的时候同时存放一个签名 cookie,以保证 Cookie 内容不被篡改。配置了cookie签名,一旦cookie被改动,那么该cookie直接会被浏览器移除。

Koa中需要配置 Cookie 签名密钥才能使用 Cookie功能,否则将报错。

app.keys=['signedkey'];//这是自定义密钥,你可以写任何字符串,推荐使用随机字符串,开启签名后会根据密钥使用加密算法加密该值

4.2❀ 写入Cookie

const Koa = require("koa");
const app = new Koa();
app.keys = ["signedkey"];
app.use(async (ctx) => {
  ctx.cookies.set("logged", 1, {
    signed: true,//启用cookie签名
    httpOnly: true,
    maxAge: 3600 * 10,
  });
  ctx.body = "ok";
});

app.listen(8080);

cookie中不仅存在logged还有logged.sid,这个logged.sid就绑定的签名。服务器读取logged的时候还会读取logged.sid,一旦发现二者不匹配,设置cookie未undefined.
在这里插入图片描述

4.3❀ 读取Cookie

const Koa = require("koa");
const app = new Koa();
app.keys = ["signedkey"];
app.use(async (ctx) => {
    const logged=ctx.cookies.get("logged",  {
      signed: true,
    });
    ctx.body = logged;
  });
app.listen(8080);

5️⃣中间件

5.1❀ 概念

类似Express中间件,可以访问请求对象、响应对象 和 next函数。只不过Koa的中间件通过操作Context对象获取请求对象、响应对象。

如果一个请求流程中,任何中间件都没有输出响应,Koa 中此次请求将返回404状态码(在Express中会将请求挂起直至超时)。

造成这种差别的原因是Express 需要手动执行输出函数才可以结束请求流程,而Koa 使用了async/await来进行异步编程,不需要执行回调函数,直接对ctx.body赋值即可(如果连body都没有,相当于资源不存在,自然404)。

Koa的中间件是一个标准的异步函数,函数签名如下:

async function middleware(ctx, next) //next函数:指向下一个中间件。

运行完逻辑代码,将需要传递的数据挂载到ctx.state,并且调用 await next()才能将请求交给下一个中间件处理。

5.2❀ 洋葱模型 (中间件执行流程)

在这里插入图片描述
Koa的中间件模型称为“洋葱圈模型”,请求从左边进入,有序地经过中间件处理,最终从右边输出响应。

最先use的中间件在最外层,最后use的中间件在最内层。

一般的中间件会执行两次,调用next之前为第一次,也就是“洋葱左半边”这一部分,从外层向内层依次执行。当后续没有中间件时,就进入响应流程,也就是“洋葱右半边”这一部分,从内层向外层依次执行,这是第二次执行。


🌰🌰洋葱圈模型🌰🌰
const Koa = require("koa");
const app = new Koa();

async function middleware1(ctx,next){
    console.log('m1 start');
    await next();
    console.log('m1 end');
}

async function middleware2(ctx,next){
    console.log('m2 start');
    await next();
    console.log('m2 end');
}

app.use(middleware1);
app.use(middleware2);

app.use(async (ctx)=>{
    console.log('我是路由,我后面没有中间件了');
    ctx.body='洋葱圈模型';
})

app.listen(8080);

在这里插入图片描述
类似栈帧,多了路由。

6️⃣错误处理

Koa采用了洋葱圈模型,所以Koa的错误处理中间件需要在应用的开始处挂载,这样才能将整个请求-响应周期涵盖,捕获其发生的错误。而Express错误处理中间件需要放置在应用末尾。


🌰🌰多个错误处理器(先处理后记录)🌰🌰
const Koa = require("koa");
const app = new Koa();

async function errorHandler(ctx,next){
    try{
        await next();
    }catch(e){
        ctx.status=e.status||500;
        ctx.body='Error:'+e.message;
    }
}

async function errorLogger(ctx,next){
    try{
        await next();
    }catch(e){
       console.log(`${ctx.method} ${ctx.path} Error:${e.message}`);
        throw e;//继续往外抛出错与,被errorHandler接收处理
    }
}

app.use(errorHandler);
app.use(errorLogger);

app.use((ctx)=>{
    ctx.throw(403,'Forbidden');
})

app.listen(8080);

在这里插入图片描述

在这里插入图片描述

7️⃣路由模块

7.1❀ 默认路由函数

Koa核心并没有提供路由功能,但是可以使用一个默认的路由函数来提供响应。所有的请求都会执行该默认的路由函数。

路由函数的定义如下:

async function(ctx,next)

如果路由函数内部未使用异步逻辑,async是可以省略的。一个路由可以有多个处理函数。

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

app.use(async (ctx,next)=>{//默认路由函数1
    ctx.body=1;
    next();//没有next就不会执行下一个路由函数了
});
app.use((ctx)=>{//默认路由函数2
    ctx.body=2;
});
app.listen(8080,()=>{
    console.log('listen on 8080');
})

这个并不算真正路由功能,下面介绍koa-router.

7.2❀ Hello koa-router

npm i koa-router --save
const Koa = require("koa");
const Router=require('koa-router');
const app = new Koa();
const router=new Router();

router.get('/',async (ctx)=>{
    ctx.body='root page';
})

router.post('/user/:userId(\\d+)',ctx=>{//限定路由参数为数值
    ctx.body=ctx.path;
})

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

app.listen(8080,()=>{
    console.log('listen on 8080');
})

allowedMethods是当所有路由中间件执行完成之后,若ctx.status为空或者404的时候,丰富response对象的header头。当然,如果我们不设置router.allowedMethods()在表现上除了ctx.status不会自动设置,以及response header中不会加上Allow之外,不会造成其他影响.
在这里插入图片描述

7.3❀ 路由对象

路由需要实例化后才能配置和挂载。

路由构造器:

function Router([options]) //常用的option有 prefix,指定路由前缀

路由定义方法:

router.method(path,handler);//支持多个handler

7.4❀ 路由函数

Koa-router的路由函数与默认路由函数相似,支持多个路由函数处理同一个请求。

router.get(
  "/",
  async (ctx, next) => {
    ctx.state.data = "root page";
    await next();
  },
  (ctx) => {
    ctx.body = ctx.state.data;
  }
);

7.5❀ 路由级别中间件

Koa默认的中间件是应用级别的,所有请求都被中间件处理。因为Koa-router支持多个路由函数,因此可以在指定路由或者整个路由对象上(常应用于模块化路由)使用中间件。

async function logger(ctx,next){
    console.log(`${ctx.method} ${ctx.path} `);
    await next();
}
router.get('/',logger,ctx=>{
	ctx.body='hello';
})

7.6❀ 模块化路由

将路由拆分在对应业务模块,方便维护!

  • 独立文件实现路由逻辑
  • 入口文件挂载路由

user.js

const Router = require("koa-router");
const router = new Router({prefix:'/user'});

router.get('/',ctx=>{
    ctx.body='user Page';
})

router.post('/login',async (ctx)=>{
    ctx.body='login';
})

module.exports=router;

sites.js

const Router = require("koa-router");
const router = new Router();

router.get('/',ctx=>{
    ctx.body='index Page';
})

router.get('/about',ctx=>{
    ctx.body='about Page';
})

module.exports=router;

app.js

const Koa = require("koa");
const Router = require("koa-router");
const app = new Koa();
const user=require('./user');
const sites=require('./sites');

app.use(user.routes()).use(user.allowedMethods());
app.use(sites.routes()).use(sites.allowedMethods());

app.listen(8080,()=>{
    console.log('listen on 8080');
})

在这里插入图片描述

8️⃣模板渲染

支持很多模板,暂时使用ejs模板

8.1❀ koa-ejs

koe-ejs支持ctx.state,挂载到ctx.state中的变量可以直接在ejs模板中使用。

安装koa-ejs模块

npm i koa-ejs --save 

templates/home.ejs

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><%= title  %> </title>
</head>
<body>
    <%= name  %> 
</body>
</html>

app.js

const Koa = require("koa");
const ejsRender =require('koa-ejs');
const app = new Koa();

ejsRender(app,{//使用ejs插件
    root:'./templates',//模板目录
    layout:false,//关闭模板布局
    viewExt:'ejs'//使用ejs模板引擎
})

app.use(async (ctx)=>{
    ctx.state.title='首页';
    await ctx.render('home',{//给home模板页传递属性
        name:'Joe'
    })
});

app.listen(8080, () => {
    console.log("listen on 8080");
  });

在这里插入图片描述

8.2❀ 模板布局

Web页面一般由一下结构组成:

  • 头部区域(导航栏等)
  • 内容主体
  • 底部区域(版权声明等)

一般而言,头部和底部区域不变化,只有内容主体变化,那就以内容主体构建布局!

templates/main.ejs

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>main 页面</title>
</head>
<body>
    <header style="border: 1px solid black;">页头</header>
    <%- body %> 
    <footer style="border: 1px solid black;">页尾</footer>
</body>
</html>

templates/home.ejs

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><%= title  %> </title>
</head>
<body>
    <%= name  %> 
</body>
</html>

app.js

const Koa = require("koa");
const ejsRender =require('koa-ejs');
const app = new Koa();

ejsRender(app,{//使用ejs插件
    root:'./templates',//模板目录
    layout:'main',//开启模板布局 以main模板作为布局模板
    viewExt:'ejs'//使用ejs模板引擎
})

app.use(async (ctx)=>{
    ctx.state.title='主页';
    await ctx.render('home',{//指定home模板作为子模板 之后父模板 <%- body %> body就是home模板的body节点 
        name:'Joe'
    })
});

app.listen(8080, () => {
    console.log("listen on 8080");
  });

在这里插入图片描述