快速初始化
用npm
1
| npm init egg --type=simple
|
目录结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| egg-project ├── package.json ├── app.js (可选) # 用于自定义启动时的初始化工作 ├── agent.js (可选) # 用于自定义启动时的初始化工作 ├── app | ├── router.js # 用于配置 URL 路由规则 │ ├── 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
|
路由(Router)
将用户的请求基于 method 和 URL 分发到了对应的 Controller 上,感觉api和Koa差不多。
参数获取
query
就列一个query参数的获取,需要注意的是,app.controller.search与文件相对应(也就是app/controller/search.js)
1 2 3 4 5 6 7 8 9 10 11
|
module.exports = app => { app.router.get('/search', app.controller.search.index); };
exports.index = async ctx => { ctx.body = `search: ${ctx.query.name}`; };
|
为了避免用户恶意传递重复key的query参数导致系统报错,ctx.query只取第一次出现的值,并且保证一定是字符串类型
如果必须要重复的key,比如需要传递数组:id=1&id=2&id=3,可以用ctx.queries
,其也保证了一定是数组类型
body
框架内置了 bodyParser 中间件来对JSON和Form-Data的请求 body 解析成 object 挂载到 ctx.request.body
上。
body 最大长度为 100kb
,超出长度413状态码,可以在 config/config.default.js
中覆盖框架的默认值
1 2 3 4 5 6
| module.exports = { bodyParser: { jsonLimit: '1mb', formLimit: '1mb', }, };
|
ctx.body是ctx.response.body的简写
可以通过ctx.get(name)
获取,其会自动处理大小写
Cookie
获取、设置方式和Koa一样,配置方式如下
1 2 3 4 5 6
| module.exports = { cookies: { }, };
|
Session
框架内置 Session 插件,可以通过ctx.session
,具体配置可以看 Cookie 与 Session
参数校验
借助 Validate 插件提供便捷的参数校验机制,配置方式如下
1 2 3 4 5
| exports.validate = { enable: true, package: 'egg-validate', };
|
ctx上有一个validate方法,可以传入校验配置来对参数进行校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
module.exports = app => { app.router.post('/user', app.controller.user); };
const createRule = { username: { type: 'email', }, password: { type: 'password', compare: 're-password', }, };
exports.create = async ctx => { ctx.validate(createRule); ctx.body = ctx.request.body; };
|
重定向
内部重定向
1
| app.router.redirect('/', '/home/index', 302);
|
外部重定向
1
| ctx.redirect(`http://cn.bing.com`);
|
resources生成CRUD路由结构
1 2 3 4 5
| module.exports = app => { const { router, controller } = app; router.resources('posts', '/api/posts', controller.posts); };
|
接下来在对应文件下实现对应函数就可以了
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| exports.index = async () => {};
exports.new = async () => {};
exports.create = async () => {};
exports.show = async () => {};
exports.edit = async () => {};
exports.update = async () => {};
exports.destroy = async () => {};
|
Method |
Path |
Route Name |
Controller.Action |
GET |
/posts |
posts |
app.controllers.posts.index |
GET |
/posts/new |
new_post |
app.controllers.posts.new |
GET |
/posts/:id |
post |
app.controllers.posts.show |
GET |
/posts/:id/edit |
edit_post |
app.controllers.posts.edit |
POST |
/posts |
posts |
app.controllers.posts.create |
PUT |
/posts/:id |
post |
app.controllers.posts.update |
DELETE |
/posts/:id |
post |
app.controllers.posts.destroy |
模块化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| module.exports = app => { require('./router/news')(app); require('./router/admin')(app); };
module.exports = app => { app.router.get('/news/list', app.controller.news.list); };
module.exports = app => { app.router.get('/admin/user', app.controller.admin.user); };
|
控制器(Controller)
解析用户的输入,处理后返回相应的结果,框架建议在 controller 对请求参数进行处理(校验、转换),然后调用对应的 service 方法处理业务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| const Controller = require('egg').Controller;
class PostController extends Controller { async create() { const { ctx, service } = this; const createRule = { title: { type: 'string' }, content: { type: 'string' }, }; ctx.validate(createRule); const author = ctx.session.userId; const req = Object.assign(ctx.request.body, { author }); const res = await service.post.create(req); ctx.body = { id: res.id }; ctx.status = 201; } }
module.exports = PostController;
|
获取上传的文件
file模式
在config中配置file模式
1 2 3 4
| exports.multipart = { mode: 'file', };
|
controller层代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const Controller = require('egg').Controller; const fs = require('mz/fs');
module.exports = class extends Controller { async upload() { const { ctx } = this; console.log(ctx.request.body); console.log('got %d files', ctx.request.files.length); for (const file of ctx.request.files) { console.log('field: ' + file.fieldname); console.log('filename: ' + file.filename); console.log('encoding: ' + file.encoding); console.log('mime: ' + file.mime); console.log('tmp filepath: ' + file.filepath); let result; try { result = await ctx.oss.put('egg-multipart-test/' + file.filename, file.filepath); } finally { await fs.unlink(file.filepath); } console.log(result); } } };
|
stream模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const path = require('path'); const sendToWormhole = require('stream-wormhole'); const Controller = require('egg').Controller;
class UploaderController extends Controller { async upload() { const ctx = this.ctx; const stream = await ctx.getFileStream(); const name = 'egg-multipart-test/' + path.basename(stream.filename); let result; try { result = await ctx.oss.put(name, stream); } catch (err) { await sendToWormhole(stream); throw err; }
ctx.body = { url: result.url, fields: stream.fields, }; } }
module.exports = UploaderController;
|
Service层
其不是单例,在每次请求时访问ctx.service.xx
时延迟实例化,所以可以用this.ctx
获取每一次请求的上下文
1 2 3 4 5 6 7 8 9 10 11
| const Service = require('egg').Service;
class UserService extends Service { async find(uid) { const user = await this.ctx.db.query('select * from user where uid = ?', uid); return user; } }
module.exports = UserService;
|
函数return的值会返回给前端
定时任务
所有的定时任务都统一存放在 app/schedule
目录下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const Subscription = require('egg').Subscription;
class UpdateCache extends Subscription { static get schedule() { return { interval: '1m', type: 'all', }; }
async subscribe() { const res = await this.ctx.curl('http://www.api.com/cache', { dataType: 'json', }); this.ctx.app.cache = res.data; } }
module.exports = UpdateCache;
|
还可以简写为
1 2 3 4 5 6 7 8 9 10 11 12
| module.exports = { schedule: { interval: '1m', type: 'all', }, async task(ctx) { const res = await ctx.curl('http://www.api.com/cache', { dataType: 'json', }); ctx.app.cache = res.data; }, };
|
没有用过,具体配置看官方文档 定时任务
egg-mysql
安装与配置
开启插件
1 2 3 4 5
| exports.mysql = { enable: true, package: 'egg-mysql', };
|
数据库配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| exports.mysql = { clients: { db1: { host: 'mysql.com', port: '3306', user: 'test_user', password: 'test_password', database: 'test', }, db2: { host: 'mysql2.com', port: '3307', user: 'test_user', password: 'test_password', database: 'test', }, } default: {}, app: true, agent: false, };
|
使用方式:
1 2 3 4 5
| const client1 = app.mysql.get('db1'); await client1.query(sql, values);
const client2 = app.mysql.get('db2'); await client2.query(sql, values);
|