「这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战」
支持插件机制的框架或库有很多,如 webpack、rollup、vue、markdown-it、koa、redux 等,今天来研究一下这些框架是如何支持 plugin 的。
koa 的中间件
koa 是一个轻量的node服务端框架,源码见 koajs/koa仓库。
它自带的功能是比较弱的,但是你可以通过添加各种中间件的方式如 koa-json,koa-router,koa-multer等扩展它作为后端的功能。
我们给 koa 应用中间件的方法很简单:
import json from 'koa-json'
import logger from 'koa-logger'
const app = new Koa()
app.use(json()).use(logger()).listen(3000)
以上几行代码就依次应用了koa-json和koa-logger中间件,用户请求后端时,会依次被koa-json和koa-logger处理。
实际上 koa 做的事情很简单,它的 use 方法定义如下:
class Application extends Emitter {
// ....
use (fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
debug('use %s', fn._name || fn.name || '-')
this.middleware.push(fn)
return this
}
}
每次调用 use 方法,都会将函数push到 this.middleware 数组中。
在调用listen方法的时候就会通过 http模块监听端口,在监听回调中会将 this.mdiddleware 组合成Promise 任务队列,比如当this.middleware = [A, B, C, D]时,组合结果就是A.then(B).then(C).then(D),然后执行任务队列,每个任务都有机会处理ctx对象,当所有任务(即用户use的中间件函数)都执行完毕后,调用respond函数自动处理剩下的任务(根据 ctx.status、ctx.method等处理最终返回结果,并响应给用户)。
对于 koa2 的组合中间件的函数定义如下
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
可以看出每个中间件都会被包裹成Promise,如果在中间件中调用了 next 方法,则会调度下一个中间件,如果没有下一个中间件就返回 Promise.resolve()。
定义一个 koa-json 中间件的代码如下:
function(opts){
var opts = opts || {};
var param = opts.param;
var pretty = null == opts.pretty ? true : opts.pretty;
var spaces = opts.spaces || 2;
return function filter(ctx, next){
return next().then(() => {
var body = ctx.body;
// unsupported body type
var stream = body
&& typeof body.pipe === 'function'
&& body._readableState
&& body._readableState.objectMode;
var json = isJSON(body);
if (!json && !stream) return;
// query
var hasParam = param && hasOwnProperty.call(ctx.query, param);
var prettify = pretty || hasParam;
// always stringify object streams
if (stream) {
ctx.response.type = 'json';
var stringify = Stringify();
if (prettify) stringify.space = spaces;
ctx.body = body.pipe(stringify);
return;
}
// prettify JSON responses
if (json && prettify) {
return ctx.body = JSON.stringify(body, null, spaces);
}
});
}
};
可以总结出开发一个 koa2-中间件的方式如下:
- 编写一个高阶函数,传入option 对象,返回的函数入参是ctx 和 next,在返回函数中编写业务逻辑,在适当的时机调用next函数;
- 发布到npm仓库,命名一般以
koa-为前缀 一个中间件的形式如下:
export function middleware(options) {
return (ctx, next) => {
// ...
}
}
使用中间件的形式如下:
const options = {/**/}
const app = new Koa()
app.use(middleware(options))
markdown-it 插件
markdown-it 是一个markdown的解析器,用于将markdown文本转换成 html标签,它同样存在插件机制,允许我们自制插件或使用第三方插件来扩展markdown语法和功能。
markdown-it 源码:markdown-it
markdown-it 文档:Ruler | markdown-it 中文文档 (docschina.org)
markdown-it 插件的package名一般是以 markdown-it-为前缀,如markdown-it-table-of-contents。
markdown-it 使用插件也是通过 use 函数
const MarkdownIt = require("markdown-it");
const md = new MarkdownIt();
md.use(require("markdown-it-table-of-contents"));
markdown-it-table-of-contents模块导出的也是一个函数,函数大概是这个这个样子的
function(md, options) {
// ...
}
markdown-it-table-of-contents 是用于解析markdown文本中的 [[toc]]符号后生成目录,在这个插件函数中,我们需要几种能力:
- 获取当前解析器已经解析到的位置,这里称之为游标
- 创建新token后移动游标或者解析失败
- 创建解析规则
- 确定解析规则的使用时机,在某个规则之前或之后
总结
设计一个 plugin 机制的步骤可以总结如下:
- 确定暴露给插件的内容,即允许用户定制的问题域
- 确定plugin之间的时序关系,各个plugin的执行顺序可以是按照定义plugin的顺序,也可以是满足某个plugin必须在另一个plugin之前或之后的拓扑序
- 确定函数名和入参定义
- 提供开发和应用一个新插件的开发范式
- 编码实现