# 1. 前言

在前面几个章节中,我们已经实现了项目中的几个常见操作:环境搭建、启动服务器、路由中间件、请求参数解析、数据库操作等。但是,到目前为止整个示例中所有的代码都写在 app.ts 中。然而在业务代码持续增大,场景更加复杂的情况下,这种做法无论是对后期维护还是对患有强迫症的同学来说都不是好事。所以我们现在要做的就是:『代码分层』。根据代码职责功能进行划分,将不同职责功能的代码分开存放。

# 2. 分离 router

首先,我们要做的是分离 router ,通过情况下我们会在 src 目录下新建一个名为 router 的文件夹,将项目中所有的路由规则文件都放在router 文件夹里面,我们以上一章节的 /user 路由为例,在 router 文件夹下创建 user.ts 文件,并写入以下内容:

import * as Koa from "koa";
import * as Router from "koa-router";
const router = new Router();

export default (app: Koa) => {
  router.post("/user", async (ctx: Koa.Context, next: Koa.Next) => {
    // ...
  });

  router.put("/user", async (ctx: Koa.Context, next: Koa.Next) => {
    // ...
  });

  router.del("/user", async (ctx: Koa.Context, next: Koa.Next) => {
    //
  });

  router.get("/user/:name", async (ctx: Koa.Context, next: Koa.Next) => {
    // ...
  });

  app.use(router.routes()).use(router.allowedMethods());
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

然后在 app.ts 文件中引入路由,如下:

import * as Koa from "koa";
import * as bodyParser from "koa-bodyparser";
import userRouter from "./router/user";

const app = new Koa();

// 注册中间件
app.use(bodyParser());
// 添加路由
userRouter(app);

app.listen("3000", () => {
  console.log("server is running at http://localhost:3000");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

代码看起来清爽了很多。

然而到了这一步,还是不能够高枕无忧。router 文件独立出来以后,应用的主文件 app.ts 虽然暂时看起来比较清爽,但这是在只有一个 /user 路由的情况下,如果路由慢慢多起来,那么在 app.ts 中就会出现这样的情况:

import userRouter from "./router/user";
import xxx1 from "xxx1";
import xxx2 from "xxx2";
import xxx3 from "xxx3";

// 添加路由
userRouter(app);
userRouter(xxx1);
userRouter(xxx2);
userRouter(xxx3);
1
2
3
4
5
6
7
8
9
10

显然这样也是不美观的,鉴于此,我们再在router 文件夹下创建 index.ts 文件,并写入以下内容:

import * as Koa from "koa";
import user from "./user";

const routerList = [user];

export default (app: Koa) => {
  routerList.forEach((router) => router(app));
};
1
2
3
4
5
6
7
8

继续修改 app.ts 文件:

import * as Koa from "koa";
import * as bodyParser from "koa-bodyparser";
import router from "./router";

const app = new Koa();

app.use(bodyParser());
// 添加路由
router(app);

app.listen("3000", () => {
  console.log("server is running at http://localhost:3000");
});
1
2
3
4
5
6
7
8
9
10
11
12
13

这样一来,将来路由变多了,只需在 router 文件夹下添加对应的路由文件,并在 router/index.ts 文件中像如下方式添加即可:

import * as Koa from "koa";
import user from "./user";
import xx1 from "xx1";
import xx2 from "xx2";

const routerList = [user, xx1, xx2];

export default (app: Koa) => {
  routerList.forEach((router) => router(app));
};
1
2
3
4
5
6
7
8
9
10

至此,router 已经让我们从应用的主文件 app.ts 中分离出来了。

# 3. 分离 Controller 层

# 3.1 什么是 Controller

虽然 router 已经让我们从 app.ts 中分离出来了,但是每个路由里面的具体逻辑还是不应该都写在路由规则配置文件里,路由就应该只用来配置路由规则,而具体的路由逻辑应该再分发到了对应的 Controller 上,那 Controller 负责做什么?

简单的说 Controller 负责解析用户的输入,处理后返回相应的结果,例如

  • RESTful 接口中,Controller 接受用户的参数,从数据库中查找内容返回给用户或者将用户的请求更新到数据库中。
  • HTML 页面请求中,Controller 根据用户访问不同的 URL,渲染不同的模板得到 HTML 返回给用户。
  • 在代理服务器中,Controller 将用户的请求转发到其他服务器上,并将其他服务器的处理结果返回给用户。

通常推荐的做法是:Controller 层主要对用户的请求参数进行处理(校验、转换),然后调用对应的 Service 层方法处理业务,得到业务结果后封装并返回:

  1. 获取用户通过 HTTP 传递过来的请求参数。
  2. 校验、组装参数。
  3. 调用 Service 进行业务处理,必要时处理转换 Service 的返回结果,让它适应用户的需求。
  4. 通过 HTTP 将结果响应给用户。

# 3.2 如何编写 Controller

/user 路由为例,我们在 src 目录下创建一个名为 controller 的文件夹用于存放 controller 层的文件,在 controller 文件夹下创建 user.ts 文件,并写入以下内容:

import * as Koa from "koa";
import UserModel from "../models/User";

export default {
  getUser: async (ctx: Koa.Context, next: Koa.Next) => {
    const { name } = ctx.params;
    const user = await UserModel.findOne({
      where: { name },
    });
    if (user) {
      ctx.body = {
        data: user,
      };
    } else {
      ctx.body = {
        msg: "该用户不存在",
      };
    }
  },
  createUser: async (ctx: Koa.Context, next: Koa.Next) => {
    const { name, age } = ctx.request.body;
    const user = await UserModel.create({
      name,
      age: Number(age),
    });
    ctx.body = {
      msg: "添加成功",
      data: user,
    };
  },
  updateUser: async (ctx: Koa.Context, next: Koa.Next) => {
    const { name } = ctx.request.body;
    let res: { msg: string; data: any } = {
      msg: "",
      data: null,
    };
    const user = await UserModel.findOne({
      where: { name },
    });
    if (user) {
      await user.update({
        age: 19, //修改的字段对应的内容
      });
      res.msg = "修改成功";
      res.data = user;
    } else {
      res.msg = "您要修改的用户不存在";
      res.data = null;
    }
    ctx.body = res;
  },
  deleteUser: async (ctx: Koa.Context, next: Koa.Next) => {
    const { name } = ctx.request.body;
    let res: { msg: string; data: any } = {
      msg: "",
      data: null,
    };
    const user = await UserModel.findOne({
      where: { name },
    });
    if (user) {
      await user.destroy();
      res.msg = "删除成功";
      res.data = user;
    } else {
      res.msg = "您要删除的用户不存在";
      res.data = null;
    }
    ctx.body = res;
  },
};
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
66
67
68
69
70
71

然后在 router/user.ts 文件中引入该 controller,如下:

import * as Koa from "koa";
import * as Router from "koa-router";
import UserController from "../controller/user";

const router = new Router();

export default (app: Koa) => {
  router.post("/user", UserController.createUser);

  router.put("/user", UserController.updateUser);

  router.del("/user", UserController.deleteUser);

  router.get("/user/:name", UserController.getUser);

  app.use(router.routes()).use(router.allowedMethods());
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

如此一来,router/user.ts 文件是不是就清爽多了。

# 4. 分离 Service 层

# 4.1 什么是 Service

到目前为止,在 controller 层里面,不但做了一些常规的请求参数解析的工作,而且还包含了一些数据操作逻辑,通常情况下,我们还会从 controller 层里面分离出 service 层,以分担 controller 层里面的逻辑。

简单来说,Service 就是在复杂业务场景下用于做业务逻辑封装的一个抽象层,提供这个抽象有以下几个好处:

  • 保持 Controller 中的逻辑更加简洁。
  • 保持业务逻辑的独立性,抽象出来的 Service 可以被多个Controller 重复调用。
  • 将逻辑和展现分离,更容易编写测试用例,

# 4.2 如何编写 Service

还是以 /user 路由为例,我们在 src 目录下创建一个名为 service 的文件夹用于存放 service 层的文件,在 service 文件夹下创建 user.ts 文件,并写入以下内容:

import UserModel from "../models/User";

export default {
  findUserByName: async (name: string) => {
    let res: { msg: string; data: any } = {
      msg: "",
      data: null,
    };
    const user = await UserModel.findOne({
      where: { name },
    });
    if (user) {
      res.data = user;
    } else {
      res.msg = "该用户不存在";
    }
    return res;
  },
  deleteUserByName: async (name: string) => {
    let res: { msg: string; data: any } = {
      msg: "",
      data: null,
    };
    const user = await UserModel.findOne({
      where: { name },
    });
    if (user) {
      await user.destroy();
      res.msg = "删除成功";
      res.data = user;
    } else {
      res.msg = "您要删除的用户不存在";
    }
    return res;
  },
  createUser: async (name: string, age: number) => {
    const user = await UserModel.create({
      name,
      age,
    });
    return {
      msg: "添加成功",
      data: user,
    };
  },
  updateUserByName: async (name: string, data: { [key: string]: any }) => {
    let res: { msg: string; data: any } = {
      msg: "",
      data: null,
    };
    const user = await UserModel.findOne({
      where: { name },
    });
    if (user) {
      await user.update(data);
      res.msg = "修改成功";
      res.data = user;
    } else {
      res.msg = "您要修改的用户不存在";
    }
    return res;
  },
};
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

然后在 controller/user.ts 文件中引入该 service,如下:

import * as Koa from "koa";
import UserService from "../service/user";

export default {
  getUser: async (ctx: Koa.Context, next: Koa.Next) => {
    const { name } = ctx.params;
    const res = await UserService.findUserByName(name);
    ctx.body = res;
  },
  createUser: async (ctx: Koa.Context, next: Koa.Next) => {
    const { name, age } = ctx.request.body;
    const res = await UserService.createUser(name, Number(age));
    ctx.body = res;
  },
  updateUser: async (ctx: Koa.Context, next: Koa.Next) => {
    const { name } = ctx.request.body;
    const res = await UserService.updateUserByName(name, { age: 19 });
    ctx.body = res;
  },
  deleteUser: async (ctx: Koa.Context, next: Koa.Next) => {
    const { name } = ctx.request.body;
    const res = await UserService.deleteUserByName(name);
    ctx.body = res;
  },
};
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

这样一来,controller 层的职责就变得比较单一清晰了。

# 5. 分离 model

对于数据库的模型定义,我们也应当将其单独统一放在一起。我们在 src 目录下创建一个名为 model 的文件夹用于存放数据库模型定义文件。以 UserModel 为例,我们在 model 文件夹下创建 user.ts 文件,如下:

import sequelize from "../libs/db";
import { DataTypes } from "Sequelize";
const UserModel = sequelize.define(
  "user", // 定义名为 user 的表
  {
    name: DataTypes.STRING, // 定义 name 字段,类型为string
    age: DataTypes.NUMBER, // 定义 age 字段,类型为number
  }
);
export default UserModel;
1
2
3
4
5
6
7
8
9
10

# 6. 分离 middleware

到目前为止,我们项目中的中间件仅使用了 koa-bodyparser,我们将其直接写在了项目主文件 app.ts 中,但是随着项目越来越大,使用的中间件也会越来越多,如果都将其直接写在 app.ts 中的话,就会显得 app.ts 文件过于臃肿,所以我们也将其分离出来。

通常情况下,我们会在 src 目录下新建一个名为 middleware 的文件夹,并在该文件夹下创建 index.ts 文件,写入如下内容:

// src/middleware/index.ts

import * as Koa from "koa";
import * as bodyParser from "koa-bodyparser";

export default (app: Koa) => {
  app.use(bodyParser());
};
1
2
3
4
5
6
7
8

然后在 app.ts 文件中引入 middleware,如下:

import * as Koa from "koa";
import sequelize from "./config/db";
import router from "./router";
import middleware from "./middleware";

const app = new Koa();

// 注册中间件
middleware(app);
// 添加路由
userRouter(app);

app.listen("3000", () => {
  console.log("server is running at http://localhost:3000");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

如此以来,我们就将项目中的中间件从主文件 app.ts 中分离出去了。

# 7. 分离配置文件

对于一些恒定不变的配置性的东西,如数据库的信息,环境变量等信息,我们也应当将其统一管理起来。我们在 src 目录下创建一个名为 config 的文件夹用于存放配置信息。我们在 config 文件夹下创建 index.ts 文件,如下:

export const DB_CONFIG = {
  databaseName: "", // 数据库名称
  userName: "", //数据库登录名
  password: "", //数据库登录密码
  host: "", //数据库服务地址
};
1
2
3
4
5
6

这样以来,我们在连接数据库的时候就可以从这里读取相关的配置信息。

我们再在该目录下创建一个 db.ts 文件,用于存放 orm 与数据库连接的定义,如下:

import { Sequelize } from "Sequelize";
import { DB_CONFIG } from "./index";

const { databaseName, userName, password, host } = DB_CONFIG;

const sequelize = new Sequelize(databaseName, userName, password, {
  host,
  dialect: "mysql",
});

export default sequelize;
1
2
3
4
5
6
7
8
9
10
11

# 8. 代码分离完毕

至此,代码分离就完成了,最后我们看下分离后的项目目录结构,如下:

src
├── app.ts
├── config
│   ├── db.ts
│   └── index.ts
├── controller
│   └── user.ts
├── middleware
│   └── index.ts
├── models
│   └── User.ts
├── router
│   ├── index.ts
│   └── user.ts
└── service
    └── user.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16