Skip to content

Node-Express、Koa2

[TOC]

Express

Express 初体验

认识 Web 框架

前面我们已经学习了使用 http 内置模块来搭建 Web 服务器,为什么还要使用框架?

  • 原生 http 在进行很多处理时,会较为复杂;
  • 有 URL 判断、Method 判断、参数处理、逻辑代码处理等,都需要我们自己来处理和封装;
  • 并且所有的内容都放在一起,会非常的混乱;

目前在 Node 中比较流行的 Web 服务器框架是 express、koa;

  • 我们先来学习 express,后面再学习 koa,并且对他们进行对比;

express 早于 koa 出现,并且在 Node 社区中迅速流行起来:

  • 我们可以基于 express 快速、方便的开发自己的 Web 服务器;
  • 并且可以通过一些实用工具和中间件来扩展自己功能;

express 的安装

express 的使用过程有两种方式:

  • 方式一:通过 express 提供的脚手架,直接创建一个应用的骨架;
  • 方式二:从零搭建自己的 express 应用结构;

方式一:安装 express-generator

sh
npm install -g express-generator

创建项目:

sh
express express-demo

项目目录如下:

sh
├── app.js
├── bin
   └── www
├── package-lock.json
├── package.json
├── public
   ├── images
   ├── javascripts
   └── stylesheets
       └── style.css
├── routes
   ├── index.js
   └── users.js
└── views
    ├── error.jade
    ├── index.jade
    └── layout.jade

我们可以安装依赖,将程序跑起来:

sh
npm install
node bin/www

方式二:从零学习搭建

刚才创建的项目 express 项目,很多内容可能我们并不认识,所以刚开始我们最好从零来学习。

初始化一个新的项目

sh
npm init -y

express 的安装:

  • 目前最新的 release 版本是 4.17.1,我们使用该版本;
sh
npm install express

express 初体验

我们来创建自己的第一个 express 程序:

js
const express = require('express')

// 创建服务器
const app = express()

// /home的get请求处理
app.get('/home', (req, res) => {
  res.end('Hello Home')
})

// /login的post请求处理
app.post('/login', (req, res) => {
  res.end('Hello Login')
})

// 开启监听
app.listen(8000, () => {
  console.log('服务器启动成功~')
})

我们会发现,之后的开发过程中,可以方便的将请求进行分离:

  • 无论是不同的 URL,还是 get、post 等请求方式;

  • 这样的方式非常方便我们已经进行维护、扩展;

  • 当然,这只是初体验,接下来我们来探索更多的用法;

请求和响应

请求的路径中如果有一些参数,可以这样表达:

  • /users/:userId
  • request对象中要获取可以通过 req.params.userId;

返回数据,我们可以方便的使用 json:

js
const express = require('express')

const app = express()

app.get('/users/:userId', (req, res, next) => {
  console.log(req.params.userId)
  res.json({ username: 'coderwhy', level: 99 })
})

app.listen(8000, () => {
  console.log('静态服务器启动成功~')
})

Express 中间件

认识中间件

Express 是一个路由和中间件的 Web 框架,它本身的功能非常少:

  • Express 应用程序本质上是一系列中间件函数的调用;

中间件是什么呢?

  • 中间件的本质就是传递给 express 的一个回调函数;

  • 这个回调函数接受三个参数:

    • 请求对象(request 对象);
    • 响应对象(response 对象);
    • next 函数(在 express 中定义的用于执行下一个中间件的函数);

中间件中可以执行哪些任务呢?

  • 执行任何代码;
  • 更改请求(request)和响应(response)对象;
  • 结束请求-响应周期(返回数据);
  • 调用栈中的下一个中间件;

如果当前中间件功能没有结束请求-响应周期,则必须调用 next()将控制权传递给下一个中间件功能,否则,请求将被挂起。

中间件函数调用的元素:

image-20240719155910147

应用中间件

那么,如何将一个中间件应用到我们的应用程序中呢?

  • express 主要提供了两种方式:app/router.useapp/router.methods
  • 可以是 app,也可以是 router,router 我们后续再学习:
  • methods 指的是常用的请求方式,比如:app.get或app.post等;

我们先来学习 use 的用法,因为 methods 的方式本质是 use 的特殊情况;

案例一:最普通的中间件

之所以称之为最普通的中间件,是因为无论是什么 path、methods 都会应用该中间件;

js
const express = require('express')

const app = express()

app.use((req, res, next) => {
  console.log('common middleware 01')
  next()
})

app.use((req, res, next) => {
  console.log('common middleware 02')
  res.end('Hello Common Middleware~')
})

app.listen(8000, () => {
  console.log('中间件服务器启动成功~')
})

中间件的执行顺序:在匹配上的情况下,中间件按照注册的顺序执行;

案例二:path 匹配中间件

如果我们希望匹配一个明确的路径,也可以使用 use 方法:

js
// 案例二: 路径匹配中间件
app.use('/home', (req, res, next) => {
  console.log('home middleware 01')
  next()
})

app.use('/home', (req, res, next) => {
  console.log('home middleware 02')
  next()
  res.end('Hello Home middleware')
})

app.use((req, res, next) => {
  console.log('common middleware')
})

案例三:path 和 method 匹配中间件

js
// 案例三: method匹配中间件
app.get('/home', (req, res, next) => {
  console.log('home get middleware')
  next()
})

app.post('/login', (req, res, next) => {
  console.log('login post middleware')
  next()
})

app.use((req, res, next) => {
  console.log('common middleware')
})

案例四:注册多个中间件

js
// 案例四: 注册多个中间件
const homeMiddleware1 = (req, res, next) => {
  console.log('home middleware 01')
  next()
}

const homeMiddleware2 = (req, res, next) => {
  console.log('home middleware 02')
  next()
}

const homeHandle = (req, res, next) => {
  res.end('Hello Home~')
}

app.get('/home', homeMiddleware1, homeMiddleware2, homeHandle)

应用其他中间件

并非所有的中间件都需要我们从零去编写:

  • express 有内置一些帮助我们完成对 request 解析的中间件;
  • registry 仓库中也有很多可以辅助我们开发的中间件;
解析请求
  • express.json()
  • express.urlencoded()

在客户端发送 post 请求时,会将数据放到 body 中:

  • 客户端可以通过 json 的方式传递;
  • 也可以通过 form 表单的方式传递;

我们这里先使用 json 传递给服务器 body:

image-20240719155926918

不进行解析时的操作:

js
app.post('/login', (req, res, next) => {
  req.on('data', (data) => {
    console.log(data.toString())
  })
  req.on('end', () => {
    res.end('登录成功~')
  })
})

我们也可以自己编写中间件来解析 JSON:

js
app.use((req, res, next) => {
  if (req.headers['content-type'] === 'application/json') {
    req.on('data', (data) => {
      const userInfo = JSON.parse(data.toString())
      req.body = userInfo
    })
    req.on('end', () => {
      next()
    })
  } else {
    next()
  }
})

app.post('/login', (req, res, next) => {
  console.log(req.body)
  res.end('登录成功~')
})

但是,事实上我们可以使用 expres 内置的中间件或者使用body-parser来完成:

js
app.use(express.json())

app.post('/login', (req, res, next) => {
  console.log(req.body)
  res.end('登录成功~')
})

如果我们解析的是 application/x-www-form-urlencoded

form 传递 body

image-20240719155943157

我们可以使用 express 自带的 urlencoded函数来作为中间件:

  • 传入的 extended 用于表示使用哪一种解析方式:

    • true:使用 qs 第三方模块;
    • false:使用 querystring 内置模块;
    • 备注:它们之间的区别这里不展开讲解;
js
app.use(express.json())
app.use(express.urlencoded({ extended: true }))

app.post('/login', (req, res, next) => {
  console.log(req.body)
  res.end('登录成功~')
})
包:morgan

如果我们希望将请求日志记录下来,那么可以使用 express 官网开发的第三方库:morgan

安装 morgan:

sh
npm install morgan

直接作为中间件使用即可:

js
const loggerWriter = fs.createWriteStream('./log/access.log', {
  flags: 'a+'
})
app.use(morgan('combined', { stream: loggerWriter }))
包:multer

文件上传我们可以使用 express 官方开发的第三方库:multer

sh
npm install multer

上传文件,并且默认文件名:

js
const upload = multer({
  dest: 'uploads/'
})

app.post('/upload', upload.single('file'), (req, res, next) => {
  console.log(req.file.buffer)
  res.end('文件上传成功~')
})

添加文件名后缀:

js
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, 'uploads/')
  },
  filename: (req, file, cb) => {
    cb(null, Date.now() + path.extname(file.originalname))
  }
})

const upload = multer({
  storage
})

app.post('/upload', upload.single('file'), (req, res, next) => {
  console.log(req.file.buffer)
  res.end('文件上传成功~')
})

我们也可以上传多张图片:

js
app.use('/upload', upload.array('files'), (req, res, next) => {
  console.log(req.files)
})

如果我们希望借助于 multer 帮助我们解析一些 form-data 中的普通数据,那么我们可以使用 any:

  • 请求如下:

image-20240719160003319

js
app.use(upload.any())

app.use('/login', (req, res, next) => {
  console.log(req.body)
})

请求和响应

客户端传递到服务器参数的方法常见的是 5 种:

  • 方式一:通过 get 请求中的 URL 的 params;
  • 方式二:通过 get 请求中的 URL 的 query;
  • 方式三:通过 post 请求中的 body 的 json 格式(中间件中已经使用过);
  • 方式四:通过 post 请求中的 body 的 x-www-form-urlencoded 格式(中间件使用过);
  • 方式五:通过 post 请求中的 form-data 格式(中间件中使用过);
请求解析

方式一:params

请求地址:http://localhost:8000/login/abc/why

获取参数:req.params

js
app.use('/login/:id/:name', (req, res, next) => {
  console.log(req.params)
  res.json('请求成功~')
})

方式二:query

请求地址:http://localhost:8000/login?username=why&password=123

获取参数:req.query

js
app.use('/login', (req, res, next) => {
  console.log(req.query)

  res.json('请求成功~')
})
响应方式

end 方法

类似于 http 中的response.end方法,用法是一致的。只能传入:string 、buffer、uint8array 类型

js
res.end('Hello World')

json 方法

json 方法中可以传入很多的类型:object、array、string、boolean、number、null 等,它们会被转换成 json 格式返回;

js
res.json({ name: 'why', age: 18 })

status 方法

用于设置状态码:

js
res.status(204)

其他支持补充

路由的使用

如果我们将所有的代码逻辑都写在 app 中,那么 app 会变得越来越复杂:

  • 一方面完整的 Web 服务器包含非常多的处理逻辑;

  • 另一方面有些处理逻辑其实是一个整体,我们应该将它们放在一起:比如对 users 相关的处理

    • 获取用户列表;
    • 获取某一个用户信息;
    • 创建一个新的用户;
    • 删除一个用户;
    • 更新一个用户;

我们可以使用 express.Router()来创建一个路由处理程序:

  • 一个 Router 实例拥有完整的中间件和路由系统;
  • 因此,它也被称为 迷你应用程序(mini-app);
js
// 用户相关的处理
const userRouter = express.Router()

userRouter.get('/', (req, res, next) => {
  res.end('用户列表')
})

userRouter.post('/', (req, res, next) => {
  res.end('创建用户')
})

userRouter.delete('/', (req, res, next) => {
  res.end('删除用户')
})

app.use('/users', userRouter)

当然,我们可以配置更多的路由,并且将所有的逻辑放到一个单独的文件中。

静态资源服务器

部署静态资源我们可以选择很多方式:

  • Node 也可以作为静态资源服务器,并且 express 给我们提供了方便部署静态资源的方法;
js
const express = require('express')

const app = express()

;+app.use(express.static('./build'))

app.listen(8000, () => {
  console.log('静态服务器启动成功~')
})

错误处理方式

js
app.use((req, res, next) => {
  next(new Error('USER DOES NOT EXISTS'))
})

app.use((err, req, res, next) => {
  const message = err.message

  switch (message) {
    case 'USER DOES NOT EXISTS':
      res.status(400).json({ message })
  }

  res.status(500)
})

源码

express()

1、创建 app

image-20240206173619910

2、express()函数的本质其实是createApplication(),返回一个 app 函数对象

image-20240206174820129

app.listen()

1、调用app.listen()

image-20240206175005205

2、在createApplication()中通过mixin()将 app 进行了混入

image-20240206175403640

3、app.listen()本质上是对http.createServer(this)的封装,此处的 this 指向 app

image-20240206175054809

app.use()

1、注册中间件

1、通过 use 来注册一个中间件

js
// 注册普通中间件
+++ app.use(
  (req, res, next) => {
    console.log('普通中间件1')
    next()
  },
  (req, res, next) => {
    console.log('普通中间件2')
    next()
  }
)

2、无论是 app.use 还是 app.methods 都会注册一个主路由, app 本质上会将所有的函数,交给这个主路由去处理

js
// application.js

// 2. 实现use()
app.use = function use(fn) {
  // 初始化变量
  var offset = 0;
  var path = '/';

  // default path to '/'
  // disambiguate app.use([fn])
  // 参数fn可以是function也可以是path + function
  if (typeof fn !== 'function') {
    var arg = fn;

    // 取出参数列表中的第一个参数,此时它是path
    while (Array.isArray(arg) && arg.length !== 0) {
      arg = arg[0];
    }

    // first arg is the path
    // 获取到path
    if (typeof arg !== 'function') {
      offset = 1;
      path = fn;
    }
  }

  var fns = flatten(slice.call(arguments, offset));

  if (fns.length === 0) {
    throw new TypeError('app.use() requires a middleware function')
  }

  // setup router
  // 路由器懒加载
  this.lazyrouter();
  var router = this._router;

  // 遍历中间件函数
  fns.forEach(function (fn) {
    // non-express app
    // 非Express应用中间件,直接使用router.use(path, fn)将其注册到指定的路径
    if (!fn || !fn.handle || !fn.set) {
+++      return router.use(path, fn);
    }

    debug('.use app under %s', path);
    fn.mountpath = path;
    fn.parent = this;

    // restore .app property on req and res
    // 创建一个新的中间件函数
    router.use(path, function mounted_app(req, res, next) {
      var orig = req.app;
      fn.handle(req, res, function (err) {
        setPrototypeOf(req, orig.request)
        setPrototypeOf(res, orig.response)
        next(err);
      });
    });

    // mounted an app
    fn.emit('mount', this);
  }, this);

  return this;
};

3、在主路由router.use(path, fn)中,一个函数 fn 会创建一个 layer,并被放入到 router.stack

js
proto.use = function use(fn) {
  var offset = 0;
  var path = '/';

  // default path to '/'
  // disambiguate router.use([fn])
  if (typeof fn !== 'function') {
    var arg = fn;

    while (Array.isArray(arg) && arg.length !== 0) {
      arg = arg[0];
    }

    // first arg is the path
    if (typeof arg !== 'function') {
      offset = 1;
      path = fn;
    }
  }

  var callbacks = flatten(slice.call(arguments, offset));

  if (callbacks.length === 0) {
    throw new TypeError('Router.use() requires a middleware function')
  }

  for (var i = 0; i < callbacks.length; i++) {
    var fn = callbacks[i];

    if (typeof fn !== 'function') {
      throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn))
    }

    // add the middleware
    debug('use %o %s', path, fn.name || '<anonymous>')

+++    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);

    layer.route = undefined;
	// this指向router,所以fns也是保存在router.stack中
+    this.stack.push(layer);
  }

  return this;
};

4、在 Layer 中,会将 fn 赋值给layer.handle

js
function Layer(path, options, fn) {
  if (!(this instanceof Layer)) {
    return new Layer(path, options, fn);
  }

  debug('new %o', path)
  var opts = options || {};

+  this.handle = fn;
  this.name = fn.name || '<anonymous>';
  this.params = undefined;
  this.path = undefined;
  this.regexp = pathRegexp(path, this.keys = [], opts);

  // set fast path flags
  this.regexp.fast_star = path === '*'
  this.regexp.fast_slash = path === '/' && opts.end === false
}

2、请求的处理过程

如果有一个请求过来,那么从哪里开始呢?

1、当请求过来时,会被app.listen监听并执行http.createServer(this)中的 this(app)

js
// 1、调用 app.listen
+++ app.listen(8000, () => {
  console.log('express is running...')
})
js
// 2、调用 app.listen 的时候,本质上是调用 proto 中的 listen
app.listen = function listen() {
+++  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};

2、app 函数被调用开始的;

js
function createApplication() {
  // 2.1 定义app变量,给变量赋值为一个中间件函数
  var app = function(req, res, next) {
+++    app.handle(req, res, next);
  };
  // 省略
}

3、app.handle 本质上会去调用 router.handle

js
app.handle = function handle(req, res, callback) {
  var router = this._router;

  // final handler
  var done = callback || finalhandler(req, res, {
    env: this.get('env'),
    onerror: logerror.bind(this)
  });

  // no routes
  if (!router) {
    debug('no routes defined on app');
    done();
    return;
  }

+++  router.handle(req, res, done);
};

4、router.handle 中做的事:

  • 取出 fns(layer):var stack = self.stack
  • 执行 next:next()
    • 遍历 fns(layer),匹配 path
    • 当匹配到时,执行 fn(layer)
js
proto.handle = function handle(req, res, out) {
  var self = this;

  debug('dispatching %s %s', req.method, req.url);

  var idx = 0;
  var protohost = getProtohost(req.url) || ''
  var removed = '';
  var slashAdded = false;
  var sync = 0
  var paramcalled = {};

  // store options for OPTIONS request
  // only used if OPTIONS request
  var options = [];

  // middleware and routes
++  var stack = self.stack;

  // manage inter-router variables
  var parentParams = req.params;
  var parentUrl = req.baseUrl || '';
  var done = restore(out, req, 'baseUrl', 'next', 'params');

  // setup next layer
  req.next = next;

  // for options requests, respond with a default if nothing else responds
  if (req.method === 'OPTIONS') {
    done = wrap(done, function(old, err) {
      if (err || options.length === 0) return old(err);
      sendOptionsResponse(res, options, old);
    });
  }

  // setup basic req values
  req.baseUrl = parentUrl;
  req.originalUrl = req.originalUrl || req.url;

++  next();

  function next(err) {
    var layerError = err === 'route'
      ? null
      : err;

    // remove added slash
    if (slashAdded) {
      req.url = req.url.slice(1)
      slashAdded = false;
    }

    // restore altered req.url
    if (removed.length !== 0) {
      req.baseUrl = parentUrl;
      req.url = protohost + removed + req.url.slice(protohost.length)
      removed = '';
    }

    // signal to exit router
    if (layerError === 'router') {
      setImmediate(done, null)
      return
    }

    // no more matching layers
    if (idx >= stack.length) {
      setImmediate(done, layerError);
      return;
    }

    // max sync stack
    if (++sync > 100) {
      return setImmediate(next, err)
    }

    // get pathname of request
    var path = getPathname(req);

    if (path == null) {
      return done(layerError);
    }

    // find next matching layer
    var layer;
    var match;
    var route;

    while (match !== true && idx < stack.length) {
      layer = stack[idx++];
      match = matchLayer(layer, path);
      route = layer.route;

      if (typeof match !== 'boolean') {
        // hold on to layerError
        layerError = layerError || match;
      }

      if (match !== true) {
        continue;
      }

      if (!route) {
        // process non-route handlers normally
        continue;
      }

      if (layerError) {
        // routes do not match with a pending error
        match = false;
        continue;
      }

      var method = req.method;
      var has_method = route._handles_method(method);

      // build up automatic options response
      if (!has_method && method === 'OPTIONS') {
        appendMethods(options, route._options());
      }

      // don't even bother matching route
      if (!has_method && method !== 'HEAD') {
        match = false;
      }
    }

    // no match
    if (match !== true) {
      return done(layerError);
    }

    // store route for dispatch on change
    if (route) {
      req.route = route;
    }

    // Capture one-time layer values
    req.params = self.mergeParams
      ? mergeParams(layer.params, parentParams)
      : layer.params;
    var layerPath = layer.path;

    // this should be done for the layer
    self.process_params(layer, paramcalled, req, res, function (err) {
      if (err) {
        next(layerError || err)
      } else if (route) {
++        layer.handle_request(req, res, next)
      } else {
        trim_prefix(layer, layerError, layerPath, path)
      }

      sync = 0
    });
  }

  function trim_prefix(layer, layerError, layerPath, path) {
	// 省略
  }
};

Koa2

概述

定位: node.js 的下一代 web 框架

前面我们已经学习了 express,另外一个非常流行的 Node Web 服务器框架就是 Koa。

事实上,koa 是 express 同一个团队开发的一个新的 Web 框架:

  • 目前团队的核心开发者 TJ 的主要精力也在维护 Koa,express 已经交给团队维护了;
  • Koa 旨在为 Web 应用程序和 API 提供更小、更丰富和更强大的能力;
  • 相对于 express 具有更强的异步处理能力(后续我们再对比);
  • Koa 的核心代码只有 1600+行,是一个更加轻量级的框架,我们可以根据需要安装和使用中间件;

koa

依赖包: koa

安装: npm i koa

基本使用

js
const Koa = require('koa')

// 1、创建app对象
const app = new Koa()

// 4、注册中间件
app.use((ctx, next) => {
  console.log('匹配到中间件1')
  // 5、返回数据
  ctx.body = '中间件1'
})

// 2、启动服务器
app.listen(8000, () => {
  console.log('koa is running~')
})

// 3、运行服务器

中间件

说明: 和 express 中间件具有相同的功能

语法:

js
app.use((ctx, next) => {
  console.log('middleware 01')
  next()
})

app.use((ctx, next) => {
  console.log('middleware 02')
  ctx.response.body = 'Hello World'
})

参数:

  • ctx,上下文(Context)对象,包括请求对象req和响应对象res,见ctx
  • next:``,本质上是一个 dispatch,类似于之前的 next

返回值: undefined

注意: koa 原生注册中间件只能通过 use() 方法,没有提供 methods 的方式,也没有提供 path 中间件来匹配路径

js
app.use((ctx, next) => {
  console.log('middleware 02')
  ctx.response.body = 'Hello World'
})

路由

说明:koa 官方并没有给我们提供路由的库,我们可以选择第三方库:@koa/router

分离 path、method

真实开发中我们如何将路径和 method 分离呢?

方式一:根据 request 自己判断

js
app.use((ctx, next) => {
  if (ctx.request.path === '/users') {
    if (ctx.request.method === 'POST') {
      ctx.response.body = 'Create User Success~'
    } else {
      ctx.response.body = 'Users List~'
    }
  } else {
    ctx.response.body = 'Other Request Response'
  }
})

整个代码的逻辑是非常复杂和混乱的,真实开发中我们会使用路由。

方式二: 使用第三方路由中间件

@koa/router

安装

依赖包: @koa/router

安装: pnpm add @koa/router

注意: 过气的依赖包:koa-router

基本使用
js
const Koa = require('koa')
const KoaRouter = require('@koa/router')

const app = new Koa()

// 1. 创建路由对象
const userRouter = new KoaRouter({ prefix: '/users' })

// 2. 注册路由中间件
userRouter.get('/', (ctx, next) => {
  ctx.body = 'user列表'
})
userRouter.get('/:id', (ctx, next) => {
  const id = ctx.params.id
  ctx.body = '获取某个用户的信息:' + id
})
userRouter.post('/', (ctx, next) => {
  ctx.body = '创建用户成功~'
})
userRouter.delete('/:id', (ctx, next) => {
  const id = ctx.params.id
  ctx.body = '删除用户成功~' + id
})
userRouter.patch('/:id', (ctx, next) => {
  const id = ctx.params.id
  ctx.body = '修改用户成功~' + id
})

// 3. 挂载路由
app.use(userRouter.routes())

app.listen(8000, () => {
  console.log('koa is running~')
})
封装路由

1、创建userRouter.js文件

js
const KoaRouter = require('@koa/router')

// 1. 创建路由对象
const userRouter = new KoaRouter({ prefix: '/users' })

// 2. 注册路由中间件
userRouter.get('/', (ctx, next) => {
  ctx.body = 'user列表'
})
userRouter.get('/:id', (ctx, next) => {
  const id = ctx.params.id
  ctx.body = '获取某个用户的信息:' + id
})
userRouter.post('/', (ctx, next) => {
  ctx.body = '创建用户成功~'
})
userRouter.delete('/:id', (ctx, next) => {
  const id = ctx.params.id
  ctx.body = '删除用户成功~' + id
})
userRouter.patch('/:id', (ctx, next) => {
  const id = ctx.params.id
  ctx.body = '修改用户成功~' + id
})

// 3. 导出userRouter
module.exports = userRouter

2、导入路由模块并挂载

js
const Koa = require('koa')

// 4. 导入userRouter
const userRouter = require('./d03-userRouter')

const app = new Koa()

// 5. 挂载路由
app.use(userRouter.routes())

app.listen(8000, () => {
  console.log('koa is running~')
})
allowedMethods

allowedMethods用于判断某一个method是否支持:

  • 如果我们请求 get,那么是正常的请求,因为我们有实现 get;
  • 如果我们请求 put、delete、patch,那么就自动报错:Method Not Allowed,状态码:405;
  • 如果我们请求 link、copy、lock,那么就自动报错:Not Implemented,状态码:501;
router 前缀

通常一个路由对象是对一组相似路径的封装,那么路径的前缀都是一直的,所以我们可以直接在创建 Router 时,添加前缀

js
+ const userRouter = new Router({ prefix: '/users' })

userRouter.get('/', (ctx, next) => { // 匹配/users/
  ctx.response.body = 'user list~'
})

userRouter.get('/:id', (ctx, next) => { // 匹配/users/:id
  const id = ctx.params.id
  ctx.body = '获取某个用户的信息:' + id
})

module.exports = userRouter
API
js
const router = new Router({ prefix: '/xxx' })
new Router()

说明: 创建一个路由器(Router)实例

语法:

js
const router = new Router(options?)

参数:

  • options?:``,
    • prefix?:``,为路由器中所有路由定义的 URL 前缀。默认为 ""

返回值:

  • router:``,路由实例
router.routes()

说明: 将路由器中定义的路由添加到 Koa 应用程序中

语法:

js
router.routes()

参数: void

返回值: 返回一个路由中间件

示例:

js
// 将路由器中定义的路由添加到Koa应用程序中
app.use(router.routes())
router.METHODS()

说明: 包括一系列的请求方法:

  • router.get():``
  • router.post():``
  • router.delete():``
  • router.patch():``
  • router.put():``
  • router.head():``
  • router.options():``

语法:

js
router.get(path, middleware)

参数:

  • pathstring | reg,要匹配的 URL 路径模式
  • middleware:``,中间件函数,可以添加多个中间件

返回值: undefined

示例:

js
// 定义一个简单的 GET 请求路由
router.get('/hello', (ctx, next) => {
  ctx.body = 'Hello, Koa!'
})
router.allowedMethods()

说明: 一个中间件函数,用于处理在路由处理之后、但未发送响应之前的阶段。它的作用是根据请求的方法(GET、POST、PUT 等)来检查路由是否允许该方法,并进行适当的处理。

未在后端封装的方法,前端请求时会返回Method Not Allowed,而不是Not Found

语法:

js
router.allowedMethods(options?)

参数:

  • options?:``,
    • throw:``,当请求方法不匹配时,是否抛出错误。默认为 true
    • notImplemented:``,未实现的 HTTP 方法的响应状态码。默认为 501
    • methodNotAllowed:``,不允许的 HTTP 方法的响应状态码。默认为 405

返回值: 返回一个路由中间件

示例:

js
// 定义路由
router.get('/users', (ctx, next) => {
  // 处理 GET /users 请求
})

router.post('/users', (ctx, next) => {
  // 处理 POST /users 请求
})

// 加载路由中间件
app.use(router.routes())
;+app.use(router.allowedMethods())

请求

ctx

说明: ctx 是指当前请求的上下文对象,它是一个可读写的 JavaScript 对象。ctx 包含了当前请求的所有信息,如请求头、请求体、URL 等等。在中间件函数中,我们可以通过 ctx 对象来访问这些信息。

属性:

  • Node 原生对象
  • ctx.req:原生 Node.js 的 request 对象。
  • ctx.res:原生 Node.js 的 response 对象。
  • Koa 封装对象
  • ctx.request:Koa 封装后的请求对象,包含了请求头、请求体等信息。
  • ctx.response:Koa 封装后的响应对象,包含了响应头、响应体等信息。
  • 请求
  • ctx.method:HTTP 请求的方法,如 GET、POST 等。
  • ctx.url:请求的 URL 地址,不包含域名部分。
  • ctx.path:请求的路径,不包含查询参数部分。
  • 获取请求参数
  • ctx.query:请求的 query 参数,以对象形式返回。
  • ctx.params:请求的 params 参数
  • ctx.request.body:请求的 post 参数,包括 json, x-www-form-urlencoded, form-data
  • 响应
  • ctx.body:响应的主体内容。
  • ctx.status:响应的状态码,默认为 404。
  • ctx.type:设置响应的 Content-Type 头部字段
  • ctx.app:获取当前应用程序的实例
  • ctx.cookies:Cookies 对象,用于读取和设置 Cookies。

请求参数解析

客户端传递到服务器参数的方法常见的是 5 种:

  • 方式一:通过 get 请求中的 URL 的 params;
  • 方式二:通过 get 请求中的 URL 的 query;
  • 方式三:通过 post 请求中的 body 的 json 格式;
  • 方式四:通过 post 请求中的 body 的 x-www-form-urlencoded 格式;
  • 方式五:通过 post 请求中的 form-data 格式;
GET:query string

请求地址/login?username=why&password=123

获取参数:通过 ctx.queryctx.request.query 获取参数

注意: 通过ctx.query获取的参数是对象格式

js
// 1. query string
userRouter.get('/', (ctx, next) => {
  console.log(ctx.query) // { name: 'tom', age: '18' }
  console.log(ctx.request.query) // { name: 'tom', age: '18' }
  console.log(ctx.query === ctx.request.query) // true

  ctx.body = JSON.stringify(ctx.query)
})
GET:params

请求地址/users/123

获取参数:通过 ctx.params 配合动态路由/:id获取参数 123

js
// 2. params
const userRouter = new Router({ prefix: '/users' })

userRouter.get('/:id', (ctx, next) => {
  console.log(ctx.params.id) // 123
  ctx.body = 'Hello World'
})
POST:json

请求地址/login

请求参数: body 是 json 格式:

js
{
    "username": "coderwhy",
    "password": "123"
}

获取参数:

  • 依赖包: koa-bodyparser

  • 安装: pnpm add koa-bodyparser

  • 1、挂载中间件 koa-bodyparser

  • 2、通过 ctx.rquest.body 获取 json 参数

注意: 不能从ctx.bodyctx.req.body中解析数据

js
// 3. json
app.use(bodyParser())

app.use((ctx, next) => {
  console.log(ctx.request.body)
  ctx.body = 'Hello World'
})
POST:x-www-form-urlencoded

请求地址: /login

请求参数: body 是 x-www-form-urlencoded 格式

image-20240719160050004

获取参数: 和 json 是一致的

  • 依赖包: koa-bodyparser

  • 安装: pnpm add koa-bodyparser

  • 1、挂载中间件 koa-bodyparser

  • 2、通过 ctx.rquest.body 获取 json 参数

注意: 不能从ctx.bodyctx.req.body中解析数据

js
app.use(bodyParser())

app.use((ctx, next) => {
  console.log(ctx.request.body)
  ctx.body = 'Hello World'
})
POST:form-data

请求地址/login

请求参数: body 是 form-data 格式

image-20240206115546019

获取参数:

  • 依赖包:
    • @koa/multer
    • multer
  • 安装: pnpm add @koa/multer multer
  • 1、挂载中间件@koa/multer
  • 2、通过 ctx.request.body 获取 form-data 参数
js
// 1、挂载中间件`@koa/multer`
const multer = require('@koa/multer')
const upload = multer({})
app.use(upload.any())

// 2、通过 ctx.request.body 获取form-data参数
app.use((ctx, next) => {
  console.log(ctx.request.body)
  ctx.body = ctx.request.body
})

form-data 实现文件上传

我们知道 multer 还可以实现文件的上传:见文件上传

js
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, './uploads/')
  },
  filename: (req, file, cb) => {
    cb(null, Date.now() + path.extname(file.originalname))
  }
})

const upload = multer({
  storage
})

const fileRouter = new Router()

fileRouter.post('/upload', upload.single('avatar'), (ctx, next) => {
  console.log(ctx.req.file)
})

app.use(fileRouter.routes())

响应

响应方式:

  • ctx.body :Koa 中通过 ctx.body 响应数据

响应数据类型

  • string :字符串数据
  • Buffer :Buffer 数据
  • Stream :流数据
  • Object|| Array:对象或者数组
  • null :不输出任何内容

示例:

js
// 1. string
ctx.response.body = 'Hello World'

// 2. object
ctx.body = {
  name: 'why',
  age: 18,
  height: 1.88
}

// 3. array
ctx.body = ['tom', 'jack', 'mark']

// 4. stream
const readableStreamTxt = fs.createReadStream('./data/stream.txt')
const readableStreamImg = fs.createReadStream('./data/plane.webp')
console.log(readableStreamImg)
ctx.type = 'image/webp'
ctx.body = readableStreamImg

// 5. buffer
const buf = Buffer.from('你好,Koa')
ctx.body = buf

// 6. null
ctx.body = null

对比 ctx.response.body、ctx.body

js
ctx.body === ctx.response.body
ctx.body !== ctx.request.body
  • 事实上,我们访问 ctx.body 时,本质上是访问 ctx.response.body
  • 我们可以看到源码中,我们访问 proto(这里就是 ctx),其实是访问 proto 中的 response 的属性

image-20240206124038126

响应状态设置

设置方式:

即可以通过 ctx.status 设置,也可以通过ctx.response.status设置

  • ctx.status
  • ctx.response.status

注意: 如果ctx.status尚未设置,Koa 会自动将状态设置为200204

文件上传

单文件上传

@koa/multer

依赖包: @koa/multer

安装: pnpm add @koa/multer

基本使用

思路:

  • 1、通过multer({dest})dest选项设置上传文件的目标目录
  • 2、通过在/single路由中调用 upload.single('plane')中间件,实现单文件上传
  • 3、单文件的信息可以通过ctx.file数组查看

注意: 通过dest的方法无法自定义文件名,并且上传的文件没有后缀名

js
const Koa = require('koa')
const KoaRouter = require('@koa/router')
const multer = require('@koa/multer')

const app = new Koa()
const uploadRouter = new KoaRouter({ prefix: '/upload' })

// 文件上传-单文件
+ const upload = multer({ dest: './upload' })
uploadRouter.post('/single', upload.single('plane'), (ctx, next) => {
  console.log(ctx.file)
  if (ctx.file.filename) {
    ctx.body = '文件上传成功~'
  }
})

app.use(uploadRouter.routes())

app.listen(8000, () => {
  console.log('koa is running~')
})
优化:自定义文件名

思路: 通过multer({storage: multer.diskStorage(destination, filename)}) 的方式设置上传文件的目标目录,可以自定义文件名,并且上传的文件也有后缀名

js
// 文件上传-单文件
const upload = multer({
  storage: multer.diskStorage({
    destination(req, file, callback) {
      callback(null, './upload')
    },
    filename(req, file, callback) {
      const newName =
        file.originalname.replace(path.extname(file.originalname), '') +
        '_' +
        Date.now() +
        path.extname(file.originalname)
      console.log(newName)
      callback(null, newName)
    }
  })
})

uploadRouter.post('/single', upload.single('plane'), (ctx, next) => {
  console.log(ctx.file)
  if (ctx.file.filename) {
    ctx.body = '文件上传成功~'
  }
})

多文件上传

思路:

  • 1、通过multer({storage: multer.diskStorage(destination, filename)}) 的方式设置上传文件的目标目录(和单文件一样)
  • 2、通过在/multi路由中调用 upload.array('vision')中间件,实现多文件上传
  • 3、多文件的信息可以通过ctx.files数组查看
js
const path = require('path')
const Koa = require('koa')
const KoaRouter = require('@koa/router')
const multer = require('@koa/multer')

const app = new Koa()
const uploadRouter = new KoaRouter({ prefix: '/upload' })

// 文件上传-多文件
const upload = multer({
  storage: multer.diskStorage({
    destination(req, file, callback) {
      callback(null, './upload')
    },
    filename(req, file, callback) {
      const newName =
        file.originalname.replace(path.extname(file.originalname), '') +
        '_' +
        Date.now() +
        path.extname(file.originalname)
      console.log(newName)
      callback(null, newName)
    }
  })
})

uploadRouter.post('/multi', upload.array('vision'), (ctx, next) => {
  console.log(ctx.files)
  ctx.body = '文件上传成功~'
})

app.use(uploadRouter.routes())

app.listen(8000, () => {
  console.log('koa is running~')
})

静态服务器

koa-static

依赖包: koa-static

安装: pnpm add koa-static

基本使用

部署的过程类似于 express

js
const Koa = require('koa')
const static = require('koa-static')

const app = new Koa() + app.use(static('./build'))

app.listen(8000, () => {
  console.log('静态服务器启动成功~')
})

错误处理

1、在需要抛出错误的位置通过 ctx.app.emit() 发射 error 事件

  • ctx.app :可以获取当前应用程序的实例
  • app.emit(eventName, ...args):触发事件并传递参数给监听器回调函数。
js
userRouter.get('/', (ctx, next) => {
  const isError = true
  if (isError) {
    // 1. 发射错误事件
    ctx.app.emit('error', -1004, ctx)
  } else {
    ctx.body = '用户列表~'
  }
})

2、在统一的位置处理抛出的错误

  • app.on(eventName, listener):注册一个事件监听器,当事件被触发时执行回调函数。
js
// 2. 监听事件,集中处理错误信息
app.on('error', (code, ctx) => {
  let message = ''
  switch (code) {
    case -1001:
      message = '请求地址错误'
      break
    case -1002:
      message = '资源错误'
      break
    default:
      message = '其他错误'
  }
  ctx.body = {
    code,
    message
  }
})

示例:

js
const Koa = require('koa')
const KoaRouter = require('@koa/router')

const app = new Koa()
const userRouter = new KoaRouter({ prefix: '/users' })

/* 错误处理 */
userRouter.get('/', (ctx, next) => {
  const isError = true
  if (isError) {
    // 1. 发射错误事件
    ctx.app.emit('error', -1004, ctx)
  } else {
    ctx.body = '用户列表~'
  }
})

// 2. 监听事件,集中处理错误信息
app.on('error', (code, ctx) => {
  let message = ''
  switch (code) {
    case -1001:
      message = '请求地址错误'
      break
    case -1002:
      message = '资源错误'
      break
    case -1003:
      message = '权限错误'
      break
    default:
      message = '其他错误'
  }
  ctx.body = {
    code,
    message
  }
})

app.use(userRouter.routes())

app.listen(8000, () => {
  console.log('koa is running~')
})

对比 express

架构设计

  • express:完整和强大的,内置了很多好用的功能
  • koa:简洁和自由的,只包含最核心功能,不限制使用其他中间件

执行同步

express 和 koa 框架的核心都是中间件,但是他们的执行机制不同,特别是中间件中包含异步操作时

**需求:**假如有三个中间件会在一次请求中匹配到,并且按照顺序执行;

  • 在 middleware1 中,在 req.message 中添加一个字符串 aaa
  • 在 middleware2 中,在 req.message 中添加一个 字符串bbb
  • 在 middleware3 中,在 req.message 中添加一个 字符串ccc
  • 当所有内容添加结束后,在 middleware1 中,通过 res 返回最终的结果
  • 注意: 是在 middleware1 中返回 res

1、通过 express 同步实现

js
const express = require('express')

const app = express()

const middleware1 = (req, res, next) => {
  req.message = 'aaa'
  next()
+  res.end(req.message) // aaabbbccc
}

const middleware2 = (req, res, next) => {
+  req.message = req.message + 'bbb'
  next()
}

const middleware3 = (req, res, next) => {
+  req.message = req.message + 'ccc'
}

app.use(middleware1, middleware2, middleware3)

app.listen(8000, () => {
  console.log('启动成功~')
})

最终的结果是:aaabbbccc,没问题

2、通过 koa 同步实现

js
const Koa = require('koa')

const app = new Koa()

const middleware1 = (ctx, next) => {
+  ctx.message = 'aaa'
  next()
  console.log('aaaa')
+  ctx.body = ctx.message // aaabbbccc
}

const middleware2 = (ctx, next) => {
+  ctx.message = ctx.message + 'bbb'
  console.log('bbbb')
  next()
}

const middleware3 = (ctx, next) => {
+  ctx.message = ctx.message + 'ccc'
}

app.use(middleware1)
app.use(middleware2)
app.use(middleware3)

app.listen(8000, () => {
  console.log('启动成功~')
})

最终的结果也是:aaabbbccc,也没问题

执行异步

如果我们最后的 ccc 中的结果,是需要异步操作才能获取到的,是否会产生问题呢?

思路:

  • koa 中如果希望等待下一个异步函数的执行结果,需要在 next 函数前加上 await
  • express 中添加 await 的方法无效

原理:

  • koa 中的 next()函数返回的是 Promise,因此可以使用 await
  • express 中的 next()返回的是 void,因此 await 无效

1、express 中遇到异步操作

1)、没有在 next 前,加 async、await

js
const middleware1 = (req, res, next) => {
  req.message = 'aaa'
  next()
  res.end(req.message)
}

const middleware2 = (req, res, next) => {
  req.message = req.message + 'bbb'
  next()
}

const middleware3 = async (req, res, next) => {
  // 异步操作
+  const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876')
  req.message = req.message + result.data.lrc.lyric
  console.log(req.message)
}

最终的结果aaabbb,是不正确。

2)、有在 next 前,加 async、await

js
const middleware1 = async (req, res, next) => {
  req.message = 'aaa'
  await next()
  res.end(req.message)
}

const middleware2 = async (req, res, next) => {
  req.message = req.message + 'bbb'
  await next()
}

const middleware3 = async (req, res, next) => {
  // 异步操作
+  const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876')
  req.message = req.message + result.data.lrc.lyric
  console.log(req.message)
}

最终的结果也是aaabbb,也是不正确。

为什么呢?

  • 原因是本质上的 next()和异步没有任何关系;
  • 它本身就是一个同步函数的调用,所以它不会等到你异步有结果之后,再继续执行后续的操作;

image-20240206170345870

2、koa 中遇到异步操作

1)没有在 next 前,加 async、await

js
const middleware1 = async (ctx, next) => {
  ctx.message = 'aaa'
  next()
  ctx.body = ctx.message
}

const middleware2 = async (ctx, next) => {
  ctx.message = ctx.message + 'bbb'
  next()
}

const middleware3 = async (ctx, next) => {
  // 异步操作
+  const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876')
  ctx.message = ctx.message + result.data.lrc.lyric
}

最终的结果也是aaabbb,也是不正确。

  • 这是因为虽然 next 函数是一个返回 promise 的异步操作,但是在前面不加 await 的情况,是不同等待结果的返回,就会继续向后执行了;

2)有在 next 前,加 async、await

js
const middleware1 = async (ctx, next) => {
  ctx.message = 'aaa'
  await next()
  ctx.body = ctx.message
}

const middleware2 = async (ctx, next) => {
  ctx.message = ctx.message + 'bbb'
  await next()
}

const middleware3 = async (ctx, next) => {
  // 异步操作
+  const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876')
  ctx.message = ctx.message + result.data.lrc.lyric
}

最终的结果是aaabbb+歌词信息,是正确。

  • 这是因为,当我们在 koa 中的 next 前面加 await 时,它会等到后续有一个确定结果时,在执行后续的代码;

洋葱模型

洋葱模型(Onion Model)是 Koa 框架中用来描述中间件执行流程的一种模型。这个模型得名于中间件的执行方式,就像剥洋葱一样,请求和响应穿过一系列中间件,每个中间件都有机会在请求到达和离开时执行特定的逻辑。

洋葱模型的执行顺序可以简单描述为:请求从外层中间件开始处理,然后依次向内层传递;在内层中间件处理完毕后又依次向外层传递响应。这种模型可以让开发者清晰地了解中间件的执行顺序,并便于对请求和响应进行各种处理。

注意:

  • Koa 中无论同步、异步都符合洋葱模型

  • Express 中同步符合洋葱模型,异步不符合

image-20240206164841772

koa 中异步请求的执行顺序

image-20240206165419516

源码

new Koa()

1、

js
+++ const app = new Koa()

2、new Koa()本质上是new Application()

js
module.exports = class Application extends Emitter {
  constructor(options) {
    super();
    options = options || {};
    this.proxy = options.proxy || false;
    this.subdomainOffset = options.subdomainOffset || 2;
    this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
    this.maxIpsCount = options.maxIpsCount || 0;
    this.env = options.env || process.env.NODE_ENV || 'development';
    if (options.keys) this.keys = options.keys;

    // 用于保存中间件fns
++    this.middleware = [];

+    this.context = Object.create(context);
+    this.request = Object.create(request);
+    this.response = Object.create(response);
    // util.inspect.custom support for node 6+
    /* istanbul ignore else */
    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect;
    }
    if (options.asyncLocalStorage) {
      const { AsyncLocalStorage } = require('async_hooks');
      assert(AsyncLocalStorage, 'Requires node 12.17.0 or higher to enable asyncLocalStorage');
      this.ctxStorage = new AsyncLocalStorage();
    }
  }
};

app.listen()

js
+++ app.listen(8000, () => {
  console.log('koa is running~')
})
js
  listen(...args) {
    debug('listen');
+++    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
js
module.exports = class Application extends Emitter {
  constructor(options) { // 省略 }
++  callback() {
    const fn = compose(this.middleware);

    if (!this.listenerCount('error')) this.on('error', this.onerror);

+    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      if (!this.ctxStorage) {
        return this.handleRequest(ctx, fn);
      }
      return this.ctxStorage.run(ctx, async() => {
        return await this.handleRequest(ctx, fn);
      });
    };

+    return handleRequest;
  }
}

app.use()

1、注册中间件

js
+++ app.use((ctx, next) => {
  console.log('匹配到中间件1')
  // 5、返回数据
  ctx.body = '中间件1'
})
js
module.exports = class Application extends Emitter {
  constructor(options) { // 省略 }
+  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
++    this.middleware.push(fn);
    return this;
  }
}

2、请求的处理过程

js
  listen(...args) {
    debug('listen');
+++    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
js
module.exports = class Application extends Emitter {
  constructor(options) { // 省略 }
+  callback() {
+    const fn = compose(this.middleware);

    if (!this.listenerCount('error')) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
+      const ctx = this.createContext(req, res);
      if (!this.ctxStorage) {
+++        return this.handleRequest(ctx, fn);
      }
      return this.ctxStorage.run(ctx, async() => {
        return await this.handleRequest(ctx, fn);
      });
    };

+    return handleRequest;
  }
}
js
  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
+++    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
js
function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
+    return dispatch(0)
     // dispatch本质上就是next()
+    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
++        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

API

Application

  • app.listen(port, callback):``,启动一个 HTTP 服务器并监听指定端口
  • app.use():``,用来注册中间件的方法
  • app.emit():``,触发事件并传递参数给监听器回调函数。
  • app.on():``,注册一个事件监听器,当事件被触发时执行回调函数。

Context

属性:

  • Node 原生对象
  • ctx.req:原生 Node.js 的 request 对象。
  • ctx.res:原生 Node.js 的 response 对象。
  • Koa 封装对象
  • ctx.request:Koa 封装后的请求对象,包含了请求头、请求体等信息。
  • ctx.response:Koa 封装后的响应对象,包含了响应头、响应体等信息。
  • 请求
  • ctx.method:HTTP 请求的方法,如 GET、POST 等。
  • ctx.url:请求的 URL 地址,不包含域名部分。
  • ctx.path:请求的路径,不包含查询参数部分。
  • 获取请求参数
  • ctx.query:请求的 query 参数,以对象形式返回。
  • ctx.params:请求的 params 参数
  • ctx.request.body:请求的 post 参数,包括 json, x-www-form-urlencoded, form-data
  • 响应
  • ctx.body:响应的主体内容。
  • ctx.status:响应的状态码,默认为 404。
  • ctx.type:设置响应的 Content-Type 头部字段
  • ctx.app:获取当前应用程序的实例
  • ctx.cookies:Cookies 对象,用于读取和设置 Cookies。

Request

  • ctx.request.body:``,请求的 post 参数,包括 json, x-www-form-urlencoded, form-data

Response

  • ctx.response.body:``,等价于 ctx.body

中间件

@koa/router

@koa/router

koa-bodyparser

初始化:

js
const bodyParser = require('koa-bodyparser')
app.use(bodyParser())
  • bodyParser():挂载 bodyParser
multer

安装: pnpm add @koa/multer

请求格式: multipart/form-data

应用: 文件上传

初始化:

js
const multer = require('@koa/multer')
  • multer(options?):``,处理文件上传。它基于 busboy 构建,可帮助你方便地处理通过表单上传的文件
    • options?:``,
      • dest?:``,指定上传文件的保存路径
      • storage?:``,详细指定上传文件的保存路径和文件名
      • fileFilter?:``,用于过滤上传文件的回调函数
  • multer.diskStorage({destination, filename}),用于配置磁盘存储引擎的函数,可以作为storage选项的值
  • multer.memoryStorage():``,用于配置内存存储引擎的函数
  • upload.single(fieldname),用于处理单个文件上传的中间件。可以通过req.file访问上传的文件
    • fieldname:``,上传文件的字段名,必须与 HTML 表单中 input 标签的 name 属性相同
  • upload.array(fieldname, maxCount?),用于处理多文件上传。可以通过req.files访问上传的文件
    • fieldname:``,上传文件的字段名,必须与 HTML 表单中 input 标签的 name 属性相同
    • maxCount?:``,允许上传的最大文件数量,默认值为 Infinity
    • 注意: 所有上传文件的字段名都必须一样
  • upload.fields(),用于处理文件上传的中间件函数。可以通过req.files访问上传的文件
    • fields:[{name: fieldname, maxCount?},...],用于定义文件上传的配置
    • 注意: 所有上传文件的字段名可以单独设置
  • upload.any(),解析通过form-data格式请求的数据。可以通过req.body获取解析后的数据
    • 注意: 不推荐使用form-data格式请求参数。通常使用form-data格式上传文件
@koa/multer

multer 的包装

koa-static

初始化:

js
const serve = require('koa-static')
app.use(serve('./xxx')) // xxx为静态服务器的根目录
  • serve(root):``,返回一个静态服务器的中间件
    • root:``,静态服务器的根目录