EGG笔记

目录结构

  • app/router.js 用于配置 URL 路由规则,具体参见 Router

  • app/controller/** 用于解析用户的输入,处理后返回相应的结果,具体参见 Controller

  • app/service/** 用于编写业务逻辑层,可选,建议使用,具体参见 Service

  • app/middleware/** 用于编写中间件,可选,具体参见 Middleware

  • app/public/** 用于放置静态资源,可选,具体参见内置插件 egg-static

  • app/extend/** 用于框架的扩展,可选,具体参见框架扩展

  • config/config.{env}.js 用于编写配置文件,具体参见配置

  • config/plugin.js 用于配置需要加载的插件,具体参见插件

  • test/** 用于单元测试,具体参见单元测试

  • app.jsagent.js 用于自定义启动时的初始化工作,可选,具体参见启动自定义。关于agent.js的作用参见Agent机制

  • app/schedule/** 用于定时任务,可选,具体参见定时任务

内置对象

  1. Application

    全局应用对象,在一个应用中,只会实例化一个,它继承自 Koa.Application,在它上面我们可以挂载一些全局的方法和对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// app.js

module.exports = app => {
app.once('server', server => {
// websocket
});
app.on('error', (err, ctx) => {
// report error
});
app.on('request', ctx => {
// log receive request
});
app.on('response', ctx => {
// ctx.starttime is set by framework
const used = Date.now() - ctx.starttime;
// log total cost
});
};
  1. Context

Context 是一个请求级别的对象,继承自 Koa.Context。在每一次收到用户请求时,框架会实例化一个 Context 对象,这个对象封装了这次用户请求的信息,并提供了许多便捷的方法来获取请求参数或者设置响应信息。框架会将所有的 Service 挂载到 Context 实例上,一些插件也会将一些其他的方法和对象挂载到它上面

  1. Request & Response

可以在 Context 的实例上获取到当前请求的 Request(ctx.request) 和 Response(ctx.response) 实例。

1
2
ctx.request.query.id;
ctx.response.body;
  1. Controller

框架提供了一个 Controller 基类,并推荐所有的 Controller 都继承于该基类实现。

  • ctx - 当前请求的 Context 实例。
  • app - 应用的 Application 实例。
  • config - 应用的配置
  • service - 应用所有的 service
  • logger - 为当前 controller 封装的 logger 对象。
  1. Service

框架提供了一个 Service 基类,并推荐所有的 Service 都继承于该基类实现。 Service 基类的属性和 Controller 基类属性一致,访问方式也类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app/service/user.js

// 从 egg 上获取(推荐)
const Service = require('egg').Service;
class UserService extends Service {
// implement
}
module.exports = UserService;

// 从 app 实例上获取
module.exports = app => {
return class UserService extends app.Service {
// implement
};
};
  1. Helper

Helper 用来提供一些实用的 utility 函数。它的作用在于我们可以将一些常用的动作抽离在 helper.js 里面成为一个独立的函数,这样可以用 JavaScript 来写复杂的逻辑,避免逻辑分散各处,同时可以更好的编写测试用例。

可以在 Context 的实例上获取到当前请求的 Helper(ctx.helper) 实例。

  1. Config

app.config 从 Application 实例上获取到 config 对象,也可以在 Controller, Service, Helper 的实例上通过 this.config 获取到 config 对象。

  1. Logger

框架内置了功能强大的日志功能,可以非常方便的打印各种级别的日志到对应的日志文件中,每一个 logger 对象都提供了 4 个级别的方法:

  • logger.debug()
  • logger.info()
  • logger.warn()
  • logger.error()
  1. Subscription

订阅模型是一种比较常见的开发模式,譬如消息中间件的消费者或调度任务。因此我们提供了 Subscription 基类来规范化这个模式。

1
2
3
4
5
6
7
const Subscription = require('egg').Subscription;

class Schedule extends Subscription {
// 需要实现此方法
// subscribe 可以为 async function 或 generator function
async subscribe() {}
}

middleware中间件

img

img

  • options: 中间件的配置项,框架会将 app.config[${middlewareName}] 传递进来。
  • app: 当前应用 Application 的实例。
1
2
3
4
5
6
7
8
// 中间件配置
config.middleware = ['printdate'];
// 中间件传值
config.printdate = {
test:'test'
}
// 中间件使用
const printdate = app.middleware.printdate();

Router 路由

app/router.js 里面定义 URL 路由规则

1
2
3
4
5
// app/router.js
module.exports = app => {
const { router, controller } = app;
router.get('/user/:id', controller.user.info);
};

app/controller 目录下面实现 Controller

1
2
3
4
5
6
7
8
9
// app/controller/user.js
class UserController extends Controller {
async info() {
const { ctx } = this;
ctx.body = {
name: `hello ${ctx.params.id}`,
};
}
}

RESTful 风格的 URL 定义

1
2
3
4
5
6
7
8
9
10
11
12
13
// app/router.js
module.exports = app => {
const { router, controller } = app;
router.resources('posts', '/api/posts', controller.posts);
};
// app/controller/posts.js
exports.index = async () => {};
exports.new = async () => {};
exports.create = async () => {};
exports.show = async () => {};
exports.edit = async () => {};
exports.update = async () => {};
exports.destroy = async () => {};

Controller 控制器

负责解析用户的输入,处理后返回相应的结果

  • RESTful 接口中,Controller 接受用户的参数,从数据库中查找内容返回给用户或者将用户的请求更新到数据库中。
  • 在 HTML 页面请求中,Controller 根据用户访问不同的 URL,渲染不同的模板得到 HTML 返回给用户。
  • 在代理服务器中,Controller 将用户的请求转发到其他服务器上,并将其他服务器的处理结果返回给用户。

获取 HTTP 请求参数

  • query this.ctx.query 字符串
  • queries this.ctx.queries 对象
  • Router params
  • body
  • header
  • cookies
  • session

Service服务

我们并不想在 Controller 中实现太多业务逻辑,所以提供了一个 Service 层进行业务逻辑的封装,这不仅能提高代码的复用性,同时可以让我们的业务逻辑更好测试。

  • 保持 Controller 中的逻辑更加简洁。
  • 保持业务逻辑的独立性,抽象出来的 Service 可以被多个 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
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    // app/router.js
    module.exports = app => {
    app.router.get('/user/:id', app.controller.user.info);
    };

    // 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;

    // curl http://127.0.0.1:7001/user/1234

安全机制csrf

  • 每个路由上配置
  • 通过中间件统一配置

默认不能设置中文

如果加密以后就可以设置

1
2
3
4
5
6
this.ctx.cookies.set('userinfo','张三',{
maxAge:1000*3600*24,
httpOnly:true,
signed:true,
encrypt:true //加密
})

或用encode加密解密来传

清除cookies

1
this.ctx.cookies.set('userinfo',null);

Session

Session保存在服务器上。基于cookies。

1
2
3
4
5
6
7
8
9
10
11
12
13
this.ctx.session.username = 'zhangsan';
// 单独设置过期时间
this.ctx.session.maxAge = 60*1000*10;

// 全局配置 基本和COOKIES一致
// config.default.js
config.session={
maxAge:1000*24*60,
httpOnly:true,
key:'SESSION_ID',
encrypt:true,
renew:true // 每次刷新页面的时候 session 都会被重新设置 延期
}

Plugin

why use plugin

  1. 中间件加载其实是有先后顺序的,但是中间件自身却无法管理这种顺序,只能交给使用者。这样其实非常不友好,一旦顺序不对,结果可能有天壤之别。
  2. 中间件的定位是拦截用户请求,并在它前后做一些事情,例如:鉴权、安全检查、访问日志等等。但实际情况是,有些功能是和请求无关的,例如:定时任务、消息订阅、后台逻辑等等。
  3. 有些功能包含非常复杂的初始化逻辑,需要在应用启动的时候完成。这显然也不适合放到中间件中去实现。

plugin.js 中的每个配置项支持:

  • {Boolean} enable - 是否开启此插件,默认为 true
  • {String} package - npm 模块名称,通过 npm 模块形式引入插件
  • {String} path - 插件绝对路径,跟 package 配置互斥
  • {Array} env - 只有在指定运行环境才能开启,会覆盖插件自身 package.json 中的配置
1
2
3
4
5
6
// config/plugin.js
// 使用 mysql 插件
exports.mysql = {
enable: true,
package: 'egg-mysql',
};

Extend 框架扩展

  • Application
  • Context
  • Request
  • Response
  • Helper
Application
1
2
3
4
// use
this.app
// extend
app/extend/application.js
Context
1
2
3
4
// use
this.ctx
// extend
app/extend/context.js
Request
1
2
3
4
// use
this.ctx.request
// extend
app/extend/request.js
Response
1
2
3
4
// use
this.ctx.response
// extend
app/extend/response.js
Helper
1
2
3
4
// use
this.ctx.helper
// extend
app/extend/helper.js

日志

  • appLogger ${appInfo.name}-web.log,例如 example-app-web.log,应用相关日志,供应用开发者使用的日志。我们在绝大数情况下都在使用它。
  • coreLogger egg-web.log 框架内核、插件日志。
  • errorLogger common-error.log 实际一般不会直接使用它,任何 logger 的 .error() 调用输出的日志都会重定向到这里,重点通过查看此日志定位异常。
  • agentLogger egg-agent.log agent 进程日志,框架和使用到 agent 进程执行任务的插件会打印一些日志到这里。