
前言
不想搭后端、不想装依赖,又希望有一个能随手记笔记的小工具?今天分享一套纯前端单文件 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, '&').replace(/</g, '<').replace(/>/g, '>');
}
// 轻量 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>
使用方法
- 把上面代码保存为
index.html。 - 双击用浏览器打开,即可开始写笔记。
- 点击「导出 .md」可把当前笔记下载到本地。
- 数据保存在浏览器 localStorage,换电脑不会同步(可自行扩展云同步)。
可扩展方向
- 接入 IndexedDB,支持大文本与附件
- 增加全文搜索、标签筛选
- 用 Service Worker 做成离线 PWA
- 对接 WebDAV / 对象存储实现多端同步
安全与合规说明
- 笔记数据仅存本地浏览器,请勿在公共电脑存放敏感信息。
- 导出的文件请自行备份;清理浏览器数据会导致笔记丢失。
- 本源码仅供学习交流,请勿用于违法违规用途。
小结
一个 HTML 文件就能搞定 Markdown 笔记:结构清晰、零依赖、易二次开发。若你想要加云同步 + 标签系统的进阶版,欢迎在评论区留言。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END






暂无评论内容