前言
最近使用了 react-to-image 實現將網頁內容存成 PNG 檔的功能,過程中有發現一些內容跑版以及手機版 UX 不如預期的議題,所以簡單寫篇文章跟大家做分享。
react-to-image 簡介
hugocxl/react-to-image 是一套基於 html-to-image 的工具,它可以將 React component 渲染成各種格式的圖片(svg/png/jpeg/canvas/blob),簡單、輕量、 使用 Typescript 和 React Hook 是它的特色。
基本用法
以最基本的轉 png 來說,useToPng
這個 hook 會 return 三個變數:
- state:是一個物件,會描述上一次轉換的狀態,包含 data、status、isIdle、isSuccess、isError⋯⋯等資訊,簡單的圖片生成可能用不太到這個變數,不使用也可以。
- convert:是一個圖片生成函式,執行這個 convert() 就可以將指定的 React 元件轉為圖片資料,並且可以在 onSuccess callback 裡面去做後續處理。
- ref:HtmlDivNode,綁定在要被轉換成圖片的 React 元件 上。如果是使用 selector 指定元件,則可不 return 這個變數。
import { useToPng } from '@hugocxl/react-to-image'
export default function App() {
const [state, convert, ref] = useToPng<HTMLDivElement>({
quality: 0.8, // 圖片品質
onSuccess: data => console.log('Success', data)
})
return (
<div ref={ref}>
<h1>My component</h1>
<button onClick={convert}>Download</button>
</div>
)
}
使用 selector
如果因為一些原因你不能使用 ref 綁定,也可以用 selector 的方式去抓取畫面上的 ID 或 class。
import { useToPng } from '@hugocxl/react-to-image'
export default function App() {
const [state, convert] = useToPng<HTMLDivElement>({
selector: '#my-element',
onSuccess: data => console.log('Success', data)
})
return (
<div>
<button onClick={convert}>Copy to clipboard</button>
</div>
)
}
使用 callback
工具本身提供 onStart、onSuccess 和 onError 三個 callback 來讓你做不同的處理。
import { useToPng } from '@hugocxl/react-to-image'
export default function App() {
const [state, convert, ref] = useToPng<HTMLDivElement>({
onStart: data => console.log('Starting...'),
onSuccess: data => console.log('Success', data),
onError: error => console.log('Error', error),
})
return (
<div ref={ref}>
<h1>My component</h1>
<button onClick={convert}>Download</button>
</div>
)
}
如何下載圖片
useToPng 在 onSuccess 時可以使用參數 data,這個 data 會是 base64 的 string,如果要瀏覽器自動下載圖片的話,我們就需要用 createElement 的方式製作一個 link 並點擊:
import { useToPng } from '@hugocxl/react-to-image'
export default function App() {
const [state, convert, ref] = useToPng<HTMLDivElement>({
quality: 0.8,
onSuccess: (base64: string) => {
const link = document.createElement('a');
link.download = 'my-image-name';
link.href = base64;
link.click();
}
})
return (
<div ref={ref}>
<h1>My component</h1>
<button onClick={convert}>Download</button>
</div>
)
}
手機版 UX:「下載檔案」還是「儲存影像至相簿」
在電腦版的時候,利用上方 create link 的方式是沒有什麼問題的,圖片就會像往常一樣下載下來;但是在手機版,這種方式會是「下載檔案」,user 會需要到手機的「檔案管理系統」去找到下載下來你的圖片,而非去「相簿」瀏覽相片。
以 iOS 來說,在 safari 下載檔案時會問你要檢視還是下載,若下載下來後,就需要去「檔案 > 下載項目」裡去找到它:
但其實這樣的行為不是一般 user 所預期的!大部份的 user 是習慣到「相簿」裡找圖片,而非「檔案」。
由於 Javascript 無權直接把相片存到 user 的手機裡,我們能做的是「觸發分享」,由 user 自行決定「儲存影像」或是真的「分享」出去。
Web Share API:觸發手機分享圖片功能
我們需要用 useToBlob
這個 hook 取得 blob 格式的圖片,並利用 Web Share API navigator.share()
觸發手機瀏覽器的分享。現在新的 Android 和 iOS 大部份都有支援此 API(可參考 MDN 文件)。
import { useToBlob } from '@hugocxl/react-to-image'
const [state, downloadPng, ref] = useToBlob<HTMLDivElement>({
quality: 0.8,
onSuccess: async (blob) => {
const shareData = {
files: [new File([blob], `"my-image-name.png`, { type: "image/png" })],
};
if (navigator.canShare && navigator.canShare(shareData)) {
try {
await navigator.share(shareData);
} catch {
// 若使用者中斷分享,會在這裡被捕獲
}
}
},
});
在 iOS 當中,分享效果如下圖,而 user 可以自己選擇將圖片「儲存影像」或是真的分享出去。
但為了應付不支援 Web Share API 的手機以及使用習慣就是「下載檔案」的電腦,我們仍需要保留原本 create link 的方式。
同時滿足電腦版與手機版的方式
我們一樣用 useToPng 這個 hook 取得 base64 的 string,用 navigator.canShare
判斷是否可以使用 Web Share PAI,可以的話使用 fetch 轉換 base64 取得 blob 後分享,否則就是以 create link 的方式下載檔案。
import { useToPng } from '@hugocxl/react-to-image'
const [state, downloadPng, ref] = useToPng<HTMLDivElement>({
quality: 0.8,
onSuccess: async (base64: string) => {
if (navigator.canShare) {
const response = await fetch(base64);
const blob = await response.blob();
const shareData = {
files: [new File([blob], `"my-image-name`, { type: "image/png" })],
};
if (navigator.canShare(shareData)) {
try {
await navigator.share(shareData);
} catch {
// 使用者若中斷分享,會在這裡被 catch
}
}
} else {
const link = document.createElement("a");
link.download = "my-image-name";
link.href = base64;
link.click();
}
},
});
這樣子就大功告成囉。