
博主时隔两个月又回来了😂 ,入职阿里巴巴以来,先后参与了三个项目的制作:
- 淘宝武林大会答题面板
- 淘宝直播基地的绑定和签到h5页面
- 收藏夹暂存栏 weex 页面
现在淘宝的收藏夹项目也是我在负责。这次重点也是想讲讲淘宝收藏夹中 weex 的动画优化技术——BindingX。主要还是写给新人们的
weex 和 BindingX 扫盲
什么是 weex?
Weex 致力于使开发者能基于通用跨平台的 Web 开发语言和开发经验,来构建 Android、iOS 和 Web 应用。简单来说,在集成了 WeexSDK 之后,你可以使用 JavaScript 语言和前端开发经验来开发移动应用。
通俗点讲就是,js 可以通过 weex 与 native 通信,所以你可以在前端编写 js 代码来开发手机 app 的一部分功能和页面。一般来说在淘宝 app 中的 weex 层主要是一些不依赖手机原生功能 api 调用的静态页面,大部分是展示一些静态数据的。weex对于feeds流的列表展示还是很友好的,例如这次我接手的 weex 淘宝收藏夹项目,关于收藏夹的宝贝内容列表页、宝贝搜索页、宝贝榜单页等一系列静态页面。也可以看看我这次做的暂存栏功能:

什么是 BindingX
BindingX 是解决 weex 和 React Native 上富交互问题的一种解决方案。 它提供了一种称之为 "Expression Binding" 的机制可以在 weex、React Native 上让手势等复杂交互操作以 60fps 的帧率流畅执行,而不会导致卡顿,因而带来了更优秀的用户体验。———— 来自 BindingX 官网:alibaba.github.io/bindingx/gu…
BindingX 的核心思想就是将"交互行为"以表达式的方式描述,并提前预置到 Native,避免在行为触发时 JS 与 native 的频繁通信。———— 来自 BindingX 官网:alibaba.github.io/bindingx/gu…
怎么在 rax 中使用 weex
在这里,我不会讲太多 weex 的基础,大部分我会讲应用相关的,结合weex在rax中的使用给大家一一讲解。
- 打开控制台,在控制台中输入:npx rax init [your project name],全局安装了 rax 的同学可以直接使用命令 rax init [your project name]。
- 选择 App。

- 将 web 和 weex 勾选上。

- 作为示例使用我们就选择 spa(单页应用)。

- 输入你自己的名字。

- 选择你要使用的语言,这里我们选择 JavaScript。

- 这里直接回车。(注意:不要引入 react,即不要勾选第三个选项:Compatibility with React)

- 这里提示你是否自动安装依赖,我们选择 yes。

至此我们的项目就创建好了,接下来就是我们的实战环节!
收藏夹小案例
为了让大家体验一下 weex 中 BindingX 技术 的魅力我就用大家耳熟能详的一个收藏夹小功能作为演示:收藏夹列表的左滑功能,具体操作相信大家都知道,就是左滑出现三个选项,去店铺、分享、删除。

首先我们先不使用 BindingX 来实战一把,OK,开始切入正题!
未使用BindingX如何实现
我们直接在 src/Home 目录下的 index.jsx 文件中编写我们的代码。首先我们来思考一下,其实要实现这个功能并不难,布局也很简单,一个外包裹容器套一个子容器和一个左滑后出现的容器,这个容器定位是position: absolute;。当我们 touchStart 的时候记录开始点的 x 坐标,然后在 touchEnd 的时候计算 结束点 - 开始点 的值就可以知道滑动的距离,通过正负可以判断滑动的方向,最后我们再让盒子移动到相应的位置即可。
想明白之后我们就可以开始编写代码了:
- 先引入我们所需要的依赖:
import { createElement, useEffect, useRef } from 'rax';
// rax中默认的div容器
import View from 'rax-view';
// rax中的文本标签
import Text from 'rax-text';
// 当组件已经被挂载到 DOM 上时,findDOMNode 可用于获取真正的 DOM 元素,以便对 DOM 节点进行操作。
import findDOMNode from 'rax-find-dom-node';
import transition from 'universal-transition';
// rax中监听手势的组件,可用手势有:
// onHorizontalPan 监听水平滑动手势
// onVerticalPan 监听垂直滑动手势
import GestureView from 'rax-gesture-view';
import './index.css';
- 其次开始布局,这里就直接上代码了:
// 储存起始点的X坐标
let startX = useRef(0);
// 储存终点 - 起始点的X坐标差
let moveX = useRef(0);
// 储存终点的X坐标
let endX = useRef(0);
// 是否开启动画
let animation = useRef(true);
// 绑定目标移动容器的ref
const panBoxRef = useRef(null);
return (
<View className="container">
<View ref={deleteBoxRef} className="deleteBox">
<Text>Delete</Text>
</View>
<GestureView ref={panBoxRef} class="box">
<View className="head">
<View className="avatar"></View>
</View>
<View className="content">
<Text className="desc">this is container</Text>
</View>
</GestureView>
</View>
css 代码:
.container {
width: 750;
height: 360;
flex: 1;
background-color: #eeeeee;
}
.box {
width: 750;
height: 240;
background-color: #651fff;
}
.head {
background-color: #651fff;
width: 750;
height: 120;
flex-direction: row;
align-items: center;
}
.content {
width: 750;
height: 240;
background-color: #651fff;
padding-left: 24;
padding-top: 24;
padding-right: 24;
box-sizing: border-box;
}
.avatar {
width: 96;
height: 96;
border-radius: 48;
background-color: #cddc39;
margin-left: 36;
margin-right: 48;
}
.deleteBox {
width: 200;
height: 360;
position: absolute;
right: 0;
top: 0;
background-color: red;
}
接下来你就可以看到布局如下:

<GestureView></GestureView>,使用<View></View>也可以,通过 onTouchStart 和 onTouchEnd 事件可以记录下起始点和终点的 x 坐标,相减之后就是水平滑动的手势的距离,和这里用手势监听容器是一个道理:
<GestureView ref={panBoxRef} onHorizontalPan={handleHorizontalPan} class="box">
...
</GestureView>
接下来写我们的 handleHorizontalPan 和 控制动画的 transitionShowDelete 方法:
const handleHorizontalPan = (e) => {
// console.log(e);
if (e.state === 'start') {
startX.current = e.changedTouches[0].pageX;
} else if (e.state === 'end') {
endX.current = e.changedTouches[0].pageX;
moveX.current = endX.current - startX.current;
if (moveX.current < 0) {
transitionShowDelete(true);
} else if (moveX.current >= 0) {
transitionShowDelete(false);
}
}
};
/**
* 具体的transition使用方法可以查看官方文档
* https://rax.js.org/docs/api/transition
*/
const transitionShowDelete = (shouldShowDelete) => {
transition(
findDOMNode(panBoxRef.current),
{
// 位移距离你可以使用百分比也可以使用带rpx单位的数字
transform: shouldShowDelete
? `translateX(-${(20 / 75) * 100}%)`
: 'translateX(0%)',
},
{
timingFunction: 'ease-in-out',
delay: 0,
duration: 300,
},
() => {
// 动画执行成功后的回调函数
}
);
};
这里我们可以打印一下这个方法的事件参数 e 看看是啥 ?

你会发现 e.state 储存了 touchStart、touchMove、touchEnd 的状态:“start”表示滑动开始,“end”表示滑动结束。至此我们的功能就完成啦!下面是效果图:

但是如果你知道了 weex 的的工作原理后,你就会觉得这并不是一个最佳的做法,接下来我们就来了解一下 weex 和 native 之间的通信过程:
前面我们说了我们可以通过编写 js 代码可以达到使用一些 native 底层的 api 功能,那么这个是怎么实现的呢?接下来我将画一张图简单给大家解答一下。

JS 层负责视觉控制、事件处理;Bridge 层是 JS 层和 Native 层的沟通桥梁,负责指令翻译,传递给 native 层;Native 层负责视觉绘制、事件收集。
由上图我们可以看出我们每一次响应事件,执行交互操作、改变 UI 都需要 js 层和 native 层的频繁通信。
试想一下如果我们在 touch 事件响应时去改变 ui,想让 native 层做一些复杂的交互操作时,JS 层就需要不停得处理从 Native 层收集来的事件然后作出及时的视觉响应,如果响应不及时就会导致视觉卡顿。我们都知道一般手机的屏幕刷新率是 60fps,是我们人肉眼可以接受到的流畅程度,也就是说一次刷新要在 1/60s 内完成才不会让人觉得卡顿。所以如果 JS 层从事件接受、处理、回馈到 Native 绘制新的视图完成超过了 16.67ms 将会出现视觉卡顿。而且如果每一次的视觉更新都需要大量的通讯,也会加大 cpu 的消耗,以至于增加 crash 的风险。
所以你会想到了我们上面那种方法就是这样一种情况:每次 touch 事件触发都需要去和 native 通信一次然后更新视图,这还是没有将 UI 绘制放在onTouchMove中执行,如果是放在onTouchMove中执行那性能肯定会受到很大的影响,视觉的绘制也会明显的感觉到卡顿。不信我们可以来试一下:
这里我们如果要将动画处理操作放在TouchMove事件中,可以通过e.state === 'move'判断,直接修改handleHorizontalPan方法,修改后的代码如下:
const handleHorizontalPan = (e) => {
// console.log(e);
if (e.state === 'start') {
startX.current = e.changedTouches[0].pageX;
} else if (e.state === 'move') {
endX.current = e.changedTouches[0].pageX;
moveX.current = endX.current - startX.current;
if (moveX.current < 0) {
transitionShowDelete(true);
} else if (moveX.current >= 0) {
transitionShowDelete(false);
}
}
};
接下来看看效果:

怎么样,是不是感觉到了很明显的卡顿!那么问题来了,如果我非要在每次 touchMove 的时候去更新视图呢,那又要怎么解决呢?如此一来我们的神器 BindingX 闪亮登场!
使用BindingX如何实现
我们通过上面的扫盲相信大家已经初步了解了一下什么是 BindingX,如果你还不懂 BindingX 的引入的意义所在,没关系可以看看我接下来的讲解:

上图可以看出,如果我们没有引入 BindingX 那么我们要在 touchMove 时渲染视图就需要频繁的 js 和 native 通信,每一次的 touchMove 就是一次通信的性能消耗。而如果我们引入了 BindingX 那么我们就可以通过onTouchStart事件向 native 层注入一个表达式,这个表达式会在 native 层解析,然后执行。
比如说我们注入一个在 X 轴上移动的表达式,那么当我们在 touchStart 注入成功时就可以在每次 touchMove 的时候 native 层自动帮我们更新了视图,然后视图更新成功后会返回给 js 层一个成功的回调。
如果你还不明白,没关系,接下里我们就来实战一下:
先安装我们的依赖:npm i weex-bindingx --save,引入我们需要的依赖项如下:
import { createElement, useEffect, useRef } from 'rax';
// rax中默认的div容器
import View from 'rax-view';
// rax中的文本标签
import Text from 'rax-text';
// 当组件已经被挂载到 DOM 上时,findDOMNode 可用于获取真正的 DOM 元素,以便对 DOM 节点进行操作。
import findDOMNode from 'rax-find-dom-node';
import transition from 'universal-transition';
import Bindingx from 'weex-bindingx';
// 判断当前环境
import { isWeex } from 'universal-env';
import './index.css';
布局也需要改变一下:
return (
<View className="container">
<View ref={deleteBoxRef} className="deleteBox">
<Text>Delete</Text>
</View>
<View ref={panBoxRef} onTouchStart={handleTouchStart} class="box">
<View className="head">
<View className="avatar"></View>
</View>
<View className="content">
<Text className="desc">this is container</Text>
</View>
</View>
</View>
);
接下里的重头戏是我们的handleTouchStart方法:
useEffect(
() => {},
() => {
// 在组件销毁时BindingX解绑
Bindingx.unbind({
token: gesToken.current,
eventType: 'pan',
});
},
[]
);
// 判断当前环境:weex和web上的传入的ref情况不同
const getEl = (panBoxRef) => {
return isWeex ? findDOMNode(panBoxRef).ref : panBoxRef;
};
const handleTouchStart = (e) => {
console.log(e);
if (!animation.current) {
return;
}
startX.current = e.changedTouches[0].pageX;
let ref = getEl(panBoxRef.current);
// 向native注入表达式,注入后的返回值有一个token属性用来BindingX的解绑
let gesTokenObj = Bindingx.bind(
{
// 要绑定的元素的锚点
anchor: ref,
// 触发动画的事件类型:'pan'为触摸时触发动画
eventType: 'pan',
props: [
{
element: ref, // 要实现动画的元素的锚点
property: 'transform.translateX', // 动画的属性
expression: 'x+0', // 代表在x轴移动
},
],
},
(e) => {
// 注入成功之后的回调,根据注入的表达式的不同e.state的状态代表不同的情况,这里我们注入的是translateX,那么e.state === 'end' 代表移动结束后的状态,就是touchEnd的时候
console.log(e);
if (e.state === 'end') {
moveX.current = e.deltaX;
animation.current = true;
if (moveX.current <= -100) {
anchorToCurrent(-200);
} else if (moveX.current > -100) {
anchorToCurrent(0);
}
}
}
);
gesToken.current = gesTokenObj.token;
};
// 在touchEnd的时候,根据移动的距离不同,触发一个回弹的效果
const anchorToCurrent = (currentWidth) => {
let result = Bindingx.bind(
{
// 事件类型:timing为根据时间执行动画
eventType: 'timing',
exitExpression: 't>600', // 动画结束的时间
props: [
{
element: getEl(panBoxRef.current), // 要实现动画的元素的锚点
property: 'transform.translateX', // 动画属性
expression: `easeOutElastic(t, ${moveX.current}, ${
currentWidth - moveX.current
}, 600)`,
},
],
},
(e) => {
animation.current = false;
}
);
};
至此我们的优化完成,如果还有同学不懂的地方可以查看 BindingX 官方文档,官网还提供了很多动画差值器,可以供我们使用:alibaba.github.io/bindingx/gu…
接下来我们来看看效果:

怎么样,是不是感觉到了如德芙般的丝滑和柔顺!这就是 BindingX 的魅力,让你的 weex 交互动画丝滑般的顺畅!
其实 BindingX 可以用到的地方很多,只要你是以下的交互场景,都可以考虑试试它:
- 监听
pan手势,更新 UI。 - 监听滚动容器(如 List)的 onscroll 事件,更新 UI。
- 监听设备传感器方向变化,更新 UI。
- 动画。(即监听设备的每一帧的屏幕刷新回调事件,更新 UI)。
BindingX 还可以实现很多的动画交互,官网上都有例子大家也可以去看看:alibaba.github.io/bindingx/
weex补充
做这方面的补充主要是想给新手们的一些编码建议,这边就简单的列举了一些注意点,想要更详细的可以去官方查看文档。对于我们程序员来说,官方文档是最好的学习助手
weex 的大坑
对于 weex 我们首先要了解到 weex 的一些坑,也就是和我们平常写代码的一些差别。其中可能对于大部分同学来说就是 css 的坑了。这里我整理了一份 css 注意事项可以参考一下,这里主要是 weex 在 rax 中的写法:
- border 和 background 不支持组合写法,必须分开编辑,标签和标签不支持 background-image 属性 只支持像素值,不支持相对值,适配以 750 像素为标准
- weex 支持 position 定位:relative | absolute |fixed |sticky ,默认值是 relative
- weex 不支持 z-index 设置元素层级关系,但是靠后的元素层级越高
- 如果定位元素超过容器边界,在 Android 下,超出部分将不可见,原因在于在 Android 端元素 overflow 默认值为 hidden,且 overflow 没有其他值。
- background-image 只支持线性渐变:linea-gradient,暂不支持径向渐变:radius-gradient
- weex 中 box-shadow 仅仅支持 ios
- ios 中 image 组件无法定义一个或多个角的 border-radius,只能直接使用 border-radius 定义圆角,Android 可随意定义
- weex 中,flexbox 是默认并且唯一的布局模型,每个元素都默认拥有 display:flex 属性
- Weex 盒模型的 box-sizing 默认为 border-box
- 子元素的样式不会继承自父元素,比如 color 和 font-size 等样式
- 容器的层级嵌套不要超过 14 层,否则容易引发 crash
具体可以查看官方文档:weex.apache.org/zh/docs/sty…
weex 的环境变量
每个 Weex 页面的 JS 上下文中都有一个相互独立的 weex 变量,它可以像全局变量一样使用,不过它在不同页面中是隔离而且只读的。 weex 中的环境变量可以通过 WXEnvironment 使用,这是 weex 内置的一个全局变量 ,主要字段如下: platform——当前的运行平台 appVersion——当前 app 的版本 deviceWidth——设备屏幕宽度 deviceHeight——设备屏幕高度
具体可查看官方文档:weex.apache.org/zh/docs/api…
同时 weex 有自己内置的文档模型对象,weex.document 是当前页面的文档模型对象,可以用来创建和操作 DOM 树中元素。它是 Weex DOM API 规范的一部分,但是它和 W3C 的 DOM 规范中的 document 对象是不同的。
我们可以通过let dom = __weex_require__('@weex-module/dom');引入。
dom API 主要有:
- scrollToElement: 将 list 的某个子节点滚动到当前视口
- getComponentRect: 获取某个组件的 bounding rect 布局信息
- addRule: 添加 font-face rule
- getLayoutDirection: 获取某个组件的布局方向(rtl、lrt、inherit)
具体可查看官方文档:weex.apache.org/zh/docs/mod…