Express 基本结构,路由,middleware,错误处理

382 阅读5分钟

Express 基本结构

import express from 'express';
const app = express();

上方的范例程式码定义 app 是 Express 的实例,在这个层级我们称为 应用程式层 (Application-level) ,而 Express 的应用是由一系列 中介软体 (Middleware)  所构成,其定义为:可以存取 请求 (req)回应 (res)  以及呼叫下一个中介软体的 函式 (Function) ,要呼叫下一个中介软体就要使用next()

应用程式层定义基本路由

可以透过 app 提供的 方法 (Methods)  来指定要用什么方式载入路由,比如说:使用 GET 方式载入路由。下方为基本的路由结构,其中,大写的部分为指定参数:

app.METHOD(PATH, HANDLER)
  • METHOD:为载入路由的方式,如:GET、POST、PUT、PATCH、DELETE 等
  • PATH:为资源的路径,即要设定的路由位置
  • HANDLER:为载入路由后要执行的操作,即 中介软体函数,可以是 一个或多个 function

image.png

路由器层定义基本路由

├── src
|   ├── index.ts
|   ├── app
|   |   └── app.routing.ts
|   └── environments
|       ├── development.env
|       └── production.env
├── package.json
└── tsconfig.json

写一下 `app.routing.ts` 的内容:
import express from 'express';
const router = express.Router();

router.get('/test', (req, res, next) => {
    res.send('test!');
});

export default router;

//改一下 index.ts 的内容:

import express from 'express';
import path from 'path';
import dotenv from 'dotenv';

import appRoute from './app/app.routing';

const app = express();

dotenv.config({ path: path.resolve(__dirname, `./environments/${process.env.NODE_ENV}.env`) });

app.get('/', (req, res, next) => {
    res.send('Hello, World!!');
});

app.use('/', appRoute);

app.listen(process.env.PORT, () => console.log(`http server is running at port ${process.env.PORT}.`));

执行npm run start:dev: image.png

从上方范例可以看出,定义完路由器层的路由配置后,需要在应用程式层透过 use 方法来载入 Router 的实例,进而使用该路由的配置,是十分方便的功能。

等等,所以 use 是干嘛用的?
简单来说 use 是用来载入middleware的方法,与getpost等方法一样,透过指定路径,使它在特定路径下才会触发,是经常使用的功能,与其他方法不同的是它并不会受请求资源的方式所限制。

middleware于 Express 中扮演何种角色

从上面的案例不难看出 Express 的路由机制可以建立成树状结构,由许多 Router 组合起来,最终收敛于应用程式层。

image.png

body-parser于 Express 中扮演何种角色

body-parser 是 Express 经常使用的中介软体,用于解析请求的资料 (body),比如说:POST 一笔 JSON 格式的资料到我们的 Express App,这时可以透过 body-parser 快速解析这笔资料,以方便取用。下方的图为 Express 的运作模型,可以看到 body-parser 会先进行资料的解析,才会把解析后的资料传给其他相关的 middleware 使用。

image.png

常用的解析方式

解析 JSON

用来解析 JSON 格式的请求资料。

bodyParser.json(options);

options的参数:

  • inflate: 解压缩 deflate 资料,若为false, deflate 资料会被拒绝,预设为true
  • limit: 设置请求大小的上限,预设为100kb
  • reviver: 为 JSON.parse 的第二个参数
  • strict: 仅解析 Object 与Array,若为 false 则采用 JSON.parse 的解析模式,预设为true
  • type: 设定何种类型的资源要进行解析,预设为application/json

解析 urlencoded

用来解析 urlencoded 格式的请求资料。

bodyParser.urlencoded(options);

options的参数:

  • extended: 使用 qs 进行解析,若为false,则采用 querystring 进行解析,预设为true
  • inflate: 解压缩 deflate 资料,若为false, deflate 资料会被拒绝,预设为true
  • limit: 设置请求大小的上限,预设为100kb
  • parameterLimit: 设置最大参数数量,预设为1000
  • type: 设定何种类型的资源要进行解析,预设为application/x-www-form-urlencoded

使用方法

Express 将 body-parser 包起来了,所以 body-parser 的 API 都可以从 express 中调用,如:

express.json();
import express from 'express';
const router = express.Router();

router.post('/test', express.json(), (req, res, next) => {
  res.send(JSON.stringify(req.body));
});

export default router;

image.png

一句话: body-parser可以帮我们处理不同类型的资料

Express 抛出错误的方式

在 Express 中抛出错误的方式大致上可以分成两种,会在下方进行示范,并在 app.routing.ts 中进行测试

直接抛出

透过 throw 进行抛出:

router.get('/error', (req, res, next) => {
  throw 'error page.';
});

next 抛出

next()带入错误讯息,Express 会视为错误:

router.get('/error', (req, res, next) => {
  next('error page.');
});

Promise next(err)

如果今天有一连串的资料要处理,并且用 then chain 的方式处理,大致上会长这样:

router.get('/data/error', (req, res, next) => {
  // Fake API
  const getProfile = new Promise((resolve, reject) => {
    setTimeout(() => resolve({ name: 'HAO', age: 22 }), 100);
  });
  const getFriends = new Promise((resolve, reject) => {
    setTimeout(() => resolve([]), 120);
  });
  const errorRequest = new Promise((resolve, reject) => {
    setTimeout(() => reject('Oops!'), 2000);
  });

  getProfile
  .then(profile => getFriends)
  .then(friends => errorRequest)
  .then(() => res.send('GoGoGo!'))
  .catch(err => next(err));
});

如果比较讲究 (龟毛) 的人就会希望不要看到一堆next(err),那可以怎么做呢?运用 await/async 的机制可以办到,先定义一个函式:

const errorHandler = (
  func: (req: Request, res: Response, next: NextFunction) => Promise<void>
) => (req: Request, res: Response, next: NextFunction) =>
  func(req, res, next).catch(next);

并把 middleware 带入此函式中:

router.get('/data/error/promise', errorHandler(async (req, res, next) => {
  // Fake API
  const getProfile = new Promise((resolve, reject) => {
    setTimeout(() => resolve({ name: 'HAO', age: 22 }), 100);
  });
  const getFriends = new Promise((resolve, reject) => {
    setTimeout(() => resolve([]), 120);
  });
  const errorRequest = new Promise((resolve, reject) => {
    setTimeout(() => reject('Oops!'), 2000);
  });

  const profile = await getProfile;
  const friends = await getFriends;
  const none = await errorRequest;
  res.send('GoGoGo!');
}));

两者结果相同,但前者会将错误处理直接放在 middleware 里,而后者是将 middleware 包起来,并执行一个被包装好的 async function,只要发生错误就会自动将错误传至 next() 中。

Express 全域错误处理

Express 本身有内建错误处理器,只要抛出错误就会将错误资讯带入其中,这个就是 全域错误处理 机制,以一个庞大的系统而言,建议自行设计一套全域错误处理机制来格式化错误资讯,再送回客户端。究竟要如何设计呢?只需要在 应用程式层 用中介软体来解决,这个中介软体正是前面所提到的Error-First Callback

app.use(function(err: any, req: Request, res: Response, next: NextFunction) {
  res.status(500).json({ message: err.message || err });
});

现在,打开浏览器并进入刚刚设计的http://localhost:3000/data/error来测试看看是不是变成 JSON 格式了