taoliujun / blog

https://taoliujun.github.io/blog/
https://taoliujun.github.io/blog/
0 stars 0 forks source link

AST 在 css module 自动匹配中的应用 #21

Open taoliujun opened 1 year ago

taoliujun commented 1 year ago

AST 在 css module 自动匹配中的应用

我们已经非常明白为什么在项目中要使用 css modules,可直接说到“自动匹配”有点莫名其妙,所以有必要介绍下为什么提出这个问题。

第一步,我想用官方的话来镇住大家。

css modules的官方描述:A CSS Module is a CSS file in which all class names and animation names are scoped locally by default.

官方只提供了一个规范的描述,本身并未提供工具去实现。但无需担心,常见的打包工具都支持了 css modules。比如 webpack 的css-loader

css-loader

css-loader 在很早就支持 modules,只需要配置modules属性,程序员们使用它的方式是各种各样的。有设置 local modules,在 css 文件中使用:global来支持全局样式的;有设置 global modules,在 css 文件中使用:local来支持局部样式的。这些都是可以的,但是都需要在 css 文件中做一些特殊的标记,这样就会导致 css 文件的可读性变差。

后来人们学会了配置根据 css 文件名中是否包含.modules..global.字符串来启用/禁用该文件的 modules 功能。于是项目里有大量的.modules.css/.global.css 文件,而通过文件名去匹配某个功能的开关却恰恰是程序设计的忌讳。

符合直觉的自动识别

我就在 *.modules.css 文件堆中浑浑噩噩的度过了好多年,直到近两年的某一天在UmiJS脚手架中发现了这个奇特的细节。

使用import xxx from './styles.css'启用 modules,使用import './styles.css'启用 global。

这思路对我来说简直是惊为天人,无关乎它的技术细节。因为在直觉中,import module就是要直接执行它,而import xxx from module就是要使用它的某些东西。对于 css 是同样的,既要直接引入全局样式,又要使用局部样式。

技术细节

webpack 的 module rules 支持使用resourceQuery去匹配模块文件名的 query 部分,所以为 css-loader 增加一个 rules 如下:

{
  test: /\.css$/,
  oneOf: [
    {
      resourceQuery: /modules/,
      use: [
        { loader: 'style-loader' },
        { loader: 'css-loader', options: { modules: true } },
      ],
    },
    {
      use: [
        { loader: 'style-loader' },
        { loader: 'css-loader' },
      ],
    },
  ],
}

如此,使用import xxx from './styles.css?modules'就会命中第一个规则而启用 modules 功能了。

但显然这样写太麻烦了,有什么办法让它在写import xxx from './styles.css'时候,自动加上?modules而启用 css modules 呢?

答案就是使用 AST。由于项目几乎都在使用 babel 去处理 js 文件,所以可以直接使用 babel 插件去分析 AST 从而给 css 文件加上?modules后缀即可。就直接看umijs 的插件代码:

import * as Babel from "@umijs/bundler-utils/compiled/babel/core";
import * as t from "@umijs/bundler-utils/compiled/babel/types";
import { extname } from "path";

const CSS_EXT_NAMES = [".css", ".less", ".sass", ".scss", ".stylus", ".styl"];

export default function () {
  return {
    visitor: {
      ImportDeclaration(path: Babel.NodePath<t.ImportDeclaration>) {
        const {
          specifiers,
          source,
          source: { value },
        } = path.node;
        if (specifiers.length && CSS_EXT_NAMES.includes(extname(value))) {
          source.value = `${value}?modules`;
        }
      },

      // more codes
    },
  };
}

如上代码中,AST specifiers 里有内容,说明是import xxx from module的形式,而source.value就是 css 文件的路径,所以直接加上?modules后缀即可。

参考

taoliujun commented 1 year ago

如果想使用umijs提供的该插件,安装 @umijs/babel-preset-umi,在babel配置中引入:

plugins: [
    [
      require('@umijs/babel-preset-umi/dist/plugins/autoCSSModules')
    ]
  ],