Koa2
# 基本用法
# 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') // a/c/lolo
path.resolve('/a', '/b') // '/b'
path.resolve('./a', './b') // '/User/../a/b'
const filePath = './bar/baz/asdf/quux.html'
path.basename(filePath) // quux.html
path.dirname(filePath) // ./bar/baz/asdf
path.extname(filePath) // .html
path.isAbsolute(filePath) // false
2
3
4
5
6
7
8
9
10
例子,文件路径有如下结构:
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());
2
3
4
5
6
然后定位在newapp目录下,执行命令 node demo/hello.js,输出结果如下:
/Users/jerry/51talk/newapp/demo
/Users/jerry/51talk/newapp/demo/hello.js
true
/Users/jerry/51talk/newapp
/Users/jerry
2
3
4
5
# 启动HTTP服务
const Koa = require('koa');
const app = new Koa();
app.listen(...) //方法是如下的一个语法糖。
// const http = require('http')
// http.createServer(app.callback()).listen(3000)
2
3
4
5
6
# 开启import
//server.js
require('@babel/register')({
babelrc: false,
presets: ['@babel/preset-env'],
plugins: ["@babel/plugin-transform-runtime"]
});
//package.json
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"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 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);
2
3
4
5
6
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);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 设置响应头和请求头
// 设置响应头
ctx.set('Content-Type', 'application/zip')
// 添加请求头
ctx.append('userName','hzf');
2
3
4
5
# 网页模板
实际开发中,返回给用户的网页往往都写成模板文件。我们可以让 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);
2
3
4
5
6
7
8
# 路由
网站一般都有多个页面。通过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);
2
3
4
5
6
7
8
9
10
11
12
# 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);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 路由传值
// http://localhost:3000/api/1?type=123
app.use((req, res, next) => {
console.log(req.query) // { type: '123' }
console.log(req.path) // /api/1
console.log(req.params) // 动态路由 ->类似于 '/api/:_id'
console.log(req.body) // 通常是 post method
console.log(req.cookies) // need cookie-parser middleware
// extend http.IncomingMessage
console.log(req.url) // /api/1?type=123
console.log(req.headers) // header object
console.log(req.method) // GET
next()
})
2
3
4
5
6
7
8
9
10
11
12
13
14
# 静态资源
如果网站提供静态资源(图片、字体、样式表、脚本……),为它们一个个写路由就很麻烦,也没必要。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);
2
3
4
5
6
7
8
# 重定向
有些场合,服务器需要重定向(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);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 中间件
# 中间件的概念
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")
})
// 打印结果是:1,2,4,3
2
3
4
5
6
7
8
9
10
11
# 异步中间件
如果有异步操作(比如读取数据库),中间件就必须写成 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 中间件的合成
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);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 错误处理
# 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);
2
3
4
5
6
7
8
9
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);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 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);
2
3
4
5
6
7
8
9
10
11
12
13
# 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);
2
3
4
5
6
7
8
9
10
11
# Session
session
是另一种记录客户状态的机制,不同的是 Cookie
保存在客户端浏览器中,而 session
保存在服务器上
Session 的工作流程
当浏览器访问服务器并发送第一次请求时,服务器端会创建一个 session
对象,生 成一个类似于 key,value
的键值对, 然后将key(cookie)
返回到浏览器(客户)端,浏览 器下次再访问时,携带 key(cookie)
,找到对应的 session(value)
.
koa-session 的使用
const session = require('koa-session');
// 通过任意字符串为基准进行加密算法的字符串 base64
// keys 作用在cookie 的value值时加密后的内容
app.keys = ['some secret hurr'];
const CONFIG = {
key: 'koa:sess', // 设置 session的名字 也是cookie中key
maxAge: 86400000,
autoCommit: true,
overwrite: true,
httpOnly: true, // 是否允许客户端操作cookies true:不允许 false 允许
signed: true, // 数字签名,保证数据不被修改
rolling: false, // 过期时间访问顺延,指的是数据存储过期后;时候否继续加时间 false 不顺延 true 顺延
renew: false,
};
app.use(session(CONFIG, app));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
使用
//设置值
ctx.session.username = "张三";
// 获取值
ctx.session.username
2
3
4
5
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 (opens new window) 是一个开放标准(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
中间件的默认验证方式进行身份验证,中间件会进行验证成功和验证失败的分流。
// koa-jwt的默认验证方式:
{'authorization': "Bearer " + token}
2
在项目中使用
- 安装依赖
yarn add jsonwebtoken koa-jwt
- 中间件 请求验证token
// 中间件对token进行验证
app.use(async (ctx, next) => {
// 不用
// let token = ctx.header.authorization; // 解密操作
// let payload = await jwt.verify(token, customConfig.passportJwt);
return next().catch((err) => {
if (err.status === 401) {
ctx.status = 401;
ctx.body = {
code: 401,
msg: err.message
}
} else {
throw err;
}
})
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 排除不验证的请求
app.use(koajwt({ secret: SECRET }).unless({
path: [/^\/api\/login/,/^\/api\/register/] // 登录注册接口不需要验证
}));
2
3
- 登陆签发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, // 加密
// {expiresIn: '1h'} 也可以这么设置过期时间
);
ctx.body = {
code: 200,
msg: '登录成功',
token: token
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 表单 (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);
}
}
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
打开命令行窗口,运行下面的命令
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
2
3
4
5
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
})
);
2
3
4
5
6
7
8
9
10
11
12
# 文件上传
实现文件上传的中间件有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 = "上传成功!";
});
2
3
4
5
6
7
8
9
10
11
12
上传多个文件
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
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# nodemailer
发送邮件
let transporter = nodemailer.createTransport({
host: 'smtp.163.com',
service: 'smtp.163.com', // 使用了内置传输发送邮件 查看支持列表:https://nodemailer.com/smtp/well-known/
port: 465, // SMTP 端口
secureConnection: true, // 使用了 SSL
auth: { //用户信息
user: 'xxxxxx@163.com',
pass: 'xxxxxx', // smtp授权码
}
});
let title = '标题';
let mailOptions = {
from: `<feng960106@163.com>`,
to: `feng960106@163.com`, // `1 | 2 | 3` 多个邮箱
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
});
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# koa-compress
压缩数据
const Koa = require('koa');
const app = new Koa();
const compress = require('koa-compress');
app.use(compress({
//只有在请求的content-type中有gzip类型,我们才会考虑压缩
filter: function (content_type) {
return /text/i.test(content_type);
},
threshold: 1024*2, //阀值,当数据超过2kb的时候,可以压缩
flush: require('zlib').Z_SYNC_FLUSH
}));
//使用
app.use(async(ctx, next) => {
ctx.compress = true; //是否压缩数据
await next();
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 遇到的问题
# async/await后ctx.body失效
事由
在做ssr的时候,在Promise.all()里面返回的ctx.body没有值,但是能打印出来
原因
中间件在调用next()的时候 并没有把next当作一个异步函数使用。因此你在promise中异步赋值了ctx.body,但是由于next函数没有等你,在你赋值之前这一网络请求就已经完成了。
解决办法
- 使用异步的中间件
async/await和promise
- 检查你的中间件的next函数是否等待了。