Perfect ant design dynamic skin changing scheme

Time:2021-10-22

preface

Preview address:Ant Design Theme

const App = () => {
  return (
    <ThemeProvider
        theme={{
            name: 'dark',
            variables: { 'primary-color': '#00ff00' },
        }}
    >
      <Button type="primary">Primary Button</Button>
    </ThemeProvider>
  );
};

Out of the boxantd-themeWelcome, star ❤

Defects of existing schemes

  • CSS variables

    • Complex expressions cannot be supportedmix(var(--primary-color), #fff, 20%)
    • Limited browser compatibility
  • Multiple sets of CSS themes

    • The theme style is fixed, and the theme color cannot be switched in real time
    • CSS code separation is not easy to handle
  • Less dynamic switching

    • Less runtime needs to be introduced, and the volume is large
    • During theme switching, parse – > eval – > gencss processing will be performed on the whole style sheet, resulting in slow switching speed

What you want to achieve

  • Provide interfaces to support real-time modification of parameters such as @ primary color and @ border radius base
  • After the browser page is refreshed, the default is the switched style, without redundant animation and obvious loading process
  • The packaging volume is small, and there is no need to package multiple style files
  • Code separation is friendly, and the styles of different pages can be loaded separately

Implementation process

During compilation, leave the less variables and expressions that need to be modified for different skins blank and fill them in at run time.

such as

.btn {
    background: @primary-color;
    &:active {
        background: mix(@primary-color, white, 10%);
    }
}

Will compile into

/* less-loader!./style.less */
.btn {
    background: "[theme:primaryColor,default:#1890ff]";
}
.btn:active {
    background: "[theme:e8efafb1,default:#40a9ff]";
}

This style cannot be loaded directly into the page, so it will be further processed into

/* themed-style-loader!./style.css */

var loadStyle = require('./load-themed-style').loadStyle;

var css = `
.btn {
    background: "[theme:primaryColor,default:#1890ff]";
}
.btn:active {
    background: "[theme:e8efafb1,default:#40a9ff]";
}
`;

loadStyle(css);

At the same time, the replacement variables of different skins will be compiled and injected into specific filesthemes.js

/* themes.js */
module.exports = {
    default: {
        primaryColor: '#1890ff',
        e8efafb1: '#40a9ff'
    },
    dark: {
        primaryColor: '#1890ee',
        e8efafb1: '#40a9ee'
    },
    compact: {
        primaryColor: '#1890dd',
        e8efafb1: '#40a9dd'
    }
}

The skin loader looks like this

/* load-themed-style.js */
var themes = require('./themes.js')
var styles = [];

var loadStyle = function(css) {
    styles.push(css);
    applyStyles();
}

var loadTheme = function(name) {
    applyStyles(themes[name]);
}

var applyStyles = function(variables) {
    var css = styles.join('').replace(
        /"\[theme:([\w]+),default:(\S+)\]"/,
        function(_, themeSlot, defaultValue){ 
            return varialbes && varialbes[themeSlot] || defaultValue;
        }
    );
    //The generated CSS is inserted into the page
}

module.exports = {
    loadStyle,
    loadTheme
}

Last callloadTheme('xx')You can switch to the corresponding skin

Real time modification of variables

If the plug-in analyzes that an expression depends on a variable that needs to be modified in real time, it will inject the ast corresponding to the expression into thethemes.jsinside

/* themes.js */

// @primary-color
var expr1 = {
    type: 'Variable',
    name: '@primary-color'
};

// mix(@primary-color, white, 10%)
var expr2 = {
    type: 'Call',
    name: 'mix',
    args: [
        {
            type: 'Variable',
            name: '@primary-color'
        },
        {
            type: 'Color',
            rgb: [255, 255, 255],
            alpha: 255,
        },
        {
            type: 'Dimension',
            value: 10,
            unit: '%'
        }
    ]
};

module.exports = {
    default: {
        background: 'white',
        primaryColor: { expr: expr1, default: '#1890ff' },
        e8efafb1: { expr: expr2, default: '#40a9ff' }
    },
    dark: {
        background: 'black',
        primaryColor: { expr: expr1, default: '#1890ff' },
        e8efafb1: { expr: expr2, default: '#1890ff' }
    },
    ...
}

loadThemeThe filling value will be calculated according to the incoming real-time variables and AST in the skin, the filling will be left blank and the modification will be applied

var loadTheme = function(name, runtimeVariables) {
    //Calculate the skin variable of this time according to the incoming real-time variable
    var themeVariables = compute(
        themes[name],
        runtimeVariables
    );
    //Apply style
    applyStyles(themeVariables);
}

Call nowloadTheme('xx', { 'primary-color': '#xxxxxx' })You can modify the main color of the page in real time

Problems encountered

  • Colorpalette function

ant-designInternal use~`colorPalette('@{background}', 7)`Inline JavaScript blocks make it impossible to track the variable dependency of expressions, so preprocess the style code before less parsing and replace all withcolorPalette(@background, 7)And provide the corresponding colorpalette function implementation.

  • Mixins Expansion & CSS guards transformation

The skinning scheme is based on CSS attribute replacement. Under all skins, the style generated by the same component needs the same number of lines and consistent expression hash.

// Mixin
.button-color(@color) {
    color: @color;
}

.btn-primary {
    &:active {
        // CSS Guard 1
        & when (@theme = dark) {
            .button-color(@primary-7);
        }
        
        // CSS Guard 2
        & when not (@theme = dark) {
            .button-color(~`colorPalette('@{btn-primary-bg}', 7)`);
        }
    }
}

The button style above will be generated under the default skin

.btn-primary:active {
    color: "[theme:primary7]";
}

Generated in dark mode

/* sha1(colorPalette(@btn-primary-bg, 7))= 9ebde6df87d1def7be1e8e5c80144b793cb1e2c2 */
.btn-primary:active {
    color: "[theme:9ebde6df]";
}

In this way, the runtime variable filling error will occur, so the ast after style resolution needs to be modified. Expand the mixin call first, and then convert the CSS guards. Before the AST is executed, the button style above will be converted to:

.btn-primary {
    &:active {
        color: if(@theme = darak, @primary-7);
        color: if(not @theme = dark, colorPalette(@btn-primary-bg, 7));
    }
}

The converted code can be handled in the above way happily. Multiple color definitions generated here will be deleted after getting the skin variables at runtime.

limit

  • The loop variable of recursive mixin call cannot be used as a skin variable, such asant-designGrid system related code@grid-columns

    .loop-grid-columns(@index, @class) when (@index > 0) {
      // ...
      [email protected]{ant-prefix}[email protected]{class}[email protected]{index} {
        order: @index;
      }
      .loop-grid-columns((@index - 1), @class);
    }
    
    .loop-grid-columns(@grid-columns, @class);
  • Postcss position incompatible

    Postcss position will directly the position attribute value in CSSvalue.match(/^static|absolute|fixed|relative.../).toString(),

    and'"[theme:position,default:relative]"'.match(...) === null

    Therefore, errors will be reported during compilation. The specific postcss position code is shown inhere

What’s Next

  • Support CSS variable backend configuration

    After opening CSS variable backend, the style file will be compiled into

    /* less-loader!./style.less */
    .btn {
        background: var(--primaryColor, #1890ff);
    }
    .btn:active {
        background: var(--e8efafb1, #40a9ff);
    }

    applyStyleThe internal implementation of is adjusted tostyle.setProperty(--primaryColor, '#xxxxxx')

  • requestIdleCallback

    useReact FiberSimilar schemes deal with the style rendering process asynchronously to avoid page jamming caused by too many styles synchronous rendering