vue-clipboard2在vue的created生命周期中直接调用copyText方法报错的原因分析

探讨Vue-Clipboard2在Vue的created生命周期直接调用copyText方法导致的错误,分析源码揭示问题根源在于浏览器安全性限制,提供解决思路。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

vue-clipboard2在vue的created生命周期中直接调用copyText方法报错

先说现象:在created生命周期中会进入reject状态(被catch到),不在生命周期的方法中调用而通过click事件来调用会正常进入resolved状态(成功进入then阶段)。

下面进行相关源码分析:

出错代码:

created() {
    this.$copyText('asdasdasdas').then(() => {
        console.log('复制成功');
    }).catch(err => {
        console.error('复制出错', err); // 执行到这里了
    })
},

打开vue-clipboard2的源码,可以发现底层使用了clipboard这个库:

Vue.prototype.$copyText = function (text, container) {
  return new Promise(function (resolve, reject) {
    var fakeElement = document.createElement('button')
    // 注意这里,使用了Clipboard的构造方法
    var clipboard = new Clipboard(fakeElement, {
      text: function () { return text },
      action: function () { return 'copy' },
      container: typeof container === 'object' ? container : document.body
    })
    clipboard.on('success', function (e) {
      clipboard.destroy()
      resolve(e)
    })
    // 注意这里
    clipboard.on('error', function (e) {
      clipboard.destroy()
      reject(e)
    })
    if (VueClipboardConfig.appendToBody) document.body.appendChild(fakeElement)
    fakeElement.click()
    if (VueClipboardConfig.appendToBody) document.body.removeChild(fakeElement)
  })
}

在它的package.json中找到clipboard的依赖,确保别找错了:

"dependencies": {
  "clipboard": "^2.0.0"
},

再看一下我们安装的clipboard的版本:

"version": "2.0.4"

github上搜一下这个库的源码:https://github.com/zenorocha/clipboard.js

方便查找代码的引用关系,我们去这个网址:https://sourcegraph.com/github.com/zenorocha/clipboard.js@master/-/blob/src/clipboard.js

之前的代码调用了clipboard的构造方法:

 constructor(trigger, options) {
        super();

        this.resolveOptions(options);
        this.listenClick(trigger);
    }

    /**
     * Defines if attributes would be resolved using internal setter functions
     * or custom functions that were passed in the constructor.
     * @param {Object} options
     */
    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;
    }

    /**
     * Adds a click event listener to the passed trigger.
     * @param {String|HTMLElement|HTMLCollection|NodeList} trigger
     */
    listenClick(trigger) {
        this.listener = listen(trigger, 'click', (e) => this.onClick(e));
    }

triggervue-clipboard2传进来的button实例,listenClick做的就是给这个button加上click事件,再看一下onClick方法:

    onClick(e) {
        // button实例 delegateTarget是事件委托dom,这里我们走的是currentTarget
        const trigger = e.delegateTarget || e.currentTarget;

        if (this.clipboardAction) {
            this.clipboardAction = null;
        }

        this.clipboardAction = new ClipboardAction({
            action    : this.action(trigger), // 'copy'
            target    : this.target(trigger), // undefined
            text      : this.text(trigger), // 'text' => 传入的text参数
            container : this.container, // 默认为body
            trigger   : trigger,
            emitter   : this
        });
    }

this.target方法再初始化时被定义成下面这个函数,因为vue-clipboard2没有传这个参数

    defaultTarget(trigger) {
        const selector = getAttributeValue('target', trigger); // 返回undefined,因为button没有target这个属性 

        if (selector) {
            return document.querySelector(selector);
        }
    }

下面再看看ClipboardAction的构造方法做了什么:

 constructor(options) {
        this.resolveOptions(options);
        this.initSelection();
    }

    /**
     * Defines base properties passed from constructor.
     * @param {Object} options
     */
    resolveOptions(options = {}) {
        this.action    = options.action;
        this.container = options.container;
        this.emitter   = options.emitter;
        this.target    = options.target;
        this.text      = options.text;
        this.trigger   = options.trigger;

        this.selectedText = '';
    }

    /**
     * Decides which selection strategy is going to be applied based
     * on the existence of `text` and `target` properties.
     */
    initSelection() {
        if (this.text) {
            this.selectFake();
        }
        else if (this.target) {
            this.selectTarget();
        }
    }

主要是对传进来的参数进行本地赋值,看到initSelection方法,进入了第一个分支:

    selectFake() {
        const isRTL = document.documentElement.getAttribute('dir') == 'rtl';

        // 这个方法做的事情是删除textarea节点,清除container上的click事件
        // 将fakeHandler,fakeHandlerCallback,fakeElem置为null
        this.removeFake();

        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 horizontallys
        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();
    }

创建了一个textareadom节点,value为我们传进去的text

select方法为外部依赖,做的事情是帮我们选中textarea中的文字。接下来调用了copyText方法,

    /**
     * Executes the copy operation based on the current selection.
     */
    copyText() {
        let succeeded;

        try {
            succeeded = document.execCommand(this.action); // this.action === 'copy'
        }
        catch (err) {
            succeeded = false;
        }

        this.handleResult(succeeded);
    }

可以看到调用了execCommand方法来执行操作系统的copy方法,而我们报的错是在handleResultemit出来的,所以我们的$copyText方法进入了catch分支。

    // vue-clipboard2的监听事件
		clipboard.on('error', function (e) {
      clipboard.destroy()
      reject(e)
    })  

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

也就是说succeeded变量值为false,这一点在我们断点调试一下可以发现确实返回了fasle。

在这里插入图片描述

为什么呢?先看一下MDN文档对于execCommand方法的说明:

bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument);
// 一个 Boolean ,如果是 false 则表示操作不被支持或未被启用。
// 注意:在调用一个命令前,不要尝试使用返回值去校验浏览器的兼容性

可是我的浏览器是chrome 78,按理说支持这个方法啊,可是为什么会返回false呢?

返回false的原因其实也是浏览器对安全性的考虑,因为copy这个操作不是由用户操作产生的,而是由代码自执行的,所以默认执行失败。

document.execCommand的特殊性

浏览器处于安全考虑,document.execCommand这个api只能在真正的用户操作之后才能被触发。

以下引用自W3C草案:

If an implementation supports ways to execute clipboard commands through scripting, for example by calling the document.execCommand() method with the commands “cut”, “copy” and “paste”, the implementation must trigger the corresponding action, which again will dispatch the associated clipboard event.

copy事件的执行过程:

  1. If the script-triggered flag is set, then
    1. If the script-may-access-clipboard flag is unset, then
      1. Return false from the copy action, terminate this algorithm
  2. Fire a clipboard event named copy
  3. If the event was not canceled, then
    1. Copy the selected contents, if any, to the clipboard. Implementations should create alternate text/html and text/plain clipboard formats when content in a web page is selected.
    2. Fire a clipboard event named clipboardchange
  4. Else, if the event was canceled, then
    1. Call the write content to the clipboard algorithm, passing on the DataTransferItemList list items, a clear-was-called flag and a types-to-clear list.
  5. Return true from the copy action

参考链接

Cannot use document.execCommand('copy'); from developer console

execCommand(‘copy’) does not work in Ajax / XHR callback?

W3C:Clipboard API and events

### 如何在 Vue 3 中使用 `vue-clipboard2` `vue-clipboard2` 是一个用于简化复制文本到剪贴板操作的插件,在 Vue 3 项目中的集成方式有所不同。由于官方文档主要针对 Vue 2 版本,因此对于 Vue 3 的兼容性和安装方法需要注意一些细节。 #### 安装依赖 首先需要通过 npm 或 yarn 来安装 `vue-clipboard2` 和其必要的 polyfill: ```bash npm install vue-clipboard2 --save ``` 或者如果使用 Yarn: ```bash yarn add vue-clipboard2 ``` #### 配置 Polyfills 为了确保所有浏览器都能正常工作,建议引入 ClipboardJS 所需的 polyfills。可以在项目的入口文件中加入如下代码来加载这些 polyfills[^1]。 #### 注册全局组件或局部导入 ##### 方法一:作为插件注册 (推荐) 创建一个新的 JavaScript 文件比如 `plugins/clipboard.js` 并编写如下内容: ```javascript import Vue from 'vue'; import VueClipboard from 'vue-clipboard2'; Vue.use(VueClipboard); ``` 接着在 main.js 中引入此插件配置: ```javascript import './plugins/clipboard' ``` 注意这里假设已经正确设置了 Webpack 别名使得 `'./plugins/clipboard'` 能够指向对应的路径。 ##### 方法二:按需引入并手动挂载实例 如果你不想将其作为一个全局可用的功能而是仅限于特定页面,则可以直接在单个 SFC 组件内部完成初始化过程: ```html <template> <!-- Your template here --> </template> <script setup> // 这里可以放置组合 API 相关逻辑... import { onMounted } from "vue"; import VueClipboard from "vue-clipboard2"; onMounted(() => { const clipboard = new VueClipboard({ autoSetContainer: true, appendToBody: false }); }); </script> ``` 以上两种方式都可以实现功能需求,但是考虑到维护成本以及可读性的因素,通常更倾向于采用第一种方案即以插件形式统一管理。 #### 使用示例 无论选择了哪种方式进行设置,在模板内调用该库都非常简单直观: ```html <button v-clipboard:copy="text" v-clipboard:success="onCopySuccess" v-clipboard:error="onCopyError"> Copy Text </button> ``` 其中 `v-clipboard:*` 表达式的具体含义已经在上述例子中有所体现;而事件处理函数则可以根据实际业务场景自定义实现。 #### 处理错误与成功回调 当触发 copy 动作之后会分别执行相应的钩子函数来进行反馈提示或其他交互设计上的优化措施: ```javascript methods: { onCopySuccess() { alert('Copied successfully!'); }, onCopyError(e) { console.error(`Failed to copy text! Error was ${e}`); } } ``` 这样就完成了整个流程的设计与编码工作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值