<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>CRIMX</title>
  
  
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://blog.crimx.com/"/>
  <updated>2018-11-13T16:58:34.528Z</updated>
  <id>https://blog.crimx.com/</id>
  
  <author>
    <name>StrayBugs</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>V8 中的快属性</title>
    <link href="https://blog.crimx.com/2018/11/25/v8-fast-properties/"/>
    <id>https://blog.crimx.com/2018/11/25/v8-fast-properties/</id>
    <published>2018-11-24T16:00:00.000Z</published>
    <updated>2018-11-13T16:58:34.528Z</updated>
    
    <content type="html"><![CDATA[<p>译自：<a href="https://v8.dev/blog/fast-properties/" target="_blank">Fast properties in V8</a>（2017-08-30）</p><p>本文我们将解释 V8 内部如何处理 JavaScript 属性（properties）。从 JavaScript 的角度来看属性只有少数的区别。JavaScript 对象通常跟字典（dictionaris）差不多，以字符串为键，任意对象为值。尽管规范确实对<a href="https://tc39.github.io/ecma262/#sec-ordinaryownpropertykeys" target="_blank">迭代过程中</a>的整数索引属性与其他属性作了区分，但除此以外，不同属性表现基本是一致的，不管是不是整数索引。</p><p>然而，在底层 V8 的确依赖了一些不同的属性表示方式，这是出于性能和内存考虑的。本文我们将解释 V8 如何在提供快属性访问的同时支持动态添加属性。理解属性如何工作对解释 V8 <a href="http://mrale.ph/blog/2012/06/03/explaining-js-vms-in-js-inline-caches.html" target="_blank">内联缓存</a>之类的优化原理是必不可少的。</p><p>本文解释了整数索引与命名属性的不同处理方式。随后我们展示了 V8 如何在添加命名属性同时维护隐藏类（HiddenClasses），以提供快速的方式识别一个对象的形状（shape）。接着我们继续深入了解命名属性是如何根据使用方式来进行优化，以支持快速访问和快速修改。最后部分我们详细介绍了 V8 如何处理整数索引属性或数组索引。</p><h2 id="命名属性与元素"><a href="#命名属性与元素" class="headerlink" title="命名属性与元素"></a>命名属性与元素</h2><p>首先我们来分析一个简单的对象 <code>{a: &quot;foo&quot;, b: &quot;bar&quot;}</code>。这个对象有两个命名属性，<code>&quot;a&quot;</code> 和 <code>&quot;b&quot;</code>，没有任何整数索引作属性名。数组索引属性（array-indexed properties），常叫元素（elements），在数组中非常重要。比如数组 <code>[&quot;foo&quot;, &quot;bar&quot;]</code>有两个数组索引属性：0 与值 &quot;foo&quot; ，和 1 与值 &quot;bar&quot;。 这是 V8 如何处理属性的第一个主要区别。</p><p>下图展示了一个基本 JavaScript 对象在内存中是什么样的。</p><p></p><figure><img src="https://v8.dev/_img/fast-properties/jsobject.png" alt=""></figure><p></p><p>元素和属性被保存在两个独立的数据结构中。两者的使用方式通常不一样，故这能让添加和访问属性或元素更加高效。</p><p>元素主要被用在各种 <a href="https://tc39.github.io/ecma262/#sec-properties-of-the-array-prototype-object" target="_blank"><code>Array.prototype</code> 方法</a>中，如 <code>pop</code> 或者 <code>slice</code>。考虑到这些函数都是连续地访问属性，V8 内部还将它们表示为简单的数组，大多数情况下都是如此。本文后面还会解释到有时我们如何切换到基于稀疏字典的表示来节省内存。</p><p>命名属性也是通过类似的方式存在另外的数组中。但是，跟元素不同，我们不能简单地通过键来推断出它们在属性数组中的位置，我们需要一些额外的元数据。在 V8 中每个 JavaScript 对象都关联了一个隐藏类（HiddenClasses）。隐藏类保存了对象的形状信息，以及从属性名称到属性索引的映射。复杂的使用情况下我们有时会用字典来存属性而不是简单的数组。我们会在专门的部分再作详细地解释。</p><p><strong>本节要点:</strong></p><ul><li>数组索引属性被保存在独立的元素存储中。</li><li>命名属性被保存在属性存储中。</li><li>元素和属性可以是数组或者字典。</li><li>每个 JavaScript 对象都有相应的隐藏类来记录该对象的形状信息。</li></ul><h2 id="隐藏类和描述符数组"><a href="#隐藏类和描述符数组" class="headerlink" title="隐藏类和描述符数组"></a>隐藏类和描述符数组</h2><p>在解释了元素和命名属性的一般区别之后，我们需要看看隐藏类在 V8 中是如何工作的。隐藏类保存了与对象相关的元信息，包括对象上的属性数和对象原型的引用。隐藏类在概念上与典型面向对象编程语言中的类相似。但是，在基于原型的语言（如 JavaScript）中，通常不可能预先知道类。因此，在这种情况下，V8 的隐藏类是动态创建的，并随着对象的变化而动态更新。隐藏类充当了对象形状的标识符，因此是 V8 优化编译器和内联缓存非常重要的组成部分。比如优化编译器可以直接内联属性访问，如果它可以通过隐藏类来确保对象结构是兼容的。</p><p>我们来看看隐藏类的重要部分。</p><p></p><figure><img src="https://v8.dev/_img/fast-properties/hidden-class.png" alt=""></figure><p></p><p>在 V8 中，JavaScript 对象的第一个字段指向隐藏类（事实上，任何在 V8 堆上且由垃圾收集器管理的对象都是这种情况）。在属性中，最重要的信息是第三位字段，它保存了属性数以及描述符数组的指针。描述符数组包含了有关命名属性的信息，例如名称本身以及值保存的位置。注意我们不会在此处跟踪整数索引属性，故描述符数组中没有相关条目。</p><p>对于隐藏类的基本判断标准是，具有相同结构的对象（如属性命名相同且顺序相同）共享相同的隐藏类。为了实现这一点，对象在添加属性后我们将使用不同的隐藏类。在下面的示例中，我们从一个空对象开始并添加三个命名属性。</p><p></p><figure><img src="https://v8.dev/_img/fast-properties/adding-properties.png" alt=""></figure><p></p><p>每次添加新属性时，对象的隐藏类都会被更改。在引擎的底层，V8 创建了一个将隐藏类链接在一起的转换树（transiton tree）。当你向一个空对象添加属性（如“a”）时，V8 会知道要采用哪个隐藏类。如果以相同的顺序添加相同的属性，此转换树会确保最后得到的是相同的最终隐藏类。以下示例显示，即使我们在其间添加了简单的索引属性，我们还是得到了相同的转换树。</p><p></p><figure><img src="https://v8.dev/_img/fast-properties/transitions.png" alt=""></figure><p></p><p>然而，如果我们创建一个添加了不同属性的新对象，如属性 <code>&quot;d&quot;</code>，则 V8 会为新的隐藏类创建一个单独的分支。</p><p></p><figure><img src="https://v8.dev/_img/fast-properties/transition-trees.png" alt=""></figure><p></p><p><strong>本节要点:</strong></p><ul><li>具有相同结构的对象（相同顺序相同属性）具有相同的隐藏类。</li><li>默认情况下，每添加新的命名属性都会导致一个新的隐藏类被创建。</li><li>添加数组索引属性不会创建新的隐藏类。</li></ul><h2 id="三种不同的命名属性"><a href="#三种不同的命名属性" class="headerlink" title="三种不同的命名属性"></a>三种不同的命名属性</h2><p>在概述了 V8 如何使用隐藏类跟踪对象的形状之后，让我们深入了解这些属性的实际存储方式。正如上面介绍中所解释的，属性有两种基本类型：命名和索引。以下部分先介绍命名属性。</p><p>一个简单的对象如 <code>{a：1，b：2}</code> 在 V8 中可以有多种内部表示。虽然 JavaScript 对象在外部看来或多或少类似于简单的字典，但 V8 试图避免使用字典，因为它们妨碍了某些优化，如<a href="https://en.wikipedia.org/wiki/Inline_caching" target="_blank">内联缓存</a>，我们会在其它文章中再作解释。</p><p><strong>对象与普通属性：</strong> V8 支持所谓的对象内属性（in-object properties），指这些属性直接存储在对象本身上。它们在 V8 可用的属性中是最快的，因为它们不需要间接层就可以访问。对象内属性的数量由对象的初始大小预先确定。如果添加的属性超出了对象分配的空间，则它们将被保存在属性存储中。属性存储多了一层间接层，但可以自由地扩容。</p><p></p><figure><img src="https://v8.dev/_img/fast-properties/in-object-properties.png" alt=""></figure><p></p><p><strong>快属性与慢属性：</strong> 下一个重要区别是快属性和慢属性。通常，我们将保存在线性属性存储中的属性定义为“快”。只需通过属性存储中的索引即可访问快属性。要从属性名称获取属性存储中的实际位置，我们必须查看隐藏类上的描述符数组，如前面所述。</p><p></p><figure><img src="https://v8.dev/_img/fast-properties/fast-vs-slow-properties.png" alt=""></figure><p></p><p>但是，如果从对象中添加和删除大量属性，则可能会产生大量时间和内存开销来维护描述符数组和隐藏类。因此 V8 还支持所谓的慢属性。带慢属性的对象内部会有独立的词典作为属性存储。所有的属性元信息不再保存在隐藏类的描述符数组中，而是直接保存在属性字典中。因此无需更新隐藏类即可添加和删除属性。由于内联缓存不适用于字典属性，故后者通常比快属性慢。</p><p><strong>本节要点:</strong></p><ul><li>有三种不同的命名属性类型：对象内属性、快属性和慢属性（字典）。<ol><li>对象属性直接保存在对象本身上，并提供最快的访问。</li><li>快属性保存在属性存储中，所有元信息都存储在隐藏类的描述符数组中。</li><li>慢属性保存在内部独立的属性字典中，不再通过隐藏类共享元信息。</li></ol></li><li>慢属性允许高效的属性删除和添加，但访问速度比其它两种类型慢。</li></ul><h2 id="元素与数组索引属性"><a href="#元素与数组索引属性" class="headerlink" title="元素与数组索引属性"></a>元素与数组索引属性</h2><p>到目前为止，我们一直在讨论命名属性，而忽略了常用于数组的整数索引属性。处理整数索引属性并不比命名属性简单。尽管所有索引属性都是在元素存储中单独保存，但不同类型的元素也有 <a href="https://cs.chromium.org/chromium/src/v8/src/elements-kind.h?q=elements-kind.h&amp;sq=package:chromium&amp;dr&amp;l=14" target="_blank">20</a> 种！</p><p><strong>挤满的还是带空隙的元素：</strong> V8 做的第一个主要区分看是元素后备存储（elements backing store）是挤满的（packed）还是带空隙的（holey）。当索引元素被删除，又或者如，没有被定义，后备存储中就会出现空隙。如一个简单的例子 <code>[1,,3]</code>，其中的第二个条目就是一个空隙。以下示例说明了此问题：</p><p></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> o = [<span class="string">'a'</span>, <span class="string">'b'</span>, <span class="string">'c'</span>];</span><br><span class="line"><span class="built_in">console</span>.log(o[<span class="number">1</span>]);          <span class="comment">// 打印 'b'.</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">delete</span> o[<span class="number">1</span>];                <span class="comment">// 在元素存储中引入空隙</span></span><br><span class="line"><span class="built_in">console</span>.log(o[<span class="number">1</span>]);          <span class="comment">// 打印 'undefined'，属性 1 不存在</span></span><br><span class="line">o.__proto__ = &#123;<span class="number">1</span>: <span class="string">'B'</span>&#125;;     <span class="comment">// 在原型中定义属性 1</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">console</span>.log(o[<span class="number">0</span>]);          <span class="comment">// 打印 'a'.</span></span><br><span class="line"><span class="built_in">console</span>.log(o[<span class="number">1</span>]);          <span class="comment">// 打印 'B'.</span></span><br><span class="line"><span class="built_in">console</span>.log(o[<span class="number">2</span>]);          <span class="comment">// 打印 'c'.</span></span><br><span class="line"><span class="built_in">console</span>.log(o[<span class="number">3</span>]);          <span class="comment">// 打印 undefined</span></span><br></pre></td></tr></table></figure><p></p><p></p><figure><img src="https://v8.dev/_img/fast-properties/hole.png" alt=""></figure><p></p><p>简而言之，如果接收方（receiver）上没有发现属性，我们就必须沿着原型链继续查找。鉴于元素是内部独立的（比如，我们不会在隐藏类上保存有关当前索引属性的信息），我们需要一个特殊值，称为 _hole，来标记不存在的属性。这对于数组函数的性能是至关重要的。如果我们知道没有空隙，即元素存储是挤满的，我们就可以直接在当前域执行操作而无需沿着原型链做昂贵的查找。</p><p><strong>快元素还是字典元素：</strong> V8 对元素做的第二个主要区分是看它们是在快速模式还是字典模式。快元素是简单的虚拟机内部数组，其中的属性索引映射到元素存储中的索引。然而，这种简单的表示对于非常大的稀疏/带空隙的数组而言是相当浪费的，其中只有很少的条目被占用。在这种情况下，我们使用基于字典的表示来节省内存，但代价是访问速度稍慢：</p><p></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> sparseArray = [];</span><br><span class="line">sparseArray[<span class="number">9999</span>] = <span class="string">'foo'</span>; <span class="comment">// 创建了一个基于字典元素的数组</span></span><br></pre></td></tr></table></figure><p></p><p>在这个例子中，开辟一个包含一万条目的完整数组会相当浪费。相反，V8 会创建一个字典来存储键-值-描述符三元组。在这个例子中，键是 <code>&#39;9999&#39;</code>，值是 <code>&#39;foo&#39;</code>，默认描述符被使用。由于我们没有办法在隐藏类上保存描述符的详细信息，故只要你使用了自定义描述符去定义索引属性，V8 就会采用慢元素：</p><p></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> array = [];</span><br><span class="line"><span class="built_in">Object</span>.defineProperty(array, <span class="number">0</span>, &#123;<span class="attr">value</span>: <span class="string">'fixed'</span> configurable: <span class="literal">false</span>&#125;);</span><br><span class="line"><span class="built_in">console</span>.log(array[<span class="number">0</span>]);      <span class="comment">// 打印 'fixed'。</span></span><br><span class="line">array[<span class="number">0</span>] = <span class="string">'other value'</span>;   <span class="comment">// 不能重写索引 0。</span></span><br><span class="line"><span class="built_in">console</span>.log(array[<span class="number">0</span>]);      <span class="comment">// 依然打印 'fixed'。</span></span><br></pre></td></tr></table></figure><p></p><p>在这个例子中，我们在数组上添加了一个不可配置的属性。该信息保存在慢元素字典三元组的描述符部分中。需要注意的是，在带有慢元素的对象上数组函数的执行速度会慢很多。</p><p><strong>小整数和双浮点元素：</strong> 对于快元素，V8 中还有另一个重要的区分。比如一个常见的例子，只在一个数组中放整数，那么垃圾回收器就不必查看数组，因为整数被直接编码为所谓的小整数（Smis）。另一个特例是只包含双浮点数（double）的数组。与小整数不同，浮点数通常表示为占据多个字（word）的完整对象。然而，在 V8 中，纯双浮点数的数组会使用原生双精度浮点数来保存，以减少内存和性能开销。以下例子列出了小整数和双浮点元素的四个示例：</p><p></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> a1 = [<span class="number">1</span>,   <span class="number">2</span>, <span class="number">3</span>];  <span class="comment">// 挤满小整数</span></span><br><span class="line"><span class="keyword">const</span> a2 = [<span class="number">1</span>,    , <span class="number">3</span>];  <span class="comment">// 带空隙小整数，a2[1] 从原型中读取</span></span><br><span class="line"><span class="keyword">const</span> b1 = [<span class="number">1.1</span>, <span class="number">2</span>, <span class="number">3</span>];  <span class="comment">// 挤满双浮点</span></span><br><span class="line"><span class="keyword">const</span> b2 = [<span class="number">1.1</span>,  , <span class="number">3</span>];  <span class="comment">// 带空隙双浮点，b2[1] 从原型中读取</span></span><br></pre></td></tr></table></figure><p></p><p><strong>特殊元素：</strong> 到目前为止，我们已经涵盖了 20 种不同元素中的 7 种。为简单起见，我们排除了 9 种 TypedArrays 相关的元素类型，2 种 String 包装器相关的，还有 2 种参数对象相关的特殊类型。</p><p><strong>元素访问器：</strong> 正如你所想的，我们并不太热衷于在 C++ 中给<a href="https://v8.dev/blog/elements-kinds" target="_blank">元素种类</a>一个个对应地写 20 多遍数组函数。这里就需要一些 C++ 魔术。我们不是一遍又一遍地实现数组函数，而是构建了元素访问器 <code>ElementsAccessor</code>，这样我们大多情况下只需要实现简单的函数来从后备存储中访问元素。<code>ElementsAccessor</code>依赖于 <a href="https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern" target="_blank">CRTP</a> 来创建每个数组函数的专用版本。因此，如果在数组上调用类似 <code>slice</code> 的方法，V8 内部会调用 C++ 编写的内置函数，并通过 <code>ElementsAccessor</code> 调度到函数的专用版本：</p><p></p><figure><img src="https://v8.dev/_img/fast-properties/elements-accessor.png" alt=""></figure><p></p><p><strong>本节要点:</strong></p><ul><li>索引属性和元素有快速和字典模式。</li><li>快属性可以是挤满的（packed），也可以包含空隙（holes）表示索引属性被删除。</li><li>不同类型的元素根据其内容专门优化，以加速数组函数并减少垃圾回收器开销。</li></ul><p>了解属性如何工作是 V8 中许多优化的关键。对于 JavaScript 开发者来说，许多这样的内部决策都不是直接可见的，但它们解释了为什么某些代码模式比其它的更快。更改属性或元素类型通常会导致 V8 创建一个不同的隐藏类，这可能导致类型污染<a href="http://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html" target="_blank">阻止 V8 生成最优代码</a>。请继续关注有关 V8 虚拟机内部工作原理的更多文章。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;译自：&lt;a href=&quot;https://v8.dev/blog/fast-properties/&quot; target=&quot;_blank&quot;&gt;Fast properties in V8&lt;/a&gt;（2017-08-30）&lt;/p&gt;
&lt;p&gt;本文我们将解释 V8 内部如何处理 JavaScri
      
    
    </summary>
    
      <category term="JavaScript" scheme="https://blog.crimx.com/categories/JavaScript/"/>
    
    
      <category term="Translation" scheme="https://blog.crimx.com/tags/Translation/"/>
    
      <category term="JavaScript" scheme="https://blog.crimx.com/tags/JavaScript/"/>
    
  </entry>
  
  <entry>
    <title>如何文明提交代码</title>
    <link href="https://blog.crimx.com/2018/09/24/be-a-commitizen/"/>
    <id>https://blog.crimx.com/2018/09/24/be-a-commitizen/</id>
    <published>2018-09-23T16:00:00.000Z</published>
    <updated>2018-09-24T15:28:34.895Z</updated>
    
    <content type="html"><![CDATA[<p>程序员最烦的几件事：写测试，变量命名，还有填代码提交信息（commit message）。翻几个开源项目遍马上可以回味那作文凑字数的青春时光。</p><p>其实 commit message 的作用远不止如此，经过简单的配置便可无痛成为代码提交的文明公民。</p><h2 id="Commit-Message-的作用"><a href="#Commit-Message-的作用" class="headerlink" title="Commit Message 的作用"></a>Commit Message 的作用</h2><p>最起码的一点，项目的提交历史是其他人（包括未来的自己）了解项目的一个重要途径。好的提交历史可以方便其他人参与进来，也可以方便自己快速定位问题。</p><p>此外，提交信息还可以用来触发 CI 构建，自动生成 CHANGELOG ，版本自动语义化提升…… 只需要一点点配置就可以干这么多，真是懒人必备。</p><h2 id="选择风格"><a href="#选择风格" class="headerlink" title="选择风格"></a>选择风格</h2><p>跟 Code Style 一样，Commit Message 也有各种风格。如果没什么特殊癖好推荐用基于 Angular ， 后独立开来的 <a href="https://www.conventionalcommits.org/" target="_blank">Conventional Commits</a> 风格。 它也基本是各个工具的默认配置，所以搭配起来不需要折腾。</p><h2 id="才不要又记什么规则"><a href="#才不要又记什么规则" class="headerlink" title="才不要又记什么规则"></a>才不要又记什么规则</h2><p>虽然规则不多，但不一定能随时记住，特别是对新人，必须要有友好的方式提交。</p><p>commitizen 是一个很好的选择，通过命令行回答几个问题即可填完信息，减轻了记忆负担。 它是一个通用的工具，通过插件方式支持各种风格。我们选择 Conventional 需要安装 <a href="https://github.com/commitizen/cz-cli#adapters" target="_blank">cz-conventional-changelog</a> 。</p><p></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install --save-dev commitizen cz-conventional-changelog</span><br></pre></td></tr></table></figure><p></p><p>然后配置 <code>package.json</code> 就可以通过 <code>npm run commit</code> 提交。</p><p></p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"scripts"</span>: &#123;</span><br><span class="line">    <span class="attr">"commit"</span>: <span class="string">"git-cz"</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><p>另外 VSCode 用户也可以用 <a href="https://github.com/KnisterPeter/vscode-commitizen" target="_blank">vscode-commitizen</a> ， 通过 <code>ctrl+shift+p</code> 或 <code>command+shift+p</code> 提交。</p><h2 id="Lint-一-Lint-万无一失"><a href="#Lint-一-Lint-万无一失" class="headerlink" title="Lint 一 Lint 万无一失"></a>Lint 一 Lint 万无一失</h2><p>没错，Commit Message 也有 Linter ，可对 Commit Message 进行检验，杜绝打字手残和浑水摸鱼。</p><p>这里用 commitlint 配合 husky 实现自动检测。</p><p>commitlint 也是通用的工具，需要同时安装风格配置。 husky 可以方便使用 git hooks ，在 commit 时触发 commitlint 。</p><p></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install --save-dev @commitlint/cli @commitlint/config-conventional husky</span><br></pre></td></tr></table></figure><p></p><p>项目根新建 <code>commitlint.config.js</code></p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">  extends: [<span class="string">'@commitlint/config-conventional'</span>]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><p>配置 <code>package.json</code></p><p></p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"husky"</span>: &#123;</span><br><span class="line">    <span class="attr">"hooks"</span>: &#123;</span><br><span class="line">      <span class="attr">"commit-msg"</span>: <span class="string">"commitlint -E HUSKY_GIT_PARAMS"</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><h2 id="自动更新"><a href="#自动更新" class="headerlink" title="自动更新"></a>自动更新</h2><p>最后安装 <a href="https://github.com/conventional-changelog/standard-version" target="_blank">standard-version</a> 实现自动生成 CHANGELOG 和版本自动语义化提升。</p><p></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install --save-dev standard-version</span><br></pre></td></tr></table></figure><p></p><p>配置 <code>package.json</code></p><p></p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"scripts"</span>: &#123;</span><br><span class="line">    <span class="attr">"release"</span>: <span class="string">"standard-version"</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><p>第一次发布时可以用以下命令重置</p><p></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm run release -- --first-release</span><br></pre></td></tr></table></figure><p></p><p>以后直接 <code>npm run release</code> 即可。</p><p>也可以手动指定版本：</p><p></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"># npm run script</span><br><span class="line">npm run release -- --release-as minor</span><br><span class="line"># Or</span><br><span class="line">npm run release -- --release-as 1.1.0</span><br></pre></td></tr></table></figure><p></p><h2 id="小红花贴起来"><a href="#小红花贴起来" class="headerlink" title="小红花贴起来"></a>小红花贴起来</h2><p><a href="http://commitizen.github.io/cz-cli/" target="_blank"><img src="https://img.shields.io/badge/commitizen-friendly-brightgreen.svg?maxAge=2592000" alt="Commitizen friendly"></a></p><p><a href="https://conventionalcommits.org" target="_blank"><img src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-brightgreen.svg?maxAge=2592000" alt="Conventional Commits"></a></p><p>在 README 中加入小徽章可方便其他人了解风格。</p><p></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg?maxAge=2592000)](http://commitizen.github.io/cz-cli/)</span><br><span class="line">[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-brightgreen.svg?maxAge=2592000)](https://conventionalcommits.org)</span><br></pre></td></tr></table></figure><p></p><h2 id="完整配置"><a href="#完整配置" class="headerlink" title="完整配置"></a>完整配置</h2><p>安装</p><p></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install --save-dev commitizen cz-conventional-changelog @commitlint/cli @commitlint/config-conventional husky standard-version</span><br></pre></td></tr></table></figure><p></p><p>配置 <code>package.json</code></p><p></p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"scripts"</span>: &#123;</span><br><span class="line">    <span class="attr">"commit"</span>: <span class="string">"git-cz"</span>,</span><br><span class="line">    <span class="attr">"release"</span>: <span class="string">"standard-version"</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">"husky"</span>: &#123;</span><br><span class="line">    <span class="attr">"hooks"</span>: &#123;</span><br><span class="line">      <span class="attr">"commit-msg"</span>: <span class="string">"commitlint -E HUSKY_GIT_PARAMS"</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><p>项目根新建 <code>commitlint.config.js</code></p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">  extends: [<span class="string">'@commitlint/config-conventional'</span>]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><p>【完】</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;程序员最烦的几件事：写测试，变量命名，还有填代码提交信息（commit message）。翻几个开源项目遍马上可以回味那作文凑字数的青春时光。&lt;/p&gt;
&lt;p&gt;其实 commit message 的作用远不止如此，经过简单的配置便可无痛成为代码提交的文明公民。&lt;/p&gt;
&lt;h2
      
    
    </summary>
    
      <category term="Tools" scheme="https://blog.crimx.com/categories/Tools/"/>
    
    
      <category term="Git" scheme="https://blog.crimx.com/tags/Git/"/>
    
  </entry>
  
  <entry>
    <title>React 黑魔法之 Portal + SyntheticEvent + Iframe</title>
    <link href="https://blog.crimx.com/2018/07/15/react-dark-magic-portal-synthetic-event-iframe/"/>
    <id>https://blog.crimx.com/2018/07/15/react-dark-magic-portal-synthetic-event-iframe/</id>
    <published>2018-07-14T16:00:00.000Z</published>
    <updated>2018-07-15T10:53:27.954Z</updated>
    
    <content type="html"><![CDATA[<p>在实现划词扩展的时候，查词面板等模块需要植入到源网页，为了更方便地隔离样式污染，植入的模块均使用了 <code>&lt;iframe&gt;</code> 包装。在前一个 Vue 实现的版本 Saladict 5 中，有几个不太舒服的小小小小地方：</p><ol><li><code>&lt;iframe&gt;</code> 内外事件不通。</li><li><code>&lt;iframe&gt;</code> 内外环境不一样，变量不能共用，需要 <code>postMessage</code> 交流。</li><li>Vue 组件必须要有一个根元素，且 <code>v-if</code> 隐藏之后元素原地还是会留下注释标记。在浏览器审查元素时看起来不太干净。</li></ol><p>当然这个不影响呈现效果，所以当时也不怎么纠结了。然而后来在逛 React 文档的时候，意外发现了 <a href="https://reactjs.org/docs/portals.html" target="_blank">Portal</a> 这个神器。</p><p>Portal 精妙的地方在于从 React 的角度（也是代码作者的角度），组件可以保持原来的结构，但实际可以渲染到任意地方（甚至是<a href="https://hackernoon.com/using-a-react-16-portal-to-do-something-cool-2a2d627b0202" target="_blank">其它窗口</a>！）。</p><p>更令人拍案叫绝的是， <a href="https://reactjs.org/docs/events.html" target="_blank">SyntheticEvent</a> 也是抽象到 React 层的，所以组件事件捕获冒泡全正常使用。</p><p>对于 <code>&lt;iframe&gt;</code> 需要做些小改变，<a href="https://github.com/iphong/react-portal-frame/blob/c6d42b7bfdb07eb4b3f908912356846970f713f2/src/index.js#L151-L153" target="_blank">有人发现</a>，只需要把 <code>&lt;body&gt;</code> 组件上所有事件 <code>null</code> 掉即可。</p><p>这样就方便地解决了前两个小痛点了！</p><p>最后一点的解决方式是在组件不显示的时候将 Portal 的载体元素 <code>remove</code> 掉，显示时再 <code>appendChild</code> 回去。甚至配合动画也非常简单，在动画结束时 <code>remove</code> 即可。</p><p>最后如果对实现感兴趣的话可以参考：</p><ul><li><a href="https://github.com/crimx/ext-saladict/blob/dev/src/components/PortalFrame.tsx" target="_blank">封装的 PortalFrame 组件</a>。</li><li><a href="https://github.com/crimx/ext-saladict/blob/5242bbf596a88a04b5ea067a4b4f3989e80d46ef/src/content/components/DictPanelPortal/index.tsx#L285-L290" target="_blank">在 PortalFrame 外部监听内部事件</a>。</li><li><a href="https://github.com/crimx/ext-saladict/blob/dev/src/content/components/SaladBowlPortal/index.tsx" target="_blank">与动画组件配合移除载体元素</a>。</li></ul><p>值得一提的是，也有人吸取灵感实现了 Vue 版的 <a href="https://github.com/LinusBorg/portal-vue" target="_blank">Portal</a>。然而这个功能<a href="https://github.com/vuejs/vue/issues/4841" target="_blank">没有得到官方青睐</a>，只是社区实现。由于没有内核权限，相比于 React Portal 这仅是部分实现。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;在实现划词扩展的时候，查词面板等模块需要植入到源网页，为了更方便地隔离样式污染，植入的模块均使用了 &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; 包装。在前一个 Vue 实现的版本 Saladict 5 中，有几个不太舒服的小小小小地方：&lt;/p&gt;
&lt;ol&gt;
&lt;li
      
    
    </summary>
    
      <category term="JavaScript" scheme="https://blog.crimx.com/categories/JavaScript/"/>
    
    
      <category term="JavaScript" scheme="https://blog.crimx.com/tags/JavaScript/"/>
    
      <category term="React" scheme="https://blog.crimx.com/tags/React/"/>
    
  </entry>
  
  <entry>
    <title>选中鼠标附近的文字</title>
    <link href="https://blog.crimx.com/2018/06/22/select-cursor-word/"/>
    <id>https://blog.crimx.com/2018/06/22/select-cursor-word/</id>
    <published>2018-06-21T16:00:00.000Z</published>
    <updated>2018-06-22T08:18:15.040Z</updated>
    
    <content type="html"><![CDATA[<p>最近终于抽空给 Saladict 实现了鼠标悬浮取词功能，使用了较为简洁的实现方式，这里分享一下原理以及坑的处理。</p><h2 id="初尝试"><a href="#初尝试" class="headerlink" title="初尝试"></a>初尝试</h2><p>这个需求其实很早就被人提 issue 了，当时做了一番搜索，最后尝试了 <code>document.caretPositionFromPoint</code> / <code>document.caretRangeFromPoint</code> ，效果不太理想。</p><p>如果看 mdn 给的<a href="https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot/caretPositionFromPoint" target="_blank">例子</a>，就会发现，它是遍历每个元素添加事件的。这么做的原因是当使用这个方法的时候，如果鼠标指向元素空白的地方，它会就近取位置。所以例子通过给粒度更细的元素绑定来避免这个问题。然而实际上这么做还是不足够的，一个段落末行也许只有几个字符，这时空出接近一行，也会有上面的问题。</p><p>所以当时就搁置了这个功能。</p><h2 id="灵感"><a href="#灵感" class="headerlink" title="灵感"></a>灵感</h2><p>直到最近，看到一个同类的开源划词翻译扩展 <a href="https://github.com/revir/FairyDict" target="_blank">FairyDict</a> 实现了取词功能，遍观摩了一番<a href="https://github.com/revir/FairyDict/blob/30e12d426b9eb190142003732cdfb0d2aa64eb66/content/inject.coffee#L110-L141" target="_blank">源码</a>。</p><p>它的原理是深度优先递归遍历这个元素以及其子元素，通过不断试探选中区域，并与鼠标座标对比来定位确切位置。</p><p>有没有发现问题，这个遍历过程不正是上面 <code>document.caretPositionFromPoint</code> 干的事么，那么我们只需要最后量一下鼠标是否在取词范围中即可。</p><h2 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h2><p>现在总结一下原理：</p><ol><li>通过 <code>document.caretPositionFromPoint</code> 获得鼠标所指最接近的元素以及文本位置 offset。</li><li>找出 offset 最接近的单词。</li><li>通过 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Range" target="_blank"><code>Range</code></a> 获得部分文本（单词）的尺寸和座标。</li><li>验证鼠标此时在单词区域范围中。</li><li>选中这个单词。<code>Selection</code> 支持直接添加 <code>Range</code> 。</li></ol><h2 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h2><p>按原理来实现就很简单了。<a href="https://blog.crimx.com/select-cursor-word" target="_blank">本文</a>上按 <kbd>alt</kbd> 可体验取词效果。</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @param &#123;MouseEvent&#125; e</span></span><br><span class="line"><span class="comment"> * @returns &#123;void&#125;</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">selectCursorWord</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> x = e.clientX</span><br><span class="line">  <span class="keyword">const</span> y = e.clientY</span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> offsetNode</span><br><span class="line">  <span class="keyword">let</span> offset</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> sel = <span class="built_in">window</span>.getSelection()</span><br><span class="line">  sel.removeAllRanges()</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (<span class="built_in">document</span>[<span class="string">'caretPositionFromPoint'</span>]) &#123;</span><br><span class="line">    <span class="keyword">const</span> pos = <span class="built_in">document</span>[<span class="string">'caretPositionFromPoint'</span>](x, y)</span><br><span class="line">    <span class="keyword">if</span> (!pos) &#123; <span class="keyword">return</span> &#125;</span><br><span class="line">    offsetNode = pos.offsetNode</span><br><span class="line">    offset = pos.offset</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="built_in">document</span>[<span class="string">'caretRangeFromPoint'</span>]) &#123;</span><br><span class="line">    <span class="keyword">const</span> pos = <span class="built_in">document</span>[<span class="string">'caretRangeFromPoint'</span>](x, y)</span><br><span class="line">    <span class="keyword">if</span> (!pos) &#123; <span class="keyword">return</span> &#125;</span><br><span class="line">    offsetNode = pos.startContainer</span><br><span class="line">    offset = pos.startOffset</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (offsetNode.nodeType === Node.TEXT_NODE) &#123;</span><br><span class="line">    <span class="keyword">const</span> textNode = offsetNode</span><br><span class="line">    <span class="keyword">const</span> content = textNode.data</span><br><span class="line">    <span class="keyword">const</span> head = (content.slice(<span class="number">0</span>, offset).match(<span class="regexp">/[-_a-z]+$/i</span>) || [<span class="string">''</span>])[<span class="number">0</span>]</span><br><span class="line">    <span class="keyword">const</span> tail = (content.slice(offset).match(<span class="regexp">/^([-_a-z]+|[\u4e00-\u9fa5])/i</span>) || [<span class="string">''</span>])[<span class="number">0</span>]</span><br><span class="line">    <span class="keyword">if</span> (head.length &lt;= <span class="number">0</span> &amp;&amp; tail.length &lt;= <span class="number">0</span>) &#123;</span><br><span class="line">      <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> range = <span class="built_in">document</span>.createRange()</span><br><span class="line">    range.setStart(textNode, offset - head.length)</span><br><span class="line">    range.setEnd(textNode, offset + tail.length)</span><br><span class="line">    <span class="keyword">const</span> rangeRect = range.getBoundingClientRect()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (rangeRect.left &lt;= x &amp;&amp;</span><br><span class="line">        rangeRect.right &gt;= x &amp;&amp;</span><br><span class="line">        rangeRect.top &lt;= y &amp;&amp;</span><br><span class="line">        rangeRect.bottom &gt;= y</span><br><span class="line">    ) &#123;</span><br><span class="line">      sel.addRange(range)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    range.detach()</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><h2 id="交互"><a href="#交互" class="headerlink" title="交互"></a>交互</h2><p>最后，如果要提供功能开关或者设置不同按键的话，简单的处理可以参考 FairyDict 让事件处理空转。但对于 <code>mousemove</code> 这类比较频繁的事件，在关闭的时候取消事件监听可能更好一些。这里 Saladict 借助 RxJS 来处理复杂的逻辑，可参考<a href="https://github.com/crimx/ext-saladict/blob/ca0d8a1e58ef56277f2f0b3df4a291d4f2a0debc/src/selection/index.ts#L185-L232" target="_blank">源码</a>。</p><p><script type="text/javascript">document.addEventListener('mousemove', e => {    if (e.altKey) {        selectCursorWord(e)    }}, true)function selectCursorWord(e) {    const x = e.clientX    const y = e.clientY    let offsetNode    let offset    const sel = window.getSelection()    sel.removeAllRanges()    if (document['caretPositionFromPoint']) {        const pos = document['caretPositionFromPoint'](x, y)        if (!pos) {            return        }        offsetNode = pos.offsetNode        offset = pos.offset    } else if (document['caretRangeFromPoint']) {        const pos = document['caretRangeFromPoint'](x, y)        if (!pos) {            return        }        offsetNode = pos.startContainer        offset = pos.startOffset    } else {        return    }    if (offsetNode.nodeType === Node.TEXT_NODE) {        const textNode = offsetNode        const content = textNode.data        const head = (content.slice(0, offset).match(/[-_a-z]+$/i) || [''])[0]        const tail = (content.slice(offset).match(/^([-_a-z]+|[\u4e00-\u9fa5])/i) || [''])[0]        if (head.length <= 0 && tail.length <= 0) {            return        }        const range = document.createRange()        range.setStart(textNode, offset - head.length)        range.setEnd(textNode, offset + tail.length)        const rangeRect = range.getBoundingClientRect()        if (rangeRect.left <= x &&            rangeRect.right >= x &&            rangeRect.top <= y &&            rangeRect.bottom >= y        ) {            sel.addRange(range)        }        range.detach()    }}</script></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;最近终于抽空给 Saladict 实现了鼠标悬浮取词功能，使用了较为简洁的实现方式，这里分享一下原理以及坑的处理。&lt;/p&gt;
&lt;h2 id=&quot;初尝试&quot;&gt;&lt;a href=&quot;#初尝试&quot; class=&quot;headerlink&quot; title=&quot;初尝试&quot;&gt;&lt;/a&gt;初尝试&lt;/h2&gt;
&lt;p&gt;
      
    
    </summary>
    
      <category term="JavaScript" scheme="https://blog.crimx.com/categories/JavaScript/"/>
    
    
      <category term="JavaScript" scheme="https://blog.crimx.com/tags/JavaScript/"/>
    
      <category term="Recommended" scheme="https://blog.crimx.com/tags/Recommended/"/>
    
  </entry>
  
  <entry>
    <title>深入 React Render Props 模式</title>
    <link href="https://blog.crimx.com/2018/04/03/understanding-render-props-in-react/"/>
    <id>https://blog.crimx.com/2018/04/03/understanding-render-props-in-react/</id>
    <published>2018-04-02T16:00:00.000Z</published>
    <updated>2018-04-03T09:32:52.996Z</updated>
    
    <content type="html"><![CDATA[<p>随着 React 的新 Context API 出来，render props 模式再次发挥重要作用。本文将尝试深入理解 render props 的利弊，并结合高阶组件寻找合适的处理方式。</p><h2 id="基础"><a href="#基础" class="headerlink" title="基础"></a>基础</h2><p>先看<a href="https://reactjs.org/docs/render-props.html" target="_blank">官方</a>给出的简单例子：</p><p></p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&lt;DataProvider render=&#123;data =&gt; (</span><br><span class="line">  &lt;h1&gt;Hello &#123;data.target&#125;&lt;<span class="regexp">/h1&gt;</span></span><br><span class="line"><span class="regexp">)&#125;/</span>&gt;</span><br></pre></td></tr></table></figure><p></p><p>加个 <code>DataProvider</code> 的简单实现，</p><p></p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">DataProvider</span> <span class="keyword">extends</span> <span class="title">React</span>.<span class="title">Component</span> </span>&#123;</span><br><span class="line">  state = &#123; <span class="attr">target</span>: <span class="string">''</span> &#125;</span><br><span class="line">  handleMouseMove = <span class="function"><span class="params">e</span> =&gt;</span> <span class="keyword">this</span>.setState(&#123; <span class="attr">target</span>: e.target.title &#125;)</span><br><span class="line">  render() &#123;</span><br><span class="line">    <span class="keyword">return</span> (</span><br><span class="line">      &lt;div style=&#123;&#123; <span class="attr">height</span>: <span class="string">'1vh'</span> &#125;&#125; onMouseMove=&#123;<span class="keyword">this</span>.handleMouseMove&#125;&gt;</span><br><span class="line">        &#123;<span class="keyword">this</span>.props.render(<span class="keyword">this</span>.state)&#125;</span><br><span class="line">      &lt;<span class="regexp">/div&gt;</span></span><br><span class="line"><span class="regexp">    )</span></span><br><span class="line"><span class="regexp">  &#125;</span></span><br><span class="line"><span class="regexp">&#125;</span></span><br></pre></td></tr></table></figure><p></p><p>这里是将一个返回 React 元素的函数传给 <code>DataProvider</code> 的 <code>props.render</code>，<code>DataProvider</code> render 的时候调用 <code>this.props.render(this.state)</code> 渲染这个函数。</p><p>这也是“render props”名字的来源，而现在更流行的是“children props”，虽然依然沿用 render props 的说法。</p><p>Children props 即将 render 换为 children ，同时 JSX 中不需要显式写 chidlren ，所以成了这个样子：</p><p></p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&lt;DataProvider&gt;</span><br><span class="line">  &#123;data =&gt; <span class="xml"><span class="tag">&lt;<span class="name">h1</span>&gt;</span>Hello &#123;data.target&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span>&#125;</span><br><span class="line">&lt;<span class="regexp">/DataProvider&gt;</span></span><br></pre></td></tr></table></figure><p></p><p>子组件通过 render props 传进父组件渲染，让父组件不再依赖子组件，达到重用父组件的目的，即<a href="https://en.wikipedia.org/wiki/Dependency_inversion_principle" target="_blank">依赖反转</a>。</p><h2 id="Render-Props-与-SFC"><a href="#Render-Props-与-SFC" class="headerlink" title="Render Props 与 SFC"></a>Render Props 与 SFC</h2><p>前面提到的“返回 React 元素的函数”，一看是不是跟<a href="https://reactjs.org/docs/components-and-props.html#functional-and-class-components" target="_blank">无状态函数组件</a>（Stateless Fuctional Component）的<a href="https://github.com/DefinitelyTyped/DefinitelyTyped/blob/8a857d6473829df15ae8d695dbac310e824479df/types/react/index.d.ts#L329" target="_blank">签名</a>很接近。这么看：</p><p></p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&lt;DataProvider&gt;</span><br><span class="line">  &#123;props =&gt; <span class="xml"><span class="tag">&lt;<span class="name">h1</span>&gt;</span>Hello &#123;props.target&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span>&#125;</span><br><span class="line">&lt;<span class="regexp">/DataProvider&gt;</span></span><br></pre></td></tr></table></figure><p></p><p>但不一样的地方在于 render props 只是一个普通函数，是直接函数调用。且 inline render props 可以跳过 <code>DataProvider</code> 直接访问其它父组件：</p><p></p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> App = <span class="function"><span class="params">props</span> =&gt;</span> (</span><br><span class="line">  &lt;DataProvider&gt;</span><br><span class="line">    &#123;data =&gt; <span class="xml"><span class="tag">&lt;<span class="name">h1</span>&gt;</span>Hello &#123;data.target&#125; &#123;props.target&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span>&#125;</span><br><span class="line">  &lt;<span class="regexp">/DataProvider&gt;</span></span><br><span class="line"><span class="regexp">)</span></span><br></pre></td></tr></table></figure><p></p><p>这就造成了 <code>DataProvider</code> 不能优化为纯组件。这也是非常不好的习惯，一个解决方式是人为限制 render props 为 SFC 从而让只有一个数据来源。理想很美好，但没法强制所有人这么干。</p><h2 id="Render-Props-与-HOC"><a href="#Render-Props-与-HOC" class="headerlink" title="Render Props 与 HOC"></a>Render Props 与 HOC</h2><p>如果将 render props 限制为传入子组件，其实很容易联想到<a href="https://reactjs.org/docs/higher-order-components.html" target="_blank">高阶组件</a>（Higher-Order Components）。高阶组件是一个函数，接受一个组件，返回新的组件。</p><p>所以可以用高阶组件包装起来，并隐藏 render props 接口。</p><p></p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> withData = <span class="function"><span class="params">Base</span> =&gt;</span> () =&gt; <span class="xml"><span class="tag">&lt;<span class="name">DataProvider</span>&gt;</span>&#123;Base&#125;<span class="tag">&lt;/<span class="name">DataProvider</span>&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> BaseTarget = <span class="function"><span class="params">props</span> =&gt;</span> &lt;h1&gt;Hello &#123;props.target&#125;&lt;<span class="regexp">/h1&gt;</span></span><br><span class="line"><span class="regexp"></span></span><br><span class="line"><span class="regexp">const EnhancedComponent = withData(BaseTarget)</span></span><br><span class="line"><span class="regexp"></span></span><br><span class="line"><span class="regexp">const App = () =&gt; &lt;EnhancedComponent /</span>&gt;</span><br></pre></td></tr></table></figure><p></p><p>这样就限制了 <code>DataProvider</code> 的使用方式。如果需要多方数据，可以修改 <code>DataProvider</code> 为 <code>this.props.render({ ...this.props, ...this.state })</code> 将其它数据作为 props 传入 <code>DataProvider</code>。</p><p></p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> withData = <span class="function"><span class="params">Base</span> =&gt;</span> props =&gt; <span class="xml"><span class="tag">&lt;<span class="name">DataProvider</span> &#123;<span class="attr">...props</span>&#125;&gt;</span>&#123;Base&#125;<span class="tag">&lt;/<span class="name">DataProvider</span>&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> BaseTarget = <span class="function"><span class="params">props</span> =&gt;</span> &lt;h1&gt;Hello &#123;props.target&#125;&lt;<span class="regexp">/h1&gt;</span></span><br><span class="line"><span class="regexp"></span></span><br><span class="line"><span class="regexp">const EnhancedComponent = withData(BaseTarget)</span></span><br><span class="line"><span class="regexp"></span></span><br><span class="line"><span class="regexp">const App = props =&gt; &lt;EnhancedComponent &#123;...props&#125; /</span>&gt;</span><br></pre></td></tr></table></figure><p></p><p>「完」</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;随着 React 的新 Context API 出来，render props 模式再次发挥重要作用。本文将尝试深入理解 render props 的利弊，并结合高阶组件寻找合适的处理方式。&lt;/p&gt;
&lt;h2 id=&quot;基础&quot;&gt;&lt;a href=&quot;#基础&quot; class=&quot;head
      
    
    </summary>
    
      <category term="JavaScript" scheme="https://blog.crimx.com/categories/JavaScript/"/>
    
    
      <category term="JavaScript" scheme="https://blog.crimx.com/tags/JavaScript/"/>
    
      <category term="React" scheme="https://blog.crimx.com/tags/React/"/>
    
  </entry>
  
  <entry>
    <title>理解 RxJS ：四次元编程</title>
    <link href="https://blog.crimx.com/2018/02/16/understanding-rxjs/"/>
    <id>https://blog.crimx.com/2018/02/16/understanding-rxjs/</id>
    <published>2018-02-15T16:00:00.000Z</published>
    <updated>2018-02-17T08:54:05.822Z</updated>
    
    <content type="html"><![CDATA[<p>学习 RxJS 最大的问题是官方造了很多概念，但文档又解释得不太全面和易懂，需要结合阅读各种文章（特别是 <a href="https://medium.com/@benlesh" target="_blank">Ben Lesh</a> 的，包括视频）。本文试图整体梳理一遍再用另外的角度来介绍，希望能帮助初学者或者对 RxJS 的一些概念比较含糊的使用者。</p><h1 id="为什么需要-RxJS"><a href="#为什么需要-RxJS" class="headerlink" title="为什么需要 RxJS"></a>为什么需要 RxJS</h1><p>RxJS 属于响应式编程，其思想是将时间看作数组，随着时间发生的事件被看作是数组的项，然后以操作数组的方式变换事件。其强大的地方在于站在四维的角度看问题，这就像是拥有了上帝视野。</p><p>在处理事件之间的关系时，对于传统方式，我们需要设置各种状态变量来记录这些关系，比如对点击 <code>Shift</code> 键进行计数，需要手动设置一个 <code>let shiftPressCount: number</code>，如果需要每 600ms 清零，又需要添加计时的状态，这些状态都需要手动维护，当它们变得复杂和庞大的时候我们很快就会乱了，因为没有明确的方向，不好判断这些状态同步了没有。</p><p>而这正是 RxJS 发光发热的地方。因为从四维的角度看，这些状态就不是单个变量，而是一系列变量。比如对按键计数：</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Rx.Observable.fromEvent(<span class="built_in">document</span>, <span class="string">'keydown'</span>)</span><br><span class="line">  .filter(<span class="function">(<span class="params">&#123; key &#125;</span>) =&gt;</span> key === <span class="string">'Shift'</span>)</span><br><span class="line">  .scan(<span class="function"><span class="params">count</span> =&gt;</span> count + <span class="number">1</span>, <span class="number">0</span>)</span><br><span class="line">  .subscribe(<span class="function"><span class="params">count</span> =&gt;</span> <span class="built_in">console</span>.log(<span class="string">`按了 <span class="subst">$&#123;count&#125;</span> 遍 Shift 键`</span>))</span><br></pre></td></tr></table></figure><p></p><p>相信有使用过数组方法的人第一次看也大概能知道这里干了些什么（把 <code>scan</code> 看作是会输出中间结果的 <code>reduce</code>）。中间状态都在变换的过程中被封装起来，每一次事件的 <code>count</code> 都是独立的，不容易乱，也使得可以用纯函数去表达状态的变换。链式调用（或者 RxJS5 的 pipeable）在一定程度上限制了状态数据的流动方向，增加了可预测性，更加容易理解。</p><h1 id="理解-RxJS"><a href="#理解-RxJS" class="headerlink" title="理解 RxJS"></a>理解 RxJS</h1><h2 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h2><p>使用 RxJS 前先理解它要做什么，这里引入了两个概念，Producer （生产者）和 Observer （观察者）。</p><p>先看一个熟悉的例子：</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">document</span>.addEventListener(<span class="string">'click'</span>, <span class="function"><span class="keyword">function</span> <span class="title">handler</span> (<span class="params">e</span>) </span>&#123;</span><br><span class="line">  <span class="built_in">console</span>.log(e.clientX)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p></p><p>这里的 Producer 是 DOM 事件机制，会不定期产出 MouseEvent 事件。Observer 就是 <code>handler</code>，对事件作出反应。</p><p>再看前面的例子：</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Rx.Observable.fromEvent(<span class="built_in">document</span>, <span class="string">'keydown'</span>)</span><br><span class="line">  .filter(<span class="function">(<span class="params">&#123; key &#125;</span>) =&gt;</span> key === <span class="string">'Shift'</span>)</span><br><span class="line">  .scan(<span class="function"><span class="params">count</span> =&gt;</span> count + <span class="number">1</span>, <span class="number">0</span>)</span><br><span class="line">  .subscribe(<span class="function"><span class="params">count</span> =&gt;</span> <span class="built_in">console</span>.log(<span class="string">`按了 <span class="subst">$&#123;count&#125;</span> 遍 Shift 键`</span>))</span><br></pre></td></tr></table></figure><p></p><p>Producer 还是 DOM 事件机制，Observer 是 <code>subscribe</code> 的参数。所以可以理解 RxJS 为连接 Producer 和 Observer 的纽带。</p><p>于是这个纽带的成分叫 <a href="http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html" target="_blank">Observable</a> （可被观察的）就不难理解了。Observable 就是由事件组成的四维数组。RxJS 将 Producer 转换为 Observable，然后对 Observable 进行各种变换，最后再交给 Observer。</p><p>对 Observable 进行变换的操作符叫做 Operator，比如上面的 <code>filter</code> 和 &#39;scan&#39;，它们输入 Observable 再输出新的 Observable。RxJS 有巨量的 Operators ，这也是学习 RxJS 的第二难点，我已经分类整理了六十多个，整理完会再写一篇文章介绍，敬请关注。</p><h2 id="创建-Observable"><a href="#创建-Observable" class="headerlink" title="创建 Observable"></a>创建 Observable</h2><p>RxJS 封装了许多有用的方法来将 Producer 转换为 Observable，比如 <a href="http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#static-method-fromEvent" target="_blank"><code>fromEvent</code></a>、<a href="http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#static-method-fromPromise" target="_blank"><code>fromPromise</code></a>，但其根本是一个叫<a href="http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#static-method-create" target="_blank"><code>create</code></a> 的方法。</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> observable = Rx.Observable.create(<span class="function"><span class="params">observer</span> =&gt;</span> &#123;</span><br><span class="line">  observer.next(<span class="number">0</span>)</span><br><span class="line">  observer.next(<span class="number">1</span>)</span><br><span class="line">  observer.next(<span class="number">2</span>)</span><br><span class="line">  setTimeout(<span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">    observer.next(<span class="number">3</span>)</span><br><span class="line">    observer.complete()</span><br><span class="line">  &#125;, <span class="number">1000</span>)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p></p><p>这其实跟 Promise 的思路很像，Promise 只能 <code>resolve</code> 一遍，但这里可以 <code>observer.next</code> 很多个值（事件），最后还能 complete（不是必须的，可以有无限事件）。官方把这个类 <code>resolve</code> 的参数也叫做 <code>observer</code>，因为 <code>observer.next(0)</code> 的意思是“Subscribe 我的那个 Observer 接下来会获得这个值<code>0</code>”。我认为这是一个不好的决定，重名对于新人太容易混淆了，这个其实可以从另一个角度看，把它叫做 producer，“产生”了下个值。</p><h2 id="Subscribe-不是订阅者模式"><a href="#Subscribe-不是订阅者模式" class="headerlink" title="Subscribe 不是订阅者模式"></a>Subscribe 不是订阅者模式</h2><p>一个常见的误解是认为 RxJS 就是 <code>addEventListener</code> 那样的订阅者模式，<code>subscribe</code> 这个方法名也很有误导性。然而两者并不是一回事，订阅者模式会维护一个订阅者列表，事件来了就一一调用列表上的每个订阅者传递通知。但 RxJS 并没有这么一个列表，它就是一个函数，可以跟 Promise 类比，Promise 的 executor 是在 <code>new Promise(executor)</code> 时马上执行的，而 RxJS<code>Rx.Observable.create(observer)</code> 的 <code>observer</code> 则是在每次执行 <code>subscribe</code> 后都调用一遍，即每次 <code>subscribe</code> 的 Observables 都是独立的，都会重新走一遍整个流程。</p><p>这个时候你也许会想，这样每次都完整调用一遍岂不是很浪费性能？没错，如果需要多次 subscribe 同个 Producer 这么做会比较浪费，但如果只是 subscribe 一遍，维护一个订阅者列表也没有必要。所以 RxJS 引入了 Hot 和 Cold Observable 的概念。</p><h2 id="Hot-amp-Cold"><a href="#Hot-amp-Cold" class="headerlink" title="Hot &amp; Cold"></a>Hot &amp; Cold</h2><p>Observable 冷热概念其实就是看 Producer 的创建受不受 RxJS 控制。</p><p>前面我们知道，<code>create</code> 会将 Producer 转化为 Observable 。如果这个 Producer 也是在 <code>create</code> 回调里面产生的，那么就是 Cold ，因为 Producer 还不存在，只有 <code>subscribe</code> 了之后才会被创建。</p><p>但如果 Producer 在之前就创建了，比如 DOM 事件，<code>create</code> 回调里仅仅是对 Producer 添加 listener，那么这就叫做 Hot ，因为不需要 <code>subscribe</code> 来启动 Producer 。</p><p>只有 Hot Observable 才可以实现订阅者模式。可以通过一个特殊的 Observable 叫 <a href="http://reactivex.io/rxjs/class/es6/Subject.js~Subject.html" target="_blank">Subject</a> 来创建，其内部会维护一个订阅者列表。通过 <a href="http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-share" target="_blank"><code>share</code></a>方法可以将一个 Cold 的 Observable 转换为 Hot 。原理是内部用 Subject subscribe 上流的 Observable 实现转接。</p><h1 id="使用-RxJS"><a href="#使用-RxJS" class="headerlink" title="使用 RxJS"></a>使用 RxJS</h1><p>理解了基本概念之后就可以直接开写了，本身没有什么魔法，参考一下 api 依样画葫芦即可。</p><p>使用 RxJS 最常见的问题是不知道什么时候该用哪个 Operator 。这其实跟数组操作是一样的，RxJS 提供了数量庞大的 Operators ，基本覆盖了各种可以想到的数组操作，建议先从 JavaScript 常见的数组操作开始，如 <code>map</code>、<code>filter</code>、<code>scan</code>（也有 <code>reduce</code> ，但这个通常不是我们想要的，我们一般不需要在 complete 之后才输出结果，而是每次都输出阶段性的结果）。</p><p>多翻<a href="http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html" target="_blank">官方文档</a>，常用的 Operators 都描述得非常详细，有弹珠图（Marble Graph）和一句话总结；缺点是措辞有时可能会比较抽象，不是那么好理解。</p><p>另外就是第三方的 <a href="http://learnrxjs.io/operators" target="_blank">learnrxjs</a> 和 <a href="https://chrisnoring.gitbooks.io/rxjs-5-ultimate/content/operators.html" target="_blank">Rxjs 5 ultimate</a>，按作者的思路组织，更通俗易懂些，可以作为补充理解；缺点是可能跟官方不同步，以及不全。</p><p>我整理完也会再写一篇文章介绍，敬请期待。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;学习 RxJS 最大的问题是官方造了很多概念，但文档又解释得不太全面和易懂，需要结合阅读各种文章（特别是 &lt;a href=&quot;https://medium.com/@benlesh&quot; target=&quot;_blank&quot;&gt;Ben Lesh&lt;/a&gt; 的，包括视频）。本文试图整体梳理一
      
    
    </summary>
    
      <category term="JavaScript" scheme="https://blog.crimx.com/categories/JavaScript/"/>
    
    
      <category term="JavaScript" scheme="https://blog.crimx.com/tags/JavaScript/"/>
    
      <category term="RxJS" scheme="https://blog.crimx.com/tags/RxJS/"/>
    
      <category term="Functional" scheme="https://blog.crimx.com/tags/Functional/"/>
    
  </entry>
  
  <entry>
    <title>Web 可访问性整理</title>
    <link href="https://blog.crimx.com/2017/12/08/web-accessibility/"/>
    <id>https://blog.crimx.com/2017/12/08/web-accessibility/</id>
    <published>2017-12-07T16:00:00.000Z</published>
    <updated>2017-12-08T12:36:15.887Z</updated>
    
    <content type="html"><![CDATA[<h2 id="定义"><a href="#定义" class="headerlink" title="定义"></a>定义</h2><ul><li><p>可访问性 Accessibility<br>为障碍用户提供同等用户体验。使障碍用户能对产品感知、理解、定位和交互，并能平等地参与贡献。</p></li><li><p>可用性 Usability<br>可用性和用户体验设计（User Experience Design）是为了让目标用户使用产品高效、满意地达到特定目标。</p></li><li><p>包容性 Inclusion<br>包容性设计（Inclusive Design）、通用设计（Universal Design）和人性化设计（Design For All）尽可能让每个人都能容易地用上产品。包容性要解决的问题非常广，包括软、硬件的可访问性和质量、互联网连通性、计算机文化和技能、经济状况、教育、地理位置、语言、以及年龄和残障。</p></li></ul><h2 id="体验屏幕阅读"><a href="#体验屏幕阅读" class="headerlink" title="体验屏幕阅读"></a>体验屏幕阅读</h2><p>要解决问题首先得知道问题是怎样的，体验屏幕阅读器：</p><ul><li><a href="https://chrome.google.com/webstore/detail/chromevox/kgejglhpjiefppelpmljglcjbhoiplfn/related?hl=en" target="_blank">ChromeVox</a> ，Chrome 扩展</li><li><a href="https://www.nvaccess.org/download/" target="_blank">NVDA</a> ，开源跨平台</li><li>VoiceOver (MacOS)</li></ul><h2 id="工具"><a href="#工具" class="headerlink" title="工具"></a>工具</h2><ul><li>校验<ul><li>Chrome 自带 Audits</li><li><a href="https://www.axe-core.org/" target="_blank">aXe</a> 扩展、命令行工具等。</li><li>webaim.org</li></ul></li><li>调试<ul><li>Chrome 自带实验性 Accessibility Inspector <code>chrome://flags/#enable-devtools-experiments</code></li><li><a href="https://chrispederick.com/work/web-developer/" target="_blank">Web Developer</a> 扩展</li><li><a href="https://chrome.google.com/webstore/detail/accessibility-developer-t/fpkknkljclfencbdbgkenhalefipecmb?hl=en" target="_blank">Accessibility Developer Tools</a> 扩展</li></ul></li><li>颜色<ul><li><a href="http://colororacle.org" target="_blank">Color-Oracle</a> 模拟色盲，开源跨平台</li><li><a href="http://leaverou.github.io/contrast-ratio/" target="_blank">Lea Verous Contrast Ratio</a></li><li><a href="https://chrome.google.com/webstore/detail/color-contrast-analyzer/dagdlcijhfbmgkjokkjicnnfimlebcll" target="_blank">WCAG Color Contrast Analyzer</a></li></ul></li></ul><h2 id="参考规范"><a href="#参考规范" class="headerlink" title="参考规范"></a>参考规范</h2><p>有什么问题查规范准没错。在自行实现支持 ARIA 的元素时很有用。</p><ul><li><a href="https://www.w3.org/TR/WCAG20/" target="_blank">Web Content Accessibility Guidelines (WCAG) 2.0</a></li><li><a href="https://www.w3.org/TR/wai-aria/" target="_blank">Accessible Rich Internet Applications (WAI-ARIA)</a></li><li><a href="https://www.w3.org/TR/html5/sections.html" target="_blank">HTML 5.1 2nd Edition</a></li></ul><h2 id="其它资料"><a href="#其它资料" class="headerlink" title="其它资料"></a>其它资料</h2><h3 id="文章"><a href="#文章" class="headerlink" title="文章"></a>文章</h3><ul><li><a href="https://www.w3.org/WAI/intro/usable" target="_blank">Accessibility, Usability, and Inclusion: Related Aspects of a Web for All</a></li><li><a href="https://www.w3.org/standards/webdesign/accessibility" target="_blank">Accessibility - W3C</a></li><li><a href="https://developers.google.com/web/fundamentals/accessibility/" target="_blank">Web Fundamentals - Accessibility</a></li><li><a href="https://www.w3.org/TR/html5/common-idioms-without-dedicated-elements.html#common-idioms-without-dedicated-elements" target="_blank">Common idioms without dedicated elements</a></li><li>本文原文 <a href="https://blog.crimx.com/2017/12/08/web-accessibility/" target="_blank">Web 可访问性整理</a></li></ul><h3 id="视频"><a href="#视频" class="headerlink" title="视频"></a>视频</h3><ul><li>Google 的 <a href="https://www.youtube.com/watch?v=HtTyRajRuyY&amp;list=PLNYkxOF6rcICWx0C9LVWWVqvHlYJyqw7g&amp;index=28" target="_blank">A11ycasts</a></li><li><a href="https://www.youtube.com/watch?v=o4xHfi4t9S0&amp;list=PLWjCJDeWfDdcEtSnqq_iGLKGA_H_3o3y7" target="_blank">Accessibility</a> by Thomas Bradley</li><li><a href="https://www.youtube.com/watch?v=A5XzoDT37iM" target="_blank">Pragmatic Accessibility: A How-To Guide for Teams (Google I/O &#39;17)</a></li></ul><h3 id="课程"><a href="#课程" class="headerlink" title="课程"></a>课程</h3><ul><li>官方教程 <a href="https://www.w3.org/WAI/tutorials/" target="_blank">Web Accessibility Tutorials</a></li><li><a href="https://cn.udacity.com/course/web-accessibility--ud891" target="_blank">Web Accessibility</a></li></ul><h2 id="Tab"><a href="#Tab" class="headerlink" title="Tab"></a>Tab</h2><p>对于纯键盘使用者来说， Tab 键承担了重要责任。</p><h3 id="Tab-顺序"><a href="#Tab-顺序" class="headerlink" title="Tab 顺序"></a>Tab 顺序</h3><p>要点： Tab 顺序是按照 DOM 结构，而不是 CSSOM 结构。</p><p>所以通过 CSS 将元素提前（如 flex order）并不会影响 tab 顺序。在排版的时候需要考虑。</p><h3 id="其它元素"><a href="#其它元素" class="headerlink" title="其它元素"></a>其它元素</h3><p>Tab 默认只会识别部分元素，如果需要让其它元素也被识别，加上 <code>tabindex=&quot;0&quot;</code> 属性。</p><p>不鼓励使用大于 0 的其它数字，除了会造成混乱，一些屏幕阅读器也<a href="https://www.youtube.com/watch?v=Pe0Ce1WtnUM&amp;list=PLNYkxOF6rcICWx0C9LVWWVqvHlYJyqw7g&amp;index=25" target="_blank">不一定会遵循</a>。应该用 DOM 顺序来体现 tab 顺序。</p><h3 id="忽略-Tab"><a href="#忽略-Tab" class="headerlink" title="忽略 Tab"></a>忽略 Tab</h3><p>要让 tab 忽略一个元素，即直接跳过去，设置 <code>tabindex=&quot;-1&quot;</code> 属性。</p><p>或者用还在草稿阶段的 <code>inert</code> 属性 (<a href="https://github.com/GoogleChrome/inert-polyfill" target="_blank">polyfill</a>)。</p><h3 id="侧导航栏问题"><a href="#侧导航栏问题" class="headerlink" title="侧导航栏问题"></a>侧导航栏问题</h3><p>在一些响应式的网页，侧导航栏在宽度变小时可能会隐藏起来，这时用户如果使用 tab 跳转可能会发现焦点突然不见了，怎么按也没反应。其实是因为 tab 跳到导航栏的链接去了。</p><p>解决方式要么改变 DOM 结构，将导航栏移到最后；要么使用前面提到的 <code>inert</code> 属性。</p><h3 id="Skip-Link"><a href="#Skip-Link" class="headerlink" title="Skip Link"></a>Skip Link</h3><p>将导航的每个项目绝对定位到屏幕之上，再设置 <code>:focus</code> 样式移下来。就可以实现用户按 tab 时一次只显示一个导航链接，用户再按回车即可跳到该位置。可以参考 Github 网页。</p><h2 id="原生元素"><a href="#原生元素" class="headerlink" title="原生元素"></a>原生元素</h2><p>尽可能使用原生支持的元素，如 <code>&lt;button&gt;</code> ，而不是用 <code>&lt;span&gt;</code> 或 <code>&lt;div&gt;</code> 模拟。原因：</p><ul><li>原生符合语义。</li><li>原生对屏幕阅读友好，否则需要写一堆 ARIA 属性来提示阅读器。</li><li>原生不需要 JS 支持。</li><li>原生元素用键盘可以代替鼠标点击，否则还要监听键盘事件。</li><li>Focus Ring 浏览器自动识别原生元素，否则要另外写 CSS 隐藏。</li><li>CSS 很容易就能改变原生元素的样式，与整体设计统一。</li></ul><h2 id="语义标签"><a href="#语义标签" class="headerlink" title="语义标签"></a>语义标签</h2><p>使用符合语义的标签，辅助设备会自动理解。</p><ul><li><code>&lt;article&gt;</code>：完整、独立的内容。<ul><li>每篇 <code>&lt;article&gt;</code> 应该包含一个标题（<code>&lt;h1&gt;</code>-<code>&lt;h6&gt;</code>）。</li><li>嵌套的 <code>&lt;article&gt;</code> 主题应该跟父 <code>&lt;article&gt;</code> 相关联。</li><li>考虑使用 <code>&lt;article&gt;</code> 应该看其内容是否会出现在文档的提纲（outline）中。</li><li>辅助设备会将其 role 理解为“article”。</li></ul></li><li><code>&lt;section&gt;</code>：按主题归在一起的部分内容。<ul><li>每块 <code>&lt;section&gt;</code> 应该包含一个标题（<code>&lt;h1&gt;</code>-<code>&lt;h6&gt;</code>）。</li><li>如果内容是完整、独立的，应该考虑使用 <code>&lt;article&gt;</code>。</li><li>不要将 <code>&lt;section&gt;</code> 当 <code>&lt;div&gt;</code> 使用，只为样式时应该用 <code>&lt;div&gt;</code> 。</li><li>考虑使用 <code>&lt;article&gt;</code> 应该看其内容是否会出现在文档的提纲（outline）中。</li><li>辅助设备会将其 role 理解为“region”，见<a href="#Landmarks">下方</a>。</li></ul></li><li><code>&lt;nav&gt;</code>：包含一系列链接可以跳转到其它页面或者本页面的某部分。<ul><li>使用列表元素帮助辅助设备理解。</li><li>只为主要导航使用。</li><li>辅助设备会将其 role 理解为“navigation”，见<a href="#Landmarks">下方</a>。</li></ul></li><li><code>&lt;aside&gt;</code>：侧边栏，其内容应该不属于主体内容的一部分。<ul><li>可以放导航组、广告等。</li><li>辅助设备会将其 role 理解为“complementary”，见<a href="#Landmarks">下方</a>。</li></ul></li><li><code>&lt;h1&gt;</code>-<code>&lt;h6&gt;</code>：一个块或子块的标题。<ul><li>请勿为了样式而用标题来表示副标题、子标题、额外标题、标语等，变通可参考<a href="https://www.w3.org/TR/html5/common-idioms-without-dedicated-elements.html#common-idioms-without-dedicated-elements" target="_blank">这里</a>。</li><li>辅助设备会将其 role 理解为“heading”加上“1”到“6”。</li></ul></li><li><code>&lt;header&gt;</code>：对最近的父 <a href="https://www.w3.org/TR/html5/dom.html#sectioning-content" target="_blank">sectioning content</a> 或 <a href="https://www.w3.org/TR/html5/sections.html#sectioning-roots" target="_blank">sectioning root</a> 的介绍。<ul><li>如果父 sectioning root 是 <code>&lt;body&gt;</code>，辅助设备会将其 role 理解为“banner”，见<a href="#Landmarks">下方</a>。</li></ul></li><li><code>&lt;footer&gt;</code>：代表其最近的父 <a href="https://www.w3.org/TR/html5/dom.html#sectioning-content" target="_blank">sectioning content</a> 或 <a href="https://www.w3.org/TR/html5/sections.html#sectioning-roots" target="_blank">sectioning root</a> 的页脚。<ul><li>如果父 sectioning root 是 <code>&lt;body&gt;</code>，辅助设备会将其 role 理解为“content information”，见<a href="#Landmarks">下方</a>。</li></ul></li><li><code>&lt;address&gt;</code>：为其最近的父 <code>&lt;article&gt;</code> 或 <code>&lt;body&gt;</code> 元素提供联系信息。<ul><li>只提供必要的联系信息，不要包含其它信息。</li></ul></li></ul><p><a href="https://www.w3.org/TR/html5/sections.html#sectioning-roots" target="_blank">sectioning root</a> 包括 <code>&lt;blockquote&gt;</code>, <code>&lt;body&gt;</code>, <code>&lt;details&gt;</code>, <code>&lt;fieldset&gt;</code>, <code>&lt;figure&gt;</code>, <code>&lt;td&gt;</code>.</p><p><a href="https://www.w3.org/TR/html5/dom.html#sectioning-content" target="_blank">Sectioning content</a> 包括 <code>&lt;article&gt;</code>, <code>&lt;aside&gt;</code>, <code>&lt;nav&gt;</code>, <code>&lt;section&gt;</code>.</p><h2 id="Landmarks"><a href="#Landmarks" class="headerlink" title="Landmarks"></a>Landmarks</h2><p>Landmark roles 定义网页的几个主要部分，可以让辅助设备快速跳转。</p><p>总共有<a href="https://www.w3.org/TR/wai-aria/#landmark_roles" target="_blank">八个</a>。</p><ol><li><code>banner</code>：<strong>唯一</strong>。跟网站相关的内容，如 logo 、赞助商、站内搜索等。</li><li><code>complementary</code>：对主体内容的补充。与主体内容相关，但本身独立。如相关文章。</li><li><code>contentinfo</code>：<strong>唯一</strong>。版权信息、隐私声明等。</li><li><code>form</code>：表单。<strong>应该</strong>设置可见的标题，并用 <code>aria-labelledby</code> 引用标题来告知辅助设备这个表单是干什么的。如果用 JS 提交表单，没有触发 <code>onsubmit</code> 事件，则<strong>应该</strong>用其它方式通知表单提交。</li><li><code>main</code>：<strong>唯一</strong>。主体内容。</li><li><code>navigation</code>：导航栏。</li><li><code>region</code>：认为用户可能会感兴趣的，需要让用户可以快速跳转的内容。辅助设备会收集它来生成一个概要页面。每个 region <strong>必须</strong>有可见的标题，并用 <code>aria-labelledby</code> 引用。</li><li><code>search</code>：搜索框。</li></ol><h2 id="视觉隐藏"><a href="#视觉隐藏" class="headerlink" title="视觉隐藏"></a>视觉隐藏</h2><p>使用视觉隐藏而不是 <code>display: none;</code> 来隐藏元素同时让辅助设备识别。</p><p></p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.visually-hidden</span> &#123;</span><br><span class="line">  <span class="attribute">position</span>: absolute;</span><br><span class="line">  <span class="attribute">overflow</span>: hidden;</span><br><span class="line">  <span class="attribute">width</span>: <span class="number">1px</span>;</span><br><span class="line">  <span class="attribute">height</span>: <span class="number">1px</span>;</span><br><span class="line">  <span class="attribute">margin</span>: -<span class="number">1px</span>;</span><br><span class="line">  <span class="attribute">padding</span>: <span class="number">0</span>;</span><br><span class="line">  <span class="attribute">border</span>: <span class="number">0</span>;</span><br><span class="line">  <span class="attribute">clip</span>: <span class="built_in">rect</span>(0 0 0 0);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><p>【完】</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;定义&quot;&gt;&lt;a href=&quot;#定义&quot; class=&quot;headerlink&quot; title=&quot;定义&quot;&gt;&lt;/a&gt;定义&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;可访问性 Accessibility&lt;br&gt;为障碍用户提供同等用户体验。使障碍用户能对产品感知、理解、定位和交互，并能
      
    
    </summary>
    
      <category term="Website" scheme="https://blog.crimx.com/categories/Website/"/>
    
    
      <category term="可访问性" scheme="https://blog.crimx.com/tags/%E5%8F%AF%E8%AE%BF%E9%97%AE%E6%80%A7/"/>
    
      <category term="Accessibility" scheme="https://blog.crimx.com/tags/Accessibility/"/>
    
      <category term="语义化" scheme="https://blog.crimx.com/tags/%E8%AF%AD%E4%B9%89%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>获取选择文本所在的句子</title>
    <link href="https://blog.crimx.com/2017/12/02/how-to-get-the-sentence-of-a-selection/"/>
    <id>https://blog.crimx.com/2017/12/02/how-to-get-the-sentence-of-a-selection/</id>
    <published>2017-12-01T16:00:00.000Z</published>
    <updated>2017-12-02T11:04:32.745Z</updated>
    
    <content type="html"><![CDATA[<p>最近收到一个 <a href="https://github.com/crimx/crx-saladict/issues/12" target="_blank">issue</a> 期望能在划词的时候同时保存单词的上下文和来源网址。这个功能其实很久之前就想过，但感觉不好实现一直拖延没做。真做完发现其实并不复杂，完整代码在<a href="https://github.com/crimx/crx-saladict/blob/7a9f7048eb267be308a234000b4bf11f65cfdc01/src/helpers/selection.js#L33-L95" target="_blank">这里</a>，或者继续往下阅读分析。</p><h2 id="原理分析"><a href="#原理分析" class="headerlink" title="原理分析"></a>原理分析</h2><h3 id="获取选择文本"><a href="#获取选择文本" class="headerlink" title="获取选择文本"></a>获取选择文本</h3><p>通过 <code>window.getSelection()</code> 即可获得一个 <code>Selection</code> 对象，再利用 <code>.toString()</code> 即可获得选择的文本。</p><h3 id="锚节点与焦节点"><a href="#锚节点与焦节点" class="headerlink" title="锚节点与焦节点"></a>锚节点与焦节点</h3><p>在 <code>Selection</code> 对象中还保存了两个重要信息，<code>anchorNode</code> 和 <code>focusNode</code>，分别代表选择产生那一刻的节点和选择结束时的节点，而 <code>anchorOffset</code> 和 <code>focusOffset</code> 则保存了选择在这两个节点里的偏移值。</p><p>这时你可能马上就想到第一个方案：这不就好办了么，有了首尾节点和偏移，就可以获取句子的头部和尾部，再把选择文本作为中间，整个句子不就出来了么。</p><p>当然不会这么简单哈<span class="github-emoji" title="stuck_out_tongue" data-src="https://assets-cdn.github.com/images/icons/emoji/unicode/1f61b.png?v8">&#x1f61b;</span>。</p><h3 id="强调一下"><a href="#强调一下" class="headerlink" title="强调一下"></a>强调一下</h3><p>一般情况下，<code>anchorNode</code> 和 <code>focusNode</code> 都是 <code>Text</code> 节点（而且因为这里处理的是文本，所以其它情况也会直接忽略），可以考虑这种情况：</p><p></p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">strong</span>&gt;</span>Saladict<span class="tag">&lt;/<span class="name">strong</span>&gt;</span> is awesome!</span><br></pre></td></tr></table></figure><p></p><p>如果选择的是“awesome”，那么 <code>anchorNode</code> 和 <code>focusNode</code> 都是 <code>is awesome!</code>，所以取不到前面的 “Saladict”。</p><p>另外还有嵌套的情况，也是同样的问题。</p><p></p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Saladict is <span class="tag">&lt;<span class="name">strong</span>&gt;</span><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">"#"</span>&gt;</span>awesome<span class="tag">&lt;/<span class="name">a</span>&gt;</span><span class="tag">&lt;/<span class="name">strong</span>&gt;</span>!</span><br></pre></td></tr></table></figure><p></p><p>所以我们还需要遍历兄弟和父节点来获取完整的句子。</p><h3 id="遍历到哪？"><a href="#遍历到哪？" class="headerlink" title="遍历到哪？"></a>遍历到哪？</h3><p>于是接下就是解决遍历边界的问题了。遍历到什么地方为止呢？我的判断标准是：跳过 inline-level 元素，遇到 block-level 元素为止。而判断一个元素是 inline-level 还是 block-level 最准确的方式应该是用 <code>window.getComputedStyle()</code>。但我认为这么做太重了，也不需要严格的准确性，所以用了常见的 inline 标签来判断。</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> INLINE_TAGS = <span class="keyword">new</span> <span class="built_in">Set</span>([</span><br><span class="line">  <span class="comment">// Inline text semantics</span></span><br><span class="line">  <span class="string">'a'</span>, <span class="string">'abbr'</span>, <span class="string">'b'</span>, <span class="string">'bdi'</span>, <span class="string">'bdo'</span>, <span class="string">'br'</span>, <span class="string">'cite'</span>, <span class="string">'code'</span>, <span class="string">'data'</span>, <span class="string">'dfn'</span>, <span class="string">'em'</span>, <span class="string">'i'</span>,</span><br><span class="line">  <span class="string">'kbd'</span>, <span class="string">'mark'</span>, <span class="string">'q'</span>, <span class="string">'rp'</span>, <span class="string">'rt'</span>, <span class="string">'rtc'</span>, <span class="string">'ruby'</span>, <span class="string">'s'</span>, <span class="string">'samp'</span>, <span class="string">'small'</span>,</span><br><span class="line">  <span class="string">'span'</span>, <span class="string">'strong'</span>, <span class="string">'sub'</span>, <span class="string">'sup'</span>, <span class="string">'time'</span>, <span class="string">'u'</span>, <span class="string">'var'</span>, <span class="string">'wbr'</span></span><br><span class="line">])</span><br></pre></td></tr></table></figure><p></p><h3 id="原理总结"><a href="#原理总结" class="headerlink" title="原理总结"></a>原理总结</h3><p>句子由三块组成，选择文本作为中间，然后遍历兄弟和父节点获取首尾补上。</p><h2 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h2><h3 id="选择文本"><a href="#选择文本" class="headerlink" title="选择文本"></a>选择文本</h3><p>先获取文本，如果没有则退出</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> selection = <span class="built_in">window</span>.getSelection()</span><br><span class="line"><span class="keyword">const</span> selectedText = selection.toString()</span><br><span class="line"><span class="keyword">if</span> (!selectedText.trim()) &#123; <span class="keyword">return</span> <span class="string">''</span> &#125;</span><br></pre></td></tr></table></figure><p></p><h3 id="获取首部"><a href="#获取首部" class="headerlink" title="获取首部"></a>获取首部</h3><p>对于 <code>anchorNode</code> 只考虑 <code>Text</code> 节点，通过 <code>anchorOffset</code> 获取选择在 <code>anchorNode</code> 的前半段内容。</p><p>然后开始补全在 <code>anchorNode</code> 之前的兄弟节点，最后补全在 <code>anchorNode</code> 父元素之前的兄弟元素。注意后面是元素，这样可以减少遍历的次数，而且考虑到一些被隐藏的内容不需要获取，用 <code>innerText</code> 而不是 <code>textContent</code> 属性。</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> sentenceHead = <span class="string">''</span></span><br><span class="line"><span class="keyword">const</span> anchorNode = selection.anchorNode</span><br><span class="line"><span class="keyword">if</span> (anchorNode.nodeType === Node.TEXT_NODE) &#123;</span><br><span class="line">  <span class="keyword">let</span> leadingText = anchorNode.textContent.slice(<span class="number">0</span>, selection.anchorOffset)</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> node = anchorNode.previousSibling; node; node = node.previousSibling) &#123;</span><br><span class="line">    <span class="keyword">if</span> (node.nodeType === Node.TEXT_NODE) &#123;</span><br><span class="line">      leadingText = node.textContent + leadingText</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (node.nodeType === Node.ELEMENT_NODE) &#123;</span><br><span class="line">      leadingText = node.innerText + leadingText</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">for</span> (</span><br><span class="line">    <span class="keyword">let</span> element = anchorNode.parentElement;</span><br><span class="line">    element &amp;&amp; INLINE_TAGS.has(element.tagName.toLowerCase()) &amp;&amp; element !== <span class="built_in">document</span>.body;</span><br><span class="line">    element = element.parentElement</span><br><span class="line">  ) &#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">let</span> el = element.previousElementSibling; el; el = el.previousElementSibling) &#123;</span><br><span class="line">      leadingText = el.innerText + leadingText</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  sentenceHead = (leadingText.match(sentenceHeadTester) || [<span class="string">''</span>])[<span class="number">0</span>]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><p>最后从提取句子首部用的正则是这个</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// match head                 a.b is ok    chars that ends a sentence</span></span><br><span class="line"><span class="keyword">const</span> sentenceHeadTester = <span class="regexp">/((\.(?![ .]))|[^.?!。？！…\r\n])+$/</span></span><br></pre></td></tr></table></figure><p></p><p>前面的 <code>((\.(?![ .]))</code> 主要是为了跳过 <code>a.b</code> 这样的特别是在技术文章中常见的写法。</p><h3 id="获取尾部"><a href="#获取尾部" class="headerlink" title="获取尾部"></a>获取尾部</h3><p>跟首部同理，换成往后遍历。最后的正则保留了标点符号</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// match tail                                                    for "..."</span></span><br><span class="line"><span class="keyword">const</span> sentenceTailTester = <span class="regexp">/^((\.(?![ .]))|[^.?!。？！…\r\n])+(.)\3&#123;0,2&#125;/</span></span><br></pre></td></tr></table></figure><p></p><h2 id="压缩换行"><a href="#压缩换行" class="headerlink" title="压缩换行"></a>压缩换行</h2><p>拼凑完句子之后压缩多个换行为一个空白行，以及删除每行开头结尾的空白符</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">return</span> (sentenceHead + selectedText + sentenceTail)</span><br><span class="line">  .replace(<span class="regexp">/(^\s+)|(\s+$)/gm</span>, <span class="string">'\n'</span>) <span class="comment">// allow one empty line &amp; trim each line</span></span><br><span class="line">  .replace(<span class="regexp">/(^\s+)|(\s+$)/g</span>, <span class="string">''</span>) <span class="comment">// remove heading or tailing \n</span></span><br></pre></td></tr></table></figure><p></p><h2 id="完整代码"><a href="#完整代码" class="headerlink" title="完整代码"></a>完整代码</h2><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> INLINE_TAGS = <span class="keyword">new</span> <span class="built_in">Set</span>([</span><br><span class="line">  <span class="comment">// Inline text semantics</span></span><br><span class="line">  <span class="string">'a'</span>, <span class="string">'abbr'</span>, <span class="string">'b'</span>, <span class="string">'bdi'</span>, <span class="string">'bdo'</span>, <span class="string">'br'</span>, <span class="string">'cite'</span>, <span class="string">'code'</span>, <span class="string">'data'</span>, <span class="string">'dfn'</span>, <span class="string">'em'</span>, <span class="string">'i'</span>,</span><br><span class="line">  <span class="string">'kbd'</span>, <span class="string">'mark'</span>, <span class="string">'q'</span>, <span class="string">'rp'</span>, <span class="string">'rt'</span>, <span class="string">'rtc'</span>, <span class="string">'ruby'</span>, <span class="string">'s'</span>, <span class="string">'samp'</span>, <span class="string">'small'</span>,</span><br><span class="line">  <span class="string">'span'</span>, <span class="string">'strong'</span>, <span class="string">'sub'</span>, <span class="string">'sup'</span>, <span class="string">'time'</span>, <span class="string">'u'</span>, <span class="string">'var'</span>, <span class="string">'wbr'</span></span><br><span class="line">])</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">* @returns &#123;string&#125;</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">getSelectionSentence</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> selection = <span class="built_in">window</span>.getSelection()</span><br><span class="line">  <span class="keyword">const</span> selectedText = selection.toString()</span><br><span class="line">  <span class="keyword">if</span> (!selectedText.trim()) &#123; <span class="keyword">return</span> <span class="string">''</span> &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">var</span> sentenceHead = <span class="string">''</span></span><br><span class="line">  <span class="keyword">var</span> sentenceTail = <span class="string">''</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> anchorNode = selection.anchorNode</span><br><span class="line">  <span class="keyword">if</span> (anchorNode.nodeType === Node.TEXT_NODE) &#123;</span><br><span class="line">    <span class="keyword">let</span> leadingText = anchorNode.textContent.slice(<span class="number">0</span>, selection.anchorOffset)</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">let</span> node = anchorNode.previousSibling; node; node = node.previousSibling) &#123;</span><br><span class="line">      <span class="keyword">if</span> (node.nodeType === Node.TEXT_NODE) &#123;</span><br><span class="line">        leadingText = node.textContent + leadingText</span><br><span class="line">      &#125; <span class="keyword">else</span> <span class="keyword">if</span> (node.nodeType === Node.ELEMENT_NODE) &#123;</span><br><span class="line">        leadingText = node.innerText + leadingText</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (</span><br><span class="line">      <span class="keyword">let</span> element = anchorNode.parentElement;</span><br><span class="line">      element &amp;&amp; INLINE_TAGS.has(element.tagName.toLowerCase()) &amp;&amp; element !== <span class="built_in">document</span>.body;</span><br><span class="line">      element = element.parentElement</span><br><span class="line">    ) &#123;</span><br><span class="line">      <span class="keyword">for</span> (<span class="keyword">let</span> el = element.previousElementSibling; el; el = el.previousElementSibling) &#123;</span><br><span class="line">        leadingText = el.innerText + leadingText</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    sentenceHead = (leadingText.match(sentenceHeadTester) || [<span class="string">''</span>])[<span class="number">0</span>]</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> focusNode = selection.focusNode</span><br><span class="line">  <span class="keyword">if</span> (selection.focusNode.nodeType === Node.TEXT_NODE) &#123;</span><br><span class="line">    <span class="keyword">let</span> tailingText = selection.focusNode.textContent.slice(selection.focusOffset)</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">let</span> node = focusNode.nextSibling; node; node = node.nextSibling) &#123;</span><br><span class="line">      <span class="keyword">if</span> (node.nodeType === Node.TEXT_NODE) &#123;</span><br><span class="line">        tailingText += node.textContent</span><br><span class="line">      &#125; <span class="keyword">else</span> <span class="keyword">if</span> (node.nodeType === Node.ELEMENT_NODE) &#123;</span><br><span class="line">        tailingText += node.innerText</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (</span><br><span class="line">      <span class="keyword">let</span> element = focusNode.parentElement;</span><br><span class="line">      element &amp;&amp; INLINE_TAGS.has(element.tagName.toLowerCase()) &amp;&amp; element !== <span class="built_in">document</span>.body;</span><br><span class="line">      element = element.parentElement</span><br><span class="line">    ) &#123;</span><br><span class="line">      <span class="keyword">for</span> (<span class="keyword">let</span> el = element.nextElementSibling; el; el = el.nextElementSibling) &#123;</span><br><span class="line">        tailingText += el.innerText</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    sentenceTail = (tailingText.match(sentenceTailTester) || [<span class="string">''</span>])[<span class="number">0</span>]</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (sentenceHead + selectedText + sentenceTail)</span><br><span class="line">    .replace(<span class="regexp">/(^\s+)|(\s+$)/gm</span>, <span class="string">'\n'</span>) <span class="comment">// allow one empty line &amp; trim each line</span></span><br><span class="line">    .replace(<span class="regexp">/(^\s+)|(\s+$)/g</span>, <span class="string">''</span>) <span class="comment">// remove heading or tailing \n</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><p>【完】</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;最近收到一个 &lt;a href=&quot;https://github.com/crimx/crx-saladict/issues/12&quot; target=&quot;_blank&quot;&gt;issue&lt;/a&gt; 期望能在划词的时候同时保存单词的上下文和来源网址。这个功能其实很久之前就想过，但感觉不好实现
      
    
    </summary>
    
      <category term="JavaScript" scheme="https://blog.crimx.com/categories/JavaScript/"/>
    
    
      <category term="JavaScript" scheme="https://blog.crimx.com/tags/JavaScript/"/>
    
      <category term="Selection" scheme="https://blog.crimx.com/tags/Selection/"/>
    
      <category term="Sentence" scheme="https://blog.crimx.com/tags/Sentence/"/>
    
  </entry>
  
  <entry>
    <title>React Native 搭配 MobX 使用心得</title>
    <link href="https://blog.crimx.com/2017/11/13/react-native-with-mobx/"/>
    <id>https://blog.crimx.com/2017/11/13/react-native-with-mobx/</id>
    <published>2017-11-12T16:00:00.000Z</published>
    <updated>2017-11-13T12:18:33.761Z</updated>
    
    <content type="html"><![CDATA[<p>MobX 是一款十分优秀的状态管理库，不但书写简洁还非常高效。当然这是我在使用之后才体会到的，当初试水上车的主要原因是响应式，考虑到可能会更符合 Vue 过来的思考方式。然而其实两者除了响应式以外并没有什么相似之处<span class="github-emoji" title="joy" data-src="https://assets-cdn.github.com/images/icons/emoji/unicode/1f602.png?v8">&#x1f602;</span>。</p><p>在使用过程中走了不少弯路，一部分是因为当时扫两眼文档就动手，对 MobX 机制理解得不够；其它原因是 MobX 终究只是一个库，会受限于 React 机制，以及与其它非 MobX 管理组件的兼容问题。当中很多情况在文档已经给出了说明（<a href="https://mobx.js.org/best/react.html" target="_blank">这里</a>和<a href="https://mobx.js.org/best/react-performance.html" target="_blank">这里</a>），我根据自己遇到的再做一番总结。</p><h2 id="与非响应式组件兼容问题"><a href="#与非响应式组件兼容问题" class="headerlink" title="与非响应式组件兼容问题"></a>与非响应式组件兼容问题</h2><p>与非响应式的组件一起工作时，MobX 有时需要为它们提供一份非响应式的数据副本，以免 observable 被其它组件修改。</p><h3 id="observable-ref"><a href="#observable-ref" class="headerlink" title="observable.ref"></a>observable.ref</h3><p>使用 React Navigation 导航时，如果要交由 MobX 管理，则需要手动配置导航状态栈，此时用 <code>@observable.ref</code> “浅观察”可避免状态被 React Navigation 修改时触发 MobX 警告。</p><p>当 Navigator 接受 <code>navigation</code> props 时代表导航状态为手动管理。</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; addNavigationHelpers, StackNavigator &#125; <span class="keyword">from</span> <span class="string">'react-navigation'</span></span><br><span class="line"><span class="keyword">import</span> &#123; observable, action &#125; <span class="keyword">from</span> <span class="string">'mobx'</span></span><br><span class="line"><span class="keyword">import</span> &#123; Provider, observer &#125; <span class="keyword">from</span> <span class="string">'mobx-react'</span></span><br><span class="line"><span class="keyword">import</span> AppComp <span class="keyword">from</span> <span class="string">'./AppComp'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> AppNavigator = StackNavigator(&#123;</span><br><span class="line">  App: &#123; <span class="attr">screen</span>: AppComp &#125;,</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;, &#123;</span><br><span class="line">  initialRouteName: <span class="string">'App'</span>,</span><br><span class="line">  headerMode: <span class="string">'none'</span></span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">@observer</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="class"><span class="keyword">class</span> <span class="title">AppNavigation</span> <span class="keyword">extends</span> <span class="title">Component</span> </span>&#123;</span><br><span class="line">  @observable.ref navigationState = &#123;</span><br><span class="line">    index: <span class="number">0</span>,</span><br><span class="line">    routes: [</span><br><span class="line">      &#123; <span class="attr">key</span>: <span class="string">'App'</span>, <span class="attr">routeName</span>: <span class="string">'App'</span> &#125;</span><br><span class="line">    ],</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  @action.bound dispatchNavigation = <span class="function">(<span class="params">action, stackNavState = <span class="literal">true</span></span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> previousNavState = stackNavState ? <span class="keyword">this</span>.navigationState : <span class="literal">null</span></span><br><span class="line">    <span class="keyword">this</span>.navigationState = <span class="keyword">this</span>.AppNavigator.router.getStateForAction(action, previousNavState)</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">this</span>.navigationState</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  render () &#123;</span><br><span class="line">    <span class="keyword">return</span> (</span><br><span class="line">      &lt;Provider</span><br><span class="line">        dispatchNavigation=&#123;<span class="keyword">this</span>.dispatchNavigation&#125;</span><br><span class="line">        navigationState=&#123;<span class="keyword">this</span>.navigationState&#125;</span><br><span class="line">      &gt;</span><br><span class="line">        &lt;AppNavigator navigation=&#123;addNavigationHelpers(&#123;</span><br><span class="line">          dispatch: <span class="keyword">this</span>.dispatchNavigation,</span><br><span class="line">          state: <span class="keyword">this</span>.navigationState,</span><br><span class="line">        &#125;)&#125; /&gt;</span><br><span class="line">      &lt;<span class="regexp">/Provider&gt;</span></span><br><span class="line"><span class="regexp">    )</span></span><br><span class="line"><span class="regexp">  &#125;</span></span><br><span class="line"><span class="regexp">&#125;</span></span><br></pre></td></tr></table></figure><p></p><h3 id="observable-shallowArray-与-observable-shallowMap"><a href="#observable-shallowArray-与-observable-shallowMap" class="headerlink" title="observable.shallowArray() 与 observable.shallowMap()"></a><code>observable.shallowArray()</code> 与 <code>observable.shallowMap()</code></h3><p>MobX 还提供其它方便的数据结构来存放非响应式数据。</p><p>比如使用 <code>SectionList</code> 的时候，我们要为其提供数据用于生成列表，由于 Native 官方的实现跟 MobX 不兼容，这个数据不能是响应式的，不然 MobX 会报一堆警告。</p><p>MobX 有个 <code>mobx.toJS()</code> 方法可以导出非响应式副本；如果结构不相同还可以使用 <code>@computed</code> 自动生成符合的数据。但这两个方法每次添加项目都要全部遍历一遍，可能会存在性能问题。</p><p>这时其实可以维护一个 <code>observable.shallowArray</code>，里面只放 <code>key</code> 数据，只用于生成列表（像骨架一样）。传给 <code>SectionList</code> 的 <code>sections</code> props 时 <code>slice</code> 数组复制副本（shallowArray 里的数据非响应式，所以只需浅复制，复杂度远小于上面两种方式）。</p><p>然后 store 维护一个 <code>observable.map</code> 来存放每个项的数据，在项（item）组件中 <code>inject</code> store 进去，再利用 <code>key</code> 从 map 中获取数据来填充。</p><p>通过 shallowArray 可以让 MobX 识别列表长度变化自动更新列表，利用 map 维护项数据可以使每个项保持响应式却互不影响，对长列表优化效果很明显。</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// store comp</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MyStore</span> </span>&#123;</span><br><span class="line">  @observable sections = observable.shallowArray()</span><br><span class="line">  @observable itemData = observable.map()</span><br><span class="line"></span><br><span class="line">  @action.bound appendSection (section) &#123;</span><br><span class="line">    <span class="keyword">const</span> data = []</span><br><span class="line">    section.items.forEach(action(<span class="function"><span class="params">item</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">this</span>.itemData.set(item.id, item)</span><br><span class="line">      data.push(&#123;<span class="attr">key</span>: item.id&#125;)</span><br><span class="line">    &#125;))</span><br><span class="line">    <span class="keyword">this</span>.sections.push(&#123;</span><br><span class="line">      key: section.id,</span><br><span class="line">      data</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// MyList comp</span></span><br><span class="line"><span class="keyword">import</span> &#123; SectionList &#125; <span class="keyword">from</span> <span class="string">'react-native'</span></span><br><span class="line">@inject(<span class="string">'myStore'</span>)</span><br><span class="line">@observer</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MyList</span> <span class="keyword">extends</span> <span class="title">React</span>.<span class="title">Component</span> </span>&#123;</span><br><span class="line">  _renderItem = <span class="function">(<span class="params">&#123;item&#125;</span>) =&gt;</span> &lt;SectionItem id=&#123;item.key&#125; /&gt;</span><br><span class="line"></span><br><span class="line">  render () &#123;</span><br><span class="line">    <span class="keyword">return</span> (</span><br><span class="line">      &lt;SectionList</span><br><span class="line">        getItemLayout=&#123;<span class="keyword">this</span>._getItemLayout&#125;</span><br><span class="line">        sections=&#123;<span class="keyword">this</span>.props.myStore.sections.slice()&#125;</span><br><span class="line">        renderSectionHeader=&#123;<span class="keyword">this</span>._renderSectionHeader&#125;</span><br><span class="line">        renderItem=&#123;<span class="keyword">this</span>._renderItem&#125;</span><br><span class="line">      /&gt;</span><br><span class="line">    )</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// SectionItem comp</span></span><br><span class="line">@inject(<span class="string">'myStore'</span>)</span><br><span class="line">@observer</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">SectionItem</span> <span class="keyword">extends</span> <span class="title">React</span>.<span class="title">Component</span> </span>&#123;</span><br><span class="line">  render () &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123;myStore, id&#125; = <span class="keyword">this</span>.props</span><br><span class="line">    <span class="keyword">const</span> itemData = myStore.itemData.get(id)</span><br><span class="line">    <span class="keyword">return</span> (</span><br><span class="line">      &lt;Text&gt;&#123;itemData.title&#125;&lt;<span class="regexp">/Text&gt;</span></span><br><span class="line"><span class="regexp">    )</span></span><br><span class="line"><span class="regexp">  &#125;</span></span><br><span class="line"><span class="regexp">&#125;</span></span><br></pre></td></tr></table></figure><p></p><h2 id="computed"><a href="#computed" class="headerlink" title="computed"></a>computed</h2><p>利用 <code>@computed</code> 缓存数据可以做一些优化。</p><p>比如有一个响应式的数组 <code>arr</code>，一个组件要根据 <code>arr</code> 是否为空更新。如果直接访问 <code>arr.length</code>，那么只要数组长度发生变化，这个组件都要 render 一遍。</p><p>此时利用 computed 生成，组件只需要判断 <code>isArrEmpty</code> 就可以减少不必要的更新：</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">@computed get isArrEmpty () &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">this</span>.arr.length &lt;= <span class="number">0</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><h2 id="observable-map"><a href="#observable-map" class="headerlink" title="observable.map"></a>observable.map</h2><p>因 JS 机制 MobX 不能检测属性的增删，所以最好用 <code>observable.map</code> 取代简单 <code>{}</code> 对象。另外 MobX 没有提供 Set 支持，可以用 key 和 value 一样的 Map 代替。</p><h2 id="避免在父组件中访问子组件的属性"><a href="#避免在父组件中访问子组件的属性" class="headerlink" title="避免在父组件中访问子组件的属性"></a>避免在父组件中访问子组件的属性</h2><p>这条规则在文档也提到，原因很简单，MobX 对于一个 <code>observer</code> 组件，是通过访问属性来记录依赖的。所以哪怕父组件里没有用到这个属性，只是为了作为 props 传给子组件，MobX 还是算它依赖了这个属性，于是会产生不必要的更新。最好的方式是将数据统一放在 store 中，子组件通过 <code>inject</code> store 方式获取数据。</p><h2 id="小组件"><a href="#小组件" class="headerlink" title="小组件"></a>小组件</h2><p>由于 React 的机制，MobX 只能在组件层面发光发热，对于组件内部就无能为力了。所以大组件用 MobX 很容易卡死（用其它也会<span class="github-emoji" title="sweat_smile" data-src="https://assets-cdn.github.com/images/icons/emoji/unicode/1f605.png?v8">&#x1f605;</span>），小组件才能真正发挥 MobX 自动管理更新的优势。</p><p>【完】</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;MobX 是一款十分优秀的状态管理库，不但书写简洁还非常高效。当然这是我在使用之后才体会到的，当初试水上车的主要原因是响应式，考虑到可能会更符合 Vue 过来的思考方式。然而其实两者除了响应式以外并没有什么相似之处&lt;span class=&quot;github-emoji&quot; tit
      
    
    </summary>
    
      <category term="JavaScript" scheme="https://blog.crimx.com/categories/JavaScript/"/>
    
    
      <category term="JavaScript" scheme="https://blog.crimx.com/tags/JavaScript/"/>
    
      <category term="React" scheme="https://blog.crimx.com/tags/React/"/>
    
      <category term="React Native" scheme="https://blog.crimx.com/tags/React-Native/"/>
    
      <category term="MobX" scheme="https://blog.crimx.com/tags/MobX/"/>
    
  </entry>
  
  <entry>
    <title>检测 DOM 结点插入</title>
    <link href="https://blog.crimx.com/2017/09/20/new-dom-nodes-detection/"/>
    <id>https://blog.crimx.com/2017/09/20/new-dom-nodes-detection/</id>
    <published>2017-09-19T16:00:00.000Z</published>
    <updated>2017-09-20T12:16:42.863Z</updated>
    
    <content type="html"><![CDATA[<p>闲逛 Github 时碰见一个叫 SentinelJS 的库，声称能检测 DOM 结点的插入，顿时引起了好奇。因为以前无聊时也想过一下，没什么头绪，便不了了之。当时第一反应是该不会用轮询吧（比这粗暴的实现也不是没见过）。但看到 682 bytes (minified + gzipped) 大小时感觉一定又是用了什么奇淫怪巧，个人对这种东西很感兴趣（见另一篇<a href="//blog.crimx.com/2017/07/15/element-onresize/" target="_blank">《巧妙监测元素尺寸变化》</a>），便顺便看了看源码，很短，但一看到<code>animation</code> 时便拍大腿了！通过检测 animationstart 事件来检测插入，机智！</p><p>代码很短，就是维护了一个事件队列。核心在 <a href="https://github.com/muicss/sentineljs/blob/master/src/sentinel.js#L37" target="_blank"><code>onFn</code></a> 和 <a href="https://github.com/muicss/sentineljs/blob/master/src/sentinel.js#L87" target="_blank"><code>offFn</code></a> 上。后者同理，便主要看 <code>onFn</code>的实现。</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Add watcher.</span></span><br><span class="line"><span class="comment"> * @param &#123;array&#125; cssSelectors - List of CSS selector strings</span></span><br><span class="line"><span class="comment"> * @param &#123;Function&#125; callback - The callback function</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">onFn</span>(<span class="params">cssSelectors, callback, extraAnimations</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (!callback) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// initialize animationstart event listener</span></span><br><span class="line">  <span class="keyword">if</span> (!isInitialized) init();</span><br><span class="line"></span><br><span class="line">  <span class="comment">// listify argument</span></span><br><span class="line">  cssSelectors = <span class="built_in">Array</span>.isArray(cssSelectors) ? cssSelectors : [cssSelectors];</span><br><span class="line"></span><br><span class="line">  <span class="comment">// add css rules and cache callbacks</span></span><br><span class="line">  cssSelectors.map(<span class="function"><span class="keyword">function</span>(<span class="params">selector</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> animId = selectorToAnimationMap[selector];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!animId) &#123;</span><br><span class="line">      <span class="comment">// add new CSS listener</span></span><br><span class="line">      <span class="keyword">var</span> css, i;</span><br><span class="line"></span><br><span class="line">      animId = <span class="string">'sentinel-'</span> + <span class="built_in">Math</span>.random().toString(<span class="number">16</span>).slice(<span class="number">2</span>);</span><br><span class="line"></span><br><span class="line">      <span class="comment">// add keyframe rule</span></span><br><span class="line">      css = <span class="string">'@keyframes '</span> + animId +</span><br><span class="line">        <span class="string">'&#123;from&#123;transform:none;&#125;to&#123;transform:none;&#125;&#125;'</span>;</span><br><span class="line">      i = styleSheet.cssRules.length;</span><br><span class="line">      styleSheet.insertRule(css, i);</span><br><span class="line">      styleSheet.cssRules[i]._id = selector;</span><br><span class="line"></span><br><span class="line">      <span class="comment">// add selector animation rule</span></span><br><span class="line">      css = selector + <span class="string">'&#123;animation-duration:0.0001s;animation-name:'</span> + animId;</span><br><span class="line">      <span class="keyword">if</span> (extraAnimations) css += <span class="string">','</span> + extraAnimations;</span><br><span class="line">      css += <span class="string">';&#125;'</span>;</span><br><span class="line">      i += <span class="number">1</span>;</span><br><span class="line">      styleSheet.insertRule(css, i);</span><br><span class="line">      styleSheet.cssRules[i]._id = selector;</span><br><span class="line"></span><br><span class="line">      <span class="comment">// add to map</span></span><br><span class="line">      selectorToAnimationMap[selector] = animId;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// add to callbacks</span></span><br><span class="line">    <span class="keyword">var</span> x = animationCallbacks[animId] = animationCallbacks[animId] || [];</span><br><span class="line">    x.push(callback);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><p>先生成一个随机的带前缀的 <code>animId</code> 来区分每个事件。</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">animId = <span class="string">'sentinel-'</span> + <span class="built_in">Math</span>.random().toString(<span class="number">16</span>).slice(<span class="number">2</span>);</span><br></pre></td></tr></table></figure><p></p><p><code>styleSheet</code> 为一个事先挂载的 <code>&lt;style&gt;</code> 元素，所有的 animation 样式都会插入到这里。</p><p>插入 <code>@keyframes</code> 之后在这条规则上面以选择器做了一个私有的标记 <code>_id</code>，为了在移除事件的时候找到这条规则。</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// add keyframe rule</span></span><br><span class="line">      css = <span class="string">'@keyframes '</span> + animId +</span><br><span class="line">        <span class="string">'&#123;from&#123;transform:none;&#125;to&#123;transform:none;&#125;&#125;'</span>;</span><br><span class="line">      i = styleSheet.cssRules.length;</span><br><span class="line">      styleSheet.insertRule(css, i);</span><br><span class="line">      styleSheet.cssRules[i]._id = selector;</span><br></pre></td></tr></table></figure><p></p><p>接下来再插入一个持续 <code>0.0001s</code> 的动画，监听搞定。</p><p>接下来初始化的时候在 <code>document</code> 上<a href="https://github.com/muicss/sentineljs/blob/master/src/sentinel.js#L18" target="_blank">监听</a> <code>animationstart</code> 事件：</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">doc.addEventListener(event, animationStartHandler, <span class="literal">true</span>);</span><br></pre></td></tr></table></figure><p></p><p>通过事件的 <code>animationName</code> 来匹配 <code>animId</code>，成功则取消其它监听这个事件的回调。</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Animation start handler</span></span><br><span class="line"><span class="comment"> * @param &#123;Event&#125; ev - The DOM event</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">animationStartHandler</span>(<span class="params">ev</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">var</span> callbacks = animationCallbacks[ev.animationName] || [],</span><br><span class="line">      l = callbacks.length;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// exit if a callback hasn't been registered</span></span><br><span class="line">  <span class="keyword">if</span> (!l) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// stop other callbacks from firing</span></span><br><span class="line">  ev.stopImmediatePropagation();</span><br><span class="line"></span><br><span class="line">  <span class="comment">// iterate through callbacks</span></span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">var</span> i=<span class="number">0</span>; i &lt; l; i++) callbacks[i](ev.target);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><p>这个 <code>stopImmediatePropagation</code> 学到了！结合 window 上用 capture 方式监听事件，就可以保证点击植入的特定元素不会受其它事件影响。马上用在了这个 <a href="https://chrome.google.com/webstore/detail/rarbg-monitor/kkgcfdmlnfpdjmnheeojdlgpmhaeekga" target="_blank">rarbg-monitor</a> 扩展上，在 rarbg 资源页面上放一个跳转豆瓣相应页面的按钮，优先级比它的一个全局广告要高。</p><p>-EOF-</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;闲逛 Github 时碰见一个叫 SentinelJS 的库，声称能检测 DOM 结点的插入，顿时引起了好奇。因为以前无聊时也想过一下，没什么头绪，便不了了之。当时第一反应是该不会用轮询吧（比这粗暴的实现也不是没见过）。但看到 682 bytes (minified + g
      
    
    </summary>
    
      <category term="JavaScript" scheme="https://blog.crimx.com/categories/JavaScript/"/>
    
    
      <category term="闲读源码" scheme="https://blog.crimx.com/tags/%E9%97%B2%E8%AF%BB%E6%BA%90%E7%A0%81/"/>
    
  </entry>
  
  <entry>
    <title>Prototype 与 __Proto__ 的爱恨情仇</title>
    <link href="https://blog.crimx.com/2017/08/12/proto-prototype/"/>
    <id>https://blog.crimx.com/2017/08/12/proto-prototype/</id>
    <published>2017-08-11T16:00:00.000Z</published>
    <updated>2017-08-13T02:51:28.398Z</updated>
    
    <content type="html"><![CDATA[<p>经历了上次的<a href="/2016/05/12/understanding-this/">《JavaScript This 的六道坎》</a> 发现编故事有点上瘾，而且记忆效果也不错哈哈，今天继续唠叨一下 prototype 与 <code>__proto__</code> 的爱恨情仇。</p><p>先理解两者的一个本质区别，<code>prototype</code> 是函数独有的，是人为设定的；<code>__proto__</code> 是所有对象都有的，是继承的。</p><p>然后来看一个两个神的故事：</p><p>首先在 ECMAScript 星球，万物起源于 the Engineers，哦不，是一个叫 <a href="http://www.ecma-international.org/ecma-262/7.0/#sec-properties-of-the-object-prototype-object" target="_blank">%ObjectPrototype%</a> 的 intrinsic object，也就是 <strong>Object.prototype</strong>。它是万物的尽头，继承于虚无， <code>Object.prototype.__proto__</code>为 <code>null</code>。</p><p><img src="/images/post/object-prototype.jpg" alt="objectprototype"></p><p>接着由其衍生出第二神，另外一个 intrinsic object <a href="http://www.ecma-international.org/ecma-262/7.0/#sec-properties-of-the-function-prototype-object" target="_blank">%FunctionPrototype%</a>，也就是 <strong>Function.prototype</strong>。于是有</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">Function</span>.prototype.__proto__ === <span class="built_in">Object</span>.prototype <span class="comment">// true</span></span><br></pre></td></tr></table></figure><p></p><p>Function.prototype 本身也是个函数对象，这是为了<a href="http://www.ecma-international.org/ecma-262/7.0/#sec-properties-of-the-function-prototype-object" target="_blank">兼容 ES5</a>。也估计是让人引起误解的源头。但两者还是不同的，这是个特殊的函数对象，它忽略参数总是返回 undefined，且没有 [[Construct]] 内部方法。</p><p>搞清楚了这两个 Ancient Gods 接下来就很容易了，相信也听过“函数在 JS 里是一等公民”这类的说法，其实是因为它们都是 %FunctionPrototype% 的子民（这里不用 Function.prototype 是为了避免混淆，记得 prototype 是人为设定的），包括 <code>Function</code> 本身。</p><p>所以你可以看到，<code>Object</code>、<code>Function</code>、<code>String</code>、<code>Number</code>、<code>Boolean</code> 等等等的 <code>__proto__</code> 都是 <code>Function.prototype</code>。</p><p>所以接下来的问题就更容易了，比如 <code>Object instanceof Object</code>。前面我们知道 <code>Object.__proto__</code> 是 %FunctionPrototype%，而它的 <code>__proto__</code> 是万物之源 %ObjectPrototype%，恰好也是 <code>Object.prototype</code>，所以就是 <code>true</code> 啦。</p><p>其它的也是同理，举一反三很简单了。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;经历了上次的&lt;a href=&quot;/2016/05/12/understanding-this/&quot;&gt;《JavaScript This 的六道坎》&lt;/a&gt; 发现编故事有点上瘾，而且记忆效果也不错哈哈，今天继续唠叨一下 prototype 与 &lt;code&gt;__proto__&lt;/co
      
    
    </summary>
    
      <category term="JavaScript" scheme="https://blog.crimx.com/categories/JavaScript/"/>
    
    
      <category term="Understanding JavaScript" scheme="https://blog.crimx.com/tags/Understanding-JavaScript/"/>
    
      <category term="Recommended" scheme="https://blog.crimx.com/tags/Recommended/"/>
    
      <category term="闲读规范" scheme="https://blog.crimx.com/tags/%E9%97%B2%E8%AF%BB%E8%A7%84%E8%8C%83/"/>
    
  </entry>
  
  <entry>
    <title>巧妙监测元素尺寸变化</title>
    <link href="https://blog.crimx.com/2017/07/15/element-onresize/"/>
    <id>https://blog.crimx.com/2017/07/15/element-onresize/</id>
    <published>2017-07-14T16:00:00.000Z</published>
    <updated>2017-07-16T16:26:27.449Z</updated>
    
    <content type="html"><![CDATA[<p>在往下读之前不妨先想一下，你会怎么实现？如何知道元素的尺寸发生变化了？</p><p>相信很多人第一反应是 resize 事件，但这个只是 document view 变化才会触发。</p><p>然后就是轮询，反复查询值变化了没有。开销不是一般的大，但像这样的库（比如这个<a href="https://github.com/cowboy/jquery-resize" target="_blank">七年前的</a>）现在还有人用。</p><p>最后便是<a href="https://github.com/marcj/css-element-queries" target="_blank">这个</a>，号称 event based 无性能问题，便去观摩了一番源码。代码本身没什么惊喜，所以本文不会像<a href="/tags/%E9%97%B2%E8%AF%BB%E6%BA%90%E7%A0%81/">之前</a>一样逐行逐块地分析，而是着重原理，对应<a href="https://github.com/marcj/css-element-queries/blob/master/src/ResizeSensor.js#L100" target="_blank">这部分</a>的源码。</p><h2 id="整体思路"><a href="#整体思路" class="headerlink" title="整体思路"></a>整体思路</h2><p>这个方法的主要思想是在被监测元素里包裹一个跟元素位置大小相同的隐藏块。隐藏块可以滚动，并有一个远远大于它的子元素。当被监测元素尺寸变化时期望能触发隐藏块的滚动事件。</p><p>这个方法听起来很简单是不是，但如果你直接这么实现会发现时而行时而不行，问题就在于触发滚动事件的条件。</p><h2 id="如何计算滚动？"><a href="#如何计算滚动？" class="headerlink" title="如何计算滚动？"></a>如何计算滚动？</h2><p>这是我觉得这个话题值得写成文章的一个有趣点。我们理所当然地看待滚动，但有没有想过它是怎么计算的呢？</p><h3 id="发生滚动的时机"><a href="#发生滚动的时机" class="headerlink" title="发生滚动的时机"></a>发生滚动的时机</h3><p>有问题当然要去请教规范老师。</p><p>第一步我们需要知道什么时候才会发生滚动，首先一个题外话，overflow 为 hidden 也是可以滚动的，<a href="/2016/04/18/document-scrollingelement-polyfill/">另外一篇</a>分析里也遇到过。</p><p>在<a href="https://www.w3.org/TR/cssom-view-1/#scrolling-events" target="_blank">这里</a>提到滚动事件发生的时机，但说得有点笼统</p><blockquote><p>Whenever an element gets scrolled (whether in response to user interaction or by an API)</p></blockquote><p>但从这段我们可以知道，每次发生滚动的时候，浏览器会先收集起来，在下次 event loop 到达时<a href="https://html.spec.whatwg.org/multipage/webappapis.html#update-the-rendering" target="_blank">统一</a>地<a href="https://www.w3.org/TR/cssom-view-1/#run-the-scroll-steps" target="_blank">处理</a>。</p><p>滚动的描述在<a href="https://www.w3.org/TR/cssom-view-1/#scroll-an-element" target="_blank">这里</a>：</p><blockquote><p>To scroll an element element to x,y optionally with a scroll behavior behavior (which is &quot;auto&quot; if omitted) means to:</p><ol><li>Let box be element’s associated scrolling box.</li><li><ul><li><strong>If box has rightward overflow direction</strong> Let x be max(0, min(x, element scrolling area width - element padding edge width)).</li><li>If box has leftward overflow direction Let x be min(0, max(x, element padding edge width - element scrolling area width)).</li></ul></li><li><ul><li><strong>If box has downward overflow direction</strong> Let y be max(0, min(y, element scrolling area height - element padding edge height)).</li><li>If box has upward overflow direction Let y be min(0, max(y, element padding edge height - element scrolling area height)).</li></ul></li><li>Let position be the scroll position box would have by aligning scrolling area x-coordinate x with the left of box and aligning scrolling area y-coordinate y with the top of box.</li><li>If position is the <strong>same as</strong> box’s current scroll position, and box does not have an ongoing smooth scroll, <strong>abort these steps</strong>.</li><li><strong>Perform a scroll</strong> of box to position, element as the associated element and behavior as the scroll behavior.</li></ol></blockquote><p>最后一步<a href="https://www.w3.org/TR/cssom-view-1/#perform-a-scroll" target="_blank">“perform a scroll”</a>才会真正触发滚动事件。</p><p>第五步便是问题关键，位置相同的时候，滚动事件不会发生。</p><h3 id="重排"><a href="#重排" class="headerlink" title="重排"></a>重排</h3><p>根据前面的整体思路，当被监测元素尺寸发生变化时，隐藏元素也跟着变化。于是引发了 Layout/Reflow 使到重新计算滚动位置 <em>position</em>。</p><p>但这时也许你会发现 <em>position</em> 根本没有变化，如图一。</p><p><img src="/images/post/element-onresize/1.gif" alt="图一"></p><p></p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">style</span>&gt;</span><span class="undefined"></span></span><br><span class="line"><span class="css">  <span class="selector-class">.parent</span> &#123;</span></span><br><span class="line"><span class="undefined">    height: 100px;</span></span><br><span class="line"><span class="undefined">    width: 100px;</span></span><br><span class="line"><span class="undefined">    overflow: scroll;</span></span><br><span class="line"><span class="undefined">    background: red;</span></span><br><span class="line"><span class="undefined">    position: absolute;</span></span><br><span class="line"><span class="undefined">    top: 0;</span></span><br><span class="line"><span class="undefined">    left: 0;</span></span><br><span class="line"><span class="undefined">    bottom: 0;</span></span><br><span class="line"><span class="undefined">    right: 0;</span></span><br><span class="line"><span class="undefined">    margin: auto;</span></span><br><span class="line"><span class="undefined">  &#125;</span></span><br><span class="line"><span class="undefined"></span></span><br><span class="line"><span class="css">  <span class="selector-class">.child</span> &#123;</span></span><br><span class="line"><span class="undefined">    height: 200%;</span></span><br><span class="line"><span class="undefined">    width: 200%;</span></span><br><span class="line"><span class="undefined">    background: blue;</span></span><br><span class="line"><span class="undefined">  &#125;</span></span><br><span class="line"><span class="undefined"></span></span><br><span class="line"><span class="css">  <span class="selector-class">.msg</span> &#123;</span></span><br><span class="line"><span class="undefined">    position: absolute;</span></span><br><span class="line"><span class="undefined">    top: 370px;</span></span><br><span class="line"><span class="undefined">    left: 50%;</span></span><br><span class="line"><span class="undefined">    transform: translateX(-50%);</span></span><br><span class="line"><span class="undefined">    text-align: center;</span></span><br><span class="line"><span class="undefined">  &#125;</span></span><br><span class="line"><span class="undefined"></span><span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"parent"</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"child"</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"msg"</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"msg1"</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"msg2"</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="undefined"></span></span><br><span class="line"><span class="javascript">  <span class="keyword">var</span> parent = <span class="built_in">document</span>.querySelector(<span class="string">'.parent'</span>)</span></span><br><span class="line"><span class="javascript">  <span class="keyword">var</span> child = <span class="built_in">document</span>.querySelector(<span class="string">'.child'</span>)</span></span><br><span class="line"><span class="javascript">  <span class="keyword">var</span> msg1 = <span class="built_in">document</span>.querySelector(<span class="string">'.msg1'</span>)</span></span><br><span class="line"><span class="javascript">  <span class="keyword">var</span> msg2 = <span class="built_in">document</span>.querySelector(<span class="string">'.msg2'</span>)</span></span><br><span class="line"><span class="undefined"></span></span><br><span class="line"><span class="undefined">  parent.scrollTop = 1000</span></span><br><span class="line"><span class="undefined">  parent.scrollLeft = 1000</span></span><br><span class="line"><span class="undefined"></span></span><br><span class="line"><span class="javascript">  <span class="keyword">var</span> eventCount = <span class="number">0</span></span></span><br><span class="line"><span class="javascript">  <span class="function"><span class="keyword">function</span> <span class="title">log</span> (<span class="params"></span>) </span>&#123;</span></span><br><span class="line"><span class="javascript">    msg1.innerText = <span class="string">`scrollTop: <span class="subst">$&#123;parent.scrollTop&#125;</span>, scrollLeft: <span class="subst">$&#123;parent.scrollLeft&#125;</span>`</span></span></span><br><span class="line"><span class="javascript">    msg2.innerText = <span class="string">`<span class="subst">$&#123;eventCount&#125;</span> scroll event<span class="subst">$&#123;eventCount &gt; <span class="number">1</span> ? <span class="string">'s were'</span> : <span class="string">' was'</span>&#125;</span> triggered`</span></span></span><br><span class="line"><span class="undefined">  &#125;</span></span><br><span class="line"><span class="undefined"></span></span><br><span class="line"><span class="undefined">  log()</span></span><br><span class="line"><span class="undefined"></span></span><br><span class="line"><span class="javascript">  parent.addEventListener(<span class="string">'scroll'</span>, () =&gt; &#123;</span></span><br><span class="line"><span class="undefined">    eventCount += 1</span></span><br><span class="line"><span class="undefined">    log()</span></span><br><span class="line"><span class="undefined">  &#125;)</span></span><br><span class="line"><span class="undefined"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br></pre></td></tr></table></figure><p></p><p>为什么？上面的第二、三步有答案。一般来说，我们的设备都是上到下、左到右，所以属于右下方向溢出，对应上面的 2.1 和 3.1 公式。每次计算滚动距离都会跟可滚动的空间比较取最小值。</p><p>因为子元素的尺寸是固定的，且远远大于容器，故两者的差非常大，所以最小值一直是 x 和 y，每次重排都会在同个位置，触发了上面的第五步。</p><p>同时根据公式易得：当可滚动空间一开始不比 x 和 y 大，且随滚动不断变小时，就可以让 <em>position</em> 发生变化。</p><p>于是，我们先让元素滚到最尽头，那么 x 和 y 达到了最大值。当容器尺寸变大时，因为子元素的尺寸是固定的，故 scrolling area 的大小不变，所以两者的差变小了，x 和 y 得到新的最小值，发生了滚动。见图二。</p><p><img src="/images/post/element-onresize/2.gif" alt="图二"></p><p></p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* ... */</span></span><br><span class="line"><span class="selector-class">.child</span> &#123;</span><br><span class="line">  <span class="attribute">height</span>: <span class="number">200px</span>;</span><br><span class="line">  <span class="attribute">width</span>: <span class="number">200px</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">/* ... */</span></span><br></pre></td></tr></table></figure><p></p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// .....</span></span><br><span class="line"><span class="keyword">var</span> msg2 = <span class="built_in">document</span>.querySelector(<span class="string">'.msg2'</span>)</span><br><span class="line"></span><br><span class="line">parent.scrollTop = <span class="number">1000</span></span><br><span class="line">parent.scrollLeft = <span class="number">1000</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> eventCount = <span class="number">0</span></span><br><span class="line"><span class="comment">// ....</span></span><br></pre></td></tr></table></figure><p></p><p>初始的 1 个事件就是上面提到的 event loop 导致的。</p><p>可以观察滚动发生，我们期望容器达到最大时 x 和 y 都没有达到最小值，所以子元素的大小须比容器最大值要大。</p><p>动图同时也可观察到容器变小时没反应。按上面的公式也很容易知道，容器变小了，差值变大了，所以最小值还是 x 和 y，故不触发滚动。</p><p>怎么办呢？</p><h3 id="物尽其用"><a href="#物尽其用" class="headerlink" title="物尽其用"></a>物尽其用</h3><p>再看回公式，我们希望容器变小时，差值也变小。那么只能是让 scrolling area 也跟着变小了。如果子元素大小改为百分比行不？我们来证明一下。</p><p>设容器宽度为 <code>x1</code> 或者 <code>x2</code>，其中 <code>x2 &gt; x1</code>，子元素大小为 <code>n * x1</code> 或 <code>n * x2</code>，因我们不设 padding，则有</p><p></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">n * x1 - x1 &lt; n * x2 - x2</span><br><span class="line">↓</span><br><span class="line">(n - <span class="number">1</span>) * x1 &lt; (n - <span class="number">1</span>) * x2</span><br><span class="line">↓</span><br><span class="line">n &gt; <span class="number">1</span></span><br></pre></td></tr></table></figure><p></p><p>证明了通过百分比是可行的。</p><p>同时我们期望容器达到最小时 x 和 y 都没有达到最小值，容器为 0 时无意义，故设最小为 1，则</p><p></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">(n - <span class="number">1</span>) * <span class="number">1</span> &gt;= <span class="number">1</span></span><br><span class="line">↓</span><br><span class="line">n &gt;= <span class="number">2</span></span><br></pre></td></tr></table></figure><p></p><p>故我们只需让子元素大小至少为 200% 就可以！见图三</p><p><img src="/images/post/element-onresize/3.gif" alt="图三"></p><p></p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* ... */</span></span><br><span class="line"><span class="selector-class">.child</span> &#123;</span><br><span class="line">  <span class="attribute">height</span>: <span class="number">200%</span>;</span><br><span class="line">  <span class="attribute">width</span>: <span class="number">200%</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">/* ... */</span></span><br></pre></td></tr></table></figure><p></p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// .....</span></span><br><span class="line"><span class="keyword">var</span> msg2 = <span class="built_in">document</span>.querySelector(<span class="string">'.msg2'</span>)</span><br><span class="line"></span><br><span class="line">parent.scrollTop = <span class="number">1000</span></span><br><span class="line">parent.scrollLeft = <span class="number">1000</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> eventCount = <span class="number">0</span></span><br><span class="line"><span class="comment">// ....</span></span><br></pre></td></tr></table></figure><p></p><p>同时也说明百分比不能监测容器变大，因为 <code>0 &lt; n &lt; 1</code> 与 <code>n &gt;= 1 + 1/Max</code> 矛盾，可自行证明。</p><p>所以，结合两个方式就可以监测元素扩大与缩小变化。</p><h2 id="代码"><a href="#代码" class="headerlink" title="代码"></a>代码</h2><p>原理搞通之后代码就不难了，我<a href="https://codepen.io/straybugs/full/mwgWad/" target="_blank">这里</a>另外重新实现了一遍，修改了隐藏块的创建方式以及加入 <a href="https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md" target="_blank">passive events</a> 优化滚动。Demo 里拖动一个块改变大小另一个会同步变化，可以看到非常的流畅。</p><p></p><p><iframe class="iframe-defer" height="408" scrolling="no" title="Sync Twin Boxes" src="//codepen.io/straybugs/embed/mwgWad/?height=408&theme-id=0&default-tab=result&embed-version=2" frameborder="no" allowtransparency="true" allowfullscreen="true" style="width: 100%">See the Pen <a href="https://codepen.io/straybugs/pen/mwgWad/" target="_blank">Sync Twin Boxes</a> by CRIMX (<a href="https://codepen.io/straybugs" target="_blank">@straybugs</a>) on <a href="https://codepen.io" target="_blank">CodePen</a>.</iframe></p><p></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;在往下读之前不妨先想一下，你会怎么实现？如何知道元素的尺寸发生变化了？&lt;/p&gt;
&lt;p&gt;相信很多人第一反应是 resize 事件，但这个只是 document view 变化才会触发。&lt;/p&gt;
&lt;p&gt;然后就是轮询，反复查询值变化了没有。开销不是一般的大，但像这样的库（比如这个
      
    
    </summary>
    
      <category term="JavaScript" scheme="https://blog.crimx.com/categories/JavaScript/"/>
    
    
      <category term="Recommended" scheme="https://blog.crimx.com/tags/Recommended/"/>
    
      <category term="闲读规范" scheme="https://blog.crimx.com/tags/%E9%97%B2%E8%AF%BB%E8%A7%84%E8%8C%83/"/>
    
      <category term="闲读源码" scheme="https://blog.crimx.com/tags/%E9%97%B2%E8%AF%BB%E6%BA%90%E7%A0%81/"/>
    
      <category term="Resize" scheme="https://blog.crimx.com/tags/Resize/"/>
    
  </entry>
  
  <entry>
    <title>Manacher 马拉车算法</title>
    <link href="https://blog.crimx.com/2017/07/06/manachers-algorithm/"/>
    <id>https://blog.crimx.com/2017/07/06/manachers-algorithm/</id>
    <published>2017-07-05T16:00:00.000Z</published>
    <updated>2017-07-06T11:24:19.498Z</updated>
    
    <content type="html"><![CDATA[<p>马拉车算法可以在线性时间复杂度内求出一个字符串的最长回文字串。其核心思想跟 KMP 相似，即反复利用已掌握的情况。</p><p>视频推荐看这个，觉得是最清晰易懂的：</p><p></p><p><iframe width="560" height="315" style="width:100%" src="" data-type="youtube" data-src="//www.youtube.com/embed/nbTSfrEfo6M" frameborder="0" allowfullscreen></iframe></p><p></p><h2 id="整体思路"><a href="#整体思路" class="headerlink" title="整体思路"></a>整体思路</h2><p>这个算法的主要思路是维护一个跟原串 str 一样长的数组 lens。lens[i] 表示以 str[i] 为中点的回串其中一边的长度。这里有的人把中点算进去，有的人记录两边的长度，其实都一样，我这里是只记录一边的长度，不包括中点。比如 <code>&quot;CDCDE&quot;</code></p><p></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">str:  [C, D, C, D, E]</span><br><span class="line">lens: [0, 1, 1, 0, 0]</span><br></pre></td></tr></table></figure><p></p><p>那么 lens 里最大的自然就对应最长回串的中点了。所以这个算法的核心就是如何快速计算 lens。</p><h2 id="预处理"><a href="#预处理" class="headerlink" title="预处理"></a>预处理</h2><p>回文有奇偶长度两种情况，通过补充间隔符可以将这两种情况化简为奇数长度。</p><p>比如 <code>ABA</code> 补充为 <code>#A#B#A#</code> 中点还是 B，<code>ABBA</code> 补充为 <code>#A#B#B#A#</code> 中点为 <code>#</code>，最后可以去掉。</p><p>算法用 JavaScript 写，我将原串转为数组，间隔符就用 <code>null</code>。</p><p>最后在两侧补上哨兵点方便遍历中止。我用了 <code>NaN</code>。所以看起来是这样</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> arr = [<span class="literal">NaN</span>, <span class="literal">null</span>]</span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; str.length; i += <span class="number">1</span>) &#123;</span><br><span class="line">  arr.push(str[i])</span><br><span class="line">  arr.push(<span class="literal">null</span>)</span><br><span class="line">&#125;</span><br><span class="line">arr.push(<span class="literal">NaN</span>)</span><br></pre></td></tr></table></figure><p></p><h2 id="计算长度数组"><a href="#计算长度数组" class="headerlink" title="计算长度数组"></a>计算长度数组</h2><h3 id="朴素计算方法"><a href="#朴素计算方法" class="headerlink" title="朴素计算方法"></a>朴素计算方法</h3><p>以一个中心计算回串，最直接的方法当然是左右遍历对比了，比如以 i 为中心：</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">lens[i] = <span class="number">0</span></span><br><span class="line"><span class="keyword">while</span> (arr[i + lens[i] + <span class="number">1</span>] === arr[i - lens[i] - <span class="number">1</span>]) &#123;</span><br><span class="line">  lens[i] += <span class="number">1</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><p>这个就是计算长度基本方式。n 个点，每个点最多计算 n/2 遍，所以是 n 平方复杂度。</p><h3 id="手背手心都是肉"><a href="#手背手心都是肉" class="headerlink" title="手背手心都是肉"></a>手背手心都是肉</h3><p>看回上面的算法，可以发现，lens[i] 是从 0 开始的，这个很正常，一开始当然是 0 ，回串嘛，从中心开始两侧都要一一比较相等才行。</p><p>但再进一步看，0 代表从头开始，即对于每个中心点，我们都是从什么都不知道开始，什么情况都没有掌握。</p><p>事实是这样吗？</p><p>既然有了这个算法，事实当然不是。这时就很容易联系到回串的特性，对称。</p><p>先来一个简单的例子 <code>OABAXABAO</code>。两个 B 是 X 的对称点，左边 B 对应的 lens[j] 长度显然是 1，当我们计算右边 B 的 lens[i] 时候，是不是可以把 lens[j] 的值直接复制过来。因为它们是镜面对称的，所以都是一样，不过是反过来而已。</p><h3 id="最右中心"><a href="#最右中心" class="headerlink" title="最右中心"></a>最右中心</h3><p>我们维护一个已知最右的回串，设其中心点 iCenter 以及其最右点 iRight。显然两者有这么的关系 <code>iRight = iCenter + lens[iCenter]</code>。</p><p>这个回串是最右的，也就是说 iRight 是最大的。有更右的就不断更新。</p><p>为什么要维护最右回串？</p><p>当我们一个个遍历中点 i 时，因 iCenter 已知，故必然是已经遍历过了，所以 i 肯定是在 iCenter 的右边，这就保证了两种情况：</p><ol><li>i &lt;= iRight，在最右回串的范围内，可以应用上面的镜面复制；</li><li>i &gt; iRight，超出了最右，在未知区域，只能用朴素方式计算。</li></ol><p>这就是这个算法的核心思想了，最后引入两个边界情况：</p><h3 id="右贴界"><a href="#右贴界" class="headerlink" title="右贴界"></a>右贴界</h3><p>像简单例子的 <code>OABAXABAO</code> 可以明确知道 X 和 O 不相等，所以复制过来就行。但如果是 <code>OABAXABA...</code> 就不知道下一个是不是 X 了。我们只能知道下一个肯定不是 O，因为最右串 X 的范围到 A 就截止了。</p><p>所以右边 B 对应的 lens[i] 得到了 1 之后，在这基础上继续用朴素方式比较两侧。</p><p>意思是“我现在可以确定右 B 两侧 1 个长度内是对称的，其它未知，继续比较下去看如何”。</p><p>如果右 B 比较下去有戏的话，那么右 B 就是新的最右串了，更新 iCenter 和 iRight 值。</p><p>右贴界的条件是 <code>i + lens[i] === iRight</code>。</p><h3 id="左越界"><a href="#左越界" class="headerlink" title="左越界"></a>左越界</h3><p>对于串 <code>XABAXABA...</code>。两个 B 还是 X 的对称点，但是左边的 B 对应的 lens[j] 长度是 2，右边 B 的 lens[i] 可以看到是 1。</p><p>为什么？</p><p>理解上面提到的镜面对称就很简单了，X 为中心的回串是 <code>ABAXABA</code>，也就是左边到了 A 就截止了，左 X 是超出的，所以不对称。因为如果最右的下一个位也是 X 的话，最右回串就应该是 <code>XABAXABAX</code> 了是不是。</p><p>所以，当左边的 B 超出了中心 X 的范围时，我们只复制在最右回串范围内的部分。</p><p>即对于左边的 B，我们知道范围内的是 <code>ABA</code> ，为 1，复制给右边 B 对应的 lens[i]，再按右贴界处理。</p><p>设左 B 的索引为 iMirror，因为左右 B 对称，故 <code>iMiiror = iCenter - (i - iCenter) = 2 * iCenter - i</code>。</p><p>左 B 到 iCenter 左边界的距离我们用镜面对称过来就是右 B 到 iCenter 右边界的距离 <code>iRight - i</code>。</p><p>于是左越界的条件就是 <code>lens[iMiiror] &gt; iRight - i</code>。</p><h3 id="整合"><a href="#整合" class="headerlink" title="整合"></a>整合</h3><p>可以看到，我们复制镜面值要考虑三种情况，范围内、右贴界、左越界，其中左越界又包含了右贴界。于是简洁起见，我们全部当右贴界处理，因为如果在范围内比较下去自然不相等，相当于去掉了 if 判断。</p><p>然后整合范围内和左越界，范围内指 <code>lens[iMiiror] &lt;= iRight - i</code>，直接复制 <code>lens[iMiiror]</code>；左越界指 <code>lens[iMiiror] &gt; iRight - i</code>，取 <code>iRight - 1</code>。故整合为 <code>min(iRight - i, lens[iMirror])</code>。</p><h2 id="完整算法"><a href="#完整算法" class="headerlink" title="完整算法"></a>完整算法</h2><p>所以完整算法如下</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">manacher</span> (<span class="params">str</span>) </span>&#123;</span><br><span class="line">  str = <span class="built_in">String</span>(str)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">var</span> arr = [<span class="literal">NaN</span>, <span class="literal">null</span>]</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; str.length; i += <span class="number">1</span>) &#123;</span><br><span class="line">    arr.push(str[i])</span><br><span class="line">    arr.push(<span class="literal">null</span>)</span><br><span class="line">  &#125;</span><br><span class="line">  arr.push(<span class="literal">NaN</span>)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">var</span> iCenterMax = <span class="number">1</span></span><br><span class="line">  <span class="keyword">var</span> lens = []</span><br><span class="line">  <span class="keyword">var</span> iCenter = <span class="number">0</span></span><br><span class="line">  <span class="keyword">var</span> iRight = <span class="number">0</span></span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">1</span>; i &lt; arr.length - <span class="number">1</span>; i += <span class="number">1</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (arr.length - <span class="number">1</span> - i &lt;= lens[iCenterMax]) &#123;</span><br><span class="line">      <span class="keyword">break</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    lens[i] = <span class="number">0</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (i &lt; iRight) &#123;</span><br><span class="line">      <span class="keyword">let</span> iMirror = <span class="number">2</span> * iCenter - i</span><br><span class="line">      lens[i] = <span class="built_in">Math</span>.min(iRight - i, lens[iMirror])</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (arr[i + lens[i] + <span class="number">1</span>] === arr[i - lens[i] - <span class="number">1</span>]) &#123;</span><br><span class="line">      lens[i] += <span class="number">1</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (i + lens[i] &gt; iRight) &#123;</span><br><span class="line">      iCenter = i</span><br><span class="line">      iRight = i + lens[i]</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (lens[i] &gt; lens[iCenterMax]) &#123;</span><br><span class="line">      iCenterMax = i</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> arr.slice(iCenterMax - lens[iCenterMax], iCenterMax + lens[iCenterMax] + <span class="number">1</span>)</span><br><span class="line">    .filter(<span class="function"><span class="params">item</span> =&gt;</span> item !== <span class="literal">null</span>)</span><br><span class="line">    .join(<span class="string">''</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;马拉车算法可以在线性时间复杂度内求出一个字符串的最长回文字串。其核心思想跟 KMP 相似，即反复利用已掌握的情况。&lt;/p&gt;
&lt;p&gt;视频推荐看这个，觉得是最清晰易懂的：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;iframe width=&quot;560&quot; height=&quot;315&quot; style
      
    
    </summary>
    
      <category term="Algorithms" scheme="https://blog.crimx.com/categories/Algorithms/"/>
    
    
      <category term="Recommended" scheme="https://blog.crimx.com/tags/Recommended/"/>
    
      <category term="Manacher" scheme="https://blog.crimx.com/tags/Manacher/"/>
    
      <category term="算法" scheme="https://blog.crimx.com/tags/%E7%AE%97%E6%B3%95/"/>
    
      <category term="Algorithms" scheme="https://blog.crimx.com/tags/Algorithms/"/>
    
      <category term="回文" scheme="https://blog.crimx.com/tags/%E5%9B%9E%E6%96%87/"/>
    
      <category term="Palindrome" scheme="https://blog.crimx.com/tags/Palindrome/"/>
    
      <category term="字符串" scheme="https://blog.crimx.com/tags/%E5%AD%97%E7%AC%A6%E4%B8%B2/"/>
    
      <category term="String" scheme="https://blog.crimx.com/tags/String/"/>
    
  </entry>
  
  <entry>
    <title>CSS 属性值计算</title>
    <link href="https://blog.crimx.com/2017/07/03/value-calculation-of-css-properties/"/>
    <id>https://blog.crimx.com/2017/07/03/value-calculation-of-css-properties/</id>
    <published>2017-07-02T16:00:00.000Z</published>
    <updated>2017-07-03T13:22:12.512Z</updated>
    
    <content type="html"><![CDATA[<p>属性值的计算可谓是 CSS 101 。然而入门资料从来都是良莠不齐的，当初从畅销书上得来的一些误解，如今整理笔记的时候才发现。这里就结合规范梳理一遍。</p><p>一个属性的值在计算时会经过 4 个阶段</p><ol><li>Specified values</li><li>Computed values</li><li>Used values</li><li>Actual values</li></ol><h2 id="Specified-values"><a href="#Specified-values" class="headerlink" title="Specified values"></a>Specified values</h2><p>这里有 3 种可能，满足一种就可以</p><ul><li>计算 cascade 有结果</li><li>否则，若这个值可以继承，继承父元素的 computed value（根元素除外）</li><li>还是没有，则用默认值</li></ul><h3 id="计算-cascade"><a href="#计算-cascade" class="headerlink" title="计算 cascade"></a>计算 cascade</h3><p>这里就是重点，经历 4 个步骤：</p><ol><li>匹配 Media Type 的筛选</li><li>按来源排序（低到高）<ol><li>user agent declarations</li><li>user normal declarations</li><li>author normal declarations</li><li>author important declarations</li><li>user important declarations</li></ol></li><li>同个来源的按权值（specificity）排序，收集选择器里的属性个数比较<ul><li>&quot;style&quot; 属性（attribute）里的属性 * 1000</li><li>ID 属性 * 100</li><li>其它属性、类、伪类 * 10</li><li>元素名、伪元素 * 1</li></ul></li><li>如果前面得出并列结果，则按位置排序<ul><li>越靠后越高</li><li>import 进来的样式看做在本样式表前面</li></ul></li></ol><p>可以看到来源总共有三个，user agent、user 和 author。User important 最高是因为用户的自定义样式一般是为了覆盖 user agent 作为默认样式，但有时候用户也想覆盖 author 即网站本身的样式，于是让 user important 最高。</p><p>注意 <code>&lt;style&gt;</code> 与 <code>&lt;link&gt;</code> 引入的 CSS 同属 author declarations 也就是说两者的等级是<strong>一样的</strong>，按先后顺序覆盖。</p><p>HTML 里若有非 CSS 的样式属性（presentational attributes），比如 font 之类，UA 可能将其翻译为 CSS 属性，放在 author style sheet 开端，并赋予权值 0。</p><h2 id="Computed-values"><a href="#Computed-values" class="headerlink" title="Computed values"></a>Computed values</h2><p>在排版前就可以确定的值。在元素定义的 Computed Value 区域可以查到，如</p><ul><li>URI 计算绝对路径</li><li>&#39;em&#39; 和 &#39;ex&#39; 计算成 px 或绝对长度。</li></ul><p>比如</p><p></p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-tag">body</span> &#123; <span class="attribute">font-size</span>: <span class="number">10px</span>; &#125;</span><br><span class="line"><span class="selector-tag">h1</span> &#123; <span class="attribute">font-size</span>: <span class="number">120%</span>; &#125;</span><br></pre></td></tr></table></figure><p></p><p>h1 先计算 cascade 按继承得到了 10px ，再在本阶段计算得 12px。</p><h2 id="Used-values"><a href="#Used-values" class="headerlink" title="Used values"></a>Used values</h2><p>排版之后才能确定的值，如百分比宽度，必须排版才能知道父容器宽度。</p><h2 id="Actual-values"><a href="#Actual-values" class="headerlink" title="Actual values"></a>Actual values</h2><p>实际用于渲染的值，由于设备环境等因素，一些值可能不能用，需要作出变化。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;属性值的计算可谓是 CSS 101 。然而入门资料从来都是良莠不齐的，当初从畅销书上得来的一些误解，如今整理笔记的时候才发现。这里就结合规范梳理一遍。&lt;/p&gt;
&lt;p&gt;一个属性的值在计算时会经过 4 个阶段&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Specified values&lt;/li&gt;
      
    
    </summary>
    
      <category term="CSS" scheme="https://blog.crimx.com/categories/CSS/"/>
    
    
      <category term="CSS" scheme="https://blog.crimx.com/tags/CSS/"/>
    
      <category term="闲读规范" scheme="https://blog.crimx.com/tags/%E9%97%B2%E8%AF%BB%E8%A7%84%E8%8C%83/"/>
    
      <category term="Box Model" scheme="https://blog.crimx.com/tags/Box-Model/"/>
    
      <category term="Visual Formatting Model" scheme="https://blog.crimx.com/tags/Visual-Formatting-Model/"/>
    
  </entry>
  
  <entry>
    <title>CSS VFM 中易混淆的名词概念</title>
    <link href="https://blog.crimx.com/2017/05/29/css-vfm-concepts/"/>
    <id>https://blog.crimx.com/2017/05/29/css-vfm-concepts/</id>
    <published>2017-05-28T16:00:00.000Z</published>
    <updated>2017-07-03T08:51:36.722Z</updated>
    
    <content type="html"><![CDATA[<p><span class="github-emoji" title="warning" data-src="https://assets-cdn.github.com/images/icons/emoji/unicode/26a0.png?v8">&#x26a0;</span>第一遍看 CSS2.1 VFM（Visual Formatting Model）是看别人整理的<a href="http://book.mixu.net/css/" target="_blank">笔记</a>，辅助理解很不错，但是在名词概念上作者跳过了一些或者打乱了顺序，所以现在看回文档时发现当初有一些理解不太正确。于是在这篇文章将这些概念系统整理了一遍。</p><h2 id="Element-与-Box"><a href="#Element-与-Box" class="headerlink" title="Element 与 Box"></a>Element 与 Box</h2><ul><li><p>第一个概念是 Element （元素）与 Box （盒子）。用 Element 的时候是指 document tree （文档树）的节点，Box 则是指元素根据 VFM 和 Box Model 生成的盒子。一个元素可能生成零个或多个盒子。</p></li><li><p>Box Model 描述了一个矩形的盒子。每个盒子都有 content area，可能有 padding, border, margin areas。</p></li></ul><p><img src="/images/post/box-model.png" alt="box-model"></p><h2 id="Block-level-Element"><a href="#Block-level-Element" class="headerlink" title="Block-level Element"></a>Block-level Element</h2><ul><li><p>display 值为 &#39;block/list-item/table&#39; 的元素叫 <em>block-level element</em>。</p></li><li><p>每个 block-level element 都会生成一个 principal <em>block-level box</em> （list-item 还会生成其它盒子）。</p></li><li><p>除了 table box 和 replaced element，block-level box 都是 <em>block container box</em>。</p><p>Block container box 要么只包含 block-level boxes （宣布了 BFC），要么只包含 inline-level boxes （宣布了 IFC）。（BFC: Block Formatting Context, IFC: Inline Formatting Context）</p><p>（Block-level box 指的是这个盒子本身，它参与的是 BFC；而 block container box 说的是这个盒子内部，宣布了 BFC 或 IFC）。</p><p>反过来 block container box 则不一定是 block-level box，还可以是 non-replaced inline block 和 non-replaced table cell（即这些盒子参与 IFC ，内部也可以宣布 BFC 或 IFC）。</p></li><li><p>同时是 block-level box 和 block container box 的盒子也叫做 <em>block box</em>。</p></li></ul><h2 id="Inline-level-element"><a href="#Inline-level-element" class="headerlink" title="Inline-level element"></a>Inline-level element</h2><ul><li><p>display 值为 &#39;inline/inline-table/inline-block&#39; 的元素叫 <em>inline-level element</em>。</p></li><li><p>Inline-level element 会生成 <em>inline-level box</em>。</p></li><li><p>display 为 &#39;inline&#39; 的 non-replaced element 生成的盒子也叫 <em>inline box</em>，指其内部参与的与其自身所在的是同个 IFC。</p><p>（同样，Inline-level box 说的是盒子本身，inline box 说的是盒子内部）。</p></li><li><p>不是 inline box 的 inline-level box （如 replaced inline-level element, inline-block element, inline-table element）叫做 <em>atomic inline-level box</em>，它们以不透明的方式参与到 IFC 中，内部不参与自身所在的 IFC。</p></li><li><p>Inline-level boxes 打横排成一行行的 <em>line boxes</em>。</p></li></ul><h2 id="Flow"><a href="#Flow" class="headerlink" title="Flow"></a>Flow</h2><ul><li><p><em>Out-of-flow</em> 的元素指 absolutely positioned elements, floated elements 和 root element。</p></li><li><p><em>In-flow</em> 指不是 out-of-flow 的元素。</p></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;&lt;span class=&quot;github-emoji&quot; title=&quot;warning&quot; data-src=&quot;https://assets-cdn.github.com/images/icons/emoji/unicode/26a0.png?v8&quot;&gt;&amp;#x26a0;&lt;/span
      
    
    </summary>
    
      <category term="CSS" scheme="https://blog.crimx.com/categories/CSS/"/>
    
    
      <category term="CSS" scheme="https://blog.crimx.com/tags/CSS/"/>
    
      <category term="闲读规范" scheme="https://blog.crimx.com/tags/%E9%97%B2%E8%AF%BB%E8%A7%84%E8%8C%83/"/>
    
      <category term="Box Model" scheme="https://blog.crimx.com/tags/Box-Model/"/>
    
      <category term="Visual Formatting Model" scheme="https://blog.crimx.com/tags/Visual-Formatting-Model/"/>
    
  </entry>
  
  <entry>
    <title>纯 CSS 实现浮动介绍</title>
    <link href="https://blog.crimx.com/2017/04/29/pure-css-relative-aside/"/>
    <id>https://blog.crimx.com/2017/04/29/pure-css-relative-aside/</id>
    <published>2017-04-28T16:00:00.000Z</published>
    <updated>2017-04-28T16:25:53.370Z</updated>
    
    <content type="html"><![CDATA[<p>把扩展上传到 Chrome 商店需要在开发者后台填写一系列表单，非常喜欢它对详细介绍的处理，就想着偷师一下。</p><p>先上效果，下面是模仿的样子。由于详细介绍一般比设置本身要长，它使用了隐藏、按需显示的方式减少了高度。</p><p></p><p><iframe height="522" scrolling="no" title="Pure CSS Relative Aside" src="//codepen.io/straybugs/embed/JNWMmW/?height=522&theme-id=0&default-tab=result&embed-version=2" frameborder="no" allowtransparency="true" allowfullscreen="true" style="width: 100%">See the Pen <a href="https://codepen.io/straybugs/pen/JNWMmW/" target="_blank">Pure CSS Relative Aside</a> by CRIMX (<a href="http://codepen.io/straybugs" target="_blank">@straybugs</a>) on <a href="http://codepen.io" target="_blank">CodePen</a>.</iframe></p><p></p><p>看了开发者后台的代码，它是使用 JavaScript 响应鼠标事件并计算高度和显示。作为 CSS 洁癖，我第一反应当然是先考虑用 CSS 实现。</p><p>这里主要的问题其实就是如何让鼠标在设置（body）上响应介绍（aside）的显示。</p><p>在实现 iPhone 滑块开关的时候，就是通过 <code>&lt;input&gt;</code> 的 <code>:check</code> 来控制 <code>&lt;label&gt;</code> 的变化，方法是利用 <code>+</code> 选择符选择兄弟姐妹元素。</p><p>像这种控制其它元素的情况都可以用这个方法。</p><p></p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-class">.item-aside</span> &#123;</span><br><span class="line">  <span class="attribute">z-index</span>: -<span class="number">1</span>;</span><br><span class="line">  <span class="attribute">opacity</span>: <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.item-body</span><span class="selector-pseudo">:hover</span> + <span class="selector-class">.item-aside</span> &#123;</span><br><span class="line">  <span class="attribute">z-index</span>: <span class="number">1</span>;</span><br><span class="line">  <span class="attribute">opacity</span>: <span class="number">1</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><p>全部的代码，也火速使(tou)用(shi)在了 <a href="http://www.crimx.com/crx-saladict/" target="_blank">Saladit</a> 的设置界面：</p><p></p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">ul</span> <span class="attr">class</span>=<span class="string">"menu"</span>&gt;</span></span><br><span class="line">  <span class="comment">&lt;!--  (li.item&gt;((.item-header&gt;lipsum1)+(.item-body&gt;lipsum5)+(.item-aside&gt;lipsum10)))*10  --&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">li</span> <span class="attr">class</span>=<span class="string">"item"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"item-header"</span>&gt;</span>Lorem.<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"item-body"</span>&gt;</span>Lorem ipsum dolor sit amet.<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"item-aside"</span>&gt;</span>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Fugiat, vitae.<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">li</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">li</span> <span class="attr">class</span>=<span class="string">"item"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"item-header"</span>&gt;</span>Nesciunt.<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"item-body"</span>&gt;</span>Sit doloremque repellat natus libero.<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"item-aside"</span>&gt;</span>Pariatur quibusdam voluptas, vero accusamus itaque. Neque magni autem sunt.<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">li</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">li</span> <span class="attr">class</span>=<span class="string">"item"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"item-header"</span>&gt;</span>Animi!<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"item-body"</span>&gt;</span>Similique voluptas, sint quam eligendi.<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"item-aside"</span>&gt;</span>Unde repudiandae, mollitia voluptatum similique repellendus eum. Ut, quae! Deleniti.<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">li</span>&gt;</span></span><br><span class="line">   <span class="comment">&lt;!--  ......  --&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span><br></pre></td></tr></table></figure><p></p><p></p><figure class="highlight scss"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line">* &#123;</span><br><span class="line">  <span class="attribute">box-sizing</span>: border-box;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.menu</span> &#123;</span><br><span class="line">  <span class="attribute">margin</span>: <span class="number">15px</span> <span class="number">10%</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.item</span> &#123;</span><br><span class="line">  <span class="attribute">display</span>: flex;</span><br><span class="line">  <span class="attribute">position</span>: relative;</span><br><span class="line">  <span class="attribute">margin-bottom</span>: <span class="number">15px</span>;</span><br><span class="line">  <span class="attribute">line-height</span>: <span class="number">1.6</span>;</span><br><span class="line">  <span class="attribute">word-wrap</span>: break-word;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.item-header</span> &#123;</span><br><span class="line">  <span class="attribute">width</span>: <span class="number">2</span> / <span class="number">12</span> * <span class="number">100%</span>;</span><br><span class="line">  <span class="attribute">padding</span>: <span class="number">0</span> <span class="number">10px</span>;</span><br><span class="line">  <span class="attribute">text-align</span>: right;</span><br><span class="line">  <span class="attribute">font-weight</span>: bold;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.item-body</span> &#123;</span><br><span class="line">  <span class="attribute">flex</span>: <span class="number">1</span>;</span><br><span class="line">  <span class="attribute">width</span>: <span class="number">10</span> / <span class="number">12</span> * <span class="number">100%</span>;</span><br><span class="line">  <span class="attribute">margin-right</span>: <span class="number">4</span> / <span class="number">12</span> * <span class="number">100%</span>;</span><br><span class="line">  <span class="attribute">padding</span>: <span class="number">10px</span>;</span><br><span class="line">  <span class="attribute">background-color</span>: <span class="number">#fafafa</span>;</span><br><span class="line">  </span><br><span class="line">  &amp;:hover + <span class="selector-class">.item-aside</span> &#123;</span><br><span class="line">    <span class="attribute">z-index</span>: <span class="number">1</span>;</span><br><span class="line">    <span class="attribute">opacity</span>: <span class="number">1</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.item-aside</span> &#123;</span><br><span class="line">  <span class="attribute">width</span>: <span class="number">4</span> / <span class="number">12</span> * <span class="number">100%</span>;</span><br><span class="line">  <span class="attribute">position</span>: absolute;</span><br><span class="line">  <span class="attribute">z-index</span>: -<span class="number">1</span>;</span><br><span class="line">  <span class="attribute">top</span>: <span class="number">0</span>;</span><br><span class="line">  <span class="attribute">right</span>: <span class="number">0</span>;</span><br><span class="line">  <span class="attribute">padding-left</span>: <span class="number">10px</span> * <span class="number">2</span> + <span class="number">1px</span>;</span><br><span class="line">  <span class="attribute">padding-right</span>: <span class="number">10px</span>;</span><br><span class="line">  <span class="attribute">opacity</span>: <span class="number">0</span>;</span><br><span class="line">  <span class="attribute">transition</span>: all <span class="number">400ms</span>;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// left line</span></span><br><span class="line">  &amp;::after &#123;</span><br><span class="line">    <span class="attribute">content</span>: <span class="string">''</span>;</span><br><span class="line">    <span class="attribute">position</span>: absolute;</span><br><span class="line">    <span class="attribute">top</span>: <span class="number">0</span>;</span><br><span class="line">    <span class="attribute">bottom</span>: <span class="number">0</span>;</span><br><span class="line">    <span class="attribute">left</span>: <span class="number">10px</span>;</span><br><span class="line">    <span class="attribute">border-left</span>: <span class="number">1px</span> <span class="number">#ddd</span> solid;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  &amp;:hover &#123;</span><br><span class="line">    <span class="attribute">z-index</span>: <span class="number">1</span>;</span><br><span class="line">    <span class="attribute">opacity</span>: <span class="number">1</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;把扩展上传到 Chrome 商店需要在开发者后台填写一系列表单，非常喜欢它对详细介绍的处理，就想着偷师一下。&lt;/p&gt;
&lt;p&gt;先上效果，下面是模仿的样子。由于详细介绍一般比设置本身要长，它使用了隐藏、按需显示的方式减少了高度。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;iframe h
      
    
    </summary>
    
      <category term="CSS" scheme="https://blog.crimx.com/categories/CSS/"/>
    
    
      <category term="CSS" scheme="https://blog.crimx.com/tags/CSS/"/>
    
  </entry>
  
  <entry>
    <title>定位与拖动 Iframe</title>
    <link href="https://blog.crimx.com/2017/04/06/position-and-drag-iframe/"/>
    <id>https://blog.crimx.com/2017/04/06/position-and-drag-iframe/</id>
    <published>2017-04-05T16:00:00.000Z</published>
    <updated>2017-04-20T15:58:09.585Z</updated>
    
    <content type="html"><![CDATA[<h1 id="定位-iframe"><a href="#定位-iframe" class="headerlink" title="定位 iframe"></a>定位 iframe</h1><p>在写一个划词翻译扩展 <a href="http://www.crimx.com/crx-saladict/" target="_blank">Saladict</a> 时，有一个需求：用户选择一段文本之后，会在鼠标附近显示一些元素。</p><p>这个初看很简单，监听一个 <code>mouseup</code> 事件，获取 <code>clientX</code> 和 <code>clientY</code> 就行。这也是 Saladict 前几版用的方法。</p><p>但这个方法有个缺陷：iframe 里的鼠标事件不会传到父窗口上。</p><p>解决方法也很简单，就难在把它们都联系起来。</p><h2 id="iframe-里插入脚本"><a href="#iframe-里插入脚本" class="headerlink" title="iframe 里插入脚本"></a>iframe 里插入脚本</h2><p>在 <code>manifest.json</code> 里，<code>content_scripts</code> 有个选项 <code>all_frames</code>，可以让脚本插入到所有的 frame 里。</p><p></p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"content_scripts"</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"js"</span>: [<span class="string">"selection.js"</span>],</span><br><span class="line">      <span class="attr">"matches"</span>: [<span class="string">"&lt;all_urls&gt;"</span>],</span><br><span class="line">      <span class="attr">"all_frames"</span>: <span class="literal">true</span></span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><h2 id="检测点击"><a href="#检测点击" class="headerlink" title="检测点击"></a>检测点击</h2><p>现在可以检测 iframe 里的点击事件</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// selection.js</span></span><br><span class="line"><span class="built_in">document</span>.addEventListener(<span class="string">'mouseup'</span>, handleMouseUp)</span><br></pre></td></tr></table></figure><p></p><h2 id="上传坐标"><a href="#上传坐标" class="headerlink" title="上传坐标"></a>上传坐标</h2><p>当点击发生在 iframe 里时，获取的坐标是相对于 iframe 窗口的，所以把这个坐标交给上层，再加上 iframe 本身的坐标，就可以算出点击相对上层的坐标。</p><p>Chrome 里可以放心使用 <code>postMessage</code></p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// selection.js</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">handleMouseUp</span> (<span class="params">evt</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (<span class="built_in">window</span>.parent === <span class="built_in">window</span>) &#123;</span><br><span class="line">    <span class="comment">// 到了顶层</span></span><br><span class="line">    doAwesomeThings(evt.clientX，evt.clientY)</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// 把坐标传上去</span></span><br><span class="line">    <span class="built_in">window</span>.parent.postMessage(&#123;</span><br><span class="line">      msg: <span class="string">'SALADICT_CLICK'</span>,</span><br><span class="line">      mouseX: evt.clientX,</span><br><span class="line">      mouseY: evt.clientY</span><br><span class="line">    &#125;, <span class="string">'*'</span>)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><h2 id="计算偏移"><a href="#计算偏移" class="headerlink" title="计算偏移"></a>计算偏移</h2><p>上层怎么知道是哪个 iframe 传来坐标？很简单，<code>message</code> 事件里携带了 iframe 的 <code>window</code>，对比一下就可以。</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// selection.js</span></span><br><span class="line"><span class="built_in">window</span>.addEventListener(<span class="string">'message'</span>, evt =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (evt.data.msg !== <span class="string">'SALADICT_CLICK'</span>) &#123; <span class="keyword">return</span> &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> iframe = <span class="built_in">Array</span>.from(<span class="built_in">document</span>.querySelectorAll(<span class="string">'iframe'</span>))</span><br><span class="line">    .filter(<span class="function"><span class="params">f</span> =&gt;</span> f.contentWindow === evt.source)</span><br><span class="line">    [<span class="number">0</span>]</span><br><span class="line">  <span class="keyword">if</span> (!iframe) &#123; <span class="keyword">return</span> &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 计算偏移</span></span><br><span class="line">  <span class="keyword">let</span> pos = iframe.getBoundingClientRect()</span><br><span class="line">  <span class="keyword">let</span> mouseX = evt.data.mouseX + pos.left</span><br><span class="line">  <span class="keyword">let</span> mouseY = evt.data.mouseY + pos.top</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (<span class="built_in">window</span>.parent === <span class="built_in">window</span>) &#123;</span><br><span class="line">    <span class="comment">// 顶层</span></span><br><span class="line">    doAwesomeThings(mouseX, mouseY)</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// 继续上传</span></span><br><span class="line">    <span class="built_in">window</span>.parent.postMessage(&#123;</span><br><span class="line">      msg: <span class="string">'SALADICT_CLICK'</span>,</span><br><span class="line">      mouseX,</span><br><span class="line">      mouseY</span><br><span class="line">    &#125;, <span class="string">'*'</span>)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p></p><h1 id="拖动-iframe"><a href="#拖动-iframe" class="headerlink" title="拖动 iframe"></a>拖动 iframe</h1><p>Saladict 另外一个需求就是拖动一个 iframe 查词面板。</p><h2 id="实现拖动的常识"><a href="#实现拖动的常识" class="headerlink" title="实现拖动的常识"></a>实现拖动的常识</h2><p>实现拖动的一种常用方式就是检测 <code>mousedown</code>, <code>mousemove</code> 和 <code>mouseup</code>。分别对应开始、拖动、结束。然后计算偏移值应用到 <code>left</code> 和 <code>top</code> 上。</p><p>第一次实现很容易犯的一个错误就是监听元素本身的 <code>mousemove</code>。当然这个也可以正确计算出偏移，问题在于如果鼠标移动稍快超出了元素，拖动就卡掉了。所以应该监听全局的 <code>mousemove</code> 获取偏移值。</p><h2 id="iframe-特色的拖动"><a href="#iframe-特色的拖动" class="headerlink" title="iframe 特色的拖动"></a>iframe 特色的拖动</h2><p>iframe 的拖动同理，只是因为发生在 iframe 里的事件不能传到上层，需要手动打包一下。</p><h2 id="iframe-部分"><a href="#iframe-部分" class="headerlink" title="iframe 部分"></a>iframe 部分</h2><p>拖动由 iframe 里的某个元素触发，为了节省资源，在触发的时候才监听拖动和结束，并在结束的时候解绑。</p><p>在 iframe 里监听 <code>mousemove</code> 就是为了把偏移值传回上层，因为上层的 <code>mousemove</code> 事件到这里中断了。</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// iframe.js</span></span><br><span class="line"><span class="keyword">var</span> baseMouseX, baseMouseY</span><br><span class="line"></span><br><span class="line">$dragArea.addEventListener(<span class="string">'mousedown'</span>, handleDragStart)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">handleDragStart</span> (<span class="params">evt</span>) </span>&#123;</span><br><span class="line">  baseMouseX = evt.clientX</span><br><span class="line">  baseMouseY = evt.clientY</span><br><span class="line"></span><br><span class="line">  <span class="built_in">window</span>.parent.postMessage(&#123;</span><br><span class="line">    msg: <span class="string">'SALADICT_DRAG_START'</span>,</span><br><span class="line">    mouseX: baseMouseX,</span><br><span class="line">    mouseY: baseMouseY</span><br><span class="line">  &#125;, <span class="string">'*'</span>)</span><br><span class="line"></span><br><span class="line">  <span class="built_in">document</span>.addEventListener(<span class="string">'mouseup'</span>, handleDragEnd)</span><br><span class="line">  <span class="built_in">document</span>.addEventListener(<span class="string">'mousemove'</span>, handleMousemove)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">handleMousemove</span> (<span class="params">evt</span>) </span>&#123;</span><br><span class="line">  <span class="built_in">window</span>.parent.postMessage(&#123;</span><br><span class="line">    msg: <span class="string">'SALADICT_DRAG_MOUSEMOVE'</span>,</span><br><span class="line">    offsetX: evt.clientX - baseMouseX,</span><br><span class="line">    offsetY: evt.clientY - baseMouseY</span><br><span class="line">  &#125;, <span class="string">'*'</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">handleDragEnd</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="built_in">window</span>.parent.postMessage(&#123;</span><br><span class="line">    msg: <span class="string">'SALADICT_DRAG_END'</span></span><br><span class="line">  &#125;, <span class="string">'*'</span>)</span><br><span class="line"></span><br><span class="line">  <span class="built_in">document</span>.removeEventListener(<span class="string">'mouseup'</span>, handleDragEnd)</span><br><span class="line">  <span class="built_in">document</span>.removeEventListener(<span class="string">'mousemove'</span>, handleMousemove)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><h2 id="上层部分"><a href="#上层部分" class="headerlink" title="上层部分"></a>上层部分</h2><p>主要增加了<code>handleFrameMousemove</code> 补上中断的偏移。</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// parent.js</span></span><br><span class="line"><span class="keyword">var</span> pageMouseX, pageMouseY</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> frameTop = <span class="number">0</span></span><br><span class="line"><span class="keyword">var</span> frameLeft = <span class="number">0</span></span><br><span class="line">$iframe.style.top = frameTop + <span class="string">'px'</span></span><br><span class="line">$iframe.style.left = frameLeft + <span class="string">'px'</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">window</span>.addEventListener(<span class="string">'message'</span>, evt =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> data = evt.data</span><br><span class="line"></span><br><span class="line">  <span class="keyword">switch</span> (data.msg) &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">'SALADICT_DRAG_START'</span>:</span><br><span class="line">      handleDragStart(data.mouseX, data.mouseY)</span><br><span class="line">      <span class="keyword">break</span></span><br><span class="line">    <span class="keyword">case</span> <span class="string">'SALADICT_DRAG_MOUSEMOVE'</span>:</span><br><span class="line">      handleFrameMousemove(data.offsetX, data.offsetY)</span><br><span class="line">      <span class="keyword">break</span></span><br><span class="line">    <span class="keyword">case</span> <span class="string">'SALADICT_DRAG_END'</span>:</span><br><span class="line">      handleDragEnd()</span><br><span class="line">      <span class="keyword">break</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">handleDragStart</span> (<span class="params">mouseX, mouseY</span>) </span>&#123;</span><br><span class="line">  <span class="comment">// 得出鼠标在上层的位置</span></span><br><span class="line">  pageMouseX = frameLeft + mouseX</span><br><span class="line">  pageMouseY = frameTop + mouseY</span><br><span class="line"></span><br><span class="line">  <span class="built_in">document</span>.addEventListener(<span class="string">'mouseup'</span>, handleDragEnd)</span><br><span class="line">  <span class="built_in">document</span>.addEventListener(<span class="string">'mousemove'</span>, handlePageMousemove)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">handleDragEnd</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="built_in">document</span>.removeEventListener(<span class="string">'mouseup'</span>, handleDragEnd)</span><br><span class="line">  <span class="built_in">document</span>.removeEventListener(<span class="string">'mousemove'</span>, handlePageMousemove)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">handleFrameMousemove</span> (<span class="params">offsetX, offsetY</span>) </span>&#123;</span><br><span class="line">  frameTop += offsetY</span><br><span class="line">  frameLeft += offsetX</span><br><span class="line">  $iframe.style.top = frameTop + <span class="string">'px'</span></span><br><span class="line">  $iframe.style.left = frameLeft + <span class="string">'px'</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 更新鼠标在上层的位置，补上偏移</span></span><br><span class="line">  pageMouseX += offsetX</span><br><span class="line">  pageMouseY += offsetY</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">handlePageMousemove</span> (<span class="params">evt</span>) </span>&#123;</span><br><span class="line">  frameTop += evt.clientX - pageMouseX</span><br><span class="line">  frameLeft += evt.clientY - pageMouseY</span><br><span class="line">  $iframe.style.top = frameTop + <span class="string">'px'</span></span><br><span class="line">  $iframe.style.left = frameLeft + <span class="string">'px'</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 新位置直接可以更新</span></span><br><span class="line">  pageMouseX = evt.clientX</span><br><span class="line">  pageMouseY = evt.clientY</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><h2 id="例子"><a href="#例子" class="headerlink" title="例子"></a>例子</h2><p>这里实现了一个例子，下面的正方形 iframe 是可以拖动的：</p><p></p><div class="drag-container"><style type="text/css">.drag-container {    position: relative;    height: 200px;}.drag-iframe {    position: absolute;    width: 200px;    height: 200px;}</style><iframe class="drag-iframe" src="/images/post/drag-iframe.html" frameborder="0"></iframe><script type="text/javascript">;(function() {    var pageMouseX, pageMouseY    var $iframe = document.querySelector('.drag-iframe')    var frameTop = 0    var frameLeft = 0    $iframe.style.top = frameTop + 'px'    $iframe.style.left = frameLeft + 'px'    window.addEventListener('message', evt => {        const data = evt.data        switch (data.msg) {            case 'SALADICT_DRAG_START':                handleDragStart(data.mouseX, data.mouseY)                break            case 'SALADICT_DRAG_MOUSEMOVE':                handleFrameMousemove(data.offsetX, data.offsetY)                break            case 'SALADICT_DRAG_END':                handleDragEnd()                break        }    })    function handleDragStart(mouseX, mouseY) {        // 得出鼠标在上层的位置        pageMouseX = frameLeft + mouseX        pageMouseY = frameTop + mouseY        document.addEventListener('mouseup', handleDragEnd)        document.addEventListener('mousemove', handlePageMousemove)    }    function handleDragEnd() {        document.removeEventListener('mouseup', handleDragEnd)        document.removeEventListener('mousemove', handlePageMousemove)    }    function handleFrameMousemove(offsetX, offsetY) {        frameTop += offsetY        frameLeft += offsetX        $iframe.style.top = frameTop + 'px'        $iframe.style.left = frameLeft + 'px'        // 更新鼠标在上层的位置，补上偏移        pageMouseX += offsetX        pageMouseY += offsetY    }    function handlePageMousemove(evt) {        frameTop += evt.clientX - pageMouseX        frameLeft += evt.clientY - pageMouseY        $iframe.style.top = frameTop + 'px'        $iframe.style.left = frameLeft + 'px'        // 新位置直接可以更新        pageMouseX = evt.clientX        pageMouseY = evt.clientY    }})()</script></div><p></p><h2 id="兼容性"><a href="#兼容性" class="headerlink" title="兼容性"></a>兼容性</h2><p>可以看到，这里主要就是传鼠标的坐标偏移值。所以需要兼容老浏览器的话，用繁琐的旧方式与 iframe 交流就行。如果是同域的话也可以直接从 iframe 里获取偏移。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;定位-iframe&quot;&gt;&lt;a href=&quot;#定位-iframe&quot; class=&quot;headerlink&quot; title=&quot;定位 iframe&quot;&gt;&lt;/a&gt;定位 iframe&lt;/h1&gt;
&lt;p&gt;在写一个划词翻译扩展 &lt;a href=&quot;http://www.crimx.com/
      
    
    </summary>
    
      <category term="JavaScript" scheme="https://blog.crimx.com/categories/JavaScript/"/>
    
    
      <category term="JavaScript" scheme="https://blog.crimx.com/tags/JavaScript/"/>
    
      <category term="Recommended" scheme="https://blog.crimx.com/tags/Recommended/"/>
    
      <category term="iframe" scheme="https://blog.crimx.com/tags/iframe/"/>
    
      <category term="postMessage" scheme="https://blog.crimx.com/tags/postMessage/"/>
    
      <category term="Drag" scheme="https://blog.crimx.com/tags/Drag/"/>
    
      <category term="Extension" scheme="https://blog.crimx.com/tags/Extension/"/>
    
  </entry>
  
  <entry>
    <title>Position and Drag Iframe</title>
    <link href="https://blog.crimx.com/2017/04/06/position-and-drag-iframe-en/"/>
    <id>https://blog.crimx.com/2017/04/06/position-and-drag-iframe-en/</id>
    <published>2017-04-05T16:00:00.000Z</published>
    <updated>2017-04-20T15:58:02.934Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Position-in-iframes"><a href="#Position-in-iframes" class="headerlink" title="Position in iframes"></a>Position in iframes</h1><p>I wrote a Chrome extension <a href="http://www.crimx.com/crx-saladict/" target="_blank">Saladict</a>, an inline translator, which involved such requirement: When user makes a text selection, something will pop up nearby the cursor.</p><p>It looks simple at first view. Just listen to a <code>mouseup</code> event and get <code>clientX</code> and <code>clientY</code> from it.</p><p>But there is a flaw in it - <code>mouseup</code> events inside iframes won&#39;t bubble up to the top frame.</p><p>The solution is actually quite simple. If you know how to connect the dots.</p><h2 id="iframe-script-injection"><a href="#iframe-script-injection" class="headerlink" title="iframe script injection"></a>iframe script injection</h2><p>Using the <code>all_frames</code> property in <code>manifest.json</code>, a content script can run in all frames.</p><p></p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"content_scripts"</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"js"</span>: [<span class="string">"selection.js"</span>],</span><br><span class="line">      <span class="attr">"matches"</span>: [<span class="string">"&lt;all_urls&gt;"</span>],</span><br><span class="line">      <span class="attr">"all_frames"</span>: <span class="literal">true</span></span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><h2 id="Mouse-Event-Detection"><a href="#Mouse-Event-Detection" class="headerlink" title="Mouse Event Detection"></a>Mouse Event Detection</h2><p>Now you can listen to <code>mouseup</code> event in all iframes.</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// selection.js</span></span><br><span class="line"><span class="built_in">document</span>.addEventListener(<span class="string">'mouseup'</span>, handleMouseUp)</span><br></pre></td></tr></table></figure><p></p><h2 id="Upload-Cursor-Coordinates"><a href="#Upload-Cursor-Coordinates" class="headerlink" title="Upload Cursor Coordinates"></a>Upload Cursor Coordinates</h2><p><code>clientX</code> and <code>clientY</code> of the mouse events that are triggered in iframes are coordinates within iframe windows. Upload these coordinates as offsets to the upper frame, then plus the iframe position you will get the cursor positionwithin the upper frame window.</p><p>On Chrome you can boldly use <code>postMessage</code>.</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// selection.js</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">handleMouseUp</span> (<span class="params">evt</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (<span class="built_in">window</span>.parent === <span class="built_in">window</span>) &#123;</span><br><span class="line">    <span class="comment">// Top frame</span></span><br><span class="line">    doAwesomeThings(evt.clientX，evt.clientY)</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// Pass the coordinates to upper frame</span></span><br><span class="line">    <span class="built_in">window</span>.parent.postMessage(&#123;</span><br><span class="line">      msg: <span class="string">'SALADICT_CLICK'</span>,</span><br><span class="line">      mouseX: evt.clientX,</span><br><span class="line">      mouseY: evt.clientY</span><br><span class="line">    &#125;, <span class="string">'*'</span>)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><h2 id="Add-offsets"><a href="#Add-offsets" class="headerlink" title="Add offsets"></a>Add offsets</h2><p>How does the upper frame know which iframe is sending coordinates? Well, the <code>message</code> event contains the content window of the iframe. Use it to match the iframe element.</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// selection.js</span></span><br><span class="line"><span class="built_in">window</span>.addEventListener(<span class="string">'message'</span>, evt =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (evt.data.msg !== <span class="string">'SALADICT_CLICK'</span>) &#123; <span class="keyword">return</span> &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> iframe = <span class="built_in">Array</span>.from(<span class="built_in">document</span>.querySelectorAll(<span class="string">'iframe'</span>))</span><br><span class="line">    .filter(<span class="function"><span class="params">f</span> =&gt;</span> f.contentWindow === evt.source)</span><br><span class="line">    [<span class="number">0</span>]</span><br><span class="line">  <span class="keyword">if</span> (!iframe) &#123; <span class="keyword">return</span> &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// calculate coordinates within current window</span></span><br><span class="line">  <span class="keyword">let</span> pos = iframe.getBoundingClientRect()</span><br><span class="line">  <span class="keyword">let</span> mouseX = evt.data.mouseX + pos.left</span><br><span class="line">  <span class="keyword">let</span> mouseY = evt.data.mouseY + pos.top</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (<span class="built_in">window</span>.parent === <span class="built_in">window</span>) &#123;</span><br><span class="line">    <span class="comment">// Top frame</span></span><br><span class="line">    doAwesomeThings(mouseX, mouseY)</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// Keep uploading</span></span><br><span class="line">    <span class="built_in">window</span>.parent.postMessage(&#123;</span><br><span class="line">      msg: <span class="string">'SALADICT_CLICK'</span>,</span><br><span class="line">      mouseX,</span><br><span class="line">      mouseY</span><br><span class="line">    &#125;, <span class="string">'*'</span>)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p></p><h1 id="iframe-Dragging"><a href="#iframe-Dragging" class="headerlink" title="iframe Dragging"></a>iframe Dragging</h1><p>Another requirement for Saladict is to drag an iframe panel.</p><h2 id="Dragging-101"><a href="#Dragging-101" class="headerlink" title="Dragging 101"></a>Dragging 101</h2><p>Before getting into iframe dragging. There are few basic ideas of implementing a draggable element.</p><p>One of the most common approaches is to listen to <code>mousedown</code>, <code>mousemove</code> and <code>mouseup</code> events, which handle drag start, dragging and drag end. And apply the offsets to the element&#39;s <code>left</code> and <code>top</code>style properties.</p><p>If this is your first time implementing this feature, you are likely to listen to <code>mousemove</code> events of the element itself.</p><p>You can indeed get the correct result in the way. The problem is, if the curser moves a bit too fast and leaves the element, the dragging will stop. That&#39;s why you should listen to global <code>mousemove</code> event instead.</p><h2 id="Dragging-with-iframe"><a href="#Dragging-with-iframe" class="headerlink" title="Dragging with iframe"></a>Dragging with iframe</h2><p>The theory behind iframe dragging is the same. Only the mouse events triggered in iframes will not bubble up to the upper frame. You need to wrap it up yourselves.</p><h2 id="iframe-Part"><a href="#iframe-Part" class="headerlink" title="iframe Part"></a>iframe Part</h2><p>Drag start is triggered by a draggable element inside iframe. For better performance, dragging and drag end event listeners are attached in drag start and are detached in drag end.</p><p>Dragging event listener is required here because the <code>mousemove</code> event of the upper frame breaks inside the iframe. We need to let upper frame know what is happening inside iframe.</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// iframe.js</span></span><br><span class="line"><span class="keyword">var</span> baseMouseX, baseMouseY</span><br><span class="line"></span><br><span class="line">$dragArea.addEventListener(<span class="string">'mousedown'</span>, handleDragStart)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">handleDragStart</span> (<span class="params">evt</span>) </span>&#123;</span><br><span class="line">  baseMouseX = evt.clientX</span><br><span class="line">  baseMouseY = evt.clientY</span><br><span class="line"></span><br><span class="line">  <span class="built_in">window</span>.parent.postMessage(&#123;</span><br><span class="line">    msg: <span class="string">'SALADICT_DRAG_START'</span>,</span><br><span class="line">    mouseX: baseMouseX,</span><br><span class="line">    mouseY: baseMouseY</span><br><span class="line">  &#125;, <span class="string">'*'</span>)</span><br><span class="line"></span><br><span class="line">  <span class="built_in">document</span>.addEventListener(<span class="string">'mouseup'</span>, handleDragEnd)</span><br><span class="line">  <span class="built_in">document</span>.addEventListener(<span class="string">'mousemove'</span>, handleMousemove)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">handleMousemove</span> (<span class="params">evt</span>) </span>&#123;</span><br><span class="line">  <span class="built_in">window</span>.parent.postMessage(&#123;</span><br><span class="line">    msg: <span class="string">'SALADICT_DRAG_MOUSEMOVE'</span>,</span><br><span class="line">    offsetX: evt.clientX - baseMouseX,</span><br><span class="line">    offsetY: evt.clientY - baseMouseY</span><br><span class="line">  &#125;, <span class="string">'*'</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">handleDragEnd</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="built_in">window</span>.parent.postMessage(&#123;</span><br><span class="line">    msg: <span class="string">'SALADICT_DRAG_END'</span></span><br><span class="line">  &#125;, <span class="string">'*'</span>)</span><br><span class="line"></span><br><span class="line">  <span class="built_in">document</span>.removeEventListener(<span class="string">'mouseup'</span>, handleDragEnd)</span><br><span class="line">  <span class="built_in">document</span>.removeEventListener(<span class="string">'mousemove'</span>, handleMousemove)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><h2 id="Upper-Frame-Part"><a href="#Upper-Frame-Part" class="headerlink" title="Upper Frame Part"></a>Upper Frame Part</h2><p>Use <code>handleFrameMousemove</code> to handle the offsets from iframe.</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// parent.js</span></span><br><span class="line"><span class="keyword">var</span> pageMouseX, pageMouseY</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> frameTop = <span class="number">0</span></span><br><span class="line"><span class="keyword">var</span> frameLeft = <span class="number">0</span></span><br><span class="line">$iframe.style.top = frameTop + <span class="string">'px'</span></span><br><span class="line">$iframe.style.left = frameLeft + <span class="string">'px'</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">window</span>.addEventListener(<span class="string">'message'</span>, evt =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> data = evt.data</span><br><span class="line"></span><br><span class="line">  <span class="keyword">switch</span> (data.msg) &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">'SALADICT_DRAG_START'</span>:</span><br><span class="line">      handleDragStart(data.mouseX, data.mouseY)</span><br><span class="line">      <span class="keyword">break</span></span><br><span class="line">    <span class="keyword">case</span> <span class="string">'SALADICT_DRAG_MOUSEMOVE'</span>:</span><br><span class="line">      handleFrameMousemove(data.offsetX, data.offsetY)</span><br><span class="line">      <span class="keyword">break</span></span><br><span class="line">    <span class="keyword">case</span> <span class="string">'SALADICT_DRAG_END'</span>:</span><br><span class="line">      handleDragEnd()</span><br><span class="line">      <span class="keyword">break</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">handleDragStart</span> (<span class="params">mouseX, mouseY</span>) </span>&#123;</span><br><span class="line">  <span class="comment">// get the coordinates within the upper frame</span></span><br><span class="line">  pageMouseX = frameLeft + mouseX</span><br><span class="line">  pageMouseY = frameTop + mouseY</span><br><span class="line"></span><br><span class="line">  <span class="built_in">document</span>.addEventListener(<span class="string">'mouseup'</span>, handleDragEnd)</span><br><span class="line">  <span class="built_in">document</span>.addEventListener(<span class="string">'mousemove'</span>, handlePageMousemove)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">handleDragEnd</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="built_in">document</span>.removeEventListener(<span class="string">'mouseup'</span>, handleDragEnd)</span><br><span class="line">  <span class="built_in">document</span>.removeEventListener(<span class="string">'mousemove'</span>, handlePageMousemove)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">handleFrameMousemove</span> (<span class="params">offsetX, offsetY</span>) </span>&#123;</span><br><span class="line">  frameTop += offsetY</span><br><span class="line">  frameLeft += offsetX</span><br><span class="line">  $iframe.style.top = frameTop + <span class="string">'px'</span></span><br><span class="line">  $iframe.style.left = frameLeft + <span class="string">'px'</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// Add the missing coordinates</span></span><br><span class="line">  pageMouseX += offsetX</span><br><span class="line">  pageMouseY += offsetY</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">handlePageMousemove</span> (<span class="params">evt</span>) </span>&#123;</span><br><span class="line">  frameTop += evt.clientX - pageMouseX</span><br><span class="line">  frameLeft += evt.clientY - pageMouseY</span><br><span class="line">  $iframe.style.top = frameTop + <span class="string">'px'</span></span><br><span class="line">  $iframe.style.left = frameLeft + <span class="string">'px'</span></span><br><span class="line"></span><br><span class="line">  pageMouseX = evt.clientX</span><br><span class="line">  pageMouseY = evt.clientY</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p></p><h2 id="Demo"><a href="#Demo" class="headerlink" title="Demo"></a>Demo</h2><p>You can drag the iframe square below:</p><p></p><div class="drag-container"><style type="text/css">.drag-container {    position: relative;    height: 200px;}.drag-iframe {    position: absolute;    width: 200px;    height: 200px;}</style><iframe class="drag-iframe" src="/images/post/drag-iframe.html" frameborder="0"></iframe><script type="text/javascript">;(function() {    var pageMouseX, pageMouseY    var $iframe = document.querySelector('.drag-iframe')    var frameTop = 0    var frameLeft = 0    $iframe.style.top = frameTop + 'px'    $iframe.style.left = frameLeft + 'px'    window.addEventListener('message', evt => {        const data = evt.data        switch (data.msg) {            case 'SALADICT_DRAG_START':                handleDragStart(data.mouseX, data.mouseY)                break            case 'SALADICT_DRAG_MOUSEMOVE':                handleFrameMousemove(data.offsetX, data.offsetY)                break            case 'SALADICT_DRAG_END':                handleDragEnd()                break        }    })    function handleDragStart(mouseX, mouseY) {        pageMouseX = frameLeft + mouseX        pageMouseY = frameTop + mouseY        document.addEventListener('mouseup', handleDragEnd)        document.addEventListener('mousemove', handlePageMousemove)    }    function handleDragEnd() {        document.removeEventListener('mouseup', handleDragEnd)        document.removeEventListener('mousemove', handlePageMousemove)    }    function handleFrameMousemove(offsetX, offsetY) {        frameTop += offsetY        frameLeft += offsetX        $iframe.style.top = frameTop + 'px'        $iframe.style.left = frameLeft + 'px'        pageMouseX += offsetX        pageMouseY += offsetY    }    function handlePageMousemove(evt) {        frameTop += evt.clientX - pageMouseX        frameLeft += evt.clientY - pageMouseY        $iframe.style.top = frameTop + 'px'        $iframe.style.left = frameLeft + 'px'        pageMouseX = evt.clientX        pageMouseY = evt.clientY    }})()</script></div><p></p><h2 id="Browser-Compatibility"><a href="#Browser-Compatibility" class="headerlink" title="Browser Compatibility"></a>Browser Compatibility</h2><p>As you can see, nothing fancy here, just passing coordinates around. So for older browsers, just use the old ways to communicate. You can also manipulate the values directly if they are same-origin.</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Position-in-iframes&quot;&gt;&lt;a href=&quot;#Position-in-iframes&quot; class=&quot;headerlink&quot; title=&quot;Position in iframes&quot;&gt;&lt;/a&gt;Position in iframes&lt;/h1&gt;
&lt;p&gt;I
      
    
    </summary>
    
      <category term="JavaScript" scheme="https://blog.crimx.com/categories/JavaScript/"/>
    
    
      <category term="JavaScript" scheme="https://blog.crimx.com/tags/JavaScript/"/>
    
      <category term="Recommended" scheme="https://blog.crimx.com/tags/Recommended/"/>
    
      <category term="iframe" scheme="https://blog.crimx.com/tags/iframe/"/>
    
      <category term="postMessage" scheme="https://blog.crimx.com/tags/postMessage/"/>
    
      <category term="Drag" scheme="https://blog.crimx.com/tags/Drag/"/>
    
      <category term="Extension" scheme="https://blog.crimx.com/tags/Extension/"/>
    
  </entry>
  
  <entry>
    <title>Get All Images in DOM (Including Background)</title>
    <link href="https://blog.crimx.com/2017/03/09/get-all-images-in-dom-including-background-en/"/>
    <id>https://blog.crimx.com/2017/03/09/get-all-images-in-dom-including-background-en/</id>
    <published>2017-03-08T16:00:00.000Z</published>
    <updated>2017-03-15T16:05:31.892Z</updated>
    
    <content type="html"><![CDATA[<p>Quite useful if you are writing an browser extension or something.</p><p>To get all the images in DOM there are actually three places we are going to look at: <code>&lt;img&gt;</code> element, <code>background-image</code> CSS property and, <code>&lt;iframe&gt;</code>. Yes, every iframe hides a magical kingdom.</p><h1 id="img"><a href="#img" class="headerlink" title="img"></a>img</h1><p>If you only want to get images in <code>&lt;img&gt;</code>, two options for you to choose:</p><p>You can either search the DOM for <code>img</code> tag:</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">getImgs</span> (<span class="params">doc</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">Array</span>.from(doc.getElementsByTagName(<span class="string">'img'</span>))</span><br><span class="line">    .map(<span class="function"><span class="params">img</span> =&gt;</span> (&#123;</span><br><span class="line">      src: img.currentSrc, <span class="comment">// img.src if you want the origin</span></span><br><span class="line">      width: img.naturalWidth,</span><br><span class="line">      height: img.naturalHeight</span><br><span class="line">    &#125;))</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">getImgs(<span class="built_in">document</span>)</span><br></pre></td></tr></table></figure><p></p><p>Or just use <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/images" target="_blank">document.images</a>:</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">getImgs</span> (<span class="params">doc</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">Array</span>.from(doc.images)</span><br><span class="line">    .map(<span class="function"><span class="params">img</span> =&gt;</span> (&#123;</span><br><span class="line">      src: img.currentSrc, <span class="comment">// img.src if you want the origin</span></span><br><span class="line">      width: img.naturalWidth,</span><br><span class="line">      height: img.naturalHeight</span><br><span class="line">    &#125;))</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">getImgs(<span class="built_in">document</span>)</span><br></pre></td></tr></table></figure><p></p><h1 id="background-image"><a href="#background-image" class="headerlink" title="background-image"></a>background-image</h1><p>For <code>background-image</code>, we need to check every node in DOM:</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">getBgImgs</span> (<span class="params">doc</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> srcChecker = <span class="regexp">/url\(\s*?['"]?\s*?(\S+?)\s*?["']?\s*?\)/i</span></span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">Array</span>.from(</span><br><span class="line">    <span class="built_in">Array</span>.from(doc.querySelectorAll(<span class="string">'*'</span>))</span><br><span class="line">      .reduce(<span class="function">(<span class="params">collection, node</span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">let</span> prop = <span class="built_in">window</span>.getComputedStyle(node, <span class="literal">null</span>)</span><br><span class="line">          .getPropertyValue(<span class="string">'background-image'</span>)</span><br><span class="line">        <span class="comment">// match `url(...)`</span></span><br><span class="line">        <span class="keyword">let</span> match = srcChecker.exec(prop)</span><br><span class="line">        <span class="keyword">if</span> (match) &#123;</span><br><span class="line">          collection.add(match[<span class="number">1</span>])</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> collection</span><br><span class="line">      &#125;, <span class="keyword">new</span> <span class="built_in">Set</span>())</span><br><span class="line">  )</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">getBgImgs(<span class="built_in">document</span>)</span><br></pre></td></tr></table></figure><p></p><p>We can&#39;t simply get the width and height of a background image. If you need them, you have to load it.</p><p>Since the images you get in DOM are most likely already in the browser cache, the loading process should be fairly quick.</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">loadImg</span> (<span class="params">src, timeout = <span class="number">500</span></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">var</span> imgPromise = <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">let</span> img = <span class="keyword">new</span> Image()</span><br><span class="line">    img.onload = <span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">      resolve(&#123;</span><br><span class="line">        src: src,</span><br><span class="line">        width: img.naturalWidth,</span><br><span class="line">        height: img.naturalHeight</span><br><span class="line">      &#125;)</span><br><span class="line">    &#125;</span><br><span class="line">    img.onerror = reject</span><br><span class="line">    img.src = src</span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="keyword">var</span> timer = <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">    setTimeout(reject, timeout)</span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">Promise</span>.race([imgPromise, timer])</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">loadImgAll</span> (<span class="params">imgList, timeout = <span class="number">500</span></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">Promise</span>.all(</span><br><span class="line">      imgList</span><br><span class="line">        .map(<span class="function"><span class="params">src</span> =&gt;</span> loadImg(src, timeout))</span><br><span class="line">        .map(<span class="function"><span class="params">p</span> =&gt;</span> p.catch(<span class="function"><span class="params">e</span> =&gt;</span> <span class="literal">false</span>))</span><br><span class="line">    ).then(<span class="function"><span class="params">results</span> =&gt;</span> resolve(results.filter(<span class="function"><span class="params">r</span> =&gt;</span> r)))</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">loadImgAll(getBgImgs(<span class="built_in">document</span>)).then(<span class="function"><span class="params">imgs</span> =&gt;</span> <span class="built_in">console</span>.log(imgs))</span><br></pre></td></tr></table></figure><p></p><h1 id="iframe"><a href="#iframe" class="headerlink" title="iframe"></a>iframe</h1><p>Just recursively search in all iframes</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">searchIframes</span> (<span class="params">doc</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">var</span> imgList = []</span><br><span class="line">  doc.querySelectorAll(<span class="string">'iframe'</span>)</span><br><span class="line">    .forEach(<span class="function"><span class="params">iframe</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">        iframeDoc = iframe.contentDocument || iframe.contentWindow.document</span><br><span class="line">        imgList = imgList.concat(getImgs(iframeDoc) || []) <span class="comment">// or getBgImgs(iframeDoc)</span></span><br><span class="line">        imgList = imgList.concat(searchIframes(iframeDoc) || [])</span><br><span class="line">      &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">        <span class="comment">// simply ignore errors (e.g. cross-origin)</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;)</span><br><span class="line">  <span class="keyword">return</span> imgList</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">searchIframes(<span class="built_in">document</span>)</span><br></pre></td></tr></table></figure><p></p><h1 id="Together"><a href="#Together" class="headerlink" title="Together"></a>Together</h1><p>Can be used out of the box. It was made when I was writing a <a href="https://github.com/crimx/crx-weitweet" target="_blank">Chrome Extension</a>.</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">getImgAll</span> (<span class="params">doc</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">    loadImgAll(<span class="built_in">Array</span>.from(searchDOM(doc)))</span><br><span class="line">      .then(resolve, reject)</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">searchDOM</span> (<span class="params">doc</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> srcChecker = <span class="regexp">/url\(\s*?['"]?\s*?(\S+?)\s*?["']?\s*?\)/i</span></span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">Array</span>.from(doc.querySelectorAll(<span class="string">'*'</span>))</span><br><span class="line">      .reduce(<span class="function">(<span class="params">collection, node</span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="comment">// bg src</span></span><br><span class="line">        <span class="keyword">let</span> prop = <span class="built_in">window</span>.getComputedStyle(node, <span class="literal">null</span>)</span><br><span class="line">          .getPropertyValue(<span class="string">'background-image'</span>)</span><br><span class="line">        <span class="comment">// match `url(...)`</span></span><br><span class="line">        <span class="keyword">let</span> match = srcChecker.exec(prop)</span><br><span class="line">        <span class="keyword">if</span> (match) &#123;</span><br><span class="line">          collection.add(match[<span class="number">1</span>])</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (<span class="regexp">/^img$/i</span>.test(node.tagName)) &#123;</span><br><span class="line">          <span class="comment">// src from img tag</span></span><br><span class="line">          collection.add(node.src)</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="regexp">/^frame$/i</span>.test(node.tagName)) &#123;</span><br><span class="line">          <span class="comment">// iframe</span></span><br><span class="line">          <span class="keyword">try</span> &#123;</span><br><span class="line">            searchDOM(node.contentDocument || node.contentWindow.document)</span><br><span class="line">              .forEach(<span class="function"><span class="params">img</span> =&gt;</span> &#123;</span><br><span class="line">                <span class="keyword">if</span> (img) &#123; collection.add(img) &#125;</span><br><span class="line">              &#125;)</span><br><span class="line">          &#125; <span class="keyword">catch</span> (e) &#123;&#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> collection</span><br><span class="line">      &#125;, <span class="keyword">new</span> <span class="built_in">Set</span>())</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">loadImg</span> (<span class="params">src, timeout = <span class="number">500</span></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> imgPromise = <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">let</span> img = <span class="keyword">new</span> Image()</span><br><span class="line">      img.onload = <span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">        resolve(&#123;</span><br><span class="line">          src: src,</span><br><span class="line">          width: img.naturalWidth,</span><br><span class="line">          height: img.naturalHeight</span><br><span class="line">        &#125;)</span><br><span class="line">      &#125;</span><br><span class="line">      img.onerror = reject</span><br><span class="line">      img.src = src</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">var</span> timer = <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">      setTimeout(reject, timeout)</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">Promise</span>.race([imgPromise, timer])</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">loadImgAll</span> (<span class="params">imgList, timeout = <span class="number">500</span></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="built_in">Promise</span>.all(</span><br><span class="line">        imgList</span><br><span class="line">          .map(<span class="function"><span class="params">src</span> =&gt;</span> loadImg(src, timeout))</span><br><span class="line">          .map(<span class="function"><span class="params">p</span> =&gt;</span> p.catch(<span class="function"><span class="params">e</span> =&gt;</span> <span class="literal">false</span>))</span><br><span class="line">      ).then(<span class="function"><span class="params">results</span> =&gt;</span> resolve(results.filter(<span class="function"><span class="params">r</span> =&gt;</span> r)))</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">getImgAll(<span class="built_in">document</span>).then(<span class="function"><span class="params">list</span> =&gt;</span> <span class="built_in">console</span>.log(list))</span><br></pre></td></tr></table></figure><p></p><p>[EOF]</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;Quite useful if you are writing an browser extension or something.&lt;/p&gt;
&lt;p&gt;To get all the images in DOM there are actually three places we
      
    
    </summary>
    
      <category term="JavaScript" scheme="https://blog.crimx.com/categories/JavaScript/"/>
    
    
      <category term="JavaScript" scheme="https://blog.crimx.com/tags/JavaScript/"/>
    
      <category term="Recommended" scheme="https://blog.crimx.com/tags/Recommended/"/>
    
      <category term="Image" scheme="https://blog.crimx.com/tags/Image/"/>
    
      <category term="DOM" scheme="https://blog.crimx.com/tags/DOM/"/>
    
  </entry>
  
  <entry>
    <title>获取 DOM 里所有图片（包括背景和Iframe）</title>
    <link href="https://blog.crimx.com/2017/03/09/get-all-images-in-dom-including-background/"/>
    <id>https://blog.crimx.com/2017/03/09/get-all-images-in-dom-including-background/</id>
    <published>2017-03-08T16:00:00.000Z</published>
    <updated>2017-03-15T16:05:43.353Z</updated>
    
    <content type="html"><![CDATA[<p>在写浏览器扩展什么的时候可能会用上。</p><p>获取 DOM 里的图片主要是在这几个地方里面找: <code>&lt;img&gt;</code> 元素, <code>background-image</code> CSS 属性和 <code>&lt;iframe&gt;</code>。</p><h1 id="img"><a href="#img" class="headerlink" title="img"></a>img</h1><p>如果只想获取 <code>&lt;img&gt;</code> 的图片，有两种方式:</p><p>直接获取所有 <code>img</code> 标签:</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">getImgs</span> (<span class="params">doc</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">Array</span>.from(doc.getElementsByTagName(<span class="string">'img'</span>))</span><br><span class="line">    .map(<span class="function"><span class="params">img</span> =&gt;</span> (&#123;</span><br><span class="line">      src: img.currentSrc, <span class="comment">// 用 img.src 如果要本来的 src</span></span><br><span class="line">      width: img.naturalWidth,</span><br><span class="line">      height: img.naturalHeight</span><br><span class="line">    &#125;))</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">getImgs(<span class="built_in">document</span>)</span><br></pre></td></tr></table></figure><p></p><p>还可以用 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/images" target="_blank">document.images</a>:</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">getImgs</span> (<span class="params">doc</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">Array</span>.from(doc.images)</span><br><span class="line">    .map(<span class="function"><span class="params">img</span> =&gt;</span> (&#123;</span><br><span class="line">      src: img.currentSrc, <span class="comment">// img.src if you want the origin</span></span><br><span class="line">      width: img.naturalWidth,</span><br><span class="line">      height: img.naturalHeight</span><br><span class="line">    &#125;))</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">getImgs(<span class="built_in">document</span>)</span><br></pre></td></tr></table></figure><p></p><h1 id="background-image"><a href="#background-image" class="headerlink" title="background-image"></a>background-image</h1><p>获得背景图片需要查看所有 DOM 节点的 <code>background-image</code> 属性:</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">getBgImgs</span> (<span class="params">doc</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> srcChecker = <span class="regexp">/url\(\s*?['"]?\s*?(\S+?)\s*?["']?\s*?\)/i</span></span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">Array</span>.from(</span><br><span class="line">    <span class="built_in">Array</span>.from(doc.querySelectorAll(<span class="string">'*'</span>))</span><br><span class="line">      .reduce(<span class="function">(<span class="params">collection, node</span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">let</span> prop = <span class="built_in">window</span>.getComputedStyle(node, <span class="literal">null</span>)</span><br><span class="line">          .getPropertyValue(<span class="string">'background-image'</span>)</span><br><span class="line">        <span class="comment">// match `url(...)`</span></span><br><span class="line">        <span class="keyword">let</span> match = srcChecker.exec(prop)</span><br><span class="line">        <span class="keyword">if</span> (match) &#123;</span><br><span class="line">          collection.add(match[<span class="number">1</span>])</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> collection</span><br><span class="line">      &#125;, <span class="keyword">new</span> <span class="built_in">Set</span>())</span><br><span class="line">  )</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">getBgImgs(<span class="built_in">document</span>)</span><br></pre></td></tr></table></figure><p></p><p>背景图片不能直接得到尺寸信息，如果需要的话要加载一遍。因为搜集的图片很有可能已经在浏览器缓存里，所以加载过程应该很快。</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">loadImg</span> (<span class="params">src, timeout = <span class="number">500</span></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">var</span> imgPromise = <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">let</span> img = <span class="keyword">new</span> Image()</span><br><span class="line">    img.onload = <span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">      resolve(&#123;</span><br><span class="line">        src: src,</span><br><span class="line">        width: img.naturalWidth,</span><br><span class="line">        height: img.naturalHeight</span><br><span class="line">      &#125;)</span><br><span class="line">    &#125;</span><br><span class="line">    img.onerror = reject</span><br><span class="line">    img.src = src</span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="keyword">var</span> timer = <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">    setTimeout(reject, timeout)</span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">Promise</span>.race([imgPromise, timer])</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">loadImgAll</span> (<span class="params">imgList, timeout = <span class="number">500</span></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">Promise</span>.all(</span><br><span class="line">      imgList</span><br><span class="line">        .map(<span class="function"><span class="params">src</span> =&gt;</span> loadImg(src, timeout))</span><br><span class="line">        .map(<span class="function"><span class="params">p</span> =&gt;</span> p.catch(<span class="function"><span class="params">e</span> =&gt;</span> <span class="literal">false</span>))</span><br><span class="line">    ).then(<span class="function"><span class="params">results</span> =&gt;</span> resolve(results.filter(<span class="function"><span class="params">r</span> =&gt;</span> r)))</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">loadImgAll(getBgImgs(<span class="built_in">document</span>)).then(<span class="function"><span class="params">imgs</span> =&gt;</span> <span class="built_in">console</span>.log(imgs))</span><br></pre></td></tr></table></figure><p></p><h1 id="iframe"><a href="#iframe" class="headerlink" title="iframe"></a>iframe</h1><p>只需要递归遍历 iframe 的 document</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">searchIframes</span> (<span class="params">doc</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">var</span> imgList = []</span><br><span class="line">  doc.querySelectorAll(<span class="string">'iframe'</span>)</span><br><span class="line">    .forEach(<span class="function"><span class="params">iframe</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">        iframeDoc = iframe.contentDocument || iframe.contentWindow.document</span><br><span class="line">        imgList = imgList.concat(getImgs(iframeDoc) || []) <span class="comment">// or getBgImgs(iframeDoc)</span></span><br><span class="line">        imgList = imgList.concat(searchIframes(iframeDoc) || [])</span><br><span class="line">      &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">        <span class="comment">// 直接忽略错误的 iframe (e.g. cross-origin)</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;)</span><br><span class="line">  <span class="keyword">return</span> imgList</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">searchIframes(<span class="built_in">document</span>)</span><br></pre></td></tr></table></figure><p></p><h1 id="整合一起"><a href="#整合一起" class="headerlink" title="整合一起"></a>整合一起</h1><p>直接使用就行。</p><p></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">getImgAll</span> (<span class="params">doc</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">    loadImgAll(<span class="built_in">Array</span>.from(searchDOM(doc)))</span><br><span class="line">      .then(resolve, reject)</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">searchDOM</span> (<span class="params">doc</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> srcChecker = <span class="regexp">/url\(\s*?['"]?\s*?(\S+?)\s*?["']?\s*?\)/i</span></span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">Array</span>.from(doc.querySelectorAll(<span class="string">'*'</span>))</span><br><span class="line">      .reduce(<span class="function">(<span class="params">collection, node</span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="comment">// bg src</span></span><br><span class="line">        <span class="keyword">let</span> prop = <span class="built_in">window</span>.getComputedStyle(node, <span class="literal">null</span>)</span><br><span class="line">          .getPropertyValue(<span class="string">'background-image'</span>)</span><br><span class="line">        <span class="comment">// match `url(...)`</span></span><br><span class="line">        <span class="keyword">let</span> match = srcChecker.exec(prop)</span><br><span class="line">        <span class="keyword">if</span> (match) &#123;</span><br><span class="line">          collection.add(match[<span class="number">1</span>])</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (<span class="regexp">/^img$/i</span>.test(node.tagName)) &#123;</span><br><span class="line">          <span class="comment">// src from img tag</span></span><br><span class="line">          collection.add(node.src)</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="regexp">/^frame$/i</span>.test(node.tagName)) &#123;</span><br><span class="line">          <span class="comment">// iframe</span></span><br><span class="line">          <span class="keyword">try</span> &#123;</span><br><span class="line">            searchDOM(node.contentDocument || node.contentWindow.document)</span><br><span class="line">              .forEach(<span class="function"><span class="params">img</span> =&gt;</span> &#123;</span><br><span class="line">                <span class="keyword">if</span> (img) &#123; collection.add(img) &#125;</span><br><span class="line">              &#125;)</span><br><span class="line">          &#125; <span class="keyword">catch</span> (e) &#123;&#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> collection</span><br><span class="line">      &#125;, <span class="keyword">new</span> <span class="built_in">Set</span>())</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">loadImg</span> (<span class="params">src, timeout = <span class="number">500</span></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> imgPromise = <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">let</span> img = <span class="keyword">new</span> Image()</span><br><span class="line">      img.onload = <span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">        resolve(&#123;</span><br><span class="line">          src: src,</span><br><span class="line">          width: img.naturalWidth,</span><br><span class="line">          height: img.naturalHeight</span><br><span class="line">        &#125;)</span><br><span class="line">      &#125;</span><br><span class="line">      img.onerror = reject</span><br><span class="line">      img.src = src</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">var</span> timer = <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">      setTimeout(reject, timeout)</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">Promise</span>.race([imgPromise, timer])</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">loadImgAll</span> (<span class="params">imgList, timeout = <span class="number">500</span></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="built_in">Promise</span>.all(</span><br><span class="line">        imgList</span><br><span class="line">          .map(<span class="function"><span class="params">src</span> =&gt;</span> loadImg(src, timeout))</span><br><span class="line">          .map(<span class="function"><span class="params">p</span> =&gt;</span> p.catch(<span class="function"><span class="params">e</span> =&gt;</span> <span class="literal">false</span>))</span><br><span class="line">      ).then(<span class="function"><span class="params">results</span> =&gt;</span> resolve(results.filter(<span class="function"><span class="params">r</span> =&gt;</span> r)))</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">getImgAll(<span class="built_in">document</span>).then(<span class="function"><span class="params">list</span> =&gt;</span> <span class="built_in">console</span>.log(list))</span><br></pre></td></tr></table></figure><p></p><p>如果是开发 Chrome 插件则不受跨域影响，可以直接使用 <a href="https://github.com/nodeca/probe-image-size" target="_blank">probe-image-size</a>，它支持 timeout 参数，就不需要自己写 timer 了。我在写一个 <a href="https://github.com/crimx/crx-weitweet" target="_blank">Chrome 扩展</a>时用上了，很方便。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;在写浏览器扩展什么的时候可能会用上。&lt;/p&gt;
&lt;p&gt;获取 DOM 里的图片主要是在这几个地方里面找: &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; 元素, &lt;code&gt;background-image&lt;/code&gt; CSS 属性和 &lt;code&gt;&amp;lt;iframe&amp;gt
      
    
    </summary>
    
      <category term="JavaScript" scheme="https://blog.crimx.com/categories/JavaScript/"/>
    
    
      <category term="JavaScript" scheme="https://blog.crimx.com/tags/JavaScript/"/>
    
      <category term="Recommended" scheme="https://blog.crimx.com/tags/Recommended/"/>
    
      <category term="Image" scheme="https://blog.crimx.com/tags/Image/"/>
    
      <category term="DOM" scheme="https://blog.crimx.com/tags/DOM/"/>
    
  </entry>
  
</feed>
