Matrix-js-sdk 入门到精通:构建去中心化聊天应用
还在被 IM 平台的 walled garden 困住?Matrix + matrix-js-sdk 让你掌控自己的通讯!
前言
你是否想过:
- 为什么微信用户不能给 WhatsApp 用户发消息?
- 为什么换个聊天软件就要重新加一遍好友?
- 为什么你的聊天数据存在别人的服务器上?
Matrix 协议 就是为了解决这些问题而生的。
今天我们来学习 Matrix 的官方 JavaScript SDK —— matrix-js-sdk,让你能快速构建自己的去中心化聊天应用。
一、什么是 Matrix?
1.1 一句话定义
Matrix 是一个开放的去中心化实时通信协议——就像电子邮件,但是用于即时通讯。
1.2 架构对比
传统 IM(微信、WhatsApp):
用户A ──┐
用户B ──┼──► 中央服务器(公司控制)◄── 用户C、D、E...
用户C ──┘
问题:
❌ 数据被公司控制
❌ 换不了服务器
❌ 无法互通(微信用户不能给 WhatsApp 用户发消息)
Matrix(去中心化):
用户A ──► 服务器1 ◄──► 服务器2 ◄──► 服务器3
▲ ▲ ▲
用户B 用户C 用户D
优势:
✅ 任何人都能搭建服务器
✅ 不同服务器的用户可以互通
✅ 数据自己控制
1.3 类比理解
| 特性 | 电子邮件 | Matrix |
|---|---|---|
| 账号 | user@gmail.com | @user:matrix.org |
| 服务器 | Gmail、Outlook 可以互通 | matrix.org、自建服务器可以互通 |
| 协议 | SMTP/IMAP | Matrix 协议 |
| 供应商锁定 | ❌ 可以换 | ❌ 可以换 |
1.4 核心特性
| 特性 | 说明 |
|---|---|
| 去中心化联邦 | 服务器之间互相通信,单个服务器挂了不影响整个网络 |
| 端到端加密 | 只有通信双方能解密,服务器管理员也无法查看 |
| 开放标准 | 协议完全开源,API 文档公开 |
| 桥接能力 | 可以连接 Slack、Discord、Telegram 等 |
| VoIP/WebRTC | 内置音视频通话支持 |
1.5 Matrix 生态
客户端(用户使用的 App):
| 客户端 | 平台 | 说明 |
|---|---|---|
| Element | 全平台 | 最流行的官方客户端 |
| FluffyChat | 移动端 | 轻量级 |
| Nheko | 桌面端 | 原生性能 |
| Cinny | Web | 现代界面 |
服务器(服务端实现):
| 服务器 | 语言 | 说明 |
|---|---|---|
| Synapse | Python | 官方参考实现,最成熟 |
| Dendrite | Go | 新一代,性能更好 |
| Conduit | Rust | 轻量级,适合自建 |
SDK:
| SDK | 语言 | 用途 |
|---|---|---|
| matrix-js-sdk | JS/TS | Web/Node.js(本文主角) |
| matrix-rust-sdk | Rust | Rust 客户端 |
| matrix-java-sdk | Java | Android |
二、matrix-js-sdk 简介
2.1 什么是 matrix-js-sdk?
matrix-js-sdk 是 Matrix 协议官方的 JavaScript/TypeScript 客户端 SDK,用于构建 Matrix 客户端应用。
核心特点:
- ✅ 可在浏览器和 Node.js 中运行
- ✅ Element 的 Web 和 Desktop 客户端都基于此 SDK
- ✅ 支持端到端加密 (E2EE)
- ✅ 完整的事件驱动模型
- ✅ TypeScript 支持完善
2.2 SDK 为你做了什么?
| 功能 | 说明 |
|---|---|
| 同步 | 自动处理 /sync 长轮询 |
| 房间管理 | 自动生成友好的房间/成员名称 |
| 成员状态 | 管理 typing、power levels、membership 变化 |
| 本地回显 | 发送的消息立即显示为 "sending" 状态 |
| 错误重试 | 网络错误、限流时自动重试 |
| 消息队列 | 离线消息自动排队 |
| 分页 | 历史消息分页加载 |
| WebRTC 通话 | 内置音视频通话支持 |
三、快速开始
3.1 安装
# 推荐使用 pnpm
pnpm add matrix-js-sdk
# 或 npm
npm install matrix-js-sdk
3.2 最简示例
import * as sdk from "matrix-js-sdk";
// 创建客户端
const client = sdk.createClient({
baseUrl: "https://matrix.org"
});
// 获取公开房间列表(无需登录)
client.publicRooms().then(data => {
console.log("公开房间:", data.chunk.map(r => r.name));
});
3.3 登录并启动
import { createClient, ClientEvent } from "matrix-js-sdk";
const client = createClient({
baseUrl: "https://matrix.org"
});
// 密码登录
const loginResponse = await client.login("m.login.password", {
user: "your_username",
password: "your_password"
});
console.log("登录成功!");
console.log("Token:", loginResponse.access_token);
// 启动客户端
await client.startClient({ initialSyncLimit: 20 });
// 监听同步状态
client.on(ClientEvent.sync, (state) => {
if (state === "PREPARED") {
console.log("✅ 客户端已准备好!");
}
});
四、核心概念
4.1 MatrixClient
一切的核心,是与 Matrix 服务器通信的主要接口。
import { createClient, MatrixClient } from "matrix-js-sdk";
const client: MatrixClient = createClient({
baseUrl: "https://matrix.org",
accessToken: "your_token",
userId: "@you:matrix.org"
});
4.2 对象模型
SDK 提供的高级对象模型:
MatrixClient
│
├── Room[] (所有加入的房间)
│ ├── roomId: string
│ ├── name: string
│ ├── timeline: MatrixEvent[] (消息历史)
│ └── RoomState (当前状态)
│ └── RoomMember[] (成员列表)
│
└── User[] (用户信息)
4.3 事件类型
Matrix 是事件驱动的,一切皆事件:
import { ClientEvent, RoomEvent, RoomMemberEvent } from "matrix-js-sdk";
// 常用事件类型
ClientEvent.sync // 同步状态变化
ClientEvent.Event // 所有原始事件
RoomEvent.Timeline // 新消息/事件
RoomEvent.Name // 房间名变化
RoomMemberEvent.Membership // 成员加入/离开
RoomMemberEvent.Typing // 正在输入
五、认证与登录
5.1 密码登录
const client = createClient({ baseUrl: "https://matrix.org" });
const response = await client.login("m.login.password", {
user: "username", // 或 email: "user@example.com"
password: "your_password"
});
console.log("Access Token:", response.access_token);
console.log("Device ID:", response.device_id);
console.log("User ID:", response.user_id);
5.2 Token 登录(推荐用于已有 Token)
const client = createClient({
baseUrl: "https://matrix.org",
accessToken: "syt_xxxxx",
userId: "@you:matrix.org"
});
5.3 注册新账号
const client = createClient({ baseUrl: "https://matrix.org" });
try {
const response = await client.register("username", "password", null, {});
console.log("注册成功:", response);
} catch (error) {
if (error.data?.errcode === "M_USER_IN_USE") {
console.log("用户名已存在");
}
}
5.4 登出
await client.logout();
// 登出后 accessToken 失效
六、消息处理
6.1 发送文本消息
import { MsgType } from "matrix-js-sdk";
// 方式一:最简单
const eventId = await client.sendTextMessage(
"!roomId:matrix.org",
"Hello, Matrix!"
);
// 方式二:sendMessage
await client.sendMessage(roomId, {
msgtype: MsgType.Text,
body: "Hello, World!"
});
// 方式三:sendEvent(最底层)
await client.sendEvent(
roomId,
"m.room.message",
{ msgtype: MsgType.Text, body: "Hello!" }
);
6.2 发送富文本(HTML)
import { MsgType } from "matrix-js-sdk";
await client.sendMessage(roomId, {
msgtype: MsgType.Text,
body: "**粗体** 和 *斜体*", // 纯文本备选
format: "org.matrix.custom.html",
formatted_body: "<b>粗体</b> 和 <i>斜体</i>" // HTML 内容
});
6.3 发送图片
import { MsgType } from "matrix-js-sdk";
// 1. 上传图片
const file = document.querySelector('input[type="file"]').files[0];
const mxcUrl = await client.uploadContent(file, {
name: file.name,
type: file.type,
progressHandler: (p) => console.log(`${p.loaded}/${p.total}`)
});
// 2. 发送图片消息
await client.sendMessage(roomId, {
msgtype: MsgType.Image,
body: "图片描述",
url: mxcUrl, // mxc:// 格式的 URI
info: {
mimetype: file.type,
size: file.size,
w: 1920,
h: 1080
}
});
6.4 接收消息
import { RoomEvent, MatrixEvent } from "matrix-js-sdk";
client.on(RoomEvent.Timeline, (event: MatrixEvent, room, toStartOfTimeline) => {
// toStartOfTimeline = true 表示是历史消息加载,非实时
if (toStartOfTimeline) return;
if (event.getType() !== "m.room.message") return;
const content = event.getContent();
const sender = event.getSender();
console.log(`[${room.name}] ${sender}: ${content.body}`);
// 根据消息类型处理
switch (content.msgtype) {
case "m.text":
console.log("文本:", content.body);
break;
case "m.image":
console.log("图片:", content.url);
break;
case "m.file":
console.log("文件:", content.body);
break;
}
});
6.5 回复消息
import { MsgType } from "matrix-js-sdk";
// 回复某条消息
const replyContent = {
msgtype: MsgType.Text,
body: "> <@user:matrix.org> 原消息\n\n这是回复内容",
"m.relates_to": {
"m.in_reply_to": { event_id: originalEventId }
}
};
await client.sendMessage(roomId, replyContent);
七、房间管理
7.1 创建房间
const room = await client.createRoom({
name: "我的聊天室",
topic: "这是房间主题",
visibility: "public", // "public" 或 "private"
preset: "public_chat", // "private_chat", "public_chat", "trusted_private_chat"
invite: ["@user1:matrix.org", "@user2:matrix.org"]
});
console.log("房间创建成功:", room.room_id);
// 格式: !randomstring:server.com
7.2 加入/离开房间
// 通过房间 ID 或别名加入
await client.joinRoom("!roomId:matrix.org");
await client.joinRoom("#room-alias:matrix.org");
// 离开房间
await client.leave("!roomId:matrix.org");
7.3 获取房间信息
// 获取所有房间
const rooms = client.getRooms();
rooms.forEach(room => {
console.log(room.roomId, room.name);
});
// 获取单个房间
const room = client.getRoom("!roomId:matrix.org");
console.log("房间名:", room.name);
console.log("主题:", room.topic);
console.log("成员数:", room.getJoinedMembers().length);
console.log("消息数:", room.timeline.length);
7.4 邀请/踢人/封禁
// 邀请用户
await client.invite(roomId, "@user:matrix.org");
// 踢人
await client.kick(roomId, "@user:matrix.org", "原因");
// 封禁
await client.ban(roomId, "@user:matrix.org", "原因");
// 解封
await client.unban(roomId, "@user:matrix.org");
7.5 房间设置
// 设置房间名称
await client.setRoomName(roomId, "新房间名");
// 设置房间主题
await client.setRoomTopic(roomId, "新主题");
// 设置房间头像
const mxcUrl = await client.uploadContent(avatarFile);
await client.setRoomAvatar(roomId, mxcUrl);
八、事件系统详解
Matrix-js-sdk 使用 Node.js EventEmitter 风格的事件系统。
8.1 监听成员变化
import { RoomMemberEvent } from "matrix-js-sdk";
client.on(RoomMemberEvent.Membership, (event, member) => {
const prevMembership = event.getPrevContent()?.membership;
const newMembership = member.membership;
console.log(`${member.userId}: ${prevMembership} -> ${newMembership}`);
// 判断类型
if (prevMembership === "join" && newMembership === "leave") {
if (event.getSender() !== member.userId) {
console.log(`${member.userId} 被踢出`);
} else {
console.log(`${member.userId} 主动离开`);
}
}
if (newMembership === "invite") {
console.log(`${member.userId} 收到邀请`);
}
if (newMembership === "ban") {
console.log(`${member.userId} 被封禁`);
}
});
8.2 监听正在输入
client.on(RoomMemberEvent.Typing, (event, member) => {
if (member.typing) {
console.log(`${member.name} 正在输入...`);
} else {
console.log(`${member.name} 停止输入`);
}
});
// 发送输入状态
await client.sendTyping(roomId, true); // 开始输入
await client.sendTyping(roomId, false); // 停止输入
8.3 监听已读回执
import { RoomEvent } from "matrix-js-sdk";
// 发送已读回执
await client.sendReadReceipt(event);
// 监听已读回执
client.on(RoomEvent.Receipt, (event, room) => {
const receipts = room.getReceiptsForEvent(event);
receipts.forEach(r => {
console.log(r.userId, "已读");
});
});
8.4 监听自己被踢
client.on(RoomMemberEvent.Membership, (event, member) => {
if (member.userId !== client.getUserId()) return;
const prev = event.getPrevContent()?.membership;
const now = member.membership;
const sender = event.getSender();
// 被踢出
if (prev === "join" && now === "leave" && sender !== member.userId) {
const reason = event.getContent().reason;
console.log(`🚫 你被 ${sender} 踢出了群聊`);
console.log(`原因: ${reason || "未提供"}`);
}
// 被封禁
if (now === "ban") {
console.log(`⛔ 你被封禁了`);
}
});
九、存储系统
9.1 内存存储(默认)
import { createClient, MemoryStore } from "matrix-js-sdk";
const client = createClient({
baseUrl: "https://matrix.org",
store: new MemoryStore() // 默认,重启后数据丢失
});
9.2 IndexedDB 存储(浏览器推荐)
import { createClient, IndexedDBStore } from "matrix-js-sdk";
const client = createClient({
baseUrl: "https://matrix.org",
store: new IndexedDBStore({
indexedDB: window.indexedDB,
dbName: "matrix-js-sdk"
})
});
// 初始化
await client.store.startup();
await client.startClient();
9.3 LevelDB 存储(Node.js)
import { createClient, LevelDBStore } from "matrix-js-sdk";
const client = createClient({
baseUrl: "https://matrix.org",
store: new LevelDBStore({
path: "./matrix-store"
})
});
十、端到端加密 (E2EE)
10.1 初始化加密
import {
createClient,
IndexedDBStore,
IndexedDBCryptoStore
} from "matrix-js-sdk";
const client = createClient({
baseUrl: "https://matrix.org",
accessToken: "your_token",
userId: "@you:matrix.org",
store: new IndexedDBStore({
indexedDB: window.indexedDB,
dbName: "matrix-store"
}),
cryptoStore: new IndexedDBCryptoStore(
window.indexedDB,
"matrix-crypto"
),
// 加密回调
cryptoCallbacks: {
getCrossSigningKey: async (type) => {
// 从安全存储获取密钥
},
saveCrossSigningKey: async (type, key) => {
// 保存密钥到安全存储
}
}
});
// 初始化加密
await client.initCrypto();
await client.store.startup();
await client.startClient();
10.2 加密房间
// 检查房间是否加密
const room = client.getRoom(roomId);
const isEncrypted = room.hasEncryptionStateEvent();
// 为房间启用加密
await client.sendStateEvent(roomId, "m.room.encryption", {
algorithm: "m.megolm.v1.aes-sha2"
});
// 之后发送的消息会自动加密
await client.sendTextMessage(roomId, "这是加密消息");
十一、实战:简单 Bot
下面是一个完整的 Bot 示例:
import {
createClient,
ClientEvent,
RoomEvent,
MsgType
} from "matrix-js-sdk";
const client = createClient({
baseUrl: "https://matrix.org",
accessToken: process.env.MATRIX_TOKEN!,
userId: process.env.MATRIX_USER_ID!
});
// 命令处理器
const commands = new Map<string, (args: string, roomId: string) => Promise<string>>();
commands.set("ping", async () => "Pong! 🏓");
commands.set("echo", async (args) => args || "请提供内容");
commands.set("time", async () => new Date().toLocaleString("zh-CN"));
commands.set("help", async () =>
`可用命令:\n${Array.from(commands.keys()).map(c => ` !${c}`).join("\n")}`
);
// 启动
await client.startClient();
client.on(ClientEvent.sync, async (state) => {
if (state !== "PREPARED") return;
console.log("🤖 Bot 已就绪!");
// 监听消息
client.on(RoomEvent.Timeline, async (event, room) => {
if (event.getType() !== "m.room.message") return;
if (event.getSender() === client.getUserId()) return; // 忽略自己
const content = event.getContent();
const body = content.body?.trim();
if (!body?.startsWith("!")) return;
const [cmd, ...args] = body.slice(1).split(" ");
const handler = commands.get(cmd.toLowerCase());
if (handler) {
try {
const response = await handler(args.join(" "), room.roomId);
await client.sendTextMessage(room.roomId, response);
} catch (error) {
await client.sendTextMessage(room.roomId, `错误: ${error.message}`);
}
}
});
});
十二、实战:React 聊天组件
import React, { useEffect, useState, useRef } from "react";
import {
createClient,
ClientEvent,
RoomEvent,
MatrixClient,
Room,
MatrixEvent
} from "matrix-js-sdk";
interface Message {
id: string;
sender: string;
body: string;
timestamp: number;
}
function ChatApp() {
const [client, setClient] = useState<MatrixClient | null>(null);
const [rooms, setRooms] = useState<Room[]>([]);
const [messages, setMessages] = useState<Message[]>([]);
const [selectedRoom, setSelectedRoom] = useState<string | null>(null);
const [input, setInput] = useState("");
const messagesEndRef = useRef<HTMLDivElement>(null);
// 初始化
useEffect(() => {
const mc = createClient({
baseUrl: "https://matrix.org",
accessToken: localStorage.getItem("token")!,
userId: localStorage.getItem("userId")!
});
mc.on(ClientEvent.sync, (state) => {
if (state === "PREPARED") {
setRooms(mc.getRooms());
}
});
mc.startClient();
setClient(mc);
return () => mc.stopClient();
}, []);
// 选择房间
useEffect(() => {
if (!client || !selectedRoom) return;
const room = client.getRoom(selectedRoom);
if (!room) return;
// 加载历史消息
const msgs = room.timeline
.filter((e: MatrixEvent) => e.getType() === "m.room.message")
.map((e: MatrixEvent) => ({
id: e.getId()!,
sender: e.getSender()!,
body: e.getContent().body,
timestamp: e.getTs()
}));
setMessages(msgs);
// 监听新消息
const handler = (event: MatrixEvent, room: Room) => {
if (room.roomId !== selectedRoom) return;
if (event.getType() !== "m.room.message") return;
setMessages(prev => [...prev, {
id: event.getId()!,
sender: event.getSender()!,
body: event.getContent().body,
timestamp: event.getTs()
}]);
};
client.on(RoomEvent.Timeline, handler);
return () => client.removeListener(RoomEvent.Timeline, handler);
}, [client, selectedRoom]);
// 滚动到底部
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
// 发送消息
const sendMessage = async () => {
if (!client || !selectedRoom || !input.trim()) return;
await client.sendTextMessage(selectedRoom, input.trim());
setInput("");
};
return (
<div style={{ display: "flex", height: "100vh" }}>
{/* 房间列表 */}
<div style={{ width: 250, borderRight: "1px solid #ccc", overflow: "auto" }}>
{rooms.map(room => (
<div
key={room.roomId}
onClick={() => setSelectedRoom(room.roomId)}
style={{
padding: 12,
cursor: "pointer",
background: selectedRoom === room.roomId ? "#e0e0e0" : "transparent"
}}
>
{room.name}
</div>
))}
</div>
{/* 消息区 */}
<div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
<div style={{ flex: 1, overflow: "auto", padding: 16 }}>
{messages.map(msg => (
<div key={msg.id} style={{ marginBottom: 8 }}>
<strong>{msg.sender.split(":")[0].slice(1)}</strong>
<span style={{ marginLeft: 8, color: "#666", fontSize: 12 }}>
{new Date(msg.timestamp).toLocaleTimeString()}
</span>
<div>{msg.body}</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
{/* 输入区 */}
<div style={{ padding: 16, borderTop: "1px solid #ccc" }}>
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyPress={e => e.key === "Enter" && sendMessage()}
placeholder="输入消息..."
style={{ width: "100%", padding: 8 }}
/>
</div>
</div>
</div>
);
}
export default ChatApp;
十三、常见问题
Q1: 如何处理网络断开重连?
client.on(ClientEvent.sync, (state) => {
switch (state) {
case "RECONNECTING":
console.log("网络断开,重连中...");
break;
case "SYNCING":
console.log("已恢复连接");
break;
case "ERROR":
console.log("同步错误,可能需要重新登录");
break;
}
});
Q2: 如何获取未读消息数?
const room = client.getRoom(roomId);
const unreadCount = room.getUnreadNotificationCount();
const highlightCount = room.getUnreadNotificationCount("highlight");
Q3: 如何下载媒体文件?
// 将 MXC URL 转换为 HTTP URL
const httpUrl = client.mxcUrlToHttp(
"mxc://matrix.org/abc123",
1920, // width
1080, // height
"scale"
);
// 带认证下载(Matrix 1.11+)
const authUrl = client.mxcUrlToHttp(
mxcUrl, undefined, undefined, undefined, false, true, true
);
const response = await fetch(authUrl, {
headers: { Authorization: `Bearer ${client.getAccessToken()}` }
});
const blob = await response.blob();
Q4: 如何分页加载历史消息?
const room = client.getRoom(roomId);
const timelineSet = room.getUnfilteredTimelineSet();
// 向前翻页
const result = await client.scrollback(timelineSet, 50);
// 检查是否还有更多
const hasMore = timelineSet.canPaginate("b" as any);
十四、最佳实践
- 错误处理:始终监听
ClientEvent.sync的错误状态 - Token 安全:不要在前端暴露 token,使用 OIDC 登录流程
- 离线支持:使用持久化存储,处理网络断开重连
- 性能优化:控制
initialSyncLimit,避免一次加载过多历史 - 类型安全:使用 TypeScript,SDK 提供完整类型定义
十五、资源链接
| 资源 | 链接 |
|---|---|
| GitHub | github.com/matrix-org/… |
| API 文档 | matrix-org.github.io/matrix-js-s… |
| Matrix 规范 | spec.matrix.org |
| 示例代码 | github.com/matrix-org/… |
| Element Web | github.com/element-hq/… (官方客户端实现) |
总结
matrix-js-sdk 是构建 Matrix 客户端应用的强大工具:
- 入门简单:几行代码就能跑起来
- 功能完整:消息、房间、加密、媒体全覆盖
- 类型友好:TypeScript 支持完善
- 生产可用:Element 官方客户端基于此
如果你正在寻找一个开放、去中心化、隐私友好的即时通讯解决方案,Matrix + matrix-js-sdk 绝对值得一试!