构建 Synapse:全功能社交媒体应用,使用 React Native 与 Supabase(即将上线!)
Source: Dev.to
关键特性
- 多块帖子(文本、图片、视频、音频、文件、Live Photos)
- 支持语法高亮和 MathJax 的 Markdown
- 动态推荐
- 实时私信
- 全文搜索
为什么我们构建它
Synapse 的想法来源于许多研究人员和开发者共同的一个挫败感:现有的社交平台并未针对技术讨论而构建。
我们不断遇到的问题
1. 讨论碎片化
尝试在 X(Twitter)上跟踪一场机器学习研究的讨论。单篇论文的拆解会被分成 15 条以上的推文在一个线程里。公式是截图,代码是图片。引用推文和回复会导致上下文丢失。简直是一团乱。
2. 缺乏对富内容的内建支持
想分享论文的 PDF?上传代码的音频解释?附加数据集文件?都做不到。当前平台把一切都视为文字 + 图片/视频,迫使研究者和开发者为了任何超出基础媒体的需求而链接到外部服务。
3. 发现机制失效
在你的细分领域找到合适的关注对象出奇地困难。算法优化的是互动率,而不是相关性。没有好的方式来发现你所在子领域的研究者或从事相似问题的开发者。
愿景
我们希望有一个平台,您可以编写一篇完整的帖子,内容包括:
- 您的解释使用 Markdown 并具有适当的格式
- 使用 LaTeX/MathJax 优美渲染的数学公式
- 带语法高亮的代码片段
- 实际的 PDF 或数据集文件
- 为喜欢听的人提供音频讲解
一站式。没有线程。没有外部链接。没有方程截图。
谁适合?
研究人员与学者
想要分享工作、讨论论文并与同行建立联系的科学家。无论你从事机器学习、物理、生物还是任何技术学科,都值得拥有一个真正懂你语言的平台——字面意义上的原生 LaTeX 支持。
开发者与技术创作者
希望以美观、易读的格式分享想法、教程和项目的程序员。使用合适的代码高亮编写技术内容,附加相关文件,打造一个重视实质而非流量的受众。
我们相信,最好的想法值得拥有超越字符限制和碎片化思路的表达空间。
技术栈
前端
- React Native 0.81,启用新架构
- Expo SDK 54,用于快速开发和 OTA 更新
- React 19,具备最新的并发特性
- TypeScript 严格模式(不允许使用
any!)
状态管理
我们采用了混合方案:
- React Query 5,用于服务器状态和缓存
- Zustand,用于客户端存储
- Context API,用于全局应用状态(认证、音频、分析)
后端
Supabase 作为我们的后端即服务(BaaS)
- PostgreSQL,作为数据库
- Supabase Auth,使用邮件 OTP
- Supabase Storage(兼容 S3),用于媒体存储
- 实时订阅,获取实时更新
- Edge Functions(Deno),用于无服务器计算
技术深度解析
1. 内容块系统
与其将帖子存储为平面文本,我们设计了灵活的块式架构:
type ContentBlock = TextBlock | MediaBlock | AudioBlock | FileBlock;
interface MediaBlock {
type: 'media';
items: MediaItem[];
}
interface MediaItem {
type: 'image' | 'video' | 'live_photo';
storagePath: string;
width: number;
height: number;
thumbnailUrl?: string;
// Live photos have separate video/photo URLs
videoStoragePath?: string;
}
为什么重要
- 帖子可以包含混合内容类型
- 每个块负责自己的渲染逻辑
- 未来可以轻松添加新块类型
- 通过运行时类型守卫实现类型安全
2. 乐观消息
没有人想等服务器响应才能看到自己的消息。我们使用本地 ID 实现了乐观更新:
// Generate local ID for immediate display
const localId = `local_${Date.now()}_${Math.random().toString(36)}`;
// Show message immediately with "pending" status
addMessageToCache({
id: localId,
content,
status: 'pending',
created_at: new Date().toISOString(),
});
// Server confirms and provides real ID
// (replace the local entry with the server‑provided one)
const { data } = await supabase
.from('messages')
.insert({ content, conversation_id })
.select()
.single();
// Reconcile local and server states
replaceLocalMessage(localId, data);
棘手的地方?处理边缘情况
- 如果服务器响应在乐观更新之前到达怎么办?
- 如果用户快速发送多条消息怎么办?
- 如何优雅地处理失败?
我们构建了 OptimisticMessageManager,通过适当的调和逻辑处理所有这些场景。
3. 带智能缓存的签名 URL
Supabase Storage 中的媒体文件需要签名 URL 进行私有访问。难点在于:这些 URL 会过期!
我们的解决方案
- 使用 TTL 追踪缓存签名 URL
- 在过期前 5 分钟刷新 URL
- 批量请求 URL 以减少 API 调用
class SignedUrlService {
private cache = new Map<string, { url: string; expiresAt: number }>();
async getSignedUrl(storagePath: string, postId?: string): Promise<string> {
const cacheKey = this.buildCacheKey(storagePath, postId);
const cached = this.cache.get(cacheKey);
if (cached && !this.isExpiringSoon(cached.expiresAt)) {
return cached.url;
}
return this.fetchAndCache(storagePath, postId);
}
// Helper methods (buildCacheKey, isExpiringSoon, fetchAndCache) omitted for brevity
}
传播 postId 上下文以便缓存失效。
4. 动态推荐引擎
我们在服务器端实现了一个推荐 RPC,通过多因素为帖子打分:
- 新鲜度
- 互动指标
- 用户的社交图谱
- 内容类型偏好
// Edge Function: feed-recommendation
const candidates = await fetchCandidates(userId, excludeIds);
const scored = candidates.map(post => ({
...post,
score: calculateRelevanceScore(post, userPreferences)
}));
return scored
.sort((a, b) => b.score - a.score)
.slice(0, limit);
用户可以在时间顺序和推荐两种 feed 之间切换。
5. 手势检测的音频录制
我们实现了自定义的 HoldToRecordButton 与 HoldToRecordOverlay 组件:
- 长按开始录音
- 拖动到取消区域中止录制
- 可视化波形反馈
- 状态变化时提供触觉反馈
这需要与 React Native Gesture Handler 和 Expo Audio 深度集成。
Source: …
经验教训
1. 从 TypeScript 严格模式开始
我们从第一天起就开启了 strict 模式。它捕获了无数在开发阶段本会成为运行时错误的 bug。前期的投入会带来丰厚回报。
2. 服务器端逻辑 > 客户端复杂度
我们不在客户端管理复杂状态(例如阻止用户时的级联删除),而是使用数据库触发器:
CREATE TRIGGER on_user_blocked
AFTER INSERT ON blocked_users
FOR EACH ROW
EXECUTE FUNCTION cleanup_follows_on_block();
即使客户端在操作中途崩溃,也能确保数据一致性。
3. 为离线优先进行设计
我们使用 MMKV(一种高速键值存储)进行本地缓存并设置 TTL:
- 个人资料:5 天
- 动态数据:2 天
- 消息:带有待处理状态的同步
大部分数据都从缓存中读取,使得应用体验非常流畅。
4. 不要低估媒体处理的复杂性
图片/视频上传看似简单,实际要考虑:
- 为加快上传进行压缩
- 生成缩略图
- 进度跟踪
- 上传取消
- 配额控制
- 签名 URL 管理
我们的 postService.ts 有 56 KB 并非偶然。
5. React Query 改变了游戏规则
在使用 React Query 之前,我们在各处手动失效缓存。现在:
const { data, isLoading, refetch } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchUserPosts(userId),
staleTime: 5 * 60 * 1000,
});
// 变更自动失效相关查询
useMutation({
mutationFn: createPost,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
}
});
接下来是什么?
我们正在完善提交到 App Store 前的最后细节:
- 在旧设备上进行性能分析
- 可访问性审计
- 最后一轮 beta 测试
- App Store 资产和截图
想要跟进吗?
我们正在公开构建!关注我们的旅程:
- X:
- LinkedIn:
- Landing page & waitlist:
如果你正在构建类似的东西或对我们的做法有疑问,请在下方留言。很乐意分享关于技术栈任何方面的更多细节!
你在构建移动应用时遇到了哪些技术挑战?在评论中讨论吧!


