从零开始的野路子React/Node(8)后端套餐 TS + MySQL + Sequelize + TSOA

最近自己尝试了一下后端套餐的搭建,发现其实也有挺多小坑的,之前都是同事大佬和实习生大佬搭好了架子我往里塞东西,现在觉得还是有必要好好了解一下整个过程。

 

首先简单介绍一下这四个东西:

TS即TypeScript,一种可以被编译成JS的语言,在后端的某些事务上表现得比JS简洁好使,所以就用它了。

MySQL,自家机器上就装了这,那就用它啦。

Sequelize,一个ORM,用人话写语句,告别SQL,更愉快地CRUD。

TSOA,用TS写controller,自动生成路由和swagger。

 

1、准备工作

在正式开始之前,我们先要做些铺垫:

(1)首先得安装typescript,通过npm install -g typescript。安装完后,试试在cmd中能否执行tsc -v,Windows上来可能就有一坑,如果提示:'tsc' 不是内部或外部命令,也不是可运行的程序或批处理文件,那么以下3篇博客应该可以解决这个问题:

https://www.cnblogs.com/sanyekui/p/13157918.html

https://www.cnblogs.com/fighxp/p/7411376.html

https://www.cnblogs.com/fighxp/p/7411608.html

之后再以管理员身份运行cmd,就没问题了。

(2)准备一下MySQL,我们可以通过MySQL Workbench新建一个数据库abc

 

然后在MySQL Command Line中通过:

CREATE USER user123@localhost IDENTIFIED BY '123456'; 

来新建一个用户user123,密码123456。

GRANT ALL PRIVILEGES ON abc.* TO 'user123'@'localhost'; 

来给user123赋予数据库abc的所有权限。

 

2、正式开工

首先,我们新建一个tmst的文件夹作为我们的项目,cd到tmst下,用最简单的npm init来初始化一个package.json,在其中main设置为app.js,其他一阵默认回车。

 

接下来,我们新建2个文件夹,src和build,前者用于储存我们的ts代码,后者用来储存编译后生成的js代码。

然后,我们把express, @types/express装上,然后在src中新建一个app.ts作为我们的程序入口。app.ts的内容非常简单:

1 import express from 'express';
2 
3 // 创建一个express实例
4 const app: express.Application = express();
5 
6 app.listen(3000, ()=> {
7     console.log('Example app listening on port 3000!');
8 });

看起来跟js的也差不多吧?

现在,在package.json的scripts中增加一条“start”: “tsc && node ./build/app.js”

这代表了我们一会儿在cmd中执行npm start时,会执行2条命令,一条是tsc,负责把ts文件编译成js文件(放在build中)。另一条是执行app.js,多个命令可以用&&连接。

 

看起来差不多了,但是我们还缺少一个配置文件,在cmd中执行tsc --init,会发现tsmt的目录下有多出一个tsconfig.json,这个可以让我们对ts的编译等行为作出一定的配置。

我们稍微修改几个地方:

rootDir为ts文件所在的路径,也就是我们的src文件夹;outDir是我们希望编译后的js文件存放的路径,也就是我们的build文件夹。

这俩之后我们用sequelize和tsoa时使用装饰器的时候会需要。

好了,现在我们在cmd中npm start,可以发现程序正常启动了:

我们查看一下目录,目前应该是这样一个结构:

其中app.js是自动编译产生的。

 

3、连接MySQL

现在我们来连接MySQL,我们可以在src下新建一个config文件夹,用于存放我们的各类配置文件,其中再新建一个database.ts:

 1 module.exports = 
 2 {
 3     development: {
 4         host: "localhost",
 5         port: 3306,
 6         database: "abc",
 7         dialect: "mysql",
 8         username: "user123",
 9         password: "123456",
10         logging: console.log,
11         maxConcurrentQueries: 100,
12     },
13 };

这里我们导出的内容其实就是个json,我们设置了development环境,未来我们可能还有其他环境,比如test或者production等等。然后在其下,我们设置了连接数据库需要的一些参数,比如我们要连接的数据库是abc,使用的是3306端口(MySQL默认),用户名和密码等等。

接下来,我们在src下新建一个models文件夹,之后我们定义的各个model(类似于表)就都会放在这里。我们新建一个sequelize.ts用来连接MySQL。不过在此之前,我们需要先安装一下sequelize, sequelize-typescript, reflect-metadata和mysql2。要注意,此处又有一坑,最新版的sequelize有些bug,后续过程会报错,按照github上的反馈5.22.3这个版本貌似是没啥问题的,我们可以安装一下该版本,npm install sequelize@5.22.3 -s

现在,来写一下sequelize.ts:

1 import { Sequelize } from 'sequelize-typescript';
2 var node_env = process.env.NODE_ENV || "development";
3 const mysqlConfig = require("../config/database")[node_env];
4 
5 console.log(mysqlConfig)
6 export var sequelize = new Sequelize(mysqlConfig);
7 sequelize.authenticate();
8 
9 sequelize.addModels([]);

这里node_env会根据环境变量NODE_ENV的设置而改变,从而从database.ts中读取不同的配置,如果没有设置,则默认读取development。我们可以在cmd中通过set NODE_ENV=development来设置自己想要的环境。

此外,由于我们暂时还没定义任何model,所以addModels这里暂时留个空的array。

另外,我们还需要在app.ts中把我们的sequelize.ts加入进去:

1 import express from 'express';
2 const db = require("./models/sequelize");
3 
4 // 创建一个express实例
5 const app: express.Application = express();
6 
7 app.listen(3000, ()=> {
8     console.log('Example app listening on port 3000!');
9 });

现在,我们再来npm start一下:

看到这个就代表成功了。

 

4、创建表,填充数据

现在我们的数据库还是家徒四壁,空空如也,我们来装模作样地创建一个表,并且填充一些数据吧。这里我们需要装一下sequelize-cli。完成之后,我们在src下新建一个db文件夹,我们会将创建/删除表和填充/卸载数据的操作都放在这里。

然后,我们在cmd中cd到db目录下,执行npx sequelize-cli init来自动生成一些必要的文件:

我们会发现,自动生成的config.json里的内容,跟我们刚才连接数据库写的配置文件差不多,在此,我们只需要留下migrations和seeders文件夹,另外两个可以无情删掉。但我们需要有个文件来告诉sequelize-cli,我们的migrations, seeders, config和models在哪里。

我们在tmst根目录下新建一个.sequelizerc文件来配置这一内容:

1 const path = require('path');
2 
3 module.exports = {
4   'config': path.resolve('src', 'config', 'database.ts'),
5   'models-path': path.resolve('src', 'models'),
6   'seeders-path': path.resolve('src', 'db', 'seeders'),
7   'migrations-path': path.resolve('src', 'db', 'migrations')
8 };

路径各级之间用逗号隔开。

 

现在,我们来创建我们的第一张表,我们需要一张用来存放项目的表,需要项目名称(name),一个简单的描述(desc)和一个版本号(version)。在cmd中,我们在tmst目录下执行:

npx sequelize-cli model:generate --name project --attributes name:string,desc:string,version:string

(--name后跟表的名称,--attributes后跟表的每一列及相应的数据类型)

我们发现有2个文件被自动生成了:

我们看一下migrations下的那个文件:

 1 'use strict';
 2 module.exports = {
 3   up: async (queryInterface, Sequelize) => {
 4     await queryInterface.createTable('projects', {
 5       id: {
 6         allowNull: false,
 7         autoIncrement: true,
 8         primaryKey: true,
 9         type: Sequelize.INTEGER
10       },
11       name: {
12         type: Sequelize.STRING
13       },
14       desc: {
15         type: Sequelize.STRING
16       },
17       version: {
18         type: Sequelize.STRING
19       },
20       createdAt: {
21         allowNull: false,
22         type: Sequelize.DATE
23       },
24       updatedAt: {
25         allowNull: false,
26         type: Sequelize.DATE
27       }
28     });
29   },
30   down: async (queryInterface, Sequelize) => {
31     await queryInterface.dropTable('projects');
32   }
33 };

up代表创建,down代表删除,我们可以看到,sequelize-cli自动帮我们把表的内容都设置好了,还加入了id,创建时间戳(createdAt)和修改时间戳(updatedAt),我们对这两个时间戳稍加修改,把allowNull改为true,并加入defaultValue: new Date(),这样一来,每次插入数据时,就会自动打上时间戳了。

注意,这里也有个坑,MySQL会把表名自动变成复数,所以这里是projects,而不是project……

 

我们再看看models下的project.js,我们可以把它删掉,用ts直接写一个。新建一个Project.ts:

 1 import { Table, Column, Model, CreatedAt, UpdatedAt } from 'sequelize-typescript';
 2  
 3 @Table({
 4   tableName: 'projects', modelName: 'projects', freezeTableName:true
 5 })
 6 export class Project extends Model<Project> {
 7 
 8   @Column
 9   name!: string;
10  
11   @Column
12   desc!: string;
13 
14   @Column
15   version!: string;
16 
17   @CreatedAt
18   @Column
19   createdAt!: Date;
20 
21   @UpdatedAt
22   @Column
23   updatedAt!: Date;
24 
25 }

这里我们指明了我们需要关联的表是projects。这里也可以看出用ts写非常简洁。

我们再加一个index.ts,用于归置所有的model,以后我们有了其他model之后,可以一并加入index.ts:

1 export * from "./Project";

最后不要忘了把这个model加入到sequelize.ts中:

 1 import { Sequelize } from 'sequelize-typescript';
 2 import * as models from "./index"; //加入models
 3 var node_env = process.env.NODE_ENV || "development";
 4 const mysqlConfig = require("../config/database")[node_env];
 5 
 6 console.log(mysqlConfig)
 7 export var sequelize = new Sequelize(mysqlConfig);
 8 sequelize.authenticate();
 9 
10 sequelize.addModels([
11     models.Project //加入Project
12 ]);

我们试着在cmd中执行一下npx sequelize-cli db:migrate

现在我们在MySQL Workbench中可以看到,我们多了一张空的表projects:

创建成功了!我们可以在cmd中通过npx sequelize-cli db:migrate:undo来删除这张表。

接下来我们来填充一些数据,我们在src下新建一个data文件夹,用来存放我们要填充的数据,我们可以使用最常用的json格式,新建一个projects.json,并加入2个项目的信息:

 1 [
 2     {
 3         "name": "ABCD",
 4         "desc": "A simple project",
 5         "version": "1.0"
 6     },
 7     {
 8         "name": "NEW",
 9         "desc": "A new project",
10         "version": "0.1"
11     }
12 ]

我们再在cmd中执行npx sequelize-cli seed:generate --name demo-project来获取一个数据填充的模板(会出现在src/db/seeders目录下),替换一下模板内的内容:

 1 'use strict';
 2 
 3 module.exports = {
 4   up: async (queryInterface, Sequelize) => {
 5     var projects = [...require("../../data/project.json")]
 6     await queryInterface.bulkInsert('projects', projects, {})
 7   },
 8 
 9   down: async (queryInterface, Sequelize) => {
10     await queryInterface.bulkDelete('projects', null, {});
11   }
12 };

类似地,up是填充数据,down是卸载数据。

在cmd中执行一下npx sequelize-cli db:seed:all(如果之前已经删除了表,则先npx sequelize-cli db:migrate,再执行npx sequelize-cli db:seed:all),可以看到:

 

回到MySQL Workbench,会发现表中已经有了内容:

现在,我们的程序已经和数据库完全打通了。你可以通过npx sequelize-cli db:seed:undo:all来卸载所有数据(先留着吧)。各个操作的内容基本也可以在sequelize的官方文档中找到:https://sequelize.org/master/manual/migrations.html

截止此步的目录结构:

 

5、传统艺能CRUD

既然我们已经完成了跟数据库的对接,那还不赶紧CRUD?

这里我们在src下新建services和controllers文件夹,分别存放我们的service(负责CRUD)和controller(负责对接路由和service)。

在services下,我们写一个非常简单的ProjectService.ts:

1 import { Project } from "../models";
2 
3 export class ProjectService {
4     public static async getAll(): Promise<Project[] | null> {
5         var projects = null;
6         projects = await Project.findAll({});
7         return projects;
8     }
9 }

我们这个class只有一个函数getAll,用来查询所有项目的信息。由于ts是强类型的语言,所以我们需要声明函数的参数和返回值各是什么类型,这里我们在Promise中声明我们返回的东西要么是一堆Project(中括号表示一堆,没有中括号表示一个),要么是null(竖线隔开表示或)。

然后在controllers下新建一个ProjectController.ts:

 1 import { 
 2     Controller, 
 3     Get, 
 4     Route
 5 } from 'tsoa';
 6 import { Project } from "../models/";
 7 import { ProjectService } from "../services/ProjectService";
 8 
 9 @Route("project")
10 export class ProjectController extends Controller {
11     @Get("/all")
12     public async getAllProjects(): Promise<Project[] | null> {
13         var projects = await ProjectService.getAll()
14         return projects
15     }
16 }

这里我们的controller都继承自tsoa的Controller(别忘了先装tsoa),这里我们定义了所有project相关的操作都会在project这个路径下(也就是http://localhost:3000/project),其中我们定义一个GET方法,通过http://localhost:3000/project/all这个路径来调用ProjectService中getAll的这个操作。也就是说我们对http://localhost:3000/project/all发出GET请求,我们就可以获得所有项目的信息了(装饰器真香)。

另外,我们还需要在tmst的根目录下定义一个tsoa.json,作为tsoa的配置文件:

 1 {
 2     "entryFile": "src/app.ts",
 3     "noImplicitAdditionalProperties": "throw-on-extras",
 4     "controllerPathGlobs": ["src/**/*Controller.ts"],
 5     "spec": {
 6       "outputDirectory": "src/routes",
 7       "specVersion": 3
 8     },
 9     "routes": {
10       "routesDir": "src/routes"
11     }
12 }

告诉tsoa,我们程序的入口是src/app.ts,所有的controller都在src目录下,且以Controller.ts结尾,另外自动生成的swagger文件和路由文件都放入src/routes目录下。我们去src下新建一个routes文件夹留着给tsoa用,然后在cmd中执行yarn run tsoa routes,让tsoa给我们自动生成路由:

差不多了,我们再把路由放入app.ts中,并加入bodyParser(解析json用,需要安装body-parser和@types/body-parser):

 1 import express from 'express';
 2 import bodyParser from 'body-parser';
 3 import { RegisterRoutes } from "./routes/routes";
 4 const db = require("./models/sequelize");
 5 
 6 // 创建一个express实例
 7 const app: express.Application = express();
 8 
 9 app.use(
10     bodyParser.urlencoded({
11       extended: true,
12     })
13 );
14 app.use(bodyParser.json());
15 
16 RegisterRoutes(app); // 添加路由 
17 
18 app.listen(3000, ()=> {
19     console.log('Example app listening on port 3000!');
20 });

最后一步,在package.json的scripts中,给start再加一条命令:

tsoa spec-and-routes将用来自动生成路由文件(routes.ts)和swagger配置文件(swagger.json)。

大功告成,我们再试试npm start吧:

让我们打开postman,兴冲冲地给http://localhost:3000/project/all发送一个GET请求:

居然报错了……原来这里还有个最后一坑,让我们进入ts的配置文件tsconfig.json,把target里的ES5改成ES6:

重新migrate,seed,npm start,再发个GET请求试试:

问题解决了。

现在我们搭的架子已经可以正常工作了,后续只要往里填东西就可以啦。

 

代码见:

https://github.com/SilenceGTX/tmst

posted @ 2020-12-06 12:02  SilenceGTX  阅读(1118)  评论(0编辑  收藏  举报