Webpack 的配置文件:
当执行 webpack
命令时,会先去根目录下查找是否有 webpack.config.js
配置文件。如果有,就会读取该配置文件中的配置去执行相关的编译;如果没有的话,就会根据默认配置去执行相关的编译。
在项目根目录下新建 webpack.config.js
配置文件,通过 module.exports = {}
导出。
因为 Webpack 是在 Node 环境下执行的,Webpack 查找和读取配置文件的时候,是通过 CommonJS 的方式,因此在 Webpack 配置文件中要通过 CommonJS 的方式导出。
-
可以导出一个对象。
module.exports = {}
-
也可以导出一个函数,将会接收一个参数,返回值是一个对象
module.exports = function(env) { console.log('webpack 的 env 对象:', env) return { } }
手动指定 Webpack 的配置文件:
可以不使用 webpack.config.js
这个默认的配置文件名,假如使用 wk.config.js
作为 Webpack 的配置文件名,但是当执行 webpack
命令时就会找不到配置文件,可以给 webpack
命令添加一个 config 参数来提供 Webpack 配置文件的路径。webpack --config wk.config.js
。
Webpack 常见的配置选项:
content 和 entry 入口:
Webpack 通过 context 和 entry 这两个配置项来共同决定入口文件的路径。在配置入口时,实际上做了两件事:
- 确定入口模块位置,告诉 Webpack 从哪里开始进行打包。
- 定义
chunk name
。如果工程只有一个入口,那么默认 chunk name 为 main;如果工程有多个入口,需要为每个入口定义chunk name
,来作为该 chunk 的唯一标识。
content:
context:配置 Webpack 打包的入口的路径前缀,必须是绝对路径。可以省略,默认值为当前工程的根目录。
也就是说,content 会和 entry 拼接成一个绝对路径。
其实,context 不仅会影响 Webpack 打包的入口的路径前缀,也会影响 loader 的路径前缀(module/rules
下的loader
)。
const path = require('path');
module.exports = {
context: path.join(__dirname,'./src'),
entry: './index.js',
}
entry:
entry:配置 Webpack 打包的入口。属性值可以有多种形式:字符串、数组、对象、函数。默认使用当前目录下的 src/index.js
作为打包的入口文件。
在使用字符串或数组定义单入口时,并没有办法更改
chunk name
,只能使用默认的 main;在使用对象来定义多入口时,则必须为每一个入口定义chunk name
。
对于单页应用来说,一般定义单一入口即可。无论是框架、库,还是各个页面的模块,都由单一的入口进行引用。这样做的好处是只会产生一个 JavaScript 文件,依赖关系清晰。而这种做法也有弊端,即所有模块都打包到一起,当应用的规模上升到一定程度之后会导致产生的资源体积过大,降低用户的页面渲染速度。
对于多页应用的场景,为了尽可能减少资源的体积,希望每个页面都只加载各自必要的逻辑,而不是将所有页面都打包到同一个 bundle 中,因此每个页面都需要一个独立的 bundle,每个 HTML 只要引入各自的 JavaScript 就可以加载其所需要的模块,这种情形使用多入口来实现。
- 字符串类型入口:
module.exports = { // 入口文件 entry:'./src/index.js' }
- 数组类型入口:传入一个数组的作用是将多个资源预先合并,在打包时 Webpack 会将数组中的最后一个元素作为实例的入口路径。
上面的配置等同于:module.exports = { entry:['babel-polyfill','./src/index.js'] }
//webpack.config.js module.exports = { entry:'./src/index.js' } //index.js import 'babel-polyfill'
- 对象类型入口:如果想要定义多入口,则必须使用对象的形式。对象的属性名是
chunk name
;属性值是入口路径。module.exports = { entry:{ index:'./src/index.js', lib:'./src/lib.js' } }
- 函数类型入口:用函数定义入口时,只要返回上面的任何配置形式即可。 传入一个函数的优点在于可以在函数体里添加一些动态的逻辑来获取工程的入口,并且函数也支持返回一个 Promise 对象来进行异步操作。
module.exports = { entry:()=>'./src/index.js' }
module.exports = { entry:() => new Promise((resolve)=>{ //模拟异步操作 setTimeout(()=>{ resolve('./src/index.js'); },1000) }) }
output 出口:
output:配置 Webpack 打包的出口。默认使用当前目录下的 dist/main.js
作为打包的出口文件。output 的常用属性有:
-
path:指定打包出口的路径,必须是绝对路径。
-
filename:指定打包出口的文件名。
// Node 中内置的 path 模块 const path = require('path') module.exports = { // 出口 output: { // 出口路径,只能是绝对路径 path: path.resolve(__dirname, './build'), // 通过 path.resolve(__dirname) 可以获取到当前文件所在的绝对路径 // 出口文件名 filename: 'bundle.js', } }
-
publicPath:指定打包生成的
index.html
文件引用其他打包生成的文件时的基本路径。默认是空字符串。const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { // 不配置 output.publicPath,默认是空字符串 plugins: [ new HtmlWebpackPlugin() ] }
const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { // 配置 output.publicPath output: { publicPath: './', }, plugins: [ new HtmlWebpackPlugin() ] }
-
library:打包生成的 JavaScript 文件作为一个 library 库的名称。
-
libraryTarget:打包生成的 JavaScript 文件作为一个 library 库时支持哪些环境。
-
clean:在打包生成文件之前,先清空之前的 output 目录。就相当于是使用 CleanWebpackPlugin 插件。
devServer 本地服务器:
devServer
:在开发过程中,开启一个本地服务。生产环境用不到这个选项。
如果修改了 Webpack 配置,需要重新运行一下 Webpack。
devServer
的选项有:
-
hot:是否开启模块热替换。
-
hotOnly:当代码有问题导致编译失败,修复代码后又编译成功时,是否刷新整个页面。默认是会刷新整个浏览器。
devServer.hotOnly
是 Webpack4 中的选项,Webpack5 已废弃,默认已经不会刷新整个浏览器了。 -
host:设置主机地址。默认值是
localhost
计算机网络规定
127.0,0.1
和localhost
都指向本机,任何用户都能通过127.0,0.1
和localhost
访问到自己电脑中的服务。localhost
:本质上是一个域名,通过情况下会被解析成127.0.0.1
。
127.0.0.1
:回环地址,表示主机自己发出去的包,直接被自己接收。正常的包要经过应用层->传输层->网络层->数据链路层->物理层
,而回环地址在网络层就直接被获取到了,不会经过数据链路层和物理层。每台电脑都有局域网 IP 地址,也是指向本机。只要关掉电脑的防火墙,同一局域网下的任何电脑都能通过访问到这台电脑的 IP 地址来访问其中的服务。
127.0,0.1
、localhost
和电脑的局域网 IP 地址,用户都可以通过访问它们访问到自己电脑中的服务,对于用户自己来说,它们是一样的。但是,电脑的局域网 IP 地址还可以提供给别人来访问,同一局域网下的任何电脑都能通过访问到这台电脑的 IP 地址来访问其中的服务。
通过局域网 IP 地址可以找到某台电脑,通过端口可以找到电脑中的某个服务。
-
port:设置端口号。默认值是 8080。
-
open:当编译成功后是否自动打开浏览器。默认值是 false。属性值除了设置布尔值之外,也可以设置
Google Chrome
等值。 -
compress:是否为静态文件开启 gzip 压缩。默认值是 true。
浏览器发现是 gzip 文件的话,会自动对其进行解压。
-
publicPath:会将文件打包到该路径下,通过本地服务在浏览器中可以访问到该路径下的打包文件。
默认值是
/
,假设本地服务运行在http://localhost:8080
上,打包后的根目录下有index.html
文件和main.js
文件,那么直接访问http://localhost:8080
就可以访问到打包后的index.html
文件,访问http://localhost:8080/mainn.js
就可以访问到打包后的main.js
文件。
但是,如果将devServer.publicPath
设置为/home
,那么会将文件打包到home
目录下,需要通过http://localhost:8080/home
才可以访问到打包后的index.html
文件,通过http://localhost:8080/home/mainn.js
才可以访问到打包后的main.js
文件;并且此时需要将outpot.publicPath
也设置为/home
,否则的话,打包生成的index.html
文件中引用的main.js
的的地址为http://localhost:8080/main.js
,是无法访问到的。devServer.publicPath
是 Webpack4 中的选项,Webpack5 已废弃。
output.publicPath
影响打包后资源的路径;devServer.publicPath
影响在本地服务中对资源的访问。const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { devServer: { publicPath: '/home', }, plugins: [ new HtmlWebpackPlugin() ] }
const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { output: { publicPath: '/home', }, devServer: { publicPath: '/home', }, plugins: [ new HtmlWebpackPlugin() ] }
-
contentBase:指定不由 Webpack 打包的静态资源的根目录。默认值是
/
根目录。devServer.contentBase
是 Webpack4 中的选项,Webpack5 已废弃。例如:有一些图片资源很少发生改变,那么这些图片资源就可以不通过 Webpack 打包,以此来提高 Webpack 的效率。假设将这些图片资源放置在根目录下的 images 文件夹下,并且在
index.html
中通过<image src="default-avatar.jpg"></image>
引入,由于 contentBase 的默认值是根目录,启动webpack-dev-server
后就会去根目录下寻找图片资源。
配置contentBase: path.resolve(__dirname, './images')
后,重新启动webpack-dev-server
,根目录就变为了之前的根目录拼接上 images,就会去这个根目录下寻找图片资源。const path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { devServer: { contentBase: path.resolve(__dirname, './images'), }, plugins: [ new HtmlWebpackPlugin({ template: './index.html' }) ] }
-
watchContentBase:指定当修改了 contentBase 下的静态资源时,是否自动刷新浏览器。默认值为 false。
-
proxy: 配置代理。开发过程中,如果本地服务想要访问后端接口,就会产生跨域,proxy 就是为了解决跨域的问题。
proxy 是利用
http-proxy-middleware
这个 http 代理中间件来实现代理的。当浏览器发出请求时,该请求就会被转发到代理服务器上,代理服务器再向后端服务器请求数据,后端服务器返回数据给代理服务器,代理服务器再返回到浏览器上。proxy 的属性值是一个对象,默认会使用 key 值去匹配请求路径的开头,value 值对匹配到的路径进行各种配置。
- target:要代理到的目标服务器地址。
- pathRewrite:重写请求的路径,默认是匹配请求路径的开头。
- secure:默认不支持代理到 https 上,可以通过设置
secure: false
选项使其支持。 - changeOrigin:是否更改代理后请求的 headers 中的 host 地址。
由于浏览器的同源策略,如果浏览器访问不同源的请求,就会产生跨域问题。
只要协议、域名、端口有一个不一样,就是不同源。
跨域是浏览器的安全策略,服务器之间是不存在跨域的,因此可以通过设置一个代理服务器的方式来解决跨域问题。// index.js import axios from 'axios' axios.get('/api/staff/').then(res => { console.log(res) })
// webpack.config.js module.exports = { devServer: { proxy: { // key 默认会去匹配请求路径的开头,value 是要代理到的目标服务器地址 // 将所有以 /api 路径开头的请求代理到 http://127.0.0.1:8000 目标服务器上。也就是说,假设本地服务是 http://localhost:8080/,那么当请求 http://localhost:8080/api/staff/ 时,将会被代理到 http://127.0.0.1:8000/api/staff/ 上 '/api': 'http://127.0.0.1:8000', } }, }
如果请求的路径开头不希望带上
/api
,可以重写路径。// webpack.config.js module.exports = { devServer: { proxy: { // 假设本地服务是 http://localhost:8080/,那么当请求 http://localhost:8080/api/staff/ 时,将会被代理到 http://127.0.0.1:8000/staff/ 上 '/api': { // 代理到的目标服务器地址 target: 'http://127.0.0.1:8000', // 重写请求的路径,默认是匹配请求路径的开头 // 会将 /api 替换为空字符串 pathRewrite: { '^/api': '', } } } }, }
如果后端服务器有对请求的来源做验证的话,可以设置
changeOrigin: true
来避免来源不一致导致请求出现问题。module.exports = { devServer: { proxy: { '/api': { target: 'https://127.0.0.1:8000', // 假设本地服务是 http://localhost:8080/,但其实真正向后端服务器发出请求的是 https://127.0.0.1:8000。默认情况下后端服务器获取到的 headers.host 还是 http://localhost:8080/,可以通过配置 changeOrigin 将其修改为 https://127.0.0.1:8000 changeOrigin: true, } } }, }
-
historyApiFallback:主要作用是解决 SPA 单页应用在 history 模式下,路由跳转之后进行页面刷新,返回 404 的问题。它会将没有匹配到静态文件的请求重定向到指定的 HTML 文件,以确保前端路由能够正确处理这些请求并渲染相应的页面。
开发一个简单的 React 应用,点击首页按钮调整到首页页面,点击关于按钮跳转到关于页面。如果跳转到首页或者关于页面后,刷新浏览器,页面就会 404。这是因为,SPA 单页应用中的路由都是前端路由,前端监听到路径发生变化时,执行 JS 代码来渲染对应的组件;但是如果刷新页面,浏览器就会向服务器发出当前页面的链接的请求,服务器中并没有这个路径的页面,因此返回 404。
生产环境的话,可以让后端在 nginx 中配置只要 404 就返回
index.html
文件,然后前端路由再去渲染对应的组件,也可以解决这个问题。
配置 historyApiFallback 选项来解决刷新浏览器 404 的问题。module.exports = { devServer: { // 如果刷新浏览器会导致 404 的话,将会默认返回入口的 index.html 文件。属性值可以是一个对象,配置匹配到不同的路径返回不同的 HTML 文件 historyApiFallback: true, } }
mode 模式:
mode:告知 Webpack 使用相应模式的内置优化,配置一个选项就相当于是配置了一堆选项。属性值有 production、development、none。默认是 production。
在被 Webpack 编译打包的源文件中可以通过
process.env.NODE_ENV
来获取到 mode 设置的属性值。
resolve 模块解析:
resolve:配置模块如何被解析。使得 Webpack 可以根据 require/import
等模块导入语句,正确找到需要引入的的模块。
Webpack 是如何根据模块导入语句查找到文件的?
首先,Webpack 使用 enhanced-resolve
库来解析文件路径,能解析三种文件路径:
- 绝对路径:由于已经获取文件的绝对路径,因此不需要再做进一步的解析。
- 相对路径:会把使用
require/import
等模块导入语句的文件所在的目录作为上下文目录,将require/import
等模块导入语句中给定的相对路径拼接上上下文路径,生成模块的绝对路径。 - 模块路径:会在
resolve.modules
指定的目录中查找模块。
然后,确定了文件路径之后,Webpack 会确定是文件还是文件夹:
- 如果是一个文件:
- 有扩展名的话,就可以直接找到并加载文件。
- 如果没有扩展名的话,会根据
resolve.extensions
选项中指定的文件扩展名来解析。
- 如果是一个文件夹:会在文件夹中根据
resolve.mainFiles
选项中指定的文件名来查找;再根据resolve.extensions
选项中指定的文件扩展名来解析。
选项:
-
modules:配置 Webpack 解析模块时要查找的目录。默认值是
['node_modules']
。 -
extensions:配置 Webpack 解析文件时的扩展名,用户在引入模块时就不需要再带扩展名了。默认值是
['.js', '.json', '.wasm']
。如果有多个后缀名不同但文件名相同的文件,Webpack 会解析在数组首位的后缀的文件。
module.exports = { resolve: { // 配置 Webpack 解析文件时的扩展名为 js 和 json extensions: ['.js', '.json'], }, };
// 用户在引入模块时就不需要再带扩展名了,Webpack 将会去查找是否有 home.js 并对其进行解析,如果没有的话,继续去去查找是否有 home.json 并对其进行解析 import Home from '../pages/home';
配置
resolve.extensions
会覆盖默认的扩展名,也就是 Webpack 将不会再使用默认的扩展名来解析模块。可以使用...
来配置默认的扩展名。module.exports = { resolve: { extensions: ['.ts', '...'], }, };
-
mainFiles:配置 Webpack 解析文件夹时要使用的文件名。
-
alias:配置路径的别名。当项目的目录结构比较深的时候,引入一个模块可能就需要
../../../
这种路径片段,此时就可以给路径配置一个别名,使得导入模块时更加简单。
const path = require('path');
module.exports = {
resolve: {
alias: {
Utilities: path.resolve(__dirname, 'src/utilities/'),
},
},
};
// 配置路径别名之前导入模块的写法
import Utility from '../../utilities/utility';
// 配置路径别名之后导入模块的写法
import Utility from 'Utilities/utility';
resolveLoader:
resolveLoader:配置 loader 如何被解析。
- modules:配置 Webpack 解析 loader 时要查找的目录。默认值是
['node_modules']
。
optimization 优化:
optimization 的选项有:
-
chunkIds:告知 Webpack 采用什么算法生成打包后的模块的 id 。
属性值有:- natural:使用自然数。不推荐。
- named:使用包所在的目录作为 name。在开发环境下推荐。
- deterministic:生成 id,并且同一个文件生成的 id 总是不变的。
-
runtimeChunk:是否要将运行时的代码抽离到一个单独的文件中。
属性值有:- false:直接嵌入到打包到输出的文件中。是默认值。
- true/multiple:将每个入口下的运行时的代码分别抽离到一个单独的文件中。
- single:将每个入口下的运行时的代码统一抽离到一个单独的文件中。
- 对象:还可以是一个对象,来为其设置 name 属性。
Webpack 运行时的代码是 Webpack 自己生成并添加到输出文件中的一些逻辑,用于对模块进行加载和管理,以便在浏览器中正确加载和执行打包后的代码。
// webpack.config.js module.exports = { optimization: { runtimeChunk: true, } }
-
SplitChunks:对导入的模块进行分离(详细内容可查看《十二》对打包后的代码进行分离一节)。
externals 外部扩展:
可以在 Webpack 配置文件中配置 externals 选项,实现对通过 import 导入的第三方包不进行打包,而是在运行时在从外部去获取这些扩展依赖。
- 安装 lodash。
- 新建
src/index.js
文件,并编写代码。// src/index.js import _ from 'lodash' console.log(_.join(['a', 'b', 'c'], '/'))
- 先在
webpack.config.js
中配置 splitChunks 来使导入的模块进行代码分离,以便查看效果。const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { optimization: { splitChunks: { chunks: "all" } }, plugins: [ new HtmlWebpackPlugin({ template: './index.html' }) ] }
- 运行
webpack
命令进行打包,可以看到导入的 lodash 被单独打包为了一个文件。
-
在
webpack.config.js
中配置 externals 选项来排除对 lodash 库的打包。module.exports = { // 配置 externals 选项,实现对指定的通过 import 导入的包不进行打包,而是在运行时在从外部去获取这些扩展依赖。externals 是一个对象,其中的 key 是要忽略不进行打包的 import 包,value 是这个包向外暴露的全局对象。 externals: { lodash: '_', }, optimization: { splitChunks: { chunks: "all" } } }
-
在
index.html
中加入 lodash 库的 CDN 服务器地址。<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script> </body> </html>
-
运行
webpack
命令进行打包,会发现 lodash 没有再被打包进去了,在浏览器中运行也会发现没有任何问题。就可以实现对指定的通过 import 导入的包不进行打包,而是在运行时在从外部去获取这些扩展依赖。