让 URL 描述搜索结果

JavaScriptBeginner
立即练习

介绍

Fieldnote 是一份小型周末活动指南。实用的搜索链接应当描述当前视图:关键词、类别、排序和结果页码。朋友打开链接时,应当看到相同的活动,包括两个同名活动各自正确的时间和地点。你将围绕已准备好的响应式网站和小型本地目录建立这些状态之间的关系。

你需要了解基本终端路径、HTML 表单、CSS 布局和焦点,以及 JavaScript 数组、对象、函数、DOM 选择和事件。本实验介绍数组筛选、比较函数、分页边界、URL 和 URLSearchParams。搜索和 URL 解析有很多用途;这里的成果是一个完整、可复现的四值视图。准备好的渲染器会根据你的代码选中的记录创建卡片。不需要后端或框架知识。所有活动均为合成数据。55 分钟是作者估计的时长,包含阅读和普通错误处理。

使用 Workspace 执行命令,使用 Fieldnote 打开入口页。本实验独立初始化。只编辑 /home/labex/project/fieldnote/work/selection.js、state.js 和 url.js;本实验中的 app.js 是准备好的控制器,下一实验才由你实现。保留 catalog.js、view.js、index.html、style.css、独立的 Study 视图和准备好的服务。控制器已经能够提交表单并重绘页面,用 replaceState 更新当前历史条目。这里的浏览器历史行为属于准备环境,暂时不是学习要求。网站标签保持英文。

按稳定顺序选择真实记录

本步骤先打开一个正常的共享视图,再让你的作品页面具备可预测的关键词、类别和排序行为。

在 Fieldnote 中选择 Open Study view in a new page。这个真实链接会在产品内嵌 Tab 之外打开独立浏览器页面。网站操作都在这个新页面中完成。地址包含 q、category、sort 和 page;关键词为 print & fold,类别为 Make,页面显示 Page 2 of 2,包含 Print & Fold: map edition 和 Print & Fold。后者是 North room 中 14:00 的活动,而不是同名的较早活动。选择结果下方的 Open this view in a new page。新页面应显示相同的控件、页码、标题、日期和地点。链接只在本实验及其代理路由运行期间有效。这是独立示例,不是你的作品,也不是评分结果。

下方独立 Study 视图展示了第二页的两条记录;除了标题,还要比较日期、时间和地点。

独立 Study 视图中的第二页记录

关闭额外打开的示例页面。在 Fieldnote 入口页选择 Open Work view in a new page。保留这个独立作品页面。编辑文件时切回产品的 Workspace Tab;切换 Tab 与选择网站链接或按浏览器 Back 按钮不同。即使 Study 视图正常,你的作品仍以未完成状态开始。

cd 改变此终端的目录;绝对路径以 / 开头。pwd 输出当前目录。cat 输出小型文本文件的内容,不修改文件:

cd /home/labex/project/fieldnote/work
pwd
cat catalog.js

阅读三条记录:FN-101 和 FN-102 都在 2026-11-14 / 10:00 开始;FN-108 与 FN-102 标题相同,但日期更晚。目录的输入顺序故意与显示顺序不同。预测关键词为 print & fold、类别为 make、按最早时间排序时的前三条匹配记录:FN-101、FN-102、FN-104。比较实际记录的身份,不要只数卡片。

我们将在不改变目录的情况下筛选。filter 创建由匹配对象组成的新数组。includes 检查字面子串;toLowerCase 让搜索不区分大小写。搜索对象是单个连接字符串 title + ' ' + description,因此匹配可以跨过中间连接的空格。&& 表示两个条件都成立;|| 表示任一条件成立即可。all 接受所有类别。箭头函数接收每个活动。

sort 会修改它排序的数组;对新的筛选数组排序,就能保留准备好的目录。比较函数返回负数、零或正数。下面的辅助函数使用 JavaScript 字符串的 < 和 > 比较。最早时间排序先比较 date + 'T' + time,相同时再比较 ASCII 活动 ID。标题排序先比较小写标题,再比较 ID。这种固定文本顺序不依赖浏览器区域设置或原数组位置。准备好的日期采用同一时区;你不需要实现跨时区排期。

条件表达式 condition ? first : second 选择一个值。这里的 primary || ... 只在主比较结果为零时使用 ID 比较。CATALOG 是在你的文件之前加载的准备对象。state 是包含 q、category、sort 和 page 的对象;下一步会让它的值安全且一致。

将完整函数保存到 selection.js。cat > filename 会替换该文件。带引号的 <<'JS' 开始一个 here-document:粘贴后续行,直到单独一行的结束标记 JS。给标记加引号可以原样保留 JavaScript 标点。这是在终端中编辑并保存文件,不是在自动操作 WebIDE 编辑器。

cat > selection.js <<'JS'
function compare(a, b) {
  return a < b ? -1 : a > b ? 1 : 0;
}
function matches(state) {
  return CATALOG.events.filter(event =>
    (state.category === 'all' || event.category === state.category) &&
    (event.title + ' ' + event.description).toLowerCase()
      .includes(state.q.toLowerCase())
  ).sort((a, b) => {
    const primary = state.sort === 'title'
      ? compare(a.title.toLowerCase(), b.title.toLowerCase())
      : compare(a.date + 'T' + a.time, b.date + 'T' + b.time);
    return primary || compare(a.id, b.id);
  });
}
JS

返回独立作品页面并刷新。等待控件和卡片出现。在 Keyword 输入 print & fold,在 Category 选择 Make,保持 Soonest first,然后激活 Search。应看到预测的三条记录;两个 10:00 活动通过标题和地点区分。将 Sort by 改为 Title A–Z,再次提交,应依次看到 Print & Fold、Print & Fold、Print & Fold: colour。阅读日期:两个同名活动依次为 FN-102、FN-108。只改变选择框是在编辑草稿;Search 才会一起提交三个筛选值。共享 URL 此时还不完整。

如果结果为空,先检查关键词和类别。像 prnit 这样的拼写错误确实没有匹配;改为 print & fold 后再次提交。如果编辑后页面不再响应,返回 Workspace,运行 cat selection.js,将花括号和引号与完整代码比较,再次替换文件。保存后刷新作品页面;不需要重置 VM。

观察两种排序后,使用本步骤的 Check。它会在一次性浏览器 fixture 中执行你的原始前端文件,并检查完整记录关系;不会补全现场页面,也不会把 Study 视图当作你的成果。

规范化筛选值和分页边界

本步骤把草稿或传入值转为可预测的统一状态,包括空结果和最后一个可用页面。

留在独立作品页面。使用 print & fold、Make、Soonest first,提交后选择 Next。共有五条匹配,每页三个位置,因此第二页包含最后两条。每页三条是准备数据 CATALOG.pageSize;渲染器用数组的 slice(start, end) 获取一页,不改变完整列表。索引从零开始,结束位置不包含在结果中:第一页使用 slice(0, 3),第二页使用 slice(3, 6)。每张卡片使用选中的活动对象,包括其自身的 ID、标题、描述、日期、时间和地点。

现在考虑传入页码 999、未知类别,或两端带空格的关键词。规范化函数会在渲染之前赋予这些值公开、明确的含义。缺少关键词时使用空字符串;只去掉两端空白,保留内部空格和用户输入的大小写。匹配仍然不区分大小写。未知或缺少类别时使用 all;未知或缺少排序时使用 soon。使用 Number 转换页码:允许正的安全整数,包括等价的 2.0。无效值、零、负数、非整数或不安全整数变为 1。有效但过大的页码限制到最后一个匹配页。零结果仍使用第 1 页、共 1 页,并禁用两个分页按钮。

Number.isSafeInteger 避免使用不能精确表示的整数值。Math.ceil 向上取整:五条匹配除以三,得到两页。Math.max(1, ...) 将空结果的页数保持为一;Math.min 限制请求的页码。Array.includes 还用于检查允许的类别和排序列表。|| 默认值处理缺少或为空的输入。规范化状态存放的是值,而不是已经编码的 URL 文本。

切换到 Workspace,目录仍为 /home/labex/project/fieldnote/work。用下面的完整函数替换 state.js:

cat > state.js <<'JS'
function normalize(raw) {
  const state = {
    q: (raw.q || '').trim(),
    category: CATALOG.categories.includes(raw.category) ? raw.category : 'all',
    sort: ['soon', 'title'].includes(raw.sort) ? raw.sort : 'soon',
    page: Number(raw.page || 1)
  };
  if (!Number.isSafeInteger(state.page) || state.page < 1) state.page = 1;
  const pages = Math.max(1, Math.ceil(matches(state).length / CATALOG.pageSize));
  state.page = Math.min(state.page, pages);
  return state;
}
JS

这里调用你的 matches 函数,在筛选后计算边界。它不会对原始目录排序。预测 q: ' print & fold '、category: 'make'、sort: 'soon'、page: '999' 对应的状态:关键词两端空格去掉,类别和排序不变,页码为 2。如果是 page: '2.5',页码则为 1。下一步有了 URL 读取器之后,你会实际测试传入的 URL 值。

返回独立作品页面并刷新。在 Keyword 先输入两个空格,再输入 print & fold,最后再输入两个空格。选择 Make,激活 Search。提交后控件应包含不带两端空格的 print & fold。选择 Next:只剩地图版本和 14:00 的 North room 活动,Next 被禁用。从第二页搜索 sound + light、Listen、Title A–Z。这会提交新的筛选条件,并将页码重置为 Page 1 of 1。字面加号是标题的一部分,不是空格。搜索 no such constellation:没有卡片,有可读的无结果消息,页码为 1/1,两个按钮都禁用。这是正确结果。恢复匹配的关键词和类别,再次提交即可恢复。

在 Check 之前,确认原始目录和所有记录内容仍然完整。未知值和数字 URL 写法的检查会在下一步加入;本步骤检查规范化表单和可到达的分页边界。

读取和写入真实视图 URL

本步骤让直接打开的链接恢复四个已提交的值及其可见记录。

URL 包含来源、路径、查询和可选的 # 后片段。LabEx 代理提供实际来源和路径;不要用来源再拼接 / 来猜测新基址。new URL(location.href) 从本页面的真实地址开始。它的 searchParams 用 get 读取已解码的查询值,用 set 写入普通、未编码的值。它会编码关键词中的 & 等分隔符,并将字面加号编码为 %2B。空格可以序列化成 + 或 %20。在 set 之前调用 encodeURIComponent 会造成双重编码;应传入原始值。

本课程只管理 q、category、sort 和 page。保留所有额外查询参数和现有片段;它们不属于筛选状态,可能属于平台。缺少值或默认值可以省略,也可以显式写出。参数顺序、等价数字页码和两种空格写法不会改变含义。初始化可以规范化 URL 而不创建新访问。本实验由准备好的控制器执行替换;下一实验才教授历史 API 调用。

下面的 for...of 循环遍历四个已知键字符串。delete 在写入器设置非默认值之前,删除每个旧的自有查询值。

在 Workspace 中,将读取器和写入器保存到 url.js:

cat > url.js <<'JS'
function readURL() {
  const params = new URL(location.href).searchParams;
  return normalize({q: params.get('q'), category: params.get('category'),
    sort: params.get('sort'), page: params.get('page')});
}
function stateURL(state) {
  const url = new URL(location.href);
  for (const key of ['q', 'category', 'sort', 'page']) url.searchParams.delete(key);
  if (state.q) url.searchParams.set('q', state.q);
  if (state.category !== 'all') url.searchParams.set('category', state.category);
  if (state.sort !== 'soon') url.searchParams.set('sort', state.sort);
  if (state.page !== 1) url.searchParams.set('page', state.page);
  return url;
}
JS

先删除这些键,就能在选择默认值时移除陈旧筛选,同时保留未知键。条件行只省略我们的默认值。写入器返回 URL 对象,本身不导航页面。准备好的控制器先改变地址,然后渲染器再更新真实共享字段和链接。

返回独立作品页面并刷新。搜索 print & fold、Make、Title A–Z,然后选择 Next。阅读共享字段:它表示了当前全部四个值,包括 sort=title 和 page=2。选择 Open this view in a new page,刷新新页面。应依次看到 Print & Fold: map edition、Print & Fold: tiny type,即标题排序中的最后两条记录。检查它们的日期和地点,不要只数数量。这是真正指向作品文件的链接。独立 Check 也会在干净浏览器上下文中打开它,不依赖已缓存的本地偏好。

使用独立页面的地址栏测试这些传入值。保持真实来源和 /work/ 路径,只替换 ? 后的查询。在查询后添加 #directory-title,测试片段保留。先使用以下查询:

?edition=november&platform=keep-me&q=print%20%26%20fold&category=make&sort=soon&page=999#directory-title

页面应显示第二页的地图版本和 North room 活动,并选中 Soonest first。规范化地址可以省略 sort=soon,也可以用 + 表示空格;edition、platform 和片段保持不变。现在将查询替换为:

?edition=november&category=unknown&sort=unknown&page=2.5#directory-title

应看到空关键词、All categories、Soonest first、Page 1 of 4,以及前三条记录 FN-101、FN-102、FN-105。未知值会使用默认值,不是需要重置的错误。最后,在第一个测试的关键词和类别下,使用 page=2.0 的查询与第二页含义相同。参数顺序和默认值写法可以灵活变化,但准确的可见状态和完整记录不能改变。

保留独立作品页面以便阅读。在接近 768 像素、然后 390 像素的宽度下,所有筛选控件、分页控件、共享链接和完整记录文本都应可用,不出现水平滚动。按 Tab 观察可见焦点。页面外壳和这些样式已经准备好;保留它们。运行 Check。本实验验证直接 URL 恢复、当前 URL/控件/结果一致性和安全边界。它暂不要求 Back/Forward 恢复:准备好的控制器会替换当前条目,这正是下一实验的起点。

总结

你使用稳定的同值排序规则选择了完整活动记录,规范化了筛选值与分页边界,并在真实视图 URL 中读写四个值。等价 URL 写法能够复现相同视图,不依赖原始目录顺序。下一实验将替换准备好的控制器,让真正的浏览器 Back 和 Forward 也恢复这些视图。