- 关于全局状态管理、计算属性、组件跨级传递数据、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 作者做了十年前端开发,关注我,获取更多实用的小程序开发技巧
04-30 - 🎆我们开源啦 | 基于Skyline开发的组件库🚀
我们开源啦,希望可以给大家的开发之旅带来一些灵感。我后溪的小程序也都会基于这个组件库开发,并且会保持组件库的更新与维护。 我是第一次进行开源,肯定会有错漏,欢迎大家指正,我会以最快的时间响应修改。 Skyline UI 组件库 前言 Skyline 是微信小程序推出的一个类原生的渲染引擎,其使用更精简高效的渲染管线,性能比 WebView 更优异,并且带来诸多增强特性,如 Worklet 动画、手势系统、自定义路由、共享元素等。 使用这个组件库的前提是:通过微信小程序原生+skyline框架开发,所以目前我们不保证兼容webview框架(也就是电脑端与低版本的微信),但后续会进行系统性的兼容。 使用 Skyline UI前,请确保你已经学习过微信官方的 微信小程序开发文档 和 Skyline 渲染引擎文档 。 背景 随着Skyline 渲染引擎 1.1.0 版本发布,我们所运营的小程序也平稳的渡过了阵痛期,团队使用Skyline也越来得心应手,所以接下来,团队的开发重心全面偏向Skyline渲染框架,考虑有大量的UI交互重复,我们决定基于Skyline开发了这个UI组件库。 但团队力量有限,这个新生的组件可能有很多的不尽如人意,所以希望能以开源的方式吸引更多开发者使用Skyline框架,如果这个框架不适合你,也可以借鉴其思路。 Gitee Gitee仓库 在线预览 以下是目前两个使用该框架的小程序 SkylineUI组件库 [图片] NONZERO COFFEE [图片] 开始使用 UI库结构 Skyline UI组件库 依赖于以下四部分,具体使用参考以下的具体说明 utils工具库: 其中包含了UI库自定义的一个工具类SkyUtils,它包含了组件中所含的各种函数,非常重要。 各组件元素:sky-*(组件名) skywxss样式库:其中包含深浅色色彩、文字字体、布局等样式wxss 在小程序中引入 UI库 一、直接下载引入 点击下载组件包 将src下所有文件复制到您项目根目录下的components文件夹中,没有的话请自行新建。 二、npm引入 1.在小程序项目中,可以通过 npm 的方式引入 SkylineUI组件库 。如果你还没有在小程序中使用过 npm ,那先在小程序目录中执行命令: [代码]npm init -y [代码] 2.安装组件库 [代码]npm install jieyue-ui-com [代码] 3.npm 命令执行完后,需要在开发者工具的项目中点菜单栏中的 工具 - 构建 npm 两种引入方式的不同可能导致后续使用时,引用组件的路径不同,请注意区别 1.直接引入components文件夹内,引用地址通常是 ‘./components/‘ 2.npm引入,组件引用地址通常是’./miniprogram_npm/jieyue-ui-com/’ 如何使用 1.在app.js文件中初始化工具类,并且添加两个全局变量 [代码]// app.js App({ onLaunch() { ;(async ()=>{ // 全局注册工具类SkyUtils // 这里默认npm引用,地址为'./components/utils/skyUtils',如果是直接引用组件,地址可能是'./components/utils/skyUtils',后面不再说明 const SkyUtils = await import('./components/utils/skyUtils'); wx.SkyUtils = SkyUtils.default; // 初始化设备与系统数据 wx.SkyUtils.skyInit() // 小程序自动更新方法 wx.SkyUtils.versionUpdate() })() }, globalData: { sky_system:{}, sky_menu:{} }, }) [代码] 2.在app.wxss文件中引入样式文件 [代码]//wxss * _dark.wxss 是适配深色模式的色彩变量 @import '/miniprogram_npm/jieyue-ui-com/skywxss/skycolor.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skycolor_dark.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skyfontline.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skyfont.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skyother.wxss'; [代码] 3.page.json中引用组件 [代码]//page.json { "usingComponents": { "sky-text":"/miniprogram_npm/jieyue-ui-com/sky-text/sky-text" } } [代码] 4.页面中使用 [代码] // wxml <sky-text content="文本内容" max-lines="2" fade></sky-text> [代码] 5.其他组件具体使用请参考组件包中的redeme.md 适配深色模式 如果您在开发时,全部使用我们预设好的颜色变量,那么可以自动适配深色模式。 [代码].page{ background-color: var(--bg-l0); } [代码] [代码] <view style="background-color: var(--bg-l0)"></view> <view style="background-color: {{color}}"></view> [代码] [代码] Page({ data: { color: "var(--bg-l0)" } }) [代码]
01-09 - 微信小程序答题页——swiper渲染优化及swiper分页实现
前言 swiper的加载太多问题,网上资料好像没有一个特别明确的,就拿这个答题页,来讲讲我的解决方案 这里实现了如下功能和细节: 保证swiper-item的数量固定,加载大量数据时,大大优化渲染效率记录上次的位置,页面初次加载不一定非得是第一页,可以是任何页答题卡选择某一index回来以后的数据替换,并去掉swiper切换动画,提升交互体验示例动图 [图片] 截图 [图片] [图片] 问题原因 当swiper-item数量很多的时候,会出现性能问题 我实现了一个答题小程序,在一次性加载100个swipe-item的时候,低端手机页面渲染时间达到了2000多ms 也就是说在进入答题页的时候,会卡顿2秒多去加载这100个swiper-item 思考问题 那我们能不能让他先加载一部分,然后滑动以后再去改变item的数据,让swiper一直保持一定量的swiper-item? 注意到官方文档有这么两个属性可以利用,我们可以开启衔接滑动,然后再bindchange方法中去修改data [图片] 1、保证swiper-item的数量固定,加载大量数据时,优化渲染效率 假设我们请求到的数据的为list,实际渲染的数据为swiperList 我们现在给他就固定3个swiper-item,前后滑动的时候去替换数据 正向滑动的时候去替换滑动后的下一页数据,反向滑动的时候去替换滑动后的上一页数据 当我们知道了要替换的条件,我们便可以去替换数据了 但是我们应该考虑到临界值的问题,如果当前页是list第一项和最后一项该怎么办,向左向右滑是不是得禁止啊 这边是判断没数据会让它再弹回去 2、记录上次的位置,页面初次加载不一定非得是第一页,可以是任何页 有很多时候,我们是从某一项直接进来的,比如说上次答题答到了第五题,我这次进来要直接做第六题 那么我们需要去初始化这个swiperList,让它当前页、上一页、下一页都有数据 3、答题卡选择某一index回来以后的数据替换,并去掉swiper切换动画,提升交互体验 从答题卡选择index,那就不仅仅是滑动上下页了,它可以跳转到任何页,所以也采用类似初始化swiperList的方法 swiper切换动画我这边是默认250ms,但是发现有时候从答题卡点击回来,你在答题卡点击的下一项不知道会从左还是从右滑过来 体验真的很差,一开始不知道怎么禁掉动画,其实在跳转到答题卡页的时候把duration设为0就可以了 然后在答题卡页的unload方法中恢复 关键点: 在固定3个swiper-item的同时,要保证我们可以有办法来替代微信自带swiper的current属性和change方法 swiper-limited-load使用方法及说明: 将components中的swiper-limited-load复制到您的项目中在需要的页面引用此组件,并且创建自己的自定义组件item-view在初始化数据时,为你的list的每一项指定index属性具体可以参照项目目录start-swiper-limited-load中的用法说明:其它属性和swiper无异,你们可以自己单独添加你们需要的属性总结 一开始很头疼,为什么微信小程序提供的这个swiper,没去考虑这方面 然后在网上和社区找也没有一个特别好的解决方案。 后来想想,遇到需求就静下来解决吧。 项目地址:https://github.com/pengboboer/swiper-limited-load 如果错误,欢迎指出。 如有新的需求也可以提出来,如果有时间的话,我会帮你们完善。 如果能帮到你们,记得给一个star,谢谢。 ---补充 有很多朋友在评论区提到了分页的需求,抽时间写了一个分页的Demo和大家分享一下。 还是以答题为例,比如我们一共有500条数据,一页20条,可能需要如下功能,乍一看不就加了个分页,挺简单的,其实实现起来挺麻烦的,下面说一下思路和一些需要特别注意的点: 1、从其他页面跳转到答题页时,不光只能默认在第一题,可以是任意一题,比如第80题。 跳转到任意一题,那么需要我们根据index算出该数据在第几页,然后需要请求该页数据,最后显示对应的index。我的思路更注重用户体验,不可能是上滑或者下滑才开始去请求数据,一定是要用户滑动前提前请求好数据。所以起码要保证左右两侧在初始化那一刻都有数据。如果此题和它的上一题下一题都在同一页,那么我们只需要请求一页数据(第15题,那么只需请求第1页数据)。如果此题和它的上一题或者下一题不在同一页,那么我们可能需要请求两页数据。(第20题,那么需要请求第1页和第2页数据) 2、左滑、右滑没数据时,都可以加载新数据。直到滑到第一题或者最后一题。 如果我们初始化时是第24题,那么我们左滑到第21题时,就应该去请求第一页的数据。那么用户在看完21题时,再滑到20题,可能就根本不会感知到通过网络请求了数据。但是如果用户此刻滑动特别快:滑到21题时请求了网络,请求还没成功,就又向左滑了。那么我们需要限制用户的滑动,给用户一个提示:数据正在加载中。 3、从答题卡点击任意一题可以跳转到相应的题目,并且左右滑动显示正常数据 比如我们初始化是跳转到了第80题,不一会点击答题卡又要跳转到200题,一会又跳转到150题。各种无序操作,你也不知道用户要往哪里点。 一开始是想着维护一个主list,点到哪道题往list中添加这道题所在的当页的数据,但是还得判断这一页或者左滑右滑请求新一页的数据得往list的哪个位置添加。这来回来去乱七八糟的判断就很麻烦了,很容易出bug。而且list长度太长了以后insert的性能也不好。 后来就去想,要不答题卡点击任意一题都清空旧的list,然后请求新的数据,左右滑动没数据了再请求新的数据呗。但是这样很浪费资源,并且用户体验也不好,用户已经从第1题答到第200题了,这时用户从答题卡选择了一个25题,还得重新请求网络。而且200道题的数据都没了,那再选个26题,再重新请求网络?网络有延时不说,还浪费资源。 最后转念一想,这时候就需要弄一个缓存了。所以最终的解决方法就出来了:我们维护一个map,在网络请求成功后,在map中保存对应页的数据,同时我们维护一个主list来显示对应的题目。当我们在答题卡选择某一题目,就清空list,然后判断map中有没有该页的数据,如果有就直接拿来,没有就再去网络请求。这个处理方式,写法相对来说简单,不需要乱七八糟的判断,也不浪费资源,用户体验也很不错。 总结 以上就是一些思路和要注意的地方。这个Demo断断续续花了好几天时间写出来的。可能我说的比较啰嗦比较细,只是想让需要用到这个分页Demo的同学能理解我是如何实现的。 如果觉得能帮到你,记得给一个star,谢谢。同时如果这个demo有bug或者你们有新想法,欢迎提出来。
2021-01-07 - 小程序使用webview嵌套H5中使用iframe真机无法跳转 wx.miniProgram.getEnv等失效问题分享
小程序使用webview嵌套H5真机无法跳转 wx.miniProgram.getEnv、wx.miniProgram.navigateTo等失效无响应问题分享 原因分析:由于页面使用了iframe导致在子页面中真机无法准确获取页面运行环境(开发者工具中无影响); 解决方案:在调用方法前加一个 parent. 在父页面中调用方法即可正常运行; 过程:尝试过切换jweixin代码版本但是问题依旧; 分享:遇到同类问题用这个方法解决了的小伙伴欢迎评论区留言点赞👍! <script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.3.2.js"></script> <script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script> 核心代码: var ua = navigator.userAgent.toLowerCase(); if(ua.match(/MicroMessenger/i)=="micromessenger") { //由于页面是iframe 所以在调用wx.miniProgram.getEnv的时候直接调用会没反应,需要加一个parent父组件调用即可成功调起来。(开发者工具可以成功跳转但是真机没办法跳转1.3.2-1.6.0的js都试过问题依旧) parent.wx.miniProgram.getEnv((res)=>{ if (res.miniprogram) { // alert("在微信内,在小程序内") // console.log('在小程序内') // alert("在小程序内") parent.wx.miniProgram.navigateTo({ url: "/pages/minipay/minipay?outTradeNo="+outTradeNo+"&type=miniapp&code=" , //小程序的支付地址,queryParam是需要传递的商品id等数据 fail: function(err){ alert("跳转小程序失败!") }, }); return false; } else{ alert("在微信内,但是不在小程序内") console.log('在微信内,但是不在小程序内')//嵌入公众号 return false; } }) } else{ console.log('在微信外') if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) { console.log("移动") // 支付在同一页面打开 // return; } else { console.log("PC") // 支付在同一页面打开 // return; } return false; }
2020-12-21 - 目前为止最全的微信小程序项目实例
wx-gesture-lock 微信小程序的手势密码 WXCustomSwitch 微信小程序自定义 Switch 组件模板 WeixinAppBdNovel 微信小程序demo:百度小说搜索 shitoujiandaobu 小程序:石头剪刀布(附代码说明) audiodemo 微信小程序开发之视频播放器 Video 弹幕 弹幕颜色自定义 star 微信小程序开发之五星评分 switchCity 微信小程序开发之城市选择器 城市切换 huadong_del 微信小程序滑动删除效果 jianhang_menu 微信小程序开发之圆形菜单 仿建行圆形菜单 xiaoxiaoxiao_lazyload 实现微信小程序图片懒加载特效 kangaiduowei 微信小程序:康爱多微商城:学习界面设计 tianmao_dazhuanpan 小程序实现大转盘 仿天猫抽奖 跑马灯效果(有图有源码) weapp-meirong 微信小程序学习用demo推荐:美容商城;列表,预约 baisi 微信小程序仿百思不得姐 weapp-one 仿 「ONE · 一个」 的微信小程序 netmusic-app 仿网易云音乐APP的微信小程序 a_takeaway 微信小程序的外卖demo sideslip 微信小程序『侧边栏滑动』特效 wx_plo 微信小程序之仿微信漂流瓶 kwonWhere 微信小程序-知亦行 audiodemo 微信小程序开发之视频播放 弹幕 弹幕颜色自定义 wxChart 微信小程序图标插件 guoku 微信小程序-果库 snake 微信小程序-贪吃蛇小程序 douban_movie 微信小程序-仿豆瓣电影 RecordDemo 麦克风动画 shishanggou 实现了包括常用组件,ajax获取数据,模板使用,路由等的使用,下拉刷新数据;
2019-02-12