离线私密日记本:纯前端HTML/CSS/JS实现本地加密存储
2026/9/14 1:40:19 网站建设 项目流程

简介:这是一份轻量级、开箱即用的私密日记本网页模板,面向前端初学者与注重隐私记录的个人用户,解决本地化、安全化数字日记书写需求。资源以HTML单页应用形式实现,共4个核心文件:index.html为入口页面,style.css负责复古日记本视觉样式(含双页布局、纸张纹理与装订线等细节),script.js处理密码验证、localStorage数据持久化及富文本编辑逻辑,password.txt提供默认密码说明;整包仅9KB,便于快速部署与离线使用。已有67人学习下载,适合希望理解本地存储机制、密码保护流程与简易富文本编辑器实现原理的学习者。读者可直接运行体验完整功能:输入密码进入、编辑带格式文字、插入图片、设置日期标题,并在切换或退出时获得未保存提醒;所有数据仅存于浏览器本地,无服务器依赖,兼顾安全性与易用性。

1. 这不是普通 HTML 模板:一个运行在浏览器里的「离线私密日记本」,所有数据从不离开你的电脑

你打开index.html,输入密码,眼前展开的是一本棕褐色封面、带装订线与纸张纹理的双页笔记本——这不是网页渲染的“假效果”,而是用纯 HTML + CSS + JavaScript 实现的、完全离线运行的本地日记应用。它不调用任何后端接口,不上传数据到服务器,不依赖网络连接;所有日记内容通过localStorage直接写入浏览器本地存储区,关机重启后依然存在。它解决的不是“怎么建个博客”,而是“如何在没有云服务、不信任第三方、甚至断网状态下,安全记录敏感生活片段”的真实需求。适合对隐私有强意识的开发者、备考学生、心理咨询师笔记场景,或需要临时隔离环境记录信息的 IT 运维人员。它不提供账号体系、不支持多设备同步,恰恰是这种“功能克制”构成了它的安全边界:没有远程传输,就没有泄露路径;没有服务端逻辑,就无法被注入或劫持。


2. 密码验证与 localStorage 数据持久化:为什么它真能“锁住你的文字”

2.1 密码校验流程:从password.txt到 DOM 阻断的完整链路

该模板的密码并非硬编码在 JS 中,而是以明文形式存于同目录下的password.txt文件。这看似不安全,实则是为离线场景做的权衡设计:当用户双击打开index.html(即通过file://协议加载),浏览器出于安全策略会阻止fetch('password.txt')跨源读取本地文件——但该模板巧妙绕过了这一限制:它使用<script src="password.txt"></script>的方式尝试加载,而password.txt内容实际为一行 JS 赋值语句:

// password.txt 内容(示例) const PASSWORD = "mySecret2024";

提示:此设计仅适用于本地双击打开场景。若部署到 HTTP 服务器(如http://localhost:8080),需改用fetch+ CORS 配置,否则script标签加载.txt会报 MIME 类型错误。生产环境应替换为环境变量注入或服务端渲染。

script.js在页面加载时立即执行校验逻辑:

// script.js 片段:密码验证核心 function checkPassword() { const input = document.getElementById('passwordInput').value; if (typeof PASSWORD !== 'undefined' && input === PASSWORD) { document.body.classList.add('unlocked'); document.getElementById('loginScreen').style.display = 'none'; loadAllEntries(); // 加载 localStorage 中所有日记条目 } else { alert('密码错误,请重试'); } }

该函数绑定在登录按钮onclick上,未通过form.submit,避免页面刷新导致状态丢失。验证通过后,body添加unlocked类,CSS 通过.unlocked .page控制日记主区域显隐,实现无跳转解锁。

2.2 localStorage 存储结构:每篇日记如何被序列化与索引

所有日记数据以 JSON 格式存入localStorage,键名为diaryEntries。其值为数组,每个元素代表一篇日记,结构如下:

[ { "id": "20240521-153247", "title": "项目启动会议纪要", "date": "2024-05-21", "content": "<p><strong>参会人:</strong>张三、李四</p><p><mark>关键结论:</mark>采用微服务架构</p>", "createdAt": "2024-05-21T15:32:47.123Z" }, { "id": "20240522-091402", "title": "晨间随笔", "date": "2024-05-22", "content": "<p>阳光很好,咖啡微苦。</p><img src=\"data:image/png;base64,iVBOR...\" alt=\"手绘草图\">", "createdAt": "2024-05-22T09:14:02.456Z" } ]

注意:content字段直接存储富文本 HTML 字符串,包含<strong><mark><img>等标签。图片通过FileReader转为 base64 内嵌,确保单文件可迁移——这是该模板区别于其他“伪离线”日记本的关键:它不依赖外部图片路径,所有资源打包进localStorage

script.jssaveEntry()函数负责写入:

function saveEntry() { const id = document.getElementById('entryId').value || generateId(); const title = document.getElementById('entryTitle').value; const date = document.getElementById('entryDate').value; const content = document.getElementById('editor').innerHTML; // 直接取 contenteditable 区域 HTML const entries = JSON.parse(localStorage.getItem('diaryEntries') || '[]'); const existingIndex = entries.findIndex(e => e.id === id); if (existingIndex >= 0) { entries[existingIndex] = { id, title, date, content, createdAt: new Date().toISOString() }; } else { entries.push({ id, title, date, content, createdAt: new Date().toISOString() }); } localStorage.setItem('diaryEntries', JSON.stringify(entries)); updateSidebar(); // 刷新侧边栏列表 }

generateId()使用Date.now()+ 随机数生成唯一 ID,避免时间精度不足导致冲突。updateSidebar()遍历entries数组,动态生成<li>let isDirty = false; const editor = document.getElementById('editor'); // 监听编辑器内容变化(兼容 IE9+) editor.addEventListener('input', () => { isDirty = true; }); // 切换日记前检查 function switchEntry(entryId) { if (isDirty && !confirm('当前日记尚未保存,确定要切换吗?')) { return; // 阻止切换 } loadEntry(entryId); // 加载目标日记 isDirty = false; // 重置标志 } // 页面卸载前检查 window.addEventListener('beforeunload', (e) => { if (isDirty) { e.preventDefault(); e.returnValue = ''; // 触发浏览器确认弹窗 } });

提示:beforeunload在现代浏览器中已限制自定义提示文案,仅显示统一提示(如 Chrome 显示“您确定要离开此页面吗?”)。该设计不依赖文案说服力,而靠强制中断流程保障数据不丢失。


3. 仿真日记本 UI 实现:CSS 如何用纯前端还原纸质质感

3.1 双页布局与响应式装订线:Flexbox 与伪元素的组合技巧

日记主区域.book采用display: flex实现左右双页,并通过::before伪元素绘制居中装订线:

/* style.css 片段 */ .book { display: flex; justify-content: space-between; max-width: 1200px; margin: 0 auto; padding: 2rem 1rem; position: relative; } .book::before { content: ''; position: absolute; top: 0; bottom: 0; left: 50%; width: 4px; background: linear-gradient(to bottom, #8B4513, #5D2906); transform: translateX(-50%); box-shadow: 0 0 12px rgba(0,0,0,0.2); z-index: 10; } .page { width: 48%; min-height: 70vh; background: #fdf6e3; border: 1px solid #d4b98a; border-radius: 8px; padding: 1.5rem; position: relative; overflow-y: auto; box-shadow: 0 4px 12px rgba(0,0,0,0.08); } .page::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 100%; background: linear-gradient(rgba(255,255,255,0.8), rgba(255,255,255,0.8)), url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="20"><line x1="0" y1="10" x2="100" y2="10" stroke="%23d4b98a" stroke-width="0.5"/></svg>'); background-repeat: repeat-y; background-position: center; pointer-events: none; }

background中嵌入 SVG Base64 编码的横线图案,实现纸张纹理;box-shadow模拟纸张厚度阴影;border-radiusborder模拟旧书边缘磨损。width: 48%留出 4% 间隙供装订线占用,避免视觉挤压。

3.2 复古配色系统与 CSS 自定义属性的可维护性设计

模板未使用固定色值硬编码,而是通过 CSS 自定义属性建立色彩体系,便于快速主题切换:

:root { --primary-color: #8B4513; /* 鞣酸棕:封面与装订线主色 */ --page-bg: #fdf6e3; /* 米白:纸张底色 */ --line-color: #d4b98a; /* 暖灰褐:横线与边框色 */ --text-color: #332a1f; /* 深褐:正文文字 */ --highlight-color: #ffeb3b; /* 高亮黄:标记色 */ --accent-color: #ff9800; /* 橙色:按钮与强调色 */ } .book { background-color: var(--page-bg); } .page { border-color: var(--line-color); } .editor-content p { color: var(--text-color); } .mark { background-color: var(--highlight-color); } .btn-primary { background-color: var(--accent-color); }

注意:--highlight-color默认设为#ffeb3b(黄色),但script.js中颜色选择器支持动态修改style.setProperty('--highlight-color', selectedHex),实现运行时主题微调。

3.3 响应式适配与移动端交互优化:媒体查询与触摸事件补全

针对小屏设备,模板在@media (max-width: 768px)下强制切换单页模式:

@media (max-width: 768px) { .book { flex-direction: column; } .book::before { display: none; /* 移动端隐藏装订线 */ } .page { width: 100%; } .sidebar { position: static; width: 100%; } }

同时,为兼容 iOS Safari 的contenteditable焦点问题,script.js注入了触摸增强逻辑:

// 修复 iOS Safari 点击编辑器不聚焦问题 if (/iPad|iPhone|iPod/.test(navigator.userAgent)) { document.getElementById('editor').addEventListener('touchstart', function(e) { if (!this.hasAttribute('contenteditable')) { this.setAttribute('contenteditable', 'true'); this.focus(); e.preventDefault(); } }, { passive: false }); }

4. 富文本编辑与图片内嵌:contenteditable 的深度定制实践

4.1 工具栏命令映射:document.execCommand的封装与降级处理

编辑器工具栏按钮(粗体、斜体等)全部绑定execCommand,但做了兼容性兜底:

<!-- index.html 工具栏 --> <div class="toolbar"> <button onclick="formatText('bold')" title="加粗">B</button> <button onclick="formatText('italic')" title="斜体">I</button> <button onclick="formatText('underline')" title="下划线">U</button> <button onclick="formatText('strikethrough')" title="删除线">S</button> <button onclick="formatText('backColor', '#ffeb3b')" title="高亮">HL</button> </div>
// script.js 封装函数 function formatText(command, value = null) { // 确保编辑器获得焦点 const editor = document.getElementById('editor'); editor.focus(); try { // 执行原生命令 document.execCommand(command, false, value); } catch (e) { // 降级:手动包裹选中文本 if (command === 'bold') { wrapSelectionWith('<strong>', '</strong>'); } else if (command === 'italic') { wrapSelectionWith('<em>', '</em>'); } else if (command === 'backColor') { wrapSelectionWith(`<span style="background-color:${value}">`, '</span>'); } } } function wrapSelectionWith(openTag, closeTag) { const sel = window.getSelection(); if (sel.rangeCount > 0) { const range = sel.getRangeAt(0); const fragment = range.extractContents(); const wrapper = document.createElement('span'); wrapper.innerHTML = openTag + fragment.textContent + closeTag; range.insertNode(wrapper); } }

提示:execCommand已被标记为废弃,但目前仍是contenteditable最可靠方案。wrapSelectionWith作为降级路径,仅处理纯文本包裹,不解析 HTML 结构,避免嵌套污染。

4.2 图片插入的全流程:FileReader + Canvas 压缩 + Base64 内嵌

点击“插入图片”按钮后,触发文件选择,经压缩后转为 base64 写入编辑器:

function insertImage() { const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; input.onchange = function(e) { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(event) { const img = new Image(); img.onload = function() { // 创建 Canvas 压缩至最大宽度 800px const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const maxWidth = 800; let width = img.width; let height = img.height; if (width > maxWidth) { height *= maxWidth / width; width = maxWidth; } canvas.width = width; canvas.height = height; ctx.drawImage(img, 0, 0, width, height); // 转为压缩 base64(质量 0.8) const compressedBase64 = canvas.toDataURL('image/jpeg', 0.8); // 插入编辑器 const editor = document.getElementById('editor'); const imgTag = `<img src="${compressedBase64}" alt="插入图片" style="max-width:100%;height:auto;">`; document.execCommand('insertHTML', false, imgTag); }; img.src = event.target.result; }; reader.readAsDataURL(file); }; input.click(); }

注意:toDataURL('image/jpeg')强制转 JPEG,比 PNG 小 40%~60%,且localStorage容量有限(通常 5~10MB),压缩是必要步骤。maxWidth限制防止大图撑爆布局。

4.3 日期与标题的元数据管理:独立字段与 DOM 同步策略

日记的datetitle不存于contentHTML 中,而是分离为独立表单字段:

<input type="date" id="entryDate" class="form-control"> <input type="text" id="entryTitle" placeholder="给这篇日记起个名字..." class="form-control"> <div id="editor" contenteditable="true" class="editor-content"></div>

loadEntry(entryId)函数从localStorage读取数据后,分别填充这三个字段:

function loadEntry(entryId) { const entries = JSON.parse(localStorage.getItem('diaryEntries') || '[]'); const entry = entries.find(e => e.id === entryId); if (entry) { document.getElementById('entryId').value = entry.id; document.getElementById('entryTitle').value = entry.title || ''; document.getElementById('entryDate').value = entry.date || getCurrentDate(); document.getElementById('editor').innerHTML = entry.content || '<p><br></p>'; isDirty = false; // 加载后重置脏标志 } }

getCurrentDate()返回YYYY-MM-DD格式字符串,确保input[type=date]兼容。这种分离设计使datetitle可被侧边栏列表直接读取(无需解析 HTML),提升updateSidebar()性能。


5. 数据导出与本地备份:如何将 localStorage 日记打包为可迁移 ZIP

5.1 JSON 导出功能:一键下载结构化数据文件

模板未内置 ZIP 打包,但提供了标准 JSON 导出入口,为后续迁移打下基础。点击“导出数据”按钮,触发以下逻辑:

function exportData() { const entries = JSON.parse(localStorage.getItem('diaryEntries') || '[]'); const dataStr = JSON.stringify(entries, null, 2); // 格式化缩进 const blob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `diary-export-${new Date().toISOString().slice(0,10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }

导出的 JSON 文件可被任意文本编辑器查看,也可用 Python 脚本批量转换为 Markdown 或 PDF:

# 示例:Python 转 Markdown(保存为 export_to_md.py) import json with open('diary-export-2024-05-21.json', 'r', encoding='utf-8') as f: entries = json.load(f) for entry in entries: md_file = f"diary_{entry['id']}.md" with open(md_file, 'w', encoding='utf-8') as f: f.write(f"# {entry['title']}\n") f.write(f"**日期:** {entry['date']}\n\n") # 简单 HTML to MD 转换(仅处理基础标签) content = entry['content'].replace('<p>', '').replace('</p>', '\n') content = content.replace('<strong>', '**').replace('</strong>', '**') content = content.replace('<em>', '*').replace('</em>', '*') f.write(content)

5.2 本地 ZIP 打包方案:使用 JSZip 库实现浏览器端压缩

若需真正 ZIP 包(含index.htmlstyle.cssscript.js及导出 JSON),可引入轻量库jszip(仅 35KB):

npm install jszip # 或直接引入 CDN <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>

script.js中扩展导出函数:

async function exportAsZip() { const JSZip = (await import('https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js')).default; const zip = new JSZip(); // 添加日记数据 const entries = JSON.parse(localStorage.getItem('diaryEntries') || '[]'); zip.file('diary-data.json', JSON.stringify(entries, null, 2)); // 添加静态资源(需提前 fetch) const files = ['index.html', 'style.css', 'script.js']; for (const file of files) { const res = await fetch(file); const content = await res.text(); zip.file(file, content); } const content = await zip.generateAsync({ type: 'blob' }); const url = URL.createObjectURL(content); const a = document.createElement('a'); a.href = url; a.download = `diary-backup-${new Date().toISOString().slice(0,10)}.zip`; a.click(); URL.revokeObjectURL(url); }

提示:fetch读取同目录文件依赖 HTTP 服务器环境(file://协议下会跨域失败)。部署到http-servernginx后即可使用。

5.3 重置与清空:安全擦除本地数据的不可逆操作

“清空所有日记”功能执行彻底清除,无回收站:

function clearAllEntries() { if (!confirm('确定要永久删除所有日记?此操作不可撤销!')) return; localStorage.removeItem('diaryEntries'); document.getElementById('sidebar').innerHTML = '<li>暂无日记</li>'; document.getElementById('editor').innerHTML = '<p><br></p>'; document.getElementById('entryTitle').value = ''; document.getElementById('entryDate').value = getCurrentDate(); document.getElementById('entryId').value = ''; isDirty = false; }

该函数直接调用localStorage.removeItem(),不保留任何残留键值。对于高敏用户,建议在清空后手动打开浏览器开发者工具 → Application → Storage → Clear storage,确保localStoragesessionStorage、缓存全清。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询