乐趣区

谜之wxsuniapp如何用它大幅提升性能

小程序技术领域,有几个谜一样的存在:微信的 WXS、支付宝的 SJS、百度的 Filter。

很多开发者都不明白为什么要造这种语言脚本的轮子出来,甚至很多开发者根本不知道它们的存在。

其实几大小程序平台创造它们,都是为了解决性能问题,但不得不吐槽下,设计的实在是很难用,文档也语焉不详。

uni-app支持将 WXSSJSFilter 编译到这 3 家小程序平台,同时还在 App 和 H5 实现了 WXS 的解析。为什么做这些事?也是为了性能。

比如 uni-ui 组件库中的 swiperaction 组件,就是列表项向左滑动时拉出几个挤压式联动的菜单按钮,这种流畅的跟手动画,正是借助于 WXS 机制实现的。

微信为何要创造 WXS

WXS(WeiXin Script)是微信创造的一套脚本语言,它的官方说法是:“WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致”。

那微信为何要脱离 JavaScript,单独创造一套语言呢?这要从微信小程序的底层逻辑(运行环境)讲起。

小程序的运行环境分为逻辑层和视图层,分别由 2 个线程管理,其中:

  • WXML 模板和 WXSS 样式工作在视图层,界面使用 WebView 进行渲染
  • JavaScript 代码工作在逻辑层,运行在 JsCore 或 v8 里

小程序在视图层与逻辑层两个线程间提供了数据传输和事件系统。这样的分离设计,带来了显而易见的好处:

  • 逻辑和视图分离,即使业务逻辑计算非常繁忙,也不会阻塞渲染和用户在视图层上的交互

但同时也带来了明显的坏处:

  • 视图层(webview)中不能运行 JS,而逻辑层 JS 又无法直接修改页面 DOM,数据更新及事件系统只能靠线程间通讯,但跨线程通信的成本极高,特别是需要频繁通信的场景

什么是需要频繁通讯的场景?最典型的例子就是用户持续交互的情况,比如触摸、滚动等。我们以侧滑菜单为例,假设在页面上滑动 A 元素,要求 B 元素跟随移动,一次滑动操作(touchmove)的响应过程如下:

  1. touchmove 事件从视图层(Webview)传递到逻辑层,中间会由微信客户端(Native)做中转

  1. 逻辑层处理 touchmove 事件,计算需移动的位置,然后再通过 setData 传递到视图层,中间同样会由微信客户端(Native)做中转

一次 touchmove 的响应需要经过 视图层、Native、逻辑层三者之间 2 个完整来回的通信,通信的耗时开销较大,用户的交互就会出现延时卡顿的情况。

除了滚动、拖动交互外,在 for 循环里对数据做格式修改,也会造成逻辑层和视图层频繁通讯。

其实这类通信损耗问题,在业内由来已久,react native 和 weex 都有类似问题,weex 提供了 bindingx 来解决。

但对于小程序来讲,这类问题解决起来更容易。因为其实视图层的 webview,是有 js 环境的,只不过过去不给开发者开放。

如果在视图层的 js 直接处理滚动或拖动交互、直接处理数据格式,就能避免大量通信损耗。

但对于小程序平台而言,大量开放 webview 里的 js 编写,违反了它的初衷,比如开发者会直接操作 dom,影响性能体验。所以小程序平台提出一种新规范,限制 webview 里可运行的 js 的能力。这就是 wxs、sjs、filter 的由来。

从本质来讲,wxs、sjs、filter 是一种被限制过的、运行在视图层 webview 里的 js。它并不是真的发明了一种新语言。

WXS 特征及适用场景

WXS 具备如下特征:

  • WXS 是可以在视图层(webview)中运行的 JS
  • WXS 无法直接修改业务数据,仅能设置当前组件的 classstyle,或者对数据进行格式化。要修改逻辑层的数据,需要通过 callMethod,传递参数给逻辑层
  • WXS 是被限制过的 JavaScript,可以进行一些简单的逻辑运算
  • WXS 可以监听 touch 事件,处理滚动、拖动交互

故可以得出 WXS 的适用场景,主要包括:

  • 用户交互频繁、仅需改动组件样式(比如布局位置),无需改动数据内容的场景,比如侧滑菜单、索引列表、滚动渐变等
  • 数据格式处理,比如文本、日期格式化,或者国际化。通过 WXS 可以模拟实现 Vue 框架的过滤器, 如下是一个通过 wxs 实现首字母大写的示例:
<wxs module="m1">
  // 首字母大写
  var capitalize = function(value) {if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
  module.exports = {capitalize: capitalize}
</wxs>
<view class="content">
  <view class="text-area">
    <!-- title 为当前页面 data 中定义的初始数据 -->
    <text class="title">{{m1.capitalize(title)}}</text>
  </view>
</view>

uni-app 如何支持 WXS

uni-app遵循 Vue 单文件组件(SFC)规范,组件 / 样式 / 脚本是写在一个 .vue 文件中的,但微信小程序是多文件分离(wxml/wxss/js/json)的,所以在微信端的主要工作是扩展 vue-template-compiler,解析template/style/script 节点,并正确生成到对应的 wxml/wxss/js 文件中,具体编译工作如下图:

Tips-1:关于 <wxs> 标签重构为 <script lang="wxs"> 的说明:

.vue 文件中的 <wxs> 标签及内嵌 WXS 代码,在主流前端开发工具(vscode/HBuilderX 等)中,均无法实现语法提示、代码高亮及格式化,故 uni-app<wxs module="m1">重构为 <script module="m1" lang="wxs">,便捷实现了语法提示、代码高亮等,如下为 vscode/HBuilderX 中对于<wxs> 标签重构前后的代码高亮对比,明显重构为 <script lang="wxs"> 后,开发体验更佳:

Tips-2:鉴于 Vue 的自定义标签规范,我们建议将 <wxs><script lang="wxs">)和template 平级编写

编译器的具体解析扩展工作,这里不详述,仅给出 wxs 生成的示例代码,让大家有个直观理解:

createFilterTag (filterTag, {
    content,
    attrs
  }) {content = content.trim()
    if (content) { //<wxs> 标签内直接编写 wxs 代码
      return `<${filterTag} module="${attrs.module}">
        ${content}
        </${filterTag}>`
    } else if (attrs.src) { // 外联 .wxs 文件
      return `<${filterTag} src="${attrs.src}" module="${attrs.module}"></${filterTag}>`
    }
  }

在保证编译正确的情况下,微信小程序运行时会正确解析并执行 WXS 脚本,框架 runtime 无需干预。

基于 WXS 提升性能体验的实现示例

下面的 gif 显示的内容,是借助 WXS 实现的一个 swipeaction 组件示例,列表项向左滑动时拉出几个挤压式联动的菜单按钮,跟手动画、回弹动画都很自然流畅。

这里简单给出主要实现思路:

  1. 在 vue 中引用 wxs 文件,并绑定 touch 事件
<template>
  <view class="uni-swipe_content">
      <!-- 可滑动的菜单项容器,绑定 touch 事件 -->
    <view :data-position="pos" class="move-hock"
      @touchstart="swipe.touchstart" @touchmove="swipe.touchmove" @touchend="swipe.touchend" @change="change">
      <view class="uni-swipe_box">
        <slot />
      </view>
      <view class="uni-swipe_button-group move-hock">
          <!-- 滑动后,右侧挤压式的联动菜单按钮 -->
        <view v-for="(item,index) in options" :data-button="btn" :key="index"  class="button-hock">
        {{item.text}}
        </view>
      </view>
    </view>
  </view>
</template>
<script module="swipe" lang="wxs" src="./index.wxs"></script>
  1. 在 wxs 文件中,处理 touch 事件逻辑,通过 translateX 移动元素位置
function touchstart(e, ins) {
    // 记录开始位置及动画状态
  var pageX = e.touches[0].pageX;
  ....
}

function touchmove(e, ownerInstance) {
  var instance = e.instance;
  var pageX = e.touches[0].pageX;// 获取当前移动位置
  // 计算偏移位置
  var x = Math.max(-instance.getState().position[1].width, Math.min((value), 0));
  // 设置左侧元素移动位置
  instance.setStyle({transform: 'translateX(' + x + 'px)'}) 
  
  // 循环右侧挤压式联动菜单
  var btnIns = ownerInstance.selectAllComponents('.button-hock');
  for (var i = 0; i < btnIns.length; i++) {
    ...
    // 设置每个联动菜单的移动位置
    btnIns[i].setStyle({transform: 'translateX(' + (arr[i - 1] + value * (arr[i - 1] / position[1].width)) + 'px)'})
    ...
  }
}

function touchend(e, ownerInstance) {
  var instance = e.instance;
  var state = instance.getState()
  // 根据当前移动位置,实现菜单项的自动展开或回弹
  move(state.left, -40, instance, ownerInstance)
}

该示例的完整源码参考 github

在这段代码中,响应手势并移动菜单,是在视图层直接完成的。而不用 wxs 的传统写法,实现这个功能就会很卡。首先是视图层接收到 touch 事件,然后传递给逻辑层,逻辑层的 js 响应 touch 事件,判断移动距离,再通知视图层更新界面元素的位置。在持续的拖动过程中,视图层和逻辑层不停交互通信,无法做到跟手的顺滑。

虽然我们了解了 wxs 的原理,但老实讲,wxs 挺难用的,直到现在,大多数开发者仍然不会用它。比较合适的做法,还是一些框架的作者对它进行封装。uni-app提供的 uni-ui 组件库,就是这样做的,开发者只需要按标准 vue 组件的方式去引用 uni uiswiperaction组件,就能得到流畅的滑动跟手菜单。

更多平台的兼容性

uni-app的 App 端也是一个小程序引擎,为了在 App 端实现流畅的跟手拖动,也实现和兼容了 wxs。

其实 H5 平台倒不存在逻辑层和视图层通讯折损的问题,但为了平台兼容性拉齐,uni-app在 H5 端也实现了 wxs 机制。

这样编写 wxs 代码,在 uni-app 中可同时运行在 App 端、H5 端、微信小程序端。

百度小程序的 Filter 过滤器和支付宝小程序的 SJS,成熟度还比较低,目前只能处理基本的数据格式过滤,还不能响应 touch 等交互事件。

至于头条和 QQ 小程序,还不支持类 WXS 机制。

期待其他小程序平台尽快补齐这个重要功能,实现体验的提升。

uni-app目前也支持单独编写百度小程序的 Filter 过滤器和支付宝小程序的 SJS,这两种脚本无法跨多端,仅支持自有平台。开发者若需使用,可分别编写 wxs/filter/sjs 脚本,然后依次通过 script 引用,uni-app编译器会根据目标平台,分别编译发行,如下为示例代码:
示例代码要有条件编译

<!-- App/H5/ 微信小程序平台调用 wxs 脚本 -->
<script module="utils" lang="wxs" src="./utils.wxs"></script>
<!-- 百度小程序平台调用 filter.js 脚本 -->
<script module="utils" lang="filter" src="./utils.filter.js"></script>
<!-- 支付宝小程序平台调用 sjs 脚本 -->
<script module="utils" lang="sjs" src="./utils.sjs"></script>

后续

用运行在视图层的 js 解决通讯阻塞,可能很多人都没意识到。希望本文能给大家解惑,解开 WXS 之谜。

其实小程序的性能体验优化,仍然有大量空间。DCloud 团队在这个领域研究了 6 年,清楚小程序技术架构的优势,也清楚当前的问题。我们会继续分享这些问题及对应的解决方案,为小程序产业发展贡献力量。

本文涉及的 uni-uiswiperaction组件,代码开源在 https://github.com/dcloudio/uni-ui,uni-app框架代码开源在 https://github.com/dcloudio/uni-app,欢迎大家 star 或提交 pr。

退出移动版