公司动态
前端表单加密笔记本实现:从Web Crypto API到数据持久化
1. 项目概述从表单到加密笔记本的完整实现最近在做一个个人知识管理的小工具核心需求很简单通过一个网页表单Form来新建笔记然后能保存到本地或服务器并且还得给笔记内容上个“锁”——也就是加密功能。听起来像是很多笔记应用的基础功能但真自己动手从零实现一遍里面涉及的前端交互、数据流处理和加密逻辑踩的坑可不少。特别是当你深入去想“点击保存按钮后到底是先执行我的JavaScript事件还是先把数据提交出去”这种细节时就会发现一个健壮的功能远不是把几个标签拼起来那么简单。这个项目非常适合前端初学者进阶或者想深入理解Web应用数据流和基础安全概念的开发者。通过实现一个带加密的笔记本你会亲手串联起HTML表单处理、JavaScript事件循环、前端加密算法以及数据持久化存储这几个关键环节。最终你将得到一个可以实际运行、保护隐私的笔记工具原型更重要的是你能彻底弄明白这些技术点是如何协同工作的而不是停留在API调用的层面。2. 核心需求与方案设计拆解2.1 功能需求深度解析首先我们把“form实现note笔记本新建保存加密功能”这个标题拆解成三个核心动作新建、保存、加密。这每一个动作背后都对应着一系列具体的技术决策。新建Create这不仅仅是呈现一个空白的输入框。它意味着要有一个用户界面UI通常是一个包含标题、内容、标签等字段的HTML表单form。更深一层“新建”还隐含着状态管理——当前编辑的笔记是全新的还是正在修改已有的笔记这会影响后续的保存逻辑。保存Save这是数据持久化的过程。保存到哪里常见的有几种选择浏览器本地存储如localStorage或IndexedDB。优点是简单、快速无需后端数据保存在用户本地浏览器。缺点是容量有限通常5-10MB且数据无法跨设备同步。后端服务器通过Ajax如Fetch API将表单数据提交到后端接口存入数据库。优点是可扩展、能同步、更安全密钥可服务器管理。缺点是需要完整的后端支持。文件系统通过现代浏览器的 File System Access API 直接将笔记保存为用户设备上的文件。更贴近桌面应用体验但API较新兼容性需考虑。 对于这个项目为了聚焦前端和加密逻辑我们优先选择localStorage作为存储方案。它足够演示完整流程且复杂度适中。加密Encrypt这是保障隐私的关键。我们需要在保存之前对笔记的敏感内容通常是正文进行加密在读取时进行解密。这里有几个关键决策点加密什么通常只加密笔记正文标题和创建时间等元数据可以明文存储便于索引和展示列表。何时加密在数据离开前端、即将被持久化保存到localStorage或发送给服务器之前。使用什么加密算法在前端环境中应使用成熟的、经过广泛验证的加密库。AESAdvanced Encryption Standard是对称加密算法的首选它速度快、安全性高。我们将使用浏览器的 Web Crypto API 来实现AES-GCM模式该模式同时提供加密和完整性验证。密钥如何管理这是前端加密最核心也最棘手的问题。密钥不能硬编码在代码里那等于没加密。通常方案是由用户输入一个密码Passphrase然后通过PBKDF2一种密钥派生函数将这个密码“加工”成符合AES要求的加密密钥。这个用户密码必须妥善保管一旦丢失加密数据将无法恢复。2.2 技术栈与工具选型基于以上分析我们确定具体的技术实现方案前端框架/库为了最清晰地展示原理我们使用原生HTML、CSS和JavaScriptES6实现不引入大型框架。这样能避免框架抽象带来的理解成本让每一步逻辑都清晰可见。当然你可以很容易地将这些逻辑移植到Vue、React等框架中。UI组件使用原生HTML表单元素input,textarea构建界面。为了更好的用户体验可以引入一个轻量级的CSS框架如Pure.css或Milligram来快速获得整洁的样式。加密库使用浏览器内置的Web Crypto API。这是一个原生的、性能优异的加密接口支持AES、SHA、PBKDF2等算法无需引入第三方库安全性也有保障。数据存储使用window.localStorage。它是一个简单的键值对存储API极其易用setItem,getItem非常适合存储结构相对简单的笔记列表JSON序列化后的字符串。构建与开发不需要复杂的构建工具。一个现代的浏览器Chrome 80、Firefox 75和一个代码编辑器如VS Code即可。我们可以通过浏览器内置的开发者工具DevTools进行调试。注意关于前端加密的局限性必须清醒认识到任何运行在用户浏览器里的JavaScript加密其安全性最终都依赖于用户设备的完整性。恶意浏览器扩展、被篡改的CDN资源、XSS攻击等都可能导致密钥或明文泄露。因此这种方案适用于“防窥屏”或防止简单的数据泄露场景如云存储服务商窥探绝不能替代传输层加密HTTPS和后端安全措施。它的核心价值在于即使存储介质如localStorage数据文件被直接窃取攻击者也无法在没有密码的情况下读取内容。3. 核心实现细节与实操要点3.1 构建笔记表单与处理提交事件这是所有功能的起点。我们先构建一个最基础的笔记编辑表单。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title加密笔记本/title link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/milligram1.4.1/dist/milligram.min.css style .container { max-width: 800px; margin: 50px auto; padding: 20px; } .note-list { margin-top: 30px; border-top: 1px solid #eee; padding-top: 20px; } .note-item { padding: 10px; border-bottom: 1px solid #f5f5f5; } .note-item:hover { background-color: #f9f9f9; } /style /head body div classcontainer h1我的加密笔记本/h1 form idnoteForm fieldset label fortitle笔记标题/label input typetext idtitle nametitle placeholder请输入标题... required label forcontent笔记内容/label textarea idcontent namecontent rows10 placeholder开始记录... required/textarea label forpassword加密密码/label input typepassword idpassword namepassword placeholder用于加密内容的密码 required small请牢记此密码丢失将无法解密笔记。/small input typesubmit classbutton-primary value保存笔记 button typebutton classbutton idbtnNew新建空白笔记/button /fieldset /form div classnote-list h3笔记列表/h3 div idnotesContainer !-- 笔记列表将通过JS动态加载 -- /div /div /div script srcapp.js/script /body /html接下来是关键的JavaScript部分我们需要处理表单的提交事件。这里就必须回答热词中的那个问题form表单提交按钮点击事件先触发还是提交数据先触发在标准的HTML表单提交流程中当你点击typesubmit的按钮时顺序是这样的按钮的click事件触发如果监听了。表单的submit事件触发。浏览器执行默认的提交行为即根据form的action和method组装数据并跳转或刷新页面。我们的目标是完全在前端处理数据必须阻止第3步的默认提交行为否则页面会刷新所有状态丢失。因此我们应该监听表单的submit事件而不是按钮的click事件。在submit事件的处理函数中我们拿到表单数据执行加密和保存逻辑最后调用event.preventDefault()来阻止表单默认提交。// app.js document.addEventListener(DOMContentLoaded, function() { const noteForm document.getElementById(noteForm); const notesContainer document.getElementById(notesContainer); // 核心监听表单提交事件 noteForm.addEventListener(submit, async function(event) { // 关键一步阻止表单默认提交行为避免页面刷新 event.preventDefault(); // 1. 收集表单数据 const title document.getElementById(title).value.trim(); const content document.getElementById(content).value.trim(); const password document.getElementById(password).value; // 简单的客户端验证 if (!title || !content || !password) { alert(标题、内容和密码均为必填项); return; } // 2. 对笔记内容进行加密 let encryptedContent; try { // 这里调用我们即将实现的加密函数 encryptedContent await encryptContent(content, password); } catch (error) { console.error(加密失败:, error); alert(加密过程出错请重试。); return; } // 3. 构建笔记对象 const note { id: Date.now(), // 使用时间戳作为简单ID title: title, // 标题明文存储 content: encryptedContent, // 内容存储加密后的数据 iv: encryptedContent.iv, // 存储初始化向量解密时需要 createdAt: new Date().toISOString(), // 注意我们不存储密码和原始密钥 }; // 4. 保存到本地存储 saveNoteToLocalStorage(note); // 5. 更新UI清空表单 alert(笔记保存成功); noteForm.reset(); loadNotes(); // 重新加载笔记列表 }); // “新建”按钮事件仅仅清空表单 document.getElementById(btnNew).addEventListener(click, function() { noteForm.reset(); // 可以在这里清除一个可能存在的“当前编辑笔记ID”以区分新建和编辑 }); // 页面加载时显示已有的笔记列表 loadNotes(); });3.2 基于Web Crypto API的加密解密实现这是本项目的技术核心。我们将使用 Web Crypto API 的 AES-GCM 算法。GCM模式的好处是它不仅加密还会生成一个认证标签Authentication Tag用于验证密文在传输或存储过程中是否被篡改。加密流程如下密钥派生用户输入的密码字符串不是直接可用的加密密钥。我们需要使用PBKDF2算法结合一个随机盐salt将其“拉伸”成一个符合AES要求的CryptoKey对象。盐是随机生成的需要和加密数据一起保存用于解密时再次派生相同的密钥。加密使用派生出的密钥结合一个随机生成的初始化向量IVInitialization Vector对明文内容进行加密。IV也必须保存用于解密。数据组装将加密后的密文、IV、盐如果用了以及算法标识打包成一个对象方便存储和传递。// app.js - 加密解密工具函数 /** * 使用AES-GCM算法加密文本内容 * param {string} plaintext - 要加密的明文 * param {string} password - 用户输入的密码 * returns {Promise{ciphertext: string, iv: string, salt: string}} 返回包含密文、IV和盐的对象 */ async function encryptContent(plaintext, password) { // 1. 将文本编码为ArrayBuffer const textEncoder new TextEncoder(); const data textEncoder.encode(plaintext); // 2. 生成随机盐Salt和初始化向量IV const salt window.crypto.getRandomValues(new Uint8Array(16)); const iv window.crypto.getRandomValues(new Uint8Array(12)); // GCM推荐IV长度为12字节 // 3. 从密码派生加密密钥 const passwordKey await window.crypto.subtle.importKey( raw, textEncoder.encode(password), PBKDF2, false, [deriveKey] ); const key await window.crypto.subtle.deriveKey( { name: PBKDF2, salt: salt, iterations: 100000, // 迭代次数增加破解难度 hash: SHA-256 }, passwordKey, { name: AES-GCM, length: 256 }, // 派生一个256位的AES-GCM密钥 false, // 是否可导出密钥这里设为false更安全 [encrypt, decrypt] // 密钥用途 ); // 4. 执行加密 const ciphertext await window.crypto.subtle.encrypt( { name: AES-GCM, iv: iv }, key, data ); // 5. 将ArrayBuffer转换为Base64字符串方便用JSON存储 return { ciphertext: arrayBufferToBase64(ciphertext), iv: arrayBufferToBase64(iv), salt: arrayBufferToBase64(salt) }; } /** * 解密函数 * param {{ciphertext: string, iv: string, salt: string}} encryptedData - 加密数据对象 * param {string} password - 用户输入的密码 * returns {Promisestring} 解密后的明文 */ async function decryptContent(encryptedData, password) { const { ciphertext, iv, salt } encryptedData; // 1. 将Base64字符串转换回ArrayBuffer const ciphertextBuffer base64ToArrayBuffer(ciphertext); const ivBuffer base64ToArrayBuffer(iv); const saltBuffer base64ToArrayBuffer(salt); const textEncoder new TextEncoder(); // 2. 使用相同的盐和密码派生密钥必须与加密时完全一致 const passwordKey await window.crypto.subtle.importKey( raw, textEncoder.encode(password), PBKDF2, false, [deriveKey] ); const key await window.crypto.subtle.deriveKey( { name: PBKDF2, salt: saltBuffer, iterations: 100000, hash: SHA-256 }, passwordKey, { name: AES-GCM, length: 256 }, false, [decrypt] ); // 3. 执行解密 try { const decryptedBuffer await window.crypto.subtle.decrypt( { name: AES-GCM, iv: ivBuffer }, key, ciphertextBuffer ); // 4. 将解密后的ArrayBuffer解码为字符串 const textDecoder new TextDecoder(); return textDecoder.decode(decryptedBuffer); } catch (error) { // 解密失败通常是因为密码错误、数据被篡改或IV/Salt不匹配 console.error(解密失败:, error); throw new Error(解密失败。请检查密码是否正确或数据是否完整。); } } /** * 工具函数ArrayBuffer 转 Base64 */ function arrayBufferToBase64(buffer) { const bytes new Uint8Array(buffer); let binary ; for (let i 0; i bytes.byteLength; i) { binary String.fromCharCode(bytes[i]); } return window.btoa(binary); } /** * 工具函数Base64 转 ArrayBuffer */ function base64ToArrayBuffer(base64) { const binaryString window.atob(base64); const bytes new Uint8Array(binaryString.length); for (let i 0; i binaryString.length; i) { bytes[i] binaryString.charCodeAt(i); } return bytes.buffer; }实操心得关于PBKDF2迭代次数代码中iterations: 100000这个值需要权衡。迭代次数越多从密码派生密钥的时间就越长暴力破解的难度也呈指数级增长但也会增加用户等待时间尤其是在性能较弱的设备上。10万次对于现代浏览器是一个比较合理的起点能在安全性和用户体验间取得平衡。如果构建更严肃的应用可以考虑根据设备性能动态调整这个值或者使用scrypt或argon2这类内存硬算法可能需要引入第三方库它们能更好地抵抗硬件破解。3.3 数据持久化与本地存储管理加密后的数据需要保存起来。我们使用localStorage但直接存储对象是不行的它只能存字符串。我们需要将笔记列表序列化为JSON字符串。这里的设计是在localStorage中用一个固定的键例如encryptedNotes来存储所有笔记的数组。每次新增、删除或修改都操作这个数组然后整体写回localStorage。// app.js - 数据持久化函数 const STORAGE_KEY encrypted_notes_app_data; /** * 将单条笔记保存到本地存储 * param {Object} note - 笔记对象 */ function saveNoteToLocalStorage(note) { // 1. 从本地存储读取现有笔记列表 let notes []; try { const stored localStorage.getItem(STORAGE_KEY); if (stored) { notes JSON.parse(stored); // 简单验证是否是数组 if (!Array.isArray(notes)) { notes []; } } } catch (e) { console.error(读取本地存储失败将创建新列表:, e); notes []; } // 2. 检查是新增还是更新通过ID判断 const existingIndex notes.findIndex(n n.id note.id); if (existingIndex 0) { // 更新已有笔记 notes[existingIndex] note; } else { // 新增笔记 notes.push(note); } // 3. 将更新后的列表写回本地存储 try { localStorage.setItem(STORAGE_KEY, JSON.stringify(notes)); } catch (e) { console.error(写入本地存储失败:, e); alert(保存失败可能是存储空间已满。); } } /** * 从本地存储加载所有笔记 * returns {Array} 笔记对象数组 */ function loadNotesFromLocalStorage() { try { const stored localStorage.getItem(STORAGE_KEY); if (stored) { const notes JSON.parse(stored); return Array.isArray(notes) ? notes : []; } } catch (e) { console.error(加载笔记失败:, e); } return []; } /** * 从本地存储删除指定ID的笔记 * param {number|string} noteId - 笔记ID */ function deleteNoteFromLocalStorage(noteId) { const notes loadNotesFromLocalStorage(); const filteredNotes notes.filter(note note.id ! noteId); try { localStorage.setItem(STORAGE_KEY, JSON.stringify(filteredNotes)); } catch (e) { console.error(删除笔记失败:, e); } }3.4 笔记列表渲染与解密查看保存之后我们需要在页面上展示笔记列表。列表只显示标题和创建时间等明文信息。当用户想查看某条笔记的详情时需要弹出一个密码输入框解密内容后显示。// app.js - 渲染与交互函数 /** * 加载并渲染笔记列表 */ async function loadNotes() { const notesContainer document.getElementById(notesContainer); const notes loadNotesFromLocalStorage(); if (notes.length 0) { notesContainer.innerHTML p暂无笔记创建你的第一条加密笔记吧/p; return; } let html ; for (const note of notes) { // 注意note.content 是加密后的字符串我们不解密它只显示元数据 html div classnote-item>// 示例创建并显示一个编辑模态框 function showEditModal(noteData, decryptedContent) { // 创建模态框DOM元素 const modal document.createElement(div); modal.style.cssText position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 1000;; const modalContent document.createElement(div); modalContent.style.cssText background: white; padding: 2rem; border-radius: 8px; min-width: 500px; max-width: 80%; max-height: 80%; overflow-y: auto;; modalContent.innerHTML h3编辑笔记/h3 form ideditNoteForm label标题/label input typetext ideditTitle value${escapeHtml(noteData.title)} classu-full-width label内容/label textarea ideditContent rows15 classu-full-width${escapeHtml(decryptedContent)}/textarea label密码用于重新加密/label input typepassword ideditPassword placeholder输入密码以保存更改 classu-full-width div stylemargin-top: 1rem; text-align: right; button typebutton ideditCancel classbutton取消/button button typesubmit classbutton-primary保存更改/button /div /form ; modal.appendChild(modalContent); document.body.appendChild(modal); // 处理表单提交 modalContent.querySelector(#editNoteForm).addEventListener(submit, async function(e) { e.preventDefault(); const newTitle document.getElementById(editTitle).value.trim(); const newContent document.getElementById(editContent).value.trim(); const newPassword document.getElementById(editPassword).value; if (!newPassword) { alert(必须提供密码才能保存加密内容。); return; } try { const newEncrypted await encryptContent(newContent, newPassword); const updatedNote { ...noteData, title: newTitle, content: newEncrypted.ciphertext, iv: newEncrypted.iv, salt: newEncrypted.salt }; saveNoteToLocalStorage(updatedNote); alert(更新成功); document.body.removeChild(modal); loadNotes(); } catch (error) { alert(加密保存失败 error.message); } }); // 处理取消 modalContent.querySelector(#editCancel).addEventListener(click, function() { document.body.removeChild(modal); }); }然后在promptForPasswordAndViewNote函数解密成功后调用showEditModal(noteData, decryptedContent)代替简单的prompt。其他体验优化点密码确认在保存新笔记时增加一个“确认密码”输入框减少输错密码导致笔记永久锁死的风险。密码强度提示实时检查用户输入的密码强度长度、复杂度给予提示。自动保存草稿利用localStorage或sessionStorage在用户输入时定时保存未加密的草稿防止浏览器意外关闭导致内容丢失。搜索功能由于标题是明文存储的可以实现基于标题的简单搜索。若想搜索加密内容则需在加密前建立索引或使用可搜索加密技术非常复杂通常不用于前端。4.2 数据备份、导出与导入localStorage的数据绑定在特定域名和浏览器上。我们需要提供导出/导入功能让用户能备份数据或迁移到其他设备。导出将localStorage中encrypted_notes_app_data键对应的值已经是JSON字符串直接提供给用户下载为一个.json文件。function exportNotes() { const data localStorage.getItem(STORAGE_KEY) || []; const blob new Blob([data], { type: application/json }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download encrypted_notes_backup_${new Date().toISOString().slice(0,10)}.json; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }导入读取用户选择的JSON文件解析后与现有数据合并注意处理ID冲突然后写回localStorage。function importNotesFromFile(file) { const reader new FileReader(); reader.onload function(e) { try { const importedNotes JSON.parse(e.target.result); if (!Array.isArray(importedNotes)) { throw new Error(文件格式无效); } // 简单的合并策略追加并更新ID避免冲突 const existingNotes loadNotesFromLocalStorage(); const maxId existingNotes.reduce((max, note) Math.max(max, note.id), 0); importedNotes.forEach((note, index) { note.id maxId index 1; // 重新分配ID }); const mergedNotes [...existingNotes, ...importedNotes]; localStorage.setItem(STORAGE_KEY, JSON.stringify(mergedNotes)); loadNotes(); alert(成功导入 ${importedNotes.length} 条笔记); } catch (error) { alert(导入失败 error.message); } }; reader.readAsText(file); }4.3 提升安全性与健壮性考量加密算法参数固定化在encryptContent函数中我们将算法名称AES-GCM、密钥长度256、PBKDF2迭代次数100000、盐和IV长度都硬编码了。这是为了确保加密和解密时参数一致。在实际产品中这些参数可以作为元数据与密文一起存储以便未来算法升级但初期固定化能避免很多兼容性问题。错误处理与用户反馈加密解密是可能失败的密码错误、数据损坏。我们用了try...catch并给用户明确的错误提示如“解密失败密码错误”而不是晦涩的控制台错误。避免内存中的密码残留密码字符串在JavaScript内存中直到被垃圾回收。虽然我们无法完全控制但可以在使用后尽快清除引用。例如在加密函数末尾可以将保存密码的变量置空password null;但这在函数作用域内效果有限。更安全的环境需要考虑使用 Web Workers 在独立线程中处理密钥或使用CryptoKey对象本身它比字符串更难被恶意脚本提取。内容安全策略CSP如果部署到线上务必设置严格的CSP头部防止XSS攻击导致恶意脚本读取localStorage数据或窃听密码输入。例如禁止内联脚本和不安全的外部脚本。考虑使用 IndexedDB当笔记数量增多、单条笔记内容很大比如包含图片时localStorage的5-10MB容量可能不够。IndexedDB提供了更大的异步存储空间和更复杂的数据查询能力是更好的选择。迁移时只需修改saveNoteToLocalStorage和loadNotesFromLocalStorage这两个函数内部的实现即可。5. 常见问题与排查技巧实录在实际开发和测试中你可能会遇到以下问题。这里记录了我的排查思路和解决方法。5.1 加密解密相关错误问题现象可能原因排查步骤与解决方案解密时抛出DOMException1.密码错误这是最常见的原因。派生出的密钥与加密时不同解密必然失败。2.加密数据被破坏存储或传输过程中密文、IV或Salt的字符串被意外修改如编码错误。3.算法参数不匹配加密和解密时使用的算法名称、密钥长度、IV长度等不一致。1.确认密码让用户再次确认输入的密码注意大小写和特殊字符。2.检查数据完整性在保存和加载时打印或记录encryptedData对象对比加密后和解密前的内容是否完全一致Base64字符串。确保使用JSON.stringify和JSON.parse时没有数据丢失。3.核对算法代码确保encryptContent和decryptContent函数中所有Web Crypto API的参数name,length,iterations,hash完全一致。强烈建议将这些参数定义为常量在两个函数中引用同一个常量。window.crypto.subtle为undefined浏览器运行在非安全上下文非HTTPS且非localhost。Web Crypto API 的subtle属性仅在安全上下文中可用。开发时确保通过http://localhost或https://访问页面。部署时必须使用HTTPS协议。现在有很多免费的SSL证书服务如Let‘s Encrypt和托管平台Vercel, Netlify等都默认提供HTTPS。加密后的数据非常长将二进制数据ArrayBuffer转换为Base64字符串后体积会增加约33%。这是正常的。AES-GCM加密本身也会增加一些认证标签的长度。这是预期行为。如果存储空间紧张可以考虑1. 使用localStorage的压缩如pako库压缩JSON字符串但注意加密数据本身压缩率很低。2. 迁移到IndexedDB。3. 评估是否所有字段都需要加密或许只加密核心正文字段。5.2 表单与数据流问题问题现象可能原因排查步骤与解决方案点击保存按钮页面刷新数据丢失表单的submit事件没有被正确阻止默认行为。检查表单submit事件监听器1. 是否使用了event.preventDefault()2. 事件监听器是否成功绑定在回调函数开头加console.log测试。3. 确保监听的是form的submit事件而不是button的click事件。保存后笔记列表没有立即更新saveNoteToLocalStorage执行后没有调用loadNotes()来刷新UI。在保存笔记的函数末尾确保执行了更新UI的逻辑。如果涉及异步加密确保在Promise解析then或async/await后再调用loadNotes()。localStorage存储失败1.存储空间已满单个域名下的localStorage通常有5MB限制。2.浏览器隐私模式有些浏览器在隐私模式下可能禁用localStorage或会在会话结束后清除。3.数据格式错误尝试存储非字符串数据。1. 用try...catch包裹localStorage.setItem在catch中提示用户。2. 在隐私模式下测试并告知用户该限制。考虑降级方案如临时使用内存存储并提示。3. 确保存储前使用JSON.stringify。5.3 性能与兼容性问题问题现象可能原因排查步骤与解决方案保存或解密笔记时界面“卡住”几秒钟PBKDF2的迭代次数设置过高例如超过50万次在性能较弱的设备如旧手机、低端PC上会导致主线程阻塞。1.减少迭代次数可以尝试降到10万次或5万次这是一个安全与性能的平衡点。2.使用Web Workers将耗时的加密解密操作放到Web Worker后台线程中避免阻塞UI渲染。这是最推荐的方案。在部分旧浏览器如IE中无法运行Web Crypto API 和某些ES6语法如async/await不被支持。1.明确项目兼容性要求如果不需要支持旧浏览器可以忽略。2.使用Polyfill对于Web Crypto API可以使用node-forge或asmCrypto等库作为polyfill但体积较大且可能性能有差异。3.语法转换使用Babel等工具将ES6代码转换为ES5。一个关键的实操心得关于密码的传输与缓存在这个纯前端加密的方案中密码从未离开过用户的浏览器这是它的安全优势。但这也带来了一个用户体验问题每次查看或编辑一条笔记都需要重新输入密码。一种改进方案是在用户首次输入密码并成功解密某条笔记后在当前页面会话的生命周期内将该密码缓存在内存中例如用一个Map对象以笔记ID为键以解密后的内容或密码本身为值。当用户再次点击同一条笔记时无需再次输入密码。但必须清楚地向用户说明密码仅缓存在内存中关闭页面后即消失且要避免将密码缓存在localStorage或sessionStorage中因为那相当于明文存储密码违背了加密的初衷。