浅入解析 React Fiber 结构

浅入解析 React Fiber 结构

React Fiber 是 React 中用于表示组件树的一种数据结构,它的设计和实现是 React 中的一项重要内容。本文将深入探讨 React Fiber 的结构,包括其所有属性及其含义,并对属性中的对象类型进行详细说明和解释。通过阅读本文,读者将更好地理解 React Fiber 的内部机制。

React@18+ Fiber 结构概述

在 React 中,每个组件都对应一个 Fiber 对象,用于表示组件树中的一个节点。以下是 Fiber 对象的结构定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type Fiber = {
tag: WorkTag,
key: null | string,
elementType: string | FunctionComponent | ClassComponent | HostComponent | SuspenseComponent | ...,
type: string | FunctionComponent | ClassComponent | HostComponent | SuspenseComponent | ...,
stateNode: HTMLElement | Component | null,
return: Fiber | null,
child: Fiber | null,
sibling: Fiber | null,
index: number,
ref: RefObject | null,
pendingProps: any,
memoizedProps: any,
updateQueue: UpdateQueue<any> | null,
memoizedState: Hook | StateObject | null,
dependencies: Dependencies | null,
mode: TypeOfMode,
effectTag: SideEffectTag,
nextEffect: Fiber | null,
firstEffect: Fiber | null,
lastEffect: Fiber | null,
lanes: Lanes,
childLanes: Lanes,
alternate: Fiber | null,
// ...
};

根据提供的 Fiber 类型定义,下面是完整的 Fiber 节点的属性列表:

  1. tag

    • 标识 Fiber 节点的类型,如 HostComponentClassComponentFunctionComponent 等。
  2. key

    • 用于在DOM更新期间识别节点。可以是字符串类型或 null。
  3. elementType

    • 元素类型,通常是 React.createElement() 中传递的类型,用于保持节点的身份。
  4. type

    • 节点的具体类型,与 elementType 相似,但对于 ClassComponent 等需要再次处理。
  5. stateNode

    • 与此 Fiber 节点关联的实际 DOM 节点、组件实例或其他实体。
  6. return

    • 指向此节点的父节点。
  7. child

    • 指向此节点的第一个子节点。
  8. sibling

    • 指向此节点的下一个兄弟节点。
  9. index

    • 表示节点在兄弟节点中的位置索引。
  10. ref

    • 表示与此节点关联的 ref,可以是函数、字符串或 RefObject 对象。
  11. refCleanup

    • 用于清理 ref 的函数。
  12. pendingProps

    • 待处理的属性,即将应用于此节点的属性。
  13. memoizedProps

    • 表示此节点最近一次渲染时应用的属性。
  14. updateQueue

    • 包含了所有待处理的更新操作。
  15. memoizedState

    • 上一次渲染时的状态。如果组件使用了 Hooks,那么 memoizedState 就应该是一个链表结构,每个节点表示一个 Hook 的状态值。如果组件是类组件,则 memoizedState 应该是该组件在上一次渲染时的状态对象。
  16. dependencies

    • 表示此节点更新所依赖的上下文、Props、State等信息。
  17. mode

    • 表示当前渲染模式,如并发模式。
  18. flags

    • 描述 Fiber 节点和其子树的一些属性的位标志,用于标记节点需要执行的操作。
  19. subtreeFlags

    • 描述 Fiber 子树的属性的位标志,用于标记节点需要执行的操作。
  20. deletions

    • 用于存储要删除的 Fiber 节点。
  21. lanes

    • 表示此节点的调度优先级。
  22. childLanes

    • 表示此节点子树中的调度优先级。
  23. alternate

    • 指向上一次渲染时与当前 Fiber 节点对应的 Fiber 节点。
  24. actualDuration

    • 当前渲染阶段的实际持续时间,用于性能分析。
  25. actualStartTime

    • 当前渲染阶段的开始时间,用于性能分析。
  26. selfBaseDuration

    • 最近一次渲染阶段的持续时间,不包括子节点。
  27. treeBaseDuration

    • 所有子节点渲染阶段持续时间的总和。
  28. _debugInfo

    • 用于调试的附加信息。
  29. _debugOwner

    • 指向此节点的拥有者。
  30. _debugIsCurrentlyTiming

    • 标志位,指示当前是否正在记录渲染时间。
  31. _debugNeedsRemount

    • 标志位,指示是否需要重新挂载组件。
  32. _debugHookTypes

    • 用于调试的 hook 类型信息。

这些属性组成了 Fiber 节点的完整表示,用于 React 内部的渲染和更新过程。

属性详解

tag

tag 属性表示 Fiber 节点的类型,其值可以是以下几种之一:

  • HostRoot: 表示根节点。
  • FunctionComponent: 表示函数组件。
  • ClassComponent: 表示类组件。
  • HostComponent: 表示 DOM 元素。
  • ContextProvider: 表示 Context 提供者。
  • ContextConsumer: 表示 Context 消费者。
  • SuspenseComponent: 表示 Suspense 组件。
  • DehydratedFragment: 表示脱水片段。

memoizedState

memoizedState 属性表示组件在上一次渲染时的状态,其类型根据组件的具体情况而定。如果组件使用了 Hooks,那么 memoizedState 就应该是一个链表结构,每个节点表示一个 Hook 的状态值。如果组件是类组件,则 memoizedState 应该是该组件在上一次渲染时的状态对象。memoizedState 具体的属性如下所示

  • memoizedState: 组件的记忆状态,即上次渲染时的状态。
  • next: 指向下一个 hook 节点的指针。

flags

flags 属性用于标记节点需要执行的操作,其值是一个位掩码,描述 Fiber 节点的不同状态和行为,并在调度和渲染过程中起着重要作用,包含以下几种标记:

这里有一些重要的标志位和常量的含义:

  • NoFlags: 用于表示没有任何状态或行为。
  • Update: 表示组件需要更新。
  • Placement: 表示组件需要被放置到 DOM 树中。
  • ChildDeletion: 表示组件的子节点被删除。
  • Callback: 表示需要执行回调函数。
  • Visibility: 表示组件的可见性发生变化。
  • Ref: 表示组件的引用发生变化。
  • Snapshot: 表示需要获取组件的快照。
  • Passive: 表示组件处于被动模式。
  • StoreConsistency: 表示需要保持状态的一致性。

以前使用 effectTag 属性来表示副作用。

源码解析

React Fiber 的源码位于 React 源码库中的 react-reconciler 模块。读者可以在该模块中找到 Fiber 结构的定义以及相关的操作和算法实现。

总结

本文浅入解析了 React Fiber 结构,介绍了其所有属性及其含义,并对对象类型的属性进行了进一步说明和解释。通过深入理解 Fiber 结构,可以更好地理解 React 内部的工作原理,并能够更加高效地使用 React 进行开发。

跨浏览器标签页进行通讯的方式简介

跨浏览器标签页进行通讯的方式简介

在现代 Web 应用程序中,跨浏览器标签页之间进行通讯是一项重要的功能。无论是在多标签页应用程序中同步状态,还是在不同浏览器窗口之间共享数据,实现跨标签页通讯都是必不可少的。在本文中,我们将探讨跨浏览器标签页进行通讯的各种方式,并详细介绍每种方式的 API 和使用场景。

所有方法

  1. 使用 Web Storage API
  2. 使用 Broadcast Channel API
  3. 使用 SharedWorker
  4. 使用 Service Worker
  5. 使用 WebSocket
  6. 使用 PostMessage API
  7. 使用 IndexedDB

Web Storage API

简介

  • 简介Web Storage API 提供了一种在客户端存储数据的方法,包括 localStoragesessionStorage 两种方式。它们可以在不同的浏览器标签页之间共享数据,而不受页面刷新或关闭的影响。
  • 优点:简单易用,支持持久化存储。
  • 缺点:只能存储字符串类型的数据,且容量有限。
  • 适用场景:适合存储小型数据,如用户偏好设置或临时状态。

API

  • localStorage: 保存的数据没有过期时间,可以一直存在于浏览器中。
  • sessionStorage: 保存的数据在浏览器会话结束时被清除,适合临时存储数据。

使用场景

  • 在同一浏览器的不同标签页中共享数据。
  • 存储用户首选项或状态信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Storage Example</title>
</head>
<body>
<input type="text" id="input">
<button onclick="saveData()">Save Data</button>
<button onclick="getData()">Get Data</button>

<script>
function saveData() {
const input = document.getElementById('input').value;
localStorage.setItem('data', input);
}

function getData() {
const data = localStorage.getItem('data');
alert(data);
}

window.addEventListener('storage', event => {
alert('Data changed in another tab: ' + event.newValue);
});
</script>
</body>
</html>

<!-- another-page.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Another Page</title>
</head>
<body>
<button onclick="getData()">Get Data from Main Page</button>

<script>
function getData() {
const data = localStorage.getItem('data');
alert(data);
}

window.addEventListener('storage', event => {
alert('Data changed in another tab: ' + event.newValue);
});
</script>
</body>
</html>

在一个页面中输入数据并保存,然后在另一个页面中点击按钮获取数据。同时,当一个标签页修改了 localStorage 的值,另一个标签页也会收到通知。

Broadcast Channel API

简介

  • 简介Broadcast Channel API 允许在不同的浏览器标签页之间进行实时通信,通过创建一个共享的消息通道来传递数据。
  • 优点:支持实时通信,消息发送和接收都非常简单。
  • 缺点:不支持 IE 浏览器。
  • 适用场景:适合需要实时通讯的场景,如多标签页间的数据同步。

API

  • BroadcastChannel: 创建一个用于跨文档通信的通道。

使用场景

  • 在不同的浏览器标签页之间传递消息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Broadcast Channel Example</title>
</head>
<body>
<input type="text" id="input">
<button onclick="sendMessage()">Send Message</button>

<script>
const channel = new BroadcastChannel('channel');

function sendMessage() {
const input = document.getElementById('input').value;
channel.postMessage(input);
}
</script>
</body>
</html>

<!-- another-page.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Another Page</title>
</head>
<body>
<p id="message"></p>

<script>
const channel = new BroadcastChannel('channel');

channel.onmessage = event => {
document.getElementById('message').textContent = event.data;
};
</script>
</body>
</html>

在一个页面中输入消息并发送,在另一个页面中接收并显示消息。

SharedWorker

简介

  • 简介SharedWorker 允许在多个浏览器上下文之间共享同一个 Worker 实例,提供了一种全局范围的通讯机制。
  • 优点:支持多标签页之间的实时通信,可以与所有标签页共享相同的数据。
  • 缺点:不支持 IE 浏览器。
  • 适用场景:适合需要共享状态或实现实时通讯的场景。

API

  • SharedWorker: 创建一个共享的 Web Worker 实例,可以被多个浏览上下文共享。

使用场景

  • 在不同浏览器标签页之间共享数据或进行通讯。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SharedWorker Example</title>
</head>
<body>
<input type="text" id="input">
<button onclick="sendMessage()">Send Message</button>

<script>
const worker = new SharedWorker('worker.js');

function sendMessage() {
const input = document.getElementById('input').value;
worker.port.postMessage(input);
}

worker.port.onmessage = event => {
alert(event.data);
};
</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
// worker.js
const ports = [];

onconnect = event => {
const port = event.ports[0];
ports.push(port);
port.onmessage = event => {
const message = event.data;
ports.forEach(port => port.postMessage(message));
};
};

在一个页面中输入消息并发送,在另一个页面中接收并显示消息。

Service Worker

简介

  • 简介Service Worker 是一种在浏览器后台运行的脚本,可以拦截和处理网络请求,并实现离线缓存和推送通知等功能。
  • 优点:支持后台运行,可以拦截网络请求,实现离线缓存和推送通知。
  • 缺点:只能用于现代浏览器,且需要 HTTPS 支持。
  • 适用场景:适合需要离线访问或推送通知的场景,如聊天应用或离线应用。

API

  • Service Worker: 在后台运行的脚本,可以拦截和处理网络请求,并进行推送通知等功能。

使用场景

  • 在不同标签页之间共享数据或进行通讯。
  • 实现离线缓存和推送通知。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Service Worker Example</title>
</head>
<body>
<input type="text" id="input">
<button onclick="sendMessage()">Send Message</button>

<script>
// 注册 Service Worker
navigator.serviceWorker.register('sw.js');

// 发送消息
function sendMessage() {
const input = document.getElementById('input').value;
navigator.serviceWorker.controller.postMessage(input);
}

// 监听消息
navigator.serviceWorker.addEventListener('message', event => {
alert(event.data);
});
</script>
</body>
</html>
1
2
3
4
5
6
7
// sw.js
self.addEventListener('message', event => {
const message = event.data;
clients.matchAll().then(clients => {
clients.forEach(client => client.postMessage(message));
});
});

在一个页面中输入消息并发送,在另一个页面中接收并显示消息。

WebSocket

简介

  • 简介WebSocket 提供了一种在客户端和服务器之间建立持久连接的方式,实现了双向通信。
  • 优点:支持双向通信,可以实现实时通讯。
  • 缺点:需要在服务器端实现 WebSocket 服务,且不支持跨域请求。
  • 适用场景:适合实时通讯场景,如聊天应用或在线游戏。

API

  • WebSocket: 在客户端和服务器之间建立持久连接,实现双向通信。

使用场景

在不同浏览器标签页之间进行实时通讯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// server.js
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', ws => {
ws.on('message', message => {
wss.clients.forEach(client => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Example</title>
</head>
<body>
<input type="text" id="input">
<button onclick="sendMessage()">Send Message</button>

<script>
const socket = new WebSocket('ws://localhost:8080');

function sendMessage() {
const input = document.getElementById('input').value;
socket.send(input);
}

socket.onmessage = event => {
alert(event.data);
};
</script>
</body>
</html>

在一个页面中输入消息并发送,在另一个页面中接收并显示消息。

PostMessage API

简介

  • 简介PostMessage API 允许跨文档之间安全地传递消息,可以实现跨域通信。
  • 优点:支持跨域通信,使用简单。
  • 缺点:需要对接收消息的文档进行信任验证,存在安全风险。
  • 适用场景:适合不同域名之间的数据交换或通信。

API

  • window.postMessage(): 向其他窗口发送消息。

使用场景

  • 在不同窗口之间进行跨域通信。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PostMessage Example</title>
</head>
<body>
<input type="text" id="input">
<button onclick="sendMessage()">Send Message</button>

<script>
const popup = window.open('another-page.html');

function sendMessage() {
const input = document.getElementById('input').value;
popup.postMessage(input, '*');
}
</script>
</body>
</html>

<!-- another-page.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Another Page</title>
</head>
<body>
<p id="message"></p>

<script>
window.addEventListener('message', event => {
document.getElementById('message').textContent = event.data;
});
</script>
</body>
</html>

在一个页面中输入数据并保存,然后在另一个页面中点击按钮获取数据。

IndexedDB

简介

  • 简介IndexedDB 提供了一个异步的、事务型的数据库,适用于存储大量结构化数据。
  • 优点:支持存储大量结构化数据,数据存储在客户端本地。
  • 缺点:使用复杂,需要学习 IndexedDBAPI
  • 适用场景:适合需要存储大量结构化数据的场景,如离线应用或数据分析应用。

API

  • IndexedDB: 提供了一个异步的、事务型的数据库。

使用场景

  • 存储大量结构化数据,如离线应用程序的数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IndexedDB Example</title>
</head>
<body>
<input type="text" id="input">
<button onclick="saveData()">Save Data</button>
<button onclick="getData()">Get Data</button>

<script>
const request = indexedDB.open('myDatabase');

request.onupgradeneeded = event => {
const db = event.target.result;
const objectStore = db.createObjectStore('data', { keyPath: 'id' });
};

function saveData() {
const input = document.getElementById('input').value;
const db = request.result;
const transaction = db.transaction('data', 'readwrite');
const objectStore = transaction.objectStore('data');
objectStore.add({ id: 1, data: input });
}

function getData() {
const db = request.result;
const transaction = db.transaction('data', 'readonly');
const objectStore = transaction.objectStore('data');
const request = objectStore.get(1);
request.onsuccess = event => {
const data = event.target.result;
alert(data.data);
};
}
</script>
</body>
</html>

<!-- another-page.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Another Page</title>
</head>
<body>
<button onclick="getData()">Get Data from Main Page</button>

<script>
function getData() {
const request = indexedDB.open('myDatabase');
request.onsuccess = event => {
const db = event.target.result;
const transaction = db.transaction('data', 'readonly');
const objectStore = transaction.objectStore('data');
const request = objectStore.get(1);
request.onsuccess = event => {
const data = event.target.result;
alert(data.data);
};
};
}
</script>
</body>
</html>

在一个页面中输入数据并保存,然后在另一个页面中点击按钮获取数据。

总结

在本文中,我们介绍了跨浏览器标签页进行通讯的多种方式,并提供了详细的 API 和使用场景。根据您的具体需求和项目要求

WebRTC:实时通信的未来

引言

在当今数字化的世界中,实时通信变得越来越重要。从视频会议到在线游戏,从远程医疗到物联网设备,人们希望能够立即与其他人或设备进行交流和互动。WebRTC(Web Real-Time Communication) 技术的出现填补了这一需求,为Web应用程序提供了强大的实时通信能力。本文将深入探讨 WebRTC 技术的原理、应用场景以及实现细节,带您了解这一引人注目的技术。

什么是WebRTC?

WebRTC 是一种开放标准的实时通信技术,允许浏览器之间进行点对点的音频、视频和数据传输,而无需任何插件或附加软件。它是由Google、Mozilla和Opera等公司发起的一个开放源代码项目,旨在为Web应用程序提供实时通信的能力。

WebRTC的核心特性

  1. 实时性
    WebRTC 提供了实时的音频和视频传输能力,使用户可以在几乎没有延迟的情况下进行实时通信。这种实时性对于视频会议、在线教育和远程医疗等应用非常重要。

  2. 安全性
    WebRTC 通过使用加密技术来保护通信内容的安全性。它使用 DTLS(Datagram Transport Layer Security)协议来保护数据传输的隐私,并使用 SRTP(Secure Real-time Transport Protocol)协议来加密音频和视频数据。

  3. 去中心化
    WebRTC 采用了点对点(P2P)通信模型,即浏览器之间直接进行通信,而不需要经过中间服务器。这种去中心化的通信方式可以提高通信的效率,并减少了对服务器资源的依赖。

  4. 跨平台
    WebRTC 可以在各种平台上运行,包括桌面浏览器、移动浏览器和原生应用程序。这使得开发人员可以轻松地创建跨平台的实时通信应用程序。

名词和方法解释

RTCPeerConnection

RTCPeerConnectionWebRTC API 中的一个关键接口,用于在两个对等端之间建立点对点连接。它提供了一种实现浏览器之间实时音视频通信的方式,允许在不同浏览器之间直接传输数据,而无需通过服务器。通过 RTCPeerConnection,用户可以在浏览器中创建一个实时通信的会话,并在其中发送和接收音频、视频或其他任何类型的数据。

作用

RTCPeerConnection 的主要作用包括:

  1. 建立和管理对等连接:RTCPeerConnection 提供了一种方法来建立和管理两个浏览器之间的点对点连接,使它们可以直接通信,无需通过中间服务器。

  2. 处理媒体流:它允许用户将本地音频、视频或数据流发送到远程对等端,并从远程对等端接收相应的媒体流。

  3. 网络协商和协议处理:RTCPeerConnection 处理与 ICESDP 等相关的网络协商和协议处理,以确保对等连接的正确建立和维护。

  4. 实现信号传输和媒体传输:它还负责在对等连接之间传输信令消息和媒体数据,以确保通信的顺利进行。

参数

RTCPeerConnection 构造函数包含的1个参数,一个 RTCConfiguration 对象,用于指定配置参数。这个对象包含以下属性:

  • iceServers:一个 RTCIceServer 对象数组,用于指定 ICE 服务器信息。
    • urlsICE 服务器的 URLURL 数组。
    • username (可选):ICE 服务器的用户名。
    • credential (可选):ICE 服务器的密码。
  • iceTransportPolicy (可选):一个枚举值,用于指定 ICE 传输策略,默认为 "all",目前支持的值有 "all" | "relay"
  • bundlePolicy (可选):一个枚举值,用于指定 SDP 处理策略,默认为 "balanced",目前支持的值有 "balanced" | "max-bundle" | "max-compat"
  • rtcpMuxPolicy (可选):一个枚举值,用于指定 RTCP 复用策略,默认为 "require",目前支持的值有 "require"
  • peerIdentity (可选):PEER 身份验证,默认为 null。
  • certificates (可选):一个 RTCCertificate 对象数组,用于指定本地证书数组。
  • iceCandidatePoolSize (可选):一个数值,指定 ICE 候选地址池的大小。

属性和方法

  1. restartIce()

restartIce() 方法用于在 WebRTC 对等连接中重新启动 ICE 连接过程。ICE(Interactive Connectivity Establishment)是一种用于建立对等连接的网络协议,它可以帮助确定网络上的最佳路径,以确保数据能够在两个 RTCPeerConnection 之间进行有效传输。

重新启动 ICE 连接过程通常在网络连接发生变化或连接质量变差时使用,以尝试建立更稳定的连接。这可能包括重新检测网络接口、重新收集 ICE 候选项、更新 ICE Agent 状态等操作。

restartIce() 方法不接受任何参数,调用该方法将触发 ICE 连接重新启动过程。在调用此方法之后,ICE Agent 将重新开始收集候选项并尝试建立新的 ICE 连接。

下面是一个示例代码:

1
2
// 重新启动 ICE 连接过程
peerConnection.restartIce();
  1. setLocalDescription
    setLocalDescription() 方法用于将本地描述(即 SDP,Session Description Protocol)设置为 RTCPeerConnection 对象的本地描述。本地描述包含了当前对等连接的配置信息,例如媒体类型、编解码器、传输协议等。

通常情况下,当我们创建一个新的对等连接时,会先通过调用 createOffer()createAnswer() 方法生成一个 SDP 描述,并将其设置为本地描述。然后,我们将本地描述发送给远程对等方,远程对等方通过调用 setRemoteDescription() 方法将其应用到对等连接中。这样,两个对等方就可以根据各自的本地描述进行协商,建立一条可靠的连接。

以下是一个简单的示例,展示了如何使用 setLocalDescription() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建 RTCPeerConnection
const peerConnection = new RTCPeerConnection();

// 创建一个 SDP offer
peerConnection.createOffer()
.then((offer) => {
// 设置本地描述为 SDP offer
return peerConnection.setLocalDescription(offer);
})
.then(() => {
console.log("Local description set successfully.");
})
.catch((error) => {
console.error("Error setting local description:", error);
});

在上面的示例中,我们首先创建了一个 SDP offer,然后调用 setLocalDescription() 方法将其设置为本地描述。成功设置本地描述后,我们会在控制台输出成功的消息。如果设置本地描述失败,则会捕获到错误并输出错误消息。

  1. setRemoteDescription
    setRemoteDescription() 方法用于将远程对等连接的描述信息设置到本地对等连接中。这个描述信息通常是由远程对等方通过 SDP(Session Description Protocol)提供的,包含了远程对等方的媒体信息和网络连接信息。

在 WebRTC 中,当本地对等连接收到远程对等方发送的 SDP offer 或 answer 时,需要通过 setRemoteDescription() 方法将其设置到本地对等连接中,以便进行后续的连接协商和媒体交换。

以下是一个简单的示例,展示了如何使用 setRemoteDescription() 方法将远程对等连接的描述信息设置到本地对等连接中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 假设 remoteDescription 是远程对等连接的描述信息(SDP offer 或 answer)
const remoteDescription = {
type: 'offer', // 描述类型为 SDP offer
sdp: '...' // SDP offer 的内容
};

// 设置远程对等连接的描述信息到本地对等连接中
peerConnection.setRemoteDescription(remoteDescription)
.then(() => {
console.log('Remote description set successfully.');
// 在这里可以继续进行后续的连接协商或媒体交换操作
})
.catch((error) => {
console.error('Error setting remote description:', error);
});

在上面的示例中,我们假设 remoteDescription 是远程对等连接的描述信息,包含了描述的类型(这里假设为 SDP offer)和 SDP offer 的内容。然后,我们通过 setRemoteDescription() 方法将这个描述信息设置到本地对等连接中。如果设置成功,则会打印日志表示远程描述已成功设置,否则会捕获到错误并输出错误消息。

  1. addTransceiver

addTransceiver() 方法用于向对等连接中添加新的媒体传输通道(RTCRtpTransceiver)。它的参数包括:

  • trackOrKind:要添加到传输通道的媒体轨道(MediaStreamTrack)或媒体类型(string)。如果是 MediaStreamTrack 类型,则表示要添加的具体媒体轨道。如果是 string 类型,则表示要添加的媒体类型,如 "audio""video"
  • init(可选):一个可选的 RTCRtpTransceiverInit 对象,用于配置传输通道的初始化选项。该对象包括以下属性:
    • direction:传输通道的传输方向,可以是 "sendrecv""sendonly""recvonly""inactive"
    • streams:一个包含了要与传输通道关联的 MediaStream 对象的数组。
    • sendEncodings:一个包含了与传输通道相关联的编码设置的数组,用于配置传输的编码参数。

该方法返回一个 RTCRtpTransceiver 对象,表示新添加的传输通道。这个传输通道可以用于控制和管理新添加的媒体流的传输。通过使用 addTransceiver() 方法,您可以动态地向对等连接中添加新的媒体轨道,从而实现更灵活和动态的媒体流管理。

事件

  1. onnegotiationneeded

onnegotiationneeded 事件是 WebRTC 中的一个事件,它在需要重新协商(negotiation)时触发。当需要创建或重新创建对等连接时(例如添加或删除数据流或轨道,或者更改连接配置),就会触发此事件。

在此事件触发时,通常会调用 createOffer()createAnswer() 方法来生成一个新的 SDP(会话描述协议)以进行协商。然后,这个新的 SDP 将被传递给远程 RTCPeerConnection,以更新对等连接的配置。

以下是一个示例代码:

1
2
3
4
5
6
7
8
9
10
peerConnection.onnegotiationneeded = async () => {
try {
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
// 将 SDP 发送给远程 `RTCPeerConnection`
sendOfferToRemotePeer(offer);
} catch (error) {
console.error('Error creating offer:', error);
}
};

在实际应用中,您可以根据需要在 onnegotiationneeded 事件中执行其他操作,例如创建或更新对等连接的配置,以确保连接的稳定性和可靠性。

  1. onicecandidate
    onicecandidate 事件在 ICE(Interactive Connectivity Establishment)协商过程中生成 ICE 候选时触发。ICE 候选用于发现网络路径和连接两个 RTCPeerConnection 之间的传输地址。当本地 RTCPeerConnection 发现一个新的 ICE 候选时,会触发 onicecandidate 事件,并将候选信息传递给应用程序。

通常,在收到 ICE 候选后,应用程序会将候选信息发送给远程 RTCPeerConnection,以便远程 RTCPeerConnection 能够知道如何直接与本地 RTCPeerConnection 进行通信。

以下是一个简单的示例,展示了如何使用 onicecandidate 事件处理程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 创建 RTCPeerConnection
const peerConnection = new RTCPeerConnection();

// 设置 onicecandidate 事件处理程序
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
// 将 ICE 候选发送给远程 `RTCPeerConnection`
sendIceCandidateToRemotePeer(event.candidate);
} else {
// 所有 ICE 候选已发送
console.log("All ICE candidates have been sent.");
}
};

// 开始 ICE 协商过程
peerConnection.createOffer()
.then((offer) => {
// 设置本地描述
return peerConnection.setLocalDescription(offer);
})
.catch((error) => {
console.error("Error creating offer:", error);
});

在上面的示例中,onicecandidate 事件处理程序将在每次发现新的 ICE 候选时被触发。在处理程序中,我们检查 event.candidate 是否存在,如果存在则将候选信息发送给远程 RTCPeerConnection。当 event.candidatenull 时,表示所有的 ICE 候选已经发现并发送完毕。

RTCIceCandidate

RTCIceCandidateWebRTC 中的一个接口,表示 ICE (Interactive Connectivity Establishment) 候选者,用于描述网络中的地址信息,以帮助建立对等连接。以下是关于 RTCIceCandidate 接口的详细解释:

RTCIceCandidate 表示一个 ICE 候选者,包含了描述网络地址的相关信息。

RTCIceCandidateWebRTC 中用于建立对等连接的关键组成部分,用于确定连接双方的网络地址和传输协议。

参数

RTCIceCandidate 接口具体的参数如下所示:

  • candidate:描述 ICE 候选者的字符串,通常是以 SDP (Session Description Protocol) 格式表示的。该字符串包含了 ICE 候选者的网络地址、传输协议、优先级等信息。
  • sdpMid:候选者所对应的媒体流的标识符,用于区分不同的媒体流。
  • sdpMLineIndex:候选者所在的媒体流的索引,表示候选者属于媒体流描述中的哪一行。

案例

下面是一个简单的例子,展示了如何创建一个 RTCIceCandidate 对象,并将其添加到 RTCPeerConnection 中。ICE 候选者通常是通过信令服务器交换的,用于建立对等连接并进行网络地址交换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 从 Websocket 中收到的远端 RTCPeerConnection 的 ICE 候选者的的信息
const remoteIceCandidateInfo = {
candidate: 'candidate:1838085505 1 udp 2122260223 192.168.1.101 52041 typ host generation 0',
sdpMid: 'audio',
sdpMLineIndex: 0
}

// 创建一个新的 ICE 候选者对象
const iceCandidate = new RTCIceCandidate(remoteIceCandidateInfo);

// 将 ICE 候选者添加到本地的 PeerConnection
peerConnection.addIceCandidate(iceCandidate)
.then(() => {
console.log('ICE 候选者添加成功');
})
.catch(error => {
console.error('添加 ICE 候选者时出错:', error);
});

RTCSessionDescription

RTCSessionDescription 是 WebRTC API 中的一个重要接口,用于表示会话描述协议 (SDP) 中的一部分。SDP 是一种用于描述媒体会话的文本协议,包含了媒体流的相关信息,如编解码器、媒体类型、媒体格式、网络传输协议等。

在 WebRTC 中,RTCSessionDescription 主要用于描述通信会话中的两个关键部分:

  1. 描述远程媒体的信息:当一个端点(通常是浏览器)接收到来自远程端点(如另一个浏览器或媒体服务器)的 SDP 描述时,它会使用 RTCSessionDescription 对象来表示该描述。这个描述包含了远程端点发送的媒体流的相关信息,如编解码器、媒体类型、网络地址等。

  2. 描述本地媒体的信息:同样地,当一个端点想要向远程端点发送媒体流时,它会创建一个 RTCSessionDescription 对象,其中包含了本地媒体流的描述信息。这个描述会随后通过信令传输给远程端点,以便它能够了解到本地端点要发送的媒体流的相关信息。

属性

RTCSessionDescription 是 WebRTC 中用于描述会话信息的对象。它有两个主要属性:

  • type:描述会话的类型,通常是 "offer""answer""pranswer" 中的一个。

    • "offer":表示发起一个会话描述,通常由发送方生成并发送给接收方。
    • "answer":表示响应一个会话描述,接收方收到 "offer" 后生成的响应。
    • "pranswer":表示暂时的会话描述,用于表示正在协商会话的过程中,尚未达成最终协议的描述。
  • sdp:会话描述协议 (SDP) 的文本内容,包含了媒体流的相关信息,如编解码器、媒体类型、传输协议等。

案例

举个例子,一个典型的 RTCSessionDescription 对象可以看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 远端收到的Offer描述
const sessionDescription = {
type: 'offer',
sdp: 'v=0\no=- 123456789 0 IN IP4 127.0.0.1\ns=...\nt=0 0\n...'
};

// `setRemoteDescription()` 是 WebRTC 中 `RTCPeerConnection` 接口提供的方法之一,用于设置远端描述。它的作用是将远端的 Session Description(会话描述)应用到本地的 `RTCPeerConnection` 对象上,以建立或更新连接所需的各种参数和配置。
// 是一个异步操作,因为它可能需要与远端进行交互,解析和验证会话描述信息,并根据描述信息更新本地连接的状态。
peerConnection.setRemoteDescription(sessionDescription)
.then(() => {
// 处理成功设置远端描述后的逻辑
})
.catch(error => {
// 处理设置远端描述失败的逻辑
});

RTCCertificate

RTCCertificate 是用于配置和管理 WebRTC 连接所使用的安全证书的接口。它可以用来指定本地端点的安全凭证,以确保通信的安全性和可靠性。

RTCCertificate 接口本身并没有提供直接创建证书的方法。相反,WebRTC API 中的 RTCPeerConnection.generateCertificate() 方法用于生成新的证书。这个方法返回一个 Promise,在解析时会返回一个新的 RTCCertificate 实例,或者在生成证书时出现错误时返回一个错误对象。

属性和方法

RTCCertificate 接口没有提供构造函数来创建新的证书,而是通过 createOffer()createAnswer() 等方法间接使用。它的主要属性和方法包括:

  • expires:表示证书的到期时间,以 UNIX 时间戳表示。
  • getFingerprints():返回一个包含证书指纹信息的数组。

案例

以下是一个使用 RTCPeerConnection.generateCertificate() 方法生成证书的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// RSASSA-PKCS1-v1_5 算法
let stdRSACertificate = {
// name: 必需,指定生成证书所使用的算法名称。目前支持的值有 "RSASSA-PKCS1-v1_5"、"RSA-PSS" 和 "ECDSA"。
name: "RSASSA-PKCS1-v1_5",
// modulusLength: 对于 RSA 算法,指定生成的 RSA 密钥的长度(以位为单位)。可选值为 1024、2048、4096 等。默认为 2048。
modulusLength: 2048,
// publicExponent: 对于 RSA 算法,指定生成的 RSA 密钥的公共指数。可选值为 3、65537 等。默认为 65537。
publicExponent: new Uint8Array([1, 0, 1]),
// hash: 对于 RSA 算法,指定用于签名的散列算法。可选值为 "SHA-1"、"SHA-256"、"SHA-384" 等。默认为 "SHA-256"。
hash: "SHA-256",
};

// RSA-PSS 算法
let stdRSAPSSCertificate = {
// name: 必需,指定生成证书所使用的算法名称。目前支持的值有 "RSASSA-PKCS1-v1_5"、"RSA-PSS" 和 "ECDSA"。
name: 'RSA-PSS',
// modulusLength: RSA 公钥的模数长度,即 N 的位数。一般来说,这个值越大,密钥的安全性就越高。典型的值为 2048 或 4096。
modulusLength: 2048,
// publicExponent: RSA 公钥的指数部分,通常选择一个较小的质数,如 65537 (0x10001)。
publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 对应于十进制的65537
// hash: 用于生成签名的哈希算法,如 SHA-256、SHA-384、SHA-512 等。
hash: 'SHA-256',
// saltLength: 签名中所使用的盐的长度,通常设置为哈希算法的输出长度。
saltLength: 32 // 假设使用32字节的盐
};

// ECDSA 算法
let stdECDSACertificate = {
// name: 必需,指定生成证书所使用的算法名称。目前支持的值有 "RSASSA-PKCS1-v1_5"、"RSA-PSS" 和 "ECDSA"。
name: "ECDSA",
// namedCurve 是用于指定生成椭圆曲线数字签名算法 (ECDSA) 证书时所使用的椭圆曲线的参数。目前支持的值有 "P-256"、"P-384" 和 "P-521"。
namedCurve: "P-256",
};

// 使用 RTCPeerConnection.generateCertificate() 方法生成新的证书
const certificatePromise = RTCPeerConnection.generateCertificate(stdECDSACertificate);

// 处理生成证书的结果
certificatePromise.then((certificate) => {
// 生成证书成功,可以使用 certificate 对象
console.log('New certificate generated:', certificate);

// 在这里可以使用生成的证书进行后续操作,例如创建 RTCPeerConnection 实例
}).catch((error) => {
// 生成证书失败,输出错误信息
console.error('Error generating certificate:', error);
});

RTCRtpTransceiver

RTCRtpTransceiver 接口表示一个媒体传输通道,用于在 WebRTC 对等连接(RTCPeerConnection)中传输媒体流。每个传输通道可以处理一个或多个媒体流的发送和接收。

RTCRtpTransceiver 接口通常由 RTCPeerConnection 对象的 addTransceiver() 方法创建,并在对等连接的创建和管理过程中使用。它提供了灵活的控制选项,使您能够更好地管理对等连接中的媒体流传输。

属性

RTCRtpTransceiver 表示 WebRTC 对等连接(RTCPeerConnection)中的一个媒体传输通道,该对象包含的属性和方法如下所示:

  • mid:传输通道的标识符(MediaStream ID),在 SDP 中用于唯一标识传输通道。
  • receiver:与传输通道相关联的接收器对象(RTCReceiver),用于接收远程媒体流。
  • sender:与传输通道相关联的发送器对象(RTCSender),用于发送本地媒体流。
  • direction:传输通道的传输方向,可以是 "sendrecv""sendonly""recvonly""inactive"
  • currentDirection:当前传输通道的传输方向。
  • stopped:指示传输通道是否已停止的布尔值。

方法

  • setCodecPreferences(codecs):设置传输通道的编解码器偏好,codecs 是通过 RTCRtpSender.getCapabilitiesRTCRtpReceiver.getCapabilities 静态方法获得的编解码器。
  • stop():停止传输通道,不再发送或接收媒体流。

RTCRtpSenderRTCRtpReceiver 是 WebRTC API 中用于发送和接收媒体流的对象。

RTCRtpSender:

RTCRtpSender 接口表示 RTCPeerConnection 中的媒体发送器。它负责发送媒体流到远程对等方。

属性

  • track:成员属性,代表当前与 RTCRtpSender 相关联的 MediaStreamTrack 对象。可以是音频轨道或视频轨道。

方法

  • getParameters():成员方法,返回一个 RTCRtpSendParameters 对象,该对象包含了当前发送器的参数信息,如编码器设置等。

  • setParameters(parameters):成员方法,用于设置发送器的参数,例如修改编码器设置等。参数 parameters 是一个包含新参数的 RTC RtpSendParameters 对象。

  • replaceTrack(newTrack):成员方法,用于替换当前发送器的媒体轨道。可以用于在运行时更改发送的媒体类型(例如切换摄像头)。

  • getCapabilities(kind):静态方法,用于获取特定类型(音频或视频)的编解码器能力信息。参数 kind 可以是 “audio” 或 “video”。

示例

以下是关于如何使用 getCapabilities 方法来获取编解码器能力信息的示例代码:

1
2
3
4
5
6
7
// 获取视频编解码器能力信息
const videoCapabilities = RTCRtpSender.getCapabilities('video');
console.log("Video Codecs:", videoCapabilities.codecs);

// 获取音频编解码器能力信息
const audioCapabilities = RTCRtpSender.getCapabilities('audio');
console.log("Audio Codecs:", audioCapabilities.codecs);

这段代码中,我们首先使用 RTCRtpSender.getCapabilities 方法来获取视频和音频编解码器的能力信息。然后,我们打印出返回的 codecs 属性,其中包含了关于支持的编解码器的详细信息,例如编解码器的类型、编码器和解码器参数等。

RTCRtpReceiver:

RTCRtpReceiver 接口表示 RTCPeerConnection 中的媒体接收器。它负责接收远程对等方发送的媒体流。

属性

  • track:属性,代表当前与 RTCRtpReceiver 相关联的 MediaStreamTrack 对象。可以是音频轨道或视频轨道。

方法

  • getParameters():成员方法,返回一个 RTC RtpReceiveParameters 对象,该对象包含了当前接收器的参数信息,如解码器设置等。

  • getContributingSources():成员方法,返回一个包含贡献源信息的数组,表示当前接收器接收到的媒体流的贡献者。

  • getStats():成员方法,返回一个 Promise,该 Promise 在解析后会提供有关接收器的统计信息。

  • getCapabilities(kind):静态方法,用于获取特定类型(音频或视频)的编解码器能力信息。参数 kind 可以是 “audio” 或 “video”。

示例

以下是一个使用 RTCRtpReceiver.getCapabilities 方法来获取音频和视频编解码器能力信息的示例代码:

1
2
3
4
5
6
7
// 获取视频编解码器能力信息
const videoCapabilities = RTCRtpReceiver.getCapabilities('video');
console.log("Video Codecs:", videoCapabilities.codecs);

// 获取音频编解码器能力信息
const audioCapabilities = RTCRtpReceiver.getCapabilities('audio');
console.log("Audio Codecs:", audioCapabilities.codecs);

这段代码中,我们使用 RTCRtpReceiver.getCapabilities 方法来获取音频和视频编解码器的能力信息。在真实使用当中,我们可以设置传输的音视频的编解码器
然后,我们打印出返回的 codecs 属性,其中包含了关于支持的编解码器的详细信息,例如编解码器的类型、编码器和解码器参数等。

其他名词术语解释

当涉及到 WebRTC 和实时通信时,以下术语是很常见的:

  • SDP(Session Description Protocol):会话描述协议,用于描述多媒体会话的参数,例如编解码器信息、媒体流传输方式等。在 WebRTC 中,SDP 用于建立和管理对等连接。

  • RTCP(Real-Time Control Protocol):实时控制协议,是用于在 RTP(实时传输协议)会话中提供控制和监控功能的协议。RTCP 主要用于传输统计信息和控制信息。

  • RTP(Real-Time Transport Protocol):实时传输协议,是一种用于在 IP 网络上传输多媒体数据的协议。RTP 通常与 RTCP 一起使用,用于在网络上传输音频和视频流。

  • ICE(Interactive Connectivity Establishment):交互式连接建立,是一种用于在两个设备之间建立网络连接的技术。ICE 使用一系列技术(包括 STUN、TURN 和 ICE)来克服网络地址转换(NAT)和防火墙等网络障碍。

这些术语在 WebRTC 中起着至关重要的作用,它们共同构建了实时通信系统的基础。

WebRTC的工作原理

WebRTC(Web Real-Time Communication) 是一项用于实现浏览器之间实时通信的开放标准技术。它允许在不需要额外插件的情况下,通过浏览器直接进行音频、视频和数据传输。WebRTC 技术的出现,极大地促进了在线会议、远程教育、视频直播等领域的发展。下面将深入探讨 WebRTC 的核心技术,包括媒体获取媒体传输信令交换以及NAT穿越等方面,并结合详细的例子进行说明。

摄像头媒体获取(Media Acquisition)

navigator.mediaDevices.getUserMedia() 是 WebRTC 提供的方法之一,用于从用户的摄像头和麦克风中获取媒体流。它允许 Web 应用程序访问用户的音视频设备,并将其转换为可用于实时通信、音视频录制等目的的媒体流。

示例代码

以下是关于 navigator.mediaDevices.getUserMedia() 方法的例子:

1
2
3
4
5
6
7
8
9
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then((stream) => {
// 获取到用户的音视频流
const videoElement = document.getElementById('localVideo');
videoElement.srcObject = stream;
})
.catch((error) => {
console.error('获取用户媒体设备失败:', error);
});

参数说明

  • constraints 是一个可选参数,用于指定获取媒体流的约束条件。
  • 可以包含两个属性:
    • video:指定是否捕获视频。可以是布尔值或包含视频约束的对象。
    • audio:指定是否捕获音频。可以是布尔值或包含音频约束的对象。

返回值

  • getUserMedia() 方法返回一个 Promise 对象。
  • 如果用户授权并成功获取媒体流,则 Promise 对象的 then 回调函数会被调用,并传递一个 MediaStream 对象作为参数,其中包含捕获的视频和/或音频轨道。
  • 如果用户拒绝权限或获取媒体流失败,则 Promise 对象的 catch 回调函数会被调用,并传递一个 Error 对象作为参数,其中包含错误的详细信息。

用途

获取到媒体流后,您可以将其用于各种用途,例如:

  • <video> 元素中播放捕获的视频流。
  • 将视频流传输到远程对等端,实现实时视频通话或视频会议。
  • 对音频流进行处理、录制或转发等操作。

权限和安全性

  • 浏览器通常会在第一次调用 getUserMedia() 方法时向用户请求音视频设备的权限。
  • 用户需要允许或拒绝权限请求,以控制是否允许网站访问其摄像头和麦克风。
  • 为了保护用户隐私和安全,浏览器会限制某些情况下的媒体设备访问,例如当网页位于不安全的上下文中时。

总的来说,navigator.mediaDevices.getUserMedia() 方法为开发者提供了一种方便的方式来获取用户的音视频流,从而实现各种实时音视频通信、录制和处理等功能。

说明

上面的代码演示了如何使用 getUserMedia API 获取用户的音视频流,并将其绑定到一个 <video> 元素上进行实时预览。通过这种方式,我们可以轻松地在浏览器中获取用户的音视频数据,为后续的通信做准备。

桌面媒体获取(Media Acquisition)

navigator.mediaDevices.getDisplayMedia() 是 WebRTC 提供的方法之一,用于获取屏幕共享流。它允许用户捕获屏幕上的内容并将其转换为媒体流,以便在 Web 应用程序中进行处理和展示。

示例代码

让我们详细解释一下这个方法的用法和相关概念:

1
2
3
4
5
6
7
navigator.mediaDevices.getDisplayMedia(constraints)
.then(stream => {
// 处理媒体流
})
.catch(error => {
// 处理错误
});

参数说明

  • constraints 是一个可选参数,用于指定获取屏幕共享流的约束条件。
  • 可以包含两个属性:
    • video:指定是否捕获视频。默认值为 true
    • audio:指定是否捕获音频。默认值为 false

返回值

  • getDisplayMedia() 方法返回一个 Promise 对象。
  • 如果用户授权并成功获取屏幕共享流,则 Promise 对象的 then 回调函数会被调用,并传递一个 MediaStream 对象作为参数,其中包含屏幕共享的视频和/或音频轨道。
  • 如果用户拒绝权限或获取屏幕共享流失败,则 Promise 对象的 catch 回调函数会被调用,并传递一个 Error 对象作为参数,其中包含错误的详细信息。

用途

获取到媒体流后,您可以将其用于各种用途,例如:

  • <video> 元素中播放屏幕共享内容。
  • 将屏幕共享流传输到远程对等端,实现屏幕共享功能。
  • 对媒体流进行处理、录制或转发等操作。

权限和安全性

  • 浏览器通常会在第一次调用 getDisplayMedia() 方法时向用户请求屏幕共享权限。
  • 用户需要允许或拒绝权限请求,以控制是否允许网站捕获屏幕内容。
  • 为了保护用户隐私和安全,浏览器会限制某些情况下的屏幕共享,例如当网页位于不安全的上下文中时。

说明

navigator.mediaDevices.getDisplayMedia() 方法为开发者提供了一种方便的方式来获取屏幕共享流,从而实现屏幕录制、远程教学、视频会议等各种实时协作和分享功能。

媒体传输(Media Transmission)

WebRTC 的媒体传输主要涉及使用 RTCPeerConnection 建立点对点的连接,并将音视频数据传输给其他 RTCPeerConnection。这个过程涉及到 ICE 候选、会话描述等技术,以确保数据能够安全、高效地传输。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 创建一个RTCPeerConnection对象
const peerConnection = new RTCPeerConnection();

// 添加ICE候选
peerConnection.onicecandidate = function(event) {
let candidateObject = {};
const candidate = event.candidate;
if (!candidate) {
log.debug('message: Gathered all candidates and sending END candidate');
candidateObject = {
sdpMLineIndex: -1,
sdpMid: 'end',
candidate: 'end',
};
} else {
candidateObject = {
sdpMLineIndex: candidate.sdpMLineIndex,
sdpMid: candidate.sdpMid,
candidate: candidate.candidate,
};
if (!candidateObject.candidate.match(/a=/)) {
candidateObject.candidate = `a=${candidateObject.candidate}`;
}
}
// 发送ICE候选给对方
socket.sendSDP(
'connectionMessage',
{
roomId: 'xxx',
msg: {
type: 'candidate',
candidate: candidateObject
},
browser: 'xxx'
}
);
};

// 添加本地流
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then((stream) => {
stream.getTracks().forEach((track) => {
peerConnection.addTrack(track, stream);
});
});

// 创建Offer
peerConnection.createOffer()
.then((offer) => {
return peerConnection.setLocalDescription(offer);
})
.then(() => {
// 将Offer发送给对方
socket.sendSDP(
'connectionMessage',
{
roomId: 'xxx',
msg: {
type: peerConnection.localDescription.type,
sdp: peerConnection.localDescription.sdp,
},
browser: 'xxx'
}
);
})
.catch((error) => {
console.error('创建Offer失败:', error);
});

说明

上面的代码演示了如何使用 RTCPeerConnection 建立点对点连接,并将本地的音视频流添加到连接中。然后,通过创建一个Offer,并将其发送给对方,以发起一个通信会话。在这个过程中,我们还需要处理 ICE 候选,以便在连接过程中进行 NAT 穿越。

信令交换(Signaling)

WebRTC 的信令交换主要涉及将会话描述、ICE 候选等元数据信息交换给其他 RTCPeerConnection,以建立和管理通信会话。这个过程通常需要借助于信令服务器来协调客户端之间的消息传递。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 建立WebSocket连接
const ws = new WebSocket('ws://localhost:8080');

// 监听WebSocket消息
ws.onmessage = function(event) {
const message = JSON.parse(event.data).msg;
// 当收到远端的Offer时,我们需要设置远端描述,并生成一个Answer,添加到本地描述里面,然后在通过WebSocket发生给远端,让对方设置的远端描述里面。
if (message.type === 'offer') {
// 收到对方的Offer
peerConnection.setRemoteDescription(new RTCSessionDescription(message))
.then(() => {
// 创建并发送Answer
return peerConnection.createAnswer();
})
.then((answer) => {
peerConnection.setLocalDescription(answer);
return answer
})
.then(() => {
// 将Answer发送给对方
socket.sendSDP(
'connectionMessage',
{ roomId: 'xxx', msg: answer, browser: 'xxx' }
);
})
.catch((error) => {
console.error('处理Offer失败:', error);
});
} else if (message.type === 'answer') {
// 当收到远端的Answer时,设置到远端描述,建立通信连接成功!
// 收到对方的Answer
peerConnection.setRemoteDescription(new RTCSessionDescription(message))
.then(() => {
console.log('成功建立通信连接!');
})
.catch((error) => {
console.error('处理Answer失败:', error);
});
} else if (message.type === 'candidate') {
// 收到远端的ICE候选描述信息时,需要创建RTCIceCandidate对象,然后调用addIceCandidate方法添加ICE候选
const candidateParams = typeof (message.candidate) === 'object' ? message.candidate : JSON.parse(message.candidate);

candidateParams.candidate = candidateParams.candidate.replace(/a=/g, '');
candidateParams.sdpMLineIndex = parseInt(candidateParams.sdpMLineIndex, 10);
const candidate = new RTCIceCandidate(candidateParams);
// 收到ICE候选
peerConnection.addIceCandidate(candidate)
.then(() => {
console.log('成功添加ICE候选!');
})
.catch((error) => {
console.error('添加ICE候选失败:', error);
});
}
};

说明

上面的代码演示了如何使用 WebSocket 进行信令交换。客户端通过 WebSocket 连接到信令服务器,并监听来自服务器的消息。当收到对方发送的 OfferAnswerICE 候选时,客户端需要相应地处理并执行相应的操作,以建立和管理通信会话。

NAT穿越(NAT Traversal)

NAT(Network Address Translation) 穿越是指在 NAT 环境下,通过一系列的技术手段实现两个位于私有网络中的计算机或设备之间的直接通信。WebRTC 通过使用 STUNTURN 服务器来实现 NAT 穿越,以确保在各种网络环境下都能够正常进行实时通信。

示例代码

1
2
3
4
5
6
7
8
9
10
// 配置ICE服务器
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'turn:your-turn-server.com', username: 'username', credential: 'password' }
]
};

// 创建RTCPeerConnection对象
const peerConnection = new RTCPeerConnection(configuration);

添加传输通道

当通道和管理通信会话建立成功后,我们需要将我们本地的音视频流传输到远端的 RTCPeerConnection 中,所以我们需要调用 RTCPeerConnection.addTransceiver 方法。

addTransceiver() 方法用于向对等连接(RTCPeerConnection 对象)添加传输通道(transceiver),以便在单个轨道上发送和接收媒体流。通过这种方式,您可以更灵活地控制对等连接的行为,并更精细地管理媒体流的传输。

该方法接受一个或多个参数,具体取决于您要添加的传输通道的类型和配置。通常情况下,addTransceiver() 方法用于以下两种情况:

  1. 添加本地传输通道(用于发送):您可以通过将本地媒体轨道(例如音频或视频轨道)添加到对等连接中来创建一个新的传输通道,以便将该媒体流发送给远端。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const transceivers = [];
localStream.getTracks().forEach(async (track) => {
const options = {
streams: [localStream],
};
if (track.kind === 'video') {
options = {
// 生成视频流编码的参数
sendEncodings: generateEncoderParameters(),
};
}
// 需要保存传输通道,在不需要传输的时候,方便停止传输
const transceiver = peerConnection.addTransceiver(track, options);
transceivers.push(transceiver);
});

当需要移除的时候

1
2
3
4
5
6
7
8
9
transceivers.forEach((transceiver) => {
console.debug('Stopping transceiver', transceiver);
// Don't remove the tagged m section, which is the first one (mid=0).
if (transceiver.mid === '0') {
peerConnection.removeTrack(transceiver.sender);
} else {
transceiver.stop();
}
});
  1. 添加远程传输通道(用于接收):当您接收到远端对等连接发送的媒体流时,WebRTC 会自动为每个远程媒体轨道创建传输通道,并将其添加到对等连接中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
peerConnection.ontrack = (event) => {
const remoteTrack = event.track;
if (trackEvt.streams.length !== 1) {
console.warning('More that one mediaStream associated with track!');
}

// 远端的流
const stream = trackEvt.streams[0];
// 监听流移除轨道事件
stream.onremovetrack = () => {
// 不存在音频和视频
if (stream.getTracks().length === 0) {
this.emit(ConnectionEvent({ type: 'remove-stream', stream }));
}
};
this.emit(ConnectionEvent({ type: 'add-stream', stream }));
// 处理远程视频轨道
};

说明

上面的代码示例中,我们通过配置 ICE 服务器来实现 NAT 穿越。其中,STUN 服务器用于获取公网 IP 地址和端口,而 TURN 服务器则用于在无法直接通信的情况下进行中转。通过合理配置 ICE 服务器,我们可以在不同的网络环境中都能够顺利地进行实时通信。

设置 Codec 编解码器

获取可用 Codec

1
const supportsSetCodecPreferences = window.RTCRtpTransceiver && 'setCodecPreferences' in window.RTCRtpTransceiver.prototype;

获取 Codec

通过 RTCRtpSender.getCapabilities('video') 获取可支持的 codec。然后把它们放进列表 codecPreferences 里,通过 Select 元素展示,让用户选择想使用的 codec

1
2
3
4
5
6
7
8
9
10
if (supportsSetCodecPreferences) {
const { codecs } = RTCRtpSender.getCapabilities('video');
const codecPreferences = [];
codecs.forEach(codec => {
if (['video/red', 'video/ulpfec', 'video/rtx'].includes(codec.mimeType)) {
return;
}
codecPreferences.push({ ...codec, value: (codec.mimeType + ' ' + (codec.sdpFmtpLine || '')).trim() });
});
}

配置 Codec

找到用户选择的 Codec。调用 transceiver.setCodecPreferences(codecs) ,把选中的 Codec 交给 transceiver 最上面,优先使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (supportsSetCodecPreferences) {
// 获取选择的codec
let selectedIndex = 1
const preferredCodec = codecPreferences[selectedIndex];
if (preferredCodec.value !== '') {
const [mimeType, sdpFmtpLine] = preferredCodec.value.split(' ');
const { codecs } = RTCRtpSender.getCapabilities('video');
const selectedCodecIndex = codecs.findIndex(c => c.mimeType === mimeType && c.sdpFmtpLine === sdpFmtpLine);
const selectedCodec = codecs[selectedCodecIndex];
// 先移除
codecs.splice(selectedCodecIndex, 1);
// 插入第一个
codecs.unshift(selectedCodec);
const transceiver = pc1.getTransceivers().find(t => t.sender && t.sender.track === localStream.getVideoTracks()[0]);
transceiver.setCodecPreferences(codecs);
}
}

WebRTC的应用场景

WebRTC 技术已经被广泛应用于多个领域,包括:

  1. 视频会议
    WebRTC 使得在网页浏览器中进行高清视频会议成为可能,用户可以通过浏览器直接加入会议室,与其他参与者进行实时视频通话。

  2. 在线教育
    教育机构可以利用 WebRTC 技术搭建在线教育平台,让老师和学生之间进行实时的远程教学,实现互动和教学资源共享。

  3. 社交应用
    社交应用程序可以利用 WebRTC 技术实现实时语音和视频通话功能,让用户之间进行更加直观和自然的交流。

  4. 远程医疗
    医疗机构可以利用 WebRTC 技术搭建远程医疗平台,实现医生和患者之间的远程会诊和医疗服务,提高医疗资源的利用效率。

WebRTC的未来发展趋势

随着实时通信需求的不断增加,WebRTC 技术也在不断发展和完善。未来,我们可以期待以下几个方面的发展趋势:

  1. 网络性能的提升
    随着网络基础设施的不断改善和网络带宽的增加,WebRTC 技术将能够实现更高质量、更稳定的实时通信体验。

  2. 新的应用场景
    随着 WebRTC 技术的成熟和普及,我们可以预见到它将被应用于更多领域,如工业控制、智能家居和虚拟现实等。

  3. 标准化和开放性
    WebRTC 作为一个开放标准的实时通信技术,将继续推动标准化工作的进行,以确保不同厂商和平台之间的互操作性和兼容性。

  4. 安全性和隐私保护
    随着个人隐私和数据安全意识的提高,WebRTC 技术将会加强对通信内容的加密和隐私保护,以确保用户数据的安全性。

总结

WebRTC 技术的出现为实时通信应用提供了强大的支持,使得开发者可以轻松构建具有高质量和稳定性的实时通信应用。随着技术的不断发展和完善,我们有理由相信 WebRTC 将会在未来的数字化世界中扮演越来越重要的角色,为用户带来更丰富、更便捷的通信体验。

Web前端最新优化指标:从FP到FPS的全面解析

Web前端最新优化指标:从FP到FPS的全面解析

摘要

在当今互联网时代,Web前端性能优化是网站开发中至关重要的一环。随着技术的不断发展,出现了一系列新的性能指标,如FPFCPFMPLCPTTICLSFIDFPS等。本文将深入探讨这些最新的Web前端优化指标,详细介绍获取和优化的方法,并提供丰富的实例和技巧,帮助开发者全面了解和应用于实践中。

引言

随着Web技术的不断发展,用户对网页加载速度和性能的要求越来越高。为了提供更好的用户体验,出现了一系列新的Web前端优化指标,如FPFCPFMPLCPTTICLSFIDFPS等。本文将深入探讨这些指标的含义、获取方法以及优化技巧,帮助开发者更好地理解和应用于实践中。

DOMContentLoaded 事件

DOMContentLoaded 事件,当 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式表、图像和子框架的完成加载。

获取 DOMContentLoaded 事件的方法:

通过监听 document 对象上的 DOMContentLoaded 事件获得:

1
document.addEventListener('DOMContentLoaded', function() {}, false)

DOMContentLoaded 事件持续时间

可以通过 Performance API 中的相关接口来获取 DOMContentLoaded 事件的开始和结束时间,如performance.timing.domContentLoadedEventEndperformance.timing.domContentLoadedEventStart,两者相差就为持续时间。

1
2
// 计算规则
const dclTime = performance.timing.domContentLoadedEventEnd - performance.timing.domContentLoadedEventStart

load 事件

load 事件,当页面中依赖的所有资源:DOM、图片、CSS、Flash、javascript 等都加载完后,执行完后会在 window 对象上触发对应的事件,

window.onload 注册的回调就会在 load 事件触发时候被调用,或者通过 window.addEventListener 来进行监听。

1
2
3
4
5
window.onload = function() {}

// or

window.addEventListener('load', function() {}, false)

load 事件持续时间

可以通过 Performance API 中的相关接口来获取 load 事件的开始和结束时间,如performance.timing.loadEventEndperformance.timing.loadEventStart,两者相差就为持续时间。

1
const loadTime = performance.timing.loadEventEnd - performance.timing.loadEventStart;

FP(First Paint)

FP是指浏览器首次将像素呈现到屏幕上的时间点,即首次绘制。它标志着页面开始加载的时间,但并不表示页面内容已经完全可见。下面是获取和优化FP的方法:

获取FP的方法:

可以通过 Performance API 中的相关接口来获取FP时间,如performance.timing.navigationStartperformance.getEntriesByType('paint')等。

1
2
3
4
5
6
7
8
9
10
// 获取FP时间
const entries = performance.getEntriesByType('paint');
for (const entry of entries) {
// 首次渲染
if (entry.name === 'first-paint') {
// FP开始时间
const fpTime = entry.startTime;
console.log("FP时间:", fpTime);
}
}

FP(First Paint)持续时间

FP持续时间是指从页面开始加载到首次绘制内容到屏幕上的时间间隔。可以通过 PerformanceObserver 对象,通过监听 paint 类型来获取,还能获得 FP(First Paint) 所花费的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
for (const entry of entries) {
// 首次渲染
if (entry.name === 'first-paint') {
// FP开始时间
const fpTime = entry.startTime;
// 持续时间
const duration = entry.duration;
// FP结束时间
const fpDurationTime = fpTime + duration;
console.log("FP持续时间:", fpDurationTime);
}
}
});

observer.observe({ type: "paint", buffered: true });

优化FP的方法:

优化FP可以通过减少页面加载时间和优化渲染流程来实现。例如,通过合并和压缩CSS、JavaScript文件,减少网络请求次数和文件大小,以加快页面加载速度。

1
2
3
4
5
<!-- 合并和压缩CSS文件 -->
<link rel="stylesheet" href="styles.css">

<!-- 合并和压缩JavaScript文件 -->
<script src="scripts.js"></script>

FCP(First Contentful Paint)

FCP是指浏览器首次绘制来自DOM的内容的时间点,即首次内容绘制。它表示页面开始显示内容的时间,但并不表示所有内容都已加载完毕。下面是获取和优化FCP的方法:

获取FCP的方法:

可以通过 Performance API 中的相关接口来获取FCP时间,如 performance.timing.navigationStartperformance.timing.getEntriesByType('paint') 等。

1
2
3
4
5
6
7
8
9
10
// 获取FP时间
const entries = performance.getEntriesByType('paint');
for (const entry of entries) {
// 首次渲染
if (entry.name === 'first-contentful-paint') {
// FCP开始时间
const fcpTime = entry.startTime;
console.log("FCP时间:", fcpTime);
}
}

FCP(First Contentful Paint)持续时间

FCP持续时间是指从页面开始加载到首次绘制来自DOM的内容的时间间隔。可以通过 PerformanceObserver 对象,通过监听 paint 类型来获取,还能获得 FCP(First Contentful Paint) 所花费的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 获取FCP时间
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
for (const entry of entries) {
// 首次渲染
if (entry.name === 'first-contentful-paint') {
// FCP开始时间
const fcpTime = entry.startTime;
// 持续时间
const duration = entry.duration;
// FCP持续时间
const fcpDurationTime = fcpTime + duration;
console.log("FCP持续时间:", fcpDurationTime);
}
}
});

observer.observe({ type: "paint", buffered: true });

优化FCP的方法:

优化FCP可以通过减少关键资源的加载时间和优化关键路径资源来实现。例如,通过预加载关键资源、懒加载技术和延迟加载非关键资源等。

1
2
3
4
5
<!-- 预加载关键资源 -->
<link rel="preload" href="critical.css" as="style">

<!-- 懒加载非关键资源 -->
<img src="placeholder.jpg" data-src="image.jpg" loading="lazy">

FMP(First Meaningful Paint)

FMP是指浏览器首次绘制页面主要内容的时间点,即首次有意义的绘制。它表示用户认为页面已经有用的时间点。下面是获取和优化FMP的方法:

获取FMP的方法:

可以通过 Performance API 中的相关接口来获取FMP时间,如 PerformanceObserver 接口监听 paint 事件,判断首次有意义的绘制。

1
2
3
// FMP计算比较复杂,lighthouse的计算的大体思路是,将页面中最大布局变化后的第一次渲染事件作为FMP事件,并且计算中考虑到了可视区的因素。

// FMP计算过于复杂,没有现成的performance API,如果希望在监控中上报这个指标,可以自己使用MutationObserver计算。

FMP(First Meaningful Paint)持续时间

FMP持续时间是指从页面开始加载到首次绘制页面主要内容的时间间隔。可以通过监测FMP事件和页面开始加载之间的时间差来计算。

1
// FMP计算过于复杂,没有现成的performance API,如果希望在监控中上报这个指标,可以自己使用MutationObserver计算。

优化FMP的方法:

优化FMP可以通过减少关键资源的加载时间和提高关键路径资源加载速度来实现。例如,使用HTTP/2多路复用和服务器推送技术,以及使用CDN加速关键资源加载。

1
2
<!-- 使用CDN加速关键资源 -->
<script src="https://cdn.example.com/scripts.js"></script>

LCP(Largest Contentful Paint)

LCP是指浏览器在视觉上渲染的最大内容元素的时间点,即最大内容渲染时间点。它衡量的是页面主要内容加载完成的时间点。下面是获取和优化LCP的方法:

获取LCP的方法:

可以通过 Performance API 中的相关接口来获取 LCP 时间,如 PerformanceObserver 接口监听 largest-contentful-paint 事件。

1
2
3
4
5
6
7
// 监听LCP事件
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lcpTime = entries[0].startTime;
console.log("LCP时间:", lcpTime);
});
observer.observe({ type: "largest-contentful-paint", buffered: true });

LCP(Largest Contentful Paint)持续时间

LCP持续时间是指从页面开始加载到最大内容元素被渲染完成的时间间隔。可以通过监测LCP事件和页面开始加载之间的时间差来计算。

1
2
3
4
5
6
7
8
9
10
11
12
const observer = new PerformanceObserver((list) => {
// 计算最大的内容
const entries = list.getEntries().sort((pre, next) => next.size - pre.size);
// LCP开始事件
const lcpTime = entries[0].startTime;
// 持续时间
const duration = entries[0].duration;
// FCP持续时间
const lcpDuration = lcpTime + duration;
console.log('LCP持续时间:', lcpDuration);
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });

优化LCP的方法:

优化LCP可以通过优化关键路径资源的加载顺序和减少页面主要内容的渲染时间来实现。例如,使用懒加载技术延迟加载非关键内容,以及减少渲染阻塞资源的加载。

1
2
<!-- 使用懒加载延迟加载非关键内容 -->
<img src="placeholder.jpg" data-src="image.jpg" loading="lazy">

TTI(Time to Interactive)

TTI是指页面变得可交互的时间点,即用户可以与页面进行交互的时间点。它是衡量页面可用性的重要指标。下面是获取和优化TTI的方法:

获取TTI的方法:

可以通过 Performance API 中的相关接口来获取TTI时间,通过 performance.timing.domInteractiveperformance.timing.fetchStart 的时间差来获得。

1
2
// 监听TTI事件
const timeToInteractive = performance.timing.domInteractive - performance.timing.fetchStart;

优化TTI的方法:

优化TTI可以通过减少主线程阻塞时间和延迟加载非关键资源来实现。例如,通过减少JavaScript执行时间、使用服务端渲染技术和懒加载技术等。

1
2
3
4
5
// 使用懒加载延迟加载非关键资源
const image = document.createElement("img");
image.src = "image.jpg";
image.loading = "lazy";
document.body.appendChild(image);

CLS(Cumulative Layout Shift)

CLS是指页面在加载过程中发生的所有不良布局变化的总和,即累积布局偏移。它衡量的是页面的视觉稳定性。发生的每次布局变化中的最大幅度的布局变化得分的指标。为了提供良好的用户体验,站点应该努力使 CLS 分数达到 0.1 或更低。下面是获取和优化CLS的方法:

获取CLS的方法:

可以通过 Performance API 中的相关接口来获取CLS值,如 PerformanceObserver 接口监听 layout-shift 事件。

1
2
3
4
5
6
7
// 监听CLS事件
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const clsValue = entries.reduce((acc, entry) => acc + entry.value, 0);
console.log("CLS值:", clsValue);
});
observer.observe({ type: "layout-shift", buffered: true }); // 必须加上 buffered: true

优化CLS的方法:

优化CLS可以通过避免页面元素的不稳定布局和动态元素的尺寸变化来实现。例如,指定图片和媒体元素的尺寸、避免动态插入内容导致页面布局变化等。

1
2
3
4
5
/* 指定图片和媒体元素的尺寸 */
img, video {
width: 100%;
height: auto;
}

FID(First Input Delay)

FID是指用户首次与页面交互到浏览器响应交互的时间间隔,即首次输入延迟。它衡量的是页面的交互性能。

第一次输入延迟,用于测量可交互性。FID 衡量的是从用户第一次与页面交互(例如,当他们点击链接,点击按钮,或使用自定义的 JavaScript 驱动的控件)到浏览器实际能够开始响应该交互的时间,为了提供良好的用户体验,站点应该努力使 FID 保持在 100 毫秒以内。

下面是获取和优化FID的方法:

获取FID的方法:

可以通过 Performance API 中的相关接口来获取FID值,如 PerformanceObserver 接口监听 first-input 事件。

1
2
3
4
5
6
7
// 监听FID事件
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const fidTime = entries[0].startTime;
console.log("FID值:", fidTime);
});
observer.observe({ type: "first-input", buffered: true });

优化FID的方法:

优化FID可以通过减少主线程阻塞时间和优化JavaScript执行时间来实现。例如,减少长任务的执行时间、优化事件处理程序的性能等。

1
2
3
4
// 优化事件处理程序的性能
document.getElementById("button").addEventListener("click", () => {
// 执行优化后的代码
}, { passive: true });

FPS(Frames per Second)

FPS是指页面在每秒钟内渲染的帧数,即每秒钟刷新的次数。它衡量的是页面的流畅度和动画效果。下面是获取和优化FPS的方法:

获取FPS的方法:

可以通过浏览器的性能监控工具或第三方工具来获取页面的FPS值,如 Chrome DevToolsWebPageTest 等。

FPS(Frames per Second)持续时间

FPS持续时间是指页面在每秒内渲染的帧数。可以通过监测页面的渲染性能并计算平均帧率来获取。

1
2
3
4
5
6
7
8
9
10
11
// 使用requestAnimationFrame来监测FPS
let fps = 0;
let lastTime = performance.now();
function loop() {
const currentTime = performance.now();
const elapsedTime = currentTime - lastTime;
fps = 1000 / elapsedTime;
lastTime = currentTime;
requestAnimationFrame(loop);
}
loop();

优化FPS的方法:

优化FPS可以通过减少页面渲染的复杂度和优化动画效果来实现。例如,使用CSS3动画代替JavaScript动画、避免频繁的重绘和重排等。

1
2
3
4
5
6
7
8
9
/* 使用CSS3动画 */
.element {
animation: slide-in 1s ease-in-out infinite;
}

@keyframes slide-in {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}

总结

本文详细介绍了Web前端最新优化指标,包括FPFCPFMPLCPTTICLSFIDFPS等,并提供了获取和优化的方法和实例。这些指标不仅帮助开发者更好地评估和优化网页性能,也有助于提升用户体验和网站竞争力。我们可以参考这些指标对网站的性能进行相关的优化。优化是一把双刃剑,有好的一面也有坏的一面,请谨慎优化。

使用 AbortController 实现异步操作控制

使用 AbortController 实现异步操作控制

背景

JavaScript 中,我们经常需要执行一些异步操作,例如发起网络请求、执行定时任务等。然而,有时候我们希望能够在某些条件下中止这些异步操作,以节省资源或提高用户体验。这时候,AbortController 就派上了用场。

AbortController 是一个可以用来控制异步操作的对象,它可以与 PromiseFetch API 等配合使用,实现在异步操作进行中中止操作的功能。本文将介绍 AbortController 的基本用法,并提供详细的示例来说明其在实际场景中的应用。

基本用法

AbortController 提供了两个主要的方法:abort()signal 属性。

  • abort(): 调用该方法可以中止与 AbortController 相关联的异步操作。
  • signal: 这是一个只读属性,它返回一个 AbortSignal 对象,用于监听异步操作的中止状态。
    下面是一个基本的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13

const controller = new AbortController();
const signal = controller.signal;

// 监听中止信号
signal.addEventListener('abort', () => {
console.log('Operation aborted');
});

// 5秒后中止操作
setTimeout(() => {
controller.abort();
}, 5000);

在这个示例中,我们创建了一个 AbortController 对象 controller,并从中获取了 signal 属性。然后,我们通过 setTimeout() 函数设定了一个 5 秒后的定时任务,当定时任务执行时,调用了 controller.abort() 方法来中止操作。同时,我们通过 signal.addEventListener() 方法监听了中止信号,并在中止时输出了一条日志。

结合 Fetch API

AbortController 最常见的用法之一是与 Fetch API 结合使用,实现中止网络请求的功能。下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

const controller = new AbortController();
const signal = controller.signal;

fetch('https://api.example.com/data', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Request aborted');
} else {
console.error('Error:', error);
}
});

// 10秒后中止请求
setTimeout(() => {
controller.abort();
}, 10000);

在这个示例中,我们创建了一个 AbortController 对象 controller,并将其与 Fetch API 中的 signal 属性关联起来。然后,我们发起了一个网络请求,当请求完成时输出了返回的数据,如果请求被中止,则捕获到 AbortError 并输出一条相应的日志。最后,我们设置了一个 10 秒后的定时任务,当定时任务执行时,调用了 controller.abort() 方法来中止网络请求。

结合 Promise

除了与 Fetch API 结合使用外,AbortController 还可以与 Promise 结合使用,实现中止 Promise 执行的功能。下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

function fetchData(signal) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (signal.aborted) {
reject(new DOMException('AbortError', 'AbortError'));
} else {
resolve('Data fetched successfully');
}
}, 3000);
});
}

const controller = new AbortController();
const signal = controller.signal;

fetchData(signal)
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Operation aborted');
} else {
console.error('Error:', error);
}
});

// 5秒后中止操作
setTimeout(() => {
controller.abort();
}, 5000);

在这个示例中,我们定义了一个 fetchData() 函数,该函数返回一个 Promise 对象,在一定时间后返回数据或者中止操作。然后,我们创建了一个 AbortController 对象 controller,并将其与 Promise 关联起来。最后,我们设置了一个 5 秒后的定时任务,当定时任务执行时,调用了 controller.abort() 方法来中止 Promise 执行。

Axios 请求通过 CancelToken 来取消请求

Axios 自带有取消请求的借口,在 Axios 中通过 CancelToken 取消请求发送。下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/ 引入 Axios
const axios = require('axios');

// 创建一个 CancelToken.source 对象
const { CancelToken, axiosInstance } = axios;
const source = CancelToken.source();

// 创建一个 Axios 请求
const request = axiosInstance.get('https://api.example.com/data', {
cancelToken: source.token // 传递 CancelToken 对象到请求配置中
});

// 设置一个定时器,在 3 秒后取消请求
setTimeout(() => {
source.cancel('Request canceled due to timeout');
}, 3000);

// 发起请求并处理响应
request.then(response => {
console.log('Response:', response.data);
}).catch(error => {
if (axios.isCancel(error)) {
console.log('Request canceled:', error.message);
} else {
console.error('Error:', error);
}
});

在这个示例中,我们首先引入 Axios 库,并创建了一个 CancelToken.source 对象 source。然后,我们发起一个 GET 请求,并在请求配置中传递了 cancelToken: source.token,以便 Axios 知道我们要使用哪个 CancelToken 来取消请求。

接着,我们设置了一个定时器,在 3 秒后调用 source.cancel() 方法取消请求,并传递了一个取消原因。最后,我们发起请求,并在 .then().catch() 方法中分别处理响应和错误。如果请求被取消,我们通过 axios.isCancel(error) 来检查错误类型,并输出相应的日志。

XMLHttpRequest 通过 abort 来取消请求

在现代浏览器环境中,我们可以使用 XMLHttpRequest(XHR) 对象来发起网络请求,XHR 里面存在 abort 能用来取消这些请求。以下是一个使用XHR取消请求的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 创建一个XHR对象
const xhr = new XMLHttpRequest();

// 监听请求状态变化
xhr.onreadystatechange = function() {
// 请求完成并且响应状态为200时,处理响应
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
console.log('Response:', xhr.responseText);
}
};

// 准备发送请求,但不发送
xhr.open('GET', 'https://api.example.com/data');

// 发送请求
xhr.send();

// 设置一个定时器,在3秒后取消请求
setTimeout(() => {
xhr.abort();
console.log('Request canceled');
}, 3000);

在这个示例中,我们首先创建了一个 XMLHttpRequest 对象 xhr,并设置了它的 onreadystatechange 事件处理程序来监听请求状态变化。然后,我们调用 xhr.open() 方法来准备发送一个 GET 请求到指定的 URL,但并不发送请求。接着,我们调用 xhr.send() 方法来实际发送请求。

同时,我们设置了一个定时器,在3秒后调用 xhr.abort() 方法来取消请求。当调用 xhr.abort() 方法时,XHR 对象将会立即终止当前的网络请求。

最后,当请求完成并且响应状态为 200 时,我们通过 xhr.responseText 属性获取响应数据,并输出到控制台。

结论

AbortController 是一个非常有用的工具,它为我们提供了在异步操作进行中中止操作的能力。通过结合 Fetch APIPromise 等,我们可以在网络请求、定时任务等场景中灵活地使用 AbortController,从而提高代码的可控性和可靠性。希望本文的介绍能够帮助你更好地理解和应用 AbortController

小程序性能优化实践指南

小程序性能优化实践指南

背景

随着移动互联网的发展,小程序已经成为了许多企业和开发者的首选平台之一。然而,随着小程序功能的不断丰富和用户需求的增加,小程序的性能优化变得愈发重要。本文将介绍一些常见的小程序性能优化实践,并提供详细的例子来说明如何在实际项目中进行优化。

性能指标

小程序(或微信小程序)的性能优化是确保小程序在用户设备上以流畅、快速的方式运行的重要方面之一。以下是一些常见的小程序性能指标:

LoadPackage

指小程序代码包下载阶段。在此阶段,小程序需要从服务器下载代码包以执行后续的逻辑。代码包大小和下载速度会影响小程序的启动性能。

获取方式: 可通过网络监控工具或浏览器开发者工具查看网络请求的时间和代码包大小等信息。

First Paint (FP)

页面首次绘制是浏览器开始将内容呈现在屏幕上的时间点。此时用户无法与页面交互,但首次绘制仍可显示部分内容,向用户展示页面正在加载。

获取方式: 通过浏览器性能分析工具或 Performance API 来捕获此事件,并了解页面渲染启动时间。

First Contentful Paint (FCP)

页面的第一个内容块被绘制到屏幕上的时间点。用户可以看到页面的某些部分,但是页面尚未完全加载。

获取方式: 通过 Chrome DevToolsPerformance 面板或 Lighthouse 等工具可以获取该指标。

First Meaningful Paint (FMP)

用户认为有用内容被展示在屏幕上的时间点。FMP 更准确地反映用户感知到的页面加载速度,因为它考虑了页面加载时的实际内容。

获取方式: 通过用户体验监控工具、Web Vitals 库或Chrome User Experience Report 等方式可以收集数据并分析用户感知到的页面加载速度。

Largest Contentful Paint (LCP)

页面中最大的内容元素(如图片、文本等)被绘制到屏幕上且变得可见的时间点。LCP 是衡量用户认为页面已可交互的重要指标之一,优化 LCP 可提升用户体验和页面加载速度。

获取方式: 通过 Chrome DevToolsPerformance 面板、Web Vitals 库或 PageSpeed Insights 等工具来获取和分析 LCP 数据。

这些指标参数是小程序性能优化的重要参考,通过监控和优化这些参数,可以提升小程序的性能,提升用户体验。

具体措施

图片优化

图片是小程序中常见的资源,但过多或未经优化的图片会增加页面加载时间和用户流量消耗。以下是一些图片优化的实践:

使用适当的图片格式:

  • JPEG 格式适合照片和渐变色图片。
  • PNG 格式适合图标和带有透明背景的图片。
  • WebP 格式是一种新的图片格式,具有更好的压缩率和质量,但需要注意兼容性。

压缩图片大小:

  • 使用工具如 TinyPNGImageOptim 等对图片进行压缩,减小图片文件大小。
  • 避免在小程序中直接使用大图,尽量将图片压缩至合适的尺寸。

懒加载图片:

  • 当页面中存在大量图片时,可以使用懒加载技术,延迟加载图片,减少页面加载时间。
  • 通过小程序的 wx.createIntersectionObserver() 方法监听图片元素是否进入视窗,然后动态加载图片。
1
2
3
4
5
6
7
8
9
10
11
12
13
14

// 示例:图片懒加载
const observer = wx.createIntersectionObserver();
observer.relativeToViewport().observe('.lazy-img', (res) => {
if (res.intersectionRatio > 0) {
const lazyImg = res.dataset.lazyImg;
// 加载图片
this.setData({
imgUrl: lazyImg
});
// 停止观察
observer.disconnect();
}
});

数据缓存和请求优化

小程序中频繁的网络请求会增加用户等待时间和服务器负载,因此优化数据请求和缓存是提升小程序性能的关键。

使用缓存:

  • 对于频繁使用的数据,可以使用小程序的本地缓存功能,如 wx.setStorageSync()wx.getStorageSync(),将数据存储在本地缓存中,减少网络请求。
  • 对于需要实时更新的数据,可以设置合适的缓存过期时间,避免数据过期而导致的不一致性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 示例:数据缓存
const key = 'userData';
const data = wx.getStorageSync(key);
if (data) {
// 从缓存中获取数据
console.log('Data from cache:', data);
} else {
// 发起网络请求获取数据
wx.request({
url: 'https://api.example.com/user',
success: (res) => {
const userData = res.data;
// 将数据存入缓存
wx.setStorageSync(key, userData);
console.log('Data from network:', userData);
}
});
}

合并请求:

  • 尽量减少网络请求的次数,可以将多个请求合并为一个请求,减少请求的重复和消耗。
  • 使用小程序的 wx.request() 方法发送请求,并通过 Promise.all() 方法等待所有请求完成。
1
2
3
4
5
6
7
8
9
10
11
12
// 示例:合并请求
Promise.all([
wx.request({ url: 'https://api.example.com/data1' }),
wx.request({ url: 'https://api.example.com/data2' }),
wx.request({ url: 'https://api.example.com/data3' })
]).then(([res1, res2, res3]) => {
console.log('Data 1:', res1.data);
console.log('Data 2:', res2.data);
console.log('Data 3:', res3.data);
}).catch(error => {
console.error('Error:', error);
});

页面渲染优化

页面渲染是影响用户体验的重要因素,优化页面渲染可以提高小程序的性能和流畅度。

使用 WXML 和 WXSS:

  • 尽量避免使用复杂的 HTMLCSS 样式,可以使用小程序的 WXMLWXSS 实现相同的效果,减少页面渲染时间。
  • 避免使用嵌套过深的结构,尽量保持页面结构简洁。

减少 DOM 操作:

  • 减少在页面渲染过程中对 DOM 的操作,例如频繁的节点添加、删除和更新。
  • 使用小程序的数据绑定功能,在数据变化时自动更新页面,避免手动操作 DOM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 示例:数据绑定
Page({
data: {
userInfo: {}
},
onLoad() {
// 获取用户信息
wx.getUserInfo({
success: (res) => {
// 更新页面数据
this.setData({
userInfo: res.userInfo
});
}
});
}
});

代码包体积优化

当小程序项目过大时,可以将不同功能模块分包加载,按需加载页面和资源,减少初始化时间和页面首次加载时间。

分包策略

独立分包允许小程序页面独立运行,无需依赖主包或其他分包。当从独立分包页面进入小程序时,用户无需等待主包下载,从而显著提升了启动速度。

  • 主包:包含小程序的初始启动逻辑、基础框架和一些常用页面等内容。用户打开小程序时会首先下载主包。
  • 分包:除了主包以外的其他代码包,每个分包可以包含若干个页面或组件。分包相对独立于主包运行,可以根据业务逻辑将相关页面或组件划分到不同的分包中。
分包异步化

分包异步化进一步细化了小程序的分包粒度,从页面级别深入到组件甚至文件级别。这使得原本只能放在主包内的部分插件、组件和代码逻辑得以剥离到分包中,并在运行时异步加载。这一技术有效解决了主包过度膨胀的问题,进一步降低了启动时的代码加载量。

app.json 配置:

1
2
3
4
5
6
"subpackages": [
{
"root": "pages/goods/",
"name": "goods"
}
]

动态引入:

1
2
3
4
5
6
7
8
9
wx.loadSubPackage({
root: 'pages/goods/',
success: function(res) {
// 分包加载成功后的回调
},
fail: function(res) {
// 分包加载失败处理
}
});
分包预下载机制

尽管分包加载能够提升启动速度,但当用户跳转到分包内页面时,仍需等待分包下载,这可能导致页面切换的延迟。为解决这一问题,需要引入了分包预下载机制。该机制允许小程序在后台预先下载分包,确保用户在首次进入分包页面时无需等待下载,从而提升了页面切换的流畅性。

预下载原理:

分包预下载机制利用小程序框架的预加载能力,在用户打开小程序时提前加载分包资源,使得当用户需要访问对应分包页面时,可以更快地展示内容,减少加载等待时间。

预下载机制通常会在主包的某些关键页面或事件触发时开始执行,提前下载分包所需的资源文件,如 JSCSS、图片等。

实现步骤:

  • 识别关键页面:需要确定哪些页面是用户经常访问的重要页面,这些页面通常会被定义为关键页面,可以考虑在这些页面触发时启动预下载机制。

  • 触发预下载:在合适的时机,例如用户打开小程序时或进入关键页面时,调用相应的预下载函数或方法,开始下载分包资源。

  • 资源加载:下载完成后,将分包资源缓存至本地,以便在用户访问分包页面时直接使用已下载的资源,避免重新下载。

实际项目场景:

  • 图片密集页面:对于包含大量图片的分包页面(如相册、产品展示页),通过预下载图片资源可以提高用户体验。

  • 复杂交互页面:对于交互复杂的分包页面(如地图、视频播放页),提前加载相关组件和数据能够加快页面展示速度。

总结

小程序性能优化是一个持续改进的过程,通过合理的设计和优化,可以提高小程序的用户体验和性能表现。本文介绍了一些常见的小程序性能优化实践,包括图片优化、数据缓存和请求优化、页面渲染优化等方面的内容,并提供了详细的示例来说明如何在实际项目中进行优化。希望本文能够帮助开发者更好地优化自己的小程序项目,提升用户满意度和使用体验。

使用 GitHub Actions 自动化部署 Nuxt3 应用到 Docker 容器中

使用 GitHub Actions 自动化部署 Nuxt3 应用到 Docker 容器中

介绍

在当今的软件开发中,自动化部署是提高生产效率和保证代码质量的关键步骤之一。GitHub Actions 是一个强大的持续集成和持续部署工具,而 Docker 则提供了一种轻量级、可移植的容器化解决方案。本文将介绍如何结合 GitHub ActionsDocker,自动化部署一个基于 Nuxt.js 3 的应用到 Docker 容器中。

前提条件

在开始之前,请确保您已经具备以下环境和工具:

  • 一个 GitHub 账号,并且在该账号下创建了一个仓库用于存放您的 Nuxt.js 3 项目。
  • 一个 Docker Hub 账号,用于存放您的 Docker 镜像。
  • 安装了 DockerDocker Compose 的开发环境。

步骤一:准备 Nuxt.js 3 项目

首先,您需要有一个基于 Nuxt.js 3 的项目。如果还没有,可以通过以下命令创建一个新的 Nuxt.js 3 项目:

1
2
Copy code
npx create-nuxt-app@latest my-nuxt-app

按照提示选择项目配置,然后进入项目目录。

步骤二:编写 Dockerfile

接下来,我们需要创建一个 Dockerfile 文件,用于构建 Docker 镜像。在项目根目录下创建一个名为 Dockerfile 的文件,并添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 使用 Node 18 作为基础镜像
FROM node:18 AS builder

# 设置工作目录
WORKDIR /app

# 拷贝 package.json 和 package-lock.json 到工作目录
COPY package*.json ./

# 安装依赖
RUN npm install

# 拷贝源代码到工作目录
COPY . .

# 构建应用
RUN npm run build

# 使用 Nginx 作为基础镜像
FROM nginx:alpine

# 拷贝 Nuxt.js 应用到 Nginx 静态文件目录
COPY --from=builder /app/dist /usr/share/nginx/html

# 暴露 80 端口
EXPOSE 80

# 启动 Nginx
CMD ["nginx", "-g", "daemon off;"]

Dockerfile 文件中定义了两个阶段的构建过程。第一阶段使用 Node 14 作为基础镜像,用于构建 Nuxt.js 应用;第二阶段使用 Nginx 作为基础镜像,用于运行 Nuxt.js 应用。

步骤三:编写 Docker Compose 文件

为了简化 Docker 容器的管理,我们可以使用 Docker Compose 来定义和运行多个容器。在项目根目录下创建一个名为 docker-compose.yml 的文件,并添加以下内容:

1
2
3
4
5
6
7
8
9
version: '3'
services:
app:
build: .
ports:
- '3000:80'
environment:
- NODE_ENV=production
restart: always

Docker Compose 文件定义了一个名为 app 的服务,使用了刚才编写的 Dockerfile 来构建镜像,并将容器的 80 端口映射到宿主机的 3000 端口。另外,设置了 NODE_ENV 环境变量为 production,并且设置容器始终在退出时重新启动。

步骤四:配置 GitHub Actions

接下来,我们将配置 GitHub Actions,使其在每次推送代码到仓库时自动构建并部署应用到 Docker 容器中。

在项目根目录下创建一个名为 .github/workflows 的目录,并在该目录下创建一个名为 deploy.yml 的文件,并添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
name: Deploy to Docker

on:
push:
branches:
- master

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1

- name: Log in to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/my-nuxt-app:latest

- name: Deploy to Docker Compose
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: docker-compose pull && docker-compose up -d

GitHub Actions 配置文件定义了一个名为 Deploy to Docker 的工作流,在代码推送到 master 分支时触发。它包含了以下几个步骤:

  • 检出仓库代码。
  • 设置 Docker Buildx,以便支持多平台构建。
  • 登录到 Docker Hub
  • 构建并推送 Docker 镜像到 Docker Hub
  • 使用 SSH 连接到部署目标主机,并执行 docker-compose pulldocker-compose up -d 命令来更新容器。

步骤五:配置 Secrets

为了安全地管理敏感信息,如 Docker Hub 和部署目标主机的凭据,我们需要在 GitHub 仓库的 Settings -> Secrets 页面中添加这些凭据。

添加以下凭据:

  • DOCKER_USERNAME: 您的 Docker Hub 用户名。
  • DOCKER_PASSWORD: 您的 Docker Hub 密码或访问令牌。
  • SSH_HOST: 部署目标主机的 IP 地址或域名。
  • SSH_USERNAME: 部署目标主机的用户名。
  • SSH_PRIVATE_KEY: SSH 私钥,用于与部署目标主机建立安全连接。

总结

通过结合 GitHub ActionsDockerDocker Compose,我们成功实现了一个自动化部署流程,可以在每次代码更新时自动构建并部署 Nuxt.js 3 应用到 Docker 容器中。这种自动化流程可以大大提高开发团队的生产效率,并且确保了部署的一致性和可靠性。

使用 GitHub Action 实现自动化发布 npm 包

使用 GitHub Action 实现自动化发布 npm 包

在开发 JavaScript 应用程序或库时,发布到 npm 上是一种常见的方式来分享和分发你的代码。手动发布 npm 包可能会变得繁琐和容易出错,因此自动化这个过程是非常有帮助的。

GitHub Actions 是一个功能强大的工具,它可以帮助你实现自动化发布 npm 包的流程。在本文中,我们将学习如何使用 GitHub Action 实现自动化发布 npm 包的步骤。

准备工作

  1. 确保你有一个 npm 账号,并且已经登录到 npm
  2. 创建一个 GitHub 仓库用于存储你的 npm 包的代码。

设置 GitHub Action

  1. 在你的 GitHub 仓库中,创建一个名为 .github/workflows/npm-publish.yml 的文件,用于存储 GitHub Action 的配置。
  2. npm-publish.yml 中添加以下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
name: Publish to npm

on:
push:
branches:
# 触发ci/cd的代码分支
- master

jobs:
build:
# 指定操作系统
runs-on: ubuntu-latest
steps:
# 将代码拉到虚拟机
- name: 获取源码 🛎️
uses: actions/checkout@v2
# 指定node版本
- name: Node环境版本 🗜️
uses: actions/setup-node@v3
with:
node-version: 18
registry-url: 'https://registry.npmjs.org'
# 依赖缓存策略
- name: Npm缓存 📁
id: cache-dependencies
uses: actions/cache@v3
with:
path: |
**/node_modules
key: ${{runner.OS}}-${{hashFiles('**/package-lock.json')}}
# 依赖下载
- name: 安装依赖 📦
if: steps.cache-dependencies.outputs.cache-hit != 'true'
run: npm install
# 打包
- name: 打包 🏗️
run: npm run build
# 测试
- name: 测试 💣
run: npm run test
# 发布
- name: 发布 🚀
run: npm publish
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

申请 npm 访问令牌

  1. 登录 npm 官网,登录成功后,点开右上角头像,并点击 Access Tokens 选项。

image

  1. 点开 Generate New Token 下拉框,点击 Classic Token 选项。

image

  1. 创建一个名称为 GITHUB_PUBLISH_TOKEN 的令牌,并选择 publish 发布权限。

image

  1. 复制新生成的访问令牌。

image

配置 npm 访问令牌

  1. 进入项目仓库,点击仓库tab选项卡的 Settings ,点开 Secrets and variables 选项卡,点击 Actions 选项,点击对应页面的 "New repository secret" 按钮。

image

  1. 新建名称为 NPM_TOKENsecret, 并将刚刚申请到的GITHUB_PUBLISH_TOKEN填入 secret 字段。

image

发布 npm 包

  1. 在你的代码中做任何更改。
  2. 提交这些更改并创建一个新的 Release
  3. GitHub Action 将自动触发并自动构建、测试和发布你的 npm 包。

通过以上步骤,你已经成功地设置了 GitHub Action 来实现自动化发布 npm 包的流程。现在,每当你创建一个新的 Release,你的代码将自动发布到 npm 上,让你的开发流程更加高效和方便。

MacOS 和 Linux 的 Homebrew 安装与卸载

MacOS 和 Linux 的 Homebrew 安装与卸载

Homebrew

macOS(或 Linux)缺失的软件包的管理器

安装 Homebrew

1
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

脚本会在执行前暂停,并说明它将做什么。高级安装选项在 这里

Homebrew 能干什么?

  1. 使用 Homebrew 安装 Mac(或您的 Linux 系统)没有预装但 你需要的东西
1
brew install wget
  1. Homebrew 会将软件包安装到独立目录,并将其文件软链接至 /opt/homebrew
1
2
3
4
5
6
7
8
9
cd /opt/homebrew
find Cellar

# Cellar/wget/1.16.1
# Cellar/wget/1.16.1/bin/wget
# Cellar/wget/1.16.1/share/man/man1/wget.1

ls -l bin
# bin/wget -> ../Cellar/wget/1.16.1/bin/wget
  1. Homebrew 不会将文件安装到它本身目录之外,所以您可将 Homebrew 安装到任意位置。

  2. 轻松创建你自己的 Homebrew 包。

1
2
brew create https://foo.com/foo-1.0.tgz
# Created /opt/homebrew/Library/Taps/homebrew/homebrew-core/Formula/foo.rb
  1. 完全基于 GitRuby,所以自由修改的同时你仍可以轻松撤销你的变更或与上游更新合并。
1
brew edit wget # 使用 $EDITOR 编辑!
  1. Homebrew 的配方都是简单的 Ruby 脚本:
1
2
3
4
5
6
7
8
9
10
class Wget < Formula
homepage "https://www.gnu.org/software/wget/"
url "https://ftp.gnu.org/gnu/wget/wget-1.15.tar.gz"
sha256 "52126be8cf1bddd7536886e74c053ad7d0ed2aa89b4b630f76785bac21695fcd"

def install
system "./configure", "--prefix=#{prefix}"
system "make", "install"
end
end
  1. Homebrew 使 macOS(或您的 Linux 系统)更完整。使用 gem 来安装 RubyGems、用 brew 来安装那些依赖包。

  2. “要安装,请拖动此图标……”不会再出现了。使用 Homebrew Cask 安装 macOS 应用程序、字体和插件以及其他非开源软件。

1
brew install --cask firefox
  1. 制作一个 cask 就像创建一个配方一样简单。
1
2
brew create --cask https://foo.com/foo-1.0.dmg
# Editing /opt/homebrew/Library/Taps/homebrew/homebrew-cask/Casks/foo.rb

国内源安装

国内安装 Homebrew 可能很慢,所以我们推荐使用国内的源来进行安装

macOS

  • 常规安装(推荐):
1
/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"
  • 极速安装(精简版):
1
/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)" speed
  • 卸载
1
/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/HomebrewUninstall.sh)"

Linux

  • 安装

    1
    rm Homebrew.sh; wget https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh; bash Homebrew.sh
  • 卸载

1
rm HomebrewUninstall.sh; wget https://gitee.com/cunkai/HomebrewCN/raw/master/HomebrewUninstall.sh; bash HomebrewUninstall.sh

macOS 常见错误说明

官方表示只支持最新的三个Mac os版本,老的Mac系统可以试试MacPorts。

首先确保运行的/bin/zsh -c "$(curl -fsSL https://gitee.com/ **cunkai** /HomebrewCN/raw/master/Homebrew.sh)" 中间那个 cunkai 不是别的。

1. 如果遇到安装软件报错 404 ,切换网络如果还不行:

查看下官方更新记录https://brew.sh/blog/ 如果近期有更新,可以发我邮箱cunkai.wang@foxmail.com。我看看是否官方修改了某些代码。

2. 不小心改动了brew文件夹里面的内容,如何重置,运行:

1
brew update-reset

3. 报错提示中如果有 git -c xxxxxxx xxx xxx 等类似语句。

如果有这种提示,把报错中提供的解决语句(git -C ….)逐句运行一般就可以解决。

4. 如果遇到报错中含有errno 54 / 443 / 的问题:

这种一般切换源以后没有问题,因为都是公益服务器,不稳定性很大。

5. 检测到你不是最新系统,需要自动升级 Ruby 后失败的:

1
2
3
4
5
HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles

rm -rf /Users/$(whoami)/Library/Caches/Homebrew/

brew update

6. 如果报错 command not found : brew

先运行此命令/usr/local/Homebrew/bin/brew -v ,如果是ARM架构的芯片运行/opt/homebrew/bin/brew -v 看是否能出来Homebrew的版本号。

如果能用就是电脑PATH配置问题,重启终端运行 echo $PATH 打印出来自己分析一下。

7. Error: Running Homebrew as root is extremely dangerous and no longer supported.
As Homebrew does not drop privileges on installation you would be giving all
build scripts full access to your system.

此报错原因是执行过su命令,把账户切换到了root权限,退出root权限即可。一般关闭终端重新打开即可,或者输入命令exit回车 或者su - 用户名

8. /usr/local/bin/brew: bad interpreter: /bin/bash^M: no such file or directory

git config --global core.autocrlf

如果显示true那就运行下面这句话可以解决:

git config --global core.autocrlf input

运行完成后,需要重新运行安装脚本。

9. from /usr/local/Homebrew/Library/Homebrew/ brew.rb:23:in `

brew update-reset

10. M1芯片电脑运行which brew如果显示/usr/local/Homebrew/bin/brew

解决方法,手动删除/usr/local目录,重新安装:

1
2
/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"

11. The x86_64 architecture is required
这句话意思是,这个软件不支持M1芯片,只支持x86_64架构的CPU。

12. Warning: No remote ‘origin’ in /usr/local/Homebrew/Library/Taps/homebrew/homebrew-cask, skipping update!

看评论区说解决方法(我未测试):https://gitee.com/cunkai/HomebrewCN/issues/I5A7RV

13. fatal: not in a git directory Error: Command failed with exit 128: git

1
git config --global http.sslVerify false

14. /usr/local/Homebrew/Library/Homebrew/cmd/vendor-install.sh:1ine245:./3.1.4/bin/ruby:BadCPUtype inexecutable

如果你是苹果的M芯片有这种报错,说明你电脑有两个brew,简单粗暴的方法是删除/usr/local/Homebrew目录,保留/opt/homebrew即可。
(提示:如何去指定访达,屏幕左上角找到前往->前往文件夹然后输入/usr/local回车把Homebrew删除即可。反之如果你是英特尔处理器就保留/usr/local下的去opt目录删除)
温和的方法是分别运行下面三句话,看看是否包含/usr/local/Homebrew的字符串,删掉整行保存。

1
2
3
open ${HOME}/.zprofile
open $HOME/.bash_profile
open ${HOME}/.profile

Emoji 大全

Emoji 大全

记录下 Emoji 表情大全,方便以后拷贝使用

下面内容可以直接复制来用,emoji 不是图片,所以可以任意字号展示,这里只是一部分,并不是全部:

😀😃😄😁😆😅🤣😂🙂🙃😉😊😇🥰😍🤩😘😚😙😋😛😜🤪😝🤑🤗🤭🤫🤔🤐🤨😐😑😶😏😒🙄😬🤥😌😔😪🤤😴😷🤒🤕🤢🤮🤧🥵🥶🥴😵🤯🤠🥳😎🤓🧐😕😟🙁☹️😮😯😲😳🥺😦😧😨😰😥😢😭😱😖😣😞😓😩😫🥱😤😡😠🤬

👶🧒👦👧🧑👱👨🧔👨‍🦰👨‍🦱👨‍🦳👨‍🦲👩👩‍🦰🧑👩‍🦱🧑👩‍🦳🧑👩‍🦲🧑👱‍♀️👱‍♂️🧓👴👵🙍🙍‍♂️🙍‍♀️🙎🙎‍♂️🙎‍♀️🙅🙅‍♂️🙅‍♀️🙆🙆‍♂️🙆‍♀️💁💁‍♂️💁‍♀️🙋🙋‍♂️🙋‍♀️🧏🧏‍♂️🧏‍♀️🙇🙇‍♂️🙇‍♀️🤦‍♂️🤦‍♀️🤷‍♀️👨‍⚕️👩‍⚕️👨‍🎓👩‍🎓🧑‍🏫

👋🤚🖐️✋🖖👌🤏✌️🤞🤟🤘🤙👈👉👆🖕👇☝️👍👎✊👊🤛🤜👏🙌👐🤲🤝🙏✍️💅🤳💪

👣👀👁️👄💋👂🦻👃👅🧠🦷🦴💪🦾🦿🦵🦶👓🕶️🥽🥼🦺👔👕👖🧣🧤🧥🧦👗👘🥻🩱🩲🩳👙👚👛👜👝🎒👞👟🥾🥿👠👡🩰👢👑👒🎩🎓🧢⛑️💄💅💍💼🌂☂️💈🛀🛌💥💫💦💨

⬆️➡️⬇️⬅️↩️↪️⤴️⤵️🔃🔄🔙🔚🔛🔜🔝🛐⚛️🕉️✡️️☯️✝️☦️☪️☮️🕎🔯♈♉♊♋♌♍♎♏♐♑♒♓⛎🔀🔁🔂▶️⏩⏭️⏯️◀️⏪⏮️🔼⏫🔽⏬⏸️⏹️⏺️⏏️🎦✖️➕➖➗♾️⁉️❓❔❕❗💱💲⚕️♻️️🔱📛🔰⭕✅☑️✔️❌❎➰➿✳️✴️❇️#️⃣*️⃣0️⃣1️⃣2️⃣3️⃣4️⃣5️⃣6️⃣7️⃣8️⃣9️⃣🔟🅰️🆎🅱️🆑🉐🈚🈲🉑🈸🈴🈳㊗️㊙️🈺🈵🔴🟠🟡🟢🔵🟣🟤⚫⚪🟥🟧🟨🟩🟦🟪🟫⬛⬜◼️◻️◾◽▪️▫️🔶🔷🔸🔹🔺🔻💠🔘🔳🔲🏁🚩🎌🏴🏳️🏳️‍🌈🏳️‍⚧️🏴‍☠️

🙈🙉🙊💥💫💦💨🐵🐒🦍🦧🐶🐕🦮🐕‍🦺🐩🐺🦊🦝🐱🐈🐈‍⬛🦁🐯🐅🐆🐴🐎🦄🦓🦌🐮🐂🐃🐄🐷🐖🐗🐽🐏🐑🐐🐪🐫🦙🦒🐘🦏🦛🐭🐁🐀🐹🐰🐇🐿️🦔🦇🐻🐻‍❄️🐨🐼🦥🦦🦨🦘🦡🐾🦃🐔🐓🐣🐤🐥🐦🐧🕊️🦅🦆🦢🦉🦩🦚🦜🐸🐊🐢🦎🐍🐲🐉🦕🦖🐳🐋🐬🐟🐠🐡🦈🐙🐚🐌🦋🐛🐜🐝🐞🦗🕷️🕸️🦂🦟🦠🦀🦞🦐🦑

💐🌸💮🏵️🌹🥀🌺🌻🌼🌷🌱🌲🌳🌴🌵🌾🌿☘️🍀🍁🍂🍃

🌍🌎🌏🌐🌑🌒🌓🌔🌕🌖🌗🌘🌙🌚🌛🌜☀️🌝🌞⭐🌟🌠☁️⛅⛈️🌤️🌥️🌦️🌧️🌨️🌩️🌪️🌫️🌬️🌈☂️☔⚡❄️☃️⛄☄️🔥💧🌊

🍇🍈🍉🍊🍋🍌🍍🥭🍎🍏🍐🍑🍒🍓🥝🍅🥥🥑🍆🥔🥕🌽🌶️🥒🥬🥦🧄🧅🍄🥜🌰🍞🥐🥖🥨🥯🥞🧇🧀🍖🍗🥩🥓🍔🍟🍕🌭🥪🌮🌯🥙🧆🥚🍳🥘🍲🥣🥗🍿🧈🧂🥫🍱🍘🍙🍚🍛🍜🍝🍠🍢🍣🍤🍥🥮🍡🥟🥠🥡🦪🍦🍧🍨🍩🍪🎂🍰🧁🥧🍫🍬🍭🍮🍯🍼🥛☕🍵🍶🍾🍷🍸🍹🍺🍻🥂🥃🥤🧃🧉🧊🥢🍽️🍴🥄

🧗‍♀️🤺🏇⛷️🏂🏌️🏌️‍♂️🏌️‍♀️🏄🏄‍♂️🏄‍♀️🚣‍♀️🏊‍♀️⛹️⛹️‍♂️⛹️‍♀️🏋️🏋️‍♂️🚴🚵‍♀️🤸🤼‍♀️🤽🤾‍♀️🤹🧘‍♀️🎪🛹🛼🛶🎗️🎟️🎫🎖️🏆🏅🥇🥈🥉⚽⚾🥎🏀🏐🏈🏉🎾🥏🎳🏏🏑🏒🥍🏓🏸🥊🥋🥅⛳⛸️🎣🎽🎿🛷🥌🎯🎱🎮🎰🎲🧩♟️🎭🎨🧵🧶🎼🎤🎧🎷🎸🎹🎺🎻🥁🎬🏹

😈👿👹👺💀☠👻👽👾💣

👣🎠🎡🎢🚣🏔️⛰️🌋🗻🏕️🏖️🏜️🏝️🏞️🏟️🏛️🏗️🏘️🏚️🏠🏡🏢🏣🏤🏥🏦🏨🏩🏪🏫🏬🏭🏯🏰💒🗼🗽⛪🕌🛕🕍⛩🕋⛲⛺🌁🌃🏙️🌄🌅🌆🌇🌉🎠🎡🎢🚂🚃🚄🚅🚆🚇🚈🚉🚊🚝🚞🚋🚌🚍🚎🚐🚑🚒🚓🚔🚕🚖🚗🚘🚙🚚🚛🚜🏎️🏍️🛵🛺🚲🛴🚏🛣️🛤️⛽🚨🚥🚦🚧⚓⛵🚤🛳️⛴️🛥️🚢✈️🛩️🛫🛬🪂💺🚁🚟🚠🚡🛰️🚀🛸🪐🌠🌌⛱️🎆🎇🎑💴💵💶💷🗿🛂🛃🛄🛅🧭

💌💎🔪💈🚪🚽🚿🛁⌛⏳⌚⏰🎈🎉🎊🎎🎏🎐🎀🎁📯📻📱📲☎📞📟📠🔋🔌💻💽💾💿📀🎥📺📷📹📼🔍🔎🔬🔭📡💡🔦🏮📔📕📖📗📘📙📚📓📃📜📄📰📑🔖💰💴💵💶💷💸💳✉📧📨📩📤📥📦📫📪📬📭📮📝📁📂📅📆📇📈📉📊📋📌📍📎📏📐✂🔒🔓🔏🔐🔑🔨🔫🔧🔩🔗💉💊🚬🔮🚩🎌💦💨
💘❤💓💔💕💖💗💙💚💛💜💝💞💟