原文:http://www.zcfy.cc/article/312
几乎每个程序员都有接手维护别人遗留项目的经历。或者,有可能一个老项目某一天又被重新启动。通常情况下,接手老项目都会让人恨不得抛弃掉整个代码库从头开始。老代码凌乱、文档缺失、需要研究很多天才能完全搞明白它。然而,通过合适的规划、分解和好的工作流,项目代码可以变得干净、有组织和可扩展。
我曾经接手清理许多项目的代码,让我不得不重头开始的项目真心不多,不过我最近就遇到了一个。我从中学到了很多关于 JavaScript 代码组织的内容,以及最重要的是冷静,不要为你的前任抓狂。在这篇文章里,我想要让你知道我是怎么一步步处理项目代码的,告诉你我的经验。
第一步是先概览整个项目,弄明白问题所在。如果这是一个网站,通过点击测试所有功能点:打开模块、提交表单以及其他的。在做这件事的时候,打开开发者工具,看看是否有任何报错,看看控制台有没有日志。如果这是一个Node.js项目,打开命令行界面然后检查各个API。最好的情况是项目通过统一的入口(例如:main.js
, index.js
, app.js
, ……)来初始化所有的模块,最差的情况是整个业务逻辑散落于各处。
搞清楚项目采用了哪些工具。是 jQuery、React 或是 Express?将所有重要的信息整理在一个清单里。假设这个项目是用 Angular 2 写的,你之前对 Angular 2 不熟悉,先去查看文档,对它有一个初步的了解,并寻找最佳实践的范例。
了解技术点是一个好开端,但要进一步深入理解,我们需要看一看它的单元测试。单元测试通过测试代码的功能函数与方法来保证代码如预期的那样运行。不同于仅仅阅读代码,查看和运行单元测试能让你更加深入理解代码的期望运行结果。如果接手的项目没写单元测试,没关系,我们可以自己来写。
这样做是为了确立一致性。你已经了解了项目的工具链、代码结构、模块的逻辑关系,现在该为项目创建基线了。我推荐添加一个 .editorconfig
文件让代码风格在不同的编辑器、IED和不同的开发者之间保持一致。
使用 tab 还是空格来缩进是一个老问题 ,常引发程序员争论不休,不过没关系,不管项目用的是空格还是 tab,继续使用之前的就好了。除非代码库既有空格缩进又有 tab 缩进的代码,那就只好在两者中做出一个取舍。每个人都可以保持自己的观点,但一个好的项目要保证所有的开发者都可以无争议地协同工作。
为什么这个很重要?因为每一个人都有自己使用编辑器或 IDE 的习惯。比如,我是个代码折叠控,要是编辑器没有正确的代码折叠功能,我整个人都会迷失在文件里。如果代码的缩进不一致,就会影响到折叠功能,因此每一次我打开一个文件,不得不先修复那些缩进然后才能开始工作,这十分浪费时间。
// 虽然这段 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 对象和其他对象。
// 不一致的命名让代码变得难以检查和理解
// 它还会误导维护者
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,开发者不得不额外去熟悉它们。这虽然增加了麻烦,但是值得去做的。模块化的基本原则是将所有功能拆分为小模块。这么做了以后,不仅让你更容易解决代码里的问题,也让项目组的其他成员更容易协同工作。每个模块只做一件事,它们不用关心外部逻辑,可以被复用在不同的地方。
如何将一大堆功能拆分成许多逻辑关联的小模块?让我们来做做看:
// 这个例子使用 Fetch API 来请求一个服务器的 API
// 让我们假设它返回一个 JSON 文件,包含一些基本信息
// 然后我们创建一个新的元素,统计 json 所有属性的
// content 字段中的字符数,然后将结果插入 DOM 的某个位置。
fetch("https://api.somewebsite.io/post/61454e0126ebb8a2e85d", { method: "GET" })
.then(response => {
if (response.status === 200) {
return response.json();
}
})
.then(json => {
if (json) {
Object.keys(json).forEach(key => {
const item = json[key];
const count = item.content.trim().replace(/\s+/gi, "").length;
const el = `
<div class="foo-${item.className}">
<p>Total characters: ${count}</p>
</div>
`;
const wrapper = document.querySelector(".info-element");
wrapper.innerHTML = el;
});
}
})
.catch(error => console.error(error));
上面的代码不是模块化的。所有的功能都耦合在一起。想象一下,如果这是更复杂的函数,由于出了一些错误你必须调试它们,可能 API 没返回,可能某些原因 JSON 内部的值被改变或者别的什么问题。调试这一大坨代码如同噩梦般,不是吗?
让我们将代码按不同的职责拆分开来:
// 在前一个例子里,有一个功能是统计字符串的字符数
// 让我们将它单独抽出来成为一个模块
function countCharacters (text) {
const removeWhitespace = /\s+/gi;
return text.trim().replace(removeWhitespace, "").length;
}
// 这一部分,我们也独立成一个模块,使用 DOM API 来创建 HTML
// 取代之前直接插入字符串的做法
function createWrapperElement (cssClass, content) {
const className = cssClass || "default";
const wrapperElement = document.createElement("div");
const textElement = document.createElement("p");
const textNode = document.createTextNode(`Total characters: ${content}`);
wrapperElement.classList.add(className);
textElement.appendChild(textNode);
wrapperElement.appendChild(textElement);
return wrapperElement;
}
// 前一个例子 .forEach 中的匿名函数也被我们抽出来形成一个模块
function appendCharacterCount (config) {
const wordCount = countCharacters(config.content);
const wrapperElement = createWrapperElement(config.className, wordCount);
const infoElement = document.querySelector(".info-element");
infoElement.appendChild(wrapperElement);
}
好了,我们现在有了三个新模块,让我们看看重构之后的 fetch
调用:
fetch("https://api.somewebsite.io/post/61454e0126ebb8a2e85d", { method: "GET" })
.then(response => {
if (response.status === 200) {
return response.json();
}
})
.then(json => {
if (json) {
Object.keys(json).forEach(key => appendCharacterCount(json[key]))
}
})
.catch(error => console.error(error));
当然我们还可以进一步将 .then( )
中的逻辑也抽出来形成模块,不过我想我已经充分表达了模块化的含义。
如我前面提到的,将你的代码拆成小模块会增加额外的一层 API。如果你不想这么做,但是又想要让代码易于与其他开发者一起维护,不拆函数也没问题,你依然可以将你的代码分解成更简单的部分并把重点放在可测试的代码上。
文档化是一个老生常谈的话题。程序员社区的一部分人提倡将一切文档化,而另一部分人认为好代码即是文档。我奉行中庸之道,我觉得代码的可读和可扩展之间需要保持平衡。你可以使用 JSDoc 来帮助你实现文档化。
JSDoc 是一个 JavaScript 的 API 文档生成器。常用的编辑器和 IDE 都有支持它的插件。我们看一个例子:
function properties (name, obj = {}) {
if (!name) return;
const arr = [];
Object.keys(obj).forEach(key => {
if (arr.indexOf(obj[key][name]) <= -1) {
arr.push(obj[key][name]);
}
});
return arr;
}
这个函数有两个参数,遍历一个对象,返回一个数组。这也许不是一个过于复杂的方法,但是对于没写过这段代码的人来说,搞懂它还是有点费劲。此外,这个方法具体的作用也不是很明确。让我们对它文档化:
/**
* 遍历一个对象,将将所有属性对象的 "name" push 到一个新数组中
* 如果有重复,只 push 一次
* @param {String} propertyName - 属性的名字
* @param {Object} obj - 你想要遍历的对象
* @return {Array}
*/
function getArrayOfProperties (propertyName, obj = {}) {
if (!propertyName) return;
const properties = [];
Object.keys(obj).forEach(child => {
if (properties.indexOf(obj[child][propertyName]) <= -1) {
properties.push(obj[child][propertyName]);
}
});
return properties;
}
我没有改变任何代码,只是改了一下函数名,添加了一段简短的注释,就已经让这段代码的可读性变得好多了。
重构本身是一项艰巨的任务。为了能随时回滚你的修改(假如你破坏了一些原有功能,过了一会才意识到,你可能就需要回滚代码到之前版本),你的每一部分修改,比如重写了一个方法、重命名了一个名字空间,都应该及时提交到 git (或者 svn)。这么做也许会让你觉得麻烦,但这么做有助于让你的清理工作更有条理。
为你的重构工作开一个新的分支,千万别总是在主线 (master) 上改。因为主线版本你有可能需要临时做一些更新或者随时发布一些 bug fixes 到线上环境,而你又不能将你没有经过测试的和未完成的重构一同发布到线上,因此我建议你还是应该在不同的分支上工作。
在 GitHub 上有一份有趣的指导,是关于如何使用他们的版本控制流程的。
除了用技术解决问题之外,有一个很重要的点很少被人提及:别为你的前任抓狂。我无意指责任何人,但是我知道一些人经历过这种情况。我花了很多年的时间去理解和克服心理上的不爽。我曾经对前任开发者们留下的代码、解决方案感到有些抓狂,他们做的一切在我眼里看来都造成混乱。
结果,这些消极情绪没带给我任何好处。消极情绪会导致你过度重构,浪费你的时间,而且可能破坏一些原有功能,而这一切又导致你越来越恼怒。你可能会花费额外的时间去重写一个本来毫无问题的模块,没有人会因此感谢你,因为你在做无用功。先分析状况,然后做有价值的重构。在任何时候,你随时可以对一个模块做一些细节的改进。
一段代码为什么写成这样往往是有历史原因的,也许前任程序员没有足够的时间将代码写得足够好、或者不知道有更好的写法,或者别的什么原因。我们都是过来人。
让我们从头回顾一下所有的步骤,为你的下一个项目创建一个 checklist:
* 先忘掉自己的开发者身份,以一个用户的身份来看清它的全貌。
* 浏览代码库,列出项目使用的工具。
* 阅读项目相关工具的文档和最佳实践。
* 浏览单元测试,从更高层面上了解项目。
* 引入 `.editorconfig` 以保证在所有的编辑器和 IDE 下保持代码风格一致。
* 使缩进风格一致,至于是用 tab 还是空格,无所谓。
* 执行命名约定。
* 如果代码检查工具不存在, 添加一个,可以是 ESLint、 JSLint 或者 JSHint。
* 升级依赖,但是需要格外小心,弄清楚到底升级了什么。
* 建立单元测试与浏览器自动化测试,可以使用一些工具,例如 Karma、Jasmine、或者 Nightwatch.js。
* 确保架构和设计模式保持一致。
* 不要混用 设计模式,坚持使用已经存在的设计模式。
* 决定你是否需要将代码库拆分成模块。每一个模块应当具有单一的目的,模块不用关心自身之外的其他逻辑。
* 如果你不想拆分模块,把重点放在可测试的代码上,把它们分解成更简单的代码块。
* 恰当地命名你的函数,为代码适当撰写文档,保持可读和可扩展的平衡。
* 使用 JSDoc 来生成文档。
* 定期提交代码,特别在有重要改变时。这样如果有什么改错了,可以方便回滚。
* 别为你的前任开发者抓狂。负面情绪只会导致过度重构而浪费时间。
* 一段代码为什么写成这样总是有原因的。要牢记我们都是过来人。
我非常希望这篇文章能帮到你。如果你正在为代码重构做这些努力,或者你有一些我没有提到过的好建议,我希望你可以告诉我。
英文原文:https://www.sitepoint.com/write-maintainable-javascript/?utm_source=javascriptweekly&utm_medium=email
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。