Source address: codebase.byted.org/repo/lark/d…

background

  1. You want to switch between dark and light modes using CSS variables
  2. The original project is CSS defined in less form, and there are functions of Less, such as fade. I don’t want to change the functions of less manually, and HOPE that the plug-in can support parsing of less functions
  3. You need to support local unswitch modes, such as light mode where an area is fixed

steps

Step 1: Convert less variables to CSS variables

This step is relatively simple, less already provides fields for the transformation, and you just need to add a configuration item, the globalVars attribute.

See the Example code reference

{
    loader: 'less-loader'.options: {
      globalVars: LessGlobalCSSVars,
    }
}
Copy the code

LessGlobalCSSVars look something like this

{
    "bg-body": "var(--bg-body)"."static-white": "var(--static-white)". }Copy the code

Less appends the LessGlobalCSSVars mapping to the LESS file and replaces it with the corresponding CSS variables during variable look-ups

For example, the less file below

div {
    color: @bg-body;
}

Copy the code

The actual file contents parsed by less are

bg-body: "var(--bg-body)";
static-white: "var(--static-white)"
div {
    color: @bg-body;
}

Copy the code

Finally, the file above is compiled to

div { color: var(--bg-body); }Copy the code

Step 2: How to parse less function? 【 Less plug-in 】

But there is still the question of how to parse less functions. For example, the fade(‘ @bg-body, 20% ‘) will throw an exception if nothing is done, because var(–bg-body) is not a node type that less can parse, Var (–bg-body) cannot be converted to Color (a node type of less). This is the parse tree of LESS. The first parameter of the fade function needs to be parsed to Color node type, otherwise an exception will be thrown. Therefore, we need to rewrite the less function, specifically through the way of less plug-in. Modify the configuration of less-loader as follows, and add a plug-in.

{
    loader: 'less-loader'.options: {
      globalVars: LessGlobalCSSVars,
      plugins: [
        new LessSkipVarsPlugin ()
      ]
    }
}
Copy the code

Less all functions will be registered with the [functions provides] (HTTP: / / https://github.com/less/less.js/blob/master/packages/less/src/less/functions/index.js “Functions “), the plug-in exposes the functions so that function coverage can be achieved by modifying the less function of the response. Functions object is a mapping from function name to function body, so we can reset the functions we need to rewrite to our own. The calculation results of the function through calc and VAR two functions and CSS variables are expressed in the page according to the REAL-TIME calculation of CSS variables!

The following are just two of the functions we support — fade and Darken. Fade is represented by rGBA function, while Darken is represented by HSL function, mainly because rGBA representation cannot be represented by CSS supported functions. So we use the HSL function. Here you can see that we need some special CSS variables, such as — bg-body-sa, –bg-body-raw, — bg-hs, and — bg-body-l. So we need to use the original color value (BG-body) for conversion

(The following code is omitted, mainly for illustration, the specific code can see the source code)

class LessSkipVarsPlugin {
    install(less, pluginManager, functions) {
        functions.add('fade'.function (color, percent) {
            if (color.type === 'Call') {
              if (color.name === 'var') {
                const key = color.args[0].value.substring(2);
                return `rgba(var(--${key}-raw), calc(var(--${key}-SA) * ${parseLessNumber(percent)})) `; }}...return `rgba(${red}.${green}.${blue}.${alpha * parseLessNumber(percent)}) `;
        });
        functions.add('darken'.function (color, amount, method) {...if(color.type ! = ='Color')
              throw new Error(`fade function parameter type error: except Color, get ${color.type}`);
            const hsl = (new Color(color.rgb, color.alpha)).toHSL();
            if (typeofmethod ! = ='undefined' && method.value === 'relative') {
                hsl.l = hsl.l * (1 - parseLessNumber(amount));
            } else {
                hsl.l = hsl.l - parseLessNumber(amount);
            }
            return `hsl(${hsl.h}.${hsl.s}.${hsl.l}) `; }}})Copy the code

Step 3: How is partial light mode supported? Postcss plugin

Click here to quickly locate the source code. A classname prefix can be added to a DOM node to indicate that the styles under the DOM are in static brightly-colored mode and do not switch with the theme. There are three main steps to do here:

  1. Step 1: Add the dom prefix className

Add a classname prefix to the corresponding DOM node, such as static-light;

// The original DOM structure
<div className="test">aaa</div>
// New DOM structure
<div className="static-light">
    <div className="test">aaa</div>
</div>
Copy the code
  1. Step 2: [Append prefix style]

    1. When generating styles, prefix all styles with static-light via postCSS;

This step actually adds a postCSS plug-in to the CSS-loader process to generate an additional static style for each rule.

For example, I define the following less style

.test {
    background-color: @static-white;
}
Copy the code

After passing through the PostCSS plug-in, the resulting artifact becomes

.test {
    background-color: var(--static-white);
}
.static-light .test {
    background-color: var(--static-white);
}
Copy the code

So how do you append this CSS?

You can see the source code of CSS-Loader, the nodes are processed by the PostCSS plug-in, we just need to add our plug-in to the plug-in list

result = await postcss([...plugins, new colorPlugin({
  staticEx: {prefix:'.static-light'}, })]).process(content, {... });Copy the code

So the next step is to implement our PostCSS plug-in

var postcss = require('postcss');
module.exports = postcss.plugin('postcss-color-and-function'.function (options) {
    const { staticEx } = options;
    function processNode(node, type) {
        let staticNode;
        switch (node.type) {
            ......
            case 'rule':
                staticNode =  node.clone();
                staticNode.selectors = staticNode.selectors.map(i= > {
                  return `${options.staticEx.prefix} ${i}`
                });
                break;
            default:
                break;
        }
        return staticNode;
    }
    return function (css) {
        let last = [];
        css.each((node, type) = > {
            const staticNode = processNode(node, type);
            if(staticNode) { last.push(staticNode); }}); css.nodes = css.nodes.concat(last); }; });Copy the code

The main ideas are as follows:

  • Clone an identical node from the current node, and concatenate the node at the end of the return, thus generating two styles;
  • For the cloned node, append the selector,
staticNode.selectors = staticNode.selectors.map(i= > {
  return `${options.staticEx.prefix} ${i}`
});
Copy the code

This allows you to append nodes and local CSS variable definitions.

Note: CSS-loader validates parameters, so if you need to change the format of the parameters passed in, you need to change options.json and normalizeOptions.

  1. Step 3: Insert a set of CSS var variables that specify classname (in this case static-light).

We use the webPack plugin to do this. See the next section for more details

Step 4: Append global CSS variable definitions

The CSS variables can be changed by adding @media (darkcolor scheme: dark). The CSS variables can be changed by adding @media (darkcolor scheme: dark).

:root {
 --bg-body: "#1f1f1f";
 --static-white: '#fff'
}
@media (prefers-color-scheme: dark) {
    :root {
     --bg-body: "#2f2f2f";
     --static-white: '#fff'}}Copy the code

In the previous section, we also need to append the CSS variable corresponding to the local light style. We need to append the following code to the above variable.

:root {
 --bg-body: "#1f1f1f";
 --static-white: '#fff'
}
@media (prefers-color-scheme: dark) {
    :root {
     --bg-body: "#2f2f2f";
     --static-white: '#fff'}}.static-light {
 --bg-body: "#1f1f1f";
 --static-white: '#fff'
}
Copy the code

This makes it complicated and error prone to append variables manually, so we can use the WebPack plugin to append variables. Webpack provides various hooks that we can use to execute logic at the appropriate time.

  1. Step 1: [Generate CSS file]

    1. AlterAssetTags, the lifecycle hook function provided by HtmlWebpackPlugin, returns a list of all the current resources. Users can append links to these resources. So we can trigger the build file at this lifecycle hook.
compiler.hooks.compilation.tap('LarkThemePlugin'.compilation= > {
    HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync('LarkThemePlugin'.(data, cb) = > {
        const source = xxx;
        compilation.assets['theme.css'] = { source: () = > source, size: () = > Buffer.byteLength(source, 'utf-8')};
      cb(null,data); })})Copy the code
  1. Step 2: Generate link tag references:

    1. In the previous step, CSS resource files were generated. We need to add a link tag to the HTML to reference the CSS resource. In practical applications, we often have many resource tags inserted into the HTML, and we want the tag to be inserted into all resource files before loading
compiler.hooks.compilation.tap('LarkThemePlugin'.compilation= > {
    HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync('LarkThemePlugin'.(data, cb) = > {
      const { assetTags: { styles }} = data;
      styles.unshift({
        tagName: 'link'.voidTag: true.attributes: {
          href: 'theme.css'.rel: 'stylesheet'
        }
      })
      cb(null,data); })})Copy the code

The complete WebPack plug-in code is below:

const fs = require('fs');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { getRootCSSVarMap } = require('.. /util');
class InjectThemeWebpackPlugin {
    constructor({ lessVarsSet, darkTokens, lightTokens }) {
      this.darkTokens = darkTokens;
      this.lightTokens = lightTokens;
    }
    // Generate CSS styles for CSS variables
    generateResult() {
      const generateCss = (cssObj) = > {
        let css = ' ';
        for (let key in cssObj) {
            const value = cssObj[key];
            css += `${key}: ${value}; `
        }
        return `:root{${css}} `;
      }
      const darkCSSObj = getRootCSSVarMap(this.darkTokens, 'DARK');
      const lightCSSObj = getRootCSSVarMap(this.lightTokens, 'LIGHT');
      return `${generateCss(lightCSSObj)}\n@media (prefers-color-scheme: dark) {${generateCss(darkCSSObj)}} `;
    }

    apply(compiler) {
      // Append link labels
      compiler.hooks.compilation.tap('LarkThemePlugin'.compilation= > {
        HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync('LarkThemePlugin'.(data, cb) = > {
          const source = this.generateResult();
          compilation.assets['theme.css'] = { source: () = > source, size: () = > Buffer.byteLength(source, 'utf-8')};
          const { assetTags: { styles }} = data;
          styles.unshift({
            tagName: 'link'.voidTag: true.attributes: {
              href: 'theme.css'.rel: 'stylesheet'
            }
          })
          cb(null,data); }}})})module.exports = InjectThemeWebpackPlugin;
Copy the code

Note: It is important to ensure that the HTML-webpack-plugin is always the same, otherwise the link tag cannot be appended

🧭 Technical point quick navigation:

This article involves the content of less plug-in, PostCSS plug-in, and Webpack plug-in, for friends who have no plug-in background will be a little strange, you can learn to understand the following content, convenient to complement the information of the use of each plug-in.

An example of the less plug-in

Less. Js pluginMananger source code

PostCSS common plug-ins and syntax introduction

PostCSS API documentation | PostCSS Chinese website

www.webpackjs.com/api/compile…

Life cycle hooks for htML-webpack-plugin plug-ins

The appendix

Github.com/webpack-con…

www.postcss.com.cn/api/#declar…