likes
comments
collection
share

如何隔离第三方脚本与样式

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

0、省流总结版

  1. 利用影子Dom隔离样式
  2. 利用同源iframe隔离脚本
  3. 将iframe中获取dom的方法全部替换为主环境的(iframeDocument.getElementById = (...args) => document.getElementById(...args))。

1、前言

因为页面内经常需要嵌入第三方的电话条组件,来提供拨号,接听来电的功能。

如何隔离第三方脚本与样式

而随着对接的电话条厂商增多,一些的问题随之浮现。

  1. 工具库版本冲突,一些工具如jquery,可能两方都有在使用但是版本不同,往往会顾此失彼。
  2. 样式污染,有些电话条厂商并没有很好的做到样式隔离,经常出现样式污染了页面的其他元素的情况。
  3. 全局变量的污染,这个跟第一点类似,因为电话条厂商提供的sdk经常会使用全局挂载的方式来暴露一些api给接入方使用,这在一定程度上也污染了全局的作用域。

如何进行样式、脚本的隔离,就成了当务之急。

2、实现思路

2.1、友商是怎么做的

类似的需求,在其他场景也会存在。比如说,微前端

如何隔离第三方脚本与样式

这里参考的是无界这个微前端的框架,我们来看看他们是怎么解决相关的隔离问题的。

如何隔离第三方脚本与样式

简单来说呢,就是利用iframe来隔离脚本,通过影子 DOM来隔离样式。

2.2、隔离脚本

因为iframe天然就具备了隔离的能力,创建一个同源的iframe,把script丢给他去加载,不就污染不到我主环境了?(摊手)

但这样还是会有问题,因为一般js脚本就做这几类事情:

  1. 发出接口请求
  2. 运算
  3. 操作dom

前面两个放在iframe内做都没什么问题,但是操作dom,要怎么才能让iframe能够操作到主环境的dom呢?

无界的做法是这样的:

document的查询类接口:getElementsByTagName、getElementsByClassName、getElementsByName、getElementById、querySelector、querySelectorAll、head、body全部代理到webcomponent,这样instancewebcomponent就精准的链接起来。

也就是将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
评论
请登录