几乎每个程序员都有接手维护别人遗留项目的经历。或者,有可能一个老项目某一天又被重新启动。通常情况下,接手老项目都会让人恨不得抛弃掉整个代码库从头开始。老代码凌乱、文档缺失、需要研究很多天才能完全搞明白它。然而,通过合适的规划、分解和好的工作流,项目代码可以变得干净、有组织和可扩展。
我曾经接手清理许多项目的代码,让我不得不重头开始的项目真心不多,不过我最近就遇到了一个。我从中学到了很多关于 JavaScript 代码组织的内容,以及最重要的是冷静,不要为你的前任抓狂。在这篇文章里,我想要让你知道我是怎么一步步处理项目代码的,告诉你我的经验。
分析项目
第一步是先概览整个项目,弄明白问题所在。如果这是一个网站,通过点击测试所有功能点:打开模块、提交表单以及其他的。在做这件事的时候,打开开发者工具,看看是否有任何报错,看看控制台有没有日志。如果这是一个Node.js项目,打开命令行界面然后检查各个API。最好的情况是项目通过统一的入口(例如:main.js
, index.js
, app.js
, ……)来初始化所有的模块,最差的情况是整个业务逻辑散落于各处。
搞清楚项目采用了哪些工具。是 jQuery、React 或是 Express?将所有重要的信息整理在一个清单里。假设这个项目是用 Angular 2 写的,你之前对 Angular 2 不熟悉,先去查看文档,对它有一个初步的了解,并寻找最佳实践的范例。
从更高层面上理解项目
了解技术点是一个好开端,但要进一步深入理解,我们需要看一看它的单元测试。单元测试通过测试代码的功能函数与方法来保证代码如预期的那样运行。不同于仅仅阅读代码,查看和运行单元测试能让你更加深入理解代码的期望运行结果。如果接手的项目没写单元测试,没关系,我们可以自己来写。
创建基线
这样做是为了确立一致性。你已经了解了项目的工具链、代码结构、模块的逻辑关系,现在该为项目创建基线了。我推荐添加一个 .editorconfig
文件让代码风格在不同的编辑器、IED和不同的开发者之间保持一致。
一致的缩进
使用 tab 还是空格来缩进是一个老问题 ,常引发程序员争论不休,不过没关系,不管项目用的是空格还是 tab,继续使用之前的就好了。除非代码库既有空格缩进又有 tab 缩进的代码,那就只好在两者中做出一个取舍。每个人都可以保持自己的观点,但一个好的项目要保证所有的开发者都可以无争议地协同工作。
为什么这个很重要?因为每一个人都有自己使用编辑器或 IDE 的习惯。比如,我是个代码折叠控,要是编辑器没有正确的代码折叠功能,我整个人都会迷失在文件里。如果代码的缩进不一致,就会影响到折叠功能,因此每一次我打开一个文件,不得不先修复那些缩进然后才能开始工作,这十分浪费时间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// 虽然这段 JavaScript 代码是合法的, 但这段代码块没办法正常折叠 // 因为这段代码的缩进不一致 function foo (data) { let property = String(data); if (property === "bar") { property = doSomething(property); } //... more logic. } // 修复缩进后,这段代码才可以被正确折叠 // 从而获得更好的编码体验和更整洁的代码库 function foo (data) { let property = String(data); if (property === "bar") { property = doSomething(property); } //... more logic. } |
命名
确保项目中使用命名约定是值得推崇的做法。驼峰命名通常在 JavaScript 代码中使用,但是我看到过许多不一致的命名。例如:jQuery 项目经常会有代码在命名上混淆 jQuery 对象和其他对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// 不一致的命名让代码变得难以检查和理解 // 它还会误导维护者 const $element = $(".element"); function _privateMethod () { const self = $(this); const _internalElement = $(".internal-element"); let $data = element.data("foo"); //... more logic. } // 这样改就更易于理解了 const $element = $(".element"); function _privateMethod () { const $this = $(this); const $internalElement = $(".internal-element"); let elementData = $element.data("foo"); //... more logic. } |
代码检查
之前所做的一切是在美化代码,主要是让它变得更易于检查。接下来我们介绍保证代码质量的通用最佳实践。ESLint,JSLint,还有 JSHint 是目前最受欢迎的三个 JavaScript 代码检查工具。我个人用 JSHint 最多,但我现在最喜欢 ESLint,主要是因为它可以自定义规则,也较早地支持了 ES2015。
一旦你开始代码检查,如果有很多错误信息出现,立即修复它们。别跳过这些步骤,直到你的代码检查工具对你的代码彻底满意了。
升级依赖
升级依赖需要仔细些,如果你不注意依赖本身的升级带来的一些变化,就很容易导致错误。一些项目可能只能依赖某些库的固定版本(例如:v1.12.5
),而另一些则使用版本通配符(例如:v1.12.x
)。如果你要快速升级依赖,你需要知道依赖模块的版本号通常按如下规则建立:主版本.小版本.补丁
。如果你对 semantic versioning 的方式不熟悉,我推荐你先阅读 Tim Oxley 的这篇文章。
升级依赖没有通用方法。每个项目是不一样的,需要区别对待。升级依赖的补丁
版本通常不会出什么问题,小版本
一般也还OK。但如果你要升级依赖的主版本
,你就需要仔细检查版本升级带来的改变。有可能 API 完全改变了,那样你就得重写你项目的一大堆代码。如果非必要,我一般避免将依赖升级到下一个主版本。
如果你的项目使用 npm 来管理依赖,你可以很方便地使用 npm outdated
命令来检查你的依赖是否已经过时了。我用一个项目 FrontBook 来举例说明,在这个项目里,我经常升级所有的依赖:
如你所见,我这个项目里的依赖有很多主版本升级。我不会一次将他们全部升级,但是会一次升级一个。虽然这会耗费许多时间,但这是确保不会出问题的唯一办法(尤其是如果这个项目没有任何测试)。
下面该干脏活了
我必须让你知道的非常重要的一点是,清理代码并不意味着需要移除和重写大量的代码片段。当然,有时候这可能是唯一的解决办法,但是这不应该是你的首选方案。JavaScript 特别灵活,因此难以给出一般性的建议,通常情况下你必须对症下药。
建立单元测试
单元测试能保证你理解代码是如何工作的,这样避免一些意外导致错误。JavaScript 单元测试的内容足够写另一篇文章,所以我在这里不能详细介绍。目前被广泛使用的单元测试框架有 Karma、Jasmine、Mocha 以及 Ava。如果你还要测试你的用户界面,Nightwatch.js 和 DalekJS 是适合浏览器自动化测试的工具。
单元测试和浏览器自动化测试的区别是,前者测试你的 JavaScript 代码本身,来确保你所有的模块和主要逻辑运行无误。后者,测试用户界面,确保界面元素在正确的位置,且如预期地工作。
在你开始动手重构代码之前,认真对待单元测试,那样你的项目的稳定性将得到改善,而你甚至还没有开始考虑可扩展性。单元测试带来的另一个好处是你不再需要无时无刻担心你的改动会无意中破坏原有功能。
Rebecca Murphey 写了一篇很棒的文章关于如何为现有代码写单元测试。
架构
JavaScript 架构是另一个大话题。重构和清理架构归结于你在这方面积累了多少经验。我们可以选择许多不同的设计模式,但不是所有的模式都适合于提升可扩展性。限于篇幅,我不能涵盖所有模式,但我至少可以给你一些通用的建议。
首先,你需要找出哪些设计模式在你的项目中已经使用到了。阅读有关这些模式的部分,确保它们在项目中使用上保持一致性。可扩展性的关键之一便是坚持一致的模式,避免混搭。当然,你可以针对项目中的不同目的采用不同的设计模式(例如,将单例模式用于数据结构和短命名空间的辅助功能函数,以及将观察者模式用于与模块),但是别对一个模块使用了一种设计模式,对另一个模块又用另一种不同的设计模式。
如果你的项目没有任何架构(可能一切都堆在一个巨大的app.js
文件里),从现在开始让它有架构。不过别指望一口吃成胖子,需要一点一点来。再次强调,没有对任何项目都适用的万精油方案,每一个项目的情况都是不同的。根据规模和复杂度不同,项目文件目录结构各有不同。通常,最基本的原则是,目录结构应当将第三方库、模块、数据以及负责初始化模块与逻辑的入口文件(比如:index.js
、main.js
)分开来。
简而言之就是模块化。
将一切模块化?
模块化不是解决 JavaScript 扩展性问题的唯一选择。模块化增加了一层 API,开发者不得不额外去熟悉它们。这虽然增加了麻烦,但是值得去做的。模块化的基本原则是将所有功能拆分为小模块。这么做了以后,不仅让你更容易解决代码里的问题,也让项目组的其他成员更容易协同工作。每个模块只做一件事,它们不用关心外部逻辑,可以被复用在不同的地方。
如何将一大堆功能拆分成许多逻辑关联的小模块?让我们来做做看:
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 |
// 这个例子使用 Fetch API 来请求一个服务器的 API // 让我们假设它返回一个 JSON 文件,包含一些基本信息 // 然后我们创建一个新的元素,统计 json 所有属性的 // content 字段中的字符数,然后将结果插入 DOM 的某个位置。 fetch("https://api.somewebsite.io/post/61454e0126ebb8a2e85d", { method: "GET" }) .then(response => { if (r">(response => { if (r ڄ经历。或者,有可能一个老项目某一天又被重新启动。通常情况下,接手老项目都会让人恨不得抛弃掉整个代码库从头开始。老代码凌乱、文档缺失、需要研究很多天才能完全搞明白它。然而,通过合适的规划、分解和好的工作流,项目代码可以变得干净、有组织和可扩展。
我曾经接手清理许多项目的代码,让我不得不重头开始的项目真心不多,不过我最近就遇到了一个。我从中学到了很多关于 JavaScript 代码组织的内容,以及最重要的是冷静,不要为你的前任抓狂。在这篇文章里,我想要让你知道我是怎么一步步处理项目代码的,告诉你我的经验。
分析项目第一步是先概览整个项目,弄明白问题所在。如果这是一个网站,通过点击测试所有功能点:打开模块、提交表单以及其他的。在做这件事的时候,打开开发者工具,看看是否有任何报错,看看控制台有没有日志。如果这是一个Node.js项目,打开命令行界面然后检查各个API。最好的情况是项目通过统一的入口(例如: 搞清楚项目采用了哪些工具。是 jQuery、React 或是 Express?将所有重要的信息整理在一个清单里。假设这个项目是用 Angular 2 写的,你之前对 Angular 2 不熟悉,先去查看文档,对它有一个初步的了解,并寻找最佳实践的范例。 从更高层面上理解项目了解技术点是一个好开端,但要进一步深入理解,我们需要看一看它的单元测试。单元测试通过测试代码的功能函数与方法来保证代码如预期的那样运行。不同于仅仅阅读代码,查看和运行单元测试能让你更加深入理解代码的期望运行结果。如果接手的项目没写单元测试,没关系,我们可以自己来写。 创建基线这样做是为了确立一致性。你已经了解了项目的工具链、代码结构、模块的逻辑关系,现在该为项目创建基线了。我推荐添加一个 一致的缩进使用 tab 还是空格来缩进是一个老问题 ,常引发程序员争论不休,不过没关系,不管项目用的是空格还是 tab,继续使用之前的就好了。除非代码库既有空格缩进又有 tab 缩进的代码,那就只好在两者中做出一个取舍。每个人都可以保持自己的观点,但一个好的项目要保证所有的开发者都可以无争议地协同工作。 为什么这个很重要?因为每一个人都有自己使用编辑器或 IDE 的习惯。比如,我是个代码折叠控,要是编辑器没有正确的代码折叠功能,我整个人都会迷失在文件里。如果代码的缩进不一致,就会影响到折叠功能,因此每一次我打开一个文件,不得不先修复那些缩进然后才能开始工作,这十分浪费时间。
命名确保项目中使用命名约定是值得推崇的做法。驼峰命名通常在 JavaScript 代码中使用,但是我看到过许多不一致的命名。例如:jQuery 项目经常会有代码在命名上混淆 jQuery 对象和其他对象。
代码检查之前所做的一切是在美化代码,主要是让它变得更易于检查。接下来我们介绍保证代码质量的通用最佳实践。ESLint,JSLint,还有 JSHint 是目前最受欢迎的三个 JavaScript 代码检查工具。我个人用 JSHint 最多,但我现在最喜欢 ESLint,主要是因为它可以自定义规则,也较早地支持了 ES2015。 一旦你开始代码检查,如果有很多错误信息出现,立即修复它们。别跳过这些步骤,直到你的代码检查工具对你的代码彻底满意了。 升级依赖升级依赖需要仔细些,如果你不注意依赖本身的升级带来的一些变化,就很容易导致错误。一些项目可能只能依赖某些库的固定版本(例如: 升级依赖没有通用方法。每个项目是不一样的,需要区别对待。升级依赖的 如果你的项目使用 npm 来管理依赖,你可以很方便地使用 如你所见,我这个项目里的依赖有很多主版本升级。我不会一次将他们全部升级,但是会一次升级一个。虽然这会耗费许多时间,但这是确保不会出问题的唯一办法(尤其是如果这个项目没有任何测试)。 下面该干脏活了我必须让你知道的非常重要的一点是,清理代码并不意味着需要移除和重写大量的代码片段。当然,有时候这可能是唯一的解决办法,但是这不应该是你的首选方案。JavaScript 特别灵活,因此难以给出一般性的建议,通常情况下你必须对症下药。 建立单元测试单元测试能保证你理解代码是如何工作的,这样避免一些意外导致错误。JavaScript 单元测试的内容足够写另一篇文章,所以我在这里不能详细介绍。目前被广泛使用的单元测试框架有 Karma、Jasmine、Mocha 以及 Ava。如果你还要测试你的用户界面,Nightwatch.js 和 DalekJS 是适合浏览器自动化测试的工具。 单元测试和浏览器自动化测试的区别是,前者测试你的 JavaScript 代码本身,来确保你所有的模块和主要逻辑运行无误。后者,测试用户界面,确保界面元素在正确的位置,且如预期地工作。 在你开始动手重构代码之前,认真对待单元测试,那样你的项目的稳定性将得到改善,而你甚至还没有开始考虑可扩展性。单元测试带来的另一个好处是你不再需要无时无刻担心你的改动会无意中破坏原有功能。 Rebecca Murphey 写了一篇很棒的文章关于如何为现有代码写单元测试。 架构JavaScript 架构是另一个大话题。重构和清理架构归结于你在这方面积累了多少经验。我们可以选择许多不同的设计模式,但不是所有的模式都适合于提升可扩展性。限于篇幅,我不能涵盖所有模式,但我至少可以给你一些通用的建议。 首先,你需要找出哪些设计模式在你的项目中已经使用到了。阅读有关这些模式的部分,确保它们在项目中使用上保持一致性。可扩展性的关键之一便是坚持一致的模式,避免混搭。当然,你可以针对项目中的不同目的采用不同的设计模式(例如,将单例模式用于数据结构和短命名空间的辅助功能函数,以及将观察者模式用于与模块),但是别对一个模块使用了一种设计模式,对另一个模块又用另一种不同的设计模式。 如果你的项目没有任何架构(可能一切都堆在一个巨大的 简而言之就是模块化。 将一切模块化?模块化不是解决 JavaScript 扩展性问题的唯一选择。模块化增加了一层 API,开发者不得不额外去熟悉它们。这虽然增加了麻烦,但是值得去做的。模块化的基本原则是将所有功能拆分为小模块。这么做了以后,不仅让你更容易解决代码里的问题,也让项目组的其他成员更容易协同工作。每个模块只做一件事,它们不用关心外部逻辑,可以被复用在不同的地方。 如何将一大堆功能拆分成许多逻辑关联的小模块?让我们来做做看: |