Qiankun实践——实现一个CSS沙箱
本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
前言哈喽,大家好,我是海怪。
上篇文章讲了如何实现一个 Qiankun 的 JS 沙箱(实际应该是 3 个,哈哈),那这篇文章就带大家来实现一下 CSS 的沙箱。
Qiankun 的 CSS 沙箱原理上并不难,但与 JS 沙箱不同的是,它的源码比较分散,阅读起来要跳转好几个地方,有点麻烦。因此,这篇文章同样也会精简整个实现过程,尽量让读者读起来不费劲。
文章中的源码都放在我的这个仓库 mini-css-sandbox 里,需要的自行提取即可。废话不多说,那现在就让我们开始吧~
准备工作首先,我们来做一些准备工作,分别添加以下文件:
index.html:入口 HTMLshadowDOMIsolation.js:Shadow DOM 沙箱实现scopedCSSIsolation.js:Scoped CSS 沙箱实现因为样式是否成功隔离可以通过肉眼去看,这里就不用 TDD 方式来做测试了,文章也会更精简一些。
在index.html里添加:
htmllang="en"
head
metacharset="UTF-8"
title样式隔离沙箱/title
style
p{
color:
}
/style
/head
body
h1ShadowDOM隔离/h1
divid="shadow-dom"
pShadowDOM隔离/p
/div
h1ScopedCSS隔离/h1
divid="scoped-css"
pScopedCSS隔离/p
/div
p外部文本/p
scriptsrc="scopedCSSIsolation.js"/script
scriptsrc="shadowDOMIsolation.js"/script
/body
/html
这个 HTML 里有一个全局的style,里面有全局样式,会将的颜色变成红色,剩下的都是一些测试要用的 HTML 结构。
Shadow DOM 沙箱我们先来实现 Shadow DOM 沙箱,它对应Qiankun 样式隔离的严格模式。
原理在开始写代码前,我们来简单了解一下 Shadow DOM 原理。
Shadow DOM 可以将一个隐藏的、独立的 DOM 附加到一个元素上,一般来说是微应用的容器div上。
其中:
Shadow host:一个常规 DOM 节点,Shadow DOM 会被附加到这个节点上。Shadow tree:Shadow DOM 内部的 DOM 树。Shadow boundary:Shadow DOM 结束的地方,也是常规 DOM 开始的地方。Shadow root: Shadow tree 的根节点。这并不是什么新技术,我们常见的video和audio用的就是 Shadow DOM,浏览器把一些相关逻辑封装和内部结构封装在里面。外部看来就是一个video但里面包含着对应的按钮、轨道、滚动条等结构。
实现假如我们把微应用的内容用 Shadow DOM 封装起来,比如把style挂截到 Shadow Tree 上,那么就可以实现样式的硬隔离了。
假如有下面的微应用,里面有一个style会把字体颜色改成紫色:
constshadowDOMSection=document.querySelector('#shadow-dom');
constappElement=shadowDOMIsolation(`
divclass="wrapper"
stylep{color:purple}/style
p内部文本
/div
`);
shadowDOMSection.appendChild(appElement);
我们现在要做的就是把div.wrapper与外部隔离,不要让里面的style影响到外部的。按照刚刚对 Shadow DOM 的理解,我们来实现一下:
functionshadowDOMIsolation(contentHtmlString){
//清理HTML
contentHtmlString=contentHtmlString.trim();
//创建一个容器div
constcontainerElement=document.createElement('div');
//生成内容HTML结构
containerElement.innerHTML=contentHtmlString;//content最高层级必需只有一个div元素
//获取根div元素
constappElement=containerElement.firstChild;
const{innerHTML}=appElement;
//清空内容,以便后面绑定shadowDOM
appElement.innerHTML='';
letshadow;
if(appElement.attachShadow){
//兼容性更广的写法
shadow=appElement.attachShadow({mode:'open'
}else{
//旧写法
shadow=appElement.createShadowRoot();
}
//生成shadowDOM的内容
shadow.innerHTML=innerHTML;
returnappElement;
}
可以看到 Shadow DOM 沙箱实现还是比较简单的,主要做了几件事:
把当前元素的内容拿出来生成shadowDOM再刚刚的内容放入这个 shadow DOM清除这个元素,并追加 shadow DOM 即可最终效果如下:
会发现外部文本依然是红色,不会受微应用的样式影响。
Scoped CSS 沙箱接下来讲讲 Scoped CSS 沙箱,它对应的是Qiankun 样式隔离的实验性模式。
原理Scoped CSS 沙箱的原理更简单:将微应用里的style的文本提取出来,将所有的选择器进行转换:
普通选择器-微应用容器选择器普通选择器
例如:
span -div[data-app-name=我的微应用]span
这样span的样式只会作用在div[data-app-name=我的微应用]元素,而不会跑到外面了。
实现原理很简单,但实现起来还是有些复杂的。其中比较绕的一个点就是获取 CSS 文本:
functionprocessCSS(appElement,stylesheetElement,appName){
//生成 CSS 选择器:div[data-app-name=微应用名字]
constprefix=`${appElement.tagName.toLowerCase()}[data-app-name="${appName}"]`;
//生成临时style节点
consttempNode=document.createElement('style');
document.body.appendChild(tempNode);
tempNode.sheet.disabled=true
if(stylesheetElement.textContent!==''){
//将原来的CSS文本复制一份到临时style上
consttextNode=document.createTextNode(stylesheetElement.textContent||'');
tempNode.appendChild(textNode);
//获取CSS规则
constsheet=tempNode.sheet;
construles=[...sheet?.cssRules??
//生成新的CSS文本
stylesheetElement.textContent=this.rewrite(rules,prefix);
//清理
tempNode.removeChild(textNode);
}
}
functionscopedCSSIsolation(appName,contentHtmlString){
//清理HTML
contentHtmlString=contentHtmlString.trim();
//创建一个容器div
constcontainerElement=document.createElement('div');
//生成内容HTML结构
containerElement.innerHTML=contentHtmlString;//content最高层级必需只有一个div元素
//获取根div元素
constappElement=containerElement.firstChild;
//打上data-app-name=appName的标记
appElement.setAttribute('data-app-name',appName);
//获取所有style/style元素内容,并将它们做替换
conststyleNodes=appElement.querySelectorAll('style')||
[...styleNodes].forEach((stylesheetElement)={
processCSS(appElement,stylesheetElement,appName);
})
returnappElement;
}
由于我们要改成div[data-app-name=我的微应用] span,所以第一个参数为应用名,以此作区分。接下来是对 CSS 文进行替换了,这部分 Qiankun 用了好几replace,属实有点绕,我这里就精简成对最常见的情况p { color: blue }进行替换:
//多种规则
constRuleType={
STYLE:1,
MEDIA:4,
SUPPORTS:12,
}
functionruleStyle(rule,prefix){
//匹配p{...,a{...,span {...这类字符串
returnrule.cssText.replace(/^[\s\S]+{/,(selectors)={
//匹配div,body,span {...这类字符串
returnselectors.replace(/(^|,\n?)([^,]+)/g,(selector,_,matchedString)={
//将p{=div[data-app-name=微应用名]p{
return`${prefix}${matchedString.replace(/^*/,'')}`;
})
}
functionrewrite(rules,prefix){
letcss='';
rules.forEach((rule)={
switch(rule.type){
caseRuleType.STYLE:
css+=ruleStyle(rule,prefix);
break;
//caseRuleType.MEDIA:
//css+=this.ruleMedia(rule,prefix);
//break;
//caseRuleType.SUPPORTS:
//css+=this.ruleSupport(rule,prefix);
//break;
default:
css+=`${rule.cssText}`;
break;
}
return
}
可以看到除了STYLE规则,还有媒体以及兼容性的规则,这些 Qiankun 都有对于的正则匹配去改写 CSS,这里只关注 STYLE 就可以了。当然,这部分基本就是正则的替换,我认为不需要花太多时间纠结正则表达式是怎么写的,只要理解整体思路就好。
同样写一个用例测试一下:
constscopedCSSSection=document.querySelector('#scoped-css');
constwrappedScopedCSSAppElement=scopedCSSIsolation('MyApp',`
divclass="wrapper"
stylep{color:blue}/style
pScopedCSSIsolation
/div
`);
scopedCSSSection.appendChild(wrappedScopedCSSAppElement);
效果如下:
可以看到,外部文本依然为红色,而内部文本为蓝色。打开控制台也可以到对应的 CSS 选择器已做了改写:
上面为微应用样式,下面被划掉的为全局样式变成 Web Component问题可以发现上面的代码有一些重复:每次都要获取 container 元素,写好 HTML,最后再appendChild追加appElement。有重复,我们就应该用一个函数去封装好它,这才是良好的写代码习惯。
通常来说写个函数包装一下就好了,Qiankun 也是如此。不过,这里我想跳脱 Qiankun 微前端的范畴,我希望不要自己手写htmlString,而是可以这样去使用:
h1ShadowDOM隔离/h1
isolation-contentdata-app-name="Sub1"data-isolation-mode="shadowDOM"
stylep{color:purple}/style
pShadowDOMIsolation/p
/isolation-content
h1ScopedCSS隔离/h1
isolation-contentdata-app-name="Sub2"data-isolation-mode="scopedCSS"
stylep{color:blue}/style
pScopedCSSIsolation/p
/isolation-content
p外部文本/p
这其实就是 Web Component 了,其它微应用框架 single-spa 周边库和京东的 MicroApp 也用到同样的技术。
实现Web Componet 不多介绍,具体可以看我的这篇《秒懂 Web Component》。按 Web Component 的理念来实现一下:
classIsolationextendsHTMLElement{
constructor(){
super();
constname=this.getAttribute('data-app-name');
constmode=this.getAttribute('data-isolation-mode');
consthtml=`divclass="wrapper"${this.innerHTML.trim()}/div`;
//根据隔离模式来生成对应的appElement
constappElement=mode==='shadowDOM'?shadowDOMIsolation(html):scopedCSSIsolation(name,html);
//清除内容
this.innerHTML='';
//再追加包裹的内容
this.appendChild(appElement);
}
}
customElements.define('isolation-content',Isolation)
还要记得在index.html里引入这个文件:
scriptsrc="scopedCSSIsolation.js"/script
scriptsrc="shadowDOMIsolation.js"/script
scriptsrc="Isolation.js"/script
最终的效果如下:
总结最后我们来总结一下这篇实践:
Qiankun 的样式隔离主要分为Shadow DOM 隔离以及Scoped CSS 隔离两种Shadow DOM 隔离主要利用了 Shadow DOM 硬隔离的特点来做样式隔离Scoped CSS 则是对style元素的 CSS 文本进行处理,在原有选择器上添加下个父类选择器,以此做样式隔离
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线