<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【W(wǎng)eb技術(shù)】1030- 拖拽竟然還能這樣玩!

          共 22083字,需瀏覽 45分鐘

           ·

          2021-07-27 22:10

          在大多數(shù)低代碼平臺(tái)中的設(shè)計(jì)器都支持組件拖拽的功能,這樣大大地提高了用戶的設(shè)計(jì)體驗(yàn)。而拖拽另一個(gè)比較常見的場(chǎng)景就是文件上傳,通過(guò)拖拽的方式,可以讓用戶方便地上傳文件。其實(shí)利用拖拽功能,我們還可以 跨越瀏覽器的邊界,實(shí)現(xiàn)數(shù)據(jù)共享

          那么如何 跨越瀏覽器的邊界,實(shí)現(xiàn)數(shù)據(jù)共享 呢?本文阿寶哥將介紹谷歌的一個(gè)開源項(xiàng)目 —— transmat,利用該項(xiàng)目可以實(shí)現(xiàn)上述功能。不僅如此,該項(xiàng)目還可以幫助我們實(shí)現(xiàn)一些比較好玩的功能,比如針對(duì)不同的可釋放目標(biāo),做出不同的響應(yīng)。

          下面我們先通過(guò) 4 張 Gif 動(dòng)圖來(lái)感受一下,使用 transmat 開發(fā)的 神奇、好玩 的拖拽功能。

          圖 1(把可拖拽的元素,拖拽至富文本編輯器)

          圖 2(把可拖拽的元素,拖拽至 Chrome 瀏覽器,也支持其他瀏覽器)

          圖 3(把可拖拽的元素,拖拽至自定義的釋放目標(biāo))

          圖 4(把可拖拽的元素,拖拽至 Chrome 開發(fā)者工具)

          以上示例使用的瀏覽器版本:Chrome 91.0.4472.114(正式版本) (x86_64)

          以上 4 張圖中的 可拖拽元素都是同一個(gè)元素,當(dāng)它被放置到不同的可釋放目標(biāo)時(shí),產(chǎn)生了不同的效果。同時(shí),我們也跨越了瀏覽器的邊界,實(shí)現(xiàn)了數(shù)據(jù)的共享??赐暌陨?4 張動(dòng)圖,你是不是覺得挺神奇的。其實(shí)除了拖拽之外,該示例也支持復(fù)制、粘貼操作。不過(guò),在詳細(xì)介紹如何使用 transmat 實(shí)現(xiàn)上述功能之前,我們先來(lái)簡(jiǎn)單介紹一下 transmat 這個(gè)庫(kù)。

          一、Transmat 簡(jiǎn)介

          Transmat 是一個(gè)圍繞 DataTransfer API 的小型庫(kù) ,它使用 drag-dropcopy-paste 交互簡(jiǎn)化了在 Web 應(yīng)用程序中傳輸和接收數(shù)據(jù)的過(guò)程。DataTransfer  API 能夠?qū)⒍喾N不同類型的數(shù)據(jù)傳輸?shù)接脩粼O(shè)備上的其他應(yīng)用程序,該 API 所支持的數(shù)據(jù)類型,常見的有這幾種:text/plain、text/htmlapplication/json 等。

          (圖片來(lái)源:https://google.github.io/transmat/)

          了解完 transmat 是什么之后,我們來(lái)看一下它的應(yīng)用場(chǎng)景:

          • 想以便捷的方式與外部應(yīng)用程序集成。
          • 希望為用戶提供與其他應(yīng)用程序共享數(shù)據(jù)的能力,即使是那些你不知道的應(yīng)用程序。
          • 希望外部應(yīng)用程序能夠與你的 Web 應(yīng)用程序深度集成。
          • 想讓你的應(yīng)用程序更好地適應(yīng)用戶現(xiàn)有的工作流程。

          現(xiàn)在你已經(jīng)對(duì) transmat 有了一定的了解,下面我們來(lái)分析如何使用 transmat 實(shí)現(xiàn)以上 4 張 Gif 動(dòng)圖對(duì)應(yīng)的功能。

          二、Transmat 實(shí)戰(zhàn)

          2.1 transmat-source

          html

          在以下代碼中,我們?yōu)?div#source 元素添加了 draggable 屬性,該屬性用于標(biāo)識(shí)元素是否允許被拖動(dòng),它的取值為 truefalse。

          <script src="https://unpkg.com/transmat/lib/index.umd.js"></script>
          <div id="source" draggable="true" tabindex="0">大家好,我是阿寶哥</div>

          css

          #source {
            background#eef;
            border: solid 1px rgba(002550.2);
            border-radius8px;
            cursor: move;
            display: inline-block;
            margin1em;
            padding4em 5em;
          }

          js

          const { Transmat, addListeners, TransmatObserver } = transmat;

          const source = document.getElementById("source");

          addListeners(source, "transmit", (event) => {
            const transmat = new Transmat(event);
            transmat.setData({
              "text/plain""大家好,我是阿寶哥!",
              "text/html"`
                 <h1>大家好,我是阿寶哥</h1>
                 <p>聚焦全棧,專注分享 TS、Vue 3、前端架構(gòu)等技術(shù)干貨。
                    <a  />
               `
          ,
               "text/uri-list""https://juejin.cn/user/764915822103079",
               "application/json": {
                  name"阿寶哥",
                  wechat"semlinker",
                },
             });
           });

          在以上代碼中,我們利用 transmat 這個(gè)庫(kù)提供的 addListeners 函數(shù)為 div#source 元素,添加了 transmit 的事件監(jiān)聽。在對(duì)應(yīng)的事件處理器中,我們先創(chuàng)建了 Transmat 對(duì)象,然后調(diào)用該對(duì)象上的 setData 方法設(shè)置不同 MIME 類型的數(shù)據(jù)。

          下面我們來(lái)簡(jiǎn)單回顧一下,示例中所使用的 MIME 類型:

          • text/plain:表示文本文件的默認(rèn)值,一個(gè)文本文件應(yīng)當(dāng)是人類可讀的,并且不包含二進(jìn)制數(shù)據(jù)。
          • text/html:表示 HTML 文件類型,一些富文本編輯器會(huì)優(yōu)先從 dataTransfer 對(duì)象上獲取 text/html 類型的數(shù)據(jù),如果不存在的話,再獲取 text/plain 類型的數(shù)據(jù)。
          • text/uri-list:表示 URI 鏈接類型,大多數(shù)瀏覽器都會(huì)優(yōu)先讀取該類型的數(shù)據(jù),如果發(fā)現(xiàn)是合法的 URI 鏈接,則會(huì)直接打開該鏈接。如果不是的合法 URI 鏈接,對(duì)于 Chrome 瀏覽器來(lái)說(shuō),它會(huì)讀取 text/plain 類型的數(shù)據(jù)并以該數(shù)據(jù)作為關(guān)鍵詞進(jìn)行內(nèi)容檢索。
          • application/json:表示 JSON 類型,該類型對(duì)前端開發(fā)者來(lái)說(shuō),應(yīng)該都比較熟悉了。

          介紹完 transmat-source 之后,我們來(lái)看一下圖 3 自定義目標(biāo)(transmat-target)的實(shí)現(xiàn)代碼。

          2.2 transmat-target

          html

          <script src="https://unpkg.com/transmat/lib/index.umd.js"></script>
          <div id="target" tabindex="0">放這里喲!</div>

          css

          body {
            text-align: center;
            font1.2em Helvetia, Arial, sans-serif;
          }
          #target {
            border: dashed 1px rgba(0000.5);
            border-radius8px;
            margin1em;
            padding4em;
          }
          .drag-active {
             backgroundrgba(25525500.1);
          }
          .drag-over {
             backgroundrgba(25525500.5);
          }

          js

          const { Transmat, addListeners, TransmatObserver } = transmat;

          const target = document.getElementById("target");

          addListeners(target, "receive", (event) => {
            const transmat = new Transmat(event);
            // 判斷是否含有"application/json"類型的數(shù)據(jù)
            // 及事件類型是否為drop或paste事件
            if (transmat.hasType("application/json"
              && transmat.accept()
            ) {
              const jsonString = transmat.getData("application/json");
              const data = JSON.parse(jsonString);
              target.textContent = jsonString;
            }
          });

          在以上代碼中,我們利用 transmat 這個(gè)庫(kù)提供的 addListeners 函數(shù)為 div#target 元素,添加了 receive 的事件監(jiān)聽。顧名思義,該 receive 事件表示接收消息。在對(duì)應(yīng)的事件處理器中,我們通過(guò) transmat 對(duì)象的 hasType 方法過(guò)濾了 application/json 的消息,然后通過(guò) JSON.parse 方法進(jìn)行反序列化獲得對(duì)應(yīng)的數(shù)據(jù),同時(shí)把對(duì)應(yīng) jsonString 的內(nèi)容顯示在 div#target 元素內(nèi)。

          在圖 3 中,當(dāng)我們把可拖拽的元素,拖拽至自定義的釋放目標(biāo)時(shí),會(huì)產(chǎn)生高亮效果,具體如下圖所示:

          這個(gè)效果是利用 transmat 這個(gè)庫(kù)提供的 TransmatObserver 類來(lái)實(shí)現(xiàn),該類可以幫助我們響應(yīng)用戶的拖拽行為,具體的使用方式如下所示:

          const obs = new TransmatObserver((entries) => {
            for (const entry of entries) {
              const transmat = new Transmat(entry.event);
              if (transmat.hasType("application/json")) {
                entry.target.classList.toggle("drag-active", entry.isActive);
                entry.target.classList.toggle("drag-over", entry.isTarget);
              }
            }
          });
          obs.observe(target);

          第一次看到 TransmatObserver 之后,阿寶哥立馬想到了 MutationObserver API,因?yàn)樗鼈兌际?strong style="line-height: 1.75em;">觀察者且擁有類似的 API。利用 MutationObserver API 我們可以監(jiān)視 DOM 的變化。DOM 的任何變化,比如節(jié)點(diǎn)的增加、減少、屬性的變動(dòng)、文本內(nèi)容的變動(dòng),通過(guò)這個(gè) API 我們都可以得到通知。如果你對(duì)該 API 感興趣的話,可以閱讀 是誰(shuí)動(dòng)了我的 DOM? 這篇文章。

          現(xiàn)在我們已經(jīng)知道 transmat 這個(gè)庫(kù)如何使用,接下來(lái)阿寶哥將帶大家一起來(lái)分析這個(gè)庫(kù)背后的工作原理。

          Transmat 使用示例:Transmat Demo

          https://gist.github.com/semlinker/c40baa3d4a0567e555e2e839c84d10dd

          三、Transmat 源碼分析

          在 transmat 源碼分析環(huán)節(jié),因?yàn)樵谇懊鎸?shí)戰(zhàn)部分,我們使用到了 addListenersTransmat、TransmatObserver 這三個(gè) “函數(shù)” 來(lái)實(shí)現(xiàn)核心的功能,所以接下來(lái)的源碼分析,我們將圍繞它們展開。這里我們先來(lái)分析 addListeners 函數(shù)。

          3.1 addListeners 函數(shù)

          addListeners 函數(shù)用于設(shè)置監(jiān)聽器,調(diào)用該函數(shù)后會(huì)返回一個(gè)用于移除事件監(jiān)聽的函數(shù)。在分析函數(shù)時(shí),阿寶哥習(xí)慣先分析函數(shù)的簽名:

          // src/transmat.ts
          function addListeners<T extends Node>(
            target: T,
            type: TransferEventType,
            listener: (event: DataTransferEvent, target: T) => void,
            options = {dragDrop: true, copyPaste: true}
          ): () => void

          通過(guò)觀察以上的函數(shù)簽名,我們可以很直觀的了解該函數(shù)的輸入和輸出。該函數(shù)支持以下 4 個(gè)參數(shù):

          • target:表示監(jiān)聽的目標(biāo),它的類型是 Node 類型。
          • type:表示監(jiān)聽的類型,該參數(shù)的類型 TransferEventType 是一個(gè)聯(lián)合類型 —— 'transmit' | 'receive'。
          • listener:表示事件監(jiān)聽器,它支持的事件類型為 DataTransferEvent,該類型也是一個(gè)聯(lián)合類型 —— DragEvent | ClipboardEvent,即支持拖拽事件和剪貼板事件。
          • options:表示配置對(duì)象,用于設(shè)置是否允許拖拽和復(fù)制、粘貼操作。

          在 addListeners 函數(shù)體中,主要包含以下 3 個(gè)步驟:

          • 步驟 ①:根據(jù) isTransmitEventoptions.copyPaste 的值,注冊(cè)剪貼板相關(guān)的事件。
          • 步驟 ②:根據(jù) isTransmitEventoptions.dragDrop 的值,注冊(cè)拖拽相關(guān)的事件。
          • 步驟 ③:返回函數(shù)對(duì)象,用于移除已注冊(cè)的事件監(jiān)聽。
          // src/transmat.ts
          export function addListeners<T extends Node>(
            target: T,
            type: TransferEventType, // 'transmit' | 'receive'
            listener: (event: DataTransferEvent, target: T) => void,
            options = {dragDrop: true, copyPaste: true}
          ): () => void 
          {
            const isTransmitEvent = type === 'transmit';
            let unlistenCopyPaste: undefined | (() => void);
            let unlistenDragDropundefined | (() => void);

            if (options.copyPaste) {
              // ① 可拖拽源監(jiān)聽cutcopy事件,可釋放目標(biāo)監(jiān)聽paste事件
              const events = isTransmitEvent ? ['cut', 'copy'] : ['paste'];
              const parentElement = target.parentElement!;
              unlistenCopyPaste = addEventListeners(parentElement, events, event => {
                if (!target.contains(document.activeElement)) {
                  return;
                }
                listener(event as DataTransferEvent, target);

                if (event.type === 'copy' || event.type === 'cut') {
                  event.preventDefault();
                }
              }
          );
            }

            if (options.dragDrop) {
              // ② 可拖拽源監(jiān)聽dragstart事件,可釋放目標(biāo)監(jiān)聽dragoverdrop事件
              const events = isTransmitEvent ? ['dragstart'] : ['dragover', 'drop'];
              unlistenDragDrop = addEventListeners(target, events, event => {
                listener(event as DataTransferEvent, target);
              }
          );
            }

            // ③ 返回函數(shù)對(duì)象,用于移除已注冊(cè)的事件監(jiān)聽
            return () =>
           {
              unlistenCopyPaste && unlistenCopyPaste();
              unlistenDragDrop && unlistenDragDrop();
            };
          }

          以上代碼的事件監(jiān)聽最終是通過(guò)調(diào)用 addEventListeners 函數(shù)來(lái)實(shí)現(xiàn),在該函數(shù)內(nèi)部會(huì)循環(huán)調(diào)用 addEventListener 方法來(lái)添加事件監(jiān)聽。以前面 Transmat 的使用示例為例,在對(duì)應(yīng)的事件處理回調(diào)函數(shù)內(nèi)部,我們會(huì)以 event 事件對(duì)象為參數(shù),調(diào)用 Transmat 構(gòu)造函數(shù)創(chuàng)建 Transmat 實(shí)例。那么該實(shí)例有什么作用呢?要搞清楚它的作用,我們就需要來(lái)了解 Transmat 類。

          3.2 Transmat 類

          Transmat 類被定義在 src/transmat.ts 文件中,該類的構(gòu)造函數(shù)含有一個(gè)類型為 DataTransferEvent 的參數(shù) event

          // src/transmat.ts
          export class Transmat {
            public readonly event: DataTransferEvent;
            public readonly dataTransfer: DataTransfer;

            // type DataTransferEvent = DragEvent | ClipboardEvent;
            constructor(event: DataTransferEvent) {
              this.event = event;
              this.dataTransfer = getDataTransfer(event);
            }
          }

          Transmat 構(gòu)造函數(shù)內(nèi)部還會(huì)通過(guò) getDataTransfer 函數(shù)來(lái)獲取 DataTransfer 對(duì)象并賦值給內(nèi)部的 dataTransfer 屬性。DataTransfer 對(duì)象用于保存拖動(dòng)并放下(drag and drop)過(guò)程中的數(shù)據(jù)。它可以保存一項(xiàng)或多項(xiàng)數(shù)據(jù),這些數(shù)據(jù)項(xiàng)可以是一種或者多種數(shù)據(jù)類型。

          下面我們來(lái)看一下 getDataTransfer 函數(shù)的具體實(shí)現(xiàn):

          // src/data_transfer.ts
          export function getDataTransfer(event: DataTransferEvent): DataTransfer {
            const dataTransfer =
              (event as ClipboardEvent).clipboardData ??
              (event as DragEvent).dataTransfer;
            if (!dataTransfer) {
              throw new Error('No DataTransfer available at this event.');
            }
            return dataTransfer;
          }

          在以上代碼中,使用了空值合并運(yùn)算符 ??。該運(yùn)算符的特點(diǎn)是:當(dāng)左側(cè)操作數(shù)為 null 或 undefined 時(shí),其返回右側(cè)的操作數(shù),否則返回左側(cè)的操作數(shù)。即先判斷是否為剪貼板事件,如果是的話就會(huì)從 clipboardData 屬性獲取 DataTransfer 對(duì)象。否則,就會(huì)從 dataTransfer 屬性獲取。

          對(duì)于可拖拽源,在創(chuàng)建完 Transmat 對(duì)象之后,我們就可以調(diào)用該對(duì)象上的 setData 方法保存一項(xiàng)或多項(xiàng)數(shù)據(jù)。比如,在以下代碼中,我們?cè)O(shè)置了不同類型的多項(xiàng)數(shù)據(jù):

          transmat.setData({
            "text/plain""大家好,我是阿寶哥!",
            "text/html"`
              <h1>大家好,我是阿寶哥</h1>
            ...
             `
          ,
            "text/uri-list""https://juejin.cn/user/764915822103079",
            "application/json": {
               name"阿寶哥",
               wechat"semlinker",
             },
          });

          了解完 setData 方法的用法之后,我們來(lái)看一下它的具體實(shí)現(xiàn):

          // src/transmat.ts
          setData(
            typeOrEntries: string | {[typestring]: unknown},
            data?: unknown
          ): void {
            if (typeof typeOrEntries === 'string') { 
              this.setData({[typeOrEntries]: data});
            } else {
              // 處理多種類型的數(shù)據(jù)
              for (const [type, data] of Object.entries(typeOrEntries)) {
                const stringData =
                  typeof data === 'object' ? JSON.stringify(data) : `${data}`;
                this.dataTransfer.setData(normalizeType(type), stringData);
               }
             }
          }

          由以上代碼可知,在 setData 方法內(nèi)部最終會(huì)調(diào)用 dataTransfer.setData 方法來(lái)保存數(shù)據(jù)。dataTransfer 對(duì)象的 setData 方法支持兩個(gè)字符串類型的參數(shù):formatdata。它們分別表示要保存的數(shù)據(jù)格式和實(shí)際的數(shù)據(jù)。如果給定數(shù)據(jù)格式不存在,則將對(duì)應(yīng)的數(shù)據(jù)保存到末尾。如果給定數(shù)據(jù)格式已存在,則將使用新的數(shù)據(jù)替換舊的數(shù)據(jù)

          下圖是 dataTransfer.setData 方法的兼容性說(shuō)明,由圖可知主流的現(xiàn)代瀏覽器都支持該方法。

          (圖片來(lái)源:https://caniuse.com/mdn-api_datatransfer_setdata)

          Transmat 類除了擁有 setData 方法之外,它也含有一個(gè) getData 方法,用于獲取已保存的數(shù)據(jù)。getData 方法支持一個(gè)字符串類型的參數(shù) type,用于表示數(shù)據(jù)的類型。在獲取數(shù)據(jù)前,會(huì)調(diào)用 hasType 方法判斷是否含有該類型的數(shù)據(jù)。如果有包含的話,就會(huì)通過(guò) dataTransfer 對(duì)象的 getData 方法來(lái)獲取該類型對(duì)應(yīng)的數(shù)據(jù)。

          // src/transmat.ts
          getData(typestring): string | undefined {
            return this.hasType(type)
              ? this.dataTransfer.getData(normalizeType(type))
              : undefined;
          }

          此外,在調(diào)用 getData 方法前,還會(huì)調(diào)用 normalizeType 函數(shù),對(duì)傳入的 type 類型參數(shù)進(jìn)行標(biāo)準(zhǔn)化操作。具體的如下所示:

          // src/data_transfer.ts
          export function normalizeType(input: string{
            const result = input.toLowerCase();
            switch (result) {
              case 'text':
                return 'text/plain';
              case 'url':
                return 'text/uri-list';
              default:
                return result;
            }
          }

          同樣,我們也來(lái)看一下 dataTransfer.getData 方法的兼容性:

          (圖片來(lái)源:https://caniuse.com/mdn-api_datatransfer_getdata)

          好的,Transmat 類中的 setDatagetData 這兩個(gè)核心方法就先介紹到這里。接下來(lái)我們來(lái)介紹另一個(gè)類 —— TransmatObserver 。

          3.3 TransmatObserver 類

          TransmatObserver 類的作用是可以幫助我們響應(yīng)用戶的拖拽行為,可用于在拖拽過(guò)程中高亮放置區(qū)域。比如,在前面的示例中,我們通過(guò)以下方式來(lái)實(shí)現(xiàn)放置區(qū)域的高亮效果:

          const obs = new TransmatObserver((entries) => {
            for (const entry of entries) {
              const transmat = new Transmat(entry.event);
              if (transmat.hasType("application/json")) {
                entry.target.classList.toggle("drag-active", entry.isActive);
                entry.target.classList.toggle("drag-over", entry.isTarget);
              }
            }
          });
          obs.observe(target);

          同樣,我們先來(lái)分析一下 TransmatObserver 類的構(gòu)造函數(shù):

          // src/transmat_observer.ts
          export class TransmatObserver {
            private readonly targets = new Set<Element>(); // 觀察的目標(biāo)集合
            private prevRecords: ReadonlyArray<TransmatObserverEntry> = []; // 保存前一次的記錄
            private removeEventListeners = () => {};

            constructor(private readonly callback: TransmatObserverCallback) {}
          }

          由以上代碼可知,TransmatObserver 類的構(gòu)造函數(shù)支持一個(gè)類型為 TransmatObserverCallback 的參數(shù) callback,該參數(shù)對(duì)應(yīng)的類型定義如下:

          // src/transmat_observer.ts
          export type TransmatObserverCallback = (
            entries: ReadonlyArray<TransmatObserverEntry>,
            observer: TransmatObserver
          ) => void;

          TransmatObserverCallback 函數(shù)類型接收兩個(gè)參數(shù):entriesobserver。其中 entries 參數(shù)的類型是一個(gè)

          只讀數(shù)組(ReadonlyArray),數(shù)組中每一項(xiàng)的類型是 TransmatObserverEntry,對(duì)應(yīng)的類型定義如下:

          // src/transmat_observer.ts
          export interface TransmatObserverEntry {
            target: Element;
            /** type DataTransferEvent = DragEvent | ClipboardEvent */
            event: DataTransferEvent;
            /** Whether a transfer operation is active in this window. */
            isActive: boolean;
            /** Whether the element is the active target (dragover). */
            isTarget: boolean;
          }

          在前面 transmat-target 的示例中,當(dāng)創(chuàng)建完 TransmatObserver 實(shí)例之后,就會(huì)調(diào)用該實(shí)例的 observe 方法并傳入待觀察的對(duì)象。observe 方法的實(shí)現(xiàn)并不復(fù)雜,具體如下所示:

          // src/transmat_observer.ts
          observe(target: Element) {
            /** private readonly targets = new Set<Element>(); */
            this.targets.add(target);
            if (this.targets.size === 1) {
              this.addEventListeners();
            }
          }

          observe 方法內(nèi)部,會(huì)把需觀察的元素保存到 targets Set 集合中。當(dāng) targets 集合的大小等于 1 時(shí),就會(huì)調(diào)用當(dāng)前實(shí)例的 addEventListeners 方法來(lái)添加事件監(jiān)聽:

          // src/transmat_observer.ts
          private addEventListeners() {
            const listener = this.onTransferEvent as EventListener;
            this.removeEventListeners = addEventListeners(
              document,
              ['dragover''dragend''dragleave''drop'],
              listener,
              true
            );
          }

          在私有的 addEventListeners 方法內(nèi)部,會(huì)利用我們前面介紹的 addEventListeners 函數(shù)來(lái)為 document 元素批量添加與拖拽相關(guān)的事件監(jiān)聽。而對(duì)應(yīng)的事件說(shuō)明如下所示:

          • dragover:當(dāng)元素或選中的文本被拖到一個(gè)可釋放目標(biāo)上時(shí)觸發(fā);
          • dragend:當(dāng)拖拽操作結(jié)束時(shí)觸發(fā)(比如松開鼠標(biāo)按鍵);
          • dragleave:當(dāng)拖拽元素或選中的文本離開一個(gè)可釋放目標(biāo)時(shí)觸發(fā);
          • drop:當(dāng)元素或選中的文本在可釋放目標(biāo)上被釋放時(shí)觸發(fā)。

          其實(shí)與拖拽相關(guān)的事件并不僅僅只有以上四種,如果你對(duì)完整的事件感興趣的話,可以閱讀 MDN 上 HTML 拖放 API 這篇文章。下面我們來(lái)重點(diǎn)分析 onTransferEvent 事件監(jiān)聽器:

          private onTransferEvent = (event: DataTransferEvent) => {
            const records: TransmatObserverEntry[] = [];
            for (const target of this.targets) {
              // 當(dāng)光標(biāo)離開瀏覽器時(shí),對(duì)應(yīng)的事件將會(huì)被派發(fā)到body或html節(jié)點(diǎn)
              const isLeavingDrag =
                event.type === 'dragleave' &&
                  (event.target === document.body ||
                   event.target === document.body.parentElement);

              // 頁(yè)面上是否有拖拽行為發(fā)生
              // 當(dāng)拖拽操作結(jié)束時(shí)觸發(fā)dragend事件
              // 當(dāng)元素或選中的文本在可釋放目標(biāo)上被釋放時(shí)觸發(fā)drop事件
              const isActive = event.type !== 'drop'
                && event.type !== 'dragend' && !isLeavingDrag;

              // 判斷可拖拽的元素是否被拖到target元素上
              const isTargetNode = target.contains(event.target as Node);
              const isTarget = isActive && isTargetNode 
                && event.type === 'dragover';

              records.push({
                target,
                event,
                isActive,
                isTarget,
             });
           }
              
           // 僅當(dāng)記錄發(fā)生變化的時(shí)候,才會(huì)調(diào)用回調(diào)函數(shù)
           if (!entryStatesEqual(records, this.prevRecords)) {
             this.prevRecords = records as ReadonlyArray<TransmatObserverEntry>;
             this.callback(records, this);
           }
          }

          在以上代碼中,使用了 node.contains(otherNode) 方法來(lái)判斷可拖拽的元素是否被拖到 target 元素上。當(dāng) otherNodenode 的后代節(jié)點(diǎn)或者 node 節(jié)點(diǎn)本身時(shí),返回 true,否則返回 false。此外,為了避免頻繁地觸發(fā)回調(diào)函數(shù),在調(diào)用回調(diào)函數(shù)前會(huì)先調(diào)用 entryStatesEqual 函數(shù)來(lái)檢測(cè)記錄是否發(fā)生變化。entryStatesEqual 函數(shù)的實(shí)現(xiàn)比較簡(jiǎn)單,具體如下所示:

          // src/transmat_observer.ts
          function entryStatesEqual(
            a: ReadonlyArray<TransmatObserverEntry>,
            b: ReadonlyArray<TransmatObserverEntry>
          ): boolean 
          {
            if (a.length !== b.length) {
              return false;
            }
            // 如果有一項(xiàng)不匹配,則立即返回false。
            return a.every((av, index) => {
              const bv = b[index];
              return av.isActive === bv.isActive && av.isTarget === bv.isTarget;
            });
          }

          與 MutationObserver 一樣,TransmatObserver 也提供了用于獲取最近已觸發(fā)記錄的 takeRecords 方法和用于 “斷開” 連接的 disconnect 方法:

          // 返回最近已觸發(fā)記錄
          takeRecords() {
            return this.prevRecords;
          }

          // 移除所有目標(biāo)及事件監(jiān)聽器
          disconnect() {
            this.targets.clear();
            this.removeEventListeners();
          }

          到這里 Transmat 源碼分析的相關(guān)內(nèi)容已經(jīng)介紹完了,如果你對(duì)該項(xiàng)目感興趣的話,可以自行閱讀該項(xiàng)目的完整源碼。該項(xiàng)目是使用 TypeScript 開發(fā),已入門 TypeScript 的小伙伴可以利用該項(xiàng)目鞏固一下所學(xué)的 TS 知識(shí)及 OOP 面向?qū)ο蟮脑O(shè)計(jì)思想。

          四、總結(jié)

          本文阿寶哥介紹了谷歌 transmat 開源項(xiàng)目的應(yīng)用場(chǎng)景、使用方式及相關(guān)源碼。在源碼分析環(huán)節(jié),我們一起回顧了與拖拽相關(guān)的事件及 DataTransfer API。此外,我們還分析了可以幫助我們響應(yīng)用戶的拖拽行為的 TransmatObserver 類,希望分析完該類之后,你對(duì) MutationObserver API 能有更深刻的理解。同時(shí),在今后的工作中,若遇到類似的場(chǎng)景可以參考 TransmatObserver 類來(lái)實(shí)現(xiàn)自己的 Observer 類。

          雖然自定義負(fù)載(自定義 JSON 數(shù)據(jù))對(duì)于你控制的應(yīng)用程序之間的通信很有用,但它也限制了將數(shù)據(jù)傳輸?shù)酵獠繎?yīng)用程序的能力。要解決這個(gè)問(wèn)題,你可以考慮使用輕量的 JSON-LD(Linked Data) 數(shù)據(jù)格式,它對(duì)應(yīng)的 MIME 類型是 'application/ld+json'。利用該數(shù)據(jù)格式,可以更好地組織和鏈接數(shù)據(jù),從而創(chuàng)建更好的 Web 應(yīng)用。如果你對(duì)該數(shù)據(jù)格式感興趣,想深入學(xué)習(xí) JSON-LD(Linked Data) 的話,可以閱讀這篇文章。

          五、參考資源

          1. JavaScript 重溫系列(22篇全)
          2. ECMAScript 重溫系列(10篇全)
          3. JavaScript設(shè)計(jì)模式 重溫系列(9篇全)
          4. 正則 / 框架 / 算法等 重溫系列(16篇全)
          5. Webpack4 入門(上)|| Webpack4 入門(下)
          6. MobX 入門(上) ||  MobX 入門(下)
          7. 120+篇原創(chuàng)系列匯總

          回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~

          點(diǎn)擊“閱讀原文”查看 120+ 篇原創(chuàng)文章

          瀏覽 45
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲ⅴ国产v天堂a无码二区 | 日韩一区二区三区无码视频 | 黄色草逼视频 | 久久免费成人视频 | 91九色视频网站 |