一、實現原理
使用一些外部依賴,在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-loader
的option
設置:
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變量即可。