CruxF / Blog

个人博客,记载学习的点点滴滴,怕什么技术无穷,进一寸有一寸的欢喜:sparkles:
63 stars 19 forks source link

Node开发实战知识整理 #13

Open CruxF opened 6 years ago

CruxF commented 6 years ago

工具

介绍一款很方便的在线编辑器——webIDE,点击进入官网

前言

这是一本书的学习总结,书名为《Node.js开发实战详解》,作者为腾讯出身的黄丹华。由于这会是一篇很长的课程学习总结,为了优化大家的阅读体验,强烈建议安装Chrome浏览器的插件——GayHub。下载安装地址

第 1 章:Node.js基础知识

为什么使用Node

因为Node能处理高并发请求,而且由于Node.js是事件驱动,因此能够更好的节约服务器内存资源。同时,Node.js可以单独实现一个server,这也是Node一个非常大的优点,对于那些简单的server,通过Node实现比使用C++实现会简单得多。

最后,牢记Node解决了长连接、多请求引发的成本问题,因此在一些项目中,比如实时在线的游戏、实时聊天室、实时消息推送功能、实时监控系统等开发过程中,应该把握机会,应用Node来开发。

同步调用和异步调用

1、同步调用时一种阻塞式调用,一段代码调用另一段代码时,必须等待这段代码执行结束并返回结果后,代码才能继续执行下去。例如下面的代码

let n1 = 1
let n2 = 2
let n3 = 3
alert(n1)
alert(n2)
alert(n3)

2、异步调用是一种非阻塞式调用,一段异步代码还未执行完,可以继续执行下一段代码逻辑,当其他同步代码执行完之后,通过回调返回继续执行相应的逻辑,而不耽误其他代码的执行。例如下面的代码

let n1 = 1
let n2 = 2
let n3 = 3
alert(n1)
setTimeout(function() {
  alert(n2)
}, 2000)
alert(n3)

当然,关于异步还有另外一个例子,这个栗子也引出了异步调用和回调这两个东东的概念,下面请看代码

function Person() {
  this.think = function(callback) {
    setTimeout(function() {
      console.log('想出来了!')
      callback()
    }, 5000)
  }
  this.answer = function() {
    console.log('我正在思考一个问题^_^')
  }
}
var person = new Person()
person.think(function() {
  console.log('花费5s,得到一个正确的思考')
})
person.answer()


回调和异步调用

首先明确一点,回调并非是异步调用,回调是一种解决异步函数执行结果的处理方法。在异步调用里,如果我们希望将执行的结果返回并且处理时,可以通过回调的方法解决。为了能够更好的区分回调和异步回调的区别,下面看一个简单的栗子

function waitFive(name, callbackfn) {
  var pus = 0
  var currentDate = new Date()
  while(pus < 5000) {
    var now = new Date()
    pus = now - currentDate
  }
  // 执行回调函数
  callbackfn(name)
}
// 定义回调函数echo()
function echo(name) {
  console.log(name)
}
// 调用waitFive方法
waitFive('回调函数被调用啦', echo)
console.log('略略略略')

以上代码是一个回调逻辑,但不是一个异步代码逻辑,因为其中并没有涉及Node的异步调用接口。从上面的代码结果可以看出回调和异步调用的区别,当waitFive()方法执行时,整个代码执行过程都会等待waitFive()方法的执行,而并非如异步调用那样:waitFive()方法未结束,还会继续执行console.log('略略略略')。这也说明了回调还是一种阻塞式调用。

获取异步函数的执行结果

异步函数往往不是直接返回执行结果,而是通过事件驱动的方式,将执行结果返回到回调函数中,之后在回调函数中处理相应的逻辑代码。

如何来理解以上的代码呢?请看下面一个代码案例

var dns = require('dns')
dns.resolve4('id.qq.com', function(err, address) {})
console.log(address)

dns.resolve4()是一个异步函数,由此带来的问题就是console.log(address)输出的结果是undefined,因为你懂得,异步嘛,对不对。

既然异步函数会出现这个问题,那么我们就可以使用回调函数去获取参数,下面请看代码

var dns = require('dns')
dns.resolve4('id.qq.com', function(err, address) {
  if(err) {
    throw err
  }
  console.log(address)
})


第 2 章:模块和NPM

Node模块的概念

需要了解一点的是,Node会对原生模块和文件模块都进行缓存,因此在第二次require该模块的时候,不会有重复开销去加载模块,只需要从缓存中读取相应模块数据即可。

原生模块的调用

使用Node提供的API——require来加载相应的Node模块,require加载成功后会返回一个Node模块对象,该对象拥有该模块的所有属性和方法,如下代码

var httpModule = require('http')
httpModule.createServer(function(req, res) {

}).listen(port)

以上就是一个简单的调用原生模块的方法,Node中其他原生模块的调用方法都是一样的,主要是学会如何查看Node的API文档,以及如何应用其中的模块提供的方法和属性。

文件模块调用

文件模块的调用和原生模块的调用方式基本一致,但是需要注意的是,其两者的加载方式存在一定的区别,原生模块不需要指定模块路径,而文件模块加载时必须指定文件路径。比如我们在项目中创建一个test.js文件,代码如下

exports.name = '能被调用的变量'
exports.happy = function() {
  console.log('能被调用的方法')
}

var yourName = '不能被调用的变量'
function love() {
  console.log('不能被调用的方法')
}

接着我们在同一个目录中创建diaoyong.js文件加载test.js这个文件模块,代码如下

var test = require('./test.js')
console.log(test)

以上代码也指明了,在文件模块中,只有exports和module.exports对象暴露给该外部的属性和方法,才能够通过返回的require对象进行调用,其他方法和属性是无法获取的。

Node原生模块实现web解析DNS

我们使用Node的原生模块和文件模块两个方法实现DNS解析工具,通过分析对比,来说明文件模块存在的必要性,以及其存在的必要性。

下面我们先看一下使用原生模块创建的DNS解析工具代码(先创建parse_dns_ex.js文件)

// 加载创建web的HTTP服务器模块
var http = require('http')
// 加载DNS解析模块
var dns = require('dns')
// 加载文件读取模块
var fs = require('fs')
// 加载URL处理模块
var url = require('url')
// 加载处理请求参数模块
var querystring = require('querystring')
http.createServer(function(req, res) {
  res.writeHead(200, {
    'Content-Type': 'text/html'
  })
  // 获取当前html文件路径
  var readPath = __dirname + '/' + url.parse('2-1-3.html').pathname
  // 同步读取文件
  var indexPage = fs.readFileSync(readPath)
  res.end(indexPage)
}).listen(3000, '127.0.0.1')

接着我们创建一个html文件,代码如下

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>DNS查询</title>
  </head>
  <body>
    <h1 style="text-align: center;">DNS查询工具</h1>
    <div style="text-align: center;">
      <form action="/parse" method="post">
        <span>查询DNS:</span>
        <input type="text" name="search-dns" />
        <input type="submit" value="查询" />
      </form>
    </div>
  </body>
</html>

然后我们在命令行输入命令:node parse_dns_ex.js,就能够在浏览器输入http://127.0.0.1:3000,看到HTML页面内容。

由于以上代码只是实现了一个表单提交的页面,对于DNS解析部分还没有做任何处理,因此我们需要对js文件做如下的处理

var http = require('http')
var dns = require('dns')
var fs = require('fs')
var url = require('url')
var querystring = require("querystring")
http.createServer(function(req, res) {
  // 获取当前请求资源的url路径
  var pathname = url.parse(req.url).pathname
  // 设置编码格式,避免出现乱码
  req.setEncoding("utf8")
  res.writeHead(200, {
    'Content-Type': 'text/html'
  })
  router(res, req, pathname)
}).listen(3000, "127.0.0.1")
console.log('Server running at http://127.0.0.1:3000/')
// 路由函数实现
function router(res, req, pathname) {
  switch(pathname) {
    case "/parse":
      parseDns(res, req)
      break;
    default:
      goIndex(res, req)
  }
}
// 解析域名函数实现
function parseDns(res, req) {
  var postData = ""
  req.addListener("data", function(postDataChunk) {
    postData += postDataChunk
  });
  req.addListener("end", function() {
    var retData = getDns(postData, function(domain, addresses) {
      res.writeHead(200, {
        'Content-Type': 'text/html'
      });
      res.end(`
        <html>
          <head>
            <title>DNS解析结果</title>
            <meta charset='utf-8'>
          </head>
          <body>
            <div style='text-align:center'>
              Domain:<span style='color:red'>${domain}</span>
              IP:<span style='color:red'>${addresses.join(',')}</span>
            </div>
          </body>
        </html>
      `)
    })
    return
  })
}
// 返回html文件函数实现
function goIndex(res, req) {
  var readPath = __dirname + '/' + url.parse('2-1-3.html').pathname
  // 同步读取html文件的信息
  var indexPage = fs.readFileSync(readPath)
  res.end(indexPage)
}
// 异步解析域名函数
function getDns(postData, callback) {
  var domain = querystring.parse(postData).search_dns;
  dns.resolve(domain, function(err, addresses) {
    if(!addresses) {
      addresses = ['不存在域名']
    }
    callback(domain, addresses)
  })
}

以上代码就能够实现当你输入www.qq.com的时候,显示它的IP。

Node文件模块实现web解析DNS

文件模块的好处在于将业务处理分离,每个模块处理相应的职责,避免业务混乱。接下来我们分析DNS解析系统需要划分哪些模块,以及这些模块之间的功能和作用分别是什么。下面就来看看各个模块的作用以及具体代码。

入口模块(index.js),创建http服务器处理客户端请求

// 加载原生http和url模块
var http = require('http')
var url = require('url')
// 加载文件模块之路由处理模块
var router = require('./router.js')
http.createServer(function(req, res) {
  // HTTP请求路径
  var pathname = url.parse(req.url).pathname
  req.setEncoding('utf8')
  res.writeHead(200, {
    'Content-Type': 'text/html'
  })
  // router(res,req,pathname)是router文件模块中的exports方法
  router.router(res, req, pathname)
}).listen(3000, '127.0.0.1')
console.log('Server runing http://127.0.0.1:3000')

路由处理模块(router.js),处理所有请求资源,分发到相应处理器。说白了就是负责url转发以及请求资源分配。

// 加载文件模块之DNS解析模块
var ParseDns = require('./parse_dns.js')
// 加载文件模块之首页展示模块
var MainIndex = require('./main_index.js')
exports.router = function(res, req, pathname) {
  switch(pathname) {
    case '/parse':
      ParseDns.parseDns(res, req)
      break;
    default:
      MainIndex.goIndex(res, req)
  }
}

这里需要注意的是,router方法必须应用exports暴露给require返回的对象,如果不使用exports方法,相对于router.js文件模块来说就是私有方法,require router模块返回对象将无法调用。

DNS解析模块(parse_dns.js),DNS处理逻辑,根据获取的域名进行解析,返回相应的处理结果到页面,这部分代码和上面的原生模块的代码类似,主要是parseDns和getDns两个方法。

var querystring = require('querystring')
var dns = require('dns')
exports.parseDns = function(res, req) {
  var postData = ''
  req.addListener('data', function(postDataChunk) {
    postData += postDataChunk
  })
  req.addListener('end', function() {
    var retData = getDns(postData, function(domain, addresses) {
      res.writeHead(200, {
        'Content-Type': 'text/html'
      });
      res.end(`
        <html>
          <head>
            <title>DNS解析结果</title>
            <meta charset='utf-8'>
          </head>
          <body>
            <div style='text-align:center'>
              Domain:<span style='color:red'>${domain}</span>
              IP:<span style='color:red'>${addresses.join(',')}</span>
            </div>
          </body>
        </html>
      `)
    })
    return
  })
  // 获取post数据
  function getDns(postData, callback) {
    var domain = querystring.parse(postData).search_dns
    // 异步解析域名
    dns.resolve(domain, function(err, addresses) {
      if(!addresses) {
        addresses = ['不存在域名']
      }
      callback(domain, addresses)
    })
  }
}

以上的js代码对应html代码为

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title></title>
  </head>
  <body>
    <h1 style="text-align: center;">DNS查询工具</h1>
    <div style="text-align: center;">
      <form action="/parse" method="post">
        <span>查询DNS:</span>
        <input type="text" name="search_dns" />
        <input type="submit" value="查询" />
      </form>
    </div>
  </body>
</html>

首页展示模块(main_index.js),处理主页index.html页面的显示,使用fs模块进行读取index.html页面字符数据,然后返回到客户端。

var fs = require('fs')
var url = require('url')
exports.goIndex = function(res, req) {
  var readPath = __dirname + '/' + url.parse('index.html').pathname
  // 同步读取index.html页面数据
  var indexPage = fs.readFileSync(readPath)
  res.end(indexPage)
}

整个过程结束后,运行index.js同样可以实现跟原生模块DNS解析的作用。

exports和module.exports

两者的作用都是将文件模块的方法和属性暴露给require返回的对象进行调用。但是二者存在本质的区别:exports的属性和方法都可以被module.exports替代,反过来则不行。它们之间还有以下的不同点

// 调用index.js文件模块 var Book = require('./index.js') console.log('调用Book:' + Book) console.log('调用Book中的name:' + Book.name) console.log('调用Book中的showName():' + Book.showName())


### 习题检测
(1)实现person.js文件模块,其返回的是一个person函数,该函数中有eat和say方法
```js
module.exports = function() {
  this.eat = function(){}
  this.say = function(){}
}

(2)实现person.js文件模块,其返回的是一个eat方法和say方法的对象

exports.Person = {
  'eat':function() {},
  'say':function() {}
}

(3)实现person.js文件模块,其返回的是一个数组,数组内容为人名

module.exports = ['jack', 'tom', 'lucy']

(4)实现person.js文件模块,其返回的是一个对象,该对象中包含一个数组元素

exports.arr = ['jack', 'tom', 'lucy']

使用Express

express是一个Node.js的web开源框架,该框架可以快速搭建web项目开发的框架。其主要集成了web的http服务器的创建、静态文件管理、服务器url请求处理、get和post请求分支、session处理等功能。下面是使用express的步骤

查看当前版本:express --version

使用jade模块

该模块的作用就是可以内嵌其他代码到html页面中,比如在html页面中内嵌php代码

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title></title>
  </head>
  <body>
    <?php
      $name = 'hello php';
      echo $name;
    ?>
  </body>
</html>

下面我们在一个express项目中使用jade模块。首先创建一个jade.js文件,记得事先安装(npm install jade)好了jade模块

var express = require('express');
var http = require('http');
var app = express();
// 设置模板引擎
app.set('view engine', 'jade');
// 设置模板相对路径(相对当前目录)
app.set('views', __dirname);
app.get('/', function(req, res) {
  // 调用当前路径下的 test.jade 模板
  res.render('test');
})
var server = http.createServer(app);
server.listen(3002);
console.log('server started on http://127.0.0.1:3002/');

接着,创建一个test.jade模板

- console.log('hello'); // 这段代码在服务端执行
- var s = 'hello world' // 在服务端空间中定义变量
p #{s}
p= s

最后,在命令行终端运行:node jade.js。关于更多jade知识,可以点击这里进行学习。

习题检测

在安装好jade模块的express项目中,修改routes/index.js文件,传递一个名为info的json对象,json值为{'name':'jack','book':'Node.js'}。同时在views/index.jade页面使用div展示name,使用h2标签展示book

var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
  res.render('index.jade', {
    name: 'jack',
    book: 'Node.js',
    title: 'a'
  })
});
module.exports = router

extends layout
block content
  h2 #{book}
  p Welcome to #{name}

使用forever模块

服务器管理是系统上线后,必须要面对的问题。最好有一个软件可以提供整套的服务器运行解决方案:要求运行稳定,支持高并发,启动/停止命令简单,支持热部署,宕机重启,监控界面和日志,集群环境。接下来,就让我们使用forever模块来实现以上的要求。

使用步骤

更多关于forever模块的知识请点击这里,不过到了现在,该模块貌似被PM2给取代了,有兴趣的去看看吧。

使用socket.io模块

socket.io是一个基于Node.js的项目,其作用主要是将WebSocket协议应用到所有的浏览器。该模块主要应用于实时的长连接、多请求项目中,例如在线互联网游戏、实时聊天、实时股票查看、二维码扫描登录等。

由于书中给的案例并没有跑起来,因此只将其代码抄了下来,相关的解释说明等过后再说

// 设置监听端口
var io = require('socket.io').listen(8080)
// 当客户端connection时,执行回调函数
io.sockets.on('connection', function(socket) {
  // 连接成功后发送一个news消息,消息内容为json对象
  socket.emit('news', {
    hello: 'world'
  })
  // 客户端发送my other event消息时,服务器端接受该消息
  // 成功获取该消息后执行回调函数
  socket.on('my other event', function(data) {
    console.log(data)
  })
})

以及客户端代码

<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <title>socket</title>
  </head>
  <body>
    <script src="socket.js"></script>
    <script>
      // 使用socket连接本地socket服务器
      var socket = io.connect('http://localhost:8080')
      // 接受到服务器发送的news消息后,当服务器推送news消息后执行回调函数
      socket.on('news', function(data) {
        console.log(data)
        // 客户端接受news消息成功后,发送my other event消息到
        // 服务器,其发送的消息内容为json对象
        socket.emit('my other event', {
          my: 'data'
        })
      })
    </script>
  </body>
</html>

使用request模块

request模块为Node.js开发者提供了一种简单访问HTTP请求的方法。在开始之前我们需要进行模块安装npm install request,不过在安装之前要记得先使用npm init初始化一个package.json文件,否则模块是安装不上去的。

下面我们来看重点内容,get和post请求。要牢记的是:get和post只是发送机制不同,并不是一个取一个发!

首先看看处理get请求,首先创建一个HTTP服务器发出一个请求(app_get.js)

// 创建一个http服务器
var http = require('http')
http.createServer(function(req, res) {
  res.writeHead(200, {
    'Content-Type': 'text/plain'
  })
  // 返回一个字符 ,并且打印出请求的方式
  res.end('Hello World\n' + req.method)
}).listen(1337, "127.0.0.1")
console.log('服务运行地址=> http://127.0.0.1:1337/');

然后通过request模块去请求该服务器数据,并将服务器返回结果打印出来(request_get.js)

var request = require('request')
request.get('http://127.0.0.1:1337', function(error, response, result) {
  console.log(result)
})

应用request模块的get方法,发起一个HTTP请求,请求本地http://127.0.0.1:1337服务器数据,request.get()方法中的两个参数分别是请求url和回调函数。

接下来运行node app_get.js代码,启动服务器,可以到浏览器看看输出的内容;然后再打开一个命令行(记得之前启动服务器的命令行不要关闭),运行node request_get.js代码,可以看到从服务器中请求回来的数据。

下面我们再来看一看关于post请求的一些代码,和get方式类似,也是先创建一个HTTP服务器(app_post.js),该服务器接收客户端的post参数,并将该参数作为字符串响应到客户端

var http = require('http')
// querystring从字面上的意思就是查询字符串,一般是对http请求所带的数据进行解析
var querystring = require('querystring')
http.createServer(function(req, res) {
  var postData = ""
  // 开始异步接收客户端post的数据
  req.addListener("data", function(postDataChunk) {
    postData += postDataChunk
  })
  // 异步post数据接收完成后执行匿名回调函数
  req.addListener("end", function() {
    // 解析客户端发送的post数据,并将其转化为字符
    var postStr = JSON.stringify(querystring.parse(postData))
    res.writeHead(200, {
      'Content-Type': 'text/plain'
    })
    // 响应客户端请求的post数据
    res.end(postStr + '\n' + req.method)
  })
}).listen(1400, "127.0.0.1")
console.log('Server running at http://127.0.0.1:1400/')

接着应用request模块发起HTTP的post请求,将数据传到HTTP服务器中,然后又取得服务器返回的数据(request_post.js)

var request = require('request')
request.post('http://127.0.0.1:1400', {
  // form意思是表单数据
  form: {
    'name': 'danhuang',
    'book': 'node.js'
  }
}, function(error, response, result) {
  console.log(result)
});

接下来运行node app_post.js代码,启动服务器,可以到浏览器看看输出的内容;然后再打开一个命令行(记得之前启动服务器的命令行不要关闭),运行node request_post.js代码,可以看到从服务器中返回来处理过的数据,同时将HTTP请求方式也响应到客户端。

使用Formidable模块

formidable模块实现了上传和编码图片和视频。它支持GB级上传数据处理,支持多种客户端数据提交。有极高的测试覆盖率,非常适合在生产环境中使用。

在原生的Node.js模块中,提供了获取post数据的方法,但是没有直接获取上传文件的方法,因此需要利用formidable模块来处理文件上传逻辑。接下来我们看一下实例(记得先npm init,然后npm install formidable)

var formidable = require('formidable')
var http = require('http')
var util = require('util')
// 创建一个HTTP服务器
http.createServer(function(req, res) {
  // 判断请求路径是否为upload,如果是则执行文件上传处理逻辑
  // 同时还判断HTTP请求方式是否为post
  if(req.url == '/upload' && req.method.toLowerCase() == 'post') {
    // 创建form对象
    var form = new formidable.IncomingForm()
    // 解析post数据
    form.Parse(req, function(err, fields, files) {
      res.writeHead(200, {
        'content-type': 'text/plain'
      })
      res.write('received upload:\n\n')
      // 将json对象转化为字符串
      res.end(util.inspect({
        fields: fields,
        files: files
      }))
    })
    return
  }
  res.writeHead(200, {
    'content-type': 'text/html'
  })
  res.end(`
    <form action='/upload' enctype='multipart/form-data' method='pst'>
      <input type='text' name='title'><br>
      <input type='file' name='upload' multiple='multiple'><br>
      <input type='submit' vaule='upload'>
    </form>
  `)
}).listen(8080)

之后运行这个项目,上传一张图片即可看到效果。

开发一个自己的NPM模块

$ npm whoami fengxiong

- 注意:如果出现403错误,那么很有可能是邮箱尚未验证或者仓库地址使用了淘宝镜像,[点击这里](https://blog.csdn.net/u012971339/article/details/80594903)查看解决方法
- 步骤八:发布模块(npm publish)
- 步骤九:下载使用该模块。

1、创建一个新项目 2、初始化一个package.json文件 3、安装自己的模块:npm install --save "原先模块package.json文件中的name值"

最后是下载该模块的项目结构
![image](https://user-images.githubusercontent.com/20301892/44297197-09877580-a300-11e8-97a3-87d002c7d94f.png)

我们可以在根目录的test.js文件中这么来写
```js
var fn = require('./node_modules/fengxiong/index.js')
fn.testNpm('hello,这是测试模块')

然后在运行node test.js,那么就能得到相应的结果。

模块与类

模块是程序设计中,为完成某一功能所需的一段程序或者子程序;或者指能由编译程序,装配程序等处理的独立程序单位;或者指大型软件系统的一部分。

而在Node.js中可以理解为完成某一功能所需的程序或者子程序,同时也可以将Node.js的一个模块理解为一个“类”,但注意,其本身并非是类,而只是简单意义上的一个对象,该对象拥有多个方法和属性,Node.js模块也拥有私有成员和公有成员。

一般来说exports和module.exports的成员为公有成员,而非exports和module.exports的成员为私有成员。

Node.js中的继承

继承的方式主要是通过Node.js的util模块inherits的API来实现继承,将一个构造函数的原型方法继承到另一个构造函数中。

constructor构造函数的原型将被设置为使用superConstructor构造函数所创建的一个新对象。可以看看下面的栗子,其目的就是使用MyStream继承events.EventEmitter对象

// 获取util和event模块
var util = require("util")
var events = require("events")

// 使用MyStream来继承events.EventEmitter方法属性
function MyStream() {
  events.EventEmitter.call(this)
}

// 应用inherits来实现Mystream继承EventEmitter
util.inherits(MyStream, events.EventEmitter)
// 为MyStream类添加方法
MyStream.prototype.write = function(data) {
  this.emit("data", data)
}

// 创建一个MyStream对象
var stream = new MyStream()
// 判断是否继承了events.EventEmitter
console.log(stream instanceof events.EventEmitter)
// 通过super_获取父类对象
console.log(MyStream.super_ === events.EventEmitter)

// 调用继承来自events.EventEmitter的方法
stream.on("data", function(data) {
  console.log("接收到的数据是:" + data)
})
stream.write("现在是2018-08-29 21:31")

上面的栗子可能稍微抽象些,下面我搞一个比较具体的栗子,比如:学生、老师、程序员继承人这个类,实现学生学习、老师教书、程序员写代码的功能。首先,我们创建一个Person基类作为人

module.exports = function() {
  this.name = 'person'
  // 定义sleep方法
  this.sleep = function() {
    console.log('入夜了,需要睡觉')
  }
  // 定义eat方法
  this.eat = function() {
    console.log('肚子饿了,吃点东西')
  }
}

下面我们再创建一个Student类继承Person类

var util = require("util")
var Person = require("./person")
// 定义Student类
function Student() {
  Person.call(this)
}
// 将Student类继承Person
util.inherits(Student, Person)
// 重写study方法
Student.prototype.study = function() {
  console.log('我正在学习')
}
// 暴露Student类
module.exports = Student

这样就实现了student继承person类,同时新增类的自我方法study。下面我们再来看看实现一个Teacher类继承Person类

var util = require('util')
var Person = require('./person')
// 定义Teacher类
function Teacher() {
  Person.call(this)
}
// 将Teacher类继承Person类
util.inherits(Teacher, Person)
// 重写teach方法
Teacher.prototype.teach = function() {
  console.log('我正在教学')
}
// 暴露Teacher类
module.exports = Teacher

为了加深下印象,我们再写一个Coder类继承Person类

var util = require("util")
var Person = require("./person")
// 定义个Coder类
function Coder() {
  Person.call(this)
}
// 继承
util.inherits(Coder, Person)
// 重写code方法
Coder.prototype.code = function() {
  console.log("我正在敲代码")
}
// 暴露Coder类
module.exports = Coder

下面我们进入重头戏,看看如何调用继承类,创个app.js文件

var Person = require('./person')
var Student = require('./student')
var Teacher = require('./teacher')
var Coder = require('./coder')
// 创建对象
var personObj = new Person()
var studentObj = new Student()
var teacherObj = new Teacher()
var coderObj = new Coder()

// 执行personObj对象的所有方法
console.log('---Person类---')
personObj.sleep()
personObj.eat()
console.log('----------')
// 执行studentObj对象的所有方法
console.log('---Student类---')
studentObj.sleep()
studentObj.eat()
studentObj.study()
console.log('----------')
// 执行teacherObj对象的所有方法
console.log('---Teacher类---')
teacherObj.sleep()
teacherObj.eat()
teacherObj.teach()
console.log('----------')
// 执行coderObj对象的所有方法
console.log('---Coder类---')
coderObj.sleep()
coderObj.eat()
coderObj.code()
console.log('----------')

之后在终端执行命令:node app.js,那么就可以看到继承的效果出现了。那么该如何改写父类定义好的方法呢?其实很简单,只要直接覆盖就行了,下面就来看看在Student类中重写父类的eat()方法

var util = require("util")
var Person = require("./person")
// 定义Student类
function Student() {
  Person.call(this)
  this.eat = function() {
    console.log('身为一个学生,肚子饿了也得忍着')
  }
}
// 将Student类继承Person
util.inherits(Student, Person)
// 重写study方法
Student.prototype.study = function() {
  console.log('我正在学习')
}
// 暴露Student类
module.exports = Student

动态类对象和静态类对象

Node.js中可以应用module.exports实现一个动态类对象,那么Node.js中如何实现一个静态类对象呢?例如:假设有一个基类Person,其有继承类Student,但是我们希望使用静态类对象的方式调用Student中的方法和属性,而不希望new一个对象。可以结合exports以及继承方法来实现静态类。

首先,我们先使用动态类对象调用的方式实现一下代码

// 基类Person
module.exports = function() {
  this.name = 'person'
  // 定义sleep方法
  this.sleep = function() {
    console.log('入夜了,需要睡觉')
  }
  // 定义eat方法
  this.eat = function() {
    console.log('肚子饿了,吃点东西')
  }
}

// 子类Student
var util = require("util")
var Person = require("./person")
// 定义Student类
function Student() {
  Person.call(this)
  // 将Student类继承Person
  util.inherits(Student, Person)
  this.study = function() {
    console.log('我正在学习')
  }
}
// 暴露Student类
module.exports = Student

// 动态调用Student类的方式
var Student = require('./student')
var student = new Student()
student.study()

现在我们使用静态类对象的调用方式,调用Student中的对象,那么我们可以将student这个模块实现方式修改为如下代码(这一处书中代码有误,我修改过来了):

// 子类Student
var util = require("util")
var Person = require("./person")
// 定义Student类
function Student() {
  Person.call(this)
  // 将Student类继承Person
  util.inherits(Student, Person)
  this.study = function() {
    console.log('我正在学习')
  }
}
var student = new Student()
// 暴露以下的方法
exports.study = student.study
exports.eat = student.eat
exports.sleep = student.sleep

// 静态调用Student类的方式
var student = require('./student')
student.study()
student.eat()
student.sleep()

通过在类定义模块中new一个本身对象,并将该对象的方法全部通过exports暴露给外部接口,就无需在每次调用该类的地方new一个该对象了。因此在使用上就可以将student这个类看成一个静态类

这种方法实现很简单,也很容易理解,在很多时候非常有用。这样做的好处是可以避免代码的冗余,当student这个类被多个地方调用时,如果是动态调用的话,就必须每次都去new一个该对象,而如果使用类静态方法调用时,就可以直接通过require返回的对象进行调用。

当然,不是所有的类都可以这样去调用,如果每次在该类的内部new一个对象都需要初始化一些参数变量,那么就可以选择使用静态调用方法。

习题练习

实现一个基类animal,该基类包含方法say,该方法输出内容,接下来实现两个继承类duck和bird,其中duck是一个静态类模块,其有方法say,该方法输出ga..ga...ga..,而bird则是一个动态调用模块,其有方法输出ji...ji...ji...

// 基类
module.exports = function() {
  this.say = function() {
    console.log('动物特性')
  }
}

// 子类A
var util = require('util')
var Animal = require('./animal')
function Duck() {
  // 应用arguments对象获取函数参数
  var _res = arguments[0]
  var _req = arguments[1]
  // 把animal对象中的this指向绑定到Duck对象中
  Animal.call(this)
  // 继承
  util.inherits(Duck, Animal)
  this.say = function() {
    console.log('ga..ga..ga..')
  }
}
// 暴露方法给外部接口进行调用
var duck = new Duck()
exports.say = duck.say

// 子类B
var util = require('util')
var Animal = require('./animal')
function Bird() {
  var _res = arguments[0]
  var _req = arguments[1]
  Animal.call(this)
  util.inherits(Bird, Animal)
  this.say = function() {
    console.log('ji...ji...ji...')
  }
}
module.exports = Bird

// 测试类
var duck = require('./duck')
var Bird = require('./bird')
duck.say()
var bird = new Bird()
bird.say()

单例模式

一般认为单例就是保证一个类只有一个实例,实现的方法一般是先判断实例存在与否,如果存在,就直接返回,如果不存在,则会创建该对象,并将该对象保存在静态变量中,当下次请求时,则可以直接返回该对象,这就确保了一个类只有一个实例对象。

下面请看一个例子,使用私有变量记录new的相应对象

// 单例类文件
// 舒适化一个私有变量
var _instance = null
module.exports = function(time) {
  // 创建Class类
  function Class(time) {
    this.name = 'no锋'
    this.book = 'Node.js'
    this.time = time
  }
  // 创建类方法
  Class.prototype = {
    constructor: Class,
    show: function() {
      console.log(`《${this.book}》这本书是${this.name}在${this.time}编写的`)
    }
  }
  // 获取单例对象接口
  this.getInstance = function() {
    if(_instance === null) {
      _instance = new Class(time)
    }
    return _instance
  }
}

注意,_instance变量是要放在单例方法之外,否则无法实现单例模式。原因是当调用单例方法时每次都会重新将其赋值为null,而放在单例函数之外时,调用单例函数不会影响到_instance变量的值。

接下来,我们再创建一个js文件来调用类对象

var Single = require('./student')
var singleObjOne = new Single('2018-09-02')
var singleClassOne = singleObjOne.getInstance('2018-09-02')
singleClassOne.show()

var singleObjTwo = new Single('2018-09-01')
var singleClassTwo = singleObjTwo.getInstance('2018-09-01')
singleClassTwo.show()

从输出结果可以看出来,第二次new单例对象的时候,没有创建新的Class类对象,而是返回了第一次创建的Class类对象。这样就应用Node.js实现了单例模式。

适配器模式

若将一个类的接口转换成客户希望的另外一个接口,Adapter(适配器)模式可以使原本由于接口不兼容而不能一起工作的那些类可以一起工作。下面我们直接看案例代码

// Target父类
module.exports = function() {
  this.request = function() {
    console.log('这是父类的request方法')
  }
}

// Adaptee类
module.exports = function() {
  this.specialRequest = function() {
    console.log('我才是被子类真正调用的方法')
  }
}

// Adapter子类
var util = require('util')
var Target = require('./target')
var Adaptee = require('./adaptee')
// 定义Adapter函数类
function Adapter() {
  Target.call(this)
  this.request = function() {
    var adapteeObj = new Adaptee()
    adapteeObj.specialRequest()
  }
}
// Adapter类继承Target类
util.inherits(Adapter, Target)
// 暴露Adapter类
module.exports = Adapter

// 一个测试脚本文件
var Adapter = require('./adapter')
var target = new Adapter()
target.request()

从运行结果可以看到,其通过适配器调用了Adaptee中的specialRequest方法,这样就实现了Node.js中的适配器模式。

装饰模式

装饰模式就是动态的给一个对象添加一些额外的职责,就扩展功能而言,它比生成子类方式更为灵活。下面我们就先创一个Component父类

module.exports = function() {
  this.operation = function() {
    console.log('这是父类的operation方法')
  }
}

接着在创建一个子类ConcreteComponent,其作用就是展示父类装饰之前类中的属性和方法,重定义operation方法

var util = require('util')
var Component = require('./component')
// 定义函数类
function ConcreteComponent() {
  Component.call(this)
  this.operation = function() {
    console.log('这是子类的operation方法')
  }
}
// 继承父类component
util.inherits(ConcreteComponent, Component)
// 暴露ConcreteComponent类
module.exports = ConcreteComponent

然后再创建一个Decorator基类,用于装饰Component类

var util = require('util')
var Component = require('./component')
function Decorator() {
  Component.call(this)
}
util.inherits(Decorator, Component)
module.exports = Decorator

创建ConcreteDecoratorA装饰类,该类的目的是为Component类的operation方法提供一些额外的操作,比如添加一些额外的计算规则和输出一些额外的数据

var util = require('util')
var Decorator = require('./decorator')
function ConcreteDecoratorA() {
  Decorator.call(this)
  this.operation = function() {
    // 调用被装饰类的operation基本方法
    Decorator.operation
    console.log('为父类的父类提供额外的一些操作')
  }
}
util.inherits(ConcreteDecoratorA, Decorator)
module.exports = ConcreteDecoratorA

创建ConcreteDecoratorB装饰类,该类的目的是为Component类的operation方法提供一些额外的操作,同时添加新的功能方法

var util = require('util')
var Decorator = require('./decorator')
function ConcreteDecoratorB() {
  Decorator.call(this)
  this.operation = function() {
    // 调用被装饰类的operation基本方法
    Decorator.operation
    console.log('继续为父类的父类提供额外的一些操作')
  }
  this.addedBehavior = function() {
    console.log('装饰类Component添加新的行为动作')
  }
}
util.inherits(ConcreteDecoratorB, Decorator)
module.exports = ConcreteDecoratorB

最后我创建一个测试类

var ConcreteDecoratorA = require('./concreteDecoratorA')
var ConcreteDecoratorB = require('./concreteDecoratorB')
var target = new ConcreteDecoratorA()
target.operation()

var targetone = new ConcreteDecoratorB()
targetone.operation()
targetone.addedBehavior()

装饰类的应用场景是在不改变基类的情况下,为基类新增属性和方法。

工厂模式

定义一个用于创建对象的接口,让子类决定将哪一个类实例化,工厂模式就是使一个类的实例化延迟到其子类。下面我们编写一个基类Product

module.exports = function() {
  this.getProduct = function() {
    console.log('这个是父类的getProduct方法')
  }
}

创建两个子类ProductA和ProductB,分别重写父类的getProduct方法

// 子类ProductA
var util = require('util')
var Product = require('./product')
function ProductA() {
  Product.call(this)
  this.getProduct = function() {
    console.log('这是子类A的getProduct方法')
  }
}
util.inherits(ProductA, Product)
module.exports = ProductA

// 子类ProductB
var util = require('util')
var Product = require('./product')
function ProductB() {
  Product.call(this)
  this.getProduct = function() {
    console.log('这是子类B的getProduct方法')
  }
}
util.inherits(ProductB, Product)
module.exports = ProductB

接着创建工厂对象productFactory,根据不同的参数获取不同的Product对象。这里需要注意的是,createProduct使用exports而不是使用module.exports,目的是传递一个ProductFactory对象,而非一个ProductFactory类

var ProductA = require('./productA')
var ProductB = require('./productB')
exports.createProduct = function(type) {
  switch(type) {
    case 'ProductA' : return new ProductA
    break
    case 'ProductB' : return new ProductB
    break
    default : return false
  }
}

最后创建一个测试文件

var ProductFactory = require('./productFactory')
var ProductA = ProductFactory.createProduct('ProductA')
ProductA.getProduct()

var ProductB = ProductFactory.createProduct('ProductB')
ProductB.getProduct()

从结果可以看出通过传递不同的字符串,获取了不同的对象ProductA和ProductB,在工厂模式中还包括工厂方法和抽象工厂两个模式。

第 3 章:Node.js的Web应用

简单的HTTP服务器

我们首先来看下面一个代码栗子,根据这个栗子进行分析

var http = require('http')
http.createServer(function(req, res) {
  res.writeHead(200, {
    'Content-Type':'text/plain'
  })
  res.end('hello world\n')
}).listen(1337, '127.0.0.1')
console.log('Server running at http://localhost:1337/')

http.createServer()接收一个request事件函数,该事件函数有两个参数req和res。req主要是获取请求资源信息,包括请求的url、客户端参数、资源文件、header信息、HTTP版本、设置客户端编码等;res主要是响应客户端请求数据,包括HTTP的header处理、HTTP请求返回码、响应请求数据等。

http.createServer()调用返回的是一个server对象,server拥有listen和close方法,listen方法可以指定监听的IP和端口。

接下来我们整一个实例:创建一个HTTP服务器,获取并输出请求url、method和headers,同时根据请求资源做不同的输出。

var http = require('http')
var fs = require('fs')
var url = require('url')
// 创建一个HTTP服务器
http.createServer(function(req, res) {
  // 获取客户端请求路径
  var pathname = url.parse(req.url).pathname
  // 输出请求地址
  console.log(req.url)
  // 输出请求方法
  console.log(req.method)
  // 输出请求的headers信息
  console.log(req.headers)
  switch(pathname) {
    case '/index':resIndex(res)
      break;
    case '/img':resImage(res)
      break;
    default:resDefault(res)
      break;
  }
}).listen(1337)
// 定义一个/index路径处理返回函数
function resIndex(res) {
  // 获取当前index.html的路径
  var readPath = __dirname + '/' + url.parse('index.html').pathname
  var indexPage = fs.readFileSync(readPath)
  res.writeHead(200, {
    'Content-Type': 'text/html'
  })
  res.end(indexPage)
}
// 定义一个/img路径处理返回函数
function resImage(res) {
  // 获取当前image的路径
  var readPath = __dirname + '/' + url.parse('logo.png').pathname
  var indexPage = fs.readFileSync(readPath)
  res.writeHead(200, {
    'Content-Type': 'image/png'
  })
  res.end(indexPage)
}
// 定义一个根路径处理返回函数
function resDefault(res) {
  res.writeHead(404, {
    'Content-Type': 'text/plain'
  })
  res.end('can not find source')
}
console.log('Server running at http://localhost:1337/')

然后node执行这个js文件(记住在当前项目中得存在一个html文件和logon.png文件)

路由处理

一般情况下,我们可以根据上一节内容那样来使用switch来实现路由,但是当请求资源非常复杂时,使用这种方式来判断处理就会显得很庞大,而且难以维护,下面介绍其他两种路由处理办法

根据上述实现必须在本地路径创建image.js和index.js文件,其中两个模块中都含有init方法来初始化模块中的res和req变量。image.js中img方法处理图片返回,index.js中index方法处理index.html页面展示

源代码有误,不继续了

Node中的GET

Node.js中HTTP客户端发送的GET请求参数数据都存储在request对象中的url属性中,例如:http://locallhost:1337/test?name=jack。其中ur的请求路径名为test,使用GET传输的name数据暴露在url上

下面我们使用http模块创建一个服务器,该服务器接收任意的url请求资源,使用GET方法传递参数,服务器接收客户端请求url,输出每次请求的路径名和请求参数的json对象

var http = require('http')
// 路径解析模块
var url = require('url')
// 字符串解析为json对象模块
var querystring = require('querystring')
http.createServer(function(req, res) {
  // 获取HTTP的GET参数
  var pathname = url.parse(req.url).pathname
  // 获取url中的非路径字符串
  var paramStr = url.parse(req.url).query
  // 对字符串进行解析
  var param = querystring.parse(paramStr)
  // 阻止发送图标请求
  if('/favicon.ico' == pathname) {
    return
  }
  console.log(pathname)
  console.log(paramStr ? paramStr : 'no params')
  console.log(param)
  res.writeHead(200, {
    'Content-Type': 'text/plain'
  })
  res.end('success')
}).listen(1337)
console.log('Server running at http://127.0.0.1:1337/')

在命令行运行该文件,分别输入:http://127.0.0.1:1337/http://127.0.0.1:1337/indexhttp://127.0.0.1:1337/index?id=4535 ,观察下输出的结果是什么。

Node中POST

相比较GET请求,POST请求一般比较复杂,Node为了使整个过程非阻塞,会将POST数据拆分成很多小的数据块,然后通过触发特定的事件,将这些小数据块有序传递给回调函数。这部分涉及到request对象中的addListener方法,该方法有两个事件参数data和end,data表示数据传输开始,end表示数据传输结束。下面我们来看一个实例

var http = require('http')
// 读取index.html页面信息
var fs = require('fs')
var url = require('url')
var querystring = require('querystring')
http.createServer(function(req, res) {
  var pathname = url.parse(req.url).pathname
  // 路由处理
  switch(pathname){
    case '/add' : resAdd(res, req)
    break;
    default: resDefault(res)
    break;
  }
}).listen(1337)
function resDefault(res){
  //获取当前index.html的路径
  var readPath = __dirname + '/' +url.parse('index.html').pathname
  var indexPage = fs.readFileSync(readPath)
  res.writeHead(200, { 
    'Content-Type': 'text/html' 
  })
  res.end(indexPage)
}
function resAdd(res, req){
  var postData = '';
  // 设置接收数据编码格式为 UTF-8
  req.setEncoding('utf8')
  // 接收数据块并将其赋值给 postData
  req.addListener('data', function(postDataChunk) {
    postData += postDataChunk;
  })
  // 数据接收完毕后,执行POST参数解析
  req.addListener('end', function() {
  // 数据接收完毕,执行回调函数
  var param = querystring.parse(postData)
  console.log(postData)
  console.log(param)
  res.writeHead(200, { 
    'Content-Type': 'text/plain' 
  })
  res.end('success')
  })
}

运行该js文件,然后打开地址:http://127.0.0.1:1337/ , 接着你会看到一个表单,往表单里面输入数据看看返回了什么,下面是index.html文件代码

<html>
  <head>
    <title>Test Post</title>
  </head>
  <body>
    <div>
      <form action='add' method='POST'>
        <label>name: <input type='text' name='name'></label>
        <label>book: <input type='text' name='book'></label>
        <input type='submit' />
      </form>
    </div>
  </body>
</html>

Node中的POST和GET

上面介绍了HTTP中POST和GET参数的获取方式,现在我们将两个方法作为一个公用的模块,可以在很大程度上减少工作量,毕竟获取GET和POST参数的方法都不是一步完成的,都必须进行多步操作,接下来我们来实践开发一个HTTP参数获取模块,主要是利用其中的两个方法GET和POST来直接得到客户端传递的数据

在该模块起始部分,首先要获取该模块的一些必要的Node.js原生模块,分别是url和querystring,其次需要在代码中应用init方法来初始化该模块对象中的res和req对象,代码如下

// http_param.js文件
var _res, _req
var = url = require('url')
// 解析GET参数,并返回
var = querystring = require('querystring')
// 初始化http中的res和req参数
exports.init = function(req, res) {
  _res = res
  _req = req
}
// 获取GET参数方法
exports.GET = function(key) {
  var paramStr = url.parse(_req.url).query
  // 获取传递参数的json数据方法
  var param = querystring.parse(paramStr)
  return param[key] ? param[key] : ''
}
// 获取POST参数方法
exports.POST = function(key, callback) {
  var postData = ''
  _req.addListener('data', function(postDataChunk) {
    postData += postDataChunk
  })
  _req.addListener('end', function() {
    // 数据接收完毕,执行回调函数
    var param = querystring.parse(postData)
    var value = param[key] ? param[key] : ''
    callback(value)
  })
}

在代码最后使用的是一个callback回调函数作为参数,来解决异步调用中返回结果的传递。这样就简单地实现了一个HTTP中如何获取GET和POST传递的数据模块。

下面这个是调用模块代码

// client.js文件
var http = require('http')
var fs = require('fs')
var url = require('url')
var querystring = require('querystring')
var httpParam = require('./http_param')
http.createServer(function(req, res) {
  var pathname = url.parse(req.url).pathname
  httpParam.init(req, res)
  switch(pathname) {
    case '/add':resAdd(res, req)
      break;
    default:resDefault(res)
      break;
  }
}).listen(1337)

function resDefault(res) {
  // 获取当前index.html的路径 
  var readPath = __dirname + '/' + url.parse('index.html').pathname
  var indexPage = fs.readFileSync(readPath)
  res.writeHead(200, {
    'Content-Type': 'text/html'
  })
  res.end(indexPage)
}

function resAdd(res, req) {
  // 设置接收数据编码格式为 UTF-8
  req.setEncoding('utf8')
  httpParam.POST('name', function(value) {
    res.writeHead(200, {
      'Content-Type': 'text/plain'
    })
    res.end(value)
  })
}

运行该js文件,然后打开地址:http://127.0.0.1:1337/ , 接着你会看到一个表单,往表单里面输入数据看看返回了什么,下面是index.html文件代码

<html>
  <head>
    <title>Test Post</title>
  </head>
  <body>
    <div>
      <form action='add' method='POST'>
        <label>name: <input type='text' name='name'></label>
        <label>book: <input type='text' name='book'></label>
        <input type='submit' />
      </form>
    </div>
  </body>
</html>

HTTP和HTTPS模块介绍

HTTP是一种详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议。

而HTTPS是以安全为目标的HTTP通道,简单的说他就是HTTP的安全版。

两者创建服务器的方式接口都是一致的,都是使用各自模块中的createServer方法,但是HTTPS中的createServer会附加一个参数opts,其中保存有key和cert的信息。

为什么需要静态资源管理

因为在对于前端种种类型的静态资源,服务器端无法为每个静态资源类型实现一个处理逻辑,因此在Web应用开发中我们需要自己设计一个静态资源的管理。请看以下代码

var http = require('http')
var fs = require('fs')
var url = require('url')
http.createServer(function(req, res) {
  // 获取当前index.html的路径
  var readPath = __dirname + '/' + url.parse('index.html').pathname
  var indexPage = fs.readFileSync(readPath)
  res.writeHead(200, {
    'Content-Type': 'text/html'
  })
  res.end(indexPage)
}).listen(1339)
console.log('Server running at http://localhost:1339/')

当你的index.html文件中新增了css外部静态资源时,那么该css将不会被处理

<html>
  <head>
    <title>Test Post</title>
    <link href="reset.css" rel="stylesheet" />
  </head>
  <body>
    <div>
      hello world
    </div>
  </body>
</html>

Node实现简单静态资源管理

接下来我们学习如何利用Node.js实现简单的静态资源管理。创建http.js,首先分析需要的Node.js模块:HTTP模块创建服务器、fs模块处理文件的读写、url模块处理url请求路径

var http = require('http')
var fs = require('fs')
var url = require('url')
// 获取当前脚本路径,BASE_DIR是定义常量数据的规范
var BASE_DIR = __dirname
http.createServer(function(req, res) {
  // 获取当前index.html的路径
  var pathname = url.parse(req.url).pathname
  // 获取静态资源文件存储路径
  var realPath = BASE_DIR + '/static' + pathname
  // 过滤favicon.ico请求
  if(pathname == '/favicon.ico') {
    return
  } else if(pathname == '/index' || pathname == '/') {
    goIndex(res)
  } else {
    dealWithStatic(pathname, realPath, res)
  }
}).listen(1330)
console.log('Server running at http://localhost:1330/')

function goIndex(res) {
  var readPath = BASE_DIR + '/' + url.parse('index.html').pathname
  var indexPage = fs.readFileSync(readPath)
  res.writeHead(200, {
    'Content-Type': 'text/html'
  })
  res.end(indexPage)
}
// 处理静态资源文件逻辑,主要是根据不同静态资源路径,返回相应的静态资源文件信息
// 3个参数分别代码请求资源路径名、静态资源实际存储路径和相应HTTP相应对象res
// 这个函数的作用就是根据不同的请求资源后缀名,返回相应的MMIE类型和数据到客户端
function dealWithStatic(pathname, realPath, res) {
  fs.exists(realPath, function(exists) {
    if(!exists) {
      res.writeHead(404, {
        'Content-Type': 'text/plain'
      })
      res.write("该请求路径:" + pathname + "没有被服务器找到!")
      res.end()
    } else {
      var pointPostion = pathname.lastIndexOf('.')
      // 获取文件后缀名
      var mmieString = pathname.substring(pointPostion + 1)
      var mmieType
      // 根据文件后缀名,设置HTTP响应的content-type类型
      switch(mmieString) {
        case 'css':mmieType = "text/css"
          break;
        case 'png':mmieType = "image/png"
          break;
        default:mmieType = "text/plain"
      }
      fs.readFile(realPath, "binary", function(err, file) {
        if(err) {
          res.writeHead(500, {
            'Content-Type': 'text/plain'
          })
          res.end(err)
        } else {
          res.writeHead(200, {
            'Content-Type': mmieType
          })
          res.write(file, "binary")
          res.end()
        }
      })
    }
  })
}

在该项目中有一个index.html文件,以及以static文件夹以及文件夹下面的logo.png和style.css这两个静态资源文件

<html>
  <head>
    <title>Test Http</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <div>Oh, good~!</div>
    <div><img src='logo.png' /></div>
  </body>
</html>

这里只针对两种类型的静态资源文件,对于其他的资源暂时不考虑,代码编写完后,运行http.js,打开浏览器输入http://localhost:1330/ ,那么你将看到静态资源被渲染出来了。

静态资源库设计

HTTP请求中大概有409中MMIE类型,在设计时将这409种MMIE类型作为一个json配置信息存储在mmie_type.json文件中,以便维护和管理,由于代码量多,具体的请看[这里]()。记得在写demo的时候需要添加上去

下面我们创建一个静态资源调用以及解析文件static_module.js

// 本地文件夹绝对路径
var BASE_DIR = __dirname
// 配置文件绝对路径
var CONF = BASE_DIR + '/conf/'
// 静态文件存储路径
// 在这段代码中,CONF末尾加上了一个斜杆,而STATIC却没有,原因
// 在于静态资源请求的pathname会自带一个斜杆
var STATIC = BASE_DIR + '/static'
// 设置缓存的时间
var CACHE_TIME = 60 * 60 * 24 * 365
// MMIE类型的json存储配置内容
var mmieConf
// 工具类模块,主要是对一些异常处理打印debug信息
var sys = require('util')
var http = require('http')
// 解析json配置文件
var fs = require('fs')
var url = require('url')
var path = require('path')
// 获取MMIE的配置文件内容
mmieConf = getMmieConf()
// 响应静态资源请求
exports.getStaticFile = function(pathname, res, req) {
  var extname = path.extname(pathname)
  // 获取请求pathname的后缀名字符串
  extname = extname ? extname.slice(1) : ''
  // 设置静态文件路径
  var realPath = STATIC + pathname
  // 设置请求静态文件的MMIE类型,当配置中不存在该类型时,则默认为'text/plain'类型
  var mmieType = mmieConf[extname] ? mmieConf[extname] : 'text/plain'
  // 实现响应不同种MMIE类型的逻辑处理
  fs.exists(realPath, function(exists) {
    if(!exists) {
      res.writeHead(404, {
        'Content-Type': 'text/plain'
      })
      res.write("This request URL " + pathname + " was not found on this server.")
      res.end()
    } else {
      // 同步执行获取静态文件realPath信息
      var fileInfo = fs.statSync(realPath)
      // 获取文件最后更改时间,并转化UTC字符串
      var lastModified = fileInfo.mtime.toUTCString()
      // 判断请求的资源文件是否需要缓存
      if(mmieConf[extname]) {
        var date = new Date()
        date.setTime(date.getTime() + CACHE_TIME * 1000)
        // 设置响应header的expires值
        res.setHeader("Expires", date.toUTCString())
        // 设置响应header的Cache-Control值,缓存时间
        res.setHeader("Cache-Control", "max-age=" + CACHE_TIME)
      }
      console.log(req.headers['if-modified-since'])
      // 检测浏览器是否发送了If-Modified-Since请求头,并判断服务器文件有没修改
      // 如果客户端发送的最后修改时间与服务器文件的修改时间相同的话,
      // HTTP响应304状态码,不需要再次IO读取磁盘中的静态资源文件数据
      if(req.headers['if-modified-since'] && lastModified == req.headers['if-modified-since']) {
        res.writeHead(304, "Not Modified")
        res.end()
      } else {
        // 当客户端请求服务器端不同的静态文件资源时,
        // 服务端会根据客户端请求的mmieType来响应不同的Content-Type类型
        fs.readFile(realPath, "binary", function(err, file) {
          if(err) {
            res.writeHead(500, {
              'Content-Type': 'text/plain'
            })
            res.end(err)
          } else {
            // 静态文件有所更改时,重新设置浏览器Last-Modified的值
            res.setHeader("Last-Modified", lastModified)
            res.writeHead(200, {
              'Content-Type': mmieType
            })
            res.write(file, "binary")
            res.end()
          }
        })
      }
    }
  })
}
//获取MMIE配置信息,读取配置文件信息,并转化为json对象
function getMmieConf() {
  var routerMsg = {}
  try {
    // utf8编码读取json配置信息
    var str = fs.readFileSync(CONF + 'mmie_type.json', 'utf8')
    routerMsg = JSON.parse(str)
  } catch(e) {
    // 解析错误时,显示debug日志
    sys.debug("JSON parse fails")
  }
  return routerMsg
}

我们为了减轻硬盘IO的承受压力,因此对静态资源文件进行了缓存配置。其中的原理是:浏览器缓存中存有文件副本的时候,不能确定该文件是否有效时,会生成一个get请求,在该请求的header中包含If-Modified-Since参数。如果服务器端文件在这个时间后发生过更改,就发送整个文件给客户端,如果没有修改,则返回304状态码,并不发送整个文件给客户端。

如果确定该副本有效时,客户端不会发送GET请求。判断有效的主要方法是,服务端响应的时候带上expires的头,浏览器会判断expires头,直到指定的日期过期,才会发起新的请求

接着我们创建一个client.js文件,并且在node环境中运行

var http = require('http')
var fs = require('fs')
var url = require('url')
// 获取静态资源管理模块
var staticModule = require('./static_module')
var BASE_DIR = __dirname
http.createServer(function(req, res) {
  // 获取当前index.html的路径
  var pathname = url.parse(req.url).pathname
  if(pathname == '/favicon.ico') {
    return
  } else if(pathname == '/index' || pathname == '/') {
    goIndex(res)
  } else {
    // 如果是静态资源文件的请求,调用getStaticFile来统一项目中的静态资源文件
    staticModule.getStaticFile(pathname, res, req)
  }
}).listen(1337)
console.log('Server running at http://localhost:1337/')

function goIndex(res) {
  var readPath = BASE_DIR + '/' + url.parse('index.html').pathname
  var indexPage = fs.readFileSync(readPath)
  res.writeHead(200, {
    'Content-Type': 'text/html'
  })
  res.end(indexPage)
}

运行之后打开浏览器,查看当前请求资源的network信息,仔细观察下。其中index.html文件的内容是

<html>
  <head>
    <title>Test Http</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <div>Oh, good~!</div>
    <div><img src='logo.png' /></div>
  </body>
</html>

文件处理

文件I/O是由标准的POSIX函数封装而成,需要使用require('fs')访问这个模块,所有的方法都提高了异步和同步两种方式

重命名文件

项目中txt格式的文件记得自己先创建一份,下面的几个文件处理也是同样的道理

// 设置当前执行路径
var BASE_DIR = __dirname;
var fs = require('fs');
// 重命名文件
fs.rename(BASE_DIR + '/danhuang.txt', BASE_DIR + '/dan.txt', function(err) {
  if(err) {
    console.log('第一个抛出的错误:' + err)
  }
  console.log('重命名文件完成');
});
fs.stat(BASE_DIR + '/dan.txt', function(err, stats) {
  if(err) {
    console.log('第二个抛出的错误:' + err)
  }
  console.log('stats: ' + JSON.stringify(stats));
});

修改文件权限和文件权限属组

var BASE_DIR = __dirname;
var fs = require('fs');
// 将文件的权限修改为777
fs.chmod(BASE_DIR + '/danhuang.txt', '777', function(err) {
  if(err) throw err;
  console.log('修改权限完毕');
});

获取文件元信息

var BASE_DIR = __dirname;
var fs = require('fs');
fs.stat(BASE_DIR + '/danhuang.txt', function(err, stats) {
  if(err) throw err;
  console.log(stats);
});

验证文件是否存在

var BASE_DIR = __dirname;
var fs = require('fs');
// 验证danhuang.txt文件是否存在,函数异步执行完后
// 将验证结果返回到回调函数的参数中,如existBool
fs.exists(BASE_DIR + '/danhuang.txt', function(existBool) {
  if(existBool) {
    console.log('danhuang.txt 存在');
  } else {
    console.log('danhuang.txt 不存在');
  }
});
fs.exists(BASE_DIR + '/dan.txt', function(existBool) {
  if(existBool) {
    console.log('dan.txt 存在');
  } else {
    console.log('dan.txt 不存在');
  }
});

删除文件

var BASE_DIR = __dirname;
var fs = require('fs');
fs.unlink(BASE_DIR + '/danhuang.txt', function(err) {
  if(err) throw err;
});

图片和文件上传

本节涉及到Node.js中的API主要是HTTP、FileSystem、querystring和url等模块。HTTP模块创建服务器和HTTP的处理请求,FileSystem负责图片文件的处理,querystring处理字符串,url模块处理url解析。其中还涉及到了GET和POST请求处理模块和静态文件管理模块。

文件上传和图片上传前端页面原则上都是一致的,主要是通过POST文件数据,而Node.js服务器端则需要利用NPM中一个应用模块formidable来处理图片的上传。

我们先来看一下文件上传文件index.html

<html>
  <head>
    <meta charset="utf-8">
    <title>上传图片文件</title>
    <link rel="stylesheet" href="static/style.css">
  </head>
  <body>
    <div id='main_content'>
      <div>
        <form enctype="multipart/form-data" action='upload' method='POST'></form>
        <input type="file" name="image" />
        <input type='submit' id='upload' value='上传图片'>
        </form>
      </div>
    </div>
  </body>
</html>

接着来看一下显示文件show_image.html

<html>
  <head>
    <meta charset="utf-8">
    <title>显示已经上传的图片</title>
    <link rel="stylesheet" href="static/style.css">
    <script src="static/jquery-1.8.3.min.js"></script>
  </head>
  <body>
    <div id='main_content'>
      <div>
        <img src='/uploadFile/test.png' alt='upload file' />
      </div>
  </body>
</html>

我们需要一个上传资源处理文件index.js

var http = require('http')
var fs = require('fs')
var url = require('url')
var querystring = require('querystring')
var httpParam = require('./http_param')
// 导入静态文件资源处理库
var staticModule = require('./static_module')
// 安装成功formidable模块后,require该模块
var formidable = require("formidable")
var BASE_DIR = __dirname;
http.createServer(function(req, res) {
  var pathname = url.parse(req.url).pathname;
  // 初始化GET和POST参数获取模块httpParam的req和res
  httpParam.init(req, res);
  // 避免icon请求
  if(pathname == '/favicon.ico') {
    return;
  }
  // 根据pathname来做路由分发处理
  switch(pathname) {
    case '/upload':
      upload(res, req);
      break;
    case '/image':
      showImage(res, req);
      break;
    case '/':
      defaultIndex(res);
      break;
    case '/index':
      defaultIndex(res);
      break;
    case '/show':
      show(res);
      break;
      // 使用静态资源模块来处理
    default:
      staticModule.getStaticFile(pathname, res, req);
      break;
  }
}).listen(1337);

function defaultIndex(res) {
  // 获取当前index.html的路径 
  var readPath = __dirname + '/' + url.parse('index.html').pathname;
  var indexPage = fs.readFileSync(readPath);
  res.writeHead(200, {
    'Content-Type': 'text/html'
  });
  res.end(indexPage);
}

function upload(res, req) {
  var readPath = __dirname + '/' + url.parse('show_image.html').pathname;
  // 读取show_image.html数据
  var indexPage = fs.readFileSync(readPath);
  // 创建formidable表单对象
  var form = new formidable.IncomingForm();
  // 获取上传文件数据,执行form表单数据解析,获取其中的post参数
  form.parse(req, function(error, fields, files) {
    // 同步获取上传文件,并保存在/uploadFile下,重命名为test.png
    fs.renameSync(files.image.path, BASE_DIR + '/uploadFile/test.png');
    res.writeHead(200, {
      'Content-Type': 'text/html'
    });
    res.end(indexPage);
  });
}

function show(res) {
  var readPath = __dirname + '/' + url.parse('show_image.html').pathname;
  var indexPage = fs.readFileSync(readPath);
  res.writeHead(200, {
    'Content-Type': 'text/html'
  });
  res.end(indexPage);
}

function showImage(res, req) {
  var retData = {
    'retCode': 0,
    'imageUrl': '/uploadFile/test.png'
  };
  res.writeHead(200, {
    'Content-Type': 'text/plain'
  });
  res.end(JSON.stringify(retData));
}

当然还有静态资源管理库文件static_module.js

// 本地文件夹绝对路径
var BASE_DIR = __dirname
// 配置文件绝对路径
var CONF = BASE_DIR + '/conf/'
// 静态文件存储路径
// 在这段代码中,CONF末尾加上了一个斜杆,而STATIC却没有,原因
// 在于静态资源请求的pathname会自带一个斜杆
var STATIC = BASE_DIR + '/static'
// 设置缓存的时间
var CACHE_TIME = 60 * 60 * 24 * 365
// MMIE类型的json存储配置内容
var mmieConf
// 工具类模块,主要是对一些异常处理打印debug信息
var sys = require('util')
var http = require('http')
// 解析json配置文件
var fs = require('fs')
var url = require('url')
var path = require('path')
// 获取MMIE的配置文件内容
mmieConf = getMmieConf()
// 响应静态资源请求
exports.getStaticFile = function(pathname, res, req) {
  var extname = path.extname(pathname)
  // 获取请求pathname的后缀名字符串
  extname = extname ? extname.slice(1) : ''
  // 设置静态文件路径
  var realPath = STATIC + pathname
  // 设置请求静态文件的MMIE类型,当配置中不存在该类型时,则默认为'text/plain'类型
  var mmieType = mmieConf[extname] ? mmieConf[extname] : 'text/plain'
  // 实现响应不同种MMIE类型的逻辑处理
  fs.exists(realPath, function(exists) {
    if(!exists) {
      res.writeHead(404, {
        'Content-Type': 'text/plain'
      })
      res.write("This request URL " + pathname + " was not found on this server.")
      res.end()
    } else {
      // 同步执行获取静态文件realPath信息
      var fileInfo = fs.statSync(realPath)
      // 获取文件最后更改时间,并转化UTC字符串
      var lastModified = fileInfo.mtime.toUTCString()
      // 判断请求的资源文件是否需要缓存
      if(mmieConf[extname]) {
        var date = new Date()
        date.setTime(date.getTime() + CACHE_TIME * 1000)
        // 设置响应header的expires值
        res.setHeader("Expires", date.toUTCString())
        // 设置响应header的Cache-Control值,缓存时间
        res.setHeader("Cache-Control", "max-age=" + CACHE_TIME)
      }
      console.log(req.headers['if-modified-since'])
      // 检测浏览器是否发送了If-Modified-Since请求头,并判断服务器文件有没修改
      // 如果客户端发送的最后修改时间与服务器文件的修改时间相同的话,
      // HTTP响应304状态码,不需要再次IO读取磁盘中的静态资源文件数据
      if(req.headers['if-modified-since'] && lastModified == req.headers['if-modified-since']) {
        res.writeHead(304, "Not Modified")
        res.end()
      } else {
        // 当客户端请求服务器端不同的静态文件资源时,
        // 服务端会根据客户端请求的mmieType来响应不同的Content-Type类型
        fs.readFile(realPath, "binary", function(err, file) {
          if(err) {
            res.writeHead(500, {
              'Content-Type': 'text/plain'
            })
            res.end(err)
          } else {
            // 静态文件有所更改时,重新设置浏览器Last-Modified的值
            res.setHeader("Last-Modified", lastModified)
            res.writeHead(200, {
              'Content-Type': mmieType
            })
            res.write(file, "binary")
            res.end()
          }
        })
      }
    }
  })
}
//获取MMIE配置信息,读取配置文件信息,并转化为json对象
function getMmieConf() {
  var routerMsg = {}
  try {
    // utf8编码读取json配置信息
    var str = fs.readFileSync(CONF + 'mmie_type.json', 'utf8')
    routerMsg = JSON.parse(str)
  } catch(e) {
    // 解析错误时,显示debug日志
    sys.debug("JSON parse fails")
  }
  return routerMsg
}

以及POST和GET资源请求处理文件http_param.js

// http_param.js文件
var _res, _req
var = url = require('url')
// 解析GET参数,并返回
var = querystring = require('querystring')
// 初始化http中的res和req参数
exports.init = function(req, res) {
  _res = res
  _req = req
}
// 获取GET参数方法
exports.GET = function(key) {
  var paramStr = url.parse(_req.url).query
  // 获取传递参数的json数据方法
  var param = querystring.parse(paramStr)
  return param[key] ? param[key] : ''
}
// 获取POST参数方法
exports.POST = function(key, callback) {
  var postData = ''
  _req.addListener('data', function(postDataChunk) {
    postData += postDataChunk
  })
  _req.addListener('end', function() {
    // 数据接收完毕,执行回调函数
    var param = querystring.parse(postData)
    var value = param[key] ? param[key] : ''
    callback(value)
  })
}

最后运行node index.js文件,查看效果。记得在那之前先执行npm init ==> npm install formidable --save

jade模板实现图片上传展示功能

由于Node版本更新迭代问题,这个章节的代码跑不起来,因此不做啥笔记了,先放着。

文件读写

由于Node版本更新迭代问题,这个章节的代码跑不起来,因此不做啥笔记了,又只能先放着。

Cookie和Session

Session和Cookie都是基于Web服务器的,不同的是Cookie存储在客户端,而Session存储在服务器端。

当用户在浏览网站的时候,Web服务器会在浏览器上存储一些当前用户的相关信息,而在本地Web客户端存储的就是Cookie数据。当下次用户再浏览同一个网站时,Web服务器就会先查看并读取本地的Cookie资料,如果有Cookie就会依据Cookie里的内容以及判断其过期时间来给用户特殊的数据返回。

Crypto模块加密

加密模块需要底层系统提供OpenSSL的支持,它提供了一种安全凭证的封装方式,可以用于HTTPS安全网络以及普通HTTP连接。该模块还提供了一套针对OpenSS的hash(哈希)、hmac(密钥哈希)、cipher(编码)、decipher(解码)、sign(签名)以及verify(验证)等方法的封装。

哈希

// 获取原生crypto模块
var crypto = require('crypto');
// 使用md5进行加密
var hash = crypto.createHash("md5");
// 使用二进制数据流将字符串进行加密
hash.update(new Buffer("huangdanhua", "binary"));
// 返回hash对象加密后的字符串
var encode = hash.digest('hex');
console.log("二进制加密后的数据: " + encode);

var hash = crypto.createHash("md5");
// 字符加密代码
hash.update("huangdanhua");
var encode = hash.digest('hex');
console.log("字符加密后的数据:" + encode);

// 使用sha1进行加密
var hash = crypto.createHash("sha1");
hash.update("huangdanhua");
var encode = hash.digest('hex');
console.log("使用sha1加密后的数据:" + encode);

hash.digest()这个函数可以传入3个类型的参数hex(十六进制)、binary(二进制)或者base64输出加密后的字符,默认参数时binary。如果传递的参数非指定的这3个字符时,函数会自动使用binary返回加密字符串。

HMAC

HMAC是密钥相关的哈希运算消息认证码,HMAC运算利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。

var crypto = require('crypto');
/*-------------binary md5------------------*/
var hmac = crypto.createHmac("md5", 'dan');
hmac.update(new Buffer("huangdanhua", "binary"));
var encode = hmac.digest('hex');
console.log("binary data: " + encode);
/*-------------string md5------------------*/
var hmac = crypto.createHmac("md5", 'dan');
hmac.update("huangdanhua");
var encode = hmac.digest('hex');
console.log("string:" + encode);
/*-------------string sha1------------------*/
var hmac = crypto.createHmac("sha1", 'dan');
hmac.update("huangdanhua");
var encode = hmac.digest('hex');
console.log("string sha1:" + encode);

Cipher和Decipher

如果希望对一个字符进行加密时,必须保证cipher对象和decipher对象加密的私钥和加密算法是相同的,才能正确的解密,解密和加密调用的所有函数都是类似的。

var crypto = require('crypto')
var key = 'salt_from'
var plaintext = 'fengxiong'
// 创建一个cipher加密对象,第一个参数是算法类型,第二参数是需要加密的秘钥
var cipher = crypto.createCipher('aes-256-cbc', key)
// 创建一个decipher解密对象,第一个参数是算法类型,第二参数是需要加密的秘钥
var decipher = crypto.createDecipher('aes-256-cbc', key);
// 使用参数data更新要加密的内容
cipher.update(plaintext, 'utf8', 'hex');
// 返回所有剩余的加密内容
var encryptedPassword = cipher.final('hex')
// 使用参数data更新要解密的内容
decipher.update(encryptedPassword, 'hex', 'utf8');
// 返回所有剩余的解密内容
var decryptedPassword = decipher.final('utf8');
console.log('encrypted :', encryptedPassword);
console.log('decrypted :', decryptedPassword);