1、前期回顾
1.1、什么是沙箱
在计算机安全中,沙箱(Sandbox)是一种用于隔离正在运行程序的安全机制,通常用于执行未经测试或者不受信任的程序或代码,它会为待执行的程序创建一个独立的执行环境,内部程序的执行不会影响到外部程序的运行。
1.2、常见的JS沙箱实现
要实现一个沙箱,其实就是去制定一套程序执行机制,在这套机制的作用下沙箱内部程序的运行不会影响到外部程序的运行。
- 基于作用域隔离,例如:function scope、with、
- 原生浏览器对象模拟,例如:基于Proxy的沙箱机制
- 天然的优质沙箱iframe
2、CSS 样式隔离
由于在微前端场景下,不同的技术栈的子应用会被集成到同一个运行池中,所以我们必须在框架层确保各个子应用之间不会出现样式互相干扰的问题。
常见的解决方案:
- 严格的命名约定
- CSS Module
- CSS-in-JS
- shadow DOM
2.1、qiankun样式隔离方案
在CSS隔离上,qiankun 提供了三种样式隔离的功能:宽松模式、严格模式、以及基于属性选择器实现的实验性样式隔离。
在start API的options中 sandbox 默认为 true 表示 宽松模式({loose:true}),此外他还提供了两个可选项 strictStyleIsolation:严格的样式隔离,experimentalStyleIsolation:实验性的样式隔离
sandbox - boolean | { strictStyleIsolation?: boolean, experimentalStyleIsolation?: boolean } - 可选,是否开启沙箱,默认为 true
默认情况下沙箱可以确保单实例场景子应用之间的样式隔离,但是无法确保主应用跟子应用、或者多实例场景的子应用样式隔离。当配置为 {strictStyleIsolation: true}
时表示开启严格的样式隔离模式。这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow DOM 节点,从而确保微应用的样式不会对全局造成影响。
除此之外,qiankun 还提供了 experimentStyleIsolation,当其被设置为 true 时,qiankun 会改写子应用所添加的所有样式规则增加一个特殊的选择器规则来限定其影响范围,因此改写后的代码会表达为如下的结构:
/** 假设应用名是 react16 **/
.app-main {
font-size: 14px;
}
div[data-qiankun-react16] .app-main {
font-size: 14px;
}
3、Web Components
Web Component 是一组浏览器标准和API,允许你创建可重用的定制元素并且在你的 web 应用中使用它们,旨在解决HTML、CSS和JavaScript的复用问题。它由三项主要技术组成,它们可以一起使用来创建封装功能的定制元素,可以在任何地方重用,不必担心代码冲突。
- Custom element(自定义元素):一组 JavaScript API,允许你定义 Custom elements 及其行为,然后可以在任何地方按需使用。
- Shadow DOM(影子DOM):一组 JavaScript API,用于将封装的 Shadow DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样他们就可以被脚本化和样式化,而不必担心与文档的其他部分发生冲突。
- HTML template(HTML模板):
<template>
和<slot>
元素使您可以在编写不在呈现页面中显示的标记目标。然后他们可以作为自定义元素结构的基础被多次使用。
其有四个生命周期函数:
- connectedCallback:当自定义元素第一次被挂载到文档DOM时被调用
- disconnectedCallback:当自定义元素与文档DOM断开连接时调用
- adoptedCallback:当自定义元素被移动到新文档时被调用(移动节点的本质就是先从文档树中删除节点,但是不销毁。然后再插入到文档树中,只要没有销毁重新调用constructor就是移动。一般从主DOM移动到iframe中)
- attributeChangeCallback:当自定义元素的一个属性被增加、移除或更改时被调用
4、定义一个Web Component
实现 web component 的基本方法通常如下所示:
- 创建一个类或函数来指定 Web 组件的功能。
使用 Element.attachShadow() 方法将一个Shadow DOM 附加到自定义元素上。使用通常的DOM方法向Shadow DOM中添加子元素、事件监听器等等
注:可以使用<template>
和<slot>
定义一个HTML模板,简化书写。 - 使用 CustomElementRegistry.define() 方法注册您的新定义元素,并向其传要定义的元素名称、指定元素功能的类、以及可选的其所继承自的元素。
- 像使用常规HTML元素那样在页面任何位置使用自定义元素。
<!-- index.html -->
<head>
<meta charset="utf-8">
<title>Life cycle callbacks test</title>
<style>
custom-square {
margin: 20px;
}
</style>
<script defer src="main.js"></script>
</head>
<body>
<h1>Life cycle callbacks test</h1>
<div>
<button class="add">Add custom-square to DOM</button>
<button class="update">Update attributes</button>
<button class="move">move custom-square </button>
<button class="remove">Remove custom-square from DOM</button>
</div>
</body>
</html>
/** main.js **/
// Create a class for the element
class Square extends HTMLElement {
// Specify observed attributes so that
// attributeChangedCallback will work
static get observedAttributes() {
return ["c", "l"];
}
constructor() {
// Always call super first in constructor
super();
const shadow = this.attachShadow({ mode: "open" });
const div = document.createElement("div");
const style = document.createElement("style");
shadow.appendChild(style);
shadow.appendChild(div);
}
connectedCallback() {
console.log("Custom square element added to page.");
updateStyle(this);
}
disconnectedCallback() {
console.log("Custom square element removed from page.");
}
adoptedCallback() {
console.log("Custom square element moved to new page.");
}
attributeChangedCallback(name, oldValue, newValue) {
console.log("Custom square element attributes changed.");
updateStyle(this);
}
}
customElements.define("custom-square", Square);
function updateStyle(elem) {
const shadow = elem.shadowRoot;
shadow.querySelector("style").textContent = `
div {
width: ${elem.getAttribute("l")}px;
height: ${elem.getAttribute("l")}px;
background-color: ${elem.getAttribute("c")};
}
`;
}
const add = document.querySelector(".add");
const update = document.querySelector(".update");
const move = document.querySelector(".move");
const remove = document.querySelector(".remove");
let square;
update.disabled = true;
move.disabled = true;
remove.disabled = true;
function random(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
add.onclick = function () {
// Create a custom square element
square = document.createElement("custom-square");
square.setAttribute("l", "100");
square.setAttribute("c", "red");
document.body.appendChild(square);
update.disabled = false;
move.disabled = false;
remove.disabled = false;
add.disabled = true;
};
update.onclick = function () {
// Randomly update square's attributes
square.setAttribute("l", random(50, 200));
square.setAttribute(
"c",
`rgb(${random(0, 255)}, ${random(0, 255)}, ${random(0, 255)})`
);
};
move.onclick = function () {
const oIframe = document.createElement("iframe");
oIframe.srcdoc = `<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iframe</title>
</head>
<body>
</body>
</html>`;
document.body.appendChild(oIframe);
oIframe.onload = () => {
const _container = oIframe.contentDocument.querySelector("body");
// 移动节点的本质就是
// 先从文档树中删除节点,但是不销毁
// 然后再插入到文档树中
// 只要没有销毁 重新调用 constructor 就是移动
_container.append(square);
};
};
remove.onclick = function () {
// Remove the square
document.body.removeChild(square);
update.disabled = true;
move.disabled = true;
remove.disabled = true;
add.disabled = false;
};
5、Web Components 优点
5.1、原生支持
原生支持意味着可以不需要任何框架即可完成开发,同时也意味着它可以在任何框架中使用。由于其有w3c标准做背书,所以它有更稳定的迭代前景,且其使用方式与原生元素一样。在使用时无需大刀阔斧的颠倒现有的逻辑体系,以及无需依赖组件的依赖库,如:在使用基于React的组件时,需要依赖React库,而使用Web Component自定义元素则无额外依赖项。
5.2、组件隔离
Shadow DOM 为自定义元素提供了包括 CSS、事件的有效隔离,不再担心不同的组件之间的样式、事件污染了。这相当于为自定义组件提供了一个天然有效的保护伞。
Shadow DOM 实际上是一个独立的子 DOM tree,通过有限的接口和外部发生作用,即它是游离在主 DOM 树之外的节点树。所以 Web Components 中的样式计算不会跨越 Shadow DOM 范围,仅在单个的 Web Component 中进行,而不是在整个页面的 DOM 树上进行。我们都知道页面中的 DOM 节点数量越多,运行时性能将会越差,以及对 CSS 的隔离也将加快选择器的匹配速度。
6、Web Components 劣势
- Web Components 和其他原生元素一样,偏向于 UI 层面,与现在的主流前端框架的数据驱动不符,和现在的组件库能力上相比功能会比较弱。
- 兼容性有待提升,一是浏览器兼容性;二是框架兼容性。(qiankun 和 micro-app 中都提到了如果开启 Shadow DOM 会有兼容性问题)。
- 开发成本较高,使用原生方式书写,且为命令式编程。
7、基于 Web Components 的微前端框架 - mirco-app
在 micro-app 之前,业内已经有一些开源的微前端框架,比较流行的有2个:single-spa 和 qiankun。
single-spa 是通过监听 url change 事件,在路由变化时匹配到渲染的子应用并进行渲染,这个思路也是目前实现微前端的主流方式。同时single-spa 要求子应用修改渲染逻辑并暴露出三个方法:bootstrap、mount、unmount,分别对应初始化、渲染和卸载,这也导致子应用需要对入口文件进行修改。因为 qiankun 是基于 single-spa 进行封装,所以这些特点也被 qiankun 继承下来,并且需要对 webpack 配置进行一些修改。
micro-app 并没有沿袭 single-spa 的思路,而是借鉴了 WebComponent 的思想,通过 CustomElement 结合自定义的ShadowDom,将微前端封装成一个类 WebComponent 组件,从而实现微前端的组件化渲染。并且由于自定义 ShadowDom 的隔离特性,micro-app 不需要像 single-spa 和 qiankun 一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改 webpack 配置,是目前市面上接入微前端成本最低的方案。
参考:
Web Components MDN 介绍
Web Components MDN 例子
micro-app 官网
qiankun 官网