如何写一个插件

使用svrx-create-plugin帮助你更容易的开发插件

第一个 svrx 插件

让我们实现第一个插件 —— svrx-plugin-hello-world, 它用来在终端和浏览器端分别打印用户欢迎语

文件结构

└── svrx-plugin-hello-world
  ├── client.js
  ├── index.js
  └── package.json

*其实只有package.jsonindex.js*是必须的

以下分别做下介绍

  • package.json

{
  "name" : "svrx-plugin-hello-world",
  "engines": {
    "svrx" : "0.0.x"
  }
  ...略..
}

package.json中只有 2 个字段是 svrx 依赖的

  • name: 包名要求必须是 svrx-plugin 开头,这是为了方便使用 npm 服务来进行插件查找

  • engines.svrx: 定义此插件的可运行 svrx 版本,svrx 会自动加载最新的匹配插件, engines.svrx 可以是一个 semver,如0.1.x~0.1

- index.js

module.exports = {
  configSchema: {
    user: {
      type: 'string',
      default: 'svrx',
      description: 'username for hello world'
    }
  },

  assets: {
    script: ['./client.js']
  },

  hooks: {
    async onCreate({ logger, config }) {
      logger.log(`Hello ${config.get('user')} from server`);
    }
  }
};

其中

  1. configSchema 插件参数定义,请参考参数定义了解更多,这里我们定义了一个默认为'svrx'的字符串类型的参数 user

  2. assets: 前端资源配置,他们会被自动注入到前端脚本中

    • style: css 脚本注入,默认注入 html 头部,本例没有使用

    • script: script 脚本注入,默认注入 html 尾部

    注入前端资源都会被合并成一个资源

  3. hook.onCreate: 插件创建的钩子,在这里我们可以通过注入的服务组件来扩展插件,这里仅介绍使用到的服务

    • logger: 日志服务

    • config: 获取用户参数的组件

      本例做的是在终端打印关于 user 参数的欢迎语

hook.onCreate 实际上还接受多种组件服务,具体请参考插件服务组件

- client.js

前端注入脚本中有一个**「全局变量 svrx」**, 挂载了一些 svrx 在前端的内置服务。

这个全局 svrx 仅在插件脚本内部可以访问,不用担心全局污染

const { config } = svrx;

config.get('user').then(user => {
  console.log(`Hello ${user} from browser`);
});

这里我们仅仅是做了一个用户相关控制台 log 输出

与服务端的 config 组件不一样,这里的 config 是通过 websocket 传递的,所以接口返回的是 promise

svrx 其实还暴露了其它前端服务,具体请参考前端 API

'发布' && 运行

在根目录,我们直接运行npm publish .尝试发布,会发现发布失败了,因为hello-world插件已经被官方发布了

所以我们跳过这一步,直接运行

svrx -p hello-world

检查下浏览器端和命令行终端将可以看到 'Hello svrx from browser' 的类似日志,说明插件已经生效

也可以通过脚本的方式来启动

const svrx = require('@svrx/svrx');

svrx({
  plugins: ['hello-world']
}).start();

进一步阅读: 如何本地测试?

插件服务 API

所有插件服务都可以在两个地方被注入

  • 插件定义中的hooks.onCreate钩子

module.exports = {
  hooks: {
    async onCreate({
      middleware,
      injector,
      events,
      router,
      config,
      logger,
      io
    }) {
      // use component here
    }
  }
};
  • svrx 实例的plugin事件回调,此事件会在所有插件组装完毕后执行

svrx().on('plugin', async ({ logger, io }) => {
  // use component here
});

接下来,我们依次说明这 7 个组件

middleware

middleware 负责添加满足 koa 风格的中间件 完成后端逻辑的注入

- middleware.add(name, definition)

增加一个中间件

Usage

middleware.add('hello-world-middleware', {
  priority: 100,
  async onRoute(ctx, next) {}
});

Param

  • name [String]: 定义一个唯一的中间件名

    debug 模式下根据此命名可以 track 请求响应的管线

  • definition.priority [Number]: 默认为 10. svrx 会根据 priority 由高到低的组装中间件,即请求会先经过高优先级的插件

  • definition.onRoute [Function]: 满足 koa 规范的中间件定义,如果 definition 是一个函数,则会自动成为 definition.onRoute

- middleware.del(name)

删除一个中间件

Usage

middleware.del('hello-world-middleware');

Param

  • name: 传入add的名称

injector

injector用来改写响应流或注入前端资源

- injector.add(type, definition)

增加一种注入资源, 目前支持原生的 js 和 css 注入,他们注入规则如下

  • 样式会合并到/svrx/svrx-client.css下,并注入到 html 的head闭合标签前

  • 脚本会合并到/svrx/svrx-client.js,并注入到 html 的body闭合标签前

Usage

injector.add('script', {
  content: `
    console.log('hello world')
  `
});

上例会往/svrx/svrx-client.js 注入 content 字段中的脚本内容

Param

  • type: 目前仅支持script以及style

  • definition.content [String]: 资源的内容

  • definition.filename [String]: 资源文件,必须是绝对地址

content 的优先级高于 filename,二者取其一即可

- injector.replace(pattern, replacement)

自定义 html 的替换规则

Usage

injector.replace(/svrx/g, 'server-x');

上例将 html 中的 svrx 替换为 server-x

Param

  • pattern [String|RegExp]

  • replacement [String|Function]

injector.replace的使用与String.prototype.replace完全一致 资源注入就是通过injector.replace实现的

events

内置事件监听器,支持 async sorted emitter,即可以依次调用监听函数,并可在任意一个事件监听中终止事件传递

- events.on(type, fn)

Usage

events.on('hello', async payload => {});

events.on('hello', async (payload, ctrl) => {
  ctrl.stop(); // stop events emit, only works in sorted emit mode
});

Param

  • type [String]: 事件名

  • fn(payload, ctrl): 事件回调,它包含 2 个入参

    • payload [String]: 事件参数, 通过 emit 传入

    • ctrl [Object]: 控制器, 调用ctrl.stop()可终止 sorted emit

如果回调返回一个Promise(比如 async function),视为一个异步回调

- events.emit(type, param, sorted)

Usage

// sorted emit, handler will be called one by one
events.emit('hello', { param1: 'world' }, true).then(() => {
  console.log('emit is done');
});
// parallel emit
events.emit('hello', { param1: 'world' }).then(() => {
  console.log('emit is done');
});

Param

  • type [String]: 事件名

  • payload: 事件参数,被on中注册的回调函数接收

  • sorted [Boolean]: 默认false, 是否串行阻塞的触发事件

Return

Promise

- events.off(name, handler)

Usage

events.off('hello'); // remove all hello's handler
events.off('hello', handler); // remove specific handler

内置事件

  • plugin: 当插件准备完毕后触发

  • file:change: 当文件发生变更时被触发

  • ready: 当服务启动时触发,如果你需要在服务启动处理部分逻辑(如获取服务端口),请注册这个事件

config

插件配置管理

- config.get(path)

获取一个插件配置

config 内部维护的数据结构为不变数据,对于配置有实时性要求的,必须使用 config.get 来取值,确保获得最新配置项目

Usage

config.get('user'); // get the user param  of plugin
config.get('user.name'); // get the user.name param  of plugin

你也可以获取全局参数,只需要加$.前缀

config.get('$.port'); // get the server's port
config.get('$.root'); // get the svrx working directory

Param

  • field: 配置名,深层配置用.分割,比如user.name

Return

配置值

- config.set(field, value)

设置配置项

Usage

config.set('a.b.c', 'hello'); // deep set
config.get('a'); // => { b: { c: 'hello' } }

Param

  • field: 配置名,深层配置用.分割,比如user.name

  • value 配置值

- config.watch([field, ]handler)

监听配置变化, 配置变化检查会在setdelsplice方法后被触发

config.watch( (evt)=>{
  console.log(evt.affect('a.b.c')) => true
  console.log(evt.affect('a')) // => true
  console.log(evt.affect('a.c')) // => false
})
config.set('a.b.c', 'hello');

Param

  • field: 字段名, field是可选的,如不传入则所有 config 变更都会触发

  • handler(evt): 监听回调

    • evt.affect [Function]: 判断本次变更是否影响某字段

- config.del(field)

删除某个配置项

Usage

config.del('a.b.c');
config.get('a.b.c'); //=> undefined

Param

配置名

- config.splice(field, start[, delCount[, items...])

数组 splice 的 config 版本

除了field外,其他参数与 Array.prototype.splice 一致

router

从插件层面扩展或注册Routing 模块

- router.route(register)

快捷注册路由,与Routing DSL一致

Usage

const {route} = router;

route(({all, get, post})=>{

  all('/blog').to.send('Hi, Blog')
  get('/user').to.send('Hi, user')
  post('/user').to.send({code: 200, data: 'Success!'})

})

Param

  • register({...methods}): 注册回调

- router.action(name, builder)

注册一个与 proxyjson 类似的 action

Usage

const { action, route } = router;
action('hello', user => ctx => {
  ctx.body = `hello ${user}`;
});
route(({ all }) => {
  all('/blog').hello('svrx'); //=> response 'hello svrx'
});

Param

  • name [String]: action 名,即后续的调用方法名

  • builder(payload)

    • payload: 即传入 action 方法的参数,如上栗的'svrx'

    • Return: builder 返回标准的koa 中间件

- router.load(filename)

手动加载一个 route 文件,与启动参数 route 一致

加载的文件同样支持 hot reloading

Usage

await router.load('/path/to/route.md');

Param

  • filename: 路由文件的绝对路径

Return

Promise

logger

日志模块,它的分级可以通过logger.level来控制(默认为warn)

svrx({
  logger: {
    level: 'error'
  }
});

或从 cli 端

svrx --logger.level error

上例将会输出warn以上的日志,如notifyerror

logger[level](msg)

svrx 提供了多种级别的日志,分别是silent, notify, error , warn(默认日志分级), info, debug

Usage

logger.notify('notify'); // show `notify`
logger.error('error'); // show `error` and `notify`
logger.warn('warn'); // show `warn`、`error` and `notify`

logger.notify 由于会非常常用,所以它有一个 alias logger.log

io

io 负责插件后端与前端的通信, 请结合 client 端的 io 查看

- io.on( type, handler )

监听浏览器端发送消息(即通过浏览器端io.emit发送)

io.on('hello', payload => {
  console.log(payload); // =>1
});

Param

  • type: 事件名

  • handler(payload): 事件回调

    • payload: 事件参数

- io.emit(type, payload)

发送 io 事件到客户端

Usage

server side

io.emit('hello', 1);

client side

const { io } = svrx;
io.on('hello', payload => {
  console.log(payload); //=>1
});

Param

  • type: 事件名

  • payload: 事件参数

注意事件参数必须是可序列化的,因为要通过网络传输

- io.off(type[, handler])

解除监听

Usage

io.off('hello'); //=> remove all hello handlers
io.off('hello', handler); //=> remove specific handler

- io.register(name, handler)

注册一个 io 服务, 它可以在客户端或服务端以io.call的方式调用

Usage

io.register('hello.world', async payload => {
  return `Hello ${payload}`;
});

Param

  • name [String]: 服务名,call 中会被使用

  • handler: 服务逻辑实现,异步请返回 Promise 或使用 async/await

- io.call(name, payload)

调用注册的服务

Usage

上例如下调用会返回 'Hello svrx'

io.call('hello.world', 'svrx').then(data => {
  console.log(data); // => Hello svrx
});

Param

  • name [String]: 服务名

  • payload [Any]: 服务入参

Return

Promise

客户端 API

客户端 API 统一通过 svrx 全局变量暴露,如下例

const { io, events, config } = svrx;

以下依次说明

io

通信模块,负责与服务端通信

- io.on(type, handler)

监听服务端 io 事件

Usage

server side

io.emit('hello', 1);

client side

const { io } = svrx;
io.on('hello', payload => {
  console.log(payload); //=>1
});

注意后端 io.emit 属于广播,所有前端页面都会收到信息

Param

  • type: 事件名

  • handler(payload): 事件回调

    • payload: 事件参数

- io.emit(type, payload)

发送 io 事件到服务端

Usage

client side

const { io } = svrx;
io.emit('hello', 1);

server side

{
  hooks: {
    async onCreate({io}){
      io.on('hello', payload=>{
        console.log(payload) // =>1
      })
    }
  }
}

Param

  • type: 事件名

  • payload: 事件参数

注意事件参数必须是可序列化的,因为要通过网络传输

- io.off(type[, handler])

解除监听

Usage

io.off('hello'); //=> remove all hello handlers
io.off('hello', handler); //=> remove specific handler

- io.call(name, payload)

在 client 端的 io.call 与服务端完全一致,但要确保 payload 是可序列化的,因为会经过网络传输

Usage

io.call('hello.world', 'svrx').then(data => {
  console.log(data);
});

Param

  • name [String]: 服务名

  • payload [Any]: 服务入参

Return

Promise

events

浏览器端内部的事件发射器,这部分和服务端 events完全一致,不做赘述

Usage

const { events } = svrx;
events.on('type', handler);
events.emit('type', 1);
events.off('type');

events 与 io 的不同在于 io 是 服务端与客户端的通信,而 events 是单端的事件触发器

config

客户端的config模块与服务端几乎一致,唯一区别是从同步接口变成了 Promise 化的异步接口(因为 socket 的网络通信)

- config.get(field)

Usage

config.get('$.port').then(port => {
  // get the server port
});

config.get('user').then(user => {
  //get the param belongs to current plugin
});

注意获取的参数是属于脚本的,如果获取全局请加$.前缀

- config.setconfig.spliceconfig.del

上述三个方法和get一样,与服务端表现一致,不过返回值变为 Promise

Usage

config.set('a.b', 1).then(() => {
  config.get('a.b').then(ab => {
    console.log(ab); // => 1
  });
});

参数定义

svrx 支持一种基于 json-schema 扩展的参数定义,以内部定义的参数为例

Usage

module.exports = {
  root: {
    type: 'string',
    default: process.cwd(),
    description: 'where to start svrx',
    ui: false
  },
  route: {
    description: 'the path of routing config file',
    anyOf: [{ type: 'string' }, { type: 'array' }]
  },
  logger: {
    description: 'global logger setting',
    type: 'object',
    properties: {
      level: {
        type: 'string',
        default: 'error',
        description:
          "set log level, predefined values: 'silent','notify','error','warn', 'debug'"
      }
    }
  }
};

Field 详解

  • type [String]: JSON-Schema 允许的字段类型 ,可以是arraystringnumberbooleanobjectnull

  • default [Any]: 默认值

  • required [Boolean]: 是否是必须的, 默认是false

  • properties [Object]: 定义属性,每层属性又是一层 schema 定义

  • ui: svrx 的扩展字段,是否要在 svrx-ui 中被展示

  • anyOf: 属性任选其一,如

    route: {
      description: 'the path of routing config file',
      anyOf: [{ type: 'string' }, { type: 'array' }],
    },

    进一步的 JSON Schema 定义请参考官方文档

如何测试?

不建议大家发布测试插件到 npm, 可以通过以下方式来进行本地测试

svrx({
  plugins: [
    {
      name: 'hello-world',
      path: '/path/to/svrx-plugin-hello-world'
    }
  ]
}).start();

指定 path 参数后,svrx 会加载本地包,而不是从 npm 中获取

更容易的插件开发 —— svrx-create-plugin

svrx 官方提供的脚手架帮助你更容易的开发和发布你的插件

Last updated