- 关于全局状态管理、计算属性、组件跨级传递数据、onLaunch延迟加载页面、switchTab传参等功能的最佳实现方案
在小程序开发中,你是否曾经想要这些功能之一: globalData变化自动更新视图 storage数据变化能自动更新视图 监听globalData和storage的变化 像vue一样的computed计算属性 像component一样的页面observers字段监听器 组件跨级传递数据(类似于react的context.Provider,或vue的provide/inject) 跨页跨组件通信(像eventBus一样) 还没准备好必要数据,希望onLaunch延迟加载页面 全局路由守卫,拦截没权限的页面 全局对page或组件的视图层的事件进行监听或拦截 希望wx.switchTab能够传递参数 希望input双向绑定能支持字段路径 子页面向上一个页面回传数据通过getCurrentPages找到上一个页面再去调方法比较别扭,希望有更好的方式 像vue中 {{ var | filter }} 一样的过滤器功能 组件支持的pageLifetimes太少 全局开启所有页面的分享功能 等等其它更多。。。。 以上所有的这些功能都包含在我写的这个微信小程序js库中,它的名字叫做[代码]wxbuf[代码],github仓库地址 https://github.com/laivv/wxbuf 如果觉得有用,请动动小手点个star,支持我持续维护和更新更好用的功能,谢谢 只需要这一个库,就能减少很多代码量,让写小程序成为一种享受 下面我们来列举一些功能,假定你已经在你的小程序中引入了[代码]wxbuf[代码],看了之后你一定会觉得,原来还可以这样! wx.switchTab传参 wx.switchTab原本是不支持传参的,但是现在你可以像wx.navigateTo用法一样来传递参数,就是这么简单 [代码]wx.switchTab({ url: '/pages/mytab/index?id=1&type=2' }) [代码] [代码]// /pages/mytab/index.js Page({ // 首次进入页面在onLoad钩子接收wx.switchTab参数 onLoad({ id, type }) { }, // wxbuf 提供了onSwitchTab钩子,仅针对tabbar页并且在第二次及之后切入页面进行回调,用于接收参数 onSwitchTab({ id, type }) { } }) [代码] 双向数据绑定支持字段路径 原生的input是不支持字段路径的,这里我们先封装一个叫c-input的组件 c-input.wxml: [代码]<!-- c-input.wxml --> <input placeholder="{{placeholder}}" type="text" bindinput="handleInput" value="{{value}}" /> [代码] c-input.js: [代码]//c-input.js Component({ properties: { placeholder: String, vModel: { type: String, value: '', observer() { this.init() } }, }, // wxbuf提供了一些parentLifetimes生命周期 parentLifetimes: { // 当父组件调用this.setData时 setData() { this.init() } }, lifetimes: { attached() { this.init() } }, data: { value: '', }, methods: { init() { // wxbuf给所有实例提供了$parent属性,其值是父组件实例 if (this.$parent && this.data.vModel) { const value = this.getValueByKeypath(this.$parent.data, this.data.vModel) this.setData({ value }) } }, handleInput({ detail: { value } }) { if (this.$parent && this.data.vModel) { this.$parent.setData({ [this.data.vModel]: value }) } this.setData({ value }) }, getValueByKeypath(data, keypath) { const keys = keypath.split(/\[|\]|\./).filter(Boolean).map(i => i.replace(/\'|\"/g, '')) let val = undefined for (let i = 0; i < keys.length; i++) { const key = keys[i] data = val = data[key] } return val } } }) [代码] 接下来就可以直接使用了: wxml: [代码]<c-input v-model="form.name" placeholder="请输入姓名" /> <c-input v-model="form.phone" placeholder="请输入手机号" /> [代码] js: [代码]Page({ data: { form: { name: '', phone: '' } } }) [代码] 跨组件通信 需要跨组件或者跨页通信时,在构造器选项中配置[代码]listeners[代码]字段来指定事件接收函数,通过实例方法[代码]fireEvent[代码]来触发一个事件 例子: [代码]// pageA.js Page({ listeners: { updateOk(event) { console.log(event) } } }) [代码] [代码]// pageB.js Page({ handleBtnTap() { this.fireEvent('updateOk', 'hello') } }) [代码] 传统的发布订阅(on、emit)的做法是在page的[代码]onLoad[代码]或组件的[代码]attached[代码]钩子中调用[代码]on(eventName, handler)[代码]来注册事件监听,在[代码]onUnload[代码]或[代码]detached[代码]中调用[代码]off(eventName, handler)[代码]来移除事件监听,若不移除事件监听会造成内存溢出,非常麻烦,而[代码]wxbuf[代码]提供的配置式方式则不需要关心这些,更适合于小程序的开发场景 页面间数据传递,数据回传 使用实例方法[代码]openPage[代码]来打开新页面,新页面通过实例方法[代码]finish[代码]来关闭自身页面并回传数据给上一个页面,[代码]finish[代码]方法包括了关闭页面([代码]wx.navigateBack[代码])与回传数据两项功能于一身 例子: [代码]// pageA.js Page({ async handleTap() { const acceptVal = await this.openPage({ url: '/pages/detail/index?name=wxbuf', // 使用params字段传递参数,会追加在url后面,最终生成 /pages/detail/index?name=wxbuf&id=123 params: { id: '123' }, // 使用body字段传递参数 body: { value: 1 }, success(page) { // 给被打开page设置字段 page.setData({ age: 18 }) } }) console.log(acceptVal) // '这是回传数据' } }) [代码] [代码]// pages/detail/index.js Page({ onLoad({ name, id, value }) { console.log(name) // 'wxbuf' console.log(id) // '123' console.log(value) // 1 }, async handleOk() { console.log(this.data.age) // 18 // 调用finish方法回传数据给父page,并且关闭当前页面 this.finish('这是回传数据') } }) [代码] 因此,可以指定[代码]params[代码]字段来代替字符串拼接,并且[代码]params[代码]中的字段还可以是对象,比自己拼接更方便;[代码]body[代码]方式则是通过内存传递参数 获取全局数据 [代码]wxbuf[代码]提供了实例方法[代码]getStore[代码]来获取全局数据 ,可以代替 [代码]getApp().globalData[key][代码] 例子: app.js: [代码]// app.js import wxbuf from 'wxbuf' App({ globalData: { count: 1 } }) [代码] page.js: [代码]// page.js Page({ handleTap() { const count = this.getStore('count') // 相当于getApp().globalData.count console.log(count) // 1 } }) [代码] 修改全局数据 使用实例方法[代码]setStore[代码]来修改全局数据以获得响应式更新 例子: app.js: [代码]// app.js import wxbuf from 'wxbuf' App({ globalData: { count: 1 } }) [代码] page.js: [代码]// page.js Page({ handleTap() { this.setStore('count', 2) console.log(getApp().globalData.count) // 2 } }) [代码] 使用响应式的全局数据 响应式全局数据的优点是可以自动更新视图,在构造器选项中配置[代码]mixinStore[代码]字段来将[代码]store[代码]的值设置到当前实例的data字段中,并且后续一直保持同步,即依赖的全局数据的[代码]key[代码]值一但变化,当前实例引用的值也跟着变化! 如果需要实时保持一致,应当用此方式代替传统的[代码]getStore(key)[代码]或[代码]getApp().globalData.xxx[代码]的取值方式 例子: app.js: [代码]// app.js import wxbuf from 'wxbuf' App({ globalData: { count: 1, appVersion: '1.0' } }) [代码] page.js: [代码]// page.js Page({ mixinStore: ['count', 'appVersion'], onLoad() { console.log(this.data.count) // 1 console.log(this.data.appVersion) // '1.0' }, handleTap() { this.setStore('count', 2) console.log(this.data.count) // 2 console.log(this.getStore('count')) // 2 console.log(getApp().globalData.count) // 2 } }) [代码] 使用响应式的storage 响应式[代码]storage[代码]的优点是可以自动更新视图,在构造器选项中配置[代码]mixinStorage[代码]字段来将[代码]storage[代码]的值设置到当前实例的data字段中,并且后续一值保持同步,这和[代码]mixinStore[代码]的机制一样,如果需要实时保持一致,应该放弃使用传统的[代码]wx.getStorage[代码]、[代码]wx.getStorageSync[代码]来取值,而应该用此方式 例子: [代码]// 假如 storage中 count=1, isLogin=true // page.js Page({ mixinStorage: ['count', 'isLogin'], onLoad() { console.log(this.data.count) // 1 console.log(this.data.isLogin) // true }, handleTap() { this.setStorageSync('count', 2) console.log(this.data.count) // 2 console.log(this.getStorageSync('count')) // 2 console.log(wx.getStorageSync('count')) // 2 } }) [代码] 全局注入store和storage到所有页面和组件 app.js: [代码]// app.js import wxbuf from 'wxbuf' App({ // 全局注入store到所有页面、组件的实例上 injectStore: { // 注入到实例的命名空间(前缀) namespace: '$store', // 注入globalData中的哪些字段 keys: ['appVersion', 'appCount'], }, // 全局注入storage到所有页面、组件 injectStorage: { namespace: '$storage', // 注入storage中的哪些字段 keys: ['count'], }, globalData: { appVersion: 'v1.0', appCount: 0 }, onLaunch(){ } }) [代码] 接下来可以在任意页面或组件中访问这些状态数据,并且是响应式的 wxml: [代码]<view>{{$store.appCount}}</view> <view>{{$store.appVersion}}</view> <view>{{$storage.count}}</view> [代码] js: [代码] Page({ onLoad(){ console.log(this.data.$store) console.log(this.data.$storage) } }) [代码] 对store和storage进行变化监听 你可能不需要响应式的[代码]storage[代码]和[代码]store[代码],但需要监听它们的变化,因此[代码]wxbuf[代码]提供了[代码]onStorageChange[代码]与[代码]onStoreChange[代码]回调钩子,可以使用它们来监听变化 例子: [代码] Page({ onStorageChange(kvs, oldKvs) { console.log(kvs) console.log(oldKvs) }, onStoreChange(kvs, oldKvs) { console.log(kvs) console.log(oldKvs) } }) [代码] 跨级传递数据 微信小程序中并没有提供跨组件层级传递数据的方法,要给嵌套较深的组件传递数据只能一层层定义[代码]properties[代码]来传递,这非常难受,而且使用全局数据来传递数据无法限定视图的组件tree,不过[代码]wxbuf[代码]提供了跨级传递数据的方法,通过[代码]provide[代码]与[代码]inject[代码]来进行跨级数据的传递,如果你了解vue,就知道vue也是通过这种方式来实现跨级传递数据的 例子: 宿主页面: [代码]// 宿主页面 Page({ provide: { rootName: '这是page数据', rootFn() { console.log('this is rootFn') } } }) [代码] 子组件: [代码]// 子组件 Component({ inject: ['rootName', 'rootFn'], lifetimes: { attached(){ console.log(this.data.rootName) // '这是page数据' this.rootFn() // 'this is rootFn' } } }) [代码] 从上面的例子可以看出,[代码]inject[代码]注入的数据如果是非函数,则会挂载到[代码]this.data[代码]上,否则挂载到[代码]this[代码]上。 [代码]provide[代码]除了可以写成对象以外,还可以写成函数的形式,该函数必须返回一个对象,这样就成为响应式的[代码]provide[代码],即后代组件注入来自上层组件的数据发生变化后,自身也会更新 例子: 宿主页面: [代码]// 宿主页面 Page({ data: { number: 1 }, provide() { return { pageNumber: this.data.number } } }) [代码] 子组件: [代码]// 子组件 Component({ inject: ['pageNumber'], lifetimes: { attached(){ console.log(this.data.pageNumber) // 1 } } }) [代码] 要注意的是,小程序中的父子组件关系并不是[代码]jsx[代码]中的那种父子标签(slot)嵌套的关系,而是父组件在[代码]json[代码]文件中的[代码]usingComponents[代码]里导入了某个子组件,并且在[代码]wxml[代码]里使用了子组件,这样即形成父子组件关系,而将组件标签放在另一个组件的[代码]slot[代码]中并不形成父子关系 另一个要注意的是,只能在组件的[代码]attached[代码]及其之后的生命周期才能获取到[代码]inject[代码]的数据,因为只有在[代码]attached[代码]阶段才能确定其父组件是谁 计算属性 在构造器选项中声明[代码]computed[代码]字段来实现计算属性,计算属性字段会在[代码]this.data[代码]中生成对应的字段,这和[代码]vue[代码]中的[代码]computed[代码]一样 例子: wxml: [代码]<view>{{ ageDesc }}</view> [代码] js: [代码]Page({ data: { age: 18 } computed: { // 将在this.data中生成ageDesc ageDesc() { return '你的年龄是' + this.data.age } }, handleTap() { this.setData({ age: 19 }) console.log(this.data.ageDesc) // 你的年龄是19 } }) [代码] 页面的observers page支持[代码]observers[代码]了,在构造器选项中声明[代码]observers[代码]来监听[代码]data[代码]对象中某个字段值的变化,和[代码]compontent[代码]中的[代码]observers[代码]功能一样 例子: [代码]Page({ data: { count: 1 } observers: { count(newVal, oldVal) { //... } }, handleTap() { this.setData({ count: 2 }) } }) [代码] 类似vue中的 {{ var | filter }} 过滤器功能实现 小程序中视图层变量绑定并没有过滤器功能,[代码]wxs[代码]的语法又比较受限,要想自己实现[代码]{{ var | filter }}[代码]这样的语法是不行的,但我们通过自定义一个组件能达到相似的过滤器效果,从此告别[代码]wxs[代码] 定义一个全局过滤器组件 在[代码]app.json[代码]中声明一个全局组件,就叫[代码]c-text[代码],接下来实现这个组件: c-text.wxml: [代码]{{text}} [代码] c-text.js: [代码] Component({ externalClasses: ["class"], options: { virtualHost: true, }, properties: { value: { optionalTypes: [String, Number, Object, Array, Boolean, null] }, // 过滤器函数名 filter: String, // 过滤器参数 params: { optionalTypes: [String, Number, Object, Array, Boolean, null] } }, observers: { "filter,params,value"() { this.render() }, }, lifetimes: { attached() { this.render() }, }, data: { text: "", }, methods: { render() { const { value, filter, params } = this.data let text = value if (filter) { // 获取过滤器函数 const handler = this.$parent[filter] const _params = Array.isArray(params) ? params : [params] if (handler) { text = handler.call(this.$parent, value, ..._params) } } this.setData({ text: text ?? "" }) }, }, }) [代码] 现在我们就可以使用这个组件来使用过滤器功能了 基出用法 wxml: [代码]<c-text value="{{timeStamp}}" /> [代码] [代码] Page({ data: { timeStamp: 1714123672808 } }) [代码] 以上是一个普通的显示,和以下写法没什么区别: [代码]{{timeStamp}} [代码] 指定过滤器 wxml: [代码]<c-text value="{{timeStamp}}" filter="formatDate" /> [代码] [代码]Page({ data: { timeStamp: 1714123672808 }, formatDate(value) { return dayjs(value).format('YYYY-MM-DD') } }) [代码] 指定过滤器的参数 [代码]params[代码]属性指定传递给过滤器的参数 wxml: [代码]<c-text value="{{timeStamp}}" filter="formatDate" params="YYYY-MM-DD HH:mm:ss" /> [代码] [代码]Page({ data: { timeStamp: 1714123672808 }, formatDate(value, format) { return dayjs(value).format(format) } }) [代码] [代码]params[代码]属性也可以是一个数组: wxml: [代码]<c-text value="{{timeStamp}}" filter="formatDate" params="{{ ['YYYY-MM-DD HH:mm:ss', '-'] }}" /> [代码] [代码]Page({ data: { timeStamp: '', }, formatDate(value, format, defaultValue) { return value ? dayjs(value).format(format) : defaultValue } }) [代码] 全局路由拦截 在[代码]app[代码]文件中通过[代码]beforePageEnter[代码]可以进行全局路由守卫,返回[代码]布尔值[代码]来决定是否拦截某个页面 例子: app.js: [代码]import wxbuf from 'wxbuf' App({ beforePageEnter(options) { return false } }) [代码] 注意的是,[代码]beforePageEnter[代码]无法拦截[代码]Launch[代码]进来的页面,即无法拦截通过正常启动或外链打开小程序等其它非js调用进入的页面,只能拦截通过js调用打开的页面 全局对wxml视图层事件监听或拦截 可以在app.js中对所有的page和组件的视图层事件进行监听和拦截 基础的监听 page.wxml: [代码] <view bindtap="handleTap" data-name="wxbuf"></view> [代码] page.js: [代码]Page({ handleTap(event) { //... } }) [代码] app.js: [代码]// app.js import wxbuf from 'wxbuf' App({ onEventDispatch(event, next) { console.log(event.currentTarget.dataset.name) // 'wxbuf' // 继续执行原始的事件handler next(event) }, }) [代码] 当page中的[代码]view[代码]元素被点击时,会先调用[代码]app[代码]中的[代码]onEventDispatch[代码]钩子,[代码]event[代码]对象为原始的事件[代码]event[代码],可以利用此对象获取被点击元素的信息,常见的应用场景如全局埋点上报功能。 [代码]next[代码]是一个函数,调用它并传入[代码]event[代码]对象让页面上的原始的事件[代码]handler[代码]正常执行,并且必须原封不动的传入[代码]event[代码]对象,否则可能引起原始的事件[代码]handler[代码]不能接收到[代码]event[代码]对象参数 对视图层事件进行拦截 page.wxml: [代码] <view bindtap="handleTap" data-not-allowed="{{true}}"></view> [代码] page.js: [代码]Page({ handleTap(event) { wx.showToast({ title: '正常执行' }) } }) [代码] app.js: [代码]// app.js import wxbuf from 'wxbuf' App({ onEventDispatch(event, next) { if(event.currentTarget.dataset.notAllowed){ wx.showToast({ title: '没有权限' }) // 不调用next(event)则不执行原始的事件handler } else { // 继续执行原始的事件handler next(event) } }, }) [代码] 当page中的[代码]view[代码]元素被点击时,会弹出[代码]没有权限[代码]的toast提示,原始的事件[代码]handler[代码]被拦截无法执行 减少event.currentTarget.dataset解构层数 日常开发中经常会在某个元素上自定义[代码]data-[代码]的数据,并在事件处理函数中通过[代码]event.currentTarget.dataset.xxx[代码]来获取这些数据,每次都很繁琐, 利用[代码]onEventDispatch[代码]钩子可以减少取[代码]dataset[代码]的层数 page.wxml: [代码] <view bindtap="handleTap" data-name="wxbuf" data-id="123"></view> [代码] page.js: [代码]Page({ handleTap(e, { id, name }) { console.log(id) // '123' console.log(name) // 'wxbuf' } }) [代码] app.js: [代码]// app.js import wxbuf from 'wxbuf' App({ onEventDispatch(event, next) { // 将第二个参数传递给原始的事件handler next(event, event.currentTarget.dataset) }, }) [代码] 全局顶层变量定义 可以在app中使用[代码]wxbuf.global.extend[代码]定义一些顶层全局变量,在其它文件中无需import即可使用 例子: app.js: [代码]// app.js import wxbuf from 'wxbuf' wxbuf.global.extend('getAppVersion',function(){ return 'v1.0.0' }) App({ globalData: {}, onLaunch() {} // ... }) [代码] pageA.js: [代码]// pageA.js Page({ onLoad() { console.log(getAppVersion()) // 'v1.0.0' } }) [代码] 全局给page或componet的实例扩展方法 使用[代码]wxbuf.page.extend[代码] 或 [代码]wxbuf.component.extend[代码] 分别给[代码]page[代码]和[代码]component[代码]实例挂载公共方法 例子: app.js: [代码]// app.js import wxbuf from 'wxbuf' wxbuf.page.extend({ getData(key){ return this.data[key] } }) App({ globalData: {}, onLaunch() {} // ... }) [代码] pageA.js: [代码]// pageA.js Page({ data: { name: 'wxbuf is a library' }, onLoad() { const name = this.getData('name') console.log(name) // 'wxbuf is a library' } }) [代码] 组件更多的pageLifetimes支持 现在增加了一些pageLifeTimes,组件逻辑再也不用和页面耦合起来了 pageLifeTimes.pullDownRefresh 所在页面onPullDownRefresh pageLifeTimes.reachBottom 所在页面onReachBottom pageLifeTimes.pageScroll 所在页面onPageScroll pageLifeTimes.switchTab 所在tabbar页面发生onSwitchTab时调用 github仓库 还有更多功能是文章中未列举的,具体请查看 github仓库地址 https://github.com/laivv/wxbuf 作者做了十年前端开发,关注我,获取更多实用的小程序开发技巧
2024-04-30 - 如何利用 promise 影响代码的执行顺序?
[图片] 如何利用 promise 影响代码的执行顺序? 我们写代码的时候,经常会遇到这样的场景。[代码]2[代码]个不同组件,它们的生命周期本身都是相互独立,毫无关联的,但是它们调用的接口,存在相互依赖的情况。 我举个例子: 开发小程序时候,里面 [代码]App[代码] 有一个 [代码]onLaunch[代码] 的 [代码]hook[代码],在小程序初始化时调用,而 [代码]Page[代码] 里也有一个 [代码]onLoad[代码] 的 [代码]hook[代码],在页面加载时被调用。正常的执行顺序为: [代码]// 应用启动 onLaunch(options) // do sth.... // 页面加载 onLoad(query) [代码] 但是,我们往往也经常遇到这种 [代码]case[代码]: [代码]async onLaunch(){ store.dispatch('set-token',await getToken()) } async onLoad(){ // getUserInfo 依赖 token setUserInfo(await getUserInfo()) } [代码] 现在问题来了,依据上面的执行顺序,[代码]getToken[代码] 和 [代码]getUserInfo[代码] 请求实际上是并发执行的。而我们的预期是,先执行 [代码]getToken[代码] 并设置好全局 [代码]token[代码] 值之后,才调用 [代码]getUserInfo[代码],这样后端才能依据请求携带的,用户 [代码]token[代码] 信息,来给我们返回指定的数据,不然那就只有一个 [代码]401[代码] 了。 那么我们如何让它们之间产生调用的依赖关系呢? 实际上很简单 [代码]promise[代码],[代码]event-emitter[代码] 都是方法之一。接下来我们来构建一个最小化模型。 最小化模型 我们想要 [代码]onLoad[代码] 中一部分的代码的执行在 [代码]onLaunch[代码] 中特定代码之后。即把一部分并行跑的代码,变更为串行的顺序,同时也允许原先并行运行的方式。 根据描述,我们天然的就想到了 [代码]Microtask[代码],它运行在每个事件循环的执行代码,和运行[代码]Task[代码]后,[代码]Rerender[代码] 前。 接下来为了实现期望,我们就需要在 [代码]onLaunch[代码] 中去产生 [代码]Promise[代码],然后到 [代码]onLoad[代码] 中依据 [代码]Promise[代码] 状态的变化,执行代码。 那么我们就很容易在一个文件中,构建出一个最小化模型,见下方代码: [代码]let promise function producer () { console.log('producer start!') promise = new Promise((resolve) => { setTimeout(() => { console.log('promise resolved') resolve(Math.random()) }, 2_000) }) console.log('producer end!') } async function consumer () { console.log('consumer start!') console.log(await promise) console.log('consumer end!') } producer() consumer() [代码] 这段代码中,我在 [代码]producer[代码] 创建了一个 [代码]promise[代码],在 [代码]2s[代码] 后 [代码]resolve[代码] 一个随机数,然后再在 [代码]consumer[代码] 中,去 [代码]await[代码] 它的状态,变为 [代码]fulfilled[代码] 后打印 [代码]consumer end![代码]。 当然 [代码]async/await[代码] 只是语法糖,你用 [代码]then/catch[代码] 也是可以的,不过使用 [代码]await[代码] 有一个好处就是,它在面对非 [代码]Promise[代码] 对象的时候,它会自动把值进行包裹转化成 [代码]Promise[代码],即 [代码]Promise.resolve(value)[代码] 接着,让我们把这个模型进行扩充,变为多文件模型。 [代码]// ref.js 创建一个引用 export default { promise: undefined } [代码] [代码]// producer.js import ref from './ref.js' export default () => { console.log('producer start!') ref.promise = new Promise((resolve) => { setTimeout(() => { console.log('promise resolved') resolve(Math.random()) }, 2_000) }) console.log('producer end!') } [代码] [代码]// consumer.js import ref from './ref.js' export default async () => { console.log('consumer start!') console.log(await ref.promise) console.log('consumer end!') } [代码] [代码]// index.js import producer from './producer.js' import consumer from './consumer.js' producer() consumer() [代码] 执行结果同理。 移花接木 根据上述的代码,我们就可以对小程序的开发,进行一系列劫持的操作。我们以 [代码]uni-app vue2/3[代码] 和原生为例。 [代码]// vue2 Vue.mixin({ created () { if (Array.isArray(this.$options.onLoad) && this.$options.onLoad.length) { this.$options.onLoad = this.$options.onLoad.map(fn => { return async (params:Record<string, any>) => { await ref.promise fn.call(this, params) } }) } } }) // vue3 const app = createSSRApp(App) app.mixin({ created () { if (this.$scope) { const originalOnLoad = this.$scope.onLoad this.$scope.onLoad = async (params:Record<string, any>) => { await ref.promise originalOnLoad.call(this, params) } } } }) // native const nativePage = Page Page = function (options: Parameters<typeof Page>[0]) { if (options.onLoad && typeof options.onLoad === 'function') { const originalOnLoad = options.onLoad options.onLoad = async function (params: Record<string, any>) { await ref.promise originalOnLoad.call(this, params) } } nativePage(options) } [代码] 思路其实都差不多。 增强 上述的方法,虽然达到了目的,但是实在太简陋了,扩展性也很差。 我们以 [代码]ref.js[代码] 为例,里面只放了一个 [代码]promise[代码] 太浪费了,为什么不把它放入全局状态里去呢?这样随时可以取出来进行观察。 为什么不创建多个 [代码]Promise queue[代码] 呢? 这样还能循环往复地利用不同的队列,来作为代码执行的信道,同时又能够自定义并发度,超时,执行事件间隔等等。p-queue 就是不错的选择。 当然,这些也只是抛砖引玉,这些相信大家各自有各自的看法,反正先做到满足当前的需求,再根据进阶的需求进行适当的改造,做出来的才是最适合自己的。
2022-09-19 - 《小程序隐私保护指引》开发指南
收录于小程序技术文章合集中,更多文章请查看: https://note.mowen.cn/note/detail?noteUuid=cc1RI0m5js2WsxKkYd0ZK 2024年4月26日 更新: 基础库 3.4.2 更新 API 有授权弹窗的隐私接口(例如 wx.getLocation),将不会再弹官方隐私弹窗,而是在授权弹窗上增加“隐私勾选”;无授权弹窗的隐私接口(如 wx.getClipboardData),将继续保留原来的官方隐私弹窗 2023年12月25日 更新: 未授权时,input type="nickname" 会自动降级为 input type="text" 且 input 无法拉起官方隐私授权弹窗,需要按开发步骤 4、5 自行处理 2023年10月19日 更新: 已经过了官方公告的 10 月 17 日了,线上依然未生效。除非在 app.json 中手动配置 "__usePrivacyCheck__": true 来开启。 目前的处理方式为,把报错信息转换为中文 if (message.match('privacy permission is not authorized')) message = '授权失败,请同意《用户隐私保护指引》' 2023年9月14日 更新: 可以不开发,直接使用系统弹窗,见图 3 。生效日期延长到 10 月 17日了。 《墨问便签》目前已回退了版本。 https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/PrivacyAuthorize.html#六、官方隐私弹窗功能说明 官方公告里目前的机制,目前还没生效上线,但看起来有 2 个坑: 1. 必须 wx.onNeedPrivacyAuthorization 上报,才能弹自定义弹窗。 意味着方案 1,如果用户取消授权了,是没有上报的,只能选择执行退出小程序。 否则调用隐私接口时,系统弹窗会出现。 2. 用户取消授权后,间隔 10s,才能再次请求授权。 意味着 10s 内功能不可用,需要给用户一个提示语。而系统默认的报错不会 toast,以及报错信息是英语,用户也看不懂呀。 ——————— 总体思路: 目前有两个方案可供选择 1. 小程序启动时,弹出,拒绝后直接退出小程序(只用 wx.getPrivacySetting 即可) 2. 调用隐私接口前,弹出,同意后才调用隐私接口(不用 wx.getPrivacySetting,需要使用 wx.requirePrivacyAuthorize 和 wx.onNeedPrivacyAuthorization) 推荐方案 2,下面具体说说 事前准备: 1. 在文档中查看是否使用了相关隐私接口 https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/miniprogram-intro.html 2. 在「小程序管理后台」设置《小程序用户隐私保护指引》 见图 1,具体位置是:小程序后台 - 设置 - 基本设置 - 服务内容声明 - 用户隐私保护指引 [图片] 3. 如果有用到隐私接口,则需要进行小程序开发。如果没有用到隐私接口,则无需开发 实际运行的流程为: 1. 发起授权 wx.requirePrivacyAuthorize 2. 触发授权事件 wx.onNeedPrivacyAuthorization 3. 弹窗,用户点击同意或拒绝 执行第 2 步的回调函数的 resolve 方法 4. 继续或终止流程 执行第 1 步的回调函数的 success 或者 fail 方法 5. 如果继续流程,调用隐私相关 api(录音、相册等等)或者 input (微信昵称)去聚焦 开发步骤: 开发前,先在小程序 app.json 配置中添加 "__usePrivacyCheck__": true 1. 开发弹窗,并且在需要的页面注册使用。“同意” 按钮需要符合规范 同意 不同意 如果需要查看隐私协议,可以使用 wx.openPrivacyContract({}) [图片] 2. 使用 wxToPromise 方法,便于使用 await 处理相关接口 function wxToPromise (api, option) { // API 存在判断 if (!api) { wx.showModal({ title: '提示', content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。' }) return Promise.reject() } return new Promise((resolve) => { api({ ...option, success (res) { resolve([res, undefined]) }, fail (err) { resolve([undefined, err]) }, }) }) } 3. app.ts 里在 onLaunch 方法里添加触发授权事件监听 // 导入弹窗的弹出方法 import { showModal } from './components/modal/showModal' // 需要用户同意隐私授权时 if (wx.onNeedPrivacyAuthorization) wx.onNeedPrivacyAuthorization(async (resolve) => { // 触发弹窗 const [res] = await wxToPromise(showModal, { title: '个人信息保护提示', }) // 点击同意 if (res?.confirm) { resolve({ buttonId: 'agree-btn', event: 'agree' }) // 点击拒绝 } else if (res?.cancel) { resolve({ buttonId: 'disagree-btn', event: 'disagree' }) } }) 4. 封装请求隐私授权方法 import { wxToPromise } from './wxToPromise' /** * 请求隐私授权 * @return 是否已授权 */ export const requirePrivacyAuthorize = async () => { if (typeof wx.requirePrivacyAuthorize === 'function') { const [res] = await wxToPromise(wx.requirePrivacyAuthorize) if (res) return true // < 2.33.2 基础库,无 api 默认已授权 } else { return true } return false } 5. 业务页面触发隐私授权,并且在回调里调用隐私接口 例如处理微信昵称时 // wxml 用户昵称 // js /** * 请求隐私授权 * 未授权时,input type="nickname" 会自动降级为 input type="text" */ async requirePrivacyAuthorize () { if (wx.requirePrivacyAuthorize) { [res, err] = await wxToPromise(wx.requirePrivacyAuthorize) if (res) // 成功 - 继续调用隐私接口 if (err) // 失败 - 终止流程,或者走兜底方案 } // PC 端 api 不存在,可以继续调用隐私接口 // 成功后去聚焦,失败后降级为普通文本框,依然去聚焦 this.setData({nickNameInputFocus: true}) } 如果只需要成功后,才执行后续逻辑。失败时不执行。 可以这样写: async requirePrivacyAuthorize () { // 请求隐私授权 if (!await requirePrivacyAuthorize()) return this.setData({nickNameInputFocus: true}) } 例外情况,无需主动调用 requirePrivacyAuthorize,也能正常触发 wx.onNeedPrivacyAuthorization: 1. 写入剪贴板:wx.setClipboardData 2. 读取剪贴板:wx.getClipboardData 3. 获取微信头像: 常见问题: 1. 已经同意过隐私协议,如何清除掉状态? 答:把最近使用中的小程序删除 2. 如果不同意隐私协议,下次调用隐私接口时,还能触发 onNeedPrivacyAuthorization 吗? 答:可以 3. onNeedPrivacyAuthorization 多次注册,会重复监听吗? 答:会的,所以只在 app.js 里启动小程序时注册一次 4. 如果不使用 requirePrivacyAuthorize 主动触发,隐私协议和权限弹窗,哪个会先弹? 答:权限弹窗会先弹。如果拒绝权限后,不会弹隐私协议(onNeedPrivacyAuthorization 无法触发) 5. 隐私协议需要每个权限都弹一次吗? 答:如果用户同意了,则只弹一次,后续不再弹出,除非移除了小程序。如果用户一直拒绝,则需要每次调用权限之前弹出。 6. 以前我记得写入相册是系统弹窗,现在要改成自己写弹窗吗,还是说两个都要弹? 答:如果没有同意过隐私协议,并且没有授权过权限,两个都弹。 7. 如果版本是2.32.3 以下(不含)是否可以使用button的agreePrivacyAuthorization进行上报? 答:从基础库 2.32.3 开始,需要做适配的。这个基础库以下的版本,没有新增的几个 API,也就不需要做隐私协议的开发适配。只需要使用 if 判断下有没有方法就行。 例如:typeof wx.getPrivacySetting === 'function' 在 2.32.3 以下(不含),会返回 false,就直接跳过相关的隐私协议逻辑,不处理 8. 可以直接调用隐私接口吗? 答:可以但不建议。原因是有个 [bug] wx.onNeedPrivacyAuthorization 在原有授权弹窗之后触发的 https://developers.weixin.qq.com/community/develop/doc/000042e44347a0a201308a5de61800 9. PC 端如何处理? 答:PC 基础库虽然达到 2.32.3 了但目前没有 wx.getPrivacySetting 等新增的方法,因此目前无需处理 10. 企业微信环境里的小程序需要调整吗? 答:企业微信小程序,基础库比较落后,现在不需要。等基础库自动升级后,未来是需要的 11. 关于小程序隐私保护指引设置的公告,是否包括小程序里嵌入的h5页面? A:暂不包括 相关资料地址: 官方 FAQ: https://developers.weixin.qq.com/community/develop/article/doc/000c648556cb40603e406c0ac6b013 关于小程序隐私保护指引设置的公告 https://developers.weixin.qq.com/community/develop/doc/00042e3ef54940ce8520e38db61801 小程序隐私协议开发指南 https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/PrivacyAuthorize.html
2024-12-20 - 运用小程序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 - IOS scroll-view中的自定义组件fixed问题
这个是正常现象,因为 iOS 下加了 -webkit-overflow-scrolling: touch,这个会产生滚动惯性,体验更好,但会改变 fixed 的行为,建议不在 scroll-view 里有 fixed 元素
2020-04-23