概述
本质
JavaScript 应用程序的静态模块打包器
核心
加载器(Loader)机制
工作流程
- 配置初始化 webpack 会首先读取配置文件,执行默认配置
- 编译前准备 webpack 会实例化 compiler,注册 plugins、resolverFactory、hooks。
- reslove 前准备 webpack 实例化 compilation、NormalModuleFactory 和 ContextModuleFactory
- reslove 流程解析文件的路径信息以及 inline loader 和配置的 loader 合并、排序
- 构建 module runLoaders 处理源码,得到一个编译后的字符串或 buffer。将文件解析为 ast,分析 module 间的依赖关系,递归解析依赖文件
- 生成 chunk 实例化 chunk 并生成 chunk graph,设置 module id,chunk id,hash 等
- 资源构建 使用不同的 template 渲染 chunk 资源
- 文件生成 创建目标文件夹及文件并将资源写入,打印构建信息
基本使用
安装
yarn add webpack webpack-cli --dev
配置
// package.json
"scripts": { "build": "webpack"
}
// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin') // 用于访问内置插件
module.exports = { mode: 'development', // 指定打包时的工作模式 entry: './src/main.js', // 指定 webpack 打包入口文件 output: { // 打包后,打包结果的输出路径 filename: 'bundle.js', // 输出文件的名称 path: path.join(__dirname, 'output'), // 输出文件所在的目录,必须是绝对路径 publicPath: 'dist/', // 打包后文件的发布路径,默认为空,即项目的根目录 }, module: { // 指定打包所使用到的 loader rules: [ // 配置文件编译规则 { test: '/\.txt$/', use: 'raw-loader' } ] }, plugins: [ // 指定使用到的插件,需要配合 require 引入 new HtmlWebpackPlugin({template: './src/index.html'}) ]
}
打包
yarn webpack
工作模式
production(生产模式)
- 默认模式
- 会自动启动优化,对代码进行压缩、编译
- 打包结果代码会极大的丢失可读性
development(开发模式)
- 会自动优化打包速度
- 打包时,会自动添加一些调试过程中需要的辅助到代码中
none
- 运行最原始状态的打包,不会对代码做任何额外的处理
资源加载
Webpack 默认只能编译 JS 文件,会将所有文件都当成 JS 来解析编译。要编译打包其他非 JS 类型的文件,需要安装引入对应的解析模块,否则便会报错
加载方式
- 遵循 ES Modules 标准的 import 声明
import tools from './tools.js'
import icon from './icon.png'
import './main.css'
- 遵循 CommonJS 标准的 require 函数
const tools = require('./tools.js').default
const icon = require('./icon.png')
require('./main.css')
- 遵循 AMD 标准的 define 函数和 require 函数
define(['./tools.js', './icon.png', './main.css'], (tools, icon) => {})
require(['./tools.js', './icon.png', './main.css'], (tools, icon) => {})
- CSS 样式代码中的 @import 指令和 url 函数
- HTML 代码中图片标签的 src 属性
Loader(资源加载器)
作用
- 负责资源文件从输入到输出的转换
- 支持链式传递
- 对于同一个资源可以一次使用多个 loader
- 多个 loader 按有后往前的顺序执行,即先执行的 loader 应该排在后边
- 用于对模块源码的转换,因为 webpack 本身只支持 js 处理,loader 描述了 webpack 如何处理非 javascript 模块,并且在 build 中引入这些依赖
常用加载器
- 编译转换类
- 将加载的资源模块转化成 JS 代码模块
- 文件操作类
- 会将导入的文件拷贝到输出的路径
- 代码检查类
- 统一代码风格
- 一般不会自主修改代码
文件资源加载器
// 安装
yarn add file-loader --dev
// 导入文件
import icon from './icon.png'
const img = new Image()
img.src = icon
document.body.append(img)
// 配置 webpack.config.js
module: { rules: [ { test: /.png$/, use: 'file-loader' } ]
}
URL 资源加载器
- Data URLs
- 特殊的 URL 协议,可以用来直接表示一个文件的内容
- 引用时不会发起 http 请求
- url-loader
- 可以将(图片)文件转化成 Data URL,从而直接在 JS 中导入
- 可针对小文件(小于10K)使用,减少请求的次数
- 大文件(大于10K)则应单独提取存放,提高加载速度
data:text/html;charset=UTF-8,html content
data:image/png;base64,iVBodfasfjl...jfoasfe
// 安装 url-loader
yarn add url-loader --dev
// 配置 webpack.config.js
module: { rules: [ { test: /.png$/, use: { loader: 'url-loader', options: { limit: 10 * 1024 // 10KB,即只将小于 10KB 的文件进行转化 } } } ]
}
Plugin(插件)
本质
一个函数或者一个包含 apply 方法的对象。
工作机制
Plugin 通过在生命周期的钩子中挂载函数实现扩展
作用
增强 Webpack 自动化能力,解决其他自动化工作,如:清除 dist 目录、拷贝静态文件至输出目录、压缩输出代码等。
常用插件
-
自动清除输出目录
// 安装
yarn add clean-webpack-plugin --dev
// 配置 webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = { plugins: [ new CleanWebpackPlugin() ]
}
-
自动生成使用 bundle.js 的 HTML
// 安装
yarn add html-webpack-plugin
// 配置 webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = { plugins: [ // 用于生成 index.html new HtmlWebpackPlugin({ title: 'Webpack Plugin Sample', // 设置导出的 html 的 title meta: { viewport: 'width=device-width' }, template: './src/index.html' // 设置导出的模板文件 }), // 用于生成 about.html new HtmlWebpackPlugin({ filename: 'about.html' // 设置生成的文件的文件名 }) ]
}
-
复制静态文件
// 安装
yarn add copy-webpack-plugin --dev
// 配置 webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = { plugins: [ new CopyWebpackPlugin([ 'pulic' // 需要复制的文件所在的目录 ]) ]
}
自定义插件案例
class MyPlugin { apply (compiler) { console.log('MyPlugin 启动') // 将插件挂载到钩子上 compiler.hooks.emit.tap('MyPlugin', compilation => { // compilation => 可以理解为此次打包的上下文 for (const name in compilation.assets) { if (name.endsWith('.js')) { // 对 js 文件进行操作 // 获取文件的内容 const contents = compilation.assets[name].source() const withoutComments = contents.replace(/\/\*\*+\*//g, '') // 匹配注释 compilation.assets[name] = { source: () => withoutComments, // 返回处理结果的内容 size: () => withoutComments.length // 返回处理结果的大小,必须 } } } }) }
}
// 配置 webpack.config.js
module.exports = { plugins: [ new MyPlugin() ]
}
Dev Server
Webpack Dev Server 是由 Webpack 官方开发的工具,将自动编译和自动刷新的功能集成在一起
安装
yarn add webpack-dev-server --dev
使用
yarn webpack-dev-server
// 自动唤醒浏览器 ===>
yarn webpack-dev-server --open
原理
- 监听文件变化
- 当文件变化时,重新执行打包
- 将打包后的文件暂存再内存中,而不写入磁盘
- 内部的 http server 直接从内存中读取文件并发送给浏览器,从而减少不必要的磁盘读写操作,大大提高构建效率
配置
// webpack.config.js
module.exports = { devServer: { // 静态资源访问 contentBase: './public' // 指定静态资源目录 // 代理 API proxy: { '/api': { // http://localhost:8080/api/users => https://api.github.com/ target: 'https://api.github.com' pathRewrite: { // 配置代理路径重写规则 '^/api': '' // 将代理路径中的指定字符串替换掉 }, // 不能使用当前主机名(如:localhost:8080)作为请求的主机名 changeOrigin: true } } }
}
Source Map
作用
定位代码中错误信息的位置。
因为打包后运行的代码与开发的源代码之间存在较大差异,而调试和报错都是基于运行代码执行的,如此一来就很难定位到错误信息的位置。所以,就需要 Source Map 来描述结果代码和开发源码之间的对应关系。
配置
// webpack.config.js
module.exports = { devtool: 'source-map'
}
模式
- eval
- 是否使用 eval 执行模块代码
- cheap
- Source Map 是否包含行信息
- module
- 是否能够得到 Loader 处理之前的源代码
devtool | build | rebuild | production | quality (lo: lines only) |
---|---|---|---|---|
(none) | fastest | fastest | yes | bundled code |
eval | fastes | fastest | no | generated code |
cheap-eval-source-map | fast | faster | no | transformed code (lo) |
cheap-module-eval-source-map | slow | faster | no | original source (lo) |
eval-source-map | slowest | fast | no | original source |
cheap-source-map | fast | slow | yes | transformed code (lo) |
cheap-module-source-map | slow | slower | yes | original source (lo) |
inline-cheap-source-map | fast | slow | no | transformed code (lo) |
inline-cheap-module-source-map | slow | slower | no | original source (lo) |
source-map | slowest | slowest | yes | original source |
inline-source-map | slowest | slowest | no | original source |
hidden-source-map | slowest | slowest | yes | original source |
nosources-source-map | slowest | slowest | yes | without source content |
HMR(模块热更新)
Dev Server 在自动编译后 ,会自动刷新页面。
但是,在页面自动刷新后,会导致原有的页面状态丢失。如,自动刷新前测试输入的内容的丢失。
配置
// 方案一
webpack-dev-server --hot
// 方案二 webpack.config.js
const webpack = require('webpack')
module.exports = { plugins: [ new webpack.HotModuleReplacementPlugin() ]
}
示例
// main.js
const editor = createEditor()
document.body.appendChild(editor)
const img = new Image()
img.src = background
document.body.appendChild(img)
// 使用手动热更新的 API,会替换原有的处理方案,不会再自动刷新
if(module.hot) { // 判断是否已开启 HMR,避免 API 报错 // js 模块热替换 let lastEditor = editor module.hot.accept('./editor', () => { // 手动处理人更新后的处理方式 console.log('editor 模块更新了') const value = lastEditor.innerHTML document.body.removeChild(lastEditor) const newEditor = createEditor() newEditor.innerHTML = value document.body.appendChild(newEditor) lastEditor = newEditor }) // 图片热替换 module.hot.accept('./better.png', () => { img.src = background console.log(background) })
}
注意事项
- 手动处理的代码中出现错误时,会自动回退使用自动刷新,从而导致无法看到错误信息
- 没启用 HMR 的情况下,HMR API 报错
- webpack 打包时会自动去除没有意义的代码
关于样式文件热更新问题
因为样式文件是通过 loader 处理的,在 style loader 中就会自动执行热更新。样式文件更新后只需要把对应的更新替换到原本的文件中。所以,样式文件的热更新是开箱即用的,不需手动处理。
生产环境优化
配置方案
- 配置文件根据环境不同导出不同的配置
// webpack.config.js
module.exports = (env, argv) => { const config = { // 开发环境配置 } if (env === 'production') { config.mode = 'production' config.devtool = false config.plugins = [ ...config.plugins, new CleanWebpackPlugin() new CopyWebpackPlugin(['public']) ] } return config
}
- 一个环境对应一个配置文件
- webpack.common.js
- webpack.dev.js
- webpack.prod.js
DefinePlugin
通过伪代码注入全局成员
// webpack.config.js
const webpack = require('webpack')
module.exports = { mode: 'none', entry: './src/main.js', output: { filename: 'bundle.js' }, plugins: [ new webpack.DefinePlugin({ API_BASE_URL: '"https://api.example.com"' }) ]
}
// main.js
console.log(API_BASE_URL)
Tree Shaking
// 配置 webpack.config.js
module.exports = { optimization: { // 集中配置 webpack 中的优化功能 useExports: true, // 是否只导出被外部使用的成员 minimize: true, // 是否移除、压缩掉未被使用的成员代码 }
}
Tree Shaking 的作用在于去除代码中未被引用的部分,从而减轻代码的重量。
Tree Shaking 并不特指某一个配置选项,而是在 production 模式下会自动启动的一组具有优化效果的功能的搭配只用。
Tree Shaking 的实现是存在限制性的。实现 tree shaking 的前提是 ESM,即由 Webpack 打包的代码必须使用 ESM 模式。而且,在同时使用 babel-loader 时,由于低版本的 babel 可能会会先将模块转化成 CommonJS,从而导致 Tree Shaking 无法实现
合并模块
// webpack.config.js
module.exports = { optimization: { concatenateModules: true, // 是否将所有模块合并到同一个函数中 }
}
又称为作用域提升(Scope Hoisting)。即将所有的模块尽可能地合并输出到一个函数中,提升运行效率的同时,减小代码的体积。
代码分割
-
多入口打包
// webpack.config.js
optimization: { splitChunks: { chunks: 'all' // 提取公共模块,组成一个单独的文件 }
},
plugins: [ new HTMLWebpackPlugin({ title: 'Multi Entry', template: './src/index.html', filename: 'index.html', chunks: ['index'] }), new HTMLWebpackPlugin({ title: 'Multi Entry', template: './src/album.html', filename: 'album.html', chunks: ['album'] })
]
-
动态导入
// 在引用时才执行导入
const render = () => { const hash = window.location.hash || '#posts' const mainElement = document.querySelector('.main') mainElement.innerHTML = '' if (hash === '#posts') { // 满足条件则导入、使用 /* 魔法注释,通过 webpackChunkName 给分包进行命名,若输出注释同名,则会被打包到一个文件中 */ import(/* webpackChunkName: 'posts' */'./posts/posts').then(({ default: posts }) => { mainElement.appendChild(posts()) }) }
}
MiniCssExtractPlugin
将 CSS 提取打包到单独的文件当中,在通过 link 标签的方式注入到输出结果的 html 中
// 安装
yarn add mini-css-extract-plugin
// 配置 webpack.config.js
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = { optimization: { // 一旦配置,则会覆盖默认的压缩模式,需要重新手动配置 minimizer: [ new OptimizeCssAssetsWebpackPlugin(), // 压缩输出的 css 文件 new TerserWebpackPlugin() ] }, module: { rules: [ { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, 'css-loader' ] } ] }, plugins: [ new MiniCssExtractPlugin() ]
}
输出文件名 Hash
添加 hash 名,可以在每次模块内容更改时得到一个新的包含 hash 值的文件名。
生产环境下,当系统识别到新的文件名时,就会重新发送文件请求,能有效避免缓存的问题。
// 项目级
// 项目中任意位置有改动,都会触发整个项目的重置、更新
module.exports = { output: { filename: '[name]-[hash].bundle.js' }
}
// 目录级
// 目录下的文件有改动,则触发整个文件夹中的文件重置、更新
filename: '[name]-[chunkhash].bundle.js'
// 文件级
// 只更新有改动过的文件
filename: '[name]-[contenthash].bundle.js'