alifeiliu

alifeiliu

哪有什么无限进步,能进一步是一步
github
x
discord server
email
strava
bilibili

我們寫的代碼是如何被用戶看到的

“用戶在瀏覽器地址輸入 URL 之後發生了什麼?” 這個問題對於我們前端開發者來說簡直是典中典了,是前端基礎,也是工作面試八股,更是性能優化依據。但本文想分享的重點不是之後發生了什麼,而是之前發生了什麼,即我們平時碼出來的代碼經歷了哪些步驟處理,成為互聯網用戶能打開瀏覽的頁面?我們又是如何合理的更新網頁的?

前一個問題涉及開發與部署,後一個問題涉及發布。下面我將會從網頁入口、開發、部署與發布這 4 Part 進行講解

Part 1 網頁入口#

這一 part 還是簡單對用戶看到的網頁由什麼構成,瀏覽器又做了哪些工作才讓這些構成部分呈現在用戶面前的網頁進行簡單介紹。首先,這,是 bilibili 主頁面:

image

一個內容豐富、設計美觀、互動友好的網頁離不開前端三劍客 HTML、CSS、JS 以及圖片、字體等資源文件:

  • HTML 決定網頁內容,是用戶訪問任意一個網站的入口,既可以在 HTML 中直接編寫 CSS、JS 代碼,也可以將 CSS、JS 代碼寫在單獨的文件中在 HTML 引入
  • CSS 作用於網頁樣式
  • JS 實現用戶互動
<!-- 網頁入口 HTML 的基本結構 -->
<!DOCTYPE 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://s.alicdn.com/@g/msite/msite-rax-detail-cdn/1.0.73/web/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://s.alicdn.com/@g/msite/msite-rax-detail-cdn/1.0.73/web/screen.js"></script>
    </body>
</html>

用戶訪問任意網站之前,要先在地址欄輸入一個有效地址,接著瀏覽器會向服務器發起請求,去拿到該地址對應的網頁入口文件即 "xxx.html",打開瀏覽器 Network 控制台便可以看到,這一定是瀏覽器第一個接收到的響應內容

緊接著,瀏覽器解析 HTML 代碼,識別到其他資源發起更多請求,經過各種類型資源的加載、解析、執行(非必需)逐步成為用戶眼前看到的完整頁面,講到這裡就不得不提到 CRP(Critical Rendering Path,關鍵路徑渲染),即瀏覽器將 HTML、JS、CSS 代碼轉換成屏幕上用戶可見像素必經的一系列關鍵步驟,如下:

image

  • 網絡下載 HTML,解析 HTML 代碼構建 DOM
  • 網絡下載 CSS,解析 CSS 代碼構建 CSSOM
  • 網絡下載 JS,解析執行 JS 代碼,可能會修改 DOM 或 CSSOM
  • 待 DOM & CSSOM “定型”,瀏覽器根據 DOM 和 CSSOM 構造 Render Tree
  • 重排過程計算每個元素節點所在位置與樣式
  • 重繪過程將繪製真實像素於屏幕上

至此,網頁呈現在用戶面前,進行下一步的瀏覽和操作

Part 2 開發階段#

看完上一 part ,想必你也知道瀏覽器搜索 - 呈現網頁是怎麼回事了,這一 part 將簡單介紹現代化網頁開發過程

代碼編寫#

在網頁內容越來越豐富,網頁功能越來越複雜的今天,前端三劍客 HTML、CSS、JS 代碼都變得龐大起來,顯然,將 CSS、JS 代碼都組織在單一 HTML 文件裡不再合適,我們也並不會再以傳統的方式直接去編寫 HTML、CSS、JS 代碼,取而代之得是使用各種 UI 框架(如 React/Vue/Angular 等)進行組件式開發,CSS 預處理器(如 Sass/Less/Stylus 等) 等去編寫樣式

image (1)

工程能力#

利用前端構建工具(如 webpack/vite/Rollup 等)組織各種類型的文件,並提供模塊化、自動化、優化、轉義等構建能力,進行本地開發與生產打包

image (2)

其中,有必要對模塊化進行說明,它帶來的好處就是,在開發階段,我們可以將不同類型的文件統一視為模塊處理,模塊成為模塊系統中的第一公民,它們之間可以相互引用,至於不同文件類型模塊之間的差異,交由構建工具去解決

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!important}.free-shipping-block .content .desc img{padding-top:2px;vertical-align:text-top;width:120px}.free-shipping-block .co.....

構建工具輸出的 HTML 代碼自動引入了 JS、CSS 資源:

<!doctype 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>

Part 3 代碼部署#

至此,我們就得到了網頁入口所需所有資源(HTML 及相應的 CSS、JS、其他靜態資源),雙擊 html 文件在瀏覽器中打開即可本地訪問我們的頁面,哈!前端就是這麼簡單!

那麼可以考慮下一步了,我們還得讓測試、產品、運營以及網絡上的全球用戶都能訪問到我們的頁面吧?那只在本地運行起來玩一玩兒(doge)肯定不行的,起碼得把這些資源全部上傳到網絡上

在開發階段網頁的訪問,在本地運行的開發服務器上, IP 通常是 127.0.0.1,端口號自定,通過 IP + Port + Path 形式訪問,一種是手動將資源上傳至服務器,其他人拿到服務器 IP + Port + Path 訪問頁面(關於網站域名申請備案、映射綁定本文省略一萬個字...),另一種是通過專門的發布平台將整個流程自動化,發布平台做的事情簡單來說有:

image (3)

  • 檢查分支提交信息、必須配置、依賴合規檢查等系列卡口
  • 運行腳本,執行事先配置好的依賴安裝及打包構建指令,開啟雲構建,安裝項目依賴,並打包一份生產環境產物(說白了雲構建這一步就跟我們剛剛 git clone 項目到本地初始化運行、本地 build 是一樣的)
  • 將產物上傳至 CDN

至此,用戶就可以在瀏覽器輸入網址訪問我們的頁面了,服務器返回 HTML,HTML 中引用 CDN 上的資源,交給端(瀏覽器)去把頁面渲染出來

Part 4 發布對外#

迭代更新#

對於上萬(百萬)DAU 的頁面來說,超多的訪問量和極致性能指標,要求我們對頁面的迭代修改在正式對外訪問前,必須考慮安全發布與用戶體驗

.foo {
  background-color: red;
}

對於 index.css,如果用戶每次打開頁面都要重新發起對該文件的請求,不僅浪費帶寬而且用戶還要多等待一段下載時間,完全可以利用 HTTP 緩存中的強緩存將靜態資源緩存在瀏覽器本地,使用戶更快看到頁面(快體現在瀏覽器直接從 memory/dist cache 中讀取文件,省去了下載時間)

<!-- 設置緩存有效時間 -->
Cache-Control: max-age=2592000,s-maxage=86400

對於靜態資源,服務器往往設置一個非常大的緩存過期時間以充分利用緩存,這樣瀏覽器就徹底不用發起請求了。但是瀏覽器都不發請求了,如果我們頁面有更新 /bug 修復該怎麼辦呢?很容易想到的辦法是在資源 url 上拼接版本號,如:

<!-- 通過版本號更新 -->
<!doctype html>
<html>
  <head>
    <script defer="defer" src="https://s.alicdn.com/build/foo.js?t=0.0.1"></script>
    <link href="https://s.alicdn.com/build/index.css?t=0.0.1" rel="stylesheet">
  </head>
  <body>
    <div class="foo"></div>
  </body>
</html>

下次更新時更換版本號就能強制讓瀏覽器重新發起新的請求:

<!-- 0.0.2迭代版本 -->
<!doctype html>
<html>
  <head>
    <script defer="defer" src="https://s.alicdn.com/build/foo.js?t=0.0.2"></script>
    <link href="https://s.alicdn.com/build/index.css?t=0.0.2" rel="stylesheet">
  </head>
  <body>
    <div class="foo"></div>
  </body>
</html>

但這樣做存在一個問題,HTML 同時引用了多個文件,如果在一次迭代中只變更了其中的某個文件,其他文件沒做修改,統一加版本號的方法豈不是連帶讓其他文件的本地緩存都失效了!

為解決這個問題,就得實現文件級別粒度的緩存控制,我們很容易想到 HTTPS 中的數據摘要算法,根據文件內容生成唯一 hash 值,文件無修改 hash 值不變,這樣就能精確到單個文件的緩存了:

<!-- 通過文件內容摘要控制更新 -->
<!doctype html>
<html>
  <head>
    <!-- foo.js 無修改繼續使用緩存 -->
    <script defer="defer" src="https://s.alicdn.com/build/foo.js"></script>
    <!-- index.css 改了樣式,得請求更新後的文件並緩存 -->
    <link href="https://s.alicdn.com/build/index_1i0gdg6ic.css" rel="stylesheet">
  </head>
  <body>
    <div class="foo"></div>
  </body>
</html>

或者通過迭代版本號加入資源路徑 Path 的方式:

<!-- 通過資源路徑控制更新 -->
<!doctype html>
<html>
  <head>
    <!-- 資源路徑更新,請求新的資源 -->
    <script defer="defer" src="https://s.alicdn.com/0.0.2/build/foo.js"></script>
    <!-- 資源路徑更新,請求新的資源 -->
    <link href="https://s.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 中,那麼問題來了,我們在更新頁面時先發布靜態資源還是先發布頁面呢?
先發布頁面,後發布資源:

<!-- 新頁面,老資源 -->
<!doctype html>
<html>
  <head>
    <!-- 資源還沒發布完 -->
    <script defer="defer" src="https://s.alicdn.com/0.0.1/build/foo.js"></script>
    <link href="https://s.alicdn.com/0.0.1/build/index.css" rel="stylesheet">
  </head>
  <body>
    <!-- 頁面修改了 -->
    <div class="bar"></div>
  </body>
</html>

態資源發布完成前,期間用戶訪問到新的頁面結構,但是靜態資源還是老的,用戶可能會看到一個樣式錯亂的頁面,也可能因舊的 JS 腳本找不到元素節點而執行錯誤的白屏頁面,不可行 🙅

先發布資源,再發布頁面:

<!-- 老頁面,新資源 -->
<!doctype html>
<html>
  <head>
    <!-- 資源已發布 -->
    <script defer="defer" src="https://s.alicdn.com/0.0.2/build/foo.js"></script>
    <link href="https://s.alicdn.com/0.0.2/build/index.css" rel="stylesheet">
  </head>
  <body>
    <!-- 頁面還沒發布 -->
    <div class="foo"></div>
  </body>
</html>

頁面發布完成前,頁面結構沒變,而資源是新的了,如果用戶此前訪問過,本地存在老資源的緩存,那麼他看到的頁面是正常的,否則訪問到舊頁面卻加載新資源,還會出現上述一樣的問題,要麼樣式錯亂、要麼 JS 執行錯誤導致白屏,不可行 🙅

所以先部署誰都不行!這也是為啥古早上線項目時要辛苦程序員大佬們半夜偷偷上,挑流量低谷時上的緣故了,畢竟影響面能小些。但是哇,這對於大廠來說可沒有絕對的低峰期只有相對低峰期。但哪怕是相對低峰期,對於做事追求極致的我們,也是不可接受的!

上面的问题其实是覆盖式发布导致的,当待发布资源覆盖已发布资源时就会出现问题,对应的解决办法就是非覆盖式发布,通过文件路径添加版本号或文件名加 hash,发布新的资源时不覆盖旧的资源,先全量发布静态资源再逐步灰度推全量发布页面,整个问题就完美解决了

所以,關於靜態資源優化基本要做到:

  • 配置超長緩存過期時間,提高緩存命中率,節省帶寬
  • 采用內容摘要或帶版本號的文件路徑作為緩存更新依據,做到精確緩存控制
  • 靜態資源 CDN 部署,節省網絡請求傳輸路徑,縮短請求響應時間
  • 以非覆蓋式發布更新資源,平滑過渡升級

至此,前端大佬們辛苦碼的代碼經過不斷迭代、(雲)構建、產物資源部署,發布對外,全球用戶就可以在互聯網上,體驗我們的產品,愉快衝浪了~

參考鏈接#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。