Koa2
[[toc]]
基本用法
path
__filename 全局值,当前文件绝对路径 module.filename === filename 等价 
**dirname** 全局值,当前文件夹绝对路径。等效于path.resolve(__filename, ‘..’)
path.join([…paths]) 相当于把所传入的任意多的参数 按照顺序 进行命令行般的推进
path.resolve([…paths]) 以当前文件的路径为起点,返回绝对路径。可以理解为每次都是新建cd命令
path.dirname(path) 返回指定路径所在文件夹的路径
path.basename(path) 返回指定Path路径所在文件的名字
path.extname(path) 获取指定字符串或者文件路径名字的后缀名,带.比如.txt
path.isAbsolute(path) 是否是绝对路径,返回boolean值
process.cwd()   返回运行当前脚本的工作目录的路径 
process.chdir() 改变工作目录
path.join('a','b','../c/lolo') 
  path.resolve('/a', '/b')  path.resolve('./a', './b') 
  const filePath = './bar/baz/asdf/quux.html' path.basename(filePath)  path.dirname(filePath)  path.extname(filePath)  path.isAbsolute(filePath) 
  | 
 
例子,文件路径有如下结构:
newapp > demo > hello.js
在hello.js文件中编写如下代码:
console.log(__dirname); console.log(__filename); console.log(module.filename===__filename); console.log(process.cwd()); process.chdir('/Users/jerry') console.log(process.cwd());
   | 
 
然后定位在newapp目录下,执行命令 node demo/hello.js,输出结果如下:
/Users/jerry/51talk/newapp/demo /Users/jerry/51talk/newapp/demo/hello.js true /Users/jerry/51talk/newapp /Users/jerry
   | 
 
启动HTTP服务
const Koa = require('koa'); const app = new Koa(); app.listen(...)  
 
 
 
  | 
 
开启import
 require('@babel/register')({     babelrc: false,     presets: ['@babel/preset-env'],     plugins: ["@babel/plugin-transform-runtime"] });   devDependencies: {     "@babel/core": "^7.4.5",     "@babel/plugin-transform-runtime": "^7.4.4",     "@babel/preset-env": "^7.4.5",     "@babel/register": "^7.4.4",     "@babel/runtime": "^7.4.5",     "nodemon": "^1.19.1" }
 
  | 
 
Context对象
Koa 提供一个 Context 对象,表示一次对话的上下文(包括 HTTP 请求和 HTTP 回复)。通过加工这个对象,就可以控制返回给用户的内容
Context.response.body属性就是发送给用户的内容
const Koa = require("koa"); const app = new Koa();
  app.use(ctx => {      ctx.response.body = "hello world"; }).listen(3000);
  | 
 
ctx.response代表 HTTP Response。同样地,ctx.request代表 HTTP Request
HTTP Response 的类型
Koa 默认的返回类型是text/plain,如果想返回其他类型的内容,可以先用ctx.request.accepts判断一下,客户端希望接受什么数据,然后使用ctx.response.type指定返回类型
const Koa = require("koa"); const app = new Koa();
  app.use(ctx => {     if (ctx.request.accepts('xml')) {         ctx.response.type = 'xml';         ctx.response.body = '<data>Hello World</data>';     } else if (ctx.request.accepts('json')) {         ctx.response.type = 'json';         ctx.response.body = { data: 'Hello World' };     } else if (ctx.request.accepts('html')) {         ctx.response.type = 'html';         ctx.response.body = '<p>Hello World</p>';     } else {         ctx.response.type = 'text';         ctx.response.body = 'Hello World';     } }).listen(3000);
  | 
 
设置响应头和请求头
 ctx.set('Content-Type', 'application/zip')
 
  ctx.append('userName','hzf');
 
  | 
 
网页模板
实际开发中,返回给用户的网页往往都写成模板文件。我们可以让 Koa 先读取模板文件,然后将这个模板返回给用户
const Koa = require("koa"); const app = new Koa(); const fs = require('fs');
  app.use(ctx => {     ctx.response.type = 'html';     ctx.response.body = fs.createReadStream('./public/template.html'); }).listen(3000);
  | 
 
路由
网站一般都有多个页面。通过ctx.request.path可以获取用户请求的路径,由此实现简单的路由
const Koa = require("koa"); const app = new Koa(); const fs = require('fs');
  app.use(ctx => {     if (ctx.request.path !== '/') {         ctx.response.type = 'html';         ctx.response.body = '<a href="/">Index Page1</a>';     } else {         ctx.response.body = 'Hello World';     } }).listen(3000);
  | 
 
koa-router 模块
原生路由用起来不太方便,我们可以使用封装好的koa-router模块
const Koa = require("koa"); const app = new Koa(); const fs = require('fs'); const route = require('koa-router')();
  route.get("/", ctx => {     ctx.response.type = 'html';     ctx.response.body = '<a href="/">Index Page1</a>'; }) route.get("/about", ctx => {     ctx.response.body = 'Hello World'; })
  app.use(router.routes()); //作用:启动路由 app.use(router.allowedMethods()); /* 作用: 这是官方文档的推荐用法,我们可以看到router.allowedMethods()用在了路由匹配 router.routes()之后,目的在于:根据ctx.status 设置response 响应头 */ app.listen(3000);
  | 
 
路由传值
 app.use((req, res, next) => {     console.log(req.query)      console.log(req.path)      console.log(req.params)      console.log(req.body)      console.log(req.cookies) 
           console.log(req.url)      console.log(req.headers)      console.log(req.method)      next() })
 
  | 
 
静态资源
如果网站提供静态资源(图片、字体、样式表、脚本……),为它们一个个写路由就很麻烦,也没必要。koa-static模块封装了这部分的请求
// 访问 http://localhost:3000/index.html const Koa = require("koa"); const app = new Koa(); const path = require('path'); const serve = require('koa-static');
  app.use(serve(process.cwd() + '/public')); app.listen(3000);
   | 
 
重定向
有些场合,服务器需要重定向(redirect)访问请求。比如,用户登陆以后,将他重定向到登陆前的页面。ctx.response.redirect()方法可以发出一个302跳转(临时性重定向),将用户导向另一个路由
const Koa = require("koa"); const app = new Koa(); const route = require("koa-router")();
  route.get("/orderList", ctx => {     ctx.response.redirect('/');     ctx.response.body = '<a href="/">Index Page</a>'; }) route.get("/", ctx => {     ctx.response.body = "hello world"; });
  app.use(router.routes()); app.use(router.allowedMethods()); app.listen(3000);
  | 
 
中间件
中间件的概念
Koa 的最大特色,也是最重要的一个设计,就是中间件(middleware)
- 基本上,Koa 所有的功能都是通过中间件实现的,前面例子里面的routes()也是中间件
 
- 每个中间件默认接受两个参数,
第一个参数是 Context 对象,第二个参数是next函数。只要调用next函数,就可以把执行权转交给下一个中间件,如果中间件内部没有调用next函数,那么执行权就不会传递下去 
多个中间件会形成一个栈结构(middle stack),以”先进后出”(first-in-last-out)的顺序执行,看下面的洋葱模型

- 最外层的中间件首先执行。
 
- 调用next函数,把执行权交给下一个中间件。
 
- …
 
- 最内层的中间件最后执行。
 
- 执行结束后,把执行权交回上一层的中间件。
 
- …
 
- 最外层的中间件收回执行权之后,执行next函数后面的代码
 
app.use(async (ctx,next)=>{   console.log("1");   await next();   console.log("3") }) app.use(async (ctx,next)=>{   console.log("2");   await next()   console.log("4") })
 
  | 
 
异步中间件
如果有异步操作(比如读取数据库),中间件就必须写成 async 函数
const response = () => {     function render({data, msg, status, code = 200,...option}) {         this.status = status || 200;         this.set("Content-Type", "application/json");         this.body = {             code: code,             msg,             data,             is_login: this['is_login'],             ...option         };     }
      return async (ctx, next) => {         ctx.send = render.bind(ctx);         await next()     } }; export default response
  | 
 
中间件的合成
koa-compose模块可以将多个中间件合成为一个
const Koa = require('koa'); const compose = require('koa-compose'); const app = new Koa();
  const logger = (ctx, next) => {   console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`);   next(); }
  const main = ctx => {   ctx.response.body = 'Hello World'; };
  const middlewares = compose([logger, main]);
  app.use(middlewares); app.listen(3000);
  | 
 
错误处理
500 错误
如果代码运行过程中发生错误,我们需要把错误信息返回给用户。HTTP 协定约定这时要返回500状态码
Koa提供了ctx.throw()方法,用来抛出错误,ctx.throw(500)就是抛出500错误
const Koa = require('koa'); const app = new Koa();
  const main = ctx => {   ctx.throw(500); };
  app.use(main); app.listen(3000);
  | 
 
ctx.response.status设置成404,就相当于ctx.throw(404),返回404错误
处理错误的中间件
为了方便处理错误,最好使用try...catch将其捕获。但是,为每个中间件都写try...catch太麻烦,我们可以让最外层的中间件,负责所有中间件的错误处理
const catchErr = () => {     return async (ctx, next) => {         try {             await next();         } catch (err) {             ctx.status = err.statusCode || err.status || 500;             ctx.body = {                 msg: "服务器错误",                 code: -1,                 data:[]             };         }
      } }; export default catchErr app.use(catchErr);
  | 
 
error 事件的监听
运行过程中一旦出错,Koa 会触发一个error事件。监听这个事件,也可以处理错误
const Koa = require('koa'); const app = new Koa();
  const main = ctx => {   ctx.throw(500); };
  app.on('error', (err, ctx) => {   console.error('server error', err); });
  app.use(main); app.listen(3000);
  | 
 
Web 的功能
Cookies
ctx.cookies用来读写 Cookie
ctx.cookies.set(name, value, [options])
   | 
 
访问 http://127.0.0.1:3000 ,你会看到1 views。刷新一次页面,就变成了2 views。再刷新,每次都会计数增加1
const Koa = require('koa'); const app = new Koa();
  const main = function(ctx) {     const n = Number(ctx.cookies.get('view') || 0) + 1;     ctx.cookies.set('view', n);     ctx.response.body = n + ' views'; }
  app.use(main); app.listen(3000);
  | 
 

Session
session 是另一种记录客户状态的机制,不同的是 Cookie 保存在客户端浏览器中,而 session 保存在服务器上
Session 的工作流程
当浏览器访问服务器并发送第一次请求时,服务器端会创建一个 session 对象,生 成一个类似于 key,value 的键值对, 然后将key(cookie)返回到浏览器(客户)端,浏览 器下次再访问时,携带 key(cookie),找到对应的 session(value).
koa-session 的使用
const session = require('koa-session');
 
  app.keys = ['some secret hurr'];   const CONFIG = {   key: 'koa:sess',     maxAge: 86400000,   autoCommit: true,   overwrite: true,    httpOnly: true,    signed: true,    rolling: false,    renew: false,  };
  app.use(session(CONFIG, app));
  | 
 
使用
 ctx.session.username = "张三";
 
  ctx.session.username
 
  | 
 
Cookie 和 Session 关系
coolies 的value 为session 存的内容,过程经过了请求与响应
通过cookies 与session存储数据;可以知道当前登录的是哪个用户
Cookie 和 Session 区别
- cookie 数据存放在客户的浏览器上,session 数据放在服务器上
 
- cookie 不是很安全,别人可以分析存放在本地的 COOKIE 并进行 COOKIE 欺骗 考虑到安全应当使用 session
 
- session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能 考虑到减轻服务器性能方面,应当使用 COOKIE
 
- 单个 cookie 保存的数据不能超过 4K,很多浏览器都限制一个站点最多保存 20 个 cookie
 
JWT(Json Web Token)
JWT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。它具备两个特点:
- 简洁(Compact)可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快
 
- 自包含(Self-contained) 负载中包含了所有用户所需要的信息,避免了多次查询数据库
 
- JWT的主要作用在于
- 可附带用户信息,后端直接通过JWT获取相关信息。
 
- 使用本地保存,通过HTTP Header中的
Authorization位提交验证。 
 
koa-jwt的工作流程
- 用户通过登录Api获取当前用户在有效期内的token
 
- 需要身份验证的API则都需要携带此前认证过的token发送至服务端
 
koa2会利用koa-jwt中间件的默认验证方式进行身份验证,中间件会进行验证成功和验证失败的分流。 
 {'authorization': "Bearer " + token}
 
  | 
 
在项目中使用
- 安装依赖
 
yarn add jsonwebtoken koa-jwt
   | 
 
- 中间件 请求验证token
 app.use(async (ctx, next) => {                    return next().catch((err) => {         if (err.status === 401) {             ctx.status = 401;             ctx.body = {                 code: 401,                 msg: err.message             }         } else {             throw err;         }     }) });
 
  | 
 
- 排除不验证的请求
app.use(koajwt({ secret: SECRET }).unless({     path: [/^\/api\/login/,/^\/api\/register/]    }));
  | 
 
- 登陆签发token
 
let customConfig = {     passportJwt: 'xxxxxxxxxx' }; const token = jwt.sign({          username:'xx',          password:'xx',          admin: true,          exp: Math.floor(Date.now() / 1000) + (60 * 60 * 24 * 30),      },     customConfig.passportJwt,       );
  ctx.body = {     code: 200,     msg: '登录成功',     token: token }
  | 
 
表单 (POST)
Web应用离不开处理表单。本质上,表单就是POST方法发送到服务器的键值对。koa-bodyparser模块可以用来从 POST 请求的数据体里面提取键值对
原生 Nodejs 获取 post 提交数据
function parsePostData(ctx){     return new Promise((resolve,reject)=>{         try{             let postdata="";             ctx.req.on('data',(data)=>{                 postdata += data             })             ctx.req.on("end",function(){                 resolve(postdata);             })         }catch(error){             reject(error);         }       }     }); }
  | 
 
Koa 中 koa-bodyparser 中间件的使用
const Koa = require('koa'); import bodyParser from "koa-bodyparser"; const app = new Koa();
  const main = async function(ctx) {   const body = ctx.request.body;   if (!body.name) ctx.throw(400, '.name required');   ctx.body = { name: body.name }; };
  app.use(bodyParser({     enableTypes: ['json', 'form', 'text'] })); app.use(main); app.listen(3000);
  | 
 
打开命令行窗口,运行下面的命令
curl -X POST --data "name=Jack" 127.0.0.1:3000 {"name":"Jack"}
  $ curl -X POST --data "name" 127.0.0.1:3000 name required
   | 
 
Koa-body模块
Koa2中利用Koa-body代替koa-bodyparser和koa-multer。原来通过koa-bodyparser来打包Post请求的数据,通过koa-multer来处理multipart的文件;使用koa-body后,ctx.request.files获得Post中的文件信息。ctx.request.body获得Post上传的表单信息。
// 添加koaBody中间件 app.use(   koaBody({     // 如果需要上传文件,multipart: true     // 不设置无法传递文件     multipart: true,     formidable: {       maxFileSize: 10 * 1024 * 1024  // 设置上传文件大小最大限制,默认2M     },     patchKoa: true   }) );
   | 
 
文件上传
实现文件上传的中间件有3个
- koa-body 
 
- busboy 
 
- koa-multer 
 
因为上面POST用了 koa-body ,这里还继续用koa-body,使用方式跟上面的一样,这里就不在写了
使用koa-body中间件后,即可通过ctx.request.files获取上传的文件
提醒: 
新版本的koa-body通过ctx.request.files获取上传的文件 
旧版本的koa-body通过ctx.request.body.files获取上传的文件 
上传单个文件
router.post('/uploadfile', async (ctx, next) => {   const file = ctx.request.files.file;        let filePath = path.join(__dirname, 'public/upload/') + `/${file.name}`;      const reader = fs.createReadStream(file.path);      const upStream = fs.createWriteStream(filePath);      reader.pipe(upStream);   return ctx.body = "上传成功!"; });
  | 
 
上传多个文件
router.post('/uploadfiles', async (ctx, next) => {   const filePaths = [];      const files = ctx.request.files.file;    for (let file of files) {          let filePath = path.join(__dirname, 'public/upload/') + `/${file.name}`;          const reader = fs.createReadStream(file.path);          const upStream = fs.createWriteStream(filePath);          reader.pipe(upStream);     filePaths.push(filePath);   }  return ctx.body = filePaths });
  | 
 
nodemailer
发送邮件
let transporter = nodemailer.createTransport({     host: 'smtp.163.com',     service: 'smtp.163.com',      port: 465,      secureConnection: true,      auth: {          user: 'xxxxxx@163.com',         pass: 'xxxxxx',      } });
  let title = '标题'; let mailOptions = {     from: `<feng960106@163.com>`,     to: `feng960106@163.com`,      subject: title || '自动发邮件',     text: JSON.stringify(params),     html: 'html模板',          attachments:[       {         filename:'',         path:'',       }     ] };
  new Promise((resolve, reject) => {     transporter.sendMail(mailOptions, (error, info) => {         if (error) return reject(error);         resolve(info)     }); }).then((info) => {     return ctx.send({         msg: info,     }); }).catch((err) => {     return ctx.send({         msg: err,         code: -1     }); })
  | 
 
koa-compress
压缩数据
const Koa = require('koa'); const app = new Koa(); const compress = require('koa-compress');   app.use(compress({          filter: function (content_type) {          return /text/i.test(content_type);     },     threshold: 1024*2,      flush: require('zlib').Z_SYNC_FLUSH }));  
  app.use(async(ctx, next) => {     ctx.compress = true;      await next(); });
  | 
 
遇到的问题
async/await后ctx.body失效
事由
在做ssr的时候,在Promise.all()里面返回的ctx.body没有值,但是能打印出来
原因
中间件在调用next()的时候 并没有把next当作一个异步函数使用。因此你在promise中异步赋值了ctx.body,但是由于next函数没有等你,在你赋值之前这一网络请求就已经完成了。
解决办法
- 使用异步的中间件
async/await和promise 
- 检查你的中间件的next函数是否等待了。