项目背景
公司有一个运营同事使用的素材库,是h5开发的企微插件,原本素材主要是公司域名的文章,点击文章可以进入详情页查看文章内容,并且可以转发客户、群发客户、发朋友圈等。现在希望可以支持非公司域名的文章,主要以微信公众号的文章为主,兼顾一些其他文章,还需要统计分享出去的链接访问量,根据分享OA人员进行统计
原本设计方案
详情页使用iframe展示文章,底部是功能按钮,相当简单
开搞
需求到手后,想着这不很简单吗,拿着键盘就要开撸,先让后端配几个微信公众号文章,结果一试傻眼了,居然报了iframe的错误
百度之后发现,原来这是微信防盗链导致的
但是,虽说微信的防盗链很牛逼,但是也是有解决方案的,那就是cors-anywhere,但是目前该项目是停运的了,需要手动下载代码,起一下服务即可
https://github.com/Rob–W/cors-anywhere
接下来就可以通过代理和一系列字符串操作加载微信公众号文章了
主要就几个点
- 设置X-Requested-With为XMLHttpRequest,否则会请求报错
- 将图片的data-src属性转成src属性
- 将css文件中的href="//res.wx.qq.com xxx"改成带href="https://res.wx.qq.com xxx"前缀的,不然会请求相对路径
- 微信拦截所有图片访问受限,会出现图片加载不了问题:添加一下代码解决
- 解决背景图片不展示问题
- 主体内容不可见:获取rich_media_content元素,将元素的visibility设置为可见
具体代码如下:
function processLoadWeChatUrl(url) {
const instance = Axios.create({
headers: {
"X-Requested-With": "XMLHttpRequest",
},
})
url = `http://127.0.0.1:8090/${url}`
instance.get(url).then(response => {
let html = response.data
// 将data-src属性转成src
html = html.replace(/data-src/g, "src")
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/g, '')
.replace(/https/g, 'http')
.replace(/href="\/\/res.wx.qq.com\//g, 'href="https://res.wx.qq.com/')
//根据微信传回的html中的特殊路径data-src转为src等
let html_src = html
let iframe = document.getElementById('iFrame-id');
iframe.src = html_src
//这时候就成功拿到了微信公众号,这个html_src是个静态网页
let doc = iframe.contentDocument || iframe.document
//添加meta展示图片不加这个图片出不来
let htmlArr = html_src.split('</head>')
let html_src_add = htmlArr[0] + '<meta name="referrer" content="never"></head>' + htmlArr[1]
let backgroundUrlReg = /url[(]"(\S*)"/g
//获取背景图
let backgroundImgs = html_src_add.match(backgroundUrlReg)
if (backgroundImgs && backgroundImgs.length) {
backgroundImgs.forEach(item => {
let url = item.replace(/url[(]"/g, '').replace(/"/g, '')
let img = document.createElement('img')
img.src = url
doc.querySelector("body").appendChild(img)
})
}
//配置无效img绕过背景图片路径限制
doc.write(html_src_add)
setLoadWeChatSuccess(true)
// 设置主体内容为可见
const rich_media_content = doc.querySelector(".rich_media_content")
rich_media_content.style.visibility = 'unset'
const images = doc.querySelectorAll('img')
images.forEach(function (image) {
image.style.maxWidth = '100%'
})
}).catch(function (error) { // 请求失败处理
setLoadWeChatSuccess(false)
console.log(error)
})
}
<Iframe url={webViewUrl}
width="100%"
id="iFrame-id"
className="myClassname"
height="87%"
styles={{height: "87%"}}
security="restricted"
sandbox=""
/>
至此,已经可以实现在iframe绕过微信防盗链加载微信文章了
到这里以为就很简单了,剩下的就是埋点统计访问量的问题了,写一个中间页,转发分享时候把中间页地址带上要跳转的url、分享用户信息等分享出去,在中间页埋点,埋点成功后通过重定向到文章地址就解决问题了
然后就写下了这样几行代码,其中神策的pageview事件在setTitle完成,url的公共参数在神策js已经处理,这里省略
function UrlMiddlePage() {
useEffect(() => {
const search = new URLSearchParams(window.location.search)
const url = search.get('jumpUrl')
const title = search.get('title')
setTitle(title)
window.location.replace(url)
}, [])
return <Loading key='loading' show />
}
本以为这样已经没问题了,其实在浏览器上确实没有问题了
然后,坑来了
当我使用微信打开中间页时候,可以成功跳转微信公众号文章,但是当我返回上一页的时候,居然是展示了中间页,微信浏览器重定向居然不起作用??重定向之后的中间页的页面栈还在,所以导致重定向到微信文章后,有两个页面,这样就会出现死循环,看了文章返回中间页,然后触发useEffect,又跳转微信公众号文章
解决方案
既然已经知道是微信浏览器不支持重定向,那就想加个状态判断,如果是已经跳转过微信文章,就关闭微信浏览器退出即可,这时候首先想到的是加个state存一下状态即可,但是后来发现,在微信文章返回中间页,中间页是重新加载的,所以不能用state保存状态了,只能用sessionStorage进行状态存储,跳转之前设置sessionStorage,后续判断sessionStorage有值的话,直接关闭微信浏览器即可,其中使用window.WeixinJSBridge进行浏览器的关闭
注意:本来关闭微信浏览器是想用wx.closeWindow api的,但是发现该api只能是手动触发,否则不起作用,所以转成使用window.WeixinJSBridge.call(“closeWindow”)
function UrlMiddlePage() {
useEffect(() => {
if (sessionStorage.getItem(KEY)) {
closeWindow()
return
} else if (isInWechatBrowser() || isInWeComBrowser()) {
sessionStorage.setItem(KEY, KEY)
}
const search = new URLSearchParams(window.location.search)
const url = search.get('jumpUrl')
const title = search.get('title')
setTitle(title)
window.location.replace(url)
}, [])
// 兼容微信,微信执行window.location.replace,页面栈还是存在的,关闭微信的页面,退回聊天页面
const closeWindow = () => {
if (window.WeixinJSBridge) {
window.WeixinJSBridge.call("closeWindow")
} else if (document.addEventListener) {
document.addEventListener("WeixinJSBridgeReady", () => {
window.WeixinJSBridge && window.WeixinJSBridge.call("closeWindow")
})
} else if (document.attachEvent) {
document.attachEvent("onWeixinJSBridgeReady", () => {
// 关闭ios的页面
window.WeixinJSBridge && window.WeixinJSBridge.call("closeWindow")
})
}
}
return <Loading key='loading' show />
}
还有高手
到这里,本以为已经完美解决问题了(测试机是安卓手机),上到ios测试的时候,又出现问题了,在ios的微信浏览器中,在微信文章返回中间页时候,是不会触发页面的重新加载、也不会触发useEffect的(企微对微信聊天框也会这样),导致没有触发判断sessionStorage的时机,最终,发现在返回中间页的时候,会触发window.onpageshow,只要判断是ios并且是微信的情况下,在window.onpageshow判断sessionStorage即可
最终代码如下:
function UrlMiddlePage() {
useEffect(() => {
processWeChatSpecialScenario()
// 安卓微信,在其他域名页面返回本页面时候会触发useEffect
if (sessionStorage.getItem(KEY)) {
closeWindow()
return
} else if (isInWechatBrowser() || isInWeComBrowser()) {
sessionStorage.setItem(KEY, KEY)
}
const search = new URLSearchParams(window.location.search)
const url = search.get('jumpUrl')
const title = search.get('title')
setTitle(title)
window.location.replace(url)
}, [])
/**
* 处理微信浏览器、企微浏览器的特定场景
* IOS微信或者企微对微信聊天框 ,在window.location.replace跳转页面后,返回不会触发useEffect,但是会触发window.onpageshow
*/
const processWeChatSpecialScenario = () => {
if (!isInWechatBrowser() && !isInWeComBrowser()) { return }
window.onpageshow = (event) => {
if (!event.persisted || !sessionStorage.getItem(KEY)) { return }
closeWindow()
}
}
// 兼容微信,微信执行window.location.replace,页面栈还是存在的,关闭微信的页面,退回聊天页面
const closeWindow = () => {
if (window.WeixinJSBridge) {
window.WeixinJSBridge.call("closeWindow")
} else if (document.addEventListener) {
document.addEventListener("WeixinJSBridgeReady", () => {
window.WeixinJSBridge && window.WeixinJSBridge.call("closeWindow")
})
} else if (document.attachEvent) {
document.attachEvent("onWeixinJSBridgeReady", () => {
// 关闭ios的页面
window.WeixinJSBridge && window.WeixinJSBridge.call("closeWindow")
})
}
}
return <Loading key='loading' show />
}
总结
到这里,已经成功完整需求,包括:
- 通过cors-anywhere实现在iframe展示微信文章
- 兼容微信浏览器不能重定向问题,使用WeixinJSBridge.call(“closeWindow”)关闭微信浏览器
- 兼容ios、android的微信返回中间页的触发事件和机制问题