【筆記】使用 react-to-image 實現網頁截圖功能並考量電腦與手機版差異

React to Image(圖片來源:hugocxl/react-to-image

前言

最近使用了 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 下載檔案時會問你要檢視還是下載,若下載下來後,就需要去「檔案 > 下載項目」裡去找到它:

iOS 下載檔案介面
iOS 檔案 > 下載項目

但其實這樣的行為不是一般 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 可以自己選擇將圖片「儲存影像」或是真的分享出去。

iOS 分享

但為了應付不支援 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();
    }
  },
});

這樣子就大功告成囉。

您可能也有興趣:

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments