使用egg.js開發後端API介面系統
什麼是Egg.js
Egg.js 為企業級框架和應用而生,我們希望由 Egg.js 孕育出更多上層框架,幫助開發團隊和開發人員降低開發和維護成本。詳細的了解可以參考Egg.js的官網://eggjs.org/zh-cn/intro/ 。
Egg.js 奉行『約定優於配置』,按照一套統一的約定進行應用開發,Egg 有很高的擴展性,可以按照團隊的約定訂製框架,團隊內部採用這種方式可以減少開發人員的學習成本。
可以理解Egg.js是一個Node框架,同時它也是基於Koa框架基礎上的框架,我們大概了解一下它的前身和主要特點即可。
它的特點有:
本篇隨筆不是細說Egg.js 的詳細內容,畢竟官網介紹還是比較清晰的,我們主要說使用它來做一個後端的API介面系統,後端肯定需要對資料庫進行各種操作,用一個JS的方式來訪問資料庫,利用egg-sequelize插件,創建和資料庫表進行綁定的模型進行操作,還是比較新鮮的,用了會發現確實很方便。用Egg.js來開發後端系統,相當於用前端的語言、做法,來開發後端系統了(雖然Egg.js 也可以用來做前端)。

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 是一個日期處理插件,可以處理各種日期格式、轉換的一個插件庫。
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。
'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: ['*'] // ['//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.
關於它的介面,可以參考下文檔//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開發的時候,總體還是很方便,不過就是有時候一些拼寫錯誤,或者一些配置原因,控制台 提示資訊不是很明確,需要自己掌握各種排錯的經驗才行。