初始化
$ mkdir egg-example && cd egg-example$ npm init egg --type=simple$ npm i
启动项目
$ npm run dev$ open http://localhost:7001
常用插件
配置env环境变量
npm install dotenv --save## {app_root}/config/config.default.jsrequire('dotenv').config()module.exports = appInfo => {}
开启跨域 egg-cors
npm i egg-cors -S
exports.cors = {enable: true,package: 'egg-cors',};
// 关闭csrfconfig.security = {csrf: {enable: false},// 跨域白名单domainWhiteList: [ 'http://localhost:4200' ],}// 开启跨域config.cors = {origin: '*',allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH'}
配置路由 router-plus
npm i -S egg-router-plus## pluginrouterPlus: {enable: true,package: 'egg-router-plus',}
app / router.js
'use strict'module.exports = app => {require('./routes/user')(app)}
app / routes / user.js
module.exports = app => {const { router, controller } = appconst { create, createMore, finById, updateById } = controller.userconst userRouter = router.namespace('/users')userRouter.patch('/:id', updateById)}
mysql 配置
具体配置:https://www.eggjs.org/zh-CN/tutorials/sequelize
// 数据库配置config.sequelize = {dialect: 'mysql',host: process.env.MYSQL_HOST,password: process.env.MYSQL_PWD,port: process.env.MYSQL_PORT,database: process.env.MYSQL_DATABASE,timezone: "+08:00",define: {freezeTableName: true, // 关闭复数表名paranoid: true, // 生成 deleted_at 软删字段underscored: true, // 驼峰转下划线createdAt: 'created_at', // 自定义创建时间字段updatedAt: 'updated_at',deletedAt: 'deleted_at'}}
redis 配置
npm i -S egg-redis
// redisconfig.redis = {client: {port: 6379, // Redis porthost: '127.0.0.1', // Redis hostpassword: '',db: 0,},}
'use strict';const Service = require('egg').Service;class CacheService extends Service {/*** 获取列表* @param {string} key 键* @param {boolean} isChildObject 元素是否为对象* @return { array } 返回数组*/async getList(key, isChildObject = false) {const { redis } = this.applet data = await redis.lrange(key, 0, -1)if (isChildObject) {data = data.map(item => {return JSON.parse(item);});}return data;}/*** 设置列表* @param {string} key 键* @param {object|string} value 值* @param {string} type 类型:push和unshift* @param {Number} expir 过期时间 单位秒* @return { Number } 返回索引*/async setList(key, value, type = 'push', expir = 0) {const { redis } = this.appif (expir > 0) {await redis.expire(key, expir);}if (typeof value === 'object') {value = JSON.stringify(value);}if (type === 'push') {return await redis.rpush(key, value);}return await redis.lpush(key, value);}/*** 设置 redis 缓存* @param { String } key 键* @param {String | Object | array} value 值* @param { Number } expir 过期时间 单位秒* @return { String } 返回成功字符串OK*/async set(key, value, expir = 0) {const { redis } = this.appif (expir === 0) {return await redis.set(key, JSON.stringify(value));} else {return await redis.set(key, JSON.stringify(value), 'EX', expir);}}/*** 获取 redis 缓存* @param { String } key 键* @return { String | array | Object } 返回获取的数据*/async get(key) {const { redis } = this.appconst result = await redis.get(key)return JSON.parse(result)}/*** redis 自增* @param { String } key 键* @param { Number } value 自增的值* @return { Number } 返回递增值*/async incr(key, number = 1) {const { redis } = this.appif (number === 1) {return await redis.incr(key)} else {return await redis.incrby(key, number)}}/*** 查询长度* @param { String } key* @return { Number } 返回数据长度*/async strlen(key) {const { redis } = this.appreturn await redis.strlen(key)}/*** 删除指定key* @param {String} key*/async remove(key) {const { redis } = this.appreturn await redis.del(key)}/*** 清空缓存*/async clear() {return await this.app.redis.flushall()}}module.exports = CacheService;
validator
安装验证插件
npm i egg-valparams
plugin 配置
exports.valparams = {enable : true,package: 'egg-valparams'};
config 配置
exports.valparams = {locale : 'zh-cn',throwError: false};
验证
class XXXController extends app.Controller {// ...async XXX() {const {ctx} = this;ctx.validate({system : {type: 'string', required: false, defValue: 'account', desc: '系统名称'},token : {type: 'string', required: true, desc: 'token 验证'},redirect: {type: 'string', required: false, desc: '登录跳转'}});// if (config.throwError === false)if(ctx.paramErrors) {// get error infos from `ctx.paramErrors`;}let params = ctx.params;let {query, body} = ctx.request;// ctx.params = validater.ret.params;// ctx.request.query = validater.ret.query;// ctx.request.body = validater.ret.body;// ...ctx.body = query;}// ...}
验证规则
Valparams.setParams(req, params, options);
| Param | Type | Description | Example |
|---|---|---|---|
| req | Object | request 对象,这里我们就是取相应的三种请求的参数进行参数验证 | {params, query, body} |
| params | Object | 参数的格式配置 { pname: {alias, type, required, range: {in, min, max, reg, schema }, defValue, trim, allowEmptyStr, desc[, detail] } } | {sysID : {alias:’sid’,type: ‘int’, required: true, desc: ‘所属系统id’}} |
| params[pname] | String | 参数名 | |
| params[pname].alias | String | 参数别名,可以使用该参数指定前端使用的参数名称 | |
| params[pname].type | String | 参数类型 | 常用可选类型有 int, string, json 等,其他具体可见下文或用 Valparams.vType 进行查询 |
| params[pname].required | Boolean | 是否必须 | |
| params[pname].range | Object | 参数范围控制 | {min: ‘112.80.248.10’, max: ‘112.80.248.72’} |
| params[pname].range.min | ALL | 最小值、最短、最早(不同 type 参数 含义有所差异) | |
| params[pname].range.max | ALL | 最大值、最长、最晚(不同 type 参数 含义有所差异) | |
| params[pname].range.in | Array | 在XX中,指定参数必须为其中的值 | |
| params[pname].range.reg | RegExp | 正则判断,参数需要符合正则 | |
| params[pname].range.schema | Object | jsonSchema,针对JSON类型参数有效,使用ajv对参数进行格式控制 | |
| params[pname].defValue | ALL | 默认值,没传参数或参数验证出错时生效,此时会将该值赋值到相应参数上 | |
| params[pname].trim | Boolean | 是否去掉参数前后空格字符,默认false | |
| params[pname].allowEmptyStr | Boolean | 是否允许接受空字符串,默认false | |
| params[pname].desc | String | 参数含义描述 | |
| options | Object | 参数关系配置 | |
| options.choices | Array | 参数挑选规则 | [{fields: [‘p22’, ‘p23’, ‘p24’], count: 2, force: true}] 表示’p22’, ‘p23’, ‘p24’ 参数三选二 |
| options.choices[].fields | Array | 涉及的参数 | |
| options.choices[].count | Number | 需要至少传 ${count} 个 | |
| options.choices[].force | Boolean | 默认 false,为 true 时,涉及的参数中只能传 ${count} 个, 为 false 时,可以多于 ${count} 个 | |
| options.equals | Array | 参数相等 | [[‘p20’, ‘p21’], [‘p22’, ‘p23’]] 表示 ‘p20’, ‘p21’ 两个值需要相等,’p22’, ‘p23’ 两个值需要相等 |
| options.equals[] | Array | 涉及的参数(涉及的参数的值需要是相等的) | |
| options.compares | Array | 参数大小关系 | [[‘p25’, ‘p26’, ‘p27’]] 表示 ‘p25’, ‘p26’, ‘p27’ 必须符合 ‘p25’ <= ‘p26’ <= ‘p27’ |
| options.compares[] | Array | 涉及的参数(涉及的参数的值需要是按顺序从小到大的) | |
| options.cases | Object | 参数条件判断 | [{when: [‘p30’], then: [‘p31’], not: [‘p32’]}] 表示 当传了 p30 就必须传 p31 ,同时不能传p32 |
| options.cases.when | Array | 条件 | |
| options.cases.when[] | String | 涉及的参数,(字符串)只要接收到的参数有这个字段即为真 | |
| options.cases.when[].field | 涉及的参数的名(对象) | —- | |
| options.cases.when[].value | 涉及的参数的值(对象)需要参数的值与该值相等才为真 | —- | |
| options.cases.then | Array | 符合when条件时,需要必传的参数 | |
| options.cases.not | Array | 符合when条件时,不能接收的参数 |
const Valparams = require('path/to/Valparams[/index]');Valparams.locale('zh-cn');function list(req, res, next) {let validater = Valparams.setParams(req, {sysID : {alias:'sid',type: 'int', required: true, desc: '所属系统id'},page : {type: 'int', required: false, defValue: 1, range:{min:0}, desc: '页码'},size : {type: 'int', required: false, defValue: 30, desc: '页面大小'},offset: {type: 'int', required: false, defValue: 0, desc: '位移'}}, {choices : [{fields: ['sysID', 'page'], count: 1, force: false}],});if (validater.err && validater.err.length) {console.log(validater.err);}else {console.log(validater);//{ query: { page: 1, size: 30 },// body: {},// params: { sysID: 2 },// all: { sysID: 2, page: 1, size: 30 },// err: null }// raw: { query: { page: 1, size: 30 },// body: {},// params: { sid: 2 },// }//}//do something}}
返回支持的类型列表
Valparams.vType = {ALL : 'all',STRING : 'string',ARRAY : 'array',DATE : 'date',INT : 'int',FLOAT : 'float',LETTER : 'letter',NUMBER : 'number',IP : 'ip',EMAIL : 'email',PHONE : 'phone',URL : 'url',JSON : 'json',BOOL : 'bool',NULL : 'null',RANGE : 'range',DATERANGE : 'dateRange',INTRANGE : 'intRange',FLOATRANGE : 'floatRange',NUMBERRANGE: 'numberRange'};
jwt 鉴权
用户登录
// 用户登录async login() {// 1. 查询用户信息const { ctx } = thisconst { email } = this.ctx.request.bodyconst user = await ctx.service.user.findOne({ email })// 2. 生成 access_token 和 refresh_tokenconst access_token = ctx.getToken({ id: user.id, is_refresh: false })const refresh_token = ctx.getRefreshToken({ id: user.id, is_refresh: true })// 3. access_token 和 refresh_token 存入redisctx.service.redis.set(`user_token_${user.id}`, {access_token,refresh_token})ctx.body = {access_token,refresh_token}}
access_token 鉴权
配置鉴权中间件
// 全局中间件config.middleware = ['errorHandler','auth' // token 鉴权];config.auth = {// 不需要token验证的路由ignore: ['/users/login', '/users/reg', '/users/refresh_token']}
const { verify } = require('jsonwebtoken')/*** 验证token* 1. 从请求头获取token* 2. 解析token && 阻止 refresh_token 解析* 3. 判断用户是否登录(redis中获取用户信息)* 4. 判断用户是否被禁用 (mysql 中查询用户信息)* 5. 用户信息挂载到ctx.state全局对象*/module.exports = (options, app) => {return async (ctx, next) => {// 1. 获取tokenconst { authorization = '' } = ctx.headerconst token = authorization.replace("Bearer ", '')if (!token) {ctx.throw(401, 'token 不合法')}// 2. 解析tokenlet user = {}try {user = verify(token, 'abcdefg')// 3. 阻止 refresh_tokenif (user.is_refresh) {ctx.throw(401, 'token 不合法')}} catch (err) {switch (err.name) {case 'TokenExpiredError':ctx.throw(401, `token 已过期`)breakdefault:ctx.throw(401, `token 不合法`)break}}// 4. 判断用户是否登录(从redis中查询....)let { access_token } = await ctx.service.redis.get(`user_token_${user.id}`)if (access_token !== token) {ctx.throw(401, '请登录')}// 5. 判断用户是否被禁用user = await ctx.service.user.findOne({ id: user.id })if (!user) ctx.throw(401, '用户被禁用')// 6. 用户信息挂载全局对象ctx.state.user = userreturn await next(options)}}
刷新token
/*** 刷新token* 1. 请求头获取 refresh_token* 2. 解析 refresh_token && 阻止 access_token 解析* 3. 判断用户是否登录* 4. 判断用户是否禁用* 5. 生成新的 access_token 和 refresh_token**/const { verify } = require('jsonwebtoken')module.exports = (options, app) => {return async (ctx, next) => {// 1. 请求头获取tokenconst { authorization = '' } = ctx.headerconst token = authorization.replace('Bearer ', '')if (!token) ctx.throw(401, 'token 不合法')// 2. 解析 refresh_tokenlet user = {}try {user = verify(token, 'abcdefg')// 3. 阻止 access_token 解析if (!user.is_refresh) { ctx.throw(401, 'refresh_token 不合法') }} catch (err) {switch (err.name) {case 'TokenExpiredError':ctx.throw(403, `refresh_token 已过期`)breakdefault:ctx.throw(401, `refresh_token 不合法`)break}}// 4. 判断用户是否登录 (根据用户id查询redis)const { refresh_token } = await ctx.service.redis.get(`user_token_${user.id}`)if (refresh_token !== token) {ctx.throw(401, '请登录')}// 5. 判断用户是否被禁用user = await ctx.service.user.findOne({ id: user.id })if (!user) {ctx.throw(401, '用户被禁用')}// 6. 生成新的 access_token 和 refresh_tokenconst accessToken = ctx.getToken({ id: user.id, is_refresh: false })const refreshToken = ctx.getRefreshToken({ id: user.id, is_refresh: true })// 7. 更新旧的refresh_tokenctx.service.redis.set(`user_token_${user.id}`,{ access_token: accessToken, refresh_token: refreshToken })// 8. 新生成的token返回客户端ctx.state.generateToken = {access_token: accessToken,refresh_token: refreshToken}await next(options)}}
