iframe动态操作标签分享

本文最后更新于:3 分钟前

前言

分享一个近期工作中遇到的关于IFrame的需求,以及解决方案。

需求大致是说在我们系统中嵌套了另一个文档页面,这个文档页面是爬取的,并且页面是原先使用后端渲染实现的,取到的css和script标签都是相对路径比如: “./mian.css” 这种,这么写会导致当origin发生变化时取不到静态资源,怎么解决这个问题呢?

解决方案思考

使用Nginx做反向代理

配置Nginx反向代理,在Nginx配置中添加一个代理规则,将请求定向到目标文档页面的地址。

后端动态修改页面路径

将相对路径改为绝对路径,通过解析文档页面,找到其中的相对路径资源引用,将其改为绝对路径。这样不受origin变化的影响。

前端代理(只能在dev环境下实现)

使用vite的proxy实现反向代理效果

先说结论,上述三种方式均被pass了

第一种方法由于页面请求到了宿主的网址下,导致Nginx监听不到资源请求,再有是前端想配置Nginx并不容易。。。

第二种缺乏可复用性,如果宿主的origin发送变化,则后端规则也要跟着改变

第三种就更不用说了,只能在开发环境下实现,不过这种方式给了我一定的启发,如果将静态资源打包进正式包里,再动态修改IFrame的资源路径,或许可以解决相关问题

设计概要

有了方案就需要技术的实施,总共有两步:

第一步是将静态资源打包进正式包中,这里可以使用rollup的插件rollup-plugin-copy来达到复制静态资源的目的

第二步是动态修改iframe中的link标签的href地址,达到资源替换的效果

方案实现

静态资源打包

和webpack有些不同,webpack可以通过CopyWebpackPlugin或者IgnorePlugin等方式复制或排除文件,而使用vite则需要借助其他plugs工具实现,比如vite-plugin-cp或者vite-plugin-static-copy,然而事情并没有这么简单,由于项目环境的复杂性较高,在esm和cjs上发生了错误,有些包是以esm导入的,但是项目中什么文件都有,无法兼顾既要又要,就像下面这样:

为了尽量不改变项目结构,我决定自己造轮子,自己写个插件,在vite的closeBundle生命时将上面要用到的静态文件复制到dist文件夹下

在项目根目录下新建script文件夹,创建新的脚本

其中helpers是工具函数

const noop = (_ = {}) => {}
export const defer = () => {
  let resolve = noop,
    reject = noop
  const promise = new Promise((_resolve, _reject) => {
    resolve = _resolve
    reject = _reject
  })
  return { promise, resolve, reject }
}

接着实现一下复制文件夹的node脚本

import fs from 'fs-extra'
import { defer } from './helpers.js'
// 手动复制文件夹
export const copyFile = async (
  source,
  target,
  config = { overwrite: true },
) => {
  const { promise, resolve, reject } = defer()
  fs.copy(source, target, config, (err) => {
    if (err) {
      reject(err)
      console.error('复制出错', err)
    } else {
      resolve()
      console.log('复制成功!')
    }
  })
  return promise
}

然后实现一下vite插件的hook函数,需要注意的是,在我的脚本之前有个打包zip的插件,为了在zip打包之前进行复制静态文件操作,我做了个异步响应操作

import { copyFile } from './copyFile.js'

export const copyStaticAfterBuild = (opts, cb) => {
  return {
    name: 'copy-static-after-build',
    closeBundle() {
      const taskers = opts.map((item) => {
        console.log(`copy static ${item.src} to ${item.dest}`)
        return copyFile(item.src, item.dest, { overwrite: true })
      })
      return Promise.all(taskers).then(cb)
    },
  }
}

最后是在vite.config中使用

import { defineConfig } from 'vite'

import { zipAfterBuild, copyStaticAfterBuild } from "./scripts"


// https://vitejs.dev/config/
export default defineConfig(({ command }) => {
    return {
        plugins: [
            copyStaticAfterBuild([
                {
                    src: './static',
                    dest: './dist/static'
                }
            ], zipAfterBuild({}).closeBundle),
        ],
    }
})

实现效果就是下面这样的

打包后的效果

iframe的通信及标签动态修改

参考之前写的博客,我们可以取iframe的标签并对其dom进行操作,这里我是在react中进行操作,所以写个ref获取标签,其中我们通过iframe.contentWindow.document获取到iframe的dom对象,然后对其内容进行修改,由于操作的步骤不多,使用ipc反而会增加代码量,完整的代码如下

import { useLocation } from 'react-router-dom'
import './detail.scss'
import { useCallback, useEffect, useRef } from 'react'

// 定义 LinksType 类型,可以是 HTMLLinkElement 或 HTMLScriptElement
type LinksType = HTMLLinkElement | HTMLScriptElement

// 定义网址替换规则的接口 IRule
type IRule = {
  source: string // 源网址
  target: string // 目标网址
}

// 定义替换网址参数的接口 ParamsOfReplaceUrl
interface ParamsOfReplaceUrl<T extends LinksType> {
  links: NodeListOf<T> // 标签列表,可以是 link、a 等等
  rules: IRule[] // 网址替换规则,全字匹配
}

// 获取当前页面的基础网址
const base = `${window.location.origin}/`

// 定义默认的替换规则数组
const rules = [
  { source: base + 'static/css/', target: base + 'static/' }, // 替换 CSS 资源的规则
  {
    source: base + 'static/components/bootstrap-4.3.1/css/', // Bootstrap CSS 资源的规则
    target: base + 'static/', // 替换目标
  },
]

/**
 * 批量替换Href网址
 * @param links 标签列表,link、a 等等
 * @param rules 网址替换规则,全字匹配
 */
const replaceHrefUrl = <T extends HTMLLinkElement>({
  links,
  rules,
}: ParamsOfReplaceUrl<T>) => {
  links.forEach((link) =>
    rules.forEach((rule) => {
      link.href.includes(rule.source) &&
        (link.href = link.href?.replace?.(rule.source, rule.target))
    }),
  )
}

/**
 * 批量替换Src网址
 * @param links 标签列表,img、video、script 等等
 * @param rules 网址替换规则,全字匹配
 */
const replaceSrcUrl = <T extends HTMLScriptElement>({
  links,
  rules,
}: ParamsOfReplaceUrl<T>) => {
  links.forEach((link) =>
    rules.find((rule) => {
      if (rule.source.includes(link.src)) {
        const newLink = document.createElement('script')
        newLink.src = link.src?.replace?.(rule.source, rule.target)
        link?.parentNode?.replaceChild(newLink, link)
      }
    }),
  )
}

// IntelDetail 组件
const IntelDetail = () => {
  const location = useLocation()
  const iframe = useRef<HTMLIFrameElement>(null)

  // 加载处理程序
  const loadHandler = useCallback(() => {
    const elem = iframe?.current
    const scriptSrc = elem?.contentWindow?.document.querySelectorAll('script')
    const cssLink = elem?.contentWindow?.document.querySelectorAll('link')

    if (cssLink) {
      replaceHrefUrl({ links: cssLink, rules }) // 替换 CSS 链接
    }
    if (scriptSrc) {
      replaceSrcUrl({ links: scriptSrc, rules }) // 替换 Script 资源
    }
  }, [])

  // 组件加载时执行加载处理程序,并在组件卸载时清理事件监听器
  useEffect(() => {
    const elem = iframe?.current
    elem?.addEventListener('load', loadHandler)
    return () => {
      elem?.removeEventListener('load', loadHandler)
    }
  }, [])

  // 渲染组件
  return (
    <div className="intel-detail">
      <iframe
        ref={iframe}
        src={`${window.location.origin}/jaguar/vul_intelligence/content_s/${location.state.id}`}
        width="100%"
        height="100%"
        frameBorder={0}
      ></iframe>
    </div>
  )
}

export default IntelDetail

上述代码中,我们将iframe中的相对资源路径改到了项目的./static中,修复了资源缺失的问题。

效果展示

在使用了上述方案对项目打包后,之前的资源未找到的问题也被解决,效果如下

可以看到在iframe中首先会加载两次资源,在未取到资源后,又会重新获取两个css资源,随后DOM树和CSS树发生重排操作重新渲染,虽然有比较明显的样式过渡,但是这已经是目前最佳的解决方案了

写在最后

本文分享了通过打包静态资源并动态替换 iframe 中的资源路径,成功解决了因 origin 变化导致的资源加载问题。这种方案也让我想起了原先写的:基于内网穿透+Fiddler的私有化项目调试前端解决方案_fiddler onbeforerequest-CSDN博客

二者在实现方案上不太相同,但是思路还是有相似之处的

以上就是文章全部内容了,感谢你看到了最后,如果觉得文章不错的话,还望三连支持一下,谢谢!