Recently, the company needs to add the multi-skin function to several WebApps (about 20+). The default is white skin, so we will start with the dark mode and gradually realize the multi-skin function. This article records the implementation idea.

Avi scheme

css variables

CSS variables is a Web standard implementation of dark mode support, the following code through CSS media query, the simplest implementation.

:root {
    color-scheme: light dark;
    background: white;
    color: black;
}

@media (prefers-color-scheme: dark) {
    :root {
        background: black;
        color: white; }}Copy the code

Use CSS variables when there are many colors

:root {
    color-scheme: light dark;
    --nav-bg-color: #F7F7F7;
    --content-bg-color: #FFFFFF;
    --font-color: rgba(0.0.0.9);
}

@media (prefers-color-scheme: dark) {
    :root{-nav-bg-color: #2F2F2F;
        --content-bg-color: #2C2C2C;
        --font-color: rgba(255.255.255.8); }}:root {
    color: var(--font-color)
}

.header {
    background-color: var(--nav-bg-color);
}

.content {
    background-color: var(--content-bg-color);
}
Copy the code

Advantages: the least amount of code, easy to implement;

Disadvantages: browser compatibility, edge16+ support, old project implementation, need to rebuild CSS, so it is not suitable for our company, if it is a new WebApp, I will not hesitate to choose this way.

Less online compilation

The most typical example of this scheme is antdtheme.com/, which enables runtime modifications to fewer variables through the less modifyVars method. When called with the new value, fewer files are recompiled without reloading.

<script src="less.js"></script>
<script>
less.modifyVars({ '@text-color': '#fff'.'@bg-color': '# 000' });
</script>
Copy the code

If you have too many color variables to change, or too many style files, you can get stuck switching.

Pack multiple CSS files

You can also manually package 2 copies of CSS styles

var less = require("less"); var fs = require("fs"); fs.readFile("./index.less", "utf-8", (err, str) => { less.render( str, { paths: [".", "./ Componnents "], // Specify search path for @import directive compress: true, // Compress modifyVars: {"@text-color": "# FFF ", "@bg-color": "#000", }, }, function (e, output) { console.log(output.css); }); });Copy the code

You can then skin it by dynamically inserting CSS

function changeTheme(theme) {
    const styleCss = document.querySelector("#styleCss");
    if (styleCss) {
        styleCss.href = `/assets/css/${theme}.css`;
    } else {
        const head = document.getElementsByTagName("head") [0];
        const link = document.createElement("link");
        link.id = "styleCss";
        link.type = "text/css";
        link.rel = "stylesheet";
        link.dataset.type = "theme";
        link.href = `/assets/css/${theme}.css`;
        head.appendChild(link);   
    }
    localStorage.setItem("theme", theme);
}
Copy the code

One problem with this approach is that clicking on the switch causes the entire page to be rearranged, so we need to pack a separate style file that contains only colors. From this point of view, we get to PostCSS.

PostCSS

The PostCSS core contains a parser that generates a CSS AST (Abstract Syntax Tree), which is a representation of the tree of nodes that parses the CSS string. When we modify something in the CSS abstract syntax tree, PostCSS generates the syntax tree (AST) back to the CSS string.

The core is compile -> transform -> generate is not similar to Babel?

As you know, astexplorer.net/ is a website that can be used to write Babel plugins. I’m going to select CSS and PostCSS so that I can parse CSS into A CSS AST.

purpose

Currently I have one less style and two color variables, I need to generate the following style:

This way I can add and remove dark styles to the HTML and nodes to achieve peels.

Maybe some students will ask, how did it suddenly become less? Can PostCSS parse Less? The answer is no. For now, assume that our WebApp is built on WebPack.

module: {
    rules: [/ /...
        {
           test: /\.less$/i,
           use: ['style-loader'.'css-loader'.'postcss-loader'.'less-loader'],},/ /...]}Copy the code

The preceding loaders are executed in the sequence from right to left 👈. After less-loader processing, less becomes CSS. Therefore, postCSS-plugin can be used for any CSS preprocessor.

Start writing a PostCSS plug-in

We can create a PostCSS-plugin using the scaffolding postCSs-plugin-Boilerplate, which is also configured with jEST unit tests. A PostCSS-Plugin project can be created with a few simple commands.

Of course, we can create a JS file directly under the project directory

// test-plugin.js
var postcss = require("postcss");

module.exports = postcss.plugin("pluginname".function (opts) {
  opts = opts || {}; / / the plugin parameters
  return function (root, result) {
    // Root is the converted CSS AST
  };
});

Copy the code

Then import it in postcss.config.js

module.exports = {
  plugins: [
    require('./test-plugin'),
    require('autoprefixer')]};Copy the code

PostCSS plugin Hello world

Write a plug-in that reverses CSS property values

var postcss = require("postcss");

module.exports = postcss.plugin("postcss-backwards".function (opts) {
  opts = opts || {};
  return function (root, result) {
    // Iterate over all style nodes
    root.walkDecls((declaration) = > {
      declaration.value = declaration.value.split("").reverse().join("");
    });
  };
});
Copy the code

Of course, this plugin, it doesn’t make any sense, we’re just going to learn how to write a PostCSS plugin, right

Multi-skin plug-in

use

JS entry brings in 2 style files

import "./default-theme.less";
import "./dark-theme.less";
Copy the code

component.less

.box{
  width: 100px;
  height: 100px;
  border: 1px solid @border;
  background-color: @bg;
  color: @color;
}
Copy the code

default-theme.less

@import "./component";

@border: #333;
@color: #000;
@bg: #fff;
Copy the code

dark-theme.less

@import "./component";

@border: #999;
@color: #fff;
@bg: #000;
Copy the code

Generate CSS

.box {
  width: 100px;
  height: 100px;
  border: 1px solid # 333;
  background-color: #fff;
  color: # 000;
}
.dark .box {
  border: 1px solid # 999;
  background-color: # 000;
  color: #fff;
}
Copy the code

The source code

function isEmpty(arr) {
  return Array.isArray(arr) && arr.length === 0;
}

const hasColorProp = (colorProps, declProp) = >
  colorProps.some((prop) = > declProp.includes(prop));

module.exports = (opts = {}) = > {
  if(! opts.colorProps) { opts.colorProps = ["color"."background"."border"."box-shadow"."stroke"];
  }
  return (root) = > {
    let theme;
    const file = root.source.input.file || "";

    const matched = file.match(
      / (? 
      
       [a-zA-Z0-9]+)-theme.(less|css|scss|sass)/
      
    );
    if(matched && matched.groups.theme ! = ="default") {
      theme = matched.groups.theme;
    } else {
      if (process.env.NODE_ENV == "test") {
        theme = "test"; }}if (theme) {
      root.walkRules((rule) = > {
        rule.walkDecls((decl) = > {
          if (!hasColorProp(opts.colorProps, decl.prop)) {
            decl.remove();
          }
        });

        if (isEmpty(rule.nodes)) {
          rule.remove();
        } else {
          rule.selector = rule.selector
            .replace(/\n/g."")
            .split(",")
            .map((s) = > `.${theme} ${s}`)
            .join(",\n"); }}); }}; };Copy the code

implementation

1. Determine if a skin style needs to be generated based on the file name

const file = root.source.input.file || "";

const matched = file.match(
  / (? 
      
       [a-zA-Z0-9]+)-theme.(less|css|scss|sass)/
      
);
Copy the code

2, delete the style that does not contain color, keep the style that contains color such as border-color background-color

["color", "background","border","box-shadow","stroke",]

3. If there are no CSS properties in the CSS selector, delete the selector

4. Add the.theme style name in front of the CSS selector

Upgrading of old projects

The original project may not have separated color variables into separate style files, and may have written absolute color values in styles.

Can we write a tool to help us upgrade?

Can 20+ projects be automatically converted by writing a tool?

Postcss-less converts less to AST, and then we can configure rules to replace colors with variables

Configuration rules

module.exports = [
  {
    prop: ["background-color"."background"].from: ["#fff"."#ffffff"."@white"].to: "@component-background"}, {prop: ["border"."border-color"].from: ["#D3D9E4"."#D3D9E2"].to: "@border-color"}, {prop: ["color"].from: ["#666E79"."#5C6268"].to: "@text-color",}];Copy the code

conversion

const syntax = require("postcss-less");
var fs = require("fs");
const path = require("path");
const rules = require("./rule.js");

var glob = require("glob");

function log(file, node, to) {
  console.log(
    "\x1b[32m".`convert ${file} ${node.source.start.line}:${node.source.start.column}  ${node.parent.selector} ${node.prop} from ${node.value} to ${to}`
  );
}

let codes = {};

// options is optional
glob("./src/**/*.less".function (er, files) {  
  files.forEach((file) = > {
    var ast = syntax.parse(file);
    
    // traverse AST and modify it
    ast.walkDecls(function (node) {
        rules.forEach((item) = > {
          if(item.prop.includes(node.prop) && item.from.includes(node.value)) { node.value = item.to; log(file, node, item.to); }}); }); fs.writeFileSync(path.resolve(file), syntax.nodeToString(ast)); }); });Copy the code

The main steps

1. Read all less files from glob

2. Run postCSs-less to convert less to AST

3. Iterate over all CSS properties and replace the judgment with the less variable in the rule

4. Convert to less to write files

Of course, the above code is the simplest, and there are many styles not covered

For example, “border” can be “border-color” and so on

Search for missing colors with VSCODE re

When the above rules can not cover all projects, developers can input in VSCODE regular ((# [a – fA – F0-9] {3}) | (# [a – fA – F0-9] {6}) | ^ RGB) to find out in the code color, again one by one to extract into less variable.

summary

1. This article summarizes several common ways of front-end skin peening. Through the comparison, it is found that using PostCSS to generate skin styles is the most convenient in our project, and it is also the easiest for your website to support dark mode. I opened the postCSs-multiple-themes plugin to Github and published the NPM package

2. Think about how to replace CSS colors into variables in old projects through PostCSS. When there are more projects, labor costs are also saved to a certain extent.

The last

If you are also doing skin changing work for WebApp and are troubled by the problem of multiple skin in the front end, I hope this article will be helpful to you. You can also refer to my previous articles or share your thoughts and experiences in the comments section. Welcome to explore the front end together.