背景
作者维护的可视化搭建平台所提供的投放数据配置表单是基于搭建物料中配置的JSON Schema经过统一的渲染生成的,这就意味着:表单项的类型是预先约定好的,虽然这可以满足业务绝大部分的诉求,但是总有一些高度定制的配置项需要支持业务自定义。作为一个通用的平台,内部耦合业务逻辑是个很愚蠢的办法,所以便开了业务自定义扩展渲染组件的口子。
原理
支持UMD
类型的像Input
、Select
这些基础组件一样的可用于表单渲染的组件通过cdn
远程加载。
组件设计
要想使用UMD
的组件首先要做一个容器组件用于渲染。同时该容器组件又要用于表单渲染。所以组件props
设计如下:
interface PropsType {
render?: {
name: string; // library name
entry: string; // 自定义渲染组件 umd 格式 url
style: string; // 自定义渲染组件 css文件
};
value: any;
onChange: (value: any) => void;
[x: string]: any; // 业务自定义参数
}
render
参数用于组件渲染,其他参数用于组件逻辑
组件核心逻辑
CustomRender
const CustomRender: React.FC<PropsType> = (props) => {
const {
render,
...otherProps
} = props;
const [Com, setCom] = useState<any>();
useEffect(() => {
if (!render?.entry) {
return;
}
(async () => {
// 加载UMD组件
const C = await importScript(render?.entry, render?.name);
// 加载组件样式
render?.style && importStyle(render?.style);
setCom(() => C);
})();
}, [render?.entry]);
return (
<ErrorBoundary fallback={
<div>配置项加载失败</div>}>
<div>{
Com ? <Com {
...otherProps} /> : <div>加载中...</div>}</div>
</ErrorBoundary>
);
};
importScript
export const importScript = (() => {
// 自执行函数,创建一个闭包,保存 cache 结果
const cache: {
[x: string]: any } = {
};
return (url: string, name?: string) => {
// 如果有缓存,则直接返回缓存内容
if (cache[url]) return Promise.resolve(cache[url]);
return new Promise((resolve, reject) => {
// 保存最后一个 window 属性 key
const lastWindowKey = Object.keys(window).pop();
// 创建 script
const script = document.createElement('script');
script.setAttribute('src', url);
document.head.appendChild(script);
// 监听加载完成事件
script.addEventListener('load', () => {
document.head.removeChild(script);
// 最后一个新增的 key,就是 umd 挂载的,可自行验证
const newLastWindowKey = name || Object.keys(window).pop();
console.log('newLastWindowKey', newLastWindowKey);
// 获取到导出的组件
const res = lastWindowKey !== newLastWindowKey ? window[newLastWindowKey] : {
};
const Com = res.default ? res.default : res;
cache[url] = Com;
resolve(Com);
});
// 监听加载失败情况
script.addEventListener('error', (error) => {
reject(error);
});
});
};
})();
importStyle
export const importStyle = (() => {
return (url: string) => {
if (document.querySelector(`link[href='${
url}']`)) {
return;
}
return new Promise((resolve, reject) => {
// 创建 link
const link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('href', url);
document.head.appendChild(link);
// 监听加载完成事件
link.addEventListener('load', () => {
resolve(link);
});
// 监听加载失败情况
link.addEventListener('error', (error) => {
reject(error);
});
});
};
})();