uniapp WeChat applet webview pitfalls

sleep app


WeChat mini programs have many functional limitations and constraints. In some cases, webview has to be used for development and implementation requirements, such as

Native cannot satisfy (for example, a team maintaining SDK only provides WEB-side jsSDK and does not maintain mini-program SDK)
H5 can be applied to multiple terminals at the same time (wider scope of application)
H5 can make up for the lack of mini-programs
. There are some limitations in the WeChat ecosystem (package size, design specifications, etc.)

Since the small program I recently made does not support playing videos with transparent channels, I switched to webview.

Here is a summary of the complete development process and various existing solutions


Note : Do not verify legal domain names needs to be checked during the development stage


webview style

When the mini program is embedded in the webview, it will automatically cover the entire page , set the width and height to be invalid, and cover other components.

And it will have a native navigation bar. This navigation bar cannot be eliminated, even if it is set on the mini program side and webview side, it will navigationStyle: custombe invalid.

Insert image description here

pages.jsonThe title of the navigation bar will first load the corresponding component in the applet navigationBarTitleText. If not, globalStylethe value will be read.

Then the title will be refreshed to the corresponding component on the webview side navigationBarTitleText. If not, globalStylethe value will be read.

If there is none on the webview side navigationBarTitleText, only the value on the applet side will be displayed and will not be refreshed.

If there are none, it is displayed blank and its background color can be changed. Only hexadecimal color codes are supported.



WeChat applet and webview communication

  • The applet can pass parameters to the webview side through URL splicing parameters.

  • wx.miniProgram.postMessageCurrently, the webview side can only pass parameters to the mini program , but this API is very limited
    . It can only trigger the message event of the component at specific times (mini program retreat, component destruction, sharing), so it is basically useless.

  • Messages can only be sent and received through long connections through webSocket, but this will increase server pressure.

It is very easy for the applet to pass parameters to the web-view, but it is very painful when the other way around.

Communication between web-view web pages and mini programs other than the API provided by JSSDK is not supported.

If calling the JSSDK api does not solve the problem and you do not want to use websocket, it is best to use a small program for single implementation.

In iOS, if there is no response when calling the JSSDK interface, you can add a #wechat_redirectsolution after the src of web-view.



JSSDK

If you want to call the interface of the mini program on the webview side, you need the official jssdk documentation.

Bind domain name

If you want to use jssdk in webview, you need to setJS接口安全域名

Since we are using webview (considered as a public account), it is not a mini program (the mini program is just a shell that carries webview)

Therefore, it is filled in the function settings of the official account settings JS接口安全域名. Authentication is allowed only after the domain name where the webview is located is set.

Note: Setting up a server JS接口安全域名requires public network and ICP registration , which means that the local environment cannot be used and an online server environment is required.

Insert image description here
If the path where the webview is deployed is www.abc.xx/page, then the domain name should also be set towww.abc.xx/page

业务域名, js接口安全域名, 网页授权域名can be set to the same together, they can all be the domain names where the webview is deployed.

When setting up, you need to save a configuration file to the corresponding directory, just download it and put it in. If the setting fails, it is most likely that the proxy jump is set and the file is not read.

For nginx settings, please refer to nginx configuration.


Import JS files

Then the js file of jssdk is introduced into the webview

However, if <script>introduced throughwx.config is not a function

This is because uniapp itself has a wx object, so it will conflict with the wx object in jssdk


The solution here is to save two js files separately and then import them by renaming them through import.

First open the addresses of the two js files

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

Then save js to local js files respectively, introduce it through import, and then rename the wx variable of jssdk to jweixin

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

config injection permissions

If you need to call the API of the mini program, you must be authorized and register the interface you need to use before you can call it.

So first you need config to verify permissions

// 官方文档示例代码 仅展示参数 无实际作用 业务代码在下文
wx.config({
    
    
  debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
  appId: '', // 必填,公众号的唯一标识
  timestamp: , // 必填,生成签名的时间戳
  nonceStr: '', // 必填,生成签名的随机串
  signature: '',// 必填,签名
  jsApiList: [] // 必填,需要使用的JS接口列表
});
  • debug: When set to true, whether the verification is passed or not, a pop-up window will be prompted for the result of calling the API. You can open it first during the debugging phase.
  • appid: This appidand all that need to be used for subsequent verification AppSecretmust be public accounts .
    If you use a mini program, you appid、AppSecretcan also get it access_token, but the subsequent signature will fail the verification (no prompt, very pitiful)
  • timestamp、nonceStr: This is a value you set by yourself. You can fill it in casually, but it will also be used in subsequent signature generation. You need to ensure that the values ​​used in these two places are the same.
  • jsApiList: What is filled in the array is the API that needs to be used for subsequent development. Only if you register here can you call it. The values ​​that can be filled in are in Appendix 2 at the bottom of the document .
  • signature: The key point is the signature that needs to be verified. This signature is generated based on a series of parameters.

signature

Need to obtain access_token, access_tokenobtain according to go jsapi_ticket, and then jsapi_ticketencrypt and obtain according to go and other parameterssignature

// 流程如下
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)
}

After getting the signature, you can compare it with the signature you generated in the WeChat verification URL to see if there are any problems.

In fact, the verification and encryption step should be placed on the server side. For convenience, it is implemented on the front end here.

 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接口列表
            });
        }
    })
},

At this step, if the permissions are obtained successfully, the configuration is basically completed. If WeChat pops up, config: okit is successful.

The verification results of the config can be viewed in the mini program and the official account web debugging tool of the mini program.

Applets

Insert image description here
Insert image description here

No public

Insert image description here
Jump after entering the URL

Insert image description here

Then use jweixin.startRecordthe call api. If the configuration fails, check which field has an error.


jssdk is relatively fragile, and many methods are asynchronous, so the calling frequency needs to be controlled not to be too fast.

For example startRecord, stopRecordif the calls are triggered at the same time, they may stopRecordbe executed first, resulting in the callback not being triggered.

You can read this blog for specific details , but I won’t go into details here.


Note : This method only supports online debugging, and it will be displayed locally localhostas not in the whitelist for public account verification.

When the mini program goes online, legal domain name verification will be automatically enabled. At this time, you need to add the safe domain name in the official account in the mini program's open platform - development management - development settings ( ), and then you can access it in the trial www.abc.xxversion




other problems

In webview, you can zoom in on a web page with two fingers

Use meta tags to disable page enlargement

<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 cache on mini program

Every time we update the webview, due to the cache of the applet, we will not be able to access the latest page every time we open it.

  1. You can add a timestamp in the src of the webview component.
src = `https://XXX.com?timestamp=${new Date().getTime()}`
<web-view src='{
     
     {src}}'></web-view>
  1. Add non-caching configuration to the head of index.html
<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. If it doesn't work on the Android side, just clear the cache directly.
    Clear the cache in WeChat Settings -> General -> Storage Space to access the latest webview through the mini program.

Long pressing the picture in webview will pop up the system menu

This will affect uniapp's touchstartand touchendevents, and it's not very beautiful either.

pointer-events:noneThe solution is to add css to all img tags


Finger sliding to determine direction in webview

The requirement is that swiping your finger to the left will pop up the menu, and swiping your finger to the right will retract the menu. However, uniapp only supports judging sliding and stopping sliding, so we need to add a judgment.

<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
			},
		}
}

Click on webview to get the video address and play it

When onloadyou get the object, just videocall it when you get the address.play

This method is not 100% effective. When I migrate the component where the video is located to a sub-component, it fails. The specific reason is unknown.

Playing the video directly during page initialization still fails to take effect and an error will be reported ( 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()

Play video on webview

In webview, it is not possible to v-showcontrol the video to play for a period of time before displaying it. If the video is hidden, the video will not play.

When v-showit is switched to true, it will not start playing when it is displayed, and will always be in a paused state.

If you need the video to be displayed after playing for a period of time, you can only use css opacityto 0 or 1 to control its display or hiding.




All in all, the development experience on uniapp is very bad, the efficiency of the community in solving problems is worrying, and the information on the Internet is also very fragmented, which wastes a lot of time.

Guess you like

Origin blog.csdn.net/Raccon_/article/details/132588352