uniapp 微信小程序webview 踩坑

2023-10-27

uniapp


微信小程序的存在许多功能上的限制和约束,有些情况不得不去使用webview进行开发实现需求,比如

原生无法满足(例如某团队维护SDK 只提供了WEB端jsSDK,且不维护小程序SDK)
H5可以同时适用多端(适用范围更广)
H5可以弥补小程序部分欠缺
微信生态有部分限制(包大小,设计规范等)

由于最近做的需求小程序不支持播放带有透明通道的视频,所以转到了webview

这里总结下完整的开发流程和现有的各种解决方案


: 开发阶段需要将不校验合法域名勾选上


webview样式

当小程序嵌入webview之后,会自动铺满一整个页面,设置宽高无效,并覆盖其他组件

并且会带有原生的导航栏,这个导航栏是无法消除的,即使在小程序端和webview端设置navigationStyle: custom都无效

在这里插入图片描述

导航栏的标题会先加载小程序端的pages.json中对应组件的navigationBarTitleText,如果没有则读globalStyle中的值

然后标题会刷新为webview端的对应组件的navigationBarTitleText,如果没有则读globalStyle中的值

如果webview端都没有navigationBarTitleText,则只会显示小程序端的值,不会刷新

如果都没有,则显示空白,可以改变其背景颜色,仅支持十六进制颜色码



微信小程序和webview通信

  • 小程序可以通过url拼接参数的方式向webview端传参

  • webview端向小程序传参现有只能通过wx.miniProgram.postMessage进行传参,但是这个api非常有局限性
    只有在特定时机(小程序后退、组件销毁、分享)触发组件的 message 事件,所以基本没用

  • 只能通过webSocket进行长连接发送接收消息,但是会增加服务器压力

小程序向web-view传参非常轻松,反过来则非常折磨

web-view 网页与小程序之间不支持除 JSSDK 提供的api之外的通信

如果调用JSSDK的api并不能解决问题,并且不想使用websocket,最好使用小程序单一实现

在 iOS 中,若存在 JSSDK 接口调用无响应的情况,可在 web-view 的 src 后面加个#wechat_redirect解决



JSSDK

想要在webview端调用小程序的接口,都需要jssdk 官方文档

绑定域名

如果想要在webview中使用jssdk,那么需要设置JS接口安全域名

由于我们使用的是webview(视为公众号),并不是小程序(小程序只是一个承载webview的壳子)

因此是在公众号设置的功能设置中填写JS接口安全域名,webview所在的域名设置进去后才允许鉴权

注意: 设置JS接口安全域名需要公网ICP备案了的服务器,也就是说本地的环境没办法使用,必须在线的服务器环境

在这里插入图片描述
如果webview部署所在的路径为www.abc.xx/page,那么设置域名也应该为www.abc.xx/page

业务域名js接口安全域名网页授权域名可以一起设置为相同的,都为webview部署所在的域名即可

设置的时候需要保存一个配置文件到对应的目录,下载下来放进去即可,如果设置失败大概率是设置了代理跳转了没有读到文件

nginx的设置参考 nginx配置


引入JS文件

然后就是在webview中引入jssdk的js文件

然而如果通过<script>的方式引入,会出现wx.config is not a function

这是由于uniapp本身具有wx对象,因此会跟jssdk中的wx对象冲突


这里解决方案是在分别保存两个js文件然后通过import重命名进行引入

首先打开两个js文件的地址

https://res.wx.qq.com/open/js/jweixin-1.6.0.js
https://open.work.weixin.qq.com/wwopen/js/jwxwork-1.0.0.js

然后分别保存js到本地的js文件,通过import引入,然后将jssdk的wx变量重命名为jweixin

import jweixin from './jweixin.js'
import wxx from './jwxwork-1.0.0.js'

config注入权限

如果需要调用小程序的api则必须要通过授权并且注册需要使用的接口,才能进行调用

所以首先需要config进行权限校验

// 官方文档示例代码 仅展示参数 无实际作用 业务代码在下文
wx.config({
  debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
  appId: '', // 必填,公众号的唯一标识
  timestamp: , // 必填,生成签名的时间戳
  nonceStr: '', // 必填,生成签名的随机串
  signature: '',// 必填,签名
  jsApiList: [] // 必填,需要使用的JS接口列表
});
  • debug: 设置为true时,通过或不通过校验,调用api的结果都会进行弹窗提示,调试阶段可以先打开
  • appid : 这个appid以及后面校验需要用到的AppSecret都必须是公众号的
    如果使用了小程序的appid、AppSecret也能拿到access_token,但是后续签名会校验不通过(没有提示,非常坑)
  • timestamp、nonceStr: 这是自己定的值,随便填即可,但是后续签名生成也会用到,需要保证这两个地方用的值是一样的
  • jsApiList: 数组中填的就是后续开发需要用的api,只有在这里注册了,才能进行调用,里面可以填的值在文档下面附录2
  • signature: 重点,需要进行验证的签名,这个签名是根据一系列参数生成的

signature

需获取access_token,根据access_token去获取jsapi_ticket, 再根据jsapi_ticket去和其他参数加密获取signature

// 流程如下
const appid = "xxxx"  // 注意appid 和 secret必须是公众号的!
const secret = "xxxx"
const noncestr = "Wm3WZYTPz0wzccnW" // noncestr 和 timestamp随便写,这里用了文档的
const timestamp = 1414587457
const url = 'https://www.abc.xx:5173/' // webview所在的网址,我这里部署在5173端口 
// 因为上面js安全域名设置的www.abc.xx 所以其他端口也是没问题的
// 另外需要注意 这里填写的网址需要跟<web-view src=''>的src的值一致,如果src需要携带参数 那么需要再src中用#隔开
// 如 <web-view :src="`https://www.abc.xx:5173/#/?time=${123}`"></web-view>
// 如果没用#隔开直接`?time=${123}`, 会由于这里加密用的url和webview src的url 不一致(???) 而签名失败

// const wxApiUrl = 'https://api.weixin.qq.com/cgi-bin'
const wxApiUrl = 'https://www.abc.xx:5173/api' // 由于直接请求可能会有跨域问题, 因此还需要在nginx中设置代理(这里就省略了, 就是设置proxy_pass)

const encodeUTF8 = (s) => {
  var i, r = [], c, x;
  for (i = 0; i < s.length; i++)
    if ((c = s.charCodeAt(i)) < 0x80) r.push(c);
    else if (c < 0x800) r.push(0xC0 + (c >> 6 & 0x1F), 0x80 + (c & 0x3F));
    else {
      if ((x = c ^ 0xD800) >> 10 == 0) //对四字节UTF-16转换为Unicode
        c = (x << 10) + (s.charCodeAt(++i) ^ 0xDC00) + 0x10000,
          r.push(0xF0 + (c >> 18 & 0x7), 0x80 + (c >> 12 & 0x3F));
      else r.push(0xE0 + (c >> 12 & 0xF));
      r.push(0x80 + (c >> 6 & 0x3F), 0x80 + (c & 0x3F));
    };
  return r;
}

// 字符串加密成 hex 字符串
const sha1 = (s) => {
  var data = new Uint8Array(encodeUTF8(s))
  var i, j, t;
  var l = ((data.length + 8) >>> 6 << 4) + 16, s = new Uint8Array(l << 2);
  s.set(new Uint8Array(data.buffer)), s = new Uint32Array(s.buffer);
  for (t = new DataView(s.buffer), i = 0; i < l; i++)s[i] = t.getUint32(i << 2);
  s[data.length >> 2] |= 0x80 << (24 - (data.length & 3) * 8);
  s[l - 1] = data.length << 3;
  var w = [], f = [
    function () { return m[1] & m[2] | ~m[1] & m[3]; },
    function () { return m[1] ^ m[2] ^ m[3]; },
    function () { return m[1] & m[2] | m[1] & m[3] | m[2] & m[3]; },
    function () { return m[1] ^ m[2] ^ m[3]; }
  ], rol = function (n, c) { return n << c | n >>> (32 - c); },
    k = [1518500249, 1859775393, -1894007588, -899497514],
    m = [1732584193, -271733879, null, null, -1009589776];
  m[2] = ~m[0], m[3] = ~m[1];
  for (i = 0; i < s.length; i += 16) {
    var o = m.slice(0);
    for (j = 0; j < 80; j++)
      w[j] = j < 16 ? s[i + j] : rol(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1),
        t = rol(m[0], 5) + f[j / 20 | 0]() + m[4] + w[j] + k[j / 20 | 0] | 0,
        m[1] = rol(m[1], 30), m.pop(), m.unshift(t);
    for (j = 0; j < 5; j++)m[j] = m[j] + o[j] | 0;
  };
  t = new DataView(new Uint32Array(m).buffer);
  for (var i = 0; i < 5; i++)m[i] = t.getUint32(i << 2);

  var hex = Array.prototype.map.call(new Uint8Array(new Uint32Array(m).buffer), function (e) {
    return (e < 16 ? "0" : "") + e.toString(16);
  }).join("");
  return hex;
}

class useStorage {
    /**
     * 额外设置一条 `key__expires__: 时间戳` 的storage来判断过期时间
     * @param {string} key
     * @param {any} value
     * @param {number} expired 过期时间 以分钟为单位
     * @returns {any}
     */
    
    setItem(key, value, expired) {
        uni.setStorageSync(key, JSON.stringify(value))
        if (expired) {
            uni.setStorageSync(`${key}__expires__`, Date.now() + 1000 * 60 * expired)
        }
        return value;
    }

    /**
     * 获取storage时先获取`key__expires__`的值判断时间是否过期 
     * 过期则清空该两条storage 返回空
     * @param {string} key
     * @returns {any}
     */
    getItem(key) {
        let expired = uni.getStorageSync(`${key}__expires__`) || Date.now + 1;
        const now = Date.now();

        if (now >= expired) {
            uni.removeStorageSync(key)
            uni.removeStorageSync(`${key}__expires__`)
            return;
        }
        return uni.getStorageSync(key) ? JSON.parse(uni.getStorageSync(key)) : uni.getStorageSync(key);
    }
}

const storage = new useStorage();

const fetchUrl = (url) => {
    return fetch(url).then((response) => {
        return response.json()
    })
}

// 主要逻辑  为了便于理解把工具函数全部整合到一块代码了 实际上可以把工具函数进行抽离封装
const fetToken = async () => {
    let save_ticket = storage.getItem('TICKET')
    console.log('ticket', save_ticket)
    if(!save_ticket){
        try{
            const {access_token: token} = await fetchUrl(`${wxApiUrl}/token?grant_type=client_credential&appid=${appid}&secret=${secret}`)
            const {ticket} = await fetchUrl(`${wxApiUrl}/ticket/getticket?access_token=${token}&type=jsapi`)
            save_ticket = ticket
            storage.setItem('TICKET', ticket, 100)
        }catch{
            uni.showToast({
                title: '授权异常',
                duration: 1000
            })
        }
    }
    const result = `jsapi_ticket=${save_ticket}&noncestr=${noncestr}&timestamp=${timestamp}&url=${url}`
    console.log('result', result)
    return sha1(result)
}

得到签名后可以在 微信校验网址 中对比自己生成的签名有没有问题

实际上校验加密这个步骤应该放在服务端,这里为了方便就放在了前端来实现

 onLoad() {
    // 获取ticket并注册jsApi
    fetToken().then((res) => {
        if (jweixin) {
            jweixin.config({
                debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
                appId: appid, // 必填,公众号的唯一标识
                timestamp: timestamp, // 必填,生成签名的时间戳
                nonceStr: noncestr, // 必填,生成签名的随机串
                signature: res, // 必填,签名
                jsApiList: ['startRecord', 'stopRecord', 'uploadVoice', 'downloadVoice'] // 必填,需要使用的JS接口列表
            });
        }
    })
},

进行到这一步权限如果获取成功基本就完成配置了,如果微信弹出config: ok就是成功了

小程序和小程序的公众号网页调试工具都是可以查看到config的校验结果的

小程序

在这里插入图片描述
在这里插入图片描述

公众号

在这里插入图片描述
输入网址后跳转

在这里插入图片描述

然后使用jweixin.startRecord调用api即可,如果配置失败则检查哪个字段有错误


jssdk比较脆弱, 且很多方法都是异步的,需要控制调用频率不能过快

比如startRecordstopRecord的调用,如果同时触发是可能 stopRecord先执行完的,导致回调不触发

具体详情可以看这篇博客,这里不过多赘述


: 这种方法仅支持线上调试,本地会显示localhost不在公众号校验的白名单中

小程序上线的时候会自动开启合法域名校验,此时需要在小程序的开放平台 - 开发管理 - 开发设置中把在公众号中的安全域名加上去(www.abc.xx),然后在体验版中才能访问




其他问题

webview中可以双指放大网页

使用meta标签禁止页面放大

<meta content='width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=0;' name='viewport' />
<meta name="viewport" content="width=device-width" />

小程序上webview缓存

每次更新webview时由于小程序的缓存会导致我们每次打开无法访问到最新的页面

  1. 可以在webview组件的src里面添加一个时间戳.
src = `https://XXX.com?timestamp=${new Date().getTime()}`
<web-view src='{{src}}'></web-view>
  1. 在index.html的head头部添加不缓存的配置
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
  1. 安卓端如果都不行就直接清除缓存
    在 微信 设定 -> 一般 -> 存储空间 中将缓存清除即可通过小程序访问最新的webview。

webview中长按图片会弹出系统菜单

这个会影响uniapp的touchstarttouchend事件,也不太美观

解决方案是在img标签全部添加上csspointer-events:none即可


webview中手指滑动判断方向

需求是手指往左划会弹出菜单,往右则收回菜单,但是uniapp只支持判断滑动和停止滑动,因此需要我们加个判断

<view @touchstart="touchMoveStart" @touchmove="chatExpandMove" @touchend="chatMoveEnd"></view>
export default {
        data() {
            return {
				touchingFlag: false, // 是否正在滑动
	            moveingPosition: { // 正在滑动的手指位置
	                X: 0,
	                Y: 0
	            },
	            startPosition: { // 滑动开始时点击的位置
	                X: 0,
	                Y: 0
	            },
	            endPosition: { // 滑动结束时手指的位置
	                X: 0,
	                Y: 0
	            },
	            Xflag: false, // 是否在x轴滑动
	            Yflag: false, // 是否在y轴滑动
	            startMoveTime: 0, // 开始滑动的时间 用于判断手指滑动的时间 过短则不操作
			}
		},
		methods: {
			touchMoveStart() {
			    this.startMoveTime = Date.now()
			    this.startPosition.X = event.changedTouches[0].clientX
			    this.startPosition.Y = event.changedTouches[0].clientY
			},
			/**
			 * 通过当前手指的位置和开始滑动时点击的位置来判断手指滑动的方向
			 */
			chatExpandMove() {
			    this.touchingFlag = true //移动进行
			
			    this.moveingPosition.X = event.changedTouches[0].clientX
			    this.moveingPosition.Y = event.changedTouches[0].clientY
			
			    let Xlength = parseInt(Math.abs(this.moveingPosition.X - this.startPosition.X))
			    let Ylength = parseInt(Math.abs(this.moveingPosition.Y - this.startPosition.Y))
			
			    if (Xlength > Ylength && Xlength > 10 && this.Yflag == false) { //x轴方向
			        this.Xflag = true
			        let direction = this.moveingPosition.X - this.startPosition.X > 0 ? "right" : "left"
			
			        if (direction === 'right') {
			            // 右
			        } else if (direction === 'left') {
			            // 左
			        }
			    }
			    if (Xlength < Ylength && Ylength > 10 && this.Xflag == false) { //Y轴方向
			        this.Yflag = true
			        let direction = this.moveingPosition.Y - this.startPosition.Y > 0 ? "down" : "up"
			        
			        if (direction === "up") {
			            // 上
			        } else if (direction === 'down') {
			            // 下
			        }
			    }
			},
			/**
			 * 滑动结束时判断手指滑动的距离 如果时间过短或距离过短 则取消影响
			 */
			chatMoveEnd() {
			    //关闭手指移动的标识
			    this.touchingFlag = false
			    const endTime = Date.now()
			    if (endTime - this.startTime < 300) {
			        // 如果手指滑动的距离超过0.3s 就默认不合法
			        return;
			    }
			    //获取滑动结束后的坐标
			    this.endPosition.X = event.changedTouches[0].clientX
			    this.endPosition.Y = event.changedTouches[0].clientY
			    if (Math.abs(this.endPosition.X - this.startPosition.X) > 10 && this.Xflag) { //大于10个单位才有效
			        //long的滑动长度绝对值作为视频s的值。
			        let long = Math.abs(this.endPosition.X - this.startPosition.X)
			        let height = Math.abs(this.endPosition.Y - this.startPosition.Y)
			        //left向前 right向后 
			        let elePosition = this.endPosition.X - this.startPosition.X > 0 ? "right" : "left"

					// 结束移动时与开始时的 距离 和 高度 
			    }
			    //复原互斥开关
			    this.Xflag = false
			    this.Yflag = false
			},
		}
}

webview上点击获得视频地址并播放

onload时获取到video对象,获取到地址时调用play即可

这个方法并不是100%有效,当我将video所在的组件迁移到一个子组件中时就失效了,具体原因未知

在页面初始化时直接播放视频仍然无法生效,会报错(DOMException: play() failed because the user didn't interact with the document first)

<video :src="videoPath" autoplay="true" :controls='false'  id='myVideo' @ended='videoEnd'></video>
onLoad() {
     // 获取video对象 存储起来 在ios中通过这种方法自动播放 
     // 这个wx不是jweixin 时uniapp中的wx
     if (wx) this.videoContext = wx.createVideoContext('myVideo', this);
}

// 点击播放
this.videoContext && this.videoContext.play()

webview上播放视频

在webview中时无法实现v-show控制视频播放一段时间后再显示的,如果视频处于隐藏状态,那么视频将不会播放

v-show切换为true时也不会在显示时开始播放,会一直处于暂停状态

如果需要video播放一段时间后显示,只能使用css的opacity 为0或1控制其显示或隐藏




总而言之,在uniapp上的开发体验非常糟糕,社区解决问题的效率堪忧,网上的资料也非常零散,浪费了非常多的时间。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

uniapp 微信小程序webview 踩坑 的相关文章

随机推荐

  • dubbo消费者源码解析

    一 服务消费者核心类ReferenceBean public void afterPropertiesSet throws Exception if applicationContext null BeanFactoryUtils bean
  • C++中类型转化

    对象类型向上向下转化 对象转化是对象间的按成员赋值 有新的存储被分配 有新对象产生 类型指针向上向下转化 转化后没有得到引得对象 也可以说没有新的存储分配 我们只是通过一个小的 转化后的对象的大小 视角去看待这个对象 因此在诸如对象的释 放
  • 网络总结知识点(网络工程师必备)一

    作者 小刘在C站 个人主页 小刘主页 每天分享云计算网络运维课堂笔记 努力不一定有收获 但一定会有收获加油 一起努力 共赴美好人生 夕阳下 是最美的绽放 树高千尺 落叶归根人生不易 人间真情 前言 本专栏为就业必修秘籍 技术分册 本张试读篇
  • 某程序员披露华为外包员工现状:工作氛围好,没有歧视,但转正后也不算正式员工?...

    有些想进互联网大厂的人在无法成为正式员工的时候 会通过成为外包员工的方式来曲线救国 但在互联网大厂中 外包员工和正式员工的差别待遇一直是比较热门的话题 最近 一个网友总结了华为外包和正式员工的区别 干货满满 先来看看 看完以后 许多人表示赞
  • iframe被拒绝_IFRAME权限被拒绝的解决方案

    iframe被拒绝 I was recently rolling my own AJAX upload script posting a form to a hidden IFRAME and using the load event to
  • TEMPLATE METHOD 模式

    Template Method 模式使用继承 来分离通用的算法和具体的上下文 简单的讲 我有一个冒泡排序的程序 用来对int 型数据排序 要想对double 类型的数据排序 可能又得重新编写一个排序程序 Template Method 模式
  • 微信小程序cover-image手机上不显示问题

    微信小程序标签cover image 在未设置宽高的情况下 在手机上不显示 但是在微信web开发者工具上却显示 如果我们想使图片宽高比保持一致 是图片不变形 解决方法 在cover image标签外放一个父容器给它设置相对定位 子元素设置绝
  • QT周记大总结

    初识UI 增加子组件 private 成员变量 QPushButton btn1 btn1 new QPushButton 关闭 this btn1 gt move 100 100 样式表 define QPushButton STYTLE
  • 毛笔字识别--详细注释解析恩培作品6

    感谢恩培大佬对项目进行了完整的实现 并将代码进行开源 供大家交流学习 一 项目简介 本项目最终达到的效果为识别毛笔字 并对字体进行分类 如下所示 项目用python实现 调用opencv等库 使用SVM对字体进行分类 由以下步骤组成 1 使
  • 设计约束文件SDC

    设计约束文件SDC 全称Synopsys design constraints 主要包括以下内容 Units Time capacitance Resistance Voltage Current Power 单位 时间 电容 电阻 电压
  • QJsonObject 类

    1 QJsonObject简介 QJsonObject类封装了一个JSON对象 JSON对象是键值对的列表 其中键是唯一的字符串 值由QJsonValue表示 QJsonObject可以转换为QVariantMap 也可以从QVariant
  • k8s动态使用glusterfs存储

    k8s中运行Heketi服务 在使用kismatic安装k8s和glusterfs后 需要配置k8s默认动态管理 provisioning 使用glusterfs 以下内容是实现这样的目的 Hekeli glusterfs的restful端
  • Scrapy框架的使用之Scrapy通用爬虫

    通过Scrapy 我们可以轻松地完成一个站点爬虫的编写 但如果抓取的站点量非常大 比如爬取各大媒体的新闻信息 多个Spider则可能包含很多重复代码 如果我们将各个站点的Spider的公共部分保留下来 不同的部分提取出来作为单独的配置 如爬
  • word目录编号自动更新排版技巧

    二 多级列表生成目录 文档中一般需要插入目录 手动添加 写上页面非常麻烦 而且文档修改后还得手动修改 今天给大家介绍一种通过多级列表生成目录的方法 1 选择多级列表 定义新的多级列表 2 选择 要修改的级别 选择1 在样式中 选择样式 级别
  • 数字电路-门电路

    摘要 本章系统地讲述了数字集成电路中的基本逻辑单元电路 门电路 由于门电路中的二极管和三极管经常工作在开关状态 所以首先介绍它们在开关状态下的工作特性 然后 重点讨论了目前广泛使用的CMOS门电路和TTL门电路 对于每一种门电路 除了讲解它
  • java21天打卡-day14 日期时间

    import java util Calendar import java util Date public class Day14 数字和日期 Date 题目1 分别打印出当前时间所属的年月日 Calendar类 题目2 计算出当前时间的
  • buuctf-[极客大挑战 2019]LoveSQL

    打开靶机进入之后是个登录界面 查看源码得知是get类型注入 因为是sql注入的题嘛 所以我们直接尝试用万能密码进行登录 用户名1 or 1 1 密码随意 成功登录 并没有得到flag 此时发现url中貌似可以进行sql注入 并且已经把万能密
  • scrapy 部署到服务器上

    1 安装依赖库 pip install scrapyd pip install scrapyd client 2 验证是否成功 cmd中输入 scrapyd deploy h 如下图ok的 3 运行 scrapyd dep 若遇到错误 报错
  • 运行报错Parsing error: The keyword ‘import‘ is reserved

    问 运行报错Parsing error The keyword import is reserved 答 分析 这是因为我们还没有在配置文件 eslintrc中配置parserOptions来指定语言版本为和模块类型 在 eslintrc添
  • uniapp 微信小程序webview 踩坑

    uniapp 微信小程序的存在许多功能上的限制和约束 有些情况不得不去使用webview进行开发实现需求 比如 原生无法满足 例如某团队维护SDK 只提供了WEB端jsSDK 且不维护小程序SDK H5可以同时适用多端 适用范围更广 H5可