本文使用「署名 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 可以生效的前提下,咱們知道不少流行的應用客戶端爲了性能優化,都爲資源(甚至包含頁面)設置了很長的有效期,能夠說這個方案並非一個頗有效的方案。
因此,假設你採起相似這種方案,你必須確保下面四個條件都生效,才能達到你的目的:
聽起來是否是很魔幻。
那麼有沒有什麼簡單可靠的方案能夠解決這個問題呢?
有,讓資源在前端層面進行自動切換。
經過在前端環境監聽資源加載錯誤信息,並根據必定策略自動加載其餘位置的資源,實現前端依賴的資源在前端(用戶側)進行自動切換,達到前端資源高可用的目的,減小因前端資源加載失敗而致使的服務不可用和用戶流失。
爲了更直觀的演示方案如何生效,我這裏使用 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、編程上的一些問題,也會在羣裏不按期的分享一些技術沙龍的資料。
喜歡折騰的小夥伴歡迎掃碼添加好友。(請註明來源和目的,不然不會經過審覈)