Skip to content

2016 #3

@playing

Description

@playing

2016

现在是2017年的1月1日21点13分. 距离我记忆中上一次写东西, 好像已经过了一年还多. 毕竟坚持还是一个很困难的事情, 尤其是坚持的事. 回头看看过去的一年, 2016年过得有一些艰难, 发生了许多的意料之外, 但是庆幸的是, 也有许多好的事情发生. 今天终于下定决心, 要写点什么记录一下过去的这一年.也算是对去年什么都没写的一些补偿吧.

工作

虽然现在说这一年什么都没写是因为工作太忙, 有一些自欺欺人. 但是不得不说, 过去的一年, 在工作上确实花了大量的时间和精力.

2016年应该算是我工作的第三年了, 古人用三来指代多, 还真的不是没有道理, 从三开始, 事情往往要发生一些不一样的变化. 一个入行三年的工程师, 已经不能再把自己当做一个新手了. 别人对你的期望, 已经从最开始的作为一个新人已经做得不错了.变成了能不能做得更快更好?, 对错误的忍耐程度也在下降. 同时, 自己对自己的要求, 也在不断的提高, 比如 够用了, 我尽力了 这种想法, 每次出现的时候, 我都要再多考虑一下, 是否真的要做这个结论?坦白的说, 这带来了更大的压力.

尤其是作为一个前端, 2016年堪称前端开发最混乱的一年. 新工具和框架层出不穷. 无数的新概念和新想法, 短短几天没有关注, 就有了许多从来没听过说的新鲜事. 从好的方面来看, 前端的领域空前的繁荣, 人们需要新的技术和想法来不断革新自己, 寻找更好更高效的工具和方法, 来帮助自己写更好的代码. 但是也有不好的方面, 大量的项目鱼龙混杂, 首先检索信息的成本被放大了, 其次学习和实践的成本也变得非常高, 导致出现了更多的讨论甚至争吵. 而这些讨论又把事情弄得更加复杂. 比如今天你问一个项目是选择React合适还是Vue.js合适,这个问题甚至比豆浆是喝咸豆浆还是甜豆浆和香菜到底好不好吃还要难回答的多.

在这样的背景之下, 回顾去年的工作, 虽然做的不算多, 但是至少认真做了一些对的事情.我一直认为, 一件事情, 想把它做烂, 方法和途径可能有无数种, 但是想要做好, 往往只有几个或者唯一的途径.所以如何把一件事情做对做好,是我去年思考的最多的问题.

Do the Right Things.

今年几乎所有的项目, 都是围绕小狐分期这个新产品在做. 这个产品应该算是我第一个全程参与并且从头开始的一个全新项目.所以这个机会也格外的宝贵.

通过去年的工程化实践和对业务以及技术的思考, 今年的这个项目开始, 我们就定下了我们的目标.

软件开发没有银弹!, 在现有的工具和资源下, 提高开发的效率和体验,更快的响应业务的变化.降低开发和变更的成本.

几乎后续所有的工作, 都是围绕着这一目标.总结下来大概也就几条

  • 基于JavaScript的同构应用
  • 敏捷开发
  • 工具和工具链

All JavaScript

首先是基于JavaScript的一整套应用设计.在我们对人员的水平, 业务的需求, 未来可能的变化, 之前的经验和痛点等一系列因素做了考量并且不断tradeoff之后,我们认为,All JavaScript几乎是对我们最好的方案.

于是我们选择了Node.js上的Web中间层, 以及React Native实现的Android&iOS客户端, 再加React实现的Mobile Web端.大量的同构设计,提高了代码适用性并且降低了代码的重用成本, 使得我们可以在少量人员的情况下, 发挥更大的战斗力.

同时同构的设计, 也减少了错误的重复出现和不同系统之间冲突情况.尤其是在客户端和Web层的设计实现上,根据以往的经验, 客户端和服务器经常因为例如数据格式,接口的设计,甚至接口数据内容传多传少等等问题上发生分歧. 不但要花时间在沟通上, 最后的解决方案往往还不够好. 或者是同样的问题, 每个平台出现一遍, 测试和修改也带来不小的麻烦.

现在我们做了一个统一的实现, 提供了更多的灵活性和能动性. 首先接口和逻辑都只需要保持一份, 减少了冗余和重复, 其次客户端实现起来不方便,性能不好的,数据需要处理的问题.就在Web中间层上处理.同时中间层还提供不同服务的封装,接口的mock和调试日志等等功能.这样真正的服务提供方,就可以专注在提供服务上, 而不需要浪费精力关注Web层的闲杂琐事.节省各自的时间和精力.既提升了后台服务的开发体验, 也解决了客户端开发的诸多问题.

举个小例子, 在一个字典集的问题上, 后台会根据配置生产一个.js文件, 提供给Web端直接引用, 来获取一些键值对的映射或者像省市区等固定配置信息.

但是我们在实现的时候遇到了问题,虽然客户端也可以直接引用并解析这个.js文件来获取里面的值,但是第一这个文件的大小已经达到了几百KB,对于移动端来说,有一些过于庞大,第二这个文件还有动态更新的需求,不可能每次都让客户端去下载这么大的文件.

如果改造成接口, 第一有对应的服务但是没有合适的接口, 第二目前的服务也不能满足根据业务的需求来变更且缓存结果并提供更新.第三,这个小功能,还需要后台的时间和精力来开发.诸如此类的零碎功能对于一个新产品来说, 往往要占用后台开发一定的时间.

但是我们只花了几十分钟, 就在中间层上实现了一个从服务获取后先选择需要的部分放入Redis, 再根据内容产生一条Hash, 客户端第一次获取之后, 之后每次启动都根据本地的Hash和远程对比, 有更新再重新获取的方式.在不需要对服务和后台做任何改动的情况下, 解决了这个小问题.

再回到客户端上, 解决了Web层的问题之后, 我们把目光投向了客户端的接口层和数据层.

在以往的开发中, 遇到过许多, Android和iOS的接口设计之间大相径庭,后台维护起来代价高昂.做起变更来还需要变更多套接口.或者就是同样的接口, 写出了两套不同的字段.

各种重复的接口和功能带来了重复的代码, 重复的代码又带来了更多的BUG和维护成本.于是时间和精力就被无尽的BUG所消耗,更没有时间去重构和优化.进入了一个恶性循环.

我们通过一个的统一的接口和数据层实现来解决了这一问题.

首先基于Fetch来支持所有的API访问, 这样在Android&iOS&Web 三个平台上, 都可以用一个模块来访问数据. 任何改动也会同步到三个平台上, 任何接口的增删修改都需要认真的设计和思考, 确保这一改动能够在不同平台上都正常工作.

然后在基于Redux来设计一个状态机, 将客户端中所有的状态和动作都纳入到Redux中来管理.再通过Redux去调用API.实现了数据层和接口与表现层的完全分离.整个产品被高度抽象成一个状态机.

具体的界面和操作只是Redux中state和action的映射.每一层都完全独立并且非常精简, 只包含抽象的逻辑. 方便单独进行开发与测试.

与具体表现层的连接, 则通过react-redux来注入到App的props中. 与表现层本身的state也进行了分离.实现了完全的共享和通用.只要具有JavaScript的运行环境, 就可以完整的实现所有的数据和业务逻辑. 大大降低了表现层的复杂度.

+--------------+    +--------------+    +--------------+  
|              |    |              |    |              |
|     iOS      |    |   Android    |    |     Web      |
|              |    |              |    |              |
+--------------+    +--------------+    +--------------+
      ^                    ^                   ^
      |                    |                   |
      --------+            |             +-----+
              |            |             |
          +---+------------+-------------+--+
          |                                 |
          |           Redux 状态机           |
          |                                 |
          +---------------------------------+
              ^                          ^
              |                          |
          +---+--------------------------+--+
          |                                 |
          |            Fetch API            |
          |                                 |
          +---------------------------------+
              ^                          ^
              |                          |
          +---+--------------------------+--+
          |                                 |
          |        Node.js Web 中间层        |
          |                                 |
          +---------------------------------+
              ^                         ^
              |     Lots of Service     |
              +-------------------------+

最后要解决的, 也是前端开发中最麻烦的部分, 就是表现层. 因为这一层, 没有通用解决方案, 必须逐个平台处理, 而且还涉及到兼容性, 还涉及到不同平台上设计与交互的差距, 可以说在这个问题上. 一直是前端开发的痛点, 也没有完美的解决方案.只能做取舍.

我们这次的方案选择了舍弃部分体验和细节, 来换取代码的通用和开发效率的提升.在UI设计的阶段, 我们就与设计师进行大量沟通, 在设计阶段就将各平台的设计与交互规范进行考量, 选择我们可以实现同时效果也可以接受的方案,针对通用组件,比如表单, 按钮等都设计一致的样式和交互.但是对与导航和对话框等组件, 就根据不同的平台来设计不同的样式, 但是保持逻辑的一致.

设计完毕之后, 就需要实现上的考虑, 我们首先解决的是CSS的方案, React Native中虽然也用的是CSS的写法, 但是其实和Web中大不一样, 首先React Native的全部布局都依赖FlexBox, 以竖直方向为默认主轴, 而且也没有class的概念, CSS也不会继承, 一些样式的写法也不完全按照CSS, 比如paddingHorizontal: 20其实等价于Web上的padding: 0 20.单位也不一致, 客户端是根据分辨率和屏幕大小运算过后得到一个类似于dp的单位, 而Web大部分还需要使用px.

在CSS这个问题上, 我们最终选择以客户端为主, 毕竟客户端的重要性和要求都比Web端要更高, 所以优先实现客户端是可行的.至于Web, 其实也有过CSS in JavaScript的例子, 所以我们通过对React Native的布局和CSS写法在Web上的完全模拟, 实现了一个Web版本的StyleSheet.create()方法, 先将CSS属性做一次转化, 单位也统一用rem来实现, 再通过Platform.select()来添加一些各自平台的特殊样式.使得90%的CSS得以被重用.

到了组件这个级别, 客户端基于React Native, 大部分的组件都可以直接实现, 但是在Web上, 许多React Native中提供的组件, 比如ListView, ScrollView, Toast, TabBar等, 都没有直接的实现, 而且Api和props的不同也很明显.

参考了社区中的方案之后, 我们选择了吸取其中的一些思想, 首先在客户端中, 直接调用React Native的API和组件, 在Web中, 我们实现了一个react-native-common的模块, 通过Webpack把React Native指向这个模块,在这个模块中, 来实现对应的组件和方法, 这样Web平台和客户端的代码, 就又可以继续保持一致.

.
├── __specs__
│   └── toast.spec.js
├── index.js
├── index.web.js
└── toast.style.js

一个典型的组件实现就像这样, 分别有客户端和Web的实现, 样式作为单独的一个文件抽离出来, 再加上一个单元测试. 一个组件就完成了.如果更复杂的组件, 我们会拆分的更细, 首先是公共的数据和方法定义在一个基础文件里, 其他平台各自继承并实现它,并处理自己平台的逻辑.

我们的客户端现在包含了超过40个这样的组件, 其中大约有80%的代码, 是工作在不同平台上的.这帮助我们用不到40,000行JavaScript代码,和两个前端开发, 就实现了三个平台客户端的开发与维护.

敏捷开发

其次是敏捷开发, 我一直认为对于小而美的团队来说, 敏捷开发可以有效的提高效率和质量.这尤其适合我们这个项目的前端团队.在我们有限的实践中, 也总结了一些比较重要且较为容易做到的敏捷开发原则.

尽早并且持续的集成和交付, 这几乎是我们最早意识到的一点.对于一个新的项目来说, 因为很多工作都是不可见且不容易量化的.比如很多技术调研和早期的编码.而且因为缺乏一个清晰的目标, 很容易在沟通和合作上浪费时间. 对于开发来说, 尤其是跟产品和测试的合作. 三方互相不清楚其他人的工作状况和产出, 我们的策略是, 从最小可用模型出发, 细分模块和功能, 用短和小的迭代, 来快速完成一个模块, 然后交付给产品和测试, 得到反馈之后, 又可以马上进行修改和优化, 缩短了反馈的路径和时间, 减少了问题的生命周期. 还能让大家都清晰, 目前所处的节点, 并关注当前的部分. 一系列小的模块之后, 再通过集成测试来回归. 最后完成一个大的功能点.

测试驱动的开发, 为了实现上面所说的持续集成和交付, 就不得不提到测试驱动的开发. 持续集成和交付的前提是, 你的每次重构或者改动, 都有明确的可观测的指标或者表现, 这样才能在后续的迭代中, 保证之前的部分没有问题. 这就需要开发人员, 对于模块和组件, 要有完整的单元测试, 并且最好是测试代码先行, 由测试来驱动开发, 这样在快速的迭代中, 可以随时验证这一个组件或者模块的正确性, 当对它进行修改和重构的时候, 如果不能通过之前的测试用例, 那么说明这部分代码的功能发生了变化, 就需要关注并修改.特别是在逻辑复杂的模块中, 分支和代码块覆盖的越多, 修改和维护起来也就越轻松.还可以放心的进行重构.

在我们的项目中, 对数据层和接口层都尽可能的添加了测试, 大部分组件也都有测试覆盖主要分支. 这些模块的测试覆盖率大约在50%, 整体测试覆盖率大约在30%.虽然离敏捷开发的目标还离得比较远, 但是已经能在我们重构的时候, 提供不少的保障.

不停的重构和优化, 有了测试和持续集成和交付的保障,重构和优化,就不在是一个痛苦的事情,软件开发中, 大家常说, 过早的优化是万恶之源, 但是事实上, 持续的小优化和重构, 从长远来看, 是具备很大意义的.不管是循环判断的重写, 还是变量名的优化, 甚至是代码格式的调整, 都是很有必要的. 这些小的改进, 积累起来, 才能长期的保持项目代码的优质和稳定. 指望一个集中的时间来进行重构在业务发展的过程中, 几乎是一个不可能的任务. 但是每天做一点小的重构, 可能并不是一件困难的事情. 长期下来, 其实也能达到不错的效果.

时刻关注代码中你觉得不爽的地方, 如果有时间, 就进行一些修改, 而不是抱着能用就行, 先赶需求的想法, 将这些问题永远留在项目里.勿以善小而不为, 哪怕是一个空格或者改一个变量名, 也算是一次重构, 请坚持下去.

虽然实际工作中, 往往有各种各样的突发状况和事件, 也有做不完的新需求和改不完的BUG.很多时候很难坚持这几条原则. 这个时候, 就需要合理的运用工具, 通过工具的辅助, 用更少的时间来完成更多的工作. 才能有剩下的时间来思考如何做的更好.

更好的工具

最后说到工具相关, 很多人对工具的使用, 存在一些误区. 要么是过度依赖和滥用工具, 要么就是排斥或者抗拒使用工具. 其实正确的方法, 是根据具体业务和团队的需求, 谨慎的选择和引入工具, 持续评估工具带来的效果, 并不断改进.

刚工作的时候, 我还属于比较激进的类型, 试图用一系列All in One的工具链来实现所有的需求. 但是最后的实践结果表明, 很多时候, 并不需要一个大而全的复杂工具链, 而合理挑选一些简单的工具, 将他们组合使用, 就像Liunx一样, 用管道符把许多基础命令连接起来, 也能达到想要的目的, 同时更加灵活.于是今年我对现有的工具进行了更好的研究和改造. 试图发挥他们更多的功效.

首先是GitLab, 在上一个项目中, 我用GitLab的Hooks实现了一个简单的自动编译和打包系统, 每次push之后, 在服务器上用gulp来进行一系列构建, 然后进行部署, 想解决前端的预处理器和工作流的问题. 但是实践中, 一个是需要多写很多shell脚本, 一个就是对于多分枝和多工程的处理需要很多额外的工作. 本质上也只是一个服务器上运行的打包器, 和Git仓库本身的关系也比较弱.

在这一次的项目中, 首先就针对Git的用法, 做了大量的优化, 针对这个项目的特殊需求, 比如要将客户端的Binary部分和JavaScript部分分开, 因为JavaScript部分要提供基于CodePush的动态更新, 但是Binary部分需要跟着React Native的版本升级, 同时两条线还要保持对应的所有依赖, 我们选择了将版本基于Git Branch的开发流程, 所有的代码从develop出发, 在feature/xxx上开发和测试, 并最终rebase之后合并回develop, 每次新版本都从develop生成一个类似release/1.2.0的新分支提供给测试, 最终线上再对应一个production/1.2.0版本. 之后Binary部分不做改动, 只cherry-pickJavaScript部分的补丁给已经发出去的版本, 这样既保留了每个版本的代码随时更新, 又保证了主分枝可以不停的前进. 新添加的代码, 如果不涉及Binary, 就可以同步更新至现有的版本, 降低了上线的周期和成本.

.
├── develop
├── feature
│   └── new
├── hotfix
│   └── bugfix
├── releaase
│   ├── 1.2.0
│   ├── 1.3.0
│   └── 2.0.0
├── production
│   ├── 1.2.0
│   └── 1.3.0

之后我们又将大量重复性的工作写成shell脚本添加进工程, 并通过npm script调用.从开发测试到打包部署, 几乎所有的流程, 都只需要短短几个命令就可以完成.这还远远不够, 我们还需要一个平台来把统一执行这些命令, 在评估了许多工具之后, 选择了GitLab CI. 相比于其他CI, GitLab CI与GitLab的结合, 更为紧密, 同时也提供多种Runner的实现, 搭建起来也比较简单. 最终我们使用了一台Mac Mini加一台Linux Server来作为CI的Runner, 其中OS X系统负责客户端相关的Build阶段, 其他Lint和Test以及Deploy则由两台机器分担.每一次commit, 都会根据自己的branch或者tag经过不同的阶段, 并根据需求完成打包上传或者部署.后续还添加了邮件通知, 代码质量报告和测试报告的生成和预览等等功能.

至于其他小工具, 都像积木一样, 搭在CI这块板子上, 加入我们的工作流, 不断的帮助我们提升效率和质量, 减少问题.像ESLint+Airbnb Style(代码静态分析检查), commitizen(Git commit message规范), jsinspect(代码重复检查), 都有很好的使用效果.

总结

总体来说, 这一年主要在React Native和React这一方面研究和学习, 也算是深入了一个领域. 同时利用新技术对已有的开发带来了许多新的改变. 希望新的一年, 能够继续坚持下去, 保持学习和钻研的动力, 保持不断的思考和前进!

♥ Made With Love by Playing

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions