前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

JavaScript复制粘贴与剪贴板操作详解

首页2018-12-23 09:10:43Front-End
JavaScript复制粘贴剪贴板

来源网络

产品提了个需求,文档页要加一个「复制代码」按钮,顺手还想学掘金那样,用户复制超过一定长度的正文时自动在末尾追加版权信息。这两件事看起来都很小,真动手才发现剪贴板这块的 API 有点乱,clipboardData 在不同浏览器上挂的位置不一样,execCommand 又被标记成废弃了。

这篇把浏览器里操作剪贴板的几条路径捋一遍,从最基础的三个事件讲到能落地的几个场景,再说清楚老方案和新方案各自的适用范围。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • copy、cut、paste 三个事件的触发时机和绑定方式
  • clipboardData 对象的兼容处理和三个方法
  • 复制正文时自动追加版权信息的完整实现
  • 防复制的几种做法,以及它们为什么拦不住人
  • 用隐藏 input 加 execCommand 实现点击复制
  • 现代异步 Clipboard API 的用法和它的两道限制
  • 什么时候该直接上 clipboard.js

# 一、剪贴板的三个事件

# 1.1 事件与触发条件

浏览器暴露了三个剪贴板相关的事件:

  • copy 发生复制操作时触发
  • cut 发生剪切操作时触发
  • paste 发生粘贴操作时触发

每个事件都有一个 before 事件对应,beforecopy、beforecut、beforepaste。这几个 before 一般不怎么用,把注意力放在另外三个事件就可以了。

能触发它们的操作有两类,一类是鼠标右键菜单里的复制、粘贴、剪切,另一类是键盘组合键,比如 command+c、command+v。注意用 JS 主动调用 document.execCommand('copy') 同样会触发 copy 事件,这一点在后面实现点击复制的时候会用到。

# 1.2 怎么绑

以 copy 为例,两种写法都可以。

document.body.oncopy = e => {
  // 监听全局复制 做点什么
};
// 还有这种写法:
document.addEventListener('copy', e => {
  // 监听全局复制 做点什么
});
@前端进阶之旅: 代码已经复制到剪贴板

上面是在 document.body 上全局监听的。很多人不知道的是,我们还可以为某些 DOM 单独添加剪贴板事件。

// html结构
<div id="test1"></div>
<div id="test2"></div>

<script>
    // 写法一样:
    let test1 = document.querySelector('#test1');
    test1.oncopy = e => {
        // 监听test1发生的复制事件 做点什么
        // test1发生的复制事件会触发回调,其他地方不会触发回调
    }
</script>
@前端进阶之旅: 代码已经复制到剪贴板

其他事件也是一样的。

为什么绑在 document.body 上就能拦下页面里任何位置的复制?因为剪贴板事件和 click 一样是会冒泡的,从选区所在的元素一路往上传到 document。所以你既可以做全局拦截,也可以只在文章容器上绑一个,让评论区、代码块之类的地方不受影响。事件冒泡和事件委托的完整机制,可以看 JavaScript事件机制。

# 二、clipboardData 对象

clipboardData 对象用于访问以及修改剪贴板中的数据。

不同浏览器,它所属的对象不同。在 IE 中这个对象是 window 对象的属性,在 Chrome、Safari 和 Firefox 中,它是相应的 event 对象的属性。所以使用的时候需要做一下兼容。

document.body.oncopy = e => {
  let clipboardData = e.clipboardData || window.clipboardData;
  // 获取clipboardData对象 + do something
};
@前端进阶之旅: 代码已经复制到剪贴板

这个差别不只是挂载位置的问题。把它放在 event 上,等于强制你只能在真实的剪贴板事件回调里访问剪贴板,页面脚本没法在任意时刻偷偷读用户的剪贴板,这其实是一种安全措施。IE 那种挂在 window 上、随时能读写的设计,正是后来被否掉的原因。

对象有三个方法,getData()、setData()、clearData()。

# 2.1 getData 读数据

getData() 接受一个 text 参数,即要取得的数据的格式。

实际上在 Chrome 上测试,只有 paste 粘贴的时候才能用 getData() 访问到数据,用法如下。

// 要粘贴的数据:

document.body.onpaste = e => {
  let clipboardData = e.clipboardData || window.clipboardData; // 兼容处理
  console.log('要粘贴的数据', clipboardData.getData('text'));
};
@前端进阶之旅: 代码已经复制到剪贴板

这里补一句规范上的说法。copy 和 cut 事件里 clipboardData 处于「只写」状态,读不到东西是设计如此,不是浏览器 bug。要拿被复制的内容得走另一条路。

被复制和被剪切的数据,需要通过 window.getSelection(0).toString() 来访问。

document.body.oncopy = e => {
  console.log('被复制的数据:', window.getSelection(0).toString());
};
@前端进阶之旅: 代码已经复制到剪贴板

顺带说个原文里的小问题,getSelection() 按规范是不接收参数的,写成 getSelection(0) 里的这个 0 会被直接忽略,不影响结果,但新写代码的时候直接写 window.getSelection().toString() 更规范。

# 2.2 setData 写数据

setData() 第一个参数也是 text,第二个参数是要放在剪贴板中的文本。

格式参数这块也演进过。规范里现在用的是标准 MIME 类型,比如 text/plain、text/html,'text' 是浏览器为了兼容老代码保留的别名。新写的代码建议直接写 text/plain,语义清楚,而且如果你要同时写入富文本,还能追加一份 text/html。

# 2.3 clearData

clearData() 清空剪贴板中指定格式的数据,实际业务里用得很少。

# 三、复制大段文本时追加版权信息

# 3.1 类知乎/掘金的实现

实现很简单。取消默认复制之后,主要是在被复制的内容后面添加信息,然后根据 clipboardData 的 setData() 方法将信息写入剪贴板。

// 掘金这里不是全局监听,应该只是监听文章的dom范围内。
document.body.oncopy = event => {
  event.preventDefault(); // 取消默认的复制事件
  let textFont,
    copyFont = window.getSelection(0).toString(); // 被复制的文字 等下插入
  // 防知乎掘金 复制一两个字则不添加版权信息 超过一定长度的文字 就添加版权信息
  if (copyFont.length > 10) {
    textFont =
      copyFont +
      '\n' +
      '作者:OBKoro1\n' +
      '链接:https://juejin.im/user/58714f0e325b123db4a2eb95372/posts\n' +
      '来源:掘金\n' +
      '著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。';
  } else {
    textFont = copyFont; // 没超过十个字 则采用被复制的内容。
  }
  if (event.clipboardData) {
    return event.clipboardData.setData('text', textFont); // 将信息写入粘贴板
  } else {
    // 兼容IE
    return window.clipboardData.setData('text', textFont);
  }
};
@前端进阶之旅: 代码已经复制到剪贴板

然后 command+c、command+v,输出:

你复制的内容
作者:OBKoro1
链接:https://juejin.im/user/58714f0eb123db4a2eb95372/posts
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
@前端进阶之旅: 代码已经复制到剪贴板

# 3.2 这段代码的关键点

event.preventDefault() 这一行是整段的枢纽。剪贴板事件的默认行为就是「把当前选区的内容写进剪贴板」,你不取消它,后面 setData 写进去的东西会被默认行为覆盖掉。取消之后剪贴板就是一块空白画布,完全由你来填。

长度阈值那个 if 也不是随便加的。用户复制一个词去查释义,结果粘出来跟着四行版权声明,体验很差。设个门槛只对成段的正文追加,是这类功能里必备的一步。

还有一处原文没提但实际会踩的,就是监听范围。示例绑在 document.body 上,那用户从你页面的输入框里复制自己刚打的字,也会被追加版权信息。真上线的话建议绑在文章容器上,代码块所在的元素再单独 stopPropagation 一下。

# 四、防复制功能

# 4.1 几条常见做法

  • 禁止复制和剪切
  • 禁止右键,右键某些选项:全选、复制、粘贴等
  • 禁用文字选择,能选择却不能复制,体验很差
  • user-select 用 CSS 禁止选择文本
// 禁止右键菜单
document.body.oncontextmenu = e => {
    console.log(e, '右键');
    return false;
    // e.preventDefault();
};
// 禁止文字选择。
document.body.onselectstart = e => {
    console.log(e, '文字选择');
    return false;
    // e.preventDefault();
};
// 禁止复制
document.body.oncopy = e => {
    console.log(e, 'copy');
    return false;
    // e.preventDefault();
}
// 禁止剪切
document.body.oncut = e => {
    console.log(e, 'cut');
    return false;
    // e.preventDefault();
};
// 禁止粘贴
document.body.onpaste = e => {
    console.log(e, 'paste');
    return false;
    // e.preventDefault();
};
@前端进阶之旅: 代码已经复制到剪贴板
/** css 禁止文本选择 这样不会触发js**/
body {
    user-select: none;
    -moz-user-select: none;
    -webkit-user-select: none;
    -ms-user-select: none;
}
@前端进阶之旅: 代码已经复制到剪贴板

使用 e.preventDefault() 也可以禁用,但原文建议使用 return false,这样就不用去访问 e 和 e 的方法了。有个前提要说清楚,return false 只在 DOM0 级的 onxxx = fn 这种写法下才等价于阻止默认行为,如果你用的是 addEventListener,return false 什么都不会发生,必须老老实实调 preventDefault()。

fe
  • 一、剪贴板的三个事件
    • 1.1 事件与触发条件
    • 1.2 怎么绑
  • 二、clipboardData 对象
    • 2.1 getData 读数据
    • 2.2 setData 写数据
    • 2.3 clearData
  • 三、复制大段文本时追加版权信息
    • 3.1 类知乎/掘金的实现
    • 3.2 这段代码的关键点
  • 四、防复制功能
    • 4.1 几条常见做法
    • 4.2 破解防复制
  • 五、点击复制功能
    • 5.1 为什么不能直接用 clipboardData
    • 5.2 隐藏 input 加 execCommand
    • 5.3 现在的做法是异步 Clipboard API
  • 六、第三方库 clipboard.js
  • 总结
  • 参考

← Object.defineProperty详解与Vue2响应式原理JavaScript原型链回顾与new的实现原理 →