愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

Egg学习笔记(三):Service/插件/模板

一、Service

Service是在复杂业务场景下对业务逻辑的一个封装抽象,这种抽象的好处在于:

  • 使Controller中的逻辑更加简洁
  • 使Service可被多个Controller重复调用,提高可复用性
  • 便于编写测试用例,做单元测试

编写Service,可以在app/service文件夹下建立文件(一样也支持多级目录),如:

// app/service/user.js
const { Service } = require('egg')
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类,一个Service实例拥有如下属性:

  • this.ctx
  • this.app
  • this.service
  • this.config
  • this.logger

为了获取用户请求的链路,Service在初始化中注入了请求上下文,故可以通过this.ctx获取上下文相关信息,从而方便进行一些调用:

  • this.ctx.curl 发起网络调用
  • this.ctx.service.otherService 调用其他Service
  • this.ctx.db 发起数据库调用

注意事项:
1)Service只能通过class方式定义,其必须继承egg.Service
2)一个Service文件只能包含一个类,用module.exports导出
3)Service是请求级别的对象,非单例,框架在每次请求中首次访问ctx.service.xxx时会延迟实例化,故Service中可以通过this.ctx获取请求上下文


二、插件

插件机制是Egg提供的一大特色功能,之所以除了中间件外还需要引入插件机制,是因为:

  • 中间件加载有先后顺序,并且这种顺序是由使用者自行管理的,某些情况下若顺序不对,会影响执行结果
  • 中间件的定位是拦截用户请求,在请求前后做一些处理,如:鉴权、安全检查、访问日志等,但有些功能却与请求无关,如:定时任务、消息订阅、后台逻辑
  • 有些功能包含很复杂的初始化逻辑,需要在应用启动时完成,不适合在中间件中实现

1、插件与中间件、应用的关系

  • 一个插件其实相当于一个迷你应用,故插件和应用十分类似:都有Service/中间件/配置/框架扩展,但是没有独立的Router和Controller
  • 插件中也可以使用中间件
  • 多个插件可以包装为一个上层插件

2、使用插件

插件一般通过npm模块的形式进行复用:

npm i egg-mysql --save

然后在config/plugin.js里声明,如:

// config/plugin.js
exports.mysql = {
    enable: true,
    package:'egg-mysql'
}
/* 配置项支持:
    {
        enable: <boolean>, // 是否开启插件
        package: <string>, // npm模块名称
        path: <string>,    // 插件绝对路径,此项配置与`package`配置互斥
        env: <array>       // 只有在指定环境才能开启,会覆盖插件自身`package.json`中的配置
    }
*/
// 对于内置插件,还可以不需要配置package,如`exports.onerror = false`

如此一来,就可以直接使用插件提供的功能了,如:

app.mysql.query(sql, value)

此外,框架还支持plugin.[env].js这种方式进行配置,指定在特定环境下启用的插件,但注意:
1)没有plugin.default.js
2)只能在应用层使用,在框架层不应该使用

3、内置插件

框架中,内置了企业级应用常用的插件,它们有:

  • onerror 统一异常处理
  • Session 实现Session
  • i18n 多语言
  • watcher 文件和文件夹监控
  • multipart 文件流式上传
  • security 安全
  • development 开发环境配置
  • logrotator 日志切分
  • schedule 定时任务
  • static 静态服务器
  • jsonp JSONP支持
  • view 模板引擎


三、模板渲染

Egg支持多种模板渲染引擎,每个模板引擎都以插件的方式引入,但保持渲染的API一致。以下为模板使用实例(以egg-view-nunjucks为例):

1、引入方法

先安装模板插件:

npm install egg-view-nunjucks --save

然后启用插件:

// config/plugin.js
exports.nunjucks = {
    enable: true,
    package: 'egg-view-nunjucks'
}

2、配置插件

egg-view提供了config.view通用配置,可以进行自定义配置,如下:

// config/config.default.js
const path = require('path')
module.exports = appInfo => {
    const config = {}
    config.view = {
        // 模板文件的根目录,默认为app/view,支持多目录(以,分割,会从多目录中查找文件)
        root: [
            path.join(appInfo.baseDir, 'app/view'),
            path.join(appInfo.baseDir, 'path/to/another'),
        ].join(','),
        // 是否开启缓存,开启后,下次渲染同样路径的模板时不会重新查找
        cache: true,
        // 指定使用的模板引擎
        // 当渲染时,就会根据后缀名查找使用相应的模板引擎渲染
        // 如:await ctx.render('home.nj')
        mapping: {
            '.nj': 'nunjucks'
        },
        // 可以设置默认的模板引擎
        defaultViewEngine: 'nunjucks',
        // 设置默认的模板引擎后缀,设置后就可以在调用时省略
        // 如:await ctx.render('home')
        defaultExtension: '.nj'
    }
    return config
}

3、渲染页面

框架在Context中提供了3个接口,返回值均为Promise:

  • render(name, locals) 渲染模板文件,并赋值给ctx.body
  • renderView(name, locals) 渲染模板文件,仅返回不赋值
  • renderString(tpl, locals) 渲染模板字符串,仅返回不赋值(使用时需要指定模板引擎,不过若定义了defaultViewEngine,则可以省略)

以下为使用实例:

// app/controller/home.js
class HomeController extends Controller {
    async index() {
        const data = {
            name: 'egg'
        }
        // 方式一
        await ctx.render('home/index.tpl', data)
        // 方式二
        ctx.body = await ctx.renderView('home/index.tpl', data)
        // 方式三
        ctx.body = await ctx.renderString('Hi, {{ name }}', data, {
            viewEngine: 'nunjucks'
        })
    }
}

4、Locals

在渲染页面的过程中,需要有一个变量收集传递给模板的变量。因此Egg里提供了app.localsctx.locals,它们的区别主要在于:前者是全局的,一般在app.js里配置,而后者是单次请求的,会合并app.locals,此外,还可以直接赋值一个对象,框架通过setter进行了自动合并,如下:

// app.locals会合并到ctx.locals里
ctx.app.locals = { a:1 }
ctx.locals.b = 2
ctx.locals // { a:1, b:2 }
// 一次请求过程中,仅在第一次使用ctx.locals会把`app.locals`合并进去
ctx.app.locals = { a:2 }
ctx.locals // { a:1, b:2 }
// 可以直接赋值整个对象,框架进行了处理
ctx.locals.c = 3
ctx.local = { d: 4 }
ctx.locals // { a:1, b:2, c:3, d:4 }

但是一般情况下,我们不需要直接使用这两个对象,可以直接用ctx.render(name, data),因为:

  • 框架会自动将data合并到ctx.locals
  • 框架会自动注入ctxrequesthelper到模板里,所以模板里可以调用如:{{ helper.lowercaseFirst(ctx.app.config.baseDir) }}