基础
配置文件
初次打包体验
// 初始化项目
npm init -y
// 安装 webpack 依赖
npm i webpack webpack-cli -D
// cli 打包
npx webpack
// cli 参数
npx webpack --entry ./src/main.js --output-path ./build
代码被视为 JavaScript 模块
- 不支持
CommonJS - 兼容性不好,还是需要通过
webapck构建。
<script src="..." type="module"></script>
npx 说明
- 主要有以下特点:
- 临时安装可执行依赖包,不用全局安装,不用担心长期的污染。
- 可以执行依赖包中的命令,安装完成自动运行。
- 自动加载 node_modules 中依赖包,不用指定 $PATH。
- 可以指定 node 版本、命令的版本,解决了不同项目使用不同
- npx 执行流程如下:
- 到 node_modules/.bin 路径检查对应的命令是否存在,找到之后执行;
- 没有找到,就去环境变量 $PATH 里面,检查对应命令是否存在,找到之后执行;
- 还是没有找到,自动下载一个临时的依赖包最新版本在一个临时目录,然后再运行命令,运行完之后删除,不污染全局环境。
配置文件 webpack.config.js
- 根目录下创建一个webpack.config.js文件,来作为 webpack 的配置文件
const path = require("path");
module.exports = {
entry: "./src/main.js",
output: {
filename: "js/index.js",
// 必须是绝对路径
path: path.resolve(__dirname, "./build"),
},
};
- 指定 webpack 配置文件
npx webpack --config wx.config.js
loader 基本使用
- 配置方式
- 内联方式
loader执行顺序从右往左(从下往上)
打包 css
npm i style-loader css-loader -D
module: {
rules: [
{
// 正则表达式
test: /\.css$/i, // 匹配资源
// loader: "css-loader",
// use: [
// { loader: "css-loader" }
// ],
use: ["style-loader", "css-loader"],
},
],
},
- style-loader
- 插入style标签(处理内联样式)
- css-loader
- 解析css文件
解析css文件中的@import和url语句,处理css-modules,并将结果作为一个js模块返回
- 解析css文件
原理??
打包 less
npm i less less-loader -D
{
test: /\.less$/i,
use: ["style-loader", "css-loader", "less-loader"],
},
- 可以通过 cli 使用
npx less ./src/css/title.less > title.css
游览器兼容性
查询浏览器市场占有率 caniuse
browserslist
- 在不同的前端工具之间,共享目标浏览器和Node.js版本的配置 postcss-prest-env、babel、autoprefixer
- 通常其他包会附带安装,所以不需要安装
npx i browserslist -D - 命令行使用
npx browserslist ">1%, last 2 version, not dead" - 配置
-
在 package.json 文件中配置 "browserslist": [ "> 1%", "last 2 versions", "not dead" ]
-
单独的一个配置文件 .browserslistrc defaults // Browserslist的默认浏览器 // > 0.5%, last 2 versions, Firefox ESR, not dead
> 1% // 全球市场占有率 >1% last 2 versions // 每个浏览器的最后2个版本 not dead // dead: 24个月内没有官方支持或更新的浏览器
-
PostCSS
什么是PostCSS呢?
- PostCSS是一个通过JavaScript来转换样式的工具;
- 这个工具可以帮助我们进行一些CSS的转换和适配,比如自动添加浏览器前缀、css样式的重置;
- 但是实现这些工具,我们需要借助于PostCSS对应的插件;
命令行使用
npm i postcss postcss-cli autoprefixer -D
- postcss
- postcss-cli
- 终端使用命令行(实际项目可以不用安装)
- autoprefixer
- 添加css前缀插件
- postcss-preset-env 已经集成,所以使用的会比较少
npx postcss --use autoprefixer -o result.css ./src/css/style.css
webpack 使用
npm i postcss postcss-loader -D- 修改加载 css 的 loader
module: {
rules: [
{
test: /\.css$/i,
use: [
"style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: ["autoprefixer"],
},
},
},
],
},
],
},
单独的 postcss 配置文件
-
在根目录下创建
postcss.config.js -
npm i postcss-preset-env -D- 它可以帮助我们将一些现代的CSS特性,转成大多数浏览器认识的CSS,并且会根据目标浏览器或者运行时环境添加所需的polyfill
- 也包括会自动帮助我们添加autoprefixer(所以相当于已经内置了autoprefixer)
-
webpack.config.js
- css-loader 中的 importLoaders 属性
- 允许为
@import样式规则设置在 CSS loader 之前应用的 loader 的数量,例如:@import ./test.css'等
module: { rules: [ { test: /\.css$/i, use: [ "style-loader", { loader: "css-loader", options: { importLoaders: 1, }, }, "postcss-loader", ], }, { test: /\.less$/i, use: [ "style-loader", { loader: "css-loader", options: { importLoaders: 2, }, }, "less-loader", "postcss-loader", ], }, ], }, -
postcss.config.js
module.exports = { plugins: [ // require("postcss-preset-env"), "postcss-preset-env" ], };
打包图片资源
- 引入图片的方式
// import import tempImg from "../img/1.png"; // require() // file-loader 6.x版本需要添加 .default imgEl.src = require("../img/1.png").default; // css background-image: url('../img/nhlt.jpg');
file-loader
- 处理
import/require()方式引入的一个文件资源 npm i file-loader -D- 文件的名称规则 placeholders
{ test: /\.(png|jpe?g|gif|svg)$/, use: [ { loader: "file-loader", options: { // outputPath: 'img', name: "img/[name].[hash:8].[ext]", }, }, ], }
url-loader
- 与 file-loader 相似,但是可以将较小的文件,转成base64的URI
- 小的图片转换
base64之后可以和页面一起被请求,减少不必要的请求过程 - 大的图片也进行转换,反而会影响页面的请求速度
- 小的图片转换
npm i url-loader -D{ test: /.(png|jpe?g|gif|svg)$/, use: [ { loader: "url-loader", options: { name: "img/[name].[hash:8].[ext]", limit: 100 * 1024, }, }, ], }
资源模块 asset
资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:
asset/resource发送一个单独的文件并导出 URL。之前通过使用file-loader实现。asset/inline导出一个资源的 data URI。之前通过使用url-loader实现。asset/source导出资源的源代码。之前通过使用raw-loader实现。asset在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用url-loader,并且配置资源体积限制实现。
可以通过在 webpack 配置中设置
output.assetModuleFilename来修改此模板字符串
{
test: /\.(png|jpe?g|gif|svg)$/,
type: "asset",
generator: {
filename: "img/[name].[hash:6][ext]"
},
parser: {
dataUrlCondition: {
maxSize: 100 * 1024
}
}
},
加载字体
{
test: /\.ttf|eot|woff2?$/i,
type: "asset/resource",
generator: {
filename: "font/[name].[hash:6][ext]"
}
}
认识 plugin
clean-webpack-plugin
清空打包目录的文件夹
-
npm i clean-webpack-plugin -Dconst { CleanWebpackPlugin } = require('clean-webpack-plugin');module.exports = { // ... plugins: [ new CleanWebpackPlugin(), ], }
html-webpack-plugin
创建 html 文件
-
npm i html-webpack-plugin -D -
默认情况下是根据在html-webpack-plugin的源码中,有一个default_index.ejs模块生成的 const HtmlWebpackPlugin = require('html-webpack-plugin');
new HtmlWebpackPlugin({ title: 'demo', template: './public/index.html', })
DefinePlugin
允许在 编译时 将你代码中的变量替换为其他值或表达式
- webpack 内置插件
const webpack = require("webpack"); new webpack.DefinePlugin({ BASE_URL: JSON.stringify('./'), })
copy-webpack-plugin
将单个文件或整个目录复制到生成目录
npm i copy-webpack-plugin -D
const CopyPlugin = require("copy-webpack-plugin");
new CopyPlugin({
patterns: [
{
from: 'public',
globOptions: {
ignore: [ // 忽略项
"**/index.html",
"**/abc.txt",
],
},
}
],
})
进阶
source-map
什么是source-map
- source-map是从已转换的代码,映射到原始的源文件
- 使浏览器可以重构原始源并在调试器中显示重建的原始源
如何使用
- 根据源文件,生成source-map文件,webpack在打包时,可以通过配置生成source-map
- 在转换后的代码,最后添加一个注释,它指向sourcemap
//# sourceMappingURL=common.bundle.js.map
source-map值含义
下面几个值不会生成source-map
- false
- none(不写)
- production 模式下的默认值
- eval
- 会在eval执行的代码中,添加
//# sourceURL= - 它会被浏览器在执行时解析,并且在调试面板中生成对应的一些文件目录,方便我们调试代码
- 会在eval执行的代码中,添加
生成source-map值
- source-map
- 生成一个独立的source-map文件,并且在bundle文件中有一个注释
//# sourceMappingURL=bundle.js.map,指向source-map文件 - 开发工具会根据这个注释找到source-map文件,并且解析
- 生成一个独立的source-map文件,并且在bundle文件中有一个注释
- eval-source-map
- source-map是DataUrl以添加到eval函数的后面
- inline-source-map
- source-map是以DataUrl添加到bundle文件的后面
- hidden-source-map
- 不会对source-map文件进行引用
- 相当于删除了打包文件中对sourcemap的引用注释
- 如果我们手动添加进来,那么sourcemap就会生效了
- nosources-source-map
- 只有错误信息的提示,不会生成源代码文件
- cheap-source-map
- 更加高效一些(cheap低开销),因为它没有生成列映射(Column Mapping)
- cheap-module-source-map
- 类似于cheap-source-map,但是对源自loader的sourcemap处理会更好
- 如果loader对我们的源码进行了特殊的处理,比如babel
cheap-source-map和cheap-module-source-map的区别
-
cheap-module-source-map
-
babel转换后与源代码一致
-
cheap-source-map
-
babel转换后与源代码不一致
组合规则
[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
最佳的实践
- 开发、测试阶段
- cheap-module-source-map
- 发布阶段
- false
- (none)
Babel 深入解析
参考链接
Babel到底是什么
- 微内核架构 @babel/core
- Babel是一个工具链,主要用于旧浏览器或者缓解中将ECMAScript 2015+代码转换为向后兼容版本的 JavaScript
- 包括:语法转换、源代码转换、Polyfill实现目标缓解缺少的功能等
命令行使用
npm i @babel/core @babel/cli @babel/plugin-transform-arrow-functions @babel/plugin-transform-block-scoping -D
- @babel/core:babel的核心代码,必须安装
- @babel/cli:在命令行使用babel
- @babel/plugin-transform-arrow-functions:转换箭头函数
- @babel/plugin-transform-block-scoping:转换const为var
npx babel ./src --out-dir dist --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions
- --out-dir
- --plugins
Babel的预设preset
npm install @babel/preset-env -Dnpx babel src --out-dir dist --presets=@babel/preset-env
底层原理
- Babel编译器的作用就是将我们的源代码,转换成浏览器可以直接识别的另外一段源代码
Babel也拥有编译器的工作流程:
- 解析阶段(Parsing)
- 转换阶段(Transformation)
- 生成阶段(Code Generation)
Babel使用
npm i @babel/core babel-loader -D- 使用插件,安装依赖
@babel/plugin-transform-arrow-functions@babel/plugin-transform-block-scoping{ test: /\.m?js$/, use: { loader: "babel-loader", options: { plugins: [ '@babel/plugin-transform-arrow-functions', '@babel/plugin-transform-block-scoping', ] } } } - 使用 presets,安装依赖
@babel/preset-env - 根据 browserslist 工具 /
tagert 属性使用插件 - tagert 属性权重高,不建议使用
use: { loader: "babel-loader", options: { presets: [ ["@babel/preset-env", { targets: "last 2 version", }] ], }, },
Babel的配置文件
- 文件后缀可选
.json .js .cjs .mjs .babelrc.json早期使用,现在不推荐- babel.config.json (babel7)可以直接作用于Monorepos项目的子包,更加推荐
- Monorepos 多包管理(babel本身、element-plus、umi等)
module.exports = { presets: [ "@babel/preset-env" ], };
认识polyfill
填充物(垫片),一个补丁,可以帮助我们更好的使用JavaScript
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: "babel-loader",
},
@babel/polyfill 已经不推荐使用
单独引入 core-js 和 regenerator-runtime
npm i core-js regenerator-runtime -S- 配置 babel.config.js
module.exports = { presets: [ [ "@babel/preset-env", { // false // entry useBuiltIns: "usage", corejs: 3, }, ], ], }; - useBuiltIns
- false
- 打包后的文件不使用polyfill来进行适配
- 不需要设置corejs属性的
- usage
- 自动检测所需要的polyfill
- 打包的包相对会小一些
- 设置corejs属性来确定使用的corejs的版本
- entry
- 需要在入口文件中添加
import 'core-js/stable';import 'regenerator-runtime/runtime'; - 会根据 browserslist 目标导入所有的polyfill,但是对应的包也会变大
- 需要在入口文件中添加
- false
- corejs
认识Plugin-transform-runtime
- 可以避免 polyfill 全局污染
- 编写一个工具库,通过polyfill添加的特性,可能会污染它们的代码
- 当编写工具时,babel更推荐我们使用一个插件:
@babel/plugin-transform-runtime来完成 polyfill 的功能;
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime-corejs3
module.exports = {
presets: [
[
"@babel/preset-env",
// 配置了 plugin-transform-runtime 插件,这个就要注释(二选一)
// {
// useBuiltIns: "usage",
// corejs: 3,
// },
],
],
plugins: [
[
"@babel/plugin-transform-runtime",
{
corejs: 3,
},
],
],
};
React的jsx支持
npm install @babel/preset-react -D
module.exports = {
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "usage",
corejs: 3,
},
],
"@babel/preset-react",
],
};
加载 Vue
npm i vue -Snpm i vue-loader vue-template-compiler -D
const VueLoaderPlugin = require("vue-loader/lib/plugin");
module: {
rules: [
// ...
{
test: /\.vue$/,
use: "vue-loader",
},
],
},
plugins: [
// ...
new VueLoaderPlugin(),
],
TypeScript的编译
使用 ts-loader
缺点:不能添加对应的 polyfill
npm install ts-loader -D- 安装
ts-loader会一起安装typescript依赖
- 安装
- 命令行打包
npx tsc - webpack 打包
npx tsc --init初始化 ts 配置信息tsconfig.json文件
{ test: /.ts$/, exclude: /node_modules/, use: 'ts-loader', }
使用 babel-loader
优点:可以将ts转换成js,并且可以实现polyfill的功能
缺点:在编译的过程中,不会对类型错误进行检测
npm install @babel/preset-typescript -D- module.rules 修改为
babel-loader{ test: /.ts$/, exclude: /node_modules/, use: 'babel-loader', } - babel.config.js 添加
@babel/preset-typescript预设 module.exports = { presets: [ [ "@babel/preset-env", { useBuiltIns: "usage", corejs: 3, }, ], "@babel/preset-react", + "@babel/preset-typescript", ], };
babel vs tsc 最佳实践
使用Babel来完成代码的转换,使用tsc来进行类型的检查
- package.json 修改运行命令
"scripts": {
"build": "npm run type-check & webpack",
"type-check": "tsc --noEmit",
"type-check-watch": "tsc --noEmit --watch"
},
ESlint
npm i eslint -Dnpx eslint --init创建配置文件 .eslintrc.jsnpx eslint ./src/main.js执行检测命令
ESLint的配置文件解析
- env:运行的环境,比如是浏览器,并且我们会使用es2021(对应的ecmaVersion是12)的语法;
- extends:可以扩展当前的配置,让其继承自其他的配置信息,可以跟字符串或者数组(多个);
- parserOptions:这里可以指定ESMAScript的版本、sourceType的类型
- pparser:默认情况下是espree(也是一个JS Parser,用于ESLint),但是因为我们需要编译TypeScript,所 以需要指定对应的解释器;
- plugins:指定我们用到的插件;
- rules:自定义的一些规则;
eslint-loader使用
{
test: /\.jsx?$/, // /.ts$/
exclude: /node_modules/,
use: [
"babel-loader",
"eslint-loader"
],
},
vscode 可以安装Eslint Prettier插件
- settings.json配置
"eslint.format.enable": true,
"eslint.alwaysShowStatus": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
HMR 模块热替换
Webpack 可以监听文件变化,当它们修改后会重新编译
自动编译代码
watch
-
开启 watch
- webapck.config.js
module.exports = { //... watch: true, };- package.json
"scripts": { "dev": "webpack --watch", }, -
缺点:
- 对所有的源代码都重新编译
- 编译成功后,都会生成新的文件
webpack-dev-server
- 提供了实时重新加载功能
- 如果项目配置了
browserslist选项,可能导致页面不能自动刷新,需要配置target: "web"
- 如果项目配置了
- webpack-dev-server 在编译之后不会写入到任何输出文件。而是将文件保留在内存中
- webpack-dev-server使用了一个库叫 memfs
- 会刷新整个页面
npm i webpack-dev-server -D
- package.json 修改启动命令
"serve": "webpack serve"
开启HMR
优点:
- 在应用程序运行过程中,替换、添加、删除模块,而无需重新刷新整个页面
- 保留某些应用程序的状态不丢失
- 节省开发的时间,立即在浏览器更新
开启HMR
- 修改webpack的配置 module.exports = { //... devServer: { hot: true, }, };
- 在入口文件,指定哪些模块发生更新时进行HMR if (module.hot) { module.hot.accept("./math.js", () => { // 更新后的回调函数 console.log("math模块发生了更新~"); }); }
Vue 开启 HMR
- Vue Loader 支持 vue 组件的 HMR,提供开箱即用体验
React 开启 HMR
-
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh -
修改 webpack.config.js const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module: { rules: [ { test: /\.jsx?$/i, exclude: /node_modules/, use: "babel-loader", }, ], }, plugins: [ new ReactRefreshWebpackPlugin() ], -
修改 babel.config.js
module.exports = { presets: [ "@babel/preset-env", "@babel/preset-react" ], plugins: [ "react-refresh/babel" ], };
HMR 原理全解析
深入配置
output.publicPath
- path 输出目录对应一个绝对路径
- publicPath
- 默认值是一个空字符串,http协议后面会自动添加
/ - 路径拼接规则 http://domain:80 +
publicPath+ 'js/index.js' - 开发阶段
http://,可以设置为/ - 生成阶段(本地直接打开,不开启服务)
file:///,可以设置为相对路径./
- 默认值是一个空字符串,http协议后面会自动添加
devServer
publicPath
- 指定本地服务所在的文件夹,默认值是
/ - 如果我们将其设置为了
/abc,那么我们需要通过http://localhost:8080/abc才能访问到对应的打包后的资源 - 必须将
output.publicPath也设置为/abc- 建议
devServer.publicPath与output.publicPath相同
- 建议
module.exports = {
//...
output: {
publicPath: '/abc',
},
devServer: {
publicPath: '/abc'
},
};
contentBase
告诉服务器从哪里提供静态文件
最好不要修改!!!
<script src="./static/test.js"></script>- 设置
contentBase: path.resolve(__dirname, './static')可以省略static <script src="./test.js"></script>
watchContentBase
- 默认启用,静态资源文件更改将触发整个页面重新加载
hotOnly
启用热模块替换功能,在构建失败时不刷新页面
- 默认情况下当代码编译失败修复后,我们会重新刷新整个页面;
- 旧版本设置
hotOnly: true,新版本设置hot: 'only';
host
设置主机地址
- 默认值是 localhost;
- 如果希望其他地方也可以访问,可以设置为 0.0.0.0
- localhost 和 0.0.0.0 的区别:
- localhost:本质上是一个域名,通常情况下会被解析成127.0.0.1;
- 127.0.0.1:回环地址(Loop Back Address),表达的意思其实是我们主机自己发出去的包,直接被自己接收;
- 正常的数据库包经常 应用层 - 传输层 - 网络层 - 数据链路层 - 物理层
- 而回环地址,是在网络层直接就被获取到了,是不会经常数据链路层和物理层的
- 比如我们监听 127.0.0.1时,在同一个网段下的主机中,通过ip地址是不能访问的
- 0.0.0.0:监听IPV4上所有的地址,再根据端口找到不同的应用程序
- 比如我们监听 0.0.0.0时,在同一个网段下的主机中,通过ip地址是可以访问的;
port
指定监听请求的端口号
open
在服务器已经启动后打开浏览器。设置其为
true以打开你的默认浏览器
compress
启用 gzip 压缩
- 响应头会携带
Content-Encoding: gzip
proxy
- 对
/api/users的请求会将请求代理到http://localhost:8888/api/users
proxy: {
// "/api" 替换为 "http://localhost:8888"
"/api": {
target: "http://localhost:8888",
// 不希望传递/api,则需要重写路径
pathRewrite: {
// ^/api 是个正则表达式
// 将 /api 替换为 ""
// 最终代理到 http://localhost:8888/users
"^/api": ""
},
// 默认情况下,将不接受在 HTTPS 上运行且证书无效的后端服务器
// 设置 false 关闭校验
secure: false,
// 默认情况下,代理时会保留主机头的来源,可以将 changeOrigin 设置为 true 以覆盖此行为
// Remote Address: 127.0.0.1:8080 => 127.0.0.1:8888
changeOrigin: true,
}
},
historyApiFallback
- 解决SPA页面在路由跳转之后,进行页面刷新 时,返回404的错误
- vue-router history 模式
http://localhost:8080/abc - 设置为
true,那么在刷新时,返回404错误时,会自动返回 index.html 的内容
Resolve
配置模块如何解析
webpack能解析三种文件路径
- 绝对路径
- 不需要再做进一步解析
- 相对路径
- 在这种情况下,使用 import 或 require 的资源文件所处的目录,被认为是上下文目录
- 在 import/require 中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径
- 模块路径
- 在
resolve.modules中指定的所有目录检索模块 - 可以通过设置
resolve.alias的方式来替换初识模块路径
- 在
确实文件还是文件夹
- 如果是一个文件:
- 如果文件具有扩展名,则直接打包文件;
- 否则,将使用
resolve.extensions选项作为文件扩展名解析;
- 如果是一个文件夹:
- 会在文件夹中根据
resolve.mainFiles配置选项中指定的文件顺序查找 - 再根据
resolve.extensions来解析扩展名
- 会在文件夹中根据
modules
告诉 webpack 解析模块时应该搜索的目录
- 默认值
['node_modules']
alias
创建
import或require的别名
const path = require('path');
module.exports = {
//...
resolve: {
alias: {
Utilities: path.resolve(__dirname, 'src/utilities/'),
Templates: path.resolve(__dirname, 'src/templates/'),
},
},
};
- 引入时可以改为
import 'Utilities/test.js'
extensions
解析到文件时自动添加扩展名
['.js', '.json', '.wasm']- 如果有多个文件有相同的名字,但后缀名不同,webpack 会解析列在数组首位的后缀的文件 并跳过其余的后缀
mainFiles
解析目录时要使用的文件名
- 默认值
['index']
优化
如何区分开发环境
环境变量
使用相同的一个入口配置文件,通过设置参数来区分它们
- 新建 config/webpack.common.js 文件测试
const path = require("path"); module.exports = function(env) { console.log('Goal: ', env.goal); // 'local' console.log('Production: ', env.production); // true return { // 不配置 context 按照 node.js 进程的当前目录 entry: "./src/index.js", output: { path: path.resolve(__dirname, "../dist"), }, }; }; npx webpack --config ./config/webpack.common.js --env production goal=local--env允许你传入任意数量的环境变量;例如,--env production或--env goal=local
Tips: 与
mode配置选项的值无关,只是为 env(参数)追加一个变量
{
WEBPACK_BUNDLE: true,
WEBPACK_BUILD: true,
production: true,
goal: 'local'
}
配置分离
以下文件都是放在 config 文件夹中
新建 webpack.common.js 文件
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
// webpack 提供的合并文件插件
const { merge } = require("webpack-merge");
// 引入 dev prod 环境配置
const prodConfig = require("./webpack.prod");
const devConfig = require("./webpack.dev");
// 公共的 webpack 配置项
const commonConfig = {
// 没有使用 context
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "../dist"),
},
module: {
rules: [
//...
],
},
plugins: [
new HtmlWebpackPlugin({
template: "./index.html",
}),
new VueLoaderPlugin(),
],
};
module.exports = function(env) {
const isProduction = env.production;
// 因为插件的生命周期问题,babel 获取不到 DefinePlugin 的值,所以需要手动修改
// env 属性的值,如果赋值不是字符串,会使用 String() 转为字符串,会导致 undefined 转换为字符串
process.env.NODE_ENV = isProduction ? "production" : "development";
// 区分环境合并代码
const config = isProduction ? prodConfig : devConfig;
const mergeConfig = merge(commonConfig, config);
return mergeConfig;
};
新建 webpack.prod.js 文件
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// 生产环境
module.exports = {
mode: "production",
plugins: [
new CleanWebpackPlugin({}),
]
}
新建 webpack.dev.js 文件
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
// 开发环境
module.exports = {
mode: "development",
devServer: {
hot: true,
hotOnly: true,
compress: true,
},
plugins: [
new ReactRefreshWebpackPlugin(),
]
}
修改 babel.config.js
const presets = [
["@babel/preset-env"],
["@babel/preset-react"],
];
const plugins = [];
const isProduction = process.env.NODE_ENV === "production";
// React HMR -> 模块的热替换 必然是在开发时才有效果
if (!isProduction) {
plugins.push(["react-refresh/babel"]);
}
module.exports = {
presets,
plugins
}
入口文件解析
基础目录,绝对路径,用于从配置中解析入口点(entry point)和 加载器(loader)
- 默认使用 Node.js 进程的当前工作目录(与
package.json平级),但是推荐在配置中传入一个值
const path = require("path");
module.exports = function(env) {
return {
// 不配置 context 按照 node.js 进程的当前目录
// entry: "./src/index.js",
context: path.resolve(__dirname, "./"),
entry: "../src/index.js",
output: {
path: path.resolve(__dirname, "../dist"),
},
};
};
代码分离
此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。
代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,优化代码加载性能。
常用的代码分离方法有三种:
- 入口起点:使用
entry配置手动地分离代码。 - 防止重复:使用
Entry dependencies或者SplitChunksPlugin去重和分离 chunk。 - 动态导入:通过模块的内联函数调用来分离代码。
多入口起点
const path = require('path');
module.exports = {
entry: {
index: './src/index.js',
another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
- 在 index.html 中,会引入多入口的所有文件
Q: vue-cli 中的多页面是如何实现的?
Entry dependencies 入口依赖
- 不推荐使用
- index.js 和 another.js 都依赖两个库:
lodash dayjs- 打包后的两个bunlde都有会有一份lodash和dayjs
- 使用
shared在多个 chunk 之间共享模块
entry: {
index: { import: "./src/index.js", dependOn: "shared" },
math: { import: "./src/math.js", dependOn: "shared" },
// String | Array<String>
// shared: "lodash",
shared: ["lodash", "axios"],
},
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "../dist"),
},
SplitChunksPlugin
- chunks
- async 默认值,异步导入
import() - initial 同步导入
- all 两者都处理
- async 默认值,异步导入
- minSize
- 拆分包的大小, 至少为minSize
- 如果一个包拆分出来达不到minSize,那么这个包就不会拆分
- maxSize
- 将大于maxSize的包,拆分为不小于minSize的包
- minChunks
- 至少被引入的次数,默认是1
- 如果我们写一个2,但是引入了一次,那么不会被单独拆分
- name:设置拆包的名称
- 可以设置一个名称,也可以设置为false
- 设置为false后,需要在cacheGroups中设置名称
- cacheGroups
- 用于对拆分的包就行分组,比如一个lodash在拆分之后,并不会立即打包,而是会等到有没有其他符合规则的包一起来打包
- test:匹配符合规则的包;
name:拆分包的name属性,固定值;- filename:拆分包的名称,可以自己使用placeholder属性;
vue-element-admin 配置信息
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial' // only package third parties that are initially dependent
},
elementUI: {
name: 'chunk-elementUI', // split elementUI into a single package
priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
},
commons: {
name: 'chunk-commons',
test: resolve('src/components'), // can customize your rules
minChunks: 3, // minimum common number
priority: 5,
reuseExistingChunk: true
}
}
}
}
动态导入 import()
// 只要是异步导入的代码, webpack都会进行代码分离
import("./foo").then(res => {
console.log(res);
});
import("./foo_02").then(res => {
console.log(res);
});
动态导入的文件命名
-
修改 chunk 文件的名称
output: { filename: "[name].bundle.js", path: path.resolve(__dirname, "../dist"), chunkFilename: "[name].[hash:6].chunk.js" }, -
获取到的
[name]是和id的名称保持一致的 -
修改name的值,可以通过magic comments(魔法注释)的方式 // magic comments import(/* webpackChunkName: "foo" */"./foo").then(res => { console.log(res); });
import(/* webpackChunkName: "foo_02" */"./foo_02").then(res => { console.log(res); });
vue-router 把组件按组分块
const UserDetails = () =>
import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
const UserDashboard = () =>
import(/* webpackChunkName: "group-user" */ './UserDashboard.vue')
const UserProfileEdit = () =>
import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue')
- webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。
- 把 output 的
chunkFilename: "[name].[hash:6].chunk.js"可以添加 hash,这样即使同名的文件,也不会合并到一个模块中
chunkIds
告知 webpack 当选择模块 id 时需要使用哪种算法
- natural
- 按使用顺序的数字 id
- 不推荐使用
- named
- 对调试更友好的可读的 id
- development下的默认值
- deterministic
- 在不同的编译中不变的短数字 id。有益于长期缓存。
- 在生产模式中会默认开启。
module.exports = {
//...
optimization: {
chunkIds: 'named',
},
};
代码懒加载
- 新建一个导出文件 element.js
const element = document.createElement('div');
element.innerHTML = "Hello Element";
export default element;
- 按钮点击时,加载这个对象
const button = document.createElement("button");
button.innerHTML = "加载元素";
button.addEventListener("click", () => {
import(
/* webpackChunkName: 'element' */
"./element"
).then(({default: element}) => {
document.body.appendChild(element);
})
});
document.body.appendChild(button);
Prefetch和Preload
- 在声明 import 时,使用下面这些内置指令,来告知浏览器:
- prefetch(预获取):将来某些导航下可能需要的资源
- preload(预加载):当前导航下可能需要资源
- 与 prefetch 指令相比,preload 指令有许多不同之处:
- preload chunk 会在父 chunk 加载时,以并行方式(不会有新的文件请求)开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
- preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
- preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
// prefetch -> 魔法注释(magic comments)
/* webpackPrefetch: true */
/* webpackPreload: true */
import(
/* webpackChunkName: 'element' */
/* webpackPrefetch: true */
"./element"
).then(({ default: element }) => {
document.body.appendChild(element);
});
参考文档
Q:vue-router 中的路由如果不设置这两个注释,会立即下载吗?
runtimeChunk
- 配置runtime相关的代码是否抽取到一个单独的chunk中
- runtime相关的代码指的是在运行环境中,对模块进行解析、加载、模块信息相关的代码;
true或'multiple'会为每个入口添加一个只含有 runtime 的额外 chunk"single"会创建一个在所有生成 chunk 之间共享的运行时文件
CDN
- 购买CDN服务器
- 可以直接修改publicPath,在打包时添加上自己的CDN地址
publicPath: 'https://xxxx.com/cdn'
- 第三方库的CDN服务器
- 通常是生产环境才需要修改
- 第一步,通过webpack配置,来排除一些库的打包
externals: { // window._ lodash: "_", // window.dayjs dayjs: "dayjs" },- 第二步,在html模块中,加入CDN服务器地址:
<!-- ejs中的if判断 --> <% if (process.env.NODE_ENV === 'production') { %> <script src="https://unpkg.com/dayjs@1.8.21/dayjs.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script> <% } %>
Shimming 预置依赖
- 不推荐使用,可以查阅文档了解
MiniCssExtractPlugin
将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载
npm install --save-dev mini-css-extract-plugin
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: "css/[name].[contenthash:6].css"
})
],
module: {
rules: [
{
test: /.css$/i,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
};
Hash、ContentHash、ChunkHash
- 在我们给打包的文件进行命名的时候,会使用placeholder,placeholder中有几个属性比较相似:
- hash、chunkhash、contenthash
- hash本身是通过MD4的散列函数处理后,生成一个128位的hash值(32个十六进制);
- hash:
- 项目里面的内容变动后,会生成新的 hash
- 多入口文件,如果修改了一个入口的内容,另一个入口文件也会生成新的 hash
- chunkhash
- 不同的入口进行借来解析来生成hash值
- 多入口文件时,互不影响
- contenthash
- 表示生成的文件hash名称,只和内容有关系
- 自己生成的文件被改动后,才会生成新的 hash
- css(独立文件) 和 chunkFilename 都建议使用
contenthash
参考文档
Terser
压缩、丑化js,让bundle变得更小
- 在production模式下,默认就是使用TerserPlugin来处理我们的代码的
- development 不推荐使用
- 设置
parallel使用多进程并发运行以提高构建速度
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
},
};
关于 source maps 说明
只对 devtool 选项的 source-map,inline-source-map,hidden-source-map 和 nosources-source-map 有效。
eval会包裹 modules,通过eval("string"),而 minimizer 不会处理字符串。cheap不存在列信息,minimizer 只产生单行,只会留下一个映射。
CSS 压缩
npm install css-minimizer-webpack-plugin --save-dev- 这将仅在生产环境开启 CSS 优化。
- 如果还想在开发环境下启用 CSS 优化,请将
optimization.minimize设置为true
// 抽离css文件
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
// 压缩css代码
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
module: {
rules: [
{
test: /.s?css$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
},
],
},
optimization: {
minimizer: [
// `...`,
new CssMinimizerPlugin(),
],
},
plugins: [new MiniCssExtractPlugin()],
};
Scope Hoisting
对作用域进行提升,并且让webpack打包后的代码更小、运行更快
- webpack已经内置了对应的模块
- 在production模式下,默认这个模块就会启用
- 在development模式下,我们需要自己来打开该模块
plugins: [
// ...
new webpack.optimize.ModuleConcatenationPlugin()
]
Tree Shaking
移除 JavaScript 上下文中的未引用代码(dead-code)
usedExports
- production 默认开启
- 需要将
mode配置设置成development,以确定 bundle 不会被压缩
mode: 'development',
optimization: {
usedExports: true,
},
- 应该删除掉未被引用的
export
/* 1 */
/***/ (function (module, __webpack_exports__, __webpack_require__) {
'use strict';
/* unused harmony export square */
/* harmony export (immutable) */ __webpack_exports__['a'] = cube;
function square(x) {
return x * x;
}
function cube(x) {
return x * x * x;
}
});
- 注意,上面的
unused harmony export square注释。没有引用square,但它仍然被包含在 bundle 中。 - 告知 Terser 在优化时,可以删除掉这段代码
- minimize 设置 true:
- usedExports设置为false时,square 函数没有被移除掉;
- usedExports设置为true时,square 函数有被移除掉;
side-effect-free
告知webpack compiler哪些模块时有副作用的
有副作用的文件,例如:文件中有 window.a = 1
- package.json 的
"sideEffects"属性
{
"name": "your-project",
"sideEffects": false
// "sideEffects": ["./src/some-side-effectful-file.js", "**/*.css"]
}
- 还可以在
module.rules配置选项 中设置"sideEffects"
CSS实现Tree Shaking
删除未使用的 CSS
npm install purgecss-webpack-plugin -D
new PurgeCssPlugin({
paths: glob.sync(`${resolveApp("./src")}/**/*`, {nodir: true}),
safelist: function() {
return {
standard: ["body", "html"]
}
}
})
- paths:表示要检测哪些目录下的内容需要被分析,这里我们可以使用glob;
- 默认情况下,Purgecss会将我们的html标签的样式移除掉,如果我们希望保留,可以添加一个safelist的属性;
HTTP压缩
HTTP压缩是一种内置在 服务器 和 客户端 之间的,以改进传输速度和带宽利用率的方式
npm install compression-webpack-plugin -D
new CompressionPlugin({
test: /\.(css|js)$/i,
threshold: 0,
minRatio: 0.8,
algorithm: "gzip",
// exclude
// include
}),
HTML文件中代码的压缩
- production 默认会压缩
- cache:设置为true,只有当文件改变时,才会生成新的文件(默认值也是true)
- minify:默认会使用一个插件
html-minifier-terser
new HtmlWebpackPlugin({
template: "./index.html",
// inject: "body"
cache: true, // 当文件没有发生任何改变时, 直接使用之前的缓存
minify: isProduction ? {
removeComments: false, // 是否要移除注释
removeRedundantAttributes: false, // 是否移除多余的属性
removeEmptyAttributes: true, // 是否移除一些空属性
collapseWhitespace: false,
removeStyleLinkTypeAttributes: true,
minifyCSS: true,
minifyJS: {
mangle: {
toplevel: true
}
}
}: false
}),
InlineChunkHtmlPlugin
将一些chunk出来的模块,内联到html中
npm install react-dev-utils -D
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime.*\.js/,])
Library
const path = require('path');
module.exports = {
mode: "production",
entry: "./index.js",
output: {
path: path.resolve(__dirname, "./build"),
filename: "coderwhy_utils.js",
// AMD/CommonJS/浏览器
libraryTarget: "umd",
// window.coderwhyUtils
library: "coderwhyUtils",
// root 的值
globalObject: "this"
}
}