TL;DR: 如果没有把握解决混合应用的性能问题,请尽量使用 native 方式开发。
在为期四周的开发比赛(培训、比赛,形式无所谓)结束后,自然应该做个总结,不然这段时间的废寝忘食不就白费了?
两句话介绍项目
我们小组共有15个成员,包括产品、设计、开发、QA 等所有该有的角色。经过1周(微妙地加粗:整个开发周期只有4周好吗)的需求讨论之后,我们的结论是做一个以找室友为核心,周边功能有找房源、社交等,这样一个app。
管理方面
需求
现象:在做需求的那周,发生了许多产品与开发互相撕X的场景,在双方互相让步后,处理地还挺优雅的。如果能在激烈讨论后定下需求就万事大吉了,问题是当开发进行了几天之后,觉得功能点还是太繁杂,于是又只能砍,有一些开发成果就直接废弃了。
其实需求是很难确定的,这点相信各位都能体会到。(道理我都懂,但是……)
特别是对于工作经验比较缺乏的新人来说,他们很喜欢天马行空地幻想,把这个功能加进去,那个不错,也要,还有还有……是因为心太大了吗?还是对开发同学的能力过于高估?那我作为开发,就先说自己的不是好了。只要水平到位,还不是有什么需求就做什么?为了防止抨击,这句话的前提是,不考虑需求是否合理。就是说,当开发人员抱怨产品的不切实际时,也应该反思一下自己的水平为什么不足以支撑这个需求。所以开发人员更多时候应该去提高自己的技术水平,做好本职工作。
另一方面对于PM,其实他们有两方面因素需要权衡。一个是产品定位,什么功能是非要不可的,失去它,就无法体现出产品核心了。第二是开发成本,如果你的开发团队水平够的话,当然可以适当增加一些需求,以提高竞争力。然而,我们几个开发大多数都是没有工作经验的,都是应届本科生或者硕士生。
那摆在PM面前的可用资源就是这些了,他必须得认清现实,有几斤面粉就做多少馒头。一个年轻的PM往往很难估计每个成员的能力,更别说我们这15个人才刚认识不久,所以为什么需求那么不稳定?其实也就很容易理解了。
说到理解,产品和开发之间要怎么互相理解?产品总会认为开发搞了一天都没什么成果,而开发老是觉得产品什么都不懂还在那瞎XX。这个问题用经验和沟通手段就可以解决了。经验的意思是说,工作经验(不是混几年)越多,这方面的问题就越少,因为大家都是过来人,很多事情都心照不宣。但经验在我们团队中貌似行不通,至于沟通手段,就是说怎么向对方解释了。如果产品一上来就说,需要增加一个“摇一摇”功能,相信绝大部分开发的第一反应都是拒绝的。而如果开发跟产品说,cookie 中不能获取 session id,所以自动登录没法做,相信产品就会想,别人可以做,你怎么做不了。
这样对双方都是很伤的。作为PM,他可以先问嘛,“图片裁剪”能不能做?大概需要多久?而开发的话,永远把提高技术水平放首位,然后有空再关注一些产品方面的东西。
时间
如何提高时间利用率?这段时间的集中式开发过程中,我发现一个很有趣的现象,就是很多人来到公司后,需要花大量的时间才能投入状态。比如说,9点到座位上,花10分钟吹空调,再打开邮件、音乐、微博、知乎等等,花去半个小时,周围的人陆陆续续到了之后,再聊上半个小时,路上怎么赌了,今天天气怎么样。真当打开IDE后,需要花半个小时回想昨天做了什么,从哪里着手今天的任务。更可悲的是,正要写代码的时候,马上拉去开晨会,回来又无所适从了。
那解决办法是什么?专注。程序员这个职业很特殊,不像跑步,跑20分钟,休息10分钟,再来一次,减肥效果更好。一旦开发人员的思绪被打断,再要投入进去就难了,编程20分钟,休息10分钟,再来一次,啥也没有。
这是个人的时间,那团队时间就更复杂了。本来对于新人来说就没什么时间概念,而新人又偏偏过于自信,觉得这个很简单,那个很简单,两三天肯定做得完吧!经过很多次挫败和反思之后,我得出的结论就是,不要低估任何需求的难度,多估一点时间总不会错的。
代码审查
在最后技术答辩时,有评委问我在开发过程中有没有做 code review,我老实说没有。但很庆幸他问到了这一点,说明公司还是比较注重代码规范的。
为什么代码审查很重要?工作中,我们基本上是在一个现有的代码库中添加或修改代码,而不是从头开始编写全新的代码。如果要求改动的代码必须符合原有的规范,那么请别人做审查就很有必要了。这是第一,第二是可以降低错误率。当局者迷,旁观者清,别人多多少少可以看出一点代码中潜在的问题,这些问题往往会被作者的自信所掩盖。第三点可以从另一方面来讲,就是给别人做审查的时候(没错,并不是只有资深员工给新员工做审查,反过来也可以),可以学习到他们的编码技巧。无论是被动还是主动,对个人对团队,都是有帮助的。
那为什么我们没有做代码审查呢?一是时间不允许,二是,好吧,能把功能完成就可以了,既然是短期项目,不需要偿还技术债务,就没必要把规范进行到底了。
技术方面
精简再精简,非技术部分写得还是有点多。
我在团队中担任的是前端开发这个角色,所以技术方面就只能总结前端相关的内容了。
前端选型
一开始说要做 app 的时候,我其实是拒绝的。考虑到我们所有 4 个前端都只会 web,很难去估量做 app 的复杂度,以及遇到问题是不是一定能解决,不能解决怎么办。但本着体验新事物的态度(被产品知道要气死了)就答应了。
那混合型应用的技术有什么呢?无非就是用 cordova 把一个单页应用打包成 apk(android),或者用 React Native 做 ios 平台的应用。
最后我们前端用的是 cordova + ionic,后台就万年 spring mvc 啦。ionic 它是一个移动端 css 框架兼 JS 框架。其中 JS 具体是指它内置了丰富的 angular directive,所以开发效率会快很多,毕竟不用自己写侧边栏布局、图片轮播、对话框等组件了。有兴趣的同学可以了解下,官网。
后来评委也问到为什么你们明知道 angular 有那么多性能方面的问题,却还是要用它?我实在是想不到更合适的技术了。什么是“合适”?抛开具体情况光谈技术,那永远没有最佳答案。而我们的具体情况是,大家都是 web 前端,没用过 react,不熟悉 angular,平时会点 jQuery,有些 css 属性还想不起来了,需要查手册。所以如果各位看客大人有更好的建议,在下是真心求教。
虽然说前端的技术多,但很多都是大同小异,比如 underscore 和 lodash,说不定过段时间就合并了。
最后我想提一个疑惑就是,为什么在浏览器中调试时,这页面跳转、列表的下拉都流畅得飞起,怎么装到手机上就很卡了呢?难道真的要把一切都怪罪于 angular 吗?
移动端布局
对于没有接触过的领域就应该大胆尝试,学习大概就是这样一个过程吧。
移动端的特点就是屏幕大小极其碎片化,但兼容性还过得去;网速慢、有电量限制。
对于屏幕适配,可以通过 media query 做成响应式的,难点在于设计师的脑洞了,他如果非要做成 pintrest 这样的瀑布式列表,我想你的关注点应该在于怎么实现而不是怎么适配其他的屏了吧。
至于网速,css 和 js 都打包在 app 的安装包里了,所以网速不会影响这两个资源的加载,它的瓶颈在于图片,这个在后面小节有提。
那怎么降低电耗呢?就只能在视觉上妥协了。比如说,两个页面之间的转场动画,往往有滑动的效果,它可能是 js 强行计算出来的,也可能是 css 中 animation 和 transition 的功劳(这个可能性大点),靠 GPU 计算每帧的画面,也算流畅。但计算都是耗电的,所以权衡一下,哪些特效可以不用,哪些资源可以不需要加载(图片和 DOM)。
自动登录与跨域
这个问题在开发过程中纠结了很久,完全可以另写一篇文章来详说。
首先,以传统 web 开发的思维来考虑,每次发送http请求时,服务端会判断这是不是已有的 session,以 tomcat 为例,它根据 cookie 中是否有 JSESSIONID 来判断,如果没有这个属性,就新建一个,在返回的响应中,写到 Set-Cookie 中,浏览器会自动把这个值写到 cookie 里的,对前端开发者来说完全透明,甚至后台开发人员也不需要知道细节,只要会调用 getSession() 类似的方法就可以了。
但是,当一切碰到跨域的时候,问题的性质就变了。显然,我们的 app 和服务端接口不是同一个域,至少在浏览器中调试页面时不是。所以,服务端必须在响应头中加上
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: OPTIONS, POST, GET
然而,跨域时,虽然说浏览器可以取得 JSESSIONID,但它无法传回服务端,这就导致服务端认为每次请求都是一个新的 session 。当然有办法可以做到传回 JSESSIONID,就是在响应头中加上
Access-Control-Allow-Credential: true
问题是,一旦这么设置,Access-Control-Allow-Origin 就不能是通配符。
没关系,我们几个开发在本地调试时都用一个端口部署前端页面好了,这样后台设置起来也方便,比如统一设置成
Access-Control-Allow-Origin: http://localhost:3000/
当一切似乎在和平中进展时,冒出一个新的需求,叫自动登录。
这和跨域有什么关系呢?我们原以为只需要把 JSESSIONID 存到 localStorage 中,如果它没过期,再放到 cookie 中就行了,但是 AJAX 是无法取得 Set-Cookie 响应头中的信息的,请见标准。
也就是说,这是死胡同。基于 cookie 的验证方式在我们的场景中是行不通的。
随后,我们使用了 JWT 的方式,即忘掉 session 的概念,当登录接口成功调用后,后台计算一个用户 token,然后前端把该 token 存到 localStorage 中,然后每次请求服务端接口时,都把这个 token 传过去。注销后就从 localStorage 中删除 token 。
那么能否自动登录就是判断 localStorage 中是否有 token 了。
为了不改变原有的前端调用方式,我们通过 angular 的 httpProvider 注入了一个拦截器,对于每个请求,都把 token 写到 request header 的 Authorization 属性中。
app.factory('authInterceptor', function ($rootScope, $q, $window) {
return {
request: function (config) {
config.headers = config.headers || {};
if ($window.localStorage.access_token) {
// config.headers.Authorization = 'Bearer ' + $window.localStorage.access_token;
config.headers.Authorization = $window.localStorage.access_token;
}
return config;
},
responseError: function (response) {
// console.log('intercept error response', response.status);
if (response.status === 401 || response.status === 403) {
// 用户无权限时跳转到登录页
$rootScope.go('/login');
}
return $q.reject(response);
}
};
});
大型列表显示和图片优化
必须明确一点,angular 不适合做大型数据的展示,因为 ng-repeat 的性能实在太差,还因为它的脏检查。必要时就自己写 directive 。
想象一个图片 gallery,当元素个数达到一定程度时,在浏览器中显示都难免会出现卡顿的现象,更别说在移动端了。
图片方面有哪些可以优化?简单的做法是压缩图片,占的内存少了,自然就好点。复杂一点就做响应式,什么意思?同样一个列表,在移动端显示 100 100 的图,在浏览器中显示 300 300 的图。怎么做?用 img 元素的 srcset 和 sizes 属性来指定图片集和判断条件,这方面的内容也可以花一篇文章来说明,先给个链接预热一下。
当图片的大小可以控制时,现在就来看,就算是 100 * 100 的图,如果显示 500 张,那还是大啊,内存占的多,还是会卡。那么在大型的列表中,对这些元素又该如何显示呢?简单(相对而言,最简单就是什么都不做嘛)的做法就是,把视窗之外的元素设置成
visibility: hidden;
尽管这个元素还占着位置,但貌似内存会消耗得少一点,不妨一试。不过要注意的是,怎么去判断元素已经在视窗之外了,这是个难点。
另一个做法,我没有试过,就是把视窗之外的图片节点删除。由于图片是缓存的,所以当重新添加图片节点是,只要地址没变,就不会产生服务端请求,这点不用担心,关键是还原正确的图片,以及添加和删除节点会导致 reflow,这个代价真的值得吗?
再小结
其实在这个项目中,我的观点都是基于“15个新人在4周内完成一个不会上线的小型项目”而出发的。这意味着什么?我们必须把重点放在产品的完整性上,而不是规范和维护。所以很多细节都没有做好,最关键的就是性能优化,移动端在性能方面的要求比浏览器端高多了。另一方面,评委不会这样想,评委就是喜欢扯负载均衡,分布式数据库等,当然可以理解。我想说的是两者的标准不同,这可能是我自己犯的错,因为题目或者赢的途径就是走高大上、走情怀。
或许是技术选型上的失误,但至少我体验到了它的样子。
不入红尘焉能看破红尘,不曾拿起谈何放下。(仙剑一)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。