uniapp WeChat アプレット WebView の落とし穴

睡眠アプリ


WeChat ミニ プログラムには多くの機能制限と制約があり、場合によっては、開発および実装要件に Webview を使用する必要があります。

Nativeでは満足できない(例えば、SDKを保守しているチームがWEB側のjsSDKのみを提供し、ミニプログラムのSDKを保守していない)
H5なら複数の端末に同時に適用できる(適用範囲が広がる)
H5で不足を補える
WeChat エコシステムにはいくつかの制限があります (パッケージ サイズ、設計仕様など)

最近作成した小さなプログラムは透明チャンネルでのビデオの再生をサポートしていないため、Webview に切り替えました。

ここでは、完全な開発プロセスとさまざまな既存のソリューションの概要を示します。


: 開発段階では正当なドメイン名を確認する必要があるため、検証しないでください。


ウェブビュースタイル

ミニ プログラムが Web ビューに埋め込まれると、自動的にページ全体をカバーし、幅と高さを無効に設定し、他のコンポーネントをカバーします。

また、ネイティブナビゲーションバーを搭載しますが、このナビゲーションバーはミニプログラム側やWebView側で設定しても削除できず、無効となりますnavigationStyle: custom

ここに画像の説明を挿入します

ナビゲーション バーのタイトルは、最初にアプレットpages.json内の対応するコンポーネントを読み込みますnavigationBarTitleText。そうでない場合は、globalStyle値が読み取られます。

その後、タイトルが Web ビュー側の対応するコンポーネントに更新されますnavigationBarTitleText。そうでない場合は、globalStyle値が読み取られます。

WebView 側に何もない場合はnavigationBarTitleText、アプレット側の値のみが表示され、更新されません。

存在しない場合は空白で表示され、背景色を変更できます (16 進数のカラーコードのみがサポートされています)。



WeChat アプレットと WebView の通信

  • アプレットは、URL スプライシング パラメータを通じて Webview 側にパラメータを渡すことができます。

  • 現在、WebView 側はミニ プログラムにwx.miniProgram.postMessageパラメーターを渡すことしかできませんが、この API は非常に制限
    されており、特定の時間 (ミニ プログラムの退避、コンポーネントの破棄、共有) でのみコンポーネントのメッセージ イベントをトリガーできるため、基本的には役に立ちません。 。

  • メッセージは、webSocket を介した長い接続でのみ送受信できますが、これによりサーバーの負荷が増加します。

アプレットが Web ビューにパラメータを渡すのは非常に簡単ですが、その逆の場合は非常に面倒です。

WebビューWebページとJSSDKが提供するAPI以外のミニプログラムとの間の通信はサポートされていません。

JSSDK API を呼び出しても問題が解決せず、WebSocket を使用したくない場合は、単一の実装に小さなプログラムを使用するのが最善です。

iOS では、JSSDK インターフェースを呼び出したときに応答がない場合、 Web-view の src の後に#wechat_redirect解決策を追加できます。



JSSDK

Webview 側でミニ プログラムのインターフェイスを呼び出したい場合は、jssdk の公式ドキュメントが必要です。

ドメイン名をバインドする

Webviewでjssdkを使用したい場合は、次のように設定する必要がありますJS接口安全域名

私たちは webview (パブリック アカウントとみなされます) を使用しているため、これはミニ プログラムではありません (ミニ プログラムは webview を運ぶ単なるシェルです)

そのため、公式アカウント設定の機能設定に記入されておりJS接口安全域名、WebViewが配置されているドメイン名を設定した上でのみ認証が許可されます。

注:サーバーのセットアップにはパブリック ネットワークICP 登録JS接口安全域名が必要です。つまり、ローカル環境は使用できず、オンライン サーバー環境が必要です。

ここに画像の説明を挿入します
WebView がデプロイされるパスが の場合www.abc.xx/page、ドメイン名も に設定する必要があります。www.abc.xx/page

业务域名、、js接口安全域名网页授权域名一緒に同じに設定でき、それらはすべて Web ビューがデプロイされるドメイン名にすることができます。

設定時には、設定ファイルを対応するディレクトリに保存する必要があります。ダウンロードして配置するだけです。設定に失敗する場合は、プロキシ ジャンプが設定されており、ファイルが読み込まれていない可能性があります。

nginxの設定については、nginxの設定を参照してください


JSファイルをインポートする

次に、jssdkのjsファイルがWebviewに導入されます

ただし、<script>経由で導入した場合、wx.config is not a function

これは、uniapp 自体が wx オブジェクトを持っているため、jssdk の wx オブジェクトと競合するためです。


ここでの解決策は、2 つの js ファイルを別々に保存し、インポートを通じて名前を変更してインポートすることです

まず、2 つの 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 ファイルに保存し、インポートを通じて導入し、jssdk の wx 変数の名前を jweixin に変更します。

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

設定インジェクション権限

ミニ プログラムの API を呼び出す必要がある場合は、呼び出す前に承認され、使用する必要があるインターフェイスを登録する必要があります。

したがって、最初に権限を確認するための設定が必要です

// 官方文档示例代码 仅展示参数 无实际作用 业务代码在下文
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: これは自分で設定した値です。気軽に入力できますが、その後の署名生成でも使用されます。この 2 つの場所で使用される値が同じであることを確認する必要があります。
  • jsApiList: 配列に記入されているのは、その後の開発で使用する必要のある API です。ここで登録した場合のみ呼び出すことができます。記入できる値は、ドキュメント下部の付録 2 にあります
  • signature: 重要なのは検証する必要がある署名であり、この署名は一連のパラメータに基づいて生成されます。

サイン

access_tokenを取得し、access_tokengo に従って取得しjsapi_ticketてから、jsapi_ticketgo およびその他のパラメータに従って暗号化して取得する必要があります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)
}

署名を取得したら、WeChat 検証 URLで生成した署名と比較して、問題があるかどうかを確認できます。

実際には、検証と暗号化のステップはサーバー側に配置する必要がありますが、便宜上、ここではフロントエンドに実装しています。

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

ここで、権限の取得が成功すれば基本的に設定は完了し、WeChatがポップアップ表示されればconfig: ok成功です。

コンフィグの検証結果は、ミニプログラムおよびミニプログラムの公式アカウント Web デバッグツールで確認できます。

アプレット

ここに画像の説明を挿入します
ここに画像の説明を挿入します

一般公開なし

ここに画像の説明を挿入します
URLを入力してジャンプ

ここに画像の説明を挿入します

次に、jweixin.startRecord呼び出し API を使用します。構成が失敗した場合は、どのフィールドにエラーがあるかを確認します。


jssdk は比較的脆弱であり、多くのメソッドは非同期であるため、呼び出し頻度が速くなりすぎないように制御する必要があります。

たとえばstartRecordstopRecord呼び出しが同時にトリガーされた場合、それらの呼び出しがstopRecord先に実行され、コールバックがトリガーされない可能性があります。

具体的な詳細については、このブログを参照してください。ただし、ここでは詳しく説明しません。


: この方法はオンライン デバッグのみをサポートしており、公式アカウント検証のホワイトリストに含まれていないものとしてローカルに表示されますlocalhost

ミニ プログラムがオンラインになると、合法的なドメイン名の検証が自動的に有効になります。このとき、ミニ プログラムのオープン プラットフォーム - 開発管理 - 開発設定 ( ) の公式アカウントに安全なドメイン名を追加する必要があります。試用www.abc.xx版でアクセスできます




その他の問題

WebView では、2 本の指で Web ページを拡大できます。

メタタグを使用してページの拡大を無効にする

<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 キャッシュ

Web ビューを更新するたびに、アプレットのキャッシュが原因で、ページを開くたびに最新のページにアクセスできなくなります。

  1. Webview コンポーネントの src にタイムスタンプを追加できます。
src = `https://XXX.com?timestamp=${new Date().getTime()}`
<web-view src='{
     
     {src}}'></web-view>
  1. 非キャッシュ設定を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. Android 側で動作しない場合は、
    WeChat の [設定] -> [一般] -> [記憶域スペース] でキャッシュを直接クリアし、ミニ プログラムを通じて最新の WebView にアクセスしてください。

Webビューで画像を長押しすると、システムメニューがポップアップ表示されます

これはユニアプリtouchstarttouchendイベントに影響を与えますし、あまり美しくありません。

解決策は、すべての img タグに CSS を追加するpointer-events:noneことです


Webビューで指をスライドさせて方向を決定する

指を左にスワイプするとメニューが表示され、指を右にスワイプするとメニューが戻るというのが要件ですが、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% 効果的ではなく、ビデオが配置されているコンポーネントをサブコンポーネントに移行すると失敗します。具体的な理由は不明です。

ページの初期化中にビデオを直接再生しても有効にならず、エラーが報告されます ( 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 でビデオを再生する

v-showWebview では、ビデオを表示する前に一定時間再生するように制御することはできません。ビデオが非表示になっている場合、ビデオは再生されません

trueに切り替えるとv-show、表示時に再生が開始されず、常に一時停止状態になります。

一定期間再生した後にビデオを表示する必要がある場合は、css をopacity0 または 1 に設定して表示または非表示を制御することしかできません。




全体として、uniapp での開発経験は非常に悪く、問題解決におけるコミュニティの効率には懸念があり、インターネット上の情報も非常に断片的であり、多くの時間を無駄にしています。

おすすめ

転載: blog.csdn.net/Raccon_/article/details/132588352