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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值