Skip to content

背景

在开发某些项目中,需要大量的自研组件,而每次修改组件后,要重新发布、安装依赖,这时候通常会使用 Monorepo 的方式解决,但如果这些组件包会应用到不同的项目中,则无法解决。所以想通过远程组件的方式,尽量减少人工操作,当我要迭代功能时,只需一键命令,就能完成组件的构建、发布,并且应用也无需安装新的依赖和重新构建发布。

目标

流程对比: ![[diff.excalidraw]]

  1. 搭建一个远程组件系统,提供组件管理和使用相关的接口
  2. 提供前端 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 模式,通过注入到全局对象,也可以自定义全局对象的路径(减轻全局污染)达到动态引入的效果。 优点:

  1. 简单、兼容性好

缺点:

  1. 全局变量污染
  2. 有依赖路径问题,比如 B 依赖 A,所以要先引入 A
  3. 版本问题,不同库依赖同一个库不同的版本,会造成冲突
  4. JS、CSS 不隔离

基于 ESM 加载

也就是使用原生 import 这个 API:

javascript
const loadComponents2 = async (url) => {
  const { add } = await import(url)
  console.log(add(1,2));
}

如果这个依赖里有依赖其他文件,import 语法也会按需加载。

优点:

  1. 支持按需加载
  2. 不会污染全局对象
  3. 官方标准,后续的支持会越来越好

缺点:

  1. 浏览器兼容性问题
  2. 不支持 UMD、CommonJS 等规范的库
  3. JS、CSS 不隔离

Eval

通过 Ajax 获取到文件的文本代码,然后使用 Eval 执行,和 script 标签的方式类似

new Function + 沙箱

  1. new Function 动态执行字符串代码
  2. 使用 with + proxy 实现一个沙箱

new Functioneval 的区别:

  1. new Function 在执行代码时,只能访问到全局的变量
  2. eval 在执行代码时,可以访问当前执行区域的变量

可以利用 new Function 这个特性,再配置 withproxy,解决全局变量污染和引入的库需要基于 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);
})

优点:

  1. 没有污染全局变量
  2. 无需改造第三方库,通过 proxy 解决库依赖 window 的问题
  3. 相对 eval 更安全,执行的代码只能获取到代理的全局对象

缺点:

  1. 只支持 UMD 规范

设计

使用流程

前端组件设计

系统设计

参考资料

  1. 掘金 - # 浅谈低代码平台远程组件加载方案
  2. Garfishjs - 沙箱机制