从零开始的野路子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请求试试:
问题解决了。
现在我们搭的架子已经可以正常工作了,后续只要往里填东西就可以啦。
代码见: