浅谈原子化 css

2,802 阅读12分钟

什么是原子化 css

定义:每一个css属性都通过一个css类来实现的一种 css 写法。

举个例子,我们平时是这么写 css 的:

<div class="text">这是一行文字</div>

<style>
.text {
  font-size: 12px;
  color: red;
  line-height: 40px;
}
</style>

而使用原子化 css 就变成了这样:

<div class="text-12 text-red leading-40">这是一行文字</div>

<!-- 这里的样式可以使用工具自动生成 -->
<style>
.text-12 {
  font-size: 12px;
}

.text-red {
  color: red;
}

.leading-40 {
  line-height: 40px;
}
</style>

这里我们能看出原子化 css 的一些优缺点

原子化 css 的优势:

  1. 不必再浪费精力在定义类名上

细细想想我们平时在写 css 的时候都有思考哪些?

「根据所写功能定义类名 ---> 根据 DOM 层级定义 css 模块 ---> 编写 css 属性」

而在写原子化 css 时,我们可以跳过前两步直接编写css,并且不需要在 html 文件和 css 文件之间来回切换,开发效率大大大幅提高!

  1. 控制 CSS 体积

image.png

随着项目越来越大,css 体积一定是成等比增加的,就算有复用css,但也无法像原子化 css 那样精确到每一个 css 属性。在极致的原子化 css 开发中,我们能做到每一个 css 属性有且只有一个,复用性可达到最大。项目越大优势越明显

我自己项目中 row-y-center 这个类目前使用了 110 次,如果按不复用样式正常写 css 的话,那原子化css 写法所占用的css 体积基本可以忽略不计了

.row-y-center {
  display: -webkit-box;
  display: -ms-flexbox;
  display: -webkit-flex;
  display: flex;
  -webkit-box-orient: horizontal;
  -webkit-box-direction: normal;
  -ms-flex-direction: row;
  -webkit-flex-direction: row;
  flex-direction: row;
  -webkit-box-align: center;
  -ms-flex-align: center;
  -webkit-align-items: center;
  align-items: center;
}
  1. 调试和修改都变得轻松

在 chrome devtools 里可以直接看到有啥样式,而且样式之间基本没有交叉,很容易调试:

image.png

普通 css 写法容易多个 class 的样式相互覆盖,还要确定优先级和顺序,因此更难调试:

image.png

在修改原子化 css 时不需要考虑 css 层级,也不需要担心修改一个类影响到其他类。

原子化 css 的不足和注意点:

  1. 类型过长

因为原子化 css 相当于把一部分 css 代码揉进了 html 里,所以有时 className 会变得很长

<div className="text-center mb-84 w-full text-555 text-20 leading-28 relative">按钮</div>
  1. 注意样式优先级 (这不是原子化 css 的不足,只是它的特性)

因为原子化 css 的类都是平铺的,所以两个样式之间是不会相互覆盖的;并且类是自动生成的,所以也不能确定最后生成的类谁在前谁在后,最多可以使用 !important 进行样式覆盖。

举个例子,比如有一个按钮,分为可点和不可点两种状态。

我们平时可能会这样写

<button className={`btn ${disabled ? 'disabled' : ''}`}>按钮</button>


<style>
.btn {
  width: 100px;
  height: 50px;
  background: #f0f;
}

.btn.disabled {
  background: #999;
}
</style>

其中 disabled 的样式是覆盖了原本的 btn 样式的,而在原子化 css 中就没有这种类权重的概念了,需要通过判断使用不同的类

<button className={`w-100 h-50 ${disabled ? 'bg-[#999]' : 'bg-[#f0f]'}`}>按钮</button>

如果实在是需要通过覆盖类来改变样式的话,可以在类名前加一个 !,这样生成的样式会自动增加 !important

<div class="!text-20></div>

<style>
.\!text-20 {
  font-size: 20px !important;
}
</style>
  1. 需要记一些类名 😜

不可否认,这确实需要一些时间去记忆,不过相比于记英语单词,这简直就是小儿科~ 而且 vscode 提供了代码提示插件,更不需要强记忆了

image.png

我的项目是否适合用原子化 css?

根据亲身实践,我总结了以下几点场景比较合适使用原子化 css:

  • 团队成员对原子化 css 的思想至少持开放态度,可以接受并学习使用
  • 项目 UI 定制化程度较高,需要写大量 css
  • 对项目体积有较高要求
  • 如果是现有项目,要考虑迁移成本,将原有样式替换成原子化 css 样式也算是个费时费力的工程了🥲

实战

技术选型

原子化 css 最初是由 Thierry Koblentz (Yahoo!)在 2013 年挑战 CSS 最佳实践时使用的,经历了 10 年的打磨,目前市面上比较成熟的框架有 tailwindcsswindicssunocss

  • tailwindcss:基于 postcss 插件系统开发,自动收集用户编写的原子化 css,并生成 style 样式。可通过配置和自定义插件实现定制化样式,基于 rust 实现样式解析算法提高编译速度。tailwind是目前市场上生态最健全、最稳定的原子化 css 库之一
  • windicss:设计初衷是作为 tailwindcss 替代品,并提供了很多亮眼特性,比如「自动值推导」、「属性化模式」、「无用样式剔除」等,很多特性也在 tailwindcss3.0 版本被吸取。不过很遗憾,目前这个库已经停止维护了,不过从使用上是不影响的,目前我自己的项目用的也是它。
  • unocss:由原 windicss 团队成员创建,在技术设计上与 tailwind 完全不同,不再依赖 postcss,构建速度相比于 tailwindcss 有较大提升;windicss 有的它基本也都有,同时还增加了比如 Web fontsCDN Runtime 等新特性,目前是 0.55 版本所以稳定性上还有待考证。

配置

这里贴下我项目的配置吧,底层库用的是 windicss,因为是在微信小程序中使用,所以有一些特性是无法使用的,比如 attributify

import { defineConfig } from 'windicss/helpers';

const fromEntries = function fromEntries(iterable: string[][]) {
  return [...iterable].reduce<Record<string, any>>((obj, [key, val]) => {
    obj[key] = val;
    return obj;
  }, {});
};

const range = (size: number) =>
  fromEntries([...Array(size).keys()].slice(1).map((i) => [`${i}_${size}`, `${(i / size) * 100}%`]));

const generateSpacing = (num: number, unit = 'px') => {
  return new Array(num).fill(1).reduce((cur, _next, index) => ({ ...cur, [index]: `${index}${unit}` }), {});
};


export default defineConfig({
  separator: '_',
  compile: false,
  globalUtility: false,
  darkMode: 'media',
  attributify: false,
  preflight: false,
  prefixer: true,
  extract: {
    exclude: ['node_modules', '.git', 'dist'],
  },
  // important: true,
  corePlugins: {
    container: false,
    space: false,
    divideStyle: false,
    divideWidth: false,
    divideColor: false,
    divideOpacity: false,
    // 涉及到通配符(*),wx 小程序不支持
    ringWidth: false,
    ringColor: false,
    ringOpacity: false,
    ringOffsetWidth: false,
    ringOffsetColor: false,
  },
  // 样式类名黑名单,包含在里面的不会被 windicss 解析,可以用于过滤一些有问题的类名
  blocklist: [],
  shortcuts: {
    // #region flex 布局
    row: 'flex flex-row',

    'row-x-start': 'row justify-start',
    'row-x-center': 'row justify-center',
    'row-x-end': 'row justify-end',
    'row-x-between': 'row justify-between',
    'row-x-around': 'row justify-around',
    'row-x-evenly': 'row justify-evenly',

    'row-y-start': 'row items-start',
    'row-y-center': 'row items-center',
    'row-y-end': 'row items-end',
    'row-y-baseline': 'row items-baseline',
    'row-y-stretch': 'row items-stretch',

    'row-center': 'row justify-center items-center',

    column: 'flex flex-col',

    'column-x-start': 'column items-start',
    'column-x-center': 'column items-center',
    'column-x-end': 'column items-end',
    'column-x-baseline': 'column items-baseline',
    'column-x-stretch': 'column items-stretch',

    'column-y-start': 'column justify-start',
    'column-y-center': 'column justify-center',
    'column-y-end': 'column justify-end',
    'column-y-between': 'column justify-between',
    'column-y-around': 'column justify-around',
    'column-y-evenly': 'column justify-evenly',

    'column-center': 'column justify-center items-center',
    // #endregion

    // 单行文字溢出显示省略号
    'text-overflow-ellipsis': 'truncate',

    // 使用绝对定位实现的「垂直居中」
    'absolute-y-center': 'absolute top-1_2 transform -translate-y-1_2',
  },
  // windicss 会主动编译这里的样式,适用于一些不得不拼接类名的场景
  safelist: ['border-bottom-c222-z1', 'border-bottom-ce6e6e6-z1', 'border-bottom-cfff-z1'],
  theme: {
    extend: {
      colors: {
        primary: '#222',
        input: {
          /** 输入框字体颜色 */
          value: '#333a4e',
          placeholder: '#bbb',
        },
        green: {
          theme: '#27AE60',
        },
      },
      transitionProperty: {
        height: 'height',
        spacing: 'margin, padding',
      },
    },
    spacing: {
      ...generateSpacing(301),
    },
    boxShadow: {
      vehicle: '0px 4px 14px 0px #0000001a',
    },
    backgroundColor: (theme) => ({
      ...theme('colors'),
    }),
    borderColor: (theme) => ({
      ...theme('colors'),
    }),
    fontSize: (theme) => theme('spacing'),
    borderWidth: (theme) => theme('spacing'),
    lineHeight: (theme) => theme('spacing'),
  },
  plugins: [
    require('windicss/plugin/line-clamp'),
    require(path.resolve(__dirname, 'config/plugins/windicss/border')),
    require(path.resolve(__dirname, 'config/plugins/windicss/safe-area/index')),
  ],
});

这里再分享一个自己写的一像素边框插件,使用方法如下:

border-[all|top|bottom|left|right]-c[不带#的颜色hex="fff"]-w[border-width="1"]-r[border-radius="0"]-z[z-index="auto"]-o[opacity="1"]

  • 例子1:border-top-c222-w2-r10-z1000-o3 ==> 颜色为 #222,宽度为 2px,圆角为 10px,层级为 1000,透明度为 0.3 的上边框
  • 例子2:border-bottom-c222 ===> 颜色为 #222,宽度为 1px,圆角为 0,层级为 auto,透明度是 1 的下边框
  • 例子3:border-all-r16-c333 ===> 颜色为 #333,宽度为 1px,圆角为 16px,层级为 auto,透明度是 1 的全边框
import plugin from 'windicss/plugin';

const generateStyles = (className: string) => {
  const matchRes = `${className}-`.match(/(?<=-).+?(?=-)/g) || [];

  const styles = {
    borderColor: '#fff',
    borderWidth: '2px',
    borderRadius: '0',
    zIndex: 'auto',
    opacity: '1',
  };

  matchRes.forEach((item) => {
    const fir = item.charAt(0);
    const val = item.substr(1, item.length);

    switch (fir) {
      case 'c':
        styles.borderColor = `#${val}`;
        break;
      case 'w':
        styles.borderWidth = `${+val * 2}px`;
        break;
      case 'r':
        styles.borderRadius = `${+val * 2}px`;
        break;
      case 'z':
        styles.zIndex = val;
        break;
      case 'o':
        styles.opacity = `0.${val}`;
        break;
    }
  });

  return styles;
};

module.exports = plugin(function ({ addDynamic }) {
  /** 全边框 */
  addDynamic(
    'border-all',
    ({ Utility, Style }) => {
      const { borderColor, borderWidth, borderRadius, zIndex, opacity } = generateStyles(
        Utility.raw.replace('border-all', ''),
      );

      return Style.generate(Utility.class, {
        position: 'relative',
        '&::before': {
          content: '""',
          position: 'absolute',
          top: '0',
          left: '0',
          width: '200%',
          height: '200%',
          transform: 'scale(0.5)',
          transformOrigin: 'left top',
          boxSizing: 'border-box',
          pointerEvents: 'none',
          zIndex,
          opacity,
          border: `${borderWidth} solid ${borderColor}`,
          borderRadius: `${borderRadius}`,
        },
      });
    },
    {
      group: 'border-all',
      completions: ['border-all-c-w-r-z-o'],
      // 自定义 className
      respectSelector: true,
    },
  );

  /** 上/下/左/右边框 */
  ['top', 'bottom', 'left', 'right'].forEach((item) => {
    const name = `border-${item}`;

    addDynamic(
      name,
      ({ Utility, Style }) => {
        const { borderColor, borderWidth, zIndex, opacity } = generateStyles(Utility.raw.replace(name, ''));

        return Style.generate(Utility.class, {
          position: 'relative',
          '&::before': {
            content: '""',
            position: 'absolute',
            transform: ['top', 'bottom'].includes(item) ? 'scaleY(0.5)' : 'scaleX(0.5)',
            transformOrigin: '0, 0',
            pointerEvents: 'none',
            zIndex,
            opacity,
            background: borderColor,
            [item]: '0',
            ...(['top', 'bottom'].includes(item)
              ? {
                  left: '0',
                  right: '0',
                  height: borderWidth,
                }
              : {
                  top: '0',
                  bottom: '0',
                  width: borderWidth,
                }),
          },
        });
      },
      {
        group: name,
        completions: [`${name}-c-w-z-o`],
        // 自定义 className
        respectSelector: true,
      },
    );
  });
});

源码解析(tailwindcss)

从本质上看,tailwind 是一个 postcss 插件,它基于 postcss 对 css 进行处理,那我们先来了解下 postcss

什么 是 postCss?

通俗的讲法:postCss 就是一个开发工具,是一个用 JavaScript 工具和插件转换 CSS 代码的工具。支持变量,混入,未来 CSS 语法,内联图像等等。

它具备以下特性与常见的功能:

  • 增强代码的可读性:Autoprefixer 自动获取浏览器的流行度和能够支持的属性,并根据这些数据帮你自动为 CSS 规则添加前缀。
  • 将未来的 CSS 特性带到今天!:帮你将最新的 CSS 语法转换成大多数浏览器都能理解的语法,并根据你的目标浏览器或运行时环境来确定你需要的 polyfills
  • 终结全局 CSS:CSS 模块 能让你你永远不用担心命名太大众化而造成冲突,只要用最有意义的名字就行了。
  • 避免 CSS 代码中的错误:通过使用 stylelint 强化一致性约束并避免样式表中的错误。stylelint 是一个现代化 CSS 代码检查工具。它支持最新的 CSS 语法,也包括类似 CSS 的语法,例如 SCSS
  • 可以作为预处理器使用,类似:Sass, Less 和 Stylus。但是 PostCSS 是模块化的工具,比之前那些快3-30 倍,而且功能更强大。并演化出了一系列的插件来使用。

postCss的核心原理/工作流

PostCSS 包括 CSS 解析器,CSS 节点树 API,一个源映射生成器和一个节点树 stringifier。

PostCSS 主要的原理核心工作流:

  • 通过 fs 读取CSS文件
  • 通过 parser 将CSS解析成抽象语法树(AST树)
  • 将AST树”传递”给任意数量的插件处理
  • 诸多插件进行数据处理。插件间传递的数据就是AST树
  • 通过 stringifier 将处理完毕的AST树重新转换成字符串

这一系列的工作流,讲简单一点就是对数据进行一系列的操作。为此PostCSS提供了一系列的数据操作API。比如:walkAtRules ,walkComments,walkDecls,walkRules等相关API, 具体相关文档可查看: postcss官方API文档

使用 astexplorer 我们可以看到 postcss 将 css 处理为 AtRule 的结果

image.png

tailwindcss 工作流

我们写一个简单的测试用例来说明 tailwindcss 是如何运行的

import { run, html, css } from './util/run'

test('font-size utilities can include a default line-height', () => {
  let config = {
    content: [{ raw: html`<div class="text-md text-sm text-lg"></div>` }],
    theme: {
      fontSize: {
        sm: '12px',
        md: ['16px', '24px'],
        lg: ['20px', '28px'],
      },
    },
  }

  return run('@tailwind utilities', config).then((result) => {
    expect(result.css).toMatchFormattedCss(css`
      .text-lg {
        font-size: 20px;
        line-height: 28px;
      }
      .text-md {
        font-size: 16px;
        line-height: 24px;
      }
      .text-sm {
        font-size: 12px;
      }
    `)
  })
})

入口文件 src/plugin.js

module.exports = function tailwindcss(configOrPath) {
  return {
    postcssPlugin: 'tailwindcss',
    plugins: [
      // ...,
      ...handleImportAtRules(),
      async function (root, result) {
        // ...
        // MARK: 核心编译方法
        await processTailwindFeatures(context)(root, result)
      },
      function lightningCssPlugin(_root, result) {
        // ...
        // MARK: Lightning CSS 是一个用 Rust 编写的极快的CSS 解析器、转换器和压缩器。
          let transformed = lightningcss.transform({
            filename: result.opts.from,
            code: Buffer.from(intermediateResult.css),
            minify: false,
            sourceMap: !!intermediateMap,
            targets: lightningcss.browserslistToTargets(browserslist(browsersListConfig)),
            drafts: {
              nesting: true,
              customMedia: true,
            },
            nonStandard: {
              deepSelectorCombinator: true,
            },
            include: Features.Nesting,
            exclude: Features.LogicalProperties,
          })

          // MARK: 生成最后的 css 代码
          let code = transformed.code.toString()

          result.root = postcss.parse(license() + code, {
            ...result.opts,
            map: intermediateMap,
          })
        // ...
      },
      // ...
    ].filter(Boolean),
  }
}

我们可以看到,这里注册了名称为 'tailwindcss' 的 postcss 插件,其中:

  • handleImportAtRules 方法中注册了 postcssImport 这个插件,它的作用是 允许在CSS文件中使用@import语句导入其他CSS文件
  • processTailwindFeatures 为核心编译方法,其中 context 为编译上下文对象,存放一些临时缓存数据和工具函数;root 是 postcss 处理后的根节点AST;result 为最终输出的编译结果。

核心编译方法 src/processTailwindFeatures.js

// MARK: 构建上下文环境
export default function processTailwindFeatures(setupContext) {
  return async function (root, result) {
    // ...
    
    // MARK: 构建上下文对象
    let context = setupContext({
      tailwindDirectives,
      applyDirectives,
      registerDependency(dependency) {
        result.messages.push({
          plugin: 'tailwindcss',
          parent: result.opts.from,
          ...dependency,
        })
      },
      createContext(tailwindConfig, changedContent) {
        return createContext(tailwindConfig, changedContent, root)
      },
    })(root, result)
    // ...

    // MARK: 编译生成 AtRule 对象
    await expandTailwindAtRules(context)(root, result)
    // ...
  }       
}

我们可以看到,第一步需要构建出 context 对象,在编译 css 过程中用到的 cache 数据和 utils 方法都保存在这个对象里,然后将 context、root、result 一起传给 expandTailwindAtRules 方法,我们打个断点看下 context 里都有哪些属性:

image.png

我们挑几个重点的看下:

  • candidateRuleMap 是一个样式映射关系对象,里面存放了所有类名和样式的映射关系,在这里面我们可以找到我们在测试用例中设置的 text-md image.png

  • changedContent 存放我们要解析的 html 内容 image.png

  • tailwindConfig 存放 tailwindcss 的配置

src/lib/expandTailwindAtRules.js

export default function expandTailwindAtRules(context) {
  return async (root) => {
    let layerNodes = {
      base: null,
      components: null,
      utilities: null,
      variants: null,
    }

    // MARK: 识别引入了哪些 @tailwind 拓展
    root.walkAtRules((rule) => {
      if (rule.name === 'tailwind') {
        if (Object.keys(layerNodes).includes(rule.params)) {
          layerNodes[rule.params] = rule
        }
      }
    })

    if (Object.values(layerNodes).every((n) => n === null)) {
      return root
    }

    // ...

    // Find potential rules in changed files
    let candidates = new Set([...(context.candidates ?? []), sharedState.NOT_ON_DEMAND])
    let seen = new Set()

    // MARK: 这里选择解析 html 使用 rust 还是 js
    if (flagEnabled(context.tailwindConfig, 'oxideParser')) {
      let rustParserContent = []
      let regexParserContent = []

      for (let item of context.changedContent) {
        // MARK: 这里可以通过 config 自定义一些转换规则,默认是没有转换逻辑的
        let transformer = getTransformer(context.tailwindConfig, item.extension)
        // MARK: 用来提取 html 中的 class,可以自定义,默认是通过 rust 提取
        let extractor = getExtractor(context, item.extension)

        if (transformer === builtInTransformers.DEFAULT && extractor?.DEFAULT_EXTRACTOR === true) {
          rustParserContent.push(item)
        } else {
          regexParserContent.push([item, { transformer, extractor }])
        }
      }

      // MARK: 使用 rust 提取 html 中的 class
      if (rustParserContent.length > 0) {
        for (let candidate of parseCandidateStrings(
          rustParserContent,
          IO.Parallel | Parsing.Parallel
        )) {
          candidates.add(candidate)
        }
      }

      // MARK: candidates 为 rust 解析结果
      console.log('candidates ---> ', candidates)
    } else {
      // ...
    }

    // MARK: 生成 Rules
    generateRules(sortedCandidates, context)
    
    // MARK: 将 context.ruleCache 根据 layer 字段进行分类
    if (context.stylesheetCache === null || context.classCache.size !== classCacheCount) {
      context.stylesheetCache = buildStylesheet([...context.ruleCache], context)
    }
    
    // ...

    // MARK: 这里将 utilities 的 rules 赋值给了 root 对象
    if (layerNodes.utilities) {
      layerNodes.utilities.before(
        cloneNodes([...utilityNodes], layerNodes.utilities.source, {
          layer: 'utilities',
        })
      )
      layerNodes.utilities.remove()
    }
    
    // ...

    // MARK: root.nodes 就是最后生成的 postcss Rules 了
    console.log('root ---> ', root)
  }
}

代码有些长,我们提取下核心步骤

  1. 构建 layerNodes 对象,根据引入的css拓展来赋值不同的规则,比如这里我们引入了 @tailwind utilities,因此 layerNodes.utilities 引入了对应的 rules
  2. 创建 transformerextractor,这两个我们都可以自定义。默认的 transformer 不做任何转换,默认的 extractor 使用内置的 rust 算法
  3. transformerextractor 都使用默认方法的前提下,windicss 使用 rust 提取出 context.changedContent 数组中每一段 html 的类名,我们看下提取结果(我对 rust 实在不熟悉,里面源码就不写了,具体方法在 oxide/crates/node/index.jsimage.png
  4. 调用 generateRules 方法,根据 sortedCandidates 生成对应的 Rules 并挂载到 context.ruleCache 属性中
  5. context.ruleCache 根据 layer 字段进行分类,根据 layerNodes 筛选只留下用到的 Rules
  6. utilityNodes 挂载到 root.nodes 对象中,继续传递给 lightningCssPlugin 插件,我们看下最终生成的 root.nodes image.png

src/lib/generateRules.js

function generateRules(candidates, context) {
  let allRules = []
  let strategy = getImportantStrategy(context.tailwindConfig.important)

  // MARK: 循环遍历每个 candidate
  /**
   * 第一个 candidate 是 * ,看起来是匹配出了所有规则
   */
  for (let candidate of candidates) {
    if (context.notClassCache.has(candidate)) {
      continue
    }

    if (context.candidateRuleCache.has(candidate)) {
      allRules = allRules.concat(Array.from(context.candidateRuleCache.get(candidate)))
      continue
    }

    // MARK: 核心 context.candidateRuleMap 去匹配每个 candidate 生成 Declaration,赋值给 allRules 和 context.ruleCache
    let matches = Array.from(resolveMatches(candidate, context))

    if (matches.length === 0) {
      context.notClassCache.add(candidate)
      continue
    }

    context.classCache.set(candidate, matches)

    let rules = context.candidateRuleCache.get(candidate) ?? new Set()
    context.candidateRuleCache.set(candidate, rules)

    for (const match of matches) {
      let [{ sort, options }, rule] = match

      if (options.respectImportant && strategy) {
        let container = postcss.root({ nodes: [rule.clone()] })
        container.walkRules(strategy)
        rule = container.nodes[0]
      }

      let newEntry = [sort, rule]
      rules.add(newEntry)
      context.ruleCache.add(newEntry)
      allRules.push(newEntry)
    }
  }

  return allRules
}

generateRules 会遍历每一个 candidate,在 resolveMatches 方法中使用context.candidateRuleMap 去匹配每个 candidate 生成 Declaration,赋值给 allRulescontext.ruleCache

lightningCssPlugin

function lightningCssPlugin(_root, result) {
    // ...
    
    // MARK: 通过 postcss 的 toResult 生成样式字符串
    let intermediateResult = result.root.toResult({
      map: map ? { inline: true } : false,
    })
    
    // MARK: Lightning CSS 是一个用 Rust 编写的极快的 CSS 解析器、转换器和压缩器。
      let transformed = lightningcss.transform({
        filename: result.opts.from,
        code: Buffer.from(intermediateResult.css),
        minify: false,
        sourceMap: !!intermediateMap,
        targets: lightningcss.browserslistToTargets(browserslist(browsersListConfig)),
        drafts: {
          nesting: true,
          customMedia: true,
        },
        nonStandard: {
          deepSelectorCombinator: true,
        },
        include: Features.Nesting,
        exclude: Features.LogicalProperties,
      })

      // MARK: 生成最后的 css 代码
      let code = transformed.code.toString()

      result.root = postcss.parse(license() + code, {
        ...result.opts,
        map: intermediateMap,
      })
    // ...
}

终于到了最后一步~

  1. 通过 postcss 的 toResult 方法生成样式字符串
  2. 使用 lightningcss,对 css 进行一批操处理,比如「生成 sourceMap」、「自动增加厂商前缀」等
  3. 最终使用 code.toString() 方法生成终版样式字符串,挂载到 result.root 上输出

总体流程

我们用流程图的方式将整体流程串起来

tailwindcss.png

结语

有人说原子化 css 的出现是历史倒退,因为它看起来跟直接写 style 一样。从表面上确实如此,但能用如此看似原始的方式实现精美的界面,用「大道至简」来形容它再合适不过了~

原创文章,如有写错的地方欢迎指正~