当前位置: 首页
前端开发
MarkView纯前端Markdown实时预览工具架构设计

MarkView纯前端Markdown实时预览工具架构设计

热心网友 时间:2026-07-19
转载

MarkView是一款纯前端Markdown实时预览工具,数据存于IndexedDB,零后端。采用CodeMirror6编辑并分层架构:core层纯逻辑可测试,controllers封装命令式操作,workspace组织响应式状态,组件仅渲染。渲染在WebWorker执行,滚动同步基于锚点表插值,分享通过压缩文档至URLhash,PWA可注册为 md文件处理

MarkView 是一款纯前端的本地 Markdown 实时预览工具(官网:markview.art)。它左侧采用 CodeMirror 6 编辑器,右侧实时渲染,所有数据均存储于浏览器的 IndexedDB 中,完全无需后端服务。支持暗色/浅色主题、命令面板、URL 分享,并可作为 PWA 离线安装,甚至能注册为系统 .md 文件处理器——双击本地文件即可直接打开。

本文不逐一罗列功能,而是深入探讨其架构取舍:如何在一个没有后端、没有状态管理库的 Vue 3 项目中,将编辑器、渲染、持久化、搜索、同步等子系统组织得清晰可测,为开发者提供参考。

一、为什么是「纯前端」

首先聊聊定位。市面上 Markdown 编辑器众多,MarkView 选择了约束最强的路线:零后端、零上传、离线可用。文档全部存储在浏览器 IndexedDB 中,分享通过将文档压缩进 URL hash 实现,链接自包含。PWA 预缓存构建产物,即使断网也能正常编辑和预览。

这一约束反过来塑造了诸多技术决策,带来了一系列精巧的设计。例如图片处理:没有后端就没有图床,因此粘贴或拖拽的图片会被压缩(缩放+WebP编码)为 base64 直接内联进文档——图片随文档一起存储本地,分享和导出天然自包含。再如 PDF 导出:不引入 html2pdf 等庞大依赖,直接调用浏览器原生打印,得到文字可选、体积小的矢量输出,版式交给一份 @media print 样式表。

二、CodeMirror-First 分层架构

整个项目的核心设计是一套严格单向依赖的分层架构,从下到上依次为:

 复制代码components(哑组件,inject 工作区直接消费)
    ↓
workspace(响应式状态 + 横切编排)
    ↓
controllers(EditorController / PreviewController,命令式门面)
    ↓
core(纯逻辑领域层:零 Vue、零 DOM,Vitest 直测)

core:零 Vue、零 DOM 的纯领域层

最底层的 core/ 是一个纯函数世界:文档模型、IndexedDB 存储、保存状态机、Markdown 渲染与源行标注、跨文档搜索、滚动锚点表、分享链接编解码……全部不依赖 Vue 和 DOM。其优势显而易见:这一层可直接被 Vitest 测试,无需挂载组件、无需模拟浏览器环境。测试文件与被测模块同目录放置,例如保存状态机的防抖、竞态、串行化逻辑,均通过纯逻辑单测覆盖:

 复制代码// core/documents/persistenceScheduler.js
// 保存状态机:防抖调度 + revision 防竞态 + Promise 链串行化写入。
// 存储实现(sa ve)与快照来源(getSnapshot)以参数注入,不依赖 Vue 与 DOM。
export const createPersistenceScheduler = ({
  getSnapshot,
  sa ve,
  canPersist,
  onStatus,
}) => {
  // ...
};

依赖注入贯穿整层:存储写入方式、快照获取方式,均作为参数传入。测试时只需替换为内存实现即可。

controllers:命令式能力的唯一访问入口

CodeMirror 6 和预览区的 DOM 操作本质上是命令式的:设置光标、滚动到某行、高亮某段。这些能力被封装进两个门面——EditorControllerPreviewController,它们是组件之下操作编辑器或预览的唯一入口。 EditorController 还承担多文档的 EditorState 缓存:每个文档拥有独立的撤销历史,切换文档时替换 state 而非重建编辑器。

workspace:响应式状态与横切编排层

workspace/ 持有全部响应式状态,按职责拆分为多个会话:文档会话(多文档 CRUD + 持久化)、搜索会话、布局会话(分栏宽度/折叠/目录宽度)、预览渲染会话等。跨子系统的流程——例如「搜索命中后定位到行并双侧高亮」「折叠编辑区时把按钮移入预览区头部」——集中在 orchestrations.js 中,不散落在组件里。 createWorkspace.js 负责将这一切组装起来,并通过 provide 下发。

components:仅负责 UI 的哑组件

最上层的组件通过 inject 获取工作区并直接消费,自身不含业务逻辑。工具栏、命令面板、目录、搜索面板等,都只是状态的投影和动作的转发。这套分层的收益在于:变更有明确的落点。例如添加一个「导出 HTML」功能,纯逻辑进入 services/exporter.js,动作挂到 workspace,组件加一个按钮,三层各改一处,互不渗透。

三、几个关键子系统的取舍

渲染搬到 Web Worker

Markdown 渲染(marked 解析 + 代码高亮 + 源行标注)对长文档来说开销较大,跑在主线程会卡顿输入。MarkView 将渲染整体搬进 Web Worker,从而保持主线程流畅:

 复制代码// workspace/markdownRenderer.js
// Markdown 渲染调度:优先用 Web Worker(渲染在后台线程,主线程不卡顿),
// Worker 不可用时动态 import 同步渲染降级(保持主 bundle 精简,仅降级时才加载)。
// 用递增序号丢弃过期结果,避免快速输入时旧结果覆盖新结果。

有三个细节值得一提:

  1. 降级路径:Worker 不可用时动态 import 同步渲染模块——降级代码不进主 bundle,只有真正降级时才加载;
  2. 序号防乱序:快速输入时会有多个渲染请求在途,用递增序号丢弃过期结果,保证旧结果不会覆盖新结果;
  3. 渲染是纯函数renderMarkdown.js 本身在 core 层,Worker 只是它的一个宿主。

滚动同步:锚点表 + 单调插值

编辑区和预览区的双向滚动同步,一直是 Markdown 工具的一个经典难题——两侧内容高度不成线性比例(一行 Markdown 可能渲染成一张大图)。MarkView 的做法是渲染时给块级元素标注源码行号,再据此构建锚点表:

 复制代码// core/sync/anchorMap.js
// 构建锚点表:pairs 是若干对已对齐的滚动偏移 { editor, preview }。
// 两端各加一个哨兵锚点,把文档顶部与底部钉在一起,使两侧能同时到达 0 和最大值。
// 只保留严格落在两侧滚动范围内、且在两个轴上都严格递增的锚点,
// 以保证每一段插值都是单调的。

锚点之间线性插值,配合「滚动主控方」标记防止两侧互相触发形成回环。这套逻辑同样是纯函数,边界情况(锚点乱序、超出滚动范围)全部有单测覆盖。

URL 分享:文档压缩进 hash

没有后端,分享怎么做?答案是 lz-string:

 复制代码// core/share/shareLink.js
export const encodeSharePayload = ({ name, content, anchor }) => {
  const payload = JSON.stringify({
    n: name || "",
    c: content || "",
    ...(anchor ? { a: anchor } : {}),
  });
  return compressToEncodedURIComponent(payload);
};

文档名和内容 JSON 序列化后用 lz-string 的 URI 变体压缩,直接放进 URL hash。链接自包含——打开即还原文档,还可以携带标题锚点直达某一节。hash 不会发往服务器,天然私密。

PWA:不止离线,还是文件处理器

除了常规的预缓存离线,MarkView 在 manifest 里注册了 file_handlers

 复制代码file_handlers: [
    {
        action: base,
        accept: { 'text/markdown': ['.md', '.markdown'] }
    }
],
launch_handler: {
    client_mode: ['focus-existing', 'auto']
}

安装为 PWA 后,它成为系统里 .md / .markdown 文件的处理器——双击文件,操作系统唤起 MarkView ,应用侧由 launchQueue 消费者接收文件句柄并导入为新文档。launch_handler 设为优先复用已有窗口,避免每次双击都新开实例。一个 Web 应用,就这样长出了一点「桌面应用」的质感。

四、工程化闭环

最后聊聊保证质量的一揽子配置。一条命令跑完全部校验:

 复制代码pnpm check   # lint + 格式检查 + 测试 + 构建
  • ESLint --max-warnings=0,警告即失败;
  • Prettier 统一格式;
  • Vitest 直测 core 层与 workspace 编排逻辑;
  • 最后跑一次生产构建,确保产物健康。

结语

MarkView 的经验,可以浓缩成三句话:

  1. 约束是设计的朋友——「零后端」逼出了图片内联、URL 分享、原生打印这些干净的方案;
  2. 纯逻辑下沉是可测试性的根基——core 层零 Vue 零 DOM,测试成本低到没有借口不写;
  3. 命令式的世界要有门面——CodeMirror 和 DOM 的复杂性被 controller 挡住,上层始终面对声明式接口。

关于多标签页打开时文档状态如何实时同步、编辑中的内容如何保证不被覆盖,那又是另一套有意思的机制,另外有一篇文章专门讨论这个话题。

来源:https://juejin.cn/post/7662691781213519908

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
CSS星空背景遮挡页面链接的有效修复方法

CSS星空背景遮挡页面链接的有效修复方法

CSS星空背景元素因绝对定位与高z-index覆盖视口,导致底层链接的鼠标事件被屏蔽。为纯装饰层添加`pointer-events:none`属性,可透传鼠标事件,恢复链接交互,且不影响动画渲染与性能表现。这样既保留视觉装饰,又确保用户正常点击底层链接。

时间:2026-07-21 06:22
组合键Ctrl+C触发不同音符的播放方法

组合键Ctrl+C触发不同音符的播放方法

通过监听键盘事件并检测修饰键状态,实现同一字母键在不同组合下播放不同音符。需注意使用event ctrlKey正确拼写,调用preventDefault阻止默认行为,统一转小写,并考虑重复触发过滤。可扩展的键映射方案便于维护。

时间:2026-07-21 06:21
Vue与Three.js实现人物奔跑3D场景

Vue与Three.js实现人物奔跑3D场景

基于Vue与Three js构建人物奔跑场景:人物加载GLTF骨骼动画原地循环跑步,巨型草坪平面持续向后平移,营造无限奔跑错觉,并添加线性白雾增强空间层次,代码工程化实现。

时间:2026-07-21 06:21
CSS :has()父选择器根据图片是否存在调整文字宽度

CSS :has()父选择器根据图片是否存在调整文字宽度

聊聊` card:has(img)`和` card:not(:has(img))`的真实用法 这两个组合是目前最稳定、纯粹基于DOM结构进行判断的方案。适用场景非常明确: * ` card:has(img) text`:当卡片里有``标签时,文字容器收窄,比如设为`width: 70%`,

时间:2026-07-21 06:21
正确使用回调函数实现链式调用多个函数

正确使用回调函数实现链式调用多个函数

本文深入解析回调函数链式调用的常见误区和正确用法,通过修复 a → b → c 顺序执行失败的典型问题,阐明回调传递的核心本质:传递函数引用而非调用结果,并提供可运行的代码示例与关键注意事项,帮助开发者彻底掌握回调函数链式调用写法。 在实际项目开发中,利用回调函数实现 a → b → c 这样的顺序

时间:2026-07-21 06:21
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 热门数据榜