一、业务背景
笔者某天正愉快的码业务时,突然接到领导谈话,明示某核心业务需要使用 React 进行重构。 不过回头想想,随着团队发展(React为主导的技术栈),已经基于 React 开发出了很多配套设施。但是个别项目因为历史遗留原因,无法使用这些配套设施,在开发需求时,陷入了死局,存在以下几个问题:
- 新人无法适应,老旧技术栈(Regular + Nej + Mcss 公司内部远古前端框架)。
- 开发的代码无法被其他项目复用,当然其他公共基建它也用不了。
- 代码维护成本高,例如无法使用 TypeScript。
- 开发成本高,编辑器的一些提效插件无法使用。
当然还有其他一些问题,比如无法接入统一的构建流程、CI/CD 等,以上种种问题都表明,是时候重构了。
由于重构与新业务需求的开发都是并行的,所以给予重构的工时并不会太多,那么在技术选型上尤为重要,它决定了重构的效率以及后续开发的整体风格。本文就介绍一下笔者在业务重构中---样式方案技术选型。
先说下本次重构在样式方面使用的方案吧:Tailwind CSS + Twin.macro + Emotion + CSS Variables。
二、Tailwind CSS
2.1 优势/劣势
提及到高效的样式技术选型方案,首先想到的是时下流行的 Tailwind CSS,当然它也是本次重构中的重要角色,它有以下优点:
- 提供统一的样式风格(边距、颜色、阴影等)。
- 无需纠结如何给类命名。
- 无需担心类名冲突问题。
- 无需在 CSS 与 HTML 代码之间来回切换,主打一个专注开发。
但是在实际业务开发中还是遇到了一些问题:
- 默认情况下 Tailwind CSS 的单位是
rem
。 - 开发组件需要打包给其他业务使用时,Tailwind CSS 配置可能存在差异。
2.2 将 rem
单位转换成 px
单位
typescript
// tailwind.config.js
const defaultTheme = require('tailwindcss/defaultTheme');
function rem2px(input, fontSize = 16) {
if (input == null) {
return input;
}
switch (typeof input) {
case 'object':
if (Array.isArray(input)) {
return input.map((val) => rem2px(val, fontSize));
}
const ret = {};
for (const key in input) {
ret[key] = rem2px(input[key], fontSize);
}
return ret;
case 'string':
return input.replace(/(\d*\.?\d+)rem$/, (_, val) => `${parseFloat(val) * fontSize}px`);
case 'function':
return eval(input.toString().replace(/(\d*\.?\d+)rem/g, (_, val) => `${parseFloat(val) * fontSize}px`));
default:
return input;
}
}
module.exports = {
corePlugins: {
preflight: false,
// 兼容低版本 Edge 和 IE11
divideOpacity: false,
backgroundOpacity: false,
textOpacity: false,
borderOpacity: false,
placeholderOpacity: false,
},
content: ['./index.html', './source/**/*.{js,ts,jsx,tsx}'],
theme: {
...rem2px(defaultTheme),
extend: {},
},
};
2.3 项目之间配置差异导致冲突问题
javascript
A业务中:w-1 => 4px
B业务中:w-1 => 2px
虽然 Tailwind.css 支持 自定义前缀 ,但是差点意思,因为你写的时候也要把前缀加上。 那么有什么方案能够让我们即能享受到 Tailwind CSS 的红利,又不需要担心项目之间原子类冲突呢?答案:Twin.macro
三、Twin.macro
官方介绍:The magic of Tailwind with the flexibility of css-in-js.
3.1 原理介绍
这个工具能够在构建时,通过 babel 宏,将我们的 Tailwind CSS 原子类转换成样式对象,输出给各类 css-in-js 库,例如 styled-components / emotion 等。
javascript
import tw from 'twin.macro'
tw`text-sm md:text-lg`
// ↓ ↓ ↓ ↓ ↓ ↓
{
fontSize: '0.875rem',
'@media (min-width: 768px)': {
fontSize: '1.125rem',
},
}
3.2 风格约束
静态写法
markdown
<div tw="w-10 h-10 bg-white">Hello World</div>
动态写法
typescript
import React from 'react';
import tw from 'twin.macro';
type Props = {
primary: boolean;
};
export const Demo = (props: Props) => {
const { primary } = props;
return <div css={[tw`w-10 h-10 bg-white`, primary ? tw`bg-red-500 text-white` : '']}>Hello World</div>;
};
当然还有编辑器的加持,能够提示各种原子类
3.3 TypeScript 支持
默认情况下 React
无法识别 tw
属性的,所以我们需要添加额外的 twin.d.ts 声明
typescript
import 'twin.macro';
import type { css as cssImport } from '@emotion/react';
import type { CSSInterpolation } from '@emotion/serialize';
import type styledImport from '@emotion/styled';
declare module 'twin.macro' {
// The styled and css imports
const styled: typeof styledImport;
const css: typeof cssImport;
}
declare module 'react' {
// The tw and css prop
interface DOMAttributes<T> {
tw?: string;
css?: CSSInterpolation;
}
}
3.4 配套库及说明
babel-plugin-twin
自动导入 babel 宏函数 import "twin.macro"
typescript
import "twin.macro"
babel-plugin-macros
babel 插件宏,通过在源码中引入 macro 模块,在要转换的地方调用相应的 api,macro 内部会拿到相应的 ast,然后进行转换的一种方式。
twin.macro
通过 babel-plugin-macros 创建宏函数 使用 generateRules 实现 tailwind.css 原子类转成 style 对象,
typescript
<div tw="hidden!" /> || <div tw="!hidden" />
// ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
<div css={{ "display": "none !important" }} />
@emotion/babel-preset-css-prop
此预设通过 babel 配置的单个条目为整个项目启用 css prop。添加预设后,编译后的 JSX 代码将使用 Emotion 的 jsx 函数而不是 React.createElement
typescript
const Link = props => (
<a
css={{
color: 'hotpink',
'&:hover': {
color: 'darkorchid'
}
}}
{...props}
/>
)
typescript
import { jsx as ___EmotionJSX } from '@emotion/react'
function _extends() {
/* babel Object.assign polyfill */
}
var _ref =
process.env.NODE_ENV === 'production'
? {
name: '1fpk7dx-Link',
styles: 'color:hotpink;&:hover{color:darkorchid;}label:Link;'
}
: {
name: '1fpk7dx-Link',
styles: 'color:hotpink;&:hover{color:darkorchid;}label:Link;',
map: '/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImF1dG9tYXRpYy1pbXBvcnQuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRUkiLCJmaWxlIjoiYXV0b21hdGljLWltcG9ydC5qcyIsInNvdXJjZXNDb250ZW50IjpbImNvbnN0IExpbmsgPSBwcm9wcyA9PiAoXG4gIDxhXG4gICAgY3NzPXt7XG4gICAgICBjb2xvcjogJ2hvdHBpbmsnLFxuICAgICAgJyY6aG92ZXInOiB7XG4gICAgICAgIGNvbG9yOiAnZGFya29yY2hpZCdcbiAgICAgIH1cbiAgICB9fVxuICAgIHsuLi5wcm9wc31cbiAgLz5cbilcbiJdfQ== */'
}
const Link = props =>
___EmotionJSX(
'a',
_extends(
{
css: _ref
},
props
)
)
四、换肤方案
换肤方案我们选择的是 CSS Variables,当然 CSS-in-JS 也能够实现。 但使用 CSS 变量,具有以下优势:
- 更简单的实现:使用 CSS 变量只需要在根元素中定义变量,然后在需要使用的地方调用即可,不需要引入额外的库或语法。
- 更高的性能:CSS 变量是原生 CSS 的一部分,浏览器可以在样式计算期间进行优化,因此使用 CSS 变量可以获得更高的性能。
- 更易于维护:使用 CSS 变量可以将样式与 JavaScript 代码分离,从而更易于维护和修改。
- 更容易与 Tailwind CSS 结合
javascript
// tailwind.config.js
module.exports = {
// ...其他配置:
theme: {
extend: {
colors: {
primary: {
DEFAULT: `var(--ysf-color-primary)`,
reverse: `var(--ysf-color-primary-reverse)`,
light1: `var(--ysf-color-primary-light1)`,
light2: `var(--ysf-color-primary-light2)`,
light3: `var(--ysf-color-primary-light3)`,
light4: `var(--ysf-color-primary-light4)`,
light5: `var(--ysf-color-primary-light5)`,
light6: `var(--ysf-color-primary-light6)`,
light7: `var(--ysf-color-primary-light7)`,
light8: `var(--ysf-color-primary-light8)`,
light9: `var(--ysf-color-primary-light9)`,
},
fore: {
DEFAULT: `var(--ysf-color-fore)`,
reverse: `var(--ysf-color-fore-reverse)`,
},
color: {
warning: `var(--ysf-color-warning)`,
danger: `var(--ysf-color-danger)`,
success: `var(--ysf-color-success)`,
link: `var(--ysf-color-link)`,
border: `var(--ysf-color-border)`,
bgChat: `var(--ysf-color-bg-chat)`,
},
},
},
},
};
五、兼容性相关
由于历史原因,需要兼容到 IE 11, 但是 CSS Variables 不兼容 IE 11,但是好在有个库能够帮我们处理这个问题,css-vars-ponyfill 当检测到浏览器不支持 CSS Variables 时,它能够动态的帮助我们将 CSS 变量转换成固定值
typescript
import cssVars from 'css-vars-ponyfill';
import colorLightenTool from '@/utils/colorLightenTool';
export const setTheme = (theme) => {
const themeLighten1 = colorLightenTool(theme.primary, 0.1);
const themeLighten2 = colorLightenTool(theme.primary, 0.2);
const themeLighten3 = colorLightenTool(theme.primary, 0.3);
const themeLighten4 = colorLightenTool(theme.primary, 0.4);
const themeLighten5 = colorLightenTool(theme.primary, 0.5);
const themeLighten6 = colorLightenTool(theme.primary, 0.6);
const themeLighten7 = colorLightenTool(theme.primary, 0.7);
const themeLighten8 = colorLightenTool(theme.primary, 0.8);
const themeLighten9 = colorLightenTool(theme.primary, 0.9);
cssVars({
variables: {
'--ysf-color-white': '#fff',
'--ysf-color-highlight': 'red', // 高亮颜色
'--ysf-color-link': '#176ae5', // a标签超链接颜色
'--ysf-color-text': '#333', // 文本颜色,默认 #333
'--ysf-color-primary': theme.primary, // 主题色 原同步数据:bgColor
'--ysf-color-primary-light1': themeLighten1,
'--ysf-color-primary-light2': themeLighten2,
'--ysf-color-primary-light3': themeLighten3,
'--ysf-color-primary-light4': themeLighten4,
'--ysf-color-primary-light5': themeLighten5,
'--ysf-color-primary-light6': themeLighten6,
'--ysf-color-primary-light7': themeLighten7,
'--ysf-color-primary-light8': themeLighten8,
'--ysf-color-primary-light9': themeLighten9,
'--ysf-color-primary-reverse': '#fff', // 主题色-反色
'--ysf-color-fore': '#fff', // 前景色
'--ysf-color-fore-reverse': '#333', // 前景色-反色
'--ysf-color-border': '#e6e7eb', // 边框颜色
'--ysf-color-success': '#67c23a', // 成功颜色
'--ysf-color-warning': '#e6a23c', // 危险颜色
'--ysf-color-danger': '#f25058', // 失败颜色
'--ysf-color-placehoder': '#d3d1d7', // 输入框提示颜色
},
});
};
六、相关配套设施
6.1 VsCode 插件智能提示
Tailwind CSS IntelliSense Tailwind CSS IntelliSense and Twin
6.2 Prettier 原子类顺序格式化
typescript
module.exports = {
...require('@ysf/prettier-config'),
plugins: [require('./plugin/prettier-plugin-tailwindcss/index.js')],
tailwindConfig: './tailwind.config.js',
};
七、总结
在本次历史遗留项目重构过程中,采用这套样式方案,提效非常明显,在样式开发中至少节约一半以上的人力,写代码时,能够更加专注于业务逻辑,而不被样式弄的死去活来,当然得益于 Twin 的优化,对于相同原子类组合会打包成同一份代码,也一定程度上减少了样式相关内容编译后的大小。