Webpack
概览
- 官网:https://webpack.js.org,进入文档从 Guides 模块的 Getting Started 开始看
- 中文文档
webpack 是一个构建工具,用于前端项目的打包和优化。
构建工具能解决什么问题?
- 混淆代码,提高保密性
- 提高代码兼容性
- 模块整合,减少 http 请求
- 压缩代码体积,提高性能
pnpm add -D webpack webpack-cli
基本概念:
- entry: 使用哪个模块来作为构建的起始入口
- output: 打包后的文件放在哪里、如何命名这些文件
- loader: 处理文件的转换器,用于对模块源码进行转换。
- webpack 自身只能识别 js、json 文件,像 css 、ts 、jsx 等文件都需要通过 loader 解析
- plugin: 扩展 webpack 的功能。比如打包优化、资源管理、注入环境变量等
- mode: 对于不同的环境选择不同的配置
- 开发模式:development
- 生产模式:production
// webpack.config.js
module.exports = {
entry: '',
output: {},
module: {
rules: []
},
plugins: [],
mode: ''
};
mode
对于不同的环境选择不同的配置
- 开发模式:development
- 生产模式:production
- none
开发模式
这个模式下主要做两件事:
1、编译代码,使浏览器能识别运行
webpack 默认不能处理样式、字体、图像、html 等资源,所以要加载配置来编译这些资源
2、代码质量检查,检查代码规范和格式,统一团队编码风格
生产模式
- 优化代码运行性能
- 优化代码打包速度
环境配置
pnpm add -D webpack webpack-cli webpack-dev-server webpack-merge
- webpack 、webpack-cli:打包必备
- webpack-dev-server:一个提供热更新的开发服务器
- webpack-merge:合并配置文件
在根目录新建 scripts 目录,在里面创建三个配置文件:
webpack.base.js
,公用配置webpack.dev.js
,开发环境配置webpack.prod.js
,生产环境配置
获取环境变量:
pnpm add cross-env -D
配置脚本:
{
"scripts": {
"dev": "cross-env NODE_ENV=development webpack serve -c scripts/webpack.dev.js",
"build": "cross-env NODE_ENV=production webpack -c scripts/webpack.prod.js"
}
}
// webpack.base.js
const path = require('path');
module.exports = {
entry: path.resolve(__dirname, '../src/index.tsx'),
output: {
path: path.resolve(__dirname, '../dist'),
filename: '[name].[hash:8].js'
}
};
// webpack.dev.js
const { merge } = require('webpack-merge');
const base = require('./webpack.base.js');
module.exports = merge(base, {
mode: 'development',
devServer: {
open: true,
port: 8080
}
});
// webpack.prod.js
const { merge } = require('webpack-merge');
const base = require('./webpack.base.js');
module.exports = merge(base, {
mode: 'production'
});
Output
const path = require('path');
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'assets/js/main.js', // 将 js 文件输出到 assets/js 目录中
clean: true // 自动将上次打包目录资源清空
}
};
Loader
Loader 本质上是一个转换器,将匹配到的文件中的源码通过转换,使其变成另一种形态。
例如,浏览器不认识 TypeScript 语法,但是可以通过 ts-loader 对其进行转换。从原理上来看,Loader 就是一个函数,参数为需要转换的源代码,返回转换后的新代码。
示例:假如我想在代码里用中文定义变量,浏览器肯定不认识,这时可以写个 loader 去转换,设定的语法格式:变量 name = 'zgh'
function chineseLoader(originCode) {
return originCode.replace(/变量/g, 'let');
}
这里的chineseLoader
就是一个极简单的 Loader。
可以通过传入多个 Loader 以达到链式调用的效果,loader 会从右到左被应用。
Plugin
Loader 和 Plugin 的区别
不同的作⽤:
- Loader 直译为「加载器」。Webpack 将⼀切⽂件视为模块,但是 webpack 原⽣是只能解析 js ⽂件,如果想将其他⽂件也打包的话,就会⽤到 loader 。 所以 Loader 的作⽤是让 webpack 拥有了加载和解析⾮ JavaScript ⽂件的能⼒。
- Plugin 直译为"插件"。Plugin 可以扩展 webpack 的功能,让 webpack 具有更多的灵活性。 在 Webpack 运⾏的⽣命周期中会⼴播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
不同的⽤法:
- Loader 在
module.rules
中配置,作为模块的解析规则⽽存在。类型为数组,每⼀项都是⼀个 Object ,⾥⾯描述了对于什么类型的⽂件( test ),使⽤什么加载( loader )和使⽤的参数( options ) - Plugin 在 plugins 中单独配置。类型为数组,每⼀项是⼀个 plugin 的实例,参数都通过构造函数传⼊。
webpack-dev-server
webpack-dev-server,一个提供热更新的开发服务器
pnpm add -D webpack-dev-server
// webpack.config.js
module.exports = {
mode: 'development',
devServer: {
open: true, // 自动打开浏览器
// host: "localhost", // 服务器域名
port: 8080 // 服务器端口号
}
};
运行指令:
npx webpack serve
在使用开发服务器时,所有代码都在内存中编译打包,并不会输出到 dist 目录下
处理样式资源
webpack 本身不能识别样式资源,需要使用对应的 loader 来处理。快速上手 -> 加载 css
处理 CSS
处理引入的 css 资源,如import './index.css'
pnpm add -D css-loader style-loader
css-loader
:将 css 转为 CommonJS 规范的 js 代码style-loader
:将 js 模块转为 css 样式,并创建一个 style 标签,将样式插入到 DOM 中
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
},
module: {
rules: [
{
test: /\.css$/i,
// 执行顺序是从右到左
use: ['style-loader', 'css-loader']
}
]
}
};
处理 Less
less-loader,将 less 文件编译成 css 文件
pnpm add -D less-loader
module.exports = {
module: {
rules: [
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
}
]
}
};
处理 Scss
sass-loader,将 Sass/SCSS 文件编译为 CSS
pnpm add -D sass sass-loader
module.exports = {
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: ['style-loader', 'css-loader', 'sass-loader']
}
]
}
};
处理 Stylus
stylus-loader,将 Stylus 文件编译为 CSS
pnpm add -D stylus stylus-loader
module.exports = {
module: {
rules: [
{
test: /\.styl$/,
use: ['style-loader', 'css-loader', 'stylus-loader']
}
]
}
};
处理 CSS 兼容性
postcss-loader,使用 PostCSS 处理 CSS
pnpm add -D postcss postcss-loader postcss-preset-env
module.exports = {
module: {
rules: [
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [['postcss-preset-env', {}]]
}
}
},
'less-loader'
]
}
]
}
};
将 CSS 提取到单独的文件
mini-css-extract-plugin,会将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。
pnpm add -D mini-css-extract-plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
mode: 'production',
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.less$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
},
{
test: /\.s[ac]ss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
},
{
test: /\.styl$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'stylus-loader']
}
]
},
plugins: [
new MiniCssExtractPlugin({
// 定义输出文件名和目录
filename: 'static/css/main.css'
})
]
};
压缩 CSS 体积
css-minimizer-webpack-plugin,优化、压缩 CSS 体积
pnpm add -D css-minimizer-webpack-plugin
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
mode: 'production',
plugins: [
// css压缩
new CssMinimizerPlugin()
]
};
处理图像
module.exports = {
module: {
rules: [
{
test: /\.(png|svg|jpg|jpeg|gif|webp)$/i,
type: 'asset/resource',
parser: {
dataUrlCondition: {
maxSize: 25 * 1024 // 小于 25kb 会被转为 Base64
}
},
generator: {
filename: 'assets/imgs/[name].[hash:8][ext]'
}
}
]
}
};
1、Rule.parser.dataUrlCondition,如果资源小于 maxSize,则会以 Base64 编码的形式注入到包里
- 优点:减少请求数量
- 缺点:资源会大一些
2、修改输出资源的名称和路径
{
generator: {
filename: 'assets/imgs/[name].[hash:8][ext]';
}
}
- [name]:文件名
- [ext]:文件之前的后缀
- [hash:8]:hash 值前 8 位
打包后,会生成一个assets/imgs
文件夹,里面包含图像文件
修改输出 js 的名称和路径
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'assets/js/main.js' // 将 js 文件输出到 assets/js 目录中
}
};
处理字体
module.exports = {
module: {
rules: [
{
test: /\.(eot|ttf|woff|woff2|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'assets/fonts/[name].[hash:8][ext]'
}
}
]
}
};
处理音视频
module.exports = {
module: {
rules: [
{
test: /\.(ttf|woff2?|map4|map3|avi)$/,
type: 'asset/resource',
generator: {
filename: 'assets/media/[hash:8][ext]'
}
}
]
}
};
处理 html
自动引入打包之后的资源,避免手动引入打包后的资源。
例如,打包后的 js 资源形如c84b3819.js
,如果手动引入如<script defer="defer" src="c84b3819.js"></script>
,则太麻烦了
pnpm add -D html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './public/index.html')
})
]
};
以某个 html 文件为模板创建文件,这里是 ./public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no, minimum-scale=1, maximum-scale=1, minimal-ui, viewport-fit=cover"
/>
<title></title>
</head>
<body>
<div id="app"></div>
</body>
</html>
生产模式默认开启了 html 压缩
处理 js
生产模式默认开启了 js 压缩
- 兼容性处理。比如将 ES6 语法转换为 ES5 语法,使用 Babel
- 代码格式处理,使用 Eslint
Eslint 配置
https://webpack.js.org/plugins/eslint-webpack-plugin/
pnpm add -D eslint eslint-webpack-plugin
const ESLintPlugin = require('eslint-webpack-plugin');
module.exports = {
plugins: [
new ESLintPlugin({
// 指定检查文件的根目录
context: path.resolve(__dirname, 'src')
})
]
};
Babel 配置
pnpm add -D babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript
将 js、ts、jsx、tsx 文件都交给 babel-loader 处理,并配置对应的 presets,这些 presets 会从右向左执行
const path = require('path');
module.exports = {
entry: path.resolve(__dirname, '../src/index.tsx'),
output: {
path: path.resolve(__dirname, '../dist'),
filename: '[name].[hash:8].js'
},
resolve: {
// 配置 extensions 来告诉 webpack 在没有书写后缀时,以什么样的顺序去寻找文件
extensions: ['.mjs', '.js', '.json', '.jsx', '.ts', '.tsx'],
// 配置别名
alias: {
'@': path.resolve(__dirname, '../src')
}
},
module: {
rules: [
{
test: /.(jsx?)|(tsx?)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
targets: 'last 2 versions, > 0.2%, not dead', // 根据项目去配置
useBuiltIns: 'usage', // 会根据配置的目标环境找出需要的polyfill进行部 分引入
corejs: 3
}
],
['@babel/preset-typescript'],
['@babel/preset-react']
]
}
}
}
]
}
};
执行pnpm build
GZIP 压缩
依赖分析和 CDN 加速
这里以 Vue 项目打包优化为例,减少打包体积,生产环境使用 CDN 加速
依赖分析
依赖分析可以看出项目中各个依赖所占的打包体积,辅助分析可以优化的地方。
1、安装webpack-bundle-analyzer
插件,仓库地址
npm i -D webpack-bundle-analyzer
2、在package.json
中配置分析命令:build:analyze
,这里定义一个ANALYZE_MODE
字段,并设置为 true
{
"scripts": {
"dev": "vue-cli-service serve",
"build:prod": "vue-cli-service build",
"build:analyze": "ANALYZE_MODE=true vue-cli-service build --mode analyze"
}
}
3、配置vue.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
chainWebpack(config) {
// 依赖分析
if (process.env.ANALYZE_MODE) {
config.plugin('webpack-bundle-analyzer').use(BundleAnalyzerPlugin);
}
config.when(process.env.NODE_ENV !== 'development', config => {
// 设置哪些资源不需要被打包,改为获取CDN资源
config.set('externals', {
vue: 'Vue',
'vue-router': 'VueRouter',
vuex: 'Vuex',
axios: 'axios',
'element-ui': 'ELEMENT'
});
// 给 index.html 传参
config.plugin('html').tap(args => {
args[0].useCdn = true;
return args;
});
});
}
};
关于externals,{ key: value }
,其中 key 是第三方依赖库的名称,和package.json
文件中的依赖名称一样。关于 value 的值,先把 CDN 的链接打开查看源代码,一般就是暴露出来的全局变量名称
4、配置 index.html 模板,引入 CDN 链接
<% if(htmlWebpackPlugin.options.useCdn==true) { %>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-router@3.5.4/dist/vue-router.global.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@1.3.6/dist/axios.min.js"></script>
<% } %>
常见的 CDN 提供商:jsdelivr、cdnjs、bootcdn、unpkg
如果有自建 CDN 服务,可以从 jsdelivr 上面下载各个库、各个版本的源码
5、执行命令,查看项目依赖体积情况
npm run build:analyze
bundle、chunk、module
- bundle:是由 webpack 打包出来的⽂件
- chunk:代码块,⼀个 chunk 由多个模块组合⽽成,⽤于代码的合并和分割
- module:是开发中的单个模块,在 webpack 中⼀切皆模块,⼀个模块对应⼀个⽂件,webpack 会从配置的 entry 中递归开始找出所有依赖的模块
webpack 构建过程
- Compiler
- Compilation
- Module
- Chunk
- Bundle
项目打包优化
从以下角度来优化:
- 提升开发体验
- 提升打包构建速度
- 减少代码体积
- 优化代码运行性能
SourceMap
SourceMap 用于映射源文件到构建文件,方便调试。会生成一个 map 后缀的文件,包含源代码和构建后的代码每一行、每一列的映射关系,当构建后的代码出错时,浏览器会自动跳转到源文件的对应位置。
Devtool,控制是否生成、如何生成 SourceMap
// 开发环境
module.exports = {
mode: 'development',
devtool: 'cheap-module-source-map'
};
// 生产环境
module.exports = {
mode: 'production',
devtool: 'source-map'
};
HMR
Hot Module Replacement,在应用程序运行时替换、添加或删除模块,而无需完全重新加载
HotModuleReplacementPlugin,启用热更新 (HMR),
不应在生产环境开启 HMR,因为 HMR 会触发浏览器刷新,导致用户操作丢失
- 更新 webpack-dev-server 配置
module.exports = {
devServer: {
hot: true // 开启HMR功能
}
};
此时 css 样式会经过 style-loader 处理后具备 HMR 功能,但是 js 还不行
- 配置 js
// main.js
import moduleA from './moduleA.js';
import moduleB from './moduleB.js';
if (module.hot) {
module.hot.accept('./moduleA.js', function () {
console.log('Accepting the updated moduleA module!');
moduleA();
});
module.hot.accept('./moduleB.js', function () {
console.log('Accepting the updated moduleB module!');
moduleB();
});
}
这样配置很麻烦,实际开发会使用其他 loader 来处理 js,如:
Tree Shaking
Tree Shaking 是指在构建时,移除未使用的 js 代码,以减少代码体积。
把应用程序想象成一棵树,实际使用的代码是绿色、活的叶子,未使用的代码是灰色、死的叶子。为了摆脱枯叶,你必须要摇动树使枯叶掉落。
下面的例子,如果 multiply 函数没有被用到,Tree Shaking 会将它从最终的打包文件中移除。
import { sum } from './math';
console.log(sum(1, 2));