使用Backbone.js, ExpressJS, node.js, MongoDB的web应用样板工程

这是web应用的一个完整的样板工程,用到了Backbone.js & Marionette, node.js & ExpressJS, MongoDB & Moogoose, Handlebars, Grunt.js, Bower和Browserify!
我建立了一个超级基础的单页面应用程序(SPA),就是一个简单的通讯录管理器, 但麻雀虽小,五脏俱 全。我写这篇文章的目的是介绍一下这个应用所用到的整个技术栈:后端,数据,前端,工具和测试。主 要包括下面这些技术:
  • 后端: node.js和ExpressJS
    • 测试: Mocha,Chai, Sinon,Proxyquire
  • 数据层: MongoDB和Mongoose
  • 工具:Grunt,Bower和Browserify
  • 前端:Backbone.js和Marionette.js
    • 测试:Jasmine,Kama和PhantomJS
我会详细介绍组成上述完整技术栈的各个部分。但要说明一下,因为这是一个非常简单的应用,我不会花 太多时间来讲用户界面的细节以及应用是如何工作的,因为这都相当明显(我也不会花太多时间在具体的 实现上,因为这不是本应用的核心价值)。相反,我会非常详细地说明搭建应用过程中的各种工作以及使 用工具的工作流程。
本文的终极目标是为将来任何的新项目提供一套可以作为初始模板的基础代码。代码应该是能够工作且功 能完善,但也要易于消化吸收。最终,如果在读完本文后能够对背后所有的东西都有一个深入的理解,你 就能够通过简单的克隆这个代码库来启动你自己的应用!
在本文的下面部分,你将看到:
  • Part 0:工程设置和依赖
  • Part 1:后端-一个简单的node.js和ExpressJS服务器
    • node.js - 初始设置
    • ExpressJS - 路由/控制器
    • Handlebars - 用于渲染页面、布局的模板库
    • 数据层 - Mongoose和MongoDB:
      • MongoDB - 启动服务以及如何在Express中工作
      • Mongoose - 数据模型的Schema定义
      • Seeder - 应用第一次运行时如何构造数据
    • 路由和控制器 - 前端调用的API层
    • 使用Mocha,Chai,Sinon和Proxyquire来测试
  • Part 2:开发工具(Grunt,Bower,Browserfy,TDD)
    • Grunt - 做所有的事情!
      • 初始化配置(加载grunt task)
      • 清理
      • Bower 安装
      • Browserify(vendor, shim依赖和文件)
      • Less 编译
      • Handlebars(通过Browserify进行预编译/转换)
      • Concatenation/Minification和Uglification/Copying
      • 监视器(前后端的重新构建)
      • Karma 命令/监视器
      • jshint
      • 并发任务
      • 分解命令- init:dev, build:dev, build:prod, tdd, test:client, etc.
    • Bower - 管理项目的前端依赖
      • bower.json已经很清楚了
    • Karma/Jasmine - 简单解释一下TDD vs test:client(single run)
  • Part 3: 前端 - 通讯录应用,使用Backbone和Marionette
    • Browerify - 为什么用Browserify和它的优点
    • Backbone/Underscore和Marionette
    • 通讯录应用 - 介绍及简单解释
      • 核心:
        • Marionette
        • 连接到数据层来缓存数据
        • 路由和控制器设置
        • 启动!
      • 视图:
        • Marionette.ItemView(与Backbone.View对比)
        • Handlebar预编译模板
      • 模型/集合
      • 控制器 - 调用API
    • TDD,使用Karma和Jasmine
  • Part 4: 用Heroku来部署!

Part 0: 工程设置和依赖

在开始之前,先确定你已经拥有这个工程并且可以运行它。首先,你需要一份工程的拷贝。最简单的方法 是使用Git通过克隆的方式下载一份拷贝:
$ git clone git@github.com:jkat98/benm.git
如果你没有安装Git或者不熟悉这个概念,我强烈要求你读一些入门文章(我写了一篇快速入门可能会有帮助)。
一旦克隆了工程到你的本地目录,你首先要确认一些基本需求。

安装node.js和npm

如果还没有安装node.js(和npm),去http://nodejs.org安装它(npm包含在一个标准的node.js安装包中 )。一定要下载最新的稳定版本(写这篇文章时版本是v0.10.24)。
安装之后,确定它能否工作的最简单的办法是打开一个终端或者命令行提示符,输入:
$ node --version
假设它工作正常,你应该能够看到输出“0.10.24”或者任何你安装的版本号。

安装MongoDB服务器

接下来你要确认MongoDB服务器已经安装在你的机器上。安装过程很简单,很像node.js。你可以通过访问 MongoDB官网的下载页面(http://www.mongodb.org/downloads)找到合适的下载安装文件。请一定要阅读 一下安装/入门文档,因为除了安装还有一些额外的步骤(比如,创建、设置数据目录,等等)
注解:安装MongoDB不是必须的,但是这样的话,本项目中的一大部分就无法工作,除非手工做一些修改将 MongoDB/Mongoose部分移除。

安装Grunt CLI

这个项目必须的最后一个工具是Grunt命令行,通过npm安装也非常容易:
$ npm install -g grunt-cli
注释:npm带有-g参数的安装将会是全局安装,意味着在你机器上安装的package是到处可用的。没有-g, package只会安装到你正在工作的本地特定工程或者代码仓库中(保存在工程中一个叫node_modules的目录 中)。
Mac用户:全局安装可能需要sudo才能正确安装。

运行app!

这样,如果上面的工作都成功了,现在你可以准备启动这个web应用,然后带她去兜风了!首先你需要安装 工程的所有依赖(下一节会有更详细的说明),然后用Grunt.js来初始化工程并启动服务:
$ npm install
$ grunt init:dev
$ grunt server
再一次假设一切工作正常,Grunt应该报告服务已经启动了。你应该看到大量的日志输出,包括MongoDB服 务器的连接,以及一些seed数据的插入。让你的浏览器访问http://localhost:3300,app应该已经启动并 运行了!随便尝试一个操作,点一个联系人来查看详细信息,添加一个联系人,等等。
不必担心上面刚刚发生的各种疯狂的事情  - 我们将覆盖所有的细节!

Part 1:后端 - 一个简单的node.js和ExpressJS服务器

我们的服务器,运行在node.js上,像其他web服务器一样有两个主要目的。第一个是它应该处理从浏览器 过来的请求,然后返回各种被请求的文件。这些文件一般分为两类:静态文件(JavaScript,CSS,HTML, 图片,等等)和动态文件(服务器根据模板生成的HTML)。由于我们的app是一个富客户端应用,因此服务 器端的模板就不是必须的了(我们只需要返回一个静态html文件)。但是工程中还是包括了对Handlebars 模板的支持,因此这仍旧是一个完整的代码基础。假设万一你决定建立并拥有一个比较传统的网站(比如 ,不是Backbone单页面前端应用),你只需通过在后端使用Handlebars模板就可以很容易地达到目的。
服务器应该做的第二类事情是与数据层通信将用户的session数据持久化。(比如,你与一个app进行交互 ,关掉浏览器,重新回来后你以前被保存的工作可以再次展现给你)这是MongoDB的价值所在 - 一个noSQL 数据库,主要使用JSON来存储数据。

Package.json - 工程需要的所有东西都在这儿:

在我们准备写服务器代码之前,首先要确保服务器所需的所有东西都已经存在于开发环境之中。达到这个 目的最简单的方法是将一个基本的package.json放在工程中,文件中包含一个服务端依赖的package列表。 由你来决定工程需要什么,通过npm安装他们,package.json文件会随着这些新依赖的加入而逐渐变大。
让我们一起来看看这个文件并解释一下到底发生了什么:
{
  "name": "myapp",
  "description": "Boilerplate web app using node, express, mongodb, backbone, marionette. Tooling includes Grunt, Bower, Browserify, etc.",
  "version": "0.0.1",
  "author": "Jason Krol",
  "repository": {
    "type": "git",
    "url": "https://github.com/jkat98/benm.git"
  },
1. 上面最开始的部分包含了标准的项目描述。非常的不言自明。
  "engines": {
    "node": "0.10.x"
  },
2.engines项列出了运行工程所必须的运行时,很明显这里是node.js和它的版本
  "scripts": {
    "start": "node server.js"
  },
3.scripts说明了用“run“来启动工程、服务器、应用或者不管是什么,真正运行的是什么命令
  "dependencies": {
    "express": "latest",
    "mongoose": "~3.8.3",
    "handlebars-runtime": "~1.0.12",
    "express3-handlebars": "~0.5.0",
    "MD5": "~1.2.0"
  },
4.dependencies列举了工程要正确运行所需的最小依赖。没有这些,服务器不会运行。
  "devDependencies": {
      "bower": "~1.2.8",
      "grunt": "~0.4.1",
      "grunt-contrib-concat": "~0.3.0",
      "grunt-contrib-jshint": "~0.7.2",
      "grunt-contrib-uglify": "~0.2.7",
      "grunt-bower-task": "~0.3.4",
      "grunt-nodemon": "~0.1.2",
      "karma-script-launcher": "~0.1.0",
      "karma-chrome-launcher": "~0.1.1",
      "karma-html2js-preprocessor": "~0.1.0",
      "karma-firefox-launcher": "~0.1.2",
      "karma-jasmine": "~0.1.4",
      "karma-requirejs": "~0.2.0",
      "karma-coffee-preprocessor": "~0.1.1",
      "karma-phantomjs-launcher": "~0.1.1",
      "karma": "~0.10.8",
      "grunt-contrib-copy": "~0.4.1",
      "grunt-contrib-clean": "~0.5.0",
      "browserify": "2.36.1",
      "grunt-browserify": "~1.3.0",
      "load-grunt-tasks": "~0.2.0",
      "time-grunt": "~0.2.3",
      "grunt-contrib-watch": "~0.5.3",
      "grunt-concurrent": "~0.4.2",
      "grunt-karma": "~0.6.2",
      "grunt-contrib-less": "~0.8.3",
      "grunt-contrib-handlebars": "~0.6.0",
      "grunt-contrib-cssmin": "~0.7.0",
      "hbsfy": "~1.0.0",
      "grunt-shell-spawn": "~0.3.0",
      "chai": "~1.9.0",
      "sinon": "~1.8.1",
      "sinon-chai": "~2.5.0",
      "grunt-simple-mocha": "~0.4.0",
      "proxyquire": "~0.5.2"
    }
}
5.devDependencies是项目在开发过程中的依赖列表。这个列表很大,大部分是Grunt的插件(稍后会解释 ),还包括Bower,Browserify,Karma,等等。就是说,这是一个在项目开发时会用到的所有东西的列表 ,而不是工程运行时需要的。
这样,将package.json配置好后,下一步就是执行一个简单的npm安装命令来安装好所有依赖并准备好与我 们的服务器共舞:
$ npm install
因为要下载相当多的模块,所以这可能会花几分钟的时间。说明一下,你有可能在Part 0让工程启动并运 行时已经完成了这一步。
如果你是开始一个新项目,配置package.json最简单的方式是执行一个npm init命令,它将问你几个简单问题然后帮你生成一个package.json文件:
$ npm init
或许可以在每个提示后直接按回车键,前提是你对一个基本是空的但完整的package.json文件还满意的话。

用装有ExpressJS和Mongoose的node.js作为服务器:

让我们看看作为主服务器的node.js的一些非常基本的代码。我们让ExpressJS做它擅长的事情,来处理底 层很多不太需要关心的事情,这样我们就可以聚焦于配置和建立API路由给前端app来用。
根目录下的大部分文件夹属于node.js服务器,包括app、controllers、views和views/layouts。全部前端 代码都在client目录下,public目录用于提供对外服务。通常,任何在public或者views目录下的内容都是 浏览器可见的,其它都不可见。
---- app
---- controllers
---- views
-------- layouts
---- public
-------- js
-------- css
---- client
-------- requires
-------- spec
-------- src
-------- styles
-------- templates
---- spec
-------- app
-------- controllers
最核心的服务器文件叫做server.js,它包含了一堆node.js代码(至少包括启动代码)。我们来看一下这 个文件:
var express = require('express'),
    http = require('http'),
    path = require('path'),
    routes = require('./app/routes'),
    exphbs = require('express3-handlebars'),
    mongoose = require('mongoose'),
    seeder = require('./app/seeder'),
    app = express();
在这里我们通过node的require方法引入了一堆模块。传递给require()的模块,如果没有./或者../,表示 是通过已安装的或者node.js内置的模块被加载的(例如:http是node.js内核的一部分,express是通过 npm安装的)。requires()中包含./或者../的是应用程序自己的模块,我们会稍微说明一下。
现在我们有一堆可以使用的模块了,那我们就看一下启动服务器所需的ExpressJS基本配置:
app.set('port', process.env.PORT || 3300);
// ...

app.use(express.logger('dev'));
app.use(express.json());
app.use(express.urlencoded());
app.use(express.methodOverride());
app.use(express.cookieParser('some-secret-value-here'));
app.use(app.router);
app.use('/', express.static(path.join(__dirname, 'public')));

// development only
if ('development' == app.get('env')) {
    app.use(express.errorHandler());
}
上面的代码是很标准的ExressJS配置。每一个app.use都是加载一种ExpressJS中间件,它们都是基本插件 ,用于处理一些不需要我们操心的事情!最后一个app.use()(在//development only注释之前)是设置 public目录为静态目录,意味着Express将原封不动地提供这些文件(不会对这些文件做任何修改就返回给 客户端)。

用于提供动态HTML页面的Handlebars模板

app.set('views', __dirname + '/views');
app.engine('handlebars', exphbs({
    defaultLayout: 'main',
    layoutsDir: app.get('views') + '/layouts'
}));
app.set('view engine', 'handlebars');
我们在用Handlebars初始化视图引擎并将它指向views目录。很有可能你会碰到在这里用Jade的情况,因为 它是一个很流行的模板引擎,我很喜欢Jade(特别是精炼的HTML语法让它看起来像Zen代码一样)。但是, 这里我想用Handlebars,因为这样可以保持前后端的模板语言的统一。服务器端的Handlebars模板保存在 views目录中,对应的布局保存在layouts目录中。在后端使用Handlebars的语法跟前端的情况非常类似。

使用Mongoose和MongoDB作为数据层:

目前为止,一切正常,要做的最后一件事情是包含一个数据库服务器连接。这个项目我们用的数据库是 MongoDB - 一个noSQL、基于JSON的数据存储系统。在我看来,像这样一个全部基于JavaScript的项目使用 MongoDB几乎就是不需要过脑子的事情,非常明显的选择。当然这因人而异。
MongoDB入门
如果你不熟悉MongoDB,下面就是一个速成教程。假设你已经在电脑上安装了MongoDB,你可以在任何终端 或者命令行简单敲击mongod命令来启动服务:
$mongod
在另一个终端或者命令行中,使用mongo命令进入MongoDB shell:
$mongo
进入了MongoDB shell后,命令行提示符会改变。几个常用命令:
show dbs - 显示系统中可用的数据库列表
use databasename - 切换到已经存在的数据库(或者如果不存在就新建它)
db.things.find() - 列出当前活动数据库(用上面‘use’设置的)中“things”collection中的所有记
db.things.insert({a: 1, b: 2 c: 3}) - 将参数对象插入到“things”collection中作为一条新记录
db.things.find({a: 1}) - 返回一个含有属性a且其值为‘1’的记录列表
//connect to the db server:
mongoose.connect('mongodb://localhost/MyApp');
mongoose.connection.on('open', function() {
    console.log("Connected to Mongoose...");

    // check if the db is empty, if so seed it with some contacts:
    seeder.check();
});
我们用于连接MongoDB的node.js驱动是Mongoose。有许多类似的用于MongoDB的node.js驱动,但我喜欢 Mongoose,因为它有可以用来与model一起工作的schema。(从一个有“代码优先”的.Net程序员的背景来 看,这是我学习node时最直接的一个比较)在server.js文件中我们只需要包含一小段代码来使用Mongoose 驱动连接MongoDB。注意,用于连接MongoDB的url是“mongodb://localhost/”,后面跟着要连接的数据库 名。MongoDB的一个好处是如果要连接的数据库不存在,它会自动创建一个!
一旦连接建立起来,我们会执行一个简单的seeder check函数来检查是否是第一次运行app - 如果是的话 ,它会将一些记录扔进Contacts collection中(就是“表”),所以第一次运行的时候app也不是完全没 记录的。这不是必须的,但会给初次使用带来好的第一印象。

Seeder模块:

seeder对象真是node.js的一个伟大之举。如果回到server.js最开头的部分,你会回忆起我们包含了一堆 require()语句。在这里我们将模块引入了node.js,不管是node本身的,或者用npm安装的,甚或就是我们 自己的,引入后就可以在代码中像普通JavaScript变量一样来使用它们。我们一起来看一下seeder模块都 做了些什么:
var mongoose = require('mongoose'),
    models = require('./models'),
    md5 = require('MD5');

module.exports = {
    check: function() {
        // ...
这里我们又一次包含了几个require(),但这次少了一些,只是由于这是一个特别的模块,只需做它分内的 事情,有较少的依赖;换句话说,只有Mongoose、数据模型和MD5(MD5只用来在Gravatar处理图像时生成 一个hash值)最重要的是module.exports这一行 - 导出了一个JavaScript对象,其中含有一个名叫 ‘check’的属性方法。Check()会检查Contacts collection中是否含有任何记录,如果没有,它会首先插 入一些模拟数据。

API路由和控制器:

//routes list:
routes.initialize(app);

//finally boot up the server:
http.createServer(app).listen(app.get('port'), function() {
    console.log('Server up: http://localhost:' + app.get('port'));
});
我们的server code现在终于配置完成了,并且连接到了数据库服务器,要做的最后一件事是配好路由然后 启动服务!
var home = require('../controllers/home'),
    contacts = require('../controllers/contacts');

module.exports.initialize = function(app) {
    app.get('/', home.index);
    app.get('/api/contacts', contacts.index);
    app.get('/api/contacts/:id', contacts.getById);
    app.post('/api/contacts', contacts.add);
    app.put('/api/contacts', contacts.update);
};
module.exports = {
    index: function(req, res) {
        res.render('index');
    }
};
这个工程中我们在路由和控制器之间用了1:1的关系,所以如果你看一下app目录中的route.js文件,你会 看到里面非常简单。字面上仅仅定义了API层的访问点,用于响应从前端过来的请求。每一条路由的真正实 现定义在控制器中,可以在controller/*.js中找到。此外,注意route.js文件最上面的require(),通过 引用../controllers/file(没有.js)引入了我们自己的模块。要注意的一点是我们的home控制器只返回 一个含有index属性的对象,这个属性会对Handlebars模板进行渲染。contacts控制器返回纯JSON结果 -  根据URL请求的不同,结果也会不一样。
最重要的控制器是contacts控制器(controllers/contacts.js),它用于前端获取初始联系人列表,返回 每个联系人的详细信息,以及添加新联系人,更新已经存在的联系人。contact.js中的每一个方法主要是 通过Mongoose来对MongoDB进行插入、修改、查询的。

数据模型:

服务器代码最后一部分是数据模型。我们的数据模型是定义了数据schema的对象。如果你看一下 app/models.js文件,你会在最上头看到一些requires()(尤其是Mongoose,Schema和ObjectId - 都与 Mongoose有关)。我们定义了Contact模型,也就是一个新的数据schema和新定义的属性(以及类型),最 终导出一个对象,包含有Contact的模型定义。这种方式可以定义项目的所有数据schema,然后通过主 “models”模块返回。
假设除了联系人之外,我们还想保存一些喜欢的书籍。在model.js中,我们要声明另一个变量Book,让它 =new Schema({}),在这个对象内部定义每本书的属性(例如,标题,作者,描述,ISBN,等等)。然后在 moduel.exports里Contact:行之后增加一个逗号然后添加Book:mongoose.model('Book',Book); 这样,所 有包含有var models=require('./models')的地方将能够立刻得到Book模型。你可以不必这样在一个文件/ 模块中包含所有的模型 - 这只是我在这个工程的做法(可能对大多数项目也可以这样,除非你有大量的模 型)。
var mongoose = require('mongoose'),
    Schema = mongoose.Schema,
    ObjectId = Schema.ObjectId;

var Contact = new Schema({
    email:      { type: String },
    name: {
        first:  { type: String },
        last:   { type: String }
    },
    phone:      { type: String },
    gravatar:   { type: String }
});

module.exports = {
    Contact: mongoose.model('Contact', Contact)
};
我们的整个服务端代码只包含6个小js文件,大多数里面只有几行代码。他们一起支撑起一个全功能的,具 有数据库连接和CRUD逻辑的web服务器。

用Mocha,Chai,Sinon和Proxyquire测试node

在服务器端我们用了几种不同的工具来处理node代码的测试问题。主要是用Mocha,结合Grunt mocha  runner来执行测试。测试例本身是用Chai写的,其本身就是断言语言, Sinon用于监视和打桩,如果需要 在node世界中造一点”假”的话,Proxyquire会大有用武之地。
要运行测试例,执行下面的命令:
$grunt test:server
这会执行mocha runner,在spec目录里的所有测试都会被跑到。在终端中你应该能够看到每一个测试例的 输出,希望列表中都是绿色的对勾。
测试例按照与app本身相似的结构进行组织,有一个app和controllers目录。
要想了解更多有关node.js测试的内容,参阅我的另一篇博文,里面有更多细节。

Part2: 开发工具(Bower,Grunt,Browserify)

现在服务器部分已经讲完了,在我们继续讲前端app之前,我想重温一下开发过程中用到的工具套件。

Bower - 前端依赖管理

Bower是一个伟大的小工具!相当于node中的npm。使用Bower你可以快速方便地安装工程所需的前端依赖。 通常,在过去,当我们要用到像jQuery,underscore,Backboen.js这些类库时,总是去他们的网站或者 GitHub仓库(千篇一律的),找到下载链接,保存一份.js文件的拷贝到工程中的某处,然后在主布局文件 中增加一行对那个文件的引用。有了Bower,整个过程简化了许多。
像node那样,Bower依赖于它自己的bower.json文件,这非常类似于已有的package.json(上面讲过了)文 件。
{
  "name": "myapp",
  "version": "0.0.1",
  "private": true,
  "dependencies": {
    "backbone.marionette": "~1.4.1",
    "jquery": "~1.10.2",
    "underscore": "~1.5.2",
    "backbone": "~1.1.0"
  },
  "devDependencies": {
    "jasmine": "~1.3.1"
  },
  "exportsOverride": {
    "jquery": {
      "js": "jquery.js"
    },
    "underscore": {
      "js": "underscore.js"
    },
    "backbone": {
      "js": "backbone.js"
    },
    "backbone.marionette": {
      "js": "lib/backbone.marionette.js"
    },
    "jasmine": {}
  }
}
dependencies和devDepandencies两部分都有定义。有了这些定义,你可以通过简单执行‘bower install ’,就像‘npm install’那样,它将会把在bower.json文件中预定义的文件下载下来(不需要挨个手工安 装)。
exportsOverride部分简单定义了当从‘bower_components’安装到‘client/requires’目录时,如何组 织这些文件。注意到这里没有Jasmine的定义,说明它在客户端require阶段不会被拷贝。这是由于app运行 在前端,而客户端不需要加载Jasmine库,它只是在本地开发阶段做测试用的。
如果你要从头开始一个项目并且想在工程中引入jQuery,通过Bower能够很方便地安装:
$ bower install jquery
同时,假设你也需要Backbone.js和underscore.js:
$ bower install backbone
$ bower install upderscore
然后,你需要做的就是在你的主布局文件中增加一个到bower_components/jquery/jquery.js的引用!
当然,如果能再简单一些就更好了...

Grunt.js - 全能的任务执行者

Grunt.js非常狗屎,它可用插件的总量十分庞大。在这种情况下,如果你没有明确为什么要用这个插件, 那理解和使用Grunt会非常令人困惑。
让我们后退一步来看一下如果没有自动化任务(类似于Grunt)来协助的话,典型的开发流程会是什么样:
你用自己喜欢的方式将前端文件都组织好了。可能由于各种原因你有非常多的文件 - Backbone模型、视图、集合、路由、控制器,等等。此外,你决定使用LESS来写CSS - 因为你很潮而且大家都在用它。作为一名优秀的程序员,你最终希望工程中所有.js文件都合并到一个单独的文件,最好压缩过(并进行丑化,这样别人就无法搞懂你的源码也就不能偷走你的工作了)。很明显,LESS文件不能原样地返回给浏览器 - 它们也需要转换为普通的.css文件。此外,你践行TDD,希望在每次应用的代码或者测试代码本身发生变化时能定期测试前端代码。不仅如此,你需要真正启动node.js服务器这样你才能在本地浏览器上看到你的app - 每次你修改node代码时,你需要重启服务器。最后一点 - 上面这些只是为了执行app的一个独立测试,你将需要在每次修改代码时都重复上面大多数步骤。
哦噢,有太多烦心事了!这就是Grunt的用武之地。用Grunt你可以创建一个自动化”脚本“,他会在每次你编辑文件或者做任何修改时自动执行。将这个流程自动化后,执行”grunt server“这样的命令会做下列的事情:
  • 用Bower安装任何前端依赖
  • 合并所有的.js文件到一个单独的文件(包括那些Bower依赖)
  • 压缩(最小化)这个单独的.js文件并丑化它
  • 编译LESS文件为单独的.css文件
  • 复制这两个文件到一个public目录,以便你应用的index.html文件可以引用到它们
  • 启动Karma并执行测试套
  • 启动node服务器
  • 每次在任何有关的文件变化时做上述事情
    • 修改.js文件 - 重新执行合并、压缩和复制。测试也要重新执行。
    • 修改.LESS文件 - 重新执行编译和复制。
    • 修改服务器相关的node.js文件 - 重启node服务器
  • 此外,你也可以运行任何其他的便捷任务,比如jsHint,这样就能够立刻检查所有JavaScript文件的语法错误了。
现在,一个典型的开发流程看起来应该是这样:
  • 启动”grunt server“。
  • 开始工作。
  • 刷新浏览器*
*注:你甚至可以更有想象力一些,使用Live reload插件,这样就不需要最后一步了!
很了不起吧?!你担心的所有乱七八糟的事情都可以抛到窗户外面并忘记了,所有的这些让网站开发走了一条非同寻常的路!
我们看一下Gruntfile.js文件。提示 - 它很大,所以我们把它拆成一块一块来分析。
module.exports = function(grunt) {
    require('time-grunt')(grunt);
    require('load-grunt-tasks')(grunt);

    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
这是Gruntfile最基础的部分。它基本就是用了node的module.exports来返回一个Grunt函数,然后你可以传入一个配置对象给initConfig来启动它。前面两行require了两个很方便的Grunt插件,最重要的一个是load-grunt-tasks。通常对于Gruntfile,你需要手工指定使用一个插件要完成什么任务。这个方便的小插件通过读取package.json文件并查找和加载在dependencies及devDependencies列表中以“grunt-”开头的插件来帮你自动实现这个目标!这让配置文件的很大一部分是不必要的了。
下一步,我们运行Grunt的initConfig函数并配置整个脚本。第一行读取主package.json文件以便后面使用 - 具体说,我们会从package文件中拿到工程的名称,用它来命名在构建过程中生成的一些文件。

Bower安装

去读bower.json文件并安装任何前端依赖。
bower: {
    install: {
        options: {
            targetDir: 'client/requires',
            layout: 'byComponent'
        }
    }
},

Clean

在每次构建之前都要清空我们维护的任何临时构建目录(这样不会到处都是残留文件)。‘build’命令清空整个‘build’目录。‘dev’命令清空指定的一系列文件(例如,因为通常不会变化而不必删除vendor.js文件)。最后,‘prod’命令会清空一个叫做‘dist’的目录,在app准备发布时这个目录用于将其分发。(这个文件夹通常只包含2个关键文件,myapp.js和myapp.css - 再加上必要的图片文件)
clean: {
     build: ['build'],
     dev: {
         src: ['build/app.js', 'build/<%= pkg.name %>.css', 'build/<%= pkg.name %>.js']
     },
     prod: ['dist']
},

Browserify

这可是个大头 - 不可小觑。Browserify是一个很神奇的工具,基本上它能让你在前端代码中就像在后端的node.js中一样使用模块。这意味着你可以在整个技术栈使用相同的编程习惯和风格。Browserify允许你使用模块化的方式搭建前端代码,只在你需要的时候引入依赖。此外,Grunt的Browserify插件会将app和第三方文件分别合并到一个单独的文件。(这样,如果你有5个第三方文件和20个app文件,你将只会得到一个vendor.js文件和一个app.js文件。)
Browserify的配置可以拆分为3个主要部分:vendor,app和test。Vendor部分从Bower中得到所有的前端依赖,Browserify他们然后将其合并为一个单独的vendor.js文件。App部分为你核心的app文件做同样的事情。注意到在app部分,我们只定义了main.js(并不是我们所有的.js文件)。这是由Browserify的工作方式决定的,它通常会遍历整个app,在每次发现require()的时候加载必需的依赖。node.js几乎以相同的方式工作。
另外,你可以shim第三方类库,定义他们自己的依赖。因此,例如,对于Marionette,你可以定义它依赖jQuery、Backbone和underscore。这样,在你前端的app.js文件里,你只需要require('backbone.marionette'),它会自动包含jQuery、underscore和Backbone,也不需要你手工require它们。
最后,Browserify会自动搜索前端Handlebars模板中的所有requires()并自动将其编译为JavaScript函数。这会非常显著地为用户加速前端的渲染速度,因为视图模板不必在运行时渲染了(而这正是Handlebars通常的工作方式)。
browserify: {
    vendor: {
        src: ['client/requires/**/*.js'],
        dest: 'build/vendor.js',
        options: {
            shim: {
                jquery: {
                    path: 'client/requires/jquery/js/jquery.js',
                    exports: '$'
                },
                underscore: {
                    path: 'client/requires/underscore/js/underscore.js',
                    exports: '_'
                },
                backbone: {
                    path: 'client/requires/backbone/js/backbone.js',
                    exports: 'Backbone',
                    depends: {
                        underscore: 'underscore'
                    }
                },
                'backbone.marionette': {
                    path: 'client/requires/backbone.marionette/js/backbone.marionette.js',
                    exports: 'Marionette',
                    depends: {
                        jquery: '$',
                        backbone: 'Backbone',
                        underscore: '_'
                    }
                }
            }
        }
    },
    app: {
        files: {
            'build/app.js': ['client/src/main.js']
        },
        options: {
            transform: ['hbsfy'],
            external: ['jquery', 'underscore', 'backbone', 'backbone.marionette']
        }
    },
    test: {
        files: {
            'build/tests.js': [
                'client/spec/**/*.test.js'
            ]
        },
        options: {
            transform: ['hbsfy'],
            external: ['jquery', 'underscore', 'backbone', 'backbone.marionette']
        }
    }
},

LESS

将所有.less文件编译为.css文件并将.css文件命名为'myapp'.css放到构建目录中。这个文件不仅包含所有.less文件,而且还有其它存在并被第三方类库(jQueryUI,reset.css等等)依赖的.css文件。
less: {
    transpile: {
        files: {
            'build/<%= pkg.name %>.css': [
                'client/styles/reset.css',
                'client/requires/*/css/*',
                'client/styles/less/main.less'
            ]
        }
    }
},

concat

将所有.js文件合并为一个单独的文件。拿到vendor.js和app.js文件(都由Browserify生成)并合并为一个单独的'myapp'.js文件,将最终生成的这个文件放到构建目录中。
concat: {
    'build/<%= pkg.name %>.js': ['build/vendor.js', 'build/app.js']
},

copy

当我们在开发模式下运行Grunt服务时,从构建目录复制文件到它们应该在的目录,这样前端app才能够看到它们,后端服务器也能够用其提供服务。.js、.css和图片文件分别放在不同的目录中(看一下server/views/layout/里面的主.handlebars布局文件,你会看到最终的.js和.css文件放在布局文件的什么地方)
copy: {
    dev: {
        files: [{
            src: 'build/<%= pkg.name %>.js',
            dest: 'server/public/js/<%= pkg.name %>.js'
        }, {
            src: 'build/<%= pkg.name %>.css',
            dest: 'server/public/css/<%= pkg.name %>.css'
        }, {
            src: 'client/img/*',
            dest: 'server/public/img/'
        }]
    },
    prod: {
        files: [{
            src: ['client/img/*'],
            dest: 'dist/img/'
        }]
    }
},

cssmin

从.css文件中删除所有的空白字符并尽可能地压缩。默认的,这只在生产环境中设置(因为在开发时,最好还是能够直接在浏览器中查看源码来检阅最终的.css文件)。
cssmin: {
    minify: {
        src: ['build/<%= pkg.name %>.css'],
        dest: 'dist/css/<%= pkg.name %>.css'
    }
},

uglify

类似的对于我们的主.js文件,删除任何空白字符,修剪变量名称和注释。在能正常工作的前提下让文件尽量小。再一次,这只是用于生产环境(因为在开发时,当在浏览器中调试时,我们仍旧希望能够读取.js文件)。
uglify: {
    compile: {
        options: {
            compress: true,
            verbose: true
        },
        files: [{
            src: 'build/<%= pkg.name %>.js',
            dest: 'dist/js/<%= pkg.name %>.js'
        }]
    }
},

watch

随时监控文件变化。当它们变化时,执行预定义好的Grunt任务(上面大都讲过了)。被监控的client/src/*.js文件会重新运行Browserify任务,被监控的.less文件会重新运行编译步骤,然后进行必要的复制,在每个文件被修改时重复上述步骤。
watch: {
    scripts: {
        files: ['client/templates/*.hbs', 'client/src/**/*.js'],
        tasks: ['clean:dev', 'browserify:app', 'concat', 'copy:dev']
    },
    less: {
        files: ['client/styles/**/*.less'],
        tasks: ['less:transpile', 'copy:dev']
    },
    test: {
        files: ['build/app.js', 'client/spec/**/*.test.js'],
        tasks: ['browserify:test']
    },
    karma: {
        files: ['build/tests.js'],
        tasks: ['jshint:test', 'karma:watcher:run']
    }
},

nodemon

像watch一样,除了几个相关的.js文件,每次服务器端node.js文件变化时,重启服务器,这样最新版本就能得到执行。
nodemon: {
    dev: {
        options: {
            file: 'server/server.js',
            nodeArgs: ['--debug'],
            watchedFolders: ['server/controllers', 'server/app'],
            env: {
                PORT: '3300'
            }
        }
    }
}

shell

这只是简单执行一个命令行。具体点,就是每次在主服务器启动时执行‘mongod’命令(因为app要正常工作它们两个必须同时运行)。
shell: {
    mongo: {
        command: 'mongod',
        options: {
            async: true
        }
    }
},

concurrent

并发意味着你能够同时异步地执行多个“阻塞”任务。对于开发环境,这些任务包括监控服务器的nodemon,数据库的mongod和监控前端脚本、less和测试的watcher。没有并发,那就由于同时只能执行一个任务而不得不排队等待,这导致Grunt在执行所有其它必须同时并行运行的任务之前被挂起、阻塞。
concurrent: {
    dev: {
        tasks: ['nodemon:dev', 'shell:mongo', 'watch:scripts', 'watch:less', 'watch:test'],
        options: {
            logConcurrentOutput: true
        }
    },
    test: {
        tasks: ['watch:karma'],
        options: {
            logConcurrentOutput: true
        }
    }
},

karma

具体执行Karma测试的runner和watcher的任务。后面我们将会再多讲一点。
karma: {
    options: {
        configFile: 'karma.conf.js'
    },
    watcher: {
        background: true,
        singleRun: false
    },
    test: {
        singleRun: true
    }
},

jsHint

在所有必需的.js文件上运行jsHint语法检查器。
jshint: {
    all: ['Gruntfile.js', 'client/src/**/*.js', 'client/spec/**/*.js'],
    dev: ['client/src/**/*.js'],
    test: ['client/spec/**/*.js']
}
这部分是Gruntfile.js文件中initConfig的最后一部分!前面讲了很多,但是通常如果你想让所有的事情都自动化并让自己过得舒服些,它可以变得更长。在最开始你配置Gruntfile花费的时间能够为你节省在项目开发过程中10倍于它自己的时间。更别提它让你的的大脑得到解放,不必担心其它乱七八糟的事情。
现在,所有都不再是问题了,你只需要配置一些简单的命令行命令,在启动Grunt时可以用来干具体的活!
grunt.registerTask('init:dev', ['clean', 'bower', 'browserify:vendor']);
grunt.registerTask('build:dev', ['clean:dev', 'browserify:app', 'browserify:test','jshint:dev', 'less:transpile', 'concat', 'copy:dev']);
grunt.registerTask('build:prod', ['clean:prod', 'browserify:vendor', 'browserify:app','jshint:all', 'less:transpile', 'concat', 'cssmin', 'uglify', 'copy:prod']);
grunt.registerTask('server', ['build:dev', 'concurrent:dev']);
grunt.registerTask('test:client', ['karma:test']);
grunt.registerTask('tdd', ['karma:watcher:start', 'concurrent:test']);
每个‘注册’的任务都很不言自明,基本上你需要先给它一个名字,然后这样执行它
$grunt mytask
‘mytask’可以是你想要的任何名字。在registerTask()里跟在名字后面的是你想要执行的任务列表,就是你在initConfig中定义的那些任务。
init:dev - 在你要开始新项目时首先要执行的任务。它会进行Bower安装,复制那些文件到client_requires目录,然后给第三方文件执行Browserify。这个任务只需要在开发开始的时候执行一次,因为你的Bower依赖不会总是变化,所以没有必要在你每次修改app文件时重新执行Bower安装。
build:dev - 这个会做大部分工作。它会在你每次运行grunt服务时被执行,负责处理每次你修改文件需要重新构建时的任务。
grunt server - 操作的大脑。这个是你最经常使用的命令,它执行一个build:dev命令,然后是concurrent:dev来启动所有的watcher和服务器。
最后是 test:clienttdd,二者都会启动Karma并运行测试例,唯一的不同是‘test:client’只执行一次,‘tdd’在自动模式下每次都会运行 - 每次在app.js和test.js文件被修改时重新执行测试例。

使用Jasmine、Karma和PhantomJS来做TDD

目前我在前面已经断断续续得提到过一些关于Karma和TDD的事情,但是并没有进行详细解释。这是因为我已经写过 一篇相当详尽的文章来讲它们,你可以在这里阅读!只要说TDD(测试驱动开发)非常重要就够了。它背后的基本思想是你用一套测试例反过来验证真正的代码。假如你在调试或者修改核心代码时不小心break了什么,你的测试例能够马上警告你!如果你不经常做TDD,我强烈建议你考虑一下,至少通读一下我的其他文章。

Part 3: 使用Backbone.js和Marionette.js的前端应用

现在该来详细讲一下如何真正搭建前端应用了。这个app本身是一个很原始很简单的通讯录管理应用。app将联系人以带有名字、email和头像的小卡片的形式呈现。点击卡片会打开联系人的详细信息视图。你也可以添加联系人或者更新、删除已有联系人。它不是一个“To Do”应用,而真正实现了上述功能。
有关Browserify的注释
像上面在Gruntfile.js配置部分讲述的那样,我们在应用的前端部分严重依赖Browserify。这只是因为我真的非常喜欢Browserify的工作方式,我喜爱它将前端编程变得像后端开发一样。以前,我写过一篇文章讨论在Backbone.js中使用require.js- 但是为了现在这篇文章,我决定做些改变,尝试一些不同的东西。其实相比于以前用require.js的经验,我个人更喜欢使用Browserify。可以说,如果你以前从来没有用过node.js或者任何前端模块化开发框架,它们看起来都会很让人困惑。我建议你读一下我以前的文章,require.js和Browserify的主题和概念说起来很类似。

Main.js和App.js - 启动和大脑

var App = require('./app');
var myapp = new App();
myapp.start();
Main.js就像它听起来那样 - 是主JavaScript文件。这个文件很小,但是它将启动所有东西!它做的第一件事情是,使用Browserify,require我们的主app对象(位于app.js)。有了这个对象我们可以创建一个新实例并正式启动这个app。
app.js也像它听起来那样 - 它基本上可以说是我们的整个app。它就是位于中央的大脑,协调app的全部可动部分。我们来将它拆开,看一下究竟做了些什么。
var Marionette = require('backbone.marionette'),
    Controller = require('./controller'),
    Router = require('./router'),
    ContactModel = require('./models/contact'),
    ContactsCollection = require('./collections/contacts');
看起来很熟悉,对吗?这里我们声明app.js的依赖,声明了与Marionette、控制器、路由、联系人模型和联系人集合相对应的变量。每一个模块(除了Marionette)都是我们自己的文件,后面会有更多解释。注意Marionette依赖于jQuery、underscore和Backbone,但是我们不需要require它们,因为这已经在Gruntfile.js的Browserify的配置中通过依赖shim被实现了。
下面,创建了一个空函数并通过模块系统导出。即便是空的,它会立即在原型中增加了一个“start”的函数,这个函数将做所有工作。
module.exports = App = function App() {};

App.prototype.start = function(){
    App.core = new Marionette.Application();
在App.start()中发生了4件重要的事情:
  • 创建了一个新的Marionette.Application()
  • 绑定事件到initialize:before
  • 绑定事件到app:start
  • 启动Marionette应用
绑定的两个事件不会真正被触发直到启动Marionette应用(这就是为什么最后我会调用App.core.start())。现在我们分别看一下每个事件:

initialize:before

App.core.on("initialize:before", function (options) {
    App.core.vent.trigger('app:log', 'App: Initializing');

    App.views = {};
    App.data = {};

    // load up some initial data:
    var contacts = new ContactsCollection();
    contacts.fetch({
        success: function() {
            App.data.contacts = contacts;
            App.core.vent.trigger('app:start');
        }
    });
});
这个事件在app真正启动之前先一步发生。你可以参考Marionette文档来弄清有哪些可用的事件以及它们发生的顺序。这里initialize:before发生在app:start之前 - 我们这样定义在app启动之前想要处理的事情。具体说,我们新建了App的一些缓存对象(视图和数据),然后从服务器得到联系人数据。一旦数据获取完成,我们真正触发app:start。

app:start

App.core.vent.bind('app:start', function(options){
    App.core.vent.trigger('app:log', 'App: Starting');
    if (Backbone.history) {
        App.controller = new Controller();
        App.router = new Router({ controller: App.controller });
        App.core.vent.trigger('app:log', 'App: Backbone.history starting');
        Backbone.history.start();
    }

    //new up and views and render for base app here...
    App.core.vent.trigger('app:log', 'App: Done starting and running!');
});
// ...
App.core.start();
在app:start里面,我们新建了一个controller的实例和一个router的实例,router将controller作为它构造函数一部分。二者都是Marionette对象(会再解释)。在本项目中,我们的前端路由和控制器的工作方式几乎跟后端node.js的路由和控制器一样,唯一的不同是node.js路由管理了服务器端可以访问的URL - Marionette路由管理着前端app运行时可以被访问的URL。将Backbone或者Marionette路由看作是与DOM事件类似的东西,但是DOM元素是window.location地址栏。如果浏览器地址栏中的URL改变了,触发应用中一个路由事件。(不像普通URL那样加载一个新的页面。)

Marionette路由和控制器

像你在app.js中看到的那样,我们在app:start事件中定义了一个路由和控制器的新实例。我也提到他们几乎就像node.js中的对手那样工作。
route.js
var Marionette = require('backbone.marionette');

module.exports = Router = Marionette.AppRouter.extend({
    appRoutes: {
        '#'  : 'home',
        'details/:id' : 'details',
        'add' : 'add'
    }
});
router.js文件非常简单 - 新建一个Marionette AppRouter对象并将其赋值为一个appRoutes的集合。这些appRoutes将与Controller对象中的同名函数1:1对应。这里我们将app的根URL'#'指向控制器中的'home'函数。然后'details/:id'是得到联系人详细信息视图的URL,指向控制器的'detail'函数。最后我们将'add'URL指向控制器中的'add'函数。
controller.js
home: function() {
    var view = window.App.views.contactsView;
    this.renderView(view);
    window.App.router.navigate('#');
},
控制器是路由背后真正逻辑的拥有者。像我们前面提到的那样,路由和控制器有1:1的关系,意味着每一个定义在路由表中的路由都对应于一个定义在控制器中的函数。控制器中的每一个函数负责相应路由的屏幕渲染。用'home'函数举例来说,我们建立了一个视图,如果我们正在看详细信息视图的话还包括一个模型,然后通过调用控制器的renderView函数来渲染它。renderView函数首先会摧毁已经存在的视图,如果这个视图已经被渲染的话。这里要小心事件处理并保证不要有僵尸视图和/或者事件处理器留存。

Models和Collections

var Backbone = require('backbone');

module.exports = ContactModel = Backbone.Model.extend({
    idAttribute: '_id'
});

var Backbone = require('backbone'),
    ContactModel = require('../models/contact');

module.exports = ContactsCollection = Backbone.Collection.extend({
    model:  ContactModel,
    url: '/api/contacts'
});
这个简单的app有一个最基本的模型 - 联系人,另外还有一个联系人集合。二者都定义在‘src’目录中的各自文件夹里。你能看到集合的url已经被设置为我们的API。另外,因为我们用mongoDB,所以我们手工将模型的id属性指向_id字段,MongoDB默认用这个字段作为唯一标识。

Views

这个应用有3个主要的视图,一个小的联系人“卡片”,它在主页中显示为列表,一个联系人详细信息页面以及一个新增联系人表单。
views/contacts.js
联系人视图实际上包含了2个视图,一个Marionette ItemView作为单独的联系人“卡片”,然后是一个Marionette CollectionView,它是“卡片”视图的集合,作为联系人列表。联系人CollectionView监听任何集合变更事件,如果有变化,集合视图会被重新渲染。这就是为什么任何新的联系人添加后主列表会被重新渲染。
var Marionette = require('backbone.marionette');

var itemView = Marionette.ItemView.extend({
    template: require('../../templates/contact_small.hbs'),
    initialize: function() {
        this.listenTo(this.model, 'change', this.render);
    },
    events: {
        'click': 'showDetails'
    },

    showDetails: function() {
        window.App.core.vent.trigger('app:log', 'Contacts View: showDetails hit.');
        window.App.controller.details(this.model.id);
    }
});

module.exports = CollectionView = Marionette.CollectionView.extend({
    initialize: function() {
        this.listenTo(this.collection, 'change', this.render);
    },
    itemView: itemView
});
ItemView本身就含有一个会触发控制器中‘detail’函数的事件。ItemView还有一个对它的模型的监听器,这样如果联系人详细信息发生变化,视图也会被重新渲染。注意到ItemView有一个Handlebars模板,通过require()被引用进来。Browserify会在构建的时候用一个预编译好的JavaScript函数来替换这一行,这会让视图渲染相比于在浏览器中视图每次渲染时才编译要快得多。
views/contact_details.js
联系人的详细信息视图相当简单 - 只有一个去“<<Back”的事件处理器和一个Handlebars模板。
views/add.js
最后,我们有一个用于在插入新联系人时渲染表单的视图,最主要的功能是save函数:
events: {
    'click a.save-button': 'save'
},

save: function(e) {
    e.preventDefault();
    var newContact = {
        name: {
            first: this.$el.find('#name_first').val(),
            last: this.$el.find('#name_last').val()
        },
        email: this.$el.find('#email').val(),
        phone: this.$el.find('#phone').val()
    };

    window.App.data.contacts.create(newContact);
    window.App.core.vent.trigger('app:log', 'Add View: Saved new contact!');
    window.App.controller.home();
}
这里我们定义了一个新的联系人对象,这只是一个普通的JavaScript对象,对应于在MongoDB侧我们的数据对象的样子。然后我们简单将它传递给Backbone的collection.create()函数 - 它将用Backbone的默认实现,将定义好的JSON对象变量传递给一个对API URL(在前面的集合中定义的)的POST请求。回到服务器侧,node.js路由监听对‘api/contacts’的POST请求,它会调用(node.js)控制器的‘add’函数。服务器端联系人控制器中的‘add’函数会用Mongoose创建一个新的联系人模型然后保存到MongoDB服务器。
server/controllers/contacts.js
add: function(req, res) {
    var newContact = new models.Contact(req.body);
    newContact.gravatar = md5(newContact.email);
    newContact.save(function(err, contact) {
        if (err)
            res.json({});
        console.log('successfully inserted contact: ' + contact._id);
        res.json(contact);
    });
},
有关验证的重要提示:
无论前端还是后端都没有进行任何验证,认识到这一点很重要。很明显这不好,但是我没有这样做只是希望让代码更简洁和易于消化。这是在真正的app中你需要实现的。(为安全起见,在前端和后端同时进行验证是一个好主意!)

用Handlebars作为视图模板

像我在讲述后端时提到的那样,我们决定用于前端和后端的模板引擎是Handlebars。我这样做既想保持一致又因为我是Handlebars的粉丝。你能够在‘client/templates’目录中找到视图模板,每一个基本上都是一个带有.hbs后缀的HTML文档。

Part 4: 用Heroku部署!

从这开始,我们已经有了一个功能完整的app,尽管它有点简单。测试它最好的办法,最理智的下一步,是将它真正部署到因特网上!一个免费并且超级简单的神奇服务是Heroku!Heroku基本上是一个可扩展的云主机解决方案,它有很多令人惊讶的特性并支持插件。对像我们这样一个app来说,她是主机托管服务的理想选择。让我们一起看一下启动app的步骤。

在Heroku.com上注册一个免费账户

如果你没有Heroku的账户,现在就去免费 注册一个。只要两秒...

下载Heroku Toolbelt

有了账户之后,确保 下载并安装Heroku Toolbelt,这是让建立app并推送代码到Heroku变得很容易的一个命令行工具!

建立Heroku app,安装插件,适时推送你的代码

现在你有了Heroku账户并且安装了Toolbelt,你准备好了在你的账户下建立一个app,准备这个app并适时推送代码。在此过程中也可以安装一些插件。
提示:下面的所有命令都应该在工程的根目录下执行。
$ heroku login
登录后,你可能会被提示添加SSH key。按照提示进行操作。
下一步,建立一个名为‘procfile’的文件,然后将下面一行添加进去:
web: node server.js
在你的Heroku账号下建立一个app:
$ heroku create
下面我们需要安装第一个插件,MongoHQ。MongoHQ是另外一个特别针对MongoDB的云主机服务,与Heroku配合得非常好。像Heroku一样,MongoHQ有一个免费的沙箱服务!
$ heroku addons: add mongohq
提示:为了真正使用插件,你需要确保你的账户有完整的计费信息。这只是因为大多数服务是可扩展的并有限制条款。一旦超出那些条款,计费就会介入。
下一步,去Heroku网站并访问你的dashboard/Apps。点击刚刚创建的app,然后点击MongoHQ插件。进入Collections,为你的app添加一个集合(例如,“contacts”)。然后去Admin并选择Users tab。创建一个新用户来访问数据库(起任意你想要的用户名/密码)。返回Overview tab并复制Mongo URI。
编辑server.js,连接mongodb服务器的那行需要修改为刚刚复制的Mongo URI。修改<user>和<password>为你刚刚创建的MongoHQ用户和密码。
现在我们可以推送代码到Heroku:
提示:在你推送代码到Heroku之前,因为需要Git来完成这件事,你要确保到目前为止做的任何修改都已经staged并committed(例如你刚刚为MongoHQ连接URI而修改了server.js)。做这个最简单的方式是用下面的命令:
$ git add .
$ git commit -m "Updates for Heroku"
$ git push heroku master
到目前为止,app配置好了,插件安装了,代码推送上去了,我们需要Heroku真正的启动服务器。要做到这一点,Heroku用了所谓的dyno,它读取Procfile并且执行这个命令:
$ heroku ps:scale web=1
最后,在浏览器中启动你的新Heroku应用:
$ heroku open

结论

对于一个看起来似乎很小的app来说,仍旧有很多内容需要讲述。这个样板项目对于未来任何想快速启动运行的app来说都是一个很好的基础。不需要操心一个标准web项目的方方面面的细节,你的时间可以被用来关心如何更好地制作app!
如果你对本项目由任何疑问,请随时给我留一条评论或者在Twitter上找到我。同时,非常欢迎任何的意见和建议!感谢阅读本文,希望你发现这里的代码对你有用 - 现在去创建些东西吧!


编后语:
这篇文章在一年前刚开始学习node.js时就看到了,当时还摸不到node.js的门,这篇文章给了我很大的帮助,算是迈出了第一步。当时就曾经想翻译本文,但是实在有点长,一直没有下定决心。大概一个月以前,主动申请做公司产品的前端开发,一个偶然的机会又搜到了这篇文章,终于决心把它翻译过来。
断断续续地翻译了两三个星期,中间因为evernote网页版的问题还把整个翻译好的part 2丢了。还好终于赶在农历大年之前翻译完了,如果不是因为昨天抢了一天的红包,可能还会提前一天:),也算对自己有一个交代吧。
希望本文能对大家有些帮助,水平有限,错误难免,发现人家那些翻译整本书的还真得有一定的定力和水平。
祝大家羊年快乐,幸福吉祥!

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值