回顾一下上篇讲到的内容,上篇讲了:
运行环境
Config 配置
中间件(Middleware)
路由
控制器(Controller)
服务(Service)
Service 就是在复杂业务场景下用于做业务逻辑封装的一个抽象层
使用场景
复杂数据的处理,比如要展现的信息需要从数据库获取,还要经过一定的规则计算,才能返回用户显示。或者计算完成后,更新到数据库。
第三方服务的调用,比如 GitHub 信息获取等。
定义 Service
// app/service/user.js
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;
属性
每一次用户请求,框架都会实例化对应的 Service 实例,由于它继承于 egg.Service
,故拥有下列属性方便我们进行开发:
this.ctx
: 当前请求的上下文 Context 对象的实例this.app
: 当前应用 Application 对象的实例this.service
:应用定义的 Servicethis.config
:应用运行时的配置项this.logger
:logger 对象,上面有四个方法(debug
,info
,warn
,error
),分别代表打印四个不同级别的日志,使用方法和效果与 context logger 中介绍的一样,但是通过这个 logger 对象记录的日志,在日志前面会加上打印该日志的文件路径,以便快速定位日志打印位置。
Service ctx 详解
this.ctx.curl
发起网络调用。this.ctx.service.otherService
调用其他 Service。this.ctx.db
发起数据库调用等, db 可能是其他插件提前挂载到 app 上的模块。
注意事项
Service 文件必须放在
app/service
目录,可以支持多级目录,访问的时候可以通过目录名级联访问。
app/service/biz/user.js => ctx.service.biz.user // 多级目录,依据目录名级联访问
app/service/sync_user.js => ctx.service.syncUser // 下划线自动转换为自动驼峰
app/service/HackerNews.js => ctx.service.hackerNews // 大写自动转换为驼峰
一个 Service 文件只能包含一个类, 这个类需要通过 module.exports 的方式返回。
Service 需要通过 Class 的方式定义,父类必须是 egg.Service。
Service 不是单例,是 请求级别 的对象,框架在每次请求中首次访问
ctx.service.xx
时延迟实例化,所以 Service 中可以通过 this.ctx 获取到当前请求的上下文。
使用 Service
// app/controller/user.js
const Controller = require('egg').Controller;
class UserController extends Controller {
async info() {
const { ctx } = this;
const userId = ctx.params.id;
const userInfo = await ctx.service.user.find(userId);
ctx.body = userInfo;
}
}
module.exports = UserController;
// app/service/user.js
const Service = require('egg').Service;
class UserService extends Service {
// 默认不需要提供构造函数。
// constructor(ctx) {
// super(ctx); 如果需要在构造函数做一些处理,一定要有这句话,才能保证后面 `this.ctx`的使用。
// // 就可以直接通过 this.ctx 获取 ctx 了
// // 还可以直接通过 this.app 获取 app 了
// }
async find(uid) {
// 假如 我们拿到用户 id 从数据库获取用户详细信息
const user = await this.ctx.db.query('select * from user where uid = ?', uid);
// 假定这里还有一些复杂的计算,然后返回需要的信息。
const picture = await this.getPicture(uid);
return {
name: user.user_name,
age: user.age,
picture,
};
}
async getPicture(uid) {
const result = await this.ctx.curl(`http://photoserver/uid=${uid}`, { dataType: 'json' });
return result.data;
}
}
module.exports = UserService;
插件
为什么要插件
在使用 Koa 中间件过程中发现了下面一些问题:
中间件加载其实是有先后顺序的,但是中间件自身却无法管理这种顺序,只能交给使用者。这样其实非常不友好,一旦顺序不对,结果可能有天壤之别。
中间件的定位是拦截用户请求,并在它前后做一些事情,例如:鉴权、安全检查、访问日志等等。但实际情况是,有些功能是和请求无关的,例如:定时任务、消息订阅、后台逻辑等等。
有些功能包含非常复杂的初始化逻辑,需要在应用启动的时候完成。这显然也不适合放到中间件中去实现。
中间件、插件、应用的关系
一个插件其实就是一个『迷你的应用』,和应用(app)几乎一样:+
它包含了 Service、中间件、配置、框架扩展等等。
它没有独立的 Router 和 Controller。
它没有
plugin.js
,只能声明跟其他插件的依赖,而不能决定其他插件的开启与否。
他们的关系是:
应用可以直接引入 Koa 的中间件。
插件本身可以包含中间件。
多个插件可以包装为一个上层框架。
使用插件
插件一般通过 npm 模块的方式进行复用:
npm i egg-mysql --save
建议通过 ^ 的方式引入依赖,并且强烈不建议锁定版本。
{
"dependencies": {
"egg-mysql": "^3.0.0"
}
}
然后需要在应用或框架的 config/plugin.js
中声明:
// config/plugin.js
// 使用 mysql 插件
exports.mysql = {
enable: true,
package: 'egg-mysql',
};
就可以直接使用插件提供的功能:app.mysql.query(sql, values);
egg-mysql 插件文档
参数介绍
plugin.js
中的每个配置项支持:
{Boolean} enable
- 是否开启此插件,默认为 true{String} package
- npm 模块名称,通过 npm 模块形式引入插件{String} path
- 插件绝对路径,跟 package 配置互斥{Array} env
- 只有在指定运行环境才能开启,会覆盖插件自身 package.json 中的配置
开启和关闭
在上层框架内部内置的插件,应用在使用时就不用配置 package 或者 path,只需要指定 enable 与否:
// 对于内置插件,可以用下面的简洁方式开启或关闭
exports.onerror = false;
根据环境配置
同时,我们还支持 plugin.{env}.js
这种模式,会根据运行环境加载插件配置。
比如定义了一个开发环境使用的插件 egg-dev
,只希望在本地环境加载,可以安装到 devDependencies
。
// npm i egg-dev --save-dev
// package.json
{
"devDependencies": {
"egg-dev": "*"
}
}
然后在 plugin.local.js
中声明:
// config/plugin.local.js
exports.dev = {
enable: true,
package: 'egg-dev',
};
这样在生产环境可以 npm i --production
不需要下载 egg-dev
的包了。
注意:
不存在
plugin.default.js
只能在应用层使用,在框架层请勿使用。
package 和 path
package
是npm
方式引入,也是最常见的引入方式path
是绝对路径引入,如应用内部抽了一个插件,但还没达到开源发布独立 npm 的阶段,或者是应用自己覆盖了框架的一些插件
// config/plugin.js
const path = require('path');
exports.mysql = {
enable: true,
path: path.join(__dirname, '../lib/plugin/egg-mysql'),
};
插件配置
插件一般会包含自己的默认配置,应用开发者可以在 config.default.js
覆盖对应的配置:
// config/config.default.js
exports.mysql = {
client: {
host: 'mysql.com',
port: '3306',
user: 'test_user',
password: 'test_password',
database: 'test',
},
};
插件列表
框架默认内置了企业级应用常用的插件:
onerror
统一异常处理