Navigator Clipboard 复制不生效

使用 navigator.clipboard.writeText 完成复制功能的实现时,在本地测试没有问题,部署后报错navigator.clipboard Cannot read property ‘writeText‘ of undefined

原因:Navigator API 的安全策略禁用了非安全域的 navigator.clipboard 对象,API 仅支持通过 HTTPS 提供的页面。为帮助防止滥用,仅当页面是活动选项卡时才允许访问剪贴板。活动选项卡中的页面无需请求许可即可写入剪贴板,但从剪贴板读取始终需要许可。

https://w3c.github.io/clipboard-apis/#dom-navigator-clipboard

解决:判断当前环境是否支持navigator clipboard API,不允许则使用 document.execCommand('copy')进行剪贴板交互。
使用兼容方案原因:navigator clipboard API是异步API,而使用document.execCommand('copy')进行剪贴板访问是同步的,只能读写 DOM,效率低下且在各浏览器之间还可能存在不同,在支持navigator clipboard API的情况下应尽量避免使用document.execCommand('copy')

传入DOM ID

export function copyCurrentTarget(text, id = '') {
    if (navigator.clipboard && window.isSecureContext) {
        navigator.clipboard.writeText(text)
    } else {
        window.getSelection().removeAllRanges()
        const questionToCopy = document.querySelector('#' + id)
        const range = document.createRange()
        range.selectNode(questionToCopy)
        window.getSelection().addRange(range)
        try {
            const successful = document.execCommand('copy')
            if (successful) {
                console.log('复制成功')
            } 
        } catch (error) {
            console.error(error)
        }
    }
}

视口外创建一个新的DOM,传入要复制的内容

function copyToClipboard(textToCopy) {
    if (navigator.clipboard && window.isSecureContext) {
        return navigator.clipboard.writeText(textToCopy)
    } else {
        let textArea = document.createElement("textarea")
        textArea.value = textToCopy
        textArea.style.position = "fixed"
        textArea.style.left = "-999999px"
        textArea.style.top = "-999999px"
        document.body.appendChild(textArea)
        textArea.focus()
        textArea.select()
        return new Promise((res, rej) => {
            document.execCommand('copy') ? res() : rej()
            textArea.remove()
        })
    }
}

扩展:安全和权限

复制和粘贴权限已添加到 Permissions API 中。当页面处于活动标签页时,会自动授予 clipboard-write 权限。 clipboard-read 权限必须手动请求。如果尚未授予权限,尝试读取或写入剪贴板数据的操作会自动提示用户授予权限。

const queryOpts = { name: 'clipboard-read', allowWithoutGesture: false };
const permissionStatus = await navigator.permissions.query(queryOpts);
// 'granted', 'denied' or 'prompt':
console.log(permissionStatus.state);

permissionStatus.onchange = () => {
  console.log(permissionStatus.state);
};

因为 Chrome 仅在页面是活动选项卡时才允许剪贴板访问,某些示例如果直接粘贴到 DevTools 中将无法运行,因为 DevTools 本身就是活动选项卡。有一个技巧:使用 setTimeout() 延迟剪贴板访问,然后在调用函数之前快速点击页面内部以将其聚焦:

setTimeout(async () => {
const text = await navigator.clipboard.readText();
console.log(text);
}, 2000);

要在 iframe 中使用 API,需要使用权限策略启用它

<iframe
    src="index.html"
    allow="clipboard-read; clipboard-write"
>
</iframe>

参考:https://stackoverflow.com/questions/51805395/navigator-clipboard-is-undefined、https://developer.chrome.com/blog/cut-and-copy-commands/、https://web.dev/async-clipboard/

判断对象为空

❌ 错误做法:

let obj = {}
obj == {} // false

✔ 正确做法:

const isObjEmpty = (obj) => !Reflect.ownKeys(obj).length && obj.constructor === Object;

注意:

  • 数组也可以使用Reflect.ownKeys方法,所以需要判断类型
  • 为什么不用Object.keys()Object.keys只能返回对象中可枚举的属性,Reflect.ownKeys返回所有属性
var obj = {
    a: 1,
    b: 2
}
Object.defineProperty(obj, 'method', {
    value: function () {
    alert("Non enumerable property")
},
    enumerable: false
})
​
console.log(Object.keys(obj))
// ["a", "b"]
console.log(Reflect.ownKeys(obj))
// ["a", "b", "method"]

层叠上下文和z-index

渲染过程和层叠顺序

浏览器将HTML解析为DOM的同时还创建了另一个树形结构,渲染树(render tree)。渲染树代表了每个元素的视觉样式和位置,同时决定浏览器绘制元素的顺序。

  • 通常情况下(没有使用定位),元素在HTML里出现的顺序决定了绘制顺序。后绘制的元素会出现在先绘制的元素前面。
  • 定位元素时,这种行为会改变。浏览器会先绘制所有非定位的元素,然后绘制定位元素。默认情况下,所有的定位元素会出现在非定位元素前面。

使用z-index控制层叠顺序

z-index属性的值可以是任意整数(正负都行)。z表示的是笛卡儿x-y-z坐标系里的深度方向。拥有较高z-index的元素出现在拥有较低z-index的元素前面。拥有负数z-index的元素出现在静态元素后面。

z-index的行为很好理解,但是使用它时要注意两个小陷阱。

  • z-index只在定位元素上生效,不能用它控制静态元素。
  • 给一个定位元素加上z-index可以创建层叠上下文。

层叠上下文

一个层叠上下文包含一个元素或者由浏览器一起绘制的一组元素。其中一个元素会作为层叠上下文的根,比如给一个定位元素加上z-index的时候,它就变成了一个新的层叠上下文的根。所有后代元素就是这个层叠上下文的一部分。

所有层叠上下文内的元素会按照以下顺序,从后到前叠放:

  • 层叠上下文的根
  • z-index为负的定位元素(及其子元素)
  • 非定位元素
  • z-indexauto的定位元素(及其子元素)
  • z-index为正的定位元素(及其子元素)

举个例子

下面这个例子可以用来理解层叠上下文,nested在第一个盒子的层叠上下文中,就算设置了很高的z-index,也会被第二个盒子遮挡。因为第一个盒子形成的层叠上下文在第二个盒子后面。

<body>
  <div class="box one positioned">
    one
    <div class="absolute">nested</div>
  </div>
  <div class="box two positioned">two
  </div>

</body>
body {
  margin: 40px;
}

.box {
  display: inline-block;
  width: 200px;
  line-height: 200px;
  text-align: center;
  border: 2px solid black;
  background-color: #ea5;
  margin-left: -60px;
  vertical-align: top;
}

.one { margin-left: 0; }
.two { margin-top: 30px; }


.positioned {        (以下5行)每个定位的盒子都创建了一个层叠上下文,z-index为1
  position: relative;               
  background-color: #5ae;           
  z-index: 1;                      
}                                  

.absolute {
  position: absolute;
  top: 1em;
  right: 1em;
  height: 2em;
  background-color: #fff;
  border: 2px dashed #888;              
  z-index: 100;       ←---- z-index只控制元素在它所处层叠上下文内的层叠顺序
  line-height: initial;
  padding: 1em;
}

表现结果

扩展到实际应用中的场景是,在已经打开的弹窗中再打开一个弹窗,就算第一个弹窗中的某个元素设置了很高的z-index,也还是会被第二个(后绘制)的弹窗挡住。