Egg+Mysql从零到一开发部署配置指南
前言
开始之前简单介绍一下,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项目初始化
接下来,使用官方提供的脚手架,快速生成项目:
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文件。
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命令,生成对应的表结构。
最后在项目中执行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]
总结
以上就是本文的所有内容,主要是记录一下使用过程中的配置方法和遇到的问题,欢迎大家提出相关意见。
转载自:https://juejin.cn/post/7204742468611850297