乐趣区

一个webpack构建速度优化误区

问题描述

项目中使用了一个 npm 包 a。前几天一直用得好好的,突然某次拉了别的分支代码后,就出 Bug 了。

第一反应是别人把这个包的版本变了。查看了下项目的 package.jsonpackage-lock.json 文件,该模块和依赖模块的信息并没有改变,node_modules/a中的版本信息也和 package.json 中的对应。

一下子没了头绪,只好到 node_modules 中去调试一下。

TL;DR;

拉到最后看总结 XD

node_modules 目录结构

项目中 node_modules 目录如下:

node_modules
│
└───a
│   │   index.js
|   |   ...
│   │
│   └───node_modules
│       │   ...
│       └───c
|           |   index.js
|           |   ...
│   
└───c
    │   index.js
    │   ...

从该目录结构中可以发现,模块 a 的目录下还有一个 node_modules 目录,这个目录里放的是模块 a 的依赖。眼尖的同学可能发现了,项目本身的 node_modules目录和 a 模块的 node_modules 目录中都有安装了模块 c。这是为什么呢?

原因有 2 个:

  1. 项目直接依赖了模块 c
  2. 项目没有直接依赖模块 c,但是项目直接依赖的模块 b 中依赖了模块 c,并且和 a 中依赖的模块 c 版本不兼容。

我们的项目中并没有直接引用模块 c,所以是第二种情况。

npm 的模块安装机制

本节主要解释为什么项目没有直接依赖模块 c,却会把 c 安装在项目的 node_modules 目录下。不感兴趣的同学可以直接跳过。

假设项目依赖了模块 a 和模块 b,模块 a 依赖模块 c 的 1.0.0 版本,模块 b 依赖模块 c 的 2.0.0 版本。

npm2

在 npm2 的时候,使用嵌套的方式来安装模块,c 模块分别被安装到 a 和 b 模块的 node_modules 目录中。

这种方式虽然简单,但是却会导致 node_modules 中存在大量相同的模块。想象一下,如果模块 a 和模块依赖的模块 c 都是 1.0.0 版本,使用这种方式就会产生冗余的模块。

npm3

到 npm3 的时候,npm2 中产生冗余模块的情况得到改善。npm 安装模块时会尽量把模块安装到最外层的 node_modules 目录中,让模块能够尽量被复用。

  1. 安装模块时,如果外层 node_modules 目录中没有同名模块,就将其安装到最外层 ndoe_modules 目录中
  2. 如果外层 node_modules 目录中已经存在了同名模块,并且 版本兼容,则不再安装(使用时直接使用外层模块)
  3. 如果外层 node_modules 目录中已经存在了同名模块,并且 版本不兼容,则安装在父模块的 node_modules 目录中

上述情况的安装模块如图

引用了错误的模块

node_modules/a/node_modules/c/index.js 中加了一些 log,发现居然没执行!?

到这一步,要么是 log 的位置没写对,要么是没有引入这个模块。确认了 webpack 配置中的 resolve.mainFields 属性和模块 c 的 package.json 文件信息后,排除了第一种可能。

Tips: resolve.mainFields属性用来告诉 webpack,引入一个 npm 模块时,如何找到这个模块的入口。

这时候已经有点懵了,引用模块时不是先从当前目录下的 node_modules 目录中开始一级一级向上查到吗?说到向上查找,那便到上一级的 node_modules 目录中去试一试。

果然!引用的是最外层 node_modules 中的模块 c!

难道 webpack 查找模块的方式和 Node.js 不一样吗?还是因为 webpack 的某些配置导致的?

使用如下 webpack 配置来构建,发现并没有存在上述问题。

const path = require('path')
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, './dist/js')
  },
};

那接下来只要排查到底是哪些 webpack 配置影响到模块检索。查看项目中的 webpack 配置,和模块检索相关的只有 resolve 属性。

const config = {
  resolve: {
    modules: [path.resolve(projectDir, 'src'),
        path.resolve(projectDir, 'node_modules'),
        path.resolve(imtPath, 'node_modules'),
    ],
    // es tree-shaking
    mainFields: ['jsnext:main', 'browser', 'main'],
    alias: {},
    extensions: ['.jsx', '.js'],
  }
}

所幸配置不多,对着 webpack 文档查一下,很快便找到了问题:resolve.modules中使用了绝对路径。以下为 webpack 文档原文:

告诉 webpack 解析模块时应该搜索的目录。

绝对路径和相对路径都能使用,但是要知道它们之间有一点差异。

通过查看当前目录以及祖先路径(即 ./node_modules, ../node_modules 等等),相对路径将类似于 Node 查找 ‘node_modules’ 的方式进行查找。

使用绝对路径,将只在给定目录中搜索。

上述 webpack 配置中,path.resolve(projectDir, 'node_modules')为项目的 node_modules 目录。这样配置的原因,是因为想要优化模块检索的速度,结果却导致了这么严重的 Bug。

根据 webpack 文档,就是因为这个绝对路径导致了 Bug。那么只要把这个绝对路径换成node_modules,Bug 便解决了。

总结

npm 在安装模块时,会优先将包安装在 node_modules 目录的最外层,除非有冲突才会安装到父模块下的 node_modules 中。而 webpack 配置中的 resolve.modules 设置成项目 node_modules 目录的绝对路径时,会导致 webpack 在查找 node_modules 目录时,只在最外层目录查找,忽略掉更深层次的同名模块。这与默认的查找策略“优先使用深层模块”相反,导致构建时使用了错误的 npm 包。

退出移动版