- 🎆我们开源啦 | 基于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)" } }) [代码]
2024-01-09 - 这个库能轻松解决99%的异步和逻辑加载时机问题(异步篇)
[图片] 你是否纠结过底层业务逻辑(登陆、获取用户信息等)到底是放app.js的onLaunch还是page的onLoad里比较好,或者因为异步问题被迫放在了onload,我们来分析一下优劣 - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 分析 onLaunch处理 优点:底层业务逻辑集中并且只需写一次,比较好维护 缺点:目前没有一个理想的方案来解决onLaunch和onLoad的异步问题,包括注册回调、重写onLoad、请求拦截等。 onLoad处理 优点:因为不涉及跨页面通知,因此异步逻辑比较好处理 缺点:每个页面都得写一次底层业务逻辑,非常繁琐,而且既然是公用的底层业务逻辑,分散在每个页面的onLoad里,好像也不大对劲。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 抉择 按照高内聚低耦合的原则,那逻辑和数据放onLaunch里肯定的,不应该和普通page逻辑耦合在一起,通用的数据和逻辑应该在入口去处理,执行一次到处使用,就像vue的main.js一样,会注册一些技术层的基础设施(路由、状态管理等插件),那业务层的基础设施不就是token、用户信息、所在位置等逻辑吗? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 想象中的最佳实践 那我们的目标就是如何满足两者的优点,避免两者的缺点,做到真正的“高内聚低耦合” 1.保持底层业务逻辑写在入口app.js,避免耦合page里的逻辑 2.能在任何page里第一时间拿到globalData数据 3.使用方便,做到在业务开发中无感知,不需要写额外的调用、通知等代码 4.无任何副作用,不会影响其他功能,比如重写阻塞onLoad 5.灵活可配,适用以后此类任何业务 - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 梦想成真先看一段代码 ⬇️ // page.js export default { name: 'Home', onLoadLogin(){ //登录成功(拿到token) && 页面初始化完成 //Tips:适用于某页面发送的请求依赖token的场景 }, onLoadUser(){ //页面初始化完成 && 获取用户信息完成 //Tips:适用于页面初始化时需要用到用户信息去做判断再走页面逻辑的场景 }, onReadyUser(){ //dom渲染完成 && 获取用户信息完成 //Tips:适用于首次进入页面需要在canvas上渲染头像的类似场景 }, onReadyShow(){ //小程序内页面渲染完成 && 页面显示 //Tips:适用于需要获取小程序组件或者dom,并且每次页面显示都会执行的场景 }, } 应该懂什么意思了吧?是不是你理想中的样子,使用起来跟没有似的 ⬆️ 这段示例代码满足了上面的第2、3、4条目标 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 再来看一段 ⬇️ // app.js // 配置自定义钩子,所有钩子都可以随意组合搭配使用,执行机制类似于Promise.all(但不是用Promise实现的) CustomHook.install({ 'Login':{ // 自定义钩子名称、必须大写字母开头 name:'Login', watchKey: 'token', onUpdate(token){ //有token则触发此钩子 return !!token; } }, 'User':{ // 自定义钩子名称 name:'User', watchKey: 'userInfo', onUpdate(user){ //获取到userinfo里的userId则触发此钩子 return !!user.userId; } } //依赖globalData中数据 }, globalData) 怎么样,是不是很棒,依赖globalData,名字可配,连触发规则都可配,而且还附加了可随意组合的功能(意外还解决了页面内逻辑执行时机问题,在下篇讲) ⬆️ 这段示例代码满足了上面的第1、5条目标。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 是不是跃跃欲试了,那就赶紧试试,好用回来告诉我! ⬇️(公司内部已接入两年了很稳定) GitHub:https://github.com/1977474741/spa-custom-hooks [图片]
2023-07-07 - 小程序自动销毁后的使用体验优化
2023年12月24日更新 经过测试发现,官方提供的onSaveExitState有一些问题,不太符合官方文档提出的预期,大家暂时慎重使用onSaveExitState功能,用其他方案吧。 ======================================================================== 假设用户在小程序内进行一个答题的活动,或者进行一个测试,这个活动或测试的时间比较长,大概需要10分钟的时间。当用户答题进行到一半的时候,来了一个重要的电话,电话打了十几分钟,回来之后想着继续进行操作,发现小程序是重新打开的状态。之前答题答了5分钟,白费了。这样,用户需要重新进行答题。 问题场景分析 用户离开小程序时间太久(官方说30分钟以上,但测试十几分钟分钟以上)或者手机内存不够用的时候,小程序会被销毁,也就是完全终止运行了。此时用户再想进入小程序进行之前的操作,只能重新操作一遍。 解决方案 以本场景为例,如果用户正在答题,在用户退出小程序的时候,将当前页面的答题进度数据进行一个保存,当用户再重新进入小程序的时候,检查是否有答题进行一半的数据。如果有,自动跳转到答题的页面,并且在onload中恢复退出之前状态的数据,让用户继续进行答题的操作。 微信小程序有一个非常好用的回调函数onSaveExitState。 退出状态onSaveExitState 每当小程序可能被销毁之前,页面回调函数 [代码]onSaveExitState[代码] 会被调用。如果想保留页面中的状态,可以在这个回调函数中“保存”一些数据,下次启动时可以通过 [代码]exitState[代码] 获得这些已保存数据。 代码示例: { "restartStrategy": "homePageAndLatestPage" } Page({ onLoad: function() { var prevExitState = this.exitState // 尝试获得上一次退出前 onSaveExitState 保存的数据 if (prevExitState !== undefined) { // 如果是根据 restartStrategy 配置进行的冷启动,就可以获取到 prevExitState.myDataField === 'myData' } }, onSaveExitState: function() { var exitState = { myDataField: 'myData' } // 需要保存的数据 return { data: exitState, expireTimeStamp: Date.now() + 24 * 60 * 60 * 1000 // 超时时刻 } } }) onSaveExitState 返回值可以包含两项: 字段名 类型 含义 data Any 需要保存的数据(只能是 JSON 兼容的数据) expireTimeStamp Number 超时时刻,在这个时刻后,保存的数据保证一定被丢弃,默认为 (当前时刻 + 1 天) 一个更完整的示例:在开发者工具中预览效果 注意事项如果超过 [代码]expireTimeStamp[代码] ,保存的数据将被丢弃,且冷启动时不遵循 [代码]restartStrategy[代码] 的配置,而是直接从首页冷启动。[代码]expireTimeStamp[代码] 有可能被自动提前,如微信客户端需要清理数据的时候。在小程序存活期间, [代码]onSaveExitState[代码] 可能会被多次调用,此时以最后一次的调用结果作为最终结果。在某些特殊情况下(如微信客户端直接被系统杀死),这个方法将不会被调用,下次冷启动也不遵循 [代码]restartStrategy[代码] 的配置,而是直接从首页冷启动。
2023-12-24 - 用Proxy实现页面和组件公共状态动态更新(类Vuex)
支持vuex中的mapActions,mapMutations辅助函数,也可以在bindStore时添加到页面中,页面卸载时最好调佣 removeStore.remove() 来删除不需要更新的页面对象 页面中使用 [图片] 或者 [图片] 组件中使用 [图片] 也可以这样 [图片] //初始化state值 let state = { } //更新state方法 let mutations = { } //公共方法 let actions = { } let stateProxy; const Store = (options = {}) => { if(options.state) state = options.state if(options.state) mutations = options.mutations if(options.state) actions = options.actions //创建Proxy stateProxy = new Proxy(state, { get(target, property) { return target[property] }, set(target, property, value) { pageArrays.forEach(item => { const newData = {} //判断该页面是否需要更新property if (item.arr.includes(property)) { newData[property] = value } item.self.setData(newData) }) return Reflect.set(target, property, value) } }) return { state, commit: commitFun, dispatch: dispatchFun, } } //以下为主要代码(原理) // Actions commit方法 export const commitFun = (str, params) => { return mutations[str](stateProxy, params) } // Actions dispatch方法 export const dispatchFun = (str, params) => { return actions[str]({ state, commit: commitFun, dispatch: dispatchFun }, params) } //存放页面对象数组 const pageArrays = [] //创建数组索引 let id = 1; //删除卸载的页面和组件 class RemovePageItem { ids = null; constructor(ids) { this.ids = ids } remove() { pageArrays.some((item, index, arr) => { if (item.id === this.ids) { arr.splice(index, 1) return true } }) } } //绑定页面并初始化所需state export const bindStore = (self, useState = [], useMutations = [], useActions = []) => { id++ const newData = {} useState.forEach(item => { newData[item] = (state[item] || state[item] === 0) ? state[item] : '' }) useMutations.forEach(item => { self[item] = (params) => mutations[item](stateProxy, params) }) useActions.forEach(item => { self[item] = (params) => actions[item]({ state, commit: commitFun, dispatch: dispatchFun }, params) }) self.setData(newData) pageArrays.push({ self, id, arr: useState }) return new RemovePageItem(id) } //绑定更新state方法 export const mapMutations = (arr = []) => { const List = {} arr.forEach(item => { List[item] = (params) => mutations[item](stateProxy, params) }) return List } export const mapActions = (arr = []) => { const List = {} arr.forEach(item => { List[item] = params => actions[item]({ state, commit: commitFun, dispatch: dispatchFun }, params) }) return List } const _default = { ...Store() } export default _default
2022-06-14