加入收藏代码怎么写:用 JavaScript 实现网页收藏功能

加入收藏代码怎么写:用 JavaScript 实现网页收藏功能
网页上的“加入收藏”通常指用户收藏网站内的文章、商品或其他内容,以便稍后在本站查看。它看起来像一个按钮,背后却涉及状态保存、页面更新、数据校验和存储失败处理。对于刚接触前端开发的人来说,最重要的一步是先区分两种常被混为一谈的“收藏”:网站内收藏和浏览器书签。
网站内收藏由网站应用自己管理,常见实现方式包括浏览器本地存储和服务端数据库。用户收藏一篇文章后,网站可以在“我的收藏”页面列出它。本教程先用 JavaScript 的 localStorage 完成一个不依赖后端的示例,适合原型、个人工具和单浏览器体验;随后说明它的限制,以及真实项目如何升级。
一、先明确:网站收藏不等于浏览器书签
浏览器书签属于浏览器自身。网页脚本不能在所有主流浏览器中可靠地静默添加书签,也没有一个通用的 JavaScript API 能让网站直接修改用户的书签栏。用户要收藏整个网页,可以使用浏览器菜单或快捷键;网站能做的是提供清楚的提示,例如“按 Ctrl+D(macOS 通常为 Command+D)将本页加入浏览器书签”。快捷键可能因浏览器、系统和用户设置而异,不应假设每个用户都使用相同组合。
如果收藏的是网站内的商品、文章或视频,就应实现应用自己的收藏功能。它能提供取消收藏、收藏列表、登录后跨设备同步等产品能力,也能配合服务端权限与数据管理。不要把这类按钮描述成“替用户写入浏览器书签”。
二、什么场景适合用 localStorage
localStorage 会按来源(origin,即协议、主机名和端口的组合)存储字符串。相同来源下的页面通常可以读取这些数据,浏览器会在页面重新加载后保留它们。它适合以下场景:
- 制作不需要账号的个人收藏清单或交互原型。
- 希望用户刷新页面后仍能看到本浏览器中的收藏状态。
- 存放不敏感、体积较小、可以丢失或重建的数据。
- 为已经从服务器获取到的内容提供本地快速标记。
它不适合被当作用户账户数据库。数据保存在当前浏览器的本地环境中,清除站点数据、使用隐私浏览模式、换浏览器、换设备或更换来源,都可能导致用户看不到原有收藏。浏览器对本地存储的可用容量和管理方式也会因环境而异;不能把某个固定容量当作所有浏览器都保证的额度。存储可能被禁用,也可能因为配额等原因写入失败,所以代码应处理异常。
更重要的是,localStorage 内容并不可信。用户可以通过开发者工具修改它,页面上的其他同源脚本也可能读取它。不要在其中保存密码、访问令牌、支付信息或其他敏感数据,也不要把其中的值直接拼进 HTML 并按 HTML 执行。
三、实现思路与数据结构
示例使用固定的内容列表,每条内容有唯一的字符串 id。收藏状态只保存 ID,例如:
["article-1", "article-3"]
与保存整篇文章相比,只保存 ID 通常更简单:标题和描述仍以页面数据为准,收藏列表负责记录用户选择了哪些内容。实际应用中,如果收藏对象来自服务端,服务端仍应验证 ID 是否存在、用户是否有权访问,并决定返回什么内容。
下面的完整示例可以保存为 favorites.html,用浏览器直接打开运行。点击按钮后,收藏状态会写入 localStorage;刷新页面后会读取并恢复状态。页面还提供“只看收藏”筛选,并使用 textContent 显示文本,避免把数据当作 HTML 插入。
四、可直接运行的完整示例
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>网页收藏示例</title>
<style>
body {
max-width: 760px;
margin: 40px auto;
padding: 0 16px;
font: 16px/1.6 system-ui, sans-serif;
color: #222;
}
.toolbar { margin: 20px 0; }
.item {
display: flex;
justify-content: space-between;
gap: 20px;
align-items: center;
padding: 16px 0;
border-top: 1px solid #ddd;
}
.item h2 { margin: 0 0 6px; font-size: 1.1rem; }
.item p { margin: 0; color: #555; }
button { padding: 8px 12px; cursor: pointer; }
#message { min-height: 1.5em; color: #8a2d00; }
</style>
</head>
<body>
<h1>文章列表</h1>
<p>收藏只保存在当前浏览器中,不会同步到其他设备。</p>
<div class="toolbar">
<label>
<input id="only-favorites" type="checkbox">
只看收藏
</label>
</div>
<p id="message" role="status" aria-live="polite"></p>
<ul id="items" aria-label="文章列表"></ul>
<script>
"use strict";
const STORAGE_KEY = "demo-favorites-v1";
const articles = [
{
id: "article-1",
title: "JavaScript 入门",
description: "了解变量、函数和浏览器中的脚本。"
},
{
id: "article-2",
title: "CSS 布局基础",
description: "认识常见的网页布局方式。"
},
{
id: "article-3",
title: "Web 存储简介",
description: "比较 localStorage 与 sessionStorage。"
}
];
const articleIds = new Set(articles.map(article => article.id));
const list = document.querySelector("#items");
const filter = document.querySelector("#only-favorites");
const message = document.querySelector("#message");
function readFavorites() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw === null) return new Set();
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return new Set();
// 只接受当前内容列表中存在的字符串 ID。
return new Set(
parsed.filter(id => typeof id === "string" && articleIds.has(id))
);
} catch (error) {
// 读取可能因存储不可用或 JSON 损坏而失败。
return new Set();
}
}
let favorites = readFavorites();
function saveFavorites() {
try {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify([...favorites])
);
message.textContent = "";
return true;
} catch (error) {
message.textContent =
"无法保存收藏:浏览器存储可能不可用或空间不足。";
return false;
}
}
function render() {
list.replaceChildren();
const visibleArticles = articles.filter(article =>
!filter.checked || favorites.has(article.id)
);
if (visibleArticles.length === 0) {
const emptyItem = document.createElement("li");
emptyItem.textContent = "当前没有收藏内容。";
list.append(emptyItem);
return;
}
for (const article of visibleArticles) {
const item = document.createElement("li");
item.className = "item";
const text = document.createElement("div");
const title = document.createElement("h2");
const description = document.createElement("p");
// 使用 textContent,而不是把数据拼接到 innerHTML。
title.textContent = article.title;
description.textContent = article.description;
text.append(title, description);
const button = document.createElement("button");
const isFavorite = favorites.has(article.id);
button.type = "button";
button.dataset.id = article.id;
button.setAttribute("aria-pressed", String(isFavorite));
button.textContent = isFavorite ? "取消收藏" : "加入收藏";
item.append(text, button);
list.append(item);
}
}
list.addEventListener("click", event => {
const button = event.target.closest("button[data-id]");
if (!button || !list.contains(button)) return;
const id = button.dataset.id;
if (!articleIds.has(id)) return;
const nextFavorites = new Set(favorites);
if (nextFavorites.has(id)) {
nextFavorites.delete(id);
} else {
nextFavorites.add(id);
}
// 写入成功后更新当前页面状态;失败时保留原状态。
const previousFavorites = favorites;
favorites = nextFavorites;
if (!saveFavorites()) {
favorites = previousFavorites;
}
render();
});
filter.addEventListener("change", render);
// 同一来源的其他标签页修改该键时,同步更新本页显示。
window.addEventListener("storage", event => {
if (event.key === STORAGE_KEY || event.key === null) {
favorites = readFavorites();
render();
}
});
render();
</script>
</body>
</html>
五、逐段理解关键代码
1. 用稳定 ID 表示收藏对象
articles 中的 id 用于识别内容,不能依赖标题作为主键。标题可能被修改,也可能重复;稳定 ID 更适合保存和查询。示例中的 ID 由代码预先定义,服务端应用则通常由数据库或 API 提供。
articleIds 是一个 Set,用于检查 ID 是否属于当前列表。读取本地数据时,程序先检查 JSON 是否是数组,再只保留字符串且存在于当前列表的 ID。这个校验能避免格式错误的数据影响渲染,但它不是身份验证或权限控制。前端校验不能替代服务端校验。
2. 读取时处理异常与旧数据
localStorage.getItem() 返回字符串或 null,所以要先判断是否没有保存过数据,再调用 JSON.parse()。JSON 可能因用户手动修改、旧版本写入错误或其他原因而无法解析;存储访问本身也可能失败。示例在 try...catch 中读取,并在异常时返回空集合,避免页面因此无法启动。
实际产品可以进一步决定如何处理损坏数据:记录诊断信息、提示用户,或在确认后删除无效数据。不要在每次读取失败时无条件清空所有站点存储;同一个来源下可能还有其他功能的数据。需要清理时,只操作自己的键,例如 localStorage.removeItem(STORAGE_KEY)。
3. 写入时检查结果
localStorage.setItem() 接收键和值,值会以字符串形式保存,因此示例使用 JSON.stringify() 序列化数组。写入可能失败,代码通过 saveFavorites() 返回成功或失败,并给用户显示提示。点击后先构造新的集合,只有写入成功才保留新状态;这样可以避免界面显示“已收藏”,刷新后却发现写入失败。
存储是否可用不能只靠检查某个属性是否存在来判断。即使浏览器提供了 localStorage,当前环境仍可能不允许访问或写入。对小型收藏数据,直接在实际读写操作周围处理异常更可靠。
4. 用 DOM API 渲染,而不是拼接 HTML
示例通过 document.createElement() 建立元素,并把标题和描述赋给 textContent。这会将内容作为普通文本显示。若把不可信数据直接拼入 innerHTML,其中包含的标签或事件属性可能被浏览器解释,造成跨站脚本风险。收藏功能本身不是安全边界;无论数据来自本地存储、接口还是用户输入,都应按数据来源和使用位置正确处理。
本示例的文章数据写在源代码中,并非用户输入,但仍采用安全的文本渲染方式,便于之后改成 API 数据时不留下危险的拼接习惯。需要插入富文本时,应使用经过审查的 HTML 清理方案,并限制允许的标签和属性,而不是把原始字符串直接交给 innerHTML。
5. 用事件委托处理按钮
列表是每次渲染时重新生成的,因此按钮也会被替换。示例只在列表容器上注册一个点击监听器,通过 closest() 找到被点击的收藏按钮,再读取其 data-id。这种方式称为事件委托,适合动态生成的列表,避免每次渲染都给每个按钮重复注册事件。
处理点击时仍检查 ID 是否存在于允许的内容列表中。HTML 的 data-id 属性只是页面数据,不应被当作可信凭证。若内容 ID 来自 URL、存储或服务端响应,也应根据业务规则进行校验。
6. 提供清楚的可访问状态
按钮使用原生 <button>,键盘用户可以通过 Tab 聚焦,并用 Enter 或空格触发。aria-pressed 表示按钮当前是否处于按下状态,屏幕阅读器可以据此理解收藏状态;按钮文案也随状态变化。状态提示使用 aria-live="polite",让保存失败信息有机会被辅助技术读出。
如果按钮采用“收藏/已收藏”切换,建议保持语义一致:可以使用稳定的名称配合 aria-pressed,也可以像示例这样更新文案并同步状态属性。不要只用颜色或星形图标传达状态;图标按钮还需要可访问名称,例如合适的 aria-label。
六、跨标签页、跨设备与登录用户
浏览器的 storage 事件可以通知同一来源下其他文档的存储变化。示例监听了指定键的变化,并重新读取数据。需要注意:调用 setItem() 的当前页面不会收到由自己这次写入触发的 storage 事件,所以当前页面仍要主动更新界面。事件也不会让数据自动跨浏览器、跨设备或跨来源同步。
如果用户登录后希望在手机和电脑间同步收藏,通常应把数据保存到服务端。典型流程是:前端向 API 发送收藏或取消收藏请求;服务端从可信的登录会话中识别用户;服务端校验内容 ID、执行权限检查并持久化;前端根据响应更新状态。不要仅凭客户端传来的用户 ID 就允许修改他人的收藏,也不要把本地存储当作服务端身份凭证。
服务端方案还要考虑并发更新、网络错误、重复请求和离线行为。接口可以将“设置为已收藏”设计成幂等操作,避免重复点击或重试造成错误;也可以在前端提供乐观更新,但失败时应回滚并清楚提示。使用 Cookie 会话时,还需依据应用的认证方式处理跨站请求伪造等安全问题;具体防护应结合后端框架和部署配置,而不是靠收藏按钮的 JavaScript 单独解决。
七、常见错误与修正方式
- 误以为 JavaScript 可以静默修改浏览器书签。 通用网页 API 不提供这种能力。应用内收藏应由网站保存;浏览器书签应交由用户通过浏览器功能添加。
- 直接写入对象却忘记序列化。 Web Storage 保存的是字符串。读取后要解析 JSON,并处理无效 JSON;写入时要调用
JSON.stringify()。 - 把标题当作唯一标识。 标题可修改且可能重复。使用稳定 ID,并在业务层明确 ID 的生成规则。
- 把收藏状态只存在内存中。 刷新后状态会消失。需要在初始化时读取持久化数据,并在每次状态变化时保存。
- 不处理存储读写失败。 浏览器策略、隐私环境或容量限制都可能让操作失败。应捕获异常,并让界面如实反馈。
- 把存储容量说成固定且普遍适用的值。 实际配额受浏览器、设备、存储策略和可用空间影响。收藏数据应保持精简,并准备失败处理。
- 相信 localStorage 中的数据。 用户能够修改它。读取后校验类型和 ID;涉及访问权限、价格或账户数据时,必须以服务端校验为准。
- 把本地内容拼进
innerHTML。 这会增加脚本注入风险。显示普通文本时优先使用textContent和 DOM API。 - 把本地收藏说成云端同步。
localStorage只提供当前来源下的浏览器本地存储,不会自动同步账号或其他设备。需要同步时应设计服务端接口。 - 只改变按钮颜色,不更新语义。 同步更新文案或
aria-pressed,并让键盘和辅助技术用户能辨认状态。
八、从示例升级到实际项目
若收藏列表会出现在单独页面,可以把示例中的内容渲染逻辑抽成函数,提供一个“全部内容”视图和一个“我的收藏”视图。列表为空时显示有帮助的空状态;内容已下架或无权访问时,也要定义明确的处理方式。保存 ID 并不意味着对应内容永远存在。
若页面包含分页或搜索,要明确筛选范围:是只过滤当前已加载的内容,还是查询完整的收藏列表。收藏状态可能从接口分页返回,前端就不能假定当前页面数据包含所有已收藏对象。数据量较大时,可由服务端提供收藏列表查询接口,而不是把所有记录都塞进浏览器存储。
若同一用户会在多个标签页操作,storage 事件对小型本地实现已经够用,但它不是事务机制。多个页面同时更新时,基于“先读、再改、再写”的数组可能发生覆盖。对要求严格的账户数据,应使用服务端持久化和明确的冲突处理策略。
部署时还要留意来源变化:从 http 切换到 https、更改域名或端口后,浏览器会把它们视为不同来源,原有本地数据不会自动迁移。开发环境和正式环境也可能使用不同来源。若要迁移,必须设计安全、可验证的数据迁移流程,不能假设读取旧来源数据总是可行。
九、总结
实现一个基础的网页收藏功能,可以按以下步骤进行:为内容定义稳定 ID;从本地存储读取并校验收藏 ID;用户操作时更新集合并序列化写回;用安全的 DOM API 更新界面;对存储异常和空列表给出明确反馈。
这套 localStorage 方案适合轻量、本地、非敏感的收藏体验,但不提供账号同步、可靠备份或权限控制,也无法代替浏览器书签。面向正式产品时,应根据需求选择服务端存储,并把用户身份、访问权限和数据校验放在服务端处理。清楚划分这些边界,收藏按钮才不仅“能点”,也能在刷新、异常和真实用户场景下保持可预期。









