不要被标题吓怕了,这篇文章没有那么高深。
工作中使用了 clipboard.js 这个库,实现文本复制的功能。于是对在浏览器中实现复制功能的原理产生兴趣。
上网查了一下:
- 读了这篇文章,知道可以通过
docuemnt.execCommand('cpoy')和docuemnt.execCommand('cut')实现剪切和复制功能,它们的作用类似于手动按下快捷键Ctrl + C和Ctrl + X。 - 还看了一个 Vue 指令库 v-clipboard 的源码(源码量很少,功能也很简单),知道是如何实现的了。
- 又看了另一个 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()
解释一下就是:
- 选择元素
- 执行复制/剪切(类似于按下 Ctrl + C/Ctrl + X)
- 取消元素选择
第一步中出现的 select() 是引入的一个库——select.js 中提供的方法。它的作用是选择元素内容,并且将元素设置为聚焦状态(可能的话)。
下面我们看看它的实现:
有趣的是,在查看 clipboard.js 源码的时候,我发现源码里引入了三个依赖:tiny-emitter、select、good-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 函数后,发生了两件事情:
- 元素被聚焦(可以的话)
- 选择元素内容(并且内容以函数返回值的形式返回)
除了 input 之外,再来看看其他元素的支持情况:

下面再来看,功能是如何实现的:
代码实现
本库将 HTML 元素分为三类处理:
<select><input>和<textarea>- 其他元素
一、<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 的时间上花费太长。当然跟咱们具体项目的需求也是有关系的,但知道实现原理更能得心应手一点,不是嘛。
(完)