前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实战教程 | 微信小程序动态换肤解决方案

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

作者头像
极乐君
发布2021-07-05 14:44:12
2.1K0
发布2021-07-05 14:44:12
举报
文章被收录于专栏:极乐技术社区极乐技术社区

本地篇

需求说明

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

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

方案和问题

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

  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)

})

}

})

效果展示

总结

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

极乐技术社区

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-06-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 极乐技术社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云开发 CloudBase
云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档