課程:RN 跨平台開發基礎 第 10 堂:原生功能整合
文件匯出與檔案分享
想像一下,你的使用者剛在 AuraFlow 中生成了一張精美的人類圖。這張圖表包含了複雜的幾何線條、五顏六色的能量中心,以及大量的分析文字。此時,使用者心裡想的通常不是「我想在螢幕上看它」,而是「我想把這張圖傳給朋友」或是「我想把它印出來貼在筆記本上」。
從開發者的角度來看,這是一個從 「記憶體中的資料物件」 轉換為 「標準化實體檔案」,最後交由 「作業系統分發」 的過程。在 React Native 的世界裡,這不是一個 API 就能搞定的事,而是一場由 expo-print、expo-file-system 與 expo-sharing 三位成員共同完成的接力賽。
這一部分我們將深入探討這個「文件匯出流水線」,理解為什麼我們需要這樣的組合,以及在 iOS 與 Android 兩大陣營中,這場接力賽有哪些截然不同的規則。
文件匯出的三階火箭:流水線架構
在 Web 開發中,我們習慣直接呼叫 window.print(),瀏覽器就會幫我們處理剩下的一切。但在行動裝置上,為了系統安全與效能,流程被拆解得更加精細。我們可以將其想像成一個三階火箭:
- 第一階:燃料轉換 (
**expo-print**) —— 將你的資料(通常是 HTML/SVG)轉換成作業系統看得懂的 PDF 二進位格式。 - 第二階:暫存倉庫 (
**expo-file-system**) —— 產生的 PDF 總得有個地方放。這層級負責管理檔案的路徑、暫存與清理。 - 第三階:發射塔 (
**expo-sharing**) —— 呼叫系統原生的分享介面(Share Sheet),讓使用者決定要把這個檔案丟給 Line、存入雲端硬碟,還是透過 AirDrop 傳送。
這就是我們在上一 Part 提到的「JS 指揮官」模式:JS 層下達一連串指令,驅動原生部隊在底層完成這些繁重的 IO 操作。
Phase 1: expo-print —— 從 HTML 到 PDF 的煉金術
為什麼 expo-print 是處理複雜圖表(如人類圖)的首選?答案是 HTML 渲染能力。
在 APP 中直接繪製 PDF 是一件極其痛苦的事,你需要手動計算坐標、處理換行。但 expo-print 允許我們使用熟悉的 HTML 與 CSS。對於 AuraFlow 來說,這意味著我們可以用 SVG 標籤來繪製人類圖的幾何線條,並用 CSS Grid 或 Flexbox 來排版分析報告。
核心 API:printAsync vs. printToFileAsync
這兩個方法雖然名字很像,但對使用者體驗(UX)的影響卻完全不同,這是最容易產生困惑的地方:
**printAsync**** (彈出列印視窗):** 它會直接呼叫系統內建的「列印服務」。在 iOS 上會跳出預覽畫面與印表機選擇;在 Android 上則是系統的列印管理員。這適用於使用者明確表示「我要列印」的場景。**printToFileAsync**** (靜默產生檔案):** 這是開發中最常用的方法。它在後台悄悄地將 HTML 轉換成 PDF 檔案並存儲在快取目錄中,然後返回一個檔案路徑(URI)。使用者不會看到任何介面,直到你呼叫expo-sharing。這對於「下載圖表」或「分享報告」的流程來說是完美的。
參數控制的細節
在呼叫 printToFileAsync({ html, width, height, base64 }) 時,有幾個關鍵點:
- HTML 字串: 你可以動態注入 CSS。例如,為了確保 PDF 列印出來有高品質,通常會建議在 HTML 裡面加入
<style> @page { margin: 20px; } </style>。 - 動態內容: 對於人類圖,我們會將資料轉換成一個超長的 Template Literal(模板字串),並將 SVG 直接嵌入其中。
Phase 2: expo-file-system —— 隱形的路徑管理者
當 expo-print 告訴你:「嘿,我把 PDF 做好了,放在 file:///.../uuid.pdf」時,這就是 expo-file-system 介入的時候。
在行動端,我們不能像在電腦上那樣隨意讀寫任意資料夾。每個 APP 都有自己的 沙盒(Sandbox)。expo-print 產生的檔案預設會放在 cacheDirectory(快取目錄)。
為什麼是快取目錄(Cache Directory)?
快取目錄的特性是「暫時性」。當手機儲存空間不足時,作業系統可能會自動清理這裡的內容。這非常適合匯出功能:
- 使用者點擊分享,PDF 產生。
- 分享完成後,這個 PDF 就不再需要佔用手機空間了。
- 注意: 雖然系統會清理,但作為負責任的開發者,我們應該在分享成功後主動刪除舊的暫存檔,避免 APP 的佔用體積(Document & Data)莫名其妙地膨脹。
Phase 3: expo-sharing —— 觸及世界的最後一哩路
最後,我們需要讓使用者決定檔案的去向。expo-sharing 的功能非常單一且強大:開啟系統原生的分享面板。
這裡有一個重要的開發習慣:可用性檢查 (**isAvailableAsync**)。
並非所有裝置都支援分享功能(雖然現代手機幾乎都有,但在某些受限的模擬器或特殊硬體上可能會失效)。在執行分享邏輯前,先檢查是一個好習慣。
iOS vs. Android 的行為差異
這是跨平台開發中最有趣的「避雷區」:
- iOS (Share Sheet): iOS 的分享介面非常優雅,它會自動根據檔案類型(PDF)過濾掉不支援的 APP。你可以直接傳遞檔案路徑,iOS 的
UIActivityViewController會處理剩下的權限問題。 - Android (Intent Chooser): Android 稍微複雜一點。它使用的是「Intent」機制。當你分享一個檔案 URI 時,Android 需要透過一個叫
FileProvider的機制來暫時授權給其他 APP(如 Gmail)讀取你的沙盒檔案。幸運的是,expo-sharing在底層幫你處理了大部分的content://URI 轉換,但你可能會發現 Android 的分享選單長得跟 iOS 完全不同,且對檔案標題(Title)的支援度也因作業系統版本而異。
實戰範例:AuraFlow 「圖表匯出」完整流程
讓我們把這一切串起來。假設我們正在實作 AuraFlow 的「導出人類圖 PDF」功能:
import * as Print from 'expo-print';
import * as Sharing from 'expo-sharing';
import * as FileSystem from 'expo-file-system';
import { Alert } from 'react-native';
async function exportHumanDesignChart(userData) {
try {
// 1. 準備 HTML 內容(包含動態資料與 SVG)
const htmlContent = `
<html>
<style>
body { font-family: sans-serif; padding: 40px; }
.chart { width: 100%; height: auto; }
h1 { color: #4A90E2; }
</style>
<body>
<h1>${userData.name} 的人類圖分析</h1>
<div class="chart">
<!-- 這裡可以嵌入複雜的 SVG 代碼 -->
<svg>...</svg>
</div>
<p>類型:${userData.type}</p>
</body>
</html>
`;
// 2. 產出 PDF 檔案(靜默模式)
const { uri } = await Print.printToFileAsync({
html: htmlContent,
base64: false // 除非你要直接上傳到 API,否則設為 false 效能較好
});
console.log('PDF 已產生在:', uri);
// 3. 檢查分享功能是否可用
const isSharingAvailable = await Sharing.isAvailableAsync();
if (isSharingAvailable) {
// 4. 開啟系統分享面板
await Sharing.shareAsync(uri, {
mimeType: 'application/pdf',
dialogTitle: '分享你的分析報告',
UTI: 'com.adobe.pdf', // iOS 專用,協助辨識檔案類型
});
// 5. (選修) 分享完後的清理工作
// 注意:有些分享行為是非同步的,太快刪除可能會導致分享失敗
// 建議在 APP 下次啟動或確認分享視窗關閉後執行清理
} else {
Alert.alert('抱歉', '您的裝置不支援分享功能');
}
} catch (error) {
console.error('匯出失敗:', error);
Alert.alert('錯誤', '無法生成 PDF,請稍後再試');
}
}
這裡隱藏的細節:
- URI 格式:
printToFileAsync返回的是一個file://開頭的本地路徑。在 Android 上,如果你嘗試手動傳遞這個路徑而不透過expo-sharing,其他 APP 通常會報錯說「沒有權限」,因為這違反了沙盒原則。expo-sharing的價值就在於它能自動將這個路徑轉換成一個具備臨時讀取權限的content://路徑。 - SVG 的魔力: 對於內容類 APP,PDF 的清晰度至關重要。使用 HTML-to-PDF 的最大好處是 SVG 在 PDF 中是向量的。這意味著無論使用者在電腦上將這份報告放大到幾倍,人類圖的線條永遠不會有鋸齒。
- 記憶體壓力: 如果你的 HTML 包含大量的 Base64 圖片,轉換過程會非常消耗 JS Thread 與 UI Thread 的通訊頻寬(因為大型字串要跨越 Bridge)。建議儘量使用外部連結的圖片或純 SVG。
常見雷區與優化建議
1. Android 的 FileProvider 陷阱
雖然 Expo 處理了大部分問題,但如果你是在開發原生模組(Native Modules)或是使用一些較舊的第三方套件,Android 常會因為沒設定好 AndroidManifest.xml 中的 FileProvider 而崩潰。使用 Expo 的好處是它預設幫你把這套「臨時通行證」制度建立好了。
2. 暫存檔的「衛生」問題
每次點擊下載,都會產生一個新的 PDF。如果不清理,使用者手機裡可能會累積幾百 MB 的無用快取。
- 最佳實踐: 使用
FileSystem.readDirectoryAsync(FileSystem.cacheDirectory)檢查快取資料夾,並刪除所有以Print開頭的舊檔案。
3. 多分頁的 CSS 控制
如果人類圖報告很長,需要分頁,記得在 HTML 中使用 CSS 屬性 page-break-before: always;。這能確保你的分析標題不會尷尬地被切在兩頁之間。
總結
文件匯出看似只是一個簡單的功能,但它實際上整合了 Web 渲染技術(HTML/CSS)、**系統檔案權限(Sandbox)**與 原生組件通訊(Intents/Activity)。
理解了這個三階火箭的架構,當你遇到「為什麼 Android 抓不到檔案」或是「為什麼 iOS 預覽圖是空白的」這類問題時,你就能準確判斷是哪一階火箭出了問題:是 HTML 沒寫好(第一階)、路徑傳錯了(第二階),還是 MIME 類型設定錯誤導致系統不認得(第三階)。
接下來...
掌握了如何讓使用者「帶走」資料後,下一個關鍵課題是如何「主動召喚」使用者回來。
我們將進入 Part 2:推播通知完整架構。這是一個更為複雜的原生功能整合挑戰——我們將拆解 Apple 的 APNs 與 Google 的 FCM 兩套完全不同的推播體系,並看看 Expo 如何用一套 Unified API 讓你能優雅地管理這些來自雲端的「敲門聲」。同時,我們也會對應到 AuraFlow 的站方公告系統,看看它是如何在實務中運作的。