前言
webpack那么多配置都是怎么用的?
通过webpack可以做哪些优化?
相信每一个前端仔都有过这些疑问,这里通过此文章由浅入深介绍webpack这款打包工具,篇幅较长,希望大家可以耐心看完。
此文章以webpack5为例讲解。
webpack是什么
本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。
简单来说,webpack可以将我们项目中每个chunk(模块),经过编译打包成一个或多个bundle。
为什么要使用webpack
那么有个疑问:我们的前端项目不经过webpack打包不也是可以挂到服务器上的吗?那为什么还要多此一举呢。
举个例子:项目有3个js文件,那么浏览器就需要发送三次http请求来获取这三个文件,然后依次执行其中代码。如果其中一个文件因为网络波动而延误时间,那么整个页面的加载也会随之延误。当项目日益复杂,存在上百个js文件时,页面的加载速度会被严重阻塞。
这种情况,就需要webpack帮助我们把项目编译打包成一个或几个bundle文件,减少网络请求,提升页面加载时间!!!这只是使用webpack众多好处中的一个,更多的请继续向下看。
初始化项目
接下来开始尝试使用webpack
- 初始化项目 yarn init
- 安装webpack有关依赖 yarn add webpack webpack-cli
- 新建src文件夹 并写一些简单的js文件
文件结构:
// src/js/sum.js
const sum = (...args) => {
return args.reduce((p, c) => p + c, 0);
};
export default sum;
// src/main.js
import sum from "./js/sum";
const res = sum(1, 2);
console.log(res);
<!-- src/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>
<h1>Webpack Demo</h1>
<script src="./main.js"></script>
</body>
</html>
此时打开index.html页面,发现浏览器输出如下错误:
Uncaught SyntaxError: Cannot use import statement outside a module
这是因为浏览器原生是不支持es6 module的。
解决以上问题 可以通过设置script标签type='module'
这种情况就需要webpack帮助我们进行代码转译工作啦!
项目根目录下运行npx webpack ./src/main.js --mode=development,会发现根目录下自动创建dist文件下,且其下存在main.js文件,将我们index.html中script标签src路径修改为src="../dist/main.js",再次刷新浏览器,发现浏览器成功输出3。
我们先不关注这条命令具体代表什么意思,先继续向下进行。
webpack的5大核心概念
- entry(入口)
指示 Webpack 从哪个文件开始打包
- output(输出)
指示 Webpack 打包完的文件输出到哪里去,如何命名等
- loader(加载器)
webpack 本身只能处理 js、json 等资源,其他资源需要借助 loader,Webpack 才能解析
- plugins(插件)
扩展 Webpack 的功能
- mode(模式)
- 开发模式:development
- 生产模式:production
再回头看npx webpack ./src/main.js --mode=development这条命令
npx webpack: 是用来运行本地安装 Webpack 包的。
./src/main.js: 指定 Webpack 从 main.js 文件开始打包,不但会打包 main.js,还会将其依赖也一起打包进来。
--mode=xxx:指定模式(环境)。
写一个简单的webpack配置文件
项目根目录下新建webpack.config.js
// Node.js的核心模块之一,专门用来处理文件路径
const path = require("path");
module.exports = {
// 入口
// 相对路径和绝对路径都行
entry: "./src/main.js",
// 输出
output: {
// path: 文件输出目录,必须是绝对路径
// path.resolve()方法返回一个绝对路径
// __dirname 当前文件的文件夹绝对路径
path: path.resolve(__dirname, "dist"),
// filename: 输出文件名
filename: "main.js",
},
// 加载器 配置loader
module: {
rules: [],
},
// 插件
plugins: [],
// 模式
mode: "development", // 开发模式
};
Webpack 是基于 Node.js 运行的,所以采用 Common.js 模块化规范
运行npx webpack,发现结果同npx webpack ./src/main.js --mode=development一样
如何处理样式资源
我们此时的配置还是不能够处理css资源的,这就需要我们借助loader了。
首先安装css-loader,style-loader:yarn add css-loader style-loader
css-loader:负责将 Css 文件编译成 Webpack 能识别的模块style-loader:会动态创建一个 Style 标签,里面放置 Webpack 中 Css 模块内容
配置:
const path = require("path");
module.exports = {
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "main.js",
},
module: {
rules: [
{
// 正则匹配以.css结尾的文件
test: /.css$/,
// use数组里面放置loader 执行顺序是从右向左 从下向上
use: ["style-loader", "css-loader"],
},
],
},
plugins: [],
mode: "development",
};
添加代码测试
/* src/css/index.csss */
.box1 {
width: 100px;
height: 100px;
background: #bfa;
}
// src/main.js
import sum from "./js/sum";
import "./css/index.css";
const res = sum(1, 2);
console.log(res);
<!-- src/index.html -->
<body>
<h1>Webpack Demo</h1>
<div class="box1"></div>
<script src="../dist/main.js"></script>
</body>
运行npx webpack并打开index.html页面:
可以看到即使我们的index.html文件中并没有写样式,但是webpack帮助我们打包编译,将main.js中引入的css先后经过css-loader,style-loader最终渲染到我们的DOM中。
CSS预处理器
在现在的实际项目中,我们大都会使用诸如less,scss,sass之类的css预处理器,那么webpack如何处理css这些预处理器呢?
答案当然是借助对应的loader,例如:less-loader,sass-loader,stylus-loader配置如下:
module: {
rules: [
{
test: /.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /.less$/,
// loader: 'xxx' // 只能使用一个loader
use: ["style-loader", "css-loader", "less-loader"],
},
{
test: /.s[ac]ss$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
{
test: /.styl$/,
use: ["style-loader", "css-loader", "stylus-loader"],
},
]
}
这里就不写测试代码进行测试,大家可以自行测试
处理静态资源
在webpack5之前,处理资源模块是需要额外安装loader的:
raw-loader: 将文件导入为字符串
url-loader:将文件作为data URI内联到bundle中
file-loader:将文件发送到输出目录
而在webpack5中,可以直接使用资源模块类型asset module type来替代上面的loader:
asset/resource发送一个单独的文件并导出URL。之前通过使用file-loader实现asset/inline导出一个资源的data URI。之前通过使用url-loader实现asset/source导出资源的源代码。之前通过使用raw-loader实现asset在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用url-loader,并且配置资源体积限制实现。小于 8kb 的文件,将会视为inline模块类型,否则会被视为resource模块类型。
图片类静态资源
src文件下新建image文件下,并分别添加两个大小差异较大的图片,16kb.jpeg,426kb.png。
添加配置
module: {
rules: [
// xxx
{
test: /.(png|jpe?g|gif|webp|svg)$/,
type: "asset",
parser: {
dataUrlCondition: {
/**
* 小于20kb的图片转base 64
* 优点:减少请求数量
* 缺点:体积会更大 体积较小图片可以忽略
*/
maxSize: 20 * 1024, // 20kb
},
},
generator: {
/**
* 输出图片名称
* [hash:10] hash值取前10位
* [ext] 文件扩展名
*/
filename: "static/images/[hash:10][ext][query]",
}
}
]
}
修改代码
/* src/css/index.css */
.box1 {
width: 100px;
height: 100px;
background: url("../image/16kb.jpeg");
background-size: cover;
}
// src/scss/index.scss
.box3 {
width: 100px;
height: 100px;
background: url("../image/426kb.png");
background-size: cover;
}
运行效果
小于20kb的图片资源被打包成base 64类型
大于20kb的图片资源被打包进dist/static/images目录下
字体图标类静态资源
首先在阿里巴巴矢量图标库中添加一些字体图标到我们项目中,这个具体如何添加官网上已经有详细教程,就不在这里演示。
修改配置
module: {
rules: [
// xxx
{
test: /.(ttf|woff2?)$/,
// 字体图标不需要转base 64 采用asset/resource方式打包输出
type: "asset/resource",
generator: {
filename: "static/fonts/[hash:10][ext][query]",
},
},
]
}
自动清空上次打包内容
目前我们每次运行npx webpack,都需要手动将dist文件清空,而在webpack5中可以通过配置来实现自动清空上次打包内容。
修改配置
module.exports = {
entry: "./src/main.js",
output: {
// 文件输出路径 要求绝对路径
path: path.resolve(__dirname, "dist"),
filename: "main.js",
// 在打包之前 将path整个目录内容清空 再进行打包
clean: true
}
}
eslint配置
首先需要安装插件eslint-webpack-plugin以及eslint
运行命令yarn add eslint eslint-webpack-plugin
修改配置
// webpack.config.js
const ESLintPlugin = require("eslint-webpack-plugin");
module.exports = {
// ...
plugins: [
new ESLintPlugin({
// 指定检查文件的根目录
context: path.resolve(__dirname, "src"),
}),
],
}
添加eslint规则配置文件
配置文件有很多种写法
-
.eslintrc.*:新建文件,位于项目根目录
.eslintrc.eslintrc.js.eslintrc.json- 区别在于配置格式不一样
-
package.json中eslintConfig:不需要创建文件,在原有文件基础上写
ESLint 会查找和自动读取它们,所以以上配置文件只需要存在一个即可
以.eslintrc.js配置文件为例,根目录下新建.eslintrc.js
module.exports = {
/**
* 自己配置规则过于麻烦
* 可以选择继承现有的规则
* 这里采用eslint官方提供的规则模板
* https://eslint.bootcss.com/docs/rules/
*/
extends: ["eslint:recommended"],
env: {
// 启用node中全局变量
node: true,
// 启用浏览器中全局变量
browser: true,
},
parserOptions: {
ecmaVersion: 6, // ES 语法版本
sourceType: "module", // ES 模块化
},
rules: {
// 不能使用var定义变量
"no-var": 2,
},
};
测试
修改代码使其违反eslint规则校验
// src/main.js
import sum from "./js/sum";
var res = sum(1, 2);
console.log(res);
运行命令npx webpack观察效果
打包失败 因为在main.js文件中使用var定义了变量 违反了eslint的规则
需将var修改为let 或者 const 即可打包成功
eslint即时检查
以上配置只有在打包编译的时候才会发现代码中的错误,那如果想要实时进行代码检查呢?可以在vscode中安装ESLint插件,即可不用编译就能看到错误,可以提前解决,但是此时就会对项目所有文件默认进行Eslint检查,我们dist目录下打包后文件就会报错,此时需要忽略检查dist文件
根目录下新建.eslintignore文件,作用:对一些没必要进行eslint检查的文件进行忽略
这里我只配置了dist文件下内容不需要进行检查
# 忽略dist目录下所有文件
dist
如果下载了ESLint插件发现没有效果,可以在vscode setting.json中查看"eslint.enable"配置,需要将其设置为true
babel配置
Babel 主要用于将es6高级语法转化为向后兼容的语法,以便在旧版本浏览器中可以运行js代码
首先安装插件yarn add babel-loader @babel/core @babel/preset-env
修改配置
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /.js$/,
// node_modules目录下文件无需编辑
exclude: /node_modules/,
loader: "babel-loader",
},
],
},
}
添加babel配置文件
-
babel.config.*:新建文件,位于项目根目录
babel.config.jsbabel.config.json
-
.babelrc.*:新建文件,位于项目根目录
.babelrc.babelrc.js.babelrc.json
-
package.json中babel:不需要创建文件,在原有文件基础上写
Babel 会查找和自动读取它们,所以以上配置文件只需要存在一个即可
根目录下创建文件 .babelrc.js
module.exports = {
/**
* 预设 允许我们使用最新的js语法
* 且无需关注目标环境需要进行哪些语法转换
*/
presets: ["@babel/preset-env"],
};
处理html文件
目前我们的src/index.html文件中是手动引入打包后生成的js文件的,不仅麻烦还有可能出现引用路径不对而报错,那么我们可以通过webpack中的HtmlWebpackPlugin插件来实现自动引入打包生成的bundle
该插件将为你生成一个 HTML5 文件, 在 body 中使用
script标签引入你所有 webpack 生成的 bundle。
安装插件yarn add html-webpack-plugin
修改配置
// webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
/**
* 以 src/index.html 文件为模板创建新的html文件
* 新html文件特点: 内容和源文件一致 自动引入打包生成的js等资源
*/
template: path.resolve(__dirname, "src/index.html"),
}),
],
}
运行命令npx webpack
这将会在dist目录下生成一个新的index.html文件,并且其中自动引入main.js文件
<!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>
<script defer src="main.js"></script></head>
<body>
<h1>webpack config</h1>
<span class="iconfont icon-gongyi"></span>
<div class="box1"></div>
<div class="box2"></div>
<div class="box3"></div>
<div class="box4"></div>
<div class="box5"></div>
</body>
</html>
开发服务器
当前配置 我们每次写完代码都需要手动编译 太麻烦了 可以通过webpack-dev-server实现自动化打包
yarn add webpack-dev-server安装依赖
修改配置
// webpack.config.js
module.exports = {
// ...
// 开发服务器:不会输出打包资源 在内存中编译打包
devServer: {
host: "localhost", // 启动服务器的域名
port: "3000", // 端口号
open: true, // 自动打开浏览器
},
}
运行指令
npx webpack serve
生产模式
生产模式介绍
在我们开发完成代码后,需要得到打包编译后的代码进行部署上线
准备工作
一般一个项目中会存在两套webpack的配置,分别对应开发模式和生产模式 ,接下来我们修改下当前目录结构。
├── config (Webpack配置文件目录)
│ ├── webpack.dev.js(开发模式配置文件)
│ └── webpack.prod.js(生产模式配置文件)
根目录下新建config文件夹,将原webpack.config.js文件重命名为webpack.dev.js,并复制一份并重命名为webpack.prod.js
修改web pack.dev.js
文件目录变了,所以所有绝对路径需要回退一层目录才能找到对应的文件
const path = require("path");
const ESLintPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/main.js",
output: {
// 开发模式没有输出 不需要指定输出路径
path: undefined,
filename: "main.js",
// clean: true // 开发模式没有输出 不需要清空输出结果
},
module: {
rules: [
// loader的配置
{
test: /.css$/,
/**
* 执行顺序 从右到左(从上到下)
* css-loader 将css资源编译成commonjs的模块到js中
* style-loader 将js中的css通过创建style标签添加到html中生效
* index.html文件中不会看到style标签 需要通过f12查看elements面板
*/
use: ["style-loader", "css-loader"],
},
{
test: /.less$/,
// loader: 'xxx' // 只能使用一个loader
use: ["style-loader", "css-loader", "less-loader"],
},
{
test: /.s[ac]ss$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
{
test: /.styl$/,
use: ["style-loader", "css-loader", "stylus-loader"],
},
{
test: /.(png|jpe?g|gif|webp|svg)$/,
type: "asset",
parser: {
dataUrlCondition: {
/**
* 小于20kb的图片转base 64
* 优点:减少请求数量
* 缺点:体积会更大 体积较小图片可以忽略
*/
maxSize: 20 * 1024, // 20kb
},
},
generator: {
/**
* 输出图片名称
* [hash:10] hash值取前10位
* [ext] 文件扩展名
*/
filename: "static/images/[hash:10][ext][query]",
},
},
{
test: /.(ttf|woff2?)$/,
// 字体图标不需要转base 64 采用asset/resource方式打包输出
type: "asset/resource",
generator: {
filename: "static/fonts/[hash:10][ext][query]",
},
},
{
test: /.js$/,
// node_modules目录下文件无需编辑
exclude: /node_modules/,
loader: "babel-loader",
},
],
},
// 插件
plugins: [
new ESLintPlugin({
// 指定检查文件的根目录 回退一层
context: path.resolve(__dirname, "../src"),
}),
new HtmlWebpackPlugin({
/**
* 以 src/index.html 文件为模板创建新的html文件
* 新html文件特点: 内容和源文件一致 自动引入打包生成的js等资源
* 回退一层
*/
template: path.resolve(__dirname, "../src/index.html"),
}),
],
// 开发服务器:不会输出打包资源 在内存中编译打包
devServer: {
host: "localhost",
port: "3000",
// 自动打开浏览器
open: true,
},
mode: "development",
};
开发 模式运行指令: npx webpack serve --config ./config/webpack.dev.js
修改web pack.prod.js
const path = require("path");
const ESLintPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
filename: "main.js",
clean: true
},
module: {
rules: [
// loader的配置
{
test: /.css$/,
/**
* 执行顺序 从右到左(从上到下)
* css-loader 将css资源编译成commonjs的模块到js中
* style-loader 将js中的css通过创建style标签添加到html中生效
* index.html文件中不会看到style标签 需要通过f12查看elements面板
*/
use: ["style-loader", "css-loader"],
},
{
test: /.less$/,
// loader: 'xxx' // 只能使用一个loader
use: ["style-loader", "css-loader", "less-loader"],
},
{
test: /.s[ac]ss$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
{
test: /.styl$/,
use: ["style-loader", "css-loader", "stylus-loader"],
},
{
test: /.(png|jpe?g|gif|webp|svg)$/,
type: "asset",
parser: {
dataUrlCondition: {
/**
* 小于20kb的图片转base 64
* 优点:减少请求数量
* 缺点:体积会更大 体积较小图片可以忽略
*/
maxSize: 20 * 1024, // 20kb
},
},
generator: {
/**
* 输出图片名称
* [hash:10] hash值取前10位
* [ext] 文件扩展名
*/
filename: "static/images/[hash:10][ext][query]",
},
},
{
test: /.(ttf|woff2?)$/,
// 字体图标不需要转base 64 采用asset/resource方式打包输出
type: "asset/resource",
generator: {
filename: "static/fonts/[hash:10][ext][query]",
},
},
{
test: /.js$/,
// node_modules目录下文件无需编辑
exclude: /node_modules/,
loader: "babel-loader",
},
],
},
// 插件
plugins: [
new ESLintPlugin({
// 指定检查文件的根目录
context: path.resolve(__dirname, "../src"),
}),
new HtmlWebpackPlugin({
/**
* 以 src/index.html 文件为模板创建新的html文件
* 新html文件特点: 内容和源文件一致 自动引入打包生成的js等资源
*/
template: path.resolve(__dirname, "../src/index.html"),
}),
],
mode: "production",
};
生产模式运行指令: npx webpack --config ./config/webpack.prod.js
配置运行指令
为方便不同模式的指令,我们在package.json中scripts进行定义
"scripts": {
"start": "yarn dev",
"dev": "webpack serve --config ./config/webpack.dev.js",
"build": "webpack --config ./config/webpack.prod.js"
},
开发模式:yarn start
生产模式:yarn build
CSS优化
提取CSS
可以看到我们当前的打包输出中script标签中会配置defer属性,会使得js脚本在html解析完成后才执行,而此配置中我们的css样式资源也被打包到js文件中,这就会导致页面会出现短暂的样式缺失(白屏),这种情况我们就需要对css进行单独打包,并通过link标签引入html文件(link引入的外部样式文件是异步加载的)。达到html与css并行加载的目的。
script标签中defer/async与html资源解析加载图示:
浏览器渲染页面过程:
为实现以上效果,首先需安装插件yarn add mini-css-extract-plugin
本插件会将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。
⚠️ 注意,如果你从 webpack 入口处导入 CSS 或者在 初始 chunk 中引入 style,
mini-css-extract-plugin则不会将这些 CSS 加载到页面中。请使用html-webpack-plugin自动生成link标签或者在创建index.html文件时使用link标签。
修改配置
// config/webpack.prod.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
// xxx
module: {
// 替换style-loader
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: [
// 提取css成单独文件
new MiniCssExtractPlugin({
// 定义输出文件名和目录
filename: "css/main.css",
}),
]
};
观察运行结果
运行yarn build,观察输出打包文件目录
├── dist (Webpack打包输出文件夹)
├── css
│ └── main.css
├── static
│ └── fonts
│ └── images
├── index.html
├── main.js
CSS兼容性处理
yarn add postcss-loader postcss postcss-preset-env
posts-loader 使用此loader对css增加前缀,保证浏览器兼容性
修改配置
// config/webpack.prod.js
module.exports = {
// xxx
module: {
rules: [
{
test: /.css$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"postcss-preset-env", // 能解决大多数样式兼容性问题
]},
},
},
],
},
],
},
};
添加browserslist控制样式兼容性程度
// package.json
{
"browserslist": ["last 2 version", "> 1%", "not dead"]
}
由于我们的配置中包含了css less scss stylus 每一个都加上posts-loader的话 代码过于冗余 这里进行函数封装
// config/webpack.prod.js
const getStyleLoaders = (loader) => {
return [
MiniCssExtractPlugin.loader,
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"postcss-preset-env", // 能解决大多数样式兼容性问题
],
},
},
},
loader,
].filter(Boolean);
};
module.exports = {
// xxx
module: {
rules: [
{
test: /.css$/,
use: getStyleLoaders(),
},
{
test: /.less$/,
use: getStyleLoaders("less-loader"),
},
{
test: /.s[ac]ss$/,
use: getStyleLoaders("sass-loader"),
},
{
test: /.styl$/,
use: getStyleLoaders("stylus-loader"),
},
],
},
};
CSS压缩
yarn add css-minimizer-webpack-plugin
这个插件使用 cssnano 优化和压缩 CSS。
就像 optimize-css-assets-webpack-plugin 一样,但在 source maps 和 assets 中使用查询字符串会更加准确,支持缓存和并发模式下运行。
修改配置
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
// xxx
plugins: [
// css压缩
new CssMinimizerPlugin(),
]
};
总结
至此,一个基础的webpack配置就已经完成了。后续将会考虑如何通过webpack进行种种优化。