专栏首页极乐技术社区实战教程 | 微信小程序动态换肤解决方案

实战教程 | 微信小程序动态换肤解决方案

本地篇

需求说明

在开发小程序的时候,尤其是开发第三方小程序,我们作为开发者,只需要开发一套模板,客户的小程序对我们进行授权管理,我们需要将这套模板应用到对方的小程序上,然后由我们进行发版审核即可;

但是个别客户的小程序需要做 定制化配色方案,也就是说,不同的小程序个体需要对页面的元素(比如:按钮,字体等)进行不同的配色设置,接下来我们来讨论一下怎么实现它。

方案和问题

一般来说,有两种解决方案可以解决小程序动态换肤的需求:

  1. 小程序内置几种主题样式,通过更换类名来实现动态改变小程序页面的元素色值;
  2. 后端接口返回色值字段,前端通过 内联 方式对页面元素进行色值设置。

当然了,每种方案都有一些问题,问题如下:

  • 方案1较为死板,每次更改主题样式都需要发版小程序,如果主题样式变动不大,可以考虑这种;
  • 方案2对于前端的改动很大,内联 也就是通过 style 的方式内嵌到wxml 代码中,代码的阅读性会变差,但是可以解决主题样式变动不用发版小程序的问题。

前期准备

本文采用的是 gulp + stylus 引入预编译语言来处理样式文件,大家需要全局安装一下 gulp,然后安装两个 gulp 的插件

  • gulp-stylus(stylus文件转化为css文件)
  • gulp-rename(css文件重命名为wxss文件)。

gulp

这里简单贴一下gulpfile文件的配置,比较简单,其实就是借助 gulp-stylus 插件将 .styl 结尾的文件转化为 .css 文件,然后引入 gulp-rename 插件对文件重命名为 .wxss 文件;

再创建一个任务对 .styl 监听修改,配置文件如下所示:

var gulp = require('gulp');

var stylus = require('gulp-stylus');

var rename = require('gulp-rename');

function stylusTask() {

return gulp.src('./styl/*.styl')

.pipe(stylus())

.pipe(rename(function(path) {

path.extname = '.wxss'

}))

.pipe(gulp.dest('./wxss'))

}

function autosTask() {

gulp.watch('./styl/*.styl', stylusTask)

}

exports.default = gulp.series(gulp.parallel(stylusTask, autosTask))

stylus

这里会分为两个文件,一个是主题样式变量定义文件,一个是页面皮肤样式文件,依次如下所示:

  • 主题样式变量设置

// define.styl

// theme1

theme1-main = rgb(254, 71, 60)

theme1-sub = rgb(255, 184, 0)

// theme2

theme2-main = rgb(255, 158, 0)

theme2-sub = rgb(69, 69, 69)

// theme3

theme3-main = rgb(215, 183, 130)

theme3-sub = rgb(207, 197, 174)

  • 页面皮肤样式

@import './define.styl'

// 拼接主色值

joinMainName(num)

theme + num + -main

// 拼接辅色值

joinSubName(num)

theme + num + -sub

// 遍历输出改变色值的元素类名

for num in (1..3)

.theme{num}

.font-vi

color joinMainName(num)

.main-btn

background joinMainName(num)

.sub-btn

background joinSubName(num)

输出结果:

.theme1 .font-vi {

color: #fe473c;

}

.theme1 .main-btn {

background: #fe473c;

}

.theme1 .sub-btn {

background: #ffb800;

}

.theme2 .font-vi {

color: #ff9e00;

}

.theme2 .main-btn {

background: #ff9e00;

}

.theme2 .sub-btn {

background: #454545;

}

.theme3 .font-vi {

color: #d7b782;

}

.theme3 .main-btn {

background: #d7b782;

}

.theme3 .sub-btn {

background: #cfc5ae;

}

代码我写上了注释,我还是简单说明一下上面的代码:我首先定义一个主题文件 define.styl 用来存储色值变量,然后会再定义一个皮肤文件 vi.styl ,这里其实就是不同 主题类名 下需要改变色值的元素的属性定义,元素的色值需要用到 define.styl 预先定义好的变量,是不是很简单,哈哈哈。

具体使用

但是在具体页面中需要怎么使用呢,接下来我们来讲解一下

  • 页面的 wxss 文件导入编译后的 vi.wxss文件

@import '/wxss/vi.wxss';

  • 页面的 wxml 文件需要编写需要改变色值的元素,并且引入变量 theme

<view class="intro {{ theme }}">

<view class="font mb10">正常字体</view>

<view class="font font-vi mb10">vi色字体</view>

<view class="btn main-btn mb10">主色按钮</view>

<view class="btn sub-btn">辅色按钮</view>

</view>

  • 页面 js 文件动态改变 theme变量值

data: {

theme: ''

},

handleChange(e) {

const { theme } = e.target.dataset

this.setData({ theme })

}

效果预览

接口篇

需求说明

但是产品经理觉得每次改主题配置文件,都要发版,觉得太麻烦了,于是发话了:我想在管理后台有一个界面,可以让运营自行设置颜色,然后小程序这边根据运营在后台设置的色值来实现动态换肤,你们来帮我实现一下。

方案和问题

首先我们知道小程序是不能动态引入 wxss 文件的,这时候的色值字段是需要从后端接口获取之后,然后通过 style 内联的方式动态写入到需要改变色值的页面元素的标签上;工作量之大,可想而知,因此,我们需要思考下面几个问题,然后尽可能写出可维护性,可扩展性的代码来:

  1. 页面元素组件化 —— 像按钮 标签 选项卡 价格字体 模态窗等组件抽离出来,认真考虑需要换肤的页面元素,避免二次编写;
  2. 避免内联样式直接编写,提高代码可阅读性 —— 内联编写样式会导致大量的 wxml 和 wxss 代码耦合一起,可考虑采用 wxs 编写模板字符串,动态引入,减少耦合;
  3. 避免色值字段频繁赋值 —— 页面或者组件引入 behaviors 混入色值字段,减少色值赋值代码编写;

实现

接下来具体来详细详解一下我的思路和如何实现这一过程:

model层

接口会返回色值配置信息,我创建了一个 model 来存储这些信息,于是,我用单例的方式创建一个全局唯一的 model 对象 —— ViModel

// viModel.js

/**

* 主题对象:是一个单例

* @param {*} mainColor 主色值

* @param {*} subColor 辅色值

*/

function ViModel(mainColor, subColor) {

if (typeof ViModel.instance == 'object') {

return ViModel.instance

}

this.mainColor = mainColor

this.subColor = subColor

ViModel.instance = this

return this

}

module.exports = {

save: function(mainColor = '', subColor = '') {

return new ViModel(mainColor, subColor)

},

get: function() {

return new ViModel()

}

}

service层

这是接口层,封装了读取主题样式的接口,比较简单,用 setTimeout 模拟了请求接口访问的延时,默认设置了 500 ms,如果大家想要更清楚的观察 observer 监听器 的处理,可以将值调大若干倍

// service.js

const getSkinSettings = () => {

return new Promise((resolve, reject) => {

// 模拟后端接口访问,暂时用500ms作为延时处理请求

setTimeout(() => {

const resData = {

code: 200,

data: {

mainColor: '#ff9e00',

subColor: '#454545'

}

}

// 判断状态码是否为200

if (resData.code == 200) {

resolve(resData)

} else {

reject({ code: resData.code, message: '网络出错了' })

}

}, 500)

})

}

module.exports = {

getSkinSettings,

}

view层

视图层,这只是一个内联css属性转化字符串的过程,我美其名曰视图层,正如我开篇所说的,内联 样式的编写会导致大量的 wxml 和 wxss代码冗余在一起,如果换肤的元素涉及到的 css 属性改动过多,再加上一堆的 js 的逻辑代码,后期维护代码必定是灾难性的,根本无法下手,大家可以看下我优化后的处理方式:

// vi.wxs

/**

* css属性模板字符串构造

*

* color => color属性字符串赋值构造

* background => background属性字符串赋值构造

*/

var STYLE_TEMPLATE = {

color: function(val) {

return 'color: ' + val + '!important;'

},

background: function(val) {

return 'background: ' + val + '!important;'

}

}

module.exports = {

/**

* 模板字符串方法

*

* @param theme 主题样式对象

* @param key 需要构建内联css属性

* @param second 是否需要用到辅色

*/

s: function(theme, key, second = false) {

theme = theme || {}

if (typeof theme === 'object') {

var color = second ? theme.subColor : theme.mainColor

return STYLE_TEMPLATE[key](color)

}

}

}

注意:wxs文件的编写不能出现es6以后的语法,只能用es5及以下的语法进行编写

mixin

上面解决完 wxml 和 wxss 代码混合的问题之后,接下来就是 js 的冗余问题了;我们获取到接口的色值信息之后,还需要将其赋值到Page 或者 Component 对象中去,也就是 this.setData({....})的方式, 才能使得页面重新 render,进行换肤;

微信小程序原生提供一种 Behavior 的属性,使我们避免反复 setData 操作,十分方便:

// viBehaviors.js

const observer = require('./observer');

const viModel = require('./viModel');

module.exports = Behavior({

data: {

vi: null

},

attached() {

// 1. 如果接口响应过长,创建监听,回调函数中读取结果进行换肤

observer.addNotice('kNoticeVi', function(res) {

this.setData({ vi: res })

}.bind(this))

// 2. 如果接口响应较快,modal有值,直接赋值,进行换肤

var modal = viModel.get()

if (modal.mainColor || modal.subColor) {

this.setData({ vi: modal })

}

},

detached() {

observer.removeNotice('kNoticeVi')

}

})

到这里为止,基本的功能性代码就已经完成了,接下来我们来看一下具体的使用方法吧

具体使用

  • 小程序启动,我们就需要去请求色值配置接口,获取主题样式,如果是需要从后台返回前台的时候也要考虑主题变动,可以在 onShow 方法处理

// app.js

const { getSkinSettings } = require('./js/service');

const observer = require('./js/observer');

const viModel = require('./js/viModel');

App({

onLaunch: function () {

// 页面启动,请求接口

getSkinSettings().then(res => {

// 获取色值,保存到modal对象中

const { mainColor, subColor } = res.data

viModel.save(mainColor, subColor)

// 发送通知,变更色值

observer.postNotice('kNoticeVi', res.data)

}).catch(err => {

console.log(err)

})

}

})

  • 混入主题样式字段

Page 页面混入

// interface.js

const viBehaviors = require('../../js/viBehaviors');

Page({

behaviors: [viBehaviors],

onLoad() {}

})

Component 组件混入

// wxButton.js

const viBehaviors = require('../../js/viBehaviors');

Component({

behaviors: [viBehaviors],

properties: {

// 按钮文本

btnText: {

type: String,

value: ''

},

// 是否为辅助按钮,更换辅色皮肤

secondary: {

type: Boolean,

value: false

}

}

})

  • 内联样式动态换肤

Page 页面动态换肤

<view class="intro">

<view class="font mb10">正常字体</view>

<view class="font font-vi mb10" style="{{_.s(vi, 'color')}}">vi色字体</view>

<view class="btn main-btn mb10" style="{{_.s(vi, 'background')}}">主色按钮</view>

<view class="btn sub-btn" style="{{_.s(vi, 'background', true)}}">辅色按钮</view>

<!-- 按钮组件 -->

<wxButton class="mb10" btnText="组件按钮(主色)" />

<wxButton class="mb10" btnText="组件按钮(辅色)" secondary />

</view>

<!-- 引入模板函数 -->

<wxs module="_" src="../../wxs/vi.wxs"></wxs>

Component 组件动态换肤

<view class="btn" style="{{_.s(vi, 'background', secondary)}}">{{ btnText }}</view>

<!-- 模板函数 -->

<wxs module="_" src="../../wxs/vi.wxs" />

再来对比一下传统的内联方式处理换肤功能的实现:

<view style="color: {{ mainColor }}; background: {{ background }}">vi色字体</view>

如果后期再加入复杂的逻辑代码,开发人员后期再去阅读代码简直就是要抓狂的;当然了,这篇文章的方案只是一定程度上简化了内联代码的编写,原理还是内联样式的注入;我目前有一个想法,想通过某种手段在获取接口主题样式字段之后,借助 stylus 等预编译语言的变量机制,动态修改其变量,改变主题样式,方为上策;

效果展示

  • 接口响应较快 —— ViModel 取值换肤
  • 接口响应过慢 —— observer 监听器回调取值换肤

终极篇

回顾

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

  1. 小程序动态换肤解决方案 -- 本地篇
  2. 小程序动态换肤解决方案 -- 接口篇

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

方案

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

解决思路就是:

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

  1. 采用 css变量 的方式替代原有 内联修改样式 的方式;
  2. 采用小程序原生提供的mixin解决方案 - Behavior,对页面还有组件对象来说,虽有一定的侵害性,但是可以极大程度的降低重复代码的编写;

代码

监听器模块

我们知道,接口返回的数据是异步的,所以,当我们进入到指定的 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()

皮肤对象模型模块

因为皮肤接口只会在程序首次加载运行的时候执行,换言之,通过 发布-订阅 的方式来设置皮肤只会发生在第一次接口请求成功之后,后期都不会再执行;因此,我们需要通过一个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()

}

}

小程序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};

`

})

},

},

})

【应用】—— 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);

}

【应用】 —— 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;

}

【初始化】—— 接口调用

这里就是在小程序的启动文件 app.js 调用皮肤请求接口,初始化皮肤

// app.js

const { getSkinSettings } = require('./js/service');

App({

onLaunch: function () {

// 页面启动,请求接口

getSkinSettings().catch(err => {

console.log(err)

})

}

})

效果展示

总结

目前来看,【终极篇】无疑是小程序动态换肤的最佳解决方案,但是我也希望能给大家娓娓道来,一个功能的开发是跟业务需求有强依赖关系的,也就是说,我们应该根据业务来选择合适的技术方案,在满足业务方的需求之余,可以就目前功能可扩展性给业务方提供更多更好的优化思路和方向,这也是为了给产品的持续性迭代提供了可靠性。

极乐技术社区

本文分享自微信公众号 - 极乐技术社区(wxapp-union),作者:csonchen

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-06-10

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 黄荣奎:腾讯云微信小程序解决方案

    本文介绍小程序在腾讯云上的解决方案,如何快速帮助开发者快速完成开发和上线。

    Techeek
  • 【开源公告】微信小程序解决方案 Westore 开源

    众所周知,小程序本身的工程化十分优秀,不管从组件化、开发、调试、发布、灰度、回滚、上报、统计、监控和最近的云能力都非常完善。据统计,开发小程序使用最多的技术栈是...

    腾讯开源
  • 监测生命体征、活动水平的可穿戴电子产品设计方案

    移动电话的普及使我们能够随时随地拨打电话。又经过20年的创新后,语音通话已不再是手机这款智能设备的主要功能,它不仅可以拍摄美丽的照片、播放音频和视频流文件,而且...

    不脱发的程序猿
  • 更完整更系统的python入门知识总结!评价:1.5w月薪

    只要接触一点编程的同学就知道,我一点也没有言过其实。对于学习Python的重要性,这里不再赘述。今天整理的教程,是给零基础的同学入门Python。

    一墨编程学习
  • 微信公开课:解密小程序视频客服解决方案

    ? 就在前天(1月9日),为期两天的微信公开课PRO又一次如期而至。除了微信之父张小龙的4小时演说外,这次微信公开课PRO还带来了各行业中关于微信和微信小程序...

    腾讯云音视频
  • 微信公开课:解密小程序视频客服解决方案

    就在1月9日,为期两天的微信公开课PRO又一次如期而至。除了微信之父张小龙的4小时演说外,这次微信公开课PRO还带来了各行业中关于微信和微信小程序的一些应用和分...

    腾讯云通信团队
  • 【程序源代码】微信商城类小程序开发最佳实践和解决方案

    最近恰逢毕业季,有好多快毕业的大学生同学电话或者微信私下问我。因为他们大部分人要进行毕业设计和答辩。涉及到程序开发的作业和论文。

    程序源代码
  • 「跳一跳」推出皮肤道具!腾讯,你果然是个卖皮肤的公司……

    知晓君
  • 模拟变色龙,软体机器人也能实时根据背景变色!依赖温度实现,研究登上Nature子刊

    有科学家从蜻蜓大脑中看到了未来导弹防御的高效应用,有科学家从蝙蝠超声波中受到启发,研发出了新的扫描系统,用来检测输油管道的腐蚀情况……

    大数据文摘
  • 微信小程序教学第三章(含视频):小程序中级实战教程:列表-静态页面制作

    § 列表 - 开发准备 本文配套视频地址: https://v.qq.com/x/page/f0554syejjd.html 开始前请把 ch3-1 分...

    iKcamp
  • 字节头条部Android二面:说一说Android动态换肤实现原理吧,答不上来下一个

    这种换肤的方式,也就是我们所说的内置换肤,就是在APP内部放置多套相同的资源。进行资源的切换。

    Android技术干货分享
  • 人类医生的终结者:深度学习医疗2016-17两次大突破

    【新智元导读】南澳大学医学专家 Luke Oakden-Rayner 发表了一系列博客文章,讨论机器会不会在短时间内取代人类医生,他总结出了医疗AI领域的 6 ...

    新智元
  • 小程序云开发实现微信支付,小程序支付常见问题汇总及解决方案

    通常出现这个问题,是因为你拿到老师的代码以后,没有把project.config.json和pay的config下面的index.js里的appid改成你自己的...

    编程小石头
  • 微软改进Face API,显著降低肤色识别错误率

    这一改进解决了最近的担忧,即商业上可用的面部识别技术更准确地识别出肤色较浅的人的性别,而不是深色的肤色,而且他们在肤色较浅的男性身上表现最好,而肤色较深的女性则...

    AiTechYun
  • 微信小程序实战开发四:小程序获取用户信息流程及信息存储方式解析

    微信小程序在创建初期会给一个获取用户信息的示例代码,我们今天就来解析一下小程序全局app.js运行方式及用户数据存储建议。

    睿儿网络郝刚
  • 脑洞#微服务架构令川普赢得美国大选?

    据美国全国广播公司(NBC)11月9日最新报道,美国共和党总统候选人唐纳德•特朗普战胜民主党总统候选人希拉里•克林顿,赢得2016年美国总统大选。 此前,根据美...

    yuanyi928
  • 微信早报 | 微软第一款微信小程序正式发布;微信将管制「互骂群」;腾讯官方感谢 360

    知晓君
  • 小程序一周报 | 第三方小游戏可正式发布 / 跳一跳小游戏皮肤上线

    极乐君
  • 新闻动态|腾讯披露AI+产业成绩单,落地应用上百个行业

    “腾讯AI在消费互联网领域积累了海量实践,目前已经在产业互联网领域 广泛落地。”腾讯副总裁姚星认为,当前AI已进入产业应用时代。

    优图实验室

扫码关注云+社区

领取腾讯云代金券