文章目录
-
- 目标
- [什么是 Egg.js?](#什么是 Egg.js?)
- [Egg.js 项目起步](#Egg.js 项目起步)
-
- [1. 创建项目](#1. 创建项目)
- [2. 项目结构](#2. 项目结构)
- [Egg.js 核心概念](#Egg.js 核心概念)
-
- [1. 应用程序结构](#1. 应用程序结构)
- [2. 控制器 (Controller)](#2. 控制器 (Controller))
- [3. 服务 (Service)](#3. 服务 (Service))
- [4. 路由配置](#4. 路由配置)
- 配置管理
-
- [1. 默认配置](#1. 默认配置)
- [2. 开发环境配置](#2. 开发环境配置)
- [3. 生产环境配置](#3. 生产环境配置)
- [4. 插件配置](#4. 插件配置)
- 中间件开发
-
- [1. 认证中间件](#1. 认证中间件)
- [2. 参数验证中间件](#2. 参数验证中间件)
- [3. 错误处理中间件](#3. 错误处理中间件)
- 数据库集成
-
- [1. Sequelize 模型定义](#1. Sequelize 模型定义)
- 认证与授权
-
- [1. JWT 配置](#1. JWT 配置)
- [2. 认证控制器](#2. 认证控制器)
- 定时任务
- 部署与生产环境
-
- [1. 环境变量配置](#1. 环境变量配置)
- [2. PM2 配置](#2. PM2 配置)
- [3. Docker 配置](#3. Docker 配置)
- 总结
目标
- 了解什么是egg.js。
- 学会简单使用egg.js。创建一个简单的egg.js。
什么是 Egg.js?
Egg.js 是基于 Node.js 和 Koa 的企业级应用开发框架,它为开发团队和项目提供了清晰的约定和规范,让开发者能够专注于业务逻辑,快速构建高质量的应用程序。
Egg.js 遵循 "约定优于配置" 的原则,提供了一套完整的 MVC 解决方案,内置了多进程管理、插件机制、框架扩展等企业级特性,帮助团队和开发者降低开发和维护成本。
- Egg.js 是阿里巴巴开源的企业级 Node.js 框架
- 基于 Koa 2.x,异步解决方案基于 async/await
- 提供高度可扩展的插件机制和框架定制能力
- 内置多进程模型,充分利用多核 CPU 资源
- 提供统一的开发规范和最佳实践
Egg.js 的优势
- 完善的生态系统:丰富的官方插件和社区插件
- 企业级稳定性:经过阿里巴巴大规模业务验证
- 高度可扩展:灵活的插件机制和框架定制能力
- 开发体验友好:完善的开发工具链和调试支持
- 团队协作高效:统一的约定和规范,降低沟通成本
适用场景
- 大型企业级应用开发
- 需要统一技术栈和开发规范的团队
- 高并发、高性能要求的应用
- 需要快速迭代的业务系统
- 微服务架构中的单体服务
Egg.js 项目起步
1. 创建项目
bash
# 使用 egg-init 创建项目
npm init egg --type=simple
# 或者使用官方脚手架
npx egg-init egg-example --type=simple
# 进入项目目录
cd egg-example
# 安装依赖
npm install
# 启动开发服务器
npm run dev
2. 项目结构
egg-example/
├── app/
│ ├── controller/
│ │ └── home.js
│ ├── service/
│ ├── middleware/
│ ├── router.js
│ └── extend/
├── config/
│ ├── config.default.js
│ ├── config.prod.js
│ └── plugin.js
├── test/
├── typings/
├── .autod.conf.js
├── .eslintrc
├── .gitignore
├── package.json
└── README.md
Egg.js 核心概念
1. 应用程序结构
Egg.js 遵循约定式目录结构:
app/
├── controller/ # 控制器目录
├── service/ # 服务目录
├── model/ # 模型目录(可选)
├── middleware/ # 中间件目录
├── schedule/ # 定时任务目录
├── public/ # 静态资源目录
├── view/ # 模板目录
├── router.js # 路由配置
└── extend/ # 扩展目录
├── application.js
├── context.js
├── request.js
├── response.js
└── helper.js
2. 控制器 (Controller)
控制器负责处理用户请求并返回响应:
javascript
// app/controller/user.js
const { Controller } = require('egg');
class UserController extends Controller {
// 获取用户列表
async index() {
const { ctx, service } = this;
// 验证查询参数
const query = ctx.query;
ctx.validate({
page: { type: 'int', required: false, default: 1 },
pageSize: { type: 'int', required: false, default: 10 }
}, query);
// 调用服务层
const users = await service.user.list(query);
// 返回响应
ctx.body = {
success: true,
data: users,
message: '获取用户列表成功'
};
}
// 获取用户详情
async show() {
const { ctx, service } = this;
const { id } = ctx.params;
ctx.validate({
id: { type: 'id', required: true }
}, { id });
const user = await service.user.findById(id);
if (!user) {
ctx.throw(404, '用户不存在');
}
ctx.body = {
success: true,
data: user,
message: '获取用户详情成功'
};
}
// 创建用户
async create() {
const { ctx, service } = this;
const userData = ctx.request.body;
// 参数验证
ctx.validate({
username: { type: 'string', required: true },
email: { type: 'email', required: true },
password: { type: 'string', required: true, min: 6 }
});
const user = await service.user.create(userData);
ctx.status = 201;
ctx.body = {
success: true,
data: user,
message: '用户创建成功'
};
}
// 更新用户
async update() {
const { ctx, service } = this;
const { id } = ctx.params;
const userData = ctx.request.body;
ctx.validate({
id: { type: 'id', required: true }
}, { id });
const user = await service.user.update(id, userData);
ctx.body = {
success: true,
data: user,
message: '用户更新成功'
};
}
// 删除用户
async destroy() {
const { ctx, service } = this;
const { id } = ctx.params;
ctx.validate({
id: { type: 'id', required: true }
}, { id });
await service.user.delete(id);
ctx.status = 204;
}
}
module.exports = UserController;
3. 服务 (Service)
服务层封装业务逻辑:
javascript
// app/service/user.js
const { Service } = require('egg');
class UserService extends Service {
// 获取用户列表
async list(query = {}) {
const { app } = this;
const { page = 1, pageSize = 10, ...where } = query;
const options = {
where,
limit: parseInt(pageSize),
offset: (parseInt(page) - 1) * parseInt(pageSize),
order: [[ 'created_at', 'DESC' ]]
};
const { count, rows } = await app.model.User.findAndCountAll(options);
return {
list: rows,
pagination: {
page: parseInt(page),
pageSize: parseInt(pageSize),
total: count,
totalPages: Math.ceil(count / parseInt(pageSize))
}
};
}
// 根据ID查找用户
async findById(id) {
const { app } = this;
const user = await app.model.User.findByPk(id, {
attributes: { exclude: ['password'] }
});
return user;
}
// 根据邮箱查找用户
async findByEmail(email) {
const { app } = this;
const user = await app.model.User.findOne({
where: { email }
});
return user;
}
// 创建用户
async create(userData) {
const { app, ctx } = this;
// 检查邮箱是否已存在
const existingUser = await this.findByEmail(userData.email);
if (existingUser) {
ctx.throw(422, '邮箱已被注册');
}
// 加密密码
userData.password = await ctx.genHash(userData.password);
const user = await app.model.User.create(userData);
// 返回用户信息(排除密码)
const result = user.toJSON();
delete result.password;
return result;
}
// 更新用户
async update(id, userData) {
const { app, ctx } = this;
const user = await app.model.User.findByPk(id);
if (!user) {
ctx.throw(404, '用户不存在');
}
// 如果更新邮箱,检查是否重复
if (userData.email && userData.email !== user.email) {
const existingUser = await this.findByEmail(userData.email);
if (existingUser) {
ctx.throw(422, '邮箱已被注册');
}
}
// 如果更新密码,重新加密
if (userData.password) {
userData.password = await ctx.genHash(userData.password);
}
await user.update(userData);
const result = user.toJSON();
delete result.password;
return result;
}
// 删除用户
async delete(id) {
const { app, ctx } = this;
const user = await app.model.User.findByPk(id);
if (!user) {
ctx.throw(404, '用户不存在');
}
await user.destroy();
return true;
}
}
module.exports = UserService;
4. 路由配置
javascript
// app/router.js
module.exports = app => {
const { router, controller, middleware } = app;
// 中间件
const auth = middleware.auth();
const validate = middleware.validate();
// 首页
router.get('/', controller.home.index);
// 用户相关路由
router.get('/users', controller.user.index);
router.get('/users/:id', controller.user.show);
router.post('/users', validate, controller.user.create);
router.put('/users/:id', auth, validate, controller.user.update);
router.delete('/users/:id', auth, controller.user.destroy);
// 认证相关路由
router.post('/auth/register', validate, controller.auth.register);
router.post('/auth/login', validate, controller.auth.login);
router.post('/auth/logout', auth, controller.auth.logout);
router.get('/auth/me', auth, controller.auth.me);
// 文章相关路由
router.get('/posts', controller.post.index);
router.get('/posts/:id', controller.post.show);
router.post('/posts', auth, validate, controller.post.create);
router.put('/posts/:id', auth, validate, controller.post.update);
router.delete('/posts/:id', auth, controller.post.destroy);
};
配置管理
1. 默认配置
javascript
// config/config.default.js
const { EggAppConfig, EggAppInfo, PowerPartial } = require('egg');
module.exports = (appInfo: EggAppInfo) => {
const config = {} as PowerPartial<EggAppConfig>;
// 覆盖框架默认配置
config.keys = appInfo.name + '_1234567890';
// 中间件配置
config.middleware = ['errorHandler'];
// 安全配置
config.security = {
csrf: {
enable: false, // 开发环境关闭 CSRF
},
};
// 跨域配置
config.cors = {
origin: '*',
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH',
};
// 数据库配置
config.sequelize = {
dialect: 'mysql',
host: '127.0.0.1',
port: 3306,
database: 'egg_example',
username: 'root',
password: 'password',
timezone: '+08:00',
define: {
timestamps: true,
paranoid: true,
underscored: true,
freezeTableName: true,
},
};
// 参数验证配置
config.validate = {
convert: true,
widelyUndefined: true,
};
// 日志配置
config.logger = {
level: 'INFO',
consoleLevel: 'DEBUG',
};
return {
...config,
};
};
2. 开发环境配置
javascript
// config/config.local.js
module.exports = () => {
const config = {};
// 开发环境数据库配置
config.sequelize = {
dialect: 'mysql',
host: '127.0.0.1',
port: 3306,
database: 'egg_example_dev',
username: 'root',
password: 'password',
logging: console.log, // 显示 SQL 日志
};
// 开发环境日志配置
config.logger = {
level: 'DEBUG',
consoleLevel: 'DEBUG',
};
return config;
};
3. 生产环境配置
javascript
// config/config.prod.js
module.exports = () => {
const config = {};
// 生产环境数据库配置
config.sequelize = {
dialect: 'mysql',
host: process.env.DB_HOST || '127.0.0.1',
port: process.env.DB_PORT || 3306,
database: process.env.DB_NAME || 'egg_example',
username: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || 'password',
logging: false, // 生产环境关闭 SQL 日志
};
// 生产环境日志配置
config.logger = {
level: 'WARN',
consoleLevel: 'WARN',
dir: '/path/to/your/logs', // 日志文件目录
};
// 集群配置
config.cluster = {
listen: {
path: '',
port: 7001,
hostname: '0.0.0.0',
},
};
return config;
};
4. 插件配置
javascript
// config/plugin.js
module.exports = {
// 数据库插件
sequelize: {
enable: true,
package: 'egg-sequelize',
},
// 参数验证插件
validate: {
enable: true,
package: 'egg-validate',
},
// 跨域插件
cors: {
enable: true,
package: 'egg-cors',
},
// Redis 插件
redis: {
enable: true,
package: 'egg-redis',
},
// 会话插件
session: {
enable: true,
package: 'egg-session',
},
// JWT 插件
jwt: {
enable: true,
package: 'egg-jwt',
},
};
中间件开发
1. 认证中间件
javascript
// app/middleware/auth.js
module.exports = (options = {}) => {
return async (ctx, next) => {
// 从请求头获取 token
const token = ctx.get('Authorization');
if (!token || !token.startsWith('Bearer ')) {
ctx.throw(401, '未提供认证令牌');
}
const accessToken = token.slice(7);
try {
// 验证 JWT token
const decoded = ctx.app.jwt.verify(accessToken, ctx.app.config.jwt.secret);
// 从数据库获取用户信息
const user = await ctx.service.user.findById(decoded.userId);
if (!user) {
ctx.throw(401, '用户不存在');
}
// 将用户信息挂载到上下文
ctx.state.user = user;
ctx.state.userId = user.id;
await next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
ctx.throw(401, '认证令牌已过期');
} else if (error.name === 'JsonWebTokenError') {
ctx.throw(401, '无效的认证令牌');
} else {
ctx.throw(401, '认证失败');
}
}
};
};
2. 参数验证中间件
javascript
// app/middleware/validate.js
module.exports = (options = {}) => {
return async (ctx, next) => {
try {
await next();
} catch (error) {
if (error.name === 'ValidationFailed') {
ctx.status = 422;
ctx.body = {
success: false,
message: '参数验证失败',
errors: error.errors
};
return;
}
throw error;
}
};
};
3. 错误处理中间件
javascript
// app/middleware/error_handler.js
module.exports = (options = {}) => {
return async (ctx, next) => {
try {
await next();
// 处理 404
if (ctx.status === 404 && !ctx.body) {
if (ctx.acceptJSON) {
ctx.body = {
success: false,
message: '接口不存在'
};
} else {
ctx.body = '<h1>页面不存在</h1>';
}
}
} catch (error) {
// 记录错误日志
ctx.app.emit('error', error, ctx);
const status = error.status || 500;
const message = status === 500 ? '内部服务器错误' : error.message;
// 生产环境不返回错误堆栈
const isProd = ctx.app.config.env === 'prod';
ctx.status = status;
if (ctx.acceptJSON) {
ctx.body = {
success: false,
message,
...(isProd ? {} : { stack: error.stack })
};
} else {
ctx.body = isProd ?
`<h1>${message}</h1>` :
`<h1>${message}</h1><pre>${error.stack}</pre>`;
}
}
};
};
数据库集成
1. Sequelize 模型定义
javascript
// app/model/user.js
module.exports = app => {
const { STRING, INTEGER, DATE, BOOLEAN } = app.Sequelize;
const User = app.model.define('user', {
id: {
type: INTEGER,
primaryKey: true,
autoIncrement: true,
},
username: {
type: STRING(50),
allowNull: false,
unique: true,
comment: '用户名',
},
email: {
type: STRING(100),
allowNull: false,
unique: true,
validate: {
isEmail: true,
},
comment: '邮箱',
},
password: {
type: STRING(255),
allowNull: false,
comment: '密码',
},
avatar: {
type: STRING(255),
allowNull: true,
comment: '头像',
},
is_active: {
type: BOOLEAN,
defaultValue: true,
comment: '是否激活',
},
last_login_at: {
type: DATE,
allowNull: true,
comment: '最后登录时间',
},
created_at: DATE,
updated_at: DATE,
deleted_at: DATE,
}, {
tableName: 'users',
paranoid: true, // 软删除
underscored: true, // 字段名使用下划线
});
// 关联关系
User.associate = function() {
app.model.User.hasMany(app.model.Post, {
foreignKey: 'author_id',
as: 'posts',
});
};
return User;
};
javascript
// app/model/post.js
module.exports = app => {
const { STRING, TEXT, INTEGER, DATE, BOOLEAN, ENUM } = app.Sequelize;
const Post = app.model.define('post', {
id: {
type: INTEGER,
primaryKey: true,
autoIncrement: true,
},
title: {
type: STRING(200),
allowNull: false,
comment: '文章标题',
},
content: {
type: TEXT('long'),
allowNull: false,
comment: '文章内容',
},
excerpt: {
type: TEXT,
allowNull: true,
comment: '文章摘要',
},
status: {
type: ENUM('draft', 'published', 'archived'),
defaultValue: 'draft',
comment: '文章状态',
},
view_count: {
type: INTEGER,
defaultValue: 0,
comment: '阅读次数',
},
author_id: {
type: INTEGER,
allowNull: false,
comment: '作者ID',
},
published_at: {
type: DATE,
allowNull: true,
comment: '发布时间',
},
created_at: DATE,
updated_at: DATE,
deleted_at: DATE,
}, {
tableName: 'posts',
paranoid: true,
underscored: true,
});
// 关联关系
Post.associate = function() {
app.model.Post.belongsTo(app.model.User, {
foreignKey: 'author_id',
as: 'author',
});
app.model.Post.belongsToMany(app.model.Category, {
through: 'post_categories',
foreignKey: 'post_id',
otherKey: 'category_id',
as: 'categories',
});
};
return Post;
};
认证与授权
1. JWT 配置
javascript
// config/config.default.js
module.exports = appInfo => {
const config = {};
// JWT 配置
config.jwt = {
secret: appInfo.name + '_jwt_secret_123456',
expiresIn: '7d', // token 过期时间
};
return config;
};
2. 认证控制器
javascript
// app/controller/auth.js
const { Controller } = require('egg');
class AuthController extends Controller {
// 用户注册
async register() {
const { ctx, service } = this;
const userData = ctx.request.body;
// 参数验证
ctx.validate({
username: { type: 'string', required: true, min: 2, max: 50 },
email: { type: 'email', required: true },
password: { type: 'string', required: true, min: 6 }
});
const user = await service.user.create(userData);
// 生成 token
const token = ctx.app.jwt.sign(
{ userId: user.id },
ctx.app.config.jwt.secret,
{ expiresIn: ctx.app.config.jwt.expiresIn }
);
ctx.success({
user: {
id: user.id,
username: user.username,
email: user.email
},
token
}, '注册成功');
}
// 用户登录
async login() {
const { ctx, service } = this;
const { email, password } = ctx.request.body;
ctx.validate({
email: { type: 'email', required: true },
password: { type: 'string', required: true }
});
// 查找用户
const user = await service.user.findByEmail(email);
if (!user) {
ctx.throw(401, '邮箱或密码错误');
}
// 验证密码
const isValidPassword = await ctx.comparePassword(password, user.password);
if (!isValidPassword) {
ctx.throw(401, '邮箱或密码错误');
}
// 更新最后登录时间
await user.update({ last_login_at: new Date() });
// 生成 token
const token = ctx.app.jwt.sign(
{ userId: user.id },
ctx.app.config.jwt.secret,
{ expiresIn: ctx.app.config.jwt.expiresIn }
);
ctx.success({
user: {
id: user.id,
username: user.username,
email: user.email
},
token
}, '登录成功');
}
// 退出登录
async logout() {
// JWT 是无状态的,客户端删除 token 即可
ctx.success(null, '退出成功');
}
// 获取当前用户信息
async me() {
const { ctx, service } = this;
const userId = ctx.state.userId;
const user = await service.user.findById(userId);
ctx.success({
user: {
id: user.id,
username: user.username,
email: user.email,
avatar: user.avatar,
last_login_at: user.last_login_at
}
}, '获取用户信息成功');
}
}
module.exports = AuthController;
定时任务
javascript
// app/schedule/clear_expired_tokens.js
const { Subscription } = require('egg');
class ClearExpiredTokens extends Subscription {
// 通过 schedule 属性来设置定时任务的执行间隔等配置
static get schedule() {
return {
interval: '1h', // 1 小时间隔
type: 'all', // 指定所有的 worker 都需要执行
};
}
// subscribe 是真正定时任务执行时被运行的函数
async subscribe() {
const { ctx } = this;
try {
// 清理过期的 refresh tokens 等
const result = await ctx.app.model.Token.destroy({
where: {
expires_at: {
[ctx.app.Sequelize.Op.lt]: new Date()
}
}
});
if (result > 0) {
ctx.logger.info(`Cleared ${result} expired tokens`);
}
} catch (error) {
ctx.logger.error('Clear expired tokens error:', error);
}
}
}
module.exports = ClearExpiredTokens;
部署与生产环境
1. 环境变量配置
bash
# .env.production
NODE_ENV=production
EGG_SERVER_ENV=prod
DB_HOST=localhost
DB_PORT=3306
DB_USER=egg_blog
DB_PASSWORD=your_password
DB_NAME=egg_blog_prod
JWT_SECRET=your-jwt-secret-key
REDIS_HOST=localhost
REDIS_PORT=6379
2. PM2 配置
javascript
// ecosystem.config.js
module.exports = {
apps: [{
name: 'egg-blog',
script: 'node',
args: '--require egg-scripts/start',
instances: 'max', // 根据 CPU 核心数启动实例
exec_mode: 'cluster', // 集群模式
watch: false,
env: {
EGG_SERVER_ENV: 'prod',
NODE_ENV: 'production',
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_file: './logs/combined.log',
time: true,
merge_logs: true,
instance_var: 'INSTANCE_ID',
}],
};
3. Docker 配置
dockerfile
# Dockerfile
FROM node:14-alpine
# 设置时区
RUN apk add --no-cache tzdata
ENV TZ Asia/Shanghai
# 创建应用目录
WORKDIR /app
# 复制 package.json
COPY package*.json ./
# 安装依赖
RUN npm install --production
# 复制应用代码
COPY . .
# 创建日志目录
RUN mkdir -p /app/logs
# 暴露端口
EXPOSE 7001
# 启动应用
CMD ["npm", "start"]
yaml
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "7001:7001"
environment:
- NODE_ENV=production
- EGG_SERVER_ENV=prod
depends_on:
- mysql
- redis
volumes:
- ./logs:/app/logs
restart: unless-stopped
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: egg_blog
MYSQL_USER: egg_user
MYSQL_PASSWORD: egg_password
volumes:
- mysql_data:/var/lib/mysql
restart: unless-stopped
redis:
image: redis:6-alpine
volumes:
- redis_data:/data
restart: unless-stopped
volumes:
mysql_data:
redis_data:
总结
Egg.js 作为一个企业级的 Node.js 框架,通过其"约定优于配置"的设计理念,为团队提供了统一的开发规范和最佳实践。它基于 Koa 2.x,充分利用了现代 JavaScript 的特性,同时提供了完整的 MVC 解决方案和丰富的插件生态。
本文详细介绍了 Egg.js 的核心概念、目录结构、配置管理、中间件开发、数据库集成、认证授权。掌握这些知识后,你将能够使用 Egg.js 构建出高质量、可维护的企业级应用程序。
Egg.js 的插件机制和框架扩展能力使其具有极高的灵活性,能够满足各种复杂业务场景的需求。无论是小型项目还是大型企业应用,Egg.js 都能提供稳定可靠的解决方案。