安装egg
我们推荐直接使用脚手架,只需几条简单指令,即可快速生成项目(npm >=6.1.0):
mkdir egg-example && cd egg-example npm init egg --type=simple npm i
启动项目:
npm run dev open http://localhost:7001
目录结构
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
路由相关
1. get传值
// router.js router.get('/admin/:id', controller.admin.index); // controller async index(ctx) { // 获取路由get传值参数(路由:id) ctx.params; // 获取url的问号get传值参数 ctx.query; }
2. 4种配置方法
router.verb('path-match', app.controller.action); router.verb('router-name', 'path-match', app.controller.action);// 第一个参数可以给name router.verb('path-match', middleware1, ..., middlewareN, app.controller.action); router.verb('router-name', 'path-match', middleware1, ..., middlewareN, app.controller.action);
重定向
1. ctx
async index() { this.ctx.status = 301; // 把重定向改为301 this.ctx.redirect('/admin/add'); // 默认临时重定向 302 }
2. 路由重定向
app.router.redirect('/', '/home/index', 302);
3.路由分组
// app/router.js module.exports = app => { require('./router/news')(app); require('./router/admin')(app); }; // app/router/news.js module.exports = app => { app.router.get('/news/list', app.controller.news.list); app.router.get('/news/detail', app.controller.news.detail); }; // app/router/admin.js module.exports = app => { app.router.get('/admin/user', app.controller.admin.user); app.router.get('/admin/log', app.controller.admin.log); };
控制器
自定义 Controller 基类
// app/core/base_controller.js const { Controller } = require('egg'); class BaseController extends Controller { get user() { return this.ctx.session.user; } success(data) { this.ctx.body = { success: true, data, }; } notFound(msg) { msg = msg || 'not found'; this.ctx.throw(404, msg); } } module.exports = BaseController;
此时在编写应用的 Controller 时,可以继承 BaseController,直接使用基类上的方法:
//app/controller/post.js const Controller = require('../core/base_controller'); class PostController extends Controller { async list() { const posts = await this.service.listByUser(this.user); this.success(posts); } }
模板引擎
1. 安装和使用ejs
(1)安装:
npm i egg-view-ejs --save
(2)配置:/config
config/config.default.js
module.exports = appInfo => { ... config.view = { mapping: { '.html': 'ejs', }, }; ... };
config/plugin.js
module.exports = { // 配置ejs ejs: { enable: true, package: 'egg-view-ejs', } };
(3)使用
app/controller
async index() { const { ctx } = this; // 渲染变量 let msg = "测试内容"; let list = [1, 2, 3, 4, 5, 6]; // 渲染模板(render需要加await) await ctx.render('index', { msg, list }); }
app/view/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <!--渲染变量--> <%=msg%> <ul> <% for(var i=0; i < list.length; i++){ %> <li> <%=list[i]%> </li> <% } %> </ul> <!--加载 app/public 下的资源文件--> <img src="/public/images/find.png"> </body> </html>
服务(模型)
控制器调用 home 模型的 ceshi 方法
await this.service.home.ceshi();
模型之间相互调用(同上)
模型和数据库
配置和创建迁移文件
配置
- 安装并配置egg-sequelize插件(它会辅助我们将定义好的 Model 对象加载到 app 和 ctx 上)和mysql2模块:
npm install --save egg-sequelize mysql2
- 在config/plugin.js中引入 egg-sequelize 插件
exports.sequelize = { enable: true, package: 'egg-sequelize', };
- 在config/config.default.js
config.sequelize = { dialect: 'mysql', host: '127.0.0.1', username: 'root', password: 'root', port: 3306, database: 'friends', // 中国时区 timezone: '+08:00', define: { // 取消数据表名复数 freezeTableName: true, // 自动写入时间戳 created_at updated_at timestamps: true, // 字段生成软删除时间戳 deleted_at paranoid: true, createdAt: 'created_at', updatedAt: 'updated_at', deletedAt: 'deleted_at', // 所有驼峰命名格式化 underscored: true } };
- sequelize 提供了sequelize-cli工具来实现Migrations,我们也可以在 egg 项目中引入 sequelize-cli。
npm install --save-dev sequelize-cli
- egg 项目中,我们希望将所有数据库 Migrations 相关的内容都放在database目录下,所以我们在项目根目录下新建一个.sequelizerc配置文件:
'use strict'; const path = require('path'); module.exports = { config: path.join(__dirname, 'database/config.json'), 'migrations-path': path.join(__dirname, 'database/migrations'), 'seeders-path': path.join(__dirname, 'database/seeders'), 'models-path': path.join(__dirname, 'app/model'), };
- 初始化 Migrations 配置文件和目录
npx sequelize init:config npx sequelize init:migrations // npx sequelize init:models
- 行完后会生成database/config.json文件和database/migrations目录,我们修改一下database/config.json中的内容,将其改成我们项目中使用的数据库配置:
{ "development": { "username": "root", "password": null, "database": "test", "host": "127.0.0.1", "dialect": "mysql", "timezone": "+08:00" } }
- 创建数据库
npx sequelize db:create
创建数据迁移表
npx sequelize migration:generate --name=init-users
1.执行完命令后,会在database / migrations / 目录下生成数据表迁移文件,然后定义
'use strict'; module.exports = { up: async (queryInterface, Sequelize) => { const { INTEGER, STRING, DATE, ENUM } = Sequelize; // 创建表 await queryInterface.createTable('users', { id: { type: INTEGER(20).UNSIGNED, primaryKey: true, autoIncrement: true }, username: { type: STRING(30), allowNull: false, defaultValue: '', comment: '用户名称', unique: true}, email: { type: STRING(160), allowNull: false, defaultValue: '', comment: '用户邮箱', unique: true }, password: { type: STRING(200), allowNull: false, defaultValue: '' }, avatar_url: { type: STRING(200), allowNull: true, defaultValue: '' }, mobile: { type: STRING(20), allowNull: false, defaultValue: '', comment: '用户手机', unique: true }, prifix: { type: STRING(32), allowNull: false, defaultValue: '' }, abstract: { type: STRING(255), allowNull: true, defaultValue: '' }, role_id:{ type: INTEGER, // 定义外键(重要) references: { model: 'users', // 对应表名称(数据表名称) key: 'id' // 对应表的主键 }, onUpdate: 'restrict', // 更新时操作 onDelete: 'cascade' // 删除时操作 }, gender: { type: ENUM, values: ['男','女','保密'], allowNull: true, defaultValue: '男', comment: '用户性别'}, created_at: DATE, updated_at: DATE }, { engine: 'MYISAM' }); // 添加索引 queryInterface.addIndex('users', ['gender']); // 添加唯一索引 queryInterface.addIndex('users', { name: "name", // 索引名称 unique: true, // 唯一索引 fields: ['name'] // 索引对应字段 }); }, down: async queryInterface => { await queryInterface.dropTable('users') } };
- 执行 migrate 进行数据库变更
# 升级数据库 npx sequelize db:migrate # 如果有问题需要回滚,可以通过 `db:migrate:undo` 回退一个变更 # npx sequelize db:migrate:undo # 可以通过 `db:migrate:undo:all` 回退到初始状态 # npx sequelize db:migrate:undo:all
已创建新增字段
1.创建迁移文件:
npx sequelize migration:generate --name=user-addcolumn
2.执行完命令后,会在database / migrations / 目录下生成数据表迁移文件,然后定义
'use strict'; module.exports = { up: (queryInterface, Sequelize) => { return queryInterface.sequelize.transaction((t) => { return Promise.all([ queryInterface.addColumn('user', 'role_id', { type: Sequelize.INTEGER }, { transaction: t }), queryInterface.addColumn('user', 'ceshi', { type: Sequelize.STRING, }, { transaction: t }) ]) }) }, down: (queryInterface, Sequelize) => { return queryInterface.sequelize.transaction((t) => { return Promise.all([ queryInterface.removeColumn('user', 'role_id', { transaction: t }), queryInterface.removeColumn('user', 'ceshi', { transaction: t }) ]) }) } };
3.执行 migrate 进行数据库变更
npx sequelize db:migrate
创建模型
// app / model / user.js 'use strict'; module.exports = app => { const { STRING, INTEGER, DATE } = app.Sequelize; // 配置(重要:一定要配置详细,一定要!!!) const User = app.model.define('user', { id: { type: INTEGER, primaryKey: true, autoIncrement: true }, name: STRING(30), age: INTEGER, created_at: DATE, updated_at: DATE, },{ timestamps: true, // 是否自动写入时间戳 tableName: 'users', // 自定义数据表名称 }); return User; };
这个 Model 就可以在 Controller 和 Service 中通过 app.model.User 或者 ctx.model.User 访问到了,例如我们编写 app/controller/users.js:
// app/controller/users.js const Controller = require('egg').Controller; function toInt(str) { if (typeof str === 'number') return str; if (!str) return str; return parseInt(str, 10) || 0; } class UserController extends Controller { async index() { const ctx = this.ctx; const query = { limit: toInt(ctx.query.limit), offset: toInt(ctx.query.offset) }; ctx.body = await ctx.model.User.findAll(query); } async show() { const ctx = this.ctx; ctx.body = await ctx.model.User.findByPk(toInt(ctx.params.id)); } async create() { const ctx = this.ctx; const { name, age } = ctx.request.body; const user = await ctx.model.User.create({ name, age }); ctx.status = 201; ctx.body = user; } async update() { const ctx = this.ctx; const id = toInt(ctx.params.id); const user = await ctx.model.User.findByPk(id); if (!user) { ctx.status = 404; return; } const { name, age } = ctx.request.body; await user.update({ name, age }); ctx.body = user; } async destroy() { const ctx = this.ctx; const id = toInt(ctx.params.id); const user = await ctx.model.User.findByPk(id); if (!user) { ctx.status = 404; return; } await user.destroy(); ctx.status = 200; } } module.exports = UserController;
最后我们将这个 controller 挂载到路由上:
// app/router.js module.exports = app => { const { router, controller } = app; router.resources('users', '/users', controller.users); };
针对 users 表的 CURD 操作的接口就开发完了
模型其他参数
// 配置(重要) const User = app.model.define('user', { id: { type: INTEGER, primaryKey: true, autoIncrement: true }, name: STRING(30), age: INTEGER, created_at: DATE, updated_at: DATE, },{ // 自定义表名 'freezeTableName': true, 'tableName': 'xyz_users', // 是否需要增加createdAt、updatedAt、deletedAt字段 'timestamps': true, // 不需要createdAt字段 'createdAt': false, // 将updatedAt字段改个名 'updatedAt': 'utime', // 将deletedAt字段改名 // 同时需要设置paranoid为true(此种模式下,删除数据时不会进行物理删除,而是设置deletedAt为当前时间 'deletedAt': 'dtime', 'paranoid': true, });
sequelize 命令
外键约束(重要)
// 迁移文件 queryInterface.addConstraint('tableName', ['user_id'], { type: 'foreign key', name: 'user_id', references: { //Required field table: 'users', field: 'id' }, onDelete: 'cascade', onUpdate: 'cascade' });
创建第一个种子
假设我们希望在默认情况下将一些数据插入到几个表中. 如果我们跟进前面的例子,我们可以考虑为 User 表创建演示用户.
要管理所有数据迁移,你可以使用 seeders. 种子文件是数据的一些变化,可用于使用样本数据或测试数据填充数据库表.
让我们创建一个种子文件,它会将一个演示用户添加到我们的 User 表中.
npx sequelize seed:generate --name demo-user
这个命令将会在 seeders 文件夹中创建一个种子文件.文件名看起来像是 XXXXXXXXXXXXXX-demo-user.js,它遵循相同的 up/down 语义,如迁移文件.
现在我们应该编辑这个文件,将演示用户插入User表.
'use strict'; module.exports = { up: (queryInterface, Sequelize) => { return queryInterface.bulkInsert('Users', [{ firstName: 'John', lastName: 'Doe', email: 'demo@demo.com', createdAt: new Date(), updatedAt: new Date() }], {}); }, down: (queryInterface, Sequelize) => { return queryInterface.bulkDelete('Users', null, {}); } };
运行种子
在上一步中,你创建了一个种子文件. 但它还没有保存到数据库. 为此,我们需要运行一个简单的命令.
npx sequelize db:seed:all
这将执行该种子文件,你将有一个演示用户插入 User 表.
注意: _ seeders 执行不会存储在任何使用 SequelizeMeta 表的迁移的地方. 如果你想覆盖这个,请阅读 存储 部分_
撤销种子
Seeders 如果使用了任何存储那么就可以被撤消. 有两个可用的命令
如果你想撤消最近的种子
npx sequelize db:seed:undo
如果你想撤消特定的种子
npx sequelize db:seed:undo --seed name-of-seed-as-in-data
如果你想撤消所有的种子
npx sequelize db:seed:undo:all
关联操作
一对一
模型层:
// 一个用户对应一个用户资料 // app/model/user.js module.exports = app => { const { STRING, INTEGER, DATE } = app.Sequelize; const User = app.model.define('user', { id: { type: INTEGER, primaryKey: true, autoIncrement: true }, name: STRING(30), age: INTEGER, created_at: DATE, updated_at: DATE, }); // 关联关系 User.associate = function(models) { // 关联用户资料 一对一 User.hasOne(app.model.Userinfo); } return User; }; // app/model/userinfo.js module.exports = app => { const { STRING, INTEGER, DATE } = app.Sequelize; const userinfo = app.model.define('userinfo', { nickname: STRING, user_id: INTEGER }, {}); // 关联用户表 userinfo.associate = function(models) { app.model.Userinfo.belongsTo(app.model.User); }; return userinfo; };
控制器调用:
// app/controller/users.js // 显示单条 async show() { // 根据主键查询 查询一条用findOne this.ctx.body = await this.ctx.model.User.findOne({ // 主表查询字段限制 attributes:['name'], // 关联查询 include: [{ // 需要查询的模型 model: this.app.model.Userinfo, // 副表查询的字段 attributes: ['nickname'] }], // 主表条件 where: { id: 3 } }); }
一对多
class City extends Model {} City.init({ countryCode: Sequelize.STRING }, { sequelize, modelName: 'city' }); class Country extends Model {} Country.init({ isoCode: Sequelize.STRING }, { sequelize, modelName: 'country' }); // 在这里,我们可以根据国家代码连接国家和城市 Country.hasMany(City, {foreignKey: 'countryCode', sourceKey: 'isoCode'}); City.belongsTo(Country, {foreignKey: 'countryCode', targetKey: 'isoCode'});
多对多
User.belongsToMany(Project, { as: 'Tasks', through: 'worker_tasks', foreignKey: 'userId' }) Project.belongsToMany(User, { as: 'Workers', through: 'worker_tasks', foreignKey: 'projectId' })
关联常用操作
// 获取关联模型对象,n对一不需要加s let userinfo = await user.getUserinfo(); // n对多需要加s await user.getPosts({ attributes: ['title'], where: { id: 3 } }); // 关联操作 // 1:用户创建文章(一对多) await this.ctx.model.Post.create({ title: "第一篇文章", user_id: user.id }); // 2.获取当前用户所有文章 await user.getPosts(); await user.getPosts({ attributes: ['id'], where:{ title:"测试" } }); // 3:用户删除文章(一对多) // (1) 获取当前用户的所有文章 let posts = await user.getPosts({ attributes: ['id'] }); posts = posts.map(v => v.id); await this.ctx.model.Post.destroy({ where: { id: posts } }); // 场景三:用户关注话题(多对多) await this.ctx.model.TopicUser.bulkCreate([{ user_id: user.id, topic_id: 1 },{ user_id: user.id, topic_id: 2 }]); // 用户关注话题(多对多) await this.ctx.model.TopicUser.destroy({ where: { user_id: user.id, topic_id: [1, 2] } });