效果如下:
1 前言
本人项目使用的 ui 库为 antd,样式为 less。
主题色的变化,antd 官网提供了相关的方案:定制主题,但是,该方案是静态的换肤,也就是已经知道系统需要什么样的主题色,根据相关的配置,antd 自动做转化。
这篇文章讲解的是动态主题色的变化,也就是,页面可能会有10种,或者20种颜色需要切换,不知道到底有多少种颜色;同时,文档也考虑到多人协助开发,开发人员只需要按照约定方式去编写样式、主题文件名、目录等命名规范即可。
主要思路:动态插入样式,覆盖系统已经编译好的相关样式,包括 UI 组件库 和 自定义样式。
2 实现
步骤一:在 Umi 里配置主题
如果你在使用 Umi,那么可以很方便地在项目根目录的 .umirc.ts
或 config/config.ts 文件中 theme 字段进行主题配置。theme
可以配置为一个对象或文件路径。
"theme": { "primary-color": "#1DA57A", },
或者 一个 js 文件:
"theme": "./theme.js",
本人使用的是公司自己的框架,基于 Umi 进行二次封装,所以,在 本目录下的config/theme.ts
配置 @primary-color
,就实现了默认主题色的配置。
步骤二:新建相关目录和文件
在根目录下,新建public
目录,引入less.4x.min.js
,具体在项目中 GitHub 代码库中下载;同时新建 styles
目录,创建 antd.theme.less
、 components.less
和 custom.theme.less
三个文件;
- antd.theme.less:用来放置覆盖
antd
UI 组件库样式;(antd 官方样式,可以查看这篇文章的评论 https://www.jianshu.com/p/87023e7f34c6)
- custom.theme.less:放置扫描项目所有自定义主题的样式;
- components.less:引入 antd.theme.less 和 custom.theme.less,如下:
@import "./antd.theme.less"; @import "./custom.theme.less";
说明:为啥是 public 目录呢?因为 Umi 项目,public 目录下所有文件会被 copy 到输出路径,也就是相关的资源,是被直接放到项目根目录。
具体如下图:
步骤三:编写 scripts/theme/themeScripts.js 脚本
新建 scripts/theme/themeScripts.js 文件,编写如下脚本,该脚本的主要作用是扫描项目中所有的 xxx.theme.less
文件,写入到 custom.theme.less
文件中。
// 主题色脚本 const fs = require('fs'); const path = require('path'); const chalk = require('chalk'); const { promisify } = require('util'); const themeVars = require('./../../config/theme'); const glob = promisify(require('glob')); const root = './'; const customPath = './public/styles/custom.theme.less'; Main(); async function Main() { init(); start(); } function init() { // 清空 fs.writeFileSync(customPath, '', 'utf8'); // 主题色变量初始值,跟 src/utils/index.ts 中 less.modifyVars 定义的变量相对应。 for(let key in themeVars) { fs.appendFileSync(customPath, `${key}: ${themeVars[key]};\n`, 'utf8'); } } async function start() { const rootPath = path.resolve(root); const fileList = await glob('src/**/*.theme.less', { cwd: rootPath, ignore: ['node_modules/**', 'src/.umi/**', 'src/.umi-production/**'] }); // global.theme.less 在最前面,优先级最低 const otherList = []; const globalFile = fileList.filter(file => { if (file.indexOf('global.theme.less') > -1) { return true; } otherList.push(file); return false; }); globalFile.concat(otherList).forEach((filePath) => { writeFile(rootPath, filePath); }); console.info(chalk.greenBright('恭喜您, 主题文件扫描完成!!!')); // eslint-disable-line } // 写文件 function writeFile(rootPath, filePath) { const bufferContent = fs.readFileSync(path.resolve(rootPath, filePath)); const content = bufferContent.toString('utf8'); fs.appendFileSync(customPath, `\n// ${filePath}\n${content}`, 'utf8'); } module.exports = Main;
注意:node 使用的是 CommonJS 服务器端 js 模块化的规范,所以对模块化导入导出需要使用 module.exports,主题色变量直接使用了项目初始化配置的 config/theme.ts
变量,所以,需要把文件从 ts 改为 js,同时 config/theme.ts
的导出需要改为 module.exports
导出,如下图:
步骤四:编写 utils 主题色方法
下面的方法主要是使用 less.js
,动态插入自定义的相关样式,利用 less.modifyVars
,传入相关动态主题色参数,改变自定义的相关样式。
根据如下代码,自定义的样式文件为 /styles/components.less
,该文件是作为统一的入口,引入 antd 和 自定义样式。
自定义的变量有@header-bar-visible 和 @primary-color;
function _changeTheme(themeColor: string) { // eslint-disable-next-line @typescript-eslint/no-explicit-any if (!(window as any).less) return; // eslint-disable-next-line @typescript-eslint/no-explicit-any (window as any).less.modifyVars({ '@primary-color': themeColor, '@header-bar-visible': 'visible', }); } let lessNodesAppended: boolean = false; /** * 动态更改主题色 * @param {string} themeColor 主题颜色 */ export function onChangeTheme(themeColor: string) { if (!lessNodesAppended) { // 插入 less.js,和 颜色主题.less const lessConfigNode = document.createElement('script'); const lessScriptNode = document.createElement('script'); const lessStyleNode = document.createElement('link'); lessStyleNode.setAttribute('rel', 'stylesheet/less'); lessStyleNode.setAttribute('href', '/styles/components.less'); // public 目标下 // https://lesscss.org/usage/#api // env: 'production' development lessConfigNode.innerHTML = ` window.less = { env: 'production', async: true, javascriptEnabled: true }; `; lessScriptNode.src = '/less.4x.min.js'; lessScriptNode.async = true; lessScriptNode.onload = () => { _changeTheme(themeColor); lessScriptNode.onload = null; }; document.body.appendChild(lessStyleNode); document.body.appendChild(lessConfigNode); document.body.appendChild(lessScriptNode); lessNodesAppended = true; } else { _changeTheme(themeColor); } }
3 package.json 引入执行主题色脚本
在 package.json 的 scripts 引入执行主题色脚本,同时,在 start 和 build 之前,每次自动执行扫描主题色,如下:
"scripts": { "start": "npm run theme && umi dev", "build": "npm run theme && umi build", "theme": "node scripts/theme/themeScripts.js", },
同时,安装脚本需要的库,glob 和 chalk:
npm i glob chalk -S
4 页面组件样式编写
经过以上步骤,就已经大功告成了,开发人员只需要编写 xxx.theme.less
即可,不再需要编写其他配置文件。
约定,相关的主题颜色需要抽成 xxx.theme.less
,如编写 List 组件,List.less 和 List.theme.less,如下图:
less 样式需要使用 prefix,不能使用 import styles from './Less.less'
这种写法,因为如果使用该写法,则样式编译会被加上 hash
,动态主题色覆盖就无法覆盖了。antd 官网组件也是使用 prefix 样式写法。
注意:自定义文件 xxx.theme.less 使用 css 写法,而不是采用 @prefix 前缀变量写法,因为 @prefix 写法有时候会失效,正常样式写法示例如下:
.c-list-more{ color: @primary-color; } .c-list-more:hover{ color: @primary-color; } .c-list-item .title::before{ background-color: @primary-color; } .c-list-item .title:hover{ color: lighten(@primary-color, 15%); } .c-list-item .desc{ color: @primary-color; }
5 资源
该文章是本人之前写过的两篇文章汇总,用兴趣的同学,可以查看原来的文章,了解更多的思路。
- GitHub 源码:umi-antd-dynamic-theme master 分支