Eggjs小試

前言

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


Egg.js 爲企業級框架和應用而生 ,咱們但願由 Egg.js 孕育出更多上層框架,幫助開發團隊和開發人員下降開發和維護成本。

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

步驟

開始

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

  • 操做系統:macOS
  • 運行環境: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.jsconfig.default.js兩個文件,其餘文件要本身根據需求建立。es6

創建控制器Controller

初始項目裏會有一個示例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 = {
    // 配置項
};

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開發企業級應用仍是至關方便的,雖說要根據需求裝,但安裝和配置步驟很是簡單,不少有用的業務配置都可以很方便快速配置好,還能夠區分環境,項目結構和調用方式很合理。

不足

工具函數的訪問須要本身手動添加擴展

沒有寫測試,但願下次補上。
相關文章
相關標籤/搜索