一行配置替换antd中moment为dayjs(vite和webpack均适用)

5,828 阅读8分钟

直接上代码

resolve: {
    alias: {
      'rc-picker/es/generate/moment': 'rc-picker/es/generate/dayjs'
    }
}

resolve即webpack或者vite的配置文件中的resolve字段。

温馨提示:即使使用上面的配置成功引入了dayjs(或者你使用了官方推荐的方式引入了dayjs),如果要给antd的时间相关组件传递dayjs类型的默认值,请务必保证这个dayjs默认值对应的dayjs版本与rc-picker的package.json中配置的dayjs版本一致。rc-picker是antd的一个dependence。原因见下文。

本文中所针对的antd版本为:4.18.9

TLDR

不想看那么多,可以直接拖最后看看总结。

先看看官方替代方案

官方替代moment的文档为: ant.design/docs/react/…

文档中主要介绍了两种方法,一种是自己定义组件,一种是适用webpack的插件。这个文档对我来说,有以下不解的地方:

  • 为什么antd要默认使用moment?

  • 为什么使用文档中的方式可以替换掉moment?

关于这些,文档中并没有答案。此处不得不再次赞扬尤雨溪写文档的方式,在教你如何用之前先告诉你为什么。

关于antd为什么要默认使用moment我没有去研究,但是我研究了下为什么按照文档的方式可以替换moment。对原理感兴趣的同学请继续往下看。

起因

话说项目中使用react+antd已经大半年了,当我在使用antd的Table组件的时候,发现文档中的使用方式并不能达到效果,后面发现是文档的版本和我使用的antd版本不一致。而我去查看了老版本的文档,发现里面并没有介绍使用方式。最后我不得不选择升级antd

对三方依赖无脑升级并不是一个好的选择,很多三方库并不会保证会向后兼容,就算它保证了,也有可能它一时疏忽没有测试到位而导致升级产生新问题。

升级以后,大概进行了下冒烟测试,发现一切正常。心里顿时佩服起antd来,大厂就是大厂,稳定性有保证。

直到有一天在项目中使用了DatePicker,发现只要点击时间选择框就会报错clone.weekday is not a function

微信截图_20220307173728.png

看图中的报错是dayjs的某个实例调用了一个不存在的weekday方法。

我立即去看了下webpack的配置,发现webpack是使用了antd-dayjs-webpack-plugin这个插件替换moment为dayjs的。而我清楚记得以前使用这个插件的时候是没有报错的,很有可能就是升级antd导致的bug。

此时我是非常信任antd以及antd-dayjs-webpack-plugin这个插件的,我相信是我的使用方式除了问题。

然后我做了以下的事情:

  1. 翻阅antd如何替换moment的文档,看看是否有更新。结论是并没有更新,依然是以前的替换方式。

  2. 查看了antd-dayjs-webpack-plugin的github,看看是否有更新说明,发现这个插件已经几个月未更新了,而且issues里也没找到类似我说的这种情况,就算找到了,issues已经很久没有官方的人回复过了。

此时我开始对antd有一些怀疑态度了。

最后我选择了解决npm问题的最佳方式,那就是,重装大法好!果然,重装后,问题解决,报错消失!

如果是其他库,到这里故事可能就结束了,可是,它是antd啊,它是蚂蚁出品的,我那么信任它,它怎么能出问题呢?

刨根问底

既然antd文档里没找到为什么按照它说的方法可以替换到moment,那就自己看吧。

首先看了下antd-dayjs-webpack-plugin的源码,通过它我大概明白了为什么能够替换掉moment了。

插件主要搞了如下事情:

  • 它给webpack加了一个resolve.alias的配置,把moment模块的引用指向了dayjs模块:

源码地址:github.com/ant-design/…


 // set dayjs alias
    if (this.replaceMoment) {
      const { alias } = compiler.options.resolve
      if (alias) {
        alias.moment = 'dayjs'
      } else {
        compiler.options.resolve.alias = {
          moment: 'dayjs'
        }
      }
    }
    

这样webpack在遇到有模块引用moment的时候,会转而去引用dayjs。

  • 然后它通过直接注入js代码的方式给dayjs安装了一些插件。
if (this.plugins) {
  const { entry, module } = compiler.options;

  const initLoaderRule = {
    test: /init-dayjs-webpack-plugin-entry\.js$/,
    use: [
      {
        loader: path.resolve(__dirname, "./init-loader.js"),
        options: {
          plugins: this.plugins,
        },
      },
    ],
  };

  if (module.rules) {
    module.rules.push(initLoaderRule);
  } else {
    compiler.options.module.rules = [initLoaderRule];
  }

  const initFilePath = path.resolve(
    __dirname,
    "init-dayjs-webpack-plugin-entry.js"
  );
  const initEntry = require.resolve(initFilePath);

  compiler.options.entry = makeEntry(entry, initEntry);
}

以上代码的意思是给webpack的entry加了一个"init-dayjs-webpack-plugin-entry.js",这样webpack就会去加载这个js文件,然后插件又专门写了一个叫做init-loader.js的loader来解析"init-dayjs-webpack-plugin-entry.js"。而init-loader.js主要干的事情就是注入js代码:

源码在:github.com/ant-design/…

const { getOptions } = require("loader-utils");

module.exports = function loader(source) {
  const options = getOptions(this);

  options.plugins.forEach((plugin) => {
    source += `var ${plugin} = require('dayjs/plugin/${plugin}');`;
  });

  options.plugins.forEach((plugin) => {
    source += `dayjs.extend(${plugin});`;
  });

  // special plugin
  source += `var antdPlugin = require('antd-dayjs-webpack-plugin/src/antd-plugin');dayjs.extend(antdPlugin);`;

  return source;
};

至此,插件干的事情就完毕了,它产生了如下效果,这个效果非常重要,请一定知道

webpack编译后的boundle包里会包含一段下面的代码(有所省略,主要就是引入dayjs然后给dayjs安装各种插件)

var dayjs = __webpack_require__(/*! dayjs/dayjs.min */ "./node_modules/.pnpm/registry.npmmirror.com+dayjs@1.10.7/node_modules/dayjs/dayjs.min.js");
var isSameOrBefore = __webpack_require__(/*! dayjs/plugin/isSameOrBefore */ "./node_modules/.pnpm/registry.npmmirror.com+dayjs@1.10.7/node_modules/dayjs/plugin/isSameOrBefore.js");
dayjs.extend(isSameOrBefore);
//...省略部分安装其他插件的代码

这个插件干的事情,就是注入了这么一段代码,然后使用alias把对moment的引用改成了dayjs然后我们就可以愉快的使用dayjs了。

看起来好像没有问题,antd以前引用的moment,现在被webpack变成了引用dayjs,而且插件也是安装了的,为啥就报错了呢?等等,好像哪里不对?

webpack仅仅引入了项目根目录中package.json里配置的dayjs指向的版本且为这个版本安装了插件,那antd万一不是引用的这个版本呢?

是的,我们升级antd后,导致antd引用的dayjs的版本和antd-dayjs-webpack-plugin插件引入的dayjs的版本不一致。虽然通过插件antd最终确实引用了dayjs而非moment,但是引用的并不是同一个版本,antd引用的dayjs仅仅是一段核心代码,没有安装任何插件,因此会产生上面的报错。

antd是如何引用moment的

具体来说,antd中引用moment是通过antd引用rc-picker,然后rc-picker又引用了moment。即使我们项目中未在package.json中配置对dayjs的依赖,antd引用的rc-picker也会安装dayjs的依赖,只是,这个依赖仅仅能够被rc-picker引用到。这导致了这个严重的问题:

一旦rc-picker引用的dayjs和antd-dayjs-webpack-plugin引用的dayjs不是同一个版本,antd-dayjs-webpack-plugin插件就会导致antd中使用dayjs的组件报错

而我的项目中,第一次配置时使用了antd-dayjs-webpack-plugin引入了1.10.7版本的dayjs,antd此时也是引用的1.10.7版本(因为rc-picker里package.json中对dayjs的dependence配置为:"^1.8.30",初始化项目的时候dayjs最高版本为1.10.7)。由于版本一致,webpack编译后antd引用到的dayjs为antd-dayjs-webpack-plugin处理后的dayjs,这样就不会有任何问题。

而升级了antd以后,antd引用的dayjs被升级到了1.10.8,此时插件处理的是1.10.7版本的dayjs,导致antd引用的dayjs只有核心代码没有任何插件,所以就报错了。

这时可以解释文章开头的配置是如何生效的了

看看antd是如何引用moment的,

源码在:github.com/ant-design/…

import momentGenerateConfig from 'rc-picker/lib/generate/moment';

打开rc-picker的源码,发现在rc-picker/lib/generate目录里有对应的dayjs的实现,也就是rc-picker/es/generate/dayjs模块。

微信截图_20220307194325.png

那既然如此直接让antd引用这个现成的dayjs模块不就行了么,于是就有了文章开头的配置了。

总结一下下

  • 首先这个配置虽然简单,但是不能对dayjs的插件进行tree-shaking,这个配置根本是让rc-picker使用了它自带的dayjs引入,从而引入了weekday、localeData、weekOfYear、weekYear、advancedFormat、customParseFormat插件。这些插件有些是antd必须的,有些应该并不是。(antd应该没有用到rc-picker的所有功能。不过加上插件也不大,引入后问题不是很大)

  • 即使使用官方引入dayjs的方式,也请一定注意文章开头的温馨提示,保证你给antd的dayjs对象的版本与它自己引用的那个dayjs的版本一致(这个截止今天在官方文档里并没有特意指出,是一个坑)。

对antd的一点期望

  • 希望官方文档可以明确告诉用户在传入默认的dayjs对象时需要注意dayjs版本问题

  • 希望官方文档可以告诉使用者为什么antd会使用moment,以及antd是如何使用的moment。然后再告诉读者为什么使用官方提供的插件或者替换代码可以替换掉moment。当前官方提供的两个替换方案,根本没有告诉用户应该如何去选择,从而给用户造成较大的心智负担。

  • 最后是否可以改变一下当前antd引入moment的方式,从而让替换moment变得更加简单,因为这真的是一个非常常用的需求。