blog
blog copied to clipboard
保护样式王国不受侵犯
2016.12.7 日更新
现在可以使用 CSS 中的 all
属性解决这个问题,见 MDN,但这么做还得阻止宿主页面上注册的事件,例如 document.addEventListener('click', ...)
。
综合考虑下来,我还是觉得用 iframe 比较好。
对很多前端开发者来说,样式冲突是大部分人都会面对的问题。
何为样式冲突?
以我的划词翻译为例(我知道它最近出镜次数太多),我需要在每一个用户打开的网页里插入一段 HTML 用来显示翻译结果:
const box = document.createElement( 'div' );
box.textContent = '翻译结果!';
box.style.color = 'red';
document.body.appendChild( box );
按照我的预期,这段文字应该是红色的,但如果宿主网页有下面一段样式呢?
body { font-size: 50px; }
div { color: green !important; }
最终,文字会变成绿色,并且字会很大:这就是我所说的“样式冲突”。在划词翻译中,这都是我不得不解决的问题。
已有的解决方案
按照 DRY(Don't Repeat Yourself)原则,我先去网上搜索了已有的解决方案。
社区做了很多努力让 Web 组件成为可能(例如 Polymer),其中解决了一个问题就是让组件内的样式不会影响到使用此组件的网页(类似于 Scoped CSS)——但是,本文想要解决的问题正好相反:如何保护组件的样式不会受到宿主网页的影响。
剩下的唯有 <iframe>
了:不仅隔离了样式,还隔离了执行作用域。这确实能解决本文提出的问题,但也带来了更多的问题。
我的现有解决方案:自定义元素 + 样式覆盖
目前,我给所有 HTML 元素加上了自定义的前缀让它们变成了自定义元素,并覆盖掉了宿主里的可继承样式:
const box = document.createElement( 'my-div' );
box.style.display = 'block';
box.style.fontSize = '20px';
这确实能规避大部分的样式冲突,但它仍然不能抵挡来自可继承样式的冲突。另外,为了处理用户的输入,我还是得用到浏览器提供的表单元素(select
、input
、textarea
等),这部分元素正是样式冲突的重灾区。每当宿主上有一条样式会影响到我的表单元素,我都不得不新增一条样式盖过宿主样式。这幅画面就好像每当有一只矛刺过来的时候,我都不得不竖起一面盾来抵挡。
所以,目前最为完整的方案大概就是这样了:插入到宿主中的 HTML 片段全都使用自定义元素(包括表单元素),并且覆盖掉所有的可继承样式。最终,我的 HTML 片段看起来可能就会是这样:
<style>
/* 覆盖掉所有的可继承样式 */
app-container {
text-center: left;
font-size: 14px;
line-height: 14px;
/* and more... */
}
</style>
<template>
<!-- 使用自定义元素避免被宿主的 CSS 选择器匹配到 -->
<app-container>
<my-input></my-input>
</app-container>
</template>
看起来真是乱七八糟。
Shadow DOM 才是最好的解决方案吗?
类似于 <iframe>
,Shadow DOM 能隔离样式,但与 iframe
不同的是,Shadow DOM 内部仍然与宿主网页处于同一个 document
对象里。
我一度以为 Shadow DOM 会是最终的解决方案,但我发现仍然不够:它确实能隔离一般的样式,但如果是定义在宿主网页的 <body>
元素上的可继承样式,仍然会影响到 Shadow DOM 内部的元素样式。查看在线示例
换句话说,我可以使用 Shadow DOM 来隔离宿主对表单元素的样式,但我仍然要覆盖来自 <body>
的可继承样式。这种方式比起自定义元素 + 样式覆盖的方式并没有很大的改进。
所以,我可能仍然要一条一条样式的打补丁了。