前言
本文主要介绍我们探索远程组件方案的技术选型过程与demo尝试过程
你可以了解到远程组件的常见实现方案优缺点、SystemJS的远程组件实现方案
新人求大佬们轻喷?,欢迎提供改进意见等?!
为什么我们会需要用到远程组件
在近期的某个流程平台的技术选型阶段的时候,我们打算将每个流程的节点抽离为单独的独立组件,而这就会自然涉及到组件的仓库管理、加载组件等问题
1.组件仓库管理?
我们预期内,流程平台的组件应该是可以由不同部门的不同的人员开发,所以组件不应该局限存放在统一仓库里,我们更希望只需要大家在自己的个人仓库内按照约定好的规范开发,然后将写好的组件打包上传到云端,流程平台使用链接引入即可。
2.加载组件方式?
对于我们这个流程平台,我们希望是用户在页面进行自由选用节点组件,拼积木的方式编写一条流程(大家可以理解为常见的低代码平台),所以我们舍弃采用本地引入的方式,考虑采用远程组件的引入方式。
3.组件开发的框架选择?
对于组件开发的框架选择,我们认为更高效地开发组件,降低开发维护成本,就必须挣脱框架的限制。无论你是react党、vue党、angular党等等,我们希望只需要规范一套入口、出口、依赖管理方案,就能跨任意框架加载组件,所以远程组件是最优解。
所以三个问题的讨论最终都引领我们到了远程组件,于是我们决定尝试采用远程组件方案
那么如何加载远程组件呢?
我们对于加载远程组件的方案进行了技术方案调研。
目前部分可行的方案如下
- 动态Script方案
- eval方案
- new Function + 沙箱
- 微组件方案
具体实现原理,大家有兴趣可以参考这位大佬的文章:远程组件加载方案实践
针对这四个方案,我们此处总结一下优缺点:
- 动态Script方案
- 优点:简单;兼容性好(核心是用浏览器原生方法创建、删除script)
- 缺点:基于原文章的实现方案的话,自行编写加载script的代码会不够稳健;不支持沙箱;并且对于公共依赖必须挂载到window,导致全局变量污染
- eval方案
- 优点:简单;兼容性好(核心是用eval加载script)
- 缺点:不支持沙箱;依旧会导致全局变量污染;在严格模式下不允许 eval 函数
- new Function + 沙箱
- 优点:支持沙箱
- 缺点:沙箱技术方案不成熟
- 微组件
- 优点:简单;支持沙箱;
- 缺点:兼容性存在一定的问题;还有一些实践过程中发现的问题??
在探索实践过程中,我们采用字节跳动开源的Magic Microservices微组件框架,发现虽然在vue框架的情况下可以正常使用,但是我们在尝试用React作为我们的流程平台基座框架的时候,我发现不知道如何使用Magic Microservices注册的组件,折腾半天没解决,还发现Magic Microservices早已不维护了?,于是舍弃了这个方案。
但是这个这个方案让我接触到了SystemJS(更多元、友好的服务化组件模式)。
推荐文章:面向未来与浏览器规范的前端DDD架构设计
为什么SystemJS是更多元、友好的服务化组件模式
正如我们前面讨论的优缺点,目前我们希望一个方案可以不污染全局变量命名空间的依赖引入,兼容性好。
基于以上两点,我们找到了完美切合的方案:SystemJS
Github地址:SystemJS
保护全局变量:
对于SystemJS支持的Module包,我们只需要使用System.import引入即可,同理于公共依赖,System并不会将依赖注册到Window上,只会有一个System变量,存放着依赖映射。
兼容性好
对于旧版本浏览器,支持IE11浏览器的使用,可以兼容大部分常见浏览器。
基于SystemJS的远程组件实践
基于上面的技术选型探索,我们最终决定采用SystemJS模块加载方案进行实现远程组件。
主要实现过程:
- 开发组件,导出为System包
- 使用SystemJS管理公共依赖
- 使用远程组件
开发组件,导出为System包
对于组件的开发,采用什么框架、什么代码风格,都是任开发者喜欢选择的。规范打包配置即可。
我们只需要对于组件进行单独打包,并且需要打包为System包,并处理好公共依赖。
打包为System包的原因
由于我们的公共依赖是需要用SystemJS引入的,只有System包才可以正常使用SystemJS引入的公共依赖(基于目前尝试的结果,可能有别的方式可以实现umd等包使用SystemJS公共依赖,希望大佬们一起探讨)。
具体打包配置:
目前常见的webpack5、rollup都已经支持直接配置输出格式 为 system(具体可以参考配置文档),即可直接导出System包
至于公共依赖管理,以webpack5为例子,配置externals即可
例如:
externals: {
react: 'React',
'react-dom': 'ReactDOM',
lodash: 'lodash',
'@arco-design/web-react': '@arco-design/web-react',
'@bilibili/mega-form': '@bilibili/mega-form',
antd: 'antd',
},
打包结果预览:
System.register(
['@arco-design/web-react', 'React', 'lodash'],
function (g, Re) {
...
return (
Object.defineProperty(T, '__esModule', { value: !0 }),
{
setters: [
...
],
execute: function () {
...
},
}
)
}
)
使用SystemJS管理公共依赖
在使用远程组件的基座,我们需要对于远程组件所需要的公共依赖进行引入,我们采用SystemJS引入,原因前面已经提及了,就是保护Window
SystemJS使用介绍
主要是利用System的api,这里简单介绍一下核心Api:System.import,System.addImportMap,System.set
System.import
System.import(id [, parentURL]) -> Promise(Module)
id必须为URL,作为Module的地址,返回一个Promise(Module)
使用这个api就会自动将引入的Module记录到Window.System映射表上,从而被使用。
System.set
System.set(id, module) -> Module
我们可以设置一个id的对应值为传入的Modul
例如:System.set('app:React', { default: React, ...React })
System.addImportMap
System.addImportMap(map [, base])
利用这个api可以设置一个import映射表map
map的imports规则为{Module的名字:Module的id}
例如:
System.addImportMap({
imports: {
React: 'app:React',
ReactDOM: 'app:ReactDOM',
lodash: 'app:lodash',
antd: 'app:antd',
},
})
详细文档参考 SystemJS Api
具体流程
首先在基座的index.tsx,利用System.addImportMap给依赖建立映射表
System.addImportMap({
imports: {
React: 'app:React',
ReactDOM: 'app:ReactDOM',
lodash: 'app:lodash',
},
})
再通过System.set进行配置依赖
System.set('app:React', { default: React, ...React })
System.set('app:ReactDOM', { default: ReactDOM, __useDefault: true })
System.set('app:lodash', { default: lodash, ...lodash })
这样子我们就可以使得远程组件使用公共依赖啦
一些注意点?:
- System.set配置依赖的时候,例如React这种需要同时支持默认导出引入、按需引入的依赖,我们需要指定default为React,并展开React。这里是因为System格式打包导致的。
- 远程组件的公共依赖的打包名称配置和基座的公共依赖引入名称配置需要注意大小写一致,因为System的依赖寻找是大小写敏感的
对于rollup可以直接配置output.globals,wp5则是直接根据externals的属性值
使用远程组件
至于如何使用远程组件,我们只需要引入一个胶水层组件,内部封装使用System.import进行引入远程组件,并且进行一些载入组件状态的处理即可(此处参考动态Script方案demo进行优化的)
胶水组件
import { Skeleton } from '@arco-design/web-react'
import { get } from 'lodash'
import { useState, useEffect } from 'react'
export const SysComponent = ({ url, children, props = {} }: any) => {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [SysCom, setSysCom] = useState<any>(null)
useEffect(() => {
if (!url) return
System.import(url)
.then((Com) => {
console.log(Com)
const Component = get(Com, 'default')
// 这里需要注意的是,res 因为是组件,所以类型是 function
// 而如果直接 setSysCom 可以接受函数或者值,如果直接传递 setSysCom(Com),则内部会先执行这个函数,则会报错
// 所以值为函数的场景下,必须是 如下写法
if (Component) {
setSysCom(() => Component)
} else {
throw new Error('未找到组件')
}
})
.catch(setError)
.finally(() => {
setLoading(false)
})
}, [url])
if (!url) return null
if (error) return <div>error!!!</div>
return (
<Skeleton loading={loading}>
{SysCom && <SysCom {...props}>{children}</SysCom>}
</Skeleton>
)
}
使用层
import { Divider } from '@arco-design/web-react'
import { SysComponent } from './components/SysComponent'
const App = () => {
const insertFnTest = () => {
console.log('传入函数 成功!!')
}
const baseURL: string = 'http://127.0.0.1:3004/'
const fileFormat: string = '.system.js'
const compList = [
{
url: 'testArco',
props: { name: '测试arco', insertFnTest },
},
{
url: 'testMegafom',
props: { name: '测试megaform', insertFnTest },
},
{
url: 'testImage',
props: { name: '测试图片', insertFnTest },
},
]
const propsList = []
return (
<div id="app">
{compList.map(({ url, props }) => (
<>
<Divider />
<SysComponent
key={url}
url={baseURL + url + fileFormat}
props={props}
></SysComponent>
</>
))}
</div>
)
}
export default App
总结
一些遗憾
对于公共依赖,我们是在index.tsx impot引入依赖后使用的,而非使用cdn链接,因为我们测试例如使用antd的cdn链接,就会导致antd的公共依赖丢失,因为antd的cdn远程包是umd包,它会去Window上查找公共依赖。
这一点我们暂时没有想到更好的解决方案,欢迎大佬们提供点子呀!
未来展望
本次我们还是只将整套基本流程跑通,还有许多细节有待完善:
- 更加规范简洁的公共依赖导入
- 对于远程组件的公共依赖版本的统一控制方案,避免不同远程组件需要的依赖版本不同导致加载出错
- 更加完善的组件开发规范,例如出入口参数规范,组件版本控制等等,期望未来可以实现一个组件模块平台,统一管理发布的组件模块
欢迎指点
本文可能有有些遗漏点与错误点,希望大家积极指出,也欢迎大家一起探讨如何更好实现远程组件呀