参考文献
一、概述
本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。
从图中我们可以看出,webpack 可以将多种静态资源 js、css、sass 转换成一个静态文件,减少了页面的请求。
1. 什么是webpack?
webpack 可以看做是 模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Sass,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。在3.0出现后,Webpack还肩负起了优化项目的责任。
这段话有三个重点:
打包:可以把多个 JavaScript 文件打包成一个文件,减少服务器压力和下载带宽。转换:把拓展语言转换成为普通的 JavaScript,让浏览器顺利运行。优化:前端变的越来越复杂后,性能也会遇到问题,而 webpack 也开始肩负起了优化和提升性能的责任。
2. 为什么需要webpack?
webpack 是现代前端技术的基石,常规的开发方式,比如 jQuery、HTML、CSS 静态网页开发已经落后了。现在是 MVVM 的时代,数据驱动视图,webpack 将现代js开发中的各种新型有用的技术,集合打包。通过下图理解webpack生态圈:
3. 模块化
模块化是一种将复杂系统分解为更好的可管理模块的方式,简单来说就是解耦。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。
其优势为:简化开发、按需加载、便于管理、可复用。
目前流行的js模块化规范有CommonJS、AMD、CMD以及ES6的模块系统。webpack是一个模块打包机,它对模块有一个更广泛的定义,对于webpack来说,模块是:
- Common JS modules
- AMD modules
- ES modules
- CSS import
- Images url
webpack 还可以从这些模块中获取 依赖关系。
更多内容,可参考 这里 >>
4. 构建过程
初始化 → 编译 → 输出
- 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。
- 编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
- 输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到本地。
二、基础知识
1. Entry
Entry >>:入口起点,指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
入口文件常用的配置形式如下:
module.exports = {
entry: {
"main": "./src/js/main.js",
"news": "./src/js/news.js"
}
}
提示:在多页面项目中设置出口时,通过
[name]即可获取文件名,其中文件名就是入口设置中的key项。
2. Output
Output >>:该属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件。你可以通过在配置中指定一个 output 字段,来配置这些处理过程。
出口文件常用的配置形式如下:
output: {
// 输出目录/绝对路径
path: path.resolve(__dirname, "./dist/"),
// 输出文件名
filename: "js/[name]-bundle-[hash].js",
}
[name]:模块名称,也就是在指定入口时的key值。[hash]:打包后文件的 hash 值,md5,保证文件唯一性。[chunkhash]:模块自身的hash值。
3. Loader
Loader >>:webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 模块,以供应用程序使用,以及被添加到依赖图中。
module.exports = {
module: {
rules: [...loaders]
}
}
在 webpack 的配置中,loader 有以下个属性:
test:required - 处理文件use:required - 加载器include/exclude:包含/不包含文件(可选);
4. Plugins
Plugins >>:插件是用来拓展Webpack功能的,它们会在整个构建过程中生效,执行相关的任务。
Loaders 和 Plugins 对于新手常常被弄混,但是他们其实是完全不同的东西,可以这么来说,loaders 是在打包构建过程中用来处理源文件的,一次处理一个,插件并不直接操作单个文件,它直接对整个构建过程起作用。
Webpack有很多内置插件,同时也有很多第三方插件,可以让我们完成更加丰富的功能。
使用插件步骤:安装插件 → 导入插件 → 配置插件:在 plugins 数组中创建插件实例
5. Mode
Mode >>:用于配置打包环境,它主要有以下两个值:
development:开发环境production:生成环境(会自动压缩打包后的文件)
6. Context
Context >>:上下文,基础目录,绝对路径,用于从配置中解析入口起点和 loader,入口起点会相对于此目录查找。默认使用 Node.js 进程的当前工作目录,即配置文件所在的目录。webpack 推荐在配置中传入一个值,这使得你的配置独立于 CWD(current working directory, 当前工作目录)。
一般当自定义配置文件之后,我们需要设置该属性,比如配置文件放在在 build/ 目录中,则上下文配置如下:
context: path.resolve(__dirname, "../");
三、实战
了解了webpack的基础知识以后,接下来我们通过实战的形式帮助大家去理解webpack的配置。
本案例主要以单页配置为主,多页配置其实也就是在配置入口时根据实际需要配置多个入口即可。
1. 准备工作
① 创建项目 - 安装依赖
$ mkdir webpack-demo & cd webpack-demo & npm init -y
$ npm install webpack webpack-cli webpack-dev-server clean-webpack-plugin webpack-bundle-analyzer --save-dev
$ ./node_modules/.bin/webpack --version
webpack: 5.65.0
webpack-cli: 4.9.1
webpack-dev-server 4.7.1
提示:windows 系统提示 “'.' 不是内部或外部命令,也不是可运行的程序或批处理文件。”,需将上述指令中路径部分中的
/变为\即可。
依赖解读:
- webpack-dev-server:开发服务,配置此插件可以实现热替换和自动刷新。
- clean-webpack-plugin:文件清除,每次webpack打包之前需调用此插件清除上一次打包的文件。
- webpack-bundle-analyzer:依赖分析,可以据此分析项目哪些模块体积较大,然后进行优化。
② 目录结构
webpack-test
.
├── node_modules # 安装依赖时自动生成
├── public
│ └── UHgu8uXGys.txt # 校验文件,可随意创建,比如README.MD,主要用于测试打包时拷贝
├── src # 源码文件
│ ├── fonts # 字体文件,可自行到字体网站下载
│ │ └── din-regular.otf
│ ├── images # 图片资源
│ │ └── logo.png
│ ├── styles # 样式,本案例主要讲解 less 编译
│ │ └── index.less
│ ├── utils # 工具函数
│ │ └── index.js
│ ├── app.js # 入口文件
│ └── index.html # 模板文件
├── package.json #
└── webpack.config.js # webpack 配置文件
③ 文件内容
npm script
"scripts": {
"build": "webpack --mode=production",
"dev": "webpack --mode=development",
"serve": "webpack serve --open --hot --host=local-ip --port=8090 --mode=development"
},
webpack.config.js
// 1. 引入模块
const path = require('path');
const webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// 2. 导出配置
module.exports = {
context: path.resolve(__dirname, './'),
entry: {
main: './src/app.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name]-bundle-[hash].js',
},
plugins: [
new CleanWebpackPlugin(),
new webpack.BannerPlugin('版权Li-HONGYAO所有,翻版必究!'),
],
devServer: {
liveReload: true,
watchFiles: ['src/**'],
static: {
directory: path.join(__dirname, 'dist'),
},
},
};
src/styles/index.less
@keyframes ani {
to {
transform: translateY(100px);
}
}
@font-face {
font-family: 'din-regular';
src: url('../fonts/din-regular.otf');
}
img {
width: 500px;
}
.logo {
width: 200px;
height: 78px;
background: url('../images/logo.png') no-repeat center center / cover;
margin: 50px 0;
}
#title {
color: blue;
letter-spacing: 2px;
font-size: 36px;
font-family: "din-regular";
animation: ani 2s linear infinite alternate;
}
.wrapper {
display: flex;
justify-content: center;
align-items: center;
}
src/utils/index.js
/**
* 修改元素标题
* @param {*} id
* @param {*} title
*/
export function setTitle(id, title) {
const dom = document.getElementById(id);
dom.textContent = title;
}
src/app.js
import * as Utils from './utils/index.js';
import "./styles/index.less";
Utils.setTitle("title", "Hello, webpack!!!");
src/index.html
<!DOCTYPE html>
<html lang="zh-CN">
<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>webpack - test</title>
</head>
<body>
<h1 id="title"></h1>
<div class="logo"></div>
<img src="./images/logo.png"/>
</body>
</html>
2. 打包HTML
安装依赖:
$ npm install html-webpack-plugin --save-dev
配置文件:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
// -- 模板文件
template: 'src/index.html',
// -- 文件名,相对于output.path,
// -- 可通过文件名设置目录,如 static/pages/detail.htm
filename: 'index.html',
// -- 指定输出文件所依赖的入口文件(*.js)的[name]
chunks: ['main'],
}),
]
};
3. 打包脚本
安装依赖:
$ npm install babel-loader @babel/core @babel/preset-env --save-dev
配置文件(module.rules):
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', { targets: 'defaults' }],
},
},
},
4. 打包样式
安装依赖:
$ npm install style-loader css-loader less less-loader postcss-loader postcss-preset-env --save-dev
依赖解读:
- style-loader:将所有计算后的样式加入页面中;
- css-loader:使你能够使用类似
import和url()的方法实现require()的功能; - less-loader:编译LESS,如果需要编译SASS,可以使用 sass-loader;
- postcss-loader:CSS代码转换工具;
- postcss-preset-env:预设
配置文件:
{
test: /\.less$/,
exclude: /node_modules/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['postcss-preset-env'],
},
},
},
'less-loader',
],
},
注意:引用顺序为从右到左。
→ 分离样式
如果需要分离CSS文件,可使用插件 mini-css-extract-plugin,webpack v4.0之前使用 extract-text-webpack-plugin 。
首先安装依赖:
$ npm install --save-dev mini-css-extract-plugin
然后修改配置文件:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name]-[hash].css',
}),
],
module: {
rules: [
{
test: /\.less$/,
exclude: /node_modules/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['postcss-preset-env'],
},
},
},
'less-loader',
],
},
],
},
};
提示:如果使用
MiniCssExtractPlugin,就不需要引入style-loader了。
→ 去除无效样式
安装依赖:
$ npm install purgecss-webpack-plugin --save-dev
配置代码:
const glob = require('glob');
const PurgeCSSPlugin = require('purgecss-webpack-plugin');
module.exports = {
plugins: [
new PurgeCSSPlugin({
paths: glob.sync('./src/**/*', { nodir: true }),
}),
]
}
5. 打包图片
webpack5 新增了资源模块 >>(asset module),它允许使用资源文件(字体,图标等)而无需配置额外 loader。在 webpack5之前,通常使用 url-loader 或 file-loader 处理图片、字体等静态资源。
安装依赖:
$ npm install html-loader --save-dev
提示:安装
html-loader的目的是为了能够在 html 文件中通过src属性引入的图片资源,需将esModule: false。
配置文件:
module.exports = {
module: {
rules: [
// -- 打包图片
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
exclude: /node_modules/,
type: 'asset/resource',
generator: {
filename: 'images/[hash][ext][query]',
},
},
// - 处理html文件中的img图片(负责引入img)
{
test: /\.html$/,
exclude: /node_modules/,
loader: 'html-loader',
options: {
esModule: false,
},
},
],
},
};
6. 打包字体
同样的,打包字体我们直接使用 webpack5中的 Asset Modules >>,无需安装 loader,直接配置即可:
// -- 打包字体
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
exclude: /node_modules/,
type: 'asset/resource',
generator: {
filename: 'fonts/[hash][ext][query]',
},
},
7. 拷贝资源
开发中,有时我们需要将一些资源在打包时直接拷贝至根目录,比如微信公众号配置业务域名时,需将校验文件放置在域名根目录,我们可以将其放置在 public 目录下,然后通过 copy-webpack-plugin >> 将其拷贝至输出根目录下。
安装依赖:
$ npm install copy-webpack-plugin --save-dev
配置文件:
const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
plugins: [
new new CopyPlugin({
patterns: [{ from: 'public' }]
})
]
}
8. 完整配置文件
// 1. 引入模块
const path = require('path');
const webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const glob = require('glob');
const PurgeCSSPlugin = require('purgecss-webpack-plugin');
// 2. 导出配置
module.exports = {
context: path.resolve(__dirname, './'),
entry: {
main: './src/app.js',
},
output: {
path: path.resolve(__dirname, './dist/'),
filename: '[name]-bundle-[hash].js',
},
module: {
rules: [
// -- 打包脚本
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
// -- 打包样式
{
test: /\.less$/,
exclude: /node_modules/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['postcss-preset-env'],
},
},
},
'less-loader',
],
},
// -- 打包图片
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
exclude: /node_modules/,
type: 'asset/resource',
generator: {
filename: 'images/[hash][ext][query]',
},
},
// -- 处理html文件中的img图片(负责引入img)
{
test: /\.html$/,
exclude: /node_modules/,
loader: 'html-loader',
options: {
esModule: false,
},
},
// -- 打包字体
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
exclude: /node_modules/,
type: 'asset/resource',
generator: {
filename: 'fonts/[hash][ext][query]',
},
},
],
},
plugins: [
new CleanWebpackPlugin(),
new webpack.BannerPlugin('版权Li-HONGYAO所有,翻版必究!'),
new MiniCssExtractPlugin({
filename: 'css/[name]-[hash].css',
}),
new CopyPlugin({
patterns: [{ from: 'public' }],
}),
new PurgeCSSPlugin({
paths: glob.sync('./src/**/*', { nodir: true }),
}),
new BundleAnalyzerPlugin(),
new HtmlWebpackPlugin({
// -- 模板文件
template: 'src/index.html',
// -- 文件名,相对于output.path,
// -- 可通过文件名设置目录,如 static/pages/detail.htm
filename: 'index.html',
// -- 指定输出文件所依赖的入口文件(*.js)的[name]
chunks: ['main'],
}),
],
devServer: {
liveReload: true,
watchFiles: ['src/**'],
static: {
directory: path.join(__dirname, 'dist'),
},
},
};
四、延伸
1. 指定配置文件编译
在实际开发过程中,你可能会分环境创建不同的配置文件来满足不同的开发需求,比如你在开发阶段,通常会创建一个 webpack.dev.config.js 文件,那么你在执行编译指令的时候需要指向该配置文件,如下所示:
$ ./node_modules/.bin/webpack --config ./build/webpack.dev.config.js
提示:假设配置文件的路径是:
./build/webpack.dev.config.js,那你需要在配置文件中做如下修改:module.exports = { context: path.resolve(__dirname, "../"), output: { path: path.resolve(__dirname, "../dist/"), } };
注意:
- 一旦修改了webpack的配置文件,必须重启服务或重新build。否则失效。
- 如果自定义配置文件,切记在执行打包时一定要指定配置文件路径
2. 编译参数配置
webpack 自身提供了一些参数来优化编译任务,以下简单列出了一些参数:
| 参数 | 描述 |
|---|---|
--config | 指定配置文件 |
--watch, -w | 监听变动并自动打包 |
-p | 压缩混淆脚本 |
--progress | 显示进度条 |
提示:想了解webpack更多参数,可在终端输入
./node_modules/.bin/webpack -h查看
3. 引用三方库
→ 局部引入
import $ from 'jquery';
→ 全局引入
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
}),
5. alias
创建 import 或 require 的别名,来确保模块引入变得更简单。例如,一些位于 src/ 文件夹下的常用模块:
moudle.exports = {
resolve: {
alias: {
'@utils': path.resolve(__dirname, 'src/utils/'),
},
},
};
现在,替换“在导入时使用相对路径”这种方式,就像这样:
import * as Utils from './utils/index.js';
可以这样使用别名:
import * as Utils from '@utils/index.js';
6. 抽离公共文件
当一部分代码需要反复被用到,反复请求浪费资源,将公共代码 抽离,需要时读取缓存即可
output: {
...
chunkFilename: "[name].chunk.js"
}
optimization: {
splitChunks: {
cacheGroups: {// 缓存组,缓存公共代码
// 首先:打包node_modules中的文件
vendor: {
test: /node_modules/,
name: "vendor",
minSize: 0,
minChunks: 1,
chunks: "all",
priority: 1
},
// 其次: 打包业务中公共代码
common: {
name: "common",
chunks: "all",
minSize: 0,
minChunks: 2,
priority:0
}
}
}
}