前端远端SDK组件加载方案

背景

  作者维护的可视化搭建平台所提供的投放数据配置表单是基于搭建物料中配置的JSON Schema经过统一的渲染生成的,这就意味着:表单项的类型是预先约定好的,虽然这可以满足业务绝大部分的诉求,但是总有一些高度定制的配置项需要支持业务自定义。作为一个通用的平台,内部耦合业务逻辑是个很愚蠢的办法,所以便开了业务自定义扩展渲染组件的口子。

原理

  支持UMD类型的像InputSelect这些基础组件一样的可用于表单渲染的组件通过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);
      });
    });
  };
})();

猜你喜欢

转载自blog.csdn.net/zSY_snake/article/details/141639115