devServer
devServer 使用
- 操作一:npm run build,编译相关的代码
- 操作二:通过 live-server 或者直接通过浏览器,打开 index.html,查看效果
为了完成自动编译,webpack 提供了几种可选方式:
- webpack watch mode
- webpack-dev-server(常用)
- webpack-dev-middleware
watch
- 在该模式下,webpack 依赖图中的所有文件,只要有一个发生了更新,那么代码将被重新编译
- 我们不需要手动去运行 npm run build 指令了
如何开启 watch:
- 在导出的配置中,添加
watch: true
- 在启动 webpack 命令中,添加
--watch
标识
{
"scripts": {
"watch": "webpack --watch"
}
}
devServer
上面的方式可以监听到文件的变化,但是事实上它本身是没有自动刷新浏览器的功能
- 可以在 VSCode 中使用 live-server 来完成这样的功能
- 我们希望在不使用 live-server 的情况下,可以具备 live reloading(实时重新加载)的功能
npm install webpack-dev-server -D
webpack-dev-server 在编译之后 不会写入到任何输出文件,而是将 bundle 文件 保留在内存 中
- webpack-dev-server 使用了一个库叫 memfs(memory-fs webpack 自己写的)
- 开发阶段:contentBase
- 打包阶段:copyWebpackPlugin
// package.json
{
"scripts": {
"serve": "webpack serve"
}
}
// webpack.config.js
module.exports = {
devServer: {
contentBase: './public'
}
}
HMR
HMR 的全称是 Hot Module Replacement,翻译为 模块热替换
- 模块热替换是指在 应用程序运行过程中,替换、添加、删除模块,而 无需重新刷新整个页面
HMR 通过如下几种方式,提高开发速度
- 不重新加载整个页面,这样 可以保留某些应用程序的状态不丢失
- 只更新 需要变化的内容,节省开发的时间
- 修改了 css、js 源代码,会 立即在浏览器更新,相当于直接在浏览器的 devtools 中直接修改样式
如何使用 HMR?
- 默认情况下,webpack-dev-server 已经支持 HMR,我们只需要开启即可
- 在不开启 HMR 的情况下,当我们修改了源代码之后,整个页面会自动刷新,使用的是 live reloading
module.exports = {
target: 'web',
devServer: {
hot: true
}
}
浏览器可以看到如下效果:
当我们修改了某一个模块的代码时,依然是刷新的整个页面
- 我们需要去指定哪些模块发生更新时,进行 HMR
if (module.hot) {
module.hot.accept('./js/element.js', () => {
console.log('element模块发生更新了')
})
}
在开发 Vue、React 项目,我们修改了组件,希望进行热更新,这时候如何操作?
- 比如 vue 开发中,我们使用 vue-loader,此 loader 支持 vue 组件的 HMR,提供开箱即用的体验
- 比如 react 开发中,有 React Hot Loader,实时调整 react 组件(目前 React 官方已经弃用了,改成使用 react-refresh)
vue-loader 特性:
- 用来解析和转换 vue文件,提取出其中的 script、style、template,把它们交给对应的 loader 去处理
- 将 style 和 template 中引用的资源当做模块依赖处理,为每个这样的组件模拟出 scope
- 允许使用热更新
HMR 原理
- webpack-dev-server 会创建两个服务: 提供静态资源的服务(express)和 Socket 服务(net.Socket)
- express server 负责提供 静态资源的服务(打包后的资源直接被浏览器请和解析)
HMR Socket Server 是一个 socket 的长连接:
- 长连接有一个最好的好处是 建立连接后双方可以通信(服务器可以直接发送文件到客户端)
- 通过长连接,可以直接 将两个文件主动发送给客户端(浏览器)
- 浏览器 拿到两个新的文件 后,通过 HMR runtime 机制,加载这两个文件,并且 针对修改的模块进行更新
其他配置
hotOnly、host 配置
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:设置监听的端口,默认情况是 8080
open:是否打开浏览器
- 默认值是 false,设置为 true 会打开浏览器
- 也可以设置为类似 Google Chorme 等值
compress:是否为静态文件开启 gzip compression
- 默认值是 false,可以设置为 true
Proxy
proxy:设置代理来解决跨域访问的问题
- 比如一个 api 请求是
http://localhost:8888
但是本地启动服务器的域名是http://localhost:8000
,这时候发送网络请求会出现跨域问题 - 可以将请求先发送到一个代理服务器,代理服务器和 API 服务器没有跨域问题,就可以解决我们的跨域问题了
一些配置项:
- target:代理到的目标地址,比如:
/api-hy/moment
会被代理到http://localhost:8888/api-hy/moment
- pathRewrite:默认情况下,我们的
/api-hy
也会被写入到 URL 中,如果希望删除,可以使用 pathRewirite - secure:默认情况下不接收转发到 https 的服务器上,如果希望支持,可以设置为 false
- changeOrigin:它表示是否更新代理后请求的 headers 中 host 地址
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8888',
pathRewrite: {
'^/api': ''
},
secure: false,
changeOrigin: true
}
}
},
}
changeOrigin 其实是修改代理请求中的 headers 中的 host 属性:
- 真实的请求,其实是需要通过
http://localhost:8888
来请求的 - 因为使用了代码,默认情况下它的值是
http://localhost:8000
- 如果我们需要修改,那么可以将 changeOrigin 设置为 true 即可
historyApiFallback
它主要的作用是解决 SPA 页面在路由跳转之后,进行页面刷新时,返回 404 的错误
boolean 值:默认是 false
- 如果设置为 true,那么在刷新时,返回 404 错误时,会自动返回 index.html 的内容
object 类型的值,可以配置 rewrites 属性:
- 可以配置 from 来匹配路径,决定要跳转到哪一个页面
事实上 devServer 中实现 historyApiFallback 功能是通过 connect-history-api-fallback 库的:
- 可以查看 connect-history-api-fallback 文档
resolve
resolve
resolve 用于设置模块如何被解析:
- 在开发中我们会有各种各样的模块依赖,这些模块可能来自于自己编写的代码,也可能来自第三方库
- resolve 可以帮助 webpack 从 require/import 语句中,找到需要引入到合适的模块代码
- webpack 使用 enhanced-resolve 来解析文件路径
webpack 能解析三种文件路径
绝对路径
- 由于已经获得文件的绝对路径,因此不需要再做进一步解析
相对路径
- 在这种情况下,使用 import 或 require 的资源文件所处的目录,被认为是上下文目录
- 在 import/require 中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径
模块路径
在 resolve.modules 中指定的所有目录检索模块
默认值是
['node_modules']
,所以默认会从 node_modules 中查找文件可以通过设置别名的方式来替换初识模块路径(alias)
确认是文件还是文件夹
如果是文件夹:
- 如果文件具有扩展名,则直接打包文件
- 否则,将使用 resolve.extensions 选项作为文件扩展名解析
如果是一个文件夹:
会在文件夹中根据 resolve.mainFiles 配置选项中指定的文件顺序查找
resolve.mainFiles 的默认值是 ['index']
再根据 resolve.extensions 来解析扩展名
extensions 和 alias
extensions 是解析到文件时自动添加扩展名:
- 默认值是:['.wasm', '.mjs', '.js', '.json']
- 所以如果我们代码中想要添加加载 .vue 或者 .jsx 或者 .ts 等文件时,我们必须自己写上扩展名
另一个非常好用的功能是配置别名 alias:
- 特别是当我们项目的目录结构比较深的时候,或者一个文件的路径可能需要
../../../
这种路径片段 - 我们可以给某些常见的路径起一个别名
区分开发生产环境
我们所有的 webpack 配置信息都是放到一个配置文件中的:webpack.config.js
- 当配置越来越多时,这个文件会变得越来越不容易维护
- 并且某些配置是在开发环境需要使用的,某些配置是在生成环境需要使用的,当然某些配置是在开发和生成环境都会使用的
- 所以,我们最好对配置进行划分,方便我们维护和管理
在启动时如何可以区分不同的配置呢?
- 方案一:编写两个不同的配置文件,开发和生成时,分别加载不同的配置文件即可
- 方案二:使用相同的一个入口配置文件,通过设置参数来区分它们
入口文件解析:
context 的作用是用于解析入口(entry point)和加载器(loader)
默认是当前路径(测试,默认是 webpack 的启动目录)
我们创建三个文件:
- webpack.comm.conf.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { DefinePlugin } = require('webpack')
const { VueLoaderPlugin } = require('vue-loader/dist/index')
module.exports = {
target: 'web',
entry: './src/main.js',
output: {
path: path.resolve(__dirname, '../build'),
filename: 'js/bundle.js'
},
resolve: {
extensions: ['.js', '.json', '.mjs', '.vue', '.ts', '.jsx', '.tsx'],
alias: {
'@': path.resolve(__dirname, '../src'),
js: path.resolve(__dirname, '../src/js')
}
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
// },
{
test: /\.(jpe?g|png|gif|svg)$/,
type: 'asset',
generator: {
filename: 'img/[name]_[hash:6][ext]'
},
parser: {
dataUrlCondition: {
maxSize: 10 * 1024
}
}
},
{
test: /\.(eot|ttf|woff2?)$/,
type: 'asset/resource',
generator: {
filename: 'font/[name]_[hash:6][ext]'
}
},
{
test: /\.js$/,
loader: 'babel-loader'
},
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
title: '哈哈哈哈'
}),
new DefinePlugin({
BASE_URL: "'./'",
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false
}),
new VueLoaderPlugin()
]
}
- webpack.dev.conf.js
const { merge } = require('webpack-merge')
const commonConfig = require('./webpack.comm.config')
module.exports = merge(commonConfig, {
mode: 'development',
devtool: 'source-map',
devServer: {
contentBase: './public',
hot: true,
// host: "0.0.0.0",
port: 7777,
open: true,
// compress: true,
proxy: {
'/api': {
target: 'http://localhost:8888',
pathRewrite: {
'^/api': ''
},
secure: false,
changeOrigin: true
}
}
}
})
- webpack.prod.conf.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const { merge } = require('webpack-merge')
const commonConfig = require('./webpack.comm.config')
module.exports = merge(commonConfig, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
{
from: './public',
globOptions: {
ignore: ['**/index.html']
}
}
]
})
]
})