大事记 把女儿心心哄睡着之后,我躺在她旁边,听着她平稳的呼吸声,心里充满了幸福和感激。女儿降生的那种感动,是无以伦比的,之前的很多事情变得无所谓起来,这当然是我今年最大的收获。
当然,初为人父,有好多好多东西需要学习,她还在月子里的时候,我整理过一次思维导图:
今年也有一些其他的里程碑:
骑行了第一个100公里 学会了驾驶(已经行驶了近6000公里) 学会了抱娃,换尿布,给娃洗澡,试水温等等 《轻量级Web应用开发》被翻译成繁体中文版在台湾发售 Hiking到了秦岭最高点(大爷海,海拔3500m) 学习 今年的大部分时间都不在办公室,在客户现场出差有8-9个月。最大的感受是归属感的缺乏,另外就是大部分时候再输出,会导致积累的存货快要被掏空的感觉。很多知识需要花时间来补充上,咨询工作本身有很大一部分是需要咨询师在非咨询项目上做积累,然后再去输出的。
这一点希望在2017年可以做的更好一些:减少一些出差,多在办公室的项目上工作,然后能有更多的时间来积累自己的知识库。
上半年花了一些时间和设计师唐婉莹合写的《在迭代1之前》,后来出差网络比较封闭的时候,就只有唐婉莹在做更新(频率和质量都很高),我自己的开发部分很久没有改动了,算是一个比较遗憾的地方。2017年希望可以完成其中的烂尾部分。
总体来说,2016年很多时间是做咨询工作,内容比较杂乱。我梳理了一下,大致如下:
一些微小的练习 我的第一个React Natvie应用,感谢傅若愚:
闪电计划的页面局部,感谢张小虫:
Graphviz一个例子:
闪电计划 7月,和另外两个同事组织了一个系列的活动,这个活动旨在通过一系列的刻意练习,包括但不限于:
TDD,Tasking,构建,环境自动化 自动化测试(集成测试,UI测试) 项目中的常见场景(多表关联,异常处理,RESTful API设计) 常见静态页面 一些具体而微的端到端Web项目 闪电计划的另一个副作用是找出了一些前端的Kata,可以供后续的前端们做练习用。我已经找张小虫,李彤欣做过了第一次的实验,证明是一个比较合理的方向。在2017年,可以作为新的Workshop进行。
项目经历 元月份经历了一个史上最奇特的咨询项目:Staffing好的3个人中,有两个离职了……。不过还好,通过两个月的微小工作,客户最终还是比较满意的,也很顺利的有了项目的第二期(虽然由于其他原因,第二期只做了一半)。
其他的经历比较平淡:
在回南天的3月去了一趟广州出差,做了一些前端性能相关的测试。 在最热的7月份去了一趟上海出差,莫名其妙的做了一些前端性能相关的测试,以及React Native的一些预研。 最后在寒冷的12月去了一趟深圳出差,做了一些安全测试(基于OWASP)、依赖关系图(基于Doxygen)等的Spike。 不过总体来看,2016年经历的项目是最多的,而且多样性也很高:
3个咨询项目 3个交付项目 3个售前 其他 应王欢要求,除了一起吃过几次饭之外,今年跟王欢基本上没有交集。倒是多亏了他的脸,在楼下的来福士和他喝了不少次咖啡。
这是九月份去付莹家的黄河时和同事们的合影,第一次开车走高速去那么远的地方,后座还有一个澳洲的客户。
骑行了一半儿,有一群开着车的同事追了上来,跟我们一起吃了个饭,我们骑车回去,他们则开车/坐车回去了。
QA的职责 我们先讨论一下传统的瀑布模型下QA是如何工作的,其中最主要的问题是什么;然后作为对比,我们来看看在敏捷团队里QA又是如何工作的,工作重点又是什么;最后,我们详细看一看在新的职责下,QA应该如何做。
瀑布开发模型 即使在今天,在很多企业中,瀑布模型仍然是主流。每一个需求都需要经过分析,设计,开发,测试,上线部署,运维等阶段。虽然一些企业已经在实施敏捷开发,比如项目/产品以迭代的方式运作,也有诸如每日站会,代码检视等敏捷实践,但是如果仔细审视,你会发现其实开发模式骨子里还是瀑布:按照软件组件划分的部门结构(详见康威定律),按照职能划分的团队(开发和测试分属不同部门),过长的反馈周期,永远无法摆脱的集成难题等等。
随着软件变得越来越复杂,团队里没有任何一个人可以说出系统是如何运作的,也不知道最终用户是谁,以及最终用户会以何种方式来使用最终的软件。
更糟糕的是,按照职能划分的团队在物理上都是隔离的,比如独立的测试部门,独立的运维部门,整日忙碌而难以预约到档期的业务人员,当然还有经常疲于交付,无处吐槽的苦逼开发。由于这些隔离,信息的反馈周期会非常长,一个本来很容易修复的缺陷可能在4周之后才可能被另一个部门的测试发现,然后通过复杂的工作流(比如某种形式的缺陷追踪系统)流到开发那里,而开发可能还在拼命的完成早就应该交付的功能,从而形成恶性循环。
瀑布模式中的QA 在这样的环境中,QA们能做的事情非常有限。在需求开始时会他们参加需求澄清的会议,制定一些测试计划,然后进行测试用例的设计。有的企业会用诸如Excel之类的工具来记录这些用例。这些写在Excel里的,死的用例用处非常有限。而最大的问题在于:它们无法自动化执行。另外,在实际软件开发中,需求总是会经常发生变化,需求的优先级也会有调整,然后这些记录在Excel中的死的用例会很快过期,变得无人问津。
除此之外,QA中的有些成员会使用工具来录制一些UI测试的场景,然后在每个新版本出来之后进行回放即可。然而,当UI发生一点变化之后,这些自动化的用例就会失效:比如HTML片段中元素位置的调整,JavaScript的异步调用超时等等。
显然,这种单纯以黑盒的形式来检查功能点的测试方式是不工作的,要真正有效的提升软件质量,仅仅通过事后检查是远远不够的,软件的质量也应该内建于软件之中。QA的工作也应该是一个贯穿软件生命周期的活动,从商业想法,到真实上线,这其中的所有环节,都应该有QA的参与。
系统思考 如果不从一个系统的角度来思考软件质量,就无法真正构建出健壮的、让业务和团队都有信心的软件系统。质量从来都不只是QA的职责,而是整个团队的职责。
关于软件质量,一个根深蒂固的误解是:缺陷在开发过程中被引入,然后在测试阶段被发现,最后在QA和开发的来来回回的撕扯中被解决(或者数量被大规模降低),最后在生产环境中,就只会有很少的,优先级很低的缺陷。
然而事实上,很多需求就没有仔细分析,业务价值不很确定,验收条件模糊,流入开发后又会引入一些代码级别的错误,以及业务规则上的缺陷,测试阶段会漏掉一些功能点,上线之后更是问题百出(网络故障,缓存失效,黑客攻击,操作系统补丁,甚至内存溢出,log文件将磁盘写满等等)。
在一个敏捷团队中,每个个人都应该对质量负责,而QA则以自己的丰富经验和独特视角来发掘系统中可能的质量隐患,并帮助团队将这些隐患消除。
我在ThoughtWorks的同事Anand Bagmar在他的演讲What is Agile testing- How does automation help?中详细讨论过这部分内容。
QA到底应该干什么? 本质上来说,任何软件项目的目标都应该是:更快地将高质量的软件从想法变成产品。
将这个大目标细分一下,会得到这样几个子项,即企业需要:
更多的商业回报(发掘业务价值) 更快的上线时间(做最简单,直接的版本) 更好的软件质量(质量内嵌) 更少的资源投入(减少浪费) 其实就是传说中的多、快、好、省。如果说这是每一个软件项目的目标的话,那么团队里的每一个个人都应该向着这个目标而努力,任何其他形式的工作都可以归类为浪费。用Excel记录那些经常会失效,而且无法自动执行的测试用例是浪费,会因为页面布局变化而大面积失效的UI测试也是浪费,一个容易修复的缺陷要等到数周之后才被发现也是浪费。
在这个大前提下,我们再来思考QA在团队里应该做什么以及怎么做。
QA的职责 Lisa Crispin在《敏捷软件测试》中提到过一个很著名的模型:敏捷测试四象限。这个模型是QA制定测试策略时的一个重要参考:
如果按照纵向划分的话,图中的活动,越向上越面向业务;越向下越面向技术。横向划分的话,往左是支撑团队;往右是评价产品。
其实简化一下,QA在团队里的工作,可以分为两大类:
确保我们在正确的交付产品 确保我们交付了正确的产品 根据这个四象限的划分,大部分团队可能都会从Q2起步:QA会和BA,甚至UX一起,从需求分析入手,进行需求分析,业务场景梳理,这时候没有具体的可以被测试的软件代码。不过这并不妨碍测试活动,比如一些纸上原型的设计(感谢刘海生供图):
通过这一阶段之后,我们已经有了用户故事,这时候QA需要和开发一起编写用户故事的自动化验收测试。当开发交付一部分功能之后,QA就可以做常规的用户故事测试,几个迭代之后,QA开始进行跨功能需求测试和探索性测试等。根据探索性测试的结果,QA可能会调整测试策略,调整测试优先级,完善测试用例等等。
根据项目的不同,团队可以从不同的象限开始测试策略的制定。事实上,Q1-Q4仅仅是一个编号,与时间、阶段并无关系,Lisa Crispin还专门撰文解释过。
关于QA如何在软件分析的上游就介入,然后通过BDD的方式与业务分析师一起产出软件的各种规格描述,并通过实例的方式来帮助整个团队对需求的理解,ThoughtWorks的林冰玉有一篇文章很好的介绍了BDD的正确做法。如果将QA的外延扩展到在线的生产环境,制定合理的测量指标,调整测试策略,强烈推荐林冰玉写的另一篇文章产品环境中的QA。
其他职责 事实上,软件生命周期中有很多的活动,有很多处于灰色地段。既可以说是应该开发做,又可以说应该QA做,甚至可以推给其他角色(比如OPs)。不过我们知道,一旦涉及角色,人们就再也不会按照全局优化的思路来应对问题了。这种灰色的活动包括:
持续集成的搭建 测试环境的创建于维护 UAT上的数据准备 代码中的测试代码的维护 测试代码的重构 在团队实践中,这些活动我们通常会让QA和开发或者OPs同事一起结对来完成。一方面避免知识孤岛的形成,另一方面在跨角色的工作中,也可以激发出更多不同的思路。
万能的QA? 虽然在这些活动中,QA都会参与,但是并不是说团队里只要有一个QA就可以了。QA在参与这些活动时,侧重点还是有很大不同的。
比如需求分析阶段,如果有QA的加入,一些从QA角度可以发现的有明显缺陷的场景,则可以在分析阶段就得到很好的处理。另一方面,尽早介入可以设计出更合理的测试计划(比如哪些功能的优先级比较高,用户更会频繁使用,那么对应的测试比重也会更高)。在Story分析与书写阶段,QA可以帮助写出更加合理的验收条件,既满足业务需求,又可以很好的指导开发。
在和开发一起编写澄清需求时,主要是编写自动化验收测试,而不是实际编写业务逻辑的实现(虽然QA应该参与Code Reivew环节,学习并分享自己的观点);甚至在上线运维阶段,QA还需要和OPs一起来设计用户数据的采集指标(比如用户访问的关键路径,浏览器版本,地区的区分等),从而制定出新的测试策略。
扩展阅读 What is Agile testing - How does automation help?...
培训元模式 最近在帮客户设计一个微服务进阶版培训的材料,整理的过程中我意识到这类事情我已经做过好多次了。比如在ThoughtWorks的P2能力建设项目,3周3页面工作坊等等,我觉得应该将设计课程/设计培训中的模式、原则和实践都提取一下,形成一个元模式(即关于培训的培训)。
一个培训中的活动,按照时间顺序可以分为三个步骤:
设计培训内容 培训前期准备 培训中的一些实践 设计培训内容 根据经验,只有那些正好处于瓶颈阶段,需要别人给予一些具体指导的人,培训是最有效的。比如我最近在学习iOS开发,那么用Swift开发一个Todo List应用就特别合适,而另一个基于Objective-C的自动化测试则对我来说一点用也没有(即使这个可能更高级,讲师更牛)。
在任何一个培训可能的设计之前,首先需要回答几个问题:
培训目的是什么? 参与者对主题的了解程度如何? 参与者的组成比例(比如junior占比,senior占比等) 结果如何检验? 一个例子 例子是人类的好朋友,这里我们来看一个例子:
客户要为负责开发的同事做一次培训,培训的目的是要帮助他们建立微服务架构下的常见实践的知识框架。从结果的长期效果来看,这次培训要能指导实际的开发工作。参与者对微服务的一些概念有初步了解,也做过小的练习,但是诸如如何划分服务边界,如何拆分微服务等,都不了解,也比较迫切的想要了解。
根据这个上下文,客户希望在培训中可以传递这样一些内容:
如何拆分微服务 常见的微服务设计原则 拆分微服务的时机 如何做微服务的测试 为了确保信息传递的准确性,我问了一遍上边都提到的列表,并且得到了答案:
听众是开发经验相对丰富的开发(3-5年) 学习拆分为了将大的应用拆分,以方便维护 自动化测试能力和意识都较为薄弱 听众自己的期望是可以有一些切实可以指导实际开发的收获 在有了这些输入的情况下,我做出了这样一些调整:
不专门讲拆分微服务 主要精力讲DDD(Domain Driven Design) 设计迭代式的,逐步变得复杂的场景来练习DDD 讲解和练习自动化测试(Consumer Driven Contract) Session+Workshop+讨论的形式 看了这个计划之后,客户开始觉得挺困惑,说这个怎么跟我们梳理的课程诉求不一致?对于这个疑惑,我的建议是这样的:
之前的例子不能落地(找不到足够复杂的,又适合在培训中拆分的场景) 微服务的核心不是基础设施,而是设计原则,或者说如何在开发中找出边界 有了DDD的指导,划分本身并非难事儿 自动化测试(集成测试和契约测试)的能力和认识必须建立 然后我把整理好的课件,实例分解,课程安排给客户讲了一遍之后,他觉得很满意。客户自己也是懂技术的,在分析了现状之后,后来又专门要求给部门内做一些DDD培训(而不是微服务本身)。
培训方式 同样,方式上也需要一些问题的解答才能有效进行:
培训总时长 更偏重练习还是偏重讲解(工作坊还是Session,以及各自的占比) 参与者如何投入(比如是工作时间,还是晚上等) 根据经验来看,不论是TWU还是对客户的培训,工作坊和Session结合的方式效果最好。 Workshop至少需要包含这样几部分:
明确要做的练习(多长时间,达到什么目的等) Showcase(参与感,如果有多轮的话,要保证每个人都有Show的机会,而不是每次同一个人) 讨论环节(点评,这个时机可以做一些小结,将要传递的信息润物细无声的传递出去) 应该避免的做法是:...
高效幻象 通过对自己的行为观察,我发现在很多时候,我以为我掌握了的知识和技能其实并不牢靠。我引以为豪的高效其实犹如一个彩色的肥皂泡,轻轻一碰就会破碎,散落一地。
你可能只是精通搜索 我们现在所处的时代,信息爆炸,每个人每天都会接触,阅读很多的信息,快速消费,快速遗忘。那种每天早上起来如同皇帝批阅奏折的、虚假的误以为掌握知识的错觉,驱动我们进入一个恶性循环。
即使在我们真的打算解决问题,进行主动学习时,更多的也只是在熟练使用搜索引擎而已(在一个领域待久了,你所使用的关键字准确度自然要比新人高一些,仅此而已)。精通了高效率搜索之后,你会产生一种你精通搜索到的知识本身的错觉。
如何写一个Shell脚本 在写博客的时候,我通常会在文章中配图。图片一般会放在一个有固定格式的目录中,比如现在是2016年5月,我本地就会有一个名为$BLOG_HOME/images/2016/05的目录。由于使用的是markdown,在插入图片时我就不得不输入完整的图片路径,如:/images/2016/05/stack-overflow.png。但是我又不太记得路径中的images是单数(image)还是复数(images),而且图片格式又可能是jpg,jpeg,gif或者png,我也经常会搞错,这会导致图片无法正确显示。另外,放入该目录的原始文件尺寸有可能比较大,我通常需要将其缩放成800像素宽(长度无所谓,因为文章总是要从上往下阅读)。
为了自动化这个步骤,我写了一个小的Shell脚本。当你输入一个文件名如:stack-overflow.png后,它会缩放这个文件到800像素宽,结果是一个新的图片文件,命名为stack-overflow-resized.png,另外它将符合markdown语法的文件路径拷贝到剪贴板里:/images/2016/05/stack-overflow-resized.png,这样我在文章正文中只需要用Command+V粘贴就可以了。
有了思路,写起来就很容易了。缩放图片的命令我是知道的:
$ convert -resize 800 stack-overflow.png stack-overflow-resized.png 但是要在文件明上加入-resized,需要分割文件名和文件扩展名,在Bash里如何做到这一点呢?Google一下:
FULLFILE=$1 FILENAME=$(basename "$FULLFILE") EXTENSION="${FILENAME##*.}" FILENAME="${FILENAME%.*}" convert -resize 800 $FULLFILE $FILENAME-resized.EXTENSION 难看是有点难看,不过还是可以工作的。接下来是按照当前日期生成完整路径,date命令我是知道的,而且我知道它的format格式很复杂,而且跟JavaScript里Date对象的format又不太一样(事实上,世界上有多少种日期工具,基本上就有多少种格式)。再Google一下:
$ date +"/images/%Y/%m/" 最后一步将路径拷贝到剪贴板也容易,Mac下的pbcopy我也会用:echo一下字符串变量,再管道到pbcopy即可:
PREFIX=`date +"/images/%Y/%m/"` echo "$PREFIX$FILENAME-resized.EXTENSION" | pbcopy 但是将内容粘贴到markdown里之后,我发现这个脚本多了一个换行。我想这个应该是echo自己的行为吧,会给字符串自动加上一个换行符。Google一下,发现加上-n参数就可以解决这个问题。
好了,完整的脚本写好了:
#!/bin/bash FULLFILE=$1 FILENAME=$(basename "$FULLFILE") EXTENSION="${FILENAME##*.}" FILENAME="${FILENAME%.*}" convert -resize 800 $FULLFILE $FILENAME-resized.EXTENSION PREFIX=`date +"/images/%Y/%m/"` echo -n "$PREFIX$FILENAME-resized.EXTENSION" | pbcopy 嗯,还不错,整个过程中就用了我十几分钟时间而已,以后我在写博客时插入图片就方便多了!
不过等等,好像有点不对劲儿,我回过头来看了看这段脚本:7行代码只有1行是我独立写的!没有Google的话,查看man date和man echo也可以解决其中一部分问题,不过文件扩展名部分估计又得花较长时间。
仔细分析一下,之前的成就感荡然无存。
更多的例子 我相信,过几周我再来写这样一个简单的脚本时,上面那一幕还是会出现。开发者的IDE的外延已经将Google和Stack Overflow集成了。很难想象没有这两个IDE的插件我要怎样工作。
其实除此之外,日常工作中这样的事情每时每刻都在发生:
Ansible里如何创建一个给用户robot读写权限的目录? Python 3中启动简单HTTPServer的命令是? Spring Boot的Gradle String是? Mongodb中如何为用户robot授权? Gulp里一个Task依赖另一个Task怎么写? 等等等等,这个列表可以根据你的技术栈,偏向前端/后端的不同而不同,但是相同的是在Google和Stack Overflow上搜索,阅读会浪费很多时间,而这些本来都是可以避免的。...
Web站点的响应速度 雅虎在2006年就发布了一份提升Web站点响应速度的最佳实践指南。该指南包含了35条规则,分为7个类别。这些规则已经被广泛使用,并指导人们在新的站点设计时更有针对性的考虑问题。这份指南已经成了Web前端性能度量的一个事实标准了。
YSlow是一个基于这份指南的测试工具,它可以测试一个站点是否“慢”,以及为什么“慢”?你可以通过很多方式来使用YSlow,比如Firefox,Chrome的插件,命令行工具,甚至PhantomJS这样的无头(Headless)浏览器。YSlow会检测你的站点中的资源是否没有压缩,是否缺失了超时设置,更进一步,它还会检测你的JS/CSS是否已经压缩/精简化,图片的尺寸,是否使用了CDN等等很多的维度。它还可以生成很多格式的报告,比如打分信息,TAP协议的输出,以及junit测试报告的格式。
我们这里讨论如何在持续集成服务器上设置一个YSlow任务,这个任务会在每次构建之后,测试你应用的性能指标,以帮助你更快的发现和定位问题。当然,我推荐你在staging环境,很多开发者在测试环境,本地开发环境都会启动很多对Debug友好的设置,比如未压缩的JS/CSS,没有超时设置的响应等,这会导致该构建任务的打分不够准确。
搭建CI环境 按照传统方式,如果要搭建一个这样的CI任务,我们需要至少做这样一些事情:
安装JDK 安装Jenkins 安装PhantomJS 安装YSlow.js脚本 然后设置环境变量,在Jenkins上创建任务,并运行YSlow.js脚本。这个任务很简单,只需要设置好参数,然后将结果输出为Jenkins上的报告即可。比如:
$ phantomjs /var/share/yslow.js -i grade -threshold "B" -f junit \ http://bookmarks-frontend.s3-website-us-west-2.amazonaws.com/ > yslow.xml -i grade 展示打分(grade)信息(还可以是basic/stats/all)等 -threshold "B" 指定失败的阈值为B -f junit 输出为junit识别的XML格式 这里的阈值可以是数字(0-100分),字母(A-F级别)或者一个JSON字符串(混合使用)
-threshold '{"overall": "B", "ycdn": "F", "yexpires": 85}' 上面的命令会测试站点http://bookmarks-frontend.s3-website-us-west-2.amazonaws.com/的各项指标,并应用雅虎的那35条规则,并最终生成一个junit测试报告格式的文件:yslow.xml。
但是维护CI环境是一个比较麻烦的事情,而且既然每个项目都可能会用到这样的基础设施,一种好的做法就是将其做成一个镜像保存起来,以方便其他项目的复用!这里我们使用docker来安装和配置我们的CI环境,配置完成之后,我们可以将docker的镜像分享给其他团队,也可以供我们在下一个项目中使用。
基于docker/docker-compose的环境搭建 在docker出现之前,我们要搭建一个测试或者staging环境,往往需要很多个不同角色的机器:有专门的数据库服务器,文件服务器,缓存服务器,Web服务器,反向代理等等。这样在成本上显然是个不小的开销,如果将所有不同的组件部署在同一台机器上,则又可能互相干扰,只需要一个小小的失误,整个系统就需要重新配置。更可怕的是,这个环境和生产系统不一致,那么很可能真实的错误要等到系统上线之后才会被发现。
比如在2012年,我所在的一个项目中,客户的系统采用传统的J2EE架构。本地开发中,我们使用了Jetty作为容器,而测试和Staging环境使用了Tomcat。由于Tomcat对空格的处理和Jetty有所不同,我们在本地测试通过,并且运行良好的代码,在Staging变得完全不能工作。这个问题花费了团队很多时间来排查错误。
在docker出现之后,我们可以在一台物理机器上运行多个互不干涉的容器,每个容器可以是一个组件(比如运行数据库的容器,Web服务器容器等等)。这样不但缩减了成本,而且可以让我们的环境尽可能和生产环境一致(有的项目甚至直接将CI出来的镜像应用到生产中)。不过对多个容器的管理是一个很麻烦的事情,好在docker提供了docker-compose工具来解决这个问题。使用docker-compose可以定义一组互相独立,但是又可以协作在一起的容器,这样我们可以很容易的搭建一个仿生产环境。
比如我们可以定义个docker-compse.yml
app:build:.links:- db:postgresports:- 8000:8000volumes:- .:/appworking_dir:/appentrypoint:/app/start.shenvironment:JDBC_DATABASE_URL:jdbc:postgresql://postgres:5432/bookmarksDATABASE_USER:bookmarks-userDATABASE_PASS:bookmarks-passworddb:image:postgres:9.3ports:- 5432:5432environment:POSTGRES_DB:bookmarksPOSTGRES_USER:bookmarks-userPOSTGRES_PASSWORD:bookmarks-password这个docker-compose定义了两个组件,app和db。db使用了postgres:9.3镜像,并设置了自己的环境变量。app则从当前目录.构建一个新的镜像,app与db通过links属性连接起来。
如果在当前目录执行docker-compose build命令,docker-compose会找到本地的Dockerfile,然后构建出一个docker的镜像,并启动该容器,同时,它还会启动postgres:9.3容器作为数据库组件。这样我们的环境就被完整的搭建好了。
搭建CI环境 app:build:.ports:- 8080:8080- 50000:50000volumes:- ./data:/var/jenkins_home这个配置,表明我们会根据当前目录的Dockerfile来构建一个镜像。
通过命令volumns,我们将本地目录./data映射为jenkins_home,这样我们定义的job信息,以及插件的安装都会放到本地的目录中,方便管理。配置完成之后,构建并启动该容器:
FROM jenkins:latest # Env ENV PHANTOMJS_VERSION 1....