通过浏览器导航恢复搜索

JavaScriptBeginner
立即练习

介绍

Fieldnote 的关键词、类别、排序和页码已经能随 URL 传递。准备好的控制器会在每次搜索后替换当前历史条目。这样直接链接仍然有用,却丢失了前一个视图:读者无法浏览多个搜索后,再可靠地用 Back 返回。你将区分新的用户操作与恢复操作,让浏览器自身的历史真正发挥作用。

本独立实验准备了上一实验已完成的选择、规范化和 URL 文件,不需要之前的 VM。你需要理解上一实验的四状态契约,以及基本 JavaScript 函数、对象、DOM 选择和事件。你将学习 pushState、replaceState、popstate,以及为什么共享需要 URL,而不是旧的本地偏好或内存历史对象。这里没有异步请求或框架路由器。40 分钟是作者估计的时长。

使用 Workspace 和 Fieldnote。只编辑 /home/labex/project/fieldnote/work/app.js。保留 selection.js、state.js、url.js、catalog.js、view.js、HTML/CSS、独立 Study 视图和服务。渲染器已经准备好:它显示每个选中活动自身的标题、描述、日期、时间和地点。网站保持英文。链接只在本 VM 的公开路由可用期间能打开;这不是向互联网发布网站。

为已提交的用户更改创建访问记录

本步骤将初始化保持在一个历史条目中,并在读者提交不同搜索或页码时创建新访问。

在 Fieldnote 中选择 Open Study view in a new page。这是独立浏览器页面,不是内嵌的产品 Tab。阅读其 print & fold、Make、Soonest first 的第二页结果。选择 Previous,再把 Keyword 改为 sound + light,Category 改为 Listen,Sort by 改为 Title A–Z,激活 Search。页码重置为 1。使用浏览器的 Back 按钮,而不是网站的 Previous 分页按钮:它会恢复先前的关键词、类别、排序、页码以及完整的匹配活动记录。用浏览器 Forward 恢复较新的视图。这是正常示例,不会完成你的作品。

这是编辑前的独立 Study 视图。其共享 href 描述了相同的可见页面,不会完成 Work。

独立的十一月 Study 视图

关闭 Study 页面,从 Fieldnote 入口选择 Open Work view in a new page。作品文件已经包含全部 G1 函数,但准备好的控制器仍然只替换条目。切换到 Workspace。cd 设置此持久终端的目录;cat 读取文件,不修改它:

cd /home/labex/project/fieldnote/work
cat app.js

当前的 show 函数调用 normalize,用 stateURL 创建 URL,替换地址,并让 draw 显示选中的记录。这些辅助函数来自准备好的 state.js、url.js、view.js 和 selection.js。你将保留这个顺序,同时区分显示视图的两种原因。

history.pushState(null, '', url) 添加一个同源历史条目并改变地址,不请求新页面。replaceState 只更新当前条目。第一个参数是可选的关联状态;我们使用 null,因为全部必需状态已经在 URL 中。空字符串是该 API 未使用的标题参数。两种调用本身都不会触发 popstate。下一步会处理真实的历史遍历。初始规范化使用 replace,因此打开链接时不会额外添加历史条目。当提交的 URL 不同时,搜索和分页按钮使用 push;提交未改变的视图不必增加重复条目。

document.querySelector('form') 查找准备好的表单。form.elements.q 访问名为 q 的控件。preventDefault 阻止浏览器默认的表单导航,使控制器能够更新视图。事件监听器会在指定事件发生时运行。展开语法 {...current, page: ...} 复制当前状态的字段,再替换它的页码,从而保留已提交的筛选。单独编辑控件仍是草稿,直到激活 Search;分页按钮使用已提交状态。

保存下面的完整替换内容。cat > app.js 只替换该文件。带引号的 <<'JS' here-document 保留其内容原样;以单独一行的 JS 结束:

cat > app.js <<'JS'
const form = document.querySelector('form');
let current;
function show(raw, mode) {
  current = normalize(raw);
  const url = stateURL(current);
  if (mode === 'push' && url.href !== location.href) {
    history.pushState(null, '', url);
  } else if (mode === 'replace') {
    history.replaceState(null, '', url);
  }
  draw(current, matches(current));
}
form.addEventListener('submit', event => {
  event.preventDefault();
  show({q: form.elements.q.value, category: form.elements.category.value,
    sort: form.elements.sort.value, page: 1}, 'push');
});
document.querySelector('#previous').addEventListener('click', () =>
  show({...current, page: current.page - 1}, 'push'));
document.querySelector('#next').addEventListener('click', () =>
  show({...current, page: current.page + 1}, 'push'));
show(readURL(), 'replace');
document.documentElement.dataset.ready = 'true';
JS

&& 要求两个条件都成立,因此只有请求 push 且地址不同时才创建访问。else if 为初始化选择替换。准备文件仍然负责规范化,并且只管理本课程的四个查询键;真实代理路径、额外查询参数和片段保持不变。末尾的 data-ready 标记让浏览器知道控制器已经完成初次绘制;不要把 HTML 控件出现误当作 JavaScript 已经执行完成。

返回独立作品页面并刷新。搜索 print & fold、Make、Soonest first,然后选择 Next。留意第二页的地图版本和 North room 活动。改为 sound + light、Listen、Title A–Z 并提交:第一页显示两个不同的声音活动。激活 Open this view in a new page,再刷新新页面。它必须保留 Title A–Z 以及正确的日期和地点。这里不需要缓存偏好;四个已提交值都属于共享 URL,包括非默认排序。关闭新页面,保留原作品页面。

暂时不要期待 Back 能重绘作品页面:新的历史条目已经存在,但恢复监听器仍然缺失。运行本步骤的 Check。它会验证直接 URL 行为、有效的新访问和初始化替换,不要求下一步的监听器。如果 JavaScript 拼写错误导致更新停止,使用 cat app.js,比较完整代码,替换后刷新。保存修正文件不会清空准备目录,也不需要重置 VM。

用真正的 Back 和 Forward 恢复视图

本步骤在真实历史导航后读取 URL,并证明另一个浏览器上下文可以重建相同视图。

Back 和 Forward 改变当前历史条目。浏览器遍历适用的同文档条目时会发出 popstate;你自己调用 pushState 不会发出它。监听真实事件,读取当前 URL,规范化后绘制视图。不要从该监听器 push,否则会创建新的前进访问,而不是恢复现有条目。即使没有内存中的 event.state,例如直接打开链接时,URL 也应正常工作。不要把 localStorage 当作权威来源,也不要只把排序存入历史对象。

切换到 Workspace。向上一步保存的文件追加一个监听器。与 > 不同,>> 会追加内容,不删除已有内容。此代码块运行一次即可;如果误追加了两次,用上一步完整代码替换 app.js,再追加一次。

cat >> app.js <<'JS'
window.addEventListener('popstate', () => show(readURL(), 'replace'));
JS

箭头回调在原生历史遍历时运行。它复用同一个 show 路径,因此控件、记录顺序、页码标签、禁用边界和实际共享 href 都与新当前 URL 一致。这里的替换可以规范化等价输入,但不得添加历史条目。学员流程没有模拟派发事件,也没有不可见的测试捷径。

返回原来的独立作品页面。为得到清楚的初始条目,在其地址栏打开真实 /work/ URL,使用以下查询并保留真实来源和路径:

?edition=november&q=print%20%26%20fold&category=make&sort=title&page=2#directory-title

这是状态 A:Title A–Z、第二页,依次为 Print & Fold: map edition、Print & Fold: tiny type。阅读它们的时间和地点。选择 Previous 创建状态 B:第一页包含两个普通 Print & Fold 记录,然后是 Print & Fold: colour。提交 sound + light、Listen、Soonest first 创建状态 C:第 1 页、共 1 页,包含两个不同日期。这些操作必须保留 edition 和片段。改变筛选会重置页码。

使用真实浏览器 Back 两次:C → B → A。每到一个条目,比较全部控件、页码、排序后的标题,以及每个活动的日期、时间和地点。再用 Forward 两次:A → B → C。将这个 Back/Back/Forward/Forward 循环再重复一次;恢复不能悄悄添加访问,也不能丢失前进链。产品 Tab 切换不是历史遍历,网站的 Previous 是结果分页操作,而不是浏览器 Back。

在 C 激活真实的 Open this view in a new page 链接。新页面必须显示相同的已提交状态。刷新后再次比较。检查器还会在没有旧 localStorage 或历史状态的干净浏览器上下文中打开准确的生成链接。在普通浏览器中,新页面是单独的标签页,并不代表所有浏览器存储都被隔离。不需要剪贴板 API,也不需要未经测试的 Copied 消息。

返回原作品页面,Back 到 B 并刷新。仍应显示 B,因为刷新会读取 URL。改变但未提交的关键词是草稿,刷新时会丢弃。明确测试一次:输入 unsubmitted draft,不激活 Search,刷新后观察 B 的已提交关键词恢复。普通错误测试可提交 prnit,阅读有效的无结果消息,然后修正为 print & fold、Make 并提交。空结果不能出现假的兜底卡片。

在桌面宽度、接近 768 像素和接近 390 像素时,比较完整卡片、控件和共享链接。使用 Tab 和 Enter 执行一次 Search,检查可见焦点;保留准备好的响应式外壳。运行 Check。它会在隔离 fixture 中执行你未改变的前端,测试真实 Back/Forward 和干净上下文打开,将每个可见记录与独立目录比较,并保留现场工作区。它不会创建账户,也不会发布永久网站。

总结

你把新的已提交操作与初始或历史恢复区分开来。真实 Back/Forward、直接共享链接和刷新现在都使用同一个由 URL 驱动的状态及完整活动记录。挑战将把这种行为迁移到另一个目录和每页数量,不增加路由器、后端分页或私有存储依赖。