让你的本地开发支持http2

Http2 带来的好处毋庸置疑,最直接的就是能够多路复用且不会出现队头阻塞的情况,一般在本地开发的时候并不需要 http2,除非你的项目中有大量并发的请求。

          本文主要针对于在 webpack 项目中添加 http2 特性,但整体思路在其它项目中通用。

webpack 官方文档中有提供一种方式开启 http2,即在 devServer 中调整配置如下:

js 复制代码
module.exports = {
  //...
  devServer: {
    server: "spdy",
  },
};

上述配置参考 webpack devServer.server

其中,将 devServer.server 指定为 spdy 则会在 webpack 中使用 spdy 库来开启 http2,但在配置下面有一行警告,大概意思是:

    该选项在 Node 15.0.0 及以上版本中被忽略,因为对于这些版本,spdy 是无效的。一旦 Express 支持,devServer 也将迁移到 Node 的内置HTTP/2。

我尝试了 Node 14,依旧会出现一些问题,例如某些请求会出现内容被截断导致请求体解析报错 (failed)net::ERR_CONTENT_DECODING_FAILED

并且我查看了 Express 的官方文档,发现其并不支持 http2,最终我放弃了 webpack 内置的这种方案。

另辟蹊径

既然 Webpack 内置的选项支持有问题,那我们就通过代理的方式自己来实现,其原理很简单,大概思路如下:

  1. 独立启动一个 http2 服务
  2. 将 http2 服务收到的请求转发到 devServer

原来的 devServer 端口还是保持不变,我们自己手动启动一个 http2 服务器,但由于 http2 是必须建立在 https 之上,因此我们首先得准备一个本地开发的 ssl 证书,可以选择一些开源库来生成,例如 selfsigned,但每次启动都生成不一样的证书会导致浏览器每次都出现证书警告提醒:

如果出现上面这种提示,则需要点【高级】按钮,然后点继续才可以通过。

但我更倾向于直接一次性生成证书并保存到本地,这样就避免了每次启动都收到证书警告提醒的情况。

生成本地开发的 https 证书

可以参考以下方式一次性生成证书,在 bash 或者 git-bash 中:

  • 在项目内创建一个 ssl 目录用来存放 ssl 证书 mkdir ssl
  • 生成秘钥 openssl genrsa 1024 > ssl/key.pem
  • 生成证书 openssl req -x509 -new -key key.pem > ssl/key-cert.pem

启动 http2 服务

js 复制代码
const http2 = require("http2");

const http2Server = http2.createSecureServer({
  allowHTTP1: true, // 必须提供,否则 websocket 无法被代理
  key: fs.readFileSync("./ssl/key.pem"),
  cert: fs.readFileSync("./ssl/key-cert.pem"),
});

使用 http2-proxy 来代理 http2 请求

js 复制代码
const proxy = require("http2-proxy");
const finalhandler = require("finalhandler");

http2Server.on("request", (req, res) => {
  proxy.web(
    req,
    res,
    {
      hostname: "localhost",
      port: targetPort, // devServer 的端口
      // protocol: "https", // 默认为 http,如果 devServer 开启了 https 则需要显式指定 https
    },
    // 错误处理
    (err, req, res) => {
      if (err) {
        console.error("proxy error", err);
        finalhandler(req, res)(err); // 为了简化逻辑,我们使用 finalhandler 去处理错误的情况
      }
    }
  );
});

使用 http2-proxy 来代理 websocket 请求的 upgrade 消息

js 复制代码
http2Server.on("upgrade", (req, socket, head) => {
  proxy.ws(
    req,
    socket,
    head,
    {
      hostname: "localhost",
      port: targetPort, // devServer 的端口
      // protocol: "https", // 默认为 http,如果 devServer 开启了 https 则需要显式指定 https
    },
    // 错误处理
    (err, req, socket, head) => {
      if (err) {
        console.error("proxy websocket upgrade error", err);
        socket.destroy();
      }
    }
  );
});

接下来我们需要在某个端口启动 http2 服务器,但这个端口尽量不要写死,因为可能已经被其它程序占用,我们需要获取到一个可用的端口号,例如使用 find-free-port 来查找可用端口:

js 复制代码
const fp = require("find-free-port");

const http2Port = await fp(process.env.http2_port || 12345); // 从参数指定的端口开始查找,如果端口被占用则递增,直到找到一个可用端口

http2Server.listen(http2Port); // 启动 http2 服务器

我们可以先将上面启动 http2 服务的逻辑封装成函数 startProxyServer(targetPort),其中 targetPort 通过参数传入。接下来就是如何获取 targetPort

targetPort 就是 devServer 启动的端口,由于可能出现默认端口被占用导致实际端口产生变化的情况,我们最好也不要把 targetPort 写死,还是通过动态获取的方式来获取它比较好。

在 webpack devServer 中有一个回调函数 onListening

js 复制代码
const isDev = process.env.NODE_ENV === "development";

module.exports = {
  devServer: {
    onListening(devServer) {
      const devServerPort = devServer.server.address().port;
      // 一定要 dev 环境才启动,否则可能会因为启动的服务让进程无法结束,从而导致 ci 卡住
      if (isDev) {
        startProxyServer(devServerPort); // 启动 http2 服务
      }
    },
  },
};

到目前为止,你已经可以在开发环境使用 http2 了。如果端口 12345 且没有被占用的话,那访问链接就是 https://localhost:12345,注意是 https !!!

其它配置调整

调整 HMR 的 ws 协议和端口

webpack 并不知道我们是使用代理的方式启动了 http2,生成的资源中的端口还是 devServer 的端口,例如 HMR 使用的 ws 的协议和端口都还是 devServer 的协议和端口,如果 devServer 是 http 的话,在打开的 https 页面中可能会被认为是不安全的,从而被浏览器阻止导致 HMR 失效,为了避免被浏览器阻止掉,我们需要调整成以我们代理服务器的协议和端口为准:

js 复制代码
module.exports = {
  devServer: {
    client: {
      webSocketURL: "auto://0.0.0.0:0/ws", // 表示协议和端口都以打开的网页为准
    },
  },
};

至此,你的本地开发也已经可以支持 http2 了:

写在最后

devServer 在每次编译完后会提示当前的服务地址:

我们也可以写一个简单的 webpack plugin 来给与提示(毕竟如果同时开发的项目太多可能会混淆端口):

js 复制代码
const chalk = require("chalk");

module.exports = {
  plugins: [
    {
      apply(compiler) {
        compiler.hooks.done.tap("done", async (stats) => {
          if (stats.compilation.errors.length === 0) {
            await Promise.resolve(); // 延迟打印日志,将消息放在其它编译消息之后
            console.log(
              chalk.green.bold(`Http2 running at: https://localhost:${HTTP2_PORT}\n`) // 使用 chalk 美化一下输出颜色,使得提醒更加醒目
            );
          }
        });
      },
    },
  ],
};

谨以此文给与新手朋友们一些参考思路。

startProxyServer 完整代码

js 复制代码
const http2 = require("http2");
const proxy = require("http2-proxy");
const fp = require("find-free-port");
const finalhandler = require("finalhandler");

async function startProxyServer(targetPort) {
  const http2Server = http2.createSecureServer({
    allowHTTP1: true, // 必须提供,否则 websocket 无法被代理
    key: fs.readFileSync("./ssl/key.pem"),
    cert: fs.readFileSync("./ssl/key-cert.pem"),
  });

  // 代理 http 请求
  http2Server.on("request", (req, res) => {
    proxy.web(
      req,
      res,
      {
        hostname: "localhost",
        port: targetPort, // devServer 的端口
      // protocol: "https", // 默认为 http,如果 devServer 开启了 https 则需要显式指定 https
      },
      // 错误处理
      (err, req, res) => {
        if (err) {
          console.error("proxy error", err);
          finalhandler(req, res)(err); // 为了简化逻辑,我们使用 finalhandler 去处理错误的情况
        }
      }
    );
  });

  // 代理 ws upgrade 请求
  http2Server.on("upgrade", (req, socket, head) => {
    proxy.ws(
      req,
      socket,
      head,
      {
        hostname: "localhost",
        port: targetPort, // devServer 的端口
      // protocol: "https", // 默认为 http,如果 devServer 开启了 https 则需要显式指定 https
      },
      // 错误处理
      (err, req, socket, head) => {
        if (err) {
          console.error("proxy websocket upgrade error", err);
          socket.destroy();
        }
      }
    );
  });

  const http2Port = await fp(process.env.http2_port || 12345); // 从参数指定的端口开始查找,如果端口被占用则递增,直到找到一个可用端口

  http2Server.listen(http2Port); // 启动 http2 服务器
}
相关推荐
前端小小王15 分钟前
React Hooks
前端·javascript·react.js
迷途小码农零零发25 分钟前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀1 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪1 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef3 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6413 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻4 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云4 小时前
npm淘宝镜像
前端·npm·node.js
dz88i84 小时前
修改npm镜像源
前端·npm·node.js
Jiaberrr4 小时前
解锁 GitBook 的奥秘:从入门到精通之旅
前端·gitbook