先前参加了D2,其中天猫的不四用 Node.js 构建海量页面渲染服务分享中,关于稳定性提到并重复三次的一个事情就是:单元测试

之前做项目的时候,也曾经使用mocha写一些单元测试,开始写了部分接口单元测试后,就没再坚持下去。至于为什么没坚持下去,后面我再分析。
但是打从心底觉得,单元测试是开发中很重要的一环,乘着年前稍微空闲,总结一下对于单元测试的一些想法。

为什么没坚持

回想先前的一些项目的情况:一开始,下很大决心,准备完善的编写测试,可是写着写着,就没坚持下来,这是为啥呢?

分析原因有几个:

费时间,拖进度

单元测试往往会占用一半或以上开发时间,从开发感觉上,就会自己进度变得缓慢,心里有点慌,就没继续写下去了

有时候觉得很傻逼

有时候一些接口比较简单,写的过程,就会觉得,我明知道这个代码的执行结果(输出)了,还写这个测试代码?有意义吗?

无法模拟真实环境

有时候,有些代码,运行在webview下或者接口依赖于内部的某些api,模拟环境依赖于mocha这些库很难做到,这时候,要写单元测试起来,就很麻烦了
于是,干脆就算了~~ ̄□ ̄||

不清晰应该测试哪些内容

我们都知道,单元测试,在于构造一些场景出来,作为输入条件,然后最终期待正确的接口,这里就涉及到一个场景构造的问题,一个接口对外暴露给别人使用,
我们就得预测各种场景,所以一个非常简单的函数,就得构建不少的场景,心里会有一堆堆的场景出现,

  1. 参数A未传的情况,参数B未传的情况~~
  2. 上下场景不存在的情况
  3. 参数缺失要不要做处理
  4. 数据类型要不要验证下,要不要判断,转换一下?
  5. 。。。

一个简单函数,得写多少个单元测试,心里压力就上来啦

1

#### 部份代码无法测试

先前的项目,基本上是用seajs ,匿名闭包 或 commonjs 进行模块化,只能对对外暴露的接口进行测试,测试范围有限。在没了解`rewire` 或 `jest`
之前,觉得单纯测试接口,效果不大,呵呵呵~~那就算了~~

总结为什么没坚持下来,就两个字:

```
    麻烦
```

## 为什么还得坚持

分析了之前为啥没坚持,的确麻烦,真心麻烦,构造场景,写单元测试,改源码,再测,再改

但是,纵观各个编程语言,无不强调单元测试的重要性,更有甚者(golang),直接集成单元测试相关的库,可见在质量保障上的地位。
坚持下去肯定是必须的,它会给我们带来什么好处?

信心

信心源自于对代码的掌控,对代码的把握在于单元测试。有完善的单元测试,能够建立起对项目代码的信心。毕竟,这些代码都通过了单元测试,
预料之中,预料之外的情况,都做了考虑,无需担忧。

撸棒性

程序的稳定,在于不同场景下,能够经得起折腾,不轻易down机,这就需要我们单元场景中,多考虑各种情况,编写代码解决这些情况,
遇到问题,解决问题的同时,编写对应的单元测试,不断完善测试覆盖面

可维护性

程序员最讨厌的事情,就是维护别的系统。但却是大部分人工作的主要内容,如果有幸参与从零开始的项目,为了防止被后人骂得太厉害,单元测试还是写一下吧。
为什么要写呢?单元测试其实就是示例,当人家看不懂代码时,看看你的单元测试,大概就可以猜测出,你的函数、接口是干嘛用的了。

同时,当别人改你代码时候,可能并不是通读整个系统代码,就开改了,这时候,单元测试就保障了你的程序,不会被改坏~~

如何做好单元测试

既然好处多多,那么接下来聊聊怎么才能写好单元测试

编写可测试代码

首先考虑到单元测试的同学,对代码质量要求肯定不低,下面整理了一个思维导向图,主要描述如何才能保证产出的代码有一定得可测试性。
总之一句:编写优雅代码, 让阅读你代码的人,觉得是件赏心悦目的事情,而不是煎熬。

[现值]

单元测试工具选型

最近总结部门内部使用的公用库,用到技术栈为: karma + mocha + chai + rewire + phantomjs,仅供参考,这里想说一下,
不要纠结于选什么测试库或者runner,选一个你熟悉的就可以开始干活,没用过的话选一个github上star多的

  • karma 是一个test runner,配置简单,插件丰富,不需要你写静态脚本
  • mocha 单元测试库
  • chai 断言库
  • rewire 依赖注入库
  • phantomjs 用于CI服务器自动测试使用

rewire 是一个非常不错的依赖注入库,上面提到的部份代码无法测试问题,通过这个库轻松的解决,类似的还有facebook出品的jest

单元测试原则

首先,单元测试行覆盖率越高越好,但是也无需过滤纠结那个数字。当你做的是一个业务项目,能够覆盖基本的场景就可以了,对于公用库,
则需要提高一定得覆盖率,大家一看你测试覆盖率90%以上,用起来也有信心。

如果写业务代码,考虑基本的场景就可以了,代码进行相应处理,不用过于杞人忧天,考虑非常多不可能出现的情况,代码也不至于过于罗嗦和臃肿。

Preface

有幸参加大阿里举行的D2前端大会,今天看到知乎也有类似参加D2是什么体验你有什么收获?之类的提问,
所以我也稍微总结一下,也对得起这来回的机票钱 :)

这次大会的主题是融合, 知乎上的网友这么解读的, 说得真好,我这里把它引用过来。。

1
2
3
4
5
6
7
著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
作者:徐飞
链接:https://www.zhihu.com/question/38637676/answer/77516017
来源:知乎

今年是第十届d2,主题是融合,确实,今年的主要话题有三块,一块仍然是新框架和前端方案的探索,一块是node相关的深入应用,一块是最贴近主题的融合,也就是多端合一的技术方案。

其实说直白了,主要涵盖了几个方面:

  • react-native
  • nodejs
  • 一些轮子

此行目的也很简单:

  1. 关注nodejs在各个企业中的实践经验及技术架构
  2. 关注讲师们演讲风格/技巧

对我听过每个议题的内容做一下总结:

用 Node.js 构建海量页面渲染服务

这个是第一场演讲,干货非常多,比较惊喜的一场演讲,也很惊讶在阿里内部对于nodejs有如此广泛的应用
整个演讲可以听出,nodejs在手淘中,主要用于业务逻辑处理,html渲染上,也就是

  • nodejs替换了中间层(php),后端原来干嘛还是干嘛,该提供接口还是得提供接口
  • 页面渲染还是在nodejs端,不是单页模式(SPA)
  • 接口中转(私有协议代理等)

边听边记录一些实践上具体疑问,准备最后QA环节提问,后面主持人没看到我,坐山顶的同学比较吃亏!!!
不过大部分问题也都得到答案,这里列一下当时准备的一些问题,其中一些演讲过程中,就解答了这些问题。
不得不说,演讲者内容方面准备得非常充分。

问题列表:

  1. 架构模型是如何?
  2. 如何保证nodejs稳定性?
  3. koa中间件出现异常之后,会如何处理?
  4. 发布机制
  5. 日志记录
  6. CDN缓存机制

其中第2,3,6个问题,讲师在后面的演讲中都有提到,总结一下:

问题2,稳定性

  • 单元测试,单元测试,单元测试
  • 编写可测试代码
  • 代码覆盖率
  • cfork/graceful(这里也提到为什么不用pm2,跟我目前实践是一样的pm/graceful,有种原来他们也这么做感觉)
  • 日志记录,错误预警(短信,邮件)
  • 容灾机制

问题3:koa中间件异常

这个问题后面给出了答案,koa中间件的异常并不会导致挂掉,而是会在类似app.onerror的函数中统一处理,
做一些日志记录,致命错误做告警之类的

问题6:CDN缓存机制

讲师提到他们用到了CDN缓存,一开始以为只是对js/css这些静态资源进行缓存,而实际上,不仅于此,他们还对实际的渲染出来
的页面进行缓存,而具体的推送/刷新机制,他们内部应该有一套比较成熟的CDN方案,后面有机会再去详细了解一下,这个已经也超出
前端解决的范围。

问题 1、4、5 这几个问题,本来想休息时候请教一下讲师,由于时间关系,一直到会议结束,才找了Qzone的同学问了相同的问题,(-__-)b~

手机淘宝Hybrid性能优化实战

这场有两个同学一起讲的,原以为会有react native相关的实践,其实主要是一些性能优化方面的建议以及他们hybird实现的方案,
优化的建议后面PPT出来之后都可以看到,这里就不赘述了,而hybird这一块挺有意思的,类似于,定义一些html标签属性,通过截获
这些属性,把原来的标签替换成原生组件。这里我就在YY,如果浏览器外壳能够支持这个功能,那么写出来的webapp交互体验上就非常棒了。

alinode与Node应用性能管理

下午场抱着膜拜大牛的心情, 听了朴灵alinode与Node应用性能管理, 2年前就买过他的深入浅出nodejs,这本书真心不错,通俗易懂,推荐一下。
听完整场的演讲,给我的印象是,腼腆的技术型奶爸,没有很花俏的ppt,没有俏皮的话,甚至现场互动都有点尴尬,但是有全面准备的示例代码和演示。

分享的内容主要是围绕着alinode及阿里云提供的在线分析工具,主要功能就是在node端提供一个类似Chrome开发工具的Profiler,依赖于alinode,可以对
线上实时Profile,实时抓取堆快照,然后利用他们提供的在线分析工具,排查线上内存泄漏CPU异常飙升 的问题。

Node.js加速Qzone

这场分享后面才进来,演讲嘉宾感觉很有气场,也有一些互动,因为是nodejs相关,也听得比较认真,这里罗列了一些关键点:

  • QQ nodejs装机量260台,qzone是80台服务+60台代理
  • 砍掉nginx代理,node直接提供http
  • 服务端渲染首屏(这个与手淘是一致的)
  • 本地html快照缓存,提升css响应速度
  • 服务端的fidller日志,实时日志下载
  • 流水日志

Qzone在nodejs的使用上更加激进,直接把nginx这一层去掉了,他们如何做负载均衡的,会后跟这位同学了解到,
在他们的网络层,还有另外一层做这个事情,可以更完整的做流量控制。

上面提到的1,4,5 问题也在会后跟他沟通

问题1: 架构模型

这个其实是想问手淘那边的架构模型,后面找机会再去了解一下。Qzone 在这个问题上就是,直接让nodejs作为一层中转代理,也是他在演讲
说的接入层,同事也做html模板渲染,协议转换等

问题4: 发布机制

由于他们有260台nodejs服务器,所以我想到,这么多服务器,他们发布程序是怎么进行的
会后了解到,他分两部分解释,一个是nodejs的装机,另外一个是业务程序发布

  • nodejs 发布是可执行文件和依赖包一起打包,统一发布到各个服务器
  • 业务打包,业务代码与node_module一起打包,统一发布各个服务器

这里他还提到一点,node_module他们是直接提交svn服务器,然后打包时候,从svn拉取,打包,再发布
这里有点惊讶,难道他们是不知道npm shrinkwrap

问提5: 日志记录

对于线上的日志记录,他们有一套日志系统和流水系统,应该是有专门的团队做这个事情

  • 日志系统用于记录服务器端的每次操作,执行的动作
  • 流水记录则记录服务端收到的网络请求,完整的记录http请求的请求/回复,记录与其它接口的交互,并按照fiddler的文件格式进行存储

DataV数据可视化引擎

逼格颜值最高一场分享!分享嘉宾很会讲,在台上淡定自如,思路清晰,很喜欢这样的演讲风格。这里列一下一些关键点:

  • 性能上 webgl > svg > canvas
  • 大屏幕可以通过组合屏形式实现
  • 静态内容可以使用canvas绘制

最近同学买房子,问我房贷要选30年,还是20年好?我说当然选长一点,他说贷款30年贷款时间是长一些,可是利息多26万啊,于是我说:利息是多,但是得算上通货膨胀

26万,不是个小数目。那么上面说的理由,太过于笼统,也缺乏说服力,大家都知道人民币会贬值,购买力会降低。但是还是不能确定选哪个?怎么知道20年,会不会通货膨胀到26w没了?

只代表个人观点,对于文章中的模型,有不同的意见,欢迎一起探讨!如果你是土豪,请直接付全款:P

案例分析与比较

  • 贷款总额: 89w
  • 30年月供: 4427, 总额: 159.37w
  • 20年月供: 5555,总额: 133.32w
  • 20年月供,每月多1k
  • 30年月供总额比20年多 26w,也就是要多付26w的利息

30年还款总额 - 20年还款总额

1
159.37w - 133.32w = 26.05w

26w, 26w, 26w!!!!! 好多好多钱!!!!!

如何计算通货膨胀?

如何计算通货膨胀,需要解决两个问题:

  • 通货膨胀需要用一个数值来表示
  • 利用通货膨胀,基于当前月供数,算出一个基于当前时间的价值

为了,解决上面两个问题,又做了下面三个假设条件

  • 使用央行基准利率作为通货膨胀率
  • 假设 通货膨胀率 >= 央行基准利率
  • 假设每年通货膨胀一样

计算方式:

  • 通货膨胀率:π
  • 每年月供总额:p
  • 折算后第t年的价格: pt

计算公式:

1
pt = p * (1 - π)^t

于是就有下面这个excel表格:

[现值] excel下载

最终按照2015年央行基准利率2.5%计算,则有:

1
30年现值总额 - 20年现值总额 = 71331

也即:折算成当前价值,30年月供需要比20年多支付7.1w的利息

7.1w 划算否?

数学比较

首先,上面使用基准利率2.5%作为通货膨胀率,然而通过查阅国家统计局网站,过去五年的居民消费水平 如下表:

指标 2014 2013 2012 2011 2010
居民消费价格指数(上年=100) 102.0 102.6 102.6 105.4 103.3

取平均值得3.18%,将这个指标替换基准利率后,得出:

1
30年现值总额 - 20年现值总额 = 39085.88744

按照3.18%算,实际只需多支付3.9w

那么如果按照支付7.1w, 会如何?

  • 相对于20年期贷款,前20年你多出了21.5w资金
  • 你需要为这21w资金多付出7.1w利息
  • 因此, 30年期贷款的最后10年,你需总共支付28.64w

这21.5w的贷款利率为:

1
r = 7.1/(21.5*10) = 0.033 = 3.3%

3.3%作为20年后的贷款利率可以说是非常低的,当前公积金贷款利率是3.5%,所以说,还是相当的划算。

直观因素比较

直观上,就好像其它朋友说的,每月多1k闲钱,当你结婚生子后,会发现可以让你生活过得舒服很多!

当你还是单身时,你可以把这笔钱存起来,或者做一些投资,比如放余额宝或者炒股:)

A successful Git branching model(翻译)

原文链接:
http://nvie.com/posts/a-successful-git-branching-model/

发现早早就有人翻译过了,囧rz

1
http://www.oschina.net/translate/a-successful-git-branching-model

本文将介绍我在一年前,在所有项目(工作上和个人的)上引入一个开发模式,这个模式最终在实践上也很成功。
我之前一直想要写关于这方面的文章,但从没有时间去透彻的解析这个模式,直到现在。我不会在文中讨论关于我们项目
的细节,而只是介绍分支的策略和发布管理。

[git模型]

本文只关注于git作为一个源代码管理工具相关的实践。

Why git?

关于git与其他中心化源代码工具的利弊对比,请看这个链接
网上也还有很多关于这方面的争论。作为一个开发者,我在所有工具中选择了git。Git真的改变开发者对于合并与分支的想法。
最为一个从传统CVS/Subversion过来人,合并/分支一直是有点可怕的东西(“小心合并冲突,他们会咬你”)而且很长时间才会做一次。

但是在git下,这些都是非常简单和廉价操作,也是日常工作流程中核心内容。例如,在CVS/Subversion书里面,分支与合并在最后一章
才会讲到(而且只针对高级用户),但是在每本git书里,基本上在第三章就会说到。

由于它的简单性和重复性,不用担心分支和合并的问题。相对其他事情,版本控制工具就理应更好的协助分支/合并工作。

关于工具内容就这样先,我们接着来看一下这个开发模型。这个模型实际上就是一个系列的步骤,每个团队成员都必须遵守,从而来实现一个
可管理的软件开发流程。

去中心化,但又中心化

仓库的设置和使用,也适用当前这个分支模型,也有一个中心的真正仓库。只是这个仓库被认为是中心节点(实际上git是一个分布式版本管理,
技术上是没有中心仓库这个概念)。我们把这个仓库称之为origin, 对于所有git用户对这个名字应该很熟悉。

[git]

每个开发者都往orgin 拉取/推送修改。但除了与中心节点的push-pull关系外,每个开发者之间可以形成一个子team,相互拉取修改。
例如,当两个或多个开发同时在开发一个较大的新功能,但是推送到origin有点早,这时候这个功能非常有用。在上图中,有这些子team
Alice和Bob子team,Alice 和David 以及 Clair 和David。

实际技术上的实现就是,Alice建立一个git remote,命名为bob,然后指向Bob仓库,其它子team同样道理。

主要的分支

[主要的分支]

该开发模型最核心的内容,是基于上面模型的启发。中心有两个永久存在的分支

  • master
  • develop

对于git用户应该很熟悉在origin仓库中的master分支,另外一个则称为develop.

我们认为 origin/master 源代码的HEAD指针,应该反应线上的真实状态的主要分支

我们认为 origin/develop源代码的HEAD指针,经常反应下一个版本更新的最新开发修改。有些人会叫做集成分支。nightly build也是这个分支。

当develop分支的源代码到达一个稳定的时候,而且可发布,所有的修改会合并回master,然后一个发布版本号打上标签。至于怎么做,接下来会讨论细节内容。

因此,每次当修改备合并到master时,实际意义就是一次全新的产品上线。对于这个操作我们有很严格的要求,所以理论上,我们会用一个Git钩子脚本,每次有commit到
master,执行自动化构建同时发布我们的软件到生产服务器。

其它支持的分支

除了masterdevelop分支外,我们的开发模型还有其他几个类型的分支,来辅助团队成员间的协同开发,更方便的追踪功能,产品发布准备以及快速修复线上问题。
与主分支不同的是,这些分支经常生命周期比较短,最终也都会被删除。

这些可能用到分支有以下几种类型:

  • 功能分支 (feature branch)
  • 发布分支 (release branch)
  • 修复分支 (hotfix branch)

每种类型都有特定的意义,同时有严格的要求:应该从哪个分支从开始创建,一级应该最终合并到那个分支。一会我们会过一些这些细节。

这些“特殊”的分支,在技术上没有什么意义,这些分支是依据我们的使用方式进行分类,它们自然跟普通分支没什么区别。

功能分支

[feature branch]

由哪个分支checkout

develop 

必须合并到

develop

分支命名规则

除了master,develop,release-* 和 hotfix-*之外

功能分支(有时也叫特性分支)主要用于开发一个即将或者很久以后才发布的版本,这个新功能将在那次发布中合并也是未知的。
基本上功能分支在开发过程中一直存在,最终会合并到develop分支(当确定要在即将发布中发布时)或者丢弃(万一体验非常糟糕时)。

功能分支基本上只存在开发者的仓库,不会出现在orgin

创建一个功能分支

当开始一个新功能开发,从develop分支checkout

1
2
$ git checkout -b myfeature develop
Switched to a new branch "myfeature"

集成一个完成功能到develop

已完成功能当确定要在接下来的发布一起发布时,会合并到develop分支

1
2
3
4
5
6
7
8
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff myfeature
Updating ea1b82a..05e9557
(Summary of changes)
$ git branch -d myfeature
Deleted branch myfeature (was 05e9557).
$ git push origin develop

这里--no-ff 参数,merge时只会创建一个commit对象。就算是merge是fast-forward。这可以避免丢失功能分支的历史存在,
同时对所有的该功能的提交组合一起。对比:

[no-ff]

右边的merge方式,你无法从git历史记录中看出,那些commit是实现了某个功能,你必须通过读每条日志去分辨。
还原一个功能(比如一组commit),在第二种情况下是很头疼的一件事情,但加上--no-ff标识,则会很简单。

是的,你会创建一些(空)提交对象,但是好处比这个损失多。

不幸的是,目前还没有办法git merge时,将 --no--ff设置为默认,但真的需要带上这个标识。

发布分支

由哪个分支checkout

develop 

必须合并到

develop和master

分支命名规则

release-*

发布分支用于一个发布的准备工作,它运行最后一刻的细节修改。甚至,它允许一些细小的bug修复和准备发布用的meta-data(版本号,构建日期等)
这些工作都在发布分支中完成,develop分支被清出来用于下次大发布。

从develop分支切换一个发布的关键时刻应该是,当develop能够反映(几乎反应)下个发布的状况时。至少即将发布和构建的所有功能,都确定合并到
develop分支。其它后续发布的功能则不能涵盖在这个release中,它们必须等该分支切换出去后。

它是一个即将的发布,能够确切指定一个版本号的开始的时候-不是更早时候。直到那个时候,develop分支放映“下次发布”的修改,但它不清楚,这次发布
最终会变成0.3 还是1.0,知道最终发布分支开始。这个必须在release 分支开始的时候决定,根据项目的版本号跳跃规则。

创建一个发布分支

发布分支从develop分支创建。例如,版本号1.1.5是当前线上的版本,我们有一个大版本即将发布。develop分支的已经准备好“下次发布”,同时,我们也决定
即将发布的版本称为1.2(而不是1.1.6 或2.0). 所以我们可以从develop分支创建一个名字能够反应版本号的发布分支:

1
2
3
4
5
6
7
$ git checkout -b release-1.2 develop
Switched to a new branch "release-1.2"
$ ./bump-version.sh 1.2
Files modified successfully, version bumped to 1.2.
$ git commit -a -m "Bumped version number to 1.2"
[release-1.2 74d9424] Bumped version number to 1.2
1 files changed, 1 insertions(+), 1 deletions(-)

创建一个新分支并切换到它后,我们更新版本号。这里,bump-version.sh是一个虚拟的shell脚本用于修改版本号。(当然也可以手工修改-这时候会有一些文件修改)
然后,版本修改会被提交。

这个新分支会存在一段时间,知道发布分支真正被发布出去。这段时间,bug的修复会合并到这个分支上(而不是在develop分支)。严格禁止添加大量的新功能。
它们必须合并到开发分支,等待下次发布。

完成一个发布分支

当发布分支到达一个可以真正发布的状态时,需要做几个事情。首先,发布分支合并到master(记住,master上的每个提交都是一次发布的定义)。
接着,master上的提交必须标签,以便将来回顾历史版本。最终,在release分支上的修改也必须合并会develop,从而将来的发布也会包含这些bug修复。

前两个git步骤如下:

1
2
3
4
5
6
$ git checkout master
Switched to branch 'master'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Summary of changes)
$ git tag -a 1.2

这样发布就完成,然后添加tag用于将来引用

1
Edit: 你也可以加上-s或者-u <key>标识来对你tag进行加密签名

为了保留发布分支的修改,我们需要合并到开发分支.

1
2
3
4
5
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Summary of changes)

这个步骤可能会导致合并冲突(很可能,因为我们改版了版本号)。如果这样,处理冲突并提交。

现在发布真正完成,发布分支可以删除,我们不再需要它。

1
2
$ git branch -d release-1.2
Deleted branch release-1.2 (was ff452fe).

修复分支

[hotfix]

由哪个分支checkout

master 

必须合并到

develop和master

分支命名规则

hotfix-*

Hotfix分支很想发布分支,因为它也是准备一个线上发布,虽然是计划之外的。它们方便于快速处理线上版本的问题。当一个线上版本出现一个致命错误,我们必须马上解决,
hotfix分支从master分支的标识服务器版本的tag中分支出来。

基本上,其它团队成员,还是继续在develop分支继续开发,但是另外一个成员准备一个快速的线上修复。

创建hotfix分支

hotfix分支基于master分支创建。例如,假设版本1.2是当前线上运行版本,因为一个严重的bug导致一些问题。但是在develop上的修改还不稳定。这是我们分出一个hotfix
分支来解决这个问题:

1
2
3
4
5
6
7
$ git checkout -b hotfix-1.2.1 master
Switched to a new branch "hotfix-1.2.1"
$ ./bump-version.sh 1.2.1
Files modified successfully, version bumped to 1.2.1.
$ git commit -a -m "Bumped version number to 1.2.1"
[hotfix-1.2.1 41e61bb] Bumped version number to 1.2.1
1 files changed, 1 insertions(+), 1 deletions(-)

不要忘记在切出分支后,修改版本号

然后,修改bug并提交一次或多次commits:

1
2
3
$ git commit -m "Fixed severe production problem"
[hotfix-1.2.1 abbe5d6] Fixed severe production problem
5 files changed, 32 insertions(+), 17 deletions(-)

完成hotfix分支

完成后,bug fix需要合并会master,同时也需要合并回develop,保证下次发布会包含这个bugfix。这流程跟发布分支的流程一模一样。

首先,更新master 和 为release添加tag

1
2
3
4
5
6
$ git checkout master
Switched to branch 'master'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)
$ git tag -a 1.2.1

Edit: 你也可以加上-s或者-u 标识来对你tag进行加密签名

接着,将bugfix也合并到develop

1
2
3
4
5
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)

有一个特殊的情况是,当一个发布分支已经存在时,这个修复修改必须合并到发布分支,而不是develop。在release分支完成后,最终修改也自动同步到develop分支。
(如果develop分支等不及release分支完成,马上需要这个bugfix,你也可以合并这个bugfix到develop)

最后,删除这个临时分支

1
2
$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).

总结

虽然这里没有很多全新的惊人的分支模型,大致的框架已经本文的开始那里,在我们的项目中超级有用。它让我们形成了一个优雅的思维模型,
一个容易实现,同时让我们团队成员,达成对分支与发布流程理解的共识。

react通过一个React.render函数来渲染启动一个app,具体这个函数都做了些什么事情?其实jsx编译过后,render函数中的html都转换成一堆React.createElement的代码,最终生成一个ReactElement传入到render函数

ReactElement数据结构

React.createElement 接收函数接收两个及以上参数:

  • type 通过React.createClass创建的类型
  • props 元素的属性值
  • 其它参数: 该元素的直接子级元素

最终生成ReactElement是一个树形结构,子元素保存在_store.props属性中

ReactElement树形结构

render函数

ReactElement树创建之后,通过React.render方法,将树生成具体的html,并渲染到对应的容器中。

参数

  • nextElement ReactElement对象,通过React.createElement创建
  • container 页面容器
  • callback 回调函数

render主要流程

  1. 对nextElement的合法性进行校验
  2. 检查container是否已经渲染过React组件
  3. 开始调用_renderNewRootComponent渲染

nextElement 合法性校验

主要通过ReactElement的_isReactElement 属性判断,如果非法则抛出异常

检查container是否已经渲染过React组件

  1. 根据ReactElement,从缓存查找是否存在preComponent
  2. 调用shouldUpdateReactComponent,检查是更新,还是替换原来节点

    如果需要更新,调用ReactMount._updateRootComponent更新原来节点(具体细节后续再补充,本文先针对正常流程)

    如果不需要更新,则调用unmountComponentAtNode,删除旧节点,然后执行一个全新的mount操作

_renderNewRootComponent内部调用流程图

Transaction概念

从源代码的注释可以看到,Transaction.perform 方法提供了一个执行函数的黑盒,

在函数开始时执行wrapper的`initialize`方法,结束后调用`close`方法。

Transaction作为一个基类,其它Transaction可以从它继承perform方法。

TransactionWrapper,是一个提供initializeclose方法的简单对象

Transaction 可以通过重写getTransactionWrappers来返回wrapper数组

Transaction 主要用途:

  • 恢复input的选中状态
  • 在渲染时,禁用类似blur/focus事件,渲染结束后,重新启用
  • 批量更新DOM元素的修改
  • 渲染新内容后,调用所有的componentDidUpdate函数

_renderNewRootComponent 在两个Transaction中执行:

  • ReactDefaultBatchingStrategyTransaction 用于启动批量渲染
  • ReactReconcileTransaction 执行渲染及子元素的渲染

下面是一个渲染全新React组件的核心流程:

render内部函数调用关系

现有问题

  • 现有代码基于项目,即使公用也无法共享

  • 继续抽象出来的公用模块,也需要复制到其他目录

  • 公用模块有多份代码,需维护多份代码,容易导致代码不一致

  • 更新困难,当公用模块修改一个bug时,需要搜索所有项目进行修改

  • 模块间的依赖关系混乱

使用bower及git解决方案

公用模块抽取

  • 公用代码抽取出来作为一个独立的git项目

  • 初始化该项目为一个bower项目

    该目录下执行 bower init

  • 添加公用代码,并把代码提交到gitlab

模块安装

  • 首先需要执行bower init 初始化调用目录

    初始化为bower项目的目的是为了保存该项目所引用的模块

  • 执行bower install + git地址 --save-dev

    例如

    1
    bower install http://fedgit.teiron.com/zhangbh/bower-test.git --save-dev
  • 代码会下载到bower_components目录下,页面可直接应用bower_components下的文件

    例如

    1
    <script src="bower_components/bower-test/index.js"></script>

模块更新

  • 更新模块时,需要更新bower.json文件的版本号,bower通过该版本号来判断,所以记得更新公用项目时,更新版本号

  • 调用方通过执行bower update + 模块名 来下载最新代码

注意

  • 抽取公用代码,保存在git服务器,公用模块必须有一定可复用性,不能依赖具体项目

  • bower_components不能提交源代码管理(git/svn)

  • 用户下载代码后,执行bower install 安装项目依赖模块

问题解决

  • 公用模块只存一份代码

  • 调用方无痛更新模块

  • 模块依赖关系由bower解决

  • 提高代码复用率

介绍

gruntjs是一个基于nodejs的自动构建化工具

Gruntjs官网

Getting Start

nodejs

1
2
3
4
Node.js is a platform built on Chrome's JavaScript runtime for
easily building fast, scalable network applications. Node.js uses an event-driven,
non-blocking I/O model that makes it lightweight and efficient,
perfect for data-intensive real-time applications that run across distributed devices.
  • 基于Chrome v8引擎
  • 事件驱动
  • 非阻塞io模型
  • 适合高并发,实时应用

Gruntjs可以做什么

  • 文件压缩
  • 代码合并
  • 图片处理(雪碧图合并,图片压缩等)
  • 自动化测试
  • 代码预编译(模板,sass等)

Why Gruntjs

  • 提高开发效率 (文件监听watch,自动刷新,自动构建等)
  • 提高性能 (js、css合并压缩,图片压缩,雪碧图等)
  • 质量保证(jslint, jshint)

安装

  • 安装grunt-cli(grunt命令行)

    npm install -g grunt-cli

  • 定位到你项目目录

    cd d:\www\branches\applepc\ios_v2

  • 初始化,根据提示输入项目信息,初始化完成后,会自动生成package.json文件

    npm init

  • 项目根目录,添加Gruntfile.js(文件名区分大小写), Gruntfile.js内容基本结构如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    module.exports = function(grunt) {
    grunt.initConfig({

    });


    grunt.loadNpmTasks('grunt-contrib-uglify');

    grunt.registerTask('default', ['uglify']);

    }
  • 通过npm install 安装你需要的插件. 注意: 这里加上--save-dev参数,
    安装完成后,会将该插件添加到package.json devDependencies字段

    npm install grunt-contrib-copy —save-dev

    npm install grunt-contrib-watch —save-dev

  • 回到Gruntfile.js中,为新安装的插件添加任务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    module.exports = function(grunt) {
    grunt.initConfig({
    copy:{//配置copy任务,详细配置信息,请参考grunt-contrib-copy github页
    main:{
    files:[
    {
    cwd: 'common/tpl/appDetails/compiled/',
    src: 'template.js',
    dest: 'common/js/bcvy6gt37&[8fgt4!3$8gfg82799/',
    expand: true,
    rename : function(dest, src){
    return dest + "appDetailsTpl.js";
    }
    }
    ]
    }
    }
    });

    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-copy');//加载任务

    grunt.registerTask('default', ['uglify']);
    grunt.registerTask('copy', ['copy']);//注册复制任务

    }
  • 运行grunt copy就可以执行复制任务

  • 自定义任务, grunt支持自定义任务,以腾讯tmod为例,请参考下面代码片段, 运行grunt ct可执行simple ArtTemaplte模板预编译

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    module.exports = function(grunt) {
    grunt.initConfig({
    copy:{//配置copy任务,详细配置信息,请参考grunt-contrib-copy github页
    main:{
    files:[
    {
    cwd: 'common/tpl/appDetails/compiled/',
    src: 'template.js',
    dest: 'common/js/bcvy6gt37&[8fgt4!3$8gfg82799/',
    expand: true,
    rename : function(dest, src){
    return dest + "appDetailsTpl.js";
    }
    }
    ]
    }
    }
    });

    grunt.registerTask('ct', 'complie template', function() {
    var TmodJS = require("tmodjs");
    var path = './common/tpl/appDetails/';
    var options = {
    output: 'compiled',
    helpers:'helpers.js',
    charset: 'utf-8',
    debug: false // 此字段不会保存在配置中
    }
    TmodJS.init(path, options);
    TmodJS.compile([], true);
    });

    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-copy');//加载任务

    grunt.registerTask('default', ['uglify']);
    grunt.registerTask('copy', ['copy']);//注册复制任务

    }

目录结构及源代码管理

  • node_modules目录(svn ignores)
  • package.json 及Gruntfile.js (svn)
  • 执行npm install

常见插件

  • grunt-contrib-uglify 文件压缩工具 (github
  • grunt-contrib-copy 文件复制 (github
  • grunt-css-sprite 从css文件中提取图片,生成雪碧图 (github)
  • grunt-contrib-watch 检测文件变化,然后执行相应的任务 (github)
  • 更多插件,请参考官网(Grunt.js官网插件页)

Gruntjs vs (gulp)[http://gulpjs.com/]

  • Gruntjs配置难度高
  • 任务配置不同,Gruntjs基于配置,gulp基于代码
  • gulp接口更优雅
  • gulp生态系统不如Grunt,日常使用应该足够
  • Grunt的I/O操作是瓶颈,适用小项目

reference

gulpjs官网

presentation build wars gulp vs grunt

Gruntjs vs gulpjs

背景

随着模块化进展,项目js文件越来越多,下面是越狱应用与正版应用的js引用, 真正发布产品过程,不可能直接将该文件丢到生产环境。
因此,发布前需要进行以下操作,压缩,合并js/css文件编译模板生成雪碧图, 文件进行缓存处理(filerev)
下面代码片段是越狱应用的js引用列表:

1
2
3
4
5
6
7
8
9
10
<script src="/common/js/jquery-1.7.2.min.js"></script>
<script src="/common/js/json3.min.js"></script>
<script src="/common/js/jqplugin.hashchange.min.js"></script>
<script src="/common/js/tr.jqplugin.slide.min.js"></script>

... 此处省去20个引用...

<script src="/common/js/iosV2/topicDetailController.js"></script>
<script src="/common/js/iosV2/searchController.js"></script>
<script src="/common/js/iosV2/main.js"></script>

本文档将介绍如何通过useminPrepareusemin来构建一个build流程。

构建过程

构建的最终结果是需要一个能正常运行的,可发布的程序包,同时,我们要保留源代码的结构以及原来的样子。
因此我们需要把程序构建(build)到子目录(dist),这时,一个构建过程大概需要以下步骤:

  • 清空dist目录,清空以前的构建代码,使之后编译到该文件夹的文件都是新的

  • 文件复制(copy),由于我们要保留源文件,因此我们把文件复制到指定目录,再进行后续处理

  • 合并js文件(concat)

  • 压缩合并后的js(uglify)

  • 计算压缩后js/css的md5或sha1值,替换原来的引用

实现以上步骤,则需要一系列的grunt任务

难点

其实构建过程很简单,无非是压缩、合并,其中最麻烦的是,如何通过gruntjs,自动的去替换原来的20几个应用

useminPrepare 与 usemin

整个构建流程主要通过grunt-contrib-useminPreparegrunt-usemin实现
useminPrepare原理是通过类似 <!-- build:js /common/js/xxx.js--> <!-- endbuild -->的注释块包含需要替换的js或css文件列表
动态生成concat,uglify,cssmin的任务,然后替换原来的引用
有了它,我们不需要在Gruntfile.js中的concat,uglify,cssmin添加一堆文件名
只需在最终build task中,添加concat,uglify,cssmin任务就可以
下面build任务可以很清楚看出构建流程

1
2
3
4
5
6
7
8
grunt.registerTask('build', ['clean',
'copy:build',
'useminPrepare',
'concat',
'cssmin',
'uglify',
'filerev',
'usemin']);

useminPrepare和usemin的配置也非常简单

注意:

  • useminPrepare需要添加 options:{ root :”.”} , 否则无法正常生成文件
  • usemin 需要制定assetsDirs 为dist/, 后续使用filerev时,才能正常找到对应的文件
  • 坑爹的目录关系!!
1
2
3
4
5
6
7
8
9
10
11
12
useminPrepare: {
html: ['dist/jb_app/index.html','dist/zb_pp_v2/index.html'],
options:{
root :"."
}
},
usemin:{
html: ['dist/jb_app/index.html','dist/zb_pp_v2/index.html'],
options: {
assetsDirs: ['dist/'] //指定改目录,让usemin寻找用来替换revved文件
}
}

grunt-filerev插件

以前对于静态文件的处理方法,在文件后面加上?v=10001 或者加上修改时间?update=20140203,这么做的问题虽然解决缓存问题,但是:

  1. 需要手工修改静态文件
  2. 如果文件没做出任何修改时,有时候会导致客户端重新加载文件

该插件按照一定的算法(md5,sha1等)生成一个hash值,然后取hash值的前几位作为文件名的一部分, 这样做的好处是,对于没修改过的文件,我们可以永久缓存到客户端

最后

依赖的grunt插件有 grunt-contrib-copy, grunt-contrib-clean, grunt-contrib-useminPrepare, grunt-contrib-concat, grunt-contrib-cssmin, grunt-contrib-uglify, grunt-usemin,grunt-filerev

今天地铁上看了《精通git》中的分支管理一章,刚好今天遇到一个场景,马上联系起来,心情非常激动,哈哈,这屌丝心里:),先来说说今天遇到的场景

场景

其实问题早就存在,只是在遇到git分支管理之前,一直都使用svn的蛋疼解决办法

  1. 需求新版应用详情页开发(基于项目svn进行)
  2. 开发完成后,界面拥挤,等待产品反馈过程中,对越狱应用进行重构,应用页也作相应重构
  3. 此时经理老唐过来了解情况问:现在能不能独立上线appDetails,我说问题不大,重构没有基于原有代码。
  4. 经理想先看一下效果,问题出现了

问题

  1. 重构与appdetails修改都基于项目svn,导致经理想看appdetails效果时,必须将重构appdetails的代码删除

  2. 根据产品反馈新版appdetails取消,此时需要回滚新版代码,但是保留重构代码

  3. 在测试完成之前,appdetails及重构都不能提交到当前svn,开发过程,无法追踪修改历程,也即没有commit记录

  4. 如果appdetails开发过程中,需要解决原有系统中的bug,需要revert部分代码,再做修改

  5. 重构代码无法分享给其他同事,同事需要另外配一个环境或修改nginx的配置来看新代码效果

那么我们来看看svn是如何解决上述这些问题的

svn解决办法

  1. 第一个问题由于我的修改都是基于一个备份,原来则改名为index.bak.html, 此时就需要把index.bak.html重命名index.html, 如果新功能不是基于备份修改,则更加蛋疼。

  2. 问题二需要手工删除新版appdetails代码,保留重构代码,然后小心翼翼的所有功能测试一遍

  3. 代码commit到另外一个以我名字命名的svn目录。同时,也利用这个目录来跟踪我的代码修改历史,每天下班前commit一次代码—!!!

  4. 问题四依然需要备份新修改,revert部分代码,然后修改再提交

  5. 问题五在问题三中解决了,测试环境需要重新手工配置

综上,svn解决这些问题的时候,都是比较简单粗暴,采用新建svn目录备份文件回滚手工对比

git分支的优雅解决方法

  1. 在内嵌页创建一个appdetails分支,所有修改都可以随时commit到该分支
    重构开始时,创建另外一个分支ios_refactoring,重构代码commit到该分支
    此次,需要给经理看代码时,只需切好到appdetails分支即可

    1
    2
    git checkout -b appdetails master
    git checkout appdetails
  2. 同样,需要回滚appdetails的时候,只需删除appdetails分支即可,此时并不影响重构分支

    1
    git branch -d appdetails
  3. 此问题需要搭配git服务器,然后把本地重构分支push到服务器,同事就只需要clone到本地即可

    1
    git clone ios_refactoring
  4. 与问题2一样的,只要重新切换回到主干,然后就可以马上进行修改,修改完成后,再切换回相应的分支即可,同时,可以将主干的修改也merge到分支中,或者等分支完成后,合并到主干时,再一起合并

    1
    2
    3
    4
    5
    git checkout master //切换回主干
    vim index.html //fix bug
    git commit -a -m "fixed some bugs"
    git checkout ios_refactoring //切换回重构分支
    git merge master // 将master的修改合并到分支
  5. 问题五解决办法依然是搭配git服务器,在同个目录下,进行分支切换

总结

现在一开始的问题就变成了分支间的无痛切换了,无需手工修改代码!!

1
2
3
git checkout master //切换回主干,修复原有系统的bug
git checkout appdetails // 切换回appdetails分支,可以供经理看可先上线版本的appdetails
git checkout ios_refactoring //切换回这个重构分支,继续我的优化工作

是不是很优雅呢 :), 心动不如行动~~

1
2
3
4
5
6
7
git init //初始化你的项目目录
git add . // 添加所有文件到暂存区
git commit -m "first commit" //提交暂存区文件
git checkout -b appdetails //新建appdetails分支
vim index.html // 在这个分支上修改
git commit -m "new version appdetails" // 提交修改到appdetails分支
git checkout -b ios_refactoring // 基于appdetails分支,再新建ios_refactoring分支