网上流传一句话 “webpack工程师 > 前端工程师” 。
Webpack作为这些年主流的前端构建工具(目前仍是),可以说它前端工程化的核心。
学好Webpack有助于塑造个人优势,塑造简历亮点。
本篇文章将深入讲解webpack绝大部分配置项,核心功能等。包括不限于以下内容
output inputCSS资源加载相关loaderbrowserlist配置规则及哪些工具支持browserlistplugindevtool sourcemap配置这篇文章原写于去年8-9月份,发表在老博客上,因博客迁移,决定所有文章重新翻写整理,主要目的还是为了知识巩固和梳理。这篇文章主要参考 《小码哥深入Webpack5等构建工具(gulp/rollup/vite)》 视频课,包含视频课的大半内容(因为近3/4的内容都是讲webpack)。边听边实践,因为技术在变化,npm包也在升级,所以文章也融入了新的知识点,有些知识点也进行了扩展完善。总之本篇文章会非常的长,介绍了Webpack大部分配置及功能。
这里也友情提示一下,本文所有案例及demo演示 请点击这里
webpack is a static module bundler for modern javascript applications
webpack 是一个静态的模块化打包工具, 为现代的JavaScript应用程序。
bundler打包工具:webpack可以将帮助我们进行打包,所以它是一个打包工具;static静态资源:将代码打包成最终的静态资源(部署到静态服务器);module模块化:webpack默认支持各种模块化开发,ESM、CommonJS、AMD等;modern现代的:现代前端开发面临各种各样的问题,才催生了webpack的出现和发展;
babel、postcss、eslint、typescript、rollup等,这些工具是干什么的,之间有什么依赖,如何搭配使用等,让人头晕目眩迷失方向;vue-cli/react-createapp,虽然这些脚手架都号称开箱即用,但具体如何配置,及面对业务的特殊性和复杂性无从下手(不知道cli配置项意味着什么,不知道cli配置项如何映射到webpack.config.js);js,离开cli不知道webpack怎么用,比如如何在原生项目中使用webpack,如何在vue项目中使用react;Rollup
rollup是一个模块化打包工具,它默认只处理ES Module,当然也可以是通过添加Plugin处理其他模块(cjs, amd)。rollup相比webpack功能要少很多,配置简单,构建后的代码易读,通常使用webpack构建应用程序,使用rolup构建类库Vite
Vite号称 下一代的前端开发与构建工具
webpack、rollup、Parcel等工具极大的改善了前端工程化的体验,但是面对大型项目JS代码指数增长,这些工具还是会遇到性能瓶颈,热更新及构建都比较耗时,然而vite要比上述工具快很多。Vite介绍的很美好,但是Webpack流行的这些年(2015-2022)沉淀了大量的企业项目,你说Vite能取代它吗?另外友情提示一下Vite的配置和Webpack的配置很像,高手都是借力打力,不会刻意重复造轮子,有造轮子的零件就不会自己再造零件。Gulp
gulp是一个基于流(pipeline模式)的自动化构建工具| - | Gulp | Webpack |
|---|---|---|
| 理念 | 定义一系列任务然后执行,基于流的自动化构建工具 | 模块化打包工具 |
| 底层原理 | pipeline设计模式 | 基于tapable的微内核架构 |
| 优缺点 | gulp思想更加的简单、易用,更适合编写一些自动化的任务 | 对于大型项目还是使用Webpackgulp默认也是不支持模块化的 |
npm i webpack webpack/cli
webpack webpack核心 可类比babel的@babel/core 可以通过编程的方式运行webpackwebpack-cli 提供webpack命令行运行 可类比babel的 @babel/cli
webpack默认支持JS和JSON模块,如果想加载其他类型的文件需要添加对应的loaderJS模块,默认支持 ESM、CJS、AMDjavascriptconst path = require('path');
module.exports = {
entry: "./src/index.js",
output: {
filename: 'bundle.js',
// path: './dist' // 必须是绝对路径
path: path.resolve(__dirname, './dist')
}
}
// 执行 `npx webpack` 生成 `dist/bundle.js`
// 如果想让bundle.js运行还需要用一个html引用该文件,
// 借助vscode插件live-server或 http-server命令在浏览器上打开页面
entry 打包入口,可以类比 rollup的input,值可以是 String 、Array 、Object
String 单入口打包Array 也是单入口打包, 会合并成一个文件,导出以最后一个文件为准Object 多入口打包,需要将output.filename配合使用 filename: "[name].js"output 打包后的文件输出配置
path 输出文件的目录, 必须是绝对路径,filename 输出文件的文件名, 这里有一些占位符output占位符
id 加载模块的序号name 与enrty的文件名相对应hash 整合项目的hash,每个文件的hash都是一样,只要有一个文件改动,所有资源文件的hash都会跟着变更,chunkhash 根据entry进行解析,生成相应的hash, 每个文件的hash不一样,主文件变更不影响依赖文件的hashcontenthash 与chunhash区别很小,通常在生产环境需要CSS单独抽取成一个包,就用contenthash这样css改变,不改变js文件的hash[ext] 处理文件扩展名<length> hash长度,默认32位字符[path] 文件相对于webpack的路径这里给一些示例:
output.filename: "[name]-[chunkhash:8].js"url-loader 中的配置 options.name: "img/[name].[hash:8].[ext]"loader 模块转换器,处理某种类型的文件module 在webpack中万物皆模块,loader就在module里面配置,一个文件可能需要多个loader处理。module的配置非常灵活,在下面的CSS章节讲解plugins 作用于webpack整个构建过程,webpack有一个生命周期钩子概念,plugin可以在webpack运行在某一阶段帮你做一些事情。(这些钩子底层是使用tapable实现的)devtool 目标代码与源码之间的映射关系 即sourcemapdevServer 开发环境一些配置 如热更新,接口代理等optimization 性能优化相关 如treeSharking 代码分割 DII缓存等resolve 配置别名 省略文件后缀外部资源配置等mode webpack4新增的, 有三个值 none production developmentjavascriptmodule: {
rules: [
{
test: /\.xxx$/, // 指定匹配的文件类型 必须是正则表达式
use: [ // 一种类型的文件可能需要多个经过多个loader进行处理
// loader 的执行顺序是从后到前
{
loader: 'xxx-load', // 指定使用的loader
// 该loader的配置项,不同loader配置项不同需要查看官方文档
options: {}
},
{
// 。。。 其他loader
}
]
}
]
}
// 1. 如果loader没有配置项可简写为 'xxx-load'
// 2. 如果该类型的文件只需要一个loader处理 则use可以简写成 ['xxx-loader']
// 3. 还可以用 loader: 'xxx-loader' 它是 Rule.use:[{loader}] 的简写
如果我们直接在js文件中引入css,webpack编译会报错

报错提示我们添加一个合适的loader处理该类型的文件
javascriptmodule: {
rules: [
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' }
]
}
]
}
css-loader 只负责解析css插入到js文件中style-loader 将js文件中的css代码插入到dom中通过添加loader处理其他资源只是其一方法(也是最主要的方式),事实上webpack有三种方式处理其他资源,以css为例子
import"style-loader!css-loader!./css/style.css";--module-bind
webpack5文档中已经废弃了javascript// 配置less npm i -D less less-loader
{
test: /\.less$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{ loader: 'less-loader' },
]
},
// 配置scss npm i -D dart-sass sass sass-loader
{
test: /\.scss$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{ loader: 'sass-loader' },
]
}
sass 有 dart-sass 和 node-sass两种编译器可选,官方推荐 dart-sass
node-sass 后来才有 dart-sassdart-sass性能好 (node-sass实时编译,dart-sass 保存时编译)node-sass国内经常出现安装失败,node-sass与node版本有依赖关系然后我们尝试在css中引入less @import "./test.less"发现 test.less并未生效,这时候则要使用css-loader配置项了
javascript{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
importLoaders: 1 // 后面有几个loader配置几
}
},
{ loader: 'less-loader' },
]
},
browserslist我们知道babel为我们提供了将es6+代码编译为es5(目标浏览器可以运行的)代码,那样CSS有没有类似的工具吗?
-- 肯定是有的
postcss | @babel/core |
|---|---|
postcss-cli | @babel/cli |
postcss-preset-env | @babel/preset-env |
它和babel一样也会读取.browserslitrc文件或pkg.browserslist字段
再说postcss之前,我们先来看一下browserslist
**browserslist**是一个不同的前端工具之间,共享目标浏览器和Node.js的版本配置
以下工具在使用时会读取browserslist配置
css在目标浏览器上不支持normalizebabel、postcss类似,如果你使用了浏览器的一些特性如 Service Worker, WebAssembly, CSS Grid Layout 它会告知用户升级浏览器Q: 我们知道市场上有大量的浏览器,它们的市场占有率多少?我们要不要兼容?
A: 可以使用caniuse查询 点这里
Q:browserlist如何根据配置计算需要兼容的目标浏览器呢?
A: 它使用的是caniuse-lite的工具,这个工具的数据来自于caniuse的网站上;
browserslist 可以通过.browserslistrc文件或者pkg.browserslist字段配置
可以通过browserslist命令 检查配置是否正确 也可以在线查询
例如 npx browserslist ">1%, last 2 version, not dead"
和前面的工具(less、sass、webpack)一样,browserslist也有cli
npm i -D browserslist
配置项介绍
defaults 默认配置> 0.5%, last 2 versions, Firefox ESR, not dead5% 通过全局使用情况统计信息选择的浏览器版本。 可以使用>=,<和<=这些符号。
5% in US:使用美国使用情况统计信息。它接受两个字母的国家/地区代码。> 5% in alt-AS:使用亚洲地区使用情况统计信息。有关所有区域代码的列表,请参见caniuse-lite/data/regions> 5% in my stats:使用自定义用法数据。> 5% in browserslist-config-mycompany stats:使用自定义的数据 browserslist-config-mycompany/browserslist-stats.json。cover 99.5%:提供覆盖率的最受欢迎的浏览器。cover 99.5% in US:与上述相同,但国家/地区代码由两个字母组成。cover 99.5% in my stats:使用自定义用法数据。dead:24个月内没有官方支持或更新的浏览器。现在是IE 10,IE_Mob 11,BlackBerry 10,BlackBerry 7, Samsung 4和OperaMobile 12.1。last 2 versions:每个浏览器的最后2个版本。
last 2 Chrome versions:最近2个版本的Chrome浏览器。last 2 major versions或last 2 iOS major versions:最近2个主要版本的所有次要/补丁版本。node 10和node 10.4:选择最新的Node.js10.x.x 或10.4.x版本。
current node:使用当前环境中的Node.js版本。maintained node versions:所有Node.js版本,仍由 Node.js Foundation维护。iOS 7:直接使用iOS浏览器版本7。
Firefox > 20:Firefox的版本高于20, >=,<并且<=也可以使用。它也可以与Node.js一起使用。ie 6-8:选择一个包含范围的版本。Firefox ESR:最新的Firefox ESR版本。PhantomJS 2.1和PhantomJS 1.9:选择类似于PhantomJS运行时的Safari版本。extends browserslist-config-mycompany:从browserslist-config-mycompanynpm包中查询supports es6-module:支持特定功能的浏览器。 es6-module这是“我可以使用” 页面feat的URL上的参数。有关所有可用功能的列表,请参见 。caniuse-lite/data/featuresbrowserslist config:在Browserslist配置中定义的浏览器。在差异服务中很有用,可用于修改用户的配置,例如 browserslist config and supports es6-module。since 2015或last 2 years:自2015年以来发布的所有版本(since 2015-03以及since 2015-03-10)。unreleased versions或unreleased Chrome versions:Alpha和Beta版本。not ie <= 8:排除先前查询选择的浏览器。Q: 我们编写了多个条件之后,多个条件之间是什么关系呢?
A:
text>1% last 2 version not dead
textAndroid >= 4.4 ios >= 8
textchrome >= 52 ios >= 12
这里有个疑问,为什了android 7.0不能通过Android >= 7这种配置?
这要要了解下android的webview, 可以看我的另篇文章 android系统webview, 上述配置 android 7.0系统的webview大致对应 chrome52版本
可以通过以下两个链接去匹配android4.4以上系统的webview对应chrome的版本
postcss 是一个通过JS来进行CSS的转换和适配,比如增加浏览器前缀,浏览器样式初始化,将CSS新语法转换成目标浏览器可以支持的CSS,但是实现这些功能要借助postcss插件。
npm i -D postcss postcss-clicss.content {
user-select: none;
display: flex;
color: red;
}
运行 npx postcss --use autoprefixer -o ./dist/end.css ./src/css/start.css
编译后的代码如下
css.content {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
display: flex;
color: red;
}
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJ......
.browserslist如下txtAndroid >= 4 firefox > 20
css.content {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
display: -webkit-box;
display: -moz-box;
display: flex;
color: red;
}
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJ......
autoprefixer 在线查询
我们以可以使用postcss配置文件 postcss.config.js
jsmodule.exports = {
plugins: [
require('autoprefixer'),
]
}
npx postcss -o ./dist/end.css ./src/css/start.css 得到一样的效果真实的项目中我们肯定不会用命令行对css进行处理,以上演示只是为了快速了解上手postcss
postcss-loader在webpack中添加postcss-loader来集成postcss的功能
javascript{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
require('autoprefixer')
]
}
}
}
postcss-preset-env目前来说autoprefixer已经过时了,被postcss-preset-env替代了
它处理实现了autoprefixer的功能还可以帮助我们将一些现代的css转换成大部分浏览器可以识别的css
比如使用了16进制颜色这里有8位
env.csscss.content {
background-color: #12345678;
}
postcss.config.jsjsmodule.exports = {
plugins: [
require('postcss-preset-env'),
]
}
执行 npx postcss -o ./dist/env.css ./src/css/env.css
dist/env.css
css.content {
background-color: rgba(18,52,86,0.47059);
}
总结cssloader配置顺序, 以scss为例
jsmodule.exports = {
// ...其他配置项
module: {
rules: [
{
test: /\.scss$/,
use: [
'style-loader', // 或者其他的 CSS 注入 loader
{
loader: 'css-loader',
options: { // 解决.css文件中引入.scss文件编译问题
importLoaders: 2
}
},
'postcss-loader' // 这里不写配置项会读取 .postcss.config.js
'sass-loader', // 先执行它,loader从下到上执行
]
}
]
}
};
src/index.jsjsimport "./css/index.css";
import smallImageUrl from './img/38.jpg'
const smallImage = new Image();
smallImage.src = smallImageUrl;
document.body.appendChild(smallImage);
const bigImage = document.createElement('div');
bigImage.className = 'big-img';
document.body.appendChild(bigImage);
src/css/index.csscss.big-img {
background-image: url("../img/295.png");
background-size: contain;
height: 200px;
width: 200px;
display: inline-block;
background-color: red;
}
module.rules增加代码如下js{
test: /\.(je?pg|png|gif)$/i,
// webpack5 不再需要file-loader url-loader raw-loader
// 它内置了 资源模块类型 asset module type
type: 'asset',
generator: { // 也可以配置 output.assetModuleFilename: 'img/[name].[chunkhash:6][ext]'
filename: 'img/[name].[hash:6][ext]'
},
parser: {
dataUrlCondition: {
maxSize: 100 * 1024,
}
}
}
webpack5 新增了静态文件类型asset module type
取代了file-loader url-loader``raw-loader
asset/resource 发送一个单独的文件并导出URL, 替代file-loaderasset/inline 导出一个资源的data URI (base64) ,替代 url-loaderasset/source 导出资源的源代码, 替代raw-loaderasset 可以配置资源大于某个值,使用单独文件方式,反之使用base64方式,取代url-loader + limit配置尽管file-loader和url-loader不用了,但是很多项目中还是存在这两个loader,这里有必要再简单说一下
file-loader 用来处理静态资源,当我们只是包资源文件移动到打包目录就需要用file-loaderjavascript{
test: /\.(je?pg|png|gif)$/,
use: [
{
loader: 'file-loader',
options: {
// name: "img/[name].[hash:6].[ext]",
name: "[name].[hash:6].[ext]",
outputPath: 'img'
}
}
]
}
url-loader 默认将静态资源以base64的方式引入, 可通过配置项limit设定小文件走base64, 大文件走url, 网上也有中说法:它是file-loader的加强版javascript{
test: /\.(je?pg|png|gif)$/,
use: [
{
loader: 'url-loader',
options: {
name: "[name].[hash:6].[ext]",
outputPath: 'img',
limit: 50 * 1024, // 50kb 一般配置10kb都顶天了,这里只是为了演示
}
}
]
}
注意事项
- 使用
url-loader、file-loader要安装css-loader@5若最新的css-loader6.7.1,则资源文件无法正确运行
js// 创建一个i元素, 设置一个字体
import "./font/iconfont.css";
const iEl = document.createElement("i");
iEl.className = "iconfont icon-ashbin why_icon";
document.body.appendChild(iEl);
font/iconfont.css
font/iconfont.eot
font/iconfont.ttf
font/iconfont.ttf2
配置loader
json{
test: /\.ttf|eot|woff2?$/i,
type: "asset/resource",
generator: {
filename: "font/[name].[hash:6][ext]"
}
}
plugin 作用于webpack的生命周期中,执行更广泛的任务 如打包优化、资源管理、环境变量注入等
之前我们每次打包都需要手动清一下dist目录
或者使用其他辅助方式 如 rm -rf ./dist && npx webpack
这种方式的缺点是一旦我们webpack的配置output.path改变,命令也需要跟着改变
clean-webpack-plugin就不存在上述问题
jsconst { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
// ...
plugins: [
new CleanWebpackPlugin(),
]
}
前面构建后的静态文件想要运行,我们手动写了一个html导入,这是不太规范的,我们希望能够自动化构建。
html 如配置title icon等示例
public/index.html (内容copy自vue-cli项目)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">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
favicon.ico (copy自vue-cli项目)
配置wepack.config.js
jsconst HtmlWebpackPlugin = require('html-webpack-plugin');
const { DefinePlugin } = require('webpack');
module.exports = {
plugins: [
// 1. 如果不配置template选项会使用该插件默认的模版
// new HtmlWebpackPlugin({
// title: 'webpack html 模版',
// }),
new HtmlWebpackPlugin({
title: 'webpack html 模版', // 对应html <%= htmlWebpackPlugin.options.title %>
template: './public/index.html'
}),
new DefinePlugin({
BASE_URL: JSON.stringify('./')
})
]
}
<% 变量 %> 是ejs模版语法, html-webpack-plugin使用的是ejs模版动态填充数据HtmlWebpackPlugin传入json数据 在模版中通过 htmlWebpackPlugin.options.xxx获取<%= BASE_URL %> 这个是全局变量需要使用DefinePlugin传值,DefinePlugin是webpack内置的插件DefinePluginDefinePlugin 定义的全局变量除了在html中使用,还可以在js中使用,通常用来做按配置打包webpack.config.jsjsmodule.exports = {
entry: ["./src/index.js", "./src/define-plugin.js"],
plugins: [
new DefinePlugin({
ENV: JSON.stringify('development')
})
]
}
src/define-plugin.jsjsif(ENV === 'development') {
console.log('你将在控制台看到这段内容')
} else {
console.log('这段内容既不会在控制台打印,也不会在bundle.js找到');
}
// 上述代码会经过DefinePlugin处理编译成如下代码
// console.log('你将在控制看看到这段内容')
copy-webpack-plugin上述的案例有个问题就是 favicon.ico 没能正确加载,favicon.ico同index.html一样在 public目录,
在vue-cli项目中,public目录的文件除了index.html都要copy到dist目录,
这时候就用到了copy-webpack-plugin
jsnew CopyWebpackPlugin({
patterns: [
{
from: 'public',
globOptions: {
ignore: [
"**/.DS_Store",
"**/index.html"
]
}
}
]
})
再说source-map之前简单介绍下mode
Mode配置选项,可以告知webpack使用响应模式的内置优化
production(什么都不设置的情况下);'none' | 'development' | 'production';
development 与 production的区别

mdn-source-map介绍
最初source-map生成的文件带下是原始文件的10倍,第二版减少了约50%,第三版又减少了50%,所以目前一个
133kb的文件,最终的source-map的大小大概在300kb。

准备工作
jsimport '../css/index.css';
import {add} from './math'
console.log('add(1,2,3) ', add(1,2,3));
jsexport function add(...arr) {
console.log(wesd);
return arr.reduce((sum, item) => sum + item, 0);
}
csshtml, body {
height: 100%;
width: 100%;
}
body {
background-color: aquamarine;
}
mode: 'production'的默认值 不填devtools,它的默认值就是(none)


mode: 'development' 的默认值


js文件及定位到报错行列,但是js文件不太干净css样式文件
eval的原理是浏览器对eval代码字符串后面的 //# sourceURL= 有特殊的解析[eval-|inline-|hidden-]source-mapsource-map


js错误所在文件及行列号,文件内容与源码一致
eval-source-map
source-map效果一样,只是不单独生成source-map文件,而是把source-map放在eval函数中
inline-source-map
source-map效果一样,不单独生成source-map文件, 而是放到打包文件末尾。
hidden-source-map
source-map效果一致,只是隐藏了source-map链接(删除了 //# sourceMappingURL=bundle.js.map)

source-map,能定位到错误文件,但看不到内容(没有sourcesContent)[cheap-[module-]]source-mapcheap是廉价的意思 会生成source-map但更高效一些因为他没有列映射

cheap-source-map与cheap-module-source-map很类似,但是对源自loader的source-map处理会更好,比如当你使用babel的时候,你会发现后者的source-map更友好
cheap-source-map与cheap-module-source-map 对比准备工作
js{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env'
]
}
}
]
}
js// CommonJS导出内容, es module导入内容
import {dateFormat} from './CommonJS.js';
// es module导出内容, CommonJS导入内容
const {add} = require('./math');
console.log('add(1,2,3) ', add(1,2,3));
console.log('dateFormat() ', dateFormat());
console.log(123+x);
CommonJS.js math.js自己准备吧

cheap-module-source-map还有一个优势,能定位到css文件事实上webpack给我们提供了26个值!
别慌,因为他们是可以组合的
[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
[inline-|hidden-|eval]:三个值时 三选一或不选 共4种nosources:可选值; 共2种cheap: 可选值,并且可以跟随module的值; 共3种
那么我们应该选择哪一种呢?
source-map或者cheap-module-source-map(none)或 hidden-source-map 更推荐 (none)关于babel/eslint,在我的其他文章中有细节的讲解(《Babel深入浅出》 《Eslint深入浅出》),但是这里还是大致提一嘴核心知识点。
关于TS,笔者也有单独的篇幅讲TS,这里把这些知识点用webpack串起来。
babel和typescript都可以翻译es6+,推荐使用@babel/preset-typescript翻译es6,原因是
typescript只翻译es6句法,不提供es6API的垫片,这是致命的缺点babel更灵活,可以根据目标浏览器按需转译,而TS只能转移到指定ECMA版本@babel/preset-typescript的主要缺点是丢失了类型检测,可以使用typescript单独做类型检测 npx tsc --noEmit --watchbabel支持ts 有的项目使用的是 @babel/plugin-transform-typescript,笔者更推荐使用@babel/preset-typescript, 理由是后者包含前者
ESLint Prettier EditorConfig三者之间关系
EditorConfig提供配置磨平不同编辑器代码书写是的差异,作用于代码书写和预览阶段
Prettier 处理代码风格问题,作用于代码保存和提交阶段,格式化快捷键
Eslint 不仅是关注代码风格,还关注代码质量
JS/TS,Prettier更全面,且还支持HTML/CSS/Markdown 等文件npx eslint --fix src格式化代码Prettier与EditorConfig冲突
Prettier 配置 > editorconfig 配置 > Prettier 默认值。editorconfig作用的文件类型更广,相交属性交给editorConfig管理,其他属性交给prettier管理ESLint与Preitter冲突
eslint-config-airbnb、eslint-config-standard这时候可以不使用prettiereslint-plugin-prettier关闭ESLint中与Prettier交叉的配置,eslint-plugin-prettier插件,将prettier融合到eslint示例代码
.editorconfig 文件ini# https://editorconfig.org
# 已经是顶层配置文件,不必继续向上搜索
root = true
[*]
# 编码字符集
charset = utf-8
# 缩进风格是空格
indent_style = space
# 一个缩进占用两个空格,因没有设置tab_with,一个Tab占用2列
indent_size = 2
# 换行符 lf
end_of_line = lf
# 文件以一个空白行结尾
insert_final_newline = true
# 去除行首的任意空白字符
trim_trailing_whitespace = false
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false
.prettierrc.json json{
"printWidth": 100,
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"useTabs": false
}
.eslintrc.jsnpx eslint --init 生成模版prettier融入eslint
npm install -D eslint-config-prettier eslint-plugin-prettiernpm install -D --save-exact prettierjsmodule.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
plugins: ['@typescript-eslint', 'prettier'],
overrides: [],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
},
rules: {
'prettier/prettier': 'error',
},
};
eslint-loaderjson{
test: /\.[j|t]s$/,
exclude: /node_modules/,
use: [
"babel-loader",
"eslint-loader",
]
}
TypeError: Cannot read property 'getFormatter' of undefinednpm i -D eslint@7前面的打包后的文件都是依靠 http-server 命令或者 VSCode插件live-server来运行,而且每次修改代码后都需要手动打包效率较低。本段落我们来自己搭建本地服务器
官方给出了三种可选的方式
webpack配置项watch 监控文件发生改变会重新编译
--watchwatch: truewebpack-dev-server 内置了live-server和socket热更新
npx webpack-dev-server 或 npx webpack server 启动webpack-dev-middlewares
webpack处理后的文件发送到serverexpress启动一个服务,webpack-dev-middlewares会生成一个express中间件示例代码
pkg.script 增加 server: 'node server.js'server.js内容如下jsconst express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config');
const app = express();
const compiler = webpack(config);
const middleware = webpackDevMiddleware(compiler);
app.use(middleware);
app.listen(9081, function () {
console.log('webpack-dev-middlewares start by express on port 9081');
});
webpack-dev-server启动后不会输出任何文件memory-fs)HMR 是什么?
Hot Module Replacement 模块热替换相对live server的优势
webpack-dev-server 可以通过配置项devServer.hot: true 开启HMR,
但实测发现, css改动触发了热更新, 但JS改动还是会刷新页面,
这个原因也很简单,css很好处理 删除style标签再插入即可,而JS的处理比较麻烦,比如输入框文字,内存变量等,它不知道怎样做,webpack提供了 module.hot.accept方法,你监听哪些文件,哪些文件就知道怎么热更而不刷新页面
示例代码
index.js 入口文件jsimport createComp from './js/comp';
const comp = createComp('h2', { class: 'content' }, 'hello webpack');
document.body.appendChild(comp);
if (module.hot) {
module.hot.accept('./js/comp', () => {
console.log('./js/comp模块热更新了');
document.body.removeChild(document.body.querySelector('.content')!);
const comp = createComp('h2', { class: 'content' }, texts.at(-1));
document.body.appendChild(comp);
});
}
js/comp.jsjsfunction createComp(
tag = 'div',
attrs = {},
children = ''
) {
const ele = document.createElement(tag);
Object.entries(attrs).forEach(([k, v]) => {
ele.setAttribute(k, v);
});
if (typeof children === 'string') {
ele.appendChild(document.createTextNode(children));
} else {
children.forEach((child) => {
if (typeof child === 'string') {
ele.appendChild(document.createTextNode(child));
} else {
ele.appendChild(createComp(child.tag, child.attrs, child.children));
}
});
}
return ele;
}
export default createComp;
对于框架而言,早已提供了解决方案
配置Vue热更新
npm i vue@2index.js 增加代码 import './js/loadVue';js/loadVue.jsjsimport Vue from 'vue';
import App from './app.vue';
const vueRoot = document.createElement('div');
document.body.appendChild(vueRoot);
new Vue({
render: (h) => h(App),
}).$mount(vueRoot);
js/app.vuevue<template> <div class="vue-comp"> <div>{{message}}</div> <input type="text" v-model="message"> </div> </template> <script> export default { data() { return { message: '这是一个vue组件' } } } </script> <style> .vue-comp { border: 1px dotted blue; padding: 20px; text-align: center; display: flex; flex-direction: column; } </style>
npm i vue-loader@15 vue-template-compilerjs{
test: /\.vue$/,
use: 'vue-loader'
}
import VueLoaderPlugin from 'vue-loader/lib/plugin';plugins 增加 new VueLoaderPlugin(),resolve.extensions字段 增加 .vue配置React热更新
import './js/loadReact';jsximport React from 'react';
import ReactDom from 'react-dom';
import ReactApp from './reactApp.jsx';
const reactRoot = document.createElement('div');
document.body.appendChild(reactRoot);
ReactDom.render(<ReactApp />, reactRoot);
js/reactApp.jsxjsximport React, { Component } from 'react';
import '../css/reactapp.scss';
export default class ReactApp extends Component {
constructor() {
super();
this.state = {
message: 'Hello React',
};
}
render() {
return (
<div className="react-app">
<div>{this.state.message}</div>
</div>
);
}
}
css/reactapp.scssscss.react-app {
background-color: red;
padding: 20px;
color: white;
font-size: 40px;
}
npm i react react-dom@17 -S && npm i -D @babel/preset-react react-refresh @pmmmwh/react-refresh-webpack-plugin'@babel/preset-react''react-refresh/babel'test: /\.(js|jsx)$/i,new ReactRefreshPlugin(),
webpack-dev-server 提供两个服务, 一个静态资源的服务(express), 一个Socket服务
socket链接socket发送信号(变更文件路径及hash值),hash比对,是否需要拉取代码,拉取代码通过dom插入script标签或eval函数执行代码webpack-dev-server v4 相对 v3发生了很多变更 https://github.com/webpack/webpack-dev-server/blob/master/migration-v4.md
output.publicPath该文件指定index.html引用静态资源的基准路径
devServer.publicPath 该服务指定本地服务所在的文件夹,建议与output.publicPath一致
devServer.devMiddleware.publicPathdevServer.contentBase v4版本已变更为 devServer.static.directiry
html里面有引入外部资源 ,比如 lib/a.js'lib'index.html上通过因为该资源<script src='a.js'></script> 则可以正常加载hotOnly当前代码编译失败,是否刷新整个页面 (默认是会刷新)
hot: 'only'host设置主机地址
localhost是个域名,会被解析成127.0.0.1127.0.0.1是一个回环地址,表示我们主机自己发出去的包直接被自己接受
127.0.0.1时,在同一个网段下的主机 中,通过ip地址是不能访问的;0.0.0.0 监听ipV4所有的地址,再根据端口找到不同的应用程序
host配置域名,随便什么域名都可以访问port open 不解释compress是否为静态文件开启gzip compression v4默认值truehistoryApiFallback 它用于解决SPA页面在路由跳转后,进行页面刷新时,返回404的错误。
boolean值: 默认值是 false, true表示刷新页面遇到404错误则返回index.html内容object类型值 , 可以配合rewrites属性devServer中实现historyApiFallback功能是通过connect-history-api-fallback库的前后端分离项目, 往往遇到跨域问题,可以通过这个选项解决跨域问题
准备一个接口
abc.json文件 内容你随便填http-server -p 8080在项目里增加测试代码
javascriptimport axios from 'axios';
axios
.get('http://localhost:8080/abc.json')
.then(console.log)
.catch(console.error);
启动项目在浏览器可以看到跨域问题

devServer.proxy对象是一个 key-value 的键值对,key表示路径,value可以是string或json,
当为string时,key建议不要给空字符串,所以我们配置如下
"/api": "http://localhost:8080"
同时修改请求path axios.get('/api/abc.json')
这时候其实请求的是 http://localhost:8080/api/abc.json该路径其实不存在

我们需要想办法把 /api去掉, 这时就要把value配置成json
javascriptproxy: {
// "/api": "http://localhost:8080",
"/api": {
target: "http://localhost:8080",
pathRewrite: {
"^/api": ""
},
}
}
target:表示的是代理到的目标地址pathRewrite: 修改路径这时候可以正常拿到数据了

value为对象时,还有一些其他值
secure 默认情况下不接收转发到https的服务器上,如果希望支持,可以设置为false;changeOrigin它表示是否更新代理后请求的headers中host地址
falsetrue时, 在上例中 等于把host请求头从 'localhost:9081' 修改为'localhost:8080' (host请求头与跨域有关)当配置信息越来越多,所有配置信息都放在一个文件中,这个文件会越来越不容易维护
有些配置是仅开发时候的,有些配置是仅生产环境才使用的,基于此我们对配置最好进行划分,方面维护和管理。
pkg.script 处命令区分,走不同入口配置文件pkg.script 处传递参数区分环境把将该环境与公共配置聚合。
基于前面一个章节我整理了下webpack配置
代码较多,但改动点并不多,我简述下改动点
webpack.config.js导出一个函数,参数来自命令行,区分开发还是生产环境,赋值给 process.env.NODE_ENVdevServer、ReactRefreshPlugin、babel配置增加插件react-refresh/babel mode为developmentCleanWebpackPlugin CopyWebpackPlugin mode为productionresolve用于设置模块如何被解析,它可以帮助webpack从每一个require/import语句中找到合适的模块代码,webpack使用 enhanced-resolve 来解析文件路径;第一步: 获取绝对路径
webpack能解析三种文件路径
import/require的资源文件所在的目录被认为是上下文路径import/require中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径;resolve.modules中指定的所有目录检索模块;
['node_modules'],所以默认会从node_modules中查找文件;alias的方式来替换初始模块路径第二步: 判断该路径是文件还是文件夹
webpack如何确认一个资源文件?
首先判断是文件还是文件夹
resolve.extensions选项作为文件扩展名解析;resolve.mainFiles配置选项中指定的文件顺序查找
resolve.mainFiles的默认值是['browser', 'module', 'main']package.json, 递归往上查找,找到为止pkg.browser pkg.module pkg.main字段,找到为止index拼接resolve.extensions来解析扩展名resolve.extensions解析到文件时自动添加扩展名
['.wasm', '.mjs', '.js', '.json'];.vue .jsx .ts``.tsxresolve.alias 配置目录别名
jsalias: {
'@': path.resolve(__dirname, './src'),
}
比如有一个文件src/js/a.js则可通过 import '@/js/a.js';方式引入
context的作用是用于解析入口和加载器loader
为了解释清楚这个配置我们来看一个场景
- 项目根目录下有 package.json和webpack.config.js
- webpack配置的入口 entry: './src/index.js'
- 这时候我们把webpack.config.js移动到config目录,
这时候entry不用修改是可以运行的,如果修改entry为 '../src/index.js' 反而不能运行了 原因是 entry的解析是相对于context的,context的默认值是package.json所在目录
一般开始的时候我们使用style-loader,但是生产环境中我们通常把css单独打包到一个文件中。
MiniCssExtractPlugin可以帮助我们将css提取到一个独立的css文件中,该插件需要在webpack4+才可以使用
示例代码如下
js{
test: /\.s?css/,
use: [
isProduction? MiniCssExtrctPlugin.loader : 'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
'postcss-loader',
'sass-loader',
],
}
jsif(isProduction) {
plugins.push(new MiniCssExtrctPlugin({
filename: "css/[name].[hash:8].css",
// chunkFilename: "css/[name].[hash:8].css",
}))
}
shimming用来给我们的代码加一些垫片来处理一些问题
比如 我们使用了一个abc.js的第三方库,但是这个库依赖lodash.js,我们的项目也有使用loadsh但通过import使用,也就是window对象下没有 _这个对象。那我们就需要 ProvidePlugin来实现shimming效果。
示例代码如下
js/shimmingjs// import axios, { get } from "axios";
axios.get("http://123.207.32.32:8000/home/multidata").then((res) => {
console.log(res);
});
get("http://123.207.32.32:8000/home/multidata").then((res) => {
console.log(res);
});
/* eslint-enable */
js// ProvidePlugin 是webpack内置插件
new ProvidePlugin({
axios: "axios",
get: ["axios", "get"]
})
代码分割是指 将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件;
entry配置手动分离代码;Entry Dependencies或者SplitChunksPlugin去重和分离代码;在讲解代码分离之前,我准备了下代码 点这里,代码还是基于上一个段落的配置,删除了资源加载、vue、react的支持
多入口打包 也是属于一种代码分割
jsonentry: {
pageA: "./src/pageA.ts",
pageB: "./src/pageB.ts"
},
output.filename的 值为 "[name].bundle.js"jsonentry: {
pageA: {
import: "./src/pageA.ts",
dependOn: 'lodash'
},
pageB: {
import: "./src/pageB.ts",
dependOn: 'lodash'
},
lodash: 'lodash'
},
jsonentry: {
pageA: {
import: "./src/pageA.ts",
dependOn: ['lodash', 'dayjs']
},
pageB: {
import: "./src/pageB.ts",
dependOn: ['lodash', 'dayjs']
},
lodash: 'lodash',
dayjs: 'dayjs'
},
loadsh.bundle.js、dayjs.bundle.jsjsonentry: {
pageA: {
import: "./src/pageA.ts",
dependOn: 'shared'
},
pageB: {
import: "./src/pageB.ts",
dependOn: 'shared'
},
shared: ['lodash', 'dayjs'],
},
splitChunks.chunks 该属性共有3个值
initial 同步、 async异步、all 同步+异步minSize 代码分割的最小单位默认是 20Kb 即 20*1024maxSize 分割出来的包最大限制
minSize 优先于 maxSizenpm 包大于 maxSize 他会单独打出来,不会再拆分minChunks 最少引用几次,才参与分包cacheGroups 缓存组,cacheGroups结构
tsinterface CacheGroups {
// key可以随便定义
[key: string]: {
test: RegExp;
filename: string; // 可以使用占位符如 "[id]_venders.js"
name: string | Function; // string不可以写占位符
priority:number; // 通常为负数哦,谁大谁优先
// 。。。
};
}
看一个示例
jsoncacheGroups: {
// 我们通常将第三方包单独打出来 叫venders.js
// 第三方包通常不会改变,可以更好的利用强缓存
venders: {
test: /[\\/]node_modules[\\/]/,
filename: "[id]_venders.js",
priority: -10
},
// 将某类文件单独提取出来
// aa: {
// test: /aa_/,
// filename: "[id]_aa.js",
// // name: "aa.js"
// },
// default: {
// minChunks: 1,
// filename: "common_[id].js",
// priority: -20
// }
}
react脚手架splitChunks配置
jssplitChunks: {
chunk: 'all',
name: false,
}
vue3脚手架splitChunks配置
jsvenders: {
name: 'chunk-venders',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial',
}
common: {
name: 'chunk-common',
priority: -20,
minChunks: 2,
chunks: 'initial',
reuseExistingChunk: true,
}
webpack实现了两种动态导入的语法
ECMAScript中的 import() 语法来完成,也是目前推荐的方式;webpack遗留的 require.ensure,目前已经不推荐使用;javascriptconst isDev = process.env.NODE_ENV === 'development';
if (isDev) {
import(
/* webpackChunkName: 'my-vconsole' */
'vconsole'
).then((module) => {
new module.default();
});
// require.ensure([], () => {
// const Vconsole = require('vconsole');
// new Vsonsole();
// });
}
import()语法,可以使用webpack魔法注释
/* webpackChunkName: 'vconsole' *//* webpackPrefetch: true */注意:动态导入的代码需要配置 output.chunkFilename 如
chunkFilename: 'chunk_[id]_[name].js'
js/element.tstsconst element = document.createElement('div');
element.innerHTML = 'Hello Element';
export default element;
pageB.tstsconst button = document.createElement('button');
button.innerHTML = '加载元素';
button.addEventListener('click', () => {
// prefetch -> 魔法注释(magic comments)
/* webpackPrefetch: true */
/* webpackPreload: true */
import(
/* webpackChunkName: 'element' */
/* webpackPrefetch: true */
'./js/element'
).then(({ default: element }) => {
document.body.appendChild(element);
});
});
document.body.appendChild(button);
button 在开发者工具 network js可以看到element.js的加载| prefetch 预获取 | preload 预加载 |
|---|---|
| 将来某些导航下可能需要的资源 | 当前导航下可能需要资源 |
| 只是把资源下载下来,暂时不运行(后面用到的时候在运行) | |
| 设置、命中缓存; 正确使用 preload、prefetch 不会导致重复请求 |
|
| 在父 chunk 加载结束后,空闲的时候开始加载。 | 在父 chunk 加载时,以并行方式开始加载。 |
| prefetch chunk 在浏览器闲置时下载 | 中等优先级,并立即下载 |
一般开发中都是使用于预获取prefetch,它的兼容性要比preload好很多

更多关于 预获取
加载原理 分两步
<link rel="preload" href="./a.js" as="script">jsvar script = document.createElement('script');
script.src = './a.js';
doucment.head.appendChild(script);
在webpack中通过代码魔法注释开启预获取和预加载
/* webpackPrefetch: true *//* webpackPreload: true */该字段告知webpack模块的id采用什么算法生成。
有三个比较常见的值
natural 按数字的顺序使用idnamed development模式下的默认值,一个可读的名称iddeterministic 确定性的,在不同的编译中确定的id
production模式的默认值 这个也是前面演示splitChunks为什么会有id的原因webpack4 没有这个值最佳实践: 开发模式使用named ,生产模式使用deterministic
配置runtime相关的代码是否能抽取到一个单独的chunk中
runtime相关的代码指的是,对模块进行解析、加载模块信息相关的代码。即包含静态资源的mainfest.json,也包含它们之间的引用关系Q: 为什么要抽离runtime ?
抽离出来后,有利于浏览器的缓存策略
js文件叫做bundle,把异步加载的js文件叫做chunk,bundle怎么知道要加载哪些chunk呢? 肯定是在bundle文件中有chunk文件的引用,pageA-chunk.js的hash值肯定发生改变 ==> 在bundle中的引用也发生改变 ==> 进而bundle.js文件的hash也发生改变, 有没办法只重新加载 pageA-chunk.js呢? 这就用到了runtimeThunk。bundle.js通过runtime.js找到pageA-chunk.js, 这样bundle.js就不用重新加载了runtime.js,这不还是要加载两个文件了?runtime包一般都很小,可以借助react-dev-utils/InlineChunkHtmlPlugin(也可以自己写一个plugin)将其内容打包到index.html里面。 因为生产环境下index.html通常设置为协商缓存或不缓存,其他资源设置为强缓存)这样就做到了chunk变更,只加载chunk.js更大程度上利用了缓存。案例如下
src/index.jsjsconst button = document.createElement('button');
button.innerHTML = '加载元素';
button.addEventListener('click', () => {
import(
/* webpackChunkName: 'element' */
'./element'
).then(({ default: element }) => {
document.body.appendChild(element);
});
});
document.body.appendChild(button);
element.jsjsconst element = document.createElement('div');
element.innerHTML = 'Hello Element';
export default element;
jsconst HtmlWebpackPlugin = require('html-webpack-plugin');
const {DefinePlugin} = require('webpack');
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin')
module.exports = {
mode: 'none',
// devtool: 'source-map',
entry: './src/index.js',
output: {
path: path.join(__dirname, './dist'),
filename: '[name].[contenthash:6].js',
chunkFilename: "[name].[contenthash:6].chunk.js",
publicPath: './'
},
optimization: {
chunkIds: 'named',
runtimeChunk: {
name: function(entrypoint) {
return `runtime-${entrypoint.name}`
}
}
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './index.html',
title: 'webpack 代码分割 runtime',
}),
new DefinePlugin({
'process.env.NODE_ENV': JSON.stringify("development"),
}),
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.*.js/]),
]
}
runtime方案二 手写个plugin
jsconfig.plugin('html-inline')
.use(InlineChunkHtmlPlugin, [/runtime-.*.js/])
jsclass InlineChunkHtmlPlugin {
constructor(tests) {
this.tests = tests;
}
apply(compiler) {
compiler.hooks.afterEmit.tap('InlineChunkHtmlPlugin', (compilation) => {
let filePath = Object.keys(compilation.assets)
.find(key => this.tests.test(key));
const fileContent = compilation.assets[filePath]?._value;
const htmlPath = compilation.assets['index.html']?.existAt;
if(!filePath || !htmlPath) return;
filePath = compilation.options.output.path + filePath;
let htmlData = fs.readFileSync(htmlData,'utf-8');
htmlData = htmlData.replace(
/\<body\>/,
`<script>${fileContent}</script><body>`
);
fs.outputFileSync(htmlPath, htmlData);
})
}
}
补充知识: CDN
CDN称之为内容分发网络(Content Delivery Network或Content Distribution Network,缩写:CDN)
- 它通过利用相互连接的网络系统,从最靠近用户的服务器返回资源
- 更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户
- 来提供高性能、可扩展性及低成本的网络内容传递给用户
在开发中,我们使用CDN主要是两种方式
- 方式一:打包的所有静态资源(除去入口
index.html),放到CDN服务器,用户所有资源都是通过CDN服务器加载的;- 方式二:一些第三方资源放到CDN服务器上;
通常一些比较出名的开源框架都会将打包后的源码放到一些比较出名的、免费的CDN服务器上:
- 国际上使用比较多的是
unpkg、JSDelivr、cdnjs;- 国内也有一个比较好用的CDN是bootcdn;
在项目中,我们如何去引入这些CDN呢?
lodash或者dayjs这些库进行打包;一般有一些库或者项目历史原因,我们通过html的script标签引用cdn地址
但是开发环境如果通过外链加载资源首屏加载要慢很多(尽管资源在cdn上)
拿lodash举例,我们在开发时加载本地资源, 打包后加载cdn上的lodash
配置示例如下:
jsconst path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = function (env) {
const isProduction = env.production;
process.env.NODE_ENV = isProduction ? 'production' : 'development';
const externals = {};
if (isProduction) {
// window._
externals.lodash = "_";
}
return {
mode: process.env.NODE_ENV,
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, './dist')
},
externals,
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './index.html',
title: 'webpackp cdn',
}),
]
}
}
jsimport _ from 'lodash';
console.log(_.join(['hello', 'webpack']));
TIP: 静态资源部署到cdn 可以修改output.publicPath 实现
windows电脑中实现共享函数的一种扩展方式webpack中,它指的是我们可以将不经常改变的代码,抽取成一个共享的库;具体步骤如下
1. 打包dll
"dll": "webpack --config ./webpack.dll.js",jsconst path = require('path');
const webpack = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
entry: {
react: ["react", "react-dom"]
},
output: {
path: path.resolve(__dirname, "./dll"),
filename: "dll_[name].js",
library: 'dll_[name]'
},
optimization: {
minimizer: [
new TerserPlugin({
extractComments: false
})
]
},
plugins: [
new webpack.DllPlugin({
name: "dll_[name]",
path: path.resolve(__dirname, "./dll/[name].manifest.json")
})
]
}
2. 使用dll
"serve": "webpack serve --env development"jsnew webpack.DllReferencePlugin({
context: path.join(__dirname, "./dll"),
manifest: path.join(__dirname, "./dll/react.manifest.json")
}),
new AddAssetHtmlPlugin({
publicPath: '/',
filepath: path.join(__dirname, './dll/dll_react.js')
})
注意: 在升级到webpack4之后,React和Vue脚手架都移除了DLL库
犹大说,webpack4已经提供了足够性能,不需要DLL了
Terser 目前主流的压缩js代码的工具 支持ES6+uglify-js来压缩、混淆JavaScript代码,但是目前已经不再维护,它不支持ES6+语法Q:为什么需要压缩混淆代码?
A: 1)代码保护 2)缩小体积,性能提升
Terser和其他工具一样,可以在命令行使用
terser [input files] [options]
terser js/file1.js -o foo.min.js -c -m
Terser主要有两个配置选项 Compress 和Mangle
Compress option:
arrows class或者object中的函数,转换成箭头函数;arguments 将函数中使用arguments[index]转成对应的形参名称;dead_code 移除不可达的代码(tree shaking);Mangle option
toplevel 默认值是false,顶层作用域中的变量名称,进行丑化(转换);keep_classnames 默认值是false,是否保持依赖的类名称;keep_fnames 默认值是false,是否保持原来的函数名称;production模式下,默认就是使用TerserPlugin来处理我们的代码的webpack5内置了 terser-webpack-plugin配置 terser
jsoptimization: {
minimize: true,
// 只有打开minimize, minimizer配置才有意义
minimizer: [
new TerserPlugin({
parallel: true, // 使用多进程并发运行以提高构建速度
extractComments: false, // 是否将注释移除
terserOptions: {
compress: {
arguments: true,
dead_code: true
},
mangle: true, // 非顶层变量 被混淆
// 在 mangle: true 的前提下 配置toplevel: true 顶层变量被混淆
toplevel: true,
keep_classnames: true,
keep_fnames: true
}
})
]
}
示例代码
js/**
* 测试 terserOptions.mangle: true
* 测试 terserOptions.toplevel
*/
function myAlert (arg) {
alert(arg);
}
myAlert('abc')
/**
* 测试 terserOptions.compress.arguments: true
* 将 console.log(arguments[0] + arguments[1]); 转换成 console.log(num1 + num2);
*/
function mySum(num1, num2) {
console.log(arguments[0] + arguments[1]);
}
mySum(2, 3);
/**
* 测试 terserOptions.compress.dead_code
* 移除不可达代码
* 实测,不管配置不配置都会将不可代码移除
*
*/
if(false) {
console.log('不可达代码,将被移除');
}
/**
* 测试 terserOptions.keep_classnames: true,
*/
class Persion {
}
new Persion();
CSS压缩通常是去除无用的空格等,因为很难去修改选择器、属性的名称、值等;css-minimizer-webpack-plugincssnano工具来优化、压缩CSS(也可以单独使用)css压缩通常与css独立出来mini-css-extract-plugin一样都是在生产环境配置,一起使用使用示例
npm i css-minimizer-webpack-plugin mini-css-extract-plugin -Djsnew MiniCssExtractPlugin({
filename: "css/[name].[hash:8].css",
}),
new CSSMinimizerPlugin({
parallel: true,
})
之前的章节讲述了使用HtmlWebpackPlugin插件来生成HTML的模版,事实上它还有一些其他的配置:
inject: 设置打包资源插入的位置
cache: 设置为true,只有当文件改变是才会生成新的文件(默认值是true)minify: 默认会使用一个插件 html-minifier-terserjavascriptnew HtmlWebpackPlugin({
template: "./index.html",
title: 'webpack 代码压缩',
// inject: "body"
cache: true, // 当文件没有发生任何改变时, 直接使用之前的缓存
minify: isProduction ? {
removeComments: true, // 是否要移除html中注释
// 是否移除多余的属性
// <input type="text"> type 默认值就是 text --> <input>
removeRedundantAttributes: true,
// 是否移除一些空属性
removeEmptyAttributes: true,
// 移除空格
collapseWhitespace: true,
// 移除style标签中的多余属性
// <link rel="stylesheet" type="text/css" href="" /> -> <link rel="stylesheet" href="" />
removeStyleLinkTypeAttributes: true,
// 压缩css
minifyCSS: true,
// 压缩js
// minifyJS: true,
minifyJS: {
mangle: { // 这里的配置同 terser-webpack-plugin
toplevel: false
}
},
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
}: false
}),
http压缩是一种在服务端和客户端之间的,以改进传输速度和带宽利用率的方式;http压缩的流程
HTTP数据在向服务器发送前就已经被压缩了;(可以在webpack中完成)

deflate 基于deflate算法的压缩,使用zlib数据格式封装gzip GNU zip格式, 是目前使用比较广泛的压缩算法;br 一种新的开源压缩算法,专为HTTP内容的编码而设计;Tree Shaking 摇树 把树上的枯树叶摇下来, 在计算机中表示消除死代码(dead_code)
LISP,用于消除未调用的代码(纯函数无副作用)LISP这种语言虽然消亡了,但是treeSharking却被其他语言所继承JavaScript进行Tree Shaking是源自打包工具rollup
ES Module的静态语法分析webpack2正式内置支持了ESM模块,和检测未使用模块的能力;webpack4正式扩展了这个能力,并且通过package.json的 sideEffects属性作为标记,告知webpack在哪些文件有副作用编译时不能进行Tree Shaking优化webpack5中,也提供了对部分CommonJS的tree shaking的支持;哪里文件可以安全的删除掉;webpack实现Tree Shaking采用了两种不同的方案
usedExports:通过标记某些函数是否被使用,之后通过Terser 来进行优化的;sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用;示例演示
jsimport {add} from './usedExports';
console.log(add(2,3));
jsexport function add(a, b) {
return a+b;
}
export function muti(a, b) {
return a * b;
}
jsoptimization: {
// usedExports: 目的是标注出来哪些函数是没有被使用 unused
usedExports: true, // production
minimize: false,
},

事实上,optimization.minimize 默认不会移除未使用的代码,它会生成标注哪些导出没有被使用
再通过 terser-webpack-plugin 移除未使用的代码
修改配置如下
jsoptimization: {
// usedExports: 目的是标注出来哪些函数是没有被使用 unused
usedExports: true, // production
minimize: true, // 打开
minimizer: [ // 新增
new TerserPlugin({
parallel: true, // 使用多进程并发运行以提高构建速度
extractComments: false, // 是否将注释
terserOptions: {
compress: {
dead_code: true
},
mangle: false,
}
})
]
},

再次查看生成的文件没有了muti方法
假如在配置usedExports: false
muti又回来了
前面的usedExports方案具有局限性,我们来看个例子
src/index.js 新增 import './useExports_format';useExports_format.jsjsimport './useExports_format';
import {add} from './usedExports';
console.log(add(2,3));

我们来分析下:
import 'useExports_format.js' 但是并没用执行useExports_format.js 中的任何代码useExports_format.js 的内容,但有其文件地址的引用我们能不能去掉无效的文件引用呢? 这是就用到了 sideEffects 它在package.json中配置(默认值是true)
"sideEffects": false"再次构建后,找不到了useExports_format.js的引用
这时候假如往useExports_format.js增加以下两行代码
console.log('1233');console.log(window.a++);之前可以被移除,但现在useExports_format.js具有副作用,如果移除会产生隐患,那么我们怎样告知webpack哪些文件有副作用呢?
我们再来看个示例
jsexport function abc() {
return "abc";
}
window.abc = 'abc'
json{
"sideEffects": [
"./src/sideEffect_abc.js"
]
}

这时候 abc方法被移除了,有副作用的代码留了下来,
注意:通过import './style.css'引入的代码也会被识别为js语句,进而被sideEffects进行treeSharking处理, 所以pkg.sideEffects数组通常有 "**.css"
总结: 开发过程中推荐写纯模块无副作用的代码,所以配置 pkg.sideEffects 为false, css的treeSharking通过另一种方式处理(在loader中配置sideEffects)
一般pkg.sideEffects不用考虑css, 通过webpack.config.js 中的 rules 配置sideEffects: true,
告知webpack不对css进行tree-sharking
jsmodule: {
rules: [
{
test: /\.css$/i,
use: [
CssExtractPlugin.loader,
'css-loader',
],
sideEffects: true,
}
]
}
如果确实想要对CSS进行tree-sharking可以采用另一种方案
PurgeCSS 可以用来检测移除未使用css代码
补充说明: 还有一个工具
PurifyCss也可以用作css的tree-sharking但这个工具已经很多年没有维护了
示例演示
src/index.jsjsimport './style.css';
const ele = document.createElement('h3');
ele.className = 'title';
ele.innerText = '测试css tree sharking';
document.body.appendChild(ele);
src/style.csscsshtml, body {
padding: 0;
margin: 0;
height: 100%;
width: 100%;
background-color: aqua;
}
.title {
font-size: 30px;
}
.unusedcss {
color: blue;
}
webpack配置 新增pluginjsnew PurgeCssPlugin({
paths: glob.sync(`${path.join(__dirname, "./src")}/**/*`, {nodir: true}),
safelist: function() {
return {
standard: ["html"]
}
}
})
.unusedcss 将被移除standard: ["html"], html将被移除比如utils.js用个add方法 只在main.js用到,那么可以把add方法挪到main.js里面, 这个叫作用于提升
作用于提升减少了代码调用链路,也减少了代码量,提升了性能
esmodule下有效webpack会计算,能不能进行作用域提升配置作用域提升很简单,只需要webpack.config.js的plugins新增
new webpack.optimize.ModuleConcatenationPlugin()该插件是webpack内置的
比如有如下代码
jsimport {add, muti} from './mymath';
console.log(add(2,3));
console.log(muti(2,3));
jsexport function add(a, b) {
return a+b;
}
export function muti(a, b) {
return a * b;
}

上图可以看出 mymath.js中的add muti方法被挪到和调用js所在的代码片段一起了。
能看到这里非常感谢,我们在讲一些干货,一些可以作为项目亮点的示例
一般项目中我是使用脚手架构建项目,但官方的脚手架已经非常完善了,比如
mode:'production'已经做了treeSharking、 代码压缩、Scope Hoisting 项目启动时也做了缓存策略进而提升启动速度.scss、.ts、.vue、.jsx、.tsx资源的加载那么在构建工具方面我们还能做哪些事情呢?
iOS 12、Andoird 7.0, browserslist如何配置呢?
uni-app项目中,发现打包后的代码含有很多CSS注释,这些注释没引入一次文件就会残留一次, 本人通过使用patch-package.js覆盖uni-app的cssnano配置,移除了这些CSS注释本文作者:郭敬文
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!