浏览器剪贴板交互-复制与粘贴

691 阅读3分钟

document.execCommand('copy')

当我们接到复制需求,技术实现上一般是两种选择:

  1. 简单一点的就直接document.execCommand('copy')命令;
  2. 如果项目引入了Clipboard库,那么直接用库函数解决复制。

当你去看clipboard库的底层实现的时候,会发现它也是基于document.execCommand实现的复制功能:

image.png

那么问题来了,MDN已经明确说明该API已废弃:

image.png

这种情况下,为了考虑代码的向后兼容性,我们当然还是要避免调用此类API,除此之外,还有什么可用来实现复制的操作呢?接下来,就到了BOM对象为我们提供的Cliboard-API登场了!

Cliboard

它拥有四个跟剪贴板交互的方法:

  1. writeText()

navigator.clipboard.writeText(text)
写入特定字符串到操作系统的剪切板,返回promise,一般业务需要支持的复制交互,也就是复制url链接或者文本内容,这类操作通过writeText就能满足

  1. readText()

navigator.clipboard.readText()
解析系统剪贴板的文本内容,返回promise

read和write这两个API比较特殊,支持读取/写入任意在剪贴板的数据,从安全性设计考虑,需要校验用户权限

  1. read()

navigator.clipboard.read(); 如果读取的是非文本资源,会报错

  1. write()

navigator.clipboard.write([ClipboardItem]),ClipboardItem是用于表示Clipboard-API的剪贴项的单个标准接口,一般用于读写,即read和write操作。

new ClipboardItem({
    'text/plain': new Blob(['hello world'], { type: 'text/plain' }),
})

clipbard与图片复制

要实现复制图片粘贴到浏览器,需要结合window的paste事件,当我们监听paste事件时,得到的是ClipboardEvent,它包含一个只读的DataTransfer类型(本身是用来保存拖拽交互过程中的数据)的clipboardData对象,于是我们可以从该对象实例上的items属性获取到数据传输列表,从而获得我们想要的图片数据,通过构造File对象,完成文件上传。

window.addEventListener('paste', function (event) {
    event.preventDefault()
    /** ClipboardEvent.clipboardData 属性保存了一个 DataTransfer 对象 */
    const items = event.clipboardData && event.clipboardData.items
    var file = null
    if (items && items.length) {
        // 检索剪切板items
        for (var i = 0; i < items.length; i++) {
            if (items[i].type.indexOf('image') !== -1) {
                file = items[i].getAsFile() // 返回File对象
                break
            }
            if (items[i].type === 'text/html' || items[i] === 'text/plain') {
                navigator.clipboard.readText().then((res) => {
                    console.log(res, '------------') // 文本节点处理
                })
            }
        }
    }
    event.preventDefault()
})

至于兼容性,一般浏览器都支持该API了,发文的时候查询的caniuse, 如果要兼容低版本浏览器,则需要做一个兜底方案:

image.png

调试代码:

/**
<body>
    <div>12345</div>
    <input type="text" />
    <ul class="copy-list">
        <li>复制文本</li>
        <li>粘贴文本</li>
        <li>复制图片</li>
    <li>写入剪贴板数据</li>
    </ul>
    <ul>
        <li>测试</li>
    </ul>
    <script src="./copy.js"></script>
</body>
 */

const inputEl = document.getElementsByTagName('input')[0]
function readCopyText() {
    // Clipboard.readText(): 解析系统剪贴板的文本内容
    return new Promise((resolve) => {
        navigator.clipboard
            .readText()
            .then((res) => {
                inputEl.setAttribute('value', res)
                resolve(res)
            })
            .catch(() => {
                resolve('')
            })
    })
}
function copyText() {
    // Clipboard.writeText(): 写入特定字符串到操作系统的剪切板
    return new Promise((resolve) => {
        const text = inputEl.value
        navigator.clipboard
            .writeText(text)
            .then((res) => {
                resolve(res)
            })
            .catch(() => resolve(''))
    })
}
/** 需要权限校验:提前获取 "Permissions API" 的 "clipboard-write" 权限
 *  Clipboard.write(): 写入图片等任意的数据到剪贴板
 *  Clipboard.read():  读取剪贴板内容,如果不是纯文本,会报错
 */
function authCheck() {
    return new Promise((resolve) => {
        navigator.permissions
            .query({ name: 'clipboard-read' })
            .then((result) => {
                if (result.state == 'granted' || result.state == 'prompt') {
                    resolve(true)
                } else {
                    resolve(false)
                }
            })
            .catch(() => {
                return false
            })
    })
}
function copyImg() {
    return new Promise(async (resolve) => {
        const authResult = await authCheck()
        if (authResult) {
            // 如果读取的非文本资源,报错:MException: No valid data on clipboard.
            const clipboardItems = await navigator.clipboard.read()
            for (let i = 0; i < clipboardItems.length; i++) {
                const clipboardItem = clipboardItems[i]
                const types = clipboardItem.types
                for (let j = 0; j < types.length; j++) {
                    const type = types[j]
                    if (type === 'text/plain' || type === 'text/html') {
                        /** 文本节点处理 */
                        const contentBlob = await clipboardItem.getType(type)
                        const text = await contentBlob.text()
                        resolve(text)
                        return
                    }
                }
            }
            resolve('')
        }
    })
}
function setClipboardData() {
    return new Promise(async (resolve) => {
        const authResult = await authCheck()
        if (authResult) {
            // ClipboardItem类型
            const item = new ClipboardItem({
                'text/plain': new Blob(['hello world'], { type: 'text/plain' }),
            })
            navigator.clipboard.write([item]).then((res) => {
                console.log('res', res)
                resolve(res)
            })

            // DataTransfer类型, 仅用于交互式复制对象
            // const transferData = new DataTransfer()
            // transferData.items.add('text/plain', '替换了数据')
            // navigator.clipboard.write(transferData).then((res) => {
            //     console.log('res', res)
            //     resolve(res)
            // })
        }
    })
}

const copyFnList = [copyText, readCopyText, copyImg, setClipboardData]
const ulList = document.querySelectorAll('ul.copy-list>li')
for (let i = 0; i < ulList.length; i++) {
    ulList[i].addEventListener('click', () => {
        const bindFn = copyFnList[i]
        bindFn().then((res) => {
            console.log(res, '??copyImg??')
        })
    })
}

window.addEventListener('paste', function (event) {
    event.preventDefault()
    /** ClipboardEvent.clipboardData 属性保存了一个 DataTransfer 对象 */
    const items = event.clipboardData && event.clipboardData.items
    var file = null
    if (items && items.length) {
        // 检索剪切板items
        for (var i = 0; i < items.length; i++) {
            if (items[i].type.indexOf('image') !== -1) {
                file = items[i].getAsFile() // 返回File对象
                break
            }
            if (items[i].type === 'text/html' || items[i] === 'text/plain') {
                navigator.clipboard.readText().then((res) => {
                    console.log(res, '------------') // 文本节点处理
                })
            }
        }
    }
    event.preventDefault()
})