整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

重构于 Vite:我如何做 SSG、静态资源发布以及自动化部署

2021 年元旦 Vite 发布 2.0 Beta 版就一直在关注 Vite 的动态,借着春节放假有时间,而且 Vue 3.0 和 Vite 2.0 都才大版本更新上线不久,预感后面会火,先开荒尝试一波,也当给以后工作上的业务先提前踩踩坑,对博客做了第三次重构,这一次把客户端和服务端都重新写了,由 PHP 的 LNMP 全家桶全部换成了前端侧的技术栈。

在经历了春节假期每天大概花 2 ~ 3 小时的投入,终于如期上线,第一个版本是发布于 2 月 14 日情人节 ,算是给自己的情人节礼物,当时是先部署在我闲置的香港服务器做了一波测试服调试,期间做了一些体验上的优化,然后 2 月 18 日 在休假的最后一天,部署到我正式服务器上了。

而且特别巧的是,这一天也是 Vite 2.0 正式版发布的日子:Vite 2.0 发布了 - 尤雨溪[1],同一天上线,就感觉特别美好,值得纪念。

LightHouse 的打分

运作流程

本次重构后,从开发到部署更新的运作流程图如下,日常只需要维护 GitHub 仓库的代码,其他的都是自动化完成。

博客运作流程

重构的价值

这次重构,并非是因为放假有空就找点事情做,而是带着几个目的来的:

  1. 提前开荒 Vite 2.0[2] ,为公司后续的业务提前踩坑,可以为团队进行技术选型提供帮助,因为之前我在做 JSSDK、Vue Plugin 的时候,已经开始脱离 Webpack,用 Rollup[3] 作为构建工具,而 Vite 正是基于 Rollup ,不仅构建速度非常快,而且也像 Webpack 一样提供了热更新,对于一线开发来说,体验上是非常好的,而且它还是 Vue 团队大力推广的新工具,这让我很有兴趣去研究它。
  2. 了解一下当前的一些新生的前端工具,比如 UI 框架方面之前一直停留在适合 B 端产品的 Ant-Design、 Vuetify 、 饿了么等等,说实话我做 B 端产品的时候才会用,面向 C 端因为有设计稿,我基本上都是手写样式,听闻新一代的 UI 框架 Tailwind CSS[4] 已经有一段时间了,虽然很奇怪为什么还会回到十年前一样用原子类的 class,“开倒车” 竟然还有 3 万多的 Star,让我非常的好奇到底为什么,结果一用,真香!没错,这次博客的样式,就是用的 Tailwind 。还有像 CSS 预处理器之前也一直停留在 Sass / Less / Stylus 三驾马车,这一次我抛弃了他们,用上了 PostCSS Language[5] + CSS Variable[6],也是真香!
  3. 借此机会多了解一下生产环境的服务端开发,公司业务几乎没有机会让自己实操服务端,所以大部分情况下都是在跑本机的 Server,很多场景是开发环境下遇不到的,要想进步,还是要多在生产环境磨练。
  4. 接触更多优秀的开源作品,比如代码语法高亮之前一直只知道 highlight.js[7] (因为 WordPress 的高亮插件就是用这个……),这一次我是用了 prism[8] ,更小巧,颗粒度更细,虽然目前还没有太多时间去定制代码高亮的配色,不过后面有时间想要处理,prism 会更加方便。
  5. 享受从 0 到 1 搭建脚手架的一个过程,目前这个版本算是实现一个简易版的 VuePress ,但是如果一直使用开箱即用的 VuePress ,很多时候并没有想去了解那些功能是怎么实现的,或者用哪些工具可以实现想要的功能(Btw: 我自从用了 Vue-CLI 之后就很久没自己配置 Webpack 了,直到 Rollup 的时候才算重新玩转了一次,这一次的 Vite 又是新的体验)

更多的更多,尽在未来,这肯定不是最后的一个版本,还有非常大的优化空间。

重构前的目标

其实去年就有想法要对博客做一波改版,但有几个原因导致一拖再拖,一个是因为业务比较忙(这个没办法,工作为重),一个是懒(主要是懒得去思考怎么设计,当然期间有在考虑一些不同的落地方案),还有一个主要的原因是当时 Vue 3.0 刚发布,我当时主要的精力放在踩坑体验 3.0,那段时间,大部分的时间和精力都放在撰写 Vue3.0 学习教程与实战案例[9] 上面去了,休息时间有限,能够闲下来的时间也只有下班回来和周末,除掉一些自己的事情外,留下来捣鼓新东西的时间并不算很多,只能先押后了。

相比 2018 年那次改版,当时只是单纯想重新弄一个干净的博客写东西,这一次的目标是比较明确了,就是从基于 PHP 的 WordPress,用前端的技术栈全部重构一遍,做一个纯前端的博客出来,当然还要保留 SEO ,就要求还要上 SSR(Server Side Render) 或者 SSG(Server Side Generation) 。

技术栈的选择

由于开工前已经是 2021 年了,因为有前面几个月玩 Vue 3.0 的基础打底,非常想用 3.0 来重构博客,加上元旦期间 Vite 2.0 Beta 版刚好发布(就很突然),注意力完全放在了 Vue3 和 Vite2 上面,非常想跑一下两者结合有多爽。

由于重构的最终目标还是要保持网站的 SEO 能力,所以肯定不能使用默认的 SPA 应用模式,要走服务端渲染,所以技术栈方面只需要考虑两条线:

基于 SSR

虽然在此之前考虑过几个方案,最开始是优先考虑做 SSR ,考虑过 Nuxt[10]Vapper[11] 等一些比较流行的开箱即用的 SSR 框架,但这些框架目前都还在弄 Vue 2.0,甚至部分框架看起来有点 “弃坑” 的趋势(背靠字节大厂的 Vapper 居然一年多没更新了 emm…… )。

加上搞 SSR 的话,服务器成本比较高,我的低配 ECS 可能 Hold 不住,好好玩一玩的话还要投点钱,想了想先算了,那么退而求次就是上 SSG 。

基于 SSG

玩转 SSG 也是有考虑过一些开箱即用的 SSG 框架,比如用的人最多的 Hexo[12],但我本身一直对 Hexo 不太感兴趣,而且似乎满大街随便找一个独立博客都是基于 Hexo 的,模板也千篇一律,缺乏个人特色。

好友小毅 @chawyehsu[13] 安利的 Saber[14],跑了个 demo 玩了一下,觉得真的蛮不错的,原本打算就直接用 Saber 的,不过目前 Saber 还是以 Vue 2.0 为主(听说下个版本会支持 3.0 ,不过也不知道什么时候会发布),由于内心实在是非常想用 Vue3 ,所以这个方案最终作为备选。

好吧,对 3.0 的执念,还让我想起两个 Vue 官方的作品:VuePress[15]和它的弟弟 VitePress[16],他们的新版本都是基于 Vue 3.0,而且已经可以用了,但一直以来我觉得它们都更适合用来写项目文档……

最终敲定

期间,Vite 官网在 2.0 Beta 版发布后,也新增了一 Part Server-Side Rendering | Vite[17] 指导如何实现 Vite SSR,我觉得可行。

加上有两个开源项目让我非常感兴趣,一个是 vite-ssr[18],一个是 vite-ssg[19],我也分别对他们跑了 demo ,很给力,So,最后决定基于这两个开源项目之一,选择自己搭脚手架……

最终用到的核心技术是:

Vite 2.0 —— 超快的构建工具

Vue 3.0 —— 更强大更灵活的 Vue

SSG —— 服务端渲染方案,利于 SEO 进行内容收录

PWA —— 构建离线应用

当然还要考虑的事情很多,每个环节还要用到不同的技术栈,具体我在下面逐个环节说明。

重构过程分析

下面来说说决定重构之后,整个思考的过程顺序,以及对每一个技术模块的技术栈选型原因分析吧,希望对有计划重构项目的朋友带来一些帮助。

构建工具

其实 Vue-CLI[20] 对 Vue 3.0 的支持已经非常好了,我的 Vue 3.0 教程[21] 也是基于 Vue-CLI 写的。

之所以选择 Vite,一方面是它的构建速度真的比 Webpack[22] 要快好多,另一方面是,自从 Vue 3.0 推出以来, Vue 官方团队就一直在投入精力优化和宣传 Vite,尽管 1.0 版本的功能和生态不如人意,但超快的构建速度已经体现了出来。

加上在我准备动手重构的时候官方刚好发布了 2.0 大更新,对比了 1.0 简直是质的飞跃,让我非常感兴趣,而且按照目前官方团队的态度,我觉得后面 Vite 会逐步代替 Vue-CLI ,提前了解,提前踩坑,对以后的工作也有帮助。

而且在生态方面,Vite 2.0 的各种支持都算很完善了,不得不说整个春节期间,Vue 团队的人都在忙着给 Vite 2.0 干活,我在春节提的 Issue,基本上 2 ~ 3 小时就能给我回应,解决问题速度非常快(大过年的耶!),重构过程感觉自己拥有一个强大的技术支持团队一样!

开荒虽然辛苦,但也有另一番乐趣!

服务端渲染

这是在选择合适的构建工具之后,应该考虑的第二件事。

个人博客之前一直选择用 WordPress ,一方面除了有 LNMP[23] 一键部署等快速搭建方案,和各种各样的模板之外,主要也是归功于 WP 对 SEO 的支持也是非常好,我这个博客的日常访问都是来自于搜索引擎。

单纯选择用 Vue 3.0 重新开发 SPA 应用肯定会丢失 SEO,所以才有了前面的 技术栈的选择[24],本次是通过 SSG 方案来落地服务端渲染。

项目架构规划

在开始动手之前,还要对网站架构做一波规划,盲目动手只能给自己挖坑,自己的博客虽然说内容不多,但也有一些东西要考虑:

  1. 对外展示的网站结构要保持不变,也就是原来的页面地址要尽量一样,避免用户访问不到原来的内容
  2. 对实在不能保持原样的 URL ,或者要废弃的页面,需要做 301 重定向
  3. 降低后续更新的构建和部署成本,尽量自动化,减少人工操作
  4. 数据需要无缝迁移,不能有丢失
  5. 减少服务器压力,把大部分资源消耗放在开源平台上(诸如 Github、jsdelivr CDN 等等)

当然其他的如移动端适配啥的也要看情况顾及,之前博客还有一个小程序版本,不过因为没人看(害,真的整整一年过去了,完全没人看小程序版本…),所以小程序的依赖保留没有在这次的重构兼容考虑范围里,重构完毕后我就直接把原来的服务停了,回头有空了再重新写一版接口给小程序用。

模板开发

基于 Vue 3.0 的项目,主要的模板肯定还是 Vue 文件,站点的主要结构、页面的布局、美化等等都是基于 .vue 文件,只需要按照原来的习惯,路由页面放在你的 src/views 文件夹下,组件模板放置于 src/components 下,就可以自动生成路由访问。

同时也加入了 .md 文件的支持,用于书写 Markdown 格式的内容,日常记录博客会更方便,并且像 VuePress 那样,同时支持在 Markdown 里嵌套 Vue,让博客的定制更加灵活。

整个项目的路由页面、组件结构,跟你平时开发 Vue 项目是完全一样的,无缝切换。

src 
├─components 
│ ├─Footer.vue 
│ └─Header.vue 
└─views 
│ ├─[page].vue
├─article 
│ └─rewrite-in-vite.md 
│ ├─about.md 
└─index.vue

在这里推荐几个非常方便的 Vite 插件:

vite-plugin-pages[25] :能够自动读取指定目录下的 Vue / Md 文件生成 Vue 路由,只需要管理好 views 文件夹的层级关系,无需再单独维护路由配置

vite-plugin-md[26] :一个能让 Markdown 文件像 Vue 组件一样导入使用的插件,它也基于 markdown-it,支持进行一系列 md 生态扩展

vite-plugin-components[27]:可以像 VuePress 一样,无需 import,会自动根据组件的标签名去 components 目录下寻找组件

基本上你只需要按照开发 Vue 项目的习惯去开发就可以了,如果有一些思路被卡住不知道怎么下手,可以参考我仓库源码。

样式处理器

有设计稿的时候我更喜欢借助 CSS 预处理器(目前常用 Stylus[28]),借助他们的变量 、 嵌套书写,以及 Mixin 、 Extend 等功能,避免写原生 CSS 带来的烦恼。

没有设计稿的时候,会用上 Ant Design[29] 等 UI 框架来帮我减少页面设计上的一些时间浪费,但这些框架通常更适合用在 B 端产品。

去年底在知乎刷到过一篇 如何评价 CSS 框架 TailwindCSS?[30] ,了解到一款全新的 CSS 框架 Tailwind CSS,乍一看很像是在开历史的倒车,回归原子类 className ,评价也是褒贬不一,自己光看文档的时候也是想着这啥玩意…

但是考虑到如果真的是开倒车,凭什么可以拿到 3 万的 Star,抱着试一下的心态在这次重构里面引入尝试,确实真香!

目前感受到的好处就是:

延续 CSS 的属性命名,你需要什么属性自己放,也就是自己必须有一定的 CSS 基础,特别是在多端适配方面,不用担心框架用久了自己不会写 CSS 的问题

比如,你要实现一个容器内完全居中,手写 CSS 是:

.container {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100px;
  height: 100px;
}

用 Tailwind CSS 的写法是:

<div class="flex justify-center items-center w-40 h-40"></div>

写法跟你在 VSCode 里自动补全代码时,敲入的命令非常接近,不像传统的 UI 框架一样,你写个标签就自动生成按钮,都不知道它是怎么写出来的(这也是我比较少想用 UI 框架的原因,我怕久了自己都不会写了),实际上,使用 Tailwind 之后,你还是在自己写 CSS, 只不过更方便了!

支持 CSS tree-shaking ,构建后的文件非常迷你

传统的 Atom CSS ,引入了就得整包引入,而 Tailwind 可以借助 PostCSS ,可以在最终项目构建的时候,抽离出我们用到的样式,用不到的会被直接扔掉。

我自己体验了一下,核心样式文件在配置 Purge 之前构建出来大概有 6M 多,Purge 之后只有 24K !

可以组合使用,类似于 CSS 预处理器的 Extend

比如,我要写一个通用的图片样式,让图片具备自适配不变型的效果,我只需要借助 @apply 像这样子:

.img {
  @apply w-full h-full object-cover;
}

编译出来就是我想要的效果:

.img {
  width: 100%;
  height: 100%;
  -o-object-fit: cover;
  object-fit: cover;
}

支持目前主流的暗黑模式,通过 dark:xxxxx 的前缀就可以轻松定制两款皮肤

点一下切换皮肤:

用了 Tailwind 之后,你几乎可以不用写 Sass / Stylus 了,那么问题来了:如何弥补 CSS 预处理器提供的一些功能?

借助 PostCSS Language[31]CSS Variable[32],可以轻松的书写像 CSS 预处理一样的嵌套和变量。

a {
  color: var(--fg-deeper);
  text-decoration: none;
  &:hover {
    border-bottom: 1px solid var(--fg-light);
  }
}

独立的文件使用 .postcss 或者 .pcss 作为文件后缀,在 Vue 组件里则使用 <style lang="postcss"></style> 来指定 PostCSS Language 。

当然,说的再多也不如亲手写一写,我之前在知乎也是看了好久始终不能决定用不用,之前赶业务也没时间,这一次也终于动手体验了一把,后悔,特别后悔,后悔怎么没有早点用!!!

SEO 优化

虽然前面的 服务端渲染[33] 帮我们解决了空 HTML 文档的问题,但要更好的进行 SEO 优化,还需要落实到具体的页面上去。

比如页面的 title 、 description 、 keyword 等等,这里我是用到了以下两个工具来帮我实现每个页面的 TKD 定制。

gray-matter[34]:支持对 .md 文件的 TKD 优化,你可以在 Markdown 文件的最前面加入这样的代码,即可实现对页面展示对应的 TKD 信息。

---
title: 这是页面的标题
desc: 这是页面的描述
keywords: 关键词1,关键词2,关键词3
---
下面是要书写的 Markdown 内容…

@vueuse/head[35]:可以让你在 .vue 文件里实现优化,在 Vue 组件里的 script 部分,写入以下的代码,就可以实现 TKD 信息的配置。

import { useHead } from '@vueuse/head'
useHead({
  meta: [
    {
      name: 'title',
      content: '这是页面的标题'
    },
    {
      name: 'description',
      content: '这是页面的描述'
    },
    {
      name: 'keywords',
      content: '关键词1,关键词2,关键词3'
    }
  ]
})

你还可以扩展更多的信息上去,具体都在各自对应的 Github 仓库的 README 里有详细的说明。

当然,SEO 优化远远不止这一点,包括 robots 、 链接语义化 、减少死链 、 旧地址重定向等等,后面也会有说明。

静态资源处理

静态资源指 js 、 css 、 img 这些资源,放自己服务器也不是不好,我之前就是放自己服务器上,没有去改,虽然 WordPress 虽然有配置 CDN 的插件,但是 CDN 平台诸如七牛、又拍云,免费额度只针对 http , 都是需要付费才可以使用 https,总的来说还是要多出一笔钱来处理这块服务,反正自己的博客访问量不大,而且技术博客很少多媒体资源,日常使用的带宽消耗很少,我三年前在阿里云充的 50 块钱,三年过去了到现在还有 45.91 …

不过这次改版就不一样了,后续我可能还会开辟一些图片模块,加上改版后是把项目托管到了 Github ,先天优势存在,那么就要多考虑一下利用 Github 提供的免费服务了!

开发过 NPM 包的同学,或者日常使用 NPM 插件比较细心的同学,应该能够发现发布在 NPM 上的包都自动部署到了 CDN 平台,诸如 jsdelivr 、 unpkg 、cdnjs 等等,那么 Github 和这些 CDN 能关联吗?在此之前其实我也没去关注能不能,但这一次我查了一下,确实可以,而且其中对国内访问速度最友好的 jsdelivr ,支持度最高!超棒的!

关于 jsdelivr 的速度可以参考:国内有哪些靠谱的 Javascript 库 CDN 可用?[36],也可以测试下我的博客,我自己对测试结果还是挺满意的。

测试我自己网站的速度

所以最后我是把所有静态资源都指向了 jsdelivr CDN ,它无需你自己再做任何部署工作,只需要把代码文件更新到你的 GitHub 仓库里,就会自动同步到 jsdelivr 。

访问格式为在 jsdelivr CDN 官网[37] 有案例说明,更多用法可以查看官网的文档 Features - jsdelivr[38],为了避免项目源码过大,你可以像我一样单独创建一个类似 assets-storage[39] 这样的仓库用来存储这些静态资源,在仓库的 README 也有简单介绍下如何引用 CDN 地址和清除 CDN 缓存。

回到项目里,只需要在 vite.config.ts[40] 里修改 base 的路径即可。

export default defineConfig({
  base: isDev
    ? '/'
    : 'https://cdn.jsdelivr.net/gh/chengpeiquan/chengpeiquan.com@gh-pages/'
})

详细可以看官网的文档 Configuring Vite | Vite[41]

当然这种方式如果你用平时的命令行或者老乌龟界面工具来提交文件,始终还是比较麻烦,这里推荐一个现成的图床工具 PicGo[42] ,支持多个平台的 CDN 服务,其中就有 Github 。

PicGo 图床界面

你可以在 Github 仓库上的 Releases[43] 下载最新的客户端版本,只是使用的话,可以单独下载对应系统的安装文件,不需要克隆整个仓库下来自己构建。

资源导出

本次的资源导出主要是指原来的那些图片,前面有提到,我之前没有启动 CDN 服务,所以图片资源都还在自己的服务器上。

WordPress 的上传资源都存放在 /wp-content/uploads/ 目录下,阿里云非常方便的就是,你可以连 SFTP 上去把这些文件直接拖下来就可以了。

重新传到 Github 上又非常简单,克隆你的仓库下来后,放到指定的文件夹里,重新提交就可以了。

等未来某一次你不想继续用 Github 托管了,只需要把仓库拉下来,所有文件又都在了,都是非常方便和灵活。

爬虫编写

这一部分主要针对原来的文章,虽然我之前的 WordPress 就开启了 Markdown 编辑器支持,但如 SEO 优化[44] 里提到的,缺少很多 TKD 信息配置,而且里面的图片地址也都要更换为 CDN 的路径,所以就算用现成工具去处理 HTML / XML 转 Markdown,都还要去补充这些信息,也比较繁琐。

所以是借助了 Node 编写了个静态爬虫,在爬取过程中对一些内容进行追加、转换。

具体的实现可以参考我之前写的 网站改版迁移经验记录:基于 node 的爬虫编写[45] ,这里就不重复赘述了。

数据统计

既然是 Vue 项目,那么当然支持 Vue 系的统计插件,之前写的两个统计平台插件,都是可以开箱即用的,均已支持 Vue 3.0 的使用。你可以在 main.ts[46] 里了解如何开启流量的统计上报功能,如果你需要记录埋点,也都有 API 可以轻松触发数据的上报。

百度统计:vue-baidu-analytics[47]

友盟统计:vue-cnzz-analytics[48]

服务端开发

服务端之前是 WordPress 所依赖的 Nginx + PHP + MySQL ,这一次重构也把服务端直接换了,更换为 Node.JS + Express ,通过 PM2 守护进程来运行在阿里云。

对,这一次没有数据库,第一版暂时不打算做数据库,暂时用不到,目前大部分数据都已经迁移到 Github 仓库了,下个版本功能迭代用到了再考虑弄一下。

我的服务器系统是 CentOS 7,也就是 Linux 系统,关于 Linux 下如何安装 Node ,搜素引擎很多方法,这里也不赘述了,放几个自己用到的关键命令参考吧。

  1. 清除缓存然后升级系统和软件
sudo yum clean all sudo yum makecache sudo yum update sudo yum upgrade -y
  1. 安装 NPM 并通过 stable 安装最新版本的 Node
sudo yum install npm sudo npm install -g n sudo n stable
  1. 全局安装 yarn[49] ,没错,我现在更喜欢用 yarn 来进行包管理,这一步你可以跳过
npm i -g yarn
  1. 然后是全局安装 pm2[50],这个是必须要装的,否则我们的终端一关,服务就停了,需要通过 PM2 来守护进程,当然,你也可以用 forever[51]
yarn global add pm2

其他的步骤就不用说了,创建服务器的文件夹,初始化,安装 express[52] 或者其他你更熟悉的服务程序,搞起吧!

有几件事要特别叮嘱一下:

1. 因为服务端变了,如果原来有开启 HTTPS,记得重新配置你的 SSL 证书(我用的是阿里云的免费证书,只需要 1 年更换 1 次)

2. 域名也要重新做 301 重定向(HTTP 强切 HTTPS , WWW 强切无 3W 等)

3. 检查之前是否有在推广的的链接挂掉了,也要重新 301 到新地址 (比如 RSS 源之前是 /feed/ ,现在是 /feed.xml)

4. 最重要的,配置上对路由 history 模式的支持

第一版其实不复杂,后面有需要会继续迭代。

自动化部署

代码托管在 GitHub 的好处就是 GitHub Actions 可以帮我们实现 CI / CD,通过配置分支的 push 或者 pull_request 等行为来实现自动触发项目的构建打包,并实现一键部署到阿里云服务器。

具体的脚本可以参考我写的 workflow[53] ,里面都提供了注释。

workflow 里所有以 secrets.XXXXXX 的格式均为仓库独立配置的密钥变量,在仓库的 settings > Actions secrets 里添加。

其中一些关键环节说明如下:

  1. on 是指分支行为,我配置了合并分支才会触发,因为平时都是托管在 develop 分支,包括未开发完毕的功能,写一半的文章草稿,只有确认可以发布的代码,才会合并到 main 进行更新
  2. jobs 是触发自动打包 / 发布一系列行为的各种操作,从上到下按顺序处理,其中的 ACCESS_TOKEN 是 GitHub 的 Token,请来 Personal access tokens[54] 创建,创建后只会显示一次,请保存好,后面涉及到 Token 的地方可以重复使用同一个 Token,请勿泄露!
  3. gh-pages 分支是打包完毕后的文件,推送到阿里云服务器的也是这个分支下的所有文件,之所以托管一份在 GitHub,是因为我们前面部署了 CDN 支持,JS / CSS 文件是需要读取这个分支的 CDN 文件
  4. 部署到阿里云的环节,配置的 SERVER_SSH_KEY 是自己服务器的密钥对,如果你也是跟我一样使用阿里云的 ECS ,可以参考 创建 SSH 密钥对[55], 创建后还需要绑定给实例才能激活生效,绑定操作请参考 绑定 SSH 密钥对[56]
  5. SERVER_IP 是自己服务器的公网 IP,这个其实可以不用配置为密钥变量,因为 ping 一下你的域名也知道是什么 IP ,只是因为我有两台服务器,所以配置为变量可以方便的通过 SERVER_IP 和 SERVER_IP_TEST 去切换,其他变量其实也有一个 TEST 版本
  6. 最后的 TARGET 是你在服务器上,node 服务器所安装的目录。

如果其中有什么环节不清楚的,善用搜索引擎,或者到我博客仓库给我提 issue 也可以。

如果你不是托管在 GitHub ,而是别的 Git 平台诸如自建的 Gitlab ,你也可以通过 Jenkins[57] 去配置 CI / CD 的支持。

离线应用构建

使用 Vue-CLI 创建新项目的时候,可以了解到有一个选项是关于 PWA 的,关于 PWA 的定义建议直接阅读 渐进式 Web 应用(PWA) | MDN[58]

Vite 官方团队也对 PWA 做了支持,通过 vite-plugin-pwa[59] 可以方便的实现一个离线应用的配置。

不过目前发现了一个问题就是,当 vite.config.ts 的 base 选项设置为 CDN 地址时,构建出来的 PWA manifest 资源路径会读取错误,原因是 manifest 不能走 CDN,要单独从网站内读取,虽然跟作者提了优化建议(详见 #25[60]),不过还需要点时间去优化。

所以在原版进行版本更新之前,自己先发布了个私有调试包 fix 了这个问题,有遇到一样情况的朋友可以先安装 \@chengpeiquan/vite-plugin-pwa[61] 这个去用,不过最好还是留意原版的更新,这个私有包不会长期维护。

2021-02-22 更新:目前原版已更新,Fix 了我反馈的问题,请使用 v0.5.3 以后的版本可以避免该问题的产生,给作者点赞!

关于 PWA 的配置可以参考我的项目,这里单独说一下需要特别注意的点:

  1. 因为使用了 CDN,所以 scope 和 manifest.start_url 选项需要显式指定,否则资源会读取出错
  2. 基于我上面提到的路径问题,从 v0.5.3 开始,配置 CDN 的同时,也需要显式指定 base 选项,避免出现 404

其他的选项根据实际需要去处理就可以了。

结语

因为网站的设计一向不是我的专长,加上不喜欢花里胡哨的东西,所以这一次重构后的 UI 设计还是基本继承了原来的风格。

但也有一些新的迭代,比如加上了跟随系统的暗黑风格(也可以通过导航右上角进行手动切换),还有首页的变化,对于内容不多的博客来说,挺好的一个 idea,这是来自好友小毅 The Art of Chawye Hsu[62] 和 Vite 开发者 Antfu Anthony Fu[63] 的博客参考。

当然,整个项目的重构,更多的技术支持来自于 Anthony,他也是 Vue 和 Vite 官方团队的开发者,他比我早几天上线的 Rewrite in Vite[64] 给了我很多思路,很多基于 Vite 的插件也是他写的,都是在这几天发布和迭代,有那种瞌睡来了枕头的感觉,美妙!

完整的项目依赖和配置请查看仓库的 package.json[65]vite.config.ts[66] ,整个项目也完全开源了,具体的实现可以查看 Github 仓库[67] ,在这里就不赘述了,如果觉得对你有用,欢迎 Star 。

公众号:小何成长,佛系更文,都是自己曾经踩过的坑或者是学到的东西

有兴趣的小伙伴欢迎关注我哦,我是:何小玍。 大家一起进步鸭

们经常写 HTML 、 CSS 和 JavaScript ,写好这些之后,我们就会在浏览器中看到页面,那浏览器究竟在这背后做了一些什么事情呢?本篇文章将揭晓答案!

了解浏览器的渲染原理是我们在通往更深层次的前端开发中不可缺少的,它可以让我们从更深层次、角度去考虑性能优化等~

下面进入正文~

进程、线程

浏览器会分配一个线程“自上而下,从左到右”依次解析和渲染代码,那么进程和线程是什么,它们之间有着怎样的关系呢?

进程

一个进程就是一个程序运行的实例。当启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码,运行中的数据和一个执行任务的主线程,这样的一个运行环境就叫进程

线程

线程不能单独存在,它是由进程来启动和管理的。线程依附于进程,进程中使用多线程并行处理能提升运算效率

两者之间的关系

1、进程中的任意一线程执行出错,都会导致整个进程的崩溃

2、线程之间可以共享数据

3、当一个进程关闭后,操作系统会回收进程所占用的内存

4、进程之间的内容相互隔离

渲染机制

从HTML、CSS和JavaScript开始

了解浏览器的渲染原理,我们就要从理解 HTML 、 CSS 和 JavaScrip 开始,我们先来看一张图

HTML (超文本标记语言),顾名思义,由标记(标签)和文本组成,每个标签都有自己的语意,浏览器会根据标签和文本展示对应的内容。

CSS (层叠样式表),由选择器和属性组成,它可以改变 HTML 的样式,比如上图中,我们改变了 span 的颜色由蓝色为绿色。

JavaScript ,我们可以通过 JS 完成很多事情,例如上图中修改样式。

下面开始分析渲染的原理

渲染流水线

渲染模块由于渲染的机制的复杂,被划分为了很多子阶段,输入的 HTML 经过这些子阶段,最后会输出为像素。这样的处理流程就叫做 渲染流水线

按照渲染的时间顺序,流水线可分为几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成

构建DOM树

由于浏览器无法直接理解和使用 HTML ,所以需要将 HTML 转换为浏览器能够理解的结构( DOM 树)

树结构示意图

DOM树的构建过程

我们来分析一下下面这段代码会构建出一棵什么样的 DOM 树

我们先将上面的代码运行,然后在浏览器控制台输入 document ,看看会有什么效果

我们一层级一层级的打开就会看到如上图的效果,我们可以根据这每一层级展开的效果,绘制出一棵 DOM 树结构,如下:

接下来,我们试一下利用 JS 修改一下内容,看有什么改变:

我们可以看到“浏览器”的文字变成了“chrome”

再来看一下 DOM 树是否有改变

我们看到在“浏览器”的位置换成了“chrome”,那么如何让 DOM 节点拥有样式?

样式计算

样式计算,顾名思义,就是 计算出 DOM 节点中每个元素的具体样式 ,这个阶段会分为三部分:

  • 把 CSS 转换为浏览器能够理解的结构
  • 转换样式表中的属性值,使其标准化
  • 计算出 DOM 树中每个节点的样式

CSS样式来源

  • link 导入外部样式资源

浏览器会新开辟一个线程,去服务器获取对应的资源文件(不阻碍主线程的渲染)

  • style 内嵌样式

从上到下解析,解析完继续解析 DOM 结构。在真实项目中,如果 css 代码不是很多,或是移动端项目,我们应该使用内嵌式,以此来减少 http 资源的请求,提高页面渲染速度

  • 行内样式
  • @import 导入

它是同步的,不会开辟新线程去加载资源文件,而是让主线程去获取,这阻碍 DOM 结构的继续渲染;只有把外部样式导入进来,并且解析后,才会继续渲染 DOM 结构

把CSS转换为浏览器能够理解的结构

浏览器就像不能理解 HTML 一样,不理解 CSS ,所以当渲染引擎接收到 CSS 文件时,会执行转换操作,将 CSS 文本转换为浏览器可以理解的 styleSheets 结构。

在 HTML 中,在浏览器中输入 document 可以查看 html 的结构。在 css 中,可以输入 document.styleSheets 看到 css 的结构

现在的结构是空的,我们来加一些样式,看看效果

转换样式表中的属性值,使其标准化

属性值标准化就是将所有值转换为渲染引擎容易理解的、标准化的计算值。我们大致看一下效果:

  • 标准化前
body {
    font-size: 2em;
    color: black;
    font-weight: bold;
    ...
}
复制代码
  • 标准化后
body {
    font-size: 16px;
    color: rgb(0, 0, 0);
    font-weight: 700;
    ...
}
复制代码

计算出DOM树中每个节点的具体样式

样式计算有两个CSS的规则:继承规则和层叠规则

  • CSS继承规则

CSS 继承就是每个 DOM 节点都包含有父节点的样式。我们来看一下下面这段代码中如何应用到 DOM 节点上

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        h1 {
            color: red;
        }

        div {
            color: blue;
        }

        span {
            font-size: 16px;
        }
    </style>
</head>
<body>
    <h1>掘金</h1>
    <div>
        <span>浏览器</span>
        <span>渲染原理</span>
        构建DOM树
    </div>
</body>
</html>
复制代码

子节点会拥有父节点的样式,由此我们可以画出这样一张图

我们还可以打开控制台,看一下选中 span 标签,都会看到哪些内容

通过上图,我们可看到一个元素的样式、继承过程等, userAgent 样式是浏览器默认的内置样式,如果我们不提供任何样式,就会使用此样式。

  • 样式层叠规则

层叠在 CSS 处于核心地位,它是 CSS 的一个基本特征,它定义了如何合并来自多个源的属性值的算法。

样式计算阶段最终输出的内容是每个 DOM 节点的样式,并且保存在了 ComputedStyle 中。我们可以通过控制台看到某个 DOM 元素最终的计算样式

布局阶段

现在我们不知道 DOM 元素的几何位置信息,所以现在我们需要计算出 DOM 树中可见元素的几何位置,这个计算过程就叫做布局。布局阶段有两个过程:

  • 创建布局树
  • 布局计算

创建布局树

创建布局树的意思就是创建一棵只包含可见元素的树。我们来看下面一段代码创建布局树的过程

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        h1 {
            color: red;
        }

        div {
            color: blue;
        }

        div span {
            font-size: 16px;
        }

        div span:last-child {
            display: none;
        }
    </style>
</head>
<body>
    <h1>掘金</h1>
    <div>
        <span>浏览器</span>
        <span>渲染原理</span>
        构建DOM树
    </div>
</body>
</html>
复制代码

构建布局树的过程中, DOM 树中所有不可见的节点都不会包含在这棵树中。浏览器会遍历 DOM 树中所有能看见的节点,然后把这些节点加入到布局中;不可见的节点就会被忽略, head 标签下面的内容、 div 下最后一个 span 节点都不会在布局树中,我们看一下这个过程图感受一下~

布局计算

布局计算就是计算布局树节点的坐标位置。这个计算过程极为复杂。

分层

渲染引擎会为特定的节点生成专用的图层,并生成一棵对应的图层树。这样做是因为页面中可能含有很多复杂的效果,我们可以打开控制台看一下页面的分层情况

我们可以看到,渲染引擎给页面分了很多图层,这些图层会按照一定顺序叠加在一起,形成最终的页面

那么图层的来源有哪些?

1、拥有层叠上下文属性的元素会被提升为单独的一层

层叠上下文可以使能够使 HTML 元素具有三维的概念,这些 HTML 元素按照自身属性的优先级分布在垂直于这个二维平面的 z 轴上。哪些元素具有层叠上下文属性?

2、需要剪裁的地方会被创建为图层

当我们创建一个有宽度和高度的 div 时,里面的文字内容可能会超出这个区域,这时候渲染引擎会把裁剪文字内容的一部分用于显示在 div 区域,例如

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        div {
            width: 100px;
            height: 100px;
            background: yellow;
            overflow: auto;
        }
    </style>
</head>
<body>
    <div>
        我们经常写`HTML`、`CSS`和`JavaScript`,写好这些之后,我们就会在浏览器中看到页面,那浏览器究竟在这背后做了一些什么事情呢?本篇文章将揭晓答案!

        了解浏览器的渲染原理是我们在通往更深层次的前端开发中不可缺少的,它可以让我们从更深层次、角度去考虑性能优化等~
    </div>
</body>
</html>
复制代码

运行结果

我们再打开控制台的 layers 看一下效果

可以看到渲染引擎为文字部分单独创建了一个图层。

在布局树中的节点如果拥有对应的图层,这个节点就是一个图层,如果没有,这个节点就属于父节点的图层,如下图:

图层绘制

创建好图层树后,渲染引擎会绘制图层树中的每个图层。渲染引擎会将图层绘制分解为很多小的绘制指令,然后将这些指令按照顺序组成待绘制列表,我们可以打开控制台的 layers ,选择 document 层,看一下效果

栅格化操作

栅格化就是将图块转换位位图,图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有图块的栅格化都是在线程池内执行的。

图层绘制列表准备好之后,主线程会把这个绘制列表提交给合成线程,绘制操作由渲染引擎中的合成线程来完成。

合成线程将图层划分为图块,然后合成线程会按照视口(可见区域)附近的图块优先生成位图。

合成与显示

所有的图块都被光栅化后,合成线程会生成一个绘制图块的命令( DrawQuad ),然后将该命令提交给浏览器进程。浏览器进程里面 viz 组件用来接收 DrawQuad 命令,将其页面内容绘制到内存中,最后将内存显示到屏幕。这个时候,我们就看到了页面

完善渲染流水线示意图

根据上文中描述,我们可以画出这样一张图

我还在网上找到了另外一张图

这两张图都是描述浏览器的渲染流程的。

TTP/2 相比于 HTTP/1.1,可以说是大幅度提高了网页的性能,只需要升级到该协议就可以减少很多之前需要做的性能优化工作。当然,兼容问题以及如何优雅降级应该是国内还不普遍使用的原因之一。虽然 HTTP/2 提高了网页的性能,但并不代表它已经完美,HTTP/3 就是为了解决 HTTP/2 存在的一些问题而被推出的。

HTTP/1.1 发明以来发生了哪些变化?

如果仔细观察打开那些最流行的网站首页所需要下载的资源的话,会发现一个非常明显的趋势。近年来,加载网站首页需要的下载的数据量在逐渐增加,并已经超过了 2100K。但在这里我们更应该关心的是:平均每个页面为了完成显示与渲染所需要下载的资源数已经超过了 100 个。

正如下图所示,从 2011 年以来, 传输数据大小与平均请求资源数量不断持续增长,并没有减缓的迹象。该图表中,绿色直线展示了传输数据大小的增长,红色直线展示了平均请求资源数量的增长。

HTTP/1.1 自从 1997 年发布以来,我们已经使用 HTTP/1.x 相当长一段时间了,但是随着近十年互联网的爆炸式发展,从当初网页内容以文本为主, 到现在以富媒体(如图片、声音、视频)为主, 而且对页面内容实时性要求高的应用越来越多 (如聊天、视频直播), 于是当时协议规定的某些特性,已经无法满足现代网络的需求了。

HTTP/1.1 的缺陷

1. 高延迟 – 带来页面加载速度的降低

虽然近几年来网络带宽增长非常快,然而我们却并没有看到网络延迟有对应程度的降低。网络延迟问题主要由于队头阻塞 (Head-Of-Line Blocking) 产生,导致带宽无法被充分利用

队头阻塞是指,当顺序发送的请求序列中的一个请求因为某种原因被阻塞时,在后面排队的所有请求也一并被阻塞,会导致客户端迟迟收不到数据。针对队头阻塞, 人们尝试过以下办法来解决:

  • 将同一页面的资源分散到不同域名下,提升连接上限。Chrome 有个机制,对于同一个域名,默认允许同时建立 6 个 TCP 持久连接。使用持久连接时,虽然能共用一个 TCP 管道,但是在一个管道中同一时刻只能处理一个请求,在当前的请求没有结束之前,其他的请求只能处于阻塞状态。另外,如果在同一个域名下同时有 10 个请求发生,那么其中 4 个请求会进入排队等待状态,直至进行中的请求完成。
  • Spriting 合并多张小图为一张大图, 再用 JavaScript 或者 CSS 将小图重新“切割”出来的技术。
  • 内联 (Inlining) 是另一种防止发送很多小图请求的技巧,将图片的原始数据嵌入在 CSS 文件里面的 URL 里,减少网络请求次数。
.icon1 {
 background: url(data:image/png;base64,<data>) no-repeat;
 }
.icon2 {
 background: url(data:image/png;base64,<data>) no-repeat;
 }

  • 拼接 (Concatenation) 将多个体积较小的 JavaScript 使用 webpack 等工具打包成 1 个体积更大的 JavaScript 文件, 但如果其中 1 个文件改动,会导致大量数据被重新下载多个文件。

2. 无状态特性 – 带来巨大的 HTTP 头部

由于报文 Header 一般会携带 "User Agent" “Cookie”“Accept”"Server" 等许多固定的头字段(如下图),多达几百字节甚至上千字节,但 Body 却经常只有几十字节(比如 GET 请求、 204/301/304 响应),成了不折不扣的“大头儿子”。Header 里携带的内容过大,在一定程度上增加了传输成本。更要命的是,成千上万的请求响应报文里有很多字段值都是重复的,非常浪费。

3. 明文传输 – 带来不安全性

HTTP/1.1 在传输数据时,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份,这在一定程度上无法保证数据的安全性。

你有没有听说过 " 免费 WiFi 陷阱”之类的新闻呢?黑客就是利用了 HTTP 明文传输的缺点,在公共场所架设一个 WiFi 热点开始“钓鱼”,诱骗网民上网。一旦你连上了这个 WiFi 热点,所有的流量都会被截获保存,里面如果有银行卡号、网站密码等敏感信息的话那就危险了,黑客拿到了这些数据就可以冒充你,为所欲为。

4. 不支持服务器推送消息

SPDY 协议与 HTTP/2 简介

1.SPDY 协议

上面我们提到, 由于 HTTP/1.x 的缺陷,我们会引入雪碧图、将小图内联、使用多个域名等方式来提高性能,不过这些优化都绕开了协议。直到 2009 年,谷歌公开了自行研发的 SPDY 协议,主要解决 HTTP/1.1 效率不高的问题。谷歌推出 SPDY,才算是正式改造 HTTP 协议本身。降低延迟,压缩 header 等等,SPDY 的实践证明了这些优化的效果,也最终带来 HTTP/2 的诞生。

HTTP/1.1 有两个主要的缺点:安全不足和性能不高。由于背负着 HTTP/1.x 庞大的历史包袱, 所以协议的修改, 兼容性是首要考虑的目标,否则就会破坏互联网上无数现有的资产。如上图所示, SPDY 位于 HTTP 之下,TCP 和 SSL 之上,这样可以轻松兼容老版本的 HTTP 协议 (将 HTTP1.x 的内容封装成一种新的 frame 格式),同时可以使用已有的 SSL 功能。

SPDY 协议在 Chrome 浏览器上证明可行以后,就被当作 HTTP/2 的基础,主要特性都在 HTTP/2 中得到继承。

2.HTTP/2 简介

2015 年,HTTP/2 发布。HTTP/2 是现行 HTTP 协议(HTTP/1.x)的替代,但它不是重写,HTTP 方法 / 状态码 / 语义都与 HTTP/1.x 一样。HTTP/2 基于 SPDY,专注于性能,最大的目标是在用户和网站间只用一个连接(connec-tion)。从目前的情况来看,国内外一些排名靠前的站点基本都实现了 HTTP/2 的部署,使用 HTTP/2 能带来 20%~60% 的效率提升。

HTTP/2 由两个规范(Specification)组成:

  1. Hypertext Transfer Protocol version 2 - RFC7540
  2. HPACK - Header Compression for HTTP/2 - RFC7541

HTTP/2 新特性

1. 二进制传输

HTTP/2 传输数据量的大幅减少, 主要有两个原因: 以二进制方式传输和 Header 压缩。我们先来介绍二进制传输,HTTP/2 采用二进制格式传输数据,而非 HTTP/1.x 里纯文本形式的报文 ,二进制协议解析起来更高效。HTTP/2 将请求和响应数据分割为更小的帧,并且它们采用二进制编码

它把 TCP 协议的部分特性挪到了应用层,把原来的 "Header+Body" 的消息 " 打散 " 为数个小片的二进制 " 帧 "(Frame), 用 "HEADERS" 帧存放头数据、"DATA" 帧存放实体数据。HTTP/2 数据分帧后,“Header+Body" 的报文结构就完全消失了,协议看到的只是一个个 " 碎片”。

HTTP/2 中,同域名下所有通信都在单个连接上完成,该连接可以承载任意数量的双向数据流。每个数据流都以消息的形式发送,而消息又由一个或多个帧组成。多个帧之间可以乱序发送,根据帧首部的流标识可以重新组装

2.Header 压缩

HTTP/2 并没有使用传统的压缩算法,而是开发了专门的 "HPACK”算法,在客户端和服务器两端建立“字典”,用索引号表示重复的字符串,还采用哈夫曼编码来压缩整数和字符串,可以达到 50%~90% 的高压缩率。

具体来说:

  • 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键 - 值对,对于相同的数据,不再通过每次请求和响应发送。
  • 首部表在 HTTP/2 的连接存续期内始终存在,由客户端和服务器共同渐进地更新 。
  • 每个新的首部键 - 值对要么被追加到当前表的末尾,要么替换表中之前的值。

例如下图中的两个请求, 请求一发送了所有的头部字段,第二个请求则只需要发送差异数据,这样可以减少冗余数据,降低开销 。

3. 多路复用

在 HTTP/2 中引入了多路复用的技术。多路复用很好地解决了浏览器限制同一个域名下请求数量的问题,同时也更容易实现全速传输,毕竟新开一个 TCP 连接都需要慢慢提升传输速度。

大家可以通过该链接直观感受下 HTTP/2 比 HTTP/1 到底快了多少: https://http2.akamai.com/demo

在 HTTP/2 中,有了二进制分帧之后,HTTP /2 不再依赖 TCP 链接去实现多流并行了,在 HTTP/2 中:

  • 同域名下所有通信都在单个连接上完成。
  • 单个连接可以承载任意数量的双向数据流。
  • 数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装。

这一特性使性能有了极大提升:

  • 同个域名只需要占用一个 TCP 连接,使用一个连接并行发送多个请求和响应, 这样整个页面资源的下载过程只需要一次慢启动,同时也避免了多个 TCP 连接竞争带宽所带来的问题。
  • 并行交错地发送多个请求 / 响应,请求 / 响应之间互不影响。
  • 在 HTTP/2 中,每个请求都可以带一个 31bit 的优先值,0 表示最高优先级, 数值越大优先级越低。有了这个优先值,客户端和服务器就可以在处理不同的流时采取不同的策略,以最优的方式发送流、消息和帧。

如上图所示,多路复用的技术只通过一个 TCP 连接就可以传输所有的请求数据。

4.Server Push

HTTP2 还在一定程度上改变了传统的“请求 - 应答”工作模式,服务器不再完全被动地响应请求,也可以新建“流”主动向客户端发送消息。比如,在浏览器刚请求 HTML 的时候就提前把可能会用到的 JS、CSS 文件发给客户端,减少等待的延迟,这被称为 " 服务器推送 "( Server Push,也叫 Cache push)。

如下图所示,服务端主动把 JS 和 CSS 文件推送给客户端,而不需要客户端解析 HTML 时再发送这些请求。

另外需要补充的是,服务端可以主动推送,客户端也有权利选择是否接收。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送 RST_STREAM 帧来拒收。主动推送也遵守同源策略。换句话说,服务器不能随便将第三方资源推送给客户端,而必须是经过双方确认才行。

5. 提高安全性

出于兼容的考虑,HTTP/2 延续了 HTTP/1 的“明文”特点,可以像以前一样使用明文传输数据,不强制使用加密通信,不过格式还是二进制,只是不需要解密。

但由于 HTTPS 已经是大势所趋,而且主流的浏览器 Chrome、Firefox 等都公开宣布只支持加密的 HTTP/2,所以“事实上”的 HTTP/2 是加密的。也就是说,互联网上通常所能见到的 HTTP/2 都是使用 "https”协议名,跑在 TLS 上面。HTTP/2 协议定义了两个字符串标识符:“h2" 表示加密的 HTTP/2,“h2c”表示明文的 HTTP/2。

HTTP/3 新特性

1.HTTP/2 的缺点

虽然 HTTP/2 解决了很多之前旧版本的问题,但是它还是存在一个巨大的问题,主要是底层支撑的 TCP 协议造成的。HTTP/2 的缺点主要有以下几点:

  • TCP 和 TCP+TLS 建立连接的延时

HTTP/2 是使用 TCP 协议来传输的。如果使用 HTTPS 的话,还需要使用 TLS 协议进行安全传输,而使用 TLS 也需要一个握手过程,这样就需要有两个握手延迟过程

①在建立 TCP 连接的时候,需要和服务器进行三次握手来确认连接成功,也就是说需要在消耗完 1.5 个 RTT 之后才能进行数据传输。

②进行 TLS 连接,TLS 有两个版本——TLS1.2 和 TLS1.3,每个版本建立连接所花的时间不同,大致是需要 1~2 个 RTT。

总之,在传输数据之前,我们需要花掉 3~4 个 RTT。

  • TCP 的队头阻塞并没有彻底解决

上文我们提到在 HTTP/2 中,多个请求是跑在一个 TCP 管道中的。但当出现了丢包时,HTTP/2 的表现反倒不如 HTTP/1 了。因为 TCP 为了保证可靠传输,有个特别的“丢包重传”机制,丢失的包必须要等待重新传输确认,HTTP/2 出现丢包时,整个 TCP 都要开始等待重传,那么就会阻塞该 TCP 连接中的所有请求(如下图)。而对于 HTTP/1.1 来说,可以开启多个 TCP 连接,出现这种情况反倒只会影响其中一个连接,剩余的 TCP 连接还可以正常传输数据。

读到这里,可能就会有人考虑为什么不直接去修改 TCP 协议?其实这已经是一件不可能完成的任务了。因为 TCP 存在的时间实在太长,已经充斥在各种设备中,并且这个协议是由操作系统实现的,更新起来不大现实。

2.HTTP/3 简介

Google 在推 SPDY 的时候就已经意识到了这些问题,于是就另起炉灶搞了一个基于 UDP 协议的“QUIC”协议,让 HTTP 跑在 QUIC 上而不是 TCP 上。而这个“HTTP over QUIC”就是 HTTP 协议的下一个大版本,HTTP/3。它在 HTTP/2 的基础上又实现了质的飞跃,真正“完美”地解决了“队头阻塞”问题。

QUIC 虽然基于 UDP,但是在原本的基础上新增了很多功能,接下来我们重点介绍几个 QUIC 新功能。不过 HTTP/3 目前还处于草案阶段,正式发布前可能会有变动,所以本文尽量不涉及那些不稳定的细节。

3.QUIC 新功能

上面我们提到 QUIC 基于 UDP,而 UDP 是“无连接”的,根本就不需要“握手”和“挥手”,所以就比 TCP 来得快。此外,QUIC 也实现了可靠传输,保证数据一定能够抵达目的地。它还引入了类似 HTTP/2 的“流”和“多路复用”,单个“流 " 是有序的,可能会因为丢包而阻塞,但其他“流”不会受到影响。具体来说,QUIC 协议有以下特点:

  • 实现了类似 TCP 的流量控制、传输可靠性的功能。

虽然 UDP 不提供可靠性的传输,但 QUIC 在 UDP 的基础之上增加了一层来保证数据可靠性传输。它提供了数据包重传、拥塞控制以及其他一些 TCP 中存在的特性。

  • 实现了快速握手功能。

由于 QUIC 是基于 UDP 的,所以 QUIC 可以实现使用 0-RTT 或者 1-RTT 来建立连接,这意味着 QUIC 可以用最快的速度来发送和接收数据,这样可以大大提升首次打开页面的速度。0RTT 建连可以说是 QUIC 相比 HTTP2 最大的性能优势

  • 集成了 TLS 加密功能。

目前 QUIC 使用的是 TLS1.3,相较于早期版本 TLS1.3 有更多的优点,其中最重要的一点是减少了握手所花费的 RTT 个数。

  • 多路复用,彻底解决 TCP 中队头阻塞的问题。

和 TCP 不同,QUIC 实现了在同一物理连接上可以有多个独立的逻辑数据流(如下图)。实现了数据流的单独传输,就解决了 TCP 中队头阻塞的问题。

总结

  • HTTP/1.1 有两个主要的缺点:安全不足和性能不高。
  • HTTP/2 完全兼容 HTTP/1,是“更安全的 HTTP、更快的 HTTPS",头部压缩、多路复用等技术可以充分利用带宽,降低延迟,从而大幅度提高上网体验。
  • QUIC 基于 UDP 实现,是 HTTP/3 中的底层支撑协议。该协议基于 UDP,又汲取了 TCP 中的精华,实现了既快又可靠的协议。