egg 编码规范_Egg 框架简述 (仅学习)
簡述
簡單層級關系
路由(Router)
內置對象
配置(Config)
中間件(MiddleWare)
插件(Plugins)
常用對象關系模型(egg-sequelize)
Worker和Agent
定時任務
1. 簡述
官方文檔:Born to build better enterprise frameworks and apps
egg.js是基于koa為底層,由阿里nodejs團隊封裝的企業級Web應用解決方案,以約束和規范化團隊開發,幫助開發團隊和開發人員降低開發和維護成本為核心設計理念的優秀解決方案。
官方文檔對 egg.js 的闡述極致細致,撰寫本文的目的僅僅是對 Egg 的整體結構做一個簡述,以引導學習為主要目的。
P.S. 本文示例代碼部分使用 TypeScript 進行編寫,因此所有源碼文件都以 .ts 作為擴展名。
2. 簡單層級關系
MVC(Model View Controller)是一種軟件設計模式,一種以“展示界面、業務邏輯、數據模型”分離的方法組織代碼,將業務設計打散分離,以便實現高可復用性,及可維護性。
早些年的項目中,Controller層級中需要處理的事情非常之多:接受用戶請求、驗證請求有效性、計算或發送請求至Model抓取數據或修改、計算響應數據、返回響應數據等。
image
隨著一些項目逐漸龐大,這樣的設計造成了同一文件(或函數)的代碼劇增,可維護性降低。同時,有一些可公用的業務操作也急需單獨提取,因此形成了獨立的業務層,分化了Controller部分。
image
至此,形成了常見的軟件設計層次結構的主線路:
View:作為用戶的 視圖表現 部分,常見的展示形式如瀏覽器作為載體的網頁、原生APP應用界面、桌面應用界面等,用于提供用戶界面以便收集、響應用戶行為產生的數據;
Controller:作為 控制器層 部分,控制用戶界面(View)的數據流轉途徑,主要行為包含接收用戶數據請求、發送請求至業務層(Service)、獲取業務層(Service)數據響應,將響應數據發送至用戶界面(View),或生成相應的模板界面發送至用戶;
Service:作為 業務處理層 部分,主要負責收集及對數據進行相應的運算處理,主要行為包含收集控制器請求數據、數據有效性驗證、運算、請求數據模型(Model)、接收數據模型(Model)響應消息、響應結果至控制器等;
Model:作為 數據模型層 部分,主要用于將數據持久化(OUT)、查詢持久化數據(IN),常見行為如對數據庫進行操作、緩存數據庫數據等;
// 這是一個 egg 項目的目錄結構
├─ app
│ ├─ controller
│ │ └─ home.ts
│ ├─ service
│ │ └─ home.ts
│ └─ model
│ └─ user.ts
3. 路由(Router)
路由主要用于對數據流向進行指引,并處理請求轉發。生活中常見的就是家用的路由器:
image
在Web應用進行前后端交互的過程中,路由亦起到了通過URL地址定位控制器函數的作用,當然,更準確的說法應該是定位靜態資源(無論是接口數據、頁面、圖片等其他文件)。如假設 app/controller/home.ts 中存在函數 a() 和函數 b(),我們約定了跳轉 http://luv-ui.com/a 則執行函數 a();跳轉 http://luv-ui.com/b 則執行函數 b()。這是Web應用中的控制器-路由的常見表現手段。
在JAVA項目中,常見的路由表現手段例如
在XML配置文件中對路由進行統一描述:
<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 中,約定了路由統一由 app/router.ts 進行定義,理由是:通過統一的配置,我們可以避免路由規則邏輯散落在多個地方,從而出現未知的沖突,集中在一起我們可以更方便的來查看全局的路由規則。
因此,我們的目錄結構變化為:
// 這是一個 egg 項目的目錄結構
├─ 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);
// ...
}
其業務邏輯如下圖所示:
image
4. 內置對象
Egg 中包含兩種內置對象:
由 Koa 繼承的對象:Application、Context、Request、Response
框架擴展的對象:Controller、Service、Helper、Config、Logger
其主要作用如下:
對象名 注釋
Application 全局應用對象,在一個應用中,只會實例化一個,我們可以為其掛載一些全局的方法和對象。在框架運行時,會在 Application 實例上觸發一些事件。我們幾乎可以在編寫應用時的任何一個地方獲取到 Application 對象用于操作。
Context 一個請求級別的對象,在每一次收到用戶請求時,框架都會實例化一個 Context 對象,這個對象封裝了這次用戶請求的信息,并提供了許多便捷的方法來獲取請求參數或者設置響應信息。通常在 Middleware、Controller、Service 中獲取操作。
Request 一個請求級別的對象,封裝了 Node.js 原生的 HTTP Request 對象,提供了一系列輔助方法獲取 HTTP 請求常用參數。通過 Context 對象的 ctx.request 來獲取其實例。
Response 一個請求級別的對象,封裝了 Node.js 原生的 HTTP Response 對象,提供了一系列輔助方法設置 HTTP 響應。通過 Context 對象的 ctx.response 來獲取其實例。
Controller Controller 控制器的基類,所有的 Controller 都應該繼承于該基類。它提供了如下常用屬性:
- ctx: 獲取當前請求中的Context對象;
- app: 應用的 Application 實例;
- config:當前應用的配置對象。
- service:包含應用所有 Service 的對象。
- logger:為當前 Controller 封裝的 logger 日志對象。
Service Service 業務層的基類,所有的 Service 都應該繼承于該基類。其提供的屬性和基類調用的方式,都與 Controller 類似。
Helper 用來提供一些實用的 utility 函數。它的作用在于我們可以將一些常用的隸屬于工具對象的動作抽離在 helper.js 里面成為一個獨立的函數,避免邏輯分散各處,同時可以更好的編寫測試用例。
Config Egg 推薦應用開發遵循配置和代碼分離的原則,將一些需要硬編碼的業務配置都放到配置文件中。在不同的運行環境可以應用不同的配置改變框架運行方式。(如開發環境和生產環境不同,對數據源、日志、插件等的應用也可能有所不同)
Logger Egg 內置了功能強大的日志功能,可以非常方便的打印各種級別的日志到對應的日志文件中,每一個 logger 對象都提供了 4 個級別的方法:
- logger.debug():用于調試階段日志記錄。
- logger.info():用于正常流程日志記錄。
- logger.warn():用于警告級別的日志記錄。
- logger.error():用于嚴重錯誤的日志記錄。
4.1 應用過程 - Controller
結合數據流轉過程,當數據傳遞至 Controller 時,我們需要進行相應的處理。Egg 約定了所有的 Controller 對象都放在 app/controller/ 位置。 Controller 部分大致長這個樣子:
import { Context, Controller } from 'egg';
export default class HomeController extends Controller {
constructor(ctx: Context) {
super(ctx);
// do something
}
// 具體的請求函數
public async foo() {
const { ctx } = this; // this 代表當前 Controller 對象本身
const { code } = ctx.query; // 獲取 Get 請求中的參數 code
ctx.body = await ctx.service.home.foo( code ); // 異步調用 Service 對象中的相應業務處理,并將結果對調用者響應
}
}
在應用的過程中,我們也可以創建自己的 BaseController 繼承自 Controller 基類。再由具體的控制器類繼承自 BaseController ,以便于實現統一的代碼部分封裝。
該示例中,默認導出的類命名方式為 XxxController ,此時,在 router.ts 中,便可以通過 app.controller.home.foo 來指定業務流轉至該函數,來獲取相應資源。
同理,ctx 對象中包含的 service 對象, 囊括了所有 app/service/ 層級下的 Service 繼承類,因此可以簡單的使用 ctx.service.xxx.yyy 來定位業務函數。
4.2 應用過程 - Service
在業務處理的 Service 部分,Egg 約定了所有的 Service 對象都放在 app/service/ 位置。大概長這個樣子:
import { Context, Service } from 'egg';
export default class HomeService extends Service {
constructor(ctx: Context) {
super(ctx);
// do something
}
// 具體的業務處理函數
public async foo( code: string ) {
const { ctx } = this; // this 代表當前 Service 對象本身
const where = { code };
return await ctx.model.user.findAll({where}); // 通過 Model(數據模型) 部分獲取靜態資源
}
}
至此,我們所看到的業務流程就變成了這個樣子:
image
5. 配置(Config)
Egg 使用代碼的方式配置當前應用的運行方式,Egg 約定了所有的配置文件都放在 ./config/ 位置。目錄結構如下:
// 這是一個 egg 項目的目錄結構
├─ app
│ ├─ controller
│ │ └─ home.ts
│ ├─ service
│ │ └─ home.ts
│ ├─ model
│ │ └─ user.ts
│ └─ router.ts
├─ config
│ ├─ config.default.ts
│ ├─ config.prod.ts
│ └─ config.local.ts
配置文件返回的是一個 object 對象,可以覆蓋框架的一些配置,應用也可以將自己業務的配置放到這里方便管理。配置文件大概長這個樣子:
// 配置文件的寫法 (config.default.ts)
import { EggAppConfig, EggAppInfo, PowerPartial } from 'egg';
export default (appInfo: EggAppInfo) => {
const config = {} as PowerPartial<EggAppConfig>;
// 其他的配置內容...
return {
...config
};
}
我們常常在配置文件中定義 中間件、日志、其他插件 的運行方式,比如在整個應用啟動的過程中,運行哪些中間件;日志輸出的方式、其他一些插件在運行過程中的參數配置之類的。這樣的配置,可能會區分為 開發環境、測試環境、生產環境 等等,在每個環境中的配置方式都可能有所不同。例如你的本地開發使用本地數據庫跑數據,連接本地庫的 IP、用戶、密碼、端口等,與線上環境的肯定有所不同。因此,針對不同環境應用不同的配置非常有意義。
值得注意的是,config.default 在任何環境中都會被加載,但加載的過程中,若環境配置中有重復項,則會覆蓋 default 中的內容。
image
由于 config.{env}.ts 的優先級更大 (它需要覆蓋默認配置,來彰顯自己的獨立性),因此應用啟動時配置文件的加載順序是:
config.default.ts
config.{env}.ts
如何變更當前運行環境中的啟動配置:
在 config 目錄下新建文件 env,在文件中鍵入當前環境關鍵字。如鍵入 prod,則在應用啟動時加載文件 config/config.prod.ts;
配置環境變量 EGG_SERVER_ENV 指定運行環境,啟動應用的過程中會讀取 process.env.EGG_SERVER_ENV 來判斷當前應使用何種方式配置應用。
注意,與其他語言開發項目不同的是,nodejs 作為服務器端環境,自提供了一個 webserver,而無需使用其他容器作為應用載體。因此,應用的啟動就代表著服務器的啟動。
此時,我們的項目結構變成了這個樣子:
image
6. 中間件(MiddleWare)
Egg 是基于 Koa 實現的,所以 Egg 的中間件形式和 Koa 的中間件形式是一樣的,都是基于洋蔥圈模型。每次我們編寫一個中間件,就相當于在洋蔥外面包了一層。類似于這個樣子:
image
Egg 約定一個中間件是一個放置在 app/middleware/ 下的獨立文件,并會 exports 一個函數。函數接收兩個參數:
- options: 中間件的配置項,框架會將 app.config[${middlewareName}] 傳遞進來。
- app: 當前應用 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'];
// 為中間件添加動態配置
config.xtoken = {
exclude: { '/access': true }
};
// 其他的配置內容...
return {
...config
};
}
屆時,我們通過該中間件,描述了所有的請求必須經過 token 校驗,除了排除列表中的請求。當然,這是應用中使用中間件的方式,還可以在框架、插件,乃至于在 router 中明確哪個請求才會由中間件進行處理。
此時的目錄結構如下:
// 這是一個 egg 項目的目錄結構
├─ 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
多個中間件時
當應用中包含有多個中間件,則中間件的加載順序以 config 中聲明中間件的數組順序而定,假設我們在中間件定義中聲明:config.middleware = ['mw1', 'mw2', 'mw3']; ,則中間件的加載順序為:mw1 -> mw2 -> mw3,在請求攔截處理中的嵌套關系為:
image
由此可見,最后被加載的中間件,將置于請求過程中的最內層進行攔截。
更簡單的攔截處理
在上述示例中,我們在 config 配置文件中,在聲明中間件結束時,為 xtoken 設置了自定義屬性 exclude 作為攔截條件,在中間件的定義文件 app/middleware/xtoken.ts 中以參數 options 獲取了攔截條件并執行相應的邏輯。而在實際開發應用時,中間件已配備了幾個通用參數,用以更簡便的設置中間件的狀態:
屬性名 類型 注釋
enable boolean 控制中間件是否開啟。
match string、stringp[]、RegEx、function 設置只有符合某些規則的請求前綴才會經過這個中間件。
ignore string、stringp[]、RegEx、function 設置符合某些規則的請求前綴不經過這個中間件。
因此,我們在 config 中的攔截規則便可以簡單的改造為:
// 配置文件 (config.default.ts)
import { EggAppConfig, EggAppInfo, PowerPartial } from 'egg';
export default (appInfo: EggAppInfo) => {
const config = {} as PowerPartial<EggAppConfig>;
// 配置中間件
config.middleware = ['xtoken', 'otherMiddleWare'];
// 為中間件添加動態配置
config.xtoken = {
// 配置所有的前綴為 /access 或 /morepath 的 url 不經過該中間件
ignore: [ '/access', '/morepath' ]
};
// 其他的配置內容...
return {
...config
};
}
總結
以上是生活随笔為你收集整理的egg 编码规范_Egg 框架简述 (仅学习)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 办信用卡机器是真的吗 办信用卡通过官方渠
- 下一篇: java web 连接linux_如何将