React App Framework - 4. css/theme

css语言和插件

React技术栈中有各种各样的样式使用方案, 常见的如CSS-in-JSstyled-componentsemotionstyled-system等, 但是这些方案有的不够直观, 有的不方便复用, 看了一下还是使用styled-jsx比较全面, 支持常见的所有功能, 同时也是直接使用css语法

同时在项目中常用的css扩展语言是sass, styled-jsx也同时支持这种用法, 而我们引入的antd需要修改主题配色的话, 还需要引入less. 所以项目最终支持的css种类为:

  • styled-jsx + sass(组件内用法)
  • sass(独立文件)
  • less(仅限antd)

插件安装

styled-jsx

配置起来非常简单, 只需要在项目中引入styled-jsx插件即可, 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"babel": {
"presets": ["react-app"],
"plugins": [
[
"styled-jsx/babel",
{
"plugins": ["styled-jsx-plugin-sass"]
}
]
]
}
}

sass

create-react-app里面已经默认安装了sass-loader插件, 安装下sass模块就可以使用了

1
npm install -D node-sass

less

less是比较复杂的一个模块, 在create-react-app中默认不安装, 需要手动安装, 由于我们项目已经eject过, 所以直接给出配置方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const lessRegex = /\.less$/;
const use = [
// ...
{
test: lessRegex,
use: getStyleLoaders({
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
}).concat([
{
loader: require.resolve('less-loader'),
options: {
lessOptions: {
javascriptEnabled: true,
},
},
},
]),
},
// ...
];

主题配置

由于我们需要修改antd的主题色, 同时这个颜色也需要在各处使用, 所以定义一份颜色配置, 在less-loader里加入即可

这一步问题就来了, 由于我们代码是typescript, webpack.config.js只支持js格式, 所以需要在js中引入ts的代码和变量, 这里我们使用typescript-require来实现, 它的原理是通过修改require.extensions的方式来实现, 给require.extensions[.ts]加上一个ts的处理函数, 这样就可以在js中使用ts的代码了

theme-config

1
2
3
4
5
6
7
8
9
10
11
12
// src/config/theme.ts

const THEME = {
primary_color: '#1890ff',
};

const ANTD_THEME = {
'@primary-color': THEME.primary_color,
};

export default THEME;
export { ANTD_THEME };
1
2
3
4
5
6
7
8
9
10
11
12
// config/theme/get-theme.js
require('./typescript-require');
const theme = require('./../../src/config/theme');

module.exports = function getTheme(refresh = true) {
if (refresh) {
delete require.cache[require.resolve('./../../src/config/theme')];
}
const theme = require('./../../src/config/theme');
return theme.default;
};
module.exports.ANTD_THEME = theme.ANTD_THEME;

less

less在解决了ts引用的变量问题后, 可以直接使用了

1
2
3
4
5
// config/webpack.config.js
lessOptions = {
javascriptEnabled: true,
modifyVars: require('./theme/get-theme').ANTD_THEME,
};

sass

sass会稍微麻烦一点, 因为sass的编译不是使用变量配置, 而是使用prependData的方式注入外部变量, 那就需要把变量对象转化为sass字符串

1
2
3
4
5
6
7
8
9
10
11
12
// config/theme/flatten-obj-sass.js
module.exports = function flattenObjSass(obj, prefix = '$', transform = (key, val) => val) {
return Object.entries(obj).reduce((r, el) => {
const key = `${prefix}${el[0]}`;
const val = el[1];
if (typeof val === 'object' && !Array.isArray(val) && val) {
return r + `${flattenObjSass(val, `${key}-`, transform)}`;
} else {
return r + `${key}: ${Array.isArray(val) ? `(${transform(key, val)})` : transform(key, val)}; `;
}
}, '');
};
1
2
3
4
5
6
7
8
// config/webpack.config.js

const flattenObjSass = require('./theme/flatten-obj-sass');

sassOptions = {
sourceMap: true,
prependData: flattenObjSass(require('./theme/get-theme')()),
};

styled-jsx

styled-jsx的配置会更麻烦点, 需要在styled-jsxbabel-loader中注入option, 所以不能使用package.json里面的babel.plugins来注入插件了, 而是需要在webpack.config.js中自己注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// config/webpack.config.js

plugins = [
// ...
[
'styled-jsx/babel',
{
plugins: [
[
'styled-jsx-plugin-sass',
{
sassOptions: {
data: require('./theme/flatten-obj-sass')(require('./theme/get-theme')()),
},
},
],
],
},
],
];

使用和问题

上面的配置完成后, 看起来就比较完美了, 在代码中可以使用三种方式来设置主题配置

  1. 使用js直接引入, 如color: ${THEME.primary_color};
  2. 使用sass变量, 如color: $primary_color;
  3. 使用class注入, 这是在App.scss中使用sass脚本语法自动生成的相关变量的class, 如className='theme-color-primary_color'

但是在实际开发使用中却碰见了一个大问题: 修改primary_color的值之后, 使用第二种方式引入的组件颜色不会改变

因为sassless的变量注入都是在编译前注入的, 所以需要重启才能生效, 也是正常流程, 但是styled-jsx里的sass变量属性重启后都没效果, 所以猜想是缓存的原因

styled-jsx-sass缓存问题

仔细排查一番以后发现了原因:

styled-jsx的编译是通过babel-loader注入的, 而babel-loader在编译完成一份文件后, 会将所有编译参数和文件内容作为一个module放入cache中, 如果没有任何参数改变, 下次就不会走完整的编译流程, 而是直接使用现成的编译结果. 问题就出现在这里, 我们修改完theme文件中的primary_color变量后, 对于babel-loader编译的组件来说实际上没有任何改变, 所以不会重新编译, 而sass-options却是在编译时动态注入的, 不重新编译就不会注入新的变量, 所以导致了旧的组件代码修改sass变量不会生效

排查到具体原因后解决起来就比较简单了, babel-loader提供了customize设置, 我们可以将theme加入对应文件的编译参数即可

这里注意一下, 我们只需要将包含styled-jsx的组件加上theme注入即可, 其他没有使用过的不需要, 可以减少编译次数
1
2
// config/webpack.config.js
babelLoader.options.customize = path.resolve('./config/theme/theme-cache-babel-custom-loader.js');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// config/theme/theme-cache-babel-custom-loader.js
const getTheme = require('./get-theme');

const build = (custom) => {
return (babel) => {
const preCustom = custom ? custom(babel) : {};
return {
...preCustom,
config(config, { source, map: inputSourceMap, customOptions }) {
const result = preCustom.config ? preCustom.config(config, { source, map: inputSourceMap, customOptions }) : config.options;
const plugins = result.plugins;
const styledJsxPlugin = _.find(plugins, (x) => _.isMatch(x, { file: { request: 'styled-jsx/babel' } }));
if (styledJsxPlugin && /<style\s+jsx(\s|>)/.test(source)) {
styledJsxPlugin.options.theme = getTheme(false);
}
return result;
},
};
};
};

module.exports = build(require('babel-preset-react-app/webpack-overrides'));

小结

这里通过sass编译时动态注入变量的方式实现了全局统一主题配置, 但仍然存在的问题是不方便做主题切换, 看了一下antd自己实现的动态主题切换发现是引入了less页面编译, 这样不太友好而且会导致页面卡顿. 而且看了下sass貌似没有动态主题切换的功能, 所以目前项目的多主题色只能通过预先设置好的主题配置文件来实现, 如果需要新增一套主题, 则需要新增一份配置以及切换选项, 无法实现自定义主题色的功能

但除开动态自定义主题的问题以外, 其他功能都比较完善了, 比较好用的点有

  • 统一变量样式定义文件
  • jsx动态注入jssass变量
  • sass全局变量文件
  • 通过className实现快速样式设置
  • 通过全局className实现主题切换

CSS variables

css变量动态切换主题是最新的一种方案, 没有使用它的原因是:

  1. 即使使用了它, 也只能优化我们sass变量的写法, 无法对antdless变量生效
  2. 某些语法写起来比较别扭, 也不支持如sass提供的一系列函数方法, 自定义程度暂时还不够高
  3. 兼容性不够好

但是感觉这个方法才是未来的方向, 可以实现在全局范围内实时替换所有主题变量, 再也不需要静态编译和修改配置文件了

作者

Mosby

发布于

2019-10-10

许可协议

评论