前言

相信有过一些前端开发经验的同学都有遇到过使用的npm包有bug,或者npm包只要再修改一点点就能满足自己的需求这样尴尬的情况。如果给包作者提需求,作者一般也不会马上给你修改,这时候就需要使用各种修改npm包源码的骚操作了。

通常有四类做法:

  • 单个文件修改法
  • 拷贝覆盖法
  • 修改引用法
  • 整个项目copy法
  • 直接引用法
  • 发布私包法
  • 外部代码修改法
  • 最优解patch-package大法

先直接讲最优解吧

patch-package是一个用来给其他npm包打补丁的包,实际原理也是在本工程保存一份修改的代码,只不过不是用全量代码的形式保存,而是保存了git diff的结果,节省了代码体积

用法如下:

  • npm i -S patch-package安装patch-package
  • 直接在node_modules下修改需要修改的包源码
  • 执行npx patch-package 包名, patch-package会将当前node_modules下的源码与原始源码进行git diff,并在项目根目录下生成一个patch补丁文件
  • 后续只要执行npx patch-package命令,就会把项目patches目录下的补丁应用到node_modules的对应包中,这个执行时机一般可以设置为postinstall这个勾子
"scripts": {
    "postinstall": "patch-package"
}



为什么是最优解?
毕竟是专门团队开发的工具,在能实现目的的前提下,该考虑的都帮你考虑好了,比如这个包的版本升级了怎么办,你会发现如果你装的包版本和你之前生成的补丁中记录的版本不一样,npx patch-package会直接报错**ERROR** Failed to apply patch for package xxxx at path,另外使用git diff来记录补丁比起重写一份源码的方法更节省空间,即安全,又便捷。

为什么还需要其他方法?
其他方法存在的原因大部分是因为使用者不知道patch-package的存在,基于自己对代码的理解自创的方法,虽然有这样那样的缺点,但在大部分场景下也是没问题的,由于缺少封装,反而使得方法更能体现node运行机制,对初学的开发者还是有一定帮助的。

单文件修改法

之前用过的方法之一,原理是先找到要修改的npm包的文件,先把这个文件拷贝一份到项目目录下,修改,然后只要想办法让这个文件最终被使用就行了

  • 拷贝覆盖法
    还是用postinstall这个勾子,在这个勾子执行cp 修改过的文件 ./node_modules/包名/原始文件拷贝过去,最终node_modules下的文件就变成了修改后的文件了
    例如:
    想修改lodash中的array方法,array-hack.js是被修改后的js文件,现在想用这个文件替换原始文件,只需在package.json加入
"scripts": {
    "postinstall": "cp ./array-hack.js ./node_modules/lodash/array.js"
}



即在每次install包后执行用修改后文件覆盖原始文件逻辑

  • 修改引用法
    配置一个webpack alias别名,如'原始文件的引用路径': '修改后文件的引用路径',使得最终修改后的文件被引用
    例如:
    与上面同样的情景,只需在webpack配置中加入
resolve: {
      alias: {
          'lodash/array': path.resolve(__dirname, '../array-hack.js'),
      }
  },



整个项目copy法

也是有同事用过的方法,将需要修改的包的项目源码整个拷贝下来,进行修改,然后使用

  • 直接引用法
    人家完整源码已经有了,直接用就是了,不用npm包了
  • 发布私库法
    适合一个npm包几个项目在用的场景,可以把修改后的源码发布到私有的npm仓库上,供项目使用,这样多个项目就只需要修改一次源码

外部代码修改法

这个方法就是不直接修改node_modules的源码,而是利用js特性,在执行时,修改这个包的内部属性,达到目的
简单来说就是利用defineProperty、prototype等特性修改包内的类
例如:
近期在项目中发现大量使用的设备判断包xxxuser-agent竟然有bug,部分场景下判断异常
则在代码执行最开始阶段,执行以下代码



const ua = require('xxxuser-agent').default;
Object.defineProperty(ua, 'isMobilePhone', {
  get() {
      return ua.isMobile && !ua.isIPad && !ua.isWindows && !ua.isMac;
  },
  enumerable: true,
  configurable: true,
});



总结

尽管这些方法都很骚,但始终无法解决修改源码怕升级的问题,如果该npm包升级,可能会导致原先的修改产生错误,所以用了这些骚方法,最好是把包的版本号写死。
唯有patch-package的方法有版本校验,可以防止包被误升级时无人发现,因此还是优先使用这个方法,其余方法仅