Egg 內(nèi)置基礎(chǔ)對(duì)象

2020-02-06 10:55 更新

在本章,我們會(huì)初步介紹一下框架中內(nèi)置的一些基礎(chǔ)對(duì)象,包括從 Koa 繼承而來的 4 個(gè)對(duì)象(Application, Context, Request, Response) 以及框架擴(kuò)展的一些對(duì)象(Controller, Service, Helper, Config, Logger),在后續(xù)的文檔閱讀中我們會(huì)經(jīng)常遇到它們。

Application

Application 是全局應(yīng)用對(duì)象,在一個(gè)應(yīng)用中,只會(huì)實(shí)例化一個(gè),它繼承自 Koa.Application,在它上面我們可以掛載一些全局的方法和對(duì)象。我們可以輕松的在插件或者應(yīng)用中擴(kuò)展 Application 對(duì)象。

事件

在框架運(yùn)行時(shí),會(huì)在 Application 實(shí)例上觸發(fā)一些事件,應(yīng)用開發(fā)者或者插件開發(fā)者可以監(jiān)聽這些事件做一些操作。作為應(yīng)用開發(fā)者,我們一般會(huì)在啟動(dòng)自定義腳本中進(jìn)行監(jiān)聽。

  • server: 該事件一個(gè) worker 進(jìn)程只會(huì)觸發(fā)一次,在 HTTP 服務(wù)完成啟動(dòng)后,會(huì)將 HTTP server 通過這個(gè)事件暴露出來給開發(fā)者。
  • error: 運(yùn)行時(shí)有任何的異常被 onerror 插件捕獲后,都會(huì)觸發(fā) error 事件,將錯(cuò)誤對(duì)象和關(guān)聯(lián)的上下文(如果有)暴露給開發(fā)者,可以進(jìn)行自定義的日志記錄上報(bào)等處理。
  • request 和 response: 應(yīng)用收到請(qǐng)求和響應(yīng)請(qǐng)求時(shí),分別會(huì)觸發(fā) request 和 response 事件,并將當(dāng)前請(qǐng)求上下文暴露出來,開發(fā)者可以監(jiān)聽這兩個(gè)事件來進(jìn)行日志記錄。
// 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
});
};

獲取方式

Application 對(duì)象幾乎可以在編寫應(yīng)用時(shí)的任何一個(gè)地方獲取到,下面介紹幾個(gè)經(jīng)常用到的獲取方式:

幾乎所有被框架 Loader 加載的文件(Controller,Service,Schedule 等),都可以 export 一個(gè)函數(shù),這個(gè)函數(shù)會(huì)被 Loader 調(diào)用,并使用 app 作為參數(shù):

  • 啟動(dòng)自定義腳本// app.jsmodule.exports = app => { app.cache = new Cache();};
  • Controller 文件// app/controller/user.jsclass UserController extends Controller { async fetch() { this.ctx.body = this.app.cache.get(this.ctx.query.id); }}

和 Koa 一樣,在 Context 對(duì)象上,可以通過 ctx.app 訪問到 Application 對(duì)象。以上面的 Controller 文件舉例:

// app/controller/user.js
class UserController extends Controller {
async fetch() {
this.ctx.body = this.ctx.app.cache.get(this.ctx.query.id);
}
}

在繼承于 Controller, Service 基類的實(shí)例中,可以通過 this.app 訪問到 Application 對(duì)象。

// app/controller/user.js
class UserController extends Controller {
async fetch() {
this.ctx.body = this.app.cache.get(this.ctx.query.id);
}
};

Context

Context 是一個(gè)請(qǐng)求級(jí)別的對(duì)象,繼承自 Koa.Context。在每一次收到用戶請(qǐng)求時(shí),框架會(huì)實(shí)例化一個(gè) Context 對(duì)象,這個(gè)對(duì)象封裝了這次用戶請(qǐng)求的信息,并提供了許多便捷的方法來獲取請(qǐng)求參數(shù)或者設(shè)置響應(yīng)信息??蚣軙?huì)將所有的 Service 掛載到 Context 實(shí)例上,一些插件也會(huì)將一些其他的方法和對(duì)象掛載到它上面(egg-sequelize 會(huì)將所有的 model 掛載在 Context 上)。

獲取方式

最常見的 Context 實(shí)例獲取方式是在 MiddlewareController 以及 Service 中。Controller 中的獲取方式在上面的例子中已經(jīng)展示過了,在 Service 中獲取和 Controller 中獲取的方式一樣,在 Middleware 中獲取 Context 實(shí)例則和 Koa 框架在中間件中獲取 Context 對(duì)象的方式一致。

框架的 Middleware 同時(shí)支持 Koa v1 和 Koa v2 兩種不同的中間件寫法,根據(jù)不同的寫法,獲取 Context 實(shí)例的方式也稍有不同:

// Koa v1
function* middleware(next) {
// this is instance of Context
console.log(this.query);
yield next;
}

// Koa v2
async function middleware(ctx, next) {
// ctx is instance of Context
console.log(ctx.query);
}

除了在請(qǐng)求時(shí)可以獲取 Context 實(shí)例之外, 在有些非用戶請(qǐng)求的場(chǎng)景下我們需要訪問 service / model 等 Context 實(shí)例上的對(duì)象,我們可以通過 Application.createAnonymousContext() 方法創(chuàng)建一個(gè)匿名 Context 實(shí)例:

// app.js
module.exports = app => {
app.beforeStart(async () => {
const ctx = app.createAnonymousContext();
// preload before app start
await ctx.service.posts.load();
});
}

定時(shí)任務(wù)中的每一個(gè) task 都接受一個(gè) Context 實(shí)例作為參數(shù),以便我們更方便的執(zhí)行一些定時(shí)的業(yè)務(wù)邏輯:

// app/schedule/refresh.js
exports.task = async ctx => {
await ctx.service.posts.refresh();
};

Request & Response

Request 是一個(gè)請(qǐng)求級(jí)別的對(duì)象,繼承自 Koa.Request。封裝了 Node.js 原生的 HTTP Request 對(duì)象,提供了一系列輔助方法獲取 HTTP 請(qǐng)求常用參數(shù)。

Response 是一個(gè)請(qǐng)求級(jí)別的對(duì)象,繼承自 Koa.Response。封裝了 Node.js 原生的 HTTP Response 對(duì)象,提供了一系列輔助方法設(shè)置 HTTP 響應(yīng)。

獲取方式

可以在 Context 的實(shí)例上獲取到當(dāng)前請(qǐng)求的 Request(ctx.request) 和 Response(ctx.response) 實(shí)例。

// app/controller/user.js
class UserController extends Controller {
async fetch() {
const { app, ctx } = this;
const id = ctx.request.query.id;
ctx.response.body = app.cache.get(id);
}
}
  • Koa 會(huì)在 Context 上代理一部分 Request 和 Response 上的方法和屬性,參見 Koa.Context
  • 如上面例子中的 ctx.request.query.id 和 ctx.query.id 是等價(jià)的,ctx.response.body= 和 ctx.body= 是等價(jià)的。
  • 需要注意的是,獲取 POST 的 body 應(yīng)該使用 ctx.request.body,而不是 ctx.body。

Controller

框架提供了一個(gè) Controller 基類,并推薦所有的 Controller 都繼承于該基類實(shí)現(xiàn)。這個(gè) Controller 基類有下列屬性:

  • ctx - 當(dāng)前請(qǐng)求的 Context 實(shí)例。
  • app - 應(yīng)用的 Application 實(shí)例。
  • config - 應(yīng)用的配置。
  • service - 應(yīng)用所有的 service。
  • logger - 為當(dāng)前 controller 封裝的 logger 對(duì)象。

在 Controller 文件中,可以通過兩種方式來引用 Controller 基類:

// app/controller/user.js

// 從 egg 上獲取(推薦)
const Controller = require('egg').Controller;
class UserController extends Controller {
// implement
}
module.exports = UserController;

// 從 app 實(shí)例上獲取
module.exports = app => {
return class UserController extends app.Controller {
// implement
};
};

Service

框架提供了一個(gè) Service 基類,并推薦所有的 Service 都繼承于該基類實(shí)現(xiàn)。

Service 基類的屬性和 Controller 基類屬性一致,訪問方式也類似:

// app/service/user.js

// 從 egg 上獲?。ㄍ扑])
const Service = require('egg').Service;
class UserService extends Service {
// implement
}
module.exports = UserService;

// 從 app 實(shí)例上獲取
module.exports = app => {
return class UserService extends app.Service {
// implement
};
};

Helper

Helper 用來提供一些實(shí)用的 utility 函數(shù)。它的作用在于我們可以將一些常用的動(dòng)作抽離在 helper.js 里面成為一個(gè)獨(dú)立的函數(shù),這樣可以用 JavaScript 來寫復(fù)雜的邏輯,避免邏輯分散各處,同時(shí)可以更好的編寫測(cè)試用例。

Helper 自身是一個(gè)類,有和 Controller 基類一樣的屬性,它也會(huì)在每次請(qǐng)求時(shí)進(jìn)行實(shí)例化,因此 Helper 上的所有函數(shù)也能獲取到當(dāng)前請(qǐng)求相關(guān)的上下文信息。

獲取方式

可以在 Context 的實(shí)例上獲取到當(dāng)前請(qǐng)求的 Helper(ctx.helper) 實(shí)例。

// app/controller/user.js
class UserController extends Controller {
async fetch() {
const { app, ctx } = this;
const id = ctx.query.id;
const user = app.cache.get(id);
ctx.body = ctx.helper.formatUser(user);
}
}

除此之外,Helper 的實(shí)例還可以在模板中獲取到,例如可以在模板中獲取到 security 插件提供的 shtml 方法。

// app/view/home.nj
{{ helper.shtml(value) }}

自定義 helper 方法

應(yīng)用開發(fā)中,我們可能經(jīng)常要自定義一些 helper 方法,例如上面例子中的 formatUser,我們可以通過框架擴(kuò)展的形式來自定義 helper 方法。

// app/extend/helper.js
module.exports = {
formatUser(user) {
return only(user, [ 'name', 'phone' ]);
}
};

Config

我們推薦應(yīng)用開發(fā)遵循配置和代碼分離的原則,將一些需要硬編碼的業(yè)務(wù)配置都放到配置文件中,同時(shí)配置文件支持各個(gè)不同的運(yùn)行環(huán)境使用不同的配置,使用起來也非常方便,所有框架、插件和應(yīng)用級(jí)別的配置都可以通過 Config 對(duì)象獲取到,關(guān)于框架的配置,可以詳細(xì)閱讀 Config 配置章節(jié)。

獲取方式

我們可以通過 app.config 從 Application 實(shí)例上獲取到 config 對(duì)象,也可以在 Controller, Service, Helper 的實(shí)例上通過 this.config 獲取到 config 對(duì)象。

Logger

框架內(nèi)置了功能強(qiáng)大的日志功能,可以非常方便的打印各種級(jí)別的日志到對(duì)應(yīng)的日志文件中,每一個(gè) logger 對(duì)象都提供了 4 個(gè)級(jí)別的方法:

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

在框架中提供了多個(gè) Logger 對(duì)象,下面我們簡(jiǎn)單的介紹一下各個(gè) Logger 對(duì)象的獲取方式和使用場(chǎng)景。

App Logger

我們可以通過 app.logger 來獲取到它,如果我們想做一些應(yīng)用級(jí)別的日志記錄,如記錄啟動(dòng)階段的一些數(shù)據(jù)信息,記錄一些業(yè)務(wù)上與請(qǐng)求無關(guān)的信息,都可以通過 App Logger 來完成。

App CoreLogger

我們可以通過 app.coreLogger 來獲取到它,一般我們?cè)陂_發(fā)應(yīng)用時(shí)都不應(yīng)該通過 CoreLogger 打印日志,而框架和插件則需要通過它來打印應(yīng)用級(jí)別的日志,這樣可以更清晰的區(qū)分應(yīng)用和框架打印的日志,通過 CoreLogger 打印的日志會(huì)放到和 Logger 不同的文件中。

Context Logger

我們可以通過 ctx.logger 從 Context 實(shí)例上獲取到它,從訪問方式上我們可以看出來,Context Logger 一定是與請(qǐng)求相關(guān)的,它打印的日志都會(huì)在前面帶上一些當(dāng)前請(qǐng)求相關(guān)的信息(如 [$userId/$ip/$traceId/${cost}ms $method $url]),通過這些信息,我們可以從日志快速定位請(qǐng)求,并串聯(lián)一次請(qǐng)求中的所有的日志。

Context CoreLogger

我們可以通過 ctx.coreLogger 獲取到它,和 Context Logger 的區(qū)別是一般只有插件和框架會(huì)通過它來記錄日志。

Controller Logger & Service Logger

我們可以在 Controller 和 Service 實(shí)例上通過 this.logger 獲取到它們,它們本質(zhì)上就是一個(gè) Context Logger,不過在打印日志的時(shí)候還會(huì)額外的加上文件路徑,方便定位日志的打印位置。

Subscription

訂閱模型是一種比較常見的開發(fā)模式,譬如消息中間件的消費(fèi)者或調(diào)度任務(wù)。因此我們提供了 Subscription 基類來規(guī)范化這個(gè)模式。

可以通過以下方式來引用 Subscription 基類:

const Subscription = require('egg').Subscription;

class Schedule extends Subscription {
// 需要實(shí)現(xiàn)此方法
// subscribe 可以為 async function 或 generator function
async subscribe() {}
}

插件開發(fā)者可以根據(jù)自己的需求基于它定制訂閱規(guī)范,如定時(shí)任務(wù)就是使用這種規(guī)范實(shí)現(xiàn)的。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)