react+less實現antd全局主題在線替換

一、實現原理

使用一些外部依賴,在webpack打包前先把antd所有的樣式抽離出來到一個獨立的css樣式表,然後在html模板的<body>頂層中手動引用這個樣式來達到樣式覆蓋的。

二、前期準備

(一) 安裝相關依賴

npm i less less-loader antd-theme-generator

(二) 配置less-loader以啓用變量修改功能

webpack.config.js中(注意,如果是使用create-react-app創建的項目,需要運行npm run eject暴露配置纔會有這個webpack配置文件):

//先在頂部定義好less文件類型解析的正則
const lessRegex = /\.less$/;
const lessModuleRegex = /\.module\.less$/;

找到file-loader的配置,在它前面加上:

 // 增加對xx.less文件解析
            {
              test: lessRegex,
              exclude: lessModuleRegex,
              use: getStyleLoaders(
                  {
                      importLoaders: 2,
                      sourceMap: isEnvProduction && shouldUseSourceMap,
                  },
                  'less-loader',
                  {
                    javascriptEnabled:true
                  }
              ),
              sideEffects: true,
          },
          // 增加對modules內關於import'/xx.less'文件的識別
          {
              test: lessModuleRegex,
              use: getStyleLoaders(
                  {
                      importLoaders: 2,
                      sourceMap: isEnvProduction && shouldUseSourceMap,
                      modules:{
                        getLocalIdent: getCSSModuleLocalIdent,
                      },
                  },
                  'less-loader',
                  {
                    javascriptEnabled:true
                  }
              )
          },

另外,對於create-react-app版本比較高的同學,或許需要在webpack.config.js中的getStyleLoaders中做些許修改來適配less-loaderoption設置:

  const getStyleLoaders = (cssOptions, preProcessor,more={}) => {
    const loaders = [
      isEnvDevelopment && require.resolve('style-loader'),
      isEnvProduction && {
        loader: MiniCssExtractPlugin.loader,
        options: paths.publicUrlOrPath.startsWith('.')
          ? { publicPath: '../../' }
          : {},
      },
      {
        loader: require.resolve('css-loader'),
        options: cssOptions,
      },
      {
        loader: require.resolve('postcss-loader'),
        options: {
          ident: 'postcss',
          plugins: () => [
            require('postcss-flexbugs-fixes'),
            require('postcss-preset-env')({
              autoprefixer: {
                flexbox: 'no-2009',
              },
              stage: 3,
            }),
            postcssNormalize(),
          ],
          sourceMap: isEnvProduction && shouldUseSourceMap,
        },
      },
    ].filter(Boolean);
    if (preProcessor) {
      loaders.push(
        {
          loader: require.resolve('resolve-url-loader'),
          options: {
            sourceMap: isEnvProduction && shouldUseSourceMap,
          },
        },
        {
          loader: require.resolve(preProcessor),
          options: {
            sourceMap: true,
            ...more  //主要是增加了額外配置的合併
          },
        }
      );
    }
    return loaders;
  };

(三) 編寫generator工具

script目錄下新建generateColor.js文件:

const path = require("path");
const fs = require('fs');
const { generateTheme } = require("antd-theme-generator");

const options = {
    stylesDir: path.join(__dirname, "../src/styles"),
    antDir: path.join(__dirname, "../node_modules/antd"),
    varFile: path.join(__dirname, "../src/styles/theme/variables.less"),
    mainLessFile: path.join(__dirname, "../src/styles/theme/index.less"),
    themeVariables: [
        "@primary-color",
        "@secondary-color",
        "@text-color",
        "@text-color-secondary",
        "@heading-color",
        "@layout-body-background",
        "@layout-header-background",
        "@border-radius-base"
    ],
    outputFilePath: path.join(__dirname, "../public/color.less")
};
fs.unlinkSync(options["outputFilePath"]);
generateTheme(options)
    .then((less) => {
        console.log("Theme generated successfully");
    })
    .catch((error) => {
        console.log("Error", error);
    });

(四) 創建樣式容器文件

之所以要做這一步,目的是用來收集antd的樣式和自己的樣式作爲同一個樣式文件輸出.
src目錄下新建styles文件夾,並且在裏面創建一個variables.less文件:

@import "~antd/lib/style/themes/default.less";

@primary-color: #1890ff;

然後在裏面新建一個thmem文件夾,並且在theme中創建一個index.less文件,隨便寫入一點東西:

.base-color {
    color: @primary-color;
}

(五) 在html模板中配置引用

找到html模板,在public/index.html中,找到<body>標籤頂層:

<link rel="stylesheet/less" type="text/css" href="/color.less" />
    <script>
        // eslint-disable-next-line no-undef
        window.less = {
            async: false,
            env: "production"
        };
    </script>

另外,在<style>標籤中,提前命名幾個CSS變量,後面會有非常有用:

<style>
body {
    --themeColor: #5ba9f3;
    --baseMenuColor: #001529;
    --reverseColor: white
    }
</style>

三、使用方法

可以結合react-color使用,也可以自定義幾種UI定好的主題色進行設置,在需要觸發變更顏色的地方,調用less的modifyVars方法即可:

const colorArr: string[] = generate(colorName);
(window as any).less
        .modifyVars({
        "@primary-color": colorArr[colorArr.length - 5], //這裏其實只需要賦值一個十六進制的顏色就可以,比如'#1296db'這樣的
        "@text-color": ColorReverse(colorArr[colorArr.length - 5]),
        "@layout-header-background": colorArr[colorArr.length - 6],
        "@item-hover-bg": colorArr[colorArr.length - 2],
        })
        .then(() => {
        setColor(colorName);
            document.body.style.setProperty("--themeColor", colorArr[colorArr.length - 4]);
            document.body.style.setProperty("--baseMenuColor", colorArr[colorArr.length - 7]);
            document.body.style.setProperty("--reverseColor", ColorReverse(colorArr[colorArr.length - 5]));
        })
        .catch((error: any) => {
            message.error(`Failed to update theme`);
            console.error(error);
        });

因爲筆者的項目主要是要改antd的主題色,所以順帶使用了antd的調色算法工具@ant-design/colors,將react-color選取的顏色按照antd設計來進行深淺計算,返回一組調色板顏色出來使用,另外,防止有些主題色與文字撞色,另外也自己手寫了反轉顏色的計算方法ColorReverse對顏色進行反轉,大多數情況下,其實只需要直接賦值自己想要的顏色就行了。

最後一步,就是在package.json中,修改script命令,在打包前,先進行color的收集編譯:

"scripts": {
    "dev": "node ./scripts/generateColor.js && node scripts/start.js",
    "build": "node ./scripts/generateColor.js && node scripts/build.js",
    "test": "node scripts/test.js",
    "fix": "eslint --fix src/**"
  },

四、項目難點

如果也使用antd側邊欄組件的同學,估計發現了,側邊欄的樣式本身就只有淺色和深色,一旦修改了主題色,側邊欄會和選中樣式格格不入,這時候,之前在body中創建的幾個CSS變量就派上用場了。

我們可以在側邊欄組件賦值一個全局唯一的id屬性,用id來提升它的css等級,達到劫持樣式的效果,比如我的側邊欄id是’mainSider’,那麼我們可以在當前頁的less樣式中這樣寫:

#mainSider{
    .ant-menu-dark .ant-menu-inline.ant-menu-sub {
        background: var(--baseMenuColor);
      }
    .ant-menu.ant-menu-dark, .ant-menu-dark .ant-menu-sub{
        background: var(--baseMenuColor);
    }
    .ant-menu-submenu-title {
        color:var(--reverseColor); 
    }
}

爲什麼可以這樣來把側邊欄的底色換掉?還記得上面使用方法中,我們有這幾行代碼嗎?

.then(() => {
    setColor(colorName);
        document.body.style.setProperty("--themeColor", colorArr[colorArr.length - 4]);
        document.body.style.setProperty("--baseMenuColor", colorArr[colorArr.length - 7]);
        document.body.style.setProperty("--reverseColor", ColorReverse(colorArr[colorArr.length - 5]));
        })

這就是在設置antd的@primary-color的同時,也同時將調色算法返回的顏色順帶設置到了提前定義好的CSS變量中,然後又因爲側邊欄的內部樣式已經被我們用id的方法重新賦值了背景色爲我們定義的CSS變量,所以CSS變量一變,側邊欄也跟着變了。

同理,當我們有其他組件的內部顏色需要根據主題色一起變化時,只需要使用提前定義好的CSS變量即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章