手上的工程非常古老,详细可看我的文章Vue2.6+Webpack4项目迁移至Rsbuild。迁移的过程中,老板明确用户分批迁移且迁移时间按月维度甚至更长。还提出必须要提供出问题后快速切回稳定环境的能力。因此,大刀阔斧修改现有代码的方案需要排除,需要采用同一套代码兼容新(Rsbuild)、旧(Webpack 4)两个构建工具的方案。
主要改造点
首先列举迁移到Rsbuild主要的改造点:
- Node版本从14升级到18+
Node Sass改为Dart Sass
Sass不当语法修改
升级Dart Sass 后存在部分不兼容语法,按照提示进行改造即可(详细可见上面的迁移文章)。此部分改造经测试Node Sass 和Dart Sass 均兼容。
/deep/迁移至::v-deep
最要命的部分来了。Node Sass与Dart Sass 有一个水火不容不可调和的语法,那就是穿透符号:Node Sass 使用/deep/ 而Dart Sass使用::v-deep。
如果同一套代码要适配两个不同的sass,那么只能从sass-loader 中入手:使用Rsbuild的Sass plugin在解析Sass内容时全局将代码中的/deep/ 替换为::v-deep 。
可以按照以下语句进行替换:
js
// rsbuild.config.js
import { defineConfig } from '@rsbuild/core';
import { pluginSass } from '@rsbuild/plugin-sass';
import { pluginVue2 } from '@rsbuild/plugin-vue2';
export default defineConfig({
plugins: [
pluginVue2(),
pluginSass({
sassLoaderOptions: {
additionalData: (content) => content.replace(/\/deep\//g, '::v-deep '),
},
}),
],
});
但是经测试,这个配置只能将.vue 文件中写在<style lang="scss"></style> 中的代码进行替换,通过下面这种形式引入的独立scss 文件中的代码替换不了,因而引发构建报错:
scss
// style/test.scss
/deep/ .deep-component {
color: red;
}
vue
<style scoped lang="scss">
@import "~@style/test.scss";
</style>
在上面的additionalData 方法中,此处语句打印出的content结果是:
js
// log
@import "~@style/test.scss";
所以独立文件内的Sass内容使用additionalData 方法替换是不生效的。
然后网上找了各种方法,包括但不限于使用string-replace-loader自定义插件替换Sass内容,都没有成功。
仔细想一想,sass-loader的解析流程:先将.vue 文件中的style 标签解析,然后再读取style 内部的@import 语句导入的内容进行合并。
那么,有没有方法,是在sass-loader读取@import 语句导入内容时运行的呢?答案是有,Sass Importer就介绍了自定义加载逻辑的方法。
由于Rsbuild 使用sass-embedded 且默认开启modern-compilerAPI,因此需要使用新的方法自定义:
js
// scss-replace-importer.js
import fs from "fs";
export const SCSSDeepReplaceFileImporter = (options) => {
return {
findFileUrl(url) {
const target = options.find(i => i.url === url)
if (target) {
const content = fs.readFileSync(target.path, "utf8");
const newContent = content.replace(/\/deep\//g, "::v-deep");
const cachePath = `.cache/${target.cacheName}`;
fs.writeFileSync(cachePath, newContent);
return new URL(`./${cachePath}`, import.meta.url);
}
return null;
},
}
};
js
import { dirname, resolve } from "node:path"
import { fileURLToPath } from "node:url";
import { defineConfig } from '@rsbuild/core';
import { pluginSass } from '@rsbuild/plugin-sass';
import { pluginVue2 } from '@rsbuild/plugin-vue2';
import { SCSSDeepReplaceFileImporter } from "./importer"
const __dirname = dirname(fileURLToPath(import.meta.url));
export default defineConfig({
resolve: {
alias: {
"@style": resolve(__dirname, "style")
}
},
plugins: [
pluginVue2(),
pluginSass({
sassLoaderOptions: {
additionalData: (content) => content.replace(/\/deep\//g, '::v-deep '),
sassOptions: {
importers: [
SCSSDeepReplaceFileImporter([
{
url: "~@style/test.scss",
path: resolve(__dirname, "style/test.scss"),
cacheName: "test.scss"
}
])
],
},
},
}),
],
});
此方法成功将预设的独立scss 文件中的/deep/ 替换成::V-deep ,项目成功运行。但是HMR模式下发现会一直运行hot reload。因为.cache 文件写入时会触发HMR的内容更新,导致一直循环运行hot reload。要解决这个问题,需要在Rsbuild 配置中将.cache 文件夹从HMR的文件监听列表中排除:
js
import { defineConfig } from '@rsbuild/core';
export default defineConfig({
tools: {
rspack: {
watchOptions: {
ignored: /\.cache/
}
}
},
});
这样会出现一个小问题:更改目标scss文件时不会触发HMR,只有引入此scss 文件的文件内容保存时才会触发。
不过对于工程迁移来说算是小事。新、旧两个构建器共存期一过,此importer就可以取消,直接将目标文件的/deep/ 语法修改即可。
插曲
远离Node Sass,要不然你的生活会变得不幸!!!
在发现自定义importer的方案之前,一直找不到好的方式解决此问题。曾经一度想采用升级node-sass@8 然后修改 Sass implementation的方式进行升级。
但是!但是!但是!node-sass@8在我的电脑上一直安装失败。我打定主意,不能让这坨垃圾继续祸害世人,一定要改,所以才出现本文。