回顧一下上篇講到的內(nèi)容,上篇講了:
服務(wù)(Service)
Service 就是在復(fù)雜業(yè)務(wù)場景下用于做業(yè)務(wù)邏輯封裝的一個抽象層
使用場景
- 復(fù)雜數(shù)據(jù)的處理,比如要展現(xiàn)的信息需要從數(shù)據(jù)庫獲取,還要經(jīng)過一定的規(guī)則計算,才能返回用戶顯示?;蛘哂嬎阃瓿珊?,更新到數(shù)據(jù)庫。
- 第三方服務(wù)的調(diào)用,比如 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;
復(fù)制代碼
屬性
*每一次用戶請求,框架都會實例化對應(yīng)的 Service 實例,由于它繼承于 egg.Service,故擁有下列屬性方便我們進行開發(fā):
-
this.ctx: 當(dāng)前請求的上下文 Context 對象的實例 -
this.app: 當(dāng)前應(yīng)用 Application 對象的實例 -
this.service:應(yīng)用定義的 Service -
this.config:應(yīng)用運行時的配置項 -
this.logger:logger 對象,上面有四個方法(debug,info,warn,error),分別代表打印四個不同級別的日志,使用方法和效果與 context logger 中介紹的一樣,但是通過這個 logger 對象記錄的日志,在日志前面會加上打印該日志的文件路徑,以便快速定位日志打印位置。
Service ctx 詳解
-
this.ctx.curl發(fā)起網(wǎng)絡(luò)調(diào)用。 -
this.ctx.service.otherService調(diào)用其他 Service。 -
this.ctx.db發(fā)起數(shù)據(jù)庫調(diào)用等, db 可能是其他插件提前掛載到 app 上的模塊。
注意事項
- Service 文件必須放在
app/service目錄,可以支持多級目錄,訪問的時候可以通過目錄名級聯(lián)訪問。
app/service/biz/user.js => ctx.service.biz.user // 多級目錄,依據(jù)目錄名級聯(lián)訪問
app/service/sync_user.js => ctx.service.syncUser // 下劃線自動轉(zhuǎn)換為自動駝峰
app/service/HackerNews.js => ctx.service.hackerNews // 大寫自動轉(zhuǎn)換為駝峰
復(fù)制代碼
- 一個 Service 文件只能包含一個類, 這個類需要通過 module.exports 的方式返回。
- Service 需要通過 Class 的方式定義,父類必須是 egg.Service。
- Service 不是單例,是 請求級別 的對象,框架在每次請求中首次訪問
ctx.service.xx時延遲實例化,所以 Service 中可以通過 this.ctx 獲取到當(dāng)前請求的上下文。
使用 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 {
// 默認(rèn)不需要提供構(gòu)造函數(shù)。
// constructor(ctx) {
// super(ctx); 如果需要在構(gòu)造函數(shù)做一些處理,一定要有這句話,才能保證后面 `this.ctx`的使用。
// // 就可以直接通過 this.ctx 獲取 ctx 了
// // 還可以直接通過 this.app 獲取 app 了
// }
async find(uid) {
// 假如 我們拿到用戶 id 從數(shù)據(jù)庫獲取用戶詳細(xì)信息
const user = await this.ctx.db.query('select` *` from user where uid = ?', uid);
// 假定這里還有一些復(fù)雜的計算,然后返回需要的信息。
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=<span class="hljs-subst">${uid}</span>`, { dataType: 'json' });
return result.data;
}
}
module.exports = UserService;
復(fù)制代碼`
插件
為什么要插件
在使用 Koa 中間件過程中發(fā)現(xiàn)了下面一些問題:
- 中間件加載其實是有先后順序的,但是中間件自身卻無法管理這種順序,只能交給使用者。這樣其實非常不友好,一旦順序不對,結(jié)果可能有天壤之別。
- 中間件的定位是攔截用戶請求,并在它前后做一些事情,例如:鑒權(quán)、安全檢查、訪問日志等等。但實際情況是,有些功能是和請求無關(guān)的,例如:定時任務(wù)、消息訂閱、后臺邏輯等等。
- 有些功能包含非常復(fù)雜的初始化邏輯,需要在應(yīng)用啟動的時候完成。這顯然也不適合放到中間件中去實現(xiàn)。
中間件、插件、應(yīng)用的關(guān)系
一個插件其實就是一個『迷你的應(yīng)用』,和應(yīng)用(app)幾乎一樣:+
- 它包含了 Service、中間件、配置、框架擴展等等。
- 它沒有獨立的 Router 和 Controller。
- 它沒有
plugin.js,只能聲明跟其他插件的依賴,而不能決定其他插件的開啟與否。
他們的關(guān)系是:
- 應(yīng)用可以直接引入 Koa 的中間件。
- 插件本身可以包含中間件。
- 多個插件可以包裝為一個上層框架。
使用插件
插件一般通過 npm 模塊的方式進行復(fù)用:
npm i egg-mysql --save
復(fù)制代碼
建議通過 ^ 的方式引入依賴,并且強烈不建議鎖定版本。
{
"dependencies": {
"egg-mysql": "^3.0.0"
}
}
復(fù)制代碼
然后需要在應(yīng)用或框架的 config/plugin.js 中聲明:
// config/plugin.js
// 使用 mysql 插件
exports.mysql = {
enable: true,
package: 'egg-mysql',
};
復(fù)制代碼
就可以直接使用插件提供的功能:app.mysql.query(sql, values);
egg-mysql 插件文檔
參數(shù)介紹
plugin.js 中的每個配置項支持:
-
{Boolean} enable- 是否開啟此插件,默認(rèn)為 true -
{String} package- npm 模塊名稱,通過 npm 模塊形式引入插件 -
{String} path- 插件絕對路徑,跟 package 配置互斥 -
{Array} env- 只有在指定運行環(huán)境才能開啟,會覆蓋插件自身 package.json 中的配置
開啟和關(guān)閉
在上層框架內(nèi)部內(nèi)置的插件,應(yīng)用在使用時就不用配置 package 或者 path,只需要指定 enable 與否:
// 對于內(nèi)置插件,可以用下面的簡潔方式開啟或關(guān)閉
exports.onerror = false;
復(fù)制代碼
根據(jù)環(huán)境配置
同時,我們還支持 plugin.{env}.js 這種模式,會根據(jù)運行環(huán)境加載插件配置。
比如定義了一個開發(fā)環(huán)境使用的插件 egg-dev,只希望在本地環(huán)境加載,可以安裝到 devDependencies。
// npm i egg-dev --save-dev
// package.json
{
"devDependencies": {
"egg-dev": ""
}
}
復(fù)制代碼
然后在 plugin.local.js 中聲明:
// config/plugin.local.js
exports.dev = {
enable: true,
package: 'egg-dev',
};
復(fù)制代碼
這樣在生產(chǎn)環(huán)境可以 npm i --production 不需要下載 egg-dev 的包了。
注意:
- 不存在
plugin.default.js - 只能在應(yīng)用層使用,在框架層請勿使用。
package 和 path
-
package是npm方式引入,也是最常見的引入方式 -
path是絕對路徑引入,如應(yīng)用內(nèi)部抽了一個插件,但還沒達(dá)到開源發(fā)布獨立 npm 的階段,或者是應(yīng)用自己覆蓋了框架的一些插件
// config/plugin.js
const path = require('path');
exports.mysql = {
enable: true,
path: path.join(__dirname, '../lib/plugin/egg-mysql'),
};
復(fù)制代碼
插件配置
插件一般會包含自己的默認(rèn)配置,應(yīng)用開發(fā)者可以在 config.default.js 覆蓋對應(yīng)的配置:
// config/config.default.js
exports.mysql = {
client: {
host: 'mysql.com',
port: '3306',
user: 'test_user',
password: 'test_password',
database: 'test',
},
};
復(fù)制代碼
插件列表
框架默認(rèn)內(nèi)置了企業(yè)級應(yīng)用常用的插件:
-
onerror統(tǒng)一異常處理 -
SessionSession 實現(xiàn) - i18n 多語言
- watcher 文件和文件夾監(jiān)控
- multipart 文件流式上傳
- security 安全
- development 開發(fā)環(huán)境配置
- logrotator 日志切分
- schedule 定時任務(wù)
- static 靜態(tài)服務(wù)器
- jsonp jsonp 支持
- view 模板引擎
更多社區(qū)的插件可以 GitHub 搜索 egg-plugin。
插件開發(fā)詳情見 插件開發(fā)
定時任務(wù)
雖然我們通過框架開發(fā)的 HTTP Server 是請求響應(yīng)模型的,但是仍然還會有許多場景需要執(zhí)行一些定時任務(wù)
- 定時上報應(yīng)用狀態(tài)。
- 定時從遠(yuǎn)程接口更新本地緩存。
- 定時進行文件切割、臨時文件刪除。
編寫定時任務(wù)
所有的定時任務(wù)都統(tǒng)一存放在 app/schedule 目錄下,每一個文件都是一個獨立的定時任務(wù),可以配置定時任務(wù)的屬性和要執(zhí)行的方法。
在 app/schedule 目錄下創(chuàng)建一個 update_cache.js 文件
const Subscription = require('egg').Subscription;
class UpdateCache extends Subscription {
// 通過 schedule 屬性來設(shè)置定時任務(wù)的執(zhí)行間隔等配置
static get schedule() {
return {
interval: '1m', // 1 分鐘間隔
type: 'all', // 指定所有的 worker 都需要執(zhí)行
};
}
// subscribe 是真正定時任務(wù)執(zhí)行時被運行的函數(shù)
async subscribe() {
const res = await this.ctx.curl('http://www.api.com/cache', {
dataType: 'json',
});
this.ctx.app.cache = res.data;
}
}
module.exports = UpdateCache;
復(fù)制代碼
還可以簡寫為
module.exports = {
schedule: {
interval: '1m', // 1 分鐘間隔
type: 'all', // 指定所有的 worker 都需要執(zhí)行
},
async task(ctx) {
const res = await ctx.curl('http://www.api.com/cache', {
dataType: 'json',
});
ctx.app.cache = res.data;
},
};
復(fù)制代碼
這個定時任務(wù)會在每一個 Worker 進程上每 1 分鐘執(zhí)行一次,將遠(yuǎn)程數(shù)據(jù)請求回來掛載到 app.cache 上。
任務(wù)
-
task或subscribe同時支持generator functio和async function。 -
task的入?yún)?ctx,匿名的 Context 實例,可以通過它調(diào)用service等。
定時方式
定時任務(wù)可以指定 interval 或者 cron 兩種不同的定時方式。
interval
通過 schedule.interval 參數(shù)來配置定時任務(wù)的執(zhí)行時機,定時任務(wù)將會每間隔指定的時間執(zhí)行一次。interval 可以配置成
- 數(shù)字類型,單位為毫秒數(shù),例如 5000
- 字符類型,會通過 ms 轉(zhuǎn)換成毫秒數(shù),例如 5s。
module.exports = {
schedule: {
// 每 10 秒執(zhí)行一次
interval: '10s',
},
};
復(fù)制代碼
cron
通過 schedule.cron 參數(shù)來配置定時任務(wù)的執(zhí)行時機,定時任務(wù)將會按照 cron 表達(dá)式在特定的時間點執(zhí)行。cron 表達(dá)式通過 cron-parser 進行解析。
注意:cron-parser 支持可選的秒(linux crontab 不支持)。*
┬ ┬ ┬ ┬ ┬ ┬ │ │ │ │ │ | │ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun) │ │ │ │ └───── month (1 - 12) │ │ │ └────────── day of month (1 - 31) │ │ └─────────────── hour (0 - 23) │ └──────────────────── minute (0 - 59) └───────────────────────── second (0 - 59, optional)
module.exports = { schedule: { // 每三小時準(zhǔn)點執(zhí)行一次 cron: '0 0* /3 *', }, };
類型 type
*worker 和 all。worker 和 all 都支持上面的兩種定時方式,只是當(dāng)?shù)綀?zhí)行時機時,會執(zhí)行定時任務(wù)的 worker 不同:
-
worker類型:每臺機器上只有一個 worker 會執(zhí)行這個定時任務(wù),每次執(zhí)行定時任務(wù)的 worker 的選擇是隨機的。 -
all類型:每臺機器上的每個 worker 都會執(zhí)行這個定時任務(wù)。
其他參數(shù)
除了剛才介紹到的幾個參數(shù)之外,定時任務(wù)還支持這些參數(shù):
-
cronOptions: 配置 cron 的時區(qū)等,參見 cron-parser 文檔 -
immediate:配置了該參數(shù)為 true 時,這個定時任務(wù)會在應(yīng)用啟動并 ready 后立刻執(zhí)行一次這個定時任務(wù)。 -
disable:配置該參數(shù)為 true 時,這個定時任務(wù)不會被啟動。 -
env:數(shù)組,僅在指定的環(huán)境下才啟動該定時任務(wù)。
執(zhí)行日志
執(zhí)行日志會輸出到 ${appInfo.root}/logs/{app_name}/egg-schedule.log,默認(rèn)不會輸出到控制臺,可以通過 config.customLogger.scheduleLogger 來自定義。
// config/config.default.js
config.customLogger = {
scheduleLogger: {
// consoleLevel: 'NONE',
// file: path.join(appInfo.root, 'logs', appInfo.name, 'egg-schedule.log'),
},
};
復(fù)制代碼
動態(tài)配置定時任務(wù)
module.exports = app => {
return {
schedule: {
interval: app.config.cacheTick,
type: 'all',
},
async task(ctx) {
const res = await ctx.curl('http://www.api.com/cache', {
contentType: 'json',
});
ctx.app.cache = res.data;
},
};
};
復(fù)制代碼
手動執(zhí)行定時任務(wù)
我們可以通過 app.runSchedule(schedulePath) 來運行一個定時任務(wù)。app.runSchedule 接受一個定時任務(wù)文件路徑(app/schedule 目錄下的相對路徑或者完整的絕對路徑),執(zhí)行對應(yīng)的定時任務(wù),返回一個 Promise。
- 通過手動執(zhí)行定時任務(wù)可以更優(yōu)雅的編寫對定時任務(wù)的單元測試。
const mm = require('egg-mock');
const assert = require('assert');
it('should schedule work fine', async () => {
const app = mm.app();
await app.ready();
await app.runSchedule('update_cache');
assert(app.cache);
});
復(fù)制代碼
- 應(yīng)用啟動時,手動執(zhí)行定時任務(wù)進行系統(tǒng)初始化,等初始化完畢后再啟動應(yīng)用。參見應(yīng)用啟動自定義章節(jié),我們可以在
app.js中編寫初始化邏輯。
module.exports = app => {
app.beforeStart(async () => {
// 保證應(yīng)用啟動監(jiān)聽端口前數(shù)據(jù)已經(jīng)準(zhǔn)備好了
// 后續(xù)數(shù)據(jù)的更新由定時任務(wù)自動觸發(fā)
await app.runSchedule('update_cache');
});
};
復(fù)制代碼
框架擴展
框架提供了多種擴展點擴展自身的功能:Application、Context、Request、Response、Helper。
Application
訪問方式
ctx.app- Controller,Middleware,Helper,Service 中都可以通過
this.app訪問到 Application 對象,例如this.app.config訪問配置對象。 - 在 app.js 中 app 對象會作為第一個參數(shù)注入到入口函數(shù)中
// app.js
module.exports = app => {
// 使用 app 對象
};
復(fù)制代碼
擴展方式
框架會把 app/extend/application.js 中定義的對象與 Koa Application 的 prototype 對象進行合并,在應(yīng)用啟動時會基于擴展后的 prototype 生成 app 對象。
// app/extend/application.js
module.exports = {
foo(param) {
// this 就是 app 對象,在其中可以調(diào)用 app 上的其他方法,或訪問屬性
},
};
復(fù)制代碼
屬性擴展
一般來說屬性的計算只需要進行一次,那么一定要實現(xiàn)緩存,否則在多次訪問屬性時會計算多次,這樣會降低應(yīng)用性能。
推薦的方式是使用 Symbol + Getter 的模式。
// app/extend/application.js
const BAR = Symbol('Application#bar');
module.exports = {
get bar() {
// this 就是 app 對象,在其中可以調(diào)用 app 上的其他方法,或訪問屬性
if (!this[BAR]) {
// 實際情況肯定更復(fù)雜
this[BAR] = this.config.xx + this.config.yy;
}
return this[BAR];
},
};
復(fù)制代碼
Context
Context 指的是 Koa 的請求上下文,這是 請求級別 的對象,每次請求生成一個 Context 實例,通常我們也簡寫成 ctx。在所有的文檔中,Context 和 ctx 都是指 Koa 的上下文對象。
訪問方式
- middleware 中返回函數(shù)的第一個參數(shù)就是 ctx,例如
ctx.cookies.get('foo')。 - controller 有兩種寫法,類的寫法通過
this.ctx,方法的寫法直接通過ctx入?yún)ⅰ?/li> - helper,service 中的 this 指向 helper,service 對象本身,使用 this.ctx 訪問 context 對象,例如
this.ctx.cookies.get('foo')。
擴展方式
框架會把 app/extend/context.js 中定義的對象與 Koa Context 的 prototype 對象進行合并,在處理請求時會基于擴展后的 prototype 生成 ctx 對象。
// app/extend/context.js
module.exports = {
foo(param) {
// this 就是 ctx 對象,在其中可以調(diào)用 ctx 上的其他方法,或訪問屬性
},
};
復(fù)制代碼
屬性擴展同 Application
Request
Request 對象和 Koa 的 Request 對象相同,是 請求級別 的對象,它提供了大量請求相關(guān)的屬性和方法供使用。
訪問方式
ctx.request
ctx 上的很多屬性和方法都被代理到 request 對象上,對于這些屬性和方法使用 ctx 和使用 request 去訪問它們是等價的,例如 ctx.url === ctx.request.url。
擴展方式
框架會把 app/extend/request.js 中定義的對象與內(nèi)置 request 的 prototype 對象進行合并,在處理請求時會基于擴展后的 prototype 生成 request 對象。
// app/extend/request.js
module.exports = {
get foo() {
return this.get('x-request-foo');
},
};
復(fù)制代碼
Response
Response 對象和 Koa 的 Response 對象相同,是 請求級別 的對象,它提供了大量響應(yīng)相關(guān)的屬性和方法供使用。
訪問方式
ctx.response
ctx 上的很多屬性和方法都被代理到 response 對象上,對于這些屬性和方法使用 ctx 和使用 response 去訪問它們是等價的,例如 ctx.status = 404 和 ctx.response.status = 404 是等價的。
擴展方式
框架會把 app/extend/response.js 中定義的對象與內(nèi)置 response 的 prototype 對象進行合并,在處理請求時會基于擴展后的 prototype 生成 response 對象。
// app/extend/response.js
module.exports = {
set foo(value) {
this.set('x-response-foo', value);
},
};
復(fù)制代碼
就可以這樣使用啦:this.response.foo = 'bar';
Helper
Helper 函數(shù)用來提供一些實用的 utility 函數(shù)。
它的作用在于我們可以將一些常用的動作抽離在 helper.js 里面成為一個獨立的函數(shù),這樣可以用 JavaScript 來寫復(fù)雜的邏輯,避免邏輯分散各處。另外還有一個好處是 Helper 這樣一個簡單的函數(shù),可以讓我們更容易編寫測試用例。
框架內(nèi)置了一些常用的 Helper 函數(shù)。我們也可以編寫自定義的 Helper 函數(shù)。
訪問方式
通過 ctx.helper 訪問到 helper 對象,例如:
// 假設(shè)在 app/router.js 中定義了 home router
app.get('home', '/', 'home.index');
// 使用 helper 計算指定 url path
ctx.helper.pathFor('home', { by: 'recent', limit: 20 })
// => /?by=recent&limit=20
復(fù)制代碼
擴展方式
框架會把 app/extend/helper.js 中定義的對象與內(nèi)置 helper 的 prototype 對象進行合并,在處理請求時會基于擴展后的 prototype 生成 helper 對象。
// app/extend/helper.js
module.exports = {
foo(param) {
// this 是 helper 對象,在其中可以調(diào)用其他 helper 方法
// this.ctx => context 對象
// this.app => application 對象
},
};
復(fù)制代碼
啟動自定義
框架提供了統(tǒng)一的入口文件(app.js)進行啟動過程自定義,這個文件返回一個 Boot 類,我們可以通過定義 Boot 類中的生命周期方法來執(zhí)行啟動應(yīng)用過程中的初始化工作。
框架提供了這些生命周期函數(shù)供開發(fā)人員處理:
- 配置文件即將加載,這是最后動態(tài)修改配置的時機(
configWillLoad) - 配置文件加載完成(
configDidLoad) - 文件加載完成(
didLoad) - 插件啟動完畢(
willReady) - worker 準(zhǔn)備就緒(
didReady) - 應(yīng)用啟動完成(
serverDidReady) - 應(yīng)用即將關(guān)閉(
beforeClose)
// app.js
class AppBootHook {
constructor(app) {
this.app = app;
}
configWillLoad() {
// 此時 config 文件已經(jīng)被讀取并合并,但是還并未生效
// 這是應(yīng)用層修改配置的最后時機
// 注意:此函數(shù)只支持同步調(diào)用
// 例如:參數(shù)中的密碼是加密的,在此處進行解密
this.app.config.mysql.password = decrypt(this.app.config.mysql.password);
// 例如:插入一個中間件到框架的 coreMiddleware 之間
const statusIdx = this.app.config.coreMiddleware.indexOf('status');
this.app.config.coreMiddleware.splice(statusIdx + 1, 0, 'limit');
}
async didLoad() {
// 所有的配置已經(jīng)加載完畢
// 可以用來加載應(yīng)用自定義的文件,啟動自定義的服務(wù)
// 例如:創(chuàng)建自定義應(yīng)用的示例
this.app.queue = new Queue(this.app.config.queue);
await this.app.queue.init();
// 例如:加載自定義的目錄
this.app.loader.loadToContext(path.join(__dirname, 'app/tasks'), 'tasks', {
fieldClass: 'tasksClasses',
});
}
async willReady() {
// 所有的插件都已啟動完畢,但是應(yīng)用整體還未 ready
// 可以做一些數(shù)據(jù)初始化等操作,這些操作成功才會啟動應(yīng)用
// 例如:從數(shù)據(jù)庫加載數(shù)據(jù)到內(nèi)存緩存
this.app.cacheData = await this.app.model.query(QUERY_CACHE_SQL);
}
async didReady() {
// 應(yīng)用已經(jīng)啟動完畢
const ctx = await this.app.createAnonymousContext();
await ctx.service.Biz.request();
}
async serverDidReady() {
// http / https server 已啟動,開始接受外部請求
// 此時可以從 app.server 拿到 server 的實例
this.app.server.on('timeout', socket => {
// handle socket timeout
});
}
}
module.exports = AppBootHook;
復(fù)制代碼
應(yīng)用部署
在本地開發(fā)時,我們使用 egg-bin dev 來啟動服務(wù),但是在部署應(yīng)用的時候不可以這樣使用。因為 egg-bin dev 會針對本地開發(fā)做很多處理,而生產(chǎn)運行需要一個更加簡單穩(wěn)定的方式。
部署
服務(wù)器需要預(yù)裝 Node.js,框架支持的 Node 版本為 >= 8.0.0。
框架內(nèi)置了 egg-cluster 來啟動 Master 進程,Master 有足夠的穩(wěn)定性,不再需要使用 pm2 等進程守護模塊。
同時,框架也提供了 egg-scripts 來支持線上環(huán)境的運行和停止。
npm i egg-scripts --save
復(fù)制代碼
{
"scripts": {
"start": "egg-scripts start --daemon",
"stop": "egg-scripts stop"
}
}
復(fù)制代碼
這樣我們就可以通過 npm start 和 npm stop 命令啟動或停止應(yīng)用。
啟動命令
egg-scripts start --port=7001 --daemon --title=egg-server-showcase
復(fù)制代碼
支持以下參數(shù):
-
--port=7001端口號,默認(rèn)會讀取環(huán)境變量process.env.PORT,如未傳遞將使用框架內(nèi)置端口 7001。 -
--daemon是否允許在后臺模式,無需nohup。若使用 Docker 建議直接前臺運行。 -
--env=prod框架運行環(huán)境,默認(rèn)會讀取環(huán)境變量process.env.EGG_SERVER_ENV, 如未傳遞將使用框架內(nèi)置環(huán)境 prod。 -
--workers=2框架 worker 線程數(shù),默認(rèn)會創(chuàng)建和 CPU 核數(shù)相當(dāng)?shù)?app worker 數(shù),可以充分的利用 CPU 資源。 -
--title=egg-server-showcase用于方便 ps 進程時 grep 用,默認(rèn)為egg-server-${appname}。 -
--framework=yadan如果應(yīng)用使用了自定義框架,可以配置 package.json 的 egg.framework 或指定該參數(shù)。 -
--ignore-stderr忽略啟動期的報錯。 -
--https.key指定 HTTPS 所需密鑰文件的完整路徑。 -
--https.cert指定 HTTPS 所需證書文件的完整路徑。
更多參數(shù)可查看 egg-scripts 和 egg-cluster 文檔。
啟動配置項
// config/config.default.js
exports.cluster = {
listen: {
port: 7001,
hostname: '127.0.0.1',
// path: '/var/run/egg.sock',
}
}
復(fù)制代碼
停止命令
egg-scripts stop [--title=egg-server]
復(fù)制代碼
該命令將殺死 master 進程,并通知 worker 和 agent 優(yōu)雅退出。
--title=egg-server 用于殺死指定的 egg 應(yīng)用,未傳遞則會終止所有的 Egg 應(yīng)用。
日志
框架內(nèi)置了強大的企業(yè)級日志支持,由 egg-logger 模塊提供。
- 日志分級
- 統(tǒng)一錯誤日志,所有 logger 中使用 .error() 打印的 ERROR 級別日志都會打印到統(tǒng)一的錯誤日志文件中,便于追蹤
- 啟動日志和運行日志分離
- 自定義日志
- 多進程日志
- 自動切割日志
- 高性能
日志路徑
- 所有日志文件默認(rèn)都放在
${appInfo.root}/logs/${appInfo.name}路徑下,例如/home/admin/logs/example-app。 - 在本地開發(fā)環(huán)境 (env: local) 和單元測試環(huán)境 (env: unittest),為了避免沖突以及集中管理,日志會打印在項目目錄下的 logs 目錄,例如
/path/to/example-app/logs/example-app。
如果想自定義日志路徑:
// config/config.${env}.js
exports.logger = {
dir: '/path/to/your/custom/log/dir',
};
復(fù)制代碼
日志分類
框架內(nèi)置了幾種日志,分別在不同的場景下使用:
- appLogger
${appInfo.name}-web.log,例如example-app-web.log,應(yīng)用相關(guān)日志,供應(yīng)用開發(fā)者使用的日志。我們在絕大數(shù)情況下都在使用它。 - coreLogger
egg-web.log框架內(nèi)核、插件日志。 - errorLogger
common-error.log實際一般不會直接使用它,任何 logger 的 .error() 調(diào)用輸出的日志都會重定向到這里,重點通過查看此日志定位異常。 - agentLogger
egg-agent.logagent 進程日志,框架和使用到 agent 進程執(zhí)行任務(wù)的插件會打印一些日志到這里。
如果想自定義以上日志文件名稱,可以在 config 文件中覆蓋默認(rèn)值:
// config/config.${env}.js
module.exports = appInfo => {
return {
logger: {
appLogName: <span class="hljs-subst">${appInfo.name}</span>-web.log,
coreLogName: 'egg-web.log',
agentLogName: 'egg-agent.log',
errorLogName: 'common-error.log',
},
};
};
復(fù)制代碼
如何打印日志
Context Logger
用于記錄 Web 行為相關(guān)的日志。
每行日志會自動記錄上當(dāng)前請求的一些基本信息, 如 [$userId/$ip/$traceId/${cost}ms $method $url]。
ctx.logger.debug('debug info');
ctx.logger.info('some request data: %j', ctx.request.body);
ctx.logger.warn('WARNNING!!!!');
// 錯誤日志記錄,直接會將錯誤日志完整堆棧信息記錄下來,并且輸出到 errorLog 中
// 為了保證異??勺粉櫍仨毐WC所有拋出的異常都是 Error 類型,因為只有 Error 類型才會帶上堆棧信息,定位到問題。
ctx.logger.error(new Error('whoops'));
復(fù)制代碼
對于框架開發(fā)者和插件開發(fā)者會使用到的 Context Logger 還有 ctx.coreLogger。
App Logger
如果我們想做一些應(yīng)用級別的日志記錄,如記錄啟動階段的一些數(shù)據(jù)信息,可以通過 App Logger 來完成。
// app.js
module.exports = app => {
app.logger.debug('debug info');
app.logger.info('啟動耗時 %d ms', Date.now() - start);
app.logger.warn('warning!');
app.logger.error(someErrorObj);
};
復(fù)制代碼
對于框架和插件開發(fā)者會使用到的 App Logger 還有 app.coreLogger。
// app.js
module.exports = app => {
app.coreLogger.info('啟動耗時 %d ms', Date.now() - start);
};
復(fù)制代碼
Agent Logger
在開發(fā)框架和插件時有時會需要在 Agent 進程運行代碼,這時使用 agent.coreLogger。
// agent.js
module.exports = agent => {
agent.logger.debug('debug info');
agent.logger.info('啟動耗時 %d ms', Date.now() - start);
agent.logger.warn('warning!');
agent.logger.error(someErrorObj);
};
復(fù)制代碼
日志文件編碼
默認(rèn)編碼為 utf-8,可通過如下方式覆蓋:
// config/config.${env}.js
exports.logger = {
encoding: 'gbk',
};
復(fù)制代碼
日志文件格式
// config/config.${env}.js
exports.logger = {
outputJSON: true,
};
復(fù)制代碼
日志級別
日志分為 NONE,DEBUG,INFO,WARN 和 ERROR 5 個級別。
日志打印到文件中的同時,為了方便開發(fā),也會同時打印到終端中。
文件日志級別
默認(rèn)只會輸出 INFO 及以上(WARN 和 ERROR)的日志到文件中。
打印所有級別日志到文件中:
// config/config.${env}.js
exports.logger = {
level: 'DEBUG',
};
復(fù)制代碼
關(guān)閉所有打印到文件的日志:
// config/config.${env}.js
exports.logger = {
level: 'NONE',
};
復(fù)制代碼
生產(chǎn)環(huán)境打印 debug 日志
為了避免一些插件的調(diào)試日志在生產(chǎn)環(huán)境打印導(dǎo)致性能問題,生產(chǎn)環(huán)境默認(rèn)禁止打印 DEBUG 級別的日志,如果確實有需求在生產(chǎn)環(huán)境打印 DEBUG 日志進行調(diào)試,需要打開 allowDebugAtProd 配置項。
// config/config.prod.js
exports.logger = {
level: 'DEBUG',
allowDebugAtProd: true,
};
復(fù)制代碼
終端日志級別
默認(rèn)只會輸出 INFO 及以上(WARN 和 ERROR)的日志到終端中。(注意:這些日志默認(rèn)只在 local 和 unittest 環(huán)境下會打印到終端)
logger.consoleLevel: 輸出到終端日志的級別,默認(rèn)為 INFO
打印所有級別日志到終端:
// config/config.${env}.js
exports.logger = {
consoleLevel: 'DEBUG',
};
復(fù)制代碼
關(guān)閉所有打印到終端的日志:
// config/config.${env}.js
exports.logger = {
consoleLevel: 'NONE',
};
復(fù)制代碼
基于性能的考慮,在正式環(huán)境下,默認(rèn)會關(guān)閉終端日志輸出。如有需要,你可以通過下面的配置開啟。(不推薦)
// config/config.${env}.js
exports.logger = {
disableConsoleAfterReady: false,
};
復(fù)制代碼
日志切割
框架對日志切割的支持由 egg-logrotator 插件提供。
按天切割
這是框架的默認(rèn)日志切割方式,在每日 00:00 按照 .log.YYYY-MM-DD 文件名進行切割。
以 appLog 為例,當(dāng)前寫入的日志為 example-app-web.log,當(dāng)凌晨 00:00 時,會對日志進行切割,把過去一天的日志按 example-app-web.log.YYYY-MM-DD 的形式切割為單獨的文件。
按照文件大小切割*
// config/config.${env}.js
const path = require('path');
module.exports = appInfo => {
return {
logrotator: {
filesRotateBySize: [
path.join(appInfo.root, 'logs', appInfo.name, 'egg-web.log'),
],
maxFileSize: 2` *` 1024 * 1024 * 1024,
},
};
};
按照小時切割
這和默認(rèn)的按天切割非常類似,只是時間縮短到每小時。
// config/config.${env}.js
const path = require('path');
module.exports = appInfo => {
return {
logrotator: {
filesRotateByHour: [
path.join(appInfo.root, 'logs', appInfo.name, 'common-error.log'),
],
},
};
};
復(fù)制代碼
性能
通常 Web 訪問是高頻訪問,每次打印日志都寫磁盤會造成頻繁磁盤 IO,為了提高性能,我們采用的文件日志寫入策略是:
日志同步寫入內(nèi)存,異步每隔一段時間(默認(rèn) 1 秒)刷盤
更多詳細(xì)請參考 egg-logger 和 egg-logrotator。
HttpClient
框架基于 urllib 內(nèi)置實現(xiàn)了一個 HttpClient,應(yīng)用可以非常便捷地完成任何 HTTP 請求。
通過 app 使用 HttpClient
架在應(yīng)用初始化的時候,會自動將 HttpClient 初始化到 app.httpclient。 同時增加了一個 app.curl(url, options) 方法,它等價于 app.httpclient.request(url, options)。
// app.js
module.exports = app => {
app.beforeStart(async () => {
// 示例:啟動的時候去讀取 https://registry.npm.taobao.org/egg/latest 的版本信息
const result = await app.curl('https://registry.npm.taobao.org/egg/latest', {
dataType: 'json',
});
app.logger.info('Egg latest version: %s', result.data.version);
});
};
復(fù)制代碼
通過 ctx 使用 HttpClient
框架在 Context 中同樣提供了 ctx.curl(url, options) 和 ctx.httpclient,保持跟 app 下的使用體驗一致。 這樣就可以在有 Context 的地方(如在 controller 中)非常方便地使用 ctx.curl() 方法完成一次 HTTP 請求。
// app/controller/npm.js
class NpmController extends Controller {
async index() {
const ctx = this.ctx;
// 示例:請求一個 npm 模塊信息
const result = await ctx.curl('https://registry.npm.taobao.org/egg/latest', {
// 自動解析 JSON response
dataType: 'json',
// 3 秒超時
timeout: 3000,
});
ctx.body = {
status: result.status,
headers: result.headers,
package: result.data,
};
}
}
復(fù)制代碼
基本 HTTP 請求
GET
// app/controller/npm.js
class NpmController extends Controller {
async get() {
const ctx = this.ctx;
const result = await ctx.curl('https://httpbin.org/get?foo=bar');
ctx.status = result.status;
ctx.set(result.headers);
ctx.body = result.data;
}
}
復(fù)制代碼
POST
const result = await ctx.curl('https://httpbin.org/post', {
// 必須指定 method
method: 'POST',
// 通過 contentType 告訴 HttpClient 以 JSON 格式發(fā)送
contentType: 'json',
data: {
hello: 'world',
now: Date.now(),
},
// 明確告訴 HttpClient 以 JSON 格式處理返回的響應(yīng) body
dataType: 'json',
});
復(fù)制代碼
PUT
const result = await ctx.curl('https://httpbin.org/put', {
// 必須指定 method
method: 'PUT',
// 通過 contentType 告訴 HttpClient 以 JSON 格式發(fā)送
contentType: 'json',
data: {
update: 'foo bar',
},
// 明確告訴 HttpClient 以 JSON 格式處理響應(yīng) body
dataType: 'json',
});
復(fù)制代碼
DELETE
const result = await ctx.curl('https://httpbin.org/delete', {
// 必須指定 method
method: 'DELETE',
// 明確告訴 HttpClient 以 JSON 格式處理響應(yīng) body
dataType: 'json',
});
復(fù)制代碼
options 參數(shù)詳解
httpclient.request(url, options)
HttpClient 默認(rèn)全局配置,應(yīng)用可以通過 config/config.default.js 覆蓋此配置。
常用
-
data: Object需要發(fā)送的請求數(shù)據(jù),根據(jù) method 自動選擇正確的數(shù)據(jù)處理方式。-
GET,HEAD:通過querystring.stringify(data)處理后拼接到 url 的 query 參數(shù)上。 -
POST,PUT和DELETE等:需要根據(jù)contentType做進一步判斷處理。-
contentType = json:通過JSON.stringify(data)處理,并設(shè)置為 body 發(fā)送。 - 其他:通過
querystring.stringify(data)處理,并設(shè)置為 body 發(fā)送。
-
-
files: Mixed-
method: String設(shè)置請求方法,默認(rèn)是 GET。 支持 GET、POST、PUT、DELETE、PATCH 等所有 HTTP 方法。 -
contentType: String設(shè)置請求數(shù)據(jù)格式,默認(rèn)是 undefined,HttpClient 會自動根據(jù) data 和 content 參數(shù)自動設(shè)置。 data 是 object 的時候默認(rèn)設(shè)置的是 form。支持 json 格式。 -
dataType: String設(shè)置響應(yīng)數(shù)據(jù)格式,默認(rèn)不對響應(yīng)數(shù)據(jù)做任何處理,直接返回原始的 buffer 格式數(shù)據(jù)。 支持 text 和 json 兩種格式。 -
headers: Object自定義請求頭。 -
timeout: Number|Array請求超時時間,默認(rèn)是[ 5000, 5000 ],即創(chuàng)建連接超時是 5 秒,接收響應(yīng)超時是 5 秒。
調(diào)試輔助(對 ctx.curl 進行抓包)
如果你需要對 HttpClient 的請求進行抓包調(diào)試,可以添加以下配置到 config.local.js:
// config.local.js
module.exports = () => {
const config = {};
// add http_proxy to httpclient
if (process.env.http_proxy) {
config.httpclient = {
request: {
enableProxy: true,
rejectUnauthorized: false,
proxy: process.env.http_proxy,
},
};
}
return config;
}
復(fù)制代碼
然后啟動你的抓包工具,如 charles 或 fiddler。
最后通過以下指令啟動應(yīng)用:
http_proxy=http://127.0.0.1:8888 npm run dev
復(fù)制代碼
windows 下可以用cmder 或者 git bash
set http_proxy=http://127.0.0.1:8888 && npm run dev
復(fù)制代碼
然后就可以正常操作了,所有經(jīng)過 HttpClient 的請求,都可以你的抓包工具中查看到。