likes
comments
collection
share

从零开始学习一下 MidwayJs,打造个人后台服务

作者站长头像
站长
· 阅读数 3

MidwayJs Midway (midwayjs.org)、阿里巴巴、node框架、基于koa2、学习分享,官网默认示例也是基于用koa2(不知道为啥不用自家出的egg,不理解)。可能是egg对于ts的支持不是特别友好吧(纯属个人认为)。

话痨时刻

可能会有人回复,文章不错,我选nest。这这这,咋说呢,其实刚开始了解到这两个框架时,俺也在纠结,不知道该选哪个好,也在网上找了一些关于两者的文章和对比,也很苦劳,不知道选啥好,但最终还是向着“自家人”了。这里请允许俺解释一下,俺为啥决定选 MidwayJs

  • 毕竟是 “自家人” 开发的,中文文档比较通俗易懂,比较符合国人的开发习惯,还用官方群交流答疑;
  • nest是基于express,而Midway是基于koa2的,两者的底层不同(也不能这样说,毕竟人家koa2也是基于express的),俺个人刚开始习惯用koa2去写服务;
  • next官网说它借鉴了angular的理念,那时候俺只听说过angular,并没有真正去了解过,nest比较符合国外开发者的习惯,有中文文档但都是英译的或国内个人开发者总结的;
  • 都说 技术无国界 ,是真的无国界吗!

以上都是在俺的个人理解下,才决定选用Midway,当然,不管是nest还是Midway,各有各的优势和缺点,根据个人喜好和环境去选择,它们都是很不错的框架;

第一个服务

服务技术选型:

  • node
  • pnpm
  • midway
  • typescript
  • kao2
  • typeorm
  • mysql
  • redis
  • swagger

实现基于用户相关的接口和增删改查的demo

初始化创建

pnpm create midway

从零开始学习一下 MidwayJs,打造个人后台服务

cd midway-project
pnpm install
pnpm dev
# 启动后浏览器访问:http://127.0.0.1:7001

从零开始学习一下 MidwayJs,打造个人后台服务

调整ESLint配置

为了保证代码分隔统一,我们调整下ESLint配置

// .prettierrc.js
module.exports = {
  ...require('mwts/.prettierrc.json'),
  semi: false,
  printWidth: 100,
  tabWidth: 2,
  useTabs: false,
  singleQuote: true,
  endOfLine: 'lf',
  trailingComma: 'none',
}

ESLint 中文网 (nodejs.cn),不知道如何配置的话可以去官网查。

项目基础结构

├─logs			  # 日志文件
├─node_modules	          # 项目依赖
├─src                     # 源码目录
│  ├─config               # 配置
│  ├─controller           # 控制器
│  ├─entity               # 数据对象模型
│  ├─filter               # 过滤器
│  ├─middleware           # 中间件
│  ├─service              # 服务类
│  ├─configurations.ts    # 服务生命周期管理及配置
│  └─interface.ts         # 接口定义
├─test                    # 测试类目录
├─bootstrap.js            # 启动入口
├─package.json            # 包管理配置
├─tsconfig.json           # TypeScript 编译配置文件

使用vscode启动项目并调试

你可以不输入命令行,直接点击小绿三角形,就可以运行项目,当然,你也可以去package.json中可以看到调试字样,点击选择对应的命令即可。(俺只是觉得这个方法更帅一点,就这样😎)

从零开始学习一下 MidwayJs,打造个人后台服务

// 覆盖.vscode/launch.json初始配置
{
    // 使用 IntelliSense 了解相关属性。
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [{
        "name": "Midway Local",
        "type": "node",
        "request": "launch",
        "cwd": "${workspaceRoot}",
        "runtimeExecutable": "pnpm",
        "windows": {
            "runtimeExecutable": "pnpm.cmd"
        },
        "runtimeArgs": [
            "dev"
        ],
        "env": {
            "NODE_ENV": "local"
        },
        "console": "integratedTerminal",
        "restart": true,
        "autoAttachChildProcesses": true
    }]
}

从零开始学习一下 MidwayJs,打造个人后台服务

数据库mysql

可能有滴人不会用 docker ,那俺就在本地创建个数据库。

mysql -u root -p
password: ******
create database twhc;

使用TypeORM

TypeORM:(github.com)node.js现有社区最成熟的对象关系映射器(ORM )。

相关文档:

安装 typeorm 相关依赖,提供数据库ORM:

pnpm i @midwayjs/typeorm@3 typeorm --save

src/configuration.ts 引入 orm 组件:

import { Configuration, App } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import * as orm from '@midwayjs/typeorm';
// 略

@Configuration({
  imports: [
    koa,
    orm,
    // 略
  ],
  // 略
})
// 略

安装数据库mysql

pnpm install mysql2 --save

配置src/config/config.default.ts文件:

import { MidwayConfig } from '@midwayjs/core';

export default {
  // use for cookie sign key, should change to your own and keep security
  keys: '1701053437454_8538',
  koa: {
    port: 7001,
  },
  typeorm: {
    dataSource: {
      default: {
        // 单个数据库
        type: 'mysql',
        host: 'localhost',
        port: 3306,
        username: 'root',
        password: '123456',
        database: 'twhc',
        entities: ['**/entity/*{.ts,.js}'], // 扫描entity文件夹
        synchronize: true, // 如果第一次使用,不存在表,有同步的需求可以写 true,注意会丢数据
        logging: true
      }
    }
  }
} as MidwayConfig;

数据库客户端连接已创建的数据库,俺用的是 DataGrip 2023.2.2 推荐使用Navicat,它对 mysql 的支持更好,但俺用DataGrip 用习惯了,两个软件都是收费的,免费的推荐 DBeaver Download | DBeaver Community --- 下载 |DBeaver 社区 这个挺好用的:

从零开始学习一下 MidwayJs,打造个人后台服务

创建实体模型

entity 文件夹下创建个简单滴 user.ts ,使用 Entity 来定义一个实体模型类:(实现数据库同步更新数据表和字段)

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'

@Entity('user')
export class User {
  @PrimaryGeneratedColumn()
  id: number

  @Column()
  username: string

  @Column()
  password: string
}

启动服务,查看数据库客户端:

从零开始学习一下 MidwayJs,打造个人后台服务

测试一下typeorm,改造src/controller/home.controller.ts文件:

import { Controller, Get } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { User } from '../entity/user';
import { Repository } from 'typeorm';

@Controller('/')
export class HomeController {
  // 自动注入模型
  @InjectEntityModel(User)
  userModel: Repository<User>;
  @Get('/')
  async home(): Promise<User[]> {
    // 查询user表数据
    return await this.userModel.find();
  }
}

出现以下图示,表示没问题,因为还没有插入数据,所以为空:

从零开始学习一下 MidwayJs,打造个人后台服务

手动在数据库中添加一条数据,再测试一下:

从零开始学习一下 MidwayJs,打造个人后台服务

从零开始学习一下 MidwayJs,打造个人后台服务

缓存redis

使用docker安装redis:

拉取redis镜像:

# 搜索镜像
docker search redis
# 拉取
docker pull redis
# 创建目录用于挂载
mkdir -p /home/redis/myredis /home/redis/myredis/data

myredis.conf 是俺手动上传的 (redis.conf的标准文件在redis官网也可以找到):

# bind 192.168.1.100 10.0.0.1
# bind 127.0.0.1 ::1
#bind 127.0.0.1

protected-mode no

port 6379

tcp-backlog 511

requirepass 123456

timeout 0

tcp-keepalive 300

daemonize no

supervised no

pidfile /var/run/redis_6379.pid

loglevel notice

logfile ""

databases 30

always-show-logo yes

save 900 1
save 300 10
save 60 10000

stop-writes-on-bgsave-error yes

rdbcompression yes

rdbchecksum yes

dbfilename dump.rdb

dir ./

replica-serve-stale-data yes

replica-read-only yes

repl-diskless-sync no

repl-disable-tcp-nodelay no

replica-priority 100

lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no

appendonly yes

appendfilename "appendonly.aof"

no-appendfsync-on-rewrite no

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

aof-load-truncated yes

aof-use-rdb-preamble yes

lua-time-limit 5000

slowlog-max-len 128

notify-keyspace-events ""

hash-max-ziplist-entries 512
hash-max-ziplist-value 64

list-max-ziplist-size -2

list-compress-depth 0

set-max-intset-entries 512

zset-max-ziplist-entries 128
zset-max-ziplist-value 64

hll-sparse-max-bytes 3000

stream-node-max-bytes 4096
stream-node-max-entries 100

activerehashing yes

hz 10

dynamic-hz yes

aof-rewrite-incremental-fsync yes

rdb-save-incremental-fsync yes

从零开始学习一下 MidwayJs,打造个人后台服务

启动redis容器:

docker run --restart=always \
	--log-opt max-size=100m \
	--log-opt max-file=2 \
	-p 6379:6379 \
	--name myredis \
	-v /home/redis/myredis/myredis.conf:/etc/redis/redis.conf \
	-v /home/redis/myredis/data:/data \
	-d redis redis-server /etc/redis/redis.conf  \
	--appendonly yes  \
	--requirepass 123456
  1. –restart=always 总是开机启动
  2. –log 是日志方面的
  3. -p 6379:6379 将6379端口挂载出去(加粗是容器端口,:前面是宿主机端口)
  4. –name 给这个容器取一个名字
  5. -v 数据卷挂载: /home/redis/myredis/myredis.conf:/etc/redis/redis.conf 这里是将 liunx路径下的myredis.conf 和redis下的redis.conf 挂载在一起。 /home/redis/myredis/data:/data 这个同上
  6. -d redis 表示后台启动redis
  7. redis-server /etc/redis/redis.conf 以配置文件启动redis,加载容器内的conf文件,最终找到的是挂载的目录 /etc/redis/redis.conf 也就是liunx下的/home/redis/myredis/myredis.conf
  8. –appendonly yes 开启redis 持久化
  9. –requirepass 123456 设置密码 (设置密码只有好处,信俺没错😁)

查看容器运行日志:(--since 30m 是查看此容器30分钟之内的日志情况,同时也表示可成功运行)

docker logs --since 30m myredis

从零开始学习一下 MidwayJs,打造个人后台服务

进入容器:(此处跟着的 redis-cli 是直接将命令输在上面了)

docker exec -it myredis redis-cli

进入后,你还需输入密码:(不然会出现如下报错)

从零开始学习一下 MidwayJs,打造个人后台服务

auth 123456

从零开始学习一下 MidwayJs,打造个人后台服务

当出现OK字样时,就可以正常使用redis相关命令了。

redis常用命令_redis 控制台命令-CSDN博客,该篇文章有redis的一些常用命令(为啥选着用控制台去查看redis呢,啊,这这这,因为穷,那些redis客户端连接工具都要钱呀,晓得不。当下的业务场景还用不到redis那么频繁,只用到了存储就行了)

在项目中安装redis依赖:

pnpm i @midwayjs/redis@3 --save

引入redis组件,在 src/configuration.ts 中导入:

import { Configuration, App } from '@midwayjs/core';
// 略
import * as redis from '@midwayjs/redis';
import * as validate from '@midwayjs/validate';
// 略

@Configuration({
  imports: [
    // 略
    redis,
    validate,
    // 略
  ],
  // 略
})
// 略

src/config/config.default.ts配置redis:

import { MidwayConfig } from '@midwayjs/core';

export default {
  // 略
  redis: {
    client: {
      port: 6379, // Redis port
      host: "192.168.169.128", // Redis host
      password: "123456",
      db: 0,
    }
  }
} as MidwayConfig;

使用redis服务:

import { Controller, Get, Inject } from '@midwayjs/core';
// 略
import { RedisService } from '@midwayjs/redis';

@Controller('/')
export class HomeController {
  // 自动注入redis服务
  @Inject()
  redisService: RedisService;
 // 略
  async home(): Promise<string> {
    await this.redisService.set('username', '吴某凡');
    return await this.redisService.get('username');
  }
}

从零开始学习一下 MidwayJs,打造个人后台服务

连接的是索引为0的库:

# 切换到该索引下的库
select 0
# 表示查看该库下所有的key
keys *
# get key 查看key的值
get username

从零开始学习一下 MidwayJs,打造个人后台服务

swagger接口文档

Swagger框架,用于快速生成、描述、调用和可视化 RESTful 风格的 Web 服务。它可以在线快速生成接口文档,以及快速测试接口。

安装相关依赖:

pnpm install @midwayjs/swagger@3 --save
# 如果想要在服务器上输出 Swagger API 页面,则需要将 swagger-ui-dist 安装到依赖中
pnpm install swagger-ui-dist --save-dev

开启组件:在 configuration.ts 中(可以配置启用的环境,比如下面的代码指的是“只在 local 环境下启用”)

// 略
import * as swagger from '@midwayjs/swagger';
// 略

@Configuration({
  imports: [
    // 略
    {
      component: swagger,
      enabledEnvironment: ['local']
    },
    // 略
  ],
  // 略
})
// 略

然后启动项目,访问地址:

路径可以通过 swaggerPath 参数配置。

从零开始学习一下 MidwayJs,打造个人后台服务

多语言国际化

pnpm i @midwayjs/i18n@3 --save

使用组件:在 configuration.ts 中:

// 略
import * as i18n from '@midwayjs/i18n';
// 略

@Configuration({
  imports: [
    // 略
    i18n,
    // 略
  ],
  // 略
})
// 略

配置多语言方案:新建 src/locale 目录,用于放置文案文件

# 目录结构
├── src
│   ├── locales
|   │   ├── en_US.json      # 英文
|   │   └── zh_CN.json      # 中文
// en_US.json
{
  "hello": "Hello"
}
// zh_CN.json
{
  "hello": "你好"
}

src/config/config.default.ts 加入这两个 JSON,其中 default 是语言的默认分组:

import { MidwayConfig } from '@midwayjs/core';

export default {
  // 略
  i18n: {
    localeTable: {
      en_US: {
        default: require('../locales/en_US')
      },
      zh_CN: {
        default: require('../locales/zh_CN')
      }
    }
  }
} as MidwayConfig;

home.controller.ts 使用一下:

// 略
import { MidwayI18nService } from '@midwayjs/i18n';

@Controller('/')
export class HomeController {
  // 自动注入i18n服务
  @Inject()
  i18nService: MidwayI18nService;
  @Get('/')
  async home(): Promise<string> {
    return await this.i18nService.translate('hello', {args: {username: '小甜甜'}, locale: 'en_US'})
  }
}

从零开始学习一下 MidwayJs,打造个人后台服务

locale: 'zh_CN' 后:

从零开始学习一下 MidwayJs,打造个人后台服务

统一参数校验

选着刚开始的架构创建的项目默认安装了 @midwayjs/validate 。新建 src/dto/user.ts :

import { Rule, RuleType } from '@midwayjs/validate';

export class UserDTO {
  @Rule(RuleType.string().required())
  password: string;
}
// src/controller/home.controller.ts
import { Controller, Body, Inject, Post } from '@midwayjs/core';
// 略
import { UserDTO } from '../dto/user';

@Controller('/')
export class HomeController {
  // 略
  @Post('/')
  async home(@Body() user: UserDTO): Promise<void> {
    console.log(user);
  }
}

使用swagger-ui测试一下,先传一个空对象给后端:

从零开始学习一下 MidwayJs,打造个人后台服务

从零开始学习一下 MidwayJs,打造个人后台服务

从零开始学习一下 MidwayJs,打造个人后台服务

传入个password测试一下:

从零开始学习一下 MidwayJs,打造个人后台服务

从零开始学习一下 MidwayJs,打造个人后台服务

从零开始学习一下 MidwayJs,打造个人后台服务

自定义报错文本:

// src/dto/user.ts
@Rule(RuleType.string().required().error(new Error('密码不能为空!')))
  password: string;

从零开始学习一下 MidwayJs,打造个人后台服务

统一异常处理

在统一参数校验中,当校验失败时,Response body放回的是html,不想要这要的错误形式,可以换成json格式。

Midway提供了一个内置的异常处理器,负责处理应用程序中所有未处理的异常。当您的应用程序代码抛出一个异常处理时,该处理器就会捕获该异常,然后等待用户处理。

异常处理器的执行位置处于中间件之后,所以它能拦截所有的中间件和业务抛出的错误。

新建 filter/validate.filter.ts

import { Catch } from '@midwayjs/decorator';
import { MidwayValidationError } from '@midwayjs/validate';
import { Context } from '@midwayjs/koa';
import { MidwayI18nService } from '@midwayjs/i18n';

@Catch(MidwayValidationError)
export class ValidateErrorFilter {
  async catch(err: MidwayValidationError, ctx: Context) {
    // 获取国际化服务
    const i18nService = await ctx.requestContext.getAsync(MidwayI18nService);
    // 翻译
    const message = i18nService.translate(err.message) || err.message;
    // 未捕获的错误,是系统错误,错误码是500
    ctx.status = 422;
    return {
      code: 422,
      message,
    };
  }
}

configuration.ts文件中,注册刚才我们创建的过滤器:

import { Catch } from '@midwayjs/decorator';
import { MidwayValidationError } from '@midwayjs/validate';
import { Context } from '@midwayjs/koa';
import { MidwayI18nService } from '@midwayjs/i18n';

@Catch(MidwayValidationError)
export class ValidateErrorFilter {
  async catch(err: MidwayValidationError, ctx: Context) {
    // 获取国际化服务
    const i18nService = await ctx.requestContext.getAsync(MidwayI18nService);
    // 翻译
    const message = i18nService.translate(err.message) || err.message;
    // 未捕获的错误,是系统错误,错误码是500
    ctx.status = 422;
    return {
      code: 422,
      message,
    };
  }
}

从零开始学习一下 MidwayJs,打造个人后台服务

公共业务异常处理

在开发过程中,可能会需要做一些业务校验,业务校验的时候,我们需要对外抛出异常,这时候我们需要封装公共的业务异常类,和业务异常过滤器

创建 src/common/common.error.ts

import { MidwayError } from '@midwayjs/core'

export class CommonError extends MidwayError {
  constructor(message: string) {
    super(message)
  }
}

在filter下新建 common.filter.ts

import { Catch } from '@midwayjs/decorator'
import { CommonError } from '../common/common.error'
import { Context } from '@midwayjs/koa'
import { MidwayI18nService } from '@midwayjs/i18n'

@Catch(CommonError)
export class CommonErrorFilter {
  async catch(err: CommonError, ctx: Context) {
    // 获取国际化服务
    const i18nService = await ctx.requestContext.getAsync(MidwayI18nService)
    // 翻译
    const message = i18nService.translate(err.message) || err.message
    // 未捕获的错误,是系统错误,错误码是500
    ctx.status = 400
    return {
      code: 400,
      message
    }
  }
}

src/configuration.ts中注册过滤器:

// 略
import { CommonErrorFilter } from './filter/common.filter'

// 略
export class MainConfiguration {
  @App('koa')
  app: koa.Application

  async onReady() {
    // 略
    // add filter
    this.app.useFilter([ValidateErrorFilter, CommonErrorFilter])
  }
}

因为common.filter.ts 使用了国际化:

// zh_CN.json
{
  "hello": "你好 {username}",
  "error": "出错啦"
}

测试:在home.controller.ts中

// 略
import { CommonError } from '../common/common.error'

@Controller('/')
export class HomeController {
  // 略
  @Post('/')
  async home(): Promise<void> {
    throw new CommonError('error')
  }
}

从零开始学习一下 MidwayJs,打造个人后台服务

打印日志

对于后端来说日志还是很重要的,有利于后期定位线上bug,midway也内置了一套日志组件,用起来很简单

// home.controller.ts
// 略
// 日志打印
import { ILogger } from '@midwayjs/logger'

@Controller('/')
export class HomeController {
  @Inject()
  logger: ILogger

  @Post('/')
  async home(@Body() user: UserDTO): Promise<void> {
    this.logger.info('hello')
    console.log(user)
  }
}

从零开始学习一下 MidwayJs,打造个人后台服务

测试

测试还是很重要的,默认脚手架中,已经提供了这东西,所以你可以开箱即用的运行测试,仔细了解可看官方文档测试 | Midway (midwayjs.org)。俺这就不举例了,官方文档有举例。复制粘贴就行了。

demo

实现基于用户表增删改查的demo

修改用户实体类

俺将 entity/user.ts 修改为 entity/user.entity.ts ,不想与其他目录下的文件名重复(自己单纯洁癖,不改也行):

详细的参数配置可查看文档 TypeORM | Midway (midwayjs.org)

你可具体对应列的类型,这要可让数据库更加灵活,相关文档 实体 | TypeORM 中文文档 | TypeORM 中文网 (bootcss.com)

import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'

@Entity('user')
export class User {
  @PrimaryGeneratedColumn({ comment: '主键ID' })
  id: number

  @Column({ comment: '用户头像', nullable: true })
  avatarUrl: string

  @Column({ length: 50, unique: true, comment: '用户名' })
  username: string

  @Column({ length: 30, comment: '密码' })
  password: string

  @Column({ length: 11, default: '', comment: '手机号' })
  phone: string

  @Column({ type: 'bigint', nullable: true, comment: '更新用户ID' })
  updaterId: number

  @Column({ type: 'bigint', nullable: true, comment: '创建用户ID' })
  createrId: number

  @Column({ type: 'int', default: 1, comment: '状态 1 正常 0 禁用' })
  status: number

  @CreateDateColumn({ comment: '创建时间' })
  createTime: Date

  @UpdateDateColumn({ comment: '更新时间' })
  updateTime: Date
}

对应的数据库结构:

从零开始学习一下 MidwayJs,打造个人后台服务

用户信息参数校验

俺将 dto/user.ts 文件名改成了 user.dto.ts :使用到swagger来显示相关信息,使用其中的 ApiProperty 将其中的每个属性都进行了定义

import { Rule, RuleType } from '@midwayjs/validate'
import { ApiProperty } from '@midwayjs/swagger'

export class UserDTO {
  @ApiProperty({ description: '用户id' })
  @Rule(RuleType.allow(null))
  id?: number

  @ApiProperty({ description: '用户名' })
  @Rule(RuleType.string().required().error(new Error('用户名不能为空!')))
  username: string

  @ApiProperty({ description: '密码' })
  @Rule(RuleType.string().required().error(new Error('密码不能为空!')))
  password: string
}

用户服务层

创建 service/user.service.ts

你要是看不懂这上面的有些命令,那就在官网上搜,官网上啥都有。

import { Provide } from '@midwayjs/core'
import { InjectEntityModel } from '@midwayjs/typeorm'
import {  Repository } from 'typeorm'
import { User } from '../entity/user.entity'
import { IUserOptions } from '../interface'

@Provide()
export class UserService {
  @InjectEntityModel(User)
  userModel: Repository<User>

  // 新增
  async cerate(user: User) {
    await this.userModel.save(user)
    return user
  }

  async getUser(options: IUserOptions) {
    return {
      uid: options.uid,
      username: 'mockedName',
      phone: '12345678901',
      email: 'xxx.xxx@xxx.com'
    }
  }
}

有的代码段是脚手架自带的,俺嫌麻烦这就删除了。

用户控制层

创建 controller/user.controller.ts 文件:(提一嘴,@midwayjs/core和@midwayjs/decorator中都有类似的API,调用那个的都行,俺根据脚手架默认的来用@midwayjs/core里的)

import { Body, Controller, Inject, Post, Provide, ALL } from '@midwayjs/core'
import { Validate } from '@midwayjs/validate'
import { UserDTO } from '../dto/user.dto'
import { UserService } from '../service/user.service'
import { User } from '../entity/user.entity'

@Provide()
@Controller('/user')
export class UserController {
  // 注入用户服务层
  @Inject()
  userService: UserService

  // 创建用户
  @Post('/')
  @Validate()
  async create(@Body(ALL) data: UserDTO) {
    const user = new User()
    user.username = data.username
    user.password = data.password
    return await this.userService.create(user)
  }
}

http://127.0.0.1:7001/swagger-ui/index.html里进行测试:

在之前的用户信息参数校验中结合了swagger,使得在swagger文档中有校验参数提示:

从零开始学习一下 MidwayJs,打造个人后台服务

从零开始学习一下 MidwayJs,打造个人后台服务

从零开始学习一下 MidwayJs,打造个人后台服务

从零开始学习一下 MidwayJs,打造个人后台服务

好啦好啦

借鉴文章