基于 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 的优化,对于相同原子类组合会打包成同一份代码,也一定程度上减少了样式相关内容编译后的大小。

八、相关引用

相关推荐
化作繁星27 分钟前
vue3项目图片压缩+rem+自动重启等plugin使用与打包配置
前端·vue·vite
u0104058361 小时前
构建可扩展的Java Web应用架构
java·前端·架构
swimxu1 小时前
npm 淘宝镜像证书过期,错误信息 Could not retrieve https://npm.taobao.org/mirrors/node/latest
前端·npm·node.js
qq_332394202 小时前
pnpm的坑
前端·vue·pnpm
雾岛听风来2 小时前
前端开发 如何高效落地 Design Token
前端
不如吃茶去2 小时前
一文搞懂React Hooks闭包问题
前端·javascript·react.js
alwn2 小时前
新知识get,vue3是如何实现在style中使用响应式变量?
前端
来之梦2 小时前
uniapp中 uni.previewImage用法
前端·javascript·uni-app
野猪佩奇0072 小时前
uni-app使用ucharts地图,自定义Tooltip鼠标悬浮显示内容并且根据@getIndex点击事件获取点击的地区下标和地区名
前端·javascript·vue.js·uni-app·echarts·ucharts
2401_857026232 小时前
拖动未来:WebKit 完美融合拖放API的交互艺术
前端·交互·webkit