scroll-view 是一个可以滚动的视图区域的容器组件。
一、重要属性
scroll-view 的滚动属性,实现了两套功能
- 左右或上下滚动
- 下拉更新
1.1 与滚动有关的属性:
scroll-x
允许横向滚动scroll-y
允许纵向滚动
纵向滚动
<scroll-view scroll-y style="height: 300rpx;">
<view id="demo1" class="scroll-view-item demo-text-1"></view>
<view id="demo2" class="scroll-view-item demo-text-2"></view>
<view id="demo3" class="scroll-view-item demo-text-3"></view>
</scroll-view>
横向滚动
<scroll-view class="scroll-view_H" scroll-x style="width: 100%">
<view id="demo1" class="scroll-view-item_H demo-text-1"></view>
<view id="demo2" class="scroll-view-item_H demo-text-2"></view>
<view id="demo3" class="scroll-view-item_H demo-text-3"></view>
</scroll-view>
双向滚动
<scroll-view bindscroll="onScroll" class="scroll-view_H" scroll-x scroll-y style="width: 100%;height:200rpx;">
<view id="demo1" class="scroll-view-item_H demo-text-1">1</view>
<view id="demo2" class="scroll-view-item_H demo-text-2">2</view>
<view id="demo3" class="scroll-view-item_H demo-text-3">3</view>
</scroll-view>
.scroll-view_H {
white-space: nowrap;
}
.scroll-view-item {
height: 300rpx;
}
.scroll-view-item_H {
display: inline-block;
width: 100%;
height: 300rpx;
}
.demo-text-1 {
background-color: #E6E6FA;
}
.demo-text-2 {
background-color: #E1FFFF;
}
.demo-text-3 {
background-color: #FDF5E6;
}
scroll-top 、 scroll-left
这两个属性,它们都是可以通过属性绑定、控制组件行为的属性。
如果我们想让内部的滚动实体滚动到某个位置,
并不能直接去调用它的类似于scrollTo的方法。
我们只能在js里面动态改变scroll-top,scroll-left 这两个属性绑定的变量。
然后在视图渲染更新以后,组件会自动发生滚动
scroll-into-view
滚动到某个元素,值应为某子元素id。
假如同时开启scroll-x、scroll-y横纵这两个方向的滚动,
当通过scroll-into-view滚动时,
那么它的滚动行为是怎样变化的呢?
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210713153829430.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjU4MDcwNA==,size_16,color_FFFFFF,t_70)
通过测试结果来看,结论很不明确,
如果不加scroll-with-animation的话,
也就是不开启动画,可以同时在x、y方向上瞬时移动到目标位置。
如果开启动画,同一时间只能在一个方向上滚动,
有时候在x方向滚动,有时候在y方向滚动,行为很不明确。
scroll-x scroll-y 最好不要同时开启,
如果功能需要,我们可以基于view实现同样的功能
或者是先在x方向上开启,完成移动后,再在y方向上开启,依次进行
<view class="page-section">
<view class="page-section-title">
<text>片9 测试scroll-into-view滚动</text>
</view>
<view class="page-section-spacing">
<scroll-view enable-flex scroll-into-view="{{scrollIntoViewId}}" bindscroll="onScroll" scroll-y scroll-x scroll-with-animation="{{false}}" style="width: 100%;height:300rpx;">
<view id="childview{{item}}0" wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9,10]}}" class="scroll-row">{{item}}
<view wx:for="{{[0, 1, 2, 3, 4, 5,6,7,8,9]}}" class="scroll-item" id="childview{{item}}{{item2}}" wx:for-item="item2">{{item}}:{{item2}}</view>
</view>
</scroll-view>
</view>
<view class="btn-area">
<button type="primary" bindtap="scrollToView1">滚动到子组件2</button>
</view>
</view>
data: {
scrollIntoViewId: '',
},
scrollToView1() {
viewId += 2
this.setData({
scrollIntoViewId: 'childview' + viewId
})
console.log(this.data.scrollIntoViewId)
},
scroll-anchoring
滚动锚定,默认false,控制滚动位置不随内容的变化而抖动的。
滚动锚定是什么?
假设是一个图片瀑布流的页面,
当用户浏览瀑布流页面时,
加入由于网速的原因。
在看下面图片的时候,
上面图片突然加载进来,
这时候会使下方的图片自动往下跑,
这个体验肯定很不好。
滚动锚定的策略
通过一个CSS样式控制滚动实体在内容变化的时候不发生滚动。
scroll-anchoring 就是干这个的。
`这个属性某种情况下,可能会给开发者带来意想不到的bug`,
这个页面可能会陷入一种自循环,表现出一种`抖动不止的现象`。
当出现这个现象的时候,简单的解决方法就是`关闭滚动锚定策略`,
或者`设置一个具有相同效果的样式`
overflow-anchor:none;
scroll-anchoring 该属性目前`小程序只支持IOS`,`android手机需要自己通过CSS处理
overflow-anchor:auto;`
1.2 scroll-view组件的下拉更新属性
- upper-threshold
- lower-threshold
这两个属性是为了控制scrolltoupper、scrolltolowe事件何时派发的。默认都是50px
- bindscrolltoupper
- bindscrolltolower
这两个事件是状态事件,upper-threshold为50的时候,当scroll-top小于50的时候,
只要滚动行为发生着bindscrolltoupper事件会多次派发,并且这种派发基本上是随心所欲的,
派发基本是没有规律的,所以`说基于crolltoupper、scrolltolower这两个事件写业务逻辑的时候,
我们要注意特别判断一下,是否已经处理过了,以免造成重复的处理。
不是说滚动一次派发一次,有可能是滚动一次派发多次`。
- bindscroll
<!-- 测试scrolltoupper的随心所欲 -->
<view class="page-section">
<view class="page-section-title">
<text>片12 测试scrolltoupper的随心所欲</text>
</view>
<view class="page-section-spacing">
<scroll-view upper-threshold="50" bindscrolltoupper="viewScrollToUpperEvent" scroll-y style="height: 300rpx">
<view id="demo1" class="scroll-view-item demo-text-1"></view>
<view id="demo2" class="scroll-view-item demo-text-2"></view>
<view id="demo3" class="scroll-view-item demo-text-3"></view>
</scroll-view>
</view>
</view>
upper-threshold="50"
scroll-top在小于等于这个值的时候派发bindscrolltoupper事件
.page-section-spacing {
margin-top: 60rpx;
}
.scroll-view-item {
height: 300rpx;
}
.demo-text-1 {
background-color: #E6E6FA;
}
.demo-text-2 {
background-color: #E1FFFF;
}
.demo-text-3 {
background-color: #FDF5E6;
}
viewScrollToUpperEvent(e) {
console.log('测试scrolltoupper事件', e.detail);
},
二、自定义实现下拉刷新
2.1 与自定义下拉刷新相关的属性
:
- refresher-enabled boolean,默认false,是否开启自定义下拉刷新
- refresher-threshold number,触发下拉更新的临界值
- refresher-triggered boolean,默认false,它是为了在更新后取消下拉更新的状态,当组件处于下拉更新的状态后,它的值变为true,此时程序要去做一些异步耗时的事情,例如网络加载,待处理完成了,再将这个值设置为false
- bindrefresherpulling
- bindrefresherrefresh
- bindrefresherrestore
- bindrefresherabort 后面4个事件是自定义实现下拉动画效果的关键
2.2 使用wxs实现自定义下拉刷新
bindrefresherpulling 手指按住往下拉的过程中派发的,自定义的动画效果要在这个事件里面处理
当向下拉动时,区域慢慢的放大,同时箭头图标有一个方向的翻转。
主要是做了三件事情,
第一,计算拉到哪里了,占总量高度80的多少,找到icon图标,设置它的旋转角度
第二,找到下拉动画的容器,设置它的缩放,达到看起来越往下拉,容器越来越大的一个效果。
第三,当拉到refresher-threshold 临界值时,改变下拉更新的提示文本
<!-- 自定义下拉更新 -->
<!-- 使用wxs自定义实现下拉刷新-->
<!--module="refresh"可以让我们在WXML中引用-->
<wxs module="refresh">
var pullingMessage = "下拉刷新"
module.exports = {
// onPulling 下拉的过程当中我们干什么事情
/*
主要是做了三件事情,
第一,计算拉到哪里了,占总量高度80的多少,找到icon图标,设置它的旋转角度
第二,找到下拉动画的容器,设置它的缩放,达到看起来越往下拉,容器越来越大的一个效果。
第三,当拉到refresher-threshold 临界值时,改变下拉更新的提示文本
*/
onPulling: function (e, instance) { // instance 传进来的页面的实例对象
// 80的高度,因为refresher-threshold设置的是80,手指按住往下拉的状态
var p = Math.min(e.detail.dy / 80, 1) // 目前拉倒哪里了,进度 不大于1
// console.log(p)
// 这里在视图层,不怕频繁操作DOM
var icon = instance.selectComponent('#refresherIcon') // 图标 WeUi组件
icon.setStyle({
opacity: p, // 透明度,越往下拉越清晰
transform: "rotate(" + (90 + p * 180) + "deg)" // 旋转角度
})
var view = instance.selectComponent('.refresh-container') // 拉动的动画本身的容器
view.setStyle({
opacity: p,
transform: "scale(" + p + ")" // 设置缩放
})
//拉到80的高度,可以释放状态
if (e.detail.dy >= 80) {
if (pullingMessage == "下拉刷新") {
pullingMessage = "释放更新"
instance.callMethod("setData", { // wxs 调用js的setData方法
pullingMessage
})
}
}
},
// 此时手拉开了,进入了加载中的状态
onRefresh: function (e, instance) {
// 此时手拉开了,进入了加载中的状态
pullingMessage = "更新中"
console.log(pullingMessage)
instance.callMethod("setData", {
pullingMessage: pullingMessage,
refresherTriggered: true
})
instance.callMethod("willCompleteRefresh", {}) //调用js方法
//willCompleteRefresh 方法
},
onAbort: function (e, instance) {
// 异常状态,例如被事件突然打断,事件包括电话等,被迫松手了
pullingMessage = "下拉刷新"
console.log(pullingMessage)
},
onRestore: function (e, instance) {
// 回去了,松手了,恢复原位了,不刷了
pullingMessage = "下拉刷新"
console.log(pullingMessage)
},
}
</wxs>
这是一段WXS代码,是在视图层执行的,在这里基本上可以肆意操作更新视图,
而不用担心更新频繁操作导致开销太大影响性能
在我们的代码里面之所以用callMethod方法,调用页面主体的setData方法,
就是为了曲线救国,达到更新视图的目的。
每个WXS代码里的事件句柄函数,在执行的时候都有两个参数传递进来,
事件对象与当前页面的实例对象,如果没有这两个参数,动画就实现不了。
WXS是在视图层里面执行的,js文件中的js代码是在逻辑层执行的。
WXS是WeXin Script的简写,它有自己的语法。严格按照官方文档去写。
<view class="page-section">
<view class="page-section-title">自定义下拉刷新</view>
<!--
bindrefresherpulling 下拉的过程当中我们干什么事情
bindrefresherrefresh 当它拉到可以松手的状态的时候
refresher-triggered 为true是小程序设定的,异步操作完成之后,要设置为false,下拉状态就自己回去了
-->
<scroll-view scroll-y style="width: 100%; height: 400px;overflow-anchor:auto;" bindscroll="onScroll"
bindscrolltoupper="onScrolltoupper" scroll-top="{{scrollTopValue}}" scroll-into-view="{{scrollIntoViewId}}"
scroll-with-animation enable-back-to-top enable-flex scroll-anchoring refresher-enabled
refresher-threshold="{{80}}" refresher-default-style="none" refresher-background="#FFF"
bindrefresherpulling="{{refresh.onPulling}}" bindrefresherrefresh="{{refresh.onRefresh}}"
bindrefresherrestore="{{refresh.onRestore}}" bindrefresherabort="{{refresh.onAbort}}"
refresher-triggered="{{refresherTriggered}}">
<!-- slot="refresher" 写死的名字,写其他的不行 -->
<view slot="refresher" class="refresh-container"
style="display: block; width: 100%; height: 80px; background: #F8f8f8; display: flex; align-items: center;">
<view class="view1"
style="position: absolute; text-align: center; width: 100%;display:flex;align-items:center;justify-content:center;color:#888;">
<mp-icon id="refresherIcon" icon="arrow" color="#888" size="{{20}}"
style="margin-right:5px;transform:rotate(90deg)"></mp-icon>
<text style="min-width:80px;text-align:left;">{{pullingMessage}}</text>
</view>
</view>
<view wx:for="{{arr}}" id="view{{item+1}}" style="display: flex;height: 100px;">
<text style="position:relative;top:5px;left:5px;color:black;">{{item+1}}</text>
<image src="https://p.qqan.com/up/2021-6/16232893151517011.jpg"></image>
<image src="https://p.qqan.com/up/2021-6/16232893724729414.jpg"></image>
<image src="https://p.qqan.com/up/2021-6/16232893157513212.jpg"></image>
</view>
</scroll-view>
<view class="btn-area">
<button bindtap="plusScrollUpValue">向上滚动</button>
<button bindtap="scrollToView1">滚动到子视图</button>
<button bindtap="unshiftOnePic">顶部添加一张图</button>
</view>
</view>
bindrefresherrestore 事件,是状态恢复了,是设置了refresher-triggered 为false动画完成以后派发的事件。
bindrefresherabort 是下拉行为被打断时派发的事件,正常情况下这种事件是不会收到的
data: {
pullingMessage: '下拉刷新', //下拉刷新,释放更新,加新中...
refresherTriggered: false, //
},
willCompleteRefresh() {
console.log('更新中')
// ... 的动画
let intervalId = setInterval(() => {
let pullingMessage = this.data.pullingMessage
console.log(pullingMessage, pullingMessage == '更新中')
if (pullingMessage.length < 7) {
pullingMessage += '.'
} else {
pullingMessage = '更新中'
}
this.setData({
pullingMessage
})
}, 500)
// 2s 1.清理定时器 2.setData
setTimeout(() => {
console.log('更新完成了')
clearInterval(intervalId)
this.setData({
pullingMessage: "已刷新",
refresherTriggered: false,
// 为true是小程序设定的,异步操作完成之后,要设置为false,下拉状态就自己回去了
})
}, 2000)
},
bindrefresherrefresh事件,它是组件进入更新中状态时派发的事件,我们需要一个定时器,模拟网络异步加载,
但是WXS没有定时器,它只有一个页面实例对象的requestAnimationFrame函数,要么使用requestAnimationFrame方法模拟一个定时器,要么在js中实现
在js中定义willCompleteRefresh的方法,然后在WXS里面,在合适的时机,通过callMethod去调用它。
在这个组件中,willCompleteRefresh主要做了两件事情,
第一,使用一个定时器模拟实现 "更新中..." 后面的 ... 跳动的动画
第二,通过一个延时定时器在两秒以后设置刷新完成
2.3 总结
关于下拉刷新组件有两个开源项目,
mescroll https://github.com/mescroll/mescroll
minirefresh https://github.com/minirefresh/minirefresh
三、最佳实践
使用scroll-view组件有几点需要我们注意:
第一点,启用scroll-anchoring 属性时,同时添加一个overflow-anchor:auto;的样式,来应对Android机型不兼容的情况,
第二点,任何时候只开启一个方向的滚动,scroll-x 或者 scroll-y 只取其一,
当开启scroll-y时,必须给组件一个高度,子组件的高度之和一定要大于这个高度,
当开启scroll-x时,必须给组件一个宽度,一般这个值是100%,等于屏宽,子组件的宽度之和要大于屏宽,
在启用scroll-x时,宽度为100%,如果出现不滚动的现象,可以尝试给滚动容器添加两个这样的样式
white-space:nowrap; 不换行
display:inline-block; 行内块元素
目的是让子元素在横向上排列成一排,
第三点,开启enable-flex,这个属性是在scroll-view组件上启用Flex布局的,相当于添加了一个display:flex这样的样式,但是如果是我们自己添加的话,是添加在了外围的容器上,只有通过这个属性添加才能加到内部真正的容器上,
第四点,如果需要使用refresher-enabled 启用下拉动画的自定义,自定义可以很方便实现一些有创意的交互效果,
下拉动画容器的slot属性要标记为refresher。
第五点,下拉动画组件的背景色用#F8F8F8,前景色包括图标与文本用#888888这个颜色,符合微信设计规范
第六点,尽量不在js代码里面在scroll事件的句柄函数中直接更新视图,`把相关的频繁的更新视图的代码,放在WXS模块中`,
`在大列表视图中`,尤其要这么做。
四、在开发中遇到的问题
4.1 如何优化使用setData向其传递大数据、渲染长列表?scroll-view追加数据会自动回到顶部,怎么解决?
//更新二维数组
const updateList = `tabs[${activeTab}].list[${page}]`
const updatePage = `tabs[${activeTab}].page`
this.setData({
[updateList]:res.data,
[updatePage]:page + 1
})
<view wx-for="{{gameListWrap}}" wx:for-item="gameList">
......
</view>
上面的代码中,作者是想实现一个多tab页的功能,
数据是tabs,gameListWrap是对tabs子数据访问的再封装,
activeTab、page是模板字符串中的变量
updateList、updatePage是setData更新的时候用的key,因为是变量,所以需要用中括号
let tabData = this.data.tabs[activeTab];
tabData.list.push(res.data);
tabData.page=page + 1;
let key = `tabs[${activeTab}]`
this.setData({
[key]:tabData
})
作者为什么不直接使用push方法呢?
当有新数据进来的时候,直接往tab页数据的底部推入新数据,这样不就可以了吗?
但这种操作有一个问题,setData受限于视图层与逻辑层之间,用于传话的evaluateJavascript函数,
每次携带的数据大小,官方要求在文本序列化以后,大小不能超过256KB,
如果每个tab页是一个瀑布流页面,它的tabData.list可能是一个越来越大的数据,很有可能就超过256KB。
将tab数据与页面数据分开,在当前页面循环渲染时,按照pages[activeTab].page的数字循环,
取数据的时候依照page当前的值,从gameListData[activeTab]中查取,
gameListData这个时候在形式上相当于一个数据,但实际上它是一个map。
另外在渲染长列表的时候,微信在WeUI扩展组件库中,给出了一个长列表组件recycle-view,它用于渲染无限长的列表。
`那么这个问题怎么解决呢?`
使用`recycle-view扩展组件`:
https://developers.weixin.qq.com/miniprogram/dev/extended/component-plus/recycle-view.html
这个长列表实现的原理也很简单,通过监听scroll事件,
只渲染当前视图窗口的内的list列表,看不见的地方用空白的占位符代替。
在使用recycle-view扩展组件的时候,batch属性的值必须为batchSetRecycleData,这是由组件自动管理的。
在js代码中调用createRecycleContext时,传入的dataKey:recycleList,
这个名称必须与WXML中的wx:for指定的数据名称一致,
如果一个页面中还使用了另外一个长列表,则需要再换一个名字。
<view class="page-section">
<view class="page-section-title">使用recycle-view扩展组件</view>
<recycle-view height="200" batch="{{batchSetRecycleData}}" id="recycleId" batch-key="batchSetRecycleData" style="background:white;">
<recycle-item wx:for="{{recycleList}}" wx:key="index" class='item'>
<view>
{{item.id}}: {{item.name}}
</view>
</recycle-item>
</recycle-view>
</view>
const createRecycleContext = require('miniprogram-recycle-view')
function rpx2px(rpx) {
return (rpx / 750) * wx.getSystemInfoSync().windowWidth
}
onReady: function () {
var ctx = createRecycleContext({
id: 'recycleId',
dataKey: 'recycleList',
page: this,
itemSize: {
width: rpx2px(650),
height: rpx2px(100)
}
})
let newList = []
for (let i = 0; i < 20; i++) {
newList.push({
id: i,
name: `标题${i + 1}`
})
}
ctx.append(newList)
},
总结
当从后端拉取大数据渲染长列表的时候,大多数情况下卡顿并不是手机真的卡了,
这个时候如果打开App就会发现很流畅。
很可能这个时候只是试图渲染不及时,
影响小程序渲染效率的罪魁祸首就是底层的evaluateJavascript这个通信函数,
它可以说是逻辑层与视图层之间的一个很小的独木桥,
他无法承载过大的数据量,
所以我们要尽量减少大数据的渲染,在视图中的互动操作要尽量在WXS代码中去完成
4.2 如何实现购物类小程序,分类选择物品的页面
从效果图看,需要实现两个功能,
第一,单击左侧菜单,右侧区域自动滚动到相应的位置,
第二,在右侧滚动的时候,左侧菜单自动同步并高亮显示。
<!--实现小程序页面分类选择物品页面 -->
<!-- 左侧菜单 -->
<scroll-view class='nav' scroll-y='true'>
<view wx-for='{{list}}' wx:key='{{item.id}}' id='{{item.id}}'
class='navList{{currentIndex == index ? "active":""}}' bindTap='menuListOnClick' data-index='{{index}}'
>{{item.name}}</view>
</scroll-view>
<!-- 右侧内容 -->
<scroll-view scroll-y='true' scroll-into-view='{{activeViewId}}' bindscroll='scrollFunc'>
<view class='fishList' wx:for='{{content}}' id='{{item.id}}' wx:key='{{item.id}}'>
<p>{{item.name}}</p>
</view>
</scroll-view>
//单击左侧菜单
menuListOnClick(e){
let me = this;
me.setData({
activeViewId:e.target.id,
currentIndex:e.target.dataset.index
})
},
//滚动时触发,计算当前滚动到的位置对应的菜单是哪个
scrollFunc(e){
//看向上滚动了多少
this.setData({
scrollTop:e.detail.scrollTop
});
// 右侧的内容区域有一个heightList
// 每一块区域高度我们一个一个去对比
// 看目前滚动大概是处于哪一个高度的范围之内
//我们要事先在渲染的时候,heightList区域高度要事先计算出来并存储出来
for(let i= 0;i<this.data.heightList.length;i++){
let height1 = this.data.heightList[i];
let height2 = this.data.heightList[i + 1];
if(!height2 || (e.detail.scrollTop >= height1 && e.detail.scrollTop < height2) ){
this.setData({
currentIndex:i // 设置当前选择的是哪一个
});
}
return ;
}
this.setData({
currentIndex: 0
});
}
4.3 WeUI组件库中有一个vtabs组件,实现效果同问题 4.2,是一个有侧边栏的浏览组件,使用该组件。
案例
vtabs.wxml
<mp-vtabs
vtabs="{{vtabs}}"
activeTab="{{activeTab}}"
bindtabclick="onTabCLick"
bindchange="onChange"
class="test"
>
<block wx:for="{{vtabs}}" wx:key="title" >
<mp-vtabs-content tabIndex="{{index}}">
<view class="vtabs-content-item">我是第{{index + 1}}项: {{item.title}}</view>
</mp-vtabs-content>
</block>
</mp-vtabs>
vtabs.js
Page({
data: {
vtabs: [],
activeTab: 0,
},
onLoad() {
const titles = ['热搜推荐', '手机数码', '家用电器',
'生鲜果蔬', '酒水饮料', '生活美食',
'美妆护肤', '个护清洁', '女装内衣',
'男装内衣', '鞋靴箱包', '运动户外',
'生活充值', '母婴童装', '玩具乐器',
'家居建材', '计生情趣', '医药保健',
'时尚钟表', '珠宝饰品', '礼品鲜花',
'图书音像', '房产', '电脑办公']
const vtabs = titles.map(item => ({title: item}))
this.setData({vtabs})
},
onTabCLick(e) {
const index = e.detail.index
console.log('tabClick', index)
},
onChange(e) {
const index = e.detail.index
console.log('change', index)
}
})
vtabs.json
{
"usingComponents": {
"mp-vtabs": "../../components/vtabs/index",
"mp-vtabs-content": "../../components/vtabs-content/index"
}
}
vtabs.wxss
@import '../common.wxss';
page{
background-color: #FFFFFF;
height: 100%;
}
.vtabs-content-item {
width: 100%;
height: 300px;
box-sizing: border-box;
border-bottom: 1px solid #ccc;
padding-bottom: 20px;
}
common.wxss
@import '../components/weui-wxss/dist/style/weui.wxss';
page{
background-color: #F8F8F8;
font-size: 16px;
font-family: -apple-system-font,Helvetica Neue,Helvetica,sans-serif;
}
.page__hd {
padding: 40px;
}
.page__bd {
padding-bottom: 40px;
}
.page__bd_spacing {
padding-left: 15px;
padding-right: 15px;
}
.page__ft{
padding-bottom: 10px;
text-align: center;
}
.page__title {
text-align: left;
font-size: 20px;
font-weight: 400;
}
.page__desc {
margin-top: 5px;
color: #888888;
text-align: left;
font-size: 14px;
}
.weui-cell_example:before{
left:52px;
}
/* .weui-btn{width:184px;} */
组件源码