Minibase
2023-01-05T17:49:08.513Z
https://blog.colafornia.me/
Colafornia
Hexo
2022 年度总结
https://blog.colafornia.me/post/2023/2022-review/
2023-01-05T15:00:00.000Z
2023-01-05T17:49:08.513Z
<p><img src="https://s1.ax1x.com/2023/01/05/pSA8JH0.jpg" alt="last sunset of 2022"></p>
<p>2022: <em>ocean of stars</em></p>
<a id="more"></a>
<h3 id="工作与学习"><a href="#工作与学习" class="headerlink" title="工作与学习"></a>工作与学习</h3><p>「三年字节,两年半性能优化」</p>
<p>从元旦开始就马不停蹄地忙,每周都在加班,直到夏天结束。回想起来都不知道是怎么熬过的这大半年,但过程里就像打了鸡血一样。做性能优化时有 code change something 的真切感受,提供更好的用户体验,看到网络基建条件不太好的国家的访问数据也在蹭蹭加速很有成就感。</p>
<p>在技术上,围绕性能优化的主线任务,可视化搭建的支线任务,还是折腾研究了很多东西,但是分享性质的输出很少。秋天没那么忙了之后,突然对图和算法感兴趣,还快乐地做了一波算法题,确实揣摩到了一些在算法题中找到快乐的感觉。</p>
<p>今年也开始关注偏软技能的学习,怎么做技术规划,沟通、怎么做 one one 等等。这类技能的学习很难有可以量化的成长正反馈,还是要多观察多思考,好在字节内部文档库有很多相关的文档可以观摩。英语学习还是完全搁置了,今年对接的外国同事也比较少,在国际化业务里荒废了英语真的是离谱啊。</p>
<h3 id="生活与玩乐"><a href="#生活与玩乐" class="headerlink" title="生活与玩乐"></a>生活与玩乐</h3><p><img src="https://s1.ax1x.com/2023/01/05/pSA8NNT.jpg"></p>
<p>去年随心飞出去玩的时候还以为那只是一个开始,结果今年就一步都没有踏出北京。北京的公园倒是逛了不少,2022 年的最后一天去景山看了日落,拍下了题图封面的这张照片,2022 年的最后一次日落。</p>
<p>上半年我们工区从海淀黄庄搬到了太阳宫,我也搬家搬到了新工区附近,住在朝阳区感觉生活丰富了不少,和朋友们约饭的选择也更多了。现在住的小区是一个很神奇的地方,绿化很不错,每天外面都是鸟语花香的,刚搬过来的那段时间我家猫听鸟叫激动的都没怎么睡好觉。</p>
<h4 id="主机游戏"><a href="#主机游戏" class="headerlink" title="主机游戏"></a>主机游戏</h4><p><img src="/images/2022-game.jpeg"></p>
<p>囿于疫情,周末更多是选择在家里打游戏了。</p>
<p>Switch 平台上全收集通关了去年开始打的《灵魂渡者》,第九艺术的生命课,截图就截了两百多张,通关之后没事儿还打开相册翻截图感受一下。《NS Sports》也第一时间入手了,变成了最受欢迎的聚会游戏,经常打网球打得手臂酸痛。</p>
<p>年底蹲 Jump 上的打折和种草信息,又买了很多电子版的游戏,圣诞节开始玩《星露谷物语》,非常沉迷,预计会是明年总结的年度游戏最佳。</p>
<p>PS4 打通了《双人成行》,大镖客和 GTA5 都是断断续续地捡起来推一下剧情,比较难有心气儿和时间来玩大作。原本年底还计划买个 Xbox 实现主机御三家集齐,最后冷静了还是让 PS4 好好发挥余热,想玩的那些游戏按我的速度够玩三四年了。</p>
<p>写到这里回想起来,中间某次团队活动,我还兴致勃勃给大家分享了主机游戏购买与回血心得,感觉我还是比自己以为的要沉迷游戏啊 🤦🏻</p>
<h4 id="观影与阅读"><a href="#观影与阅读" class="headerlink" title="观影与阅读"></a>观影与阅读</h4><p>看了 76 个电影/剧,28 本书,3 部舞台剧,今年的书影音活动依然非常丰富,也浅浅参加了这一年的北影节,感谢小赵同学。</p>
<p>舞台剧:</p>
<blockquote>
<p>《婚姻情境》</p>
<p>《老式喜剧》</p>
<p>《长椅》</p>
</blockquote>
<h4 id="年度最佳清单"><a href="#年度最佳清单" class="headerlink" title="年度最佳清单"></a>年度最佳清单</h4><p><strong>观影最佳:</strong></p>
<blockquote>
<p>1.《赛博朋克 边缘行者》</p>
<p>2.《双姝奇缘》</p>
<p>3.《昨日重现》</p>
</blockquote>
<p><strong>阅读最佳:</strong></p>
<blockquote>
<p>1.《平面国》</p>
<p>2.《也许你该找个人聊聊》</p>
<p>3.《布雷顿角的叹息》</p>
</blockquote>
<p><strong>游戏最佳:</strong></p>
<blockquote>
<p>1.《灵魂渡者》</p>
<p>2.《双人成行》</p>
<p>3.《Lacuna》</p>
</blockquote>
<p><strong>好物最佳:</strong></p>
<blockquote>
<p>1.谐星聊天会(是一个播客节目,还是放在好物栏目吧)</p>
<p>2.New Balance 5740/2002R</p>
<p>3.人艺剧场会员(太超值了!)</p>
</blockquote>
<h3 id="新的期许"><a href="#新的期许" class="headerlink" title="新的期许"></a>新的期许</h3><p>又到了新年 Flag 时间~</p>
<p>工作学习方面:</p>
<ol>
<li><p>说英语听英语写英语</p>
</li>
<li><p>继续感受算法题的快乐</p>
</li>
<li><p>拓宽视野,入门 Rust、了解 Web3</p>
</li>
</ol>
<p>生活方面(大不同):</p>
<ol>
<li><p>保证睡眠</p>
</li>
<li><p>提升有氧适能到 35+</p>
</li>
<li><p>(以各种方式)记录生活</p>
</li>
<li><p>拓宽阅读题材和类型</p>
</li>
</ol>
<blockquote>
<p>Don’t ignore your dreams; don’t work too much; say what you think; cultivate friendships; be happy.</p>
</blockquote>
<p><img src="https://s1.ax1x.com/2023/01/05/pSA8tEV.jpg"></p>
<p><img src="https://s1.ax1x.com/2023/01/05/pSA8JH0.jpg" alt="last sunset of 2022"></p>
<p>2022: <em>ocean of stars</em></p>
2021 年度总结
https://blog.colafornia.me/post/2022/2021-review/
2022-01-01T13:00:00.000Z
2022-01-01T15:43:05.936Z
<p><img src="https://s4.ax1x.com/2022/01/01/ToC8qs.md.jpg" alt="airport with u"></p>
<p>2021: <em>mathematical elegance is not a question of esthetics, of taste, or fashion</em></p>
<a id="more"></a>
<h3 id="工作与学习"><a href="#工作与学习" class="headerlink" title="工作与学习"></a>工作与学习</h3><p>一入字节深似海,博客已经正式变成年更博客了,在年终总结之外,只更新了一篇在组内做的技术分享。2021 年是本科毕业之后工作的第 5 个年头了,经过了刚毕业时的快速成长期、平台期之后,今年是深入业务的一年。</p>
<p>年初即忙于 iOS 14,Apple 隐私政策的更新影响了整个广告投放链路。之后便是 TikTok 的数据合规改造,一改就是一整年,从夏天改到圣诞节,至今还没有结束。团队规模扩充之后,我也不再忙于四处救火,开始专注在自己对接的细分业务方向上,一边写代码一边写需求文档,学着从研发视角去思考业务中的优化点。主动关注竞品的动向与技术实践,对于自身业务的选型与规划也有所脾益。</p>
<p>在技术上今年主要关注的是数据可视化与 <code>Monorepo</code>,去年年底和今年年初给平台做了一些资源加载上的优化,今年年底则是在做运行时的性能提升与前后端整体交互上的优化。随着业务发展,对于 <code>i18n 本地化</code> 也有了更深的体会,新年还是要抽时间总结一些写一下的。总的来说,第一次体会到了与业务一起快速成长的感觉,感谢 TikTok 🤺当然了,自驱学习的并不多这一点还是要反省一下,新的一年还是要保持大量输入,同时多做一些输出的事情,博客/github/技术分享等等。</p>
<p>关于 coding,今年的 moment 是看到了 <a href="http://www.youtube.com/watch?v=RCCigccBzIU" target="_blank" rel="external">Dijkstra 的采访视频</a>:</p>
<blockquote>
<p>程序的优雅性不是可以或缺的奢侈品,而是决定成功还是失败的一个要素。</p>
<p>优雅并不是一个美学的问题,也不是一个时尚品味的问题,优雅能够被翻译成可行的技术。</p>
</blockquote>
<h3 id="生活与玩乐"><a href="#生活与玩乐" class="headerlink" title="生活与玩乐"></a>生活与玩乐</h3><p>双休的体验:贫穷而快乐</p>
<p><img src="https://s4.ax1x.com/2022/01/01/TIqeL8.md.jpg"></p>
<h4 id="随心飞"><a href="#随心飞" class="headerlink" title="随心飞"></a>随心飞</h4><p>每年的年终总结期许都是相同的那几条,今年总算是完成了 「至少去两个没去过的城市玩」,上半年和下半年都买了南航的随心飞,上半年由于体力限制,出去玩了两趟,下半年则是由于疫情,也没太乱跑,整体上全年应该是刚刚值回票价,最大的作用大概是前置付出了成本之后便不会被懒拖住,打包行李就出发,对于我来说还是很有效的。</p>
<p>一共去了四个没有去过的城市,上半年去了长沙和贵州,本来订了端午去顺德的机票,广东疫情就取消了,下半年去了武汉、深圳、顺德、长沙,在广东的时候又赶上广州疫情,就又跑到珠海坐飞机回的北京。每一趟其实都玩得很休闲,就是在工作的间隙去不同的城市逛一逛,观摩 city view,吃吃喝喝。</p>
<h4 id="观影与阅读"><a href="#观影与阅读" class="headerlink" title="观影与阅读"></a>观影与阅读</h4><p><img src="https://s4.ax1x.com/2022/01/01/TIqneS.md.jpg"></p>
<p>看了 66 个电影/剧,28 本书,2 部舞台剧, 2 场 live,今年的书影音活动非常丰富,看了很多老电影,舞台剧和现场,也去了几趟小西天的电影资料馆,还参加了北京电影节,感谢小赵同学。</p>
<p>舞台剧:</p>
<blockquote>
<p>降 E 大调三重奏</p>
<p>献给阿尔吉侬的花束</p>
</blockquote>
<p>Live:</p>
<blockquote>
<p>结冰水 「潜入黑夜」</p>
<p>张蔷 「非常规巡航」with 昨夜派对</p>
</blockquote>
<h4 id="年度最佳清单"><a href="#年度最佳清单" class="headerlink" title="年度最佳清单"></a>年度最佳清单</h4><p><strong>观影最佳:</strong></p>
<blockquote>
<p>1.《万物生灵》</p>
<p>2.《东京教父》</p>
<p>3.《鬼灭之刃》</p>
</blockquote>
<p><strong>阅读最佳:</strong></p>
<blockquote>
<p>1.《神经外科的黑色喜剧》</p>
<p>2.《大萝卜和难挑的鳄梨》</p>
<p>3.《旅行的艺术》</p>
</blockquote>
<p><strong>好物最佳:</strong></p>
<blockquote>
<p>1.空气炸锅 (太好用了,陷入厨房的开始)</p>
<p>2.星粒三</p>
<p>3.随心飞(辩证地看)</p>
</blockquote>
<h3 id="新的期许"><a href="#新的期许" class="headerlink" title="新的期许"></a>新的期许</h3><p>又到了新年 Flag 时间~</p>
<p>工作学习方面:</p>
<ol>
<li><p>增加技术学习的时间精力投入,博客 5+</p>
</li>
<li><p>说英语听英语写英语</p>
</li>
<li><p>入门 Rust 和区块链</p>
</li>
</ol>
<p>生活方面(和去年,前年定的总算有些不同了):</p>
<ol>
<li><p>保证睡眠,尝试用一些工具手段来保证</p>
</li>
<li><p>加强锻炼,还是从心肺功能开始</p>
</li>
<li><p>体重减掉 5kg</p>
</li>
<li><p>培养一个新爱好(这条很虚,但又很必要)</p>
</li>
</ol>
<p><img src="https://s4.ax1x.com/2022/01/01/ToC3rj.md.jpg" width="450"></p>
<p><img src="https://s4.ax1x.com/2022/01/01/ToC8qs.md.jpg" alt="airport with u"></p>
<p>2021: <em>mathematical elegance is not a question of esthetics, of taste, or fashion</em></p>
数据可视化基础学习与实践解析
https://blog.colafornia.me/post/2021/visualization-share/
2021-11-21T03:00:00.000Z
2021-11-20T15:31:42.949Z
<p><img src="/images/visualization.png" alt="cover"></p>
<blockquote>
<p>本文是对在组里做的技术分享的脱敏版本,移除了公司内技术架构相关内容,保留了开源部分</p>
</blockquote>
<a id="more"></a>
<h1 id="Overview"><a href="#Overview" class="headerlink" title="Overview"></a>Overview</h1><p>数据报表在B端业务中,是一个非常基础常用的功能模块,对数据可视化进行进一步的学习可以帮助我们保障海量数据渲染性能、代码可维护性的同时,对数据报表的业务场景有更进一步的认识</p>
<ul>
<li>数据可视化的定义与特点</li>
<li>Web 前端数据可视化的实现方式</li>
<li>图形语法</li>
<li>ECharts/HighCharts/Antv G2/D3 这些开源热门工具是做什么的,解决了什么问题</li>
<li>了解一下开源工具的实现思路:ECharts 架构实现</li>
</ul>
<h1 id="数据可视化是什么"><a href="#数据可视化是什么" class="headerlink" title="数据可视化是什么"></a>数据可视化是什么</h1><blockquote>
<p><em>Data visualization</em> <a href="https://en.wikipedia.org/wiki/Data_visualization" target="_blank" rel="external">wikipedia</a> is an interdisciplinary field that deals with the graphic representation of data. It is a particularly efficient way of communicating when the data is numerous as for example a time series.</p>
</blockquote>
<p>可视化是将数据组织成易于为人所理解和认知的结构,然后用图形的方式形象地呈现出来的理论、方法和技术。数据可视化是一个交叉领域,对于 Web 前端数据可视化而言,有三个关键要素,数据、图形与交互</p>
<p>只陈列数据,我们很难直观了解到数据中隐藏着趋势或者模式,可视化的图形和图像能够帮我们发现、分析这些隐藏信息,从另一个角度来看,数据可视化也是一种表达、传递信息的方式,选择恰当的展现形式可以帮助我们更好地传达信息</p>
<p>举例:陈列表达 VS 可视表达(南丁格尔玫瑰图)</p>
<div style="display:flex"><br><img src="https://z3.ax1x.com/2021/11/20/IOgQwn.png" width="300"><br><img src="https://z3.ax1x.com/2021/11/20/IOg0T1.png" width="100"><br></div>
<h2 id="优秀的可视化案例"><a href="#优秀的可视化案例" class="headerlink" title="优秀的可视化案例"></a>优秀的可视化案例</h2><p><a href="http://globe.cid.harvard.edu/?mode=gridSphere&id=JO" target="_blank" rel="external">哈佛大学:全球经济复杂度</a><br><img src="https://z3.ax1x.com/2021/11/20/IOg6SO.png"></p>
<blockquote>
<p>更多优秀案例可参考:<br><a href="https://www.visualcapitalist.com/" target="_blank" rel="external">https://www.visualcapitalist.com/</a></p>
</blockquote>
<p>在数据可视化话题中,前端开发同学可以认为是关键玩家:</p>
<ul>
<li>如何处理大量、海量数据的 format、丝滑渲染与流畅交互</li>
<li>如何处理可视化逻辑,维护可视化相关代码,而不是查一下 bar chart 的文档,搬来官网 demo 改改能用就行,项目里充斥着每个图表的一次性代码</li>
<li>如何基于业务场景选择、实现合适的形式和图表,这是前端同学需要主动思考来助力业务的一个场景</li>
</ul>
<h1 id="可视化基础与图形语法"><a href="#可视化基础与图形语法" class="headerlink" title="可视化基础与图形语法"></a>可视化基础与图形语法</h1><p><img src="https://z3.ax1x.com/2021/11/20/IOgWmd.png"></p>
<h2 id="四种可视化实现方式"><a href="#四种可视化实现方式" class="headerlink" title="四种可视化实现方式"></a>四种可视化实现方式</h2><ul>
<li>HTML/CSS<br>HTML/CSS 当然也是一种实现方式,并且方便,不需要第三方依赖,如果我们仅需要绘制少量常见的图表其实是可以考虑由 HTML/CSS 来实现。缺点是 CSS 属性不能直观体现数据,绘制起来也比较麻烦,并且当图形发生变化时,可能要重新执行浏览器渲染的全过程,这样的性能开销非常大</li>
<li>SVG<br>是对 HTML/CSS 的增强,弥补了 HTML 绘制不规则图形的能力。它通过属性设置图形,可以直观地体现数据,使用起来非常方便。但是 SVG 也有和 HTML/CSS 同样的问题,图形复杂时需要的 SVG 元素太多,也非常消耗性能</li>
<li>Canvas2D<br>Canvas2D 是浏览器提供的简便快捷的指令式图形系统,它通过一些简单的指令就能快速绘制出复杂的图形。由于它直接操作绘图上下文,因此没有 HTML/CSS 和 SVG 绘图因为元素多导致消耗性能的问题,性能要比前两者快得多。但是如果要绘制的图形太多,或者处理大量的像素计算时,Canvas2D 依然会遇到性能瓶颈,且交互处理比较难,需要经过数学计算,定位的方式来获取局部图形的操作事件</li>
<li>WebGL<br>使用复杂,但是功能强大,能够充分利用 GPU 并行计算的能力,来快速、精准地操作图像的像素,在同一时间完成数十万或数百万次计算。除此之外还内置了对 3D 物体的投影、深度检测等处理,这让它更适合绘制 3D 场景</li>
</ul>
<p>举个例子:选择恰当的实现方式的重要性<br>在2020 美国总统大选结果披露过程中,各家媒体都做了类似的各州选票披露展示:</p>
<ul>
<li><a href="https://www.foxnews.com/elections/2020/general-results" target="_blank" rel="external">foxnews</a></li>
<li><a href="https://apps.npr.org/elections20-interactive/#/president" target="_blank" rel="external">npr</a></li>
<li><a href="https://www.nytimes.com/interactive/2020/11/03/us/elections/results-president.html" target="_blank" rel="external">Nytimes</a></li>
</ul>
<p><img src="https://z3.ax1x.com/2021/11/20/IOgqXQ.png"></p>
<p>除了纽约时报 Nytimes 选择了用 Canvas 绘制之外,各家都选择了用 SVG 来实现,对比体验差异也是非常明显的,在这种固定时间内用户频繁刷新查看数据且图形无变化,只是进行实时的选票数字与颜色披露,SVG 无疑是非常适合的场景,简单方便。而 Nytimes 的选择则有些笨重,用户反映「卡,电脑都烫」</p>
<h2 id="那些业界热门可视化工具都是干什么的"><a href="#那些业界热门可视化工具都是干什么的" class="headerlink" title="那些业界热门可视化工具都是干什么的"></a>那些业界热门可视化工具都是干什么的</h2><ul>
<li>图表库<ul>
<li>ECharts </li>
<li>Chart.js </li>
</ul>
</li>
<li>GIS 地图库<ul>
<li><a href="https://www.mapbox.com/" target="_blank" rel="external">Mapbox</a> </li>
<li>Leaflet </li>
</ul>
</li>
<li>渲染库<ul>
<li>Three.js </li>
</ul>
</li>
<li>数据驱动框架<ul>
<li>D3 Data Driven Document <a href="https://medium.com/@Elijah_Meeks/d3-is-not-a-data-visualization-library-67ba549e8520" target="_blank" rel="external">D3 is not a Data Visualization Library</a> 使用绘图指令对数据进行转换,在源数据的基础上创建新的可绘制数据,生成 SVG 路径以及通过数据和方法在 DOM 中创建数据可视化元素<br>D3 是数据可视化基础库,ECharts 是图表库,G2 是图形语法库,如果说 D3 是面粉,ECharts 是面条,那么 G2 就是面团,它介于面粉和面条之间,比 ECharts 更加灵活,比 D3 效率更高</li>
</ul>
</li>
</ul>
<p><img src="https://z3.ax1x.com/2021/11/20/IOgj7n.png"></p>
<h2 id="图形语法"><a href="#图形语法" class="headerlink" title="图形语法"></a>图形语法</h2><p>在设计一个图表库时,很难设计出足够多的图表类型来满足用户的各类需求。一个基于图表分类的图表库会面临随着图表量增大,而整个系统的结构变得复杂而难以维护的问题。图表与图表之间的一些相似的部分也难以得到高效的复用</p>
<p>因此,在工程实现方面,有一个问题越来越突出:如何能以比较小的代价穷举(使工程适应)尽可能多的图形?归根到底,就是如何表示一个可视化图表的问题</p>
<p> <a href="https://en.wikipedia.org/wiki/Leland_Wilkinson" target="_blank" rel="external">Leland Wilkinson</a> 在上世纪 80 年代开始开发 SYSTAT 的统计图形软件包时,也遇到了这个问题。最初的版本是枚举每一个能收集到的统计图形,最终代码量非常大,约 1.5M 左右。90 年代初,他基于面向对象重构了这个项目,以一种树形结构管理图形元素,得到了更易扩展和动态的结果。这时软件包的大小下降到了 1M 以内。到了 90 年代末期,他和几个统计学家、计算机学家合作基于之前的工作开发了统计图形绘图工具 <a href="http://www.unige.ch/ses/sococ/cl/spss/graph/gpl.html" target="_blank" rel="external">GPL</a> 。这个 Java 版本的软件代码量下降到了 0.5M 以下,并且沉淀出了一套稳定可靠的架构</p>
<p><img src="https://z3.ax1x.com/2021/11/20/IO2AB9.png"></p>
<p>《The Grammar of Graphics》(简称 GOG)就是 Wilkinson 在开发这套可视化软件的时候编写的,既有他对无数统计图表分析研究后的理论总结,也不乏实现图形语法的软件架构细节。至此,用一套语法描述任意图形的方法诞生了,编写基于图形语法的软件包有了理论依据和设计实践指导</p>
<p>GOG 把创建图形的步骤分为 3 步:</p>
<ol>
<li>Specification 定义、描述</li>
<li>Assembly 装配</li>
<li>Display 展现</li>
</ol>
<h3 id="Specification-定义、描述"><a href="#Specification-定义、描述" class="headerlink" title="Specification 定义、描述"></a>Specification 定义、描述</h3><p>一个图形系统需要通过描述来处理数据,定义图形,显示图形。描述的另一种定义是说它是图形的底层语法。一个图形系统的描述总结为以下六条:<br><img src="https://z3.ax1x.com/2021/11/20/IO28HA.png"></p>
<ol>
<li>DATA:从数据集创建变量的一系列数据操作 </li>
<li>TRANS:对数据的一些转化操作,例如聚合、排序、筛选 </li>
<li>SCALE: 缩放变换 </li>
<li>COORD:坐标系统。例如极坐标系 </li>
<li>ELEMENT:图形的视觉属性和属性。例如颜色、大小、形状 </li>
<li>GUIDE:一个或多个辅助项。例如刻度、图例 </li>
</ol>
<h3 id="Assembly-装配"><a href="#Assembly-装配" class="headerlink" title="Assembly 装配"></a>Assembly 装配</h3><p>将上面示例图表的描述信息转换为下图所示的树形结构<br><img src="https://z3.ax1x.com/2021/11/20/IO25DJ.png"><br>这就是面向对象思想的应用,将场景内包含的元素进行分类和组合,利用继承实现分类和代码复用,利用集合(包含关系)对元素进行分组划分。有了一个完整的树形结构之后,在展现阶段,就可以遍历这棵树进行渲染了</p>
<h3 id="Display-展现"><a href="#Display-展现" class="headerlink" title="Display 展现"></a>Display 展现</h3><p>上面的树形结构结合渲染工具(符号、折线,曲线,多边形等),布局规则,我们就可以渲染最终结果了。图形语法使得我们可以单独对其中的某个元素进行添加、删除、修改属性,而不需要重新定义整个对象结构</p>
<h3 id="从数据生成图形的步骤"><a href="#从数据生成图形的步骤" class="headerlink" title="从数据生成图形的步骤"></a>从数据生成图形的步骤</h3><p><img src="https://z3.ax1x.com/2021/11/20/IORZrQ.png"><br>Antv G2 其中的 两个 G 就是来源于 The <em>G</em>rammar of <em>G</em>raphics 中的两个 G,是目前 JavaScript 社区对《The Grammar of Graphics》还原度最高的实现,也受到了 Wilkinson 本人的肯定</p>
<p>Antv G2 实现饼图的示例:<br><figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">const</span> data = [</div><div class="line"> { k: ‘一’, v: <span class="number">40</span> }, </div><div class="line"> { k: ‘二’, v: <span class="number">21</span> }, </div><div class="line"> { k: ‘三’, v: <span class="number">17</span> },</div><div class="line"> { k: ‘四’, v: <span class="number">13</span> }, </div><div class="line"> { k: ‘五’, v: <span class="number">9</span> }</div><div class="line">]</div><div class="line"><span class="keyword">const</span> dv = <span class="keyword">new</span> DataSet.View().source(data) <span class="comment">// 载入数据</span></div><div class="line"> .transform({ <span class="comment">// 数据处理:统计每一个 key 对应数值 value 占总和的比例</span></div><div class="line"> type: ‘percent’,</div><div class="line"> field: ‘v’,</div><div class="line"> dimension: ‘k’,</div><div class="line"> <span class="keyword">as</span>: ‘percent’</div><div class="line"> })</div><div class="line"><span class="keyword">const</span> chart = <span class="keyword">new</span> G2.Chart({</div><div class="line"> container: ‘id’ <span class="comment">// 指定图表容器,可以是 DomNode,也可以是 id</span></div><div class="line">})</div><div class="line">chart.source(dv) <span class="comment">// 载入数据</span></div><div class="line">chart.axis(<span class="literal">false</span>) <span class="comment">// 不显示坐标轴</span></div><div class="line">chart.coordinate(‘polar’).transpose() <span class="comment">// 坐标转换</span></div><div class="line">chart.intervalStack() <span class="comment">// interval 类型的 element,做堆叠处理</span></div><div class="line"> .position(‘<span class="number">1</span>*percent’) <span class="comment">// 位置映射</span></div><div class="line"> .color(‘k’) <span class="comment">// 颜色映射</span></div><div class="line">chart.render() <span class="comment">// 渲染图表</span></div></pre></td></tr></table></figure></p>
<p>如果通过传统的枚举型图表来实现饼图的话,我们需要写一个饼图的类,然后提供数据的接口和配置,数据需要满足要求的格式,配置只能支持固定的选项,然后实现绘制</p>
<p>而在图形语法的设计下,生成每一个图形的过程就是组合不同的基础图形语法的过程。上述饼图的生成过程就经历了从原始数据 data 到数据转换、坐标轴转换、指定辅助元素、指定基础图形对象、度量转换、数据映射等过程</p>
<p>图形语法的灵活和强大之处就在于,我们只需要改动其中某一步的处理过程,就能得到完全不同的、全新的图表。通过一连串链式的变换调用,就实现了饼图,而其中任何一部都可以修改和复用。所以通过这种抽象机制,代码本身就表示了配置<br><img src="https://z3.ax1x.com/2021/11/20/IOfABQ.png"></p>
<h1 id="开源库拆解:ECharts-架构"><a href="#开源库拆解:ECharts-架构" class="headerlink" title="开源库拆解:ECharts 架构"></a>开源库拆解:ECharts 架构</h1><blockquote>
<p>主要参考论文 Li, Deqing, et al. “ECharts: a declarative framework for rapid construction of web-based visualization.” <a href="https://www.sciencedirect.com/science/journal/2468502X" target="_blank" rel="external">Visual Informatics</a> June 2018, Pages 136-146<br><a href="https://www.sciencedirect.com/science/article/pii/S2468502X18300068" target="_blank" rel="external">https://www.sciencedirect.com/science/article/pii/S2468502X18300068</a></p>
</blockquote>
<h1 id="数据流架构"><a href="#数据流架构" class="headerlink" title="数据流架构"></a>数据流架构</h1><p><img src="https://z3.ax1x.com/2021/11/20/IOfeNn.png"></p>
<p>作为一个非常复杂的图表库,ECharts 采用了数据驱动架构来实现,把步骤切分成数据处理,视觉编码和渲染阶段,其最终产生图形元素。数据流是单向的,任何用户交互只会会修改原始选项或数据,并从头开始运行 pipeline。此外,每个阶段都可以作为切面暴露给开发者</p>
<h1 id="渐进式的可视化渲染"><a href="#渐进式的可视化渲染" class="headerlink" title="渐进式的可视化渲染"></a>渐进式的可视化渲染</h1><p><img src="https://z3.ax1x.com/2021/11/20/IOf3B4.png"><br>在进行数据量较大的图表绘制时,数据处理和渲染都会耗费许多时间,当用户进行交互操作时,问题会更加明显,会触发图表从头更新,会导致主线程阻塞,无法处理动画和交互响应</p>
<p>ECharts 采用 chunk data 的方式来解决这个问题。将数据源划分为多个小的 chunk。每一个区块的数据独立进行布局渲染。这样做是为了保证每个 chunk 都能尽可能在 16ms 内完成计算,每个 chunk 计算任务完成后,会调用 requestAnimationFrame,同时暂停接下来的任务,直到下一帧。如果在这个过程中发生了交互行为,那么已创建的旧任务都会被废弃,重新创建新的任务。这样,就可以保证浏览器的主线程不被阻塞,及时响应用户交互</p>
<h1 id="2D-绘图引擎-ZRender"><a href="#2D-绘图引擎-ZRender" class="headerlink" title="2D 绘图引擎 ZRender"></a>2D 绘图引擎 ZRender</h1><p>官网: <a href="https://ecomfe.github.io/zrender-doc/public/" target="_blank" rel="external">https://ecomfe.github.io/zrender-doc/public/</a><br>GitHub: <a href="https://github.com/ecomfe/zrender" target="_blank" rel="external">https://github.com/ecomfe/zrender</a></p>
<p>ZRender 是 ECharts 底层依赖的 2D 绘图引擎,用于图形元素管理、渲染器管理和事件系统,提供 Canvas、SVG、VML 多种渲染方式</p>
<p>ZRender 中有三种绘制类型,image / text / path,image 和 text 的绘制都可以借助 canvas API 来实现,对于 path, 会通过一个叫 PathProxy 的类,它会负责记录底层的绘制指令,把路径命令数据以 Float32Array 存储起来,通过这个数据实现了对多种渲染方式的支持,path 数据也会被用来在事件系统中做事件命中检查</p>
<p>为了让不同的渲染器可以有相同的事件处理机制,ECharts 对整个图表容器绑定了事件。SVG 也并不是其对 DOM 树的每个元素绑定事件,SVG/Canvas 都是使用一套统一的事件处理机制。ECharts 会检测事件的位置坐标 (x, y) 是否在图形边框里,对于 image/text 来说,这一步就可以判断事件是否命中、命中了哪个元素。对于 path 来说,会先检查事件位置是否在实际渲染区域里,然后再去做精确判断</p>
<h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul>
<li><a href="https://www.yuque.com/antv/blog/hq2dmt" target="_blank" rel="external">可视化图形语法简史 · 语雀</a></li>
<li><a href="https://antv-g2.gitee.io/en/docs/manual/concepts/grammar-of-graphics/" target="_blank" rel="external">Antv: The Grammar of Graphics</a></li>
</ul>
<p><img src="/images/visualization.png" alt="cover"></p>
<blockquote>
<p>本文是对在组里做的技术分享的脱敏版本,移除了公司内技术架构相关内容,保留了开源部分</p>
</blockquote>
2020 年度总结
https://blog.colafornia.me/post/2021/2020-review/
2021-01-01T02:00:00.000Z
2021-01-01T15:29:35.347Z
<p><img src="https://s3.ax1x.com/2021/01/01/rxbQ91.jpg" width="650"></p>
<p>2020: <em>self-struggle VS historical schedule</em></p>
<a id="more"></a>
<h3 id="工作"><a href="#工作" class="headerlink" title="工作"></a>工作</h3><p>今年的年终总结其实还是挺难写的,有疫情这个大前提在,很多个人选择都不得不为此进行妥协和取舍。年后居家办公一段时间之后,于 3.6 从百词斩离职,说实话在这个节骨眼裸辞当时压力还是有点大,有很多劝退言论说疫情期间找工作很难。但其实最后发现由于疫情各公司都默认远程面试,对于身在成都的我其实是很大的便利,网络一线牵从投简历到远程入职都毫无压力,如果在往年的话这必然办不到,飞来飞去的面试是免不了的。</p>
<p>具体的面试情况都在<a href="/post/2020/2020-interviews/">前面这篇博客</a>了,现在回头看那段时间的日历日程还是觉得有点可怕,真的累,感觉面完这一波之后我的口才都有了一定的进步。由于刚辞职时心里对于跨城找工作这件事没底,为了避免陷入递归复习到爆栈这种情况,大略复习了一些之后就开始投简历面试了,后面变成被各家 offer 的入职时间催的心慌,只能马不停蹄地继续面(这句凡尔赛是不是还行)。最后决定加入字节,做海外商业变现方向,成为 「 TikTok 一等社畜」。</p>
<p><img src="https://s3.ax1x.com/2021/01/01/rx5Z0s.jpg" width="500"></p>
<p>从成都搬回了北京,住在中关村这边,又折腾了一遍家猫,带它坐飞机回来了。作为北京本地猫,感觉回京它倒是蛮开心的,把在成都期间少晒的太阳都一口气补了回来。后面的事情也和历史的进程脱不开干系,夏天碰上了海外切分,接着是 TikTok 在印度与美国的风波,忙的昏天黑地。到了年底之后才好了一些,借着出差机会去了深圳一趟,一本满足。技术上主要是年初很认真地刷了题,借着 Coursera 的疫情扶持政策,系统地学了一下 Python 语法。其余就没什么了,2020 应该是毕业以来技术投入精力最少的一年。</p>
<h3 id="生活与玩乐"><a href="#生活与玩乐" class="headerlink" title="生活与玩乐"></a>生活与玩乐</h3><p>「在字节还是跳动了不少哦」</p>
<h4 id="主机游戏"><a href="#主机游戏" class="headerlink" title="主机游戏"></a>主机游戏</h4><p>与技术投入相对,今年是主机大面积清灰的一年,吃灰一年的 PS4 开始继续玩了。换完工作入手了 Switch 和健身环,懒得去健身房的时候就拿健身环在家活动一下,还是很实用的。</p>
<p><img src="https://s3.ax1x.com/2021/01/01/rxb5vV.jpg" width="650"></p>
<h4 id="观影与阅读"><a href="#观影与阅读" class="headerlink" title="观影与阅读"></a>观影与阅读</h4><p>刚回北京的时候还是有点焦虑,经常后半夜下单实体书来缓解焦虑,现在租的房子的超大书架也纵容了这种行为,今年买了很多实体书,也看掉了很多本,虽然没有达标 35 但也很满足啦。冬天又看了很多动画片,真是越看越快乐。</p>
<h4 id="年度最佳清单"><a href="#年度最佳清单" class="headerlink" title="年度最佳清单"></a>年度最佳清单</h4><p><strong>观影最佳:</strong></p>
<blockquote>
<p>1.《我亲爱的朋友们》</p>
<p>2.《女大法官金斯伯格》</p>
<p>3.《银之匙》</p>
</blockquote>
<p><strong>阅读最佳:</strong></p>
<blockquote>
<p>1.《当我谈跑步时,我谈些什么》(没错我今年才算从头到尾看完了)</p>
<p>2.《回归故里》</p>
<p>3.《知晓我姓名》</p>
</blockquote>
<p><strong>好物最佳:</strong></p>
<blockquote>
<p>1.健身环大冒险</p>
<p>2.Apple Watch (oncall 神器)</p>
<p>3.优衣库 JW ANDERSON 系列</p>
</blockquote>
<h3 id="新的期许"><a href="#新的期许" class="headerlink" title="新的期许"></a>新的期许</h3><p>又到了新年 Flag 时间~</p>
<p>工作学习方面:</p>
<ol>
<li><p>增加技术学习的时间精力投入,博客 5+</p>
</li>
<li><p>说英语听英语写英语</p>
</li>
<li><p>Github 和开源方面再做一些尝试</p>
</li>
</ol>
<p>生活方面(和去年,前年定的<strong>一模一样</strong>):</p>
<ol>
<li><p>阅读量 35 本+</p>
</li>
<li><p>至少去两个没去过的城市玩</p>
</li>
<li><p>体重减掉 5kg</p>
</li>
<li><p>培养一个新爱好(这条很虚,但又很必要)</p>
</li>
</ol>
<p><img src="https://s3.ax1x.com/2021/01/01/rxHQOS.md.jpg" width="450"></p>
<p><img src="https://s3.ax1x.com/2021/01/01/rxbQ91.jpg" width="650"></p>
<p>2020: <em>self-struggle VS historical schedule</em></p>
年轻人的第一次裸辞找工作面试记录
https://blog.colafornia.me/post/2020/2020-interviews/
2020-04-17T14:00:00.000Z
2020-04-17T13:33:52.019Z
<p><img src="/images/2020-interview.png" alt="cover"></p>
<blockquote>
<p>16年本科毕业,一直以来也没太多面试经历。</p>
<p>今年三月初裸辞,赶上疫情,有蛮多时间进行面试,按时间顺序面了伴鱼/好未来/OPPO/贝壳/滴滴/Shopee/头条/快手/阿里,在此特地总结一下。</p>
<p>前面的面试都记得很认真,到后面精神和体力值都指数级下降,面经就记录得越发简洁了……</p>
</blockquote>
<a id="more"></a>
<h1 id="面经"><a href="#面经" class="headerlink" title="面经"></a>面经</h1><p>先放面经,主要贴一下与项目没太大关系的知识点考察题目</p>
<h3 id="伴鱼(C轮在线教育公司)Offer"><a href="#伴鱼(C轮在线教育公司)Offer" class="headerlink" title="伴鱼(C轮在线教育公司)Offer"></a>伴鱼(C轮在线教育公司)<code>Offer</code></h3><p>一开始投了两家教育类公司,算是我老东家的竞品,业务比较类似所以第一波来面。三轮技术面体验非常好,收到 offer 之后大前端负责人也给了很多关于选组与个人发展的建议。</p>
<h4 id="一面"><a href="#一面" class="headerlink" title="一面"></a>一面</h4><ol>
<li>看代码说输出,常规 EventLoop 题目</li>
<li>看代码说输出,考察 JavaScript 参数按值传递</li>
<li>说说内存堆栈</li>
<li>Fiber 架构</li>
<li>尤雨溪说 Vue3 没有采用 Fiber,依然很快,为什么 (这是个好题目)</li>
<li>懒加载与动态 import 语法的坑</li>
<li>Webpack 怎么给 chunk 包命名,再说说怎么合理分包</li>
<li>说说怎么本地调试 npm 包,考察 npm link</li>
<li>WeakMap 是什么,相比 Object 有什么优点</li>
<li>有没有写过 Webpack 插件,讲讲 Webpack 原理</li>
<li>老生常谈说说从输入 url 到页面加载发生了什么,面试官追问了网络应用层以下的内容</li>
<li>Promise 的状态,Promise.all 是干什么的</li>
<li>Promise.then.catch.then 会执行吗</li>
<li>HTTP 缓存</li>
<li>简短聊了聊项目,顺着简历提到的性能优化点问了一些内容</li>
</ol>
<h4 id="二面"><a href="#二面" class="headerlink" title="二面"></a>二面</h4><ol>
<li>CI / CD怎么做的</li>
<li>Docker 分层是什么,怎么做</li>
<li>Tree-shaking 原理</li>
<li>新版 React 的特性</li>
<li>对 React 做的优化</li>
<li>Hooks VS HOC/ Render Props</li>
<li>给 Render props 传 pure component 有优化吗,解决了什么问题 (也是个埋坑问题)</li>
<li>HTML 渲染机制, CSS 阻塞渲染吗</li>
<li>合成层怎么触发,怎么分的</li>
<li>Node 端 Event Loop</li>
<li>微前端了解多少,如果让你做微前端的技术选型,你怎么考虑</li>
</ol>
<h4 id="三面"><a href="#三面" class="headerlink" title="三面"></a>三面</h4><p>三面是大前端负责人,疯狂问项目,也考察了对业务的思考和看法,没有什么具体的知识点就没记了</p>
<h3 id="好未来(中台)技术面通过,放弃-Offer"><a href="#好未来(中台)技术面通过,放弃-Offer" class="headerlink" title="好未来(中台)技术面通过,放弃 Offer"></a>好未来(中台)<code>技术面通过,放弃 Offer</code></h3><p>题目很神奇,恍惚回到了四年前找实习的时候 = = 两轮技术面但都没问什么有用的,就没怎么记</p>
<ol>
<li>CSS 定位方式, sticky 的使用场景</li>
<li>说说 CSS3 新属性</li>
<li>居中方式, flex布局怎么做</li>
<li>script 标签的 async 和 defer 属性</li>
<li>怎么实现一个扑克牌翻转效果</li>
<li>怎么实现一个富文本编辑器</li>
</ol>
<h2 id="OPPO(智能搜索-base-北京)-Offer"><a href="#OPPO(智能搜索-base-北京)-Offer" class="headerlink" title="OPPO(智能搜索 base 北京) Offer"></a>OPPO(智能搜索 base 北京) <code>Offer</code></h2><h4 id="一面-1"><a href="#一面-1" class="headerlink" title="一面"></a>一面</h4><ol>
<li>JSBridge 原理</li>
<li>React 16 中 Diff 算法的变化</li>
<li>向一个 DOM 后面插入节点怎么做</li>
<li>怎么实现 lodash.get 方法</li>
</ol>
<h4 id="二面-1"><a href="#二面-1" class="headerlink" title="二面"></a>二面</h4><ol>
<li>CDN 的特点,用 CDN 资源为什么快(分布式节点,回源,缓存,CDN 主动拉取)</li>
<li>Vue 与 React 的区别</li>
<li>浏览器渲染相关的思考,怎么优化,对平时开发有什么启示</li>
<li>Node 的特点,为什么适合高并发</li>
<li>Node 服务部署,运维</li>
<li>Node 框架用的什么, Koa 与 Express 相比有什么不同</li>
<li>简历项目挨个聊</li>
</ol>
<h4 id="三面-1"><a href="#三面-1" class="headerlink" title="三面"></a>三面</h4><ol>
<li>介绍一个项目</li>
<li>懒加载怎么做</li>
<li>说说其它性能优化方式</li>
<li>浏览器缓存</li>
<li>路由模式</li>
<li>HTTPS 原理</li>
<li>ContentType 模式,formData 里面是什么结构</li>
<li>数据上报怎么做,除了 img 标签</li>
<li>git workflow,merge 与 rebase 的区别</li>
<li>Promise 原理</li>
</ol>
<h4 id="四面"><a href="#四面" class="headerlink" title="四面"></a>四面</h4><ol>
<li>Node 底层了解多少</li>
<li>React 冷启动很慢,为什么,哪些地方可以优化</li>
<li>用户弱网环境问题排查与优化</li>
<li>性能数据上报怎么实现,什么时候上报</li>
<li>CSRF 防范方法</li>
</ol>
<h3 id="贝壳(如视)-Offer"><a href="#贝壳(如视)-Offer" class="headerlink" title="贝壳(如视) Offer"></a>贝壳(如视) <code>Offer</code></h3><p>如视是贝壳的 VR 业务线,链家、自如 app 里的 VR 看房就是他们做的,很好的团队,做的事情也很酷!</p>
<h4 id="一面-2"><a href="#一面-2" class="headerlink" title="一面"></a>一面</h4><ol>
<li>JSBridge 原理</li>
<li>开发 WebView 遇到过哪些问题</li>
<li>开发直播应用的心得体会(算是与简历项目相关)</li>
<li>如何设计一个日志分析,现场还原系统(记录画面)</li>
<li>实现一个单例</li>
<li>算法,括号匹配问题,Leetcode Easy 题目小变形</li>
</ol>
<h4 id="二面-2"><a href="#二面-2" class="headerlink" title="二面"></a>二面</h4><ol>
<li>SSO 鉴权流程</li>
<li>samesite cookie</li>
<li>toB 和 toC 业务特点,区别</li>
<li>在上家公司遇到了什么问题,怎么解决的,技术和非技术都可以说说</li>
<li>Electron 播放音视频踩过什么坑吗</li>
<li>CSRF 防范方法</li>
<li>Buffer 与 Stream</li>
<li>createObjectURL 与 canvas.toDataURL</li>
<li>base64 是什么</li>
</ol>
<h4 id="三面-2"><a href="#三面-2" class="headerlink" title="三面"></a>三面</h4><p>聊项目为主,问了一些个人规划职业发展的问题</p>
<h3 id="滴滴(网约车-C端)-Offer"><a href="#滴滴(网约车-C端)-Offer" class="headerlink" title="滴滴(网约车 C端) Offer"></a>滴滴(网约车 C端) <code>Offer</code></h3><p>从晚上五点开始,连着面三轮,流程很快</p>
<h4 id="一面-3"><a href="#一面-3" class="headerlink" title="一面"></a>一面</h4><ol>
<li>TypeScript 里有哪些 JavaScript 没有的类型</li>
<li>React Hooks 原理</li>
<li>节流防抖的使用场景</li>
<li>Event Loop</li>
<li>数组与对象有哪些遍历方式,for…in 与 Object.keys 有什么区别</li>
<li>ES6 的 Module 与 CommonJS</li>
<li>Promise 有哪些方法,都是做什么的</li>
<li>按需加载怎么做</li>
<li>实现 Bind</li>
<li>实现一个乱序算法</li>
<li>实现一个深拷贝</li>
<li>跨域场景与常规解决方案</li>
<li>HTTP 缓存</li>
<li>垂直居中与盒子模型</li>
<li>图片跑马灯效果怎么实现</li>
</ol>
<h4 id="二面-3"><a href="#二面-3" class="headerlink" title="二面"></a>二面</h4><ol>
<li>JSBridge 原理</li>
<li>如何自己实现一个组件按需加载</li>
<li>Webpack 原理,追问让说的更细一些</li>
<li>Webpack 热更新原理</li>
<li>Flex 布局</li>
<li>BFC 的原理和使用场景</li>
<li>CI/CD 怎么做的,哪些有提效</li>
<li>说一件让你有成就感的事情</li>
</ol>
<h4 id="三面-3"><a href="#三面-3" class="headerlink" title="三面"></a>三面</h4><ol>
<li>TypeScript type 与 interface 的区别</li>
<li>React Fiber</li>
<li>Babel loader 原理</li>
<li>Node Stream 是干什么的</li>
<li>写一个 ES5 继承</li>
<li>写一个 twoSum 算法</li>
<li>Webpack 常用优化方式</li>
<li>Webpack 原理</li>
<li>React 里 key 做什么的</li>
<li>z-index 干啥的,有哪些条件会形成层叠上下文</li>
</ol>
<h3 id="Shopee-卖家平台-base-深圳-Offer"><a href="#Shopee-卖家平台-base-深圳-Offer" class="headerlink" title="Shopee (卖家平台 base 深圳) Offer"></a>Shopee (卖家平台 base 深圳) <code>Offer</code></h3><p>疫情期间被家里催着回北方,就还是拒了 offer</p>
<h4 id="一面(各种基础题,没记多少)"><a href="#一面(各种基础题,没记多少)" class="headerlink" title="一面(各种基础题,没记多少)"></a>一面(各种基础题,没记多少)</h4><ol>
<li>CSS 块级元素与行内元素, BFC 等等</li>
<li>各种看输出题目,event loop,原型链</li>
<li>写一个大数加法</li>
<li>写一个数组拍平</li>
</ol>
<h4 id="二面-4"><a href="#二面-4" class="headerlink" title="二面"></a>二面</h4><ol>
<li>遍历 Object 属性的方式,哪些可以只遍历自有属性</li>
<li>async 的异常捕获</li>
<li>如何并发执行 async</li>
<li>针对 HTTP 请求的优化方案</li>
<li>icon 是怎么引入的, iconfont 里面怎么识别我们引入的 icon</li>
<li>如何检测浏览器、服务器是否支持 http2.0</li>
<li>Sentry 这类监控怎么监控错误的,对于跨域脚本错误呢</li>
<li>GraphQL 接口的性能与质量,单机QPS多少,单核还是多核部署(这个算问项目)</li>
<li>怎么做懒加载,如果我们想点击一个按钮,然后动态加载 modal,要怎么做</li>
<li>mobx 和 redux</li>
<li>实现一个记忆函数</li>
</ol>
<h3 id="头条(广告系统)Offer"><a href="#头条(广告系统)Offer" class="headerlink" title="头条(广告系统)Offer"></a>头条(广告系统)<code>Offer</code></h3><p>快被二面虐哭了</p>
<h4 id="一面-4"><a href="#一面-4" class="headerlink" title="一面"></a>一面</h4><ol>
<li>移动端自适应方案</li>
<li>几道看输出的题目,考察变量声明提升,暂时性死区,原型链</li>
<li>React 虚拟 DOM diff 算法</li>
<li>Webpack HMR 原理</li>
<li>说几个 HTTP Content-Type</li>
<li>cookie 的属性有哪些,都是干啥的</li>
<li>React 组件间通信机制</li>
<li>React 路由模式原理</li>
<li>JSBridge 原理</li>
<li>手写实现 Promise.all,后面还有加上并发限制的几个小变形</li>
</ol>
<h4 id="二面-5"><a href="#二面-5" class="headerlink" title="二面"></a>二面</h4><ol>
<li>聊聊 React Fiber,Fiber 是依据什么切分任务的</li>
<li>显示器刷新率与浏览器帧率</li>
<li>瀑布流计算逻辑</li>
<li>CSS 幽灵空白节点与解决方案</li>
<li>CSS 行内元素的 baseline 问题</li>
<li>写个深拷贝</li>
<li>Object.create 是干啥的,自己实现一个</li>
<li>JavaScript 里的包装类型</li>
<li>聊聊 JavaScript 的语法解析, JIT 等等</li>
<li>WebAssembly 了解么,是干啥的</li>
<li>说说最近很火的面试题 a == 1 && a == 2 && a == 3</li>
<li>聊聊尾递归</li>
<li>HTTPS 原理</li>
<li>HTTP 缓存</li>
</ol>
<h4 id="三面-4"><a href="#三面-4" class="headerlink" title="三面"></a>三面</h4><ol>
<li>介绍一个项目</li>
<li>这个项目还有哪些可以做的优化,技术优化,产品优化等等</li>
<li>错误监控的实现原理</li>
<li>数据上报的几种方案比较</li>
<li>挨个问了问简历里大佬比较感兴趣的内容</li>
</ol>
<h3 id="快手(中台)Offer"><a href="#快手(中台)Offer" class="headerlink" title="快手(中台)Offer"></a>快手(中台)<code>Offer</code></h3><h4 id="一面-5"><a href="#一面-5" class="headerlink" title="一面"></a>一面</h4><ol>
<li>聊了很久项目,各种项目</li>
<li>Webpack 常见优化手段</li>
<li>Webpack 里有几种哈希,都是干什么的</li>
<li>实现 LRU</li>
</ol>
<h4 id="二面-6"><a href="#二面-6" class="headerlink" title="二面"></a>二面</h4><ol>
<li>介绍一个项目</li>
<li>介绍一下 React Hooks,相比于 HOC 它有什么特点,解决了什么问题</li>
<li>Mobx 如何监听到 Object array 的元素属性变化</li>
<li>开发移动端与 PC 端项目有什么不同</li>
<li>JSBridge 原理,怎么解决安全问题</li>
<li>实现 Promise.all,加上一些变形</li>
<li>写个 treeToArray,再写个 arrayToTree</li>
</ol>
<h4 id="三面-5"><a href="#三面-5" class="headerlink" title="三面"></a>三面</h4><ol>
<li>移动端适配方案,rem 解决了什么问题,为什么要采用 rem</li>
<li>只用原生 API 实现一个拖拽跟随效果(好题目,又扩展地问了很多问题,有空再写写)</li>
<li>除了你一面说的那些,Webpack 还有什么优化手段(这个问法好厉害…)</li>
<li>深入考察了一波 CSS 绝对定位,相对定位,块级元素的本身概念</li>
<li>拐着弯问了个 @import 的特性和坑</li>
</ol>
<h3 id="阿里(钉钉-base-北京)HR面挂"><a href="#阿里(钉钉-base-北京)HR面挂" class="headerlink" title="阿里(钉钉 base 北京)HR面挂"></a>阿里(钉钉 base 北京)<code>HR面挂</code></h3><p>作为面试季的结束,这个结果挺酸爽的……</p>
<p>总体感觉钉钉重点考察候选人对业务甚至商业模式的理解,hr 面都详细地问了做的事情对业务的帮助与影响,当前业务模式下技术有哪些手段可以降低项目成本等等</p>
<p>由于已经到了面试季的最尾声了,题目几乎都没怎么记,其实挺多知识点的切入,扩展方式都很赞</p>
<h4 id="一面-6"><a href="#一面-6" class="headerlink" title="一面"></a>一面</h4><ol>
<li>聊项目</li>
<li>React Fiber</li>
<li>比较一下 Mixin / HOC / Hooks</li>
<li>数据劫持的几种方式</li>
<li>手写 compareVersion 两种解法</li>
</ol>
<h4 id="二面-7"><a href="#二面-7" class="headerlink" title="二面"></a>二面</h4><ol>
<li>聊项目,有没有调研过业界竞品的技术方案</li>
<li>CI 怎么做的, E2E测试的具体收益,适用于哪些场景</li>
<li>扫码登录的实现逻辑</li>
<li>JSBridge 原理</li>
</ol>
<h4 id="三面-6"><a href="#三面-6" class="headerlink" title="三面"></a>三面</h4><ol>
<li>各种聊项目,对过往工作经历里的一些业务理解(各种对业务的理解,有哪些深挖的点)</li>
<li>聊虚拟 DOM 和 diff 算法,与真实 DOM 对比</li>
<li>聊闭包</li>
<li>聊工程化</li>
<li>聊 Web 安全</li>
<li>聊 Go 语言</li>
<li>聊你对钉钉的看法和对业务,前景理解</li>
</ol>
<h4 id="四面-1"><a href="#四面-1" class="headerlink" title="四面"></a>四面</h4><p>这一面聊技术的内容不多,更多还是一些职业规划,技术规划,和业务方有分歧怎么处理这些软素质相关的问题。</p>
<p>四面之后本来还要有一轮大佬面,但是时间没排上就先进行 hr 面了,hr 面了一个半小时,然后就挂了…</p>
<h1 id="体会"><a href="#体会" class="headerlink" title="体会"></a>体会</h1><p>本来春节期间趁着在家还着重刷了一波算法的,但还是菜,面试期间手撕算法的场合也不多。</p>
<p>除了以上列出的技术考察点,还是聊项目更多,所以功在平时,好好做项目,多思考是必要积累。大多数面试里都是一轮纯基础面,类似于题库抽题的方式来考察各种基本点。第一轮通过之后都是聊项目为主,穿插着一些技术点面试也是很虚的“聊聊xx”,在我看来这种提问方式很考察对技术点的纵向理解,要自己扩展着一直说说说才行,这种题目也达成了一问一答的话就会减分,在面试官那里看起来是卡壳的。</p>
<p>对于贴在简历里的项目而言,项目的方方面面自己都要心里有数,最基本的打包体积有多少,首屏时间有多少,除此之外,针对项目在业务上下游中的作用也要有所了解和思考。针对面试官对项目的提问,一些场景的方案设计也要尽量做到,从业务出发,主要为业务考虑,权衡多种方案,给出自己的答案。<strong>很多时候,思考过程比答案更重要,毕竟面试并不是考察机器记忆。</strong></p>
<p>还是要继续努力才行~</p>
<p><img src="/images/2020-interview.png" alt="cover"></p>
<blockquote>
<p>16年本科毕业,一直以来也没太多面试经历。</p>
<p>今年三月初裸辞,赶上疫情,有蛮多时间进行面试,按时间顺序面了伴鱼/好未来/OPPO/贝壳/滴滴/Shopee/头条/快手/阿里,在此特地总结一下。</p>
<p>前面的面试都记得很认真,到后面精神和体力值都指数级下降,面经就记录得越发简洁了……</p>
</blockquote>
React Hooks 起手式
https://blog.colafornia.me/post/2020/react-hooks-starter/
2020-01-06T14:10:00.000Z
2020-01-17T03:24:56.661Z
<p><img src="/images/react-hooks-cover.jpeg" alt="react hooks cover"></p>
<p>React Hooks 在 React@16.8 版本中正式发布,之后在两个项目中尝鲜使用了一下,很大提升了开发效率和体验,尤其是在 WebRTC 直播项目里,简直是救我狗命的存在。在此来整理一下相关知识,用一个舒适的顺序剖析关于 React Hooks 的方方面面。</p>
<a id="more"></a>
<h3 id="Motivation"><a href="#Motivation" class="headerlink" title="Motivation"></a>Motivation</h3><blockquote>
<p>They let you use state and other React features without writing a class.</p>
<p>React hooks 可以让你在 Class 之外使用 state 以及 React 的其它特性</p>
</blockquote>
<p>React 的官方文档中明确说明了引入 React Hooks 是为了解决以下问题:</p>
<ol>
<li><p>难以在组件间复用逻辑</p>
<p>之前是通过 render props 和 高阶组件(HOC)来解决的,但这大大提升了程序的复杂度,多个高阶组件的嵌套也会形成嵌套地狱(Wrapper Hell)。</p>
</li>
<li><p>组件越来越复杂,难以理解难以维护</p>
</li>
</ol>
<p> 复杂的生命周期机制,每个生命周期方法里都是几种不相干的逻辑代码和副作用。事件监听的代码在 <code>componentDidMount</code>里,解绑的清理代码在 <code>componentWillUnmount</code> 里,这让组件更难拆分了。</p>
<ol>
<li>Class 让人和计算机都难以理解</li>
</ol>
<p> 光 <code>this</code> 指针这一项就已经有点难用了,除此之外,Class 对于代码压缩与热加载也并不友好,会有一些边界 Case。</p>
<p>因此,我们可以理解为 React Hooks 的<strong>设计目标</strong>就是:</p>
<ol>
<li>免去编写 Class 的复杂性,解决生命周期的复杂性</li>
<li>解决逻辑复用难的问题</li>
</ol>
<p>接下来,我们来看看是怎么实现的。</p>
<h3 id="Implement"><a href="#Implement" class="headerlink" title="Implement"></a>Implement</h3><p>看到前面的动机,可能心里会有个念头“既然 Class 这么难用,当初为什么要这么设计非要使用 Class 来编写组件呢”。这当然有其历史必然性了。</p>
<p>React 中有两种组件形式,Class 类组件与 Function 函数组件:</p>
<p><code>Class Component</code>:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div></pre></td><td class="code"><pre><div class="line"><span class="class"><span class="keyword">class</span> <span class="title">App</span> <span class="keyword">extends</span> <span class="title">React</span>.<span class="title">Component</span></span>{</div><div class="line"> <span class="keyword">constructor</span>(props){</div><div class="line"> <span class="keyword">super</span>(props);</div><div class="line"> <span class="keyword">this</span>.state = {</div><div class="line"> <span class="comment">//...</span></div><div class="line"> }</div><div class="line"> }</div><div class="line"> componentDidMount() {</div><div class="line"> <span class="keyword">this</span>.fetchData()</div><div class="line"> }</div><div class="line"> <span class="comment">//...</span></div><div class="line"> render() {</div><div class="line"> <span class="comment">// ...</span></div><div class="line"> }</div><div class="line">}</div></pre></td></tr></table></figure>
<p><code>Function Component</code>:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">App</span>(<span class="params">links</span>)</span>{</div><div class="line"> <span class="keyword">return</span> (</div><div class="line"> <span class="xml"><span class="tag"><<span class="name">div</span>></span></span></div><div class="line"> <span class="tag"><<span class="name">ul</span>></span></div><div class="line"> {links.map(({href, title})=> <span class="tag"><<span class="name">li</span>></span><span class="tag"><<span class="name">a</span> <span class="attr">href</span>=<span class="string">{href}</span>></span>{title}<span class="tag"></<span class="name">a</span>></span><span class="tag"></<span class="name">li</span>></span> )}</div><div class="line"> <span class="tag"></<span class="name">ul</span>></span></div><div class="line"> <span class="tag"></<span class="name">div</span>></span></div><div class="line"> )</div><div class="line">}</div></pre></td></tr></table></figure>
<p>类组件中会维护内部状态,函数组件则是无状态的。</p>
<p>这就是关键之处,<strong>函数组件无法保存状态</strong>,每次重新运行函数都会导致其作用域内所有函数被重置,也无法像类组件一样通过继承 <code>React.Component</code> 原型上的方法 <code>setState</code> 来更新自己的状态。所以,在过去,如果我们想让一个函数组件具有状态,就不得不将其转为类组件。</p>
<p>那么,如果我们想抛弃 Class,转向函数组件的话,必须解决的事便是让函数组件也能保留、修改、持久化自己的状态。</p>
<p>怎么能让函数状态持久化?</p>
<p>答案是 <strong>闭包</strong></p>
<p><img src="https://s2.ax1x.com/2020/01/06/ly6NJ1.md.png" alt="closure"></p>
<p>惊不惊喜,意不意外?就是初学 JavaScript 时阴魂不散的闭包,简直闭包天天见。</p>
<h4 id="Based-on-closures"><a href="#Based-on-closures" class="headerlink" title="Based on closures"></a>Based on closures</h4><p>所以,以 <code>useState</code> 为例,我们可以简单实现(以下均为简单实现,非源码)为:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">let</span> _state</div><div class="line"><span class="keyword">const</span> useState = (initialValue) => {</div><div class="line"> _state = _state || initialValue</div><div class="line"> <span class="keyword">const</span> setState = (newValue) => {</div><div class="line"> _state = newValue</div><div class="line"> render()</div><div class="line"> }</div><div class="line"> <span class="keyword">return</span> [_state, setState]</div><div class="line">}</div></pre></td></tr></table></figure>
<p>怎么支持组件内维护多个状态?最简单的方式当然是用数组:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">let</span> _hooks = []</div><div class="line"><span class="keyword">let</span> _cursor = <span class="number">0</span></div><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">useState</span>(<span class="params">initialValue</span>) </span>{</div><div class="line"> _hooks[_cursor] = _hooks[_cursor] || initialValue</div><div class="line"> <span class="keyword">const</span> setStateHookCursor = _cursor</div><div class="line"> <span class="keyword">const</span> setState = (newVal) => {</div><div class="line"> _hooks[setStateHookCursor] = newVal</div><div class="line"> }</div><div class="line"> <span class="keyword">return</span> [hooks[_cursor++], setState]</div><div class="line">}</div></pre></td></tr></table></figure>
<p>维护一个数组变量,一个锚点 cursor 就可以指哪打哪,准确读取、修改状态值了。</p>
<p><code>useEffect</code> 也是类似的,只是状态数组里保存的是 deps,除此之外再增加一个浅比较 deps 是否有变化的逻辑即可:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">useEffect</span>(<span class="params">callback, depArray</span>) </span>{</div><div class="line"> <span class="keyword">const</span> hasNoDeps = !depArray</div><div class="line"> <span class="keyword">const</span> deps = _hooks[_cursor]</div><div class="line"> <span class="keyword">const</span> hasChangedDeps =</div><div class="line"> !deps || !depArray.every((el, i) => el === deps[i])</div><div class="line"> <span class="keyword">if</span> (hasNoDeps || hasChangedDeps) {</div><div class="line"> callback()</div><div class="line"> hooks[_cursor] = depArray</div><div class="line"> }</div><div class="line"> _cursor++;</div><div class="line">}</div></pre></td></tr></table></figure>
<p>React 的源码实现当然比上面复杂的多,事实上,React Hooks 是在 Fiber 架构基础上实现的。</p>
<h4 id="Based-on-Fiber"><a href="#Based-on-Fiber" class="headerlink" title="Based on Fiber"></a>Based on Fiber</h4><p>按照<a href="https://github.com/acdlite/react-fiber-architecture" target="_blank" rel="external">官方的说法</a>,<code>React Fiber</code> 是<strong>对核心算法的一次重新实现</strong>,也可以说它是一个新的 <code>Reconciler</code>。所以, <code>Reconciler</code> 是什么呢?</p>
<p>React 的源码可以分为三个主要部分:</p>
<blockquote>
<p><code>React Core</code></p>
<p>这一部分只涵盖了与定义组件相关的顶层 API</p>
<p><code>Renderers</code></p>
<p>渲染器模块负责管理 React 树如何被具体底层平台所调用,Web 则为 DOM API,React Native 的话则是安卓 iOS 的视图 API。</p>
<p>除此之外还有 <a href="https://github.com/facebook/react/tree/master/packages/react-test-renderer" target="_blank" rel="external">React Test Renderer</a> 渲染器,可以把 React 组件转化为 JSON 树,供 Jest 这类测试框架做快照测试使用</p>
<p><code>Reconcilers</code></p>
<p>Reconciler 是一种 diff 算法用以确定在状态改变时需要更新那些 DOM 元素。它没有公开的 API,因此也没被独立打包,它只被 React DOM 和 React Native 这类渲染器使用</p>
<p>在推出 v16 的 Fiber 架构后,新的 Reconciler 被称为 <code>Fiber Reconciler</code>,v15 及之前的实现则被称为 <code>Stack Reconciler</code></p>
</blockquote>
<p><img src="https://blog.atulr.com/static/cc397bd7316079f477f29f36fd058a80/832fe/common-reconciler.png" alt="react codebase"></p>
<p>了解到这里, <code>Fiber Reconciler</code> 是为了解决 React v15 的DOM元素多,频繁刷新场景下的主线程阻塞问题,直观显示,则是“掉帧”问题。v15 是一次同步处理整个组件树,通过递归的方式进行渲染,使用 JavaScript 引擎自身的函数调用栈,它会一直执行到栈空位置,一旦工作量大就会阻塞整个主线程(就像前面说的用 HOC 方式形成 Wrapper Hell 的话不仅 debug 难,对性能也会产生严重影响)。然而我们的更新工作可能并不需要一次性同步完成,其中是可以按照优先级调整工作,把整个过程分片处理的,这就是 Fiber 想做的事。</p>
<p> <code>Fiber Reconciler</code> 以<strong>链表</strong>的形式遍历组件数,可以灵活的暂停、继续、放弃当前任务。通过 Scheduler 调度器来进行任务分配,每次只做一个小任务,通过 <code>requestIdleCallback</code> 回到主线程看看有没有更高优先级的任务需要处理,如果有就暂停当前任务,去做优先级更高的,否则就继续执行。</p>
<p><img src="https://s2.ax1x.com/2020/01/06/lycVOO.md.png" alt="Fiber"></p>
<p>关于 Fiber 的内容可以了解的还有更多,包括它是怎么划分优先级的,对现有代码的影响等等。但是还是先回到关于 Hooks 的实现。</p>
<p>Fiber 的类型定义在<a href="https://github.com/facebook/react/blob/7dc9745427046d462506e9788878ba389e176b8a/packages/react-reconciler/src/ReactFiber.js#L127" target="_blank" rel="external">源码的 react-reconciler/src/ReactFiber.js 文件里</a>,我们抽取一下需要了解的字段:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">export</span> type Fiber = {</div><div class="line"> tag: WorkTag,</div><div class="line"> key: <span class="literal">null</span> | string,</div><div class="line"> type: any,</div><div class="line"> <span class="comment">// ...</span></div><div class="line"> memoizedState: any</div><div class="line">}</div></pre></td></tr></table></figure>
<p><code>memoizedState</code> 就是用来储存当前渲染节点的最终状态值。</p>
<p>我们再看一下 <a href="https://github.com/facebook/react/blob/7dc9745427046d462506e9788878ba389e176b8a/packages/react-reconciler/src/ReactFiberHooks.js#L142" target="_blank" rel="external">Hook 的类型定义</a>:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">export</span> type Hook = {</div><div class="line"> memoizedState: any, <span class="comment">// 上一次更新之后的最终状态值</span></div><div class="line"> queue: UpdateQueue | <span class="literal">null</span>, <span class="comment">// 更新队列,存储多次更新操作</span></div><div class="line"></div><div class="line"> next: Hook | <span class="literal">null</span>, <span class="comment">// 指向链表的下一个 Hook</span></div><div class="line">};</div><div class="line"></div><div class="line">type Update {</div><div class="line"> action: any,</div><div class="line"> next: Update,</div><div class="line">};</div><div class="line"></div><div class="line">type UpdateQueue {</div><div class="line"> last: Update,</div><div class="line"> dispatch: any,</div><div class="line"> lastRenderedState: any,</div><div class="line">};</div></pre></td></tr></table></figure>
<p>不难看到,在实际的实现中,React Hooks 并没有采用数组,而是通过单向链表的方式来存储多个 Hooks。</p>
<p>除此之外,可以看到 Queue 有个 last 字段,我们可以调用 <a href="https://github.com/facebook/react/blob/7dc9745427046d462506e9788878ba389e176b8a/packages/react-reconciler/src/ReactFiberHooks.js#L1224" target="_blank" rel="external">dispatchAction</a>(即更新 state 的方法) 多次,也只有最后那次会生效,生效为 last 存储的最后一次 update 的 state 值。</p>
<p>因此,在每个组件内,都会有个 Fiber 对象以这样的形式来存储:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">const</span> [catName, setCatName] = useState(<span class="string">'Tom'</span>);</div><div class="line"><span class="keyword">const</span> [mouseName, setMouseName] = useState(<span class="string">'Jerry'</span>);</div><div class="line">setCatName(<span class="string">'Tommy'</span>)</div><div class="line"></div><div class="line"><span class="comment">// FiberNode</span></div><div class="line"><span class="keyword">const</span> fiber = {</div><div class="line"> <span class="comment">//...</span></div><div class="line"> memoizedState: {</div><div class="line"> memoizedState: <span class="string">'Tom'</span>, </div><div class="line"> queue: {</div><div class="line"> last: {</div><div class="line"> action: <span class="string">'Tommy'</span></div><div class="line"> }, </div><div class="line"> dispatch: dispatch,</div><div class="line"> lastRenderedState: <span class="string">'Tommy'</span></div><div class="line"> },</div><div class="line"> next: {</div><div class="line"> memoizedState: <span class="string">'Jerry'</span>,</div><div class="line"> queue: {</div><div class="line"> <span class="comment">// ...</span></div><div class="line"> },</div><div class="line"> next: <span class="literal">null</span></div><div class="line"> }</div><div class="line"> },</div><div class="line"> <span class="comment">//...</span></div><div class="line">}</div></pre></td></tr></table></figure>
<p>调用 Hook API 实际上就是新增一个 Hook 实例并将其追加到 Hooks 链表上,返回给组件的是这个 Hook 的 state 和对应的 setter,链表的结构决定了 re-render 时 React 并不会知道这个 setter 对应的是哪个 hooks,因此它会从链表的头开始一一执行(这是采用了链表结构的弊端,但是如果通过 HashMap 来存储的话,每次调用 Hook API 都需要显示地传入一个 Key 值来区分不同 Hook,更复杂了)。</p>
<p>这个 Hooks 链表是在 mount 阶段时构造的,所以声明 Hook 时的<strong>顺序很重要</strong>,这也是为什么我们只能在函数组件顶部作用域调用Hook API,不能在条件语句、循环、子函数里调用 Hooks。</p>
<h3 id="Notice-Capture-Value"><a href="#Notice-Capture-Value" class="headerlink" title="Notice: Capture Value"></a>Notice: Capture Value</h3><p>这是一个新手常见坑,首先来看一段代码(<a href="https://codesandbox.io/s/k5pmk0omx7" target="_blank" rel="external">在线地址</a>):</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">Example</span>(<span class="params"></span>) </span>{</div><div class="line"> <span class="keyword">const</span> [count, setCount] = useState(<span class="number">0</span>);</div><div class="line"></div><div class="line"> <span class="keyword">const</span> handleAlertClick = useCallback(</div><div class="line"> () => {</div><div class="line"> setTimeout(() => {</div><div class="line"> alert(<span class="string">"You clicked on: "</span> + count);</div><div class="line"> }, <span class="number">3000</span>);</div><div class="line"> },</div><div class="line"> [count]</div><div class="line"> );</div><div class="line"></div><div class="line"> <span class="keyword">return</span> (</div><div class="line"> <span class="xml"><span class="tag"><<span class="name">div</span>></span></span></div><div class="line"> <span class="tag"><<span class="name">p</span>></span>You clicked {count} times<span class="tag"></<span class="name">p</span>></span></div><div class="line"> <span class="tag"><<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">{()</span> =></span> setCount(count + 1)}>增加 count<span class="tag"></<span class="name">button</span>></span></div><div class="line"> <span class="tag"><<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">{handleAlertClick}</span>></span>显示 count<span class="tag"></<span class="name">button</span>></span></div><div class="line"> <span class="tag"></<span class="name">div</span>></span></div><div class="line"> );</div><div class="line">}</div></pre></td></tr></table></figure>
<p>先点击 “显示 count” 按钮,然后快速点击 “增加 count” 两下,会发现 alert 弹窗上显示的是 0,然而 p 标签里已经是2了。这就是 React Hook 的 <code>Capture Value</code> 快照特性。</p>
<p><img src="https://s2.ax1x.com/2020/01/17/lziPWn.png" alt="image-20200106180246276"></p>
<blockquote>
<p><strong>Each Render Has Its Own Props and State</strong></p>
<p>记住,每次 Render 都有自己的 Props 和 State</p>
</blockquote>
<p>每次 Render 的内容都会形成一个快照并保存下来,因此当状态变更而 re-render 时,就有了 N 个快照,每个都拥有自己独立的,固定不变的 Props 和 State。在每个快照之间(在这段代码里即每次点击之间),count 只是一个常量,不存在数据绑定,watcher 或者 proxy 之类的东西,它只是一个常量数字。因此点击“显示 count” 按钮时,当前快照内 count 值为 0,alert 弹窗为 0,后面无论点击多少次“增加 count” 按钮,都是新的快照,与它无关了。</p>
<p><code>Capture Value</code> 特性存在于除 <code>useRef</code> 之外的所有 Hook API 中(因为<strong>非 useRef 相关的 Hook API,本质上都形成了闭包,闭包有自己独立的状态,这就是 Capture Value 的本质</strong>)。所以如果想避免上述例子中取不到 state 最新值的情况,可以通过 <code>useRef</code> 把所需的 state 值保存下来:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">Example</span>(<span class="params"></span>) </span>{</div><div class="line"> <span class="keyword">const</span> [count, setCount] = useState(<span class="number">0</span>);</div><div class="line"> <span class="keyword">const</span> countRef = useRef(<span class="literal">null</span>)</div><div class="line"></div><div class="line"> <span class="keyword">const</span> handleAlertClick = useCallback(</div><div class="line"> () => {</div><div class="line"> setTimeout(() => {</div><div class="line"> alert(<span class="string">"You clicked on: "</span> + countRef.current);</div><div class="line"> }, <span class="number">3000</span>);</div><div class="line"> },</div><div class="line"> [count]</div><div class="line"> );</div><div class="line"></div><div class="line"> <span class="keyword">return</span> (</div><div class="line"> <span class="xml"><span class="tag"><<span class="name">div</span>></span></span></div><div class="line"> <span class="tag"><<span class="name">p</span>></span>You clicked {count} times<span class="tag"></<span class="name">p</span>></span></div><div class="line"> <span class="tag"><<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">{()</span> =></span> {</div><div class="line"> countRef.current = count + 1</div><div class="line"> setCount(count + 1)</div><div class="line"> }}>增加 count<span class="tag"></<span class="name">button</span>></span></div><div class="line"> <span class="tag"><<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">{handleAlertClick}</span>></span>显示 count<span class="tag"></<span class="name">button</span>></span></div><div class="line"> <span class="tag"></<span class="name">div</span>></span></div><div class="line"> );</div><div class="line">}</div></pre></td></tr></table></figure>
<h3 id="Hooks-API"><a href="#Hooks-API" class="headerlink" title="Hooks API"></a>Hooks API</h3><p>接下来简单介绍一下官方的几个有意思的 Hooks 的常规用法和注意事项。</p>
<h4 id="useRef"><a href="#useRef" class="headerlink" title="useRef"></a>useRef</h4><p>前面在 Capture Value 也介绍了,它是唯一返回 mutable 数据的 Hook,它不仅可以 DOM 引用,还可以存储任意 JavaScript 值。</p>
<p>修改 useRef 的值必须改其 current 属性,否则不会触发 re-render</p>
<h4 id="useCallback"><a href="#useCallback" class="headerlink" title="useCallback"></a>useCallback</h4><p>useCallback 可以保证在 re-render 之间返回的始终是同一回调引用:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// 只要 a 或 b 不变,这个值就不会变化</span></div><div class="line"><span class="keyword">const</span> memoizedCallback = useCallback(() => {</div><div class="line"> doSomething(a, b);</div><div class="line">}, [a, b]);</div></pre></td></tr></table></figure>
<p>需要注意的是,用 useCallback 包裹的函数所用参数,也必须在 hook 的 deps 数组里。</p>
<p>使用场景:</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">Counter</span>(<span class="params"></span>) </span>{</div><div class="line"> <span class="keyword">const</span> [count, setCount] = useState(<span class="number">0</span>)</div><div class="line"> <span class="keyword">const</span> handleIncrement = useCallback(() => {</div><div class="line"> setCount(count + <span class="number">1</span>)</div><div class="line"> }, [count])</div><div class="line"></div><div class="line"> <span class="keyword">return</span> (<span class="xml"><span class="tag"><<span class="name">div</span>></span></span></div><div class="line"> {count}:</div><div class="line"> <span class="tag"><<span class="name">ComplexButton</span> <span class="attr">onClick</span>=<span class="string">{handleIncrement}</span>></span>increment<span class="tag"></<span class="name">ComplexButton</span>></span></div><div class="line"> <span class="tag"></<span class="name">div</span>></span>)</div><div class="line">}</div></pre></td></tr></table></figure>
<p>如果不使用 useCallback 来包住回调函数的话,那么每次点击按钮修改 count 值时触发 re-render 生成新的回调函数,传入 ComplexButton 的 props 发生变化,导致了 ComplexButton 重新渲染。</p>
<h4 id="useMemo"><a href="#useMemo" class="headerlink" title="useMemo"></a>useMemo</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">const</span> memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);</div></pre></td></tr></table></figure>
<p>仅当依赖项发生改变时,才会重新计算 memoizedValue,常被用于缓存昂贵计算函数的返回值。</p>
<p><strong><code>useCallback(fn, deps)</code>== <code>useMemo(() => fn, deps)</code></strong></p>
<p>React 提供了一个与类组件的 <code>PureComponent</code> 相同功能的 API <code>React.memo</code>,会在自身 re-render 时,对每一个 <code>props</code> 项进行浅比较,如果引用没有变化,就不会触发重渲染。</p>
<h4 id="useReducer"><a href="#useReducer" class="headerlink" title="useReducer"></a>useReducer</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">const</span> [state, dispatch] = useReducer(reducer, initialArg, init);</div></pre></td></tr></table></figure>
<p>useState 用于相对扁平结构的状态,useReducer 则用于复杂结构的状态。而其返回的 dispatch 方法可以放心传递给子组件,而不会造成子组件的 re-render</p>
<p>具体可参考 <a href="https://codesandbox.io/s/xzr480k0np" target="_blank" rel="external">Dan Abramov 的示例代码</a></p>
<p>先写到这里,后面希望可以再学习整理一下 React Hooks 的逻辑复用实践~</p>
<h3 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h3><ul>
<li><a href="https://www.youtube.com/watch?v=KJP1E-Y-xyo&t=930s" target="_blank" rel="external">Getting Closure on React Hooks by Shawn Wang | JSConf.Asia 2019 - YouTube</a></li>
<li><a href="https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889" target="_blank" rel="external">Making Sense of React Hooks - Dan Abramov - Medium</a></li>
<li><a href="https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e" target="_blank" rel="external">React hooks: not magic, just arrays - Medium</a></li>
<li><a href="https://blog.atulr.com/react-custom-renderer-1/" target="_blank" rel="external">Beginners guide to Custom React Renderers</a></li>
<li><a href="https://www.infoq.cn/article/fiWNgsIOLaCmt-hphLYC" target="_blank" rel="external">React Hook 构建过程:没有设计就是最好的设计</a></li>
<li><a href="https://github.com/chemdemo/chemdemo.github.io/issues/15" target="_blank" rel="external">React Hooks完全上手指南</a></li>
</ul>
<p><img src="/images/react-hooks-cover.jpeg" alt="react hooks cover"></p>
<p>React Hooks 在 React@16.8 版本中正式发布,之后在两个项目中尝鲜使用了一下,很大提升了开发效率和体验,尤其是在 WebRTC 直播项目里,简直是救我狗命的存在。在此来整理一下相关知识,用一个舒适的顺序剖析关于 React Hooks 的方方面面。</p>
2019 年度总结
https://blog.colafornia.me/post/2019/2019-review/
2019-12-31T14:00:00.000Z
2019-12-31T16:29:14.669Z
<p><img src="/images/chengdu-2019.jpeg" alt="sunset roller coaster"></p>
<p>2019: to see the world as it is, and <em>to love it</em></p>
<a id="more"></a>
<h3 id="工作与学习"><a href="#工作与学习" class="headerlink" title="工作与学习"></a>工作与学习</h3><p>写<a href="/post/2019/2018-review/">去年的年终总结</a>当天收到了百词斩的 offer,这让这一整年画风都变得完全不一样了。</p>
<p>接触到了我自以为从不会踏足的“儿童教育”领域,体验了“966”的神奇作息,被“成都式外企”的英文用语轰炸了一年,也在圣诞被拉到了西双版纳和全公司一起“读书”。</p>
<p>这一年总算是得偿所愿做了很多服务器相关的东西,用 <code>Golang</code> 开发了内容量产系统(总算学习且投产了一门新的编程语言),也给基于 <code>Ruby on Rails</code> 的老项目做了些删删改改,这大概是今年与 Coding 相关的最激动人心的事情了。</p>
<p>在前端的投入并不算多,与其概括为平台期,“疲劳期”可能更为合适,做了三年多的前端开发之后,更想一窥 Web 软件开发的全貌,而不是沉迷于 TC39 的提案或是前端框架的源码了。与前端比较相关的内容也基本是关于工程化,<code>CI/CD</code> 等等。到了年底翻了翻各个技术资讯平台上的内容还是集中在事件循环,deep clone , vue 原理的时候松了口气,问题不大不用太慌,当然关于 <code>WebAssembly</code>,<code>Serverless</code> , <code>TypeScript</code> 最佳实践这些东西还是要尽快打起鸡血学起来。</p>
<p>关于工作最大的收获还是一个好 leader,带着我去做了这些很 Cool 的东西,让我对写代码这件事的态度从“小心翼翼别搞坏了”变成了“来吧都能学,我也都能搞得定,要么能 fancy 地搞定,最差也能挫挫地解决”。这大概是给我高投入高消耗了三年多的开发经历里注入一管新鲜血液,让我能走得更远。</p>
<p>关于计算机基础学习有了一些进步,刷了 <a href="https://github.com/Colafornia/Coursera" target="_blank" rel="external">Coursera</a> 的两门课,北大的计算机组成原理和普林斯顿的算法,野生程序员的野生含量低了点。</p>
<p>今年的一大变化是显式地学了很多英语,平时跟着公司的薄荷英语作为每日的基本英语输入,年底裸考了一把均6的雅思感觉还是很6的(至于为啥变成裸考又是个 long long story 了)。学习过程中发现我的发音还是很成问题,元音的发音有些是混淆的,基本都是往小嘴型,往懒的方向发音,现在想纠正回来真的很费劲。</p>
<h3 id="生活与玩乐"><a href="#生活与玩乐" class="headerlink" title="生活与玩乐"></a>生活与玩乐</h3><p>「没有年假的 966 生活与玩乐就相当于没有吧」</p>
<h4 id="健身"><a href="#健身" class="headerlink" title="健身"></a>健身</h4><p>今年没有买私教课,选择自己练了,练的不是很较真,“三分练七分吃”里大概只得了一分练两分吃,今年的锻炼主要是为了保证运动量和心肺锻炼,避免在 966 里萎靡不振。但是年底 trans 到新的项目组之后,可以利用的健身时间越来越少了,这是新年亟待解决的第一个问题。</p>
<h4 id="观影与阅读"><a href="#观影与阅读" class="headerlink" title="观影与阅读"></a>观影与阅读</h4><p>今年电影和剧看得都很少,书有 36 本,达标了去年定的 35本+,基本上大多数都是非技术类。</p>
<h4 id="年度最佳清单"><a href="#年度最佳清单" class="headerlink" title="年度最佳清单"></a>年度最佳清单</h4><p>观影最佳:</p>
<blockquote>
<p>1.《昨日的美食》</p>
<p>2.《盗梦特工队》</p>
<p>3.《Live》</p>
</blockquote>
<p><strong>阅读最佳:</strong></p>
<blockquote>
<p>1.《献给阿尔吉侬的花束》</p>
<p>2.《海风中失落的血色馈赠》</p>
<p>3.《网 阿加西自传》</p>
</blockquote>
<p><strong>好物最佳:</strong></p>
<blockquote>
<p>1.戴森V7</p>
<p>2.Asics 运动鞋</p>
<p>3.Airpods Pro</p>
</blockquote>
<h3 id="新的期许"><a href="#新的期许" class="headerlink" title="新的期许"></a>新的期许</h3><p>又到了新年 Flag 时间~</p>
<p>工作学习方面:</p>
<ol>
<li><p>提高英文输出能力,新年用英文写一篇技术博客</p>
</li>
<li><p>Leetcode Hit 300</p>
</li>
<li><p>补足 <a href="https://github.com/Colafornia/golang-exercise" target="_blank" rel="external">Golang 的练习项目</a></p>
</li>
<li><p>完成两门 CS 公开课(再学一门语言或者熟悉数据库吧)</p>
</li>
</ol>
<p>生活方面(和去年定的<strong>一模一样</strong>):</p>
<ol>
<li><p>阅读量 35 本+</p>
</li>
<li><p>至少去两个没去过的城市玩</p>
</li>
<li><p>体重减掉 5kg</p>
</li>
<li><p>培养一个新爱好(这条很虚,但又很必要)</p>
</li>
</ol>
<p><img src="https://s2.ax1x.com/2020/01/01/l8kcXq.jpg" alt=""></p>
<p><img src="/images/chengdu-2019.jpeg" alt="sunset roller coaster"></p>
<p>2019: to see the world as it is, and <em>to love it</em></p>
理解 Go 语言中的 Slice
https://blog.colafornia.me/post/2019/understand-golang-slice/
2019-11-05T01:30:00.000Z
2019-11-05T03:31:42.858Z
<p><img src="/images/go-slice.png" alt="go-slice"></p>
<p>数组是面向过程的编程语言里最重要的概念之一。</p>
<a id="more"></a>
<h2 id="一、翻车现场"><a href="#一、翻车现场" class="headerlink" title="一、翻车现场"></a>一、翻车现场</h2><p>在短暂的 Go 语言学习使用中,已经连续两次踩进了下面这个坑</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div></pre></td><td class="code"><pre><div class="line">vals := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">5</span>)</div><div class="line"></div><div class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i < <span class="number">5</span>; i++ {</div><div class="line"> vals = <span class="built_in">append</span>(vals, i)</div><div class="line">}</div><div class="line"></div><div class="line">fmt.Println(vals)</div><div class="line"></div><div class="line"><span class="comment">// Playground: https://play.golang.org/p/7PgUqBdZ6Z</span></div></pre></td></tr></table></figure>
<p>输出结果是 <code>[0 0 0 0 0 0 1 2 3 4]</code></p>
<p><strong>回顾一下翻车现场:</strong></p>
<ol>
<li><p>开始是通过 <code>var vals []int</code> 来进行变量声明</p>
</li>
<li><p>后续逻辑开发完成,自测完成,一切 ok,发布上线,一切 ok</p>
</li>
<li><p>某天在项目里跑了一下 <code>golangci-lint</code>,发现了一吨报错,其中有:</p>
</li>
</ol>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">Consider preallocating <span class="string">`vals`</span> (prealloc)</div><div class="line"> <span class="keyword">var</span> vals []<span class="keyword">int</span></div><div class="line"> ^</div></pre></td></tr></table></figure>
<ol>
<li><p>遂修改为 <code>vals := make([]int, 5)</code>,再改完那一吨报错,基本都是关于写法的修正,lint 不再报错,提交发布</p>
</li>
<li><p>线上,Duang💥💥💥(看用在哪里啦。。很有可能也不会💥,只会是一个隐秘的 Bug)</p>
</li>
</ol>
<p>挺刺激的。第一次遇到这个问题的时候其实已经有个印象不能这么声明 slice,再 append 了,但还是被打的不够疼,又有了第二次。</p>
<p>所以还是来学习理解一下具体是怎么回事吧。</p>
<h2 id="二、Array-vs-Slice"><a href="#二、Array-vs-Slice" class="headerlink" title="二、Array vs Slice"></a>二、Array vs Slice</h2><p>Go 语言中有数组 array,也有切片 slice。二者有很多区别,但是本次想讨论的主要是,<strong>array 的大小是固定的且不会改变,其 length 就是其类型的一部分,而 slice 的大小是动态变化的</strong>,因为 slice 是 array 的 <strong>wrapper</strong>。</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line">aArray := [<span class="number">2</span>]<span class="keyword">string</span>{<span class="string">"Tom"</span>, <span class="string">"Jerry"</span>}</div><div class="line">fmt.Printf(<span class="string">"%T\n"</span>, aArray) <span class="comment">// Print "[2]string"</span></div></pre></td></tr></table></figure>
<p>所以说,声明一个数组:<code>var a [10]int</code> a 的大小就已经固定,不会改变。调用 <code>len(a)</code> 的结果永远为 10。如果想要再向数组里添加、删除任何元素,都必须要重新声明一个新的类型。</p>
<p>这种固定大小的数组设计在某些场景下是非常有用的(官方文档的举例是矩阵变换),但是大多数情况下,工程中还是需要一个可变大小可增删的数组容器。而这就是设计 Slice 的原因。</p>
<p>切片 Slice 是基于数组实现的,它描述了数组的一个连续片段,所以叫切片还是很生动形象的。切片其实就是动态数组,它的长度并不固定,可以追加元素并会在切片容量不足时进行扩容。</p>
<p><img src="http://static.git-star.com/go-slices-usage-and-internals_slice-struct.png" alt="img"></p>
<p>如上图,<code>Slice</code> 的结构本质上是:</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">type</span> SliceHeader <span class="keyword">struct</span> {</div><div class="line"> Ptr <span class="keyword">uintptr</span></div><div class="line"> Len <span class="keyword">int</span></div><div class="line"> Cap <span class="keyword">int</span></div><div class="line">}</div></pre></td></tr></table></figure>
<p>Ptr 作为一个指针指向数组的第一个元素,其实就是指向一片连续的内存空间,这片内存空间可以用于存储切片中保存的全部元素,数组其实就是一片连续的内存空间,数组中的元素只是逻辑上的概念,底层存储其实都是连续的,所以我们可以将切片理解成一片连续的内存空间加上长度与容量标识。</p>
<p>Length 是 slice 中所包含的元素个数,而<code>Capacity</code>则记录了其底层数组的大小(从切片指针引用的元素开始,直到底层数组的最后一个元素)。因此,必然会有 <code>Capacity >= Length</code> 。试图把切片扩展到超出容量就会和访问超出数组/切片范围的索引一样,导致 panic。</p>
<p>因此如果我们声明 <code>slice := iBuffer[0:0]</code>,其 header 则为:</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line">slice := SliceHeader{</div><div class="line"> Len: <span class="number">0</span>,</div><div class="line"> Cap: <span class="number">10</span>,</div><div class="line"> Ptr: &iBuffer[<span class="number">0</span>],</div><div class="line">}</div></pre></td></tr></table></figure>
<p>引入 slice header 的这个概念非常有助于理解 slice 的特点和一些坑。</p>
<p>再来看看几种声明 slice 的方式:</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// 字面量</span></div><div class="line">letters := []<span class="keyword">string</span>{<span class="string">"a"</span>, <span class="string">"b"</span>, <span class="string">"c"</span>, <span class="string">"d"</span>}</div><div class="line"></div><div class="line"><span class="comment">// make</span></div><div class="line"><span class="function"><span class="keyword">func</span> <span class="title">make</span><span class="params">([]T, <span class="built_in">len</span>, <span class="built_in">cap</span>)</span> []<span class="title">T</span></span></div><div class="line"><span class="title">s</span> := <span class="title">make</span><span class="params">([]<span class="keyword">byte</span>, 5)</span></div><div class="line"></div><div class="line">// <span class="title">re</span>-<span class="title">slice</span></div><div class="line"><span class="title">s</span> = <span class="title">s</span>[2:4]</div></pre></td></tr></table></figure>
<p>第三种 re-slice 方式并没有拷贝原 slice 中的数据,它只是创建了一个新的切片,并将其指针指向同一个底层数组。</p>
<p>如果更改了 slice 中某些元素的值,实际上是在改变 slice 所指向的数组元素的值。因此,在下面这种通过 re-slice 生成了子切片,又通过索引修改元素值的行为,会导致所有指向这个底层数组的 slice 的值都发生改变。<strong>即 Slice 的结构会导致多个 slice 实际引用的是同一个数组,要谨慎修改 slice 中元素的值</strong>。</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div></pre></td><td class="code"><pre><div class="line">foo = <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">5</span>) <span class="comment">// 被初始化为 [0 0 0 0 0]</span></div><div class="line">foo[<span class="number">3</span>] = <span class="number">42</span></div><div class="line">foo[<span class="number">4</span>] = <span class="number">100</span></div><div class="line">bar := foo[<span class="number">1</span>:<span class="number">4</span>]</div><div class="line">bar[<span class="number">1</span>] = <span class="number">99</span></div><div class="line">fmt.Println(foo) <span class="comment">// [0 0 99 42 100]</span></div><div class="line">fmt.Println(bar) <span class="comment">// [0 99 42]</span></div></pre></td></tr></table></figure>
<h2 id="修改-Slice-中元素的值与-header"><a href="#修改-Slice-中元素的值与-header" class="headerlink" title="修改 Slice 中元素的值与 header"></a>修改 Slice 中元素的值与 header</h2><p>修改这两者的方法并不一样。</p>
<h3 id="Example1-将-slice-作为参数传入函数,修改-slice-中元素的值"><a href="#Example1-将-slice-作为参数传入函数,修改-slice-中元素的值" class="headerlink" title="Example1: 将 slice 作为参数传入函数,修改 slice 中元素的值"></a>Example1: 将 slice 作为参数传入函数,修改 slice 中元素的值</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">func</span> <span class="title">AddOneToEachElement</span><span class="params">(slice [])</span></span> {</div><div class="line"> <span class="keyword">for</span> i := <span class="keyword">range</span> slice {</div><div class="line"> slice[i]++</div><div class="line"> }</div><div class="line">}</div><div class="line"></div><div class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</div><div class="line"> slice := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">5</span>)</div><div class="line"> fmt.Println(<span class="string">"before"</span>, slice)</div><div class="line"> AddOneToEachElement(slice)</div><div class="line"> fmt.Println(<span class="string">"after"</span>, slice)</div><div class="line">}</div><div class="line"></div><div class="line"><span class="comment">// before [0 0 0 0 0]</span></div><div class="line"><span class="comment">// after [1 1 1 1 1]</span></div></pre></td></tr></table></figure>
<p>修改成功。虽然作为函数参数所传递的只是 slice 的值,但是其实在函数中我们所操作修改的是指向同一底层数组的 <code>newSlice</code>,底层数组的元素被修改,因此当函数执行完毕,修改后的元素值可由原 slice 访问到。</p>
<h3 id="Example2-将-slice-作为参数传入函数,修改-slice-的长度"><a href="#Example2-将-slice-作为参数传入函数,修改-slice-的长度" class="headerlink" title="Example2: 将 slice 作为参数传入函数,修改 slice 的长度"></a>Example2: 将 slice 作为参数传入函数,修改 slice 的长度</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">func</span> <span class="title">SubtractOneFromLength</span><span class="params">(slice []<span class="keyword">int</span>)</span> []<span class="title">int</span></span> {</div><div class="line"> slice = slice[<span class="number">0</span> : <span class="built_in">len</span>(slice)<span class="number">-1</span>]</div><div class="line"> <span class="keyword">return</span> slice</div><div class="line">}</div><div class="line"></div><div class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</div><div class="line"> slice := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">5</span>)</div><div class="line"> fmt.Println(<span class="string">"Before: len(slice) ="</span>, <span class="built_in">len</span>(slice))</div><div class="line"> newSlice := SubtractOneFromLength(slice)</div><div class="line"> fmt.Println(<span class="string">"After: len(slice) ="</span>, <span class="built_in">len</span>(slice))</div><div class="line"> fmt.Println(<span class="string">"After: len(newSlice) ="</span>, <span class="built_in">len</span>(newSlice))</div><div class="line">}</div><div class="line"></div><div class="line"><span class="comment">// Before: len(slice) = 5</span></div><div class="line"><span class="comment">// After: len(slice) = 5</span></div><div class="line"><span class="comment">// After: len(newSlice) = 4</span></div></pre></td></tr></table></figure>
<p>储存在 slice header 中的 length 属性并不会被函数修改,传入函数的只是其拷贝。 如果我们想通过一个函数来修改 slice 的 header 的话,必须通过把修改过的返回结果重新赋值给待修改的 slice。</p>
<h3 id="Example3-将-slice-的指针作为参数传入函数,修改其-header"><a href="#Example3-将-slice-的指针作为参数传入函数,修改其-header" class="headerlink" title="Example3: 将 slice 的指针作为参数传入函数,修改其 header"></a>Example3: 将 slice 的指针作为参数传入函数,修改其 header</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">func</span> <span class="title">PtrSubtractOneFromLength</span><span class="params">(slicePtr *[]<span class="keyword">int</span>)</span></span> {</div><div class="line"> slice := *slicePtr</div><div class="line"> *slicePtr = slice[<span class="number">0</span> : <span class="built_in">len</span>(slice)<span class="number">-1</span>]</div><div class="line">}</div><div class="line"></div><div class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</div><div class="line"> slice := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">5</span>)</div><div class="line"> fmt.Println(<span class="string">"Before: len(slice) ="</span>, <span class="built_in">len</span>(slice))</div><div class="line"> PtrSubtractOneFromLength(&slice)</div><div class="line"> fmt.Println(<span class="string">"After: len(slice) ="</span>, <span class="built_in">len</span>(slice))</div><div class="line">}</div><div class="line"></div><div class="line"><span class="comment">// Before: len(slice) = 5</span></div><div class="line"><span class="comment">// After: len(slice) = 4</span></div></pre></td></tr></table></figure>
<h2 id="三、Append"><a href="#三、Append" class="headerlink" title="三、Append"></a>三、Append</h2><p>内置的 <code>append</code>方法有这几个功能特点:</p>
<ol>
<li>向 slice 附加一个或多个元素</li>
<li>分配足够大的 slice (如果 append 后的长度超出了现有的容量)</li>
<li>总会成功,除非机器内存耗光</li>
</ol>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// 这是官方实现的用来说明功能特性的版本,并不是源码</span></div><div class="line"><span class="comment">// Append appends the elements to the slice.</span></div><div class="line"><span class="comment">// Efficient version.</span></div><div class="line"><span class="function"><span class="keyword">func</span> <span class="title">Append</span><span class="params">(slice []<span class="keyword">int</span>, elements ...<span class="keyword">int</span>)</span> []<span class="title">int</span></span> {</div><div class="line"> n := <span class="built_in">len</span>(slice)</div><div class="line"> total := <span class="built_in">len</span>(slice) + <span class="built_in">len</span>(elements)</div><div class="line"> <span class="keyword">if</span> total > <span class="built_in">cap</span>(slice) {</div><div class="line"> <span class="comment">// Reallocate. Grow to 1.5 times the new size, so we can still grow.</span></div><div class="line"> <span class="comment">// 一次分配更多的内存通常都比多次分配少量内存的开销更小且速度更快</span></div><div class="line"> newSize := total*<span class="number">3</span>/<span class="number">2</span> + <span class="number">1</span></div><div class="line"> newSlice := <span class="built_in">make</span>([]<span class="keyword">int</span>, total, newSize)</div><div class="line"> <span class="built_in">copy</span>(newSlice, slice)</div><div class="line"> slice = newSlice</div><div class="line"> }</div><div class="line"> slice = slice[:total]</div><div class="line"> <span class="built_in">copy</span>(slice[n:], elements)</div><div class="line"> <span class="keyword">return</span> slice</div><div class="line">}</div></pre></td></tr></table></figure>
<h2 id="四、回顾一下翻车现场"><a href="#四、回顾一下翻车现场" class="headerlink" title="四、回顾一下翻车现场"></a>四、回顾一下翻车现场</h2><p>回到我们一开始演示翻车现场的代码示例:</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line">vals := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">5</span>)</div><div class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i < <span class="number">5</span>; i++ {</div><div class="line"> vals = <span class="built_in">append</span>(vals, i)</div><div class="line">}</div><div class="line">fmt.Println(vals)</div></pre></td></tr></table></figure>
<p>在声明 <code>vals</code> 的时候,把它的 <code>length</code> 和 <code>capacity</code>都设置为5,它被初始化为空值 slice : <code>[0 0 0 0 0]</code>,再进行 append 操作,它假设我们是想在初始的 5 个元素后添加新元素,因此得到的结果是 <code>[0 0 0 0 0 0 1 2 3 4]</code>。</p>
<p>那在 golang-lint 要求我们必须通过 <code>prealloc</code> 的方式初始化 vals,或是我们为了避免每个循环中都 re-allocate 造成的过大开销,或是我们并不能确定执行<code>append</code> 后的切片大小的情况下,要怎么避免这种翻车情况呢?</p>
<p>有两种方式:</p>
<h3 id="1-直接通过索引对切片元素赋值(只针对遍历操作后的数组大小已知的情况)"><a href="#1-直接通过索引对切片元素赋值(只针对遍历操作后的数组大小已知的情况)" class="headerlink" title="1. 直接通过索引对切片元素赋值(只针对遍历操作后的数组大小已知的情况)"></a>1. 直接通过索引对切片元素赋值(只针对遍历操作后的数组大小已知的情况)</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line">vals := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">5</span>)</div><div class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i < <span class="number">5</span>; i++ {</div><div class="line"> vals[i] = i</div><div class="line">}</div><div class="line">fmt.Println(vals)</div></pre></td></tr></table></figure>
<p>这种方案并不难理解。</p>
<h3 id="2-设置长度为-0,指定切片的-Capacity"><a href="#2-设置长度为-0,指定切片的-Capacity" class="headerlink" title="2. 设置长度为 0,指定切片的 Capacity"></a>2. 设置长度为 <code>0</code>,指定切片的 Capacity</h3><p>并不是所有场景下我们都可以精确一一对应到每个索引下该存储的元素是什么,比如说:</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div></pre></td><td class="code"><pre><div class="line">vals := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">5</span>)</div><div class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i < <span class="number">5</span>; i++ {</div><div class="line"> items := getItems(i)</div><div class="line"> vals = <span class="built_in">append</span>(vals, items...)</div><div class="line">}</div><div class="line">fmt.Println(vals)</div></pre></td></tr></table></figure>
<p>每次将一个不定长度的 slice 铺平追加到切片,最终得到的切片的长度无法提前确定。</p>
<p>那只能通过设置长度为 <code>0</code>,指定切片的容量来解决了。即:</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div></pre></td><td class="code"><pre><div class="line">vals := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">0</span>, <span class="number">5</span>)</div><div class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i < <span class="number">5</span>; i++ {</div><div class="line"> items := getItems(i)</div><div class="line"> vals = <span class="built_in">append</span>(vals, items...)</div><div class="line">}</div><div class="line">fmt.Println(vals)</div></pre></td></tr></table></figure>
<p>当然我们初始化时指定的容量5很有可能比最终切片长度小,循环过程中还是会发生 re-allocate,但这样已经避免了前面的翻车现场,也减少了 re-allocate 次数,可以作为这种场景下的折中解决方案了。</p>
<h2 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h2><ul>
<li><a href="https://blog.golang.org/slices" target="_blank" rel="external">Arrays, slices (and strings): The mechanics of ‘append’</a></li>
<li><a href="https://blog.golang.org/go-slices-usage-and-internals" target="_blank" rel="external">Go Slices: usage and internals</a></li>
<li><a href="https://www.calhoun.io/how-to-use-slice-capacity-and-length-in-go/" target="_blank" rel="external">How to use slice capacity and length in Go</a></li>
</ul>
<p><img src="/images/go-slice.png" alt="go-slice"></p>
<p>数组是面向过程的编程语言里最重要的概念之一。</p>
用 Cypress 拯救业务项目的前端自动化测试
https://blog.colafornia.me/post/2019/e2e-test-cypress/
2019-10-24T02:00:00.000Z
2019-10-25T03:13:12.557Z
<p><img src="/images/cypress.png" alt="cypress-cover"></p>
<p>关于前端测试的一些理论与基于 <code>Cypress</code> 的 E2E 测试具体实践。</p>
<a id="more"></a>
<h2 id="关于前端自动化测试的一些碎碎念"><a href="#关于前端自动化测试的一些碎碎念" class="headerlink" title="关于前端自动化测试的一些碎碎念"></a>关于前端自动化测试的一些碎碎念</h2><p>日常业务项目开发的痛点之一便是前端的<code>回归测试</code>,免不了各种手动点点点,但凡改动了某个公用组件,函数,都要漫山遍野地把项目的主要页面都点进去看一遍有没有问题。项目用了 <code>GraphQL</code> 的话,Schema 一个更新不及时,某个没注意到的页面就挂了,然后就等着开 issue 或者报线上 Bug 吧 😐</p>
<p>通过人工手动点点点不仅是累,也并不靠谱,没法保证每一次都测到了需要回归测试的功能。想解决这一痛点,就不得不提<code>前端的自动化测试</code>。通过命令行跑测试,集成 CI 自动测试岂不美滋滋。</p>
<p>然而,国内各厂对于前端自动化测试尚未形成很好的实践,说起自动化测试,大家想到的也还是后端测试。几次技术大会(JSConf、GMTC 等等)里关于前端测试的话题也是寥寥无几,有的话也是国外前端工程师的分享,或是 QA 关于搭建测试平台的分享。</p>
<p>在我的经验里,<strong>对于业务项目而言的前端测试</strong>都很“尴尬”。如果是开发工具库,那可以通过单元测试来保证质量,如果是开发 UI 组件库,可以通过 <a href="https://storybook.js.org/" target="_blank" rel="external">Storybook</a> 来进行视觉与快照测试。所以之前每每想到业务项目如何集成自动化测试都感觉无从下手(还有几次是被比业务代码还多的测试代码吓跑了)。</p>
<p>但是业务项目里并没有太多工具函数需要单元测试(大部分通过 lodash 或其他第三方库来解决复杂逻辑处理),UI 组件也基本是直接采用了业界比较成熟的 ant-mobile, ant-design 等方案,需要在业务项目中开发的 UI 组件并不多,大多数是在第三方的 UI 组件基础上结合业务逻辑进行二次封装(事实上 ant-design 也把自己的组件单独放到 <a href="https://github.com/react-component" target="_blank" rel="external">https://github.com/react-component</a> 里维护了)。</p>
<p>业务项目里需要自动化测试的场景主要是想覆盖用户的主要使用路径,例如登录注册,加购到购物车,查看操作订单,修改个人信息等等,都是与 UI 界面的渲染逻辑强相关的,需要测试这些页面的表单提交,自动跳转,数据渲染是否有异常。</p>
<p>所以在此可以梳理一下我们的需求是:</p>
<blockquote>
<ol>
<li>可以模拟用户的点击输入操作,事件驱动来验证页面渲染是否符合预期</li>
<li>可以使用命令行跑测试,可以集成到 CI</li>
<li>轻量高效,环境易搭建,测试代码易编写(毕竟是作为对敏捷开发,持续集成的环节补充,并不是 QA 环节的测试,不应舍本逐末)</li>
</ol>
</blockquote>
<p>想到这里不难发现,前端业务项目里最需要的是 E2E 测试,但是在如题图的测试金字塔所示,E2E 测试在金字塔顶端,执行 E2E 测试成本高又速度慢。因此 <a href="https://github.com/cypress-io/cypress" target="_blank" rel="external">Cypress</a> 应运而生,Cypress 提供了完备的解决方案,从测试金字塔顶端的 E2E 到集成测试再到单元测试都实现。</p>
<h2 id="Cypress"><a href="#Cypress" class="headerlink" title="Cypress"></a>Cypress</h2><p>Cypress 是在 Mocha API 的基础上开发的一套开箱即用的 E2E 测试框架,并不依赖前端框架,也无需其他测试工具库,配置简单,并且提供了强大的 GUI 图形工具,可以自动截图录屏,实现时空旅行并在测试流程中 Debug 等等。</p>
<p>总结一下,Cypress 的优点有:</p>
<blockquote>
<ol>
<li>配置简单,可快速集成到现有项目中</li>
<li>支持所有等级的测试(即前面所提到的 e2e 测试,集成测试,单元测试等)</li>
<li>可以给每一步测试都生成快照,易于 Debug</li>
<li>可以获取、操作 Web 页面里的所有 DOM 节点</li>
<li>自动重试功能,Cypress 会在当前节点重试几次再断定测试失败</li>
<li>易于集成到 CI 系统中</li>
</ol>
</blockquote>
<p>与其它类似测试工具如 Selenium、Puppeteer、Nightwatch 相比,Cypress 的测试代码语法更简单,并且在保证了框架的轻量高效的前提下,对前端工程师更友好。</p>
<p><img src="https://tva1.sinaimg.cn/large/007X8olVly1g89haxufrqj317k0u0dwx.jpg" alt="cypress-gui"></p>
<video autoplay loop src="https://www.cypress.io/static/running-tests-a75997cdc1013fc4b1705c1be3a094c7.webm"></video>
<p>简单介绍一下使用方法(具体可以参照<a href="https://docs.cypress.io/guides/overview/why-cypress.html" target="_blank" rel="external">官网引导</a>):</p>
<p>安装:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">yarn add cypress --dev</div></pre></td></tr></table></figure>
<p>添加到项目的 npm 脚本中:</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line">{</div><div class="line"> <span class="attr">"scripts"</span>: {</div><div class="line"> <span class="attr">"cypress:open"</span>: <span class="string">"cypress open"</span></div><div class="line"> }</div><div class="line">}</div></pre></td></tr></table></figure>
<p>根目录里配置 <code>cypress.json</code>:</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line">{</div><div class="line"> "baseUrl": "http://localhost:8080", // 本地启动的 webpack-dev-server 地址</div><div class="line"> "viewportHeight": 800, // 测试环境的页面视口高度</div><div class="line"> "viewportWidth": 1280 // 测试环境的页面视口宽度</div><div class="line">}</div></pre></td></tr></table></figure>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">npm run cypress:open</div></pre></td></tr></table></figure>
<p>这就已经在本地打开了测试 GUI,可以进行测试了。</p>
<p>用官方文档的一个例子说明一下测试代码怎么写:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div></pre></td><td class="code"><pre><div class="line">describe(<span class="string">'My First Test'</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</div><div class="line"> it(<span class="string">'Gets, types and asserts'</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</div><div class="line"> cy.visit(<span class="string">'https://example.cypress.io'</span>)</div><div class="line"></div><div class="line"> cy.contains(<span class="string">'type'</span>).click()</div><div class="line"></div><div class="line"> <span class="comment">// Should be on a new URL which includes '/commands/actions'</span></div><div class="line"> cy.url().should(<span class="string">'include'</span>, <span class="string">'/commands/actions'</span>)</div><div class="line"></div><div class="line"> <span class="comment">// Get an input, type into it and verify that the value has been updated</span></div><div class="line"> cy.get(<span class="string">'.action-email'</span>)</div><div class="line"> .type(<span class="string">'fake@email.com'</span>)</div><div class="line"> .should(<span class="string">'have.value'</span>, <span class="string">'fake@email.com'</span>)</div><div class="line"> })</div><div class="line">})</div></pre></td></tr></table></figure>
<video autoplay loop src="https://docs.cypress.io/img/snippets/first-test-assertions-30fps.1fbd2a2d.mp4"></video>
<p>这其实已经测试了:</p>
<ol>
<li>打开目标页面,这里是示例的 <a href="https://example.cypress.io," target="_blank" rel="external">https://example.cypress.io,</a> 实际上在项目里应是本地启动的 server 页面如 <a href="http://localhost:8080" target="_blank" rel="external">http://localhost:8080</a></li>
<li>找到页面的 dom 里文字内容为 ‘type’ 的按钮,点击(如果页面里并没有渲染这个按钮即测试没跑通)</li>
<li>按钮点击后,页面应跳转到了路由中包含 ‘/commands/actions’ 的页面</li>
<li>在此页面的 dom 里可以找到 class 类名为 ‘.action-email’ 的 input 框,在里面输入 ‘fake@email.com’ 后,输入框的 value 值应该为 ‘fake@email.com’</li>
</ol>
<p>测试代码语义化比较好,代码量不多,也不需要写很多 async 逻辑。</p>
<p>到这里为止体验了一下安装配置,本地测试,感觉还可以,功能丰富,上手比较简单,集成到项目里也不麻烦。</p>
<h2 id="测试覆盖率与持续集成(Gitlab-为例)"><a href="#测试覆盖率与持续集成(Gitlab-为例)" class="headerlink" title="测试覆盖率与持续集成(Gitlab 为例)"></a>测试覆盖率与持续集成(Gitlab 为例)</h2><p>凡是没有集成到 CI 里的测试都只是玩具,并不能算数。所以我们来看看 Cypress 这块的表现吧。</p>
<p>我们希望 Cypress 可以通过配置,在开发的不同阶段执行不同的测试命令。比如在发起 PR 到 feature 分支时可以在当前分支执行集成测试,到 master 主分支时还需计算测试覆盖率并将数据上报到 Sonar 等质检平台(还可以设置测试覆盖率不满足xx%的话则测试失败等等)。</p>
<p>因此我们先看看测试覆盖率要怎么计算。Cypress 的测试覆盖率计算貌似是后来才添加上的功能,配置稍有点复杂。</p>
<p>依然还是具体说明可以<a href="https://docs.cypress.io/guides/tooling/code-coverage.html#E2E-code-coverage" target="_blank" rel="external">参照文档</a>,博客中只是简单介绍一下:</p>
<p>首先安装依赖:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">npm install -D @cypress/code-coverage nyc istanbul-lib-coverage</div></pre></td></tr></table></figure>
<p>再配置一下 Cypress 中的配置:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// cypress/support/index.js</span></div><div class="line"><span class="keyword">import</span> <span class="string">'@cypress/code-coverage/support'</span></div><div class="line"></div><div class="line"><span class="comment">// cypress/plugins/index.js</span></div><div class="line"><span class="built_in">module</span>.exports = (on, config) => {</div><div class="line"> on(<span class="string">'task'</span>, <span class="built_in">require</span>(<span class="string">'@cypress/code-coverage/task'</span>))</div><div class="line">}</div></pre></td></tr></table></figure>
<p>文档只介绍到这里,如果项目用了 TypeScript 的话这就还远远不够,翻了一下官方的 github 示例才发现还需要几个步骤:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">npm i -D babel-plugin-istanbul</div></pre></td></tr></table></figure>
<p>设置一下 <code>.babelrc</code></p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">{</div><div class="line"> <span class="attr">"plugins"</span>: [<span class="string">"istanbul"</span>]</div><div class="line">}</div></pre></td></tr></table></figure>
<p>再修改一下 <code>cypress/plugins/index.js</code></p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// cypress/plugins/index.js</span></div><div class="line"><span class="built_in">module</span>.exports = (on, config) => {</div><div class="line"> on(<span class="string">'task'</span>, <span class="built_in">require</span>(<span class="string">'@cypress/code-coverage/task'</span>))</div><div class="line"> on(<span class="string">'file:preprocessor'</span>, <span class="built_in">require</span>(<span class="string">'@cypress/code-coverage/use-babelrc'</span>))</div><div class="line">}</div></pre></td></tr></table></figure>
<figure class="highlight"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">"cy:run": "cypress run && npm run test:report",</div><div class="line">"instrument": "nyc instrument --compact=false client instrumented",</div><div class="line">"test:report": "npm run instrument && npx nyc report --reporter=text-summary",</div></pre></td></tr></table></figure>
<p>通过 <code>cypress run</code> 可以直接在命令行跑测试,不启动 GUI,在 CI 里使用的话就该用这个命令。</p>
<p>看看结果,真是快快乐乐。</p>
<p><img src="https://tva1.sinaimg.cn/large/007X8olVly1g89hawy7ymj30gn034glt.jpg" alt="coverage-result"></p>
<p>Cypress 的 E2E 测试的覆盖率也可以和单元测试,或是通过其它框架 Jest 等的测试覆盖率进行合并,具体方法可以去官网查找。</p>
<p>下面我们来以 Gitlab CI runner 为例来看一下 Cypress 怎么集成到 CI:</p>
<figure class="highlight yml"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div><div class="line">44</div><div class="line">45</div><div class="line">46</div><div class="line">47</div><div class="line">48</div><div class="line">49</div><div class="line">50</div></pre></td><td class="code"><pre><div class="line">// .gitlab-ci.yml</div><div class="line"><span class="attr">variables:</span></div><div class="line"><span class="attr"> npm_config_cache:</span> <span class="string">"$CI_PROJECT_DIR/.npm"</span></div><div class="line"><span class="attr"> CYPRESS_CACHE_FOLDER:</span> <span class="string">"$CI_PROJECT_DIR/cache/Cypress"</span></div><div class="line"></div><div class="line"><span class="attr">stages:</span></div><div class="line"><span class="bullet"> -</span> test</div><div class="line"><span class="bullet"> -</span> sonar</div><div class="line"></div><div class="line"><span class="attr">cache:</span></div><div class="line"><span class="attr"> paths:</span></div><div class="line"><span class="bullet"> -</span> .npm</div><div class="line"><span class="bullet"> -</span> node_modules/</div><div class="line"><span class="bullet"> -</span> cache/Cypress</div><div class="line"></div><div class="line"><span class="attr">build:</span></div><div class="line"><span class="attr"> stage:</span> sonar</div><div class="line"><span class="attr"> tags:</span></div><div class="line"><span class="bullet"> -</span> docker</div><div class="line"><span class="attr"> script:</span></div><div class="line"><span class="bullet"> -</span> yarn</div><div class="line"><span class="bullet"> -</span> sh ci/sonar.sh</div><div class="line"><span class="bullet"> -</span> yarn build</div><div class="line"><span class="attr"> artifacts:</span></div><div class="line"><span class="attr"> expire_in:</span> <span class="number">7</span> day</div><div class="line"><span class="attr"> paths:</span></div><div class="line"><span class="bullet"> -</span> codeclimate.json</div><div class="line"><span class="bullet"> -</span> build</div><div class="line"></div><div class="line"><span class="attr">cypress-e2e-local:</span></div><div class="line"><span class="attr"> image:</span> cypress/base:<span class="number">10</span></div><div class="line"><span class="attr"> tags:</span></div><div class="line"><span class="bullet"> -</span> docker</div><div class="line"><span class="attr"> stage:</span> test</div><div class="line"><span class="attr"> script:</span></div><div class="line"><span class="bullet"> -</span> unset NODE_OPTIONS</div><div class="line"><span class="bullet"> -</span> yarn</div><div class="line"><span class="bullet"> -</span> $(npm bin)/cypress cache path</div><div class="line"> <span class="comment"># show all installed versions of Cypress binary</span></div><div class="line"><span class="bullet"> -</span> $(npm bin)/cypress install</div><div class="line"><span class="bullet"> -</span> $(npm bin)/cypress cache list</div><div class="line"><span class="bullet"> -</span> $(npm bin)/cypress verify</div><div class="line"><span class="bullet"> -</span> npm run test</div><div class="line"><span class="attr"> artifacts:</span></div><div class="line"><span class="attr"> expire_in:</span> <span class="number">1</span> week</div><div class="line"><span class="attr"> when:</span> always</div><div class="line"><span class="attr"> paths:</span></div><div class="line"><span class="bullet"> -</span> coverage/lcov.info</div><div class="line"><span class="bullet"> -</span> cypress/screenshots</div><div class="line"><span class="bullet"> -</span> cypress/videos</div></pre></td></tr></table></figure>
<p>在这里我们设置了两个 CI 阶段,test 与 build(与 Sonar 扫描,数据上报等),在 test 阶段中使用了 Cypress 的官方镜像 <code>cypress/base:10</code>。(其它环境变量设置和依赖如 Sonar 扫描,yarn 等都在我们自己的 Docker 镜像中)</p>
<p>其中 CI 所执行的命令 <code>npm run test</code> 是:</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">"test": "start-server-and-test start http://localhost:5000 cy:run"</div></pre></td></tr></table></figure>
<p>在这里为了简化命令,使用了 npm 包 <a href="https://github.com/bahmutov/start-server-and-test" target="_blank" rel="external">start-server-and-test</a> 来实现待本地 Server 启动之后再执行测试这一逻辑。</p>
<p>我们也在 <code>.gitlab-ci.yml</code> 中设置了 <code>artifacts</code>:</p>
<figure class="highlight yml"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div></pre></td><td class="code"><pre><div class="line"><span class="attr">artifacts:</span></div><div class="line"><span class="attr"> expire_in:</span> <span class="number">1</span> week</div><div class="line"><span class="attr"> when:</span> always</div><div class="line"><span class="attr"> paths:</span></div><div class="line"><span class="bullet"> -</span> coverage/lcov.info</div><div class="line"><span class="bullet"> -</span> cypress/screenshots</div><div class="line"><span class="bullet"> -</span> cypress/videos</div></pre></td></tr></table></figure>
<p>这是 Gitlab 的 <a href="https://docs.gitlab.com/ee/user/project/pipelines/job_artifacts.html" target="_blank" rel="external">job artifacts</a> 功能,可以设置在某一步骤完成之后将特定文件夹的内容上传到服务器,在有效时间内,我们可以在网页端查看或下载这些文件内容。这样如果在 CI 测试失败的话我们就可以在 artifacts 中查看其测试失败视频和快照,避免盲猜式 Debug。</p>
<p><img src="https://tva1.sinaimg.cn/large/007X8olVly1g89hawui46j307o03ldfu.jpg" alt="artifacts"></p>
<p>在 CI 设置和测试用例管理中可以深挖的点还有很多。比如将测试用例分为冒烟测试,全量测试,或者 Client 端测试,Node 层测试等等。</p>
<p>Cypress 可应用的测试场景也更多,比如通过设置 Cookie 实现不同权限用户的测试,引入 Chance.js 实现随机点击 Tab 进行不同选项卡的测试,Mock 接口返回值等等。</p>
<p><a href="https://glebbahmutov.com/blog/" target="_blank" rel="external">https://glebbahmutov.com/blog/</a> 是 Cypress 的主要维护者的博客,其中也记录了很多骚操作(比如检查网页对比度是否满足条件等等),如有兴趣,可以继续进行挖掘。</p>
<h2 id="一个坑点"><a href="#一个坑点" class="headerlink" title="一个坑点"></a>一个坑点</h2><p>在我的实践中发现的一个坑点是 <a href="https://github.com/cypress-io/cypress/issues/95" target="_blank" rel="external">Cypress 缺少对于 <code>fetch</code> 请求的支持</a>,它无法捕捉或者 mock 请求,只能通过一个有点脏的方法来 hack 解决。</p>
<p>在项目中引入<a href="https://github.com/whatwg/fetch" target="_blank" rel="external">whatwg-fetch</a>,再修改 <code>cypress/support/command.js</code>:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// cypress/support/command.js</span></div><div class="line">Cypress.Commands.add(<span class="string">'visitWithDelWinFetch'</span>, (path, opts = {}) => {</div><div class="line"> cy.visit(</div><div class="line"> path,</div><div class="line"> <span class="built_in">Object</span>.assign(opts, {</div><div class="line"> onBeforeLoad(win) {</div><div class="line"> <span class="keyword">delete</span> win.fetch;</div><div class="line"> },</div><div class="line"> })</div><div class="line"> );</div><div class="line">});</div></pre></td></tr></table></figure>
<p>这样我们就可以测试我们项目的登录重定向判断了:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div></pre></td><td class="code"><pre><div class="line">describe(<span class="string">'Node server'</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</div><div class="line"></div><div class="line"> it(<span class="string">'no cookie get 401'</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</div><div class="line"> cy.server()</div><div class="line"></div><div class="line"> cy.clearCookies()</div><div class="line"></div><div class="line"> cy.route(<span class="string">'POST'</span>, <span class="string">'**/graphql'</span>).as(<span class="string">'login'</span>)</div><div class="line"></div><div class="line"> cy.visitWithDelWinFetch(<span class="string">'/'</span>);</div><div class="line"></div><div class="line"> cy.wait(<span class="string">'@login'</span>).then((xhr) => {</div><div class="line"> expect(xhr.status).to.eq(<span class="number">401</span>)</div><div class="line"> })</div><div class="line"> })</div><div class="line"></div><div class="line"> it(<span class="string">'with cookie get 200'</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</div><div class="line"> cy.server()</div><div class="line"></div><div class="line"> cy.route(<span class="string">'POST'</span>, <span class="string">'**/graphql'</span>).as(<span class="string">'loginWithCookie'</span>)</div><div class="line"></div><div class="line"> cy.visitWithCookie(<span class="string">'/'</span>);</div><div class="line"></div><div class="line"> cy.wait(<span class="string">'@loginWithCookie'</span>).then((xhr) => {</div><div class="line"> expect(xhr.status).to.eq(<span class="number">200</span>)</div><div class="line"> })</div><div class="line"> <span class="comment">// login successfully, so display the content</span></div><div class="line"> cy.get(<span class="string">'.ant-layout-sider'</span>)</div><div class="line"> cy.get(<span class="string">'.ant-layout-content'</span>)</div><div class="line"> })</div><div class="line">})</div></pre></td></tr></table></figure>
<p>aha~</p>
<p><img src="https://tva1.sinaimg.cn/large/007X8olVly1g89hawob9rj31180560ta.jpg" alt="sonar"></p>
<p><img src="/images/cypress.png" alt="cypress-cover"></p>
<p>关于前端测试的一些理论与基于 <code>Cypress</code> 的 E2E 测试具体实践。</p>
2019.3 Webpack 升级改造小记
https://blog.colafornia.me/post/2019/2019-webpack-optimization/
2019-03-31T06:00:00.000Z
2019-10-24T02:43:40.140Z
<p><img src="/images/2019-3-webpack.png" alt="webpack-cover"></p>
<blockquote>
<p>时间:2019.3</p>
<p>Webpack 稳定版本为4, 正在 5 的 roadmap 中</p>
<p>记录一下当前时间对前端项目构建打包优化的策略</p>
</blockquote>
<a id="more"></a>
<h2 id="当前项目问题"><a href="#当前项目问题" class="headerlink" title="当前项目问题"></a>当前项目问题</h2><p>我们的前端项目基本都是使用的是 <a href="https://github.com/facebook/create-react-app" target="_blank" rel="external">create-react-app</a> 的配置,冗余项太多,升级难。</p>
<p>在这个前提下,有个“极限项目”每次代码改动之后的 hot reload 都需要 30s,实在坐不住了,就动手做了打包升级。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V1dq29.png" alt="before"></p>
<p>这次是从代码依赖相对简单,后续测试回滚负担小的后台管理系统下手的。</p>
<p>总结下当前的痛点为:</p>
<ol>
<li>项目启动慢</li>
<li>Hot reload 慢</li>
<li>Build 慢(上线前在后端项目里 build 时尤其明显)</li>
<li>没分包,文件体积太大</li>
</ol>
<h2 id="解决手段"><a href="#解决手段" class="headerlink" title="解决手段"></a>解决手段</h2><p>首先把 Webpack 版本从 3 升到 4,起码先享受上工具本身升级后带来的优越性</p>
<h3 id="1-分离配置文件(与性能无关,与开发维护体验有关)"><a href="#1-分离配置文件(与性能无关,与开发维护体验有关)" class="headerlink" title="1.分离配置文件(与性能无关,与开发维护体验有关)"></a>1.分离配置文件(与性能无关,与开发维护体验有关)</h3><p>分成 base、dev、prod 三个 config 文件</p>
<p>把通用配置放到 base 中,dev 与 prod 中只放与这两种模式强相关的配置</p>
<h3 id="2-HappyPack"><a href="#2-HappyPack" class="headerlink" title="2.HappyPack"></a>2.HappyPack</h3><p><img src="https://s2.ax1x.com/2019/06/01/V1db8J.png" alt="HappyPack"></p>
<p>治疗各种编译慢</p>
<p>本地启动 server 编译时间: 30s => 10s</p>
<h3 id="3-splitChunks-分包"><a href="#3-splitChunks-分包" class="headerlink" title="3.splitChunks 分包"></a>3.splitChunks 分包</h3><p><img src="https://s2.ax1x.com/2019/06/01/V1dXK1.png" alt="splitChunks"></p>
<p>这一功能在之前版本中是通过 <a href="https://webpack.js.org/plugins/commons-chunk-plugin/" target="_blank" rel="external">CommonsChunkPlugin</a> 来进行的。</p>
<p>初步分成三个包,减小 main 包体积。</p>
<p>根据业务特点,其实可以做懒加载,但是注意不能分太多,增加 http 请求数得不偿失。后台管理项目体积目前不大,各模块也没什么业务上的明显区分,就没啥做懒加载的必要。</p>
<p>结果:包体积:6M => 3M</p>
<h3 id="4-将第三方巨型包打入-externals"><a href="#4-将第三方巨型包打入-externals" class="headerlink" title="4.将第三方巨型包打入 externals"></a>4.将第三方巨型包打入 externals</h3><p>经过以上分包后,用 <a href="https://github.com/webpack-contrib/webpack-bundle-analyzer" target="_blank" rel="external">BundleAnalyzerPlugin</a> 看了下结果</p>
<p>发现有个非常显眼的巨型包 <code>echarts</code></p>
<p>把它的 min.js 扔到公司 cdn 上,在 html 中直接引入:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">externals: {</div><div class="line"> echarts: <span class="string">'echarts'</span></div><div class="line">}</div></pre></td></tr></table></figure>
<p>效果也很明显,当时没截图,就不贴对比图了</p>
<h3 id="5-gzip"><a href="#5-gzip" class="headerlink" title="5.gzip"></a>5.gzip</h3><p><img src="https://s2.ax1x.com/2019/06/01/V1dLvR.png" alt="after"></p>
<p>通过引入 CompressionWebpackPlugin 插件,打出来 .js.gz 资源,在服务器已支持 gzip 的情况下,所加载资源体积(直接到了 1.3M)和时间提升非常明显</p>
<h3 id="6-常规操作"><a href="#6-常规操作" class="headerlink" title="6.常规操作"></a>6.常规操作</h3><p>还有很多升到 webpack@4 之后的常规操作:</p>
<blockquote>
<p>MiniCssExtractPlugin</p>
<p>TerserPlugin</p>
<p>OptimizeCSSAssetsPlugin</p>
<p>等等</p>
</blockquote>
<p>随便搜搜,或者按图索骥去最新版的 create-react-app 源码里看看用了啥就行</p>
<p><img src="/images/2019-3-webpack.png" alt="webpack-cover"></p>
<blockquote>
<p>时间:2019.3</p>
<p>Webpack 稳定版本为4, 正在 5 的 roadmap 中</p>
<p>记录一下当前时间对前端项目构建打包优化的策略</p>
</blockquote>
2018 年度总结
https://blog.colafornia.me/post/2019/2018-review/
2019-01-02T04:00:00.000Z
2021-01-01T15:30:36.567Z
<p><img src="/images/chengdu-2018.jpeg" alt="chengdu in 2018"></p>
<p>2018: have some fun</p>
<a id="more"></a>
<h3 id="工作与学习"><a href="#工作与学习" class="headerlink" title="工作与学习"></a>工作与学习</h3><p>说来有点打脸,去年的年终总结写完不久,我就申请了内部转岗到成都研发中心,比预计至少早了两三年离京。</p>
<p>新部门属于公司内为数不多使用 <code>React + Node 中间层</code>技术栈的团队,对我而言也是很不错的机会,连滚带爬地学习了一波,勉强跟上趟了。下半年开始有机会独自 backup 一个小业务项目,也就对公司当前的微服务架构,构建部署流程,Node 服务的线上运维,问题排查更为熟练了,这是在公司业务中的主要收获。</p>
<p>从 17 年年底,我开始写一个 side project <a href="https://github.com/Colafornia/little-robot" target="_blank" rel="external">「little-robot」</a>,一开始只是一个为了熟悉 Node 服务端开发部署而写的小脚本,夏天终于有时间给它做了一次升级,搭建了 <code>Koa Server</code>,封装成 api,基于 <code>Docker</code> 实现了构建部署与持续集成。最后还厚着脸皮在掘金上推广了一波,目前用户量稳定在 100+,Github Star 60+,对于我而言是个不小的鼓舞。</p>
<p>在上一个年终总结里立的一个 Flag 是 “<strong>Leetcode 每周一题</strong>”,万万没想到最后还是完成了(在 18 年的最后两个月里突击完成😂)。<a href="http://blog.colafornia.me/leetcode-solutions/">「leetcode-solutions」</a> 完成了 54 个题目,一边复习数据结构和算法知识一边刷题,题目最多覆盖到动态规划。最大的收获应该是找到了做算法题的快乐(传说中的用算法娱乐身心),希望把算法学习能够作为一个长期习惯保持下去。</p>
<p>今年的一个重点学习项目是 <strong>iOS 开发</strong>,花了蛮多时间学习 <code>Objective-C</code> 语法,已经可以照着文档画一些简单的交互页面。目前粗浅的感觉 iOS 开发还是很规整,Xcode 功能齐全,在 IDE 里看文档非常舒服。主要是战线拉的太长,这些准备工作都做完之后,有点失去耐心转去做算法题了,还是要有个 deadline 短期集中精力来做更好些。新的一年要把这一块的学习完成,完成一个成品 iOS App。</p>
<p>博客有 <strong>11 篇</strong>,翻译方面也还是做了一些,翻译了很多长文,积分足够兑换了一个 GDD 的音响。十一假期完成了<a href="https://blog.colafornia.me/post/2018/translation-blink-render/">Blink 演讲视频的翻译</a>。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V1dZh4.png" width="450"></p>
<p>完全没有进展的内容是 CS 公开课学习,又当了一年的野生程序员,计算机基础方面除了算法几乎没什么进步。</p>
<h3 id="生活与玩乐"><a href="#生活与玩乐" class="headerlink" title="生活与玩乐"></a>生活与玩乐</h3><p><img src="https://s2.ax1x.com/2019/06/01/V1ducR.md.jpg" alt="Phuket"></p>
<p>下定决心回成都之后,对自己的一个期许是“更好的体验生活”,所以今年的生活娱乐还是很丰富多彩的。</p>
<h4 id="旅行"><a href="#旅行" class="headerlink" title="旅行"></a>旅行</h4><p>今年四月带我妈去了普吉岛+曼谷玩,上面这张照片便是去斯米兰岛潜水时拍的。普吉岛非常好玩,景色和水质都很不错,消费水平也不高,两个人吃一顿海鲜烧烤最多也才 100RMB 左右,再去夜市买点水果小吃简直美滋滋。每天去便利店买一大袋零食,酸奶,加起来最多不超过 30RMB。去斯米兰潜水的一天也很爽,当时那个季节水面很稳,没有晕船,我在当地租了一个 GoPro5,在水下录的很嗨皮。</p>
<p>但曼谷的观感就很一般,堵车程度是帝都两倍,购物中心也没有什么实惠or限定的东西可买。好在只在曼谷停留了两天,本着来都来了的态度还是去大皇宫逛了逛,也有了用 Grab 叫车,和司机用蹩脚的英文乱侃的体验。</p>
<p>总而言之还是一趟很完满的旅行,第一次由我带着妈妈出国玩,她体验了这一波 Airbnb,Grab,全程只管拍照就行,也玩的很开心。决定以后每年都带妈妈出一趟远门✌️。</p>
<p>除此之外还去了乐山吃了三天,以及甘孜阿坝爬山,成都以及四川周边好玩的地方还是很多,应该趁机多去周边玩玩。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V1dm9J.md.jpg" width="450"></p>
<h4 id="健身"><a href="#健身" class="headerlink" title="健身"></a>健身</h4><p>回成都之后便考察了一下软件园的健身房,选定一家买了私教课,由教练带着练了三个月,之后便是自己练了。</p>
<p>成果还是比较满意的,体态纠正了不少,终于能在70%的时间里都站直了,心肺功能也有一定改善,感觉还是全靠健身才保证了来成都后每天胡吃海喝也没胖……</p>
<p>只是到了冬天之后没再坚持锻炼,有点可惜,健身期间的精神头真的很不一样。</p>
<h4 id="Live"><a href="#Live" class="headerlink" title="Live"></a>Live</h4><p>总结回来,这一年也听了几场 Live:</p>
<blockquote>
<ol>
<li>The xx -I See You Tour@北京展览馆</li>
<li>Hello Nico「慢慢,而远」@成都小酒馆</li>
<li>沼泽乐队「争鸣二十年」@成都小酒馆</li>
<li>惘闻乐队 「看不见的城市」@成都小酒馆</li>
</ol>
</blockquote>
<p>这篇文章的题图便是沼泽乐队的现场照片。成都这边还是有很多听现场 Live 的机会,票也并不贵,年中工作不忙的时候与朋友去听了几场,散场后去吃冰粉、串串,也是非常美滋滋。</p>
<h4 id="观影与阅读"><a href="#观影与阅读" class="headerlink" title="观影与阅读"></a>观影与阅读</h4><p>观影量与往年持平,阅片内容也没太大变化,还是商业片为主,独立电影看了一小部分。</p>
<p>比较欣慰的是今年看了不少书,有 <strong>32 本</strong>,大概是去年阅读量的两倍。</p>
<p>其中还是技术书居多,把早些时间买的迟迟没有看完的实体书都 K 掉了,因为焦虑程度比在北京好了不少,也总算有心情看些非技术类的书。</p>
<h4 id="年度最佳清单"><a href="#年度最佳清单" class="headerlink" title="年度最佳清单"></a>年度最佳清单</h4><p><strong>观影最佳:</strong></p>
<blockquote>
<ol>
<li>《西部世界 第二季》</li>
<li>《横道世之介》</li>
<li>《Call Me by Your Name》</li>
</ol>
</blockquote>
<p><strong>阅读最佳:</strong></p>
<blockquote>
<p>技术类</p>
<ol>
<li>《JavaScript 语言精粹》</li>
<li>《Objective-C 编程(第2版)》</li>
<li>《深入React技术栈》</li>
</ol>
<p>非技术类</p>
<ol>
<li>《十一种孤独》</li>
<li>《费马大定理》</li>
<li>《为人文教育辩护》</li>
</ol>
</blockquote>
<p><strong>好物最佳:</strong></p>
<blockquote>
<ol>
<li>除湿机</li>
<li>Kindle Oasis2</li>
<li>发热猫窝(有了这个窝之后,家猫几乎没在别的地方睡过觉,使用率100%)</li>
</ol>
</blockquote>
<h3 id="新的期许"><a href="#新的期许" class="headerlink" title="新的期许"></a>新的期许</h3><p>又到了新年 Flag 时间~</p>
<p>工作学习方面:</p>
<ol>
<li>拓端,完成一个 iOS 成品 App</li>
<li>Leetcode 不能停,2019年进度完成到 100 题(目标定得挺低的)</li>
<li>补足 CS 基础,通过 HIT 的基础课学习</li>
<li>开源两个完整的 Side Project</li>
</ol>
<p>生活方面:</p>
<ol>
<li>阅读量 35 本+</li>
<li>至少去两个没去过的城市玩</li>
<li>体重减掉 5kg</li>
<li>培养一个新爱好(这条很虚,但又很必要)</li>
</ol>
<p><img src="https://s2.ax1x.com/2019/06/01/V1dn39.png" alt=""></p>
<p><img src="/images/chengdu-2018.jpeg" alt="chengdu in 2018"></p>
<p>2018: have some fun</p>
8102年末,前端路由基本思路
https://blog.colafornia.me/post/2018/implement-of-frontend-route/
2018-11-12T12:00:00.000Z
2019-10-24T02:22:04.202Z
<p><img src="/images/router/cover.jpg" alt="cover"></p>
<p>最近看了一些相关资料,特地来整理一下,当前前端主流路由 <code>react-router</code>、<code>vue-router</code> 的实现思路,内容不多也并不复杂,作为知识体系的补全。</p>
<a id="more"></a>
<h2 id="两种模式"><a href="#两种模式" class="headerlink" title="两种模式"></a>两种模式</h2><p>此处默认你已经至少使用过主流框架 Router 中的一种,那就肯定知道路由配置时肯定会有个配置项是关于,使用 <code>hash</code> 模式还是 <code>history</code> 模式。</p>
<p><img src="/images/router/01.jpg" alt=""></p>
<div style="text-align:center">vue-router 通过给实例传入 <code>mode</code> 字段来设置</div>
<p><img src="/images/router/02.jpg" alt=""></p>
<div style="text-align:center">react-router 则通过直接使用不同的路由组件进行区分</div>
<p>这两种模式便是目前我们在浏览器环境中为单页应用实现“<strong>无需重载页面即可更新视图</strong>”的原理。</p>
<p>接下来我们分别进行分析。</p>
<h3 id="hash-模式"><a href="#hash-模式" class="headerlink" title="hash 模式"></a>hash 模式</h3><p>url 中使用了 hash 符号 <code>#</code> 后的内容便属于 <code>fragment</code>。</p>
<p><img src="/images/router/03.jpg" alt=""></p>
<p>有别于 url 中的 <code>?</code> 符号,<code>fragment</code> 设计之初便是为了<code>锚点</code>这一特性,通过 <code>fragment</code> 指定网页中的位置,浏览器会匹配到 id 或 name 为 <code>fragment</code> 值的 a 标签,将其滚动到可视区域的顶部。</p>
<p>除此之外,<code>fragment</code> 还具备以下三个特性:</p>
<ol>
<li>修改#后的 <code>fragment</code> 值不会导致页面重新加载,但是会改变浏览器的历史记录</li>
<li>作为 url 发起 HTTP 请求时,<code>fragment</code> 部分不会被包含在请求头中,也就不会被发送到服务器</li>
<li><code>fragment</code> 一般不会被搜索引擎收录(虽然 Google 也出了相应对策作为补救,但整体上这种模式对 SEO 依然算不上不友好)</li>
</ol>
<p>那 hash 模式是如何进行路由的呢?</p>
<p>通过监听 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Window/onhashchange" target="_blank" rel="external">onhashchange</a> 事件即可捕获 hash 值的改变然后执行后续的更新视图逻辑(具体实现后面再解释)。</p>
<h3 id="history-模式(又名-browser-模式、HTML5-模式)"><a href="#history-模式(又名-browser-模式、HTML5-模式)" class="headerlink" title="history 模式(又名 browser 模式、HTML5 模式)"></a>history 模式(又名 browser 模式、HTML5 模式)</h3><p>这一模式的实现基于 <a href="https://developer.mozilla.org/en-US/docs/Web/API/History" target="_blank" rel="external">Web API 中的 History</a>,浏览器工具栏的前进与后退实际上也是在操作 <code>History</code>。</p>
<p>其中,<code>History.pushState()</code> 与 <code>History.replaceState()</code> 让我们可以实现路由:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line"><span class="built_in">window</span>.history.pushState(stateObject, title, URL);</div><div class="line"><span class="built_in">window</span>.history.replaceState(stateObject, title, URL);</div></pre></td></tr></table></figure>
<p>两者用法类似,<strong>URL 必须为与当前页面属于同域</strong>。这两个方法执行之后都会使得浏览器地址栏更新,但不跳转,同时 <code>History.state</code> 对象也将更新为传入的 URL 值,这就为前端路由的实现提供可能。</p>
<p>每当 <code>History</code> 对象发生变化,都会触发 <code>popstate</code> 事件,同理,我们可以通过监听这一事件,在回调中执行路由匹配逻辑。</p>
<h2 id="源码"><a href="#源码" class="headerlink" title="源码"></a>源码</h2><p><code>vue-router</code> 的源码更为易读,几个版本下来 API 变化也不是很大,此处以 <code>vue-router</code> 源码作为示例。</p>
<p>先看<a href="https://github.com/vuejs/vue-router/tree/dev/src" target="_blank" rel="external">目录结构</a>:</p>
<p><img src="/images/router/04.jpg" alt=""></p>
<p>components 文件夹中便是涉及到视图更新的 <code>Link</code> 与 <code>RouterView</code> 组件,history 文件中涉及到我们刚刚提到的浏览器中两种路由模式。</p>
<p>先看入口文件<a href="https://github.com/vuejs/vue-router/blob/dev/src/index.js" target="_blank" rel="external">https://github.com/vuejs/vue-router/blob/dev/src/index.js</a></p>
<p>在构造器中进行了 <code>mode</code> 读取,可以得知 vue-router 默认使用 <code>hash 模式</code>:</p>
<p><img src="/images/router/05.jpg" alt=""></p>
<p>在 switch 函数中分别调用各自模式对路由的 history 对象进行加工。随后调用 <a href="https://github.com/vuejs/vue-router/blob/701d02b810da200b9ee7bac757d62b628327c6dd/src/install.js" target="_blank" rel="external">install.js</a> 将 vue-router 混入 Vue 实例中:</p>
<p><img src="/images/router/06.jpg" alt=""></p>
<p>通过全局的 Mixin 对象,在 Vue 实例的 <code>beforeCreate</code> 钩子函数中将其混入,并将两个组件进行挂载。</p>
<p>那两个针对不同模式下 histroy 的包装方法呢?</p>
<p>以 <a href="https://github.com/vuejs/vue-router/blob/701d02b810da200b9ee7bac757d62b628327c6dd/src/history/hash.js" target="_blank" rel="external">hash.js</a>为例:</p>
<p><img src="/images/router/07.jpg" alt=""><br>先是设置了事件监听,然后声明了 go、push、replace 等方法。</p>
<p>其中 <code>supportsPushState</code> 是工具方法,通过 <code>window.navigator.userAgent</code> 读取设备信息判断移动端设备是否支持。</p>
<p>从 go、push、replace 等方法的实现可以看出,基本都是通过在history 基础上改写的 <a href="https://github.com/vuejs/vue-router/blob/701d02b810da200b9ee7bac757d62b628327c6dd/src/util/push-state.js" target="_blank" rel="external">pushState</a> 方法实现的。</p>
<p>hash.js 还有一些针对 hash 模式特有的方法如 <code>ensureSlash()</code>,其余实现思路基本与 <a href="https://github.com/vuejs/vue-router/blob/701d02b810da200b9ee7bac757d62b628327c6dd/src/history/html5.js" target="_blank" rel="external">html5.js</a> 相同。</p>
<h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>所以在不借助框架的情况下如何实现一个极简版的前端路由也不是什么难题了,<a href="https://github.com/Colafornia/Wheels/blob/master/Router/index.js" target="_blank" rel="external">Wheels/Router</a> 轻松写两种~</p>
<p>关于实际工程中使用的路由其实还有很多边界情况需要处理,<code>react-router</code> 与 <code>vue-router</code> 结合各自框架实例与上下文,实现了非常简洁高效的路由机制,推荐大家阅读源码好好挖掘一下。</p>
<p><img src="/images/router/cover.jpg" alt="cover"></p>
<p>最近看了一些相关资料,特地来整理一下,当前前端主流路由 <code>react-router</code>、<code>vue-router</code> 的实现思路,内容不多也并不复杂,作为知识体系的补全。</p>
Blink 渲染: 重建引擎
https://blog.colafornia.me/post/2018/translation-blink-render/
2018-10-07T11:30:00.000Z
2019-10-24T02:59:33.016Z
<p><img src="https://s2.ax1x.com/2019/06/01/V3uJG8.jpg" alt="cover"></p>
<blockquote>
<p>本文系掘金委托翻译整理的 BlinkOn9 会议演讲内容<br>演讲资料 <a href="https://www.youtube.com/watch?v=ExNYN_phaxI" target="_blank" rel="external">视频</a>/ <a href="https://docs.google.com/presentation/d/1Iko1oIYb-VHwOOFU3rBPUcOO_9lAd3NutYluATgzV_0/edit#slide=id.g36f1b50c08_0_3702" target="_blank" rel="external">PPT</a></p>
</blockquote>
<p>在 <code>BlinkOn9</code> 会议中,Google Blink 团队开发者 Philip Rogers 与 Stefan Zager 进行了<a href="https://www.youtube.com/watch?v=ExNYN_phaxI" target="_blank" rel="external">《Blink Rendering - Rebuilding the Engine Mid-Flight》分享</a>,旨在介绍 Blink 渲染的基本原理与开发团队近期对滚动性能、绘制合成与排版的改进。</p>
<a id="more"></a>
<h2 id="第一部分:渲染是什么?"><a href="#第一部分:渲染是什么?" class="headerlink" title="第一部分:渲染是什么?"></a>第一部分:渲染是什么?</h2><p>简单来说,渲染是浏览器的某种基础功能,它将你的 HTML 和 CSS 解析成 DOM 树,并将其转换成屏幕上的像素点。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3uRsJ.jpg" alt=""></p>
<p>图中显示了 <code>document</code> 生命周期的主要阶段,中间四个黑色框是渲染流水线(<code>render pipeline</code>)。</p>
<p>我一直认为研究 Chrome 的追踪器有助于理解 document 生命周期。因此,下图是一个渲染进程的 Chrome 追踪器面板,图中的高亮区域是渲染主线程,底部的一小部分属于合成器线程(<code>compositor thread</code>)。在渲染的开始,我们可能会处理资源加载,运行 JavaScript,修改 DOM 树等等,其间会有一段空闲阶段,用于处理一般任务。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3u4d1.jpg" alt=""></p>
<p>接下来,就会发生 <code>VSync</code>(垂直同期,Vertical Synchronization)。vsync 是浏览器刚刚将一个满满的像素窗口推到显示器上,并且开始生成下一个像素窗口了。因此对于渲染进程来说,这意味着全员都已做好准备生成新的像素点。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3u5Ix.jpg" alt=""></p>
<p>vsync 触发了 <code>BeginMainFrame</code>,这是一个重要方法,它<strong>驱动了渲染流水线</strong>。<code>BeginMainFrame</code> 首先会处理输入事件,如滚动、触屏、手势、鼠标等,然后会运行 <code>requestAnimationFrame</code> 回调。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3uLsH.jpg" alt=""></p>
<p>接下来便是开始执行渲染流水线了,如下图,共有四个步骤:</p>
<ul>
<li><p>style: 将 DOM 树转化为 layout 树,遍历 layout 树为每一个节点标注其样式信息,然后将带有样式信息的 layout 树传递到下一阶段</p>
</li>
<li><p>layout: 我们将再次遍历 layout 树,为节点标注其尺寸、位置信息,至此我们已两次对 layout 树进行标注,然后将它传递给合成阶段</p>
</li>
<li><p>composition setup: 在合成设置阶段我们会确定需要绘制多少个合成层(<code>compositing layers</code>),以及它们的尺寸、位置、层叠顺序等</p>
</li>
<li><p>paint: 绘制阶段会获取 layout 树的标注以及在合成设置阶段所记录信息,然后创建一个由原始绘图命令组成的“显示列表”,它会指示合成器如何进行像素绘制。</p>
</li>
</ul>
<p><img src="https://s2.ax1x.com/2019/06/01/V3ujeA.jpg" alt=""></p>
<p>在绘制阶段的结尾,会由主线程切换到合成线程(即下图追踪器中的绿色区域),将光栅化工作切分成几个“瓦片”,分配给几个工作线程来进行。待光栅化完成,我们将进入 Chrome 合成器。这一过程会循环往复地执行下去。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3K9W8.jpg" alt=""></p>
<p>以上便是关于渲染的简单介绍,值得注意的一点是,主线程非常繁忙,所有动作都发生在主线程,脚本在主线程运行,还负责了渲染和许多其它功能,因此<strong>主线程是非常拥挤的</strong>。经过多年的优化工作,我们发现一个非常有效的优化方式,就是把主线程的工作切分,交给其它线程处理。</p>
<h2 id="第二部分:渲染的重要性与时下的难题"><a href="#第二部分:渲染的重要性与时下的难题" class="headerlink" title="第二部分:渲染的重要性与时下的难题"></a>第二部分:渲染的重要性与时下的难题</h2><p>对于 Web 平台来说,渲染是非常重要的。</p>
<p>一是因为,动态网页的本质是接受用户或脚本生成的输入,并将其转化为视觉结果。<strong>渲染是这个过程的核心</strong>,因此无论你的页面做的有多么酷炫,如果渲染出了问题,用户就不会有任何好的体验。</p>
<p>其二,<strong>渲染是网页性能的主要决定因素</strong>(感知的和实际的),渲染是无法中断的,如果 JavaScript 运行太久页面就会变得笨重,这当然会引起用户注意。</p>
<p>其三,现代网页是动态的——会不断地修改内容,加载内容,进行动画。为了跟上步伐,保证交互流畅,<strong>渲染代码必须是一等公民</strong>。</p>
<p>下面开始介绍我们在渲染代码中遇到的挑战,以及为了解决这些问题我们正在着手进行的改进。</p>
<h3 id="1-滚动"><a href="#1-滚动" class="headerlink" title="1. 滚动"></a>1. 滚动</h3><p>正如前文所说,渲染是网页性能的主要决定因素,而<strong>滚动体验则是其重中之重</strong>。用户对于滚动体验是非常敏感的,滚动的体验决定了其对页面整体性能的感知,如果滚动体验很糟糕,页面再酷炫也拯救不了。Blink 中涉及到滚动的代码巧妙地隐藏在各处,跨越了渲染器中的主线程与合成线程,甚至包括浏览器进程。</p>
<p>回首历史,在 1998 年 <code>KHTML</code> 的原始版本中首次赋予了 <code>document</code> 滚动能力。其后,2003 年 <code>WebKit</code> 中 div 也可以进行滚动了,然而这两种滚动都需要重新触发渲染流水线来进行。起初,这两种滚动的代码是分开编写的,这也没什么大不了的。</p>
<p>然而几年之后,随着对滚动添加了很多功能,做了很多优化,这些关于滚动的代码直接变成了 Blink 中最复杂也最难懂的部分。我们依然维护着这两套滚动代码,所有的功能都要写两遍。不仅如此,由于滚动属于核心代码,实现其它功能也难免要去修改它,复杂度直线上升,越来越难以维护了。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3KPSS.jpg" alt=""></p>
<p>由于目前滚动代码的现状,以及任何功能改动都要写两遍,我们所有开发者的工作都变得很困难,因此,在 2014 年 Steve Kobus 与 Elliott 想到了一个绝妙的主意:通过根层滚动(<code>Root Layer Scrolling</code>)来解决这个问题。</p>
<p>他们决定取消 <code>document</code> 文档级滚动,只使用 <code>overflow</code> 实现所有的滚动功能,这一决定主要是为了降低代码的复杂度,改善代码质量。除此之外还有别的好处,比如,由于两套代码已经分别维护了很长时间,他们的行为表现也并不一致。实际上,文档级滚动行为有明显差异,这是因为文档级滚动与 div 滚动会有一些完全不相关的 Bug,一种滚动有 Bug,另一张滚动可能没有,真是一团糟。</p>
<p>实现根层滚动也是一个漫长艰辛的过程,历经 4 年,终于完成,在 M66 版本交付。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3KiQg.jpg" alt=""></p>
<p>想要大规模改动修改渲染代码的布局部分,第一件事是要通过大约四万五千个布局测试,上图中测试失败次数是由 1500 开始的,事实上,我们刚开始进行修改时,大约有 6000 个测试都失败了。这些测试都需要分门别类,挨个解决,因此在这个过程中我们又顺便解决了很多历史遗留 Bug。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3M58K.jpg" alt=""></p>
<p>在我们的性能基准测试图中可以发现,在我们刚开展工作时,性能有了一次明显退化,大概退化了 40% 到 50%,随着深入研究这些性能 Bug,我们发现这些是深递归到 CPU 路径的代码,因此我们必须做 CPU 相关优化与 Chrome chromium 部分的代码修改。这是一个非常艰难的过程,要各种不同的代码修复才能让我们真正回到基线性能。</p>
<p>所以我也不得不重申,这块代码真的很难处理,如果我们犯了任何错误,用户都会立即发现,这些错误也会影响所有页面。</p>
<p>接下来我们来了解一下关于绘制与合成我们所做的改进。</p>
<h3 id="2-绘制与合成"><a href="#2-绘制与合成" class="headerlink" title="2. 绘制与合成"></a>2. 绘制与合成</h3><p>同滚动代码一样,绘制与合成部分的代码也相当古老,大概已经有 16 年了,在当前的代码架构中开发新功能实属不易。现在有机会对这一部分代码进行性能优化,降低内存占用,使得代码易于扩展,便于开发新功能。因此我们开展了一个综合工程项目:绘制代码瘦身。</p>
<p>有必要先从技术方面概述绘制是什么,为什么它如此酷炫,以及我们在整体项目中所处的位置。因此,我们先从前文所提到的滚动是如何工作的开始吧。</p>
<p>在过去,如果我们想进行 div 滚动,我们需要重绘出每一帧。这意味着如果用户一直拖动滚轮,我们就需要生成所有的像素点,用户需要等待我们运行整个渲染流水线后才可以继续移动。</p>
<p>这里有一个惊人的创新叫做合成线程滚动(<code>composited threaded scrolling</code>),其中有两个部分,一个是合成,这很像从电子游戏中获得的灵感,其思想是将整个可滚动区域绘制到一个图像图形缓冲区中,然后并不是每一帧重绘移动区域,而是将一个子纹理复制到不同的纹理中。第二个创新是将滚动操作脱离出主线程,还记得前文提到过的吧,主线程的资源是多么宝贵,此处的基本思想是我们可以在 JavaScript 运行的同时进行滚动。这两件事结合在一起,是一项非常惊人的创新,这种合成线程渲染的思想可以推广到任何需要对纹理进行修改的地方。</p>
<p>比如说,transform,opacity,filter,clip 等等这些都可以通过合成线程思想来实现。当你在软件上运行,用 CPU 绘制像素时,速度很快,但是如果在 GPU 上运行,它的速度更会快成一道闪电。</p>
<p>但是这里有一个叫“老巢爆炸(<code>lair explosion</code>)”的问题。如下图,如果我们将绿盒子使用合成线程进行旋转,它会贯穿蓝盒子。问题是我们需要确认蓝盒子会被绘制在绿盒子之上,因此蓝盒子也会被合成。这种情况会占用相当多的内存。你作为一名前端工程师,在页面上设置了透明度,有可能你就突然发现内存爆炸了,因为页面上其它部分也都被合成了。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3MLVA.jpg" alt=""></p>
<p>下面来介绍一下当下合成器架构体系来阐述合成器是如何工作的,绘制代码瘦身又有什么样的成效。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3QVP0.jpg" alt=""></p>
<p>我们有一个简单的 DOM 树结构,有 emoji 笑脸表情的 div 是可以滚动的。它的生命周期与前文所述的并无二致,因此在排版环节我们将标注 layout 树的尺寸与位置信息,然后便是合成设置环节了,我们重点讲一下。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3Qe2T.jpg" alt=""></p>
<p>a、b、d 都不可滚动,所以它们仨可以一起绘制到同一个图形缓冲区中(<code>graphics buffer</code>)。而 emoji 笑脸表情是可以滚动的,我们不想为它的滚动重绘每一帧,因此把它单独放到一个图形缓冲区中。现在我们有了两个图形缓冲区,是时候进行绘制了。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3QMqJ.jpg" alt=""></p>
<p>在绘制过程中,我们实际上是遍历 layout 树,记录绘图命令。然后是进行光栅化。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3QlZ9.jpg" alt=""></p>
<p>此时我们将执行绘制步骤中所记录的绘图命令,生成真正的像素点。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3Q1aR.jpg" alt=""></p>
<p>最终我们将在页面上它们安放到一起,上下滚动 emoji 表情时也不会触发重绘步骤了。</p>
<p>在目前的架构体系下,有两个问题,一是<strong>合成仅限于特定子树</strong>。layout 树有一个属性,决定我们能否进行合成。并非所有子树都有这个属性,因此我们不能随意将页面上的 div 转换成图形缓冲区,这导致了一个基本性合成 Bug,在 2014 年首次发现。</p>
<p>当时我们试图让 iframe 在任意地方合成,以提高滚动性能,结果发现页面上的内容瞬间都消失了,原因是如果制作了一个合成的 iframe,你还需要确保任何绘制在它上方的内容也是合成的。这是一个在 2014 年发现的毁灭性错误,因为你已经建立了这些特殊的逻辑来不创建过多的图形缓冲区处理诸如此类的事情,结果在游戏的后期发现了一种基本的缺陷,这种缺陷束缚了你的手,这并不是是把你的手绑在一个边缘案例中,这一个可能遇到的情况(Gmail 在进行滚动优化时就遇到了这个问题,优化无法生效),这阻止了我们继续在当前架构中构建。</p>
<p>我们当前合成体系结构的第二个问题是<strong>合成设置是在绘制之前完成的</strong>。我们在系统早期就创建了图像缓冲区,你需要在绘制步骤中重新计算,所以我们有重复的逻辑,很难描述这个逻辑有多复杂,但是我可以说大约一半的绘制代码是用于这种大小和效果,比如 clip。</p>
<p>除了在绘制之前进行这种合成设置之外,还有一个问题,因为它在主线程上,这意味着任何可能改变绘制对象大小的效果都需要回到主线程。例如,如果你有两个可以合成的盒子,其中一个是可以滚动的,那么在很多情况下你必须假设最坏的情况。你必须假设合成器可以在页面上的任何地方进行,所以你必须为页面上的许多东西创建图像缓冲区,这是我们之前讨论过的老巢爆炸问题,导致了真正的性能问题。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3Q3I1.jpg" alt=""></p>
<p>绘制代码瘦身项目改变了我们整个架构中的这两个问题。它改变了我们如何选择合成事物的粒度,这样你就可以合成,将任何效果转换成图像缓冲区,第二是我们将合成设置移动到绘制后。这不仅可以解决基础性合成 Bug,也避免了逻辑重复。</p>
<p>因此,新的合成架构可以在任何边界进行合成,我们已经移动了合成设置应用程序,以释放主线程的压力。这使我们能够对重叠的事物做出精确的合成决定,可以做一些改变主线程外绘制对象大小的事情。</p>
<p>在这个项目的里程碑中,我们已经完成了关于绘制缓存的功能,目前处于 M67,刚刚发布了绘制代码瘦身的 V1.75 版本。在今年(2018)年底,我们将发布 V2 版本,将合成设置移动到绘制后进行。</p>
<h3 id="3-布局排版"><a href="#3-布局排版" class="headerlink" title="3. 布局排版"></a>3. 布局排版</h3><p>布局有两个主要问题,第一个是 web 平台问题,我们称之为<strong>组合问题</strong>(<code>The Combinatorial Problem</code>)。我们有大量的 web 标准,并且还在不断添加更多新的标准,同时旧的标准也依然存在,每次我们定义新的 CSS 标准时,它都会创建一组带有与所有现有 CSS 标准的新交互。它们结合的方式有一点奇怪,随之而来有很多的边界 case,让我们以 <code>flexbox</code> 为例看一看:</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3QJG6.jpg" alt=""></p>
<p>很简单的三个 flex item 盒子,我们添加几个属性看看布局会发生什么变化。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3Qase.jpg" alt=""></p>
<p>设置 <code>direction: rtl</code> 会使得布局方向变为从右往左。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3Yufs.jpg" alt=""></p>
<p>在此基础上,添加一个 <code>flex-direction: row-reverse</code>,布局方向又恢复为从左往右了。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3Yl60.jpg" alt=""></p>
<p>把 <code>direction</code> 属性去掉,从右往左排布。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3YG0U.jpg" alt=""></p>
<p><code>flex-direction</code> 设置为 <code>columb-reverse</code>,布局改为按列排布。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3YJ7F.jpg" alt=""></p>
<p>设置 <code>writing-mode</code> 同时 <code>flex-direction</code> 改为行排布,使得文字方向也发生了改变。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3Y06x.jpg" alt=""></p>
<p><code>flex-direction</code> 改为反向,依然复合预期。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3YrnK.jpg" alt=""></p>
<p><code>flex-direction</code> 改为列,也是一样。举例到这里就足够了,以上之所以表现复合预期,是因为我花了三周的时间解决各种 Bug。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3YcAe.jpg" alt=""></p>
<p>在其它内核的浏览器中可就不一定了,如上图,第一个图是以上 flexbox 示例在 chromium 中的表现,第一排第二个浏览器表现也几乎相同,然而第三个第四个可就相去甚远。</p>
<p>我无意 diss 其它浏览器,换个功能示例,可能 chromium 就是表现最差的那一个。我是想强调这个兼容性问题确实存在,复杂的 CSS 特性也在持续堆积。</p>
<p>第二个问题是 <strong>Blink 中布局相关的代码是非常远古的,里面充斥着无封装,不可重入,非线程安全的面条式巨石代码</strong>。</p>
<p>先解释一下巨石代码,这里有一个 layout 树,节点是 layout 对象,假设我们在树下面的一个元素上改变 CSS。元素现在变脏了,需要转发出去。接下来我们要做的是标记整个祖先链,当我们想执行 layout 阶段时,我们总是从树顶开始,一直往下走,现在我们进行了一系列优化,但是优化后的也没有跳过很多步骤。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3Y2hd.jpg" alt=""></p>
<p>我们仍然要进行完整的树遍历,这也是耗费资源的,每次我们执行 layout 都会进行遍历。底部节点可能位于一个尺寸固定的盒子里,它甚至可以使用 <code>CSS containment</code>,这是一个新特性,有点类似于浏览器的契约,意味着这个子树不会影响它自身以外的任何东西,子树以外的任何东西也不会影响它。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3Yjcq.jpg" alt=""></p>
<p>如果布局这棵子树时我们已经有了所有我们所需要的信息,无需在这个子树之外寻找任何额外的信息来确定大小和位置就好了。然而事实上,我们一直在运行布局代码来获取其他信息。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3tpHU.jpg" alt=""></p>
<p>处于图中这个节点中,如果出于某种原因我们可以跳到树的另一部分吗?不可以,这是一个毁灭性操作。</p>
<p>至于线程安全,还记得最开始我们了解的渲染流水线吧?我们遍历 layout 树,还对它进行标注,然后传递给绘制阶段。当我们完成所有任务准备生成下一帧内容时,会从上次使用的 layout 树开始,根据已改变的内容来更新它。这里是没有什么是线程安全的,可能有多个线程修改它。</p>
<p>对于以上两个问题,相应有两个解决方案。针对<strong>组合问题</strong>,解决方案是 CSS 定制布局即 <a href="https://developers.google.com/web/updates/2016/05/houdini" target="_blank" rel="external">Houdini</a>,这意味着可以在元素上设置特定的 CSS 属性,然后定义一个 JavaScript 函数,该函数负责布局该元素及子树。在常规布局过程中,我们会暂停然后去调用 JavaScript 函数,传给它一组布局元素所需要的信息,函数将消费它。这里不会讲太多 <code>Houdini</code> 的细节,大家有兴趣可以自行研究。</p>
<p>针对第二个问题的解决方案是 <code>Layout NG</code>,这实际上是对如何完成布局的全盘反思。<code>Layout NG</code> 有两个特性,一是它使用约束驱动的布局,输入一个子树来进行布局,我们传递给它所有它所需要的在子树中进行布局的信息,而且它根本不看子树的外面。实现这一点也并不容易,通过在中强制封装,我们让底层布局代码更容易实现刚才提到的 CSS 定制布局。第二个特性是,输入(layout 树)与输出(fragment 树)的树都是<strong>不可变对象</strong>,我们每次都创建一个新的布局树,一旦我们创建了它,该树就不可变了,我们并不是在这个输入树上进行注释,而是复制它,并用新的替换子树来改变子树,我们将拥有布局树的全新副本。</p>
<p>这两个特性的实现将使得布局方面的各种强力优化成为可能。这一项目尚属早期,第一阶段预计在今年年底、明年年初发布。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V3uJG8.jpg" alt="cover"></p>
<blockquote>
<p>本文系掘金委托翻译整理的 BlinkOn9 会议演讲内容<br>演讲资料 <a href="https://www.youtube.com/watch?v=ExNYN_phaxI">视频</a>/ <a href="https://docs.google.com/presentation/d/1Iko1oIYb-VHwOOFU3rBPUcOO_9lAd3NutYluATgzV_0/edit#slide=id.g36f1b50c08_0_3702">PPT</a></p>
</blockquote>
<p>在 <code>BlinkOn9</code> 会议中,Google Blink 团队开发者 Philip Rogers 与 Stefan Zager 进行了<a href="https://www.youtube.com/watch?v=ExNYN_phaxI">《Blink Rendering - Rebuilding the Engine Mid-Flight》分享</a>,旨在介绍 Blink 渲染的基本原理与开发团队近期对滚动性能、绘制合成与排版的改进。</p>
生命在于折腾,写一个前端资讯推送服务
https://blog.colafornia.me/post/2018/the-beginning-of-little-robot/
2018-09-11T04:39:00.000Z
2019-10-24T03:04:03.719Z
<p><img src="https://s2.ax1x.com/2019/06/01/V1wi2d.jpg" width="600"></p>
<p>去年年底开始写的一个小项目,断断续续做了些优化,在此简单的记录一下。</p>
<a id="more"></a>
<h2 id="源头"><a href="#源头" class="headerlink" title="源头"></a>源头</h2><p>起源是之前一直没什么机会接触到 Node 项目,工作中接触到的也仅限于用 Node 写脚本,做一些小工具,与服务器上跑的 Node 服务相差甚远。所以想写一个在服务器上跑的 Node 小项目练手。</p>
<p>一直喜欢用 RSS 订阅资讯这种方式,简单高效,与其每天不定时地接收推送,打开各网站 App 来接收资讯,不如自己拿到主动权集中在同一时间段统一阅读。这样避免了每天不定时接受信息的焦虑堆积,但是又常常想不起来打开😅,过了一周打开 Reeder,发现累积的未读资讯又爆炸了,人真是很难满足。</p>
<p>于是决定自己搞个资讯推送服务吧,满足自己的核心诉求,<strong>每个工作日早上 10 点微信推送 RSS 前端资讯的更新</strong>,这样就可以在每天抵达工位的时候舒舒服服浏览一下新鲜事,挑一些有用的存起来慢慢研读。</p>
<p>项目仓库: <a href="https://github.com/Colafornia/little-robot" target="_blank" rel="external">https://github.com/Colafornia/little-robot</a></p>
<p>推送大概长这样:</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V1wi2d.jpg" width="600"></p>
<p>现在推送源主要是各厂的知乎专栏,大佬们的个人博客,掘金前端热门文章,都是我自己的个人口味。</p>
<p>下面来讲一下开发(与自己给自己加需求)历程。</p>
<h2 id="开始"><a href="#开始" class="headerlink" title="开始"></a>开始</h2><p>最开始感觉这个需求是很简单的,具体操作可以分解为:</p>
<ol>
<li>写一个配置文件,把我想抓取的 RSS 源地址写在里面</li>
<li>找一个能解析 RSS 的 npm 包,遍历配置文件里的源,解析之后处理数据</li>
<li>仅筛出在过去 24 小时内更新的文章,把数据处理一下,汇总成一段字符串,用微信推送</li>
<li>以上写出的脚本通过定时任务跑起来,done!</li>
</ol>
<p>最后选择了 <a href="https://github.com/bobby-brennan/rss-parser" target="_blank" rel="external">rss-parser</a> 作为解析工具包,<a href="https://pushbear.ftqq.com/admin/#/" target="_blank" rel="external">PushBear</a> 作为推送服务,<a href="https://github.com/node-schedule/node-schedule" target="_blank" rel="external">node-schedule</a> 任务调度工具写出来了一版。</p>
<p>然后就发现自己知识的匮乏了,没有考虑到脚本部署到服务器上时,进程守护的问题,于是研习了一波 <a href="https://github.com/Unitech/pm2" target="_blank" rel="external">pm2</a>,完美完成任务。</p>
<h2 id="过渡"><a href="#过渡" class="headerlink" title="过渡"></a>过渡</h2><p>项目写到这里其实是可以凑和用了,但是看起来很 low 很难受。主要问题有:</p>
<ol>
<li>当时 RSS 源大概有四五十个,一次性遍历解析所有的源经常会有超时或者出错的</li>
<li>RSS 源写在配置文件里,每次想添加、修改源都需要改代码,很 low</li>
<li><a href="https://pushbear.ftqq.com/admin/#/" target="_blank" rel="external">PushBear</a> 这个推送服务只能存储三天内的推送,三天前,一周前的推送内容都看不了,这也很难受</li>
<li>掘金的 RSS 源内容不多,也不是按照热门程度排序的(也可能是我姿势不对😅),不太符合要求</li>
</ol>
<p>第一点稍微有点复杂,可能现在解决的方案依然很原始。出现第一个问题一是需要控制请求的并发数量,二是 RSS 源本身有一定的不稳定性。目前的解决方案是:</p>
<ol>
<li>把抓取任务和推送任务分开,预留出可以循环抓取三次的时间,后面两次只抓取之前失败的源</li>
<li>用 <a href="https://github.com/caolan/async" target="_blank" rel="external">async</a> 的 <code>mapLimit</code> 和 <code>timeout</code> 方法设置最大并发数量和超时时间</li>
</ol>
<p>大致代码如下(有一些细节处理没贴上来):</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// 抓取定时器 ID</span></div><div class="line"><span class="keyword">let</span> fetchInterval = <span class="literal">null</span>;</div><div class="line"><span class="comment">// 抓取次数</span></div><div class="line"><span class="keyword">let</span> fetchTimes = <span class="number">0</span>;</div><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">setPushSchedule</span> (<span class="params"></span>) </span>{</div><div class="line"> schedule.scheduleJob(<span class="string">'00 30 09 * * *'</span>, () => {</div><div class="line"> <span class="comment">// 抓取任务</span></div><div class="line"> log.info(<span class="string">'rss schedule fetching fire at '</span> + <span class="keyword">new</span> <span class="built_in">Date</span>());</div><div class="line"> activateFetchTask();</div><div class="line"> });</div><div class="line"></div><div class="line"> schedule.scheduleJob(<span class="string">'00 00 10 * * *'</span>, () => {</div><div class="line"> <span class="comment">// 发送任务</span></div><div class="line"> log.info(<span class="string">'rss schedule delivery fire at '</span> + <span class="keyword">new</span> <span class="built_in">Date</span>());</div><div class="line"> <span class="keyword">let</span> message = makeUpMessage();</div><div class="line"> log.info(message);</div><div class="line"> sendToWeChat(message);</div><div class="line"> });</div><div class="line">}</div><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">activateFetchTask</span>(<span class="params"></span>) </span>{</div><div class="line"> fetchInterval = setInterval(fetchRSSUpdate, <span class="number">120000</span>);</div><div class="line"> fetchRSSUpdate();</div><div class="line">}</div><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">fetchRSSUpdate</span>(<span class="params"></span>) </span>{</div><div class="line"> fetchTimes++;</div><div class="line"> <span class="keyword">if</span> (toFetchList.length && fetchTimes < <span class="number">4</span>) {</div><div class="line"> <span class="comment">// 若抓取次数少于三次,且仍存在未成功抓取的源</span></div><div class="line"> log.info(<span class="string">`第<span class="subst">${fetchTimes}</span>次抓取,有 <span class="subst">${toFetchList.length}</span> 篇`</span>);</div><div class="line"> <span class="comment">// 最大并发数为15,超时时间设置为 8000ms</span></div><div class="line"> <span class="keyword">return</span> mapLimit(toFetchList, <span class="number">15</span>, (source, callback) => {</div><div class="line"> timeout(parseRSS(source, callback), <span class="number">8000</span>);</div><div class="line"> })</div><div class="line"> }</div><div class="line"> log.info(<span class="string">'fetching is done'</span>);</div><div class="line"> clearInterval(fetchInterval);</div><div class="line"> <span class="keyword">return</span> fetchDataCb();</div><div class="line">}</div></pre></td></tr></table></figure>
<p>这样基本解决了 90% 以上的抓取问题,保证了脚本的稳定性。</p>
<p>针对 RSS 源写在配置文件里,每次想添加、修改源都需要改代码的问题,解决方法很简单,把源配置写到 MongoDB 里也就好了,有一些 GUI 软件可以直接在图形界面来添加、修改数据。</p>
<p>为了解决推送服务只能存储三天内的推送,决定新增一个每周五的周抓取任务,抓取一周内的新文章,把内容作为 issue 发到仓库。也还算是一个解决方案。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V1wVqP.jpg" width="480"></p>
<p>针对掘金的 RSS 源问题,最后决定直接调用掘金的接口来取数据,这就可以随心所欲按自己的需求来了,每天只抓取❤️点赞数在 70 以上的文章。</p>
<p>顺便给抓取的文章时间范围加了一个偏移值,避免筛掉质量好但是由于刚刚发布点赞较少的文章。感觉自己棒棒哒~</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">filterArticlesByDateAndCollection</span> (<span class="params"></span>) </span>{</div><div class="line"> <span class="keyword">const</span> threshold = <span class="number">70</span>;</div><div class="line"> <span class="comment">// articles 是已按❤️数由高到低排序的文章列表</span></div><div class="line"> <span class="keyword">let</span> results = articles.filter((article) => {</div><div class="line"> <span class="comment">// 偏移值五小时,避免筛掉质量好但是由于刚刚发布点赞较少的文章</span></div><div class="line"> <span class="keyword">return</span> moment(article.createdAt).isAfter(moment(startTime).subtract(<span class="number">5</span>, <span class="string">'hours'</span>))</div><div class="line"> && moment(article.createdAt).isBefore(moment(endTime).subtract(<span class="number">5</span>, <span class="string">'hours'</span>))</div><div class="line"> && article.collectionCount > threshold;</div><div class="line"> });</div><div class="line"> <span class="comment">// 掘金文章最多收录 8 篇,避免信息爆炸</span></div><div class="line"> <span class="keyword">return</span> results.slice(<span class="number">0</span>, <span class="number">8</span>);</div><div class="line">}</div></pre></td></tr></table></figure>
<p>在这个期间也充分感受到了日志的重要性,在数据库里新增了一个表用来存每天的推送内容。</p>
<p>另外在 <a href="https://pushbear.ftqq.com/admin/#/" target="_blank" rel="external">PushBear</a> 上新添加了一个 Channel 来给自己推送日志,每天在抓取任务完成后,先给我发送一下抓取到的内容,如果发现有任何问题,我可以自己登服务器紧急修复一下(这么想来还是很 low 😅)。</p>
<h2 id="升级"><a href="#升级" class="headerlink" title="升级"></a>升级</h2><p>做完以上改动之后,脚本稳定地跑了快半年,这期间我也一直在忙着搬砖,没什么精力再来改造它。</p>
<p>一直没做推广,但某天突然发现已经有了三十多个用户在订阅这个服务,于是良心发现,本着对用户负责(也是自己有了新的想练习的技术👻),就又做了一次改造。</p>
<p>此时项目的问题有:</p>
<ol>
<li>没有文章去重,如果文章在知乎专栏发了,掘金也发了,作者个人博客也发了的话,就相当于会重复出现几次</li>
<li>推送的时间间隔不精确,都是当前时间的过去 24 小时来筛的</li>
<li>脚本直连数据库进行存取操作也不太好,感觉这个形式做成 server,对外暴露 api 更合理(等哪天想写个 RSS 阅读器也就用上了)</li>
<li>每次代码有更新,依赖有更新,都 ssh 上服务器然后 <code>npm install</code> 感觉也不太专业,有提升空间(其实就是想用 <code>docker</code> 了)</li>
</ol>
<p>1,2 问题很好解决,每次抓取之前先查一下日志,上次推送的具体时间。每抓到新文章时,再与最近 7 天日志里的文章比对一下,重复的不放到抓取结果中,也就解决了。</p>
<p>对于问题 3,于是决定搭建 Koa Server,先把从 MongoDB 读取推送源,存取推送日志变成 api。</p>
<p>目录结构如下,添加 <code>Model</code> 与 <code>Controller</code>。把 RSS 抓取脚本与掘金爬虫放到 task 文件。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V1wQ2j.jpg" width="400"></p>
<p>没什么难点,就可以调用 api 来获取 RSS 源了:</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V1w8rq.jpg" width="500"></p>
<p>此时想到了一个重要问题,<strong>身份验证</strong>。肯定不能把所有 api 都随意暴露出去,让外界可以任意调用,这也就相当于把数据库都暴露出去了。</p>
<p>最终决定用 <code>JSON Web Token(缩写 JWT)</code> 作为认证方案,主要原因是 JWT 适合一次性、短时间的命令认证,目前我的服务仅限于服务器端的 api 调用,每天的使用时间也不长,无需签发有效期很长的令牌。</p>
<p>Koa 有一个 <a href="https://github.com/koajs/jwt" target="_blank" rel="external">jwt</a> 的中间件</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// index.js</span></div><div class="line">app.use(jwtKoa({ secret: config.secretKey }).unless({</div><div class="line"> path: [<span class="regexp">/^\/api\/source/</span>, <span class="regexp">/^\/api\/login/</span>]</div><div class="line">}))</div></pre></td></tr></table></figure>
<p>加上中间件后,除了 <code>/api/source</code> 与 <code>/api/login</code> 接口就都需要经过 jwt 认证才能访问了。</p>
<p>因此写了一个 <code>/api/login</code> 接口,用于签发令牌,拿到令牌之后,把令牌设置到请求头里就可以通过认证了:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// api/base.js</span></div><div class="line"><span class="comment">// 用于封装 axios</span></div><div class="line"><span class="comment">// http request 拦截器</span></div><div class="line"><span class="keyword">import</span> axios <span class="keyword">from</span> <span class="string">'axios'</span>;</div><div class="line"><span class="keyword">const</span> config = <span class="built_in">require</span>(<span class="string">'../config'</span>);</div><div class="line"><span class="keyword">const</span> Instance = axios.create({</div><div class="line"> baseURL: <span class="string">`http://localhost:<span class="subst">${config.port}</span>/api`</span>,</div><div class="line"> timeout: <span class="number">3000</span>,</div><div class="line"> headers: {</div><div class="line"> post: {</div><div class="line"> <span class="string">'Content-Type'</span>: <span class="string">'application/json'</span>,</div><div class="line"> }</div><div class="line"> }</div><div class="line">});</div><div class="line">Instance.interceptors.request.use(</div><div class="line"> (config) => {</div><div class="line"> <span class="comment">// jwt 验证</span></div><div class="line"> <span class="keyword">const</span> token = config.token;</div><div class="line"> <span class="keyword">if</span> (token) {</div><div class="line"> config.headers[<span class="string">'Authorization'</span>] = <span class="string">`Bearer <span class="subst">${token}</span>`</span></div><div class="line"> }</div><div class="line"> <span class="keyword">return</span> config;</div><div class="line"> },</div><div class="line"> error => {</div><div class="line"> <span class="keyword">return</span> <span class="built_in">Promise</span>.reject(error);</div><div class="line"> }</div><div class="line">);</div></pre></td></tr></table></figure>
<p>如果请求头里没有正确的 token,则会返回 <code>Authentication Error</code>。</p>
<p>至于问题 4,现在服务比较简单,也只在一个机器上部署,手动登机器 npm install 问题还不大,如果机器很多,依赖项也复杂的话,很容易出问题,具体参见<a href="https://zhuanlan.zhihu.com/p/39209596" target="_blank" rel="external">科普文:为什么不能在服务器上 npm install ?</a>。</p>
<p>于是决定基于 <code>Docker</code> 做构建部署。</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">FROM</span> daocloud.io/node:<span class="number">8.4</span>.<span class="number">0</span>-<span class="keyword">onbuild</span></div><div class="line"><span class="keyword">COPY</span> package*.json ./</div><div class="line"><span class="keyword">RUN</span> npm install -g cnpm --registry=https://registry.npm.taobao.org</div><div class="line"><span class="keyword">RUN</span> cnpm install</div><div class="line"><span class="keyword">RUN</span> echo "Asia/Shanghai" > /etc/timezone</div><div class="line"><span class="keyword">RUN</span> dpkg-reconfigure -f noninteractive tzdata</div><div class="line"><span class="keyword">COPY</span> . .</div><div class="line"><span class="keyword">EXPOSE</span> <span class="number">3001</span></div><div class="line">CMD [ <span class="string">"npm"</span>, <span class="string">"start"</span>, <span class="string">"$value1"</span>, <span class="string">"$value2"</span>, <span class="string">"$value3"</span>]</div></pre></td></tr></table></figure>
<p>用的比较简单,主要就是负责安装依赖,启动服务。需要注意的主要有两点:</p>
<ol>
<li>国内拉去外网的镜像很慢,像 Node 官方的镜像我都拉了好久都没拉下来,这样的话推荐使用国内的镜像,比如我用的 DaoCloud,还有阿里云镜像等等</li>
<li>由于推送服务是对时间敏感的,基础镜像的时区并不是国内时区,要手动设置一下</li>
</ol>
<p>然后去 <a href="https://dashboard.daocloud.io/" target="_blank" rel="external">DaoCloud</a> 等提供公有云服务的网站授权访问 Github 仓库,连接自己的主机,就可以实现持续集成,自动构建部署我们的镜像了。具体步骤可参考<a href="https://zhuanlan.zhihu.com/p/37961402" target="_blank" rel="external">基于 Docker 打造前端持续集成开发环境</a>。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V1wwRJ.jpg" alt="daocloud"></p>
<p>本次优化大概就到这里了。接下来要做的可能是提供一个推送历史查看页面,优先级不是很高,有时间再做吧(顺便练习一下 Nginx)。</p>
<p>现在的实现方案可能还是有很不合理的地方,欢迎提出建议。</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V1wi2d.jpg" width="600"></p>
<p>去年年底开始写的一个小项目,断断续续做了些优化,在此简单的记录一下。</p>
字符编码
https://blog.colafornia.me/post/2018/character-encoding/
2018-09-03T06:20:00.000Z
2019-10-24T02:22:04.196Z
<p><img src="https://i.dailymail.co.uk/i/pix/2016/10/27/21/39C8A74600000578-3879480-The_touch_bar_can_display_emotions_in_messaging_apps_playback_co-a-5_1477598860842.jpg" alt="cover"></p>
<p>作为编程知识基础中的基础,有必要消化整理输出一次。</p>
<p>本文主要介绍了字符编码的几个重要基础概念,从 <code>ASCII</code> 到 <code>Unicode</code> 再到 <code>Emoji</code> 与 <code>JavaScript</code> 字符处理的一些坑。</p>
<a id="more"></a>
<h3 id="基础概念"><a href="#基础概念" class="headerlink" title="基础概念"></a>基础概念</h3><p>由于计算机只能处理数字,如果要处理文本,就必须先把文本转换为数字。计算机中,<code>字节(byte)</code> 是一个 <code>8bit</code> 的储存单元,一个字节能表示的最大的整数就是 255(二进制的11111111 = 十进制255),如果要表示更大的整数,就必须用更多的字节。</p>
<h4 id="字符集"><a href="#字符集" class="headerlink" title="字符集"></a>字符集</h4><p>字符是文字与符号的总称,它是一个信息单位。字符集就是字符的集合。<code>ASCII码</code>(American Standard Code for Information Interchange)就是一个字符集,这个集合中只有数字,英文字母和一些符号共 127 个字符。如果我们想处理中文、日文文本,仅通过 <code>ASCII码</code> 就做不到了。在历史中由于眼光的局限性,出现了一些仅能处理部分字符的字符集,无法通用。</p>
<p><img src="https://i.loli.net/2018/09/03/5b8d2209e4247.jpg" alt=""></p>
<h4 id="字符编码"><a href="#字符编码" class="headerlink" title="字符编码"></a>字符编码</h4><p>字符编码规定了字符集和实际存储的二进制数值之间的转换关系。一般来说,每个字符集都有其对应的字符编码方式(有的字符集有一个对应字符编码,有的则有多个)。像 <code>ASCII</code> 与 <code>GB18030</code> 都仅有一种编码实现,因此既可以作为字符集的名字,也可以用来指代它们的字符编码。</p>
<p>通过以上概念的介绍不难窥探在字符编码的历史中存在以下痛点:</p>
<ol>
<li>字符集不够通用,总有覆盖不到的字符</li>
<li>新的字符集难以向下兼容老的</li>
<li>覆盖更多字符的字符集,难以避免需要更多字节,如果我们的文本仅通过 <code>ASCII</code> 就能处理的话,使用占用字节更多的字符集在储存和传输都不划算</li>
</ol>
<p>这些问题都由 <code>Unicode</code> 及其字符编码一起打包解决了。</p>
<h3 id="Unicode"><a href="#Unicode" class="headerlink" title="Unicode"></a>Unicode</h3><p><a href="https://en.wikipedia.org/wiki/Unicode" target="_blank" rel="external">Unicode</a> 是一个<strong>字符集</strong>,旨于涵盖所有国家语言中可能出现的符号与文字,是目前绝大多数程序使用的字符编码。</p>
<p><code>Unicode</code>的诞生也不是一蹴而就,也有历史过程。</p>
<h4 id="历史进程"><a href="#历史进程" class="headerlink" title="历史进程"></a>历史进程</h4><p>(这段不是用来凑数的,这几个英文简写后面还会一直出现,知道了历史更方便记忆分辨)</p>
<p>ISO 与 IEC 分别推出了 <code>Unicode</code> 与 <code>UCS</code>(Universal Multiple-Octet Coded Character Set) 。后来(只过了一年),两者进行整合,到了 Unicode2.0 时代,Unicode 的编码和 UCS 的编码都完全一致。</p>
<p><code>USC</code> 这个名字也并未从此消失在历史中。<code>UCS</code> 标准有自己的格式,如<code>UCS-2</code>,<code>UCS-4</code>等等 而 Unicode 也有自己的不同编码实现,如<code>UTF-8</code>,<code>UTF-16</code>,<code>UTF-32</code>等等。</p>
<h4 id="关于-Unicode-自己"><a href="#关于-Unicode-自己" class="headerlink" title="关于 Unicode 自己"></a>关于 Unicode 自己</h4><p><code>码点 code point</code> 是指在 Unicode 字符集中字符的值,根据 Unicode 标准,是前缀为 <code>U+</code> 的十六进制数字。</p>
<p>Unicode 字符分为 17 组平面(plane),每个平面拥有 2^16 (65,536) 个码点。每一个码点都可以用 16 进制 xy0000 到 xyFFFF 来表示,这里的 xy 是表示一个 16 进制的值,从 00 到 10。目前我们常用字符大多都在 BMP 基本平面中。</p>
<p><img src="https://s1.ax1x.com/2018/08/29/PXQfOA.png" alt="plane"></p>
<h4 id="字节序与-BOM"><a href="#字节序与-BOM" class="headerlink" title="字节序与 BOM"></a>字节序与 BOM</h4><p>在了解 Unicode 的字符编码之前,还需要了解一个关于 <code>字节序</code> 的知识。</p>
<p>计算机硬件有两种储存数据的方式:<code>大端</code>字节序(big endian)和<code>小端</code>字节序(little endian)。</p>
<ul>
<li>大端字节序:高位字节在前,低位字节在后</li>
<li>小端字节序:低位字节在前,高位字节在后</li>
</ul>
<p>因此,<code>0x1234567</code> 的大端字节序和小端字节序的写法如下图:</p>
<p><img src="https://www.ruanyifeng.com/blogimg/asset/2016/bg2016112201.gif" alt=""></p>
<p>字节序的存在主要是因为计算机电路先处理低位字节,因为计算都是从低位开始的。但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。</p>
<p>Unicode 规范中推荐的标记字节顺序的方法是 <code>BOM</code>(Byte Order Mark)。有一个叫做”零宽度非换行空格(ZERO WIDTH NO-BREAK SPACE)”的字符,它的编码是 <code>FEFF</code>。而 <code>FFFE</code> 在 UCS 中是不存在的字符,所以不应该出现在实际传输中。Unicode 规范中定义每个文件的最前面加入这个零宽度非换行空格字符,如果一个文本文件的头两个字节是 <code>FE FF</code>,就表示该文件采用大端方式;如果头两个字节是<code>FF FE</code>,就表示该文件采用小端方式。</p>
<p>需要清楚的是,<strong>不是所有的东西都有字节序</strong>,而且字符序是以单字节为单位的顺序问题。</p>
<p>前面提到 <code>Unicode</code> 有多种字符编码实现方式,我们主要介绍 <code>UTF-8</code> 与 <code>UCS-2</code>。</p>
<h4 id="UTF-8"><a href="#UTF-8" class="headerlink" title="UTF-8"></a>UTF-8</h4><p><code>UTF-8</code> 作为最常见的 Unicode 实现方式,解决了前面提到的字符编码几大痛点。</p>
<p> <code>UTF-8</code> 编码是<strong>变长编码</strong>,用 1 到 6 个字节编码,完全兼容 <code>ASCII</code> 码,对于 ASCII 涵盖的那些字符,单字节实现,其余大多数为三字节实现。对于以英文为主的文本非常友好,最节省存储空间。缺点主要在于</p>
<p><code>UTF-8</code> 编码通过多个字节组合的方式来显示,这是计算机处理<code>UTF-8</code> 的机制,它是无字节序之分的。</p>
<p>UTF 家族还有 <code>UTF-16(双字节)</code> 与 <code>UTF-32(四字节)</code> 实现,两者都有字节序问题,前者更适合汉字编码但不支持单字节的 <code>ASCII</code>,后者由于浪费储存空间很不常见,HTML5 中明确规定禁止使用 UTF-32 编码。</p>
<h4 id="UCS-2"><a href="#UCS-2" class="headerlink" title="UCS-2"></a>UCS-2</h4><p>JavaScript 设计之初,还没有出现 <code>UTF-16</code>,因此采用的是 <code>USC-2</code> 编码。前面提到 <strong>Unicode 的编码和 UCS 的编码都完全一致</strong>。<code>UCS-2</code> 是一种定长的编码方式,用两位字节来表示一位码位。</p>
<p><code>UTF-16</code> 可看成是 <code>UCS-2</code> 的父集。在没有<code>辅助平面字符(surrogate code points)</code>前,<code>UTF-16</code> 与 <code>UCS-2</code> 所指的是同一的意思。但当引入辅助平面字符后,就称为 <code>UTF-16</code> 了。现在若有软件声称自己支持 <code>UCS-2</code> 编码,那其实是暗指它不能支持在 <code>UTF-16</code> 中超过 2 字节的字集。对于小于 <code>0x10000</code> 的 <code>UCS</code> 码,<code>UTF-16</code> 编码就等于 <code>UCS</code> 码。</p>
<p>因此在 ES6 之前,JavaScript 对于超出 USC-2 的字符无法正确处理,会导致字符长度、正则匹配判断错误,使用字符串的 <code>charCodeAt()</code> 与 <code>fromCharCode()</code> 也无法正确识别字符与码点。</p>
<p>ES6 新增了 <code>codePointAt()</code> 与 <code>fromCodePoint()</code> 方法以正确处理 32 位的 <code>UTF-16</code> 字符之外的字符。</p>
<h3 id="Emoji"><a href="#Emoji" class="headerlink" title="Emoji"></a>Emoji</h3><figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div></pre></td><td class="code"><pre><div class="line"><span class="string">'😂'</span>.length <span class="comment">// 2</span></div><div class="line"><span class="string">'1️⃣'</span>.length <span class="comment">// 3</span></div><div class="line"><span class="string">'👨👨👦'</span>.length <span class="comment">// 8</span></div><div class="line"><span class="string">'👨👩👧👦'</span>.length <span class="comment">// 11</span></div></pre></td></tr></table></figure>
<p>看起来就很刺激。</p>
<p>随着 <code>Emoji</code> 表情的流行,在开发中就不得不了解、考虑 <code>Emoji</code> 字符了。否则最简单的 textarea 文本字数限制需求都难以正常完成。</p>
<p>随着政治正确的发展,Emoji 现在是非常多元化了:</p>
<p><img src="https://i.loli.net/2018/09/03/5b8d23bc27a4c.jpg" width="500"></p>
<p><img src="https://i.loli.net/2018/09/03/5b8d23bc4ed9b.jpg" width="500"></p>
<p>肤色,职业,性别,取向,家庭组成都十分多元,基本覆盖了所有情况。</p>
<p>其实在原先的基础 Emoji 字符上拓展出这些多元化字符并不难,通过码点组合就能实现。</p>
<p>肤色修饰符: 🏻 🏼 🏽 🏾 🏿</p>
<p>通过这几个肤色修饰符拼接到原有表情上,就可以实现肤色多元化:</p>
<p><img src="https://i.loli.net/2018/09/03/5b8d21b17273c.jpg" alt=""></p>
<p>通过 <code>零宽字符 ZWJ(U+200D)</code> 可以实现 family emoji,U+200D 相当于是一个连接符,连接家庭成员 emoji:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// family (man, woman, boy)</span></div><div class="line"><span class="comment">// '\u{1F468}' + '\u{200D}' + '\u{1F469}' + '\u{200D}' + '\u{1F466}'</span></div><div class="line"><span class="comment">// 👨 + '\u{200D}' + 👩 + '\u{200D}' + 👦</span></div><div class="line"><span class="comment">// length: 8</span></div><div class="line">> 👨👩👦</div><div class="line"><span class="comment">// family (woman, woman, girl)</span></div><div class="line"><span class="comment">// '\u{1F469}' + '\u{200D}' + '\u{1F469}' + '\u{200D}' + '\u{1F467}'</span></div><div class="line"><span class="comment">// 👩 + '\u{200D}' + 👩 '\u{200D}' + 👧</span></div><div class="line"><span class="comment">// length: 8</span></div><div class="line">> 👩👩👧</div><div class="line"><span class="comment">// family (woman, woman, girl, girl)</span></div><div class="line"><span class="comment">// '\u{1F469}' + '\u{200D}' + '\u{1F469}' + '\u{200D}' + '\u{1F467}' + '\u{200D}' + '\u{1F467}'</span></div><div class="line"><span class="comment">// '👩' + '\u{200D}' + '👩' + '\u{200D}' + '👧' + '\u{200D}'+ 👧</span></div><div class="line"><span class="comment">// length: 11</span></div><div class="line">> 👩👩👧👧</div></pre></td></tr></table></figure>
<p>因此,遇到文本有可能含有 Emoji 的情况中,需将 Emoji 字符正则匹配出来,单独进行计算。</p>
<h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><p>字符编码相关知识还有很多,本文仅介绍最近工作中所涉及的部分。更完善更准确的内容建议参考英文维基。</p>
<ul>
<li><a href="https://www.thoughtco.com/what-is-unicode-2034272" target="_blank" rel="external">What Is Unicode?</a></li>
<li><a href="cenalulu.github.io/linux/character-encoding/">十分钟搞清字符集和字符编码</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/41203455" target="_blank" rel="external">从 Unicode 到 emoji</a></li>
<li><a href="https://www.ruanyifeng.com/blog/2016/11/byte-order.html" target="_blank" rel="external">理解字节序</a></li>
</ul>
<p><img src="https://i.dailymail.co.uk/i/pix/2016/10/27/21/39C8A74600000578-3879480-The_touch_bar_can_display_emotions_in_messaging_apps_playback_co-a-5_1477598860842.jpg" alt="cover"></p>
<p>作为编程知识基础中的基础,有必要消化整理输出一次。</p>
<p>本文主要介绍了字符编码的几个重要基础概念,从 <code>ASCII</code> 到 <code>Unicode</code> 再到 <code>Emoji</code> 与 <code>JavaScript</code> 字符处理的一些坑。</p>
又双叒叕学习了一遍正则表达式
https://blog.colafornia.me/post/2018/learning-regex-again/
2018-07-11T12:00:00.000Z
2019-10-24T02:59:57.055Z
<p><img src="/images/regex.jpg" alt="cover"></p>
<p>正则表达式基本上每用到一次就得从头自学一次,用完了写出来了也就忘光了。</p>
<p>前两天在 Twitter 上看到了题图,感觉又是个大坑,趁着手头还有 <a href="https://caraws.github.io/" target="_blank" rel="external">Caraws</a> 给的书就又双叒叕学习了一遍正则表达式。</p>
<p>本文试图先用最易懂的方式理顺正则表达式的知识点(就不贴一摞一摞的文档截图了,至于正则的使用场景和用处也不啰嗦了),主要介绍正则本身和在 JavaScript 中使用正则的坑。</p>
<a id="more"></a>
<h2 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h2><p>形如题图中的 <code>/abc/</code> 就是一个最简单的正则表达式(regular expression),一般被称之为模式(pattern)。</p>
<p>更具体一点的定义,正则表达式是用“正则表达式语言”来创建的,用于匹配和处理文本的字符串,它是内置于其它语言中的“迷你语言”。在不同语言中的正则表达式实现中,<strong>语法和功能可能会有一定差异</strong>(后面我们会详细讲一下)。</p>
<p><a href="https://regex101.com/" target="_blank" rel="external">https://regex101.com/</a> 是一个在线练习网站,我们可以在界面上勾选不同的编程语言,也可以看到正则表达式的性能(匹配完成所需时间)以及具体的匹配步骤。</p>
<h2 id="基础"><a href="#基础" class="headerlink" title="基础"></a>基础</h2><p>总结了一下,我觉得把正则中的语法符号分为四类比较容易记忆:</p>
<ul>
<li>字符与字符集</li>
<li>预定义字符类</li>
<li>重复次数</li>
<li>功能字符(最后这种是我概括出来的名字)</li>
</ul>
<h3 id="字符与字符集"><a href="#字符与字符集" class="headerlink" title="字符与字符集"></a>字符与字符集</h3><p>首先要分清<code>字符</code>与<code>字符集</code>。</p>
<p><code>/abc/</code> 中的 a 匹配的是单个字符,这个模式就匹配的是三个字符,当文本是 ‘abcd’ 时会<strong>一次性匹配到字符串 ‘abc’</strong>。</p>
<p>然而用 <code>[]</code> <strong>字符集操作符</strong>包裹起来的 <code>/[abc]/</code> 就是一个字符集,<code>[abc]</code> 匹配的是一个字符,表明匹配为 a 或 b 或 c 的一个字符。因此,当文本是 ‘abcd’ 时执行匹配,<strong>每次只能匹配到单个字符</strong>,第一次匹配到 ‘a’,第二次匹配到’b’……</p>
<p>区分这两个概念并不难,一般(我自己是)等到了各种表达式嵌套的时候就开始懵逼了。</p>
<p>只能在字符集中使用的操作符有两个,<code>取非操作符^</code> 与 <code>字符区间-</code>。</p>
<p>在字符集中我们可以使用<code>取非操作符^</code>,<code>/[^abc]/</code>即为匹配 a,b,c 以外的任意字符。</p>
<p><code>/[a-c]/</code> 与 <code>/[abc]/</code> 相同,通过字符区间我们可以编写 <code>/[A-Za-z0-9]/</code> 这种简洁易读的正则表达式了。</p>
<h3 id="预定义字符类"><a href="#预定义字符类" class="headerlink" title="预定义字符类"></a>预定义字符类</h3><p>正则表达式预定义了一些常用的术语来代表一类字符。</p>
<p>比如 <code>\d</code> 为任意数字,<code>\D</code> 为任意非数字,更多预定义字符可以参看 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions" target="_blank" rel="external">MDN</a>。</p>
<h3 id="重复次数"><a href="#重复次数" class="headerlink" title="重复次数"></a>重复次数</h3><p>以下符号跟在字符或者字符集的后面,代表重复次数:</p>
<ul>
<li><code>+</code>:重复一次或多次</li>
<li><code>*</code>:重复零次或多次</li>
<li><code>?</code>:重复零次或一次</li>
<li><code>{m, n}</code>:可表示区间,或是至少 m 次</li>
</ul>
<p>需要注意的是,除了 <code>?</code> 之外的三种都是贪婪型字符,可能会发生<strong>过度匹配</strong>的情况。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">var</span> str=<span class="string">'aacbacbc'</span>;</div><div class="line"><span class="keyword">var</span> reg=<span class="regexp">/a.*b/</span>;</div><div class="line"><span class="built_in">console</span>.log(str.match(reg)); <span class="comment">// ["aacbacb", index:0...]</span></div></pre></td></tr></table></figure>
<p>此时执行一次匹配,由于 <code>*</code> 作为贪婪型字符会尽可能匹配更多内容,因此匹配到的是 aacbacb,而不是 aacb。在贪婪型字符后面加上 <code>?</code> 即变为非贪婪字符。</p>
<h3 id="功能字符"><a href="#功能字符" class="headerlink" title="功能字符"></a>功能字符</h3><p>举几个例子:</p>
<ul>
<li><p>正则尾部的 <code>/i</code> 表示忽略大小写,<code>/g</code> 表示匹配所有实例,<code>/m</code> 多行匹配</p>
</li>
<li><p>竖线符号 <code>|</code> 表明“或”,<code>a|b</code> 即匹配 a 或 b</p>
</li>
<li><p>小括号 <code>()</code> 可以用来分割子表达式</p>
</li>
<li><p><code>^字符串$</code> 代表字符串的前后边界</p>
</li>
</ul>
<h2 id="进阶"><a href="#进阶" class="headerlink" title="进阶"></a>进阶</h2><p>以上介绍的都是基础语法与字符匹配规则,正则还有两种较为高级的使用语法。</p>
<h3 id="回溯引用-backreference"><a href="#回溯引用-backreference" class="headerlink" title="回溯引用 backreference"></a>回溯引用 backreference</h3><p><code>回溯引用</code>是指模式的后半部分引用在前半部分中定义的子表达式。</p>
<p>语法:</p>
<ol>
<li><code>(x)</code> 子表达式</li>
<li><code>\</code> 标识回溯引用,<code>\n</code> 即代表第 n 个子表达式所匹配到的内容(在 replace 操作中使用 <code>$</code>)</li>
</ol>
<p>举一个典型例子,匹配 HTML 中的标题标签,HTML 可能如下:</p>
<figure class="highlight html"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line"><span class="tag"><<span class="name">h1</span>></span>111<span class="tag"></<span class="name">h1</span>></span></div><div class="line"><span class="tag"><<span class="name">h4</span>></span>lalala<span class="tag"></<span class="name">h3</span>></span></div></pre></td></tr></table></figure>
<p>通过 <code>/<[Hh][1-5]>.*?<\/[Hh][1-5]>/</code> 我们会将 <code><h4>lalala\</h3></code> 这种非法标签也匹配到。回溯引用就适用于这种场景,它可以实现<strong>前后一致匹配</strong>。</p>
<p><code>/<([hH][1-5])>.*?<\/\1>/</code> 中的 <code>\1</code> 只匹配第一个子表达式 <code>([hH][1-5])</code> 所匹配到的内容,从而避免匹配到对应不上的标签组合。</p>
<h3 id="前后查找-lookaround"><a href="#前后查找-lookaround" class="headerlink" title="前后查找 lookaround"></a>前后查找 lookaround</h3><p>还是引用上面的标题标签匹配例子,如果我们想只匹配到 <code><h1>111</h1></code> 中的标题内容要怎么写正则呢?</p>
<p>这里涉及到两个新语法:</p>
<ul>
<li><code>?=</code>:向前查找</li>
<li><code>?<=</code>:向后查找</li>
</ul>
<p><strong>以向前、向后查找开头的子表达式</strong>就是前后查找。</p>
<p>因此,正则可以为:<code>/(?<=<[Hh][1-5]>).*(?=<\/[Hh][1-5]>)/</code></p>
<p>结合回溯引用与前后查找,还可以实现条件式的正则表达式,威力爆炸,只是这种形式的正则太难读了,有兴趣可以 Google 学习一下,这里不讲了。</p>
<h2 id="高级"><a href="#高级" class="headerlink" title="高级"></a>高级</h2><p>以上是正则表达式的知识点,现在讲讲坑。首先说说 JavaScript 中的正则。</p>
<h3 id="字面量-VS-RegExp"><a href="#字面量-VS-RegExp" class="headerlink" title="字面量 VS RegExp()"></a>字面量 VS RegExp()</h3><p>在 JavaScript 中创建正则表达式有两种方式:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// 正则字面量</span></div><div class="line"><span class="keyword">var</span> pattern1 = <span class="regexp">/\d+/</span>;</div><div class="line"></div><div class="line"><span class="comment">// 构造 RegExp 实例,以字符串形式传入正则</span></div><div class="line"><span class="keyword">var</span> pattern2 = <span class="keyword">new</span> <span class="built_in">RegExp</span>(<span class="string">'\\d+'</span>);</div></pre></td></tr></table></figure>
<p>两种方式创建出的正则没有任何差别。从创建方式上看,<strong>正则字面量可读性更优</strong>,因为正则中经常使用 <code>\</code> 反斜杠在字符串中是一个转义字符,想以字符串中表示反斜杠的话,需要使用 <code>\\</code> 两个反斜杠。</p>
<p>但是,需要注意,<strong>每个正则表达式都有一个独立的对象表示,每次创建正则表达式,都会为其创建一个新的正则表达式对象,这和其它类型(字符串、数组)不同</strong>。</p>
<p>我们可以通过<strong>让正则表达式只编译一次并将其保存在一个变量中以供后续使用</strong>来实现优化。</p>
<p>因此,第一段代码将创建三个正则表达式对象,并进行了三次编译,虽然表达式是相同的。而第二段代码则性能更高。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div></pre></td><td class="code"><pre><div class="line"><span class="built_in">console</span>.log(<span class="regexp">/abc/</span>.test(<span class="string">'a'</span>));</div><div class="line"><span class="built_in">console</span>.log(<span class="regexp">/abc/</span>.test(<span class="string">'ab'</span>));</div><div class="line"><span class="built_in">console</span>.log(<span class="regexp">/abc/</span>.test(<span class="string">'abc'</span>));</div><div class="line"></div><div class="line"><span class="keyword">var</span> pattern = <span class="regexp">/abc/</span>;</div><div class="line"><span class="built_in">console</span>.log(pattern.test(<span class="string">'a'</span>));</div><div class="line"><span class="built_in">console</span>.log(pattern.test(<span class="string">'ab'</span>));</div><div class="line"><span class="built_in">console</span>.log(pattern.test(<span class="string">'abc'</span>));</div></pre></td></tr></table></figure>
<p>这其中有<strong>性能隐患</strong>。先记住这一点,我们继续往下看。</p>
<h3 id="冷知识-lastIndex"><a href="#冷知识-lastIndex" class="headerlink" title="冷知识 lastIndex"></a>冷知识 lastIndex</h3><p>这里我们来解释下题图中的情况是怎么回事。</p>
<p><img src="/images/regex.jpg" alt="cover"></p>
<p>这其实是全局匹配的坑,也就是正则后的 <code>/g</code> 符号。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">var</span> pattern = <span class="regexp">/abc/g</span>;</div><div class="line"><span class="built_in">console</span>.log(pattern.global) <span class="comment">// true</span></div></pre></td></tr></table></figure>
<p>用 <code>/g</code> 标识的正则作为全局匹配,也就拥有了 global 属性并导致了题图中呈现的异常行为。</p>
<p>全局正则表达式的另一个属性 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/lastIndex" target="_blank" rel="external">lastIndex</a> 用于存放上一次匹配文本之后的第一个字符的位置。</p>
<p><code>RegExp.prototype.exec()</code> 和 <code>RegExp.prototype.test()</code> 方法都以 <code>lastIndex</code> 属性中所存储的位置作为下次正则匹配检索的起点。连续调用这两个方法就可以遍历字符串中的所有匹配文本。</p>
<p><code>lastIndex</code> 属性可读写,当 <code>RegExp.prototype.exec()</code> 或 <code>RegExp.prototype.test()</code> 再也找不到可以匹配的文本时,会自动把 lastIndex 属性重置为 0。因此<strong>使用这两个方法来检索文本,是可以无限执行下去的</strong>。我们也就明白了题图中为何每次执行 <code>RegExp.prototype.test()</code> 返回的结果都不一样。</p>
<p>不仅如此,看看下面这段代码,能看出来有什么问题吗?</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">var</span> count = <span class="number">0</span>;</div><div class="line"><span class="keyword">while</span> (<span class="regexp">/a/g</span>.test(<span class="string">'ababc'</span>)) count++;</div></pre></td></tr></table></figure>
<p>不要轻易拷贝到控制台中尝试,会把浏览器卡死的。</p>
<p>由于每个循环中 <code>/a/g.test('ababc')</code> 都创建了新的正则表达式对象,每次匹配都是重新开始,这一操作会无限执行下去,形成死循环。</p>
<p>正确的写法是:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">var</span> count = <span class="number">0</span>;</div><div class="line"><span class="keyword">var</span> regex = <span class="regexp">/a/g</span>;</div><div class="line"><span class="keyword">while</span> (regex.test(<span class="string">'ababc'</span>)) count++;</div></pre></td></tr></table></figure>
<p>这样,每次循环中操作的都是同一个正则表达式对象,随着每次匹配后 <code>lastIndex</code> 的增加,等到将整个字符串匹配完成后,就跳出循环了。</p>
<p>给以上知识点画个<strong>重点</strong>:</p>
<ol>
<li>将正则表达式保存到变量中,只在逻辑中使用这个变量,不仅性能更高,还安全。</li>
<li>谨慎使用全局匹配,<code>RegExp.prototype.exec()</code> 或 <code>RegExp.prototype.test()</code>这两个方法的执行结果可能每次都不同。</li>
<li>做到了以上两点后,还要谨慎在循环中使用正则匹配。</li>
</ol>
<h3 id="回溯陷阱-Catastrophic-Backtracking"><a href="#回溯陷阱-Catastrophic-Backtracking" class="headerlink" title="回溯陷阱 Catastrophic Backtracking"></a>回溯陷阱 Catastrophic Backtracking</h3><p>回溯陷阱是正则表达式本身的一个坑了,会导致非常严重的性能问题,事故现场可以参看<a href="https://juejin.im/post/5b287ea6f265da596d04a324" target="_blank" rel="external">《一个正则表达式引发的血案,让线上 CPU100% 异常!》</a>。</p>
<p>简单介绍一下回溯陷阱的问题源头,正则引擎分为 <code>NFA(确定型有穷自动机)</code> 和 <code>DFA(不确定型有穷自动机)</code>,<code>DFA</code> 是从匹配文本入手,同一个字符不会匹配两次(可以理解为手里捏着文本,挨个字符拿去匹配正则),时间复杂度是线性的,它的功能有限,不支持回溯。大多数编程语言选用的都是 <code>NFA</code>,相当于手里拿着正则表达式,去匹配文本。</p>
<p><code>/(a(bdc|cbd|bcd)/</code> 中已经有三种匹配路径,在 <code>NFA</code> 中,以文本 ‘abcd’ 为例,将花费 7 步才能匹配成功:</p>
<p><img src="https://s2.ax1x.com/2019/06/01/V1wCPe.jpg" alt="regex101"><br>(图中还包括了字符边界的匹配步骤,因此多了三步)</p>
<ol>
<li>正则中的第一个字符 a 匹配到 ‘abcd’ 中的第一个字母 ‘a’,匹配成功。</li>
<li>此时遇到了匹配路径的分叉口,bdc 或 cbd 或 bcd,先使用 bdc 来匹配。</li>
<li>bdc 中的第一个字符 b 匹配到了 ‘abcd’ 中的第二个字母 ‘b’,匹配成功。</li>
<li>bdc 中的第二个字符 d 与 ‘abcd’ 中的第三个字母 ‘c’ 不匹配,这条路径匹配失败,此时将发生回溯(backtrack),把 ‘b’ 还回去。选择第二条路径 cbd 进行匹配。</li>
<li>cbd 的第一个字符 ‘c’ 就与 ‘b’ 匹配失败。开始第三条路径 bcd 的匹配。</li>
<li>bcd 的第一个字符 ‘b’ 与文本 ‘b’ 匹配成功。</li>
<li>bcd 的第一个字符 ‘c’ 与文本 ‘c’ 匹配成功。</li>
<li>bcd 的第一个字符 ‘d’ 与文本 ‘d’ 匹配成功。</li>
</ol>
<p>至此匹配完成。</p>
<p>可想而知,如果正则中再多一些匹配路径或者匹配本文再长一点,匹配步骤将多到难以控制。</p>
<p>比如用 <code>/(a*)*bc/</code> 来匹配 ‘aaaaaaaaaaaabc’ 都会导致性能问题,匹配文本中每增加一个 ‘a’,都会导致执行时间翻倍。</p>
<p>禁止这种回溯陷阱的方法有两种:</p>
<ol>
<li>占有优先量词(Possessive Quantifiers)</li>
<li>原子分组(Atomic Grouping)</li>
</ol>
<p>可惜 <strong>JavaScript 不支持这两种语法</strong>,有兴趣可以 Google 自行了解下。</p>
<p>在 JavaScript 中我们没有方法可以直接禁止回溯陷阱,我们只能:</p>
<ol>
<li>避免量词嵌套 <code>(a*)* => a*</code></li>
<li>减少匹配路径</li>
</ol>
<p>除此之外,我们也可以把正则匹配放到 Service Worker 中进行,从而避免影响页面性能。</p>
<p>查资料的时候发现,回溯陷阱不仅会导致性能问题,也有安全问题,有兴趣可以看看先知白帽大会上的<a href="https://xianzhi.aliyun.com/forum/attachment/big_size/WAF%E6%98%AF%E6%97%B6%E5%80%99%E8%B7%9F%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E8%AF%B4%E5%86%8D%E8%A7%81.pdf" target="_blank" rel="external">《WAF是时候跟正则表达式说再见》</a>分享。</p>
<h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul>
<li><a href="https://book.douban.com/subject/26285406/" target="_blank" rel="external">《正则表达式必知必会》</a></li>
<li><a href="https://book.douban.com/subject/26638316/" target="_blank" rel="external">《JavaScript 忍者秘籍》第七章</a></li>
</ul>
<p><img src="/images/regex.jpg" alt="cover"></p>
<p>正则表达式基本上每用到一次就得从头自学一次,用完了写出来了也就忘光了。</p>
<p>前两天在 Twitter 上看到了题图,感觉又是个大坑,趁着手头还有 <a href="https://caraws.github.io/">Caraws</a> 给的书就又双叒叕学习了一遍正则表达式。</p>
<p>本文试图先用最易懂的方式理顺正则表达式的知识点(就不贴一摞一摞的文档截图了,至于正则的使用场景和用处也不啰嗦了),主要介绍正则本身和在 JavaScript 中使用正则的坑。</p>
浏览器 GPU 动画优化与 Render Pipeline
https://blog.colafornia.me/post/2018/gpu-animation-render-pipeline/
2018-05-28T10:17:00.000Z
2019-10-24T02:22:04.200Z
<p><img src="/images/render-pipeline-cover.jpg" alt="cover"></p>
<p>上周在组里做了一个小的技术分享,本文是对这次分享内容的一个文字化梳理。</p>
<a id="more"></a>
<h3 id="一、前言"><a href="#一、前言" class="headerlink" title="一、前言"></a>一、前言</h3><p><img src="https://p0.meituan.net/scarlett/e328aa2a7280812a89eced601d302f3418041.jpg" alt=""></p>
<p>一个 Web 页面由代码最终转化为屏幕上的像素点,大致遵循图中的步骤:</p>
<blockquote>
<p>JS/CSS > 样式 > 布局 > 绘制 > 合成</p>
</blockquote>
<p>① 指由 JavaScript 和 CSS 编写的动画代码</p>
<p>② 浏览器根据 CSS 选择器匹配计算(计算权重等)每个元素的最终样式</p>
<p>③ 浏览器计算元素所占的空间大小及其在屏幕上的位置(由于元素会互相影响,计算布局这一步骤会经常发生)</p>
<p>④ 在多个层上填充像素进行绘制,绘制每个元素的可视部分</p>
<p>⑤ 合成,将上一步中绘制出的多个层,正确合成到页面上</p>
<p>在之前的知识中,我们都知道,要正确使用、访问 CSS 属性,尽量少触发浏览器的 <code>重排重绘</code>,从而提升动画性能。</p>
<p>重排重绘指的是上述的③④步骤,本文主要探讨的是步骤⑤中的<strong>合成</strong>相关的概念与优化手段。</p>
<h3 id="二、渲染基础概念"><a href="#二、渲染基础概念" class="headerlink" title="二、渲染基础概念"></a>二、渲染基础概念</h3><p>在研究浏览器的 Composite 步骤前,有几个渲染相关的概念必须了解。</p>
<p>本文主要基于 Chrome 的内核 <code>Blink</code> 的渲染概念描述。</p>
<h4 id="1-Blink"><a href="#1-Blink" class="headerlink" title="1.Blink"></a>1.Blink</h4><p><img src="https://p1.meituan.net/scarlett/dc2500cbb537de8934117c2579ecad3227964.jpg" alt=""></p>
<p>在此之前我一直以为 Chrome 的内核依然是 <code>Webkit</code>,真是村通网。</p>
<p>实际上 <code>Webkit</code> 内核是苹果团队的开源作品,Chrome 在 2013 年之前一直基于其作为浏览器内核。直至 <code>Webkit2</code> 与 <code>Chromium</code> 的沙箱设计存在冲突,两方团队才决定分道扬镳。</p>
<p>Google 团队从 Webkit 中 fork 出一份代码,将在 <code>WebKit</code> 代码的基础上研发更加快速和简约的渲染引擎,并逐步脱离 <code>WebKit</code> 的影响,创造一个完全独立的 <code>Blink</code>(据说删掉了 Webkit 中 880W 行代码)。</p>
<p>由于此缘故,Blink 与 Webkit 对于渲染过程中的一些流程,术语并不完全相同,本文以 Blink 为准。</p>
<h4 id="2-RenderObject-与-RenderLayer"><a href="#2-RenderObject-与-RenderLayer" class="headerlink" title="2.RenderObject 与 RenderLayer"></a>2.RenderObject 与 RenderLayer</h4><p><img src="https://p1.meituan.net/scarlett/aabc7b208641fdf392dfe9b855a109b576936.jpg" alt=""></p>
<p>浏览器解析 HTML 文件生成 <code>DOM 树</code>,然而 <code>DOM 树</code>是不可以直接被用于排版的,内核还会再生成 <code>RenderObject 树</code>。每一个可见的 DOM 节点都会生成相应的 RenderObject 节点。</p>
<p>排版引擎经过 DOM 树与 CSS 定义对 Render 树进行排版,Render 树作为排版引擎的输出,渲染引擎的输入。</p>
<p>拥有相同坐标空间的 RenderObject 属于同一渲染层(RenderLayer),RenderLayer 最初被用来实现<code>层叠上下文(stacking context)</code>,以保证页面元素以正确顺序合成。</p>
<p>生成 RenderLayer 与具备层叠上下文的条件是一样的:</p>
<p><img src="https://p0.meituan.net/scarlett/85b3d7376bc7c693e872ca668bef980b232822.jpg" alt=""></p>
<h4 id="3-GraphicsLayer"><a href="#3-GraphicsLayer" class="headerlink" title="3.GraphicsLayer"></a>3.GraphicsLayer</h4><p><img src="https://p0.meituan.net/scarlett/d48ec20953d86e41c8fb62bb6267690d38392.jpg" alt=""></p>
<p>某些特殊的 <code>RenderLayer</code> 渲染层会被认为是<code>合成层(Compositing Layers)</code>,合成层拥有单独的 GraphicsLayer。这其实是浏览器为了提升动画性能做出的设计。</p>
<p>为了在动画的每一帧的过程中不必每次都重新绘制整个页面。在特定方式下可以触发生成一个合成层,合成层拥有单独的 <code>GraphicsLayer</code>。</p>
<p>需要进行动画的元素包含在这个合成层之下,这样动画的每一帧只需要去重新绘制这个 <code>GraphicsLayer</code> 即可,从而达到提升动画性能的目的。</p>
<p>生成 GraphicsLayer 的条件:</p>
<p><img src="https://p1.meituan.net/scarlett/5855a920418f70e0e50f1b0438601e42168085.jpg" alt=""></p>
<h3 id="三、Render-Pipeline-渲染流水线"><a href="#三、Render-Pipeline-渲染流水线" class="headerlink" title="三、Render Pipeline 渲染流水线"></a>三、Render Pipeline 渲染流水线</h3><p>在了解了以上渲染概念后,我们可以来看看一个极简版的渲染流水线示意图:</p>
<p><img src="https://p0.meituan.net/scarlett/ff9a521cfb9093806a408fa2ba2a51c26992.png" alt=""></p>
<p>Blink 内核运行在主线程上,负责 JavaScript 的解析执行,HTML/CSS 解析,DOM 操作,排版,图层树的构建和更新等任务。</p>
<p>Layer Compositor(图层合成器)运行在 Compositor 线程上,接收 Blink 的输入,负责图层树的管理。</p>
<p>Display Compositor 接收 Layer Compositor 的输入,负责输出最终的 OpenGL 绘制指令,将网页内容通过 GL 绘制到显示屏上。</p>
<p>将渲染流水线的内容按照线程做一下区分:</p>
<p><img src="https://p0.meituan.net/scarlett/f7537a196f23b0f1f6b7ed9d893fedea39673.jpg" alt=""></p>
<p>由此,Web 动画可以分为两大类:</p>
<ul>
<li>合成器动画:大多数基于 CSS 的动画,<code>transforms</code> 和 <code>opacity</code> 等都可以在合成线程中处理。</li>
<li>非合成器动画:引起了绘制、布局的动画,<code>Timer</code> 或者 <code>requestAnimationFrame</code> 等由 JavaScript 驱动的动画。</li>
</ul>
<p><strong>如果浏览器在主线程上运行一些耗时的任务,合成器动画可以继续运行而不会中断</strong>。</p>
<h3 id="四、Web-动画优化建议"><a href="#四、Web-动画优化建议" class="headerlink" title="四、Web 动画优化建议"></a>四、Web 动画优化建议</h3><p><img src="https://www.html5rocks.com/zh/tutorials/speed/high-performance-animations/cheap-operations.jpg" alt=""></p>
<p>现代浏览器在完成以上四种属性的动画时,消耗成本较低。根本原因是这四种属性生成了自己的图形层(GraphicsLayer),开启了 <code>GPU 硬件加速</code>。</p>
<p>开启 GPU 硬件加速的方法主要有两种:</p>
<ul>
<li>will-change</li>
<li>transform: translateZ(0)</li>
</ul>
<p>第二种我们应该都不陌生,第一种是 CSS3 的属性,它会通知浏览器你打算更改元素的属性。浏览器会在你进行更改之前做最合适的优化。</p>
<p>然而通过生成图形层(GraphicsLayer)的方式来进行性能优化却有个深坑 —— <code>隐式合成</code>。</p>
<p><img src="https://p1.meituan.net/scarlett/4651555d9265a7d0280b038d501d31ac60552.jpg" alt=""></p>
<p>如图所示,a, b两个元素都具有 absolute 和 z-index 属性,其中 a 元素的 z-index 大于 b,因此 a 位于 b 图层之上。</p>
<p>如果我们将 a 元素使用 left 属性,做一个移动动画,那么 a元素就有了一个合成层,动画得到了性能提升。</p>
<p>那么,如果 a 静止不动,我们让 b 元素做动画呢?b 元素将拥有一个独立合成层;然后它们将被 GPU 合成。但是因为 a 元素要在 b 元素的上面(因为 a 元素的 z-index 比 b 元素高),那么浏览器会做什么?<strong>浏览器会将 a 元素也单独做一个合成层!</strong></p>
<p>所以我们现在有三个合成层 a 元素所在的复合层、b 元素所在的合成层、其他内容及背景层。</p>
<p>没有自己合成层的元素要出现在有合成层元素的上方,它就会拥有自己的复合层;这种情况被称为<strong>隐式合成</strong>。</p>
<blockquote>
<p>GraphicsLayer 虽好,但不是越多越好,每一帧的渲染内核都会去遍历计算当前所有的 GraphicsLayer ,并计算他们下一帧的重绘区域,所以过量的 GraphicsLayer 计算也会给渲染造成性能影响。</p>
</blockquote>
<p>因此我们的最终结论是:</p>
<ol>
<li>尽量保持让需要进行 CSS 动画的元素的 z-index 保持在页面最上</li>
<li>有节制地优化,不要过早优化(不要滥用 will-change 等 GPU 加速手段)</li>
<li>根据 Chrome Devtool 查看 GraphicsLayer 每层占用的内存</li>
</ol>
<h3 id="五、参考内容"><a href="#五、参考内容" class="headerlink" title="五、参考内容"></a>五、参考内容</h3><ul>
<li><a href="https://www.smashingmagazine.com/2016/12/gpu-animation-doing-it-right/" target="_blank" rel="external">CSS GPU Animation: Doing It Right</a></li>
<li><a href="http://taobaofed.org/blog/2016/04/25/performance-composite/" target="_blank" rel="external">无线性能优化:Composite</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/30534023" target="_blank" rel="external">浏览器渲染流水线解析与网页动画性能优化</a></li>
</ul>
<p><img src="/images/render-pipeline-cover.jpg" alt="cover"></p>
<p>上周在组里做了一个小的技术分享,本文是对这次分享内容的一个文字化梳理。</p>
装饰者模式 => AOP => ES7 decorator => React 高阶组件
https://blog.colafornia.me/post/2018/from-decorator-to-hoc/
2018-05-19T08:56:00.000Z
2019-10-24T02:22:04.199Z
<p><img src="https://cdn-images-1.medium.com/max/1600/1*o6Q0MpSmQni2Q_sB5y9jig.png" alt=""></p>
<p>五月是学习的好时节啊,翻翻书继续学习一下设计模式吧。</p>
<p>该到<code>装饰者模式</code>了。来,学习一下。</p>
<p>书里的 <code>AOP</code> 是啥?学习一下。</p>
<p>新时代了再看看 ES7 规范的 <code>decorator</code> 吧,学习一下。。</p>
<p>啊还有 <code>React 高阶组件</code>的事儿呢,都学到这了,不差这一会儿。。。</p>
<a id="more"></a>
<h3 id="一、装饰者模式是什么"><a href="#一、装饰者模式是什么" class="headerlink" title="一、装饰者模式是什么"></a>一、装饰者模式是什么</h3><p>先看一下最为精确的英文维基定义:</p>
<blockquote>
<p>In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class.</p>
</blockquote>
<p>对于传统的 OOP 语言来说,给对象(object)添加功能通常使用继承的方式,这不仅导致了超类与子类间的强耦合,也违反了<a href="https://en.wikipedia.org/wiki/Single_responsibility_principle" target="_blank" rel="external">单一职责原则</a>。</p>
<p>装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。</p>
<h3 id="二、装饰者模式的典型应用:AOP"><a href="#二、装饰者模式的典型应用:AOP" class="headerlink" title="二、装饰者模式的典型应用:AOP"></a>二、装饰者模式的典型应用:AOP</h3><p>AOP 全称为 <code>Aspect-oriented programming</code>,即<code>面向切面编程</code>。主要适用于需要有横切逻辑的场景,比如数据上报,错误处理,鉴权,请求拦截等。</p>
<p>理解这个概念之后,实际操作就并不复杂。可以通过在原型上设置 <code>Function.prototype.before</code> 方法和 <code>Function.prototype.after</code> 方法,实现 <code>AOP 装饰函数</code>。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div></pre></td><td class="code"><pre><div class="line"><span class="built_in">Function</span>.prototype.before = <span class="function"><span class="keyword">function</span>(<span class="params">beforefn</span>)</span>{</div><div class="line"> <span class="keyword">var</span> __self = <span class="keyword">this</span>;</div><div class="line"> <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</div><div class="line"> beforefn.apply(<span class="keyword">this</span>, <span class="built_in">arguments</span>);</div><div class="line"> <span class="keyword">return</span> __self.apply(<span class="keyword">this</span>, <span class="built_in">arguments</span>);</div><div class="line"> }</div><div class="line">}</div><div class="line"></div><div class="line"><span class="built_in">Function</span>.prototype.after = <span class="function"><span class="keyword">function</span>(<span class="params">afterfn</span>)</span>{</div><div class="line"> <span class="keyword">var</span> __self = <span class="keyword">this</span>;</div><div class="line"> <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</div><div class="line"> <span class="keyword">var</span> ret = __self.apply(<span class="keyword">this</span>, <span class="built_in">arguments</span>);</div><div class="line"> afterfn.apply(<span class="keyword">this</span>, <span class="built_in">arguments</span>);</div><div class="line"> <span class="keyword">return</span> ret;</div><div class="line"> }</div><div class="line">};</div></pre></td></tr></table></figure>
<p>这两个装饰函数都接收函数作为参数,只是所接收参数的执行顺序不同。</p>
<p>同理,我们也可以给 service 编写装饰函数,作为接口拦截器。如 <a href="https://github.com/axios/axios" target="_blank" rel="external">axios</a> 中的 <code>Interceptors</code>:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// 给请求添加拦截器</span></div><div class="line">axios.interceptors.request.use(<span class="function"><span class="keyword">function</span> (<span class="params">config</span>) </span>{</div><div class="line"> <span class="comment">// 在发起请求前 do something</span></div><div class="line"> <span class="keyword">return</span> config;</div><div class="line"> }, <span class="function"><span class="keyword">function</span> (<span class="params">error</span>) </span>{</div><div class="line"> <span class="comment">// 处理错误</span></div><div class="line"> <span class="keyword">return</span> <span class="built_in">Promise</span>.reject(error);</div><div class="line"> });</div><div class="line"></div><div class="line"><span class="comment">// 给返回添加拦截器</span></div><div class="line">axios.interceptors.response.use(<span class="function"><span class="keyword">function</span> (<span class="params">response</span>) </span>{</div><div class="line"> <span class="comment">// 处理返回数据</span></div><div class="line"> <span class="keyword">return</span> response;</div><div class="line"> }, <span class="function"><span class="keyword">function</span> (<span class="params">error</span>) </span>{</div><div class="line"> <span class="comment">// 处理错误</span></div><div class="line"> <span class="keyword">return</span> <span class="built_in">Promise</span>.reject(error);</div><div class="line"> });</div></pre></td></tr></table></figure>
<p>这样,我们就可以在拦截器中统一处理错误与数据,不再需要在每一个 Promise 中都写一遍了,也便于统一项目中的处理方式。</p>
<p>关于 AOP 简单介绍到这里。</p>
<h3 id="三、ES7-Decorator"><a href="#三、ES7-Decorator" class="headerlink" title="三、ES7 Decorator"></a>三、ES7 Decorator</h3><p>ES7 的 decorator 装饰器借鉴于 Python 的思想,由 Yehuda Katz 提出,这里有<a href="https://github.com/wycats/javascript-decorators" target="_blank" rel="external">提案的细节设计与语法糖在 ES6/ES5 中的转换</a>。</p>
<p>定义非常简短:</p>
<blockquote>
<p>Decorators make it possible to annotate and modify classes and properties at design time.</p>
</blockquote>
<p>”装饰器可以让我们在设计时对类和类的属性进行注解和修改“</p>
<p>有点抽象,我们先全盘了解这些讯息再来研究到底是怎么回事。</p>
<p>想理解 <code>decorator</code> 的用法,离不开 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty" target="_blank" rel="external">Object.defineProperty</a>,ES7 也正是利用 <code>Object.defineProperty</code> 实现装饰器特性。</p>
<h4 id="1-前置知识:Object-defineProperty"><a href="#1-前置知识:Object-defineProperty" class="headerlink" title="1. 前置知识:Object.defineProperty"></a>1. 前置知识:Object.defineProperty</h4><p>如果了解过 Vue 双向绑定的实现原理,对 <code>Object.defineProperty</code> 就一定不陌生。</p>
<blockquote>
<p><code>Object.defineProperty(obj, prop, descriptor)</code></p>
<p>可以在对象上定义新属性,或修改已有属性,并将对象返回</p>
<p>参数 <code>obj</code>:要在其上添加或修改属性的对象</p>
<p>参数 <code>prop</code>:属性名</p>
<p>参数 <code>descriptor</code>:属性描述符,可以设置属性的数据属性与访问器属性</p>
</blockquote>
<p>其中 <code>descriptor</code> 可设置的属性有:</p>
<p>通用描述符:</p>
<ul>
<li>enumerable:Boolean,属性可否枚举</li>
<li>configurable:Boolean,若为 false,任何尝试删除目标属性或修改属性以下特性(writable, configurable, enumerable)的行为将被无效化</li>
</ul>
<p>数据描述符 data descriptor:</p>
<ul>
<li>value:属性值</li>
<li>writable:Boolean,是否可写</li>
</ul>
<p>访问器描述符 accessor descriptor:</p>
<ul>
<li>get: 一旦目标属性被访问就会调回此方法,并将此方法的运算结果返回用户。</li>
<li>set:一旦目标属性被赋值,就会调回此方法。</li>
</ul>
<p>(Vue 就是在 get 和 set 函数中进行了拦截,判断数据是否变化,发送通知到订阅器中,详情可参考<a href="https://blog.colafornia.me/2017/03/14/observer-pattern-in-vue/">《观察者模式以及在 Vue 源码中的实践》</a>)</p>
<h4 id="2-ES7-Decorator-的用法"><a href="#2-ES7-Decorator-的用法" class="headerlink" title="2. ES7 Decorator 的用法"></a>2. ES7 Decorator 的用法</h4><p>ES7 Decorator 的使用场景不少,我们先看最简单典型的一个示例:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">readonly</span>(<span class="params">target, name, descriptor</span>) </span>{</div><div class="line"> descriptor.writable = <span class="literal">false</span></div><div class="line"> <span class="keyword">return</span> descriptor</div><div class="line">}</div><div class="line"></div><div class="line"><span class="class"><span class="keyword">class</span> <span class="title">Cat</span> </span>{</div><div class="line"> @readonly</div><div class="line"> say() {</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">'喵'</span>);</div><div class="line"> }</div><div class="line">}</div><div class="line"></div><div class="line"><span class="keyword">let</span> tom = <span class="keyword">new</span> Cat();</div><div class="line">tom.say = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">'汪'</span>);</div><div class="line">}</div><div class="line">tom.say() <span class="comment">// 喵</span></div></pre></td></tr></table></figure>
<p>readonly 就是一个 decorator 装饰器,它通过设置修饰符的 <code>writable</code> 属性,使得被装饰的 <code>say()</code> 只读。</p>
<p>装饰器本身是一个函数,接受三个参数,target,name 和 descriptor。</p>
<p>写一个 log 装饰器来看看这仨参数都是啥:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">log</span>(<span class="params">target, name, descriptor</span>) </span>{</div><div class="line"> <span class="built_in">console</span>.log(target);</div><div class="line"> <span class="built_in">console</span>.log(target.hasOwnProperty(<span class="string">'constructor'</span>));</div><div class="line"> <span class="built_in">console</span>.log(target.constructor);</div><div class="line"> <span class="built_in">console</span>.log(name);</div><div class="line"> <span class="built_in">console</span>.log(descriptor);</div><div class="line">}</div><div class="line"></div><div class="line"><span class="class"><span class="keyword">class</span> <span class="title">Foo</span> </span>{</div><div class="line"> @log</div><div class="line"> bar() {}</div><div class="line">}</div><div class="line"></div><div class="line"><span class="keyword">const</span> test = <span class="keyword">new</span> Foo();</div><div class="line">test.bar();</div></pre></td></tr></table></figure>
<p>输出结果:</p>
<p><img src="/images/decorator1.jpg" alt=""></p>
<p>由此可以看出,target 就是被装饰的类本身,name 为被装饰的属性名,descriptor 与前述 Object.defineProperty 方法的属性描述符完全一样。</p>
<p>这仅仅是作为类属性的装饰器而言。实际上 decorator 有两种使用方法:</p>
<ul>
<li>装饰 Class,作为类装饰器</li>
<li>装饰类的属性</li>
</ul>
<p>作为类装饰器时,由于类本身是一个函数,因此 decorator 仅有 <code>target</code> 这一个参数。</p>
<p>需要注意的是,<strong>decorator 不能用于函数,因为存在函数提升</strong>。</p>
<h4 id="3-decorator-的使用场景"><a href="#3-decorator-的使用场景" class="headerlink" title="3. decorator 的使用场景"></a>3. decorator 的使用场景</h4><p>如前面所提到的 AOP 的用途,我们可以通过 decorator 实现横切逻辑,如日志上报,鉴权等。</p>
<p><a href="https://github.com/jayphelps/core-decorators/tree/master/src" target="_blank" rel="external">core-decorators</a> 中实现了一系列基础常用的装饰器,可以参考一下其中的实现。</p>
<p>平时开发中难免遇到需要使用定时器的场景,于是:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">setTimeout(() => {</div><div class="line"> doSomething();</div><div class="line">}, <span class="number">2000</span>);</div></pre></td></tr></table></figure>
<p>遇到一个就得写一个,函数被包裹来包裹去,并不是很美观。可以编写一个简单的 <code>timeout</code> 装饰器来重构:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">timeout</span>(<span class="params">milliseconds = 0</span>) </span>{</div><div class="line"> <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"> target, key, descriptor </span>) </span>{</div><div class="line"> <span class="keyword">const</span> fn = descriptor.value;</div><div class="line"> descriptor.value = <span class="function"><span class="keyword">function</span> (<span class="params">...args</span>) </span>{</div><div class="line"> setTimeout(() => {</div><div class="line"> fn.apply(<span class="keyword">this</span>, args);</div><div class="line"> }, milliseconds);</div><div class="line"> };</div><div class="line"> <span class="keyword">return</span> descriptor;</div><div class="line"> }</div><div class="line">}</div><div class="line"></div><div class="line"><span class="class"><span class="keyword">class</span> <span class="title">Demo</span> </span>{</div><div class="line"> <span class="keyword">constructor</span>() {}</div><div class="line"> @timeout()</div><div class="line"> doSomething() {}</div><div class="line"> @timmeout(<span class="number">2000</span>)</div><div class="line"> doAnotherThing() {}</div><div class="line">}</div></pre></td></tr></table></figure>
<p>代码结构清晰多了,装饰器也起到了注释的作用。</p>
<h4 id="4-decorator-在什么时候运行?"><a href="#4-decorator-在什么时候运行?" class="headerlink" title="4. decorator 在什么时候运行?"></a>4. decorator 在什么时候运行?</h4><p>尝试一下:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">log</span>(<span class="params">message</span>) </span>{</div><div class="line"> <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</div><div class="line"> <span class="built_in">console</span>.log(message);</div><div class="line"> }</div><div class="line">}</div><div class="line"></div><div class="line"><span class="built_in">console</span>.log(<span class="string">'before class'</span>);</div><div class="line"></div><div class="line">@log(<span class="string">'class Bar'</span>)</div><div class="line"><span class="class"><span class="keyword">class</span> <span class="title">Bar</span> </span>{</div><div class="line"> @log(<span class="string">'class method bar'</span>)</div><div class="line"> bar() {}</div><div class="line"></div><div class="line"> @log(<span class="string">'class property foo'</span>)</div><div class="line"> foo = <span class="number">1</span>;</div><div class="line">}</div><div class="line"></div><div class="line"><span class="built_in">console</span>.log(<span class="string">'after class'</span>)</div><div class="line"></div><div class="line"><span class="keyword">let</span> bar = {</div><div class="line"> @log(<span class="string">'object method bar'</span>)</div><div class="line"> bar() {}</div><div class="line">};</div></pre></td></tr></table></figure>
<p>输出结果:<br><img src="/images/decorator2.jpg" alt=""></p>
<p>由此我们可以看出:</p>
<blockquote>
<p>装饰器是在声明期就起效的,并不需要类进行实例化。</p>
<p>类实例化并不会致使装饰器多次执行,因此不会对实例化带来额外的开销。</p>
<p>按编码时的声明顺序执行,并不会将属性、方法、访问器进行重排序。</p>
<p>因为以上这 2 个规则,我们需要特别注意一点,在装饰器运行时,你所能得到的环境是空的,在 Bar.prototype 或者 Bar 上的属性是获取不到的,也就是说整个 target 里其实只有 constructor 这一个属性。</p>
<p>换句话说,装饰器运行时所有的属性和方法均未定义。</p>
</blockquote>
<h3 id="四、React-高阶组件"><a href="#四、React-高阶组件" class="headerlink" title="四、React 高阶组件"></a>四、React 高阶组件</h3><p>之所以会有<code>高阶组件 higher-order component(HOC)</code>这个东西,主要是为了实现<strong>组件的抽象</strong>。</p>
<h4 id="1-Mixin"><a href="#1-Mixin" class="headerlink" title="1. Mixin"></a>1. Mixin</h4><p>想了解 HOC 干了啥,以及为啥需要它。依然要用 Vue 举例,Vue 的 <code>mixins</code> 混入方法实现了组件的混入,借此我们可以将组件粒度切细,使得项目高度配置化。</p>
<p>官网示例:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// 定义一个混入对象</span></div><div class="line"><span class="keyword">var</span> myMixin = {</div><div class="line"> created: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>{</div><div class="line"> <span class="keyword">this</span>.hello()</div><div class="line"> },</div><div class="line"> methods: {</div><div class="line"> hello: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>{</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">'hello from mixin!'</span>)</div><div class="line"> }</div><div class="line"> }</div><div class="line">}</div><div class="line"></div><div class="line"><span class="comment">// 定义一个使用混入对象的组件</span></div><div class="line"><span class="keyword">var</span> Component = Vue.extend({</div><div class="line"> mixins: [myMixin]</div><div class="line">})</div><div class="line"></div><div class="line"><span class="keyword">var</span> component = <span class="keyword">new</span> Component() <span class="comment">// => "hello from mixin!"</span></div></pre></td></tr></table></figure>
<p>Vue 中的 mixin 数据对象在内部会进行浅合并 (一层属性深度),在和组件的数据发生冲突时<strong>以组件数据优先</strong>。这也是实现 mixin 的重点逻辑。</p>
<p>看看 <code>core-decorators</code> 中 mixin 的核心实现:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">import</span> { getOwnPropertyDescriptors } <span class="keyword">from</span> <span class="string">'./private/utils'</span>;</div><div class="line"></div><div class="line"><span class="keyword">const</span> { defineProperty } = <span class="built_in">Object</span>;</div><div class="line"></div><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">handleClass</span>(<span class="params">target, mixins</span>) </span>{</div><div class="line"> <span class="keyword">if</span> (!mixins.length) {</div><div class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">SyntaxError</span>(<span class="string">`@mixin() class <span class="subst">${target.name}</span> requires at least one mixin as an argument`</span>);</div><div class="line"> }</div><div class="line"></div><div class="line"> <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>, l = mixins.length; i < l; i++) {</div><div class="line"> <span class="keyword">const</span> descs = getOwnPropertyDescriptors(mixins[i]);</div><div class="line"> <span class="keyword">const</span> keys = getOwnKeys(descs);</div><div class="line"></div><div class="line"> <span class="keyword">for</span> (<span class="keyword">let</span> j = <span class="number">0</span>, k = keys.length; j < k; j++) {</div><div class="line"> <span class="keyword">const</span> key = keys[j];</div><div class="line"></div><div class="line"> <span class="keyword">if</span> (!(hasProperty(key, target.prototype))) {</div><div class="line"> defineProperty(target.prototype, key, descs[key]);</div><div class="line"> }</div><div class="line"> }</div><div class="line"> }</div><div class="line">}</div><div class="line"></div><div class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="function"><span class="keyword">function</span> <span class="title">mixin</span>(<span class="params">...mixins</span>) </span>{</div><div class="line"> <span class="keyword">if</span> (<span class="keyword">typeof</span> mixins[<span class="number">0</span>] === <span class="string">'function'</span>) {</div><div class="line"> <span class="keyword">return</span> handleClass(mixins[<span class="number">0</span>], []);</div><div class="line"> } <span class="keyword">else</span> {</div><div class="line"> <span class="keyword">return</span> target => {</div><div class="line"> <span class="keyword">return</span> handleClass(target, mixins);</div><div class="line"> };</div><div class="line"> }</div><div class="line">}</div></pre></td></tr></table></figure>
<p>其中把待 mixin 对象的每个方法都叠加到了 target 对象的原型上。其中通过 <code>defineProperty</code> 这个方法避免了覆盖 target 的原有属性。</p>
<p>但是 mixin 有很多弊病,这也是为什么最后我们选择了高阶组件来实现组件的 compose。主要问题有:</p>
<ul>
<li><strong>破坏组件原有封装</strong>:被 mixin 进来的组件都有自己的 props 和 state,导致在引入的时候需要千般小心,去维护那些我们不可见的状态。</li>
<li><strong>命名冲突</strong>:mixin 是一个平面结构,不同 mixin 中的命名不可知,譬如 <code>handleChange</code> 这种常见名就很容易冲突,无形中增加了开发和维护成本。</li>
</ul>
<p>因此高阶组件应运而生。</p>
<h4 id="2-高阶组件"><a href="#2-高阶组件" class="headerlink" title="2. 高阶组件"></a>2. 高阶组件</h4><p><code>高阶组件(higher-order component)</code>的概念类似于<code>高阶函数</code>,它接受 React 组件作为输入,输出一个新的 React 组件:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">const</span> EnhancedComponent = higherOrderComponent(WrappedComponent);</div></pre></td></tr></table></figure>
<p>先看一个最简单的例子:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">import</span> React, { Component } <span class="keyword">from</span> <span class="string">'react'</span>;</div><div class="line"><span class="keyword">import</span> simpleHoc <span class="keyword">from</span> <span class="string">'./simple-hoc'</span>;</div><div class="line"></div><div class="line"><span class="class"><span class="keyword">class</span> <span class="title">Normal</span> <span class="keyword">extends</span> <span class="title">Component</span> </span>{</div><div class="line"> <span class="comment">// 可以做很多自定义逻辑</span></div><div class="line"> render() {</div><div class="line"> <span class="built_in">console</span>.log(<span class="keyword">this</span>.props, <span class="string">'props'</span>);</div><div class="line"> <span class="keyword">return</span> (</div><div class="line"> <div></div><div class="line"> Usual</div><div class="line"> </div></div><div class="line"> )</div><div class="line"> }</div><div class="line">}</div><div class="line"><span class="keyword">export</span> <span class="keyword">default</span> simpleHoc(Normal);</div></pre></td></tr></table></figure>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">import</span> React, { Component } <span class="keyword">from</span> <span class="string">'react'</span>;</div><div class="line"></div><div class="line"><span class="keyword">const</span> simpleHoc = WrappedComponent => {</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">'im a hoc!'</span>);</div><div class="line"> <span class="keyword">return</span> <span class="class"><span class="keyword">class</span> <span class="keyword">extends</span> <span class="title">Component</span> </span>{</div><div class="line"> render() {</div><div class="line"> <span class="keyword">return</span> <WrappedComponent {...this.props}/></div><div class="line"> }</div><div class="line"> }</div><div class="line">}</div><div class="line">export default simpleHoc;</div></pre></td></tr></table></figure>
<p>我们所定义的 Normal 组件通过 simpleHoc 的包裹后输出的新组件后,在 Normal 本身的功能上可以多打一个 Log,并继承了 simpleHoc 的 props。这是最简单的一个例子啦,高阶组件做的事情也比较逊。我们继续看看~</p>
<p>实现高阶组件的方法有两种:</p>
<ol>
<li>属性代理(<code>props proxy</code>):高阶组件通过 WrappedComponent 来操作 props</li>
<li>反向代理(<code>inheritance inversion</code>):高阶组件继承于 WrappedComponent</li>
</ol>
<p>这两种方法的使用场景也各不相同。</p>
<h3 id="五、实现高阶组件的两种方法与使用场景"><a href="#五、实现高阶组件的两种方法与使用场景" class="headerlink" title="五、实现高阶组件的两种方法与使用场景"></a>五、实现高阶组件的两种方法与使用场景</h3><h4 id="1-属性代理"><a href="#1-属性代理" class="headerlink" title="1. 属性代理"></a>1. 属性代理</h4><p>这是较为常见的一种方法,上面的 <code>simpleHoc</code> 的实现其实就属于属性代理。<strong>通过高阶组件传递 props 的方法就是属性代理</strong>。</p>
<p>使用场景:</p>
<ul>
<li>操作 <code>props</code></li>
<li>通过 <code>Refs</code> 访问到组件实例</li>
<li>提取 <code>state</code></li>
<li>用其他元素包裹 <code>WrappedComponent</code></li>
</ul>
<p>我们主要介绍一下最常见的,操作 props。其它三种应用在网上也能找到具体例子,不赘述了(文章到这里感觉已经非常长了……)</p>
<p>我们可以通过属性代理,来读取,编辑,增加或是删除 WrappedComponent 的 props。但应该注意小心编辑、删除重要的 props,尽量通过<strong>对高阶组件的 props 作新的命名来避免混淆</strong>。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">myHOC</span> (<span class="params">WrappedComponent</span>) </span>{</div><div class="line"> <span class="keyword">return</span> <span class="class"><span class="keyword">class</span> <span class="title">myHoc</span> <span class="keyword">extends</span> <span class="title">React</span>.<span class="title">Component</span> </span>{</div><div class="line"> render() {</div><div class="line"> <span class="keyword">const</span> newProps = {</div><div class="line"> user: currentLoggedInUser</div><div class="line"> }</div><div class="line"> <span class="keyword">return</span> <WrappedComponent {...this.props} {...newProps}/></div><div class="line"> }</div><div class="line"> }</div><div class="line">}</div></pre></td></tr></table></figure>
<p>这样,输出的新组件就可以通过 <code>this.props.user</code> 来获得当前登录人的信息。</p>
<p>使用的时候可以通过 decorator 来简化:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line">@myHoc</div><div class="line"><span class="class"><span class="keyword">class</span> <span class="title">MyComponent</span> <span class="keyword">extends</span> <span class="title">React</span>.<span class="title">Component</span> </span>{</div><div class="line"> render() {}</div><div class="line">}</div><div class="line"><span class="keyword">export</span> <span class="keyword">default</span> MyComponent;</div></pre></td></tr></table></figure>
<h4 id="2-反向继承"><a href="#2-反向继承" class="headerlink" title="2. 反向继承"></a>2. 反向继承</h4><p>先看例子:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">myHOC</span>(<span class="params">WrappedComponent</span>) </span>{</div><div class="line"> <span class="keyword">return</span> <span class="class"><span class="keyword">class</span> <span class="title">myHoc</span> <span class="keyword">extends</span> <span class="title">WrappedComponent</span> </span>{</div><div class="line"> render() {</div><div class="line"> <span class="keyword">return</span> <span class="keyword">super</span>.render()</div><div class="line"> }</div><div class="line"> }</div><div class="line">}</div></pre></td></tr></table></figure>
<p>高阶组件返回的组件继承于 WrappedComponent,因此被称为 <code>Inheritance Inversion</code> 反向继承。</p>
<p><strong>反向继承模式下的高阶组件可以通过 this 访问到 WrappedComponent 的 state、props、组件生命周期方法和 render 方法</strong>。</p>
<p>使用场景:</p>
<ul>
<li>渲染劫持(<code>Render Highjacking</code>)</li>
<li>操作 <code>state</code></li>
</ul>
<p>渲染劫持是指高阶组件可以控制 WrappedComponent 的渲染过程并修改渲染结果,这意味着可以:</p>
<ul>
<li>在由 render <strong>输出</strong>的任何 React 元素中读取、添加、编辑、删除 props</li>
<li>读取和修改由 render 输出的 React 元素树</li>
<li>有条件地渲染元素树</li>
<li>把样式包裹进元素树(就像在 <code>Props Proxy</code> 中的那样)</li>
</ul>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">myHOC</span>(<span class="params">WrappedComponent</span>) </span>{</div><div class="line"> <span class="keyword">return</span> <span class="class"><span class="keyword">class</span> <span class="title">Enhancer</span> <span class="keyword">extends</span> <span class="title">WrappedComponent</span> </span>{</div><div class="line"> render() {</div><div class="line"> <span class="keyword">if</span> (<span class="keyword">this</span>.props.show) {</div><div class="line"> <span class="keyword">return</span> <span class="keyword">super</span>.render()</div><div class="line"> } <span class="keyword">else</span> {</div><div class="line"> <span class="keyword">return</span> <span class="literal">null</span></div><div class="line"> }</div><div class="line"> }</div><div class="line"> }</div><div class="line">}</div></pre></td></tr></table></figure>
<p>前面提到了高阶组件可以通过 this 访问到 WrappedComponent 的 state,可以对其进行编辑、删除,但这会使得 WrappedComponent 的内部状态混乱,难以维护,应避免这样使用。</p>
<p>最后,我们来看下高阶组件与 Mixin 的区别:</p>
<p><img src="/images/decorator3.jpg" alt=""></p>
<p>高阶组件更符合函数式编程思想,原组件不会感知到高阶组件的存在,最后我们所使用的都是一个新组件,从而避免了 Mixin 的那些弊病。</p>
<h3 id="五、参考内容"><a href="#五、参考内容" class="headerlink" title="五、参考内容"></a>五、参考内容</h3><ul>
<li><a href="https://book.douban.com/subject/26918038/" target="_blank" rel="external">《深入React技术栈》</a></li>
<li><a href="http://efe.baidu.com/blog/introduction-to-es-decorator/" target="_blank" rel="external">ES Decorators简介</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/24776678" target="_blank" rel="external">深入理解 React 高阶组件</a></li>
</ul>
<p><img src="https://cdn-images-1.medium.com/max/1600/1*o6Q0MpSmQni2Q_sB5y9jig.png" alt=""></p>
<p>五月是学习的好时节啊,翻翻书继续学习一下设计模式吧。</p>
<p>该到<code>装饰者模式</code>了。来,学习一下。</p>
<p>书里的 <code>AOP</code> 是啥?学习一下。</p>
<p>新时代了再看看 ES7 规范的 <code>decorator</code> 吧,学习一下。。</p>
<p>啊还有 <code>React 高阶组件</code>的事儿呢,都学到这了,不差这一会儿。。。</p>
Git 原理应知应会
https://blog.colafornia.me/post/2018/dive-into-git/
2018-05-08T05:10:00.000Z
2019-10-24T02:22:04.197Z
<p><img src="/images/dive-into-git/cover.jpg" alt="cover"></p>
<p>初学编程时,Git 算是最令人心有余悸的 Boss 了,毕竟相比于写出 Bug 这种常见事情,把自己/别人代码弄丢这种事更为可怕。</p>
<p>本文只介绍 Git 原理中最为硬核的部分,如果想把所有原理都探究明白,内容多到可以写本书。因此,本文只试图解答:</p>
<blockquote>
<ol>
<li>Git 是怎么存储内容的?存到哪里了?</li>
<li>工作区,暂存区,版本库,各种教程里这仨名词一看就晕,能说人话吗?</li>
<li>git reset 来,git reset 去,感觉啥都能 reset,我到底在干啥?</li>
<li>Git 快在哪里?大神都说 Git 好用,怎么还这么难学?</li>
</ol>
</blockquote>
<p>Let’s go!</p>
<a id="more"></a>
<h3 id="一、Git-简介"><a href="#一、Git-简介" class="headerlink" title="一、Git 简介"></a>一、Git 简介</h3><p>首先回顾一下 Git 是什么,Git 是一个<code>分布式版本控制系统(Distributed Version Control System - DVCS)</code>。由 Linux 之父 Linus Torvalds 用两周时间写出了第一个版本。经过十多年的发展,Git 的内部设计基本没有变化。</p>
<p>它与先前流行的版本控制系统 <code>SVN</code> 相比,区别在于,除了中央仓库,还有本地仓库。中央仓库仅负责同步团队代码,其它如查看历史,提交代码等操作可以在成员的本地仓库中进行。除此之外,SVN 存储的是版本间的文件差异,Git 存储的则是每一个版本的<code>快照</code>,后面我们将详细说明。</p>
<h3 id="二、Git-文件系统"><a href="#二、Git-文件系统" class="headerlink" title="二、Git 文件系统"></a>二、Git 文件系统</h3><p>我们对于 Git 的使用,都是在使用命令,例如 <code>git add</code>,<code>git commit</code> 等,它们都属于 Git 的<code>高层命令</code>。</p>
<p>通过 <code>git help -a</code> 查看所有的 Git 命令:</p>
<p><img src="/images/dive-into-git/dive-into-git1.jpg" alt="dive-into-git1"></p>
<p>实际上,Git 有一百多个命令,其中<code>底层命令</code>被设计为 Unix 风格,由脚本调用,并不常用。我们平时所使用的<code>高层命令</code>设计的更为友好(可以理解为是被高度封装过的现成工具)。因此,想要了解 Git 原理就必须从<code>底层命令</code>入手。</p>
<p><img src="https://yanhaijing.com/blog/464.png" alt=""></p>
<p>每当执行 <code>git init</code> 时,Git 便会创建一个 <code>.git</code> 目录,几乎 Git 所储存,操作的所有内容都在这个目录下(如果想拷贝一个 Git 仓库,拷贝这个目录即可)。了解 Git 原理也可以称为了解这个这个目录各部分是做什么的。</p>
<p><img src="/images/dive-into-git/dive-into-git2.jpg" alt="dive-into-git2"><br>(图中是我的一个小项目的 <code>.git</code> 目录)</p>
<ul>
<li>指针 (HEAD, FETCH_HEAD, ORIG_HEAD)</li>
<li>对象<ul>
<li>objects/(所有的对象,包括blob, tree, commit)</li>
<li>refs/ (所有的引用)<ul>
<li>local branch</li>
<li>remote branch</li>
<li>tag</li>
</ul>
</li>
</ul>
</li>
<li>index (索引)</li>
<li>config (设置)</li>
</ul>
<p>接下来我们来重点介绍一下 objects 和 refs 两个部分。</p>
<h4 id="1-objects"><a href="#1-objects" class="headerlink" title="1. objects"></a>1. objects</h4><p><img src="/images/dive-into-git/dive-into-git7.jpg" alt="dive-into-git7"><br>(刚初始化的项目,objects 目录下只有两个空文件夹)</p>
<p>首先看一下最基本的部分,objects 目录,Git 所存储的数据都在这里。我们来看看 Git 到底是怎么存储内容的吧。</p>
<p>前面提到了 Git 存储的是<code>快照</code>,这实际上说的是 <code>SHA-1 哈希值</code>。</p>
<blockquote>
<p>Git 为每份内容生成一个文件,取得其 SHA-1 哈希值,用哈希值的前两个字符为名称创建子目录,用剩下 38 个字符为文件命名 (保存至子目录下)。</p>
</blockquote>
<p>听起来有点绕口,我们可以动手操作实验一下,通过底层命令 <code>hash-object</code> 可以计算内容的 SHA-1 值:</p>
<p><img src="/images/dive-into-git/dive-into-git3.jpg" alt="dive-into-git3"><br>字符串 hello git 的哈希结果是 <code>8d0e41234f24b6da002d962a26c2495ea16a425f</code></p>
<p>把这段字符串保存在一个文件中再计算 SHA-1:</p>
<p><img src="/images/dive-into-git/dive-into-git4.jpg" alt="dive-into-git4"></p>
<p>得到了一样的哈希值。</p>
<p>改变一下文件的文本内容,哈希值则发生了改变:</p>
<p><img src="/images/dive-into-git/dive-into-git5.jpg" alt="dive-into-git5"></p>
<p>至此,我们已经知道:</p>
<blockquote>
<ol>
<li>Git 由文件内容计算其哈希值</li>
<li>哈希值相同则文件内容相同(即使我们将一个文件拷贝到不同目录下,Git 也仅存储一份内容)</li>
</ol>
</blockquote>
<p>现在我们把 hello.txt 文件提交:</p>
<p><img src="/images/dive-into-git/dive-into-git8.jpg" alt="dive-into-git8"></p>
<p>再看一下 objects 目录。</p>
<p><img src="/images/dive-into-git/dive-into-git6.jpg" alt=""></p>
<p>发现多了三个文件夹!其中 8d 文件夹和子文件名加起来(<code>8d0e4123...</code>)正好是字符串 hello git 的哈希结果。而 10 文件夹则是本次 commit 的哈希值。</p>
<p>除了 <code>hash-object</code> 命令之外,还有一个好用的底层命令 <code>cat-file</code>,它可以将数据内容取回,传入 <code>-p</code> 参数可以让该命令输出数据内容的类型。我们拿 commit 的哈希值试一试:</p>
<p><img src="/images/dive-into-git/dive-into-git9.jpg" alt="dive-into-git9"><br>我们得到了 commit 对象,其中包含了本次提交的时间,commit message,提交者信息,以及一个类型为 <code>tree</code> 的哈希值 07ed5a7。对其取值查看,发现了第三个哈希对象,类型为 blob,其哈希值为 hello.txt 内容的哈希值。</p>
<p>三个哈希对象之间的关系:</p>
<p><img src="/images/dive-into-git/dive-into-git10.jpg" alt="dive-into-git10"><br>在此,我们已经知道了三种 <code>Git 基本对象</code>:</p>
<ul>
<li>Blob 对象:对单个文件的压缩存储</li>
<li>Tree 对象:对文件目录树的存储</li>
<li>Commit 对象:对 tree 对象的包装,带有其它提交信息</li>
</ul>
<p>因此仓库中的一个常规项目结构,在 .git 中会存储为右图所示的结构:</p>
<p><img src="/images/dive-into-git/dive-into-git12.jpg" alt="dive-into-git12"></p>
<p>现在我们再把 bye.txt 也提交了。可以发现新的 commit 哈希对象中还包含了 parent 信息,其值为上一个 commit 的哈希值。</p>
<p>Git history 中的各个 commit 其实是一个<code>单向链表</code>的结构,通过 parent 关联父节点。</p>
<p><img src="/images/dive-into-git/dive-into-git11.jpg" alt="dive-into-git11"><br>其中每个 commit 中都包含了当时仓库的目录结构与文件内容。这便是达成版本管理的基础。</p>
<h4 id="2-refs"><a href="#2-refs" class="headerlink" title="2. refs"></a>2. refs</h4><p>refs 目录存储了所有的引用文件。</p>
<p><img src="/images/dive-into-git/dive-into-git13.jpg" alt="dive-into-git13"><br>引用文件的内容也都是 40 位的 SHA-1 值。先看一下 master 是什么:</p>
<p><code>cat .git/refs/heads/master</code></p>
<p>哈希值为 <code>38779e1ee3e4959e21e599ad0974a2c915613d9e</code>,就是第二次提交 commit 的哈希值。我们可以猜测,<strong>branch 其实就是 commit 的引用</strong>。为了验证一下这个想法,我们新建一个分支试试:</p>
<p><img src="/images/dive-into-git/dive-into-git14.jpg" alt="dive-into-git14"><br>可以发现,refs 目录中多了一个与新分支同名的文件,且其哈希值依然为第二次提交 commit 的哈希值。所以我们的猜想没错。</p>
<p>但是当我们新建 new-branch 分支时,Git 是怎么知道最后一次提交的 SHA-1 值呢?答案就是 <code>HEAD 文件</code>。<strong><code>HEAD 文件</code>是一个指向你当前所在分支的引用标识符</strong>。也就是我们每次查找 log 时看到的 HEAD 标记:</p>
<p><img src="/images/dive-into-git/dive-into-git15.jpg" alt="dive-into-git15"></p>
<p>介绍到这里,我们可以发现<strong>Git 中的引用是非常廉价的</strong>,开新的 branch 和 tag 都只是多了一个引用文件,而有些中央式版本控制系统开分支时会复制一份内容,非常耗费资源。</p>
<p>分支是一种移动的引用。而标签则是静止的引用。<code>.git/refs</code> 目录下的 tags 目录就是保存标签信息的地方,标签同样也是 commit 对象的引用,只是它<strong>永远指向同一个 commit,不会变化</strong>。</p>
<p>最后一种引用类型是 <code>remote reference 远程引用</code>,我们每次执行将提交 push 到远端后,<code>.git/refs/remotes/</code> 目录下就会记录此次与远端通信的最后一个 commit 的哈希值。</p>
<p>与 Git 的引用文件强相关的高层命令是 <code>git reset [--soft | --mixed | --hard | --merge | --keep] [-q] [<commit>]</code>,虽然这个命令后面可以加很多参数,但实际上它们所操作的都是 <code>.git/refs/heads</code> 目录下当前分支对应的引用文件。</p>
<h3 id="三、Git-暂存区"><a href="#三、Git-暂存区" class="headerlink" title="三、Git 暂存区"></a>三、Git 暂存区</h3><p>工作区,暂存区,版本库,这三个名词是我一开始看各种 Git 教程时最脑阔疼的东西,每次都要小心辨认,再心里默念一遍才分的清楚。其中工作区和版本库还好理解,<code>暂存区</code> 是最为懵逼的一个概念。</p>
<p>所以来看看 Git 暂存区到底是什么,为什么需要这个东西呢。</p>
<p>在 .git 目录下中有一个 index 文件它与暂存区的概念相关,我们动手实验看看它是干啥的。就着前面的实验,我们继续操作一下,改一下 hello.txt 的内容,然后执行 <code>git checkout</code> 撤销对这个文件的修改:</p>
<p><img src="/images/dive-into-git/dive-into-git16.jpg" alt="dive-into-git16"><br>撤销后工作区已经没有文件改动了,发现 <code>.git/index</code>文件的时间戳是 17:02:00。</p>
<p><img src="/images/dive-into-git/dive-into-git17.jpg" alt="dive-into-git17"><br>再通过 <code>git status</code> 看一下工作区状态,发现 <code>.git/index</code> 文件的时间戳没有变化。</p>
<p>我们用 Linux 命令 <code>touch</code> 改一下 hello.txt 的时间戳再看看:</p>
<p><img src="/images/dive-into-git/dive-into-git18.jpg" alt="dive-into-git18"><br>发现虽然文件没有变化,<code>.git/index</code> 文件的时间戳却发生了改变。</p>
<blockquote>
<p>这是因为 <code>git status</code> 命令查看工作区状态时,先根据 .git/index 文件中记录的时间戳,长度等信息判断工作区文件是否改变。如果时间戳变了,说明文件<strong>有可能</strong>发生改变,Git 需要读取文件,与原始文件进行对比,去判断它是否发生变化。如果没有改变,则将文件新的时间戳记录到 .git/index 文件中。</p>
<p><strong>因为判断文件是否更改,使用时间戳、文件长度等信息进行比较要比通过文件内容比较要快的多</strong>,所以 Git 这样的实现方式可以让工作区状态扫描更快速的执行,这也是 Git 高效的因素之一。</p>
</blockquote>
<p><strong>.git/index 文件实际上是一个包含文件索引的目录树,就是所谓的<code>暂存区</code></strong>,它记录了文件的名称,时间戳,长度等信息,但并不储存文件,文件内容依然位于 <code>.git/objects</code> 中。<code>.git/index</code> 中的索引建立了文件和对象库中对象实体之间的对应。</p>
<p><img src="https://www.worldhello.net/wpfiles/2010/11/git-stage.png" alt="index"></p>
<p>图中版本库中的 index 区域就是暂存区,可以看到 index 区域与 master 区域其实都是对 objects 中存储的文件内容的索引。与先前了解到的一致,游标形状的 <code>HEAD</code> 是一个指向当前所在分支的标识符。</p>
<p>图中还列出了 Git 命令是如何影响工作区与暂存区的。值得注意的是 <code>git reset HEAD</code> 命令。</p>
<p><code>git reset</code> 有两种使用方法:</p>
<ol>
<li><code>git reset [--soft | --mixed | --hard | --merge | --keep] [-q] [<commit>]</code></li>
<li><code>git reset [-q] [<commit>] [--] <paths></code></li>
</ol>
<p>在前面我们了解到第一种使用方法实际上是改变了引用文件。 git reset 的第二种使用方法并不会改变引用,它会用已经 commit 到版本库的文件替换掉暂存区中的文件。因此,<code>git reset HEAD <paths></code> 就是取消之前执行 <code>git commit <paths></code> 时所改变的暂存区。</p>
<h3 id="四、Git-的痛点"><a href="#四、Git-的痛点" class="headerlink" title="四、Git 的痛点"></a>四、Git 的痛点</h3><p>粗浅地了解了以上原理之后,对于 Git 的痛点也可以窥知一二。</p>
<p>Git 的诞生经历和 JavaScript 有些相似,都是短时间内打造的兵器,其设计思路一开始就是很粗糙的,甚至有些不合理反人类的地方。(但是 JavaScript 还有 ECMAScript 一年一年的修补,Git 却没啥指望改进了……)</p>
<p>最明显的一个痛点是,一个 Git 命令身兼数职的情况非常多(git rebase,git reset,git checkout 是重灾区),这也是造成新手入门时每天一脸懵逼的一个主要原因。</p>
<p>Git 本身的设计理念是非常清晰明确的,如果可以重新设计的话,希望指令可以与其设计思路统一,指令分为四类:</p>
<blockquote>
<ol>
<li>操作当前指针</li>
<li>操作分支</li>
<li>操作版本</li>
<li>操作工作环境</li>
</ol>
</blockquote>
<p>有一篇论文<a href="https://groups.csail.mit.edu/sdg/pubs/2016/gitless-oopsla16.pdf" target="_blank" rel="external">《Purposes, Concepts, Misfits, and a Redesign of Git》</a>专门分析 Git 的设计问题。最后设计了一款新工具叫 Gitless。</p>
<p>除了这一点之外,由于 Git 存储的是文件快照,如果项目需要频繁修改大文件的话很容易造成存储库的膨胀,这一点虽然有<a href="https://blog.colafornia.me/2018/03/09/slove-git-clone-speed/">解决方案</a>,但不可否认依然是其痛点之一。</p>
<h3 id="五、最后"><a href="#五、最后" class="headerlink" title="五、最后"></a>五、最后</h3><p>除了以上介绍到的内容,git rebase,git reflog,git checkout,git cherry-pick 也都是值得探究的命令。</p>
<h4 id="参考内容:"><a href="#参考内容:" class="headerlink" title="参考内容:"></a>参考内容:</h4><ul>
<li><a href="https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain" target="_blank" rel="external">Git Internals</a></li>
<li><a href="https://gotgit.readthedocs.io/en/latest/index.html" target="_blank" rel="external">Git 权威指南</a></li>
<li><a href="https://yanhaijing.com/git/2017/02/08/deep-git-3/" target="_blank" rel="external">起底Git-Git内部原理</a></li>
</ul>
<p><img src="/images/dive-into-git/cover.jpg" alt="cover"></p>
<p>初学编程时,Git 算是最令人心有余悸的 Boss 了,毕竟相比于写出 Bug 这种常见事情,把自己/别人代码弄丢这种事更为可怕。</p>
<p>本文只介绍 Git 原理中最为硬核的部分,如果想把所有原理都探究明白,内容多到可以写本书。因此,本文只试图解答:</p>
<blockquote>
<ol>
<li>Git 是怎么存储内容的?存到哪里了?</li>
<li>工作区,暂存区,版本库,各种教程里这仨名词一看就晕,能说人话吗?</li>
<li>git reset 来,git reset 去,感觉啥都能 reset,我到底在干啥?</li>
<li>Git 快在哪里?大神都说 Git 好用,怎么还这么难学?</li>
</ol>
</blockquote>
<p>Let’s go!</p>
Thrift 速记
https://blog.colafornia.me/post/2018/thrift-note/
2018-03-27T03:11:00.000Z
2019-10-24T02:22:04.206Z
<h3 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h3><p><code>Apache Thrift</code> 是一款 <code>RPC</code> (跨语言的服务)框架,传输数据采用二进制格式,相对 XML 和 JSON 体积更小,对于高并发、大数据量和多语言的环境更有优势。</p>
<p><img src="/images/thrift.png" alt=""></p>
<h3 id="RPC-是什么"><a href="#RPC-是什么" class="headerlink" title="RPC 是什么"></a>RPC 是什么</h3><p><code>Remote Procedure Call</code> 即远程过程调用。</p>
<blockquote>
<p>RPC 是一个 <code>计算机通信协议</code>。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。 —— 中文维基</p>
</blockquote>
<a id="more"></a>
<p>远程过程调用总是由客户端对服务器发出一个执行若干过程请求,并用客户端提供的参数。执行结果将返回给客户端。</p>
<p>标准化的 RPC 大部分采用接口描述语言(Interface Description Language,IDL),方便跨平台的远程过程调用。</p>
<p>相比于 <a href="http://www.ruanyifeng.com/blog/2011/09/restful" target="_blank" rel="external">RESTful</a>,RPC 的优势在于:</p>
<ul>
<li>采用二进制的传输格式,相比于 RESTful 采用的 JSON 格式体积更小速度更快</li>
<li>支持多种传输协议与传输格式</li>
<li>支持同步和异步通信</li>
</ul>
<h3 id="Thrift-文件概览"><a href="#Thrift-文件概览" class="headerlink" title="Thrift 文件概览"></a>Thrift 文件概览</h3><p>以 <code>.thrift</code> 为后缀的文件,是服务消费方(Consumer)与服务提供方(Provider)之间用来进行接口描述(IDL)的文件。</p>
<h4 id="1-代码示例:"><a href="#1-代码示例:" class="headerlink" title="1.代码示例:"></a>1.代码示例:</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// thrift 文件一般包含两部分的内容</span></div><div class="line"><span class="comment">// 1) 类型说明,类似于前后端所约定的请求对象有哪些字段值</span></div><div class="line"><span class="comment">// 2) 接口服务 Service 说明,包含一系列的方法,类似于前后端之间约定的请求方法</span></div><div class="line"><span class="comment">// IDL 中可以定义以下一些类型:基本数据类型,结构体,容器,异常、服务</span></div><div class="line"><span class="comment">// 以下 IDL 定义了一个叫 Sample 的服务,其有一个叫 getOrderById 的方法</span></div><div class="line"></div><div class="line">struct Order {</div><div class="line"> <span class="number">1</span>:i64 id;</div><div class="line"> <span class="number">2</span>:i32 status;</div><div class="line">}</div><div class="line">service Sample</div><div class="line">{</div><div class="line"> <span class="function">Order <span class="title">getOrderById</span><span class="params">(<span class="number">1</span>:i64 id)</span></span>;</div><div class="line">}</div></pre></td></tr></table></figure>
<h4 id="2-基本类型:"><a href="#2-基本类型:" class="headerlink" title="2.基本类型:"></a>2.基本类型:</h4><ul>
<li><p><code>bool</code>: 布尔类型</p>
</li>
<li><p><code>byte</code>: 有符号字节</p>
</li>
<li><p><code>i16/i32/i64</code>: 16/32/64位有符号整型</p>
</li>
<li><p><code>double</code>: 64位浮点数</p>
</li>
<li><p><code>string</code>: 未知编码或者二进制的字符串</p>
</li>
</ul>
<h4 id="3-容器:"><a href="#3-容器:" class="headerlink" title="3.容器:"></a>3.容器:</h4><ul>
<li><p><code>list<t1></code>: 排序数组,可以重复</p>
</li>
<li><p><code>set<t1></code>: 集合,每个元素唯一</p>
</li>
<li><p><code>map<t1, t2></code>: key/value 键值对(key 的类型是 t1且 key 唯一,value 类型是 t2)</p>
</li>
</ul>
<h4 id="4-通过-IDL-文件生成代码"><a href="#4-通过-IDL-文件生成代码" class="headerlink" title="4.通过 IDL 文件生成代码"></a>4.通过 IDL 文件生成代码</h4><p>通过 IDL 一般生成两种类型的文件,1)类型文件 2)接口文件</p>
<p>形如 <code>xxx_types.js</code> 即是将 IDL 文件中的类型说明输出为类型文件。</p>
<p>形如 <code>xxxService.js</code> 即是接口文件,服务消费方通过接口文件来创建和 Provider 的连接。</p>
<p>在通信的过程中,thrift 会对数据进行序列化后传递给另一方,在接收方则对数据进行反序列化后映射成对应的语言对象。于是,我们就可以不关心数据格式和类型转换,直接调用远程服务了。</p>
<h3 id="Node-Thrift-应用"><a href="#Node-Thrift-应用" class="headerlink" title="Node Thrift 应用"></a>Node Thrift 应用</h3><h4 id="1-安装"><a href="#1-安装" class="headerlink" title="1.安装"></a>1.安装</h4><p>除了<a href="http://thrift.apache.org/docs/install/os_x" target="_blank" rel="external">官网下载</a>,Mac 下比较方便的安装方式是使用 homebrew:</p>
<p><code>brew install thrift</code></p>
<h4 id="2-生成代码"><a href="#2-生成代码" class="headerlink" title="2.生成代码"></a>2.生成代码</h4><p>语法为 <code>thrift --gen <language> <Thrift filename></code></p>
<p>生成 Node.js 代码的话:</p>
<p><code>thrift --gen js:node Service.thrift</code></p>
<p>在实际项目中一般会封装多个 shell 脚本来做这件事,方便维护与使用。</p>
<h4 id="3-使用"><a href="#3-使用" class="headerlink" title="3.使用"></a>3.使用</h4><p>Apache 已经推出了 <a href="http://thrift.apache.org/tutorial/nodejs" target="_blank" rel="external">官方的 Node.js 库</a></p>
<p>Client:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// 此示例代码大致浏览即可</span></div><div class="line"><span class="keyword">var</span> thrift = <span class="built_in">require</span>(<span class="string">'thrift'</span>);</div><div class="line"><span class="keyword">var</span> Calculator = <span class="built_in">require</span>(<span class="string">'./gen-nodejs/Calculator'</span>);</div><div class="line"><span class="keyword">var</span> ttypes = <span class="built_in">require</span>(<span class="string">'./gen-nodejs/tutorial_types'</span>);</div><div class="line"><span class="keyword">const</span> assert = <span class="built_in">require</span>(<span class="string">'assert'</span>);</div><div class="line"></div><div class="line"><span class="keyword">var</span> transport = thrift.TBufferedTransport;</div><div class="line"><span class="keyword">var</span> protocol = thrift.TBinaryProtocol;</div><div class="line"></div><div class="line"><span class="keyword">var</span> connection = thrift.createConnection(<span class="string">"localhost"</span>, <span class="number">9090</span>, {</div><div class="line"> transport : transport,</div><div class="line"> protocol : protocol</div><div class="line">});</div><div class="line"></div><div class="line">connection.on(<span class="string">'error'</span>, <span class="function"><span class="keyword">function</span>(<span class="params">err</span>) </span>{</div><div class="line"> assert(<span class="literal">false</span>, err);</div><div class="line">});</div><div class="line"></div><div class="line"><span class="comment">// Create a Calculator client with the connection</span></div><div class="line"><span class="keyword">var</span> client = thrift.createClient(Calculator, connection);</div><div class="line"></div><div class="line"></div><div class="line">client.ping(<span class="function"><span class="keyword">function</span>(<span class="params">err, response</span>) </span>{</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">'ping()'</span>);</div><div class="line">});</div><div class="line"></div><div class="line"></div><div class="line">client.add(<span class="number">1</span>,<span class="number">1</span>, <span class="function"><span class="keyword">function</span>(<span class="params">err, response</span>) </span>{</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">"1+1="</span> + response);</div><div class="line">});</div></pre></td></tr></table></figure>
<p>这样,就可以调用其它语言编写的服务了。</p>
<p>公司内部有一个封装好了连接过程的 thrift 包,使用起来更简单些,把端口号,ip 等信息传进去即可使用。</p>
<p>更多语言和使用示例可参考 <a href="http://thrift-tutorial.readthedocs.io/en/latest/usage-example.html" target="_blank" rel="external">thrift-tutorial</a></p>
<h3 id="微服务概览"><a href="#微服务概览" class="headerlink" title="微服务概览"></a>微服务概览</h3><p>一直以来都是使用 HTTP 协议进行接口调用,只知道有一些“后端之间的服务调用”是使用 Thrift 接口,没有想过前端项目中也会直接调用。</p>
<p>之所以会用到 Thrift,是因为公司采用的是“面向服务的架构”,我们所开发的 Web 应用也是一个服务,其中还会依赖其它服务。</p>
<p><code>SOA(Service-Oriented Architecture,面向服务的架构)</code>是一种设计方法,其中包含多个服务,而服务之间通过配合最终会提供一系列功能。一个服务通常以独立的形式存在于操作系统进程中。服务之间通过网络调用,而非采用进程内调用的方式进行通信。</p>
<p>微服务架构是 SOA 的一种特定方法。</p>
<p><img src="/images/2018-3-microservices.png" alt="microservices"></p>
<p>更多内容参考<a href="https://book.douban.com/subject/26772677/" target="_blank" rel="external">《微服务设计》</a></p>
<h3 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h3><p><code>Apache Thrift</code> 是一款 <code>RPC</code> (跨语言的服务)框架,传输数据采用二进制格式,相对 XML 和 JSON 体积更小,对于高并发、大数据量和多语言的环境更有优势。</p>
<p><img src="/images/thrift.png" alt=""></p>
<h3 id="RPC-是什么"><a href="#RPC-是什么" class="headerlink" title="RPC 是什么"></a>RPC 是什么</h3><p><code>Remote Procedure Call</code> 即远程过程调用。</p>
<blockquote>
<p>RPC 是一个 <code>计算机通信协议</code>。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。 —— 中文维基</p>
</blockquote>