簡單策略讓前端資源實現高可用

本文使用「署名 4.0 國際 (CC BY 4.0)」許可協議,歡迎轉載、或從新修改使用,但須要註明來源。 署名 4.0 國際 (CC BY 4.0)html

本文做者: 蘇洋前端

建立時間: 2019年05月14日 統計字數: 6024字 閱讀時間: 13分鐘閱讀 本文連接: soulteary.com/2019/05/14/…vue


簡單策略讓前端資源實現高可用

前幾天有朋友問我,曾經在前公司裏使用過的前端資源高可用方案是怎麼作的。資源高可用聽起來應該是後端、運維同窗的「份內之事」。可是前端資源的高可用並無那麼簡單,在當前複雜的網絡環境下,你是期望用戶多刷新幾回、仍是指望用戶把Wi-Fi切換爲4G,撞大運解決問題?獲客成本如此之高的今天,放棄用戶是不明智的。webpack

想到許久沒有寫前端相關的文章了,決定在這裏簡單聊聊。但願能幫助到創業階段的公司和團隊。nginx

在聊技術細節以前,咱們先聊聊「什麼是前端資源高可用」。git

資源高可用和前端有什麼關係?

前端資源高可用這個需求,對於「大廠」的同窗來講應該很陌生。github

由於對於大公司來講,有大量冗餘的雲主機資源能夠爲業務團隊提供,而且會配套必定規模的運維團隊。當監控系統發現線上出現資源不可用的狀況時,系統可以根據策略自動切換問題資源到備份資源,而有些不能自動切換的服務,則會有值守的運維同窗,在第一時間手動進行切換,保障業務的高可用。web

網絡服務高可用基本玩法

而小一些規模的創業公司就沒那麼幸運了,資源相對緊張,甚至沒有完善的監控措施,更別提配一隻相對完善的運維團隊了。docker

或許會有人認爲,將靜態資源扔到 CDN 上後就一勞永逸了。然而現實世界中,網絡環境十分複雜,相同主機在不一樣線路、不一樣地區、不一樣時間段的可用性和訪問質量是不一樣的,因此使用 CDN 不是解決這個問題的銀彈,可是同時使用多個 CDN 或許是當前階段比較通用的方案。編程

好比默認不一樣地域的用戶經過不一樣線路訪問網站,若是其中一條線路出現問題,那麼一部分用戶就沒法訪問網站提供的服務。

常見用戶訪問狀況

這個時候,咱們一般會使用切換請求資源服務器的方法來解決問題,好比下面這樣。

如何解決用戶可用性

當某條 CDN / 服務線路不正常的時候,咱們能夠經過切換域名來解決資源獲取不到的問題,可是別忘記一件很重要的事情:

域名生效須要時間、多地域生效週期漫長,在這個切換域名的時間窗口內,你的服務質量將會持續受到影響。

而且這個方案的資源切換動做一般會在後端進行,而此時頁面已經推送到用戶側,資源已經不可用,用戶須要刷新後纔有可能請求到新的資源地址,而且是在 DNS 可以生效的前提下,咱們知道不少流行的應用客戶端爲了性能優化,都爲資源(甚至包含頁面)設置了很長的有效期,能夠說這個方案並非一個頗有效的方案。

因此,假設你採起相似這種方案,你必須確保下面四個條件都生效,才能達到你的目的:

  • 你的監控系統發現了問題,並自動進行了資源切換。
  • 你的業務負責人,發現了問題,並手動進行資源切換。
  • 你成功切換了資源,而且 DNS 快速生效(網絡層、客戶端層)。
  • 你的用戶在你切換資源、DNS 生效後,恰如其分的刷新了頁面,而不是直接離開。

聽起來是否是很魔幻。

那麼有沒有什麼簡單可靠的方案能夠解決這個問題呢?

有,讓資源在前端層面進行自動切換。

方案簡介

經過在前端環境監聽資源加載錯誤信息,並根據必定策略自動加載其餘位置的資源,實現前端依賴的資源在前端(用戶側)進行自動切換,達到前端資源高可用的目的,減小因前端資源加載失敗而致使的服務不可用和用戶流失。

環境模擬

爲了更直觀的演示方案如何生效,我這裏使用 Docker 作一個常見場景的模擬。

模擬多個網絡

咱們先建立一個 docker-compose.yml ,裏面包含下面的內容。

version: '3'

services:

  web:
    image: ${NGX_IMAGE}
4    expose:
      - 80
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.frontend.rule=Host:${MAIN_HOST}"
      - "traefik.frontend.entryPoints=${SUPPORT_PROTOCOL}"
    volumes:
      - ./public/${MAIN_HOST}:/usr/share/nginx/html
    extra_hosts:
      - "${MAIN_HOST}:127.0.0.1"

  cdn1:
    image: ${NGX_IMAGE}
    expose:
      - 80
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.frontend.rule=Host:${CDN_HOST1}"
      - "traefik.frontend.entryPoints=${SUPPORT_PROTOCOL}"
      - "traefik.frontend.headers.customResponseHeaders=Access-Control-Allow-Origin:*"
    volumes:
      - ./public/${CDN_HOST1}:/usr/share/nginx/html
    extra_hosts:
      - "${CDN_HOST1}:127.0.0.1"

  cdn2:
    image: ${NGX_IMAGE}
    expose:
      - 80
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.frontend.rule=Host:${CDN_HOST2}"
      - "traefik.frontend.entryPoints=${SUPPORT_PROTOCOL}"
      - "traefik.frontend.headers.customResponseHeaders=Access-Control-Allow-Origin:*"
    volumes:
      - ./public/${CDN_HOST2}:/usr/share/nginx/html
    extra_hosts:
      - "${CDN_HOST2}:127.0.0.1"

networks:
  traefik:
    external: true
複製代碼

能夠看到,編排文件裏面定義了一個應用網站,和兩個 CDN 服務,爲了更接近真實場景。其中一個 CDN 和應用網站根域名相同、另一個採起徹底不一樣的域名,好比下面這樣。

# 默認使用的鏡像
NGX_IMAGE=nginx:1.15.8-alpine
# 支持訪問的協議
SUPPORT_PROTOCOL=https,http

# 主站點的域名
MAIN_HOST=demo.lab.io
# 模擬根域名相同的CDN
CDN_HOST1=demo-cdn.lab.io
# 模擬根域名不一樣的CDN
CDN_HOST2=demo.cdn2.io
複製代碼

將上面的內容保存爲 .env ,並將上面內容中的域名綁定到本地以後,執行 docker-compose up ,就能夠開始實戰了。

模擬常規場景

執行 docker-compose up 以後,咱們會看到 Docker 自動幫咱們建立了幾個目錄。

./public
├── demo-cdn.lab.io
├── demo.cdn2.io
└── demo.lab.io
複製代碼

咱們在 demo.lab.io 目錄下建立 index.html 文件,做爲應用入口。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script src="assets/app.js"></script>
</head>
<body>

</body>
</html>
複製代碼

而後在 ./demo.lab.io/public/assets/app.js 建立一個腳本文件,隨便寫點什麼,模擬被加載的資源。

document.addEventListener('DOMContentLoaded', function () {
    var p = document.createElement('p');
    p.innerText = 'script excute success.';
    document.body.appendChild(p);
});
複製代碼

當咱們訪問 http://demo.lab.io/index.html 的時候,不出意外,將會看到 由腳本輸出的 script excute success. 內容。

咱們將 ./public/demo.lab.io/assets/app.js 複製到 ./public/demo-cdn.lab.io/assets/app.js./public/demo.cdn2.io/assets/app.js 中,模擬資源分發到 CDN 的場景。

最簡單的技術實現

先將上面請求的資源地址修改成「CDN」的地址,驗證一下「CDN」服務是否可用。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script src="//demo-cdn.lab.io/assets/app.js"></script>
</head>
<body>

</body>
</html>
複製代碼

而後經過刪除 ./public/demo-cdn.lab.io/assets/app.js 這個腳本,模擬 CDN 資源失效的場景。

若是你的瀏覽器沒有奇怪的緩存行爲,你將會獲得一個空白的頁面,以及一行報錯信息:

default.html:8 GET http://demo-cdn.lab.io/assets/app.js 404 (Not Found)
複製代碼

若是碰到域名解析錯誤的場景下,咱們會得到另一種錯誤信息:

GET http://demo-cdn.lab.io/assets/app.js net::ERR_NAME_NOT_RESOLVED
複製代碼

這個時候,咱們能夠在頁面上作一些修改,讓它可以在資源加載出錯的時候,將資源切換到另一個 CDN 資源上,好比這樣:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script>
        function loadOthers(resource) {
            var script = document.createElement('script');
            script.src = resource.src.replace('demo-cdn.lab.io','demo.cdn2.io');
            document.head.appendChild(script);
        }
    </script>
    <script src="//demo-cdn.lab.io/assets/app.js" onerror="loadOthers(this)"></script>
</head>
<body>

</body>
</html>
複製代碼

再次打開地址,你會發現頁面又正常了。

進階版本

上面場景,咱們模擬了常規場景下前端自動切換資源的方式。

接下來咱們來作一些小小的優化,讓腳本加載支持更多的資源地址,達到更高的可用性。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script>
        function loadResource(links, fnSuccess, fnError) {
            var script = document.createElement('script');
            script.onerror = function () {
                document.head.removeChild(script);
                fnError();
            };
            script.onload = fnSuccess
            script.src = links.shift();
            document.head.appendChild(script);
        }

        function autoSwitch(resourceList) {
            var resource = resourceList.shift();
            loadResource([resource], function (success) {
                console.log('loaded');
            }, function (err) {
                console.log('load error')
                autoSwitch(resourceList);
            });
        }
    </script>
</head>
<body>
    <script>
        var resourceList = [
            'http://demo-cdn.lab.io/assets/app.js',
            'http://demo.cdn2.io/assets/app.js',
            'assets/app.js',
        ];

        autoSwitch(resourceList);
    </script>
</body>
</html>
複製代碼

上面的實現中,咱們將資源加載寫的更加通用,而且添加了加載成功、失敗的回調,以及額外作了一個自動切換資源的函數,並將頁面腳本資源加載交給了腳本去處理。

這個方案已經可以解決多數場景下的問題了,可是若是你的資源之間存在依賴關係,又該怎麼處理呢?

結合資源加載器使用

咱們以 AMD 模塊規範爲例,聊聊如何結合 requirejs 使用資源自動切換。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>

    <script src="assets/require-v2.3.6.min.js"></script>

    <script>
        function autoSwitch(resourceList) {
            var resource = resourceList.shift();
            requirejs([resource], function (success) {
                console.log('loaded');
            }, function (err) {
                console.log('load error')
                autoSwitch(resourceList);
            });
        }
    </script>
</head>
<body>
    <script>
        var resourceList = [
            'http://demo-cdn.lab.io/assets/app.js',
            'http://demo.cdn2.io/assets/app.js',
            'assets/app.js',
        ];

        autoSwitch(resourceList);
    </script>
</body>
</html>
複製代碼

requirejs 引入頁面,而後使用 requirejs 方法替換 loadResource 方法後,你會發現彷佛一切沒有什麼不一樣。

可是你其實能夠經過配置 requirejs.config 來讓資源在加載的過程當中,將依賴資源先進行下載和初始化,舉兩個實際的例子:

requirejs.config({
    map: {
        // 這是一個 hack 用法,具體含義參考官方 API 文檔
        '*': { 'http://demo.cdn2.io/assets/app.js': 'lodash' },
    }
});
複製代碼
requirejs.config({
    shim:{
        // 或者這樣聲明
        'http://demo.cdn2.io/assets/app.js':{
            deps:['vue']
        }
    }
});
複製代碼

固然,你也能夠改造 autoSwitch 函數,本身動態維護依賴關聯。

其餘的坑

講到這裏,資源自動加載幾乎講完了,可是實際上還存在一些額外的坑。

好比結合當前最流行的構建工具 webpack 使用,圖片資源是一次性寫死的,須要支持動態化。

17年的時候,我曾經提交了一個解決方案,有興趣的同窗能夠圍觀一下:github.com/soulteary/w…,主要解決了 ** Not generating ouput with multiple entries** 的問題。

最後

許多看似高大上的方案,本質其實都十分簡單。與其追求高大上的概念,不如靜下心來,踏實鑽研細節,思考技術到底該如何有效的服務業務、產生價值。

—EOF


我如今有一個小小的折騰羣,裏面彙集了一些喜歡折騰的小夥伴。

在不發廣告的狀況下,咱們在裏面會一塊兒聊聊軟件、HomeLab、編程上的一些問題,也會在羣裏不按期的分享一些技術沙龍的資料。

喜歡折騰的小夥伴歡迎掃碼添加好友。(請註明來源和目的,不然不會經過審覈)

關於折騰羣入羣的那些事

相關文章
相關標籤/搜索