全栈式JavaScript

posted in: javascript | 0

reprint:http://blog.jobbole.com/52745/

如今,在创建一个Web应用的过程中,你需要做出许多架构方面的决策。当然,你会希望做的每一个决定都是正确的:你想要使用能够快速开发的技术,支持持续的迭代,最高的工作效率,迅速,健壮性强。你想要精益求精并且足够敏捷。你希望你选择的技术能够在短期和长期上都让你的项目取得成功。但这些技术都不是轻而易举就能选出来的。

我的经验告诉我,全栈式JavaScript符合了这所有的要求。可能你已经发现了些许端倪,又或许你已经在考虑它的实用性,并且在和朋友讨论争论它的话题。但是你是否亲自尝试过呢?在这篇文章中,我会对于全栈式JavaScript给出一个比较全面的介绍,为什么它会是正确的选择,它又是如何施展它的魔法的。

先给出一个概括预览:

toptal-blog-500-opt

接下来我会一项一项地介绍这些组件。但是在这之前,我们简短地回顾一下,我们是如何发展到现在的这个阶段的。

我为什么选择用JavaScript

从1998年开始,我就是一个Web开发者。当时,我们使用Perl进行大多数的服务器端的开发;但是从那时候开始,我们就在客户端使用JavaScript。Web服务器端的技术已经发生了翻天覆地的变化:我们被一波又一波的技术潮流推着往前走,PHP,ASP,JSP,.NET,Ruby,Python,这里只列出了几个例子。开发人员们开始意识到,在服务器端和客户端使用不同的语言使得事情变得复杂化。

在早期的PHP和ASP的时代,那个时候模板引擎还仅仅是个设想,开发人员们在HTML中嵌入他们的应用代码。我们经常可以看到下面这种脚本嵌入的写法:

<script>
<?php
if ($login == true){
?>
alert("Welcome");
<?php
}
?>
</script>

或者更糟糕:

<script>
var users_deleted = [];
<?php
$arr_ids = array(1,2,3,4);
foreach($arr_ids as $value){
?>
users_deleted.push("<php>");
<?php
}
?>
</script>

对于新手来说,很容易被不同语言之间的用法而混淆,犯下一些很典型的错误,比如for和foreach。更为不爽的是,以这样的方式来写代码,使得服务器端和客户端很难以非常和谐的方式处理相同的数据结构,即使是今天也是如此(当然除非你的开发团队有专职的前端和后端工程师 — 但即使他们之间能够共享信息,但仍然不能仅仅基于对方的代码进行合作)。

<?php
$arr = array("apples", "bananas", "oranges", "strawberries"),
$obj = array();
$i = 10;
foreach($arr as $fruit){
$obj[$fruit] = $i;
$i += 10;
}
echo json_encode(obj);
?>
<script>
$.ajax({
url:"/json.php",
success: function(data){
var x;
for(x in data){
alert("fruit:" + x + " points:" + data[x]);
}
}
});
</script>

最初,对于统一使用一种编程语言的尝试是使用后台的语言编写客户端的组件,然后编译成JavaScript。但这种方式并没有如期望的一样很好地工作,许多相关的项目都失败了(比如被ASP MVC取代了的ASP.NET Web forms, 又比如正在逐步被Polymer取代的GWT)。当然这些想法都是伟大的,从本质上讲,都是想在服务器端和客户端使用同一种语言,让我们可以重用一些组件和资源(注意这里的关键词:资源)。

最终得出的答案很简单:将JavaScript放到服务端

其实JavaScript诞生之初是在网景公司的企业及服务器的服务端,只是当时它还没有完全准备好。经过数年的磨炼和错失,最终Node.js出现了,它不仅将JavaScript放到了服务器端,同时也推广了非阻塞式编程(non-blocking programming)的思想,这种思想来自于nginx的世界。感谢Node的创始者们nginx的技术背景,并且继续(聪明地)保持了它的简单性,也感谢JavaScript天生的事件轮询机制。

(一句话概括,非阻塞式编程目的在于将消耗时间的任务放到一边,通过指定在这些任务结束时需要做的操作,这样可以在同一时刻让处理器去处理其他的请求。)

Node.js永久性地改变了我们处理I/O访问的方式。作为Web开发者,我们过去一直使用如下的方式访问数据库(I/O):

var resultset = db.query("SELECT * FROM 'table'");
drawTable(resultset);

这里的第一行代码本质上已经阻塞了你的代码,因为你的代码停止下来等待数据库驱动返回一个结果集(resultset)。而与此同时,你的平台架构其实给你提供了并发的方法,通常是通过线程(threads)和派生(forks)。

在Node.js和非阻塞式编程的帮助下,我们可以更多的控制我们程序的执行流。现在(尽管在数据库I/O驱动器的背后可能已经有并行执行),你可以定义你的程序在I/O操作期间并行做的事情,以及在接收到结果集之后做的操作。

db.query("SELECT * FROM 'table'", function(resultset){
drawTable(resultset);
});
doSomeThingElse();

上面的代码片段中,我们定义了两个程序流:第一个在我们发出数据库查询之后执行的操作,第二个是以回调的方式在我们接收到结果集之后做的操作。这是一个非常优雅并且强大的处理并发的方式。正如他们所说的,“一切都在并行执行——除了你的代码。(Evetything runs in parallel — except your code.)”这样,你的代码会更易写,有更高的可读性,容易理解,也便于维护,这些都基于你找回了对程序流的控制。

这些观点早就不是很新的观点,那为什么他们随着Node.js变得如此流行起来。很简单:非阻塞式编程可以有多重实现的方式。但可能最简单的就是使用回调和事件轮询。在大多数于语言里,做到这点并不是一个简单的事情。回调机制在其他的一些于语言里是一个比较常见的功能,但是事件轮询却不是。你会经常发现自己还需要在一些扩展库上做挣扎(比如,Python中使用Tornado)。

但是在JavaScript中,回调机制已经被内建在语言中, 事件轮询也是如此。而对JavaScript稍有了解的程序员对它们也非常熟悉(或者至少使用过它们,即使他们有可能并不完全理解什么是事件轮询)。突然之间,地球上所有的创业公司都可以在客户端和服务器端重用开发人员(或者资源),解决了“需要Python大师(Python Guru Needed)”的招聘发布问题

因此,现在我们有了一个发展迅速的平台(感谢于非阻塞式编程),和一个非常易于使用的语言(感谢JavaScript)。但是这就足够了吗?它是可持续的吗?我确信,JavaScript在将来会有一个非常重要的地位。下面我来告诉你为什么。

函数式编程

JavaScript是第一个将函数式范式带给民众的语言(当然,Lisp第一个出现,但是大多数的程序员都没有使用它开发过一个可以作为产品的应用)。Lisp和Self,这两个深深影响了JavaScript的语言,充满了创新的理念,它们解放了我们的思想,去挖掘新的技术,模式和规范。这些都延续到了JavaScript上。看一下mondas, Church number, 或者甚至(作为更有实践性的例子)UnderscoreCollections functions,这些可以节约你一行又一行的代码。

动态对象以及原型继承

没有类(Classes),也没有无穷无尽的类层次结构的面向对象(Object-oriented)编程是提供了更快速的编程体验——只要创建对象,添加方法然后使用他们。更重要的是,它大大减少了维护时重构的成本,因为它允许程序员直接修改对象的实例,而不需要修改类。这种速度和灵活的方式为快速开发铺平了道路。

JavaScript就是互联网

JavaScript是因互联网而生的。它从一开始就出现了,并且伴随到现在。任何想要摧毁它的尝试都以失败而告终,比如Java Applets的衰落,VBScript被微软的TypeScript(它最终会被编译成JavaScript)所取代,以及Flash在手机市场以及HTML5上的一败涂地。如果想不破坏成千上万个Web页面而取代JavaScript是不可能的,所以我们接下来的目标应该是提高和完善它。这个工作,没有谁比ECMA的Technical Committee 39更适合了。

当然,JavaScript的替代者们每天都在诞生,比如CoffeeScriptTypeScript,以及成千上万能被编译成JavaScript的语言。这些替代者们在开发过程中也许是有用的(通过source maps),但是他们最终都不可能成功地代替JavaScript,两个主要原因:他们的社区永远不会比JavaScript更大,他们中的优秀特性会被ECMAScript(也就是JavaScript)所吸收。JavaScript不是汇编语言,它是一个你能理解代码的高级编程语言——所以你应该理解它。

端到端(End-to-End)JavaScript:Node.js和MongoDB

我们已经介绍了为什么要使用JavaScript。接着,我们来看看使用Node.js和MongoDB的理由。

NODE.JS

Node.js是一个搭建快速和可扩展的网络应用的平台——正如Node.js网站上所说。但是Node.js远不止这些:它是如今最火的JavaScript运行环境,被大量的应用和程序库所使用——甚至是浏览器的库代码也运行在Node.js上。更重要的是,这种服务器端的快速执行让程序员可以专注于更复杂的问题,比如做自然语言处理Natural。即使你并没有计划用Node.js来写你的服务器端应用,你也有可能使用基于Node.js的工具来改进你的开发流程。举例来说:用Bower来做前端包依赖管理,Mocha做单元测试,Grunt做自动化打包,甚至用Brachets做全文代码编辑。

因此,如果你正准备开发服务器端活客户端的JavaScript应用,你就需要对Node.js更加熟悉,因为你在日常工作中会需要他。有一些很有趣的代替的选择,但是它们中的任何一个的社区都不及Node.js的10%。

MONGODB

MongoDB是一个基于文档(Document-based)NoSQL数据库,它使用JavaScript作为它的查询语言(但是它不是用JavaScript写的),它完善了我们端到端的JavaScript平台。但是这个并不是我们选择MonoDB的主要原因。

MongoDB 是无模式的(schema-less),允许你以非常灵活的方式把对象持久化,因此能够迅速的应对需求变更。此外,它具有高度可扩展性,并且基于map-reduce,让它非常适合于大数据的应用。MongoDB如此灵活,以至于它既可以用作无模式的文档数据库,也可以用作关系数据存储(尽管它缺少事务,只能通过模拟来实现),甚至是用来缓存结果的键值对存储,就像MemcachedRedis

基于Express的服务器端组件化

服务器端的组件化开发一直不是一件容易的是。但是 Express(和Connect)带来了“中间件(middleware)的思想”。在我看来,中间件是服务器端定义组件最好的方式。如果你想找个熟悉的模式来对比一下的话,那它非常接近于管道和过滤器(pipes and filters)。

基本思想就是将你的组件作为管道的一部分。管道处理一个请求(也叫输入),生成一个结果(也叫输出),但是你的组件并不负责整个响应结果。相反,它只做它需要做的修改,然后将委派给下管道的下一节点。当管道的最后的节点处理完之后,这个结果再返回给客户端。

我们称这些管道的节点为中间件。很明显,我们可以创建两种类型的中间件:

  • 中间型(Intermediates)
    一个中间型节中间件理请求和响应,但是它不负全权责整个响应,而是继续将它们分派给下一个中间件。
  • 终结型(Finals)
    一个结束型中间件负责最终的响应结果。它对请求和响应进行处理,之后不会分派给下一个中间件。但实践中,继续分派给一个中间件可以给架构带来更高的灵活性(比如,之后需要增加其他的中间件),即使下一个中间件并不存在(这种情况下,结果会直接被传递到客户端)。

user-manager-500-opt

取一个具体的例子,假设服务器端有一个“用户管理”的组件。根据中间件的方式,我们最好能有终结型和中间型的中间件。对于终结节点,我们要有创建用户和列出用户的功能。但是在我们做这些操作之前,我们需要使用中间节点来做认证(因为我们不希望没有认证过的请求能进来,甚至创建用户)。一旦我们创建好了这些认证中间件,当我们想要把一个原先不需要认证的功能改变成认证功能的时候,我们只需要将这个中间件安插在相应的位置。

单页面(Single-Page)应用

当你使用全栈式JavaScript的时候,多数情况下你会专注开发单页面应用。大多数的Web开发者们都禁不住不止一次地尝试着着手于单页面应用。我已经创建了几个(多数为个人的),我相信他们就是Web应用的未来。你是否在移动链接上对比过单页面应用和通常的Web应用?他们在响应速度的差距有数十秒之多。

(注意:有些人可能不同意我的观点。比如Twitter,回滚了他们的单页面途径。与此同时,很多大的网站正在步入单页面时代,比如Zendesk。我已经看到足够的证据证明单页面应用带来的好处,并且对此深信不疑。但是具体还是因情况而异。)

如果单页面应用如此强大,那为什么还是要选择老土的方式来创建你的应用呢?我经常听到的一种争论就是他们担心SEO(Search Engine Optimization)。但是如果你对此做了正确的处理,这将不是一个问题:你可以有多种解决方式,从使用无界面的浏览器(headless browser),比如PhantomJS,在检测到网络爬虫的时候渲染HTML,到使用一些现有框架执行服务器端渲染

基于Backbones.js,Marionette和Twitter Bootstrap的客户端MV*模式

关于使用MV*框架开发单页面应用已经有太多的讨论了。尽管很难选择,但是我想说排名前三的是Backbone.js, EmberAngularJS

这三个都是非常被推崇的,但哪个是最适合你的

不幸的是,我必须得承认我在AngularJS上的经验有限,所以我就把它放在讨论范围之外。那么,Ember和Backbone.js代表了解决同一问题的两种不同方式。

Backbone.js很小,但是恰到好处的提供了创建一个简单的单页面应用所需要的功能。另一方面,Ember是一个创建单页面应用的完整且专业的框架。它有更多的辅助工具,但是也有更加陡峭的学习曲线。(你可以阅读更多关于Ember.js的内容。)

基于你的应用的大小,可以简单地通过比较“需要的功能”占“可用的功能”的比例来做出决定,它会给你很大的提示。

样式设计也同样是一个挑战,但是再次,我们也可以列举出一些可以助我们一臂之力的框架。对于CSS,Twitter Bootstrap是一个非常好的选择,它提供了一套完整的样式,它们可以立即使用,也非常便于自定义

Bootstrap是使用LESS语言创建的,它是开源的,我们可以根据我们的需要来修改它。伴随它的还有一大堆用户友好的组件,它们也有非常完善的文档。此外,一个定制化模式让你很方便地创建你自己的。毫无疑问,它正是这个工作所需要的正确的工具。

最佳实践:Grunt,Mocha,Chai,RequireJS 和 CoverJS

最后,我们将定义一些最佳实践,同时谈谈该如何实现和维护它们。具有代表性的,我的解决方案,最终聚焦到几个工具上,他们本身都是基于Node.js。

MOCHA 和 CHAI

这些工具能帮助你使用测试驱动开发模式(test-driven development)或者行为驱动开发模式(behavior-driven development)来改进你的开发流程,创建一些基础架构来管理你的单元测试,并且自动运行这些测试。

现在有大量的JavaScript单元测试框架,为什么要用Mocha?简短的回答就是它即灵活又完善。我来解释一下:

  • 用户界面(Interfaces)
    也许你习惯于测试驱动的程序组和单元测试的概念,又或许倾向于行为驱动测试的使用describle和should来定义行为定义的理念。Mocha让你可以同时使用这两种方式。
  • 报表生成器(reporter)
    运行你的测试代码会生成测试结果的报表,你可以使用各式各样的reporter来格式化这些结果。举例来说,如果你需要提供一个持续集成服务器信息,你可以找到一个report来做这些。
  • 没有指定断言库(Lack of an assertion library)
    这几乎不是一个问题,Mocha决定让你选择自己要使用的断言库,从而给你更多的灵活性。你有很多的选择,这正是Chai施展身手的地方。

Chai 是一个非常灵活的断言库,它可以让你使用如下三中主要断言方式的任何一种:

  • assert
    这是来自老派测试驱动开发的经典的assert方式。比如:

    1
    assert.equal(
    1
    var
    1
    iable,
    1
    "value"
    1
    );
  • expect
    这种链式的断言风格在行为驱动开发中最为常见。比如:

    1
    expect(
    1
    var
    1
    iable).to.equal(
    1
    "value"
    1
    );
  • should
    这也是用在测试驱动开发中,但是我更推荐expect,因为should经常听起来比较反复(比如,定义一个行为规范,”it (should do something…)”)。举例:

    1
    var
    1
    iable.should.equal(
    1
    "value"
    1
    );

Chai和Mocha可以无缝集成。使用这两个程序库,你可以使用测试驱动,行为驱动活任何想得到的方式来写你的测试代码。

GRUNT

Grunt是你能够自动化你的build任务,包含简单的复制粘贴和文件拼接,模板预编译,style语言(SASS和LESS)编译,单元测试(使用Mocha),代码检查,以及代码最小化(比如,使用UglifyJS或者Closure Compiler)。你可以添加你自己的自动化任务到Grunt中或者搜索registry,那里数百个插件可供使用(再次提醒,选择使用有良好的社区支持的工具)。Grunt也可以监控你的文件,当发生更改时触发一些操作。

REQUIREJS

RequireJS 听起来是基于AMD API的另一种加载模块的方式,但是我敢保证地告诉你,它远远不止这个功能。使用RequireJS,你可以定义你的模块之间的依赖和层次结构,让RequireJS库帮你来加载他们。它还提供了一种非常简便的方式来避免全局变量污染,通过在函数体中定义你的模块。这让模块可以重用,不像命名空间模块(namespaced modules)。试想一下:如果定义了一个类似于Demoapp.helloWorlModule的模块,你想把他改成Firstapp.helloWorldModule,那么你需要把所有引用到Demoapp命名空间的地方都做修改,才能让它变得可移植。

RequireJS还能让你拥抱依赖注入模式。假设你有一个模块需要用到主应用对象(单例)的一个实例。通过使用RequireJS,你意识到你不需要使用全局变量来存储它,你也不能使用一个实例作为RequireJS的依赖。所以,你需要在你的模块构造器中加载这个依赖。让我们看一个例子:

在main.js:

define(
["App","module"],
function(App, Module){
var app = new App();
var module = new Module({
app: app
})
return app;
}
);

在module.js

define([],
function(){
var module = function(options){
this.app = options.app;
};
module.prototype.useApp = function(){
this.app.performAction();
};
return module
}
);

注意,我们不能在module的定义中加入对main.js的依赖,否则我们会创建出一个循环引用。

COVERJS

代码覆盖率(Code coverage)是你测试的一个度量标准。正如它的名字所示,它能告诉你当前的测试集覆盖了你代码的多少部分。CoverJS通过检测你代码中的语句(而不是像JSCoverage那样看代码行)并生成一个检测过的版本的代码来测量你的测试代码的覆盖率。它也可以支持对持续集成服务器提供持续报表生成。

总结

全栈式JavaScript并不能解决所有的问题。但是它的社区和技术会带领你走很长一段路。使用JavaScript,你可以创建基于统一的语言的可扩展的,可维护的应用。毫无疑问,这是绝对值得我们关注的。