火芯のMD Editor 是一款纯前端实现的在线Markdown编辑器,无需安装、跨平台访问、支持实时预览与多种格式导出。(项目更新不再更新文章!)
项目地址:http://blog.hxdxw.cn/index.php/18.html
技术栈:原生 JavaScript + HTML + CSS,无任何框架依赖
📑 目录
核心功能
- 📝 实时预览:左侧编辑、右侧预览,毫秒级响应
- 🔄 同步滚动:编辑区与预览区滚动联动
- 🎨 语法高亮:100+ 编程语言代码着色
- 📤 多格式导出:MD / HTML / PNG / PDF
- 📱 响应式设计:适配桌面端与移动端
- 🔌 离线可用:所有依赖本地化,CDN 回退保障
HTML 结构
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>火芯のMD editor</title>
<!-- CSS 模块化加载 -->
<link rel="stylesheet" href="style-base.css">
<link rel="stylesheet" href="style-layout.css">
<link rel="stylesheet" href="style-preview.css">
<link rel="stylesheet" href="style-modal.css">
<link rel="stylesheet" href="style-print.css">
<link rel="stylesheet" href="style-mobile.css">
<!-- 核心依赖 CSS -->
<link rel="stylesheet" href="lib/all.min.css">
<link rel="stylesheet" href="lib/github.min.css">
</head>
<body>
<!-- 加载动画 -->
<div id="loading-overlay">
<div class="loading-spinner"></div>
<div class="loading-text">加载中...</div>
</div>
<header>
<nav>
<div class="left">
<div class="logo">火芯のMD editor</div>
<div class="tools">
<!-- 工具栏按钮... -->
</div>
</div>
<!-- 导出下拉菜单 -->
<div class="export-dropdown">
<button id="export-btn">导出 <i class="fas fa-chevron-down"></i></button>
<div class="export-menu">
<button id="export-md">导出MD</button>
<button id="export-html">导出HTML</button>
<button id="export-png">导出PNG</button>
<button id="export-pdf">导出PDF</button>
</div>
</div>
</nav>
</header>
<main>
<div class="editor-container">
<div class="section-header">
<i class="fas fa-pen-fancy"></i>
<span>编辑</span>
<div id="word-count">字符: 0 | 词数: 0 | 行数: 0</div>
</div>
<textarea id="editor" placeholder="在这里输入Markdown..."></textarea>
</div>
<div class="preview-container">
<div class="section-header">
<i class="fas fa-eye"></i>
<span>预览</span>
</div>
<div id="preview"></div>
</div>
</main>
<!-- FAQ 折叠面板 -->
<div class="faq-container">
<!-- FAQ 内容... -->
</div>
<!-- 弹窗组件 -->
<div id="code-modal" class="modal">...</div>
<div id="link-modal" class="modal">...</div>
<div id="image-modal" class="modal">...</div>
<div id="table-modal" class="modal">...</div>
<div id="emoji-modal" class="modal">...</div>
<!-- JS 模块 -->
<script src="lib-loader.js"></script>
<script src="lib/html2canvas.min.js"></script>
<script src="lib/jspdf.umd.min.js"></script>
<script src="script-editor.js"></script>
<script src="script-tools.js"></script>
<script src="script-export.js"></script>
<script src="script-ui.js"></script>
<script src="sample-content.js"></script>
<footer>
<p>© Copyright 2026.04 <a href="https://blog.hxdxw.cn" target="_blank">火芯の博客</a></p>
</footer>
</body>
</html>设计要点:
- CSS 模块化拆分:基础、布局、预览、弹窗、打印、移动端
- 加载动画覆盖层:依赖加载期间显示加载状态
- 模态框复用:通过
class="modal"统一管理 - 按需加载:html2canvas 和 jsPDF 仅在导出时使用
CSS 架构
采用「原子化 + 模块化」策略,将样式拆分为多个文件:
style-base.css - 基础样式
/* 全局重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #24292e;
background-color: #fafbfc;
}
/* 按钮基础样式 */
button {
border: none;
background: none;
cursor: pointer;
font-family: inherit;
}
button:hover {
opacity: 0.8;
}
/* 链接样式 */
a {
color: #0366d6;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}style-layout.css - 布局样式
/* 头部布局 */
header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 50px;
background: #fff;
border-bottom: 1px solid #e1e4e8;
z-index: 100;
display: flex;
align-items: center;
padding: 0 20px;
}
header nav {
display: flex;
justify-content: space-between;
width: 100%;
}
header .left {
display: flex;
align-items: center;
gap: 20px;
}
header .logo {
font-weight: 600;
font-size: 16px;
white-space: nowrap;
}
header .tools {
display: flex;
gap: 4px;
}
/* 主内容区布局 */
main {
display: flex;
margin-top: 50px;
height: calc(100vh - 50px);
}
.editor-container,
.preview-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.editor-container {
border-right: 2px solid #ddd;
}
.section-header {
padding: 10px 15px;
background: #f6f8fa;
border-bottom: 1px solid #e1e4e8;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
#editor {
flex: 1;
padding: 15px;
border: none;
resize: none;
font-family: 'Fira Code', Consolas, monospace;
font-size: 14px;
line-height: 1.6;
outline: none;
}
#preview {
flex: 1;
padding: 15px 30px;
overflow-y: auto;
}style-preview.css - 预览区渲染样式
/* Markdown 渲染样式 */
#preview h1,
#preview h2,
#preview h3,
#preview h4,
#preview h5,
#preview h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
#preview h1 {
font-size: 2em;
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
}
#preview h2 {
font-size: 1.5em;
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
}
/* 段落与列表 */
#preview p {
margin-top: 0;
margin-bottom: 16px;
}
#preview ul,
#preview ol {
padding-left: 2em;
margin-bottom: 16px;
}
#preview li {
margin: 0.25em 0;
}
/* 代码块 */
#preview pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f6f8fa;
border-radius: 6px;
margin-bottom: 16px;
}
#preview code {
background-color: rgba(27, 31, 35, 0.06);
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
border-radius: 3px;
font-family: 'Fira Code', Consolas, monospace;
}
#preview pre code {
background-color: transparent;
padding: 0;
}
/* 表格 */
#preview table {
border-spacing: 0;
border-collapse: collapse;
margin-bottom: 16px;
width: 100%;
display: block;
overflow: auto;
}
#preview th,
#preview td {
padding: 6px 13px;
border: 1px solid #dfe2e5;
}
#preview th {
font-weight: 600;
background-color: #f6f8fa;
}
/* 行号样式 */
pre.line-numbers {
position: relative;
padding-left: 3.8em !important;
counter-reset: linenumber;
}
pre.line-numbers > code {
position: relative;
padding-left: 0 !important;
}
pre.line-numbers > code > .code-line {
display: block;
position: relative;
}
pre.line-numbers > code > .code-line::before {
counter-increment: linenumber;
content: counter(linenumber);
position: absolute;
left: -3.5em;
width: 3em;
text-align: right;
padding-right: 0.5em;
color: #999;
border-right: 1px solid #ddd;
margin-right: 0.5em;
user-select: none;
font-size: 0.9em;
}style-modal.css - 弹窗样式
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal.show {
display: flex;
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 8px;
min-width: 400px;
max-width: 600px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.modal-content h3 {
margin-bottom: 15px;
}
.modal-content label {
display: block;
margin-bottom: 10px;
}
.modal-content input,
.modal-content textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
margin-top: 4px;
}
.modal-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
.modal-buttons button {
padding: 8px 16px;
border-radius: 4px;
background: #0366d6;
color: #fff;
}
.modal-buttons button.cancel {
background: #ccc;
}style-print.css - 打印样式
@media print {
header,
.editor-container,
.faq-container,
.modal,
#loading-overlay {
display: none !important;
}
main {
margin-top: 0;
height: auto;
}
#preview {
padding: 0;
overflow: visible;
}
}style-mobile.css - 响应式适配
@media (max-width: 768px) {
main {
flex-direction: column;
}
.editor-container,
.preview-container {
height: 50%;
}
.editor-container {
border-right: none;
border-bottom: 2px solid #ddd;
}
header .tools {
flex-wrap: wrap;
overflow-x: auto;
}
header .tools button {
padding: 6px 10px;
font-size: 12px;
}
}JavaScript 模块
1. 模块化加载器:lib-loader.js
采用「本地优先 + CDN 回退」策略:
const LibLoader = {
isReady: false,
readyCallbacks: [],
loadWithFallback(type, localPath, cdnUrl, attrs = {}) {
return new Promise((resolve) => {
const element = type === 'link'
? document.createElement('link')
: document.createElement('script');
if (type === 'link') {
element.rel = 'stylesheet';
element.href = localPath;
} else {
element.src = localPath;
}
Object.assign(element, attrs);
element.onload = () => resolve({ source: 'local', path: localPath });
element.onerror = () => {
if (type === 'link') {
element.href = cdnUrl;
} else {
element.src = cdnUrl;
}
element.onerror = null;
element.onload = () => resolve({ source: 'cdn', path: cdnUrl });
};
document.head.appendChild(element);
});
},
async load() {
const basePath = 'lib/';
await Promise.all([
this.loadWithFallback('link', basePath + 'all.min.css', CDN_FALLBACKS.fontAwesome),
this.loadWithFallback('link', basePath + 'github.min.css', CDN_FALLBACKS.highlightCss),
this.loadWithFallback('script', basePath + 'marked.min.js', CDN_FALLBACKS.marked),
this.loadWithFallback('script', basePath + 'highlight.min.js', CDN_FALLBACKS.highlight)
]);
this.isReady = true;
this.readyCallbacks.forEach(cb => cb());
this.readyCallbacks = [];
},
whenReady(callback) {
if (this.isReady) {
callback();
} else {
this.readyCallbacks.push(callback);
}
}
};
LibLoader.load();设计亮点:
Promise.all并行加载,提升首屏速度- 回调队列机制,解决脚本执行顺序问题
- 静默降级,用户无感知
2. 全局状态管理:window 对象
使用 window 作为模块间通信的「中央总线」:
// script-editor.js - 状态定义
LibLoader.whenReady(function() {
window.editor = document.getElementById('editor');
window.preview = document.getElementById('preview');
window.isSyncing = false;
window.isFullscreen = false;
});
// script-tools.js - 状态消费
LibLoader.whenReady(function() {
const editor = window.editor;
const preview = window.preview;
window.isSyncing = true;
updatePreview();
window.isSyncing = false;
});关键机制:使用 isSyncing 标志位防止编辑区↔预览区的滚动同步形成死循环。
3. Markdown 渲染:updatePreview 函数
function updatePreview() {
// 核心:marked.js 解析 Markdown
preview.innerHTML = marked.parse(editor.value);
// 代码高亮处理
preview.querySelectorAll('pre code').forEach((block) => {
const code = block.textContent;
const language = block.className
.replace('language-', '')
.split(' ')[0] || '';
// 过滤末尾空行
let lines = code.split('\n');
if (lines[lines.length - 1] === '') {
lines = lines.slice(0, -1);
}
block.parentElement.classList.add('line-numbers');
// 逐行高亮
block.innerHTML = lines.map((line) => {
if (line.trim() === '') return '<span class="code-line"> </span>';
const highlighted = hljs.highlight(line, {
language: language || 'plaintext'
});
return `<span class="code-line">${highlighted.value}</span>`;
}).join('');
});
}性能优化:逐行高亮而非一次性处理,避免长代码块阻塞主线程。
4. PNG 导出
document.getElementById('export-png').addEventListener('click', () => {
const previewClone = preview.cloneNode(true);
const tempDiv = document.createElement('div');
tempDiv.style.cssText = 'position:fixed;left:-9999px;top:0;width:100%;padding:20px;background:#fff;';
tempDiv.innerHTML = previewClone.innerHTML;
document.body.appendChild(tempDiv);
html2canvas(tempDiv, {
allowTaint: true,
useCORS: true,
backgroundColor: '#ffffff',
scale: 2,
logging: false
}).then(canvas => {
const link = document.createElement('a');
link.download = getFilename('png');
link.href = canvas.toDataURL('image/png');
link.click();
document.body.removeChild(tempDiv);
}).catch(err => {
console.error('导出PNG失败:', err.message);
document.body.removeChild(tempDiv);
});
});5. PDF 导出:分页算法
document.getElementById('export-pdf').addEventListener('click', () => {
const { jsPDF } = window.jspdf;
const pdfWidth = 210;
const pdfHeight = 297;
const margin = 15;
const contentWidthMM = pdfWidth - (margin * 2);
const tempContainer = document.createElement('div');
tempContainer.style.cssText = 'position:fixed;left:-9999px;top:0;width:794px;background:#fff;padding:40px;box-sizing:border-box;';
tempContainer.innerHTML = preview.innerHTML;
document.body.appendChild(tempContainer);
// 等待所有图片加载完成
const images = tempContainer.querySelectorAll('img');
Promise.all(Array.from(images).map(img => {
return new Promise(resolve => {
if (img.complete) resolve();
else {
img.onload = resolve;
img.onerror = resolve;
}
});
})).then(() => {
return html2canvas(tempContainer, {
allowTaint: true,
useCORS: true,
backgroundColor: '#ffffff',
scale: 2,
width: 874
});
}).then(canvas => {
const pdf = new jsPDF('p', 'mm', 'a4');
const scale = canvas.width / contentWidthMM;
const pageHeightPx = (pdfHeight - margin * 2) * scale;
let sourceY = 0;
let pageIndex = 0;
while (sourceY < canvas.height) {
const remainingHeight = canvas.height - sourceY;
const sliceHeight = Math.min(pageHeightPx, remainingHeight);
const sliceCanvas = document.createElement('canvas');
sliceCanvas.width = canvas.width;
sliceCanvas.height = sliceHeight;
const ctx = sliceCanvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, sliceCanvas.width, sliceCanvas.height);
ctx.drawImage(canvas, 0, sourceY, canvas.width, sliceHeight,
0, 0, canvas.width, sliceHeight);
const imgHeightInMM = sliceHeight / scale;
if (pageIndex > 0) pdf.addPage();
pdf.addImage(sliceCanvas.toDataURL('image/png'), 'PNG',
margin, margin, contentWidthMM, imgHeightInMM);
sourceY += pageHeightPx;
pageIndex++;
}
document.body.removeChild(tempContainer);
pdf.save(getFilename('pdf'));
}).catch(err => {
console.error('导出PDF失败:', err.message);
});
});算法要点:
- 固定
canvas.width = 874px - 计算
scale = canvas.width / contentWidthMM作为转换桥梁 - 按
pageHeightPx分页裁剪 canvas - 预加载所有图片,避免导出时图片缺失
6. 工具栏插入:insertText 函数
function insertText(before, after = '', newline = false) {
if (editor.value.startsWith('# Markdown 语法教程')) {
editor.value = '';
updatePreview();
updateWordCount();
}
const start = editor.selectionStart;
const end = editor.selectionEnd;
const selected = editor.value.substring(start, end);
let replacement = before + selected + after;
if (newline) replacement += '\n';
editor.value = editor.value.substring(0, start)
+ replacement
+ editor.value.substring(end);
const newPos = start + before.length + selected.length
+ (newline ? after.length + 1 : after.length);
window.isSyncing = true;
updatePreview();
updateWordCount();
window.isSyncing = false;
editor.focus();
editor.setSelectionRange(newPos, newPos);
}
function insertHeading(level) {
insertText('#'.repeat(level) + ' ', '', true);
}
document.getElementById('bold').addEventListener('click', () => insertText('**', '**'));
document.getElementById('italic').addEventListener('click', () => insertText('*', '*'));
document.getElementById('h1').addEventListener('click', () => insertHeading(1));文件结构
markdown-editor/
├── index.html # 主页面
├── lib-loader.js # 依赖加载器
├── script-editor.js # 核心功能
├── script-tools.js # 工具栏
├── script-export.js # 导出功能
├── script-ui.js # UI 交互
├── sample-content.js # 示例内容
├── lib/ # 本地依赖
│ ├── marked.min.js
│ ├── highlight.min.js
│ ├── html2canvas.min.js
│ └── jspdf.umd.min.js
└── style-*.css # 样式模块
├── style-base.css # 基础重置
├── style-layout.css # 布局结构
├── style-preview.css # 预览渲染
├── style-modal.css # 弹窗组件
├── style-print.css # 打印适配
└── style-mobile.css # 响应式希望这篇解析对你理解前端项目开发有所帮助! 🚀
评论区:
还没有评论,快来抢沙发吧!