用戶編寫長文章、填寫複雜表單時,瀏覽器崩潰、網路斷開、誤關分頁、Session 過期,都可能導致內容徹底丟失。草稿自動保存 是提升用戶體驗、降低內容流失率的關鍵功能。本文提供基於 原生 JavaScript (無框架依賴) + JSP 後端 的完整實現,涵蓋定時保存、手動保存、草稿恢復、多用戶隔離、編碼處理。
<!DOCTYPE html>
<html><head>
<meta charset="UTF-8">
<title>AJAX 草稿自動保存演示</title>
<script src="ajaxrequest.js"></script> <!-- 通用 AJAX 封裝 -->
<script src="autosave.js"></script> <!-- 自動保存邏輯 -->
</head><body>
<h2>文章編輯器</h2>
<!-- 用戶識別 (實際專案從 Session/Token 獲取) -->
<label>用戶名:<input type="text" id="memName" value="user001" readonly></label><br><br>
<label><input type="checkbox" id="Draft_AutoSave" checked onchange="setAutoSave()"> 啟用自動保存 (10秒間隔)</label><br><br>
<label>內容:<br>
<textarea id="message" rows="10" cols="60">開始寫作...</textarea></label><br><br>
<div id="AutoSaveMsg" style="color:#666;min-height:1.2em;"></div><br>
<button onclick="manualSave()">手動保存</button>
<button onclick="restoreDraft()">恢復草稿</button>
<button onclick="submitFinal()">最終發布</button>
</body></html>
// 簡易 AJAX 封裝 (支援 GET/POST、JSON/表單、回調)
function AJAXRequest() {
this.url = '';
this.method = 'POST';
this.content = '';
this.async = true;
this.callback = function(xmlObj) {};
this.send = function() {
var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) this.callback(xhr);
else console.error('AJAX Error:', xhr.status);
}
}.bind(this);
xhr.open(this.method, this.url, this.async);
if (this.method === 'POST') {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
}
xhr.send(this.content);
};
}
// 全域狀態
var FormContent = document.getElementById('message');
var AutoSaveMsg = document.getElementById('AutoSaveMsg');
var memName = document.getElementById('memName').value;
var AutoSaveTime = 10000; // 10 秒
var AutoSaveTimer = null;
// 初始化
setAutoSave();
function autoSave() {
if (!FormContent.value || !memName) return;
var ajax = new AJAXRequest();
ajax.url = 'autosave.jsp';
ajax.content = 'action=AutoSave&memname=' + encodeURIComponent(memName)
+ '&postcontent=' + encodeURIComponent(FormContent.value);
ajax.callback = function(xhr) {
AutoSaveMsg.innerHTML = xhr.responseText;
};
ajax.send();
}
function setAutoSave() {
var chk = document.getElementById('Draft_AutoSave');
if (chk.checked) {
AutoSaveTimer = setInterval(autoSave, AutoSaveTime);
AutoSaveMsg.innerHTML = '自動保存已啟用';
} else {
clearInterval(AutoSaveTimer);
AutoSaveMsg.innerHTML = '自動保存已停用';
}
}
function restoreDraft() {
if (!memName) return;
AutoSaveMsg.innerHTML = '正在恢復草稿...';
var ajax = new AJAXRequest();
ajax.url = 'autosave.jsp';
ajax.content = 'action=Restore&memname=' + encodeURIComponent(memName);
ajax.callback = function(xhr) {
var text = xhr.responseText.trim();
if (text) {
FormContent.value = text;
AutoSaveMsg.innerHTML = '草稿恢復成功!';
} else {
AutoSaveMsg.innerHTML = '無可用草稿';
}
};
ajax.send();
}
function manualSave() {
if (!FormContent.value || !memName) return;
var ajax = new AJAXRequest();
ajax.url = 'autosave.jsp';
ajax.content = 'action=Save&memname=' + encodeURIComponent(memName)
+ '&postcontent=' + encodeURIComponent(FormContent.value);
ajax.callback = function(xhr) {
AutoSaveMsg.innerHTML = xhr.responseText;
};
ajax.send();
}
function submitFinal() {
// 實際提交業務邏輯 (略)
alert('提交至正式資料表...');
}
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" import="java.io.*,java.util.*" %>
<%
String action = request.getParameter("action");
String memName = request.getParameter("memname");
String postContent = request.getParameter("postcontent");
if (memName == null || memName.trim().isEmpty()) {
out.print("錯誤:用戶名為空"); return;
}
// 安全:防止路徑遍歷
String safeName = memName.replaceAll("[^a-zA-Z0-9_-]", "");
String dir = getServletContext().getRealPath("/WEB-INF/drafts/");
new File(dir).mkdirs();
File file = new File(dir, safeName + ".txt");
if ("AutoSave".equals(action) || "Save".equals(action)) {
// 接收端已 UTF-8,此處無需轉碼;若前端未設 charset 需處理 ISO-8859-1
try (FileWriter fw = new FileWriter(file, StandardCharsets.UTF_8)) {
fw.write(postContent == null ? "" : postContent);
out.print("最後於 " + new Date() + " 自動保存成功!");
} catch (IOException e) {
out.print("保存失敗:" + e.getMessage());
}
} else if ("Restore".equals(action)) {
if (file.exists()) {
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) sb.append(line).append('\n');
out.print(sb.toString().trim());
} catch (IOException e) {
out.print("讀取失敗:" + e.getMessage());
}
} else {
out.print(""); // 無草稿
}
} else {
out.print("未知操作:" + action);
}
%>
oninput 事件 + 防抖 2 秒 + 內容變更檢測 (MD5/長度比較) → 僅在內容真正變更時發請求。| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| Server 檔案/DB (本文) | 跨設備同步、持久可靠 | 需網路、延遲高 | 正式編輯器、協作寫作 |
| LocalStorage (5MB) | 極快、離線可用、無需後端 | 同源限制、無法跨設備、容量小 | 簡單表單、臨時輸入 |
| IndexedDB (數百 MB) | 大容量、異步、支援索引查詢 | API 複雜、瀏覽器差異 | 富文本編輯器、大量草稿 |
| Service Worker + Cache API | 離線優先、背景同步 | 生命週期複雜、調試難 | PWA 應用、極致離線體驗 |