Home

Awesome

@zougt/some-loader-utils

提供了

让你轻松实现基于lesssass的 web 应用在线动态主题切换。

动态主题模式预设主题模式

特点:

demo repositories

动态主题模式

v1.4.0+支持

可用颜色板选择任意颜色切换相关的梯度颜色,这里以 scss 为例

one inline demo

one demo repository

效果图

在 webpack 中使用

# use npm or pnpm
npm install color@3.2.1 @zougt/some-loader-utils @zougt/theme-css-extract-webpack-plugin -D
# use yarn
yarn add color@3.2.1 @zougt/some-loader-utils @zougt/theme-css-extract-webpack-plugin -D

webpack.config.js

const path = require('path');

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

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

const multipleScopeVars = [
    {
        // 必需,任意名称
        scopeName: 'theme-vars',
        // path和varsContent选一个
        path: path.resolve('src/theme/theme-vars.scss'),
        // varsContent:`@--color-primary:#9c26b;`
    },
];

module.exports = {
    module: {
        rules: [
            {
                // 添加 setCustomTheme 的热更新loader
                test: /setCustomTheme\.js$/,
                enforce: 'pre',
                loader: require.resolve(
                    '@zougt/theme-css-extract-webpack-plugin/dist/hot-loader/index.js'
                ),
            },
            {
                test: /\.(scss|sass)$/i,
                // 请确保支持 implementation 属性的 sass-loader版本,webpack4 => sass-loader v10.x,webpack5 => sass-loader v12.x,请安装sass, 非 node-sass
                loader: 'sass-loader',
                options: {
                    implementation: getSass({
                        // getMultipleScopeVars优先于 sassOptions.multipleScopeVars
                        getMultipleScopeVars: (lessOptions) =>
                            multipleScopeVars,
                        // 可选项
                        // implementation:sass
                    }),
                },
            },
        ],
    },
    plugins: [
        new ThemeCssExtractWebpackPlugin({
            // 启用动态主题模式
            arbitraryMode: true,
            // 默认主题色,与"src/theme/theme-vars.scss"的@--color-primary主题色相同
            defaultPrimaryColor: '#512da7',
            multipleScopeVars,
            // 【注意】includeStyleWithColors作用: css中不是由主题色变量生成的颜色,也让它抽取到主题css内,可以提高权重
            includeStyleWithColors: [
                {
                    // color也可以是array,如 ["#ffffff","#000"]
                    color: '#ffffff',
                    // 排除属性,如 不提取背景色的#ffffff
                    // excludeCssProps:["background","background-color"]
                    // 排除选择器,如 不提取以下选择器的 #ffffff
                    // excludeSelectors: [
                    //   ".ant-btn-link:hover, .ant-btn-link:focus, .ant-btn-link:active",
                    // ],
                },
            ],
            // 是否在html默认添加主题的style标签
            InjectDefaultStyleTagToHtml: true,
            // setCustomTheme.js的一个依赖的生成路径,默认是 @zougt/theme-css-extract-webpack-plugin/dist/hot-loader/setCustomThemeContent.js
            customThemeOutputPath: '',
            // 调整色相值偏差,某些颜色值是由主题色通过mix等函数转化后,两者色相值不相等,无法确认是梯度颜色,可以调整low和high,允许偏差范围, 例如 hueDiffControls:{low: 2,high:2}
            // hueDiffControls: {
            //     low: 0,
            //     high: 0,
            // },
        }),
    ],
};

src/theme/theme-vars.scss

/*说明:此文件不应该被其他@import,此文件的变量并不是来设置项目的主题(当然,你可以作为加载时的默认主题),主要作用是,这里的变量值只要与项目的原变量值有差别,编译后就会抽取跟随主题色梯度变化的css*/

/*注意(重要):此文件的内容一旦固定下来就不需要改,在线动态切换主题,调用setCustomTheme方法即可*/

/*注意(强调):变量值改动会影响 gradientReplacer 和 targetValueReplacer 的可用属性的变化,所以内容一旦固定下来就不需要改(强调)*/

/*主题色,通常与  插件的 defaultPrimaryColor 相同, 使用setCustomTheme({primaryColor})切换*/

$--color-primary: #512da7;

/*与此颜色对应的样式,默认情况也会跟主色变化的,要切换它对应的梯度颜色,使用setCustomTheme({gradientReplacer:{"#F7D06B":"#F7D06B"}})切换 */
$--color-success: #f7d06b;

// /*圆角值,尽量与原值差别大一点,方便分析 targetValueReplacer 的可用属性,非颜色值的切换,可以使用 setCustomTheme({targetValueReplacer:{"6px"}}) 精准替换*/
// @border-radius-base:6px;

在线切换主题

动态主题切换必须使用的 "setCustomTheme" 模块,会自动处理项目中包括组件库涉及的梯度颜色替换

// color@4 使用了Numeric separators,如需良好兼容性应该安装 color@3
import Color from 'color';
// setCustomTheme的参数必须提供Color模块,至于为什么不把 Color 直接依赖进去是有原因的
import setCustomTheme from '@zougt/theme-css-extract-webpack-plugin/dist/setCustomTheme';
// 设置任意主题色既可
setCustomTheme({
    Color,
    primaryColor: '#FF005A',
    //gradientReplacer:{},
    //targetValueReplacer:{}
});

setCustomTheme 的可选参数 gradientReplacer 与 targetValueReplacer 的可用属性会跟随 .scss 内容变化的,所以整个项目动态主题的模型应该最开始固化下来

# npm run dev 之后
# 可以在终端使用 z-theme 命令查看  gradientReplacer 与 targetValueReplacer 的可用属性
npx z-theme inspect

预设主题模式

只预设多种可选主题,这里以less为例

one inline demo

one demo repository

效果图

在 webpack 中使用

# use npm or pnpm
npm install @zougt/some-loader-utils @zougt/theme-css-extract-webpack-plugin -D
# use yarn
yarn add @zougt/some-loader-utils @zougt/theme-css-extract-webpack-plugin -D

webpack.config.js

const path = require('path');
const webpack = require('webpack');

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

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

const multipleScopeVars = [
    {
        // 必需
        scopeName: 'theme-default',
        // path 和 varsContent 必选一个
        path: path.resolve('src/theme/theme-default.less'),
        // varsContent参数等效于 path文件的内容
        // varsContent:`@primary-color:${defaultPrimaryColor};`
    },

    {
        scopeName: 'theme-red',
        path: path.resolve('src/theme/theme-red.less'),
    },
];
const extract = process.env.NODE_ENV === 'production';
const publicPath = '/';
const assetsDir = 'assets';
const extractCssOutputDir = `${assetsDir}/css`;

module.exports = {
    output: {
        publicPath,
    },
    module: {
        rules: [
            {
                test: /\.less$/i,
                // webpack4 => less-loader v7.x , webpack5 => less-loader v10.x
                loader: 'less-loader',
                options: {
                    lessOptions: {
                        javascriptEnabled: true,
                    },
                    implementation: getLess({
                        // getMultipleScopeVars优先于 lessOptions.multipleScopeVars
                        getMultipleScopeVars: (lessOptions) =>
                            multipleScopeVars,
                        // 可选项
                        // implementation:less
                    }),
                },
            },
        ],
    },
    plugins: [
        // 添加参数到浏览器端
        new webpack.DefinePlugin({
            'env.themeConfig': {
                multipleScopeVars: JSON.stringify(multipleScopeVars),
                extract: JSON.stringify(extract),
                publicPath: JSON.stringify(publicPath),
                extractCssOutputDir: JSON.stringify(extractCssOutputDir),
            },
        }),

        new ThemeCssExtractWebpackPlugin({
            multipleScopeVars,
            // 【注意】includeStyleWithColors作用: css中不是由主题色变量生成的颜色,也让它抽取到主题css内,可以提高权重
            includeStyleWithColors: [
                {
                    // color也可以是array,如 ["#ffffff","#000"]
                    color: '#ffffff',
                    // 排除属性,如 不提取背景色的#ffffff
                    // excludeCssProps:["background","background-color"]
                    // 排除选择器,如 不提取以下选择器的 #ffffff
                    // excludeSelectors: [
                    //   ".ant-btn-link:hover, .ant-btn-link:focus, .ant-btn-link:active",
                    // ],
                },
                {
                    color: ['transparent', 'none'],
                },
            ],
            // 默认使用哪份主题,默认取 multipleScopeVars[0].scopeName
            defaultScopeName: '',
            // 在生产模式是否抽取独立的主题css文件,extract为true以下属性有效
            extract,
            // 独立主题css文件的输出路径
            outputDir: extractCssOutputDir,
            // 会选取defaultScopeName对应的主题css文件在html添加link
            themeLinkTagId: 'theme-link-tag',
            // 是否对抽取的css文件内对应scopeName的权重类名移除
            removeCssScopeName: false,
        }),
    ],
};

在线切换主题

预设主题切换,需要做的事情

1、开发时只需,html 标签的 calss 添加对应的 scopeName,移除上个 scopeName
2、打包后,如果开启 extract: true,需要切换对应的 link 标签的 href

可以选择使用如下封装好的方法

import { toggleTheme } from '@zougt/theme-css-extract-webpack-plugin/dist/toggleTheme';
// env.themeConfig 来源 (webpack.DefinePlugin)
const themeConfig = env.themeConfig;
toggleTheme({
    scopeName,
    multipleScopeVars: themeConfig.multipleScopeVars,
    extract: themeConfig.extract,
    publicPath: themeConfig.publicPath,
    outputDir: themeConfig.extractCssOutputDir,
    // customLinkHref: (href) => href,
    // themeLinkTagId: "theme-link-tag",
    // removeCssScopeName: false,
    // loading: {
    //   show: () => {},
    //   hide: () => {},
    // },
});

预设多主题编译原理示例(以 sass 为例)

主题包含的可能不只是颜色部分

//src/theme/default-vars.scss
/**
*此scss变量文件作为multipleScopeVars去编译时,会自动移除!default以达到变量提升
*同时此scss变量文件作为默认主题变量文件,被其他.scss通过 @import 时,必需 !default
*/
$primary-color: #0081ff !default;
$--border-radius-base: 4px !default;
//src/theme/mauve-vars.scss
$primary-color: #9c26b0 !default;
$--border-radius-base: 8px !default;
//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;
    }
}

编译之后

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;
}

html中改变 classname 切换主题,只作用于 html 标签 :

<!DOCTYPE html>
<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>
document.documentElement.className = 'theme-mauve';

使用 Css Modules

如果是模块化的 scss,得到的 css 类似:

.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;
}

实际需要的结果应该是这样:

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

在 webpack.config.js 需要对css-loader (v4.0+) 的 modules 属性添加 getLocalIdent:

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]',
                                //使用 getLocalIdent 自定义模块化名称 , css-loader v4.0+
                                getLocalIdent: (
                                    loaderContext,
                                    localIdentName,
                                    localName,
                                    options
                                ) => {
                                    if (
                                        multipleScopeVars.some(
                                            (item) =>
                                                item.scopeName === localName
                                        )
                                    ) {
                                        //localName 属于 multipleScopeVars 的不用模块化
                                        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优先于 sassOptions.multipleScopeVars
                                getMultipleScopeVars: (sassOptions) =>
                                    multipleScopeVars,
                                // 可选项
                                // implementation:sass
                            }),
                        },
                    },
                ],
            },
        ],
    },
};

以上是基于 webpack 的多主题的编译方案实现,如需 vite 版本的请看 vite 插件@zougt/vite-plugin-theme-preprocessor

multipleScopeVars

必需的

当 multipleScopeVars 只有一项时, scopeName 就没有意义,但是 path 可以起到 变量提升的作用

Type object[]

multipleScopeVars[].scopeName

Type string

multipleScopeVars[].path

必需的,变量文件的绝对路径

Type string || string[]

const multipleScopeVars = [
    {
        scopeName: 'theme-default',
        path: path.resolve('src/theme/default-vars.less'),
    },
    {
        scopeName: 'theme-mauve',
        path: path.resolve('src/theme/mauve-vars.less'),
    },
];

multipleScopeVars[].includeStyles

v1.3.0 支持 includeStyles,只在预设主题模式有效

Type: Object

当存在以下情况时,可以用这个属性处理

.theme-blue .el-button:focus,
.theme-blue .el-button:hover {
    /*这里的color值由 $primary-color 编译得来的,所以选择器前面加了 .theme-blue 提高了权重*/
    color: #0281ff;
    border-color: #b3d9ff;
    background-color: #e6f2ff;
}
.el-button--primary:focus,
.el-button--primary:hover {
    /*这里的color值不是由 变量 编译得来的,这时就会被上面那个 color 覆盖了, 实际上这里的color才是需要的效果*/
    color: #fff;
}
const includeStyles = {
    '.el-button--primary:hover, .el-button--primary:focus': {
        color: '#FFFFFF',
    },
};
const multipleScopeVars = [
    {
        scopeName: 'theme-default',
        path: path.resolve('src/theme/default-vars.less'),
        includeStyles,
    },
    {
        scopeName: 'theme-mauve',
        path: path.resolve('src/theme/mauve-vars.less'),
        includeStyles,
    },
];

得到

.theme-blue .el-button:focus,
.theme-blue .el-button:hover {
    /*这里的color值由 $primary-color 编译得来的,所以选择器前面加了 .theme-blue 提高了权重*/
    color: #0281ff;
    border-color: #b3d9ff;
    background-color: #e6f2ff;
}
.theme-blue .el-button--primary:focus,
.theme-blue .el-button--primary:hover {
    /*这里的color值不是由 变量 编译得来的,通过includeStyles也提高了权重得到实际的效果*/
    color: #ffffff;
}

出现权重问题效果图

includeStyles

使用了 includeStyles 的效果图

includeStyles