han's bolg - 年糕記

比iframe和dangerouslySetInnerHTML更好用的插入其他网站的方式

最近的项目里,有这样一个需求:

在 A 页面里插入 B 页面的内容,且要求:

  1. B 的内容格式不能乱
  2. A 的内容和 B 的内容要无缝衔接,像一个页面一样
  3. B 页面里有很多script标签,功能要正常可用。
  4. B 页面不会为了适配 A 页面,去做改动优化

方案 1:iframe

要在一个页面插入另一个页面的内容,首先想到的方案肯定是 iframe,只要简单填一个 src 就 ok 了。但存在三个问题:

  1. 高度问题

如果给 iframe 设置了宽高,那么 B 页面的内容只能在 iframe 里滚动显示。这与需求 2 不符。
如果要让 iframe 的宽高由内容决定,应该设置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const iframeAutoHeight = iframe => {
if (
navigator.userAgent.indexOf('MSIE') > 0 ||
navigator.userAgent.indexOf('rv:11') > 0 ||
navigator.userAgent.indexOf('Firefox') > 0
) {
iframe.height = iframe.contentWindow.document.body.scrollHeight
} else {
iframe.height = iframe.contentWindow.document.documentElement.scrollHeight
}
}
useEffect(() => {
const iframe = document.getElementById('xFrame')
if (iframe.attachEvent) {
iframe.attachEvent('onload', () => {
iframeAutoHeight(iframe)
})
} else {
iframe.onload = () => {
iframeAutoHeight(iframe)
}
}
})
  1. 链接跳转问题

如果 B 页面里有 a 标签之类的跳转,使用 iframe 的方式,只能在 iframe 里跳转,而不能在整个页面里进行跳转。

如果只是少数几个明确的跳转方式,

  • 比如 a 标签,可以改target='_top'

  • location.href 跳转,改为

1
2
3
<script language="JavaScript">
if (window != top) top.location.href = location.href;
</script>

但因为需求 4 以及 B 页面存在大量我们无法预知的跳转方式,这些无法满足。

  1. 跨域问题

这个是更大的问题,如果 A、B 两个页面存在跨域问题,那么 A 页面处理问题 1,2 的方案就不可用 😭

方案 2:react 的 dangerouslySetInnerHTML

让我们靠谱的后端同学,在接口返回数据里直接把 B 页面的内容返回给 A,然后在 A 里使用 dangerouslySetInnerHTML,但这样子可以满足需求 1,2,4,但是需求 3 出问题了。

因为 dangerouslySetInnerHTML 无法解析出里面的 script 标签,并进行执行。
dangerouslySetInnerHTML 其实就是 react 给我们写的一个dom.innerHMTL = xxx的语法糖,innerHTML 是不会执行 script 标签里的内容的。

方案 3:createContextualFragment + appendChild

仍然让我们靠谱的后端同学,把 B 页面的内容用接口的形式发给我们,然后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, {useEffect, useRef} from 'react'

function DangerouslySetHtmlContent(props) {
const {html} = props
const divRef = useRef(null)

useEffect(() => {
const slotHtml = document.createRange().createContextualFragment(html) // Create a 'tiny' document and parse the html string
divRef.current.innerHTML = '' // Clear the container
divRef.current.appendChild(slotHtml) // Append the new content
}, [html])

return <div ref={divRef}></div>
}

export default DangerouslySetHtmlContent

完美!

参考资料