外观
CSS隔离
如果微应用和主应用在同一个 DOM 环境中,有如下思路可以避免样式污染:
- 对微应用的每一个 CSS 样式和对应的元素进行特殊处理,从而保证样式唯一性,例如 Vue 的 Scoped CSS
- 对微应用的所有 CSS 样式添加一个特殊的选择器规则,从而限定其影响范围
- 使用 Shadow DOM 实现 CSS 样式隔离
思路一和思路二,都需要遵循设计和编码规范,进行特殊处理。思路三是利用浏览器的标准来实现的CSS隔离,下面主要讨论实现细节。
Shadow DOM 隔离
Web Components技术
Shadow DOM是Web Components其中的一项技术。用于将封装的“影子”DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,你可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
影子 DOM 允许将隐藏的 DOM 树附加到常规 DOM 树中的元素上——这个影子 DOM 始于一个影子根,在其之下你可以用与普通 DOM 相同的方式附加任何元素。
示例
第一步,微应用中创建一个标签
javascript
// micro1.js
// MDN: https://developer.mozilla.org/zh-CN/docs/Web/Web_Components
// MDN: https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_custom_elements
class MicroApp1Element extends HTMLElement {
constructor() {
super();
}
// [生命周期回调函数] 当 custom element 自定义标签首次被插入文档 DOM 时,被调用
// 类似于 React 中的 componentDidMount 周期函数
// 类似于 Vue 中的 mounted 周期函数
connectedCallback() {
// 挂载应用
this.mount();
}
// [生命周期回调函数] 当 custom element 从文档 DOM 中删除时,被调用
// 类似于 React 中的 componentWillUnmount 周期函数
// 类似于 Vue 中的 destroyed 周期函数
disconnectedCallback() {
// 卸载处理
this.unmount();
}
mount() {
// MDN: https://developer.mozilla.org/zh-CN/docs/Web/API/Element/attachShadow
// 给当前自定义元素挂载一个 Shadow DOM
const $shadow = this.attachShadow({ mode: "open" });
const $micro = document.createElement("div");
$micro.textContent = "微应用1";
// 将微应用的内容挂载到当前自定义元素的 Shadow DOM 下,从而与主应用进行 DOM 隔离
$shadow.appendChild($micro);
}
unmount() {
// 这里可以去除相应的副作用处理
}
}
// MDN:https://developer.mozilla.org/zh-CN/docs/Web/API/CustomElementRegistry/define
// 创建自定义元素,可以在浏览器中使用 <micro-app-1> 自定义标签
window.customElements.define("micro-app-1", MicroApp1Element);第二步,主应用挂载上一步创建的自定义标签
javascript
class MicroAppManager extends UtilsManager {
micrpApps = [
// 应用名称
name: "micro1",
// 应用标识
id: "micro1",
// Web Components 方案
// 自定义元素名称
customElement: 'micro-app-1',
// 应用脚本(示例给出一个脚本,多个脚本也一样)
script: `http://${host}:${port.micro}/micro1.js`,
];
constructor() {
super();
this.init();
}
init() {
this.hashChangeListener();
}
loadStyle({ style, id }) {
return new Promise((resolve, reject) => {
const $style = document.createElement("link");
$style.href = style;
$style.setAttribute("micro-style", id);
$style.rel = "stylesheet";
$style.onload = resolve;
$style.onerror = reject;
// 动态 Script 方案
// document.body.appendChild($style);
// Web Components 方案
// 将微应用的 CSS 样式添加到可以隔离的 Shadow DOM 中
const $webcomponent = document.querySelector(`[micro-id=${id}]`);
const $shadowRoot = $webcomponent?.shadowRoot;
$shadowRoot?.insertBefore($style, $shadowRoot?.firstChild);
});
}
hashChangeListener() {
// Web Components 方案
// 微应用的插槽
const $slot = document.getElementById("micro-app-slot");
window.addEventListener("hashchange", () => {
this.microApps?.forEach(async (microApp) => {
// await 动态 Script
// Web Components 方案
const $webcomponent = document.querySelector(
`[micro-id=${microApp.id}]`
);
if (microApp.id === window.location.hash.replace("#", "")) {
// Web Components 方案
if (!$webcomponent) {
// 动态 Script 方案
// window?.[microApp.mount]?.("#micro-app-slot");
// Web Components 方案
// 下载并执行相应的 JS 后会声明微应用对应的自定义元素
// 在服务端的接口里通过 customElement 属性进行约定
const $webcomponent = document.createElement(
microApp.customElement
);
$webcomponent.setAttribute("micro-id", microApp.id);
$slot.appendChild($webcomponent);
// 将 CSS 插入到自定义元素对应的 Shadow DOM 中
this.loadStyle(microApp);
} else {
// Web Components 方案
$webcomponent.style.display = "block";
}
} else {
// 动态 Script 方案
// this.removeStyle(microApp);
// window?.[microApp.unmount]?.();
// Web Components 方案
$webcomponent.style.display = "none";
}
});
});
}
}事件隔离
Shadow DOM,它不仅仅可以做到 DOM 元素的 CSS 样式隔离,还可以做到事件的隔离处理。
React17委托事件
React 17 以下会使用 Document 进行事件委托处理,此时会因为拿不到 Shadow DOM 中的事件对象,而导致事件失效。为了解决类似的问题,React 17 不再使用 Document 进行事件委托,而是使用 React 挂载的 Root 节点进行事件委托
示例
javascript
class CustomElement extends HTMLElement {
constructor() {
super();
// Shadow Root: Shadow Tree 的根节点
const shadowRoot = this.attachShadow({ mode: "open" });
const $template = document.getElementById("custom-element-template");
// cloneNode:
// 克隆一个元素节点会拷贝它所有的属性以及属性值,当然也就包括了属性上绑定的事件 (比如 onclick="alert(1)"),
// 但不会拷贝那些使用 addEventListener() 方法或者 node.onclick = fn 这种用 JavaScript 动态绑定的事件。
shadowRoot.appendChild($template.content.cloneNode(true));
}
}
customElements.define("custom-element", CustomElement);