Webpack 如何解析模块路径
你一定见过这些导入方式,无论是 ESM 还是 CommonJS 模块,或是其他模块规范。
import react from 'react'
import button from './components/button'
const path = require('path')
那么 webpack 是如何去解析查找它们的呢?
模块解析
resolver 是一个库(library),用于帮助找到模块的绝对路径。一个模块可以作为另一个模块的依赖模块,然后被后者引用。例如:
import foo from 'path/to/module'
所依赖的模块可以是来自应用程序或者第三方库(library)。resolver 帮助 webpack 找到 bundle 中需要引入的模块代码,这些代码在每个 import/require 语句中。
webpack 使用 enhanced-resolve 来解析文件路径。
解析规则
使用 enhanced-resolve 解析模块,支持三种形式:绝对路径、相对路径、模块路径。
1. 绝对路径
不建议使用。
由于已经取得文件的绝对路径,因此不需要进一步再做解析了。
在实际项目中,除了设置别名 resolve.alias 时采用绝对路径的方式,其他的我几乎没见过使用绝对路径的。(也可能我读的项目太少了)
import button from '/Users/frankie/component/button'
2. 相对路径
在这种情况下,使用 import/require 的资源文件(resource file)所在的目录被认为是上下文目录(context directory)。在 import/require 中给定的相对路径,会添加此上下文路径(context path),以产生模块的绝对路径(absolute path)。
import button from './component/button'
3. 模块路径
上面两种方式,应该没有太多理解难度,而模块名才是我们要重点理解的。
直接引入模块名,首先查找当前文件目录,若查找不到,会继续往父级目录一个一个地查找,直至到项目根目录下的 node_modules 目录(默认)。若再查找不到,则会抛出错误。
import 'react'
import 'module/lib/file'
注意:
- 默认的
node_modules
可以通过 resolve.modules 进行更改。- 查找中会根据 resolve.extensions 自动补全扩展名,默认是
['.wasm', '.mjs', '.js', '.json']
。- 查找中会根据 resolve.alias 替换掉别名。
模块将在 resolve.modules 中指定的目录内搜索。可以通过 resolve.alias 配置创建一个别名来替换初始模块路径。
一旦上述规则解析路径之后,解析器(resolver)将检查路径是否指向文件或目录。
-
指向文件
- 如果路径具有文件扩展名,则被直接打包。
- 否则,将使用 resolve.extensions 选项作为文件扩展名来解析。
-
指向目录
按以下步骤找到具有正确扩展名的文件:
- 如果文件夹中包含 package.json 文件,则按顺序查找 resolve.mainFields 配置选项中指定的字段,并且 package.json 中的第一个这样的字段确定文件路径。
- 如果 package.json 文件不存在或者 package.json 文件中 main 字段没有返回一个有效路径,则按顺序查找 resolve.mainFields 配置选项中指定的文件名,看是否能在 import/require 目录下匹配到一个存在的文件名。
- 文件扩展名通过 resolve.extensions 选项采用类似的方法进行解析。
若使用 webpack-dev-server 3.x 版本,建议不要随意修改 resolve.mainFields 配置项,它会报错。已确认是 webpack-dev-server 的 bug,将在不久要发布的 4.x 版本修复。详请 #2801
解析与缓存
Loader 解析遵循与文件解析器指定的规则相同的规则。resolveLoader 配置选项可以用来为 Loader 提供独立的解析规则。
每个文件系统访问都被缓存,以便更快触发对同一文件的多个并行或者串行请求。在观察模式下,只有修改过的文件会从缓存中摘出。如果关闭观察模式,在每次编译前清理缓存。
Resolve 配置
该选项用于配置模块如何解析。例如,当在 ES6 中调用 import 'lodash'
,resolve
选项能够对 webpack 查找 lodash
的方式去做修改。
1. resolve.alias
创建 import 或 require 的别名,来确保模块引入变得更简单。
例如,一些位于 src/ 文件夹下的常用模块:
// webpack.config.js
const path = require('path')
module.exports = {
//...
resolve: {
alias: {
// 可以是绝对路径,或者是相对路径。
// 据我不完全观察,结合 path 模块和 __dirname 拼接成“绝对路径”的方案更多。
// 以下为模糊匹配
Utilities: path.resolve(__dirname, 'src/utilities/'),
Templates: path.resolve(__dirname, 'src/templates/')
}
}
}
现在,你可以这样使用别名了:
import Utility from '../../utilities/utility'
// 别名
import Utility from 'Utilities/utility'
也可以在给定的对象的键后的末尾添加 $
,以表示精准匹配。这里不展开赘述,详细请看这里。
注意,采用别名引入模块时,先替换后解析。先将模块路径中匹配
alias
中的key
替换成对应的value
,再做查找。
2. resolve.extensions
自动解析确定的扩展。
// webpack.config.js
module.exports = {
//...
resolve: {
// 使用此选项,会覆盖默认数组,默认值:['.wasm', '.mjs', '.js', '.json']。
// 注意不要少了符号(.),有些人配置不成功,就是因为少了它。
// 从左到右(从上到下)先后匹配扩展名,选项中没有的后缀,是不会自动补全的。
extensions: ['.js', '.json']
}
}
3. resolve.modules
告诉 webpack 解析模块时应该搜索的目录。可以是绝对路径或者相对路径,但是它们之间有一点差异。
通过查看当前目录以及祖先路径(即 ./node_modules
,../node_modules
等等),相对路径将类似于 Node 查找 node_modules
的方式进行查找。
当使用绝对路径,将只在给定目录中搜索。
// webpack.config.js
const path = require('path')
module.exports = {
//...
resolve: {
// 默认值
modules: ['node_modules']
// 添加一个目录到模块搜索目录,此目录优先于 node_modules 搜索。
modules: [path.resolve(__dirname, 'src'), 'node_modules']
}
}
一般地,不要去更改该选项。
4. resolve.mainFields
当从 npm 包中导入模块时(例如,import * as D3 from 'd3'
),此选项将决定在 package.json
中使用哪个字段导入模块。根据 webpack 配置中指定的 target 不同,默认值也会有所不同。
// webpack.config.js
module.exports = {
//...
resolve: {
// 不建议修改
// target 为 webworker、web 或没有指定时,默认值为:
mainFields: ['browser', 'module', 'main'],
// 除去上述几个 target,对于其他任意 target(包括 node),默认值为:
mainFields: ['browser', 'module', 'main']
}
}
通常情况下,模块的
package.json
都不会声明browser
或module
字段,所以便是使用main
了。(该选项同样不建议更改)
5. resolve.mainFiles
解析目录时要使用的文件名。
当目录中没有 package.json
时,结合 resolve.extensions 来指明使用该目录中哪个文件。
// webpack.config.js
module.exports = {
//...
resolve: {
// 默认值
// 可添加多个,但不建议修改。
mainFiles: ['index']
}
}
尽可能地,不要去修改该选项。因为它同样会影响第三方依赖包解析,可能会导致部分第三方包解析错误。例如,我在验证该配置时,就发现 webpack-dev-server v3 的一个 bug,开发者表示将在 v4 版本中修复。
所以,不建议随意修改的配置包括 modules、mainFields、mainFiles。
6. 更多
它还有其他一些配置项,但比较少用,所以不展开赘述。更多请看这里。
ResolveLoader 配置
从 webpack 2 开始,在配置 loader 时强烈建议使用全名。例如 example-loader
,以尽可能地清晰。
然而,如果你确实想省略 -loader
,也就是说只使用 example
,则可以使用 resolveLoader.moduleExtensions 此选项来实现:
// webpack.config.js
module.exports = {
//...
resolve: {
// ...
}
resolveLoader: {
moduleExtensions: ['-loader']
}
}
我使用 webpack 4 在不配置该选项时,假如将
css-loader
省略为css
,会报错提示找不到 loader。为什么我会单独拿出来介绍一下,因为网上很多文章表示在配置 module.rules 时可以省略-loader
,但我是省略了就不行。所以这里补充一下原因。
小技巧
关于 webpack 默认配置可以从 node_modules/webpack/lib/WebpackOptionsDefaulter.js
查看。