想要複製圖像?Clipboard API 瞭解一下

在寫了 這個 29.7 K 的剪貼板 JS 庫有點東西! 這篇文章之後,收到了小夥伴提的兩個問題:

1.clipboard.js 這個庫除了複製文字之外,能複製圖像麼?

2.clipboard.js  這個庫依賴的 document.execCommand API 已被廢棄了,以後應該怎麼辦?

(圖片來源:https://developer.mozilla.org/zh-CN/docs/Web/API/Document/execCommand)

接下來,本文將圍繞上述兩個問題展開,不過在看第一個問題之前,我們先來簡單介紹一下 剪貼板 📋。

剪貼板(英語:clipboard),有時也稱剪切板、剪貼簿、剪貼本。它是一種軟件功能,通常由操作系統提供,作用是使用複製和粘貼操作短期存儲數據和在文檔或應用程序間轉移數據。它是圖形用戶界面(GUI)環境中最常用的功能之一,通常實現爲匿名、臨時的數據緩衝區,可以被環境內的大部分或所有程序使用編程接口訪問。—— 維基百科

通過以上的描述我們可以知道,剪貼板架起了一座橋樑,使得在各種應用程序之間,傳遞和共享信息成爲可能。然而美中不足的是,剪貼板只能保留一份數據,每當新的數據傳入,舊的便會被覆蓋。

瞭解完 剪貼板 📋 的概念和作用之後,我們馬上來看一下第一個問題:clipboard.js 這個庫除了複製文字之外,能複製圖像麼?

一、clipboard.js 能否複製圖像?

clipboard.js 是一個用於將 文本 複製到剪貼板的 JS 庫。沒有使用 Flash,沒有使用任何框架,開啓 gzipped 壓縮後僅僅只有 3kb

(圖片來源:https://clipboardjs.com/#example-text)

當你看到 “A modern approach to copy text to clipboard” 這個描述,你是不是已經知道答案了。那麼實際的情況是怎樣呢?下面我們來動手驗證一下。在 這個 29.7 K 的剪貼板 JS 庫有點東西! 這篇文章中,阿寶哥介紹了在實例化 ClipboardJS 對象時,可以通過 options 對象的 target 屬性來設置複製的目標:

// https://github.com/zenorocha/clipboard.js/blob/master/demo/function-target.html
let clipboard = new ClipboardJS('.btn', {
  targetfunction({
    return document.querySelector('div');
  }
});

利用 clipboard.js 的這個特性,我們可以定義以下 HTML 結構:

<div id="container">
   <img src="http://cdn.semlinker.com/abao.png" width="80" height="80"/>
   <p>大家好,我是阿寶哥</p>
</div>
<button class="btn">複製</button>

然後在實例化 ClipboardJS 對象時設置複製的目標是 #container 元素:

const clipboard = new ClipboardJS(".btn", {
  targetfunction ({
    return document.querySelector("#container");
  }
});

之後,我們點擊頁面中的 複製 按鈕,對應的效果如下圖所示:

觀察上圖可知,頁面中的圖像和文本都已經被複制了。對於文本來說,大家應該都很清楚。而對於圖像來說,到底複製了什麼?我們又該如何獲取已複製的內容呢?針對這個問題,我們可以利用 HTMLElement 對象上的 onpaste 屬性或者監聽元素上的 paste 事件。

這裏我們通過設置 document 對象的 onpaste 屬性,來打印一下粘貼事件對應的事件對象:

document.onpaste = function (e{
  console.dir(e);
}

當我們點擊 複製 按鈕,然後在頁面執行 粘貼 操作後,控制檯會打印出以下內容:

通過上圖可知,在 ClipboardEvent 對象中含有一個 clipboardData 屬性,該屬性包含了與剪貼板相關聯的數據。詳細分析了 clipboardData 屬性之後,我們發現已複製的圖像和普通文本被封裝爲 DataTransferItem 對象。

爲了更方便地分析 DataTransferItem 對象,阿寶哥重新更新了 document 對象的 onpaste 屬性:

在上圖中,我們可以清楚的看到 DataTransferItem 對象上含有 kindtype 屬性分別用於表示數據項的類型(string 或 file)及數據對應的 MIME 類型。利用 DataTransferItem 對象提供的 getAsString 方法,我們可以獲取該對象中保存的數據:

相信看完以上的輸出結果,小夥伴們就很清楚第一個問題的答案了。那麼如果想要複製圖像的話,應該如何實現呢?其實這個問題的答案與小夥伴提的第二個問題的答案是一樣的,我們可以利用 Clipboard API 來實現複製圖像的問題及解決 document.execCommand API 已被廢棄的問題。

接下來,我們的目標就是實現複製圖像的功能了,因爲要利用到 Clipboard API,所以阿寶哥先來介紹一下該 API。

二、Clipboard API 簡介

Clipboard 接口實現了 Clipboard API,如果用戶授予了相應的權限,就能提供系統剪貼板的讀寫訪問。在 Web 應用程序中,Clipboard API 可用於實現剪切、複製和粘貼功能。該 API 用於取代通過 document.execCommand API 來實現剪貼板的操作。

在實際項目中,我們不需要手動創建 Clipboard 對象,而是通過 navigator.clipboard 來獲取  Clipboard 對象:

在獲取 Clipboard 對象之後,我們就可以利用該對象提供的 API 來訪問剪貼板,比如:

navigator.clipboard.readText().then(
  clipText => document.querySelector(".editor").innerText = clipText);

以上代碼將 HTML 中含有 .editor 類的第一個元素的內容替換爲剪貼板的內容。如果剪貼板爲空,或者不包含任何文本,則元素的內容將被清空。這是因爲在剪貼板爲空或者不包含文本時,readText 方法會返回一個空字符串。

在繼續介紹 Clipboard API 之前,我們先來看一下 Navigator API: clipboard 的兼容性:

(圖片來源:https://caniuse.com/mdn-api_navigator_clipboard)

異步剪貼板 API 是一個相對較新的 API,瀏覽器仍在逐漸實現它。由於潛在的安全問題和技術複雜性,大多數瀏覽器正在逐步集成這個 API。對於瀏覽器擴展來說,你可以請求 clipboardRead 和 clipboardWrite 權限以使用 clipboard.readText() 和 clipboard.writeText()。

好的,接下來阿寶哥來演示一下如何使用 clipboard 對象提供的 API 來操作剪貼板,以下示例的運行環境是 Chrome 87.0.4280.88

三、將數據寫入到剪貼板

3.1 writeText()

writeText 方法可以把指定的字符串寫入到系統的剪貼板中,調用該方法後會返回一個 Promise 對象:

<button onclick="copyPageUrl()">拷貝當前頁面地址</button>
<script>
   async function copyPageUrl({
     try {
       await navigator.clipboard.writeText(location.href);
       console.log("頁面地址已經被拷貝到剪貼板中");
     } catch (err) {
       console.error("頁面地址拷貝失敗: ", err);
     }
  }
</script>

對於上述代碼,當用戶點擊 拷貝當前頁面地址 按鈕時,將會把當前的頁面地址拷貝到剪貼板中。

3.2 write()

write 方法除了支持文本數據之外,還支持將圖像數據寫入到剪貼板,調用該方法後會返回一個 Promise 對象。

<button onclick="copyPageUrl()">拷貝當前頁面地址</button>
<script>
   async function copyPageUrl({
     const text = new Blob([location.href], {type'text/plain'});
     try {
       await navigator.clipboard.write(
         new ClipboardItem({
           "text/plain": text,
         }),
       );
       console.log("頁面地址已經被拷貝到剪貼板中");
     } catch (err) {
       console.error("頁面地址拷貝失敗: ", err);
     }
  }
</script>

在以上代碼中,我們先通過 Blob API 創建 Blob 對象,然後使用該 Blob 對象來構造 ClipboardItem 對象,最後再通過 write 方法把數據寫入到剪貼板。介紹完如何將數據寫入到剪貼板,下面我們來介紹如何從剪貼板中讀取數據。

對 Blob API 感興趣的小夥伴,可以閱讀 你不知道的 Blob 這篇文章。

四、從剪貼板中讀取數據

4.1 readText()

readText 方法用於讀取剪貼板中的文本內容,調用該方法後會返回一個 Promise 對象:

<button onclick="getClipboardContents()">讀取剪貼板中的文本</button>
<script>
   async function getClipboardContents({
     try {
       const text = await navigator.clipboard.readText();
       console.log("已讀取剪貼板中的內容:", text);
     } catch (err) {
       console.error("讀取剪貼板內容失敗: ", err);
     }
   }
</script>

對於上述代碼,當用戶點擊 讀取剪貼板中的文本 按鈕時,如果當前剪貼板含有文本內容,則會讀取剪貼板中的文本內容。

4.2 read()

read 方法除了支持讀取文本數據之外,還支持讀取剪貼板中的圖像數據,調用該方法後會返回一個 Promise 對象:

<button onclick="getClipboardContents()">讀取剪貼板中的內容</button>
<script>
async function getClipboardContents({
  try {
    const clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      for (const type of clipboardItem.types) {
        const blob = await clipboardItem.getType(type);
        console.log("已讀取剪貼板中的內容:"await blob.text());
      }
    }
  } catch (err) {
      console.error("讀取剪貼板內容失敗: ", err);
    }
  }
</script>

對於上述代碼,當用戶點擊 讀取剪貼板中的內容 按鈕時,則會開始讀取剪貼板中的內容。到這裏 clipboard 對象中涉及的 4 個 API,阿寶哥都已經介紹完了,最後我們來看一下如何實現複製圖像的功能。

五、實現複製圖像的功能

在最後的這個示例中,阿寶哥將跟大家一步步實現複製圖像的核心功能,除了複製圖像之外,還會同時支持複製文本。在看具體代碼前,我們先來看一下實際的效果:

在上圖對應的網頁中,我們先點擊 複製 按鈕,則圖像和文本都會被選中。之後,我們在點擊 粘貼 按鈕,則控制檯會輸出從剪貼板中讀取的實際內容。在分析具體的實現方式前,我們先來看一下對應的頁面結構:

<div id="container">
   <img src="http://cdn.semlinker.com/abao.png" width="80" height="80" />
   <p>大家好,我是阿寶哥</p>
</div>
<button onclick="writeDataToClipboard()">複製</button>
<button onclick="readDataFromClipboard()">粘貼</button>

上面的頁面結構很簡單,下一步我們來逐步分析一下以上功能的實現過程。

5.1 請求剪貼板寫權限

默認情況下,會爲當前的激活的頁面自動授予剪貼板的寫入權限。出於安全方面考慮,這裏我們還是主動向用戶請求剪貼板的寫入權限:

async function askWritePermission({
  try {
    const { state } = await navigator.permissions.query({
      name"clipboard-write",
    });
      return state === "granted";
  } catch (error) {
      return false;
  }
}

5.2 往剪貼板寫入圖像和普通文本數據

要往剪貼板寫入圖像數據,我們就需要使用 navigator.clipboard 對象提供的 write 方法。如果要寫入圖像數據,我們就需要獲取該圖像對應的 Blob 對象,這裏我們可以通過 fetch API 從網絡上獲取圖像對應的響應對象並把它轉化成 Blob 對象,具體實現方式如下:

async function createImageBlob(url{
  const response = await fetch(url);
  return await response.blob();
}

而對於普通文本來說,只需要使用前面介紹的 Blob API 就可以把普通文本轉換爲 Blob 對象:

function createTextBlob(text{
  return new Blob([text], { type"text/plain" });
}

在創建完圖像和普通文本對應的 Blob 對象之後,我們就可以利用它們來創建 ClipboardItem 對象,然後再調用 write 方法把這些數據寫入到剪貼板中,對應的代碼如下所示:

async function writeDataToClipboard({
  if (askWritePermission()) {
    if (navigator.clipboard && navigator.clipboard.write) {
        const textBlob = createTextBlob("大家好,我是阿寶哥");
        const imageBlob = await createImageBlob(
          "http://cdn.semlinker.com/abao.png"
        );
        try {
          const item = new ClipboardItem({
            [textBlob.type]: textBlob,
            [imageBlob.type]: imageBlob,
          });
          select(document.querySelector("#container"));
          await navigator.clipboard.write([item]);
          console.log("文本和圖像複製成功");
        } catch (error) {
          console.error("文本和圖像複製失敗", error);
        }
      }
   }
}

在以上代碼中,使用了一個 select 方法,該方法用於實現選擇的效果,對應的代碼如下所示:

function select(element{
  const selection = window.getSelection();
  const range = document.createRange();
  range.selectNodeContents(element);
  selection.removeAllRanges();
  selection.addRange(range);
}

通過 writeDataToClipboard 方法,我們已經把圖像和普通文本數據寫入剪貼板了。下面我們來使用 navigator.clipboard 對象提供的 read 方法,來讀取已寫入的數據。如果你需要讀取剪貼板的數據,則需要向用戶請求 clipboard-read 權限。

5.3 請求剪貼板讀取權限

這裏我們定義了一個 askReadPermission 函數來向用戶請求剪貼板讀取權限:

async function askReadPermission({
  try {
    const { state } = await navigator.permissions.query({
      name"clipboard-read",
    });
    return state === "granted";
  } catch (error) {
    return false;
  }
}

當調用 askReadPermission 方法後,將會向當前用戶請求剪貼板讀取權限,對應的效果如下圖所示:

5.4 讀取剪貼板中已寫入的數據

創建好 askReadPermission 函數,我們就可以利用之前介紹的 navigator.clipboard.read 方法來讀取剪貼板的數據了:

async function readDataFromClipboard({
  if (askReadPermission()) {
    if (navigator.clipboard && navigator.clipboard.read) {
      try {
        const clipboardItems = await navigator.clipboard.read();
        for (const clipboardItem of clipboardItems) {
          console.dir(clipboardItem);
          for (const type of clipboardItem.types) {
            const blob = await clipboardItem.getType(type);
            console.log("已讀取剪貼板中的內容:"await blob.text());
          }
        }
      } catch (err) {
         console.error("讀取剪貼板內容失敗: ", err);
      }
     }
   }
}

其實,除了點擊 粘貼 按鈕之外,我們還可以通過監聽 paste 事件來讀取剪貼板中的數據。需要注意的是,如果當前的瀏覽器不支持異步 Clipboard API,我們可以通過 clipboardData.getData 方法來讀取剪貼板中的文本數據:

document.addEventListener('paste'async (e) => {
  e.preventDefault();
  let text;
  if (navigator.clipboard) {
    text = await navigator.clipboard.readText();
  } else {
    text = e.clipboardData.getData('text/plain');
  }
  console.log('已獲取的文本數據: ', text);
});

而對於圖像數據,則可以通過以下方式進行讀取:

const IMAGE_MIME_REGEX = /^image\/(p?jpeg|gif|png)$/i;

document.addEventListener("paste"async (e) => {
  e.preventDefault();
  if (navigator.clipboard) {
    let clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
       for (const type of clipboardItem.types) {
         if (IMAGE_MIME_REGEX.test(type)) {
           const blob = await clipboardItem.getType(type);
           loadImage(blob);
           return;
         }
        }
     }
   } else {
       const items = e.clipboardData.items;
       for (let i = 0; i < items.length; i++) {
         if (IMAGE_MIME_REGEX.test(items[i].type)) {
         loadImage(items[i].getAsFile());
         return;
       }
    }
  }
});

以上代碼中的 loadImage 方法用於實現把複製的圖片插入到當前選區已選擇的區域中,對應的代碼如下:

function loadImage(file{
  const reader = new FileReader();
  reader.onload = function (e{
    let img = document.createElement("img");
    img.src = e.target.result;

    let range = window.getSelection().getRangeAt(0);
    range.deleteContents();
    range.insertNode(img);
  };
  reader.readAsDataURL(file);
}

在前面代碼中,我們監聽了 document 對象的 paste 事件。除了該事件之外,與剪貼板相關的常見事件還有 copycut 事件。篇幅有限,阿寶哥就不繼續展開介紹了,感興趣的小夥伴可以自行閱讀相關資料。

好的,至此本文就已經結束了,希望閱讀完本文之後,大家對異步的 Clipboard API 會有些瞭解,有寫得不清楚的地方,歡迎你隨時跟阿寶哥交流喲。

六、參考資源

  • 維基百科 - 剪貼板
  • MDN - Clipboard
  • MDN - execCommand
  • Web.dev - async-clipboard


順手點“在看”,每天早下班;轉發加關注,共奔小康路~

加站長好友可進微信羣,跟衆多大佬一起交流技術!

本文分享自微信公衆號 - 1024譯站(trans1024)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章