在模块化编程中,开发者将程序分解为功能离散的文件,并称之为模块。
Node.js 从一开始就支持模块化编程,目前大多数主流浏览器已支持 ESM 模块化。
一、Webpack 模块与解析原理
1. Webpack 模块
能在 Webpack 工程化环境里成功导入的模块,都可以视作 Webpack 模块。与 Node.js 模块相比,Webpack 模块能以各种方式表达它们的依赖关系。如:
- ES6 的
import语句import a from './a' - CommonJS 的
require语句const a = require('./a') - AMD 的
define和require语句define([], function () { return () => {} }) require(['./a.js', './b.js'], function (a, b) {}) - css/sass/less 的
@import语句@import './a.less' - stylesheet 的
url(...)或者 HTML 的<img src=...>.box-bg { background-image: url("./a.png"); }
Webpack 默认支持以下模块类型:
- ECMAScript 模块
- CommonJS 模块
- AMD 模块
- Assets
- WebAssembly 模块
通过 loader 可以使 Webpack 支持其他语言和预处理器语法编写的模块,loader 向 Webpack 描述了如何处理非原生模块,并将相关依赖引入到你的 bundles 中。如:
- TypeScript
- Sass、Less
- JSON、YAML
2. compiler 与 Resolvers
-
compiler
在使用
webpack命令打包的时候,相当于执行了以下代码:const webpack = require('webpack') const compiler = webpack({ // ... 这是配置的 webpackconfig 对象 })webpack命令的执行会返回一个compiler对象,它描述了 Webpack 打包编译的整个流程。它内置了一个打包状态,随着打包过程的进行,状态会实时变更,同时触发对应的 Webpack 生命周期钩子。每一次
webpack打包,就是创建一个compiler对象,走完整个生命周期的过程。 -
Resolvers
Webpack 中所有关于模块的解析,都是依靠
compiler对象里的Resolvers(内置模块解析器)去工作的。Resolvers的主要功能就是解析模块,它是基于enhanced-resolve这个包实现的。无论使用怎样的模块引入语句,本质其实都是在调用这个包的 api 进行模块路径解析。
Webpack 模块解析简易原理图示:
二、模块解析(resolve)
Webpack 通过 Resolvers 实现了模块之间的依赖和引用。如:
import _ from 'lodash'
// 或
const add = require('./utils/add')
所引用的模块可以是来自应用程序的代码,也可以是第三方库。Resolvers 帮助 Webpack 从每个 require 或 import 语句中,找到需要引入到 bundle 中的模块代码。当打包模块时,Webpack 使用 enhanced-resolve 来解析文件路径。
1. Webpack 中的模块路径解析规则
通过内置的 enhanced-resolve,Webpack 能解析三种文件路径:
-
绝对路径
import '/home/me/file' import 'C:\\Users\\me\\file'已经获得文件的绝对路径,不需要再做进一步解析。
-
相对路径
import '../utils/reqFetch' import './styles.css'使用
import或require引入的资源文件所处的目录,会被认为是上下文目录。在import或require中给定的相对路径,enhanced-resolve会拼接此上下文路径,来生成模块的绝对路径:path.resolve(__dirname, RelativePath)。 -
模块路径
import 'module' import 'module/lib/file'在
resolve.modules中指定的所有目录检索模块(node_modules 里的模块已经被默认配置了),也可以通过配置别名(resolve.alias)的方式来替换初始模块路径。
2. resolve.alias
通过 resolve.alias 可以自定义配置模块路径。
假设有文件 src/index.js、src/utils/add.js,在 index.js 中使用以下方式引入 add.js 会报错:
import add from '@/utils/add'
因为 Webpack 会将其当做一个模块路径来识别,所以无法找到 @ 这个模块。这时,我们配置一下 resolve:
// webpack.config.js
const path = require('path')
module.exports = {
resolve: {
alias: {
"@": path.resolve(__dirname, 'src') // 将 src 配置为一个模块路径,并起别名为 @
},
},
}
这样做的好处是,在项目任何文件中引入 src 目录中的资源,不用再通过相对路径去引入,换句话说不用写一堆 ../,而是直接通过 @ 就可以访问到 src 目录。
3. resolve.extentions
以上代码,通过 import add from '@/utils/add',就可以引入 add.js 文件。如果 utils 目录中同时存在 add.js、add.json、add.wasm 文件,那么引入的会是哪个文件呢?答案是仍然会引入 add.js。
如果把 add.js 文件删掉,又会引入什么文件呢?这就要看 resolve.extentions 是如何配置的了:
module.exports = {
resolve: {
extensions: ['.js', '.json', '.wasm'],
},
}
Webpack 会按照数组顺序去解析这些后缀名。也就是说,如果把 add.js 删掉,默认就会引入 add.json 文件。
三、外部扩展(Externals)
有时我们为了减小 bundle 的体积,把一些不变的第三方库用 cdn 的形式引入进来,比如 jQuery:
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
此时我们想在代码里使用引入的 jQuery,但似乎三种模块引入方式都不行,这时候怎么办呢?Webpack 给我们提供了 Externals 的配置属性,让我们可以配置外部扩展模块:
module.exports = {
externals: {
jquery: 'jQuery',
// 或者
// jquery: '$',
},
}
此时就可以在代码中引入 jQuery 并使用:
import $ from 'jquery'
console.log($)
如果不想在页面通过 script 标签手动引入,还可以通过以下方式配置外部扩展模块:
module.exports = {
externalsType: 'script',
externals: {
jquery: ['https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js', 'jQuery']
},
}
四、依赖图(dependency graph)
当一个文件依赖另一个文件时,Webpack 会将文件视为存在依赖关系。这使得 Webpack 可以获取非代码资源,如图片或字体等,并把它们作为依赖提供给应用程序。
当 Webpack 开始工作时,它会根据我们写好的配置,从入口(entry)开始递归地构建一个依赖关系图,这个依赖关系图包含着应用程序中所需要的每个模块,然后将所有模块打包为 bundle(output)。
这里有一些 bundle 分析工具,可以帮助你查看打包产物的依赖关系:
- webpack-chart:webpack stats 可交互饼图;
- webpack-visualizer:可视化并分析你的 bundle,检查哪些模块占用空间,哪些可能是重复使用的;
- webpack-bundle-analyzer:一个 plugin 和 CLI 工具,它将 bundle 内容展示为一个便捷的、交互式、可缩放的树状图形式;
- Bundle optimize helper:这个工具会分析你的 bundle,并提供可操作的改进措施,以减少 bundle 的大小;
- bundle-stats:生成一个 bundle 报告(bundle 大小、资源、模块),并比较不同构建之间的结果。
使用 webpack-bundle-analyzer:
npm install --save-dev webpack-bundle-analyzer
// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
当执行 npx webpack,会自动在浏览器中打开下面的链接,可以查看每个模块的依赖关系及其他信息。