网上流传一句话 “webpack工程师 > 前端工程师” 。
Webpack作为这些年主流的前端构建工具(目前仍是),可以说它前端工程化的核心。
学好Webpack有助于塑造个人优势,塑造简历亮点。
本篇文章将深入讲解webpack绝大部分配置项,核心功能等。包括不限于以下内容
output
input
CSS
资源加载相关loader
browserlist
配置规则及哪些工具支持browserlist
plugin
devtool
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为什么快,具体怎样解决这些问题不属于本篇文章的范畴)Vite
介绍的很美好,但是Webpack
流行的这些年(2015-2022)沉淀了大量的企业项目,你说Vite
能取代它吗?另外友情提示一下Vite
的配置和Webpack
的配置很像,高手都是借力打力,不会刻意重复造轮子,有造轮子的零件就不会自己再造零件。Gulp
gulp
是一个基于流(pipeline
模式)的自动化构建工具Gulp | Webpack | |
---|---|---|
理念 | 定义一系列任务然后执行,基于流的自动化构建工具 | 模块化打包工具 |
底层原理 | pipeline 设计模式 | 基于tapable 的微内核架构 |
优缺点 | gulp 思想更加的简单、易用,更适合编写一些自动化的任务 | 对于大型项目还是使用Webpack gulp 默认也是不支持模块化的 |
npm i webpack webpack/cli
webpack
webpack核心 可类比babel的@babel/core
可以通过编程的方式运行webpackwebpack-cli
提供webpac命令行运行 可类比babel的 @babel/cli
webpack
默认支持JS
和JSON
模块,如果想加载文件需要添加对应的loader
JS
模块,默认支持 ESM
、CJS
、AMD
javascriptconst 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
不一样,主文件变更不影响依赖文件的hash
contenthash
与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
目标代码与源码之间的映射关系 即sourcemap
devServer
开发环境一些配置 如热更新,接口代理等optimization
性能优化相关 如treeSharking
代码分割 DII缓存等resolve
配置别名 省略文件后缀外部资源配置等mode
webpack4
新增的, 有三个值 none
production
development
javascriptmodule: {
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-sass
dart-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
在目标浏览器上不支持normalize
babel
、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 dead
5%
通过全局使用情况统计信息选择的浏览器版本。 可以使用>=
,<
和<=
这些符号。
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-mycompany
npm包中查询supports es6-module
:支持特定功能的浏览器。 es6-module
这是“我可以使用” 页面feat的URL上的参数。有关所有可用功能的列表,请参见 。caniuse-lite/data/features
browserslist 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-cli
css.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.css
css.content {
background-color: #12345678;
}
postcss.config.js
jsmodule.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.js
jsimport "./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.css
css.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-loader
asset/inline
导出一个资源的data URI
(base64) ,替代 url-loader
asset/source
导出资源的源代码, 替代raw-loader
asset
可以配置资源大于某个值,使用单独文件方式,反之使用base64方式,取代url-loader
+ limit配置
尽管file-loader
和url-loader
不用了,但是很多项目中还是存在这两个loader
,这里有必要再简单说一下
file-loader
用来处理静态资源,当我们只是包资源文件移动到打包目录就需要用file-loader
javascript{
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
内置的插件DefinePlugin
DefinePlugin
定义的全局变量除了在html
中使用,还可以在js
中使用,通常用来做按配置打包webpack.config.js
jsmodule.exports = {
entry: ["./src/index.js", "./src/define-plugin.js"],
plugins: [
new DefinePlugin({
ENV: JSON.stringify('development')
})
]
}
src/define-plugin.js
jsif(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-map
source-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-map
cheap
是廉价的意思 会生成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 --watch
babel
支持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
这时候可以不使用prettier
eslint-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.js
npx eslint --init
生成模版prettier
融入eslint
npm install -D eslint-config-prettier eslint-plugin-prettier
npm install -D --save-exact prettier
jsmodule.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-loader
json{
test: /\.[j|t]s$/,
exclude: /node_modules/,
use: [
"babel-loader",
"eslint-loader",
]
}
TypeError: Cannot read property 'getFormatter' of undefined
npm i -D eslint@7
前面的打包后的文件都是依靠 http-server
命令或者 VSCode
插件live-server
来运行,而且每次修改代码后都需要手动打包效率较低。本段落我们来自己搭建本地服务器
官方给出了三种可选的方式
webpack
配置项watch
监控文件发生改变会重新编译
--watch
watch: true
webpack-dev-server
内置了live-server
和socket
热更新
npx webpack-dev-server
或 npx webpack server
启动webpack-dev-middlewares
webpack
处理后的文件发送到server
express
启动一个服务,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.js
jsfunction 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@2
index.js
增加代码 import './js/loadVue';
js/loadVue.js
jsimport 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.vue
vue<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-compiler
js{
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.jsx
jsximport 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.scss
scss.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.publicPath
devServer.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.1
127.0.0.1
是一个回环地址,表示我们主机自己发出去的包直接被自己接受
127.0.0.1
时,在同一个网段下的主机 中,通过ip地址是不能访问的;0.0.0.0
监听ipV4
所有的地址,再根据端口找到不同的应用程序
host
配置域名,随便什么域名都可以访问port
open
不解释compress
是否为静态文件开启gzip compression
v4默认值true
historyApiFallback
它用于解决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
地址
false
true
时, 在上例中 等于把host
请求头从 'localhost:9081'
修改为'localhost:8080'
(host
请求头与跨域有关)当配置信息越来越多,所有配置信息都放在一个文件中,这个文件会越来越不容易维护
有些配置是仅开发时候的,有些配置是仅生产环境才使用的,基于此我们对配置最好进行划分,方面维护和管理。
pkg.script
处命令区分,走不同入口配置文件pkg.script
处传递参数区分环境把将该环境与公共配置聚合。基于前面一个章节我整理了下webpack配置
代码较多,但改动点并不多,我简述下改动点
webpack.config.js
导出一个函数,参数来自命令行,区分开发还是生产环境,赋值给 process.env.NODE_ENV
devServer
、ReactRefreshPlugin
、babel
配置增加插件react-refresh/babel
mode
为development
CleanWebpackPlugin
CopyWebpackPlugin
mode
为production
resolve
用于设置模块如何被解析,它可以帮助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``.tsx
resolve.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/shimming
js// 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.js
jsonentry: {
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*1024
maxSize
分割出来的包最大限制
minSize
优先于 maxSize
npm
包大于 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.ts
tsconst element = document.createElement('div');
element.innerHTML = 'Hello Element';
export default element;
pageB.ts
tsconst 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
按数字的顺序使用id
named
development
模式下的默认值,一个可读的名称id
deterministic
确定性的,在不同的编译中确定的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.js
jsconst 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.js
jsconst 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-plugin
cssnano
工具来优化、压缩CSS
(也可以单独使用)css
压缩通常与css
独立出来mini-css-extract-plugin
一样都是在生产环境配置,一起使用使用示例
npm i css-minimizer-webpack-plugin mini-css-extract-plugin -D
jsnew MiniCssExtractPlugin({
filename: "css/[name].[hash:8].css",
}),
new CSSMinimizerPlugin({
parallel: true,
})
之前的章节讲述了使用HtmlWebpackPlugin
插件来生成HTML
的模版,事实上它还有一些其他的配置:
inject
: 设置打包资源插入的位置
cache
: 设置为true
,只有当文件改变是才会生成新的文件(默认值是true
)minify
: 默认会使用一个插件 html-minifier-terser
javascriptnew 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.js
jsimport './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.js
jsimport './style.css';
const ele = document.createElement('h3');
ele.className = 'title';
ele.innerText = '测试css tree sharking';
document.body.appendChild(ele);
src/style.css
csshtml, body {
padding: 0;
margin: 0;
height: 100%;
width: 100%;
background-color: aqua;
}
.title {
font-size: 30px;
}
.unusedcss {
color: blue;
}
webpack
配置 新增plugin
jsnew 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 许可协议。转载请注明出处!