react css module 踩坑

2,389 阅读2分钟

背景

最近有从 vue 转 react 的需求,刚学 react 就想着用官方提供 create-react-app 一把梭了。写到 css module 这一块就特别蛋疼(相对于 vue 来讲确实麻烦不少),然后从社区里得知 babel-plugin-react-css-modules 可以提供一些语法糖。由于不想 eject webpack 配置,就配合着 react-app-rewired,customize-cra 一顿折腾。 使用的版本信息和配置如下:

  • cra: 3.4.1
  • css-loader: 4.3.0
  • babel-plugin-react-css-modules: 5.2.6
// config-override.js
const {
  override,
  addWebpackAlias,
  addBabelPlugin,
  adjustStyleLoaders,
} = require("customize-cra");
const path = require("path");
module.exports = override(
  addWebpackAlias({
    "@": path.join(__dirname, "src"),
  }),
  addBabelPlugin([
    "react-css-modules",
    {
      exclude: "node_modules",
      context: __dirname,
      webpackHotModuleReloading: true,
      generateScopedName: "[name]_[local]_[hash:base64:5]",
      filetypes: {
        ".scss": {
          syntax: "postcss-scss",
        },
      },
    },
  ]),
  adjustStyleLoaders(({ use: [, css] }) => {
    if (css.options.modules) {
      css.options.modules = {
        localIdentName: "[name]_[local]_[hash:base64:5]",
        localIdentContext: __dirname,
      };
    }
  })
);

查看了下页面,css 并没有应用在页面上,搜了一下,基本都是围绕这 generateScopedNamelocalIdentName 以及 context 来解决问题。但是我基本都提供了,看来只能自己 debug 了。

排查

  1. 定位 DOM 元素,发现 styleName 有进行转换,格式也匹配提供 generateScopeName,那么 babel-plugin-react-css-modules 的配置应该生效了
  2. 定位生成的 CSS,查看类名,也有进行装换,格式也正确,那么 css-loader 的配置也应该生效了
  3. 对比两者生成的名字,居然 hash 部分是不一致的。说明他们是独立生成 hash 的(巨坑,一开始没仔细看 hash 值)
  4. localIdentNamegenerateScopedName 更改为 [path]_[name]_[local] 再次运行项目,发现 css 正常应用了。那么问题就出在 hash 值的生成逻辑了
  5. 根据这两个字段,分别在对应源码里检索使用的位置,分别定位到 css-loader/utilsdefaultGetLocalIdent。默认情况下 babel-plugin-react-css-modules 使用 generic-names 来处理 hash 值,继续定位生成代码位置
  6. 对比双方源码

image.png

image.png 啊,这。。。双方默认拼接 content 的逻辑不一样,中间拼接用 \x00+ 号的区别...

  1. 打断点验证一下,虽然最后都是用 loader-utils 提供的方法进行处理,但是两者提供的 content 不一样,导致最后的 hash 不一样
  2. css-loader 提供 getLocalIdent 方法,让它和 babel-plugin-react-css-modules 的规则一致,即可解决问题

最终代码如下

// config-override.js
const {
  override,
  addWebpackAlias,
  addBabelPlugin,
  adjustStyleLoaders,
} = require("customize-cra");
const path = require("path");
const genericName = require("generic-names");
const generateRule = "[name]_[local]_[hash:base64:5]";
const generateIdentName = genericName(generateRule, {
  context: __dirname,
});

module.exports = override(
  addWebpackAlias({
    "@": path.join(__dirname, "src"),
  }),
  addBabelPlugin([
    "react-css-modules",
    {
      exclude: "node_modules",
      context: __dirname,
      webpackHotModuleReloading: true,
      generateScopedName: generateRule,
      filetypes: {
        ".scss": {
          syntax: "postcss-scss",
        },
      },
    },
  ]),
  adjustStyleLoaders(({ use: [, css] }) => {
    if (css.options.modules) {
      css.options.modules = {
        localIdentContext: __dirname,
        getLocalIdent: (loaderContext, localIdentName, localName, options) => {
          const { resourcePath } = loaderContext;
          return generateIdentName(localName, resourcePath);
        },
      };
    }
  })
);

后续

我寻思着为啥同样的配置别人可以使用,轮到我就不行了,于是查看了下 css-loader 的迭代历史,emmm。 所以 css-loader v3.6.0 以前的版本两者配合着应该是能正常使用的。 image.png 总得来说,还是要有独立的 debug 能力才比较好解决问题,npm 包迭代比较快,网上提供的解决方案相对来讲比较滞后,遇到这个问题,建议还是自己 debug 一下。