从0开始Typescript + Express + Sequelize + Swagger + PM2 + Docker 搭建部署后端服务

avatar
前端工程师

最新项目需要,就搭建了Node后端服务,记录下整个流程,这里不会太深入的说明每个插件的使用,仅对流程进行说明,具体可以查看对应工具的官网。

附项目源码:github.com/richLpf/exp…

项目环境:

  • OS: m1、
  • node: 17.9
  • Dcoekr Image: node:18-alpine
  • 部署环境:centos

一、初始化项目

1、新建项目

express-demo
npm init

2、安装必要的依赖

yarn add typescript ts-node @types/node @types/express cross-env nodemon -D
yarn add express

3、配置tsconfig.json, 常规配置

{
    "compilerOptions": {
      "target": "es2017",
      "module": "commonjs", //通过commonjs处理模块化
      "rootDir": "./",
      "outDir": "./dist",
      "esModuleInterop": true,
      "baseUrl": "src",
      "strict": true,
      "strictPropertyInitialization": false // 不用严格要求值的初始化
    },
    "exclude": ["node_modules"]
}

4、在项目下新建src目录,用来存放源文件,项目目录结构如下:

image.png

核心目录就是:

  • controllers 控制器,主要用来处理api逻辑
  • models 模型,数据库表模型
  • services 操作数据库的API
  • databases 数据库相关配置和初始化

然后在这个基础上,我们还有一些辅助的目录

  • config 用来获取外部传进来的环境变量或者配置的数据库参数
  • exceptions 用来定义接口返回的JSON结构体
  • interface 用来声明变量类型
  • routes 用来暴露对外的API接口
  • utils 作为工具函数的文件目录
  • app.ts 用来构建整个app,将各种需要提前处理的集中处理
  • index.ts 用来作为整个项目的入口文件

当前节点用到的插件

  • nodemon通过检测到目录中的文件更改时自动重新启动节点,在开发时保持热更新
  • cross-env用来通过命令行设置环境变量,区分开发环境和生产环境

入口文件index.ts, 我们用来引入路由和启动服务

import App from './app'
import monitorRouter from './routes/monitor.route'

const app = new App([new monitorRouter()])
app.listen()

app.ts,当app实例化的时候,要连接数据库、初始化路由、中间件、文档等,这里先定义好方法

class App {
    app: express.Application
    port: number = 3000
    constructor(routers: Routes[]){
        this.app = express();
        this.connectToDatabase()
        this.initializeMiddlewares()
        this.initializeRoutes(routers)
        this.initializeSwagger()
    }
    // 连接数据库
    private connectToDatabase() {
        DB.sequelize.sync({ force: false });
    }
    // 初始化中间件
    private initializeMiddlewares() {
        this.app.use(express.json());
        this.app.use(express.urlencoded({ extended: true }));
    }
    // 初始化路由
    private initializeRoutes(routes: Routes[]) {
        routes.forEach(route => {
            this.app.use('/', route.router);
        });
    }
    // 初始化接口文档
    private initializeSwagger() {
        // 生成文档路由
        this.app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
    }
    // 启动服务
    public listen(){
        this.app.listen(this.port, () => {
            console.log(`TypeScript with Express
              http://localhost:${this.port}/`);
        });
    }
}

export default App

二、初始化路由

路由类,这样就在index.ts中引入路由实例化,所有的路由就生效了,然后我们看每个路由对应的都是controller的方法,只要实现这些方法就可以了

import { Router } from 'express';
import MonitorsController from '../controllers/monitor.controller';
import { Routes } from '../interfaces/routes.interface';

class MonitorRoute implements Routes {
  public path = '/v1/monitor';
  public router = Router();
  public monitorsController = new MonitorsController();

  constructor() {
    this.initializeRoutes();
  }

  private initializeRoutes() {
    this.router.get(`${this.path}`, this.monitorsController.getMonitor);
    this.router.post(`${this.path}`, this.monitorsController.createMonitor);
    this.router.post(`${this.path}/:id`, this.monitorsController.getMonitorById);
  }
}

export default MonitorRoute;

三、初始化数据库

yarn add sequelize mysql2

安装两个依赖,一个操作数据库的orm,一个是数据库驱动。

databases中,我们初始化数据库

new Sequelize.Sequelize(database, user, password, {
    dialect: 'mysql',
    host: DB_HOST,
    port: DB_PORT
} as any);

四、配置区分生产环境和预发环境

package.json下新增scripts命令,配置项目以不同的环境变量启动

"start": "cross-env NODE_ENV=development nodemon src/index.ts",
"start:prod": "cross-env NODE_ENV=pruduction nodemon src/index.ts",

然后在项目根目录下新建.env.development.local文件,配置变量

PORT = 3000

DB_HOST = localhost
DB_PORT = 3306
DB_USER = root
DB_PASSWORD = 12345678
DB_DATABASE = stark

安装dotenv可以读取各种环境变量

yarn add dotenv

config/index.ts中,读取环境变量,在其他地方共享

import { config } from 'dotenv';
config({ path: `.env.${process.env.NODE_ENV || 'development'}.local` });
export { PORT } = process.env

之后根据业务需要写controller和service就可以了

五、引入swagger

如图所示,可以自动生成API,这样就不用,每次单独写了 image.png 安装依赖

yarn add swagger-jsdoc swagger-ui-express
    
yarn add @types/swagger-jsdoc @types/swagger-ui-express -D

配置swagger文档,读取对应的yaml文件,生成对应的路由,然后在项目初始化的时候执行该函数

private initializeSwagger() {
    const options = {
        swaggerDefinition: {
            info: {
                title: 'REST API',
                version: '1.0.0',
                description: 'Example docs',
            },
        },
        apis: ['swagger*.yaml'],
    };

    const specs = swaggerJSDoc(options);
    this.app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
}

yaml配置文件:配置文档路由和Modal(可以直接通过注释生成,需要自行查看文档)

tags:
- name: monitors
  description: monitors

paths:
# [GET] monitors
  /v1/monitor:
    get:
      tags:
      - monitors
      summary: Find All monitors
      responses:
        200:
          description: "OK"
        500:
          description: "Server Error"
# definitions
definitions:
  monitors:
    type: object
    required:
        - msg
    properties:
      msg:
        type: string
        description: error message
      column:
        type: number
        description: error column

六、配置PM2启动服务

在部署项目之前,需要先编译成js,这里我们使用swc(通过 rust 实现的 babel:swc,一个将 ES6 转化为 ES5 的工具)当然也可以配置webpack啥的。

yarn add @swc/cli @swc/core -D

在根目录下新建文件配置文件.swcrc,这里附一份配置,具体的内容可以查看文档

{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": false,
      "dynamicImport": true,
      "decorators": true
    },
    "transform": {
      "legacyDecorator": true,
      "decoratorMetadata": true
    },
    "target": "es2017",
    "externalHelpers": false,
    "keepClassNames": true,
    "loose": false,
    "minify": {
      "compress": false,
      "mangle": false
    },
    "baseUrl": "src",
    "paths": {
      "@/*": ["*"]
    }
  },
  "module": {
    "type": "commonjs"
  }
}

package.json中配置scripts命令

"build": "swc src -d dist --source-maps --copy-files",

执行yarn build,就可以看到根目录下生成了dist目录,就是解析后的js文件,要部署的也是这个文件

接着我们使用pm2启动项目

yarn add global pm2

因为pm2运行时肯定要区分生产环境和预发环境,所以我们需要给pm2增加配置文件.ecosystem.config.js

module.exports = {
  apps: [
    {
      name: 'monitor', // pm2 start App name
      script: 'dist/index.js',
      autorestart: true, // auto restart if process crash
      watch: false, // files change automatic restart
      ignore_watch: ['node_modules', 'logs'], // ignore files change
      max_memory_restart: '1G', // restart if process use more than 1G memory
      merge_logs: true, // if true, stdout and stderr will be merged and sent to pm2 log
      output: './logs/access.log', // pm2 log file
      error: './logs/error.log', // pm2 error log file
      env_test: {
        PORT: 3000,
        NODE_ENV: 'development',
        DB_HOST: "localhost",
        DB_PORT: 3306,
        DB_USER: "root",
        DB_PASSWORD: 12345678,
        DB_DATABASE: "stark"
      },
      env_production: { // environment variable
        PORT: 3000,
        NODE_ENV: 'production',
        DB_HOST: "localhost",
        DB_PORT: 3306,
        DB_USER: "root",
        DB_PASSWORD: "12345",
        DB_DATABASE: "monitor"
      }
    }
  ]
};

执行命令pm2 start ecosystem.config.js --env test

image.png

到这里已经可以部署项目成功了,接着我们通过Docker部署一下

七、Docker构建镜像并部署

FROM node:18-alpine as common-build-stage

COPY . ./app

WORKDIR /app

RUN npm i -g pm2 --registry=https://registry.npm.taobao.org && yarn add production

EXPOSE 3000

FROM common-build-stage as production-build-stage

ENV NODE_ENV production

CMD ["pm2-runtime", "start", "ecosystem.config.js", "--env", "production"]

这里使用pm2-runtime,是因为如果pm2的话,Docker监听不到服务的运行,就会退出,所以这里pm2官方给出了pm2-runtime来解决这个问题

docker build -t demo --platform linux/amd64 --target production-build-stage -f Dockerfile .

我们要构建的镜像最终是要部署到centos上的,但是m1下打包的无法兼容,所以增加参数--platform linux/amd64 就可以了

八、项目中使用的插件