The local paper

specifications

In the development of small programs, especially the development of third-party small programs, we, as developers, only need to develop a set of templates, the customer’s small program for our authorization management, we need to apply this template to the other side of the small program, and then we can release audit;

But individual customer applet need to do custom color scheme, that is, different applet individual need to page elements (such as: buttons, fonts, etc.) for different color Settings, let’s discuss how to implement it.

Programs and problems

In general, there are two solutions to the need for dynamic peels for applets:

  1. Applet built-in several theme styles, by changing the class name to achieve dynamic change applet page element color value;

  2. The backend interface returns the color value field, and the front end sets the color value of the page element through inline mode.

Of course, there are some problems with each solution, including the following:

  • Scheme 1 is more rigid, every time you change the theme style, you need to issue a small program, if the theme style does not change much, you can consider this;

  • Solution 2 has a big change on the front end. Inline is embedded in the WXML code by style, which will make the code read worse, but it can solve the problem of changing the theme style without issuing applets.

preparation

This article uses gulp + stylus to introduce a precompiled language to handle style files. You need to install gulp globally and then install two gulp plug-ins

  1. gulp-stylus(Convert stylus file to CSS file)
  2. gulp-renameCSS file renamed to WXSS file.

gulp

To convert a.styl file to a.css file using the gulp-stylus plugin, rename the file to a.wxss file using the gulp-rename plugin.

Create another task to listen for changes to.styl with the following configuration file:

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))
Copy the code

stylus

There will be two files, a theme style variable definition file and a page skin style file, as follows:

  1. Theme style variable Settings
// 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)
Copy the code
  1. Page skin Style
@import './define.styl'

// Concatenate the primary color values
joinMainName(num) 
  theme + num + -main

// Splice the auxiliary color values
joinSubName(num)
  theme + num + -sub  

// Iterate over the name of the element class that changes color values
for num in (1.3)
  .theme{num}
    .font-vi
      color joinMainName(num)

    .main-btn
      background joinMainName(num)
      
    .sub-btn
      background joinSubName(num)   
Copy the code

Output:

.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;
}
Copy the code

I have commented the code, but I will briefly explain the above code: Styl: define a theme file define. Styl to store color variables, and then define a skin file vi. Styl, which is the attribute definition of elements that need to change color values under different theme class names. Isn’t it easy, hahaha.

The specific use

But how do you use it on a specific page

  1. Of the pagewxssFile import compiledvi.wxssfile
@import '/wxss/vi.wxss';
Copy the code
  1. Of the pagewxmlThe file needs to write elements that change color values and introduce variablestheme
<view class="intro {{ theme }}">
  <view class="font mb10">Normal font</view>
  <view class="font font-vi mb10">Vi color fonts</view>
  <view class="btn main-btn mb10">The main color button</view>
  <view class="btn sub-btn">Complementary color button</view>
</view>
Copy the code
  1. pagejsDynamic file changethemeA variable’s value
  data: {
    theme: ' '
  },

  handleChange(e) {
    const { theme } = e.target.dataset
    this.setData({ theme })
  }
Copy the code

Results the preview

Interface article

specifications

However, the product manager felt that it was too troublesome to change the theme configuration file every time, so he said: I want to have an interface in the management background, so that the operation can set the color by itself, and then the small program can achieve dynamic skin changing according to the color value set by the operation in the background, you can help me to achieve it.

Programs and problems

First of all, we know that small programs can not be dynamically introduced into the WXSS file, at this time, the color value field is to be obtained from the back-end interface, and then through the style inline way to dynamically write to the need to change the color value of the page element label; As you can imagine, this is a lot of work, so we need to think about the following questions and write as maintainable and extensible code as possible:

  1. Componentization of page elements – components such as buttons, labels, tabs, prices, fonts, modal Windows, etc., are removed, and the page elements that need to be skinned are carefully considered to avoid secondary writing;

  2. Avoid inline style to write directly, improve code readability — Inline style will lead to a large number of WXML and WXSS code coupled together, can consider using WXS to write template strings, dynamic introduction, reduce coupling;

  3. Avoid frequent assignment of color value fields — Introducing behaviors into color value fields on pages or components reduces the writing of color value assignment codes;

implementation

The following is a detailed explanation of my thinking and how to achieve this process:

  1. Model layer:The interface will return the color value configuration information, and I created onemodelTo store this information, so I create a globally unique singletonmodelObject –ViModel
// viModel.js
/** * topic object: is a singleton *@param {*} MainColor mainColor value *@param {*} SubColor Secondary color value */
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()
  }
}
Copy the code
  1. Service layer:This is the interface layer, encapsulating the read topic style interface, relatively simple, withsetTimeoutSimulates the latency for requesting interface access, set by default500Ms, if you want a clearer viewThe observer listenerThe value can be increased several times
// service.js
const getSkinSettings = () = > {
  return new Promise((resolve, reject) = > {
    // Simulate the back-end interface access, temporarily use 500ms as a delay processing request
    setTimeout(() = > {
      const resData = {
        code: 200.data: {
          mainColor: '#ff9e00'.subColor: '# 454545'}}// Check whether the status code is 200
      if (resData.code == 200) {
        resolve(resData)
      } else {
        reject({ code: resData.code, message: 'Network error'})}},500)})}module.exports = {
  getSkinSettings,
}
Copy the code
  1. The view layer:View layer, which is just a process of converting inline CSS properties to strings, as I call itThe view layerAs I said at the beginning,inlineThe writing of styles leads to a lot ofwxmlwxssThe code is redundant together if the skin element is involvedcssToo many property changes, plus a bunch ofjsThe logic code, the later maintenance code must be disastrous, can not start, we can look at my optimized processing:
// vi.wxs
/** * CSS attribute template string constructor ** color => color attribute string assignment constructor ** BACKGROUND => background attribute string assignment constructor */
var STYLE_TEMPLATE = {
  color: function(val) {
    return 'color: ' + val + '! important; '
  },

  background: function(val) {
    return 'background: ' + val + '! important; '}}module.exports = {
  /** * Template string method **@param Theme Theme style object *@param Key requires building an inline CSS property *@param Second whether to use the secondary color */
  s: function(theme, key, second = false) {
    theme = theme || {}
    
    if (typeof theme === 'object') {
      var color = second ? theme.subColor : theme.mainColor
      return STYLE_TEMPLATE[key](color)
    }
  }
}
Copy the code

Note: WXS files cannot be written using es6 syntax, only ES5 and below syntax

  1. Mixins:That’s donewxmlwxssAfter the issue of code mixing, here’s what happensjsThe problem of redundancy; Once we get the color information for the interface, we need to assign it toPageorComponentObject, which is thetathis.setData({.... })In order to make the page againrender, skin change;

Wechat applet natively provides a Behavior attribute, which enables us to avoid repeated setData operations and is very convenient:

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

module.exports = Behavior({
  data: {
    vi: null
  },

  attached() {
    // 1. If the interface response is too long, create a listener
    observer.addNotice('kNoticeVi'.function(res) {
      this.setData({ vi: res })
    }.bind(this))

    // 2. If the interface responds quickly and modal has a value, it is directly assigned to perform skin change
    var modal = viModel.get()
    if (modal.mainColor || modal.subColor) {
      this.setData({ vi: modal })
    }
  },

  detached() {
    observer.removeNotice('kNoticeVi')}})Copy the code

Now that the basic functional code is complete, let’s look at how to use it

The specific use

  1. Small program start, we need to request color value configuration interface, get the theme style, if you need to return to the foreground from the background also want to consider the theme change, can be inonShowMethods to deal with
// app.js
const { getSkinSettings } = require('./js/service');
const observer = require('./js/observer');
const viModel = require('./js/viModel');

App({
  onLaunch: function () {
    // The page starts, requesting the interface
    getSkinSettings().then(res= > {
      // Gets the color value and saves it in the Modal object
      const { mainColor, subColor } = res.data
      viModel.save(mainColor, subColor)

      // Send notification to change color value
      observer.postNotice('kNoticeVi', res.data)
    }).catch(err= > {
      console.log(err)
    })
  }
})
Copy the code
  1. Mix in the theme style field
  • PageThe page with
// interface.js
const viBehaviors = require('.. /.. /js/viBehaviors');

Page({
  behaviors: [viBehaviors],

  onLoad(){}})Copy the code
  • ComponentThe component with
// wxButton.js
const viBehaviors = require('.. /.. /js/viBehaviors');

Component({
  behaviors: [viBehaviors],

  properties: {
    // Button text
    btnText: {
      type: String.value: ' '
    },

    // Whether it is an auxiliary button, change the auxiliary color skin
    secondary: {
      type: Boolean.value: false}}})Copy the code
  1. Inline style dynamic peels
  • PagePage dynamic skin
<view class="intro">
  <view class="font mb10">Normal font</view>
  <view class="font font-vi mb10" style="{{_.s(vi, 'color')}}">Vi color fonts</view>
  <view class="btn main-btn mb10" style="{{_.s(vi, 'background')}}">The main color button</view>
  <view class="btn sub-btn" style="{{_.s(vi, 'background', true)}}">Complementary color button</view>

  <! -- Button component -->
  <wxButton class="mb10" btnText="Component button (main color)" />
  <wxButton class="mb10" btnText="Component button (secondary color)" secondary />
</view>

<! -- add template function -->
<wxs module="_" src=".. /.. /wxs/vi.wxs"></wxs>
Copy the code
  • ComponentDynamic skin of components
<view class="btn" style="{{_.s(vi, 'background', secondary)}}">{{ btnText }}</view>

<! -- Template function -->
<wxs module="_" src=".. /.. /wxs/vi.wxs" />
Copy the code

Compare the realization of the skin changing function with the traditional inline method:

<view style="color: {{ mainColor }}; background: {{ background }}">Vi color fonts</view>
Copy the code

If you add complex logical code later, it will drive developers crazy to read it later;

Of course, the solution of this article only simplifies the writing of inline code to a certain extent, and the principle is the injection of inline style;

At present, I have an idea that the best way is to dynamically modify the variables of stylus and change the theme style after obtaining the interface theme style field through the variable mechanism of precompiled languages.

Results the preview

  1. The interface responds quicklyViModelValues change skin

  1. The interface responds slowlyobserverThe listener callback takes the value to peel

The ultimate article

review

Earlier, I wrote two articles about how to realize the skin change function in wechat small program, the link is posted below, those who have not seen it can have a look first

  1. Small program dynamic skin solution – local

  2. Small program dynamic skin solution – interface

But the above two schemes have shortcomings, so I also noted in the end of the article will be the final chapter of the solution, delayed some time, today I saw someone cue me to say when the final chapter, so today, I spent the writing time to sort out, I hope to help you.

plan

In fact, the solution provided in this article is more of an optimized version of the interface article.

The solution is:

Dynamically set the skin-value property acquired by the interface to a property of the element to be skinned, essentially replacing the element’s CSS property value by embedding pre-set CSS variables into the js files of the current Page and Component objects. Then the setData method is used to echo back to the corresponding WXML file.

  1. Using CSS variables instead of the original inline modification style;

  2. Behavior, a native mixin solution provided by small programs, is invasive to pages and component objects, but it can greatly reduce the writing of repeated code.

code

1. Listener module

We know that the interface returns asynchronous data, so when we go inside the specified Page and Component objects, we may need to register a listener function, wait until the skin interface request succeeds, and then execute the skin set operation.

// observer.js

function Observer() {
  this.actions = new Map()}// Listen on events
Observer.prototype.addNotice = function(key, action) {
  // Because the same Page or Component object can import multiple components
  // These components all use the same listener, and each listener's callback needs to be handled separately
  // Therefore, the result is: 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])
  }
}

// Delete the listening event
Observer.prototype.removeNotice = function(key) {
  this.actions.delete(key)
}

// Send events
Observer.prototype.postNotice = function(key, params) {
  if (this.actions.has(key)) {
    const handlers = this.actions.get(key)
    // The skin interface gets the data successfully
    handlers.forEach(handler= > handler(params))
  }
}


module.exports = new Observer()
Copy the code

2. Skin object model module

Because the skin interface is only executed when the program is first loaded and run, in other words, setting up the skin through publish-subscribe only happens after the first successful interface request. Therefore, we need to store the data through a Model object from which all subsequent skin set operations are fetched;

// viModel.js

/ * * *@param {*} MainColor mainColor value *@param {*} SubColor Secondary color value *@param {*} Reset reset * /

function ViModel(mainColor, subColor, reset = false) {
  // If the current instance is already set, the instance is returned directly
  if (typeof ViModel.instance == 'object' && !reset) {
    return ViModel.instance
  }

  this.mainColor = mainColor
  this.subColor = subColor
  // The instance assignment action is triggered when the interface has data returned
  if (this.mainColor || this.subColor) {
    ViModel.instance = this
  }
  return this
}

module.exports = {
  // To assign a value using the save method, reset the object with reset = true
  save: function(mainColor = ' ', subColor = ' ') {
    return new ViModel(mainColor, subColor, true)},// Directly return all singleton instances that already have values
  get: function() {
    return new ViModel()
  }
}
Copy the code

3. Mixin module — Behavior

This is the most important module to share — injecting the CSS variable for themeStyle

Let’s go straight to this code:

setThemeStyle({ mainColor, subColor }) {
  this.setData({
    themeStyle: `
      --main-color: ${mainColor};
      --sub-color: ${subColor};
    `})}Copy the code

At this point, you should have guessed the implementation principle from the beginning

The themeStyle here is the data property that we will inject into the Page and Component next, which is the dynamic CSS variable property that needs to be set in the Page and Component

//skinBehavior.js

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


module.exports = Behavior({
  data: {
    themeStyle: null
  },

  attached() {
    // 1. If the interface response is too long, create a listener
    observer.addNotice('kNoticeVi'.function(res) {
      this.setThemeStyle(res)
    }.bind(this))

    // 2. If the interface responds quickly and modal has a value, it is directly assigned to perform skin change
    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};
        `})},}})Copy the code

4. 【 用】 — Component module

  • The js file imports skinbehavior. js, which is injected with the Behaviors attribute provided by the Component object.

  • WXML file root node set style=”{{themeStyle}}”, set CSS variable value;

  • Background: var(–main-color, #0366d6);

// wxButton2.js

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

Component({
  behaviors: [skinBehavior],

  properties: {
    // Button text
    btnText: {
      type: String.value: ' '
    },

    // Whether it is an auxiliary button, change the auxiliary color skin
    secondary: {
      type: Boolean.value: false}}})Copy the code
<! -- wxButton2.wxml -->

<view class="btn-default btn {{secondary ? 'btn-secondary' : ''}}" style="{{themeStyle}}">{{ btnText }}</view>
Copy the code
/* 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);
}
Copy the code

5. 【 application 】 — Page module

Use the same method as the Component module.

// skin.js
const skinBehavior = require('.. /.. /js/skinBehavior');

Page({
  behaviors: [skinBehavior],

  onLoad() {
    console.log(this.data)
  }
})
Copy the code
<! --skin.wxml-->
<view class="page" style="{{themeStyle}}">The ultimate skin change<view class="body">
    <wxButton2 class="skinBtn" btnText="Button 1"></wxButton2>
    <wxButton2 class="skinBtn"btnText="Button 2" secondary></wxButton2>
    <wxButton2 class="skinBtn" btnText="Button 2" ></wxButton2>
  </view>
</view>
Copy the code
/* skin.wxss */
.page {
  padding: 20px;
  color: var(--main-color);
}

.skinBtn {
  margin-top: 10px;
  float: left;
}
Copy the code

6. [Initialization] — Interface call

This is to initialize the skin by calling the skin request interface in the startup file app.js of the applet

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

App({
  onLaunch: function () {
    // The page starts, requesting the interface
    getSkinSettings().catch(err= > {
      console.log(err)
    })
  }
})

Copy the code

Results show

conclusion

At present, [Final] is undoubtedly the best solution for dynamic skin of small program, but I also hope to tell you that the development of a function is strongly dependent on business needs, that is to say, we should choose the appropriate technical solution according to business, in addition to meeting the needs of the business side, It can provide more and better optimization ideas and directions for the current function scalability to the business side, which also provides reliability for the continuous iteration of the product.

The project address

Project address: github.com/csonchen/wx…

This is the project address of this case, in order to facilitate you to browse the project, I have also uploaded the compiled WXSS file, you can open the preview, the code word is not easy, if you feel good, I hope you all go to click the star ha, thank you…