Eggjs小試

Juven發表於2019-02-16

前言

這段時間,用Eggjs作為後端服務框架開發了幾個專案。專案都很小,但為了進一步瞭解Eggjs,特意選擇了Eggjs作為框架基礎開發後端服務。期間也遇到過一些問題和坑,還有幾個值得注意的點,下面來講一下我這段時間開發的總結。


Egg.js 為企業級框架和應用而生 ,我們希望由 Egg.js 孕育出更多上層框架,幫助開發團隊和開發人員降低開發和維護成本。

這個是Eggjs文件Eggjs的解釋,關於Eggjs的詳細介紹和使用請點解前面的地址;相對於Egg.js 1.x版本的文件,已經有很大的改進了,很多關鍵的地方都可以比較完整講解和帶有代表性的例項。

步驟

開始

用的Egg.js版本是2.2.1,對環境有一定的要求,本人用的配置如下:

  • 作業系統:macOS
  • 執行環境:v9.8.0

使用腳手架快速建立專案:

$ npm i egg-init -g
$ egg-init egg-example --type=simple
$ cd egg-example
$ npm i

專案安裝完畢,啟動專案:

$ npm run dev
$ open localhost:7001

至此,專案順利建立及啟動完畢。

專案結構:(摘自文件)

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.jsconfig.default.js兩個檔案,其他檔案要自己根據需求建立。

建立控制器Controller

初始專案裡會有一個示例Controller,在建立一個新的Controller可以參考/app/controller/home.js的示例,一般而言,推薦使用module.exports暴露出一個類或者引數為app返回一個類的函式(文件示例中為箭頭函式,其他方式沒試過不清楚),類裡面包含著這塊業務的一些操作,下面在控制器檔案目錄/app/controller/裡新建一個檔名為user.js的控制器檔案:

// 繼承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,新增路由的程式碼如下:

// 引數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 = {
    // 配置項
};

egg-mysql

因使用mysql資料庫,需要一個nodejsmysql的操作庫,基於eggjs選擇了egg-mysql ,操作文件點選這裡。基本的資料庫增刪查改都能操作,寫起來還挺方便,但是有個需求,編寫某個介面,返回當前使用者某一段時間的資料,這就比較蛋疼了,找了很久,就連egg-mysql封裝的庫ali-rds檢視了原始碼也找不到這類方法,無奈之下,只能通過組織原生的mysql查詢語句去動態拼湊,雖然不推薦,不過如果找到更好的方法,還是願意改寫的。

查詢指定日期資料的mysql相關參考資料:

egg-redis

redis相當於基於記憶體的一個微型資料庫,其存取速度非常快,程式碼執行的時候幾乎感覺不到阻塞,這裡使用egg-redis作為專案對redis的操作庫,文件點選這裡。文件說明解析了基本的存取和設定操作,對於較為複雜的操作只能通過檢視redis官方文件,對應的命令小寫即為方法名:
redis命令DECR,對應的方法使用:

// /app/controller/user.js
const value = await this.app.redis.decr(keyName);

擴充套件內建物件

新增過幾個外掛之後,發現其中的原始碼都是以擴充套件內建物件的方式去掛載相關的庫或者外掛的。

ctx

文件原文:“一般來說屬性的計算在同一次請求中只需要進行一次,那麼一定要實現快取,否則在同一次請求中多次訪問屬性時會計算多次,這樣會降低應用效能。

推薦的方式是使用 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的模式,後來基於這個問題,查詢資料,發現這種方式更能避免和其他屬性名發生衝突,上述程式碼中,ctxjwt定義為只讀方式。在方便維護的同時,生成一個帶有名稱空間(Context#jwt)字串描述的Symbol例項資料, 作為ctx的屬性,通過只讀屬性jwt來獲取內部的JWT屬性。
PS:

ctx.JWT == ctx[JWT] // false

關於Symbol的介紹和使用請參考阮一峰ES6

app

在擴充套件app物件的時候,遇到個問題,就是如果需要獲取ctx怎麼辦?
查詢文件,找到了在擴充套件app物件時,只需要在函式體裡新增一句程式碼:

const ctx = app.createAnonymousContext();

就可以獲取ctx物件,這對於使用其他函式提供了一道橋樑。

編寫中介軟體

eggjs的中介軟體處理流程遵循koa的洋蔥式請求模型
中介軟體的寫法:

module.exports = options => {
    return async function middleWareFunctionName (ctx, next) {
        // 控制器之前業務處理程式碼
        // ...
        await next();
        //控制器之後業務處理程式碼
        // ...
    }
}

中介軟體以返回一個處理業務的函式為主體,函式接收兩個引數:ctxnextctx則是請求級別的物件,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;

在程式啟動的時候,就會在配置的指定時機執行相關的業務程式碼。

配置(待補充)

csrf的討論

eggjsv2.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開發企業級應用還是相當方便的,雖然說要根據需求裝,但安裝和配置步驟非常簡單,很多有用的業務配置都能夠很方便快速配置好,還可以區分環境,專案結構和呼叫方式很合理。

不足

工具函式的訪問需要自己手動新增擴充套件

沒有寫測試,希望下次補上。

相關文章