<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
        <title>胡尊杰的博客</title>
        <link>http://blog.pyzy.net</link>
        <description>以思想为梁，用知识做瓦，拿创意装潢，每个人都可以搭建自己的宫殿。这里是一个Web前端从业者的日常点滴、积累、总结。</description>
        <atom:link href="http://blog.pyzy.net/rss.html" rel="self" />
        <language>zh-cn</language>
        <lastBuildDate>Mon, 09 Mar 2026 09:34:20 GMT</lastBuildDate>
        
        <item>
            <title>混沌系统-系统设计的基础原则</title>
            <link>http://blog.pyzy.net/post/chaotic.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-ae3">混沌系统</a><ul>
<li><a href="#toc-e45">引言</a></li>
<li><a href="#toc-d24">二义性发散问题带来的不可靠</a><ul>
<li><a href="#toc-73a">经典“二义性”需求示例</a></li>
<li><a href="#toc-40c">二义性程序逻辑叠加示例</a></li>
</ul>
</li>
<li><a href="#toc-3ca">复杂度的经典例子</a><ul>
<li><a href="#toc-e85">单摆</a></li>
<li><a href="#toc-8d9">双摆</a></li>
<li><a href="#toc-8cc">三摆（多摆）</a></li>
</ul>
</li>
<li><a href="#toc-941">最后</a></li>
</ul>
</li>
</ul>
</div><h1><a id="toc-ae3" class="anchor" href="#toc-ae3"></a>混沌系统</h1>
<h2><a id="toc-e45" class="anchor" href="#toc-e45"></a>引言</h2>
<p>一个系统或流程功能的设计，应该尽量收敛到可靠状态。</p>
<p>当多个不可靠状态不断叠加，便会逐渐变成混沌系统 --- 事态变的不可控。</p>
<p>导致人们难以理解、分不清是 Feature 还是 Bug。</p>
<p>也自然对系统未来的迭代，悄悄地预埋上了随时崩溃的风险。</p>
<h2><a id="toc-d24" class="anchor" href="#toc-d24"></a>二义性发散问题带来的不可靠</h2>
<h3><a id="toc-73a" class="anchor" href="#toc-73a"></a>经典“二义性”需求示例</h3>
<p>老婆：“老公你去买一斤包子回来，如果看到卖西瓜的，就买一个”。</p>
<p>然后，看到老公买了一个包子回来。</p>
<h3><a id="toc-40c" class="anchor" href="#toc-40c"></a>二义性程序逻辑叠加示例</h3>
<pre><code class="hljs lang-lasso"><span class="hljs-comment">// 似乎粗看没什么问题</span>
<span class="hljs-keyword">if</span> (num === <span class="hljs-number">1</span> || num !== <span class="hljs-number">2</span>) { <span class="hljs-params">...</span>any<span class="hljs-params">...</span> }

<span class="hljs-comment">// 也许未来有一天会变成</span>
num2 = <span class="hljs-number">2</span>;
<span class="hljs-keyword">if</span> (num === <span class="hljs-number">1</span> || num !== num2) { <span class="hljs-params">...</span>any<span class="hljs-params">...</span> }

<span class="hljs-comment">// 再继续来一次无脑迭代</span>
num2 = <span class="hljs-number">1</span> || <span class="hljs-number">2</span>;
<span class="hljs-keyword">if</span> (num === <span class="hljs-number">1</span> || num !== num2) { <span class="hljs-params">...</span>any<span class="hljs-params">...</span> }

<span class="hljs-comment">// 经典问题</span>
num = top.undefined;
num2 = <span class="hljs-built_in">self</span>.undefined;
<span class="hljs-keyword">if</span> (num === num2) { <span class="hljs-params">...</span>any<span class="hljs-params">...</span> }
</code></pre><h2><a id="toc-3ca" class="anchor" href="#toc-3ca"></a>复杂度的经典例子</h2>
<h3><a id="toc-e85" class="anchor" href="#toc-e85"></a>单摆</h3>
<p>一个具备「可靠&amp;可预知」轨迹状态的系统功能。</p>
<p>其运行过程中有1个动态的状态节点控制，可以推演解释任意时刻轨迹状态。</p>
<p><img src="https://blog.pyzy.net/static/upload/20230829/hxILgYFMSRJ7Z70T_S9-SzRT.gif" alt="alt"></p>
<h3><a id="toc-8d9" class="anchor" href="#toc-8d9"></a>双摆</h3>
<p>一个具备「不可靠&amp;难以预知」轨迹状态的系统功能。</p>
<p>其运行过程中有2个动态的状态节点，想要推演解释任意时刻的状态值结论，已经非常艰难。</p>
<p><img src="https://blog.pyzy.net/static/upload/20230829/VVaiCJbFz5NDwDRtBknY_mHy.gif" alt="alt"></p>
<h3><a id="toc-8cc" class="anchor" href="#toc-8cc"></a>三摆（多摆）</h3>
<p>一个具备「不可靠&amp;不可预知」轨迹状态的系统功能。</p>
<p>其运行过程中有多个动态的状态节点，任意时刻的状态值恐怕不是有生之年能解释的清楚推演明白的。</p>
<p> <img src="https://blog.pyzy.net/static/upload/20230829/xDJgoM-_aDYv-r4-6T3aABmd.gif" alt="alt"></p>
<h2><a id="toc-941" class="anchor" href="#toc-941"></a>最后</h2>
<p>所以，一个好的系统或程序设计，应该具备可归纳可收敛、数据信息可沉淀可二次迭代创作的一些基本特征。</p>

            ]]></description>
            <pubDate>Tue, 29 Aug 2023 09:45:03 GMT</pubDate>
            <guid>http://blog.pyzy.net/post/chaotic.html</guid>
        </item>
        
        <item>
            <title>浏览器与Electron端文件与文件夹打包发送</title>
            <link>http://blog.pyzy.net/post/jszip.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-cba">几个文件文件夹数据交互途径及技术方案及各自的弊端</a><ul>
<li><a href="#toc-cda">1、点击按钮或菜单项的“选择文件”发送</a><ul>
<li><a href="#toc-449">技术原理</a></li>
<li><a href="#toc-55f">技术实现</a></li>
<li><a href="#toc-ec1">特殊情况</a></li>
<li><a href="#toc-2ca">典型弊端</a></li>
</ul>
</li>
<li><a href="#toc-d24">2、点击按钮或菜单项的“选择文件夹”发送</a><ul>
<li><a href="#toc-449">技术原理</a></li>
<li><a href="#toc-55f">技术实现</a></li>
<li><a href="#toc-ec1">特殊情况</a></li>
<li><a href="#toc-2ca">典型弊端</a></li>
</ul>
</li>
<li><a href="#toc-9de">3、从磁盘拖拽文件或文件夹到APP</a><ul>
<li><a href="#toc-449">技术原理</a></li>
<li><a href="#toc-55f">技术实现</a></li>
<li><a href="#toc-ec1">特殊情况</a></li>
<li><a href="#toc-2ca">典型弊端</a></li>
</ul>
</li>
<li><a href="#toc-d0a">4、在 APP 中 CtrlOrCmd+V 从 磁盘 CtrlOrCmd+C 复制的文件和文件夹</a><ul>
<li><a href="#toc-449">技术原理</a></li>
<li><a href="#toc-55f">技术实现</a></li>
<li><a href="#toc-ec1">特殊情况</a></li>
<li><a href="#toc-2ca">典型弊端</a></li>
</ul>
</li>
<li><a href="#toc-8e6">5、在 APP 的消息输入框右键“粘贴” 从 磁盘 CtrlOrCmd+C 复制的文件和文件夹</a><ul>
<li><a href="#toc-449">技术原理</a></li>
<li><a href="#toc-55f">技术实现</a></li>
<li><a href="#toc-ec1">特殊情况</a></li>
<li><a href="#toc-2ca">典型弊端</a></li>
</ul>
</li>
<li><a href="#toc-903">使用JsZip进行文件夹压缩打包</a></li>
<li><a href="#toc-a64">以上方案的大前提</a></li>
<li><a href="#toc-f4e">后续优化</a></li>
</ul>
</li>
</ul>
</div><h2><a id="toc-cba" class="anchor" href="#toc-cba"></a>几个文件文件夹数据交互途径及技术方案及各自的弊端</h2>
<h3><a id="toc-cda" class="anchor" href="#toc-cda"></a>1、点击按钮或菜单项的“选择文件”发送</h3>
<h4><a id="toc-449" class="anchor" href="#toc-449"></a>技术原理</h4>
<p>使用浏览器原生的文件输入框 HTMLInputElement，用户选择文件后可以再 onChange 检测输入框内容变化、从 HTMLInputElement.files 获取FileList。</p>
<p>文档可参考： <a href="https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input/file">https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input/file</a></p>
<h4><a id="toc-55f" class="anchor" href="#toc-55f"></a>技术实现</h4>
<p>支持多选（multiple=&quot;true&quot;）、未限制类型。
<img src="https://blog.pyzy.net/static/upload/20230802/hdCZ69ET_p1GSBKROZ5IT3IR.png" alt="alt"></p>
<p>在onChange 获取并校验后得到合法 FileList 后，使用 sendFiles 组件打开文件发送框或直接执行发送（用户选择了“选择文件直接发送”的）。
<img src="https://blog.pyzy.net/static/upload/20230802/AoaqNcHxT9UrluPKs_uZxAU3.png" alt="alt"></p>
<h4><a id="toc-ec1" class="anchor" href="#toc-ec1"></a>特殊情况</h4>
<p>按浏览器和操作系统不同，选择的内容会有特殊问题，比如：</p>
<ul>
<li>Mac端选择 “xxx.app”会实际往输入框输入一个“xxx.app.zip”</li>
<li>文件和文件夹混选时<ul>
<li>先选文件还是先选文件夹、windows还是Mac都会有差异、不能读到文件夹对象或指针。</li>
<li>综上，业务逻辑中会忽略文件夹，防止旧版里&quot;误把文件夹当文件发送，最终消息发送失败&quot;的问题发生。</li>
</ul>
</li>
</ul>
<h4><a id="toc-2ca" class="anchor" href="#toc-2ca"></a>典型弊端</h4>
<ul>
<li>FileList 是一维数组，不知道文件夹包含关系，文件量大浏览器Native层面可能就会响应很长时间。</li>
<li>“xxx.app”特殊文件变“xxx.app.zip”的情形，浏览器Native层面会响应很长时间（处理时长取决于.app包大小）。</li>
<li>用户选择文件的量不可控，比如用户有个  /Logs 目录，里面的 “*.log”很多并且体量很大，时间空间复杂度一定会很高。</li>
</ul>
<h3><a id="toc-d24" class="anchor" href="#toc-d24"></a>2、点击按钮或菜单项的“选择文件夹”发送</h3>
<h4><a id="toc-449" class="anchor" href="#toc-449"></a>技术原理</h4>
<p>使用浏览器原生的文件输入框 HTMLInputElement，并设置实验性的“webkitdirectory”属性，用户选择文件夹后可以再 onChange 检测输入框内容变化、从 HTMLInputElement.files 获取 FileList（一个被递归扁平化的一维文件对象数组），通过 File.webkitRelativePath 判定是否在同一个文件夹下。</p>
<p>文档可参考： <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLInputElement/webkitdirectory">https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLInputElement/webkitdirectory</a></p>
<h4><a id="toc-55f" class="anchor" href="#toc-55f"></a>技术实现</h4>
<p>支持多选（multiple=&quot;true&quot;）、未限制类型。
<img src="https://blog.pyzy.net/static/upload/20230802/YeZJxI1X4whqOVd7h7Ii4BYW.png" alt="alt"></p>
<p>在onChange 获取并校验后得到合法 FileList 后，转换成“文件夹对象”、使用 sendFiles 组件打开文件发送框、执行文件夹压缩、或直接执行发送（用户选择了“选择文件直接发送”的）。</p>
<h4><a id="toc-ec1" class="anchor" href="#toc-ec1"></a>特殊情况</h4>
<p>虽然设置了多选，但目前看并不能多选。</p>
<h4><a id="toc-2ca" class="anchor" href="#toc-2ca"></a>典型弊端</h4>
<ul>
<li>不能多选文件夹（浏览器API层面设置了多选没用）、不能文件文件夹混选。</li>
<li>FileList 是扁平化的文件一维数组：<ul>
<li>文件量大浏览器Native层面可能就会响应很长时间。</li>
<li>要得到树状文件夹包含关系，就必须枚举遍历，根据 File.webkitRelativePath 做出判定，文件量大循环响应的时长也自然会长。</li>
</ul>
</li>
<li>“xxx.app”特殊文件变“xxx.app.zip”的情形，浏览器Native层面会响应很长时间（处理时长取决于.app包大小）。</li>
<li>用户选择文件夹里的内容量不可控，比如用户直接选/Windows系统安装包目录，时间空间复杂度一定会很高。</li>
</ul>
<h3><a id="toc-9de" class="anchor" href="#toc-9de"></a>3、从磁盘拖拽文件或文件夹到APP</h3>
<h4><a id="toc-449" class="anchor" href="#toc-449"></a>技术原理</h4>
<p>使用浏览器原生的 doucument.onDrop 监听拖入事件，从 event.dataTransfer.items 读取拖入的内容（可以是多个文件夹或文件的混合内容），再 DataTransferItem.webkitGetAsEntry() 得到 entry、再 entry.isDirectory 判定是否文件夹，是文件夹的递归得到目录内容（用于判断超限、文件夹压缩）；最终得到一个一维的 FileList 供 sendFile 组件走后续交互逻辑。</p>
<p>文档和官方示例参考： <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransferItem/webkitGetAsEntry#%E7%A4%BA%E4%BE%8B">https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransferItem/webkitGetAsEntry#%E7%A4%BA%E4%BE%8B</a></p>
<h4><a id="toc-55f" class="anchor" href="#toc-55f"></a>技术实现</h4>
<p>支持多选&amp;混选。
<img src="https://blog.pyzy.net/static/upload/20230802/xpOc5eyTSmKxCMQ_mvuIy_AX.png" alt="alt"></p>
<h4><a id="toc-ec1" class="anchor" href="#toc-ec1"></a>特殊情况</h4>
<p>能得到树状目录结构。</p>
<h4><a id="toc-2ca" class="anchor" href="#toc-2ca"></a>典型弊端</h4>
<ul>
<li>从 DataTransferItems 到 FileList 流程偏长，存在异步IO读取，耗时长短取决于内容量（参见下图）。
<img src="https://blog.pyzy.net/static/upload/20230802/6Cd3a6OeyD4hGgMR_ZM7DDqR.png" alt="alt"></li>
<li>用户拖拽内容量不可控，比如用户直接在C盘下全选、拖拽，时间空间复杂度一定会很高。</li>
</ul>
<h3><a id="toc-d0a" class="anchor" href="#toc-d0a"></a>4、在 APP 中 CtrlOrCmd+V 从 磁盘 CtrlOrCmd+C 复制的文件和文件夹</h3>
<h4><a id="toc-449" class="anchor" href="#toc-449"></a>技术原理</h4>
<p>使用浏览器原生的 doucument.onPaste 监听粘贴事件，然后复用上面拖拽的FileList获得逻辑：</p>
<ul>
<li>从 event.dataTransfer.items 读取拖入的内容（可以是多个文件夹或文件的混合内容）。</li>
<li>再 DataTransferItem.webkitGetAsEntry() 得到 entry、再 entry.isDirectory 判定是否文件夹，是文件夹的递归得到目录内容（用于判断超限、文件夹压缩）</li>
<li>最终得到一个一维的 FileList 供 sendFile 组件走后续交互逻辑。</li>
</ul>
<p>文档参考： <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/paste_event">https://developer.mozilla.org/en-US/docs/Web/API/Document/paste_event</a></p>
<h4><a id="toc-55f" class="anchor" href="#toc-55f"></a>技术实现</h4>
<p>理论上支持多选&amp;混选，然后 Ctrl+C 写入剪切板，读取时得到。</p>
<p><img src="https://blog.pyzy.net/static/upload/20230802/b28tEgsywxJJ_iEIgJSOjLwT.png" alt="alt"></p>
<p><img src="https://blog.pyzy.net/static/upload/20230802/6DYDLT27MmwQ_1s0vZOWavWg.png" alt="alt"></p>
<h4><a id="toc-ec1" class="anchor" href="#toc-ec1"></a>特殊情况</h4>
<ul>
<li>剪切板有可能会是截图、纯文本、HTML、文件、文件夹，所以必须要做各CASE分支处理、并达成各自的需求。</li>
<li>这里纯粹靠浏览器onPaste事件的DataTransfer，如果有文件或文件夹读取不到的情况，首先需要确定是否权限问题、被浏览器滤掉等等。</li>
</ul>
<h4><a id="toc-2ca" class="anchor" href="#toc-2ca"></a>典型弊端</h4>
<ul>
<li>从 DataTransferItems 到 FileList 流程偏长，存在异步IO读取，耗时长短取决于内容量。</li>
<li>用户复制内容量不可控，比如用户直接在C盘下全选、Ctrl+C，时间空间复杂度一定会很高。</li>
</ul>
<h3><a id="toc-8e6" class="anchor" href="#toc-8e6"></a>5、在 APP 的消息输入框右键“粘贴” 从 磁盘 CtrlOrCmd+C 复制的文件和文件夹</h3>
<h4><a id="toc-449" class="anchor" href="#toc-449"></a>技术原理</h4>
<p>使用 Electron 的 NodeJS 能力，读取剪切板里的文件或文件夹路径、最终转换得到 FileList：</p>
<ul>
<li>先通过NodeJS读取剪切板中的文件或文件夹路径</li>
<li>再通过NodeJS按路径递归读取得到 FIle 对象，传递给渲染进程进行UI展现、发送服务端。</li>
</ul>
<p>文档参考： <a href="https://github.com/electron/electron/issues/9035">https://github.com/electron/electron/issues/9035</a></p>
<h4><a id="toc-55f" class="anchor" href="#toc-55f"></a>技术实现</h4>
<p>读取剪切板中的文件或文件夹路径：
<img src="https://blog.pyzy.net/static/upload/20230802/IOq5YL8NQFUaW1raJ2Hrmvx3.png" alt="alt"></p>
<p>通过文件路径得到目录关系、文件数据，并进行超限判定：</p>
<p><img src="https://blog.pyzy.net/static/upload/20230802/RLHPecjFn1uhaDPOXFlvxuo4.png" alt="alt"></p>
<p>将Native层的数据，转为渲染进程可用的FileList：</p>
<p><img src="https://blog.pyzy.net/static/upload/20230802/r4tg8b-8wKJ9XZvkV7wRbxkm.png" alt="alt"></p>
<p>粘贴按钮按下后，插入文件或文本或HTML到编辑器或sendFile 组件：</p>
<p><img src="https://blog.pyzy.net/static/upload/20230802/xzcruZOmizCAGBMySK5221gF.png" alt="alt"></p>
<h4><a id="toc-ec1" class="anchor" href="#toc-ec1"></a>特殊情况</h4>
<p>getClipboardPaths 不可靠，比如windows端，无法读取到多个路径，如果 Ctrl+C 了多个，这里只能拿到其中的一个。</p>
<h4><a id="toc-2ca" class="anchor" href="#toc-2ca"></a>典型弊端</h4>
<ul>
<li>从剪切板路径读取 到 FileList 流程更长，也存在异步IO读取，耗时长短取决于内容量，且更突出。</li>
<li>用户Ctrl+C复制内容量、或文件夹里的内容量不可控，时间空间复杂度一定会很高。</li>
</ul>
<h3><a id="toc-903" class="anchor" href="#toc-903"></a>使用JsZip进行文件夹压缩打包</h3>
<p><img src="https://blog.pyzy.net/static/upload/20230802/ZME0RQXyfxGtZsjNeidS29jQ.png" alt="alt"></p>
<h3><a id="toc-a64" class="anchor" href="#toc-a64"></a>以上方案的大前提</h3>
<p>目前是考虑最快方案：不考虑各种复杂的UI和产品交互设计、减少各场景CASE分支的定制化开发、尽量保证 Web端&amp;Electron端 尽快先跑起来达成基本需求。</p>
<h3><a id="toc-f4e" class="anchor" href="#toc-f4e"></a>后续优化</h3>
<ul>
<li>将异步IO，放在UI展示之后，需要UI先展示Loading或“文件夹大小计算中”等中间状态。<ul>
<li>对交互体验肯定有优化效果，但对交互流程的时长，只可能会消耗更多。</li>
</ul>
</li>
<li>将文件读取压缩的能力放 Native NodeJS 层。<ul>
<li>需要考虑拖拽、Ctrl+V 怎么从渲染进程拿到 NodeJS，去做监听响应。</li>
<li>需要考虑清楚怎么解决UI交互、文件发送时在渲染层的问题。</li>
<li>会增加跨进程数据通讯或异步数据更新的数据交互频次（耗时增加）。</li>
</ul>
</li>
</ul>

            ]]></description>
            <pubDate>Wed, 02 Aug 2023 05:42:10 GMT</pubDate>
            <guid>http://blog.pyzy.net/post/jszip.html</guid>
        </item>
        
        <item>
            <title>如何在网页上低成本实现一个简单的“护眼模式”</title>
            <link>http://blog.pyzy.net/post/cye-enabled.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-391">先看个效果</a></li>
<li><a href="#toc-435">最简单的护眼实现代码</a></li>
<li><a href="#toc-f00">让是否开启“护眼模式”可控</a></li>
<li><a href="#toc-207">配合离线存储，记住用户设置</a></li>
<li><a href="#toc-751">支持微调亮度对比度（修改文档流上下文的CSS规则）</a></li>
<li><a href="#toc-a7f">如何监听系统主题色调变化</a></li>
<li><a href="#toc-84c">不带UI的，“完整”效果代码</a></li>
<li><a href="#toc-cb0">CSS中的其他可用方案</a></li>
</ul>
</div><h2><a id="toc-391" class="anchor" href="#toc-391"></a>先看个效果</h2>
<p>默认模式：</p>
<p><img src="https://blog.pyzy.net/static/upload/20220324/Mk7C2BuMELIiR1MAtK7QpFhf.png" alt="alt"></p>
<p>护眼模式：</p>
<p><img src="https://blog.pyzy.net/static/upload/20220324/x5h20y7qQriOEBCJvQO7jJAx.png" alt="alt"></p>
<p>对于需要长时间盯着屏幕同学，后面这个“护眼模式”相对“默认模式”的亮底，应该算是更舒适、不那么容易引起视觉疲劳的一个选择。</p>
<p>在当下，作为Web前端开发者，要在网页上添加这么个护眼模式，其实可能并不需要定制化的去针对暗色调写一套完整皮肤样式。</p>
<h2><a id="toc-435" class="anchor" href="#toc-435"></a>最简单的护眼实现代码</h2>
<p>要在任意网站启用上图的“护眼模式”，其实你只需要打开浏览器控制台，执行以下代码就可以了：</p>
<pre><code class="hljs lang-processing">    <span class="hljs-keyword">const</span> cssEl = document.createElement(<span class="hljs-string">'style'</span>)
    cssEl.innerHTML = `
        html.cye-enabled {
          <span class="hljs-built_in">filter</span>: contrast(<span class="hljs-number">0.96</span>) <span class="hljs-built_in">brightness</span>(<span class="hljs-number">0.9</span>) invert(<span class="hljs-number">1</span>);
        }
        html.cye-enabled img {
          <span class="hljs-built_in">filter</span>: <span class="hljs-built_in">brightness</span>(<span class="hljs-number">0.9</span>) invert(<span class="hljs-number">1</span>);
        }
    `;
    document.head.appendChild(cssEl);
    document.documentElement.classList.<span class="hljs-built_in">add</span>(<span class="hljs-string">'cye-enabled'</span>);
</code></pre><p>很简单对不对。</p>
<p>其实就是给 documentElement 添加一个样式类，相应的，也就是添加个反色滤镜（让亮色变成暗色）。</p>
<p>不想让img图片变成反色而导致肉眼无法识别，则可以再通过滤镜反转回来。</p>
<h2><a id="toc-f00" class="anchor" href="#toc-f00"></a>让是否开启“护眼模式”可控</h2>
<h2><a id="toc-207" class="anchor" href="#toc-207"></a>配合离线存储，记住用户设置</h2>
<h2><a id="toc-751" class="anchor" href="#toc-751"></a>支持微调亮度对比度（修改文档流上下文的CSS规则）</h2>
<h2><a id="toc-a7f" class="anchor" href="#toc-a7f"></a>如何监听系统主题色调变化</h2>
<h2><a id="toc-84c" class="anchor" href="#toc-84c"></a>不带UI的，“完整”效果代码</h2>
<p>可以复制以下代码到浏览器控制台执行，然后通过 <code>window.setCyeFilter(enabled, contrast, brightness)</code>设置护眼模式开关方式、对比度、亮度。</p>
<pre><code class="hljs lang-scheme">(() =&gt; {
    const cssEl = document.createElement(<span class="hljs-symbol">'style</span>')
    cssEl.innerHTML = `
        html.cye-enabled {
          background: <span class="hljs-literal">#f</span>ff<span class="hljs-comment">; /* 如果有自己的背景色，那么去掉这行 */</span>
          filter: contrast(<span class="hljs-name">0.96</span>) brightness(<span class="hljs-name">0.9</span>) invert(<span class="hljs-name">1</span>)<span class="hljs-comment">;</span>
        }
        html.cye-enabled img {
          filter: brightness(<span class="hljs-name">0.9</span>) invert(<span class="hljs-name">1</span>)<span class="hljs-comment">;</span>
        }
        html.cye-enabled body {
            filter: none !important<span class="hljs-comment">; /* 防止三方浏览器插件等重复的护眼样式生效 */</span>
        }
    `<span class="hljs-comment">;</span>
    document.head.appendChild(<span class="hljs-name">cssEl</span>)<span class="hljs-comment">;</span>
    // document.documentElement.classList.add(<span class="hljs-symbol">'cye-enabled</span>')<span class="hljs-comment">;</span>

    /***** 一些JS设置逻辑 ****/
    // 护眼模式
    const getLV = (<span class="hljs-name">k</span>) =&gt; JSON.parse(<span class="hljs-name">localStorage.getItem</span>(<span class="hljs-name">k</span>))<span class="hljs-comment">;</span>
    const setLV = (<span class="hljs-name">k</span>, v) =&gt; localStorage.setItem(<span class="hljs-name">k</span>, JSON.stringify(<span class="hljs-name">v</span>))<span class="hljs-comment">;</span>

    let cyeMediaQueryList = null<span class="hljs-comment">;</span>
    try {
      cyeMediaQueryList = window.matchMedia('(prefers-color-scheme: dark)')<span class="hljs-comment">;</span>
      cyeMediaQueryList.addListener(<span class="hljs-name">setDocCyeByLS</span>)<span class="hljs-comment">; // 通过浏览器API监听系统层面的主题模式配置变换</span>
    } catch(<span class="hljs-name">e</span>) {
      console.warn(<span class="hljs-symbol">'当前环境可能不支持</span> matchMedia')<span class="hljs-comment">;</span>
    }
    window.setCyeFilter = (<span class="hljs-name">enabled</span>, contrast, brightness) =&gt; {
      setLV(<span class="hljs-symbol">'cyeEnabled</span>', enabled)<span class="hljs-comment">; // true 开启护眼模式，false 关闭护眼模式，2 跟随系统主题是否黑暗模式动态设定</span>
      setLV(<span class="hljs-symbol">'cyeContrast</span>', contrast)<span class="hljs-comment">; // 对比度，可微调视觉效果</span>
      setLV(<span class="hljs-symbol">'cyeBrightness</span>', brightness)<span class="hljs-comment">; // 亮度，可微调视觉效果</span>

      // 护眼模式为“开启”或“跟随系统”且系统是开启的
      const isOpen = enabled === true || (<span class="hljs-name">enabled</span> == <span class="hljs-number">2</span> &amp;&amp; cyeMediaQueryList &amp;&amp; cyeMediaQueryList.matches)<span class="hljs-comment">;</span>
      const { classList } = document.documentElement || {}<span class="hljs-comment">;</span>
      classList &amp;&amp; classList[<span class="hljs-name">isOpen</span> ? <span class="hljs-symbol">'add</span>' : <span class="hljs-symbol">'remove</span>'](<span class="hljs-symbol">'cye-enabled</span>')<span class="hljs-comment">;</span>

      if (<span class="hljs-name">!isOpen</span>) return<span class="hljs-comment">;</span>
      const sss = document.styleSheets<span class="hljs-comment">;</span>
      if (<span class="hljs-name">!sss</span>) return<span class="hljs-comment">;</span>
      let csss = null<span class="hljs-comment">;</span>
      for (<span class="hljs-name"><span class="hljs-builtin-name">let</span></span> i = <span class="hljs-number">0</span><span class="hljs-comment">; i &lt; sss.length; i++) {</span>
        const cssi = sss[<span class="hljs-name">i</span>]<span class="hljs-comment">;</span>
        if (<span class="hljs-name">cssi</span> &amp;&amp; cssi.ownerNode &amp;&amp; cssi.ownerNode.id == <span class="hljs-symbol">'ghdef</span>') {
          csss = cssi<span class="hljs-comment">;</span>
          break<span class="hljs-comment">;</span>
        }
      }
      if (<span class="hljs-name">!csss</span>) return<span class="hljs-comment">;</span>
      const ruls = csss.rules<span class="hljs-comment">;</span>
      if (<span class="hljs-name">!ruls</span>) return<span class="hljs-comment">;</span>
      let clsObj = null<span class="hljs-comment">;</span>
      for (<span class="hljs-name"><span class="hljs-builtin-name">let</span></span> j = <span class="hljs-number">0</span><span class="hljs-comment">; j &lt; ruls.length; j++) {</span>
        const rulj = ruls[<span class="hljs-name">j</span>]<span class="hljs-comment">;</span>
        if (<span class="hljs-name">rulj</span> &amp;&amp; rulj.selectorText === <span class="hljs-symbol">'html.cye-enabled</span>') {
          clsObj = rulj<span class="hljs-comment">;</span>
          break<span class="hljs-comment">;</span>
        }
      }
      if (<span class="hljs-name">!clsObj</span>) return<span class="hljs-comment">;</span>
      clsObj.style.filter = <span class="hljs-symbol">'contrast</span>(' + contrast + ') brightness(' + brightness + ') invert(<span class="hljs-name">1</span>)'<span class="hljs-comment">;</span>
      return true<span class="hljs-comment">;</span>
    }<span class="hljs-comment">;</span>
    // 根据本地存储，判断是否开启护眼模式
    function setDocCyeByLS() {
      setCyeFilter(
        getLV(<span class="hljs-symbol">'cyeEnabled</span>'),
        getLV(<span class="hljs-symbol">'cyeContrast</span>') || <span class="hljs-number">0.96</span>,
        getLV(<span class="hljs-symbol">'cyeBrightness</span>') || <span class="hljs-number">0.9</span>
      )<span class="hljs-comment">;</span>
    }<span class="hljs-comment">;</span>
    setDocCyeByLS()<span class="hljs-comment">;</span>
})()
</code></pre><h2><a id="toc-cb0" class="anchor" href="#toc-cb0"></a>CSS中的其他可用方案</h2>
<p><code>mix-blend-mode: difference</code></p>
<p><a href="https://blog.csdn.net/weixin_44733660/article/details/121958062">https://blog.csdn.net/weixin_44733660/article/details/121958062</a></p>

            ]]></description>
            <pubDate>Thu, 24 Mar 2022 03:58:26 GMT</pubDate>
            <guid>http://blog.pyzy.net/post/cye-enabled.html</guid>
        </item>
        
        <item>
            <title>跨网页或APP数据文件共享续篇</title>
            <link>http://blog.pyzy.net/post/datatransfer.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-df3">前言</a></li>
<li><a href="#toc-967">剪切板富文本的应用</a><ul>
<li><a href="#toc-4c9">剪切板HTML内容的读取</a></li>
<li><a href="#toc-855">HTML内容特定场景的二次利用</a></li>
<li><a href="#toc-6b6">主动写HTML内容到剪切板</a></li>
</ul>
</li>
<li><a href="#toc-c20">网页中通过拖拽分享数据</a><ul>
<li><a href="#toc-0f7">网页中接收文件拖入</a></li>
<li><a href="#toc-38f">从网页中拖出文本内容</a></li>
<li><a href="#toc-d25">网页中接收拖入的文本内容</a></li>
<li><a href="#toc-6e3">查看拖入网页的各种内容数据</a></li>
<li><a href="#toc-ee3">单次拖出多种类型数据</a></li>
<li><a href="#toc-4da">设置拖拽图标</a></li>
<li><a href="#toc-186">从网页中拖出文件</a><ul>
<li><a href="#toc-f12">File 拖出</a></li>
<li><a href="#toc-1c9">文件拖出结论</a></li>
</ul>
</li>
<li><a href="#toc-2c3">拖拽下载</a></li>
</ul>
</li>
<li><a href="#toc-9d0">写在最后</a></li>
</ul>
</div><h2><a id="toc-df3" class="anchor" href="#toc-df3"></a>前言</h2>
<p>我在前面一篇文章《<a href="https://blog.pyzy.net/post/editor.html">记一个网页端IM文本编辑器的演进过程 </a> （<a href="https://blog.pyzy.net/post/editor.html">https://blog.pyzy.net/post/editor.html</a> ）》虽然没有提到 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransfer">DataTransfer</a>，但实际也涉及到了从剪切板（clipboardData）或拖拽元素（dataTransfer）来读取内容、插入编辑区的功能。</p>
<p>而在更早的另一篇文章《<a href="https://blog.pyzy.net/post/clipboard.html">Web前端剪切板文本分享到文件发送</a>  （ <a href="https://blog.pyzy.net/post/clipboard.html">https://blog.pyzy.net/post/clipboard.html</a> ）》一文里，也有涉及到过剪切板（clipboardData）或拖拽磁盘文件（dataTransfer）读取识别内容达成文件上传发送相关的介绍。</p>
<p>今天，再通过具体示例，接续前面两篇文章内容，来聊一聊“通过DataTransfer 读取使用剪切板HTML富文本、以及通过拖拽分享数据内容”的两个场景化应用。</p>
<h2><a id="toc-967" class="anchor" href="#toc-967"></a>剪切板富文本的应用</h2>
<p>先接续上最近的、也就是上一篇编辑器相关的能力拓展。</p>
<h3><a id="toc-4c9" class="anchor" href="#toc-4c9"></a>剪切板HTML内容的读取</h3>
<p>在《<a href="https://blog.pyzy.net/post/clipboard.html">Web前端剪切板文本分享到文件发送</a> 》一文的《<a href="https://blog.pyzy.net/post/clipboard.html#toc-d4e">认识剪切板中的内容</a>》一节中，有介绍通过监听 document 的 paste 粘贴事件来读取剪切版内容，通过当时的 <a href="https://lab.pyzy.net/paste.html?bublog">Demo工具</a> 也可以直接在线看到能从剪切板读取的内容。</p>
<p>比如复制前面正在看到的这段文字，到Demo工具页粘贴，可以看到能读取到<code>text/plain</code>和<code>text/html</code>两种类型的内容：</p>
<p><img src="https://blog.pyzy.net/static/upload/20211121/2O8Tqan_ntMUeh0CvumPq40O.png" alt="alt"></p>
<p>简化后的“通过对粘贴事件的监听读到剪切板中的HTML内容”代码如下：</p>
<pre><code class="hljs lang-javascript"><span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'paste'</span>, (evt) =&gt; {
  <span class="hljs-keyword">const</span> { clipboardData } = evt;
  <span class="hljs-keyword">const</span> html = clipboardData.getData(<span class="hljs-string">'text/html'</span>);
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'html:'</span>, html);
});
</code></pre>
<h3><a id="toc-855" class="anchor" href="#toc-855"></a>HTML内容特定场景的二次利用</h3>
<p>我们在之前《<a href="https://blog.pyzy.net/post/editor.html">记一个网页端IM文本编辑器的演进过程 </a> （<a href="https://blog.pyzy.net/post/editor.html">https://blog.pyzy.net/post/editor.html</a> ）》中有介绍，当时实现的这个“富文本编辑器”，并不是能接受任意HTML的，有<a href="https://blog.pyzy.net/post/editor.html#toc-71f">按照场景化需求特意定制的能力实现</a>。</p>
<p>所以还需要给编辑器添加一个 <code>editor.insertHTML(html)</code>的能力，将HTML内容还原成符合编辑器需要的内容就可以了「这里需要留意是否可能产生XSS风险、而需要过滤防范」。</p>
<p>这其实也简单，因为之前实现的“<a href="https://blog.pyzy.net/post/editor.html#toc-22f">富文本与正文数据互转</a> ”中已经实现了对普通HTML节点树进行过滤、转换为编辑器所需JSON描述内容的能力 <code>richTextNodes2MsgData(richTextEl)</code>，最后再通过 insertNodes 插入编辑器即可。</p>
<p>所以这里只要从任意三方复制的 html 内容是能符合编辑器合法输入元素规则的，都可以很方便的还原成可二次使用的数据。</p>
<p>同理，如果从Execl复制一个表格，要在自己的WEB网页应用中粘贴还原出表格，自然也可以做到，只要做好特定场景的HTML内容格式化转换能力就够了。</p>
<h3><a id="toc-6b6" class="anchor" href="#toc-6b6"></a>主动写HTML内容到剪切板</h3>
<p>有时候我们也会遇到主动写HTML富文本内容到剪切板的情况，按最新API能力可以如下实现：</p>
<pre><code class="hljs lang-javascript"><span class="hljs-keyword">const</span> type = <span class="hljs-string">'text/html'</span>;
navigator.clipboard.write([
  <span class="hljs-keyword">new</span> ClipboardItem({
    [type]: <span class="hljs-keyword">new</span> Blob(
        [<span class="hljs-string">`&lt;span&gt;html content...&lt;/span&gt;`</span>],
        { type }
    ),
  }),
]).then(
  <span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'ok'</span>), 
  (err) =&gt; <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'err'</span>, err)
);
</code></pre>
<p>但当下（撰写本文时）各浏览器对 Clipboard API 能力的支持程度还不够理想。</p>
<p>比如：支持<code>clipboard.write</code>的浏览器，也未必支持write <code>text/html</code>内容到剪切板；而支持<code>text/html</code>的浏览器可能也未必开启了剪切板操作权限等等。</p>
<p>如果要考虑兼容性降级支持，来实现一个 <code>HTMLElement</code> 的复制，不妨试一下下面这个方案：</p>
<pre><code class="hljs lang-xml"><span class="hljs-meta">&lt;!DOCTYPE html&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"robots"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"noindex"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>复制HTML元素<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"el"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"btn"</span>&gt;</span>复制<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"ret"</span>&gt;</span>点击复制试试<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
    btn.onclick = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> ret = <span class="hljs-keyword">await</span> copyElement(el);
      ret.innerText = (
        ret === <span class="hljs-literal">true</span> ? <span class="hljs-string">'复制成功'</span> : <span class="hljs-string">`复制失败: <span class="hljs-subst">${ret}</span>`</span>
      );
    };

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">copyElement</span>(<span class="hljs-params">el</span>) </span>{
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> type = <span class="hljs-string">'text/html'</span>;
        <span class="hljs-keyword">const</span> text = <span class="hljs-string">'text/plain'</span>;
        <span class="hljs-keyword">const</span> ret = <span class="hljs-keyword">await</span> navigator.clipboard.write([
          <span class="hljs-keyword">new</span> ClipboardItem({
            [type]: <span class="hljs-keyword">new</span> Blob([el.outerHTML], { type }),
            [text]: <span class="hljs-keyword">new</span> Blob([el.innerText], { <span class="hljs-attr">type</span>: text }),
          }),
        ]).then(<span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> <span class="hljs-literal">true</span>, (err) =&gt; {
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[clipboard.write] err:'</span>, err);
          <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
        });
        <span class="hljs-keyword">if</span> (ret) <span class="hljs-keyword">return</span> ret;
      } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-comment">// 忽略异常，走下面的备用方案</span>
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[Error] Clipboard write:'</span>, err);
      }
      <span class="hljs-keyword">try</span> {
        <span class="hljs-comment">// 【注意】目标元素CSS属性user-select 不能是禁用选择的</span>

        <span class="hljs-comment">/* 选中目标元素 sta */</span>
        <span class="hljs-keyword">const</span> pEl = el.parentElement;
        <span class="hljs-keyword">const</span> idx = [...pEl.childNodes].indexOf(el);
        <span class="hljs-keyword">const</span> ra = <span class="hljs-built_in">document</span>.createRange();
        ra.setStart(pEl, idx);
        ra.setEnd(pEl, idx + <span class="hljs-number">1</span>);
        <span class="hljs-keyword">const</span> sel = <span class="hljs-built_in">window</span>.getSelection();
        sel.removeAllRanges();
        sel.addRange(ra);
        <span class="hljs-comment">/* 选中目标元素 end */</span>

        <span class="hljs-comment">/* 复制选中内容 */</span>
        <span class="hljs-built_in">document</span>.execCommand(<span class="hljs-string">'Copy'</span>);

        <span class="hljs-comment">/* 移除选择状态 */</span>
        sel.removeAllRanges();

        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
      } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-keyword">return</span> <span class="hljs-string">'[Error] Set Selection And Copy'</span> + err;
      }
    };
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre><p>上面的复制HTML元素到剪切板的代码实现，你也可以<a href="https://code.h5jun.com/nabaj/edit?js,output">点这里在线试一下</a>。</p>
<h2><a id="toc-c20" class="anchor" href="#toc-c20"></a>网页中通过拖拽分享数据</h2>
<p><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_Drag_and_Drop_API">HTML5中<code>Drag</code>、<code>Drop</code>相关的API</a>能力，早已经不是个新的话题了。</p>
<p>通过拖拽文件放置到网页中来达成上传、网页中通过拖拽商品放置购物车实现购买等等，很多具体应用场景，大家也许早就不知不觉中有过相关的交互体验。</p>
<p>而<code>Drag</code>、<code>Drop</code>又完全可以分开了单独使用。</p>
<h3><a id="toc-0f7" class="anchor" href="#toc-0f7"></a>网页中接收文件拖入</h3>
<p>比如可以通过监听 document 的 drop 事件，感知到文件的放入，通过 event.dataTransfer 拿到拖入的数据信息：</p>
<pre><code class="hljs lang-javascript"><span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'drop'</span>, (e) =&gt; {
  e.preventDefault();
  <span class="hljs-built_in">console</span>.log(e.dataTransfer)
});
<span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'dragover'</span>, (e) =&gt; {
  e.preventDefault();
});
</code></pre>
<p>而浏览器对文件拖入是有默认行为的，比如HTML会直接被打开，所以当你想自己接管数据拖入的后续行为时，必须知道 <code>preventDefault</code> 是必须的。</p>
<p>而在文件上传的场景中，如果对用户拖入文件夹展开，以实现对文件逐个上传，往往也是必须的（<a href="https://code.h5jun.com/fowiz/4/edit?js,output">文件拖入监听的在线demo</a>）：</p>
<pre><code class="hljs lang-html"><span class="hljs-meta">&lt;!DOCTYPE html&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"robots"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"noindex"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>JS Bin<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"jsbin-css"</span>&gt;</span><span class="css">
    <span class="hljs-selector-tag">h2</span> {
      <span class="hljs-attribute">border-bottom</span>: solid <span class="hljs-number">1px</span> <span class="hljs-number">#eee</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">16px</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">10px</span>;
    }
    <span class="hljs-selector-id">#container</span> {
      <span class="hljs-attribute">border</span>: solid <span class="hljs-number">1px</span> <span class="hljs-number">#eee</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">10px</span>;
      <span class="hljs-attribute">background</span>: <span class="hljs-number">#f9fdff</span>;
    }
    <span class="hljs-selector-id">#container</span><span class="hljs-selector-pseudo">:empty</span><span class="hljs-selector-pseudo">:after</span> {
      <span class="hljs-attribute">content</span>: <span class="hljs-string">'试试从磁盘拖拽文件或文件夹到这里'</span>;
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#aaa</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">14px</span>;
    }
    <span class="hljs-selector-class">.item</span> {
      <span class="hljs-attribute">line-height</span>: <span class="hljs-number">22px</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">14px</span>;
      <span class="hljs-attribute">margin</span>: <span class="hljs-number">10px</span> <span class="hljs-number">0</span>;
      <span class="hljs-attribute">border-bottom</span>: <span class="hljs-number">1px</span> solid <span class="hljs-number">#ccc</span>;
    }
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>文件列表：<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"container"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
    <span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'drop'</span>, (e) =&gt; {
      e.stopPropagation();
      e.preventDefault();
      <span class="hljs-keyword">const</span> entrys = items2Entrys(e.dataTransfer.items);
      getFileListByEntrys(entrys).then(<span class="hljs-function">(<span class="hljs-params">fileList</span>) =&gt;</span> {
        container.innerHTML = fileList.map(<span class="hljs-function">(<span class="hljs-params">file, i</span>) =&gt;</span> {
          <span class="hljs-keyword">const</span> { fullPath, type, name, size } = file;
          <span class="hljs-keyword">return</span> <span class="hljs-string">`
            &lt;div class="item"&gt;
              序号: <span class="hljs-subst">${i}</span>; &lt;br /&gt;
              path: <span class="hljs-subst">${fullPath || name}</span>; &lt;br /&gt;
              type: <span class="hljs-subst">${type}</span>; &lt;br /&gt;
              size: <span class="hljs-subst">${size}</span>
            &lt;/div&gt;
          `</span>;
        }).join(<span class="hljs-string">''</span>);
      });
    });

    <span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'dragover'</span>, (e) =&gt; {
      e.stopPropagation();
      e.preventDefault();
    });

    <span class="hljs-comment">// 获取 fileList</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getFileListByEntrys</span>(<span class="hljs-params">entrys</span>) </span>{
      <span class="hljs-keyword">const</span> fileList = [];
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(
        [...entrys].map(<span class="hljs-keyword">async</span> (entry) =&gt; {
          <span class="hljs-keyword">if</span> (entry.isDirectory) {
            <span class="hljs-comment">// 子文件夹内容</span>
            <span class="hljs-keyword">const</span> subEntries = <span class="hljs-keyword">await</span> unfoldDirectory(entry);
            <span class="hljs-comment">// 递归展开</span>
            <span class="hljs-keyword">const</span> subItems = <span class="hljs-keyword">await</span> getFileListByEntrys(subEntries);
            fileList.push(...subItems);
          } <span class="hljs-keyword">else</span> {
            <span class="hljs-comment">// 读取文件信息</span>
            <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve</span>) =&gt;</span> {
              <span class="hljs-keyword">const</span> errCbk = <span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
                <span class="hljs-built_in">console</span>.warn(err);
                resolve();
              };
              <span class="hljs-keyword">try</span> {
                entry.file(<span class="hljs-function">(<span class="hljs-params">file</span>) =&gt;</span> {
                  <span class="hljs-keyword">if</span> (file) {
                    file.fullPath = entry.fullPath;
                    fileList.push(file);
                  }
                  resolve();
                }, errCbk);
              } <span class="hljs-keyword">catch</span> (err) {
                errCbk(err);
              }
            });
          }
        })
      );
      <span class="hljs-keyword">return</span> fileList;
    }

    <span class="hljs-comment">// DataTransferItem 转 FileSystemEntry</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">items2Entrys</span>(<span class="hljs-params">items</span>) </span>{
      <span class="hljs-keyword">return</span> [...items].map(<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> (
          (item.getAsEntry &amp;&amp; item.getAsEntry()) ||
          (item.webkitGetAsEntry &amp;&amp; item.webkitGetAsEntry())
        );
      }).filter(<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> !!item);
    }

    <span class="hljs-comment">// 展开文件列表</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">unfoldDirectory</span>(<span class="hljs-params">item</span>) </span>{
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve</span>) =&gt;</span> {
        item.createReader().readEntries(resolve, () =&gt; resolve([]));
      });
    }
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>而更多的场景可能还需要记录下文件所处文件夹信息，也可以基于上面代码自己试着修改实现一下，怎么还原出拖拽内容的树状目录结构。</p>
<h3><a id="toc-38f" class="anchor" href="#toc-38f"></a>从网页中拖出文本内容</h3>
<p>按惯例，从简入繁，后面介绍文件拖拽前，我们先试验一下文本的拖拽应用。</p>
<p>在网页里创建个<code>&lt;span /&gt;</code>，为其增加 <code>draggable=&quot;true&quot;</code> 配置为可拖拽的元素，然后再监听其被拖拽的行为事件，并写一段自定义数据到 <code>dataTransfer</code>中。</p>
<p>大致代码如下（<a href="https://code.h5jun.com/tubi/edit?html,js,output">自定义文本内容拖拽DEMO</a>）：</p>
<pre><code class="hljs lang-html"><span class="hljs-meta">&lt;!DOCTYPE html&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>可拖拽内容页<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="css">
    <span class="hljs-selector-tag">body</span> {
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">50px</span>;
    }
    <span class="hljs-selector-id">#draggable</span> {
      <span class="hljs-attribute">border</span>: solid <span class="hljs-number">1px</span> <span class="hljs-number">#ccc</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">10px</span> <span class="hljs-number">20px</span>;
    }
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"draggable"</span> <span class="hljs-attr">draggable</span>=<span class="hljs-string">"true"</span>&gt;</span>可拖拽元素块<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
    <span class="hljs-keyword">const</span> el = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'draggable'</span>);
    el.addEventListener(<span class="hljs-string">'dragstart'</span>, (e) =&gt; {
      e.dataTransfer.setData(
        <span class="hljs-string">'text'</span>,
        <span class="hljs-string">'这是一段拖拽内容 wakakaka~ '</span>
      );
    });
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>效果截图：</p>
<p><img src="https://blog.pyzy.net/static/upload/20211121/v-DwFmDZXQ39HdprNxIV91Q8.png" alt="alt"></p>
<p>这时候，就可以通过拖拽“可拖拽文本”将自定义内容拖拽到任意三方APP里了，比如拖拽到一个本地文本编辑器中正编辑内容中的话，你就会看到放入位置多出了我们设置的 &#39;这是一段拖拽内容 wakakaka~ &#39; 文案内容。</p>
<p>需要注意的是：如果拖拽文本内容到系统磁盘或桌面，可以得到一个扩展名为<code>textClipping</code>、内容为自定义文本内容的文本文件。</p>
<p>这有时候并不是想要的，比如拖拽一个商品到购物车的应用场景。</p>
<p>如果又刚好是单页应用内部的拖拽（非垮页、垮应用交互），可能放弃使用 <code>dataTransfer.getData</code>，而选择走内存变量的方式更符合应用场景（这不会干扰浏览器或WebView原有默认拖拽行为）。</p>
<h3><a id="toc-d25" class="anchor" href="#toc-d25"></a>网页中接收拖入的文本内容</h3>
<p>当然，我们自然可以实现一个接受拖入文案的网页，来对上面场景的文本拖拽做相关应用，大致代码实现和“网页中接收文件拖入类似”，不同的是只用来读取数据。</p>
<p>也可以通过打开 <a href="https://code.h5jun.com/muret/edit?js,output">网页中接收拖入文本内容Demo</a> 来在线试验：</p>
<pre><code class="hljs lang-javascript"><span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'drop'</span>, (e) =&gt; {
  e.preventDefault();
  <span class="hljs-keyword">const</span> text = e.dataTransfer.getData(<span class="hljs-string">'text'</span>);
  <span class="hljs-built_in">document</span>.body.innerText = <span class="hljs-string">`[<span class="hljs-subst">${+<span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>()}</span>] 您拖入了内容：<span class="hljs-subst">${text}</span>`</span>;
});
<span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'dragover'</span>, (e) =&gt; {
  e.preventDefault();
});
</code></pre>
<p><img src="https://blog.pyzy.net/static/upload/20211121/xjw6wnnx_P8v6rPuNVnMkb-e.png" alt="alt"></p>
<p>这里为了方便演示垮应用数据分享，将“从网页中拖出文本内容”、“网页中接收拖入的文本内容”分成了两个独立网页来分别实现。</p>
<p>如果是单页应用内的交互能力，当然也可以放到一个页面里去实现。</p>
<h3><a id="toc-6e3" class="anchor" href="#toc-6e3"></a>查看拖入网页的各种内容数据</h3>
<p>综合上面各种交互能力，为了方便观察拖拽数据内容。</p>
<p>类似之前<a href="https://code.h5jun.com/jucef/edit?js,output">剪切板内容读取Demo</a> ，我们也先实现一个<a href="https://code.h5jun.com/lexex/edit?css,js,output">拖拽内容读取Demo工具</a>。</p>
<pre><code class="hljs lang-html"><span class="hljs-meta">&lt;!DOCTYPE html&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"robots"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"noindex"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>拖拽dataTransfer内容读取试验<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"jsbin-css"</span>&gt;</span><span class="css">
    <span class="hljs-selector-tag">h2</span> {
      <span class="hljs-attribute">border-bottom</span>: solid <span class="hljs-number">1px</span> <span class="hljs-number">#eee</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">16px</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">10px</span>;
    }
    <span class="hljs-selector-id">#container</span> {
      <span class="hljs-attribute">border</span>: solid <span class="hljs-number">1px</span> <span class="hljs-number">#eee</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">10px</span>;
      <span class="hljs-attribute">background</span>: <span class="hljs-number">#f9fdff</span>;
    }
    <span class="hljs-selector-id">#container</span><span class="hljs-selector-pseudo">:empty</span><span class="hljs-selector-pseudo">:after</span> {
      <span class="hljs-attribute">content</span>: <span class="hljs-string">'试试拖拽任意内容到当前页面中看看。'</span>;
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#aaa</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">14px</span>;
    }
    <span class="hljs-selector-tag">h3</span> {
      <span class="hljs-attribute">border-bottom</span>: solid <span class="hljs-number">1px</span> <span class="hljs-number">#eee</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">16px</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">10px</span>;
      <span class="hljs-attribute">margin</span>: <span class="hljs-number">10px</span> <span class="hljs-number">0</span>;
    }
    <span class="hljs-selector-tag">textarea</span> {
      <span class="hljs-attribute">display</span>: block;
      <span class="hljs-attribute">width</span>: <span class="hljs-number">95%</span>;
      <span class="hljs-attribute">height</span>: <span class="hljs-number">200px</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">8px</span> <span class="hljs-number">10px</span>;
      <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">1px</span> <span class="hljs-number">1px</span> <span class="hljs-number">3px</span> <span class="hljs-built_in">rgb</span>(0 0 0 / 20%) inset;
      <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">4px</span>;
    }
    <span class="hljs-selector-tag">img</span> {
      <span class="hljs-attribute">display</span>: block;
      <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>;
    }
    <span class="hljs-selector-tag">p</span> {
      <span class="hljs-attribute">margin</span>: -<span class="hljs-number">10px</span> -<span class="hljs-number">10px</span> <span class="hljs-number">0</span>;
      <span class="hljs-attribute">display</span>: block;
      <span class="hljs-attribute">background</span>: <span class="hljs-number">#d4f2ff</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">10px</span>;
      <span class="hljs-attribute">border-bottom</span>: solid <span class="hljs-number">1px</span> <span class="hljs-number">#cde0e9</span>;
      <span class="hljs-attribute">text-shadow</span>: <span class="hljs-number">1px</span> <span class="hljs-number">1px</span> <span class="hljs-number">0px</span> <span class="hljs-number">#fff</span>;
    }
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>拖拽dataTransfer内容：<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"container"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
    <span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'drop'</span>, (evt) =&gt; {
      evt.preventDefault();
      container.innerHTML = <span class="hljs-string">''</span>;
      <span class="hljs-keyword">const</span> items = <span class="hljs-built_in">Array</span>.from((evt.dataTransfer || {}).items || []);
      <span class="hljs-keyword">const</span> countEl = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'p'</span>);
      countEl.innerText = <span class="hljs-string">'event.dataTransfer.items.length: '</span> + items.length;
      container.appendChild(countEl);
      items.forEach(<span class="hljs-function">(<span class="hljs-params">item, i</span>) =&gt;</span> {

        <span class="hljs-keyword">const</span> { kind, type } = item;
        <span class="hljs-keyword">const</span> entry = item.webkitGetAsEntry &amp;&amp; item.webkitGetAsEntry();
        <span class="hljs-keyword">const</span> isDirectory = entry &amp;&amp; entry.isDirectory;

        <span class="hljs-keyword">const</span> hdEl = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'h3'</span>);
        hdEl.innerText = <span class="hljs-string">`序号：<span class="hljs-subst">${i}</span>；kind：<span class="hljs-subst">${kind}</span>; type：<span class="hljs-subst">${type}</span>;`</span>;
        container.appendChild(hdEl);

        <span class="hljs-keyword">let</span> previewEl = <span class="hljs-literal">null</span>;

        <span class="hljs-keyword">if</span> (isDirectory) {
          previewEl = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'a'</span>);
          previewEl.innerText = <span class="hljs-string">'文件夹：'</span> + entry.fullPath;
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (kind === <span class="hljs-string">'file'</span>) {
          <span class="hljs-keyword">const</span> file = item.getAsFile();
          <span class="hljs-keyword">const</span> url = URL.createObjectURL(file);
          <span class="hljs-keyword">if</span> (<span class="hljs-regexp">/image/i</span>.test(type)) {
            previewEl = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'img'</span>);
            previewEl.src = url;
          } <span class="hljs-keyword">else</span> {
            previewEl = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'a'</span>);
            previewEl.href = url;
            previewEl.download = file.name;
            previewEl.innerText = file.name;
          }
        } <span class="hljs-keyword">else</span> {
          previewEl = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'textarea'</span>);
          item.getAsString(<span class="hljs-function">(<span class="hljs-params">str, ...args</span>) =&gt;</span> {
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'str'</span>, str, ...args)
            previewEl.value = str;
          });
        }

        container.appendChild(previewEl);
      });
    });
    <span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'dragover'</span>, (e) =&gt; {
      e.preventDefault();
    });
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>现在如果从磁盘上拖入以下内容到上面网页：</p>
<p><img src="https://blog.pyzy.net/static/upload/20211123/rmRtDGVdKmoG6EPI7RlW5bL1.png" alt="alt"></p>
<p>大致能看到如下读取到的文件或目录信息：</p>
<p><img src="https://blog.pyzy.net/static/upload/20211123/SSiYjXTW6mY_zug_t3uyR6nd.png" alt="alt"></p>
<h3><a id="toc-ee3" class="anchor" href="#toc-ee3"></a>单次拖出多种类型数据</h3>
<p>前面我们只试验了单次拖出一段文本内容，实际上现有API能力已经允许单次拖拽得到多种不同类型的数据，到达目标APP后再自行选择要使用的数据。</p>
<p>有点类似从Excel复制一段表格内容，能得到截图、文本、HTML数据内容。</p>
<p>具体实现可以<a href="https://code.h5jun.com/vayot/1/edit?js,output">点这里在线试验&quot;单次拖出多种类型数据&quot;</a>，也可以参考下面代码对前面的文本数据拖拽进行修改：</p>
<pre><code class="hljs lang-javascript"><span class="hljs-keyword">const</span> el = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'draggable'</span>);
el.addEventListener(<span class="hljs-string">'dragstart'</span>, (e) =&gt; {
  <span class="hljs-keyword">const</span> { items } = e.dataTransfer;
  items.add(<span class="hljs-string">'text plain test'</span>, <span class="hljs-string">'text/plain'</span>);
  items.add(<span class="hljs-string">"&lt;p&gt;... html test ...&lt;/p&gt;"</span>, <span class="hljs-string">"text/html"</span>);
  items.add(<span class="hljs-string">"https://blog.pyzy.net"</span>,<span class="hljs-string">"text/uri-list"</span>);
  items.add(<span class="hljs-string">"hzj custom content"</span>,<span class="hljs-string">"custom-hzj"</span>);
});
</code></pre>
<h3><a id="toc-4da" class="anchor" href="#toc-4da"></a>设置拖拽图标</h3>
<p>如果你有留意API文档，会发现还提供了一个 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransfer/setDragImage">dataTransfer.setDragImage</a> 方法。
可以用来设置拖拽内容时跟随鼠标的图标。</p>
<p>另外，为了对比试验，我们在页面里使用一个<code>img</code>元素，来对比一下浏览器原生图片拖拽与通过JS API setDragImage 的差异（<a href="https://code.h5jun.com/puner/2/edit?js,output">打开试验图片文件拖拽</a>）：</p>
<pre><code class="hljs lang-html"><span class="hljs-meta">&lt;!DOCTYPE html&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"robots"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"noindex"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>图片拖拽<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="css">
    <span class="hljs-selector-tag">body</span> {
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">50px</span>;
    }
    <span class="hljs-selector-id">#draggable</span> {
      <span class="hljs-attribute">border</span>: solid <span class="hljs-number">1px</span> <span class="hljs-number">#ccc</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">10px</span> <span class="hljs-number">20px</span>;
    }
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">span</span>
    <span class="hljs-attr">id</span>=<span class="hljs-string">"draggable"</span>
    <span class="hljs-attr">draggable</span>=<span class="hljs-string">"true"</span>
  &gt;</span>
    可拖拽元素块
  <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">br</span> /&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">br</span>/&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">h3</span>&gt;</span>原生IMG标签<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"img"</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://p4.ssl.qhimg.com/t0157fa323b319adac4.png"</span> /&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
    <span class="hljs-keyword">const</span> el = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'draggable'</span>);
    el.addEventListener(<span class="hljs-string">'dragstart'</span>, (e) =&gt; {
      e.dataTransfer.setDragImage(img, <span class="hljs-number">20</span>, <span class="hljs-number">20</span>);
    });
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>这里值得注意的是，设置拖拽图和单纯图片文件拖拽不是一回事；这一点可能会比较容易混淆。</p>
<p>其次，原生<code>img</code>元素拖拽时，实际也是HTMLElement元素的拖拽，目标接收者能得到图片资源对应的 <code>text/uri-list</code>、以及图片的outerHTML <code>text/html</code> 两种类型的具体数据，并没能得到预期的<code>File</code>数据。</p>
<h3><a id="toc-186" class="anchor" href="#toc-186"></a>从网页中拖出文件</h3>
<p>前文中从网页中拖出文本使用的是<code>dataTransfer.setData(...args)</code>，这个方法接收的两个参数均为字符串，也就是只能往dataTransfer里写字符串。</p>
<p>但我们想直接让用户从网页上拖拽一个真正的文件（<code>比如一个 new File(...args)对象</code>）到磁盘或桌面，基于现有API是否能实现呢？</p>
<p>我们看到 <code>event.dataTransfer.items</code> 是有为前端开发者暴露了一个 <code>add</code> 方法的， 对应MDN文档<a href="https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItemList/add">DataTransferItemList.add(...args)</a>。</p>
<h4><a id="toc-f12" class="anchor" href="#toc-f12"></a>File 拖出</h4>
<p>不妨来创建个<a href="https://code.h5jun.com/qave/edit?js,output">网页拖拽文件Demo</a>试验一下自定义 File 拖出的思路：</p>
<pre><code class="hljs lang-html"><span class="hljs-meta">&lt;!DOCTYPE html&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>可拖拽文件<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="css">
    <span class="hljs-selector-tag">body</span> {
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">50px</span>;
    }
    <span class="hljs-selector-id">#draggable</span> {
      <span class="hljs-attribute">border</span>: solid <span class="hljs-number">1px</span> <span class="hljs-number">#ccc</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">10px</span> <span class="hljs-number">20px</span>;
    }
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"draggable"</span> <span class="hljs-attr">draggable</span>=<span class="hljs-string">"true"</span>&gt;</span>可拖拽元素块<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
    <span class="hljs-keyword">const</span> el = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'draggable'</span>);
    el.addEventListener(<span class="hljs-string">'dragstart'</span>, (e) =&gt; {
      <span class="hljs-keyword">const</span> file = <span class="hljs-keyword">new</span> File([<span class="hljs-string">'text content'</span>], <span class="hljs-string">'filename.txt'</span>, {
         <span class="hljs-attr">type</span>: <span class="hljs-string">'text/plain'</span>,
      });
      e.dataTransfer.items.add(file);
    });
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>可以拖拽到前文<a href="https://code.h5jun.com/jupiy/1/edit?js,output">拖拽内容读取Demo工具</a>中看看数据内容。</p>
<p>经试验你会发现，目前（2021-11-25）还是无法达成预期的文件拖拽的：其表现和个别浏览器中剪切板复制粘贴文件类似，只能拿到文件名...</p>
<h4><a id="toc-1c9" class="anchor" href="#toc-1c9"></a>文件拖出结论</h4>
<p>虽然看起来API设计上允许 <code>add</code>一些<code>File</code> 到<code>DataTransferItemList</code>或<code>setDragImage</code>来写入图片，但还是跟理想的“文件拖拽”的预期结果存在巨大差异；目前来看，也就没有靠谱方案基于浏览器端 JS API 直接实现文件拖出了。</p>
<p>但不妨发散一下脑洞，如果是被拖出和接收拖入的目标APP都是可控范围内的，也许可以 <code>dataTransfer.setData(&#39;my-cus-type&#39;, DataURL)</code> 的方案，通过自定义字符串数据的类型和内容来自行解决。</p>
<p>如果刚好是Electron中的网页应用，那就方便的多了，可以通过<a href="https://www.electronjs.org/zh/docs/latest/tutorial/native-file-drag-drop#preloadjs">原生文件拖拽相关API</a>，达成Electron场景中的网页内拖拽文件给三方APP或桌面的能力。</p>
<h3><a id="toc-2c3" class="anchor" href="#toc-2c3"></a>拖拽下载</h3>
<p>另外，想要拖拽触发浏览器的下载并将文件存储到磁盘目标位置，还有下面这个非标准方案：</p>
<pre><code class="hljs lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"btnDownload"</span> <span class="hljs-attr">draggable</span>=<span class="hljs-string">"true"</span>&gt;</span>拖拽下载<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">const</span> TYPE = <span class="hljs-string">'image/png'</span>;
<span class="hljs-keyword">const</span> NAME = <span class="hljs-string">'name.png'</span>;
<span class="hljs-keyword">const</span> URL = <span class="hljs-string">'https://example.com/tmpname.ext'</span>;
btnDownload.onDragStart = <span class="hljs-function">(<span class="hljs-params">evt</span>) =&gt;</span> {
    event.dataTransfer.setData(<span class="hljs-string">'DownloadURL'</span>, <span class="hljs-string">`<span class="hljs-subst">${TYPE}</span>:<span class="hljs-subst">${NAME}</span>:<span class="hljs-subst">${URL}</span>`</span>);
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre><h2><a id="toc-9d0" class="anchor" href="#toc-9d0"></a>写在最后</h2>
<p>首先，感谢您的阅读关注。</p>
<p>还是如同往常一样，请读者注意：以上所有示例代码更侧重在思路示意，甚至可以说是伪代码，如果要在正式业务场景应用，请一定酌情调整、增加严谨性、健壮性、兼容性相关考量。</p>
<p>另外，相关API能力也是日新月异，本文的方案只是基于时下的形式考虑，如果发现不符欢迎随时指正，以免误导。</p>
<p>有更具体和拓展思路的应用场景，欢迎联系作者补充进来，一起拓展Web前端的能力边界，达成更多实用的业务能力。</p>

            ]]></description>
            <pubDate>Sun, 21 Nov 2021 06:44:14 GMT</pubDate>
            <guid>http://blog.pyzy.net/post/datatransfer.html</guid>
        </item>
        
        <item>
            <title>记一个网页端IM文本编辑器的演进过程</title>
            <link>http://blog.pyzy.net/post/editor.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-8e1">背景</a></li>
<li><a href="#toc-b72">基础效果</a></li>
<li><a href="#toc-abf">历史问题</a></li>
<li><a href="#toc-9b2">精简提效</a></li>
<li><a href="#toc-9af">隐含功能</a></li>
<li><a href="#toc-d0f">新需求-“图文混发”</a><ul>
<li><a href="#toc-164">&quot;需求文档&quot;：</a></li>
<li><a href="#toc-bdd">最终实现效果</a></li>
<li><a href="#toc-71f">最终达成功能概要</a></li>
</ul>
</li>
<li><a href="#toc-f76">“图文混发”关键技术环节</a><ul>
<li><a href="#toc-15a">技术选型</a></li>
<li><a href="#toc-3b9">insert 方法改造</a></li>
<li><a href="#toc-22f">富文本与正文数据互转</a></li>
<li><a href="#toc-1ba">光标位置读写</a></li>
<li><a href="#toc-c09">编辑器内容插入的入口方法</a></li>
<li><a href="#toc-27e">草稿存储改造（含插入图片存储）</a></li>
<li><a href="#toc-c11">兼容性</a></li>
<li><a href="#toc-bc6">特殊内容相关</a></li>
</ul>
</li>
<li><a href="#toc-7a0">后记</a></li>
</ul>
</div><h2><a id="toc-8e1" class="anchor" href="#toc-8e1"></a>背景</h2>
<p>前端项目开发中，为用户输入内容提供一个编辑框是非常常见的基础交互需求。
比如博客文章下面的评论区、可视化富文本编辑、在线代码编辑器... </p>
<p>根据不同的业务场景，我们应该做怎么样的调研？怎么决定选型？相关技术点又有哪些？</p>
<h2><a id="toc-b72" class="anchor" href="#toc-b72"></a>基础效果</h2>
<p>比如某个历史项目里，有下图这么一个编辑器。</p>
<p><img src="https://blog.pyzy.net/static/upload/20210513/6gp55IW9RERNCbHdOB80_lrT.png" alt="alt"></p>
<p>通过示意可以看到，大致功能：</p>
<ol>
<li>可以输入文本。</li>
<li>编辑器外点击表情图，插入类似“[微笑]”的方括号定界描述符。</li>
<li>编辑器外点击人名，可以插入“@xxx ”。</li>
</ol>
<h2><a id="toc-abf" class="anchor" href="#toc-abf"></a>历史问题</h2>
<p>通过读源码，发现这个项目编辑器的选型是基于开源的<a href="https://www.slatejs.org"><code>Slate</code></a>、以及依赖的一些模块 <code>slate-react</code>、<code>slate-plug-xxx</code>等实现的。</p>
<p>继续读代码发现，原来除了需要 <code>insertText</code> 到目标光标位置，并且输入“@xx”后，还需要按照光标在文档流中坐标位置，展示备选菜单。</p>
<p><img src="https://blog.pyzy.net/static/upload/20210513/pY8IHQnNGBznDfSegUKxedYT.png" alt="alt"></p>
<p>基于<code>Slate</code>的API和插件，确实可以很方便的实现插入内容到光标位置、光标坐标获取，但也很明显带来了新的问题：</p>
<ol>
<li>三方依赖包体量过大。</li>
<li>对中文输入法兼容不友好：会意外多出现类似“zhong&#39;wen&#39;xxx”的内容。</li>
<li>编辑器自带个别能力与上下文环境冲突，会导致JS崩溃、React渲染空白等。</li>
<li>...</li>
</ol>
<p>参考前文功能需求描述，这选型无疑是高射炮打蚊子了。
而且带来的问题确实不能容忍。</p>
<h2><a id="toc-9b2" class="anchor" href="#toc-9b2"></a>精简提效</h2>
<p>相对<code>Slate</code>来说更换成原生HTML元素 <code>&lt;textarea&gt;</code>，是更轻量级、更稳妥可靠的一个方案。这里就不具体赘述了，一个简化的实现效果可到 <a href="https://lab.pyzy.net/txt_editor.html">https://lab.pyzy.net/txt_editor.html</a> 查看。</p>
<h2><a id="toc-9af" class="anchor" href="#toc-9af"></a>隐含功能</h2>
<p>除了直观上看到的纯文本编辑，额外还有看不到的能力：</p>
<ol>
<li>被@人员列表数据。</li>
<li>草稿存储。</li>
<li>消息撤回重新编辑。</li>
</ol>
<p>除了(1)，只能用额外数据列表记录，2和3基于纯文本都比较好实现，2直接存储String到localStorage，3直接insertText(String)全文就可以了。</p>
<h2><a id="toc-d0f" class="anchor" href="#toc-d0f"></a>新需求-“图文混发”</h2>
<h3><a id="toc-164" class="anchor" href="#toc-164"></a>&quot;需求文档&quot;：</h3>
<p><img src="https://blog.pyzy.net/static/upload/20210513/2ZgYtWBEnCQjdI1wOlPhRl2w.png" alt="alt"></p>
<h3><a id="toc-bdd" class="anchor" href="#toc-bdd"></a>最终实现效果</h3>
<p><img src="https://blog.pyzy.net/static/upload/20210513/OTRW8aCCE6FmGmS_v1sNSnRt.png" alt="alt"></p>
<p>相对前面的纯文本编辑，区别也很明显，这里@、表情是所见即所得了，同时另外支持了插入图片（图文混排）。</p>
<h3><a id="toc-71f" class="anchor" href="#toc-71f"></a>最终达成功能概要</h3>
<p>通过前面截图，我们视觉能直观看到编辑时达成的功能，基本可归纳为：</p>
<ol>
<li>可以输入任意纯文本内容。</li>
<li>通过表情面板，点击插入所见即所得的图片表情图。</li>
<li>通过@面板，可以插入高亮的“@xxx ”HTML标记。</li>
<li>通过工具栏文件选择或粘贴截图来插入图片。</li>
</ol>
<p>而点击发送时，我们再将编辑器呈现的富文本，转换为符合发送需要的JSON描述符即可。比如：</p>
<ol>
<li>表情图转为&quot;[微笑]&quot;描述符;</li>
<li>高亮的“@xxx”转为纯本文，并提取被@人uid、uname，生成JSON数据;</li>
<li>将插入的图片上传服务端，得到文件唯一标识，放入JSON描述体中，并对应到文本中应该在的位置。</li>
<li>...</li>
</ol>
<h2><a id="toc-f76" class="anchor" href="#toc-f76"></a>“图文混发”关键技术环节</h2>
<ol>
<li>技术选型</li>
<li>insertText 改造。</li>
<li>富文本与正文数据互转。</li>
<li>光标位置。</li>
<li>草稿存储改造（含插入图片存储）。</li>
<li>兼容性。</li>
</ol>
<h3><a id="toc-15a" class="anchor" href="#toc-15a"></a>技术选型</h3>
<p>基于前文考虑，如果我们使用现成的开源编辑器，自然开发成本最低，但稳定性和维护成本无法估量。</p>
<p>这里，最终选择了基于原生的 <code>&lt;div contenteditable=&quot;true&quot; /&gt;</code> 为业务量身打造一个编辑器。</p>
<h3><a id="toc-3b9" class="anchor" href="#toc-3b9"></a>insert 方法改造</h3>
<p>在基于<code>&lt;textarea&gt;</code>实现编辑时，<code>insertText</code>的实现我们基于 <code>textareaHTMLElement.value</code>、<code>textareaHTMLElement.selectionStart</code>、<code>textareaHTMLElement.selectionEnd</code>便可以得到输入的完整文本内容、选中区间，然后进行替换、重写，基于<code>textareaHTMLElement.setSelectionRange(sta, end)</code>重新设置选中内容区间。（详见<a href="https://lab.pyzy.net/txt_editor.html">示例</a>源码 ）</p>
<p>基于<code>&lt;div contenteditable=&quot;true&quot; /&gt;</code>时，<code>insertText</code>的实现，需要转换为 insertNodes，对应到基础层各Nodes类型的insert方法代码片段如下：</p>
<pre><code class="hljs lang-javascript"><span class="hljs-comment">// 注意：下面代码仅是示意，包含缺少定义的环境变量</span>

<span class="hljs-comment">// 创建一个文本节点</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createTextNode</span>(<span class="hljs-params">text</span>) </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">document</span>.createTextNode(<span class="hljs-built_in">String</span>(text));
}
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createBrEl</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'br'</span>);
}
<span class="hljs-comment">// 创建一个 At El</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createAtEl</span>(<span class="hljs-params">uid, name</span>) </span>{
  <span class="hljs-keyword">const</span> atEl = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'span'</span>);
  atEl.className = atCls;
  atEl.innerText = <span class="hljs-string">'@'</span> + name + _atSuffix;
  setAttributes(atEl, {
    <span class="hljs-string">'data-uid'</span>: uid,
    <span class="hljs-string">'data-name'</span>: name,
    <span class="hljs-attr">contenteditable</span>: <span class="hljs-literal">false</span>, <span class="hljs-comment">// 不可编辑修改Span内容</span>
  });
  <span class="hljs-keyword">return</span> atEl;
}

<span class="hljs-comment">// 创建一个表情图 El</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createEmojiEl</span>(<span class="hljs-params">tips, src</span>) </span>{
  <span class="hljs-keyword">const</span> emojiEl = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'img'</span>);
  emojiEl.className = emojiCls;
  setAttributes(emojiEl, {
    src,
    <span class="hljs-attr">alt</span>: <span class="hljs-string">`[<span class="hljs-subst">${tips}</span>]`</span>,
    <span class="hljs-string">'data-tips'</span>: tips,
  });
  <span class="hljs-keyword">return</span> emojiEl;
}
<span class="hljs-comment">// 创建一个插图 El</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createImgEl</span>(<span class="hljs-params">file, width, height</span>) </span>{
  <span class="hljs-keyword">let</span> src = <span class="hljs-string">''</span>;
  <span class="hljs-keyword">let</span> title = <span class="hljs-string">''</span>;
  <span class="hljs-keyword">let</span> draftFid = <span class="hljs-string">''</span>;
  <span class="hljs-keyword">if</span> (file) {
    draftFid = getDraftFidByFile(file);
    title = file.name;
    <span class="hljs-keyword">try</span> {
      src = URL.createObjectURL(file);
    } <span class="hljs-keyword">catch</span> (err) {}
  }
  <span class="hljs-keyword">const</span> imgEl = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'img'</span>);
  imgEl.className = picCls;
  setAttributes(imgEl, {
    src,
    title,
    <span class="hljs-attr">alt</span>: <span class="hljs-string">'[图片]'</span>,
    <span class="hljs-string">'data-width'</span>: width,
    <span class="hljs-string">'data-height'</span>: height,
    <span class="hljs-string">'data-draft-fid'</span>: draftFid,
  });
  <span class="hljs-comment">/*
  这里的图片产品需求中要支持双击打开图片查看器预览，
  但用户是否预览、是否多图、什么时机预览不确定的，
  所以不可以 revokeObjectURL 掉。
  if (src) {
    imgEl.onload = () =&gt; {
      try {
        URL.revokeObjectURL(src);
      } catch (err) {}
    };
  }*/</span>
  <span class="hljs-keyword">return</span> imgEl;
}
</code></pre>
<h3><a id="toc-22f" class="anchor" href="#toc-22f"></a>富文本与正文数据互转</h3>
<p>首先，看信息字串转为富文本节点数组。</p>
<p>归根核心，也就是怎么将类似&quot;<code>测试文本 [微笑] @xxx 等等等</code>&quot;的消息字串，转为能使用insertNodes插入富文本编辑器的节点数组。</p>
<p>具体实现，依赖一个名为 <code>rich2nodeRegExp</code> 的正则变量，将文本中内容按定界符提取并替换为对应HTMLNode。</p>
<pre><code class="hljs lang-javascript"><span class="hljs-comment">// 注意：下面代码仅是示意，包含缺少定义的环境变量</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">richText2Nodes</span>(<span class="hljs-params">richText</span>) </span>{
    <span class="hljs-keyword">const</span> rtelNodes = [];
    <span class="hljs-keyword">const</span> rtelTexts = <span class="hljs-built_in">String</span>(richText)
      .replace(rich2nodeRegExp, (str, br, atName, emojiTips) =&gt; {
        <span class="hljs-keyword">if</span> (br) {
          rtelNodes.push(createBrEl());
          <span class="hljs-keyword">return</span> rtelSplitter;
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (atName) {
          <span class="hljs-keyword">const</span> atUser = at &amp;&amp; _.find(at, ({ name }) =&gt; name === atName);
          <span class="hljs-keyword">if</span> (atUser) {
            <span class="hljs-keyword">const</span> { uid, name } = atUser;
            rtelNodes.push(createAtEl(uid, name));
            <span class="hljs-keyword">return</span> rtelSplitter;
          }
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (emojiTips) {
          <span class="hljs-keyword">const</span> emojiItem = emojiMaps[emojiTips];
          <span class="hljs-keyword">if</span> (emojiItem) {
            rtelNodes.push(createEmojiEl(emojiTips, emojiItem.url));
            <span class="hljs-keyword">return</span> rtelSplitter;
          }
        }
        <span class="hljs-keyword">return</span> str;
      })
      .split(rtelSplitter);
    _.forEach(rtelTexts, (txt, i) =&gt; {
      rtelNodes.push(createTextNode(txt));
    });
    <span class="hljs-keyword">return</span> rtelNodes;
}
</code></pre>
<p>接下来，再看怎么将富文本编辑器内，用户输入的内容转为消息JSON数据。</p>
<pre><code class="hljs lang-javascript"><span class="hljs-comment">// 注意：下面代码仅是示意，包含缺少定义的环境变量</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">richTextNodes2MsgData</span>(<span class="hljs-params">richTextEl</span>) </span>{
  <span class="hljs-keyword">const</span> ret = {
    <span class="hljs-attr">txt</span>: <span class="hljs-string">''</span>, <span class="hljs-comment">// 用于纯文本展示需求</span>
    at: [], <span class="hljs-comment">// 用于记录at人员名单 [{ uid, name }]</span>
    msgs: [], <span class="hljs-comment">// 用于支持图文混排 [{ key: 'txt', val: '' }, { key: 'img', width: 123, height: 456, file: File }]</span>
  };
  <span class="hljs-keyword">if</span> (!_.get(richTextEl, <span class="hljs-string">'childNodes.length'</span>)) {
    <span class="hljs-comment">// 输入框是空的，直接返回空消息体内容</span>
    <span class="hljs-keyword">return</span> ret;
  }
  <span class="hljs-comment">/* 图、文（含At文本）、表情提取 sta */</span>
  <span class="hljs-keyword">const</span> picEls = richTextEl.getElementsByClassName(picCls);
  <span class="hljs-comment">// 生成图文混排中的图片定界符</span>
  <span class="hljs-keyword">const</span> picSplitter = <span class="hljs-string">`[IMG:<span class="hljs-subst">${getUuid()}</span>]`</span>;
  <span class="hljs-keyword">const</span> tmpEl = getTransCodeEl();
  <span class="hljs-comment">// 用定界符替换用户插入图片产生的DOM元素</span>
  tmpEl.innerHTML = richTextEl.innerHTML
    <span class="hljs-comment">// 去除用来便于光标移动的0宽字符</span>
    .replace(blankRegExp, <span class="hljs-string">''</span>)
    <span class="hljs-comment">// 插图转为定界符</span>
    .replace(picTagRegExp, picSplitter)
    <span class="hljs-comment">// 表情图转未文本标记符如“[微笑]”</span>
    .replace(emojiTagRegExp, (str) =&gt; (emojiTipsRegExp.exec(str) || <span class="hljs-string">''</span>)[<span class="hljs-number">1</span>]);

  <span class="hljs-keyword">const</span> { msgs } = ret;
  <span class="hljs-comment">// 格式化消息内容</span>
  <span class="hljs-keyword">const</span> txts = tmpEl.innerText.split(picSplitter);
  <span class="hljs-keyword">const</span> maxI = txts.length - <span class="hljs-number">1</span>;
  _.forEach(txts, (val, i) =&gt; {
    <span class="hljs-keyword">const</span> picEl = picEls[i];
    <span class="hljs-keyword">if</span> (val) {
      <span class="hljs-keyword">if</span> (i === <span class="hljs-number">0</span>) {
        <span class="hljs-comment">// 对第一条文本消息的前面换行符进行过滤; 不可以做trim处理</span>
        val = val.replace(<span class="hljs-regexp">/^[\r\n]+/</span>, <span class="hljs-string">''</span>);
      }
      <span class="hljs-keyword">if</span> (i === maxI &amp;&amp; !picEl) {
        <span class="hljs-comment">// 对最后一条文本消息的末尾换行符进行过滤; 不可以做trim处理</span>
        val = val.replace(<span class="hljs-regexp">/[\r\n]+$/</span>, <span class="hljs-string">''</span>);
      }
      <span class="hljs-keyword">if</span> (val) {
        ret.txt += val;
        msgs.push({ <span class="hljs-attr">key</span>: <span class="hljs-string">'txt'</span>, val });
      }
    }
    <span class="hljs-keyword">if</span> (picEl) {
      ret.txt += <span class="hljs-string">'[图片]'</span>;
      <span class="hljs-keyword">const</span> {
        <span class="hljs-string">'data-width'</span>: width,
        <span class="hljs-string">'data-height'</span>: height,
        <span class="hljs-string">'data-draft-fid'</span>: draftFid,
      } = getAttributes(picEl, [<span class="hljs-string">'data-width'</span>, <span class="hljs-string">'data-height'</span>, <span class="hljs-string">'data-draft-fid'</span>]);
      msgs.push({
        <span class="hljs-attr">key</span>: <span class="hljs-string">'img'</span>,
        height,
        width,
        <span class="hljs-attr">file</span>: getFileByDraftfId(draftFid),
      });
    }
  });
  <span class="hljs-comment">/* 图、文（含At文本）、表情提取 end */</span>

  <span class="hljs-comment">/* At列表提取 sta */</span>
  <span class="hljs-keyword">const</span> { at } = ret;
  <span class="hljs-keyword">const</span> atEls = richTextEl.getElementsByClassName(atCls);
  _.forEach(atEls, (el) =&gt; {
    <span class="hljs-keyword">const</span> { <span class="hljs-string">'data-uid'</span>: uid, <span class="hljs-string">'data-name'</span>: name } = getAttributes(el, [
      <span class="hljs-string">'data-uid'</span>,
      <span class="hljs-string">'data-name'</span>,
    ]);
    at.push({ uid, name });
  });
  <span class="hljs-comment">/* At列表提取 end */</span>
  tmpEl.innerHTML = <span class="hljs-string">''</span>;
  <span class="hljs-keyword">return</span> ret;
}
<span class="hljs-string">`
</span></code></pre>
<p>富文本内容转消息JSON，后便可以进行草稿存储、提交服务端了，大体格式如下：</p>
<pre><code class="hljs lang-javascript"> {
    <span class="hljs-attr">txt</span>: <span class="hljs-string">''</span>, <span class="hljs-comment">// 用于纯文本展示需求</span>
    at: [], <span class="hljs-comment">// 用于记录at人员名单 [{ uid, name }]</span>
    msgs: [], <span class="hljs-comment">// 用于支持图文混排 [{ key: 'txt', val: '' }, { key: 'img', width: 123, height: 456, file: File }]</span>
  }
</code></pre>
<h3><a id="toc-1ba" class="anchor" href="#toc-1ba"></a>光标位置读写</h3>
<p>这里按基础能力需要，也需要拆分多个核心基础方法。</p>
<p>首先，需要最基本的光标选取读写能力。</p>
<pre><code class="hljs lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSel</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (<span class="hljs-built_in">window</span>.getSelection &amp;&amp; <span class="hljs-built_in">window</span>.getSelection()) || {};
}

<span class="hljs-comment">// 更换选中内容</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">replaceRange</span>(<span class="hljs-params">opts</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> { staNode, staOffset, endNode, endOffset } = opts;
    <span class="hljs-keyword">if</span> (!staNode.parentNode || !endNode.parentNode) {
      <span class="hljs-keyword">return</span>;
    }
    <span class="hljs-keyword">const</span> ra = <span class="hljs-built_in">document</span>.createRange();
    ra.setStart(staNode, amendChildOffset(staNode, staOffset));
    ra.setEnd(endNode, amendChildOffset(endNode, endOffset));
    <span class="hljs-keyword">const</span> sel = getSel();
    sel.removeAllRanges();
    sel.addRange(ra);
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[Errror] replaceRange'</span>, opts, err);
  }
}
<span class="hljs-comment">// 修正子元素定位下标值</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">amendChildOffset</span>(<span class="hljs-params">pEl, childOffset</span>) </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">Math</span>.min(
    childOffset,
    _.get(pEl, <span class="hljs-string">'childNodes.length'</span>) || _.get(pEl, <span class="hljs-string">'length'</span>) || <span class="hljs-number">0</span>
  );
}
<span class="hljs-comment">// 移动光标到目标元素的第几个子内容后面</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">moveOffset</span>(<span class="hljs-params">targetNode, idx = <span class="hljs-number">0</span></span>) </span>{
  <span class="hljs-keyword">if</span> (!targetNode) <span class="hljs-keyword">return</span>;
  replaceRange({
    <span class="hljs-attr">staNode</span>: targetNode,
    <span class="hljs-attr">staOffset</span>: idx,
    <span class="hljs-attr">endNode</span>: targetNode,
    <span class="hljs-attr">endOffset</span>: idx,
  });
}
<span class="hljs-comment">// 移动光标到目标节点前后 @isAfter 是否定位到目标后面</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">moveOffset2Node</span>(<span class="hljs-params">nodeEl, isAfter = false</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> parentNd = nodeEl.parentNode;
    <span class="hljs-keyword">if</span> (parentNd) {
      <span class="hljs-keyword">const</span> idx = _.indexOf(parentNd.childNodes, nodeEl) + (isAfter ? <span class="hljs-number">1</span> : <span class="hljs-number">0</span>);
      moveOffset(parentNd, idx);
    }
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[Errror] moveOffset2Node：'</span>, nodeEl, isAfter, err);
  }
}
</code></pre>
<p>按光标所在位置获取左侧文本内容及文本节点对象。</p>
<pre><code class="hljs lang-javascript">getCursorLeftObj = <span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> {
  <span class="hljs-keyword">this</span>.focus(); <span class="hljs-comment">// 先保证光标在输入框里</span>
  <span class="hljs-keyword">const</span> sel = getSel();
  <span class="hljs-keyword">const</span> ret = { sel };
  <span class="hljs-keyword">const</span> { anchorNode, focusNode, anchorOffset, focusOffset } = sel;
  <span class="hljs-comment">// 光标不在一个独立的文本节点内，或者用户有在主动选取文本内容，则不做At文案提取</span>
  <span class="hljs-keyword">if</span> (
    focusNode &amp;&amp;
    anchorNode === focusNode &amp;&amp;
    isTxtNode(focusNode) &amp;&amp;
    !<span class="hljs-built_in">isNaN</span>(focusOffset) &amp;&amp;
    anchorOffset === focusOffset
  ) {
    <span class="hljs-keyword">const</span> { data } = focusNode;
    <span class="hljs-keyword">if</span> (data) {
      ret.node = focusNode;
      ret.offset = focusOffset;
      <span class="hljs-comment">// 光标左侧文本内容</span>
      ret.txt = data.substring(<span class="hljs-number">0</span>, focusOffset);
    }
  }
  <span class="hljs-keyword">return</span> ret;
};
</code></pre>
<p>获取光标在文档流中的坐标位置。</p>
<pre><code class="hljs lang-javascript">getRangePosition = <span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> {
  <span class="hljs-keyword">this</span>.focus(); <span class="hljs-comment">// 先保证光标在输入框里</span>
  <span class="hljs-keyword">const</span> sel = getSel();
  <span class="hljs-keyword">let</span> ret = {};
  <span class="hljs-keyword">if</span> (sel.getRangeAt) {
    <span class="hljs-keyword">const</span> ra = sel.getRangeAt(<span class="hljs-number">0</span>);
    <span class="hljs-keyword">if</span> (ra &amp;&amp; ra.getBoundingClientRect) {
      ret = ra.getBoundingClientRect();
    }
  }
  <span class="hljs-keyword">return</span> ret;
};
</code></pre>
<h3><a id="toc-c09" class="anchor" href="#toc-c09"></a>编辑器内容插入的入口方法</h3>
<p>结合上文的各种方法，还需要再实现一个可以一次批量插入多个节点到编辑器的  insertNodes 方法，作为内容插入的总入口：</p>
<pre><code class="hljs lang-javascript">
  <span class="hljs-comment">// 插入多个HTML节点元素到光标区域</span>
  insertNodes = <span class="hljs-function">(<span class="hljs-params">nodes</span>) =&gt;</span> {
    <span class="hljs-comment">// this.focus(); // 让编辑器输入区为焦点获取状态</span>
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> sel = getSel();
      <span class="hljs-keyword">const</span> richEl = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'richTextEl'</span>);
      <span class="hljs-keyword">const</span> fnode = sel.focusNode;
      <span class="hljs-keyword">if</span> (!fnode || (fnode !== richEl &amp;&amp; !richEl.contains(fnode))) {
        <span class="hljs-keyword">return</span>; <span class="hljs-comment">// 防止内容插入到编辑器区域外</span>
      }
      <span class="hljs-keyword">let</span> ra = <span class="hljs-literal">null</span>;
      <span class="hljs-keyword">if</span> (sel.rangeCount) {
        ra = sel.getRangeAt(<span class="hljs-number">0</span>);
        ra.deleteContents();
      } <span class="hljs-keyword">else</span> {
        ra = <span class="hljs-built_in">document</span>.createRange();
        sel.removeAllRanges();
        sel.addRange(ra);
      }
      <span class="hljs-keyword">const</span> nodeCount = _.get(nodes, <span class="hljs-string">'length'</span>);
      <span class="hljs-keyword">if</span> (nodeCount) {
        <span class="hljs-keyword">const</span> fragment = <span class="hljs-built_in">document</span>.createDocumentFragment();
        <span class="hljs-keyword">const</span> atEls = [];
        _.forEach(nodes, (el, i) =&gt; {
          isAtNode(el) &amp;&amp; atEls.push(el);
          fragment.appendChild(el);
        });
        ra.insertNode(fragment); <span class="hljs-comment">// 关键就是这里</span>

        <span class="hljs-comment">// autoAddBlank(atEls);  // 为Element补全光标占位符</span>
        <span class="hljs-comment">// 光标到最后一个元素后</span>
        moveOffset2Node(nodes[nodeCount - <span class="hljs-number">1</span>], <span class="hljs-literal">true</span>);
      }
    } <span class="hljs-keyword">catch</span> (err) {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'insertNode Err：可能是浏览器环境兼容问题。'</span>, err);
    }
    <span class="hljs-comment">// 触发变更事件</span>
    <span class="hljs-comment">// this.onChangeHandler();</span>
  };
</code></pre>
<h3><a id="toc-27e" class="anchor" href="#toc-27e"></a>草稿存储改造（含插入图片存储）</h3>
<p>原有草稿存储，是基于本地存储的 localStorage.set localStorage.get；如果要进行图片文件存储，肯定是不合适的，所以这里又引入了 indexedDB，写了一个 ttIndexedDb 操作对象。</p>
<pre><code class="hljs lang-javascript"><span class="hljs-keyword">const</span> ttIndexedDb = <span class="hljs-function">(<span class="hljs-params">(win</span>) =&gt;</span> {
  <span class="hljs-keyword">let</span> { _ttIndexedDb } = win;
  <span class="hljs-keyword">if</span> (_ttIndexedDb) <span class="hljs-keyword">return</span> _ttIndexedDb;

  win._hzj_idx_db_log = <span class="hljs-number">0</span>;

  idxDbPolyfill(win);

  <span class="hljs-comment">// 打开并连接的 database 对象</span>
  <span class="hljs-keyword">let</span> db = <span class="hljs-literal">null</span>;

  <span class="hljs-comment">// 草稿文件存储对象名称</span>
  <span class="hljs-keyword">const</span> draftFilesStoreName = <span class="hljs-string">'draft_files'</span>;
  _ttIndexedDb = win._ttIndexedDb = {
    <span class="hljs-comment">/* 读草稿文件从本地数据存储
     * @sid 会话ID
     * 返回 Promise
     */</span>
    <span class="hljs-keyword">async</span> readDraftFiles(sid) {
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-keyword">this</span>.readwriteStore({
        <span class="hljs-attr">storeName</span>: draftFilesStoreName,
        <span class="hljs-attr">action</span>: <span class="hljs-string">'get'</span>,
        <span class="hljs-attr">data</span>: sid,
      });
    },
    <span class="hljs-comment">/* 写草稿文件到本地数据存储
     * @data 草稿数据，示例： {
        sid: '123', 
        files: {
         'b456': new File(['123333'], 'test33.txt'),
         'b4563': new File(['123333'], 'test33.txt'),
        }
      }
     * 返回 Promise
     */</span>
    <span class="hljs-keyword">async</span> writeDraftFiles(data) {
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-keyword">this</span>.readwriteStore({
        <span class="hljs-attr">storeName</span>: draftFilesStoreName,
        <span class="hljs-attr">action</span>: <span class="hljs-string">'put'</span>,
        data,
      });
      <span class="hljs-comment">// if (req.error) console.log('写草稿文件失败！');</span>
    },
    <span class="hljs-comment">/* 删除草稿文件从本地数据存储
     * @sid 会话ID
     * 返回 Promise
     */</span>
    <span class="hljs-keyword">async</span> delDraftFiles(sid) {
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-keyword">this</span>.readwriteStore({
        <span class="hljs-attr">storeName</span>: draftFilesStoreName,
        <span class="hljs-attr">action</span>: <span class="hljs-string">'delete'</span>,
        <span class="hljs-attr">data</span>: sid,
      });
    },
    <span class="hljs-comment">/* 按名称、行为、参数，读写存储对象中对应数据
     * @storeName 存储对象名称
     * @action    操作行为，可选值：get、put、...
     * @data    行为对应的参数们
     * 返回 Promise
     */</span>
    readwriteStore({ storeName, action = <span class="hljs-string">'get'</span>, data }) {
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-keyword">async</span> (resolve) =&gt; {
        <span class="hljs-keyword">const</span> isRead = action === <span class="hljs-string">'get'</span>;
        <span class="hljs-keyword">const</span> { db, error } = <span class="hljs-keyword">await</span> <span class="hljs-keyword">this</span>.openDb();
        <span class="hljs-keyword">if</span> (error) {
          <span class="hljs-keyword">return</span> resolve({ error });
        }
        <span class="hljs-keyword">try</span> {
          <span class="hljs-keyword">const</span> store = db
            .transaction(storeName, isRead ? <span class="hljs-string">'readonly'</span> : <span class="hljs-string">'readwrite'</span>)
            .objectStore(storeName);
          <span class="hljs-keyword">const</span> req = store[action](data);
          req.onsuccess = <span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> resolve(req);
          req.onerror = <span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> resolve({ error });
        } <span class="hljs-keyword">catch</span> (error) {
          win._hzj_idx_db_log &amp;&amp;
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[Error] indexedDB.readwriteStore'</span>, error);
          resolve({ error });
        }
      });
    },
    <span class="hljs-comment">// 打开数据库</span>
    openDb() {
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve</span>) =&gt;</span> {
        <span class="hljs-keyword">if</span> (db) {
          resolve({ db });
          <span class="hljs-keyword">return</span>;
        }
        <span class="hljs-keyword">const</span> { indexedDB } = win;
        <span class="hljs-keyword">if</span> (!indexedDB) {
          win._hzj_idx_db_log &amp;&amp;
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[OpenError] Does not support with indexedDB!'</span>);
          resolve({ <span class="hljs-attr">error</span>: <span class="hljs-string">'[OpenError] Does not support with indexedDB!'</span> });
        }
        <span class="hljs-keyword">const</span> req = indexedDB.open(<span class="hljs-string">'TX_IM'</span>, <span class="hljs-number">1</span>);
        req.onupgradeneeded = <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
          win._hzj_idx_db_log &amp;&amp;
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[onupgradeneeded] indexedDB.open'</span>, e);
          <span class="hljs-comment">// 创建草稿文件存储对象</span>
          db = e.target.result;
          <span class="hljs-keyword">const</span> store = db.createObjectStore(
            draftFilesStoreName, <span class="hljs-comment">// 草稿文件存储对象</span>
            <span class="hljs-comment">// 按会话ID作为主键</span>
            { <span class="hljs-attr">keyPath</span>: <span class="hljs-string">'sid'</span>, <span class="hljs-attr">autoIncrement</span>: <span class="hljs-literal">false</span> }
          );
          <span class="hljs-comment">// 支持按会话ID去索引</span>
          store.createIndex(<span class="hljs-string">'sid'</span>, <span class="hljs-string">'sid'</span>, { <span class="hljs-attr">unique</span>: <span class="hljs-literal">true</span> });
        };
        req.onerror = <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
          db = <span class="hljs-literal">null</span>;
          win._hzj_idx_db_log &amp;&amp; <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[Error] indexedDB.open'</span>, e);
          resolve({ <span class="hljs-attr">error</span>: <span class="hljs-string">'[OpenError]'</span> + req.error });
        };
        req.onsuccess = <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
          win._hzj_idx_db_log &amp;&amp; <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[onsuccess] indexedDB.open'</span>, e);
          db = e.target.result;
          db.onerror = <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
            win._hzj_idx_db_log &amp;&amp;
              <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[onerror] creating/accessing IndexedDB database'</span>, e);
          };
          db.onclose = <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
            win._hzj_idx_db_log &amp;&amp; <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[onclose] indexedDB.open'</span>, e);
            db = <span class="hljs-literal">null</span>;
          };
          <span class="hljs-comment">// 数据库版本变化，则关闭之（下次调用再开启）</span>
          db.onversionchange = <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
            win._hzj_idx_db_log &amp;&amp;
              <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[onversionchange] indexedDB.open'</span>, e);
            db.close();
          };
          resolve({ db });
        };
      });
    },
  };
  <span class="hljs-keyword">return</span> _ttIndexedDb;
})(<span class="hljs-built_in">window</span>);

<span class="hljs-comment">// API兼容抹平问题</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">idxDbPolyfill</span>(<span class="hljs-params">win</span>) </span>{
  <span class="hljs-keyword">if</span> (!win.indexedDB) {
    win.indexedDB =
      win.webkitIndexedDB ||
      win.mozIndexedDB ||
      win.OIndexedDB ||
      win.msIndexedDB;
  }
  <span class="hljs-keyword">if</span> (
    win.indexedDB &amp;&amp;
    IDBObjectStore &amp;&amp;
    IDBObjectStore.prototype &amp;&amp;
    <span class="hljs-keyword">typeof</span> IDBObjectStore.prototype.getAll !== <span class="hljs-string">'function'</span>
  ) {
    IDBObjectStore.prototype.getAll = <span class="hljs-function">(<span class="hljs-params">...args</span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> ret = {};
      <span class="hljs-keyword">const</span> req = <span class="hljs-keyword">this</span>.openCursor(...args);
      req.onerror = <span class="hljs-function">(<span class="hljs-params">...errArgs</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> { onerror } = ret;
        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> onerror == <span class="hljs-string">'function'</span>) {
          onerror(...errArgs);
        }
      };
      <span class="hljs-keyword">const</span> retArr = [];
      req.onsuccess = <span class="hljs-function">(<span class="hljs-params">...sucArgs</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> { onsuccess } = ret;
        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> onsuccess == <span class="hljs-string">'function'</span>) {
          <span class="hljs-keyword">const</span> { target } = sucArgs[<span class="hljs-number">0</span>] || {};
          <span class="hljs-keyword">const</span> cursor = target.result;
          <span class="hljs-keyword">if</span> (cursor) {
            retArr.push(cursor.value);
            cursor.continue();
          } <span class="hljs-keyword">else</span> {
            ret.result = retArr;
            target.result = retArr;
            onsuccess(...sucArgs);
          }
        }
      };
      <span class="hljs-keyword">return</span> ret;
    };
  }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ttIndexedDb;
</code></pre>
<h3><a id="toc-c11" class="anchor" href="#toc-c11"></a>兼容性</h3>
<p>细节问题很多，先记录两个。</p>
<p>在Safari浏览器端，contenteditable的元素可能还是无法获取光标的不可编辑状态，一定记得添加CSS：</p>
<pre><code class="hljs lang-css"><span class="hljs-selector-attr">[contenteditable]</span>{
    <span class="hljs-attribute">-webkit-user-select</span>: text;
    <span class="hljs-attribute">user-select</span>: text;
}
</code></pre>
<p>兼容性方面还要特别注意，focus到DIV并不能保证保持之前的光标选中状态，所以这里要自己做光标状态记录、和恢复。</p>
<pre><code class="hljs lang-javascript">onSelectionChange = <span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> {
    <span class="hljs-comment">// 进一步保证focus方法无效的问题</span>
    <span class="hljs-keyword">const</span> { richTextEl } = <span class="hljs-keyword">this</span>.refs;
    <span class="hljs-keyword">const</span> {
      <span class="hljs-attr">focusNode</span>: staNode,
      <span class="hljs-attr">focusOffset</span>: staOffset,
      <span class="hljs-attr">anchorNode</span>: endNode,
      <span class="hljs-attr">anchorOffset</span>: endOffset,
    } = getSel();
    <span class="hljs-keyword">if</span> (
      (endNode === richTextEl || richTextEl.contains(endNode)) &amp;&amp;
      (staNode === richTextEl || richTextEl.contains(staNode))
    ) {
      <span class="hljs-keyword">this</span>._selCache = { staNode, staOffset, endNode, endOffset };
      <span class="hljs-comment">// console.log('记录光标状态！', this._selCache)</span>
    }
  };
  <span class="hljs-comment">// 设置输入框为焦点元素</span>
  focus = <span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> {
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">this</span>.state.draftReading) <span class="hljs-keyword">return</span>;
    <span class="hljs-keyword">const</span> { richTextEl } = <span class="hljs-keyword">this</span>.refs;
    <span class="hljs-comment">// 恢复光标选中状态</span>
    <span class="hljs-keyword">const</span> { focusNode } = getSel();
    <span class="hljs-keyword">if</span> (
      !focusNode ||
      (focusNode !== richTextEl &amp;&amp; !richTextEl.contains(focusNode))
    ) {
      <span class="hljs-keyword">const</span> staOffset = richTextEl.childNodes.length;
      <span class="hljs-keyword">const</span> rangeOpts = <span class="hljs-keyword">this</span>._selCache || {
        <span class="hljs-attr">staNode</span>: richTextEl,
        staOffset,
        <span class="hljs-attr">endNode</span>: richTextEl,
        <span class="hljs-attr">endOffset</span>: staOffset,
      };
      <span class="hljs-comment">// console.log('恢复光标状态', rangeOpts)</span>
      replaceRange(rangeOpts);
    }
    richTextEl.focus();
  };
</code></pre>
<h3><a id="toc-bc6" class="anchor" href="#toc-bc6"></a>特殊内容相关</h3>
<p>上文中反复有用到一个  <code>getTransCodeEl()</code>方法，该方法完整代码如下：</p>
<pre><code class="hljs lang-javascript"><span class="hljs-comment">// 用临时容器来解析富文本消息内容为JSON数据体</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getTransCodeEl</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> tmpId = <span class="hljs-string">'rich_text_trans'</span>;
  <span class="hljs-keyword">let</span> tmpEl = <span class="hljs-built_in">document</span>.getElementById(tmpId);
  <span class="hljs-keyword">if</span> (!tmpEl) {
    tmpEl = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'div'</span>);
    tmpEl.id = tmpId;
    tmpEl.setAttribute(<span class="hljs-string">'desc'</span>, <span class="hljs-string">'富文本与消息数据互转专用'</span>);
    <span class="hljs-comment">// 必须添加到文档流才能保证正常得到换行符</span>
    <span class="hljs-built_in">document</span>.body.appendChild(tmpEl);
  }
  <span class="hljs-keyword">return</span> tmpEl;
}
</code></pre><p>对应CSS如下：</p>
<pre><code class="hljs lang-scss"><span class="hljs-selector-id">#rich_text_trans</span> {
    <span class="hljs-attribute">position</span>: absolute;
    <span class="hljs-attribute">top</span>: -<span class="hljs-number">9px</span>;
    <span class="hljs-attribute">left</span>: -<span class="hljs-number">9px</span>;
    <span class="hljs-attribute">overflow</span>: hidden;
    <span class="hljs-attribute">width</span>: <span class="hljs-number">1px</span>;
    <span class="hljs-attribute">height</span>: <span class="hljs-number">1px</span>;
    <span class="hljs-attribute">white-space</span>: pre; <span class="hljs-comment">// 保证空格、制表符正常渲染展示并能通过innerText 获取</span>
}
</code></pre><p>所以不要忘了，基于 <code>contentEditable</code> 实现的编辑输入框，也要按自己需求添加 <code>white-space: pre-wrap;</code>或其他相同作用的设置。</p>
<h2><a id="toc-7a0" class="anchor" href="#toc-7a0"></a>后记</h2>
<p>相关细节点还有很多，暂不逐一赘述了，以上先仅做流水记录，如果有所帮助、或拓展问题，欢迎沟通。</p>

            ]]></description>
            <pubDate>Thu, 13 May 2021 04:12:25 GMT</pubDate>
            <guid>http://blog.pyzy.net/post/editor.html</guid>
        </item>
        
        <item>
            <title>我经常和面试者聊的一个题目</title>
            <link>http://blog.pyzy.net/post/itker.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-d4a">本文背景</a></li>
<li><a href="#toc-803">面试原题描述</a></li>
<li><a href="#toc-47a">粗略解答思路</a></li>
<li><a href="#toc-482">一点面试的心得感受</a></li>
<li><a href="#toc-faf">学霸式解题方法思路</a></li>
<li><a href="#toc-030">逻辑推理式解题思路之一</a></li>
<li><a href="#toc-a4c">必要参数值获取</a><ul>
<li><a href="#toc-432">事件处理及对应值获取</a></li>
<li><a href="#toc-8c9">DOM属性读取</a></li>
<li><a href="#toc-9b1">延展讨论</a></li>
</ul>
</li>
<li><a href="#toc-ae2">收个尾</a></li>
</ul>
</div><h2><a id="toc-d4a" class="anchor" href="#toc-d4a"></a>本文背景</h2>
<p>作为一个Web前端从业者，资深B/S架构的应用开发，通常我们负责的内容都是侧重的浏览器端比较多，即便目前Vue、React等各种框架库流行的年代，核心实现也依然是JS API操作DOM；如果资历更深一些的同学，还会熟知jQuery，更是因为各浏览器API不统一作为其解决的问题痛点。</p>
<p>在长期担任Web前端面试官的过程中，我归纳沉淀了一些有趣的面试题目，今天来聊其中一个与DOM操作相关的题，可以用于考察一个前端工程师的逻辑条理性、问题分析解决能力、纯Web前端（浏览器端）JS技术基础情况等。</p>
<h2><a id="toc-803" class="anchor" href="#toc-803"></a>面试原题描述</h2>
<p>如图，红色矩形是网页中的一个DOM元素（比如是个普通DIV）。</p>
<p>需求：当用户鼠标在该DOM元素上移动时，判定光标相对于灰色对角线所处位置状态（左上、右下、刚好在线上）。</p>
<p><img src="https://blog.pyzy.net/static/upload/20210331/Yp5Cx1j3qSAdP887HsVqkrxG.png" alt="alt"></p>
<h2><a id="toc-47a" class="anchor" href="#toc-47a"></a>粗略解答思路</h2>
<p>相信你一定已经看懂是个啥需求，需要做些啥了吧？</p>
<p>所以不需要赘述了，直接上解法：！！！</p>
<ol>
<li><p>首先，我们需要确定判定方法，以及需要传入的参数。</p>
</li>
<li><p>然后，再通过DOM行为监听、事件&amp;DOM属性读取，得到参数具体数值，传入上面实现的方法。</p>
</li>
</ol>
<p>很简单对不对？让我想到了经典的“怎么画好一匹马”。</p>
<p><img src="https://blog.pyzy.net/static/upload/20210331/HfuroHYQCz3GH1Flzrm3GB0Y.jpg" alt="alt"></p>
<p>别急，具体处理方式容我再慢慢道来。</p>
<h2><a id="toc-482" class="anchor" href="#toc-482"></a>一点面试的心得感受</h2>
<p>被我面试时聊过这个问题的人，不上几千肯定也过几百了。</p>
<p>所以，技术问题之外，也会有一些心得感悟。</p>
<p>比如我经常对新参加面试工作的同事说的一句话：“<strong>切记：面试不是拿你自己准备好的、或者熟悉的问题，创造出一个不公平的沟通场景，然后再跟被面试者比谁更聪明；面试官的职责是通过针对特定话题的沟通，来检验其过往经历的真实性、判定这个人特定方向的能力边界。</strong>”</p>
<p>如果一个被面试者，第一次遇到这个非纯知识考查的怪异问题，还能做到冷静思考、清晰表述思路，那么我们完全可以判定这个人的基本素质：临场应变、抽象变通都不错。</p>
<p>如果接下来还能提供代码实现过程的表述，那更是这个方向上，问题分析解决能力、编程经验丰富了。</p>
<p>能达到上述表现的，对心理抗压、临场应变、相关技能储备是个不小的挑战，往往是凤毛麟角。</p>
<p>而在我面试他人的经历中，还有两种非常有趣的极端情况会经常遇到：</p>
<ol>
<li>看完不假思索、或稍微思考一会，直接选择放弃的。</li>
<li>缺乏思考或没有清晰的思路，强拉硬扯一通，然后提供一些牵强附会的结论。</li>
</ol>
<p>当我作为面试官遇到上述极端情形时的感受往往是：</p>
<ol>
<li>是不是没太大必要继续浪费时间了。</li>
<li>这个同学任用可一定得慎重。</li>
</ol>
<p>当然，感受归感受，现实里面我并不会这么武断的定下结论。此时需要再回顾一下前文加粗的那点感想。</p>
<p>如果一个人的临场应变能力不是非常优秀，也不一定就是能力问题。</p>
<p>谁也难免会有紧张、脑筋卡壳的时候。我自己就经常这样啊 ----- 往往晚上睡觉时趟在床上，回想一天的过往，“嘿，我那个想法、那句话，简直就是SB至极”。</p>
<p>所以遇到上述两种极端情况，换到应聘者角色又或者是这样呢：</p>
<ol>
<li>问我这些？简直是羞辱，我不屑回答。</li>
<li>怎么办，怎么办，好紧张，可我不能不要面子啊：“巴拉巴拉巴拉...”</li>
</ol>
<p>所以如果我作为面试官遇到不太能给出思路的同学，会试图引导一下，强调“<strong>面试不一定非得纯问答题，也是个相互沟通、相互学习的过程，想到啥都可以说说看</strong>”。</p>
<p>而作为面试官通过进一步沟通，也可以更可靠、清晰的确认一下对被面试者能力范围的判定。</p>
<h2><a id="toc-faf" class="anchor" href="#toc-faf"></a>学霸式解题方法思路</h2>
<p>如果面试中遇到数学基础不错的学霸，往往会直接甩出<code>向量</code>方案。</p>
<p>像下图三种状态代表性的点 <code>P1</code>、<code>P2</code>、<code>P3</code>，求其点坐标到对角线的向量值，根据正、负、零自然也就映射到要判定的状态了。</p>
<p><img src="https://blog.pyzy.net/static/upload/20210331/B68lDo8c0QcdttuxVBK_UHZ9.png" alt="alt"></p>
<p>但是，像我这种连“<code>向量：具有大小和方向的量</code>”的基础概念都忘光光的学渣，肯定不会啊。</p>
<p>所以引导他人解题时候，更是不敢聊<code>向量</code>这个话题了，我经常用逻辑推理的方式。</p>
<h2><a id="toc-030" class="anchor" href="#toc-030"></a>逻辑推理式解题思路之一</h2>
<p>毕竟是引导别人，我也要面子嘛：通常会先建议再想一想、确定一下参照坐标系、画一画辅助线试试。</p>
<p>比如，我们假设任意光标点为<code>P1</code>，DOM元素左下角为二维坐标系原点<code>P0</code>呢？有没有想到啥思路？</p>
<p>还没有思路的话，将<code>P1-P0</code>连线呢？跟对角线比较是啥关系，能不能映射到题目需求状态上？</p>
<p>对嘛，斜线<code>P0-P1</code>与底边的夹角如果大于、等于、小于对角线与底边或左边的夹角，不就判定出来了。</p>
<p>不过要计算角的大小好像也会很麻烦（我三角函数都忘光光了，太难了、不会啊）....</p>
<p>其实，刚刚我们已经不知不解决的假定了底边是x轴、左边是y轴（原点<code>P0</code>嚒 ლ(′◉❥◉｀ლ)），也就形成了一个二维坐标系。</p>
<p>在这个坐标系里，<code>P1</code>到左边的距离X、到底边的距离Y，跟矩形宽度boxWidth、boxHeight的比例关系刚好也可以映射到题目需求的三个位置状态上诶!!</p>
<p>即： </p>
<ol>
<li><code>X/Y === boxWidth/boxHeight</code> 那么点在对角线上。</li>
<li><code>X/Y &lt; boxWidth/boxHeight</code> 那么点在对角线左上。</li>
<li><code>X/Y &gt; boxWidth/boxHeight</code> 那么点在对角线右下。</li>
</ol>
<p><img src="https://blog.pyzy.net/static/upload/20210331/q_yeqiUX2xf7qIJdI1WURs3S.png" alt="alt"></p>
<p>到这里判定方法确定了，剩下就是怎么去获取这些参数值了。</p>
<h2><a id="toc-a4c" class="anchor" href="#toc-a4c"></a>必要参数值获取</h2>
<p>现在我们需要获取4个数值：X、Y、boxWidth、boxHeight。</p>
<p>我就知道，你一定会觉得很简单，没错，这是算是基本JS API基础了。</p>
<h3><a id="toc-432" class="anchor" href="#toc-432"></a>事件处理及对应值获取</h3>
<p>回到题目“当用户鼠标在该DOM元素上移动时”，不就是监听鼠标事件、然后取相关对象的目标属性值么，这有啥难点？</p>
<p>我也一直这么认为的，但经过那么多次面试，发现竟然很多人都不知道（╮(╯▽╰)╭），能通过筛选的怎么也是有几年前端工作经验的呢。</p>
<p>遇到过：“不知道”、 “<code>onMouseover</code>”、“<code>onTouch</code>”、“o(╯□╰)o”...</p>
<p>确定了事件监听方式，那么我们通过事件中的哪个对象啥属性获取X、Y呢？</p>
<p>曾经收到过的答案们（只拿X值关联属性示例）：不知道、div.x、event.x、evnet.left、event.offsetX、event.clientX、event.pageX、... ??? </p>
<p>上面这些肯定有错误的或者存在问题的，聪明的你，一定想到了正确的方案了。</p>
<p>你拿到的值是相对哪个坐标系的？如果得到的是 pageX值，是不是还得换算一下，又进一步依赖哪个属性怎么获取？</p>
<h3><a id="toc-8c9" class="anchor" href="#toc-8c9"></a>DOM属性读取</h3>
<p>X、Y坐标值能取到了，那么boxWidth、boxHeight怎么取呢？</p>
<p>曾经收到的答案们（只拿宽度值关联属性示例）：不知道、div.width、div.style.width、div.contentWidth、div.getAttribute(&#39;xxx&#39;)、div.offsetWidth、div.outerWidth、div.innerWidth、div.getBoundingClientRect().width....</p>
<p>上面这些还是存在错误的，聪明的你，又一定想到了正确的方案了。~O(∩_∩)O~</p>
<h3><a id="toc-9b1" class="anchor" href="#toc-9b1"></a>延展讨论</h3>
<p>“既然聊到事件了，如果我页面里有好多元素，还存在懒加载动态插入的，都想达成这个题目的需求，事件绑定有没有一次绑定也都能生效的方案嘞？”、“是不是有个啥事件委托、事件代理？”</p>
<p>“事件可以自定义么？&quot;</p>
<p>&quot;如果我在JS代码里悄悄的自动触发一个元素的事件可以么，会不会有啥问题？”</p>
<p>“诶，上面你好像聊到了 div.style.width、div.offsetWidth，这俩货有啥区别啊？”、“浏览器里是不是有个盒模型的说法？”</p>
<p>“如果鼠标移动频度很高，会不会有性能问题？”、“可以怎么优化一下吗？”。</p>
<p>“如果要在DOM元素里绘制这个斜对角线，该怎么画？”</p>
<p>&quot;你刚说用两个DOM元素分别实现上下三角区域，再绑定onClick判断？&quot; “那这俩三角区域CSS咋写出来？”</p>
<h2><a id="toc-ae2" class="anchor" href="#toc-ae2"></a>收个尾</h2>
<p>今天先想到啥写啥了，回头再想到啥再补充。</p>
<p>可能有同学会非常纳闷，“怎么会有这么个题目，前端工程师怎么可能用到？”。</p>
<p>首先这个问题，来自我11年前的一个真实项目经历中的产品需求：</p>
<p><img src="https://blog.pyzy.net/static/upload/20210331/GA1Cths7jAQdyfB0WwYjmvFN.png" alt="alt"></p>
<p>如上图：“当某个WEB应用启用精简排版模式时，将第二行中俩功能按钮合并成第一行的那一个，按照点击位置判定并执行为独立按钮一样的目标行为。”</p>
<p>再后来，稍微了解了一点计算机图形学三角剖分法、纹理贴片，我勒个乖乖，全是三角形，太吓人了，果断放弃。</p>
<p>ﾚ(ﾟ∀ﾟ;)ﾍ=3=3=3</p>
<p><img src="https://blog.pyzy.net/static/upload/20210331/KEUam4fhPYxZrb1XxOYsE1pe.png" width="200"></p>

            ]]></description>
            <pubDate>Wed, 31 Mar 2021 16:07:17 GMT</pubDate>
            <guid>http://blog.pyzy.net/post/itker.html</guid>
        </item>
        
        <item>
            <title>为基于Electron开发的Mac端本地应用启用别名</title>
            <link>http://blog.pyzy.net/post/electron-app-display-name-mac.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-ed9">背景交代</a></li>
<li><a href="#toc-de8">解决方案</a></li>
<li><a href="#toc-696">具体代码</a></li>
<li><a href="#toc-aab">补充配置</a></li>
<li><a href="#toc-7a0">后记</a></li>
</ul>
</div><h2><a id="toc-ed9" class="anchor" href="#toc-ed9"></a>背景交代</h2>
<p>最近在做的一个基于Electron开发的Mac端项目，要进行产品更名。</p>
<p><img src="https://blog.pyzy.net/static/upload/20210220/KjWzipaJ9bcmIdOjCW_GJLCz.png" alt="alt"></p>
<p>但是如果直接修改编译打包后的应用名称，问题会比较大。</p>
<p><img src="https://blog.pyzy.net/static/upload/20210219/xO78UIrYqJbzsoQeD5A6zSry.png" alt="alt"></p>
<p>首先，类似上图里所有缓存资源、以及涉及鉴权的各种Cookie&amp;Storage ，都是存放在类似<code>/Users/huzunjie/Library/Application Support/旧名称</code>目录下的。</p>
<p>其次，通过<code>旧名称.dmg</code>安装到启动台中的应用，对应物理目录为<code>/Applications/旧名称.app</code>。</p>
<p>如果直接对应用打包时更名，缓存必然全部失效，也会导致<code>/Applications/</code>目录下同时存在新旧2个不同名称的APP。</p>
<p>这显然不是想看到的结果。</p>
<h2><a id="toc-de8" class="anchor" href="#toc-de8"></a>解决方案</h2>
<p>通过<a href="https://medium.com/@bevins/how-to-localize-your-i-watch-mac-os-bundle-display-name-2a76dfb49005">查询资料</a>，我们发现实际MacOS是允许在不影响原<code>/Applications/旧名称.app</code>物理路径的基础上，给应用设置一系列语言包，用于告知MacOS的文件系统及启动台等地方，按本地化语言环境展示对应名字。</p>
<p>这么以来也就找到了一个“为编译打包<code>旧名称.app</code>中增加语言包来配置别名”的解决方案。</p>
<p>我是基于 <code>electron-build</code>进行编译打包的，所以按上面这个思路，给<code>package.json</code>增加语言配置，并另外基于<code>electron-build</code>的<code>afterPack Hook</code>实现了个补充语言包的脚本。</p>
<h2><a id="toc-696" class="anchor" href="#toc-696"></a>具体代码</h2>
<p>首先 <code>package.json</code> 中要添加 <code>electronLanguagesInfoPlistStrings</code> 配置，及钩子脚本<code>afterPack</code>、并在<code>extendInfo</code>中添加<code>LSHasLocalizedDisplayName</code>字段:</p>
<pre><code class="hljs lang-clojure">{
  <span class="hljs-string">"name"</span>: <span class="hljs-string">"my-app-name"</span>,
  <span class="hljs-string">"productName"</span>: <span class="hljs-string">"旧名称"</span>,
  ....
  <span class="hljs-string">"electronLanguagesInfoPlistStrings"</span>: {
    <span class="hljs-string">"en"</span>: {
      <span class="hljs-string">"CFBundleDisplayName"</span>: <span class="hljs-string">"En新名称"</span>,
      <span class="hljs-string">"CFBundleName"</span>: <span class="hljs-string">"En新名称"</span>
    },
    <span class="hljs-string">"zh_CN"</span>: {
      <span class="hljs-string">"CFBundleDisplayName"</span>: <span class="hljs-string">"新名称"</span>,
      <span class="hljs-string">"CFBundleName"</span>: <span class="hljs-string">"新名称"</span>
    },
  },
  <span class="hljs-string">"build"</span>: {
    <span class="hljs-string">"afterPack"</span>: <span class="hljs-string">"./buildAfterPack.js"</span>,
    <span class="hljs-string">"mac"</span>: {
      <span class="hljs-string">"extendInfo"</span>: {
        <span class="hljs-string">"LSHasLocalizedDisplayName"</span>: <span class="hljs-literal">true</span>
      }
    }
    ....
  }
  ....
}
</code></pre><p>编译时的钩子脚本<code>./buildAfterPack.js</code>用于读取<code>package.json</code>中的语言配置，并生成语言包物理文件，实现如下 :</p>
<pre><code class="hljs lang-typescript"><span class="hljs-comment">/* 基于 APP 打包后的钩子功能补充语言包配置，以满足类似“旧名字”在Mac文件系统中显示为“新名字”的需求 */</span>
<span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);

exports.default = <span class="hljs-keyword">async</span> (context) =&gt; {
  <span class="hljs-keyword">const</span> { electronPlatformName, appOutDir } = context;
  <span class="hljs-keyword">if</span> (electronPlatformName !== <span class="hljs-string">'darwin'</span>) {
    <span class="hljs-keyword">return</span>;
  }
  <span class="hljs-keyword">const</span> {
    productFilename,
    info: {
      _metadata: { electronLanguagesInfoPlistStrings },
    },
  } = context.packager.appInfo;

  <span class="hljs-keyword">const</span> resPath = <span class="hljs-string">`<span class="hljs-subst">${appOutDir}</span>/<span class="hljs-subst">${productFilename}</span>.app/Contents/Resources/`</span>;
  <span class="hljs-built_in">console</span>.log(
    <span class="hljs-string">'\n&gt; 基于 package.json 配置项 “electronLanguagesInfoPlistStrings” 创建语言包 Sta \n'</span>,
    <span class="hljs-string">'\n&gt;  electronLanguagesInfoPlistStrings:\n'</span>,
    electronLanguagesInfoPlistStrings,
    <span class="hljs-string">'\n\n'</span>,
    <span class="hljs-string">'&gt;  ResourcesPath:'</span>,
    resPath
  );

  <span class="hljs-comment">// 创建APP语言包文件</span>
  <span class="hljs-keyword">const</span> createLangFilesPromise = <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(
    <span class="hljs-built_in">Object</span>.keys(electronLanguagesInfoPlistStrings).map(<span class="hljs-function">(<span class="hljs-params">langKey</span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> infoPlistStrPath = <span class="hljs-string">`<span class="hljs-subst">${langKey}</span>.lproj/InfoPlist.strings`</span>;
      <span class="hljs-keyword">let</span> infos = <span class="hljs-string">''</span>;
      <span class="hljs-keyword">const</span> langItem = electronLanguagesInfoPlistStrings[langKey];
      <span class="hljs-built_in">Object</span>.keys(langItem).forEach(<span class="hljs-function">(<span class="hljs-params">infoKey</span>) =&gt;</span> {
        infos += <span class="hljs-string">`"<span class="hljs-subst">${infoKey}</span>" = "<span class="hljs-subst">${langItem[infoKey]}</span>";\n`</span>;
      });
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve</span>) =&gt;</span> {
        fs.writeFile(<span class="hljs-string">`<span class="hljs-subst">${resPath}</span><span class="hljs-subst">${infoPlistStrPath}</span>`</span>, infos, <span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
          resolve();
          <span class="hljs-keyword">if</span> (err) <span class="hljs-keyword">throw</span> err;
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`&gt;  “{ResourcesPath}/<span class="hljs-subst">${infoPlistStrPath}</span>” 创建完毕。`</span>);
        });
      });
    })
  );
  <span class="hljs-built_in">console</span>.log(
    <span class="hljs-string">'\n&gt; 基于 package.json 配置项 “electronLanguagesInfoPlistStrings” 创建语言包 End \n'</span>
  );
  <span class="hljs-keyword">return</span> createLangFilesPromise;
};
</code></pre><h2><a id="toc-aab" class="anchor" href="#toc-aab"></a>补充配置</h2>
<p>以上解决了安装后的应用更名需求。</p>
<p>但编译打包后的文件还是“旧名称 1.2.3.dmg”、“旧名称 1.2.3.zip”。</p>
<p>用户安装过程中操作界面上大部分显示的也是“旧名称.app”。</p>
<p><img src="https://blog.pyzy.net/static/upload/20210220/TlbwbQYfeqgu8HLMA86u36hr.png" alt="alt"></p>
<p>如果要保证编译产生文件及上图中“旧名称”显示为新名称，还需要继续修改 <code>package.json</code>，按下面位置添加 <code>artifactName</code> 和  <code>dmg.title</code>:</p>
<pre><code class="hljs lang-routeros">{
  <span class="hljs-built_in">..</span><span class="hljs-built_in">..</span>
  <span class="hljs-string">"build"</span>: {
    <span class="hljs-string">"artifactName"</span>: <span class="hljs-string">"新名称-<span class="hljs-variable">${version}</span>.<span class="hljs-variable">${ext}</span>"</span>,
    <span class="hljs-string">"dmg"</span>: {
      <span class="hljs-string">"title"</span>: <span class="hljs-string">"新名称 <span class="hljs-variable">${version}</span>"</span>,
       <span class="hljs-built_in">..</span>.
    }
    <span class="hljs-built_in">..</span><span class="hljs-built_in">..</span>
  }
  <span class="hljs-built_in">..</span><span class="hljs-built_in">..</span>
}
</code></pre><h2><a id="toc-7a0" class="anchor" href="#toc-7a0"></a>后记</h2>
<p>这确实解决了我遇到的需求，但未必是最好的解决方案，如果发现有疏忽的地方或有更好的方案，欢迎回复。</p>

            ]]></description>
            <pubDate>Sat, 20 Feb 2021 12:47:22 GMT</pubDate>
            <guid>http://blog.pyzy.net/post/electron-app-display-name-mac.html</guid>
        </item>
        
        <item>
            <title>Web前端剪切板文本分享到文件发送</title>
            <link>http://blog.pyzy.net/post/clipboard.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-df3">前言</a></li>
<li><a href="#toc-85a">实现一个“复制”按钮，方便内容分享传播</a></li>
<li><a href="#toc-eca">修改将要写入剪切板的文本内容</a></li>
<li><a href="#toc-d4e">认识剪切板中的内容</a></li>
<li><a href="#toc-807">写一张图片到剪切板中</a></li>
<li><a href="#toc-8b9">剪切板操作权限判断</a></li>
<li><a href="#toc-48d">将File上传到服务端</a></li>
<li><a href="#toc-4fe">浏览器端能否通过复制发送文件或文件夹？</a></li>
<li><a href="#toc-e65">Electron 场景能否通过复制发送文件或文件夹？</a></li>
<li><a href="#toc-9d0">写在最后</a></li>
</ul>
</div><h2><a id="toc-df3" class="anchor" href="#toc-df3"></a>前言 　</h2>
<p>现在前端富交互能力越来越强，也有很多产品基于前端技术进行离线应用开发或在线应用体验增强；这其中剪切板操作也是一个经常会亮相客串的一个基础能力。</p>
<p>今天这篇文章我们就一起整理一下关于剪切板读写操作的一些实践。会包含浏览器端、Electron客户端两种不同的业务场景下，一些具体的需求实现示例。</p>
<h2><a id="toc-85a" class="anchor" href="#toc-85a"></a>实现一个“复制”按钮，方便内容分享传播</h2>
<p>在网页上一段文本后添加一个“复制”按钮并不是啥新鲜的事情，比如“复制分享网址”、“帮我砍一刀”...，早先FLASH还红火的时候，也有很多是基于FLASH实现的。</p>
<p><img src="https://blog.pyzy.net/static/upload/20210208/EMSVZKkhy2bkKrpH5T9713TG.png" alt="alt"></p>
<p>实际基于浏览器自身JS能力实现也非常方便：</p>
<pre><code class="hljs lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">textarea</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"txt"</span>&gt;</span>这里是将要被复制的文案<span class="hljs-tag">&lt;/<span class="hljs-name">textarea</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"btn"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"button"</span>&gt;</span>点我复制<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
  <span class="hljs-keyword">const</span> iptEl = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'txt'</span>);
  <span class="hljs-keyword">const</span> btnEl = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'btn'</span>);
  btn.onclick = <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
    iptEl.select();
    <span class="hljs-built_in">document</span>.execCommand(<span class="hljs-string">'copy'</span>);
    alert(<span class="hljs-string">'复制完毕。'</span>);
  };
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p><a href="https://code.h5jun.com/zetex/3/edit?html,js,output">点这里在线试验一下效果</a>。</p>
<p>上面代码中复制文本框中的内容到剪切板，主要使用了 <code>document.execCommand(&#39;copy&#39;)</code>，我们 <a href="https://caniuse.com/?search=document.execCommand%20copy">通过 Can I use 查看</a> 目前各浏览器兼容都挺好，而且也非常方便，很多富文本编辑器厂商也还是在依赖这个API。</p>
<p><img src="https://blog.pyzy.net/static/upload/20210206/p-6dxb4qmwa4u2SesR-nyBFZ.png" alt="alt"></p>
<p>不过需要注意，在<a href="https://developer.mozilla.org/zh-cn/docs/Web/API/Document/execCommand">相关标准及浏览器厂商的WEB API文档</a>中已经警告开发者这是个“已废弃”的API了。</p>
<p>在新的标准和JS API实现中，已经为我们提供了专门用于剪切板操作的<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Clipboard">Clipboard API</a>。</p>
<p>当然我们也就可以基于这个新的API，来实现上面相同的功能。只需要将<code>onclick</code>响应中的代码改成下面这样：</p>
<pre><code class="hljs lang-javascript">btnEl.onclick = <span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> {
  navigator.clipboard.writeText(iptEl.value).then(<span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> {
    alert(<span class="hljs-string">'复制完毕。'</span>);
  });
};
</code></pre>
<p><a href="https://code.h5jun.com/qila/1/edit?html,js,output">点这里在线试验一下效果</a>。</p>
<p>修改后，主要是使用了 <code>clipboard.writeText</code>将文本写入剪切板。同样我们也可以<a href="https://caniuse.com/?search=clipboard.writeText">去 Can I use 查看一下在各浏览器的兼容性</a> ；</p>
<p><img src="https://blog.pyzy.net/static/upload/20210206/_WrvLaJx4Yhq3doE4QLM2EZy.png" alt="alt"></p>
<p>和前一个方案比较，兼容性及浏览器覆盖率目前还是差一些；不过就API能力来说，<code>clipboard</code>可要比之前的 <code>execCommand</code> 更明确、更强大的多了。</p>
<h2><a id="toc-eca" class="anchor" href="#toc-eca"></a>修改将要写入剪切板的文本内容</h2>
<p>前面介绍了怎么在网页里添加一个“复制”按钮，来让用户触发复制一段文案的操作。</p>
<p>不妨就这个功能再稍微发散考虑一下：
我们知道触发复制的行为当然不会只有这一种方式（比如也可以“Command+C || Ctrl + C”这种快捷键的形式），那么当用户复制一段文本内容的时候，我们能不能进行二次加工呢？</p>
<p>比如，你可能早就留意到，在某一些网页中复制一段选中文本，粘贴的时发现除了选择内容，后面还会有&quot;原文出处、版权信息&quot;等，是怎么做到的呢？</p>
<p>这里要修改将要写入剪切板的内容，可以将功能逻辑分成3部分：</p>
<ol>
<li>监听文本复制的操作行为，阻止浏览器默认行为。</li>
<li>获取用户选择的目标文本内容，并追加内容。</li>
<li>将文本内容写入剪切板。</li>
</ol>
<p>第1步，监听<code>document</code>或目标元素的<code>copy</code>事件就可以。</p>
<p>第2步，要获取用户选中的内容，需要用到另外一个能力 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Selection">Selection API</a> ；下面示例代码中考虑兼容性实现了一个工具函数 <code>getSelectionTxt</code>。</p>
<p>第3步，我们在前面的示例中，已经实践过2种方案，这里我们再换一个方案，使用事件对象暴露的 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/ClipboardEvent/clipboardData">clipboardData.setData</a> 。</p>
<pre><code class="hljs lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSelectionTxt</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">document</span>.selection) {
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">document</span>.selection.createRange().text;
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">String</span>(<span class="hljs-built_in">window</span>.getSelection());
  }
}

<span class="hljs-comment">// 监听 copy 事件</span>
<span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'copy'</span>, (evt) =&gt; {
  <span class="hljs-keyword">const</span> { clipboardData } = evt;
  <span class="hljs-keyword">if</span> (!clipboardData) <span class="hljs-keyword">return</span>;

  <span class="hljs-comment">// 获取选中文本</span>
  <span class="hljs-keyword">let</span> txt = getSelectionTxt();
  <span class="hljs-keyword">if</span> (!txt) <span class="hljs-keyword">return</span>;

  <span class="hljs-comment">// 有要操作的目标内容，阻止浏览器默认行为</span>
  evt.preventDefault();

  <span class="hljs-comment">// 追加内容</span>
  txt += <span class="hljs-string">'\n\n 【原文地址：https://blog.pyzy.net/post/clipboard.html 】'</span>;

  <span class="hljs-comment">// 写入剪切板</span>
  clipboardData.setData(<span class="hljs-string">'text/plain'</span>, txt);
});
</code></pre>
<p><a href="https://code.h5jun.com/mefuv/2/edit?html,js,output">在线试验一下</a>。</p>
<h2><a id="toc-d4e" class="anchor" href="#toc-d4e"></a>认识剪切板中的内容</h2>
<p>上面我们都是基于纯文本的实践示例，实际剪切板里不止是可以用于文本内容的复制粘贴。</p>
<p>如果选中网页中一段带格式的富文本内容，在一个富文本编辑器粘贴，会连带格式粘贴过去。</p>
<p>如果我们使用微信等IM的截图功能、或者使用MacOS系统中的 <code>Command+Shift+4+Control</code>截图到剪切板，那么就可以在任意支持图片粘贴的地方粘贴一个图片过去，也可以在网页中粘贴实现上传。</p>
<p>另外在使用一些IM工具时候，通常也允许我们复制磁盘中的任意文件或文件夹，在会话交流界面粘贴发送出去。</p>
<p>更有代表意义的是：在Excel等Office编辑器中复制一段内容，下面也通过具体代码示例来逐步了解一下。</p>
<p>跟前面的示例监听 copy 类似地，可以通过监听 paste 粘贴事件，来读取剪切板中数据内容： </p>
<pre><code class="hljs lang-javascript"><span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'paste'</span>, (evt) =&gt; {
  <span class="hljs-keyword">const</span> { clipboardData } = evt;
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'clipboardData:'</span>, clipboardData);
});
</code></pre>
<p>可以将以上代码在网页中执行。</p>
<p>之后打开一个 Excel 表格，任意选中几个单元格，并“复制”内容。</p>
<p><img src="https://blog.pyzy.net/static/upload/20210207/blZWvz71mPgXzGqQVV2aWKwF.png" alt="alt"></p>
<p>再回到刚才执行<code>paste</code>事件监听的页面，<code>Command+V</code>(如果你是Windows系统需要<code>Ctrl+V</code>)。</p>
<p>可以看到控制台打印出的<code>clipboardData</code>中，<code>types</code>字段值数组长度为3，另外还有<code>items</code>、<code>files</code>等字段。</p>
<p><img src="https://blog.pyzy.net/static/upload/20210207/ce4_sK3kV78k7fttmuMHPGxX.png" alt="alt"></p>
<p>我们接下来对代码稍加改造，打印看一下<code>types</code>、<code>items</code>字段值中具体是什么内容：</p>
<pre><code class="hljs lang-javascript"><span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'paste'</span>, (evt) =&gt; {
  <span class="hljs-keyword">const</span> { types = [], items=[] } = evt.clipboardData || {};
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'clipboardData.types:'</span>, [...types]);
  <span class="hljs-built_in">console</span>.log(
    <span class="hljs-string">'clipboardData.items:'</span>,
    [...items].map(
       <span class="hljs-function">(<span class="hljs-params">{ kind, type }</span>) =&gt;</span> ({ kind, type })
    )
  );
});
</code></pre>
<p>神奇的事情发生了，一次复制行为，原来是可能会产生多种不同类型可用于粘贴的数据的：</p>
<p><img src="https://blog.pyzy.net/static/upload/20210207/uzISuAoL3ckKQeqGuleuIjqo.png" alt="alt"></p>
<p>其中<code>types</code>字段的内容分别为 <code>[&quot;text/plain&quot;, &quot;text/html&quot;, &quot;Files&quot;]</code>，对应到 <code>items</code>字段中的数据，我们发现这里的<code>Files</code>类型是一个<code>type</code>为<code>image/png</code>的文件对象。</p>
<p>我们再改造一下示例代码，实现一个可以将<code>&quot;text/plain&quot;, &quot;text/html&quot;, &quot;image/png&quot;</code>这3种类型内容都分别打印到页面上的功能，看看剪切板里具体内容到底是什么东东：</p>
<pre><code class="hljs lang-html"><span class="hljs-meta">&lt;!DOCTYPE html&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>剪切板内容读取试验<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="css">
    <span class="hljs-selector-tag">h2</span> {
      <span class="hljs-attribute">border-bottom</span>: solid <span class="hljs-number">1px</span> <span class="hljs-number">#eee</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">16px</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">10px</span>;
    }

    <span class="hljs-selector-id">#container</span> {
      <span class="hljs-attribute">border</span>: solid <span class="hljs-number">1px</span> <span class="hljs-number">#eee</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">10px</span>;
      <span class="hljs-attribute">background</span>: <span class="hljs-number">#f9fdff</span>;
    }

    <span class="hljs-selector-id">#container</span><span class="hljs-selector-pseudo">:empty</span><span class="hljs-selector-pseudo">:after</span> {
      <span class="hljs-attribute">content</span>: <span class="hljs-string">'请执行复制操作后，在当前页面尝试Command+V或Ctrl+V。'</span>;
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#aaa</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">14px</span>;
    }

    <span class="hljs-selector-tag">h3</span> {
      <span class="hljs-attribute">border-bottom</span>: solid <span class="hljs-number">1px</span> <span class="hljs-number">#eee</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">16px</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">10px</span>;
      <span class="hljs-attribute">margin</span>: <span class="hljs-number">10px</span> <span class="hljs-number">0</span>;
    }

    <span class="hljs-selector-tag">textarea</span> {
      <span class="hljs-attribute">display</span>: block;
      <span class="hljs-attribute">width</span>: <span class="hljs-number">95%</span>;
      <span class="hljs-attribute">height</span>: <span class="hljs-number">200px</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">8px</span> <span class="hljs-number">10px</span>;
      <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">1px</span> <span class="hljs-number">1px</span> <span class="hljs-number">3px</span> <span class="hljs-built_in">rgb</span>(0 0 0 / 20%) inset;
      <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">4px</span>;
    }

    <span class="hljs-selector-tag">img</span> {
      <span class="hljs-attribute">display</span>: block;
      <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>;
    }
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>剪切板内容：<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"container"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
    <span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'paste'</span>, (evt) =&gt; {
      container.innerHTML = <span class="hljs-string">''</span>;
      <span class="hljs-keyword">const</span> items = <span class="hljs-built_in">Array</span>.from((evt.clipboardData || {}).items || []);
      items.forEach(<span class="hljs-function">(<span class="hljs-params">item, i</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> { kind, type } = item;
        <span class="hljs-keyword">const</span> entry = item.webkitGetAsEntry &amp;&amp; item.webkitGetAsEntry();
        <span class="hljs-keyword">const</span> isDirectory = entry &amp;&amp; entry.isDirectory;
        <span class="hljs-keyword">const</span> h3 = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'h3'</span>);
        h3.innerText = <span class="hljs-string">`序号：<span class="hljs-subst">${i}</span>；kind：<span class="hljs-subst">${kind}</span>; type：<span class="hljs-subst">${type}</span>;`</span>;
        container.appendChild(h3);
        <span class="hljs-keyword">let</span> previewEl = <span class="hljs-literal">null</span>;
        <span class="hljs-keyword">if</span> (isDirectory) {
          previewEl = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'a'</span>);
          previewEl.innerText = <span class="hljs-string">'文件夹：'</span> + entry.fullPath;
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (kind === <span class="hljs-string">'file'</span>) {
          <span class="hljs-keyword">const</span> file = item.getAsFile();
          <span class="hljs-keyword">const</span> url = URL.createObjectURL(file);
          <span class="hljs-keyword">if</span> (<span class="hljs-regexp">/image/i</span>.test(type)) {
            previewEl = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'img'</span>);
            previewEl.src = url;
          } <span class="hljs-keyword">else</span> {
            previewEl = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'a'</span>);
            previewEl.href = url;
            previewEl.download = file.name;
            previewEl.innerText = file.name;
          }
        } <span class="hljs-keyword">else</span> {
          previewEl = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'textarea'</span>);
          item.getAsString(<span class="hljs-function">(<span class="hljs-params">str, ...args</span>) =&gt;</span> {
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'str'</span>, str, ...args)
            previewEl.value = str;
          });
        }
        container.appendChild(previewEl);
      });
    });
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>以上代码和之前一样，也可以<a href="https://code.h5jun.com/xawic/3/edit?js,output">“在线试验一下”</a>。</p>
<p>将刚刚复制到剪切板的Execl单元格内容在这个页面中粘贴，可以看到如下效果：</p>
<p><img src="https://blog.pyzy.net/static/upload/20210207/vJBjO8Yrv_Cs46kvYfL9X_iL.png" alt="alt"></p>
<p>右边内容展示区中，从上往下依次是：带有定界符的纯文本内容、带有CSS样式及table代码的HTML片段、一张对选择区域截图产生的图片文件。</p>
<p>我们再试验一下前面说到的使用微信截个图：</p>
<p><img src="https://blog.pyzy.net/static/upload/20210207/z35WDsQNO_rVdbnXhirVKqnv.png" alt="alt"></p>
<p>也还是<a href="https://code.h5jun.com/jucef/edit?js,output">打开上面的剪切板粘贴试验页</a>，进行粘贴试验：</p>
<p><img src="https://blog.pyzy.net/static/upload/20210207/7xV88vkdsSBSJVMkhHKgLSlK.png" alt="alt"></p>
<p>哇欧，我们在网页中通过剪切板API读取到了截取的图片 ---- 并且跟前面Excel复制行为比较仅有一个截图数据。</p>
<p>试试在文章左边Blog作者头像上右键复制图片：</p>
<p><img src="https://blog.pyzy.net/static/upload/20210207/l_bokpSOuJ1zQx9o7jT1rXrj.png" alt="alt"></p>
<p>也还是回到<a href="https://code.h5jun.com/jucef/edit?js,output">试验页</a>，Command+V 粘贴试验：</p>
<p><img src="https://blog.pyzy.net/static/upload/20210207/iUBl5RtmYmBH4eW3nVeXK5Wq.png" alt="alt"></p>
<p>哦，原来在网页里复制的图片，可以通过剪切板拿到一端富文本HTML代码和一个图片文件对象。</p>
<p>那么我们从任意磁盘目录中选择并<code>Command+C</code>复制一个图片呢？</p>
<p><img src="https://blog.pyzy.net/static/upload/20210207/ZngV-CrICgcqM-s0vqny5ALL.png" alt="alt"></p>
<p><a href="https://code.h5jun.com/jucef/edit?js,output">剪切板粘贴试验页</a>进行粘贴读取到的内容：</p>
<p><img src="https://blog.pyzy.net/static/upload/20210207/yN3BzBy4bF-URTdYjqu5KYsG.png" alt="alt"></p>
<p>可以看到，我们在网页中拿到了被复制图片的一个<code>text/plain</code>类型的<code>string</code>文件名和一个<code>image/png</code>类型的<code>File</code>对象。</p>
<p>厉害了，通过上面的示例实现的代码能力，已经实现了剪切板资源读取、文件的<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/File"><code>File</code>对象</a>获取、图片预览。</p>
<p>如果能将<code>File</code>直接发送给服务端，那么一个通过剪切板的复制粘贴能力来上传分享图片、发送截图的能力就可以实现了。</p>
<h2><a id="toc-807" class="anchor" href="#toc-807"></a>写一张图片到剪切板中</h2>
<p>上面是从页面或磁盘里复制图片资源，我们也可以让用户点一个按钮时，<a href="https://code.h5jun.com/jejac/edit?html,js,output">主动写一个图片到剪切板中</a>：</p>
<pre><code class="hljs lang-html"><span class="hljs-meta">&lt;!DOCTYPE html&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"robots"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"noindex"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>图片复制<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-comment">&lt;!-- https://p3.ssl.qhimg.com/t01f8b7c8780afb7342.jpg --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"img"</span> <span class="hljs-attr">src</span>=<span class="hljs-string">'https://p3.ssl.qhimg.com/t011e94f0b9ed8e66b0.png'</span> /&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">br</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"btn"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"button"</span>&gt;</span>复制<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"ret"</span>&gt;</span>点击“复制”，再去粘贴试试<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
    btn.onclick = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetch(img.src);
      <span class="hljs-keyword">const</span> blob = <span class="hljs-keyword">await</span> res.blob();
      <span class="hljs-keyword">const</span> item = <span class="hljs-keyword">new</span> ClipboardItem({<span class="hljs-string">'image/png'</span>: blob});
      navigator.clipboard.write([item]).then(<span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> {
        ret.innerText = <span class="hljs-string">'复制完毕。'</span>;
      }, (err) =&gt; {
        <span class="hljs-built_in">console</span>.error(err);
        ret.innerText = <span class="hljs-string">'复制失败！'</span> + err;
      });
    };

    <span class="hljs-comment">/*
    在被 iframe 的场景，记得增加 allow="clipboard-write" 属性，来设置好权限：
    ifrEl = document.querySelector('[src="https://code.h5jun.com/romi/edit?js,output"]');
    ifrEl.setAttribute('allow', 'clipboard-write')
    ifrEl.src = ifrEl.src;
    */</span>
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<h2><a id="toc-8b9" class="anchor" href="#toc-8b9"></a>剪切板操作权限判断</h2>
<p>剪切板读写会涉及用户隐私问题，有可能默认是禁止站点进行该操作的，正式业务应用中，最好也<a href="https://code.h5jun.com/romi/edit?js,output">做一下是否具备剪切板读写权限的判断</a></p>
<pre><code class="hljs lang-html"><span class="hljs-meta">&lt;!DOCTYPE html&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"robots"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"noindex"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>检查剪切板操作权限<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="css">
    <span class="hljs-selector-id">#box</span> {
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">10px</span> <span class="hljs-number">20px</span>;
    }
    <span class="hljs-selector-id">#box</span><span class="hljs-selector-pseudo">:empty</span><span class="hljs-selector-pseudo">:after</span> {
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#999</span>;
      <span class="hljs-attribute">content</span>: <span class="hljs-string">'请稍候...'</span>
    }
    <span class="hljs-selector-id">#box</span> <span class="hljs-selector-tag">h3</span> {
      <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">400</span>;
      <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">4px</span>;
      <span class="hljs-attribute">background</span>: <span class="hljs-number">#eee</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">5px</span> <span class="hljs-number">10px</span>;
    }
    <span class="hljs-selector-tag">p</span> {
      <span class="hljs-attribute">margin</span>: <span class="hljs-number">10px</span> <span class="hljs-number">20px</span>;
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#999</span>;
    }
    <span class="hljs-selector-id">#p</span><span class="hljs-selector-pseudo">:empty</span><span class="hljs-selector-pseudo">:after</span> {
      <span class="hljs-attribute">content</span>: <span class="hljs-string">'...操作结果将显示在这里...'</span>
    }
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>权限状态可能会是：prompt、granted、denied<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"box"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"p"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
    <span class="hljs-keyword">const</span> confs = [
      { <span class="hljs-attr">name</span>: <span class="hljs-string">'clipboard-read'</span> },
      { <span class="hljs-attr">name</span>: <span class="hljs-string">'clipboard-write'</span> }
    ];

    <span class="hljs-built_in">Promise</span>.all(
      confs.map(
        <span class="hljs-function">(<span class="hljs-params">conf</span>) =&gt;</span> navigator.permissions.query(conf)
      )
    ).then(<span class="hljs-function">(<span class="hljs-params">pers</span>) =&gt;</span> {
      box.innerHTML = pers.map(<span class="hljs-function">(<span class="hljs-params">status, i</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> { name } = confs[i];
        <span class="hljs-keyword">const</span> stateId = <span class="hljs-string">`state_<span class="hljs-subst">${i}</span>`</span>;
        status.onchange = <span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> { 
          <span class="hljs-built_in">document</span>.getElementById(stateId).innerText = status.state;
        };
        <span class="hljs-keyword">return</span> <span class="hljs-string">`
          &lt;h3&gt;
            权限项：<span class="hljs-subst">${name}</span>,&lt;br /&gt;
            状态值：&lt;span id="<span class="hljs-subst">${stateId}</span>"&gt;<span class="hljs-subst">${status.state}</span>&lt;/span&gt;,&lt;br /&gt;
            试一下：&lt;button onclick="runTest('<span class="hljs-subst">${name}</span>')"&gt;点这里&lt;/button&gt;
          &lt;/h3&gt;
        `</span>;
      }).join(<span class="hljs-string">''</span>);
    });

    <span class="hljs-keyword">const</span> tests = {
      <span class="hljs-string">'clipboard-read'</span>: <span class="hljs-keyword">async</span> () =&gt; {
        <span class="hljs-keyword">const</span> txt = <span class="hljs-keyword">await</span> navigator.clipboard.readText();
        p.innerText = <span class="hljs-string">'读取结果：'</span> + txt;
      },
      <span class="hljs-string">'clipboard-write'</span>: <span class="hljs-keyword">async</span> () =&gt; {
        <span class="hljs-keyword">await</span> navigator.clipboard.writeText(<span class="hljs-string">'test text'</span>);
        p.innerText = <span class="hljs-string">'写入完毕'</span>;
      },
    };
    <span class="hljs-built_in">window</span>.runTest = <span class="hljs-function">(<span class="hljs-params">name</span>) =&gt;</span> tests[name]();
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<h2><a id="toc-48d" class="anchor" href="#toc-48d"></a>将File上传到服务端</h2>
<p>完整的文件上传示例，必须依赖服务端，这里只提供纯前端伪代码的几种方案：</p>
<p>首先，如果你的服务端有一个独立的用于文件上传的接口，基于<code>XMLHttpRequest</code>（也就是熟知的AJAX）方式发送<code>File</code>对象给服务端即可：</p>
<pre><code class="hljs lang-javascript"><span class="hljs-keyword">const</span> xhr = <span class="hljs-keyword">new</span> XMLHttpRequest();
xhr.onload = <span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'上传结果：'</span>, xhr.responseText);
};
xhr.open(<span class="hljs-string">'POST'</span>, <span class="hljs-string">'./uploadFile'</span>, <span class="hljs-literal">true</span>);
xhr.send(file);
</code></pre>
<p>如果服务端接口在接受文件内容时，还要求有别的必填字段信息，往服务端send一个<code>FormData</code>对象即可：</p>
<pre><code class="hljs lang-javascript"><span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData();
formData.append(<span class="hljs-string">'type'</span>, <span class="hljs-string">'image'</span>);
formData.append(<span class="hljs-string">'file'</span>, file);

<span class="hljs-keyword">const</span> xhr = <span class="hljs-keyword">new</span> XMLHttpRequest();
xhr.onload = <span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'上传结果：'</span>, xhr.responseText);
};
xhr.open(<span class="hljs-string">'POST'</span>, <span class="hljs-string">'./uploadFile'</span>, <span class="hljs-literal">true</span>);
xhr.send(formData);
</code></pre>
<p>如果服务端接口还额外要求必须携带一些自定义请求头字段信息，也可以改成使用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API"><code>Fetch API</code></a> 来发送 formData 给服务端。</p>
<pre><code class="hljs lang-coffeescript">fetch(<span class="hljs-string">'./uploadFile'</span>, {
   method: <span class="hljs-string">'POST'</span>,
   body: formData,
   headers: <span class="hljs-keyword">new</span> Headers({
     <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span>
   })
}) .<span class="hljs-keyword">then</span>(<span class="hljs-function"><span class="hljs-params">(res)</span> =&gt;</span> { ... });
</code></pre><p>当然，文件上传并不会这么简单，比如还可能会涉及到传输进度同步、用户主动取消传输、大文件分片、文件秒传等等，不符合这里的主题，就不展开讨论了。</p>
<h2><a id="toc-4fe" class="anchor" href="#toc-4fe"></a>浏览器端能否通过复制发送文件或文件夹？</h2>
<p>当剧情发展到这里，聪明的你也许已经意识到：好像哪里不太对劲，是不是刻意隐瞒了一些问题？</p>
<p>又或者，聪明的你早就发现了，前面基于剪切板传递的都是纯文本字串、富文本HTML、或者静态图片啊！</p>
<p>我们先来复制一个GIF动图，比如下面这个：</p>
<p><img src="https://blog.pyzy.net/static/upload/20210207/KboChzyw8At5qJychfmd.gif" alt="alt"></p>
<p>到前面的<a href="https://code.h5jun.com/jucef/edit?js,output">剪切板粘贴试验页</a>粘贴一下，看看效果：</p>
<p><img src="https://blog.pyzy.net/static/upload/20210207/b7VuKC-_aw2_SnDb-g_UHs-N.png" alt="alt"></p>
<p>好的，类别<code>kind: string</code> 对应内容和前面示例中发现的规律表现一致，我们继续看。</p>
<p>诶呦，喂！不对啊，明明复制的是<code>image/gif</code>，图片内容咋也变成了和前面复制个png一样，你看它，不会动了嘿！</p>
<p>先暂且按下不管，我们干脆再多一些尝试、复制点别的文件试试，比如分别尝试粘贴一个CSS文件、粘贴一个JS文件、粘贴一个HTML文件、粘贴一组图片、粘贴一个文件夹、粘贴一组任意文件：</p>
<p><img src="https://blog.pyzy.net/static/upload/20210207/rWTSZTrXlz0-g_Qts_sftDHh.png" alt="alt"></p>
<p>WTF！！ 什么烂七八糟的？拿不到文件信息？</p>
<p>对了（突然灵鸡一动），记得么？前面我们尝试打印<code>clipboardData</code>对象到控制台的时候，和<code>items</code>平级的有个<code>files</code>字段诶。</p>
<p>我们<a href="https://code.h5jun.com/qehim/edit?js,console,output">再在粘贴时多打印一下这个<code>files</code>字段</a>，看看这里面会不会就是被复制的文件集合。</p>
<p>先来一组任意文件：</p>
<p><img src="https://blog.pyzy.net/static/upload/20210207/TMx2U8YRPqKF_t3GtpK14PSk.png" alt="alt"></p>
<p>啥也没读出来啊。再来一组图片试试：</p>
<p><img src="https://blog.pyzy.net/static/upload/20210207/9rmqaDzTJxUWuG3hg1Swa_4F.png" alt="alt"></p>
<p>复制了3张图片，但 Files 里只读出了1张图片。</p>
<p>看来是根本无法像拖拽上传那样获得要使用的文件列表啊？！</p>
<blockquote>
<p><strong>【2021-11-24 作者注】</strong>：</p>
<p> 旧有版本比如 Chrome 80 是无法按预期读取到FileList数据的；</p>
<p> 但此刻(2021-11-24)类似 Chrome 95.0.4638.69 已经能按预期读取到文件列表。</p>
<p>所以如果你是 Electron 场景、又能切换到比较新的Chrome核，那么可以无视下一个章节内容、而直接使用WEB前端JS API的方案了。</p>
</blockquote>
<h2><a id="toc-e65" class="anchor" href="#toc-e65"></a>Electron 场景能否通过复制发送文件或文件夹？</h2>
<p>浏览器中看来无解了，但是WEB前端的魔爪可是早就已经不止于浏览器中了，比如我们是可以借助 Electron 开发离线应用，并拓展使用一些浏览器中不具备的 Native 能力。</p>
<p>而且，刚好笔者涉及当前需求的产品场景就是同时支持浏览器端和Electron包壳两种场景的，那么我们不妨也调研一下。</p>
<p>通过 <a href="https://www.electronjs.org/docs/api/clipboard">Electron 剪贴板 API</a>可以看到，当下开放的方法能力实际和浏览器端没有太大区别，文档告诉我们可以通过剪切板的读写的数据或文件资源也是比较基础的文本、富文本、特殊标记语言文本、静态Image资源数据。</p>
<p>在<a href="https://github.com/electron/electron/issues/9035#issuecomment-359160710">Electron社区有人说</a>：通过<a href="https://developer.apple.com/library/archive/samplecode/ClipboardViewer/Introduction/Intro.html">Mac剪切板查看器示例程序</a>发现应该能通过<code>clipboard.read(&#39;NSFilenamesPboardType&#39;)</code>读取到一个被复制的文件或文件夹列表的XML格式描述文本：</p>
<pre><code class="hljs lang-xml"><span class="php"><span class="hljs-meta">&lt;?</span>xml version=<span class="hljs-string">"1.0"</span> encoding=<span class="hljs-string">"UTF-8"</span><span class="hljs-meta">?&gt;</span></span>
<span class="hljs-meta">&lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">plist</span> <span class="hljs-attr">version</span>=<span class="hljs-string">"1.0"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">array</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>/path/to/file1.ext<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>/path/to/file2.ext<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">array</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">plist</span>&gt;</span>
</code></pre>
<p>该XML内容的<code>plist</code>节点下的信息就是描述了被复制文件或文件夹的路径。</p>
<p>虽然不能拿到文件对象，但这可是Electron端哦。既然能拿到文件路径，再在页面里通过路径读取到文件的buffer不也就能做很多后续文件操作能力了？</p>
<p>太棒了，不妨按照程序逻辑流程逐步试验一下！</p>
<p>首先，需要从剪切板中拿到的这个XML里提取出被复制的文件或文件夹的路径：</p>
<pre><code class="hljs lang-javascript"><span class="hljs-keyword">const</span> { clipboard } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'electron'</span>);

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getClipboardPaths</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> filePathsXML = clipboard.read(<span class="hljs-string">'NSFilenamesPboardType'</span>) || <span class="hljs-string">''</span>;
  <span class="hljs-keyword">return</span> (filePathsXML.match(<span class="hljs-regexp">/&lt;string&gt;([^&lt;]*)&lt;\/string&gt;/gim</span>) || []).map(
    <span class="hljs-function">(<span class="hljs-params">filePath</span>) =&gt;</span> {
      <span class="hljs-keyword">return</span> filePath
        .replace(<span class="hljs-regexp">/(^&lt;string&gt;|&lt;\/string&gt;$)/g</span>, <span class="hljs-string">''</span>)
        .replace(<span class="hljs-regexp">/&amp;amp;/g</span>, <span class="hljs-string">'&amp;'</span>)
        .replace(<span class="hljs-regexp">/&amp;lt;/g</span>, <span class="hljs-string">'&lt;'</span>)
        .replace(<span class="hljs-regexp">/&amp;gt;/g</span>, <span class="hljs-string">'&gt;'</span>);
    }
  );
}
</code></pre>
<p>如果我们复制了2个文件和一个文件夹，上面的代码可以帮我们读取到如下数组：</p>
<pre><code class="hljs lang-json">[
  '/path/to/folder1',
  '/path/to/file1.ext',
  '/path/to/file2.ext',
]
</code></pre>
<p>上面的 <code>/path/to/folder1</code> 是一个文件夹，如果我们的目的是要复制粘贴文件，那么递归遍历展开文件夹也是必须的：</p>
<pre><code class="hljs lang-javascript"><span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);
<span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>);

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">flatPaths</span>(<span class="hljs-params">filePathsArr</span>) </span>{
  <span class="hljs-keyword">const</span> filePaths = [];
  <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(
    filePathsArr.map(<span class="hljs-function">(<span class="hljs-params">filePath</span>) =&gt;</span> {
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve</span>) =&gt;</span> {
        fs.stat(filePath, (err, stats) =&gt; {
          <span class="hljs-keyword">if</span> (!err) {
            <span class="hljs-keyword">if</span> (stats.isDirectory()) {
              fs.readdir(filePath, <span class="hljs-keyword">async</span> (err, files) =&gt; {
                <span class="hljs-keyword">if</span> (!err &amp;&amp; files) {
                  <span class="hljs-keyword">const</span> filesPaths = files.map(<span class="hljs-function">(<span class="hljs-params">fileName</span>) =&gt;</span> {
                    <span class="hljs-keyword">return</span> path.join(filePath, fileName);
                  });
                  <span class="hljs-keyword">const</span> flatFilesPaths = <span class="hljs-keyword">await</span> flatPaths(filesPaths);
                  filePaths.push(...flatFilesPaths);
                }
                resolve();
              });
              <span class="hljs-keyword">return</span>;
            } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (stats.isFile()) {
              filePaths.push(filePath);
            }
          }
          resolve();
        });
      });
    })
  );
  <span class="hljs-keyword">return</span> filePaths;
}
</code></pre>
<p>接下来是重点了，我们先简单粗暴一些，直接循环将前面加工拿到的所有文件绝对路径通过<code>fs.readFile</code>读取出文件的buffer数据。</p>
<pre><code class="hljs lang-javascript"><span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);
<span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>);

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getClipboardFiles</span>(<span class="hljs-params">fileAbsPathsArr</span>) </span>{
  <span class="hljs-keyword">const</span> files = [];
  <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(
    fileAbsPathsArr.map(<span class="hljs-function">(<span class="hljs-params">filePath</span>) =&gt;</span> {
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve</span>) =&gt;</span> {
        fs.readFile(filePath, (err, data) =&gt; {
          <span class="hljs-keyword">if</span> (!err) {
            files.push({
              filePath,
              <span class="hljs-attr">fileName</span>: path.basename(filePath),
              data, <span class="hljs-comment">// 可用于在Web端JS new File([data], fileName)</span>
            });
          }
          resolve();
        });
      });
    })
  );
  <span class="hljs-keyword">return</span> files;
}
</code></pre>
<p>这次经过<code>getClipboardFiles</code>之后我们拿到的是像下面这样，带有文件绝对路径、文件名、文件buffer数据的数组：</p>
<pre><code class="hljs lang-undefined">[
  {
    filePath: '/path/to/folder1/fileN.ext',
    fileName: 'fileN.ext',
    data: [...Uint8Array...],
  },
  {
    filePath: '/path/to/file1.ext',
    fileName: 'file1.ext',
    data: [...Uint8Array...],
  },
  ...,
]
</code></pre>
<p>现在可以回到我们熟悉的 Web 页面中JS交互逻辑里了。</p>
<pre><code class="hljs lang-javascript"><span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'paste'</span>, <span class="hljs-keyword">async</span> (evt) =&gt; {
  <span class="hljs-keyword">const</span> clipboardPaths = <span class="hljs-keyword">await</span> flatPaths(getClipboardPaths());
  <span class="hljs-keyword">if</span> (clipboardPaths.length) {
    evt.stopPropagation();
    evt.preventDefault();
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">return</span>; <span class="hljs-comment">// any ...</span>
  }
  <span class="hljs-comment">// 得到剪切板中文件数据们</span>
  <span class="hljs-keyword">const</span> clipboardFiles = <span class="hljs-keyword">await</span> getClipboardFiles(clipboardPaths);
  <span class="hljs-comment">// 使用 new File 生成可用于Web端的 File 对象</span>
  <span class="hljs-keyword">const</span> fileList = clipboardFiles.map(<span class="hljs-function">(<span class="hljs-params">{ fileName, data }</span>) =&gt;</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> File([data], fileName);
  });

  <span class="hljs-comment">/* 文件预览、发送.... sendFiles(fileList); */</span>
});
</code></pre>
<p>到这里，我们在Electron端发送文件的效果也就实现了。</p>
<p>拿到文件列表就可以后面的折腾了，比如用户发送前，给一个文件预览，二次确认的列表：</p>
<p><img src="https://blog.pyzy.net/static/upload/20210208/7s0ofEQGePK_IKmqutBybNkD.png" alt="alt"></p>
<p>另外，Electron场景的<code>NSFilenamesPboardType</code>还有一个非常值得令人振奋的地方，不但可以读取剪切板文件列表，也可以写任意文件列表到剪切板了（注：笔者该试验仅限 MacOS 场景中）。</p>
<pre><code class="hljs lang-javascript"><span class="hljs-keyword">const</span> { clipboard } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'electron'</span>);

clipboard.writeBuffer(
  <span class="hljs-string">'NSFilenamesPboardType'</span>,
  Buffer.from(<span class="hljs-string">`
    &lt;?xml version="1.0" encoding="UTF-8"?&gt;
    &lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&gt;
    &lt;plist version="1.0"&gt;
      &lt;array&gt;
        &lt;string&gt;/path/to/file1.ext&lt;/string&gt;
        &lt;string&gt;/path/to/file2.ext&lt;/string&gt;
      &lt;/array&gt;
    &lt;/plist&gt;
  `</span>)
)
</code></pre>
<h2><a id="toc-9d0" class="anchor" href="#toc-9d0"></a>写在最后</h2>
<p>首先，请读者注意：以上所有示例代码更侧重在思路示意，甚至可以说是伪代码，如果要在正式业务场景应用，请一定酌情调整、增加严谨性、健壮性、兼容性相关考量。</p>
<p>另外，相关标准委员会、浏览器厂商（比如：<a href="https://bugs.chromium.org/p/chromium/issues/detail?id=897289">Chrome</a>）、Electron官方也都还在不断迭代扩展Clipboard API，本文的方案只是基于时下的形式考虑，如果发现不符欢迎随时指正，以免误导。</p>
<p>最后，郑重感谢您花时间在本文中，如果有哪些点描述不准确或需要讨论的，也欢迎评论留言。</p>
<p>新年快乐~</p>

            ]]></description>
            <pubDate>Sat, 06 Feb 2021 10:49:37 GMT</pubDate>
            <guid>http://blog.pyzy.net/post/clipboard.html</guid>
        </item>
        
        <item>
            <title>Web前端HEVC播放器实践剖析</title>
            <link>http://blog.pyzy.net/post/qhww.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-ba9">序言</a></li>
<li><a href="#toc-583">正文</a><ul>
<li><a href="#toc-b98">1. 需求背景</a><ul>
<li><a href="#toc-564">1.1 浏览器端HEVC的支持情况</a></li>
<li><a href="#toc-739">1.2 Web端解码方案</a></li>
<li><a href="#toc-473">1.3 浏览器端WebAssembly的支持情况</a></li>
<li><a href="#toc-f2f">1.4 HEVC播放器需求目标</a></li>
</ul>
</li>
<li><a href="#toc-93f">2. 架构设计</a></li>
<li><a href="#toc-d1a">3. 分解实现</a><ul>
<li><a href="#toc-5e3">3.1 下载器</a><ul>
<li><a href="#toc-ddb">线性的数据流的合并与拆分</a></li>
<li><a href="#toc-db8">内部维护管理 range 状态</a></li>
<li><a href="#toc-2bf">不同媒体类型数据获取的差异</a></li>
<li><a href="#toc-10b">MOOV 前置或后置</a></li>
<li><a href="#toc-d8d">慎重并折中的控制内存消耗</a></li>
</ul>
</li>
<li><a href="#toc-38d">3.2 解码器</a><ul>
<li><a href="#toc-2be">启动解码前依赖数据量控制</a></li>
<li><a href="#toc-f4b">主动向下载器获取数据</a></li>
<li><a href="#toc-1e8">动态解码模式控制CPU消耗</a></li>
<li><a href="#toc-ce1">独立的音频、画面帧数据队列</a></li>
<li><a href="#toc-e0b">音频重新采样</a></li>
</ul>
</li>
<li><a href="#toc-0a3">3.3 渲染器</a><ul>
<li><a href="#toc-8cd">依赖解码、UI提供画布</a></li>
<li><a href="#toc-c22">主动向解码器获取帧数据</a></li>
<li><a href="#toc-251">分缓存队列、渲染队列</a></li>
<li><a href="#toc-da3">音画同步、倍速播放、Waiting</a></li>
<li><a href="#toc-529">动态码率变化</a></li>
</ul>
</li>
<li><a href="#toc-53f">3.4 UI</a></li>
<li><a href="#toc-a4a">3.5 控制层</a><ul>
<li><a href="#toc-9fd">而WebWorker本身的设计存在各种不便：</a></li>
<li><a href="#toc-c23">针对这个问题我们结合Promise 实现了PromiseWebWorker。</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#toc-fa0">4. 要点回顾</a></li>
<li><a href="#toc-54e">5. 难点突破</a></li>
<li><a href="#toc-c91">6. 未来展望</a></li>
</ul>
</li>
</ul>
</div><h1><a id="toc-ba9" class="anchor" href="#toc-ba9"></a>序言</h1>
<blockquote>
<p>前面给360视频云团队围绕HEVC前端播放及解密实现了一套基于WebAssembly、WebWorker的通用模块化Web播放器。</p>
<p>在LiveVideoStackCon2019深圳的演讲中对其架构设计、核心原理，具体痛点问题的解决方式进行了详细剖析分享。</p>
<p>这里整理为一篇技术文章，希望能更大范围的给大家带来一些帮助。</p>
<p>原分享PPT链接： <a href="https://ppt.baomitu.com/d/fcaa3a47">https://ppt.baomitu.com/d/fcaa3a47</a></p>
<p>本分享实现播放器在线DEMO：  <a href="http://lab.pyzy.net/qhww">http://lab.pyzy.net/qhww</a></p>
<p>参数API文档：<a href="https://zyun.360.cn/developer/doc?did=QHWWPlayer">https://zyun.360.cn/developer/doc?did=QHWWPlayer</a></p>
</blockquote>
<h1><a id="toc-583" class="anchor" href="#toc-583"></a>正文</h1>
<p><img src="http://blog.pyzy.net/static/upload/20200426/HQAie97xsaAe694p9n2t.jpg" alt="alt"></p>
<p>文 / 胡尊杰     &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  整理 / LiveVideoStack</p>
<p>奇舞团是360集团最大的大前端团队，同样也是TC39和W3C会员，拥有Web前端、服务端、Android、iOS、设计、产品、运营等岗位人员，旗下的开源框架和技术品牌有SpriteJS、ThinkJS、MeshJS、Chimee、QiShare、声享、即视、奇字库、众成翻译、奇舞学院、奇舞周刊、泛前端分享等。</p>
<p>奇舞团支持的业务基本上涵盖了360大部分业务线。我个人最开始的时候也曾带队负责360核心安全平台的Web前端支持，包括大家耳熟能详的安全卫士、杀毒软件等。随着公司的业务发展，后面也负责了IoT业务前端支持，最近两年主要配合360视频云的一些Web前端支持工作。基于HEVC的播放器，实际上就是来源于我们最近做的一个叫QHWWPlayer的播放器。HEVC并不是一个新鲜事物，但对于我们团队来说，Web前端的HEVC播放器一直是个亟待优化的领域。虽然移动终端或PC端HEVC播放器已经遍地开花，但在Web端仍旧有很多地方需要改进。包括现存一系列智能硬件产品，也在固件采集端已经应用了HEVC的编码，不过如果想让其在Web端呈现并达到用户需求仍需加倍的努力。本次分享将从以下几个维度展开，希望能给大家带来一定的参考价值。</p>
<p><img src="http://blog.pyzy.net/static/upload/20200426/kbew0T51uZwO4TRc2KK8.png" alt="alt"></p>
<h2><a id="toc-b98" class="anchor" href="#toc-b98"></a>1. 需求背景</h2>
<h3><a id="toc-564" class="anchor" href="#toc-564"></a>1.1 浏览器端HEVC的支持情况</h3>
<p><img src="https://p3.ssl.qhimg.com/t01a0d1ff4ef6a5a561.png?size=1080x608" alt="alt"></p>
<p>上图展示了HEVC在浏览器端的支持情况，其中红色代表不支持的浏览器对应版本，绿色代表对HEVC具有良好的支持，青色代表无法保证浏览器可以很好地支持HEVC。总体上来说HEVC在浏览器端并不是一个得到广泛支持的靠谱方案。</p>
<p>一般情况下，PC端浏览器都给我们提供了相应的API，如果我们的业务场景是支持HEVC的浏览器，可尝试有效利用浏览器的原生能力。</p>
<p>基于浏览器原生video，配置source时指定解码器，告知浏览器当前视频采取的是哪一种编码方案。如果浏览器自身有能力进行解码那么其自然会走入“支持HEVC”的逻辑分支当中。</p>
<p>也可以另外通过JS实现检测功能，JS也提供了相应API——canPlayType来判断当前浏览器环境是否支持HEVC解码。</p>
<p>但如果以上流程无法得到有效支持呢？这也是本次分享我们讨论的重点。</p>
<h3><a id="toc-739" class="anchor" href="#toc-739"></a>1.2 Web端解码方案</h3>
<p><img src="https://p3.ssl.qhimg.com/t01d3064eb9fd3d55d6.png?size=1080x608" alt="alt"></p>
<p>浏览器端视频解码总共有以上三种方案，首先就是前文我们提到的基于浏览器原生能力的播放，例如基于video标签拉流、解码以及渲染播放，整个过程完全由浏览器实现。第二种方案是首先通过JS来下载视频流、对视频流进行解封装与转封装处理，最后再通过浏览器提供的相关API，交由浏览器原生video进行解码与渲染播放。如开源社区当中的HLS.JS或FLV.JS等就是基于该思路。</p>
<p> 但是HEVC不能仅靠解封装与转封装来实现，因为其本质上在解码层就不支持。因此第三种方案就是：JS下载的视频流首先经由解封装（解密）处理，并在接下来进行解码，解码完成后渲染播放。如果我们这里转成浏览器普遍支持的解码格式并让video标签进行播放，尽管理论上可行，但成本显然是非常高的，并且中间存在一个无端的浪费。因此这里通常直接采用浏览器端Canvas+WebAudio API实现视频与音频的渲染，而不再使用浏览器原生video能力。这里如果使用纯浏览器原生的JS，由于 JS天生单线程执行的弱势，会导致整个处理的效率比较差。</p>
<p>近期，万维网标准化委员会正式推出了WebAssembly规范。一方面我们可以借助WebAssembly高于JS的能力，实现更加出色的大规模数据处理与解码，另一方面基于WebAssembly，我们也能方便地将传统媒体处理中基于C或C++开发的一些媒体处理能力集成在浏览器端执行，并且可通过JS来调用API。对于熟悉传统Web前端开发的我们来说，这也是一个值得我们坚持探索与实践的全新领域。有了WebAssembly之后，我们就可以让部门内擅长视频处理的专家级同事来配合实现更加出色的浏览器端视频播放，相对以往的开发流程来说，无论是能力、成本控制还是效率与灵活程度都有十分显著的提升。</p>
<h3><a id="toc-473" class="anchor" href="#toc-473"></a>1.3 浏览器端WebAssembly的支持情况</h3>
<p><img src="https://p3.ssl.qhimg.com/t01bf4893857626e717.png?size=1080x608" alt="alt"></p>
<p>上图展现了浏览器端WebAssembly的支持情况，尽管个别低版本的浏览器有一些支持限制，但随着标准化委员会对该标准的不断推进，情况会变得越来越好。在包括一些混合式场景，例如APP内嵌（比如聊天工具或通讯工具当中打开一个链接）等情况，是否支持也取决于WebView本身提供的能力以及WebAssembly的支持情况，总体上来说趋于向好。</p>
<h3><a id="toc-f2f" class="anchor" href="#toc-f2f"></a>1.4 HEVC播放器需求目标</h3>
<p><img src="https://p3.ssl.qhimg.com/t01c0d64ef26d5894e9.png?size=1080x608" alt="alt"></p>
<p>HEVC播放器的需求目标，就是基于 JavaScript 相关API，配合FFmpeg+WASM达成 HEVC 在浏览器端的解码解密、渲染播放的需求，接下来我们就开始研究如何落地这一目标。</p>
<h2><a id="toc-93f" class="anchor" href="#toc-93f"></a>2. 架构设计</h2>
<p><img src="https://p3.ssl.qhimg.com/t0133aed4100858e6bf.png?size=1080x608" alt="alt"></p>
<p>总体架构设计思路如上图所示，首先我们需要一个专门负责下载的下载器，该下载器也是基于浏览器的JS Fetch或XHR API，以实现文件获取或直播拉流等操作。成功拉取的视频流会被存储在一个数据队列当中，随后基于WebAssembly（WASM）+FFmpeg的解码器会来消费处理队列里这些流数据，解码出音视频数据，并放置在音视频帧数据队列当中，等待随后的渲染器对其进行渲染处理。渲染器基于WebGL+Canvas与WebAudio调用硬件渲染出图像与音频。</p>
<p>最后则是控制层用于贯穿整体流程中下载、解码、渲染等独立模块，同时实现底层一些基本功能：如之前我们提到JS为单线程，而浏览器提供的WebWork API可拉起一个子线程。该流程中每一个模块都是独立的，队列中的生产与消费过程也是异步进行的。（我们可基于JS本身一些比较好的特性实现诸多便捷的功能。例如基于Promise可以将异步过程进行较为合理的封装，并呈现一些异步处理逻辑流程的关键环节的控制到UI层。）</p>
<p>除此之外，还有控制层的一些基础配置选项，包括播放器本身的一些事件或消息的管理，都可以基于控制层来实现。</p>
<h2><a id="toc-d1a" class="anchor" href="#toc-d1a"></a>3. 分解实现</h2>
<h3><a id="toc-5e3" class="anchor" href="#toc-5e3"></a>3.1 下载器</h3>
<p><img src="https://p3.ssl.qhimg.com/t01cbfabaf3ce63fbe8.png?size=1080x608" alt="alt"></p>
<p>下载器作为一个基本模块独立存在，具有初始配置、启动、暂停、停止、队列管理与Seek响应（用于进度条拖拽）等基本功能。上图左侧图标是在开发完成后，基于下载器的事件消息呈现的数据可视化结果。（柱状图表示单位时间下载量，这里我们可以看到的是，下载量并不均匀，其中的变化可能取决于推流端、服务端、用户端，也可能取决于整个网络环境。）</p>
<p>下载器方面需要留意五个关键问题点：</p>
<h4><a id="toc-ddb" class="anchor" href="#toc-ddb"></a>线性的数据流的合并与拆分</h4>
<p>我们应当进行线性数据流的合并与拆分。理论上浏览器从服务端下载一个视频流的过程是线性的，但浏览器的表现实际上并非如此，二者的差异可能会很大。</p>
<p>例如当一个浏览器启动并基于JSFetch API抓取流，其过程也是通过API监听数据回调来实现，每次回调可能间隔会很短、数据量也只是一个很小的一千字节左右的数据包。但有些浏览器的表现并非如此，它们会等抓取到一个1M或2M的数据包之后才反馈给API回调。</p>
<p>而那些过于零碎的数据直接丢给队列或之后的流程来处理，这样势必导致更频繁的数据处理；数据包体积大的直接队列和后续流程势必增加单次处理成本。</p>
<p>因此对线性数据流的合理合并与拆分十分必要，整个过程也是结合初始配置来实现阈值控制。</p>
<p>通过阈值调节控制，我们希望能够做好用户端浏览器硬件资源消耗，与该业务场景下媒体播放产品服务体验之间的取舍与平衡。</p>
<h4><a id="toc-db8" class="anchor" href="#toc-db8"></a>内部维护管理 range 状态</h4>
<p>除此之外，下载器实际上也需要内部维护管理range 状态。例如当用户选择点播时，我们需要明确是从哪一个字节位置到另一个字节位置下载传输中间这一片数据。而在直播过程中，则可能出现由网络环境造成卡顿或用户端主动暂停的现象，此时下载器需要明确知道播放或当前下载的位置。</p>
<h4><a id="toc-2bf" class="anchor" href="#toc-2bf"></a>不同媒体类型数据获取的差异</h4>
<p>第三点是不同媒体类型数据获取的差异，也就是下载器针对不同的媒体类型开发不同的下载功能。例如一个FLV直播流可以理解为是一个连续的线性的数据获取，而点播则以包为单位获取。对于HLS流需要获取m3u8列表，完成分析之后再从中选取数据包的地址并单独下载，随后进行流的合并或拆分。总地来说，我们需要保证数据的最终产出尽量均匀存储到队列中，以便于后续的一系列处理。</p>
<h4><a id="toc-10b" class="anchor" href="#toc-10b"></a>MOOV 前置或后置</h4>
<p>在媒体处理中像MOOV等的索引数据有前置与后置两种情况，这里需要注意的是，我们的播放器基于Web端。</p>
<p>若索引文件为后置，如果播放器直接下载了一部分数据就直接丢给FFmpeg解码器进行解码，由于FFmpeg解码器无法获取索引，当然也就无法解码成功。除非解码器等待整体媒体源下载完毕，实际上这样是不现实的。</p>
<p>另外由于我们无法控制MOOV索引数据的体量，前置索引的大小无法确定，尤其对于一些特殊情况，这种逻辑会带来很多问题。（但是这里有一个取巧的办法，就是我们可以尝试首先抓取前面几个数据包，探测MOOV边界，并基于此得到MOOV的长度，从而判断取舍在什么时机启动后续的解码。）</p>
<h4><a id="toc-d8d" class="anchor" href="#toc-d8d"></a>慎重并折中的控制内存消耗</h4>
<p>最后，慎重并折中控制内存消耗也至关重要。例如尽管较大的缓存能带来流畅的播放，但在Seek时就会带来很大的浪费，我们则需要根据服务所在的应用场景、帧率码率等来实现合理的折中与取舍。</p>
<h3><a id="toc-38d" class="anchor" href="#toc-38d"></a>3.2 解码器</h3>
<p><img src="https://p3.ssl.qhimg.com/t014773c92795ea9e0c.png?size=1080x608" alt="alt"></p>
<p>下载器之后，整个流程的核心能力就是解码器。解码器的基本功能与下载器相比大同小异，需要特别关注的是解码器并不是像下载器完全是去调用一个原生的JS Fetch API或XHR，而是在启动WebWorker之后再启动WebAssembly（这里的WebAssembly依赖中是引入了定制化的FFmpeg API，以解决解容器、解码等需求），并实现一些API的交互。上图左侧展现了音频与视频帧解码数据队列的可视化结果。</p>
<p>解码器方面，需要关注的关键问题主要有以下几点：</p>
<h4><a id="toc-2be" class="anchor" href="#toc-2be"></a>启动解码前依赖数据量控制</h4>
<p>刚才讲到MOOV前置与后置时我们也提及这一点，也就是在启动解码前做好数据量控制，明确其数据量是否已经达到FFmpeg的基本需求。如果索引文件的数据还没有完全给到就直接使用命令行启动FFmpeg，那么就会出现报错的情况。我们应当结合数据量的精准控制来对解码器的启动时机做合理的判断。</p>
<h4><a id="toc-f4b" class="anchor" href="#toc-f4b"></a>主动向下载器获取数据</h4>
<p>解码器需要主动获取下载器生成的数据队列，这样系统便可根据数据消费效率获知当前解码器是否处于繁忙的状态。同时，主动向下载器获取数据也能在一定程度上减轻CPU的负担，并可根据CPU的负载来决定当前从下载端应该获取多少数据。例如如果CPU负载较大则数据队列自然会出现累积，我们可以在下载器初始化时设置一个阈值，如果数据队列积累达到该阈值则下载器暂停下载，这样就可合理控制处理的整体流程并确保播放的正常。</p>
<h4><a id="toc-1e8" class="anchor" href="#toc-1e8"></a>动态解码模式控制CPU消耗</h4>
<p>整个解码过程实际上还依赖CPU的性能，如果单帧解码的时间较长，例如一个帧率是25的视频，仅单帧解码就需耗费半秒钟甚至更长时间，此时如果我们依然按照这样半秒钟或更久的频度解码，则解码数据生产效率完全跟不上渲染的自然时间进度，效果肯定不符合预期，播放也会断断续续。因此我们需要针对不同的应用场景，使用动态解码模式（主动丢帧）控制好CPU的消耗。例如在直播或安防场景下，我们可以舍弃一些指标以保证解码与传输的时效性。</p>
<h4><a id="toc-ce1" class="anchor" href="#toc-ce1"></a>独立的音频、画面帧数据队列</h4>
<p>如上图左侧所示，独立的音频与画面帧数据队列分别管理；比如我们启动丢帧策略的话，会看到画面帧数据量变少，但声音没有变化。</p>
<h4><a id="toc-e0b" class="anchor" href="#toc-e0b"></a>音频重新采样</h4>
<p>采集端编码数据的音频采样率需要结合播放端的支持情况来留意兼容问题。</p>
<p>浏览器是一个比较特殊的应用场景，各浏览器对音频渲染中采样率的支持程度也是不同的。</p>
<p>例如安防场景对声音的要求并不是很高，通常16,000的采样率即可，但是如果想在浏览器端播放视频，则部分浏览器要求至少22,050的采样率，否则浏览器端播放无法成功识别并渲染音频数据。FFmpeg本身可以进行音频重新采样，因此我们可以在解码器端加入相应的配置项，如果用户有该需求那么就可以启动音频重新采样，重新把16,000的音频采样率重采样成符合浏览器所要求的22050采样率。有了符合要求的独立的音频与视频数据帧队列，接下来也自然就能基于浏览器实现对音视频的渲染与呈现。</p>
<h3><a id="toc-0a3" class="anchor" href="#toc-0a3"></a>3.3 渲染器</h3>
<p><img src="https://p3.ssl.qhimg.com/t0177bc16693c97c26b.png?size=1080x608" alt="alt"></p>
<p>渲染器的基本功能与下载器、解码器相似，不同之处在于以下几个关键点：</p>
<h4><a id="toc-8cd" class="anchor" href="#toc-8cd"></a>依赖解码、UI提供画布</h4>
<p>渲染器需要浏览器提供一个独立的画布用于绘制相应的视觉画面内容。在UI模块初始化时呈现出一个画布的容器，渲染器渲染生成的画面才能表现在网页上。</p>
<p>除此之外，渲染器依赖解码器解码生产出的音视频帧数据才能进行音画渲染。</p>
<h4><a id="toc-c22" class="anchor" href="#toc-c22"></a>主动向解码器获取帧数据</h4>
<p>这一点与解码器向下载器主动拿数据相似。</p>
<h4><a id="toc-251" class="anchor" href="#toc-251"></a>分缓存队列、渲染队列</h4>
<p>渲染器会消费处理等待渲染的帧数据队列，只不过帧数据会被分为缓存队列与渲染队列。</p>
<p>而之前我们介绍的下载器与解码器，本身只有一组数据队列。为什么要这样呢？渲染器调用WebAudio API将音频数据传输给浏览器进行PCM渲染时，无法将已经通过该API传输给浏览器的数据做取回控制，因此就需要记录当前已经给了多少数据到浏览器，这就是“渲染队列”。而“缓存队列”则是从进程中获取一部分数据先存储在一个临时队列当中，从而避免频繁地向处于另一个独立WebWorker中的解码器索取其音画帧队列数据，而带来不必要的时间消耗。</p>
<h4><a id="toc-da3" class="anchor" href="#toc-da3"></a>音画同步、倍速播放、Waiting</h4>
<p>音画同步、倍速播放以及判定是否处于等待状态至关重要。比如要追求直播的低延时，网络抖动导致数据堆积发生的时候，倍速追帧是个有效的办法。</p>
<h4><a id="toc-529" class="anchor" href="#toc-529"></a>动态码率变化</h4>
<p>一个视频在播放的过程中，可能随网络状态的波动出现码率的动态变化，例如为适应较差的网络状况，播放器可以主动将媒体流获取从一个较为清晰的高分辨率变化到一个比较模糊的低分辨率源。</p>
<p>而再渲染中，基于WebGLCanavas的渲染器，我们首先需要对YUV着色器进行初始化操作，而YUV着色器的初始化，依赖于其所绘制的数据对应的分辨率、比例与尺寸。如果最开始的分辨率、比例和尺寸与之后要渲染的数据不一样，而我们又未对此做相应的响应适配，那么就会出现画面绘制花屏的情况。而动态码率变化就是要随时响应每一画面帧所对应的分辨率变化，对YUV着色器作动态调整，从而保证画面的实时性与稳定性。</p>
<p>从下载、解码到渲染，视频播放器的基本流程就此建立，播放器便有了获取媒体数据、完成解码、呈现音画效果的基本能力。</p>
<h3><a id="toc-53f" class="anchor" href="#toc-53f"></a>3.4 UI</h3>
<p><img src="https://p3.ssl.qhimg.com/t01c595696c315ad2bc.png?size=1080x608" alt="alt"></p>
<p>基本的UI如上图左侧所示，上半部分是整个播放器在实例化之前我们可以去做的一系列初始化配置。图中所示的仅是一小部分参数，例如媒体源的地址、是否启用了加密Key、对应的解密算法，包括渲染时为满足某些特定场景下的需求，音视频是同时进行渲染还是在主动控制下仅渲染音频或视频——例如在安防监控业务场景，会有一些设备需要音频采集、另一些不需要，或者干脆播放时就不想播放源流音频等等。若在这里播放器不做判定支持，则存在由于音画同步控制依赖音频帧视频帧时间戳比对，但没有音频帧数据的原因导致无法正常播放，而播放器使用者能进行主动控制则可以避免该问题。</p>
<p>UI的基本功能包括实例化、用户操作触发后续流程涉及的各模块接下来要做什么，还有状态信息响应展现，也就是根据用户交互行为和播放器工作状态作出反馈与信息传递。</p>
<p>另外，UI也需要对相应的状态变化作出响应，例如用户控制当前播放器从正在播放切换到暂停，那么UI层面则需要针对用户操作进行相应的变化。还有快进、拖拽进度条等等。</p>
<h3><a id="toc-a4a" class="anchor" href="#toc-a4a"></a>3.5 控制层</h3>
<p><img src="https://p3.ssl.qhimg.com/t017390f7001d0317bf.png?size=1080x608" alt="alt"></p>
<p>最后的控制层至关重要，首先控制层隔离校验对外暴露的参数及方法。播放器可实现或具备的特性有很多，不可能全部暴露给用户。在播放视频时，下载与解码的数据实际上存在一个前后呼应的关系，如果我们不考虑用户行为与需求，在网页上呈现播放器的所有特性。而用户也不对其进行科学性选择与判断，而是随意调用API，势必会带来矛盾、冲突与混乱。因此我们需要隔离配置信息、校验对外暴露的控制参数及方法，以避免可能存在的冲突。</p>
<p>另外根据之前的介绍我们可以看到，不同模块的基本功能大致相同。因此在控制层我们需要统一各模块的生命周期，并完成用于调度各模块工作的基础类的实现。</p>
<p>每个独立的模块什么时刻可以实例装载？什么时刻销毁？该模块是否支持热插拔？各模块生命周期状态的管控与事件消息的监听与调度…… 这些都由控制层进行管理。</p>
<p>有时我们需要做一些取舍，例如编码器并不是基于FFmpeg，而是基于我们自己的解码解决方案，那么就可以尝试在播放器实例化时候，更换对应模块当中相对应的部分依赖为自己的解码方案；如果我们需要调整播放器UI层界面样式，那么就可能需要定制自己的UI模块……</p>
<p>在这个播放器实现中，为了规避单线程一些弊端，我们基于WebWorker API对重点模块开启子线程。</p>
<h4><a id="toc-9fd" class="anchor" href="#toc-9fd"></a>而WebWorker本身的设计存在各种不便：</h4>
<p>首先，要求我们必须单独打包一个JS文件，基于 new Worker(“*.js”)引入到项目中。</p>
<p>但我们整个播放器作为SDK项目的构建来说，通常只产生一个JS文件发布出去，才是合理的。如果同时产生多个JS文件，这对我们的调试、开发或后续应用等来说都不方便。</p>
<h4><a id="toc-c23" class="anchor" href="#toc-c23"></a>针对这个问题我们结合Promise 实现了PromiseWebWorker。</h4>
<p>PromiseWebWorker 相对于原生Worker，参数不再必须是传入一个JS引用路径，而是可以传入一个函数。</p>
<p>这样以来我们就可以在项目编译时生成一个独立的JS文件，在播放器的执行过程中将其中worker依赖的那部分函数内容生成一个虚拟的文件依赖地址，作为WebWorker执行的资源。</p>
<p>其次，WebWorker原生能力实现父子线程之间数据传递通讯，只能通过postMessage传送数据、通过onMessage获取传送过来的数据，这对于频繁的数据交互中想保证上下文关联对应关系是比较麻烦的。PromiseWebWorker则借助了Promise的优势，对以上整个数据交换过程做严格的应答封装处理，从而实现播放器功能的健壮可靠。</p>
<p><img src="https://p3.ssl.qhimg.com/t01edf670a05b1619ce.png?size=1080x608" alt="alt"></p>
<p>上图链接 <a href="http://lab.pyzy.net/qhww">http://lab.pyzy.net/qhww</a> 中是播放器DEMO展示地址。</p>
<p>若对播放器的效果和实现感兴趣可以前往试用研究。</p>
<h2><a id="toc-fa0" class="anchor" href="#toc-fa0"></a>4. 要点回顾</h2>
<p><img src="https://p3.ssl.qhimg.com/t014fc603354dab3803.png?size=1080x608" alt="alt"></p>
<p>调度控制层控制下载器、解码器、渲染器与UI交互四大模块，如果要做某功能模块的业务定制化开发、功能增强补充，对对应独立的模块内部进行优化并做出相应的功能扩展或者调整即可。</p>
<h2><a id="toc-54e" class="anchor" href="#toc-54e"></a>5. 难点突破</h2>
<p><img src="https://p3.ssl.qhimg.com/t014cae0f4c090e8bd6.png?size=1080x608" alt="alt"></p>
<p>开发过程所遇到的难点总体可以用以上三点来概括：首先基于WebAssembly 工具链（emscripten.org），借助EMSC编译器我们可以直接将一个C和C++编译成JS可用。这一过程本身存在诸多不便之处，主要是因为其本身对系统一些底层库的依赖或对于开发环境的要求，导致可移植性并没有那么好，当需要跨机器协作时容易出现诸多问题。现在我们内部的解决方案是自己找一台专用机器来配置做为编译发布使用。</p>
<p>第二点是队列管理与状态控制，只有精确实现队列管理与状态控制，我们才能保证整个程序能合理稳定的执行。</p>
<p>第三点就是项目构建打包，我们要解决前端一些构建打包的习惯以及其在逻辑需求上存在的一些冲突。</p>
<h2><a id="toc-c91" class="anchor" href="#toc-c91"></a>6. 未来展望</h2>
<p><img src="https://p3.ssl.qhimg.com/t0182a1d268b0d1736b.png?size=1080x608" alt="alt"></p>
<p>展望未来，我希望未来浏览器能对HEVC有更加出色的支持。本次分享虽然是一个播放器，但我们知道FFmpeg的能力不只是解码播放，还可以做更多实用工具的发掘实现。同时我也希望未来媒体类型百花齐放，甚至私有编解码也能够形成Web端场景更规范灵活的解决方案。WASM成熟、标准化完善、各业务领域对应解决能力的细分，也是很值得期待的一件事情；而回到播放器本身，字幕、AI、互动交互等都是能进一步提升音视频播放服务的可玩性与用户体验方向值得研究的方向。</p>

            ]]></description>
            <pubDate>Fri, 10 Apr 2020 02:54:52 GMT</pubDate>
            <guid>http://blog.pyzy.net/post/qhww.html</guid>
        </item>
        
        <item>
            <title>纯CSS实现滚动的DOM边框</title>
            <link>http://blog.pyzy.net/post/shine.html</link>
            <description><![CDATA[
            <div class="toc"></div><p>有时候，我们的产品交互上需要一些特别的效果，比如一个可编辑区域往往会添加一些动态描边效果，那么下面这个可能不失为一个不错的备选方案。</p>
<p>废话不说，直接上效果：</p>
<iframe src="https://code.h5jun.com/guhan/edit?html,output" style="border:0;width:100%;height:400px"></iframe>


<p>代码：</p>
<pre><code class="hljs lang-html"><span class="hljs-meta">&lt;!DOCTYPE html&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>JS Bin<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="undefined">
    .box{
      position: relative;
      width:128px;
      height: 128px;
      1overflow: hidden;
    }
    .content {
      position: absolute;
      left:1px;
      top:1px;
      right:1px;
      bottom:1px;
      background:#fff;
    }
    .box:after{
      content:'';
      position: absolute;
      z-index: -1;
      left:0;
      bottom:0;
      top:1px;
      right:1px;
      background: repeating-linear-gradient(135deg, transparent, transparent 3px, #000 3px, #fff 8px);
      animation: shine 1s infinite linear;
    }
    .box:before{
      content:'';
      position: absolute;
      z-index: -1;
      left:1px;
      bottom:1px;
      top:0;
      right:0;
      background: repeating-linear-gradient(135deg, transparent, transparent 3px, #fff 3px, #000 8px);
      animation: shine2 1s infinite linear;
    }
    @keyframes shine {
      0% { background-position: -1px -1px;}
      100% { background-position: -12px -12px;}
    }

    @keyframes shine2 {
      0% { background-position: -12px -12px;}
      100% { background-position: -1px -1px;}
    }
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"box"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"content"</span>&gt;</span>内容占位<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>

            ]]></description>
            <pubDate>Wed, 27 Feb 2019 09:46:15 GMT</pubDate>
            <guid>http://blog.pyzy.net/post/shine.html</guid>
        </item>
        
    </channel>
</rss>
