mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
248 lines
7.9 KiB
TypeScript
248 lines
7.9 KiB
TypeScript
import type { ItemStopPayload, Payload } from 'dev-tools/dev-server/payload'
|
|
import { useScopedConsole } from '@/core/utils/log'
|
|
import { ComponentMetadata, componentsMap } from '@/components/component'
|
|
import { loadInstantStyle, removeStyle } from '@/core/style'
|
|
import { autoUpdateOptions, getDevClientOptions } from './options'
|
|
import { RefreshMethod, HotReloadMethod } from './update-method'
|
|
import { monkey } from '@/core/ajax'
|
|
import { plugins } from '@/plugins/plugin'
|
|
import { Toast } from '@/core/toast'
|
|
|
|
const options = getDevClientOptions()
|
|
const console = useScopedConsole('DevClient')
|
|
const handleSocketMessage = (event: MessageEvent, callback: (payload: Payload) => void) => {
|
|
const { data } = event
|
|
try {
|
|
const payload: Payload = JSON.parse(data)
|
|
console.log('接收信息', payload)
|
|
callback(payload)
|
|
} catch (error) {
|
|
console.error('无效信息', data)
|
|
}
|
|
}
|
|
export enum DevClientEvents {
|
|
CoreUpdate = 'coreUpdate',
|
|
ItemUpdate = 'itemUpdate',
|
|
SessionsUpdate = 'sessionsUpdate',
|
|
ServerChange = 'serverChange',
|
|
ServerConnected = 'serverConnected',
|
|
ServerDisconnected = 'serverDisconnected',
|
|
}
|
|
|
|
export class DevClient extends EventTarget {
|
|
socket: WebSocket
|
|
sessions: string[] = []
|
|
|
|
addEventListener(
|
|
type: DevClientEvents,
|
|
callback: EventListenerOrEventListenerObject,
|
|
eventOptions?: boolean | AddEventListenerOptions,
|
|
) {
|
|
super.addEventListener(type, callback, eventOptions)
|
|
}
|
|
removeEventListener(
|
|
type: DevClientEvents,
|
|
callback: EventListenerOrEventListenerObject,
|
|
eventOptions?: boolean | EventListenerOptions,
|
|
) {
|
|
super.removeEventListener(type, callback, eventOptions)
|
|
}
|
|
|
|
createSocket(toastError = false) {
|
|
return new Promise<boolean>(resolve => {
|
|
this.closeSocket()
|
|
|
|
const unloadHandler = () => {
|
|
this.closeSocket()
|
|
}
|
|
this.socket = new WebSocket(`ws://localhost:${options.port}`)
|
|
this.socket.addEventListener('error', () => {
|
|
console.warn('未能连接到 DevServer')
|
|
if (toastError) {
|
|
Toast.error('连接失败, 请确保 DevServer 已启动, 并检查连接配置.', 'DevClient', 2000)
|
|
}
|
|
this.closeSocket()
|
|
resolve(false)
|
|
})
|
|
this.socket.addEventListener('close', () => {
|
|
console.log('已断开 DevServer 连接')
|
|
window.removeEventListener('unload', unloadHandler)
|
|
})
|
|
this.socket.addEventListener('open', () => {
|
|
console.log('已连接到 DevServer')
|
|
this.dispatchEvent(new CustomEvent(DevClientEvents.ServerChange, { detail: true }))
|
|
this.dispatchEvent(new CustomEvent(DevClientEvents.ServerConnected))
|
|
resolve(true)
|
|
})
|
|
this.socket.addEventListener('message', e => {
|
|
handleSocketMessage(e, payload => {
|
|
switch (payload.type) {
|
|
default: {
|
|
break
|
|
}
|
|
case 'start': {
|
|
this.sessions = payload.sessions
|
|
this.dispatchEvent(new CustomEvent(
|
|
DevClientEvents.SessionsUpdate,
|
|
{ detail: this.sessions },
|
|
))
|
|
break
|
|
}
|
|
case 'stop': {
|
|
this.closeSocket()
|
|
break
|
|
}
|
|
case 'coreUpdate': {
|
|
this.handleCoreUpdate()
|
|
break
|
|
}
|
|
case 'itemUpdate': {
|
|
const { path } = payload
|
|
this.handleItemUpdate(path)
|
|
break
|
|
}
|
|
}
|
|
})
|
|
})
|
|
window.addEventListener('unload', unloadHandler)
|
|
})
|
|
}
|
|
|
|
closeSocket() {
|
|
if (!this.socket) {
|
|
return
|
|
}
|
|
this.socket.close()
|
|
this.socket = null
|
|
this.sessions = []
|
|
this.dispatchEvent(new CustomEvent(DevClientEvents.SessionsUpdate, { detail: this.sessions }))
|
|
this.dispatchEvent(new CustomEvent(DevClientEvents.ServerChange, { detail: false }))
|
|
this.dispatchEvent(new CustomEvent(DevClientEvents.ServerDisconnected))
|
|
}
|
|
|
|
get isConnected() {
|
|
return this.socket?.readyState === WebSocket.OPEN
|
|
}
|
|
|
|
private handleCoreUpdate() {
|
|
this.dispatchEvent(new CustomEvent(DevClientEvents.CoreUpdate))
|
|
if (options.coreRefreshMethod === RefreshMethod.AlwaysRefresh) {
|
|
console.log('本体已更新, 刷新页面...')
|
|
location.reload()
|
|
}
|
|
}
|
|
|
|
private async handleItemUpdate(path: string) {
|
|
this.dispatchEvent(new CustomEvent(DevClientEvents.ItemUpdate, { detail: path }))
|
|
const url = `http://localhost:${options.port}${path}`
|
|
const componentRecord = Object.entries(autoUpdateOptions.urls.components)
|
|
.find(([, { url: itemUrl }]) => itemUrl.endsWith(path))
|
|
if (componentRecord) {
|
|
const [name] = componentRecord
|
|
const oldComponent = componentsMap[name]
|
|
if (!oldComponent) {
|
|
return
|
|
}
|
|
const code: string = await monkey({ url })
|
|
const { installFeatureFromCode } = await import(
|
|
'@/core/install-feature'
|
|
)
|
|
const { metadata } = await installFeatureFromCode(code, url)
|
|
const newComponent = metadata as ComponentMetadata
|
|
const oldInstantStyles = oldComponent.instantStyles ?? []
|
|
const newInstantStyles = newComponent.instantStyles ?? []
|
|
// const isEntryEmpty = oldComponent.entry === none && newComponent.entry === none
|
|
// console.log({ oldInstantStyles, newInstantStyles, isEntryEmpty })
|
|
|
|
const doNotReload = () => {
|
|
console.log(`组件 [${newComponent.displayName}] 已更新`)
|
|
}
|
|
const reload = () => {
|
|
console.log(`组件 [${newComponent.displayName}] 已更新, 刷新页面...`)
|
|
location.reload()
|
|
}
|
|
const reloadInstantStyles = () => {
|
|
if (oldInstantStyles.length > 0 || newInstantStyles.length > 0) {
|
|
loadInstantStyle(newComponent)
|
|
oldInstantStyles.forEach(style => {
|
|
removeStyle(style.name)
|
|
})
|
|
// 修改旧的引用, 否则之前设的事件监听还是用旧样式
|
|
oldComponent.instantStyles = newInstantStyles
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
switch (options.registryReloadMethod) {
|
|
default:
|
|
case HotReloadMethod.Disabled: {
|
|
if (options.registryRefreshMethod === RefreshMethod.DoNotRefresh) {
|
|
doNotReload()
|
|
} else {
|
|
reload()
|
|
}
|
|
break
|
|
}
|
|
case HotReloadMethod.Enabled: {
|
|
if (reloadInstantStyles()) {
|
|
doNotReload()
|
|
} else {
|
|
reload()
|
|
}
|
|
break
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
const pluginRecord = Object.entries(autoUpdateOptions.urls.plugins)
|
|
.find(([, { url: itemUrl }]) => itemUrl.endsWith(path))
|
|
if (pluginRecord) {
|
|
const [name] = pluginRecord
|
|
const plugin = plugins.find(p => p.name === name)
|
|
if (!plugin) {
|
|
return
|
|
}
|
|
const { displayName } = plugin
|
|
if (options.registryRefreshMethod !== RefreshMethod.DoNotRefresh) {
|
|
console.log(`插件 [${displayName}] 已更新, 刷新页面...`)
|
|
location.reload()
|
|
} else {
|
|
console.log(`插件 [${displayName}] 已更新`)
|
|
}
|
|
}
|
|
}
|
|
|
|
private async querySessions() {
|
|
return new Promise<string[]>(resolve => {
|
|
this.socket?.addEventListener('message', e => {
|
|
handleSocketMessage(e, payload => {
|
|
if (payload.type === 'querySessionsResponse') {
|
|
this.sessions = payload.sessions
|
|
this.dispatchEvent(
|
|
new CustomEvent(DevClientEvents.SessionsUpdate, { detail: payload.sessions }),
|
|
)
|
|
resolve(payload.sessions)
|
|
}
|
|
})
|
|
}, { once: true })
|
|
this.socket?.send(JSON.stringify({ type: 'querySessions' }))
|
|
})
|
|
}
|
|
|
|
async startDebug(url: string) {
|
|
await monkey({ url })
|
|
return this.querySessions()
|
|
}
|
|
|
|
async stopDebug(path: string) {
|
|
const stopPayload: ItemStopPayload = {
|
|
type: 'itemStop',
|
|
path,
|
|
}
|
|
this.socket?.send(JSON.stringify(stopPayload))
|
|
return this.querySessions()
|
|
}
|
|
}
|
|
export const devClient = new DevClient()
|