DNS 之前,浏览器在忙什么:Chrome Omnibox 原理与自定义

「从输入 URL 到页面展示,都发生了什么?」

这道面试题的常见回答,是 DNS 查询、建立连接、发送 HTTP 请求,然后解析 HTML、构建 DOM、布局和绘制。可是在 DNS 之前,还有一个容易被略过的问题:浏览器怎么知道,输入框里的文字是 URL?

输入 foo.com,我们希望打开网站;输入 foo bar,我们希望搜索;输入 foo,它又可能是搜索词、以前访问过的网站,或者公司内网里一台机器的名字。输入过程中还会出现书签、历史记录、已打开标签页和搜索建议。回车之后,浏览器执行的是其中哪一个?

Omnibox:一个输入,多个候选

Chrome 把这个地址栏叫作 Omnibox。它把网址输入、搜索、内容检索和部分操作入口放进了同一个框里。理解它以后,面试题的起点就要往前挪一点:先从输入和上下文里确定要做什么,再进入对应页面的加载过程。

地址栏里看到的候选

例如,在地址栏输入 github.com,先不按回车,Chrome 就会把不同动作放进同一个下拉列表:

Chrome 地址栏输入 github.com 后的真实下拉列表:同时显示 GitHub 访问入口、Google 搜索、搜索补全、Puppeteer 文档和 GitHub Actions 页面

最上面是打开 GitHub 的候选,下一行是把 github.com 交给 Google 搜索;后面还有 github.com codex 等搜索补全,以及 Puppeteer 文档和 GitHub Actions 的具体页面。输入的是一段文字,列表里承载的却是不同目的地和动作。只有确认当前候选以后,浏览器才知道这次要沿哪条路径继续。

调试页里看到的分类与来源

桌面 Chrome 的 chrome://omnibox/ 调试页可以把候选背后的来源直接展开。在同一个浏览器中,把 github.com 输入调试页左上角,保留历史记录和书签,就能对照地址栏观察输入分类与候选来源。

Provider 是地址栏里的候选提供者。某个 Provider 可以查历史记录,另一个可以向搜索引擎请求建议;它们都返回浏览器能统一处理的候选结果。

Chrome Omnibox 调试页查询 github.com:顶部显示 type 为 url、host 为 github.com;候选中同时出现 HistoryURL、Search、Bookmark 和 HistoryQuick

先看顶部状态行:type = url、host = github.com,表示输入处理已经把它识别为具有导航意图的地址。这里的 Connect window omnibox 未勾选,查询来自调试页自己的输入框。

再看候选中的前两行。HistoryURL 提供了 url-what-you-typed,目的地是 GitHub 地址;Search 仍然提供 search-what-you-typed,目的地是把 github.com 当作查询词的 Google 搜索 URL。输入被归为 URL,并没有排除搜索这个动作。

继续往下,Bookmark 命中了已收藏的 Puppeteer 文档,HistoryQuick 找到了以前访问过的 GitHub Actions 页面,分别对应上一张图下拉列表里的具体页面。同一段输入既可以直接访问,也可以执行搜索,还可以找回更具体的已知页面。

截图中的候选、分数和历史信息反映这一份浏览器状态;换一个人的历史记录、书签或默认搜索引擎,结果就可能不同。输入分类、候选来源和最终动作,也由此成为三个需要分别理解的问题。

2008 年:从两个输入框到一个 Omnibox

现在把网址和搜索词输入同一个框,已经是很自然的动作。但回到 Chrome 刚出现的时候,浏览器顶部的界面往往会把这两件事分开。

Google 在 2008 年 9 月 1 日的发布预告里宣布,第二天推出 Windows 版 Chrome Beta;到同年 12 月 11 日,Chrome 才去掉 Beta 标签。因此,观察最早的 Chrome,要从 9 月的首发 Beta 看起。当时 IE 的正式版本是已经于 2006 年发布的 IE7,IE8 要到 2009 年 3 月才正式发布。

IE7:地址栏右边还有一个搜索框

下面是微软《Using Windows Vista》指南里的 IE7 界面。窗口顶部左侧是一条较长的地址栏,里面显示 http://www.microsoft.com/windows/;右侧还有一个较短、提示文字为 Live Search 的独立搜索框。

微软 Vista 指南中的 IE7 界面:顶部左侧为地址栏,右侧为独立的 Live Search 搜索框

图片直接提取自微软原始指南第 6 页的 Figure 5,保留了完整浏览器窗口。页面内容里的搜索框属于微软网站,顶部工具栏右侧的 Live Search 才是浏览器提供的搜索入口。

这套布局给用户一个明确的选择:知道网址,可以在地址栏里输入;只有关键词,可以在搜索框里输入。用户把光标放进哪个框,本身就在表达意图,浏览器能利用这个信号决定接下来做什么。

Chrome 首发 Beta:搜索和导航共用一个框

Chrome 把顶部独立的搜索框合并进了地址栏。首发当天的宣传截图里,浏览器工具栏已经只保留一个统一输入框,访问网站和搜索都从这里开始。

2008 年 Chrome 首发宣传界面:浏览器顶部使用统一输入框,没有 IE7 那样并列的独立搜索框

这张图来自 Google 的首发宣传素材,由 Google Blogoscoped 在 2008 年 9 月 2 日保存。图中新标签页内的 Searches 区域用于检索历史,和浏览器顶部统一搜索、网址输入的 Omnibox 是不同位置。

同一批素材里的输入特写更能说明设计变化。输入 carolina 后,一个下拉列表里同时出现 Search Google for carolina、carolina/、www.carolina.com 和更长的搜索建议:

Chrome 首发宣传素材中的 Omnibox 特写:carolina 对应的搜索、网址和搜索补全同时出现在一个列表里

用户不必先选择「网址框」还是「搜索框」,可以直接输入自己记得的文字,再从混合候选中确认目标。这是 Chrome 当时一个很醒目的交互创新,也已经包含今天 Omnibox 的基本思路:统一入口、生成多种解释,再把合适的动作放到用户面前。

少一个输入框,多一层意图判断

界面上少了一个框,浏览器就必须承担更多意图判断。原来用户还可以通过选择输入框表达意图;Chrome 则需要结合文字本身、历史和当前模式,在同一个入口里处理搜索与导航。

比较对象 IE7 的双框布局 Chrome 的 Omnibox
开始输入时 可以先选择地址栏或独立搜索框 从统一输入框开始
意图信号 输入内容,加上所选输入框 输入内容、历史、搜索模式和候选选择等
候选组织 保留独立搜索入口,地址栏有自己的处理逻辑 在统一入口中组织搜索与导航候选

把搜索和导航统一到一个默认入口,是 Omnibox 输入分类机制的直接产品动机。既然同一段输入可能通向网站,也可能成为搜索词,浏览器就需要判断它更像什么,还要为不确定的情况保留选择。前面的 foo.com、foo bar 和 foo,正是这种设计必须面对的不同输入。

地址栏搜索本身比 Chrome 更早。微软的 IE6 部署指南已经记录了从地址栏自动搜索及相应策略。因此,Chrome 的创新应放在统一入口和候选交互上理解;输入识别与地址栏搜索并不是从 2008 年才开始存在。

正文按 2026 年 10 月 17 日可获取的官方资料整理。源码分析固定在 Chromium 155.0.8059.85,这是当日 Chrome Version History API 返回的 Windows Stable 在投放版本之一。源码中的实验分支、正式扩展 API 和按地区开放的产品功能,会分别说明。

一个 URL 解析器为什么不够

开发者要判断字符串是不是 URL,很容易想到 new URL():

new URL('https://foo.com/').href; // 'https://foo.com/'
new URL('foo.com'); // TypeError

第二行报错,但人在地址栏里输入 foo.com,显然不希望看到「URL 不合法」。浏览器需要接受省略协议的输入,修正一些常见写法,再判断人的意图。

给 URL 构造器补上 base,也解决不了这个问题:

new URL('foo.com', 'https://example.org/').href;
// 'https://example.org/foo.com'

这次解析成功了,foo.com 却被当成相对路径。按照 URL Standard 的语义,这个结果完全正确;它回答的是「如何依照规则解析这个字符串」,地址栏要回答的则是「用户此刻想访问哪个地址,还是想搜索这段文字」。

这两个任务还会产生相反的偏好。解析器能够把某段文字转成一个地址,不代表把它当网址打开就是合适的交互。user@foo.com 可以让人联想到带用户名的 URL,但对大多数输入者来说,它更像邮箱。1.2 也存在 IPv4 解析的历史兼容写法,但把每一个小数都拿去访问,会让日常搜索变得很奇怪。

Chromium 因此在地址栏前面加了一层专门的输入处理。它既使用 URL 库,也增加导航意图、协议和平台方面的判断。

输入分类:网址、搜索,以及暂时无法确定

这层逻辑的入口是 AutocompleteInput::Parse()。名字虽然是 Parse,返回值却不是「合法 / 不合法」两个选项。

四种分类各自表示什么

分类 含义 后续处理的方向
EMPTY 没有有效输入,例如全部为空白 可以展示无需输入文字的候选
URL 输入有较强的导航意图 可以生成直接访问输入地址的候选
QUERY 输入有较强的搜索意图 可以生成搜索这段文字的候选
UNKNOWN 搜索和导航两种解释都还说得通 保留歧义,交给后续候选和上下文处理

UNKNOWN 是这套设计里很重要的一部分。它允许浏览器承认「仅凭这段文字,还不能确定」,而不必立即丢掉其中一种解释。源码最后的注释举了几个例子:单个词可能是内网主机,未知后缀可能是尚未收录的新域名,而 browser.tabs.closeButtons 这种带点的字符串又可能只是一项配置名。

因此,输入分类、下拉候选和最终动作,需要放在三个层次里理解。分类为 UNKNOWN 的字符串仍可能得到一个网址候选;分类为 URL,也不等于用户必须接受这个候选。

从搜索词到导航输入:三张实拍

在同一个浏览器的调试页中,依次输入 foo bar、foo 和 foo/,可以观察这三个层次怎样分开工作。截图保留了相同的显示选项;状态行里的小写 query、unknown、url,分别对应上表中的 QUERY、UNKNOWN、URL。

foo bar:搜索意图明确

这次状态行显示 type = query。可见候选由 Search 提供,第一条 search-what-you-typed 把原文交给 Google 搜索,后面的 search-suggest 则补出了 foo bar baz、foo bar 意思 等查询。

Omnibox 调试页输入 foo bar:状态行为 type = query,可见候选包含搜索原文和 foo bar baz 等搜索补全

foo:分类保留歧义

删去空格和 bar,只留下一个短词,状态行变成 type = unknown。截图中可见的仍然是搜索候选:UNKNOWN 允许后续考虑多种解释,并不要求每次都同时显示网址与搜索候选。

Omnibox 调试页输入 foo:状态行为 type = unknown,Search 同时提供普通搜索补全和带图标的 Footlocker、Foothill College 实体建议

这里还能看到两种不同的候选类型。food 标为 search-suggest;Footlocker 和 Foothill College 则标为 search-suggest-entity,同时带有图标、对象名称和补充描述。这些实体建议仍由 Search 提供,右侧目的地仍是 Google 搜索 URL。候选展示得更丰富,并没有给整个输入增加一种新的分类。

foo/:一个斜杠改变导航意图

在 foo 后面追加 /,状态行变成 type = url。HistoryURL 给出了 url-what-you-typed,右侧目的地是 http://foo/;Search 同时保留了搜索 foo/ 的候选,以及其他搜索建议。

Omnibox 调试页输入 foo/:状态行为 type = url,HistoryURL 生成指向 http://foo/ 的导航候选,Search 仍保留搜索候选

foo 与 foo/ 只差一个字符,却从保留歧义变成了具有导航意图的输入。这是地址栏在判断用户想做什么;生成 http://foo/ 这个候选,也不代表当前网络里一定存在名为 foo 的主机。具体为何是末尾斜杠改变了判断,还要回到输入解析规则。

先分段和修正,再判断意图

Parse() 先识别输入的各个部分,再尝试构造修正后的 URL。下面是入口里的真实节选,省略了中间变量准备:

const std::u16string parsed_scheme(url_formatter::SegmentURL(text, parts));
// ... 省略输出参数和临时变量准备。
*canonicalized_url =
    url_formatter::FixupURL(base::UTF16ToUTF8(text), desired_tld);
if (!canonicalized_url->is_valid())
  return metrics::OmniboxInputType::QUERY;

这里的 scheme 是协议部分,例如 https:、file:、javascript:。SegmentURL() 先帮助浏览器分辨它;FixupURL() 则尝试修正输入,得到能继续分析的地址。

这一层必须理解用户会省略什么。www.foo.com:81 里的冒号通常分隔主机和端口,不能仅凭「开头出现一段文字和冒号」就认定前面是协议。反过来,link:foo.com 又可能是搜索操作符,不能机械地补一个协议就结束。

修正成功以后,浏览器还要检查主机、IP 格式、后缀、端口、用户名、路径和特殊协议。它使用已知注册后缀的数据,但也保留对内网名字和特殊用途名字的处理。所以「看有没有点」「看后缀在不在名单」「用一个 URL 正则」都只能覆盖其中一部分。

为什么 localhost、内网名字和小数会不同

在上述版本中,下面这段分支直接表达了几个强信号:

if (has_known_tld || canonicalized_url->DomainIs("localhost") ||
    canonicalized_url->has_port())
  return metrics::OmniboxInputType::URL;

已知后缀、localhost 和端口都能增加导航的确定性,但这段代码位于其他合法性检查之后,不能读成「任何带端口的字符串都是网址」。例如无效端口已经可能在前面被排除。

下面按判断线索拆开看。图中着色的是参与判断的输入片段,中间给出对应线索,右侧是输入分类。结果采用该版本上游 AutocompleteInputTest.InputType 的预期,图中的理由同时对照了 Parse() 分支;它们不是本机运行 Chromium 测试得到的报告,也不代表最终下拉列表。

先看主机名与端口。同样是 foo,加上已知后缀或合法端口会增加导航的确定性;localhost 本身就有专门规则,因此带不带端口都能归为 URL。

主机名、空格和端口的分类依据:高亮 .com、空格、单个词、端口和 localhost,并连接到命中的线索与 URL、QUERY 或 UNKNOWN

斜杠也要看位置。foo/bar 只有路径线索,仍保留歧义;末尾再补一个 /,就变成了更明确的导航输入。邮箱形式和前导问号,则把判断带向不同方向:

高亮末尾斜杠、中间路径、邮箱用户名和前导问号,解释它们如何分别得到 URL、UNKNOWN 和 QUERY 分类

数字输入需要同时看原始分段和导航线索。即使 URL 库能接受某种 IP 简写,地址栏也可能默认把它当成搜索;这就是 1.2 与 1.2/、三段与四段 IPv4 形式之间的区别。

高亮小数点、末尾斜杠、IPv4 的三段与四段以及 IPv6 的方括号和冒号,连接对应判断与输入分类

这些差异已经足以解释正则方案为什么容易失效:同一段文字是否「能表示地址」,和用户是否「更可能想访问地址」,并不是同一道判断题。

当前源码还对 .example、.test、.local、.internal 做了明确的特殊处理:这些后缀下有子域名时,可以被识别为 URL。.local 对应多播 DNS 的使用场景,.internal 则用于私有用途。这个分支不会自动创建 DNS 记录;浏览器认出了网址,机器仍然可能解析不到它。

对不确定的内网名字,最稳妥的做法通常是输入完整的 http://主机名/ 或 https://主机名/,明确协议和导航意图,再单独排查名称解析与服务是否可访问。

javascript: 为什么还需要一条启发式规则

这条规则的起点,是一个很普通的需求:在地址栏里搜一本书。用户输入的是书名,浏览器却可能把它理解成执行脚本的指令。

书名与协议撞在了一起

2010 年 1 月 6 日提交的 Chromium issue 31661,标题就是「无法在 Omnibox 中搜索 JavaScript: The Definitive Guide」。报告说明:输入 JavaScript: The Definitive Guide 后,浏览器把它当成 javascript: URL,没有执行搜索。这条旧 issue 现在迁移到了 41071946。

Chromium issue 31661 的原始报告截图:搜索 JavaScript: The Definitive Guide 时被识别成 javascript URL,报告提出过区分协议名大小写的方案

冲突出在冒号上。对读者来说,JavaScript: 是书名的一部分;对 URL 解析器来说,前面的文字和冒号又正好符合协议部分的形状。javascript: URL 的用途,是让浏览器在页面上下文中处理后面的脚本。把「识别到协议」直接等同于「用户想执行脚本」,就把搜索一本书的意图丢掉了。

报告最后提出过一个修复方向:让 javascript: 协议检测区分大小写,以便把大写的书名和小写的代码分开。但这是当时的提议,不能当成今天的实现。URL Standard 的协议解析规则会把协议名转成小写;当前 Chromium 的比较与正则也都忽略大小写。只按首字母大小写判断,既不能覆盖小写书名,也会影响原本合法的混合大小写协议写法。

从直接搜索到保留歧义

与这条 issue 直接关联的 2019 年 3 月修复提交,增加了一个更有针对性的判断:javascript: 后面如果不像代码,就返回 QUERY,让它可以被搜索。判断线索是有没有分号、等号、括号和点号等标点。

同年 4 月的后续提交又把 QUERY 改成了 UNKNOWN,并把双引号加入代码线索。对应测试也随之改变:javascript:foo、JavaScript:Tutorials 保留为 UNKNOWN,javascript:"foo" 则归为 URL。

这个改动的效果,是把「它不像典型代码」与「它一定是搜索词」分开。返回 UNKNOWN 能保留用户确实想执行脚本的可能,把选择交给后续候选和上下文。启发式规则解决的,是书名这类自然文本不该仅凭一个前缀就被强行送进脚本路径;它没有承诺彻底消除所有歧义。

标点线索的作用与局限

在本文固定的 Chromium 版本中,这段判断仍然存在:

if (base::EqualsCaseInsensitiveASCII(parsed_scheme_utf8,
                                    url::kJavaScriptScheme) &&
    RE2::FullMatch(base::UTF16ToUTF8(text), "(?i)javascript:([^;=().\"]*)")) {
  return metrics::OmniboxInputType::UNKNOWN;
}

[^;=().\"]* 表示后面的内容完全不包含这组字符,FullMatch() 要求整个输入匹配;满足时返回 UNKNOWN。从 JavaScript 的语法形态看,这些字符常见于语句分隔、赋值、函数调用、成员访问和字符串,因此能提供「更像代码」的线索。它们也会出现在自然语言里,不能证明后面一定是代码。

JavaScript: The Definitive Guide 的书名正文没有这些标点,按这个分支会保留为 UNKNOWN。javascript:alert(1+1) 则带有调用括号,不会命中这条保留歧义的分支,会继续按 javascript: 协议处理为 URL。下面用同一个 foo 改变标点,可以更清楚地看见这一差别:

javascript:foo 没有特征标点而归为 UNKNOWN;高亮新增的分号和点号,展示 javascript:foo; 与 javascript:foo.getter 归为 URL 的代码形态判断

这组标点并不能替代 JavaScript 语法分析。javascript:1+1 后面的 1+1 是合法表达式,却没有上述标点,按该分支仍会归为 UNKNOWN;反过来,在书名末尾加一个英文句点,就可能让它绕过这条规则。即使完整解析了语法,也无法证明用户希望执行一段合法表达式,还是把它交给搜索引擎。

至于 javascript:alert(1+1)「弹出 2」,说的是后续脚本确实获准执行时,alert 展示算式的结果。分类为 URL 本身不会执行脚本;实际结果仍受候选选择、粘贴处理、当前页面和安全限制影响。HTML Standard 的 javascript: 特殊导航流程也把脚本求值与相关检查单独规定。这条路径本身不会为了加载脚本 URL 发起网络请求,脚本内部仍可自行联网。

这也说明了开头那道面试题为什么需要先问清输入:即使浏览器把某段文字归为 URL,接下来也未必是 DNS 和 HTTP。必须先确定它代表搜索、网络导航,还是特殊协议动作,才能讨论后面的加载过程。

补协议也有后续规则

输入 foo.com 后,浏览器需要决定补什么协议。不能因为修正层曾产生 HTTP URL,就断言实际导航永远先走 HTTP。

ShouldUpgradeToHttps() 负责其中一层 HTTPS 默认处理:对于符合条件、没有显式输入 HTTP 协议的地址,可以改用 HTTPS。IP、非唯一主机名和指定端口等情况又有相应例外。后续的 HTTPS 升级、安全策略与回退,还属于导航链路自己的职责。

输入识别决定「它像什么」,协议选择决定「怎样访问它」,网站是否存在又是另一件事。这几个问题在界面上常常只表现为一次回车,内部却需要分别处理。

下拉列表从哪里来,回车又选中了什么

输入分类完成后,Omnibox 还要从多个来源生成候选。这也是为什么同一个词,在不同人的电脑上会有不同结果。

地址栏将输入分类和修正,交给网址、历史书签、搜索和扩展等来源生成候选,合并排序后,由用户确认访问、搜索或其他动作

Provider 提供候选,Controller 组织更新

AutocompleteController 负责组织这些来源。候选既可以同步产生,也可以异步补充,所以我们会看到下拉列表随着输入和网络返回不断更新。

候选来源 提供什么 和输入的关系
直接导航候选 修正后可访问的地址 对应用户原本输入的地址
历史记录 以前访问过的 URL、标题等 可以匹配过去的浏览行为
书签 已收藏的网站和标题 可以用自己记得的名称检索
搜索 搜索原文、搜索引擎返回的建议等 可以把不完整输入补成搜索请求
已打开标签页 当前已有的页面 可以切换到现有标签页
关键词与扩展 指定站点或扩展提供的候选 受搜索模式和扩展接入条件约束

这些是职责分组,实际实现包含多个 Provider,也会随平台和功能开关变化。源码入口可以从 components/omnibox/browser/ 里的 history_*_provider、bookmark_provider、search_provider、open_tab_provider 和 keyword_provider 看起。

排序不只是找一个最高分

候选生成后,浏览器需要处理重复结果、数量限制、展示分组和默认项资格。两个来源可能都找到了同一个网站,不应简单地重复展示;一个候选即使可以出现在下拉列表里,也未必适合直接成为回车的默认动作。

这些规则在 AutocompleteResult 里有明确入口,例如 DeduplicateMatches()、SortAndCull(),以及候选上的 allowed_to_be_default_match。

我们看到的行内补全,也是排序和默认项选择的结果之一。输入区里出现了选中的补全文字,回车时就要看当前选中的候选,而不能只盯着自己最初敲进去的几个字符。

评分本身也在演进。Chrome 从 M124 开始,把机器学习模型引入桌面版地址栏的网页建议评分。一个官方介绍过的例子很有意思:访问某个页面后立刻回到地址栏重新输入,可能说明刚才选错了;因此「刚访问过」在很短的时间间隔内,反而可能降低相关性。

这说明评分需要理解行为的含义,不能简单等价为「访问越近,永远排得越高」。但机器学习评分也没有替代前面的 URL 解析、协议规则和候选资格判断,它只参与整个决策过程中的一部分。

候选里保存的是动作,不只有一段文字

输入框显示的文字、候选描述和最终目的地,不一定相同。

直接访问候选可能指向 https://foo.com/;搜索候选可能把同一段输入放进默认搜索引擎的查询参数;标签页候选可以切换现有页面;操作入口则可以打开浏览器设置。

观察地址栏时,可以依次确认输入类型、当前选中的候选,以及它指向的动作。把它们混成「是不是 URL」一个问题,就很难解释那些看起来反常的结果。

今天的地址栏都能做什么

Omnibox 的能力可以按检索对象和执行动作理解。以下以桌面 Chrome 为主;移动端、不同语言、默认搜索引擎和受管理环境,可能有不同入口。

从网站检索到浏览器内检索

最熟悉的两项仍然是访问网址和执行搜索。除此之外,地址栏还能找回历史页面、搜索书签,以及切换到已经打开的标签页。

对于后面几种检索,可以用内置快捷字明确指定范围:

输入入口 查找范围 交互方式
@tabs 已打开标签页 输入快捷字,按 Tab 或空格,再输入关键词
@bookmarks 书签 进入书签搜索模式后输入关键词
@history 浏览历史 进入历史搜索模式后输入关键词
自定义站点快捷字 某个网站的搜索结果 进入该站点的搜索模式

这些入口分别写在标签页、书签和历史记录的官方帮助里。它们能减少歧义:输入 react 可能同时得到网站和搜索建议,先进入 @tabs 模式,就明确是在找现有标签页。

地址栏也可以展示计算结果等答案型建议。例如 CalculatorProvider 会观察 SearchProvider 的返回,处理计算类候选。这类结果不能一概当作浏览器内置的离线计算器;它们可能依赖搜索引擎的建议服务、语言和当前功能配置。

浏览器操作也可以成为搜索结果

Chrome Actions 会为某些任务提供操作按钮。例如搜索与清除浏览数据相关的词时,可以出现打开对应设置的按钮。

它把「找设置」变成了一次意图匹配。点击按钮后进入相应操作入口,后续需要确认的操作仍然由对应界面完成。触发词随语言和平台变化,也不能把所有自然语言请求都视为浏览器支持的命令。

AI 搜索、页面上下文和 Gemini 是不同入口

最近的变化集中在搜索入口对上下文和多模态输入的支持,但需要分清功能归属。

AI Mode 属于 Google Search 的搜索体验。2026 年 4 月的官方更新介绍了侧边搜索体验,以及把近期标签页、图片和文件组合到搜索中的方式。同年 5 月的搜索更新继续扩展了搜索框的输入形式。这些更新涉及新标签页搜索框、AI Mode 和侧边面板,不能全部等同于传统的一行地址栏。

「Ask Google about this page」使用当前页面作为上下文。当前搜索帮助列出了从地址栏提问、选取页面区域和在侧边面板查看结果的方式,也列出了 Google 默认搜索引擎、相关设置及首次内容共享确认等条件。

@gemini 打开 Gemini 网页应用。官方帮助描述的是从地址栏进入 Gemini 网页应用的快捷方式。它与 Gemini in Chrome 的浏览器助手集成,还需要分别理解,不能把输入 @gemini 解释为已授予读取全部标签页的权限。

这些产品功能存在账号、地区、语言、平台和逐步开放的限制。源码里出现某个 Provider,或者官方宣布开始开放,都不能推出所有 Chrome 用户已经拥有同样的按钮。

普通用户、网站和扩展分别能自定义什么

自定义可以从最轻的一层开始。只是想少点几次鼠标,通常不需要开发扩展;只有当「把关键词放进搜索 URL」已经表达不了需求时,才需要更深的接入。

普通用户:默认搜索引擎和站点快捷字

在设置里的「搜索引擎 → 管理搜索引擎和网站搜索」,也可以手动输入 chrome://settings/searchEngines,能管理默认搜索引擎与站点搜索快捷字。官方帮助把新增项分成三个主要字段:名称、快捷字,以及用 %s 代替查询内容的 URL。

例如,搜索结果页如果长这样:

https://example.com/search?q=browser

就可以把模板设为:

https://example.com/search?q=%s

这里的 example.com 是示意站点,读者应换成目标网站真实的搜索结果页。假设快捷字设为 docs,使用时就在地址栏输入 docs,按 Tab 进入该站点的搜索模式,再输入搜索词。

浏览器负责把查询放进模板,网站负责返回搜索结果。这种自定义改变的是搜索入口,没有修改网站自身的搜索算法。名称也不一定非要叫搜索引擎:文档中心、代码托管站和知识库,只要有合适的查询 URL,都可以成为候选对象。

它也有明确局限。搜索必须依赖登录时,结果页仍需要相应会话;只支持 POST、复杂请求体或页面内部状态的网站,不一定能靠一个 %s 模板完成。纯前端的搜索弹窗如果没有能从 URL 恢复查询的页面,填一个看起来像 /search?q=%s 的地址也不会凭空生效。

内置的标签页、书签和历史快捷字也能在网站搜索设置里编辑或停用。部分站点会先出现在「未启用的快捷字」区域,需要用户主动启用。受管理的浏览器则可能由管理员决定这些选项是否可以修改。

网站作者:用 OpenSearch 描述自己的搜索入口

网站可以通过 OpenSearch Description Document 向浏览器描述搜索方式。它是一个声明搜索模板的 XML 文档,浏览器可以据此发现站点搜索入口。

按 Chromium 的 Tab to Search 文档,网站首页可以在 <head> 里放一条发现链接:

<link rel="search"
      type="application/opensearchdescription+xml"
      href="/opensearch.xml">

其中 /opensearch.xml 由网站作者提供,内容的核心是搜索 URL:

<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
  <ShortName>Example Docs</ShortName>
  <Description>搜索 Example 文档</Description>
  <Url type="text/html"
       template="https://example.com/search?q={searchTerms}" />
</OpenSearchDescription>

OpenSearch 使用 {searchTerms},前面 Chrome 设置界面使用 %s,两者属于不同配置格式,不能直接互换。网站必须真的有这个搜索页面;描述文件只告诉浏览器如何调用它。

OpenSearch 还可以声明建议服务,让网站根据部分输入返回建议。这就从「回车后跳转」延伸到了「输入过程中参与候选生成」。具体格式见 OpenSearch 描述规范。

发现入口不等于默认启用,更不等于网站可以自行替换用户的默认搜索引擎。旧设计文档还介绍了从 GET 搜索表单推导模板的方式,但其中列出了表单结构限制,不能据此保证现代 SPA 的任意搜索框都会被自动识别。对用户来说,设置里的未启用快捷字和手动添加仍是更直接的确认入口。

扩展开发者:关键词模式中的动态建议

当需求变成查询多个系统、给出动态候选,或者选中结果后执行特定动作,就可以看 chrome.omnibox。

正式文档里的基本接入方式,是在扩展的 manifest.json 声明关键词:

{
  "omnibox": { "keyword": "docs" }
}

这是 manifest 的局部节选,完整扩展还需要 Manifest V3 的基础字段和后台 service worker。进入这个扩展的关键词模式以后,输入事件才交给扩展,扩展再返回候选。

下面是后台监听的接口形状,用来展示候选内容和展示文字的区别;它不是完整扩展:

chrome.omnibox.onInputChanged.addListener((text, suggest) => {
  suggest([{
    content: 'open:reference',
    description: '<match>参考文档</match> <dim>打开 API 索引</dim>'
  }]);
});

description 是下拉列表里的文字;content 是选中后返回给扩展的内容,它可以是一个内部命令标识,浏览器不会因为它看起来像 URL 就自动替扩展完成导航。扩展要在接受输入的事件中处理它。

结合当前 API schema,关键词模式主要开放这些接入点:

接口 扩展可以做什么
onInputStarted 在一次关键词输入会话开始时准备状态
onInputChanged 根据输入生成候选,通过 suggest() 返回
setDefaultSuggestion() 设置默认候选的描述
onInputEntered 处理用户确认的内容
onInputCancelled 取消等待中的工作,清理本次会话状态
deletable 与 onDeleteSuggestion 允许删除候选,并处理删除事件
disposition 按用户意图在当前页、前台新标签页或后台新标签页展示结果

描述支持 <url>、<match>、<dim> 这几个样式标签,不是任意 HTML,也不能注入自己的 CSS。拼入外部文本时,要转义 XML 实体,不能把用户输入直接当标记处理。

实现动态建议时,还需要处理旧请求晚返回的问题:输入从 rea 变成 react 后,前一个请求的结果不应覆盖新结果。用户取消输入时可以终止请求;监听器要在 service worker 顶层注册,避免异步初始化期间漏接事件。官方 service worker 教程提供了一个可继续阅读的完整接入示例。

仅为关键词模式注册 omnibox,不需要顺手申请所有网站、历史记录或标签页读取权限。只有扩展确实要读取这些数据,或跨域请求特定服务时,才添加对应权限,并限制目标范围。查内部资料时,候选服务的访问控制仍然由服务本身负责。

无需关键词的扩展建议仍有实验边界

当前稳定版源码里确实有 UnscopedExtensionProvider,对应不先进入扩展关键词模式就参与建议的路径。它还涉及 omnibox.directInput、建议操作按钮和图标等能力。

但能找到实现,不代表它已成为普通扩展的稳定接口。该版本的权限定义里仍写着:

"omnibox.directInput": {
  "channel": "dev",
  "feature_flag": "ExperimentalOmniboxLabs",
  "extension_types": ["extension"]
}

同时,功能定义将 kExperimentalOmniboxLabs 设为默认关闭,调用入口还会同时检查功能是否启用和权限是否授予:

return base::FeatureList::IsEnabled(
           extensions_features::kExperimentalOmniboxLabs) &&
       extension->permissions_data()->HasAPIPermission(
           mojom::APIPermissionID::kOmniboxDirectInput);

当前公开 API 文档也没有把这些字段列入常规接口。因此,面向普通用户分发扩展时,应把正式的关键词模式作为可依赖入口,不能把 unscoped 模式写成已经普遍可用的能力。研究它时,还要考虑隐私差异:无需关键词就接收输入,比只在用户明确进入扩展模式后接收输入,作用范围更大。

企业管理员:统一搜索入口和企业候选

受管理的 Chrome 还有策略层,职责和个人设置、扩展不同。

SiteSearchSettings 可以统一下发站点搜索,字段包含名称、快捷字和使用 {searchTerms} 的 URL,也可以配置精选入口及是否允许用户覆盖。策略里写的快捷字本身不带 @,用户使用时可以通过普通快捷字或 @快捷字 进入。

EnterpriseSearchAggregatorSettings 则接入企业搜索聚合器,让企业内容参与地址栏和新标签页搜索框的建议。它从 Chrome 135 起支持桌面平台与 ChromeOS,当前定义要求 name、shortcut、search_url、suggest_url 等字段。

这条路径不只是静态跳转:浏览器会向 suggest_url 发 POST 请求,查询内容放在 query 参数中;require_shortcut 可以要求用户先进入企业搜索模式,再展示企业建议。若需要控制什么时候向企业服务发送查询,这个字段就直接影响使用边界。

企业搜索服务还要自己实现认证、权限过滤和返回格式。策略提供的是浏览器接入能力,不能让本来无权访问的文档变成可访问。部署后也应通过 chrome://policy 核对策略是否真的生效。

输入还没确认,网络可能已经开始工作

把输入识别放到 DNS 之前,是在补全决策逻辑,并不是说浏览器一定等到回车后才联网。

地址栏里的远程建议本来就需要网络。按照Chrome 的隐私说明,启用「改进搜索建议」时,聚焦或输入地址栏可能向默认搜索引擎发送数据;其他相关设置还可能影响页面 URL 等上下文是否参与建议。

所以,关闭搜索建议与清空下拉列表不是同一件事。关闭远程建议后,本地历史和书签仍然可以提供候选;默认搜索引擎的隐私设置,也不能代替扩展或企业建议服务自己的数据处理约定。

浏览器还可能进行预连接和预渲染。预连接提前准备网络连接;预渲染则尝试在不可见状态下加载和渲染很可能访问的页面,用户确认以后再激活。

当前预渲染文档明确列出了地址栏相关触发场景:根据历史预测即将访问的 URL,以及由搜索引擎建议触发的搜索结果页预渲染。是否实际发生,仍受用户设置、资源情况和启发式判断影响。

这给开头那道面试题补上了第二层意思:浏览器不只是先判断目标,再按固定顺序加载;它还可能在用户确认之前,为若干高概率目标提前做准备。一个页面很快出现,有时是因为加载工作已经提前完成了一部分。

想继续研究,可以从哪里看起

源码阅读和本机观察适合相互配合。源码能解释规则,本机调试页能说明当前配置、历史数据和实验状态下实际生成了什么候选。

用调试页区分分类、候选和目的地

桌面 Chrome 可以手动打开 chrome://omnibox。它的用途是查看地址栏自动补全结果;具体字段和布局会随版本变化。对应实现位于 omnibox_page_handler.cc。

可以从 foo.com、foo、foo/、1.2、localhost:8080 这几组输入开始,分别观察:

  1. 输入类型是 URL、QUERY 还是 UNKNOWN。
  2. 哪个 Provider 提供了每条候选。
  3. 候选类型、相关性和默认项资格分别是什么。
  4. 编辑区填入内容与目标 URL 是否相同。
  5. 异步建议返回后,候选顺序是否变化。

其中 url-what-you-typed、search-what-you-typed 等名称描述的是候选类型,不能和输入分类的 URL、QUERY 互换。一个输入可以同时生成搜索候选与导航候选,这正是调试页值得看的地方。

还可以配合 chrome://predictors 看地址栏对导航的预测,以及 chrome://version 确认本机版本。比较两个环境时,最好记录默认搜索引擎、是否登录、是否无痕、相关建议设置和扩展情况;只比较两个截图,很难知道差异来自规则、历史还是配置。

按问题进入源码

想知道什么 优先阅读的入口
字符串为何被当成网址或搜索 autocomplete_input.cc
省略协议等输入怎样修正 url_fixer.cc
多个来源怎样一起工作 autocomplete_controller.cc
重复候选和默认项怎样处理 autocomplete_result.cc
扩展事件和实验权限怎样接入 omnibox_api.cc

这些入口各自解决不同的问题,不必先把整个浏览器工程读完。先找一个会改变判断的输入,例如 1.2 与 1.2/,沿着分类、候选和目的地往下看,通常更容易理解。

本文选用的上游分类样例和版本来源另存为资料快照,方便对照后续版本;样例记录的是测试预期和源码规则,并非本机运行 Chromium 测试得到的报告。

回到「从输入 URL 到页面展示」:我们敲进地址栏的最初只是文字。浏览器先要为它寻找合适的解释,再组织候选、接受选择,才知道下一步是访问网站、执行搜索,还是完成另一项操作。DNS 要解析哪个主机,正是在这些选择之后才有了明确对象。