Jekyll2020-07-22T02:49:29+00:00http://daner1990.github.io/atom.xml自留地留一亩地,自耕自种,静待开花DannielWeb 字体最佳实践2020-07-22T00:00:00+00:002020-07-22T00:00:00+00:00http://daner1990.github.io/web/2020/07/22/%E4%BA%92%E8%81%94%E7%BD%91%E5%AE%89%E5%85%A8%E5%AD%97%E4%BD%93<p><img src="http://upload-images.jianshu.io/upload_images/1665040-7dbfec5bc0c6c8c9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="据说把名字取得特别长更容易被搜索到" /></p>
<p>据说把名字取得特别长更容易被搜索到。</p>
<p>故事的起源,要从UED界两大种族前端设计师和视觉设计师的爱恨说起。</p>
<p>下面是设计师的视觉稿:</p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-6ae27b1a9a35ce09.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="设计师的视觉稿" /></p>
<p>下面是前端开发出来的真实效果:</p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-956bb7caf993d063.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="前端开发出来的真实效果" /></p>
<p>于是战争爆发了:</p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-fd5af7c83a40fd9d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="于是战争爆发了" /></p>
<p>像这种视觉效果不一致问题,在日常开发中比比皆是。最近遇到的比较多的是字体问题,开了写轮眼的设计师经常抱怨手机上的字体跟设计稿不一致,前端只能无奈的回一句手机上没这字体啊…然而实际情况远比这个复杂,正义的王二小见此情况决定挺身而出,踏上了Web字体修真之路,来寻找传说中的最优解。</p>
<h2 id="从0开始">从0开始</h2>
<p>这将是一次冒险,我们从0开始探索网页中的文字是如何一步步呈现在我们眼前的。计算机的数据,本质上都是由01组成的序列,不同的序列可以传达不同的信息,而同样的序列通过不同的编码和解码方式也会传达不同的信息。</p>
<p>我们所看到的网页,都是从服务端网络传输而来的一个个数据包通过浏览器解析而成,网络传输其实是一个很复杂的编码解码过程,你可能听过数据段,报文,分组,数据包,数据帧等关键词,这些术语其实只是OSI模型中各个层对数据单位的不同划分,最底层的表示还是以bit为单位的01。假设浏览器现在要渲染一段文本,它从服务端收到的数据包有一段信息是这样的(当然为了简化,除去报文头等信息,假设下面这段信息就页面上要展示的文本信息):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>11100101 10001010 10101000
11100110 10000100 10011111
11100101 10110000 10001111
11100101 10001001 10001101
11100111 10101011 10101111
</code></pre></div></div>
<p>这是一串字节流,浏览器得到它的第一件事自然是解码,那么第一个问题,编码方式很多种,浏览器怎么知道用哪种方式去解码呢?</p>
<h2 id="编码与解码">编码与解码</h2>
<p>我们所熟知的编码方式有ASCII,GB2321,UTF-8,UTF-16等等,对于浏览器来说,它会按照以下规则去寻找数据的编码类型:</p>
<ol>
<li>Web 服务器返回的 HTTP 头中的 Content-Type: text/html; charset=“xxx”。其中charset=“xxx”就是编码方式,当浏览器拿到这个信息之后,就能愉快的解码了;</li>
<li>如果服务端没有指定编码方式,浏览器会去网页文件的head中查找信息,来确定编码方式;</li>
<li>如果还没找到,那浏览器就只能自行判断编码了,或者让用户设置解码方式。</li>
</ol>
<p>可以看到,前两步信息都是确定的,只有第三步是无法确定编码方式的。所以为了让你的页面能正常展示出来,一定得要在前两步就设定好charset编码方式,以便于浏览器以你期望的方式解码。</p>
<p>现代网页通常都使用utf-8的编码方式,所以我们就以此为例。utf-8是unicode字符集的一种实现方式,unicode本质上就是一个表,一个将二进制数据映射到各种文字符号的表,这个表野心很大,想要囊括世界上所有文字符号,并且他也实现了自己的目标,所以它也成了网络世界应用最广泛的一个表。</p>
<p>假设上面那串字节流采用了utf-8编码,那么根据utf-8字节流到unicode的编码规则:</p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-49cf3dbde21a08ab.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="Unicode~UTF-8映射表" /></p>
<p>可以看到上面那段字节流全都是1110xxxx 10xxxxxx 10xxxxxx的形式,那么根据表中第三行映射关系,应该是3个utf-8字节对应1个unicode编码,将三个字节中的16个x用两个字节表示,然后转化成十六进制的unicode表示,最终可得到以下结果:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>11100101 10001010 10101000 -> 01010010 10101000 -> \u52a8
11100110 10000100 10011111 -> 01100001 00011111 -> \u611f
11100101 10110000 10001111 -> 01011100 00001111 -> \u5c0f
11100101 10001001 10001101 -> 01010010 01001101 -> \u524d
11100111 10101011 10101111 -> 01111010 11101111 -> \u7aef
</code></pre></div></div>
<p>得到unicode编码之后,我们就可以根据unicode字符表找到对应的文字符号,最终得到了以下结果:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>\u52a8\u611f\u5c0f\u524d\u7aef -> 动感小前端
</code></pre></div></div>
<p>如果对最终的结果不确定,可以反向验证一下:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">escape</span><span class="p">(</span><span class="dl">'</span><span class="s1">动感小前端</span><span class="dl">'</span><span class="p">)</span> <span class="c1">// "%u52A8%u611F%u5C0F%u524D%u7AEF"</span>
</code></pre></div></div>
<p>得出的unicode字符数值完全一致,看来计算没错,那么紧接着第二个问题来了,浏览器该如何去展示它?就好比我知道你的名字叫什么,但并不知道怎么写一样。</p>
<h2 id="寻找字体">寻找字体</h2>
<p>字体的渲染是一个很复杂的过程,首先我们需要知道在Web世界中存在着五大字体家族,江湖人称font-family:serif、sans-serif、monospace、cursive和fantasy。在这五大家族下面,又演变出各个不同的字体,比如宋体,微软雅黑,Arial,Helvetica等等。同样的文字,在不同的字体下面会呈现出不同的效果:</p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-807e55c98bf3e11c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="相同大小粗细但不同字体的差别" /></p>
<p>但是,不管是什么字体,他们本质上都是一个表。你可以把这个表理解成三个部分:</p>
<ul>
<li>轮廓:用来记载字符的形状;</li>
<li>编码:用来记载字符内部编号与字符形状以及unicode编码之间的映射关系;</li>
<li>封装:将上面这些东西封装成特定的文件格式</li>
</ul>
<blockquote>
<p>想要深入了解字体内部原理,请走支线剧情<a href="https://book.douban.com/subject/2362953/">《Fonts & Encodings》</a></p>
</blockquote>
<p>浏览器在渲染字体时,首先会把这些文字分为不同语言的小段,然后依次确定该用哪一种字体,确定之后按照字符的unicode编码在字体中匹配相应的轮廓,并最终渲染在屏幕上。通常我们都会给页面指定一套字体规则:</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">font-family</span><span class="o">:</span> <span class="nt">Helvetica</span><span class="o">,</span> <span class="nt">STXihei</span><span class="o">,</span> <span class="s1">"Microsoft YaHei"</span><span class="o">,</span> <span class="nt">Arial</span><span class="o">,</span> <span class="nt">SimSun</span><span class="o">,</span><span class="nt">sans-serif</span><span class="o">;</span>
</code></pre></div></div>
<p>浏览器会按照字体声明的顺序依次去寻找系统中已安装的字体,如果找到了就按照该字体渲染,没找到则依次往后查找,如果最后还是没找到,则使用浏览器设置的<strong>神秘的默认字体</strong>。</p>
<h2 id="渲染排版">渲染排版</h2>
<p>确定了字体之后,浏览器就真的要去渲染了。如果你以为把字体设置的一样就能万事大吉了,那就太天真了。即使是相同的字体,不同的环境下渲染出来的结果也是不一样的!就好比同样是须佐能乎,不同人产生的形态也是不一样的,先看两张图:</p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-44c01567d212cce1.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="相同字体在不同环境下的效果" /></p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-90539a9776d8145d.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="放大后的对比效果" /></p>
<p>这是同一个页面在不同环境下的显示效果,其实如果在真实环境下看的话基本看不出来差别,但是对比一看差别还是很明显的。MBP下是retina屏,显示效果更细腻一些,而MBA下则更厚重些。放大来看,MBP下字体边缘有灰色的边缘(灰度渲染),而MBA下则是彩色的边缘(次像素渲染)。</p>
<p>可以看到,同样是Mac系统+Chrome浏览器,只是版本号稍微不同,渲染效果就会有所差别。更别说在Windows和Android上了。那么造成这种差异的原因是什么呢?</p>
<h4 id="排版引擎">排版引擎</h4>
<p>不同浏览器有着不同的渲染引擎,不同的操作系统上面也有不同的文字排版引擎,而浏览器在渲染页面文本的时候都会调用系统的文字排版引擎。不同的排版策略就会造成不同的渲染结果。</p>
<p>Mac使用的排版引擎为CoreText,Windows7为DirectWrite/GDI,Windows XP则使用GDI。我们不会深入探索各个排版引擎的原理(想要深入了解Web字体渲染知识,可以去<a href="http://blog.typekit.com/2010/10/05/type-rendering-on-the-web/">Typekit</a>上了解更多),只需要知道不同的渲染引擎可能会造成字体有细节上的差异。即使是同一种渲染引擎,采用不同的渲染策略,比如灰度渲染和亚像素渲染,得出的效果也是不一样的。</p>
<p>Core Text 渲染引擎:</p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-1da702bbfc543f5a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="Core Text 渲染引擎" /></p>
<p>DirectWrite渲染引擎:</p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-f571d8243cb139c4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="DirectWrite渲染引擎" /></p>
<p>GDI渲染引擎,开启标准抗锯齿:</p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-584d0b7d4257c95e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="GDI渲染引擎,开启标准抗锯齿" /></p>
<p>GDI渲染引擎,无抗锯齿:</p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-7dc48972419e984d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="GDI渲染引擎,无抗锯齿" /></p>
<p>由此可看出排版引擎渲染策略的差异是造成字体显示效果不一致的根本原因之一,但是这种差异非常之小,对于普通用户来说,根本不会注意到这些细节,所以前端工程师大可不必为此操心。</p>
<p>至此,我们终于走完文字从0渲染到屏幕上的整个过程。</p>
<h2 id="诸子百家">诸子百家</h2>
<p>之前有提到,当浏览器没有找到所声明的字体时,会使用默认字体。问题就在于,这个默认字体到底是什么字体呢?不同设备之间的默认字体又分别是什么?影响默认字体的因素又有哪些呢?</p>
<p>在旧PC时代,统治人类的主要是windows和mac两大阵营,我们扳着手指头都能列出各大平台和浏览器上的默认字体。但是到了如今的无线乱世,安卓的开源让每个设备厂商都可能会有自己独特的默认字体,这对网页的视觉统一性又带来了巨大的挑战。</p>
<h4 id="裸奔字体">裸奔字体</h4>
<p>裸奔字体就是你的页面不设置任何样式,浏览器呈现出的默认字体,我写了个小<a href="https://aliqin.github.io/2016-08-21-web-font-1/aliquanfeng.com/demo/font.html">demo</a>,你可以点击试试看你浏览器上面的裸奔字体是啥,也可以扫码看看手机上的情况:</p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-9839a6fa2a39e9e4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="浏览器默认字体测试" /></p>
<p>在<a href="https://app.crossbrowsertesting.com/screenshots/1675917?size=small&type=windowed">CrossBrowserTesting</a>上跑了一下效果如下:</p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-f1170d657feb58a1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="Win8/OSX 部分浏览器对比" /></p>
<p>而在本人真机下的效果:</p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-7c24d04a0d44b9e1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="自上而下分别为Firefox,Safari,Chrome" /></p>
<p>很明显能看出,裸奔字体千变万化,根本不靠谱!</p>
<h4 id="安全字体">安全字体</h4>
<p>好在,现在已经没有人裸奔了,一般都会在页面中手动声明一下字体,比如百度首页是这样的:</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">body</span><span class="p">{</span><span class="nl">font</span><span class="p">:</span> <span class="m">12px</span> <span class="n">arial</span><span class="p">;}</span> <span class="o">//</span> <span class="err">写的这么精简是为了省流量么</span><span class="o">...</span>
</code></pre></div></div>
<p>谷歌首页是这样:</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">body</span><span class="p">{</span><span class="nl">font-family</span><span class="p">:</span> <span class="n">arial</span><span class="p">,</span><span class="nb">sans-serif</span><span class="p">;}</span> <span class="o">//</span> <span class="err">好歹加了字体族</span>
</code></pre></div></div>
<p>天猫首页是这样的:</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">body</span><span class="p">{</span><span class="nl">font</span><span class="p">:</span> <span class="m">12px</span><span class="p">/</span><span class="m">1.5</span> <span class="n">tahoma</span><span class="p">,</span><span class="n">arial</span><span class="p">,</span><span class="s1">"\5b8b\4f53"</span><span class="p">;}</span>
</code></pre></div></div>
<p>淘宝首页是这样的:</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">body</span><span class="p">{</span><span class="nl">font</span><span class="p">:</span> <span class="m">12px</span><span class="p">/</span><span class="m">1.5</span> <span class="n">tahoma</span><span class="p">,</span><span class="n">arial</span><span class="p">,</span><span class="s2">'Hiragino Sans GB'</span><span class="p">,</span><span class="s2">'\5b8b\4f53'</span><span class="p">,</span><span class="nb">sans-serif</span><span class="p">;}</span>
</code></pre></div></div>
<p>上面四种写法可能都有自己的考虑,但仅从终端字体表现的角度来看,很明显淘宝的写法更专业。Arial可谓是支持性最广的字体了,所以大家都用上了,这种被大多数系统所默认支持的字体,就是Web安全字体。</p>
<p><a href="http://www.cssfontstack.com/">CSS Font Stack</a>上有对Web安全字体的整理,建议设计师们在作图的时候多考虑一下,这样能一定程度上降低视觉差异。并且某些字体其实长得还是蛮像的,你还可以使用安全字体来代替长相相似的非安全字体。</p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-45dffe609bc17e97.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="Arial支持性最广,而Helvetica在Mac上100%支持" /></p>
<p>到目前为止,我们所做的一切考虑就是让页面字体效果在不同终端下尽可能保持一致,初步结论就是要使用安全字体,然而设计师并不这样想。设计师一般会使用逼格比较高的非安全字体,比如兰亭细黑,苹方字体。一旦浏览器发现系统没有这些字体,就会不断降级,最坏的情况,就是一直降级到默认字体。所以通常我们会在font-family最后加上一个默认的字体族,比如sans-serif,这样浏览器在最坏的情况下也能使用特定的字体族,并在该字体族下选择一名指定字体来展示。</p>
<p>那么在这些指定的种族背后,被选中的孩子们到底都有谁呢?</p>
<h4 id="神秘的默认字体">神秘的默认字体</h4>
<p>首先系统会默认安装一些字体,维基上有对Win/Mac内置字体的整理:</p>
<ul>
<li><a href="https://en.wikipedia.org/wiki/List_of_typefaces_included_with_Microsoft_Windows">Windows 字体列表</a></li>
<li><a href="https://en.wikipedia.org/wiki/List_of_typefaces_included_with_OS_X">Mac OS X 字体列表</a></li>
</ul>
<p>然后当你安装软件时,有可能会附带安装一些字体,这样你系统上能支持的字体又变多了。在上面那份列表中,Win/Mac共同支持的字体只有Arial, Verdana, Tahoma, Trebuchet MS, Georgia等少数Web安全字体,对于Win/Mac平台实际字体效果分析,请参考此文:</p>
<blockquote>
<p>跨平台字体效果浅析:https://isux.tencent.com/5058.html</p>
</blockquote>
<p>重点说下无线端, <a href="http://iosfonts.com/">iOS Fonts</a> 和<a href="http://iosfontlist.com/">iOS Font List</a>网站整理了一份各个版本的iOS字体清单,可以很方便的查出各版本支持情况:</p>
<p>Helvetica字体完美支持:</p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-41168fefdf385c50.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="Helvetica字体完美支持" /></p>
<p>苹方字体从 iOS 9 才开始支持:</p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-c0acab37107bf77f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="苹方字体从 iOS 9 才开始支持" /></p>
<p>虽然方便,但毕竟第三方网站,不排除数据有误的情况,于是附上官网声明的字体清单:</p>
<ul>
<li><a href="https://support.apple.com/zh-cn/HT4980">iOS 5:字体列表</a></li>
<li><a href="https://support.apple.com/zh-cn/HT202599">iOS 6:字体列表</a></li>
<li><a href="https://support.apple.com/zh-cn/HT202771">iOS 7:字体列表</a></li>
<li><a href="https://aliqin.github.io/2016-08-21-web-font-1/#"><del>iOS 8:字体列表</del></a></li>
<li><a href="https://aliqin.github.io/2016-08-21-web-font-1/#"><del>iOS 9:字体列表</del></a></li>
</ul>
<p>对于安卓,<strong>原生的安卓</strong>使用的是Droid Sans(英文/数字)和Droidsansfallback(中文),4.0后修改为Google的开源字体Roboto。而非原生安卓,实在没有总结性可言。比如小米和华为用了方正兰亭黑,锤子则使用了华文黑体,并且同一厂商下的不同手机品牌,同一品牌的不同型号默认字体都可能不同,不做展开。</p>
<p>一张图总结一下:</p>
<p><img src="http://7xp4vm.com1.z0.glb.clouddn.com/WX20170319-123733@2x.png" alt="各平台默认字体对比" /></p>
<p>哦,忘了还有YunOS,貌似是方正兰亭细黑…</p>
<h4 id="强大的自定义字体">强大的自定义字体</h4>
<p>是的,用户可以选择自己喜欢的字体。你永远不知道用户会干什么,什么安全字体,默认字体,一个主题包下来全都是浮云:</p>
<p>用户修改系统字体:</p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-d645d4ef24dcdfbf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="用户修改系统字体" /></p>
<p>当然这不是最绝的,换个字体最多样子变了,最绝的是用户开启老人机模式,放大字体!</p>
<p>普通-放大模式对比:</p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-f0e57cbd6932550f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="普通-放大模式对比" /></p>
<p>这两招一出,基本会给设计师和前端造成10000+伤害,不过我们仍然可以做点什么:</p>
<ul>
<li>严格控制页面布局,字体超出部分截断,保证页面正常显示;</li>
<li>监测页面缩放情况并给予用户提示;</li>
<li>页面自适应或者,针对老人模式单独开发一套页面。</li>
</ul>
<h2 id="小结">小结</h2>
<p>看到这里王二小已经残血,稍微修整总结一下,字体表现不一致的根本原因有: - 排版引擎渲染策略差异(影响小,不可规避) - 各终端默认字体设置差异(影响中,可规避) - 用户手动设置自定义字体(影响大,不可控)</p>
<p>目前为止我们能做的就是尽量使用Web 安全字体,针对不同终端对font-family字体选择顺序进行优雅降级,并设置默认字体族来规避风险。</p>
<p>但只做到这些还远远不够,我们完全处于被动状态,一切都依赖于终端环境的字体情况,并且还没考虑到字体格式,中英混排,字体动画,字体优化,Web标准技术等方面。接下来我们要主动出击,站在巨人的肩膀上去各个击破,打怪升级,去寻找Web字体应用最佳实践之道。</p>
<p>冒险越来越深入了,等待王二小的将会是什么呢?请看下集:</p>
<p><img src="http://upload-images.jianshu.io/upload_images/1665040-f0c2c4a760fed16e.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="美丽新世界" /></p>
<h2 id="参考资料">参考资料</h2>
<p>以下是相关参考资料,若想深入了解,建议仔细研读。</p>
<h5 id="web-字体的选择和运用">Web 字体的选择和运用</h5>
<p>https://blog.coding.net/blog/Web-Fonts</p>
<h5 id="网页字体优化">网页字体优化</h5>
<p>https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/webfont-optimization?hl=zh-cn</p>
<h5 id="字体的各个概念术语">字体的各个概念术语</h5>
<p>http://www.zhihu.com/question/20366900</p>
<h5 id="字体渲染相关">字体渲染相关</h5>
<ul>
<li>http://ued.ctrip.com/blog/font-rendering.html</li>
<li>http://blog.jobbole.com/21671/</li>
<li>http://isux.tencent.com/website-font-rendering-process.html</li>
<li>https://developer.apple.com/fonts/TrueType-Reference-Manual/RM02/Chap2.html</li>
</ul>
<h5 id="typekit-web字体渲染系列文章">Typekit Web字体渲染系列文章</h5>
<ul>
<li>http://blog.typekit.com/2010/10/05/type-rendering-on-the-web/</li>
<li>http://blog.typekit.com/2010/10/15/type-rendering-operating-systems/</li>
<li>http://blog.typekit.com/2010/10/21/type-rendering-web-browsers/</li>
<li>http://blog.typekit.com/2010/11/09/type-rendering-the-design-of-fonts-for-the-web/</li>
<li>http://blog.typekit.com/2010/12/08/type-rendering-font-outlines-and-file-formats/</li>
<li>http://blog.typekit.com/2010/12/14/a-closer-look-at-truetype-hinting/</li>
<li>http://blog.typekit.com/2010/12/17/type-rendering-review-and-fonts-that-render-well/</li>
</ul>
<h5 id="网页设计中默认字体详解">网页设计中默认字体详解</h5>
<p>https://waxdoll.gitbooks.io/webdesignfoundations/content/appendix/font_browser_default.html</p>
<h5 id="mac-os-x-字体列表">Mac OS X 字体列表</h5>
<p>https://en.wikipedia.org/wiki/List_of_typefaces_included_with_OS_X</p>
<h5 id="windows-字体列表">Windows 字体列表</h5>
<ul>
<li>https://en.wikipedia.org/wiki/List_of_typefaces_included_with_Microsoft_Windows</li>
<li>http://www.microsoft.com/typography/fonts/product.aspx</li>
</ul>
<h5 id="开源字体列表">开源字体列表</h5>
<p>https://en.wikipedia.org/wiki/Open-source_Unicode_typefaces</p>
<h5 id="css-font-stack">CSS Font Stack</h5>
<p>http://www.cssfontstack.com/</p>
<h5 id="数字设计之美">数字设计之美</h5>
<p>http://www.typeisbeautiful.com/2009/09/1467/</p>
<h5 id="跨平台字体效果浅析">跨平台字体效果浅析</h5>
<p>https://isux.tencent.com/5058.html</p>
<h5 id="对比-ios-系统-android-的字体渲染有何区别">对比 iOS 系统 Android 的字体渲染有何区别</h5>
<p>https://www.zhihu.com/question/21211748</p>
<h5 id="ios-font-字体整理">iOS Font 字体整理</h5>
<ul>
<li>http://iosfonts.com/</li>
<li>http://iosfontlist.com/</li>
</ul>
<h5 id="marsfont-family">Mars/font-family</h5>
<p>https://github.com/AlloyTeam/Mars/blob/master/solutions/font-family.md</p>
<h5 id="网页字体设置你了解吗">网页字体设置你了解吗?</h5>
<p>http://ued.ctrip.com/blog/web-page-font-settings-did-you-know.html</p>Danniel利用canvas进行图片文件压缩2020-04-24T00:00:00+00:002020-04-24T00:00:00+00:00http://daner1990.github.io/javascript/2020/04/24/%E5%88%A9%E7%94%A8canvas%E8%BF%9B%E8%A1%8C%E5%9B%BE%E7%89%87%E6%96%87%E4%BB%B6%E5%8E%8B%E7%BC%A9<h2 id="背景">背景</h2>
<p>对于大尺寸图片的上传,在前端进行压缩除了省流量外,最大的意义是极大的提高了用户体验。</p>
<p>这种体验包括两方面:</p>
<ol>
<li>由于上传图片尺寸比较小,因此上传速度会比较快,交互会更加流畅,同时大大降低了网络异常导致上传失败风险。</li>
<li>最最重要的体验改进点:省略了图片的再加工成本。很多网站的图片上传功能都会对图片的大小进行限制,尤其是头像上传,限制5M或者2M以内是非常常见的。然后现在的数码设备拍摄功能都非常出众,一张原始图片超过2M几乎是标配,此时如果用户想把手机或相机中的某个得意图片上传作为自己的头像,就会遇到因为图片大小限制而不能上传的窘境,不得不对图片进行再处理,而这种体验其实非常不好的。如果可以在前端进行压缩,则理论上对图片尺寸的限制是没有必要的。</li>
</ol>
<h2 id="技术储备">技术储备</h2>
<h3 id="canvas">canvas</h3>
<ul>
<li>
<p>canvas元素可以用来绘制图形以及图形动画</p>
</li>
<li>
<p>canvas暴露HTMLCanvasElement接口,包含了操作canvas元素布局和呈现的方法和属性,其中canvas.drawImage可以在canvas上绘制图片或者图片的一部分</p>
</li>
<li>
<p>可以通过HTMLCanvasElement.getContext(“2d”)获取canvas上下文进行布局,然后通过canvas.toDataURL()返回:data 的URL,或者通过canvas.toBlob()导出一个Blob文件</p>
</li>
<li>
<p><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLCanvasElement"><code class="language-plaintext highlighter-rouge">HTMLCanvasElement</code></a> ,<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Basic_usage"><code class="language-plaintext highlighter-rouge">Canvas教程</code></a></p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">test</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">canvas</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">canvas</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">toDataURL</span><span class="p">();</span>
<span class="c1">//canvas.toBlob(function(blob){...}, "image/jpeg", 0.95); // JPEG 质量为95%</span>
<span class="kd">var</span> <span class="nx">newImg</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">"</span><span class="s2">img</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">newImg</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">url</span><span class="p">;</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">newImg</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div> </div>
</li>
</ul>
<h3 id="blob">blob</h3>
<ul>
<li>Binary Large Object:二进制数据的大对象;原始数据,无法变更</li>
<li>使用字符串,或者对象创建blob,处理方式不同,用对象创建blob对象,会调用普通对象的toString()方法得到字符串数据再去创建blob对象</li>
<li>无法对blob对象进行编辑改变,但可以通过slice方法对blob进行分割,生成一个新的blob对象</li>
<li>使用场景有分片上传,以及BLob URL作为图片地址或者资源下载地址</li>
<li><a href="https://zh.javascript.info/blob"><code class="language-plaintext highlighter-rouge">blob 应用</code></a></li>
</ul>
<h3 id="urlcreateobjecturl">url.createObjectURL</h3>
<ul>
<li>URL接口有两个方法,URL.createObjectURL 创建一个DOMString,包含一个唯一的blob链接, URL.invokeObjectURL 销毁通过createObjectURL创建的URL实例,可以动态创建链接a进行blob下载</li>
</ul>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">link</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">a</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">link</span><span class="p">.</span><span class="nx">download</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">hello.txt</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">blob</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Blob</span><span class="p">([</span><span class="dl">'</span><span class="s1">Hello, world!</span><span class="dl">'</span><span class="p">],</span> <span class="p">{</span><span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/plain</span><span class="dl">'</span><span class="p">});</span>
<span class="nx">link</span><span class="p">.</span><span class="nx">href</span> <span class="o">=</span> <span class="nx">URL</span><span class="p">.</span><span class="nx">createObjectURL</span><span class="p">(</span><span class="nx">blob</span><span class="p">);</span>
<span class="c1">//URL.createObjectURL 取一个 Blob,并为其创建一个唯一的 URL,形式为 blob:<origin>/<uuid></span>
<span class="c1">//link.href = "blob:https://javascript.info/1e67e00e-860d-40a5-89ae-6ab0cbee6273"</span>
<span class="c1">//浏览器内部为每个通过 URL.createObjectURL 生成的 URL 存储了一个 URL → Blob 映射。因此,此类 URL 很短,但可以访问 Blob。</span>
<span class="nx">link</span><span class="p">.</span><span class="nx">click</span><span class="p">();</span>
<span class="nx">URL</span><span class="p">.</span><span class="nx">revokeObjectURL</span><span class="p">(</span><span class="nx">link</span><span class="p">.</span><span class="nx">href</span><span class="p">);</span>
<span class="c1">//生成的 URL(即其链接)仅在当前文档打开的状态下才有效。它允许引用 <img>、<a> 中的 Blob,以及基本上任何其他期望 URL 的对象。</span>
<span class="c1">//不过它有个副作用。虽然这里有 Blob 的映射,但 Blob 本身只保存在内存中的。浏览器无法释放它。</span>
<span class="c1">//应用程序寿命很长,那这个释放就不会很快发生。</span>
<span class="c1">//因此,如果我们创建一个 URL,那么即使我们不再需要该 Blob 了,它也会被挂在内存中。</span>
<span class="c1">//URL.revokeObjectURL(url) 从内部映射中移除引用,因此允许 Blob 被删除(如果没有其他引用的话),并释放内存</span>
</code></pre></div></div>
<h3 id="filereader">FileReader</h3>
<ul>
<li>fileReader对象允许web内置应用程序读取用户计算机中文件内容,或者原始数据缓冲区中的内容,已File 或者Blob的数据格式读取</li>
<li>不能直接通过路径读取,需要使用ajax解决方案进行服务器端文件读取</li>
<li>可以通过FileReader.readAsArrayBuffer,readerAsDataURL,readerAsBinaryString,readerAsText等方法读取Blob中内容再以不同形式进行输出</li>
</ul>
<h3 id="file">File</h3>
<ul>
<li>file对象是一种特殊格式的blob,通常,是用户通过input标签选择文件后返回的FileLIst对象,以及其他如DataTransfer对象;</li>
<li>可以把Blob文件生成File文件</li>
</ul>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">aBlob</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Blob</span><span class="p">(</span><span class="nx">array</span><span class="p">,</span> <span class="nx">options</span> <span class="p">);</span>
<span class="kd">var</span> <span class="nx">aFile</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Blob</span><span class="p">(</span><span class="nx">bits</span><span class="p">,</span><span class="nx">name</span><span class="p">,</span><span class="nx">options</span><span class="p">);</span>
<span class="c1">//bits :一个包含ArrayBuffer,ArrayBufferView,Blob,或者 DOMString 对象的 Array — 或者任何这些对象的组合。这是 UTF-8 编码的文件内容。</span>
<span class="kd">var</span> <span class="nx">aFileParts</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1"><a id="a"><b id="b">hey!</b></a></span><span class="dl">'</span><span class="p">];</span> <span class="c1">// 一个包含DOMString的数组</span>
<span class="kd">var</span> <span class="nx">oMyBlob</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Blob</span><span class="p">(</span><span class="nx">aFileParts</span><span class="p">,</span> <span class="p">{</span><span class="na">type</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">text/html</span><span class="dl">'</span><span class="p">});</span> <span class="c1">// 得到 blob</span>
<span class="kd">var</span> <span class="nx">file</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">File</span><span class="p">([</span><span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">],</span> <span class="dl">"</span><span class="s2">foo.txt</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
<span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">text/plain</span><span class="dl">"</span><span class="p">,</span>
<span class="p">});</span>
<span class="c1">//把Blob文件生成File文件,File最主要是比blob多了一个名字</span>
<span class="kd">let</span> <span class="nx">blobToFile</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">theBlob</span><span class="p">,</span> <span class="nx">fileName</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">File</span><span class="p">([</span><span class="nx">theBlob</span><span class="p">],</span> <span class="nx">fileName</span><span class="p">);</span>
<span class="c1">//var file2 = new File([blob], 'test.png', {type: 'image/png'});</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="文件压缩思路">文件压缩思路</h2>
<p>利用HTML5 File API加上canvas实现前端图片JS压缩功能,核心是使用canvas的drawImage方法</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">context</span><span class="p">.</span><span class="nx">drawImage</span><span class="p">(</span><span class="nx">img</span><span class="p">,</span> <span class="nx">dx</span><span class="p">,</span> <span class="nx">dy</span><span class="p">);</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">drawImage</span><span class="p">(</span><span class="nx">img</span><span class="p">,</span> <span class="nx">dx</span><span class="p">,</span> <span class="nx">dy</span><span class="p">,</span> <span class="nx">dWidth</span><span class="p">,</span> <span class="nx">dHeight</span><span class="p">);</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">drawImage</span><span class="p">(</span><span class="nx">img</span><span class="p">,</span> <span class="nx">sx</span><span class="p">,</span> <span class="nx">sy</span><span class="p">,</span> <span class="nx">sWidth</span><span class="p">,</span> <span class="nx">sHeight</span><span class="p">,</span> <span class="nx">dx</span><span class="p">,</span> <span class="nx">dy</span><span class="p">,</span> <span class="nx">dWidth</span><span class="p">,</span> <span class="nx">dHeight</span><span class="p">);</span>
</code></pre></div></div>
<ul>
<li>
<p><strong>img</strong></p>
<p>就是图片对象,可以是页面上获取的DOM对象,也可以是虚拟DOM中的图片对象。</p>
</li>
<li>
<p><strong>dx, dy, dWidth, dHeight</strong></p>
<p>表示在<code class="language-plaintext highlighter-rouge">canvas</code>画布上规划处一片区域用来放置图片,<code class="language-plaintext highlighter-rouge">dx, dy</code>为canvas元素的左上角坐标,<code class="language-plaintext highlighter-rouge">dWidth, dHeight</code>指canvas元素上用在显示图片的区域大小。如果没有指定<code class="language-plaintext highlighter-rouge">sx,sy,sWidth,sHeight</code>这4个参数,则图片会被拉伸或缩放在这片区域内。</p>
</li>
<li>
<p><strong>sx,sy,swidth,sheight</strong></p>
<p>这4个坐标是针对图片元素的,表示图片在<code class="language-plaintext highlighter-rouge">canvas</code>画布上显示的大小和位置。<code class="language-plaintext highlighter-rouge">sx,sy</code>表示图片上<code class="language-plaintext highlighter-rouge">sx,sy</code>这个坐标作为左上角,然后往右下角的<code class="language-plaintext highlighter-rouge">swidth,sheight</code>尺寸范围图片作为最终在canvas上显示的图片内容。</p>
</li>
</ul>
<p><code class="language-plaintext highlighter-rouge">drawImage()</code>方法有一个非常怪异的地方,大家一定要注意,那就是5参数和9参数里面参数位置是不一样的,这个和一般的API有所不同。一般API可选参数是放在后面。但是,这里的<code class="language-plaintext highlighter-rouge">drawImage()</code>9个参数时候,可选参数<code class="language-plaintext highlighter-rouge">sx,sy,swidth,sheight</code>是在前面的。如果不注意这一点,有些表现会让你无法理解。</p>
<p><img src="https://image.zhangxinxu.com/image/blog/201711/Canvas_drawimage.jpg" alt="Canvas drawimage()原理示意" /></p>
<p>对于本文的图片压缩,需要用的是是5个参数语法。举个例子,一张图片(假设图片对象是<code class="language-plaintext highlighter-rouge">img</code>)的原始尺寸是4000<em>3000,现在需要把尺寸限制为400</em>300大小,很简单,原理如下代码示意:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">canvas</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">canvas</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">context</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">getContext</span><span class="p">(</span><span class="dl">'</span><span class="s1">2d</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="mi">400</span><span class="p">;</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">height</span> <span class="o">=</span> <span class="mi">300</span><span class="p">;</span>
<span class="c1">// 核心JS就这个</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">drawImage</span><span class="p">(</span><span class="nx">img</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">400</span><span class="p">,</span><span class="mi">300</span><span class="p">);</span>
</code></pre></div></div>
<p>把一张大的图片,直接画在一张小小的画布上。此时大图片就天然变成了小图片,压缩就这么实现了,是不是简单的有点超乎想象。</p>
<h2 id="实践思路">实践思路</h2>
<p>重点要解决图片来源和图片去向的问题</p>
<h3 id="1-如何把系统中图片呈现在浏览器中">1. 如何把系统中图片呈现在浏览器中?</h3>
<p>HTML5 file API可以让图片在上传之前直接在浏览器中显示,通常使用<code class="language-plaintext highlighter-rouge">FileReader</code>方法,代码示意如下:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">reader</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FileReader</span><span class="p">(),</span> <span class="nx">img</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Image</span><span class="p">();</span>
<span class="c1">// 读文件成功的回调</span>
<span class="nx">reader</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// e.target.result就是图片的base64地址信息</span>
<span class="nx">img</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">result</span><span class="p">;</span>
<span class="p">};</span>
<span class="nx">eleFile</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">change</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">reader</span><span class="p">.</span><span class="nx">readAsDataURL</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">files</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
<span class="p">});</span>
</code></pre></div></div>
<p>于是,包含图片信息的<code class="language-plaintext highlighter-rouge">context.drawImage()</code>方法中的<code class="language-plaintext highlighter-rouge">img</code>图片就有了。</p>
<h3 id="2-如果把canvas画布转换成img图像"><strong>2. 如果把canvas画布转换成img图像</strong></h3>
<p><code class="language-plaintext highlighter-rouge">canvas</code>天然提供了2个转图片的方法,一个是:</p>
<ul>
<li>
<p><strong>canvas.toDataURL()方法</strong></p>
<p>语法如下:<code class="language-plaintext highlighter-rouge">canvas.toDataURL(mimeType, qualityArgument)</code>可以把图片转换成base64格式信息,纯字符的图片表示法。其中: <code class="language-plaintext highlighter-rouge">mimeType</code>表示<code class="language-plaintext highlighter-rouge">canvas</code>导出来的<code class="language-plaintext highlighter-rouge">base64</code>图片的类型,默认是png格式,也即是默认值是<code class="language-plaintext highlighter-rouge">'image/png'</code>,我们也可以指定为jpg格式<code class="language-plaintext highlighter-rouge">'image/jpeg'</code>或者webp等格式。<code class="language-plaintext highlighter-rouge">file</code>对象中的<code class="language-plaintext highlighter-rouge">file.type</code>就是文件的mimeType类型,在转换时候正好可以直接拿来用(如果有file对象)。 <code class="language-plaintext highlighter-rouge">qualityArgument</code>表示导出的图片质量,只要导出为<code class="language-plaintext highlighter-rouge">jpg</code>和<code class="language-plaintext highlighter-rouge">webp</code>格式的时候此参数才有效果,默认值是<code class="language-plaintext highlighter-rouge">0.92</code>,是一个比较合理的图片质量输出参数,通常情况下,我们无需再设定。</p>
</li>
<li>
<p><strong>canvas.toBlob()方法</strong></p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob">语法</a>如下:<code class="language-plaintext highlighter-rouge">canvas.toBlob(callback, mimeType, qualityArgument)</code>可以把canvas转换成<a href="http://www.zhangxinxu.com/wordpress/2013/10/understand-domstring-document-formdata-blob-file-arraybuffer/">Blob文件</a>,通常用在文件上传中,因为是二进制的,对后端更加友好。和<code class="language-plaintext highlighter-rouge">toDataURL()</code>方法相比,<code class="language-plaintext highlighter-rouge">toBlob()</code>方法是异步的,因此多了个<code class="language-plaintext highlighter-rouge">callback</code>参数,这个<code class="language-plaintext highlighter-rouge">callback</code>回调方法默认的第一个参数就是转换好的<code class="language-plaintext highlighter-rouge">blob</code>文件信息,本文demo的文件上传就是将<code class="language-plaintext highlighter-rouge">canvas</code>图片转换成二进制的<code class="language-plaintext highlighter-rouge">blob</code>文件,然后再<code class="language-plaintext highlighter-rouge">ajax</code>上传的,代码如下:<code class="language-plaintext highlighter-rouge">// canvas转为blob并上传 canvas.toBlob(function (blob) { // 图片ajax上传 var xhr = new XMLHttpRequest(); // 开始上传 xhr.open("POST", 'upload.php', true); xhr.send(blob); });</code></p>
</li>
</ul>
<p>于是,经过“图片→canvas压缩→图片”三步曲,我们完成了图片前端压缩并上传的功能。</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">HTML</span><span class="err">代码:</span>
<span class="o"><</span><span class="nx">input</span> <span class="nx">id</span><span class="o">=</span><span class="dl">"</span><span class="s2">file</span><span class="dl">"</span> <span class="nx">type</span><span class="o">=</span><span class="dl">"</span><span class="s2">file</span><span class="dl">"</span><span class="o">></span>
<span class="nx">JS</span><span class="err">代码:</span>
<span class="kd">var</span> <span class="nx">eleFile</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">#file</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">// 压缩图片需要的一些元素和对象</span>
<span class="kd">var</span> <span class="nx">reader</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FileReader</span><span class="p">(),</span> <span class="nx">img</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Image</span><span class="p">();</span>
<span class="c1">// 选择的文件对象</span>
<span class="kd">var</span> <span class="nx">file</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="c1">// 缩放图片需要的canvas</span>
<span class="kd">var</span> <span class="nx">canvas</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">canvas</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">context</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">getContext</span><span class="p">(</span><span class="dl">'</span><span class="s1">2d</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">// base64地址图片加载完毕后</span>
<span class="nx">img</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
<span class="c1">// 图片原始尺寸</span>
<span class="kd">var</span> <span class="nx">originWidth</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">width</span><span class="p">;</span>
<span class="kd">var</span> <span class="nx">originHeight</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">height</span><span class="p">;</span>
<span class="c1">// 最大尺寸限制</span>
<span class="kd">var</span> <span class="nx">maxWidth</span> <span class="o">=</span> <span class="mi">400</span><span class="p">,</span> <span class="nx">maxHeight</span> <span class="o">=</span> <span class="mi">400</span><span class="p">;</span>
<span class="c1">// 目标尺寸</span>
<span class="kd">var</span> <span class="nx">targetWidth</span> <span class="o">=</span> <span class="nx">originWidth</span><span class="p">,</span> <span class="nx">targetHeight</span> <span class="o">=</span> <span class="nx">originHeight</span><span class="p">;</span>
<span class="c1">// 图片尺寸超过400x400的限制</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">originWidth</span> <span class="o">></span> <span class="nx">maxWidth</span> <span class="o">||</span> <span class="nx">originHeight</span> <span class="o">></span> <span class="nx">maxHeight</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">originWidth</span> <span class="o">/</span> <span class="nx">originHeight</span> <span class="o">></span> <span class="nx">maxWidth</span> <span class="o">/</span> <span class="nx">maxHeight</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// 更宽,按照宽度限定尺寸</span>
<span class="nx">targetWidth</span> <span class="o">=</span> <span class="nx">maxWidth</span><span class="p">;</span>
<span class="nx">targetHeight</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">round</span><span class="p">(</span><span class="nx">maxWidth</span> <span class="o">*</span> <span class="p">(</span><span class="nx">originHeight</span> <span class="o">/</span> <span class="nx">originWidth</span><span class="p">));</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">targetHeight</span> <span class="o">=</span> <span class="nx">maxHeight</span><span class="p">;</span>
<span class="nx">targetWidth</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">round</span><span class="p">(</span><span class="nx">maxHeight</span> <span class="o">*</span> <span class="p">(</span><span class="nx">originWidth</span> <span class="o">/</span> <span class="nx">originHeight</span><span class="p">));</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c1">// canvas对图片进行缩放</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="nx">targetWidth</span><span class="p">;</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">height</span> <span class="o">=</span> <span class="nx">targetHeight</span><span class="p">;</span>
<span class="c1">// 清除画布</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">clearRect</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">targetWidth</span><span class="p">,</span> <span class="nx">targetHeight</span><span class="p">);</span>
<span class="c1">// 图片压缩</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">drawImage</span><span class="p">(</span><span class="nx">img</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">targetWidth</span><span class="p">,</span> <span class="nx">targetHeight</span><span class="p">);</span>
<span class="c1">// canvas转为blob并上传</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">toBlob</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">blob</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// 图片ajax上传</span>
<span class="kd">var</span> <span class="nx">xhr</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">XMLHttpRequest</span><span class="p">();</span>
<span class="c1">// 文件上传成功</span>
<span class="nx">xhr</span><span class="p">.</span><span class="nx">onreadystatechange</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">xhr</span><span class="p">.</span><span class="nx">status</span> <span class="o">==</span> <span class="mi">200</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// xhr.responseText就是返回的数据</span>
<span class="p">}</span>
<span class="p">};</span>
<span class="c1">// 开始上传</span>
<span class="nx">xhr</span><span class="p">.</span><span class="nx">open</span><span class="p">(</span><span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span> <span class="dl">'</span><span class="s1">upload.php</span><span class="dl">'</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
<span class="nx">xhr</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">blob</span><span class="p">);</span>
<span class="p">},</span> <span class="nx">file</span><span class="p">.</span><span class="nx">type</span> <span class="o">||</span> <span class="dl">'</span><span class="s1">image/png</span><span class="dl">'</span><span class="p">);</span>
<span class="p">};</span>
<span class="c1">// 文件base64化,以便获知图片原始尺寸</span>
<span class="nx">reader</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">img</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">result</span><span class="p">;</span>
<span class="p">};</span>
<span class="nx">eleFile</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">change</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">file</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">files</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
<span class="c1">// 选择的文件是图片</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">file</span><span class="p">.</span><span class="nx">type</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">"</span><span class="s2">image</span><span class="dl">"</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">reader</span><span class="p">.</span><span class="nx">readAsDataURL</span><span class="p">(</span><span class="nx">file</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>
<h2 id="compressorjs源码解读">CompressorJS源码解读</h2>
<p><a href="https://github.com/fengyuanchen/compressorjs/blob/master/dist/compressor.js"><code class="language-plaintext highlighter-rouge">compressorJS源码</code></a></p>
<h3 id="使用">使用</h3>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o"><</span><span class="nx">input</span> <span class="nx">type</span><span class="o">=</span><span class="dl">"</span><span class="s2">file</span><span class="dl">"</span> <span class="nx">id</span><span class="o">=</span><span class="dl">"</span><span class="s2">file</span><span class="dl">"</span> <span class="nx">accept</span><span class="o">=</span><span class="dl">"</span><span class="s2">image/*</span><span class="dl">"</span><span class="o">></span>
<span class="k">import</span> <span class="nx">axios</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">axios</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">Compressor</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">compressorjs</span><span class="dl">'</span><span class="p">;</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">file</span><span class="dl">'</span><span class="p">).</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">change</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">file</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">files</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">file</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">new</span> <span class="nx">Compressor</span><span class="p">(</span><span class="nx">file</span><span class="p">,</span> <span class="p">{</span>
<span class="na">quality</span><span class="p">:</span> <span class="mf">0.6</span><span class="p">,</span>
<span class="na">maxHeight</span><span class="p">:</span><span class="mi">526</span><span class="p">,</span>
<span class="na">minHeight</span><span class="p">:</span><span class="mi">216</span><span class="p">,</span>
<span class="nx">success</span><span class="p">(</span><span class="nx">result</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">formData</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FormData</span><span class="p">();</span>
<span class="c1">// The third parameter is required for server</span>
<span class="nx">formData</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">'</span><span class="s1">file</span><span class="dl">'</span><span class="p">,</span> <span class="nx">result</span><span class="p">,</span> <span class="nx">result</span><span class="p">.</span><span class="nx">name</span><span class="p">);</span>
<span class="c1">// Send the compressed image file to server with XMLHttpRequest.</span>
<span class="nx">axios</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="dl">'</span><span class="s1">/path/to/upload</span><span class="dl">'</span><span class="p">,</span> <span class="nx">formData</span><span class="p">).</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Upload success</span><span class="dl">'</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">},</span>
<span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span>
<span class="p">},</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<h3 id="重点代码">重点代码</h3>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* Transform array buffer to Data URL.
* @param {ArrayBuffer} arrayBuffer - The array buffer to transform.
* @param {string} mimeType - The mime type of the Data URL.
* @returns {string} The result Data URL.
*/</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nx">arrayBufferToDataURL</span><span class="p">(</span><span class="nx">arrayBuffer</span><span class="p">,</span> <span class="nx">mimeType</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">chunks</span> <span class="o">=</span> <span class="p">[];</span>
<span class="kd">const</span> <span class="nx">chunkSize</span> <span class="o">=</span> <span class="mi">8192</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">uint8</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Uint8Array</span><span class="p">(</span><span class="nx">arrayBuffer</span><span class="p">);</span>
<span class="k">while</span> <span class="p">(</span><span class="nx">uint8</span><span class="p">.</span><span class="nx">length</span> <span class="o">></span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// XXX: Babel's `toConsumableArray` helper will throw error in IE or Safari 9</span>
<span class="c1">// eslint-disable-next-line prefer-spread</span>
<span class="nx">chunks</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">fromCharCode</span><span class="p">.</span><span class="nx">apply</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">toArray</span><span class="p">(</span><span class="nx">uint8</span><span class="p">.</span><span class="nx">subarray</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">chunkSize</span><span class="p">))));</span>
<span class="nx">uint8</span> <span class="o">=</span> <span class="nx">uint8</span><span class="p">.</span><span class="nx">subarray</span><span class="p">(</span><span class="nx">chunkSize</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="s2">`data:</span><span class="p">${</span><span class="nx">mimeType</span><span class="p">}</span><span class="s2">;base64,</span><span class="p">${</span><span class="nx">btoa</span><span class="p">(</span><span class="nx">chunks</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">''</span><span class="p">))}</span><span class="s2">`</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nx">init</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">file</span><span class="p">,</span> <span class="nx">options</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
<span class="c1">//压缩文件 为Blob格式</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">isBlob</span><span class="p">(</span><span class="nx">file</span><span class="p">))</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">fail</span><span class="p">(</span><span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">The first argument must be a File or Blob object.</span><span class="dl">'</span><span class="p">));</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//获取文件type值</span>
<span class="kd">const</span> <span class="nx">mimeType</span> <span class="o">=</span> <span class="nx">file</span><span class="p">.</span><span class="nx">type</span><span class="p">;</span>
<span class="c1">//判断是否是图片类型</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">isImageType</span><span class="p">(</span><span class="nx">mimeType</span><span class="p">))</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">fail</span><span class="p">(</span><span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">The first argument must be an image File or Blob object.</span><span class="dl">'</span><span class="p">));</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">//通用处理文件URL,FileReader的属性是否存在</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">URL</span> <span class="o">||</span> <span class="o">!</span><span class="nx">FileReader</span><span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">fail</span><span class="p">(</span><span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">The current browser does not support image compression.</span><span class="dl">'</span><span class="p">));</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nb">ArrayBuffer</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">options</span><span class="p">.</span><span class="nx">checkOrientation</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">URL</span> <span class="o">&&</span> <span class="o">!</span><span class="nx">options</span><span class="p">.</span><span class="nx">checkOrientation</span><span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">load</span><span class="p">({</span>
<span class="na">url</span><span class="p">:</span> <span class="nx">URL</span><span class="p">.</span><span class="nx">createObjectURL</span><span class="p">(</span><span class="nx">file</span><span class="p">),</span>
<span class="p">});</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">reader</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FileReader</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">checkOrientation</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">checkOrientation</span> <span class="o">&&</span> <span class="nx">mimeType</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">image/jpeg</span><span class="dl">'</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">reader</span> <span class="o">=</span> <span class="nx">reader</span><span class="p">;</span>
<span class="nx">reader</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">target</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">result</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">target</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="p">{};</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">checkOrientation</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// Reset the orientation value to its default value 1</span>
<span class="c1">// as some iOS browsers will render image with its orientation</span>
<span class="kd">const</span> <span class="nx">orientation</span> <span class="o">=</span> <span class="nx">resetAndGetOrientation</span><span class="p">(</span><span class="nx">result</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">orientation</span> <span class="o">></span> <span class="mi">1</span> <span class="o">||</span> <span class="o">!</span><span class="nx">URL</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// Generate a new URL which has the default orientation value</span>
<span class="nx">data</span><span class="p">.</span><span class="nx">url</span> <span class="o">=</span> <span class="nx">arrayBufferToDataURL</span><span class="p">(</span><span class="nx">result</span><span class="p">,</span> <span class="nx">mimeType</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">orientation</span> <span class="o">></span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
<span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span> <span class="nx">parseOrientation</span><span class="p">(</span><span class="nx">orientation</span><span class="p">));</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">data</span><span class="p">.</span><span class="nx">url</span> <span class="o">=</span> <span class="nx">URL</span><span class="p">.</span><span class="nx">createObjectURL</span><span class="p">(</span><span class="nx">file</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">data</span><span class="p">.</span><span class="nx">url</span> <span class="o">=</span> <span class="nx">result</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">this</span><span class="p">.</span><span class="nx">load</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">};</span>
<span class="nx">reader</span><span class="p">.</span><span class="nx">onabort</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">fail</span><span class="p">(</span><span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Aborted to read the image with FileReader.</span><span class="dl">'</span><span class="p">));</span>
<span class="p">};</span>
<span class="nx">reader</span><span class="p">.</span><span class="nx">onerror</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">fail</span><span class="p">(</span><span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Failed to read the image with FileReader.</span><span class="dl">'</span><span class="p">));</span>
<span class="p">};</span>
<span class="nx">reader</span><span class="p">.</span><span class="nx">onloadend</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">reader</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">};</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">checkOrientation</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">reader</span><span class="p">.</span><span class="nx">readAsArrayBuffer</span><span class="p">(</span><span class="nx">file</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">reader</span><span class="p">.</span><span class="nx">readAsDataURL</span><span class="p">(</span><span class="nx">file</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">load</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">file</span><span class="p">,</span> <span class="nx">image</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
<span class="nx">image</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">draw</span><span class="p">({</span>
<span class="p">...</span><span class="nx">data</span><span class="p">,</span>
<span class="na">naturalWidth</span><span class="p">:</span> <span class="nx">image</span><span class="p">.</span><span class="nx">naturalWidth</span><span class="p">,</span>
<span class="na">naturalHeight</span><span class="p">:</span> <span class="nx">image</span><span class="p">.</span><span class="nx">naturalHeight</span><span class="p">,</span>
<span class="p">});</span>
<span class="p">};</span>
<span class="nx">image</span><span class="p">.</span><span class="nx">onabort</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">fail</span><span class="p">(</span><span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Aborted to load the image.</span><span class="dl">'</span><span class="p">));</span>
<span class="p">};</span>
<span class="nx">image</span><span class="p">.</span><span class="nx">onerror</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">fail</span><span class="p">(</span><span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Failed to load the image.</span><span class="dl">'</span><span class="p">));</span>
<span class="p">};</span>
<span class="c1">// Match all browsers that use WebKit as the layout engine in iOS devices,</span>
<span class="c1">// such as Safari for iOS, Chrome for iOS, and in-app browsers.</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">WINDOW</span><span class="p">.</span><span class="nb">navigator</span> <span class="o">&&</span> <span class="sr">/</span><span class="se">(?:</span><span class="sr">iPad|iPhone|iPod</span><span class="se">)</span><span class="sr">.*</span><span class="se">?</span><span class="sr">AppleWebKit/i</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">WINDOW</span><span class="p">.</span><span class="nb">navigator</span><span class="p">.</span><span class="nx">userAgent</span><span class="p">))</span> <span class="p">{</span>
<span class="c1">// Fix the `The operation is insecure` error (#57)</span>
<span class="nx">image</span><span class="p">.</span><span class="nx">crossOrigin</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">anonymous</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">image</span><span class="p">.</span><span class="nx">alt</span> <span class="o">=</span> <span class="nx">file</span><span class="p">.</span><span class="nx">name</span><span class="p">;</span>
<span class="nx">image</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">url</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">draw</span><span class="p">({</span>
<span class="nx">naturalWidth</span><span class="p">,</span>
<span class="nx">naturalHeight</span><span class="p">,</span>
<span class="nx">rotate</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
<span class="nx">scaleX</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
<span class="nx">scaleY</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
<span class="p">})</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">file</span><span class="p">,</span> <span class="nx">image</span><span class="p">,</span> <span class="nx">options</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">canvas</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">canvas</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">context</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">getContext</span><span class="p">(</span><span class="dl">'</span><span class="s1">2d</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">aspectRatio</span> <span class="o">=</span> <span class="nx">naturalWidth</span> <span class="o">/</span> <span class="nx">naturalHeight</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">is90DegreesRotated</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">abs</span><span class="p">(</span><span class="nx">rotate</span><span class="p">)</span> <span class="o">%</span> <span class="mi">180</span> <span class="o">===</span> <span class="mi">90</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">maxWidth</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">maxWidth</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">||</span> <span class="kc">Infinity</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">maxHeight</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">maxHeight</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">||</span> <span class="kc">Infinity</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">minWidth</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">minWidth</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">||</span> <span class="mi">0</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">minHeight</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">minHeight</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">||</span> <span class="mi">0</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">width</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">width</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">||</span> <span class="nx">naturalWidth</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">height</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">height</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">||</span> <span class="nx">naturalHeight</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">is90DegreesRotated</span><span class="p">)</span> <span class="p">{</span>
<span class="p">[</span><span class="nx">maxWidth</span><span class="p">,</span> <span class="nx">maxHeight</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="nx">maxHeight</span><span class="p">,</span> <span class="nx">maxWidth</span><span class="p">];</span>
<span class="p">[</span><span class="nx">minWidth</span><span class="p">,</span> <span class="nx">minHeight</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="nx">minHeight</span><span class="p">,</span> <span class="nx">minWidth</span><span class="p">];</span>
<span class="p">[</span><span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="nx">height</span><span class="p">,</span> <span class="nx">width</span><span class="p">];</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">maxWidth</span> <span class="o"><</span> <span class="kc">Infinity</span> <span class="o">&&</span> <span class="nx">maxHeight</span> <span class="o"><</span> <span class="kc">Infinity</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">maxHeight</span> <span class="o">*</span> <span class="nx">aspectRatio</span> <span class="o">></span> <span class="nx">maxWidth</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">maxHeight</span> <span class="o">=</span> <span class="nx">maxWidth</span> <span class="o">/</span> <span class="nx">aspectRatio</span><span class="p">;</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">maxWidth</span> <span class="o">=</span> <span class="nx">maxHeight</span> <span class="o">*</span> <span class="nx">aspectRatio</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">maxWidth</span> <span class="o"><</span> <span class="kc">Infinity</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">maxHeight</span> <span class="o">=</span> <span class="nx">maxWidth</span> <span class="o">/</span> <span class="nx">aspectRatio</span><span class="p">;</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">maxHeight</span> <span class="o"><</span> <span class="kc">Infinity</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">maxWidth</span> <span class="o">=</span> <span class="nx">maxHeight</span> <span class="o">*</span> <span class="nx">aspectRatio</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">minWidth</span> <span class="o">></span> <span class="mi">0</span> <span class="o">&&</span> <span class="nx">minHeight</span> <span class="o">></span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">minHeight</span> <span class="o">*</span> <span class="nx">aspectRatio</span> <span class="o">></span> <span class="nx">minWidth</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">minHeight</span> <span class="o">=</span> <span class="nx">minWidth</span> <span class="o">/</span> <span class="nx">aspectRatio</span><span class="p">;</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">minWidth</span> <span class="o">=</span> <span class="nx">minHeight</span> <span class="o">*</span> <span class="nx">aspectRatio</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">minWidth</span> <span class="o">></span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">minHeight</span> <span class="o">=</span> <span class="nx">minWidth</span> <span class="o">/</span> <span class="nx">aspectRatio</span><span class="p">;</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">minHeight</span> <span class="o">></span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">minWidth</span> <span class="o">=</span> <span class="nx">minHeight</span> <span class="o">*</span> <span class="nx">aspectRatio</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">height</span> <span class="o">*</span> <span class="nx">aspectRatio</span> <span class="o">></span> <span class="nx">width</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">height</span> <span class="o">=</span> <span class="nx">width</span> <span class="o">/</span> <span class="nx">aspectRatio</span><span class="p">;</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">width</span> <span class="o">=</span> <span class="nx">height</span> <span class="o">*</span> <span class="nx">aspectRatio</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">width</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span><span class="nx">normalizeDecimalNumber</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">min</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span><span class="nx">width</span><span class="p">,</span> <span class="nx">minWidth</span><span class="p">),</span> <span class="nx">maxWidth</span><span class="p">)));</span>
<span class="nx">height</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span><span class="nx">normalizeDecimalNumber</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">min</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span><span class="nx">height</span><span class="p">,</span> <span class="nx">minHeight</span><span class="p">),</span> <span class="nx">maxHeight</span><span class="p">)));</span>
<span class="kd">const</span> <span class="nx">destX</span> <span class="o">=</span> <span class="o">-</span><span class="nx">width</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">destY</span> <span class="o">=</span> <span class="o">-</span><span class="nx">height</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">destWidth</span> <span class="o">=</span> <span class="nx">width</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">destHeight</span> <span class="o">=</span> <span class="nx">height</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">is90DegreesRotated</span><span class="p">)</span> <span class="p">{</span>
<span class="p">[</span><span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="nx">height</span><span class="p">,</span> <span class="nx">width</span><span class="p">];</span>
<span class="p">}</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="nx">width</span><span class="p">;</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">height</span> <span class="o">=</span> <span class="nx">height</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">isImageType</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">mimeType</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">options</span><span class="p">.</span><span class="nx">mimeType</span> <span class="o">=</span> <span class="nx">file</span><span class="p">.</span><span class="nx">type</span><span class="p">;</span>
<span class="p">}</span>
<span class="kd">let</span> <span class="nx">fillStyle</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">transparent</span><span class="dl">'</span><span class="p">;</span>
<span class="c1">// Converts PNG files over the `convertSize` to JPEGs.</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">file</span><span class="p">.</span><span class="nx">size</span> <span class="o">></span> <span class="nx">options</span><span class="p">.</span><span class="nx">convertSize</span> <span class="o">&&</span> <span class="nx">options</span><span class="p">.</span><span class="nx">mimeType</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">image/png</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">fillStyle</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">#fff</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">options</span><span class="p">.</span><span class="nx">mimeType</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">image/jpeg</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">// Override the default fill color (#000, black)</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">fillStyle</span> <span class="o">=</span> <span class="nx">fillStyle</span><span class="p">;</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">fillRect</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">beforeDraw</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">options</span><span class="p">.</span><span class="nx">beforeDraw</span><span class="p">.</span><span class="nx">call</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="nx">context</span><span class="p">,</span> <span class="nx">canvas</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">aborted</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">save</span><span class="p">();</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">translate</span><span class="p">(</span><span class="nx">width</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="nx">height</span> <span class="o">/</span> <span class="mi">2</span><span class="p">);</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">rotate</span><span class="p">((</span><span class="nx">rotate</span> <span class="o">*</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">PI</span><span class="p">)</span> <span class="o">/</span> <span class="mi">180</span><span class="p">);</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">scale</span><span class="p">(</span><span class="nx">scaleX</span><span class="p">,</span> <span class="nx">scaleY</span><span class="p">);</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">drawImage</span><span class="p">(</span><span class="nx">image</span><span class="p">,</span> <span class="nx">destX</span><span class="p">,</span> <span class="nx">destY</span><span class="p">,</span> <span class="nx">destWidth</span><span class="p">,</span> <span class="nx">destHeight</span><span class="p">);</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">drew</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">options</span><span class="p">.</span><span class="nx">drew</span><span class="p">.</span><span class="nx">call</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="nx">context</span><span class="p">,</span> <span class="nx">canvas</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">aborted</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">done</span> <span class="o">=</span> <span class="p">(</span><span class="nx">result</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">aborted</span><span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">done</span><span class="p">({</span>
<span class="nx">naturalWidth</span><span class="p">,</span>
<span class="nx">naturalHeight</span><span class="p">,</span>
<span class="nx">result</span><span class="p">,</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="p">};</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">canvas</span><span class="p">.</span><span class="nx">toBlob</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">toBlob</span><span class="p">(</span><span class="nx">done</span><span class="p">,</span> <span class="nx">options</span><span class="p">.</span><span class="nx">mimeType</span><span class="p">,</span> <span class="nx">options</span><span class="p">.</span><span class="nx">quality</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">done</span><span class="p">(</span><span class="nx">toBlob</span><span class="p">(</span><span class="nx">canvas</span><span class="p">.</span><span class="nx">toDataURL</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">mimeType</span><span class="p">,</span> <span class="nx">options</span><span class="p">.</span><span class="nx">quality</span><span class="p">)));</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">done</span><span class="p">({</span>
<span class="nx">naturalWidth</span><span class="p">,</span>
<span class="nx">naturalHeight</span><span class="p">,</span>
<span class="nx">result</span><span class="p">,</span>
<span class="p">})</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">file</span><span class="p">,</span> <span class="nx">image</span><span class="p">,</span> <span class="nx">options</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">URL</span> <span class="o">&&</span> <span class="o">!</span><span class="nx">options</span><span class="p">.</span><span class="nx">checkOrientation</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">URL</span><span class="p">.</span><span class="nx">revokeObjectURL</span><span class="p">(</span><span class="nx">image</span><span class="p">.</span><span class="nx">src</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">result</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// Returns original file if the result is greater than it and without size related options</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">strict</span> <span class="o">&&</span> <span class="nx">result</span><span class="p">.</span><span class="nx">size</span> <span class="o">></span> <span class="nx">file</span><span class="p">.</span><span class="nx">size</span> <span class="o">&&</span> <span class="nx">options</span><span class="p">.</span><span class="nx">mimeType</span> <span class="o">===</span> <span class="nx">file</span><span class="p">.</span><span class="nx">type</span> <span class="o">&&</span> <span class="o">!</span><span class="p">(</span>
<span class="nx">options</span><span class="p">.</span><span class="nx">width</span> <span class="o">></span> <span class="nx">naturalWidth</span>
<span class="o">||</span> <span class="nx">options</span><span class="p">.</span><span class="nx">height</span> <span class="o">></span> <span class="nx">naturalHeight</span>
<span class="o">||</span> <span class="nx">options</span><span class="p">.</span><span class="nx">minWidth</span> <span class="o">></span> <span class="nx">naturalWidth</span>
<span class="o">||</span> <span class="nx">options</span><span class="p">.</span><span class="nx">minHeight</span> <span class="o">></span> <span class="nx">naturalHeight</span>
<span class="p">))</span> <span class="p">{</span>
<span class="nx">result</span> <span class="o">=</span> <span class="nx">file</span><span class="p">;</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">date</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">();</span>
<span class="nx">result</span><span class="p">.</span><span class="nx">lastModified</span> <span class="o">=</span> <span class="nx">date</span><span class="p">.</span><span class="nx">getTime</span><span class="p">();</span>
<span class="nx">result</span><span class="p">.</span><span class="nx">lastModifiedDate</span> <span class="o">=</span> <span class="nx">date</span><span class="p">;</span>
<span class="nx">result</span><span class="p">.</span><span class="nx">name</span> <span class="o">=</span> <span class="nx">file</span><span class="p">.</span><span class="nx">name</span><span class="p">;</span>
<span class="c1">// Convert the extension to match its type</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">name</span> <span class="o">&&</span> <span class="nx">result</span><span class="p">.</span><span class="nx">type</span> <span class="o">!==</span> <span class="nx">file</span><span class="p">.</span><span class="nx">type</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">result</span><span class="p">.</span><span class="nx">name</span> <span class="o">=</span> <span class="nx">result</span><span class="p">.</span><span class="nx">name</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span>
<span class="nx">REGEXP_EXTENSION</span><span class="p">,</span>
<span class="nx">imageTypeToExtension</span><span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">type</span><span class="p">),</span>
<span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="c1">// Returns original file if the result is null in some cases.</span>
<span class="nx">result</span> <span class="o">=</span> <span class="nx">file</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">this</span><span class="p">.</span><span class="nx">result</span> <span class="o">=</span> <span class="nx">result</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">success</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">options</span><span class="p">.</span><span class="nx">success</span><span class="p">.</span><span class="nx">call</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="nx">result</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">fail</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">options</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">options</span><span class="p">.</span><span class="nx">error</span><span class="p">.</span><span class="nx">call</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">throw</span> <span class="nx">err</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">abort</span><span class="p">()</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">aborted</span><span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">aborted</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">reader</span><span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">reader</span><span class="p">.</span><span class="nx">abort</span><span class="p">();</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">image</span><span class="p">.</span><span class="nx">complete</span><span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">image</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">image</span><span class="p">.</span><span class="nx">onabort</span><span class="p">();</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">fail</span><span class="p">(</span><span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">The compression process has been aborted.</span><span class="dl">'</span><span class="p">));</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="结束语">结束语</h2>
<p>图片压缩其实和图片合成实际上所使用的技术和套路和本文是如出一辙的,也是“图片→canvas水印→图片”三步曲,区别在于水印合成是连续执行两次<code class="language-plaintext highlighter-rouge">context.drawImage()</code>方法,一次是原图一次水印图片,以及最后转换成图片的时候什么是<code class="language-plaintext highlighter-rouge">toDataURL()</code>方法,其他代码逻辑和原理都是一样的。</p>
<p>由此及彼,利用同样的原理和代码逻辑,我们还可以实现其它很多以前前端不太好实现的功能,比方说图片的真剪裁效果,所谓“真剪裁”指不是使用个<code class="language-plaintext highlighter-rouge">overflow:hidden</code>或者<code class="language-plaintext highlighter-rouge">clip</code>这些CSS属性的“伪剪裁”,而是真正意义上就这么大区域图片信息。甚至配合一些前端算法,我们可以直接在前端进行人脸识别,图片自动美化等一系列功能再上传等等。</p>
<p>原理都是一样的,都是利用<code class="language-plaintext highlighter-rouge">canvas</code>作为中间媒介进行处理。</p>Danniel背景理解JS中的二进制对象2020-04-24T00:00:00+00:002020-04-24T00:00:00+00:00http://daner1990.github.io/javascript/2020/04/24/%E7%90%86%E8%A7%A3Js%E4%B8%AD%E7%9A%84%E4%BA%8C%E8%BF%9B%E5%88%B6%E5%AF%B9%E8%B1%A1<h2 id="背景">背景</h2>
<p>最早js是无法处理二进制的,如果非要处理,是通过charCodeAt逐个转化成Unicode编码的二进制数据,知道es5 引入blob,js才算真正可以处理二进制</p>
<p>blob有衍生了一系列如File对象,FileReader对象,FileList对象,URL对象;</p>
<p>Ajax初期只能获取文本数据,在XMLHttpRequest第二个版本也允许了服务器返回二进制数据,如果明确知道返回的二进制数据类型,返回类型就被设置为responseType:arrayBuffer;如果不知道就为blob;</p>
<p>canvas元素输出的二进制元素是TypedArray;</p>
<p>为了配合以上API,es6吧arraybuffer对象,typedarray视图和dataview视图纳入了ecmascript规范,他们都是以数组的语法处理二进制数据,统称二进制数组。</p>
<p>二进制数组并不是真正的数组,而是类似数组的对象。</p>
<p>它很像 C 语言的数组,允许开发者以数组下标的形式,直接操作内存,大大增强了 JavaScript 处理二进制数据的能力,使得开发者有可能通过 JavaScript 与操作系统的原生接口进行二进制通信</p>
<p><a href="https://es6.ruanyifeng.com/#docs/arraybuffer"><code class="language-plaintext highlighter-rouge">es6 ArrayBuffer document</code></a></p>
<h2 id="二进制数组">二进制数组</h2>
<h3 id="arraybuffer对象">arrayBuffer对象</h3>
<ul>
<li>arrayBuffer表示一段储存了二进制数据的内存</li>
<li>不能直接读写arrayBuffer,要用typedarray视图和dataview视图</li>
<li>视图的作用是以指定格式解读二进制数据</li>
<li><code class="language-plaintext highlighter-rouge">ArrayBuffer</code>也是一个构造函数,可以分配一段可以存放数据的连续内存区域。</li>
</ul>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">buffer</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">ArrayBuffer</span><span class="p">(</span><span class="mi">32</span><span class="p">);</span>
<span class="c1">//创建一段32字节的内存区域,每个字节默认值是0</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">buffer</span><span class="p">.</span><span class="nx">byteLength</span><span class="p">)</span> <span class="c1">//32</span>
<span class="nx">newBuffer</span> <span class="o">=</span> <span class="nx">buffer</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">3</span><span class="p">);</span>
<span class="c1">//slice方法其实包含两步,第一步是先分配一段新内存,第二步是将原来那个ArrayBuffer对象拷贝过去。</span>
<span class="c1">//slice方法接受两个参数,第一个参数表示拷贝开始的字节序号(含该字节),第二个参数表示拷贝截止的字节序号(不含该字节)。如果省略第二个参数,则默认到原ArrayBuffer对象的结尾。</span>
<span class="c1">//除了slice方法,ArrayBuffer对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。</span>
<span class="nb">ArrayBuffer</span><span class="p">.</span><span class="nx">isView</span><span class="p">(</span><span class="nx">buffer</span><span class="p">)</span> <span class="c1">// false</span>
<span class="kd">const</span> <span class="nx">v</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Int32Array</span><span class="p">(</span><span class="nx">buffer</span><span class="p">);</span>
<span class="nb">ArrayBuffer</span><span class="p">.</span><span class="nx">isView</span><span class="p">(</span><span class="nx">v</span><span class="p">)</span> <span class="c1">// true</span>
<span class="c1">//ArrayBuffer有一个静态方法isView,返回一个布尔值,表示参数是否为ArrayBuffer的视图实例。这个方法大致相当于判断参数,是否为TypedArray实例或DataView实例。</span>
</code></pre></div></div>
<h3 id="typedarray视图">typedArray视图</h3>
<ul>
<li>共包括 9 种类型的视图,比如<code class="language-plaintext highlighter-rouge">Uint8Array</code>(无符号 8 位整数)数组视图, <code class="language-plaintext highlighter-rouge">Int16Array</code>(16 位整数)数组视图, <code class="language-plaintext highlighter-rouge">Float32Array</code>(32 位浮点数)数组视图等等</li>
<li><code class="language-plaintext highlighter-rouge">TypedArray</code>视图用来读写简单类型的二进制数据</li>
</ul>
<table>
<thead>
<tr>
<th>数据类型</th>
<th>字节长度</th>
<th>含义</th>
<th>对应的 C 语言类型</th>
</tr>
</thead>
<tbody>
<tr>
<td>Int8</td>
<td>1</td>
<td>8 位带符号整数</td>
<td>signed char</td>
</tr>
<tr>
<td>Uint8</td>
<td>1</td>
<td>8 位不带符号整数</td>
<td>unsigned char</td>
</tr>
<tr>
<td>Uint8C</td>
<td>1</td>
<td>8 位不带符号整数(自动过滤溢出)</td>
<td>unsigned char</td>
</tr>
<tr>
<td>Int16</td>
<td>2</td>
<td>16 位带符号整数</td>
<td>short</td>
</tr>
<tr>
<td>Uint16</td>
<td>2</td>
<td>16 位不带符号整数</td>
<td>unsigned short</td>
</tr>
<tr>
<td>Int32</td>
<td>4</td>
<td>32 位带符号整数</td>
<td>int</td>
</tr>
<tr>
<td>Uint32</td>
<td>4</td>
<td>32 位不带符号的整数</td>
<td>unsigned int</td>
</tr>
<tr>
<td>Float32</td>
<td>4</td>
<td>32 位浮点数</td>
<td>float</td>
</tr>
<tr>
<td>Float64</td>
<td>8</td>
<td>64 位浮点数</td>
<td>double</td>
</tr>
</tbody>
</table>
<p>这 9 个构造函数生成的数组,统称为<code class="language-plaintext highlighter-rouge">TypedArray</code>视图。它们很像普通数组,都有<code class="language-plaintext highlighter-rouge">length</code>属性,都能用方括号运算符(<code class="language-plaintext highlighter-rouge">[]</code>)获取单个元素,所有数组的方法,在它们上面都能使用。普通数组与 TypedArray 数组的差异主要在以下方面。</p>
<ul>
<li>TypedArray 数组的所有成员,都是同一种类型。</li>
<li>TypedArray 数组的成员是连续的,不会有空位。</li>
<li>TypedArray 数组成员的默认值为 0。比如,<code class="language-plaintext highlighter-rouge">new Array(10)</code>返回一个普通数组,里面没有任何成员,只是 10 个空位;<code class="language-plaintext highlighter-rouge">new Uint8Array(10)</code>返回一个 TypedArray 数组,里面 10 个成员都是 0。</li>
<li>TypedArray 数组只是一层视图,本身不储存数据,它的数据都储存在底层的<code class="language-plaintext highlighter-rouge">ArrayBuffer</code>对象之中,要获取底层对象必须使用<code class="language-plaintext highlighter-rouge">buffer</code>属性。</li>
</ul>
<h3 id="dataview视图">dataView视图</h3>
<ul>
<li>可以自定义复合格式的视图,比如第一个字节是 Uint8(无符号 8 位整数)、第二、三个字节是 Int16(16 位整数)、第四个字节开始是 Float32(32 位浮点数)等等,此外还可以自定义字节序。</li>
<li><code class="language-plaintext highlighter-rouge">DataView</code>视图用来读写复杂类型的二进制数据</li>
</ul>
<h2 id="dataurl">DataURL</h2>
<p>Data URLs 由四个部分组成:前缀(<code class="language-plaintext highlighter-rouge">data:</code>)、指示数据类型的MIME类型、如果非文本则为可选的<code class="language-plaintext highlighter-rouge">base64</code>标记、数据本身:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">data</span><span class="p">:[</span><span class="o"><</span><span class="nx">mediatype</span><span class="o">></span><span class="p">][;</span><span class="nx">base64</span><span class="p">],</span><span class="o"><</span><span class="nx">data</span><span class="o">></span>
<span class="nx">data</span><span class="p">:,</span><span class="nx">Hello</span><span class="o">%</span><span class="mi">2</span><span class="nx">C</span><span class="o">%</span><span class="mi">20</span><span class="nx">World</span><span class="o">!</span>
<span class="c1">//简单的 text/plain 类型数据</span>
<span class="nx">data</span><span class="p">:</span><span class="nx">text</span><span class="o">/</span><span class="nx">plain</span><span class="p">;</span><span class="nx">base64</span><span class="p">,</span><span class="nx">SGVsbG8sIFdvcmxkIQ</span><span class="o">%</span><span class="mi">3</span><span class="nx">D</span><span class="o">%</span><span class="mi">3</span><span class="nx">D</span>
<span class="c1">//上一条示例的 base64 编码版本</span>
<span class="nx">data</span><span class="p">:</span><span class="nx">text</span><span class="o">/</span><span class="nx">html</span><span class="p">,</span><span class="o">%</span><span class="mi">3</span><span class="nx">Ch1</span><span class="o">%</span><span class="mi">3</span><span class="nx">EHello</span><span class="o">%</span><span class="mi">2</span><span class="nx">C</span><span class="o">%</span><span class="mi">20</span><span class="nx">World</span><span class="o">!%</span><span class="mi">3</span><span class="nx">C</span><span class="o">%</span><span class="mi">2</span><span class="nx">Fh1</span><span class="o">%</span><span class="mi">3</span><span class="nx">E</span>
<span class="c1">//一个HTML文档源代码 <h1>Hello, World</h1></span>
<span class="nx">data</span><span class="p">:</span><span class="nx">text</span><span class="o">/</span><span class="nx">html</span><span class="p">,</span><span class="o"><</span><span class="nx">script</span><span class="o">></span><span class="nx">alert</span><span class="p">(</span><span class="dl">'</span><span class="s1">hi</span><span class="dl">'</span><span class="p">);</span><span class="o"><</span><span class="sr">/script</span><span class="err">>
</span><span class="c1">//一个会执行 JavaScript alert 的 HTML 文档。注意 script 标签必须封闭。</span>
</code></pre></div></div>
<p>mediatype <code class="language-plaintext highlighter-rouge">是个 MIME 类型的字符串,例如 "</code>image/jpeg<code class="language-plaintext highlighter-rouge">" 表示 JPEG 图像文件。如果被省略,则默认值为 </code>text/plain;charset=US-ASCII</p>
<p>如果数据是文本类型,你可以直接将文本嵌入 (根据文档类型,使用合适的实体字符或转义字符)。如果是二进制数据,你可以将数据进行base64编码之后再进行嵌入。</p>
<h3 id="base64">base64</h3>
<p>上面提到的 base64 不算是一种加密算法,它只是简单地将每 3 个 8bit 字符转换为 4 个 6Bit 字符(base64 只有 2^6 = 64 种字符,因此得名),这样保证了传输中必定使用 ASCII 中可见字符,不会出奇怪的空白字符或是功能性标志 。</p>
<p>由于是 3 个字符变 4 个,那么很明显了,base64 编码后,编码对象的<strong>体积会变成原来的 4/3 倍</strong>。</p>
<p>特别要注意的是如果 bit 数不能被 3 整除,需要在末尾添加 1 或 2 个 byte(8 或 16bit),<strong>并且末尾的 0 不使用 A 而使用 =</strong>,这就是为什么 base64 有的编码结果后面会有一或两个等号。</p>
<h3 id="btoa">btoa</h3>
<p><strong><code class="language-plaintext highlighter-rouge">WindowOrWorkerGlobalScope.btoa()</code></strong> 从 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/String"><code class="language-plaintext highlighter-rouge">String</code></a> 对象中创建一个 base-64 编码的 ASCII 字符串,其中字符串中的每个字符都被视为一个二进制数据字节。</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">encodedData</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">btoa</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello, world</span><span class="dl">"</span><span class="p">);</span> <span class="c1">// 编码</span>
<span class="kd">let</span> <span class="nx">decodedData</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">atob</span><span class="p">(</span><span class="nx">encodedData</span><span class="p">);</span> <span class="c1">// 解码</span>
</code></pre></div></div>
<h3 id="unicode-字符串">unicode 字符串</h3>
<p>在多数浏览器中,使用 <code class="language-plaintext highlighter-rouge">btoa()</code> 对 Unicode 字符串进行编码都会触发 <code class="language-plaintext highlighter-rouge">InvalidCharacterError</code> 异常。</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ucs-2 string to base64 encoded ascii</span>
<span class="kd">function</span> <span class="nx">utoa</span><span class="p">(</span><span class="nx">str</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nb">window</span><span class="p">.</span><span class="nx">btoa</span><span class="p">(</span><span class="nx">unescape</span><span class="p">(</span><span class="nb">encodeURIComponent</span><span class="p">(</span><span class="nx">str</span><span class="p">)));</span>
<span class="p">}</span>
<span class="c1">// base64 encoded ascii to ucs-2 string</span>
<span class="kd">function</span> <span class="nx">atou</span><span class="p">(</span><span class="nx">str</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nb">decodeURIComponent</span><span class="p">(</span><span class="nx">escape</span><span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nx">atob</span><span class="p">(</span><span class="nx">str</span><span class="p">)));</span>
<span class="p">}</span>
<span class="c1">// Usage:</span>
<span class="nx">utoa</span><span class="p">(</span><span class="dl">'</span><span class="s1">✓ à la mode</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// 4pyTIMOgIGxhIG1vZGU=</span>
<span class="nx">atou</span><span class="p">(</span><span class="dl">'</span><span class="s1">4pyTIMOgIGxhIG1vZGU=</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// "✓ à la mode"</span>
<span class="nx">utoa</span><span class="p">(</span><span class="dl">'</span><span class="s1">I </span><span class="se">\</span><span class="s1">u2661 Unicode!</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// SSDimaEgVW5pY29kZSE=</span>
<span class="nx">atou</span><span class="p">(</span><span class="dl">'</span><span class="s1">SSDimaEgVW5pY29kZSE=</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// "I ♡ Unicode!"</span>
</code></pre></div></div>
<h2 id="blob">blob</h2>
<ul>
<li>二进制大对象,并非前端特有,计算机通用术语</li>
<li>一个Blob对象就是一个包含有只读原始数据的类文件对象</li>
<li>一个不可修改的二进制文件</li>
</ul>
<h2 id="二进制数据关联">二进制数据关联</h2>
<h3 id="概述">概述</h3>
<ul>
<li>Blob、ArrayBuffer、File可以归为一类,它们都是数据;</li>
<li>FileReader算是一种工具,用来读取数据;</li>
<li>FormData可以看做是一个应用数据的场景。</li>
</ul>
<h3 id="blob和arraybuffer">blob和ArrayBuffer</h3>
<ul>
<li>相同点: Blob和ArrayBuffer都是二进制的容器;</li>
<li>ArrayBuffer:ArrayBuffer更底层,就是一段纯粹的内存上的二进制数据,我们可以对其任何一个字节进行单独的修改,也可以根据我们的需要以我们指定的形式读取指定范围的数据</li>
<li>Blob:Blob就是将一段二进制数据做了一个封装,我们拿到的就是一个整体,可以看到它的整体属性大小、类型;可以对其分割,但不能了解到它的细节</li>
<li>联系:Blob可以接受一个ArrayBuffer作为参数生成一个Blob对象,此行为就相当于对ArrayBuffer数据做一个封装,之后就是以整体的形式展现了</li>
<li>应用上的区别:由于ArrayBuffer和Blob的特性,Blob作为一个整体文件,适合用于传输;而只有需要关注细节(比如要修改某一段数据时),才需要用到ArrayBuffer</li>
</ul>
<h3 id="blob与file">Blob与File</h3>
<ul>
<li>file是二进制文件,是blob的一个小类,blob的所有属性和方法都可以用于file</li>
</ul>
<h2 id="数据转化">数据转化</h2>
<h3 id="arraybuffer-to-dataurlbase64">ArrayBuffer to DataUrl(base64)</h3>
<ul>
<li>利用blob作为媒介</li>
<li>buffer->blob->filereader->dataurl</li>
</ul>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">buffer</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">ArrayBuffer</span><span class="p">(</span><span class="mi">32</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">blob</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Blob</span><span class="p">([</span><span class="nx">buffer</span><span class="p">]);</span> <span class="c1">// 注意必须包裹[]</span>
<span class="kd">var</span> <span class="nx">a</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FileReader</span><span class="p">();</span>
<span class="nx">a</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span><span class="nx">callback</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">result</span><span class="p">);};</span>
<span class="nx">a</span><span class="p">.</span><span class="nx">readAsDataURL</span><span class="p">(</span><span class="nx">blob</span><span class="p">);</span>
</code></pre></div></div>
<h3 id="fileblob-to-dataurlbase64">File/Blob to DataUrl(base64)</h3>
<ul>
<li>利用filereader的readAsDataURL</li>
</ul>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">readBlobAsDataURL</span><span class="p">(</span><span class="nx">blob</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">a</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FileReader</span><span class="p">();</span>
<span class="nx">a</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span><span class="nx">callback</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">result</span><span class="p">);};</span>
<span class="nx">a</span><span class="p">.</span><span class="nx">readAsDataURL</span><span class="p">(</span><span class="nx">blob</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">//example:</span>
<span class="nx">readBlobAsDataURL</span><span class="p">(</span><span class="nx">blob</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">dataurl</span><span class="p">){</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">dataurl</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">readBlobAsDataURL</span><span class="p">(</span><span class="nx">file</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">dataurl</span><span class="p">){</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">dataurl</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>
<h3 id="blob-to-arraybuffer">Blob to ArrayBuffer</h3>
<ul>
<li>利用filereader的readAsArrayBuffer</li>
</ul>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">blob</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Blob</span><span class="p">([</span><span class="nx">data</span><span class="p">],</span> <span class="p">{</span><span class="na">type</span><span class="p">:</span> <span class="err">‘</span><span class="nx">text</span><span class="o">/</span><span class="nx">plain</span><span class="err">‘</span><span class="p">});</span>
<span class="kd">var</span> <span class="nx">reader</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FileReader</span><span class="p">();</span>
<span class="nx">reader</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">callback</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">result</span><span class="p">);</span>
<span class="p">};</span>
<span class="nx">reader</span><span class="p">.</span><span class="nx">readAsArrayBuffer</span><span class="p">(</span><span class="nx">blob</span><span class="p">);</span>
</code></pre></div></div>
<h3 id="arraybuffer-to-blob">ArrayBuffer to Blob</h3>
<ul>
<li>arraybuffer可以作为New Blob的入参</li>
</ul>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">buffer</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">ArrayBuffer</span><span class="p">(</span><span class="mi">32</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">blob</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Blob</span><span class="p">([</span><span class="nx">buffer</span><span class="p">]);</span> <span class="c1">// 注意必须包裹[]</span>
</code></pre></div></div>
<h3 id="arraybuffer-to-uint8">ArrayBuffer to Uint8</h3>
<p>Uint8数组可以直观的看到ArrayBuffer中每个字节(1字节 == 8位)的值。一般我们要将ArrayBuffer转成Uint类型数组后才能对其中的字节进行存取操作。</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">buffer</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">ArrayBuffer</span><span class="p">(</span><span class="mi">32</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">u8</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Uint8Array</span><span class="p">(</span><span class="nx">buffer</span><span class="p">);</span>
</code></pre></div></div>
<h3 id="uint8-to-arraybuffer">Uint8 to ArrayBuffer</h3>
<p>我们Uint8数组可以直观的看到ArrayBuffer中每个字节(1字节 == 8位)的值。一般我们要将ArrayBuffer转成Uint类型数组后才能对其中的字节进行存取操作。</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">uint8</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Uint8Array</span><span class="p">();</span>
<span class="kd">var</span> <span class="nx">buffer</span> <span class="o">=</span> <span class="nx">uint8</span><span class="p">.</span><span class="nx">buffer</span><span class="p">;</span>
</code></pre></div></div>
<h3 id="array-to-arraybuffer">Array to ArrayBuffer</h3>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">arr</span> <span class="o">=</span> <span class="p">[</span><span class="mh">0x15</span><span class="p">,</span><span class="mh">0xFF</span><span class="p">,</span><span class="mh">0x01</span><span class="p">,</span><span class="mh">0x00</span><span class="p">,</span><span class="mh">0x34</span><span class="p">,</span><span class="mh">0xAB</span><span class="p">,</span><span class="mh">0x11</span><span class="p">];</span>
<span class="kd">var</span> <span class="nx">uint8</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Uint8Array</span><span class="p">(</span><span class="nx">arr</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">buffer</span> <span class="o">=</span> <span class="nx">uint8</span><span class="p">.</span><span class="nx">buffer</span><span class="p">;</span>
</code></pre></div></div>
<h3 id="typearray-to-array">TypeArray to Array</h3>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">Uint8Array2Array</span><span class="p">(</span><span class="nx">u8a</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">arr</span> <span class="o">=</span> <span class="p">[];</span>
<span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o"><</span> <span class="nx">u8a</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">arr</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">u8a</span><span class="p">[</span><span class="nx">i</span><span class="p">]);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">arr</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="blob-to-file">Blob to File</h3>
<ul>
<li>对blob添加name等参数就可以变成file</li>
</ul>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//将blob转换为file</span>
<span class="nx">blobtoFile</span><span class="p">:</span><span class="kd">function</span><span class="p">(</span><span class="nx">theBlob</span><span class="p">,</span><span class="nx">filename</span><span class="p">){</span>
<span class="nx">theBlob</span><span class="p">.</span><span class="nx">lastModifiedDate</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">();</span>
<span class="nx">theBlob</span><span class="p">.</span><span class="nx">name</span> <span class="o">=</span> <span class="nx">fileName</span><span class="p">;</span>
<span class="k">return</span> <span class="nx">theBlob</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="base64dataurl-to-blob">base64(dataurl) to Blob</h3>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//将base64转换为blob</span>
<span class="c1">//dataurl = "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ"</span>
<span class="nx">dataURLtoBlob</span><span class="p">:</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">dataurl</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">arr</span> <span class="o">=</span> <span class="nx">dataurl</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">,</span><span class="dl">'</span><span class="p">),</span>
<span class="c1">// ["data:text/plain;base64", "SGVsbG8sIFdvcmxkIQ"]</span>
<span class="nx">mime</span> <span class="o">=</span> <span class="nx">arr</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">match</span><span class="p">(</span><span class="sr">/:</span><span class="se">(</span><span class="sr">.*</span><span class="se">?)</span><span class="sr">;/</span><span class="p">)[</span><span class="mi">1</span><span class="p">],</span>
<span class="c1">//[":text/plain;", "text/plain", index: 4, input: "data:text/plain;base64", groups: undefined]</span>
<span class="nx">bstr</span> <span class="o">=</span> <span class="nx">atob</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="mi">1</span><span class="p">]),</span>
<span class="c1">//atob(arr[1]) = "Hello, World!"</span>
<span class="nx">n</span> <span class="o">=</span> <span class="nx">bstr</span><span class="p">.</span><span class="nx">length</span><span class="p">,</span>
<span class="nx">u8arr</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Uint8Array</span><span class="p">(</span><span class="nx">n</span><span class="p">);</span>
<span class="k">while</span> <span class="p">(</span><span class="nx">n</span><span class="o">--</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">u8arr</span><span class="p">[</span><span class="nx">n</span><span class="p">]</span> <span class="o">=</span> <span class="nx">bstr</span><span class="p">.</span><span class="nx">charCodeAt</span><span class="p">(</span><span class="nx">n</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">//u8arr : Uint8Array(13) [72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Blob</span><span class="p">([</span><span class="nx">u8arr</span><span class="p">],</span> <span class="p">{</span><span class="na">type</span><span class="p">:</span> <span class="nx">mime</span><span class="p">});</span>
<span class="c1">//Blob {size: 13, type: "text/plain"}</span>
<span class="p">},</span>
</code></pre></div></div>
<h3 id="string-to-blob">string to blob</h3>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//将字符串转换成 Blob对象</span>
<span class="kd">var</span> <span class="nx">blob</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Blob</span><span class="p">([</span><span class="dl">'</span><span class="s1">中文字符串</span><span class="dl">'</span><span class="p">],</span> <span class="p">{</span>
<span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/plain</span><span class="dl">'</span>
<span class="p">});</span>
</code></pre></div></div>
<h3 id="blob-to-string">blob to string</h3>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//将Blob 对象转换成字符串</span>
<span class="kd">var</span> <span class="nx">reader</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FileReader</span><span class="p">();</span>
<span class="nx">reader</span><span class="p">.</span><span class="nx">readAsText</span><span class="p">(</span><span class="nx">blob</span><span class="p">,</span> <span class="dl">'</span><span class="s1">utf-8</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">reader</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">info</span><span class="p">(</span><span class="nx">reader</span><span class="p">.</span><span class="nx">result</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="使用场景">使用场景</h2>
<h3 id="生成的blob下载到本地">生成的blob下载到本地</h3>
<p>原理就是利用<code class="language-plaintext highlighter-rouge">Blob</code>对象把需要下载的内容转换为二进制,然后借助``标签的<code class="language-plaintext highlighter-rouge">href</code>属性和<code class="language-plaintext highlighter-rouge">download</code>属性,实现下载。</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">saveShareContent</span> <span class="p">(</span><span class="nx">content</span><span class="p">,</span> <span class="nx">fileName</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">downLink</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">a</span><span class="dl">'</span><span class="p">)</span>
<span class="nx">downLink</span><span class="p">.</span><span class="nx">download</span> <span class="o">=</span> <span class="nx">fileName</span>
<span class="c1">//字符内容转换为blod地址</span>
<span class="kd">let</span> <span class="nx">blob</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Blob</span><span class="p">([</span><span class="nx">content</span><span class="p">])</span>
<span class="nx">downLink</span><span class="p">.</span><span class="nx">href</span> <span class="o">=</span> <span class="nx">URL</span><span class="p">.</span><span class="nx">createObjectURL</span><span class="p">(</span><span class="nx">blob</span><span class="p">)</span>
<span class="c1">// 链接插入到页面</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">downLink</span><span class="p">)</span>
<span class="nx">downLink</span><span class="p">.</span><span class="nx">click</span><span class="p">()</span>
<span class="c1">// 移除下载链接</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">removeChild</span><span class="p">(</span><span class="nx">downLink</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="dataurl图片数据绘制到canvas">dataURL图片数据绘制到canvas</h3>
<ul>
<li>先构造Image对象,src为dataURL,图片onload之后绘制到canvas</li>
</ul>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">img</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Image</span><span class="p">();</span>
<span class="nx">img</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(){</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">drawImage</span><span class="p">(</span><span class="nx">img</span><span class="p">);</span>
<span class="p">};</span>
<span class="nx">img</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">dataurl</span><span class="p">;</span>
</code></pre></div></div>
<h3 id="fileblob的图片文件数据绘制到canvas">File,Blob的图片文件数据绘制到canvas</h3>
<ul>
<li>还是先转换成一个url,然后构造Image对象,src为dataURL,图片onload之后绘制到canvas</li>
<li>利用上面的 readBlobAsDataURL 函数,由File,Blob对象得到dataURL格式的url,再参考 dataURL图片数据绘制到canvas</li>
</ul>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">readBlobAsDataURL</span><span class="p">(</span><span class="nx">blob</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">a</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FileReader</span><span class="p">();</span>
<span class="nx">a</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span><span class="nx">callback</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">result</span><span class="p">);};</span>
<span class="nx">a</span><span class="p">.</span><span class="nx">readAsDataURL</span><span class="p">(</span><span class="nx">blob</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">readBlobAsDataURL</span><span class="p">(</span><span class="nx">file</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">dataurl</span><span class="p">){</span>
<span class="kd">var</span> <span class="nx">img</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Image</span><span class="p">();</span>
<span class="nx">img</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(){</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">drawImage</span><span class="p">(</span><span class="nx">img</span><span class="p">);</span>
<span class="p">};</span>
<span class="nx">img</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">dataurl</span><span class="p">;</span>
<span class="p">});</span>
</code></pre></div></div>
<h3 id="canvas转换为blob对象并使用ajax发送">Canvas转换为Blob对象并使用Ajax发送</h3>
<ul>
<li>转换为Blob对象后,可以使用Ajax上传图像文件。</li>
<li>先从canvas获取dataurl, 再将dataurl转换为Blob对象</li>
</ul>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">dataurl</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">toDataURL</span><span class="p">(</span><span class="err">‘</span><span class="nx">image</span><span class="o">/</span><span class="nx">png</span><span class="err">‘</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">blob</span> <span class="o">=</span> <span class="nx">dataURLtoBlob</span><span class="p">(</span><span class="nx">dataurl</span><span class="p">);</span>
<span class="c1">//使用ajax发送</span>
<span class="kd">var</span> <span class="nx">fd</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FormData</span><span class="p">();</span>
<span class="nx">fd</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">"</span><span class="s2">image</span><span class="dl">"</span><span class="p">,</span> <span class="nx">blob</span><span class="p">,</span> <span class="dl">"</span><span class="s2">image.png</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">xhr</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">XMLHttpRequest</span><span class="p">();</span>
<span class="nx">xhr</span><span class="p">.</span><span class="nx">open</span><span class="p">(</span><span class="err">‘</span><span class="nx">POST</span><span class="err">‘</span><span class="p">,</span> <span class="err">‘</span><span class="o">/</span><span class="nx">server</span><span class="err">‘</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
<span class="nx">xhr</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">fd</span><span class="p">);</span>
</code></pre></div></div>
<h2 id="总结">总结</h2>
<ol>
<li>blob作为二进制容器,可以利用fileReader来吧blob文件转化为arrayBuffer,dataurl,string等格式内容,如果要编辑blob文件,可以转化为arraybuffer,通过视图操作然后在转化回来在进行逆向操作</li>
<li>基于blob,利用canvas可以实现一系列如图片压缩,图片编辑等前端功能</li>
</ol>Danniel背景解决html2canvas截图模糊的问题2020-04-23T00:00:00+00:002020-04-23T00:00:00+00:00http://daner1990.github.io/plugins/2020/04/23/html2canvas%E6%88%AA%E5%9B%BE%E6%A8%A1%E7%B3%8A<h2 id="背景">背景</h2>
<hr />
<p>将网页保存为图片(以下简称为快照),是用户记录和分享页面信息的有效手段,在各种兴趣测试和营销推广等形式的活动页面中尤为常见。</p>
<p>快照环节通常处于页面交互流程的末端,汇总了用户最终的参与结果,直接影响到用户对于活动的完整体验。因此,生成高质量的页面快照,对于活动的传播和品牌的转化具有十分重要的意义。</p>
<p>本文基于云音乐往期优质活动的相关实践(例如「关于你的画」、「权力的游戏」和「你的使用说明书」等),从快照的内容完整性、清晰度和转换效率等多个方面,讨论将网页转换为高质量图片的实践探索。</p>
<h2 id="适用场景">适用场景</h2>
<hr />
<ul>
<li>适用于将页面转为图片,特别是对实时性要求较高的场景。</li>
<li>希望在快照中展示跨域图片资源的场景。</li>
<li>针对生成图片内容不完整、模糊或者转换过程缓慢等问题,寻求有效解决方案的场景。</li>
</ul>
<h2 id="原理简析">原理简析</h2>
<hr />
<h4 id="方案选型"><strong>方案选型</strong></h4>
<p>依据图片是否由设备本地生成,快照可分为前端处理和后端处理两种方式。</p>
<p>由于后端生成的方案依赖于网络通信,不可避免地存在通信开销和等待时延,同时对于模板和数据结构变更也有一定的维护成本。</p>
<p>因此,出于实时性和灵活性等综合考虑,我们优先选用前端处理的方式。</p>
<h4 id="基本原理"><strong>基本原理</strong></h4>
<p>前端侧对于快照的处理过程,实质上是将 DOM 节点包含的视图信息转换为图片信息的过程。这个过程可以借助 <code class="language-plaintext highlighter-rouge">canvas</code> 的原生 API 实现,这也是方案可行性的基础。</p>
<p>具体来说,转换过程是将目标 DOM 节点绘制到 canvas 画布,然后 canvas 画布以图片形式导出。可简单标记为绘制阶段和导出阶段两个步骤:</p>
<p>绘制阶段:选择希望绘制的 DOM 节点,根据nodeType调用 canvas 对象的对应 API,将目标 DOM 节点绘制到 canvas 画布(例如对于<img />的绘制使用 drawImage 方法)。
导出阶段:通过 canvas 的 toDataURL 或 getImageData 等对外接口,最终实现画布内容的导出。</p>
<h4 id="原生示例"><strong>原生示例</strong></h4>
<p>具体地,对于单个<img />元素可按如下方式生成自身的快照:</p>
<h5 id="html">HTML:</h5>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><img</span> <span class="na">id=</span><span class="s">"target"</span> <span class="na">src=</span><span class="s">"./music-icon.png"</span> <span class="nt">/></span>
</code></pre></div></div>
<h5 id="javascript">JavaScript:</h5>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 获取目标元素</span>
<span class="kd">const</span> <span class="nx">target</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">target</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">// 新建canvas画布</span>
<span class="kd">const</span> <span class="nx">canvas</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">canvas</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="mi">100</span><span class="p">;</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">height</span> <span class="o">=</span> <span class="mi">100</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">ctx</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">getContext</span><span class="p">(</span><span class="dl">"</span><span class="s2">2d</span><span class="dl">"</span><span class="p">);</span>
<span class="c1">// 导出阶段:从canvas导出新的图片</span>
<span class="kd">const</span> <span class="nx">exportNewImage</span> <span class="o">=</span> <span class="p">(</span><span class="nx">canvas</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">exportImage</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">img</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">exportImage</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">toDataURL</span><span class="p">();</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">exportImage</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">// 绘制阶段:待图片内容加载完毕后绘制画布</span>
<span class="nx">target</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// 将图片内容绘入画布</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">drawImage</span><span class="p">(</span><span class="nx">target</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">100</span><span class="p">);</span>
<span class="c1">// 将画布内容导出为新的图片</span>
<span class="nx">exportNewImage</span><span class="p">(</span><span class="nx">canvas</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>其中,drawImage是 canvas 上下文对象的实例方法,提供多种方式将 CanvasImageSource (CanvasImageSource是一个辅助类型,描述下面类型的任何一个对象:<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLImageElement"><code class="language-plaintext highlighter-rouge">HTMLImageElement</code></a>, <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLVideoElement"><code class="language-plaintext highlighter-rouge">HTMLVideoElement</code></a>, <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLCanvasElement"><code class="language-plaintext highlighter-rouge">HTMLCanvasElement</code></a>, <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D"><code class="language-plaintext highlighter-rouge">CanvasRenderingContext2D</code></a>, 或 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/ImageBitmap"><code class="language-plaintext highlighter-rouge">ImageBitmap</code></a>)源绘制到 canvas 画布上。exportNewImage用于将 canvas 中的视图信息导出为包含图片展示的 data URI。</p>
<h2 id="基础方案">基础方案</h2>
<hr />
<p>在上一部分中,我们可以看到基于 canvas 提供的相关基础 API,为前端侧的页面快照处理提供了可能。</p>
<p>然而,具体的业务应用往往更加复杂,上面的「低配版」实例显然未能覆盖多数的实际场景,例如:</p>
<ul>
<li>canvas 的<code class="language-plaintext highlighter-rouge">drawImage</code>方法只接受 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasImageSource">CanvasImageSource</a>,而<code class="language-plaintext highlighter-rouge">CanvasImageSource</code>并不包括<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType">文本节点</a>、普通的<code class="language-plaintext highlighter-rouge">div</code>等,将非``的元素绘制到 canvas 需要特定处理。</li>
<li>当有多个 DOM 元素需要绘制时,层级优先级处理较为复杂。</li>
<li>需要关注<code class="language-plaintext highlighter-rouge">float</code>、<code class="language-plaintext highlighter-rouge">z-index</code>、<code class="language-plaintext highlighter-rouge">position</code>等布局定位的处理。</li>
<li>样式合成绘制计算较为繁琐。</li>
</ul>
<p>因此,基于对综合业务场景的考虑,我们采用社区中认可度较高的方案:<code class="language-plaintext highlighter-rouge">html2canvas</code>和<code class="language-plaintext highlighter-rouge">canvas2image</code>作为实现快照功能的基础库。</p>
<h3 id="html2canvas">html2canvas</h3>
<blockquote>
<p>提供将 DOM 绘制到 canvas 的能力</p>
</blockquote>
<p>这款来自社区的神器,为开发者简化了将逐个 DOM 绘制到 canvas 的过程。简单来说,其<strong>基本原理</strong>为:</p>
<ul>
<li>递归遍历目标节点及其子节点,收集节点的样式信息;</li>
<li>计算节点本身的层级关系,根据一定优先级策略将节点逐一绘制到 canvas 画布中;</li>
<li>重复这一过程,最终实现目标节点内容的全部绘制。</li>
</ul>
<p>在使用方面,<code class="language-plaintext highlighter-rouge">html2canvas</code>对外暴露了一个可执行函数,它的第一个参数用于接收待绘制的目标节点(必选);第二个参数是可选的<a href="https://html2canvas.hertzen.com/configuration">配置项</a>,用于设置涉及 canvas 导出的各个参数:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// element 为目标绘制节点,options为可选参数</span>
<span class="nx">html2canvas</span><span class="p">(</span><span class="nx">element</span><span class="p">[,</span><span class="nx">options</span><span class="p">]);</span>
</code></pre></div></div>
<p>简易调用示例如下:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">html2canvas</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">html2canvas</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{};</span>
<span class="c1">// 输入body节点,返回包含body视图内容的canvas对象</span>
<span class="nx">html2canvas</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">,</span> <span class="nx">options</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">canvas</span><span class="p">)</span> <span class="p">{</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">canvas</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>
<h3 id="canvas2image">canvas2image</h3>
<blockquote>
<p>提供由 canvas 导出图片信息的多种方法</p>
</blockquote>
<p>相比于<code class="language-plaintext highlighter-rouge">html2canvas</code>承担的复杂绘制流程,<a href="https://github.com/hongru/canvas2image">canvas2image</a> 所要做的事情简单的多。</p>
<p><code class="language-plaintext highlighter-rouge">canvas2image</code>仅用于将输入的 canvas 对象按特定格式转换和存储操作,其中这两类操作均支持 PNG,JPEG,GIF,BMP 四种图片类型:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 格式转换</span>
<span class="nx">Canvas2Image</span><span class="p">.</span><span class="nx">convertToPNG</span><span class="p">(</span><span class="nx">canvasObj</span><span class="p">,</span> <span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="p">);</span>
<span class="nx">Canvas2Image</span><span class="p">.</span><span class="nx">convertToJPEG</span><span class="p">(</span><span class="nx">canvasObj</span><span class="p">,</span> <span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="p">);</span>
<span class="nx">Canvas2Image</span><span class="p">.</span><span class="nx">convertToGIF</span><span class="p">(</span><span class="nx">canvasObj</span><span class="p">,</span> <span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="p">);</span>
<span class="nx">Canvas2Image</span><span class="p">.</span><span class="nx">convertToBMP</span><span class="p">(</span><span class="nx">canvasObj</span><span class="p">,</span> <span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="p">);</span>
<span class="c1">// 另存为指定格式图片</span>
<span class="nx">Canvas2Image</span><span class="p">.</span><span class="nx">saveAsPNG</span><span class="p">(</span><span class="nx">canvasObj</span><span class="p">,</span> <span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="p">);</span>
<span class="nx">Canvas2Image</span><span class="p">.</span><span class="nx">saveAsJPEG</span><span class="p">(</span><span class="nx">canvasObj</span><span class="p">,</span> <span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="p">);</span>
<span class="nx">Canvas2Image</span><span class="p">.</span><span class="nx">saveAsGIF</span><span class="p">(</span><span class="nx">canvasObj</span><span class="p">,</span> <span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="p">);</span>
<span class="nx">Canvas2Image</span><span class="p">.</span><span class="nx">saveAsBMP</span><span class="p">(</span><span class="nx">canvasObj</span><span class="p">,</span> <span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="p">);</span>
</code></pre></div></div>
<p>实质上,<code class="language-plaintext highlighter-rouge">canvas2image</code>只是提供了针对 canvas 基础 API 的二次封装(例如 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/ImageData">getImageData</a>、<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLCanvasElement/toDataURL">toDataURL</a>),而本身并不依赖<code class="language-plaintext highlighter-rouge">html2canvas</code>。</p>
<p>在使用方面,由于目前<a href="https://github.com/hongru/canvas2image">作者</a>并未提供 ES6 版本的<code class="language-plaintext highlighter-rouge">canvas2image</code>(v1.0.5),暂时不能直接以 import 方式引入该模块。</p>
<p>对于支持现代化构建的工程中(例如 <a href="https://webpack.github.io/">webpack</a>),开发者可以自助 clone 源码并手动添加 export 获得 ESM 支持:</p>
<p><strong>支持 ESM 导出</strong>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// canvas2Image.js</span>
<span class="kd">const</span> <span class="nx">Canvas2Image</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
<span class="p">...</span>
<span class="p">}();</span>
<span class="c1">// 以下为定制添加的内容</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">Canvas2Image</span><span class="p">;</span>
</code></pre></div></div>
<p><strong>调用示例</strong>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">Canvas2Image</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./canvas2Image.js</span><span class="dl">'</span><span class="p">;</span>
<span class="c1">// 其中,canvas代表传入的canvas对象,width, height分别为导出图片的宽高数值</span>
<span class="nx">Canvas2Image</span><span class="p">.</span><span class="nx">convertToPNG</span><span class="p">(</span><span class="nx">canvas</span><span class="p">,</span> <span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="p">)</span>
</code></pre></div></div>
<h3 id="组合技">组合技</h3>
<p>接下来,我们基于以上两个工具库,实现一个基础版的快照生成方案。同样是分为两个阶段,对应 3.2 节的基本原理:</p>
<ul>
<li>第一步,通过<code class="language-plaintext highlighter-rouge">html2canvas</code>实现 DOM 节点绘制到 canvas 对象中;</li>
<li>第二步,将上一步返回的 canvas 对象传入<code class="language-plaintext highlighter-rouge">canvas2image</code>,进而按需导出快照图片信息。</li>
</ul>
<p>具体地,我们封装一个<code class="language-plaintext highlighter-rouge">convertToImage</code>的函数,用于输入目标节点以及<a href="http://html2canvas.hertzen.com/configuration/">配置项参数</a>,输出快照图片信息。</p>
<p><code class="language-plaintext highlighter-rouge">JavaScript</code>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// convertToImage.js</span>
<span class="k">import</span> <span class="nx">html2canvas</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">html2canvas</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">Canvas2Image</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./canvas2Image.js</span><span class="dl">'</span><span class="p">;</span>
<span class="cm">/**
* 基础版快照方案
* @param {HTMLElement} container
* @param {object} options html2canvas相关配置
*/</span>
<span class="kd">function</span> <span class="nx">convertToImage</span><span class="p">(</span><span class="nx">container</span><span class="p">,</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">html2canvas</span><span class="p">(</span><span class="nx">container</span><span class="p">,</span> <span class="nx">options</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">canvas</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">imageEl</span> <span class="o">=</span> <span class="nx">Canvas2Image</span><span class="p">.</span><span class="nx">convertToPNG</span><span class="p">(</span><span class="nx">canvas</span><span class="p">,</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">width</span><span class="p">,</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">height</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">imageEl</span><span class="p">;</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="进阶优化">进阶优化</h2>
<hr />
<p>通过上一节的实例,我们基于<code class="language-plaintext highlighter-rouge">html2canvas</code>和<code class="language-plaintext highlighter-rouge">canvas2image</code>,实现了相比原生方案<strong>通用性</strong>更佳的基础页面快照方案。然而面对实际复杂的应用场景,以上基础方案生成的快照效果往往不尽如人意。</p>
<p>快照效果的<strong>差异性</strong>,一方面是由于<code class="language-plaintext highlighter-rouge">html2canvas</code>导出的视图信息是通过各种 DOM 和 canvas 的 API 复合计算二次绘制的结果(并非一键栅格化)。因此不同宿主环境的相关 API 实现差异,可能导致生成的图片效果存在多端不一致性或者显示异常的情况。</p>
<p>另一方面,业务层面的因素,例如对于开发者<code class="language-plaintext highlighter-rouge">html2canvas</code>的配置有误或者是页面布局不当等原因,也会对生成快照的结果带来偏差。</p>
<p>社区中也可以常见到一些对于生成快照质量的讨论,例如:</p>
<ul>
<li>为什么有些内容显示不完整、残缺、白屏或黑屏?</li>
<li>明明原页面清晰可辨,为什么生成的图片模糊如毛玻璃?</li>
<li>将页面转换为图片的过程十分缓慢,影响后续相关操作,有什么好办法么?</li>
<li>…</li>
</ul>
<p>下面我们从<strong>内容完整性</strong>、<strong>清晰度优化</strong>和<strong>转换效率</strong>,进一步探究高质量的快照解决方案。</p>
<h3 id="内容完整性">内容完整性</h3>
<blockquote>
<p>首要问题:保证目标节点视图信息完整导出</p>
</blockquote>
<p>由于真机环境的兼容性和业务实现方式的不同,在一些使用<code class="language-plaintext highlighter-rouge">html2canvas</code>过程中常会出现快照内容与原视图不一致的情况。内容不完整的常见自检<code class="language-plaintext highlighter-rouge">checklist</code>如下:</p>
<ul>
<li><strong>跨域问题</strong>:存在跨域图片污染 canvas 画布。</li>
<li><strong>资源加载</strong>:生成快照时,相关资源还未加载完毕。</li>
<li><strong>滚动问题</strong>:页面中滚动元素存在偏移量,导致生成的快照顶部出现空白。</li>
</ul>
<h4 id="跨域问题">跨域问题</h4>
<p>常见于引入的图片素材相对于部署工程<a href="http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html">跨域</a>的场景。例如部署在<code class="language-plaintext highlighter-rouge">https://st.music.163.com/</code>上面的页面中引入了来源为<code class="language-plaintext highlighter-rouge">https://p1.music.126.net</code>的图片,这类图片即是属于跨域的图片资源。</p>
<p>由于 canvas 对于图片资源的<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image">同源限制</a>,如果画布中包含跨域的图片资源则会污染画布( <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image">Tainted canvases</a> ),造成生成图片内容混乱或者<code class="language-plaintext highlighter-rouge">html2canvas</code>方法不执行等异常问题。</p>
<p>对于跨域图片资源处理,可以从以下几方面着手:</p>
<h5 id="1usecors-配置"><strong>(1)useCORS 配置</strong></h5>
<p>开启<code class="language-plaintext highlighter-rouge">html2canvas</code>的<code class="language-plaintext highlighter-rouge">useCORS</code>配置项,示例如下:
</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// doc: http://html2canvas.hertzen.com/configuration/</span>
<span class="kd">const</span> <span class="nx">opts</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">useCORS</span> <span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="c1">// 允许使用跨域图片</span>
<span class="na">allowTaint</span><span class="p">:</span> <span class="kc">false</span> <span class="c1">// 不允许跨域图片污染画布</span>
<span class="p">};</span>
<span class="nx">html2canvas</span><span class="p">(</span><span class="nx">element</span><span class="p">,</span> <span class="nx">opts</span><span class="p">);</span>
</code></pre></div></div>
<p>在<code class="language-plaintext highlighter-rouge">html2canvas</code>的源码中对于<code class="language-plaintext highlighter-rouge">useCORS</code>配置项置为<code class="language-plaintext highlighter-rouge">true</code>的处理,实质上是将目标节点中的``标签注入 <a href="https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-crossorigin">crossOrigin</a> 为<code class="language-plaintext highlighter-rouge">anonymous</code>的属性,从而允许载入符合 <a href="https://developer.mozilla.org/zh-CN/docs/Glossary/CORS">CORS</a> 规范的图片资源。</p>
<p>其中,<code class="language-plaintext highlighter-rouge">allowTaint</code>默认为<code class="language-plaintext highlighter-rouge">false</code>,也可以不作显式设置。即使该项置为<code class="language-plaintext highlighter-rouge">true</code>,也不能绕过 canvas 对于跨域图片的限制,因为在调用 canvas 的<code class="language-plaintext highlighter-rouge">toDataURL</code>时依然会被浏览器禁止。</p>
<h5 id="2cors-配置"><strong>(2)CORS 配置</strong></h5>
<p>上一步的<code class="language-plaintext highlighter-rouge">useCORS</code>的配置,只是允许``接收跨域的图片资源,而对于解锁跨域图片在 canvas 上的绘制并导出,需要图片资源本身需要提供 CORS 支持。</p>
<p>这里介绍下跨域图片使用 <a href="https://developer.mozilla.org/zh-CN/docs/Glossary/CDN">CDN</a> 资源时的注意事项:</p>
<p>验证图片资源是否支持 CORS 跨域,通过 Chrome 开发者工具可以看到图片请求响应头中应含有<code class="language-plaintext highlighter-rouge">Access-Control-Allow-Origin</code>的字段,即坊间常提到的<a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Allow-Origin">跨域头</a>。</p>
<p>例如,某个来自 CDN 图片资源的响应头示例:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">Response</span><span class="w"> </span><span class="err">Headers</span><span class="w">
</span><span class="err">access-control-allow-credentials:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="err">access-control-allow-headers:</span><span class="w"> </span><span class="err">DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type</span><span class="w">
</span><span class="err">access-control-allow-methods:</span><span class="w"> </span><span class="err">GET,POST,OPTIONS</span><span class="w">
</span><span class="err">access-control-allow-origin:</span><span class="w"> </span><span class="err">*</span><span class="w">
</span></code></pre></div></div>
<p>不同的 CDN 服务商配置资源跨域头的方式不同,具体应咨询 CDN 服务商。</p>
<p>特殊情况下,部分 CDN 提供方可能会存在图片缓存不含 CORS 跨域头的情况。为保证快照显示正常,建议优先联系 CDN 寻求技术支持,不推荐通过图片链接后缀时间戳等方式强制回源,避免影响源站性能和 CDN 计费。</p>
<h5 id="3服务端转发"><strong>(3)服务端转发</strong></h5>
<p>在微信等第三方 APP 中,平台的用户头像等图片资源是不直接提供 CORS 支持的。此时需要借助服务端作代理转发,从而绕过跨域限制。</p>
<p>即通过服务端代为请求平台用户的头像地址并转发给客户端(浏览器),当然这个服务端接口本身要与页面同源或者支持 CORS。</p>
<p>为简洁表述,假设前端与后端针对跨域图片转发作如下约定,且该接口与前端工程部署在相同域名下:</p>
<table>
<thead>
<tr>
<th>请求地址</th>
<th>请求方式</th>
<th>传入参数</th>
<th>返回信息</th>
</tr>
</thead>
<tbody>
<tr>
<td>/api/redirect/image</td>
<td><code class="language-plaintext highlighter-rouge">GET</code></td>
<td>redirect,表示原图地址</td>
<td><code class="language-plaintext highlighter-rouge">Content-Type</code>为<code class="language-plaintext highlighter-rouge">image/png</code>的图片资源</td>
</tr>
</tbody>
</table>
<p>页面中的``通过拼接<code class="language-plaintext highlighter-rouge">/api/redirect/image</code>与代表原图地址的查询参数<code class="language-plaintext highlighter-rouge">redirect</code>,发出一个 GET 请求图片资源。由于接口与页面同源,因此不会触发跨域限制:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><img</span> <span class="na">src=</span><span class="s">"/api/redirect/image?redirect=thirdwx.qlogo.cn/somebody/avatar"</span> <span class="na">alt=</span><span class="s">"user-pic"</span> <span class="na">class=</span><span class="s">"avatar"</span> <span class="na">crossorigin=</span><span class="s">"anonymous"</span><span class="nt">></span>
</code></pre></div></div>
<p>对于服务端接口的实现,这里基于 <a href="https://github.com/koajs/koa">koa</a> 提供了一则简易示例:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Koa</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">koa</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">router</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">koa-router</span><span class="dl">'</span><span class="p">)();</span>
<span class="kd">const</span> <span class="nx">querystring</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">querystring</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Koa</span><span class="p">();</span>
<span class="cm">/**
* 图片转发接口
* - 接收 redirect 入参,即需要代为请求的图片URL
* - 返回图片资源
*/</span>
<span class="nx">router</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">/api/redirect/image</span><span class="dl">'</span><span class="p">,</span> <span class="k">async</span> <span class="kd">function</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">querys</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">querystring</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">querys</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">redirect</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">querystring</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">querys</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">proxyFetchImage</span><span class="p">(</span><span class="nx">redirect</span><span class="p">);</span>
<span class="nx">ctx</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">image/png</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">ctx</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">Cache-Control</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">max-age=2592000</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="nx">res</span><span class="p">;</span>
<span class="p">})</span>
<span class="cm">/**
* 请求并返回图片资源
* @param {String} url 图片地址
*/</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">proxyFetchImage</span><span class="p">(</span><span class="nx">url</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">;</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">proxyFetchImage</span><span class="p">(</span><span class="nx">redirect</span><span class="p">);</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">router</span><span class="p">.</span><span class="nx">routes</span><span class="p">());</span>
</code></pre></div></div>
<p>在浏览器看来,页面请求的图片资源仍是相同域名下的资源,转发过程对前端透明。建议在需求开发前了解图片资源的来源情况,明确是否需要服务端支持。</p>
<p>在云音乐早期的活动「<a href="https://st.music.163.com/c/gameofthrones">权力的游戏</a>」中,使用了同类方案,实现了微信平台中用户头像的完整绘制和快照导出。</p>
<h5 id="4nginx重定向">(4)Nginx重定向</h5>
<p>属于服务器转发分值中一种可选方案,都是通过服务器配置代理进行同源处理</p>
<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">location</span> <span class="p">~</span> <span class="sr">/image_proxy_oss/</span> <span class="p">{</span>
<span class="kn">include</span> <span class="nc">vhost/nginx</span><span class="s">_cor.conf</span><span class="p">;</span>
<span class="kn">if</span> <span class="s">(</span><span class="nv">$request_uri</span> <span class="p">~</span> <span class="sr">"/image_proxy_oss/(\w+)/(.*)")</span> <span class="p">{</span>
<span class="kn">set</span> <span class="nv">$proxy_uri</span> <span class="nv">$2</span><span class="p">;</span>
<span class="kn">set</span> <span class="nv">$proxy_image_url</span> <span class="s">http://</span><span class="nv">$1</span><span class="nf">.oss.baidu.storage</span><span class="p">:</span><span class="mi">8080</span><span class="p">;</span>
<span class="kn">rewrite</span> <span class="s">.</span> <span class="n">/</span><span class="nv">$proxy_uri</span> <span class="s">break</span><span class="p">;</span>
<span class="p">}</span>
<span class="kn">proxy_pass</span> <span class="nv">$proxy_image_url</span><span class="p">;</span>
<span class="kn">add_header</span> <span class="s">Access-Control-Allow-Origin</span> <span class="s">*</span><span class="p">;</span>
<span class="kn">add_header</span> <span class="s">Access-Control-Allow-Headers</span> <span class="s">X-Requested-With</span><span class="p">;</span>
<span class="kn">add_header</span> <span class="s">Access-Control-Allow-Methods</span> <span class="s">GET,POST,OPTIONS</span><span class="p">;</span>
<span class="kn">proxy_read_timeout</span> <span class="mi">240</span><span class="p">;</span>
<span class="kn">client_max_body_size</span> <span class="mi">1000m</span><span class="p">;</span>
<span class="kn">keepalive_timeout</span> <span class="mi">120</span><span class="p">;</span>
<span class="kn">proxy_http_version</span> <span class="mi">1</span><span class="s">.1</span><span class="p">;</span>
<span class="kn">proxy_set_header</span> <span class="s">Connection</span> <span class="s">""</span><span class="p">;</span>
<span class="kn">proxy_connect_timeout</span> <span class="mi">60</span><span class="p">;</span>
<span class="kn">proxy_send_timeout</span> <span class="mi">200</span><span class="p">;</span>
<span class="kn">keepalive_requests</span> <span class="mi">3000</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<h4 id="资源加载">资源加载</h4>
<p>资源加载不全,是造成快照不完整的一个常见因素。在生成快照时,如果部分资源没有加载完毕,那么生成的内容自然也谈不上完整。</p>
<p>除了设置一定的延迟外,如果要确保资源加载完毕,可以基于 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/all">Promise.all</a> 实现。</p>
<p><strong>加载图片</strong>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">preloadImg</span> <span class="o">=</span> <span class="p">(</span><span class="nx">src</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">img</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Image</span><span class="p">();</span>
<span class="nx">img</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">resolve</span><span class="p">();</span>
<span class="p">}</span>
<span class="nx">img</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">src</span><span class="p">;</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p><strong>确保在全部加载后生成快照</strong>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">preloadList</span> <span class="o">=</span> <span class="p">[</span>
<span class="dl">'</span><span class="s1">./pic-1.png</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">./pic-2.png</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">./pic-3.png</span><span class="dl">'</span><span class="p">,</span>
<span class="p">];</span>
<span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">(</span><span class="nx">preloadList</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">src</span> <span class="o">=></span> <span class="nx">preloadImg</span><span class="p">(</span><span class="nx">src</span><span class="p">))).</span><span class="nx">then</span><span class="p">(</span><span class="k">async</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">convertToImage</span><span class="p">(</span><span class="nx">container</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">canvas</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// ...</span>
<span class="p">})</span>
<span class="p">});</span>
</code></pre></div></div>
<p>实际上,以上方法只是解决页面图片的显示问题。在真实场景中,即使页面上的图片显示完整,保存快照后依然可能出现内容空白的情况。
原因是 html2canvas 库内部处理时,对图片资源仍会做一次加载请求;如果此时加载失败,那么该部分保存快照后即是空白的。</p>
<p>下面介绍图片资源转 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Blob">Blob</a> 的方案,保证图片的地址来自本地,避免在快照转化时加载失败的情况。
这里提到的 Blob 对象表示一个不可变、代表二进制原始数据的类文件对象,在特定的<a href="https://juejin.im/post/59e35d0e6fb9a045030f1f35">使用场景</a>会使用到。</p>
<p><strong>图片资源转 Blob:</strong></p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1">// 返回图片Blob地址</span>
<span class="kd">const</span> <span class="nx">toBlobURL</span> <span class="o">=</span> <span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">urlMap</span> <span class="o">=</span> <span class="p">{};</span>
<span class="c1">// @param {string} url 传入图片资源地址</span>
<span class="k">return</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">url</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// 过滤重复值</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">urlMap</span><span class="p">[</span><span class="nx">url</span><span class="p">])</span> <span class="k">return</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">resolve</span><span class="p">(</span><span class="nx">urlMap</span><span class="p">[</span><span class="nx">url</span><span class="p">]);</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">canvas</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">canvas</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">ctx</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">getContext</span><span class="p">(</span><span class="dl">'</span><span class="s1">2d</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">img</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">img</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">img</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">url</span><span class="p">;</span>
<span class="nx">img</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="nx">img</span><span class="p">.</span><span class="nx">width</span><span class="p">;</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">height</span> <span class="o">=</span> <span class="nx">img</span><span class="p">.</span><span class="nx">height</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">drawImage</span><span class="p">(</span><span class="nx">img</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
<span class="c1">// 关键👇</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">toBlob</span><span class="p">((</span><span class="nx">blob</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">blobURL</span> <span class="o">=</span> <span class="nx">URL</span><span class="p">.</span><span class="nx">createObjectURL</span><span class="p">(</span><span class="nx">blob</span><span class="p">);</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">blobURL</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">};</span>
<span class="nx">img</span><span class="p">.</span><span class="nx">onerror</span> <span class="o">=</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">reject</span><span class="p">(</span><span class="nx">e</span><span class="p">);</span>
<span class="p">};</span>
<span class="p">});</span>
<span class="p">};</span>
<span class="p">}());</span>
</code></pre></div></div>
<p>以上<code class="language-plaintext highlighter-rouge">toBlobURL</code>方法实现将加载``的资源链接转为 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/URL/createObjectURL">blobURL</a>。</p>
<p>进一步地,通过<code class="language-plaintext highlighter-rouge">convertToBlobImage</code>方法,实现对于传入的目标节点中的``批量处理为<code class="language-plaintext highlighter-rouge">Blob</code>格式。</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 批量处理</span>
<span class="kd">function</span> <span class="nx">convertToBlobImage</span><span class="p">(</span><span class="nx">targetNode</span><span class="p">,</span> <span class="nx">timeout</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">targetNode</span><span class="p">)</span> <span class="k">return</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">resolve</span><span class="p">();</span>
<span class="kd">let</span> <span class="nx">nodeList</span> <span class="o">=</span> <span class="nx">targetNode</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">targetNode</span> <span class="k">instanceof</span> <span class="nx">Element</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">targetNode</span><span class="p">.</span><span class="nx">tagName</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">()</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">img</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">nodeList</span> <span class="o">=</span> <span class="p">[</span><span class="nx">targetNode</span><span class="p">];</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">nodeList</span> <span class="o">=</span> <span class="nx">targetNode</span><span class="p">.</span><span class="nx">getElementsByTagName</span><span class="p">(</span><span class="dl">'</span><span class="s1">img</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="p">(</span><span class="nx">nodeList</span> <span class="k">instanceof</span> <span class="nb">Array</span><span class="p">)</span> <span class="o">&&</span> <span class="o">!</span><span class="p">(</span><span class="nx">nodeList</span> <span class="k">instanceof</span> <span class="nx">NodeList</span><span class="p">))</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">[convertToBlobImage] 必须是Element或NodeList类型</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">nodeList</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="k">return</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">resolve</span><span class="p">();</span>
<span class="c1">// 仅考虑<img></span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">resolved</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="c1">// 超时处理</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">timeout</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">setTimeout</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">resolved</span><span class="p">)</span> <span class="nx">resolve</span><span class="p">();</span>
<span class="nx">resolved</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">},</span> <span class="nx">timeout</span><span class="p">);</span>
<span class="p">}</span>
<span class="kd">let</span> <span class="nx">count</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="c1">// 逐一替换<img>资源地址</span>
<span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">len</span> <span class="o">=</span> <span class="nx">nodeList</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span> <span class="o"><</span> <span class="nx">len</span><span class="p">;</span> <span class="o">++</span><span class="nx">i</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">v</span> <span class="o">=</span> <span class="nx">nodeList</span><span class="p">[</span><span class="nx">i</span><span class="p">];</span>
<span class="kd">let</span> <span class="nx">p</span> <span class="o">=</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">resolve</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">v</span><span class="p">.</span><span class="nx">tagName</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">()</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">img</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">p</span> <span class="o">=</span> <span class="nx">toBlobURL</span><span class="p">(</span><span class="nx">v</span><span class="p">.</span><span class="nx">src</span><span class="p">).</span><span class="nx">then</span><span class="p">((</span><span class="nx">blob</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">v</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">blob</span><span class="p">;</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="nx">p</span><span class="p">.</span><span class="k">finally</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">++</span><span class="nx">count</span> <span class="o">===</span> <span class="nx">nodeList</span><span class="p">.</span><span class="nx">length</span> <span class="o">&&</span> <span class="o">!</span><span class="nx">resolved</span><span class="p">)</span> <span class="nx">resolve</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">convertToBlobImage</span><span class="p">;</span>
</code></pre></div></div>
<p>使用方面,<code class="language-plaintext highlighter-rouge">convertToBlobImage</code>应在调用生成快照<code class="language-plaintext highlighter-rouge">convertToImage</code>方法前执行。</p>
<h4 id="滚动问题">滚动问题</h4>
<ul>
<li>典型特征:生成快照的顶部存在空白区域。</li>
<li>原因:一般是保存长图(超过一屏),并且滚动条不在顶部时导致(常见于 <a href="https://developer.mozilla.org/en-US/docs/Glossary/SPA">SPA</a> 类应用)。</li>
<li>解决办法:在调用<code class="language-plaintext highlighter-rouge">convertToImage</code>之前,先记录此时的<code class="language-plaintext highlighter-rouge">scrollTop</code>,然后调用<code class="language-plaintext highlighter-rouge">window.scroll(0, 0)</code>将页面移动至顶部。待快照生成后,再调用<code class="language-plaintext highlighter-rouge">window.scroll(0, scrollTop)</code>恢复原有纵向偏移量。</li>
</ul>
<p><strong>示例</strong>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 待保存的目标节点(按实际修改👇)</span>
<span class="kd">const</span> <span class="nx">container</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">;</span>
<span class="c1">// 实际的滚动元素(按实际修改👇)</span>
<span class="kd">const</span> <span class="nx">scrollElement</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">;</span>
<span class="c1">// 记录滚动元素纵向偏移量</span>
<span class="kd">const</span> <span class="nx">scrollTop</span> <span class="o">=</span> <span class="nx">scrollElement</span><span class="p">.</span><span class="nx">scrollTop</span><span class="p">;</span>
<span class="c1">// 针对滚动元素是 body 先作置顶</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">scroll</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
<span class="nx">convertToImage</span><span class="p">(</span><span class="nx">container</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// ...</span>
<span class="p">}).</span><span class="k">catch</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// ...</span>
<span class="p">}).</span><span class="k">finally</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// 恢复偏移量</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">scroll</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">scrollTop</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>
<p>特别地,对于存在<strong>局部滚动</strong>布局的情况,也可以操作对应滚动元素置顶避免容器顶部空白的情况。</p>
<h3 id="清晰度优化">清晰度优化</h3>
<blockquote>
<p>清晰度是快照质量的分水岭</p>
</blockquote>
<p>下图取自「<a href="https://st.music.163.com/c/gameofthrones">权力的游戏</a>」中两张优化前后的结果页快照对比。可以看到优化前的左图,无论是在文字边缘还是图像细节上,相较优化后的清晰度存在明显可辨的差距。</p>
<p><img src="https://segmentfault.com/img/remote/1460000021275787" alt="clear" /></p>
<p>最终生成快照的清晰度,源头上取决于第一步中 DOM 转换成的 canvas 的清晰度。</p>
<p>以下介绍 5 种行之有效的清晰度优化方法。</p>
<h4 id="1-使用px单位">1 使用px单位</h4>
<p>为了给到<code class="language-plaintext highlighter-rouge">html2canvas</code>明确的整数计算值,避免因小数舍入而导致的拉伸模糊,建议将布局中使用中使用<code class="language-plaintext highlighter-rouge">%</code>、<code class="language-plaintext highlighter-rouge">vw</code>、<code class="language-plaintext highlighter-rouge">vh</code>或<code class="language-plaintext highlighter-rouge">rem</code>等单位的元素样式,统一改为使用<code class="language-plaintext highlighter-rouge">px</code>。</p>
<p><strong>good:</strong></p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">style=</span><span class="s">"width: 100px;"</span><span class="nt">></div></span>
</code></pre></div></div>
<p><strong>bad:</strong></p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">style=</span><span class="s">"width: 30%;"</span><span class="nt">></div></span>
</code></pre></div></div>
<h4 id="2-优先使用-img-标签展示图片">2 优先使用 img 标签展示图片</h4>
<p>很多情况下,<strong>导出图片模糊是由原视图中的图片是以 css 中 background 的方式显示的。</strong>
<strong>因为 background-size 并不会反馈一个具体的宽高数值,而是通过枚举值如 contain、cover 等代表图片缩放的类型;</strong>相对于<code class="language-plaintext highlighter-rouge">标签, background 方式最终生成的图片会较为模糊。
将 background 改为</code>方式呈现,对于图片清晰度会有一定的改观。对于必须要使用 background 的场景,参见 第五点的解决方案。</p>
<p><strong>good:</strong></p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><img</span> <span class="na">class=</span><span class="s">"u-image"</span> <span class="na">src=</span><span class="s">"./music.png"</span> <span class="na">alt=</span><span class="s">"icon"</span><span class="nt">></span>
</code></pre></div></div>
<p><strong>bad:</strong></p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"u-image"</span> <span class="na">style=</span><span class="s">"background: url(./music.png);"</span><span class="nt">></div></span>
</code></pre></div></div>
<h4 id="3-配置高倍的-canvas-画布">3 配置高倍的 canvas 画布</h4>
<p>对于高分辨率的屏幕,canvas 可通过将 css 像素与高分屏的物理像素对齐,实现一定程度的清晰度提升(<a href="https://www.cnblogs.com/zaoa/p/8630393.html">这里</a>对两类像素有详细描述和讨论)。</p>
<p>在具体操作中,创建由 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Window/devicePixelRatio">devicePixelRatio</a> 放大的图像,然后使用 css 将其缩小相同的倍数,有效地提高绘制到 canvas 中的图像清晰度表现。</p>
<blockquote>
<p>Window.devicePixelRatio此属性返回当前显示设备的物理像素分辨率与CSS像素分辨率的比值。该值也可以被解释为像素大小的比例:即一个CSS像素的大小相对于一个物理像素的大小的比值。</p>
</blockquote>
<p>在使用<code class="language-plaintext highlighter-rouge">html2canvas</code>时,我们可以配置一个放缩后的 canvas 画布用于导入节点的绘制。</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// convertToImage.js</span>
<span class="k">import</span> <span class="nx">html2canvas</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">html2canvas</span><span class="dl">'</span><span class="p">;</span>
<span class="c1">// 创建用于绘制的基础canvas画布</span>
<span class="kd">function</span> <span class="nx">createBaseCanvas</span><span class="p">(</span><span class="nx">scale</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">canvas</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">"</span><span class="s2">canvas</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="nx">width</span> <span class="o">*</span> <span class="nx">scale</span><span class="p">;</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">height</span> <span class="o">=</span> <span class="nx">height</span> <span class="o">*</span> <span class="nx">scale</span><span class="p">;</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">getContext</span><span class="p">(</span><span class="dl">"</span><span class="s2">2d</span><span class="dl">"</span><span class="p">).</span><span class="nx">scale</span><span class="p">(</span><span class="nx">scale</span><span class="p">,</span> <span class="nx">scale</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">canvas</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">// 生成快照</span>
<span class="kd">function</span> <span class="nx">convertToImage</span><span class="p">(</span><span class="nx">container</span><span class="p">,</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
<span class="c1">// 设置放大倍数</span>
<span class="kd">const</span> <span class="nx">scale</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">devicePixelRatio</span><span class="p">;</span>
<span class="c1">// 创建用于绘制的基础canvas画布</span>
<span class="kd">const</span> <span class="nx">canvas</span> <span class="o">=</span> <span class="nx">createBaseCanvas</span><span class="p">(</span><span class="nx">scale</span><span class="p">);</span>
<span class="c1">// 传入节点原始宽高</span>
<span class="kd">const</span> <span class="nx">width</span> <span class="o">=</span> <span class="nx">container</span><span class="p">.</span><span class="nx">offsetWidth</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">height</span> <span class="o">=</span> <span class="nx">container</span><span class="p">.</span><span class="nx">offsetHeight</span><span class="p">;</span>
<span class="c1">// html2canvas配置项</span>
<span class="kd">const</span> <span class="nx">ops</span> <span class="o">=</span> <span class="p">{</span>
<span class="nx">scale</span><span class="p">,</span>
<span class="nx">width</span><span class="p">,</span>
<span class="nx">height</span><span class="p">,</span>
<span class="nx">canvas</span><span class="p">,</span>
<span class="na">useCORS</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">allowTaint</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="p">...</span><span class="nx">options</span>
<span class="p">};</span>
<span class="k">return</span> <span class="nx">html2canvas</span><span class="p">(</span><span class="nx">container</span><span class="p">,</span> <span class="nx">ops</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">canvas</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">imageEl</span> <span class="o">=</span> <span class="nx">Canvas2Image</span><span class="p">.</span><span class="nx">convertToPNG</span><span class="p">(</span><span class="nx">canvas</span><span class="p">,</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">width</span><span class="p">,</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">height</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">imageEl</span><span class="p">;</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<h4 id="4-关闭抗锯齿">4 关闭抗锯齿</h4>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/imageSmoothingEnabled">imageSmoothingEnabled</a> 是 <code class="language-plaintext highlighter-rouge">Canvas 2D API</code> 用来设置图片是否平滑的属性,<code class="language-plaintext highlighter-rouge">true</code>表示图片平滑(默认值),<code class="language-plaintext highlighter-rouge">false</code>表示关闭 canvas 抗锯齿。</p>
<p>默认情况下,canvas 的抗锯齿是开启的,可以通过关闭抗锯齿来实现一定程度上的图像锐化,提高线条边缘的清晰度。</p>
<p>据此,我们将以上<code class="language-plaintext highlighter-rouge">createBaseCanvas</code>方法升级为:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 创建用于绘制的基础canvas画布</span>
<span class="kd">function</span> <span class="nx">createBaseCanvas</span><span class="p">(</span><span class="nx">scale</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">canvas</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">"</span><span class="s2">canvas</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="nx">width</span> <span class="o">*</span> <span class="nx">scale</span><span class="p">;</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">height</span> <span class="o">=</span> <span class="nx">height</span> <span class="o">*</span> <span class="nx">scale</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">context</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">getContext</span><span class="p">(</span><span class="dl">"</span><span class="s2">2d</span><span class="dl">"</span><span class="p">);</span>
<span class="c1">// 关闭抗锯齿</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">mozImageSmoothingEnabled</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">webkitImageSmoothingEnabled</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">msImageSmoothingEnabled</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">imageSmoothingEnabled</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">scale</span><span class="p">(</span><span class="nx">scale</span><span class="p">,</span> <span class="nx">scale</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">canvas</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<h4 id="5-锐化特定元素">5 锐化特定元素</h4>
<p>受到 canvas 画布放缩的启发,我们对特定的 DOM 元素也可以采用类似的优化操作,即设置待优化元素宽高设置为 2 倍或<code class="language-plaintext highlighter-rouge">devicePixelRatio</code>倍,然后通过 css 缩放的方式控制其展示大小不变。</p>
<p><img src="https://segmentfault.com/img/remote/1460000021275788" alt="scale" /></p>
<p>例如,对于必须用背景图<code class="language-plaintext highlighter-rouge">background</code>的元素,采用以下方式可明显提高快照的清晰度:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">.</span><span class="nx">box</span> <span class="p">{</span>
<span class="nl">background</span><span class="p">:</span> <span class="nx">url</span><span class="p">(</span><span class="sr">/path/</span><span class="nx">to</span><span class="o">/</span><span class="nx">image</span><span class="p">)</span> <span class="nx">no</span><span class="o">-</span><span class="nx">repeat</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="mi">100</span><span class="nx">px</span><span class="p">;</span>
<span class="nl">height</span><span class="p">:</span> <span class="mi">100</span><span class="nx">px</span><span class="p">;</span>
<span class="nl">transform</span><span class="p">:</span> <span class="nx">scale</span><span class="p">(</span><span class="mf">0.5</span><span class="p">);</span>
<span class="nx">transform</span><span class="o">-</span><span class="nx">origin</span><span class="p">:</span> <span class="mi">0</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>其中,<code class="language-plaintext highlighter-rouge">width</code>和<code class="language-plaintext highlighter-rouge">height</code>为实际显示宽高的 2 倍值,通过<code class="language-plaintext highlighter-rouge">transform: scale(0.5)</code>实现了元素大小的缩放,<code class="language-plaintext highlighter-rouge">transform-origin</code>根据实际情况设置。</p>
<h3 id="转换效率">转换效率</h3>
<p>快照的转换效率直接关系到用户的等待时长。我们可以在目标节点传入阶段和快照导出两个阶段对其进行一定优化。</p>
<h4 id="1-传入阶段">1 传入阶段</h4>
<blockquote>
<p>传入节点的视图信息越精简,生成快照处理的计算量就越小</p>
</blockquote>
<p>以下方式适用于传入视图信息“瘦身”:</p>
<ul>
<li>减少 <a href="https://developers.google.com/web/tools/lighthouse/audits/dom-size">DOM 规模</a>,降低<code class="language-plaintext highlighter-rouge">html2canvas</code>递归遍历的计算量。</li>
<li>压缩图片素材本身的体积,使用 <a href="https://tinypng.com/">tinypng</a> 或 <a href="https://imageoptim.com/mac">ImageOptim</a> 等工具压缩素材。</li>
<li>如果使用了自定义字体,请使用 <a href="https://github.com/ecomfe/fontmin">fontmin</a> 工具对文字进行按需裁剪,避免动辄数兆的无效资源引入。</li>
<li>传入合适的<code class="language-plaintext highlighter-rouge">scale</code>值以缩放 canvas 画布(5.2.3节)。通常情况下 2~3 倍就已经满足一般的场景,不必要传入过大的放大倍数。</li>
<li>把上文提到的图片资源转 blob,可将图片资源本地化,避免了生成快照时 html2canvas 的二次图片加载处理,同时所生成的资源链接具备 URL 长度较短等优势。</li>
</ul>
<h4 id="2-导出优化">2 导出优化</h4>
<p><code class="language-plaintext highlighter-rouge">canvas2image</code>提供了多个 <a href="https://github.com/hongru/canvas2image">API</a> 用于导出图片信息,上文已有介绍。包括:</p>
<ul>
<li>convertToPNG</li>
<li>convertToJPEG</li>
<li>convertToGIF</li>
<li>convertToBMP</li>
</ul>
<p>不同的导出格式,对于生成快照的文件体积存在较大的影响。
通常对于没有透明度展示要求的图片素材,可以使用<code class="language-plaintext highlighter-rouge">jpeg</code>格式的导出。在我们的相关实践中,<code class="language-plaintext highlighter-rouge">jpeg</code>相比于<code class="language-plaintext highlighter-rouge">png</code>甚至能够节约 80% 以上的文件体积。</p>
<p>实际场景中的的图片导出格式,按业务需求选用即可。</p>
<h2 id="小结">小结</h2>
<p>本文基于<code class="language-plaintext highlighter-rouge">html2canvas</code>和<code class="language-plaintext highlighter-rouge">canvas2image</code>,从快照的内容完整性、清晰度和转换效率等多个方面,介绍了前端页面生成高质量快照的解决方案。</p>
<p>reffer:https://segmentfault.com/a/1190000021275782</p>Danniel背景css像素和物理像素2020-04-22T00:00:00+00:002020-04-22T00:00:00+00:00http://daner1990.github.io/plugins/2020/04/22/css%E5%83%8F%E7%B4%A0%E5%92%8C%E7%89%A9%E7%90%86%E5%83%8F%E7%B4%A0<h3 id="1pxcss-pixels">1、PX(CSS pixels)</h3>
<h4 id="11-定义">1.1 定义</h4>
<blockquote>
<p>虚拟像素,可以理解为“直觉”像素,<code class="language-plaintext highlighter-rouge">CSS</code>和<code class="language-plaintext highlighter-rouge">JS</code>使用的抽象单位,浏览器内的一切长度都是以<code class="language-plaintext highlighter-rouge">CSS</code>像素为单位的,<code class="language-plaintext highlighter-rouge">CSS</code>像素的单位是px。</p>
</blockquote>
<h4 id="12-注意">1.2 注意</h4>
<p>在<code class="language-plaintext highlighter-rouge">CSS</code>规范中,长度单位可以分为两类,绝对(<code class="language-plaintext highlighter-rouge">absolute</code>)单位以及相对(<code class="language-plaintext highlighter-rouge">relative</code>)单位。<code class="language-plaintext highlighter-rouge">px</code>是一个相对单位,相对的是设备像素(<code class="language-plaintext highlighter-rouge">device pixel</code>)。</p>
<p>在同样一个设备上,每1个CSS像素所代表的物理像素是可以变化的(即CSS像素的第一方面的相对性);</p>
<p>在不同的设备之间,每1个CSS像素所代表的物理像素是可以变化的(即CSS像素的第二方面的相对性);</p>
<h4 id="13-那么px到底是什么">1.3 那么PX到底是什么?</h4>
<p><code class="language-plaintext highlighter-rouge">px</code>实际是<code class="language-plaintext highlighter-rouge">pixel</code>(像素)的缩写,根据 <a href="https://zh.wikipedia.org/wiki/像素">维基百科</a>的解释,它是图像显示的基本单元,既不是一个确定的物理量,也不是一个点或者小方块,而是一个抽象概念。所以在谈论像素时一定要清楚它的上下文!一定要清楚它的上下文!一定要清楚它的上下文!</p>
<p>不同的设备,图像基本采样单元是不同的,显示器上的物理像素等于显示器的点距,而打印机的物理像素等于打印机的墨点。而衡量点距大小和打印机墨点大小的单位分别称为<code class="language-plaintext highlighter-rouge">ppi</code>和<code class="language-plaintext highlighter-rouge">dpi</code>:</p>
<p>ppi:每英寸多少像素数,放到显示器上说的是每英寸多少物理像素及显示器设备的点距。</p>
<p>dpi:每英寸多少点。</p>
<p>关于打印机的点距我们不去关心,只要知道 当用于描述显示器设备时ppi与dpi是同一个概念 。</p>
<h4 id="14-css像素的真正含义">1.4 CSS像素的真正含义</h4>
<p>由于不同的物理设备的物理像素的大小是不一样的,所以<code class="language-plaintext highlighter-rouge">css</code>认为浏览器应该对<code class="language-plaintext highlighter-rouge">css</code>中的像素进行调节,使得浏览器中 1css像素的大小在不同物理设备上看上去大小总是差不多 ,目的是为了保证阅读体验一致。为了达到这一点浏览器可以直接按照设备的物理像素大小进行换算,而<code class="language-plaintext highlighter-rouge">css</code>规范中使用<strong>“参考像素”</strong>来进行换算。</p>
<p>1参考像素即为从一臂之遥看解析度为<code class="language-plaintext highlighter-rouge">96DPI</code>的设备输出(即1英寸96点)时,1点(即1/96英寸)的视角。它并不是1/96英寸长度,而是从一臂之遥的距离处看解析度为<code class="language-plaintext highlighter-rouge">96DPI</code>的设备输出一单位(即1/96英寸)时视线与水平线的夹角。通常认为常人臂长为28英寸,所以它的视角是:
(1/96)in / (28in * 2 * PI / 360deg) = 0.0213度。</p>
<p>由于<code class="language-plaintext highlighter-rouge">css</code>像素是一个视角单位,所以在真正实现时,为了方便基本都是根据设备像素换算的。浏览器根据硬件设备能够直接获取<code class="language-plaintext highlighter-rouge">css</code>像素</p>
<h4 id="15-举个栗子来理解css像素的相对性">1.5 举个栗子来理解css像素的相对性</h4>
<p>作为Web开发者,我们接触的更多的是用于控制元素样式的样式单位像素。这里的像素我们称之为CSS像素。</p>
<p>CSS像素有什么特别的地方?我们可以借用<a href="http://www.quirksmode.org/mobile/viewports.html">quirksmode</a>中的这个例子:</p>
<p>假设我们用PC浏览器打开一个页面,浏览器此时的宽度为800px,页面上同时有一个400px宽的块级元素容器。很明显此时块状容器应该占页面的一半。</p>
<p>但如果我们把页面放大(通过“Ctrl键”加上“+号键”),放大为200%,也就是原来的两倍。此时块状容器则横向占满了整个浏览器。</p>
<p>吊诡的是此时我们既没有调整浏览器窗口大小,也没有改变块状元素的css宽度,但是它看上去却变大了一倍——这是因为我们把CSS像素放大为了原来的两倍。</p>
<p>CSS像素与屏幕像素1:1同样大小时:</p>
<p><a href="https://camo.githubusercontent.com/324d731ed557e2620c621b09c4f0ff038a76a305/687474703a2f2f696d676c66322e70682e3132362e6e65742f55387264324f756771453351546d6d496a65796d74413d3d2f363633303738383438393734363039313631382e676966"><img src="https://camo.githubusercontent.com/324d731ed557e2620c621b09c4f0ff038a76a305/687474703a2f2f696d676c66322e70682e3132362e6e65742f55387264324f756771453351546d6d496a65796d74413d3d2f363633303738383438393734363039313631382e676966" alt="img" /></a></p>
<p>CSS像素(黑色边框)开始被拉伸,此时1个CSS像素大于1个屏幕像素</p>
<p><a href="https://camo.githubusercontent.com/138f60302f16efccdb3bfd12350e37c4b5f6fbe8/687474703a2f2f696d676c66322e70682e3132362e6e65742f4f56554b334e77515863566b68337153767a774f50413d3d2f363633303438323832353531333239363936372e676966"><img src="https://camo.githubusercontent.com/138f60302f16efccdb3bfd12350e37c4b5f6fbe8/687474703a2f2f696d676c66322e70682e3132362e6e65742f4f56554b334e77515863566b68337153767a774f50413d3d2f363633303438323832353531333239363936372e676966" alt="img" /></a></p>
<p>也就是说默认情况下一个CSS像素应该是等于一个物理像素的宽度的,但是浏览器的放大操作让一个CSS像素等于了两个设备像素宽度。在后面你会看到更复杂的情况,在高PPI的设备上,CSS像素甚至在默认状态下就相当于多个物理像素的尺寸。</p>
<p>从上面的例子可以看出,CSS像素从来都只是一个相对值。</p>
<h3 id="2dpdevice-pixels">2、DP(device pixels)</h3>
<h4 id="21-定义">2.1 定义</h4>
<blockquote>
<p>设备像素(物理像素),顾名思义,显示屏是由一个个物理像素点组成的,通过控制每个像素点的颜色,使屏幕显示出不同的图像,屏幕从工厂出来那天起,它上面的物理像素点就固定不变了,单位pt。</p>
</blockquote>
<h4 id="22-注意">2.2 注意</h4>
<p><code class="language-plaintext highlighter-rouge">pt</code>在<code class="language-plaintext highlighter-rouge">css</code>单位中属于真正的绝对单位,<code class="language-plaintext highlighter-rouge">1pt = 1/72(inch)</code>,<code class="language-plaintext highlighter-rouge">inch</code>及英寸,而1英寸等于2.54厘米。</p>
<p>不同的设备,其图像基本单位是不同的,比如显示器的点距,可以认为是显示器的物理像素。现在的液晶显示器的点距一般在<code class="language-plaintext highlighter-rouge">0.25mm</code>到<code class="language-plaintext highlighter-rouge">0.29mm</code>之间。而打印机的墨点,也可以认为是打印机的物理像素,<code class="language-plaintext highlighter-rouge">300DPI</code>就是<code class="language-plaintext highlighter-rouge">0.085mm</code>,<code class="language-plaintext highlighter-rouge">600DPI</code>就是<code class="language-plaintext highlighter-rouge">0.042mm</code>。</p>
<p>注意,我们通常所说的显示器分辨率,其实是指桌面设定的分辨率,而不是显示器的物理分辨率。只不过现在液晶显示器成为主流,由于液晶的显示原理与<code class="language-plaintext highlighter-rouge">CRT</code>不同,只有在桌面分辨率与物理分辨率一致的情况下,显示效果最佳,所以现在我们的桌面分辨率几乎总是与显示器的物理分辨率一致了。</p>
<h4 id="23-小知识">2.3 小知识</h4>
<p>小知识:屏幕普遍采用RGB色域(红、绿、蓝三个子像素构成),而印刷行业普遍使用CMYK色域(青、品红、黄和黑)</p>
<p><a href="https://camo.githubusercontent.com/b082c42d108453b01d400b6ba365c2cbf9cb2ddc/687474703a2f2f6f70656f6b6634756b2e626b742e636c6f7564646e2e636f6d2f312e6a706567"><img src="https://camo.githubusercontent.com/b082c42d108453b01d400b6ba365c2cbf9cb2ddc/687474703a2f2f6f70656f6b6634756b2e626b742e636c6f7564646e2e636f6d2f312e6a706567" alt="img" /></a></p>
<h4 id="24-设备像素dp与css像素之间的关系">2.4 设备像素(DP)与CSS像素之间的关系</h4>
<p>获得设备像素比(dpr)后,便可得知设备像素与CSS像素之间的比例。当这个比率为1:1时,使用1个设备像素显示1个CSS像素。当这个比率为2:1时,使用4个设备像素显示1个CSS像素,当这个比率为3:1时,使用9(3*3)个设备像素显示1个CSS像素。
所以,有如下公式:</p>
<blockquote>
<p>DPR = 设备像素/CSS像素</p>
</blockquote>
<h3 id="3dipdevice-independent-pixel">3、DIP(Device independent Pixel)</h3>
<p>设备独立像素,也称为逻辑像素,简称<code class="language-plaintext highlighter-rouge">dip</code>。
根据上述设备像素与<code class="language-plaintext highlighter-rouge">CSS</code>像素之间的关系、及<code class="language-plaintext highlighter-rouge">DPR</code>的官方定义,我们可以推断出:</p>
<blockquote>
<p>CSS像素 =设备独立像素 = 逻辑像素</p>
</blockquote>
<p>下面,还是引用 http://www.cnblogs.com/2050/p/3877280.html 文中的内容说明:</p>
<p>在移动端浏览器中以及某些桌面浏览器中,window对象有一个<code class="language-plaintext highlighter-rouge">devicePixelRatio</code>属性,它的官方的定义为:设备物理像素和设备独立像素的比例,也就是 devicePixelRatio = 物理像素 / 独立像素。
<code class="language-plaintext highlighter-rouge">CSS</code>像素就可以看做是设备的独立像素,所以通过<code class="language-plaintext highlighter-rouge">devicePixelRatio</code>,我们可以知道该设备上一个<code class="language-plaintext highlighter-rouge">css</code>像素代表多少个物理像素。例如,在<code class="language-plaintext highlighter-rouge">Retina</code>屏的<code class="language-plaintext highlighter-rouge">iphone</code>上,<code class="language-plaintext highlighter-rouge">devicePixelRatio</code>的值为2,也就是说1个<code class="language-plaintext highlighter-rouge">css</code>像素相当于2个物理像素。但是要注意的是,<code class="language-plaintext highlighter-rouge">devicePixelRato</code>在不同的浏览器中还存在些许的兼容性问题,所以我们现在还并不能完全信赖这个东西,具体的情况可以看下<a href="https://www.quirksmode.org/blog/archives/2012/06/devicepixelrati.html">这篇文章</a>。</p>
<p>为什么是“每四个一组”?而且要让这四个一组来显示“原来屏幕的一个像素”?这大概就是 Retina 显示技术的一种表现吧。而这“每四个一组”的“大像素”,可以被称作“设备独立像素”,<code class="language-plaintext highlighter-rouge">device independent pixel</code> ,或者 <code class="language-plaintext highlighter-rouge">density-independentpixel</code> ,它可以是系统中的一个点,这个点代表一个可以由程序使用的虚拟像素,然后由相关系统转换为物理像素。</p>
<p>“设备独立像素”也有人称为“CSS像素”,一种形象的说法,更倾向于表明与 <code class="language-plaintext highlighter-rouge">CSS</code> 中尺寸的对应。</p>
<p>设备独立像素与物理像素的对应关系,可以这样看:</p>
<p><a href="https://camo.githubusercontent.com/688d63e8c7609b6f85ada2ef7e5f1df5d4d477cc/68747470733a2f2f7773332e73696e61696d672e636e2f6c617267652f303036744e6337396779316667396474786e396c7a6a333064773035726161722e6a7067"><img src="https://camo.githubusercontent.com/688d63e8c7609b6f85ada2ef7e5f1df5d4d477cc/68747470733a2f2f7773332e73696e61696d672e636e2f6c617267652f303036744e6337396779316667396474786e396c7a6a333064773035726161722e6a7067" alt="img" /></a></p>
<p>类似的每四个一组的对应关系,也许正是 <code class="language-plaintext highlighter-rouge">Retina</code> 显示技术所做的。</p>
<h3 id="4dprdevice-pixels-ratio">4、DPR(device pixels ratio)</h3>
<h4 id="41-定义">4.1 定义</h4>
<blockquote>
<p>设备像素比(dpr 描述的是未缩放状态下,<code class="language-plaintext highlighter-rouge">物理像素</code>和<code class="language-plaintext highlighter-rouge">CSS像素</code>的初始比例关系,计算方法如下图。</p>
</blockquote>
<p><a href="https://camo.githubusercontent.com/80949f0ed64dbd3b87808ff1855614a1a2afcb2f/687474703a2f2f3778766831722e636f6d312e7a302e676c622e636c6f7564646e2e636f6d2f6470722e706e67"><img src="https://camo.githubusercontent.com/80949f0ed64dbd3b87808ff1855614a1a2afcb2f/687474703a2f2f3778766831722e636f6d312e7a302e676c622e636c6f7564646e2e636f6d2f6470722e706e67" alt="img" /></a></p>
<h4 id="42-理解">4.2 理解</h4>
<p>设备像素比(dpr) 是指在移动开发中1个css像素占用多少设备像素,如2代表1个css像素用2x2个设备像素来绘制。</p>
<p>设备像素比(dpr),公式为<code class="language-plaintext highlighter-rouge">1px = (dpr)^2 * 1dp</code>,可以理解为1px由多少个设备像素组成;</p>
<p><a href="https://camo.githubusercontent.com/2640b32c14e5d3a3accdedd35cac7c62a01b1786/68747470733a2f2f6c697a686979616f2e6769746875622e696f2f323031372f30342f30312f76696577706f72742f646576696365253230706978656c253230616e64253230637373253230706978656c2e706e67"><img src="https://camo.githubusercontent.com/2640b32c14e5d3a3accdedd35cac7c62a01b1786/68747470733a2f2f6c697a686979616f2e6769746875622e696f2f323031372f30342f30312f76696577706f72742f646576696365253230706978656c253230616e64253230637373253230706978656c2e706e67" alt="img" /></a></p>
<h3 id="5ppipixels-per-inch">5、PPI(pixels per inch)</h3>
<h4 id="51-定义">5.1 定义</h4>
<blockquote>
<p>每英寸像素取值,更确切的说法应该是像素密度,也就是衡量单位物理面积内拥有像素值的情况。</p>
</blockquote>
<h4 id="52-ppi是如何计算出来的呢">5.2 ppi是如何计算出来的呢?</h4>
<p>顾名思义,每英寸的像素点(设备像素),已知屏幕分辨率和主对角线的尺寸,则<code class="language-plaintext highlighter-rouge">ppi</code>等于
以爱疯6为例:
<a href="https://camo.githubusercontent.com/87b9207e9f1bc52b8a2d174f408a21d690d37814/687474703a2f2f75706c6f61642d696d616765732e6a69616e7368752e696f2f75706c6f61645f696d616765732f313939333433352d623766636565316134316239393766332e706e673f696d6167654d6f6772322f6175746f2d6f7269656e742f7374726970253743696d61676556696577322f322f772f31323430"><img src="https://camo.githubusercontent.com/87b9207e9f1bc52b8a2d174f408a21d690d37814/687474703a2f2f75706c6f61642d696d616765732e6a69616e7368752e696f2f75706c6f61645f696d616765732f313939333433352d623766636565316134316239393766332e706e673f696d6167654d6f6772322f6175746f2d6f7269656e742f7374726970253743696d61676556696577322f322f772f31323430" alt="img" /></a></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>var 斜边尺寸 = V(1920^2+1080^2) V代表开根号
var ppi = 斜边尺寸/5.5
ppi = 401ppi
</code></pre></div></div>
<p><a href="https://camo.githubusercontent.com/9233a97781fccdd468aba79b8944619cc9918b63/68747470733a2f2f7773312e73696e61696d672e636e2f6c617267652f303036744e6252776779316667386a7030653035316a3330737730376f6a73612e6a7067"><img src="https://camo.githubusercontent.com/9233a97781fccdd468aba79b8944619cc9918b63/68747470733a2f2f7773312e73696e61696d672e636e2f6c617267652f303036744e6252776779316667386a7030653035316a3330737730376f6a73612e6a7067" alt="img" /></a></p>
<p>我们知道,ppi越高,每英寸像素点越多,图像越清晰;我们可以类比物体的密度,密度越大,单位体积的质量就越大,ppi越高,单位面积的像素越多。</p>
<h4 id="53-ppi和dpr到底什么关系">5.3 ppi和dpr到底什么关系?</h4>
<p>毕竟这些参数是外国人先发明的,他们会优先选择自己熟悉的计量单位作为显示设备的工厂标准参数,因此<code class="language-plaintext highlighter-rouge">ppi</code>就用作显示设备的工业标准;</p>
<p>告诉业界人士,<code class="language-plaintext highlighter-rouge">ppi</code>达到多少是高清屏,此时对应的dpr是多少,而不直接告诉你我现在的显示设备<code class="language-plaintext highlighter-rouge">dpr</code>是多少,毕竟人们直接听到像素分辨率会更加有反应。</p>
<p>设备像素比与ppi相关,一般是ppi/160的整数倍:</p>
<p><a href="https://camo.githubusercontent.com/22e06cf4ddfd77e6a9744f2de75f080c75332aa9/68747470733a2f2f7773332e73696e61696d672e636e2f6c617267652f303036744e6252776779316667386a6579366c66666a333068753034636d79342e6a7067"><img src="https://camo.githubusercontent.com/22e06cf4ddfd77e6a9744f2de75f080c75332aa9/68747470733a2f2f7773332e73696e61696d672e636e2f6c617267652f303036744e6252776779316667386a6579366c66666a333068753034636d79342e6a7067" alt="img" /></a></p>
<h3 id="6倍率与逻辑像素">6、倍率与逻辑像素</h3>
<h4 id="61-基本关系">6.1 基本关系</h4>
<p><a href="https://camo.githubusercontent.com/3f81180acbdf2f99e0766a74e52d1ab61f45b6d3/68747470733a2f2f7773312e73696e61696d672e636e2f6c617267652f303036744e625277677931666739396d30743468396a33306d3830676f6a72372e6a7067"><img src="https://camo.githubusercontent.com/3f81180acbdf2f99e0766a74e52d1ab61f45b6d3/68747470733a2f2f7773312e73696e61696d672e636e2f6c617267652f303036744e625277677931666739396d30743468396a33306d3830676f6a72372e6a7067" alt="img" /></a>
用iPhone 3gs和4s来举例。假设有个邮件列表界面,我们不妨按照PC端网页设计的思维来想象。3gs上大概只能显示4-5行,4s就能显示9-10行,而且每行会变得特别宽。但两款手机其实是一样大的。如果照这种方式显示,3gs上刚刚好的效果,在4s上就会小到根本看不清字。
<a href="https://camo.githubusercontent.com/87bf86edeb85f89a51559674453303a22a3faa81/68747470733a2f2f7773342e73696e61696d672e636e2f6c617267652f303036744e625277677931666739396d71797a63656a33306d383062346a72732e6a7067"><img src="https://camo.githubusercontent.com/87bf86edeb85f89a51559674453303a22a3faa81/68747470733a2f2f7773342e73696e61696d672e636e2f6c617267652f303036744e625277677931666739396d71797a63656a33306d383062346a72732e6a7067" alt="img" /></a>
在现实中,这两者效果却是一样的。这是因为Retina屏幕把2x2个像素当1个像素使用。比如原本44像素高的顶部导航栏,在<code class="language-plaintext highlighter-rouge">Retina</code>屏上用了88个像素的高度来显示。导致界面元素都变成2倍大小,反而和3gs效果一样了。画质却更清晰。</p>
<p>在以前,iOS应用的资源图片中,同一张图通常有两个尺寸。你会看到文件名有的带<a href="https://github.com/2x">@2x</a>字样,有的不带。其中不带<a href="https://github.com/2x">@2x</a>的用在普通屏上,带<a href="https://github.com/2x">@2x</a>的用在<code class="language-plaintext highlighter-rouge">Retina</code>屏上。只要图片准备好,iOS会自己判断用哪张,<code class="language-plaintext highlighter-rouge">Android</code>道理也一样。</p>
<p>由此可以看出,苹果以普通屏为基准,给<code class="language-plaintext highlighter-rouge">Retin</code>a屏定义了一个2倍的倍率(<code class="language-plaintext highlighter-rouge">iPhone 6plus</code>除外,它达到了3倍)。实际像素除以倍率,就得到逻辑像素尺寸。只要两个屏幕逻辑像素相同,它们的显示效果就是相同的。</p>
<h4 id="62-retina显示屏">6.2 Retina显示屏</h4>
<p>这是一种显示技术,可以将把更多的像素点压缩至一块屏幕里,从而达到更高的分辨率并提高屏幕显示的细腻程度,这种分辨率在正常观看距离下足以使人肉眼无法分辨其中的单独像素。</p>
<p>最先使用<code class="language-plaintext highlighter-rouge">retina</code>屏幕是iphone 4,屏幕分辨率为960 * 640(326ppi)。</p>
<p>对比如下两幅图,可以清晰地看出是否 <code class="language-plaintext highlighter-rouge">Retina</code> 屏的显示差异:
<a href="https://camo.githubusercontent.com/40423c8a9ce46968a5ee47756070a698bcc9d044/68747470733a2f2f7773332e73696e61696d672e636e2f6c617267652f303036744e633739677931666739646e75656b61716a3330626f303773337a342e6a7067"><img src="https://camo.githubusercontent.com/40423c8a9ce46968a5ee47756070a698bcc9d044/68747470733a2f2f7773332e73696e61696d672e636e2f6c617267652f303036744e633739677931666739646e75656b61716a3330626f303773337a342e6a7067" alt="img" /></a>
图2 iPhone 3GS</p>
<p><a href="https://camo.githubusercontent.com/5fb06018a5122b75eef08f43fd6228ca5e6debbd/68747470733a2f2f7773312e73696e61696d672e636e2f6c617267652f303036744e633739677931666739646f6230646e306a3330626f3037737439692e6a7067"><img src="https://camo.githubusercontent.com/5fb06018a5122b75eef08f43fd6228ca5e6debbd/68747470733a2f2f7773312e73696e61696d672e636e2f6c617267652f303036744e633739677931666739646f6230646e306a3330626f3037737439692e6a7067" alt="img" /></a>
图3 iPhone 4</p>
<p>两代iPhone 的物理尺寸(屏幕宽高有多少英寸)是一样的,从上图可以看出,iphone 4的显示效果要明显好于iphone 3GS,虽然 iPhone 4 分辨率提高了,但它不同于普通的电脑显示器那样为了显示更多的内容,而是提升显示相同内容时的画面精细程度。这种提升方式是靠提升单位面积屏幕的像素数量,即像素密度来提升分辨率,这样做的主要目的是为了提高屏幕显示画面的精细程度。以第三代 <code class="language-plaintext highlighter-rouge">MacBook Pro with Retina Display</code>为例, 工作时显卡渲染出的2880x1880个像素每四个一组,输出原来屏幕的一个像素显示的大小区域内的图像。这样一来,用户所看到的图标与文字的大小与原来的1440x900分辨率显示屏相同,但精细度是原来的4倍。</p>
<p>注意:在桌面显示器中,我们调整了显示分辨率,比如从 800 * 600 调整到 1024 * 768 时,屏幕的文字图标会变小,显示的内容更多了。但 <code class="language-plaintext highlighter-rouge">Retina</code> 显示方式不会产生这样的问题,或者说, Retina 显示技术解决的是显示画面精细程度的问题,而不是解决显示内容容量的问题。</p>
<h3 id="7分辨率像素和屏幕尺寸">7、分辨率、像素和屏幕尺寸</h3>
<p><code class="language-plaintext highlighter-rouge">PPI</code> 说的是像素密度,而分辨率说的是块屏幕的像素尺寸,譬如说 1334*750 就是 iPhone(6~7)的分辨率,说 iPhone(6~7)的分辨率是 326 是错误的表述,326 是它的像素密度,单位是 <code class="language-plaintext highlighter-rouge">PPI</code>。</p>
<p>询问别人一粒像素有多大是一个非常鸡贼的问题(小心面试遇到这样的题),虽然我们说像素是构成屏幕的发光的点,是物理的,但是像素在脱离了屏幕尺寸之后是没有大小可言的,你可以将 1920 * 1080 颗像素放到一台 40 寸的小米电视机里面,也可以将同样多的像素全部塞到一台 5.5 寸的 iPhone7 Plus 手机里面去,那么对于 40 寸的电视而言,每个像素颗粒当然会大于 5.5 寸的手机的像素。</p>
<p><a href="https://camo.githubusercontent.com/c34db47bd8a3cc27bb8c665cb615d7ecaeadbcde/68747470733a2f2f7773332e73696e61696d672e636e2f6c617267652f303036744e633739677931666739666e64653861646a3330676f30616b6a726c2e6a7067"><img src="https://camo.githubusercontent.com/c34db47bd8a3cc27bb8c665cb615d7ecaeadbcde/68747470733a2f2f7773332e73696e61696d672e636e2f6c617267652f303036744e633739677931666739666e64653861646a3330676f30616b6a726c2e6a7067" alt="img" /></a></p>
<p>所以光看屏幕的分辨率对于设计师来说是不具备多少实际意义的,通过分辨率计算得出的像素密度(PPI)才是设计师要关心的问题,我们通过屏幕分辨率和屏幕尺寸就能计算出屏幕的像素密度的。</p>
<p>再次使用 iPhone(6~7)作为例子。我们知道该屏幕的横向物理尺寸为 2.3 英寸 ,且横向具有 750 颗像素,根据下面的公式,我们能够算出 iPhone(6~7)的屏幕是 326 PPI,意为每寸存在 326 颗像素。</p>
<p>其实不论我们怎么除,计算得出来的<code class="language-plaintext highlighter-rouge">像素密度(PPI)</code>都会是这个数,宽存在像素除以宽物理长度,高存在像素除以高物理长度,得数都接近于 326。</p>
<h3 id="8viewport">8、Viewport</h3>
<h4 id="81-ppk的关于三个viewport的理论">8.1 PPK的关于三个viewport的理论</h4>
<p><a href="http://www.quirksmode.org/">ppk大神</a>对于移动设备上的viewport有着非常多的研究(<a href="http://www.quirksmode.org/mobile/viewports.html">第一篇</a>,<a href="http://www.quirksmode.org/mobile/viewports2.html">第二篇</a>,<a href="http://www.quirksmode.org/mobile/metaviewport/">第三篇</a>),有兴趣的同学可以去看一下,本文中有很多数据和观点也是出自那里。ppk认为,移动设备上有三个viewport。</p>
<p>首先,移动设备上的浏览器认为自己必须能让所有的网站都正常显示,即使是那些不是为移动设备设计的网站。但如果以浏览器的可视区域作为<code class="language-plaintext highlighter-rouge">viewport</code>的话,因为移动设备的屏幕都不是很宽,所以那些为桌面浏览器设计的网站放到移动设备上显示时,必然会因为移动设备的<code class="language-plaintext highlighter-rouge">viewport</code>太窄,而挤作一团,甚至布局什么的都会乱掉。也许有人会问,现在不是有很多手机分辨率都非常大吗,比如768x1024,或者1080x1920这样,那这样的手机用来显示为桌面浏览器设计的网站是没问题的吧?前面我们已经说了,<code class="language-plaintext highlighter-rouge">css</code>中的1px并不是代表屏幕上的1px,你分辨率越大,<code class="language-plaintext highlighter-rouge">css</code>中1px代表的物理像素就越多,<code class="language-plaintext highlighter-rouge">devicePixelRatio</code>的值也越大,这很好理解,因为你分辨率增大了,但屏幕尺寸并没有变大多少,必须让<code class="language-plaintext highlighter-rouge">css</code>中的1px代表更多的物理像素,才能让1px的东西在屏幕上的大小与那些低分辨率的设备差不多,不然就会因为太小而看不清。所以在1080x1920这样的设备上,在默认情况下,也许你只要把一个div的宽度设为300多px(视<code class="language-plaintext highlighter-rouge">devicePixelRatio</code>的值而定),就是满屏的宽度了。回到正题上来,如果把移动设备上浏览器的可视区域设为viewport的话,某些网站就会因为<code class="language-plaintext highlighter-rouge">viewport</code>太窄而显示错乱,所以这些浏览器就决定默认情况下把<code class="language-plaintext highlighter-rouge">viewport</code>设为一个较宽的值,比如980px,这样的话即使是那些为桌面设计的网站也能在移动浏览器上正常显示了。ppk把这个浏览器默认的<code class="language-plaintext highlighter-rouge">viewport</code>叫做 layout viewport。</p>
<p>这个<code class="language-plaintext highlighter-rouge">layout viewport</code>的宽度可以通过<code class="language-plaintext highlighter-rouge">document.documentElement.clientWidth</code> 来获取。</p>
<p>然而,layout viewport 的宽度是大于浏览器可视区域的宽度的,所以我们还需要一个<code class="language-plaintext highlighter-rouge">viewport</code>来代表 浏览器可视区域的大小,ppk把这个<code class="language-plaintext highlighter-rouge">viewport</code>叫做 visual viewport 。<code class="language-plaintext highlighter-rouge">visual viewport</code>的宽度可以通过window.innerWidth 来获取,但在Android 2, Oprea mini 和 UC 8中无法正确获取。</p>
<p><a href="https://camo.githubusercontent.com/ab5ed1c0a848d65856d748771eab199965e7a6d9/68747470733a2f2f7773312e73696e61696d672e636e2f6c617267652f303036744e6337396779316667396c6162703165626a3330737130616a616d6d2e6a7067"><img src="https://camo.githubusercontent.com/ab5ed1c0a848d65856d748771eab199965e7a6d9/68747470733a2f2f7773312e73696e61696d672e636e2f6c617267652f303036744e6337396779316667396c6162703165626a3330737130616a616d6d2e6a7067" alt="img" /></a></p>
<p>现在我们已经有两个<code class="language-plaintext highlighter-rouge">viewport</code>了:layout viewport 和 visual viewport。但浏览器觉得还不够,因为现在越来越多的网站都会为移动设备进行单独的设计,所以必须还要有一个能完美适配移动设备的<code class="language-plaintext highlighter-rouge">viewport</code>。所谓的完美适配指的是,首先不需要用户缩放和横向滚动条就能正常的查看网站的所有内容;第二,显示的文字的大小是合适,比如一段14px大小的文字,不会因为在一个高密度像素的屏幕里显示得太小而无法看清,理想的情况是这段14px的文字无论是在何种密度屏幕,何种分辨率下,显示出来的大小都是差不多的。当然,不只是文字,其他元素像图片什么的也是这个道理。ppk把这个<code class="language-plaintext highlighter-rouge">viewport</code>叫做 ideal viewport,也就是第三个<code class="language-plaintext highlighter-rouge">viewport</code>——移动设备的理想<code class="language-plaintext highlighter-rouge">viewport</code>。</p>
<p><code class="language-plaintext highlighter-rouge">ideal viewport</code>并没有一个固定的尺寸,不同的设备拥有有不同的<code class="language-plaintext highlighter-rouge">ideal viewport</code>。所有的<code class="language-plaintext highlighter-rouge">iphone</code>的<code class="language-plaintext highlighter-rouge">ideal viewport</code>宽度都是320px,无论它的屏幕宽度是320还是640,也就是说,在<code class="language-plaintext highlighter-rouge">iphone</code>中,<code class="language-plaintext highlighter-rouge">css</code>中的320px就代表<code class="language-plaintext highlighter-rouge">iphone</code>屏幕的宽度。</p>
<p><a href="https://camo.githubusercontent.com/6eb081f936dcfa59e7d38dceffe8a20d8795ed25/68747470733a2f2f7773332e73696e61696d672e636e2f6c617267652f303036744e6337396779316667396c3932367676306a33306662303573337a662e6a7067"><img src="https://camo.githubusercontent.com/6eb081f936dcfa59e7d38dceffe8a20d8795ed25/68747470733a2f2f7773332e73696e61696d672e636e2f6c617267652f303036744e6337396779316667396c3932367676306a33306662303573337a662e6a7067" alt="img" /></a></p>
<p>但是安卓设备就比较复杂了,有320px的,有360px的,有384px的等等,关于不同的设备<code class="language-plaintext highlighter-rouge">ideal viewport</code>的宽度都为多少,可以到<a href="http://viewportsizes.com/">http://viewportsizes.com</a>去查看一下,里面收集了众多设备的理想宽度。</p>
<p>再总结一下:ppk把移动设备上的<code class="language-plaintext highlighter-rouge">viewport</code>分为 layout viewport 、 visual viewport 和 ideal viewport 三类,其中的<code class="language-plaintext highlighter-rouge">ideal viewport</code>是最适合移动设备的<code class="language-plaintext highlighter-rouge">viewport</code>,<code class="language-plaintext highlighter-rouge">ideal viewport</code>的宽度等于移动设备的屏幕宽度,只要在css中把某一元素的宽度设为<code class="language-plaintext highlighter-rouge">ideal viewport</code>的宽度(单位用px),那么这个元素的宽度就是设备屏幕的宽度了,也就是宽度为100%的效果。<code class="language-plaintext highlighter-rouge">ideal viewport</code> 的意义在于,无论在何种分辨率的屏幕下,那些针对<code class="language-plaintext highlighter-rouge">ideal viewport</code>而设计的网站,不需要用户手动缩放,也不需要出现横向滚动条,都可以完美的呈现给用户。</p>
<h4 id="82-利用meta标签对viewport进行控制">8.2 利用meta标签对viewport进行控制</h4>
<p>移动设备默认的<code class="language-plaintext highlighter-rouge">viewport</code>是<code class="language-plaintext highlighter-rouge">layout viewport</code>,也就是那个比屏幕要宽的<code class="language-plaintext highlighter-rouge">viewport</code>,但在进行移动设备网站的开发时,我们需要的是<code class="language-plaintext highlighter-rouge">ideal viewport</code>。那么怎么才能得到<code class="language-plaintext highlighter-rouge">ideal viewport</code>呢?这就该轮到<code class="language-plaintext highlighter-rouge">meta</code>标签出场了。</p>
<p>我们在开发移动设备的网站时,最常见的的一个动作就是把下面这个东西复制到我们的<code class="language-plaintext highlighter-rouge">head</code>标签中:</p>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<p>该<code class="language-plaintext highlighter-rouge">meta</code>标签的作用是让当前<code class="language-plaintext highlighter-rouge">viewport</code>的宽度等于设备的宽度,同时不允许用户手动缩放。也许允不允许用户缩放不同的网站有不同的要求,但让<code class="language-plaintext highlighter-rouge">viewport</code>的宽度等于设备的宽度,这个应该是大家都想要的效果,如果你不这样的设定的话,那就会使用那个比屏幕宽的默认<code class="language-plaintext highlighter-rouge">viewport</code>,也就是说会出现横向滚动条。</p>
<p>这个<code class="language-plaintext highlighter-rouge">nam</code>e为<code class="language-plaintext highlighter-rouge">viewport</code>的<code class="language-plaintext highlighter-rouge">meta</code>标签到底有哪些东西呢,又都有什么作用呢?</p>
<p><code class="language-plaintext highlighter-rouge">meta viewport</code> 标签首先是由苹果公司在其<code class="language-plaintext highlighter-rouge">safari</code>浏览器中引入的,目的就是解决移动设备的<code class="language-plaintext highlighter-rouge">viewport</code>问题。后来安卓以及各大浏览器厂商也都纷纷效仿,引入对<code class="language-plaintext highlighter-rouge">meta viewport</code>的支持,事实也证明这个东西还是非常有用的。</p>
<p>在苹果的规范中,<code class="language-plaintext highlighter-rouge">meta viewport</code> 有6个属性(暂且把<code class="language-plaintext highlighter-rouge">content</code>中的那些东西称为一个个属性和值),如下:</p>
<p><a href="https://camo.githubusercontent.com/8a7d55d30c4c919d2d9d07c5cc153f23bf5e9339/68747470733a2f2f7773322e73696e61696d672e636e2f6c617267652f303036744e6337396779316667396b78347a63766c6a33306e3030346b3075642e6a7067"><img src="https://camo.githubusercontent.com/8a7d55d30c4c919d2d9d07c5cc153f23bf5e9339/68747470733a2f2f7773322e73696e61696d672e636e2f6c617267652f303036744e6337396779316667396b78347a63766c6a33306e3030346b3075642e6a7067" alt="img" /></a></p>
<p>这些属性可以同时使用,也可以单独使用或混合使用,多个属性同时使用时用逗号隔开就行了。</p>
<p>此外,在安卓中还支持 <code class="language-plaintext highlighter-rouge">target-densitydpi</code> 这个私有属性,它表示目标设备的密度等级,作用是决定<code class="language-plaintext highlighter-rouge">css</code>中的1px代表多少物理像素</p>
<p><a href="https://camo.githubusercontent.com/f814b5484077b3fe5a41177a3814f99387e79dff/68747470733a2f2f7773312e73696e61696d672e636e2f6c617267652f303036744e6337396779316667396d6675327175636a33306e363031307438762e6a7067"><img src="https://camo.githubusercontent.com/f814b5484077b3fe5a41177a3814f99387e79dff/68747470733a2f2f7773312e73696e61696d672e636e2f6c617267652f303036744e6337396779316667396d6675327175636a33306e363031307438762e6a7067" alt="img" /></a></p>
<p>特别说明的是,当 <code class="language-plaintext highlighter-rouge">target-densitydpi=device-dpi</code> 时, css中的1px会等于物理像素中的1px。</p>
<p>因为这个属性只有安卓支持,并且安卓已经决定要废弃<code class="language-plaintext highlighter-rouge">~~target-densitydpi~~</code> 这个属性了,所以这个属性我们要避免进行使用 。</p>
<h4 id="83-把当前的viewport宽度设置为-ideal-viewport-的宽度">8.3 把当前的viewport宽度设置为 ideal viewport 的宽度</h4>
<p>要得到<code class="language-plaintext highlighter-rouge">ideal viewport</code>就必须把默认的<code class="language-plaintext highlighter-rouge">layout viewport</code>的宽度设为移动设备的屏幕宽度。因为<code class="language-plaintext highlighter-rouge">meta viewport</code>中的<code class="language-plaintext highlighter-rouge">width</code>能控制<code class="language-plaintext highlighter-rouge">layout viewport</code>的宽度,所以我们只需要把<code class="language-plaintext highlighter-rouge">width</code>设为<code class="language-plaintext highlighter-rouge">width-device</code>这个特殊的值就行了。</p>
<meta name="viewport" content="width=device-width" />
<p>下图是这句代码在各大移动端浏览器上的测试结果:</p>
<p><a href="https://camo.githubusercontent.com/a029313400fd25bb1cd8a49074b05e9f6b6717e6/68747470733a2f2f7773312e73696e61696d672e636e2f6c617267652f303036744e6337396779316667396b7a39316b6b6c6a33306f7030346433796b2e6a7067"><img src="https://camo.githubusercontent.com/a029313400fd25bb1cd8a49074b05e9f6b6717e6/68747470733a2f2f7773312e73696e61696d672e636e2f6c617267652f303036744e6337396779316667396b7a39316b6b6c6a33306f7030346433796b2e6a7067" alt="img" /></a></p>
<p>可以看到通过<code class="language-plaintext highlighter-rouge">width=device-width</code>,所有浏览器都能把当前的<code class="language-plaintext highlighter-rouge">viewport</code>宽度变成<code class="language-plaintext highlighter-rouge">ideal viewport</code>的宽度,但要注意的是,在iphone和ipad上,无论是竖屏还是横屏,宽度都是竖屏时<code class="language-plaintext highlighter-rouge">ideal viewport</code>的宽度。</p>
<p>这样的写法看起来谁都会做,没吃过猪肉,谁还没见过猪跑啊~,确实,我们在开发移动设备上的网页时,不管你明不明白什么是<code class="language-plaintext highlighter-rouge">viewport</code>,可能你只需要这么一句代码就够了。</p>
<p>可是你肯定不知道</p>
<meta name="viewport" content="initial-scale=1" />
<p>这句代码也能达到和前一句代码一样的效果,也可以把当前的的<code class="language-plaintext highlighter-rouge">viewport</code>变为 <code class="language-plaintext highlighter-rouge">ideal viewport</code>。</p>
<p>呵呵,傻眼了吧,因为从理论上来讲,这句代码的作用只是不对当前的页面进行缩放,也就是页面本该是多大就是多大。那为什么会有 <code class="language-plaintext highlighter-rouge">width=device-width</code> 的效果呢?</p>
<p>要想清楚这件事情,首先你得弄明白这个缩放是相对于什么来缩放的,因为这里的缩放值是1,也就是没缩放,但却达到了 ideal viewport 的效果,所以,那答案就只有一个了,缩放是相对于 <code class="language-plaintext highlighter-rouge">ideal viewport</code>来进行缩放的,当对<code class="language-plaintext highlighter-rouge">ideal viewport</code>进行100%的缩放,也就是缩放值为1的时候,不就得到了 <code class="language-plaintext highlighter-rouge">ideal viewport</code> 吗?事实证明,的确是这样的。下图是各大移动端的浏览器当设置了``后是否能把当前的<code class="language-plaintext highlighter-rouge">viewport</code> 宽度变成 <code class="language-plaintext highlighter-rouge">ideal viewport</code> 的宽度的测试结果。</p>
<p><a href="https://camo.githubusercontent.com/bce71263935e8075e49aee916531fe180a05dd62/68747470733a2f2f7773332e73696e61696d672e636e2f6c617267652f303036744e6337396779316667396c306a356c34796a33306f6b30346571327a2e6a7067"><img src="https://camo.githubusercontent.com/bce71263935e8075e49aee916531fe180a05dd62/68747470733a2f2f7773332e73696e61696d672e636e2f6c617267652f303036744e6337396779316667396c306a356c34796a33306f6b30346571327a2e6a7067" alt="img" /></a></p>
<p>测试结果表明 <code class="language-plaintext highlighter-rouge">initial-scale=1</code> 也能把当前的 <code class="language-plaintext highlighter-rouge">viewport</code> 宽度变成 <code class="language-plaintext highlighter-rouge">ideal viewport</code> 的宽度,但这次轮到了windows phone 上的IE 无论是竖屏还是横屏都把宽度设为竖屏时 <code class="language-plaintext highlighter-rouge">ideal viewport</code> 的宽度。但这点小瑕疵已经无关紧要了。</p>
<p>但如果 <code class="language-plaintext highlighter-rouge">width</code> 和 <code class="language-plaintext highlighter-rouge">initial-scale=1</code> 同时出现,并且还出现了冲突呢?比如:</p>
<meta name="viewport" content="width=400, initial-scale=1" />
<p><code class="language-plaintext highlighter-rouge">width=400</code> 表示把当前 <code class="language-plaintext highlighter-rouge">viewport</code> 的宽度设为<code class="language-plaintext highlighter-rouge">400px</code>,<code class="language-plaintext highlighter-rouge">initial-scale=1</code> 则表示把当前 <code class="language-plaintext highlighter-rouge">viewport</code> 的宽度设为ideal viewport的宽度,那么浏览器到底该服从哪个命令呢?是书写顺序在后面的那个吗?不是。当遇到这种情况时,浏览器会取它们两个中较大的那个值。例如,当<code class="language-plaintext highlighter-rouge">width=400</code>,<code class="language-plaintext highlighter-rouge">ideal viewport</code> 的宽度为320时,取的是400;当width=400, ideal viewport的宽度为480时,取的是<code class="language-plaintext highlighter-rouge">ideal viewport</code>的宽度。(ps:在uc9浏览器中,当initial-scale=1时,无论width属性的值为多少,此时viewport的宽度永远都是ideal viewport的宽度)</p>
<p>最后,总结一下,要把当前的viewport宽度设为ideal viewport的宽度,既可以设置 width=device-width,也可以设置 initial-scale=1,但这两者各有一个小缺陷,就是iphone、ipad以及IE 会横竖屏不分,通通以竖屏的ideal viewport宽度为准。所以,最完美的写法应该是,两者都写上去,这样就 initial-scale=1 解决了 iphone、ipad的毛病,width=device-width则解决了IE的毛病:</p>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<h4 id="84-关于meta-viewport的更多知识">8.4 关于meta viewport的更多知识</h4>
<h5 id="841-关于缩放以及initial-scale的默认值">8.4.1 关于缩放以及initial-scale的默认值</h5>
<p>首先我们先来讨论一下缩放的问题,前面已经提到过,缩放是相对于 <code class="language-plaintext highlighter-rouge">ideal viewport</code> 缩放的,缩放值越大,当前viewport的宽度就会越小,反之亦然。例如在iphone中,<code class="language-plaintext highlighter-rouge">ideal viewport</code> 的宽度是320px,如果我们设置 <code class="language-plaintext highlighter-rouge">initial-scale=2</code> ,此时 <code class="language-plaintext highlighter-rouge">viewport</code> 的宽度会变为只有160px了,这也好理解,放大了一倍嘛,就是原来1px的东西变成2px了,但是1px变为2px并不是把原来的320px变为640px了,而是在实际宽度不变的情况下,1px变得跟原来的2px的长度一样了,所以放大2倍后原来需要320px才能填满的宽度现在只需要160px就做到了。因此,我们可以得出一个公式:</p>
<blockquote>
<p>visual viewport宽度 = ideal viewport宽度 / 当前缩放值</p>
</blockquote>
<blockquote>
<p>当前缩放值 = ideal viewport宽度 / visual viewport宽度</p>
</blockquote>
<p>ps: <code class="language-plaintext highlighter-rouge">visual viewport</code> 的宽度指的是浏览器可视区域的宽度。</p>
<p>大多数浏览器都符合这个理论,但是安卓上的原生浏览器以及IE有些问题。安卓自带的<code class="language-plaintext highlighter-rouge">webkit</code>浏览器只有在 <code class="language-plaintext highlighter-rouge">initial-scale = 1</code> 以及没有设置<code class="language-plaintext highlighter-rouge">width</code>属性时才是表现正常的,也就相当于这理论在它身上基本没用;而IE则根本不甩initial-scale这个属性,无论你给他设置什么,<code class="language-plaintext highlighter-rouge">initial-scale</code>表现出来的效果永远是1。</p>
<p>好了,现在再来说下 <code class="language-plaintext highlighter-rouge">initial-scale</code> 的默认值问题,就是不写这个属性的时候,它的默认值会是多少呢?很显然不会是1,因为当 <code class="language-plaintext highlighter-rouge">initial-scale = 1</code> 时,当前的 <code class="language-plaintext highlighter-rouge">layout viewport</code> 宽度会被设为 <code class="language-plaintext highlighter-rouge">ideal viewport</code> 的宽度,但前面说了,各浏览器默认的 <code class="language-plaintext highlighter-rouge">layout viewport</code> 宽度一般都是980啊,1024啊,800啊等等这些个值,没有一开始就是 <code class="language-plaintext highlighter-rouge">ideal viewport</code> 的宽度的,所以 <code class="language-plaintext highlighter-rouge">initial-scale</code> 的默认值肯定不是1。安卓设备上的 <code class="language-plaintext highlighter-rouge">initial-scale</code> 默认值好像没有方法能够得到,或者就是干脆它就没有默认值,一定要你显示的写出来这个东西才会起作用,我们不管它了,这里我们重点说一下iphone和ipad上的 <code class="language-plaintext highlighter-rouge">initial-scale</code> 默认值。</p>
<p>根据测试,我们可以在iphone和ipad上得到一个结论,就是无论你给 <code class="language-plaintext highlighter-rouge">layout viewpor</code> 设置的宽度是多少,而又没有指定初始的缩放值的话,那么iphone和ipad会自动计算 <code class="language-plaintext highlighter-rouge">initial-scale</code> 这个值,以保证当前 <code class="language-plaintext highlighter-rouge">layout viewport</code> 的宽度在缩放后就是浏览器可视区域的宽度,也就是说不会出现横向滚动条。比如说,在iphone上,我们不设置任何的 <code class="language-plaintext highlighter-rouge">viewport meta</code> 标签,此时 <code class="language-plaintext highlighter-rouge">layout viewport</code> 的宽度为980px,但我们可以看到浏览器并没有出现横向滚动条,浏览器默认的把页面缩小了。根据上面的公式,<code class="language-plaintext highlighter-rouge">当前缩放值 = ideal viewport宽度 / visual viewport宽度</code>,我们可以得出:</p>
<blockquote>
<p>当前缩放值 = 320 / 980</p>
</blockquote>
<p>也就是当前的 <code class="language-plaintext highlighter-rouge">initial-scale</code> 默认值应该是 0.33这样子。当你指定了 <code class="language-plaintext highlighter-rouge">initial-scale</code> 的值后,这个默认值就不起作用了。</p>
<p>总之记住这个结论就行了:在iphone和ipad上,无论你给viewport设的宽的是多少,如果没有指定默认的缩放值,则iphone和ipad会自动计算这个缩放值,以达到当前页面不会出现横向滚动条(或者说viewport的宽度就是屏幕的宽度)的目的。</p>
<p><a href="https://camo.githubusercontent.com/c3e65344643f25a139e9756b0802be12f819eccf/68747470733a2f2f7773312e73696e61696d672e636e2f6c617267652f303036744e6337396779316667396c6970756472306a333071383063647767392e6a7067"><img src="https://camo.githubusercontent.com/c3e65344643f25a139e9756b0802be12f819eccf/68747470733a2f2f7773312e73696e61696d672e636e2f6c617267652f303036744e6337396779316667396c6970756472306a333071383063647767392e6a7067" alt="img" /></a></p>
<h5 id="842-动态改变meta-viewport标签">8.4.2 动态改变meta viewport标签</h5>
<p>第一种方法</p>
<p>可以使用 <code class="language-plaintext highlighter-rouge">document.write</code> 来动态输出 <code class="language-plaintext highlighter-rouge">meta viewport</code> 标签,例如:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>document.write('<meta name="viewport" content="width=device-width,initial-scale=1">')
</code></pre></div></div>
<p>第二种方法</p>
<p>通过 <code class="language-plaintext highlighter-rouge">setAttribute</code> 来改变</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><meta id="testViewport" name="viewport" content="width = 380">
<script>
var mvp = document.getElementById('testViewport');
mvp.setAttribute('content','width=480');
</script>
</code></pre></div></div>
<p>安卓2.3自带浏览器上的一个 <code class="language-plaintext highlighter-rouge">bug</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><meta name="viewport" content="width=device-width">
<script type="text/javascript">
alert(document.documentElement.clientWidth); //弹出600,正常情况应该弹出320
</script>
<meta name="viewport" content="width=600">
<script type="text/javascript">
alert(document.documentElement.clientWidth); //弹出320,正常情况应该弹出600
</script>
</code></pre></div></div>
<p>测试的手机 <code class="language-plaintext highlighter-rouge">ideal viewport</code> 宽度为320px,第一次弹出的值是600,但这个值应该是第行meta标签的结果啊,然后第二次弹出的值是320,这才是第一行meta标签所达到的效果啊,所以在安卓2.3(或许是所有2.x版本中)的自带浏览器中,对 <code class="language-plaintext highlighter-rouge">meta viewport</code> 标签进行覆盖或更改,会出现让人非常迷糊的结果。</p>
<h3 id="最后我们来看一个栗子来加深上面概念的印象">最后我们来看一个栗子来加深上面概念的印象:</h3>
<p>一只笔的像素如下:
<a href="https://camo.githubusercontent.com/18cf727547576f6804224f7514161a89892ebcf2/68747470733a2f2f7773312e73696e61696d672e636e2f6c617267652f303036744e6252776779316667386b31346d6635356a3330636930397477656a2e6a7067"><img src="https://camo.githubusercontent.com/18cf727547576f6804224f7514161a89892ebcf2/68747470733a2f2f7773312e73696e61696d672e636e2f6c617267652f303036744e6252776779316667386b31346d6635356a3330636930397477656a2e6a7067" alt="img" /></a></p>
<p>这只笔在屏幕c,d,e下的显示效果如下:
<a href="https://camo.githubusercontent.com/56bbff17103227634e98d0921ff09453b647cde1/68747470733a2f2f7773332e73696e61696d672e636e2f6c617267652f303036744e6252776779316667386b3176373978396a3330676f3039716139792e6a7067"><img src="https://camo.githubusercontent.com/56bbff17103227634e98d0921ff09453b647cde1/68747470733a2f2f7773332e73696e61696d672e636e2f6c617267652f303036744e6252776779316667386b3176373978396a3330676f3039716139792e6a7067" alt="img" /></a></p>
<p>看到同一张图片在各屏幕显示大小不一。
我们希望不同屏幕显示图片的大小要一致。
我们要计算图片缩放比例。
计算公式:
(图片逻辑像素大小px1) / (图片缩放后实际像素大小px2) = (设备像素dp) / (设备独立像素dips)
px2 = px1 * (dp / dips)
px2 = px1 * dpr
此时,这只笔在屏幕c,d,e下的显示效果如下:
<a href="https://camo.githubusercontent.com/6f4c05013894c0429aa04df00031588b0cd321f4/68747470733a2f2f7773322e73696e61696d672e636e2f6c617267652f303036744e6252776779316667386b3339386f38776a3330676f3063307132782e6a7067"><img src="https://camo.githubusercontent.com/6f4c05013894c0429aa04df00031588b0cd321f4/68747470733a2f2f7773322e73696e61696d672e636e2f6c617267652f303036744e6252776779316667386b3339386f38776a3330676f3063307132782e6a7067" alt="img" /></a></p>
<blockquote>
<p>通过上面的我们可以看到,不同的 <code class="language-plaintext highlighter-rouge">DPR</code> (设备像素比)要想显示大小一样,必须准备三张不同分辨率的图片,那么,我想一张图片就在三种不同的屏幕下显示一样的大小,能做到吗?当然能做到,这就需要缩放了,要自己计算缩放多麻烦,那有没有一种简单的方式呢?当然有,那就是你在熟悉不过的px,你会发现设置图片宽度为50px以后,在各个移动终端的大小看起来都一样,这是什么原因呢。</p>
</blockquote>
<p>按照 <code class="language-plaintext highlighter-rouge">CSS</code> 规范的定义,<code class="language-plaintext highlighter-rouge">CSS</code> 中的 <code class="language-plaintext highlighter-rouge">px</code> 是一个相对长度,它相对的,是 <code class="language-plaintext highlighter-rouge">viewing device</code> 的分辨率。这个<code class="language-plaintext highlighter-rouge">viewing device</code>,通常就是电脑显示器。典型的电脑显示器的分辨率是<code class="language-plaintext highlighter-rouge">96DPI</code>,也就是1像素为1/96英寸(实际上,假设我们的显示器分辨率都与物理分辨率一致,而液晶点距其实是0.25mm到0.29mm之间,所以不太可能是正好1/96英寸,而只是接近)。</p>
<p>一般来说,<code class="language-plaintext highlighter-rouge">px</code> 就是对应设备的物理像素,然而如果输出设备的解析度与电脑显示器大不相同,输出效果就会有问题。例如打印机输出到纸张上,其解析度比电脑屏幕要高许多,如果不缩放,直接使用设备的物理像素,那电脑上的照片由 <code class="language-plaintext highlighter-rouge">600DPI</code> 的打印机打出来就比用显示器看小了约6倍。</p>
<p>所以 <code class="language-plaintext highlighter-rouge">CSS</code> 规定,在这种情况下,浏览器应该对像素值进行缩放调节,以保持阅读体验的大体一致。也就是要保持一定像素的长度在不同设备输出上看上去的大小总是差不多。</p>
<p>怎样确保这一点呢?直接按照设备物理像素的大小进行换算当然是一个方式,但是CSS考虑得更多,它建议,转换应按照“参考像素”(<code class="language-plaintext highlighter-rouge">reference pixel</code>)来进行。</p>
<p>眼睛看到的大小,取决于可视角度。而可视角度取决于物体的实际大小以及物体与眼睛的距离。10米远处一个1米见方的东西,与1米远处的10厘米见方的东西,看上去的大小差不多是一样的,所谓一叶障目不见泰山,讲的就是这个常识。</p>
<p>因此CSS规范使用视角来定义“参考像素”,1参考像素即为从一臂之遥看解析度为96DPI的设备输出(即1英寸96点)时,1点(即1/96英寸)的视角。</p>
<p>请注意这个差别——<code class="language-plaintext highlighter-rouge">CSS</code>规范定义的参考像素并不是1/96英寸,而是1/96英寸在一臂之遥的看起来的视角。通常认为常人臂长为28英寸,所以其视角可以计算出来是0.0213度。<code class="language-plaintext highlighter-rouge">(即(1/96)in / (28in * 2 * PI / 360deg) )</code></p>
<p>我们在使用不同设备输出时,眼睛与设备输出的典型距离是不同的。比如电脑显示器,通常是一臂之距,而看书和纸张时(对应于打印机的设备输出),则通常会更近一些。看电视时则会更远,比如一般建议是电视机屏幕对角线的2.5到3倍长——如果你是个42’彩电,那就差不多是3米远。看电影的话……我就不知道多远了,您自己量吧。</p>
<p>因此,1参考像素:
对于电脑显示器是0.26mm(即1/96英寸);
对于激光打印机是0.20mm(假设阅读距离通常为55cm,即21英寸);</p>
<p>而换算时,对于300DPI的打印机(即每个点是1/300英寸),1px通常会四舍五入到3dots,也就是0.25mm左右;而对于600DPI的打印机,则可能四舍五入到5dots,也就是0.21mm。</p>
<p><a href="https://camo.githubusercontent.com/6a50818fb48e1baa57108d0413d3ddf239115ad9/687474703a2f2f7777772e77332e6f72672f54522f435353322f696d616765732f706978656c312e706e67"><img src="https://camo.githubusercontent.com/6a50818fb48e1baa57108d0413d3ddf239115ad9/687474703a2f2f7777772e77332e6f72672f54522f435353322f696d616765732f706978656c312e706e67" alt="img" /></a></p>
<p>上图中,左边的屏幕(可以认为是电脑屏幕)的典型视觉距离是71厘米即28英寸,其1px对应了0.28mm;
而右边的屏幕(可以认为是你的42寸高清电视)的典型视觉距离是3.5米即120英寸,其1px对应1.3mm。42寸的1080p电视,分辨率是1920*1080,则其物理像素只有0.5mm左右,可见确实是高清哦。</p>
<p>综上,<code class="language-plaintext highlighter-rouge">px</code> 是一个相对单位,而且在特定设备上总是一个近似值(原则是尽量接近参考像素)。</p>
<p>然而,如果你把绝对单位理解为对输出效果的绝对掌控,事情却大相径庭。就网页输出的最主要对象——电脑屏幕来说,<code class="language-plaintext highlighter-rouge">px</code> 可被视为一个基准单位——与桌面分辨率一致,如果是液晶屏,则几乎总是与液晶屏物理分辨率一致——也就是说网页设计者设定的1px,就是“最终看到这个网页的用户的显示器上的1个点距”!反倒是那些绝对单位,其实一点也不绝对。</p>
<p>转载:https://blog.csdn.net/qq_33834489/article/details/79247119</p>Danniel1、PX(CSS pixels)真机远程调试 方法 汇总2020-04-19T00:00:00+00:002020-04-19T00:00:00+00:00http://daner1990.github.io/javascript/2020/04/19/%E7%9C%9F%E6%9C%BA%E8%BF%9C%E7%A8%8B%E8%B0%83%E8%AF%95<p>-–</p>
<p>layout: post</p>
<p>title: 真机远程调试 方法 汇总</p>
<p>my_excerpt: 。</p>
<p>categories: [javascript]</p>
<p>tags: [js,tools]</p>
<p>description: “真机远程调试”(remote inspect web on real device),是指用桌面电脑(PC或MAC)远程连接上移动设备,通过类似Chrome浏览器开发人员工具的界面,来调试移动设备上运行的网页</p>
<p>pid: 201706280010</p>
<p>-–</p>
<h2 id="系统自带调试">系统自带调试</h2>
<h3 id="ios系统">IOS系统</h3>
<p><strong>运行环境要求</strong></p>
<ul>
<li>Mac + Safari 浏览器</li>
<li>iPhone(iOS 6 +) + Safari 浏览器</li>
</ul>
<p><strong>调试步骤</strong></p>
<p><strong>1、使用 Lightning 数据线将 iPhone 与 Mac 相连</strong></p>
<p><strong>2、iPhone 开启 Web 检查器(<code class="language-plaintext highlighter-rouge">设置</code> -> <code class="language-plaintext highlighter-rouge">Safari</code> -> <code class="language-plaintext highlighter-rouge">高级</code> -> <code class="language-plaintext highlighter-rouge">开启 Web 检查器</code>)</strong></p>
<p><strong>3、iPhone 使用 Safari 浏览器打开要调试的页面</strong></p>
<p><strong>4、Mac 打开 Safari 浏览器调试(<code class="language-plaintext highlighter-rouge">菜单栏</code> —> <code class="language-plaintext highlighter-rouge">开发</code> -> <code class="language-plaintext highlighter-rouge">iPhone 设备名</code> -> <code class="language-plaintext highlighter-rouge">选择调试页面</code>)</strong></p>
<ul>
<li>如果你的菜单栏没有“开发”选项,可以到左上角 <code class="language-plaintext highlighter-rouge">Safari</code> -> <code class="language-plaintext highlighter-rouge">偏好设置</code> -> <code class="language-plaintext highlighter-rouge">高级</code> -> <code class="language-plaintext highlighter-rouge">在菜单栏中显示“开发”菜单</code>。</li>
</ul>
<p><strong>5、在弹出的 Safari Developer Tools 中调试</strong></p>
<p>经过如上步骤就可在 Mac 端调试 iPhone 上 Safari 运行的页面了,但对于 WebView 页面就不适用了,另外 Windows 系统不适用此方案。</p>
<p><strong>当前测试环境:</strong></p>
<ul>
<li>Safari 版本 10.0.2</li>
<li>iPhone 7(iOS 10.1.1)</li>
</ul>
<p>没有 iPhone 设备可以在 App Store 安装 Xcode 使用其内置的 iOS 模拟器,安装完成后通过以下两种方式开启:</p>
<ul>
<li>右键 <code class="language-plaintext highlighter-rouge">Xcode 图标</code> -> <code class="language-plaintext highlighter-rouge">Open Developer Tool</code> -> <code class="language-plaintext highlighter-rouge">Simulator</code></li>
<li>右键 <code class="language-plaintext highlighter-rouge">Finder 图标</code> -> <code class="language-plaintext highlighter-rouge">前往文件夹</code> -> <code class="language-plaintext highlighter-rouge">/应用程序/Xcode.app/Contents/Developer/Applications/</code> -> 运行 <code class="language-plaintext highlighter-rouge">Simulator.app</code></li>
</ul>
<p>运行 iOS 模拟器后,在模拟器中打开调试页面,再通过 Mac Safari 开发功能就可以调试到。</p>
<p>如果我需要调试更低版本的 iOS 怎么办?实际使用的 iPhone 不可能去降版本,不必担心,Simulator 有。</p>
<p>点击左上角 <code class="language-plaintext highlighter-rouge">Xcode</code> -> <code class="language-plaintext highlighter-rouge">Preferences</code> -> <code class="language-plaintext highlighter-rouge">Downloads</code> 就可以看到提供了如下版本:</p>
<p><img src="https://misc.aotu.io/ONE-SUNDAY/MobileDebug/Xcode_Simulator_Version.jpg" alt="iOS 版本下载" /></p>
<h3 id="android-系统">Android 系统</h3>
<p><strong>运行环境要求</strong></p>
<ul>
<li>Chrome 版本 >= 32</li>
<li>Android 版本 4.0 +</li>
</ul>
<p><strong>调试步骤</strong></p>
<p><strong>1、使用 USB 数据线将手机与电脑相连</strong></p>
<p><strong>2、手机进入开发者模式,勾选 USB 调试,并允许调试</strong></p>
<p><strong>如何开启 USB 调试:</strong></p>
<p><strong>索尼 Z5:</strong><code class="language-plaintext highlighter-rouge">设置</code> -> <code class="language-plaintext highlighter-rouge">关于关机</code> -> <code class="language-plaintext highlighter-rouge">多次点击软件版本开启</code> -> <code class="language-plaintext highlighter-rouge">返回上一级</code> -> <code class="language-plaintext highlighter-rouge">开发者选项</code> -> <code class="language-plaintext highlighter-rouge">USB 调试</code></p>
<p><strong>魅蓝 Note:</strong><code class="language-plaintext highlighter-rouge">设置</code> -> <code class="language-plaintext highlighter-rouge">辅助功能</code> -> <code class="language-plaintext highlighter-rouge">开发者选项</code> -> <code class="language-plaintext highlighter-rouge">USB 调试</code></p>
<p>不同 Android 设备进入开发者模式的方式有稍稍不同,瞎捣鼓一下即可开启。</p>
<p><strong>3、电脑打开 Chrome 浏览器,在地址栏输入:<code class="language-plaintext highlighter-rouge">chrome://inspect/#devices</code> 并勾选 <code class="language-plaintext highlighter-rouge">Discover USB devices</code> 选项</strong></p>
<p><img src="https://misc.aotu.io/ONE-SUNDAY/MobileDebug/Discover_USB_devices.jpg" alt="开启 Discover USB devices" /></p>
<p><strong>4、手机允许远程调试,并访问调试页面</strong></p>
<p><strong>5、电脑点击 inspect 按钮</strong></p>
<p>如果你出现无法识别到设备的情况,建议尝试以下几种方法:</p>
<ul>
<li>使用原装数据线,不要使用山寨数据线或一线多头的数据线</li>
<li>重新插拔 USB 数据线,让手机处于充电状态</li>
<li>关闭电脑相关的应用助手</li>
<li>重启手机</li>
<li>Windows 系统下自动安装驱动失败,到 <a href="https://developer.android.com/studio/run/oem-usb.html#Drivers">Android Studio</a> 手动下载</li>
</ul>
<p>注意:使用 Chrome Inspect 查看页面时,Chrome 需要从 <a href="https://chrome-devtools-frontend.appspot.com/">https://chrome-devtools-frontend.appspot.com</a> 加载资源,如果你得到的调试界面是一片空白,那你可能需要科学上网。</p>
<p><strong>6、进入调试界面</strong></p>
<h2 id="使用代理工具调试开发环境页面">使用代理工具调试开发环境页面</h2>
<p>对于需要配 Hosts 才能访问的开发环境页面,手机在默认情况下是没有权限修改 Hosts 文件的,除非是 iOS 设备越狱后和 Android 设备 root 后,所以一般情况下手机是无法访问开发环境页面,这时需要使用到 Mac 系统的 <a href="https://www.charlesproxy.com/">Charles</a> 代理工具,Windows 系统可使用 <a href="http://www.telerik.com/fiddler">Fiddler</a> 代理工具。</p>
<p><strong>实现思路</strong></p>
<ul>
<li>Mac 作为代理服务器</li>
<li>手机通过 HTTP 代理连接到 Mac 电脑</li>
<li>手机上的请求都经过代理服务器</li>
<li>通过给 Mac 配 Hosts 实现目的</li>
</ul>
<h2 id="weinre-调试工具">Weinre 调试工具</h2>
<p>Weinre 是一款较老的远程调试工具,功能与 Chrome DevTools 相似,需要在页面中插入一段 JS 脚本来实时调试页面 DOM 结构、样式、JS 等,另外它使用的是代理的方式,所以兼容性很好,无论是新老设备系统通吃,但对于样式调试不友善,缺少断点调试及 Profiles 等常用功能。</p>
<p><strong>调试步骤:</strong></p>
<p><strong>1、安装 Weinre</strong></p>
<p>使用 NPM 全局安装 Weinre</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo npm -g install weinre
</code></pre></div></div>
<p><strong>2、启动 Weinre 监听服务</strong></p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">$</span> <span class="nx">ipconfig</span> <span class="nx">getifaddr</span> <span class="nx">en0</span> <span class="c1">// 查看本机 IP</span>
<span class="nx">$</span> <span class="nx">weinre</span> <span class="o">--</span><span class="nx">boundHost</span> <span class="mf">10.14</span><span class="p">.</span><span class="mf">217.14</span> <span class="o">--</span><span class="nx">httpPort</span> <span class="mi">8090</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">--boundHost</code> 后填入你本机 IP 地址,<code class="language-plaintext highlighter-rouge">--httpPort</code> 后填入端口号,默认为 8080</p>
<p><strong>3、进入 Weinre 管理页面</strong></p>
<p>使用 Chrome 浏览器访问 <a href="http://10.14.217.14:8090/">http://10.14.217.14:8090</a>,在管理页面你可以看到使用相关的说明,有进入客户端调试界面的地址、使用的文档、DEMO 页面等等,说明中要求将一段 JS 脚本 `` 插入到需要调试的页面中,插入代码后手机访问调试页面。</p>
<p><strong>4、进入客户端调试界面</strong></p>
<p>点击 debug client user interface:http://10.14.217.14:8090/client/#anonymous 的链接。</p>
<p><strong>5、JS 脚本注入</strong></p>
<p>手动加入 JS 脚本不优雅,这里可以结合我们前面提到的 Charles 代理工具实现动态 HTTP Script 注入。</p>
<p>打开<code class="language-plaintext highlighter-rouge">菜单</code> -> <code class="language-plaintext highlighter-rouge">Rewrite</code> -> 勾选 <code class="language-plaintext highlighter-rouge">Enable Rewrite</code></p>
<p>输入 Rewrite 的名字并且在 Rules 一项添加匹配的规则,Location 一项是用于指定的域名和端口添加规则用的,这里我们不填默认匹配所有请求。</p>
<p>Type 允许对需要匹配的请求进行 Rewrite,一共提供了 11 种:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">Add Header</code></li>
<li><code class="language-plaintext highlighter-rouge">Modify Header</code></li>
<li><code class="language-plaintext highlighter-rouge">Remove Header</code></li>
<li><code class="language-plaintext highlighter-rouge">Host</code></li>
<li><code class="language-plaintext highlighter-rouge">Path</code></li>
<li><code class="language-plaintext highlighter-rouge">URL</code></li>
<li><code class="language-plaintext highlighter-rouge">Add Query Param</code></li>
<li><code class="language-plaintext highlighter-rouge">Modify Query Param</code></li>
<li><code class="language-plaintext highlighter-rouge">Remove Query Param</code></li>
<li><code class="language-plaintext highlighter-rouge">Response Status</code></li>
<li><code class="language-plaintext highlighter-rouge">Body</code></li>
</ul>
<p>这里我们需要使用到的是 <code class="language-plaintext highlighter-rouge">Body</code>,它的作用是对请求或响应内容进行匹配替换,按照下图的配置,通过将匹配到的响应内容 `` 标签替换成需要插入到页面中的 JS 脚本,从而实现动态插入。</p>
<p><img src="https://misc.aotu.io/ONE-SUNDAY/MobileDebug/Charles_Rewrite_Rule.jpg" alt="Rewrite Rule" /></p>
<p>另外,也有基于 Weinre 进行功能扩展的工具,比如早期版本的 <a href="https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1455784140&token=&lang=zh_CN">微信 Web 开发者工具 v0.7.0</a> 和 <a href="https://github.com/wuchangming/spy-debugger">spy-debugger</a>,都在 Weinre 的基础上简化了要给每个调试页面添加 JS 脚本的步骤,spy-debugger 还增加了对 HTTPS 的支持。</p>
<hr />
<p>感谢你的阅读,如果你还有其他更为实用的调试方案,欢迎下方留言交流。</p>
<p>ref:https://aotu.io/notes/2017/02/24/Mobile-debug/index.html</p>
<p>ref:https://github.com/jieyou/remote_inspect_web_on_real_device/blob/master/README.textile</p>Danniel-–Chrome开发者工具小细节2017-06-28T00:00:00+00:002017-06-28T00:00:00+00:00http://daner1990.github.io/tools/2017/06/28/Chrome%E5%BC%80%E5%8F%91%E8%80%85%E5%B7%A5%E5%85%B7%E5%B0%8F%E7%BB%86%E8%8A%82<h3 id="查看资源发起者和依赖项">查看资源发起者和依赖项</h3>
<blockquote>
<p>在<code class="language-plaintext highlighter-rouge">NetWork</code>面板
按住<code class="language-plaintext highlighter-rouge"> Shift </code>并将<code class="language-plaintext highlighter-rouge">鼠标悬停</code>在资源上,可以查看其发起者和依赖项。 本部分将您悬停的资源称为目标。
目标上方的第一个<code class="language-plaintext highlighter-rouge">绿色</code>编码资源为目标的发起者。 如果上方存在第二个也是绿色编码的资源,那么该资源将是发起者的发起者。 目标下方红色编码的任何资源都是目标的依赖项</p>
</blockquote>
<h3 id="过滤请求">过滤请求</h3>
<p>Network 面板提供了多种方式来过滤要显示哪些资源。 点击 Filter 按钮 (Filter 按钮) 可以隐藏或显示 Filters 窗格。</p>
<p>使用内容类型按钮可以仅显示选定内容类型的资源。</p>
<p>注:按住<code class="language-plaintext highlighter-rouge"> Cmd (Mac) </code>或 <code class="language-plaintext highlighter-rouge">Ctrl (Windows/Linux) </code> 并点击过滤器可以同时启用多个过滤器。</p>
<h3 id="resource-timing">resource timing</h3>
<blockquote>
<p>Resource Timing API 提供了与接收各个资源的时间有关的大量详细信息。请求生命周期的主要阶段包括:</p>
</blockquote>
<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">重定向</span>
<span class="n">立即开始</span> <span class="sb">`startTime`</span><span class="err">。</span>
<span class="n">如果正在发生重定向</span><span class="err">,</span><span class="sb">`redirectStart`</span> <span class="n">也会开始</span><span class="err">。</span>
<span class="n">如果重定向在本阶段末发生</span><span class="err">,</span><span class="n">将采集</span> <span class="sb">`redirectEnd`</span><span class="err">。</span>
<span class="n">应用缓存</span>
<span class="n">如果是应用缓存在实现请求</span><span class="err">,</span><span class="n">将采集</span> <span class="sb">`fetchStart`</span> <span class="n">时间</span><span class="err">。</span>
<span class="n">DNS</span>
<span class="sb">`domainLookupStart`</span> <span class="n">时间在</span> <span class="n">DNS</span> <span class="n">请求开始时采集</span><span class="err">。</span>
<span class="sb">`domainLookupEnd`</span> <span class="n">时间在</span> <span class="n">DNS</span> <span class="n">请求结束时采集</span><span class="err">。</span>
<span class="n">TCP</span>
<span class="sb">`connectStart`</span> <span class="n">在初始连接到服务器时采集</span><span class="err">。</span>
<span class="n">如果正在使用</span> <span class="n">TLS</span> <span class="n">或</span> <span class="n">SSL</span><span class="err">,</span><span class="sb">`secureConnectionStart `</span><span class="n">将在握手</span><span class="err">(</span><span class="n">确保连接安全</span><span class="err">)</span><span class="n">开始时开始</span><span class="err">。</span>
<span class="sb">`connectEnd`</span> <span class="n">将在到服务器的连接完成时采集</span><span class="err">。</span>
<span class="n">请求</span>
<span class="sb">`requestStart`</span> <span class="n">会在对某个资源的请求被发送到服务器后立即采集</span><span class="err">。</span>
<span class="n">响应</span>
<span class="sb">`responseStart`</span> <span class="n">是服务器初始响应请求的时间</span><span class="err">。</span>
<span class="sb">`responseEnd`</span> <span class="n">是请求结束并且数据完成检索的时间</span></code></pre></figure>
<p><img src="/postPics/resource-timing-api.png" alt="" style="margin: 5px auto;" /></p>
<h3 id="在-devtools-中查看">在 DevTools 中查看</h3>
<blockquote>
<p><a href="https://developers.google.com/web/tools/chrome-devtools/network-performance/understanding-resource-timing">LINK:network-performance/understanding-resource-timing</a></p>
</blockquote>
<h5 id="要查看-network-面板中给定条目完整的耗时信息您有三种选择">要查看 Network 面板中给定条目完整的耗时信息,您有三种选择。</h5>
<ol>
<li>
<p>将鼠标悬停到 Timeline 列下的耗时图表上。这将呈现一个显示完整耗时数据的弹出窗口。</p>
</li>
<li>
<p>点击任何条目并打开该条目的 Timing 标签。</p>
</li>
<li>
<p>使用 Resource Timing API 从 JavaScript 检索原始数据。
Resource Timing 信息</p>
</li>
</ol>
<p><img src="/postPics/resource-timing-data.png" alt="" style="margin: 5px auto;" /></p>
<p>此代码可以在 DevTools 的 Console 中运行。 它将使用 Network Timing API 检索所有资源。 然后,它将通过查找是否存在名称中包含“style.css”的条目对条目进行过滤。 如果找到,将返回相应条目。</p>
<blockquote>
<p>performance.getEntriesByType(‘resource’).filter(item => item.name.includes(“style.css”))</p>
</blockquote>
<p><img src="/postPics/resource-timing-post.png" alt="" style="margin: 5px auto;" /></p>
<h5 id="resource-timing-条目">Resource Timing 条目</h5>
<p><code class="language-plaintext highlighter-rouge">Queuing</code></p>
<p>如果某个请求正在排队,则指示:</p>
<p>请求已被渲染引擎推迟,因为该请求的优先级被视为低于关键资源(例如脚本/样式)的优先级。 图像经常发生这种情况。</p>
<p>请求已被暂停,以等待将要释放的不可用 TCP 套接字。</p>
<p>请求已被暂停,因为在 HTTP 1 上,浏览器仅允许每个源拥有六个 TCP 连接。
生成磁盘缓存条目所用的时间(通常非常迅速)</p>
<p><code class="language-plaintext highlighter-rouge">Stalled/Blocking</code></p>
<p>请求等待发送所用的时间。 可以是等待 Queueing 中介绍的任何一个原因。 此外,此时间包含代理协商所用的任何时间。</p>
<p><code class="language-plaintext highlighter-rouge">Proxy Negotiation</code></p>
<p>与代理服务器连接协商所用的时间。</p>
<p><code class="language-plaintext highlighter-rouge">DNS Lookup</code></p>
<p>执行 DNS 查询所用的时间。 页面上的每一个新域都需要完整的往返才能执行 DNS 查询。</p>
<p><code class="language-plaintext highlighter-rouge">Initial Connection / Connecting</code></p>
<p>建立连接所用的时间,包括 TCP 握手/重试和协商 SSL 的时间。</p>
<p><code class="language-plaintext highlighter-rouge">SSL</code></p>
<p>完成 SSL 握手所用的时间。</p>
<p><code class="language-plaintext highlighter-rouge">Request Sent / Sending</code>
发出网络请求所用的时间。 通常不到一毫秒。</p>
<p><code class="language-plaintext highlighter-rouge">Waiting (TTFB)</code></p>
<p>等待初始响应所用的时间,也称为<code class="language-plaintext highlighter-rouge">至第一字节的时间 Time to first Byte</code>。
此时间将捕捉到服务器往返的延迟时间,以及等待服务器传送响应所用的时间。</p>
<p><code class="language-plaintext highlighter-rouge">Content Download / Downloading</code></p>
<p>接收响应数据所用的时间。</p>
<h3 id="诊断网络问题">诊断网络问题</h3>
<blockquote>
<p>通过 Network 面板可以发现大量可能的问题。查找这些问题需要很好地了解客户端与服务器如何通信,以及协议施加的限制。</p>
</blockquote>
<h5 id="已被加入队列或已被停止的系列">已被加入队列或已被停止的系列</h5>
<p>最常见问题是一系列已被加入队列或已被停止的条目。这表明正在从单个网域检索太多的资源。在 HTTP 1.0/1.1 连接上,Chrome 会将每个主机强制设置为最多六个 TCP 连接。如果您一次请求十二个条目,前六个将开始,而后六个将被加入队列。最初的一半完成后,队列中的第一个条目将开始其请求流程。</p>
<h5 id="被停止的请求系列">被停止的请求系列</h5>
<p>要为传统的 HTTP 1 流量解决此问题,您需要实现域分片。也就是在您的应用上设置多个子域,以便提供资源。然后,在子域之间平均分配正在提供的资源。</p>
<p>HTTP 1 连接的修复结果不会应用到 HTTP 2 连接上。事实上,前者的结果会影响后者。 如果您部署了 HTTP 2,请不要对您的资源进行域分片,因为它与 HTTP 2 的操作方式相反。在 HTTP 2 中,到服务器的单个 TCP 连接作为多路复用连接。这消除了 HTTP 1 中的六个连接限制,并且可以通过单个连接同时传输多个资源。</p>
<h5 id="至第一字节的漫长时间">至第一字节的漫长时间</h5>
<p>又称:<code class="language-plaintext highlighter-rouge">大片绿色</code></p>
<p>长 TTFB 指示灯</p>
<p>等待时间长表示至第一字节的时间 (TTFB) 漫长。建议将此值控制在 <code class="language-plaintext highlighter-rouge">200 毫秒以下</code>。长 TTFB 会揭示两个主要问题之一。</p>
<p>请执行以下任一操作:</p>
<ol>
<li>
<p>客户端与服务器之间的网络条件较差,或者</p>
</li>
<li>
<p>服务器应用的响应慢</p>
</li>
</ol>
<p>要解决长 TTFB,首先请尽可能缩减网络。理想的情况是将应用托管在本地,然后查看 TTFB 是否仍然很长。如果仍然很长,则需要优化应用的响应速度。可以是优化数据库查询、为特定部分的内容实现缓存,或者修改您的网络服务器配置。很多原因都可能导致后端缓慢。您需要调查您的软件并找出未满足您的性能预算的内容。</p>
<p>如果本地托管后 TTFB 仍然漫长,那么问题出在您的客户端与服务器之间的网络上。很多事情都可以阻止网络遍历。客户端与服务器之间有许多点,每个点都有其自己的连接限制并可能引发问题。测试时间是否缩短的最简单方法是将您的应用置于其他主机上,并查看 TTFB 是否有所改善。</p>
<h5 id="达到吞吐量能力">达到吞吐量能力</h5>
<p>又称:<code class="language-plaintext highlighter-rouge">大片蓝色</code></p>
<p>如果您看到 Content Download 阶段花费了大量时间,则提高服务器响应或串联不会有任何帮助。首要的解决办法是减少发送的字节数。</p>Danniel查看资源发起者和依赖项reviewboard工具rbtools使用2017-06-27T00:00:00+00:002017-06-27T00:00:00+00:00http://daner1990.github.io/tools/2017/06/27/reviewboard%E5%B7%A5%E5%85%B7rbtools%E4%BD%BF%E7%94%A8<h2 id="背景">背景</h2>
<p>我司在提交代码之前都需要对代码进行pre commit review。但在实际操作中并没有那么严苛。</p>
<p>由于正常流程提交代码审核需要打开review网页,生成diff文件,上传diff,对标题细节进行描述,并发布。</p>
<p>整个流程非常耗时繁琐,所以我一直是用的工具是eclipse中的插件tao-reviewborad。</p>
<p>但是eclipse的缺点也很明显,每次在进行提交代码之前都需要打开eclipse,进行review相关操作的提交。</p>
<p>为了使用其中一个插件的功能而内存中长时间打开一个1G左右的软件有些得不偿失。</p>
<h2 id="解决方案">解决方案</h2>
<p>最近了解到RBTool新版本支持ALIASES,支持自定义命令,只要在reviewboardrc文件中进行简单的配置就可以实现一条命令直接发布代码审计。</p>
<p>不需要自己写脚本去编写。十分方便,所以一试~</p>
<p>具体reviewboardrc代码如下:</p>
<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">REVIEWBOARD_URL</span> <span class="o">=</span> <span class="s">"http://reviewboard.xxx.domain/"</span>
<span class="n">REPOSITORY</span> <span class="o">=</span> <span class="s">"xxx"</span>
<span class="n">REPOSITORY_URL</span> <span class="o">=</span> <span class="s">"https://scm.xxx.domain:18080/svn/xxx/trunk/xxx/"</span>
<span class="n">OPEN_BROWSER</span> <span class="o">=</span> <span class="bp">True</span>
<span class="n">ENABLE_PROXY</span> <span class="o">=</span> <span class="bp">False</span>
<span class="n">GUESS_FIELDS</span> <span class="o">=</span> <span class="s">"auto"</span>
<span class="n">GUESS_SUMMARY</span> <span class="o">=</span> <span class="s">"auto"</span>
<span class="n">GUESS_DESCRIPTION</span> <span class="o">=</span> <span class="s">"auto"</span>
<span class="n">DEBUG</span> <span class="o">=</span> <span class="s">"true"</span>
<span class="n">PUBLISH</span> <span class="o">=</span> <span class="s">"true"</span>
<span class="n">ALIASES</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'pt'</span> <span class="p">:</span> <span class="s">'post --summary $1 --description $1'</span><span class="p">,</span>
<span class="s">'ps'</span> <span class="p">:</span> <span class="s">'post'</span>
<span class="p">}</span>
<span class="n">USERNAME</span> <span class="o">=</span> <span class="s">"xxxxx"</span>
<span class="n">PASSWORD</span> <span class="o">=</span> <span class="s">"xxxxx"</span></code></pre></figure>
<p>我在aliases中定义了一个command pt 来简化整个post的操作,如下</p>
<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">rbt</span> <span class="n">pt</span> <span class="s">"保持summary和description一致,无法空格"</span></code></pre></figure>
<p>只需要一条简单命令就可以实现提交发布。</p>
<p>具体参数配置可以参看官网:</p>
<p>https://www.reviewboard.org/docs/rbtools/dev/rbt/commands/post/#rbt-post</p>
<h2 id="弊端">弊端</h2>
<p>在实际操作过程并不是那么美好,虽然节约了字符输入量,有两个问题</p>
<blockquote>
<p>第一:耗时并没有得到特别的缩短,特别是在上传diff的时候会出现明显的等待时间,文件越大耗时越长,虽然在内网之内去掉代理</p>
</blockquote>
<p>依然有这个问题。5s左右的等待期其实有些无法忍受,具体解决方案等待调研</p>
<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="o">>>></span> <span class="n">Running</span><span class="p">:</span> <span class="n">svn</span> <span class="o">--</span><span class="n">non</span><span class="o">-</span><span class="n">interactive</span> <span class="n">status</span> <span class="o">-</span><span class="n">q</span> <span class="o">--</span><span class="n">ignore</span><span class="o">-</span><span class="n">externals</span>
<span class="o">>>></span> <span class="n">Running</span><span class="p">:</span> <span class="n">svn</span> <span class="o">--</span><span class="n">non</span><span class="o">-</span><span class="n">interactive</span> <span class="n">diff</span> <span class="o">--</span><span class="n">diff</span><span class="o">-</span><span class="n">cmd</span><span class="o">=</span><span class="n">diff</span> <span class="o">--</span><span class="n">notice</span><span class="o">-</span><span class="n">ancestry</span> <span class="o">-</span><span class="n">r</span> <span class="n">BASE</span>
<span class="o">>>></span> <span class="n">Making</span> <span class="n">HTTP</span> <span class="n">GET</span> <span class="n">request</span> <span class="n">to</span> <span class="n">http</span><span class="p">:</span><span class="o">//</span><span class="n">reviewboard</span><span class="p">.</span><span class="n">xxx</span><span class="p">.</span><span class="n">domain</span><span class="o">/</span><span class="n">api</span><span class="o">/</span><span class="n">review</span><span class="o">-</span><span class="n">requests</span><span class="o">/</span><span class="err">?</span><span class="n">only</span><span class="o">-</span><span class="n">links</span><span class="o">=</span><span class="n">create</span><span class="o">&</span><span class="n">only</span><span class="o">-</span><span class="n">fields</span><span class="o">=</span>
<span class="o">>>></span> <span class="n">在当前位置会有空白时间5s左右</span>
<span class="o">>>></span> <span class="n">Making</span> <span class="n">HTTP</span> <span class="n">POST</span> <span class="n">request</span> <span class="n">to</span> <span class="n">http</span><span class="p">:</span><span class="o">//</span><span class="n">reviewboard</span><span class="p">.</span><span class="n">xxx</span><span class="p">.</span><span class="n">domain</span><span class="o">/</span><span class="n">api</span><span class="o">/</span><span class="n">review</span><span class="o">-</span><span class="n">requests</span><span class="o">/</span>
<span class="o">>>></span> <span class="n">Making</span> <span class="n">HTTP</span> <span class="n">GET</span> <span class="n">request</span> <span class="n">to</span> <span class="n">http</span><span class="p">:</span><span class="o">//</span><span class="n">reviewboard</span><span class="p">.</span><span class="n">xxx</span><span class="p">.</span><span class="n">domain</span><span class="o">/</span><span class="n">api</span><span class="o">/</span><span class="n">review</span><span class="o">-</span><span class="n">requests</span><span class="o">/</span><span class="mi">163145</span><span class="o">/</span><span class="n">diffs</span><span class="o">/</span><span class="err">?</span><span class="n">only</span><span class="o">-</span><span class="n">fields</span><span class="o">=</span>
<span class="o">>>></span> <span class="n">Making</span> <span class="n">HTTP</span> <span class="n">POST</span> <span class="n">request</span> <span class="n">to</span> <span class="n">http</span><span class="p">:</span><span class="o">//</span><span class="n">reviewboard</span><span class="p">.</span><span class="n">xxx</span><span class="p">.</span><span class="n">domain</span><span class="o">/</span><span class="n">api</span><span class="o">/</span><span class="n">review</span><span class="o">-</span><span class="n">requests</span><span class="o">/</span><span class="mi">163145</span><span class="o">/</span><span class="n">diffs</span><span class="o">/</span>
<span class="o">>>></span> <span class="n">Making</span> <span class="n">HTTP</span> <span class="n">GET</span> <span class="n">request</span> <span class="n">to</span> <span class="n">http</span><span class="p">:</span><span class="o">//</span><span class="n">reviewboard</span><span class="p">.</span><span class="n">xxx</span><span class="p">.</span><span class="n">domain</span><span class="o">/</span><span class="n">api</span><span class="o">/</span><span class="n">review</span><span class="o">-</span><span class="n">requests</span><span class="o">/</span><span class="mi">163145</span><span class="o">/</span><span class="n">draft</span><span class="o">/</span><span class="err">?</span><span class="n">only</span><span class="o">-</span><span class="n">fields</span><span class="o">=</span><span class="n">commit_id</span>
<span class="o">>>></span> <span class="n">Making</span> <span class="n">HTTP</span> <span class="n">PUT</span> <span class="n">request</span> <span class="n">to</span> <span class="n">http</span><span class="p">:</span><span class="o">//</span><span class="n">reviewboard</span><span class="p">.</span><span class="n">xxx</span><span class="p">.</span><span class="n">domain</span><span class="o">/</span><span class="n">api</span><span class="o">/</span><span class="n">review</span><span class="o">-</span><span class="n">requests</span><span class="o">/</span><span class="mi">163145</span><span class="o">/</span><span class="n">draft</span><span class="o">/</span>
<span class="n">Review</span> <span class="n">request</span> <span class="c1">#163145 posted.</span></code></pre></figure>
<blockquote>
<p>第二:命令行输入summary&description不支持直接空格,只能一次输入一个字符,python在识别command中参数时,是通过空格来判断是第几个参数的</p>
</blockquote>
<p>在考虑加密的形式来写入空格,换行符。但是也许要设计到脚本,那就脱离了简单提交review的初衷了。后期可以对其进行优化。</p>
<p>如果愿意无视以上问题,现在的rbtools已经值得一用了~~</p>
<h2 id="碰到的问题">碰到的问题</h2>
<p>在运行ALIASES中pt命令的途中遇到<code class="language-plaintext highlighter-rouge">cannot create process</code>的问题,rbtools是基于python的东西,这个主要是python系统的问题</p>
<p>我们在安装rbtools的时候实际上是会自带一套python环境</p>
<p>我们需要做的是把python对应到我们安装的rbtools的python环境中</p>
<p>具体变动在安装的rbtools目录下:</p>
<blockquote>
<p>C:\Program Files (x86)\RBTools\Python27\Scripts\rbt-script.py</p>
</blockquote>
<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="c1">#!C:\Program Files (x86)\RBTools\Python27\python.exe
# EASY-INSTALL-ENTRY-SCRIPT: 'RBTools==0.7.10','console_scripts','rbt'
</span><span class="n">__requires__</span> <span class="o">=</span> <span class="s">'RBTools==0.7.10'</span>
<span class="kn">import</span> <span class="nn">re</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">from</span> <span class="nn">pkg_resources</span> <span class="kn">import</span> <span class="n">load_entry_point</span>
<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
<span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="n">sub</span><span class="p">(</span><span class="s">r'(-script\.pyw?|\.exe)?$'</span><span class="p">,</span> <span class="s">''</span><span class="p">,</span> <span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
<span class="n">sys</span><span class="p">.</span><span class="nb">exit</span><span class="p">(</span>
<span class="n">load_entry_point</span><span class="p">(</span><span class="s">'RBTools==0.7.10'</span><span class="p">,</span> <span class="s">'console_scripts'</span><span class="p">,</span> <span class="s">'rbt'</span><span class="p">)()</span>
<span class="p">)</span></code></pre></figure>
<p>把python.exe的注释</p>
<blockquote>
<p>#!C:\src\rbtools\build\windows-pkg\build\Python27\python.exe</p>
</blockquote>
<p>变更为代码中python.exe实际对应的位置即可,以下为我的实际位置</p>
<blockquote>
<p>#!C:\Program Files (x86)\RBTools\Python27\python.exe</p>
</blockquote>Danniel背景IOS中唤起native app2017-02-16T00:00:00+00:002017-02-16T00:00:00+00:00http://daner1990.github.io/web/2017/02/16/IOSuniversalLink<p>##背景</p>
<p>前段时间遇到一个小需求:要求在分享出来的h5页面中,有一个立即打开的按钮,如果本地安装了我们的app,那么点击就直接唤起本地app,如果没有安装,则跳转到下载。</p>
<p>因为从来没有做过这个需求,因此这注定是一个苦逼的调研过程。</p>
<p>我们最开始就面临2个问题:一是如何唤起本地app,二是如何判断浏览器是否安装了对应app。</p>
<p>##如何唤起本地app</p>
<p>首先,想要实现这个需求,肯定是必须要客户端同学的配合才行,因此我们不用知道所有的实现细节,我们从前端角度思考看这个问题,需要知道的一点是,ios与Android都支持一种叫做schema协议的链接。比如网易新闻客户端的协议为</p>
<figure class="highlight"><pre><code class="language-html" data-lang="html">newsapp://xxxxx</code></pre></figure>
<p>当然,这个协议不需要我们前端去实现,我们只需要将协议放在a标签的href属性里,或者使用location.href与iframe来实现激活这个链接。而location.href与iframe是解决这个需求的关键。</p>
<p>在ios中,还支持通过<code class="language-plaintext highlighter-rouge">smart app banner</code>来唤起app,即通过一个meta标签,在标签里带上app的信息,和打开后的行为,代码形如</p>
<figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="nt"><meta</span> <span class="na">name=</span><span class="s">"apple-itunes-app"</span> <span class="na">content=</span><span class="s">"app-id=1023600494, app-argument=tigerbrokersusstock://com.tigerbrokers.usstock/post?postId=7125"</span> <span class="nt">/></span></code></pre></figure>
<p>我们还需要知道的一点是,微信里屏蔽了schema协议。除非你是微信的合作伙伴之类的,他们专门给你配置进白名单。否则我们就没办法通过这个协议在微信中直接唤起app。</p>
<p>因此我们会判断页面场景是否在微信中,如果在微信中,则会提示用户在浏览器中打开。</p>
<p>如何判断本地是否安装了app</p>
<p>很无奈的是,在浏览器中无法明确的判断本地是否安装了app。因此我们必须采取一些取巧的思路来解决这个问题。</p>
<p>很容易能够想到,采用设置一个延迟定时器setTimeout的方式,第一时间尝试唤起app,如果200ms没有唤起成功,则默认本地没有安装app,200ms以后,将会触发下载行为。</p>
<p>结合这个思路,我们来全局考虑一下这个需求应该采用什么样的方案来实现它。</p>
<p>使用location.href的同学可能会面临一个担忧,在有的浏览器中,当我们尝试激活schema link的时候,若本地没有安装app,则会跳转到一个浏览器默认的错误页面去了。因此大多数人采用的解决方案都是使用iframe</p>
<blockquote>
<p>测试了很多浏览器,没有发现过这种情况</p>
</blockquote>
<p>后来观察了网易新闻,今日头条,YY等的实现方案,发现大家都采用的是iframe来实现。好吧,面对这种情况,只能屈服。</p>
<p>整理一下目前的思路,得到下面的解决方案</p>
<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kd">var</span> <span class="nx">url</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">open</span><span class="p">:</span> <span class="dl">'</span><span class="s1">app://xxxxx</span><span class="dl">'</span><span class="p">,</span>
<span class="na">down</span><span class="p">:</span> <span class="dl">'</span><span class="s1">xxxxxxxx</span><span class="dl">'</span>
<span class="p">};</span>
<span class="kd">var</span> <span class="nx">iframe</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">iframe</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">body</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">;</span>
<span class="nx">iframe</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">cssText</span><span class="o">=</span><span class="dl">'</span><span class="s1">display:none;width=0;height=0</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">var</span> <span class="nx">timer</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="c1">// 立即打开的按钮</span>
<span class="kd">var</span> <span class="nx">openapp</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">openapp</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">openapp</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">click</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="k">if</span><span class="p">(</span><span class="sr">/MicroMessenger/gi</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nb">navigator</span><span class="p">.</span><span class="nx">userAgent</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// 引导用户在浏览器中打开</span>
<span class="p">})</span> <span class="k">else</span><span class="p">{</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">iframe</span><span class="p">);</span>
<span class="nx">iframe</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">url</span><span class="p">.</span><span class="nx">open</span><span class="p">;</span>
<span class="nx">timer</span> <span class="o">=</span> <span class="nx">setTimeout</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">wondow</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span> <span class="o">=</span> <span class="nx">url</span><span class="p">.</span><span class="nx">down</span><span class="p">;</span>
<span class="p">},</span> <span class="mi">500</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">},</span> <span class="kc">false</span><span class="p">)</span></code></pre></figure>
<p>想法很美好,现实很残酷。一测试,就发现简单的这样实现有许多的问题。</p>
<p>第一个问题在于,当页面成功唤起app之后,我们再切换回来浏览器,发现跳转到了下载页面。</p>
<p>为了解决这个问题,发现各个公司都进行了不同方式的尝试。</p>
<p>也是历经的很多折磨,发现了几个比较有用的事件。</p>
<p><code class="language-plaintext highlighter-rouge">pageshow</code>: 页面显示时触发,在load事件之后触发。需要将该事件绑定到window上才会触发</p>
<p><code class="language-plaintext highlighter-rouge">pagehide</code>: 页面隐藏时触发</p>
<p><code class="language-plaintext highlighter-rouge">visibilitychange</code>: 页面隐藏没有在当前显示时触发,比如切换tab,也会触发该事件</p>
<p><code class="language-plaintext highlighter-rouge">document.hidden</code> 当页面隐藏时,该值为true,显示时为false</p>
<p>由于各个浏览器的支持情况不同,我们需要将这些事件都给绑定上,即使这样,也不一定能够保证所有的浏览器都能够解决掉这个小问题,实在没办法的事情也就只能这样了。</p>
<p>扩充一下上面的方案,当本地app被唤起,则页面会隐藏掉,就会触发pagehide与visibilitychange事件</p>
<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="nx">$</span><span class="p">(</span><span class="nb">document</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">visibilitychange webkitvisibilitychange</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">tag</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">hidden</span> <span class="o">||</span> <span class="nb">document</span><span class="p">.</span><span class="nx">webkitHidden</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">tag</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">clearTimeout</span><span class="p">(</span><span class="nx">timer</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="nx">$</span><span class="p">(</span><span class="nb">window</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">pagehide</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">clearTimeout</span><span class="p">(</span><span class="nx">timer</span><span class="p">);</span>
<span class="p">})</span></code></pre></figure>
<p>而另外一个问题就是IOS9+下面的问题了。ios9的Safari,根本不支持通过iframe跳转到其他页面去。也就是说,在safari下,我的整体方案被全盘否决!</p>
<p>于是我就只能尝试使用location.href的方式,这个方式能够唤起app,但是有一个坑爹的问题,使用schema协议唤起app会有弹窗而不会直接跳转去app!甚至当本地没有app时,会被判断为链接无效,然后还有一个弹窗。</p>
<p>这个弹窗会造成什么问题呢?如果用户不点确认按钮,根据上面的逻辑,这个时候就会发现页面会自动跳转到下载去了。而且无效的弹窗提示在用户体验上是无法容忍的。</p>
<p>好吧,继续扒别人的代码,看看别人是如何实现的。所以我又去观摩了其他公司的实现结果,发现网易新闻,今日头条都可以在ios直接从微信中唤起app。真是神奇了,可是今日头条在Android版微信上也没办法直接唤起的,他们在Android上都是直接到腾讯应用宝的下载里去。所以按道理来说这不是添加了白名单。</p>
<p>为了找到这个问题的解决方案,我在网易新闻的页面中扒出了他们的代码,并整理如下,添加了部分注释</p>
<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="nb">window</span><span class="p">.</span><span class="nx">NRUM</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">NRUM</span> <span class="o">||</span> <span class="p">{};</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">NRUM</span><span class="p">.</span><span class="nx">config</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">key</span><span class="p">:</span><span class="dl">'</span><span class="s1">27e86c0843344caca7ba9ea652d7948d</span><span class="dl">'</span><span class="p">,</span>
<span class="na">clientStart</span><span class="p">:</span> <span class="o">+</span><span class="k">new</span> <span class="nb">Date</span><span class="p">()</span>
<span class="p">};</span>
<span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">n</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementsByTagName</span><span class="p">(</span><span class="dl">'</span><span class="s1">script</span><span class="dl">'</span><span class="p">)[</span><span class="mi">0</span><span class="p">],</span>
<span class="nx">s</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">script</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">s</span><span class="p">.</span><span class="nx">type</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">text/javascript</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">s</span><span class="p">.</span><span class="k">async</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
<span class="nx">s</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">//nos.netease.com/apmsdk/napm-web-min-1.1.3.js</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">n</span><span class="p">.</span><span class="nx">parentNode</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">s</span><span class="p">,</span> <span class="nx">n</span><span class="p">);</span>
<span class="p">})();</span>
<span class="p">;</span>
<span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nb">window</span><span class="p">,</span><span class="nx">doc</span><span class="p">){</span>
<span class="c1">// http://apm.netease.com/manual?api=web</span>
<span class="nx">NRUM</span><span class="p">.</span><span class="nx">mark</span> <span class="o">&&</span> <span class="nx">NRUM</span><span class="p">.</span><span class="nx">mark</span><span class="p">(</span><span class="dl">'</span><span class="s1">pageload</span><span class="dl">'</span><span class="p">,</span> <span class="kc">true</span><span class="p">)</span>
<span class="kd">var</span> <span class="nx">list</span> <span class="o">=</span> <span class="p">[]</span>
<span class="kd">var</span> <span class="nx">config</span> <span class="o">=</span> <span class="kc">null</span>
<span class="c1">// jsonp</span>
<span class="kd">function</span> <span class="nx">jsonp</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">,</span> <span class="nx">c</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">d</span><span class="p">;</span>
<span class="nx">d</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">script</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">d</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">a</span><span class="p">;</span>
<span class="nx">c</span> <span class="o">&&</span> <span class="p">(</span><span class="nx">d</span><span class="p">.</span><span class="nx">charset</span> <span class="o">=</span> <span class="nx">c</span><span class="p">);</span>
<span class="nx">d</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">onerror</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">parentNode</span><span class="p">.</span><span class="nx">removeChild</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>
<span class="nx">b</span> <span class="o">&&</span> <span class="nx">b</span><span class="p">(</span><span class="o">!</span><span class="mi">0</span><span class="p">);</span>
<span class="p">};</span>
<span class="nx">d</span><span class="p">.</span><span class="nx">onerror</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">onerror</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">parentNode</span><span class="p">.</span><span class="nx">removeChild</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>
<span class="nx">b</span> <span class="o">&&</span> <span class="nx">b</span><span class="p">(</span><span class="o">!</span><span class="mi">1</span><span class="p">);</span>
<span class="p">};</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">head</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">d</span><span class="p">);</span>
<span class="p">};</span>
<span class="kd">function</span> <span class="nx">localParam</span><span class="p">(</span><span class="nx">search</span><span class="p">,</span><span class="nx">hash</span><span class="p">){</span>
<span class="nx">search</span> <span class="o">=</span> <span class="nx">search</span> <span class="o">||</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">search</span><span class="p">;</span>
<span class="nx">hash</span> <span class="o">=</span> <span class="nx">hash</span> <span class="o">||</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">hash</span><span class="p">;</span>
<span class="kd">var</span> <span class="nx">fn</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">str</span><span class="p">,</span><span class="nx">reg</span><span class="p">){</span>
<span class="k">if</span><span class="p">(</span><span class="nx">str</span><span class="p">){</span>
<span class="kd">var</span> <span class="nx">data</span> <span class="o">=</span> <span class="p">{};</span>
<span class="nx">str</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="nx">reg</span><span class="p">,</span><span class="kd">function</span><span class="p">(</span> <span class="nx">$0</span><span class="p">,</span> <span class="nx">$1</span><span class="p">,</span> <span class="nx">$2</span><span class="p">,</span> <span class="nx">$3</span> <span class="p">){</span>
<span class="nx">data</span><span class="p">[</span> <span class="nx">$1</span> <span class="p">]</span> <span class="o">=</span> <span class="nx">$3</span><span class="p">;</span>
<span class="p">});</span>
<span class="k">return</span> <span class="nx">data</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">return</span> <span class="p">{</span><span class="na">search</span><span class="p">:</span> <span class="nx">fn</span><span class="p">(</span><span class="nx">search</span><span class="p">,</span><span class="k">new</span> <span class="nb">RegExp</span><span class="p">(</span> <span class="dl">"</span><span class="s2">([^?=&]+)(=([^&]*))?</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">g</span><span class="dl">"</span> <span class="p">))</span><span class="o">||</span><span class="p">{},</span><span class="na">hash</span><span class="p">:</span> <span class="nx">fn</span><span class="p">(</span><span class="nx">hash</span><span class="p">,</span><span class="k">new</span> <span class="nb">RegExp</span><span class="p">(</span> <span class="dl">"</span><span class="s2">([^#=&]+)(=([^&]*))?</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">g</span><span class="dl">"</span> <span class="p">))</span><span class="o">||</span><span class="p">{}};</span>
<span class="p">}</span>
<span class="nx">jsonp</span><span class="p">(</span><span class="dl">'</span><span class="s1">http://active.163.com/service/form/v1/5847/view/1047.jsonp</span><span class="dl">'</span><span class="p">)</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">search</span> <span class="o">=</span> <span class="nx">localParam</span><span class="p">().</span><span class="nx">search</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">_callback</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="p">{</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">_callback</span> <span class="o">=</span> <span class="kc">null</span>
<span class="nx">list</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">list</span>
<span class="k">if</span><span class="p">(</span><span class="nx">search</span><span class="p">.</span><span class="nx">s</span> <span class="o">&&</span> <span class="o">!!</span><span class="nx">search</span><span class="p">.</span><span class="nx">s</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="sr">/^wap/i</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">config</span> <span class="o">=</span> <span class="nx">list</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">item</span><span class="p">){</span>
<span class="k">return</span> <span class="nx">item</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">wap</span><span class="dl">'</span>
<span class="p">})[</span><span class="mi">0</span><span class="p">]</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="nx">config</span> <span class="o">=</span> <span class="nx">list</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">item</span><span class="p">){</span>
<span class="k">return</span> <span class="nx">item</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="nx">search</span><span class="p">.</span><span class="nx">s</span>
<span class="p">})[</span><span class="mi">0</span><span class="p">]</span>
<span class="p">}</span>
<span class="kd">var</span> <span class="nx">isAndroid</span> <span class="o">=</span> <span class="o">!!</span><span class="nb">navigator</span><span class="p">.</span><span class="nx">userAgent</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="sr">/android/ig</span><span class="p">),</span>
<span class="nx">isIos</span> <span class="o">=</span> <span class="o">!!</span><span class="nb">navigator</span><span class="p">.</span><span class="nx">userAgent</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="sr">/iphone|ipod/ig</span><span class="p">),</span>
<span class="nx">isIpad</span> <span class="o">=</span> <span class="o">!!</span><span class="nb">navigator</span><span class="p">.</span><span class="nx">userAgent</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="sr">/ipad/ig</span><span class="p">),</span>
<span class="nx">isIos9</span> <span class="o">=</span> <span class="o">!!</span><span class="nb">navigator</span><span class="p">.</span><span class="nx">userAgent</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="sr">/OS 9/ig</span><span class="p">),</span>
<span class="nx">isYx</span> <span class="o">=</span> <span class="o">!!</span><span class="nb">navigator</span><span class="p">.</span><span class="nx">userAgent</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="sr">/MailMaster_Android/i</span><span class="p">),</span>
<span class="nx">isNewsapp</span> <span class="o">=</span> <span class="o">!!</span><span class="nb">navigator</span><span class="p">.</span><span class="nx">userAgent</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="sr">/newsapp/i</span><span class="p">),</span>
<span class="nx">isWeixin</span> <span class="o">=</span> <span class="p">(</span><span class="sr">/MicroMessenger/ig</span><span class="p">).</span><span class="nx">test</span><span class="p">(</span><span class="nb">navigator</span><span class="p">.</span><span class="nx">userAgent</span><span class="p">),</span>
<span class="nx">isYixin</span> <span class="o">=</span> <span class="p">(</span><span class="sr">/yixin/ig</span><span class="p">).</span><span class="nx">test</span><span class="p">(</span><span class="nb">navigator</span><span class="p">.</span><span class="nx">userAgent</span><span class="p">),</span>
<span class="nx">isQQ</span> <span class="o">=</span> <span class="p">(</span><span class="sr">/qq/ig</span><span class="p">).</span><span class="nx">test</span><span class="p">(</span><span class="nb">navigator</span><span class="p">.</span><span class="nx">userAgent</span><span class="p">),</span>
<span class="nx">params</span> <span class="o">=</span> <span class="nx">localParam</span><span class="p">().</span><span class="nx">search</span><span class="p">,</span>
<span class="nx">url</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">newsapp://</span><span class="dl">'</span><span class="p">,</span>
<span class="nx">iframe</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">iframe</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">isIDevicePhone</span> <span class="o">=</span> <span class="p">(</span><span class="sr">/iphone|ipod/gi</span><span class="p">).</span><span class="nx">test</span><span class="p">(</span><span class="nb">navigator</span><span class="p">.</span><span class="nx">platform</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">isIDeviceIpad</span> <span class="o">=</span> <span class="o">!</span><span class="nx">isIDevicePhone</span> <span class="o">&&</span> <span class="p">(</span><span class="sr">/ipad/gi</span><span class="p">).</span><span class="nx">test</span><span class="p">(</span><span class="nb">navigator</span><span class="p">.</span><span class="nx">platform</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">isIDevice</span> <span class="o">=</span> <span class="nx">isIDevicePhone</span> <span class="o">||</span> <span class="nx">isIDeviceIpad</span><span class="p">;</span>
<span class="kd">var</span> <span class="nx">isandroid2_x</span> <span class="o">=</span> <span class="o">!</span><span class="nx">isIDevice</span> <span class="o">&&</span> <span class="p">(</span><span class="sr">/android</span><span class="se">\s?</span><span class="sr">2</span><span class="se">\.</span><span class="sr">/gi</span><span class="p">).</span><span class="nx">test</span><span class="p">(</span><span class="nb">navigator</span><span class="p">.</span><span class="nx">userAgent</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">isIEMobile</span> <span class="o">=</span> <span class="o">!</span><span class="nx">isIDevice</span> <span class="o">&&</span> <span class="o">!</span><span class="nx">isAndroid</span> <span class="o">&&</span> <span class="p">(</span><span class="sr">/MSIE/gi</span><span class="p">).</span><span class="nx">test</span><span class="p">(</span><span class="nb">navigator</span><span class="p">.</span><span class="nx">userAgent</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">android_url</span> <span class="o">=</span> <span class="p">(</span><span class="o">!</span><span class="nx">isandroid2_x</span><span class="p">)</span> <span class="p">?</span> <span class="dl">"</span><span class="s2">http://3g.163.com/links/4304</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">http://3g.163.com/links/6264</span><span class="dl">"</span><span class="p">;</span>
<span class="kd">var</span> <span class="nx">ios_url</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">http://3g.163.com/links/3615</span><span class="dl">"</span><span class="p">;</span>
<span class="kd">var</span> <span class="nx">wphone_url</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">http://3g.163.com/links/3614</span><span class="dl">"</span><span class="p">;</span>
<span class="kd">var</span> <span class="nx">channel</span> <span class="o">=</span> <span class="nx">params</span><span class="p">.</span><span class="nx">s</span> <span class="o">||</span> <span class="dl">'</span><span class="s1">newsapp</span><span class="dl">'</span>
<span class="c1">// 判断在不同环境下app的url</span>
<span class="k">if</span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">docid</span><span class="p">){</span>
<span class="k">if</span><span class="p">(</span><span class="nx">params</span><span class="p">[</span><span class="dl">'</span><span class="s1">boardid</span><span class="dl">'</span><span class="p">]</span> <span class="o">&&</span> <span class="nx">params</span><span class="p">[</span><span class="dl">'</span><span class="s1">title</span><span class="dl">'</span><span class="p">]){</span>
<span class="nx">url</span> <span class="o">=</span> <span class="nx">url</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">comment/</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">params</span><span class="p">.</span><span class="nx">boardid</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">/</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">params</span><span class="p">.</span><span class="nx">docid</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">/</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">params</span><span class="p">.</span><span class="nx">title</span>
<span class="p">}</span><span class="k">else</span><span class="p">{</span>
<span class="nx">url</span> <span class="o">=</span> <span class="nx">url</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">doc/</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">params</span><span class="p">.</span><span class="nx">docid</span>
<span class="p">}</span>
<span class="p">}</span><span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">sid</span><span class="p">){</span>
<span class="nx">url</span> <span class="o">=</span> <span class="nx">url</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">topic/</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">params</span><span class="p">.</span><span class="nx">sid</span>
<span class="p">}</span><span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">pid</span><span class="p">){</span>
<span class="kd">var</span> <span class="nx">pid</span> <span class="o">=</span> <span class="nx">params</span><span class="p">.</span><span class="nx">pid</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">_</span><span class="dl">'</span><span class="p">)</span>
<span class="nx">url</span> <span class="o">=</span> <span class="nx">url</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">photo/</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">pid</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">/</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">pid</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
<span class="p">}</span><span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">vid</span><span class="p">){</span>
<span class="nx">url</span> <span class="o">=</span> <span class="nx">url</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">video/</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">params</span><span class="p">.</span><span class="nx">vid</span>
<span class="p">}</span><span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">liveRoomid</span><span class="p">){</span>
<span class="nx">url</span> <span class="o">=</span> <span class="nx">url</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">live/</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">params</span><span class="p">.</span><span class="nx">liveRoomid</span>
<span class="p">}</span><span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">url</span><span class="p">){</span>
<span class="nx">url</span> <span class="o">=</span> <span class="nx">url</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">web/</span><span class="dl">'</span> <span class="o">+</span> <span class="nb">decodeURIComponent</span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">url</span><span class="p">)</span>
<span class="p">}</span><span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">expertid</span><span class="p">){</span>
<span class="nx">url</span> <span class="o">=</span> <span class="nx">url</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">expert/</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">params</span><span class="p">.</span><span class="nx">expertid</span>
<span class="p">}</span><span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">subjectid</span><span class="p">){</span>
<span class="nx">url</span> <span class="o">=</span> <span class="nx">url</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">subject/</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">params</span><span class="p">.</span><span class="nx">subjectid</span>
<span class="p">}</span><span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">readerid</span><span class="p">){</span>
<span class="nx">url</span> <span class="o">=</span> <span class="nx">url</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">reader/</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">params</span><span class="p">.</span><span class="nx">readerid</span>
<span class="p">}</span><span class="k">else</span><span class="p">{</span>
<span class="nx">url</span> <span class="o">+=</span> <span class="dl">'</span><span class="s1">startup</span><span class="dl">'</span>
<span class="p">}</span>
<span class="k">if</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">?</span><span class="dl">'</span><span class="p">)</span> <span class="o">>=</span> <span class="mi">0</span><span class="p">){</span>
<span class="nx">url</span> <span class="o">+=</span> <span class="dl">'</span><span class="s1">&s=</span><span class="dl">'</span> <span class="o">+</span> <span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">s</span> <span class="o">||</span> <span class="dl">'</span><span class="s1">sps</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span><span class="k">else</span><span class="p">{</span>
<span class="nx">url</span> <span class="o">+=</span> <span class="dl">'</span><span class="s1">?s=</span><span class="dl">'</span> <span class="o">+</span> <span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">s</span> <span class="o">||</span> <span class="dl">'</span><span class="s1">sps</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span>
<span class="c1">// ios && 易信 用iframe 打开</span>
<span class="k">if</span><span class="p">((</span><span class="nx">isIos</span><span class="o">||</span><span class="nx">isIpad</span><span class="p">)</span> <span class="o">&&</span> <span class="nb">navigator</span><span class="p">.</span><span class="nx">userAgent</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="sr">/yixin/i</span><span class="p">))</span> <span class="p">{</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">iframe</span><span class="dl">'</span><span class="p">).</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">url</span><span class="p">;</span>
<span class="p">}</span>
<span class="kd">var</span> <span class="nx">height</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nx">clientHeight</span><span class="p">;</span>
<span class="c1">// 通常情况下先尝试使用iframe打开</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">iframe</span><span class="dl">'</span><span class="p">).</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">url</span><span class="p">;</span>
<span class="c1">// 移动端浏览器中,将下载页面显示出来</span>
<span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="nx">isWeixin</span> <span class="o">&&</span> <span class="o">!</span><span class="nx">isQQ</span> <span class="o">&&</span> <span class="o">!</span><span class="nx">isYixin</span> <span class="o">&&</span> <span class="o">!</span><span class="nx">isYx</span><span class="p">){</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">.main-body</span><span class="dl">'</span><span class="p">).</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">block</span><span class="dl">'</span>
<span class="k">if</span><span class="p">(</span><span class="nx">isIos9</span><span class="p">){</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">.main-body</span><span class="dl">'</span><span class="p">).</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="dl">'</span><span class="s1">showtip</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span>
<span class="nx">setTimeout</span><span class="p">(</span><span class="kd">function</span><span class="p">(){</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">scrollTop</span> <span class="o">=</span> <span class="mi">0</span>
<span class="p">},</span><span class="mi">200</span><span class="p">)</span>
<span class="p">}</span><span class="k">else</span><span class="p">{</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">guide</span><span class="dl">'</span><span class="p">).</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">block</span><span class="dl">'</span>
<span class="p">}</span>
<span class="c1">// Forward To Redirect Url</span>
<span class="c1">// Add by zhanzhixiang 12/28/2015</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">redirect</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">redirectUrl</span> <span class="o">=</span> <span class="nb">decodeURIComponent</span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">redirect</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span> <span class="k">typeof</span><span class="p">(</span><span class="nx">URL</span><span class="p">)</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">function</span><span class="dl">'</span> <span class="o">&&</span> <span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="nx">redirectUrl</span><span class="p">).</span><span class="nx">hostname</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="dl">"</span><span class="s2">163.com</span><span class="dl">"</span><span class="p">)</span> <span class="o">!==</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span> <span class="o">=</span> <span class="nx">redirectUrl</span><span class="p">;</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">redirectUrl</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="dl">"</span><span class="s2">163.com</span><span class="dl">"</span><span class="p">)</span> <span class="o">!==</span> <span class="o">-</span><span class="mi">1</span><span class="p">){</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span> <span class="o">=</span> <span class="nx">redirectUrl</span><span class="p">;</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="c1">// Forward To Redirect Url End</span>
<span class="k">if</span> <span class="p">((</span><span class="nx">isWeixin</span> <span class="o">||</span> <span class="nx">isQQ</span><span class="p">)</span> <span class="o">&&</span> <span class="nx">isAndroid</span><span class="p">)</span> <span class="p">{</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">http://a.app.qq.com/o/simple.jsp?pkgname=com.netease.newsreader.activity&ckey=CK1331205846719&android_schema=</span><span class="dl">'</span> <span class="o">+</span><span class="err"> </span><span class="nx">url</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="sr">/</span><span class="se">(</span><span class="sr">.*</span><span class="se">)\?</span><span class="sr">/</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span>
<span class="p">}</span>
<span class="k">if</span><span class="p">(</span><span class="nx">isIos</span><span class="o">||</span><span class="nx">isIpad</span><span class="p">){</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">guide</span><span class="dl">"</span><span class="p">).</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="dl">'</span><span class="s1">iosguideopen</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span><span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">isAndroid</span><span class="p">){</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">guide</span><span class="dl">"</span><span class="p">).</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="dl">'</span><span class="s1">androidguideopen</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span><span class="k">else</span><span class="p">{</span>
<span class="c1">// window.location.href = 'http://www.163.com/newsapp'</span>
<span class="p">}</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">link</span><span class="dl">'</span><span class="p">).</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">click</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span><span class="p">(){</span>
<span class="c1">// 统计</span>
<span class="nx">neteaseTracker</span> <span class="o">&&</span> <span class="nx">neteaseTracker</span><span class="p">(</span><span class="kc">false</span><span class="p">,</span><span class="dl">'</span><span class="s1">http://sps.163.com/func/?func=downloadapp&modelid=</span><span class="dl">'</span><span class="o">+</span><span class="nx">modelid</span><span class="o">+</span><span class="dl">'</span><span class="s1">&spst=</span><span class="dl">'</span><span class="o">+</span><span class="nx">spst</span><span class="o">+</span><span class="dl">'</span><span class="s1">&spsf&spss=</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">channel</span><span class="p">,</span><span class="dl">''</span><span class="p">,</span> <span class="dl">'</span><span class="s1">sps</span><span class="dl">'</span> <span class="p">)</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">config</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">android_url</span> <span class="o">=</span> <span class="nx">config</span><span class="p">.</span><span class="nx">android</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">config</span> <span class="o">&&</span> <span class="nx">config</span><span class="p">.</span><span class="nx">iOS</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">ios_url</span> <span class="o">=</span> <span class="nx">config</span><span class="p">.</span><span class="nx">iOS</span>
<span class="p">}</span>
<span class="k">if</span><span class="p">(</span><span class="nx">isWeixin</span> <span class="o">||</span> <span class="nx">isQQ</span><span class="p">){</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="kd">var</span> <span class="nx">msg</span> <span class="o">=</span> <span class="nx">isIDeviceIpad</span> <span class="p">?</span> <span class="dl">"</span><span class="s2">检测到您正在使用iPad, 是否直接前往AppStore下载?</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">检测到您正在使用iPhone, 是否直接前往AppStore下载?</span><span class="dl">"</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">isIDevice</span><span class="p">){</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">location</span> <span class="o">=</span> <span class="nx">ios_url</span><span class="p">;</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span><span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">isAndroid</span><span class="p">){</span>
<span class="c1">// uc浏览器用iframe唤醒</span>
<span class="k">if</span><span class="p">(</span><span class="nb">navigator</span><span class="p">.</span><span class="nx">userAgent</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="sr">/ucbrowser|yixin|MailMaster/i</span><span class="p">)){</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">iframe</span><span class="dl">'</span><span class="p">).</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">url</span><span class="p">;</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span> <span class="o">=</span> <span class="nx">url</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">setTimeout</span><span class="p">(</span><span class="kd">function</span><span class="p">(){</span>
<span class="k">if</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">webkitHidden</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">confirm</span><span class="p">(</span><span class="dl">"</span><span class="s2">检测到您正在使用Android 手机,是否直接下载程序安装包?</span><span class="dl">"</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">neteaseTracker</span> <span class="o">&&</span> <span class="nx">neteaseTracker</span><span class="p">(</span><span class="kc">false</span><span class="p">,</span><span class="dl">'</span><span class="s1">http://sps.163.com/func/?func=downloadapp_pass&modelid=</span><span class="dl">'</span><span class="o">+</span><span class="nx">modelid</span><span class="o">+</span><span class="dl">'</span><span class="s1">&spst=</span><span class="dl">'</span><span class="o">+</span><span class="nx">spst</span><span class="o">+</span><span class="dl">'</span><span class="s1">&spsf&spss=</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">channel</span><span class="p">,</span><span class="dl">''</span><span class="p">,</span> <span class="dl">'</span><span class="s1">sps</span><span class="dl">'</span> <span class="p">)</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span> <span class="o">=</span> <span class="nx">android_url</span><span class="p">;</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">neteaseTracker</span> <span class="o">&&</span> <span class="nx">neteaseTracker</span><span class="p">(</span><span class="kc">false</span><span class="p">,</span><span class="dl">'</span><span class="s1">http://sps.163.com/func/?func=downloadapp_cancel&modelid=</span><span class="dl">'</span><span class="o">+</span><span class="nx">modelid</span><span class="o">+</span><span class="dl">'</span><span class="s1">&spst=</span><span class="dl">'</span><span class="o">+</span><span class="nx">spst</span><span class="o">+</span><span class="dl">'</span><span class="s1">&spsf&spss=</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">channel</span><span class="p">,</span><span class="dl">''</span><span class="p">,</span> <span class="dl">'</span><span class="s1">sps</span><span class="dl">'</span> <span class="p">)</span>
<span class="p">}</span>
<span class="p">},</span><span class="mi">200</span><span class="p">)</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span><span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">isIEMobile</span><span class="p">){</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">location</span> <span class="o">=</span> <span class="nx">wphone_url</span><span class="p">;</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span><span class="k">else</span><span class="p">{</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">open</span><span class="p">(</span><span class="dl">'</span><span class="s1">http://www.163.com/special/00774IQ6/newsapp_download.html</span><span class="dl">'</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">},</span> <span class="kc">false</span><span class="p">)</span>
<span class="nx">setTimeout</span><span class="p">(</span><span class="kd">function</span><span class="p">(){</span>
<span class="k">if</span><span class="p">(</span><span class="nx">isIDevice</span> <span class="o">&&</span> <span class="nx">params</span><span class="p">.</span><span class="nx">notdownload</span> <span class="o">!=</span> <span class="mi">1</span> <span class="o">&&</span> <span class="o">!</span><span class="nx">isNewsapp</span> <span class="o">&&</span> <span class="o">!</span><span class="nx">isIos9</span><span class="p">){</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">link</span><span class="dl">'</span><span class="p">).</span><span class="nx">click</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">},</span> <span class="mi">1000</span><span class="p">)</span>
<span class="p">})(</span><span class="nb">window</span><span class="p">,</span><span class="nb">document</span><span class="p">);</span></code></pre></figure>
<p>虽然有一些外部的引用,和一些搞不懂是干什么用的方法和变量,但是基本逻辑还是能够看明白。好像也没有什么特别的地方。研究了许久,看到了一个叫做apple-app-site-association的jsonp请求很奇特。这是来干嘛用的?</p>
<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="p">{</span>
<span class="dl">"</span><span class="s2">applinks</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
<span class="dl">"</span><span class="s2">apps</span><span class="dl">"</span><span class="p">:</span> <span class="p">[</span> <span class="p">],</span>
<span class="dl">"</span><span class="s2">details</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
<span class="dl">"</span><span class="s2">TEAM-IDENTIFIER.YOUR.BUNDLE.IDENTIFIER</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
<span class="dl">"</span><span class="s2">paths</span><span class="dl">"</span><span class="p">:</span> <span class="p">[</span>
<span class="dl">"</span><span class="s2">*</span><span class="dl">"</span>
<span class="p">]</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>大家可以直接访问这个链接,查看里面的内容</p>
<p>http://active.163.com/service/form/v1/5847/view/1047.jsonp</p>
<p>为了搞清楚这个问题,费尽千辛万苦搜索了很多文章,最终锁定了一个极为关键的名词 <code class="language-plaintext highlighter-rouge">Universal links</code>。</p>
<blockquote>
<p>Apple为iOS 9发布了一个所谓的通用链接的深层链接特性,即Universal links。虽然它并不完美,但是这一发布,让数以千计的应用开发人员突然意识到自己的应用体验被打破。</p>
<p>Universal links,一种能够方便的通过传统的HTTP/HTTPS 链接来启动App,使用相同的网址打开网站和App。</p>
</blockquote>
<p>关于这个问题的提问与universal links的介绍 点击这里查看:</p>
<p>http://stackoverflow.com/questions/31891777/ios-9-safari-iframe-src-with-custom-url-scheme-not-working</p>
<p>ios9推行的一个新的协议!</p>
<p>关于本文的这个问题,国内的论坛有许许多多的文章来解决,但是提到universal links的文章少之又少。他改变了用户体验的关键在于,微信没有办法屏蔽这个协议。因此如果我们的app注册了这个协议,那么我们就能够从微信中直接唤起app。</p>
<p>至于universal links具体如何实现,让ios的同学去搞定吧,这里提供两个参考文章</p>
<p>http://www.cocoachina.com/bbs/read.php?tid-1486368.html</p>
<p>https://blog.branch.io/how-to-setup-universal-links-to-deep-link-on-apple-ios-9</p>
<p>支持了这个协议之后,我们又可以通过iframe来唤起app了,因此基本逻辑就是这样了。最终的调研结果是</p>
<p>没有完美的解决方案</p>
<p>就算是网易新闻,这个按钮在使用过程中也会有一些小bug,无法做到完美的状态。</p>
<p>因为我们面临许多没办法解决的问题,比如无法真正意义上的判断本地是否安装了app,pageshow,pagehide并不是所有的浏览器都支持等。很多其他博客里面,什么计算时间差等方案,我花了很久的时间去研究这些方案,结果是,根!本!没!有!用!</p>
<p>老实说,从微信中跳转到外部浏览器,并不是一个好的解决方案,这样会导致很多用户流失,因此大家都在ios上实现了universal links,而我更加倾向的方案是知乎的解决,他们从设计上避免了在一个按钮上来判断这个逻辑,而采用了两个按钮的方式。</p>
<p>网易新闻的逻辑是,点击打开会跳转到一个下载页面,这个下载页面一加载完成就尝试打开app,如果打开了就直接跑到app里面去了,如果没有就在页面上有一个立即下载的按钮,按钮行只有下载处理。</p>
<p>在后续与ios同学实现universal liks的时候又遇到了一些坑,总结了一些经验,欢迎持续关注我下一篇关于这个话题的讨论。</p>
<p><strong>以上内容转来自:https://gold.xitu.io/post/57b6ba9f128fe10054bb5401</strong></p>
<p>##第二篇</p>
<p>在ios9出来以后,我们发现越来越多的应用能够直接绕过微信的屏蔽,从其内置浏览器中直接唤起app。相比于通过弹窗提示让用户到浏览器中操作的方式,这无疑是极大的提高了用户体验与流量导入。因此,在ios上实现直接从微信中唤起app变得非常必要。</p>
<p>而其中的关键,就在于通用链接·universal links·:一种能够方便通过传统HTTP链接来启动app的方式,可以通过相同的网址打开网站和app。</p>
<p>对于ios开发者来说,可以很轻松在网上找到非常多给自己的app配置universal links的教程文章,这里推荐 http://www.cocoachina.com/ios/20150902/13321.html</p>
<p>这篇文章的主要目的,就是从前端角度来聊一聊·universal links·的运用。</p>
<p>无论是Android还是ios应用,都能够通过一定的方式捕获浏览器正在进行的url跳转。我们知道在页面中通常有如下三种方式能够访问别的链接</p>
<p>通过html中的a标签</p>
<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="o"><</span><span class="nx">a</span> <span class="nx">href</span><span class="o">=</span><span class="dl">"</span><span class="s2">universal links</span><span class="dl">"</span><span class="o">></span><span class="nx">action</span><span class="o"><</span><span class="sr">/a></span></code></pre></figure>
<p>通过js的location方法</p>
<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="nb">window</span><span class="p">.</span><span class="nx">location</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">universal links</span><span class="dl">'</span><span class="p">;</span></code></pre></figure>
<p>通过iframe标签,一般情况下我们会通过js创建一个iframe标签,并通过设置iframe的src属性实现跳转</p>
<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kd">var</span> <span class="nx">iframe</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">iframe</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">iframe</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">cssText</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">width: 0; height: 0</span><span class="dl">'</span><span class="p">;</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">iframe</span><span class="p">);</span>
<span class="nx">iframe</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">universal links</span><span class="dl">'</span><span class="p">;</span></code></pre></figure>
<p>为了能够在js中控制跳转行为,我们基本不会通过a标签的方式,而选择2,3种。不过比较头疼的是,并不是所有手机版本的浏览器都能够毫无顾忌的使用这两种方式,比如在ios8中,据说他们ios开发通过通常使用的方式,无法捕获window.location的跳转,因此我们得采用iframe的方式来实现唤起。而在Android上浏览器的表现就更加杂乱不一,因此如果想要兼顾所有的浏览器,从测试角度来说,这是一个比较大的工作量。</p>
<p>而universal links如果能够实现从微信中直接唤起app,那么微信以外的浏览器的复杂场景我们都不需要考虑了。因此这简直就是一件利国利民的好事,从开发到测试的工作量都大大降低。</p>
<p>读过上一篇文章的同学应该知道,单单从浏览器层面,我们无法精准的判断本地是否安装了app。这给我们实现这一需求造成了非常多的困扰。而从ios9.2开始,<strong>针对universal links,有一个非常重要的改动,通俗来说,就是必须通过访问不同的url链接,我们才能在微信中唤起本地app</strong>。</p>
<p>在上面我们介绍过,universal links能够使用和访问普通网页一样的http链接,唤起我们自己的app。比如我们访问一个页面<code class="language-plaintext highlighter-rouge">http://www.test.com/gold</code>能够在浏览器中打开一个页面,而当我们在手机浏览器中,通过上面的三种方式尝试访问同样的地址<code class="language-plaintext highlighter-rouge">http://www.test.com/gold</code>,只要本地安装了指定的app,就可以打开app中的对应页面。但是9.2的改动之后就不行了,在9.2之后,我们必须使用2个不同的域名,并且这2个域名指向同一个页面,我们才能够在微信中唤起app。</p>
<p>如果我们仅仅只是配置了一个域名,那么当我们在微信中打开这个网页,并且在页面中访问自身时,页面仅仅相当于一次刷新,而app并不会被唤起。而点击在浏览器中打开时,会唤起app。</p>
<p>对于不了解的人来说,这是一个深坑,而当我们了解了其中的细节,那么我们就能够利用这一点,完美的实现有则唤起,无则下载的需求。</p>
<p>假如我们有一个app,demoAPP。我们的ios同事已经配置好了universal links,2个域名分别为<code class="language-plaintext highlighter-rouge">a.com</code>, <code class="language-plaintext highlighter-rouge">b.com</code>。另外我们有一个宣传页面,在浏览器中,<code class="language-plaintext highlighter-rouge">a.com/activity</code>与<code class="language-plaintext highlighter-rouge">b.com/activity</code>都能够访问该宣传页面。</p>
<p>为了规范统一,我们规定将该宣传页面从demoAPP分享出来时,页面地址使用<code class="language-plaintext highlighter-rouge">a.com/activity</code>,而在当我们想要唤起demoAPP时,使用<code class="language-plaintext highlighter-rouge">b.com/activity</code>.</p>
<p>另外,在实践中我发现如下特性,如果本地安装了demoAPP,那么<code class="language-plaintext highlighter-rouge">a.com/activity</code>中唤起app之后,不会发生任何变化。而如果本地没有安装demoAPP,页面会跳转到<code class="language-plaintext highlighter-rouge">b.com/activity</code>,当到了这个页面,我们已经知道无法唤起,因此直接下载即可。</p>
<p>因此根据这个特性,我们有了如下的实践方案</p>
<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kd">var</span> <span class="nx">openURL</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">open</span><span class="p">:</span> <span class="dl">'</span><span class="s1">b.com/activity</span><span class="dl">'</span><span class="p">,</span>
<span class="na">down</span><span class="p">:</span> <span class="dl">'</span><span class="s1">downloadurl</span><span class="dl">'</span>
<span class="p">},</span>
<span class="c1">// 打开app的按钮</span>
<span class="nx">btn</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">.open-app</span><span class="dl">'</span><span class="p">),</span>
<span class="nx">curURL</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="sr">/b.com/ig</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">curURL</span><span class="p">))</span> <span class="p">{</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">location</span> <span class="o">=</span> <span class="nx">openURL</span><span class="p">.</span><span class="nx">down</span><span class="p">;</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">btn</span><span class="p">.</span><span class="nx">onclick</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">location</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">b.com/activity</span><span class="dl">'</span><span class="p">;</span>
<span class="p">})</span></code></pre></figure>
<p>是不是很简单!</p>
<p>当然在具体实现上,还有许多繁琐的细枝末节需要我们一步一步去完善。这里只是提供了一个思路。从整体来说,这个需求并不是一个简单的任务,我们需要后端同学配置2套域名,需要ios同事配合,甚至还需要和产品不停的沟通,因为有一些确实无法实现的东西需要他们理解。</p>
<p>##第三方</p>
<p>老实说,如果自己劳心劳力想要比较完善的实现这么个需求,真的吃力不讨好,需要配合的部门太多,耗时太多,最后的效果还并不是很好,很多用户还对这种弹窗下载提示真的深恶痛绝。因此,通过第三方的解决方案无疑是最好的办法。</p>
<p>这里推荐2个第三方方案,具体的优势,实现与配置方式在他们网站中都有详细的讲解,如果真的有接到这个需求的同学,强烈建议参考。当然,如果有数据隐私这方面的考虑的话,就还是自己老老实实的做兼容吧,反正方案就是这2篇文章说的这些。</p>
<p><code class="language-plaintext highlighter-rouge">磨窗</code> http://www.magicwindow.cn/</p>
<p><code class="language-plaintext highlighter-rouge">deepshare</code> http://deepshare.io/redirect-once/</p>
<p>我知道有的同学会想吐槽,说了这么多,原来还是第三方才是最佳方案,干嘛不直接说得了!那么我只能说,这么想的同学,肯定不知道经验这个东西,在撕b上到底有多重要!!</p>
<p><strong>第二篇文章转自:https://gold.xitu.io/entry/57bd1e6179bc440063b3a029/view</strong></p>Danniel##背景移动端WEB调试:Weinre2015-12-04T00:00:00+00:002015-12-04T00:00:00+00:00http://daner1990.github.io/web/2015/12/04/weinre<h2 id="背景">背景</h2>
<p>当我们在做H5移动端的开发时。我们一般通过PC浏览器如chrome的开发者工具,firefox的firebug来模拟手机环境来调试。模拟虽然可以解决大部分问题。但是不同的终端的内核不径相同,由此会报出很多兼容性问题。</p>
<p>然后我们会怎么做,针对这个问题,不断修改CSS/HTML/Javascript。并通过alert等代码断点方式来检测bug。其麻烦程度可想而知。</p>
<p>如果可以在电脑上远程调试手机页面该多好,伟大的程序员们于是就发明了<code class="language-plaintext highlighter-rouge">weinre</code>。</p>
<h2 id="weinre简介">weinre简介</h2>
<p>Weinre(WebInspector Remote)是一款基于Web Inspector(Webkit)的远程调试工具,借助于网络,可以在PC上直接调试运行在移动设备上的远程页面,中文意思是远程Web检查器,有了Weinre,在PC上可以即时修改目标网页的HTML/CSS/Javascript,调试过程可实时显示移动设备上页面的预览效果,并同步显示设备页面的错误和警告信息,可以查看网络资源的信息,不过weinre不支持断点调试。该项目目前是 Apache Cordova 的一部分。</p>
<p>Weinre也从最初的Java移植到了当前的JavaScript。我们使用的就是Apache后来推出的JavaScript版本weinre。需要在nodejs环境下安装使用,使用npm包管理工具也可以直接下载安装。</p>
<p>原理图:
<img src="/postPics/weinre/1.png" alt="" style="width:600px;margin: 5px auto;" /></p>
<h2 id="安装">安装</h2>
<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
</pre></td><td class="code"><pre>npm -g install weinre //安装weinre
weinre --boundHost [hostname | ip address |-all-] --httpPort [port] //启动weinre
</pre></td></tr></tbody></table></code></pre></figure>
<p>安装之后打开所配置的weinre:</p>
<p><img src="/postPics/weinre/2.png" alt="" style="width:600px;margin: 5px auto;" /></p>
<p>可以配置 .weinre/server.properties 让启动 weinre 变得更方便, 具体方法请参考官网, 配置好后, 以后想启动 weinre, 直接运行weinre命令即可, 无需每次都追加那些参数了。</p>
<p>环境配置好了,接下来就是打通调试环境和目标页面的联系:</p>
<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
</pre></td><td class="code"><pre><span class="nt"><script </span><span class="na">src=</span><span class="s">"http://localhost:8081/target/target-script-min.js#anonymous"</span><span class="nt">></script></span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>我们把以上代码加入我们要调试的页面中。</p>
<p>然后再手机端打开我们的待调试的页面。</p>
<p><img src="/postPics/weinre/3.png" alt="" style="width:320px;margin: 5px auto;" /></p>
<p>然后回到我们的weinre环境中点击测试接口:</p>
<p><img src="/postPics/weinre/4.png" alt="" style="width:400px;margin: 5px auto;" /></p>
<p>我们进入了一个熟悉的界面如下所示,只要在点击targets中你想测试的页面激活一下。就可以通过elments等选项修改css/javascipt/html代码直接远程调试我们的目标页面啦~</p>
<p><img src="/postPics/weinre/5.png" alt="" style="width:600px;margin: 5px auto;" /></p>Danniel背景