В этой 29,7-килобайтной JS-библиотеке буфера обмена есть что-то!

внешний интерфейс
В этой 29,7-килобайтной JS-библиотеке буфера обмена есть что-то!

2020 подходит к концу, прежде чем вы это знаетеАнализ исходного кодаВ теме написано 9 статей, а предыдущие 8 статей введеныAxios,BetterScroll,koa-composeиFileSaver.jsЧто касается отличных проектов с открытым исходным кодом, то Brother A Bao потратил много времени и сил на каждую статью в этой теме. Однако отрадно, что многие статьи в спецтеме получилиСамородки и публичные болельщикиПризнание и поддержка этой темы побудили брата Абао продолжать писать эту тему, и я искренне благодарю вас за вашу поддержку.

Для тех, кому интересны предыдущие 8 статей, можно прочитатьКак лучше читать исходный код? Эти восемь статей дают вам ответЭта статья.

Ладно, давайте сразу к делу. В этом выпуске Brother Abao представит библиотеку JS с открытым исходным кодом, на которую ссылаются 158024 проекта —clipboard.js. Я считаю, что многие мелкие партнеры также используют эту библиотеку в проекте. Итак, каков принцип работы этой библиотеки? Заинтересованные друзья, приходите с братом А Бао, чтобы раскрыть секрет, стоящий за этим.

1. Знакомство с clipboard.js

clipboard.jsэто библиотека JS для копирования текста в буфер обмена. Никакого Flash, никаких фреймворков, только сжатие gzip3kb.

(Источник изображения:буфер обмена — .com/#example-spec…

Так почему жеclipboard.jsЧто с этой библиотекой? потому что авторzenorochaсчитать:

Скопировать текст в буфер обмена не составит труда. Не требуется выполнять десятки шагов для настройки или загрузки сотен файлов КБ. Самое главное, это не должно зависеть от Flash или любого другого фреймворка.

Эта библиотека зависит отSelectionиexecCommandAPI, поддерживаемый почти всеми браузерамиSelectionAPI, однакоexecCommandЕсть некоторые проблемы совместимости с API:

(Источник изображения:потрите newser.com/?search=exe…

(Источник изображения:потрите newser.com/?search=exe…

Конечно, для старых браузеров,clipboard.jsОн также может изящно деградировать. Хорошо, теперь давайте посмотрим, как использовать clipboard.js.

Следуйте «Дороге полного совершенствования», чтобы прочитать 3 бесплатные электронные книги (в общей сложности более 20 000 загрузок) и более 50 учебных пособий «Relearn TS» от Brother Abao.

2. Использование clipboard.js

Прежде чем использовать clipboard.js, вы можете установить его через NPM или CDN:

NPM

npm install clipboard --save

CDN

<script src="https://cdn.jsdelivr.net/npm/clipboard@2.0.6/dist/clipboard.min.js"></script>

clipboard.js очень прост в использовании, обычно всего 3 шага:

1. Определите некоторую разметку

<input id="foo" type="text" value="大家好,我是阿宝哥">
<button class="btn" data-clipboard-action="copy" data-clipboard-target="#foo">复制</button>

2. Представьте clipboard.js

<script src="https://cdn.jsdelivr.net/npm/clipboard@2.0.6/dist/clipboard.min.js"></script>

3. Создать буфер обмена

<script>
  var clipboard = new ClipboardJS('.btn');

  clipboard.on('success', function(e) {
    console.log(e);
  });
    
  clipboard.on('error', function(e) {
    console.log(e);
  });
</script>

После успешного выполнения приведенного выше кода, когда вы нажимаете «копировать", текст в поле ввода будет выделен, а текст в поле ввода будет скопирован в буфер обмена. Соответствующий эффект показан на следующем рисунке:

КромеinputВ дополнение к элементам, цель копии также может бытьdivилиtextareaэлемент. В приведенном выше примере цель, которую мы скопировали, находится черезданные-* атрибутыуказать. Кроме того, мы также можем установить цель копирования при создании экземпляра объекта буфера обмена:

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

Если нам нужно установить скопированный текст, мы также можем установить скопированный текст при создании экземпляра объекта буфера обмена:

// https://github.com/zenorocha/clipboard.js/blob/master/demo/function-text.html
let clipboard = new ClipboardJS('.btn', {
  text: function() {
    return '大家好,我是阿宝哥';
  }
});

Что касается использования clipboard.js, брат Абао представит его здесь Заинтересованные партнеры могут проверить его на Github.clipboard.jsпример использования. Поскольку clipboard.js опирается на базовыйSelectionиexecCommandAPI, поэтому, прежде чем анализировать исходный код clipboard.js, давайте посмотримSelectionиexecCommandAPI.

3. Выбор и execCommand API

3.1 Selection API

SelectionОбъект представляет собой текстовый диапазон, выбранный пользователем, или текущую позицию курсора. Он представляет собой выделение текста на странице, которое может охватывать несколько элементов. Выбор текста создается пользователем, перетаскивающим мышь по тексту. Если вы хотите получитьSelectionпредмет, который можно назватьwindow.getSelectionметод.

Объект Selection соответствует объекту, выбранному пользователемranges(регион), широко известный какперетащите синий. По умолчанию эта функция нацелена только на один регион, мы можем использовать эту функцию следующим образом:

let selection = window.getSelection();
let range = selection.getRangeAt(0);

В приведенном выше примере показано, как получить первую область в выделении.На самом деле, помимо получения области в выделении, мы также можем передатьcreateRangeAPI создает новый регион, а затем добавляет его к выбору:

<div>大家好,我是<strong>阿宝哥</strong>。欢迎关注<strong>全栈修仙之路</strong></div>
<script>
   let strongs = document.getElementsByTagName("strong");
   let s = window.getSelection();

   if (s.rangeCount > 0) s.removeAllRanges(); // 从选区中移除所有区域
   for (let i = 0; i < strongs.length; i++) {
     let range = document.createRange(); // 创建range区域
     range.selectNode(strongs[i]); // 让range区域包含指定节点及其内容
     s.addRange(range); // 将创建的区域添加到选区中
   }
</script>

Приведенный выше код используется для выбора всехstrongэлемент, но следует отметить, что в настоящее время только браузеры, использующие механизм рендеринга Gecko, такие как браузер Firefox, реализуют несколько регионов.

В некоторых сценариях может потребоваться получить текст в выбранной области. Для этого сценария вы можете вызвать объект SelectiontoStringспособ получить обычный текст в выделенной области.

3.2 execCommand API

document.execCommandAPI позволяет запускать команды для управления содержимым веб-страницы.Обычные команды включают жирный шрифт, курсив, копирование, вырезание, удаление, вставкуHTML, вставку изображения, вставку текста и отмену. Давайте посмотрим на синтаксис API:

bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)

Соответствующие параметры описываются следующим образом:

  • aCommandName: тип строки, используемый для представления имени команды;
  • aShowDefaultUI: логический тип, используемый для указания, отображать ли пользовательский интерфейс, как правило, false;
  • aValueArgument: дополнительные параметры, некоторые команды (например, insertImage) требуют дополнительных параметров (укажите URL-адрес для вставки изображения), значение по умолчанию — null.

перечислитьdocument.execCommandметод возвращает логическое значение. еслиfalse, указывает, что операция не поддерживается или не включена. Для библиотеки clipboard.js пройдетdocument.execCommandAPI для выполненияcopyиcutКоманда для копирования содержимого в буфер обмена.

Итак, теперь вопрос в том, есть ли у нас способ определить, поддерживает ли его текущий браузер?copyиcutА команды? Ответ — да, то есть с помощью API, предоставляемого браузером —Document.queryCommandSupported, что позволяет нам определить, поддерживает ли текущий браузер указанную команду редактирования.

Автор библиотеки clipboard.js также учел это требование, поэтому предоставил статическийisSupportedметод, чтобы определить, поддерживает ли текущий браузер указанную команду:

// src/clipboard.js
static isSupported(action = ['copy', 'cut']) {
  const actions = (typeof action === 'string') ? [action] : action;
  let support = !!document.queryCommandSupported;

  actions.forEach((action) => {
    support = support && !!document.queryCommandSupported(action);
  });

  return support;
}

Document.queryCommandSupportedСовместимость хорошая, и вы можете с уверенностью ее использовать Конкретная совместимость показана на следующем рисунке:

(Источник изображения:потрите news.com/?search=но…

ВведениеSelection,execCommandиqueryCommandSupportedAPI, затем приступаем к анализу исходного кода clipboard.js.

Если вы хотите узнать идеи и навыки чтения исходного кода, вы можете прочитатьИспользуя эти идеи и методы, я прочитал множество отличных проектов с открытым исходным кодом.Эта статья.

Четыре, анализ исходного кода clipboard.js

4.1 Класс буфера обмена

Глядя на исходный код, брат Абао привык начинать с самого простого использования, чтобы он мог быстро понять внутренний процесс выполнения. Давайте рассмотрим предыдущий пример:

<!-- 定义一些标记 -->
<input id="foo" type="text" value="大家好,我是阿宝哥">
<button class="btn" data-clipboard-action="copy" data-clipboard-target="#foo">复制</button>

<!-- 实例化 clipboard -->
<script>
  let clipboard = new ClipboardJS('.btn');

  clipboard.on('success', function(e) {
    console.log(e);
  });
    
  clipboard.on('error', function(e) {
    console.log(e);
  });
</script>

Наблюдая за приведенным выше кодом, мы можем быстро найти точку входа —new ClipboardJS('.btn'). внутри проекта clipboard.jswebpack.configфайл конфигурации, мы можем найтиClipboardJSОпределение:

module.exports = {
  entry: './src/clipboard.js',
  mode: 'production',
  output: {
    filename: production ? 'clipboard.min.js' : 'clipboard.js',
    path: path.resolve(__dirname, 'dist'),
    library: 'ClipboardJS',
    globalObject: 'this',
    libraryExport: 'default',
    libraryTarget: 'umd'
  },
  // 省略其他配置信息
}

Основываясь на приведенной выше информации о конфигурации, мы также обнаружилиClipboardJSУказание на конструктор:

import Emitter from 'tiny-emitter';
import listen from 'good-listener';

class Clipboard extends Emitter {
  constructor(trigger, options) {
    super();
    this.resolveOptions(options);
    this.listenClick(trigger);
  }
}

В этом примере мы не задавали информацию о конфигурации буфера обмена, поэтому сейчас нам все равно.this.resolveOptions(options)логика обработки. Как следует из названияlistenClickметод используется для контроляclickСобытие, конкретная реализация этого метода выглядит следующим образом:

listenClick(trigger) {
  this.listener = listen(trigger, 'click', (e) => this.onClick(e));
}

существуетlistenClickВнутри метода он будет передавать стороннюю библиотекуgood-listenerдобавить обработчики событий. когда цель срабатываетclickКогда происходит событие, будет выполнен соответствующий обработчик события, и обработчик далее вызоветthis.onClickметод, который реализуется следующим образом:

// src/clipboard.js
onClick(e) {
  const trigger = e.delegateTarget || e.currentTarget;

  // 为每次点击事件,创建一个新的ClipboardAction对象
  if (this.clipboardAction) {
    this.clipboardAction = null;
  }
  this.clipboardAction = new ClipboardAction({
    action    : this.action(trigger),
    target    : this.target(trigger),
    text      : this.text(trigger),
    container : this.container,
    trigger   : trigger,
    emitter   : this
  });
}

существуетonClickВнутри метода цель триггера события используется для созданияClipboardActionобъект. когда вы нажмете этот примеркопироватькнопка, созданнаяClipboardActionОбъект выглядит так:

Я считаю, что после прочтения приведенной выше картины все очень рады творитьClipboardActionОбъекты, используемые методы известны. Такthis.action,this.targetиthis.textГде определены эти методы? Прочитав исходный код, мы обнаружили, что вresolveOptionsВышеупомянутые три метода инициализируются внутри метода:

// src/clipboard.js
resolveOptions(options = {}) {
  this.action = (typeof options.action === 'function') 
    ? options.action :  this.defaultAction;
  this.target = (typeof options.target === 'function') 
    ? options.target : this.defaultTarget;
  this.text = (typeof options.text === 'function')
    ? options.text : this.defaultText;
  this.container = (typeof options.container === 'object')   
    ? options.container : document.body;
}

существуетresolveOptionsМетод находится внутри, если пользователь настраивает обработчик, пользовательская функция будет иметь приоритет, иначе будет использоваться она.clipboard.jsСоответствующая функция обработчика по умолчанию в . Поскольку мы звонимClipboardконструктор, не установленoptionsпараметры, поэтому будет использоваться обработчик по умолчанию:

Из приведенного выше рисунка видно, чтоdefaultAction,defaultTargetиdefaultTextметод будет вызываться внутриgetAttributeValueметод для получения пользовательского атрибута в объекте триггера события и соответствующийgetAttributeValueМетод также очень прост, конкретный код выглядит следующим образом:

// src/clipboard.js
function getAttributeValue(suffix, element) {
  const attribute = `data-clipboard-${suffix}`;
  if (!element.hasAttribute(attribute)) {
    return;
  }
  return element.getAttribute(attribute);
}

ВведениеClipboardкласс, давайте сосредоточимся на анализеClipboardActionКласс, содержащий определенную логику репликации.

4.2 Класс ClipboardAction

В проекте clipboard.jsClipboardActionкласс определен вsrc/clipboard-action.jsВнутри файла:

// src/clipboard-action.js
class ClipboardAction {
  constructor(options) {
    this.resolveOptions(options);
    this.initSelection();
  }
}

иClipboardТо же, что конструктор класса,ClipboardActionСначала будет разрешен конструктор классаoptionsнастройте объект, затем вызовитеinitSelectionметод для инициализации выбора. существуетinitSelectionметод будет основан наtextиtargetсвойства для выбора различных стратегий выбора:

initSelection() {
  if (this.text) {
    this.selectFake();
  } else if (this.target) {
    this.selectTarget();
  }
}

В предыдущем примере мы передалиданные-* атрибутыуказать цель копии, т.е.data-clipboard-target="#foo", соответствующий код выглядит следующим образом:

<input id="foo" type="text" value="大家好,我是阿宝哥">
<button class="btn" data-clipboard-action="copy" data-clipboard-target="#foo">复制</button>

Итак, давайте сначала проанализируемtargetслучай свойства, если он содержитtargetсвойства, он войдетelse ifотделение, то звонитеthis.selectTargetметод:

// src/clipboard-action.js
selectTarget() {
  this.selectedText = select(this.target);
  this.copyText();
}

существуетselectTargetВнутри метода он вызоветselectФункция получает выделенный текст.Эта функция представляет собой еще один пакет npm, разработанный автором clipboard.js.Соответствующий код выглядит следующим образом:

// https://github.com/zenorocha/select/blob/master/src/select.js
function select(element) {
  var selectedText;

  if (element.nodeName === 'SELECT') {
    element.focus();
    selectedText = element.value;
  }
  else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
    var isReadOnly = element.hasAttribute('readonly');

    if (!isReadOnly) {
      element.setAttribute('readonly', '');
    }

    element.select();
    element.setSelectionRange(0, element.value.length);

    if (!isReadOnly) {
      element.removeAttribute('readonly');
    } 
      selectedText = element.value;
    }
  else {
    // 省略相关代码 
  }
  return selectedText;
}

Поскольку в приведенном выше примере цель, которую мы скопировали,inputelement, поэтому давайте сначала проанализируем код этой ветки. В этой ветке используйтеHTMLInputElementобъектselectиsetSelectionRangeметод:

  • select: используется для выбора одного<textarea>элемент или текстовое поле<input>Все внутри элемента.
  • setSelectionRange: используется для установки<input>или<textarea>Начальная и конечная позиции текущего выделенного текста в элементе.

Получив выделенный текст,selectTargetметод будет по-прежнему вызыватьсяcopyTextметод копирования текста:

copyText() {
  let succeeded;
  try {
    succeeded = document.execCommand(this.action);
  } catch (err) {
    succeeded = false;
  }
  this.handleResult(succeeded);
}

Брат А Бао уже кратко представилexecCommandAPI,copyTextВнутри метода используется этот API для копирования текста. После копирования метод copyText вызываетthis.handleResultметод для отправки реплицированной информации о состоянии:

handleResult(succeeded) {
  this.emitter.emit(succeeded ? 'success' : 'error', {
    action: this.action,
    text: this.selectedText,
    trigger: this.trigger,
    clearSelection: this.clearSelection.bind(this)
  });
}

Смотрите здесь, некоторые друзья могут спроситьthis.emitterОткуда появился объект? фактическиthis.emitterобъектClipboardПример:

// src/clipboard.js
class Clipboard extends Emitter {
  onClick(e) {
    const trigger = e.delegateTarget || e.currentTarget;
    // 省略部分代码
    this.clipboardAction = new ClipboardAction({
      // 省略部分属性
      trigger   : trigger,
      emitter   : this // Clipboard 实例
    });
  }
}

И дляhandleResultСобытие, отправленное методом, мы можем передатьclipboardПример для отслеживания соответствующего события, конкретный код выглядит следующим образом:

let clipboard = new ClipboardJS('.btn');

clipboard.on('success', function(e) {
  console.log(e);
});
    
clipboard.on('error', function(e) {
  console.log(e);
});

Прежде чем продолжить вводить логику обработки другой ветки, брат Абао использует картинку, чтобы обобщить поток выполнения приведенного выше примера:

Теперь давайте представим еще одну ветку, содержащуюtextВ случае атрибутов соответствующие примеры использования следующие:

// https://github.com/zenorocha/clipboard.js/blob/master/demo/function-text.html
let clipboard = new ClipboardJS('.btn', {
  text: function() {
    return '大家好,我是阿宝哥';
  }
});

когда пользователь создаетclipboardобъект при установкеtextсвойство, оно будет выполнятьсяifЛогика ветвления, т.е. вызоваthis.selectFakeметод:

// src/clipboard-action.js
class ClipboardAction {
  constructor(options) {
    this.resolveOptions(options);
    this.initSelection();
  }
  
  initSelection() {
    if (this.text) {
      this.selectFake();
    } else if (this.target) {
      this.selectTarget();
    }
  }
}

существуетselectFakeвнутри метода он сначала создаст подделкуtextareaэлемент и установить соответствующий стиль и информацию о позиционировании элемента, а также использоватьthis.textзначение для установкиtextareaсодержимое элемента, затем используйте ранее описанныйselectдля получения выделенного текста и, наконец,copyTextСкопируйте текст в буфер обмена:

// src/clipboard-action.js
selectFake() {
  const isRTL = document.documentElement.getAttribute('dir') == 'rtl';

  this.removeFake(); // 移除事件监听并移除之前创建的fakeElem

  this.fakeHandlerCallback = () => this.removeFake();
  this.fakeHandler = this.container.addEventListener('click', this.fakeHandlerCallback) || true;

  this.fakeElem = document.createElement('textarea');
  // Prevent zooming on iOS
  this.fakeElem.style.fontSize = '12pt';
  // Reset box model
  this.fakeElem.style.border = '0';
  this.fakeElem.style.padding = '0';
  this.fakeElem.style.margin = '0';
  // Move element out of screen horizontally
  this.fakeElem.style.position = 'absolute';
  this.fakeElem.style[ isRTL ? 'right' : 'left' ] = '-9999px';
  // Move element to the same position vertically
  let yPosition = window.pageYOffset || document.documentElement.scrollTop;
  this.fakeElem.style.top = `${yPosition}px`;

  this.fakeElem.setAttribute('readonly', '');
  this.fakeElem.value = this.text;

  this.container.appendChild(this.fakeElem);

  this.selectedText = select(this.fakeElem);
  this.copyText();
}

Чтобы всем было понятноselectFakeЭффект страницы после выполнения метода, брат А Бао вырезал реальный рендеринг:

фактическиclipboard.jsВ дополнение к поддержке копииinputилиtextareaВ дополнение к содержимому элемента он также поддерживает копирование содержимого других элементов HTML, таких какdivэлемент:

<div>大家好,我是阿宝哥</div>
<button class="btn" data-clipboard-action="copy" data-clipboard-target="div">Copy</button>

Для этой ситуации вclipboard.jsВнутри уже описанныйselectФункция для выбора целевого элемента и получения содержимого для копирования Конкретный код выглядит следующим образом:

function select(element) {
  var selectedText;

  if (element.nodeName === 'SELECT') {
      element.focus();
      selectedText = element.value;
  }
  else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
      // 省略相关代码 
  }
  else {
     if (element.hasAttribute('contenteditable')) {
        element.focus();
     }

     var selection = window.getSelection(); // 获取选取
     var range = document.createRange(); // 新建区域

     range.selectNodeContents(element); // 使新建的区域包含element节点的内容
     selection.removeAllRanges(); // 移除选取中的所有区域
     selection.addRange(range); // 往选区中添加新建的区域
     selectedText = selection.toString(); // 获取已选中的文本
    }

    return selectedText;
}

Получив текст для копирования, clipboard.js продолжает вызыватьcopyTextМетод копирует соответствующий текст в буфер обмена. На данный момент мы почти закончили анализ основного исходного кода clipboard.js.Я надеюсь, что после прочтения этой статьи вы не только поймете принцип работы clipboard.js, но и узнаете, как использовать диспетчеры событий для реализации сообщений. общение иSelectionиexecCommandAPI и другие сопутствующие знания.

Следуйте «Дорога к бессмертному развитию с полным стеком», чтобы прочитать 3 бесплатные электронные книги (всего более 20 000 загрузок) и 9 серий руководств по анализу исходного кода, изначально созданных А Баогэ.

5. Справочные ресурсы