背景
在开发某些项目中,需要大量的自研组件,而每次修改组件后,要重新发布、安装依赖,这时候通常会使用 Monorepo 的方式解决,但如果这些组件包会应用到不同的项目中,则无法解决。所以想通过远程组件的方式,尽量减少人工操作,当我要迭代功能时,只需一键命令,就能完成组件的构建、发布,并且应用也无需安装新的依赖和重新构建发布。
目标
流程对比: ![[diff.excalidraw]]
- 搭建一个远程组件系统,提供组件管理和使用相关的接口
- 提供前端 SDK,使业务系统可以快速使用远程组件
分析
前端如何使用远程组件?
整个系统最核心的问题就是前端代码要如何使用远程组件,什么是远程组件?当你的页面在浏览器跑的时候,可以通过 Ajax 加载 JS 文件并运行,远程组件就是在页面运行时加载并执行 JS 文件。
那 JS 如何在运行时加载 JS 文件?
基于 UMD 相关规范的 script 标签加载
javascript
const loadComponent = async (url) => {
const script = document.createElement('script')
script.src = url
document.querySelector('body').appendChild(script)
}
loadComponent('http://localhost/a.js')
// a.js
// console.log('load index successfully')
// 输出:load index successfully
// window.a -> 该文件暴露的对象
UMD 模式,通过注入到全局对象,也可以自定义全局对象的路径(减轻全局污染)达到动态引入的效果。 优点:
- 简单、兼容性好
缺点:
- 全局变量污染
- 有依赖路径问题,比如 B 依赖 A,所以要先引入 A
- 版本问题,不同库依赖同一个库不同的版本,会造成冲突
- JS、CSS 不隔离
基于 ESM 加载
也就是使用原生 import
这个 API:
javascript
const loadComponents2 = async (url) => {
const { add } = await import(url)
console.log(add(1,2));
}
如果这个依赖里有依赖其他文件,import
语法也会按需加载。
优点:
- 支持按需加载
- 不会污染全局对象
- 官方标准,后续的支持会越来越好
缺点:
- 浏览器兼容性问题
- 不支持 UMD、CommonJS 等规范的库
- JS、CSS 不隔离
Eval
通过 Ajax 获取到文件的文本代码,然后使用 Eval 执行,和 script 标签的方式类似
new Function + 沙箱
new Function
动态执行字符串代码- 使用
with
+proxy
实现一个沙箱
new Function
和 eval
的区别:
new Function
在执行代码时,只能访问到全局的变量eval
在执行代码时,可以访问当前执行区域的变量
可以利用 new Function
这个特性,再配置 with
和 proxy
,解决全局变量污染和引入的库需要基于 window
的问题
with
作用:对执行的区域块增加一层作用域链 proxy
作用:对一个对象进行代理
比如我动态引入的 A 库需要用到 Vue,但 A 库时基于 UMD 规范打包,并且是从全局获取 Vue,这时我们可以这样处理:
javascript
const fakeWindow = {}
const proxyWindow = new Proxy(window, {
// 获取属性
get(target, key) {
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol/unscopables
if (key === Symbol.unscopables) return false
// 内部可能访问当这几个变量,都直接返回代理对象
if (['window', 'self', 'globalThis'].includes(key)) {
return proxyWindow
}
return target[key] || fakeWindow[key]
},
// 设置属性
set(target, key, value) {
return fakeWindow[key] = value
},
// 判断属性是否有
has(target, key) {
return key in target || key in fakeWindow
}
})
window.proxyWindow = proxyWindow
const loadCode = (codeText) => {
const code = `;(function (win) {
with(win) {
${codeText}
}
}).call(window.proxyWindow, window.proxyWindow)`
const fn = new Function(code)
fn()
}
fetch(url).then(res => {
return res.text()
}).then(txt => {
loadCode(txt)
console.log(fakeWindow);
})
优点:
- 没有污染全局变量
- 无需改造第三方库,通过
proxy
解决库依赖window
的问题 - 相对
eval
更安全,执行的代码只能获取到代理的全局对象
缺点:
- 只支持 UMD 规范