一、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中,所以服务端是无法直接识别的。