样式隔离
应用隔离:样式隔离
无论是刚才的快照沙箱还是另外的代理沙箱,其实都可以解决 JS 之间的副作用冲突。解决完 JS 之后,我们接下来解决的是 CSS 之间的冲突。在微前端框架里所面临的样式冲突器就两种:一种是主子应用样式冲突,你的主应用和你的子应用两者之间样式会发生冲突,另一种是子应用之间样式冲突,当你挂载了应用 A 又挂载了应用 B 的时候,这两个应用是平级的,它们之间样式也会发生冲突。
样式隔离:Dynamic Stylesheet
qiankun 所做的第一件事情其实是动态样式表。当你从子应用 A 切换到子应用 B 的时候,这个时候需要把子应用的样式表 A 的样式给删除,把子应用 B 的样式表给挂载。这样就避免了子应用 A 的样式和子应用 B 的样式同时存在于这个项目中,就做到了最基本的隔离。当你从应用 A 切到应用 B 的时候,你的样式也会自然的从应用 A 的样式切换到应用 B 的样式,我们会自动的帮你做应用样式的删除和加载。当然这样做只仅仅能够保证你在单应用模式下(就是同时只能有一个应用活跃的情况下)保证子应用和子应用之间的样式不会冲突。
当然动态样式表实现之后,我们还是没有解决主应用和主应用之间的样式冲突。实际上这个问题最好的手段还是通过一些工程化的手段来解,比如说 BEM,就是说大家约定一下,你应用 A 样式就统一加一个 a-
的前缀,应用 B 样式就统一加一个 b-
的前缀,你的框架应用也统一加个前缀。大家通过一些约定(比如约定大家都加上一个应用名的前缀)来避免冲突,这是非常有效的一个方案。尤其是主子应用之间的冲突,大部分情况下你只要保证主应用的样式做好改造,保证主应用的样式是很具象的规则,不会跟子应用冲突,那主子应用之间的冲突其实也解决了。不过这种方案终归是依赖约定,往往容易出纰漏。
样式隔离:工程化手段
当前 css module 其实非常成熟一种做法,就是通过编译生成不冲突的选择器名。你只要把你想要避免冲突的应用,通过 css module (在构建工具里加一些 css 预处理器即可实现)就能很简单的做到。css module 构建打包之后,应用之间的选择器名就不同了,也就不会相互冲突了。
css-in-js 也是一种流行的方案,通过这种方式编码的样式也不会冲突,这几个方案实现起来都不复杂,而且都非常行之有效。所以绝大部分情况下,大家手动用工程化手段处理一下主子应用之间的样式冲突,就可以解决掉样式隔离的问题。
说明 | 优点 | 缺点 | |
---|---|---|---|
BEM | 不同项目用不同的前缀/命名规则避开冲突 | 简单 | 依赖约定,这也是耦合的一种,容易出纰漏 |
CSS Modules | 通过编译生成不冲突的选择器名 | 可靠易用,避免人工约束 | 只能在构建器与打包工具 |
CSS-in-JS | CSS 和 JS 编码在一起,最终生成不冲突的选择器 | 基本彻底避免冲突 | 运行时开销,略缺失完整 CSS 能力 |
样式隔离:Shadow DOM
Web Components 的目标是减少单页应用中隔离 HTML,CSS 与 JavaScript 的复杂度,其主要包含了 Custom Elements, Shadow DOM, Template Element,HTML Imports,Custom Properties 等多个维度的规范与实现。Shadow DOM 它允许在文档(document)渲染时插入一棵 DOM 元素子树,但是这棵子树不在主 DOM 树中。因此开发者可利用 Shadow DOM 封装自己的 HTML 标签、CSS 样式和 JavaScript 代码。子树之间可以相互嵌套,对其中的内容进行了封装,有选择性的进行渲染。这就意味着我们可以插入文本、重新安排内容、添加样式等等。其结构示意如下:
简单的 Shadow DOM 创建方式如下:
<html>
<head></head>
<body>
<p id="hostElement"></p>
<script>
// 创建 shadow DOM
var shadow = document
.querySelector("#hostElement")
.attachShadow({ mode: "open" });
// 给 shadow DOM 添加文字
shadow.innerHTML = "<p>Here is some new text</p>";
// 添加CSS,将文字变红
shadow.innerHTML += "<style>p { color: red; }</style>";
</script>
</body>
</html>
我们也可以将 React 应用封装为 Custom Element 并且封装到 Shadow DOM 中:
import React from "react";
import retargetEvents from "react-shadow-dom-retarget-events";
class App extends React.Component {
render() {
return <div onClick={() => alert("I have been clicked")}>Click me</div>;
}
}
const proto = Object.create(HTMLElement.prototype, {
attachedCallback: {
value: function () {
const mountPoint = document.createElement("span");
const shadowRoot = this.createShadowRoot();
shadowRoot.appendChild(mountPoint);
ReactDOM.render(<App />, mountPoint);
retargetEvents(shadowRoot);
},
},
});
document.registerElement("my-custom-element", { prototype: proto });
Shadow DOM 的兼容性较差,仅在 Chrome 较高版本浏览器中可以使用。当然来到 qiankun2.0 之后,我们追加了一个新的选项,叫作严格样式隔离,不知道大家有没有使用过。
其实严格样是隔离代表 Shadow DOM。Shadow DOM 是可以真正的做到 CSS 之间完全隔离的,在 Shadow Boundary 这个阴影边界阻隔之下,主应用的样式和子应用的样式可以完完全全的切分开来。
但是绝大部分情况下,你还是不能无脑的开启严格样式隔离的。原因之前的同学也已经提到过一些了,比如说你在使用一些弹窗组件的时候(弹窗很多情况下都是默认添加到了 document.body)这个时候它就跳过了阴影边界,跑到了主应用里面,样式就丢了。又比方说你子应用使用的是 React 技术栈,而 React 事件代理其实是挂在 document 上的,它也会出一些问题。所以实践里当你开启 Shadow DOM 之后,当你的子应用可能会遇到一些奇怪的错误,这些错误需要你一一的去手动修复,这是比较累的一个过程。
我们提供了 Shadow DOM 这么一种样式隔离方式。但是实际使用中还是工程化的手段最为可靠、最为简单。当然在允许的情况下,大家还是可以去尝试开启严格样式隔离,毕竟这才是真正的隔离。
样式隔离 RFC:runtime css transformer
qiankun 还收到过一个提案,让我们可以动态运行时地去改变 css。如图,比如说原来子应用里面的样式是这个样子,那转换之后,我们可以把它前面加一个限定,我们规定只有在 data-qiankun-app1 这个子应用里面(这个 div 就是包裹子应用的最外层的容器), .main 这条样式才能真正的生效。通过这样一个运行时的转换,我们也能够达到样式隔离的目的。当然这个方案的问题在于这个转换不是那么好做,首先我们增加了一定的运行时开销,其次对于一些媒体查询,对于一些动画很可能会出一些意料之外的情况。