- 【微信小程序】wx.request简易仿Axios封装与日期格式化函数
概要 wx.request()方法不支持Promise风格的调用方式,且无法使用拦截器、baseUrl等功能,我根据自己的使用习惯,对其进行简单封装。 封装代码: [代码]const http = { defaults: { baseUrl: 'http://localhost:8081/micronews/v1', timeout: 60000, method: 'GET', data: null, header: { 'Content-type': 'application/json' } }, interceptors: { request: config => { wx.showLoading({ title: '数据加载中。。。' }) return config }, response: res => { wx.hideLoading() console.log(res) if (!res.isSuccess) { wx.showToast({ title: '网络错误', icon: 'error' }) return res } switch (res.data.code) { case '0': break default: wx.showToast({ title: '错误码:' + res.data.code, icon: 'error' }) throw new Error('错误') } return res.data } }, request(opt) { let opts = { ...this.defaults, ...opt } opts.url = opts.baseUrl + opts.url opts = this.interceptors.request(opts) return new Promise((rs, rj) => { wx.request({ ...opts, success: res => { const rres = this.interceptors.response({ ...res, config: opts, isSuccess: true }) return rs(rres) }, fail: res => { const rres = this.interceptors.response({ ...res, config: opts, isSuccess: false }) return rj(rres) } }) }) }, get(url, data = {}, config = {}) { return this.request({ url, data, method: 'GET', ...config }) }, post(url, data = {}, config = { header: { 'Content-type': 'application/x-www-form-urlencoded' } }) { return this.request({ url, data, method: 'POST', ...config }) } } wx.$apis = { getNewsList: () => http.get('/news'), getNews: id => http.get('/news/' + id), getMyNews: id => http.get('/news/user/' + id), login: data => http.post('/user/login', data), signup: data => http.post('/user/signup', data), publish: data => http.post('/news/publish', data), changeName: data => http.post('/user/1', data) } const formatTime = date => { const year = date.getFullYear() const month = date.getMonth() + 1 const day = date.getDate() const hour = date.getHours() const minute = date.getMinutes() const second = date.getSeconds() return `${[year, month, day].map(formatNumber).join('-')} ${[hour, minute].map(formatNumber).join(':')}` } const formatNumber = n => { n = n.toString() return n[1] ? n : `0${n}` } wx.formatTime = formatTime [代码] 技术细节 提示:注意,这里没有使用模块化暴露的方式进行封装,而是直接将apis加载到wx全局对象上,这样,只需要在app.js里引入一次,即可全局使用,更加便捷。使用同样的方式,也可以把app.js中的全局数据直接挂载到wx全局对象上。app.js的代码如下: [代码]// app.js // import "./utils/axios" import './utils/http' // import './utils/util' App({ onLaunch() { // 获取登录状态 const user = wx.getStorageSync('user') if (user) { this.globalData.logined = true this.globalData.user = user } wx.$app = this wx.$data = this.globalData wx.$user = wx.$data.user }, globalData: { user: null, logined: false, newsList: [] } }) [代码] 可以看到我们挂载了formatTime、$apis、$app、$data等全局数据,一次挂载,全局使用
2024-08-03 - 运用小程序Skyline技术构建无缝用户体验 —— 同程旅行酒店最佳实践分享
[图片] 动效衔接设计与小程序渲染框架 1、什么是动效衔接设计? 随着互联网技术和设计理念的不断发展,动效设计成为现代 UI 设计中不可或缺的一部分。其中,动效衔接是非常重要的一环。动效衔接设计是指通过巧妙的动效设计,将不同的 UI 元素在动画过程中自然、流畅地衔接起来,从而增强用户的交互体验和视觉感受。在实际应用中,动效衔接设计主要应用于界面转场、信息提示、状态变化等方面,通过顺畅的衔接,降低用户因白屏等待而产生的焦虑。 2、动效衔接设计的意义 (1)极大提高用户体验,让用户感受到界面的流畅和自然,从而增加用户对产品的好感度。 (2)降低用户的操作认知成本,帮助用户更好地理解执行操作后所带来的结果,从而减少用户对产品的困惑。 (3)强化视觉层,让用户更好地区分不同的信息和元素,从而增强视觉层次感。 (4)增加界面的美感度,让界面更加生动有趣,从而提升整体的美感和设计价值。 (5)提升品牌的认知度,让产品更加具有特色和独特性,从而提高品牌的认知度和市场竞争力。 3、什么是小程序渲染框架Skyline? 为了进一步优化小程序性能,小程序在原 webview 渲染引擎之外最新推出 小程序渲染框架Skyline,其使用更精简高效的渲染管线,并拥有诸多增强特性,让它拥有更接近原生渲染的性能体验。新的增强特性有 worklet 动画系统、手势系统、自定义路由、共享元素动画,而且许多常用的组件如 scroll-view、swiper 都有了更高性能的实现。 [图片] [图片] 实践理念和场景拆解 1、动效衔接设计的核心原则 简单而清晰的动效设计,需要遵守以下几个原则: (1)一致性:动效衔接应该与整体设计风格保持一致。包括颜色、字体、动画速度等方面。 (2)可预测性:用户能够感知动画元素的变化关联性,从而增加用户对产品的掌控感和对界面的理解。 (3)反馈性:动效衔接与用户操作相响应,从而帮助用户理解他们的操作所带来的结果。 (4)视觉层次:动效衔接遵循视觉层次原则,让用户区分页面中的上下关系以及三维物理世界的关系层次,给用户清晰的层级区分感知,提高用户体验。 (5)自然性:动效衔接符合物理规律,例如重力、加速度等,从而增强动画的真实感和用户体验。 2、理念孵化与使用场景拆解 以提炼的动态感受为出发点,理性的层面给予了我们大致的产品体验感知,为我们动效理念的建成提供了框架。对此我们将继续从感性层面出发,找寻可传递真实感受的运动现象并加以组合提炼。 本次 同程旅行小程序 以酒店预订链路中核心的相册页面进行应用场景,在用户操作图片的过程中运用小程序渲染框架承接。 (1)将整个过程进行了拆解,首先为了退出时行动的路径更加清晰,做了一个响应设计,当界面向右滑动退出的过程中,相册图片进行缩小,在缩放过程中,会根据位移距离控制缩放的比例,同时蒙层的透明度以及毛玻璃效果也跟随手指移动变化,和相册列表页在视觉上呈现 XY 轴以及上下层级的空间关系,在缩小到一定的比例时,触发震动效果,松手退出到相册列表页。 (2)在交互结束时,图片退回相册列表页原始位置,在返回路径的过程中,根据交互结束时的定位点,来判断运动的方向和距离,计算运动加速度,以及模拟运动加速度带来的惯性回弹的方向和角度变化,加强与模拟真实物理世界的运动定律,和视觉上的动态感知。 结合自然世界的运动规律来看,把页面进入的元素比作是行驶的汽车,用户当作是正在斑马线上行驶的人,将马路作为页面空间。若汽车采用的是缓入运动(加速)的话,马路上的行人则看到的是一辆不断加速向他行驶过来的车辆。因为担心车辆高速的逼近导致刹车不及时的情况,行人便会本能的作出躲闪的反应。其实页面也是一个道理,进入的元素使用加速运动出现过冲的运动感知会让用户体验时产生不适。 [图片] 小程序渲染框架技术开发实践过程剖析 1、开发自定义路由实现此交互,需要 自定义路由动画,因为小程序渲染框架的页面支持自定义跳转动画。当使用自定义路由后,页面跳转时指定路由类型,就会触发自定义路由动画,而不再是默认的从右往左的动画,此处的实现可以使得页面跳转时,没有默认的路由动画,页面将直接以透明的方式渲染在屏幕上,由开发者自己控制页面内元素的动画展示方式,具体实现如下: (1)在图片查看页面配置文件 index.json 中声明 { "backgroundColor": "#00000000", "backgroundColorContent": "#00000000", // 设置客户端页面背景为透明 "navigationStyle": "custom", "renderer": "skyline", // skyline渲染引擎 "disableScroll": true, "usingComponents": { } } (2)在 wxss 中,设置图片查看页面的 page 节点为透明背景 page { background: transparent; } (3)在 js 中,使用 wx.router.addRouteBuilder(routeType, fn) 来声明自定义路由动画 wx.router.addRouteBuilder('myCustomRoute', function (params) { const handlePrimaryAnimation = () => { 'worklet'; return { // 可在此处,根据 params.primaryAnimation.value 的值,来设置页面的动画效果 backgroundColor: `rgba(0,0,0,${ params.primaryAnimation.value })` }; }; return { opaque: false, handlePrimaryAnimation, barrierColor: '', barrierDismissible: false, transitionDuration: 320, reverseTransitionDuration: 250, canTransitionTo: true, canTransitionFrom: false }; }) (4)在图片列表页面中,使用 x.navigateTo 来跳转页面,并且设置 routeType 为 myCustomRoute wx.navigateTo({ url: '/pages/skyline-image-viewer/index?index=0', routeType: 'myCustomRoute' }) [图片] 需配置页面的渲染引擎为 Skyline,并且在跳转时使用 routeType 就可以实现让页面在跳转时没有默认的路由动画。 2、共享元素穿越在连续的页面跳转时,页面间 key 相同的 share-element 节点将产生飞跃特效,还可自定义插值方式和动画曲线,通常作用于图片。为保证动画效果,前后页面的 share-element 子节点结构应该尽量保持一致 <share-element key="share-key"> <view> you code here </view> <!-- 需要注意,share-element 内要求只有一个根节点 --> </share-element> [图片] 这时,界面的表现像上面视频一样,是一个连续的动画状态,这完全是由 share-element 来控制的,share-element 的动画原理如下图所示: [图片] 3、接入手势组件,实现图片放大、缩小、平移在图片查看页面有如下结构: <scale-gesture-handle worklet:ongesture="onScaleGestureHandle"> <share-element key="{{ shareKey }}" class="current-item"> <image src="{{ src }}"/> </share-element> </scale-gesture-handle> 这里,我们使用小程序渲染框架提供的 手势组件 <scale-gesture-handle>,来实现图片的放大、缩小、平移等手势交互。 注意,所有声明为 worklet 指令的方法它们运行在UI线程,不要在方法中修改普通的变量,因为跨线程的关系,只能修改使用 wx.worklet.shared 声明的变量。 const GestureState = { POSSIBLE: 0, // 此时手势未识别 BEGIN: 1, // 手势已识别 ACTIVE: 2, // 连续手势活跃状态 END: 3, // 手势终止 CANCELLED: 4 // 手势取消 }; Component({ attached() { this.shareX = wx.worklet.shared(0); this.shareY = wx.worklet.shared(0); this.sharScale = wx.worklet.shared(1); // 声明共享变量,并且给需要变化的dom,绑定动画 this.applyAnimatedStyle('.current-item', () => { 'worklet'; return { transform: `translate3d(${this.shareX.value}px, ${this.shareY.value}px, 0) scale(${this.sharScale.value})` } }); // 页面所需的数据,需要在 attached 事件里初始化完毕,使其可以参与首帧渲染 this.setData({ src: '...', shareKey: '...' }); }, methods: { // 当手势组件识别到手势时,触发此回调 onScaleGestureHandle(e) { 'worklet'; const { state } = e; // 在worklet函数里,不要使用 const {} = this 对this解构 const shareX = this.shareX; const shareY = this.shareY; const sharScale = this.sharScale; if (state === GestureState.BEGIN) { // 手势已经识别,此时,可以获取到手势的初始值 } else if (state === GestureState.ACTIVE) { // 手势活跃状态,此时,可以获取到手势的变化值,如平移的距离、缩放的比例等 // 将当前变化的值,设置到 `shared` 变量,就可以改变元素的样式,类似于vue3的数据驱动 shareX.value += e.focalDeltaX; shareY.value += e.focalDeltaY; sharScale.value = e.scale; } else if (state === GestureState.END || state === GestureState.CANCELLED) { // 手势终止或取消,此时,可以获取到手势的最终值 } } } }) [图片] 4、手势协商(解决手势冲突) 上面的 demo 简单演示如何使用手势组件来做图片交互,但是在图片查看页面中,我们还有其他的手势交互,如图片的左右滑动切换等,一般我们会使用 <swiper> 组件来实现,但是 <swiper>组件的内部实现和 <scale-gesture-handle> 组件,都会监听手势事件,手势组件的事件不支持冒泡的,就会导致下面结构横时: <scale-gesture-handle worklet:ongesture="onScaleGestureHandle"> <swiper> <swiper-item wx:for="{{ imgs }}"> <share-element key="{{ item.shareKey }}" class="current-item"> <image src="{{ item.src }}"/> </share-element> </swiper-item> </swiper> </scale-gesture-handle> 使用手势横向滑动时,会优先触发 swiper 的横向切换事件,而无法触发 <scale-gesture-handle> 的手势事件了,这在图片放大时的图片横向移动产生了冲突。此时就需要使用手势协商来解决手势冲突。 什么是手势协商? 手势协商指的是:当页面同时有多个手势交互时,需通过一定的约定来决定哪些手势事件应该被执行,哪些需要被忽略。 小程序渲染框架解决手势冲突的方式,主要是通过手势组件的 tag、simultaneous-handlers、native-view 和 should-response-on-move 来实现 tag:手势组件的标识,用于区分不同的手势组件simultaneous-handlers:手势组件的协商者,表示需要同时触发事件的手势组件的标识should-response-on-move:参与手势时间的派发过程,返回 false时,表示该手势时间不会继续派发native-view:用当前手势组件来代理原生组件内部的手势事件,如<swiper>组件内部的手势事件<swiper> 的内部也是使用了 <horizontal-drag-gesture-handler>手势组件,但是我们不能直接在<swiper>上设置tag来使其参与手势协商,需要用相同的手势组件通过native-view=swiper将其内部的事件代理出来,使其可以参与协商<!-- <scale-gesture-handle> 缩放手势 --> <!-- <horizontal-drag-gesture-handler> 横向拖动手势 --> <!-- 通过 simultaneous-handlers=tag 来声明多个手势应该同时触发 --> <scale-gesture-handle tag="scale" simultaneous-handlers="{{['swiper']}}" worklet:ongesture="onScaleGestureHandle"> <!-- 此处使用 native-view=swiper 代理内部的手势组件 --> <!-- 通过 should-response-on-move=fn 来参与`事件派发`过程,决定手势的事件是否应该派发 --> <horizontal-drag-gesture-handler tag="swiper" native-view="swiper" simultaneous-handlers="{{['scale']}}" worklet:should-response-on-move="shouldResponseOnMove"> <swiper> <swiper-item wx:for="{{ imgs }}"> <share-element key="{{ item.shareKey }}" class="current-item"> <image src="{{ item.src }}"/> </share-element> </swiper-item> </swiper> </horizontal-drag-gesture-handler> </scale-gesture-handle> const GuestureMode = { INIT: 0, SCALE: 1, SWIPE: 2, MOVE: 3 // ... }; Component({ attached() { this.GuestureModeShared = wx.worklet.shared(GuestureMode.INIT); this.shareX = wx.worklet.shared(0); this.shareY = wx.worklet.shared(0); this.shareScale = wx.worklet.shared(1); // 声明共享变量,并且给需要变化的dom,绑定动画 this.applyAnimatedStyle('.current-item', () => { 'worklet'; return { transform: `translate3d(${this.shareX.value}px, ${this.shareY.value}px, 0) scale(${this.shareScale.value})` } }); // ... }, methods: { onScaleGestureHandle(e) { 'worklet'; const { state } = e; if (state === GestureState.BEGIN) { this.GuestureModeShared.value = GuestureMode.INIT; } else if (state === GestureState.ACTIVE) { if(this.GuestureModeShared.value === GuestureMode.INIT) { this.gestureBefore(e); // 手势类型未知时,判断手势类型 } else { this.gestureHandle(e); // 手势类型已知时,处理手势事件 } } else if (state === GestureState.END || state === GestureState.CANCELLED) { this.GuestureModeShared.value = GuestureMode.INIT; } }, // 判断手势类型 gestureBefore(e) { 'worklet'; const { focalDeltaX, focalDeltaY, scale } = e; if (Math.abs(focalDeltaX) > Math.abs(focalDeltaY)) { this.GuestureModeShared.value = GuestureMode.SWIPE; } else if (scale > 1) { this.GuestureModeShared.value = GuestureMode.SCALE; } else { this.GuestureModeShared.value = GuestureMode.MOVE; } }, // 处理手势事件 gestureHandle(e) { 'worklet'; if (this.GuestureModeShared.value === GuestureMode.SCALE) { this.shareScale.value = e.scale; } else if (this.GuestureModeShared.value === GuestureMode.SWIPE) { // swiper 切换模式时,这里什么都不用做 } else if (this.GuestureModeShared.value === GuestureMode.MOVE) { this.shareX.value += e.focalDeltaX; this.shareY.value += e.focalDeltaY; } }, // 用于判断手势事件是否应该派发 shouldResponseOnMove(e) { 'worklet'; return this.GuestureModeShared.value === GuestureMode.SWIPE; // 当模式为SWIPE时,才响应手势事件 } } }) [图片] 通过上面的代码,我们实现了手势协商,当用户在图片上进行滑动的操作时,总是会触发 <scale-gesture-handler> 的手势事件,通过对图片当前状态的判断来决定应该触发哪种手势,我们通过此种协商让 <horizontal-drag-gesture-handle> 手势在合适的时机触发,以此避免手势冲突。 5、使用小程序渲染框架时需要注意的一些地方作为一款新的渲染优化方式,开发者使用小程序渲染框架需要注意以下内容,以保证渲染的效果和性能。 (1)自定义路由时首帧渲染&首帧性能优化 小程序渲染框架的首帧渲染对共享元素动画非常重要,若共享元素节点的key 错过首帧设置的话,可能会丢失飞跃动画,所以在使用小程序渲染框架时,共享元素的 key 应该尽量在 attached 中或之前设置到页面,并且在首帧渲染时,应尽可能的减少 UI 层的渲染工作 如下: 1)所需要的数据应尽可能使用提前计算好,避免构建页面时等待太久影响响应速度 2)首次设置的数据应该尽可能的少,避免首次渲染时,页面上的元素过多,导致首帧渲染时间过长,导致动画卡顿(如:不要同时初始化太多的 <swiper-item>) 3)确保首帧渲染时,共享元素的 key 正确的设置,避免在首帧渲染时,由于找不到对应的共享元素,导致动画丢失,看不到飞跃动画 4)由于手势事件触发频繁,应尽量避免大量需要的计算的逻辑高频执行,容易导致机器发烫,或者导致动画卡顿 **worklet 函数的使用** worklet 函数的使用有一些限制,主要是由于它是在 UI 线程执行的,所以 worklet 函数中的 this 并非是页面的 this 实例, 里面所使用到的变量也是通过特殊的 babel 插件转换到UI线程的,需要与逻辑层共用的变量都需要用 wx.worklet.shared 将它声明成共享变量,在 UI 线程调用逻辑层的函数需要使用 wx.worklet.runOnJS (2)与 web 规范的差异 虽然小程序渲染框架尽可能的与 web 规范保持一致,但是由底层渲染引擎的限制,还是有一些差异,如: 1)display: flex 的默认朝向是 column,而不是 row,这需要开发者注意,官方后续会支持 block 布局方式 2)暂不支持 css 伪元素,如 ::after、::before,官方正在支持中 3)position 仅支持 absolute、relative,不支持 sticky,实现滚动吸附的效果需用 sticky-* 组件来配合 scroll-view 实现 ** <share-element> 在非小程序渲染框架运行环境里的表现是什么** 在非小程序渲染框架的运行环境内,<share-element> 组件会被视为一个 <view> 组件,需要做好布局的兼容 6、何时使用小程序渲染框架开发时,请确保小程序开发者工具版本是 最新版 nightly,sdk 版本在 2.30.2+,具体限制可参考 文档。 这些新特性的引入,使得小程序渲染框架在小程序开发中的优势更加明显,开发者可以更加便捷地实现各种复杂的交互效果,并且达到接近原生APP的体验。 [图片] 未来展望 1、个性化产品形态:将会根据不同的用户需求和场景,设计出更加符合用户喜好和习惯的动效衔接,进行组件化调用。 2、更加自然和真实的动效衔接:动效衔接将会更加贴近自然规律和真实物理效应,从而增强动画的真实感和用户体验。 3、更加智能化和自适应的动效衔接:动效衔接将会根据用户的操作行为和使用习惯,自适应调整动画效果,从而提高用户体验和产品效果。 4、扩大产品、设计与开发的协作效应:设计对动效的把控、产品对用户的洞察以及开发对新技术的应用,才可以发挥最大化的协作效应。 附1:本文作者 同程旅行研发工程师 同程旅行体验设计师 同程旅行产品经理 附2:代码片段 相册小程序代码片段(请使用 PC 端浏览器打开):https://developers.weixin.qq.com/s/E979jCmP7oHG 附3:UE标注 [图片] 附4:AB 实验效果 AB 实验显著win0.23% [图片]
2023-04-28 - Skyline|探秘下拉二楼,打造更丰富的内容展示
下拉二楼是一种常见的交互设计,可以为应用中的内容展示提供更多的可能性。 通过下拉操作,开发者可以在二楼展示更丰富、更多样化的内容,从而增加用户的点击量和留存率,例如宣传视频、精选商品、走心故事等等。 在小程序中,下拉二楼一直是一种难以实现的交互设计,即使部分小程序实现了,但效果和性能都很差。 为了丰富小程序的内容展示,提高用户的使用体验,小程序官方近期推出了下拉二楼的能力,方便小程序开发者使用。 效果展示 让我们来看看小程序 scroll-view 实现下拉效果的效果~ [图片] 实现步骤 接下来,我们来看下如何使用 scroll-view 实现下拉二楼 1、配置下拉相关属性 scroll-view 新增了以下接口供开发者配置下拉二楼的能力,开发者可以根据业务需要配置相关的属性 属性 说明 refresher-two-level-enabled 开启下拉二级能力,配置开启需同时配置 refresher-two-level-triggered 设置打开/关闭二级 refresher-two-level-threshold 下拉二级阈值 refresher-two-level-close-threshold 滑动返回时关闭二级的阈值 refresher-two-level-scroll-enabled 处于二级状态时是否可滑动 refresher-ballistic-refresh-enabled 惯性滚动是否触发下拉刷新 refresher-two-level-pinned 即将打开二级时否定住 [代码]<scroll-view type="list" scroll-y // 开启下拉刷新(下拉二级必须开启下拉刷新) refresher-enabled="{{true}}" // 开启下拉二级能力 refresher-two-level-enabled="{{true}}" // 处于二级状态是否可滑动 refresher-two-level-scroll-enabled="{{true}}" > ... </scroll-view> [代码] 2、实现二楼内容 配置完下拉二楼属性之后,接着就是将我们的二楼实现在 scroll-view 中。 在 scroll-view 放置一个子节点,声明 slot=“refresher”,该节点中的内容即为下拉二楼的内容。 [代码]<scroll-view ... > <view slot="refresher"> 这里是二楼的内容 </view> </scroll-view> [代码] 3、根据下拉状态回调进行个性化处理 接着我们需要根据业务小程序自身的诉求,根据下拉状态的回调进行个性化的处理,例如:下来完成跳转页面等。 在 scroll-view 绑定 bind:refresherstatuschange 监听下拉状态,下拉状态有以下几种 属性 说明 Idle 空闲 CanRefresh 超过下拉刷新阈值 Refreshing 下拉刷新 Completed 下拉刷新完成 Failed 下拉刷新失败 CanTwoLevel 超过下拉二级阈值 TwoLevelOpening 开始打开二级 TwoLeveling 打开二级 TwoLevelClosing 开始关闭二级 [代码]<scroll-view bind:refresherstatuschange="onStatusChange" ... > <view slot="refresher"></view> ... </scroll-view> // .js onStatusChange(e) { const status: RefreshStatus = e.detail.status if (status === RefreshStatus.TwoLeveling) { const that = this // 当打开二级之后,跳转到新的页面 wx.navigateTo({ url: '../goods/index', events: { nextPageRouteDone: function(data) { // 新页面打开之后,关闭下拉二楼 that.scrollContext.closeTwoLevel({ duration: 1 }) } } }) } } [代码] 我们来演示一下松手立即跳转(图左)、完全打开二楼后跳转(图右) [图片] 丰富小程序展示内容和形式,欢迎大家使用小程序下拉二楼,为小程序的内容展示提供更多的可能性和创意发挥的空间。 通过下拉二楼,可以展示更丰富、更多样化的内容,也为小程序的发展带来了更多的机会和挑战~ 赶紧 mark 下这个 代码片段 来接入使用吧~
2023-08-03 - setData 调用优化
[视频] 你好,我是李艺。 上节课我们主要学习了脚本优化技巧,这节课学习set Data方法在调用时的优化技巧。 首先看一下问题,在第6.6讲我们介绍过,当小程序切换至后台以后,setData便不需要调用了,这样可以节省逻辑层与视图层之间的调用消耗,还可以节省页面的渲染消耗。除了这个优化点以外,结合小程序的双线程运行机制和重渲染机制,还有其他的一些优化技巧,这节课我们就一起来看一下。 首先看实践一。 不要多次分开调用setData,尽量要合并调用,在主页的JS文件里,在dealWithListData这个方法里有三处调用setData的代码,这个代码是可以优化的,三处调用显然可以合并为一处,在一个函数内,如果调用一次setData可以达到目的就不需要分开多次调用,甚至在不同的if else分支语句里边调用了setData,适当用一些编程技巧也可以在if语句外面合并成一次调用,如我们屏幕上展示的是优化后的JS的调用代码,小程序页面没有一个beforeRender周期函数,如果有的话,所有需要setData的数据在不同地方的一个数据更新倒可以积攒起来,在这个周期函数里面集中进行更新。 下面我们看代码演示。 首先我们在主页的JS文件里面找到dealWithListData这个方法,这个方法里面我们看一下,目前我们有:这是第一个setData的调用,然后下面还有一个setData,在后面这个地方也有一个setData,一共有三处setData的调用,这三处调用它都处于同样的一个函数体代码里面是可以合并的。首先我们将这个给它移下来,移到这个位置,然后后面这个调用也可以移上来,把它放在一起,这个就删掉,同样我们后面还有一个newList,newList它其实不涉及到数据更新,所以我们是可以直接进行一个赋值的,这个地方它不需要调用setData,newList目前在我们这个里面,它其实目前是在data数据对象里面,它也不需要在data数据对象里面,它可以直接放在我们的Page对象下面,这样的话可以减少对这个视图更新的一个触发,位置改完以后我们还需要将所有的对它的调用代码,进行统一的一个查找,我们可以查this.data.newList,这个地方this.data.newList改成this.newList,再看一下其他地方,已经没有了 这个地方已经可以了,改完以后我们重新单击一下编译按钮,这个page我们再看一下,目前我们page是已经在我们Page对象下面,是没有问题的。page目前直接这样赋值也是可以的,它也不涉及到数据的视图的一个更新,所以直接这样赋值也是没有问题的,它和下面的newList一样的,也是可以通过这样的方式更新,项目已经刷新了,我们现在看一下表现,看一下我们的调试区没有错误,说明我们变动是可以的,没有问题,这个代码演示我们就说到这里。 下面看实践二。 不准备渲染的数据不要放在data数据对象里边,在主页的JS文件里边数据allList表示所有列表数据,目前它位于data数据对象里面,如我们屏幕上所示,我们可以将它直接放在当前的页面对象下,直接放在当前的页面对象上,也可以通过this关键字直接进行取用,一般方法里面this它就是指代我们当前的页面对象。 下面我们进行代码演示。 首先我们要查一下当前的allList目前位于我们data数据对象里面,前面说过所有的只有需要触发视图更新的数据才需要放在data对象里面,不需要触发的,完全可以放在外面,这是不影响的,所以我们将这个给它拷贝出来,另外我们在这个页面里面查一下所有的this.data.allList查一下这个引用,将相关的代码给它修改一下,把中间的data给它去掉,这个地方也是给它去掉,已经没有了 所有的都修改过来了,然后我们再单击编译按钮进行测试,调试区没有问题,然后看页面的表现也没有问题,这个代码演示就到这里。 下面我们看实践三,通过index局部更新长列表数据。 下面我们尝试给项目添加一个新的功能,在单击列表中的列表项标题的时候,在标题文本的末尾就压一个字符,每单击一次就追加一次,这个需求要求在主页JS文件里面的onTapRecycleItem这个方法里面进行实现,在列表数据中的某一项数据发生变化的时候,没有必要更新整个数据列表,只需要使用计算属性、使用索引局部更新的方法,更新列表中对应的某一条数据就可以了,要实现局部更新,对recycle-view组件有两种方法:第一种方法是如我们屏幕上所展示的,先改变data数据,再使用forceUpdate方法进行更新,在示例源码里面,我们在调用setData方法改变标题文本的时候,展示的便是更新某一条数据的方法,对于recycle-view recycleList是它当前真正渲染的数据,但是仅仅更新这个数据还不能完成页面的更新,还需要额外调用渲染上下文对象的forceUpdate方法,才能强制长列表进行重新渲染,对于recycle-view组件还有另外一种更新某一个列表项标题的方法,直接调用长列表组件上下文渲染对象的update方法,这种方法更简单,如我们屏幕上现在展示的,在示例源码中,item是引用对象,修改它的字段以后不需要再做其他的任何更新了,只需要再调用一下长列表组件渲染上下文对象的update方法就可以了。 下面我们看代码演示。 首先打开我们的最终的源码,在主页的JS文件里面我们找到onTapRecycleItem这个方法,在这个方法里面注意前面已经拿到了相关的一些数据,首先第一步,我们要用forceUpdate这种方法进行更新,将这个代码给它拷贝一下,然后在我们目前的项目里面找到onTapRecycleItem这个方法,前面的数据已经拿到了。我们将新代码给它粘进来,把这些代码给它反注释,我们看一下这个地方是直接将item它title属性直接在尾部加了一个加号,当然其他字符也可以,然后我们再拿到这个id,这个id其实前面已经有了,所以这个地方不再需要了,把这个给它去掉。然后我们再拿到recycleList 这是一个数据,在这个数据里面我们要找到id它所对应的需要更新的列表项,找到它以后,然后在这个地方我们用了计算属性,然后去更改它的title数据,这个数据就是item.title,就是我们前面已经更改的数据设置为这样的一种数据结果,最后再调用这个forceUpdate它其实是一个ctx,ctx是我们当前的recycle-view组件的一个上下文渲染对象,其实是它,然后在它上面调用forceUpdate方法,这个代码已经写完了,我们单击编译测试一下 看它的一个表现,单击可以看到标题后面它每次单击都会添加一个加号,这是第一种更新的方式,下面我们看第二种方式,第二种方式是更简单,直接用update这种方法将前面给它先注掉,然后使用后面这种方法,首先是改变数据属性,然后加一个加号,后面我们就直接调用渲染上下文对象的update方法,同时将index也就是我们当前的列表项里面索引,然后传给它,第二个参数是我们要更新的数据,我们需要更新哪一项数据就把这个数据传给它就可以了,注意这个地方其实它的参数类型它是一个数组,我们再单击测试看一下表现,我们看到标题后面加号仍然可以正常的追加,也没有问题,这个代码演示我们就说到这里。 下面看实践四,创建examples/pages/index页面。 按索引更新data数据,在扩展示例页面里边如我们屏幕上展示的,我们需要实现单击某一个组,将其他的组自动折叠起来这样的一个效果,折叠与否是通过一个名称为open的子属性进行控制的,在对这个属性的改变过程当中,我们就可以使用索引法进行局部更新,主要的代码如屏幕上所展示的这样,其中这个open是准备更新的数据属性,然后需要更新的数据准备好以后,就可以使用索引法调用setData方法统一进行更新了,这种更新方式具有统一性,在任何项目里面都可以使用,因为这个页面里边包含的数据量不大,修改带来的正面效果可能很小,但它也展示了如何使用索引进行局部更新的方法,当数据量很大的时候,这种更新方式的优势会更加明显 下面我们看代码演示。 首先我们看一下当前的源码在examples/pages目录下面,找到index.js文件,在这里面有一个kindToggle,这是这个方法它在我们单击每一组的时候会触发的,我们可以在wxml这个标签文件里面看到,它是有一个type属性type事件进行触发的,触发以后在js里面我们先取到id,然后这地方有一个循环,它会循环列表的每一项,同时进行检查,如果是这一项的id与我们当前传进来id相同,那就把它的open属性进行一个切换,同时其他的我们给它置为false,最后我们再通过setData重新设置一下list数据,这种方式在我们list数据很小的情况下其实没有什么问题,也看不出什么差别来,但是如果我们list数据量很大,这种方式其实它是非常耗费资源的,下面我们看我们要优化的代码是怎么写的。 首先找到我们最终的源码,找到同样的文件,也是kindToggle这个方法。第一步我们要创建一个tempData这样的一个临时对象,接下来是我们对for循环的一个改造,在这个里面我们要在属性的设置这个地方,我们要加这样一个代码,把它放在它的前面,另外还有一个这个地方也有一个,把这个代码也给它放在这个地方前面。注意这个地方,我们在往tempData数据对象里面添加这个计算属性的时候为什么要加if判断,这两个判断它本质上是为了让我们少一点往tempData对象里面,加一些不需要更新的数据。如果原来本质上它open属性没有变化的话,我们就没有必要更新,这种情况下我们就不需要去设置了。只有当它属性有变化的时候我们才需要去往里写,tempData准备好了以后,接下来我们就调set。这是set方法,通过set方法去设置,我们设置数据,这个就不要了,这个地方我们可以看一眼,打印我们可以把它放开,稍后我们会看到它的内部的数据表现。 这是原来的代码,把它注掉,然后现在是改用这样的一种方式,这个文件测试的主页目前我们还没有一个入口,下面我们给它加一个小入口,找到examples,这是一个独立的分包,然后分包里面这是它的一个主页,找到我们的app.json,找到关于分包的设置,这个地方已经存在了,所以我们也不需要添加了,接下来我们只需要改编译模式,将我们这个测试主页作为启动页面进行测试,已经启动了,现在我们单击任意组,单击以后我们可以看到这个地方,首先这个界面上它已经发生了切换,然后我们可以看一下tempData的打印情况,在这个里面我们看一下它里面它写法,是list[1].open,这是它的一个属性名称,后面这个true是它的一个值,然后上面这个也是list[0].open等于false,这是它的,而且数据更新量非常小,只有这一条数据 它这个数据量是非常小的,这是关于按索引法局部更新数据对象的一种写法,我们直接这样写也可以,但是我们如果是照着我们这个代码里面刚才看到的这种方式,就这样一种方式这样去写可能会更加清晰一点,先创建一个临时对象,然后往临时对象里边去准备我们要更新的数据属性、准备要更新的数据,所有的都准备好以后再统一调用setData,然后进行一个设置,这就是局部更新的方法,这个代码演示我们就说到这里。 在使用setData更新数据列表的时候,优先选择计算属性进行局部更新,如果有两条以上的数据需要更新,可以并排写多条计算属性,这节课我们就讲到这里。 点击查看:recycle-view 上面的网址是本课涉及的文档地址。 这节课我们主要学习了有关setData调用相关的优化技巧,下节课我们学习网络请求相关的优化技巧。 最后看一下思考题,这里有个问题请你思考一下,我们知道小程序为了保证整体上所有程序的流畅运行限制了网络请求的并发数最大为10,如果同时请求数达到了这个数字wx.request的请求将无法继续发出,后来在小程序基础库版本1.40更新以后,将这个地方进行优化了,超出的请求它不再被直接拒绝,而是放入了一个队列中排队,稍后等设备资源允许了以后,它会重新发起,只要我们使用的小程序基础库版本大于1.40便不会有10个最大限制这个问题,这样一来貌似关于并发限制这个问题便不是问题了。但是这里仍然有另外的一个问题,程序里面的并发请求它们的优先级往往并不是一样的,例如对后端数据接口的调用这类请求的优先级往往都比较高,而对于打点 日志的接口调用这类请求的优先级就比较低,那么有没有办法在网络请求发出的时候就给请求操作安排一个优先级,让高优先级的网络请求先执行呢?下节课我们就一起来深入探讨一下这个问题。
2022-07-15 - 如何实现加入购物车的抛物线效果
一、场景分析 在一些如商城、点餐小程序中实现购物车抛物线效果可以提升界面趣味性增加小程序用户体验。 二、效果预览 效果图压缩后速度有点快,请下载代码片段预览 [图片] 三、实现原理 当用户点击物品时记录当前触摸点,根据触摸点计算抛物线运动的顶点位置,通过触摸点、顶点、购物车的位置计算出抛物线运动轨迹,然后控制 icon 运动。 计算购物车在当前手机内的位置 [代码]/** 设置购物车的坐标位置 **/ wx.getSystemInfo({ success: (res) => { let busPos = {} // x y 坐标分别取屏幕百分之八十的位置 busPos['x'] = res.windowWidth * 0.8 busPos['y'] = res.windowHeight * 0.8 this.setData({ busPos }) } }) [代码] 商品点击事件的处理 点击物品后记录点击的位置,然后根据点击位置计算出抛物线的顶点位置,计算方式为点击位置的上方+150,右边+150(需要根据点击位置是否在购物左边还是右边进行判断)。 根据点击,顶点,购物车三个位置计算出抛物线运动轨迹 以3个控制点为例,点A、B、C、AB 上设置点 D、BC 上设置点 E、DE 连线上设置点 F,则最终的贝塞尔曲线是点F的坐标轨迹; 计算相邻控制点间距; 根据完成时间,计算每次执行时 D 在AB方向上移动的距离,E 在 BC 方向上移动的距离; 时间每递增 100ms,则 D、E 在指定方向上发生位移,F 在 DE 上的位移则可通过 AD/AB = DF/DE 得出; 根据 DE 的正余弦值和 DE 的值计算出F的坐标。 开启定时器,依次按照贝塞尔曲线位置做动画位移 使用定时器将抛物线运动轨迹做动画位移。 定时器执行完动画后将购物车角标+1 老规矩,结尾放代码片段 https://developers.weixin.qq.com/s/PnYfitmG7Hxv
2022-03-04