从实际案例看 H5 与 WebView 交互

现在运营需要设计一个活动页面,这个页面需要支持如下功能:

<img src="https://img-blog.csdnimg.cn/direct/9cf7eeedd91d48088c3c1695941521cf.jpeg" width="375px"/>
<img src="https://img-blog.csdnimg.cn/direct/a78eef74969a46d0bc7f9348275329eb.jpeg" width="375px"/>

  • 支持分享到微信的好友和朋友圈
  • 支持长按保存图片
  • 支持打开小程序(一键获取干货)

同时,这个活动页面不仅能在微信浏览器打开,还希望能在 APP 内部打开,并且同样支持上述功能。

现在按终端把需求进行拆解:

  • 微信端
  • APP 端

当前专注于在微信中实现各项功能,接下来我们将一一实现这些功能。

1. 微信内

1.1 实现分享到朋友圈、会话功能

需要借助 JS-SDK 来完成相关的功能,大概步骤如下:

  • 先登录微信公众平台进入公众号设置功能设置里填写JS 接口安全域名

在这里插入图片描述

  • 引入 JS 文件

大部分情况下,都是使用单页应用模式,可以通过直接在入口文件中引入或者通过动态加载的方式来使用。在这里,直接在入口文件中引入的方式更为方便。

<script src="http://res2.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
  • 通过 config 接口注入权限验证配置
// 服务端与微信后端交互,生成签名相关信息
const sha1 = require('crypto-js/sha1');
router.post('/getWxConfig', async function (req, res) {
  try {
    // 1. 获取 access_token
    const { access_token } = await request.get('/cgi-bin/token', {
      grant_type: 'client_credential',
      appid,
      secret,
    });
    // 2. 获取 ticket
    const { ticket } = await request.get('/cgi-bin/ticket/getticket', {
      access_token,
      type: 'jsapi',
    });
    // 3. 获取签名
    const { url } = req.body;
    const noncestr = Math.random().toString(36).substring(2, 15);
    const timestamp = parseInt(new Date().getTime() / 1000) + '';
    const str = `jsapi_ticket=${ticket}&noncestr=${noncestr}&timestamp=${timestamp}&url=${url}`;
    const signature = sha1(str).toString();
    // 4. 返回数据
    res.json(
      new Result({
        data: {
          appid,
          timestamp,
          nonceStr: noncestr,
          signature,
        },
      })
    );
  } catch (error) {
    res.json(
      new Result({
        code: 'BIZ_ERROR',
        msg: error.errmsg || error.message,
      })
    );
  }
});
// H5 页面,初始化 wx 配置
import { onMounted } from 'vue';
export default {
  setup() {
    const initWechatConfig = () => {
      return getWxConfig({
        url: location.href,
      })
        .then(({ data }) => {
          const { appId, timestamp, noncestr, signature } = data;
          wx.config({
            debug: false, // 开发环境使用
            appId, // 必填,公众号的唯一标识
            timestamp, // 必填,生成签名的时间戳
            nonceStr, // 必填,生成签名的随机串
            signature, // 必填,签名
            jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData'], // 必填,需要使用的JS接口列表
          });
        })
        .catch((e) => {
          console.error('getWxConfig error:::', e);
        });
    };
    onMounted(() => {
      initWechatConfig();
    });
  },
};
  • 通过 ready 接口处理成功验证

当前面签名相关信息配置成功后,就会正常执行 ready方法。

import { onMounted } from 'vue';
const shareConfig = {
  title: '测一测你是哪种类型的程序员?',
  desc: '代码世界不止0和1,还有独特的你!',
  link: `${window.location.origin}/`,
  imgUrl: `${window.location.origin}/thumbnail.png`,
};
export default {
  setup() {
    const initWechatConfig = () => {
      return getWxConfig({
        url: location.href,
      })
        .then(({ data }) => {
          const { appId, timestamp, noncestr, signature } = data;
          wx.config({
            debug: false, // 开发环境使用
            appId, // 必填,公众号的唯一标识
            timestamp, // 必填,生成签名的时间戳
            nonceStr, // 必填,生成签名的随机串
            signature, // 必填,签名
            jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData'], // 必填,需要使用的JS接口列表
          });

          wx.ready(() => {
            const { title, desc, link, imgUrl } = shareConfig;
            // 分享会话
            wx.updateAppMessageShareData({
              title, // 分享标题
              desc, // 分享描述
              link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
              imgUrl, // 分享图标
              success() {
                // todo...
              },
            });
            // 朋友圈
            wx.updateTimelineShareData({
              title, // 分享标题
              link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
              imgUrl, // 分享图标
              success() {
                // todo...
              },
            });
          });
        })
        .catch((e) => {
          console.error('getWxConfig error:::', e);
        });
    };

    onMounted(() => {
      initWechatConfig();
    });
  },
};
  • 通过 error 接口处理失败验证
<script>
import { onMounted } from 'vue';
const shareConfig = {
  title: '测一测你是哪种类型的程序员?',
  desc: '代码世界不止0和1,还有独特的你!',
  link: `${window.location.origin}/`,
  imgUrl: `${window.location.origin}/thumbnail.png`,
};
export default {
  setup() {
    const initWechatConfig = () => {
      return getWxConfig({
        url: location.href,
      })
        .then(({ data }) => {
          const { appId, timestamp, noncestr, signature } = data;
          wx.config({
            debug: false, // 开发环境使用
            appId, // 必填,公众号的唯一标识
            timestamp, // 必填,生成签名的时间戳
            nonceStr, // 必填,生成签名的随机串
            signature, // 必填,签名
            jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData'], // 必填,需要使用的JS接口列表
          });

          wx.ready(() => {
            const { title, desc, link, imgUrl } = shareConfig;
            // 分享会话
            wx.updateAppMessageShareData({
              title, // 分享标题
              desc, // 分享描述
              link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
              imgUrl, // 分享图标
              success() {
                // todo...
              },
            });
            // 朋友圈
            wx.updateTimelineShareData({
              title, // 分享标题
              link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
              imgUrl, // 分享图标
              success() {
                // todo...
              },
            });
          });

          wx.error((res) => {
              // todo 
              // 可以在这里保存状态,并在操作相关地方提醒用户,防止用户点击没任何反馈
            console.log('res', res);
          });
        })
        .catch((e) => {
          console.error('getWxConfig error:::', e);
        });
    };

    onMounted(() => {
      initWechatConfig();
    });
  },
};
</script>

上面完成微信内如何实现分享到朋友圈、分享到会话的功能。包括公众号配置、服务端生成签名、客户端请求对应签名信息以及完成相应的初始化。

1.2 完成长按保存图片到相册

在微信内部,默认是支持长按图片进行保存到相册的。由于海报是通过 canvas 进行绘制的,我们可以使用 canvas 提供的方法 toDataURL 将图像转换为对应的 base64 格式的图片地址,然后通过 img 标签进行渲染。

<template>
  <img v-if="base64URL" src="base64URL" />
</template>

<script>
import { onMounted, ref } from 'vue';
const shareConfig = {
  title: '测一测你是哪种类型的程序员?',
  desc: '代码世界不止0和1,还有独特的你!',
  link: `${window.location.origin}/`,
  imgUrl: `${window.location.origin}/thumbnail.png`,
};
export default {
  setup() {
    const base64URL = ref('');
    // 初始化公众号配置
    const initWechatConfig = () => {
      return getWxConfig({
        url: location.href,
      })
        .then(({ data }) => {
          const { appId, timestamp, noncestr, signature } = data;

          wx.config({
            debug: false, // 开发环境使用
            appId, // 必填,公众号的唯一标识
            timestamp, // 必填,生成签名的时间戳
            nonceStr, // 必填,生成签名的随机串
            signature, // 必填,签名
            jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData'], // 必填,需要使用的JS接口列表
          });

          wx.ready(() => {
            const { title, desc, link, imgUrl } = shareConfig;
            // 分享会话
            wx.updateAppMessageShareData({
              title, // 分享标题
              desc, // 分享描述
              link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
              imgUrl, // 分享图标
              success() {
                // todo...
              },
            });
            // 朋友圈
            wx.updateTimelineShareData({
              title, // 分享标题
              link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
              imgUrl, // 分享图标
              success() {
                // todo...
              },
            });
          });

          wx.error((res) => {
            console.log('res', res);
          });
        })
        .catch((e) => {
          console.error('getWxConfig error:::', e);
        });
    };

    const drawPoster = () => {
      // 直接创建元素,在内存中直接完成绘制
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');
      const dpr = window.devicePixelRatio;
      canvas.width = 375;
      canvas.height = 1334;
      // 绘制图片
      // 绘制文本
      // ...
      base64URL.value = canvas.toDataURL();
    };

    onMounted(() => {
      initWechatConfig();
      drawPoster();
    });

    return {
      base64URL,
    };
  },
};
</script>

1.3 打开小程序

这就非常简单了,只需要获取到小程序对应 URL Scheme 即可。

<template>
  <img v-if="base64URL" src="base64URL" />
  <button @click="onGetCourse">一键获取干货</button>
</template>

<script>
import { onMounted, ref } from 'vue';
const shareConfig = {
  title: '测一测你是哪种类型的程序员?',
  desc: '代码世界不止0和1,还有独特的你!',
  link: `${window.location.origin}/`,
  imgUrl: `${window.location.origin}/thumbnail.png`,
};
export default {
  setup() {
    const base64URL = ref('');
    // 初始化公众号配置
    const initWechatConfig = () => {
      return getWxConfig({
        url: location.href,
      })
        .then(({ data }) => {
          const { appId, timestamp, noncestr, signature } = data;

          wx.config({
            debug: false, // 开发环境使用
            appId, // 必填,公众号的唯一标识
            timestamp, // 必填,生成签名的时间戳
            nonceStr, // 必填,生成签名的随机串
            signature, // 必填,签名
            jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData'], // 必填,需要使用的JS接口列表
          });

          wx.ready(() => {
            const { title, desc, link, imgUrl } = shareConfig;
            // 分享会话
            wx.updateAppMessageShareData({
              title, // 分享标题
              desc, // 分享描述
              link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
              imgUrl, // 分享图标
              success() {
                // todo...
              },
            });
            // 朋友圈
            wx.updateTimelineShareData({
              title, // 分享标题
              link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
              imgUrl, // 分享图标
              success() {
                // todo...
              },
            });
          });

          wx.error((res) => {
            console.log('res', res);
          });
        })
        .catch((e) => {
          console.error('getWxConfig error:::', e);
        });
    };

    const drawPoster = () => {
      // 直接创建元素,在内存中直接完成绘制
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');
      const dpr = window.devicePixelRatio;
      canvas.width = 375;
      canvas.height = 1334;
      // 绘制图片
      // 绘制文本
      // ...
      base64URL.value = canvas.toDataURL();
    };

    const openMiniProgram = () => {
      const miniURLScheme = 'weixin://dl/business/?t=dgQQRtdOgOs';
      location.href = miniURLScheme;
    };

    const onGetCourse = () => {
      openMiniProgram();
    };

    onMounted(() => {
      initWechatConfig();
      drawPoster();
    });

    return {
      base64URL,
      onGetCourse,
    };
  },
};
</script>

目前已经实现了在微信浏览器内部的功能,包括微信分享、长按保存图片到相册、打开小程序等。接下来我们将优化这些功能,使其能够在 APP 内部实现

2. APP 内

在微信内打开 H5 时,分享朋友圈、好友等功能通常是在微信宿主环境下结合 jweixin sdk 实现的。然而,当网页在 APP 中打开时,宿主环境变成了 APP,此时 jweixin sdk 将失效。

要实现这些功能,我们需要从 APP 这个宿主环境上来想办法。完成这些功能其实并不难。

将功能分解为:

  • APP 与 微信交换
  • APP 与 H5 交互

首先,APP 可以通过引入原生的 share 模块与微信 APP 进行交互。

现在只需要解决 H5APP 的交互就可以实现这些功能。也就是说,我们需要让 H5 能够调用 APP 中的分享功能,这样就可以实现在 APP 中打开网页时也能享受到微信的分享功能。

通常,当 webviewH5 页面需要进行交互时,会使用桥接方式,也就是所谓的"jsbridge"。简单来说,原生应用会在 webview 的上下文中注入一些 API,使得 H5 页面可以通过访问这些 API 与原生应用进行交互。同时,H5 页面也可以通过特定的渠道来接收原生应用发送的消息,实现双向通信。

在 uniapp 中也不例外, uniapp 封装 webviewAPI 成特定的 sdk 供 H5 调用,这样做的好处是方便维护和更新迭代。即使内部 API 发生变更,对外使用的 API 也可以保持不变。这样可以减少对 H5 开发者的影响,同时也提高了代码的稳定性和可维护性。

上面把整个交互流程捋顺了,接下来可以进入实际编码环节了。

2.1 APP 引入原生模块

在这里插入图片描述

说明下,我们的 APP 是通过 uniapp 开发的。上图是 HBuilderX 配置的截图。

APP 分享功能配置完成后,接着实现 H5 与 APP 交互。

2.2 实现分享到朋友圈、会话功能

2.2.1 H5 页面中引入 uni.webview.js
<script
  type="text/javascript"
  src="https://gitee.com/dcloud/uni-app/raw/dev/dist/uni.webview.1.5.4.js"
></script>
2.2.2 定义 webview 加载 H5 页面
<template>
  <web-view :src="src"></web-view>
</template>

<script>
let wv;
export default {
  data() {
    return {
      src: '',
    };
  },
  onLoad(options) {
    this.src = options.src;
    // #ifdef APP-PLUS
    // 此对象相当于 html5plus 里的 plus.webview.currentWebview()。在uni-app里vue页面直接使用plus.webview.currentWebview()无效,非v3编译模式使用this.$mp.page.
    const currentWebview = this.$scope.$getAppWebview();
    setTimeout(function () {
      wv = currentWebview.children()[0];
      wv.setStyle({
        scalable: false,
      });
    }, 200); //如果是页面初始化调用时,需要延时一下
    // #endif
  },
};
</script>
2.2.3 定义 webviewmessage 事件监听器,处理 H5 发送的事件
  • APP 监听消息
<template>
  <web-view :src="src" @message="message"></web-view>
</template>

<script>
let wv;
export default {
  data() {
    return {
      src: '',
    };
  },
  onLoad(options) {
    this.src = options.src;
    // #ifdef APP-PLUS
    // 此对象相当于 html5plus 里的 plus.webview.currentWebview()。在uni-app里vue页面直接使用plus.webview.currentWebview()无效,非v3编译模式使用this.$mp.page.
    const currentWebview = this.$scope.$getAppWebview();
    setTimeout(function () {
      wv = currentWebview.children()[0];
      wv.setStyle({
        scalable: false,
      });
    }, 200); //如果是页面初始化调用时,需要延时一下
    // #endif
  },
  methods: {
    message(event) {
      /**
       * 1. webview 需要接受多种消息类型,不同的消息类型业务逻辑不同。 考虑后面扩展把消息格式约定如下:
       * {
       *    eventType: 'share|saveImageToPhotosAlbum'
       *    data: {}
       * }
       */
      const data = event.detail.data;
      if (Array.isArray(data) && data.length > 0) {
        const { eventType, data } = data[0];
        switch (config.eventType) {
          case 'share':
            uni.share(data);
            break;
          default:
            break;
        }
      }
    },
  },
};
</script>
  • H5 发送事件
<template>
  <img v-if="base64URL" src="base64URL" />
  <button @click="onGetCourse">一键获取干货</button>
  <button @click="onAppMessageShare">分享至微信好友</button>
  <button @click="onTimelineShare">分享至朋友圈</button>
</template>

<script>
import { onMounted, ref } from 'vue';
const shareConfig = {
  title: '测一测你是哪种类型的程序员?',
  desc: '代码世界不止0和1,还有独特的你!',
  link: `${window.location.origin}/`,
  imgUrl: `${window.location.origin}/thumbnail.png`,
};
const wxSceneSession = 'WXSceneSession'; // 微信会话
const wxSceneTimeline = 'WXSceneTimeline'; // 微信朋友圈
export default {
  setup() {
    const base64URL = ref('');
    const isWechatBrowser = () => {
      const ua = window.navigator.userAgent.toString();
      return ua.includes('MicroMessenger');
    };
    // 在 uniapp webview 中
    const inWebviewInner = '__WebVieW_Id__' in window;

    // 初始化公众号配置
    const initWechatConfig = () => {
      return getWxConfig({
        url: location.href,
      })
        .then(({ data }) => {
          const { appId, timestamp, noncestr, signature } = data;

          wx.config({
            debug: false, // 开发环境使用
            appId, // 必填,公众号的唯一标识
            timestamp, // 必填,生成签名的时间戳
            nonceStr, // 必填,生成签名的随机串
            signature, // 必填,签名
            jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData'], // 必填,需要使用的JS接口列表
          });

          wx.ready(() => {
            const { title, desc, link, imgUrl } = shareConfig;
            // 分享会话
            wx.updateAppMessageShareData({
              title, // 分享标题
              desc, // 分享描述
              link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
              imgUrl, // 分享图标
              success() {
                // todo...
              },
            });
            // 朋友圈
            wx.updateTimelineShareData({
              title, // 分享标题
              link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
              imgUrl, // 分享图标
              success() {
                // todo...
              },
            });
          });

          wx.error((res) => {
            console.log('res', res);
          });
        })
        .catch((e) => {
          console.error('getWxConfig error:::', e);
        });
    };

    const drawPoster = () => {
      // 直接创建元素,在内存中直接完成绘制
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');
      const dpr = window.devicePixelRatio;
      canvas.width = 375;
      canvas.height = 1334;
      // 绘制图片
      // 绘制文本
      // ...
      base64URL.value = canvas.toDataURL();
    };

    const openMiniProgram = () => {
      const miniURLScheme = 'weixin://dl/business/?t=dgQQRtdOgOs';
      location.href = miniURLScheme;
    };

    const onGetCourse = () => {
      openMiniProgram();
    };

    const sendMessage = (scene) => {
      if (window.webUni) {
        window.webUni.postMessage({
          data: {
            eventType: 'share',
            provider: 'weixin',
            scene,
            type: 0,
            href: shareConfig.link,
            title: shareConfig.title,
            summary: shareConfig.desc,
            imageUrl: shareConfig.imgUrl,
          },
        });
      } else {
        uni.showToast({
          title: '分享功能初始化失败',
        });
      }
    };

    const onAppMessageShare = () => {
      sendMessage(wxSceneSession);
    };

    const onTimelineShare = () => {
      sendMessage(wxSceneTimeline);
    };

    const addEventListener = () => {
      document.addEventListener('UniAppJSBridgeReady', () => {
        console.log(window.webUni);
      });
    };

    onMounted(() => {
      // 这里按需执行,只在微信浏览器才执行对应的初始化操作。理想情况下,对应 wxsdk 也是可以在微信环境才去加载,这里方便就直接在入口文件中引入了。
      if (isWechatBrowser()) {
        initWechatConfig();
      } else if (inWebviewInner) {
        addEventListener();
      }
      drawPoster();
    });
    return {
      base64URL,
      onGetCourse,
      onAppMessageShare,
      onTimelineShare,
    };
  },
};
</script>

在 H5 页面上,我们已经成功实现了与 APP 的交互,并成功调用了 APP 提供的分享到朋友圈和会话的功能。

2.3 完成长按保存图片到相册

在微信浏览器内部,我们可以通过微信提供的长按图片功能来保存图片。然而,在 APP 中,我们需要自己来实现长按保存功能。现在我们来分析一下如何实现。

首先,在 H5 中,我们可以获取到图片对应的 base64 URL。同样,在APP 中,我们可以使用 saveImageToPhotosAlbum 函数来保存图片到相册。只要将图片数据发送给 APP,就可以成功将图片存储到相册中。

2.3.1 分析下 saveImageToPhotosAlbum 参数
uni.saveImageToPhotosAlbum({
  filePath:
    '图片文件路径,可以是临时文件路径也可以是永久文件路径,不支持网络图片路径',
  success: () => {},
  fail: () => {},
  complete: () => {},
});

filePath 并不支持直接传入 base64 格式以及网络图片,只能把图片先保存到本地文件系统目录下。 怎么把 base64 图片保存到本地文件系统目录下? [Bitmap](https://www.dcloud.io/docs/api/zh_cn/nativeobj.html#plus.nativeObj.Bitmap)。 在 HTML5+ 中,提供通过 window.plus.nativeObj.Bitmap 方式创建 bitmap 对象,通过 bitmap 实例中 loadBase64Data 加载 Base64 编码格式图片到 Bitmap 对象中,再调用save 方法保存图片(保存到本地文件系统中,如果图片为空或者指定的路径文件已经存在则返回失败)。

2.3.2 图片添加长按事件
<!-- H5页面 -->
<template>
  <img v-if="base64URL" src="base64URL" @longtap="onLongtap" />
  <button @click="onGetCourse">一键获取干货</button>
  <button @click="onAppMessageShare">分享至微信好友</button>
  <button @click="onTimelineShare">分享至朋友圈</button>
</template>

<script>
import { onMounted, ref } from 'vue';
const shareConfig = {
  title: '测一测你是哪种类型的程序员?',
  desc: '代码世界不止0和1,还有独特的你!',
  link: `${window.location.origin}/`,
  imgUrl: `${window.location.origin}/thumbnail.png`,
};
const wxSceneSession = 'WXSceneSession'; // 微信会话
const wxSceneTimeline = 'WXSceneTimeline'; // 微信朋友圈
export default {
  setup() {
    const base64URL = ref('');
    const isWechatBrowser = () => {
      const ua = window.navigator.userAgent.toString();
      return ua.includes('MicroMessenger');
    };
    // 在 uniapp webview 中
    const inWebviewInner = '__WebVieW_Id__' in window;

    // 初始化公众号配置
    const initWechatConfig = () => {
      return getWxConfig({
        url: location.href,
      })
        .then(({ data }) => {
          const { appId, timestamp, noncestr, signature } = data;

          wx.config({
            debug: false, // 开发环境使用
            appId, // 必填,公众号的唯一标识
            timestamp, // 必填,生成签名的时间戳
            nonceStr, // 必填,生成签名的随机串
            signature, // 必填,签名
            jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData'], // 必填,需要使用的JS接口列表
          });

          wx.ready(() => {
            const { title, desc, link, imgUrl } = shareConfig;
            // 分享会话
            wx.updateAppMessageShareData({
              title, // 分享标题
              desc, // 分享描述
              link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
              imgUrl, // 分享图标
              success() {
                // todo...
              },
            });
            // 朋友圈
            wx.updateTimelineShareData({
              title, // 分享标题
              link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
              imgUrl, // 分享图标
              success() {
                // todo...
              },
            });
          });

          wx.error((res) => {
            console.log('res', res);
          });
        })
        .catch((e) => {
          console.error('getWxConfig error:::', e);
        });
    };

    const drawPoster = () => {
      // 直接创建元素,在内存中直接完成绘制
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');
      const dpr = window.devicePixelRatio;
      canvas.width = 375;
      canvas.height = 1334;
      // 绘制图片
      // 绘制文本
      // ...
      base64URL.value = canvas.toDataURL();
    };

    const openMiniProgram = () => {
      const miniURLScheme = 'weixin://dl/business/?t=dgQQRtdOgOs';
      location.href = miniURLScheme;
    };

    const onGetCourse = () => {
      openMiniProgram();
    };

    const sendMessage = (scene) => {
      if (window.webUni) {
        window.webUni.postMessage({
          data: {
            eventType: 'share',
            provider: 'weixin',
            scene,
            type: 0,
            href: shareConfig.link,
            title: shareConfig.title,
            summary: shareConfig.desc,
            imageUrl: shareConfig.imgUrl,
          },
        });
      } else {
        uni.showToast({
          title: '分享功能初始化失败',
        });
      }
    };

    const onAppMessageShare = () => {
      sendMessage(wxSceneSession);
    };

    const onTimelineShare = () => {
      sendMessage(wxSceneTimeline);
    };

    const addEventListener = () => {
      document.addEventListener('UniAppJSBridgeReady', () => {
        console.log(window.webUni);
      });
    };

    const onLongtap = () => {
      if (window.plus) {
        const bitmap = new window.plus.nativeObj.Bitmap('poster');
        bitmap.loadBase64Data(base64URL.value, () => {
          const url = `_doc/${Date.now()}.png`;
          bitmap.save(
            url,
            {
              overwrite: true,
            },
            (i) => {
              window.webUni.postMessage({
                data: {
                  eventType: 'saveImageToPhotosAlbum',
                  data: i.target,
                },
              });
            },
            () => {
              uni.showToast({
                title: '保存图片失败',
              });
            }
          );
        });
      }
    };

    onMounted(() => {
      // 这里按需执行,只在微信浏览器才执行对应的初始化操作。理想情况下,对应 wxsdk 也是可以在微信环境才去加载,这里方便就直接在入口文件中引入了。
      if (isWechatBrowser()) {
        initWechatConfig();
      } else if (inWebviewInner) {
        addEventListener();
      }
      drawPoster();
    });
    return {
      base64URL,
      onGetCourse,
      onAppMessageShare,
      onTimelineShare,
    };
  },
};
</script>
2.3.3 webview 处理自定义的 saveImageToPhotosAlbum 事件
<template>
  <web-view :src="src" @message="message"></web-view>
</template>

<script>
let wv;
export default {
  data() {
    return {
      src: '',
    };
  },
  onLoad(options) {
    this.src = options.src;
    // #ifdef APP-PLUS
    // 此对象相当于 html5plus 里的 plus.webview.currentWebview()。在uni-app里vue页面直接使用plus.webview.currentWebview()无效,非v3编译模式使用this.$mp.page.
    const currentWebview = this.$scope.$getAppWebview();
    setTimeout(function () {
      wv = currentWebview.children()[0];
      wv.setStyle({
        scalable: false,
      });
    }, 200); //如果是页面初始化调用时,需要延时一下
    // #endif
  },
  methods: {
    message(event) {
      /**
       * 1. webview 需要接受多种消息类型,不同的消息类型业务逻辑不同。 考虑后面扩展把消息格式约定如下:
       * {
       *    eventType: 'share|saveImageToPhotosAlbum|lanuchMinPro'
       *    data: {}
       * }
       */
      const data = event.detail.data;
      if (Array.isArray(data) && data.length > 0) {
        const { eventType, data } = data[0];
        switch (config.eventType) {
          case 'share':
            uni.share(data);
            break;
          case 'saveImageToPhotosAlbum':
            uni.saveImageToPhotosAlbum({
              filePath: config.data,
              success: () => {
                uni.showToast({
                  title: '图片保存成功',
                });
              },
              fail: () => {
                uni.showToast({
                  title: '图片保存失败',
                });
              },
            });
            break;
          default:
            break;
        }
      }
    },
  },
};
</script>

2.4 打开小程序

APP 中打开小程序,可以通过如下的方式:

// 方式一:
const openMiniProgram = () => {
  const miniURLScheme = 'weixin://dl/business/?t=dgQQRtdOgOs';
  location.href = miniURLScheme;
};

// 方式二:
const openMiniProgram2 = () => {
  // #ifdef APP
  plus.share.getServices(function (res) {
    let sweixin = null;
    //
    for (let i = 0; i < res.length; i++) {
      let t = res[i];
      if (t.id === 'weixin') {
        sweixin = t;
      }
    }

    if (sweixin) {
      sweixin.launchMiniProgram({
        id: 'gh_245f93d8f342',
        path: '/pages/mine/index',
        type: 0,
      });
    } else {
      uni.showToast({
        title: '当前环境不支持微信操作!',
      });
    }
  });
  // #endif
};

到目前为止,我们已经成功在微信APP 的不同宿主环境中分别实现了所有功能。

3. 常见问题

3.1 如何在本地调试微信分享功能

  • 下载花生壳
  • 配置外网映射(将本地 H5 项目 IP 和端口映射到外网)
  • 在公众号将外网映射域名添加到公众号中的JS 接口安全域名
  • 这样就可以很方便调试公众号相关的分享功能了

    3.2 配置服务器白名单

    在服务端调用微信接口时,如果没有配置服务器白名单,就会出现如下错误:

  • "invalid ip 175.9.143.154 ipv6 ::ffff:175.9.143.154, not in whitelist rid: 6571d1d4-7f45e7df-2d05fc1b"

原因是微信 access_token 刷新需要添加服务器白名单

在这里插入图片描述

注意:配置完成后,并不会实时生效。

如果您有任何疑问,请随时在评论区留言。

总结

本文介绍了如何利用 jweixin sdk 在微信内部完成会话和朋友圈分享。同时,还讲解了 webviewH5 交互的原理,并介绍了如何借助 APP 的能力实现微信分享功能。最后,还详细讲解了如何利用 bitmapH5 中的 base64 图片格式存储到本地系统目录中,并最终将图片保存到相册中。


零一行者
781 声望337 粉丝

专注力