這段時間,用Eggjs做爲後端服務框架開發了幾個項目。項目都很小,但爲了進一步瞭解Eggjs,特地選擇了Eggjs做爲框架基礎開發後端服務。期間也遇到過一些問題和坑,還有幾個值得注意的點,下面來說一下我這段時間開發的總結。javascript
Egg.js 爲企業級框架和應用而生 ,咱們但願由 Egg.js 孕育出更多上層框架,幫助開發團隊和開發人員下降開發和維護成本。
這個是Eggjs文檔對Eggjs的解釋,關於Eggjs的詳細介紹和使用請點解前面的地址;相對於Egg.js 1.x版本的文檔,已經有很大的改進了,不少關鍵的地方均可以比較完整講解和帶有表明性的實例。css
用的Egg.js版本是2.2.1
,對環境有必定的要求,本人用的配置以下:html
v9.8.0
使用腳手架快速建立項目:java
$ npm i egg-init -g $ egg-init egg-example --type=simple $ cd egg-example $ npm i
項目安裝完畢,啓動項目:node
$ npm run dev $ open localhost:7001
至此,項目順利創建及啓動完畢。mysql
項目結構:(摘自文檔)git
egg-project ├── package.json ├── app.js (可選) ├── agent.js (可選) ├── app | ├── router.js │ ├── controller │ | └── home.js │ ├── service (可選) │ | └── user.js │ ├── middleware (可選) │ | └── response_time.js │ ├── schedule (可選) │ | └── my_task.js │ ├── public (可選) │ | └── reset.css │ ├── view (可選) │ | └── home.tpl │ └── extend (可選) │ ├── helper.js (可選) │ ├── request.js (可選) │ ├── response.js (可選) │ ├── context.js (可選) │ ├── application.js (可選) │ └── agent.js (可選) ├── config | ├── plugin.js | ├── config.default.js │ ├── config.prod.js | ├── config.test.js (可選) | ├── config.local.js (可選) | └── config.unittest.js (可選) └── test ├── middleware | └── response_time.test.js └── controller └── home.test.js
上述目錄也是一個給開發者一個目錄建立的指南,但按照文檔創建的項目目錄結構沒有那麼全,基本上標註爲「可選」的都是初始沒有的,在/config
目錄裏也只有plugin.js
和config.default.js
兩個文件,其餘文件要本身根據需求建立。es6
初始項目裏會有一個示例Controller
,在建立一個新的Controller
能夠參考/app/controller/home.js
的示例,通常而言,推薦使用module.exports
暴露出一個類或者參數爲app
返回一個類的函數(文檔示例中爲箭頭函數,其餘方式沒試過不清楚),類裏面包含着這塊業務的一些操做,下面在控制器文件目錄/app/controller/
裏新建一個文件名爲user.js
的控制器文件:github
// 繼承egg的控制器 const Controller = require('egg').Controller; class UserController extends Controller { async index() { const { ctx } = this; const { name } = ctx.request.body; ctx.body = `hi, ${name}`; } async getUserById() { const { userId } = this.ctx.request.body; // 使用業務函數查詢用戶信息 const userInfo = await this.service.user.findById(userId); this.ctx.body = { msgCode: 0, message: '成功', data: userInfo }; } } // 注意:必定要將控制器暴露出去,不然請求的時候會報找不到該controller的錯誤; module.exports = UserController;
路由代碼在/app
目錄之下,文件名router.js
,添加路由的代碼以下:web
// 參數app爲全局應用的對象 module.exports = app => { const { router, controller, middleware } = app; // 在這裏controller至關於app下的controller文件目錄,user爲user.js,index爲控制器類的index方法 router.get('/', controller.user.index); };
一般,controller
主要處理數據的結構和處理返回的結果,具體的涉及的業務由service
業務類方法完成,編寫service
,在目錄/app/service/
下創建user.js
文件,並編寫代碼:
// 一樣要繼承egg的Service類 const Service = require('egg').Service; class UserService extends Service { // 根據用戶id查找用戶 async findUserById(id) { const mysql = this.app.mysql; const result = await mysql.get('users', { id }); return result; } } module.exports = UserService;
eggjs simple 版本旨在根據業務需求添加eggjs的插件來搭建上層框架。在本人開發過程當中,用到的一些插件作簡要說明。
插件安裝:
$ npm i --save egg-pluginName
在文件/config/plugin.js
添加配置:
exports.pluginName = { enable: true, package: 'egg-pluginName', };
須要插件初始化配置的狀況下,修改/config/config.default.js
:
config.pluginName = { // 配置項 };
因使用
mysql
數據庫,須要一個nodejs
對mysql
的操做庫,基於eggjs
選擇了egg-mysql
,操做文檔點擊這裏。基本的數據庫增刪查改都能操做,寫起來還挺方便,可是有個需求,編寫某個接口,返回當前用戶某一段時間的數據,這就比較蛋疼了,找了好久,就連egg-mysql
封裝的庫ali-rds查看了源碼也找不到這類方法,無奈之下,只能經過組織原生的mysql
查詢語句去動態拼湊,雖然不推薦,不過若是找到更好的方法,仍是願意改寫的。查詢指定日期數據的
mysql
相關參考資料:
redis
至關於基於內存的一個微型數據庫,其存取速度很是快,代碼執行的時候幾乎感受不到阻塞,這裏使用egg-redis
做爲項目對redis
的操做庫,文檔點擊 這裏。文檔說明解析了基本的存取和設置操做,對於較爲複雜的操做只能經過查看redis
的 官方文檔,對應的命令小寫即爲方法名:
redis
命令DECR
,對應的方法使用:
// /app/controller/user.js const value = await this.app.redis.decr(keyName);
添加過幾個插件以後,發現其中的源代碼都是以擴展內置對象的方式去掛載相關的庫或者插件的。
文檔原文:「通常來講屬性的計算在同一次請求中只須要進行一次,那麼必定要實現緩存,不然在同一次請求中屢次訪問屬性時會計算屢次,這樣會下降應用性能。
推薦的方式是使用 Symbol + Getter 的模式。」
const jwt = require('jsonwebtoken'); const JWT = Symbol('Context#jwt'); module.exports = { get jwt() { if (!this[JWT]) { this[JWT] = jwt; } return this[JWT]; } };
第一次擴展cxt
對象的時候,不明白爲什麼要使用Symbol + Getter
的模式,後來基於這個問題,查找資料,發現這種方式更能避免和其餘屬性名發生衝突,上述代碼中,ctx
的jwt
定義爲只讀方式。在方便維護的同時,生成一個帶有命名空間(Context#jwt
)字符串描述的Symbol
實例數據, 做爲ctx
的屬性,經過只讀屬性jwt
來獲取內部的JWT
屬性。
PS:
ctx.JWT == ctx[JWT] // false
關於Symbol
的介紹和使用請參考阮一峯的ES6。
在擴展app
對象的時候,遇到個問題,就是若是須要獲取ctx
怎麼辦?
查找文檔,找到了在擴展app
對象時,只須要在函數體裏添加一句代碼:
const ctx = app.createAnonymousContext();
就能夠獲取ctx
對象,這對於使用其餘函數提供了一道橋樑。
eggjs
的中間件處理流程遵循koa
的洋蔥式請求模型
中間件的寫法:
module.exports = options => { return async function middleWareFunctionName (ctx, next) { // 控制器以前業務處理代碼 // ... await next(); //控制器以後業務處理代碼 // ... } }
中間件以返回一個處理業務的函數爲主體,函數接收兩個參數:ctx
、next
。ctx
則是請求級別的對象,next()
方法可讓請求進入下一個步驟。特別注意的是:在一個控制器中,有對請求到達下一步以前作一些操做的,能夠控制next()
在代碼流程中的位置,其後也能夠處理請求以後的操做。
在eggjs
寫定時任務也是很是簡單的,關注於業務代碼,加以簡單的配置,便可使用定時任務。
下面是一個簡單的定時統計業務數據的定時任務:
const Subscription = require('egg').Subscription; class Statistics extends Subscription { // 經過 schedule 屬性來設置定時任務的執行間隔等配置 static get schedule() { return { cron: '00 59 23 * * *', // 秒 分 時 日 月 年 // interval: '10s', // 設置時間間隔觸發,單位s爲秒,ms爲毫秒 type: 'worker', // all 指定全部的 worker 都須要執行, worker 爲某一個 worker 執行 }; } // subscribe 是真正定時任務執行時被運行的函數 async subscribe() { // 定時任務業務代碼 // ... } } module.exports = Statistics;
在程序啓動的時候,就會在配置的指定時機執行相關的業務代碼。
eggjs
在v2.x
版本以後默認開啓了csrf
插件,已確保基於cookie
存儲驗證信息的網站信息安全。
csrf
能將請求限制在同源網站,即只有擁有「專有令牌」的網站發送請求才會正確響應。此處容易與jwt
的做用混淆,能夠看看這篇文章。
使用
egg-cors
;
使用jwt驗證
jwt
則在認證方式上跟csrf
上有所不一樣,jwt
能夠在不使用cookie
的狀況下,以token
的方式在先後端交互數據的body
裏傳輸,也能夠在header
裏設置相關信息,詳細能夠參考這篇文章。
類的寫法
文檔中,有《應用部署》一文,裏面介紹的很詳細的。使用egg-script
插件啓動生產環境中的應用程序。項目生產靜默部署,啓動使用npm start
,中止使用npm stop
。 另,在開發環境裏想要使用pm2管理進程後臺啓動,--watch
會不斷打印控制檯日誌,緣由不清楚。
啓動命令:
$ npm install --production $ npm start
中止命令:
$ npm stop
使用
eggjs
開發企業級應用仍是至關方便的,雖說要根據需求裝,但安裝和配置步驟很是簡單,不少有用的業務配置都可以很方便快速配置好,還能夠區分環境,項目結構和調用方式很合理。
工具函數的訪問須要本身手動添加擴展
沒有寫測試,但願下次補上。