Matrix-js-sdk 入门到精通:构建去中心化聊天应用

5 阅读7分钟

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/IMAPMatrix 协议
供应商锁定❌ 可以换❌ 可以换

1.4 核心特性

特性说明
去中心化联邦服务器之间互相通信,单个服务器挂了不影响整个网络
端到端加密只有通信双方能解密,服务器管理员也无法查看
开放标准协议完全开源,API 文档公开
桥接能力可以连接 Slack、Discord、Telegram 等
VoIP/WebRTC内置音视频通话支持

1.5 Matrix 生态

客户端(用户使用的 App)

客户端平台说明
Element全平台最流行的官方客户端
FluffyChat移动端轻量级
Nheko桌面端原生性能
CinnyWeb现代界面

服务器(服务端实现)

服务器语言说明
SynapsePython官方参考实现,最成熟
DendriteGo新一代,性能更好
ConduitRust轻量级,适合自建

SDK

SDK语言用途
matrix-js-sdkJS/TSWeb/Node.js(本文主角)
matrix-rust-sdkRustRust 客户端
matrix-java-sdkJavaAndroid

二、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);

十四、最佳实践

  1. 错误处理:始终监听 ClientEvent.sync 的错误状态
  2. Token 安全:不要在前端暴露 token,使用 OIDC 登录流程
  3. 离线支持:使用持久化存储,处理网络断开重连
  4. 性能优化:控制 initialSyncLimit,避免一次加载过多历史
  5. 类型安全:使用 TypeScript,SDK 提供完整类型定义

十五、资源链接

资源链接
GitHubgithub.com/matrix-org/…
API 文档matrix-org.github.io/matrix-js-s…
Matrix 规范spec.matrix.org
示例代码github.com/matrix-org/…
Element Webgithub.com/element-hq/… (官方客户端实现)

总结

matrix-js-sdk 是构建 Matrix 客户端应用的强大工具:

  • 入门简单:几行代码就能跑起来
  • 功能完整:消息、房间、加密、媒体全覆盖
  • 类型友好:TypeScript 支持完善
  • 生产可用:Element 官方客户端基于此

如果你正在寻找一个开放、去中心化、隐私友好的即时通讯解决方案,Matrix + matrix-js-sdk 绝对值得一试!