专栏名称: 前端大全
分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯
目录
相关文章推荐
前端大全  ·  无敌了!强烈建议前端立即拿下软考! ·  2 天前  
江苏省邮政管理局  ·  三八妇女节,致敬最美的快递女性 ·  2 天前  
江苏省邮政管理局  ·  三八妇女节,致敬最美的快递女性 ·  2 天前  
前端早读课  ·  【第3468期】从 React 看前端 ... ·  3 天前  
51好读  ›  专栏  ›  前端大全

远离面条代码:编写可维护的 JS 代码

前端大全  · 公众号  · 前端  · 2017-08-22 20:30

正文

(点击 上方公众号 ,可快速关注)


编译:伯乐在线/小谢

如有好文章投稿,请点击 → 这里了解详情


几乎每个开发者都接手或维护过遗留项目,或者说是重启一个旧的项目。通常第一反应是抛弃原有的代码,从头开始写。这些代码会混乱不堪,没有文档,并且别人可能要花费好几天去读懂代码。但是,如果结合正确的规划、分析、和一个好的工作流程,那就有可能把一个意大利面式的代码仓库整理成一个整洁、有组织并易扩展的一份项目代码。

我曾经不得不接手并整理了很多的项目。从一开始就混乱不堪的也不是特别多。但实际上,最近就遇到了一个这样的情况。我已经学会了关于JavaScript代码组织的很多知识,最重要的是,不要被之前的程序员逼疯。在这篇文章中我想分享下一些我的步骤和我的经验。

分析项目

开始的第一步是简要看一下要做什么。如果是个网站,点击网站所有的功能:打开对话框、提交表单等等。做这些的时候,打开你的开发者工具,看下是否有报错或输出日志。如果是个node.js项目,打开命令行接口检查一下api。最好的情况是项目有一个入口(例如main.js,index.js,app.js),通过入口能将所有的模块初始化;如果是最坏的情况,也要找到每个业务逻辑的位置。

找出使用的工具。jquery? React? Express? 列出需要了解的一切重要的东西。如果所在项目使用 angular2 写的,而你还没有使用过,直接去看文档,最起码有个基本的了解。总之寻找最佳实践。

深入的了解项目

了解技术是一个好的开始,但是要得到真实的感觉和理解,需要研究一下 单元测试 。单元测试是用来测试代码的功能和方法是否按预期调用的一种方式。相比阅读和运行代码,单元测试能更深入的帮你了解代码。如果在你的项目中还没有单元测试,别急,我们接着往下看。

创建一个规范

这些都是关于代码一致性的内容。现在你已经了解了项目中使用的所有工具集,你知道了代码的结构和逻辑功能的位置,是时候建立一个规范。我建议添加一个 .editorconfig 文件来保证代码在不同的编辑器、IDE 或不同的开发者之间的编写风格一致。

正确的缩进

这是一个 饱受争议 (跟战争一样),代码中使用空格还是tab,其实这不重要。如果之前代码用的空格,那么使用空格,如果使用tab,继续用tab。只有当代码中都用到的时候才有必要决定使用其中的哪一个。讨论的观点是好的,但是一个好的项目必须保证所有的开发者能在一起和谐的工作。

为什么这很重要。因为每个人都有使用自己编辑器或使用 IDE 的方式。举例来说,我是 code folding 的追捧者。没有这些特性,我几乎会在文件中迷失。如果缩进不一样,那么代码看起来会很乱。所以,每次打开一个文件,在我开始工作之前必须修复缩进的问题。这很浪费时间。

// While this is valid JavaScript, the block can't

// be properly folded due to its mixed indentation.

function foo ( data ) {

let property = String ( data );

if ( property === 'bar' ) {

property = doSomething ( property );

}

//... more logic.

}

// Correct indentation makes the code block foldable,

// enabling a better experience and clean codebase.

function foo ( data ) {

let property = String ( data );

if ( property === 'bar' ) {

property = doSomething ( property );

}

//... more logic.

}


命名

保证项目里面使用到的命名规则是合理的。通常 JavaScript 使用驼峰式命名方式,但是我看到了很多混合式的命名方式。举例来说,jQuery 项目常常含有 jQuery 变量和其他变量的混合命名。

// Inconsistent naming makes it harder

// to scan and understand the code. It can also

// lead to false expectations.

const $ element = $( '.element' );

function _privateMethod () {

const self = $( this );

const _internalElement = $( '.internal-element' );

let $ data = element . data ( 'foo' );

//... more logic.

}

// This is much easier and faster to understand.

const $ element = $( '.element' );

function _privateMethod () {

const $ this = $( this );

const $ internalElement = $( '.internal-element' );

let elementData = $ element . data ( 'foo' );

//... more logic.

}


尽可能使用 lint

前面的几步会使我们的代码变得好看些,能够帮助我们快速地浏览代码,在此我还要推荐保证代码整洁性的最佳实践方案。 ESlint,JSlint , JSHint 是现在最流行的 JavaScript 格式工具。个人来说,之前使用 JSLint 比较多,现在开始又开始喜欢 ESlint 了,主要是它的一些自定义规则和最早支持 ES2015 语法很好用。

当你使用 lint 时,如果编辑器报了一堆错误,那么修复它们。在此之前什么也不要做。

更新依赖

更新依赖需要非常谨慎,如果你更换或更新了依赖很容易引发更多的错误,所以一些项目可能在某个版本(例如 v1.12.5)下面正常工作,然而通配符匹配到另个版本(例如 v1.22.x)就出问题了。这种情况下,你需要快速升级,版本号一般是这样的: MAJOR.MINOR.PATCH 。如果你还对语义化版本不熟悉,建议先读下 Tim Oxley 的这篇文章– Semver: A Primer 。

升级依赖没有通用的处理规则。每个项目不同,必须区分对待。项目中升级补丁版本号一般都不是问题,也可以建立副本使用。只有当依赖中主版本号内容发生冲突错误时,那就应该看下具体是什么发生了变化。可能 API 改变了,那样你就要大面积重写你项目中的代码。如果那觉得这样代价太高,那么我建议不要升级这个主版本号。

如果你使用 npm 来管理依赖(而且基本没什么其他好的方案了),你可以使用 npm outdated 命令在你的CLI里来检查哪些依赖版本是比较旧的。我举一个我项目里面叫 FrontBook 的例子,在这个项目中我经常更新依赖:

如你所见,我这里有很多更新。但我一般不会马上更新,而是一次更新一个。可以说,这样花费了很多时间,然而这是保证不出问题的唯一方法(如果项目没有任何测试用例的话)。

让我们动起手来

这里我主要要表达的是整理项目并不一定意味着移除或重写大部分的代码。当然,有时候这是唯一的解决方案,但是这不是你一开始就应该考虑的问题。JavaScript 代码很可能成为一个奇怪的代码,后面去做一些调整通常是不可能的。我们通常需要根据特定的场景来给出一个改造方案。

建立单元测试

使用测试用例可以保证你的代码能进行正确的运行而不会出现意外的错误。JavaScript 单元测试直接可以写出很多文章,所有我这里没办法介绍太多。广泛使用的框架有 karma、jasmine、macha 和 ava 等。如果你也想测试你的用户界面,推荐使用 Nightwatch.js 和 Dalekjs 这类浏览器自动测试工具。

单元测试和浏览器自动化测试的区别是,前者测试 JavaScript 本身代码。它保证了所有的模块和通用逻辑能预期运行。浏览器自动化,另一方面来说是测试界面,也就是项目的用户界面,保证页面上的元素在预期正确的位置。

在重构任何事情之前先建立好单元测试。这样项目的稳定性会提升,或许你没有考虑过项目稳定性的东西。这样做的好处是,不必担心犯一些自己没有意识到的错误。

Rebecca Murphey 写过一篇非常不错的文章 writing unit tests for existing JavaScript 。

架构

JavaScript 架构是另一个大的主题。重构和整理框架决定于你在这方面有多少经验。我们在软件开发中有很多的设计模式。但是并不是所有的都能适应稳定性的需求。很不幸,这篇文章中我不能给出所有的场景,但至少还是可以给一些通用性的建议。

首先,你要知道你的项目中使用到了那种设计模式。了解下这种模式,并保证它在整个项目是一致的。可扩展性的一个关键的地方是和设计模式相结合的,而不是混合的方法。当然,在你的项目里可以使用不同的设计模式来达到不同的目的(例如使用 单例模式 来建立数据结构或者短命名的工具函数,或者在模块中使用 观察者模式 ),但是绝对不要在一个模块中使用了一种设计模式,而在另外的模块中使用不同的模式。

如果在你的项目中个确实没有使用到什么架构(可能什么代码都在一个巨大的 app.js 中 ),那么是时候改变它了。但不要马上做所有的改变,而是一点一点的来。同样,这里没有万能的方法,每个项目的设置也是不一样的。项目目录结构根据项目的规模和复杂度不同也不一样。通常,对于最基本的层级,结构一般分为分为第三方内容、模块内容、数据和一个初始化所有模块和逻辑的入口(例如 index.js、main.js)。

这样我们就需要模块化了。

所有的东西都模块化?

模块化至今也不是大规模可扩展 JavaScript 项目的解决方案。它需要开发者必须去熟悉另一层 API 。尽管这样可能会带来很多的困难,但它的原则是把你的功能划分成小的模块。这样,在团队协作过程中解决问题就变的更简单了。每个模块应该有个一个明确的目标功能点。一个模块应该是不知道你外面代码逻辑是什么样的,并且能在不同的地方和场景下复用。

那么怎样将大量关联逻辑的代码拆分成模块呢?一起看下。

// This example uses the Fetch API to request an API. Let's assume

// that it returns a JSON file with some basic content. We then create a

// new element, count all characters from some fictional content

// and insert it somewhere in your UI.

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+/g i , '' ). 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 ));


这里基本没有模块化。所有的东西都是紧密结合的,并且相互依赖。想象一下在更大、更复杂的函数里,如果出了问题你需要调试 bug。可能API 不响应、JSON 里面字段改变了或者其他的问题。这简直是噩梦。

让我们把它按照不同职责分离开来:

// In the previous example we had a function that counted

// the characters of a string. Let's turn that into a module.

function countCharacters ( text ) {

const removeWhitespace = /s+/g i ;

return text . trim (). replace ( removeWhitespace , '' ). length ;

}

// The part where we had a string with some markup in it,

// is also a proper module now. We use the DOM API to create

// the HTML, instead of inserting it with a string.

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 ;

}

// The anonymous function from the .forEach() method,

// should also be its own module.

function appendCharacterCount ( config ) {

const wordCount = countCharacters ( config . content );

const wrapperElement = createWrapperElement ( config . className , wordCount );

const infoElement = document . querySelector ( '.info-element' );

infoElement . appendChild ( wrapperElement );

}


很好,我们现在有三个模块了,我们来看下调用的情况:

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 ]))

}







请到「今天看啥」查看全文