背景
最近有个CRA(creata react app)的老项目,运行和打包太慢了,实在忍不了,打算改成用vite进行运行和打包。
问题
我们在原来的项目中用css modules来做css的模块化,vite中也提供了css.module的配置项
export default defineConfig(({ command, mode }) => ({
//...other options
css:{
modules:{
scopeBehaviour: "local",
generateScopedName: "[name]_[local]__[hash:base64:5]",
}
}
}))
但是实际上vite构建出来的classname和CRA构建的有点区别。如果你的css文件是以index.module.css命名的,那么CRA就会用css文件所在的文件夹名去作为classname的前缀,如果不是,就会以css文件名为前缀。
举个🌰: 有这么一个css文件
// footer/index.module.css
.wrapper {
//...
}
运行后,查看元素的classname,会发现CRA编译成footer_wrapper_xsdjh,vite编译成index-module_wrapper__sducx
再举个🌰:
// header/nav.module.css
.wrapper {
//...
}
运行后,查看元素的classname,会发现CRA编译成nav_wrapper__eihad,vite编译成nav-module_wrapper。
但如果说是新项目的话,这个不是啥问题,可是我们这个项目之前有客户通过classname去选择元素进行操作,这么一改的话,就可能影响到客户代码,这个是不能接受的。
查找原因
首先我们要知道,classname这一块的处理,肯定是通过loader去编译处理的,那么我们可以对比一下一下CRA和Vite是分别怎么配置的。
先在CRA项目里执行一下npm run eject 暴露出CRA的 webpack 配置,然后找一下webpack.config.js中关于cssModule的配置
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
mode: 'local',
getLocalIdent: getCSSModuleLocalIdent,
},
}),
},
关键配置是 getLocalIdent: getCSSModuleLocalIdent,那么getLocalIdent是干啥的,getCSSModuleLocalIdent又是干啥的呢,我们去看一下官方文档怎么说的
可以指定自定义
getLocalIdent函数的绝对路径,以基于不同的架构生成类名。 默认情况下,我们使用内置函数来生成 classname。 如果自定义函数返回null或者undefined, 我们将降级使用内置函数来生成 classname。
其实就是配置一个函数用来生成classname的,接下来我们再看下 getCSSModuleLocalIdent 是怎么生成classname的,从node_modules中的 react-dev-utils中找到 getCSSModuleLocalIdent.js:
const loaderUtils = require('loader-utils');
const path = require('path');
module.exports = function getLocalIdent(
context,
localIdentName,
localName,
options
) {
// Use the filename or folder name, based on some uses the index.js / index.module.(css|scss|sass) project style
// 原来是在这里判断了一下是否为index开头的,是的话就用folder
const fileNameOrFolder = context.resourcePath.match(
/index\.module\.(css|scss|sass)$/
)
? '[folder]'
: '[name]';
// Create a hash based on a the file location and class name. Will be unique across a project, and close to globally unique.
const hash = loaderUtils.getHashDigest(
path.posix.relative(context.rootContext, context.resourcePath) + localName,
'md5',
'base64',
5
);
// Use loaderUtils to find the file or folder name
const className = loaderUtils.interpolateName(
context,
fileNameOrFolder + '_' + localName + '__' + hash,
options
);
// Remove the .module that appears in every classname when based on the file and replace all "." with "_".
return className.replace('.module_', '_').replace(/\./g, '_');
};
解决问题
现在原因找到了,那么怎么解决呢?
是不是直接将这个 getCSSModuleLocalIdent 方法复制过去就可以呢?但是你会发现 getCSSModuleLocalIdent 这个方法的入参和vite的 generateScopedName 不一样呀,所以我们还需要改造一下。
先上代码:
import loaderUtils from "loader-utils";
//...
{
//...
css: {
modules: {
scopeBehaviour: "local",
generateScopedName: (name, filename, css) => {
const fileNameOrFolder = filename.match(
/index\.module\.(css|scss|sass)$/
)
? "[folder]"
: "[name]";
// Create a hash based on a the file location and class name. Will be unique across a project, and close to globally unique.
const hash = loaderUtils.getHashDigest(
filename.replace(__dirname, "") + name,
"md5",
"base32",
5
);
// Use loaderUtils to find the file or folder name
const className = loaderUtils.interpolateName(
{ resourcePath: filename },
fileNameOrFolder + "_" + name + "__" + hash
);
// Remove the .module that appears in every classname when based on the file and replace all "." with "_".
return className.replace(".module_", "_").replace(/\./g, "_");
},
},
}
- 首先判断是否为index开头的文件这里,需要将
context.resourcePath替换成 filename - 其次计算hash这里,需要安装一下
loaderUtils, 利用getHashDigest计算hash值。这里的话注意第一个参数,格式是css文件相对路径+classname,拿上面的css文件举个🌰: src/header/nav.module.csswrapper。我这里就直接通过replace替换来拿css文件的相对路径。
hash这里有个注意的点是,我将base64改成了base32,原因是base64生成的hash里会有 "/",导致无法生成对应的class样式,这个我也没搞懂为啥。。。需要再研究研究
- 最后生成classname,这里的话因为vite中是没有context的,看了一下
loaderUtils.interpolateName方法的实现,其实只要传个 { resourcePath: filename } 就可以了
TODO
到这里就基本上解决了最开始的问题。但这里还可以优化一下,可以将这个方法写成一个 vite plugin,并发布到npm上。
初学vite,如果有啥说错的地方,请多多见谅!!