简介

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

特点:

  • 轻量,无捆绑
  • 中间件架构
  • 优雅的API设计
  • 增强的错误处理

安装使用

安装

  1. # node < 7.6 版本的 Koa 中使用 async 方法需要使用 babel 处理代码
  2. npm install koa || yarn add koa

基本使用

  1. // 基本代码
  2. const Koa = require('koa');
  3. const app = new Koa();
  4. app.use(async ctx => {
  5. ctx.body = 'Hello World';
  6. });
  7. app.listen(3000);

Koa原理

一个基于nodejs的入门级http服务,类似于下面的代码:

  1. const http = require('http')
  2. const server = http.createServer((req, res)=>{
  3. res.writeHead(200)
  4. res.end('hi kaikeba')js
  5. })
  6. server.listen(3000, ()=>{
  7. console.log('Example app listening on port 3000!');
  8. })

koa的目标是用更简单化、流程化、模块化的方式实现回调部分

  1. // koa.js 封装一个自定义的 Koa 类
  2. const http = require('http')
  3. class Koa {
  4. listen(...args) {
  5. const server = http.createServer((req, res) => {
  6. this.callback(req, res)
  7. })
  8. server.listen(...args)
  9. }
  10. use(callback) {
  11. this.callback = callback
  12. }
  13. }
  14. module.exports = Koa
  15. // app.js
  16. const Koa = require("./koa");
  17. const app = new Koa();
  18. app.use((req, res) => {
  19. res.writeHead(200);
  20. res.end("Hello World");
  21. });
  22. app.listen(3000, () => {
  23. console.log('Example app listening on port 3000!');
  24. });

context

koa 为了能简化API,引入上下文 context 的概念,将原始请求对象 req 和响应对象 res 封装并挂载到 context 上,并且在 context 上设置 getter 和 setter,从而简化操作。

  1. // app.js
  2. app.use((ctx) => {
  3. ctx.body = "Hello World"
  4. });

具体实现:封装request、response和context (官方源码

  1. // request.js
  2. module.exports = {
  3. get url () {
  4. return this.req.url
  5. },
  6. get method () {
  7. return this.req.method.toLowerCase()
  8. }
  9. }
  10. // response.js
  11. module.exports = {
  12. get body () {
  13. return this._body
  14. },
  15. set body(val) {
  16. this._body = val
  17. }
  18. }
  19. // context.js
  20. module.exports = {
  21. get url () {
  22. return this.request.url
  23. },
  24. get body () {
  25. return this.response.body
  26. },
  27. set body (val) {
  28. this.response.body = val
  29. },
  30. get method () {
  31. return this.request.method
  32. }
  33. }
  34. // koa.js 中导入这三个文件
  35. const http = require('http')
  36. const context = require("./context")
  37. const request = require("./request")
  38. const response = require("./response")
  39. class Koa {
  40. listen(...args) {
  41. const server = http.createServer((req, res) => {
  42. // 构建上下文对象
  43. let ctx = this.createContext(req, res)
  44. this.callback(ctx)
  45. // 响应
  46. res.end(ctx.body)
  47. })
  48. server.listen(...args)
  49. }
  50. // 构建上下文, 把res和req都挂载到ctx之上,并且在ctx.req和ctx.request.req同时保存
  51. createContext (req, res) {
  52. const ctx = Object.create(context)
  53. ctx.request = Object.create(request)
  54. ctx.response = Object.create(response)
  55. ctx.req = ctx.request.req = req
  56. ctx.res = ctx.response.res = res
  57. // console.log(ctx)
  58. // console.log('ctx', Object.getPrototypeOf(ctx))
  59. return ctx
  60. }
  61. use(callback) {
  62. this.callback = callback
  63. }
  64. }
  65. module.exports = Koa

中间件机制

Koa中间件机制就是函数组合的概念,将一组需要顺序执行的函数复合为一个函数,外层函数的参数实际是内层函数的返回值。洋葱圈模型可以形象表示这种机制,是源码中的精髓和难点。

Koa - 图1

异步中间件

  1. // Koa 中间件的实现(核心)
  2. function compose (middlewares) {
  3. return function () {
  4. // 执行第 0 个
  5. return dispatch(0)
  6. function dispatch(i) {
  7. let fn = middlewares[i]
  8. if (!fn) {
  9. return Promise.resolve()
  10. }
  11. return Promise.resolve(
  12. fn(function next () {
  13. // promise 完成后,再执行下一个
  14. return dispatch(i +1)
  15. })
  16. )
  17. }
  18. }
  19. }
  20. async function fn1(next) {
  21. console.log('fn1')
  22. next()
  23. console.log('end fn1')
  24. }
  25. async function fn2(next) {
  26. console.log('fn2')
  27. next()
  28. console.log('end fn2')
  29. }
  30. function fn3(next) {
  31. console.log('fn3')
  32. } js
  33. const middlewares = [fn1, fn2, fn3]
  34. const finalFn = compose(middlewares)
  35. finalFn()

Koa中使用compose

  1. const http = require('http')
  2. const context = require("./context")
  3. const request = require("./request")
  4. const response = require("./response")
  5. class Koa {
  6. constructor () {
  7. this.middlewares = []
  8. }
  9. listen(...args) {
  10. const server = http.createServer(async (req, res) => {
  11. // 构建上下文对象
  12. let ctx = this.createContext(req, res)
  13. // 中间件合成
  14. const fn = this.compose(this.middlewares)
  15. // 执行合成函数并传入上下文
  16. await fn(ctx)
  17. // 响应
  18. res.end(ctx.body)
  19. })
  20. server.listen(...args)
  21. }
  22. // 构建上下文
  23. createContext (req, res) {
  24. const ctx = Object.create(context)
  25. ctx.request = Object.create(request)
  26. ctx.response = Object.create(response)
  27. ctx.req = ctx.request.req = req
  28. ctx.res = ctx.response.res = res
  29. // console.log(ctx)
  30. return ctx
  31. }
  32. use(middleware) {
  33. this.middlewares.push(middleware)
  34. }
  35. // 合成函数
  36. compose (middlewares) {
  37. return function (ctx) { // 传入上下文
  38. // 执行第 0 个
  39. return dispatch(0)
  40. function dispatch(i) {
  41. let fn = middlewares[i]
  42. if (!fn) {
  43. return Promise.resolve()
  44. }
  45. return Promise.resolve(
  46. fn(ctx, function next () { // 将上下文传入中间件,middleware(ctx,next)
  47. return dispatch(i +1)
  48. })
  49. )
  50. }
  51. }
  52. }
  53. }
  54. module.exports = Koa

中间件的实现

中间件的规范

  • 一个 async 函数
  • 接收 ctx 和 next 两个参数
  • 任务结束需要执行next
  1. const mid = async (ctx, next) => {
  2. // 来到中间件,洋葱圈左边
  3. next() // 进入其他中间件
  4. // 再次来到中间件,洋葱圈右边
  5. };

中间件常见任务

请求拦截

  1. // iptable.js
  2. module.exports = async function(ctx, next) {
  3. const { req } = ctx;
  4. const blackList = ["127.0.0.1"]
  5. const ip = getClientIP(req)
  6. if (blackList.includes(ip)) {
  7. //出现在黑名单中将被拒绝
  8. ctx.body = "not allowed"
  9. } else {
  10. await next()
  11. }
  12. }
  13. function getClientIP(req) {
  14. return (
  15. req.headers["x-forwarded-for"] || // 判断是否有反向代理 IP
  16. req.connection.remoteAddress || // 判断 connection 的远程 IP
  17. req.socket.remoteAddress || // 判断后端的 socket 的 IP
  18. req.connection.socket.remoteAddress
  19. )
  20. }
  21. // app.js
  22. app.use(require("./interceptor"));
  23. app.listen(5000, '0.0.0.0', () => {
  24. console.log("监听端口3000");
  25. });

日志

  1. // app.js
  2. // 利用koa的洋葱模型打印访问日志
  3. app.use(async (ctx, next) => {
  4. let start = new Date().getTime()
  5. console.log(`start ${ctx.url}`)
  6. next()
  7. let end = new Date().getTime()
  8. console.log(`请求耗时:${end - start} ms`)
  9. })

静态文件服务

  • 配置绝对资源目录地址,默认为static
  • 获取文件或者目录信息
  • 静态文件读取
  • 返回
  1. // static.js
  2. const fs = require("fs");
  3. const path = require("path");
  4. module.exports = (dirPath = "./public") => {
  5. return async (ctx, next) => {
  6. if (ctx.url.indexOf("/public") === 0) {
  7. // public开头 读取文件
  8. const url = path.resolve(__dirname, dirPath);
  9. const filepath = url + ctx.url.replace("/public", "");
  10. try {
  11. stats = fs.statSync(filepath);
  12. console.log(stats)
  13. if (stats.isDirectory()) {
  14. const dir = fs.readdirSync(filepath);
  15. const ret = ['<div style="padding-left:20px">'];
  16. dir.forEach(filename => {
  17. // 简单认为不带小数点的格式,就是文件夹,实际应该用statSync
  18. if (filename.indexOf(".") > -1) {
  19. ret.push(
  20. `<p><a style="color:black" href="${
  21. ctx.url
  22. }/${filename}">${filename}</a></p>`
  23. );
  24. } else {
  25. // 文件
  26. ret.push(`<p><a href="${ctx.url}/${filename}">${filename}</a></p>`);
  27. }
  28. });
  29. ret.push("</div>");
  30. ctx.body = ret.join("");
  31. } else {
  32. const content = fs.readFileSync(filepath);
  33. ctx.body = content;
  34. }
  35. } catch (e) {
  36. // 报错了 文件不存在
  37. ctx.body = "404, not found";
  38. }
  39. } else {
  40. // 否则不是静态资源,直接去下一个中间件
  41. await next();
  42. }
  43. };
  44. };
  45. // app.js
  46. const static = require('./static')
  47. app.use(static(__dirname + '/public'));

路由

  1. // router.js
  2. class Router {
  3. constructor() {
  4. this.stack = [];
  5. }
  6. register(path, methods, middleware) {
  7. let route = {path, methods, middleware}
  8. this.stack.push(route);
  9. }
  10. // 现在只支持get和post,其他的同理
  11. get(path,middleware){
  12. this.register(path, 'get', middleware);
  13. }
  14. post(path,middleware){
  15. this.register(path, 'post', middleware);
  16. }
  17. routes() {
  18. let stock = this.stack;
  19. return async function(ctx, next) {
  20. let currentPath = ctx.url;
  21. let route;
  22. for (let i = 0; i < stock.length; i++) {
  23. let item = stock[i];
  24. if (currentPath === item.path && item.methods.indexOf(ctx.method) >= 0) {
  25. // 判断path和method
  26. route = item.middleware;
  27. break;
  28. }
  29. }
  30. if (typeof route === 'function') {
  31. route(ctx, next);
  32. return;
  33. }
  34. await next();
  35. };
  36. }
  37. }
  38. module.exports = Router;

router.routes()的返回值是一个中间件,由于需要用到method,所以需要挂载method到ctx之上

  1. // request.js
  2. module.exports = {
  3. get method(){
  4. return this.req.method.toLowerCase()
  5. }
  6. }
  7. // context.js
  8. module.exports = {
  9. get method() {
  10. return this.request.method
  11. }
  12. }

测试

  1. // app.js
  2. const Koa = require('./source/koa')
  3. const Router = require('./source/router')
  4. const router = new Router()
  5. router.get('/index', async ctx => {
  6. ctx.body = "index page"
  7. })
  8. router.get('/post', async ctx => {
  9. ctx.body = "post page"
  10. })
  11. router.get('/list', async ctx => {
  12. ctx.body = "list page"
  13. })
  14. app.use(router.routes())