大家好,我是小黑
微信h5,小程序开发已经成为前端攻城狮的日常开发之一了,相信大家也遇到过这样的需求,h5中打开跳转APP,小程序。解决方案有很多种,最直接的是直接使用微信jssdk自带的开放标签,这个开放标签很好用,但是并不友好。最近小黑就遇到一个需求,一个页面少说也要写5,6个开放标签,一开始以为没什么大问题,然后做的时候,我是这样的
这tm一大堆问题啊,下面一一说清楚
问题一:与微信有强耦合
如果是纯做h5的小伙伴要注意了,打开小程序要跟微信强耦合的,所以在默认浏览器打开就要做一层适配,例如点击按钮跳应用宝链接等
问题二:样式隔离
首先上一段官方的示例代码
<wx-open-launch-weapp
id="launch-btn"
username="gh_xxxxxxxx"
path="pages/home/index?user=123&action=abc"
>
<script type="text/wxtag-template">
<style>.btn { padding: 12px }</style>
<button class="btn">打开小程序</button>
</script>
</wx-open-launch-weapp>
可以看到,样式必须是写在标签内的class或者行内样式才能生效
特别注意,标签不支持rem,vw等适配样式
,这就是关键坑所在,有一个解决方案是自己写一个px转换函数,也就是自己根据屏幕宽度计算px值,这样适配问题解决了,但是复用问题呢?
问题三:无法封装复用
正如问题二最后提出的,由于样式class必须卸载标签内,又要另外去转换px值,复用就成为了一个非常让人窒息的问题
小黑的解决思路与实现
目前小黑用了vue3开发,针对以上问题,小黑突然有了灵感,以下代码跟vue3框架耦合,但相信思路应该可以通用到其它框架,思路如下
1. 先把微信标签要包含的内容(子元素)在标签外渲染出来
2. 遍历子元素获取样式,转成行内样式style
3. 把子元素插入到微信标签
这么看思路可能有点抽象,下面来一个一个实现吧(vue3代码),我要封装一个组件,直接以slot形式使用微信标签
微信标签要包含的内容(子元素)在标签外渲染出来
例如给这个东西包上微信标签
模板很好构造,用一个div包着图片,然后右下角再加上一个图片加上定位就可以了
<template>
<div :class="$style['vip-box']">
<img :src="src" :class="$style['img']" />
<img src="@/assets/images/detail-comment-item-vip.jpg" :class="$style['vip']" />
</div>
</template>
<style module>
.vip-box {
width: 30px;
height: 30px;
border-radius: 6px;
position: relative;
}
.img {
width: 100%;
height: 100%;
border-radius: 6px;
}
.vip {
position: absolute;
right: 0;
bottom: 0;
width: 11px;
height: 11px;
border-radius: 0px 0px 6px 0px;
}
</style>
// 这是简单的代码结构
如果直接这么写,你会发现样式一点都没生效,上面说了标签内写类名或者行内样式才有效,为了方便,省略了path之类的参数,这个读者可以自行添加封装
<wx-open-launch-weapp>
<div v-is="'script'" type="text/wxtag-template">
<div :class="$style['vip-box']">
<img :src="src" :class="$style['img']" />
<img src="@/assets/images/detail-comment-item-vip.jpg" :class="$style['vip']" />
</div>
</div>
</wx-open-launch-weapp>
下面给出我的大概代码
<template>
<div ref="slotContent" :class="$style['hidden']">
<slot></slot>
</div>
<div>
<wx-open-launch-weapp>
<div v-is="'script'" type="text/wxtag-template">
<div class="wx-container" :class="['wx-container-' + hash]"></div>
</div>
</wx-open-launch-weapp>
</div>
</template>
<style module>
.hidden {
position: absolute;
left: -9999px;
top: -9999px;
}
</style>
这个.hidden类名,和额外的div,就是为了把真实的内容先渲染出来,第一步任务已经完成了,hash
的作用是为了产生一个独一无二的class好让我们插入到指定的标签中
遍历子元素样式,转换成行内样式
1.获取子元素
要确保子元素被渲染到页面,所以获取子元素要在onMounted
中执行
onMounted(() => {
hash.value = getHash()
const children = slotContent.value && slotContent.value.children
// 如果有子元素
if (children.length > 0) {
// 复制样式
copyStyle(children[0])
const ele = children[0]
// 子元素插入到微信标签内
document.querySelector(`.wx-container-${hash.value}`)?.appendChild(ele)
}
})
2. 转换样式
这是关键的一步,这里多亏了神器getCurrentStyle
,使转换成为了可能!
// 样式遍历
function copyStyle(ele: any) {
const el = ele
const childrens = el && el.children || []
// 设置当前元素的样式
setStyle(el)
// 设置完毕后把当前的class删掉,其实单纯用作标签删不删都可以,但是如果这个组件还想做微信生态外的兼容,就要删掉防止class的样式和行内发生冲突
el.removeAttribute('class')
// 遍历子元素,复制样式
if (childrens.length > 0) {
[...childrens].forEach(child => {
copyStyle(child)
})
}
}
// 样式转换成行内样式
function setStyle(ele: any) {
// 兼容写法
let s: any
if (ele.currentStyle) {
s = ele.currentStyle;
} else {
s = window.getComputedStyle(ele, null);
}
if (props.usefulStyle && props.usefulStyle.length > 0) {
console.log("usefulStyle")
props.usefulStyle.forEach((key: string | number) => {
const el = ele
// css前缀转换
const k = transCss3Style(ele, key)
// getCurrentStyle的key可能是index,把这种key排除
const t = typeof parseInt(k)
if (t !== 'number' || isNaN(k)) {
el.style[k] = s[k]
}
})
} else {
Object.keys(s).forEach(key => {
const el = ele
// css前缀转换
const k = transCss3Style(ele, key)
// getCurrentStyle的key可能是index,把这种key排除
const t = typeof parseInt(k)
if (t !== 'number' || isNaN(k)) {
el.style[k] = s[k]
}
})
}
}
// 这个是简单的css前缀转换
function transCss3Style(ele: any, style: any): any {
const s = style[0].toUpperCase() + style.slice(1, style.length)
const transformNames: Record<string, any> = {
webkit: 'webkitTransform',
Moz: 'MozTransform',
O: 'OTransform',
ms: 'msTransform',
standard: 'transform'
};
for (var key in transformNames) {
if (ele.style[transformNames[key] + s] !== undefined) {
return transformNames[key] + s;
}
}
return style;
}
// 生成随机hash值
function getHash(): string {
const ar = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
const hs = [];
const al = ar.length;
for (let i = 0; i < 16; i += 1) {
hs.push(ar[Math.floor(Math.random() * al)]);
}
return hs.join('');
}
props.usefulStyle的意义
其实就是外部要传入一个有效的样式数组,为什么要这个东西呢,因为小黑在开发的过程中发现,getCurrentStyle
在ios15不会返回string类型的key样式键值对,只会返回以index下标的键值对,这是一个兼容性问题
所以在使用的时候是这样的
<OpenMiniProgram>
<div :class="$style['vip-box']">
<img :src="src" :class="$style['img']" />
<img src="@/assets/images/detail-comment-item-vip.jpg" :class="$style['vip']" />
</div>
</OpenMiniProgram>
OpenMiniProgram就是我们上面封装的组件名称
3. 插入到开放标签
上面的onMounted
中有这么一段代码
const ele = children[0]
// 子元素插入到微信标签内
document.querySelector(`.wx-container-${hash.value}`)?.appendChild(ele)
其作用就是把复制好样式后的子元素根据hash值获取类型插入到对应的微信标签中,如此一来,我们的组件就简单完成了
最后
上面是一个微信标签复用方案,当然也有额外的性能损耗就是必须独立渲染一遍子元素出来,还要手动执行dom操作,违背了数据驱动的思想,亲爱的你是否是更好的解决方案,欢迎留言交流