
js文字特效实战:给网页标题添加滚动动画
浏览器标签页上的标题很容易被忽略,但在后台任务、长时间运行的操作或多标签工作场景里,适度变化的标题可以提示用户某项状态,例如“正在生成”“有新消息”或“任务已完成”。本文从一个简单的标题滚动效果开始,逐步处理停止与恢复、页面可见性、用户偏好和标题还原等问题。
这里的“网页标题”特指浏览器标签页、书签和部分历史记录中使用的 document.title,不是页面正文里的 <h1>。动画只改变浏览器标题文本,不会让页面中的标题元素滚动。若需要页面内的文字特效,应使用 DOM 元素与 CSS 动画,不能把两种做法混为一谈。
适合在哪些场景使用
标题滚动适合承载短小、低频、辅助性的提示,比如:
- 任务完成后,在标签页标题中短暂显示“已完成”,帮助用户从其他标签页中发现结果。
- 聊天、协作或通知类应用在收到新消息时,提示有未读内容。
- 后台处理页面展示简短阶段信息,例如“正在导出”或“等待确认”。
它不适合作为唯一的状态提示。用户可能看不到标签页,也可能使用不显示完整标题的浏览器界面;屏幕阅读器、浏览器和操作系统对标题变化的处理也不完全相同。重要状态仍应在页面内容中呈现,必要时配合合适的实时区域、通知或其他界面反馈。
此外,持续滚动的标题会增加视觉干扰,也可能与用户对标签页的预期冲突。除非有明确需求,优先考虑“发生事件时短暂提示,然后恢复原标题”,而不是让标题永久循环。
基础原理:循环改写 document.title
实现方式很直接:把标题拆成一串字符,定时改变字符顺序,再将结果赋给 document.title。下面是一个最小示例。将代码放在页面脚本中运行即可看到当前标签页标题变化。
const text = "正在处理,请稍候";
let offset = 0;
setInterval(() => {
const chars = Array.from(text);
const rotated = chars.slice(offset).concat(chars.slice(0, offset));
document.title = rotated.join("");
offset = (offset + 1) % chars.length;
}, 300);
代码使用 Array.from 按 Unicode 码点拆分字符串,而不是直接用 text[i] 逐个读取 UTF-16 编码单元。这样可避免部分常见字符被拆成无法单独显示的代理项。不过,这并不等于完整解决了所有“可见字符”的拆分问题:带组合附加符号的文字、某些旗帜和复杂 emoji 可能由多个码点组成,视觉上却是一个字形。
示例也有几个不足:它没有保存原标题,没有停止方法,也没有处理页面切到后台后的行为;计时器会一直运行,而且标题长度较长时,用户可能看到大量跳动。实际项目最好在此基础上补齐生命周期与边界处理。
可直接使用的实现
下面的版本提供启动、停止和清理接口。它会保存调用时的原标题,只在启动期间更新标题;停止时恢复原标题;当页面进入后台或用户开启“减少动态效果”偏好时暂停动画。代码默认不自动启动,方便由业务事件明确控制。
function createTitleScroller(options = {}) {
const {
text = "有新消息",
separator = " — ",
interval = 500,
runWhenHidden = false
} = options;
if (typeof text !== "string" || text.length === 0) {
throw new TypeError("text 必须是非空字符串");
}
if (typeof separator !== "string") {
throw new TypeError("separator 必须是字符串");
}
if (!Number.isFinite(interval) || interval < 100) {
throw new RangeError("interval 必须是大于或等于 100 的有限数值");
}
const originalTitle = document.title;
const message = `${text}${separator}`;
const chars = Array.from(message);
let index = 0;
let timerId = null;
let disposed = false;
const motionQuery = window.matchMedia(
"(prefers-reduced-motion: reduce)"
);
function shouldPause() {
return disposed ||
motionQuery.matches ||
(!runWhenHidden && document.hidden);
}
function render() {
if (shouldPause()) {
stopTimer();
return;
}
const visibleChars = chars.slice(index).concat(chars.slice(0, index));
document.title = visibleChars.join("");
index = (index + 1) % chars.length;
}
function stopTimer() {
if (timerId !== null) {
window.clearInterval(timerId);
timerId = null;
}
}
function startTimer() {
if (shouldPause() || timerId !== null) {
return;
}
render();
timerId = window.setInterval(render, interval);
}
function sync() {
if (shouldPause()) {
stopTimer();
document.title = originalTitle;
} else {
startTimer();
}
}
function stop() {
stopTimer();
document.title = originalTitle;
}
function destroy() {
if (disposed) {
return;
}
stop();
disposed = true;
document.removeEventListener("visibilitychange", sync);
motionQuery.removeEventListener("change", sync);
}
document.addEventListener("visibilitychange", sync);
motionQuery.addEventListener("change", sync);
return {
start: startTimer,
stop,
destroy
};
}
const titleScroller = createTitleScroller({
text: "报告已生成",
separator: " · ",
interval: 500
});
// 在合适的业务时机启动:
// titleScroller.start();
// 用户关闭提示,或组件卸载时:
// titleScroller.destroy();
例如,收到一条需要提醒用户的消息时,可以创建并启动效果;用户打开对应会话后,再销毁实例:
const titleScroller = createTitleScroller({
text: "有 1 条新消息",
separator: " · ",
interval: 600
});
function onNewMessage() {
titleScroller.start();
}
function onConversationOpened() {
titleScroller.destroy();
}
在单页应用里,页面组件或路由可能反复挂载与卸载。应在卸载时调用 destroy(),否则事件监听器和计时器可能留在页面中,导致旧提示继续覆盖新页面标题。不同路由也可能各自修改标题,建议由一个明确的页面标题管理机制协调,避免多个功能同时争夺 document.title。
示例参数与行为边界
text:要滚动显示的提示文字,必须是非空字符串。示例在创建时进行校验;具体业务仍应自行规定允许的长度和内容。separator:提示文字后附加的分隔符,用于让滚动内容更容易辨认。它可以是空字符串,也可以是任意普通文本。interval:每次更新之间的毫秒数。示例要求它是有限数值且不小于 100 毫秒,这只是本实现主动设定的下限,不是 JavaScript 或浏览器统一保证的最小计时精度。runWhenHidden:设为false时,文档进入后台会暂停滚动并恢复原标题;设为true时允许继续请求更新,但浏览器仍可能对后台计时器进行节流,不能保证精确的刷新间隔。
计时器回调的实际执行时间并不保证与设定间隔完全一致。主线程繁忙、页面不可见、设备省电策略和浏览器调度都可能造成延迟。因此,这种动画只适合展示效果,不应用来计量时间、控制任务进度或保证某个精确时限。
示例中的 interval 下限是为了避免过于频繁地修改标题,并非所有产品都必须采用同一个数值。修改它时应在目标浏览器和设备上测试,考虑可读性、CPU 使用和用户感受。对于本质上只需显示一次的状态,通常没有必要快速轮播。
Unicode、字数和标题长度
JavaScript 字符串的 length 统计的是 UTF-16 编码单元数量,并不等于用户看到的字数。直接使用 split("") 或按下标截取,有可能把补充平面字符拆开。Array.from 能按码点遍历,适用于不少常见中英文和 emoji 场景,但仍不能保证一个数组元素对应一个完整的视觉字形。
如果产品要求滚动时绝不能拆开视觉字形,可以考虑现代浏览器提供的 Intl.Segmenter,使用 granularity: "grapheme" 按字素簇分段。该接口的支持情况取决于运行环境;需要兼容旧环境时,应评估相应的垫片或依赖,并把兼容性纳入项目范围。不要仅凭某个字符在测试页面上显示正常,就推断所有语言和 emoji 都已妥善处理。
浏览器标签页可展示的标题宽度有限,具体能显示多少内容取决于浏览器界面、字体、标签页状态和设备。过长的标题容易被截断,也会降低识别效率。滚动的目的不是让用户读完一段长文,而是提示一个简短状态;更完整的说明应放在页面正文中。
页面不可见与减少动态效果
页面切到后台后,持续动画通常没有必要。基础示例通过 document.hidden 判断文档是否隐藏,并监听 visibilitychange。恢复时重新启动计时器。这里的“页面隐藏”指文档的可见性状态,不等同于窗口失去焦点;不同浏览器对标签页、最小化窗口和其他系统状态的判定可能不同。
示例还读取 prefers-reduced-motion 媒体查询,并在系统偏好要求减少动态效果时暂停。该偏好由用户的操作系统或浏览器环境提供,网站不应把它当作用户明确同意某种动画的证明。即使媒体查询不匹配,仍应避免闪烁、过快变化和无法关闭的动态效果。
创建后调用 stop() 会停止计时器并恢复原标题,但实例监听器仍然存在,以便后续再次调用 start()。当实例不再使用时调用 destroy(),它会停止动画、恢复标题并移除监听器。不要在没有必要的情况下同时创建多个实例;多个实例轮流写入同一个 document.title,结果会互相覆盖。
原标题由谁管理
标题还原看似简单,实际涉及页面所有权。示例会在创建实例时保存 document.title,随后在停止时写回这个值。如果应用在动画运行期间因路由切换、加载状态或其他功能更新了标题,恢复旧值就可能把新标题覆盖掉。
在有路由和异步状态的应用中,建议明确标题的来源,例如由路由层维护基础标题,再由通知功能计算临时展示值;也可以在开始动画时记录一个状态令牌,结束时确认当前标题仍由该动画控制。具体方案应符合应用现有的标题管理方式。核心原则是:不要默认自己是页面里唯一会修改标题的代码。
如果要实现“提示若干秒后自动恢复”,可以另设一个超时,在超时回调中停止效果。不过需要在新提示到来时清除旧超时,并在组件销毁时一并清理。还要考虑旧回调是否会错误地覆盖新标题。较可靠的做法是保存超时 ID,并为每次提示分配递增的版本号;只有版本仍匹配的回调才允许结束当前提示。
安全用途与内容处理
document.title 设置的是普通文本,不会像 innerHTML 一样把字符串解析为 HTML。把来源不可信的文本赋给标题,通常不会因此执行 HTML 标签或脚本;标题字符串中的尖括号也只是文本内容。这一点并不意味着所有外部输入都可以不加判断地使用:标题可能出现在浏览器界面、历史记录或其他系统位置,过长内容、控制字符、误导性文字和敏感信息仍可能造成问题。
不要把密码、访问令牌、个人敏感数据或不应被旁观者看到的内容放进标题。标签页可能在屏幕共享、系统任务切换界面或浏览器历史记录中暴露信息。只显示必要、简短且经过业务允许的状态文字。
示例没有将用户输入拼接进 HTML,也没有调用 eval,不需要为动画使用字符串形式的定时器。不要把不可信字符串当作代码传给定时器;需要运行的逻辑应以函数回调形式提供。如果标题文本来自服务端或用户输入,应按业务规则校验长度和可显示内容,并避免借标题传播未经核实的指令或欺骗性提示。
常见错误排查
动画没有变化
先确认代码在浏览器文档环境中执行,且 document 可用。服务端渲染阶段没有浏览器的 document,应在客户端挂载后再创建实例。还要检查是否因系统减少动态效果偏好而暂停,或者页面当前处于隐藏状态。
标题一直变,无法停止
保留创建函数返回的实例,并在不需要时调用 stop() 或 destroy()。只创建一个实例然后丢弃返回值,会让业务代码失去清理入口。不要依赖页面卸载时浏览器自动清理来管理应用内部的路由切换。
标题恢复成了错误内容
检查是否有其他代码在动画运行期间更新标题。实例创建时保存的原标题可能已经过时。采用统一的标题状态管理,或在结束前确认当前动画仍拥有标题的控制权。
某些 emoji 显示异常
Array.from 解决的是码点遍历,不是完整的字素簇分段。若业务必须保护组合字符和复杂 emoji,使用经过兼容性评估的 Intl.Segmenter 或相应方案,并针对目标浏览器测试。
设置了很短的间隔,却没有按时刷新
浏览器不承诺定时器严格按设定时间执行。后台页面通常还可能被节流。标题动画不能承担实时通信、精准倒计时或任务调度职责;这些功能应采用合适的数据与时间机制。
简要检查清单
- 确认需求指的是浏览器标签页标题,而不是页面内的标题元素。
- 标题只提供简短提示;重要状态也在页面内容中表达。
- 选择合理的更新间隔,避免快速闪烁和过长文本。
- 处理页面可见性、减少动态效果偏好和实例清理。
- 明确原标题由谁管理,避免多处代码互相覆盖。
- 考虑 Unicode 字素簇、标题信息泄露和用户输入校验。
给浏览器标题添加滚动动画,代码本身并不复杂,真正需要认真处理的是何时启动、由谁负责恢复,以及用户看不到动画时如何表现。把效果限制在明确的提示场景,提供可清理的生命周期,并把重要信息留在页面正文中,通常比追求复杂的滚动形式更可靠。



