一、前言
在富文本编辑器的使用过程中,是否常常因为全局 CSS
污染的问题让你很难受,并且在这个点上你了解到了 shadow DOM
,你多半会有把富文本放在 shadow DOM
中的可爱想法。
二、shadow DOM 和 shadowRoot
在文章正式开始之前,我们需要先了解下什么是 shadow DOM
。
shadow DOM
的挂载点 ShadowRoot
。
我简单总结了下我了解的一些 shadow
特点:
1. shadowRoot 相当于一个阉割的 document,但它只有独立的 css 作用域而没有独立的 js 作用域
独立的 css 域怎么理解呢?其实就相当于给 shadow DOM
内的所有元素都设置了 all: initial
css 样式。内部元素的样式在未单独做修饰的情况向,就是浏览器默认样式,完全不受外部样式的影响。
没有独立的 js 域?shadowRoot
与 document
访问到的是同一个 window
对象,并且 shadowRoot 内部是不支持 <script src="xxx.js"></script>
的形式进行 js 引入的。
2. MutationObserver 监听不到 shadow DOM 的变化
比如:我们定义了一个自定义元素 <is-editable>
并开启了 shadowRoot
,只要 <is-editable>
本身没有发生变化,无论其内部的 shadow DOM
如何变化都不会触发 MutationObserver
。就像 <video controls></video>
一样,无论你怎么拖进度条都不会触发 MutationObserver
一样的道理。
如果你想查看 video 的 shadow DOM,你可以打开控制台,然后打开设置,然后勾选
Show user agent shadow DOM
即可。
当然,这里说的监听不到是指无法从外部进行监听,如果你能直接拿取到 shadow DOM
中的元素进行 MutationObserver 绑定,那也是可以监听到变化的
试验代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/m-observer@0.3.1/dist/index.min.js" type="text/javascript" charset="utf-8"></script>
</head>
<body>
<div class="demo">
<h3>shadowRoot 中的编辑区</h3>
<is-editable></is-editable>
<h3>.demo 中的编辑区</h3>
</div>
<!-- 使用 <template> 构建 shadow DOM 模板 -->
<template id="tpl">
<style type="text/css">
:host {
display: inline-block;
width: 100%;
margin: 0 0 15px 0;
}
.editable {
border: 1px solid #999999;
box-sizing: border-box;
padding: 5px 10px;
}
.editable:focus {
outline: none;
}
</style>
<div class="editable" contenteditable="true">
<h3>标题三,<code>h3</code></h3>
<p>常用正文,<b>加粗</b>,<i>斜体</i></p>
<p>常用正文,<b>加粗</b>,<i>斜体</i></p>
</div>
</template>
<script type="text/javascript">
const tpl = document.querySelector('#tpl')
const demo = document.querySelector('.demo')
demo.append(tpl.cloneNode(true).content)
MObserver.observeAll('.demo', function() {
console.log('外部的 MutationObserver 监听');
})
</script>
<script type="text/javascript">
customElements.define('is-editable', class EditableElement extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: "open" })
// 找到 <template> 模板
const tpl = document.querySelector('#tpl')
// 克隆模板并将模板内容添加到当前 shadowRoot 中
this.shadowRoot.append(tpl.cloneNode(true).content)
}
mutationCallback() {
console.log('内部的 MutationObserver 监听');
}
// 当自定义元素第一次被连接到文档DOM时被调用
connectedCallback() {
const target = this.shadowRoot.querySelector('.editable')
MObserver.observeAll(target, this.mutationCallback)
}
// 当自定义元素与文档DOM断开连接时被调用
disconnectedCallback() {
const target = this.shadowRoot.querySelector('.editable')
MObserver.remove(target, this.mutationCallback)
}
})
</script>
</body>
</html>
试验结果
三、CSS 加载
在前文中我们提到 shadowRoot
是有独立的 CSS
作用域的,那么每一个自定义元素下的 shadow DOM
相关的 CSS
我们都需要进行相关依赖的管理。
例如:我们需要将 katex
封装成自定义元素,那么它的 js 部分我们可以直接用 webpack
打包或用 CDN
直接在 document
中引入,而它的 CSS
部分我们则需要手动进行管理
katex 示例
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<script src="https://cdn.jsdelivr.net/npm/m-observer@0.3.1/dist/index.min.js" type="text/javascript" charset="utf-8"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.13.13/dist/katex.min.js" integrity="sha384-pK1WpvzWVBQiP0/GjnvRxV4mOb0oxFuyRxJlk6vVw146n3egcN5C925NCP7a7BY8" crossorigin="anonymous"></script>
</head>
<body>
<template id="tpl">
<!-- <link> 引入 css -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.13.13/dist/katex.min.css" integrity="sha384-RZU/ijkSsFbcmivfdRBQDtwuwVqK7GMOw6IMvKyeWL2K5UAlyp6WonmB8m7Jd0Hn" crossorigin="anonymous">
<div class="root"></div>
</template>
<script>
customElements.define('is-katex', class KatexElement extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: "open" })
const tpl = document.querySelector('#tpl')
this.shadowRoot.append(tpl.cloneNode(true).content)
}
render() {
const text = this.getAttribute('data-text')
katex.render(text, this.shadowRoot.querySelector('.root'))
}
// 当自定义元素第一次被连接到文档DOM时被调用
connectedCallback() {
this.render()
MObserver.attributeFilter(this, this.render, ['data-text'])
}
// 当自定义元素与文档DOM断开连接时被调用
disconnectedCallback() {
MObserver.remove(this, this.render)
}
})
</script>
</body>
</html>
而富文本编辑器如果考虑了扩展性进行了插件化设计,那么我们就无法简单的引入方式将无法达到需求,因此我们需要对 CSS
的依赖进行一套系统的管理。
在之前的尝试中,为此设计了一套简单的 CSS
依赖加载机制,有兴趣的可以翻源码,详解放在此处篇幅就有点长了
同时,值得注意的是,如果你在 shadowRoot
下使用的依赖库,它的 CSS
是放在 js
中的,而不是独立的 .css
文件,那么你可以寻找替换库了。因为这种库会直接把 css
加载到 document
中,而且你拿取不到!
四、兼容处理 selection
还不知道
Selection
?看这里
如果你的富文本编辑器依赖于浏览器选区的支持,那肯定逃不过这部分的坑!
在 Chromium
内核浏览器的 shadowRoot
中,我们是没法通过 document.getSelectin
或 window.getSelection
拿取到有效选区的,我们只能通过 shadowRoot.getSelection
来拿取有效的选区。
但是在其它浏览器上,无论是在 document
中还是在 shadowRoot
中,我们都必须使用 document.getSelectin
或 window.getSelection
来获取当前的有效选区。
试验代码
<!DOCTYPE html>
<html>
<body>
<is-editable></is-editable>
<script type="text/javascript">
customElements.define('is-editable', class EditableElement extends HTMLElement {
constructor() {
super()
const shadow = this.attachShadow({ mode: 'open'})
shadow.innerHTML = `
<div class="root">
<p><i>计算机的</i>分开了的<del>据库看圣诞节分</del><u>就开始减肥</u>分开了束带结发来看</p>
<p>健身卡积分<b>接口数据的反馈了世界上的</b>借口了决定是否了</p>
<p>减税降费积分开始了</p>
</div>`
}
})
document.addEventListener('mouseup', function() {
// 使用 document.getSelection 获取选区
console.log(`document.getSelection().getRangeAt(0)`);
console.log(document.getSelection().getRangeAt(0));
// 使用 shadowRoot.getSelection 获取选区
const el = document.querySelector('is-editable')
console.log(`el.shadowRoot.getSelection().getRangeAt(0)`);
console.log(el.shadowRoot.getSelection().getRangeAt(0));
})
</script>
</body>
</html>
试验结果
结语
之前的研究就这么多,更深入的细节就只有靠各位自己啦。