任何傻瓜都能写出计算机能理解的代码。优秀的程序员编写人类能够理解的代码。——马丁·福勒T3】
在本章中,我们将介绍以下内容:
- 在 Node.js 中构建“Hello World”
- Node.js 核心模块
- npm Node 程序包管理器
- 带有 Node.js 的留言板:内存存储版本
- 单元测试 Node. js
Node.js 是一个用于构建 web 应用的非阻塞平台。它使用 JavaScript,所以它是我们 fullstack JavaScript 开发的核心。我们将从 Hello World 开始,讨论核心模块和 npm。然后,我们将 Hello World 应用部署到云中。
引导您完成实施并演示项目的补充视频: http://bit.ly/1QnqFmF 。
要检查您的计算机上是否安装了 Node.js,请在终端中键入并执行以下命令:
$ node -v
在撰写本文时,最新版本是 5.1.0。如果你没有安装 Node.js,或者你的版本落后,可以在 nodejs.org/#download 下载最新版本。您可以使用这些工具之一进行版本管理(例如,在 Node.js 版本之间切换):
通常,你可以在 https://github.com/azat-co/fullstack-javascript/tree/master/07-hello 复制示例代码,或者从头开始编写自己的程序。如果您想做后者,为您的“Hello World”node . js 应用创建一个文件夹hello。然后创建一个server.js文件,一行一行地输入下面的代码。
这将加载服务器的核心http模块(稍后将详细介绍这些模块):
var http = require(’http’)
我们需要 Node.js 服务器的端口号。要从环境中获取或分配 1337(如果未设置环境),请使用:
var port = process.env.PORT || 1337
这将创建一个服务器,一个回调函数将包含响应处理程序代码:
var server = http.createServer(function (req, res) {
要设置正确的标题和状态代码,请使用:
res.writeHead(200, {’Content-Type’: ’text/plain’})
要输出带有行尾符号的“Hello World ”,请使用:
res.end(’Hello World\n’)
})
要设置端口并显示服务器的地址和端口号,请使用:
server.listen(port, function() {
console.log(’Server is running at %s:%s ’,
server.address().address, server.address().port)
})
从您有server.js的文件夹中,在您的终端中启动以下命令:
$ node server.js
打开 localhost:1337 或 127.0.0.1:1337 或任何其他你在终端中看到的地址作为console.log()功能的结果,你应该在浏览器中看到“Hello World”。要关闭服务器,请按 Control + C。
Note
主文件的名称可以不同于 server.js(例如 index.js 或 app.js)。如果需要启动app.js文件,只需使用$ node app.js即可。
与其他编程技术不同,Node.js 没有附带沉重的标准库。node.js 的核心模块是最少的,其余的可以通过 Node 包管理器(NPM)注册中心挑选。主要的核心模块、类、方法和事件包括:
- http (
https://nodejs.org/api/http.html#http_http):使用 http 协议的模块 - util (
https://nodejs.org/api/util.html):有各种助手的模块 - querystring (
https://nodejs.org/api/querystring.html):解析来自 URI 的查询字符串的模块 - URL(
https://nodejs.org/api/url.html):URI 信息解析模块 - fs (
https://nodejs.org/api/fs.html):用于处理文件系统的模块
这些是最重要的核心模块。让我们逐一介绍。
这是负责 Node.js HTTP server 的主要模块。以下是主要方法:
http.createServer():返回一个新的 web 服务器对象http.listen():开始接受指定端口和主机名上的连接http.createClient(): node app 可以是客户端,向其他服务器发出请求http.ServerRequest():传入的请求被传递给请求处理程序data:收到一段消息体时发出end:每个请求只发出一次request.method():字符串形式的请求方法request.url():请求 URL 字符串
http.ServerResponse():这个对象是由 HTTP 服务器内部创建的——而不是由用户创建的,并被用作请求处理程序的输出response.writeHead():发送请求的响应头response.write():发送响应正文response.end():发送并结束响应体
该模块提供了用于调试的实用程序。一些方法包括:
util.inspect():返回一个对象的字符串表示,对调试很有用
这个模块提供了处理查询字符串的工具。一些方法包括:
querystring.stringify():将对象序列化为查询字符串querystring.parse():将查询字符串反序列化为对象
这个模块有 URL 解析和解析的工具。一些方法包括:
parse():取一个 URL 字符串,返回一个对象
文件系统处理文件系统操作,例如读写文件。库中有同步和异步方法。一些方法包括:
fs.readFile():异步读取文件fs.writeFile():将数据异步写入文件
不需要安装或下载核心模块。要将它们包含在您的应用中,您只需遵循以下语法:
var http = require(’http’)
非核心模块列表可在以下位置找到:
npmjs.org:Node 包管理器注册表Nipster(http://eirikb . github . io/nipster):node . js 的 NPM 搜索工具node-modules( http:// node-modules。com ): npm 搜索引擎
如果你想知道如何编写你自己的模块,看看这篇文章: https://quickleft.com/blog/creating-and-publishing-a-node-js-module/ 。
Node 程序包管理器(或称 NPM)为您管理依赖关系并安装模块。Node.js 安装自带 NPM,其网址为 npmjs.org 。
包含关于 Node.js 应用的元信息,比如版本号;作者姓名;最重要的是,我们在应用中使用什么依赖关系。所有这些信息都在 JSON 格式的对象中,由 NPM 读取。
如果您想安装package.json中指定的软件包和依赖项,请键入:
$ npm install
典型的package.json文件可能如下所示:
{
"name": "Blerg",
"description": "Blerg blerg blerg.",
"version": "0.0.1",
"author": {
"name" : "John Doe",
"email" : "john.doe@gmail.com"
},
"repository": {
"type": "git",
"url": "http://github.com/johndoe/blerg.gitT2】
},
"engines": [
"node >= 0.6.2"
],
"scripts": {
"start": "server.js"
},
"license" : "MIT",
"dependencies": {
"express": ">= 2.5.6",
"mustache": "0.4.0",
"commander": "0.5.2"
},
"bin" : {
"blerg" : "./cli.js"
}
}
虽然上面的package.json示例中的大多数属性,如description和name,都是不言自明的,但是其他的属性需要更多的解释。Dependencies 是一个对象,每一项左边有名称,右边有版本号(如“express”:>= 2 . 5 . 6)。版本可以是精确的:例如," express": "2.5.6 ",或者大于,或者通配符,例如," express": "* "(一种在生产中使用新的未经测试的依赖项来放大应用的好方法:因此不推荐)。
bin属性用于命令行实用程序。它告诉系统启动什么文件。而scripts对象有你可以用$ npm run SCRIPT_NAME启动的脚本。start脚本和测试是例外。你可以用$ npm start和$ npm test来运行它们。
要将软件包更新到当前最新版本或package.json中定义的版本规范允许的最新版本,请使用:
$ npm update name-of-the-package
或者对于单模块安装:
$ npm install name-of-the-package
本书示例中使用的唯一模块是mongodb,它不属于核心 Node.js 包。我们将在本书的后面安装它。
Heroku 将需要package.json在服务器上运行 NPM。
想了解更多关于 NPM 的信息,可以看看“游 NPM”(http://tobyho.com/2012/02/09/tour-of-npm)这篇文章。
对于 Heroku 和 Windows Azure 部署,我们需要一个 Git 存储库。要从项目的根目录创建它,请在终端中键入以下命令:
$ git init
Git 将创建一个隐藏的.git文件夹。现在,我们可以添加文件并进行第一次提交:
$ git add .
$ git commit -am "first commit"
提示要查看 Mac OS X Finder 应用上的隐藏文件,请在终端窗口中执行此命令:defaults write com.apple.finder AppleShowAllFiles -bool true。要将标志改回隐藏状态:defaults write com.apple.finder AppleShowAllFiles -bool false。
为了将我们的“Hello World”应用部署到 Windows Azure,我们必须添加 Git remote。您可以从网站下的 Windows Azure 门户复制 URL,并通过以下命令使用它:
$ git remote add azure yourURL
现在,我们应该能够使用这个命令进行推送了:
$ git push azure master
如果一切顺利,您应该会在终端中看到成功日志,并在 Windows Azure 网站 URL 的浏览器中看到“Hello World”。
要推动更改,只需执行:
$ git add .
$ git commit -m "changing to hello azure"
$ git push azure master
更细致的指导可以在 https://azure.microsoft.com/en-us/documentation/articles/web-sites-nodejs-develop-deploy-mac 教程中找到。
对于 Heroku 部署,我们需要创建两个额外的文件:Procfile和package.json。你可以从 https://github.com/azat-co/fullstack-javascript/tree/master/07-hello 获得源代码,或者自己写一个。
“Hello World”应用的结构如下所示:
/07-hello
-package.json
-Procfile
-server.js
Procfile 是一种机制,用于声明 Heroku 平台上应用的 dynos 运行哪些命令。基本上,它告诉 Heroku 运行什么进程。在这种情况下,Procfile 只有一行:
web: node server.js
对于这个例子,我们保持package.json简单:
{
"name": "node-example",
"version": "0.0.1",
"dependencies": {
},
"engines": {
"node": ">=0.6.x"
}
}
在项目文件夹中有了所有文件之后,我们可以使用 Git 来部署应用。除了我们需要添加 Git remote,并使用以下命令创建 Cedar 堆栈之外,这些命令与 Windows Azure 非常相似:
$ heroku create
完成后,我们推送并更新:
$ git push heroku master
$ git add .
$ git commit -am "changes :+1:"
$ git push heroku master
如果一切顺利,您应该会在终端中看到成功日志,并在 Heroku 应用 URL 的浏览器中看到“Hello World”。
引导您完成实施并演示项目的补充视频: http://bit.ly/1QnqO9P 。
第一版的留言板后端应用为了亲亲只在运行时内存存储中存储消息( http://en.wikipedia.org/wiki/KISS_principle )。这意味着每次我们启动/重置服务器时,数据都会丢失。
我们将首先从一个简单的测试用例开始,来说明测试驱动的开发方法。完整代码可在 https://github.com/azat-co/fullstack-javascript/tree/master/08-test 获得。
我们应该有两种方法:
Get all of the messages as an array of JSON objects for the GET /message endpoint using the getMessages() method Add a new message with properties name and message for POST /messages route via the addMessage() function
我们将从创建一个空的mb-server.js文件开始。在它出现之后,让我们切换到测试并创建包含以下内容的test.js文件:
var http = require(’http’)
var assert = require(’assert’)
var querystring = require(’querystring’)
var util = require(’util’)
var messageBoard = require(’./mb-server’)
assert.deepEqual(’[{"name":"John","message":"hi"}]’,
messageBoard.getMessages())
assert.deepEqual (’{"name":"Jake","message":"gogo"}’,
messageBoard.addMessage ("name=Jake&message=gogo"))
assert.deepEqual(’[{"name":"John","message":"hi"},{"name":"Jake",message":"gogo"}]’,
messageBoard.getMessages())
请记住,这是一个非常简单的字符串比较,而不是 JavaScript 对象。所以每个空格、引用和大小写都很重要。您可以通过将一个字符串解析成一个 JSON 对象来使比较“更智能”,方法是:
JSON.parse(str)
为了测试我们的假设,我们使用 Node.js 模块的核心断言。它提供了一堆有用的方法,如equal()、deepEqual()等。
更高级的库包括 TDD 和/或 BDD 方法的替代接口:
- 期待:极简 BDD 风格的断言库:,例如
expect(user.name).to.eql(’azat’) - 应该 (
https://github.com/shouldjs/should.js): BDD 风格的断言库,通过修改Object.prototype来工作:例如user.name.should.be.eql(’azat’)
对于更多的测试驱动开发和前沿自动化测试,您可以使用以下库和模块:
- 摩卡 (
https://mochajs.org/):功能丰富的测试框架(我的默认选择) - NodeUnit (
https://github.com/caolan/nodeunit):简单断言式单元测试库 - 茉莉 (
https://github.com/jasmine/jasmine):内置断言和 spy(用于嘲讽)库的 BDD 测试框架 - 誓言 (
http://vowsjs.org/):为测试异步代码量身定制的 Node.js 框架 - 柴 (
http://chaijs.com/): BDD/TDD 断言库,可与测试框架配对,有自己版本的 Should、Expect、Assert - Tape (
https://github.com/substack/tape):一个极简的 TAP(测试任何东西协议)库 - Jest(
http://facebook.github.io/jest/):Jasmine-and-Expect-like 测试库,具有自动模拟功能
您可以暂时将“Hello World”脚本复制到mb-server.js文件中,或者甚至保留为空。如果我们通过终端命令运行test.js:
$ node test.js
我们应该会看到一个错误。大概是这样的:
TypeError: Object #<Object> has no method ’getMessages’
那完全没问题,因为我们还没有写getMessages()方法。因此,让我们这样做,并通过添加两个新方法使我们的应用更有用:获取聊天消息列表和向集合中添加新消息。
带有全局exports对象的mb-server.js文件:
exports.getMessages = function() {
return JSON.stringify(messages)
// Output array of messages as a string/text
}
exports.addMessage = function (data){
messages.push(querystring.parse(data))
// To convert string into JavaScript object we use parse/deserializer
return JSON.stringify(querystring.parse(data))
// Output new message in JSON as a string
}
我们导入依赖关系:
var http = require(’http’)
// Loads http module
var util= require(’util’)
// Usefull functions
var querystring = require(’querystring’)
// Loads querystring module, we’ll need it to serialize and deserialize objects and query strings
并设置端口。如果它是在 env 变量中设置的,我们使用那个值;如果没有设置,我们使用硬编码值 1337:
var port = process.env.PORT || 1337
到目前为止,没什么特别的,对吧?为了存储消息列表,我们将使用一个数组:
var messages=[]
// This array will hold our messages
messages.push({
’name’: ’John’,
’message’: ’hi’
})
// Sample message to test list method
一般来说,像虚拟数据这样的设备属于测试/规范文件,而不属于主要的应用代码库。
我们的服务器代码看起来会稍微有趣一些。为了获得消息列表,根据 REST 方法,我们需要发出一个 GET 请求。对于创建/添加新消息,它应该是 POST 请求。因此,在我们的 createServer 对象中,我们应该添加req.method()和req.url()来检查 HTTP 请求类型和 URL 路径。
让我们加载 http 模块:
var http = require(’http’)
我们将需要来自util和querystring模块的一些方便的函数(来序列化和反序列化对象和查询字符串):
var util= require(’util’)
// Usefull functions
var querystring = require(’querystring’)
// Loads querystring module, we’ll need it to serialize and deserialize objects and query strings
创建一个服务器并将其暴露给外部模块(即test.js):
exports.server=http.createServer(function (req, res) {
// Creates server
在请求处理程序回调中,我们应该检查请求方法是否为 POST,URL 是否为messages/create.json:
if (req.method == ’POST’ && req.url == ’/messages/create.json’) {
// If method is POST and URL is messages/ add message to the array
如果上述条件为真,我们向数组中添加一条消息。但是,data必须在添加之前转换为字符串类型(编码为 UTF-8 ),因为它是一种缓冲区类型:
var message = ’’
req.on(’data’, function(data, msg){
console.log(data.toString(’utf-8’))
message=exports.addMessage(data.toString(’utf-8’))
// Data is type of Buffer and must be converted to string with encoding UTF-8 first
// Adds message to the array
})
这些日志将帮助我们监控终端中的服务器活动:
req.on(’end’, function(){
console.log(’message’, util.inspect(message, true, null))
console.log(’messages:’, util.inspect(messages, true, null))
// Debugging output into the terminal
输出应该是文本格式,状态为 200(正常):
res.writeHead(200, {’Content-Type’: ’text/plain’})
// Sets the right header and status code
我们输出一条带有新创建的对象 ID 的消息:
res.end(message)
// Out put message, should add object id
})
如果方法是 GET,URL 是/messages/list.json,则输出消息列表:
} else
if (req.method == ’GET’ && req.url == ’/messages/list.json’) {
// If method is GET and URL is /messages output list of messages
获取邮件列表:
var body = exports.getMessages()
// Body will hold our output
响应主体将保存我们的输出:
res.writeHead(200, {
’Content-Length’: body.length,
’Content-Type’: ’text/plain’
})
res.end(body)
下一个else是当前面的任何条件都不匹配时。这将设置正确的标题和状态代码:
} else {
res.writeHead(200, {’Content-Type’: ’text/plain’})
// Sets the right header and status code
如果不是上面的两个端点,我们输出一个带有行结束符号的字符串:
res.end(’Hello World\n’)
// Outputs string with line end symbol
}
启动服务器:
}).listen(port)
// Sets port and IP address of the server
现在,我们应该设置服务器的端口和 IP 地址:
console.log(’Server running at``http://127.0.0.1:%s/T2】
我们在test.js (exports 关键字)中公开了单元测试的方法,这个函数以字符串/文本的形式返回一组消息:
exports.getMessages = function() {
return JSON.stringify(messages)
}
addMessage()使用 querystring 中的 parse/deserializer 方法将字符串转换为 JavaScript 对象:
exports.addMessage = function (data){
messages.push(querystring.parse(data))
还以 JSON-as-a-string 格式返回新消息:
return JSON.stringify(querystring.parse(data))
}
下面是mb-server.js减去注释后的完整代码。也可在08-测试中获得:
var http = require(’http’)
// Loads http module
var util= require(’util’)
// Usefull functions
var querystring = require(’querystring’)
// Loads querystring module, we’ll need it to serialize and deserialize objects and query strings
var port = process.env.PORT || 1337
var messages=[]
// This array will hold our messages
messages.push({
’name’: ’John’,
’message’: ’hi’
})
// Sample message to test list method
exports.server=http.createServer(function (req, res) {
// Creates server
if (req.method == ’POST’ && req.url == ’/messages/create.json’) {
// If method is POST and URL is messages/ add message to the array
var message = ’’
req.on(’data’, function(data, msg){
console.log(data.toString(’utf-8’))
message=exports.addMessage(data.toString(’utf-8’))
// Data is type of Buffer and must be converted to string with encoding UTF-8 first
// Adds message to the array
})
req.on(’end’, function(){
console.log(’message’, util.inspect(message, true, null))
console.log(’messages:’, util.inspect(messages, true, null))
// Debugging output into the terminal
res.writeHead(200, {’Content-Type’: ’text/plain’})
// Sets the right header and status code
res.end(message)
// Out put message, should add object id
})
} else
if (req.method == ’GET’ && req.url == ’/messages/list.json’) {
// If method is GET and URL is /messages output list of messages
var body = exports.getMessages()
// Body will hold our output
res.writeHead(200, {
’Content-Length’: body.length,
’Content-Type’: ’text/plain’
})
res.end(body)
} else {
res.writeHead(200, {’Content-Type’: ’text/plain’})
// Sets the right header and status code
res.end(’Hello World\n’)
// Outputs string with line end symbol
}
}).listen(port)
// Sets port and IP address of the server
console.log(’Server running at``http://127.0.0.1:%s/T2】
exports.getMessages = function() {
return JSON.stringify(messages)
// Output array of messages as a string/text
}
exports.addMessage = function (data){
messages.push(querystring.parse(data))
// To convert string into JavaScript object we use parse/deserializer
return JSON.stringify(querystring.parse(data))
// Output new message in JSON as a string
}
要查看它,请访问。您应该会看到一条示例消息。
或者,您可以使用终端命令来获取消息:
$ curl http://127.0.0.1:1337/messages/list.json
使用命令行界面发出 POST 请求:
$ curl -d "name=BOB&message=test"http://127.0.0.1:1337/messages/create.jsonT3】
当您刷新时,您应该在服务器终端窗口中得到输出和一条新消息“test”。不用说,这三项测试都应该通过。
您的应用可能会随着更多的方法、要解析的 URL 路径和条件而变得更大。这就是框架派上用场的地方。它们提供了处理请求的助手和其他好东西,如静态文件支持、会话等。在这个例子中,我们故意没有使用任何类似 Express ( http://expressjs.com/ )或 Restify ( http://mcavage.github.com/node-restify/ )的框架。其他值得注意的 Node.js 框架:
- Derby (
http://derbyjs.com/):MVC 框架使得编写在 Node.js 和浏览器中运行的实时协作应用变得容易 - 快递。js (
http://expressjs.com/en/index.html):最健壮的、经过测试和使用的 Node.js 框架 - Restify(
http://restify.com/):restapi 服务器的轻量级框架 - 风帆。js(
http://sailsjs.org/):MVC node . js 框架 - hapi (
http://spumko.github.io/):构建在 Express.js 之上的 Node.js 框架 - 连接(
https://github.com/senchalabs/connect#readme):node 的中间件框架,附带超过 18 个捆绑的中间件和丰富的第三方中间件选择 - GeddyJS (
http://geddyjs.org/):一个简单的、结构化的 MVC web 框架 - compound js(
http://compoundjs.com/)(ex-rails wayjs):Node。基于 ExpressJS 的 JS MVC 框架 - 塔。js(
http://tower.github.io/):node . js 和浏览器的全栈 web 框架 - 流星 (
https://www.meteor.com/):开源平台,在极短的时间内构建高质量的网络应用
要获得精心挑选的框架列表,请查看(http://node framework。com 。改进应用的方法:
- 通过添加对象比较而不是字符串比较来改进现有的测试用例
- 将种子数据从
mb-server.js移动到test.js - 添加测试用例来支持您的前端(例如,向上投票、用户登录)
- 添加方法来支持您的前端(例如,向上投票、用户登录)
- 为每条消息生成唯一的 id,并将它们存储在哈希中,而不是存储在数组中
- 安装 Mocha 和 re-factor test.js,以便它使用这个库
到目前为止,我们一直将消息存储在应用内存中,所以每次应用重启时,我们都会丢失消息。要修复它,我们需要添加一个持久性,方法之一就是使用 MongoDB 这样的数据库。
在这一章中,我们已经讨论了一些重要的主题,这些主题将为我们打下基础。他们展示了 Node.js 中的“Hello World”应用,一些最重要的核心模块列表,NPM 工作流,将 Node.js 应用部署到 Heroku 和 Windows Azure 的详细命令;也是测试驱动开发实践的一个例子。