news 2026/10/10 15:07:29

单输入框双模式:这款 3MB 浏览器的地址栏设计哲学拆解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
单输入框双模式:这款 3MB 浏览器的地址栏设计哲学拆解

单输入框双模式:这款 3MB 浏览器的地址栏设计哲学拆解

【免费下载链接】SearchA small, fast WebKit browser for macOS, by Office Commun.项目地址: https://gitcode.com/gh_mirrors/search59/Search

浏览器地址栏在过去二十年里经历了"合一—堆料—再回归"的三段式演化。如今主流浏览器的输入框上方,往往叠加着站点 Logo、AI 摘要、新闻流、起始页、账户头像——输入框本身只是入口之一。而 Search(Office Commun 出品、基于 WebKit 的 macOS 浏览器,磁盘占用约 3MB)给出的答案是:整个浏览器只有一个输入框,且它同时且仅同时回答两个问题——"你要去哪"和"你想搜什么"。

这不是功能缺失,而是一套被代码严格约束的设计哲学。本文从仓库源码出发,拆解这个单输入框背后的双模式判定链、键盘流设计,以及砍掉 UI 的取舍清单。

一、单输入框的交互史:从 OmniBox 到极简主义的回归

2006 年 Firefox 2 引入"智能地址栏"(Smart Location Bar),开始把历史记录补全塞进地址栏;2008 年 Chrome 用 OmniBox 把"输入地址"和"输入搜索词"合并进同一个字段,成为此后所有浏览器的默认形态。但合一之后是不断的堆料:Chrome 后续把应用启动器、账户、站点建议塞进同一区域;Arc 等新浏览器则把地址栏升级为"命令栏",让输入框承担站点搜索、快捷指令甚至 AI 提问。

Search 的回归不是回到 Firefox 1 的"地址栏和搜索框分列两端",而是把合一的输入框重新做成"唯一的输入框"。仓库 README.md 的描述很直白:

One field.Type an address and you go there; type words and you search. It finishes addresses from your own history and never sends what you type anywhere until you press Return.

与之配套的是:没有工具栏、没有起始页、没有侧边栏建议、没有账号。整个窗口只有一行标签页和页面本身。在 Omnibox.swift 的头部注释里,这个设计被写得近乎执拗:

One field, in the middle, and the few places it thinks you mean. It takes addresses and only addresses: type something that isn't a place and it shivers and says so, rather than quietly handing your keystrokes to a search engine.

注意后半句——"它只接受地址"。这与 Chrome 的哲学截然相反:Chrome 会把你打的任何内容模糊处理成搜索请求,而 Search 要求输入内容必须能判定为一个地址,否则它宁可"颤抖着拒绝",也不肯悄悄把你的按键交给搜索引擎。

二、双模式判定逻辑:一条 5 步决策链

单输入框最核心的工程问题只有一个:按下 Return 时,怎么知道你打的是地址还是搜索词?大多数浏览器用"看起来像域名就当作地址,否则就搜索"的启发式。Search 的答案在 Browser.swift 的destination(for:)中,是一段严格有序的决策链:

func destination(for typed: String) -> URL? { if let site = siteChip { return site.url(for: typed) } // 1. 字段里已有站点芯片 → 站内搜索 if let url = Address.url(from: typed) { return url } // 2. 输入本身是合法地址 → 直接前往 if let (keyword, rest) = Keyword.match(typed, in: prefs.keywords), let url = Engine.url(for: rest, template: keyword.template) { // 3. 命中关键字快捷词("yt cats")→ 定向搜索 return url } return searchURL(for: typed) // 4. 兜底:交给默认搜索引擎 }

判定核心:Address.url 的严格语法

第二步的Address.url(from:)(Address.swift)是整条链的守门员,它的严格程度决定了双模式的边界:

  • 含空格的输入直接判为"不是地址"——"hello world" 不可能是一个网站;
  • scheme 白名单只有http/https/file/about/data,mailto:、自定义协议一律拒绝;
  • 主机名必须通过looksLikeHost校验:至少两个点分标签、TLD 全部为字母("todo" 不是域名,因为它只有一个标签;以数字结尾的"1.2.3.4.5"是版本号也不是地址)、四个数字段视为内网 IP、IPv6 的[::1]单独处理;
  • 协议选择按目标推断:localhost、.local域名、192.168.*、10.*、Docker 常用的172.16–31.*走http://,其余一律https://——因为"本地服务器几乎没有证书,强行 https 只会得到连接失败"。

建议列表:搜索永远排在地址之后

同一个判定还被用于构建输入框下方的建议列表(guess(),Browser.swift):

var list = history.suggestions(for: typed, limit: 3) // 最多 3 条来自你自己历史记录的建议 if !typed.isEmpty, Address.url(from: typed) == nil { // 只有当它"不可能是个地址"时 list.append(Suggestion(key: typed, …, kind: .search)) // 才会追加一条搜索建议 } if let command { list.insert(.command(command), at: 0) } // 命令栏词条置顶

注意两个细节:历史建议上限是 3 条("列表长到阅读成本超过输入地址本身");搜索建议只在输入被判定为不可能是个地址时才出现,而且排在列表末尾。灰色补全(ending)同样来自你的历史记录——Search 不会把输入发送给任何远程补全服务,直到你按下 Return。这就是 README 里"finishes addresses from your own history"的源码落点。

提交优先级与"拒绝"反馈

submit()的优先级链同样严格:箭头键选中的行 > 灰色补全的地址 > 你实际打的内容。如果三者都无法构成 URL,输入框会拒绝而不是静默转搜索——refusals += 1触发 Omnibox 的抖动动画(Omnibox.swift 中Shake修饰符),边框短暂变红,明说"这不是一个地方"。

三、键盘流设计:一个字段,整条键盘

单输入框要成立,必须把键盘操作做得比任何可视化控件都快。Search 的字段交互基本可以用一句话概括:全程键盘,无鼠标依赖。

按键行为
⌘L唤起输入框,当前地址整段选中,直接输入即替换
Tab接受灰色补全的剩余部分
↑/↓在建议列表中行走,走出列表顶端则放下列表
Return按优先级链提交
Esc收回输入框(先清掉站点芯片,再回到页面)

为什么用 NSTextField 而不是 SwiftUI TextField

Omnibox.swift 中AddressField的注释点出了一个只有实现者才懂的细节:SwiftUI 的 TextField 只能持有你打的那段字符串,而这里必须持有"你没打的那部分"——灰色补全的地址后缀要真实存在于字段里且处于选中状态,这样继续输入会替换它、Return 会接受它。这需要一个真正的NSTextField和它的 delegate。Coordinator里甚至专门处理了"删除键必须真的删得掉"的问题:如果不加保护,灰色补全会在每次退格后被原样写回,形成一个"永远缩短不了的输入框"。

从"地址模式"切换到"搜索模式":站点芯片

这是双模式设计里最优雅的一环,源码注释写得很清楚:"在地址栏搜索一个站点,如 Arc(设置 › 通用)"(SiteSearch.swift)。你输入"red",列表顶部会出现"Search Reddit",按Tab后站点以一个小芯片(SiteChip)的形式进入输入框:

func lockSiteOffer() -> Bool { guard prefs.searchesSites, siteChip == nil, let site = siteOffer else { return false } siteOffer = nil typed = "" siteChip = site // 之后输入的任何词,都进入该站点的搜索 return true }

此后destination(for:)的第一分支接管:输入框内的所有词都通过该站点的模板构造站内搜索 URL。内置了 15 个站点(Reddit、YouTube、X、ChatGPT、Claude、Perplexity、Wikipedia、GitHub、Stack Overflow、MDN、Amazon、Google Maps、IMDb、Spotify、Figma),且你去过的、声明了 OpenSearch 的站点会自动加入。字段里已经有芯片时按⌫可以把它拿走。这个特性默认关闭(Prefs.swift 中searchesSites注释:"Off unless asked for")——符合项目"功能默认最小化"的一贯原则。

第三种模式:⌘K 切换器与命令词

严格说这不止双模式。summoning状态让同一个字段变成标签切换器:⌘K时列表只回答"我已经打开了哪些页",guess()直接短路到openPages(matching:),picked预设为最新页面——"⌘K 然后 Return 就是整个手势"。另有可选的命令栏(AddressCommands.swift):输入settings、history、downloads、passwords等词,回车直接打开应用内对应面板而不是去 Google 搜这个词。同样默认关闭,注释解释得很实在:"没人指望在浏览器里输入 settings 会打开应用自己的设置面板"。

四、砍掉 UI 的背后:为隐私与速度让路的取舍清单

单输入框不是孤立的 UI 决定,它依附于一整份"不做什么"清单。README 的 "What it doesn't do" 部分几乎和 "What it does" 一样长:

速度的取舍

  • 3MB 的根源是复用系统已有的 WebKit——"没有第二份 Chromium 要下载、更新、常驻内存",这也是启动和新建标签近乎即时的原因;
  • 约 42,000 行 Swift,零第三方依赖,一个文件一个关注点,Vault.swift是钥匙串、Shield.swift是广告拦截、Session.swift是会话恢复;
  • 广告拦截是编译一次的WKContentRuleList,在 WebKit 网络层拦截,运行时零开销,而非 JS 注入式拦截;
  • 会话恢复的 Tab 是懒构建的——"带着二十个标签启动依然即时",因为不点开就不占进程。这个优化在代码里甚至细到动画层面:Breath(输入框下方的呼吸微光)从 SwiftUI 动画改为 Core Animation 图层后,空标签页的 CPU 占用从"18% 的一个核心"降到了渲染服务器托管(Omnibox.swift 中的实测注释)。

隐私的取舍

  • 没有同步、没有账号、没有云。README 的原文是:"离开你 Mac 的只有你请求的页面、它们的图标、以及每天一次检查是否有新版本的小请求";
  • 没有遥测、没有分析、不上报崩溃;
  • 密码只进 macOS 系统钥匙串,历史书签是本地 JSON 文件;唯一窗口、无多窗口模式——"标签页是唯一的新开方式"。

结语

Search 的地址栏证明了"单输入框"并不等于"功能少",而是一种更严格的输入契约:它能接受的东西被精确界定,不能接受的会明确拒绝。双模式之所以没有退化成模糊匹配,是因为判定逻辑(Address.url的语法门禁)先于建议列表(搜索建议只在"不可能是个地址"时才出现)先于提交动作(拒绝时抖动提示)形成了一条完整的链路;键盘流则把这条链路的每一步都映射成一个键。在浏览器产品普遍把输入框做成"增长入口"的今天,这种把 UI 砍到只剩一个字段、把逻辑做进判定链的做法,反而显出一种工程上的克制与自信——工具应该安静,这是它最响亮的主张。

【免费下载链接】SearchA small, fast WebKit browser for macOS, by Office Commun.项目地址: https://gitcode.com/gh_mirrors/search59/Search

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 15:07:07

SQL多行合并到一列:四大数据库聚合语法与踩坑指南

1. 多行合并到一列,本质上是在解决哪类问题先说说我为什么想写这个主题。前两天在群里帮一个朋友看需求,他要做订单导出,一张订单对应多个商品明细,需要把商品名称、数量、规格拼成一个备注字段输出到Excel里。这不就是典型的“SQ…

作者头像 李华
网站建设 2026/10/10 15:06:12

PyTorch CIFAR-10 Kaggle提交实战:训练验证推理全链路闭环

简介:本资源是一份面向深度学习初学者与PyTorch实践者的Kaggle图像分类实战教学包,聚焦CIFAR-10数据集的端到端建模流程,帮助读者掌握从数据加载、模型构建(含CNN/ResNet等结构)、训练调优到提交预测的完整竞赛链路。压…

作者头像 李华
网站建设 2026/10/10 15:05:29

Navicat连接达梦数据库报544?自动运行定时备份与同步全攻略

上个月帮客户搭一条定时数据同步链路,源库和目标库都是达梦V8,两边加起来二十几个模式,机器是台Windows服务器,需求是每天凌晨两点自动做全量备份、五点钟跑增量同步。我打开Navicat,新建达梦连接,填好IP和…

作者头像 李华
网站建设 2026/10/10 15:04:57

MySQL生产环境新增从库:停服方式最稳妥的完整实操指南

先把话放前面:如果你问我生产环境新增一台MySQL从库,最稳的办法是什么,我的答案永远是"停服方式"。别觉得它老派,恰恰相反,在遇到线上数据量动辄上百G、又要保证主从数据严格一致的时候,那些花哨…

作者头像 李华
网站建设 2026/10/10 15:04:43

ChineseSubFinder字幕自动化工具:Docker部署与智能匹配实战指南

1. 这不是“下载字幕”的工具,而是帮你重建视频观看秩序的自动化协作者你有没有过这样的经历:深夜追完一集新番,想回看时发现字幕错位、时间轴漂移,手动拖动校准到凌晨两点;或者刚下载完某部冷门纪录片,全网…

作者头像 李华