阿里云开放平台微前端方案的沙箱实现

本文涉及的产品
传统型负载均衡 CLB,每月750个小时 15LCU
应用型负载均衡 ALB,每月750个小时 15LCU
EMR Serverless StarRocks,5000CU*H 48000GB*H
简介: 微前端已经成为前端领域如今比较火爆的话题,关于微前端价值的讨论,可以参考克军的《拥抱云时代的前端开发框架——微前端》。微前端在技术方面,有一个始终绕不过去话题就是前端沙箱。本篇具体探讨一下,在微前端领域如何实现前端沙箱。

作者 | 流司

image.png

微前端已经成为前端领域如今比较火爆的话题,关于微前端价值的讨论,可以参考克军的《拥抱云时代的前端开发框架——微前端》。微前端在技术方面,有一个始终绕不过去话题就是前端沙箱。本篇具体探讨一下,在微前端领域如何实现前端沙箱。

背景

应用沙箱可能是微前端技术体系里面最有意思的部分。一般来说沙箱是微前端技术体系中不是必须要做的事情,因为如果规范做的足够好,是能够避免掉一些变量冲突读写,CSS 样式冲突的情况。但是如果你在一个足够大的体系中,总不能仅仅通过规范来保证应用的可靠性,还是需要技术手段去治理运行时的一些冲突问题,这个也是沙箱方案成为微前端技术体系的一部分原因。

首先纵观各类技术方案,有一个大前提决定了这个沙箱如何做:最终微应用是 单实例 or 多实例 存在宿主应用中。这个直接决定了这个沙箱的复杂度和技术方案。

  • 单实例:同一个时刻只有一个微应用实例存在,此刻浏览器所有浏览器资源都是这个应用独占的,方案要解决的很大程度是应用切换的时候的清理和现场恢复。比较轻量,实现起来也简单。
  • 多实例:资源不是应用独占,就要解决资源共享的情况,比如路由,样式,全局变量读写,DOM. 可能需要考虑的情况比较多,实现较为复杂。

最开始我们的想法是:

从业务场景:我们可能存在的情况是当用户操作一个产品 A 的同时和另一个产品 B 发生了关联操作,需要唤醒应用 B 做操作。虽然从产品维度可以规避掉,比如先切到 B, 然后切回 A, 但是从某种程度上因为技术的原因,我们限制了产品交互的发挥。

从技术角度:解决了多实例当然单实例的场景也不在话下,并且单实例的方案某种程度上给编码上带来了一定复杂度,比如业务代码需要自己做业务上下文的切换。

最近 qiankun 2 也转变了思路从单实例的支持到开始支持多实例,多多少少也侧面说明了,多实例是个一个值得投入和技术攻克的场景。

基于上面的考量,我们就开始了我们 Browser VM 沙箱的实现探索。总结起来可以用下图表示:

image.png

JavaScript 沙箱实现

沙箱环境构造

要实现沙箱,我们需要隔离掉浏览器的原生对象,但是如何隔离,建立一个沙箱环境呢?Node 中 有 vm 模块,来实现类似的能力,但是浏览器就不行了,但是我们可以利用了闭包的能力,利用变量作用域去模拟一个沙箱环境,比如下面的代码:


function foo(window) {
  console.log(window.document);
}

foo({
  document: {};
});

比如这段代码的输出一定是 {}. 而不是原生浏览器的 document.

所以 ConsoleOS 实现了一个 wepback 的插件在应用代码构建的时候给子应用代码加上一层 wrap 代码,创建一个闭包,把需要隔离的浏览器原生对象变成从下面函数闭包中获取的,从而我们可以在应用加载的时候,传入模拟掉的 window,document 之类的对象。


// 打包代码
__CONSOLE_OS_GLOBAL_HOOK__(id, function (require, module, exports, {window, document, location, history}) {
  /* 打包代码 */
})
function __CONSOLE_OS_GLOBAL_HOOK__(id, entry) {
  entry(require, module, exports, {window, document, location, history})
}

当然也可以不靠工程化的手段来实现,也可以通过请求脚本,然后在运行时拼接这段代码,然后eval 或者 new Function, 来达到相同的目的。

原生对象模拟

沙箱隔离能力有了,剩下的问题就是如何实现这一堆浏览器的原生对象了。最开始的想法是我们根据 ECMA 的规范实现(现在仍然有类似的想法),但是发现成本太高。不过在我们各种实验之后,发现了一个很“取巧”的做法,我们可以 new iframe 对象,把里面的原生浏览器对象通过 contentWindow 取出来,应为这些对象天然隔离,就省去了自己实现的成本。

const iframe = document.createElement( 'iframe' );

当然里面有很多的细节需要考量,比如:

只有同域的 iframe 才能取出对应的的 contentWindow. 所以需要提供一个宿主应用空的同域URL来作为这个 iframe 初始加载的 URL. 当然根据 HTML 的规范 这个 URL 用了 about:blank 一定保证保证同域,也不会发生资源加载,但是会发生关联的 和 这个iframe 中关联的 history 不能被操作,这个时候路由的变换只能变成 hash 模式。

如下图所示,我们取出对应的 iframe 中原生的对象之后,就会对特定需要隔离的对象生成对应的 Proxy, 然后对一些属性获取和属性设置,坐上一些特定的设置,比如 window.document 需要返回特定的沙箱 document 而不是当前浏览器的document.


class Window {
  constructor(options, context, frame) {
    return new Proxy(frame.contentWindow, {
      set(target, name, value) {
        target[name] = value;
        return true;
      },

      get(target, name) {
        switch( name ) {
          case 'document':
            return context.document;
          default:
        }

        if( typeof target[ name ] === 'function' && /^[a-z]/.test( name ) ){
          return target[ name ].bind && target[ name ].bind( target );
        }else{
          return target[ name ];
        }
      }
    });
  }
}

对于每一个对象的实现这里不讲细节了,有兴趣可以看看我们的开源之后的代码

但是为了文档能够被加载在同一个 DOM 树上,对于 document, 大部分的 DOM 操作的属性和方法还是直接用的宿主浏览器中的 document 的属性和方法。

由于子应用有自己的沙箱环境,之前所有独占式的资源现在都变成了应用独享(尤其是 location, history),所以子应用也能同时被加载. 并且对于一些变量的我们还能在 proxy 中设置一些访问权限的事情,从而限制子应用的能力,比如 Cookie, LocalStoage 读写。

当这个 iframe 被移除时,写在 window 的变量和设置的一些 timeout 时间也会一并被移除。(当然 DOM 事件需要沙箱记录,然后在宿主中移除)。

总结一下,我们的沙箱可以做到如下的特性:

image.png

CSS 隔离

CSS 隔离方案相对来说比较常规,常见的有:

  • CSS Module
  • 添加 css 的 namespace
  • Dynamic StyleSheet
  • Shadow DOM

CSS Module or CSS Namespace: 通过修改基础组件样式前缀来实现框架和微应用依赖基础组件样式的隔离性(依赖于工程上 CSS 的预处理器编译和运行时基础组件库配置),同时避免全局样式的书写(依赖于约定或工程 lint 手段)

Dynamic StyleSheet: 隔离方式是通过js运行时动态加载卸载微应用样式表来避免样式的冲突,局限性一是在于站点框架本身或其部件(header/menu/footer)与当前运行的微应用间仍存在样式冲突的可能性,二是没有办法支持多个微应用同时运行显示的情况

Shadow DOM: 优点是浏览器级别提供的样式隔离能力,可以做到完全隔离。缺点在于,目前兼容性还是够不太好,并且改造会涉及到旧应用的业务代码的改造,对子应用侵入性比较高。

最终经过实践,我们选择的方式是 CSS Module + 添加 CSS 的 namespace。CSS module 保证的是应用业务样式不冲突,Namespace 保证公共库不冲突。我们实现了一个 postcss 插件,会在应用构建的时候给所有的样式都加上应用前缀包括应用公共库的 CSS。(这样方便做到同一个 组件库 新旧版本样式的兼容)。如下图所示:


// 宿主 host app
.next-btn {
  color: #eee;
}

// 子应用 sub app
aliyun-slb .next-btn {
  color: #eee;
}

//宿主中生成的节点
<aliyun-slb>
  <!-- 子应用的节点 -->
</aliyun-slb>

这样实现的好处在于:

  • 每个应用都有 namespace, 可以多实例共存。
  • 不依赖特定的 CSS 预处理器。
  • 对于同一个库不同版本的 CSS(如 fusion1 和 fusion2), 可以做到彻底隔离。
  • 鉴于上面 JS 沙箱的存在,对于一些弹窗类的组件,这个微应用获取的 body 实际上是宿主生成的节点,所以弹窗会被添加到微应用的节点(也就是上面的 aliyun-slb)这个节点,样式不会失效。

不过也会有一些问题,比如:

  • 嵌套应用组件样式优先级的问题。(由于 CSS module 的存在,一般只会发生在公共 css 样式中,这个就是只能尽量避免嵌套)。
  • fusion 不同版本库公用字体的问题。(目前解法比较hack, 工程化手段替换掉了 next 字体的名字)。

如何和其他体系结合

一般来说,所以如果看完上面的文章,觉得这个沙箱方案不错,但是又有自己的微前端体系了,想套用咋办?

目前 ConsoleOS 的代码已经在 Github 上开源,这里不妨可以尝试试用一下。

JS 沙箱部分

如果看懂了上面关于原理的介绍可以看到其实沙箱实现包括两个层面

  • 原生浏览器对象的模拟(Browser-VM)的部分
  • 如何构建一个闭包环境的部分。

Browser-VM 可以直接用起来,这部分完全是通用普适的。但是涉及到闭包构建的这部分,每个微前端体系不太一致,可能需要改造比如:


import { createContext, removeContext } from '@alicloud/console-os-browser-vm';

const context = await createContext();

const run = window.eval(`
  (() => function({window, history, locaiton, document}) {
    window.test = 1;
  })()
`)
run(context);

console.log(context.window.test);
console.log(window.test);

// 操作虚拟化浏览器对象
context.history.pushState(null, null, '/test');
context.locaiton.hash = 'foo'

// 销毁一个 context
await removeContext( context );

当然可以直接选择沙箱提供好的 evalScripts 方法:

import { evalScripts } from '@alicloud/console-os-browser-vm';

const context = evalScripts('window.test = 1;')

console.log(window.test === undefined) // true

CSS 沙箱

如果用webpack 构建,可以直接配置如下:


const postcssWrap = require('@alicloud/console-toolkit-plugin-os/lib/postcssWrap')

// 下面是 webpack config
{
  test: /\.css$/,
  use: [
    'style-loader',
    {
      loader: 'postcss-loader',
      options: {
        plugins: [
          // 加入插件
          postcssWrap({
            stackableRoot: '.prefix',
            repeat: 1
          })
        ],
      },
    },
    'css-loader',
  ],
  exclude: /^node_modules$/,
}

Try Live Demo

与其干巴巴的看文章,直接试试在线 Demo 可能更有体感一些:

http://github.com/aliyun/alibabacloud-console-os#try-live-demo

或者试试生产环境的例子, 阿里云企业工作台 - 工具应用中心 , 是阿里云集成三方应用提供云管能力。这里每个应用都是一个微应用,里面涵盖了 React, Vue, Angular 三大技术栈。

image.png


image.png
关注「Alibaba F2E」
把握阿里巴巴前端新动向

相关实践学习
SLB负载均衡实践
本场景通过使用阿里云负载均衡 SLB 以及对负载均衡 SLB 后端服务器 ECS 的权重进行修改,快速解决服务器响应速度慢的问题
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
相关文章
|
1天前
|
前端开发 安全 JavaScript
在阿里云快速启动Appsmith搭建前端页面
本文介绍了Appsmith的基本信息,并通过阿里云计算巢完成了Appsmith的快速部署,使用者不需要自己下载代码,不需要自己安装复杂的依赖,不需要了解底层技术,只需要在控制台图形界面点击几下鼠标就可以快速部署并启动Appsmith,非技术同学也能轻松搞定。
|
9天前
|
前端开发 数据可视化 搜索推荐
深入剖析极态云优雅的前端框架设计方案(上)
最近在体验极态云,这款低代码软件开发产品,发现其前端框架设计方案很优雅很强大! 在接下来的学习过程中,我将持续输出自己对极态云前端框架设计方案的深入理解,包括具体的使用技巧、优势分析以及可能的应用场景等方面的内容,希望能为大家提供有价值的参考。
|
24天前
|
缓存 前端开发 UED
前端 8 种图片加载优化方案梳理
本文首发于微信公众号“前端徐徐”,详细探讨了现代网页设计中图片加载速度优化的重要性及方法。内容涵盖图片格式选择(如JPEG、PNG、WebP等)、图片压缩技术、响应式图片、延迟加载、CDN使用、缓存控制、图像裁剪与缩放、Base64编码等前端图片优化策略,旨在帮助开发者提升网页性能和用户体验。
141 0
|
2月前
|
Web App开发 前端开发 JavaScript
Web前端项目的跨平台桌面客户端打包方案之——CEF框架
Chromium Embedded Framework (CEF) 是一个基于 Google Chromium 项目的开源 Web 浏览器控件,旨在为第三方应用提供嵌入式浏览器支持。CEF 隔离了底层 Chromium 和 Blink 的复杂性,提供了稳定的产品级 API。它支持 Windows、Linux 和 Mac 平台,不仅限于 C/C++ 接口,还支持多种语言。CEF 功能强大,性能优异,广泛应用于桌面端开发,如 QQ、微信、网易云音乐等。CEF 开源且采用 BSD 授权,商业友好,装机量已超 1 亿。此外,GitHub 项目 CefDetector 可帮助检测电脑中使用 CEF
294 3
|
3月前
|
缓存 前端开发 JavaScript
前端性能优化方案
【8月更文挑战第15天】前端性能优化方案
36 2
|
2月前
|
编解码 前端开发 JavaScript
前端移动端适配方案
【9月更文挑战第8天】前端移动端适配方案
86 0
|
4月前
|
缓存 前端开发 JavaScript
前端常见的性能优化方案?
【7月更文挑战第14天】前端性能优化包括代码压缩、资源合并、图片优化、缓存策略和DOM操作改进。例如,压缩CSS、JS,懒加载图片,使用CDN,事件委托,启用HTTP/2,及利用性能工具进行评估和优化。目标是加快加载速度,提升用户体验和服务器效率。
73 2
|
4月前
|
缓存 前端开发 JavaScript
前端性能优化都有那些方案 ?
【7月更文挑战第11天】 前端性能优化包括资源合并压缩、懒加载、CDN使用、代码优化、缓存利用和图片优化等策略。例如,减少HTTP请求、压缩CSS/JS、事件委托、利用浏览器及服务器缓存、选择合适图片格式等,旨在提升网页速度和用户体验。服务工作者、异步加载和响应式设计也是关键。持续学习新技术以适应不断变化的优化需求。
69 1
|
3月前
|
存储 开发框架 前端开发
EAV模型(实体-属性-值)的设计和低代码的处理方案(3)-- 实体属性定义及前端列表展示和数据录入处理
EAV模型(实体-属性-值)的设计和低代码的处理方案(3)-- 实体属性定义及前端列表展示和数据录入处理
|
4月前
|
存储 前端开发 JavaScript
前端菜单及按钮权限拦截,实现方案及思路
此实现方案基于vue框架,并需要依赖vue项目相关的库,router、store等等;前端同学要与后端同学协商,常规是让后端返回一个树结构的菜单数据,并且将所有的涉及权限控制的页面path给到后端,如果是按钮,需要把所有的按钮 code 码统一下,这是前期工作,很重要。
下一篇
无影云桌面