零依赖的艺术:用原生 JS 打造“ZenReader”沉浸式阅读器

摘要 :在这个前端框架横行的时代,我们是否还记得原生 HTML/CSS/JS 的力量?本文将剖析一个单文件级应用 ZenReader Pro ,展示如何仅用 600 行代码实现一个包含高亮标注、拖拽笔记、本地书库、Markdown 导入导出 及沉浸式排版 的现代阅读器。

"C:\Users\86182\Downloads\ai_studio_code (7).html"


一、 项目背景与设计理念

在信息爆炸的今天,阅读体验往往被广告、弹窗和复杂的 UI 割裂。ZenReader 的设计初衷回归阅读本质,核心理念遵循 "Less is More":

  1. 视觉降噪:默认隐藏所有工具栏,仅在鼠标悬停底部时浮现。
  2. 纸感阅读 :采用米黄色温背景(#f5f4f1)与深灰文字,配合开源字体"霞鹜文楷",模拟实体书的阅读质感。
  3. 心流交互:通过快捷键(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+/, '') 智能去除列表符,只提取纯文本内容到笔记卡片中。

四、 运行界面

相关推荐
清晨Coding19 小时前
删了 Cookie 还能自动登录?Google系网站登录态自动恢复的原理分析
javascript
kyriewen19 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
程序员大阳21 小时前
HTML+CSS+JS对话式实战(9)-项目总结与问题分析
javascript·css·html·实战·项目总结
Hilaku1 天前
MCP 协议会成为前端 AI 工程的标准接口吗?
前端·javascript·程序员
RobinDevNotes1 天前
从 Electron 到 GPUI,9 个跨平台桌面方案该怎么选
前端·javascript·rust·electron·openresty·前端开发工具
程序员大阳1 天前
HTML+CSS+JS对话式实战(6)-学生端查看活动功能开发
javascript·css·html·模块·查看学生
子兮曰2 天前
Chrome 权限元素实战:144 与 151 的渐进增强写法
前端·javascript·chrome
强尼银手2 天前
我花一天做了个格斗游戏原型,判定全对,然后被「美术」劝退了
前端·javascript
总要学点什么2 天前
第98篇:三维特效-材质线效果——给一条线换十种材质,虚线的图案只填了一半
开发语言·前端·javascript·ecmascript·gis·webgl·材质
OpsEye2 天前
混合云 AI 架构,如何统一做敏感内容拦截?
javascript·ai编程