以下纯粹是个人的一点见解,欢迎讨论与指正。🍻🍻

入职后引发的思考

首先感谢梦梦、刘伟、刘阳、伟松还有其他人这段时间对我的帮助,特别是我的导师梦梦,的确帮助了我很多。😄😄

个人沟通能力有待持续提升

可能由于在校时我身边没有同伴学前端吧,习惯了独自思考,合作交流的往往也都是熟悉的朋友,大家能很快的get到彼此的意思,所以有时即便表述不清也没事。
但是在公司里是多人同时协作,这就要求我要有更高的沟通技巧来提升沟通效率。我正在有意识的提升。

我们的上线流程是否可以优化下?

几乎每逢上线,我们可能都得11-1点才能搞完。。

业务引发的思考

有时业务理解成本可能大于技术实现成本

尤指新人来到公司后接到的业务性比较强的需求,比如跨部门/跨平台合作时,这就需要我们及时沟通,询问相关人员;
可怕的是,如果了解这个业务的所有人都不在这里工作了,那后来人如何【快速上手并及时沟通】呢?-> 就目前我们的体系而言,个人认为,不能。飞书文档有些杂乱,太过注重信息透明而导致低内聚力。
所以我在想,我们可不可以建立一个 易查询到关键内容的,具体到甬道的、组织结构清晰明确的资料系统?

前后端缺少一个对接口的共同的强约束

虽然前端用上TS,也能实现接口的强约束,但毕竟最后的运行时仍是JS(弱类型->隐式转换->安全隐患)
所以仍然无法避免混乱的 undefined、null、’’、[] 此类数据,导致前后端可能都多增加了对边界case的额外维护
一个文件多10来个处理,100个文件呢?积少成多…. 而且不容易维护与后续修改代码

而且前后端对接口的定义并没有强打通,并不是真正意义上的一致性

  1. 前端说:我现在这么定义的(后来某天,某位同学,不小心改了下)
  2. 后端说:我是这么定义的(也可能,后来不知怎么的被修改了)
  3. 佛说:你俩这属于约定俗成,并不具有强约束力,而且越往后越有可能出错。
  4. 我说:那 佛祖 您能不能提供个平台,供前后端拉取统一的接口定义,您来维护这些接口。我们都听您的!
  5. 佛说:啊这,。。貌似成本有点大,先不说了,你们先这么慢慢玩,我考虑考虑

所以,我在想能不能有个服务器专门存储对各个接口的定义/约束,前后端都从这个接口服务器去拉取接口定义?但成本貌似有些高?所以想跟大家讨论讨论,有没有什么建议。

越小的东西往往越难做

有时可能95%的功能花了3天,但这剩下的5%的功能却要花2天。而且这5%的功能,要求我们对技术的掌握要更深入、更广泛。

自测需要逆向思维

先完成正常功能与流程。再想想什么是不应该的,那我就测什么,看看自己是怎么处理的。
测试时应该要宏观,因为也许我们一处小小的改动,但是配合历史包袱就会引发新的bug,又或许那个历史bug本身就存在,而且干扰了我们现在新的需求。。。

代码层面上的思考

首先,学过编译原理的同学可能都比较能get到:代码量更多不代表执行时间就更多。客观上唯一确定的是这样增加了文件体积。

尽量多的:

尽可能地考虑所有的case,并为每个case容错

我们能更好的使用上层工具,功不可没的一点是在于:各个工具良好且健壮的错误处理系统。

开发也一样,多数时间都是为了处理各种case,不同case不同处理。
所以我们最初写代码时,尽可能地考虑周全,并提供具体的打印日志/提示,便于定位错误的代码位置。
比如:这个东西我请求失败了,要给用户增加一个合理的提示并打印错误信息。失败后的逻辑,记得相应调整。

尽可能地高内聚低耦合

横切关注点,尽可能的把当前操作放到一个“独立的容器”内,尽最大可能的减少与其他“容器”交流。
封装->状态机->流程控制->逻辑处理(文章最后会有一个具体代码案例)

公共方法尽可能地设置为纯函数: y = f(x);

个人感觉,其实这也是 高内聚低耦合 的一个体现。

  1. 尽量用TS定义出函数参数的类型以及返回值类型
  2. 函数内不要更改入参与外部变量。
  3. 保持稳定性:同样的入参,保证同样的返回值
  4. 无副作用(我觉得其实不是真正无副作用,而是要让副作用变得可控,把副作用单独切出,隔离起来管控)

好处就是:

  • 更容易进行测试,结果只依赖输入,测试时可以确保输出稳定
  • 更容易维护和重构,我们可以写出质量更高的代码
  • 更容易调用,我们不用担心函数会有什么副作用(副作用可控即可—>>原子性/隔离性)
  • 结果可以缓存,因为相同的输入总是会得到相同的输出

尽量少的:

尽量少的复用业务组件

业务上往往都有细微的差别,而导致实现大不相同,所以尽量少的复用业务组件。
最重要的是,后续的人可能因为业务而对该组件做更改,很可能改动一点点就 “一发而动全身”,导致不可预估的bug。

尽量避免关键数据无效时仍然渲染组件

当关键数据有值时才应当渲染组件, 否则组件内很容易出现, xxxx?.xxx?.xx

尽量少的使用try-catch

经实测,捕获错误非常耗费性能,与正常时相差能上百倍,😱😱。具体底层跟编译原理等有关。后续写掘金的时候,顺带会详解为何try-catch如此耗费性能。

Talk is cheap, show me the code

讲了这么多,综合上述的思考,来个实际案例吧。

具有复杂依赖关系和复用关系的业务场景,考虑后续维护成本,该如何书写代码?

既然已经决定要复用,。。
我觉得此时设计上要重点考虑的是 「后续的维护成本」,谁也不知道这块代码后续会被怎样增删改。
如果用各种标识代表各种情况,用if-else还稍微好点,但是不断的各种互斥逻辑与零散的功能维护,会使得我们身心疲惫。很容易形成 “options api”式代码,低内聚了。
如何写成 “composition api” 式代码是我们要思考的主要方向。 —>> 还是要 「横切关注点」。

假设场景:
个人零碎思考分享 - 图1
现在有两个页面 pageHome(本平台使用) 和 pageManager(提供给另外一个平台使用),复用了同一个组件(因为这个组件比较复杂),但是有一些区别,比如在几个按钮交互后,背后的业务逻辑不同。

pageHome 下有个取消按钮,需要先校验,再保存,再提交(层层递进,后者逻辑都是 「强依赖前者顺利完成」 才执行),每一步的成功与失败做我们自己平台的不同的处理。

而 pageManager 下的提交审批按钮,中间不需要保存。而且每一步成功或失败,我们平台要通知另外一个平台做不同的处理。

(而且万一有好几个系统都要接入我们的这个系统呢?而且都需要有细微差异的业务逻辑呢?)

将每个关注点抽离出来放到一个状态盒里

编译原理:NFA->DFA(确定的有限状态自动机) -> 业务上 -> 保证了关注点的原子性与隔离性 -> 易于阅读维护
DFA:”给我下个状态的条件,剩下的活儿,哥都帮你干了!你快去玩儿!”。😄😄

个人零碎思考分享 - 图2

结合 DFA 、函数式编程 以及 业务逻辑 等有了如下代码产物:

  1. //抽离到其他文件. 确定不需要更改时可以选择冻结该配置对象
  2. const CONFIG = {
  3. pageHome: {
  4. name: 'pageHome',
  5. A: {
  6. //这里也可以分我们平台的callback还是别的平台的,注册不同函数并在外面适当调用即可,此处不再赘述
  7. cbAfterSuccess: () => {
  8. console.log('我们的平台 cbAfterSuccess pageHome A succeed',);
  9. },
  10. cbAfterError: () => {
  11. console.log('我们的平台 cbAfterError pageHome A failed',);
  12. },
  13. },
  14. B: {
  15. cbAfterSuccess: () => {
  16. console.log('我们的平台 cbAfterSuccess pageHome B succeed',);
  17. },
  18. cbAfterError: () => {
  19. console.log('我们的平台 cbAfterError pageHome B failed',);
  20. },
  21. },
  22. C: {
  23. cbAfterSuccess: () => {
  24. console.log('我们的平台 cbAfterSuccess pageHome C succeed',);
  25. },
  26. cbAfterError: () => {
  27. console.log('我们的平台 cbAfterError pageHome C failed',);
  28. },
  29. },
  30. },
  31. pageManager: {
  32. name: 'pageManager',
  33. A: {
  34. cbAfterSuccess: () => {
  35. console.log('通知别的平台 cbAfterSuccess pageManager A succeed',);
  36. },
  37. cbAfterError: () => {
  38. console.log('通知别的平台 cbAfterError pageManager A failed',);
  39. },
  40. },
  41. },
  42. }
  43. //推向下一个状态的条件的定义: 要操作哪个模块?什么操作?荷载数据是?
  44. interface ICurState {
  45. moduleName: string
  46. operateType: string
  47. payload?: any
  48. }
  49. //三个公共方法, 纯函数
  50. const getNextState = (moduleName: string, operateType: string, payload: any): ICurState => ({
  51. moduleName,
  52. operateType,
  53. payload,
  54. });
  55. const getStopState = (): ICurState => ({ moduleName: '', operateType: '' });
  56. const getSpecObject = (curState: ICurState) => {
  57. const { moduleName, operateType, } = curState;
  58. return (CONFIG[moduleName] && typeof CONFIG[moduleName][operateType] === 'object') ? CONFIG[moduleName][operateType] : {};
  59. }
  60. //此处的genPromise只是模拟平时的请求而已,可以换成任意业务逻辑
  61. const genPromise = (type: string) => {
  62. return new Promise((resolve, reject) => {
  63. setTimeout(() => {
  64. Math.random() <= 0.6 ? resolve('resolve ' + type) : reject('reject' + type);
  65. }, 200);
  66. })
  67. }
  68. //单独处理pageHome触发A后的逻辑
  69. const handlePageHomeA = async (curState: ICurState): Promise<ICurState> => {
  70. const cbOfObject = getSpecObject(curState);
  71. const newState: ICurState = await genPromise('A').then(data => {
  72. console.log('afterResolve A', data)
  73. //此处处理成功后的逻辑, 然后再辗转下一个状态
  74. cbOfObject.cbAfterSuccess && cbOfObject.cbAfterSuccess();
  75. let newState = getNextState('', '', {});
  76. switch (curState.payload.from) {//如果想尽可能多的复用,就可以这样
  77. case 'handlePageManagerA':
  78. //也可以在这里封装另外一个函数单独进行逻辑处理
  79. newState = getNextState('pageHome', 'C', { data });
  80. break;
  81. default:
  82. newState = getNextState('pageHome', 'B', { data });
  83. break;
  84. }
  85. return newState;
  86. }, error => {
  87. //message.error('独特的报错提示A');
  88. //打印原始日志
  89. console.log('Error A: ', error);
  90. //异常情况下,接着处理单独的业务逻辑
  91. cbOfObject.cbAfterError && cbOfObject.cbAfterError();
  92. //然后再终止状态
  93. return getStopState();
  94. });
  95. return newState;
  96. }
  97. const handlePageHomeB = async (curState: ICurState): Promise<ICurState> => {
  98. const cbOfObject = getSpecObject(curState);
  99. const newState: ICurState = await genPromise('B').then(data => {
  100. console.log('afterResolve B', data)
  101. cbOfObject.cbAfterSuccess && cbOfObject.cbAfterSuccess();
  102. return getNextState('pageHome', 'C', { data });
  103. }, error => {
  104. console.log('Error B: ', error);
  105. cbOfObject.cbAfterError && cbOfObject.cbAfterError();
  106. return getStopState();
  107. });
  108. return newState;
  109. }
  110. const handlePageHomeC = async (curState: ICurState): Promise<ICurState> => {
  111. const cbOfObject = getSpecObject(curState);
  112. const newState: ICurState = await genPromise('C').then(data => {
  113. console.log('afterResolve C', data);
  114. cbOfObject.cbAfterSuccess && cbOfObject.cbAfterSuccess();
  115. return getStopState();
  116. }, error => {
  117. console.log('Error C: ', error);
  118. cbOfObject.cbAfterError && cbOfObject.cbAfterError();
  119. return getStopState();
  120. });
  121. return newState;
  122. }
  123. const handlePageManagerA = async (curState: ICurState): Promise<ICurState> => {
  124. const cbOfObject = getSpecObject(curState);
  125. const newState: ICurState = await new Promise((res) => {
  126. res('resolve handlePageManagerA');
  127. }).then(data => {
  128. console.log('afterResolve handlePageManager A', data);
  129. cbOfObject.cbAfterSuccess && cbOfObject.cbAfterSuccess();
  130. return getNextState('pageHome', 'A', { data, from: 'handlePageManagerA', });
  131. }, error => {
  132. console.log('Error B: ', error);
  133. cbOfObject.cbAfterError && cbOfObject.cbAfterError();
  134. return getStopState();
  135. });
  136. return newState;
  137. }
  138. //逻辑处理统一入口, DFA管理员
  139. const unifyHandleOperate = async (curState: ICurState) => {
  140. let { moduleName, operateType, payload } = curState
  141. if (!(moduleName && operateType)) {
  142. return;
  143. }
  144. let type = `${moduleName}-${operateType}`;
  145. let nextState: ICurState = curState
  146. while (type) {
  147. switch (type) {
  148. case 'pageHome-A': //这种字符串也可以用常量存起来
  149. nextState = await handlePageHomeA(nextState);//副作用要返回新的状态
  150. break;
  151. case 'pageHome-B':
  152. nextState = await handlePageHomeB(nextState);
  153. break;
  154. case 'pageHome-C':
  155. nextState = await handlePageHomeC(nextState);
  156. break;
  157. case 'pageManager-A':
  158. nextState = await handlePageManagerA(nextState);
  159. break;
  160. default:
  161. nextState = await getStopState();
  162. console.log('流程结束了')
  163. return;
  164. }
  165. moduleName = nextState.moduleName;
  166. type = `${moduleName}-${nextState.operateType}`;
  167. payload = nextState.payload;
  168. }
  169. }
  170. //开启自动机
  171. unifyHandleOperate({
  172. moduleName: 'pageHome',
  173. operateType: 'A',
  174. payload: {
  175. data: 'whatever',
  176. }
  177. });
  178. // unifyHandleOperate({
  179. // moduleName: 'pageManager',
  180. // operateType: 'A',
  181. // payload: {
  182. // data: [1, 2, 3],
  183. // }
  184. // });

好处就是完全 “拍平” 了,并且易读易维护易扩展。<<— 因为维持了原子性与隔离性;

缺点就是函数会逐渐增多,CONFIG逐渐庞大(不过也可以配置到别的文件再引入,如果需要上下文相关的操作,进行拦截覆盖配置即可)等其他缺点。