前端基于mammoth.js實現(xiàn)Word文檔在線編輯與導(dǎo)出
在當(dāng)今的Web應(yīng)用開發(fā)中,實現(xiàn)文檔的在線編輯與導(dǎo)出已成為常見需求。無論是企業(yè)內(nèi)部系統(tǒng)、教育平臺還是項目管理工具,都迫切需要讓用戶能夠在瀏覽器中直接編輯Word文檔,而無需安裝桌面軟件。本文將詳細(xì)介紹如何利用mammoth.js和Blob對象實現(xiàn)這一功能,并對比其他可行方案。
一、為什么選擇mammoth.js與Blob方案
在Web前端實現(xiàn)Word文檔處理,主要有三種主流方案:瀏覽器原生Blob導(dǎo)出、mammoth.js專業(yè)轉(zhuǎn)換和基于模板的docxtemplater方案。它們各有優(yōu)劣,適用于不同場景。
mammoth.js的核心優(yōu)勢在于它能將.docx文檔轉(zhuǎn)換為語義化的HTML,而非簡單復(fù)制視覺樣式。這意味著它生成的HTML結(jié)構(gòu)清晰、易于維護(hù)和樣式定制。配合Blob對象,我們可以輕松將編輯后的內(nèi)容重新導(dǎo)出為Word文檔。
與直接使用Microsoft Office Online或Google Docs嵌入相比,mammoth.js方案不依賴外部服務(wù),能更好地保護(hù)數(shù)據(jù)隱私,且可定制性更高。
二、實現(xiàn)原理與技術(shù)架構(gòu)
2.1 mammoth.js的轉(zhuǎn)換原理
mammoth.js的工作原理可分為四個關(guān)鍵階段:
- 文檔解析:讀取.docx文件的XML結(jié)構(gòu)(.docx本質(zhì)上是包含多個XML文件的壓縮包)
- 樣式處理:識別Word文檔中的樣式定義,并應(yīng)用用戶定義的樣式映射規(guī)則
- 轉(zhuǎn)換引擎:將文檔元素轉(zhuǎn)換為對應(yīng)的HTML元素
- 輸出生成:生成最終的HTML代碼
// 基本轉(zhuǎn)換示例
mammoth.convertToHtml({arrayBuffer: arrayBuffer})
.then(function(result) {
// result.value包含生成的HTML
document.getElementById('editor').innerHTML = result.value;
})
.catch(function(error) {
console.error('轉(zhuǎn)換出錯:', error);
});
2.2 Blob對象的作用
Blob(Binary Large Object)對象代表不可變的原始數(shù)據(jù),類似于文件對象。在前端文件操作中,它扮演著關(guān)鍵角色:
- 數(shù)據(jù)包裝:將HTML內(nèi)容包裝成Word文檔格式
- 類型指定:通過MIME類型聲明文檔格式(如application/msword)
- 下載觸發(fā):結(jié)合URL.createObjectURL()實現(xiàn)文件下載
三、完整實現(xiàn)步驟
3.1 基礎(chǔ)環(huán)境搭建
首先,在HTML中引入mammoth.js并構(gòu)建基本界面:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Word在線編輯器</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.5.1/mammoth.browser.min.js"></script>
<style>
.editor-container {
display: flex;
height: 80vh;
}
#editor {
flex: 1;
border: 1px solid #ccc;
padding: 20px;
overflow-y: auto;
}
</style>
</head>
<body>
<input type="file" id="fileInput" accept=".docx">
<button id="exportBtn">導(dǎo)出為Word</button>
<div class="editor-container">
<div id="editor" contenteditable="true"></div>
</div>
<script>
// 實現(xiàn)代碼將在這里
</script>
</body>
</html>
3.2 文檔上傳與轉(zhuǎn)換
實現(xiàn)文件上傳和Word到HTML的轉(zhuǎn)換:
document.getElementById('fileInput').addEventListener('change', function(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const arrayBuffer = e.target.result;
// 使用mammoth進(jìn)行轉(zhuǎn)換
mammoth.convertToHtml({arrayBuffer: arrayBuffer})
.then(function(result) {
document.getElementById('editor').innerHTML = result.value;
})
.catch(function(error) {
console.error('轉(zhuǎn)換出錯:', error);
});
};
reader.readAsArrayBuffer(file);
});
3.3 內(nèi)容編輯與導(dǎo)出
實現(xiàn)編輯后內(nèi)容的導(dǎo)出功能:
document.getElementById('exportBtn').addEventListener('click', function() {
// 獲取編輯后的內(nèi)容
const editedContent = document.getElementById('editor').innerHTML;
// 創(chuàng)建Word文檔的HTML結(jié)構(gòu)
const fullHtml = `
<html xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:w="urn:schemas-microsoft-com:office:word">
<head>
<meta charset="UTF-8">
<title>編輯后的文檔</title>
<style>
body { font-family: '宋體', serif; font-size: 12pt; }
/* 其他樣式 */
</style>
</head>
<body>${editedContent}</body>
</html>
`;
// 創(chuàng)建Blob對象并觸發(fā)下載
const blob = new Blob(['\uFEFF' + fullHtml], {
type: 'application/msword'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = '編輯后的文檔.doc';
document.body.appendChild(a);
a.click();
// 清理資源
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
});
四、高級功能與優(yōu)化
4.1 樣式映射定制
mammoth.js的強(qiáng)大之處在于其樣式映射系統(tǒng),允許自定義轉(zhuǎn)換規(guī)則:
const options = {
styleMap: [
"p[style-name='Title'] => h1:fresh",
"p[style-name='Subtitle'] => h2:fresh",
"p[style-name='Warning'] => div.warning:fresh",
"b => strong",
"i => em"
]
};
mammoth.convertToHtml({arrayBuffer: arrayBuffer}, options)
.then(function(result) {
// 應(yīng)用自定義樣式映射的結(jié)果
});
4.2 圖片處理策略
處理文檔中的圖片是一個常見挑戰(zhàn),mammoth.js提供了靈活的解決方案:
- Base64內(nèi)嵌:默認(rèn)將圖片轉(zhuǎn)換為Base64格式直接嵌入HTML
- 外部文件輸出:可將圖片保存為獨(dú)立文件并更新引用路徑
4.3 實時協(xié)作支持(進(jìn)階)
對于需要多人協(xié)作的場景,可以結(jié)合WebSocket或SignalR實現(xiàn)實時同步:
// 簡化的協(xié)作編輯示例
const socket = new WebSocket('wss://yourserver.com/collaboration');
socket.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.type === 'content-update') {
// 應(yīng)用其他用戶的編輯
applyRemoteEdit(data.content, data.selection);
}
};
// 監(jiān)聽本地編輯事件
document.getElementById('editor').addEventListener('input', function() {
// 廣播編輯內(nèi)容
socket.send(JSON.stringify({
type: 'content-update',
content: this.innerHTML,
timestamp: Date.now()
}));
});
五、方案對比與選擇指南
下表對比了三種主要方案的特性:
| 方案 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場景 |
|---|---|---|---|
| Blob原生導(dǎo)出 | 零依賴、簡單易用 | 樣式控制有限、兼容性問題 | 簡單文本導(dǎo)出、快速原型 |
| mammoth.js轉(zhuǎn)換 | 語義化輸出、良好可定制性 | 復(fù)雜格式可能丟失、需學(xué)習(xí)曲線 | 內(nèi)容型文檔、需要樣式定制 |
| docxtemplater | 模板驅(qū)動、企業(yè)級控制 | 需要預(yù)設(shè)計模板、復(fù)雜度高 | 標(biāo)準(zhǔn)化報告、合同生成 |
六、常見問題與解決方案
6.1 中文亂碼問題
確保在HTML頭部聲明UTF-8編碼,并在Blob內(nèi)容前添加BOM頭:
const blob = new Blob(['\uFEFF' + htmlContent], {
type: 'application/msword;charset=utf-8'
});
6.2 樣式不一致問題
- 使用Word標(biāo)準(zhǔn)單位(如pt而非px)
- 盡量使用內(nèi)聯(lián)樣式確保兼容性
- 針對Word專用CSS屬性進(jìn)行優(yōu)化
6.3 大型文檔性能優(yōu)化
- 實現(xiàn)分片加載和懶渲染
- 使用Web Worker在后臺線程處理轉(zhuǎn)換任務(wù)
- 添加加載狀態(tài)指示器和進(jìn)度反饋
七、總結(jié)與最佳實踐
mammoth.js配合Blob對象提供了一種平衡功能性與復(fù)雜性的Word文檔在線編輯方案。它在保留基本格式的同時,提供了良好的可擴(kuò)展性和定制能力。
成功實施的關(guān)鍵因素包括:
- 漸進(jìn)增強(qiáng):先實現(xiàn)核心功能,再逐步添加高級特性
- 用戶體驗:提供清晰的反饋和狀態(tài)指示
- 兼容性測試:在不同版本W(wǎng)ord中測試導(dǎo)出結(jié)果
- 性能監(jiān)控:對大文檔處理進(jìn)行性能優(yōu)化
對于需要更高級功能(如復(fù)雜格式保留、實時協(xié)作)的場景,可以考慮結(jié)合Microsoft Graph API或專業(yè)文檔處理服務(wù),構(gòu)建更強(qiáng)大的文檔管理系統(tǒng)。
未來發(fā)展方向包括更智能的樣式映射、AI輔助的格式優(yōu)化以及與新興Web標(biāo)準(zhǔn)(如Web Assembly)的深度集成,這些都將進(jìn)一步提升在線文檔編輯的體驗和能力邊界。
本文介紹的方案已在實際項目中得到應(yīng)用,可根據(jù)具體需求進(jìn)行調(diào)整和擴(kuò)展。希望這篇指南能為你的Web文檔處理功能開發(fā)提供有力支持!
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Word文檔在線編輯器</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.4.2/mammoth.browser.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: white;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
header {
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
color: white;
padding: 20px 30px;
text-align: center;
}
h1 {
font-size: 2.2rem;
margin-bottom: 10px;
}
.subtitle {
font-size: 1rem;
opacity: 0.9;
}
.toolbar {
display: flex;
justify-content: space-between;
padding: 15px 30px;
background-color: #f8f9fa;
border-bottom: 1px solid #eaeaea;
flex-wrap: wrap;
}
.toolbar-group {
display: flex;
gap: 10px;
margin: 5px 0;
}
.btn {
padding: 10px 15px;
border: none;
border-radius: 5px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background-color: #4a6cf7;
color: white;
}
.btn-primary:hover {
background-color: #3a5ce0;
transform: translateY(-2px);
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.btn-success {
background-color: #28a745;
color: white;
}
.btn-success:hover {
background-color: #218838;
transform: translateY(-2px);
}
.format-btn {
background-color: white;
border: 1px solid #ddd;
padding: 8px 12px;
}
.format-btn:hover {
background-color: #f8f9fa;
}
.format-btn.active {
background-color: #e9ecef;
border-color: #6c757d;
}
.editor-container {
display: flex;
height: 70vh;
min-height: 500px;
}
.upload-section {
flex: 0 0 300px;
padding: 20px;
background-color: #f8f9fa;
border-right: 1px solid #eaeaea;
display: flex;
flex-direction: column;
gap: 20px;
}
.upload-area {
border: 2px dashed #6a11cb;
border-radius: 8px;
padding: 30px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
background-color: rgba(106, 17, 203, 0.05);
}
.upload-area:hover {
background-color: rgba(106, 17, 203, 0.1);
}
.upload-icon {
font-size: 48px;
color: #6a11cb;
margin-bottom: 15px;
}
.file-input {
display: none;
}
.editor-section {
flex: 1;
display: flex;
flex-direction: column;
}
.editor-toolbar {
padding: 10px 20px;
background-color: white;
border-bottom: 1px solid #eaeaea;
display: flex;
gap: 5px;
flex-wrap: wrap;
}
#editor {
flex: 1;
padding: 30px;
overflow-y: auto;
background-color: white;
line-height: 1.8;
font-size: 16px;
}
#editor:focus {
outline: none;
}
.status-bar {
padding: 10px 30px;
background-color: #f8f9fa;
border-top: 1px solid #eaeaea;
display: flex;
justify-content: space-between;
font-size: 0.9rem;
color: #6c757d;
}
.message {
padding: 15px;
margin: 15px 30px;
border-radius: 5px;
display: none;
}
.message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.loading {
display: none;
text-align: center;
padding: 20px;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #6a11cb;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (max-width: 768px) {
.editor-container {
flex-direction: column;
height: auto;
}
.upload-section {
flex: none;
border-right: none;
border-bottom: 1px solid #eaeaea;
}
.toolbar {
flex-direction: column;
gap: 10px;
}
.toolbar-group {
justify-content: center;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Word文檔在線編輯器</h1>
<p class="subtitle">上傳、編輯并導(dǎo)出Word文檔 - 基于mammoth.js與Blob對象實現(xiàn)</p>
</header>
<div class="toolbar">
<div class="toolbar-group">
<button class="btn btn-primary" id="uploadBtn">
<i class="upload-icon">??</i> 上傳Word文檔
</button>
<input type="file" id="fileInput" class="file-input" accept=".docx">
</div>
<div class="toolbar-group">
<button class="btn btn-success" id="exportBtn">
<i class="export-icon">??</i> 導(dǎo)出為Word文檔
</button>
</div>
</div>
<div class="message success" id="successMessage"></div>
<div class="message error" id="errorMessage"></div>
<div class="loading" id="loadingIndicator">
<div class="spinner"></div>
<p>正在處理文檔,請稍候...</p>
</div>
<div class="editor-container">
<div class="upload-section">
<div class="upload-area" id="uploadArea">
<div class="upload-icon">??</div>
<h3>上傳Word文檔</h3>
<p>點(diǎn)擊此處或使用上方上傳按鈕</p>
<p>支持.docx格式文件</p>
</div>
<div>
<h3>使用說明</h3>
<ul style="padding-left: 20px; margin-top: 10px;">
<li>上傳.docx格式的Word文檔</li>
<li>在編輯區(qū)域直接修改內(nèi)容</li>
<li>使用工具欄格式化文本</li>
<li>完成后導(dǎo)出為新的Word文檔</li>
</ul>
</div>
</div>
<div class="editor-section">
<div class="editor-toolbar">
<button class="format-btn" data-command="bold" title="加粗">B</button>
<button class="format-btn" data-command="italic" title="斜體">I</button>
<button class="format-btn" data-command="underline" title="下劃線">U</button>
<div style="width: 1px; background-color: #ddd; margin: 0 10px;"></div>
<button class="format-btn" data-command="formatBlock" data-value="h1" title="標(biāo)題1">H1</button>
<button class="format-btn" data-command="formatBlock" data-value="h2" title="標(biāo)題2">H2</button>
<button class="format-btn" data-command="formatBlock" data-value="p" title="段落">P</button>
<div style="width: 1px; background-color: #ddd; margin: 0 10px;"></div>
<button class="format-btn" data-command="insertUnorderedList" title="無序列表">●</button>
<button class="format-btn" data-command="insertOrderedList" title="有序列表">1.</button>
<div style="width: 1px; background-color: #ddd; margin: 0 10px;"></div>
<button class="format-btn" data-command="justifyLeft" title="左對齊">?</button>
<button class="format-btn" data-command="justifyCenter" title="居中對齊">?</button>
<button class="format-btn" data-command="justifyRight" title="右對齊">?</button>
</div>
<div
id="editor"
contenteditable="true"
style="border: 1px solid #ccc; min-height: 500px; padding: 20px;"
>
<p>請上傳Word文檔開始編輯,或直接在此處輸入內(nèi)容...</p>
</div>
</div>
</div>
<div class="status-bar">
<div id="charCount">字符數(shù): 0</div>
<div id="docInfo">文檔狀態(tài): 未加載</div>
</div>
</div>
<script>
// DOM元素引用
const fileInput = document.getElementById('fileInput');
const uploadBtn = document.getElementById('uploadBtn');
const uploadArea = document.getElementById('uploadArea');
const exportBtn = document.getElementById('exportBtn');
const editor = document.getElementById('editor');
const successMessage = document.getElementById('successMessage');
const errorMessage = document.getElementById('errorMessage');
const loadingIndicator = document.getElementById('loadingIndicator');
const charCount = document.getElementById('charCount');
const docInfo = document.getElementById('docInfo');
// 上傳按鈕點(diǎn)擊事件
uploadBtn.addEventListener('click', () => fileInput.click());
uploadArea.addEventListener('click', () => fileInput.click());
// 文件選擇變化事件
fileInput.addEventListener('change', function(event) {
const file = event.target.files[0];
if (!file) return;
// 檢查文件類型
if (!file.name.endsWith('.docx')) {
showMessage('請選擇.docx格式的Word文檔', 'error');
return;
}
// 顯示加載指示器
showLoading(true);
// 使用FileReader讀取文件
const reader = new FileReader();
reader.onload = function(e) {
const arrayBuffer = e.target.result;
// 使用mammoth.js轉(zhuǎn)換Word文檔為HTML
mammoth.convertToHtml({arrayBuffer: arrayBuffer})
.then(function(result) {
// 將轉(zhuǎn)換后的HTML插入編輯器
editor.innerHTML = result.value;
// 更新文檔信息
updateDocInfo(file.name, result.value);
// 顯示成功消息
showMessage(`文檔"${file.name}"加載成功!`, 'success');
// 隱藏加載指示器
showLoading(false);
})
.catch(function(error) {
console.error('轉(zhuǎn)換出錯:', error);
showMessage('文檔轉(zhuǎn)換失敗: ' + error.message, 'error');
showLoading(false);
});
};
reader.onerror = function() {
showMessage('文件讀取失敗', 'error');
showLoading(false);
};
reader.readAsArrayBuffer(file);
});
// 導(dǎo)出按鈕點(diǎn)擊事件
exportBtn.addEventListener('click', function() {
// 獲取編輯后的HTML內(nèi)容
const editedContent = editor.innerHTML;
if (!editedContent || editedContent.trim() === '') {
showMessage('編輯器內(nèi)容為空,無法導(dǎo)出', 'error');
return;
}
// 顯示加載指示器
showLoading(true);
// 創(chuàng)建完整的HTML文檔結(jié)構(gòu)
const fullHtml = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>編輯后的文檔</title>
<style>
body {
font-family: 'Times New Roman', serif;
line-height: 1.5;
margin: 1in;
}
h1, h2, h3 {
margin-top: 0.5em;
margin-bottom: 0.25em;
}
p {
margin-bottom: 0.5em;
text-align: justify;
}
table {
border-collapse: collapse;
width: 100%;
}
table, th, td {
border: 1px solid black;
}
th, td {
padding: 8px;
text-align: left;
}
</style>
</head>
<body>
${editedContent}
</body>
</html>
`;
// 創(chuàng)建Blob對象
const blob = new Blob([fullHtml], {
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
});
// 創(chuàng)建下載鏈接
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'edited-document.docx';
document.body.appendChild(a);
a.click();
// 清理
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
showLoading(false);
showMessage('文檔導(dǎo)出成功!', 'success');
}, 100);
});
// 編輯器內(nèi)容變化時更新字符計數(shù)
editor.addEventListener('input', updateCharCount);
// 格式化按鈕事件處理
document.querySelectorAll('.format-btn').forEach(button => {
button.addEventListener('click', function() {
const command = this.dataset.command;
const value = this.dataset.value;
// 切換活動狀態(tài)
if (command === 'bold' || command === 'italic' || command === 'underline') {
this.classList.toggle('active');
}
// 執(zhí)行命令
document.execCommand(command, false, value);
editor.focus();
});
});
// 顯示消息函數(shù)
function showMessage(text, type) {
const messageElement = type === 'success' ? successMessage : errorMessage;
messageElement.textContent = text;
messageElement.style.display = 'block';
// 3秒后自動隱藏消息
setTimeout(() => {
messageElement.style.display = 'none';
}, 3000);
}
// 顯示/隱藏加載指示器
function showLoading(show) {
loadingIndicator.style.display = show ? 'block' : 'none';
}
// 更新字符計數(shù)
function updateCharCount() {
const text = editor.innerText || '';
charCount.textContent = `字符數(shù): ${text.length}`;
}
// 更新文檔信息
function updateDocInfo(filename, content) {
const text = content.replace(/<[^>]*>/g, '');
docInfo.textContent = `文檔: ${filename} | 字符數(shù): ${text.length}`;
}
// 初始化字符計數(shù)
updateCharCount();
</script>
</body>
</html>
到此這篇關(guān)于前端基于mammoth.js實現(xiàn)Word文檔在線編輯與導(dǎo)出的文章就介紹到這了,更多相關(guān)mammoth.js在線編輯與導(dǎo)出Word內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
微信小程序使用scroll-view標(biāo)簽實現(xiàn)自動滑動到底部功能的實例代碼
本文通過實例代碼給大家介紹了微信小程序使用scroll-view標(biāo)簽實現(xiàn)自動滑動到底部功能,代碼簡單易懂,非常不錯,具有一定的參考借鑒價值,需要的朋友參考下吧2018-11-11
javascript實現(xiàn)json頁面分頁實例代碼
這篇文章主要介紹了javascript實現(xiàn)json頁面分頁實例代碼,需要的朋友可以參考下2014-02-02
uni-app調(diào)取接口的3種方式以及封裝uni.request()詳解
我們在實際工作中要將數(shù)據(jù)傳輸?shù)椒?wù)器端,從服務(wù)器端獲取信息,都是通過接口的形式,下面這篇文章主要給大家介紹了關(guān)于uni-app調(diào)取接口的3種方式以及封裝uni.request()的相關(guān)資料,需要的朋友可以參考下2022-08-08
location.search在客戶端獲取Url參數(shù)的方法
最近一直在寫html,剛接觸到,感覺挺復(fù)雜的。。比如傳參,在.net里可以直接用Request接受,而在html中還要經(jīng)過處理,找了一些資料,寫了個方法。2010-06-06
javascript的onchange事件與jQuery的change()方法比較
本來是想添加文本框文本內(nèi)容改變事件動作的,結(jié)果找了許多這方面的javascript代碼都不如意。2009-09-09

