前言
分享一个近期工作中遇到的关于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是工具函数
javascript
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脚本
javascript
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打包之前进行复制静态文件操作,我做了个异步响应操作
javascript
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中使用
javascript
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反而会增加代码量,完整的代码如下
TypeScript
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博客
二者在实现方案上不太相同,但是思路还是有相似之处的
以上就是文章全部内容了,感谢你看到了最后,如果觉得文章不错的话,还望三连支持一下,谢谢!