1. 程式人生 > >使用egg.js開發後端API介面系統

使用egg.js開發後端API介面系統

什麼是Egg.js

Egg.js 為企業級框架和應用而生,我們希望由 Egg.js 孕育出更多上層框架,幫助開發團隊和開發人員降低開發和維護成本。詳細的瞭解可以參考Egg.js的官網:https://eggjs.org/zh-cn/intro/ 。

Egg.js 奉行『約定優於配置』,按照一套統一的約定進行應用開發,Egg 有很高的擴充套件性,可以按照團隊的約定定製框架,團隊內部採用這種方式可以減少開發人員的學習成本。

可以理解Egg.js是一個Node框架,同時它也是基於Koa框架基礎上的框架,我們大概瞭解一下它的前身和主要特點即可。

它的特點有:

  • 提供基於 Egg 定製上層框架的能力
  • 高度可擴充套件的外掛機制
  • 內建多程序管理
  • 基於 Koa 開發,效能優異
  • 框架穩定,測試覆蓋率高
  • 漸進式開發

本篇隨筆不是細說Egg.js 的詳細內容,畢竟官網介紹還是比較清晰的,我們主要說使用它來做一個後端的API介面系統,後端肯定需要對資料庫進行各種操作,用一個JS的方式來訪問資料庫,利用egg-sequelize外掛,建立和資料庫表進行繫結的模型進行操作,還是比較新鮮的,用了會發現確實很方便。用Egg.js來開發後端系統,相當於用前端的語言、做法,來開發後端系統了(雖然Egg.js 也可以用來做前端)。

我們知道,常規的Asp.net或者WebAPI 應用裡面,一般有MVC,模型、檢視、控制器這些物件,Egg.js 裡面也有類似的概念,我們這裡沒有用用來做前端,那麼可以不用它的檢視(Egg.js 檢視就是一個帶變數的模板檔案); 控制器就是我們這裡用到需要為前端提供API入口和返回JSON的地方,類似我們Web API裡面的控制器概念;模型這裡可以理解為對資料庫物件的封裝物件吧;另外和我們常規前端開發一樣(類似Vue+Element系統),獲取資料的操作邏輯,我們可以封裝在Service層,這樣可以降低我們控制器裡面的邏輯程式碼,同時也方便重用邏輯處理函式。MVC+Service的關係,大概如下所示。  

2、 使用egg.js開發後端API介面系統所需外掛

我依照官網的簡單案例進行快速初始化,如下所示。

我們推薦直接使用腳手架,只需幾條簡單指令,即可快速生成專案(npm >=6.1.0):

$ mkdir egg-example && cd egg-example
$ npm init egg --type=simple
$ npm i

啟動專案:

$ npm run dev

其實我們還需要一些額外的外掛來跑起來,我的包依賴檔案如下所示。

package.json

{
  "name": "example",
  "version": "1.0.0",
  "description": "## Development",
  "dependencies": {
    "egg": "^2.10.0",
    "egg-cors": "^2.2.3",
    "egg-jwt": "^3.1.7",
    "egg-mysql": "^3.0.0",
    "egg-redis": "^2.4.0",
    "egg-scripts": "^2.5.0",
    "egg-sequelize": "^4.0.2",
    "egg-view-nunjucks": "^2.3.0",
    "moment": "^2.29.1",
    "mysql2": "^2.2.5",
    "node": "^15.10.0"
  },
  "devDependencies": {
    "autod": "^3.0.1",
    "autod-egg": "^1.0.0",
    "egg-bin": "^4.15.0",
    "egg-mock": "^3.19.2",
    "eslint": "^4.18.1",
    "eslint-config-egg": "^7.0.0",
    "factory-girl": "^5.0.2",
    "sequelize-cli": "^4.0.0"
  },

我們來看看紅色部分的內容,其中

egg 是本身的框架需要的外掛,這個是整個框架的核心基礎;egg-scripts 這是部署eggjs專案的工具;

egg-corss 是跨域處理所需要的,用於設定csrf的配置等;

egg-jwt 是用於對使用者身份認證的處理外掛;

egg-mysql + Mysql2 是我們做Mysql資料庫處理說需要的外掛;

egg-redis 是我們用到redis操作,所需要的外掛,可選。

egg-sequelize 是我們操作資料庫的一個外掛,提供很多方便的介面進行處理,可以搭配Mysql或者PostgreSQL、MS SQLServer資料庫外掛進行處理的

egg-view-nunjucks 是展示檢視模板的一個外掛。

moment 是一個日期處理外掛,可以處理各種日期格式、轉換的一個外掛庫。

大概就是這些,如果需要結合前端JS的處理外掛,可以引入更多的內容,不過我們這裡主要介紹後端訪問Mysql資料庫的處理操作,提供JSON資料介面的,基本上這些也夠了。   另外,我們需要知道egg.js的目錄很多是約定位置的,因此我們需要知道常規的幾個資料夾的意義。我們簡單瞭解下目錄約定規範。
egg-project
├── package.json
├── app.js (可選)
├── agent.js (可選)
├── app
|   ├── router.js
│   ├── controller
│   |   └── home.js
│   ├── service (可選)
│   |   └── user.js
│   ├── middleware (可選)
│   |   └── response_time.js
│   ├── view (可選)
│   |   └── home.tpl
│   └── extend (可選)
│       ├── helper.js (可選)
├── config
|   ├── plugin.js
|   ├── config.default.js
│   ├── config.prod.js

我們這裡大概知道以上資料夾和檔案的意思即可。

  • app/router.js 用於配置 URL 路由規則,具體參見 Router。
  • app/controller/** 用於解析使用者的輸入,處理後返回相應的結果,具體參見 Controller。
  • app/service/** 用於編寫業務邏輯層,可選,建議使用,具體參見 Service。
  • app/middleware/** 用於編寫中介軟體,可選,具體參見 Middleware。
  • app/extend/** 用於框架的擴充套件,可選,具體參見框架擴充套件。
  • config/config.{env}.js 用於編寫配置檔案,具體參見配置。
  • config/plugin.js 用於配置需要載入的外掛,具體參見外掛。
  1)外掛的配置 我們引入的外掛模組,需要在app/plugin.js裡面啟用,如下程式碼所示。 app/plugin.js
'use strict';

exports.sequelize = {
  enable: true,
  package: 'egg-sequelize',
};
exports.mysql = {
  enable: true,
  package: 'egg-mysql',
};

exports.nunjucks = {
  enable: true,
  package: 'egg-view-nunjucks'
};
exports.redis = {
  enable: true,
  package: 'egg-redis',
};
exports.jwt = {
  enable: true,
  package: 'egg-jwt',
};

exports.cors = {
  enable: true,
  package: 'egg-cors',
};

為了訪問Mysql資料庫,我們還需要在config/config.default.js檔案中配置好對應的關係。

config/config.default.js

'use strict';

module.exports = appInfo => {
  const config = exports = {};

  // use for cookie sign key, should change to your own and keep security
  config.keys = appInfo.name + '_{{keys}}';

  config.jwt = {
    secret: '123456', //自定義token的加密條件字串,可按各自的需求填寫
  };

  // Mysql
  config.sequelize = {
    dialect: 'mysql',
    host: 'localhost',
    port: 3306,
    database: 'myprojectdb',
    username: 'root',
    password: '123456',
    define: {
      //freezeTableName預設值為false,會自動在表名後加s
      freezeTableName: true,
      // timestamps預設值為true,會自動新增create_time和update_time
      timestamps: false
    }
  };

  // csrf 安全配置
  config.security = {
    csrf: {
      enable: false,
      ignoreJSON: true
    },
    // 允許訪問介面的白名單
    domainWhiteList: ['*'] // ['http://localhost:8080']
  };
  config.cors = {
    origin: '*',
    allowMethods: 'GET, HEAD, PUT, POST, DELETE, PATCH'
  };

  //........其他配置...............

  return config;
};

為了給前端提供Web API介面,我們需要為不同的業務物件提供路由入口,路由定義,統一在app/route.js檔案中定義。

app/route.js

 module.exports = app => {
    const { router, controller, jwt } = app; 

    router.get('/', controller.home.index);
    router.get('/news', controller.news.list);
    router.post('/login', controller.users.login);  //登入並生成Token
    router.resources('users', '/users', controller.users);
  };

以上我們users 是RESTful 的方式來定義路由, 我們提供了 app.router.resources('routerName', 'pathMatch', controller) 快速在一個路徑上生成 CRUD 路由結構。

類似RESTful定義

 router.resources('posts', '/api/posts', controller.posts);

我們只需要在 posts.js 裡面實現對應的函式就可以了。

 我這裡的users實現了上面部分的介面,以提供列表展示-L、建立-C、獲取-R、更新-U、刪除-D等操作。

app\controller\users.js

'use strict';

 const Controller = require('egg').Controller;

//控制器類入口
//實現路由幾個常規函式,包括列表及CRUD的操作
class UserController extends Controller {

  async index() { //展示列表資料-L
    const ctx = this.ctx;
    const query = {
      limit: ctx.helper.parseInt(ctx.query.limit),
      offset: ctx.helper.parseInt(ctx.query.offset),
    };

    var data = await ctx.service.user.list(query);
    var json = ctx.helper.json(data)
    ctx.body = json
  }

  async show() { //顯示某記錄具體的資料-R
    const ctx = this.ctx;
    ctx.body = await ctx.service.user.find(ctx.helper.parseInt(ctx.params.id));
  }

  async create() { //新增一個記錄-C
    const ctx = this.ctx;
    const user = await ctx.service.user.create(ctx.request.body);
    ctx.status = 201;
    ctx.body = user;
  }

  async update() { //更新指定的記錄-U
    const ctx = this.ctx;
    const id = ctx.helper.parseInt(ctx.params.id);
    const body = ctx.request.body;
    ctx.body = await ctx.service.user.update({
      id,
      updates: body
    });
  }

  async destroy() { //刪除指定的記錄-D
    const ctx = this.ctx;
    const id = ctx.helper.parseInt(ctx.params.id);
    await ctx.service.user.del(id);
    ctx.status = 200;
  }
}

module.exports = UserController;

這裡UserController 控制器沒有直接訪問資料庫,而是間接通過service物件進行操作資料庫的。service中的user.js程式碼如下所示。

app\service\user.js

'use strict';

const Service = require('egg').Service;

//服務類入口,用於封裝具體的資料庫訪問
class User extends Service {

  async login(usernameOrEmail, password) {
    var user = await this.ctx.model.User.findOne({ 
      where: {
          $or: [
            { username: usernameOrEmail },
            { emailaddress: usernameOrEmail }
          ]
      }
    });
    
    var success = false;
    var error = "";
    if(user) {
      success = true
    }

    return {
      success,
      error
    }
  }
  
  async list({ offset = 0, limit = 10 }) {
    return this.ctx.model.User.findAndCountAll({
      offset,
      limit,
      order: [[ 'creationtime', 'desc' ], [ 'id', 'desc' ]],
    });
  }

  async find(id) {
    const user = await this.ctx.model.User.findByPk(id);
    if (!user) {
      this.ctx.throw(404, 'user not found');
    }
    return user;
  }

  async create(user) {
    return this.ctx.model.User.create(user);
  }

  async update({ id, updates }) {
    const user = await this.ctx.model.User.findByPk(id);
    if (!user) {
      this.ctx.throw(404, 'user not found');
    }
    return user.update(updates);
  }

  async del(id) {
    const user = await this.ctx.model.User.findByPk(id);
    if (!user) {
      this.ctx.throw(404, 'user not found');
    }
    return user.destroy();
  }
}

module.exports = User;

而Service中,訪問資料庫主要通過 egg-sequelize 外掛中提供的 this.ctx.model.User 物件進行操作資料庫的

sequelize 是一個廣泛使用的 ORM 框架,它支援 MySQL、PostgreSQL、SQLite 和 MSSQL 等多個數據源。

app\model\user.js

'use strict';

module.exports = app => {
  const { STRING, INTEGER, DATE } = app.Sequelize;

  const User = app.model.define('abpusers', {
    id: { type: INTEGER, primaryKey: true, autoIncrement: true },
    name: STRING(64),
    username: STRING(64),  
    phonenumber: STRING(64),  
    creationtime: DATE,
    lastmodificationtime: DATE,
  });

  return User;
};

sequelize 定義了資料庫不同的型別,它的型別定義如下所示。

Sequelize.STRING                      // VARCHAR(255)
Sequelize.STRING(1234)                // VARCHAR(1234)
Sequelize.STRING.BINARY               // VARCHAR BINARY
Sequelize.TEXT                        // TEXT
Sequelize.TEXT('tiny')                // TINYTEXT
Sequelize.CITEXT                      // CITEXT      PostgreSQL and SQLite only.

Sequelize.INTEGER                     // INTEGER
Sequelize.BIGINT                      // BIGINT
Sequelize.BIGINT(11)                  // BIGINT(11)

Sequelize.FLOAT                       // FLOAT
Sequelize.FLOAT(11)                   // FLOAT(11)
Sequelize.FLOAT(11, 10)               // FLOAT(11,10)

Sequelize.REAL                        // REAL        PostgreSQL only.
Sequelize.REAL(11)                    // REAL(11)    PostgreSQL only.
Sequelize.REAL(11, 12)                // REAL(11,12) PostgreSQL only.

Sequelize.DOUBLE                      // DOUBLE
Sequelize.DOUBLE(11)                  // DOUBLE(11)
Sequelize.DOUBLE(11, 10)              // DOUBLE(11,10)

Sequelize.DECIMAL                     // DECIMAL
Sequelize.DECIMAL(10, 2)              // DECIMAL(10,2)

Sequelize.DATE                        // DATETIME for mysql / sqlite, TIMESTAMP WITH TIME ZONE for postgres
Sequelize.DATE(6)                     // DATETIME(6) for mysql 5.6.4+. Fractional seconds support with up to 6 digits of precision
Sequelize.DATEONLY                    // DATE without time.
Sequelize.BOOLEAN                     // TINYINT(1)

Sequelize.ENUM('value 1', 'value 2')  // An ENUM with allowed values 'value 1' and 'value 2'
Sequelize.ARRAY(Sequelize.TEXT)       // Defines an array. PostgreSQL only.
Sequelize.ARRAY(Sequelize.ENUM)       // Defines an array of ENUM. PostgreSQL only.

Sequelize.JSON                        // JSON column. PostgreSQL, SQLite and MySQL only.
Sequelize.JSONB                       // JSONB column. PostgreSQL only.

Sequelize.BLOB                        // BLOB (bytea for PostgreSQL)
Sequelize.BLOB('tiny')                // TINYBLOB (bytea for PostgreSQL. Other options are medium and long)

Sequelize.UUID                        // UUID datatype for PostgreSQL and SQLite, CHAR(36) BINARY for MySQL (use defaultValue: Sequelize.UUIDV1 or Sequelize.UUIDV4 to make sequelize generate the ids automatically)

Sequelize.CIDR                        // CIDR datatype for PostgreSQL
Sequelize.INET                        // INET datatype for PostgreSQL
Sequelize.MACADDR                     // MACADDR datatype for PostgreSQL

Sequelize.RANGE(Sequelize.INTEGER)    // Defines int4range range. PostgreSQL only.
Sequelize.RANGE(Sequelize.BIGINT)     // Defined int8range range. PostgreSQL only.
Sequelize.RANGE(Sequelize.DATE)       // Defines tstzrange range. PostgreSQL only.
Sequelize.RANGE(Sequelize.DATEONLY)   // Defines daterange range. PostgreSQL only.
Sequelize.RANGE(Sequelize.DECIMAL)    // Defines numrange range. PostgreSQL only.

Sequelize.ARRAY(Sequelize.RANGE(Sequelize.DATE)) // Defines array of tstzrange ranges. PostgreSQL only.

Sequelize.GEOMETRY                    // Spatial column.  PostgreSQL (with PostGIS) or MySQL only.
Sequelize.GEOMETRY('POINT')           // Spatial column with geometry type. PostgreSQL (with PostGIS) or MySQL only.
Sequelize.GEOMETRY('POINT', 4326)     // Spatial column with geometry type and SRID.  PostgreSQL (with PostGIS) or MySQL only.

關於它的介面,可以參考下文件https://itbilu.com/nodejs/npm/sequelize-docs-v5.html 瞭解下。

另外,我們可以在app\extend\helper.js中定義一些常規的輔助函式,方便在控制器或者service物件中使用。

app\extend\helper.js

'use strict';
const moment = require('moment');

module.exports = {
  json(data, code, msg, addition) {
    return Object.assign({
      result: code ? 'fail' : 'success',
      code: code || 0,
      message: msg,
      data,
    }, addition);
  },
  parseInt(string) {
    if (typeof string === 'number') return string;
    if (!string) return string;
    return parseInt(string) || 0;
  },

  changeTime(time) {
    return moment(time * 1000).format('YYYY-MM-DD HH:mm:ss');
  },
  relativeTime(time) {
    return moment(new Date(time * 1000)).fromNow()
  },

最後,我們使用npm run dev跑專案

測試下我們使用者列表部分的處理。

 其他CRUD介面,可以結合C#程式碼進行客戶端的測試,也可以在一個新建的Vue+Element前端專案中進行axios的呼叫,獲取對應的JSON進行測試。

在使用egg.js開發的時候,總體還是很方便,不過就是有時候一些拼寫錯誤,或者一些配置原因,控制檯 提示資訊不是很明確,需要自己掌握各種排錯的經驗才行。