这一章还是简单对用户看到的网页由什么构成,浏览器又做了哪些工作才让这些构成部分呈现在用户面前的网页进行简单介绍。
一个内容丰富、设计美观、交互友好的网页离不开前端三剑客 HTML、CSS、JS 以及图片、字体等资源文件:
HTML 决定网页内容,是用户访问任意一个网站的入口,既可以在 HTML 中直接编写 CSS、JS 代码,也可以将 CSS、JS 代码写在单独的文件中在 HTML 引入;
CSS 作用于网页样式;
JS 实现用户交互。
网页入口 HTML 的基本结构:
<html> <head> <title>网页标题,在浏览器打开的网页tab上显示</title> <meta name="keywords" content="网页关键词,SEO"/> <meta name="description" content="网页描述,SEO"/> <!-- html中内联css写法 --> <style> .foo { color: red; }</style> <!-- html引入外部单独的css文件写法 --> <link rel="stylesheet" href="https://x.alicdn.com/xx/xxx/screen.css"/> </head> <body> <!-- 网页内容 --> <div class="foo"> Page Content </div> <!-- html中内联js脚本写法 --> <script> function log(param) { console.log(param) } log('解析并执行这段js代码')</script> <!-- html引入外部单独的js文件写法 --> <script src="https://x.alicdn.com/xx/xxx/screen.js"></script> </body></html>
用户访问任意网站之前,要先在地址栏输入一个有效地址,接着浏览器会向服务器发起请求,去拿到该地址对应的网页入口文件即"xxx.html",打开浏览器 Network 控制台便可以看到,这一定是浏览器第一个接收到的响应内容。
紧接着,浏览器解析 HTML 代码,识别到其他资源发起更多请求,经过各种类型资源的加载、解析、执行(非必需)逐步成为用户眼前看到的完整页面,讲到这里就不得不提到 CRP(Critical Rendering Path,关键路径渲染),即浏览器将 HTML、JS、CSS 代码转换成屏幕上用户可见像素必经的一系列关键步骤,如下:
网络下载 HTML,解析 HTML 代码构建 DOM;
网络下载 CSS,解析 CSS 代码构建 CSSOM;
网络下载 JS,解析执行 JS 代码,可能会修改 DOM 或 CSSOM;
待 DOM & CSSOM “定型”,浏览器根据 DOM 和 CSSOM 构造 Render Tree;
重排过程计算每个元素节点所在位置与样式;
重绘过程将绘制真实像素于屏幕上。
至此,网页呈现在用户面前,进行下一步的浏览和操作。
开发阶段
看完上章,想必你也知道浏览器搜索-呈现网页是怎么回事了,这一部分将简单介绍现代化网页开发过程。
▐ 代码编写
▐ 工程能力
其中,有必要对模块化进行说明,它带来的好处就是,在开发阶段,我们可以将不同类型的文件统一视为模块处理,模块成为模块系统中的第一公民,它们之间可以相互引用,至于不同文件类型模块之间的差异,交由构建工具去解决。
在JS模块中引入其他模块:
import '@/common/style.scss' // 引入scss
import arrowBack from '@/common/arrow-back.svg' // 引入svg
import { loadScript } from '@/common/utils.js' // 引入js中的函数
区别于开发阶段,构建工具还针对生产环境提供了丰富的构建能力,能将业务源码进行压缩、tree-shaking 优化,uglify 混淆、兼容、extract 抽离等处理,成为适用于生产环境的最优代码。
构建出来的生产环境JS:
!function(){"use strict";function t(t){if(null==t)return-1;var e=Number(t);return isNaN(e)?-1:Math.trunc(e)}function e(t){var e=t.name;return/(\.css|\.js|\.woff2)/.test(e)&&!/(\.json)/.test(e)}function n(t){var e="__";return"".concat(t.protocol).concat(e).concat(t.name).concat(e).concat(t.decodedBodySize).concat(e).concat(t.encodedBodySize).concat(e).concat(t.transferSize).concat(e).concat(t.startTime).concat(e).concat(t.duration).concat(e).concat(t.requestStart).concat(e).concat(t.responseEnd).concat(e).concat(t.responseStart).concat(e).concat(t.secureConnectionStart)}var r=function(){return/WindVane/i.test(navigator.userAgent)};function o(){return r()}function c(){return!!window.goldlog}var i=function(){return a()},a=function(){var t=function(t){var e=document.querySelector('meta[name="'.concat(t,'"]'));if(!e)return;return e.getAttribute("content")}("data-spm"),e=document.body&&document.body.getAttribute("data-spm");return t&&e&&"".concat(t,".")......
构建出来的生产环境CSS:
@charset "UTF-8";.free-shipping-block{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-box-align:center;-ms-flex-align:center;-webkit-align-items:center;align-items:center;background-color:#ffe8da;background-position:100% 100%;background-repeat:no-repeat;background-size:200px 100px;border-radius:8px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;margin-top:24px;padding:12px}.free-shipping-block .content{-webkit-box-flex:1;-ms-flex-positive:1;color:#4b1d1f;-webkit-flex-grow:1;flex-grow:1;font-size:14px;margin-left:8px;margin-top:0 }.free-shipping-block .content .desc img{padding-top:2px;vertical-align:text-top;width:120px}.free-shipping-block .co.....
构建出来的生产环境HTML:
<html><head><script defer="defer" src="/build/xxx.js"></script>
<link href="/build/xxx.css" rel="stylesheet"></head><body><div id="root"></div>
</body></html>
代码部署
至此,我们就得到了网页入口所需所有资源(HTML 及相应的 CSS、JS、其他静态资源),双击 html 文件在浏览器中打开即可本地访问我们的页面,哈!前端就是这么简单!那么可以考虑下一步了,我们还得让测试、产品、运营以及网络上的全球用户都能访问到我们的页面吧?那只在本地运行起来玩一玩儿(doge)肯定不行的,起码得把这些资源全部上传到网络上。
在开发阶段访问网页,一种是在本地运行的开发服务器上, IP 通常是 127.0.0.1/本机IPv4/IPv6,端口号自定,通过 IP + Port + Path 形式访问;一种是手动将资源上传至服务器,其他人拿到服务器 IP + Port + Path 访问页面(关于网站域名申请备案、映射绑定本文省略一万个字...)。后者可通过专门的发布平台(CI/CD)将整个流程自动化,发布平台做的事情简单来说有:
检查分支提交信息、必须配置、依赖合规检查等系列卡口;
运行脚本,执行事先配置好的依赖安装及打包构建指令,开启云构建,安装项目依赖,并打包一份生产环境产物(说白了云构建这一步就跟我们刚刚 git clone 项目到本地初始化运行、本地 build 是一样的);
将产物上传至 CDN。
至此,用户就可以在浏览器输入网址访问我们的页面了,服务器返回 HTML,HTML 中引用 CDN 上的资源,交给端(浏览器)去把页面渲染出来。
发布对外
对于上万(百万)DAU 的页面来说,超多的访问量和极致性能指标,要求我们对页面的迭代修改在正式对外访问前,必须考虑安全发布与用户体验。
▐ 迭代更新
index.css:
.foo {
background-color: red;
}
对于 index.css,如果用户每次打开页面都要重新发起对该文件的请求,不仅浪费带宽而且用户还要多等待一段下载时间,完全可以利用 HTTP 缓存中的强缓存将静态资源缓存在浏览器本地,使用户更快看到页面(快体现在浏览器直接从 memory/dist cache 中读取文件,省去了下载时间)。
Cache-Control: max-age=2592000,s-maxage=86400
对于静态资源,服务器往往设置一个非常大的缓存过期时间以充分利用缓存,这样浏览器就彻底不用发起请求了。但是浏览器都不发请求了,如果我们页面有更新/bug 修复该怎么办呢?很容易想到的办法是在资源 url 上拼接版本号,如:
<html>
<head>
<script defer="defer" src="https://x.alicdn.com/build/foo.js?t=0.0.1"></script>
<link href="https://x.alicdn.com/build/index.css?t=0.0.1" rel="stylesheet">
</head>
<body>
<div class="foo"></div>
</body>
</html>
<html>
<head>
<script defer="defer" src="https://x.alicdn.com/build/foo.js?t=0.0.2"></script>
<link href="https://x.alicdn.com/build/index.css?t=0.0.2" rel="stylesheet">
</head>
<body>
<div class="foo"></div>
</body>
</html>
但这样做存在一个问题,HTML 同时引用了多个文件,如果在一次迭代中只变更了其中的某个文件,其他文件没做修改,统一加版本号的方法岂不是连带让其他文件的本地缓存都失效了!
为解决这个问题,就得实现文件级别粒度的缓存控制,我们很容易想到 HTTPS 中的数据摘要算法,根据文件内容生成唯一 hash 值,文件无修改 hash 值不变,这样就能精确到单个文件的缓存了:
<html>
<head>
<!-- foo.js 无修改继续使用缓存 -->
<script defer="defer" src="https://x.alicdn.com/build/foo.js"></script>
<!-- index.css 改了样式,得请求更新后的文件并缓存 -->
<link href="https://x.alicdn.com/build/index_1i0gdg6ic.css" rel="stylesheet">
</head>
<body>
<div class="foo"></div>
</body>
</html>
<html>
<head>
<!-- 资源路径更新,请求新的资源 -->
<script defer="defer" src="https://x.alicdn.com/0.0.2/build/foo.js"></script>
<!-- 资源路径更新,请求新的资源 -->
<link href="https://x.alicdn.com/0.0.2/build/index.css" rel="stylesheet">
</head>
<body>
<div class="foo"></div>
</body>
</html>
▐ 动静分离
现代前端部署方案,往往将静态资源(JS、CSS、图片等)往往上传到离用户更近的 CDN 上,这些资源基本不怎么改变,需要充分利用缓存提高缓存命中率;而动态页面(HTML)用户数据千人千面、为 SEO 做 SSR,以及为了性能同构,往往存放在离业务服务器更近的地方,取数查数注入数据更快。
两种资源分布在不同地方,那么静态资源就以 CDN 链接引入的方式写于 HTML 中,那么问题来了,我们在更新页面时先发布静态资源还是先发布页面呢?
1)先发布页面,后发布资源:
<html>
<head>
<!-- 资源还没发布完 -->
<script defer="defer" src="https://x.alicdn.com/0.0.1/build/foo.js"></script>
<link href="https://x.alicdn.com/0.0.1/build/index.css" rel="stylesheet">
</head>
<body>
<!-- 页面修改了 -->
<div class="bar"></div>
</body>
</html>
静态资源发布完成前,期间用户访问到新的页面结构,但是静态资源还是老的,用户可能会看到一个样式错乱的页面,也可能因旧的 JS 脚本找不到元素节点而执行错误的白屏页面,不可行。
2)先发布资源,再发布页面:
<html>
<head>
<!-- 资源已发布 -->
<script defer="defer" src="https://x.alicdn.com/0.0.2/build/foo.js"></script>
<link href="https://x.alicdn.com/0.0.2/build/index.css" rel="stylesheet">
</head>
<body>
<!-- 页面还没发布 -->
<div class="foo"></div>
</body>
</html>
页面发布完成前,页面结构没变,而资源是新的了,如果用户此前访问过,本地存在老资源的缓存,那么他看到的页面是正常的,否则访问到旧页面却加载新资源,还会出现上述一样的问题,要么样式错乱、要么 JS 执行错误导致白屏,不可行。
所以先部署谁都不行!这也是为啥古早上线项目时要辛苦程序员大佬们半夜偷偷上,挑流量低谷时上的缘故了,毕竟影响面能小些。但是哇,这对于大厂来说可没有绝对的低峰期只有相对低峰期。但哪怕是相对低峰期,对于做事追求极致的我们,也是不可接受的!
上面的问题其实是覆盖式发布导致的,当待发布资源覆盖已发布资源时就会出现问题,对应的解决办法就是非覆盖式发布,通过文件路径添加版本号或文件名加 hash,发布新的资源时不覆盖旧的资源,先全量发布静态资源再逐步灰度推全量发布页面,整个问题就完美解决了。
所以,关于静态资源优化基本要做到:
配置超长缓存过期时间,提高缓存命中率,节省带宽;
采用内容摘要或带版本号的文件路径作为缓存更新依据,做到精确缓存控制;
静态资源 CDN 部署,节省网络请求传输路径,缩短请求响应时间;
以非覆盖式发布更新资源,平滑过渡升级。
至此,前端大佬们辛苦码的代码经过不断迭代、(云)构建、产物资源部署,发布对外,全球用户就可以在互联网上,体验我们的产品,愉快冲浪了~
[02] Understanding the critical path
https://web.dev/learn/performance/understanding-the-critical-path
[03] 大公司里怎样开发和部署前端代码?
https://www.zhihu.com/question/20790576
本文分享自微信公众号 - 大淘宝技术(AlibabaMTT)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。