zoukankan      html  css  js  c++  java
  • 从零开始的野路子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

  • 相关阅读:
    软件工程概论第十六周学习进度表
    构建之法阅读笔记06
    软件工程概论第十五周学习进度表
    手机百度输入法的用户体验
    构建之法阅读笔记05
    软件工程概论第十四周学习进度表
    2020/2/1-Python学习计划
    Map Reduce数据清洗及Hive数据库操作
    《大数据技术原理与应用》暑假学习计划_06
    分布式数据库的安装与配置
  • 原文地址:https://www.cnblogs.com/silence-gtx/p/14092286.html
Copyright © 2011-2022 走看看