这几年来,一直在思考怎么做出一款功能强大且配置简易的原生JS表格组件。
为此做了很多功能,也对这些功能做过多轮的优化以达到配置简易的愿景。
而在开发过程中,总有个绕不过去的坎: 框架模板无法解析。
这是一个什么概念呢?
当在框架环境中渲染表格组件,这个表格内的模板只能使用原生JS,无法使用任何框架特性。
这也就意味着,当在表格模板内使用框架组件时将无法渲染。
如下所示,通过模板配置一个Vue的Button组件是无法渲染的。
columnData: [
{
key: '操作',
template: <el-button @click="delRelation(row)">删除</el-button>
}
]
在框架满天飞的现在,这是无法忍受的。
选择方案
当时,看着满屏的代码,发呆了许久。
期间一直在思考使用哪种方案来面向框架:
- 原生版本停止开发,重新开发多套基于框架的表格组件。
- 在现有组件上进行改造,支持框架特性。
重新开发基于框架的表格组件,需要同时维护多套代码。
而在原生组件上进行改造,可以实现一套代码多框架运行。
毕竟无论哪种框架,都是源于JS。
设计思路
一旦选择,那么就坚持下去吧。虽然,谁都会知道中间的路很坎坷。
思路讲起来很简单,只是做出来需要很多调研工作,需要对各个框架有一定的熟识度,甚至在找不到解决方案时需要去阅读框架源码。
构思了一段时间后,一套只有两个步骤的实践方案就这么设定了:
- 为每个框架提供壳项目,在壳项目中实现框架解析模板的勾子。
- 原生组件负责在渲染过程中对各类模板进行整合,并在特定的时机发送至壳项目中进行解析。
思路确定之后,就该动手开工了。
实施
三个框架虽有不同,但都提供了解析原生DOM或动态创建框架对像的方法:
- Angular: $compile()
- Vue: new Vue()
- React: render()
在对框架进行支撑前,首先要对原生组件进行一些改造。
改造原生组件
声明一个容器,用于存储待解析的模板。
// 存储容器,基本格式: {'table-key': []}
const compileMap = {
};
// 获取指定表格的存储容器
const getCompileList = gridManagerName => {
if (!compileMap[gridManagerName]) {
compileMap[gridManagerName] = [];
}
return compileMap[gridManagerName];
};
收集原生表格中使用到的模板:
- td模板
- th模板
- 为空模板
- 通栏模板
为这些模板提供解析函数, 通过该函数生成不同框架的待解析模板,并存入compileMap等待解析。
// td模板解析函数
const compileTd = (settings, el, template, row, index, key) => {
const {
gridManagerName, compileAngularjs, compileVue, compileReact } = settings;
const compileList = getCompileList(gridManagerName);
// React and not template
if (!template) {
return row[key];
}
// React element or React function
// react 返回空字符串,将单元格内容交由react控制
if (compileReact) {
compileList.push({
el, template, row, index, key, type: 'template', fnArg: [row[key], row, index, key]});
return '';
}
// 解析框架: Angular 1.x || Vue
if (compileVue || compileAngularjs) {
compileList.push({
el, row, index, key});
}
// not React
// 非react时,返回函数执行结果
if (!compileReact) {
return template(row[key], row, index, key);
}
};
// ... 其它模板的解析函数,大致上与td类似
在原生组件拥有模板解析函数后,还需要为原生组件提供与各框架版本的通迅函数。
// 通迅函数: 与各框架模板解析勾子进行通迅,在特定时间调用
function sendCompile(settings, isRunElement) {
const {
gridManagerName, compileAngularjs, compileVue, compileReact } = settings;
const compileList = getCompileList(gridManagerName);
if (compileList.length === 0) {
return;
}
if (isRunElement) {
compileList.forEach((item, index) => {
item.el = document.querySelector(`[${
getKey(gridManagerName)}="${
index}"]`);
});
}
// 解析框架: Vue
if (compileVue) {
await compileVue(compileList);
}
// 解析框架: Angular 1.x
if (compileAngularjs) {
await compileAngularjs(compileList);
}
// 解析框架: React