前言
PHP 導出 Word (.doc) 主流方案有三種:COM 組件 (Windows Only、需 Office、性能差)、第三方庫 (PhpOffice/PHPWord 功能全但學習成本高)、**MHT 格式偽裝 .doc (無依賴、純 PHP、原生支持圖片/樣式、Word 完美打開)**。本文深度解析 MHT 方案原理、完整實現代碼、圖片/Base64 封裝、CSS 樣式保留、大文件性能優化與避坑指南。
為什麼選 MHT (MIME HTML / .mht)
- 單文件封裝:HTML + 圖片 + CSS + JS 全部 Base64 編碼內嵌於一個 MIME 多部分消息中。
- Word 原生支持:Word 2003+ 可直接打開 .mht/.doc (改後綴),完美渲染圖片、表格、樣式。
- 零依賴:純 PHP 實現,無需安裝 Office、LibreOffice、Java、Python、COM、扩展。
- 跨平台:Linux/Windows/macOS 同樣運行,Docker 容器化無障礙。
- 模板靈活:直接輸出 HTML 模板,前端開發者可預覽調試,所見即所得。
MHT 文件結構 (RFC 2557)
From: <sender@example.com>
Subject: 導出文檔
Date: Mon, 15 Jan 2024 10:00:00 +0800
MIME-Version: 1.0
Content-Type: multipart/related;
boundary="----BOUNDARY_123456";
type="text/html"
This is a multi-part message in MIME format.
------BOUNDARY_123456
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: base64
Content-Location: index.html
PGh0bWw+... (Base64 編碼的 HTML 內容)
------BOUNDARY_123456
Content-Type: image/png
Content-Transfer-Encoding: base64
Content-Location: logo.png
iVBORw0KGgoAAAANSUhEUg... (Base64 編碼的圖片數據)
------BOUNDARY_123456--
核心類:MhtFileMaker (完整可用)
<?php
class MhtFileMaker {
private $headers = [];
private $headersMap = [];
private $parts = []; // ['location' => '', 'mime' => '', 'content' => '', 'encoding' => 'base64']
private $boundary;
private $baseDir;
private $firstPage;
public function __construct() {
$this->boundary = '--' . strtoupper(md5(mt_rand() . microtime())) . '_MHT';
$this->setHeader('MIME-Version', '1.0');
$this->setHeader('Date', date('D, d M Y H:i:s O'));
}
public function setHeader(string $name, string $value): void {
$this->headers[] = "$name: $value";
$this->headersMap[strtolower($name)] = $value;
}
public function addPart(string $location, string $mimeType, string $content, string $encoding = 'base64'): void {
// Base64 編碼並按 76 字符換行 (RFC 2045)
if ($encoding === 'base64') {
$content = chunk_split(base64_encode($content), 76);
}
$this->parts[] = [
'location' => $location,
'mime' => $mimeType,
'content' => $content,
'encoding' => $encoding,
];
}
// 便捷:添加 HTML 主體
public function addHtml(string $html, string $location = 'index.html'): void {
$this->addPart($location, 'text/html; charset=utf-8', $html);
if (!$this->firstPage) $this->firstPage = $location;
}
// 便捷:添加圖片/資源 (自動 Base64)
public function addResource(string $filePath, string $location = ''): bool {
if (!is_file($filePath) || !is_readable($filePath)) return false;
$location = $location ?: basename($filePath);
$mime = $this->getMimeType($filePath);
$content = file_get_contents($filePath);
$this->addPart($location, $mime, $content);
return true;
}
// 從 HTML 解析並內嵌圖片 (核心功能)
public function embedImagesFromHtml(string &$html, string $baseUrl = ''): int {
$count = 0;
$dom = new DOMDocument();
@$dom->loadHTML('' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$xpath = new DOMXPath($dom);
$images = $xpath->query('//img[@src]');
foreach ($images as $img) {
$src = $img->getAttribute('src');
if (empty($src) || str_starts_with($src, 'data:')) continue; // 已是 Data URI
$absoluteUrl = $baseUrl ? rtrim($baseUrl, '/') . '/' . ltrim($src, '/') : $src;
$content = @file_get_contents($absoluteUrl);
if ($content === false) continue; // 下載失敗跳過
$mime = $this->getMimeType($absoluteUrl);
$b64 = 'data:' . $mime . ';base64,' . base64_encode($content);
$img->setAttribute('src', $b64); // 替換為 Data URI
$count++;
}
// 保存修改後的 HTML (去除 DOMDocument 自動添加的 html/body 標籤)
$newHtml = '';
foreach ($dom->getElementsByTagName('body')->item(0)->childNodes as $node) {
$newHtml .= $dom->saveHTML($node);
}
$html = $newHtml ?: $html; // 解析失敗保留原文
return $count;
}
public function build(): string {
$output = implode("\r\n", $this->headers) . "\r\n";
$output .= "Content-Type: multipart/related;\r\n";
$output .= "\tboundary=\"{$this->boundary}\";\r\n";
$output .= "\ttype=\"text/html\"\r\n\r\n";
$output .= "This is a multi-part message in MIME format.\r\n\r\n";
foreach ($this->parts as $part) {
$output .= "{$this->boundary}\r\n";
$output .= "Content-Type: {$part['mime']}\r\n";
$output .= "Content-Transfer-Encoding: {$part['encoding']}\r\n";
$output .= "Content-Location: {$part['location']}\r\n\r\n";
$output .= $part['content'] . "\r\n";
}
$output .= "{$this->boundary}--\r\n";
return $output;
}
public function save(string $filename): void {
file_put_contents($filename, $this->build());
}
private function getMimeType(string $path): string {
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
return match ($ext) {
'htm', 'html' => 'text/html',
'txt' => 'text/plain',
'css' => 'text/css',
'js' => 'application/javascript',
'jpg', 'jpeg', 'jpe' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
'svg' => 'image/svg+xml',
'pdf' => 'application/pdf',
'doc', 'docx' => 'application/msword',
'xls', 'xlsx' => 'application/vnd.ms-excel',
'zip' => 'application/zip',
default => 'application/octet-stream',
};
}
}
導出 Word 完整流程示例
<?php
require 'MhtFileMaker.php';
// 1. 準備 HTML 內容 (可來自模板引擎 Blade/Twig/Smarty)
$html = '<h1 style="color:#2c3e50">項目報告</h1>';
$html .= '<p>生成時間: ' . date('Y-m-d H:i:s') . '</p>';
$html .= '<img src="https://example.com/logo.png" alt="Logo" style="max-width:200px">';
$html .= '<table border="1" cellpadding="5" cellspacing="0" style="border-collapse:collapse;width:100%">';
$html .= '<tr style="background:#34495e;color:white"><th>項目</th><th>進度</th><th>負責人</th></tr>';
$html .= '<tr><td>需求分析</td><td>100%</td><td>張三</td></tr>';
$html .= '<tr><td>開發編碼</td><td>80%</td><td>李四</td></tr>';
$html .= '</table>';
// 2. 創建 MHT 並內嵌圖片
$mht = new MhtFileMaker();
$mht->setHeader('Subject', '項目周報_' . date('Ymd'));
$mht->setHeader('From', 'system@example.com');
// 關鍵:解析 HTML 中的圖片並轉為 Data URI (自動下載、Base64、替換 src)
$mht->embedImagesFromHtml($html, 'https://example.com'); // baseUrl 用於補全相對路徑
// 添加 HTML 主體
$mht->addHtml($html);
// 可選:額外添加 CSS/JS/字體等資源 (若 HTML 中引用外部文件)
// $mht->addResource('/path/to/style.css', 'style.css');
// 3. 輸出為 .doc 文件 (瀏覽器下載)
$filename = '報告_' . date('Ymd_His') . '.doc';
header('Content-Type: application/msword');
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"');
header('Content-Length: ' . strlen($mht->build()));
echo $mht->build();
exit;
進階:CSS 樣式內嵌與複雜佈局支持
// 1. 外部 CSS 內嵌到 style 標籤
function inlineCss(string $html, string $cssUrl): string {
$css = @file_get_contents($cssUrl);
if ($css) {
$html = str_replace('', "", $html);
}
return $html;
}
// 2. 表格樣式增強 (Word 對 CSS 支持有限,建議內聯 style)
$html = preg_replace('/]*)>/i', '', $html);
$html = preg_replace('/| ]*)>/i', ' | ', $html);
$html = preg_replace('/ | ]*)>/i', ' | ', $html);
// 3. 分頁符 (Word 支持)
$html .= ''; // 強制分頁
$html .= ''; // 後分頁
// 4. 頁眉頁腳 (MHT 方案受限,建議導出後 Word 手動添加或用 PHPWord)
大文件性能優化
| 問題 | 解決方案 |
| 內存峰值過高 (大圖片/長文檔) | 分塊讀取文件 Base64 編碼,避免 file_get_contents 一次性載入大文件;或臨時文件落地流式輸出 |
| Base64 體積膨脹 33% | 壓縮圖片 (WebP、降低質量)、移除冗餘資源、考慮 PHPWord 僅存引用圖片路徑 (需 Word 打開時聯網) |
| 並發導出阻塞 PHP-FPM | 異步隊列:Web 請求寫入任務表,Worker 進程後台生成 MHT 存儲 OSS/本地,返回下載鏈接 |
| 超大報表 (超 50MB) | 分卷導出 (多個 .doc)、改用 PHPWord 流式寫入 .docx (XML 壓縮)、或導出 PDF (tcpdf/dompdf) |
避坑指南
- HTML 編碼:務必確保 HTML 為 UTF-8,``,DOMDocument 解析時加 `` 前綴。
- 圖片跨域/防盜鏈:下載失敗時保留原 `src` 或顯示佔位圖,記錄日志告警。
- 相對路徑補全:`embedImagesFromHtml($html, $baseUrl)` 自動處理 `/img/a.png` → `$baseUrl/img/a.png`。
- Word 兼容性:避免 Flex/Grid/浮動佈局,使用 `
` 佈局;內聯 `style` 屬性;測試 Word 2010/2016/365/WPS/LibreOffice。
- 文件名編碼:`rawurlencode($filename)` 解決中文文件名下載亂碼。
- 超時設置:`set_time_limit(300)`,大文檔生成需較長時間。
方案對比總結
| 方案 | 依賴 | 圖片支持 | 樣式支持 | 性能 | 跨平台 | 維護成本 | 推薦場景 |
| MHT 偽裝 .doc | 無 | ✅ Base64 內嵌 | ✅ 內聯 CSS/Table | ⚠️ 大文件內存壓力 | ✅ 完美 | 低 | 輕量導出、報表、合同、無依賴環境 |
| PHPWord (PhpOffice) | Composer | ✅ 內嵌/引用 | ✅ 豐富 API | 中等 | ✅ | 中 | 複雜文檔、需程序化控制結構、長期維護項目 |
| COM (Windows+Office) | Office + COM | ✅ 原生 | ✅ 完美 | 極差 (進程啟動) | ❌ 僅 Windows | 高 | 極少數需 Word 高級功能 (郵件合併、修訂、宏) 的舊系統 |
| LibreOffice Headless 轉換 | LibreOffice | ✅ | ✅ | 慢 (進程轉換) | ✅ | 中 | 批量格式轉換 (docx→pdf、html→docx) |
| HTML 直接輸出 .doc | 無 | ❌ 僅外鏈 | ⚠️ 有限 | 最快 | ✅ | 極低 | 純文本、無圖片、臨時預覽 |
總結
MHT 方案是 **PHP 導出 Word 最性價比、零依賴、跨平台** 的解決方案。核心優勢在於 **HTML 模板開發體驗佳 + Base64 內嵌圖片單文件分發 + Word 原生打開**。配合 `MhtFileMaker` 類與 `embedImagesFromHtml()` 自動化圖片內嵌,可快速構建導出功能。生產環境注意:**大文件異步化、圖片壓縮、樣式內聯、兼容性測試**。長期項目建議遷移至 **PHPWord** 獲得更強的文檔結構控制能力。
- 上一篇文章: PHP 與 Java Socket 通信實戰:TCP 長連接、協議設計與生產級客戶端封裝
- 下一篇文章: PHP 分頁實作全攻略:三種經典分頁類與函式完整教學
Copyright © 程式碼之家 All Rights Reserved
|