神秘 Arco 样式出现,祭出 Webpack 解决预期外的引用问题

神秘 Arco 样式出现,祭出 Webpack 解决预期外的引用问题

Webpack是现代化的静态资源模块化管理和打包工具,其能够通过插件配置处理和打包多种文件格式,生成优化后的静态资源,核心原理是将各种资源文件视为模块,通过配置文件定义模块间的依赖关系和处理规则,从而实现模块化开发。Webpack提供了强大的插件和加载器系统,支持了代码分割、热加载和代码压缩等高效构建能力,显著提升了开发效率和性能。Webpack ResolveWebpack中用于解析模块路径的配置项,其负责告诉Webpack如何查找和确定模块的位置,核心功能是通过配置来定义模块的查找机制和优先级,从而确保Webpack能够正确地找到和加载依赖模块。

描述

先来聊聊故事的背景,在前段时间隔壁老哥需要将大概五年前的项目逐步开发重构新版本2.0,通常如果我们开发新版本的话可能会从零启动新项目,在新项目中重新复用组件模块,但是由于新项目时间紧任务重,并且由于项目模块众多且结构复杂,在初版规划中需要修改和新增的模块并非大多数,综合评估下来从零开发新版本的成本太高,所以最终敲定的方案是依然在旧版本上逐步过渡到新版本。

当然,如果只是在原项目上修改与新增模块也不能称为重构新版本,方案的细节是在原项目上逐步引入新的组件,而这些新的组件都是在新的package中实现的,并且以StoryBook的形式作为基本调试环境。这么做的原因首先是能够保证新组件的独立性,这样便可以逐步替换原有组件模块,还有一个原因是在这里新的组件还需要发布SDK包供外部接入,这样更便于我们复用这些组件。

那么这件事情本来是可以有条不紊地进行下去,在新组件开发的过程中也进行地非常顺利,然而在我们需要将新组件引入到原有项目中时,在这里便出现了问题,实际上回过来看这个问题并不是很复杂,但是在不熟悉Webpack以及Less的情况下,处理起来确实需要一些时间。恰逢周五,原本是快乐的周末,然而将这个问题成功解决便用了两天多一点的时间,在解决问题后也便有了这篇文章,当然解决的方案肯定不只是文章中提到的方法,也希望能为遇到类似问题的同学带来一些参考。

这个问题主要是出现在样式的处理上,五年前的项目对于一些配置确实已经不适合当前的组件模块。那么在发现问题之后,我们就要进入经典的排查问题阶段了,在历经检索异常抛出原因、排除法定位问题组件、不断生成并定位问题配置之后,最终决定在Webpack的层面上处理这些问题。实际上如果完全是我们自己的代码还好,如果不适配的话我们可以直接修改,问题就出现在组件引用的第三方依赖上,这些依赖的内容我们是没有办法强行修改的,所以我们只能借助Webpack的能力去解决第三方依赖的问题。综合来看,在文章中主要解决了下面三个问题:

  • less-loader样式引用问题: 因为是最低五年前的项目,对于Webpack的管理还是使用的旧版本的Umi脚手架,且less-loader5.0.0版本,而当前最新版本已经到了12.2.0,在这其中配置和处理方式已经发生了很大变化,所以这里需要解决less-loader的对于样式的处理问题。
  • 组件样式覆盖问题: 经常用组件库的同学应该都知道,在公司内部统一样式规范之后,是不能够随意再引入原本的样式内容的,否则就会出现样式覆盖的问题,但是发布到Npm的包并不一定都遵守了这个规范,其还是有可能引用旧版本的样式文件,因此我们就需要避免由此造成的样式覆盖问题。
  • 依赖动态引入问题: 实际上在我们解决上述的问题之后,关于样式部分的问题已经结束了,而在这里也引申出了新的问题,我们在本质上是处理了Webpack的模块引用问题,那么在其他场景下,例如我们需要在海外部署的服务引入专用的依赖,或者幽灵依赖造成的编译问题,此时就需要解决动态引入依赖的问题。

针对这三个问题分别使用Webpack实现了相关DEMO,相关的代码都在https://github.com/WindrunnerMax/webpack-simple-environment/tree/master/packages/webpack-resolver中。

LessLoader

那么我们先来看看less-loader的问题,当我们打开Npm找到[email protected]README文档时,可以看到webpack resolver一节中明确了如果需要从node_modules中引用样式的话,是需要在引用路径前加入~符号的,这样才能让less-loader能够正确地从node_modules中引用样式文件,否则都会被认为是相对路径导入。

@import "~@arco-design/web-react/es/style/index.less";

在我们的项目中,其本身的依赖是没有问题的,既然能够编译通过那么必然在.less文件的引入都是携带了~标识的,但是当前我们的新组件中引入的样式文件并没有携带~标识,这就导致了less-loader无法正确地解析样式文件的位置,从而抛出模块找不到的异常。如果仅仅是我们新组件中的样式没有携带标识的话,我们是可以手动加入的,然而经过排查这部分内容是新引入的组件导致的,而且还是依赖的依赖,这就导致我们无法直接修改样式引入来解决这个问题。

那么针对于这类问题,我们首先想到的肯定是升级less-loader的版本,但是很遗憾的是当升级到最新的12版本之后,项目同样跑不起来,这个问题大概是根某些依赖有冲突,抛出了一些很古怪的异常,在检索了一段时间这个错误信息之后,最终放弃了升级less-loader的方案,毕竟如果钻牛角尖的话我们需要不断尝试各种依赖版本,需要花费大量的时间测试,而且也不一定能够解决问题。

此时我们就需要换个思路,既然本质上还是less-loader的问题,而loader本质上是通过处理各种资源文件的原始内容来处理的,那么我们是不是可以在直接实现loader来在less-loader之前预处理.less文件,将相关样式的引用都加入~标识,这样就能够在less-loader之前将正确的.less文件处理好。那么在这里的思路就是在解析到引用.less文件的.js文件时,将其匹配并且加入~标识,这里只是简单表示下正则匹配,实际需要考虑的情况还会复杂一些。

/**
 * @typedef {Record<string, unknown>} OptionsType
 * @this {import("webpack").LoaderContext<OptionsType>}
 * @param {string} source
 * @returns {string}
 */
module.exports = function (source) {
   
   
  const regexp = /@import\s+"@arco-design\/web-react\/(.*)\/index\.less";/g;
  const next = source.replace(regexp, '@import "~@arco-design/web-react/$1/index.less";');
  return next;
};

理论上这个方式是没有问题的,但是在实际使用的过程中发现依然存在报错的情况,只不过报错的文件发生了改变。经过分析之后发现这是因为在.less文件中内部的样式引用是由less-loader处理的,而我们编写的loader只是针对于入口的.less文件做了处理,深层次的.less文件并没有经过我们的预处理,依然会抛出找不到模块的异常。实际上在这里也发现了之前使用less的误区,如果我们在.less文件中随意引用样式的话,即使没有被使用,也会被重复打包出来的,因为独立的.less入口最终是会生成单个.css再交予后续的loader处理。

/* index.ts ok */
/* import "./index.less"; */

/* index.less ok */
@import "@arco-design/web-react-pro/es/style/index.less";

/* @arco-design/web-react-pro/es/style/index.less error */
@import "@arco-design/web-react/es/Button/style/index.less";

在这种存在多级样式引用的情况下,我们处理起来似乎就只能关注less-loader本身的能力了,不过实际上这种情况还是不容易出现的,一般只有在复杂业务组件库引用或者多级UI规范的情况下才可能出现。但是既然已经在我们的项目中出现了就必须要解决,幸运的是less-loader本身是支持插件化的,我们可以通过实现less-loaderloader来处理这个问题,只不过因为文档并不完善,所以我们只能参考其他插件的源码来实现。

在这里我们就参考less-plugin-sass2less来实现,less-loader的插件实际上是一个对象,而在这个对象中我们可以定义install方法,其中第二个参数就是插件的管理器实例,通过在这里调用addPreProcessor方法来加入我们的预处理器对象,预处理对象实现process方法即可,这样就可以实现我们的less-loaderloader。而对于process函数的思路就比较简单了,在这里我们可以将其按照\n切割,在处理字符串时判断是否是相关第三方库的@import语句,如果是的话就将其加入~标识,并且由于这是在less-loader中处理的,其引用路径必然是样式文件,不需要考虑非样式的内容引用。同时为了增加通用性,我们还可以将需要处理的组件库名称在实例化对象的时候传递进去,当然由于是偏向业务数据处理的,通用性可以没必要很高。

// packages/webpack-resolver/src/less/import-prefix.js
module.exports = class LessImportPrefixPlugin {
   
   
  constructor(prefixModules) {
   
   
    this.prefixModules = prefixModules || [];
    this.minVersion = [2, 7, 1];
  }

  /**
   * @param {string} source
   * @param {object} extra
   * @returns {string}
   */
  process(source) {
   
   
    const lines = source.split("\n");
    const next = lines.map(line => {
   
   
      const text = line.trim();
      if (!text.startsWith("@import")) return line;
      const result = /@import ['"](.+)['"];?/.exec(text);
      if (!result || !result[1]) return line;
      const uri = result[1];
      for (const it of this.prefixModules) {
   
   
        if (uri.startsWith(it)) return `@import "~
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值