了解NODE基于事件轮询的架构、无阻塞I/O以及事件驱动的编程方式
精通NODE.js的 API
轻松实现开发实时应用相关的技术,如Socket.IO 和 HTML5 WebSocket
编写能够支持跨多台服务器的高并发应用
通过Node来支持多种数据库以及数据存储工具
编写在单台服务器情况下能够处理万级并发量的程序
能够在一个包含更多Node知识和注解示例(含源码)的网站上和其他开发者进行实时的沟通交流
package.json
main 属性:让 Node 知道该载入哪个文件。当需要让模块暴露 API 的时候,main 属性就会变得尤为重要,因为你需要为模块定义一个入口(有的时候,入口可能是多个文件)
npm help json: 查看 package.json 文件所有的属性文档模块私有:添加“private”:“true“
命令行工具:安装时需要增加-g标志,要想分发此类脚本,发布时,在package.json文件中添加“bin”: “./path/to/script”项,并将其值指向可执行的脚本或者二进制文件
npm search <package name>: 在仓库中搜索和查看模块npm view <package name>: 返回 package.json 文件及 NPM 仓库相关的属性NPM 遵循 semver 的版本控制标准
JS 概览
如果对函数进行了命名,V8 就能在显示堆栈追踪信息时将名字显示出来。为函数命名有助于调试,因此,推荐始终对函数进行命名
通过调用方法来定义属性:访问属性:defineGetter,设置属性defineSetter
Date.prototype.__defineGetter__('ago', function() {var diff = ((new Date()).getTime() - this.getTime()) / 1000;var day_diff = Math.floor( diff / 86400 );return day_diff == 0 && (diff < 60 && "just now" ||diff < 120 && "1 minute ago" ||diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" ||diff < 7200 && "1 hour ago" ||diff < 86400 && Math.floor( diff / 3600 ) + " hours ago" ||day_diff == 1 && "Yesterday" ||day_diff < 7 && day_diff + " days ago" ||Math.ceil( day_diff / 7 ) + " weeks ago";)})var a = new Date('12/12/1990')a.ago
阻塞与非阻塞 IO
Node 采用一个长期运行的进程,Apache会产出多个线程(每个请求一个线程),每次都会刷新状态.
Node 采用了事件轮询,是非阻塞的,即是异步的.
事件轮询:Node 会先注册事件,然后不停地询问内核这些事件是否已经分发。当事件分发时,对应的回调函数就会被触发,然后继续执行下去。如果没有事件触发,则继续执行其他代码,直到有新事件时,再去执行对应的回调函数.
调用堆栈:当 V8 首次调用一个函数时,会创建一个众所周知的调用堆栈,如果该函数调用又去调用另外一个函数的话,V8 就会把它添加到调用堆栈上。
Node 的最大并发量是 1
V8 搭配非阻塞 IO 是最好的组合
除了 uncaughtException 和 error 事件外,绝大部分 Node 异步 API 接收的回调函数,第一个参数都是错误对象或者是 null
错误处理中,每一步都很重要,因为它能让你书写更安全的程序,并且不丢失触发错误的上下文信息
要捕获一个未来才会执行到的函数所抛出的错误是不可能的,这会直接抛出未捕获的异常,同样异步会导致堆栈追踪信息丢失
Node 通过单线程的执行环境,提供了极大的简便,不过正因为如此,书写网络应用时,要尽可能地避免使用同步IO
Node 中的 JS
global 对象
global: 任何 global 对象上的属性都可以被全局访问到
process: 所有全局执行上下文中的内容都在 process 对象中,在 Node 中进程的名字是 process.title
模块系统
避免出现对全局命名空间的污染及命名冲突的问题
绝对模块:Node 通过在其内部 node_modules 查找到的模块,或者 Node 内置的模块
相对模块:将 require 指向一个相对工作目录中的 JS 文件
exports 是对 module.exports 的引用,在默认情况下是一个对象,用于逐个添加属性
module.exports 可以被重写
事件
- DOM API:
addEventListener
removeEventListener
dispatchEvent
- Node Event EmitterAPI:
on
emit
removeListener
事件是 Node 非阻塞设计的重要体现,Node 通常不会直接返回数据(因为这样可能会在等待某个资源的时候发生线程阻塞),
而是采用分发事件来传递数据的方式事件是否会触发取决于实现它的 API
不管某个事件在将来会被触发多少次,我都希望只调用一次回调函数:once(str,fn)
buffer
buffer 是一个表示固定内存分配的全局对象(也就是说,要放到缓冲区中的字节数需要提前定下),它就好比是一个由八位字节元素组成的数组,可以有效地在JS中表示二进制数据
base64: 主要是一种仅用ASCII字符书写二进制数据的方式
命令行工具
文件浏览器
编写一个文件浏览器,功能是允许用户读取和创建文件
程序需要在命令行运行。意味着程序要么通过 node 命令来执行,要么直接执行,然后通过终端提供交互给用户进行输入、输出
程序启动后,需要显示当前目录下列表
选择某个文件时,程序需要显示该文件的内容
选择一个目录时,程序需要显示该目录下的信息
运行结束后程序退出
开发流程
创建模块
决定采用同步的 fs 还是异步的 fs
理解什么是流 (Stream)
实现输入输出
重构
使用 fs 进行文件交互
完成
划重点:fs 模块是唯一一个同时提供同步和异步 API 的模块
process 包含了三个流对象:
stdin: 标准输入 #0
stdout: 标准输出 #1
stderr: 标准错误 #2
CLI
- process.argv: 包含了所有 Node 程序运行时的参数值
第一个元素始终是 Node
第二个元素始终是执行的文件路径
要获取命令行参数则需要将前两个元素去掉:process.argv.slice(2)
process.cwd(): 获得程序运行时的当前工作目录
__dirname: 获取程序本身所在的目录process.chdir(): 允许灵活地更改工作目录
process.env: 访问环境变量
process.env.NODE_ENV
process.env.SHELL
process.exit(1): 退出程序
process.on(‘SIGKILL’, function() {
// 信号已收到
})
ANSI 转义码
要在文本终端下控制格式、颜色、以及其他输出选项,可以用 ANSI 转义码
console.log('\033[90m' + 'hello world' + '\033[39m')
\033: 表示转义的开始[: 表示开始颜色设置90: 表示前景色为亮灰色m: 表示颜色设置结束
FS API
fs.readFile()
fs.writeFile()
fs.createReadStream(): 允许为一个文件创建一个可读的 Stream 对象,其对内存的分配不是一次完成的
fs.readFile('my-file.txt', function(err, contents) {// 对文件进行处理})// ----------------------------------------------------var stream = fs.createReadStream('my-file.txt')stream.on('data', function(chunk) {// 处理文件部分内容});stream.on('end', function(chunk) {// 文件读取完毕})
fs.watch: 监视目录是否发生变化
fs.watchFile: 监视文件是否发生变化,监视意味着当文件系统中的文件(or 目录)发生变化时,会分发一个事件,然后触发指定的回调函数
// 例子:查找工作目录下所有的CSS 文件,然后监视其是否发生改变,一旦发生改变,就将该文件名输出到控制台var fs = require('fs')var stream = fs.createReadStream('my-file.txt')// 获取工作目录下所有的文件var files = fs.readdirSync(process.cwd())files.forEach(function(file) {// 监听“.css"后缀的文件if (/\.css/.test(file)) {fs.watchFile(process.cwd() + '/' + file, function() {console.log(' - ' + file + 'changed!');})}})
TCP
传输控制协议是一个面向连接的协议,它保证了两台计算机之间数据传输的可靠性和顺序。是一种传输层协议,它可以让你将数据从一台计算机完整有序地传输到另一台计算机。
Node 中的 http.Server 继承自 net.Server(net 是 TCP 模块)
Web浏览器和服务器(HTTP) | 邮件客户端(SMTP/IMAP/POP) | 聊天程序(IRC/XMPP) | 远程 shell(SSH) 等都是基于 TCP 协议的
当在TCP 连接内进行数据传递时,发送的IP数据包含了标识该连接以及数据流顺序的信息,从而做到让数据包送达时是有效的
基于确认和超时来实现一系列的达到可靠性的要求
通过流控制来确保两点之间传输数据的平衡
内置机制能够控制数据包的延迟率及丢包率
端口号:23
Telnet 中输入任何信息都会立刻发送到服务器,在Node服务器端,通过
\n来判断消息是否已完全到达TCP 是面向字节的协议
结束 Telnet 连接:
Mac:
Alt + [windows:
Ctrl + ]
Telnet 到 web 服务器
// server.jsrequire('http').createServer(function(req, res) {res.writeHead(200, {'Content-Type': 'text/html'})res.end(`<h1>Hello world</h1>`)}).listen(3000)
nodemon server.js
telnet localhost 3000GET/HTTP/1.1
基于 TCP 的聊天程序
需求:
成功连接到服务器后,服务器会显示欢迎信息,并要求输入用户名。同时还会告诉你当前还有多少其他客户端也连接到了该服务器
输入用户名,按下回车键后,就认为成功连接上了
连接后,就可以通过输入信息再按下回车键,来向其他客户端进行消息的收发
var net = require('net')// 状态:追踪连接数var count = 0, users = {}var server = net.createServer(function(conn) {// 设置编码conn.setEncoding('utf8')// 代表当前连接的昵称var nickname// handle connection// 接收一个net.Stream,该对象是既可读又可写的// console.log('new connection', conn);conn.write('\n > ' + 'welcome to node-chat!'+ '\n > ' + count + ' other people are connected at this time.'+ '\n > ' + 'please write your name and press enter:')count++// 处理客户端发送的数据conn.on('data', function(data) {// 接收到数据时,确保将\r\n(相当于按下回车键)清除data = data.replace('\r\n', '')// 接收到的第一份数据应当是用户输入的昵称if (!nickname) {// 对于尚未注册的用户,需要进行校验。if (users[data]) {conn.write('> nickname already in use. tyr again: ')return} else {// 如果昵称可用,则通知其他客户端当前用户已经连接进来了nickname = datausers[nickname] = connfor (var i in users) {// users[i].write(nickname + ' joined the room\n')broadcast(nickname + ' joined the room\n')}}} else {// 否则,视为聊天消息for (var i in users) {// 确保消息只发送给除了自己以外的其他客户端if (i != nickname) {// users[i].write(nickname + data)broadcast(nickname + data, true)}}}console.log(data);})// 当客户端请求关闭连接时,计数器变量就要进行递减操作conn.on('close', function() {count--// 当有人断开连接时,需要清除users数组中对应的元素delete users[nickname]// 用户断开时通知其他用户broadcast(nickname + 'left the room')})function broadcast(msg, exceptMyself) {// 给所有的用户广播消息for (var i in users) {if (!exceptMyself || i != nickname) {users[i].write(msg)}}}})// 监听server.listen(3000, function() {console.log('server listening on ');})
end: 当客户端显示关闭 TCP 连接时触发。比如,当你关闭 telnet 时,它会发送一个名为 “FIN” 的包给服务器,意味着要结束连接。
close: 当底层套接字关闭时触发。比如,当连接发生错误时(触发 error 事件),end 事件不会触发,因为服务器端并未收到 “FIN” 包信息。
共享状态的并发:两个不同连接的用户需要修改同一个状态变量
HTTP
HTTP 协议和 IRC 协议一样流行,目的是进行文档交换
NODE 默认添加的头信息
Transfer-Encoding: chunked
Connection: keep-alive
Content-Type: 告诉客户端其分发的内容类型: 文本、HTML、XML、JSON、PNG、JPEG图片等等
在调用 res.end() 前可以多次调用 res.write() 来发送数据
以数据块的形式将文件写入到响应中的好处:
高效的内存分配。要是对每个请求在写入前都完全把图片信息读取完,在处理大量请求时会消耗大量内存
数据一旦就绪就可以立刻写入了
NODE 的 HTTP 服务器拿到浏览器发送的数据后,对其进行分析(解析),然后构造了一个JS对象方便我们在脚本中使用,它甚至将所有的头信息都变成了小写
可以通过
req.connection获取 TCP 连接对象req.url: Node 会将主机名后所有的内容都放在 url 属性中Content-Type 为 urlencoded: 搜索部分的 URL 和表单内容一样都是经过编码的
require('http').createServer(function (req, res) {res.writeHead(200)res.write('Hello')setTimeout(function() {res.end('World')}, 5000)}).listen(3000)
querystring 模块
Node 提供 querystring 模块可以将 查询字符串 解析成一个JS对象
// qs.jsconsole.log(require('querystring').parse('name=lulu'))console.log(require('querystring').parse('q=lulu+liuliu'))
一个简单的 Web 服务器
// http.jsvar qs = require('querystring')require('http').createServer(function (req, res) {if ('/' == req.url) {res.writeHead(200, {'Content-Type': 'text/html'})res.end(['<form method="POST" action="/url">','<h1>My form</h1>','<fieldset>','<label>Personal information</label>','<p>What is your name?</P>','<input type="text" name="name">','<p><button>Submit</button></p>','</form>'].join(''))} else if ('/url' == req.url && 'POST' == req.method) {var body = ''req.on('data', function (chunk) {body += chunk})req.on('end', function () {res.writeHead(200, {'Content-Type': 'text/html'})res.end('<p>Your name is <b>' + qs.parse(body).name + '</b></p>')})} else {res.writeHead(404)res.end('Not Found')}}).listen(3000)
一个简单的 Web 客户端
// server.jsvar qs = require('querystring')require('http').createServer(function (req, res) {if ('/' == req.url) {var body = ''req.on('data', function (chunk) {body += chunk})req.on('end', function () {res.writeHead(200, {'Content-Type': 'text/html'})res.end('Done')console.log('\n got name \033[90m' + qs.parse(body).name + '\033[39m\n')})} else {res.writeHead(404)res.end('Not Found')}}).listen(3000)
// client.jsvar qs = require('querystring')function send(name) {require('http').request({host: '127.0.0.1',port: 3000,url: '/',method: 'POST',}, function (res) {var body = ''res.setEncoding('utf8')res.on('data', function (chunk) {body += chunk})res.on('end', function () {console.log('\n \033[90m request complete!\033[39m')process.stdout.write('\n your name: ')})}).end(qs.stringify({name: name}))}process.stdout.write('\n your name: ')process.stdin.resume()process.stdin.setEncoding('utf-8')process.stdin.on('data', function (name) {send(name.replace('\n', ''))})
Twitter 客户端
var qs = require('querystring')var http = require('http')var search = process.argv.slice(2).join(' ').trim()if (!search.length) {return console.log('\n Usage: node tweets <search term>\n')}console.log('\n searching for: \033[96m' + search + '\033[39m\n')// 用这种方式就不需要调用 end() 方法了http.get({host: 'search.twitter.com',path: '/search.json?' + qs.stringify({q: search}),method: 'GET'}, function (res) {var body = ''res.setEncoding('utf8')res.on('data', function (chunk) {body += chunk})res.on('end', function () {var obj = JSON.parse(body)console.log(obj)})})
superagent
HTTP 客户端:获取所有响应数据,根据响应消息的 Content-Type 值进行数据解析,处理消息数据;当向服务器发送数据时,情况也类似,会创建一个POST请求,然后将要发送的数据对象编码为JSON格式,并将解码后的内容放到 res.body 变量中
superagent 是基于 HTTP 客户端 API 的更高层封装,可以让上述处理变得更容易一些
npm i superagent@0.3.0
var request = require('superagent')request.get('http://twitter.com/search.json').send({q: 'justin'}).set({json: 'encoded'}).end(function(res) {console.log(res.body)})
send 和 set 方法均可被调用多次,并且均为渐进式 API:可以进行链式调用,并最后通过 end 方法来结束
提供了 get | put | post | head | del 等方法
JSON是默认的编码格式,可以通过调用set()更改请求的Content-Type的值来更改
up
自动重启 HTTP 服务器
- 要确保代码结构必须将 Node HTTP 服务器暴露出来,而不是调用 listen 来启动
npm i -g up
// server.jsmodule.exports = require('http').createServer(function(req, res) {res.writeHead(200, {'Content-Type': 'text/html'})res.end('Hello <b>World</b>')})
up -watch -port 80 server.js
使用 HTTP 构建一个简单的网站
需求:
托管静态文件
处理错误以及损坏或者不存在的URL
处理不同类型的请求
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"><title>Demo</title></head><body><h1>My website</h1><img src="/images/1.jpeg" alt=""><img src="/images/2.png" alt=""><img src="/images/3.jpeg" alt=""><img src="/images/4.jpeg" alt=""></body></html>
var qs = require('querystring')var fs = require('fs')require('http').createServer(function (req, res) {if ('GET' == req.method && '/images' == req.url.substr(0, 7)) {// 检查文件是否存在fs.stat(__dirname + req.url, function (err, stat) {if (err || !stat.isFile()) {res.writeHead(404)res.end('Not Found')return}serve(__dirname + req.url, 'application/jpg')})} else if ('GET' == req.method && '/' == req.url){// 根据文件路径来获取文件内容serve(__dirname + '/index.html', 'text/html')} else {res.writeHead(404)res.end('Not Found')}function serve(path, type) {res.writeHead(200, {'Content-Type': type})fs.createReadStream(path).pipe(res)// fs.createReadStream(path)// .on('data', function (data) {// res.write(data)// })// .on('end', function () {// res.end()// })}}).listen(3000)
通过 Connect 实现一个简单的网站
需求:
记录请求处理时间
托管静态文件
处理授权
Connect 是基于 http 模块 API 之上的
通过使用 use() 方法来添加中间件
中间件由函数组成,它除了处理 req 和 res 对象之外,还接收一个 next() 来做流控制
npm i connect
var connect = require('connect')var server = connect()server.use(function (req, res, next) {// 记录日志console.error(' %S %S', req.method, req.url)next()})server.use(function (req, res, next) {if ('GET' == req.method && 'images/' == req.url.substr(0, 7)) {// 托管图片} else {// 交给其他的中间件去处理next()}})server.use(function (req, res, next) {if ('GET' == req.method && '/' == req.url) {// 响应 index 文件} else {// 交给其他中间件去处理next()}})server.use(function (req, res, next) {// 最后一个中间件,如果到了这里,就意味着无能为力,只能返回 404 了res.writeHead(404)res.end('Not Found')})server.listen(3000)
中间件
中间件由函数组成,它除了处理 req 和 res 对象之外,还接收一个 next() 来做流控制
代码能以中间件为构建单元进行组织,并且能够获得高复用性
var connect = require('connect');var http = require('http');var app = connect();app.use(function middleware1(req, res, next) {// middleware 1next();});http.createServer(app).listen(3000);
书写可重用的中间件
用于当请求时间过长而进行提醒的中间件
// timeout.jsmodule.exports = function (opts) {var time = opts.time || 100return function (req, res, next) {// 确保响应时间在100ms以内时要清除(停下来或者取消)计时器var timer = setTimeout(function () {console.log('\033[90m%s %s\033[39m \033[91m is taking too long!\033[39m', req.method, req.url);}, time)// 保持对原始函数的引用var end = res.endres.end = function (chunk, encoding) {// 在重写的函数中,再恢复原始函数,并调用它res.end = endres.end(chunk, encoding)// 最后清除计时器clearTimeout(timer)}// 最后,总是要让其他中间件能够处理请求,所以得调用next,否则,程序不会做任何事情next()}}
// server.jsvar connect = require('connect')var morgan = require('morgan')var time = require('./timeout.js')// 创建服务器console.log(connect);var server = connect()// 记录请求情况server.use(morgan('dev'))server.use(time({time: 500}))// 实现快速响应server.use(function (req, res, next) {if ('/a' == req.url) {res.writeHead(200)res.end('Fast!')} else {next()}})// 实现模拟的慢速响应server.use(function (req, res, next) {if ('/b' == req.url) {setTimeout(function () {res.writeHead(200)res.end('Slow!')}, 1000)} else {next()}})// 服务器监听端口server.listen(3000)
serve-static
挂载:允许将任意一个 URL 匹配到文件系统中的任意一个目录
maxAge: 代表一个资源在客户端缓存的时间。对一些不经常改动的资源来说非常有用,浏览器就无需每次都去请求它了
hidden:为true时,Connect会托管那些文件名以
.开始的在UNIX文件系统中被认为是隐藏的文件
var express = require('express')var path = require('path')var serveStatic = require('serve-static')var app = express()app.use(serveStatic(path.join(__dirname, 'public'), {maxAge: '1d',setHeaders: setCustomCacheControl}))app.listen(3000)function setCustomCacheControl (res, path) {if (serveStatic.mime.lookup(path) === 'text/html') {// Custom Cache-Control for HTML filesres.setHeader('Cache-Control', 'public, max-age=0')}}
qs
查询字符串 和 JS 对象 之间的相互转换
morgan
是一个对 Web 应用非常有用的诊断工具,将发送进来的请求信息和发送出去的响应信息打印在终端
四种日志格式:
default
dev: 是一种精准简短的日志格式,能够提供行为及性能方面的信息,方便测试 Web 应用
short
tiny
允许自定义日志输出格式,下面是完整的可用token:
:req[header](如req[Accept]):res[header](如res[Content-Type]):http-version:response-time:remote-addr:date:method:url:referrer:user-agent:status
var morgan = require('morgan')morgan('dev')
body-parser
body-parser会检测Content-Type 的值,解析POST请求的消息体
var express = require('express')var bodyParser = require('body-parser')var app = express()// create application/json parservar jsonParser = bodyParser.json()// create application/x-www-form-urlencoded parservar urlencodedParser = bodyParser.urlencoded({ extended: false })// POST /login gets urlencoded bodiesapp.post('/login', urlencodedParser, function (req, res) {if (!req.body) return res.sendStatus(400)res.send('welcome, ' + req.body.username)})// POST /api/users gets JSON bodiesapp.post('/api/users', jsonParser, function (req, res) {if (!req.body) return res.sendStatus(400)// create user in req.body})
formidable
处理用户上传的文件
// index.html<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"><title>Demo</title></head><body><h1>处理上传</h1><form action="/upload" method="POST" enctype="multipart/form-data"><input type="file" name="files[]"><input type="file" name="files[]"><button>Send file!</button></form></body></html>
// server.jsvar qs = require('querystring')var fs = require('fs')var formidable = require('formidable')var form = new formidable.IncomingForm()require('http').createServer(function (req, res) {if ('/' == req.url) {serve(__dirname + '/index.html', 'text/html')} else if ('/upload' == req.url){form.parse(req, function(err, fields, files) {// 服务端会输出上传信息的对象console.log(files);});res.end('upload')} else {res.writeHead(404)res.end('Not Found')}function serve(path, type) {res.writeHead(200, {'Content-Type': type})fs.createReadStream(path).pipe(res)}}).listen(3000)
cookie-parser
当浏览器发送 cookie 数据时,会将其写到 Cookie 头信息中。其数据格式和 URL 中的查询字符串类似
Cookie: secret1=value;secret2=value2
var express = require('express')var cookieParser = require('cookie-parser')var app = express()app.use(cookieParser())app.get('/', function(req, res) {console.log('Cookies: ', req.cookies)})app.listen(3000)
cookie-session
用户会话主要通过在浏览器中设置cookie来实现,该cookie信息会在随后所有的请求头信息中被带回到服务器
var cookieSession = require('cookie-session')var express = require('express')var app = express()app.use(cookieSession({name: 'session',keys: [/* secret keys */],// Cookie OptionsmaxAge: 24 * 60 * 60 * 1000 // 24 hours}))
实现一个简单的登陆系统
// user.json{"lulu": {"password": "1234","name": "lulu"}}
npm install cookie-parser cookie-session connect
var connect = require('connect')var http = require('http')var morgan = require('morgan')var bodyParser = require('body-parser')var cookieSession = require('cookie-session')var users = require('./user.json')var app = connect()app.use(morgan('dev'))app.use(bodyParser.json())app.use(bodyParser.urlencoded({extended: false}))app.use(cookieSession({name: 'session',keys: ['my app secret'],// Cookie OptionsmaxAge: 24 * 60 * 60 * 1000 // 24 hours}))app.use(function (req, res, next) {// 检查用户是否已经登陆if ('/' == req.url && req.session.logged_in) {res.writeHead(200, {'Content-Type': 'text/html'})res.end('Welcome back, <b>' + req.session.name + '</b>' + '<a href="/logout">Logout</a>')} else {// 如果没有登陆,则交给其他中间件处理next()}})app.use(function (req, res, next) {// 展示一个登陆表单if ('/' == req.url && 'GET' == req.method) {res.writeHead(200, {'Content-Type': 'text/html'})res.end(['<form action="/login" method="POST">','<fieldset>','<legend>Please log in</legend>','<p>User: <input type="text" name="user"></p>','<p>Password: <input type="text" name="password"></p>','<button>Submit</button>','</fieldset>','</form>',].join(''))} else {next()}})app.use(function (req, res, next) {// 中间件检查登陆表单的信息是否与用户凭证匹配if ('/login' == req.url && 'POST' == req.method ) {res.writeHead(200)console.log(req.body);console.log(users[req.body.user], req.body.password, users[req.body.user].password)if (!users[req.body.user] || req.body.password != users[req.body.user].password) {res.end('Bad username/password')} else {// req.session对象在请求发送出去时会自动保存,无须我们手动处理req.session.logged_in = truereq.session.name = users[req.body.user].nameres.end('Authenticated')}} else {next()}})app.use(function (req, res, next) {// 处理登出if('/logout' == req.url) {req.session.logged_in = falseres.writeHead(200)res.end('Logged out!')} else {next()}})http.createServer(app).listen(3000)
method-override
var express = require('express')var methodOverride = require('method-override')var app = express()// override with the X-HTTP-Method-Override header in the requestapp.use(methodOverride('X-HTTP-Method-Override'))
connect-redis
将session 信息持久化存储下来的机制
Redis 是一个既小又快的数据库
node-mongodb-native
通过 Node.js 操作 MongoDB 文档数据的驱动器
Express
查询 Twitter API 的小应用
ejs(内嵌的 JS),将 JS 代码嵌在 <% 和 %> 的 EJS 标签中, 通过在 <% 之后加入 = 符号将变量值打印出来
使用视图引擎,无须显式地指明 index.ejs
npm i express ejs superagent
// views/index.ejs<h1>Twitter website</h1><p>Please enter your search term: </p><form action="/search" method="GET"><input type="text" name="q"><button>Search</button></form>
// views/search.ejs<h1>Tweet results for <%= search %></h1><% if (results.length) {%><ul><% for (var i = 0; i < results.length; i++) { %><li><%= results[i].text %> - <em><%= results[i].from_user %></em></li><% } %></ul><% } else { %><p>No results</p><% } %>
// search.jsvar request = require('superagent')module.exports = function search(query, url, fn) {// .send({q: query})request.get('http://api.douban.com/v2/movie/in_theaters').send(null).end(function (res) {console.log(res)if (res.body && Array.isArray(res.body.subjects)) {return fn(null, res.body.subjects)}fn(new Error('Bad twitter response'))})}
// server.jsvar express = require('express')var bodyParser = require('body-parser')var search = require('./search')// 返回 HTTP 服务器自带配置系统var app = express()// 通过 set 方法修改默认的配置项app.set('view engine', 'ejs')app.set('views', __dirname + '/views')// view options 参数所定义的选项,在渲染视图时,会传递到每个模板中。layout 的值设置为 false, 是为了匹配 Express 3 中的默认值app.set('view options', {layout: false})// 获取 views 的配置信息console.log(app.set('views'));app.use(bodyParser.json())app.use(bodyParser.urlencoded({extended: false}))// 配置路由app.get('/', function (req, res) {res.render('index')})app.get('/search', function (req, res, next) {search(req.query.q, 'http://api.douban.com/v2/movie/in_theaters', function (err, movies) {if (err) return next(err)res.render('search', {results: tweets, search: req.query.q})})})app.listen(3000)
cheerio
操作 DOM 结构
const cheerio = require('cheerio')const $ = cheerio.load('<h2 class="title">Hello world</h2>')$('h2.title').text('Hello there!')$('h2').addClass('welcome')$.html()//=> <h2 class="title welcome">Hello there!</h2>
设置
- 在生产环境下,让 express 将模板缓存起来
app.configure('production', function() {app.enable('view cache')})
- 设置环境变量
NODE_ENV
app.configure('development', function() {// gets called in the absence of NODE_ENV too!})
大小写敏感的路由
严格路由
jsonp回调:启用 res.send() | res.json() 对 jsonp 的支持
模版引擎
Haml
Jade
CoffeeKup
jQuery Templates for node
将 html 拓展名匹配到 jade 模板引擎
app.register('.html', require('jade'))
错误处理
定义一个特殊的错误处理器作为错误处理的中间件
app.error(function(err, req, res, next) {if('Bad twitter response' == err.message) {res.render('twitter-error')} else {next()}})
可以设置多个 .error 处理器来执行不同的处理
app.error(function(err, req, res) {res.render('error', {status: 500})})
快捷方法
- Request:
header: 让程序以函数的方式获取头信息
accepts: 分析请求中的 Accept 头信息,并根据提供的值返回 true 或者 false
is:检查 content-type 的信息, 和 accepts 类似
- Response:
header:接收一个参数来检查对应的头信息是否已经在 response 上设置了
render:传递响应消息
send:根据提供参数的类型执行响应的行为
json: 显示将内容作为JSON对象发送
redirect:等效于发送302(暂时移除)状态码及 Location 头信息
sendfiel: 和 connect 中的 static 中间件类似,不同之处在于它用于单个文件
路由
:引用的变量值会注入到 req.params 对象上
在变量后添加问号(?)来表示该变量是可选的
定义路由时也可以直接使用 RegExp 对象
在路由处理程序中也可以使用 next
中间件
可以使用 Connect 兼容的中间件
允许只在特定匹配到的路由中才使用中间件
通过调用
next('route'),就能确保当前路由会被跳过
WebSocket
由于浏览器会和服务器之间建立多个 socket 通道,因此发送多个Ajax请求就无法控制服务器接收请求的先后顺序
WebSocket 是 Web 下的 TCP,一个底层的双向 socket,允许用户对消息传递进行控制
WebSocket:
浏览器实现的 WebSocket API
服务器端实现的 WebSocket 协议
建立在 HTTP 之上
和 XMLHttpRequest 不同,并非面向请求和响应,而是
- 直接通过 send() 进行消息传递- 通过 data事件,发送和接收 UTF-8 或者二进制编码的消息- 通过 open 和 close 事件能够获知连接打开和关闭的状态
MongoDB
面向文档,结构设计(schema)无关(schema-less)的数据库
能够与其他键-值形式的 NoSQL 数据库区别开来的是文档可以是
任意深度的数据类型可以混用
jade 语法
jade 使用的是缩进(默认两个空格,应当避免使用 tab),而不是复杂的嵌套 XML、HTML 标签
使用 jade 只需输入标签名,后面仅跟内容即可
使用 doctype html 自动插入 HTML5 的 doctype
代码中还使用了特殊的关键字 block, 这样其他视图文件就能嵌入到这个位置。其他还包括 if 和 else 这样特殊的关键字
属性的写法看起来就像是 HTML 和 JavaScript 代码的混合体,且非常容易嵌入变量(或者 locals,express 将从 controller 中暴露给视图层的变量称为 locals)
可以通过
#{}这样的写法来嵌入变量特殊字符“竖线”(|)
特殊字符“.”
嵌入的Javascript代码必须以“-”开头
用户认证应用
npm i express mongodb jade
// views/layout.jadedoctype 5http://mp.weixin.qq.com/s?__biz=MjM5MTA4MjE5OA==&mid=2660144832&idx=1&sn=7115bd2dff1561062f70dded8d3c6da3&chksm=bdc0c5798ab74c6ff0f29ef67392bdb113fe84204fbfcc666c76b234bf9d8de622c530096fc4&scene=0#rdhtmlheadtitle MongoDb examplebodyh1 My first MongoDB apphrblock body
// views/index.jadeextends layoutblock bodyif (authenticated)p Welcome back, #{me.first}a(href='/logout') Logoutelsep Welcome new visitor!ulli: a(href="/login") Loginli: a(href="/signup") Signup
// views/login.jadeextends layoutblock bodyform(action="/login", method="POST")fieldsetlegend Log inplabel Emailinput(name="user[email]", type="text")plabel Passwordinput(name="user[password]", type="password")pbutton Submitpa(href="/") Go back
// views/signup.jadeextends layoutblock bodyform(action="/signup", method="POST")fieldsetlegend Sign upplabel Firstinput(name="user[first]", type="text")plabel Lastinput(name="user[last]", type="text")plabel Emailinput(name="user[email]", type="text")plabel Passwordinput(name="user[password]", type="text")pbutton Submitpa(href="/") Go back
// server.jsvar express = require('express')var mongodb = require('mongodb')var bodyParser = require('body-parser')var cookieParser = require('cookie-parser')var cookieSession = require('cookie-session')// var search = require('./search')// 返回 HTTP 服务器自带配置系统var app = express()// 通过 set 方法修改默认的配置项app.set('view engine', 'jade')app.set('views', __dirname + '/views')// view options 参数所定义的选项,在渲染视图时,会传递到每个模板中。layout 的值设置为 false, 是为了匹配 Express 3 中的默认值app.set('view options', {layout: false})// 获取 views 的配置信息console.log(app.set('views'));app.use(bodyParser.json())app.use(bodyParser.urlencoded({extended: false}))app.use(cookieParser())app.use(cookieSession({name: 'session',keys: ['my secret'],// Cookie OptionsmaxAge: 24 * 60 * 60 * 1000 // 24 hours}))// 配置路由// 默认路由app.get('/', function (req, res) {res.render('index', {authenticated: true})})// 登录路由app.get('/login', function (req, res) {res.render('login')})// 注册路由app.get('/signup', function (req, res) {res.render('signup')})app.listen(3000)
启动数据库
mongod --config /usr/local/etc/mongod.conf新启动一个命令行工具:执行
“mongo“, 则会生成一个 mongo 客户端在 mongo 客户端运行
show log global命令,会看到连接成功的消息

show dbs: 查看数据库信息show tables: 查看集合(数据表)
mongodb 语法
find(): 查找数据limit(): 指定返回条数skip(): 跳过指定条数
drop(): 删除集合
Mongoose
mongoose 提前知道需要什么样的数据类型,所以它总是会尝试去做类型转换
yarn add mongoose
MySQL
mysql 驱动器:node-mysql
对象关系映射器 ORM : 提供了一个 MySQL 数据库中数据到 JS 模型对象的映射,使得操作数据关系、数据处理等变得更加容易
购物车应用
npm i express jade mysql
/: 展示所有的商品以及创建商品的表单/item/<id>: 展示指定的商品以及用户评价/item// review(POST):创建一个评价/item/create(POST): 创建一个商品
// server.js
