7年技术写作,分享6点心得阿宝哥在这篇文章中介绍了他经常使用的一款不错的在线绘图工具——Excalidraw。有了它,你可以轻松画出各种精美的手绘示意图。目前Excalidraw在Github上的star数已经达到23.9K,所以也是一个非常不错的开源项目。在使用Excalidraw的时候,阿宝哥发现这个在线工具提供了一些不错的功能。例如,将*.excalidraw文件保存到指定目录,拖拽打开*.excalidraw文件并保存到当前文件,复制图片到剪贴板,共享只读链接,实时协作等。提示:上图演示了拖拽*.excalidraw文件打开和保存到当前文件的功能。上面提到的很多函数都与文件操作有关。关于文件处理,阿宝哥之前写过两篇文件上传,了解这8个场景就够了,文件下载,了解这9个场景就够了。第三篇阿宝哥将带大家分析Excalidraw背后与文件操作相关的技术。了解并掌握这些相关技术后,可能对以后的工作会有用,尤其是一些在线网页编辑场景,使用这些技术会大大提升产品的用户体验。例如,在支持相关Web技术的平台上,您开发的在线编辑器可以完美支持打开->编辑->保存等常见的文件处理流程。话不多说,直入主题,这里先分析一下将.excalidraw文件保存到指定目录的功能。1.将文件保存到指定目录提示:本文所有演示示例使用的Chrome版本为:版本92.0.4515.159(正式版)(x86_64)以上Gif动图演示了文件保存到指定目录的过程目录,因为在线工具Excalidraw是开源的,所以通过分析其源码,我们找到了保存文件到指定目录的实现函数://https://github.com/excalidraw/excalidraw/blob/master/src/data/json.ts#L31import{fileOpen,fileSave}from"browser-fs-access";exportconstsaveAsJSON=async(elements:readonlyExcalidrawElement[],appState:AppState,)=>{constserialized=serializeAsJSON(elements,appState);constblob=newBlob([序列化],{type:MIME_TYPES.excalidraw,});constfileHandle=awaitfileSave(blob,{fileName:`${appState.name}.excalidraw`,description:"Excalidrawfile",extensions:[".excalidraw"],},isImageFileHandle(appState.fileHandle)?null:appState.fileHandle,);return{fileHandle};};从上面的代码可以看出,saveAsJSON函数调用了fileSave函数来保存文件。fileSave函数是从browser-fs-access第三个库中引入的。该库封装了File_System_Access_API,为开发者提供读写和管理文件的能力。保存文件到指定目录的功能是通过showSaveFilePicker方法实现的。在showSaveFilePicker方法出现之前,实现客户端保存文件功能比较常见的方案是使用a标签或者FileSaver.js库。constsaveFile=async(blob,filename)=>{consta=document.createElement('a');a.download=filename;a.href=URL.createObjectURL(blob);a.addEventListener('click',(e)=>{setTimeout(()=>URL.revokeObjectURL(a.href),30*1000);});a.click();};提示:如果想了解其他文件下载方式,可以阅读文件下载,了解这9种场景就够本文了。对于上述的客户端文件保存方案,其最大的问题是无法实现打开->编辑->保存的常用文件操作流程。因为我们没有办法覆盖原来的文件,所以只能新建一个文件。使用新的File_System_Access_API可以解决以上问题。比如我们可以使用window.showOpenFilePicker方法打开文件,编辑文件后使用window.showSaveFilePicker方法保存文件。(文本编辑器地址:https://googlechromelabs.github.io/text-editor/)下面介绍一下showSaveFilePickerAPI,它是定义在Window接口中的一个方法。文件选择器。该方法的签名如下:letFileSystemFileHandle=Window.showSaveFilePicker(options);showSaveFilePicker方法支持对象类型的可选参数,可以包含以下属性:excludeAcceptAllOption:布尔类型,默认值为false。默认情况下,选择器应包含不应用任何文件类型过滤器的选项(由下面的类型选项启用)。将此选项设置为true意味着类型选项不可用。types:数组类型,表示允许保存的文件类型列表。数组中的每一项都是一个配置对象,包含以下属性:description(可选):用于描述允许保存的文件类型的类别。accept:是一个对象,它的键是一个MIME类型,它的值是一个文件扩展名列表。调用showSaveFilePicker方法后,会返回一个FileSystemFileHandle对象。使用此对象,您可以调用对象上的方法来操作文件。例如调用对象的createWritable方法后,会返回FileSystemWritableFileStream对象,可以将数据写入文件。具体用法如下:asyncfunctionsaveFile(blob,filename){try{consthandle=awaitwindow.showSaveFilePicker({suggestedName:filename,types:[{description:"PNGfile",accept:{"image/png":[".png"],},},],});constwritable=awaithandle.createWritable();awaitwritable.write(blob);awaitwritable.close();returnhandle;}catch(err){console.error(err.name,err.message);}}saveFile(imgBlob,"face.png");当你使用上面的saveFile函数保存图片时,会出现如下的保存文件选择器:你觉得showSaveFilePicker这个API很实用吗?功能强大,可惜目前API的兼容性不是很好,如下图所示:(来源:https://caniuse.com/?search=showSaveFilePicker)showSaveFilePicker定义在FileSystemAccessAPI方法中,在除了showSaveFilePicker,还有showOpenFilePicker、showDirectoryPicker等方法。接下来阿宝哥就简单介绍下另外两个好用的API。showOpenFilePickerAPI是在Window接口中定义的一种方法,调用此方法后显示一个文件选择器,允许用户选择一个或多个文件。该方法的签名如下:letFileSystemHandles=Window.showOpenFilePicker();showOpenFilePicker方法支持一个对象类型的可选参数,可以包含以下属性:multiple:布尔类型,默认值为false。如果设置为true,则允许选择多个文件。excludeAcceptAllOption:布尔型,默认值为false。默认情况下,选择器应包含不应用任何文件类型过滤器的选项(由下面的类型选项启用)。将此选项设置为true意味着类型选项不可用。types:数组类型,表示允许保存的文件类型列表。数组中的每一项都是一个配置对象,包含以下属性:description(可选):用于描述允许保存的文件类型的类别。accept:是一个对象,它的键是一个MIME类型,它的值是一个文件扩展名列表。调用showOpenFilePicker方法后,会返回FileSystemHandles,这是一个FileSystemFileHandle对象的数组。使用FileSystemFileHandle对象,您可以调用该对象的方法来操作文件。举个简单的用法例子:
打开文件在上面的示例中,当用户单击打开文件按钮时,将显示一个文件选择器。选择文本文件后,文件内容会显示在textarea#container文本框中。对于非文本文件,可以调用arrayBuffer方法读取文件的二进制内容。(图片来源:https://caniuse.com/?search=showOpenFilePicker)从上图可以看出,showOpenFilePickerAPI的兼容性还是比较差的。但是如果你想在支持FileSystemAccessAPI的平台上优先使用这些API,你可以考虑使用GoogleChromeLabs开源的browser-fs-access库,它可以让你在支持FileSystemAccess的平台上更加轻松API文件系统访问API以一致的方式使用,和的方法会针对不支持的平台自动降级。除了选择文件,我们还可以选择目录。对于这种情况,我们可以使用showDirectoryPickerAPI。它是在Window接口中定义的一种方法,调用时会显示一个选择器,允许用户选择一个目录。该方法的签名如下:varFileSystemDirectoryHandle=Window.showDirectoryPicker();与前面介绍的showOpenFilePicker方法不同的是,调用showDirectoryPicker方法后,返回的是FileSystemDirectoryHandle对象。使用这个对象,我们可以执行一些与目录相关的操作。比如读取目录的信息,读取目录中的指定文件,删除目录中的指定文件或者在目录中新建文件等。还是举个简单的例子吧。读取目录信息asyncfunctionreadDirectory(){constdirHandle=awaitwindow.showDirectoryPicker();forawait(constentryofdirHandle.values()){console.log(entry.kind,entry.name);}}读取目录下的指定文件constcontainer=document.querySelector("#container");asyncfunctionreadFile(){constdirHandle=awaitwindow.showDirectoryPicker();constfileHandle=awaitdirHandle.getFileHandle("hello.txt");constfile=awaitfileHandle.getFile();constcontents=awaitfile.text();container.value=contents;}删除目录下的指定文件复制.txt文件${typeofresult=="undefined"?"Success":"Failure"}`;}需要注意的是,removeEntry方法除了支持删除指定文件外,还可以支持删除指定目录。创建指定文件asyncfunctioncreateFile(){constdirHandle=awaitwindow.showDirectoryPicker();constfileHandle=awaitdirHandle.getFileHandle("hello.new.txt",{create:true,});container.value="hello.new.txt文件创建成功!";constwritable=awaitfileHandle.createWritable();awaitwritable.write(newBlob(["大家好,我是包哥!"]));awaitwritable.close();}在上面的代码中,我们调用了getFileHandle方法来获取指定的文件,对应的FileSystemFileHandle对象。create:true表示如果在当前目录下没有找到指定的文件,则创建一个新文件。了解了以上例子后,是不是感觉浏览器的文件处理能力越来越强了。同样,我们来看看showDirectoryPickerAPI的兼容性:(来源:https://caniuse.com/?search=showDirectoryPicker)2.拖拽打开*.excalidraw文件,保存到当前文件。以上Gif动图演示拖拽打开*.excalidraw文件并保存到当前文件。可以发现,编辑文件后,我们只需要确认是否保存文件,无需选择文件保存路径,大大提升了用户体验。classAppextendsReact.Component{//省略大部分代码constfile=event.dataTransfer?.files[0];if(file?.type===MIME_TYPES.excalidrawlib||file?.name?.endsWith(".excalidrawlib")){//导入控件库的处理逻辑}else{this.setState({isLoading:true});if(fsSupported){//判断是否支持FileSystemAccessAPItry{constitem=event.dataTransfer.items[0];//关键点:获取FileSystemHandle对象(fileasany).handle=await(itemasany).getAsFileSystemHandle();}catch(error){console.warn(error.name,error.message);}}//加载。excalidraw文件到Canvasawaitthis.loadFileToCanvas(file);}};}上面代码的关键点是调用DataTransferItem.getAsFileSystemHandle()方法获取FileSystemFileHandle对象。一旦我们有了对象,我们就可以读取和写入文件。具体用法如下:读文件示例asyncfunctiongetTheFile(){//openfilepicker[fileHandle]=awaitwindow.showOpenFilePicker(pickerOpts);//getfilecontentsconstfileData=awaitfileHandle.getFile();}写文件示例asyncfunctionwriteFile(fileHandle,contents){//CreateaFileSystemWritableFileStreamtowriteto.constwritable=awaitfileHandle.createWritable();//Writethecontentoofthefiletothestream.awaitwritable.write(contents);//Closethefileandwritethecontentstodisk.awaitwritable.close();}3.复制图片到剪贴板//https://github.com/excalidraw/excalidraw/blob/master/src/clipboard.tsexportconstcopyBlobToClipboardAsPng=async(blob:Blob)=>{awaitnavigator.clipboard.write([newwindow.ClipboardItem({"image/png":blob}),]);};上面代码中copyBlobToClipboardAsPng函数支持一个blob参数,函数内部会调用navigator.clipboard.write方法将图片复制到剪贴板。对于普通文本,可以通过navigator.clipboard.writeText方法将它们写入系统剪贴板。实际上,navigator.clipboard.write和navigator.clipboard.writeText方法是Clipboard接口定义的方法。此接口实现剪贴板API。如果用户授予相应的权限,它可以提供对系统剪贴板的读写访问。在Web应用程序中,剪贴板API可用于实现剪切、复制和粘贴功能。该API用于替代document.execCommandAPI实现的剪贴板操作。在实际工作中,我们不需要手动创建剪贴板对象,而是通过navigator.clipboard获取剪贴板对象:获取剪贴板对象后,我们可以使用该对象提供的API来访问剪贴板。例如通过navigator.clipboard.readText方法读取剪贴板的内容:navigator.clipboard.readText().then(clipText=>document.querySelector(".editor").innerText=clipText);上面的代码将HTML中带有.editor类的第一个元素的内容替换为剪贴板的内容。如果剪贴板为空,或不包含任何文本,则元素的内容将被清空。这是因为当剪贴板为空或不包含文本时,readText方法返回一个空字符串。异步剪贴板API是一个相对较新的API,浏览器仍在逐步实现它。由于潜在的安全问题和技术复杂性,大多数浏览器都在逐渐集成这个API。当前的NavigatorAPI:剪贴板兼容性如下图:(图片来源:https://caniuse.com/mdn-api_navigator_clipboard)对于浏览器扩展,可以请求clipboardRead和clipboardWrite权限来使用clipboard.readText()和clipboard.writeText().如果您对其他剪贴板API感兴趣,可以阅读想要复制图像?剪贴板API了解本文。事实上,除了上述技术外,Excalidraw还使用了其他WebAPI来实现特定的功能。例如,当使用window.cryptoAPI导出只读链接时,画布数据被加密和保护。使用WebSocketAPI实现协同编辑,使用ShareAPI实现文件共享。有兴趣的小伙伴可以阅读Excalidraw的相关源码。4.总结在这篇文章中,阿宝哥分析了在线绘图工具Excalidraw背后的技术,它提供了一些很好的功能。希望阅读本文后,您对File_System_Access_API中定义的方法window.showOpenFilePicker、window.showSaveFilePicker、window.showDirectoryPicker和DataTransferItem.getAsFileSystemHandle有一定的了解。由于目前File_System_Access_API的兼容性不是很好,如果你想在项目中使用它。推荐大家使用GoogleChromeLabs开源的browser-fs-access库,它不仅为我们提供了更简洁的API,还提供了自动降级的解决方案。在以后的项目中,如果有机会,可以尝试一下。5.参考资源web.dev—excalidraw-and-fuguweb.dev—browser-fs-accessweb.dev—file-system-accessMDN—File_System_Access_APIMDN—FileSystemFileHandle