ymcai

微信小程序混合开发笔记

类似 Hybrid App,小程序 H5 混合开发就是 在一个小程序中,采用部分小程序原生页面,部分内嵌 H5 页面¹,二者配合实现完整业务逻辑的方案。

为什么要做小程序 H5 混合开发

项目初期为支持业务团队探索方向,需同时开发上线多套 H5、微信小程序,在这个背景下选择了多端编译框架 Taro。随着时间推移,Taro 方案暴露出一些痛点:隐蔽的坑点多,响应用户操作时存在几百毫秒的延迟,难复用开源库或者引入后需要做较大改造等。

考虑到:现时需集中力量较快支持 H5 和微信小程序同步上线,未来不排除上线其他小程序平台的可能,改造为原生小程序开发显然是不可行的;小程序不能愉快地使用 canvas(会穿透底栏),而近期需求越来越多要用到 canvas,比如展示图表。

权衡之下,引入 H5 混合开发或许是比较合适的方向

通信机制

微信并不鼓励在小程序中大范围嵌入 H5,为了避免开发者把小程序变成“浏览器”,微信对小程序与内嵌 H5 的通讯做了诸多限制。

我们需要基于极有限的 API 实现小程序与内嵌 H5 的双向通信。

H5 向小程序发送消息

postMessage

小程序内嵌 H5 可以使用 JSSDK 向小程序发送消息。

wx.miniProgram.postMessage({ data: 'foo' })

小程序只能在少数的时机,例如后退、分享等,处理这些消息。所以类似 H5 调起小程序浮层这样的需求是做不到的。

路由传参

在使用 JSSDK 调起小程序页面时,可以通过在页面路径后携带参数的方式通信。

wx.miniProgram.navigateTo({ url: '/path/to/page?foo=bar' }) 

小程序向 H5 发送消息

小程序没有提供专门用于向内嵌 H5 发送消息的接口,唯一的通信途径只有网页 URL。

URL search 传参

在 H5 初始加载时,通常会带上用户的登录态作为参数,使 H5 可以继续与后台交互。

https://example.com/h5.html?token=SGVsbG9Xb3JkIQ==

URL hash 传参

在 H5 浏览期间,可以 利用网页 URL 中的 hash 部分改变不会导致网页重新加载的特点,实现小程序向 H5 发送消息

https://example.com/h5.html#/?message=%7B%22foo%22%3A%22bar%22%7D

小程序将消息内容字符串化后,更新到网页链接的 hash 部分:

let hash = '#?message=' + encodeURIComponent(JSON.stringify({ foo: 'bar' }))
let url = 'https://example.com/h5.html' + hash
this.setData({ webViewSrc: url })

H5 侧通过 hashchange 事件收到消息,作出响应。

由于 hash 改变会增加浏览器的历史记录,在处理完消息后 H5 应该通过 history.go(-1) 向后移动一页。

获取实时网页 URL

前面说到,小程序可以通过修改网页 URL 与 H5 通信 —— 这是以小程序能取得实时的网页 URL 为基础的,因为 用户在 H5 侧也会跳转页面,URL 是不断变化的

只有在最新的 URL 基础上改变 hash,才不会误造成跳转,打断用户操作。

不幸的是,小程序没有提供任何接口用于获取 web-view 组件³正在访问的网页 URL。需要我们迂回实现,找机会通知小程序网页 URL 的变化,尤其是通常用于实现 H5 页面路由的 hash 部分的变化。

当 H5 的路由更改时,通过 postMessage 同步 URL。

router.afterEach((to) => {
  const { name, path, fullPath, query = {} } = to
  if (query.hybridMessage) {
    // 处理来自小程序的消息...
  } else if (path !== '/' && fullPath !== lastPostedFullPath) {
    lastPostedFullPath = fullPath
    // 向小程序同步 URL 的变化
    wx.miniProgram.postMessage({
      type: MESSAGE_TYPES.ROUTE_CHANGE,
      detail: { /** 路由信息 **/ }
    })
  }
})

当 H5 调起小程序页面时,通过小程序页面路径的查询参数同步 URL。

wx.miniProgram.navigateTo({
  url: '/mini-program-page?fullPath=#/h5-page'
})

通信示意图

演示:H5 调起订阅消息

基于以上通信机制,可以实现小程序提供能力供 H5 调用。

例如,H5 调用小程序订阅消息并拿到订阅结果。

加载体验优化

对于小程序 H5 混合开发方案,产品最关心的事情莫过于白屏时间。

相比于加载小程序分包的“转菊花”,加载 H5 时的大面积白屏显得更为难熬,更像是“出了什么问题”。所以我们针对白屏时间做了一些优化。

使用骨架屏

首先,使用支持纯 HTML 引入的骨架屏代替一部分白屏,使用户尽快看到影像。

<div class="we-skeleton we-skeleton--front we-skeleton--loading" style="--skeleton-item-banner-height:40vw">
  <div class="we-skeleton-item we-skeleton-item--content">
    <span class="we-skeleton-item-line"></span>
    <span class="we-skeleton-item-line"></span>
    <span class="we-skeleton-item-line we-skeleton-item-line--sm"></span>
  </div>
  <!-- ... --> 
</div>

调整加载时序

增加骨架屏作为过渡后,弱网环境下(Slow 3G)依然有长达 4.5 秒的白屏时间 。

原来,默认情况下构建工具会将全局引入的依赖库全部打包到一个分包中,并尽早加载。过早加载暂时用不到的资源拖慢了骨架屏的显示速度。

chunk-vendors.css # 7kB
chunk-vendors.js  # 169kB

我们对入口点进行改造,与“显示骨架屏”无关的资源可以延迟加载

// index.js
import './index.scss'
import '@webank/webank-ui/theme/skeleton.css'
import '@webank/webank-ui/theme/skeleton-item.css'

// 此处应有代码 ...

const loadApp = function () {
  // 原入口点
  return import(/* webpackChunkName: "app" */ '../../app')
}

setTimeout(() => {
  parseQuery()
    // 显示骨架屏  
    .then((query) => showSkeleton({ query }))
    // 继续加载 JS
    .then(() => {
      return Promise.all([loadWeChatJssdk(), loadApp()])
    })
    .catch(() => {
      alert('网络异常,请稍后重试。')
      window.history.go(-1)
    })
})

将首屏需要的 资源内联到 HTML 文件中,省去不必要的请求耗时

// 以 Vue CLI 项目为例(vue.config.js)
config.plugin('html-index').tap((args) => {
  args[0].inlineSource = /\.css|\.js/
  return args
})
config.plugin('html-inline-source').use(HtmlWebpackInlineSourcePlugin)

这样,只需要用一个请求加载 17kB 的资源就能将骨架屏显示出来。

优化后的白屏时间,在弱网环境下能控制到 2.4 秒,正常 WIFI 环境下可以低至 30ms。

平滑内容过渡

骨架屏应该在什么时候隐藏?

简单粗暴的做法是:统一在 H5 资源加载完成,跳转具体 H5 页面时隐藏。不过这时候,H5 页面可能还没渲染,骨架屏隐藏后会再次出现短暂的白屏,出现“闪了一下”的不舒适感。

更好的做法是,针对用户落地的 H5 页面,精细控制骨架屏隐藏的时机,使其平滑过渡到页面内容

mounted() {
  // 在页面渲染后隐藏骨架屏
  this.$app.ready()
  
  // 在重要的初始请求完成后隐藏骨架屏
  fetch().then(() => {
    this.$app.ready()
  })
}

优化前后对比

总结

在人力相对充足,业务方向明确的情况下,原生或接近原生小程序的开发方案仍是开发小程序的首选。

在人力相对吃紧,业务需要快速试错的情况下,小程序 H5 混合开发是一个可尝试的方案,我个人认为是比多端编译更可靠一些的方案。

这个方案当然也有明显不足:

  • H5 调起小程序的能力时总是需要跳转页面

  • WebView 页面的导航栏只能固定用默认的白底黑字风格

在方案落地的初期,可能要面对产品的不理解 —— 增加操作步骤,交互视觉的不理解 —— 限制了自由度。需要多一些沟通和解释,使团队能够认可这个方案带来的好处。

总的来说,引入小程序 H5 混合方案后,需求上线效率有明显提升,综合体验符合预期。

注释

  1. H5 页面是指网页内的页面。

  2. WebView 页面是指小程序侧显示网页的页面,本质是小程序原生页面。

  3. web-view 组件是指小程序页面内承载网页的容器,嵌套在 WebView 页面中,本质是小程序内置组件。