需求说明
上一篇文章我是先通过在小程序本地预设几种主题样式,然后改变类名的方式来实现小程序的换肤功能的;
但是产品经理觉得每次改主题配置文件,都要发版,觉得太麻烦了,于是发话了:我想在管理后台有一个界面,可以让运营自行设置颜色,然后小程序这边根据运营在后台设置的色值来实现动态换肤,你们来帮我实现一下。
方案和问题
首先我们知道小程序是不能动态引入 wxss
文件的,这时候的色值字段是需要从后端接口获取之后,然后通过 style
内联的方式动态写入到需要改变色值的页面元素的标签上;
工作量之大,可想而知,因此,我们需要思考下面几个问题,然后尽可能写出可维护性,可扩展性的代码来:
-
页面元素组件化 —— 像
按钮
标签
选项卡
价格字体
模态窗
等组件抽离出来,认真考虑需要换肤的页面元素,避免二次编写; -
避免内联样式直接编写,提高代码可阅读性 —— 内联编写样式会导致大量的
wxml
和wxss
代码耦合一起,可考虑采用wxs
编写模板字符串,动态引入,减少耦合; -
避免色值字段频繁赋值 —— 页面或者组件引入
behaviors
混入色值字段,减少色值赋值代码编写;
ps: 后续我会想办法尽可能通过别的手段,来通过js结合stylus预编译语言的方式,将本地篇和接口篇两种方案结合起来,即读取接口返回的色值之后,动态改变stylus文件的预设色值变量,而不是内联的方式实现小程序动态换肤
实现
接下来具体来详细详解一下我的思路和如何实现这一过程:
- 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
,进行换肤;</br>
微信小程序原生提供一种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
等预编译语言的变量机制,动态修改其变量,改变主题样式,方为上策;
总结:这两篇文章只是给大家提供一下小程序换肤的解决思路,文中如有不足之处,希望大家多多留言,或者去github上给我提issue,必定及时处理,谢谢大家。
效果预览
- 接口响应较快 ——
ViModel
取值换肤
- 接口响应过慢 ——
observer
监听器回调取值换肤
项目地址
项目地址:https://github.com/csonchen/wxSkin
这是本文案例的项目地址,为了方便大家浏览项目,我把编译后的wxss文件也一并上传了,大家打开就能预览,码字不易,大家如果觉得好,希望大家都去点下star哈,谢谢大家。。。
借助 预编译语言的变量机制,动态修改其变量,改变主题样式,有想法了吗?
<span :style="`--main-color: red;`"> <span style="color: var(--main-color)">可以试试用css变量</span> </span>
有个疑问:下面这个,我界面引入某一个按钮怎么操作呢?
<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> </view> <!-- 引入模板函数 --> <wxs module="_" src="../../wxs/vi.wxs"></wxs>
// interface.js
const viBehaviors = require('../../js/viBehaviors');
Page({
behaviors: [viBehaviors],
onLoad() {}
})