应广大读者的要求,源码来了。我会尽我所能的把细节扣出来,分享给大家,愿大家享读。如果还没读过之前文章《Json2Html 功成》的,可以先去读下之前的内容,里面有讲解大体思路,结合思路理解起来,可能会更方便些。
以上是大致的思路以及代码实现,大家也可以直接在npm上搜索“json2html-react”下载源码看看效果,结合实际效果更方便理解。
core.js 核心逻辑源码解读
import React, { useState, useEffect } from 'react';
let JSON2HTML_ACTIONS = null; // 存放action动作
let JSON2HTML_COMPONENTS = null; // 存放component组件
// 注册action动作列表
const registerAction = (actions) => {
JSON2HTML_ACTIONS = { ...JSON2HTML_ACTIONS, ...actions };
};
// 注册components列表
const registerComponent = (components) => {
JSON2HTML_COMPONENTS = { ...JSON2HTML_COMPONENTS, ...components };
};
// 处理action动作列表
const handleAction = async (action, globalData) => {
// action判断,空则不做任何处理
if (!action) { return; }
// action类型判断,如果是数组则遍历执行里面的每个action
if (Array.isArray(action)) {
return action.forEach((i) => handleAction(i, globalData));
}
// action配置形式,固定为{type,data},type用于action映射,data为action传参。
const { type, data } = action;
if (JSON2HTML_ACTIONS[type]) {
// 支持action异步动作,获取异步结果。
const res = await JSON2HTML_ACTIONS[type](data, globalData);
// 支持链式调用action。例如请求类型的action,返回结果还是action时,则继续执行。
if (res?.type && JSON2HTML_ACTIONS[res.type]) {
// 递归处理action。
handleAction(res, globalData);
}
} else {
// 非action格式,则抛出错误。
console.error('不存在当前action:', type);
}
};
// 兼容小写开头 | 字符串中包含"-"
const getWidget = (widgetStr) => widgetStr?.replace(widgetStr[0], widgetStr[0].toUpperCase()).replace(/(-.)/g, (v) => v[1].toUpperCase());
// 解析json,将json转化为页面html。只做页面初始化解析,后续动作组件内部控制。
function Json2Html({ jsonObj, globalData }) {
const [filterProps, setFilterProps] = useState(null); // 统一管理所有子组件的props,初始化为null,避免联动产生的无效渲染。
const [uniKey, setUniKey] = useState(''); // 用于Array jChildren渲染时提供key做优化。
// 组件渲染时,只执行一遍,生成该组件的唯一key值。
useEffect(() => { setUniKey(Math.random()); }, []);
// widget:组件名;action:组件点击事件;jProps:组件属性透传;jChildren:当前组件的子组件;linkage:组件联动;needFormItem:form组件标识;dataBind:form组件绑定值;rules:form组件规则;validateTrigger:form组件校验rules的时机。
const { widget, action, jProps, jChildren, linkage, needFormItem, dataBind, rules, validateTrigger } = jsonObj || {};
const { form, FormItem } = globalData || {}; // form高阶函数注入变量,包含form的所有操作。
const globalState = form ? form.getFieldsValue() : {}; // 用于管理页面所有状态。
const events = globalData?.events || {}; // 用于给表单组件绑定事件。
//@TODO:处理表单组件默认值,其实理论上应该有form包处理,但是由于暂不支持默认值的注入,所以这边手动处理。如果有更合适的库,此代码可忽略。
useEffect(() => {
const { defaultValue } = filterProps || {};
if (defaultValue && form) {
form.setFieldsValue({ [dataBind]: defaultValue, });
}
}, [dataBind, JSON.stringify(filterProps)]);
// 处理action,给子组件注入onClick属性
useEffect(() => {
if (action) {
setFilterProps((v) => ({ ...v, onClick: () => handleAction(action, globalData) }));
}
}, [action, globalData]);
// 处理表单事件events绑定
useEffect(() => {
if (needFormItem && Object.keys(events).length > 0) {
// 处理表单事件回传,带上dataBind
const tempV = {};
Object.keys(events).forEach((e) => {
tempV[e] = (v) => events[e](dataBind, v);
});
// 事件events注入到子组件属性内
setFilterProps((v) => ({ ...v, ...tempV }));
}
}, [events, needFormItem, dataBind]);
// 处理cascade联动,目前只支持new Function形式。
useEffect(() => {
if (linkage) {
// eslint-disable-next-line
const fn = new Function('$globalState', linkage);
const newState = fn(globalState);
setFilterProps(newState ? (v) => ({ ...v, ...jProps, ...newState }) : null);
} else {
setFilterProps((v) => ({ ...v, ...jProps }));
}
}, [JSON.stringify(globalState), linkage, jProps]);
// 最里层jsonObj为undefined时childrens为null; filterProps为空时,表示组件初始化或者联动隐藏时无需渲染子组件。
if (!jsonObj || !filterProps) { return null; }
// 遍历childrens
if (Array.isArray(jsonObj)) {
return jsonObj.map((i, k) => {
const tProps = { jsonObj: i, globalData };
return (<Json2Html key={`${uniKey}-${k}`} {...tProps} />);
});
}
//处理widget,用于匹配组件库。例如:div -> Div; custom-input -> CustomInput。
const currentWidget = getWidget(widget);
//从组件库内获取当前组件。
const CurrentComponent = JSON2HTML_COMPONENTS[currentWidget];
//组件不存在则报错。
if (!CurrentComponent) {
console.error('不存在当前组件:', widget);
return null;
}
//渲染子组件
const tempProps = { jsonObj: jChildren, globalData };
//@TODO 如果是表单form组件,则需要特殊处理。如果有更合适的库,此代码可忽略。
if (needFormItem && form && FormItem) {
const tProps = form.getFieldProps(dataBind, { rules, validateTrigger: validateTrigger || '' }); // validateTrigger设置为‘’,将校验时机由页面控制
return (<FormItem key={uniKey}>
<div {...tProps}>
<CurrentComponent {...filterProps}>
{jChildren && (<Json2Html {...tempProps} />)}
</CurrentComponent>
</div>
</FormItem>);
}
//渲染子组件
return (
<CurrentComponent {...filterProps}>
{jChildren && (<Json2Html {...tempProps} />)}
</CurrentComponent>
);
}
export {
Json2Html, //解析器
handleAction, //处理action动作
registerAction, //注册action动作
registerComponent, //注册component组件
};
可能的疑问?
-
觉得当前的component组件库管理和action库管理有些粗糙,可以优化下,单独分割出来吗?
可以的,目前我是利用变量块级作用域的技术实现,大家也可以更换为全局状态管理。像vue的vuex,或者react的redux。
-
为啥action处理,默认不是链式处理呢?
我理解如果某个时间节点的action动作确定,存在多个,那应该是同步处理。如果一定要有异步的action动作,需要有先后之分,理应放到上一个action结果中处理。
-
这种形式的铺页面,较传统形式,会不会不太灵活?维护成本会不会很高?
首先可以确定,是很灵活,有多灵活?比我们传统的开发模式还要灵活!因为这套逻辑设计的初衷就已经把动态的模块进行分割。json2html解析可以理解为一个“纯函数”(虽然里面有些副作用处理),它只做解析处理,理论上不应该有组件上的各种业务交互;你也可以当它是个中转站,将传入的json数据按照一定的规则处理渲染成子组件。关键它还处理了很多共性问题,相信我,用上一段时间,你会爱上它的。
维护成本?json2html有一定的维护成本,很少,少得可怜。只有当你需要把业务公共的处理数据的逻辑抽离时,才可能会考虑到迭代它。而默认的action和linkage基本涵盖了90%的场景。而与json2html关联的自定义组件开发成本,也会降低很多。因为它默认就是用的颗粒度最小的组件来实现的;当然如果你用的第三方组件库有业务类的大组件,那也可以直接拿来用。真正需要你自定义的组件会很少,例如:需要开发防刷+验证码组件【需要注意,这个自定义组件,跟你用不用json2html无关,无论是否用它,有需求,你都需要开发它】。 -
还有更多问题?
可以进群交流,有时间的情况,绝对知无不言。如果嫌烦,也可以直接公众号留言,看到也会第一时间回复。
这就完了?
这只是刚刚开始,虽然目前json2html已成形,结合之前分享的预览器《【一抹骚】预览器》基本可以实现很顺畅的开发,基本跟传统开发模式没有两样,甚至效率会更高点。但这只是刚刚开始,真的只是个开始。后续阶段依然有很多功能待解决。例如:低代码html2json(是真正的低代码,只做解析,组件库/action库依然是自由选择),页面性能优化(既然已经实现了数据即页面,那不妨设想下,如果数据有version,进行数据缓存后的效果)等等。
愿我们奔赴山海 不负热爱
也可以关注公众号《小火球烧屁股》