JavaScript 应用程序的静态模块打包器
加载器(Loader)机制
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
Webpack 默认只能编译 JS 文件,会将所有文件都当成 JS 来解析编译。要编译打包其他非 JS 类型的文件,需要安装引入对应的解析模块,否则便会报错
import tools from './tools.js'
import icon from './icon.png'
import './main.css'
const tools = require('./tools.js').default
const icon = require('./icon.png')
require('./main.css')
define(['./tools.js', './icon.png', './main.css'], (tools, icon) => {})
require(['./tools.js', './icon.png', './main.css'], (tools, icon) => {})
// 安装
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'
}
]
}
data:text/html;charset=UTF-8,<h1>html content</h1>
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 的文件进行转化
}
}
}
]
}
一个函数或者一个包含 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()
]
}
Webpack Dev Server 是由 Webpack 官方开发的工具,将自动编译和自动刷新的功能集成在一起
yarn add webpack-dev-server --dev
yarn webpack-dev-server
// 自动唤醒浏览器 ===>
yarn webpack-dev-server --open
// 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 来描述结果代码和开发源码之间的对应关系。
// webpack.config.js
module.exports = {
devtool: 'source-map'
}
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 |
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)
})
}
因为样式文件是通过 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.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)
// 配置 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())
})
}
}
将 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 值的文件名。
生产环境下,当系统识别到新的文件名时,就会重新发送文件请求,能有效避免缓存的问题。
// 项目级
// 项目中任意位置有改动,都会触发整个项目的重置、更新
module.exports = {
output: {
filename: '[name]-[hash].bundle.js'
}
}
// 目录级
// 目录下的文件有改动,则触发整个文件夹中的文件重置、更新
filename: '[name]-[chunkhash].bundle.js'
// 文件级
// 只更新有改动过的文件
filename: '[name]-[contenthash].bundle.js'