Egg 框架模型簡述 (一)
- 簡單的骨架認知
1-1. 簡述
1-2. 簡單層級關(guān)系
1-3. 路由(Router)
1-4. 內(nèi)置對象(Router)
1-5. 配置(Config)
1-6. 中間件(MiddleWare)
筆者的其他文章推薦: 《JS 函數(shù)式編程思維簡述》
1. 簡述
官方文檔:https://eggjs.org
? ? ? ?egg.js是基于koa為底層螟碎,由阿里nodejs團隊封裝的企業(yè)級Web應(yīng)用解決方案明肮,以約束和規(guī)范化團隊開發(fā)肖爵,幫助開發(fā)團隊和開發(fā)人員降低開發(fā)和維護成本為核心設(shè)計理念的優(yōu)秀解決方案。
? ? ? ?官方文檔對 egg.js 的闡述極致細致疫向,撰寫本文的目的僅僅是對 Egg 的整體結(jié)構(gòu)做一個簡述,以引導學習為主要目的样屠。
? ? ? ? P.S. 本文示例代碼部分使用 TypeScript
進行編寫碴里,因此所有源碼文件都以 .ts
作為擴展名。
2. 簡單層級關(guān)系
? ? ? ?MVC(Model View Controller)是一種軟件設(shè)計模式移斩,一種以“展示界面肚医、業(yè)務(wù)邏輯绢馍、數(shù)據(jù)模型”分離的方法組織代碼,將業(yè)務(wù)設(shè)計打散分離肠套,以便實現(xiàn)高可復用性舰涌,及可維護性。
? ? ? ?早些年的項目中你稚,Controller層級中需要處理的事情非常之多:接受用戶請求瓷耙、驗證請求有效性、計算或發(fā)送請求至Model抓取數(shù)據(jù)或修改刁赖、計算響應(yīng)數(shù)據(jù)搁痛、返回響應(yīng)數(shù)據(jù)等。
? ? ? ?隨著一些項目逐漸龐大宇弛,這樣的設(shè)計造成了同一文件(或函數(shù))的代碼劇增鸡典,可維護性降低。同時涯肩,有一些可公用的業(yè)務(wù)操作也急需單獨提取轿钠,因此形成了獨立的業(yè)務(wù)層,分化了Controller部分病苗。
至此疗垛,形成了常見的軟件設(shè)計層次結(jié)構(gòu)的主線路:
- View:作為用戶的 視圖表現(xiàn) 部分,常見的展示形式如瀏覽器作為載體的網(wǎng)頁硫朦、原生APP應(yīng)用界面贷腕、桌面應(yīng)用界面等,用于提供用戶界面以便收集咬展、響應(yīng)用戶行為產(chǎn)生的數(shù)據(jù)泽裳;
- Controller:作為 控制器層 部分,控制用戶界面(View)的數(shù)據(jù)流轉(zhuǎn)途徑破婆,主要行為包含接收用戶數(shù)據(jù)請求涮总、發(fā)送請求至業(yè)務(wù)層(Service)、獲取業(yè)務(wù)層(Service)數(shù)據(jù)響應(yīng)祷舀,將響應(yīng)數(shù)據(jù)發(fā)送至用戶界面(View)瀑梗,或生成相應(yīng)的模板界面發(fā)送至用戶;
- Service:作為 業(yè)務(wù)處理層 部分裳扯,主要負責收集及對數(shù)據(jù)進行相應(yīng)的運算處理抛丽,主要行為包含收集控制器請求數(shù)據(jù)、數(shù)據(jù)有效性驗證饰豺、運算亿鲜、請求數(shù)據(jù)模型(Model)、接收數(shù)據(jù)模型(Model)響應(yīng)消息冤吨、響應(yīng)結(jié)果至控制器等蒿柳;
- Model:作為 數(shù)據(jù)模型層 部分饶套,主要用于將數(shù)據(jù)持久化(OUT)、查詢持久化數(shù)據(jù)(IN)其馏,常見行為如對數(shù)據(jù)庫進行操作凤跑、緩存數(shù)據(jù)庫數(shù)據(jù)等;
// 這是一個 egg 項目的目錄結(jié)構(gòu)
├─ app
│ ├─ controller
│ │ └─ home.ts
│ ├─ service
│ │ └─ home.ts
│ └─ model
│ └─ user.ts
3. 路由(Router)
? ? ? ?路由主要用于對數(shù)據(jù)流向進行指引,并處理請求轉(zhuǎn)發(fā)叛复。生活中常見的就是家用的路由器:
? ? ? ?在Web應(yīng)用進行前后端交互的過程中仔引,路由亦起到了通過URL地址定位控制器函數(shù)的作用,當然褐奥,更準確的說法應(yīng)該是定位靜態(tài)資源(無論是接口數(shù)據(jù)咖耘、頁面、圖片等其他文件)撬码。如假設(shè)
app/controller/home.ts
中存在函數(shù) a()
和函數(shù) b()
儿倒,我們約定了跳轉(zhuǎn) http://luv-ui.com/a
則執(zhí)行函數(shù) a()
;跳轉(zhuǎn) http://luv-ui.com/b
則執(zhí)行函數(shù) b()
。這是Web應(yīng)用中的控制器-路由的常見表現(xiàn)手段呜笑。? ? ? ?在JAVA項目中夫否,常見的路由表現(xiàn)手段例如
- 在XML配置文件中對路由進行統(tǒng)一描述:
<package name="default" namespace="/" extends="struts-default">
<action name="aa" class="com.pro.controller.HomeAction" method="aa">
<result>/aa.jsp</result>
</action>
<action name="bb" class="com.pro.controller.HomeAction" method="bb">
<result>/bb.jsp</result>
</action>
</package>
- 在JAVA控制器文件中以注解的形式進行單獨描述:
@RestController
@RequestMapping("/home")
public class HomeController {
@RequestMapping(value = "/aa", method = RequestMethod.POST)
public Message aa(){
// do something
}
@RequestMapping(value = "/bb", method = RequestMethod.POST)
public Message bb(){
// do something
}
}
在 Egg 中,約定了路由統(tǒng)一由 app/router.ts
進行定義叫胁,理由是:通過統(tǒng)一的配置凰慈,我們可以避免路由規(guī)則邏輯散落在多個地方,從而出現(xiàn)未知的沖突驼鹅,集中在一起我們可以更方便的來查看全局的路由規(guī)則微谓。
因此,我們的目錄結(jié)構(gòu)變化為:
// 這是一個 egg 項目的目錄結(jié)構(gòu)
├─ app
│ ├─ controller
│ │ └─ home.ts
│ ├─ service
│ │ └─ home.ts
│ ├─ model
│ │ └─ user.ts
│ └─ router.ts
而 router.ts
中的處理方式如:
import { Application } from 'egg';
export default (app: Application) => {
const { controller, router } = app;
router.get('/aa', controller.home.aa);
router.get('/bb', controller.home.bb);
router.post('/user/cc', controller.user.cc);
// ...
}
其業(yè)務(wù)邏輯如下圖所示:
4. 內(nèi)置對象
? ? ? ?Egg 中包含兩種內(nèi)置對象:
- 由
Koa
繼承的對象:Application
输钩、Context
豺型、Request
、Response
- 框架擴展的對象:
Controller
买乃、Service
姻氨、Helper
、Config
剪验、Logger
其主要作用如下:
對象名 | 注釋 |
---|---|
Application | 全局應(yīng)用對象肴焊,在一個應(yīng)用中,只會實例化一個碉咆,我們可以為其掛載一些全局的方法和對象。在框架運行時蛀恩,會在 Application 實例上觸發(fā)一些事件疫铜。我們幾乎可以在編寫應(yīng)用時的任何一個地方獲取到 Application 對象用于操作。 |
Context | 一個請求級別的對象双谆,在每一次收到用戶請求時壳咕,框架都會實例化一個 Context 對象席揽,這個對象封裝了這次用戶請求的信息,并提供了許多便捷的方法來獲取請求參數(shù)或者設(shè)置響應(yīng)信息谓厘。通常在 Middleware 幌羞、Controller 、Service 中獲取操作竟稳。 |
Request | 一個請求級別的對象属桦,封裝了 Node.js 原生的 HTTP Request 對象,提供了一系列輔助方法獲取 HTTP 請求常用參數(shù)他爸。通過 Context 對象的 ctx.request 來獲取其實例聂宾。 |
Response | 一個請求級別的對象,封裝了 Node.js 原生的 HTTP Response 對象诊笤,提供了一系列輔助方法設(shè)置 HTTP 響應(yīng)系谐。通過 Context 對象的 ctx.response 來獲取其實例。 |
Controller |
Controller 控制器的基類讨跟,所有的 Controller 都應(yīng)該繼承于該基類纪他。它提供了如下常用屬性: - ctx : 獲取當前請求中的Context 對象;- app : 應(yīng)用的 Application 實例; - config :當前應(yīng)用的配置對象晾匠。 - service :包含應(yīng)用所有 Service 的對象茶袒。 - logger :為當前 Controller 封裝的 logger 日志對象。 |
Service |
Service 業(yè)務(wù)層的基類混聊,所有的 Service 都應(yīng)該繼承于該基類弹谁。其提供的屬性和基類調(diào)用的方式,都與 Controller 類似句喜。 |
Helper | 用來提供一些實用的 utility 函數(shù)预愤。它的作用在于我們可以將一些常用的隸屬于工具對象的動作抽離在 helper.js 里面成為一個獨立的函數(shù),避免邏輯分散各處咳胃,同時可以更好的編寫測試用例植康。 |
Config |
Egg 推薦應(yīng)用開發(fā)遵循配置和代碼分離的原則,將一些需要硬編碼的業(yè)務(wù)配置都放到配置文件中展懈。在不同的運行環(huán)境可以應(yīng)用不同的配置改變框架運行方式销睁。(如開發(fā)環(huán)境和生產(chǎn)環(huán)境不同,對數(shù)據(jù)源存崖、日志冻记、插件等的應(yīng)用也可能有所不同) |
Logger |
Egg 內(nèi)置了功能強大的日志功能,可以非常方便的打印各種級別的日志到對應(yīng)的日志文件中来惧,每一個 logger 對象都提供了 4 個級別的方法:- logger.debug() :用于調(diào)試階段日志記錄冗栗。 - logger.info() :用于正常流程日志記錄。 - logger.warn() :用于警告級別的日志記錄。 - logger.error() :用于嚴重錯誤的日志記錄隅居。 |
4.1 應(yīng)用過程 - Controller
? ? ? ?結(jié)合數(shù)據(jù)流轉(zhuǎn)過程钠至,當數(shù)據(jù)傳遞至 Controller
時,我們需要進行相應(yīng)的處理胎源。Egg
約定了所有的 Controller
對象都放在 app/controller/
位置棉钧。 Controller
部分大致長這個樣子:
import { Context, Controller } from 'egg';
export default class HomeController extends Controller {
constructor(ctx: Context) {
super(ctx);
// do something
}
// 具體的請求函數(shù)
public async foo() {
const { ctx } = this; // this 代表當前 Controller 對象本身
const { code } = ctx.query; // 獲取 Get 請求中的參數(shù) code
ctx.body = await ctx.service.home.foo( code ); // 異步調(diào)用 Service 對象中的相應(yīng)業(yè)務(wù)處理,并將結(jié)果對調(diào)用者響應(yīng)
}
}
在應(yīng)用的過程中,我們也可以創(chuàng)建自己的 BaseController
繼承自 Controller
基類涕蚤。再由具體的控制器類繼承自 BaseController
宪卿,以便于實現(xiàn)統(tǒng)一的代碼部分封裝。
? ? ? ?該示例中赞季,默認導出的類命名方式為 XxxController
愧捕,此時,在 router.ts
中申钩,便可以通過 app.controller.home.foo
來指定業(yè)務(wù)流轉(zhuǎn)至該函數(shù)次绘,來獲取相應(yīng)資源。
? ? ? ?同理撒遣,ctx
對象中包含的 service
對象, 囊括了所有 app/service/
層級下的 Service
繼承類,因此可以簡單的使用 ctx.service.xxx.yyy
來定位業(yè)務(wù)函數(shù)邮偎。
4.2 應(yīng)用過程 - Service
? ? ? ?在業(yè)務(wù)處理的 Service
部分,Egg
約定了所有的 Service
對象都放在 app/service/
位置义黎。大概長這個樣子:
import { Context, Service } from 'egg';
export default class HomeService extends Service {
constructor(ctx: Context) {
super(ctx);
// do something
}
// 具體的業(yè)務(wù)處理函數(shù)
public async foo( code: string ) {
const { ctx } = this; // this 代表當前 Service 對象本身
const where = { code };
return await ctx.model.user.findAll({where}); // 通過 Model(數(shù)據(jù)模型) 部分獲取靜態(tài)資源
}
}
至此禾进,我們所看到的業(yè)務(wù)流程就變成了這個樣子:
5. 配置(Config)
? ? ? ?Egg
使用代碼的方式配置當前應(yīng)用的運行方式,Egg
約定了所有的配置文件都放在 ./config/
位置廉涕。目錄結(jié)構(gòu)如下:
// 這是一個 egg 項目的目錄結(jié)構(gòu)
├─ app
│ ├─ controller
│ │ └─ home.ts
│ ├─ service
│ │ └─ home.ts
│ ├─ model
│ │ └─ user.ts
│ └─ router.ts
├─ config
│ ├─ config.default.ts
│ ├─ config.prod.ts
│ └─ config.local.ts
配置文件返回的是一個 object 對象泻云,可以覆蓋框架的一些配置,應(yīng)用也可以將自己業(yè)務(wù)的配置放到這里方便管理狐蜕。配置文件大概長這個樣子:
// 配置文件的寫法 (config.default.ts)
import { EggAppConfig, EggAppInfo, PowerPartial } from 'egg';
export default (appInfo: EggAppInfo) => {
const config = {} as PowerPartial<EggAppConfig>;
// 其他的配置內(nèi)容...
return {
...config
};
}
我們常常在配置文件中定義 中間件宠纯、日志、其他插件 的運行方式层释,比如在整個應(yīng)用啟動的過程中婆瓜,運行哪些中間件;日志輸出的方式贡羔、其他一些插件在運行過程中的參數(shù)配置之類的廉白。這樣的配置,可能會區(qū)分為 開發(fā)環(huán)境乖寒、測試環(huán)境猴蹂、生產(chǎn)環(huán)境 等等,在每個環(huán)境中的配置方式都可能有所不同楣嘁。例如你的本地開發(fā)使用本地數(shù)據(jù)庫跑數(shù)據(jù)磅轻,連接本地庫的 IP覆获、用戶、密碼瓢省、端口等,與線上環(huán)境的肯定有所不同痊班。因此勤婚,針對不同環(huán)境應(yīng)用不同的配置非常有意義。
值得注意的是涤伐,config.default 在任何環(huán)境中都會被加載馒胆,但加載的過程中,若環(huán)境配置中有重復項凝果,則會覆蓋 default 中的內(nèi)容祝迂。
image
由于 config.{env}.ts 的優(yōu)先級更大 (它需要覆蓋默認配置,來彰顯自己的獨立性)器净,因此應(yīng)用啟動時配置文件的加載順序是:
- config.default.ts
- config.{env}.ts
如何變更當前運行環(huán)境中的啟動配置:
- 在
config
目錄下新建文件env
型雳,在文件中鍵入當前環(huán)境關(guān)鍵字。如鍵入prod
山害,則在應(yīng)用啟動時加載文件config/config.prod.ts
; - 配置環(huán)境變量
EGG_SERVER_ENV
指定運行環(huán)境纠俭,啟動應(yīng)用的過程中會讀取process.env.EGG_SERVER_ENV
來判斷當前應(yīng)使用何種方式配置應(yīng)用。
注意浪慌,與其他語言開發(fā)項目不同的是冤荆,nodejs 作為服務(wù)器端環(huán)境,自提供了一個 webserver权纤,而無需使用其他容器作為應(yīng)用載體钓简。因此,應(yīng)用的啟動就代表著服務(wù)器的啟動汹想。
此時外邓,我們的項目結(jié)構(gòu)變成了這個樣子:
6. 中間件(MiddleWare)
? ? ? ?Egg 是基于 Koa 實現(xiàn)的,所以 Egg 的中間件形式和 Koa 的中間件形式是一樣的欧宜,都是基于洋蔥圈模型坐榆。每次我們編寫一個中間件,就相當于在洋蔥外面包了一層冗茸。類似于這個樣子:
? ? ? ?Egg 約定一個中間件是一個放置在
app/middleware/
下的獨立文件席镀,并會 exports 一個函數(shù)。函數(shù)接收兩個參數(shù):? ? ? ? - options: 中間件的配置項夏漱,框架會將 app.config[${middlewareName}] 傳遞進來豪诲。
? ? ? ? - app: 當前應(yīng)用 Application 的實例。
? ? ? ? 例如挂绰,我們寫了一個驗證請求中是否攜帶 token 的中間件:
// 一個中間件 ( app/middleware/xtoken.ts )
import { Context } from 'egg';
export default (options) => {
return async (ctx: Context, next: Function) => {
// 排除登錄路徑, 其他路徑需通過 token 校驗
const { url } = ctx.request;
if (!options.exclude[url]) {
return await next();
}
// 檢查 token 有效性...
};
}
? ? ? ?中間件編寫完成之后屎篱,我們需要在配置文件中服赎,配置該中間件,使其生效:
// 配置文件 (config.default.ts)
import { EggAppConfig, EggAppInfo, PowerPartial } from 'egg';
export default (appInfo: EggAppInfo) => {
const config = {} as PowerPartial<EggAppConfig>;
// 配置中間件
config.middleware = ['xtoken', 'otherMiddleWare'];
// 為中間件添加動態(tài)配置
config.xtoken = {
exclude: { '/access': true }
};
// 其他的配置內(nèi)容...
return {
...config
};
}
屆時交播,我們通過該中間件重虑,描述了所有的請求必須經(jīng)過 token 校驗,除了排除列表中的請求秦士。當然缺厉,這是應(yīng)用中使用中間件的方式,還可以在框架隧土、插件提针,乃至于在 router
中明確哪個請求才會由中間件進行處理。
此時的目錄結(jié)構(gòu)如下:
// 這是一個 egg 項目的目錄結(jié)構(gòu)
├─ app
│ ├─ controller
│ │ └─ home.ts
│ ├─ service
│ │ └─ home.ts
│ ├─ model
│ │ └─ user.ts
│ ├─ middleware
│ │ └─ xtoken.ts
│ └─ router.ts
├─ config
│ ├─ config.default.ts
│ ├─ config.prod.ts
│ └─ config.local.ts
多個中間件時
? ? ? ?當應(yīng)用中包含有多個中間件曹傀,則中間件的加載順序以 config
中聲明中間件的數(shù)組順序而定辐脖,假設(shè)我們在中間件定義中聲明:config.middleware = ['mw1', 'mw2', 'mw3'];
,則中間件的加載順序為:mw1 -> mw2 -> mw3
皆愉,在請求攔截處理中的嵌套關(guān)系為:
由此可見嗜价,最后被加載的中間件,將置于請求過程中的最內(nèi)層進行攔截幕庐。
更簡單的攔截處理
? ? ? ?在上述示例中炭剪,我們在 config
配置文件中,在聲明中間件結(jié)束時翔脱,為 xtoken
設(shè)置了自定義屬性 exclude
作為攔截條件奴拦,在中間件的定義文件 app/middleware/xtoken.ts
中以參數(shù) options
獲取了攔截條件并執(zhí)行相應(yīng)的邏輯。而在實際開發(fā)應(yīng)用時届吁,中間件已配備了幾個通用參數(shù)错妖,用以更簡便的設(shè)置中間件的狀態(tài):
屬性名 | 類型 | 注釋 |
---|---|---|
enable | boolean | 控制中間件是否開啟。 |
match | string疚沐、stringp[]暂氯、RegEx、function | 設(shè)置只有符合某些規(guī)則的請求前綴才會經(jīng)過這個中間件亮蛔。 |
ignore | string痴施、stringp[]、RegEx究流、function | 設(shè)置符合某些規(guī)則的請求前綴不經(jīng)過這個中間件辣吃。 |
因此,我們在 config
中的攔截規(guī)則便可以簡單的改造為:
// 配置文件 (config.default.ts)
import { EggAppConfig, EggAppInfo, PowerPartial } from 'egg';
export default (appInfo: EggAppInfo) => {
const config = {} as PowerPartial<EggAppConfig>;
// 配置中間件
config.middleware = ['xtoken', 'otherMiddleWare'];
// 為中間件添加動態(tài)配置
config.xtoken = {
// 配置所有的前綴為 /access 或 /morepath 的 url 不經(jīng)過該中間件
ignore: [ '/access', '/morepath' ]
};
// 其他的配置內(nèi)容...
return {
...config
};
}
而在中間件文件中芬探,便可以省去了對于攔截條件的校驗 -