Elk — Mastodon 客户端
为了验证 Vue Lynx 能承载真正产品级的应用,我们把 Anthony Fu 团队广受喜爱的
Mastodon Web 客户端 Elk 移植成了一个
原生 Mastodon 客户端。它以游客身份浏览任意公开 Mastodon 实例
(默认 mas.to):时间线、会话串、个人主页、搜索、趋势、深色模式等。
在下方直接体验。Web 标签页通过 Lynx for Web 连接线上实例运行真实应用;
二维码 标签页原生运行同一份 main.lynx.bundle——用 Lynx Go /
Lynx Explorer 扫码即可(安装见快速开始)。
原生 viewpager 变体 需要较新的 Lynx 引擎
elk-viewpager 分叉用原生
<viewpager>
元件承载 Explore 与通知页的标签页,取代按条件渲染的面板。在
TabPager.vue
中,每个面板都是一个 <viewpager-item>:可横向滑动翻页、带原生吸附动画,
滑走再滑回时内容与滚动位置都保留,标签栏与翻页器双向同步——滑动触发翻页器的
change 事件(→ 当前标签),点按标签调用其 selectTab 方法(→ 动画翻到
该页)。体验更接近原生客户端。
<viewpager> 是一个 Lynx
XElement,
在不同平台注册为不同标签名,因此 TabPager.vue 在运行时根据
SystemInfo.platform 选择标签名(下方高亮处):Lynx for Web 用
<x-viewpager-ng>,原生 OSS 引擎用抽取出来的 <viewpager> /
<viewpager-item>。
抽取出来的 <viewpager> 是在 lynx-family/lynx c1d8d7920(2026-04)才进入
OSS 引擎的——比任何已发布的 LynxExplorer 都新。3.8.1 及更早版本既没有
<viewpager> 也没有 <x-viewpager-ng>,标签区域会渲染为空白(并伴随
LynxCreateUIException)。请在从 lynx develop 构建的宿主上运行该变体;
在已发布的 Explorer 上,请用上方的默认 Elk 示例——它按条件渲染的标签无需
pager 元件。
换成原生 pager,到底要改多少代码?
几乎不用。把整个应用切到原生 pager 只动了 三个文件:新增的
TabPager.vue,以及承载可滑动标签页的两个页面(ExplorePage.vue、
NotificationsPage.vue)。其余约 55 个源文件——masto.js 客户端、内容
渲染器、路由、虚拟化 <list>——逐字节完全一致。
在调用处就是一次直接替换。原来的手写标签栏加 v-if / v-else-if 面板,
任一时刻只有当前面板存在:
……换成一个「每个面板一个具名 slot」的组件:
UI 侧的全部改动就是这次 v-if → slot 重写——标签栏、下划线动画、
pager ↔ 标签双向同步,统统搬进 TabPager。
只有一处不是机械替换,它恰好把「原生翻页」的本质讲清楚了。用 v-if 时
只有可见面板存在,一个共享 paginator 就够了。换成 pager 后 所有面板同时
挂载、可来回滑动——于是每个标签各自保留数据和滚动位置。通知页因此从
「一个共享 feed」改成「每标签一份」:
这就是取舍:你放弃「只渲染可见内容」,换来面板始终「热着」——滑走再滑回, 还停在你离开的位置,无需重新加载。
更进一步:会折叠的个人主页
个人主页把这套模式又往「原生 profile」推进了一步。
AccountPage
把同一个 viewpager 包进了 Lynx 的折叠头部协调器
(StickyTabView.vue):
向下滚动时,banner/简介/统计构成的头部折叠收起,标签栏吸顶,
下面的 Posts / Replies / Media 面板继续横向翻页,各自保留自己的 feed
和滚动位置。协调器按「头部(折叠)+ 工具栏(吸顶标签)+ 插槽(viewpager)」
堆叠,其嵌套滚动会先折叠头部,再把滚动交给当前面板的列表。和 viewpager
一样,它按平台注册不同标签名——Lynx for Web 用旧版 <x-foldview-ng>,
原生 OSS 引擎用抽取出来的
<scroll-coordinator>。
原生 Feed 技巧
Mastodon 客户端几乎全是长列表:主页、本地、联邦、探索、通知、搜索、个人帖子、关注者。Web 版 Elk 用 virtua 做虚拟列表,再用 DOM 末端锚点的 bounding box 触发下一页。Lynx 提供两种滚动原语——选哪一个,就是整段性能故事。
<scroll-view> 会挂载全部子节点
<scroll-view> 适合短而异构的页面:设置、发帖、帖子详情、媒体 sheet。每个子节点都会挂载,你滚的是普通布局树。这也是它不适合时间线的原因——联邦时间线一旦超过几十条嘟文,视口上下每一张卡片都在付账。
<list> 负责复用与分页
<list> 是 Lynx 的复用型滚动器。离屏的 <list-item> 单元格会被回收;你只保留一扇真实节点窗口。无限滚动是原生事件,不是几何轮询:
三个属性扛起重活:
TimelinePaginator.vue
是共享的 feed 外壳:首屏、错误重试、复用型列表,以及页脚 spinner / 「时间线到底了」。时间线、Explore 帖子、话题页、书签/收藏,以及个人主页的 Posts / Replies / Media 都复用它。Explore 标签/新闻、通知、关注者、搜索则把同一套 <list> + scrolltolower 接到各自的条目模板上。
<scroll-view> 仍用在复用帮不上忙的地方——帖子详情、设置表单、发帖 sheet。移植最终定下的经验法则是:会无限变长的用 <list>;有限页面继续用 <scroll-view>。
为什么 Elk 是一块试金石
Elk 是一个约 196 个组件、55 个页面、50 个 composable 的 Nuxt 3 应用。 Vue Lynx 没有 Nuxt(没有 SSR、文件路由、Nitro、自动导入),也没有 DOM —— 所以移植没有走 fork 路线,而是复用 Elk 与框架无关的层,在 Lynx 元件上重建 UI:
完整的功能对齐清单(包括刻意未移植的项与原因 —— OAuth 重定向、TipTap、 PWA、Shiki、blurhash 等)见 PRD.md, 架构对照见 PORTING.md, 与原版 elk.zone 的逐屏截图对比见 screenshots/。
亮点
- 内容渲染器是皇冠明珠:Mastodon 的嘟文以消毒后的 HTML 下发。Elk 把它解析成
AST 再渲染为 vnode;移植保持解析步骤逐字节不变,只替换 vnode 目标 ——
自定义表情变为行内
<image>,提及/话题变为可点按的<text>,直接推入 vue-router 路由。 - 原生虚拟化时间线:feed 使用带
estimated-main-axis-size-px与@scrolltolower的 Lynx<list>(而不是<scroll-view>);masto.jsPaginator迭代与 Elk 的排序/缓冲逻辑原样驱动时间线、探索、个人主页与搜索的loadNext()。 - 深链:向 LynxView 传入
globalProps: { initialPath: '/mas.to/tags/caturday' }即可让应用直接打开对应路由 —— 宿主应用处理通知点击时用的就是同一机制。 - 游客 + 令牌会话:像 Elk 游客模式一样匿名浏览,或在设置中粘贴个人访问令牌, 解锁主页时间线、通知、发帖、转发与喜欢。