快速上手
-
yarn init --yes
初始化项目 -
yarn add webpack webpack-cli --dev
安装核心模块和cli -
yarn webpack --version
查看当前webpack版本 -
yarn webpack
打包项目内的JS文件
配置文件
- 默认打包路径为
src/index.js -> dist/main.js
修改配置文件
//webpack.config.js
const path = require('path')
module.exports = {
mode:'development',//修改工作模式,默认为production,其余可选参数为none
entry: './src/main.js',//入口文件的路径和文件名
output: {
filename: 'bundle.js',//导出的文件名
path: path.join(__dirname, 'output')//导出的路径,必须为绝对路径
}
}
资源模块加载
- yarn add css-loader --dev
将css文件进行打包 - yarn add style-loader --dev
载入样式
修改配置文件
//webpack.config.js
module.exports = {
//修改入口文件为css文件
entry: './src/main.css',
//添加module字段
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
//loader执行顺序为从后往前执行
]
}
]
}
}
导入资源模块
在JS文件载入当前模块需要的样式文件更符合webpack的设计理念
import './heading.css
文件资源加载器
- yarn add file-loader --dev
安装资源加载器
修改配置文件
module.exports = {
output: {
//当资源文件不在网站的根目录时需要修改路径
publicPath: 'dist/'
},
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
//新增资源加载器
{
test: /.png$/,
use: 'file-loader'
}
]
}
}
import icon from './icon.png'
const img = new Image()
img.src = icon
document.body.append(img)
webpack URL加载器
Data URLs
data:image/png;base64,iVBORw0KGgoAAAANSUhE...SuQmCC
//协议
data:
//媒体类型和编码
image/png;base64,
//文件内容
iVBORw0KGgoAAAANSUhE...SuQmCC!~
- yarn add url-loader --dev
安装URL加载器
建议小文件使用Data URLs,减少请求次数。 大文件单独提取存放,提高加载速度
const path = require('path')
module.exports = {
module: {
rules: [
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 将10 KB以下的文件采用Data URL的形式进行打包
}
}
}
]
}
}
常用加载器分类
- 编译转换类
将加载到的资源模块转换成JS代码,例如css loader - 文件操作类
把加载到的资源文件复制到输出目录,把访问的文件路径向外导出 - 代码检查类
把加载到的资源文件进行校验,统一风格提高代码质量
webpack与ES2015
- yarn add babel-loader @babel/core @babel/preset-env --dev
安装babel加载器,核心文件,转换插件的集合
const path = require('path')
module.exports = {
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
}
webpack加载资源的方式
- 遵循 ES Modules 标准的 import 声明
- 遵循 CommonJS 标准的 require 函数
- 遵循 AMD 标准的 define 函数和 require 函数
- 样式代码中的 @import 指令和 url 函数
@import url(reset.css);
- HTML 代码中图片标签的 src 属性
yarn add html-loader --dev
安装html加载器
const path = require('path')
module.exports = {
module: {
rules: [
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
attrs: ['img:src', 'a:href']
}
}
}
]
}
}
webpack常用插件
clean-webpack-plugin
清空缓存文件
yarn add clean-webpack-plugin --dev
//webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
plugins: [
new CleanWebpackPlugin()
]
}
html-webpack-plugin
用于生成html文件
yarn add html-webpack-plugin --dev
可以用过多个实例对象来生成多个html文件
//webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins: [
// 用于生成 index.html
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample',
meta: {
viewport: 'width=device-width'
},
template: './src/index.html'
}),
// 用于生成 about.html
new HtmlWebpackPlugin({
filename: 'about.html'
})
]
}
页面内也可以访问插件的配置参数
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Webpack</title>
</head>
<body>
<div class="container">
<h1><%= htmlWebpackPlugin.options.title %></h1>
</div>
</body>
</html>
copy-webpack-plugin
复制文件
yarn add copy-webpack-plugin --dev
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
plugins: [
new CopyWebpackPlugin([
// 'public/**'
'public'//指定需要复制的目录
])
]
}
增强webpack开发体验
自动编译
webpack的监视模式,修改内容后自动重新打包
yarn webpack --watch
自动刷新浏览器
browser-sync dist --files "**/*"
Webpack Dev Server
安装:
yarn add webpack-dev-server --dev
运行:
yarn webpack-dev-server
在后面添加open参数会唤起浏览器并实时刷新
yarn webpack-dev-server --open
webpack支持配置代理API
module.exports = {
devServer: {
//指定需要额外加载的资源的路径
contentBase: './public',
proxy: {
//设定接口的路径名称
'/api': {
// http://localhost:8080/api/users -> https://api.github.com/api/users
target: 'https://api.github.com',
// http://localhost:8080/api/users -> https://api.github.com/users
pathRewrite: {
'^/api': ''
},
// 不能使用 localhost:8080 作为请求 GitHub 的主机名
changeOrigin: true
}
}
}
}
Source Map
SourceMap解决了源代码与运行代码不一致所产生的问题
module.exports = {
devtool: 'eval'
}
| devtool | build | rebuild | production | quality |
|---|---|---|---|---|
| (none) | fastest | fastest | yes | bundled code |
| eval | fastest | fastest | no | generated code |
| cheap-eval-source-map | fast | fastest | no | transformed code (lines only) |
| cheap-module-eval-source-map | slow | fastest | no | original source (lines only) |
| eval-source-map | slowest | fast | no | original source |
| cheap-source-map | fast | slow | yes | transformed code (lines only) |
| cheap-module-source-map | slow | slower | yes | original source (lines only) |
| inline-cheap-source-map | fast | slow | no | transformed code (lines only) |
| inline-cheap-module-source-map | slow | slower | no | original source (lines only) |
| 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 |
建议:
开发模式:cheap-module-eval-source-map
每行代码不超过80个字符
代码经过loader转换过后的差异较大
重新打包相对较快
生产模式:none / nosources-source-map
避免暴露源代码
MHR
Hot Module Replacement 模块热更新
开启的命令为
webpack-dev-server --hot
也可以通过配置文件开启
//载入webpack模块
const webpack = require('webpack')
module.exports = {
devServer: {
hot: true
// hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading,在debug时建议开启
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
HMR API
自定义的JS模块需要手动调用MHR API来实现热更新
module.hot.accept('依赖模块的路径', () => {依赖路径更新过后的处理函数})
import createEditor from './editor'
import background from './better.png'
import './global.css'
const editor = createEditor()
document.body.appendChild(editor)
const img = new Image()
img.src = background
document.body.appendChild(img)
// ============ 以下用于处理 HMR,与业务代码无关 ============
// console.log(createEditor)
if (module.hot) {
let lastEditor = editor
module.hot.accept('./editor', () => {
// console.log('editor 模块更新了,需要这里手动处理热替换逻辑')
// console.log(createEditor)
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的代码情况下,建议只使用hotOnly:true,便于发现代码中的问题
- 在编写HMR代码之前添加判断条件,确定当前项目是否开启了HMR
- HMR不会被打包到最后编译完成的文件中去
生产环境优化
- 配置文件根据环境不同导出不同配置
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = (env, argv) => {
const config = {
mode: 'development',
entry: './src/main.js',
output: {
filename: 'js/bundle.js'
},
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'file-loader',
options: {
outputPath: 'img',
name: '[name].[ext]'
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin()
]
}
//假如为生产环境
if (env === 'production') {
config.mode = 'production'//将模式修改为生产环境
config.devtool = false //禁用source map
config.plugins = [ //添加一部分生产环境所需要的插件
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
}
return config
}
- 一个环境对应一个配置文件 提取公共的部分,其余的部分在单独文件里配置,将公共的部分合并过来
安装合并所需要的模块
yarn add webpack-merge --dev
打包时需要指定配置文件
yarn webpack --config webpack.prod.js
公共配置文件
webpack.common.js
// webpack.common.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/main.js',
output: {
filename: 'js/bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'file-loader',
options: {
outputPath: 'img',
name: '[name].[ext]'
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
})
]
}
开发环境的配置文件
webpack.dev.js
//webpack.dev.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'development',
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
})
生产环境的配置文件
webpack.prod.js
//webpack.prod.js
const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
})
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: JSON.stringify('https://api.example.com')
})
]
}
编写代码时
console.log(API_BASE_URL)
打包完成后自动使用键值
console.log("https://api.example.com")
Tree Shaking
移除未使用的代码
//webpack.config.js
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: {
// 模块只导出被使用的成员
usedExports: true,
// 尽可能合并每一个模块到一个函数中
concatenateModules: true,
// 压缩输出结果
minimize: true
}
}
Tree Shaking & Babel
Tree Shaking 前提是ES Modules,由webpack打包的代码必须使用ESM
为了转换代码中的ECMAScript的新特性,babel有可能会将ES Modules转换成CommonJS
最新版本的Babel并不会将代码转换成CommonJS形式
可以在配置文件中选择是否开启ESM转换
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
// 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
// ['@babel/preset-env', { modules: 'commonjs' }]
// ['@babel/preset-env', { modules: false }]
// 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换
['@babel/preset-env', { modules: 'auto' }]
]
}
}
}
]
},
optimization: {
// 模块只导出被使用的成员
usedExports: true,
// 尽可能合并每一个模块到一个函数中
// concatenateModules: true,
// 压缩输出结果
// minimize: true
}
}
sideEffects
模块执行时除了导出成员之外所做的事情
一般用于npm包标记是否有副作用
当标记为没有副作用时,没有用到的模块就不会被打包
//webpack.config.js
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: {
//生产环境下默认会开启
sideEffects: true,
}
}
//package.json
{
"name": "31-side-effects",
"version": "0.1.0",
"main": "index.js",
"author": "zce <w@zce.me> (https://zce.me)",
"license": "MIT",
"scripts": {
"build": "webpack"
},
"devDependencies": {
"css-loader": "^3.2.0",
"style-loader": "^1.0.0",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.9"
},
"sideEffects": [
//可以指定有副作用的文件,都没有副作用时候可以返回一个false布尔值
"./src/extend.js",
"*.css"
]
}
代码分割
多入口打包
适用于多页应用程序,一个页面对应一个打包入口,公共部分单独提取
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
//利用对象的形式配置多个入口
index: './src/index.js',
album: './src/album.js'
},
output: {
//根据入口动态生成文件名
filename: '[name].bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
//通过chunks属性指定需要注入的bundle
chunks: ['index']
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album']
})
]
}
提取公共的模块
module.exports = {
optimization: {
splitChunks: {
// 自动提取所有公共模块到单独 bundle
chunks: 'all'
}
}
}
动态导入
需要用到某个模块时,再加载这个模块
动态导入的模块会被自动分包
const render = () => {
const hash = window.location.hash || '#posts'
const mainElement = document.querySelector('.main')
mainElement.innerHTML = ''
if (hash === '#posts') {
// 导入模块的路径 解构
import('./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
} else if (hash === '#album') {
// 对分包产生的文件进行重命名
import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
}
}
render()
window.addEventListener('hashchange', render)
提取CSS
可以利用插件将css提取到单独文件
MiniCssExtractPlugin
yarn add mini-css-extract-plugin --dev
压缩CSS和JS
OptimizeCssAssetsWebpackPlugin
yarn add optimize-css-assets-webpack-plugin --dev
TerserWebpackPlugin
yarn add terser-webpack-plugin --dev
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
optimization: {
minimizer: [
//指定压缩的插件
new TerserWebpackPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
},
plugins: [
new MiniCssExtractPlugin()//将CSS导出
]
}
输出文件名Hash
webpack支持在filename添加[hash]字段来给文件添加哈希值
module.exports = {
mode: 'none',
entry: {
main: './src/index.js'
},
output: {
filename: '[name]-[hash].bundle.js'
}
}
支持的hash形式有3种
//只要项目中有任何改动所有文件的hash值都会发生变化
filename: '[name]-[hash].bundle.js'
//只有同一个chunk内的文件发生改变,自己的hash值才会变化
filename: '[name]-[chunkhash].bundle.js'
//只有当前文件发生变化hash值才会改变
filename: '[name]-[contenthash].bundle.js'
//可以设定hash值的长度
filename: '[name]-[contenthash:8].bundle.js'