likes
comments
collection
share

chrome插件样式隔离方案记录

作者站长头像
站长
· 阅读数 16

分析

Chrome插件的Content页面与当前域名页面共享样式和脚本,使用Antd组件时,会将Antd相关样式注入到打包好的CSS文件中。插件通过manifest.json配置引入CSS文件时,就会将样式注入到页面,导致Antd的全局样式影响网页布局。

期望寻找相关方案,将插件的Content内容与当前域名页面的内容做隔离,从而防止Content样式影响到当前当前域名的样式。

采用的方案

采用ShadowRoot API生成一个DOM子树,它与文档的主DOM树分开渲染,再将相关样式注入到这个子树中,就可以达到样式隔离的效果。

生成独立的DOM树

封装一个方法,调用document.createElement创建一个节点,并设置该节点的attachShadow属性。便可以返回一个shadowRoot节点

rootElement = document.createElement('div');
rootElement.setAttribute('id', 'appRoot');
// 生成shadowRoot节点
const shadowRoot = rootElement.attachShadow({ mode: 'open' });
document.body.appendChild(rootElement);

调用Object.defineProperty方法,在ShadowRoot中生成一个根节点用来挂载React Components

shadowRoot.defaultView = shadowRoot.ownerDocument.defaultView;

shadowRoot.createElement = function (elementName: string) {
  const node = document.createElement(elementName);
  Object.defineProperty(node, 'ownerDocument', { value: shadowRoot });
  return node;
};

const shadowMountNode = shadowRoot.createElement('div');
// 给shadowRoot里面的根节点添加id,命名规则为`驼峰命名,根节点id+Children`
shadowMountNode.setAttribute('id', `${elementId}Children`);

整合代码,并调用ReactDOM.render()方法初始化React组件

/**
 * 创建shadowDom节点
 * @param elementId 节点id
 * @returns shadowRoot里面的根节点
 */
export const createShadowElement = (elementId: string): Element => {
  let rootElement = document.getElementById(elementId);

  // 存在则获取子元素返回
  if (rootElement) {
    const shadowMountNode = rootElement.shadowRoot?.querySelector(`#${elementId}Children`) as Element;
    return shadowMountNode;
  }

  rootElement = document.createElement('div');
  rootElement.setAttribute('id', elementId);
  document.body.appendChild(rootElement);
  const shadowRoot: any = rootElement.attachShadow({ mode: 'open' });

  shadowRoot.defaultView = shadowRoot.ownerDocument.defaultView;

  shadowRoot.createElement = function (elementName: string) {
    const node = document.createElement(elementName);
    Object.defineProperty(node, 'ownerDocument', { value: shadowRoot });
    return node;
  };
  const shadowMountNode = shadowRoot.createElement('div');
  // 给shadowRoot里面的根节点添加id,命名规则为`驼峰命名,根节点id+Children`
  shadowMountNode.setAttribute('id', `${elementId}Children`);

  shadowRoot.appendChild(shadowMountNode);

  return shadowMountNode;
};

// App.tsx
import { render } from 'react-dom'
...
const rootElement = createShadowElement('appRoot');
render(<Component />, rootElement)

这样在页面就生成了一个独立于文档主DOM树的子DOM树

chrome插件样式隔离方案记录

将Antd样式注入到子树中

注入antd样式分为两步,首先将style-loader修改成to-string-loader,这样在引入样式时,就可以转为字符串的形式,最后创建一个<style></style>标签,将样式写入到该标签,并将这个标签挂载到子树中即可

  • 修改loader配置
yarn add to-string-loader -D
...
{
  test: /.css$/,
  use: ['to-string-loader', 'css-loader'],
},
...
  • 引入样式,创建style标签并挂载到子树中
import antdStyle from 'antd/dist/antd.css';

/**
 * 创建样式标签并写入样式
 * @param styleContent
 * @param rootElement
 */
function loadStyle(styleContent: string, rootElement = document.querySelector('head')) {
  const style = document.createElement('style');
  style.innerText = styleContent;
  rootElement?.appendChild(style);
}

// 在createShadowElement方法中调用loadStyle
export const createShadowElement = (elementId: string): Element => {
  ...
  loadStyle(antdStyle, shadowRoot);
  ...
};

自定义组件样式的处理

在我们编写的React组件中,通常使用CSS Module来进行样式的隔离,但经过webpack处理后的全局样式此时已经对子树中的元素不生效,因此这里也需要进行处理成字符串的形式进行注入

// App.jsx
// .less文件的处理同上
import styles from './app.less';

export default () => {
  return (
    <>
      <style>{styles}</style>
      <div className='App_wrap'>
        ...
      </div>
    </>
  )
}
/* 类的命名规则,需要改成组件_类名的形式 */
.App_wrap {
  ...
}

Antd全局组件样式的处理

通过上面两步已经可以做到样式的隔离,但对于一些全局的样式组件,如MessageModal等,还需要将对应的样式写入到全局css文件中。目前采用的是直接复制的方式

/* global.css */
/* Message样式单独引入 */
.ant-message {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  color: rgba(0, 0, 0, 0.85);
  font-size: 14px;
  font-variant: tabular-nums;
  line-height: 1.5715;
  list-style: none;
  font-feature-settings: 'tnum';
  position: fixed;
  top: 8px;
  left: 0;
  z-index: 1010;
  width: 100%;
  pointer-events: none;
}

.ant-message-notice {
  padding: 8px;
  text-align: center;
}

.ant-message-notice-content {
  display: inline-block;
  padding: 10px 16px;
  background: #fff;
  border-radius: 2px;
  box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
  pointer-events: all;
}

.ant-message-success .anticon {
  color: #52c41a;
}

.ant-message-error .anticon {
  color: #ff4d4f;
}

.ant-message-warning .anticon {
  color: #faad14;
}

.ant-message-info .anticon,
.ant-message-loading .anticon {
  color: #1890ff;
}

.ant-message .anticon {
  position: relative;
  top: 1px;
  margin-right: 8px;
  font-size: 16px;
}

.ant-message-notice.ant-move-up-leave.ant-move-up-leave-active {
  -webkit-animation-name: MessageMoveOut;
  animation-name: MessageMoveOut;
  -webkit-animation-duration: 0.3s;
  animation-duration: 0.3s;
}

@-webkit-keyframes MessageMoveOut {
  0% {
    max-height: 150px;
    padding: 8px;
    opacity: 1;
  }

  100% {
    max-height: 0;
    padding: 0;
    opacity: 0;
  }
}

@keyframes MessageMoveOut {
  0% {
    max-height: 150px;
    padding: 8px;
    opacity: 1;
  }

  100% {
    max-height: 0;
    padding: 0;
    opacity: 0;
  }
}

.ant-message-rtl {
  direction: rtl;
}

.ant-message-rtl span {
  direction: rtl;
}

.ant-message-rtl .anticon {
  margin-right: 0;
  margin-left: 8px;
}
  • manifest.json
{
  ...
  "content_scripts": [
    {
      ...
      "css": ["global.css"],
      ...
    }
  ],
  ...
}

其它的一些问题和想法

以上的样式隔离方案,可以做到将Content内容的样式和页面样式隔离开,但还存在一些未解决的问题:

  • 全局样式虽然自定义了,但webpack在打包时仍会生成content.css文件
  • 通过createFromIconfontCN引入的第三方IconFont图标库不生效
  • 组件样式写起来过于繁琐

思考了一些解决方法,后续会尝试配置webpack或其它打包工具,将打包好的content.css文件直接注入到隔离的子树中

如果还有其它更好的方案,欢迎大家留言讨论φ(゜▽゜*)♪