Rsbuild迁移之node-sass引发的血案

手上的工程非常古老,详细可看我的文章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在我的电脑上一直安装失败。我打定主意,不能让这坨垃圾继续祸害世人,一定要改,所以才出现本文。

相关推荐
明月_清风37 分钟前
Deno 终局来了:从挑战 Node 到被 Cloudflare 收编
前端·后端·node.js
Csvn2 小时前
框架性能优化
前端
回眸&啤酒鸭3 小时前
【回眸】OpenSwarm 多智能体协作系统实战指南
大数据·前端·人工智能
用户69371750013843 小时前
2026,程序员的时代拐点到了
android·前端·后端
大龄秃头程序员3 小时前
一次 iBeacon + BLE 无感解锁方案的实现记录
前端
小兔子4 小时前
Python 的 GIL 与 free-threading:3.13 之后「去 GIL」走到哪一步了
前端
IT_陈寒4 小时前
SpringBoot自动配置差点让我加班到凌晨
前端·人工智能·后端
guslegend5 小时前
脚手架原理与本地调试:从 bin 软链接到 npm link
前端·npm·node.js·脚手架·前端工程化
小宋10215 小时前
A2UI从零实战:Agent安全生成交互界面与事件回传
javascript·人工智能·安全·交互
大包子5 小时前
structuredClone vs lodash.cloneDeep:核心差异与选型指南
javascript·vue.js