Skip to content

Commit

Permalink
feat: 移除 FullScreen 兼容性实现,新增 eventBusPromise
Browse files Browse the repository at this point in the history
  • Loading branch information
lhvision committed Jan 7, 2025
1 parent 4b54145 commit 1d8c486
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 101 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ import { } from '@lhvision/helpers/upload'
- `AsyncLRUCache` - 异步 LRU 缓存
- `LRUCache` - LRU 缓存
- `eventBus` - 单例事件总线,可以实现在响应拦截器中路由跳转等功能
- `eventBusPromise` - 简单的单发布订阅模式
- `singleton` - 使用 Proxy 代理模式实现单例模式

### upload.js
Expand Down
143 changes: 143 additions & 0 deletions scripts/es2024.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
ECMAScript(简称 ES)作为 JavaScript 的标准,每年都会推出新的特性,不断优化我们的开发体验,分享几个实用的新特性。

1. Temporal API - 现代化的日期时间处理

```javascript
// 旧写法
const now = new Date()
const year = now.getFullYear()
const month = now.getMonth() + 1 // 注意 month 是从 0 开始的
const day = now.getDate()

// ES2024写法
const now1 = Temporal.Now.plainDateTimeISO()
const year1 = now1.year
const month1 = now1.month
const day1 = now1.day
```

`Temporal API`提供了更直观和不可变的日期时间操作方式。它解决了传统`Date API`的许多问题,比如更容易处理时区、更清晰的方法名称,以及更可预测的行为。

2. 数组分组操作 - Object.groupBy 和 Map.groupBy

```javascript
// 旧写法
const groups = users.reduce((acc, user) => {
if (!acc[user.role]) {
acc[user.role] = []
}
acc[user.role].push(user)
return acc
}, {})

// ES2024写法
const groups1 = Object.groupBy(users, user => user.role)
const groupsMap = Map.groupBy(users, user => user.role)
```

这个新特性极大简化了数据分组操作。Object.groupBy 返回普通对象,而 Map.groupBy 返回 Map 实例。它们都接受一个回调函数来决定分组的键,避免了手动实现分组逻辑。

3. RegExp match indices

```javascript
// 旧写法
const str = 'hello world'
const regexp = /world/
const match = str.match(regexp)
const start = match.index

// ES2024写法
const str1 = 'hello world'
const regexp1 = /world/d
const match1 = str1.match(regexp)
const start1 = match.indices[0][0]
// 直接返回索引位置,无需再处理匹配结果。
```

4. Atomics.waitAsync - 异步等待

```javascript
// 旧写法
while (Atomics.load(sharedInt32Array, 0) !== 1) {
await new Promise(resolve => setTimeout(resolve, 0))
}

// ES2024写法
await Atomics.waitAsync(sharedInt32Array, 0, 0).value
```

`waitAsync`提供了一种非阻塞方式来等待共享内存的变化,避免了手动实现轮询逻辑,更适合在现代`Web Workers`中使用。

5. ArrayBuffer.prototype.transfer - 高效内存转移

```javascript
// 旧写法
const newBuffer = new ArrayBuffer(buffer.byteLength)
new Uint8Array(newBuffer).set(new Uint8Array(buffer))

// ES2024写法
const newBuffer1 = buffer.transfer()
transfer()
```

方法提供了零拷贝方式转移`ArrayBuffer`的所有权,原`buffer`会被置为 0 长度。这在处理大型二进制数据时特别有用,可以显著提高性能。

6. 结构化错误堆栈 - Error.prototype.cause

```javascript
// 旧写法
try {
doSomething()
}
catch (error) {
console.error('Operation failed:', error)
throw error
}

// ES2024写法
try {
doSomething()
}
catch (error) {
throw new Error('Operation failed', {
cause: error,
stack: { structured: true }
})
}
```

新的错误处理方式支持结构化堆栈信息,使错误追踪和调试更容易。通过`cause`属性可以保留完整的错误链,`structured: true`提供更详细的堆栈信息。

7. 弱引用集合方法改进

```javascript
// 旧写法
const weakRef = new WeakRef(obj)
if (weakRef.deref()) {
// 使用对象
}

// ES2024写法
const weakSet = new WeakSet([obj])
if (weakSet.has(obj)) {
weakSet.cleanup() // 显式清理失效引用
}
```

新增的`cleanup()`方法允许显式触发垃圾回收,避免内存泄露。

8. Promise.withResolvers() - 简化 Promise 创建

```javascript
// 旧写法
let resolvePromise, rejectPromise
const promise = new Promise((resolve, reject) => {
resolvePromise = resolve
rejectPromise = reject
})

// ES2024写法
const { promise: p, resolve, reject } = Promise.withResolvers()
```

`withResolvers()`让我们在一行代码中同时获取 promise 及其控制函数,避免了使用闭包来获取 resolve 和 reject 函数的复杂写法。特别适合需要在外部控制 Promise 状态的场景。
73 changes: 22 additions & 51 deletions src/browser/__tests__/fullScreen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,18 @@ import {
toggleFullScreen,
} from '../fullScreen'

describe('fullscreen API', () => {
describe('全屏 API', () => {
let element: HTMLElement

// 模拟全屏状态
let isFullscreen = false

// 模拟全屏元素
let fullscreenElement: HTMLElement | null = null

// 模拟全屏变化事件的监听器
const listeners: { [key: string]: (() => void)[] } = {}

beforeEach(() => {
// 创建一个测试元素
// 创建测试元素
element = document.createElement('div')
document.body.appendChild(element)

// 重置全屏状态
// 重置状态
isFullscreen = false
fullscreenElement = null

Expand All @@ -37,43 +31,39 @@ describe('fullscreen API', () => {
listeners[key] = []
}

// 模拟 HTMLElement 的 requestFullscreen 方法
// 模拟全屏请求方法
element.requestFullscreen = vi.fn().mockImplementation(() => {
if (!isFullscreen) {
isFullscreen = true
fullscreenElement = element
// 触发 fullscreenchange 事件
listeners.fullscreenchange?.forEach(callback => callback())
}
return Promise.resolve()
})

// 模拟 Document 的 exitFullscreen 方法
// 模拟退出全屏方法
document.exitFullscreen = vi.fn().mockImplementation(() => {
if (isFullscreen) {
isFullscreen = false
fullscreenElement = null
// 触发 fullscreenchange 事件
listeners.fullscreenchange?.forEach(callback => callback())
}
return Promise.resolve()
})

// 模拟文档的 fullscreenElement 属性
// 模拟全屏元素属性
Object.defineProperty(document, 'fullscreenElement', {
get: () => fullscreenElement,
configurable: true,
})

// 模拟 addEventListener
// 模拟事件监听器
document.addEventListener = vi.fn().mockImplementation((event, callback) => {
if (!listeners[event]) {
if (!listeners[event])
listeners[event] = []
}
listeners[event].push(callback as () => void)
})

// 模拟 removeEventListener
document.removeEventListener = vi.fn().mockImplementation((event, callback) => {
if (listeners[event]) {
listeners[event] = listeners[event].filter(cb => cb !== callback)
Expand All @@ -82,111 +72,92 @@ describe('fullscreen API', () => {
})

afterEach(() => {
// 清理 DOM
document.body.removeChild(element)
// 清除所有模拟
vi.resetAllMocks()
// 重置事件监听器
for (const key in listeners) {
listeners[key] = []
}
})

it('should enter fullscreen', async () => {
it('应该能够进入全屏模式', async () => {
await requestFullScreen(element)
expect(element.requestFullscreen).toHaveBeenCalledTimes(1)
expect(isFullscreen).toBe(true)
expect(isFullScreen()).toBe(true)
expect(getFullScreenElement()).toBe(element)
})

it('should exit fullscreen', async () => {
it('应该能够退出全屏模式', async () => {
await requestFullScreen(element)
expect(isFullscreen).toBe(true)
await exitFullScreen()
expect(document.exitFullscreen).toHaveBeenCalledTimes(1)
expect(isFullscreen).toBe(false)
expect(isFullScreen()).toBe(false)
expect(getFullScreenElement()).toBe(null)
})

it('should toggle fullscreen', async () => {
it('应该能够切换全屏状态', async () => {
await toggleFullScreen(element)
expect(element.requestFullscreen).toHaveBeenCalledTimes(1)
expect(isFullscreen).toBe(true)
expect(isFullScreen()).toBe(true)
expect(getFullScreenElement()).toBe(element)

await toggleFullScreen(element)
expect(document.exitFullscreen).toHaveBeenCalledTimes(1)
expect(isFullscreen).toBe(false)
expect(isFullScreen()).toBe(false)
expect(getFullScreenElement()).toBe(null)
})

it('should listen to fullscreen change', async () => {
it('应该能够监听全屏状态变化', async () => {
const handleFullScreenChange = vi.fn(() => {
expect(isFullScreen()).toBe(true)
offFullScreenChange(handleFullScreenChange)
})

onFullScreenChange(handleFullScreenChange)
await requestFullScreen(element)

expect(handleFullScreenChange).toHaveBeenCalledTimes(1)
})

it('should handle multiple fullscreen change listeners', async () => {
it('应该能够处理多个全屏变化监听器', async () => {
const callback1 = vi.fn()
const callback2 = vi.fn()

onFullScreenChange(callback1)
onFullScreenChange(callback2)

await requestFullScreen(element)

expect(callback1).toHaveBeenCalledTimes(1)
expect(callback2).toHaveBeenCalledTimes(1)

await exitFullScreen()

expect(callback1).toHaveBeenCalledTimes(2)
expect(callback2).toHaveBeenCalledTimes(2)
})

it('should not fail if Fullscreen API is not supported', async () => {
// 移除 requestFullscreen 方法
element.requestFullscreen = undefined as any
// it('当全屏 API 不支持时应该抛出错误', async () => {
// element.requestFullscreen = undefined as any
// await expect(requestFullScreen(element)).rejects.toThrow(
// 'element.requestFullscreen is not a function',
// )
// })

await expect(requestFullScreen(element)).rejects.toThrow(
'Fullscreen API is not supported',
)
})

it('should correctly get the current fullscreen element', async () => {
it('应该能够正确获取当前全屏元素', async () => {
expect(getFullScreenElement()).toBe(null)
await requestFullScreen(element)
expect(getFullScreenElement()).toBe(element)
await exitFullScreen()
expect(getFullScreenElement()).toBe(null)
})

it('should handle exitFullScreen when not in fullscreen', async () => {
it('在非全屏状态下调用退出全屏不应该有效果', async () => {
await exitFullScreen()
expect(document.exitFullscreen).not.toHaveBeenCalled()
expect(isFullscreen).toBe(false)
expect(isFullScreen()).toBe(false)
expect(getFullScreenElement()).toBe(null)
})

it('should handle requestFullScreen when already in fullscreen', async () => {
it('在全屏状态下重复请求全屏不应该重复调用 API', async () => {
await requestFullScreen(element)
expect(element.requestFullscreen).toHaveBeenCalledTimes(1)
await requestFullScreen(element)
// 根据实现,应该不会再次调用 requestFullscreen
expect(element.requestFullscreen).toHaveBeenCalledTimes(1)
expect(isFullscreen).toBe(true)
expect(isFullScreen()).toBe(true)
expect(getFullScreenElement()).toBe(element)
})
})
Loading

0 comments on commit 1d8c486

Please sign in to comment.