Skip to content

前端工程化系列——代码质量。系统分析如何编写高质量的代码

License

Notifications You must be signed in to change notification settings

jacksplwxy/Good-Code

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 

Repository files navigation

*软件项目开发管理流程:
·瀑布模型:作为在20世纪70年代、80年代盛极一时的软件开发模型,瀑布模型通过制定计划、需求分析、软件设计、程序编写、软件测试、运行维护等6个流程将整个软件生命周期衔接起来。这6个流程有着严格的先后次序之分,只有当前面的流程结束之后,下一个流程才能开始运转。这种自上而下的流程像极了瀑布的下落,因此得名瀑布模型
·敏捷开发:敏捷开发采用“迭代开发”,将软件项目需求分成多个迭代,且每个迭代成果在完成开发、测试、反馈等环节后都可以进行交付。也就是说,在将软件交付到客户手中之前,开发过程中的任何经过测试的子项目都能够独立运行。敏捷开发强调:个体和互动高于流程和工具、工作的软件高于详尽的文档、客户合作高于合同谈判、响应变化高于遵循计划
·DevOps:敏捷开发极大地提高了软件开发的速度,但它注重的是软件的开发阶段,并未兼顾到运维阶段。在开发人员与运维人员进行交接的时候,并没有体现出敏捷的价值、原则,因此开发与运维之间仍缺乏一些必要的协作效率。这时DevOps就应运而生,DevOps促进开发、运维、测试之间的高效协同,从而做到用持续软件交付来修复并能够更快地解决问题
·文档:
  -- 《软件开发流变史:从瀑布开发到敏捷开发再到DevOps》:https://zhuanlan.zhihu.com/p/222117833


*高质量的前端软件和高质量的前端代码
·高质量软件:
  -- 软件bug少:
     -- 在满足软件所有功能需求(功能性、性能性、安全性、易用性、美观性、兼容性等)的基础上,bug尽量少
  -- 软件够健壮:
     -- 在极端情况下软件也有相应处理机制,例如断网、设备性能低、极端尺寸下屏幕UI适配、浏览器的兼容性、数据异常等
     -- 软件不够健壮本质上也就是bug,但这里强调的在极端情况下的bug,而非常规使用下的bug
  -- 软件性能高:
     -- 在满足功能和性能指标基础上,软件性能尽可能高
  -- 软件开发周期短:
     -- 软件能尽快满足客户需求
     -- 软件能够根据客户需求变化快速迭代
  -- 软件成本低
     -- 开发成本低
     -- 维护成本低
·高质量代码:
  -- 代码bug少
  -- 代码够健壮
  -- 代码易测试
  -- 代码易阅读
  -- 代码易扩展
  -- 代码性能高
·高质量代码与高质量软件的关系:
  -- 编写高质量的代码的目的是为了提高软件的质量
  -- 高质量的代码能够使产品bug更少、更稳定、性能更高、迭代周期更短、维护成本更低
  -- 编写高质量的代码通常能使软件质量提高,但是也可能使软件的部分方面的质量降低,例如提高软件可读性需要对代码进行code review从而延长软件开发周期。所以需要提高软件质量要根据实际情况进行权衡哪些方面可以忽略一定的代码质量


*代码bug少:
·软件开发中,对代码的基本要求是满足所有功能(功能性、性能性、安全性、易用性、美观性、兼容性等)并且尽量没bug
·代码bug少对开发者的要求是能够准确理解软件需求并能完整实现需求,并且做出足够多的自测以保证代码bug尽量少


*代码够健壮:
·使代码够健壮得关键是:对非常规情形进行了针对性处理
·对代码健壮性的最基本要求是:遇到JS异常不崩溃
·常见的JS异常类型:
  -- SyntaxError:语法错误
     语法错误也称为解析错误,语法错误在任何编程语言中都是最常见的错误类型,表示不符合编程语言的语法规范。js编译器的代词法分析阶段将字符流转换为记号流token,语法分析阶段会将记号流生成抽象语法树AST(详情见: https://github.com/jacksplwxy/JavaScript-compiler )。在这两个阶段,如果 Javascript引擎发现了预期之外/无法抓换的 token,或者 token 顺序和预期不一致时,就会抛出 SyntaxError。此类异常发生在JavaScript 解析/编译时,此类异常一旦发生,导致整个js文件都无法执行,而其他异常发生在代码运行时,这一类的错误会导致在错误出现的那一行之后的代码无法执行,但在那一行之前的代码不会受到影响。
     -- 对象中属性之间无逗号
     -- 多了大括号或者少了大括号等
     -- JSON.parse(function(){})
  -- TypeError:类型错误
     运行时最常见的异常,表示变量或参数不是预期类型。
     TypeError和ReferenceError的区别:ReferenceError就是在作用域中找不到,TypeError是在作用域中找到了但是做了它不可能做的事情
     例如:
     -- new关键字后面必须为构造函数
        let Dog='gg'
        new Dog()
     -- ()前必须为函数。如:
        let getName='gg'
        getName()
     -- 试图获取undefined、null的属性
        console.log(null.name)
     -- 预判错误的类型
        let a
        console.log(a.b)
        或
        let a={}
        console.log(a.getName())
  -- ReferenceError:引用错误
     引用一个不存在的变量时发生的错误,每当我们创建或定义一个变量时,变量名称都会写入一个变量存储中心中。这个变量存储中心就像键值存储一样,每当我们引用变量时,它都去存储中找到Key并提取并返回Value,如果我们要找的变量不在存储中,就会抛出ReferenceError。
     TypeError和ReferenceError的区别:ReferenceError就是在作用域中找不到,TypeError是在作用域中找到了但是做了它不可能做的事情
     -- 上下文中不存在的变量:
        console.log(jacksplwxy)
     -- 外部资源没有被正确载入:
        Uncaught ReferenceError: $ is not defined 就是因为jQuery没有正确导入而导致的
  -- RangeError:边界错误
     表示超出有效范围时发生的异常,主要的有以下几种情况:
     -- 数组长度为负数或超长
     -- 数字类型的方法参数超出预定义范围
     -- 函数堆栈调用超过最大值
  -- URIError:URL错误
   在调用URI相关的方法中URL无效时抛出的异常,主要包括encodeURI、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape()和unescape()几个函数
  -- EvalError:错误的使用了Eval 
  -- Error:所有错误的父类型
     抛出自定义错误:throw new Error("提示文字")
·JS异常的产生原因:
  -- 输入数据与预期的不一致
  -- 输入数据的来源:
     -- 各种IO数据
        -- 网络数据:由服务器响应决定
           -- http请求异常
           -- 静态资源加载异常
           -- promise异常
           -- iframe异常
           -- 跨域script error
        -- 数据库数据:库异常、读取异常等
        -- 文件数据:源数据异常、路径异常等
     -- 第三方库异常
     -- 函数参数异常
·如何保证代码遇到JS异常不崩溃
  -- 分析:js是单线程执行,为保证执行效率又加入eventLoop机制,主线程不断的循环往复的从任务队列中读取任务。其中任务分为主任务和异步任务,不同任务中发生代码异常不会影响到其他任务继续执行,只会影响同个任务中的异常后面的代码执行。
     另外,如果有语法异常,js在编译时就会报错,整个代码都不会运行,不过语法错误通常都会被ide提示出来
  -- 由于异步任务的执行依赖于主任务的执行,所以首先要保证主任务代码的健壮性,如此才能保证主流程畅通。虽然js能够很好地容错,但在一些情况下还是出现异常而导致程序终止运行
  -- 异步任务也可能影响主流程:
     例如页面弹窗后出现异常导致关闭按钮无法正常执行,从而导致弹窗影响页面其他的交互操作。
  -- 避免异常导致崩溃的办法汇总:
     -- 对我们无法控制的第三方数据源如IO数据、第三方库的数据和方法进行检验,可以通过try catch捕获或者throw主动抛出异常
     -- 需对函数中的参数进行校验,可以通过throw主动抛出异常
     -- 在处理JSON.parse()时,一定要用try/catch包起来
     -- 使用&&检验数据和方法的有效性:res&&res.data&&res.data.name
     -- 搭建异常检测系统(window.onerror/worker),不断健壮代码
        -- 《前端监控体系怎么搭建?》:closertb/closertb.github.io#46
  -- Q&A:
     -- 彻底消除JS运行时异常可能么?不能
     -- JS运行时异常可以恢复么?不能
·对代码健壮性的其他要求:
  -- 保证多状态代码的兼容性:
     -- 前端开发中,最难处理的场景就是代码的多状态管理,在编写这类代码时极易导致bug的产生(通常是遗漏状态)和代码失控。
     -- vue框架对状态管理默认库是vuex,实际上vuex对状态管理能力是很弱的,我在开发中就经常受到多状态管理的困扰,直到在编译原理的词法分析时学习到了自动机这一算法( https://github.com/jacksplwxy/JavaScript-compiler/blob/master/01_%E8%AF%8D%E6%B3%95%E5%88%86%E6%9E%90/README.md ),当时就觉得这才是进行管理状态的最佳方式。有限状态机的写法,有利于从全局角度对多状态的专门思考,代码逻辑清晰,表达力强,有利于封装事件。一个对象的状态越多、发生的事件越多,就越适合采用有限状态机的写法。另外,JavaScript语言是一种异步操作特别多的语言,常用的解决方法是指定回调函数,但这样会造成代码结构混乱、难以测试和除错等问题。有限状态机提供了更好的办法:把异步操作与对象的状态改变挂钩,当异步操作结束的时候,发生相应的状态改变,由此再触发其他操作。这要比回调函数、事件监听、发布/订阅等解决方案,在逻辑上更合理,更易于降低代码的复杂度。并且最近无意中发现了xstate( https://github.com/davidkpiano/xstate )这一状态管理库
     -- xstate学习文档:https://blog.jerry-hong.com/posts/xstate-introduction
     -- xstate在国内还是缺乏教程和最佳实践,使用相对较少,但优势还是比较明显的
  -- 保证边界条件下的兼容性
     -- 数据极小时
     -- 数据极大时
     -- 数据不全时
  -- 多模块之间操作的兼容性
  -- 保证不同尺寸设备的兼容性
  -- 保证不同性能设备的兼容性
  -- 保证不同网速环境的兼容性
  -- 保证不同浏览器的兼容性
  -- 保证高频交互下的兼容性
     -- 高频交互通常出现在游戏中,用户需要快速点击屏幕


*代码性能高
·在功能(含性能指标)满足客户情况下,各种性能指标尽可能高
·前端的主要性能指标包括这些方面:
  -- 资源请求速度尽量快:从发送请求到接受到响应的时间间隔尽量短
  -- 首次渲染时间尽量短:即页面第一次有内容呈现的时间
  -- 页面加载时间尽量短:即页面所有资源加载完成时间
  -- 交互动作的反馈时间尽量快:即用户与网页交互时的事件响应快慢
  -- 帧率FPS不低于60帧
·提升前端性能有一些最佳实践,但提升代码性能的关键是熟练掌握数据结构与算法
·提高代码性能更多内容:
  《FrontEndPerformanceOptimization》:https://github.com/jacksplwxy/FrontEndPerformanceOptimization


*代码易测试:
·高质量软件的一个基本要求是代码bug少,但这一点是很难人为把握的,而代码测试可以提供一定的保证,所以写出易测试的代码也是相当重要的
·TDD与BDD:
  -- 一种提高代码易测试性的方式是采用TDD。但一般来说我们更常用的是BDD
  -- 关于TDD与BDD:https://github.com/jacksplwxy/AutoTest/blob/main/readme.txt
·测试种类:
  -- 单元测试:对一个独立的模块进行功能测试
  -- 集成测试:对多个独立的模块进行功能测试。单元测试只能保证当个功能,如果我们测试一个项目,无法确保各个功能之间的依赖没有问题。集成测试,可以让我们站在业务流程的角度来进行测试,以确保这个流程是没有问题的。
  -- 端到端测试:集成测试只是对于前端的测试,是脱离真实后端环境的,仅仅只是将前端放在真实环境中运行,而后端和数据都应该使用Mock的。而端到端测试(E2E Test)则是将整个应用放到真实的环境中运行,包括数据在内也是需要使用真实的。
·代码易测试指的是单元测试易测试性。因为单元测试是保证代码运行正常的基石,是要熟悉代码源码的白盒测试,对代码易测试性有直接的要求
·要提升代码易测试性,首先要知道什么是难测试的代码。代码测试的本质就是调用被测试代码,并判断结果是否符合预期,所以难测试的代码就是这样的:
  -- 难以调用的代码
     -- 携带过多的状态
     -- 依赖过多其他模块
  -- 难以预测的代码
     -- 代码的结果与环境相关,例如系统时间、环境变量等
  -- 难以断言的代码
     -- 违背单一职责原则


*代码易阅读:
·代码易阅读是提高代码质量最重要的一环
·代码易阅读的最根本目的是:使代码更容易发生变更
  在前端开发中,代码需要变更就跟我们吃饭喝水一样平常,而要让我们的代码变得易更改,可读性就是基本保障,因此使代码易于阅读非常重要。
·如何使代码易阅读:
  -- 学习设计原则和设计模式:
     -- 学习设计原则和设计模式对理解什么才是好代码非常重要,毕竟什么是好代码是感性的,就像只有多学习古诗才能知道什么是好诗一样
     -- 这个项目( https://github.com/jacksplwxy/DesignPatterns_TypeScript )是我写的ts版本设计模式,总结各种设计原则和设计模式,描述了设计模式与高质量代码的关系
  -- 合理的结构:
     -- 良好的分层结构:将多变与少变甚至不变的代码分开处理
     -- 良好的文件结构:功能相似的代码整理分类
     -- 良好的代码结构:一类的或相似的变量或方法用命名空间、模块、对象、数组归纳起来
  -- 编写高类聚低耦合的模块:
     -- 依赖性:
        -- 模块之间只允许单向依赖(包括数据和方法的依赖)。
           -- 一个保持单向依赖的好主意是:被依赖的模块对上层模块不了解,只暴露尽可能少的API,即符合迪米特法则
           -- 编写代码时脑子一定要清楚哪个模块属于被依赖模块,不要将业务代码混入到被依赖模块中
           -- 当模块之间需要双向依赖时需应增加中间模块,模块之间通过中间模块通讯。中间模块的形式可以是:
              -- 第三方的实体:例如vue框架使用vuex进行状态存储,不同组件之间对数据的依赖改为第三方实体vuex的依赖
              -- 订阅发布:例如存在这个3个页面,一个是实时播放页面1,一个录像播放页面2,他们两个都依赖了一个播放器iframe页面3。播放器加载过程要展示loading层,而实时播放页面的loading和录像页面的不同。单向依赖是指播放时,页面1和页面2都单向依赖页面3,页面1和页面2都可以调用页面3的播放方法,但是页面3不允许直接调用页面1或页面2的loading方法,而必须要通过第三方模块订阅发布方式进行调用,例如页面1、页面2以通过window订阅loading消息,页面3通过window.postMessage发布loading消息
              -- 回调函数:本质上是订阅发布的一种实现
              -- 抽象层:例如中介者模式中(  http://c.biancheng.net/view/139.html ),同事类对中介者实例的依赖就放到了抽象同时类中,因为抽象就是意味着稳定的关系,代码变动可能性极小
     -- 正交性:一个模块提供的API中,多个方法之间是否有重复的功能。
     -- 紧凑性:一个模块提供的API中,公有方法总数必须很少,每个方法的参数也必须很少
  -- 合理的复用代码:
     -- 代码复用的好处:
        -- 复用代码一次编写,到处调用,十分方便高效
        -- 复用代码一次修改,到处改变,十分方便高效
        -- 可以防止未复用时漏修改的问题,十分安全可靠
        -- 代码整体更加简洁,可阅读性增强
        -- 降低软件开发风险,包括bug风险、效率风险
     -- 代码复用的坏处:
        -- 代码的复用通常需要进行一定的抽象,逻辑不直观,较大的增加编码难度和时间
        -- 代码复用后,通常需要单独提取出来保管,文件结构更复杂,依赖更多,破坏代码整体连贯性,增加维护难度
        -- 代码复用后,耦合增强,当业务需要兼容更多情况时,代码将变复杂,将增加维护难度。因为如果不复用,则可以直观修改各自逻辑即可。
     -- 代码何时需要复用:
        -- 提前对整个业务流程进行学习掌握,编码时对已知需要复用的或者存在非常大可能性需要复用的代码进行复用处理。
        -- 并非所有的代码重复都是逻辑的重复,销售和采购是两个业务,但这两个业务线中会有大量的重复代码。此代码的重复并非逻辑的重复,仅仅只是一种巧合。
     -- 代码复用的实现手段:
        -- 函数
        -- js中的原型对象:所有引用原型的对象都将复用原型对象的属性和方法
        -- java中的类:类的实例都拥有类的成员变量和方法
        -- java中类的继承
        -- vue中的组件:组件可以被引入到其他组件中实现复用
        -- vue中的混入(mixin):mixin可分发vue组件中的可复用功能
        -- vue中的指令
        -- vue中的插件
  -- 代码风格统一
     -- 统一的风格能减少阅读代码时的额外不必要的负担,使代码更整齐,更容易保证团队的代码质量
     -- eslint控制:vscode配合插件能够在保存时使代码自动优化为符合标准的代码
  -- 代码行限制:
     -- 限制代码行数是非常简单有效保证代码易读性的措施。限制行数有以下好处:
        -- 意味着一个模块中的变量和函数数是在大脑易记忆范围内
        -- 对于整体浏览代码非常有利
        -- 能够促使开发者编写代码时能够思考如何拆分和组织代码
     -- 一般情况下,js模块不要超过200行,vue组件不要超过300行
  -- 代码注释、文档清晰且准确
     -- 满篇的注释并不是好代码,也不是好习惯,好的代码是不需要注释的
     -- 没有文档比错误的文档更好
     -- 注释的缺点:注释意味着除了维护代码以外,还要维护注释。实际中,经常会出现修改代码但是忽略的注释的修改,毕竟代码可以通过测试测试出来,而注释不能。
  -- 良好的变量名
  -- 严禁枚举使用数字,必须使用有语义的字符
  -- 采用静态语言
     -- 静态类型语言因为类型强制声明,所以IDE可以做到很好的代码提示能力
     -- 静态语言有明确数据类型,很容易在编译阶段发现错误
     -- 静态语言有数据类型,能使代码更容易阅读
     -- 静态语言相对比较封闭的特点,使得第三方开发包对代码的侵害性可以降到很低
  -- 减少hack、补丁、奇技淫巧:
     -- 采用更普遍的工程代码
     -- 对hack、补丁、奇技淫巧进行详细说明
  -- 优化if else的使用:
     -- 基本原则:尽可能地维持正常流程代码在最外层。意思是说,可以写if-else语句时一定要尽量保持主干代码是正常流程,避免嵌套过深。
     -- 实现的手段有:
        -- 减少嵌套
        -- 移除临时变量
        -- 条件取反判断
        -- 合并条件表达式:如果有一系列条件测试都得到相同结果,将这些结果测试合并为一个条件表达式。
     文档:https://zhuanlan.zhihu.com/p/260731207
  -- code review:
     -- code review可执行的一个前提就是代码易阅读。同时code review能够促进代码的易读性
     -- code review的作用主要目的是始终保证随着时间的推移,代码越来越健康
        -- 减少bug:通过团队成员相互审核,避免代码层面出现显而易见的问题
        -- 提高代码质量:通过团队成员相互监督,在完成功能的基础之上不断完善代码风格和结构
        -- 提高代码效率:促进团队成思想交流,使代码可重复利用并提升可靠性
        -- 提升团队水平:代码是团队财产,团队成员在相互督促与改进中共同成长
     -- code review会影响开发效率,需得到团队全员认可或者在遇到代码维护痛点时进行推进
     -- 审核形式:
        -- 团队成员定期轮流审核
        -- 各个模块由指定高级程序员负责审核
     -- 流程:开发者根据Develop分支建立个人开发分支(英文全名,如ligang),每天自行Merge Develop分支代码;根据开发情况,定期向Develop分支发起Merge Requests请求,然后通知相关人进行codereview,根据代码情况查看是否进行合并,合并与否都需要留下coment( https://zhuanlan.zhihu.com/p/111760691  )
     -- code review的重点应该放在代码质量上,不应该讨论业务细节。更不应该有打分和评审机制,百害而无一利。团队里面不应该分个369等
     -- 文档:
        -- 谷歌的code review实践英文:https://github.com/google/eng-practices
        -- 谷歌的code review实践中文:https://github.com/xindoo/eng-practices-cn
·文档:
  -- https://github.com/kingzez/write-readable-javascript-code


*代码易扩展:
·《七大设计原则》( https://github.com/jacksplwxy/DesignPatterns_TypeScript )第一条原则是开闭原则,指出软件实体应尽量在不修改原有代码的情况下进行扩展。毕竟谁也保证不了代码修改后就一定不会产生bug。
·编写易扩展的关键是熟悉产品业务,为需要扩展的地方预留扩展点
·实现预留扩展点的关键是:对代码进行适当抽象。抽象将具有更大的通用性或者兼容性,如此实现扩展
·对于js,怎样编码能使代码具有很好的可扩展性
  -- js的对象有较好可扩展性
  -- js的数组有较好可扩展性
  -- 函数的参数:参数的改变可以导致函数运行结果的变化。
     -- 对于可能有多参数的函数,可以将参数设置为一个对象,方便拓展
  -- js中的原型:继承可以改变被继承类的成员变量和方法。
     -- 类似的还有继承
  -- ts接口或抽象类:能使类更加抽象,使代码具有通用性
  -- ts的枚举
  -- 状态机
  -- web项目的前后端分离
  -- 编译器的分层架构
  -- vue中的插槽(slot)设计:通过在slot中输入不同的内容,可以改变组件的显示


*许可协议:
  本项目所有文档遵循CC-BY-SA 4.0协议( https://creativecommons.org/licenses/by-sa/4.0/deed.zh )。使用者可以对本创作进行转载、节选、混编、二次创作,可以将其运用于商业用途,唯须署名作者,并且采用本创作的内容必须同样采用本协议进行授权


*更多前端工程化内容:
·npm:https://github.com/jacksplwxy/npm
·webpack:https://github.com/jacksplwxy/webpack
·代码质量:https://github.com/jacksplwxy/Good-Code
·前端自动化测试:https://github.com/jacksplwxy/AutoTest
·前端性能优化:https://github.com/jacksplwxy/FrontEndPerformanceOptimization
·web安全:https://github.com/jacksplwxy/security
·持续集成/持续部署:https://github.com/jacksplwxy/CI-CD




About

前端工程化系列——代码质量。系统分析如何编写高质量的代码

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published