做口播、直播、讲课,经常需要一份能随时滚动的台词。本次实践的目标很明确:写一个不依赖任何 App 商店、不依赖网络、用手机浏览器即可运行的轻量提词器。
最终产物是一个 23KB 的单文件 HTML,支持:分镜时间轴、横竖屏自适应、多文案切换、调速/字号/配色/镜像/倒计时、草稿本地保存,以及可选的 PWA「添加到主屏幕」能力。
本文将完整复盘从选型到落地的过程,并附上三个真实踩坑,帮你在做类似移动端工具时少走弯路。
方案 | 跨平台 | 离线 | 分发成本 | 上手门槛 |
|---|---|---|---|---|
原生 App(Android/iOS) | 需双端 | 是 | 高(商店审核、签名) | 高 |
跨端框架(Flutter/RN) | 是 | 是 | 中(打包、上架) | 中高 |
单文件 HTML | 全平台(浏览器即运行时) | 是(无外部依赖) | 低(发个文件即可) | 低 |
选择单文件 HTML 的核心原因:
.html 通过 IM 工具、U 盘、网盘发出去,手机浏览器打开即用,不需要应用商店。唯一短板是「安装到桌面 / 离线缓存」需要 PWA(HTTPS 环境),但纯离线用浏览器打开已完全够用,PWA 只是锦上添花。
页面只有两个「屏幕」,用 display 切换:
#editor):写台词、调参数、管理多条文案、查看分镜列表。#player):全屏黑底,台词从屏幕底部匀速升入、向结尾滚动。关键设计:滚动用 transform: translateY() 而非 scrollTop。因为 translateY 走 GPU 合成层,丝滑不卡,且能轻松叠加镜像(scaleX(-1))用于玻璃提词板。
#scroll { position: absolute; inset: 0; will-change: transform; padding: 0 4vw; text-align: center; }最初版本用 flex-end 让内容底贴屏幕底,结果一进播放就只显示稿子末尾,正向滚动完全反了。正确做法是:正文自然排列,开头从屏幕底部 92% 处升入,向结尾方向滚动。
function measure() {
var innerTop = scrollInner.getBoundingClientRect().top;
var contentH = scrollInner.getBoundingClientRect().height;
var vh = player.getBoundingClientRect().height;
startOff = vh * 0.92; // 开头从屏幕底部升入
total = Math.max(0, contentH + startOff - vh); // 滚动总距离
render();
}
function render() {
scroll.style.transform =
(state.mirror ? "scaleX(-1) " : "") +
"translateY(" + (startOff - pos) + "px)"; // pos 从 0 增大,内容向上走
}滚动循环用 requestAnimationFrame,按 speed(px/s)× 时间增量推进:
function loop(ts) {
if (!playing) return;
if (!last) last = ts;
var dt = (ts - last) / 1000; last = ts;
pos += state.speed * dt;
if (pos >= total) { pos = total; playing = false; }
render();
raf = requestAnimationFrame(loop);
}用户用「空行」分段 = 一个分镜。代码按空行切分,自动生成编辑区分镜列表 + 播放时顶部时间轴节点:
function splitScenes(txt) {
return txt.replace(/\r/g, "")
.split(/\n\s*\n/)
.map(b => b.trim())
.filter(b => b.length);
}每个分镜首句作为预览,按 ~3.5 字/秒估算时长;点列表项或时间轴节点即可 jumpToScene(i) 跳到对应分镜的开头位置。
监听 resize 和 orientationchange,旋转后重算字号并重新测量滚动距离。横屏时字号自动放大 12%,一行容纳更多字:
function effFont() {
var f = state.font;
if (window.innerWidth > window.innerHeight) f = Math.min(140, Math.round(f * 1.12));
return f;
}
window.addEventListener("resize", onOrient);
window.addEventListener("orientationchange", onOrient);播放时还提供 ⇄ 按钮主动调用 screen.orientation.lock('landscape') 锁定横屏,给不喜欢物理旋转的老板们。
本场口播常有几套词来回切,所以把文案存成数组,每条含 id/title/text:
var KEY_LIB = "teleprompter_lib_v2";
var library = []; // [{id, title, text}, ...]
// 切换
function loadPrompt(id, keepPlaying) { /* 写回当前、载入目标、可选续播 */ }
// 播放中上下篇
function switchPrompt(dir) {
var idx = library.findIndex(p => p.id === activeId);
idx = (idx + dir + library.length) % library.length;
loadPrompt(library[idx].id, true);
}删除用「二次确认」而非 `confirm()——这是移动端的关键坑(见第四节)。
单文件 HTML 之外,再加 manifest.json + sw.js 即可:
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => navigator.serviceWorker.register("sw.js"));
}
let deferredPrompt = null;
window.addEventListener("beforeinstallprompt", e => {
e.preventDefault(); deferredPrompt = e;
installBtn.style.display = "inline-block"; // 触发「装到桌面」按钮
});sw.js 用 cache-first 策略缓存 HTML/图标,部署到 HTTPS 后,手机「添加到主屏幕」即成独立 App,可完全离线。
confirm() 在移动端 WebView 被静默拦截最初的「删除文案」用了浏览器原生 confirm() 二次确认。结果在部分 App 内置浏览器 / 系统 WebView 里 confirm 根本不弹,点了毫无反应,表现为「删不了」。
修复:改用「两次点击确认」——第一次点按钮变红显示「确认删?」,3 秒内再点才真删,纯前端状态机,不依赖任何原生对话框。
if (!delArmed) { delArmed = true; this.textContent = "确认删?"; /* 3s 后复位 */ return; }
// 第二次点击才执行 splice 删除布局用 flex-end 把内容底部贴屏幕底,导致一开播就停在稿子末尾,正向滚动逻辑完全错乱。
修复:去掉 flex 对齐,改为 translateY(startOff - pos),让内容从底部 92% 处自然升入。详见第三节第 1 点。
.html 可能被拦把 teleprompter.html 当文件发某些即时通讯工具,偶尔会被安全策略拦截。兜底方案:同时打一个 .zip(压缩包通常不拦),手机解压后得 HTML;或在内网用 http://<电脑IP>:<端口>/teleprompter.html 直接访问。
方式 | 体验 | 适用 | |
|---|---|---|---|
浏览器直接打开 | 单个文件即可运行,断网可用 | 本地自用 | |
局域网直连 | 实时调试最新版本 | 同 WiFi 下多设备查看 | |
静态托管 + 添加到主屏幕 | 接近原生 App 体验,可离线 | 需要「安装到桌面」 | |
PWA → Bubblewrap(TWA) | 生成 APK 安装包 | 需要上架应用商店 |
APK 说明:本地没有 Android SDK,直接打 APK 不现实。可先把 PWA 部署到 HTTPS 静态托管,再用 Google 官方的 Bubblewrap 工具生成 Trusted Web Activity(TWA)壳 APK,离线缓存照常生效。第三方 PWA-to-APK 在线服务也可作为备选,但需注意隐私与签名安全。
一个零依赖、可离线、跨平台的手机提词器,本质就是「单文件 HTML + 几个核心函数」:
translateY + requestAnimationFramelocalStorage 数组 + 二次确认删除源码就一个 HTML 文件,约 23KB,可自由改配色、加功能(如虚拟滚动支持超长文本、远程手机遥控滚动)。这套「单文件 HTML + PWA」的范式在做其他移动端轻量工具时同样适用。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。