Webpack 本质上是一个模块化打包工具,通过“万物皆模块”的设计思想,巧妙地实现了整个前端项目的模块化。
模块化
- Node.js 中的 CommonJS:值拷贝
- 浏览器中的 ES Modules:值引用
ES modules 模块系统存在的问题:
- 浏览器环境兼容问题
- 模块化划分出来的模块文件过多,每个文件需要单独从服务器请求回来,导致频繁地发送网络请求
-
模块打包工具
具备编译代码能力,解决环境兼容问题
- 将分散的模块再打包到一起,解决浏览器频繁请求模块文件问题
- 支持不同种类的前端模块类型,是的可以拥有一个统一的模块化方案
核心特性
Loader
通过 Loader 实现特殊资源加载
// ./webpack.config.js
module.exports = {
entry: './src/main.css',
output: {
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.css$/, // 根据打包过程中所遇到文件路径匹配是否使用这个 loader
use: 'css-loader' // 指定具体的 loader
}
]
}
}
通过 JS 加载资源模块
所有资源的加载都是由 JS 代码去控制,使得 webpack 后期只需要维护 JS 代码这一条线即可
开发 Loader
每个 webpack 的 Loader 都需要导出一个函数,这个函数就是对资源的处理过程,输入参数是加载到的资源文件内容,输出就是加工后的结果。
// ./webpack.config.js
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.md$/,
// 直接使用相对路径
use: './markdown-loader'
}
]
}
}
<!-- ./src/about.md -->
# About
this is a markdown file.
// ./src/main.js
import about from './about.md'
console.log(about)
// 希望 about => '<h1>About</h1><p>this is a markdown file.</p>'
// ./markdown-loader.js
const marked = require('marked')
module.exports = source => {
// 加载到的模块内容 => '# About\n\nthis is a markdown file.'
// 1. 将 markdown 转换为 html 字符串
console.log(source)
const html = marked(source)
// html => '<h1>About</h1><p>this is a markdown file.</p>'
// 2. 将 html 字符串拼接为一段导出字符串的 JS 代码
const code = `module.exports = ${JSON.stringify(html)}`
return code
// code => 'export default "<h1>About</h1><p>this is a markdown file.</p>"'
}
webpack 加载资源文件的过程类似于一个工作管道,依次使用多个 Loader,最终这个管道结束后的结果必须是一段标准的 JS 代码字符串
插件机制
Webpack 插件机制的目的是为了增强 Webpack 在项目自动化构建方面的能力
常见的使用场景:
- 实现自动在打包之前清除 dist 目录(上次的打包结果):clean-webpack-plugin
- 自动生成应用所需要的 HTML 文件:html-webpack-plugin
- 根据不同环境为代码注入类似 API 地址这种可能变化的部分;
- 拷贝不需要参与打包的资源文件到输出目录:copy-webpack-plugin
- 压缩 Webpack 打包完成后输出的文件;
- 自动发布打包结果到服务器实现自动部署。
开发插件
钩子机制
// ./remove-comments-plugin.js
class RemoveCommentsPlugin {
apply (compiler) {
compiler.hooks.emit.tap('RemoveCommentsPlugin', compilation => {
// compilation => 可以理解为此次打包的上下文
for (const name in compilation.assets) {
if (name.endsWith('.js')) {
const contents = compilation.assets[name].source()
const noComments = contents.replace(/\/\*{2,}\/\s?/g, '')
compilation.assets[name] = {
source: () => noComments,
size: () => noComments.length
}
}
}
})
}
}
运行机制与核心工作原理
- 通过 Loader 处理特殊类型资源的加载,例如加载样式、图片
- 通过 Plugin 实现各种自动化的构建任务,例如自动压缩、自动发布
具体来看打包的过程,Webpack 启动后,会根据我们的配置,找到项目中的某个指定文件(一般这个文件都会是一个 JS 文件)作为入口。然后顺着入口文件中的代码,根据代码中出现的 import(ES Modules)或者是 require(CommonJS)之类的语句,解析推断出来这个文件所依赖的资源模块,然后再分别去解析每个资源模块的依赖,周而复始,最后形成整个项目中所有用到的文件之间的依赖关系树。
有了这个依赖关系树过后, Webpack 会遍历(递归)这个依赖树,找到每个节点对应的资源文件,然后根据配置选项中的 Loader 配置,交给对应的 Loader 去加载这个模块,最后将加载的结果放入 bundle.js(打包结果)中,从而实现整个项目的打包。
- Webpack CLI 启动打包流程;
- 载入 Webpack 核心模块,创建 Compiler 对象;
- 使用 Compiler 对象开始编译整个项目;
- 从入口文件开始,解析模块依赖,形成依赖关系树;
- 递归依赖树,将每个模块交给对应的 Loader 处理;
- 合并 Loader 处理完的结果,将打包结果输出到 dist 目录。