likes
comments
collection
share

Egg+Mysql从零到一开发部署配置指南

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

前言

开始之前简单介绍一下,Egg继承于Koa,在其基础上进行了进一步的封装,官方也给出了项目的基本目录结构,这里不再多说,详见官方文档

egg-project
├── package.json
├── app.js (可选)
├── agent.js (可选)
├── app
|   ├── router.js
│   ├── controller
│   |   └── home.js
│   ├── service (可选)
│   |   └── user.js
│   ├── middleware (可选)
│   |   └── response_time.js
│   ├── schedule (可选)
│   |   └── my_task.js
│   ├── public (可选)
│   |   └── reset.css
│   ├── view (可选)
│   |   └── home.tpl
│   └── extend (可选)
│       ├── helper.js (可选)
│       ├── request.js (可选)
│       ├── response.js (可选)
│       ├── context.js (可选)
│       ├── application.js (可选)
│       └── agent.js (可选)
├── config
|   ├── plugin.js
|   ├── config.default.js
│   ├── config.prod.js
|   ├── config.test.js (可选)
|   ├── config.local.js (可选)
|   └── config.unittest.js (可选)
└── test
    ├── middleware
    |   └── response_time.test.js
    └── controller
        └── home.test.js

环境搭建

Mysql安装

项目开始之前首先需要搭建数据库环境,采用docker可以很快速的完成mysql的安装,执行以下命令:

docker pull mysql // docker仓库拉取mysql镜像文件
docker run -p 3306:3306 --name mysqlContainer -e MYSQL_ROOT_PASSWORD=root -d mysql // 运行mysql容器 -p 本地主机端口映射到docker容器端口 --name 设置容器名称 -e 配置root密码 -d 镜像名称

安装完成后可以使用docker ps命令查看容器的运行情况,成功运行以后本地可以使用navicat客户端工具连接登录mysql进行相关表模型增加查询等相关操作。

Egg+Mysql从零到一开发部署配置指南

Egg项目初始化

接下来,使用官方提供的脚手架,快速生成项目:

mkdir egg-example && cd egg-example
npm init egg --type=simple
npm i

Egg配置开发

model

model层是用于存放数据库模型的地方,本次项目采用sequelize-auto插件自动根据数据库生成对应的模型文件,解放双手,不需要手动定义,并使用sequelize读写数据库更加便捷,首先安装相应的插件:

npm install sequelize mysql2
npm install sequelize-auto -D

接着执行自动生成模型脚本文件:

node ./database/autoModels.js
// autoModels.js
const SequelizeAuto = require('sequelize-auto');

const db = {
  dialect: 'mysql', // 数据库类型
  host: '127.0.0.1', // 数据库地址
  port: 3306, // 数据库端口
  database: 'userDB', // 数据库名称
  username: 'root', // 用户名
  password: 'root' // 密码
};
// 通过连接数据库自动同步表模型文件
const auto = new SequelizeAuto(db.database, db.username, db.password, {
  host: db.host,
  dialect: db.dialect,
  directory: './app/model/', // 生成文件存放目录
  port: db.port,
  additional: {
    timestamps: false // 不自动添加createdAt和updatedAt时间到模型里
  },
  lang: 'ts',
  caseModel: 'p', // 模型名称采用大驼峰PascalCase形式
  caseFile: 'k', // 文件名称采用kebab-case形式
  caseProp: 'c', // 属性名称采用小驼峰camelCase形式
  singularize: true // 数据库表名是复数的话采用单数形式生成文件名
});

auto.run();

最后在config中引入相关配置:

import { Sequelize } from 'sequelize'
import { initModels } from '../app/model/init-models'

export default () => {
  const config: PowerPartial<EggAppConfig> = {};
  config.sequelize = {
    dialect: 'mysql',
    host: '127.0.0.1',
    port: 3306,
    database: 'userDB',
    username: 'root',
    password: 'root'
  }
  initModels(new Sequelize(config.sequelize.database || '', config.sequelize.username || '', config.sequelize.password, { dialect: config.sequelize.dialect, logging: false, host: config.sequelize.host, port: config.sequelize.port, timezone: '+08:00' }))
  return config
};

以上就完成了数据模型的初始化定义,接下来就可以在service层实现数据库的增删改查了。

service

首先我们可以定义一个基础的service类,用于数据库基本的增删改查方法的编写,通过传入相应的modelName来调用对应的方法:

import { Service } from 'egg'
import * as model  from '../model/init-models'

export default class BaseService extends Service {
  // 查询数据
  _findAll(modelName, condition) {
    return model[modelName].findAll(condition)
  }

  // 根据条件查找某个数据
  _findOne(modelName, condition) {
    return model[modelName].findOne(condition)
  }
  
  // 根据id查询
  _findByPk(modelName, id) {
    return model[modelName].findByPk(id)
  }
  
  // 添加数据
  _add(modelName, data) {
    return model[modelName].create(data)
  }
  
  // 删除数据
  async _delete(modelName, key) {
    const result = await model[modelName].findByPk(key)
    if (result) {
      return model[modelName].destroy()
    } else {
      this.ctx.throw('该数据不存在')
    }
  }

  // 编辑数据
  async _update(modelName, key, data) {
    const result = await model[modelName].findByPk(key)
    if (result) {
      return result.update({ ...data })
    } else {
      this.ctx.throw('该数据不存在')
    }
  }
}

接着就可以在此基础上继承创建其他的service类,进行数据库增删改查操作方法的调用。

controller

controller层主要是编写相关的业务控制逻辑,它通过调用service层相关方法获取数据并进行封装返回给api调用者。类似于service同样可以定义一个基础的controller类,封装公共的请求成功和失败调用方法:

import { Controller } from 'egg'

export default class BaseController extends Controller {
  // 操作成功,返回数据
  async success(data: any, msg = 'OK', code = 0) {
    const { ctx } = this
    ctx.body = {
      code,
      msg,
      data
    }
  }

  // 操作失败,返回数据
  async error(msg = '当前系统繁忙', code = -1) {
    const { ctx } = this
    ctx.body = {
      code,
      msg
    }
  }
}

router

router层主要用于配置URL路由规则,定义了访问路径和控制器之间的关系,说白了就是用户请求访问某个路径,调用对应的controller方法返回相应的数据:

import { Application } from 'egg'

export default (app: Application) => {
  const { controller, router } = app
  router.get('/', controller.home.index)
  router.post('/user/register', controller.user.register)
  router.post('/user/login', controller.user.login)
}

middleware

egg的中间件和koa一样,是基于洋葱模型进行处理,我们可以定义一个异常错误捕获的中间件,方便捕获全局的错误。先在middleware文件夹下定义好错误处理中间件:

export default function errorHandler() {
  return async (ctx, next) => {
    try {
      await next()
    } catch (err: any) {
      // 异常在app上触发一个error事件,框架会记录一条错误日志
      if (!err?.status) {
        ctx.app.emit('error', err, ctx)
      }
      ctx.status = err?.status || 500
      ctx.body = { code: -1, msg: err.msg || '系统异常' }
    }
  }
}

然后在配置文件中进行中间件的使用配置:

import { EggAppConfig, EggAppInfo, PowerPartial } from 'egg';

export default (appInfo: EggAppInfo) => {
  const config = {} as PowerPartial<EggAppConfig>
  ...
  // add your egg config in here
  config.middleware = ['errorHandler'];
  ...
}

项目实战

以上就是egg相关的一些基础配置,接下来就用上述介绍的方法来实现一个账号注册和登录的需求。

model设计

首先通过dbdiagram网站在线设计一个user表,并导出sql文件。

Egg+Mysql从零到一开发部署配置指南

CREATE TABLE `users` (
  `id` int PRIMARY KEY AUTO_INCREMENT,
  `name` varchar(50),
  `password` varchar(50),
  `create_time` datetime DEFAULT (now()),
  `update_time` datetime
);

然后使用navicat软件中执行导出的sql命令,生成对应的表结构。

Egg+Mysql从零到一开发部署配置指南 最后在项目中执行squelize-auto命令在model目录下生成对应的模型文件。

账号注册

接下来设计service层和controller层相关功能,账号在注册时主要就是将用户名和用户密码保存到数据库当中,确保用户名的唯一性,并且密码在保存到数据库时一般不会直接明文入库,采用加密的方式进行保存:

// controller/user
import BaseController from './base';
import crypto from 'crypto'
export default class UserController extends BaseController {
  modelName = 'user'
  async register() {
    const { ctx, service } = this;
    let { name, password } = ctx.request.body || {}
    password = crypto.createHash('md5').update(password).digest('hex') // 账号加密处理
    const result = await service[this.modelName].add({ name, password })
    this.success(result)
  }
}

// service/user
import BaseService  from './base';
import { UserCreationAttributes } from '../model/init-models';
export default class UserService extends BaseService {
  modelName = 'User'
  findOne(name) {
    return this._findOne(this.modelName, { where: { name } })
  }
  async add(data: UserCreationAttributes) {
    const user = await this.findOne(data.name)
    if (user) {
      this.ctx.throw(200, { msg: '用户名已存在,请重新输入' })
    }
    return this._add(this.modelName, data)
  }
}

账号登录

因为http是无状态的,想要识别用户的行为操作,就需要账号登录返回身份识别信息带给前台,前台在下次操作时将这个信息传给后端进行识别,一般可以通过session和token进行实现。本文是采用的token方式,首先安装egg-jwt,并启用它:

// pligin.ts
jwt: {
  enable: true,
  package: 'egg-jwt'
}

安装完成后需要配置jwt私有key:

// config.default.ts
config.jwt = {
  secret: '123456'
}

配置完成后在登录接口中调用egg-jwt将用户的id信息生成token,并以cookie的形式返回给前台,这样前台在下次请求时就会自动带上这个token:

// controller/user
async login() {
  const { ctx, service, app } = this;
  let { name, password } = ctx.request.body || {}
  const user = await service[this.modelName].findOne(name)
  if (user) {
    password = crypto.createHash('md5').update(password).digest('hex')
    if (password === user.password) { // 判断密码是否正确
      const token = app.jwt.sign({ id: user.id }, app.config.jwt.secret, { expiresIn: '1h' })
      ctx.cookies.set('token', token)
      this.success('登录成功')
    } else {
      this.error('密码错误,请重新输入')
    }
  } else {
    this.error('账号不存在,请重新确认')
  }
}

最后我们还需要一个方法来校验这个token是否还在有效期内,同时解析token拿到用户的id,我们可以借助中间件实现这个功能:

// 定义token校验解析处理中间件tokenHandler
export default function tokenHandler(options, app) {
  return async (ctx, next) => {
    try {
      const token = ctx.cookies.get('token', {
        signed: false
      })
      const data = app.jwt.verify(token, app.config.jwt.secret) // 解析token
      ctx.request.body.userId = data.id // 将解析出来的用户id放到body上,方便其他接口的处理
      await next()
    } catch (err) {
      ctx.body = { code: 1001, msg: '登录失效,请重新登录' }
    }
  }
}

配置token中间件后会在全局生效,但其实用户注册和登录接口并不需要进行token处理,我们可以在配置文件中设定ignore数组排除特定接口:

config.middleware = ['tokenHandler'];
config.tokenHandler = {
  ignore:['/user/register', '/user/login'] // 忽略特定请求,不做处理
}

docker部署

以上我们就完成了一个简单的账号登录注册功能,接下来我们可以通过docker进行本地的部署,首先需要在项目根目录编写Dockerfile:

FROM node:16.15.0

RUN mkdir -p /home/egg

WORKDIR /home/egg

COPY package.json /home/egg/package.json

RUN npm i --registry=https://registry.npmmirror.com/

# 拷贝所有源代码到工作目
COPY . /home/egg

# 暴露容器端口
EXPOSE 7001

RUN npm run tsc

# 修改原有egg-scripts执行命令,不以后台方式运行,egg-scripts start --title=egg-server-practise
CMD npm run start

接着运行docker命令构建镜像:

docker build -t eggimage ./ 

最后启动容器运行即可:

docker run -itd --name egg1 -p 8000:7001  eggimage

本地通过127.0.0.1:8000即可访问运行的服务,不过有个地方需要注意,docker容器中运行的egg服务是没法直接访问到宿主机端口的,所以会导致本地数据库连接失败,需要在生产环境配置文件中修改一下数据库连接地址,可以通过host.docker.internal来访问宿主机:

config.sequelize = {
  dialect: 'mysql',
  host: 'host.docker.internal', // 本地docker部署的话需要用这个域名来访问宿主机的网络,线上部署选择数据库的域名地址就行
  port: 3306
  ...
}

一般来说我们还要防止意外情况导致运行的容器意外停止,使用docker的restart策略,容器可以在启动docker守护程序时自动启动,通过以下命令即可设置:

docker update --restart=always [CONTAINER_ID/CONTAINER_NAME]

总结

以上就是本文的所有内容,主要是记录一下使用过程中的配置方法和遇到的问题,欢迎大家提出相关意见。