修复缺陷的正确姿势

如何修复一个缺陷 如果给我一个小时来修复一个缺陷,我会花50分钟来写测试,用剩下的10分钟来改代码 – 本来是一句模仿爱因斯塔的名言,结果发现爱因斯坦并没有说过…… 你确定这是个缺陷吗? 下午2点,你喝下了一杯拿铁,它可以保证你在接下来的几个小时内保持清醒。突然,一位QA同事急匆匆的走了过来,从他的表情你就看出来事情不妙。果然,他告诉你SIT环境有个重大缺陷,如果不及时修复,好几个测试流程都不能进行。没错,你在还没有完全搞清楚发生了什么值钱,就莫名其妙的突然变成为了系统中一个“blocker”。 这位QA的神情和他对事情的描述让你不自觉的受到了一些感染,你也开始有点焦虑。你想要快速切换到IDE下,赶紧复现并修复他描述的问题。毕竟,你可不想成为一个阻塞别人工作的人。 我对你急切的心情表示理解,不过在你实际动手改代码之前,先冷静一下,稍微抑制一下赶紧修复问题的那种冲动。我们来看看在面对如此场景,如何表现的更为专业,以及更加卓有成效。 开始之前 事实上,在开始修复任何一个缺陷之前,你需要确认它确实是一个缺陷。这一点经常为很多新手忽略,从而导致修复缺陷从艺术变成了救火工作。 作为一名靠谱的开发,在真正动手修复之前,你可以做这样一些预先的check: 缺陷是不是发生在不受支持的浏览器上? 部署之后,有没有清理浏览器缓存? 下游系统是不是有计划内更新? 确定部署了最新版本吗?(部署之后,有没有机制可以确保SIT是最新版本) 或者你可以当QA宣称他找到了一个缺陷时,你可以反问:“你有没有试着重启浏览器/系统?”。很多情况下,在做了这些检查之后,你发现问题自己就解决了 – 此谓不战而屈人之兵也。 如果事情仅仅到这一步就结束的话,你就可以接着看看medium上的文章或者刷刷知乎什么的。不过有时候,事情就不会这么顺利了。 遗漏掉的需求 不论前期分析的多么完善,在实际项目的行进中,还是会遇到一些遗漏掉的需求点。比如招聘系统中的对留学生通道的考虑,银行系统中的海外信用卡的货币汇率转换等等。有时候,当听完QA的描述后,如果你和团队里的其他人都表示这个功能点是第一次听说,那么不用慌张,这很可能是一个被遗漏掉的需求。 这时候只需要冷静下来,将其记录下来,然后作为正常的Story流程进行即可(排列优先级,kick-off,被拖入in-doing等等) 还真是个缺陷 如果它竟然还不是一个漏掉的需求,承认自己写的代码有缺陷也不是什么丢人的事儿。而且即使是缺陷,也并不意味着需要立即修复。和所有的其他需求那样,缺陷也应该被分级,并当成一个正常的Story卡流入Backlog。 在实践中,我发现这一点非常关键。很多团队在开发过程进入修复缺陷阶段之后变得各种混乱,其源头也正是来源于此。一个非常糟糕的实践是:某个人负责将测试团队中发现的缺陷分发给指定的人,并一天两次的常规Check是否有所进展。这个貌似高频率反馈的过程可以毁掉团队的敏捷氛围:工作从拉动的方式变成了指派。 正确的做法是:为缺陷建立卡片,并和其他需求卡一起排列优先级,并通过拉动的方式流入开发流程,并像任何一张卡片那样进行kick-off,in-dev,sign-off等。换言之,不要特殊对待缺陷,把它当成普通的需求变更即可。 如何重现 一旦你确定了一个缺陷,并且需要修复它,那么第一件要做的事情自然是重现它。很多时候,重现并不那么容易。在实际项目中,应用可能有各种各样的外部依赖,比如: 依赖网络请求来获取数据 依赖特定的浏览器(可能是旧版本,也可能是特定浏览器) 依赖后端服务正常工作 所有的后端服务的异常都可能导致前端页面上的非预期行为:一个空白页面或者一个有着大红色叉的对话框,上书“找不着对象”等等。这些错误有时候可能会很难复现,或者至少需要一些特别的设置才可以使之发生。 网络异常 网络异常非常常见,而且可以导致各种各样的异常行为。开发中,localhost或者办公室的千兆宽带往往很难看到一些仅仅会在4G或者更慢速网络中会出现的问题。而当多个页面请求中的某一个失败时才会出现的缺陷则更难以复现。 不过,Chrome提供的DevTools可以在很大程度上帮助你(不过你可能需要每隔一个月重新学习下这些Tab的布局和新功能,Chrome的开发团队非常乐于偷偷给DevTools上线一些新功能,并完全破坏掉之前的布局)。 假设你需要调试/复现一个关于loading控件的问题,这个缺陷仅仅在网络极差的情况下才可以复现。你可以使用Chrome自带的throttling来模拟这种极端的网络场景。而有时候,你需要模拟网络完全不可用的状况,可以勾选Offline. 除了这些常规操作之外,你可以以定义黑名单的方式来屏蔽掉一些指定的URL: 这样,发往黑名单中的URL被屏蔽,这样可以很方便的模拟出改服务不可用的场景。有时候,如果你在开发海外的站点,也可以通过屏蔽特定的URL,比如facebook或者twitter链接来查看该站点在国内的表现。 消除重现缺陷的其他blocker 现实中,大部分实际应用都有多个页面。如果缺陷在第N个页面上,而你每次都需要从第一步导航到第N步。而如果每一步都有一些必填字段要填写,那么要重现这个缺陷,特别是debug的时候就会变成一场噩梦:你修改了一行代码,live-reload自动刷新并将你重定向到第一页,然后你完成所有填写并到达要调试的组件,然后发现本应该是字符串的地方显示的是[Object Object]。 不过,如果你恰好使用React+Redux组合的话,Redux Devtools Extension可以节省你很多时间。通过使用这个插件,你可以将应用的状态导入/导出。 Setup非常容易,只需要在创建redux store的时候加上一行代码即可: const store = createStore( reducer, /* preloadedState, */ window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ); 这样你可以使用该插件将应用在某一时刻的状态导出到本地文件。在导出的文件中,redux-dev-tools会将应用的初始状态和所有发生过的事件(以及事件相关的数据)记录下来,以便重新播放。 当然了,如果你并没有使用React+Redux的组合,很多表单的auto-filler也是可以完成类似动作的。你可能需要花费几分钟来学习如何定义selector以及如何用快捷键来自动填写,不过一旦学会,它就可以节省你很多时间。 寻找根因 根据我的经验,很多缺陷都并非发生在逻辑错误上(比如倒置的if-else,没有跳出的while等)。相反,很多时候错误会发生在错误的mapping,空值的保护不足等场景。 非法数据 实际应用中,每个集成点都潜在的会有这样的问题。比如UI到BFF,BFF到下游的系统等。代码中的很多配置,或者不一致的惯例也会导致类似的问题。...

June 18, 2019 2 min

前端页面性能调优

Web页面的性能 我们每天都会浏览很多的Web页面,使用很多基于Web的应用。这些站点看起来既不一样,用途也都各有不同,有在线视频,Social Media,新闻,邮件客户端,在线存储,甚至图形编辑,地理信息系统等等。虽然有着各种各样的不同,但是相同的是,他们背后的工作原理都是一样的: 用户输入网址 浏览器加载HTML/CSS/JS,图片资源等 浏览器将结果绘制成图形 用户通过鼠标,键盘等与页面交互 这些种类繁多的页面,在用户体验方面也有很大差异:有的响应很快,用户很容易就可以完成自己想要做的事情;有的则慢慢吞吞,让焦躁的用户在受挫之后拂袖而去。毫无疑问,性能是影响用户体验的一个非常重要的因素,而影响性能的因素非常非常多,从用户输入网址,到用户最终看到结果,需要有很多的参与方共同努力。这些参与方中任何一个环节的性能都会影响到用户体验。 宽带网速 DNS服务器的响应速度 服务器的处理能力 数据库性能 路由转发 浏览器处理能力 早在2006年,雅虎就发布了提升站点性能的指南,Google也发布了类似的指南。而且有很多工具可以和浏览器一起工作,对你的Web页面的加载速度进行评估:分析页面中资源的数量,传输是否采用了压缩,JS、CSS是否进行了精简,有没有合理的使用缓存等等。 如果你需要将这个过程与CI集成在一起,来对应用的性能进行监控,我去年写过一篇相关的博客。 本文打算从另一个角度来尝试加速页面的渲染:浏览器是如何工作的,要将一个页面渲染成用户可以看到的图形,浏览器都需要做什么,哪些过程比较耗时,以及如何避免这些过程(或者至少以更高效的方式)。 页面是如何被渲染的 说到性能优化,规则一就是: If you can’t measure it, you can’t improve it. - Peter Drucker 根据浏览器的工作原理,我们可以分别对各个阶段进行度量。 图片来源:http://dietjs.com/tutorials/host#backend 像素渲染流水线 下载HTML文档 解析HTML文档,生成DOM 下载文档中引用的CSS、JS 解析CSS样式表,生成CSSOM 将JS代码交给JS引擎执行 合并DOM和CSSOM,生成Render Tree 根据Render Tree进行布局layout(为每个元素计算尺寸和位置信息) 绘制(Paint)每个层中的元素(绘制每个瓦片,瓦片这个词与GIS中的瓦片含义相同) 执行图层合并(Composite Layers) 使用Chrome的DevTools - Timing,可以很容易的获取一个页面的渲染情况,比如在Event Log页签上,我们可以看到每个阶段的耗时细节(清晰起见,我没有显示Loading和Scripting的耗时): 上图中的Activity中,Recalculate Style就是上面的构建CSSOM的过程,其余Activity都分别于上述的过程匹配。 应该注意的是,浏览器可能会将Render Tree分成好几个层来分别绘制,最后再合并起来形成最终的结果,这个过程一般发生在GPU中。 Devtools中有一个选项:Rendering - Layers Borders,打开这个选项之后,你可以看到每个层,每个瓦片的边界。浏览器可能会启动多个线程来绘制不同的层/瓦片。 Chrome还提供一个Paint Profiler的高级功能,在Event Log中选择一个Paint,然后点击右侧的Paint Profiler就可以看到其中绘制的全过程:...

February 8, 2017 2 min

Reflux 101

一点背景 React在设计之初就只关注在View本身上,其余部分如数据的获取,事件处理等,全然不在考虑之内。不过构建大型的Web前端应用,这些点又实在不可避免。所以Facebook的工程师提出了前端的Flux架构,这个架构的最大特点是单向数据流(后面详述)。但是Flux本身的实现有很多不合理的地方,比如单例的Dispatcher会在系统中有多种事件时导致臃肿的switch-cases等。 这里是Facebook官方提供的提供Flux的结构图。 其实整个Flux背后的思想也不是什么新东西。在很久之前,Win32的消息机制(以及很多的GUI系统)就在使用这个模型,而且这也是一种被证实可以用来构建大型软件的模型。 鉴于Flux本身只是一个架构,而且Facebook提供的参考实现又有一些问题,所以社区有了很多版本的Flux实现。比如我们这里会用到的Reflux。 Reflux简介 简而言之,Reflux里有两个组件:Store和Action。Store负责和数据相关的内容:从服务器上获取数据,并更新与其绑定的React组件(view controller);Action是一个事件的集合。Action和Store通过convention来连接起来。 具体来说,一个典型的过程是: 用户的动作(或者定时器)在组件上触发一个Action Reflux会调用对应的Store上的callback(自动触发) 这个callback在执行结束之后,会显式的触发(trigger)一个数据 对应的组件(可能是多个)的state会被更新 React组件检测到state的变化后,会自动重绘自身 一个例子 我们这里将使用React/Reflux开发一个实际的例子,从最简单的功能开始,逐步将其构建为一个较为复杂的应用。 这个应用是一个书签展示应用(数据来源于我的Google Bookmarks)。第一个版本的界面是这样的: 要构建这样一个列表应用,我们需要这样几个部分: 一个用来fetch数据,存储数据的store (BookmarkStore) 一个用来表达事件的Action(BookmarkActions) 一个列表组件(BookmarkList) 一个组件条目组件(Bookmark) 定义Actions var Reflux = require('reflux'); var BookmarkActions = Reflux.createActions([ 'fetch' ]); module.exports = BookmarkActions; 第一个版本,我们只需要定义一个fetch事件即可。然后在Store中编写这个Action的回调: 定义Store var $ = require('jquery'); var Reflux = require('reflux'); var BookmarkActions = require('../actions/bookmark-actions'); var Utils = require('../utils/fetch-client'); var BookmarkStore = Reflux.createStore({ listenables: [BookmarkActions], init: function() { this.onFetch(); }, onFetch: function() { var self = this; Utils....

November 9, 2015 3 min

前后端分离了,然后呢

前言 前后端分离已经是业界所共识的一种开发/部署模式了。所谓的前后端分离,并不是传统行业中的按部门划分,一部分人纯做前端(HTML/CSS/JavaScript/Flex),另一部分人纯做后端,因为这种方式是不工作的:比如很多团队采取了后端的模板技术(JSP, FreeMarker, ERB等等),前端的开发和调试需要一个后台Web容器的支持,从而无法做到真正的分离(更不用提在部署的时候,由于动态内容和静态内容混在一起,当设计动态静态分流的时候,处理起来非常麻烦)。关于前后端开发的另一个讨论可以参考这里。 即使通过API来解耦前端和后端开发过程,前后端通过RESTFul的接口来通信,前端的静态内容和后端的动态计算分别开发,分别部署,集成仍然是一个绕不开的问题 — 前端/后端的应用都可以独立的运行,但是集成起来却不工作。我们需要花费大量的精力来调试,直到上线前仍然没有人有信心所有的接口都是工作的。 一点背景 一个典型的Web应用的布局看起来是这样的: 前后端都各自有自己的开发流程,构建工具,测试集合等等。前后端仅仅通过接口来编程,这个接口可能是JSON格式的RESTFul的接口,也可能是XML的,重点是后台只负责数据的提供和计算,而完全不处理展现。而前端则负责拿到数据,组织数据并展现的工作。这样结构清晰,关注点分离,前后端会变得相对独立并松耦合。 上述的场景还是比较理想,我们事实上在实际环境中会有非常复杂的场景,比如异构的网络,异构的操作系统等等: 在实际的场景中,后端可能还会更复杂,比如用C语言做数据采集,然后通过Java整合到一个数据仓库,然后该数据仓库又有一层Web Service,最后若干个这样的Web Service又被一个Ruby的聚合Service整合在一起返回给前端。在这样一个复杂的系统中,后台任意端点的失败都可能阻塞前端的开发流程,因此我们会采用mock的方式来解决这个问题: 这个mock服务器可以启动一个简单的HTTP服务器,然后将一些静态的内容serve出来,以供前端代码使用。这样的好处很多: 前后端开发相对独立 后端的进度不会影响前端开发 启动速度更快 前后端都可以使用自己熟悉的技术栈(让前端的学maven,让后端的用gulp都会很不顺手) 但是当集成依然是一个令人头疼的难题。我们往往在集成的时候才发现,本来协商的数据结构变了:deliveryAddress字段本来是一个字符串,现在变成数组了(业务发生了变更,系统现在可以支持多个快递地址);price字段变成字符串,协商的时候是number;用户邮箱地址多了一个层级等等。这些变动在所难免,而且时有发生,这会花费大量的调试时间和集成时间,更别提修改之后的回归测试了。 所以仅仅使用一个静态服务器,然后提供mock数据是远远不够的。我们需要的mock应该还能做到: 前端依赖指定格式的mock数据来进行UI开发 前端的开发和测试都基于这些mock数据 后端产生指定格式的mock数据 后端需要测试来确保生成的mock数据正是前端需要的 简而言之,我们需要商定一些契约,并将这些契约作为可以被测试的中间格式。然后前后端都需要有测试来使用这些契约。一旦契约发生变化,则另一方的测试会失败,这样就会驱动双方协商,并降低集成时的浪费。 一个实际的场景是:前端发现已有的某个契约中,缺少了一个address的字段,于是就在契约中添加了该字段。然后在UI上将这个字段正确的展现了(当然还设置了字体,字号,颜色等等)。但是后台生成该契约的服务并没有感知到这一变化,当运行生成契约部分测试(后台)时,测试会失败了 — 因为它并没有生成这个字段。于是后端工程师就找前端来商量,了解业务逻辑之后,他会修改代码,并保证测试通过。这样,当集成的时候,就不会出现UI上少了一个字段,但是谁也不知道是前端问题,后端问题,还是数据库问题等。 而且实际的项目中,往往都是多个页面,多个API,多个版本,多个团队同时进行开发,这样的契约会降低非常多的调试时间,使得集成相对平滑。 在实践中,契约可以定义为一个JSON文件,或者一个XML的payload。只需要保证前后端共享同一个契约集合来做测试,那么集成工作就会从中受益。一个最简单的形式是:提供一些静态的mock文件,而前端所有发往后台的请求都被某种机制拦截,并转换成对该静态资源的请求。 moco,基于Java wiremock,基于Java sinatra,基于Ruby 看到sinatra被列在这里,可能熟悉Ruby的人会反对:它可是一个后端全功能的的程序库啊。之所以列它在这里,是因为sinatra提供了一套简洁优美的DSL,这个DSL非常契合Web语言,我找不到更漂亮的方式来使得这个mock server更加易读,所以就采用了它。 一个例子 我们以这个应用为示例,来说明如何在前后端分离之后,保证代码的质量,并降低集成的成本。这个应用场景很简单:所有人都可以看到一个条目列表,每个登陆用户都可以选择自己喜欢的条目,并为之加星。加星之后的条目会保存到用户自己的个人中心中。用户界面看起来是这样的: 不过为了专注在我们的中心上,我去掉了诸如登陆,个人中心之类的页面,假设你是一个已登录用户,然后我们来看看如何编写测试。 前端开发 根据通常的做法,前后端分离之后,我们很容易mock一些数据来自己测试: [ { "id": 1, "url": "http://abruzzi.github.com/2015/03/list-comprehension-in-python/", "title": "Python中的 list comprehension 以及 generator", "publicDate": "2015年3月20日" }, { "id": 2, "url": "http://abruzzi.github.com/2015/03/build-monitor-script-based-on-inotify/", "title": "使用inotify/fswatch构建自动监控脚本", "publicDate": "2015年2月1日" }, { "id": 3, "url": "http://abruzzi....

June 22, 2015 2 min

我们真的缺前端工程师吗

前言 这两天在好几个地方都看到了一篇关于为什么整个互联网行业都缺前端工程师?的文章,文章本身是去年的,中心思想是:其实我们并不缺前端工程师,我们缺的是优秀的前端工程师。我还是比较同意作者观点的,不过略有意犹未尽的感觉。于是我结合自己的经验,也来聊一下这个话题:我们真的缺前端工程师吗? These walls are kind of funny like that. First you hate them, then you get used to them.Enough time passed, get so you depend on them. That’s institutionalising. 传统软件公司划分开发者的方式下,在前端部门的程序员永远不会去读缓存数据部分的代码,设计师也不太可能去和开发坐在一起,开发也不知道软件最终软件会以何种方式部署在服务器上。 什么是“前端”工程师 我在招聘广告和办公室的一些对话中,听到了一个新的角色:UI Dev,事实上我在知乎上还回答过一个关于ThoughtWorks的UI Dev的问题。简而言之,UI Dev可以快速的把设计师的作品实现为HTML/CSS/JavaScript代码。 如果按照这个标准,我觉得UI Dev对自己的要求太低了。毕竟要学会HTML/CSS实现mockup并不困难,但是成为一名前端工程师则需要掌握更多的知识: 会用PS来进行图片的处理(比如切图,微调等) 用HTML/CSS实现mockup(可能还有SASS/LESS等工具) 熟悉JavaScript(比如前端的MVVM框架,客户端模板) 前端开发的工作流程(代码检查,精简化,模块化CSS,LiveReload,调试) 编写测试(静态检查,单元测试) 跨浏览器、跨设备的解决方法(不同分辨率,不同厂商) 会根据项目的特点选择不同的前端技术栈(移动端,Web站点,响应式设计等) 在有了基础的HTML/CSS/JS技能之后,你会尝试做的更好: 如何更高效的操作DOM 如何将CSS写的更加清晰易懂 如何编写更加易于维护的代码(更有意义的单元测试) 如何组织大型的项目结构,模块化,组件化等等 这些要求事实上已经不那么容易做到了。它可能会花费你2到3年时间来完全掌握。但是2到3年之后,即便你已经成为了一个“合格的”前端工程师,这也还远远不够。在现实世界中,一个软件产品除了前端,还有非常广阔的空间,还有很多有趣的东西值得学习: HTTP协议本身(缓存,鉴权) Web容器/HTTP服务器如何工作 无状态的Web应用的工作原理(如何让网站正确地运行在集群上) 动态,静态内容如何分离部署(反向代理配置) 安全机制如何配置 监控机制如何配置 有了这些,也算是有点端到端的意思了。这时你也已经不是一个“纯前端”工程师了,系统中的大部分问题你都可以搞定,不过日常工作中可能更多的职责还是做前端的开发。但是这些还不够,软件除了交付之外,还有一些非功能性的需求: 端到端测试(UI测试,比如selenium server/web driver) devops(比如数据库环境,测试服务器,CI服务器的自动化provision) 基本的UI设计原则(在某些页面确实的情况下,根据系统的已有UI做设计) 数据库性能优化 性能测试 不过这些还只是我对于Web开发这个领域的总结。其他领域,比如大数据,机器学习,GIS,图像/视频处理等等。...

June 14, 2015 1 min