微前端 qiankun 项目实践 !!!

7,515 阅读18分钟

本文章主要是本人工作记录,无教学或科普目的,感兴趣的老师和同学可以看一下 ~ 。◕‿◕。~ !!!

1、项目背景

目前处于业务的起步阶段,FE团队主要负责B端的管理系统。随着时间的推移和更多相互独立功能的集成(涉及老应用集成),再加上人员和团队经常变动,系统变得越来越庞大和难以维护是不可避免的。同时就目前前端技术的革新节奏而言,不太可能确保一套技术方案长期适用,因此几年后如何确保遗产代码的平滑迁移和新技术的接入也是一个问题。因此前期技术体系的选择和系统软件构架的设计非常重要,以避免系统过早成为巨石应用,我们考虑的主要有如下几个方面:

  1. 如何解构巨石应用,让团队成员own一个独立功能?
  2. 如何解决人员和团队频繁变动带来的维护问题?
  3. 如何解决技术革新带来的技术栈问题?

2、调研

2.1、概述

微前端源自微服务这种服务端的技术范式,微服务的核心关键在于服务的抽象或划分,用于解决服务端高并发、高可用等问题。而微前端和微服务不尽相同,微前端是一种多个团队通过独立发布的方式来共同构建现代化 web 应用的技术手段及方法。微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用后,随之而来的应用不可维护的问题。

2.2、优势

  • 技术栈无关 主框架不限制接入应用的技术栈,微应用具备完全自主权。

  • 独立开发、独立部署 微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新。

  • 增量升级

    在面对各种复杂场景时,通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略。

  • 独立运行时 每个微应用之间状态隔离,运行时状态不共享。

2.2、主流的实现方案

  • 容器模式,基于iframe;
  • 路由分发式,基于浏览器url或者框架的路由模块,配合服务器代理进行应用分发,比如qiankun;
  • 微件式,一个应用打包成独立的模块,可直接嵌入运行;
  • EMP,基于Webpack5 Module Federation搭建,解决的是业务分拆 不是跨框架调用;
  • 容器化,结合Web Components构建;

2.3、为什么选择qiankun

业务的大部分系统均为B端PC后台系统,且各子系统之间相对独立,不会出现复杂的应用嵌套情况,基于路由分发完全可以满足业务需求。qiankun 已在蚂蚁内部服务了超过 200+ 线上应用,且公司内部也有相关实践。

3、系统设计

3.1、功能架构图

3.2、业务流程图

3.3、路由规则

主应用、子应用均采用hash模式,url如下:

https://xx.xxxxxxxx.xxx/static/fe-xxx-qiankun/#/micro-app-xxx/xxxx/xxxx

其中 /micro-app-xxx 命中 主应用路由 和 qiankun 子应用active路由,用于区分不同的子应用,后面的路径用于命中子应用内部的router 路由。

3.4、系统搭建

  • 不同系统使用统一的技术栈(vue3全家桶、vite构建、Element plus组件库、typescript);

    注: 理论上不同系统,技术栈可以保持多样化,qiankun模式不依赖统一的技术栈。

  • 目前基于fe-xxxx-xxx项目手动复制,后期会统一使用脚手架搭建;

3.5、部署方案

应用独立,各应用独立部署,相互不影响。

4、问题

4.1、编译打包

qinakun官方支持的打包工具是webpack,未提供兼容vite的能力。主要因为vite打包出来的是ES module,qiankun在运行的时候会有问题。

参考第三方vite插件(vite-plugin-qiankun)的思想,我们提供了vite-plugin-qiankun-enhance插件,提供如下能力:

  • 在qiankun的基础上提供统一的应用间通信能力。
  • 通过修改html中的JS文件引入方式,兼容qiankun的子应用执行方式。
  • 提供一系列工具函数。
    • 使用防抖解决Vue-router 钩子函数重复执行;
    • vue-router 添加统一的前缀;

4.2、子应用样式调整

因为子系统页面内嵌在了主系统中,导致子系统页面会缩小,子系统代码中部分使用了vw, vh的样式会出问题,需要进行调整适配。

4.3、样式隔离

4.3.1、隔离方案介绍

  1. 默认情况;

    沙箱可以确保单实例场景子应用之间的样式隔离,但是无法确保主应用跟子应用、或者多实例场景的子应用样式隔离。

  2. strictStyleIsolation: true

    严格的样式隔离模式,该模式下 qiankun 会为每个微应用的容器包裹上一个ShadowDOM节点,从而确保微应用的样式不会对全局造成影响。基于 ShadowDOM 的严格样式隔离并不是一个可以无脑使用的方案,大部分情况下都需要接入应用做一些适配后才能正常在 ShadowDOM 中运行起来。

  3. experimentalStyleIsolation: true

    qiankun 会改写子应用所添加的样式,为所有样式规则增加一个特殊的选择器规则来限定其影响范围,类似为如下结构:

   // 假设应用名是 react16
   .app-main {
     font-size: 14px;
   }

   div[data-qiankun-react16] .app-main {
     font-size: 14px;
   }

注意: @keyframes , @font-face , @import , @page 将不被支持 (i.e. 不会被改写)。

4.3.2、隔离方案比较

shadow dom
  1. iconffont 字体在子应用无法加载;

    shadow dom 不支持 @font-face ,引入 iconfont 的时候,尽管可以引入样式,但由于字体文件是不存在的,所以相对应的图标也无法展示。

    解决: 把字体文件放在主应用加载/使用通用的字体文件。

  2. 组件库动态创建的元素无法使用自己的样式;

    对话框或提示窗是通过 document.body.appendChild 添加的,所以 shadow dom 内引入的 CSS 是无法作用到外面元素的,比如说常见的弹窗组件,Popover、Popconfirm等。

    解决: 代理 document.body.appendChild 方法,即把新加的元素添加到 shadow dom容器下,而不是最外面的 body节点下

   function proxy(shadowDom) {
     if (!document.body.appendChild.isProxy) {
       document.body.appendChild = new Proxy(document.body.appendChild, {
         apply(target, thisArg, node) {
           if (node[0].classList.contains('el-overlay')) {
             target.apply(thisArg, node);
           } else {
             shadowDom.appendChild(node[0]);
           }
         },
         get: isProxy,
       });
     }
   }
  1. 事件代理;

    列如react组件上声明的事件最终绑定到了document这个DOM节点上,事件代理监听器依靠冒泡来知道何时处理事件。由于 Shadow DOM 事件不会冒泡到顶层,所以代理监听器不会起作用。

    思路: 在渲染一个独立的 react 应用程序时,可以检测元素是否在 Shadow DOM 中。然后在 shadow root 上注册顶级侦听器。

  2. svg 不生效;

experimentalStyleIsolation
  1. 各种奇怪样式问题;

    • [💤 子应用 var 变量丢失](#子应用 var 变量丢失)。
  2. 同shadow dom 第二条(document.body.appendChild);

  3. 其他;

4.3.3、隔离方法选择

最终选择 experimentalStyleIsolation,理由如下(主要参考qiankun作者 @kuitos 在本条 issues#1061 的建议):

  1. shadow dom 的问题不止 @font-face 这个场景,还有 svg 不生效、事件代理等问题;
  2. shadow dom 实践下来并不是可以开箱即用的方案,建议现阶段用 experimentalStyleIsolation 来解决样式隔离问题;
  3. qiankun 团队有线上应用用 experimentalStyleIsolation 在跑,还没有发现大的问题;
  4. shadow dom 后续版本不再推荐,后续待 experimentalStyleIsolation 成熟后,会将其替换掉 shadow dom 的实现,同时提供兼容的迁移方案;

作者对 shadow dom 的看法

shadowdom 提出快 10 年了,但至今生态还远不到生产可用的地步,react/vue/angular 对 shadowdom 的适配也都不是他们第一优先级的任务,现阶段我们会更多投资 scoped style 方案,思路跟 experimentalStyleIsolation 一致

qiankun 目前只是提供一个 shadow dom 容器,应用如果需要在 shadow dom 容器里正常跑起来,需要自己去做一些适配(文档里有声明)。如果 qiankun 自身期望 shadow dom 变成开箱即用的完整方案,那就不应该只解决 font-face 这一个问题(可能只是所有问题中的 1/10),框架(r/v/a)、组件库、兼容性 这些更普遍的问题都需要提供方案,这个基本上是做不到的。如果没办法提供相对完整的方案,我们就更倾向于让用户自己去解决了。

4.3.4、样式隔离带来的问题

子应用 var 变量丢失

主应用设置 experimentalStyleIsolation: true 后子应用样式混乱。qiankun 为子应用的css class 添加前缀后,css的样式中 var 取值错误。如子应用的dialog样式错误,如下:

/** 子应用,dialog 样式错误 */
div[data-qiankun="micro-app-mis"] .el-dialog__header {
  padding-top: ;
  padding-right: ;
  padding-left: ;
  padding-bottom: 10px;
}

/** 子应用,reset.less。覆盖了 父应用的.el-dialog__header*/
div[data-qiankun="micro-app-mis"] * {
  margin: 0px;
  padding: 0px;
  box-sizing: border-box;
}

/** 父应用,dialog 样式 */
.el-dialog__header {
  padding: var(--el-dialog-padding-primary);
  padding-bottom: 10px;
}

/** 父应用,reset.less。 */
* {
  margin: 0px;
  padding: 0px;
  box-sizing: border-box;
}

出错误的原因是 var(—el-dialog-padding-primary) 的值获取错误,该值定义在如下:

.el-dialog {
  --el-dialog-padding-primary: 20px;
}
复现demo
思考方向
  • css class 类中创建swapNode时设置disable = true造成(×);

  • 使用 appendChildren将css 添加到swapNode造成(×);

    const textNode = document.createTextNode(cssStr || '');
    swapNode.appendChild(textNode);
  • Css 结构中 .el-dialog__header 前没有 .el-dialog造成(×);
    .el-dialog {
      --el-dialog-padding-primary: 20px;
      width: 100px;
      height: 100px;
    }

    .el-dialog__header {
      padding: var(--el-dialog-padding-primary);
      padding-bottom: 10px;
      width: 100%;
      height: 100%;
      background-color: red;
      box-sizing: border-box;
    }
  • 将css 添加到swapNode 时没有对应的Html 结构(×);
结论

Chrome 的bug。css中**css 变量的简写属性(如, padding: var(--a)该属性具体属性(如, padding-bottom: 10px )**同时存在时,使用 style.sheet.cssRules[index].cssText 去取值此时获取到的值有问题( padding-top: ; padding-right: ; padding-left: ; padding-bottom: 10px; )。

// 错误
padding: var(--a);
padding-bottom: 10px;

// 1. 正确
padding: var(--a);

// 2. 正确
padding-top: var(--a);
padding-bottom: var(--a);
padding-left: var(--a);
padding-right: var(--a);
padding-bottom: 10px;
解决
  1. 鸵鸟策略,case by case;

  2. 将css的变量转换成具体的数值;

  3. 更换添加 class前缀的时间;

    自己开发或者利用vite/webpack已有工具链,在子应用构建阶段完成css 的class prefix操作。qiankun 是在加载子应用时完成的prefix操作,这种方式会占用客户端性能。

子应用 class 前缀丢失
  • [💤4.2、异步的css prefix](#4.2、异步的css prefix)

experimentalStyleIsolation: true 选项没有完全覆盖到子应用 里面的样式,子应用部分css中的class未添加前缀,出现在如下两种情况。

  • vue组件里面的css 样式在开发环境时通过js的方式引入;

    ships 环境和正式环境没问题,因为此时的css 样式是通过css 文件引入的;

  • 通过动态创建style引入css;

缺失原因

Vite 在开发环境提供了 updateStyle 函数用于更新class。主要调用如下两个方法:

  • document.createElement('style')
  • document.head.appendChild

updateStyle 函数如下:

export function updateStyle(id: string, content: string): void {
  let style = sheetsMap.get(id)
  if (supportsConstructedSheet && !content.includes('@import')) {
    if (style && !(style instanceof CSSStyleSheet)) {
      removeStyle(id)
      style = undefined
    }

    if (!style) {
      style = new CSSStyleSheet()
      style.replaceSync(content)
      // @ts-ignore
      document.adoptedStyleSheets = [...document.adoptedStyleSheets, style]
    } else {
      style.replaceSync(content)
    }
  } else {
    if (style && !(style instanceof HTMLStyleElement)) {
      removeStyle(id)
      style = undefined
    }

    if (!style) {
      style = document.createElement('style')
      style.setAttribute('type', 'text/css')
      style.innerHTML = content
      document.head.appendChild(style)
    } else {
      style.innerHTML = content
    }
  }
  sheetsMap.set(id, style)
}

上述涉及的两个方法均被qiankun重写。

qiankun 重写了 document.head.appendChild 方法( patchHTMLDynamicAppendPrototypeFunctions ),在该方法被调用时内部调用qiankun的 css.process 函数给css 添加前缀。

qiankun 重写了 document.createELement 方法( patchDocumentCreateElement )并将 创建结果加过wekmap 中,在调用 document.head.appendChild 函数时会校验“是否是style标签” 和 “当前 style元素是否由重写的 document.createELement 创建”。

document.head.appendChild 部分代码如下:

 getOverwrittenAppendChildOrInsertBefore(opts: {}) {
     // 此处是 造成css样式中没有添加class前缀的原因
     if (!isHijackingTag(element.tagName) || !isInvokedByMicroApp(element)) {
       // ...
       return ...
     }
     css.process()

Document.prototype.createElement 部分代码如下:

// 是否是 style
if (isHijackingTag(tagName)) {

  // 是否有正在运行的 沙盒(runningApp)
  const {
    window: currentRunningSandboxProxy
  } = getCurrentRunningApp() || {};

  // 此处是 造成css样式中没有添加class前缀的原因
  if (currentRunningSandboxProxy) {
    const proxyContainerConfig = proxyAttachContainerConfigMap.get(currentRunningSandboxProxy);
    if (proxyContainerConfig) {
      elementAttachContainerConfigMap.set(element, proxyContainerConfig);
    }
  }
}

获取不到 runningApp 的主要原因有如下代码产生

  private registerRunningApp(name: string, proxy: Window) {
    if (this.sandboxRunning) {
      setCurrentRunningApp({
        name,
        window: proxy
      });
      // FIXME if you have any other good ideas
      // remove the mark in next tick, thus we can identify whether it in micro app or not
      // this approach is just a workaround, it could not cover all complex cases, such as the micro app runs in the same task context with master in some case
      // 在下一个事件循环中删除CurrentRunningApp,用于区分当前代码时执行在 microApp 中,还是macroApp 中
      // qiankun 代理了 window对象,每次获取window对象属性的时候 set CurrentRunningApp(proxySandbox.ts 文件中)
      nextTask(() => {
        setCurrentRunningApp(null);
      });
    }
  }

如下代码都会触发 Proxy 的代理,之所以子应用的部分vue 组件或页面的css样式能正确添加上class前缀,因为在该组件或页面中调用了 qiankunwindow.__POWERED_BY_QIANKUN__ 导致执行了Proxy的 get 拦截器,从而设置上了 setCurrentRunningApp

const proxyWindow = new Proxy(window, {
  get(target, p) {
    console.log('get');
    return Reflect.get(target, p);
  }
})

4.3.5、Popover样式无效

对话框或提示窗通过 document.body.appendChild 添加到bodyu元素上,由于css的样式在设置experimentalStyleIsolation的情况下会添加上class 前缀,导致对话框或提示窗等组件的样式丢失。

  1. 将公共样式文件提取到主应用;

  2. 将所有子应用的样式重写;

    加入以子应用名称开头的class 前缀,如 .fe-xxx-xxx ,在主应用中切换子应用时在body 上添加对应的class 名称。

4.4、跨域

html/css/js跨域

主应用获取子应用的html/css/js等资源,这些资源由子应用的开发服务器伺服。通过子应用开发服务器的默认配置即可解决。

子应用的XHR请求

主应用发送的子应用的请求,由子应用的开发服务器代理到测试环境。

例如, 主应用( http://test.suanshubang.cc:9000 ) 发送 子应用的请求( http://test.suanshubang.cc:3000 ),子应用的开发服务器再把请求在代理到 ships 环境 。报错如下:

Access to XMLHttpRequest at 'http://test.suanshubang.cc:3000/registrar/shop/info'
from origin 'http://test.suanshubang.cc:9000'
has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request'
s credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

加上如下配置即可解决。

cors: {
  origin: [/test\.suanshubang\.cc:\d+/],
  credentials: true,
},

子应用的mock请求

主应用发送子应用的请求,被mockjs 提供给子应用开发服务器的中间件拦截,并返回相应mock 数据。

例如:主应用( http://test.suanshubang.cc:9000 ) 发送 子应用的请求( http://test.suanshubang.cc:3100 ), 子应用内部使用 vite-plugin-mock插件处理请求返回。报错如下:

Access to XMLHttpRequest at 'http://test.suanshubang.cc:3100/api/financial/calendar?startDate=2021-11-03&endDate=2021-11-09&rightRange[]=1'
from origin 'http://test.suanshubang.cc:9000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin'
header is present on the requested resource.

原因: Vite-plugins-mock 中间件会执行 res.end() ,并重写response 的header头,导致在Vite-plugins-mock 插件前面的 跨域插件无效;

解决方式如下(重写mock 数据的response)。

Enhance mock data
function enhanceResponseFn(responseFn: IResponseFn): IResponseFn {
  return function(url, body, query, headers) {
    const res = this.res
    const req = this.req
    if (res) {
      res.setHeader('Access-Control-Allow-Credentials', 'true')
      res.setHeader(
        'Access-Control-Allow-Origin',
        req.headers.origin || DEV_ORIGIN.origin
      )
      res.setHeader(
        'Access-Control-Allow-Methods',
        'POST, GET, OPTIONS, DELETE, PUT'
      )
      res.setHeader(
        'Access-Control-Allow-Headers',
        'Origin, X-Requested-With, Content-Type, Accept, If-Modified-Since'
      )
    }
    return responseFn.call(this, url, body, query, headers)
  }
}

/**
 * 用于解决 mock 引起的跨域问题, 
 * vite-plugin-mock 会提前res.end 导致 主应用中调用当前子应用的mock 接口是出现跨域限制
 * @param mockData 
 * @returns 
 */
export function enhanceMockData(mockData: IMockDataList): IMockDataList {
  return mockData.map((data) => {
    const {
      url,
      method,
      response
    } = data
    return {
      url,
      method,
      response: enhanceResponseFn(response),
    }
  })
}

4.5、路由

路由守卫多次触发

点击子应用的导航菜单后,会导致当前模块的页面请求被取消;

  export const clearPending = () => {
    for (const [url, cancel] of pending) {
      cancel(url)
    }
    pending.clear()
  }
  service.interceptors.request.use((config: AxiosRequestConfig) => {
    removePending(config) // 在请求开始前,对之前的请求做检查取消操作
    addPending(config) // 
  })
  // 出错
  // 路由跳转执行两次
  router.beforeEach((to, from, next) => {
    console.log('子应用:router.beforeEach');
    clearPending()
    next()
  })

router添加base

应用中使用vue-router api 给router添加base会有问题

  1. 父应用的 router 钩子报警(执行两次,第一次报警),如 /#/micro-app-mis/store/list,报警 /store/list不存在;
  2. 子应用会重复挂载,执行 mounted 函数;

4.6、js隔离

localStorage隔离

项目中使用vuex-persistedstate实现 vuex 的持久化缓存,由于Localstorage 没有做隔离导致子应用间vuex缓存冲突。

customElements定义冲突

vite 在开发环境添加的服务报错 @vite/client ,阻断页面渲染 ,子应用vite版本从2.3.7 升级到2.6.10解决,报错信息如下,:

  overlay.ts: 185 Uncaught( in promise) DOMException: Failed to execute 'define'
  on 'CustomElementRegistry': the name "vite-error-overlay"
  has already been used with this registry
  at http: //test.suanshubang.cc:3000/static/fe-store-mis/@vite/client:178:16

比如 customElements.define() 函数在子应用和父应用创建同名的组件会导致报错。

  class PopUpInfo extends HTMLElement {
    constructor() {
      // 必须首先调用 super方法
      super();
    }
  }
  // 子应用中
  customElements.define('popup-info', PopUpInfo);
  // 父应用中
  customElements.define('popup-info', PopUpInfo);

  // VM34:7 Uncaught DOMException: Failed to execute 'define' on 'CustomElementRegistry': the name "popup-info" has already been used with this registry
  at < anonymous >: 7: 16(anonymous) @ VM34: 7

Apm 打点监控

在做主应用 apm “性能监控”和“用户行为分析”接入时,由于qiankun的隔离机制,当主应用中打开子应用时,主应用上报的apm相关数据中的“应用ID”会被重写为 子应用的“应用ID”,从而导致主应用上报数据失败。

目前处理方法暂时下掉子应用的apm “性能监控” 和 “用户行为分析”,同时有关“前端横向topic沟通”的MDIAN系统“性能数据”统计指标统一采用qiankun主应用的性能指标。

apm相关团队正在做“多实例”相关的工作,后续功能完善后可接入,即主应用和子应用只持有各自的apm实例,避免冲突。

4.7、资源加载

qiankun 是 single-spa 的一层封装,而 qiankun 中,真正去加载解析子应用的逻辑是在 import-html-entry 这个包中实现的。

参考之前的一片博客,🐶有趣的BUG——Last-Modified命中强缓存

4.8、其他

子应用拆分

业务关联紧密的功能单元应该做成一个微应用,反之关联不紧密的可以考虑拆分成多个微应用。 一个判断业务关联是否紧密的标准:看这个微应用与其他微应用是否有频繁的通信需求。

5、原理

5.1、子应用添加css前缀

qiankun 提供了一个实验性的样式隔离特性,当 experimentalStyleIsolation 被设置为 true 时,qiankun 会改写子应用所添加的样式为所有样式规则增加一个特殊的选择器规则来限定其影响范围。

qiankun将待转换的 styleNode 节点 挂载在临时的 swapNode 节点上 ,再运行 this.rewrite 添加class 前缀

挂载

function getOverwrittenAppendChildOrInsertBefore() {
  return function appendChildOrInsertBefore < T extends Node > () {
    switch (element.tagName) {
      case LINK_TAG_NAME:
      case STYLE_TAG_NAME: {
        // 这里进入
        css.process(mountDOM, stylesheetElement, appName);
      }
      return rawDOMAppendOrInsertBefore.call(mountDOM, stylesheetElement, referenceNode);
    }
  }
}

return rawDOMAppendOrInsertBefore.call(this, element, refChild);
};
}

CSS 对象

rawDocumentBodyAppend.call(document.body, styleNode)
this.swapNode = styleNode;
this.sheet = styleNode.sheet!;
this.sheet.disabled = true;

css.process

const textNode = document.createTextNode(styleNode.textContent || '');
this.swapNode.appendChild(textNode);
const sheet = this.swapNode.sheet as any; // type is missing
const rules = arrayify < CSSRule > (sheet?.cssRules ?? []);
const css = this.rewrite(rules, prefix);
// eslint-disable-next-line no-param-reassign
styleNode.textContent = css;

rewrite

private rewrite(rules: CSSRule[], prefix: string = '') {
  let css = '';

  rules.forEach((rule) => {
    if (rule.cssText.includes(`el-dialog__header`)) {
      console.log(css)
      debugger;
    }
    switch (rule.type) {
      case RuleType.STYLE:
        css += this.ruleStyle(rule as CSSStyleRule, prefix);
        break;
      case RuleType.MEDIA:
        css += this.ruleMedia(rule as CSSMediaRule, prefix);
        break;
      case RuleType.SUPPORTS:
        css += this.ruleSupport(rule as CSSSupportsRule, prefix);
        break;
      default:
        css += `${rule.cssText}`;
        break;
    }
    if (rule.cssText.includes(`el-dialog__header`)) {
      console.log(css)
      debugger;
    }
  });

  return css;
}

5.2、沙盒

  • LegacySandbox(单实例Proxy);
  • ProxySandBox(多实例Proxy,默认);
  • snapshotSanBox(window 快照,降级);

实现原理如下:

const proxyWindow = new Proxy(window, {
  get(target, p) {
    console.log('get');
    return Reflect.get(target, p);
  }
  // ...
})

;(function(window, self) {
  with(window) {
    // code
  }
}.bind(proxyWindow)(proxyWindow, proxyWindow))

部分源码(源码在import-grom-entry插件中):

// 生成code
function getExecutableScript(scriptSrc, scriptText, proxy, strictGlobal) {
  const sourceUrl = isInlineCode(scriptSrc) ? '' : `//# sourceURL=${scriptSrc}\n`;

  // 通过这种方式获取全局 window,因为 script 也是在全局作用域下运行的,所以我们通过 window.proxy 绑定时也必须确保绑定到全局 window 上
  // 否则在嵌套场景下, window.proxy 设置的是内层应用的 window,而代码其实是在全局作用域运行的,会导致闭包里的 window.proxy 取的是最外层的微应用的 proxy
  const globalWindow = (0, eval)('window');
  globalWindow.proxy = proxy;
  // TODO 通过 strictGlobal 方式切换 with 闭包,待 with 方式坑趟平后再合并
  return strictGlobal ?
    `;(function(window, self, globalThis){with(window){;${scriptText}\n${sourceUrl}}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);` :
    `;(function(window, self, globalThis){;${scriptText}\n${sourceUrl}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);`;
}

// 执行code
evalCode(scriptSrc, code);

6、性能

Vue2

单纯的脚本执行性能还可以,相差大概1-1.2倍,但是一旦涉及到操作 DOM 时,性能就会严重下降、

10000个dom节点,大概会差 10 倍以上。

100000个dom节点,大概会差 20 - 30 倍以上。

Vue3

基本没差距

<template>
  <div class="pt60">
    <ul class="wrapper">
      <li v-for="item in aaa" :key="item">{{ item }}</li>
    </ul>
    <button @click="test">test</button>
  </div>
</template>

<script>;
export default {
  data() {
    return {
      aaa: 1
    };
  },
  methods: {
    test() {
      console.time("run loop", 100000);

      for (let index = 2; index < 1 * 100000; index++) {
        this.aaa = index;
      }

      console.timeLog("run loop", 100000);

      this.$nextTick(() => {
        console.timeEnd("run loop", 100000);
      });
    }
  }
};
</script>

7、扩展

  • vue 的 <style> scoped 实现;
const moduleId = 'data-v-' + hash(isProduction ? (shortFilePath + '\n' + content) : shortFilePath)
// 相对路径 + 内容: 相对路径

8、参考