• 了解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

  1. Date.prototype.__defineGetter__('ago', function() {
  2. var diff = ((new Date()).getTime() - this.getTime()) / 1000;
  3. var day_diff = Math.floor( diff / 86400 );
  4. return day_diff == 0 && (
  5. diff < 60 && "just now" ||
  6. diff < 120 && "1 minute ago" ||
  7. diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" ||
  8. diff < 7200 && "1 hour ago" ||
  9. diff < 86400 && Math.floor( diff / 3600 ) + " hours ago" ||
  10. day_diff == 1 && "Yesterday" ||
  11. day_diff < 7 && day_diff + " days ago" ||
  12. Math.ceil( day_diff / 7 ) + " weeks ago";
  13. )
  14. })
  15. var a = new Date('12/12/1990')
  16. 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 escape code

要在文本终端下控制格式、颜色、以及其他输出选项,可以用 ANSI 转义码

  1. console.log('\033[90m' + 'hello world' + '\033[39m')
  • \033: 表示转义的开始

  • [: 表示开始颜色设置

  • 90: 表示前景色为亮灰色

  • m: 表示颜色设置结束

FS API

  • fs.readFile()

  • fs.writeFile()

  • fs.createReadStream(): 允许为一个文件创建一个可读的 Stream 对象,其对内存的分配不是一次完成的

  1. fs.readFile('my-file.txt', function(err, contents) {
  2. // 对文件进行处理
  3. })
  4. // ----------------------------------------------------
  5. var stream = fs.createReadStream('my-file.txt')
  6. stream.on('data', function(chunk) {
  7. // 处理文件部分内容
  8. });
  9. stream.on('end', function(chunk) {
  10. // 文件读取完毕
  11. })
  • fs.watch: 监视目录是否发生变化

  • fs.watchFile: 监视文件是否发生变化,监视意味着当文件系统中的文件(or 目录)发生变化时,会分发一个事件,然后触发指定的回调函数

  1. // 例子:查找工作目录下所有的CSS 文件,然后监视其是否发生改变,一旦发生改变,就将该文件名输出到控制台
  2. var fs = require('fs')
  3. var stream = fs.createReadStream('my-file.txt')
  4. // 获取工作目录下所有的文件
  5. var files = fs.readdirSync(process.cwd())
  6. files.forEach(function(file) {
  7. // 监听“.css"后缀的文件
  8. if (/\.css/.test(file)) {
  9. fs.watchFile(process.cwd() + '/' + file, function() {
  10. console.log(' - ' + file + 'changed!');
  11. })
  12. }
  13. })

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 服务器

  1. // server.js
  2. require('http').createServer(function(req, res) {
  3. res.writeHead(200, {'Content-Type': 'text/html'})
  4. res.end(`<h1>Hello world</h1>`)
  5. }).listen(3000)
  1. nodemon server.js
  1. telnet localhost 3000
  2. GET/HTTP/1.1

基于 TCP 的聊天程序

需求:

  • 成功连接到服务器后,服务器会显示欢迎信息,并要求输入用户名。同时还会告诉你当前还有多少其他客户端也连接到了该服务器

  • 输入用户名,按下回车键后,就认为成功连接上了

  • 连接后,就可以通过输入信息再按下回车键,来向其他客户端进行消息的收发

  1. var net = require('net')
  2. // 状态:追踪连接数
  3. var count = 0, users = {}
  4. var server = net.createServer(function(conn) {
  5. // 设置编码
  6. conn.setEncoding('utf8')
  7. // 代表当前连接的昵称
  8. var nickname
  9. // handle connection
  10. // 接收一个net.Stream,该对象是既可读又可写的
  11. // console.log('new connection', conn);
  12. conn.write(
  13. '\n > ' + 'welcome to node-chat!'
  14. + '\n > ' + count + ' other people are connected at this time.'
  15. + '\n > ' + 'please write your name and press enter:')
  16. count++
  17. // 处理客户端发送的数据
  18. conn.on('data', function(data) {
  19. // 接收到数据时,确保将\r\n(相当于按下回车键)清除
  20. data = data.replace('\r\n', '')
  21. // 接收到的第一份数据应当是用户输入的昵称
  22. if (!nickname) {
  23. // 对于尚未注册的用户,需要进行校验。
  24. if (users[data]) {
  25. conn.write('> nickname already in use. tyr again: ')
  26. return
  27. } else {
  28. // 如果昵称可用,则通知其他客户端当前用户已经连接进来了
  29. nickname = data
  30. users[nickname] = conn
  31. for (var i in users) {
  32. // users[i].write(nickname + ' joined the room\n')
  33. broadcast(nickname + ' joined the room\n')
  34. }
  35. }
  36. } else {
  37. // 否则,视为聊天消息
  38. for (var i in users) {
  39. // 确保消息只发送给除了自己以外的其他客户端
  40. if (i != nickname) {
  41. // users[i].write(nickname + data)
  42. broadcast(nickname + data, true)
  43. }
  44. }
  45. }
  46. console.log(data);
  47. })
  48. // 当客户端请求关闭连接时,计数器变量就要进行递减操作
  49. conn.on('close', function() {
  50. count--
  51. // 当有人断开连接时,需要清除users数组中对应的元素
  52. delete users[nickname]
  53. // 用户断开时通知其他用户
  54. broadcast(nickname + 'left the room')
  55. })
  56. function broadcast(msg, exceptMyself) {
  57. // 给所有的用户广播消息
  58. for (var i in users) {
  59. if (!exceptMyself || i != nickname) {
  60. users[i].write(msg)
  61. }
  62. }
  63. }
  64. })
  65. // 监听
  66. server.listen(3000, function() {
  67. console.log('server listening on ');
  68. })
  • 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 和表单内容一样都是经过编码的

  1. require('http').createServer(function (req, res) {
  2. res.writeHead(200)
  3. res.write('Hello')
  4. setTimeout(function() {
  5. res.end('World')
  6. }, 5000)
  7. }).listen(3000)

querystring 模块

Node 提供 querystring 模块可以将 查询字符串 解析成一个JS对象

  1. // qs.js
  2. console.log(require('querystring').parse('name=lulu'))
  3. console.log(require('querystring').parse('q=lulu+liuliu'))

一个简单的 Web 服务器

  1. // http.js
  2. var qs = require('querystring')
  3. require('http').createServer(function (req, res) {
  4. if ('/' == req.url) {
  5. res.writeHead(200, {
  6. 'Content-Type': 'text/html'
  7. })
  8. res.end([
  9. '<form method="POST" action="/url">',
  10. '<h1>My form</h1>',
  11. '<fieldset>',
  12. '<label>Personal information</label>',
  13. '<p>What is your name?</P>',
  14. '<input type="text" name="name">',
  15. '<p><button>Submit</button></p>',
  16. '</form>'
  17. ].join(''))
  18. } else if ('/url' == req.url && 'POST' == req.method) {
  19. var body = ''
  20. req.on('data', function (chunk) {
  21. body += chunk
  22. })
  23. req.on('end', function () {
  24. res.writeHead(200, {
  25. 'Content-Type': 'text/html'
  26. })
  27. res.end('<p>Your name is <b>' + qs.parse(body).name + '</b></p>')
  28. })
  29. } else {
  30. res.writeHead(404)
  31. res.end('Not Found')
  32. }
  33. }).listen(3000)

一个简单的 Web 客户端

  1. // server.js
  2. var qs = require('querystring')
  3. require('http').createServer(function (req, res) {
  4. if ('/' == req.url) {
  5. var body = ''
  6. req.on('data', function (chunk) {
  7. body += chunk
  8. })
  9. req.on('end', function () {
  10. res.writeHead(200, {
  11. 'Content-Type': 'text/html'
  12. })
  13. res.end('Done')
  14. console.log('\n got name \033[90m' + qs.parse(body).name + '\033[39m\n')
  15. })
  16. } else {
  17. res.writeHead(404)
  18. res.end('Not Found')
  19. }
  20. }).listen(3000)
  1. // client.js
  2. var qs = require('querystring')
  3. function send(name) {
  4. require('http').request({
  5. host: '127.0.0.1',
  6. port: 3000,
  7. url: '/',
  8. method: 'POST',
  9. }, function (res) {
  10. var body = ''
  11. res.setEncoding('utf8')
  12. res.on('data', function (chunk) {
  13. body += chunk
  14. })
  15. res.on('end', function () {
  16. console.log('\n \033[90m request complete!\033[39m')
  17. process.stdout.write('\n your name: ')
  18. })
  19. }).end(qs.stringify({name: name}))
  20. }
  21. process.stdout.write('\n your name: ')
  22. process.stdin.resume()
  23. process.stdin.setEncoding('utf-8')
  24. process.stdin.on('data', function (name) {
  25. send(name.replace('\n', ''))
  26. })

Twitter 客户端

  1. var qs = require('querystring')
  2. var http = require('http')
  3. var search = process.argv.slice(2).join(' ').trim()
  4. if (!search.length) {
  5. return console.log('\n Usage: node tweets <search term>\n')
  6. }
  7. console.log('\n searching for: \033[96m' + search + '\033[39m\n')
  8. // 用这种方式就不需要调用 end() 方法了
  9. http.get({
  10. host: 'search.twitter.com',
  11. path: '/search.json?' + qs.stringify({q: search}),
  12. method: 'GET'
  13. }, function (res) {
  14. var body = ''
  15. res.setEncoding('utf8')
  16. res.on('data', function (chunk) {
  17. body += chunk
  18. })
  19. res.on('end', function () {
  20. var obj = JSON.parse(body)
  21. console.log(obj)
  22. })
  23. })

superagent

HTTP 客户端:获取所有响应数据,根据响应消息的 Content-Type 值进行数据解析,处理消息数据;当向服务器发送数据时,情况也类似,会创建一个POST请求,然后将要发送的数据对象编码为JSON格式,并将解码后的内容放到 res.body 变量中

superagent 是基于 HTTP 客户端 API 的更高层封装,可以让上述处理变得更容易一些

  1. npm i superagent@0.3.0
  1. var request = require('superagent')
  2. request.get('http://twitter.com/search.json')
  3. .send({q: 'justin'})
  4. .set({json: 'encoded'})
  5. .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 来启动
  1. npm i -g up
  1. // server.js
  2. module.exports = require('http').createServer(function(req, res) {
  3. res.writeHead(200, {'Content-Type': 'text/html'})
  4. res.end('Hello <b>World</b>')
  5. })
  1. up -watch -port 80 server.js

使用 HTTP 构建一个简单的网站

需求:

  • 托管静态文件

  • 处理错误以及损坏或者不存在的URL

  • 处理不同类型的请求

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
  6. <title>Demo</title>
  7. </head>
  8. <body>
  9. <h1>My website</h1>
  10. <img src="/images/1.jpeg" alt="">
  11. <img src="/images/2.png" alt="">
  12. <img src="/images/3.jpeg" alt="">
  13. <img src="/images/4.jpeg" alt="">
  14. </body>
  15. </html>
  1. var qs = require('querystring')
  2. var fs = require('fs')
  3. require('http').createServer(function (req, res) {
  4. if ('GET' == req.method && '/images' == req.url.substr(0, 7)) {
  5. // 检查文件是否存在
  6. fs.stat(__dirname + req.url, function (err, stat) {
  7. if (err || !stat.isFile()) {
  8. res.writeHead(404)
  9. res.end('Not Found')
  10. return
  11. }
  12. serve(__dirname + req.url, 'application/jpg')
  13. })
  14. } else if ('GET' == req.method && '/' == req.url){
  15. // 根据文件路径来获取文件内容
  16. serve(__dirname + '/index.html', 'text/html')
  17. } else {
  18. res.writeHead(404)
  19. res.end('Not Found')
  20. }
  21. function serve(path, type) {
  22. res.writeHead(200, {'Content-Type': type})
  23. fs.createReadStream(path).pipe(res)
  24. // fs.createReadStream(path)
  25. // .on('data', function (data) {
  26. // res.write(data)
  27. // })
  28. // .on('end', function () {
  29. // res.end()
  30. // })
  31. }
  32. }).listen(3000)

通过 Connect 实现一个简单的网站

需求:

  • 记录请求处理时间

  • 托管静态文件

  • 处理授权

  • Connect 是基于 http 模块 API 之上的

  • 通过使用 use() 方法来添加中间件

  • 中间件由函数组成,它除了处理 req 和 res 对象之外,还接收一个 next() 来做流控制

  1. npm i connect
  1. var connect = require('connect')
  2. var server = connect()
  3. server.use(function (req, res, next) {
  4. // 记录日志
  5. console.error(' %S %S', req.method, req.url)
  6. next()
  7. })
  8. server.use(function (req, res, next) {
  9. if ('GET' == req.method && 'images/' == req.url.substr(0, 7)) {
  10. // 托管图片
  11. } else {
  12. // 交给其他的中间件去处理
  13. next()
  14. }
  15. })
  16. server.use(function (req, res, next) {
  17. if ('GET' == req.method && '/' == req.url) {
  18. // 响应 index 文件
  19. } else {
  20. // 交给其他中间件去处理
  21. next()
  22. }
  23. })
  24. server.use(function (req, res, next) {
  25. // 最后一个中间件,如果到了这里,就意味着无能为力,只能返回 404 了
  26. res.writeHead(404)
  27. res.end('Not Found')
  28. })
  29. server.listen(3000)

中间件

  • 中间件由函数组成,它除了处理 req 和 res 对象之外,还接收一个 next() 来做流控制

  • 代码能以中间件为构建单元进行组织,并且能够获得高复用性

  1. var connect = require('connect');
  2. var http = require('http');
  3. var app = connect();
  4. app.use(function middleware1(req, res, next) {
  5. // middleware 1
  6. next();
  7. });
  8. http.createServer(app).listen(3000);

书写可重用的中间件

用于当请求时间过长而进行提醒的中间件

  1. // timeout.js
  2. module.exports = function (opts) {
  3. var time = opts.time || 100
  4. return function (req, res, next) {
  5. // 确保响应时间在100ms以内时要清除(停下来或者取消)计时器
  6. var timer = setTimeout(function () {
  7. console.log('\033[90m%s %s\033[39m \033[91m is taking too long!\033[39m', req.method, req.url);
  8. }, time)
  9. // 保持对原始函数的引用
  10. var end = res.end
  11. res.end = function (chunk, encoding) {
  12. // 在重写的函数中,再恢复原始函数,并调用它
  13. res.end = end
  14. res.end(chunk, encoding)
  15. // 最后清除计时器
  16. clearTimeout(timer)
  17. }
  18. // 最后,总是要让其他中间件能够处理请求,所以得调用next,否则,程序不会做任何事情
  19. next()
  20. }
  21. }
  1. // server.js
  2. var connect = require('connect')
  3. var morgan = require('morgan')
  4. var time = require('./timeout.js')
  5. // 创建服务器
  6. console.log(connect);
  7. var server = connect()
  8. // 记录请求情况
  9. server.use(morgan('dev'))
  10. server.use(time({time: 500}))
  11. // 实现快速响应
  12. server.use(function (req, res, next) {
  13. if ('/a' == req.url) {
  14. res.writeHead(200)
  15. res.end('Fast!')
  16. } else {
  17. next()
  18. }
  19. })
  20. // 实现模拟的慢速响应
  21. server.use(function (req, res, next) {
  22. if ('/b' == req.url) {
  23. setTimeout(function () {
  24. res.writeHead(200)
  25. res.end('Slow!')
  26. }, 1000)
  27. } else {
  28. next()
  29. }
  30. })
  31. // 服务器监听端口
  32. server.listen(3000)

serve-static

  • 挂载:允许将任意一个 URL 匹配到文件系统中的任意一个目录

  • maxAge: 代表一个资源在客户端缓存的时间。对一些不经常改动的资源来说非常有用,浏览器就无需每次都去请求它了

  • hidden:为true时,Connect会托管那些文件名以.开始的在UNIX文件系统中被认为是隐藏的文件

  1. var express = require('express')
  2. var path = require('path')
  3. var serveStatic = require('serve-static')
  4. var app = express()
  5. app.use(serveStatic(path.join(__dirname, 'public'), {
  6. maxAge: '1d',
  7. setHeaders: setCustomCacheControl
  8. }))
  9. app.listen(3000)
  10. function setCustomCacheControl (res, path) {
  11. if (serveStatic.mime.lookup(path) === 'text/html') {
  12. // Custom Cache-Control for HTML files
  13. res.setHeader('Cache-Control', 'public, max-age=0')
  14. }
  15. }

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

  1. var morgan = require('morgan')
  2. morgan('dev')

body-parser

body-parser会检测Content-Type 的值,解析POST请求的消息体

  1. var express = require('express')
  2. var bodyParser = require('body-parser')
  3. var app = express()
  4. // create application/json parser
  5. var jsonParser = bodyParser.json()
  6. // create application/x-www-form-urlencoded parser
  7. var urlencodedParser = bodyParser.urlencoded({ extended: false })
  8. // POST /login gets urlencoded bodies
  9. app.post('/login', urlencodedParser, function (req, res) {
  10. if (!req.body) return res.sendStatus(400)
  11. res.send('welcome, ' + req.body.username)
  12. })
  13. // POST /api/users gets JSON bodies
  14. app.post('/api/users', jsonParser, function (req, res) {
  15. if (!req.body) return res.sendStatus(400)
  16. // create user in req.body
  17. })

formidable

处理用户上传的文件

  1. // index.html
  2. <!DOCTYPE html>
  3. <html lang="en">
  4. <head>
  5. <meta charset="UTF-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
  7. <title>Demo</title>
  8. </head>
  9. <body>
  10. <h1>处理上传</h1>
  11. <form action="/upload" method="POST" enctype="multipart/form-data">
  12. <input type="file" name="files[]">
  13. <input type="file" name="files[]">
  14. <button>Send file!</button>
  15. </form>
  16. </body>
  17. </html>
  1. // server.js
  2. var qs = require('querystring')
  3. var fs = require('fs')
  4. var formidable = require('formidable')
  5. var form = new formidable.IncomingForm()
  6. require('http').createServer(function (req, res) {
  7. if ('/' == req.url) {
  8. serve(__dirname + '/index.html', 'text/html')
  9. } else if ('/upload' == req.url){
  10. form.parse(req, function(err, fields, files) {
  11. // 服务端会输出上传信息的对象
  12. console.log(files);
  13. });
  14. res.end('upload')
  15. } else {
  16. res.writeHead(404)
  17. res.end('Not Found')
  18. }
  19. function serve(path, type) {
  20. res.writeHead(200, {'Content-Type': type})
  21. fs.createReadStream(path).pipe(res)
  22. }
  23. }).listen(3000)

cookie-parser

当浏览器发送 cookie 数据时,会将其写到 Cookie 头信息中。其数据格式和 URL 中的查询字符串类似

  1. Cookie: secret1=value;secret2=value2
  1. var express = require('express')
  2. var cookieParser = require('cookie-parser')
  3. var app = express()
  4. app.use(cookieParser())
  5. app.get('/', function(req, res) {
  6. console.log('Cookies: ', req.cookies)
  7. })
  8. app.listen(3000)

cookie-session

用户会话主要通过在浏览器中设置cookie来实现,该cookie信息会在随后所有的请求头信息中被带回到服务器

  1. var cookieSession = require('cookie-session')
  2. var express = require('express')
  3. var app = express()
  4. app.use(cookieSession({
  5. name: 'session',
  6. keys: [/* secret keys */],
  7. // Cookie Options
  8. maxAge: 24 * 60 * 60 * 1000 // 24 hours
  9. }))

实现一个简单的登陆系统

  1. // user.json
  2. {
  3. "lulu": {
  4. "password": "1234",
  5. "name": "lulu"
  6. }
  7. }
  1. npm install cookie-parser cookie-session connect
  1. var connect = require('connect')
  2. var http = require('http')
  3. var morgan = require('morgan')
  4. var bodyParser = require('body-parser')
  5. var cookieSession = require('cookie-session')
  6. var users = require('./user.json')
  7. var app = connect()
  8. app.use(morgan('dev'))
  9. app.use(bodyParser.json())
  10. app.use(bodyParser.urlencoded({extended: false}))
  11. app.use(cookieSession({
  12. name: 'session',
  13. keys: ['my app secret'],
  14. // Cookie Options
  15. maxAge: 24 * 60 * 60 * 1000 // 24 hours
  16. }))
  17. app.use(function (req, res, next) {
  18. // 检查用户是否已经登陆
  19. if ('/' == req.url && req.session.logged_in) {
  20. res.writeHead(200, {'Content-Type': 'text/html'})
  21. res.end('Welcome back, <b>' + req.session.name + '</b>' + '<a href="/logout">Logout</a>')
  22. } else {
  23. // 如果没有登陆,则交给其他中间件处理
  24. next()
  25. }
  26. })
  27. app.use(function (req, res, next) {
  28. // 展示一个登陆表单
  29. if ('/' == req.url && 'GET' == req.method) {
  30. res.writeHead(200, {'Content-Type': 'text/html'})
  31. res.end([
  32. '<form action="/login" method="POST">',
  33. '<fieldset>',
  34. '<legend>Please log in</legend>',
  35. '<p>User: <input type="text" name="user"></p>',
  36. '<p>Password: <input type="text" name="password"></p>',
  37. '<button>Submit</button>',
  38. '</fieldset>',
  39. '</form>',
  40. ].join(''))
  41. } else {
  42. next()
  43. }
  44. })
  45. app.use(function (req, res, next) {
  46. // 中间件检查登陆表单的信息是否与用户凭证匹配
  47. if ('/login' == req.url && 'POST' == req.method ) {
  48. res.writeHead(200)
  49. console.log(req.body);
  50. console.log(users[req.body.user], req.body.password, users[req.body.user].password)
  51. if (!users[req.body.user] || req.body.password != users[req.body.user].password) {
  52. res.end('Bad username/password')
  53. } else {
  54. // req.session对象在请求发送出去时会自动保存,无须我们手动处理
  55. req.session.logged_in = true
  56. req.session.name = users[req.body.user].name
  57. res.end('Authenticated')
  58. }
  59. } else {
  60. next()
  61. }
  62. })
  63. app.use(function (req, res, next) {
  64. // 处理登出
  65. if('/logout' == req.url) {
  66. req.session.logged_in = false
  67. res.writeHead(200)
  68. res.end('Logged out!')
  69. } else {
  70. next()
  71. }
  72. })
  73. http.createServer(app).listen(3000)

method-override

  1. var express = require('express')
  2. var methodOverride = require('method-override')
  3. var app = express()
  4. // override with the X-HTTP-Method-Override header in the request
  5. app.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

  1. npm i express ejs superagent
  1. // views/index.ejs
  2. <h1>Twitter website</h1>
  3. <p>Please enter your search term: </p>
  4. <form action="/search" method="GET">
  5. <input type="text" name="q">
  6. <button>Search</button>
  7. </form>
  1. // views/search.ejs
  2. <h1>Tweet results for <%= search %></h1>
  3. <% if (results.length) {%>
  4. <ul>
  5. <% for (var i = 0; i < results.length; i++) { %>
  6. <li><%= results[i].text %> - <em><%= results[i].from_user %></em></li>
  7. <% } %>
  8. </ul>
  9. <% } else { %>
  10. <p>No results</p>
  11. <% } %>
  1. // search.js
  2. var request = require('superagent')
  3. module.exports = function search(query, url, fn) {
  4. // .send({q: query})
  5. request.get('http://api.douban.com/v2/movie/in_theaters')
  6. .send(null)
  7. .end(function (res) {
  8. console.log(res)
  9. if (res.body && Array.isArray(res.body.subjects)) {
  10. return fn(null, res.body.subjects)
  11. }
  12. fn(new Error('Bad twitter response'))
  13. })
  14. }
  1. // server.js
  2. var express = require('express')
  3. var bodyParser = require('body-parser')
  4. var search = require('./search')
  5. // 返回 HTTP 服务器自带配置系统
  6. var app = express()
  7. // 通过 set 方法修改默认的配置项
  8. app.set('view engine', 'ejs')
  9. app.set('views', __dirname + '/views')
  10. // view options 参数所定义的选项,在渲染视图时,会传递到每个模板中。layout 的值设置为 false, 是为了匹配 Express 3 中的默认值
  11. app.set('view options', {layout: false})
  12. // 获取 views 的配置信息
  13. console.log(app.set('views'));
  14. app.use(bodyParser.json())
  15. app.use(bodyParser.urlencoded({extended: false}))
  16. // 配置路由
  17. app.get('/', function (req, res) {
  18. res.render('index')
  19. })
  20. app.get('/search', function (req, res, next) {
  21. search(req.query.q, 'http://api.douban.com/v2/movie/in_theaters', function (err, movies) {
  22. if (err) return next(err)
  23. res.render('search', {results: tweets, search: req.query.q})
  24. })
  25. })
  26. app.listen(3000)

cheerio

操作 DOM 结构

  1. const cheerio = require('cheerio')
  2. const $ = cheerio.load('<h2 class="title">Hello world</h2>')
  3. $('h2.title').text('Hello there!')
  4. $('h2').addClass('welcome')
  5. $.html()
  6. //=> <h2 class="title welcome">Hello there!</h2>

设置

  • 在生产环境下,让 express 将模板缓存起来
  1. app.configure('production', function() {
  2. app.enable('view cache')
  3. })
  • 设置环境变量 NODE_ENV
  1. app.configure('development', function() {
  2. // gets called in the absence of NODE_ENV too!
  3. })
  • 大小写敏感的路由

  • 严格路由

  • jsonp回调:启用 res.send() | res.json() 对 jsonp 的支持

模版引擎

  • Haml

  • Jade

  • CoffeeKup

  • jQuery Templates for node

将 html 拓展名匹配到 jade 模板引擎

  1. app.register('.html', require('jade'))

错误处理

定义一个特殊的错误处理器作为错误处理的中间件

  1. app.error(function(err, req, res, next) {
  2. if('Bad twitter response' == err.message) {
  3. res.render('twitter-error')
  4. } else {
  5. next()
  6. }
  7. })

可以设置多个 .error 处理器来执行不同的处理

  1. app.error(function(err, req, res) {
  2. res.render('error', {status: 500})
  3. })

快捷方法

  • 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 不同,并非面向请求和响应,而是

  1. - 直接通过 send() 进行消息传递
  2. - 通过 data事件,发送和接收 UTF-8 或者二进制编码的消息
  3. - 通过 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代码必须以“-”开头

  • Jade 官方文档

  • Jade 中文文档

用户认证应用

  1. npm i express mongodb jade
  1. // views/layout.jade
  2. doctype 5http://mp.weixin.qq.com/s?__biz=MjM5MTA4MjE5OA==&mid=2660144832&idx=1&sn=7115bd2dff1561062f70dded8d3c6da3&chksm=bdc0c5798ab74c6ff0f29ef67392bdb113fe84204fbfcc666c76b234bf9d8de622c530096fc4&scene=0#rd
  3. html
  4. head
  5. title MongoDb example
  6. body
  7. h1 My first MongoDB app
  8. hr
  9. block body
  1. // views/index.jade
  2. extends layout
  3. block body
  4. if (authenticated)
  5. p Welcome back, #{me.first}
  6. a(href='/logout') Logout
  7. else
  8. p Welcome new visitor!
  9. ul
  10. li: a(href="/login") Login
  11. li: a(href="/signup") Signup
  1. // views/login.jade
  2. extends layout
  3. block body
  4. form(action="/login", method="POST")
  5. fieldset
  6. legend Log in
  7. p
  8. label Email
  9. input(name="user[email]", type="text")
  10. p
  11. label Password
  12. input(name="user[password]", type="password")
  13. p
  14. button Submit
  15. p
  16. a(href="/") Go back
  1. // views/signup.jade
  2. extends layout
  3. block body
  4. form(action="/signup", method="POST")
  5. fieldset
  6. legend Sign up
  7. p
  8. label First
  9. input(name="user[first]", type="text")
  10. p
  11. label Last
  12. input(name="user[last]", type="text")
  13. p
  14. label Email
  15. input(name="user[email]", type="text")
  16. p
  17. label Password
  18. input(name="user[password]", type="text")
  19. p
  20. button Submit
  21. p
  22. a(href="/") Go back
  1. // server.js
  2. var express = require('express')
  3. var mongodb = require('mongodb')
  4. var bodyParser = require('body-parser')
  5. var cookieParser = require('cookie-parser')
  6. var cookieSession = require('cookie-session')
  7. // var search = require('./search')
  8. // 返回 HTTP 服务器自带配置系统
  9. var app = express()
  10. // 通过 set 方法修改默认的配置项
  11. app.set('view engine', 'jade')
  12. app.set('views', __dirname + '/views')
  13. // view options 参数所定义的选项,在渲染视图时,会传递到每个模板中。layout 的值设置为 false, 是为了匹配 Express 3 中的默认值
  14. app.set('view options', {layout: false})
  15. // 获取 views 的配置信息
  16. console.log(app.set('views'));
  17. app.use(bodyParser.json())
  18. app.use(bodyParser.urlencoded({extended: false}))
  19. app.use(cookieParser())
  20. app.use(cookieSession({
  21. name: 'session',
  22. keys: ['my secret'],
  23. // Cookie Options
  24. maxAge: 24 * 60 * 60 * 1000 // 24 hours
  25. }))
  26. // 配置路由
  27. // 默认路由
  28. app.get('/', function (req, res) {
  29. res.render('index', {authenticated: true})
  30. })
  31. // 登录路由
  32. app.get('/login', function (req, res) {
  33. res.render('login')
  34. })
  35. // 注册路由
  36. app.get('/signup', function (req, res) {
  37. res.render('signup')
  38. })
  39. app.listen(3000)

启动数据库

  • mongod --config /usr/local/etc/mongod.conf

  • 新启动一个命令行工具:执行“mongo“, 则会生成一个 mongo 客户端

  • 在 mongo 客户端运行 show log global 命令,会看到连接成功的消息

了不起的 Node.js - 图1

  • show dbs : 查看数据库信息

  • show tables : 查看集合(数据表)

mongodb 语法

  • find(): 查找数据

    • limit(): 指定返回条数

    • skip(): 跳过指定条数

  • drop(): 删除集合

Mongoose

mongoose 提前知道需要什么样的数据类型,所以它总是会尝试去做类型转换

  1. yarn add mongoose

MySQL

  • mysql 驱动器:node-mysql

  • 对象关系映射器 ORM : 提供了一个 MySQL 数据库中数据到 JS 模型对象的映射,使得操作数据关系、数据处理等变得更加容易

购物车应用

  1. npm i express jade mysql
  • /: 展示所有的商品以及创建商品的表单

  • /item/<id>: 展示指定的商品以及用户评价

  • /item// review(POST):创建一个评价

  • /item/create(POST): 创建一个商品

  1. // server.js