chrome插件样式隔离方案记录
分析
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树
将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全局组件样式的处理
通过上面两步已经可以做到样式的隔离,但对于一些全局的样式组件,如Message
,Modal
等,还需要将对应的样式写入到全局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
文件直接注入到隔离的子树中
如果还有其它更好的方案,欢迎大家留言讨论φ(゜▽゜*)♪
转载自:https://juejin.cn/post/7149067306021175327