一、DevServer 和 HMR

为什么要搭建本地服务器

目前我们开发的代码,为了运行需要有两个操作:

  • 操作一:npm run build,编译相关的代码
  • 操作二:通过 live server 或者直接通过浏览器,打开 index.html 代码,查看效果

这个过程经常操作会影响我们的开发效率

  • 我们希望可以做到,当文件发生变化时,可以自动的完成编译和展示
  • 为了完成自动编译,webpack 提供了几种可选的方式:
    • webpack watch mode
    • webpack-dev-server
    • webpack-dev-middleware

Webpack watch

webpack 给我们提供了 watch 模式:

  • 在该模式下,webpack 依赖图中的所有文件,只要有一个发生了更新,那么代码将被重新编译
  • 我们不需要手动去运行 npm run build 指令了

如何开启 watch 呢?两种方式:

  • 方式一:在 webpack.config.js 配置文件中,添加 watch: true
1
2
3
4
5
6
watch: true,
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
  • 方式二:在启动 webpack 的命令中,添加 –watch 的标识脚本
1
2
3
4
5
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"watch": "webpack --watch"
},

npm run watch 即可

webpack-dev-server

watch 模式可以监听到文件的变化,但是有以下缺点

  • 但是事实上它本身是没有自动刷新浏览器的功能的:
  • 目前我们可以在 VSCode 中使用 live-server 来完成这样的功能
  • 我们希望在不适用 live-server 的情况下,可以具备 live reloading(实时重新加载)的功能
  • watch 模式会对所有的源代码进行重新编译
  • 编译成功后,都会生成新的文件(文件操作,效率不高)
  • live-server 每次都会刷新整个页面,效率不高

如何解决? webpack-dev-server

安装:

  • npm install –save-dev webpack-dev-server

配置:

1
2
3
4
5
6
//webpack.config.js文件 可以使用其他值
mode: 'development',

//package.json文件
//webpack5之前是 webpack-dev-server
"serve": "webpack serve"

编译:

  • npm run serve

注意:

  • webpack-dev-server 在编译之后不会写入到任何输出文件。而是将 bundle 文件保留在内存中
  • 事实上 webpack-dev-server 使用了一个库叫 memfs(之前是 memory-fs webpack 自己写的)

webpack-dev-middleware

默认情况下,webpack-dev-server 已经帮助我们做好了一切:

  • 比如通过 express 启动一个服务
  • 比如 HMR(热模块替换)

如果我们想要有更好的自由度,可以使用 webpack-dev-middleware

什么是 webpack-dev-middleware

  • webpack-dev-middleware 是一个封装器(wrapper),它可以把 webpack 处理过的文件发送到一个 server
  • webpack-dev-server 在内部使用了它,然而它也可以作为一个单独的 package 来使用,以便根据需求进行 更多自定义设置

webpack-dev-middleware 的使用

安装,也可以用 koa 主要是开启一个服务器

  • npm install –save-dev express webpack-dev-middleware

编写文件 src 同级目录下编写 server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const express = require("express");
const webapck = require("webpack");
const webpackDevMiddleware = require("webpack-dev-middleware");

const app = express();
//加载配置信息
const config = require("./webpack.config");
//将配置信息传递给webpack进行编译
const compiler = webapck(config);
//将编译后的结果传递给webpack-dev-middleware,返回的中间件供app使用
const middleware = webpackDevMiddleware(compiler);

app.use(middleware);
app.listen(3000);

二、HMR

HMR 的全称是 Hot Module Replacement,翻译为模块热替换

模块热替换是指在应用程序运行过程中,替换、添加、删除模块,而无需重新刷新整个页面

HMR 通过如下几种方式,来提高开发的速度:

  1. 不重新加载整个页面,这样可以保留某些应用程序的状态不丢失
  2. 只更新需要变化的内容,节省开发的时间
  3. 修改了 css、js 源代码,会立即在浏览器更新,相当于直接在浏览器的 devtools 中直接修改样式

使用 HMR

  1. 默认情况下,webpack-dev-server 已经支持 HMR,我们只需要开启即可
  2. 在不开启 HMR 的情况下,当我们修改了源代码之后,整个页面会自动刷新,使用的是 live reloading

webpack.config.js 文件:

1
2
3
devServer: {
hot: true,
},

入口文件 index.js:

1
2
3
4
5
6
7
8
9
import "./math";
console.log("Hello Webpack11");

//监听math模块的更新
if (module.hot) {
module.hot.accept("./math.js", () => {
console.log("模块热更新");
});
}

此时修改 math 模块内容

框架中的 HMR

在开发其他项目时,我们是否需要经常手动去写入 module.hot.accpet 相关的 API 呢?

比如开发 Vue、React 项目,我们修改了组件,希望进行热更新,这个时候应该如何去操作呢

事实上社区已经针对这些有很成熟的解决方案了:

  • 比如 vue 开发中,我们使用 vue-loader,此 loader 支持 vue 组件的 HMR,提供开箱即用的体验
  • 比如 react 开发中,有 React Hot Loader,实时调整 react 组件(目前 React 官方已经弃用了,改成使用 reactrefresh)

React 中的 HMR

在之前,React 是借助于 React Hot Loader 来实现的 HMR,目前已经改成使用 react-refresh 来实现了

安装实现 HMR 相关的依赖:

npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh

注意:这里安装@pmmmwh/react-refresh-webpack-plugin,最新的 npm 安装有 bug(建议使用 lts 版本对应的 npm 版本)

修改 webpack.config.js 和 babel.config.js 文件:

1
2
3
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");

plugins: [new ReactRefreshWebpackPlugin()];
1
2
3
4
module.exports = {
presets: [["@babel/preset-env"], ["@babel/preset-react"]],
plugins: [["react-refresh/babel"]],
};

Vue 中的 HMR

Vue 的加载我们需要使用 vue-loader,而 vue-loader 加载的组件默认会帮助我们进行 HMR 的处理

安装加载 vue 所需要的依赖:

npm install vue-loader vue-template-compiler -D

配置 webpack.config.js:

1
2
3
4
5
6
7
8
const VueLoaderPlugin = require("vue-loader/lib/plugin");

{
test: /\.vue$/;
use: "vue-loader";
}

plugins: [new VueLoaderPlugin()];

HMR 原理

webpack-dev-server 会创建两个服务:

  • 提供静态资源的服务(express)
  • Socket 服务(net.Socket)

express server 负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析)

  1. HMR Socket Server,是一个 socket 的长连接: 长连接有一个最好的好处是建立连接后双方可以通信(服务器可以直接发送文件到客户端)
  2. 当服务器监听到对应的模块发生变化时,会生成两个文件.json(manifest 文件)和.js 文件(update chunk)
  3. 通过长连接,可以直接将这两个文件主动发送给客户端(浏览器)
  4. 浏览器拿到两个新的文件后,通过 HMR runtime 机制,加载这两个文件,并且针对修改的模块进行更新

这说的有些简单,后续再深入吧。

三、一些路径

output 的 publicPath

output 中的 path 的作用是告知 webpack 之后的输出目录:

  • 比如静态资源的 js、css 等输出到哪里,常见的会设置为 dist、build 文件夹等;

output 中还有一个 publicPath 属性,该属性是指定 index.html 文件打包引用的一个基本路径:

  • 它的默认值是一个空字符串,所以我们打包后引入 js 文件时,路径是 bundle.js;

  • 开发中,我们也将其设置为 / ,路径是 /bundle.js,那么浏览器会根据所在的域名+路径去请求对应的资源;

  • 如果我们希望在本地直接打开html 文件来运行,会将其设置为 ./,路径时 ./bundle.js,可以根据相对路径去查找资源

  • 部署项目时,一般设置为’/‘

devServer 的 publicPath

devServer 中也有一个 publicPath 的属性,该属性是指定本地服务所在的文件夹

它的默认值是 /,也就是我们直接访问端口即可访问其中的资源 http://localhost:8080

如果将其设置为了 /abc,那么我们需要通过 http://localhost:8080/abc 才能访问到对应的打包后的资源,并且这个时候,其中的 bundle.js 通过 http://localhost:8080/bundle.js 也是无法访问的,所以必须将 output.publicPath 也设置为 /abc;

官方其实有提到,建议 devServer.publicPath 与 output.publicPath 相同;

devServer 的 contentBase

devServer 中 contentBase 对于我们直接访问打包后的资源其实并没有太大的作用,它的主要作用是如果我们打包后的资源,又依赖于其他的一些资源,那么就需要指定从哪里来查找这个内容:

比如在 index.html 中,需要依赖一个 abc.js 文件,这个文件我们存放在 public 文件中

在打包后的 index.html 中,我们应该如何去引入这个文件呢?

比如代码是这样的:

但是这样打包后浏览器是无法通过相对路径去找到这个文件夹的

所以代码是这样的:

但是我们如何让它去查找到这个文件的存在呢? 设置 contentBase 即可

当然在 devServer 中还有一个可以监听 contentBase 发生变化后重新编译的一个属性:watchContentBase

新版 dev-server 取消了 contentBase,改用了 static

1
2
3
4
5
<body>
<h2>Hello Webpack Devserver</h2>

<script src='./aaa.js'></script>
</body>

四、其他配置

hotOnly、host 配置

hotOnly 是当代码编译失败时,是否刷新整个页面:

默认情况下当代码编译失败修复后,我们会重新刷新整个页面;

如果不希望重新刷新整个页面,可以设置 hotOnly 为 true;

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、compress

port 设置监听的端口,默认情况下是 8080

open 是否打开浏览器:

  • 默认值是 false,设置为 true 会打开浏览器;
  • 也可以设置为类似于 Google Chrome 等值;

compress 是否为静态文件开启 gzip compression:

  • 默认值是 false,可以设置为 true;

Proxy 代理

proxy 是开发中非常常用的一个配置选项,它的目的设置代理来解决跨域访问的问题:

比如一个 api 请求是 http://localhost:8888,但是本地启动服务器的域名是 http://localhost:8000,这个时候发送网络请求就会出现跨域的问题

那么我们可以将请求先发送到一个代理服务器,代理服务器和 API 服务器没有跨域的问题,就可以解决我们的跨域问题了

可以进行如下的设置:

  • target:表示的是代理到的目标地址,比如 /api-hy/moment 会被代理到 http://localhost:8888/apihy/moment;
  • pathRewrite:默认情况下,我们的 /api-hy 也会被写入到 URL 中,如果希望删除,可以使用 pathRewrite;
  • secure:默认情况下不接收转发到 https 的服务器上,如果希望支持,可以设置为 false;
  • changeOrigin:它表示是否更新代理后请求的 headers 中 host 地址;

真实的请求,其实是需要通过 http://localhost:8888 来请求的; 但是因为使用了代码,默认情况下它的值时 http://localhost:8000如果我们需要修改,那么可以将 changeOrigin 设置为 true 即可

historyApiFallback

historyApiFallback 是开发中一个非常常见的属性,它主要的作用是解决 SPA 页面在路由跳转之后,进行页面刷新时,返回 404 的错误。

因为 SPA 页面是由路由跳转的,刷新以后去服务器请求资源没有对应路径的资源存在,就会返回 404 页面

boolean 值:默认是 false

  • 如果设置为 true,那么在刷新时,返回 404 错误时,会自动返回 index.html 的内容;

object 类型的值,可以配置 rewrites 属性:

  • 可以配置 from 来匹配路径,决定要跳转到哪一个页面;

事实上 devServer 中实现 historyApiFallback 功能是通过 connect-history-api-fallback 库的:可以查看 connect-history-api-fallback 文档

五、resolve 模块解析

resolve 用于设置模块如何被解析:

在开发中会有各种各样的模块依赖,这些模块可能来自于自己编写的代码,也可能来自第三方库;

resolve 可以帮助 webpack 从每个 require/import 语句中,找到需要引入到合适的模块代码;

webpack 使用 enhanced-resolve 来解析文件路径;

webpack 能解析三种文件路径:

  • 绝对路径

    • 由于已经获得文件的绝对路径,因此不需要再做进一步解析。
  • 相对路径

    • 在这种情况下,使用 import 或 require 的资源文件所处的目录,被认为是上下文目录;在 import/require 中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径;
  • 模块路径

    • 在 resolve.modules 中指定的所有目录检索模块;

    • 默认值是 [‘node_modules’],所以默认会从 node_modules 中查找文件;

    • 可以通过设置别名的方式来替换初识模块路径

确实文件还是文件夹

如果是一个文件:

  • 如果文件具有扩展名,则直接打包文件
  • 否则,将使用 resolve.extensions 选项作为文件扩展名解析;

如果是一个文件夹:

  • 会在文件夹中根据 resolve.mainFiles 配置选项中指定的文件顺序查找;

  • resolve.mainFiles 的默认值是 [‘index’];

  • 再根据 resolve.extensions 来解析扩展名;

extensions 和 alias 配置

extensions 是解析到文件时自动添加扩展名:

  • 默认值是 [‘.wasm’, ‘.mjs’, ‘.js’, ‘.json’];

  • 所以如果我们代码中想要添加加载 .vue 或者 jsx 或者 ts 等文件时,我们必须自己写上扩展名;

另一个非常好用的功能是配置别名 alias:

  • 特别是当我们项目的目录结构比较深的时候,或者一个文件的路径可能需要 ../../../这种路径片段;
  • 我们可以给某些常见的路径起一个别名;

上述这些配置都是在 devServer 中