一、Koa2安装
创建一个空白目录,然后进入终端,并在终端对koa进行安装:
1 2 3 4 5
| # 项目初始化 npm init -y
# 安装koa2 npm i koa2 -S
|
二、入口文件
在项目根目录创建 index.js
文件,并在上一步操作中生成的 package.json
里配置:
1 2 3 4 5 6
| { "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node index.js" }, }
|
基本使用
- app.use
- use 传入的中间件被放入一个middleware 缓存队列中(数组),这个队列会经由
koa-compose
进行串联
- 会返回自身this,可以链式调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const Koa = require('koa2'); const app = new Koa(); const port = 9000;
app.use(async (ctx)=>{ ctx.body = "Hello, Koa"; })
app.listen(port, ()=>{ console.log('Server is running at http://localhost:'+port); })
|
然后运行 npm start
,并在浏览器输入 http://localhost:9000/
即可看到页面效果。
三、洋葱模型
学Koa必须要了解 洋葱模型
:
Koa
和 Express
都会使用到中间件,Express的中间件是顺序执行,从第一个中间件执行到最后一个中间件,发出响应:
Koa是从第一个中间件开始执行,遇到 next
进入下一个中间件,一直执行到最后一个中间件,在逆序,执行上一个中间件 next
之后的代码,一直到第一个中间件执行结束才发出响应。
对于这个洋葱模型,我们用代码来解释一下。假如把上面的代码改写成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const Koa = require('koa2'); const app = new Koa(); const port = 9000;
app.use(async (ctx, next)=>{ console.log(1) await next(); console.log(1) })
app.use(async (ctx, next)=>{ console.log(2) await next(); console.log(2) })
app.use(async (ctx)=>{ console.log(3) })
app.listen(port, ()=>{ console.log('Server is running at http://localhost:'+port); })
|
那么在浏览器刷新后,控制台得到的顺序是:
现在可以看到,我们通过 next
可以先运行下个中间件,等中间件结束后,再继续运行当前 next()
之后的代码。
四、路由基本使用
当需要匹配不同路由时,可以安装:
将 index.js
修改:
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
| const Koa = require('koa2'); const app = new Koa();
const Router = require('koa-router');
const router = new Router();
const port = 9000;
router.get('/', async (ctx)=>{ ctx.body = "首页"; })
app.use(router.routes()).use(router.allowedMethods());
app.listen(port, ()=>{ console.log('Server is running at http://localhost:'+port); })
|
此时,到浏览器刷新并在地址栏最后添加 /list
即可得到首页和列表页。
备注:
1 2 3 4 5 6 7 8 9
|
allowedMethods方法可以做以下配置: app.use(router.allowedMethods({ }))
|
路由解析
GET请求参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
router.get('/user/:id', (ctx)=> { const res = {...ctx.params} ctx.body = res })
router.get('/user2', (ctx)=> { const res = {...ctx.query} console.log('res',res); console.log('\n') ctx.body = res })
|
POST请求参数
1 2 3
| const {koaBody} = require('koa-body') app.use(koaBody())
|
1 2 3 4 5 6 7 8
|
router.post('/post/a',(ctx)=> { const req = ctx.request.body ctx.body = req })
|
koa-body 与 koa-bodyparse的区别
koa-body和koa-bodyparser都是Koa框架的中间件,用于解析HTTP请求中的请求体。
koa-body
解析的是JSON格式的请求体
koa-bodyparser
支持多种请求体的格式,例如JSON、form、text、CSV等
五、路由拆分
有时候我们需要拆分路由,比如:
列表页下所有的子路由(即前端请求的api)与首页所有的子路由想分开处理,那么就需要拆分路由。
1、创建 router
文件夹
创建router文件夹,并在其中创建:index.js
(路由总入口文件)、home.js
(首页总路由文件)、list.js
(列表页总路由文件):
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
| # index.js const router = require('./router/index') app.use(router.routes(), router.allowedMethods());
# index.js const Router = require('koa-router'); const router = new Router(); const home = require('./home') const list = require('./list')
router.use('/home', home.routes(), home.allowedMethods()); router.use('/list', list.routes(), list.allowedMethods());
module.exports = router;
# home.js const Router = require('koa-router'); const home = new Router();
home.get('/', async (ctx) => { ctx.body = "首页"; })
module.exports = home;
# list.js const Router = require('koa-router'); const list = new Router();
list.get('/', async (ctx)=>{ ctx.body = "列表页"; })
module.exports = list;
|
到浏览器刷新 localhost:9000/home
与 localhost:9000/list
即可得到首页与列表页。
2、路由重定向
那么有同学会问了,如果我想直接从 localhost:9000
重定向到 localhost:9000/home
该怎么办?
我们可以在 router/index.js
中做如下配置:
1 2 3
| router.use('/home', home.routes(), home.allowedMethods()); ... router.redirect('/', '/home');
|
3、404无效路由
如果被访问到无效路由,那么我们可以统一返回404页面:
在 router
下 errorPage.js
:
1 2 3 4 5 6 7 8
| const Router = require('koa-router'); const errorPage = new Router();
errorPage.get('/', async (ctx) => { ctx.body = "访问页面不存在"; })
module.exports = errorPage;
|
在 index.js
中引用:
1 2 3 4 5 6 7 8
| app.use(async (ctx, next) => { await next(); if (parseInt(ctx.status) === 404) { ctx.response.redirect("/404") } }) app.use(router.routes(), router.allowedMethods());
|
六、统一异常处理(可选)
方法一
1. 非原生异常处理
- 404 : 请求资源找不到,或者
ctx.body
为空
- 500: 服务器运行时错误❌
ctx.throw()
手动抛出 或者直接throw()
1 2 3
| ctx.throw([status], [msg], [properties])
|
1 2 3 4 5 6
|
const error = require('koa-json-error') app.use(error())
|
* 生产环境避免抛出stack
1 2 3 4 5 6
| "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node index.js", "prod": "cross-env NODE_ENV=production node src/" },
|
1 2 3 4 5 6 7 8 9
| app.use(error({
postFormat: (err, obj)=> { const {stack, ...rest} = obj return process.env.NODE_ENV == 'production'? rest: obj } }))
|
2. 原生异常处理
1 2 3 4 5 6
|
app.on('error', (error,ctx)=> { console.error(error); ctx.body = error })
|
1 2 3
| ctx.app.emit('error',{code: 404,message: 'resouse not found', },ctx)
ctx.throw(402)
|
方法二
作为后端开发,我们经常需要统一异常处理,避免每次都要自己手写404或200进行返回,因此我们可以创建 utils/errorHandler.js
:
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
| module.exports = (app) => { app.use(async (ctx, next) => { let status = 0; let fileName = ""; try{ await next(); status = ctx.status; }catch(err){ status = 500; } if(status >= 400){ switch(status){ case 400: case 404: case 500: fileName = status; break; default: fileName = "other"; break; } } ctx.response.status = status; console.log(fileName); }); }
|
然后在 index.js
中引入:
1 2 3 4 5
| const errorHandler = require('./utils/errorHandler.js');
app.use(router.routes(), router.allowedMethods()); ... errorHandler(app);
|
其实这一块不写关系也不大,但最好还是加上。
七、操作mysql函数封装
这里已经给大家直接封装好了一个库,专门用来操作mysql的。至于mysql的学习,将在独立的mysql教程中呈现。
首先,项目内安装 mysql
:
在/src/utils/db.js
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
| const mysql = require('mysql')
var pool = mysql.createPool({ host: 'localhost', port: 3306, database: 'mytest', user: 'root', password: '252238Lzy', multipleStatements: true })
let query = function (sql, callback) { return new Promise((resolve, reject) => { pool.getConnection(function (err, connection) { if (err) reject(err) else connection.query(sql, function (err, rows) { if (err) reject(err) else resolve(rows) connection.release() }) }) }) }
module.exports = query
|
在 /src/sql/userSql.js
1 2 3 4 5 6 7 8 9 10
| const query = require('../utils/db')
let findUser = function (id) { let _sql = `select * from users where id=${id}`; return query(_sql) }
module.exports = { findUser }
|
八、后端允许跨域
前端想跨域,可以设置proxy。如果后端允许跨域,可以如下操作:
1 2 3 4 5
| cnpm i koa2-cors
const cors = require('koa2-cors') app.use(cors())
|
九、读取静态资源文件
首先,在项目的根目录下创建 assets
后,将图片资源文件夹 images
放到其中,并且执行以下操作:
1 2 3 4 5 6 7 8 9 10 11
| cnpm install koa-static
const path = require('path') const static = require('koa-static')
app.use(static(path.join(__dirname+'/assets'))); ... app.use(router.routes(), router.allowedMethods())
|
假设其中有一张图片叫做 banner1.png
,那么我们打开浏览器,访问:http://localhost:5050/images/banner1.png
即可得到图片。这里注意:
路径上不需要写assets,因为我们已经指定了访问资源时, http://localhost:5050 自动指向 assets 文件夹。
由此,我们知道数据库中图片的地址只需要填写 /images/banner1.png
即可。
十、mysql录入数据
请参考当前目录下的《Chapter2-mysql2操作.md》。
十一、POST请求
我们以登录举例讲post请求。
这里规定:前端发送 账号+密码 到后端,如果账号不存在于数据库,则注册账号。
如果账号存在于数据库中,则验证密码。
验证密码通过或注册账号成功,都返回token给前端。
1、建表
设定字段为account和pwd
1 2 3 4 5 6
| create table users ( id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, account VARCHAR(20) NOT NULL COMMENT '账号', pwd VARCHAR(20) NOT NULL COMMENT '密码', token LONGTEXT NOT NULL COMMENT '令牌' );
|
在 assets
下创建 index.html
:
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
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <label for="account">账号</label> <input type="text" value="" name="account" class="account" placeholder="请输入账号" /> <br><br> <label for="pwd">密码</label> <input type="password" value="" name="pwd" class="pwd" placeholder="请输入密码" /> <br><br> <button class="btn">登录/注册</button> </body> </html> <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script> <script> $('.btn').click(()=>{ $.ajax({ url: "/login/register", method: "POST", data: { account: $('.account').val(), pwd: $('.pwd').val() }, success(res){ console.log(res) }, error(err){ console.log(err) } }) }) </script>
|
在浏览器直接访问 http://localhost:5050/index.html
即可进入表单页。
3、安装中间件
安装 koa-bodyparser
与 jsonwebtoken
中间件:
1 2 3 4 5
| cnpm install koa-bodyparser --save
cnpm install jsonwebtoken --save
|
JWT
认证流程
前端把表单数据发送给服务器(同过post+http,最好是SSL+https)
服务器把token当作登录成功的返回
浏览器把token存在localStorage
或者sessionStorage
或者cookie
中,退出登录时让浏览器删除token
- 以后每次进入页面(main.js中)先从本地读取token,若存在则修改vuex中登陆状态
前端在每次请求时将token放入http Header中的Authorization位。
后端检查:签名正确,token是否过期
后端解析出token中包含的用户id来进行操作
注意
jwt
生成的token由 head.payload.signature
组成
- header: 保存签名算法
- payload: 存放数据
- signature:根据header、payload、密钥加密得出
- 密钥存在服务器中,只有服务器知道
基本使用
1 2 3 4 5 6 7
| const jwt = require('jsonwebtoken')
const token = jwt.sign({ account }, private_key [, { expiresIn: '30s' }, callback])
const info = jwt.verify(token, private_key)
|
4、添加post接口
在 router/login.js
中加入:
1 2 3 4 5 6 7 8
| const bodyParser = require('koa-bodyparser')
login.use(bodyParser());
login.post('/register', async (ctx)=>{ console.log(ctx.request.body); ctx.response.body = "登录或注册" })
|
5、登录与自动注册
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| const Router = require('koa-router') const login = new Router() const bodyParser = require('koa-bodyparser') const db = require('../utils/db') const jwt = require('jsonwebtoken')
login.get('/', async (ctx) => { ctx.response.body = "登录页面" })
login.use(bodyParser());
login.post('/register', async (ctx) => { let myaccount = ctx.request.body.account; let mypwd = ctx.request.body.pwd; let sql = `SELECT * FROM users WHERE account='${myaccount}'`; let result = await new Promise((resolve, reject) => { return db.query(sql, (err, data) => { if (err) throw err; if (data.length > 0) { resolve(data); } else { resolve(false); } }) }) if (result) { if (result[0].pwd == mypwd) { ctx.body = { token: result[0], msg: '登录成功', account: myaccount }; } else { ctx.body = { msg: '密码错误', account: myaccount }; } } else { let result1 = await new Promise((resolve, reject) => { const token = jwt.sign({ myaccount: myaccount, mypwd: mypwd }, 'secret', { expiresIn: 3600 }) return db.query(`INSERT INTO users (account, pwd, token) values ('${myaccount}', '${mypwd}', '${token}')`, (error, datas) => { if (error) throw error; let obj = { token, msg: '登录成功', account: myaccount } resolve(obj) }) }) if (result1) { ctx.body = result1; } } })
module.exports = login;
|
此时,前端做这个post请求后,就会得到相应的数据。
十二、部署到服务器上
部署需要先购买服务器,下载filezilla软件。
服务器上需要安装node 、mysql、pm2
具体教程请参考下面这篇文章
https://blog.csdn.net/yh8899abc/article/details/105201742
十三、 koa与express
主要优点:
- koa解决了回调地狱问题
- 拥抱async/await
axios
需要封装部分:
1. 基本全局配置: baseURL, timeout 等等
2. Token
3. 响应统一处理(错误处理)
4. 封装请求方法: 对接口的请求封装为一个个函数
1 2 3
| axios.get().then(res=> { })
|
拦截器
- 判断页面需不需要token => 通过vue路由守卫
- 向服务器发送该页面相关的数据请求
- 服务器验证token的正确性
- 正确 => 返回该用户数据
- 错误 => 返回401状态码,前端检测状态码跳转回登陆页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| axios.interceptors.request.use(config => { console.log('请求拦截成功',config); throw 'throw了一个错误' }, error=> { console.log("请求拦截失败",error) })
axios.interceptors.response.use(response=> { console.log('响应拦截成功',response) }, error=> { console.log("响应拦截失败", error) })
const myInterceptor = axios.interceptors.request.use(function(){}) axios.interceptors.request.eject(myInterceptor)
|
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| axios.interceptors.request.use(config => { console.log('请求拦截器成功',config); const token = store.state.token if (token) config.headers['Authorization'] = token else if(config.url=='http://localhost:9000/profile'){ router.replace({ name: 'login' }) console.log('拦截-没有token,重定向回login'); return false } console.log('请求数据',config.data) return config;
}, error => { console.log('请求拦截器失败',error); return Promise.reject(error) })
axios.interceptors.response.use(response => { console.log("请求拦截器成功",response) if (response.status === 200 || response.code === 200) { return Promise.resolve(response) } else { return Promise.reject(response) } }, error => { console.log('响应拦截器失败',error); let status = error.status || error.code
if (status) { switch (status) { case 401: router.replace({ path: '/login', name: 'login', params: { message: '请重新登录', redirect: router.currentRoute.fullPath } }); break; case 403: alert('登录过期请重新登录') localStorage.removeItem('token') store.commit('loginSuccess') router.replace({ path: '/login', name: 'login', params: { redirect: router.currentRoute.fullPath } }) break; case 404: alert('404请求不存在') break; default: alert('响应拦截器拦截失败') break; } }
})
|
请求取消
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
| let cancel = null; axios.get('url',{ params: {}, cancelToken: new axios.CancelToken(c=>{ cancel = c; }) }).then(res=> { cancel = null; }).catch(err){ console.log(err) }
cancel();
const controller = new AbortController(); axios.get('url',{ signal: controller.signal }).catch(err){ console.log(err) } controller.abort()
|
post方法序列化
序列化,就是将对象 序列化成URL的形式,以&进行拼接。
为什么? because:提交时候是直接以原始数据格式存储在body中的,而不是以键值对的形式附加到url中,所以服务端是无法直接识别的。