iframe 错误检测及优化方案

# 前言

  iframe 加载的核心就是构建一次上下文的过程。本次分享重点会围绕 iframe 加载优化,错误监控及跨域通信来展开。

# 加载机制

  以 Chrome 为例,在 iframe 开始加载时,浏览器会为 iframe 单独分配一个渲染进程,由于进程具有独立的内存空间,因此不会和主页面共享内存资源(完全隔离),一种情况是当进程数达到 Chrome 限制的上限时,Chrome 会将相同域名下的不同 iframe 放在同一个渲染进程中。
  当浏览器为 iframe 分配渲染进程后,iframe 同样会完成导航、资源加载解析以及渲染的过程。

需要注意的是:
iframe 和主页面共享连接池,如果嵌入 iframe 资源较多,会造成资源加载等待时间较长;
iframe 的加载过程是同步的,因此会阻塞主页面的 onload 事件,但 iframe 资源加载的网络请求错误不会阻塞主页面的 onload 触发时间(之后会提到如何采用异步无阻塞式加载)。
iframe 完成加载后无论成功与否都会触发 iframe 的 onload 事件,所以通常 onerror 方式去处理 iframe 报错基本是无效的 查看issues

# 异常处理的几种方式

(1)try-catch

  只能捕捉到运行时同步代码报错,无法捕捉到语法和异步代码报错。

(2)window.onerror

  能捕捉到同步和异步的代码报错,但还是无法捕捉到语法报错和网络异常错误。通常将 try catch 和 window.onerror 结合使用是一种不错的实践,需要注意 window.onerror 只有当函数返回 true 时才不会向上抛出错误,否则还是会打印到控制台。

(3)window.addEventListener('error', handler, true)

  除了可以捕获上述异常之外,还能捕获到网络资源加载问题。由于该错误事件不会冒泡,因此需要在事件捕获阶段进行,虽然这种方式可以捕捉到异常,但还是无法知道是 404 还是 500,所以需要配合服务端日志来排查分析。

(4)window.addEventListener('unhandledrejection', handler)

  Promise 异常错误如果没有 catch 是需要通过 unhandledrejection 全局事件监听来捕获的。

// Promise 错误监控
window.addEventListener('unhandledrejection', function(e) {
    e.preventDefault()
    console.warn('捕获 promise 错误', e.reason)
    window.top.postMessage('IFRAME_PROMISE_ERROR', '*')
})

// 全局错误监控(资源加载,运行时,语法报错)
window.addEventListener('error', function(e) {
    try {
        e.stopImmediatePropagation()
        const target = e.target || e.srcElement

        if (target === window) {
        console.warn('捕获全局错误', e.message)
            window.top.postMessage('IFRAME_GLOBAL_ERROR', '*')
    }

        if (target instanceof HTMLElement && ['LINK', 'SCRIPT', 'IMG'].indexOf(target.nodeName) !== -1) {
            const src = target.src || target.href
            if (window.location.href.indexOf(src) !== 0) {
                console.warn('捕获资源错误', e)
                window.top.postMessage('IFRAME_RESOURCE_ERROR', '*')
            }
        }
    } catch (ex) {
        console.warn('Error 拦截器报错', ex)
        window.top.postMessage('IFRAME_LISTENER_ERROR', '*')
    }
}, true)

# 同源和异源场景分析

通常我们嵌入的页面资源会存在以下三种情况:

(1) iframe 窗口和父窗口域名相同

如果 iframe 页面和主站是同域名的话,可直接捕获到错误信息。

(2)iframe 窗口和父窗口顶域相同,二级域名不同

如果 iframe 页面和主站根域名相同,则可以分别在上下游设置相同的 document.domain

(3)iframe 窗口和父窗口顶域和子域都不同

  如果你嵌入的 iframe 页面和你的主站不是同个域名的,但是 iframe 内容不属于第三方,是你可以控制的,那么可以通过与 iframe 通信的方式将异常信息抛给主站接收(通过 postMessage 实现)。
  如果你嵌入的 iframe 页面和你的主站不是同个域名,并且不受自己控制,出于安全考虑是无法监控到第三方的错误信息的。

# iframe 加载问题分析及解决方案

  通常在使用 iframe 的时候,我们需要指定对应的 src 来加载对应的页面资源,由于浏览器同源策略的因素,除了 iframe 的尺寸信息,其他我们尝试访问的信息都会被限制,因此 chrome 就会提示加载错误。此外还有一些情况,是因为加载页面本身存在报错,出现页面加载阻塞所导致。

Cookie 跨域问题

  Chrome 51 提供了 SameSite 属性(默认为 Lax),可以阻止浏览器将此 Cookie 与跨站点请求一起发送,主要目标是降低跨源信息泄漏的风险,同时也在一定程度上阻止了 CSRF 攻击。
  如果第三方应用是受信的(比如其他航道的应用),则可以使用 Nginx 配置 SameSite None 能有效的解决 iframe cookie 跨域的问题(比如 iframe 嵌入第三方应用后,通过代理配置后无需重复登录)。

如何判断是否有效加载

  可以看出,当 iframe 加载完之前,如果出现报错(比如 4xx / 5xx / TypeError 等),则 onload 事件是不会被触发的,需要 postMessage 来通信告知主页面 iframe 异常,同域名则可以直接监控。如果加载过程已经完成(只是可能存在某些图片资源或依赖项未成功加载),则 onload 事件会正常触发,并且可以在 onload 事件中判断是否加载成功。

超时处理

  当 fetch 请求超过 10s 后,会认为当前资源无法成功加载并中止请求,注销当前 iframe 实例。

异步无阻塞式加载

  当浏览器开始解析 iframe 时,可以尝试动态生成一个空的 iframe,这样 iframe 将很快会触发 onload 事件(浏览器会立刻渲染已经完成解析的 DOM)。
  同时,我们可以 fetch 当前请求的 iframe src 来判断资源的有效性(当 fetch 成功后,会一直轮询判断当前主页面是否完成加载,如果完成加载并且资源是有效的,再使用 iframe 重新加载真正的 url 资源,这样不会阻塞主页面 onload 事件。
  使用 fetch 的好处是可以异步请求页面资源,并且可以通过 response 来判断资源是否异常(比如 4xx / 5xx / CORS 之类的错误都可以通过获取到),这样在主页面完成加载后可以立刻开始渲染 iframe html 骨架,而不需要担心 iframe 静态资源加载会影响主页面资源加载的情况)。

// 动态创建 iframe
initIframe() {
    const frame = document.createElement('iframe')
    this.presetAttr(frame)
    this.iframeEl = frame
    this.$el.appendChild(this.iframeEl)
}
// iframe 加载优化
fallbackHandler() {
    this.loading = false
    this.$nextTick(() => {
        if (this.iframeEl) this.$el.removeChild(this.iframeEl)
        this.iframeEl = null
        this.showFallback = true
    })
}

errorHook(errMsg, callback) {
    try {
        this.$emit('error', { errMsg })
        callback ? callback() : this.fallbackHandler()
    } catch (error) {
        console.warn('Func errorHook 报错', error)
    }
}

fetchResource() {
    // 过滤无效的资源
    if (!this.isValidUrl(this.src)) {
        this.errorHook('invalid fetch url')
        return
    }

    // 创建请求
    const request = new Request(this.src)
    let controller = new AbortController()
    let signal = controller.signal

    // 注册请求中止回调
    signal.onabort = () => {
        this.errorHook('fetch request aborted')
    }
    
    // 10s 后进入超时处理
    const timeoutId = setTimeout(() => {
        this.loading = false
        controller.abort()
    }, 10000)

    // 异步请求访问的页面
    // 
    fetch(request, { signal, credential: 'includes' })
        .then((response) => {
            if (response.ok && response.status === 200) {
                const intervalId = setInterval(() => {
                    if (window.document.readyState === 'complete') {
                        clearInterval(intervalId)
                        this.loading = false
                        if (this.iframeEl) {
                            setTimeout(() => {
                                this.iframeEl.src = response.url
                                this.iframeEl.onload = (event) => {
                                    this.$emit('load', { event })
                                }
                            }, 16)
                        }
                    }
                }, 16)
            } else {
                throw new Error('fetch error')
            }
        })
        .catch((error) => {
            console.warn(error)
            controller.abort()
        })
        .finally(() => {
            clearTimeout(timeoutId)
        })
}


# Refer

https://www.aaronpeters.nl/blog/iframe-loading-techniques-and-performance/
https://www.stevesouders.com/blog/2009/06/03/using-iframes-sparingly/
https://calendar.perfplanet.com/2010/fast-ads-with-html5/
https://afantasy.ninja/2018/07/15/dive-into-iframe/
https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-iframe-element

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容