2024年1月

哈喽大家好,我是咸鱼

IPv4(Internet Protocol version 4)是互联网上使用最广泛的网络层协议之一,于1981年在 RFC 791 中发布,它定义了 32 位的IP地址结构和基本的协议操作。

由于 IPv4 使用 32 位的地址,因此只有四十亿(4,294,967,296,2^32)个地址。

这就导致随着地址不断被分配,IPv4 地址开始面临枯竭问题:

  • 2011 年,互联网分配与分配机构(IANA)正式宣布IPv4地址用尽
  • 由于 IPv4 地址短缺,一些机构开始收费分配IPv4地址,推动更多组织采用 IPv6

IPv 4 枯竭,升级 IPv6 任重道远。

今天我们来看一篇文章,看看向 IPv6 迁移会遇到什么样的挑战以及各个企业会拿出什么样的策略。

原文链接:
https://medium.com/stackademic/global-ipv4-depletion-charges-begin-next-month-011c914c5737

IPv4 即将迎来支付时代:

  • 去年 7 月,亚马逊云科技宣布,从 2024 年 2 月 1 日起,所有公有 IPv4 地址将按每小时 0.005 美元(约每月 4 美元)的费率收费,无论它们是否与服务绑定。
  • 基于容器的部署平台 Fly.io 最近也更新了其社区公告,表示 2 月 1 日之后,将对每个专用 IPv4 每月收取约 2 美元的费用。
  • 开源数据处理服务平台 Supabase 计划推出 IPv4 的付费附加服务,月费为 4 美元。

随着时间流逝,围绕 "IPv4 收费,迁移到 IPv6" 的讨论越来越激烈。最近,开源数据处理服务平台 Supabase 的首席执行官兼联合创始人 Paul Copplestone 呼吁大家:“做好准备,IPv6 即将到来。”

然而,由于 IPv4 和 IPv6 报文头之间的显著差异,这两个协议不能互操作。升级到 IPv6 的道路也面临着多重挑战。

一些开发人员已经尝试使用 IPv6,但得出的结论是:IPv6 是一个“灾难”。虽然我们可能在未来会解决这些困难,但目前的准备仍然不够。

全球 IPv4 地址耗尽,IPv6 升级成为焦点

据负责英国、欧洲、中东和中亚部分地区互联网资源分配的 Réseaux IP Européens Network Coordination Centre (RIPE NCC)宣布:随着互联网规模的不断扩大,设备数量的快速增加导致最后一个 IPv4 地址空间储备池于 2019年11月25日15:35 耗尽,全球 42 亿个 IPv4 地址已经分配完毕。

公网 IPv4 地址耗尽后,我们使用的公网 IPv4 地址主要靠回收和释放未被用的地址范围来获取。这些地址有的可能是来自解散的公司,有的可能是那些搬迁到 IPv6 后不再需要的。

获取日益紧缺的 IPv4 地址橙味一个复杂的过程,这导致了成本自然增加。亚马逊(AWS)此前透露,过去五年来,由于获取难度增加,单个公网 IPv4 地址的获取成本增加了300%以上。

所以大公司不得不采取收费政策,希望大家更有效地利用公 共IPv4 地址,同时促进 IPv6 在行业内的采用。

Paul Copplestone 表示:“虽然 AWS 每月收费约 4 美元,对个人来说不算昂贵,但 AWS 是许多基础设施公司的基础设施层,比如 Supabase。我们需要为每个 Postgres 数据库提供完整的 EC2 实例,这将使我们的 AWS 账单增加数百万美元。”

一些分析人士认为,对于规模较大的 AWS 客户来说,这些费用可以完全忽略,可能在他们的账单上不值一提。然而,对于很多中小企业和初创公司来说,这些费用很容易就占到他们账单的 10-30%。

三个选择

当涉及到处理这些成本时,公司有哪些选择来最小化成本呢 ?

对此,Paul Copplestone 分享了 AWS 基础设施公司的三种选择:

  • 将成本转移给客户:类似于 AWS 和 Fly.io 那样,在租用或购买 IPv4 地址时,制定新的定价政策,让客户为此付费。对于单个 IPv4 地址,AWS 的新费用为每年 43.80 美元(0.05 * 24 小时 * 365 天)
  • 提供替代解决方案(如代理):企业可以为客户提供 IPv4 代理服务,通过代理将 IPv6 流量映射到 IPv4 流量。这种方法允许 IPv6 设备访问 IPv4 资源,同时减少对 IPv4 地址的直接需求;或者通过 NAT 技术来优化 IPv4 地址的利用率:共享一个IPv4地址,使用不同的端口来区分不同的业务或用户。
  • 只提供 IPv6:希望大家能跟上它。

ipv6 面临的挑战

长期来看,第三种选择 ——“只提供 IPv6” 是最经济有效的解决方案。作为 IPv4 的继任者,IPv6 对移动设备的支持更好,地址分配更灵活,报头结构更简化,安全性更高。

IPv6 的地址空间非常大,大约有 3.4 x 10^38 个地址,能够满足未来互联网连接设备不断增长的需求。。可以说 “IPv6 为每一粒沙子提供了一个唯一的地址”。

image
IPv6 的出现无疑是一件好事,然而根据谷歌的统计,截至 2024年1月15日,IPv6 引入十多年来,互联网用户使用 IPv6 的占比还没有达到 50%,仅仅是 41.23%。

image
关于这背后的原因,Paul Copplestone 认为有两点:

  • 互联网服务提供商 (ISP) 支持不足
  • 缺乏相关工具支持

ISP 支持不足

问题来了:你的互联网服务提供商(ISP)是否支持 IPv6?

Paul Copplestone认为,全球 IPv6 面临的最大挑战在于 ISP 的支持。简单来说,当输入网站的域名时,它会被转换为 IPv4 地址:

example.com → 93.184.216.34

如果采用 IPV6,这些域名最终将被解析成:

example.com → 2607:f8b0:4006:819::200e

一旦 ISP 收到此地址,它就负责将所有流量路由到正确的目的地。

不幸的是,许多 ISP 没有为域名解析成 IPv6 地址做好充分的准备。它们需要更新的交换机、更新的软件以及与 IPv4 的互操作性——这些都会产生成本,而在过去十年中,这些成本似乎并不合理。

如果你的 ISP 不支持 IPv6,则当域名/服务器开始解析为 IPv6 而不是 IPv4 地址时,可能会遇到以下问题:

  • 如果在 AWS 中设置了 Web 服务器,则无法通过 SSH 连接到该服务器。
  • 如果直接从本地计算机连接到 Supabase 数据库,则需要使用连接池,该连接池将解析为 IPv4(提供商需要为这些 IPv4 地址付费)
  • 如果通过 Vercel 连接到任何 AWS 服务器,并且没有为服务器设置 IPv4 地址,则会连接失败。

缺乏工具支持

除了上面 ISP 支持不足的原因之外,许多开发工具还没有针对 IPv6 进行配置兼容。Paul Copplestone 以他的开源 Firebase 替代品 Supabase 为例解释说,为了让他们的数据团队的工具与 IPv6 兼容,他们需要进行以下更改:

  • 向 VPC 网络添加 IPv6 支持。
  • 向 Airflow VM 添加 IPv6 支持。
  • 向 Docker 和 Compose 添加 IPv6 支持。

虽然这些步骤看起来很简单,但实现它们实际上是相当具有挑战性的。

以配置 Docker 的步骤为例:

1、更新
/etc/docker/daemon.json

"ipv6": true,
"fixed-cidr-v6": "fd00:ffff::/80",
"ip6tables": true,
"experimental": true

2、重启 Docker 服务

systemctl restart docker

3、创建临时 IPv6 网络并测试

docker network create --ipv6 --subnet fd00:ffff::/80 ip6net
docker run --rm -it --network ip6net busybox ping6 google.com -c3

4、检查 IPv6 iptables 配置

ip6tables -L

5、将 IPv6 网络配置添加到
Docker Compose
文件

# enable IPv6 to default network
networks:
  default:
    enable_ipv6: true
    ipam:
      config:
        - subnet: fd00:c16a:601e::/80
          gateway: fd00:c16a:601e::1

6、检查是否正常运行

docker exec -it "airflow_airflow-worker_1" bash
curl -6 https://ifconfig.co/ip

可以看到,这些配置还是很繁杂的,尤其是对于 docker 这样无处不在的工具。

向 IPv6 迈进,挑战重重

DevOps 工程师 Mathew Duggan 吐槽迁移到 IPv6 困难重重:“几乎没有什么是开箱操作的。主要依赖项会立即停止工作,并且解决方法不足以满足生产需求。我们团队的 IPv6 迁移过程相当艰难,主要是因为很少有人承担这项工作,我们已经很多年没有做这项工作了,现在正在付出代价。”

Mathew Duggan 尝试使用 CDN 将他的博客 (
https://matduggan.com/ipv6-is-a-disaster-and-its-our-fault/)
迁移到 IPv6 以管理 IPv4 流量。

他说:“实际的设置很简单。我配置了一个 Debian 设备并选择了 IPv6。然后我得到了第一个惊喜:设备没有获取到 IPv6 地址,但收到了一个 64 位地址(18,446,744,073,709,551,616)。我的小型 ARM 服务器可以通过扩展,在所有公共地址上运行我曾工作过的每家公司所有网络基础设施。

然而,当他试图像普通服务器一样设置它时,问题出现了。

  • 无法 SSH 登录

因为他的工作或家庭的 ISP 不支持 IPV6,所以需要他手动设置,否则根本无法正常工作。因此,他必须先添加一个 IPv4 地址,通过 SSH 登录,然后设置 Cloudflared 来运行隧道(tunnel)。

但是 Cloudflare 系统本身不能处理转换。当他删除 IPv4 地址时,隧道意外崩溃了。因为 Cloudflared 默认使用 IPv4,如果想要支持 IPv6,要编辑 systemd 服务文件添加:
—-edge-ip-version 6
,这样隧道才能正常使用。

  • 无法使用 GitHub

当 Mathew Duggan 的服务器开始运行时,他尝试去执行一个服务器设置脚本,这个脚本会去 GitHub 获取安装文件,但是报错了。

他感到困惑,GitHub 确定支持 IPv6 吗?最后他意外发现 GitHub 不支持 IPv6

最后他使用了 TransIP Github 代理服务器,运行良好。但随后 Python 遇到了
urllib.error.URLError

“好吧,我放弃了。我猜 Debian 中的 Python 3 版本不喜欢 IPv6,但我现在不想排查了,“ Mathew Duggan 说。

  • 无法设置 Datadog

接下来,Mathew Duggan 想要设置 Datadog 来监控服务器,当他访问 Datadog、登录并开始工作时,系统立即崩溃。

他只是通过运行
curl -L https://s3.amazonaws.com/dd-agent/scripts/install_script_agent7.sh
进行设置,但是现在 S3 支持 IPv6,问题可能出在哪里?

经过故障排除后,他发现问题不在于 S3 或服务器,因为他可以使用 AWS 提供的 S3 连接测试而不会出现任何问题。后来,他通过
apt
手动修复了这个问题。

他开始意识到纯 IPv6 的使用没有未来。如果没有代理和技术补丁,那几乎没有什么东西能正常工作

后来,为了从 IPv6 访问 IPv4 资源,他选择了 NAT64 服务 (
https://nat64.net/)
作为支持。

不但如此,他还搜索了许多工具,发现其中大多数工具已经不能工作:如下面的 Dresel 链接无法工作;Trex 在测试中出现了问题;August Internet 彻底消失;大多数 Go5lab 测试设备离线;Tuxis 倒是可以工作,但在 2019 年推出之后似乎就没升级过。只有一个 Kasper Dupont 支持度还是可以的。

采用 IPv6 任重道远

虽然向 IPv6 过渡的时机已经到来,但大多数基础设施和软件仍然没有为这种变化做好准备。而且 IPv6 的培训和准备对数字专业人员来说将是一项重大挑战。

不但许多开发人员这么认为,来自 HN 上的网友也纷纷诉苦:

  • 我仍然在诅咒 IPv6 的设计者,因为他们没有使 IPv6 向后兼容 IPv4。IPv6 的设计无疑是优越的,但由于缺乏向后兼容性,过渡到 IPv6 是一个绝对的挑战。我知道设计师们认为这种转变只需要几年的时间,但它已经持续了近 30 年……我们还在原地踏步。
  • 除非 IPv6 地址成为一等公民,否则 IPv6 并不能真正解决地址耗尽的问题。只有当我们不再需要依赖 IPv4 地址时,才会发生这种情况。

如果不迁移到IPv6,继续使用IPv4,可能无法满足日益增长的需求,导致性能下降和业务不稳定。现在许多组织采用 NAT 技术来共享有限的 IPv4 地址,但是这会增加网络管理的复杂性,还可能使某些程序或服务的功能受限。

鉴于此,越来越多的组织开始加入到实施 IPv6 迁移的浪潮之中。

wmproxy

wmproxy
已用
Rust
实现
http/https
代理,
socks5
代理,
websocket
代理,反向代理, 静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透等,力争打造和nginx的性能。

项目地址

国内: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

静态文件服务器

静态文件服务器是一种用于提供静态文件(如HTML、CSS、JavaScript、图片等)的网络服务器。当客户端(如浏览器)请求这些文件时,静态文件服务器会直接从文件系统中获取文件并返回给客户端,而不需要经过任何处理或动态生成。

静态文件服务器的主要特点包括:

  1. 简单性:静态文件服务器不需要复杂的逻辑或数据库支持,只需要能够读取和发送文件即可。
  2. 高效性:由于不需要处理复杂的逻辑或动态生成内容,静态文件服务器通常能够更快地响应客户端请求。
  3. 可扩展性:静态文件服务器可以轻松地通过增加服务器数量或优化服务器配置来扩展其处理能力。

设计时注意要点

以下是此次设计时的两个注意要点:

  1. 断点续传支持:静态资源服务器通常支持断点续传。当用户下载大文件时,如果出现网络中断或其他原因导致下载中断,静态资源服务器可以记录中断位置,当用户重新请求下载时,可以恢复到中断的位置继续下载,提供更好的下载体验。

  2. 缓存和浏览器缓存支持:静态资源服务器可以通过设置合适的缓存策略,利用浏览器缓存来提高性能和减少网络流量。静态资源可以设置缓存过期时间,当浏览器再次请求相同资源时,可以直接从缓存中获取,减少了网络请求和传输时间。

启动文件服务器

对当前项目启动8080端口监听

wmproxy file-server -l :8080

缓存和浏览器缓存支持

浏览器缓存是一种机制,它将已访问过的资源的副本存储在浏览器中,以便在将来更快地加载相同的资源。可以提高网页性能、减轻服务器负担、节省网络带宽并提供更好的用户体验。在开发和优化网站时,合理地利用浏览器缓存可以显著提升网站的整体性能。

一个文件是否被修改过主要依靠以下两个属性:

  • Etag(Entity Tag)
    :


    • Etag是一个HTTP首部字段,用于验证浏览器缓存的组件与从服务器上获取的组件是否一致。
    • 它是一个由服务器生成的唯一标识符,通常基于文件内容或某些其他属性通过特定算法计算得出。
    • 当资源发生变化时,Etag值也会改变。
    • 客户端会发送一个包含If-None-Match头部的请求,其中包含之前缓存资源的Etag值。
  • Last-Modified
    :


    • Last-Modified也是一个HTTP首部字段,指定资源最后一次修改的时间。
    • 服务器在响应头中包含该字段,告诉浏览器该资源的最后修改时间。
    • 其时间粒度通常只到秒级别,不如Etag精确。
    • 客户端会发送一个包含If-Modified-Since头部的请求,其中包含之前缓存资源的最后修改时间。

通常文件服务器返回时会附带该两个参数由客户端协带进行是否读取缓存数据。

控制缓存策略:
控制过期时间主要由两种方式:

  • Expires
    :


    • Expires是一个
      HTTP 1.0
      的头部字段,用于指定资源过期的时间。
    • 它是一个日期时间值,告诉浏览器资源何时过期,过期后浏览器需要重新请求资源。
    • 使用Expires的一个缺点是它基于服务器的时间,如果服务器的时间不准确,缓存可能会出问题。
  • Cache-Control
    :


    • Cache-Control是一个
      HTTP 1.1
      的头部字段,提供了更细粒度的缓存控制。
    • 它可以包含多个指令,如public、private、no-cache、max-age等,用于控制资源在浏览器缓存中的存储方式和有效期。
    • Cache-Control的优先级高于Expires,当两者同时存在时,Cache-Control的设置会覆盖Expires。

由于Expires的客户端时间和服务端时间可能存在的不一致,此处我们服务器不做Expires的实现,如果配置过期
1d
之类,将转化成:
cache-control: max-age=86400
进行时间控制。

以下我们进行测试:
我们先对资源
wmproxy.md

curl.exe http://127.0.0.1:8082/wmproxy.md -i

HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
transfer-encoding: chunked
Server: wmproxy
cache-control: max-age=1000
Date: Tue, 23 Jan 2024 09:12:05 GMT
Last-Modified: Tue, 23 Jan 2024 09:11:30 GMT
etag: 65af82c2-11c1

从返回结果中我们可以得知缓存时间1000s,最后修改时间及etag的值。让我们添加IF-NONE-MATCH进行测试

curl.exe http://127.0.0.1:8082/wmproxy.md -i -H "If-Modified-Since: Tue, 23 Jan 2024 09:11:30 GMT"

HTTP/1.1 304 Not Modified
Server: wmproxy
cache-control: max-age=1000
Date: Tue, 23 Jan 2024 09:18:58 GMT
Last-Modified: Tue, 23 Jan 2024 09:11:30 GMT
etag: 65af82c2-11c1
content-length: 0

可以尝试添加最后修改时间的

curl.exe http://127.0.0.1:8082/wmproxy.md -i -H "IF-NONE-MATCH: 65af82c2-11c1"

HTTP/1.1 304 Not Modified
Server: wmproxy
cache-control: max-age=1000
Date: Tue, 23 Jan 2024 09:18:58 GMT
Last-Modified: Tue, 23 Jan 2024 09:11:30 GMT
etag: 65af82c2-11c1
content-length: 0

一样会进行缓存,通常浏览器会将两个值做为传参一起写入,文件发生变更,将会使缓存失败,重新返回200请求

curl.exe http://127.0.0.1:8082/wmproxy.md -i -H "IF-NONE-MATCH: 65af82c2-11c1"

HTTP/1.1 200 OK
...

断点续传的支持

断点续传也就是客户端可以指定传输范围进行传输,该标准定义在
RFC7233

HTTP 请求范围

HTTP 协议范围请求允许服务器只发送 HTTP 消息的一部分到客户端。范围请求在传送大的媒体文件,或者与文件下载的断点续传功能搭配使用时非常有用。

检测服务器端是否支持范围请求

假如在响应中存在
Accept-Ranges
首部(并且它的值不为"none"),那么表示该服务器支持范围请求。例如,你可以使用 cURL 发送一个 HEAD 请求来进行检测。

curl.exe -I http://127.0.0.1:8080/Cargo.toml
HTTP/1.1 200 OK
...
accept-ranges: bytes
content-length: 1565

在上面的响应中,
Accept-Ranges: bytes
表示界定范围的单位是
bytes
。这里
Content-Length
也是有效信息,因为它提供了要检索的文件的完整大小。
如果返回的
Accept-Ranges: none
则表示不支持,如果未返回则表示可能不支持范围请求。

从服务器端请求特定的范围

假如服务器支持范围请求的话,你可以使用
Range
首部来生成该类请求。该首部指示服务器应该返回文件的哪一或哪几部分。

单一范围

我们可以请求资源的某一部分。这次我们依然用 cURL 来进行测试。"-H" 选项可以在请求中追加一个首部行,在这个例子中,是用 Range 首部来请求图片文件的前 1024 个字节。

curl http://127.0.0.1:8080/Cargo.toml -i -H "Range: bytes=0-1023"

这样生成的请求如下:

GET /Cargo.toml HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: curl/8.0.1
Accept: */*
Range: bytes=0-1023

服务器端会返回状态码为 206 Partial Content 的响应:

HTTP/1.1 206 Partial Content
content-type: text/plain; charset=utf-8
transfer-encoding: chunked
Server: wmproxy
Date: Tue, 23 Jan 2024 07:59:20 +0000
Last-Modified: Tue, 23 Jan 2024 02:33:35 +0000
etag: 65af257f-61d
content-range: bytes 0-1023/1565
...
(binary content)

在这里,
Content-Length
首部现在用来表示先前请求范围的大小(而不是整个文件的大小)。
Content-Range
响应首部则表示这一部分内容在整个资源中所处的位置。

多范围查询

发起请求和单一范围类似,只是在请求的时候多个范围地址,如:

curl http://www.example.com -i -H "Range: bytes=0-50, 100-150"

返回内容为
Content-Type: multipart/byteranges boundary=THIS_STRING_SEPARATES
并在body中以该字符做分隔成多数据块,如。

HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=3d6b6a416f9b5
Content-Length: 282

--3d6b6a416f9b5
Content-Type: text/html
Content-Range: bytes 0-50/1270

<!doctype html>
<html>
<head>
    <title>Example Do
--3d6b6a416f9b5
Content-Type: text/html
Content-Range: bytes 100-150/1270

eta http-equiv="Content-type" content="text/html; c
--3d6b6a416f9b5--

请求多范围要针对解析body块,相对来说数据块请求比较割裂。在HTTP2中可以多流式请求范围或者用keep-alive同时发起多个请求,相对比较难与处理数据块,暂时不做实现。

条件式范围请求

当(中断之后)重新开始请求更多资源片段的时候,必须确保自从上一个片段被接收之后该资源没有进行过修改。

通过
If-Range
请求首部可以用来生成条件式范围请求:假如条件满足的话,条件请求就会生效,服务器会返回状态码为
206 Partial
的响应,以及相应的消息主体。假如条件未能得到满足,那么就会返回状态码为 200 OK 的响应,同时返回整个资源。该首部可以与 Last-Modified 验证器或者 ETag 一起使用,但是二者不能同时使用。

If-Range: Wed, 21 Oct 2015 07:28:00 GMT
或者
If-Range: 65af257f-61d

范围请求的响应

与范围请求相关的有三种状态:

  • 在请求成功的情况下,服务器会返回
    206 Partial Content
    状态码。
  • 在请求的范围越界的情况下(范围值超过了资源的大小),服务器会返回
    416 Requested Range Not Satisfiable
    (请求的范围无法满足)状态码。
  • 在不支持范围请求的情况下,服务器会返回
    200 OK
    状态码。

源码相关

  • 关于文件服务器的相关源码均在
    file_server

  • 关于时间格式由
    RFC2822
    控制,这里我们用的解析库为
    chrono

  • 关于etag,我们这里采用的与nginx一致的算法,文件最后修改时间16进制-文件长度16进制。例:
    ETag: 65af8536-11c2
    文件长度为:

10进制为->4546
转为16进制->11c2

文件最后修改时间:

标准日期格式->Tue, 23 Jan 2024 09:21:58 GMT
转为秒->1706001718
转为16进制->65af8536
pub fn calc_etag(data: &Metadata) -> String {
    let mut seconds = 0;
    let len = data.len();
    if let Ok(last) = data.modified() {
        if let Ok(n) = last.duration_since(SystemTime::UNIX_EPOCH) {
            seconds = n.as_secs();
        }
    }
    format!("{:x}-{:x}", seconds, len)
}
  • 关于中文目录,因为文件服务器是get请求,通常均带在path下,可能由于unicode的编码进行过转化,即
    问蒙服务框架
    ->
    %E9%97%AE%E8%92%99%E6%9C%8D%E5%8A%A1%E6%A1%86%E6%9E%B6
    会进行一次转码,我们在path中如果存在
    %
    的时候,尝试进行一次转码,如果成功取新的path值。
if path.contains("%") {
    if let Ok(p) = Url::url_decode(&path) {
        path = p;
    }
}
  • 断点续传(范围查询),通过在原有的基础上增加
    start_pos

    end_pos
    来表示文件的起始及结束点。
#[derive(Debug)]
struct InnerReceiver {
    receiver: Option<Receiver<(bool, Binary)>>,
    file: Option<Box<File>>,
    cache_buf: Vec<u8>,
    /// 数据包大小
    data_size: u64,
    /// 文件专用, 起始点
    start_pos: Option<u64>,
    /// 文件专用, 结束点
    end_pos: Option<u64>,
}

小结

本章中讲述了浏览器缓存的设计(ETAG, Last-Modified, Cache-Control, Expires)及断点续传(Accept-Ranges: bytes)的实现流程及相关的部分源码,希望可以让你更了解文件服务器内部的原理组成。

点击
[关注]

[在看]

[点赞]
是对作者最大的支持

前言

随着
Svelte

SolidJS
的流行,无虚拟DOM模式逐渐开始火了起来。
vue
也推出了无虚拟DOM模式的版本,就是我们今天要讲的
Vue Vapor

什么是
Vue Vapor

Vue Vapor
是一个无虚拟DOM模式版本的
vue
。这个仓库是2023年11月9日基于
vuejs/core
仓库(也就是vue3仓库)新建的,项目地址为
https://github.com/vuejs/core-vapor

Vue Vapor
目前还处于
实验阶段
,不过你可以通过官方提供的
Vue Vapor
演练场在线体验无虚拟DOM版本的
vue
。playground演练场的地址为:
https://vapor-repl.netlify.app

目前
Vue Vapor
只实现了
vue3
中的部分功能,
KeepAlive

Teleport
等还未实现。详情查看官方的TODO:
https://github.com/vuejs/core-vapor?tab=readme-ov-file#todo

如何使用
Vue Vapor

对于使用者来说无虚拟DOM模式版本的
vue
和目前的
vue3
用法是一样的,
vue
只是对内部实现进行了修改,对外暴露出的
API
还是一样的,当然关于虚拟DOM相关的API在
Vue Vapor
中没有了。

我们来看看一个demo,和目前
vue3
写法是一样的:

<script setup>
import { ref } from 'vue'
const msg = ref('Hello World!')
</script>

<template>
  <h1>{{ msg }}</h1>
  <input v-model="msg" />
</template>

我们再来看看运行效果:


*.vue
文件渲染到浏览器真实DOM的过程

我们先来看看目前版本的
vue
是如何从一个
*.vue
文件渲染到浏览器真实DOM?

目前的
vue
是通过
webpack
或者
vite
等工具先将
*.vue
文件编译成一个
js
文件,
js
文件中主要的就是
render
函数。然后执行
render
函数生成虚拟DOM,再调用浏览器的
DOM API
根据虚拟DOM生成真实DOM挂载到浏览器上。

我们再来看看
Vue Vapor
是如何从一个
*.vue
文件渲染到浏览器真实DOM?

也是一样的首先通过
webpack
或者
vite
等工具先将
*.vue
文件编译成一个
js
文件,
js
文件中主要的就是
render
函数。然后执行
render
函数后会直接调用浏览器的
DOM API
生成真实DOM挂载到浏览器上。其实就是跳过了虚拟DOM的过程。

没有虚拟DOM后,
Vue Vapor
如何实现响应式

我们先来看看demo编译后的js代码中的
render
函数,下面是我简化后的代码:

function render(_ctx) {
  _withDirectives(n2, [[_vModelText, () => _ctx.msg]])
  _on(n2, "update:modelValue", $event => ((_ctx.msg) = $event))

  _renderEffect(() => {
    _setText(n1, _ctx.msg)
  })
}

这里的
n2
变量为input输入框dom元素,n1变量为显示
msg
值的h1标签dom元素。明显可以看见
render
函数中没有
createElementVNode
等生成虚拟DOM的函数调用。

我们先来看看
_withDirectives
函数,
vue3

withDirectives
函数的功能为给虚拟DOM增加自定义指令。我们这里没有虚拟DOM,所以这里是给真实DOM(input输入框)增加
v-model
的指令。

我们再来看看下面
_on
函数,这里是让
input
输入框监听一个名为
update:modelValue
的事件,触发事件后会将上下文中的
msg
变量的值更新为输入框中的输入值。

我们最后来看看
_renderEffect
函数,看名字你可能已经猜出来了。这个函数和
vue3
中的
watchEffect
比较相似,会立即运行一个函数。并且追踪函数中用的的依赖,这里的依赖是
msg
变量。当依赖的值变化时会再次执行这个函数。

这里的
_setText(n1, _ctx.msg)
,实际就是执行了
n1.textContent = _ctx.msg

textContent
属性表示一个节点及其后代的文本内容,也可能通过给它赋值的方式删除它的所有子节点,并替换为一个具有给定值的文本节点,和
innerText
功能比较相似。


msg
变量的值变了后就会执行回调函数,在回调函数中会执行
_setText
函数。
_setText
函数会通过更新h1标签的
textContent
属性让h1标签始终显示
msg
变量最新的值,从而实现响应式。

总结

这篇文章介绍了
Vue Vapor
,没有虚拟DOM版本的
vue
。对于普通开发者来说
Vue Vapor
其实和目前的
vue3
没有什么区别,前提是不涉及虚拟DOM。只是
vue
在内部实现上去掉了虚拟DOM,改为直接操作真实DOM。

如果我的文章对你有点帮助,欢迎关注公众号:【欧阳码农】,文章在公众号首发。你的支持就是我创作的最大动力,感谢感谢!

相信大家都很喜欢用grep指令,查一下项目中有没有出错的,然后通过logid搜索相关出错的日志和一些关键参数,但是在多行日志的情况下就很难处理了,比如okhttp拦截器中分别打印了url,param和response,然后你想统计一下有多少成功的,有多少失败的,发现就比较难了。

解决这个问题的方法,大多数选择在打印日志的时候做处理,把所需要的日志打印到一个行上,但是可能body中的json有换行,或合并后的日志过长,对阅读性增加了更多挑战。能解决但不优雅!今天给大家推荐一个新的方法,ggrep指令!

ggrep是group grep的缩写,意思是将数据分组匹配。然后可以通过多个表达式匹配指定的日志数据。

ggrep指令安装:
gitee:

/bin/bash -c "$(curl -fsSL https://gitee.com/bobby96333/ggrep/raw/main/install.sh)"

or github

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/lingdor/ggrep/HEAD/install.sh)"

or 源码安装

git clone https://github.com/lingdor/ggrep.git
go build ./
sudo cp ggrep /usr/local/bin/

CASE1: 对okhttp的多行数据url,param和response过滤输出:

ggrep --smart-logid --grep 'OkHttpUtil print: --> POST' --grep 'OkHttpUtil print: {' --grep 'OkHttpUtil print: {"return_code' --orderly-match --color always *.info |head

CASE2 对多行日志进行并行,然后可以通过awk,grep,wc等脚本进行二次处理

ggrep --smart-logid --grep 'OkHttpUtil print: --> POST' --grep 'OkHttpUtil print: {' --grep 'OkHttpUtil print: {"return_code' --orderly-match --merge-lines --color always *.info |head

CASE3 将整组数据打印:

ggrep --smart-logid -grep 'OkHttpUtil print: --> POST' --print-group *.info |head -n 100

CASE4 并行处理

./ggrep --smart-logid --grep 'LOG1' --grep 'LOG2' --full-match --parallel-count 0 --color always ./demo/demo.log

CASE5 多条件
and
匹配

ggrep 
./ggrep --smart-logid --grep 'LOG1' --grep 'LOG2' --full-match --color always ./demo/demo.log

输入:

20240119 LogId[666666] LOG1
20240119 LogId[666666] LOG2
20240119 LogId[666666] LOG3
20240119 LogId[666666] LOG2
50240119 LogId[222] LOG1

输出:

20240119 LogId[666666] LOG1
20240119 LogId[666666] LOG2

使用命令行方式搭建uni-app + Vue3 + Typescript + Pinia + Vite + Tailwind CSS + uv-ui开发脚手架

项目代码以上传至码云,项目地址:
gitee.com/breezefaith…

目录

一、前言

近日心血来潮想做一个开源项目,目标是做一款可以适配多端、功能完备的模板工程,包含后台管理系统和前台系统,开发者基于此项目进行裁剪和扩展来完成自己的功能开发。但前台系统花样繁多,可能是PC端浏览器,可能是微信网页,微信小程序,安卓,鸿蒙……因此笔者只能从个人的开发需求出发,搭建了原生vue3版、原生微信小程序版、原生React版和uniapp版的前台系统模板。

创建uniapp项目可以使用官方提供的IDE工具HBuilderX可视化的创建,但项目结构与普通的vue项目有所不同;而使用命令行方式创建的项目,是传统的node项目结构。工程代码在src目录下,编译器在项目下,编译结果在dist目录下。这样的项目结构更符合笔者的开发习惯。

本文内容主要就是来介绍使用命令行方式搭建uniapp版的前台系统模板工程的完整过程。

二、脚手架技术栈简介

2.1 uni-app

uni-app 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)、快应用等多个平台。

uniapp官方文档:
uniapp.dcloud.net.cn/

2.2 vue3

Vue 3 是一个流行的开源JavaScript框架,用于构建用户界面和单页面应用。Vue 3与Vue 2的主要区别包括:

  • 构建:Vue 3使用monorepo架构,更容易管理和维护。
  • API:Vue 3引入了新的组合式API,它提供了更灵活的代码组织方式。
  • 性能:Vue 3提供了更好的性能,包括更快的渲染速度和更小的打包大小。
  • TypeScript:Vue 3提供了更好的TypeScript支持。

Vue3官方文档:
cn.vuejs.org/

2.3 TypeScript

TypeScript是由 Microsoft 开发并维护的开源编程语言,它是在 JavaScript的基础上增加了静态类型、类和接口等特性,从而将其提升为一个功能更为丰富的开发平台。TypeScript 通过编译器将 TypeScript 代码转换为 JavaScript 代码,确保这些代码能够在支持 JavaScript 的各种设备和平台上运行。这种设计允许开发者利用 TypeScript 的静态类型检查以及类、接口和其他 OOP 功能来构建大型、复杂的 Web 应用,同时享受 JavaScript 本身的高灵活性和易于使用的特点。

Typescript官网文档:
www.typescriptlang.org/

2.4 Pinia

Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。 如果您熟悉 Composition API,您可能会认为您已经可以通过一个简单的 export const state = reactive({}). 这对于单页应用程序来说是正确的,但如果它是服务器端呈现的,会使您的应用程序暴露于安全漏洞。 但即使在小型单页应用程序中,您也可以从使用 Pinia 中获得很多好处:

  • dev-tools 支持
    • 跟踪动作、突变的时间线
    • Store 出现在使用它们的组件中
    • time travel 和 更容易的调试
  • 热模块更换
    • 在不重新加载页面的情况下修改您的 Store
    • 在开发时保持任何现有状态
  • 插件:使用插件扩展 Pinia 功能
  • 为 JS 用户提供适当的 TypeScript 支持或 autocompletion
  • 服务器端渲染支持

Pinia 目前也已经是 vue 官方正式的状态库。适用于 vue2 和 vue3。可以简单的理解成 Pinia 就是 Vuex5。也就是说, Vue3 项目,建议使用Pinia。

Pinia官方文档:
pinia.web3doc.top/

2.5 Tailwind CSS

Tailwind是一个原子类方式命名的CSS工具集。Tailwind CSS 是一个功能类优先的 CSS 框架,它集成了诸如 flex, pt-4, text-center 和 rotate-90 这样的的类,它们能直接在脚本标记语言中组合起来,构建出任何设计。它将所有的css属性全部封装成语义化的类,比如你想要一个float:left,它已经帮你封装好了,你直接使用一个float-left就可以。需要一个宽度为12像素,只需要写w-3就可以。

使用tailwindcss极大的减少代码量,能轻松完成响应式设计,不需要再写单独的css,也不需要再为class取个什么名字而苦恼。

tailwindcss官方文档:
tailwindcss.com/

2.6 uv-ui

说到uv-ui,就不得不先从uni-ui和uView说起。uni-ui、uView和uv-ui都是uni-app生态下专用的组件库。

  • uni-ui 是DCloud官方提供的跨端ui库,它是基于vue组件的、flex布局的、无dom的跨全端ui框架。

    但此组件库组件相对较少,设计风格略显滞后,笔者本人并不是很喜欢。

    uni-ui官方文档:
    uniapp.dcloud.net.cn/component/u…

  • uView从2018年初发布以来,是uni-app生态最优秀的UI框架,全面的组件和便捷的工具会让您信手拈来,如鱼得水。uView2.0,全面兼容了uni-app生态下的nvue开发模式,对1.x进行了架构重构,细节和性能都有极大提升。

    但uView是基于vue2实现,在vue3框架下使用会存在诸多问题。

    uView官方文档:
    uviewui.com/

  • uv-ui是兼容vue3+2、nvue、app、h5、多端小程序的uni-app生态框架,大部分组件基于uView2.x,在经过改进后全面支持vue3,部分组件做了进一步的优化,支持单独导入,方便开发者选择导入需要的组件。

    uv-ui官方文档:
    www.uvui.cn/

结合本项目的实际情况,笔者最终选择uv-ui作为项目的ui库。

2.7 vite

Vite是Vue的作者尤雨溪开发的Web开发构建工具,它是一个基于浏览器原生ES模块导入的开发服务器,在开发环境下,利用浏览器去解析import,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随启随用。同时不仅对Vue文件提供了支持,还支持热更新,而且热更新的速度不会随着模块增多而变慢。Vue3默认使用Vite作为项目脚手架工具。

Vite官方文档:
vitejs.cn/

三、详细步骤

3.1 Node.js安装

Vite需要 Node.js 版本 14.18+,16或更高版本。

Tailwind CSS 需要 Node.js 12.13.0 或更高版本。

可使用
node -v
命令查看当前node版本,如果不符合要求请先升级Nodejs。

3.2 创建以 typescript 开发的uniapp工程

npx degit dcloudio/uni-preset-vue#vite-ts uniapp-vue3-ts-scaffold

项目创建成功后执行以下命令安装npm依赖。

npm install --registry=https://registry.npmmirror.com 
或
yarn install
或
pnpm install

依赖安装完成后,执行以下命令可运行代码。

npm run dev:h5
或
npm run dev:mp-weixin

项目创建过程中遇到问题可参考官方文档
uniapp.dcloud.net.cn/quickstart-…

3.3 集成Pinia

3.3.1 安装pinia

npm install --registry=https://registry.npmmirror.com pinia@2.0.33
或
yarn add pinia@2.0.33
或
pnpm install pinia@2.0.33

3.3.2 修改main.ts

将src/main.ts修改为以下内容:

import { createSSRApp } from "vue";
import App from "./App.vue";
import * as Pinia from 'pinia';

export function createApp() {
const app = createSSRApp(App);
app.use(Pinia.createPinia());

return {
app,
Pinia, // 此处必须将 Pinia 返回
};
}

3.3.3 创建一个store

// stores/counter.ts
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
state: () => {
return { count: 0 };
},
// 也可以这样定义
// state: () => ({ count: 0 })
actions: {
increment() {
this.count++;
},
},
});

3.3.4 在组件中使用store

以下代码template中使用的view、image、text元素是uniapp内置元素,因为uniapp要兼容浏览器、小程序和app等多种环境,不建议在uniapp中直接使用浏览器所支持的div、span等元素,也不建议在uniapp项目中使用window、document、navigator等只有浏览器支持的API。

<template>
  <view>
    <!-- 直接从 store 中访问 state -->
    <text>Current Count: {{ counter.count }}</text>
  </view>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore();
counter.count++
// 自动补全!
counter.$patch({ count: counter.count + 1 })
// 或使用 action 代替
counter.increment()
</script>

<style>
</style>

3.4 集成Tailwind CSS

Tailwind CSS 需要 Node.js 12.13.0 或更高版本。对于大多数实际项目,建议将 Tailwind 作为 PostCSS 插件安装,本文使用的也是该方式。

3.4.1 安装postcss、sass、autoprefixer、tailwindcss和@uni-helper/vite-plugin-uni-tailwind以及相关依赖

  • Sass 是一款强化 CSS 的辅助工具,它在 CSS 语法的基础上增加了变量 (variables)、嵌套 (nested rules)、混合 (mixins)、导入 (inline imports) 等高级功能,这些拓展令 CSS 更加强大与优雅。使用 Sass 以及 Sass 的样式库(如 Compass)有助于更好地组织管理样式文件,以及更高效地开发项目。
  • autoprefixer是一款自动管理浏览器前缀的插件,它可以解析CSS文件并且添加浏览器前缀到CSS内容里,使用Can I Use(caniuse网站)的数据来决定哪些前缀是需要的。把autoprefixe添加到资源构建工具(例如Grunt)后,可以完全忘记有关CSS前缀的东西,只需按照最新的W3C规范来正常书写CSS即可。如果项目需要支持旧版浏览器,可修改browsers参数设置 。
  • @uni-helper/vite-plugin-uni-tailwind支持在 uni-app 中使用 TailwindCSS 3 原有语法开发小程序。支持 vite v2,v3 和 v4,要求 node >= 14.18。使用这个插件后无需禁用 preflight,也无需调整原有语法(如 .w-[200.5rpx] 等),你只需要正常书写类名,该插件会替你处理剩下的事情。

执行以下命令安装依赖:

npm install --registry=https://registry.npmmirror.com --save-dev @uni-helper/vite-plugin-uni-tailwind autoprefixer postcss postcss-comment postcss-html postcss-import postcss-scss sass sass-loader tailwindcss 
或
yarn add --save-dev @uni-helper/vite-plugin-uni-tailwind autoprefixer postcss postcss-comment postcss-html postcss-import postcss-scss sass sass-loader tailwindcss
或
pnpm install --save-dev @uni-helper/vite-plugin-uni-tailwind autoprefixer postcss postcss-comment postcss-html postcss-import postcss-scss sass sass-loader tailwindcss

3.4.2 创建配置文件postcss.config.js和tailwind.config.js

3.4.2.1 创建配置文件

使用命令行可以自动创建postcss.config.js和tailwind.config.js配置文件,也可以手动创建。

npx tailwindcss init -p
3.4.2.2 修改tailwind.config.js
/** @type {import('tailwindcss').Config} */

module.exports = {
darkMode: "class",
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
// 兼容小程序,将 : 替换成
separator: '
',
theme: {
// 兼容小程序,将默认配置里带 .和/ 清除
},
variants: {},
plugins: [],
corePlugins: {
// 兼容小程序,将带有 * 选择器的插件禁用
preflight: false,
space: false,
divideColor: false,
divideOpacity: false,
divideStyle: false,
divideWidth: false
}
};

3.4.2.3 修改postcss.config.js

在postcss配置中主动添加tailwindcss配置文件。

const path = require("path");
const comment = require("postcss-comment");
const tailwindcss = require("tailwindcss");
const autoprefixer = require("autoprefixer");
const postcssImport = require("postcss-import");

module.exports = {
parser: comment,
plugins: [
postcssImport({
resolve(id, basedir, importOptions) {
if (id.startsWith("~@/")) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(3));
} else if (id.startsWith("@/")) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(2));
} else if (id.startsWith("/") && !id.startsWith("//")) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(1));
}
return id;
},
}),
tailwindcss({
config: "./tailwind.config.js",
}),
autoprefixer({
remove: true,
}),
],
};

3.4.3 修改vite.config.js

在vite配置中主动添加potcss配置文件。

import { defineConfig } from "vite";
import postcssConfig from './postcss.config'
import uni from "@dcloudio/vite-plugin-uni";
import uvtw from '@uni-helper/vite-plugin-uni-tailwind'

// https://vitejs.dev/config/
export default defineConfig({
css: {
postcss: postcssConfig,
},
plugins: [
uni(),
uvtw(),
],
});

3.4.4 创建并引入tailwind.css

3.4.4.1 创建tailwind.css

在src目录下创建styles目录,在styles目录下创建tailwind.css。

image.png

tailwind.css文件内容如下:

@tailwind base;
@tailwind components;
@tailwind utilities;
3.4.4.2 App.vue中引入tailwind.css

配置完成后需要引入tailwindcss,注意
不要在main.ts中引入
,这会导致APP端不生效,需要在App.vue中引入,这一点上与原生Vue有所区别。 修改src/App.vue内容如下:

<script setup lang="ts">import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
onLaunch(() => {
console.log("App Launch");
});
onShow(() => {
console.log("App Show");
});
onHide(() => {
console.log("App Hide");
});
</script> <style>@import url('styles/tailwind.css');</style>

3.4.5 在组件中使用tailwindcss

<template>
	<view class="w-full h-[100px] bg-red flex justify-center items-center">
    <text>Hello Tailwind CSS</text>
	</view>
</template>

以上代码定义了一个宽度100%,高度100px,背景是红色,使用flex布局,垂直方向和水平方向内容都居中的区域,区域中有一个文本元素,显示Hello Tailwind CSS。

3.5 集成uv-ui

3.5.1 安装uv-ui

uv-ui官方推荐使用HbuilderX 方式进行安装。

插件市场下载入口:
ext.dcloud.net.cn/plugin?name…

使用 Hbuilder X 开发的用户,可以在 uni-app 插件市场通过 uni_modules 的形式进行安装,此安装方式可以方便后续在 uni_modules 对 uv-ui 进行一键升级,右键uni_modules目录或右键某组件,均可进行一键升级。

在uni-app插件市场右上角选择
下载插件并导入HBuilderX
,导入到对应的项目中即可。

使用该方式需要注册并登录Hbuilder X。下载安装uv-ui还需要观看广告,如果介意可使用其他方式安装。

image

安装成功后会在src目录下生成一个uni_modules目录,目录下内容都是uv-ui的组件,可以在代码中直接使用。

image

3.5.2 在组件中使用uv-ui

<template>
  <view>
  	<uv-button type="primary" text="uv-ui按钮"></uv-button>
	</view>
</template>

以上代码就是在view中添加了一个uv-ui组件库的按钮。

image

四、总结

在本文中介绍了基于命令行方式搭建一个uniapp工程脚手架的完整过程,希望本文能对您有所帮助。

项目代码以上传至码云,项目地址:
gitee.com/breezefaith…