- 【好文】小程序动态换肤解决方案 - 终极篇
小程序动态换肤解决方案 – 终极篇 回顾 早些日子,我写过两篇文章介绍过在微信小程序内,如何实现换肤功能,下面贴出链接,没看过的同学可以先看看 小程序动态换肤解决方案 – 本地篇 小程序动态换肤解决方案 – 接口篇 但是上面两种方案都有不足之处,所以我在文末也备注了会出 终极篇解决方案,拖延了一些时间,今天看到评论区有人cue我说什么时候出终极篇,于是,今天花了写时间整理了一下,希望可以帮助到大家。 方案 其实这篇文章提供的解决方案,更多是 [代码]接口篇[代码]的优化版本。 解决思路就是: 将接口获取到的皮肤色值属性,动态设置到需要换肤的元素的某个属性上,本质上就是替换元素的css属性的属性值,方法就是通过给当前[代码]Page[代码]和[代码]Component[代码]对象的[代码]js文件[代码]嵌入提前设置好的[代码]css变量[代码]中,然后通过[代码]setData[代码]的方法回显到对应的[代码]wxml文件[代码]中。 采用 css变量 的方式替代原有 内联修改样式 的方式; 采用小程序原生提供的mixin解决方案 —— [代码]Behavior[代码],对页面还有组件对象来说,虽有一定的侵害性,但是可以极大程度的降低重复代码的编写; 代码 1. 监听器模块 我们知道,接口返回的数据是异步的,所以,当我们进入到指定的 [代码]Page[代码]和[代码]Component[代码] 对象内部的时候,有可能还没得到数据,就需要先注册一个监听函数,等到皮肤接口请求成功之后,然后再执行皮肤设值操作; [代码]// observer.js function Observer() { this.actions = new Map() } // 监听事件 Observer.prototype.addNotice = function(key, action) { // 因为同个Page(页面)或者Component(组件)对象有可能引入多个组件 // 这些组件都用到了同一个监听器,每个监听器的回调函数需要单独处理 // 因此,结果就是: key => [handler1, hander2, hander3....] if (this.actions.has(key)) { const handlers = this.actions.get(key) this.actions.set(key, [...handlers, action]) } else { this.actions.set(key, [action]) } } // 删除监听事件 Observer.prototype.removeNotice = function(key) { this.actions.delete(key) } // 发送事件 Observer.prototype.postNotice = function(key, params) { if (this.actions.has(key)) { const handlers = this.actions.get(key) // 皮肤接口获取数据成功,取出监听器处理函数,依次执行 handlers.forEach(handler => handler(params)) } } module.exports = new Observer() [代码] 2. 皮肤对象模型模块 因为皮肤接口只会在程序首次加载运行的时候执行,换言之,通过 [代码]发布-订阅[代码] 的方式来设置皮肤只会[代码]发生在第一次接口请求成功之后[代码],后期都不会再执行;因此,我们需要通过一个Model模型对象将数据存储起来,后面的皮肤设值操作都从该model对象中获取; [代码]// viModel.js /** * @param {*} mainColor 主色值 * @param {*} subColor 辅色值 * @param {*} reset 重置 */ function ViModel(mainColor, subColor, reset = false) { // 如果当前实例已经设置过,直接返回该实例 if (typeof ViModel.instance == 'object' && !reset) { return ViModel.instance } this.mainColor = mainColor this.subColor = subColor // 实例赋值动作触发在接口有数据返回的时候 if (this.mainColor || this.subColor) { ViModel.instance = this } return this } module.exports = { // 通过save方法来赋值要通过reset = true来重置对象 save: function(mainColor = '', subColor = '') { return new ViModel(mainColor, subColor, true) }, // 直接返回的都是已经有值的单例实例 get: function() { return new ViModel() } } [代码] 3. 小程序Mixin模块 —— Behavior 这个就是这次分享的最为重要的模块 —— 注入 themeStyle 的css变量 我们直接来看这段代码: [代码]setThemeStyle({ mainColor, subColor }) { this.setData({ themeStyle: ` --main-color: ${mainColor}; --sub-color: ${subColor}; ` }) } [代码] 想必看到这里,大家应该猜到开篇说的实现原理了 这里的 [代码]themeStyle[代码] 就是我们接下来要注入到 [代码]Page[代码] 和 [代码]Component[代码] 的 data 属性,也就是需要在页面和组件中设置的[代码]动态css变量属性[代码] [代码]//skinBehavior.js const observer = require('./observer'); const viModel = require('./viModel'); module.exports = Behavior({ data: { themeStyle: null }, attached() { // 1. 如果接口响应过长,创建监听,回调函数中读取结果进行换肤 observer.addNotice('kNoticeVi', function(res) { this.setThemeStyle(res) }.bind(this)) // 2. 如果接口响应较快,modal有值,直接赋值,进行换肤 const themeData = viModel.get() if (themeData.mainColor || themeData.subColor) { this.setThemeStyle(themeData) } }, detached() { observer.removeNotice('kNoticeVi') }, methods: { setThemeStyle({ mainColor, subColor }) { this.setData({ themeStyle: ` --main-color: ${mainColor}; --sub-color: ${subColor}; ` }) }, }, }) [代码] 4. 【应用】—— Component模块 js 文件引入[代码]skinBehavior.js[代码],通过[代码]Component对象[代码]提供的[代码]behaviors[代码]属性注入进去; wxml 文件根节点设置[代码]style="{{themeStyle}}"[代码],设置css变量值; wxss 文件通过css变量设置皮肤色值 [代码]background: var(--main-color, #0366d6);[代码] [代码]// wxButton2.js const skinBehavior = require('../../js/skinBehavior'); Component({ behaviors: [skinBehavior], properties: { // 按钮文本 btnText: { type: String, value: '' }, // 是否为辅助按钮,更换辅色皮肤 secondary: { type: Boolean, value: false } } }) [代码] [代码]<!-- wxButton2.wxml --> <view class="btn-default btn {{secondary ? 'btn-secondary' : ''}}" style="{{themeStyle}}">{{ btnText }}</view> [代码] [代码]/* wxButton2.wxss */ .btn { width: 200px; height: 44px; line-height: 44px; text-align: center; color: #fff; } .btn.btn-default { background: var(--main-color, #0366d6); } .btn.btn-secondary { background: var(--sub-color, #0366d6); } [代码] 5. 【应用】 —— Page模块 使用方法跟Component模块一样,就不写了,下面贴一下代码: [代码]// skin.js const skinBehavior = require('../../js/skinBehavior'); Page({ behaviors: [skinBehavior], onLoad() { console.log(this.data) } }) [代码] [代码]<!--skin.wxml--> <view class="page" style="{{themeStyle}}"> 换肤终极篇 <view class="body"> <wxButton2 class="skinBtn" btnText="按钮1"></wxButton2> <wxButton2 class="skinBtn"btnText="按钮2" secondary></wxButton2> <wxButton2 class="skinBtn" btnText="按钮2" ></wxButton2> </view> </view> [代码] [代码]/* skin.wxss */ .page { padding: 20px; color: var(--main-color); } .skinBtn { margin-top: 10px; float: left; } [代码] 6. 【初始化】—— 接口调用 这里就是在小程序的启动文件 app.js 调用皮肤请求接口,初始化皮肤 [代码]// app.js const { getSkinSettings } = require('./js/service'); App({ onLaunch: function () { // 页面启动,请求接口 getSkinSettings().catch(err => { console.log(err) }) } }) [代码] 效果展示 [图片] 项目地址 项目地址:https://github.com/csonchen/wxSkin 这是本文案例的项目地址,如果觉得好,希望大家都去点下star哈,谢谢大家。。。
2021-03-22 - 「笔记」简单实现服务端动态控制原生小程序界面灰色模式开关
前言 在遇到公祭日或其它特殊日子的时候,我们需要将我们的网站、小程序变成灰色(黑白)模式,如果是在网页端比较简单,只需设置*、html最外层标签的样式设置filter: grayscale(100%)即可。但是在小程序中如果直接设置page的样式的话会导致小程序内使用flex失效,布局出现错位的情况。 如何解决错位问题?以及如何通过服务端动态控制灰色模式开关请看下文。 注:此方式无法控制非自定义导航栏颜色,如想要实现更完美的效果,请配合使用自定义导航栏。 原生小程序前端设置灰色模式: wxml <view class=“grayscale”> <view class=“content”></view> …小程序代码 </view> wxss .grayscale .content, .grayscale text, .grayscale button { -moz-filter: grayscale(100%); -ms-filter: grayscale(100%); -o-filter: grayscale(100%); filter: grayscale(100%); } 第三方UI组件/库(如:vant) .grayscale .van-goods-action, .grayscale .van-submit-bar, .grayscale .van-swipe-cell { -moz-filter: grayscale(100%); -ms-filter: grayscale(100%); -o-filter: grayscale(100%); filter: grayscale(100%); } 切忌直接设置 page { filter: grayscale(100%); } 小结:简单来说,就是把样式控制明确到具体的标签。 服务端动态控制 接下来讲我们利用小程序的数据预拉取来实现动态开关。 之所以选择使用数据预拉取来控制,是因为灰色模式并不是我们日常运营所需,如果单独封装到一个请求中去,会造成不必要的资源浪费,而且数据预拉取会在用户每次访问小程序的时候都会执行一次,所以能够保证尽可能实时获取到最新的状态,但是因为本身也是异步请求,所以无法100%保证页面加载完之前,就能够实时响应,所以返回的class并没有直接设置在全局变量globalData中,而是先存到本地存储Storage里,当我们在小程序后台关闭数据预拉取后,小程序端便不会再去请求相关接口,这样就做到了随时控制开关的效果了。 实现方式: app.js App({ onLaunch: async function (options) { // 此处用于服务端鉴权,可根据自身情况设置 wx.setBackgroundFetchToken({ token: ‘grayscale’ }) wx.onBackgroundFetchData((res) => { if(res.fetchedData && res.fetchedData.class) { wx.setStorageSync(‘class’, res.fetchedData.class); } }) }, globalData: { grayscale: wx.getStorageSync(“class”).grayscale || “” } }) 需要灰色模式的wxml <view class="{{grayscale}}"> <view class=“content”></view> …小程序代码 </view> 需要灰色模式的js const app = getApp() Page({ data: { grayscale: app.globalData.class.grayscale } }) 服务端(node.js)接收到微信推送的get请求后返回数据 ctx.body = { class: { grayscale: “grayscale” } } PS:以上方案仅供参考,当前方案在微信开发者工具中因缓存问题无法实时控制,iOS端基本上是没什么大问题,如果其它更好的方案可自行处理。
2022-12-03