基于 Twin.macro + CSS Variables 的业务样式方案

一、业务背景

笔者某天正愉快的码业务时,突然接到领导谈话,明示某核心业务需要使用 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 变量,具有以下优势:

  1. 更简单的实现:使用 CSS 变量只需要在根元素中定义变量,然后在需要使用的地方调用即可,不需要引入额外的库或语法。
  2. 更高的性能:CSS 变量是原生 CSS 的一部分,浏览器可以在样式计算期间进行优化,因此使用 CSS 变量可以获得更高的性能。
  3. 更易于维护:使用 CSS 变量可以将样式与 JavaScript 代码分离,从而更易于维护和修改。
  4. 更容易与 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 原子类顺序格式化

prettier-plugin-tailwindcss

typescript 复制代码
module.exports = {
  ...require('@ysf/prettier-config'),
  plugins: [require('./plugin/prettier-plugin-tailwindcss/index.js')],
  tailwindConfig: './tailwind.config.js',
};

七、总结

在本次历史遗留项目重构过程中,采用这套样式方案,提效非常明显,在样式开发中至少节约一半以上的人力,写代码时,能够更加专注于业务逻辑,而不被样式弄的死去活来,当然得益于 Twin 的优化,对于相同原子类组合会打包成同一份代码,也一定程度上减少了样式相关内容编译后的大小。

八、相关引用

相关推荐
加班是不可能的,除非双倍日工资4 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi5 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip5 小时前
vite和webpack打包结构控制
前端·javascript
excel5 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国6 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼6 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy6 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT6 小时前
promise & async await总结
前端
Jerry说前后端6 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化
画个太阳作晴天6 小时前
A12预装app
linux·服务器·前端