mirror of
https://github.com/xfgryujk/blivechat.git
synced 2024-12-25 20:30:28 +08:00
前端动态获取后端地址、支持故障转移
This commit is contained in:
parent
99d3b567da
commit
cefca66d76
@ -270,9 +270,9 @@ temperature = 0.4
|
||||
|
||||
# 用于服务发现返回的后端端点
|
||||
[registered_endpoints]
|
||||
# 1 = https://api1.blive.chat
|
||||
1 = http://localhost:12450
|
||||
|
||||
|
||||
# 允许跨域的源,正则表达式
|
||||
[cors_origins]
|
||||
# 1 = https://(?:|.+\.)blive\.chat
|
||||
1 = http://localhost(:\d+)
|
||||
|
@ -1,2 +1,6 @@
|
||||
# 第三方库用CDN引入
|
||||
LIB_USE_CDN=false
|
||||
# production环境生成source map
|
||||
PROD_SOURCE_MAP=true
|
||||
# 动态发现后端endpoint
|
||||
BACKEND_DISCOVERY=false
|
||||
|
@ -1,3 +1,4 @@
|
||||
NODE_ENV=production
|
||||
LIB_USE_CDN=true
|
||||
PROD_SOURCE_MAP=false
|
||||
BACKEND_DISCOVERY=true
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "blivechat",
|
||||
"version": "1.9.2",
|
||||
"version": "1.9.3-dev",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
@ -15,6 +15,7 @@
|
||||
"downloadjs": "^1.4.7",
|
||||
"element-ui": "^2.15.13",
|
||||
"lodash": "^4.17.21",
|
||||
"opossum": "^8.3.0",
|
||||
"pako": "^2.1.0",
|
||||
"vue": "^2.7.14",
|
||||
"vue-i18n": "^8.28.2",
|
||||
|
165
frontend/src/api/base.js
Normal file
165
frontend/src/api/base.js
Normal file
@ -0,0 +1,165 @@
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import CircuitBreaker from 'opossum'
|
||||
|
||||
axios.defaults.timeout = 10 * 1000
|
||||
|
||||
export const apiClient = axios.create({
|
||||
timeout: 10 * 1000,
|
||||
})
|
||||
|
||||
export let getBaseUrl
|
||||
if (!process.env.BACKEND_DISCOVERY) {
|
||||
const onRequest = config => {
|
||||
config.baseURL = getBaseUrl()
|
||||
return config
|
||||
}
|
||||
|
||||
const onRequestError = e => {
|
||||
throw e
|
||||
}
|
||||
|
||||
apiClient.interceptors.request.use(onRequest, onRequestError, { synchronous: true })
|
||||
|
||||
getBaseUrl = function() {
|
||||
return window.location.origin
|
||||
}
|
||||
} else {
|
||||
const onRequest = config => {
|
||||
let baseUrl = getBaseUrl()
|
||||
if (baseUrl === null) {
|
||||
throw new Error('No available endpoint')
|
||||
}
|
||||
config.baseURL = baseUrl
|
||||
return config
|
||||
}
|
||||
|
||||
const onRequestError = e => {
|
||||
throw e
|
||||
}
|
||||
|
||||
const onResponse = response => {
|
||||
let promise = Promise.resolve(response)
|
||||
let baseUrl = response.config.baseURL
|
||||
let breaker = getOrAddCircuitBreaker(baseUrl)
|
||||
breaker.fire(promise).catch(() => {})
|
||||
return response
|
||||
}
|
||||
|
||||
const onResponseError = e => {
|
||||
let promise = Promise.reject(e)
|
||||
if (!e.response || (500 <= e.response.status && e.response.status < 600)) {
|
||||
let baseUrl = e.config.baseURL
|
||||
let breaker = getOrAddCircuitBreaker(baseUrl)
|
||||
breaker.fire(promise).catch(() => {})
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
apiClient.interceptors.request.use(onRequest, onRequestError, { synchronous: true })
|
||||
apiClient.interceptors.response.use(onResponse, onResponseError)
|
||||
|
||||
const DISCOVERY_URLS = process.env.NODE_ENV === 'production' ? [
|
||||
// 只有公共服务器会开BACKEND_DISCOVERY,这里可以直接跨域访问
|
||||
'https://api1.blive.chat/api/endpoints',
|
||||
'https://api2.blive.chat/api/endpoints',
|
||||
] : [
|
||||
`${window.location.origin}/api/endpoints`,
|
||||
'http://localhost:12450/api/endpoints',
|
||||
]
|
||||
let baseUrls = process.env.NODE_ENV === 'production' ? [
|
||||
'https://api1.blive.chat',
|
||||
'https://api2.blive.chat',
|
||||
] : [
|
||||
window.location.origin,
|
||||
'http://localhost:12450',
|
||||
]
|
||||
let curBaseUrl = null
|
||||
let baseUrlToCircuitBreaker = new Map()
|
||||
|
||||
const doUpdateBaseUrls = async() => {
|
||||
async function requestGetUrls(discoveryUrl) {
|
||||
try {
|
||||
return (await axios.get(discoveryUrl)).data.endpoints
|
||||
} catch (e) {
|
||||
console.warn('Failed to discover server endpoints from one source:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
let _baseUrls = []
|
||||
try {
|
||||
let promises = DISCOVERY_URLS.map(requestGetUrls)
|
||||
_baseUrls = await Promise.any(promises)
|
||||
} catch {
|
||||
}
|
||||
if (_baseUrls.length === 0) {
|
||||
console.error('Failed to discover server endpoints from any source')
|
||||
return
|
||||
}
|
||||
|
||||
// 按响应时间排序
|
||||
let sortedBaseUrls = []
|
||||
let errorBaseUrls = []
|
||||
|
||||
async function testEndpoint(baseUrl) {
|
||||
try {
|
||||
let url = `${baseUrl}/api/server_info`
|
||||
await axios.get(url, { timeout: 3 * 1000 })
|
||||
sortedBaseUrls.push(baseUrl)
|
||||
} catch {
|
||||
errorBaseUrls.push(baseUrl)
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(_baseUrls.map(testEndpoint))
|
||||
sortedBaseUrls = sortedBaseUrls.concat(errorBaseUrls)
|
||||
|
||||
baseUrls = sortedBaseUrls
|
||||
if (baseUrls.indexOf(curBaseUrl) === -1) {
|
||||
curBaseUrl = null
|
||||
}
|
||||
|
||||
console.log('Found server endpoints:', baseUrls)
|
||||
}
|
||||
const updateBaseUrls = _.throttle(doUpdateBaseUrls, 3 * 60 * 1000)
|
||||
|
||||
getBaseUrl = function() {
|
||||
updateBaseUrls()
|
||||
|
||||
if (curBaseUrl !== null) {
|
||||
let breaker = getOrAddCircuitBreaker(curBaseUrl)
|
||||
if (!breaker.opened) {
|
||||
return curBaseUrl
|
||||
}
|
||||
curBaseUrl = null
|
||||
}
|
||||
|
||||
// 找第一个未熔断的
|
||||
for (let baseUrl of baseUrls) {
|
||||
let breaker = getOrAddCircuitBreaker(baseUrl)
|
||||
if (!breaker.opened) {
|
||||
curBaseUrl = baseUrl
|
||||
console.log('Switch server endpoint to', curBaseUrl)
|
||||
return curBaseUrl
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const getOrAddCircuitBreaker = baseUrl => {
|
||||
let breaker = baseUrlToCircuitBreaker.get(baseUrl)
|
||||
if (breaker === undefined) {
|
||||
breaker = new CircuitBreaker(promise => promise, {
|
||||
timeout: false,
|
||||
rollingCountTimeout: 60 * 1000,
|
||||
errorThresholdPercentage: 70,
|
||||
resetTimeout: 60 * 1000,
|
||||
})
|
||||
baseUrlToCircuitBreaker.set(baseUrl, breaker)
|
||||
}
|
||||
return breaker
|
||||
}
|
||||
|
||||
await updateBaseUrls()
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import { apiClient as axios } from '@/api/base'
|
||||
import * as chat from '.'
|
||||
import * as chatModels from './models'
|
||||
import * as base from './ChatClientOfficialBase'
|
||||
|
@ -1,5 +1,4 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import { apiClient as axios } from '@/api/base'
|
||||
import * as chat from '.'
|
||||
import * as chatModels from './models'
|
||||
import * as base from './ChatClientOfficialBase'
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { getBaseUrl } from '@/api/base'
|
||||
import * as chat from '.'
|
||||
import * as chatModels from './models'
|
||||
|
||||
@ -52,8 +53,15 @@ export default class ChatClientRelay {
|
||||
|
||||
this.addDebugMsg('Connecting')
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
const url = `${protocol}://${window.location.host}/api/chat`
|
||||
let baseUrl = getBaseUrl()
|
||||
if (baseUrl === null) {
|
||||
this.addDebugMsg('No available endpoint')
|
||||
window.setTimeout(() => this.onWsClose(), 0)
|
||||
return
|
||||
}
|
||||
let url = baseUrl.replace(/^http(s?):/, 'ws$1:')
|
||||
url += '/api/chat'
|
||||
|
||||
this.websocket = new WebSocket(url)
|
||||
this.websocket.onopen = this.onWsOpen.bind(this)
|
||||
this.websocket.onclose = this.onWsClose.bind(this)
|
||||
|
@ -1,6 +1,7 @@
|
||||
import axios from 'axios'
|
||||
import MD5 from 'crypto-js/md5'
|
||||
|
||||
import { apiClient as axios } from '@/api/base'
|
||||
|
||||
export function getDefaultMsgHandler() {
|
||||
let dummyFunc = () => {}
|
||||
return {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import axios from 'axios'
|
||||
import { apiClient as axios } from './base'
|
||||
|
||||
export async function getServerInfo() {
|
||||
return (await axios.get('/api/server_info')).data
|
||||
|
@ -1,4 +1,4 @@
|
||||
import axios from 'axios'
|
||||
import { apiClient as axios } from './base'
|
||||
|
||||
export async function getPlugins() {
|
||||
return (await axios.get('/api/plugin/plugins')).data
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import axios from 'axios'
|
||||
import ElementUI from 'element-ui'
|
||||
if (!process.env.LIB_USE_CDN) {
|
||||
import('element-ui/lib/theme-chalk/index.css')
|
||||
@ -10,8 +9,6 @@ import * as i18n from './i18n'
|
||||
import App from './App'
|
||||
import NotFound from './views/NotFound'
|
||||
|
||||
axios.defaults.timeout = 10 * 1000
|
||||
|
||||
if (!process.env.LIB_USE_CDN) {
|
||||
Vue.use(VueRouter)
|
||||
Vue.use(ElementUI)
|
||||
|
@ -33,6 +33,7 @@ module.exports = defineConfig({
|
||||
const ENV = {
|
||||
APP_VERSION,
|
||||
LIB_USE_CDN,
|
||||
BACKEND_DISCOVERY: toBool(process.env.BACKEND_DISCOVERY),
|
||||
}
|
||||
config.plugin('define')
|
||||
.tap(args => {
|
||||
|
Loading…
Reference in New Issue
Block a user