本地篇
需求说明
在开发小程序的时候,尤其是开发第三方小程序,我们作为开发者,只需要开发一套模板,客户的小程序对我们进行授权管理,我们需要将这套模板应用到对方的小程序上,然后由我们进行发版审核即可;
但是个别客户的小程序需要做 定制化配色方案,也就是说,不同的小程序个体需要对页面的元素(比如:按钮,字体等)进行不同的配色设置,接下来我们来讨论一下怎么实现它。
方案和问题
一般来说,有两种解决方案可以解决小程序动态换肤的需求:
当然了,每种方案都有一些问题,问题如下:
前期准备
本文采用的是 gulp + stylus 引入预编译语言来处理样式文件,大家需要全局安装一下 gulp,然后安装两个 gulp 的插件
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 预先定义好的变量,是不是很简单,哈哈哈。
具体使用
但是在具体页面中需要怎么使用呢,接下来我们来讲解一下
@import '/wxss/vi.wxss';
<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>
data: {
theme: ''
},
handleChange(e) {
const { theme } = e.target.dataset
this.setData({ theme })
}
效果预览
接口篇
需求说明
但是产品经理觉得每次改主题配置文件,都要发版,觉得太麻烦了,于是发话了:我想在管理后台有一个界面,可以让运营自行设置颜色,然后小程序这边根据运营在后台设置的色值来实现动态换肤,你们来帮我实现一下。
方案和问题
首先我们知道小程序是不能动态引入 wxss 文件的,这时候的色值字段是需要从后端接口获取之后,然后通过 style 内联的方式动态写入到需要改变色值的页面元素的标签上;工作量之大,可想而知,因此,我们需要思考下面几个问题,然后尽可能写出可维护性,可扩展性的代码来:
实现
接下来具体来详细详解一下我的思路和如何实现这一过程:
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')
}
})
到这里为止,基本的功能性代码就已经完成了,接下来我们来看一下具体的使用方法吧
具体使用
// 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 等预编译语言的变量机制,动态修改其变量,改变主题样式,方为上策;
效果展示
终极篇
回顾
早些日子,我写过两篇文章介绍过在微信小程序内,如何实现换肤功能,下面贴出链接,没看过的同学可以先看看
但是上面两种方案都有不足之处,所以我在文末也备注了会出 终极篇解决方案,拖延了一些时间,今天看到评论区有人cue我说什么时候出终极篇,于是,今天花了写时间整理了一下,希望可以帮助到大家。
方案
其实这篇文章提供的解决方案,更多是 接口篇的优化版本。
解决思路就是:
将接口获取到的皮肤色值属性,动态设置到需要换肤的元素的某个属性上,本质上就是替换元素的css属性的属性值,方法就是通过给当前Page和Component对象的js文件嵌入提前设置好的css变量中,然后通过setData的方法回显到对应的wxml文件中。
代码
监听器模块
我们知道,接口返回的数据是异步的,所以,当我们进入到指定的 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模块
// 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)
})
}
})
效果展示
总结
目前来看,【终极篇】无疑是小程序动态换肤的最佳解决方案,但是我也希望能给大家娓娓道来,一个功能的开发是跟业务需求有强依赖关系的,也就是说,我们应该根据业务来选择合适的技术方案,在满足业务方的需求之余,可以就目前功能可扩展性给业务方提供更多更好的优化思路和方向,这也是为了给产品的持续性迭代提供了可靠性。
极乐技术社区