纯前端单文件 Markdown 笔记应用源码|实时预览 + localStorage + 导出 .md(原创教学)

纯前端单文件 Markdown 笔记应用源码|实时预览 + localStorage + 导出 .md(原创教学)

前言

不想搭后端、不想装依赖,又希望有一个能随手记笔记的小工具?今天分享一套纯前端单文件 Markdown 笔记应用源码:一个 HTML 文件即可运行,支持实时预览、localStorage 持久化、多笔记管理、导出 .md 文件。原创教学用途,适合学习现代前端交互与本地存储设计。

声明:本文代码为原创教学示例,请勿用于违法违规用途;请遵守所在地区法律法规与目标服务条款。

功能特性

  • 单文件运行:无需 Node、无需构建、双击打开即可用
  • Markdown 实时预览(标题、列表、代码块、链接、引用)
  • 多笔记管理:新建 / 重命名 / 删除 / 快速切换
  • localStorage 自动保存,刷新不丢
  • 一键导出 .md 文件到本地
  • 深色主题、快捷键(Ctrl/Cmd+S 保存提示,Ctrl/Cmd+N 新建)

环境要求

  • 任意现代浏览器(Chrome / Edge / Firefox / Safari)
  • 无需服务器与任何运行环境

目录结构

md-notes/
└── index.html    # 全部逻辑都在这一个文件里

完整源码

index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Markdown 笔记</title>
  <style>
    :root {
      --bg: #0f1115;
      --panel: #1a1d24;
      --border: #2a2f3a;
      --text: #e8eaed;
      --muted: #8b93a7;
      --accent: #f04494;
      --ok: #3dd68c;
    }
    * { box-sizing: border-box; }
    body {
      margin: 0;
      font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
      background: var(--bg);
      color: var(--text);
      height: 100vh;
      display: flex;
      flex-direction: column;
    }
    header {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 12px 16px;
      border-bottom: 1px solid var(--border);
      background: var(--panel);
    }
    header h1 { font-size: 16px; margin: 0; font-weight: 600; }
    header .spacer { flex: 1; }
    button {
      background: #12151c;
      color: var(--text);
      border: 1px solid var(--border);
      border-radius: 8px;
      padding: 8px 12px;
      cursor: pointer;
      font-size: 13px;
    }
    button.primary {
      background: var(--accent);
      border-color: transparent;
      color: #fff;
      font-weight: 600;
    }
    button:hover { filter: brightness(1.08); }
    .layout {
      flex: 1;
      display: grid;
      grid-template-columns: 220px 1fr 1fr;
      min-height: 0;
    }
    aside {
      border-right: 1px solid var(--border);
      overflow: auto;
      background: #12151c;
    }
    .note-item {
      padding: 12px 14px;
      border-bottom: 1px solid var(--border);
      cursor: pointer;
    }
    .note-item.active { background: rgba(240, 68, 148, 0.12); border-left: 3px solid var(--accent); }
    .note-item .t {
      font-size: 13px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .note-item .m { font-size: 11px; color: var(--muted); margin-top: 4px; }
    .editor, .preview {
      display: flex;
      flex-direction: column;
      min-width: 0;
    }
    .editor { border-right: 1px solid var(--border); }
    .panel-title {
      padding: 10px 14px;
      font-size: 12px;
      color: var(--muted);
      border-bottom: 1px solid var(--border);
      display: flex;
      gap: 8px;
      align-items: center;
    }
    .panel-title input {
      flex: 1;
      background: transparent;
      border: none;
      color: var(--text);
      font-size: 13px;
      outline: none;
    }
    textarea {
      flex: 1;
      width: 100%;
      resize: none;
      border: none;
      outline: none;
      padding: 16px;
      background: var(--bg);
      color: var(--text);
      font: 14px/1.7 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    }
    .preview-body {
      flex: 1;
      overflow: auto;
      padding: 16px 20px;
      line-height: 1.75;
    }
    .preview-body h1, .preview-body h2, .preview-body h3 { margin: 1.1em 0 0.5em; }
    .preview-body pre {
      background: #12151c;
      border: 1px solid var(--border);
      border-radius: 10px;
      padding: 12px 14px;
      overflow: auto;
    }
    .preview-body code {
      font-family: ui-monospace, Menlo, Consolas, monospace;
      font-size: 13px;
    }
    .preview-body blockquote {
      margin: 0.8em 0;
      padding: 8px 14px;
      border-left: 3px solid var(--accent);
      background: rgba(240, 68, 148, 0.08);
      color: var(--muted);
    }
    .preview-body a { color: var(--accent); }
    .status {
      font-size: 12px;
      color: var(--ok);
      padding: 0 16px 10px;
      min-height: 18px;
    }
    @media (max-width: 900px) {
      .layout { grid-template-columns: 1fr; grid-template-rows: 120px 1fr 1fr; }
      aside { display: flex; overflow-x: auto; border-right: none; border-bottom: 1px solid var(--border); }
      .note-item { min-width: 160px; border-right: 1px solid var(--border); border-bottom: none; }
    }
  </style>
</head>
<body>
  <header>
    <h1>Markdown 笔记</h1>
    <div class="spacer"></div>
    <button id="btnNew">新建</button>
    <button id="btnDel">删除</button>
    <button id="btnExport">导出 .md</button>
    <button class="primary" id="btnSave">保存</button>
  </header>
  <div class="layout">
    <aside id="list"></aside>
    <section class="editor">
      <div class="panel-title">
        <input id="title" placeholder="笔记标题" />
      </div>
      <textarea id="md" placeholder="# 开始用 Markdown 写笔记..."></textarea>
      <div class="status" id="status"></div>
    </section>
    <section class="preview">
      <div class="panel-title">实时预览</div>
      <div class="preview-body" id="preview"></div>
    </section>
  </div>

  <script>
    const KEY = 'md-notes-v1';
    let notes = [];
    let currentId = null;

    const $ = (id) => document.getElementById(id);
    const listEl = $('list'), titleEl = $('title'), mdEl = $('md'),
          previewEl = $('preview'), statusEl = $('status');

    function uid() {
      return Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
    }

    function load() {
      try { notes = JSON.parse(localStorage.getItem(KEY) || '[]'); } catch (e) { notes = []; }
      if (!notes.length) {
        notes = [{
          id: uid(),
          title: '欢迎使用 Markdown 笔记',
          content: '# 欢迎\n\n左侧管理笔记,右侧实时预览。\n\n- 支持 **加粗**、*斜体*\n- 支持 `代码`、代码块\n- 支持 > 引用、链接\n',
          updatedAt: Date.now()
        }];
        persist();
      }
      currentId = notes[0].id;
      renderList();
      openNote(currentId);
    }

    function persist() {
      localStorage.setItem(KEY, JSON.stringify(notes));
    }

    function renderList() {
      listEl.innerHTML = '';
      notes
        .slice()
        .sort((a, b) => b.updatedAt - a.updatedAt)
        .forEach((n) => {
          const div = document.createElement('div');
          div.className = 'note-item' + (n.id === currentId ? ' active' : '');
          div.innerHTML = '<div class="t"></div><div class="m"></div>';
          div.querySelector('.t').textContent = n.title || '未命名';
          div.querySelector('.m').textContent = new Date(n.updatedAt).toLocaleString();
          div.onclick = () => openNote(n.id);
          listEl.appendChild(div);
        });
    }

    function openNote(id) {
      saveCurrent(true);
      const n = notes.find((x) => x.id === id);
      if (!n) return;
      currentId = id;
      titleEl.value = n.title;
      mdEl.value = n.content;
      renderList();
      renderPreview();
    }

    function saveCurrent(silent) {
      const n = notes.find((x) => x.id === currentId);
      if (!n) return;
      n.title = titleEl.value.trim() || '未命名';
      n.content = mdEl.value;
      n.updatedAt = Date.now();
      persist();
      renderList();
      if (!silent) statusEl.textContent = '已保存 ' + new Date().toLocaleTimeString();
    }

    function renderPreview() {
      previewEl.innerHTML = mdToHtml(mdEl.value);
    }

    function esc(s) {
      return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    }

    // 轻量 Markdown 解析(教学实现,覆盖常用语法)
    function mdToHtml(src) {
      const lines = esc(src).split(/r?n/);
      let html = '';
      let inCode = false;
      let inList = false;
      const closeList = () => {
        if (inList) { html += '</ul>'; inList = false; }
      };
      for (const raw of lines) {
        const line = raw;
        if (line.trim().startsWith('```')) {
          if (!inCode) { closeList(); html += '<pre><code>'; inCode = true; }
          else { html += '</code></pre>'; inCode = false; }
          continue;
        }
        if (inCode) { html += line + 'n'; continue; }

        let m;
        if ((m = line.match(/^(#{1,6})s+(.*)$/))) {
          closeList();
          const lv = m[1].length;
          html += '<h' + lv + '>' + inline(m[2]) + '</h' + lv + '>';
        } else if ((m = line.match(/^>s?(.*)$/))) {
          closeList();
          html += '<blockquote>' + inline(m[1]) + '</blockquote>';
        } else if ((m = line.match(/^[-*+]s+(.*)$/))) {
          if (!inList) { html += '<ul>'; inList = true; }
          html += '<li>' + inline(m[1]) + '</li>';
        } else if (line.trim() === '') {
          closeList();
          html += '<br/>';
        } else {
          closeList();
          html += '<p>' + inline(line) + '</p>';
        }
      }
      if (inCode) html += '</code></pre>';
      closeList();
      return html;
    }

    function inline(s) {
      return s
        .replace(/`([^`]+)`/g, '<code>$1</code>')
        .replace(/**([^*]+)**/g, '<strong>$1</strong>')
        .replace(/*([^*]+)*/g, '<em>$1</em>')
        .replace(/[([^]]+)](([^)]+))/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
    }

    $('btnSave').onclick = () => saveCurrent(false);
    $('btnNew').onclick = () => {
      saveCurrent(true);
      const n = { id: uid(), title: '未命名笔记', content: '', updatedAt: Date.now() };
      notes.unshift(n);
      persist();
      openNote(n.id);
      statusEl.textContent = '已新建笔记';
    };
    $('btnDel').onclick = () => {
      if (!confirm('删除当前笔记?')) return;
      notes = notes.filter((x) => x.id !== currentId);
      if (!notes.length) {
        notes = [{ id: uid(), title: '未命名笔记', content: '', updatedAt: Date.now() }];
      }
      persist();
      openNote(notes[0].id);
      statusEl.textContent = '已删除';
    };
    $('btnExport').onclick = () => {
      saveCurrent(true);
      const n = notes.find((x) => x.id === currentId);
      const blob = new Blob([n.content], { type: 'text/markdown;charset=utf-8' });
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = (n.title || 'note') + '.md';
      a.click();
      URL.revokeObjectURL(a.href);
      statusEl.textContent = '已导出 .md';
    };

    mdEl.addEventListener('input', () => {
      renderPreview();
      saveCurrent(true);
    });
    titleEl.addEventListener('input', () => saveCurrent(true));

    document.addEventListener('keydown', (e) => {
      const mod = e.ctrlKey || e.metaKey;
      if (mod && e.key.toLowerCase() === 's') { e.preventDefault(); saveCurrent(false); }
      if (mod && e.key.toLowerCase() === 'n') { e.preventDefault(); $('btnNew').click(); }
    });

    load();
  </script>
</body>
</html>

? 源码下载

下载 markdown-notes.zip(解压即用)

下载解压后按文中步骤部署即可。若正文代码与压缩包不一致,以压缩包为准。

使用方法

  1. 把上面代码保存为 index.html。
  2. 双击用浏览器打开,即可开始写笔记。
  3. 点击「导出 .md」可把当前笔记下载到本地。
  4. 数据保存在浏览器 localStorage,换电脑不会同步(可自行扩展云同步)。

可扩展方向

  • 接入 IndexedDB,支持大文本与附件
  • 增加全文搜索、标签筛选
  • 用 Service Worker 做成离线 PWA
  • 对接 WebDAV / 对象存储实现多端同步

安全与合规说明

  • 笔记数据仅存本地浏览器,请勿在公共电脑存放敏感信息。
  • 导出的文件请自行备份;清理浏览器数据会导致笔记丢失。
  • 本源码仅供学习交流,请勿用于违法违规用途。

小结

一个 HTML 文件就能搞定 Markdown 笔记:结构清晰、零依赖、易二次开发。若你想要加云同步 + 标签系统的进阶版,欢迎在评论区留言。

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容