如何隔离第三方脚本与样式
0、省流总结版
- 利用影子Dom隔离样式
- 利用同源iframe隔离脚本
- 将iframe中获取dom的方法全部替换为主环境的(
iframeDocument.getElementById = (...args) => document.getElementById(...args)
)。
1、前言
因为页面内经常需要嵌入第三方的电话条组件,来提供拨号,接听来电的功能。
而随着对接的电话条厂商增多,一些的问题随之浮现。
- 工具库版本冲突,一些工具如jquery,可能两方都有在使用但是版本不同,往往会顾此失彼。
- 样式污染,有些电话条厂商并没有很好的做到样式隔离,经常出现样式污染了页面的其他元素的情况。
- 全局变量的污染,这个跟第一点类似,因为电话条厂商提供的sdk经常会使用全局挂载的方式来暴露一些api给接入方使用,这在一定程度上也污染了全局的作用域。
如何进行样式、脚本的隔离,就成了当务之急。
2、实现思路
2.1、友商是怎么做的
类似的需求,在其他场景也会存在。比如说,微前端。
这里参考的是无界这个微前端的框架,我们来看看他们是怎么解决相关的隔离问题的。
简单来说呢,就是利用iframe来隔离脚本,通过影子 DOM来隔离样式。
2.2、隔离脚本
因为iframe天然就具备了隔离的能力,创建一个同源的iframe,把script丢给他去加载,不就污染不到我主环境了?(摊手)
但这样还是会有问题,因为一般js脚本就做这几类事情:
- 发出接口请求
- 运算
- 操作dom
前面两个放在iframe内做都没什么问题,但是操作dom,要怎么才能让iframe能够操作到主环境的dom呢?
无界的做法是这样的:
将
document
的查询类接口:getElementsByTagName、getElementsByClassName、getElementsByName、getElementById、querySelector、querySelectorAll、head、body
全部代理到webcomponent
,这样instance
和webcomponent
就精准的链接起来。
也就是将iframe获取dom的方法,全部替换为主环境的获取dom的方法。
2.3、隔离样式
无界用来隔离样式的方案就是利用影子 DOM,他可以将添加在影子DOM的样式限制在其本身之下(即便是通配符选择器),而不影响到影子DOM之外的元素:
3、具体实现
有了思路啊,我们事不宜迟,马上放手开撸。
3.1、js隔离
鉴于样式有其他的方式隔离,比如加个命名空间,并不是非得要用影子dom的,所以我们这里先就隔离脚本实现一把。
const iframe = document.createElement('iframe');
// 某个同源的地址
iframe.src = location.origin + '/eservice/support/connect?resourceUrl=XXXXXX'
document.body.append(iframe)
iframe.onload = function () {
// 搞个jquery看看污染不污染
const script = document.createElement('script');
script.src = 'https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js'
iframe.contentWindow.document.body.append(script)
// 代理获取dom的api,这里不能使用bind,会失效
iframe.contentWindow.document.querySelector = (...args) => {
return document.querySelector(...args)
}
iframe.contentWindow.document.querySelectorAll = (...args) => {
return document.querySelectorAll(...args)
}
iframe.contentWindow.document.getElementsByTagName = (...args) => {
return document.getElementsByTagName(...args)
}
iframe.contentWindow.document.getElementsByClassName = (...args) => {
return document.getElementsByClassName(...args)
}
iframe.contentWindow.document.getElementsByName = (...args) => {
return document.getElementsByName(...args)
}
iframe.contentWindow.document.getElementById = (...args) => {
return document.getElementById(...args)
}
}
效果如下:
dom可以正常被访问
jquery正常被隔离
3.2、脚本和样式一起隔离
这里就需要用到影子dom,而为了抹平差异,我们给影子dom也挂载了html、body、head等标签。
const iframe = document.createElement('iframe');
iframe.src = location.origin + '/eservice/support/connect?resourceUrl=XXXXXX'
document.body.append(iframe)
// 影子dom的外层容器
const shadowWrap = document.createElement('div');
Object.assign(shadowWrap.style, {
position: 'fixed',
top: 0,
background: 'red',
zIndex: 999,
})
document.body.append(shadowWrap)
// 开启影子dom
const shadow = shadowWrap.attachShadow({ mode: "open" });
// 构建html等标签,最终挂载在影子dom下
const proxyHtml = document.createElement('html');
const proxyHead = document.createElement('head');
const proxyBody = document.createElement('body');
proxyHtml.appendChild(proxyHead);
proxyHtml.appendChild(proxyBody);
shadow.appendChild(proxyHtml);
iframe.onload = function () {
const script = document.createElement('script');
script.src = 'https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js'
iframe.contentWindow.document.body.append(script)
iframe.contentWindow.document.querySelector = (...args) => {
return proxyHtml.querySelector(...args)
}
iframe.contentWindow.document.querySelectorAll = (...args) => {
return proxyHtml.querySelectorAll(...args)
}
iframe.contentWindow.document.getElementsByTagName = (...args) => {
return proxyHtml.getElementsByTagName(...args)
}
iframe.contentWindow.document.getElementsByClassName = (...args) => {
return proxyHtml.getElementsByClassName(...args)
}
iframe.contentWindow.document.getElementsByName = (...args) => {
return proxyHtml.getElementsByName(...args)
}
iframe.contentWindow.document.getElementById = (...args) => {
return proxyHtml.getElementById(...args)
}
script.onload = () => {
iframe.contentWindow.$('body').append('<div>123123123</div>')
// 挂个通配样式
iframe.contentWindow.$('body').append('<style> * { background: #fff }</div>')
// $('body').append('<div style="position: fixed; top: 0px; bottom: 0px; left: 0px; right: 0px; background: red; z-index: 99999;">123</div>')
}
}
效果如下:
元素被正常挂载,样式被限制在影子dom下,即便是通配符选择器!
转载自:https://juejin.cn/post/7352387558746472500