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]
|
[registered_endpoints]
|
||||||
# 1 = https://api1.blive.chat
|
1 = http://localhost:12450
|
||||||
|
|
||||||
|
|
||||||
# 允许跨域的源,正则表达式
|
# 允许跨域的源,正则表达式
|
||||||
[cors_origins]
|
[cors_origins]
|
||||||
# 1 = https://(?:|.+\.)blive\.chat
|
1 = http://localhost(:\d+)
|
||||||
|
@ -1,2 +1,6 @@
|
|||||||
|
# 第三方库用CDN引入
|
||||||
LIB_USE_CDN=false
|
LIB_USE_CDN=false
|
||||||
|
# production环境生成source map
|
||||||
PROD_SOURCE_MAP=true
|
PROD_SOURCE_MAP=true
|
||||||
|
# 动态发现后端endpoint
|
||||||
|
BACKEND_DISCOVERY=false
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
LIB_USE_CDN=true
|
LIB_USE_CDN=true
|
||||||
PROD_SOURCE_MAP=false
|
PROD_SOURCE_MAP=false
|
||||||
|
BACKEND_DISCOVERY=true
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "blivechat",
|
"name": "blivechat",
|
||||||
"version": "1.9.2",
|
"version": "1.9.3-dev",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
@ -15,6 +15,7 @@
|
|||||||
"downloadjs": "^1.4.7",
|
"downloadjs": "^1.4.7",
|
||||||
"element-ui": "^2.15.13",
|
"element-ui": "^2.15.13",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"opossum": "^8.3.0",
|
||||||
"pako": "^2.1.0",
|
"pako": "^2.1.0",
|
||||||
"vue": "^2.7.14",
|
"vue": "^2.7.14",
|
||||||
"vue-i18n": "^8.28.2",
|
"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 chat from '.'
|
||||||
import * as chatModels from './models'
|
import * as chatModels from './models'
|
||||||
import * as base from './ChatClientOfficialBase'
|
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 chat from '.'
|
||||||
import * as chatModels from './models'
|
import * as chatModels from './models'
|
||||||
import * as base from './ChatClientOfficialBase'
|
import * as base from './ChatClientOfficialBase'
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { getBaseUrl } from '@/api/base'
|
||||||
import * as chat from '.'
|
import * as chat from '.'
|
||||||
import * as chatModels from './models'
|
import * as chatModels from './models'
|
||||||
|
|
||||||
@ -52,8 +53,15 @@ export default class ChatClientRelay {
|
|||||||
|
|
||||||
this.addDebugMsg('Connecting')
|
this.addDebugMsg('Connecting')
|
||||||
|
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
let baseUrl = getBaseUrl()
|
||||||
const url = `${protocol}://${window.location.host}/api/chat`
|
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 = new WebSocket(url)
|
||||||
this.websocket.onopen = this.onWsOpen.bind(this)
|
this.websocket.onopen = this.onWsOpen.bind(this)
|
||||||
this.websocket.onclose = this.onWsClose.bind(this)
|
this.websocket.onclose = this.onWsClose.bind(this)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import MD5 from 'crypto-js/md5'
|
import MD5 from 'crypto-js/md5'
|
||||||
|
|
||||||
|
import { apiClient as axios } from '@/api/base'
|
||||||
|
|
||||||
export function getDefaultMsgHandler() {
|
export function getDefaultMsgHandler() {
|
||||||
let dummyFunc = () => {}
|
let dummyFunc = () => {}
|
||||||
return {
|
return {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import axios from 'axios'
|
import { apiClient as axios } from './base'
|
||||||
|
|
||||||
export async function getServerInfo() {
|
export async function getServerInfo() {
|
||||||
return (await axios.get('/api/server_info')).data
|
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() {
|
export async function getPlugins() {
|
||||||
return (await axios.get('/api/plugin/plugins')).data
|
return (await axios.get('/api/plugin/plugins')).data
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import VueRouter from 'vue-router'
|
import VueRouter from 'vue-router'
|
||||||
import axios from 'axios'
|
|
||||||
import ElementUI from 'element-ui'
|
import ElementUI from 'element-ui'
|
||||||
if (!process.env.LIB_USE_CDN) {
|
if (!process.env.LIB_USE_CDN) {
|
||||||
import('element-ui/lib/theme-chalk/index.css')
|
import('element-ui/lib/theme-chalk/index.css')
|
||||||
@ -10,8 +9,6 @@ import * as i18n from './i18n'
|
|||||||
import App from './App'
|
import App from './App'
|
||||||
import NotFound from './views/NotFound'
|
import NotFound from './views/NotFound'
|
||||||
|
|
||||||
axios.defaults.timeout = 10 * 1000
|
|
||||||
|
|
||||||
if (!process.env.LIB_USE_CDN) {
|
if (!process.env.LIB_USE_CDN) {
|
||||||
Vue.use(VueRouter)
|
Vue.use(VueRouter)
|
||||||
Vue.use(ElementUI)
|
Vue.use(ElementUI)
|
||||||
|
@ -33,6 +33,7 @@ module.exports = defineConfig({
|
|||||||
const ENV = {
|
const ENV = {
|
||||||
APP_VERSION,
|
APP_VERSION,
|
||||||
LIB_USE_CDN,
|
LIB_USE_CDN,
|
||||||
|
BACKEND_DISCOVERY: toBool(process.env.BACKEND_DISCOVERY),
|
||||||
}
|
}
|
||||||
config.plugin('define')
|
config.plugin('define')
|
||||||
.tap(args => {
|
.tap(args => {
|
||||||
|
Loading…
Reference in New Issue
Block a user