Egg.js自定义插件(xml解析器)

在学习EggJs企业级web开发框架时,Egg 奉行『约定优于配置』,按照一套统一的约定进行应用开发,Egg 的插件机制有很高的可扩展性,一个插件只做一件事(比如 Nunjucks 模板封装成了 egg-view-nunjucks、MySQL 数据库封装成了 egg-mysql)。Egg 通过框架聚合这些插件,并根据自己的业务场景定制配置,这样应用的开发成本就变得很低。

文档分析

自己需要开发eggjs的插件就需要根据官方提供的文档进行学习,按照统一的开发约定进行开发。是不是看完官方文档还是一头雾水,不知道从何下手,或者你在网上搜索的内容都是一模一样的。

官方文档关键信息

  1. 插件没有独立的 router 和 controller。这主要出于几点考虑:

    • 路由一般和应用强绑定的,不具备通用性。
    • 一个应用可能依赖很多个插件,如果插件支持路由可能导致路由冲突。
    • 如果确实有统一路由的需求,可以考虑在插件里通过中间件来实现。
  2. 插件需要在 package.json 中的 eggPlugin 节点指定插件特有的信息:

    • {String} name - 插件名(必须配置),具有唯一性,配置依赖关系时会指定依赖插件的 name。
    • {Array} dependencies - 当前插件强依赖的插件列表(如果依赖的插件没找到,应用启动失败)。
    • {Array} optionalDependencies - 当前插件的可选依赖插件列表(如果依赖的插件未开启,只会 warning,不会影响应用启动)。
    • {Array} env - 只有在指定运行环境才能开启,具体有哪些环境可以参考运行环境。此配置是可选的,一般情况下都不需要配置。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    	{
    "name": "egg-rpc",
    "eggPlugin": {
    "name": "rpc",
    "dependencies": [ "registry" ],
    "optionalDependencies": [ "vip" ],
    "env": [ "local", "test", "unittest", "prod" ]
    }
    }
  3. 插件没有 plugin.js

    • eggPlugin.dependencies 只是用于声明依赖关系,而不是引入插件或开启插件。
  4. 包括了 pathpackage 两种加载模式

    • config/plugin.js 中通过 path 来挂载插件。
    1
    2
    3
    4
    5
    6
    // config/plugin.js
    const path = require('path');
    exports.ua = {
    enable: true,
    path.join(__dirname,'../lib/plugin/egg-xml-par')
    };
    • package.json 中声明对 egg-ua 的依赖。config/plugin.js 中修改依赖声明为 package 方式。
    1
    2
    3
    4
    5
    // config/plugin.js
    exports.ua = {
    enable: true,
    package: 'egg-xml-par',
    };

搭建简单的开发环境

安装eggjs基础脚手架

我们推荐直接使用脚手架,只需几条简单指令,即可快速生成项目(npm >=6.1.0):

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

启动项目进行测试:

1
2
$ npm run dev
$ open http://localhost:7001

新建插件环境

  1. 新建插件文件夹lib (插件所有的配置,代码都在这里面)

image.png

  1. 我们是做一个解析xml请求的中间件插件,所有需要搭建下图的结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
. lib
├── plugin
│ ├── egg-xml-par (插件npm名)
| ├── package.json (插件名称等配置)
| ├── app.js (中间件配置)
|
|────── app
| ├── middleware
| ├── xml-par.js(插件处理代码)
|
|────── config
├── config.default.js(插件需要的配置文件)


image.png

配置插件基础信息

Egg 是通过 eggPlugin.name 来定义插件名的,只在应用或框架具备唯一性

1
2
3
4
5
6
7
8
//lib/plugin/egg-xml-par/package.json
{
"name": "egg-xml-par",
"eggPlugin": {
"name": "XmlPar",
"version": "1.0.0"
}
}

将插件加入中间件

app.js 中将中间件插入到合适的位置(例如:下面将 xmlPar中间件放到 bodyParser 之前)

1
2
3
4
5
6
7
8
9
//lib/plugin/egg-xml-par/app.js
const assert = require('assert');

module.exports = app => {
// 将 xmlPar 中间件放到 bodyParser 之前
const index = app.config.coreMiddleware.indexOf('bodyParser');
assert(index >= 0, 'bodyParser 中间件必须存在');
app.config.coreMiddleware.splice(index, 0, 'xmlPar');
};

写插件核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//lib/plugin/egg-xml-par/app/middleware/xml-par.js
'use strict';

const parse = require('co-body');
const {xml2js} = require('xml-js');

module.exports = options => {
// 获取插件配置 //lib/plugin/egg-xml-par/config/config.default.js
const xmlTypes = options.xmlTypes;

return async function xmlPar(ctx, next){
try {
const res = await parseBody(ctx);
ctx.request.body = 'parsed' in res ? res.parsed : {};
if(ctx.request.rawBody === undefined){
ctx.request.rawBody = res.raw
}
} catch (error) {
ctx.logger.warn(error)
}

await next();
}

async function parseBody(ctx){
if(ctx.request.is(xmlTypes)){
var body = await parse.text(ctx.request);
ctx.logger.info(body);
const xmlContent = xml2js(body);
return {
raw : body,
parsed : xmlContent.elements[0]
}
}
return {}
}
};
1
2
3
4
5
6
7
8
//lib/plugin/egg-xml-par/config/config.default.js
exports.xmlPar = {
xmlTypes:[
'text/x-xml',
'application/xml',
'application/x-xml'
]
}

Egg.js使用自定义插件

  • packagenpm 方式引入,也是最常见的引入方式
  • path 是绝对路径引入,如应用内部抽了一个插件,但还没达到开源发布独立 npm 的阶段,或者是应用自己覆盖了框架的一些插件
  • 关于这两种方式的使用场景,可以参见渐进式开发
1
2
3
4
5
6
// config/plugin.js
const path = require('path');
exports.mysql = {
enable: true,
path: path.join(__dirname,'../lib/plugin/egg-xml-par')
};

取消egg.js自带解析器与关闭csrf验证

1
2
3
4
5
6
7
8
9
10
// config/config.default.js 
config.security = {
csrf: {
enable: false
}
}

config.bodyParser = {
enable: false
}

完整项目结构

image.png

运行测试

因为接口定义未post,为了方便这里需要关闭csrf验证

1
2
3
4
5
6
// config/config.default.js 
config.security = {
csrf: {
enable: false
}
}

未使用插件

发现返回结果没有显示

image.png

使用插件测试

记得要关闭默认的解析

1
2
3
4
// config/config.default.js 
config.bodyParser = {
enable: false
}

image.png

其他说明

开发其他插件原理类似,希望看完该内容你有点收获