从 0 写一个迷你 wujie:用原生 Web API 实现微前端

从 0 写一个迷你 wujie:用原生 Web API 实现微前端

学习于(www.bilibili.com/video/BV1Qt...) 感谢大佬的分享,对于实践我做了一些总结


wujie解决了前文微前端的什么问题:

1.原有的CSS隔离方式是在子应用的样式上增加名称,有点消耗性能(scoped)

2.原有的JS我们自己实现沙箱去隔离,但没有使用iframe这个天然的沙箱应用

wujie很好的使用了WEB component+iframe来解决这个问题

第一步:搭建框架,模拟子应用资源

先搭好基座页面骨架。页面上有一个"基座应用"的 div 和一个 #container 容器------子应用最终会被注入到这里。

然后准备两个变量,模拟子应用 fetch 请求拿到的资源:

  • strTemWithCss :子应用的 HTML + CSS,里面有个 #inner 的 div 和一个 div { background-color: red } 的全局样式
  • strScript :子应用的 JS,在 window 上挂一个 a=100,然后 querySelector('#inner') 查找自己的元素
js 复制代码
// 模拟子应用发起 fetch 请求
const strTemWithCss = `
  <!DOCTYPE html>
  <html>
    <body>
      <div id="inner">子应用,不被基座应用影响</div>
      <style>div { background-color: red; color: white; }</style>
    </body>
  </html>
`

const strScript = `
  window.a = 100
  console.log(window.a)
  const ele = document.querySelector('#inner')
  console.log(ele)
`

真实场景中,这两个字符串是 fetch(子应用URL) 拿到 HTML 后再解析出来的,这里先硬编码,专注讲隔离原理。


第二步:定义 <wujie-app> 自定义元素,插入到 container 中

这一步把"子应用"封装成一个自定义 HTML 元素 <wujie-app>。后面整个沙箱逻辑都放在 connectedCallback 生命周期里------当元素被插入 DOM 时自动执行。

js 复制代码
function createCustomElement() {
    class WujieApp extends HTMLElement {
        connectedCallback() {
            // 创建沙箱
            const sandbox = createSandbox()
            // 创建子应用
            sandbox.shadowRoot = this.attachShadow({ mode: 'open' })
            // 注入 html,css
            injectHemplate(sandbox, strTemWithCss)
            // 注入 js
            injectScript(sandbox, strScript)
        }
    }
    window.customElements.define('wujie-app', WujieApp)
    container.appendChild(document.createElement('wujie-app'))
}

createCustomElement()

几个关键点:

  • customElements.define('wujie-app', WujieApp) :注册自定义标签,之后在 HTML 里可以直接写 <wujie-app></wujie-app>
  • connectedCallback():元素插入 DOM 时触发,相当于子应用"挂载"的入口
  • this.attachShadow({ mode: 'open' }):这是 CSS 隔离的关键------后面会讲

至此,框架搭好了,页面上有了一个 <wujie-app> 元素,但它的沙箱逻辑还没写。


第三步:构建沙箱,注入 HTML 和 CSS

3.1 为什么需要沙箱?

直接把子应用的 HTML/CSS/JS 扔到基座页面里执行,会产生两个问题:

  1. CSS 污染 :子应用写了 div { background: red },基座页面上所有 <div> 全变红
  2. JS 污染 :子应用写了 window.a = 100,基座也跟着多了一个 window.a

所以需要一个"沙箱",让子应用的内容和基座隔离开。

3.2 iframe 提供 JS 执行环境

js 复制代码
function creatrIframe() {
    const iframe = document.createElement('iframe')
    iframe.src = 'about:blank'
    document.body.appendChild(iframe)
    return iframe
}

function createSandbox() {
    const sanbox = {
        ifame: creatrIframe(),
        shadowRoot: null
    }
    return sanbox
}

iframe 天生就是一个独立的执行环境------它有自己独立的 windowdocument,里面的变量和外层完全隔离。子应用的 JS 最终会在这个 iframe 里执行,所以 window.a = 100 不会污染基座。

3.3 Shadow DOM 提供 CSS 隔离

js 复制代码
sandbox.shadowRoot = this.attachShadow({ mode: 'open' })

attachShadow 会在 <wujie-app> 内部创建一个 Shadow Root。CSS 选择器穿不透 Shadow 边界 ------子应用在 Shadow Root 里写 div { background: red },只会影响 Shadow 内部,基座页面上的 div 不受影响。

3.4 注入 HTML 和 CSS

js 复制代码
function injectHemplate(sandbox, template) {
    const wrapper = document.createElement('div')
    wrapper.innerHTML = template
    sandbox.shadowRoot.appendChild(wrapper)
}

strTemWithCss 通过一个临时 wrapper 解析成 DOM 节点,然后 appendChild 到 Shadow Root 里。此时子应用的 div 和 <style> 标签都在 Shadow 内部------CSS 已经隔离了。


第四步:注入 JS,重写方法实现 DOM 桥接

这是整个沙箱最精妙的一步。

4.1 问题:子应用 JS 跑在 iframe 里,DOM 却在 Shadow Root 里

子应用的 HTML/CSS 放在了基座页面的 Shadow Root 里(能看到),但 JS 要跑在 iframe 里(能隔离)。

这就出现了一个矛盾:子应用代码里写的是:

js 复制代码
const ele = document.querySelector('#inner')

这个 documentiframe 里的 document ------空的,什么都没有。而 #inner 元素实际在基座页面的 Shadow Root 里。

4.2 解决方案:重写 iframe 的 document.querySelector

js 复制代码
function injectScript(sandbox, script) {
    const iframeWindow = sandbox.ifame.contentWindow
    const scriptElement = iframeWindow.document.createElement('script')
    const headElement = iframeWindow.document.querySelector('head')

    Object.defineProperty(iframeWindow.Document.prototype, 'querySelector', {
        get() {
            return new Proxy(sandbox.shadowRoot['querySelector'], {
                apply(target, thisArg, argumentsList) {
                    console.log("apply")
                    return thisArg.querySelector.apply(sandbox.shadowRoot, argumentsList)
                }
            })
        }
    })

    scriptElement.textContent = script
    headElement.appendChild(scriptElement)
}

拆开来看:

第一步 :在 iframe 里创建一个 <script> 标签,把子应用的 JS 代码塞进去,挂到 iframe 的 <head> 里------这样 JS 就在 iframe 的 window 里执行了。

第二步 (核心桥接):在注入 JS 之前 ,用 Object.defineProperty 把 iframe 的 Document.prototype.querySelector 给替换掉:

dart 复制代码
iframe 的 Document.prototype.querySelector
       ↓ 被重写为 ↓
sandbox.shadowRoot.querySelector   (基座 Shadow Root 的 querySelector)

具体做法是用 Proxy 拦截 apply------每次子应用代码调用 document.querySelector(...) 时:

  1. 实际执行的是 sandbox.shadowRoot.querySelector(...)
  2. 参数原样透传(比如 '#inner'
  3. 返回的是 Shadow Root 里的 DOM 元素

💡 为什么用 Object.defineProperty + Proxy 而不是直接赋值?

因为 Document.prototype.querySelector 是一个访问器属性 (getter),不能直接 = 赋值。需要用 Object.defineProperty 拦截 get,然后返回一个 Proxy 来代理 apply 调用。这样当子应用调用 document.querySelector('#inner') 时,get 返回 Proxy,apply 被拦截,实际执行的是 Shadow Root 上的查询。

效果 :子应用代码完全无感------它以为自己在一个普通页面里跑 document.querySelector('#inner'),实际上查询被静默转发到了基座的 Shadow Root,成功拿到了 #inner 元素。

4.3 整体数据流

xml 复制代码
基座页面
  ├── <div>基座应用</div>
  ├── <div id="container">
  │     └── <wujie-app>
  │           └── #shadow-root  ← CSS 隔离边界
  │                 ├── <div id="inner">...</div>  ← 子应用 DOM
  │                 └── <style>div { background: red }</style>
  │           ← iframe(隐藏,JS 执行环境)
  │                 └── window.a = 100       ← 不污染基座
  │                 └── document.querySelector  ← 被重写,指向 Shadow Root
  └── <iframe style="display:none" src="about:blank"></iframe>

总结:三个隔离 + 一个桥接

机制 用到的 API 解决的问题
CSS 隔离 attachShadow({ mode: 'open' }) 子应用样式不污染基座
JS 隔离 document.createElement('iframe') 子应用 window 和基座完全独立
DOM 桥接 Object.defineProperty + Proxy 子应用能正常 querySelector 到自己的元素
自定义元素 customElements.define 把子应用封装成 <wujie-app> 标签

相关推荐
妙码生花42 分钟前
从 PHP 到 AI + Golang,程序员自救转型手记(五十七):AI时代对组件封装的新理解、agInput 统一入口,动态表单预备
前端·后端·go
fairyly43 分钟前
接手新项目不用硬啃源码,我的 TRAE Work 快速上手实战工作流
前端·ai编程·trae
搬砖记录员44 分钟前
用 PyAutoGUI 手搓录屏"老板键":从原理到四套可跑源码,附工程化落地思考
前端
windliang44 分钟前
Claude Code 源码分析(七):Skill 如何进入 Agent
前端·人工智能·面试
栀鸢ouo1 小时前
《Vue 3 实现多主题切换和自定义主题》
前端
缓冲中请稍后1 小时前
React Router 完全指南:从 HashRouter 到 BrowserRouter
前端·面试
妙码生花1 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十六):附件管理、增加根据文件后缀生成 SVG 文件图标的接口
前端·后端·go
栀鸢ouo1 小时前
useTableHeight:一个优雅的 Vue 3 表格自适应高度解决方案
前端
橘子星1 小时前
一篇文章搞懂 useRef:聚焦 DOM、模拟 forceRender、管理 Worker 都靠它
前端·javascript