使用Preact 开发基于Shadow DOM的JS插件

647 阅读4分钟

前言

第三方JS插件在日常开发中经常会使用到。对于一些不涉及到展示的功能插件,仅需要引入一个js文件即可,但对于一些界面级插件,如轮播图、富文本编辑器等,往往还需要单独引入css文件使之展示正常。但由于CSS存在覆盖问题,即使遵循某些规范(如BEM),仍然不可忽视。

如果可以仅引入一个js文件,并且插件样式能完全做到与主体应用隔离,那么插件的通用性也能进一步提高。Shadow DOM无疑是一个极具诱惑的选择(还不了解Shadow DOM的同学可以看这👉Using Shadow DOM),非常契合需求。

此外,Shadow DOM也可以和MVVM框架整合,这也是本文所要介绍的。

为何使用Preact

MVVM框架的流行,在一定程度上已经影响了前端开发者的思考模式,我们不再以命令式的方式操作DOM,而是交由框架完成,极大提高了开发效率。

如果你手里有一把锤子,所有东西看上去都像钉子。MVVM框架当然也可以用来开发界面级JS插件,甚至会使事情变得更加简单。

JS插件一般都是轻量的。相比于使用React,Preact更符合我们的要。Preact是React的轻量级替代方案,体积仅有3kB,并且拥有与React相同的API(官网如是说)。关于它的更多优点以及与React的差异性,都可以在官网了解到,在此不再赘述。

开发过程

Preact提供了脚手架工具,并且也能与其他构建工具如Webpack、Rollup等整合。可以参考:preactjs.com/guide/v10/g… 。此处强烈建议使用Rollup😊

而如何将Preact与Shadow DOM结合,其思路与笔者之前写到的使用 Webpack 🔧 构建 Shadow DOM 组件异曲同工,有所不同的是,我们不需要再将HTML部分分离,借助JSX,可以做到all in JS,这也是我们选择Preact的重要原因之一。

在React或者Vue项目中,通常的做法是选择一个根节点,然后将根组件挂载至根节点上。如下:

import * as ReactDOM from 'react-dom'

ReactDOM.render(<App />, document.querySelector('#app'))

Shadow DOM也是一个DOM节点,因此我们所需要做的仅是将根组件挂载至Shadow DOM上即可。使用Preact示例如下:

import { h, render } from 'preact'

const shadowHost = document.createElement("div");
document.body.appendChild(shadowHost);

const shadow = shadowHost.attachShadow({ mode: "open" });
const shadowRoot = document.createElement("div");
shadowRoot.id = "shadow-root";

shadow.appendChild(shadowRoot);

render(<App />, shadowRoot);

根组件App与Shadow DOM的关系简化如下: image.png

Shadow host就是Shadow DOM所依附的普通DOM节点,Shadow Root才是根组件挂载的根节点。

完成这一步后,再来解决样式问题。

在Shadow DOM中可以直接添加style标签节点,并且只会Shadow DOM中生效,外部样式也不会在内部生效,完美做到样式隔离。按照这个思路,只需要将CSS文本提取,注入到style标签内,再将style标签附加到Shadow DOM上即可。

对于简单的样式,或许使用模版字符串就足矣,而当样式变得繁多时,借助构建工具将减少许多工作量。以Rollup为例,使用rollup-plugin-postcss插件,即可完成:

rollup.config.js

import postcss from "rollup-plugin-postcss";

export default {
  plugins: [
    postcss({
      include: "src/styles/**",
      inject: false,
      minimize: true,
    }),
  ],
};

设置inject为false,阻止CSS注入到head标签内,这样直接引入CSS文件得到的就是CSS文本字符串,同时设置minimize为true开启文本压缩,减少打包体积。引入方式如下:

import styleText from "./styles/index.scss";

const shadowHost = document.createElement("div");
shadowHost.id = shadowTargetId;
document.body.appendChild(shadowHost);

const shadow = shadowHost.attachShadow({ mode: "open" });

const style = document.createElement("style");
style.appendChild(document.createTextNode(styleText));

const shadowRoot = document.createElement("div");
shadowRoot.id = "shadow-root";

shadow.appendChild(style);
shadow.appendChild(shadowRoot);

render(<App />, shadowRoot);

至此就能在Shadow DOM上像写普通React应用一样开发插件了!

常见问题

  1. 组件选择 Preact可以直接使用React生态中的绝大多数组件,然而其中有许多使用的是Styled-Components,对于这类组件是无法直接在Shadow DOM中使用的,因为Styled-Components的工作方式就是向<head>内注入style标签,而由于Shadow DOM隔离了外部样式,因此不会起作用。如果想充分享受React生态系统带来的便利,应该考虑那些需要单独引入样式文件的组件。

  2. 事件监听 对于React合成事件,不需要担心什么。但有时需要监听Shadow DOM外部事件,如点击外部区域,关闭Shadow DOM内的弹窗组件。由于Shadow DOM内部所有事件的target都是Shadow Root节点,因此通过event.target只能判断UI事件来源于Shadow DOM内部,而无法知晓来源于具体哪一个元素。

- End-