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-multe
r来处理multipar
t的文件;使用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函数是否等待了。