评论

【好文】小程序动态换肤解决方案 - 终极篇

【好文】小程序动态换肤解决方案 - 终极篇

小程序动态换肤解决方案 – 终极篇

回顾

早些日子,我写过两篇文章介绍过在微信小程序内,如何实现换肤功能,下面贴出链接,没看过的同学可以先看看

  1. 小程序动态换肤解决方案 – 本地篇

  2. 小程序动态换肤解决方案 – 接口篇

但是上面两种方案都有不足之处,所以我在文末也备注了会出 终极篇解决方案,拖延了一些时间,今天看到评论区有人cue我说什么时候出终极篇,于是,今天花了写时间整理了一下,希望可以帮助到大家。

方案

其实这篇文章提供的解决方案,更多是 接口篇的优化版本。

解决思路就是:

将接口获取到的皮肤色值属性,动态设置到需要换肤的元素的某个属性上,本质上就是替换元素的css属性的属性值,方法就是通过给当前PageComponent对象的js文件嵌入提前设置好的css变量中,然后通过setData的方法回显到对应的wxml文件中。

  1. 采用 css变量 的方式替代原有 内联修改样式 的方式;

  2. 采用小程序原生提供的mixin解决方案 —— Behavior,对页面还有组件对象来说,虽有一定的侵害性,但是可以极大程度的降低重复代码的编写;

代码

1. 监听器模块

我们知道,接口返回的数据是异步的,所以,当我们进入到指定的 PageComponent 对象内部的时候,有可能还没得到数据,就需要先注册一个监听函数,等到皮肤接口请求成功之后,然后再执行皮肤设值操作;

// 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 就是我们接下来要注入到 PageComponent 的 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  
点赞 5
收藏
评论

3 个评论

  • helloWorld
    helloWorld
    发表于移动端
    11-07
    微信小程序不是不支持color: var(--main-color);这种写法吗
    11-07
    赞同
    回复
  • Serendipity
    Serendipity
    2023-03-30

    👣

    2023-03-30
    赞同
    回复
  • 薯饼啦🐕
    薯饼啦🐕
    2022-01-18

    大佬,想请教一下,上面提及两种混入(页面跟组件),若想直接在最外层也就是app.tsx里直接混入其他的所有页面都可以使用一劳永逸的话,有没有什么可靠的方案或者相关的api可以实现呢,找了好久好像没有找到什么靠谱的方案。

    2022-01-18
    赞同
    回复 2
    • Sam
      Sam
      2022-03-03
      你可以理解在app.ts里面混入了样式变量,本质上就是放在了一个全局对象上,后面还是在页面里面,通过这个全局对象把这个样式变量取出来使用,不知道你是不是这个意思。
      2022-03-03
      回复
    • Dreamer
      Dreamer
      2022-03-12
      我能想到的要么就是写一个组件,每个页面都引入个组件。要么就是楼主这种方式了,两种方式都不算优雅。但是对小程序来说,没办法操作dom是硬伤。
      2022-03-12
      回复
登录 后发表内容