egg 學習與分享
什麼是 egg
egg.js是基於koa爲底層,由阿里nodejs團隊封裝的企業級Web應用解決方案,以約束和規範化團隊開發,幫助開
發團隊和開發人員降低開發和維護成本爲核心設計理念的優秀解決方案。
官方文檔地址:
https://eggjs.org/zh-cn/index.html
創建一個 egg 應用
創建工程目錄,進入工程目錄下執行以下命令,初始化一個基本的 egg 工程結構。
npm init egg --type=simple
查看工程目錄
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-XqPRYGFg-1573725081950)(/home/xiehuafeng/.config/Typora/typora-user-images/1573093330140.png)]
- app/controller/** 用於解析用戶的輸入,處理後返回相應的結果
- app/router.js 用於配置 URL 路由規則
- config/config.{env}.js 用於編寫配置文件
- config/plugin.js 用於配置需要加載的插件
- test/** 用於單元測試
在項目工程根目錄執行以下命令,啓動項目。
npm install
npm run dev
訪問 http://localhost:7001,可得到結果:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-IbZ9JDnt-1573725081951)(/home/xiehuafeng/.config/Typora/typora-user-images/1573094226305.png)]
我們通常使用vscode作egg工程開發,開發過程當中爲了方便調試工程,可以在項目根目錄新建 .vscode 目錄,然後新建 launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Egg",
"type": "node",
"request": "launch",
"cwd": "/home/xiehuafeng/gitee/shared_documents/nodejs/helloWorld", //工程所在目錄
"runtimeExecutable": "npm",
"windows": { "runtimeExecutable": "npm.cmd" },
"runtimeArgs": [ "run", "debug" ],
"console": "integratedTerminal",
"protocol": "auto",
"restart": true,
"port": 9999
}
]
}
這樣就可以直接使用 F5 對代碼進行斷點調試。
內置對象
- Application 代表着這個應用的全局對象,在一個應用中,只會實例化一個。掛載一些全局的方法和對象。
- Context 在每次收到用戶請求的時候,會實例化一個context對象,這個對象封裝了這次請求的信息,並提供了許多便捷的方法來獲取請求參數和相應信息。
- Request 是一個請求級別的對象,繼承自 Koa.Request。封裝了 Node.js 原生的 HTTP Request 對象,提供了一系列輔助方法獲取 HTTP 請求常用參數。
- Response 是一個請求級別的對象,繼承自 Koa.Response。封裝了 Node.js 原生的 HTTP Response 對象,提供了一系列輔助方法設置 HTTP 響應。
- Controller 框架提供的 Controller 基類,並推薦所有的 Controller 都繼承於這個基類實現。
- Service 框架提供的 Service 基類,並推薦所有的 Service 都繼承於這個基類實現。
- Helper 用來提供一些實用的 utility 函數。它的作用在於我們可以將一些常用的動作抽離在 helper.js 裏面成爲一個獨立的函數,這樣可以用 JavaScript 來寫複雜的邏輯,避免邏輯分散各處,同時可以更好的編寫測試用例。
- Config 通過這個對象可以讀取到配置文件中配置的內容
- Logger 打印日誌的對象
路由(Router)
路由描述請求 URL 和具體承擔執行動作的 Controller 的對應關係 。框架約定/app/router.js
目錄用於配置路由規則。
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
};
Router 的完整定義
router.verb('path-match', app.controller.action);
router.verb('router-name', 'path-match', app.controller.action);
router.verb('path-match', middleware1, ..., middlewareN, app.controller.action);
router.verb('router-name', 'path-match', middleware1, ..., middlewareN, app.controller.action);
Controller
'use strict';
const Controller = require('egg').Controller;
class HomeController extends Controller {
async index() {
const { ctx } = this;
ctx.body = 'hi, egg';
}
}
module.exports = HomeController;
Controller 有以下屬性
- ctx:當前請求的 Context 實例
- app:應用的 Application 實例
- config:應用的配置
- service:應用所有的 service
- logger:爲當前 controller 封裝的 logger 對象
Service
框架提供了 Service 基類,並推薦所有的 Service 都繼承於這個基類實現。
例如定義一個獲取 “hi, egg” 的 Service。
新建 /app/service/home.js
文件。遵循框架的約定
const Service = require("egg").Service;
class HomeService extends Service {
async getMessage() {
return "hi, egg";
}
}
module.exports = HomeService;
Controller 上調用 Service
const Service = require("egg").Service;
class HomeService extends Service {
async getMessage() {
return "hi, egg";
}
}
module.exports = HomeService;
配置(Config)
condif/config.default.js
文件如下:
'use strict';
module.exports = appInfo => {
const config = exports = {};
config.keys = appInfo.name + '_1573093135791_9959';
config.middleware = [];
const userConfig = {
// myAppName: 'egg',
cluster: {
listen: {
port: 9001
}
}
};
return {
...config,
...userConfig,
};
};
egg 還可以使用別的方式定義配置文件。例如直接把配置賦值給 module.exports
,這裏不舉例了。
程序執行之後會在run目錄下打印出程序所有的配置項,方便檢查配置項。
所有配置項都會加載到前文提及的 config 全局對象當中,無論在 Controller 和 Service 中都可以直接通過 this.config
獲取到config對象。
例如
class HomeController extends Controller {
async index() {
console.log(this.config.cluster.listen.port);
}
}
module.exports = HomeController;
中間件(middleware)
在 /config/config.default.js
文件中添加配置:
config.middlewaredemo = {
name: "this is a middleware"
};
定義一箇中間件 /app/middleware/middlewaredemo.js
module.exports = (options, app) => {
return async function middlewaredemo(ctx, next) {
console.log(app.name);
console.log(options.name);
console.log("enter middleware");
await next();
console.log("leave middleware");
}
}
兩個參數:
options: 中間件的配置項,框架會將 app.config[${middlewareName}]
傳遞進來。
app:當前應用 Application 的實例。
應用當中使用中間件,需要在配置文件中 /config/config.default.js
添加配置
config.middleware = ["middlewaredemo"];
這樣配置是對所有的請求都生效,針對某個請求,可以在路由中配置中間件。
module.exports = app => {
const { router, controller, config } = app;
const middlewaredemo = app.middleware.middlewaredemo(config.middlewaredemo, app);
router.get('/middlewaredemo', middlewaredemo, controller.home.index);
router.get('/', controller.home.index);
};
插件(Plugin)
以 mysql 爲例引入插件。在 package.json
中加入
{
"dependencies": {
"egg-mysql": "^3.0.0"
}
}
在 /config/plugin.js
中聲明
module.exports = {
mysql: {
enable: true,
package: 'egg-mysql'
}
};
plugin.js
中的每個配置項支持:
{Boolean} enable
- 是否開啓此插件,默認爲 true{String} package
-npm
模塊名稱,通過npm
模塊形式引入插件{String} path
- 插件絕對路徑,跟 package 配置互斥{Array} env
- 只有在指定運行環境才能開啓,會覆蓋插件自身package.json
中的配置
配置文件中添加配置
config.mysql = {
client: {
host: 'localhost',
port: '3306',
user: 'root',
password: '123456',
database: 'scm',
}
};
在service 層就可以做數據庫的操作
async addUser() {
this.app.mysql.query('insert into user values(?)', "xiehuafeng");
}
正常開發過程當中,我們通常會使用 sequelize 作爲數據庫操作的框架。簡單介紹他的使用。
代碼根目錄執行 npm install --save egg-sequelize mysql2
添加依賴。
在 /config/plugin.js
中聲明
module.exports = {
sequelize: {
enable: true,
package: 'egg-sequelize',
}
};
有了這個框架之後,我們可以將數據庫的操作獨立爲model層。
新建 app/model/user.js 文件,定義 User 對象和 數據表的映射關係。
module.exports = app => {
const {STRING, INTEGER, DATE} = app.Sequelize;
const User = app.model.define("user", {
name: STRING(64)
});
return User;
}
這樣在做數據庫操作可以直接操作 User 對象。
例如 上面例子可以直接改成
async addUser() {
this.ctx.model.User.save({"name": "xiehuafeng"});
}
定時任務 (Subscription)
在 app/schedule/
目錄下定義定時任務文件,如,定義 echoTask.js
文件
const Subscription = require("egg").Subscription;
class echoTask extends Subscription {
static get schedule() {
return {
interval: "1s",
type: "all"
};
}
async subscribe() {
console.log("hello");
}
}
module.exports = echoTask;
或者使用下面這種寫法
module.exports = app => {
return {
schedule: {
cron: "* * * * * *",
type: "all"
},
async task(ctx) {
console.log("hello");
}
}
}
自定義拓展
上文提到過框架本身自帶一些全局對象,我們可以根據自己的需要對這些全局對象做一些拓展。
例如 helper 對象,我們要添加一個工具函數在這個對象上。
可以定義一個 app/extends/helper.js
文件
module.exports = {
sayHello() {
console.log("hello world");
}
}
這個對象會和 helper 的原型合併。
在別的地方可以通過下面方式調用這個函數。
this.ctx.helper.sayHello();
啓動自定義
在應用啓動和關閉的生命週期裏預留的鉤子函數,在項目啓動或關閉期間我們可以通過這些函數對應用做一些動態的調整。
如下, 在 app.js 定義一個類:
class AppBootHook {
constructor(app) {
this.app = app;
}
//此時 config 文件已經被讀取併合並,但是還未生效
//這是應用層修改配置的最後時機
//這個函數只支持同步調用
configWillLoad() {
console.log("configWillLoad");
}
//所有的配置已經加載完畢
configDidLoad() {
console.log("configWillLoad");
}
//所有文件已經加載完畢
//可以用來加載應用自定義的文件,啓動自定義的服務
async didLoad() {
console.log("didload");
}
// 所有的插件都已啓動完畢,但是應用整體還未 ready
// 可以做一些數據初始化等操作,這些操作成功纔會啓動應用
async willReady() {
console.log("willRedy");
}
// 應用已經啓動完畢
async didReady() {
console.log("didReady");
}
// http / https server 已啓動,開始接受外部請求
// 此時可以從 app.server 拿到 server 的實例
async serverDidReady() {
console.log("serverDidReady");
}
//應用關閉之前做些處理
async beforeClose() {
console.log('beforeClose');
}
}
module.exports = AppBootHook;