CruxF / IMOOC

IMOCC辛勤的搬运工:fire:

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Node开发实战知识整理

CruxF opened this issue · comments

工具

介绍一款很方便的在线编辑器——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中API提供的原生模块,原生模块在启动时已经被加载了。
  • 文件模块:是一种动态加载模块,加载文件模块的工作主要由原生模块module来实现和完成。总而言之,原生模块在启动时已经被加载,而文件模块则需要通过调用Node中的require方法来实现加载。

需要了解一点的是,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替代,反过来则不行。它们之间还有以下的不同点

  • module.exports方法还可以单独返回一个数据类型,而exports只能返回一个object对象,因此,当我们需要返回一个数组、字符串、数字等类型的时候,就必须使用module.exports
  • 当在exports前面使用了moudle.exports,那么exports的任何方法和属性都会失效,请看下面案例代码
index.js文件模块
module.exports = 'exports的属性和方法将被忽视!'
exports.name = '我无法被调用'
exports.showName = function () {
  console.log('我也无法被调用')
}
console.log('内部module.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方法

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的步骤

  • 安装: npm install -g express
  • 安装依赖: npm install -g express-generator
  • 创建一个app应用: express 项目名称
  • 进入到项目中: cd 项目名称
  • 安装jar包: npm install
  • 运行项目: npm start 或者DEBUG=nodedemo:* npm start,总之按照提示走就对了
  • 访问地址:http://127.0.0.1:3000/

查看当前版本: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模块来实现以上的要求。

使用步骤

  • 安装:npm install forever -g
  • 查看模块内置命令: forerver --help
  • 运行express框架自带的app.js文件:forever start -l forever.log -o out.log -e err.log app.js
-l指定forever运行日志,-o指定脚本流水日志,
-e指定脚本运行错误日志。启动后将会在本项目中产生out.log、err.log文件
  • 查看node进行返回结果:netstat

更多关于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模块

  • 步骤一:创建一个项目
  • 步骤二:初始化一个package.json文件(npm init)
  • 步骤三:创建一个index.js文件
function testNpm(name) {
  console.log(name)
}
exports.testNpm = testNpm
  • 步骤四:创建一个test.js文件
var fn = require('./index.js')
fn.testNpm('hello,这是测试模块')
  • 步骤五:执行test.js文件,看看是否有错误(node test.js)
  • 步骤六:来到官网注册自己的开发账号
  • 步骤七:使用命令行连接NPM,输入自己的注册信息
$ npm adduser
Username: fengxiong
Password: 9*****8*q
Email: (this IS public) 9*****0@qq.com
Logged in as fengxiong on https://registry.npmjs.org/.

$ npm whoami
fengxiong
  • 注意:如果出现403错误,那么很有可能是邮箱尚未验证或者仓库地址使用了淘宝镜像,点击这里查看解决方法
  • 步骤八:发布模块(npm publish)
  • 步骤九:下载使用该模块。
1、创建一个新项目
2、初始化一个package.json文件
3、安装自己的模块:npm install --save "原先模块package.json文件中的name值"

最后是下载该模块的项目结构
image

我们可以在根目录的test.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,在工厂模式中还包括工厂方法和抽象工厂两个模式。