<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>前端外刊評論 Archives - 成長駭客交流第一站 - HyperGrowths™</title>
	<atom:link href="https://hypergrowths.com/tag/%E5%89%8D%E7%AB%AF%E5%A4%96%E5%88%8A%E8%A9%95%E8%AB%96/feed/" rel="self" type="application/rss+xml" />
	<link>https://hypergrowths.com/tag/前端外刊評論/</link>
	<description>用SEO內容行銷加速增長? 企業發展遇到增長瓶頸？加入 HyperGrowths，學習突破性增長策略，優化行銷方案，助力企業飛躍式發展</description>
	<lastBuildDate>Sat, 30 Jan 2021 18:37:44 +0000</lastBuildDate>
	<language>zh-TW</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.3.5</generator>

<image>
	<url>https://hypergrowths.com/wp-content/uploads/2020/11/cropped-?.png</url>
	<title>前端外刊評論 Archives - 成長駭客交流第一站 - HyperGrowths™</title>
	<link>https://hypergrowths.com/tag/前端外刊評論/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>向Modern JavaScript 轉型</title>
		<link>https://hypergrowths.com/software-engineering/front-end-dev/15301/topic-345085461/</link>
		
		<dc:creator><![CDATA[marketer]]></dc:creator>
		<pubDate>Sat, 30 Jan 2021 18:37:44 +0000</pubDate>
				<category><![CDATA[前端開發]]></category>
		<category><![CDATA[前端外刊評論]]></category>
		<guid isPermaLink="false">https://hypergrowths.com/software-engineering/front-end-dev/15301/topic-345085461/</guid>

					<description><![CDATA[<p>背景自2011 年browserify 誕生開始，到我們現在更為廣泛應用的webpack/ rollup / parcel 等構建工具的普及，構建及工程化已經變成了前端開發者的開發生態中不可或缺的部分。日常開發中，我們已經習慣跟隨ESMAS…</p>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15301/topic-345085461/" data-wpel-link="internal">向Modern JavaScript 轉型</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></description>
										<content:encoded><![CDATA[<article class="Post-Main Post-NormalMain" tabindex="-1">
<header class="Post-Header">
<h1 class="Post-Title">向Modern JavaScript 轉型</h1>
<div class="Post-Author">
<div class="AuthorInfo" itemprop="author" itemscope="" itemtype="http://schema.org/Person"><meta itemprop="name" content="徐祁"><meta itemprop="image" content="https://pic2.zhimg.com/v2-384aa5bf17da19ac637b4af08cb4a4fb_l.jpg?source=172ae18b"><meta itemprop="url" content="https://www.zhihu.com/people/xu-qi-48-10"><meta itemprop="zhihu:followerCount"></div>
</div>
</header>
<div class="Post-RichTextContainer">
<div class="RichText ztext Post-RichText">
<h2>背景</h2>
<p>自2011年<code>browserify</code>誕生開始，到我們現在更為廣泛應用的<code>webpack</code> / <code>rollup</code> / <code>parcel</code>等構建工具的普及，構建及工程化已經變成了前端開發者的開發生態中不可或缺的部分。日常開發中，我們已經習慣跟隨ESMAScript或TypeScript的新特性，這些特性往往具有更簡單的語法、更高的執行效率，比較典型的例子就是ES6中<code>class</code> :</p>
<div class="highlight">
<pre><code class="language-js"><span class="kr">class</span><span class="nx">Person</span><span class="p">{</span><span class="nx">constructor</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">name</span><span class="o">=</span><span class="nx">name</span><span class="p">;</span><span class="p">}</span><span class="nx">greeting</span><span class="p">()</span><span class="p">{</span><span class="k">return</span><span class="sb">`Hi, this is </span><span class="si">${</span><span class="k">this</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="sb">.`</span><span class="p">;</span><span class="p">}</span><span class="p">}</span><span class="kr">class</span><span class="nx">Man</span><span class="kr">extends</span><span class="nx">Person</span><span class="p">{}</span></code></pre>
</div>
<p>但如果要用構建函數和原型鏈去實現兩個類的聲明並實現繼承，我相信大多數人會崩潰：</p>
<div class="highlight">
<pre><code class="language-js"><span class="kd">function</span><span class="nx">Person</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">name</span><span class="o">=</span><span class="nx">name</span><span class="p">;</span><span class="p">}</span><span class="nx">Person</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">greeting</span><span class="o">=</span><span class="kd">function</span><span class="p">()</span><span class="p">{</span><span class="k">return</span><span class="sb">`Hi, this is</span><span class="si">${</span><span class="k">this</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="sb">.`</span><span class="p">;</span><span class="p">}</span><span class="kd">function</span><span class="nx">Man</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span><span class="p">{</span><span class="nx">Persona</span><span class="p">.</span><span class="nx">call</span><span class="p">(</span><span class="k">this</span><span class="p">,</span><span class="nx">name</span><span class="p">);</span><span class="p">}</span><span class="nx">Man</span><span class="p">.</span><span class="nx">prototype</span><span class="o">=</span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">Person</span><span class="p">.</span><span class="nx">prototype</span><span class="p">);</span><span class="nx">Man</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">constructor</span><span class="o">=</span><span class="nx">Man</span><span class="p">;</span></code></pre>
</div>
<p>問題在於，這些提效的新特性並不能100%的運行在所有的瀏覽器中，比如上個時代的噩夢- IE8.在這樣的背景之下，就誕生了<code>6to5</code>這樣的編譯器，來幫助我們把開發時的ES6的語法編譯成ES5的語法，從而執行在瀏覽器中，當然<code>6to5</code>也跟隨我們一起進化，變成了現在的<code>babel</code> .</p>
<p>但<code>babel</code>也不是萬能膏藥，雖然幫助我們解決了語法的問題，但也帶來了一些副作用，比如編譯後的代碼通常都會插入一些內置的函數或者Pollyfill，這也就造成了代碼體積的驟增，上文中我們<code>class Person</code>的源碼實現總共<b>155B</b> ,使用babel編譯成ES5的代碼後，體積變成了<b>2.7K</b> !</p>
<p>從我們認為的<code>JavaScript 下一代的标准</code>的開始-- ECMAScript 2015到現在已經過去5年多的時間了，ECMAScript每年都會不斷的推進語言標準的更新，而瀏覽器廠商除了參與標準的指定，也在不斷的跟進著新的語言標准在瀏覽器中的原生實現的支持。不知道你有沒有註意到，在2021年的今天， <code>class</code>的瀏覽器支持率已經達到了<code>95%</code> ,所有的主流瀏覽器都已支持<code>class</code>特性：</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-42ff76abd775d2dd8cf29c3cacd0521c_r.jpg" data-caption="" data-size="normal" data-rawwidth="1372" data-rawheight="447" class="origin_image zh-lightbox-thumb" width="1372" data-original="https://pic1.zhimg.com/v2-42ff76abd775d2dd8cf29c3cacd0521c_b.jpg" title="v2-42ff76abd775d2dd8cf29c3cacd0521c_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-42ff76abd775d2dd8cf29c3cacd0521c_r.jpg" data-caption="" data-size="normal" data-rawwidth="1372" data-rawheight="447" class="origin_image zh-lightbox-thumb lazy" width="1372" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1372'%20height='447'&gt;&lt;/svg&gt;" data-actualsrc="https://pic1.zhimg.com/v2-42ff76abd775d2dd8cf29c3cacd0521c_b.jpg" title="v2-42ff76abd775d2dd8cf29c3cacd0521c_r"></figure>
<p>類似於<code>class</code>這類特性，如果瀏覽器已經支持了，但我們還是把編譯後的體積更大、執行更慢的ES5代碼交給瀏覽器，不管是構建、存儲、傳輸還是執行，無疑都是一種浪費。而這就關係到我們這篇文章的主題： <b>Modern JavaScript</b> .</p>
<h2>什麼是Modern JavaScript</h2>
<p><code>Modern JavaScript</code>指的不是哪一個特定版本的ECMAScript標準，而是指的一系列已經被現代瀏覽器所支持的特性的集合。</p>
<p>我們常說的現代瀏覽器包括：Chrome, Edge, Firefox, Safari, 它們佔了瀏覽器市場份額的90% 以上，除此之外，還有一些基於跟上述瀏覽器相同渲染引擎的瀏覽器實現，比如UC, QQ 等也佔了5% 左右的份額。這也就意味著，我們廣泛使用的一些特性已經得到了95% 的支持，主要包括：</p>
<ul>
<li>類<code>class</code></li>
<li>箭頭函數<code>arrow function</code></li>
<li>構造器<code>generator</code></li>
<li>塊級作用域<code>let</code> / <code>const</code></li>
<li>解構<code>destructuring</code></li>
<li> Rest參數<code>rest and spread parameters</code></li>
<li>對像簡寫<code>object shorthand</code></li>
<li> Async函數<code>async / await</code></li>
</ul>
<blockquote><p>Modern JavaScript 不是一個固定的特性集合，它是動態跟隨我們所定義的現代瀏覽器的的支持度的。就目前而言， <b>ES2017</b>是最接近Modern JavaScript的標準。</p></blockquote>
<p><b>Legacy JavaScript</b></p>
<p>對應Modern JavaScript, 我們編譯完的ES5 的結果就可以稱為Legacy JavaScript, 它是我們向瀏覽器兼容器委屈求全的結果。</p>
<p>在現代瀏覽器中，這種轉換是得不償失：我們通過編譯讓我們的代碼支持度從95% 上升到了98%, 然而這卻給我們帶來了20% 的代碼體積的上升，同時代碼執行效率會變得更低。另外，在<code>node_modules hell</code>的加成下，這個影響可能是指數級的。</p>
<h2>如何使用</h2>
<ol>
<li><b>瀏覽器</b></li>
</ol>
<blockquote><p><code>&lt;script type="module" /&gt;</code></p></blockquote>
<p>現代瀏覽器支持通過<code>&lt;script type="module" /&gt;</code>直接在瀏覽器中ES6+的代碼，因此我們可以通過這種方式來為現代瀏覽器加載Modern JavaScript,而小部分的老瀏覽器則由fallback邏輯兜底，加載執行Legacy JavaScript.</p>
<div class="highlight">
<pre><code class="language-html"><span class="p">&lt;</span><span class="nt">script</span><span class="na">type</span><span class="o">=</span><span class="s">"module"</span><span class="na">src</span><span class="o">=</span><span class="s">"https://cdn/modern.js"</span><span class="p">/&gt;</span><span class="p">&lt;</span><span class="nt">script</span><span class="na">nomodule</span><span class="na">src</span><span class="o">=</span><span class="s">"https://cdn/ledacy.js"</span><span class="p">/&gt;</span></code></pre>
</div>
<p>2. <b>NPM</b></p>
<blockquote><p><code>{ "exports": "./modern.js" }</code></p></blockquote>
<p>在<code>Node.js@12.7.0</code>的<a href="https://link.zhihu.com/?target=https%3A//nodejs.org/api/packages.html%23packages_package_entry_points" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">版本</a>中，引入了<code>package.json</code>中的<code>exports</code>字段，來增強原先僅能通過<code>main</code>聲明的包入口的功能。</p>
<div class="highlight">
<pre><code class="language-json"><span class="p">{</span><span class="nt">"main"</span><span class="p">:</span><span class="s2">"./index.js"</span><span class="p">,</span><span class="nt">"exports"</span><span class="p">:</span><span class="p">{</span><span class="nt">"."</span><span class="p">:</span><span class="s2">"./index.js"</span><span class="p">,</span><span class="nt">"./submodule"</span><span class="p">:</span><span class="p">{</span><span class="nt">"import"</span><span class="p">:</span><span class="s2">"./submodule/index.js"</span><span class="p">,</span><span class="nt">"require"</span><span class="p">:</span><span class="s2">"./submodule/index.cjs"</span><span class="p">}</span><span class="p">}</span><span class="p">}</span></code></pre>
</div>
<p>通過上面的入口聲明我們可以實現對外暴露默認入口以及submodule的入口，並且submodule可以根據是<code>require</code>還是<code>import</code>提供不同的實現：</p>
<div class="highlight">
<pre><code class="language-js"><span class="kr">import</span><span class="nx">pkg</span><span class="nx">from</span><span class="s1">'pkg'</span><span class="p">;</span><span class="kr">import</span><span class="nx">submodule</span><span class="nx">from</span><span class="s1">'pkg/submodule'</span><span class="p">;</span><span class="kr">const</span><span class="nx">submodule</span><span class="o">=</span><span class="nx">require</span><span class="p">(</span><span class="s1">'pkg/submodule'</span><span class="p">);</span></code></pre>
</div>
<p>exports除了提供了<b>多入口</b>以及<b>條件入口</b>等強大的功能外，也可以作為提供了Modern JavaScript實現的依據，因為Node.js@12.7.0已經支持ES2019了，所以在構建時如果檢測到<code>node_modules</code>中的模塊提供了<code>exports</code> entry,可以為它們單獨構建我們的Modern JavaScript Bundle.</p>
<p>3. <b>Bundle</b></p>
<p>通過上述瀏覽器和NPM 包的對Modern JavaScript 特性的支持，我們已經解決了出口的問題，並且在不支持的場景下也都可以fallback 到Legacy JavaScript 來執行。</p>
<p>接下來只需要資源構建的問題即可，我們需要提供滿足95% 的現代瀏覽器可執行的Modern JavaScript Bundle 以及fallback 剩餘的老瀏覽器的Legacy JavaScript Bundle.</p>
<p>通過babel配合不同的<a href="https://link.zhihu.com/?target=https%3A//github.com/browserslist/browserslist" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">browserslist</a> <code>targets</code>就可以簡單的達成目標。當然也有一些現成的插件可以直接使用，比如<a href="https://link.zhihu.com/?target=https%3A//github.com/developit/optimize-plugin" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">optimize-plugin</a> , <a href="https://link.zhihu.com/?target=https%3A//github.com/prateekbh/babel-esm-plugin" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">babel-esm-plugin</a>或者<a href="https://link.zhihu.com/?target=https%3A//github.com/christophehurpeau/babel-preset-modern-browsers" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">babel-preset-modern-browsers</a>等。</p>
<p>而在構建模式上，我們可以選擇從源碼分別構建出Modern Bundle 和Legacy Bundle:</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-5e91c613aaec26e05520bd9b44ebf47e_r.jpg" data-caption="" data-size="normal" data-rawwidth="2858" data-rawheight="1052" class="origin_image zh-lightbox-thumb" width="2858" data-original="https://pic3.zhimg.com/v2-5e91c613aaec26e05520bd9b44ebf47e_b.jpg" title="v2-5e91c613aaec26e05520bd9b44ebf47e_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-5e91c613aaec26e05520bd9b44ebf47e_r.jpg" data-caption="" data-size="normal" data-rawwidth="2858" data-rawheight="1052" class="origin_image zh-lightbox-thumb lazy" width="2858" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='2858'%20height='1052'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-5e91c613aaec26e05520bd9b44ebf47e_b.jpg" title="v2-5e91c613aaec26e05520bd9b44ebf47e_r"></figure>
<p>亦或是先從源碼構建出Modern Bundle, 然後從Modern Bundle 再構建出Legacy Bundle:</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-aebe74e543b85d2c821d96f43e916c67_r.jpg" data-caption="" data-size="normal" data-rawwidth="2160" data-rawheight="1054" class="origin_image zh-lightbox-thumb" width="2160" data-original="https://pic4.zhimg.com/v2-aebe74e543b85d2c821d96f43e916c67_b.jpg" title="v2-aebe74e543b85d2c821d96f43e916c67_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-aebe74e543b85d2c821d96f43e916c67_r.jpg" data-caption="" data-size="normal" data-rawwidth="2160" data-rawheight="1054" class="origin_image zh-lightbox-thumb lazy" width="2160" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='2160'%20height='1054'&gt;&lt;/svg&gt;" data-actualsrc="https://pic4.zhimg.com/v2-aebe74e543b85d2c821d96f43e916c67_b.jpg" title="v2-aebe74e543b85d2c821d96f43e916c67_r"></figure>
<blockquote><p>圖片來源: <a href="https://link.zhihu.com/?target=https%3A//web.dev/publish-modern-javascript/" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">web.dev/publish-modern-</span> <span class="invisible">javascript/</span></a></p></blockquote>
<p>Modern JavaScript 其實更像是一種理念，而不是一種技術，通過Modern JavaScript 可以讓開發者、用戶享受到技術進步帶來的福利，更甚者，Modern JavaScript 所帶來的存儲、傳輸、執行上的提升還能為地球<b>節能減排。</b></p>
<p>擺脫ES5 的禁錮，向Modern JavaScript 轉型！</p>
<h2>相關</h2>
<p>* <a href="https://link.zhihu.com/?target=https%3A//www.youtube.com/watch%3Fv%3DcLxNdLK--yI" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Transitioning to modern JavaScript</a></p>
<p>* <a href="https://link.zhihu.com/?target=https%3A//web.dev/publish-modern-javascript/" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Publish, ship, and install modern JavaScript for faster applications</a></p>
<p>* <a href="https://link.zhihu.com/?target=https%3A//web.dev/serve-modern-code-to-modern-browsers/" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Serve modern code to modern browsers for faster page loads</a></p>
<p>* <a href="https://link.zhihu.com/?target=https%3A//dev.to/garylchew/bringing-modern-javascript-to-libraries-432c" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Bringing Modern JavaScript to Libraries</a></p>
</div>
</div>
</article>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15301/topic-345085461/" data-wpel-link="internal">向Modern JavaScript 轉型</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>SEE Conf 2021 如期而至，體驗科技極緻美</title>
		<link>https://hypergrowths.com/software-engineering/front-end-dev/15295/topic-338795235/</link>
		
		<dc:creator><![CDATA[marketer]]></dc:creator>
		<pubDate>Sat, 30 Jan 2021 18:37:29 +0000</pubDate>
				<category><![CDATA[前端開發]]></category>
		<category><![CDATA[前端外刊評論]]></category>
		<guid isPermaLink="false">https://hypergrowths.com/software-engineering/front-end-dev/15295/topic-338795235/</guid>

					<description><![CDATA[<p>SEE Conf 2021將於2021年1月9日在杭州螞蟻Z空間舉辦。大會網站： https://seeconf.antfin.com/ SEE Conf（支付寶體驗科技大會）是由螞蟻集團主辦的，集專業分享、展台等內容於一身的科技盛筵。 SEE 是Seeking Expe…</p>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15295/topic-338795235/" data-wpel-link="internal">SEE Conf 2021 如期而至，體驗科技極緻美</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></description>
										<content:encoded><![CDATA[<article class="Post-Main Post-NormalMain" tabindex="-1">
<header class="Post-Header">
<h1 class="Post-Title">SEE Conf 2021 如期而至，體驗科技極緻美</h1>
<div class="Post-Author">
<div class="AuthorInfo" itemprop="author" itemscope="" itemtype="http://schema.org/Person"><meta itemprop="name" content="裕波"><meta itemprop="image" content="https://pic1.zhimg.com/f6eb46b0a_l.jpg?source=172ae18b"><meta itemprop="url" content="https://www.zhihu.com/people/yubozhou"><meta itemprop="zhihu:followerCount"></div>
</div>
</header>
<div class="Post-RichTextContainer">
<div class="RichText ztext Post-RichText">
<p>SEE Conf 2021將於2021年1月9日在杭州螞蟻Z空間舉辦。大會網站： <a href="https://link.zhihu.com/?target=https%3A//seeconf.antfin.com/" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">seeconf.antfin.com/</span></a></p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-e987351c3c4f2203d08896f8fe809c5e_r.jpg" data-size="normal" data-rawwidth="1708" data-rawheight="774" class="origin_image zh-lightbox-thumb" width="1708" data-original="https://pic3.zhimg.com/v2-e987351c3c4f2203d08896f8fe809c5e_b.jpg" title="v2-e987351c3c4f2203d08896f8fe809c5e_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-e987351c3c4f2203d08896f8fe809c5e_r.jpg" data-size="normal" data-rawwidth="1708" data-rawheight="774" class="origin_image zh-lightbox-thumb lazy" width="1708" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1708'%20height='774'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-e987351c3c4f2203d08896f8fe809c5e_b.jpg" title="v2-e987351c3c4f2203d08896f8fe809c5e_r"><figcaption> SEE Conf 2021</figcaption></figure>
<p><a href="https://link.zhihu.com/?target=https%3A//seeconf.antfin.com/" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">SEE Conf（支付寶體驗科技大會）</a>是由螞蟻集團主辦的，集專業分享、展台等內容於一身的科技盛筵。 SEE 是Seeking Experience &amp; Engineering 的縮寫，意為探索用戶體驗與工程實踐。我們希望通過SEE Conf, 與業界同行一起分享交流體驗科技的當前進展，探討切磋體驗科技的未來發展，共同努力促進體驗設計與技術的開放，讓生態繁榮共贏。</p>
<p>2020年1月初，SEE Conf 2020在西湖文體中心成功舉辦， 將設計與技術相融合，配合IMAX 大屏+ 杜比全景聲音效，給與會的近千位Developers &amp; Designers 帶來了極致的體驗，在知乎引發了<a href="https://www.zhihu.com/question/363807174" class="internal" data-wpel-link="external" rel="nofollow external noopener noreferrer">熱烈關注與討論</a>。</p>
<p>一年時光彷若白駒過隙，如今我們又將迎來SEE Conf 2021 了～</p>
<p><b>SEE Conf 2021將於2021年1月9日在杭州螞蟻Z空間舉辦。</b>下面讓我們來了解一下本屆SEE Conf 都有哪些看點吧～</p>
<p><b>主題&amp;會場</b></p>
<p>本次的主題是「探索極致用戶體驗與最佳工程實踐」，會議時長為一天，仍以<b>上午主會場</b>+<b>下午設計&amp;技術分會場</b>的形式開展，大家可以根據自己的興趣選聽。</p>
<p>除了主題演講之外，現場還有豐富的展台供大家互動、我們也邀請了樂隊現場表演～</p>
<h2><b>重磅大咖</b></h2>
<p><b>辛向陽</b></p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-08b6ea4c30e850e15091063ac5e63a44_r.jpg" data-size="normal" data-rawwidth="518" data-rawheight="510" class="origin_image zh-lightbox-thumb" width="518" data-original="https://pic1.zhimg.com/v2-08b6ea4c30e850e15091063ac5e63a44_b.jpg" title="v2-08b6ea4c30e850e15091063ac5e63a44_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-08b6ea4c30e850e15091063ac5e63a44_r.jpg" data-size="normal" data-rawwidth="518" data-rawheight="510" class="origin_image zh-lightbox-thumb lazy" width="518" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='518'%20height='510'&gt;&lt;/svg&gt;" data-actualsrc="https://pic1.zhimg.com/v2-08b6ea4c30e850e15091063ac5e63a44_b.jpg" title="v2-08b6ea4c30e850e15091063ac5e63a44_r"><figcaption>辛向陽</figcaption></figure>
<p>辛向陽，教授、博士生導師，XXY Innovation創始人，卡耐基梅隆大學設計哲學博士，四川美術學院特聘教授、澳門科技大學、江南大學、天津大學兼職教授。曾任江南大學設計學院院長、香港理工大學交互設計碩士課程主任等職務。具有跨機械、建築、平面設計、油畫、交互設計以及藝術史多個學科的教育和工作背景，主要研究交互、體驗、服務設計和組織創新等新興領域，提出了“交互五要素”、“行為邏輯”、“從用戶體驗到體驗設計”、服務設計“AC/EM”定位、“設計的蝴蝶效應：當生活方式成為設計對象”等交互與服務設計領域重要理論和方法。</p>
<p>辛向陽教授策劃和共同組織了交互設計國際大會、國際體驗設計大會、“設計教育再設計”、TTF轉型趨勢論壇等重要係列國際會議；應邀作為包括GMark、IXDA、紅星獎、台灣優秀工業設計大獎、台灣智造大獎等重要設計大賽評委；為寶潔公司總部、美國國家郵政局、香港特區政府、香港應用科技研究院、香港博愛醫院、GE、華為、長虹、騰訊、震旦、三星等公司提供過多種顧問獲研發服務；先後獲台灣工業設計十佳教育工作者、光華龍騰台灣設計貢獻獎銀質獎章、國際交互設計協會“交互設計未來之聲” 年度大獎、江蘇省歸僑僑眷先進個人、江蘇省教學成果一等獎、國家教學成果獎二等獎、光華設計基金會台灣設計貢獻獎金質獎章、“改革開放40年台灣設計40人”、“新台灣成立七十年台灣設計70人”等榮譽稱號。</p>
<p><b>辛向陽教授將為我們帶來「動機：戰略決策中的隱蔽因素」主題演講。</b></p>
<p><b>玉伯</b></p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-f6e3ef47d5f5d3fcba252c901e9be086_r.jpg" data-size="normal" data-rawwidth="496" data-rawheight="492" class="origin_image zh-lightbox-thumb" width="496" data-original="https://pic3.zhimg.com/v2-f6e3ef47d5f5d3fcba252c901e9be086_b.jpg" title="v2-f6e3ef47d5f5d3fcba252c901e9be086_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-f6e3ef47d5f5d3fcba252c901e9be086_r.jpg" data-size="normal" data-rawwidth="496" data-rawheight="492" class="origin_image zh-lightbox-thumb lazy" width="496" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='496'%20height='492'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-f6e3ef47d5f5d3fcba252c901e9be086_b.jpg" title="v2-f6e3ef47d5f5d3fcba252c901e9be086_r"><figcaption>玉伯</figcaption></figure>
<p>作為體驗技術部的負責人，玉伯對於SEE Conf這個baby可謂關心備至，每一屆都會進行「體驗科技」主題分享，上一屆的「 <a href="https://link.zhihu.com/?target=https%3A//www.yuque.com/yubo/morning/xtech-to-good-products" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">體驗科技與好的產品</a>」分享啟發了不少小伙伴（當然包括我啦嘿嘿）～</p>
<p>一起期待這次玉伯帶來的分享吧！</p>
<h2><b>演講主題</b></h2>
<p><b>主會場</b></p>
<ul>
<li>開場主題演講</li>
<li>動機：戰略決策中的隱蔽因素</li>
<li>Ant Design 資產工程化探索和實踐</li>
<li>圖釋萬物- AntV 圖可視分析解決方案</li>
<li>你不了解的Web動畫</li>
</ul>
<p><b>設計分會場</b></p>
<ul>
<li>普惠金融下的支付寶包容性設計</li>
<li>理財新趨勢：直播讓金融服務更普惠</li>
<li>新製造場景下的設計驅動：體驗直達一線</li>
<li>多元設計做物流數位化賦能-從軟硬一體到空間行為一致</li>
</ul>
<p><b>技術分會場</b></p>
<ul>
<li>企業級前端框架與工程化實踐</li>
<li>支付寶視覺稿轉代碼技術探索和實踐</li>
<li>支付寶小程序體驗優化實踐</li>
</ul>
<p><b>更多演講主題盡請期待！</b></p>
<h2>大會門票</h2>
<p>本次門票統一定價為<b>199元</b>，技術場與設計場同價。</p>
<p><b>售票時間</b>：即刻起至2021年1月8日</p>
<p><b>購票方式</b>：直接掃描下方二維碼，或者<a href="https://link.zhihu.com/?target=https%3A//www.bagevent.com/event/7020789" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">點擊鏈接</a>即可購票。</p>
<p><b>PS</b> ：由於螞蟻Z空間園區容納人數有限，門票數量有限，感興趣的朋友請趕緊搶票哈，售完即止喲～</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-ac7c93a73487ed9b17ba449fed26708e_r.jpg" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="1920" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://pic3.zhimg.com/v2-ac7c93a73487ed9b17ba449fed26708e_b.jpg" title="v2-ac7c93a73487ed9b17ba449fed26708e_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-ac7c93a73487ed9b17ba449fed26708e_r.jpg" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="1920" class="origin_image zh-lightbox-thumb lazy" width="1080" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1080'%20height='1920'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-ac7c93a73487ed9b17ba449fed26708e_b.jpg" title="v2-ac7c93a73487ed9b17ba449fed26708e_r"></figure>
</div>
</div>
</article>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15295/topic-338795235/" data-wpel-link="internal">SEE Conf 2021 如期而至，體驗科技極緻美</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>🎉 Element UI for Vue 3.0 來了！</title>
		<link>https://hypergrowths.com/software-engineering/front-end-dev/15291/topic-321311020/</link>
		
		<dc:creator><![CDATA[marketer]]></dc:creator>
		<pubDate>Sat, 30 Jan 2021 18:37:15 +0000</pubDate>
				<category><![CDATA[前端開發]]></category>
		<category><![CDATA[前端外刊評論]]></category>
		<guid isPermaLink="false">https://hypergrowths.com/software-engineering/front-end-dev/15291/topic-321311020/</guid>

					<description><![CDATA[<p>第一個使用TypeScript + Vue 3.0 Composition API 重構的組件庫Element Plus 發布了~ 2016 年3 月13 日Element 悄然誕生，經歷了4 年的風雨洗禮，我們從一個餓了麼內部業務組件庫成長為Vue 生態裡最流行的…</p>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15291/topic-321311020/" data-wpel-link="internal">🎉 Element UI for Vue 3.0 來了！</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></description>
										<content:encoded><![CDATA[<article class="Post-Main Post-NormalMain" tabindex="-1">
<header class="Post-Header">
<h1 class="Post-Title"><img src="https://s.w.org/images/core/emoji/14.0.0/72x72/1f389.png" alt="🎉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Element UI for Vue 3.0 來了！ </h1>
<div class="Post-Author">
<div class="AuthorInfo" itemprop="author" itemscope="" itemtype="http://schema.org/Person"><meta itemprop="name" content="朱昆 iamkun"><meta itemprop="image" content="https://pic1.zhimg.com/v2-8b1158e276178e02420f01264ccfb792_l.jpg?source=172ae18b"><meta itemprop="url" content="https://www.zhihu.com/people/iamkun"><meta itemprop="zhihu:followerCount"></div>
</div>
</header>
<div class="Post-RichTextContainer">
<div class="RichText ztext Post-RichText">
<blockquote><p>第一個使用TypeScript + Vue 3.0 Composition API 重構的組件庫Element Plus 發布了~</p></blockquote>
<p>2016 年3 月13 日Element 悄然誕生，經歷了4 年的風雨洗禮，我們從一個餓了麼內部業務組件庫成長為Vue 生態裡最流行的UI 組件庫之一。</p>
<p>截至本文撰寫時，Element已獲得<b>48200</b> Github Star， NPM下載量<b>95</b>萬次/每月的可喜成績。感謝超過530 名社區貢獻者的參與維護，和我們一同完成了4400 次commit 的更新迭代。</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-ef922581fbb5b6dcff6bbebb3c275da5_r.jpg" data-caption="" data-size="normal" data-rawwidth="1300" data-rawheight="435" class="origin_image zh-lightbox-thumb" width="1300" data-original="https://pic2.zhimg.com/v2-ef922581fbb5b6dcff6bbebb3c275da5_b.jpg" title="v2-ef922581fbb5b6dcff6bbebb3c275da5_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-ef922581fbb5b6dcff6bbebb3c275da5_r.jpg" data-caption="" data-size="normal" data-rawwidth="1300" data-rawheight="435" class="origin_image zh-lightbox-thumb lazy" width="1300" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1300'%20height='435'&gt;&lt;/svg&gt;" data-actualsrc="https://pic2.zhimg.com/v2-ef922581fbb5b6dcff6bbebb3c275da5_b.jpg" title="v2-ef922581fbb5b6dcff6bbebb3c275da5_r"></figure>
<p>*Element 開發團隊40000 Github Star 慶祝會</p>
<h2>Element Plus for Vue 3.0</h2>
<p>2020 年，隨著Vue 3.0 不斷完善和發布，我們也緊張投入到Element 對Vue 3.0 的升級適配工作中。歷經6 個月共計23 個alpha 版本的迭代，終於，在今天， Element Plus for Vue 3.0 Beta 版本正式發布！</p>
<p><a href="https://link.zhihu.com/?target=https%3A//github.com/element-plus/element-plus" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">github.com/element-plus</span> <span class="invisible">/element-plus</span></a></p>
<p>Vue 3.0 的大版本升級，對生態組件庫來說也是一次丟掉歷史包袱前行的好機會，我們大刀闊斧對Element 進行了一次深度重構。</p>
<p class="ztext-empty-paragraph"></p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-4e9f38e15f67b73f834ccf92e592ef4f_r.jpg" data-caption="" data-size="normal" data-rawwidth="2022" data-rawheight="916" class="origin_image zh-lightbox-thumb" width="2022" data-original="https://pic4.zhimg.com/v2-4e9f38e15f67b73f834ccf92e592ef4f_b.jpg" title="v2-4e9f38e15f67b73f834ccf92e592ef4f_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-4e9f38e15f67b73f834ccf92e592ef4f_r.jpg" data-caption="" data-size="normal" data-rawwidth="2022" data-rawheight="916" class="origin_image zh-lightbox-thumb lazy" width="2022" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='2022'%20height='916'&gt;&lt;/svg&gt;" data-actualsrc="https://pic4.zhimg.com/v2-4e9f38e15f67b73f834ccf92e592ef4f_b.jpg" title="v2-4e9f38e15f67b73f834ccf92e592ef4f_r"></figure>
<p class="ztext-empty-paragraph"></p>
<p>Element Plus for Vue 3.0 是一個使用TypeScript + Composition API 重構的全新項目。我們幾乎重寫了每一行Element 的代碼，用最Vue 3 的方式呈現了最完美的Element，主要有：</p>
<ul>
<li>使用TypeScript 開發，提供完整的類型定義文件</li>
<li>使用Vue 3.0 Composition API 降低耦合，簡化邏輯</li>
<li>使用Vue 3.0 Teleport 新特性重構掛載類組件</li>
<li>使用Lerna 維護和管理項目</li>
<li>使用更輕量更通用的時間日期解決方案Day.js</li>
<li>升級適配popperjs, async-validator 等核心依賴</li>
<li>完善52 種國際化語言支持</li>
</ul>
<p>除此以外，還有：</p>
<ul>
<li>全新的視覺*</li>
<li>優化的組件API</li>
<li>更多自定義選項</li>
<li>更加詳盡友好的文檔</li>
</ul>
<h3>Q：Element Plus 和Element UI 是什麼關係? 為什麼又一個新項目？</h3>
<p>正如<code>vue-next</code>之於<code>vue</code> ，一次100%的重構雖然解決了很多歷史遺留問題，但也不可避免的引入一些新的bug和問題，而獨立的issue和pr區可以減少大家使用和反饋的心智成本，也能更加方便我們定位問題，並行維護迭代。</p>
<p>Element will stay with Vue 2.x</p>
<p>For Vue 3.0, we recommend using Element Plus from the same team</p>
<p>具體可以參考Element的README <a href="https://link.zhihu.com/?target=https%3A//github.com/ElemeFE/element/blob/dev/README.md" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">github.com/ElemeFE/elem</span> <span class="invisible">ent/blob/dev/README.md</span></a></p>
<h3>Q: 老Element 項目可以平滑升級到Vue 3.0 + Element Plus 嗎？</h3>
<p>由於Vue 3.0 升級引入了部分API 的調整，老項目的升級不可避免的要做些許改動。但我們力爭把變更內容做到可控，只需要很少的調整就能完成項目升級。在今後Vue 3.0 的項目裡，還是熟悉的Element 的配方和味道。</p>
<h3>Q: Element UI 還會維護嗎？</h3>
<p>當然會！ (而且一直在正常迭代發布呀ヽ(✿ﾟ▽ﾟ)ノ)</p>
<p>每每看到社區類似的擔憂，對我們都是一種鞭策。作為一個負責任的開源項目，必然不會辜負大家的期待。隨著用戶的增多，肩上的壓力也越來越大，希望大家使用了Element 能真正為項目開發減負提效，這樣我們多加班，大家早下班，想到這，感覺胸前的紅領巾更鮮豔了。</p>
<h2>開始體驗吧</h2>
<p>有很多方式可以體驗Element Plus for Vue 3.0</p>
<ul>
<li>手動安裝<code>npm install element-plus</code> ，詳見官網<a href="https://link.zhihu.com/?target=https%3A//element-plus.org/%23/zh-CN/component/installation" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">安裝指南</a></li>
<li>下載<a href="https://link.zhihu.com/?target=https%3A//github.com/element-plus/element-plus-starter" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Webpack腳手架</a>直接運行體驗</li>
<li>嚐鮮時下最炫酷的Vite打包<a href="https://link.zhihu.com/?target=https%3A//github.com/element-plus/element-plus-vite-starter" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">腳手架</a></li>
<li>通過<a href="https://link.zhihu.com/?target=https%3A//github.com/element-plus/vue-cli-plugin-element-plus" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Vue CLI插件</a>引入</li>
</ul>
<h2>What's Next for Element Plus?</h2>
<ul>
<li>一次重大視覺更新</li>
<li>2 個全新組件緊張研發中</li>
<li>訪問速度更快，功能更強大的官方網站</li>
</ul>
<p>喜歡<a href="https://link.zhihu.com/?target=https%3A//github.com/element-plus/element-plus" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Element Plus</a>嗎？來點個<a href="https://link.zhihu.com/?target=https%3A//github.com/element-plus/element-plus/stargazers" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Star</a> <img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2764.png" alt="❤" class="wp-smiley" style="height: 1em; max-height: 1em;" />支持鼓勵我們一下吧~</p>
<p>感謝大家4 年以來對Element 的關注，是你們的支持讓Element 不斷完善，力爭做到最好。我們也會不忘開源初心，反哺社區，未來將對社區更加開放，非常歡迎熱愛開源的你來和我們一起完善Element Plus —— 贈人玫瑰，手有餘香，開源社區的維護，不僅僅是靠一個團隊，一個公司，我們相信是靠所有熱愛開源，擁有開源精神，熱衷於分享和交流的朋友們。</p>
</div>
</div>
</article>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15291/topic-321311020/" data-wpel-link="internal">🎉 Element UI for Vue 3.0 來了！</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>🚢 G2Plot 2.0 全新來襲&#8230;</title>
		<link>https://hypergrowths.com/software-engineering/front-end-dev/15277/topic-309265226/</link>
		
		<dc:creator><![CDATA[marketer]]></dc:creator>
		<pubDate>Sat, 30 Jan 2021 18:36:46 +0000</pubDate>
				<category><![CDATA[前端開發]]></category>
		<category><![CDATA[前端外刊評論]]></category>
		<guid isPermaLink="false">https://hypergrowths.com/software-engineering/front-end-dev/15277/topic-309265226/</guid>

					<description><![CDATA[<p>G2Plot 官網&#124; GitHub 開源地址https://github.com/antvis/G2Plot G2 是一個基於圖形語法，面向數據分析的統計圖表引擎。 G2Plot 是在G2 基礎上，屏蔽複雜概念的前提下，保留G2 強大圖形能力，封裝出業務上常用的…</p>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15277/topic-309265226/" data-wpel-link="internal">🚢 G2Plot 2.0 全新來襲&#8230;</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></description>
										<content:encoded><![CDATA[<article class="Post-Main Post-NormalMain" tabindex="-1">
<header class="Post-Header">
<h1 class="Post-Title"><img src="https://s.w.org/images/core/emoji/14.0.0/72x72/1f6a2.png" alt="🚢" class="wp-smiley" style="height: 1em; max-height: 1em;" /> G2Plot 2.0 全新來襲... </h1>
<div class="Post-Author">
<div class="AuthorInfo" itemprop="author" itemscope="" itemtype="http://schema.org/Person"><meta itemprop="name" content="逍为"><meta itemprop="image" content="https://pic4.zhimg.com/v2-ae98845882aceeae3f4befabf942ee7f_l.jpg?source=172ae18b"><meta itemprop="url" content="https://www.zhihu.com/people/aiBQ"><meta itemprop="zhihu:followerCount"></div>
</div>
</header>
<div class="Post-RichTextContainer">
<div class="RichText ztext Post-RichText">
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-df4e6efccac580fc20eee7e0b6d3f341_r.jpg" data-caption="" data-size="normal" data-rawwidth="2394" data-rawheight="1311" class="origin_image zh-lightbox-thumb" width="2394" data-original="https://pic2.zhimg.com/v2-df4e6efccac580fc20eee7e0b6d3f341_b.jpg" title="v2-df4e6efccac580fc20eee7e0b6d3f341_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-df4e6efccac580fc20eee7e0b6d3f341_r.jpg" data-caption="" data-size="normal" data-rawwidth="2394" data-rawheight="1311" class="origin_image zh-lightbox-thumb lazy" width="2394" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='2394'%20height='1311'&gt;&lt;/svg&gt;" data-actualsrc="https://pic2.zhimg.com/v2-df4e6efccac580fc20eee7e0b6d3f341_b.jpg" title="v2-df4e6efccac580fc20eee7e0b6d3f341_r"></figure>
<p><a href="https://link.zhihu.com/?target=https%3A//g2plot.antv.vision/" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">G2Plot官網</a>| <a href="https://link.zhihu.com/?target=https%3A//github.com/antvis/G2Plot" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">GitHub開源地址</a><a href="https://link.zhihu.com/?target=https%3A//github.com/antvis/G2Plot" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">github.com/antvis/G2Plo</span> <span class="invisible">t</span></a></p>
<blockquote><p>G2 是一個基於圖形語法，面向數據分析的統計圖表引擎。 G2Plot 是在G2 基礎上，屏蔽複雜概念的前提下，保留G2 強大圖形能力，封裝出業務上常用的統計圖表庫。</p></blockquote>
<p><b><a href="https://link.zhihu.com/?target=https%3A//github.com/antvis/G2Plot" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">G2Plot</a></b>是一個基於配置、體驗優雅、面向數據分析的<b>統計圖表庫，</b>幫助開發者以最小成本繪製高質量統計圖表。</p>
<p>G2Plot 最初誕生於阿里經濟體BI 產品真實場景的業務訴求。</p>
<p>動態數據量、數據場景不確定是BI 產品和中台系統業務數據的一個普遍特徵，而這種特徵對統計圖表的功能和體驗提出了巨大的挑戰。如何能夠幫助報表系統和一線前端在復雜數據條件下快速高效地創建統計圖表，同時保證圖表在各種顯示空間和數據狀態下的可讀性和可用性？為解決這兩個痛點問題，AntV 與DeepInsight、QuickBI 和FBI，阿里經濟體多個優秀的數據BI 產品技術團隊聯手打造了G2Plot。</p>
<h2>G2Plot 2.0 帶來了什麼？</h2>
<p>G2Plot 2.0版本，我們開啟了全新技術架構，全面依賴G2。一圖一做擴展常見的業務圖表，也提供了自定義擴展能力，滿足自定義個性化圖表的訴求。下面讓我們來看下，主要帶來的重要的能力和特性。</p>
<h3>開箱即用的統計圖表</h3>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-32265b3fa918a4ba0d2ddfc20f71ac0a_r.jpg" data-caption="" data-size="normal" data-rawwidth="2000" data-rawheight="1901" class="origin_image zh-lightbox-thumb" width="2000" data-original="https://pic3.zhimg.com/v2-32265b3fa918a4ba0d2ddfc20f71ac0a_b.jpg" title="v2-32265b3fa918a4ba0d2ddfc20f71ac0a_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-32265b3fa918a4ba0d2ddfc20f71ac0a_r.jpg" data-caption="" data-size="normal" data-rawwidth="2000" data-rawheight="1901" class="origin_image zh-lightbox-thumb lazy" width="2000" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='2000'%20height='1901'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-32265b3fa918a4ba0d2ddfc20f71ac0a_b.jpg" title="v2-32265b3fa918a4ba0d2ddfc20f71ac0a_r"></figure>
<p>2020年AntV的slogan是<b>利業·立業</b>，這也是G2棧今年一直在做的事情，立足業務，盈利業務。我們按照業務上優先級和使用量佔比，排列優先級，優先做了以下圖表：</p>
<blockquote><p>折線圖、面積圖、柱形圖系列（堆積、分組、區間、百分比、條圖）、餅環圖、雙軸圖、散點圖、雷達圖、詞云圖、直方圖、瀑布圖、子彈圖、儀錶盤、水波圖、漏斗圖、mini 折柱餅、進度環/條圖、熱力圖、旭日圖...</p></blockquote>
<p>其中上述頭部超過50% 在內部的重型BI 產品、搭建產品中實踐和使用，排除和解決基本的設計問題和技術漏洞。當然還有一些開發階段的圖表，大家可以試用並提出自己的需求，我們會開發階段充分參考大家的意見和建議。</p>
<h3>全新啟航的技術架構</h3>
<p>新的G2Plot 架構將全面依賴G2，G2Plot 層僅僅是基於G2 強大的圖形、交互、事件、動畫能力，去一圖一做的擴展不同的常見業務圖表。</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-9d98653ca01396e8d9f219ca9a539089_r.jpg" data-caption="" data-size="normal" data-rawwidth="882" data-rawheight="433" class="origin_image zh-lightbox-thumb" width="882" data-original="https://pic2.zhimg.com/v2-9d98653ca01396e8d9f219ca9a539089_b.jpg" title="v2-9d98653ca01396e8d9f219ca9a539089_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-9d98653ca01396e8d9f219ca9a539089_r.jpg" data-caption="" data-size="normal" data-rawwidth="882" data-rawheight="433" class="origin_image zh-lightbox-thumb lazy" width="882" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='882'%20height='433'&gt;&lt;/svg&gt;" data-actualsrc="https://pic2.zhimg.com/v2-9d98653ca01396e8d9f219ca9a539089_b.jpg" title="v2-9d98653ca01396e8d9f219ca9a539089_r"></figure>
<p>整體核心架構開發一個圖表最主要的核心工作就是開發Adaptor，所以圖表，無論是常規統計圖表，還是開發擴展的自定義業務圖表，都按照這樣的模式開發，形成統一的開發心智。 **</p>
<p>這樣做將帶來：</p>
<ol>
<li>降低架構複雜圖，降低開發入門門檻，提升可維護性。</li>
<li>充分利用和發揮G2 能力，保障技術概念上和G2 一模一樣，一次學習，整體技術棧收益，降低用戶使用門檻。</li>
<li>通過一圖一做，來沉澱、打磨G2 的圖形、交互語法能力。</li>
</ol>
<h3><img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 創意無限的開放能力</h3>
<p>G2Plot 內置的是業務中使用量佔比超過90% 的常規統計圖表，而對於業務產品來說，定制不可避免，這個時候，用戶將面臨選擇：① 使用G2 去開發② 建議G2Plot 增加圖表，抑或是③ 使用其他圖表庫、或者自研...</p>
<p>而我們作為G2Plot 開發團隊，也陷入一些問題：</p>
<ol>
<li>這個圖表通用嗎？是不是太跟隨業務了？如何抽象配置項？</li>
<li>開發者基於G2 開發了，能開源給其他需要的用戶使用嗎？</li>
</ol>
<p>基於這些問題，我們將G2Plot 基於G2 開發圖表的Adaptor 模式直接開放出來，業務同學可以基於這一個模式去使用G2 封裝定製圖表，如果需要給其他業務復用，直接發包，在G2Plot 的模式上去使用。舉個例子：</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-13242a9829beb6893d98c21fa2211a4e_r.jpg" data-caption="" data-size="normal" data-rawwidth="1708" data-rawheight="496" class="origin_image zh-lightbox-thumb" width="1708" data-original="https://pic3.zhimg.com/v2-13242a9829beb6893d98c21fa2211a4e_b.jpg" title="v2-13242a9829beb6893d98c21fa2211a4e_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-13242a9829beb6893d98c21fa2211a4e_r.jpg" data-caption="" data-size="normal" data-rawwidth="1708" data-rawheight="496" class="origin_image zh-lightbox-thumb lazy" width="1708" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1708'%20height='496'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-13242a9829beb6893d98c21fa2211a4e_b.jpg" title="v2-13242a9829beb6893d98c21fa2211a4e_r"></figure>
<p>以上是一個真實的基於G2 按照G2Plot 的模式開發一個QRCode 圖形能力，整體代碼100 行左右。基於這樣的模式，目前具備有以下的社區擴展，可以到如何<a href="https://link.zhihu.com/?target=https%3A//g2plot.antv.vision/zh/docs/manual/plugin" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">開發自定義擴展圖表</a>。</p>
<h3>隨手查閱的交互文檔</h3>
<p>G2 棧基於圖形語法，具備有很強的靈活性，G2 是麵粉，G2Plot 是麵條。而在靈活性的背後，確必然存在很大的學習成本。之前的G2 文檔是被吐槽最多的點。這一次針對G2, G2Plot 的特殊性，我們推出交互式文檔，嘗試去解決文檔問題，降低文檔查找的成本，幫助開發者提效。為完成這一目標，我們主要進行了以下三步改造：</p>
<p>第一步，<b>教程瘦身</b>，在教程部分重點講述相關的可視化概念及其配合用法，以達到“舉一”的作用，而將“反三”交給API文檔，盡量避免訊息重複透出使開發者在API 文檔和教程之間來回橫跳；</p>
<p>第二步，<b>結構化API文檔</b>，從使用視角出發，以調用模塊為單元全量羅列API內容。同時在文檔內還加入了可以執行的代碼區塊，看文檔的同時想試一下效果？ so easy～</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-6cbd2f7f405c882e6b1c4f1aeca434c8_r.jpg" data-caption="" data-size="normal" data-rawwidth="3584" data-rawheight="2194" class="origin_image zh-lightbox-thumb" width="3584" data-original="https://pic1.zhimg.com/v2-6cbd2f7f405c882e6b1c4f1aeca434c8_b.jpg" title="v2-6cbd2f7f405c882e6b1c4f1aeca434c8_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-6cbd2f7f405c882e6b1c4f1aeca434c8_r.jpg" data-caption="" data-size="normal" data-rawwidth="3584" data-rawheight="2194" class="origin_image zh-lightbox-thumb lazy" width="3584" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='3584'%20height='2194'&gt;&lt;/svg&gt;" data-actualsrc="https://pic1.zhimg.com/v2-6cbd2f7f405c882e6b1c4f1aeca434c8_b.jpg" title="v2-6cbd2f7f405c882e6b1c4f1aeca434c8_r"></figure>
<p>第三步， <b>Demo +文檔</b>，除代碼區和預覽區外，將API文檔也放到一起，方便使用者在看demo的同時查閱文檔，避免反复橫跳。同時升級了代碼編輯器，可以進行代碼提示和類型查看。另外我們還有一個隱藏功能，可以選中- 右建- search in document 快速定位到相關描述哦。</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://pic1.zhimg.com/v2-2ba05847852b1e164c11f18d048467f8_r.jpg" data-caption="" data-size="normal" data-rawwidth="2000" data-rawheight="1197" data-thumbnail="https://pic1.zhimg.com/v2-2ba05847852b1e164c11f18d048467f8_b.jpg" class="origin_image zh-lightbox-thumb" width="2000" data-original="https://pic1.zhimg.com/v2-2ba05847852b1e164c11f18d048467f8_b.gif"></noscript><img decoding="async" src="https://pic1.zhimg.com/v2-2ba05847852b1e164c11f18d048467f8_r.jpg" data-caption="" data-size="normal" data-rawwidth="2000" data-rawheight="1197" data-thumbnail="https://pic1.zhimg.com/v2-2ba05847852b1e164c11f18d048467f8_b.jpg" class="origin_image zh-lightbox-thumb lazy" width="2000" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='2000'%20height='1197'&gt;&lt;/svg&gt;" data-actualsrc="https://pic1.zhimg.com/v2-2ba05847852b1e164c11f18d048467f8_b.gif"></figure>
<h3>匠心延續的體驗打磨</h3>
<p>G2Plot 是一個注重細節體驗的通用統計圖表庫，在之前版本中和實際業務中，我們花費了大量精力打磨各個圖表的細節體驗。在v2 版本中，我們把通用的體驗優從業務實現抽取出來，通用化配置化，把大部分體驗優化方案內置到G2 中，讓G2 和G2Plot 的用戶都可以直接配置開啟各個圖表的體驗優化，用戶可以方便地通過配置和選項來開啟默認內置的體驗優化和優化方案。</p>
<ul>
<li><b>坐標軸標籤優化</b></li>
</ul>
<p>你是不是也遇到過在坐標軸數據密集或者動態變化的時候，坐標軸標籤出來重疊遮擋的問題？在實際業務場景中數據可能動態變化，設置固定的旋轉角度並不難一勞永逸的解決問題；因此在v2版本中，我們內置了自動抽樣隱藏( <code>autoHide</code> )、自動旋轉( <code>autoRotate</code> )等內置的佈局方案，用戶可簡單的通過配置開啟，在不同數據情況下動態的解決坐標軸標籤的重疊問題。</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-6e789bdbf8736357e8dc1eff6bdb8530_r.jpg" data-caption="" data-size="normal" data-rawwidth="1452" data-rawheight="388" class="origin_image zh-lightbox-thumb" width="1452" data-original="https://pic1.zhimg.com/v2-6e789bdbf8736357e8dc1eff6bdb8530_b.jpg" title="v2-6e789bdbf8736357e8dc1eff6bdb8530_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-6e789bdbf8736357e8dc1eff6bdb8530_r.jpg" data-caption="" data-size="normal" data-rawwidth="1452" data-rawheight="388" class="origin_image zh-lightbox-thumb lazy" width="1452" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1452'%20height='388'&gt;&lt;/svg&gt;" data-actualsrc="https://pic1.zhimg.com/v2-6e789bdbf8736357e8dc1eff6bdb8530_b.jpg" title="v2-6e789bdbf8736357e8dc1eff6bdb8530_r"></figure>
<ul>
<li><b>數據標籤優化</b></li>
</ul>
<p>數據標籤是可視化圖標的一個重要組成部分，但在實際業務場景中，實際的業務圖表中的數據標籤卻體驗非常差，經常出現數據標籤和數據標籤之間的重疊遮擋，數據標籤配色造成的可閱讀體驗差等問題。此前，我們在實際業務中實踐了大量的數據標籤優化方案，在v2版本中，我們把比較通用的優化方案抽取內置到G2中，作為數據標籤的可選佈局方案，用戶可以通過<code>layout</code>配置來開啟希望使用的優化方案。在下面範例中:</p>
<ul>
<li>interval-hide-overlap: 適用與柱形圖和條形圖系列圖形，開啟後可避免數據標籤之間的遮擋問題</li>
<li>adjust-color: 適用與所有含有內部數據標籤的圖形，開啟後可自動調整數據標籤文本顏色，提升可閱讀性</li>
<li>pie-spider: 適用與餅圖，開啟後可適用餅圖數據標籤蜘蛛佈局</li>
</ul>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-b85f3a774f9296fa2e970133c5bd586d_r.jpg" data-caption="" data-size="normal" data-rawwidth="1468" data-rawheight="1326" class="origin_image zh-lightbox-thumb" width="1468" data-original="https://pic2.zhimg.com/v2-b85f3a774f9296fa2e970133c5bd586d_b.jpg" title="v2-b85f3a774f9296fa2e970133c5bd586d_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-b85f3a774f9296fa2e970133c5bd586d_r.jpg" data-caption="" data-size="normal" data-rawwidth="1468" data-rawheight="1326" class="origin_image zh-lightbox-thumb lazy" width="1468" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1468'%20height='1326'&gt;&lt;/svg&gt;" data-actualsrc="https://pic2.zhimg.com/v2-b85f3a774f9296fa2e970133c5bd586d_b.jpg" title="v2-b85f3a774f9296fa2e970133c5bd586d_r"></figure>
<p>更多layout配置和使用可以參照G2Plot <a href="https://link.zhihu.com/?target=https%3A//g2plot.antv.vision/zh/examples/column/grouped%23basic" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">文檔和範例</a>。</p>
<h2>還帶來什麼？</h2>
<h3>Ant Design Charts</h3>
<p><a href="https://link.zhihu.com/?target=https%3A//charts.ant.design/" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Ant Design Charts</a>基於G2Plot，彌補Ant Design組件庫在統計圖表上的缺失，作為Ant Design的官方圖表組件解決方案。在圖表能力上，和G2Plot 保持一致，不修改技術概念，不修改配置結構。不僅降低這個模塊的維護成本，同時降低開發者使用的開發理解成本。</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-cf27276346f49b294bf6ef2387261eb4_r.jpg" data-caption="" data-size="normal" data-rawwidth="1918" data-rawheight="740" class="origin_image zh-lightbox-thumb" width="1918" data-original="https://pic1.zhimg.com/v2-cf27276346f49b294bf6ef2387261eb4_b.jpg" title="v2-cf27276346f49b294bf6ef2387261eb4_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-cf27276346f49b294bf6ef2387261eb4_r.jpg" data-caption="" data-size="normal" data-rawwidth="1918" data-rawheight="740" class="origin_image zh-lightbox-thumb lazy" width="1918" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1918'%20height='740'&gt;&lt;/svg&gt;" data-actualsrc="https://pic1.zhimg.com/v2-cf27276346f49b294bf6ef2387261eb4_b.jpg" title="v2-cf27276346f49b294bf6ef2387261eb4_r"></figure>
<h3>節省400kb, 瘋狂瘦身</h3>
<p>包大小是前端底層模塊，非常重要的指標。這次版本，我們將原包大小從1.3 Mb 降低到800 Kb 內，瘋狂減包接近40%。如果業務中只使用G2Plot 部分圖表，通過按需引入打包，將降低到更小。未來我們會繼續在G2 的基礎上進行進一步的精細化拆分，進一步降低包大小。</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-60730afae0dde51c2db5e89d1f39a590_r.jpg" data-caption="" data-size="normal" data-rawwidth="1218" data-rawheight="620" class="origin_image zh-lightbox-thumb" width="1218" data-original="https://pic1.zhimg.com/v2-60730afae0dde51c2db5e89d1f39a590_b.jpg" title="v2-60730afae0dde51c2db5e89d1f39a590_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-60730afae0dde51c2db5e89d1f39a590_r.jpg" data-caption="" data-size="normal" data-rawwidth="1218" data-rawheight="620" class="origin_image zh-lightbox-thumb lazy" width="1218" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1218'%20height='620'&gt;&lt;/svg&gt;" data-actualsrc="https://pic1.zhimg.com/v2-60730afae0dde51c2db5e89d1f39a590_b.jpg" title="v2-60730afae0dde51c2db5e89d1f39a590_r"></figure>
<h3>95%+ 單元測試覆蓋率</h3>
<p>代碼質量和穩定性，不論是底層模塊，還是產品業務，對技術同學來說，都是無限追求。為了解決之前版本帶來的穩定性問題，我們在2.0 開始之初，就強制要求有效的單元測試覆蓋率，並且在後續的迭代中，不斷提升覆蓋率要求</p>
<p>目前的CI覆蓋率狀況： <b>97%+</b></p>
<p>除此之外，我們近期會開發自動化回歸腳本，在每次發布版本前，回歸網站上的DEMO 範例，進行像素級的自動對比，來決定此次發布是否帶來重大問題，是否符合質量要求。</p>
<h3>深思熟慮的圖表配置</h3>
<p>在一圖一做，封裝圖表過程中，我們看到很多的圖表，具備相同的數據表達能力，自然在配置上也會體現出類似的感覺。我們在圖表配置API 上，盡量做到統一，讓開發者在熟悉一些圖表之後，對於新圖表，下意識認知上，可以基本知道如何去配置，這也是在解決之前版本中，不同圖表配置項各自獨立不同帶來的問題。包括：</p>
<ul>
<li>顏色、style 配置統一</li>
<li>餅環圖、水波圖、儀錶盤、進度環圖中的統計文本類配置統一</li>
<li>多圖層圖形配置統一（對於多圖層配置，我們統一定義為: GeometryOptions，統一且方便擴展，如雙軸圖、對稱條形圖中使用多View 圖層，多Geometry 能力來渲染）</li>
</ul>
<h3>實驗室：多圖層圖表</h3>
<p>G2Plot 是基於G2，按照圖表分類學來進行一圖一做，降低使用門檻的同時，也限制了底層G2 的自定義能力。為了對圖形語法非常熟悉的同學，我們通過MultiView 多圖層圖表完全透漏出G2 80%+ 的圖形語法配置能力。</p>
<p>諾貝爾獎可視化（G2Plot 多圖層實驗室繪製）</p>
<p>這個可以用來做什麼？</p>
<ol>
<li>多圖層層疊圖表，例如：嵌套餅圖、折線圖+統計區間...</li>
<li>多維圖</li>
<li>組合圖：餅+柱混合圖</li>
</ol>
<p>如下圖所示的一些複雜場景都可以使用MultiView Plot 來配置式的繪製。</p>
<figure data-size="small"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-31c105271dacea3eb165a21419f27225_r.jpg" data-caption="" data-size="small" data-rawwidth="1256" data-rawheight="930" class="origin_image zh-lightbox-thumb" width="1256" data-original="https://pic2.zhimg.com/v2-31c105271dacea3eb165a21419f27225_b.jpg" title="v2-31c105271dacea3eb165a21419f27225_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-31c105271dacea3eb165a21419f27225_r.jpg" data-caption="" data-size="small" data-rawwidth="1256" data-rawheight="930" class="origin_image zh-lightbox-thumb lazy" width="1256" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1256'%20height='930'&gt;&lt;/svg&gt;" data-actualsrc="https://pic2.zhimg.com/v2-31c105271dacea3eb165a21419f27225_b.jpg" title="v2-31c105271dacea3eb165a21419f27225_r"></figure>
<figure data-size="small"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-142e6bf8810545b81fa0a58c19d3f39c_r.jpg" data-caption="" data-size="small" data-rawwidth="1474" data-rawheight="804" class="origin_image zh-lightbox-thumb" width="1474" data-original="https://pic1.zhimg.com/v2-142e6bf8810545b81fa0a58c19d3f39c_b.jpg" title="v2-142e6bf8810545b81fa0a58c19d3f39c_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-142e6bf8810545b81fa0a58c19d3f39c_r.jpg" data-caption="" data-size="small" data-rawwidth="1474" data-rawheight="804" class="origin_image zh-lightbox-thumb lazy" width="1474" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1474'%20height='804'&gt;&lt;/svg&gt;" data-actualsrc="https://pic1.zhimg.com/v2-142e6bf8810545b81fa0a58c19d3f39c_b.jpg" title="v2-142e6bf8810545b81fa0a58c19d3f39c_r"></figure>
<figure data-size="small"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-f620fd881546bd16cf7dd6bfad455ee7_r.jpg" data-caption="" data-size="small" data-rawwidth="1562" data-rawheight="1064" class="origin_image zh-lightbox-thumb" width="1562" data-original="https://pic4.zhimg.com/v2-f620fd881546bd16cf7dd6bfad455ee7_b.jpg" title="v2-f620fd881546bd16cf7dd6bfad455ee7_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-f620fd881546bd16cf7dd6bfad455ee7_r.jpg" data-caption="" data-size="small" data-rawwidth="1562" data-rawheight="1064" class="origin_image zh-lightbox-thumb lazy" width="1562" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1562'%20height='1064'&gt;&lt;/svg&gt;" data-actualsrc="https://pic4.zhimg.com/v2-f620fd881546bd16cf7dd6bfad455ee7_b.jpg" title="v2-f620fd881546bd16cf7dd6bfad455ee7_r"></figure>
<figure data-size="small"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-0d49d1525ceab7f3016bba4df9ba5f46_r.jpg" data-caption="" data-size="small" data-rawwidth="1498" data-rawheight="974" class="origin_image zh-lightbox-thumb" width="1498" data-original="https://pic3.zhimg.com/v2-0d49d1525ceab7f3016bba4df9ba5f46_b.jpg" title="v2-0d49d1525ceab7f3016bba4df9ba5f46_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-0d49d1525ceab7f3016bba4df9ba5f46_r.jpg" data-caption="" data-size="small" data-rawwidth="1498" data-rawheight="974" class="origin_image zh-lightbox-thumb lazy" width="1498" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1498'%20height='974'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-0d49d1525ceab7f3016bba4df9ba5f46_b.jpg" title="v2-0d49d1525ceab7f3016bba4df9ba5f46_r"></figure>
<p>目前在實驗室階段，具體使用方式請看官網<a href="https://link.zhihu.com/?target=https%3A//g2plot.antv.vision/zh/examples/case/multi-view" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">DEMO</a></p>
<h3>升級指南</h3>
<p>2.0 版本和之前的版本，在核心架構和概念，有了非常大的轉變，我們深知這樣的break change 會帶來多大的業務升級迭代成本。但是我們還是堅定的要做這個事情，保證不給後續留下業務包袱，保證後續的可持續迭代。</p>
<p>為了降低之前老用戶的升級成本，我們非常用心的寫了CHANGELOG 和升級指南，幫助大家升級到最新的穩定、持續迭代的2.0 版本。</p>
<p>升級指南地址見： <a href="https://link.zhihu.com/?target=https%3A//g2plot.antv.vision/zh/docs/manual/upgrade" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">g2plot.antv.vision/zh/d</span> <span class="invisible">ocs/manual/upgrade</span></a></p>
<h2>最後</h2>
<p>非常感謝你的耐心閱讀。帶著我們對業務，對技術架構的理解，我們將G2Plot從1.0升級到2.0，歡迎在<a href="https://link.zhihu.com/?target=https%3A//github.com/antvis/x6" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">GitHub</a>給我們反饋問題，更加歡迎有興趣的同學來參入貢獻。</p>
<hr>
<h2>附</h2>
<h3>AntV 2020 品牌日的發布詳情</h3>
<ul>
<li><a href="https://link.zhihu.com/?target=https%3A//www.yuque.com/antv/blog/2020story" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">利業·立業- AntV與業務的故事</a></li>
<li><b>技術專文</b></li>
<li><a href="https://link.zhihu.com/?target=https%3A//www.yuque.com/antv/blog/2020g2" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">G2Plot 2.0全新來襲</a></li>
<li><a href="https://link.zhihu.com/?target=https%3A//www.yuque.com/antv/blog/2020f2" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">F2Native 1.0新的起點</a></li>
<li><a href="https://link.zhihu.com/?target=https%3A//www.yuque.com/antv/blog/2020g6" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">G6 4.0圖可視分析如此簡單</a></li>
<li><a href="https://link.zhihu.com/?target=https%3A//www.yuque.com/antv/blog/2020x6" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">X6 1.0抱歉來晚</a></li>
<li><a href="https://link.zhihu.com/?target=https%3A//www.yuque.com/antv/blog/2020l7" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">L7 2.3業務為本完善生態</a></li>
<li><a href="https://link.zhihu.com/?target=https%3A//www.yuque.com/antv/blog/2020ava" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">AVA 1.0你的圖表參謀</a></li>
<li><b>設計專文</b></li>
<li><a href="https://link.zhihu.com/?target=https%3A//www.yuque.com/antv/blog/2020design" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">2020，貼地飛行的AntV設計</a></li>
</ul>
<h3>AntV 項目鏈接</h3>
<p>歡迎關注我們的GitHub 項目，點亮star 了解我們的實時動態，期待PR：</p>
<ul>
<li><b>AntV官網</b>： <a href="https://link.zhihu.com/?target=https%3A//antv.vision/" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">antv.vision/</span></a></li>
<li><b>G2</b> ： <a href="https://link.zhihu.com/?target=https%3A//github.com/antvis/g2" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">github.com/antvis/g2</span></a> <i>G2是一套基於可視化編碼的圖形語法，以數據驅動，具有高度的易用性和擴展性，用戶無需關注各種繁瑣的實現細節，一條語句即可構建出各種各樣的可交互的統計圖表。</i></li>
<li><b>G2Plot：</b> <a href="https://link.zhihu.com/?target=https%3A//github.com/antvis/g2plot" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">github.com/antvis/g2plo</span> <span class="invisible">t</span></a> G2Plot的定位是開箱即用、易於配置、具有良好視覺和交互體驗的通用圖表庫。</li>
<li><b>F2</b> ： <a href="https://link.zhihu.com/?target=https%3A//github.com/antvis/f2" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">github.com/antvis/f2</span></a> <i>F2是一個專注於移動，開箱即用的可視化解決方案，完美支持H5環境同時兼容多種環境（node,小程序，weex）。完備的圖形語法理論，滿足各種可視化需求。專業的移動設計指引為你帶來最佳的移動端圖表體驗。</i></li>
<li><b>F2Native</b> ： <a href="https://link.zhihu.com/?target=https%3A//github.com/antvis/F2Native" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">github.com/antvis/F2Nat</span> <span class="invisible">ive</span></a> <i>F__2Native是一個專注於客戶端，開箱即用、高性能的可視化解決方案，完備的圖形語法理論，滿足你的各種需求，專業的移動設計指引為你帶來最佳的移動端圖表體驗。</i></li>
<li><b>G6</b> ： <a href="https://link.zhihu.com/?target=https%3A//github.com/antvis/g6" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">github.com/antvis/g6</span></a> <i>G6是AntV旗下的圖可視化與圖分析引擎<b>，</b> G來自於Graphic、Graph ，意味著我們要基於圖分析技術做圖可視化；6來自於《 <a href="https://link.zhihu.com/?target=https%3A//zh.wikipedia.org/wiki/%25E5%2585%25AD%25E5%25BA%25A6%25E5%2588%2586%25E9%259A%2594%25E7%2590%2586%25E8%25AE%25BA" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">六度分隔理論</a>》，表達了我們對關係數據、關係網絡的敬畏和著迷。</i></li>
<li><b>Graphin：</b> <a href="https://link.zhihu.com/?target=https%3A//github.com/antvis/graphin" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">github.com/antvis/graph</span> <span class="invisible">in</span></a> <i>Graphin是一個基於G6封裝<b>的關係可視分析工具</b>__，簡單，高效，開箱即用，取自Graph Insight，圖的分析洞察。</i></li>
<li><b>X6</b> ： <a href="https://link.zhihu.com/?target=https%3A//github.com/antvis/X6" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">github.com/antvis/X6</span></a> <i>X6是AntV旗下的圖編輯引擎，提供了一系列開箱即用的交互組件和簡單易用的節點定制能力，方便我們快速搭建DAG圖、ER圖、流程圖等應用。</i></li>
<li><b>L7</b> ： <a href="https://link.zhihu.com/?target=https%3A//github.com/antvis/l7" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">github.com/antvis/l7</span></a> <i>L7是一個基於WebGL的開源<b>大規模地理空間數據可視分析開發框架。</b> L7中的L代表Location，7代表世界七大洲，寓意能為全球位置數據提供可視分析的能力。</i></li>
<li><b>AVA</b> ： <a href="https://link.zhihu.com/?target=https%3A//github.com/antvis/AVA" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">github.com/antvis/AVA</span></a> <i>AVA是為了更簡便的可視分析而生的智能可視化框架。</i></li>
<li><b>G</b> ： <a href="https://link.zhihu.com/?target=https%3A//github.com/antvis/g" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">github.com/antvis/g</span></a> <i>G是AntV幾個產品共同的底層2D渲染引擎，高效易用，專注於圖形的渲染、拾取、事件以及動畫機制，給上層G2、F2、G6提供統一的渲染機制。</i></li>
<li><b>ChartCube</b> ： <a href="https://link.zhihu.com/?target=https%3A//chartcube.alipay.com/" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">https://chartcube.alipay.com</a> <i>ChartCube是一個可以快速完成圖表製作的在線工具，只需要三步就可以創建出高品質的圖表。</i></li>
</ul>
</div>
</div>
</article>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15277/topic-309265226/" data-wpel-link="internal">🚢 G2Plot 2.0 全新來襲&#8230;</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>解決Web 應用與瀏覽器快捷鍵衝突的一條野路子</title>
		<link>https://hypergrowths.com/software-engineering/front-end-dev/15253/topic-300659062/</link>
		
		<dc:creator><![CDATA[marketer]]></dc:creator>
		<pubDate>Sat, 30 Jan 2021 18:36:08 +0000</pubDate>
				<category><![CDATA[前端開發]]></category>
		<category><![CDATA[前端外刊評論]]></category>
		<guid isPermaLink="false">https://hypergrowths.com/software-engineering/front-end-dev/15253/topic-300659062/</guid>

					<description><![CDATA[<p>先介紹下大背景，我們是Ant Codespaces 團隊，主要為螞蟻的工程師們提供雲上研發能力。我們對外透出的是一個典型的B/S 應用，用戶無需在本地下載任何軟件，在瀏覽器中就能完成標準技術棧場景下的研發工作，於此…</p>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15253/topic-300659062/" data-wpel-link="internal">解決Web 應用與瀏覽器快捷鍵衝突的一條野路子</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></description>
										<content:encoded><![CDATA[<article class="Post-Main Post-NormalMain" tabindex="-1">
<header class="Post-Header">
<h1 class="Post-Title">解決Web 應用與瀏覽器快捷鍵衝突的一條野路子</h1>
<div class="Post-Author">
<div class="AuthorInfo" itemprop="author" itemscope="" itemtype="http://schema.org/Person"><meta itemprop="name" content="茶山小旋风"><meta itemprop="image" content="https://pic4.zhimg.com/v2-22afcfdde0fbf200c9284381cc7874b4_l.jpg?source=172ae18b"><meta itemprop="url" content="https://www.zhihu.com/people/howel52"><meta itemprop="zhihu:followerCount"></div>
</div>
</header>
<div class="Post-RichTextContainer">
<div class="RichText ztext Post-RichText">
<blockquote><p>先介紹下大背景，我們是Ant Codespaces 團隊，主要為螞蟻的工程師們提供雲上研發能力。我們對外透出的是一個典型的B/S應用，用戶無需在本地下載任何軟件，在瀏覽器中就能完成標準技術棧場景下的研發工作，<b>於此同時問題隨之而來。</b></p></blockquote>
<p><b>Web應用</b>相比於<b>Native應用</b>存在一個顯著的缺點：鍵盤事件會衝突，現像是部分快捷鍵組合在Web應用中會“失效”，舉幾個例子：如<code>Cmd + W</code> 、 <code>Cmd + N</code> 、 <code>Cmd + T</code>等等組合，這些事件不做額外處理是無法被Web 應用所正常響應的。</p>
<p>究其原因，可以歸於“ <b>Web App還未來得及處理Keyborads Event，Browser已經做出了對應的響應並產生了副作用</b>”，比如當<code>Cmd + W</code>被瀏覽器響應後，其副作用為關閉當前tab頁，頁面都被關閉了，Web App 自然也就被關了。</p>
<p> Ant Codespaces 作為研發上雲的重要組成部分，而快捷鍵又是工具產品提效的重要方式之一，因此在快捷鍵這件事上我們需要給用戶提供不落後於本地IDE 的按鍵組合功能與體驗。</p>
<h2>既然我們遇到了這個問題，那麼友商是怎麼做的？</h2>
<h3><a href="https://link.zhihu.com/?target=https%3A//github.com/codespaces" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Github Codespaces</a></h3>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-f3607199a99c9d00cb0106621f692bb8_r.jpg" data-caption="" data-size="normal" data-rawwidth="1500" data-rawheight="1062" class="origin_image zh-lightbox-thumb" width="1500" data-original="https://pic1.zhimg.com/v2-f3607199a99c9d00cb0106621f692bb8_b.jpg" title="v2-f3607199a99c9d00cb0106621f692bb8_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-f3607199a99c9d00cb0106621f692bb8_r.jpg" data-caption="" data-size="normal" data-rawwidth="1500" data-rawheight="1062" class="origin_image zh-lightbox-thumb lazy" width="1500" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1500'%20height='1062'&gt;&lt;/svg&gt;" data-actualsrc="https://pic1.zhimg.com/v2-f3607199a99c9d00cb0106621f692bb8_b.jpg" title="v2-f3607199a99c9d00cb0106621f692bb8_r"></figure>
<p>Github Codespaces 巧妙避開了這部分衝突的快捷鍵，用戶可以自己到相關配置界面中去設置自己想要的快捷鍵（但此時用戶無法設置成與瀏覽器衝突的組合）</p>
<p class="ztext-empty-paragraph"></p>
<h3><a href="https://link.zhihu.com/?target=https%3A//theia-ide.org/" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Theia</a></h3>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-974da4730a225baa8281ed5a72ab3923_r.jpg" data-caption="" data-size="normal" data-rawwidth="1500" data-rawheight="1146" class="origin_image zh-lightbox-thumb" width="1500" data-original="https://pic4.zhimg.com/v2-974da4730a225baa8281ed5a72ab3923_b.jpg" title="v2-974da4730a225baa8281ed5a72ab3923_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-974da4730a225baa8281ed5a72ab3923_r.jpg" data-caption="" data-size="normal" data-rawwidth="1500" data-rawheight="1146" class="origin_image zh-lightbox-thumb lazy" width="1500" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1500'%20height='1146'&gt;&lt;/svg&gt;" data-actualsrc="https://pic4.zhimg.com/v2-974da4730a225baa8281ed5a72ab3923_b.jpg" title="v2-974da4730a225baa8281ed5a72ab3923_r"></figure>
<p>Theia提供了<code>Alt + W</code>作為代替<code>Cmd + W</code>的默認組合</p>
<h3>
<p><a href="https://link.zhihu.com/?target=https%3A//coding.net/" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Coding</a></h3>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-bcd3afe80005d2403fe6e9d7a39f4452_r.jpg" data-caption="" data-size="normal" data-rawwidth="1500" data-rawheight="994" class="origin_image zh-lightbox-thumb" width="1500" data-original="https://pic3.zhimg.com/v2-bcd3afe80005d2403fe6e9d7a39f4452_b.jpg" title="v2-bcd3afe80005d2403fe6e9d7a39f4452_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-bcd3afe80005d2403fe6e9d7a39f4452_r.jpg" data-caption="" data-size="normal" data-rawwidth="1500" data-rawheight="994" class="origin_image zh-lightbox-thumb lazy" width="1500" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1500'%20height='994'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-bcd3afe80005d2403fe6e9d7a39f4452_b.jpg" title="v2-bcd3afe80005d2403fe6e9d7a39f4452_r"></figure>
<p>Coding 同上，也是避開衝突+ 自主設置</p>
<p>試用了一圈外部產品後，我枯了。不出意外的發現友商們在解決此類問題時採用了一種通用的解決方案：<b>避開與瀏覽器衝突的快捷鍵組合</b>。</p>
<p>這種方式雖然能解決問題，但代價是<b>需要改變本地用戶的使用習慣，</b>功能上是有了，但體驗上產生了差異。</p>
<p>隨後到Theia 老師處取了取經，也沒有發現太好的辦法</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-e036a74383e26d1f8f48c9c0cde0323a_r.jpg" data-caption="" data-size="normal" data-rawwidth="1020" data-rawheight="529" class="origin_image zh-lightbox-thumb" width="1020" data-original="https://pic3.zhimg.com/v2-e036a74383e26d1f8f48c9c0cde0323a_b.jpg" title="v2-e036a74383e26d1f8f48c9c0cde0323a_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-e036a74383e26d1f8f48c9c0cde0323a_r.jpg" data-caption="" data-size="normal" data-rawwidth="1020" data-rawheight="529" class="origin_image zh-lightbox-thumb lazy" width="1020" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1020'%20height='529'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-e036a74383e26d1f8f48c9c0cde0323a_b.jpg" title="v2-e036a74383e26d1f8f48c9c0cde0323a_r"></figure>
<p class="ztext-empty-paragraph"></p>
<hr>
<h2>快捷鍵背後的心智模型</h2>
<blockquote><p>既然業界已經有了通用的“方案”，為何還要繼續糾結這個問題？</p></blockquote>
<p>從我切身體驗來看，我在今年6 月份來到黃龍，當我將研發活動從本地VSCode 遷移到Cloud IDE 時，最難受的是：部分高頻快捷鍵操作與我腦海裡的第一反應不一致，典型的如關閉文件<code>Cmd + W</code> ，即便已經過了100多天，我依舊不想用非<code>Cmd + W</code>組合來關閉文件。</p>
<p>同時，糾結這一點的用戶不止我一個，我們也收到了一些來自用戶的反饋，期望能解決高頻快捷鍵的一致性問題。</p>
<p><b>這是一個很合理的訴求</b>，當我們在macOS上下載一個Native App時，自然而然的會認為<code>Cmd + W</code>是「關閉XX」， <code>Cmd + N</code>是「新建XX」，這是一種約定俗成的guide line。當然軟件開發者們也可以不遵守這個，甚至是反其道行之，那相對應的就會增加用戶使用成本，從而戴上真難用的帽子。</p>
<h2>快捷鍵對工具類軟件的重要性</h2>
<p>在特定的場景下，如果操作鼠標或是操作鍵盤都能達成某一目的，那麼大概率操作鍵盤會比純操作鼠標速度來的更快。 （當然還有一些場景可能是純鼠標更快，比如macOS 用觸發角鎖屏）</p>
<p>作為工程師，我們可以聯想一些日常研發活動時的一些場景，比如跳行、搜索、關閉/新開文件.... 無論是用Sublime 還是VSCode ，這些功能都可以用鍵盤或是鼠標做到，但很多同學會選擇使用鍵盤來喚醒對應的功能。</p>
<p>甚至很多效率工具的核心能力之一是將鼠標操作轉用鍵盤操作來代替，比如Alfred、Spectacle 等等等等。</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-a634377cfab9a0268ac0476707e153ad_r.jpg" data-caption="" data-size="normal" data-rawwidth="1500" data-rawheight="577" class="origin_image zh-lightbox-thumb" width="1500" data-original="https://pic2.zhimg.com/v2-a634377cfab9a0268ac0476707e153ad_b.jpg" title="v2-a634377cfab9a0268ac0476707e153ad_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-a634377cfab9a0268ac0476707e153ad_r.jpg" data-caption="" data-size="normal" data-rawwidth="1500" data-rawheight="577" class="origin_image zh-lightbox-thumb lazy" width="1500" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1500'%20height='577'&gt;&lt;/svg&gt;" data-actualsrc="https://pic2.zhimg.com/v2-a634377cfab9a0268ac0476707e153ad_b.jpg" title="v2-a634377cfab9a0268ac0476707e153ad_r"></figure>
<p>同樣我們也可以聯想一下行業外的其他角色是如何工作的。比如照相館老闆使用Photoshop，他們鍵盤上部分按鍵都快包漿了，可想而知他們有多依賴快捷鍵的能力，沒有快捷鍵固然還能繼續工作，但是效率上會降低很多，<b>而時間就是金錢</b>。</p>
<p>因此可以草率的得出一條結論：<b>遵循心智模型的快捷鍵組合能提升軟件使用的效率。</b></p>
<p>為什麼要用草率這個詞，因為這句話是我自己編的: )</p>
<p><b>所以我們不但要解決快捷鍵的“功能有無”問題，也要盡可能的解決快捷鍵“體驗一致”問題。</b></p>
<hr>
<h2>
<p>所有衝突的快捷鍵都會失靈嗎？</h2>
<blockquote><p>並不是</p></blockquote>
<p>從現像上看，大體上可以分為兩類快捷鍵：</p>
<ol>
<li>能被preventDefault</li>
<li>不能被preventDefault</li>
</ol>
<p>可以被preventDefault的這部分組合很好解，以<code>Cmd + S</code>舉例，</p>
<p>在一個沒有額外處理鍵盤事件的Web應用裡， <code>Cmd + S</code>會觸發瀏覽器的<code>网页保存</code>，見：</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-1150dbb5a3f071348c8392658e0ce1e7_r.jpg" data-caption="" data-size="normal" data-rawwidth="1423" data-rawheight="802" data-thumbnail="https://pic4.zhimg.com/v2-1150dbb5a3f071348c8392658e0ce1e7_b.jpg" class="origin_image zh-lightbox-thumb" width="1423" data-original="https://pic4.zhimg.com/v2-1150dbb5a3f071348c8392658e0ce1e7_b.gif" title="v2-1150dbb5a3f071348c8392658e0ce1e7_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-1150dbb5a3f071348c8392658e0ce1e7_r.jpg" data-caption="" data-size="normal" data-rawwidth="1423" data-rawheight="802" data-thumbnail="https://pic4.zhimg.com/v2-1150dbb5a3f071348c8392658e0ce1e7_b.jpg" class="origin_image zh-lightbox-thumb lazy" width="1423" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1423'%20height='802'&gt;&lt;/svg&gt;" data-actualsrc="https://pic4.zhimg.com/v2-1150dbb5a3f071348c8392658e0ce1e7_b.gif" title="v2-1150dbb5a3f071348c8392658e0ce1e7_r"></figure>
<p>當通過以下代碼取消默認事件後，我們可以愉快的在listener 中加入callback</p>
<div class="highlight">
<pre><code class="language-js"><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s2">"keydown"</span><span class="p">,</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span><span class="p">=&gt;</span><span class="p">{</span><span class="k">if</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">keyCode</span><span class="o">===</span><span class="mi">83</span><span class="o">&amp;&amp;</span><span class="nx">e</span><span class="p">.</span><span class="nx">metaKey</span><span class="p">)</span><span class="p">{</span><span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span><span class="nx">alert</span><span class="p">(</span><span class="s2">"I AM CMD_S"</span><span class="p">);</span><span class="p">}</span><span class="p">});</span></code></pre>
</div>
<p class="ztext-empty-paragraph"></p>
<p>看看效果：</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-90551ffe6b1eda182dd1ce4b6ecb855a_r.jpg" data-caption="" data-size="normal" data-rawwidth="1423" data-rawheight="802" data-thumbnail="https://pic3.zhimg.com/v2-90551ffe6b1eda182dd1ce4b6ecb855a_b.jpg" class="origin_image zh-lightbox-thumb" width="1423" data-original="https://pic3.zhimg.com/v2-90551ffe6b1eda182dd1ce4b6ecb855a_b.gif" title="v2-90551ffe6b1eda182dd1ce4b6ecb855a_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-90551ffe6b1eda182dd1ce4b6ecb855a_r.jpg" data-caption="" data-size="normal" data-rawwidth="1423" data-rawheight="802" data-thumbnail="https://pic3.zhimg.com/v2-90551ffe6b1eda182dd1ce4b6ecb855a_b.jpg" class="origin_image zh-lightbox-thumb lazy" width="1423" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1423'%20height='802'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-90551ffe6b1eda182dd1ce4b6ecb855a_b.gif" title="v2-90551ffe6b1eda182dd1ce4b6ecb855a_r"></figure>
<p>這背後發生的故事就不在本文展開了，相信各位前端老法師們比我更懂Events</p>
<h3>
<p>不能被preventDefault 的快捷鍵該如何是好</h3>
<p>觀察上述行為後，可以發現當<code>CMD + S</code>未被取消默認事件時，會先執行cb，隨後再是執行默認事件（可以在此同款<a href="https://link.zhihu.com/?target=https%3A//codesandbox.io/s/confident-nobel-ukidl%3Ffile%3D/index.html" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">demo</a>中感受一下）：</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-1bf71f7eb0dcdf3248af5dbef1ad7cb2_r.jpg" data-caption="" data-size="normal" data-rawwidth="1042" data-rawheight="130" class="origin_image zh-lightbox-thumb" width="1042" data-original="https://pic3.zhimg.com/v2-1bf71f7eb0dcdf3248af5dbef1ad7cb2_b.jpg" title="v2-1bf71f7eb0dcdf3248af5dbef1ad7cb2_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-1bf71f7eb0dcdf3248af5dbef1ad7cb2_r.jpg" data-caption="" data-size="normal" data-rawwidth="1042" data-rawheight="130" class="origin_image zh-lightbox-thumb lazy" width="1042" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1042'%20height='130'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-1bf71f7eb0dcdf3248af5dbef1ad7cb2_b.jpg" title="v2-1bf71f7eb0dcdf3248af5dbef1ad7cb2_r"></figure>
<p>當preventDefault 後，自然就變成了：</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-707327272de8fea7dd45b7950f54d26c_r.jpg" data-caption="" data-size="normal" data-rawwidth="1042" data-rawheight="130" class="origin_image zh-lightbox-thumb" width="1042" data-original="https://pic1.zhimg.com/v2-707327272de8fea7dd45b7950f54d26c_b.jpg" title="v2-707327272de8fea7dd45b7950f54d26c_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-707327272de8fea7dd45b7950f54d26c_r.jpg" data-caption="" data-size="normal" data-rawwidth="1042" data-rawheight="130" class="origin_image zh-lightbox-thumb lazy" width="1042" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1042'%20height='130'&gt;&lt;/svg&gt;" data-actualsrc="https://pic1.zhimg.com/v2-707327272de8fea7dd45b7950f54d26c_b.jpg" title="v2-707327272de8fea7dd45b7950f54d26c_r"></figure>
<p>那麼，把事件<code>CMD + S</code>換為<code>CMD + W</code>會發生什麼？</p>
<p>將上述demo 改寫後試下：</p>
<div class="highlight">
<pre><code class="language-text"> document.addEventListener("keydown", (e) =&gt; { if (e.keyCode === 69 &amp;&amp; e.metaKey) { e.preventDefault(); alert("I AM CMD_W"); } });</code></pre>
</div>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-f027888630217d0014a959b333a2d840_r.jpg" data-caption="" data-size="normal" data-rawwidth="1425" data-rawheight="806" data-thumbnail="https://pic1.zhimg.com/v2-f027888630217d0014a959b333a2d840_b.jpg" class="origin_image zh-lightbox-thumb" width="1425" data-original="https://pic1.zhimg.com/v2-f027888630217d0014a959b333a2d840_b.gif" title="v2-f027888630217d0014a959b333a2d840_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-f027888630217d0014a959b333a2d840_r.jpg" data-caption="" data-size="normal" data-rawwidth="1425" data-rawheight="806" data-thumbnail="https://pic1.zhimg.com/v2-f027888630217d0014a959b333a2d840_b.jpg" class="origin_image zh-lightbox-thumb lazy" width="1425" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1425'%20height='806'&gt;&lt;/svg&gt;" data-actualsrc="https://pic1.zhimg.com/v2-f027888630217d0014a959b333a2d840_b.gif" title="v2-f027888630217d0014a959b333a2d840_r"></figure>
<p>直接翻車，說明<code>CMD + W</code>類的事件和<code>CMD + S</code>類的事件有著本質差別，我們可以在原有的流程圖上繼續做一個推測，當瀏覽器對這部分優先級更高的快捷鍵做出不可逆的副作用響應時，listener 的cb 即便preventDefault 也將變得無能為力，因為更高優先級的副作用已經產生了：</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-9d86caa2c5d847f51d15b6a7eeef5855_r.jpg" data-caption="" data-size="normal" data-rawwidth="1488" data-rawheight="130" class="origin_image zh-lightbox-thumb" width="1488" data-original="https://pic2.zhimg.com/v2-9d86caa2c5d847f51d15b6a7eeef5855_b.jpg" title="v2-9d86caa2c5d847f51d15b6a7eeef5855_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-9d86caa2c5d847f51d15b6a7eeef5855_r.jpg" data-caption="" data-size="normal" data-rawwidth="1488" data-rawheight="130" class="origin_image zh-lightbox-thumb lazy" width="1488" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1488'%20height='130'&gt;&lt;/svg&gt;" data-actualsrc="https://pic2.zhimg.com/v2-9d86caa2c5d847f51d15b6a7eeef5855_b.jpg" title="v2-9d86caa2c5d847f51d15b6a7eeef5855_r"></figure>
<p class="ztext-empty-paragraph"></p>
<h3>那！怎！麼！辦！啊！</h3>
<p>問題不大，辦法總歸是有的，無非是權衡一下投入和產出。</p>
<p>從流程圖上看這個問題的話，思路上大概是在<code>emit keybords events</code>與<code>browser do sth.</code>之間加點騷東西，一是讓不可逆的副作用不再產生，二是讓listener的cb正常執行。只要能滿足這兩點，這事情基本上就成了，剩下的就看研發成本與實現方式。</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-b3f79c8fe2c6262209b794368f9bbd91_r.jpg" data-caption="" data-size="normal" data-rawwidth="1488" data-rawheight="130" class="origin_image zh-lightbox-thumb" width="1488" data-original="https://pic2.zhimg.com/v2-b3f79c8fe2c6262209b794368f9bbd91_b.jpg" title="v2-b3f79c8fe2c6262209b794368f9bbd91_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-b3f79c8fe2c6262209b794368f9bbd91_r.jpg" data-caption="" data-size="normal" data-rawwidth="1488" data-rawheight="130" class="origin_image zh-lightbox-thumb lazy" width="1488" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1488'%20height='130'&gt;&lt;/svg&gt;" data-actualsrc="https://pic2.zhimg.com/v2-b3f79c8fe2c6262209b794368f9bbd91_b.jpg" title="v2-b3f79c8fe2c6262209b794368f9bbd91_r"></figure>
<p>有了大致的思路後，繼續尋找實現思路的方式，大概分為三個方向：</p>
<ol>
<li>寫一個native bridge，用來代理系統級的事件，再通過某種方式與瀏覽器通信（類似Postman <a href="https://link.zhihu.com/?target=https%3A//learning.postman.com/docs/sending-requests/capturing-request-data/interceptor/" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Capture requests and cookies</a>的實現方式）</li>
<li>增加Electron 版本的IDE，理論上VSC 的快捷鍵能力都能實現了</li>
<li>通過Chrome Extension 來代理快捷鍵</li>
</ol>
<table data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">
<tbody>
<tr>
<td>方式</td>
<td>複雜度</td>
<td>優點</td>
<td>缺點</td>
</tr>
<tr>
<td>Native Bridge</td>
<td> <img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
<td> 1. 系統級能力，想做的能力基本都能做，沒有做不到只有想不到</td>
<td>1. 需要用戶額外安裝native bridge 與chrome extension<br /> 2. 需要考慮跨平台2. 複雜度較高</td>
</tr>
<tr>
<td>Electron App</td>
<td> <img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
<td> 1. 跨平台<br />2. 在現有Web 應用的基礎上追加Electron ，成本比樓上更小</td>
<td>1. 需要用戶額外在本地安裝使用Electron App，與現階段Cloud IDE 的定位與場景不符<br />2. 開了electron 的口子，後續的系統級API 如果runtime 未提供，則依舊無法避免跨平台研發成本</td>
</tr>
<tr>
<td>Chrome Extension</td>
<td> <img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
<td> 1. 跨平台<br />2. 開發量小</td>
<td>1. 用戶需要安裝對應的Chrome Extension<br /> 2. 能捕獲大部分事件，但仍有一部分事件會逃逸3. 對瀏覽器有約束</td>
</tr>
</tbody>
</table>
<p>我認為工程師的使命是<b>在有限的複雜度中尋找最優解</b>，當梳理完三種思路後，基本就將Chrome Extension作為當下的首選實現方案。</p>
<p>一方面Chrome Extension能解決用戶的幾個強訴求組合（如關閉文件<code>CMD + W</code> ，新建文件<code>CMD + N</code> ，新開Tab <code>CMD + T</code> ），解決這幾個按鍵就基本搞定了絕大部分用戶。那麼還剩下一些Chrome Extension也無法攔到的組合，比如<code>CMD + Q</code> （請不要在此時按這個組合，會錯過文末的彩蛋），熟悉macOS快捷鍵的同學應該都知道，這是強退應用的快捷鍵，理論上它的心智就是強退應用，攔不攔都無所謂了，逃逸就逃逸吧。</p>
<p>另一方面我們的Cloud IDE 不同於其他中後台產品，我們只需要兼容到Chrome@latest，為此方案提供了最佳的宿主環境。</p>
<h2>
<p>基於Chrome Extension 的快捷鍵衝突解決方案</h2>
<p>綜上所述，基本上這個方案的主旋律已經定調了：</p>
<ol>
<li>在方案足夠輕，複雜度足夠低的前提下，ROI 才足夠高</li>
<li>無需考慮跨平台</li>
<li>允許逃逸一部分不那麼常用的組合</li>
</ol>
<h3>
<p>具體思路</h3>
<p>用一句話概括這個方案的實現思路：在目標應用的tab 裡觸發鍵盤事件時，屏蔽瀏覽器的原生行為，走web 應用預期的行為。</p>
<p><b>如何屏蔽？</b></p>
<p>從這句話中，我們可以看到，“屏蔽瀏覽器的原生行為” 是此方案的大前提，那麼問題來了，Chrome Extension 可以做到麼？<br />可以</p>
<p>擴展用的很溜的同學應該接觸過這個<a href="https://zhuanlan.zhihu.com/write" class="internal" data-wpel-link="external" rel="nofollow external noopener noreferrer">入口</a>，很多擴展是提供快捷鍵自定義能力的，用於通過某個組合來執行對應的command，經過實測發現，像<code>CMD + N</code> <code>CMD + W</code>等組合可以被擴展快捷鍵override，雖然Chrome 的官方文檔並沒有提到通過擴展快捷鍵override browser 的快捷鍵是一個feature，但至少從眼前的行為來看，這條路子是可行的。</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-f8a70667496b205874a5df69749ba6da_r.jpg" data-caption="" data-size="normal" data-rawwidth="1500" data-rawheight="960" class="origin_image zh-lightbox-thumb" width="1500" data-original="https://pic3.zhimg.com/v2-f8a70667496b205874a5df69749ba6da_b.jpg" title="v2-f8a70667496b205874a5df69749ba6da_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-f8a70667496b205874a5df69749ba6da_r.jpg" data-caption="" data-size="normal" data-rawwidth="1500" data-rawheight="960" class="origin_image zh-lightbox-thumb lazy" width="1500" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1500'%20height='960'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-f8a70667496b205874a5df69749ba6da_b.jpg" title="v2-f8a70667496b205874a5df69749ba6da_r"></figure>
<p>因此最開始的流程圖就變成了：</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-a2556a865349ad66a8f8bb0f2849c26d_r.jpg" data-caption="" data-size="normal" data-rawwidth="1488" data-rawheight="130" class="origin_image zh-lightbox-thumb" width="1488" data-original="https://pic2.zhimg.com/v2-a2556a865349ad66a8f8bb0f2849c26d_b.jpg" title="v2-a2556a865349ad66a8f8bb0f2849c26d_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-a2556a865349ad66a8f8bb0f2849c26d_r.jpg" data-caption="" data-size="normal" data-rawwidth="1488" data-rawheight="130" class="origin_image zh-lightbox-thumb lazy" width="1488" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1488'%20height='130'&gt;&lt;/svg&gt;" data-actualsrc="https://pic2.zhimg.com/v2-a2556a865349ad66a8f8bb0f2849c26d_b.jpg" title="v2-a2556a865349ad66a8f8bb0f2849c26d_r"></figure>
<p><b>那麼這麼做會有什麼新的問題麼？</b></p>
<blockquote><p><b>有的</b></p></blockquote>
<p>可以看到這個流程圖中缺少了一個環節， <code>browser do sth.</code>沒有了。</p>
<p>有人可能會有疑問：我們確實是想把browser 響應的事件去掉呀，為什麼會有問題呢？</p>
<p>將上圖再擴充一下就清晰了：</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-ae9e2a7301e8559d56c701e59281b232_r.jpg" data-caption="" data-size="normal" data-rawwidth="1135" data-rawheight="913" class="origin_image zh-lightbox-thumb" width="1135" data-original="https://pic3.zhimg.com/v2-ae9e2a7301e8559d56c701e59281b232_b.jpg" title="v2-ae9e2a7301e8559d56c701e59281b232_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-ae9e2a7301e8559d56c701e59281b232_r.jpg" data-caption="" data-size="normal" data-rawwidth="1135" data-rawheight="913" class="origin_image zh-lightbox-thumb lazy" width="1135" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1135'%20height='913'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-ae9e2a7301e8559d56c701e59281b232_b.jpg" title="v2-ae9e2a7301e8559d56c701e59281b232_r"></figure>
<p>從擴充後的流程圖中不難發現，實際上我們仍然需要那部分被override 的browser 行為，因為用戶的瀏覽器除了跑我們的Web 應用之外，還需要滿足其他的日常瀏覽需求，如果只是因為想在Cloud IDE里通過<code>CMD + T</code>打開文件tab，從而失去了Browser原生的快速新建頁籤的能力，那就又繞回了這個問題的起點。</p>
<p><b>預期是什麼？</b></p>
<p>預期就像圖中所描述的，當前tab 為我們想要override 的tab 時，broswer 行為不生效，當前tab 為其他頁面時，繼續保留browser 的行為。</p>
<p><b>可以完美做到麼？</b></p>
<blockquote><p>做不到，但是可以假裝做得到</p></blockquote>
<p>先祭出一張Chrome Extension 的架構圖：</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-03cb9a8fe2de2b5948d926eb5989f20d_r.jpg" data-size="normal" data-rawwidth="641" data-rawheight="421" class="origin_image zh-lightbox-thumb" width="641" data-original="https://pic2.zhimg.com/v2-03cb9a8fe2de2b5948d926eb5989f20d_b.jpg" title="v2-03cb9a8fe2de2b5948d926eb5989f20d_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-03cb9a8fe2de2b5948d926eb5989f20d_r.jpg" data-size="normal" data-rawwidth="641" data-rawheight="421" class="origin_image zh-lightbox-thumb lazy" width="641" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='641'%20height='421'&gt;&lt;/svg&gt;" data-actualsrc="https://pic2.zhimg.com/v2-03cb9a8fe2de2b5948d926eb5989f20d_b.jpg" title="v2-03cb9a8fe2de2b5948d926eb5989f20d_r"><figcaption> via kmsfan</figcaption></figure>
<p>由圖可見，Chrome Extension 有幾個核心的概念：</p>
<ol>
<li>Background</li>
<li> Popup</li>
<li> Content Scripts</li>
<li> Injected Scripts</li>
</ol>
<p>概念不一一展開介紹了，撿兩個我們用到的概念說一說。</p>
<p> background js為開發者提供了豐富的<a href="https://link.zhihu.com/?target=https%3A//developer.chrome.com/extensions/api_index" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Chrome API</a>調用能力，常見的如<code>开关页面</code>，<code>跳转tab</code>等基本能力都能通過Chrome API模擬出來，因此可以patch瀏覽器的原生事件。</p>
<p>流程圖再次變化一下：</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-98fc269d44eabd3bb2affa9edf7c7e48_r.jpg" data-caption="" data-size="normal" data-rawwidth="1135" data-rawheight="913" class="origin_image zh-lightbox-thumb" width="1135" data-original="https://pic1.zhimg.com/v2-98fc269d44eabd3bb2affa9edf7c7e48_b.jpg" title="v2-98fc269d44eabd3bb2affa9edf7c7e48_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-98fc269d44eabd3bb2affa9edf7c7e48_r.jpg" data-caption="" data-size="normal" data-rawwidth="1135" data-rawheight="913" class="origin_image zh-lightbox-thumb lazy" width="1135" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1135'%20height='913'&gt;&lt;/svg&gt;" data-actualsrc="https://pic1.zhimg.com/v2-98fc269d44eabd3bb2affa9edf7c7e48_b.jpg" title="v2-98fc269d44eabd3bb2affa9edf7c7e48_r"></figure>
<p><b>那還有什麼問題麼？</b></p>
<blockquote><p><b>還有問題</b></p></blockquote>
<p>以Cloud IDE關閉文件的組合鍵舉例<code>CMD + OPT + W</code> ，我們雖然通過Chrome Extension攔截了<code>CMD + W</code> ，但是web應用的listener是註冊在<code>CMD + OPT +W</code>上的，也就是說我們光攔事件沒有用，還需要將對應在web 應用中的事件補上，而content js 是tab 級別並且能獲取到對應頁面的上下文的，因此可以通過content js 來patch 應用註冊的事件。</p>
<p>流程圖繼續演進：</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-06001f9019ebad04ddc0214e688f9313_r.jpg" data-caption="" data-size="normal" data-rawwidth="1330" data-rawheight="913" class="origin_image zh-lightbox-thumb" width="1330" data-original="https://pic4.zhimg.com/v2-06001f9019ebad04ddc0214e688f9313_b.jpg" title="v2-06001f9019ebad04ddc0214e688f9313_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-06001f9019ebad04ddc0214e688f9313_r.jpg" data-caption="" data-size="normal" data-rawwidth="1330" data-rawheight="913" class="origin_image zh-lightbox-thumb lazy" width="1330" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1330'%20height='913'&gt;&lt;/svg&gt;" data-actualsrc="https://pic4.zhimg.com/v2-06001f9019ebad04ddc0214e688f9313_b.jpg" title="v2-06001f9019ebad04ddc0214e688f9313_r"></figure>
<p><b>為什麼採用這種方式？</b></p>
<p>一方面我們的Web 應用是一個架構很複雜的應用，不希望因這個方案帶來侵入性的改造，避免衍生出更多的邏輯分支與環境概念。另一方面，它是一種通用的解決思路，不單單是用來解決Cloud IDE 遇到的問題，其他的Web 應用都可以無痕接入類似的方案。</p>
<p>最後看看代碼實現，以<code>CMD + N</code>新建文件舉例：</p>
<p>對於backgound js而言，我們需要模擬<code>新开window</code>行為，這個能力Chrome API已經提供了，即：</p>
<div class="highlight">
<pre><code class="language-text">chrome.windows.create()</code></pre>
</div>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-18ad818eaed928ede6ffac73b173676b_r.jpg" data-caption="" data-size="normal" data-rawwidth="1428" data-rawheight="200" class="origin_image zh-lightbox-thumb" width="1428" data-original="https://pic4.zhimg.com/v2-18ad818eaed928ede6ffac73b173676b_b.png" title="v2-18ad818eaed928ede6ffac73b173676b_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-18ad818eaed928ede6ffac73b173676b_r.jpg" data-caption="" data-size="normal" data-rawwidth="1428" data-rawheight="200" class="origin_image zh-lightbox-thumb lazy" width="1428" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1428'%20height='200'&gt;&lt;/svg&gt;" data-actualsrc="https://pic4.zhimg.com/v2-18ad818eaed928ede6ffac73b173676b_b.png" title="v2-18ad818eaed928ede6ffac73b173676b_r"></figure>
<p>對應的content js，我們需要將web 應用註冊的事件補發，即：</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-b680188a72ad4dd7db9ba2a1fcdbb3cd_r.jpg" data-caption="" data-size="normal" data-rawwidth="1070" data-rawheight="428" class="origin_image zh-lightbox-thumb" width="1070" data-original="https://pic2.zhimg.com/v2-b680188a72ad4dd7db9ba2a1fcdbb3cd_b.jpg" title="v2-b680188a72ad4dd7db9ba2a1fcdbb3cd_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-b680188a72ad4dd7db9ba2a1fcdbb3cd_r.jpg" data-caption="" data-size="normal" data-rawwidth="1070" data-rawheight="428" class="origin_image zh-lightbox-thumb lazy" width="1070" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1070'%20height='428'&gt;&lt;/svg&gt;" data-actualsrc="https://pic2.zhimg.com/v2-b680188a72ad4dd7db9ba2a1fcdbb3cd_b.jpg" title="v2-b680188a72ad4dd7db9ba2a1fcdbb3cd_r"></figure>
<p>將backgroud 與content 中的patch 組合起來，註冊到擴展對應的快捷鍵中：</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-5132289eb164b385f6552edaf5f90622_r.jpg" data-caption="" data-size="normal" data-rawwidth="1500" data-rawheight="485" class="origin_image zh-lightbox-thumb" width="1500" data-original="https://pic3.zhimg.com/v2-5132289eb164b385f6552edaf5f90622_b.jpg" title="v2-5132289eb164b385f6552edaf5f90622_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-5132289eb164b385f6552edaf5f90622_r.jpg" data-caption="" data-size="normal" data-rawwidth="1500" data-rawheight="485" class="origin_image zh-lightbox-thumb lazy" width="1500" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1500'%20height='485'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-5132289eb164b385f6552edaf5f90622_b.jpg" title="v2-5132289eb164b385f6552edaf5f90622_r"></figure>
<h3>
<p>看看最終效果<br /></h3>
<p><b>使用方案前</b></p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-2b67a3e5edb302c2946da59e8d41d8ae_r.jpg" data-size="normal" data-rawwidth="1425" data-rawheight="765" data-thumbnail="https://pic3.zhimg.com/v2-2b67a3e5edb302c2946da59e8d41d8ae_b.jpg" class="origin_image zh-lightbox-thumb" width="1425" data-original="https://pic3.zhimg.com/v2-2b67a3e5edb302c2946da59e8d41d8ae_b.gif" title="v2-2b67a3e5edb302c2946da59e8d41d8ae_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-2b67a3e5edb302c2946da59e8d41d8ae_r.jpg" data-size="normal" data-rawwidth="1425" data-rawheight="765" data-thumbnail="https://pic3.zhimg.com/v2-2b67a3e5edb302c2946da59e8d41d8ae_b.jpg" class="origin_image zh-lightbox-thumb lazy" width="1425" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1425'%20height='765'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-2b67a3e5edb302c2946da59e8d41d8ae_b.gif" title="v2-2b67a3e5edb302c2946da59e8d41d8ae_r"><figcaption>在Web 應用中使用CMD + N 打開了新的瀏覽器窗口，不符合用戶的預期</figcaption></figure>
<p><b>使用方案後</b></p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-727aca51af42c1c8ba96a5c20f3f3547_r.jpg" data-size="normal" data-rawwidth="1425" data-rawheight="765" data-thumbnail="https://pic4.zhimg.com/v2-727aca51af42c1c8ba96a5c20f3f3547_b.jpg" class="origin_image zh-lightbox-thumb" width="1425" data-original="https://pic4.zhimg.com/v2-727aca51af42c1c8ba96a5c20f3f3547_b.gif" title="v2-727aca51af42c1c8ba96a5c20f3f3547_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-727aca51af42c1c8ba96a5c20f3f3547_r.jpg" data-size="normal" data-rawwidth="1425" data-rawheight="765" data-thumbnail="https://pic4.zhimg.com/v2-727aca51af42c1c8ba96a5c20f3f3547_b.jpg" class="origin_image zh-lightbox-thumb lazy" width="1425" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1425'%20height='765'&gt;&lt;/svg&gt;" data-actualsrc="https://pic4.zhimg.com/v2-727aca51af42c1c8ba96a5c20f3f3547_b.gif" title="v2-727aca51af42c1c8ba96a5c20f3f3547_r"><figcaption>在Web 應用中使用CMD + N 打開了新的file tab，符合預期</figcaption></figure>
<p class="ztext-empty-paragraph"></p>
<p><b>啊！終於可以“關閉”文件了～</b></p>
<hr>
<h2>
<p>最後！</h2>
<p>我們是螞蟻研發效能部，致力於為螞蟻和多家金融企業提供核電級的全生命週期研發產品，研發效能部的產品涵蓋了螞蟻的整個研發活動，包含代碼服務（託管、審核、掃描、搜索、構建、內容挖掘）、代碼編輯（Cloud IDE）、CI/CD、測試集成、環境搭建、全鏈路鏈條、配置管理、資源調度，以及基於研發活動完整生命週期的數據產品。加入我們，一起打造螞蟻集團下一代基於雲原生的研發效能平台。</p>
<p> （PS 我們正在嘗試小流量WFH，每周有一天的時間可以不限地點辦公，或許是國內為數不多的在後疫情時期依舊對遠程工作保持高效探索的神秘部門，相信很多工程師們會喜歡這種體驗）</p>
<hr>
<h2>
<p>最後的最後！</h2>
<p>我是<a href="https://link.zhihu.com/?target=https%3A//me.howel52.com/" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">小旋風</a>，一個從未接過單的順風車司機，如果您遠道而來，我可以到機場接您到黃龍，這一切都是免費的，甚至還能倒擼一發冰美。</p>
<p>蕭山-&gt; 黃龍接機熱線熱郵：chaxuan.wh@antgroup.com</p>
<p>接機暗號：你們那兒還招人不。</p>
<p>暗號是什麼不重要，交通方式也不重要，title 也不重要，base 在哪也不重要，重要的是在探索的過程中發現有趣的事遇到志同道合的人。</p>
<hr>
<h2>最後的最後的最後！</h2>
<p>附上JD：</p>
<ol>
<li><a href="https://link.zhihu.com/?target=https%3A//gw.alipayobjects.com/mdn/rms_38ea80/afts/img/A%2A3IWGQLAmPZwAAAAAAAAAAAAAARQnAQ" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">資深全棧工程師</a></li>
<li><a href="https://link.zhihu.com/?target=https%3A//gw.alipayobjects.com/mdn/rms_38ea80/afts/img/A%2AoO-FRLoMHhsAAAAAAAAAAAAAARQnAQ" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">代碼平台技術專家</a></li>
<li><a href="https://link.zhihu.com/?target=https%3A//gw.alipayobjects.com/mdn/rms_38ea80/afts/img/A%2AbB6OTITGjy4AAAAAAAAAAAAAARQnAQ" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">平台型產品專家</a></li>
<li><a href="https://link.zhihu.com/?target=https%3A//gw.alipayobjects.com/mdn/rms_38ea80/afts/img/A%2ADYDxTL9b1-sAAAAAAAAAAAAAARQnAQ" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">雲原生容器專家</a></li>
<li><a href="https://link.zhihu.com/?target=https%3A//gw.alipayobjects.com/mdn/rms_38ea80/afts/img/A%2A3m8iToPgiewAAAAAAAAAAAAAARQnAQ" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">高級開發工程師</a></li>
<li><a href="https://link.zhihu.com/?target=https%3A//gw.alipayobjects.com/mdn/rms_38ea80/afts/img/A%2A_DGaQYe3yZsAAAAAAAAAAAAAARQnAQ" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">分佈式計算架構專家</a></li>
<li><a href="https://link.zhihu.com/?target=https%3A//gw.alipayobjects.com/mdn/rms_38ea80/afts/img/A%2AxdTSRLtSDSEAAAAAAAAAAAAAARQnAQ" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">資深IDE研發工程師</a></li>
<li><a href="https://link.zhihu.com/?target=https%3A//gw.alipayobjects.com/mdn/rms_38ea80/afts/img/A%2AwLOkRKZUGLwAAAAAAAAAAAAAARQnAQ" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">代碼智能化工程師</a></li>
<li><a href="https://link.zhihu.com/?target=https%3A//gw.alipayobjects.com/mdn/rms_38ea80/afts/img/A%2AyzoXT6eZYrMAAAAAAAAAAAAAARQnAQ" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">編譯器研發工程師</a></li>
</ol>
</div>
</div>
</article>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15253/topic-300659062/" data-wpel-link="internal">解決Web 應用與瀏覽器快捷鍵衝突的一條野路子</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>⏰ Moment.js 宣布停止開發，現在該用什麼？</title>
		<link>https://hypergrowths.com/software-engineering/front-end-dev/15239/topic-250152267/</link>
		
		<dc:creator><![CDATA[marketer]]></dc:creator>
		<pubDate>Sat, 30 Jan 2021 18:35:33 +0000</pubDate>
				<category><![CDATA[前端開發]]></category>
		<category><![CDATA[前端外刊評論]]></category>
		<guid isPermaLink="false">https://hypergrowths.com/software-engineering/front-end-dev/15239/topic-250152267/</guid>

					<description><![CDATA[<p>本文整理自Moment.js 官方英文公告https://momentjs.com/docs/#/-project-status/Moment.js 宣布停止開發，進入維護狀態。這是一個大而全的時間日期庫，極大方便了我們在JavaScript 中計算時間和日期，每週下…</p>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15239/topic-250152267/" data-wpel-link="internal">⏰ Moment.js 宣布停止開發，現在該用什麼？</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></description>
										<content:encoded><![CDATA[<article class="Post-Main Post-NormalMain" tabindex="-1">
<header class="Post-Header">
<h1 class="Post-Title"><img src="https://s.w.org/images/core/emoji/14.0.0/72x72/23f0.png" alt="⏰" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Moment.js 宣布停止開發，現在該用什麼？ </h1>
<div class="Post-Author">
<div class="AuthorInfo" itemprop="author" itemscope="" itemtype="http://schema.org/Person"><meta itemprop="name" content="朱昆 iamkun"><meta itemprop="image" content="https://pic2.zhimg.com/v2-8b1158e276178e02420f01264ccfb792_l.jpg?source=172ae18b"><meta itemprop="url" content="https://www.zhihu.com/people/iamkun"><meta itemprop="zhihu:followerCount"></div>
</div>
</header>
<div class="Post-RichTextContainer">
<div class="RichText ztext Post-RichText">
<p>本文整理自Moment.js官方英文公告<a href="https://link.zhihu.com/?target=https%3A//momentjs.com/docs/%23/-project-status/" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">momentjs.com/docs/#</span> <span class="invisible">/-project-status/</span></a></p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-0a61e79af62f82af0277fb6c73643e4d_r.jpg" data-caption="" data-size="normal" data-rawwidth="942" data-rawheight="904" class="origin_image zh-lightbox-thumb" width="942" data-original="https://pic2.zhimg.com/v2-0a61e79af62f82af0277fb6c73643e4d_b.jpg" title="v2-0a61e79af62f82af0277fb6c73643e4d_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-0a61e79af62f82af0277fb6c73643e4d_r.jpg" data-caption="" data-size="normal" data-rawwidth="942" data-rawheight="904" class="origin_image zh-lightbox-thumb lazy" width="942" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='942'%20height='904'&gt;&lt;/svg&gt;" data-actualsrc="https://pic2.zhimg.com/v2-0a61e79af62f82af0277fb6c73643e4d_b.jpg" title="v2-0a61e79af62f82af0277fb6c73643e4d_r"></figure>
<p>Moment.js 宣布停止開發，進入維護狀態。</p>
<p>這是一個大而全的時間日期庫，極大方便了我們在JavaScript 中計算時間和日期，每週下載量超過1200 萬，已成功用於數百萬個項目中。</p>
<p>但是，作為一個誕生於2011 年的元老級明星項目，以現在的眼光來看Moment.js 並非完美無缺，官方總結了兩大問題：</p>
<p><b>1.可變對象</b></p>
<p>Moment 對像是可變對象（mutable），簡單點說，任何時間上的加減等計算都改變了其本身。這種設計讓代碼變的十分不可控，而且很容易帶來各種隱蔽且難以調試的bug。以至於我們在每步修改之前，都要先調用<code>.clone</code>克隆一次才能放心操作。</p>
<p><b>2.包體積過大</b></p>
<p>因為Momnet.js將全部的功能和所有支持的語言都打到一個包裡，包的大小也是到了<b>280.9 kB</b>這樣一個誇張的數字，而且對於Tree shaking無效。如果要使用時區相關的功能，包體積更是有<b>467.6 kB</b>的大小。簡單點說，我們可能只需要一個<code>.format</code>格式化時間的方法，用戶就需要加載數百kB的庫，這是十分不划算的。</p>
<p>最新版本的Chrome 開發者工具也開始建議用戶更換Moment.js 為同類更小的庫。</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-f6550babff2c5d831b651c5b6e513632_r.jpg" data-caption="" data-size="normal" data-rawwidth="1058" data-rawheight="884" class="origin_image zh-lightbox-thumb" width="1058" data-original="https://pic3.zhimg.com/v2-f6550babff2c5d831b651c5b6e513632_b.jpg" title="v2-f6550babff2c5d831b651c5b6e513632_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-f6550babff2c5d831b651c5b6e513632_r.jpg" data-caption="" data-size="normal" data-rawwidth="1058" data-rawheight="884" class="origin_image zh-lightbox-thumb lazy" width="1058" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1058'%20height='884'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-f6550babff2c5d831b651c5b6e513632_b.jpg" title="v2-f6550babff2c5d831b651c5b6e513632_r"></figure>
<p>現在，Moment.js 停止開發了，那我們接下來該用什麼呢？</p>
<p>官方給了3 種替代方案：</p>
<p><b>1.不使用庫</b></p>
<p>對於一些簡單的時間處理需求，其實JavaScript自帶的<code>Date</code>和<code>Intl</code>對象完全可以滿足。強大的<code>Intl</code>對象可以展示不同時區不同語言的時間日期格式，在多數現代瀏覽器上已經有很好的支持。</p>
<p><b>2. Temporal</b></p>
<p>也許今後的某一天，我們再也不需要使用任何庫，被看作是未來的全新內置的時間日期方案Temporal 很值得期待。這是一個JS 語言內置的重新設計的時間和日期API，現在可以通過實驗性的polyfill 來嘗試Temporal，但離生產上大規模可用還有很長的路要走。</p>
<p><b>3.其他替代庫</b></p>
<p>官方推薦了Luxon，Day.js，date-fns 等更先進設計更優秀的時間日期庫。相比之下可能Day.js 是最值得嘗試的一個。</p>
<p><b>Day.js</b></p>
<blockquote><p>官方推薦語：Day.js 被設計為Moment.js 的極簡替代品，擁有幾乎一樣的API。如果你習慣使用Moment 的API 並希望快速入門，請考慮使用Day.js。</p></blockquote>
<p>上面說到了官方總結的Moment.js 的兩點設計缺陷，可變對象降低了代碼的維護性，過大的包體積影響了影響了整個項目的加載速度。</p>
<p>而將近<b>3萬Github Star</b>的<a href="https://link.zhihu.com/?target=https%3A//github.com/iamkun/dayjs" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Day.js</a>的設計理念與這份總結不謀而合。在保持了優秀的API設計不變的同時，引入不可變對象（immutable）減少了開發時所需的心智成本，同時簡化邏輯使整個包體積僅有<b>2 kB</b>大小。</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-a49439bea7856cb551fef1f44077bf1e_r.jpg" data-caption="" data-size="normal" data-rawwidth="876" data-rawheight="362" class="origin_image zh-lightbox-thumb" width="876" data-original="https://pic3.zhimg.com/v2-a49439bea7856cb551fef1f44077bf1e_b.jpg" title="v2-a49439bea7856cb551fef1f44077bf1e_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-a49439bea7856cb551fef1f44077bf1e_r.jpg" data-caption="" data-size="normal" data-rawwidth="876" data-rawheight="362" class="origin_image zh-lightbox-thumb lazy" width="876" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='876'%20height='362'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-a49439bea7856cb551fef1f44077bf1e_b.jpg" title="v2-a49439bea7856cb551fef1f44077bf1e_r"></figure>
<p class="ztext-empty-paragraph"></p>
<p>Day.js 是一個輕量的JavaScript 時間日期處理庫，和Moment.js 的API 設計保持完全一樣. 如果你曾經用過Moment.js, 那麼你已經知道如何使用Day.js</p>
<p>Day.js 基本用法如下，相同的API，相同的鍊式操作，熟悉的味道。</p>
<div class="highlight">
<pre><code class="language-text">dayjs() .startOf('month') .add(1, 'day') .set('year', 2018) .format('YYYY-MM-DD HH:mm:ss');</code></pre>
</div>
<ul>
<li>和Moment.js 相同的API 和用法</li>
<li>僅2kB 大小的微型庫</li>
<li>不可變數據(Immutable)</li>
<li>支持鍊式操作(Chainable)</li>
<li> I18n 國際化</li>
<li>支持全球時區轉換</li>
</ul>
<p>感謝Moment.js前輩的付出，讓我們的開發體驗變的更好，期待接過接力棒的<a href="https://link.zhihu.com/?target=https%3A//github.com/iamkun/dayjs" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Day.js</a>能讓時間日期處理再也不是難題，也期待未來JavaScript內置的時間日期新方案能讓我們使用Vanilla js 解決一切問題。</p>
</div>
</div>
</article>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15239/topic-250152267/" data-wpel-link="internal">⏰ Moment.js 宣布停止開發，現在該用什麼？</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>用Rust 和N-API 開發高性能NodeJS 擴展</title>
		<link>https://hypergrowths.com/software-engineering/front-end-dev/15235/topic-234914336/</link>
		
		<dc:creator><![CDATA[marketer]]></dc:creator>
		<pubDate>Sat, 30 Jan 2021 18:35:15 +0000</pubDate>
				<category><![CDATA[前端開發]]></category>
		<category><![CDATA[前端外刊評論]]></category>
		<guid isPermaLink="false">https://hypergrowths.com/software-engineering/front-end-dev/15235/topic-234914336/</guid>

					<description><![CDATA[<p>Node native addon，過去和現狀N-API 也發布一段時間了，社區中有很多Native addon 也慢慢遷移到了N-API，比如bcrypt , sse4_crc32 等。 N-API 彌補了之前nan 最痛的跨V8 版本ABI 不兼容的問題。想了解Node…</p>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15235/topic-234914336/" data-wpel-link="internal">用Rust 和N-API 開發高性能NodeJS 擴展</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></description>
										<content:encoded><![CDATA[<article class="Post-Main Post-NormalMain" tabindex="-1">
<header class="Post-Header">
<h1 class="Post-Title">用Rust 和N-API 開發高性能NodeJS 擴展</h1>
<div class="Post-Author">
<div class="AuthorInfo" itemprop="author" itemscope="" itemtype="http://schema.org/Person"><meta itemprop="name" content="太狼"><meta itemprop="image" content="https://pic2.zhimg.com/eaa1578ccae23958fa6eb25ea3e6f0b9_l.jpg?source=172ae18b"><meta itemprop="url" content="https://www.zhihu.com/people/Broooooklyn"><meta itemprop="zhihu:followerCount"></div>
</div>
</header>
<div class="Post-RichTextContainer">
<div class="RichText ztext Post-RichText">
<h2>Node native addon，過去和現狀</h2>
<p>N-API也發布一段時間了，社區中有很多Native addon也慢慢遷移到了N-API，比如<a href="https://link.zhihu.com/?target=https%3A//github.com/kelektiv/node.bcrypt.js" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">bcrypt</a> , <a href="https://link.zhihu.com/?target=https%3A//github.com/anandsuresh/sse4_crc32" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">sse4_crc32</a>等。 N-API彌補了之前<code>nan</code>最痛的<b>跨V8版本ABI不兼容</b>的問題。</p>
<blockquote><p>想了解NodeJS native addon相關接口的同學可以看<a class="member_mention" href="https://www.zhihu.com/people/fa4f2bd314bd381a8afac4055243c91c" data-hash="fa4f2bd314bd381a8afac4055243c91c" data-hovercard="p$b$fa4f2bd314bd381a8afac4055243c91c" data-wpel-link="external" rel="nofollow external noopener noreferrer">@死月絲卡蕾特</a>大佬的部落格<a href="https://link.zhihu.com/?target=https%3A//xcoder.in/2017/07/01/nodejs-addon-history/" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">從暴力到NAN再到NAPI——Node.js原生模塊開發方式變遷</a>。</p></blockquote>
<p>但即使是遷移到了N-API，編寫native addon 也有一些編寫代碼之外的痛點。</p>
<h3>分發困難</h3>
<p>目前主流的native addon 有以下幾種分發方式:</p>
<h3>1. 分發源碼</h3>
<p>需要使用的用戶自行安裝<code>node-gyp</code> ， <code>cmake</code> <code>g++</code>等構建工具，在開發階段這些都不是什麼問題，但隨著<code>Docker</code>的普及，在特定的<code>Docker</code>環境中安裝一堆編譯工具鏈實在是很多團隊的噩夢。而且這個問題如果處理不好的話，還會白白增加<code>Docker image</code>的體積(其實這個問題是可以通過構建Docker image之前就在一個專門的Builder image裡面編譯完來解決，但是我在各種公司聊下來鮮有團隊會這樣做)。</p>
<h3>2. 只分發JavaScript 代碼，postinstall 下載對應產物</h3>
<p>有些native addon 的構建依賴實在是太複雜了，讓普通的Node 開發者在開發階段安裝全套的編譯工具不太現實。還有一種情況是，native addon 本身太複雜了，可能編譯一次需要非常多的時間，庫的作者肯定不希望大家在使用他的庫的時候僅安裝就要花掉半小時吧。</p>
<p>所以還有一種比較流行的方式是通過<code>CI</code>工具，在各個平台(win32/darwin/linux)的<code>CI</code>任務中<b><i>預編譯</i></b>native addon，進行分發的時候只分發對應的JavaScript代碼，而預編譯的addon文件通過<code>postinstall</code>腳本從CDN下載下來。比如社區中有一個比較流行的用來幹這個事情的工具： <a href="https://link.zhihu.com/?target=https%3A//github.com/mapbox/node-pre-gyp" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">node-pre-gyp</a> 。這個工具會根據使用者的配置自動將<code>CI</code>中編譯出來的native addon上傳到特定的地方，然後在安裝的時候從上傳的地方下載下來。</p>
<p>這種分發方式看起來無懈可擊，但其實有幾個沒辦法繞過去的問題:</p>
<ul>
<li>諸如<code>node-pre-gyp</code>這種工具會在項目裡增加很多<b>運行時無關</b>的依賴。</li>
<li>無論上傳到哪個CDN ，都很難兼顧國內/海外用戶。回想起了你卡在<code>postinstall</code>從某個Github release下載文件等1小時最後失敗了的慘痛回憶嗎？誠然在國內搭建對應的binary mirror 可以部分緩解這個問題，但是mirror 不同步/缺失的情況也時有發生。</li>
<li>私有網絡不友好。很多公司的CI/CD 機器可能都沒法訪問外網（他們會有配套的私有NPM，沒有的話也沒有討論的意義），更別說從某些CDN 下載native addon。</li>
</ul>
<h3>3. 不同平台的native addon 通過不同的npm package 分發</h3>
<p>最近前端很火的新一代構建工具<a href="https://link.zhihu.com/?target=https%3A//github.com/evanw/esbuild" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">esbuild</a>就採用了這種方式。每一個native addon 對應一個npm package。然後通過<code>postinstall</code>腳本去安裝當前系統對應的native addon package。</p>
<p>還有一種方式是暴露給用戶安裝使用的package將所有的native package作為<code>optionalDependencies</code> ，然後通過<code>package.json</code>中的<code>os</code>與<code>cpu</code>字段，讓<code>npm/yarn/pnpm</code>在安裝的時候<b>自動選擇(其實是不符合系統要求的就安裝失敗了)</b>安裝哪一個native package，比如:</p>
<div class="highlight">
<pre><code class="language-json"><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="s2">"@node-rs/bcrypt"</span><span class="p">,</span><span class="nt">"version"</span><span class="p">:</span><span class="s2">"0.5.0"</span><span class="p">,</span><span class="nt">"os"</span><span class="p">:</span><span class="p">[</span><span class="s2">"linux"</span><span class="p">,</span><span class="s2">"win32"</span><span class="p">,</span><span class="s2">"darwin"</span><span class="p">],</span><span class="nt">"cpu"</span><span class="p">:</span><span class="p">[</span><span class="s2">"x64"</span><span class="p">],</span><span class="nt">"optionalDependencies"</span><span class="p">:</span><span class="p">{</span><span class="nt">"@node-rs/bcrypt-darwin"</span><span class="p">:</span><span class="s2">"^0.5.0"</span><span class="p">,</span><span class="nt">"@node-rs/bcrypt-linux"</span><span class="p">:</span><span class="s2">"^0.5.0"</span><span class="p">,</span><span class="nt">"@node-rs/bcrypt-win32"</span><span class="p">:</span><span class="s2">"^0.5.0"</span><span class="p">}</span><span class="p">}</span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="s2">"@node-rs/bcrypt-darwin"</span><span class="p">,</span><span class="nt">"version"</span><span class="p">:</span><span class="s2">"0.5.0"</span><span class="p">,</span><span class="nt">"os"</span><span class="p">:</span><span class="p">[</span><span class="s2">"darwin"</span><span class="p">],</span><span class="nt">"cpu"</span><span class="p">:</span><span class="p">[</span><span class="s2">"x64"</span><span class="p">]</span><span class="p">}</span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="s2">"@node-rs/bcrypt-linux"</span><span class="p">,</span><span class="nt">"version"</span><span class="p">:</span><span class="s2">"0.5.0"</span><span class="p">,</span><span class="nt">"os"</span><span class="p">:</span><span class="p">[</span><span class="s2">"linux"</span><span class="p">],</span><span class="nt">"cpu"</span><span class="p">:</span><span class="p">[</span><span class="s2">"x64"</span><span class="p">]</span><span class="p">}</span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="s2">"@node-rs/bcrypt-win32"</span><span class="p">,</span><span class="nt">"version"</span><span class="p">:</span><span class="s2">"0.5.0"</span><span class="p">,</span><span class="nt">"os"</span><span class="p">:</span><span class="p">[</span><span class="s2">"win32"</span><span class="p">],</span><span class="nt">"cpu"</span><span class="p">:</span><span class="p">[</span><span class="s2">"x64"</span><span class="p">]</span><span class="p">}</span></code></pre>
</div>
<p>這種方式是對使用native addon的用戶侵擾最小的分發方式， <a href="https://link.zhihu.com/?target=https%3A//github.com/kribblo/node-ffmpeg-installer%23readme" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">@ffmpeg-installer/ffmpeg</a>就採用了這種方式。</p>
<p>但是這種方式會對native addon 的作者帶來額外的工作量，包括需要編寫一些管理Release binary 和一堆package 的工具，這些工具一般都非常難以調試（一般會跨好幾個系統與好幾個CPU 架構）。</p>
<p>這些工具需要管理整個addon 在開發-&gt; 本地release version -&gt; CI -&gt; artifacts -&gt; deploy 整個階段的流轉過程。除此之外，還要編寫/調試大量的CI/CD 配置，這些都十分費時費力。</p>
<h3>生態和工具鏈</h3>
<p>目前大部分的NodeJS addon 基本都使用C/C++ 開發。 C/C++ 生態非常的繁榮，基本上你想做任何事情都能找到對應的C/C++ 庫。但C/C++ 的生態因為缺乏統一的構建工具鏈以及包管理工具，導致這些第三方庫在實際封裝和使用上會遇到一些其它的問題:</p>
<ul>
<li>使用多個不一樣構建工具鏈的庫的時候可能會很難搞定編譯，比如這幾年以來我一直都在嘗試封裝skia 到nodejs binding, 但是skia 的編譯。 。實在是一言難盡的複雜，所以一直都在遇到這樣或者那樣的問題。</li>
<li>由於沒有好用的包管理器，很多優質的C/C++ 代碼都是作為一個大型項目的一部分存在的，而不是獨立成一個庫。這種情況下想要使用可能只能以Copy代碼的形式: <a href="https://link.zhihu.com/?target=https%3A//github.com/kelektiv/node.bcrypt.js/blob/master/src/bcrypt.cc" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">bcrypt.cc</a> ，這樣對項目後期的升級和維護都帶來了一些問題。</li>
</ul>
<h2>When Rust meets N-API</h2>
<p><code>Rust</code>相信不用我過多介紹(能點進來這篇文章的應該對Rust都有一定的了解了吧)。</p>
<p>用<code>Rust</code>替代<code>C/C++</code>看起來是一個很美好的選擇，Rust有現代化的包管理器: <code>Cargo</code> ，經過這麼多年的發展在生態上尤其是與<code>NodeJS</code>重疊的<b>服務端開發</b>、<b>跨平台CLI工具</b>、<b>跨平台GUI</b> (electron)等領域有了非常多的沉澱。比起<code>C/C++</code>生態， <code>Rust</code>生態的包屬於<b><i>只要有，都可以直接用</i></b>的狀態，而<code>C/C++</code>生態中的第三方代碼則屬於<b><i>肯定有，但不一定能直接用</i></b>的狀態。這種狀態下，用<code>Rust</code>開發Node addon少了很多選擇，也少了很多選擇的煩惱。</p>
<p>在正式決定開始使用<code>Rust</code> + <code>N-API</code>開發<code>NodeJS</code> addon之前，還有一些問題需要討論:</p>
<h3>N-API 的Rust binding</h3>
<p>NodeJS 官方為N-API 提供了相應的頭文件，作為開發Node addon 時所需。而<code>Rust</code>沒有辦法直接使用C的頭文件，所以我們需要將<code>node.h</code>暴露的API先封裝成<code>Rust</code>可以使用的<code>Rust binding</code> .</p>
<p>在<code>Rust</code>生態中，有官方維護的<a href="https://link.zhihu.com/?target=https%3A//github.com/rust-lang/rust-bindgen" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">bindgen</a>來自動生成頭文件對應的<code>Rust binding</code> ，這個工具非常適合<code>node.h</code>這樣非常純粹的C API頭文件，如果是C++ API則會復雜很多。</p>
<p>但是這樣生成出來的<code>Rust binding</code>一般都是<code>unsafe</code>的並且會充滿底層的指針操作，這顯然不利於我們進一步封裝native addon，也享受不到<code>Safe Rust</code>帶來的種種好處。</p>
<p>在早夭的<a href="https://link.zhihu.com/?target=https%3A//github.com/atom-archive/xray" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">xray</a>項目中，最早的編輯器架構並非後來的類似<code>LSP</code>的<code>Client/Server</code>架構，而是<code>NodeJS</code>直接調用<code>Rust</code>編寫的addon。所以在早期，xray有一個非常粗糙的<code>Rust N-API</code>實現。</p>
<p>幾年前我將這些代碼從xray項目的<code>Git</code>的歷史中找回來了，並且加以封裝和改進： <a href="https://link.zhihu.com/?target=https%3A//github.com/napi-rs/napi-rs" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">napi-rs</a> ，將大部分常用的N-API接口封裝成了<code>Safe Rust</code>接口，並為它們編寫了全方位的單元測試，它可以作為使用<code>Rust</code>編寫native addon的重要基石。</p>
<h3>選擇分發方式</h3>
<p><code>Rust</code>作為出了名的編譯極緩慢的語言，分發源碼顯然是不現實的，而且也不可能要求使用的開發者全部都安裝<code>Rust</code>全家桶。</p>
<p>通過<code>postinstall</code>下載作為一種比較簡單但是對使用者極不友好的方式，我覺得也不應該繼續提倡使用。</p>
<p>那最終對於使用<code>Rust</code>編寫的<code>NodeJS native addon</code> ，我們最好的選擇就是使用<b><i>不同平台分別分發addon</i></b>的形式。</p>
<p>在<a href="https://link.zhihu.com/?target=https%3A//github.com/napi-rs/napi-rs" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">napi-rs</a>項目中，我封裝了簡單的<code>cli</code>工具，用來幫助使用<code>napi-rs</code>的開發者管理從本地開發到<code>CI</code>發布的全流程。下面我們來用一個簡單而實際的例子介紹一下如何使用<code>Rust</code>和<code>napi-rs</code>開發、測試、發布一個NodeJS native addon。</p>
<h3>用<code>Rust</code>能做哪些事情</h3>
<p>我們編寫一個native addon，肯定是想要加速一些計算的過程，然而這種加速並不是沒有代價的。</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-d70b67f0576c14343ef99a5de576a1ad_r.jpg" data-caption="" data-size="normal" data-rawwidth="918" data-rawheight="377" class="origin_image zh-lightbox-thumb" width="918" data-original="https://pic2.zhimg.com/v2-d70b67f0576c14343ef99a5de576a1ad_b.jpg" title="v2-d70b67f0576c14343ef99a5de576a1ad_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-d70b67f0576c14343ef99a5de576a1ad_r.jpg" data-caption="" data-size="normal" data-rawwidth="918" data-rawheight="377" class="origin_image zh-lightbox-thumb lazy" width="918" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='918'%20height='377'&gt;&lt;/svg&gt;" data-actualsrc="https://pic2.zhimg.com/v2-d70b67f0576c14343ef99a5de576a1ad_b.jpg" title="v2-d70b67f0576c14343ef99a5de576a1ad_r"></figure>
<p>Native code在一些純計算的場景比js快非常多，但是一旦使用<code>N-API</code>與node的js引擎打交道，就會有非常大的開銷(相對計算而言)。</p>
<p>比如在js裡面設置一個對象的屬性會比在native裡面使用<code>N-API</code>設置一個對象的屬性快好幾倍</p>
<div class="highlight">
<pre><code class="language-js"><span class="kr">const</span><span class="nx">obj</span><span class="o">=</span><span class="p">{}</span><span class="nx">obj</span><span class="p">.</span><span class="nx">a</span><span class="o">=</span><span class="mi">1</span><span class="kd">let</span><span class="nx">mut</span><span class="nx">result</span><span class="o">=</span><span class="nx">env</span><span class="p">.</span><span class="nx">create_object</span><span class="p">()</span><span class="o">?</span><span class="p">;</span><span class="nx">result</span><span class="p">.</span><span class="nx">set_named_property</span><span class="p">(</span><span class="s2">"code"</span><span class="p">,</span><span class="nx">env</span><span class="p">.</span><span class="nx">create_uint32</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="o">?</span><span class="p">)</span><span class="o">?</span><span class="p">;</span><span class="nx">Ok</span><span class="p">(</span><span class="nx">result</span><span class="p">)</span></code></pre>
</div>
<p>所以在封裝native addon的時候，我們應該盡量避免<code>N-API</code>的調用，不然native邏輯為你減少的運行時間又全部被<code>N-API</code>調用給加回去了。</p>
<blockquote><p>使用<code>N-API</code>中需要注意的性能點實在是太多了，這裡就不展開來講了，後面有時間了或許會寫一系列文章介紹各種使用場景下如何選擇最優的方式調用<code>N-API</code>來達到更好的性能。</p></blockquote>
<p>但是有一些<code>N-API</code>的調用其實是必不可少的，比如從參數中的Js值裡面獲取對應的native值，或者把native值轉換成Js值返回:</p>
<div class="highlight">
<pre><code class="language-rust"><span class="cp">#[js_function(1)]</span> <span class="c1">// -&gt; arguments length</span> <span class="k">fn</span> <span class="nf">add_one</span> <span class="p">(</span> <span class="n">ctx</span> : <span class="nc">CallContext</span> <span class="p">)</span> -&gt; <span class="nb">Result</span> <span class="o">&lt;</span> <span class="n">JsNumber</span> <span class="o">&gt;</span> <span class="p">{</span> <span class="kd">let</span> <span class="n">input</span> : <span class="nc">JsNumber</span> <span class="o">=</span> <span class="n">ctx</span> <span class="p">.</span> <span class="n">get</span> <span class="p">(</span> <span class="mi">0</span> <span class="p">)</span> <span class="o">?</span> <span class="p">;</span> <span class="c1">// get first argument</span> <span class="kd">let</span> <span class="n">input_number</span> : <span class="kt">u32</span> <span class="o">=</span> <span class="n">input</span> <span class="p">.</span> <span class="n">try_into</span> <span class="p">()</span> <span class="o">?</span> <span class="p">;</span> <span class="c1">// get u32 value from JsNumber, call `napi_get_value_uint32` under the hood</span> <span class="n">ctx</span> <span class="p">.</span> <span class="n">env</span> <span class="p">.</span> <span class="n">create_u32</span> <span class="p">(</span> <span class="n">input_number</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">)</span> <span class="c1">// convert u32 to JsNumber, call `napi_create_uint32` under the hood</span> <span class="p">}</span></code></pre>
</div>
<p>所以一個典型的native addon至少需要兩次<code>N-API</code>調用(即使返回<code>undefined</code>也要調用<code>napi_get_undefined</code> )。在你打算開始編寫一個native addon的時候，要時刻計算native帶來的加速是否能抵消其中的<code>N-API</code>調用的開銷。像上面例子中的<code>add_one</code>方法，肯定是比Js版本要慢非常多的。 Github上有一個項目對比了不同封裝方式中典型的<code>N-API</code>的調用開銷: <a href="https://link.zhihu.com/?target=https%3A//github.com/jdsaund/rust-node-perf" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">rust-node-perf</a> 。</p>
<p>這裡近似認為<code>a + b</code>這個操作對於純<code>Js</code>和<code>native</code>代碼來說執行時間近似相等:</p>
<div class="highlight">
<pre><code class="language-text">| Framework | Relative Exec Time | Effort | | ----------------------------- | ------------------ | -------- | | node.js | 1 | n/a | | wasm-pack (nodejs target) | 1.5386953312994696 | low | | rust-addon | 2.563630295032209 | high | | napi-rs | 3.1991337066589773 | mid | | neon | 13.342197321199631 | mid | | node-bindgen | 13.606728128895583 | low |</code></pre>
</div>
<p>可以看到<code>napi-rs</code>比直接使用<code>JavaScript</code>慢了3倍多。</p>
<p>這也是為什麼大家都知道native addon 比純JavaScript 快很多，但很少有人在項目中大規模使用的原因。在<code>N-API</code>的調用開銷和<code>v8</code>引擎已經非常快的前提下，大部分的<b>純計算</b>的場景也不適合使用native addon來替換Js，甚至是你還能看到一些地方提到用JavaScript替換了native模塊之後，性能有了質的提升： <a href="https://link.zhihu.com/?target=https%3A//github.com/capnproto/node-capnp%23this-implementation-is-slow" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">github.com/capnproto/no</span> <span class="invisible">de-capnp#this-implementation-is-slow</span></a></p>
<p>再比如我最早用<code>N-API</code>封裝addon的時候有一個失敗的嘗試: <a href="https://link.zhihu.com/?target=https%3A//github.com/napi-rs/node-rs/tree/simd-json" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">@node-rs/simd-json</a> 。我將simd-json封裝成了native addon，希望得到一個比<code>Node</code>自帶的<code>JSON.parse</code>更快的API，但實際測下來native parse的部分快的突破了天際，而將<code>native struct</code>轉變成<code>Js Object</code>中間的<code>N-API</code> call所需要的時間在數量級上遠超parse一個JSON字符串的時間。</p>
<p>已有的一個SIMD json port也有這個問題: <a href="https://link.zhihu.com/?target=https%3A//github.com/luizperes/simdjson_nodejs/issues/5" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">simdjson_nodejs#5</a></p>
<p>那麼到底哪些功能適合用native addon 來完成呢？</p>
<ul>
<li>簡單的輸入輸出但是中間邏輯複雜的計算邏輯，比如直接用到CPU simd指令的<a href="https://link.zhihu.com/?target=https%3A//github.com/napi-rs/node-rs/tree/master/packages/crc32" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">@node-rs/crc32</a> ,或者加密算法<a href="https://link.zhihu.com/?target=https%3A//github.com/napi-rs/node-rs/tree/master/packages/bcrypt" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">@node-rs/bcrypt</a> ,中文分詞<a href="https://link.zhihu.com/?target=https%3A//github.com/napi-rs/node-rs/tree/master/packages/jieba" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">@node-rs/jieba</a> 。這些庫的邏輯都有一個共同點：輸入輸出都非常簡單(避免額外的<code>N-API</code>調用)，中間計算邏輯非常複雜。</li>
<li>一些需要調用系統級API能力的庫，比如之前提到的<code>SIMD</code>指令，還有類似<code>GPU</code>調用等。</li>
</ul>
<p>所以下面讓我們到<a href="https://link.zhihu.com/?target=https%3A//crates.io/" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">crates.io</a>中找一個簡單的支持SIMD功能庫，將它封裝成node native addon，來演示一下如何快樂的使用<code>Rust</code> + <code>N-API</code>做一些高性能並且實用的工具庫。</p>
<h2><code>@napi-rs/fast-escape</code></h2>
<p>先上鍊接:</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-020cac067d242de029cad8f45de31996_r.jpg" data-caption="" data-size="normal" data-rawwidth="2122" data-rawheight="910" class="origin_image zh-lightbox-thumb" width="2122" data-original="https://pic3.zhimg.com/v2-020cac067d242de029cad8f45de31996_b.jpg" title="v2-020cac067d242de029cad8f45de31996_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-020cac067d242de029cad8f45de31996_r.jpg" data-caption="" data-size="normal" data-rawwidth="2122" data-rawheight="910" class="origin_image zh-lightbox-thumb lazy" width="2122" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='2122'%20height='910'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-020cac067d242de029cad8f45de31996_b.jpg" title="v2-020cac067d242de029cad8f45de31996_r"></figure>
<p>在<code>crates.io</code>裡面搜索<code>SIMD</code> ，按全時期下載量排序，找到比較流行的使用了<code>SIMD</code>技術的庫，然後逐個查看。前面2頁的庫要么已經<b>被我封裝過了（逃</b>要么輸入輸出太複雜不適合封裝，要么已經有現成的Node stdlib可以用了，翻到第三頁看到第一個<code>v_htmlescape</code> ，一看就適合用來封裝成addon:</p>
<ul>
<li>用到了SIMD 技術，計算過程可以加速很多</li>
<li>輸入輸出簡單，進一個字符串出一個字符串，不會有太多的<code>N-API</code> call來消耗性能</li>
</ul>
<p>我們從<a href="https://link.zhihu.com/?target=https%3A//github.com/napi-rs/package-template" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">package-template</a>模版項目new一個新項目出來，package-template中已經設置好了各種依賴/CI配置和命令，直接在<code>src/lib.rs</code>中開始寫代碼就行了:</p>
<div class="highlight">
<pre><code class="language-rust"><span class="cp">#[macro_use]</span><span class="k">extern</span><span class="k">crate</span><span class="n">napi</span><span class="p">;</span><span class="cp">#[macro_use]</span><span class="k">extern</span><span class="k">crate</span><span class="n">napi_derive</span><span class="p">;</span><span class="k">use</span><span class="n">napi</span>::<span class="p">{</span><span class="n">CallContext</span><span class="p">,</span><span class="n">Env</span><span class="p">,</span><span class="n">JsString</span><span class="p">,</span><span class="n">Module</span><span class="p">,</span><span class="nb">Result</span><span class="p">};</span><span class="k">use</span><span class="n">v_htmlescape</span>::<span class="n">escape</span><span class="p">;</span><span class="n">register_module</span><span class="o">!</span><span class="p">(</span><span class="n">escape</span><span class="p">,</span><span class="n">init</span><span class="p">);</span><span class="k">fn</span><span class="nf">init</span><span class="p">(</span><span class="n">module</span>:<span class="kp">&amp;</span><span class="nc">mut</span><span class="n">Module</span><span class="p">)</span>-&gt;<span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span><span class="p">{</span><span class="n">module</span><span class="p">.</span><span class="n">create_named_method</span><span class="p">(</span><span class="s">"escapeHTML"</span><span class="p">,</span><span class="n">escape_html</span><span class="p">)</span><span class="o">?</span><span class="p">;</span><span class="nb">Ok</span><span class="p">(())</span><span class="p">}</span><span class="cp">#[js_function(1)]</span><span class="k">fn</span><span class="nf">escape_html</span><span class="p">(</span><span class="n">ctx</span>:<span class="nc">CallContext</span><span class="p">)</span>-&gt;<span class="nb">Result</span><span class="o">&lt;</span><span class="n">JsString</span><span class="o">&gt;</span><span class="p">{</span><span class="kd">let</span><span class="n">input</span><span class="o">=</span><span class="n">ctx</span><span class="p">.</span><span class="n">get</span>::<span class="o">&lt;</span><span class="n">JsString</span><span class="o">&gt;</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span><span class="o">?</span><span class="p">;</span><span class="n">ctx</span><span class="p">.</span><span class="n">env</span><span class="p">.</span><span class="n">create_string_from_std</span><span class="p">(</span><span class="n">escape</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">as_str</span><span class="p">()</span><span class="o">?</span><span class="p">).</span><span class="n">to_string</span><span class="p">())</span><span class="p">}</span></code></pre>
</div>
<p>這就是一個最小可以使用的的native addon 代碼了，代碼分2 個部分:</p>
<ol>
<li><code>register_module</code>宏接受2個參數，第一個是module的名字，可以任意命名，這裡命名為<code>escape</code> ，第二個參數接受一個<code>rust function</code> ，它有唯一一個參數： <code>Module</code> ，這個參數代表了NodeJS中的<code>module</code>對象，我們可以通過設置<code>module.exports</code>對象設置需要導出的東西，而如果要導出函數則有一個<code>helper</code>方法: <code>module.create_named_method</code>來直接導出。</li>
<li> <code>#[js_function(1)]</code>宏用來定義一個<code>JsFunction</code> ，被定義的<code>Rust function</code>有唯一一個<code>CallContext</code>參數，我們從<code>JavaScript</code>代碼中傳入的參數可以通過<code>ctx::get(n)</code>方法獲取。 <code>#[js_function()]</code>宏裡面的參數定義了這個函數有幾個參數，當<code>ctx.get</code>傳入的值大於實際傳入的參數個數的時候會拋一個Js異常。</li>
</ol>
<p>在運行<code>yarn build</code>之後，我們可以在<code>js</code>裡面這樣調用這裡的<code>escape_html</code>函數:</p>
<div class="highlight">
<pre><code class="language-js"><span class="kr">const</span><span class="p">{</span><span class="nx">escapeHTML</span><span class="p">}</span><span class="o">=</span><span class="nx">require</span><span class="p">(</span><span class="s1">'./index'</span><span class="p">)</span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">escapeHTML</span><span class="p">(</span><span class="s1">'&lt;div&gt;1&lt;/div&gt;'</span><span class="p">))</span><span class="c1">// &lt;div&gt;1&lt;/div&gt;
</span></code></pre>
</div>
<p>這裡的<code>yarn build</code>其實做了很多事情:</p>
<ul>
<li>運行<code>cargo build</code> ，將<code>lib.rs</code>編譯成了動態鏈接庫，放在了<code>./target/release/escape.[dll|so|dylib]</code></li>
<li>運行<code>napi build --release --platform</code> ，這個命令將上一步的<code>(lib)escape.[dll|so|dylib]</code>從<code>target/release</code>目錄拷貝到當前目錄下，並重命名為<code>escape.[darwin|win32|linux].node</code></li>
</ul>
<p>然後<code>index.js</code>中調用<code>@node-rs/helper</code>裡面的<code>loadBinding</code>方法，自動從正確的地方加載native addon:</p>
<div class="highlight">
<pre><code class="language-js"><span class="kr">const</span> <span class="p">{</span> <span class="nx">loadBinding</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span> <span class="p">(</span> <span class="s1">'@node-rs/helper'</span> <span class="p">)</span> <span class="cm">/**</span><span class="cm"> * __dirname means load native addon from current dir</span><span class="cm"> * 'escape' means native addon name is `escape`</span><span class="cm"> * the first arguments was decided by `napi.name` field in `package.json`</span><span class="cm"> * the second arguments was decided by `name` field in `package.json`</span><span class="cm"> * loadBinding helper will load `escape.[PLATFORM].node` from `__dirname` first</span><span class="cm"> * If failed to load addon, it will fallback to load from `@napi-rs/escape-[PLATFORM]`</span><span class="cm"> */</span> <span class="nx">module</span> <span class="p">.</span> <span class="nx">exports</span> <span class="o">=</span> <span class="nx">loadBinding</span> <span class="p">(</span> <span class="nx">__dirname</span> <span class="p">,</span> <span class="s1">'escape'</span> <span class="p">,</span> <span class="s1">'@napi-rs/escape'</span> <span class="p">)</span></code></pre>
</div>
<p>這樣我們就能愉快的通過<code>index.js</code>使用封裝好的<code>escapeHTML</code>函數了。</p>
<p>那麼這個封裝能比純<code>JavaScript</code>版本快多少呢？寫一個簡單的benchmark測試一下: <a href="https://link.zhihu.com/?target=https%3A//github.com/napi-rs/fast-escape/blob/master/bench/index.ts" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">bench</a> 。這裡選取了大神<a href="https://link.zhihu.com/?target=https%3A//github.com/sindresorhus" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">sindresorhus</a>的<a href="https://link.zhihu.com/?target=https%3A//github.com/sindresorhus/escape-goat" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">escape-goat</a>作為性能比較的基準:</p>
<div class="highlight">
<pre><code class="language-bash">napi x <span class="m">799</span> ops/sec ±0.38% <span class="o">(</span> <span class="m">93</span> runs sampled <span class="o">)</span> javascript x <span class="m">586</span> ops/sec ±1.40% <span class="o">(</span> <span class="m">81</span> runs sampled <span class="o">)</span> Escape html benchmark <span class="c1"># Large input bench suite: Fastest is napi</span> napi x 2,158,169 ops/sec ±0.59% <span class="o">(</span> <span class="m">93</span> runs sampled <span class="o">)</span> javascript x 1,951,484 ops/sec ±0.31% <span class="o">(</span> <span class="m">92</span> runs sampled <span class="o">)</span> Escape html benchmark <span class="c1"># Small input bench suite: Fastest is napi</span></code></pre>
</div>
<p>我們測試兩個場景的性能: 小規模輸入和大規模輸入。小規模輸入是一行字符串: <code>&lt;div&gt;{props.getNumber()}&lt;/div&gt;</code>大規模輸入是一個<code>1610</code>行的HTML，可以看到不同輸入規模下我們的native addon性能都要優於純JavaScript版本。</p>
<p>經驗告訴我，輸入如果是<code>Buffer</code> ，性能會更優一些(N-API的JsBuffer相關API調用開銷要明顯小於字符串API)，所以我們再加一個接受<code>Buffer</code>的API:</p>
<div class="highlight">
<pre><code class="language-rust"><span class="p">...</span><span class="n">module</span><span class="p">.</span><span class="n">create_named_method</span><span class="p">(</span><span class="s">"escapeHTMLBuf"</span><span class="p">,</span><span class="n">escape_html_buf</span><span class="p">)</span><span class="o">?</span><span class="p">;</span><span class="cp">#[js_function(1)]</span><span class="k">fn</span><span class="nf">escape_html_buf</span><span class="p">(</span><span class="n">ctx</span>:<span class="nc">CallContext</span><span class="p">)</span>-&gt;<span class="nb">Result</span><span class="o">&lt;</span><span class="n">JsString</span><span class="o">&gt;</span><span class="p">{</span><span class="kd">let</span><span class="n">input</span><span class="o">=</span><span class="n">ctx</span><span class="p">.</span><span class="n">get</span>::<span class="o">&lt;</span><span class="n">JsBuffer</span><span class="o">&gt;</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span><span class="o">?</span><span class="p">;</span><span class="kd">let</span><span class="n">input_buf</span>:<span class="kp">&amp;</span><span class="p">[</span><span class="kt">u8</span><span class="p">]</span><span class="o">=</span><span class="o">&amp;</span><span class="n">input</span><span class="p">;</span><span class="n">ctx</span><span class="p">.</span><span class="n">env</span><span class="p">.</span><span class="n">create_string_from_std</span><span class="p">(</span><span class="n">escape</span><span class="p">(</span><span class="k">unsafe</span><span class="p">{</span><span class="kt">str</span>::<span class="n">from_utf8_unchecked</span><span class="p">(</span><span class="n">input_buf</span><span class="p">)}).</span><span class="n">to_string</span><span class="p">())</span><span class="p">}</span></code></pre>
</div>
<p>再來測試一下性能，運行<code>yarn bench</code> ：</p>
<div class="highlight">
<pre><code class="language-bash">napi x <span class="m">799</span> ops/sec ±0.38% <span class="o">(</span> <span class="m">93</span> runs sampled <span class="o">)</span> napi#buff x <span class="m">980</span> ops/sec ±1.39% <span class="o">(</span> <span class="m">92</span> runs sampled <span class="o">)</span> javascript x <span class="m">586</span> ops/sec ±1.40% <span class="o">(</span> <span class="m">81</span> runs sampled <span class="o">)</span> Escape html benchmark <span class="c1"># Large input bench suite: Fastest is napi#buff</span> napi x 2,158,169 ops/sec ±0.59% <span class="o">(</span> <span class="m">93</span> runs sampled <span class="o">)</span> napi#buff x 2,990,077 ops/sec ±0.73% <span class="o">(</span> <span class="m">93</span> runs sampled <span class="o">)</span> javascript x 1,951,484 ops/sec ±0.31% <span class="o">(</span> <span class="m">92</span> runs sampled <span class="o">)</span> Escape html benchmark <span class="c1"># Small input bench suite: Fastest is napi#buff</span></code></pre>
</div>
<p>果然， <code>Buffer</code>風格的API性能更好一些。</p>
<p>而一般高強度的計算任務，我們一般都希望把它<code>spawn</code>到另一個線程，以免阻塞主線程的執行。 （ <code>escape</code>這個例子可能不太合適，因為這裡的計算開銷還是挺小的)。使用<code>napi-rs</code>也可以比較輕鬆的完成這個任務:</p>
<div class="highlight">
<pre><code class="language-rust"><span class="n">module</span><span class="p">.</span><span class="n">create_named_method</span><span class="p">(</span><span class="s">"asyncEscapeHTMLBuf"</span><span class="p">,</span><span class="n">async_escape_html_buf</span><span class="p">)</span><span class="o">?</span><span class="p">;</span><span class="k">struct</span><span class="nc">EscapeTask</span><span class="o">&lt;</span><span class="na">'env</span><span class="o">&gt;</span><span class="p">(</span><span class="o">&amp;</span><span class="na">'env</span><span class="p">[</span><span class="kt">u8</span><span class="p">]);</span><span class="k">impl</span><span class="o">&lt;</span><span class="na">'env</span><span class="o">&gt;</span><span class="n">Task</span><span class="k">for</span><span class="n">EscapeTask</span><span class="o">&lt;</span><span class="na">'env</span><span class="o">&gt;</span><span class="p">{</span><span class="k">type</span><span class="nc">Output</span><span class="o">=</span><span class="nb">String</span><span class="p">;</span><span class="k">type</span><span class="nc">JsValue</span><span class="o">=</span><span class="n">JsString</span><span class="p">;</span><span class="k">fn</span><span class="nf">compute</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span><span class="bp">self</span><span class="p">)</span>-&gt;<span class="nb">Result</span><span class="o">&lt;</span><span class="n">Self</span>::<span class="n">Output</span><span class="o">&gt;</span><span class="p">{</span><span class="nb">Ok</span><span class="p">(</span><span class="n">escape</span><span class="p">(</span><span class="k">unsafe</span><span class="p">{</span><span class="kt">str</span>::<span class="n">from_utf8_unchecked</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="mi">0</span><span class="p">)</span><span class="p">}).</span><span class="n">to_string</span><span class="p">())</span><span class="p">}</span><span class="k">fn</span><span class="nf">resolve</span><span class="p">(</span><span class="o">&amp;</span><span class="bp">self</span><span class="p">,</span><span class="n">env</span>:<span class="kp">&amp;</span><span class="nc">mut</span><span class="n">Env</span><span class="p">,</span><span class="n">output</span>:<span class="nc">Self</span>::<span class="n">Output</span><span class="p">)</span>-&gt;<span class="nb">Result</span><span class="o">&lt;</span><span class="n">Self</span>::<span class="n">JsValue</span><span class="o">&gt;</span><span class="p">{</span><span class="n">env</span><span class="p">.</span><span class="n">create_string_from_std</span><span class="p">(</span><span class="n">output</span><span class="p">)</span><span class="p">}</span><span class="p">}</span><span class="cp">#[js_function(1)]</span><span class="k">fn</span><span class="nf">async_escape_html_buf</span><span class="p">(</span><span class="n">ctx</span>:<span class="nc">CallContext</span><span class="p">)</span>-&gt;<span class="nb">Result</span><span class="o">&lt;</span><span class="n">JsObject</span><span class="o">&gt;</span><span class="p">{</span><span class="kd">let</span><span class="n">input</span><span class="o">=</span><span class="n">ctx</span><span class="p">.</span><span class="n">get</span>::<span class="o">&lt;</span><span class="n">JsBuffer</span><span class="o">&gt;</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span><span class="o">?</span><span class="p">;</span><span class="kd">let</span><span class="n">task</span><span class="o">=</span><span class="n">EscapeTask</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">data</span><span class="p">);</span><span class="n">ctx</span><span class="p">.</span><span class="n">env</span><span class="p">.</span><span class="n">spawn</span><span class="p">(</span><span class="n">task</span><span class="p">)</span><span class="p">}</span></code></pre>
</div>
<p>在上面的代碼中，我們定義了一個<code>EscapeTask</code>結構，然後實現<code>napi</code>中的<code>Task trait</code> ， <code>Task trait</code>需要實現4個部分:</p>
<ul>
<li><code>type Output</code>在<code>libuv</code>線程池中計算返回的值，一般是<code>Rust</code>值</li>
<li><code>type JsValue</code>計算完成後<code>Promise resolve</code>的值</li>
<li><code>compute</code>方法，定義了在<code>libuv</code>線程池中的計算邏輯</li>
<li><code>resolve</code>方法，將計算完畢的<code>Output</code>轉化成Js值，最後被<code>Promise resolve</code></li>
</ul>
<p>而在新定義的<code>js_function</code> <code>async_escape_html_buf</code>中，我們只需要構造剛才的<code>EscapeTask</code> ，然後使用<code>spawn</code>方法就能得到一個<code>Promise</code>對象:</p>
<div class="highlight">
<pre><code class="language-rust"><span class="kd">let</span><span class="n">task</span><span class="o">=</span><span class="n">EscapeTask</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">data</span><span class="p">);</span><span class="n">ctx</span><span class="p">.</span><span class="n">env</span><span class="p">.</span><span class="n">spawn</span><span class="p">(</span><span class="n">task</span><span class="p">)</span></code></pre>
</div>
<p>在<code>js</code>中，我們可以這樣使用:</p>
<div class="highlight">
<pre><code class="language-js"><span class="kr">const</span><span class="p">{</span><span class="nx">asyncEscapeHTMLBuf</span><span class="p">}</span><span class="o">=</span><span class="nx">require</span><span class="p">(</span><span class="s1">'./index'</span><span class="p">)</span><span class="nx">asyncEscapeHTMLBuf</span><span class="p">(</span><span class="nx">Buffer</span><span class="p">.</span><span class="nx">from</span><span class="p">(</span><span class="s1">'&lt;div&gt;1&lt;/div&gt;'</span><span class="p">))</span><span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">escaped</span><span class="p">)</span><span class="p">=&gt;</span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">escaped</span><span class="p">))</span><span class="c1">// &lt;div&gt;1&lt;/div&gt;
</span></code></pre>
</div>
<p>到這里為止，我們的一個簡單的native addon 就編寫完畢了，而發布這個包，只需要以下幾步:</p>
<ul>
<li>commit 剛才變更的代碼</li>
<li>運行<code>npm version [patch | minor | major | ...]</code>命令</li>
<li><code>git push --follow-tags</code></li>
</ul>
<p>倉庫中配置好的<code>Github actions</code>會自動幫你將<code>native</code>模塊分別通過不同的npm包發布<a href="https://link.zhihu.com/?target=https%3A//github.com/napi-rs/fast-escape/actions/runs/247349235" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">Build log</a></p>
<p>當然如果你真的要做這些事情，有幾個前置修改需要做:</p>
<ul>
<li>全局替換<code>pacakge-tempalte</code>到你的包名(後面會提供CLI來幫你做這件事情)</li>
<li>修改<code>.github/workflows/CI.yml</code>中<code>Upload artifact</code>步驟中的<code>package-template</code> ，新的值需要和<code>package.json</code>中的<code>napi.name</code>字段保持一致(後面也會提供CLI來幫你做這件事)</li>
<li>如果你的包名不在<code>@scope</code>下，需要保證<code>package-name-darwin</code> , <code>package-name-win32</code> , <code>package-name-linux</code> , <code>package-name-linux-musl</code>這幾個包你都有發布權限</li>
</ul>
<p>至此，一個簡單的native addon就封裝完成了，大家可以使用<code>yarn add @napi-rs/escape</code>來試玩一下剛才封裝的這個native addon。</p>
<h2>END</h2>
<p><code>napi-rs</code>從誕生到現在，已經形成了一定規模的生態了， <a href="https://link.zhihu.com/?target=https%3A//github.com/napi-rs/node-rs" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">node-rs</a>倉庫集中封裝了一些常見的native addon (deno_lint目前還在非常初始的階段)， <a href="https://link.zhihu.com/?target=https%3A//github.com/Brooooooklyn/swc-node" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">swc-node</a>已經有很多項目用起來了，而由於<code>swc-node</code>的成功， <code>swc</code>的作者最近也從<code>neon</code>遷移到了<code>napi-rs</code>上: <a href="https://link.zhihu.com/?target=https%3A//github.com/swc-project/swc/pull/1009" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">github.com/swc-project/</span> <span class="invisible">swc/pull/1009</span></a></p>
<p>這次<code>migrate</code>讓<code>swc</code>的API性能<b>快了2倍</b><a href="https://link.zhihu.com/?target=https%3A//github.com/swc-project/swc/issues/852" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">swc#852</a> (這也是目前napi-rs對比neon的優勢之一)，並且在CI和發布管理上節省了很多代碼量。</p>
<p>最後歡迎大家試用<a href="https://link.zhihu.com/?target=https%3A//github.com/napi-rs/napi-rs" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">napi-rs</a> ，包括<a href="https://link.zhihu.com/?target=https%3A//github.com/strapi/strapi" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">strapi</a>在內的很多大型NodeJS項目(包括字節跳動內部的NodeJS基礎庫，支撐的總QPS可能超過10w)已經用上<code>napi-rs</code>封裝的庫了，所以它在代碼上已經production ready 了。</p>
<p>後面我會持續建設它的文檔和周邊工具鏈，讓它更好用更易用，所以大家也不要忘了給個Star或者<b>Sponsor</b> !</p>
<h2>招聘</h2>
<p>我所在的團隊是字節跳動IES 前端架構，基礎體驗方向。 IES 中文名稱是互娛研發，也就是抖音、tiktok 等超大體量產品所在的部門。我們團隊計劃在前端、hybrid 、flutter、自研引擎、小程序、NodeJS 等多個方面做很多關於性能、體驗、監控相關的事情。</p>
<p>最近我們有計劃通過<code>swc</code>和<code>N-API</code>做一些源碼掃描的工具，預計可以比基於<a href="https://link.zhihu.com/?target=https%3A//github.com/acornjs/acorn" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">acorn</a>的掃描邏輯快10~100倍。後面的規劃中，也會有大量涉及到<code>Rust</code>與前端/NodeJS結合的領域可以去開拓，歡迎大家踴躍聯繫我給我投簡歷！ ！ ！</p>
<p>我的個人微信在: <a href="https://link.zhihu.com/?target=https%3A//github.com/Brooooooklyn" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">github.com/Brooooooklyn</span></a></p>
<p>你也可以直接通過我的內推鏈接投遞: <a href="https://link.zhihu.com/?target=https%3A//job.toutiao.com/s/JSea1oG" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">job.toutiao.com/s/JSea1</span> <span class="invisible">oG</span></a></p>
<p>也可以發郵件到我的郵箱投遞: <a href="mailto:lynweklm@gmail.com">lynweklm@gmail.com</a></p>
</div>
</div>
</article>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15235/topic-234914336/" data-wpel-link="internal">用Rust 和N-API 開發高性能NodeJS 擴展</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>字節跳動-廣告研發&#124; 2021秋季校園人才召集令！</title>
		<link>https://hypergrowths.com/software-engineering/front-end-dev/15233/topic-208949560/</link>
		
		<dc:creator><![CDATA[marketer]]></dc:creator>
		<pubDate>Sat, 30 Jan 2021 18:35:04 +0000</pubDate>
				<category><![CDATA[前端開發]]></category>
		<category><![CDATA[前端外刊評論]]></category>
		<guid isPermaLink="false">https://hypergrowths.com/software-engineering/front-end-dev/15233/topic-208949560/</guid>

					<description><![CDATA[<p>就剛才，HR 小姐姐Lark 小窗我：寸志同學～～我來求助，我們現在上海前端校招進展特別緩慢，能不能……我有點意外，我們字節跳動廣告系統團隊成立於2014年，依托今日頭條的海量數據，專注於為全國客戶提供最佳的…</p>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15233/topic-208949560/" data-wpel-link="internal">字節跳動-廣告研發| 2021秋季校園人才召集令！</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></description>
										<content:encoded><![CDATA[<article class="Post-Main Post-NormalMain" tabindex="-1">
<header class="Post-Header">
<h1 class="Post-Title">字節跳動-廣告研發| 2021秋季校園人才召集令！ </h1>
<div class="Post-Author">
<div class="AuthorInfo" itemprop="author" itemscope="" itemtype="http://schema.org/Person"><meta itemprop="name" content="寸志"><meta itemprop="image" content="https://pic2.zhimg.com/v2-d07aa43f40c12e59a1a91fc59c719c25_l.jpg?source=172ae18b"><meta itemprop="url" content="https://www.zhihu.com/people/stein.cun"><meta itemprop="zhihu:followerCount"></div>
</div>
</header>
<div class="Post-RichTextContainer">
<div class="RichText ztext Post-RichText">
<figure data-size="normal"><noscript><img decoding="async" src="" data-caption="" data-size="normal" data-rawwidth="300" data-rawheight="300" class="content_image" width="300" data-original="https://pic1.zhimg.com/v2-48a4f25d17a1c01b90af0b9fda12e014_b.jpg"></noscript><img decoding="async" src="" data-caption="" data-size="normal" data-rawwidth="300" data-rawheight="300" class="content_image lazy" width="300" data-actualsrc="https://pic1.zhimg.com/v2-48a4f25d17a1c01b90af0b9fda12e014_b.jpg" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='300'%20height='300'&gt;&lt;/svg&gt;"></figure>
<p class="ztext-empty-paragraph"></p>
<h2><b>就剛才，HR小姐姐Lark小窗我：</b></h2>
<blockquote><p><b>寸志同學～～我來求助，我們現在上海前端校招進展特別緩慢，能不能……</b></p></blockquote>
<h2><b>我有點意外，我們</b></h2>
<p><i>字節跳動廣告系統團隊成立於2014年，依托今日頭條的海量數據，專注於為全國客戶提供最佳的商業推廣方式與方案，並不斷努力改善用戶的產品體驗，其開發的廣告系統支撐著頭條系所有產品的商業化變現。到目前為止，廣告系統團隊已經積聚了上百名優秀的研發人才，打造出了海內外領先的訊息流廣告系統，每天支撐著公司數億的穩定收入。連續三年，收入以每年3倍速度增長，前景非常廣闊！</i></p>
<h2><b>怎麼出現招聘進展緩慢的情況？ ？ ？</b></h2>
<figure data-size="normal"><noscript><img decoding="async" src="" data-caption="" data-size="normal" data-rawwidth="160" data-rawheight="177" class="content_image" width="160" data-original="https://pic1.zhimg.com/v2-e2f57b4fcf69b7a82b1249e6cfc5d004_b.png"></noscript><img decoding="async" src="" data-caption="" data-size="normal" data-rawwidth="160" data-rawheight="177" class="content_image lazy" width="160" data-actualsrc="https://pic1.zhimg.com/v2-e2f57b4fcf69b7a82b1249e6cfc5d004_b.png" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='160'%20height='177'&gt;&lt;/svg&gt;"></figure>
<h2><b>所以，各大高校的童鞋們，各路大佬們，我必須再強調一下我們部門的優勢，我喜歡這裡，推薦大家來的原因：</b></h2>
<ul>
<li><b><i>為什麼選擇廣告行業？</i></b></li>
</ul>
<p><i>（1）廣告變現對於很多互聯網公司來說是非常重要的變現方式，屬於核心領域且發展持久，是全世界互聯網公司最主流的收入來源，也成為頂尖人才的匯聚之地。前沿技術首先應用於廣告行業，使其成為一個高投入高回報的領域。在這裡你可以獲得廣闊的成長空間，大牛的輔導，實踐應用科技行業的各類最新技術。</i></p>
<p><i>（2）廣告業務相比C端業務具有更高的業務壁壘，可以更好的積累自己的業務方向的競爭優勢。後續職業生涯的發展，走技術專家路線能走出成績走出深度的畢竟是少數，那麼自己的競爭力如何構建？深入某個具有足夠深度和壁壘的業務方向業務是個可行的方案。大流量(億級)，高並發，複雜系統架構設計，智能算法設計，複雜廣告場景等，都可以施展技術提供用武之地。</i></p>
<ul>
<li><b><i>為什麼要選擇來頭條做廣告？</i></b></li>
</ul>
<p><i>（1）字節跳動是首創訊息流廣告的公司，擁有著完備的廣告系統產業鏈並在今日頭條，抖音，西瓜視頻和國際化產品上廣泛應用，創造多個行業最佳。</i></p>
<p><i>（2）頭條廣告系統匯聚大量行業top人才，在這裡，有著完備的廣告系統技術產業鏈，最複雜的廣告場景，最大的數據量級。在和優秀的人做有挑戰的事的同時，提升自己的技術深度與廣度。</i></p>
<ul>
<li><b><i>對個人技術的提高？</i></b></li>
</ul>
<p><i>技術的深度和廣度，是由業務複雜性和業務的發展速度決定的。我們的廣告後台是一個從十億廣告規模到千億廣告規模快速發展的系統，這個過程和技術，如同造火箭一般。對技術的要求和提升具體來說有這些方面：</i></p>
<p><i>1.面對複雜的業務系統，在快速迭代的場景下，穩定性可靠性要求極高，這裡面的技術都是很深的，積累下來一定是專家級水平；</i></p>
<p><i>2.我們後台語言主要使用號稱工程最友好的語言go，用過的人都說好；</i></p>
<p><i>3.廣告系統是敏捷開發模式，快速迭代，CI CD，基本是業界最先進的開發發布模式；</i></p>
<p><i>4.廣告後台也有高並發，比如打包服務，廣告庫數據服務，高並發的在線服務，近在線的數據服務，都很有挑戰，有極大的發揮空間。</i></p>
<figure data-size="normal"><noscript><img decoding="async" src="" data-caption="" data-size="normal" data-rawwidth="300" data-rawheight="300" class="content_image" width="300" data-original="https://pic2.zhimg.com/v2-68a336dd406d1281c27f40ddb99308b5_b.jpg"></noscript><img decoding="async" src="" data-caption="" data-size="normal" data-rawwidth="300" data-rawheight="300" class="content_image lazy" width="300" data-actualsrc="https://pic2.zhimg.com/v2-68a336dd406d1281c27f40ddb99308b5_b.jpg" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='300'%20height='300'&gt;&lt;/svg&gt;"></figure>
<h2>我打賭，你來字節跳動上班，一定會像我一樣地開心：</h2>
<p><b><i>自由平等的工作環境</i></b></p>
<p><i>我們在全球180多個城市設有office，</i>這次秋招，我們在北京、上海、深圳、杭州、成都、廣州、武漢、南京、廈門、無錫等多個城市都有許多崗位可以投遞！</p>
<p><b><i>靈活的辦公時間，上班不打卡</i></b></p>
<p><i>上下班無需打卡，效率為先，你的工作時間由你來安排</i></p>
<p><b><i>輕鬆的辦公環境，工位不設限</i></b></p>
<p><i>開放式工位，獨立討論區，靈感碰撞不受空間束縛</i></p>
<p><b><i>平等的工作氛圍，溝通無“大小”</i></b></p>
<p><i>大家互相之間以“同學”相稱，不講title，坦誠的溝通風格幫你更自在地工作</i></p>
<p><b><i>鼓勵多元，擁抱文化多樣性</i></b></p>
<p><i>為不同性格、年齡、文化和職業背景的同學，創造多元兼容的環境</i></p>
<p class="ztext-empty-paragraph"></p>
<figure data-size="normal"><noscript><img decoding="async" src="" data-caption="" data-size="normal" data-rawwidth="320" data-rawheight="320" class="content_image" width="320" data-original="https://pic2.zhimg.com/v2-1473b110db513c18d967d67333f7fcdd_b.jpg"></noscript><img decoding="async" src="" data-caption="" data-size="normal" data-rawwidth="320" data-rawheight="320" class="content_image lazy" width="320" data-actualsrc="https://pic2.zhimg.com/v2-1473b110db513c18d967d67333f7fcdd_b.jpg" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='320'%20height='320'&gt;&lt;/svg&gt;"></figure>
<h2>行吧，餐廳美食、健身游泳……的圖我就不發了，先觀摩一下我們的新辦公室吧！</h2>
<h2>不廢話了，咱們進入正題，想做前端的童鞋，一定要掃碼給我們投遞簡歷哦！</h2>
<p class="ztext-empty-paragraph"></p>
<p><a href="https://link.zhihu.com/?target=http%3A//qr21.cn/AWxJte" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">http://</span> <span class="visible">qr21.cn/AWxJte</span></a> (二維碼自動識別)</p>
<hr>
<p class="ztext-empty-paragraph"></p>
<figure data-size="normal"><noscript><img decoding="async" src="" data-caption="" data-size="normal" data-rawwidth="300" data-rawheight="300" class="content_image" width="300" data-original="https://pic1.zhimg.com/v2-fc83e3e2c7ceb01c4a63307afe117ec4_b.jpg"></noscript><img decoding="async" src="" data-caption="" data-size="normal" data-rawwidth="300" data-rawheight="300" class="content_image lazy" width="300" data-actualsrc="https://pic1.zhimg.com/v2-fc83e3e2c7ceb01c4a63307afe117ec4_b.jpg" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='300'%20height='300'&gt;&lt;/svg&gt;"></figure>
<h2>除了前端，還挺有各種火熱校招職位等待你的投遞，掃碼查看</h2>
<p><a href="https://link.zhihu.com/?target=https%3A//job.toutiao.com/campus/m/invite%3Freferral_code%3DGXQKUDG" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">job.toutiao.com/campus/</span> <span class="invisible">m/invite?referral_code=GXQKUDG</span></a> (二維碼自動識別)</p>
<h2>也歡迎加我微信island205諮詢各種校招問題，也可以把簡歷通過微信或者郵箱<a href="mailto:cunzhi@bytedance.com">cunzhi@bytedance.com</a>發給我，幫你內推！</h2>
</div>
</div>
</article>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15233/topic-208949560/" data-wpel-link="internal">字節跳動-廣告研發| 2021秋季校園人才召集令！</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>前端+ AI ——從圖片識別UI樣式</title>
		<link>https://hypergrowths.com/software-engineering/front-end-dev/15220/topic-207308196/</link>
		
		<dc:creator><![CDATA[marketer]]></dc:creator>
		<pubDate>Sat, 30 Jan 2021 18:34:37 +0000</pubDate>
				<category><![CDATA[前端開發]]></category>
		<category><![CDATA[前端外刊評論]]></category>
		<guid isPermaLink="false">https://hypergrowths.com/software-engineering/front-end-dev/15220/topic-207308196/</guid>

					<description><![CDATA[<p>導語：前端智能化，就是通過AI/CV技術，使前端工具鏈具備理解能力，進而輔助開發提升研發效率，比如實現基於設計稿智能佈局和組件智能識別等。本文要介紹的是前端智能化的一類實踐：通過計算機視覺和機器學習實現…</p>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15220/topic-207308196/" data-wpel-link="internal">前端+ AI ——從圖片識別UI樣式</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></description>
										<content:encoded><![CDATA[<article class="Post-Main Post-NormalMain" tabindex="-1">
<header class="Post-Header">
<h1 class="Post-Title">前端+ AI ——從圖片識別UI樣式</h1>
<div class="Post-Author">
<div class="AuthorInfo" itemprop="author" itemscope="" itemtype="http://schema.org/Person"><meta itemprop="name" content="Y.One"><meta itemprop="image" content="https://pic4.zhimg.com/v2-367c45c113efd4e16632d4e50ce72594_l.jpg?source=172ae18b"><meta itemprop="url" content="https://www.zhihu.com/people/yonechen"><meta itemprop="zhihu:followerCount"></div>
</div>
</header>
<div class="Post-RichTextContainer">
<div class="RichText ztext Post-RichText">
<p><i>導語：前端智能化，就是通過AI/CV技術，使前端工具鏈具備理解能力，進而輔助開發提升研發效率，比如實現基於設計稿智能佈局和組件智能識別等。</i></p>
<p>本文要介紹的是前端智能化的一類實踐：通過計算機視覺和機器學習實現自動提取圖片中的UI樣式的能力。</p>
<p class="ztext-empty-paragraph"></p>
<figure data-size="normal"><noscript><img decoding="async" src="https://pic1.zhimg.com/v2-bfa768b18aabbe167909e7ae3523a2c4_r.jpg" data-caption="" data-size="normal" data-rawwidth="640" data-rawheight="360" data-thumbnail="https://pic1.zhimg.com/v2-bfa768b18aabbe167909e7ae3523a2c4_b.jpg" class="origin_image zh-lightbox-thumb" width="640" data-original="https://pic1.zhimg.com/v2-bfa768b18aabbe167909e7ae3523a2c4_b.gif"></noscript><img decoding="async" src="https://pic1.zhimg.com/v2-bfa768b18aabbe167909e7ae3523a2c4_r.jpg" data-caption="" data-size="normal" data-rawwidth="640" data-rawheight="360" data-thumbnail="https://pic1.zhimg.com/v2-bfa768b18aabbe167909e7ae3523a2c4_b.jpg" class="origin_image zh-lightbox-thumb lazy" width="640" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='640'%20height='360'&gt;&lt;/svg&gt;" data-actualsrc="https://pic1.zhimg.com/v2-bfa768b18aabbe167909e7ae3523a2c4_b.gif"></figure>
<p class="ztext-empty-paragraph"></p>
<p>具體效果如上圖，當用戶框選圖片中包含組件的區域，算法能準確定位組件位置，並有效識別組件的UI樣式。</p>
<h2>樣式提取方案</h2>
<p>本文基於<a href="https://link.zhihu.com/?target=https%3A//opencv.org/about/" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">OpenCV</a> -Python實現圖像的樣式檢測，主要分為三步： 1.從圖片檢測並分離組件區域； 2.基於組件區域進行形狀檢測； 3.對符合規則形狀的組件進行樣式計算。</p>
<h2>1. 從圖片分離組件區域</h2>
<p>組件區域分離主要是通過圖像分割算法，識別組件區域（前景）和背景區域，本文主要從用戶框選操作上考慮，採用了可交互可迭代的Grab Cut算法。 Grab cut算法允許用戶框選作為前景輸入，利用混合高斯模型GMM，找到前景和背景的最佳分割路徑，具體可參考文章： <a href="https://link.zhihu.com/?target=https%3A//blog.csdn.net/zouxy09/article/details/8534954" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">圖像分割——Grab Cut算法</a></p>
<p class="ztext-empty-paragraph"></p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-4be6dbd1d1bad3b8375049a02320a488_r.jpg" data-caption="" data-size="normal" data-rawwidth="1240" data-rawheight="838" class="origin_image zh-lightbox-thumb" width="1240" data-original="https://pic1.zhimg.com/v2-4be6dbd1d1bad3b8375049a02320a488_b.jpg" title="v2-4be6dbd1d1bad3b8375049a02320a488_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-4be6dbd1d1bad3b8375049a02320a488_r.jpg" data-caption="" data-size="normal" data-rawwidth="1240" data-rawheight="838" class="origin_image zh-lightbox-thumb lazy" width="1240" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1240'%20height='838'&gt;&lt;/svg&gt;" data-actualsrc="https://pic1.zhimg.com/v2-4be6dbd1d1bad3b8375049a02320a488_b.jpg" title="v2-4be6dbd1d1bad3b8375049a02320a488_r"></figure>
<p class="ztext-empty-paragraph"></p>
<p>如上圖，通過調用OpenCV的<code>cv2.grabCut</code>方法時，我們將組件前景框(x, y, width, height)作為方法入參，識別出的組件像素被存儲在mask遮罩。</p>
<h3>代碼實現</h3>
<div class="highlight">
<pre><code class="language-python"><span class="k">def</span><span class="nf">extract</span><span class="p">(</span><span class="n">img</span><span class="p">,</span><span class="n">rect</span><span class="p">):</span><span class="s2">"""輸入框選區，輸出GrabCut遮罩"""</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="p">,</span><span class="n">w</span><span class="p">,</span><span class="n">h</span><span class="o">=</span><span class="n">rect</span><span class="n">roi_img</span><span class="o">=</span><span class="n">img</span><span class="p">[</span><span class="n">y</span><span class="p">:</span><span class="n">y</span><span class="o">+</span><span class="n">h</span><span class="p">,</span><span class="n">x</span><span class="p">:</span><span class="n">x</span><span class="o">+</span><span class="n">h</span><span class="p">]</span><span class="n">mask</span><span class="o">=</span><span class="n">np</span><span class="o">.</span><span class="n">zeros</span><span class="p">(</span><span class="n">roi_img</span><span class="o">.</span><span class="n">shape</span><span class="p">[:</span><span class="mi">2</span><span class="p">],</span><span class="n">np</span><span class="o">.</span><span class="n">uint8</span><span class="p">)</span><span class="c1">#初始化遮罩層</span><span class="n">bgdModel</span><span class="o">=</span><span class="n">np</span><span class="o">.</span><span class="n">zeros</span><span class="p">((</span><span class="mi">1</span><span class="p">,</span><span class="mi">65</span><span class="p">),</span><span class="n">np</span><span class="o">.</span><span class="n">float64</span><span class="p">)</span><span class="n">fgdModel</span><span class="o">=</span><span class="n">np</span><span class="o">.</span><span class="n">zeros</span><span class="p">((</span><span class="mi">1</span><span class="p">,</span><span class="mi">65</span><span class="p">),</span><span class="n">np</span><span class="o">.</span><span class="n">float64</span><span class="p">)</span><span class="c1">#函數的返回值是更新的mask, bgdModel, fgdModel</span><span class="n">cv2</span><span class="o">.</span><span class="n">grabCut</span><span class="p">(</span><span class="n">img</span><span class="p">,</span><span class="n">mask</span><span class="p">,</span><span class="n">rect</span><span class="p">,</span><span class="n">bgdModel</span><span class="p">,</span><span class="n">fgdModel</span><span class="p">,</span><span class="mi">4</span><span class="p">,</span><span class="n">cv2</span><span class="o">.</span><span class="n">GC_INIT_WITH_RECT</span><span class="p">)</span><span class="n">mask</span><span class="o">=</span><span class="n">np</span><span class="o">.</span><span class="n">where</span><span class="p">((</span><span class="n">mask</span><span class="o">==</span><span class="mi">2</span><span class="p">)</span><span class="o">|</span><span class="p">(</span><span class="n">mask</span><span class="o">==</span><span class="mi">0</span><span class="p">),</span><span class="mi">0</span><span class="p">,</span><span class="mi">255</span><span class="p">)</span><span class="o">.</span><span class="n">astype</span><span class="p">(</span><span class="s2">"uint8"</span><span class="p">)</span><span class="k">return</span><span class="n">mask</span></code></pre>
</div>
<p>通過這一步，我們從背景分離出目標遮罩，它是包含了N個組件區域的二值圖。</p>
<h3>2. 組件的形狀檢測</h3>
<p>接下來，我們需要通過形狀檢測從遮罩區篩選出多個可用樣式還原的組件，比如矩形、帶圓角矩形和圓形。具體分為兩步：1) 提取組件外輪廓2) 霍夫檢測識別輪廓形狀</p>
<h3>2.1 外輪廓提取</h3>
<p>第一步是通過前面圖割遮罩進行外輪廓提取，排除組件內部其它線條帶來的影響。輪廓提取主要使用<a href="https://zhuanlan.zhihu.com/(https://blog.csdn.net/yiqiudream/article/details/76864722)" class="internal" data-wpel-link="external" rel="nofollow external noopener noreferrer">Suzuki85輪廓跟踪算法</a>，該算法基於二值圖像拓補，能確定連通域的包含關係。</p>
<p class="ztext-empty-paragraph"></p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-431fef3b3209978a8cb1152ca08ea526_r.jpg" data-caption="" data-size="normal" data-rawwidth="876" data-rawheight="350" class="origin_image zh-lightbox-thumb" width="876" data-original="https://pic3.zhimg.com/v2-431fef3b3209978a8cb1152ca08ea526_b.jpg" title="v2-431fef3b3209978a8cb1152ca08ea526_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-431fef3b3209978a8cb1152ca08ea526_r.jpg" data-caption="" data-size="normal" data-rawwidth="876" data-rawheight="350" class="origin_image zh-lightbox-thumb lazy" width="876" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='876'%20height='350'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-431fef3b3209978a8cb1152ca08ea526_b.jpg" title="v2-431fef3b3209978a8cb1152ca08ea526_r"></figure>
<p class="ztext-empty-paragraph"></p>
<p>這裡採用的是<a href="https://zhuanlan.zhihu.com/p/59640437" class="internal" data-wpel-link="external" rel="nofollow external noopener noreferrer">Canny邊緣檢測</a>來得到圖像邊緣圖，再通過Suzuki85算法<code>cv2.findContours</code>從圖像邊緣提取外輪廓。</p>
<h3>代碼實現</h3>
<div class="highlight">
<pre><code class="language-python"><span class="k">def</span><span class="nf">separate</span><span class="p">(</span><span class="n">img</span><span class="p">,</span><span class="n">th</span><span class="o">=</span><span class="mi">5</span><span class="p">):</span><span class="s2">"""輸入組件區域遮罩，輸出多個組件外輪廓列表"""</span><span class="n">new_img</span><span class="o">=</span><span class="n">cv2</span><span class="o">.</span><span class="n">Canny</span><span class="p">(</span><span class="n">img</span><span class="p">,</span><span class="mi">50</span><span class="p">,</span><span class="mi">150</span><span class="p">)</span><span class="n">new_img</span><span class="o">=</span><span class="n">image_morphology</span><span class="p">(</span><span class="n">new_img</span><span class="p">)</span><span class="n">cnts</span><span class="p">,</span><span class="n">_</span><span class="o">=</span><span class="n">cv2</span><span class="o">.</span><span class="n">findContours</span><span class="p">(</span><span class="n">new_img</span><span class="p">,</span><span class="n">cv2</span><span class="o">.</span><span class="n">RETR_EXTERNAL</span><span class="p">,</span><span class="n">cv2</span><span class="o">.</span><span class="n">CHAIN_APPROX_NONE</span><span class="p">)</span><span class="n">data</span><span class="o">=</span><span class="p">[]</span><span class="k">for</span><span class="n">cnt</span><span class="ow">in</span><span class="n">cnts</span><span class="p">:</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="p">,</span><span class="n">w</span><span class="p">,</span><span class="n">h</span><span class="o">=</span><span class="n">cv2</span><span class="o">.</span><span class="n">boundingRect</span><span class="p">(</span><span class="n">cnt</span><span class="p">)</span><span class="k">if</span><span class="p">(</span><span class="n">w</span><span class="o">&lt;</span><span class="n">th</span><span class="p">)</span><span class="o">|</span><span class="p">(</span><span class="n">h</span><span class="o">&lt;</span><span class="n">th</span><span class="p">):</span><span class="s2">"""剔除噪點"""</span><span class="k">continue</span><span class="n">data</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">cnt</span><span class="p">,</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="p">,</span><span class="n">w</span><span class="p">,</span><span class="n">h</span><span class="p">))</span><span class="k">return</span><span class="n">data</span></code></pre>
</div>
<p>這一步我們得到了圖像中所有組件的外輪廓以及具體的坐標<code>x,y</code>和寬高<code>w,h</code> 。</p>
<h3>2.2 形狀檢測</h3>
<p>第二步則是對每個組件外輪廓進行圖形類型識別，其中除了矩形、圓形是樣式可還原圖形，其它都不可還原，我們的目標就是檢測出這兩種基本圖形。</p>
<p class="ztext-empty-paragraph"></p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-8cd18efebdcd4102816d092a4069d573_r.jpg" data-caption="" data-size="normal" data-rawwidth="900" data-rawheight="422" class="origin_image zh-lightbox-thumb" width="900" data-original="https://pic4.zhimg.com/v2-8cd18efebdcd4102816d092a4069d573_b.jpg" title="v2-8cd18efebdcd4102816d092a4069d573_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-8cd18efebdcd4102816d092a4069d573_r.jpg" data-caption="" data-size="normal" data-rawwidth="900" data-rawheight="422" class="origin_image zh-lightbox-thumb lazy" width="900" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='900'%20height='422'&gt;&lt;/svg&gt;" data-actualsrc="https://pic4.zhimg.com/v2-8cd18efebdcd4102816d092a4069d573_b.jpg" title="v2-8cd18efebdcd4102816d092a4069d573_r"></figure>
<p class="ztext-empty-paragraph"></p>
<p>這裡運用<a href="https://zhuanlan.zhihu.com/%5Bhttps://en.wikipedia.org/wiki/Hough_transform%5D(https://en.wikipedia.org/wiki/Hough_transform)" class="internal" data-wpel-link="external" rel="nofollow external noopener noreferrer">霍夫變換(Hough Transform)</a>方法，它是一種識別幾何形狀的算法，主要採用投票機制從多個特徵點擬合圖像中線段和曲線的參數方程。</p>
<p class="ztext-empty-paragraph"></p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-4e15c0668ff9ab1d057c1fac0545a1c9_r.jpg" data-caption="" data-size="normal" data-rawwidth="800" data-rawheight="374" class="origin_image zh-lightbox-thumb" width="800" data-original="https://pic2.zhimg.com/v2-4e15c0668ff9ab1d057c1fac0545a1c9_b.jpg" title="v2-4e15c0668ff9ab1d057c1fac0545a1c9_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-4e15c0668ff9ab1d057c1fac0545a1c9_r.jpg" data-caption="" data-size="normal" data-rawwidth="800" data-rawheight="374" class="origin_image zh-lightbox-thumb lazy" width="800" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='800'%20height='374'&gt;&lt;/svg&gt;" data-actualsrc="https://pic2.zhimg.com/v2-4e15c0668ff9ab1d057c1fac0545a1c9_b.jpg" title="v2-4e15c0668ff9ab1d057c1fac0545a1c9_r"></figure>
<p class="ztext-empty-paragraph"></p>
<h3>2.2.1 矩形檢測</h3>
<p>檢測矩形主要分兩步：1）通過霍夫直線變換檢測外輪廓的邊；2）根據邊（線段）集合判斷是否符合矩形特徵。</p>
<p class="ztext-empty-paragraph"></p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-a583e4af3faa7c3314649292e8f2c986_r.jpg" data-caption="" data-size="normal" data-rawwidth="1240" data-rawheight="423" class="origin_image zh-lightbox-thumb" width="1240" data-original="https://pic3.zhimg.com/v2-a583e4af3faa7c3314649292e8f2c986_b.jpg" title="v2-a583e4af3faa7c3314649292e8f2c986_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-a583e4af3faa7c3314649292e8f2c986_r.jpg" data-caption="" data-size="normal" data-rawwidth="1240" data-rawheight="423" class="origin_image zh-lightbox-thumb lazy" width="1240" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1240'%20height='423'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-a583e4af3faa7c3314649292e8f2c986_b.jpg" title="v2-a583e4af3faa7c3314649292e8f2c986_r"></figure>
<p class="ztext-empty-paragraph"></p>
<p>OpenCV提供線段檢測方法<code>cv2.HoughLinesP</code> ，輸入外輪廓，輸出檢測到的線段，具體代碼實現如下：</p>
<div class="highlight">
<pre><code class="language-python"><span class="c1"># 检测矩形</span><span class="k">def</span><span class="nf">detectRectangle</span><span class="p">(</span><span class="n">img</span><span class="p">,</span><span class="n">width</span><span class="p">,</span><span class="n">height</span><span class="p">):</span><span class="n">minLineLength</span><span class="o">=</span><span class="mi">10</span><span class="n">maxLineGap</span><span class="o">=</span><span class="mi">4</span><span class="c1"># 霍夫直线变换输出检测到的线段数组</span><span class="n">lines</span><span class="o">=</span><span class="n">cv2</span><span class="o">.</span><span class="n">HoughLinesP</span><span class="p">(</span><span class="n">img</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="n">np</span><span class="o">.</span><span class="n">pi</span><span class="o">/</span><span class="mi">180</span><span class="p">,</span><span class="mi">100</span><span class="p">,</span><span class="n">minLineLength</span><span class="p">,</span><span class="n">maxLineGap</span><span class="p">)</span><span class="n">segments</span><span class="o">=</span><span class="n">lines</span><span class="o">.</span><span class="n">reshape</span><span class="p">(</span><span class="n">lines</span><span class="o">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span><span class="mi">4</span><span class="p">)</span><span class="c1"># 将线段数组进行进一步检测，判断是否命中矩形规则</span><span class="k">return</span><span class="n">judgeRectangle</span><span class="p">(</span><span class="n">segments</span><span class="p">,</span><span class="n">width</span><span class="p">,</span><span class="n">height</span><span class="p">)</span></code></pre>
</div>
<p>取到線段集合後，我們再判斷是否滿足矩形邊的特徵： 1. 存在兩條水平方向線段和兩條垂直方向線段1. 上線段到下線段距離≈組件高度，左線段到右線段距離≈組件寬度</p>
<h3>代碼實現</h3>
<div class="highlight">
<pre><code class="language-python"><span class="s2">"""判斷是否為矩形"""</span><span class="k">def</span><span class="nf">judgeRectangle</span><span class="p">(</span><span class="n">lines</span><span class="p">,</span><span class="n">width</span><span class="p">,</span><span class="n">height</span><span class="p">,</span><span class="n">x</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span><span class="n">y</span><span class="o">=</span><span class="mi">0</span><span class="p">):</span><span class="n">th</span><span class="o">=</span><span class="mi">2</span><span class="n">horizontal_segments</span><span class="o">=</span><span class="n">lines</span><span class="p">[</span><span class="n">np</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="nb">abs</span><span class="p">(</span><span class="n">lines</span><span class="p">[:,</span><span class="mi">1</span><span class="p">]</span><span class="o">-</span><span class="n">lines</span><span class="p">[:,</span><span class="mi">3</span><span class="p">])</span><span class="o">&lt;</span><span class="n">th</span><span class="p">)]</span><span class="n">vertical_segments</span><span class="o">=</span><span class="n">lines</span><span class="p">[</span><span class="n">np</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="nb">abs</span><span class="p">(</span><span class="n">lines</span><span class="p">[:,</span><span class="mi">0</span><span class="p">]</span><span class="o">-</span><span class="n">lines</span><span class="p">[:,</span><span class="mi">2</span><span class="p">])</span><span class="o">&lt;</span><span class="n">th</span><span class="p">)]</span><span class="n">isRect</span><span class="o">=</span><span class="bp">False</span><span class="n">h</span><span class="o">=</span><span class="n">w</span><span class="o">=</span><span class="bp">None</span><span class="k">if</span><span class="n">horizontal_segments</span><span class="o">.</span><span class="n">size</span><span class="o">!=</span><span class="mi">0</span><span class="p">:</span><span class="n">horizontal_centers</span><span class="o">=</span><span class="p">(</span><span class="n">horizontal_segments</span><span class="p">[:,</span><span class="mi">1</span><span class="p">]</span><span class="o">/</span><span class="mi">2</span><span class="o">+</span><span class="n">horizontal_segments</span><span class="p">[:,</span><span class="mi">3</span><span class="p">]</span><span class="o">/</span><span class="mi">2</span><span class="p">)</span><span class="n">top</span><span class="o">=</span><span class="n">horizontal_centers</span><span class="o">.</span><span class="n">min</span><span class="p">()</span><span class="n">bottom</span><span class="o">=</span><span class="n">horizontal_centers</span><span class="o">.</span><span class="n">max</span><span class="p">()</span><span class="n">h</span><span class="o">=</span><span class="n">bottom</span><span class="o">-</span><span class="n">top</span><span class="k">if</span><span class="nb">abs</span><span class="p">(</span><span class="n">h</span><span class="o">-</span><span class="n">height</span><span class="p">)</span><span class="o">&gt;</span><span class="n">th</span><span class="p">:</span><span class="k">return</span><span class="bp">False</span><span class="p">,</span><span class="bp">None</span><span class="p">,</span><span class="bp">None</span><span class="c1">#如果兩線間隔非圖形高度，則不規則圖片</span><span class="n">isRect</span><span class="o">=</span><span class="bp">True</span><span class="n">h</span><span class="o">=</span><span class="nb">int</span><span class="p">(</span><span class="nb">round</span><span class="p">(</span><span class="n">h</span><span class="p">))</span><span class="k">if</span><span class="n">vertical_segments</span><span class="o">.</span><span class="n">size</span><span class="o">!=</span><span class="mi">0</span><span class="p">:</span><span class="n">vertical_centers</span><span class="o">=</span><span class="n">vertical_segments</span><span class="p">[:,</span><span class="mi">0</span><span class="p">]</span><span class="o">/</span><span class="mi">2</span><span class="o">+</span><span class="n">vertical_segments</span><span class="p">[:,</span><span class="mi">2</span><span class="p">]</span><span class="o">/</span><span class="mi">2</span><span class="n">left</span><span class="o">=</span><span class="n">vertical_centers</span><span class="o">.</span><span class="n">min</span><span class="p">()</span><span class="n">right</span><span class="o">=</span><span class="n">vertical_centers</span><span class="o">.</span><span class="n">max</span><span class="p">()</span><span class="n">w</span><span class="o">=</span><span class="n">right</span><span class="o">-</span><span class="n">left</span><span class="k">if</span><span class="nb">abs</span><span class="p">(</span><span class="n">w</span><span class="o">-</span><span class="n">width</span><span class="p">)</span><span class="o">&gt;</span><span class="n">th</span><span class="p">:</span><span class="k">return</span><span class="bp">False</span><span class="p">,</span><span class="bp">None</span><span class="p">,</span><span class="bp">None</span><span class="n">isRect</span><span class="o">=</span><span class="bp">True</span><span class="n">w</span><span class="o">=</span><span class="nb">int</span><span class="p">(</span><span class="nb">round</span><span class="p">(</span><span class="n">w</span><span class="p">))</span><span class="k">return</span><span class="n">isRect</span><span class="p">,</span><span class="n">w</span><span class="p">,</span><span class="n">h</span></code></pre>
</div>
<h3>2.2.2 圓形檢測</h3>
<p>圓形檢測可使用霍夫圓環檢測法，對應OpenCV的<code>HoughCircles</code>方法，輸入二值圖，如果存在圓形，則返回圓形和半徑。代碼實現如下：</p>
<div class="highlight">
<pre><code class="language-python"><span class="c1">#檢測圓形</span><span class="k">def</span><span class="nf">detectCircle</span><span class="p">(</span><span class="n">img</span><span class="p">,</span><span class="n">width</span><span class="p">,</span><span class="n">height</span><span class="p">):</span><span class="n">circles</span><span class="o">=</span><span class="n">cv2</span><span class="o">.</span><span class="n">HoughCircles</span><span class="p">(</span><span class="n">img</span><span class="p">,</span><span class="n">cv2</span><span class="o">.</span><span class="n">HOUGH_GRADIENT</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">20</span><span class="p">,</span><span class="n">param1</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span><span class="n">param2</span><span class="o">=</span><span class="mi">15</span><span class="p">,</span><span class="n">minRadius</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span><span class="n">maxRadius</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span><span class="k">if</span><span class="n">circles</span><span class="ow">is</span><span class="bp">None</span><span class="p">:</span><span class="k">return</span><span class="bp">False</span><span class="p">[</span><span class="n">radius</span><span class="p">,</span><span class="n">rx</span><span class="p">,</span><span class="n">ry</span><span class="p">]</span><span class="o">=</span><span class="n">circles</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="k">return</span><span class="n">judgeCircle</span><span class="p">(</span><span class="n">radius</span><span class="p">,</span><span class="n">rx</span><span class="p">,</span><span class="n">ry</span><span class="p">,</span><span class="n">width</span><span class="p">,</span><span class="n">height</span><span class="p">)</span><span class="k">def</span><span class="nf">judgeCircle</span><span class="p">(</span><span class="n">r</span><span class="p">,</span><span class="n">rx</span><span class="p">,</span><span class="n">ry</span><span class="p">,</span><span class="n">w</span><span class="p">,</span><span class="n">h</span><span class="p">,</span><span class="n">x</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span><span class="n">y</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span><span class="n">th</span><span class="o">=</span><span class="mi">4</span><span class="p">):</span><span class="k">return</span><span class="p">(</span><span class="p">(</span><span class="nb">abs</span><span class="p">(</span><span class="n">w</span><span class="o">-</span><span class="n">h</span><span class="p">)</span><span class="o">&lt;</span><span class="n">th</span><span class="p">)</span><span class="o">&amp;</span><span class="p">(</span><span class="nb">abs</span><span class="p">(</span><span class="n">r</span><span class="o">-</span><span class="n">w</span><span class="o">/</span><span class="mi">2</span><span class="p">)</span><span class="o">&lt;</span><span class="n">th</span><span class="p">)</span><span class="o">&amp;</span><span class="p">(</span><span class="nb">abs</span><span class="p">(</span><span class="n">rx</span><span class="o">-</span><span class="n">x</span><span class="o">-</span><span class="n">w</span><span class="o">/</span><span class="mi">2</span><span class="p">)</span><span class="o">&lt;</span><span class="n">th</span><span class="p">)</span><span class="o">&amp;</span><span class="p">(</span><span class="nb">abs</span><span class="p">(</span><span class="n">ry</span><span class="o">-</span><span class="n">y</span><span class="o">-</span><span class="n">h</span><span class="o">/</span><span class="mi">2</span><span class="p">)</span><span class="o">&lt;</span><span class="n">th</span><span class="p">)</span><span class="p">)</span></code></pre>
</div>
<p>通過這一步，我們篩選出屬於矩形或圓形的組件，以及組件的寬高、圓形以及對應的半徑，下一步，我們將針對這兩種基本圖形進行樣式檢測。</p>
<h2>3. 組件的樣式計算</h2>
<p>組件樣式計算主要對邊框、圓角、背景三種常用樣式分別計算。</p>
<p class="ztext-empty-paragraph"></p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-bb1bc839f27142b0e2c65590a3a80fa7_r.jpg" data-caption="" data-size="normal" data-rawwidth="930" data-rawheight="372" class="origin_image zh-lightbox-thumb" width="930" data-original="https://pic4.zhimg.com/v2-bb1bc839f27142b0e2c65590a3a80fa7_b.jpg" title="v2-bb1bc839f27142b0e2c65590a3a80fa7_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-bb1bc839f27142b0e2c65590a3a80fa7_r.jpg" data-caption="" data-size="normal" data-rawwidth="930" data-rawheight="372" class="origin_image zh-lightbox-thumb lazy" width="930" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='930'%20height='372'&gt;&lt;/svg&gt;" data-actualsrc="https://pic4.zhimg.com/v2-bb1bc839f27142b0e2c65590a3a80fa7_b.jpg" title="v2-bb1bc839f27142b0e2c65590a3a80fa7_r"></figure>
<p class="ztext-empty-paragraph"></p>
<h3>3.1 圓角計算</h3>
<p>在樣式定義中，圓角被限制在矩形的四個頂點處，圓角弧度取決於它的半徑，因此圓角計算的主要目標就是識別圓角的半徑。根據圓角的4個方位，我們將組件區域劃分為4塊進行逐塊分析。一開始，我們採用直接對圓弧點進行圓的曲線擬合，但由於圓角點的數據過於集中，擬合圓的誤差很大，如圖：</p>
<p class="ztext-empty-paragraph"></p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-4177088b1d989db6a219f43f4cba41b5_r.jpg" data-caption="" data-size="normal" data-rawwidth="1240" data-rawheight="807" class="origin_image zh-lightbox-thumb" width="1240" data-original="https://pic2.zhimg.com/v2-4177088b1d989db6a219f43f4cba41b5_b.jpg" title="v2-4177088b1d989db6a219f43f4cba41b5_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-4177088b1d989db6a219f43f4cba41b5_r.jpg" data-caption="" data-size="normal" data-rawwidth="1240" data-rawheight="807" class="origin_image zh-lightbox-thumb lazy" width="1240" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1240'%20height='807'&gt;&lt;/svg&gt;" data-actualsrc="https://pic2.zhimg.com/v2-4177088b1d989db6a219f43f4cba41b5_b.jpg" title="v2-4177088b1d989db6a219f43f4cba41b5_r"></figure>
<p class="ztext-empty-paragraph"></p>
<p>我們知道，圓角經過十字對稱後能構造出一個圓形，因此，只要我們確定了“圓角”的候選區域，構造十字軸對稱圖，就可以根據圓形擬合準確判斷是否為滿足圓角特徵了。具體步驟如下：</p>
<ol>
<li>假設存在圓角，用面積推算圓角半徑，確定“候選區域”</li>
<li>構造“候選區域”水平-豎直軸對稱圖形，對圖形進行霍夫圓環檢測，驗證是否為圓角</li>
</ol>
<h3>3.1.1 圓角半徑推算</h3>
<p>我們假設存在圓角，半徑為R，如下圖黃色色塊區域，是組件框與填充組件的差集。</p>
<p class="ztext-empty-paragraph"></p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-23eaba50cb35f932b42fe81487056528_r.jpg" data-caption="" data-size="normal" data-rawwidth="1240" data-rawheight="368" class="origin_image zh-lightbox-thumb" width="1240" data-original="https://pic1.zhimg.com/v2-23eaba50cb35f932b42fe81487056528_b.jpg" title="v2-23eaba50cb35f932b42fe81487056528_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-23eaba50cb35f932b42fe81487056528_r.jpg" data-caption="" data-size="normal" data-rawwidth="1240" data-rawheight="368" class="origin_image zh-lightbox-thumb lazy" width="1240" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1240'%20height='368'&gt;&lt;/svg&gt;" data-actualsrc="https://pic1.zhimg.com/v2-23eaba50cb35f932b42fe81487056528_b.jpg" title="v2-23eaba50cb35f932b42fe81487056528_r"></figure>
<p class="ztext-empty-paragraph"></p>
<p>同時，黃色塊也是以邊長R為正方形與半徑R為1/4圓的差集，即<code>s = R² - π × R² × ¼</code> ，於是聯立方程，可求解圓角半徑R，代碼如下：</p>
<p>這一步我們根據面積差集計算出半徑R，通過R，我們裁剪出“候選區域”，進行下一步驗證。</p>
<h3>3.1.2 候選區域驗證</h3>
<p>這一步先構造軸對稱圖像，主要是在水平和豎直方向依次做翻轉+拼接操作。</p>
<p class="ztext-empty-paragraph"></p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-1b0b85a33e934c3f7b9c64b10ba0616e_r.jpg" data-caption="" data-size="normal" data-rawwidth="1038" data-rawheight="900" class="origin_image zh-lightbox-thumb" width="1038" data-original="https://pic3.zhimg.com/v2-1b0b85a33e934c3f7b9c64b10ba0616e_b.jpg" title="v2-1b0b85a33e934c3f7b9c64b10ba0616e_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-1b0b85a33e934c3f7b9c64b10ba0616e_r.jpg" data-caption="" data-size="normal" data-rawwidth="1038" data-rawheight="900" class="origin_image zh-lightbox-thumb lazy" width="1038" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1038'%20height='900'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-1b0b85a33e934c3f7b9c64b10ba0616e_b.jpg" title="v2-1b0b85a33e934c3f7b9c64b10ba0616e_r"></figure>
<p class="ztext-empty-paragraph"></p>
<p>如圖，得到對稱圖形後，我們沿用上文的霍夫圓環變換來檢測是否存在圓形，如果存在，則圓角也存在，反之亦然。</p>
<h3>代碼實現</h3>
<div class="highlight">
<pre><code class="language-python"><span class="c1">#推算可能的圓角半徑</span><span class="k">def</span><span class="nf">getCornerRadius</span><span class="p">(</span><span class="n">img</span><span class="p">):</span><span class="n">cornerRadius</span><span class="o">=</span><span class="mi">0</span><span class="n">corner_mask_size</span><span class="o">=</span><span class="n">img</span><span class="p">[</span><span class="n">img</span><span class="p">[:,</span><span class="p">:,</span><span class="mi">3</span><span class="p">]</span><span class="o">!=</span><span class="mi">1</span><span class="p">]</span><span class="o">.</span><span class="n">size</span><span class="c1">#</span><span class="k">if</span><span class="n">corner_mask_size</span><span class="o">&gt;=</span><span class="mi">0</span><span class="p">:</span><span class="n">cornerRadius</span><span class="o">=</span><span class="nb">round</span><span class="p">(</span><span class="n">math</span><span class="o">.</span><span class="n">sqrt</span><span class="p">((</span><span class="n">corner_mask_size</span><span class="o">/</span><span class="mi">3</span><span class="p">)</span><span class="o">/</span><span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">np</span><span class="o">.</span><span class="n">pi</span><span class="o">/</span><span class="mi">4</span><span class="p">)))</span><span class="k">return</span><span class="n">cornerRadius</span><span class="c1">#驗證候選區域是否為圓角，以左上圓角為例</span><span class="k">def</span><span class="nf">vertifyCorner</span><span class="p">(</span><span class="n">img</span><span class="p">,</span><span class="n">cornerRadius</span><span class="p">):</span><span class="n">cornerArea</span><span class="o">=</span><span class="n">img</span><span class="p">[:</span><span class="n">cornerRadius</span><span class="p">,</span><span class="p">:</span><span class="n">cornerRadius</span><span class="p">]</span><span class="c1">#裁剪出候選區域</span><span class="n">binary_image</span><span class="o">=</span><span class="n">np</span><span class="o">.</span><span class="n">zeros</span><span class="p">(</span><span class="n">cornerArea</span><span class="o">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">:</span><span class="mi">2</span><span class="p">],</span><span class="n">dtype</span><span class="o">=</span><span class="n">np</span><span class="o">.</span><span class="n">uint8</span><span class="p">)</span><span class="c1">#構造二值圖</span><span class="n">binary_image</span><span class="p">[</span><span class="n">cornerArea</span><span class="p">[:,:,</span><span class="mi">3</span><span class="p">]</span><span class="o">!=</span><span class="mi">0</span><span class="p">]</span><span class="o">=</span><span class="mi">255</span><span class="n">horizontal</span><span class="o">=</span><span class="n">cv2</span><span class="o">.</span><span class="n">flip</span><span class="p">(</span><span class="n">img</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="n">dst</span><span class="o">=</span><span class="bp">None</span><span class="p">)</span><span class="c1">#水平鏡像</span><span class="n">img</span><span class="o">=</span><span class="n">cv2</span><span class="o">.</span><span class="n">hconcat</span><span class="p">([</span><span class="n">img</span><span class="p">,</span><span class="n">horizontal</span><span class="p">])</span><span class="c1">#水平拼接</span><span class="n">vertical</span><span class="o">=</span><span class="n">cv2</span><span class="o">.</span><span class="n">flip</span><span class="p">(</span><span class="n">img</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="n">dst</span><span class="o">=</span><span class="bp">None</span><span class="p">)</span><span class="c1">#垂直鏡像</span><span class="n">img</span><span class="o">=</span><span class="n">cv2</span><span class="o">.</span><span class="n">vconcat</span><span class="p">([</span><span class="n">img</span><span class="p">,</span><span class="n">vertical</span><span class="p">])</span><span class="c1">#垂直拼接</span><span class="n">img</span><span class="o">=</span><span class="n">cv2</span><span class="o">.</span><span class="n">copyMakeBorder</span><span class="p">(</span><span class="n">img</span><span class="p">,</span><span class="mi">5</span><span class="p">,</span><span class="mi">5</span><span class="p">,</span><span class="mi">5</span><span class="p">,</span><span class="mi">5</span><span class="p">,</span><span class="n">cv2</span><span class="o">.</span><span class="n">BORDER_CONSTANT</span><span class="p">,</span><span class="n">value</span><span class="o">=</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span><span class="n">circles</span><span class="o">=</span><span class="n">cv2</span><span class="o">.</span><span class="n">HoughCircles</span><span class="p">(</span><span class="n">img</span><span class="p">,</span><span class="n">cv2</span><span class="o">.</span><span class="n">HOUGH_GRADIENT</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">20</span><span class="p">)</span><span class="k">if</span><span class="n">circles</span><span class="ow">is</span><span class="bp">None</span><span class="p">:</span><span class="k">return</span><span class="bp">False</span><span class="k">else</span><span class="p">:</span><span class="k">return</span><span class="bp">True</span></code></pre>
</div>
<h3>3.2 邊框計算</h3>
<p>對於邊框的計算，我們同樣是先確定邊框的描述特徵：A. 邊框內的顏色連續與相近；B. 外輪廓和內輪廓是形狀相似的。基於這個特徵，我制定了以下步驟：</p>
<ol>
<li>色塊分離：對圖像基於顏色聚類，相近色區聚類同一色塊</li>
<li>內外輪廓相似度計算：遍歷不同色塊，提取每個色塊內外輪廓，併計算其相似度</li>
</ol>
<h3>3.2.1 色塊分離</h3>
<p>邊框具有顏色相近的特徵，我們通過聚類算法對目標圖像讓顏色相近的區域歸類，這裡採用<code>k-means</code>算法聚類，聚類特徵基於圖像的<a href="https://link.zhihu.com/?target=https%3A//blog.csdn.net/u010429424/article/details/76577399" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">HSV色彩空間</a>。</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-2ef05c60b34f8466cb492166a6ab7bc0_r.jpg" data-caption="" data-size="normal" data-rawwidth="1240" data-rawheight="551" class="origin_image zh-lightbox-thumb" width="1240" data-original="https://pic1.zhimg.com/v2-2ef05c60b34f8466cb492166a6ab7bc0_b.jpg" title="v2-2ef05c60b34f8466cb492166a6ab7bc0_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-2ef05c60b34f8466cb492166a6ab7bc0_r.jpg" data-caption="" data-size="normal" data-rawwidth="1240" data-rawheight="551" class="origin_image zh-lightbox-thumb lazy" width="1240" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1240'%20height='551'&gt;&lt;/svg&gt;" data-actualsrc="https://pic1.zhimg.com/v2-2ef05c60b34f8466cb492166a6ab7bc0_b.jpg" title="v2-2ef05c60b34f8466cb492166a6ab7bc0_r"></figure>
<p class="ztext-empty-paragraph"></p>
<h3>代碼實現</h3>
<div class="highlight">
<pre><code class="language-python"><span class="s2">"""k-means聚類"""</span><span class="k">def</span><span class="nf">image_kmeansSegement</span><span class="p">(</span><span class="n">img</span><span class="p">,</span><span class="n">k</span><span class="o">=</span><span class="mi">6</span><span class="p">):</span><span class="c1">#將圖片從RGB空間轉為HSV</span><span class="n">img</span><span class="o">=</span><span class="n">cv2</span><span class="o">.</span><span class="n">cvtColor</span><span class="p">(</span><span class="n">img</span><span class="p">,</span><span class="n">cv2</span><span class="o">.</span><span class="n">COLOR_BGR2HSV</span><span class="p">)</span><span class="n">data</span><span class="o">=</span><span class="n">img</span><span class="o">.</span><span class="n">reshape</span><span class="p">((</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="mi">3</span><span class="p">))</span><span class="n">data</span><span class="o">=</span><span class="n">np</span><span class="o">.</span><span class="n">float32</span><span class="p">(</span><span class="n">data</span><span class="p">)</span><span class="c1"># MAX_ITER最大迭代次數，EPS最高精度</span><span class="n">criteria</span><span class="o">=</span><span class="p">(</span><span class="n">cv2</span><span class="o">.</span><span class="n">TERM_CRITERIA_EPS</span><span class="o">+</span><span class="n">cv2</span><span class="o">.</span><span class="n">TERM_CRITERIA_MAX_ITER</span><span class="p">,</span><span class="mi">10</span><span class="p">,</span><span class="mf">1.0</span><span class="p">)</span><span class="n">num_clusters</span><span class="o">=</span><span class="n">k</span><span class="n">ret</span><span class="p">,</span><span class="n">label</span><span class="p">,</span><span class="n">center</span><span class="o">=</span><span class="n">cv2</span><span class="o">.</span><span class="n">kmeans</span><span class="p">(</span><span class="n">data</span><span class="p">,</span><span class="n">num_clusters</span><span class="p">,</span><span class="bp">None</span><span class="p">,</span><span class="n">criteria</span><span class="p">,</span><span class="n">num_clusters</span><span class="p">,</span><span class="n">cv2</span><span class="o">.</span><span class="n">KMEANS_RANDOM_CENTERS</span><span class="p">)</span><span class="n">center</span><span class="o">=</span><span class="n">cv2</span><span class="o">.</span><span class="n">cvtColor</span><span class="p">(</span><span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="n">center</span><span class="p">],</span><span class="n">dtype</span><span class="o">=</span><span class="n">np</span><span class="o">.</span><span class="n">uint8</span><span class="p">),</span><span class="n">cv2</span><span class="o">.</span><span class="n">COLOR_HSV2BGR</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span><span class="n">labels</span><span class="o">=</span><span class="n">label</span><span class="o">.</span><span class="n">flatten</span><span class="p">()</span><span class="k">return</span><span class="n">labels</span><span class="p">,</span><span class="n">center</span></code></pre>
</div>
<h3>3.2.2 內外輪廓相似度計算</h3>
<p>這一步是遍歷k個候選色塊，對色塊分別進行外輪廓和內輪廓提取，再判斷色塊內外輪廓是否形狀相似。其中外輪廓的提取直接復用前面的<code>cv2.findContours</code>方法，輸入色塊，輸出外輪廓填充圖。內輪廓則需要分兩步，首先對外輪廓填充圖與色塊填充圖進行差運算得到“內域”，再對內域進行<code>cv2.findContours</code> 。</p>
<p class="ztext-empty-paragraph"></p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-1b5ef996ae3bd7d60af39a8a02379af0_r.jpg" data-caption="" data-size="normal" data-rawwidth="1240" data-rawheight="679" class="origin_image zh-lightbox-thumb" width="1240" data-original="https://pic1.zhimg.com/v2-1b5ef996ae3bd7d60af39a8a02379af0_b.jpg" title="v2-1b5ef996ae3bd7d60af39a8a02379af0_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-1b5ef996ae3bd7d60af39a8a02379af0_r.jpg" data-caption="" data-size="normal" data-rawwidth="1240" data-rawheight="679" class="origin_image zh-lightbox-thumb lazy" width="1240" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1240'%20height='679'&gt;&lt;/svg&gt;" data-actualsrc="https://pic1.zhimg.com/v2-1b5ef996ae3bd7d60af39a8a02379af0_b.jpg" title="v2-1b5ef996ae3bd7d60af39a8a02379af0_r"></figure>
<p class="ztext-empty-paragraph"></p>
<p>拿到內外輪廓後，我使用<a href="https://link.zhihu.com/?target=https%3A//blog.csdn.net/mago2015/article/details/81137089" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">感知哈希pHash</a> +漢明距離進行相似度計算，它主要通過顏色低採樣將圖片統一縮小到32×32尺寸並輸出圖像簽名，很好地解決相似形狀中大小不一致帶來的誤差。</p>
<h3>代碼實現</h3>
<div class="highlight">
<pre><code class="language-python"><span class="s2">"""驗證每個色塊是否存在邊框特徵B"""</span><span class="k">def</span><span class="nf">borderExtract</span><span class="p">(</span><span class="n">labels</span><span class="p">,</span><span class="n">center</span><span class="p">,</span><span class="n">img_filled</span><span class="p">):</span><span class="c1">#遍歷k-means分離的k個色塊</span><span class="k">for</span><span class="n">i</span><span class="ow">in</span><span class="nb">range</span><span class="p">(</span><span class="n">labels</span><span class="o">.</span><span class="n">max</span><span class="p">()):</span><span class="n">area</span><span class="o">=</span><span class="n">np</span><span class="o">.</span><span class="n">zeros</span><span class="p">((</span><span class="n">labels</span><span class="o">.</span><span class="n">size</span><span class="p">),</span><span class="n">dtype</span><span class="o">=</span><span class="n">np</span><span class="o">.</span><span class="n">uint8</span><span class="p">)</span><span class="n">area</span><span class="p">[</span><span class="n">labels</span><span class="o">==</span><span class="n">i</span><span class="p">]</span><span class="o">=</span><span class="mi">255</span><span class="n">area</span><span class="o">=</span><span class="n">area</span><span class="o">.</span><span class="n">reshape</span><span class="p">(</span><span class="n">img_filled</span><span class="o">.</span><span class="n">shape</span><span class="p">)</span><span class="c1">#獲取當前色塊外輪廓，用白色填充</span><span class="n">outter_filled</span><span class="p">,</span><span class="o">*</span><span class="n">_</span><span class="o">=</span><span class="n">image_contours</span><span class="p">(</span><span class="n">area</span><span class="p">)</span><span class="c1">#獲取當前色塊內輪廓，用白色填充</span><span class="n">result</span><span class="o">=</span><span class="n">outter_filled</span><span class="o">-</span><span class="n">area</span><span class="n">result</span><span class="p">[</span><span class="n">result</span><span class="o">&lt;</span><span class="mi">0</span><span class="p">]</span><span class="o">=</span><span class="mi">0</span><span class="n">inner_filled</span><span class="p">,</span><span class="o">*</span><span class="n">_</span><span class="o">=</span><span class="n">image_contours</span><span class="p">(</span><span class="n">result</span><span class="p">)</span><span class="c1">#判斷外輪廓和內輪廓是否相似</span><span class="k">if</span><span class="n">isSimilar</span><span class="p">(</span><span class="n">outter_filled</span><span class="p">,</span><span class="n">inner_filled</span><span class="p">)</span><span class="o">&amp;</span><span class="n">isSimilar</span><span class="p">(</span><span class="n">img_filled</span><span class="p">,</span><span class="n">filled1</span><span class="p">):</span><span class="n">s1</span><span class="o">=</span><span class="n">np</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="n">filled1</span><span class="o">&gt;</span><span class="mi">0</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">size</span><span class="n">s2</span><span class="o">=</span><span class="n">np</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="n">filled2</span><span class="o">&gt;</span><span class="mi">0</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">size</span><span class="n">scale</span><span class="o">=</span><span class="p">(</span><span class="mf">1.0</span><span class="o">-</span><span class="n">math</span><span class="o">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">s2</span><span class="o">/</span><span class="n">s1</span><span class="p">))</span><span class="o">*</span><span class="mf">0.5</span><span class="n">_drawBorder</span><span class="p">(</span><span class="n">filled1</span><span class="o">-</span><span class="n">filled2</span><span class="p">,</span><span class="n">center</span><span class="p">[</span><span class="n">i</span><span class="p">])</span><span class="k">return</span><span class="n">scale</span><span class="p">,</span><span class="n">center</span><span class="p">[</span><span class="n">i</span><span class="p">],</span><span class="n">filled2</span><span class="k">return</span><span class="bp">None</span><span class="s2">"""使用pHash算法計算輪廓之間相似度"""</span><span class="k">def</span><span class="nf">isSimilar</span><span class="p">(</span><span class="n">img1</span><span class="p">,</span><span class="n">img2</span><span class="p">,</span><span class="n">th</span><span class="o">=</span><span class="mf">0.8</span><span class="p">):</span><span class="n">HASH1</span><span class="o">=</span><span class="n">PHash</span><span class="o">.</span><span class="n">pHash</span><span class="p">(</span><span class="n">img1</span><span class="p">)</span><span class="n">HASH2</span><span class="o">=</span><span class="n">PHash</span><span class="o">.</span><span class="n">pHash</span><span class="p">(</span><span class="n">img2</span><span class="p">)</span><span class="n">distance</span><span class="p">,</span><span class="n">score</span><span class="o">=</span><span class="n">PHash</span><span class="o">.</span><span class="n">hammingDist</span><span class="p">(</span><span class="n">HASH1</span><span class="p">,</span><span class="n">HASH2</span><span class="p">)</span><span class="k">print</span><span class="p">(</span><span class="n">score</span><span class="p">)</span><span class="k">return</span><span class="n">score</span><span class="o">&gt;</span><span class="n">th</span></code></pre>
</div>
<h2>總結</h2>
<p>本文通過OpenCV系列算法分別實現簡單組件區域的分離和样式的檢測，對於組件的區域檢測，目前是通過手工框選的手段確定組件區域，如果要完全自動化實現Pixels to Code，還需要藉助深度卷積網絡進行組件檢測與識別。</p>
<p>本人將於9月5號參與騰訊live開發者大會，屆時將介紹更多前端智能化實踐內容，歡迎有興趣童鞋前來觀摩</p>
<p>更多文章歡迎關注</p>
<h2>相關資料</h2>
<p>最全綜述|圖像分割算法： <a href="https://zhuanlan.zhihu.com/p/70758906" class="internal" data-wpel-link="external" rel="nofollow external noopener noreferrer"><span class="invisible">https://</span> <span class="visible">zhuanlan.zhihu.com/p/70</span> <span class="invisible">758906</span></a></p>
<p>pHash圖像相似度比較算法彙總： <a href="https://link.zhihu.com/?target=https%3A//blog.csdn.net/mago2015/article/details/81137089" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">blog.csdn.net/mago2015/</span> <span class="invisible">article/details/81137089</span></a></p>
<p>機器學習算法實踐——K-Means算法與圖像分割： <a href="https://link.zhihu.com/?target=https%3A//blog.csdn.net/google19890102/article/details/52911835" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">blog.csdn.net/google198</span> <span class="invisible">90102/article/details/52911835</span></a></p>
<p>霍夫變換： <a href="https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Hough_transform" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">en.wikipedia.org/wiki/H</span> <span class="invisible">ough_transform</span></a></p>
<p>Suzuki85輪廓跟踪算法： <a href="https://link.zhihu.com/?target=https%3A//blog.csdn.net/yiqiudream/article/details/76864722" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">blog.csdn.net/yiqiudrea</span> <span class="invisible">m/article/details/76864722</span></a></p>
</div>
</div>
</article>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15220/topic-207308196/" data-wpel-link="internal">前端+ AI ——從圖片識別UI樣式</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>一個簡易的預渲染自動骨架屏方案</title>
		<link>https://hypergrowths.com/software-engineering/front-end-dev/15216/topic-166009071/</link>
		
		<dc:creator><![CDATA[marketer]]></dc:creator>
		<pubDate>Sat, 30 Jan 2021 18:34:22 +0000</pubDate>
				<category><![CDATA[前端開發]]></category>
		<category><![CDATA[前端外刊評論]]></category>
		<guid isPermaLink="false">https://hypergrowths.com/software-engineering/front-end-dev/15216/topic-166009071/</guid>

					<description><![CDATA[<p>前言我們都知道，目前傳統的SPA 網頁在完成腳本加載後，通常還需要進行接口請求，拿到遠端數據後才能進行完整地內容呈現而在接口請求的過程中，為了過渡無數據的空白場景，並提示用戶“數據請求中”，常用的方法…</p>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15216/topic-166009071/" data-wpel-link="internal">一個簡易的預渲染自動骨架屏方案</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></description>
										<content:encoded><![CDATA[<article class="Post-Main Post-NormalMain" tabindex="-1">
<header class="Post-Header">
<h1 class="Post-Title">一個簡易的預渲染自動骨架屏方案</h1>
<div class="Post-Author">
<div class="AuthorInfo" itemprop="author" itemscope="" itemtype="http://schema.org/Person"><meta itemprop="name" content="CJY0208"><meta itemprop="image" content="https://pic4.zhimg.com/v2-b0ca68f5f877f0113974bddfa6adc1f5_l.jpg?source=172ae18b"><meta itemprop="url" content="https://www.zhihu.com/people/cjy0208"><meta itemprop="zhihu:followerCount"></div>
</div>
</header>
<div class="Post-RichTextContainer">
<div class="RichText ztext Post-RichText">
<h2>前言</h2>
<p>我們都知道，目前傳統的SPA 網頁在完成腳本加載後，通常還需要進行接口請求，拿到遠端數據後才能進行完整地內容呈現</p>
<p>而在接口請求的過程中，為了過渡無數據的空白場景，並提示用戶“數據請求中”，常用的方法為做一個loading 動畫效果</p>
<figure data-size="normal"><noscript><img decoding="async" src="" data-caption="" data-size="normal" data-rawwidth="234" data-rawheight="167" data-thumbnail="https://pic1.zhimg.com/v2-f476cacc91c9415aeea9eb1080cda17c_b.jpg" class="content_image" width="234" data-original="https://pic1.zhimg.com/v2-f476cacc91c9415aeea9eb1080cda17c_b.gif"></noscript><img decoding="async" src="" data-caption="" data-size="normal" data-rawwidth="234" data-rawheight="167" data-thumbnail="https://pic1.zhimg.com/v2-f476cacc91c9415aeea9eb1080cda17c_b.jpg" class="content_image lazy" width="234" data-actualsrc="https://pic1.zhimg.com/v2-f476cacc91c9415aeea9eb1080cda17c_b.gif" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='234'%20height='167'&gt;&lt;/svg&gt;"></figure>
<p>而在用戶胃口越來越刁的今天，一個簡單的loading 效果已經不太能安撫用戶了，而骨架屏就是一種安撫用戶的進階方案</p>
<h3>最終成品鏈接（懶人用）： <a href="https://link.zhihu.com/?target=https%3A//github.com/CJY0208/auto-skeleton-plugin" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">auto-skeleton-plugin</a></h3>
<h2>什麼是骨架屏？</h2>
<p>簡單來說，骨架屏就是在還未產生可閱讀內容時，先將網頁的大致結構框架呈現給用戶，以達到安撫用戶等待過程中的不耐煩心理、提升用戶存留的效果</p>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-b0d00126466730f1516a497af46d85c6_r.jpg" data-caption="" data-size="normal" data-rawwidth="708" data-rawheight="848" data-thumbnail="https://pic3.zhimg.com/v2-b0d00126466730f1516a497af46d85c6_b.jpg" class="origin_image zh-lightbox-thumb" width="708" data-original="https://pic3.zhimg.com/v2-b0d00126466730f1516a497af46d85c6_b.gif" title="v2-b0d00126466730f1516a497af46d85c6_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-b0d00126466730f1516a497af46d85c6_r.jpg" data-caption="" data-size="normal" data-rawwidth="708" data-rawheight="848" data-thumbnail="https://pic3.zhimg.com/v2-b0d00126466730f1516a497af46d85c6_b.jpg" class="origin_image zh-lightbox-thumb lazy" width="708" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='708'%20height='848'&gt;&lt;/svg&gt;" data-actualsrc="https://pic3.zhimg.com/v2-b0d00126466730f1516a497af46d85c6_b.gif" title="v2-b0d00126466730f1516a497af46d85c6_r"></figure>
<p>骨架屏的實現，通常有兩種方式</p>
<ol>
<li>手動書寫骨架</li>
<li>自動生成骨架</li>
</ol>
<p>手動寫骨架的方式，好處是可以做出高定制性的骨架效果，缺點是開發成本大，效率低，但本文不對此方式進行展開</p>
<p>那麼如何實現自動骨架屏的效果呢？一個簡單的方式是：將已有內容的樣式進行調整，生成對應的骨架效果，例如以下代碼，可以將所有<b>文字內容</b>，變成骨架條塊</p>
<div class="highlight">
<pre><code class="language-js"><span class="kd">function</span><span class="nx">generateSkeleton</span><span class="p">()</span><span class="p">{</span><span class="c1">//文字節點</span><span class="p">;[...</span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">'*'</span><span class="p">)]</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="p">(</span><span class="nx">node</span><span class="p">)</span><span class="p">=&gt;</span><span class="o">!</span><span class="p">[</span><span class="s1">'script'</span><span class="p">,</span><span class="s1">'style'</span><span class="p">,</span><span class="s1">'html'</span><span class="p">,</span><span class="s1">'body'</span><span class="p">,</span><span class="s1">'head'</span><span class="p">,</span><span class="s1">'title'</span><span class="p">].</span><span class="nx">includes</span><span class="p">(</span><span class="nx">node</span><span class="p">.</span><span class="nx">tagName</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">()</span><span class="p">)</span><span class="p">)</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">node</span><span class="p">)</span><span class="p">=&gt;</span><span class="p">[...</span><span class="nx">node</span><span class="p">.</span><span class="nx">childNodes</span><span class="p">].</span><span class="nx">filter</span><span class="p">((</span><span class="nx">node</span><span class="p">)</span><span class="p">=&gt;</span><span class="nx">node</span><span class="k">instanceof</span><span class="nx">Text</span><span class="p">))</span><span class="p">.</span><span class="nx">flat</span><span class="p">(</span><span class="kc">Infinity</span><span class="p">)</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">node</span><span class="p">)</span><span class="p">=&gt;</span><span class="p">{</span><span class="kd">let</span><span class="nx">span</span><span class="o">=</span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">'span'</span><span class="p">)</span><span class="nx">node</span><span class="p">.</span><span class="nx">parentNode</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">span</span><span class="p">,</span><span class="nx">node</span><span class="p">)</span><span class="nx">span</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">node</span><span class="p">)</span><span class="nx">span</span><span class="p">.</span><span class="nx">style</span><span class="o">=</span><span class="sb">`</span><span class="sb">background: #f2f2f2;</span><span class="sb">color: transparent !important;</span><span class="sb">`</span><span class="p">})</span><span class="p">}</span></code></pre>
</div>
<figure data-size="normal"><noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-4baef38f4779f0b0ee4ab302f1194127_r.jpg" data-caption="" data-size="normal" data-rawwidth="1609" data-rawheight="632" data-thumbnail="https://pic4.zhimg.com/v2-4baef38f4779f0b0ee4ab302f1194127_b.jpg" class="origin_image zh-lightbox-thumb" width="1609" data-original="https://pic4.zhimg.com/v2-4baef38f4779f0b0ee4ab302f1194127_b.gif" title="v2-4baef38f4779f0b0ee4ab302f1194127_r"></noscript><img decoding="async" src="https://hypergrowths.com/wp-content/uploads/v2-4baef38f4779f0b0ee4ab302f1194127_r.jpg" data-caption="" data-size="normal" data-rawwidth="1609" data-rawheight="632" data-thumbnail="https://pic4.zhimg.com/v2-4baef38f4779f0b0ee4ab302f1194127_b.jpg" class="origin_image zh-lightbox-thumb lazy" width="1609" data-original="data:image/svg+xml;utf8,&lt;svg%20xmlns='http://www.w3.org/2000/svg'%20width='1609'%20height='632'&gt;&lt;/svg&gt;" data-actualsrc="https://pic4.zhimg.com/v2-4baef38f4779f0b0ee4ab302f1194127_b.gif" title="v2-4baef38f4779f0b0ee4ab302f1194127_r"></figure>
<p>這樣，只要我們<b>完善不同內容如圖片、圖標等元素的骨架化過程，就可以得到一個相對可用的內容骨架化效果</b>了</p>
<p>自動骨架化的好處是，生成骨架的效率高，開發成本很低，但缺點是定制性相對較差，需要根據已有內容來確定骨架效果</p>
<p>但這有一個問題，我們期望是在應用剛打開時，還未請求數據前就呈現骨架，目前顯然是做不到的</p>
<p>而我們可以藉助“預渲染”來實現期望的效果</p>
<h2>什麼是預渲染？</h2>
<p>預渲染類似服務端渲染，它的過程大概是這樣的：<b>在應用完成打包後，立刻啟動一個headless瀏覽器進行頁面訪問，再將訪問的結果輸出成html文件的渲染過程</b></p>
<p>通俗地說就是：打包完後本地先訪問看一看，看到啥就“截個屏”存起來，然後輸出一個html 文件，覆蓋原本構建生成的index.html</p>
<p>這樣，用戶訪問打包好的index.html 時，看到的就是一個有內容的網頁</p>
<p>那麼，借助預渲染，我們可以將上述自動骨架屏的過程，放在headless瀏覽器加載出網頁內容後，具備內容後再將內容骨架化，再輸出成html，就可以實現<b>用戶訪問時，還未請求數據前，先呈現骨架</b>的效果</p>
<h2>自動骨架屏的過程實現</h2>
<p>我們可以參考一個常用的預渲染的webpack插件<a href="https://link.zhihu.com/?target=https%3A//github.com/chrisvfritz/prerender-spa-plugin" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">prerender-spa-plugin</a>來實現這個過程</p>
<p>查閱源碼可知，這個插件並未實現核心渲染過程，其實只是將<a href="https://link.zhihu.com/?target=https%3A//github.com/JoshTheDerf/prerenderer" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">prerenderer</a>包裝成了webpack插件的形式，並承擔了將最終結果輸出成html產物文件的功能</p>
<p>關鍵源碼： <a href="https://link.zhihu.com/?target=https%3A//github.com/chrisvfritz/prerender-spa-plugin/blob/master/es6/index.js%23L65-L70" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">github.com/chrisvfritz/</span> <span class="invisible">prerender-spa-plugin/blob/master/es6/index.js#L65-L70</span></a></p>
<div class="highlight">
<pre><code class="language-js"><span class="p">...</span><span class="kr">const</span><span class="nx">Prerenderer</span><span class="o">=</span><span class="nx">require</span><span class="p">(</span><span class="s1">'@prerenderer/prerenderer'</span><span class="p">)</span><span class="p">...</span><span class="kd">function</span><span class="nx">PrerenderSPAPlugin</span><span class="p">(...</span><span class="nx">args</span><span class="p">)</span><span class="p">{</span><span class="p">...</span><span class="kr">const</span><span class="nx">afterEmit</span><span class="o">=</span><span class="p">(</span><span class="nx">compilation</span><span class="p">,</span><span class="nx">done</span><span class="p">)</span><span class="p">=&gt;</span><span class="p">{</span><span class="kr">const</span><span class="nx">PrerendererInstance</span><span class="o">=</span><span class="k">new</span><span class="nx">Prerenderer</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">_options</span><span class="p">)</span><span class="nx">PrerendererInstance</span><span class="p">.</span><span class="nx">initialize</span><span class="p">()</span><span class="p">.</span><span class="nx">then</span><span class="p">(()</span><span class="p">=&gt;</span><span class="p">{</span><span class="k">return</span><span class="nx">PrerendererInstance</span><span class="p">.</span><span class="nx">renderRoutes</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">_options</span><span class="p">.</span><span class="nx">routes</span><span class="o">||</span><span class="p">[])</span><span class="p">})</span><span class="p">...</span><span class="p">}</span><span class="p">...</span><span class="p">}</span><span class="p">...</span><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span><span class="o">=</span><span class="nx">PrerenderSPAPlugin</span></code></pre>
</div>
<p><a href="https://link.zhihu.com/?target=https%3A//github.com/JoshTheDerf/prerenderer" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">prerenderer</a>承擔的則是使用headless瀏覽器訪問網頁，並輸出訪問結果的功能，其官方內置了兩種可選的headless瀏覽器： <a href="https://link.zhihu.com/?target=https%3A//github.com/puppeteer/puppeteer" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">puppeteer</a>和<a href="https://link.zhihu.com/?target=https%3A//github.com/jsdom/jsdom" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">jsdom</a></p>
<p>由於<a href="https://link.zhihu.com/?target=https%3A//github.com/puppeteer/puppeteer" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">puppeteer</a>需要下載的內容較大，我們考慮使用較輕量的<a href="https://link.zhihu.com/?target=https%3A//github.com/jsdom/jsdom" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">jsdom</a>來完成這個效果</p>
<p>在翻閱了部分<a href="https://link.zhihu.com/?target=https%3A//github.com/JoshTheDerf/prerenderer/blob/master/renderers/renderer-jsdom/es6/renderer.js" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">renderer-jsdom</a>的源碼後，可以找到headless瀏覽器採集網頁內容的部分</p>
<p>關鍵源碼： <a href="https://link.zhihu.com/?target=https%3A//github.com/JoshTheDerf/prerenderer/blob/master/renderers/renderer-jsdom/es6/renderer.js%23L25-L38" class=" external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><span class="invisible">https://</span> <span class="visible">github.com/JoshTheDerf/</span> <span class="invisible">prerenderer/blob/master/renderers/renderer-jsdom/es6/renderer.js#L25-L38</span></a></p>
<p><b>我們只需要在採集網頁內容前，對內容進行骨架化，就可以得到期望的效果</b></p>
<div class="highlight">
<pre><code class="language-js"><span class="kr">const</span><span class="nx">JSDOM</span><span class="o">=</span><span class="nx">require</span><span class="p">(</span><span class="s1">'jsdom/lib/old-api.js'</span><span class="p">).</span><span class="nx">jsdom</span><span class="p">...</span><span class="kr">const</span><span class="nx">getPageContents</span><span class="o">=</span><span class="kd">function</span><span class="p">(</span><span class="nb">window</span><span class="p">,</span><span class="nx">options</span><span class="p">,</span><span class="nx">originalRoute</span><span class="p">)</span><span class="p">{</span><span class="p">...</span><span class="k">return</span><span class="k">new</span><span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span><span class="nx">reject</span><span class="p">)</span><span class="p">=&gt;</span><span class="p">{</span><span class="p">...</span><span class="kd">function</span><span class="nx">captureDocument</span><span class="p">()</span><span class="p">{</span><span class="c1">//此處可在輸出html結果前，先對網頁內容進行骨架化</span><span class="c1">// generateSkeleton就是上邊咱們整理出來的dom操作實現自動骨架化過程</span><span class="nx">generateSkeleton</span><span class="p">(</span><span class="nb">window</span><span class="p">)</span><span class="kr">const</span><span class="nx">result</span><span class="o">=</span><span class="p">{</span><span class="p">...</span><span class="nx">html</span><span class="o">:</span><span class="nx">serializeDocument</span><span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nb">document</span><span class="p">)</span><span class="p">}</span><span class="p">...</span><span class="k">return</span><span class="nx">result</span><span class="p">}</span><span class="p">...</span><span class="p">}</span><span class="p">...</span><span class="p">}</span><span class="kr">class</span><span class="nx">JSDOMRenderer</span><span class="p">{</span><span class="p">...</span><span class="nx">async</span><span class="nx">renderRoutes</span><span class="p">(</span><span class="nx">routes</span><span class="p">,</span><span class="nx">Prerenderer</span><span class="p">)</span><span class="p">{</span><span class="p">...</span><span class="kr">const</span><span class="nx">results</span><span class="o">=</span><span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">(</span><span class="nx">routes</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">route</span><span class="p">=&gt;</span><span class="nx">limiter</span><span class="p">(()</span><span class="p">=&gt;</span><span class="p">{</span><span class="k">return</span><span class="k">new</span><span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span><span class="nx">reject</span><span class="p">)</span><span class="p">=&gt;</span><span class="p">{</span><span class="nx">JSDOM</span><span class="p">.</span><span class="nx">env</span><span class="p">({</span><span class="nx">url</span><span class="o">:</span><span class="sb">`http://127.0.0.1:</span><span class="si">${</span><span class="nx">rootOptions</span><span class="p">.</span><span class="nx">server</span><span class="p">.</span><span class="nx">port</span><span class="si">}${</span><span class="nx">route</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span><span class="p">...</span><span class="p">})</span><span class="p">})</span><span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nb">window</span><span class="p">=&gt;</span><span class="p">{</span><span class="k">return</span><span class="nx">getPageContents</span><span class="p">(</span><span class="nb">window</span><span class="p">,</span><span class="k">this</span><span class="p">.</span><span class="nx">_rendererOptions</span><span class="p">,</span><span class="nx">route</span><span class="p">)</span><span class="p">})</span><span class="p">})))</span><span class="p">...</span><span class="k">return</span><span class="nx">results</span><span class="p">}</span><span class="p">...</span><span class="p">}</span><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span><span class="o">=</span><span class="nx">JSDOMRenderer</span></code></pre>
</div>
<p>至此，簡易自動骨架屏效果的方案已經敘述完成，整個過程，需要我們自己動手的主要是骨架化過程的部分，其餘之處，都可通過參考已有過程實現來完成，那麼具體過程實現，此處就不再繼續展開了，動手能力強的小伙伴，大概可以自己一把梭出來</p>
<h2>結尾</h2>
<p>預渲染方案待展開的功能還是有不少的，例如</p>
<ol>
<li>如何內聯樣式？ （這條比較容易做到，借助jsdom 自身的resourceLoader 足矣）</li>
<li>如何保留關鍵樣式，去除無用樣式？ （有一定難度，可參考<a href="https://link.zhihu.com/?target=https%3A//github.com/uncss/uncss/blob/0.17.3/src/lib.js" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">uncss</a> ，配合postcss實現）</li>
<li>預渲染性能是否充足，能否用來做SSR? （jsdom渲染速度較快，此處進行了實踐<a href="https://link.zhihu.com/?target=https%3A//github.com/CJY0208/santi" class=" wrap external" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external">santi</a> ）</li>
</ol>
<p>以下是上述方案的自動骨架插件實現，目前自動骨架化的過程比較簡陋，只具備了基礎的可用性，也希望能得到大家的幫助，共同完善自動骨架化的過程</p>
</div>
</div>
</article>
<p>The post <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com/software-engineering/front-end-dev/15216/topic-166009071/" data-wpel-link="internal">一個簡易的預渲染自動骨架屏方案</a> appeared first on <a rel="nofollow noopener noreferrer" href="https://hypergrowths.com" data-wpel-link="internal">成長駭客交流第一站 - HyperGrowths™</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
