copy-text-to-clipboard 源码解析

232 阅读4分钟

今天来看一个非常简单的 npm 库的源码。你会发现,原来写一个(或者参与) 1k star 的开源项目会这么简单。

image.png

使用

copy-text-to-clipboard 的功能很简单,就是前端复制粘贴板的功能,其使用方式也很简单:

import copy from 'copy-text-to-clipboard';

button.addEventListener('click', () => {
  copy('🦄🌈');
});

copy 函数用于复制内部的字符串到操作系统的粘贴板里。

源码流程

先来看一下人家源码的目录:

image.png

查看 package.json:

{
    "name": "copy-text-to-clipboard",
    "version": "3.0.1",
    "description": "Copy text to the clipboard in modern browsers (0.2 kB)",
    "main": "index.js",
    "license": "MIT",
    "repository": "sindresorhus/copy-text-to-clipboard",
    "funding": "https://github.com/sponsors/sindresorhus",
    "author": {
            "name": "Sindre Sorhus",
            "email": "sindresorhus@gmail.com",
            "url": "https://sindresorhus.com"
    },
    "type": "module",
    "exports": "./index.js",
    "engines": {
            "node": ">=12"
    },
    ...
}

可以看到入口文件是 index.js, 我们进去看一看:

export default function copyTextToClipboard(input, {target = document.body} = {}) {
	const element = document.createElement('textarea');
	const previouslyFocusedElement = document.activeElement;

	element.value = input;

	// Prevent keyboard from showing on mobile
	element.setAttribute('readonly', '');

	element.style.contain = 'strict';
	element.style.position = 'absolute';
	element.style.left = '-9999px';
	element.style.fontSize = '12pt'; // Prevent zooming on iOS

	const selection = document.getSelection();
	const originalRange = selection.rangeCount > 0 && selection.getRangeAt(0);

	target.append(element);
	element.select();

	// Explicit selection workaround for iOS
	element.selectionStart = 0;
	element.selectionEnd = input.length;

	let isSuccess = false;
	try {
		isSuccess = document.execCommand('copy');
	} catch {}

	element.remove();

	if (originalRange) {
		selection.removeAllRanges();
		selection.addRange(originalRange);
	}

	// Get the focus back on the previously focused element, if any
	if (previouslyFocusedElement) {
		previouslyFocusedElement.focus();
	}

	return isSuccess;
}

然后源码的部分就结束了,没错~,就这么简单,一个 0.2kb 的开源项目就完成了。我们逐行来解析。

函数 copyTextToClipboard 接受两个参数,一个 input 是需要复制的文本字符串,target 表示文本域插入的位置,一般是用不上的,可能在复杂的前端架构中,比如多框架兼容的微服务中能用得到。

接下来创建一个文本域:

const element = document.createElement('textarea');

然后设置文本域的值:

element.value = input;

将文本域隐藏:

element.style.contain = 'strict';
element.style.position = 'absolute';
element.style.left = '-9999px';
element.style.fontSize = '12pt'; // Prevent zooming on iOS

这其中有一些 hack 操作在里边,element.style.contain = 'strict' 将元素及其内容尽可能独立于文档树的其余部分,这样做可以提高页面的性能,因为浏览器不需要频繁地重绘或重新布局整个页面;

而在iOS设备上,尤其是iOS 10及以后的版本中,Safari浏览器不再完全遵守通过meta标签设置的viewport规则来禁用页面缩放,element.style.fontSize = '12pt' 明确指定文字大小,iOS 会认为字体已经被知名样式,不再进行自定义优化和缩放操作了。

为了阻止意外的鼠标键盘输入,还需要禁用输入:

element.setAttribute('readonly', '');

插入元素到 dom 中并选中文本:

target.append(element);
element.select();

element.selectionStart = 0;
element.selectionEnd = input.length;

接下来调用浏览器 API 完成复制:

const selection = document.getSelection();
        
try {
   isSuccess = document.execCommand('copy');
} catch {}

销毁元素:

element.remove();

这期间他还做了兼容操作:

  • 在选中文本前万一页面上有其他的选中操作,需要记录状态:
const selection = document.getSelection();
const originalRange = selection.rangeCount > 0 && selection.getRangeAt(0);

在文本框销毁后还原之前的选择位置:

if (originalRange) {
    selection.removeAllRanges();
    selection.addRange(originalRange);
}
  • 还原页面一开始聚焦的元素的聚焦状态:
// 程序一开始执行
const previouslyFocusedElement = document.activeElement;

元素销毁后还原焦点:

if (previouslyFocusedElement) {
    previouslyFocusedElement.focus();
}

document.activeElement 返回当前文档中获得焦点的元素。

函数最后返回成功的标志位。

改进点

document.execCommand API 已经快要废弃掉了,官方推荐使用新的 API 代替:

navigator.clipboard.writeText('要复制的文本内容').then(() => {
    console.log('复制成功');
}).catch(err => {
    console.error('复制失败: ', err);
});

当然了,其有兼容性问题,更好的方案就是两者兼备:

const handleCopy = () => {
    if (navigator.clipboard) {
      navigator.clipboard.writeText(input);
    } else {
      // 创建text area
      const textArea = document.createElement('textarea');
      // 省略上文中的步骤
      ...
      // 执行复制命令并移除文本框
      document.execCommand('copy');
    }
};

像这种情况,你发现了源码中的问题,就可以提 issue 给作者来参与源码建设了。

测试

可以看到项目过于简单,无需其他的打包工具,作者只写了一个简单的功能测试用例:

image.png

详情可以自己去源码查看

此外还有一个代码质量检测的工具:

"scripts": {
    "test": "xo && tsd"
},

xo 是一个基于 ESLint 的代码质量工具,tsd是用于检查 TypeScript 类型定义的工具。此类的测试工具比较多,这里就不赘述。

总结

本文以一个简单的源码例子来讲解我们普通的软件工程师如何参与开源项目。

写开源项目不同于具体的业务逻辑代码,它不但要考虑各种各样的用户环境(不同平台、版本兼容性、typescript 环境等),还要考虑到各种使用情况(用户已经有选择的文本、用户已经聚焦了某个元素等等),有可能还要考虑以后的升级和扩展,其开发维护成本更高,含金量也会更大一些。