原文鏈接https://eggjs.org/zh-cn/intro/quickstart.html
Application
Application 是全局應用對象蓖乘,在一個應用中筷笨,只會實例化一個哈恰,在它上面我們可以掛載一些全局的方法和對象
事件
- server: 該事件一個 worker 進程只會觸發(fā)一次散罕,在 HTTP 服務完成啟動后蹭睡,會將 HTTP server 通過這個事件暴露出來給開發(fā)者车海。
- error: 運行時有任何的異常被 onerror 插件捕獲后碧浊,都會觸發(fā) error 事件涂邀,將錯誤對象和關聯(lián)的上下文(如果有)暴露給開發(fā)者,可以進行自定義的日志記錄上報等處理箱锐。
- request 和 response: 應用收到請求和響應請求時比勉,分別會觸發(fā) request 和 response 事件,并將當前請求上下文暴露出來驹止,開發(fā)者可以監(jiān)聽這兩個事件來進行日志記錄浩聋。
獲取方式
Application 對象幾乎可以在編寫應用時的任何一個地方獲取到,下面介紹幾個經(jīng)常用到的獲取方式:
幾乎所有被框架 Loader加載的文件(Controller臊恋,Service赡勘,Schedule 等),都可以 export 一個函數(shù)捞镰,這個函數(shù)會被 Loader 調(diào)用闸与,并使用 app 作為參數(shù):
module.exports = app => {
app.cache = new Cache();
};
Context
Context 是一個請求級別的對象,繼承自 Koa.Context岸售。在每一次收到用戶請求時践樱,框架會實例化一個 Context 對象,這個對象封裝了這次用戶請求的信息凸丸,并提供了許多便捷的方法來獲取請求參數(shù)或者設置響應信息拷邢。框架會將所有的 Service 掛載到 Context 實例上屎慢,一些插件也會將一些其他的方法和對象掛載到它上面(egg-sequelize 會將所有的 model 掛載在 Context 上)瞭稼。
獲取方式
最常見的 Context 實例獲取方式是在 Middleware, Controller 以及 Service 中。Controller 中的獲取方式在上面的例子中已經(jīng)展示過了腻惠,在 Service 中獲取和 Controller 中獲取的方式一樣环肘,在 Middleware 中獲取 Context 實例則和 Koa 框架在中間件中獲取 Context 對象的方式一致。
框架的 Middleware 同時支持 Koa v1 和 Koa v2 兩種不同的中間件寫法集灌,根據(jù)不同的寫法悔雹,獲取 Context 實例的方式也稍有不同:
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// 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);
}</pre>
除了在請求時可以獲取 Context 實例之外, 在有些非用戶請求的場景下我們需要訪問 service / model 等 Context 實例上的對象欣喧,我們可以通過 Application.createAnonymousContext() 方法創(chuàng)建一個匿名 Context 實例:
// app.js
module.exports = app => {
app.beforeStart(async () => {
const ctx = app.createAnonymousContext();
// preload before app start
await ctx.service.posts.load();
});
}
在定時任務中的每一個 task 都接受一個 Context 實例作為參數(shù)腌零,以便我們更方便的執(zhí)行一些定時的業(yè)務邏輯:
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// app/schedule/refresh.js
exports.task = async ctx => {
await ctx.service.posts.refresh();
};</pre>
Request & Response
Request 是一個請求級別的對象,繼承自 Koa.Request唆阿。封裝了 Node.js 原生的 HTTP Request 對象益涧,提供了一系列輔助方法獲取 HTTP 請求常用參數(shù)。
Response 是一個請求級別的對象驯鳖,繼承自 Koa.Response闲询。封裝了 Node.js 原生的 HTTP Response 對象久免,提供了一系列輔助方法設置 HTTP 響應。
獲取方式
可以在 Context 的實例上獲取到當前請求的 Request(ctx.request
) 和 Response(ctx.response
) 實例嘹裂。
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// 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);
}
}
</pre>
- Koa 會在 Context 上代理一部分 Request 和 Response 上的方法和屬性,參見 Koa.Context摔握。
- 如上面例子中的
ctx.request.query.id
和ctx.query.id
是等價的寄狼,ctx.response.body=
和ctx.body=
是等價的。 - 需要注意的是氨淌,獲取 POST 的 body 應該使用
ctx.request.body
泊愧,而不是ctx.body
。
Controller
框架提供了一個 Controller 基類盛正,并推薦所有的 Controller 都繼承于該基類實現(xiàn)删咱。這個 Controller 基類有下列屬性:
-
ctx
- 當前請求的 Context 實例。 -
app
- 應用的 Application 實例豪筝。 -
config
- 應用的配置痰滋。 -
service
- 應用所有的 service。 -
logger
- 為當前 controller 封裝的 logger 對象续崖。
在 Controller 文件中敲街,可以通過兩種方式來引用 Controller 基類:
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// app/controller/user.js
// 從 egg 上獲取(推薦)
const Controller = require('egg').Controller;
class UserController extends Controller {
// implement
}
module.exports = UserController;
// 從 app 實例上獲取
module.exports = app => {
return class UserController extends app.Controller {
// implement
};
};</pre>
Middleware
Koa 的中間件和 Express 不同严望,Koa 選擇了洋蔥圈模型多艇。
- 中間件洋蔥圖:
在前面的章節(jié)中,我們介紹了 Egg 是基于 Koa 實現(xiàn)的像吻,所以 Egg 的中間件形式和 Koa 的中間件形式是一樣的峻黍,都是基于洋蔥圈模型。每次我們編寫一個中間件拨匆,就相當于在洋蔥外面包了一層姆涩。
配置
一般來說中間件也會有自己的配置。在框架中惭每,一個完整的中間件是包含了配置處理的阵面。我們約定一個中間件是一個放置在 app/middleware 目錄下的單獨文件,它需要 exports 一個普通的 function洪鸭,接受兩個參數(shù):
options: 中間件的配置項样刷,框架會將 app.config[${middlewareName}] 傳遞進來。
app: 當前應用 Application 的實例览爵。
在應用中使用中間件
在應用中置鼻,我們可以完全通過配置來加載自定義的中間件,并決定它們的順序蜓竹。
如果我們需要加載上面的 gzip 中間件箕母,在 config.default.js 中加入下面的配置就完成了中間件的開啟和配置:
module.exports = {
// 配置需要的中間件储藐,數(shù)組順序即為中間件的加載順序
middleware: [ 'gzip' ],
// 配置 gzip 中間件的配置
gzip: {
threshold: 1024, // 小于 1k 的響應體不壓縮
},
};
router 中使用中間件
以上兩種方式配置的中間件是全局的,會處理每一次請求嘶是。 如果你只想針對單個路由生效钙勃,可以直接在 app/router.js
中實例化和掛載,如下:
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">module.exports = app => {
const gzip = app.middleware.gzip({ threshold: 1024 });
app.router.get('/needgzip', gzip, app.controller.handler);
};
</pre>
框架默認中間件
除了應用層加載中間件之外聂喇,框架自身和其他的插件也會加載許多中間件辖源。所有的這些自帶中間件的配置項都通過在配置中修改中間件同名配置項進行修改,例如框架自帶的中間件中有一個 bodyParser 中間件(框架的加載器會將文件名中的各種分隔符都修改成駝峰形式的變量名)希太,我們想要修改 bodyParser 的配置克饶,只需要在 config/config.default.js
中編寫
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">module.exports = {
bodyParser: {
jsonLimit: '10mb',
},
};</pre>
通用配置
無論是應用層加載的中間件還是框架自帶中間件,都支持幾個通用的配置項:
- enable:控制中間件是否開啟誊辉。
- match:設置只有符合某些規(guī)則的請求才會經(jīng)過這個中間件矾湃。
- ignore:設置符合某些規(guī)則的請求不經(jīng)過這個中間件。
enable
如果我們的應用并不需要默認的 bodyParser 中間件來進行請求體的解析堕澄,此時我們可以通過配置 enable 為 false 來關閉它
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">module.exports = {
bodyParser: {
enable: false,
},
};</pre>
match 和 ignore
match 和 ignore 支持的參數(shù)都一樣邀跃,只是作用完全相反,match 和 ignore 不允許同時配置蛙紫。
如果我們想讓 gzip 只針對 /static
前綴開頭的 url 請求開啟坞嘀,我們可以配置 match 選項
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">module.exports = {
gzip: {
match: '/static',
},
};
</pre>
match 和 ignore 支持多種類型的配置方式
- 字符串:當參數(shù)為字符串類型時,配置的是一個 url 的路徑前綴惊来,所有以配置的字符串作為前綴的 url 都會匹配上丽涩。 當然,你也可以直接使用字符串數(shù)組裁蚁。
- 正則:當參數(shù)為正則時矢渊,直接匹配滿足正則驗證的 url 的路徑。
- 函數(shù):當參數(shù)為一個函數(shù)時枉证,會將請求上下文傳遞給這個函數(shù)矮男,最終取函數(shù)返回的結果(true/false)來判斷是否匹配。
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">module.exports = {
gzip: {
match(ctx) {
// 只有 ios 設備才開啟
const reg = /iphone|ipad|ipod/i;
return reg.test(ctx.get('user-agent'));
},
},
};
</pre>
Router
Router 主要用來描述請求 URL 和具體承擔執(zhí)行動作的 Controller 的對應關系室谚, 框架約定了 app/router.js 文件用于統(tǒng)一所有路由規(guī)則毡鉴。
通過統(tǒng)一的配置,我們可以避免路由規(guī)則邏輯散落在多個地方秒赤,從而出現(xiàn)未知的沖突猪瞬,集中在一起我們可以更方便的來查看全局的路由規(guī)則。
app/router.js 里面定義 URL 路由規(guī)則
// app/router.js
module.exports = app => {
const { router, controller } = app;
router.get('/user/:id', controller.user.info);
};
app/controller 目錄下面實現(xiàn) Controller
// app/controller/user.js
class UserController extends Controller {
async info() {
const { ctx } = this;
ctx.body = {
name: `hello ${ctx.params.id}`,
};
}
}
RESTful 風格的 URL 定義
如果想通過 RESTful 的方式來定義路由入篮, 我們提供了 app.resources('routerName', 'pathMatch', controller)
快速在一個路徑上生成 CRUD 路由結構陈瘦。
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// app/router.js
module.exports = app => {
const { router, controller } = app;
router.resources('posts', '/api/posts', controller.posts);
router.resources('users', '/api/v1/users', controller.v1.users); // app/controller/v1/users.js
};
</pre>
上面代碼就在 /posts
路徑上部署了一組 CRUD 路徑結構,對應的 Controller 為 app/controller/posts.js
接下來潮售, 你只需要在 posts.js
里面實現(xiàn)對應的函數(shù)就可以了痊项。
Method | Path | Route Name | Controller.Action |
---|---|---|---|
GET | /posts | posts | app.controllers.posts.index |
GET | /posts/new | new_post | app.controllers.posts.new |
GET | /posts/:id | post | app.controllers.posts.show |
GET | /posts/:id/edit | edit_post | app.controllers.posts.edit |
POST | /posts | posts | app.controllers.posts.create |
PUT | /posts/:id | post | app.controllers.posts.update |
DELETE | /posts/:id | post | app.controllers.posts.destroy |
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// 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 () => {};
</pre>
如果我們不需要其中的某幾個方法锅风,可以不用在 posts.js
里面實現(xiàn),這樣對應 URL 路徑也不會注冊到 Router鞍泉。
參數(shù)獲取
Query String 方式
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// app/router.js
module.exports = app => {
app.router.get('/search', app.controller.search.index);
};
// app/controller/search.js
exports.index = async ctx => {
ctx.body = `search: ${ctx.query.name}`;
};
// curl http://127.0.0.1:7001/search?name=egg
</pre>
參數(shù)命名方式
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// app/router.js
module.exports = app => {
app.router.get('/user/:id/:name', app.controller.user.info);
};
// app/controller/user.js
exports.info = async ctx => {
ctx.body = `user: ${ctx.params.id}, ${ctx.params.name}`;
};
// curl http://127.0.0.1:7001/user/123/xiaoming
</pre>
復雜參數(shù)的獲取
路由里面也支持定義正則皱埠,可以更加靈活的獲取參數(shù):
<pre style="box-sizing: border-box; font: 13.6px/1.45 Consolas, "Liberation Mono", Menlo, Courier, monospace; margin-top: 0px; margin-bottom: 0px; overflow-wrap: normal; padding: 16px; overflow: auto; background-color: rgb(248, 248, 248); border-radius: 3px; word-break: normal;">// app/router.js
module.exports = app => {
app.router.get(/^\/package\/([\w-.]+\/[\w-.]+)$/, app.controller.package.detail);
};
// app/controller/package.js
exports.detail = async ctx => {
// 如果請求 URL 被正則匹配, 可以按照捕獲分組的順序咖驮,從 ctx.params 中獲取边器。
// 按照下面的用戶請求,`ctx.params[0]` 的 內(nèi)容就是 `egg/1.0.0`
ctx.body = `package:${ctx.params[0]}`;
};
// curl http://127.0.0.1:7001/package/egg/1.0.0</pre>