# 1. KOA

浏览器发送请求才会触发中间件函数

koa只会帮你自动执行第一个中间件,剩下的都要由开发者自己来调用

const koa = require('koa')
const app = new Koa()

function test() {
  console.log('__')
}

app.use(test) // 浏览器发送请求才会触发test函数
app.use(() => {
  console.log('haha')
  // koa只会帮你自动执行第一个中间件,剩下的都要由开发者自己来调用
})

app.listen(3000)

想要调用下一个中间件函数,在前一个中间件函数中调用next()(中间函数有两个参数ctxnext,其中ctx就是上下文,next就是下一个中间件函数)

app.use((ctx, next) => {
  // ctx:上下文,next:下一个中间件函数
  console.log('haha')
  next() // 在第一个中间件函数中调用下一个中间件函数
})

# 1.1. 什么是中间件?

const logger = (ctx, next) => {
  console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`);
  next();
}
app.use(logger);

像上面代码中的logger函数就叫做"中间件"(middleware),因为它处在 HTTP RequestHTTP Response 中间,用来实现某种中间功能。app.use()用来加载中间件。

基本上,Koa 所有的功能都是通过中间件实现的,前面例子里面的main也是中间件。每个中间件默认接受两个参数,第一个参数是 Context 对象,第二个参数是next函数。只要调用next函数,就可以把执行权转交给下一个中间件。

# 1.2. 洋葱模型

中间件打印顺序:1 3 4 2

app.use((ctx, next) => {
  console.log(1)
  next()
  console.log(2)
})

app.use((ctx, next) => {
  console.log(3)
  next()
  console.log(4)
})

洋葱模型 中间件只在应用程序启动时初始化一次,validator不容易做出中间件

# 1.3. async 和 await

中间件要加上 asyncawait,异步模型,如果不加上asyncawait很难保证所有的中间件函数都按照洋葱模型来执行(万无一失)。

app.use(async (ctx, next) => {
  console.log(1)
  next()
  console.log(2)
})

next()方法返回的是 Promise

app.use((ctx, next) => {
  const a = next()
  console.log(a)  // Promise { 'abc' }
})

app.use((ctx, next) => {
  next()
  return 'abc'
})

想要获取具体的值,而不是Promise,可以用then方法

app.use((ctx, next) => {
  const a = next()
  console.log(a)      // Promise { 'abc' }
  a.then(res => {
    console.log(res)  // 'abc'
  })
})

app.use((ctx, next) => {
  next()
  return 'abc'
})

也可以用asyncawaitawait对返回的Promise进行求值

app.use(async (ctx, next) => {
  const a = await next()
  console.log(a)      // 'abc'
})

app.use((ctx, next) => {
  next()
  return 'abc'
})

await不仅仅可以求Promise的值,也可以拿到表达式的值

app.use(async (ctx, next) => {
  const a = await 100 * 100
  console.log(a)      // 10000
})

资源的操作都是异步的(包括读取文件),以及发送 http 请求、操作数据库

await阻塞线程,等待异步结果的返回,把异步代码变成同步的,因为当前代码被阻塞住了,必须等待结果返回后才能继续

app.use(async (ctx, next) => {
  const start = Date.now()
  await axios.get('http://www.baidu.com')
  const end = Date.now()
  console.log(end - start) // 不加 async 和 await 时接近于0,说明没有阻塞
})

# 1.4. await的两个作用:

  1. 表达式或Proimse求值

  2. 阻塞线程,等待异步结果返回(切换线程,去做异步任务)

# 1.5. async意义:函数返回值都是Promise(返回值包装为Promise

async function fn() {
  return 'abc'
}
console.log(fn()) // Promise { 'abc' }

Koa内部处理了,中间件不加async依然会返回Promise,这里加上async主要是因为里面使用了await,不加async的话会报错。(面试)

加上asyncawait是为了保证洋葱模型,比如下面第一个中间件没加asyncawait,打印顺序为1324

app.use((ctx, next) => {
  console.log(1)
  next()
  console.log(2)
})

app.use(async (ctx, next) => {
  console.log(3)
  await axios.get('http://www.baidu.com')
  next()
  console.log(4)
})

# 1.6. 为什么一定要保证洋葱模型?

有些功能需要让下面的中间件执行完才能进行,比如计时功能,写在next()后面,当next执行完,说明下面的代码都执行完了。

# 1.7. 为什么要调用await next()

原因是koa把很多async函数组成一个处理链,每个async函数都可以做一些自己的事情,然后用await next()来调用下一个async函数。我们把每个async函数称为middleware,这些middleware可以组合起来,完成很多有用的功能。

# 1.8. 中间件传递参数

中间件传递参数可以利用ctx, 挂载在ctx上面,一定要加上asyncawait,以及写在next()后面保证后面代码执行完。

app.use(async (ctx, next) => {
  await next()
  const res = ctx.res // 传递来的参数
  console.log(res)
})

app.use(async (ctx, next) => {
  const res = await axios.get('http://www.baidu.com')
  ctx.res = res
  await next()
})

# 1.9. 路由

ctx.pathrequest.path等效,以及ctx.method

app.use(async (ctx, next) => {
  if (ctx.path === '/1234' && ctx.method === 'GET') { // 不能是小写的 get
    return 'ccc' // 直接返回不行
    ctx.body = { key: 'ccc' }  // 需要写在 ctx.body 上
  }
})
  • 不能是小写的get
  • 不能直接return返回,需写在ctx.body
  • Koa自动将对象形式转为JSON返回

# 1.10. 使用koa-router

const Router = require('koa-router')
const router = new Router()

router.get('/home', (ctx, next) => {
  ctx.body = { cc: 'happy' } 
})
router.post('/home', (ctx, next) => {
  // ...
})
app.use(router.routes())
app.listen(3000)

REST中get代表查询,post代表新增,put代表更新,delete代表删除

# 1.11. 版本号

  • 版本号可以放在路径中、查询参数中或者**header**中
  • 最好的方法是在api文件夹下分别新建v1v2等文件夹,放置不同版本的api文件。
  • 不能用ifif对扩展开放对修改关闭,所以不能新增功能就修改原来的代码

全局可以有很多个router实例,所以book.js中:

const Router = require('koa-router')
const router = new Router()

router.get('/v1/book/latest', (ctx, next) => {
  ctx.body = {
    key: 'book'
  }
})

module.exports = router

调用的时候,app.js

const book = require('./api/v1/book')
const classic = require('./api/v1/classic')

const app = new Koa()

app.use(book.routes())
app.use(classic.routes())

app.listen(3000)

注意:上层调用下层,下层调用上层可能会导致循环引用。

CommonJS 导出可以加大括号,也可以不加,加的话导入的时候也要加,不加的话导入的时候不要加(本质上是ES6的解构赋值)。

module.exports = { router }
const { router } = require('router')

// 或者
module.exports = router
const router = require('router')

# 1.12. nodemon

全局安装 nodemonnpm install nodemon -g ,不加-g需要npx nodemon来启动nodemon,因为是在命令行中输入nodemon来调用,所以要么写在package.json脚本里,要么npx nodemon,要么就全局安装

# 1.13. require-directory

requireDirectory实现路由自动,每当requireDirectory导入一个模块,就执行这个函数,检测导出的是否是router,是的话就app.use()

const requireDirectory = require('require-directory')

requireDirectory(module, './api', { visit: whenLoadModule })

function whenLoadModule(obj) {
  if (obj instanceof Router) {
    app.use(obj.routes())
  }
}

# 1.14. 获取参数

传参有四种方式,下面两种加上headerbody

/v1/book/{param}/latest?param=

使用koa-bodyparse解析body参数

const parser = require('koa-bodyparser')
app.use(parser())
router.get('/v1/:id/book/latest', (ctx, next) => {
  const path = ctx.params  // 取到url里的id
  const query = ctx.request.query  // ?param=
  const header = ctx.request.header  // 类似token
  const body = ctx.request.body  // 需要引入 koa-bodyparser
})

注意: url和问号?params=1传递的参数都是字符串,而bodyjson可以记录数据类型。

# 1.15. body-parser

bodyparser 用来解析post的请求取代了原生的 req.on 的方式 但是只能取到ajax和表单的数据 ,取不到上传的文件类型。

对请求体的四种解析方式:

  1. bodyParser.json(options): 解析json数据
  2. bodyParser.raw(options): 解析二进制格式(Buffer流数据)
  3. bodyParser.text(options): 解析文本数据
  4. bodyParser.urlencoded(options): 解析UTF-8的编码的数据(表单)。
# 1.15.0.1. bodyParser 解析json数据

var bodyParser = require('body-parser') bodyParser变量是对中间件的引用。请求体解析后,解析值都会被放到req.body属性,内容为空时是一个{}空对象。

# 1.16. koa-body作用

Web 应用离不开处理表单。本质上,表单就是 POST 方法发送到服务器的键值对。koa-body模块可以用来从 POST 请求的数据体里面提取键值对。

  • 不使用的话,this.request.bodyundefined;
  • 使用的话,this.request.body{}

# 1.17. koa-body上传文件注意事项

  • filectx.request.files.file中,不在ctx.request.body
  • 请求头Content—Type,为multipart/form-data

# 1.18. 异常处理

异常处理应该throw,而不是return null,让调用它的函数能捕捉到,让用户或开发者知道错误。

单纯的try catch捕捉不到异步错误,需要用asyncawaitawait等待异步结果,所以能捕获异常,前提是调用的函数返回的是Promise :

function f1() {
  f2()
}

async function f2() {
  try {
    await f3()
  } catch (e) {
    console.log('error')
  }
}

function f3() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const res = Math.random()
      if (res < 0.999) {
        reject('err') // 产生异步异常
      } else{
        resolve(res)
      }
    }, 1000)
  })
}

f1()

一旦throw Error就不会继续向下执行了。

# 1.19. 全局异常处理中间件

  1. 监听错误
  2. 返回有意义的信息
  3. 然后在app.js中使用这个中间件app.use(catchError)
const catchError = async (ctx, next) => {
  try {
    await next()
  } catch(e) {
    ctx.body = 'error'
  }
}

module.exports = catchError

# 1.20. KOA数据流

用户 => API => Model => MySQL => KOA => 用户

# 1.21. koa-static

Koa静态资源使用koa-static中间件 然后在浏览器中http://localhost:3000/1.jpg就能显示出来了

# 1.21.1. 静态资源存储方案(如图片、js、css、html,特点是消耗流量)

  1. 网站目录,缺点污染api
  2. 专门的静态资源服务器,或者微服务,带宽足够
  3. 云服务, OSS 贵、 (ECS、RDS、OSS、 CDN
  4. github gitpage