1、快速上手
安装依赖
npm i webpack webpack-cli --dev
文件目录结构
src/index.js文件
(function func() {
for (let i = 0; i < 2; i++) {
console.log(i);
}
})()
package.json文件
{
"name": "webpack",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start":"webpack" //添加的脚本指令
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"webpack": "^5.76.2",
"webpack-cli": "^5.0.1"
}
}
webpack.config.js
const path = require('path')
module.exports = {
entry: './src/index.js', //入口文件(相对路径)
output: {
filename: 'bundle.js', //打包后生成的文件
path: path.join(__dirname, 'output') //生成路径(绝对路径)
}
}
打包后生成的文件
bundle.js
!function(){for(let o=0;o<2;o++)console.log(o)}();
2、资源模块加载
css模块
依赖下载
npm i css-loader style-loader --dev
目录结构
heading.js
import './index.css'
export default () => {
const element = document.createElement('h2')
element.textContent = 'hello world'
element.classList.add('heading')
element.addEventListener('click', () => {
alert('hello webpack')
})
return element
}
index.css
.heading {
margin: 0 auto;
padding: 0 20px;
max-width: 800px;
background: #d31d1d;
color: blue;
}
index.js
import createHeading from './heading.js'
const heading = createHeading()
document.body.append(heading)
webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output')
},
module: {
rules: [
{
test: /\.css$/, // 正则匹配式,只检测css文件
use: [
// 从下往上依次解析,所以css在下,css解析完之后再解析style
'style-loader',
'css-loader'
]
}
]
}
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script type="module" src="./output/bundle.js"></script>
</body>
</html>
输出结果
静态资源
安装依赖
npm i file-loader --dev
目录结构
index.js
import createHeading from './heading.js'
import icon from '../public/th.png'
const heading = createHeading()
document.body.append(heading)
const img = new Image()
img.src = icon
document.body.append(img)
webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
publicPath: 'output/' //配置静态自愿的根目录 但目前这行代码好像不加也没关系
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.png$/,
use: 'file-loader'
}
]
}
}
输出结果
url加载器(静态资源优化)
安装依赖
npm i url-loader --dev
目录结构
webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
publicPath: 'output/'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.png$/,
// 将静态资源(图片)转换成十六进制的编码 ,此时output目录里没有生成单独的文件
use: 'url-loader'
},
]
}
}
总结
- 小文件使用url,减少请求次数
- 大文件单独提取存放,提高加载速度
优化写法
use:{
loader:'url-loader',
options:{
limit: 10 * 1024 //当文件大小大于10kb时单独存放,小于10kb时使用url
}
}
es模块
将es6代码解析成最初始的js代码(const -> var,()=>{} -> function(){})
webpack.config.js
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
3、开发一个loader(md文件)
安装依赖
npm i marked --dev
npm i html-loader --dev
目录结构
about.md
我是成龙
index.js
import about from './about.md'
console.log(about);
markdown-loader.js
// 解析md文档
const { marked } = require('marked')
module.exports = source => {
const html = marked(source)
// 返回的类型必须为js语句的字符串
return `module.exports = ${JSON.stringify(html)}`
}
webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
publicPath: 'output/'
},
module: {
rules: [
{
test: /\.md$/,
use: [
'./markdown-loader.js'
]
},
]
}
}
引进html-loader
markdown-loader.js
// 解析md文档
const { marked } = require('marked')
module.exports = source => {
const html = marked(source)
// 返回的类型必须为js语句的字符串
return html
}
webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
publicPath: 'output/'
},
module: {
rules: [
{
test: /\.md$/,
use: [
'html-loader'
'./markdown-loader.js'
]
},
]
}
}
这里因为引入了html-loader,所以流程为: md ->marked(依赖)-> html格式 -> html-loader -> js语句的字符串
4、插件机制
自动清理输出目录插件
安装依赖
clean-webpack-plugin
目录结构
webpack.config.js
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
publicPath: 'output/'
},
module: {
rules: [
{
test: /\.md$/,
use: [
'./markdown-loader.js'
]
},
]
},
plugins: [
new CleanWebpackPlugin
]
}
打包后会发现1.txt这个多余的文件被删除了
生成html插件
安装依赖
html-webpack-plugin
webpack.config.js
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
// 注释掉公共路径
// publicPath: 'output/'
},
module: {
rules: [
{
test: /\.md$/,
use: [
'./markdown-loader.js'
]
},
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
// 修改里面的配置
title: 'webpack测试',
meta: {
viewport: 'width-device-width'
}
})
]
}
配置完成打包后会在output文件夹里面生成一个index.html文件
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<meta name="viewport" content="width-device-width"><script defer src="bundle.js"></script></head>
<body>
<div class="container">
<h1>webpack测试</h1>
</div>
</body>
</html>
template模板
当想添加额外的标签时
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'webpack测试',
meta: {
viewport: 'width-device-width'
},
// 引入模板
template: './src/index.html'
})
]
src目录下的template模板->index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div class="container">
<h1><%= htmlWebpackPlugin.options.title %></h1>
</div>
</body>
</html>
打包生成后的文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<meta name="viewport" content="width-device-width"><script defer src="bundle.js"></script></head>
<body>
<div class="container">
<h1>webpack测试</h1>
</div>
</body>
</html>
指定生成多个html文件
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'webpack测试',
meta: {
viewport: 'width-device-width'
},
template: './src/index.html'
}),
new HtmlWebpackPlugin({
filename: 'about.html',
})
]
自己开发一个插件
插件功能: 清除掉打包后文件里的注释文字(带双引号的)
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
class Myplugin {
apply(compiler) {
console.log('MyPlugin启动');
compiler.hooks.emit.tap('Myplugin', compilation => {
// compilation =>可以理解为此次打包的上下文
for (const name in compilation.assets) {
if (name.endsWith('.js')) {
const contents = compilation.assets[name].source()
// 正则替换
const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
compilation.assets[name] = {
// 内容替换
source: () => withoutComments,
size: () => withoutComments.length
}
}
}
})
}
}
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
// 注释掉公共路径
// publicPath: 'output/'
},
module: {
rules: [
{
test: /\.md$/,
use: [
'./markdown-loader.js'
]
},
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'webpack测试',
meta: {
viewport: 'width-device-width'
},
template: './src/index.html'
}),
new HtmlWebpackPlugin({
filename: 'about.html',
}),
new Myplugin()
]
}
5、Dev Server
启动Dev Server
安装依赖
npm i webpack-dev-server
在package.json里面添一行脚本代码
"scripts": {
"server":"webpack-dev-server"
}
启动server命令:
npm run server
运行结果
静态资源访问
在webpack.config.js文件中添加代码,以访问public目录中的静态资源
module.exports = {
...
devServer: {
static: {
directory: './public',
}
},
}
运行结果
Dev Server代理
src/index.js
fetch('./api/users').then(res => res.json())
webpack.config.js
devServer: {
static: {
directory: './public',
},
proxy: {
'/api': {
// http://localhost:8080/api/users -> https://api.github.com/api/users
target: 'https://api.github.com',
// https://api.github.com/api/users -> https://api.github.com/users
pathRewrite: {
'^/api': ''
},
// 不能使用localhost:8080作为请求Github的主机名
changeOrigin: true
}
}
}
运行结果
6、Source Map
为什么要用Source Map以及简单的使用
打包后的代码如果出现了bug,会出现无法定位的问题,因为打包后的代码太乱了,所以此时可以用Source Map
webpack.config.js
module.exports = {
devtool: 'source-map'
}
在src/index.js里故意写一行错误代码,之后打包,打包后的output目录里会生成一个bundle.js.map文件
控制台
Source Map的选择
开发环境下用cheap-module-eval-source-map,因为cheap-module-eval-source-map能暴露出错误代码的文件文件位置以及行数
module.exports = {
devtool: 'cheap-module-eval-source-map'
}
上产环境下尽量不配置Source Map
7、HMR初体验
介绍和简单配置
简单来讲就是那种特么的热更新,每保存一下就更新一下,保证服务器上运行的都是你最新保存的代码
webpack.config.js
const webpack = require('webpack')
devServer: {
hot: true
}
局部热更新
index.js文件里面引入test.js文件,当test.js文件里的内容有变动时,index.js文件里的内容也会进行重新渲染,此时可以用局部热更新的方法
文件目录
test.js
export function test() {
alert(22234)
}
index.js
import { test } from './test'
fetch('./api/users').then(res => res.json())
alert('index')
test()
module.hot.accept('./test', () => {
test()
})
8、生产环境优化
不同环境下的配置
生产环境运行指令: npm run start:production
package.json
"scripts": {
"start:production": "webpack --env production"
}
webpack.config.js
const path = require('path')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
class Myplugin {
apply(compiler) {
console.log('MyPlugin启动');
compiler.hooks.emit.tap('Myplugin', compilation => {
for (const name in compilation.assets) {
if (name.endsWith('.js')) {
const contents = compilation.assets[name].source()
const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
compilation.assets[name] = {
source: () => withoutComments,
size: () => withoutComments.length
}
}
}
})
}
}
// env为一个对象:{ WEBPACK_BUNDLE: true, WEBPACK_BUILD: true, production: true }
module.exports = (env, argv) => {
const config = {
...
}
// 当运行生产环境命令时,会执行以下代码
if (env.production) {
config.mode = 'production'
config.devtool = false
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, 'public'),
to: 'output'
}
]
}),
]
}
return config
}
普通打包指令运行后
生产指令运行后的目录
不同环境的配置文件
安装依赖
npm i webpack-merge
文件目录
公共文件 webpack.common.js
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
class Myplugin {
apply(compiler) {
console.log('MyPlugin启动');
compiler.hooks.emit.tap('Myplugin', compilation => {
// compilation =>可以理解为此次打包的上下文
for (const name in compilation.assets) {
if (name.endsWith('.js')) {
const contents = compilation.assets[name].source()
// 正则替换
const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
compilation.assets[name] = {
// 内容替换
source: () => withoutComments,
size: () => withoutComments.length
}
}
}
})
}
}
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
// 注释掉公共路径
// publicPath: 'output/'
},
devtool: 'source-map',
devServer: {
hot: true,
static: {
directory: './public',
},
proxy: {
'/api': {
// http://localhost:8080/api/users -> https://api.github.com/api/users
target: 'https://api.github.com',
// https://api.github.com/api/users -> https://api.github.com/users
pathRewrite: {
'^/api': ''
},
// 不能使用localhost:8080作为请求Github的主机名
changeOrigin: true
}
}
},
module: {
rules: [
{
test: /\.md$/,
use: [
'./markdown-loader.js'
]
},
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'webpack测试',
meta: {
viewport: 'width-device-width'
},
template: './src/index.html'
}),
new Myplugin(),
new webpack.HotModuleReplacementPlugin()
]
}
生产文件 webpack.prod.js,利用merge方法引入公共配置,在此基础上添加属于自己的配置
const common = require('./webpack.common')
const { merge } = require('webpack-merge')
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = merge(common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, 'public'),
to: 'output'
}
]
})
]
})
DefinePlugin
作用:全局变量替换
index.js
consoel.log(TEST)
webpack.prod.js
module.exports = merge(common, {
plugins: [
new webpack.DefinePlugin({
// 要用双引号包起来
TEST: '"hello111"'
})
]
}
测试结果
多入口打包
目录
webpack.config.js
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
test1: './src/test1.js',
test2: './src/test2.js'
},
output: {
// 输出打包后的文件名[name] :test1.bundle.js 和 test2.bundle.js
filename: '[name].bundle.js',
path: path.join(__dirname, 'output')
}
plugins: [
new HtmlWebpackPlugin({
title: 'test1',
meta: {
viewport: 'width-device-width'
},
template: './src/test1.html',
filename: 'test1.html',
chunks: ['test1'],
}),
new HtmlWebpackPlugin({
title: 'test2',
meta: {
viewport: 'width-device-width'
},
// 引用模板
template: './src/test2.html',
// 输出后的文件名
filename: 'test2.html',
chunks: ['test2'],
}),
new Myplugin(),
new webpack.HotModuleReplacementPlugin()
]
}
提取公共模块
目录结构
test1.js和test2.js引入公共模块global.css和global.js
此时打包后的文件里面都会生成公共模块的代码,所以可以进行一个公共模块的提取
webpack.config.js
module.exports = {
...
optimization: {
splitChunks: {
chunks: 'all'
}
},
}
打包后会发现多了一个公共文件
动态导入(类似于懒加载)
目录结构
index.js
非动态导入写法
import test1 from './test1.js'
import test2 from './test2.js'
const flag = true
flag ? test1() : test2()
打包生成文件
动态导入写法
const flag = true
flag ? import('./test1.js').then(({ default: test1 }) => {
test1()
}) : import('./test2.js').then(({ default: test2 }) => {
test2()
})
test1.js和test2.js情况分类
- 情况一
生成两个额外的文件:1.index.js、2.index.js
- 情况二
生成三个额外的文件:1.index.js、2.index.js、3.index.js
多出的那个文件3.index.js为公共资源的打包,这样原本要打包两遍的文件就只会打包一遍,再被依次引入就行了
mini-css-extract-plugin
作用:将打包中引入的css样式抽取成单独的文件
webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
...
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin()
]
}
效果图
压缩打包后的css文件
下载依赖
npm i css-minimizer-webpack-plugin
webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
...
optimization: {
minimizer: [
new CssMinimizerPlugin(),
],
minimize: true,
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin()
]
}
打包后的代码结构
输出文件名hash
简单来讲hash就是在打包时会生成打包文件,打包的文件名中添加hash值,作用:省略打包资源,只有在特定的情况下才会进行重新打包
三种哈希
- hash
- chunkhash
- contenthash
一、hash
output: {
filename: '[name].[chunkhash].js',
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new MiniCssExtractPlugin({
filename: '[name]-[hash].bundle.css'
})
]
hash是只要任意文件中的一个文件内容发生变动,所有的文件就得进行重新打包
二 、chunkhash
output: {
filename: '[name].[chunkhash].js',
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new MiniCssExtractPlugin({
filename: '[name]-[chunkhash].bundle.css'
})
]
chunkhash是这一整个依赖链中的一个文件内容发生变动,这一整个依赖链中的文件都会进行重新打包
二 、contenthash
output: {
filename: '[name].[chunkhash].js',
}
plugins: [
new webpack.HotModuleReplacementPlugin(),
new MiniCssExtractPlugin({
// 在这里将chunkhash改为contenthash
filename: '[name]-[contenthash].bundle.css'
})
]
contenthash在chunkhash的基础上做了优化,比如index.js引入index.css,如果index.js发生变动,index.css也得重新打包,这显然不是我们想要的,所以contenthash很好的解决了这个问题,只有index.css本身内容发生变动,index.css才会重新进行打包