在UI组件库的过程当中,有一个常常会考虑到的问题是,使用者在使用这个UI库的过程当中,可能不满意库默认提供的表现和行为,须要对默认的表现和行为进行本身的定制。javascript
评价一个库是否好用的一条标准是可扩展性。css
评价一个库可扩展性是否优秀的一个很重要的原则是:经过增长代码来实现新功能或者改变已有的功能,而不是修改原有的代码。前端
这个原则是设计模式中的一基本原则:开放封闭原则(Open-Closed Principle OCP)java
Software entities(classes,modules,functions etc) should open for extension ,but close for modification.
开放封闭原则主要体如今两个方面:
对扩展开放,意味着有新的需求或变化时,能够对现有代码进行扩展,以适应新的状况。
对修改封闭,意味着类一旦设计完成,就能够独立其工做,而不要对类尽任何修改。node
本文分享我在基于react框架构建开源思惟导图库blink-mind的过程当中的一些实践和经验。但愿可以对你们的工做有所帮助。react
中间件思想在不少的开源项目中都有使用,其中很著名的一个例子是开源的node.js下一代 web服务端框架koagit
关于koa的中间件怎么使用这里不作过多的展开了。下面部分将具体分享在前端组件库中应用中间件思想来提升库的扩展性。github
对于一些简的组件库,使用者的需求可能只是改变一下样式,经过覆盖css就能够解决。web
可是对于一复杂的组件库,比方说富文本编辑器,表格编辑器,流程图等数据和交互都比较复杂的组件库,使用者可能不单单局限于改变样式。他们的需求多是对某个地方的popup menu 或者context menu进行定制,自定义大的组件中的某个子组件,自定义快捷键,对数据定义(model)进行扩展并对扩展的数据定义进行自定义的展现。设计模式
以我正在开发的思惟导图库为例,思惟导图中的一个节点称为一个topic。使用者可能想要:
为了可以支持使用者的这些需求,以及应用上文提到了开放封闭原则。我想到了能够将中间件思想应用到UI组件库的设计中来。
具体的实践是:
整个工程采用monorepo的组织方式,其中@blink-mind/core这个package 包含数据的scehema 定义以及 一个最关键的类Controller, Controller的主要职责是管理中间件以及经过名称调用某个中间件函数。
Controller中用一个map 来管理中间件
middleware: Map<string, Function[]>;
复制代码
注册中间件
function registerPlugin(controller: Controller, plugin: any) {
if (Array.isArray(plugin)) {
plugin.forEach(p => registerPlugin(controller, p));
return;
}
if (plugin == null) {
return;
}
for (const key in plugin) {
const fn = plugin[key];
controller.middleware[key] = controller.middleware[key] || [];
controller.middleware[key].push(fn);
}
}
复制代码
将多个同名的中间件函数组合(相似koa-compose)
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) throw new Error('next() called multiple times');
index = i;
let fn = middleware[i];
if (i === middleware.length) fn = next;
if (!fn) return null;
try {
return fn(context, dispatch.bind(null, i + 1));
} catch (err) {
return err;
}
}
};
}
复制代码
调用某个中间件函数
run(key: string, ...args: any[]) {
const { middleware } = this;
const fns = middleware[key] || [];
warning(fns.length !== 0, `the middleware function ${key} is not found!`);
const composedFn = memoizeOne(compose)(fns);
return composedFn(...args);
}
复制代码
默认的渲染方式由@blik-mind/renderer-react这个package 来实现,
其中渲染部分的中间件代码见rendering.tsx
举个例子系统默认的文本编辑器是个简单的文本编辑器,在渲染一个内容区域的过程当中会调用到renderTopicContentEditor中间件方法
renderBlock(props) {
const { controller, block } = props;
switch (block.type) {
case BlockType.CONTENT:
// 若是此内容区域block的类型是CONTENT
return controller.run('renderTopicContentEditor', props);
case BlockType.DESC:
return <TopicDescIcon {...props} />;
default:
break;
}
return null;
},
复制代码
renderTopicContentEditor(props) {
return <SimpleTopicContentEditor {...props} />;
},
复制代码
若是想要复杂的富文本编辑器,可使用@blik-mind/plugin-rich-text-editor这个package 中提供的plugin
plugin-rich-text-editor的实现代码以下
import * as React from 'react';
import { TopicContentEditor } from '../components/topic-content-editor';
import { TopicDescEditor } from '../components/topic-desc-editor';
export default function RichTextEditorPlugin() {
return {
renderTopicContentEditor(props) {
return <TopicContentEditor {...props} />;
},
renderTopicDescEditor(props) {
return <TopicDescEditor {...props} />;
}
};
}
复制代码
具体的TopicContentEditor 实现代码见rich-text-editor.tsx
和koa同样,经过compose方法将多个同名的中间件函数组合成一个函数,这些同名的中间件函数的调用次序由函数注册的次序以及函数中对next参数的使用有关。 即: 同名中间件函数中,最后注册的中间件函数先调用,在中间件函数中经过next参数调用下一个中间件函数 举个例子:
经过编写插件改变系统默认的context menu:删除edit menu 项,在第二个位置增长自定义项
function ChangeDefaultTopicContextMenuPlugin() {
return {
customizeTopicContextMenu(props, next) {
// 首先调用 next() 方法,获取系统默认的菜单项
let defaultMenus = next();
// 删除第一项,也就是 edit
defaultMenus.splice(0, 1);
// 在第二个位置增长自定义项
defaultMenus.splice(
1,
0,
<MenuItem
icon="group-objects"
label="Shift + A"
text="my customize menu"
onClick={onClickMyMenu(props)}
/>
);
console.log(defaultMenus);
return <>{defaultMenus}</>; } }; } 复制代码
这个例子的运行效果见 awehook.github.io/blink-mind/…
再举一个上文渲染内容区域的例子:
renderBlock(props) {
const { controller, block } = props;
switch (block.type) {
case BlockType.CONTENT:
// 若是此内容区域block的类型是CONTENT
return controller.run('renderTopicContentEditor', props);
case BlockType.DESC:
return <TopicDescIcon {...props} />;
default:
break;
}
return null;
},
复制代码
框架默认支持两种类的BlockType, 比方说使用者想要增长一种block的类型,好比说是个emoj图标,那么他能够编写一个插件,重写renderBlock函数,
renderBlock(props,next) {
const { controller, block } = props;
// 判断若是block 类型是emoj, 则用Emoj组件来进行渲染
if (block.type==='EMOJ') {
return <Emoj {...props}/>
}
// 其余状况下使用系统默认的渲染方式
return next();
},
复制代码
在UI组件库中使用中间件思想能够很方便的实现可扩展性,而且遵循了开放封闭原则。
最后很是欢迎各位小伙伴们给这个项目 blink-mind 点个star~~~~~~~~~~~~~~~~~~~~