摘要 :在这个前端框架横行的时代,我们是否还记得原生 HTML/CSS/JS 的力量?本文将剖析一个单文件级应用 ZenReader Pro ,展示如何仅用 600 行代码实现一个包含高亮标注、拖拽笔记、本地书库、Markdown 导入导出 及沉浸式排版 的现代阅读器。
"C:\Users\86182\Downloads\ai_studio_code (7).html"
一、 项目背景与设计理念
在信息爆炸的今天,阅读体验往往被广告、弹窗和复杂的 UI 割裂。ZenReader 的设计初衷回归阅读本质,核心理念遵循 "Less is More":
- 视觉降噪:默认隐藏所有工具栏,仅在鼠标悬停底部时浮现。
- 纸感阅读 :采用米黄色温背景(
#f5f4f1)与深灰文字,配合开源字体"霞鹜文楷",模拟实体书的阅读质感。 - 心流交互:通过快捷键(H/S)和拖拽操作,保持用户的阅读"心流"不被打断。
二、 核心技术架构分析
本项目完全摒弃了 React/Vue 等框架和 Webpack 打包工具,采用 Single File Component (SFC) 的变体------即单 HTML 文件架构。
1. 响应式布局与 CSS 变量系统
为了实现"一键换肤"和"动态调整",CSS 变量(Custom Properties)发挥了巨大作用。
css
:root {
--bg-color: #f5f4f1; /* 背景色 */
--drawer-width: 360px; /* 抽屉宽度(动态可调) */
--anim: cubic-bezier(0.25, 0.8, 0.25, 1); /* 统一的缓动函数 */
}
亮点 :右侧笔记栏的宽度调整并没有写死在 JS 里去操作 DOM 的 width,而是通过 JS 修改 --drawer-width 变量,CSS 自动响应。这极大减少了重排(Reflow)的性能开销。
2. 高亮系统的核心逻辑
如何在一个 contenteditable 或普通 div 中高亮文字?核心在于 window.getSelection() 和 Range 对象。
难点解决:智能切换(Toggle)
代码中实现了一个智能判断:如果用户选中的文字已经被高亮了,再次触发时应该是"取消高亮"。
javascript
// 核心逻辑片段
function toggleHighlightKey() {
const selection = window.getSelection();
if (!selection.rangeCount) return;
const range = selection.getRangeAt(0);
const parent = range.commonAncestorContainer.parentNode;
// 状态检测:是否已在高亮标签内?
if (parent.classList.contains('highlight-mark')) {
// 解包逻辑 (Unwrap):用文本节点替换掉 span 标签
const text = document.createTextNode(parent.innerText);
parent.parentNode.replaceChild(text, parent);
} else {
// 包裹逻辑 (Wrap):创建 span 包裹选区
const span = document.createElement('span');
span.className = 'highlight-mark';
range.surroundContents(span);
}
}
这段代码展示了原生 DOM 操作的精髓,无需 Virtual DOM 也能高效处理节点变更。
3. HTML5 原生拖拽与数据驱动
笔记栏支持拖拽排序。这里使用了 HTML5 Native Drag & Drop API,但最关键的设计决策是:数据驱动视图。
当发生拖拽(Drop)时,我们不直接交换 DOM 节点,而是操作背后的 currentSummaries 数组,然后重新渲染整个列表。
javascript
// 拖拽结束的处理逻辑
function handleDrop(e) {
// 1. 获取源索引和目标索引
const fromIndex = parseInt(e.dataTransfer.getData('text/plain'));
const toIndex = parseInt(this.dataset.index);
// 2. 数组操作 (Splice)
if (fromIndex !== toIndex) {
const item = currentSummaries.splice(fromIndex, 1)[0];
currentSummaries.splice(toIndex, 0, item);
// 3. 重新渲染
renderSummaryList();
}
return false; // 阻止浏览器默认行为
}
这种模式确保了导出的 Markdown 文件顺序永远与界面显示的顺序一致。
4. 健壮的视图状态管理
为了解决"返回编辑按钮失灵"的 Bug,我们重构了视图切换逻辑。
- 问题:之前的逻辑是根据输入框是否有值来判断是否渲染。
- 修复 :改为强制同步。
- 进入阅读时:无论之前有没有 HTML,都重新解析输入框内容。
- 退出阅读时 :必须将
contentBody.innerHTML(包含用户做的高亮)反向同步回textarea,确保数据不丢失。
三、 交互体验的微创新
作为高级前端设计,功能实现只是基础,体验才是护城河。
1. 隐形交互 (Invisible Interface)
底部的 control-bar 默认 opacity: 0,配合 backdrop-filter: blur(10px)(毛玻璃特效)。只有当阅读者需要时(鼠标靠近底部),工具栏才会显现。这种设计最大程度地保护了阅读者的注意力。
2. 轻量级反馈 (Toast)
为了配合快捷键(H/S),引入了一个极其轻量的 Toast 系统。
css
.toast {
/* 默认隐藏在视口下方 */
transform: translateX(-50%) translateY(20px);
opacity: 0;
transition: all 0.3s;
}
.toast.show {
/* 上浮显示 */
transform: translateX(-50%) translateY(0);
opacity: 1;
}
这比浏览器自带的 alert() 优雅无数倍,且不会打断用户的操作流。
3. 正则表达式的魔法 (Markdown 导入/导出)
为了兼容 Obsidian/Notion 等工具,我们手写了一个轻量级解析器:
- 导出 :将
<span class="highlight-mark">text</span>替换为 Markdown 的高亮语法==text==。 - 导入 :使用正则
replace(/^[-*>\d.]+\s+/, '')智能去除列表符,只提取纯文本内容到笔记卡片中。
四、 运行界面
