使用vite
This commit is contained in:
327
src/client.js
Normal file
327
src/client.js
Normal file
@@ -0,0 +1,327 @@
|
||||
import { List, ListItem, Avatar, Span, Dialog, Button, Input } from './weigets.js'
|
||||
|
||||
export default class ClientList {
|
||||
constructor({ channels = {}, EventListeners = {}, name: username, onexit }) {
|
||||
this.event = { onexit }
|
||||
this.channels = channels
|
||||
this.EventListeners = EventListeners
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
const host = window.location.host
|
||||
this.clientlist = []
|
||||
this.element = List({
|
||||
style: {
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
right: '0',
|
||||
display: 'flex',
|
||||
flexDirection: 'wrap',
|
||||
alignItems: 'center',
|
||||
listStyle: 'none',
|
||||
padding: '0 1rem',
|
||||
}
|
||||
})
|
||||
document.body.appendChild(this.element)
|
||||
|
||||
// 连接 WebSocket
|
||||
const linkStart = () => {
|
||||
const websocket = new WebSocket(`${protocol}://${host}/webrtc/music?name=${username}`)
|
||||
websocket.onmessage = async event => {
|
||||
const data = JSON.parse(event.data)
|
||||
const webrtc_init = async () => {
|
||||
const webrtc = new RTCPeerConnection({
|
||||
iceServers: [
|
||||
{
|
||||
urls: 'turn:satori.love:3478?transport=udp',
|
||||
username: 'x-username',
|
||||
credential: 'x-password'
|
||||
},
|
||||
{
|
||||
urls: [
|
||||
'stun:stun.1und1.de',
|
||||
'stun:stun.callwithus.com',
|
||||
'stun:stun.ekiga.net',
|
||||
'stun:stun.fwdnet.net',
|
||||
'stun:stun.fwdnet.net:3478',
|
||||
'stun:stun.gmx.net',
|
||||
'stun:stun.iptel.org',
|
||||
'stun:stun.internetcalls.com',
|
||||
'stun:stun.minisipserver.com',
|
||||
'stun:stun.schlund.de',
|
||||
'stun:stun.sipgate.net',
|
||||
'stun:stun.sipgate.net:10000',
|
||||
'stun:stun.softjoys.com',
|
||||
'stun:stun.softjoys.com:3478',
|
||||
'stun:stun.voip.aebc.com',
|
||||
'stun:stun.voipbuster.com',
|
||||
'stun:stun.voipstunt.com',
|
||||
'stun:stun.voxgratia.org',
|
||||
'stun:stun.wirlab.net',
|
||||
'stun:stun.xten.com',
|
||||
'stun:stunserver.org',
|
||||
'stun:stun01.sipphone.com',
|
||||
'stun:stun.zoiper.com'
|
||||
]
|
||||
}
|
||||
],
|
||||
iceCandidatePoolSize: 10, // 限制 ICE 候选者的数量
|
||||
iceTransportPolicy: 'all', // 使用所有可用的候选者
|
||||
bundlePolicy: 'balanced', // 每種類型的內容建立一個單獨的傳輸
|
||||
})
|
||||
webrtc.ondatachannel = ({ channel }) => {
|
||||
console.debug(data.name, '建立', channel.label, '数据通道')
|
||||
const client = this.clientlist.find(x => x.id === data.id)
|
||||
const option = this.channels[channel.label]
|
||||
channel.onopen = event => {
|
||||
console.debug('对方打开', channel.label, '数据通道')
|
||||
if (option && option.onopen) {
|
||||
option.onopen(event, client)
|
||||
}
|
||||
}
|
||||
channel.onmessage = event => {
|
||||
//console.log('对方发送', channel.label, '数据消息')
|
||||
if (option && option.onmessage) {
|
||||
option.onmessage(event, client)
|
||||
}
|
||||
}
|
||||
channel.onclose = event => {
|
||||
console.debug('对方关闭', channel.label, '数据通道')
|
||||
if (option && option.onclose) {
|
||||
option.onclose(event, client)
|
||||
}
|
||||
}
|
||||
channel.onerror = event => {
|
||||
console.error(data.name, '通道', channel.label, '发生错误')
|
||||
if (option && option.onerror) {
|
||||
option.onerror(event, client)
|
||||
}
|
||||
}
|
||||
}
|
||||
webrtc.onicecandidate = event => {
|
||||
if (event.candidate) {
|
||||
websocket.send(JSON.stringify({
|
||||
type: 'candidate',
|
||||
id: data.id,
|
||||
candidate: event.candidate
|
||||
}))
|
||||
}
|
||||
}
|
||||
webrtc.oniceconnectionstatechange = async event => {
|
||||
if (webrtc.iceConnectionState === 'disconnected' || webrtc.iceConnectionState === 'failed') {
|
||||
console.error(data.name, '需要添加新的 candidate')
|
||||
// 添加新的 candidate
|
||||
} else if (webrtc.iceConnectionState === 'connected' || webrtc.iceConnectionState === 'completed') {
|
||||
console.debug(data.name, 'WebRTC 连接已经建立成功')
|
||||
}
|
||||
}
|
||||
const channels = Object.entries(this.channels).map(([name, callback]) => {
|
||||
const channel = webrtc.createDataChannel(name, { reliable: true })
|
||||
return channel
|
||||
})
|
||||
return { webrtc, channels }
|
||||
}
|
||||
if (data.type === 'list') {
|
||||
console.debug('取得在线对端列表:', data)
|
||||
const { webrtc, channels } = await webrtc_init()
|
||||
console.debug('发送给对方 offer')
|
||||
const offer = await webrtc.createOffer()
|
||||
await webrtc.setLocalDescription(offer)
|
||||
this.clientlist.push({ id: data.id, name: data.name, webrtc, channels })
|
||||
websocket.send(JSON.stringify({ type: 'offer', id: data.id, offer }))
|
||||
// 传递正确的指针给元素, 以便其能够调取正确的头像
|
||||
this.push(this.clientlist.find(client => client.id === data.id))
|
||||
return
|
||||
}
|
||||
if (data.type === 'push') {
|
||||
console.debug('新上线客户端:', data)
|
||||
return
|
||||
}
|
||||
if (data.type === 'pull') {
|
||||
console.debug('移除客户端:', data)
|
||||
return this.exit(data)
|
||||
}
|
||||
if (data.type === 'offer') {
|
||||
console.debug('收到对方 offer', data)
|
||||
const { webrtc, channels } = await webrtc_init()
|
||||
this.clientlist.push({ id: data.id, name: data.name, webrtc, channels })
|
||||
// 传递正确的指针给元素, 以便其能够调取正确的头像
|
||||
this.push(this.clientlist.find(client => client.id === data.id))
|
||||
console.debug('发送给对方 answer')
|
||||
await webrtc.setRemoteDescription(data.offer)
|
||||
const answer = await webrtc.createAnswer()
|
||||
await webrtc.setLocalDescription(answer)
|
||||
websocket.send(JSON.stringify({ type: 'answer', id: data.id, answer }))
|
||||
return
|
||||
}
|
||||
if (data.type === 'answer') {
|
||||
console.debug('收到对方 answer', data)
|
||||
const webrtc = this.clientlist.find(client => client.id === data.id).webrtc
|
||||
await webrtc.setRemoteDescription(data.answer)
|
||||
return
|
||||
}
|
||||
if (data.type === 'candidate') {
|
||||
console.debug(data.name, '发来 candidate 候选通道')
|
||||
const webrtc = this.clientlist.find(client => client.id === data.id).webrtc
|
||||
await webrtc.addIceCandidate(data.candidate)
|
||||
return
|
||||
}
|
||||
console.error('收到未知数据:', data)
|
||||
}
|
||||
websocket.onclose = async event => {
|
||||
console.log('WebSocket 断线重连...')
|
||||
await new Promise(resolve => setTimeout(resolve, 10000))
|
||||
// this.websocket = linkStart()
|
||||
// 调试模式: 直接刷新页面重载
|
||||
window.location.reload()
|
||||
}
|
||||
return websocket
|
||||
}
|
||||
this.websocket = linkStart()
|
||||
|
||||
// 也插入自己的信息
|
||||
const avatar = localStorage.getItem('avatar')
|
||||
this.push({ id: 'self', name: username, avatar }, true)
|
||||
}
|
||||
getAvatar(id) { }
|
||||
setAvatar(user) {
|
||||
console.info('更新avatar', user)
|
||||
document.getElementById(user.id).querySelector('img').src = user.avatar
|
||||
const u = this.clientlist.find(client => client.id === user.id)
|
||||
u.avatar = user.avatar
|
||||
console.log(u, user)
|
||||
//.avatar = user.avatar
|
||||
}
|
||||
exit(item) {
|
||||
const client = this.clientlist.find(client => client.id === item.id)
|
||||
if (!client) return console.error('目标用户本不存在')
|
||||
this.clientlist = this.clientlist.filter(client => client.id !== item.id)
|
||||
this.element.removeChild(document.getElementById(item.id))
|
||||
this.event.onexit(client)
|
||||
}
|
||||
setChannel(name, option) {
|
||||
this.channels[name] = option
|
||||
}
|
||||
// 新上线客户端
|
||||
push(item, self = false) {
|
||||
this.element.appendChild(ListItem({
|
||||
id: item.id,
|
||||
style: {
|
||||
margin: '0 8px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
fontSize: '12px',
|
||||
borderRadius: '8px',
|
||||
},
|
||||
children: [
|
||||
Avatar({
|
||||
src: item.avatar ?? '/favicon.ico',
|
||||
style: {
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '50%',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
Span({
|
||||
textContent: item.name ?? item.id
|
||||
})
|
||||
],
|
||||
onclick: event => document.body.appendChild(Dialog({
|
||||
children: [
|
||||
Avatar({
|
||||
src: item.avatar ?? '/favicon.ico',
|
||||
style: {
|
||||
width: '240px',
|
||||
height: '240px',
|
||||
borderRadius: '8px',
|
||||
margin: '0 auto',
|
||||
display: 'block',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
onclick: event => {
|
||||
console.log('点击头像', item)
|
||||
if (!self) return console.log('只能修改自己的头像')
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'image/*'
|
||||
input.onchange = async event => {
|
||||
const file = event.target.files[0]
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = async event => {
|
||||
const base64 = event.target.result
|
||||
localStorage.setItem('avatar', base64)
|
||||
window.location.reload() // 简单刷新页面
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
}),
|
||||
Input({
|
||||
style: {
|
||||
width: '100px',
|
||||
border: '2px dotted #bbb',
|
||||
borderRadius: '50%',
|
||||
outline: 'none',
|
||||
padding: '5px 0',
|
||||
textAlign: 'center',
|
||||
position: 'absolute',
|
||||
bottom: '-2px',
|
||||
background: 'rgba(255, 255, 255, 0.8)',
|
||||
},
|
||||
readOnly: !self, // 如果不是自己, 则不可编辑
|
||||
value: item.name ?? item.id,
|
||||
type: 'text',
|
||||
placeholder: '请设置你的昵称',
|
||||
onchange: event => {
|
||||
localStorage.setItem('username', event.target.value)
|
||||
window.location.reload() // 简单刷新页面
|
||||
}
|
||||
})
|
||||
]
|
||||
}))
|
||||
}))
|
||||
}
|
||||
// 添加回调函数
|
||||
on(name, callback) {
|
||||
this.EventListeners[name] = callback
|
||||
}
|
||||
// 执行回调函数
|
||||
_on(name, ...args) {
|
||||
if (this.EventListeners[name]) {
|
||||
this.EventListeners[name](...args)
|
||||
}
|
||||
}
|
||||
// 通过指定通道发送数据(单播)
|
||||
sendto(id, name, data) {
|
||||
const client = this.clientlist.find(client => client.id === id)
|
||||
if (!client) {
|
||||
console.error('客户端不存在:', id)
|
||||
return
|
||||
}
|
||||
if (!client.channels.find(ch => ch.label === name)) {
|
||||
console.error('通道不存在:', name)
|
||||
return
|
||||
}
|
||||
client.channels.filter(ch => ch.label === name).forEach(async ch => {
|
||||
// 等待 datachannel 打开(临时解决方案)
|
||||
while (ch.readyState !== 'open') {
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
}
|
||||
ch.send(data)
|
||||
})
|
||||
}
|
||||
// 通过指定通道发送数据(广播)
|
||||
send(name, data) {
|
||||
this.clientlist.forEach(client => {
|
||||
client.channels.filter(ch => ch.label === name).forEach(async ch => {
|
||||
// 等待 datachannel 打开(临时解决方案)
|
||||
while (ch.readyState !== 'open') {
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
}
|
||||
ch.send(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
108
src/database.js
Normal file
108
src/database.js
Normal file
@@ -0,0 +1,108 @@
|
||||
// 使用示例:
|
||||
// const db = new IndexedDB('myDatabase', 1, 'myStore')
|
||||
// await db.open()
|
||||
// await db.add({ id: 1, name: 'John' })
|
||||
// const data = await db.get(1)
|
||||
// console.log(data)
|
||||
// await db.delete(1)
|
||||
|
||||
export default class IndexedDB {
|
||||
constructor(databaseName, databaseVersion, storeName) {
|
||||
this.databaseName = databaseName
|
||||
this.databaseVersion = databaseVersion
|
||||
this.storeName = storeName
|
||||
this.db = null
|
||||
}
|
||||
|
||||
open() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.databaseName, this.databaseVersion)
|
||||
request.onerror = (event) => {
|
||||
reject(event.target.error)
|
||||
}
|
||||
request.onsuccess = (event) => {
|
||||
this.db = event.target.result
|
||||
resolve(this.db)
|
||||
}
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result
|
||||
if (!db.objectStoreNames.contains(this.storeName)) {
|
||||
db.createObjectStore(this.storeName, { keyPath: 'id' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
add(data) {
|
||||
console.log('add', data)
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readwrite')
|
||||
const objectStore = transaction.objectStore(this.storeName)
|
||||
|
||||
// 判断是否已经存在
|
||||
const request = objectStore.get(data.id)
|
||||
request.onerror = (event) => {
|
||||
reject(event.target.error)
|
||||
}
|
||||
request.onsuccess = (event) => {
|
||||
if (event.target.result) return resolve(event.target.result)
|
||||
const request = objectStore.add(data)
|
||||
request.onerror = (event) => {
|
||||
reject(event.target.error)
|
||||
}
|
||||
request.onsuccess = (event) => {
|
||||
resolve(event.target.result)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
get(id) {
|
||||
console.log('get', id)
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readonly')
|
||||
const objectStore = transaction.objectStore(this.storeName)
|
||||
const request = objectStore.get(id)
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject(event.target.error)
|
||||
}
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
resolve(event.target.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readonly')
|
||||
const objectStore = transaction.objectStore(this.storeName)
|
||||
const request = objectStore.getAll()
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject(event.target.error)
|
||||
}
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
resolve(event.target.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
delete(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readwrite')
|
||||
const objectStore = transaction.objectStore(this.storeName)
|
||||
const request = objectStore.delete(id)
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject(event.target.error)
|
||||
}
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
resolve(event.target.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
253
src/main.js
Normal file
253
src/main.js
Normal file
@@ -0,0 +1,253 @@
|
||||
import 'virtual:windi.css'
|
||||
import 'virtual:windi-devtools'
|
||||
|
||||
import IndexedDB from './database.js'
|
||||
import MusicList from './music.js'
|
||||
import ClientList from './client.js'
|
||||
|
||||
// 缓冲分片发送
|
||||
const CHUNK_SIZE = 1024 * 64 // 默认每个块的大小为128KB
|
||||
const THRESHOLD = 1024 * 1024 // 默认缓冲区的阈值为1MB
|
||||
const DELAY = 50 // 默认延迟500ms
|
||||
|
||||
// 将两个ArrayBuffer合并成一个
|
||||
function appendBuffer(buffer1, buffer2) {
|
||||
const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength)
|
||||
tmp.set(new Uint8Array(buffer1), 0)
|
||||
tmp.set(new Uint8Array(buffer2), buffer1.byteLength)
|
||||
return tmp.buffer
|
||||
}
|
||||
|
||||
// 读取本地图像
|
||||
const imageStore = new IndexedDB('musicDatabase', 1, 'imageObjectStore')
|
||||
await imageStore.open()
|
||||
|
||||
// 读取本地音乐列表并标识为缓存状态(本地缓存)
|
||||
const musicStore = new IndexedDB('musicDatabase', 1, 'musicObjectStore')
|
||||
await musicStore.open()
|
||||
const list = (await musicStore.getAll()).map(item => {
|
||||
return { save: true, ...item }
|
||||
})
|
||||
|
||||
// 读取本地用户名(本地缓存)
|
||||
const name = localStorage.getItem('username') ?? '游客'
|
||||
const avatar = localStorage.getItem('avatar') ?? '/favicon.ico'
|
||||
|
||||
|
||||
// 初始化客户端列表
|
||||
const clientList = new ClientList({
|
||||
name,
|
||||
onexit: async client => {
|
||||
console.log(client.name, '离开频道', client)
|
||||
// 从列表中移除未缓存的此用户的音乐, 但可能多人都有此音乐且未缓存
|
||||
// 因此每条音乐都要检查是否有其他用户也有此音乐, 如果有则不移除
|
||||
const 此用户音乐 = client.musicList?.map(item => item.id) || []
|
||||
const 无数据音乐 = musicList.list.filter(item => !item.arrayBuffer).filter(item => {
|
||||
return 此用户音乐.includes(item.id)
|
||||
})
|
||||
无数据音乐.forEach(item => {
|
||||
const client = clientList.clientlist.find(client => {
|
||||
return client.musicList.find(x => x.id === item.id)
|
||||
})
|
||||
if (!client) musicList.remove(item)
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// 初始化音乐列表(加入本地缓存)
|
||||
const musicList = new MusicList({
|
||||
list,
|
||||
onplay: item => {
|
||||
console.log('播放音乐', item.name)
|
||||
},
|
||||
onstop: item => {
|
||||
console.log('停止音乐', item?.name)
|
||||
},
|
||||
onlike: (item, list) => {
|
||||
console.log('喜欢音乐', item.name)
|
||||
if (item.arrayBuffer) {
|
||||
musicStore.add(item)
|
||||
clientList.send('base', JSON.stringify({
|
||||
type: 'set_music_list',
|
||||
list: list.map(({ id, name, size, type }) => ({ id, name, size, type }))
|
||||
}))
|
||||
}
|
||||
},
|
||||
onunlike: (item, list) => {
|
||||
console.log('取消喜欢', item.name)
|
||||
if (item.arrayBuffer) {
|
||||
musicStore.delete(item.id)
|
||||
clientList.send('base', JSON.stringify({
|
||||
type: 'set_music_list',
|
||||
list: list.map(({ id, name, size, type }) => ({ id, name, size, type }))
|
||||
}))
|
||||
}
|
||||
},
|
||||
onban: item => {
|
||||
console.info('禁止音乐', item.name)
|
||||
},
|
||||
onunban: item => {
|
||||
console.info('解禁音乐', item.name)
|
||||
},
|
||||
onremove: item => {
|
||||
console.info('移除音乐', item.name)
|
||||
musicStore.delete(item.id)
|
||||
},
|
||||
onadd: (item, list) => {
|
||||
console.info('添加音乐', item.name)
|
||||
},
|
||||
onupdate: item => {
|
||||
console.info('更新音乐', item.name)
|
||||
musicStore.put(item)
|
||||
},
|
||||
onerror: error => {
|
||||
console.error('音乐列表错误', error)
|
||||
},
|
||||
onload: async item => {
|
||||
console.info('加载音乐', item)
|
||||
return await new Promise((resolve) => {
|
||||
var buffer = new ArrayBuffer(0) // 接收音乐数据
|
||||
var count = 0 // 接收分片计数
|
||||
const chunkNumber = Math.ceil(item.size / 1024 / 64) // 64KB每片
|
||||
clientList.setChannel(`music-data-${item.id}`, {
|
||||
onmessage: async (event, client) => {
|
||||
console.log('收到音乐数据 chunk', `${count + 1}/${chunkNumber}`, buffer.byteLength)
|
||||
buffer = appendBuffer(buffer, event.data) // 合并分片准备存储
|
||||
item.arrayBufferChunks?.push(event.data) // 保存分片给边下边播
|
||||
count++
|
||||
if (buffer.byteLength >= item.size) {
|
||||
console.log('音乐数据接收完毕')
|
||||
item.arrayBuffer = buffer
|
||||
event.target.close() // 关闭信道
|
||||
resolve(item)
|
||||
}
|
||||
}
|
||||
})
|
||||
const client = clientList.clientlist.find(client => {
|
||||
return client.musicList.find(x => x.id === item.id)
|
||||
})
|
||||
console.info('向', client.name, '请求音乐数据')
|
||||
|
||||
const c = Math.ceil(item.size / CHUNK_SIZE)
|
||||
console.log('需要接收', c, '个分片')
|
||||
|
||||
clientList.sendto(client.id, 'base', JSON.stringify({
|
||||
type: 'get_music_data', id: item.id, channel: `music-data-${item.id}`
|
||||
}))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const ImageList = []
|
||||
|
||||
// 只有一个基本信道, 用于交换和调度信息
|
||||
clientList.setChannel('base', {
|
||||
onopen: async event => {
|
||||
console.debug('打开信道', event.target.label, '广播请求音乐列表和身份信息')
|
||||
clientList.send('base', JSON.stringify({ type: 'get_music_list' })) // 要求对方发送音乐列表
|
||||
clientList.send('base', JSON.stringify({ type: 'get_user_profile' })) // 要求对方发送身份信息
|
||||
},
|
||||
onmessage: async (event, client) => {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data.type === 'get_user_profile') {
|
||||
console.log(client.name, '请求身份信息:', data)
|
||||
// 包过大会导致发送失败, 因此需要分开发送
|
||||
clientList.sendto(client.id, 'base', JSON.stringify({
|
||||
type: 'set_user_profile',
|
||||
name: name,
|
||||
avatar: avatar,
|
||||
}))
|
||||
return
|
||||
}
|
||||
if (data.type === 'set_user_profile') {
|
||||
console.log(client.name, '发来身份信息:', data)
|
||||
console.log('将身份信息保存到本机记录:', client)
|
||||
client.name = data.name
|
||||
client.avatar = data.avatar
|
||||
// 还需要更新组件的用户信息
|
||||
console.log('更新组件的用户信息:', data, client)
|
||||
clientList.setAvatar({ id: client.id, ...data })
|
||||
return
|
||||
}
|
||||
if (data.type === 'get_image_list') {
|
||||
// webrtc://用户@域名:端口/信道标识/资源ID
|
||||
}
|
||||
if (data.type === 'get_music_list') {
|
||||
const ms = musicList.list.filter(item => item.arrayBuffer)
|
||||
console.log(client.name, '请求音乐列表:', ms)
|
||||
clientList.sendto(client.id, 'base', JSON.stringify({
|
||||
type: 'set_music_list',
|
||||
list: ms.map(({ id, name, size, type }) => ({ id, name, size, type }))
|
||||
}))
|
||||
return
|
||||
}
|
||||
if (data.type === 'set_music_list') {
|
||||
console.log(client.name, '发来音乐列表:', `x${JSON.parse(event.data).list.length}`)
|
||||
client.musicList = data.list
|
||||
client.musicList.forEach(music => musicList.add(music))
|
||||
return
|
||||
}
|
||||
if (data.type === 'get_music_data') {
|
||||
// 建立一个信道, 用于传输音乐数据(接收方已经准备好摘要信息)
|
||||
console.log(client.name, '建立一个信道, 用于传输音乐数据', musicList.list)
|
||||
musicList.list.filter(item => item.id === data.id).forEach(item => {
|
||||
const ch = client.webrtc.createDataChannel(data.channel, { reliable: true })
|
||||
ch.onopen = async event => {
|
||||
console.log(client.name, `打开 ${data.channel} 信道传输音乐数据`, item.name)
|
||||
// 将音乐数据分成多个小块,并逐个发送
|
||||
async function sendChunk(dataChannel, data, index = 0, buffer = new ArrayBuffer(0)) {
|
||||
while (index < data.byteLength) {
|
||||
if (dataChannel.bufferedAmount <= THRESHOLD) {
|
||||
const chunk = data.slice(index, index + CHUNK_SIZE)
|
||||
dataChannel.send(chunk)
|
||||
index += CHUNK_SIZE
|
||||
buffer = appendBuffer(buffer, chunk)
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, DELAY))
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
await sendChunk(ch, item.arrayBuffer)
|
||||
console.log(client.name, `获取 ${data.channel} 信道数据结束`, item.name)
|
||||
ch.close() // 关闭信道
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
console.log('未知类型:', data.type)
|
||||
},
|
||||
onclose: event => {
|
||||
console.log('关闭信道', event.target.label)
|
||||
},
|
||||
onerror: event => {
|
||||
console.error('信道错误', event.target.label, event.error)
|
||||
}
|
||||
})
|
||||
|
||||
// 延迟1500ms
|
||||
//await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// 设置自己的主机名
|
||||
const nameInput = document.createElement('input')
|
||||
nameInput.type = 'text'
|
||||
nameInput.placeholder = '请设置你的昵称'
|
||||
nameInput.value = name
|
||||
nameInput.onchange = event => {
|
||||
localStorage.setItem('username', event.target.value)
|
||||
window.location.reload() // 简单刷新页面
|
||||
}
|
||||
document.body.appendChild(nameInput)
|
||||
|
||||
// 设置标签为自己的头像
|
||||
if (localStorage.getItem('avatar')) {
|
||||
const favicon = document.createElement('link')
|
||||
favicon.rel = 'icon'
|
||||
favicon.href = localStorage.getItem('avatar')
|
||||
document.head.appendChild(favicon)
|
||||
}
|
||||
// 设置标题为自己的昵称
|
||||
if (localStorage.getItem('username')) {
|
||||
document.title = localStorage.getItem('username')
|
||||
}
|
225
src/music.js
Normal file
225
src/music.js
Normal file
@@ -0,0 +1,225 @@
|
||||
import { Span, Button, List, ListItem, UploadMusic } from './weigets.js'
|
||||
|
||||
export default class MusicList {
|
||||
constructor({ list = [], EventListeners = {}, onplay, onstop, onadd, onremove, onlike, onunlike, onban, onload }) {
|
||||
this.event = { onplay, onstop, onadd, onremove, onlike, onunlike, onban, onload }
|
||||
this.ul = List({ classList: ['music-list'] })
|
||||
this.EventListeners = EventListeners
|
||||
this.list = []
|
||||
list.forEach(item => this.add(item)) // 列表逐一添加
|
||||
document.body.appendChild(this.ul) // 元素加入页面
|
||||
|
||||
// 添加音乐播放器
|
||||
this.audio = new Audio()
|
||||
this.audio.addEventListener('ended', () => {
|
||||
this.next()
|
||||
})
|
||||
//this.audio.addEventListener('timeupdate', () => {
|
||||
// console.log(this.audio.currentTime)
|
||||
//})
|
||||
//this.audio.addEventListener('error', event => {
|
||||
// console.error('音乐播放器错误:', event)
|
||||
//})
|
||||
// 本地添加音乐按钮
|
||||
document.body.appendChild(UploadMusic({
|
||||
style: { width: '20rem', height: '5rem', margin: '1rem 2rem' },
|
||||
onchange: files => {
|
||||
for (const file of files) {
|
||||
const id = 'music' + Date.now()
|
||||
const { name, size, type } = file
|
||||
const reader = new FileReader()
|
||||
reader.onload = async event => {
|
||||
const arrayBuffer = event.target.result
|
||||
this.add({ id, name, size, type, arrayBuffer }) // 添加到列表(默认并不存储)
|
||||
this.like({ id, name, size, type, arrayBuffer }) // 本地缓存的必要条件是喜欢
|
||||
}
|
||||
reader.readAsArrayBuffer(file)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
|
||||
// 写入 css 样式到 head
|
||||
const style = document.createElement('style')
|
||||
style.innerText = `
|
||||
ul.music-list {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
ul.music-list > li > span {
|
||||
cursor: pointer;
|
||||
}
|
||||
ul.music-list > li.play > span {
|
||||
color: #02be08;
|
||||
}
|
||||
ul.music-list > li.cache::marker {
|
||||
color: #02be08;
|
||||
font-size: 1em;
|
||||
contentx: '⚡';
|
||||
}
|
||||
ul.music-list > li.disable {
|
||||
color: #999999;
|
||||
}
|
||||
ul.music-list > li > button {
|
||||
margin-left: 10px;
|
||||
border: none;
|
||||
border-radius: 1em;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-size: .5rem;
|
||||
padding: 0 .5rem;
|
||||
color: #555555;
|
||||
}
|
||||
ul.music-list > li > button:hover {
|
||||
background-color: #ccc;
|
||||
}
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
add(item) {
|
||||
// 如果ID已存在则不添加
|
||||
if (this.list.find(i => i.id === item.id)) {
|
||||
return
|
||||
}
|
||||
// 将字节转换为可读的单位
|
||||
const bytesToSize = bytes => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]
|
||||
}
|
||||
this.list.push(item)
|
||||
this.ul.appendChild(ListItem({
|
||||
id: item.id,
|
||||
classList: item.arrayBuffer ? ['cache'] : [],
|
||||
children: [
|
||||
Span({
|
||||
textContent: `${item.name} - ${bytesToSize(item.size)}`,
|
||||
onclick: event => {
|
||||
event.stopPropagation() // !如果使用async则此处不能阻止冒泡传递
|
||||
const li = event.target.parentElement // ListItem
|
||||
const ul = li.parentElement // List
|
||||
const list = Array.from(ul.children) // ListItems
|
||||
list.forEach(li => li.classList.remove('play'))
|
||||
if (!this.audio.paused && this.playing === item) {
|
||||
li.classList.remove('play')
|
||||
this.stop(item)
|
||||
} else {
|
||||
li.classList.add('play')
|
||||
this.play(item)
|
||||
}
|
||||
}
|
||||
}),
|
||||
Button({
|
||||
textContent: item.save ? '移除' : '缓存',
|
||||
onclick: event => {
|
||||
event.stopPropagation() // !如果使用async则此处不能阻止冒泡传递
|
||||
if (item.save) {
|
||||
event.target.textContent = '缓存'
|
||||
this.ul.querySelector(`#${item.id}`).classList.remove('cache')
|
||||
this.unlike(item)
|
||||
} else {
|
||||
item.save = true
|
||||
event.target.textContent = '移除'
|
||||
this.ul.querySelector(`#${item.id}`).classList.add('cache')
|
||||
this.like(item)
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
}))
|
||||
this.event.onadd(item, this.list)
|
||||
}
|
||||
async remove(item) {
|
||||
this.ul.querySelector(`#${item.id}`)?.remove()
|
||||
if (!this.audio.paused) this.stop() // 停止播放
|
||||
this.list = this.list.filter(i => i.id !== item.id)
|
||||
this.event.onremove(item)
|
||||
}
|
||||
async load(item) {
|
||||
await this.event.onload(item)
|
||||
}
|
||||
async play(item) {
|
||||
if (!item.arrayBuffer) {
|
||||
console.log('加载音乐类型:', item.type)
|
||||
// 不支持流式加载wav和flac和m4a, 需要全部加载完毕才能播放
|
||||
if (item.type === 'audio/wav' || item.type === 'audio/flac' || item.type === 'audio/x-m4a') {
|
||||
await this.load(item)
|
||||
this.audio.src = URL.createObjectURL(new Blob([item.arrayBuffer], { type: item.type }))
|
||||
this.audio.play()
|
||||
} else {
|
||||
// 边加载边播放
|
||||
const mediaSource = new MediaSource()
|
||||
this.audio.src = URL.createObjectURL(mediaSource)
|
||||
if (!item.arrayBufferChunks) item.arrayBufferChunks = []
|
||||
mediaSource.addEventListener('sourceopen', async () => {
|
||||
const sourceBuffer = mediaSource.addSourceBuffer(item.type)
|
||||
const arrayBufferLoader = async (index = 0) => {
|
||||
console.log('开始加载====================================')
|
||||
// 按照数据长度计算出分片应有数量, 如果数量不到且没有停止加载则一直读取
|
||||
const chunkNumber = Math.ceil(item.size / 1024 / 64) // 64KB每片
|
||||
console.log({ index, chunkNumber, paused: this.audio.paused })
|
||||
while (index < chunkNumber && !this.audio.paused) {
|
||||
const 播放状态 = !this.audio.paused && this.playing === item
|
||||
const 加载状态 = item.arrayBufferChunks.length < chunkNumber
|
||||
const 结束时间 = sourceBuffer.buffered.length && sourceBuffer.buffered.end(0)
|
||||
const 缓冲时间 = 结束时间 - this.audio.currentTime
|
||||
if (!播放状态 && !加载状态) break // 播放停止且加载完毕则退出
|
||||
if (this.audio.paused || this.playing !== item) break // 播放停止或已经切歌则退出
|
||||
if (缓冲时间 > 60) { // 缓冲超过60秒则等待30秒
|
||||
await new Promise(resolve => setTimeout(resolve, 30000))
|
||||
continue
|
||||
}
|
||||
if (sourceBuffer.updating) { // sourceBuffer正在更新则等待更新结束
|
||||
await new Promise(resolve => sourceBuffer.addEventListener('updateend', resolve))
|
||||
continue
|
||||
}
|
||||
if (item.arrayBufferChunks.length <= index) { // 分片数量不足则等待
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
continue
|
||||
}
|
||||
console.log('播放器加载分片:', item.name, `${index + 1}/${chunkNumber}`)
|
||||
const chunk = item.arrayBufferChunks[index] // 顺序取出一个arrayBuffer分片
|
||||
sourceBuffer.appendBuffer(chunk) // 添加到sourceBuffer
|
||||
index++
|
||||
}
|
||||
console.log('加载完毕====================================')
|
||||
item.arrayBufferChunks = null // 加载完毕释放分片内存
|
||||
}
|
||||
this.event.onload(item)
|
||||
this.audio.play()
|
||||
arrayBufferLoader()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 本地缓存直接播放
|
||||
this.audio.src = URL.createObjectURL(new Blob([item.arrayBuffer], { type: item.type }))
|
||||
this.audio.play()
|
||||
}
|
||||
this.playing = item
|
||||
this.event.onplay(item)
|
||||
}
|
||||
async stop() {
|
||||
if (this.audio.paused) {
|
||||
return console.error('暂停播放:音乐播放器不是播放状态!')
|
||||
}
|
||||
this.audio.pause()
|
||||
this.event.onstop(this.playing)
|
||||
this.playing = null
|
||||
}
|
||||
async like(item) {
|
||||
if (!item.arrayBuffer) {
|
||||
await this.load(item)
|
||||
}
|
||||
this.event.onlike(item, this.list)
|
||||
}
|
||||
async unlike(item) {
|
||||
this.event.onunlike(item, this.list)
|
||||
}
|
||||
async ban(item) {
|
||||
this.event.onban(item)
|
||||
}
|
||||
next() { }
|
||||
prev() { }
|
||||
}
|
319
src/weigets.js
Normal file
319
src/weigets.js
Normal file
@@ -0,0 +1,319 @@
|
||||
export function createElement({ innerText, innerHTML, textContent, readOnly, children = [], dataset, style, classList = [], ...attributes }, tagName = 'div') {
|
||||
const element = document.createElement(tagName)
|
||||
for (const key in attributes) {
|
||||
if (key.startsWith('on')) element[key] = attributes[key] // 如果是事件则直接赋值
|
||||
else element.setAttribute(key, attributes[key]) // 否则是属性则使用setAttribute
|
||||
}
|
||||
if (dataset) Object.assign(element.dataset, dataset)
|
||||
if (style) Object.assign(element.style, style)
|
||||
if (classList.length) element.classList.add(...classList)
|
||||
if (textContent) element.textContent = textContent
|
||||
if (innerText) element.innerText = innerText
|
||||
if (innerHTML) element.innerHTML = innerHTML
|
||||
if (readOnly) element.readOnly = readOnly
|
||||
if (children) children.forEach(child => element.appendChild(child))
|
||||
return element
|
||||
}
|
||||
|
||||
export function List(options) {
|
||||
return createElement(options, 'ul')
|
||||
}
|
||||
|
||||
export function ListItem(options) {
|
||||
return createElement(options, 'li')
|
||||
}
|
||||
|
||||
export function Span(options) {
|
||||
return createElement(options, 'span')
|
||||
}
|
||||
|
||||
export function Button(options) {
|
||||
return createElement(options, 'button')
|
||||
}
|
||||
|
||||
export function Input(options) {
|
||||
return createElement(options, 'input')
|
||||
}
|
||||
|
||||
export function Avatar(options) {
|
||||
const element = createElement(options, 'img')
|
||||
element.onerror = () => element.src = '/favicon.ico'
|
||||
return element
|
||||
}
|
||||
|
||||
export function UploadMusic(options) {
|
||||
let dragStats = null
|
||||
const drop = createElement({
|
||||
textContent: '点击或拖拽音乐到此处共享您的音乐',
|
||||
style: {
|
||||
width: '100%',
|
||||
color: '#999',
|
||||
lineHeight: '5rem',
|
||||
textAlign: 'center',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
display: 'none'
|
||||
}
|
||||
})
|
||||
return createElement({
|
||||
...options,
|
||||
style: {
|
||||
width: '100%',
|
||||
height: '10rem',
|
||||
backdropFilter: 'blur(5px)',
|
||||
backgroundColor: '#fcfcfc',
|
||||
borderRadius: '1em',
|
||||
border: '1px solid #f1f1f1',
|
||||
position: 'relative',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
...options.style
|
||||
},
|
||||
onclick: event => {
|
||||
// 临时创建一个input触发input的点击事件
|
||||
const input = Input({
|
||||
type: 'file',
|
||||
multiple: true,
|
||||
accept: 'audio/*',
|
||||
style: {
|
||||
display: 'none',
|
||||
},
|
||||
onchange: event => {
|
||||
const files = Array.from(event.target.files)
|
||||
if (files.length === 0) return
|
||||
options.onchange(files)
|
||||
}
|
||||
})
|
||||
input.click()
|
||||
},
|
||||
onmouseenter: event => {
|
||||
drop.style.display = 'block'
|
||||
},
|
||||
onmouseleave: event => {
|
||||
drop.style.display = 'none'
|
||||
},
|
||||
ondragover: event => {
|
||||
console.log('dragover')
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (dragStats) return
|
||||
event.dataTransfer.dropEffect = 'copy'
|
||||
drop.style.display = 'block'
|
||||
},
|
||||
ondragleave: event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (dragStats) return
|
||||
clearTimeout(dragStats)
|
||||
dragStats = setTimeout(() => {
|
||||
drop.style.display = 'none'
|
||||
dragStats = null
|
||||
}, 1000)
|
||||
},
|
||||
ondrop: event => {
|
||||
console.log('drop')
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const files = Array.from(event.dataTransfer.files)
|
||||
// 检查必须是音乐文件, 移除其它文件
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (!files[i].type.startsWith('audio/')) {
|
||||
files.splice(i, 1)
|
||||
i--
|
||||
}
|
||||
}
|
||||
if (files.length === 0) return console.log('没有文件')
|
||||
console.log('files', files)
|
||||
options.onchange(files)
|
||||
},
|
||||
children: [
|
||||
// 绘制一个云朵上传图标(别放弃...还有我呢!)
|
||||
createElement({
|
||||
style: {
|
||||
width: '82px',
|
||||
height: '30px',
|
||||
background: '#e7f4fd',
|
||||
background: '-webkit-linear-gradient(top,#e7f4fd 5%,#ceedfd 100%)',
|
||||
borderRadius: '25px',
|
||||
position: 'relative',
|
||||
margin: '33px auto 5px'
|
||||
},
|
||||
children: [
|
||||
createElement({
|
||||
style: {
|
||||
width: '45px',
|
||||
height: '45px',
|
||||
top: '-22px',
|
||||
right: '12px',
|
||||
borderRadius: '50px',
|
||||
borderRadius: '25px',
|
||||
background: '#e7f4fd',
|
||||
position: 'absolute',
|
||||
zIndex: '-1'
|
||||
}
|
||||
}),
|
||||
createElement({
|
||||
style: {
|
||||
width: '25px',
|
||||
height: '25px',
|
||||
top: '-12px',
|
||||
left: '12px',
|
||||
borderRadius: '25px',
|
||||
background: '#e7f4fd',
|
||||
position: 'absolute',
|
||||
zIndex: '-1'
|
||||
}
|
||||
}),
|
||||
Span({
|
||||
width: '87px',
|
||||
position: 'absolute',
|
||||
bottom: '-2px',
|
||||
background: '#000',
|
||||
zIndex: '-1',
|
||||
boxShadow: '0 0 6px 2px rgba(0,0,0,0.1)',
|
||||
borderRadius: '50%'
|
||||
}),
|
||||
createElement({
|
||||
textContent: '♫',
|
||||
style: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
fontSize: '3rem',
|
||||
fontWeight: 'bold',
|
||||
position: 'relative',
|
||||
top: '-1.5rem',
|
||||
color: '#e7f4fd',
|
||||
textShadow: '0 1px 1px #fff'
|
||||
//textShadow: '0 0 5px #000, 0 0 10px #000, 0 0 15px #000, 0 0 20px #000, 0 0 25px #000, 0 0 30px #000, 0 0 35px #000, 0 0 40px #000, 0 0 45px #000, 0 0 50px #000, 0 0 55px #000, 0 0 60px #000, 0 0 65px #000, 0 0 70px #000, 0 0 75px #000'
|
||||
}
|
||||
})
|
||||
]
|
||||
}),
|
||||
drop
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
// 弹出窗口, 高斯模糊背景, 进入离开动画过渡
|
||||
export function Dialog(options) {
|
||||
const element = createElement({
|
||||
tabIndex: 0,
|
||||
style: {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 1000,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backdropFilter: 'blur(5px)',
|
||||
duration: '0.5s',
|
||||
transition: 'all 0.5s'
|
||||
},
|
||||
onclick: async event => {
|
||||
// 判断必须是点击自身, 而不是子元素
|
||||
if (event.target !== event.currentTarget) return
|
||||
await event.target.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 100 }).finished
|
||||
await event.target.remove()
|
||||
},
|
||||
onkeydown: async event => {
|
||||
if (event.key !== 'Escape') return
|
||||
await event.target.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 100 }).finished
|
||||
await event.target.remove()
|
||||
},
|
||||
children: [
|
||||
createElement({
|
||||
...options,
|
||||
style: {
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '150px',
|
||||
boxShadow: '0 0 1em #ccc',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
...options.style,
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
// 显示时自动聚焦
|
||||
const observer = new MutationObserver(mutationsList => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
|
||||
element.focus()
|
||||
element.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 100 }).finished
|
||||
return observer.disconnect()
|
||||
}
|
||||
}
|
||||
})
|
||||
observer.observe(document.body, { childList: true, subtree: true })
|
||||
return element
|
||||
}
|
||||
|
||||
// 深度代理, 用于监听数据的变化
|
||||
export function useProxy(data, callback, path = []) {
|
||||
if (Array.isArray(data)) {
|
||||
const array = class extends Array {
|
||||
constructor(args) {
|
||||
super(args)
|
||||
}
|
||||
push(args) {
|
||||
super.push(args)
|
||||
console.log('push', args)
|
||||
this.__notify()
|
||||
}
|
||||
pop(...args) {
|
||||
const result = super.pop(...args)
|
||||
console.log('pop')
|
||||
this.__notify()
|
||||
return result
|
||||
}
|
||||
shift(...args) {
|
||||
const result = super.shift(...args)
|
||||
this.__notify()
|
||||
return result
|
||||
}
|
||||
unshift(...args) {
|
||||
super.unshift(...args)
|
||||
this.__notify()
|
||||
}
|
||||
splice(...args) {
|
||||
super.splice(...args)
|
||||
this.__notify()
|
||||
}
|
||||
deleteProperty(...args) {
|
||||
console.log('deleteProperty', ...args)
|
||||
super.deleteProperty(...args)
|
||||
this.__notify()
|
||||
}
|
||||
__notify() {
|
||||
if (callback) callback()
|
||||
}
|
||||
}
|
||||
//console.log('为数组每项递归创建代理')
|
||||
data.forEach((item, index) => {
|
||||
data[index] = useProxy(item, callback, [...path, index])
|
||||
})
|
||||
return new array(...data)
|
||||
}
|
||||
if (typeof data === 'object' && data !== null) {
|
||||
//console.log('为对象属性递归创建代理')
|
||||
Object.keys(data).forEach(key => {
|
||||
if (typeof data[key] === 'object' && data[key] !== null) {
|
||||
data[key] = useProxy(data[key], callback, [...path, key])
|
||||
}
|
||||
})
|
||||
return new Proxy(data, {
|
||||
deleteProperty(target, key) {
|
||||
console.log('deleteProperty', key)
|
||||
return delete target[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
console.log('为其它类型直接返回')
|
||||
return data
|
||||
}
|
Reference in New Issue
Block a user