zchlab.space 支持纯键盘操作:/ 打开命令面板、j/k 在文章列表移动、[ ] 翻页、Alt+c 评论、Enter 发送。有人问:这玩意儿是怎么实现的?

这篇文章把”网站听懂键盘”这件事拆开讲,不装高深,都是前端基本功。

核心:一个全局按键监听器

所有快捷键都挂在同一个事件上:

document.addEventListener('keydown', handler);

浏览器里每一次按键,都会冒泡到 document,这个监听器统一分发。逻辑大致是:

按键来了
├─ Esc → 关面板 / 关帮助 / 退出输入框
├─ Alt 组合键 → 搜索、导航、评论(输入框里也生效)
├─ 输入框里?→ 直接放行(打字优先)
└─ 其他 → 命令面板、j/k 列表移动、Enter 打开…

最关键的一步是”输入框守卫”:当焦点在 input / textarea 里时,所有单字符快捷键直接放行——不然用户打字时,按个 j 就把列表光标移走了,那还怎么聊天?EscAlt 组合键例外,它们在任何地方都该生效(这是”随时能退出”的原则)。

命令面板:输入什么都有反应

/ 弹出命令面板,输入命令回车执行。设计上追求”输入什么都有反应”:

  • 中英文命令都认home / 首页posts / 分类logout / 退出登录……
  • 前缀补全:输入 sub 能命中 submit,取第一个匹配命令执行(类 MC 式补全)
  • 都不匹配?当搜索:输入的不是命令,就自动跳转站内搜索,绝不会让你白敲

两个特殊命令值得一提:

// 个人主页:按需加载登录逻辑,取当前用户跳转
const { supabase } = await import('../lib/supabase');
const { data: { session } } = await supabase.auth.getSession();
location.href = session?.user?.id ? `/u/?id=${session.user.id}` : '/login';

这里用了动态 import:登录相关的库只在执行命令的那一刻才加载,平时不拖累页面。轻量、按需,是这类”偶尔用到的大逻辑”的标准做法。

列表导航:j/k 走天下

j/k 在文章列表、分类卡片、标签卡片间移动光标,Enter 打开。实现朴素但有个讲究:

const kbItems = () => [...document.querySelectorAll(
  '.post-list a[href^="/posts/"], .cat-grid a[href^="/categories/"]'
)].filter((el) => el.getClientRects().length > 0);
  • 统一选择器:不同页面(首页、分类页、标签页)的列表结构各不相同,但都暴露同一个类名,一套代码全站通用
  • 只认可见项getClientRects() 过滤掉隐藏 Tab 里的元素——首页切 Tab 时,j/k 绝不会把光标挪进”看不见”的列表里

评论流:回车即执行

评论输入框里,Enter 发送、Shift+Enter 换行——跟终端一个逻辑(回车执行,Shift 是逃生门)。实现就是在 textarea 的 keydown 里拦一下:

ta.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault();          // 阻止换行
    sendButton.click();          // 触发发送
  }
});

Alt+r 进入”回复选择”模式:j/k 在评论间移动(青色高亮跟随),Enter 展开选中的那条的回复框。这样”回复谁”也变成了键盘操作。

翻页:[ ] 与 URL

首页分页用 URL 参数(?page=N)实现,SSR 直接渲染对应页——好处是刷新不丢页码、链接可分享。键盘上 [ / ] 映射到上一页/下一页,前端只需要读当前分页信息然后跳转:

else if (e.key === '[' || e.key === ']') {
  const pager = document.querySelector('.pager[data-page][data-total]');
  // 有分页才响应,翻到头自动不响应
}

踩过的坑

  • stopPropagation 的代价:命令面板输入框里曾有一个无条件的 stopPropagation(),把 Esc 也拦在了输入框里——结果面板”打开就关不掉”。修法:只拦 Enter,让 Esc 冒泡给全局监听器。事件冒泡是分发的管道,别一棍子全打死。

  • 快捷键的”可发现性”:按键太多记不住,所以做了 ? 帮助浮层,随时查看;侧栏公告和《键盘流操作介绍》文章也在持续教。功能做得再好,用户不知道等于没有。

为什么这么做

因为终端有它的精神:能少碰一次鼠标,就少碰一次;能回车就回车,能用 Esc 就退出。把这种精神搬上网页,就是键盘流——它不是炫技,是给”手不想离开键盘”的人的一份体贴。