Demand background

When using react + ant-Design or vue + Element-UI combinations or other frameworks, the client wants to add an online preset theme switch in the middle of the project or when development is complete, they have the following options:

  • Solution a: Use cSS3 Variables(consider browser support) to rearrange the less or sass Variables in the source code, modify CSS Variables online to achieve the switch effect. But the component library used a lot of less or SASS color function is only preprocessing ability does not support CSS variable compilation, need to do a lot of component style overlay processing, this is a lot of work;

  • Scheme 2: Preset multiple less or SASS variable files, use webpack or gulp and other building capabilities to compile all styles (including component libraries) into a total of multiple CSS files in advance, switch CSS files online to reach the target, However, it is necessary to adjust all the reference modes of less and Sass of the project, and it is also necessary to greatly adjust the construction environment. Styles are completely separated from JS, and it is more trouble to use CSS modules, and it is extremely unfriendly to modify debugging styles in development mode. You can’t compile less or sass on demand in a friendly way.

  • Scheme 3 :(unrealistic) using CSS in js scheme for page and component reconstruction;

  • Plan 4: If you need to use the color palette online to select any theme color switch, less can use the online compilation ability of less. Js (regardless of performance), if sASS needs the background service real-time compilation of SASS, but these are not very friendly to use CSS modules. It takes a lot of work to make changes in the original project;

  • Scheme 5: If ant-Design is used, antD-theme-webpack-plugin, ANTD-theme-Generator, UMi-plugin-ANTD-theme and so on can be selected, which is also limited to ANTD.

  • Plan 6: Using webpack-theme-color-replacer(vite version is the vite-plugin-theme), this method can choose any theme color switch, and does not require real-time compilation of background services, but it is a bit complicated to use. Component libraries such as ANTD-Design, which provide a JS method for theme colors to generate other gradient colors, are better able to see how the color gradient of the entire component library relates to the theme colors. Also, this only applies to color values. If you also need to include border-radius, font-size, and other non-color variable values, you may not be able to do this because the theme is not just the color part.

If none of the above schemes are suitable for you, you might as well read on

There is a simpler, more elegant, and friendlier solution for default multi-topic compilation that requires almost no source code modification and is independent of the framework component library, as long as it is based on less and Sass and not limited to webpack, gulp, Vite, etc., which is the focus of this article:

Default multi-topic compilation scheme based on LESS and SASS

Let’s start with a rendering

Packaged tools

  • Use @zougt/some-loader-utils getSass and getLess methods instead of less and sass compilers in the current build environment. Currently tested in Webpack and Vite.

  • To extract the compiled theme CSS into a separate file, see the webpack plugin @zougt/ theme-CSs-extract-webpack-plugin.

  • If you need the vite version, just need the vite plugin @zougt/vite-plugin-theme-preprocessor.

Multi-topic compilation example (using SASS + Webpack as an example)

In Webpack, you can simply configure the sas-Loader property implementation to view it directly@zougt/some-loader-utils

MultipleScopeVars will increase the compile time of the less/sass file as many times as there are theme variables. It is recommended to provide only one variable file in development mode. Provide a complete number of variable files for packaging when debugging is needed to switch themes or production mode.

webpack.config.js

const path = require("path");
// const sass = require("sass");
const { getSass } = require("@zougt/some-loader-utils");
const multipleScopeVars = [
  {
    scopeName: "theme-default".path: path.resolve("src/theme/default-vars.scss"),}, {scopeName: "theme-mauve".path: path.resolve("src/theme/mauve-vars.scss"),},];module.exports = {
  module: {
    rules: [{test: /\.scss$/i,
        loader: "sass-loader".options: {
          sassOptions: {
            // When getMultipleScopeVars is not used, multipleScopeVars can also be passed from here
            // multipleScopeVars
          },
          implementation: getSass({
            / / getMultipleScopeVars takes precedence over sassOptions multipleScopeVars
            getMultipleScopeVars: (sassOptions) = > multipleScopeVars,
            / / options
            // implementation:sass}),},},],},};Copy the code

The theme contains more than just the color part

Suppose there are currently SCSS variable files with two preset themes

//src/theme/default-vars.scss

/** * This SCSS variable file is automatically removed when it is compiled as multipleScopeVars! At the same time, the SCSS variable file serves as the default topic variable file, which is required by other. default */
$primary-color: #0081ff! default;$--border-radius-base: 4px! default;Copy the code
//src/theme/mauve-vars.scss

$primary-color: #9c26b0! default;$--border-radius-base: 8px! default;Copy the code

SCSS of one component

//src/components/Button/style.scss

@import ".. /.. /theme/default-vars";
.un-btn {
  position: relative;
  display: inline-block;
  font-weight: 400;
  white-space: nowrap;
  text-align: center;
  border: 1px solid transparent;
  background-color: $primary-color;
  border-radius: $--border-radius-base;
  .anticon {
    line-height: 1; }}Copy the code

After the compilation

src/components/Button/style.css

.un-btn {
  position: relative;
  display: inline-block;
  font-weight: 400;
  white-space: nowrap;
  text-align: center;
  border: 1px solid transparent;
}
.theme-default .un-btn {
    background-color: #0081ff;
    border-radius: 4px;
}
.theme-mauve .un-btn {
    background-color: #9c26b0;
    border-radius: 8px;
}
.un-btn .anticon {
  line-height: 1;
}
Copy the code

Changing the className toggle theme in HTML only applies to HTML tags:

<! DOCTYPEhtml>
<html lang="zh" class="theme-default">
  <head>
    <meta charset="utf-8" />
    <title>title</title>
  </head>
  <body>
    <div id="app"></div>
    <! -- built files will be auto injected -->
  </body>
</html>
Copy the code
document.documentElement.className = "theme-mauve";
Copy the code

Use @zougt/theme-css-extract-webpack-plugin to separate separate theme CSS files.

const toggleTheme = (scopeName = "theme-default") = > {
  let styleLink = document.getElementById("theme-link-tag");
  if (styleLink) {
    // If there is a link tag with id as theme-link-tag, modify its href directly
    styleLink.href = ` /${scopeName}.css`;
    // document.documentElement.className = scopeName;
  } else {
    // Create a new one if it does not exist
    styleLink = document.createElement("link");
    styleLink.type = "text/css";
    styleLink.rel = "stylesheet";
    styleLink.id = "theme-link-tag";
    styleLink.href = ` /${scopeName}.css`;
    // document.documentElement.className = scopeName;
    document.head.append(styleLink); }};Copy the code

Using Css Modules

If it is a modular SCSS, the resulting CSS looks like this:

.src-components-Button-style_theme-default-3CPvz
  .src-components-Button-style_un-btn-1n85E {
  background-color: #0081ff;
}
.src-components-Button-style_theme-mauve-3yajX
  .src-components-Button-style_un-btn-1n85E {
  background-color: #9c26b0;
}
Copy the code

The actual desired outcome should be this:

.theme-default .src-components-Button-style_un-btn-1n85E {
  background-color: #0081ff;
}
.theme-mauve .src-components-Button-style_un-btn-1n85E {
  background-color: #9c26b0;
}
Copy the code

For webpack, in webpack.config.js you need to add getLocalIdent to the modules property of CSS-loader (v4.0+) :

const path = require("path");
// const sass = require("sass");
const { getSass } = require("@zougt/some-loader-utils");
const { interpolateName } = require("loader-utils");
function normalizePath(file) {
  return path.sep === "\ \" ? file.replace(/\\/g."/") : file;
}
const multipleScopeVars = [
  {
    scopeName: "theme-default".path: path.resolve("src/theme/default-vars.scss"),}, {scopeName: "theme-mauve".path: path.resolve("src/theme/mauve-vars.scss"),},];module.exports = {
  module: {
    rules: [{test: /\.module.scss$/i,
        use: [
          {
            loader: "css-loader".options: {
              importLoaders: 1.modules: {
                localIdentName:
                  process.env.NODE_ENV === "production"
                    ? "[hash:base64:5]"
                    : "[path][name]_[local]-[hash:base64:5]".// Use getLocalIdent to customize the modular name, CSS-Loader V4.0 +
                getLocalIdent: (loaderContext, localIdentName, localName, options) = > {
                  if (
                    multipleScopeVars.some(
                      (item) = > item.scopeName === localName
                    )
                  ) {
                    //localName belongs to multipleScopeVars without modularization
                    return localName;
                  }
                  const { context, hashPrefix } = options;
                  const { resourcePath } = loaderContext;
                  const request = normalizePath(
                    path.relative(context, resourcePath)
                  );
                  // eslint-disable-next-line no-param-reassign
                  options.content = `${hashPrefix + request}\x00${localName}`;
                  const inname = interpolateName(
                    loaderContext,
                    localIdentName,
                    options
                  );

                  return inname.replace(/ \ \? \[local\\?] /gi, localName); },},},}, {loader: "sass-loader".options: {
              implementation: getSass({
                / / getMultipleScopeVars takes precedence over sassOptions multipleScopeVars
                getMultipleScopeVars: (sassOptions) = > multipleScopeVars,
                / / options
                // implementation:sass}),},},],},],},};Copy the code

Vue-cli created for use in projects

Recently used vuE-CLI4 to create development project (2021/8/6)


// In vue. Config. js configuration is simple, less version just need to change getSass to getLess use

const path = require('path');

const { getSass } = require('@zougt/some-loader-utils');

const ThemeCssExtractWebpackPlugin = require('@zougt/theme-css-extract-webpack-plugin');

const multipleScopeVars = [
    {
        scopeName: 'theme-mauve'.name: 'hibiscus'.path: 'src/scss/theme-mauve.scss'}, {scopeName: 'theme-cyan'.name: 'the azure'.path: 'src/scss/theme-cyan.scss'}, {scopeName: 'theme-default'.name: 'black'.path: 'src/scss/theme-default.scss',},];module.exports = {
    css: {
        loaderOptions: {
            scss: {
                // The options here are passed to sass-loader
                implementation: getSass({
                    / / getMultipleScopeVars takes precedence over sassOptions multipleScopeVars
                    getMultipleScopeVars: (sassOptions) = > multipleScopeVars.map((item) = > {
                        return { ...item, path: path.resolve(item.path) }; },},},},},chainWebpack: (config) = > {
        config
            .plugin('ThemeCssExtractWebpackPlugin')
            .use(ThemeCssExtractWebpackPlugin, [
                {
                    multipleScopeVars,
                    // extract: process.env.NODE_ENV === 'production',
                    extract: false,}]); }},Copy the code

For this purpose, after a period of research, the implementation of less, sASS (new, old) project preset theme compilation scheme, making such requirements become very simple, and compatibility is very good, here to share.