Perfect solution of topic switching based on CSS variable (recommended)

Time:2021-3-15

 

When receiving this demand, there are still a lot of schemes from Baidu to the industry about topic switching, such as CSS link replacement, classname change, etc less.modifyVars But every solution sounds tired and expensive. Is there a low code intrusion, brainless and easy to maintain solution? Of course, there are. To be exact, CSS itself supports it.

Css3 Variable

Define a global color variable, change the value of the variable, all the elements in the page that refer to the variable will be changed. It’s easy, isn’t it?


// base.less
:root {
  --primary: green;
  --warning: yellow;
  --info: white;
  --danger: red;
}

// var.less
@primary: var(--primary)
@danger: var(--danger)
@info: var(--info)

// page.less
.header {
  background-color: @primary;
  color: @info;
}
.content {
  border: 1px solid @danger;
}

// change.js
function changeTheme(themeObj) {
  const vars = Object.keys(themeObj).map(key => `--${key}:${themeObj[key]}`).join(';')
  document.documentElement.setAttribute('style', vars)
}

The end of this article

A P, it does not supportIEAh!! Is ie compatible in 0202? Yes, it is compatible with ie.

 

css vars ponyfill

Yes, there is really a Polyfill that is compatible with ie: CSS vars ponyfill. The way it handles ie is like this

+————————-+
|Get the content of style label in the page|
|Request external chain CSS content|
+————————-+
  |
  |
  v
+————————-+Yes+————————-+
|Does the content contain var () | – > | marked as SRC|
+————————-+       +————————-+
  |                                 |
| no|
  v                                 v
+————————-+       +————————-+
|Mark as skip |, replace var (*) with variable value|
||| add style tag to head|
+————————-+       +————————-+

The effect is like this

 

It’s simple, rude and elegant. It won’t be handled in browsers that support CSS VaR, so there’s no need to worry about performance issues(It’s about ie, not me

)。 Let’s transform the code

// store/theme.js
import cssVars from 'css-vars-ponyfill'

export default {
  state: {
    'primary': 'green',
    'danger': 'white'
  },
  mutations: {
    UPDATE_THEME(state, payload) {
      const variables = {}
      Object.assign(state, payload)
      Object.keys(state).forEach((key) => {
        variables[`--${key}`] = state[key]
      })
      cssVars({
        variables
      })
    }
  },
  actions: {
    changeTheme({ commit }, theme = {}) {
      commit('UPDATE_THEME', theme)
    }
  }
}

// router.js
//Because the page after route jump will load new CSS resources on demand and convert again
const convertedPages = new Set()
router.afterEach((to) => {
  if (convertedPages.has(to.path)) return
  convertedPages.add(to.path)
  context.store.dispatch('theme/changeTheme')
})

Optimization of flashing problem in SSR project

If you use the above scheme in SSR project, you may see this situation in ie

 

becausecss-vars-ponyfill

It depends on DOM element to realize the conversion, which can’t be used in node, so there is a style gap between the unconverted CSS code from server to JS file from client.

+- – – – – – – – – – – – – – – – – – – -+
‘style empty window period: ‘
                 ‘                                       ‘
+———-+     ‘ +—————-+     +————+ ‘     +————-+
|Initiate request | – > ‘| – SSR straight out page | – > – load JS dependency |’ – > – replace CSS variable|
+———-+     ‘ +—————-+     +————+ ‘     +————-+
                 ‘                                       ‘
                 +- – – – – – – – – – – – – – – – – – – -+

To solve this problem is also very simple, only need to be used in eachcss varAdd a compatible notation to where

@_primary: red
@primary: var(--primary)

:root{
  --primary: @_primary
}

.theme {
  color: @primary;
}

//Change to
.theme {
  color: @_primary;
  color: @primary;
}

Default colors are rendered on browsers that do not support CSS varredAfter JS is loaded, ponyfill replaces the style overlay.

Development of webpack plug-in

It’s hard to manually add a compatible writing method to each place where it’s used. At this time, we need to know some knowledge about the webpack life cycle and plug-in development. We can write a webpack plug-in by handnormalModuleLoader ( The V5 version has been abandoned and used NormalModule.getCompilationHooks (compilation).loader)Add a loader for all CSS modules to handle compatible code.

The author uses less in the project. Note that the order of loader execution in webpack isSimilar stack of first in, last outSo I need to make sure that we are dealing with the compiled CSS var instead of the less variable before adding the transformation loader to the less loader.

// plugin.js
export default class HackCss {
  constructor (theme = {}) {
    this.themeVars = theme
  }

  apply(compiler) {
        compiler.hooks.thisCompilation.tap('HackCss', (compilation) => {
          compilation.hooks.normalModuleLoader.tap(
            'HackCss',
            (_, moduleContext) => {
              if (/\.vue\?vue&type=style/.test(moduleContext.userRequest)) {
                //SSR project isomorphism will have two compilers. If there is a loader in the module, it will not continue to add
                if (hasLoader(moduleContext.loaders, 'hackcss-loader.js')) {
                  return
                }

                let lessLoaderIndex = 0
                //The project uses less. Find the location of less loader
                moduleContext.loaders.forEach((loader, index) => {
                  if (/less-loader/.test(loader.loader)) {
                    lessLoaderIndex = index
                  }
                })
  
                moduleContext.loaders.splice(lessLoaderIndex, 0, {
                  loader: path.resolve(__dirname, 'hackcss-loader.js'),
                  options: this.themeVars
                })
              }
            }
          )
        })
      }
    })
}

// loader.js
const { getOptions } = require('loader-utils')

module.exports = function(source) {
  if (/module\.exports/.test(source)) return source
  const theme = getOptions(this) || {}
  return source.replace(
    /\n(.+)?var\(--(.+)?\)(.+)?;/g,
    (content, before, name, after = '') => {
      const [key, indent] = before.split(':')
      const add = after.split(';')[0]
      return `\n${key}:${indent}${theme[name]}${after}${add};${content}`
    }
  )
}

So far, we can switch themes happily and freely.

 

Postscript

It will be more interesting to absorb new knowledge by “lazy to write more code”. I hope this article can help you.

This article about topic switching perfect solution based on CSS variable (recommended) is introduced here. For more topic switching content related to CSS variable, please search previous articles of developer or continue to browse the following related articles. I hope you can support developer more in the future!