clipboard.js 核心代码解析

4,521 阅读5分钟

不要被标题吓怕了,这篇文章没有那么高深。

工作中使用了 clipboard.js 这个库,实现文本复制的功能。于是对在浏览器中实现复制功能的原理产生兴趣。

上网查了一下:

  1. 读了这篇文章,知道可以通过 docuemnt.execCommand('cpoy')docuemnt.execCommand('cut') 实现剪切和复制功能,它们的作用类似于手动按下快捷键 Ctrl + CCtrl + X
  2. 还看了一个 Vue 指令库 v-clipboard 的源码(源码量很少,功能也很简单),知道是如何实现的了。
  3. 又看了另一个 Vue 指令库 vue-clipboard2,star 数量快一千了,这个库是对 clipboard.js 的一层封装。

我有点坐不住了,clipboard.js 到底好在哪里?怀着深深的疑问,我开始看它的源码。看过之后,发现无一例外,clipboard.js 也是使用 docuemnt.execCommand 方法实现复制/剪切功能的。

核心代码

clipboard.js 核心代码在 clipboard-action.js 这个文件中,抽离出来后就三行(源码位于这里):

select(targetElem);
document.execCommand(action) // action 等于 'copy' 或 'cut'
window.getSelection().removeAllRanges()

解释一下就是:

  1. 选择元素
  2. 执行复制/剪切(类似于按下 Ctrl + C/Ctrl + X)
  3. 取消元素选择

第一步中出现的 select() 是引入的一个库——select.js 中提供的方法。它的作用是选择元素内容,并且将元素设置为聚焦状态(可能的话)。

下面我们看看它的实现:

有趣的是,在查看 clipboard.js 源码的时候,我发现源码里引入了三个依赖:tiny-emitterselectgood-listener。刚开始看的时候,有点抓狂,至于嘛。最后查找了一下这些依赖库的地址,发现后两个都是 clipboard.js 作者写的小库,前一个是别人写的。

我想作者之所以这样写,一方面是为了获得 IE9+ 以上浏览器的支持(IE9 不支持 CustomEvent());另一方面是为了让这个库支持 target 特性——复制指定目标元素里的内容,最后一点(可能不重要)——自产自销 😁。

select.js

这个库的代码量少,五十行都不到。其作用是通过 JavaScript 代码选择 HTML 元素的内容。在全局中暴露了一个 select 函数供调用。

使用 demo 如下:

<input type="text" value="my name" onclick="handleSelectInput()">

<script src="select.js></script>
<script>
function handleSelectInput() {
    console.log(select(document.querySelector('input')))
}
</script>

效果是这样的:

通过观察发现,执行 select 函数后,发生了两件事情:

  1. 元素被聚焦(可以的话)
  2. 选择元素内容(并且内容以函数返回值的形式返回)

除了 input 之外,再来看看其他元素的支持情况:

下面再来看,功能是如何实现的:

代码实现

本库将 HTML 元素分为三类处理:

  1. <select>
  2. <input><textarea>
  3. 其他元素

一、<select> 的逻辑

element.focus();
selectedText = element.value;

这里变量 selectedText 是调用 select() 函数后的返回值,下面与此一样。

<select> 元素当前选中值,可以从 value 属性中获得。

二、<input><textarea> 的逻辑

var isReadOnly = element.hasAttribute('readonly');
if (!isReadOnly) { // 如果元素不是只读的,设置成只读的
    element.setAttribute('readonly', '');
}

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

if (!isReadOnly) { // 恢复到之前的状态
    element.removeAttribute('readonly');
}

<select> 类似,<input><textarea> 输入框中的值,也可通过 value 属性获得。

其实针对 <input><textarea>,执行 element.select() 就可以了,就能实现文本框的全选功能了。可能是为了兼容 iOS 系统的原因,才加了 element.setSelectionRange(0, element.value.length) 这么一句(参见这篇文章)。

三、其他元素

// 如果元素是 contenteditable 的,先 focus 一下
if (element.hasAttribute('contenteditable')) {
    element.focus();
}

var selection = window.getSelection();
var range = document.createRange().selectNodeContents(element) // 生成要选择的文本范围(element 里的内容)
selection.removeAllRanges(); // 先移除当前网页已有的选择
selection.addRange(range); // 选择 element 的内容
selectedText = selection.toString();

作用

select.js 在 clipboard.js 这个库里的作用,就是在执行复制/剪切操作之前,选择好要操作的文本内容。

临时元素

上面讲到的实现过程是基于 target 对象来说的,就是说我们有目标元素可以获取内容。但是如果只是简单指定要复制的文本内容,该如何实现的呢?

比如下面这样:

<button class="btn" data-clipboard-text="Just because you can doesn't mean you should — clipboard.js">
    Copy to clipboard
</button>

点击按钮的时候,怎样将 data-clipboard-text 的值复制到剪切板中?

方法就是创建出一个临时元素(一般使用 <textarea>),添加到文档中(不可见),使用它目标元素,在执行完复制操作后,再从文档中删除。

类似于下面的代码(改写自这里):

var fakeElem = document.createElement('textarea');
fakeElem.style['left' ] = '-9999px'; // 让元素在视口之外
fakeElem.setAttribute('readonly', '');
fakeElem.value = text // 这里的 text 可以理解为上面 `data-clipboard-text` 属性值里文本内容
document.appendChild(fakeElem) // 将这个临时元素添加到文档中

// ... 执行复制操作

仅指定文本进行复制的话,会在文档中插入一个“不可见”(实际上在视口之外)的临时元素,作为目标元素进行操作。

总结

到这里就把 clipboard.js 核心部分讲完了。

其实比较起来,对于我个人而言,我更喜欢 v-clipboard 这个库中实现的小而美的代码,而不是 clipboard.js 中的大而全的功能。

可能在内心里,小而美的代码能让我更快阅读,知道某个功能的实现细节,不至于出现 bug 时,在解决 bug 的时间上花费太长。当然跟咱们具体项目的需求也是有关系的,但知道实现原理更能得心应手一点,不是嘛。

(完)