svrx-docs

如何写一个插件

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

第一个 svrx 插件

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

文件结构

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

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

以下分别做下介绍

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

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

- 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

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

module.exports = {
  hooks: {
    async onCreate({
      middleware,
      injector,
      events,
      router,
      config,
      logger,
      io
    }) {
      // use component here
    }
  }
};
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

- middleware.del(name)

删除一个中间件

Usage

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

Param

injector

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

- injector.add(type, definition)

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

Usage

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

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

Param

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

- injector.replace(pattern, replacement)

自定义 html 的替换规则

Usage

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

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

Param

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

如果回调返回一个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

Return

Promise

- events.off(name, handler)

Usage

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

内置事件

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

Return

配置值

- config.set(field, value)

设置配置项

Usage

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

Param

- 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

- 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

- 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

- router.load(filename)

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

加载的文件同样支持 hot reloading

Usage

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

Param

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

- 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

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

- 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

- io.call(name, payload)

调用注册的服务

Usage

上例如下调用会返回 ‘Hello svrx’

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

Param

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

- 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

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

- 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

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 详解

如何测试?

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

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

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

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

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