How To Write A Plugin

Use svrx-create-plugin to help you create plugins more easily

First Plugin

Let's create our first plugin —— svrx-plugin-hello-world

File Structure

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

Only package.json and index.js are required

  • package.json

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

Only two fields are required by package.json

  • name: package name must be a string beginning with svrx-plugin, to help svrx find it in npm.

  • engines.svrx: Define the runnable svrx version of this plugin,svrx will automatically load the latest matching plugin engines.svrx can be a valid semver,like 0.1.x or ~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`);
    }
  }
};

Where

  1. configSchema Plugin param definition based on JSON Schema ,checkout JSON Schema for more detail. Here we just set a user field, it is a string

  2. assets: client resource setting,they will be automatically injected into the page

    • style: css resource injection

    • script: script resource injection

      All resources will be merged into a single file.

  3. hook.onCreate: a function invoke when plugin been creating, we can control it by injected service in this example, we only use two services

    • logger: logger service

    • config: config service

Check out setion service to find out other services that available in hook.onCreate

- client.js

There is a global variable named svrx will be injected into all pages, it has some built-in services

svrx is only accessible inside the plugin script, don't worry about global pollution

const { config } = svrx;

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

You will see Hello svrx from browser in console panel

Unlike in server side, config in client is passed through websocket, so api is async, and return a promise

svrx also provide other client services, please check out client api for help

publish && running

There'll be a failure when trying to publish it by npm publish. Because svrx-plugin-hello-world has been published by official team

So we skip this step and try the plugin directly

svrx -p hello-world

Check the terminal and browser console log, we will find Hello svrx from browser, which means plugin has worked successfully.

You can also run it in programmatical way

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

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

Further reading: How to test plugin?

Server Side Service

You can use service in two places

  • hooks.onCreate

module.exports = {
  hooks: {
    async onCreate({
      middleware,
      injector,
      events,
      router,
      config,
      logger,
      io
    }) {
      // use service here
    }
  }
};
  • plugin event

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

We will explain these 7 services in turn

middleware

Middleware is used for adding koa-style middleware to implement backend logic injection

- middleware.add(name, definition)

Adding koa-style middleware

Usage

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

Param

  • name [String]: A unique middleware name

    in debug mode, it can be used to track the middleware call process

  • definition.priority [Number]: default is 10. Svrx will assemble the middleware according to the priority from high to low, that is, the request will be passed to the high priority plugin first.

  • definition.onRoute [Function]: A koa-style middleware .If definition is a function, it will automatically become definition.onRoute

- middleware.del(name)

Delete a middleware with the specified name

Usage

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

Param

  • name: middleware name

injector

injector is used for rewriting the response and inject client resources.

- injector.add(type, resource)

Add a resource for injection , only js and css has been supported.

The rule as follow.

  • The style will be merged into /svrx/svrx-client.css and injected before the closing head tag

  • The script will be merged into /svrx/svrx-client.js and injected before the closing body tag

Usage

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

The content will be merged into bundle script

Param

  • type: Only support script and style

  • resource.content [String]: resource content

  • resource.filename [String]: the path of resource file, must be a absolute path

Content has a higher priority than filename, so you can take one of them.

- injector.replace(pattern, replacement)

injector.replace will transform response body with some or all matches of a pattern replaced by a replacement.

Usage

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

The above example replace all svrx with server-x

Param

  • pattern [String|RegExp]

  • replacement [String|Function]

The usage of injector.replace is exactly the same as String.prototype.replace

resource injection is based upon injector.replace

events

Built-in event listener, support async&sorted emitter, , which can call the listen function in turn, and can terminate the event delivery at any time.

- 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]: event name

  • fn(payload, ctrl): callback that has two params

    • payload [String]: event data that pass through emit

    • ctrl [Object]: control object, call ctrl.stop() to stop 'sorted emit'

If the callback returns a Promise (such as async function), it will be treated as an asynchronous watcher.

- 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]: event name

  • payload: event data

  • sorted [Boolean]: default is false, whether to pass events serially

Return

Promise

- events.off(name, handler)

Remove event watcher

Usage

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

builtin events

  • plugin: triggered after plugin building.

  • file:change: triggered when any file changes

  • ready: triggered when server starts, if you need to handle logic after server startup (such as getting the server port), you can register this event

config

Config service used to modify or query the options passed by user;

- config.get(path)

Get the config of this plugin.

Config is based on immutable data , you must always use config.get to ensure getting the latest config.

Usage

config.get('user'); // get the user param  belong to this plugin
config.get('user.name'); // get the user.name param  belong to this plugin

if you need to get global config,just add prefix $.

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

Param

  • field: field path,deep getter must be separated by ., such as user.name

Return

The value of the field

- config.set(field, value)

Modify the config

Usage

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

Param

  • field: field path,deep setter must be separated by ., such as user.name

  • value: field value

- config.watch(field, handler)

Listening for configuration changes, change digest will be triggered after the set, del, splice method calls

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.watch('a.b', (evt)=>{
    console.log('a.b has been changed')
})
config.set('a.b.c', 'hello');

Param

  • field: field path,deep watcher must be separated by ., such as user.name

  • handler(evt): watch handler

    • evt.affect(field) [Function]: detect whether specific field has been changed

- config.del(field)

Remove some field

Usage

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

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

The Array.prototype.slice

Except for field,params are identical to Array.prototype.splice

Example

config.set('a.b.c', [1, 2, 3]);
config.splice('a.b.c', 1, 1);
config.get('a.b.c'); // => [1,3]

router

Extending Routing DSL

- router.route(register)

Register route,as same as 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)

Register an action , like proxy or json

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 name

  • builder(payload)

    • payload: payload that passed to action,like 'svrx' in above example

    • Return: builder must return a koa style middleware

- router.load(filename)

Load routing file manually ,Same as options --route

Also support hot reloading

Usage

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

Param

  • filename: absolute path for routing file

Return

Promise

logger

Logger module, who's level can be controlled by logger.level (default is warn)

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

Or cli way

svrx --logger.level error

Above example will output log that more than warn, such as notify, error

logger[level](msg)

Svrx provides multiple levels of logging: silent, notify, error, warn (default), info, debug

Usage

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

logger.log is an alias for logger.notify

io

io is used for the communication between server and client. Please check it out in client-side io

- io.on( type, handler )

Listening for client messages (send by client side io.emit)

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

Param

  • type: message type

  • handler(payload): handler for message

    • payload: message data

- io.emit(type, payload)

Send message to client

Usage

Server side

io.emit('hello', 1);

Client side

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

Param

  • type: message type

  • payload: message data

Message payload must be serializable because they are transmitted over the network

- io.off(type[, handler])

Remove the message watcher

Usage

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

- io.register(name, handler)

Register io service, which can be invoked by io.call in client and server.

Usage

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

Param

  • name [String]: service name used by io.call

  • handler: a Function return Promise, implement service logic

- io.call(name, payload)

Invoke registered service

Usage

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

Param

  • name [String]: service name

  • payload [Any]: service payload will passed to service handler

Return

Promise

Client API

The client APIs are uniformly exposed through global variable svrx

const { io, events, config } = svrx;

io

Responsible for communicating with the server

- io.on(type, handler)

Listening server-side message

Usage

Server side

io.emit('hello', 1);

Client side

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

Note that the io.emit() in server-side is a broadcast and all pages will receive a message for server.

Param

  • type: message type

  • handler(payload): message handler

    • payload: message payload passed by io.emit

- io.emit(type, payload)

Send client message to server side

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: message type

  • payload: message data passed to message handler

payload must be serializable because it is transmitted over the network.

- io.off(type[, handler])

Remove io watcher

Usage

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

- io.call(name, payload)

io.call on the client side is exactly the same as the server, but make sure the payload is serializable** because it will be transmitted over the network

Usage

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

Param

  • name [String]: service name

  • payload [Any]: service payload

Return

Promise

events

This part is exactly the same as server events, no retelling

Usage

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

The difference between events and io is that io is used for the communication between the server and the client, but events is a single-ended communication.

config

config in client is almost identical to the server, the only difference is: the client version is asynchronous (because of network communication)

- 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
});

if you need to get global config,just add prefix $.

- config.setconfig.spliceconfig.del

The above three methods are the same as get, which is consistent with the server, but the return value becomes Promise.

Usage

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

config schema

Svrx config schema is based on 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 Details

  • type [String]: JSON-Schema field type ,can be an array,string,number,boolean,object or null

  • default [Any]: default value

  • required [Boolean]: whether it is required, default is false

  • properties [Object]: child fields

  • ui: svrx extension ,whether show the config in svrx-ui

  • anyOf: Choose one of the items, such as

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

    For further understanding, please refer to Official Documentation

How to test plugin?

It is not recommended that you publish the test plugin to npm. You can do local testing in the following ways.

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

After specifying the path parameter, svrx loads the local package instead of installing it from npm

More easier plugin development

Use Official svrx-create-plugin to help you create plugins more easily

Last updated