调试
webStorm
- 启动
- 在 idea 代码行标记断点
- control + shift 点击
dev url弹出新浏览器则进入调试界面
资源加载
images
- cdn
- 压缩(oss 支持)
- 先使用低质量图片展位,再加载原图
bugs
resize 跟 debounce 时机问题
背景:检测页面是否在前台以及 resize 进行 video 元素的播放跟暂停切换
原因:正常用户操作(切换tab)的时间间隔大于防抖间隔,因此逻辑是生效的;但是在某些情况下,比如直接使用代码进行页面的切换,导致这个间隔小于防抖间隔,原先某些 flag 作为判断是否成功的变量反而成为阻塞二次逻辑进行的阻碍(电脑性能导致的逻辑执行快慢,典型的它人电脑才能复现)
总结:因为防抖等操作创建的 flag 判断应该更加精准,细化,防止因为某些条件没有达成 flag 条件,但是却将 flag = true,导致下次的逻辑处理出现阻塞
html_element
input
[CSS]去掉 high light
scss
input:-webkit-autofill {
-webkit-text-fill-color: var(--text-fill-color, white);
-webkit-background-clip: text;
}react useRef 跟 BroadcastChannel
背景:封装一个窗体通信功能,使用 useRef 来保存引用,避免多次创建,其中一个功能需要保存一个引用对象,用于储存已读用户的信息
bug:每次给该引用对象设置新的值,但是在 BroadcastChannel.onmessage 的回调中该引用对象的值始终为空
typescript
const CHANNEL_MAP = new Map<string, BroadcastChannel>()
function createChannel<T = unknown>(
channelName: string,
onMessage: (data: T) => void
): ChannelRes<T> {
let channel: BroadcastChannel | null = null
if (CHANNEL_MAP.has(channelName)) {
channel = CHANNEL_MAP.get(channelName)!
} else {
channel = new BroadcastChannel(channelName)
CHANNEL_MAP.set(channelName, channel)
}
const promiseMap: Record<string, Receivers> = {} // 引用对象(放在 createChannel 内部)
const user = Math.random().toString().slice(-5)
channel.onmessage = () => {
console.log(promiseMap)
}
return {
broadcast(message) {
const uid = getUid()
Reflect.set(promiseMap, uid, []) // 更新对象
},
}
}
const channel = useRef(createChannel('alpha'))原因:useRef 接收值为函数执行返回结果,那么每次 react 更新这个函数都会执行,按常理说里面的引用对象都会更新,但是由于 channel 是固定的(被提升到 es 模块顶层,没有重新创建),而 channel.onmessage 只能注册一次, 即后续代码重新执行的时候,后续的 channel.onmessage 并没有生效,因此 channel.onmessage 内部使用引用的是第一次创建时候的 promiseMap,而其余地方的 promiseMap 早已随之更新,总结就是引用了不同的对象
解决:
- 方法1: 将
promiseMap移动到外部,即跟CHANNEL_MAP同级 - 方法2:
typescript
// 修改 createChannel 创建方式,避免多次创建,其引用对象也就不会改变了
const channel = useRef()
if (!channel.current) {
channel.current = createChannel('alpha')
}video
autoplay
<video src="" muted autoplay>
ts
// in weChat
elVideo.play().catch(() => {
typeof WeixinJSBridge !== 'undefined' &&
WeixinJSBridge.invoke('getNetworkType', {}, () => {
el.currentTime = 0
el.play()
})
})http
Streamable HTTP
引用
| Streamable | sse | |
|---|---|---|
| 断线重连 | 自动断线重连与数据恢复 | 客户端需要重新建立连接并手动恢复数据流 |
| 长连接 | 传输结束后服务器立即关闭连接 | 依赖持久化长连接 |
| 格式 | text/event-stream | |
| 资源消耗 | 低 | 高(SSE需为每个客户端维持TCP长连接) |
- 服务端
ts
import { NextResponse } from 'next/server'
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
const messages = ['Hello', 'World', 'Streaming', 'Example']
for (const message of messages) {
controller.enqueue(encoder.encode(`${message}\n`))
await new Promise((resolve) => setTimeout(resolve, 1000)) // 模拟延迟
}
controller.close()
},
})
return new NextResponse(stream, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Transfer-Encoding': 'chunked',
},
})
}
export async function POST() {
return NextResponse.json({ name: 'rick' })
}- 客户端
tsx
'use client'
export default function Test() {
function handleClick() {
const decoder = new TextDecoder()
// body.getReader
fetch('/api/note', { method: 'GET' })
.then((r) => r.body!.getReader())
.then((reader) =>
reader.read().then(function process({ done, value }): Promise<string> {
if (done) return Promise.resolve('')
console.log(decoder.decode(value))
return reader.read().then(process)
})
)
// 遍历方式进行读取,使用 total 进行统计
fetch('/api/note', { method: 'GET' }).then(async (response) => {
const total: number[] = []
for await (const _chunk of response.body! as unknown as NodeJS.ReadableStream) {
const chunk = _chunk as Uint8Array
total.push(...chunk)
// console.log(decoder.decode(chunk))
}
console.log(decoder.decode(new Uint16Array(total))) // total 统计时候,已经变成了 number[] 类型,decoder.decode 需要接受 Arraybuffer 类型, 然后浏览器默认使用 utf-8 格式,对应 Uint8Array
})
}
function anotherHttp() {
// 将流又重新转成 Response,整体流程实际上就相当于 response => response.text()
fetch('/api/note')
.then((response) => response.body)
.then((stream) =>
// Respond with our stream
new Response(stream, { headers: { 'Content-Type': 'text/html' } }).text()
)
.then((result) => {
// Do things with result
console.log(result)
})
}
return (
<div className="flex gap-2">
<button
className="cursor-pointer bg-amber-200 p-2"
onClick={handleClick}
>
发送请求
</button>
<button
className="cursor-pointer bg-amber-200 p-2"
onClick={anotherHttp}
>
Response 流转
</button>
</div>
)
}SSE 流式传输
一个基于 http 协议的通信技术
代码实现
服务端
javascriptconst express = require('express') const app = express() app.get('/events', (req, res) => { res.setHeader('Content-Type', 'text/event-stream') res.setHeader('Cache-Control', 'no-cache') res.setHeader('Connection', 'keep-alive') const startTime = Date.now() const sendEvent = () => { // 检查是否已经发送了10秒 if (Date.now() - startTime >= 10000) { res.write('event: close\ndata: {}\n\n') // 发送一个特殊事件通知客户端关闭 res.end() // 关闭连接 return } const data = { message: 'Hello World', timestamp: new Date() } res.write(`data: ${JSON.stringify(data)}\n\n`) // 每隔2秒发送一次消息 setTimeout(sendEvent, 2000) } sendEvent() }) app.listen(3000)客户端
html<!--客户端--> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>SSE Example</title> </head> <body> <h1>Server-Sent Events Example</h1> <div id="messages"></div> <script> const evtSource = new EventSource('/events') const messages = document.getElementById('messages') evtSource.onmessage = function (event) { const newElement = document.createElement('p') const eventObject = JSON.parse(event.data) newElement.textContent = 'Message: ' + eventObject.message + ' at ' + eventObject.timestamp messages.appendChild(newElement) } </script> </body> </html>应用场景
倒计时同步、实时天气、实时股票、库存更新
技术对比
特性/因素 SSE WebSocket 协议 基于HTTP,使用标准HTTP连接 单独的协议(ws:// 或 wss://),需要握手升级 通信方式 单向通信(服务器到客户端) 全双工通信 数据格式 文本(UTF-8编码) 文本或二进制 重连机制 浏览器自动重连 需要手动实现重连机制 实时性 高(适合频繁更新的场景) 非常高(适合高度交互的实时应用) 浏览器支持 良好(大多数现代浏览器支持) 非常好(几乎所有现代浏览器支持) 适用场景 实时通知、新闻feed、股票价格等需要从服务器推送到客户端的场景 在线游戏、聊天应用、实时交互应用 复杂性 较低,易于实现和维护 较高,需要处理连接的建立、维护和断开 兼容性和可用性 基于HTTP,更容易通过各种中间件和防火墙 可能需要配置服务器和网络设备以支持WebSocket 服务器负载 适合较低频率的数据更新 适合高频率消息和高度交互的场景
