24

后端知识点总结——NODE.JS(高级)

1.Node入门:

什么是: 针对网络应用开发的平台
主要特征:

  1. 基于Google的JavaScript运行时引擎V8
  2. 扩展了Node标准类库: TCP,同步或异步文件管理,HTTP

为什么使用Node:

  1. 可以在服务器端运行js: 现有前端团队可直接参与后端js开发
  2. js天生支持非阻塞IO:
    IO: 代表一切数据进出程序的操作:
    包括: 文件读写, 数据库操作, 网络操作
    问题: 有延迟
    传统阻塞IO: IO操作会阻塞当前主线程,直到本次IO操作完成,才能执行后续代码。
    非阻塞IO: 即使处理较慢的IO操作时,主进城仍然能处理其他请求
    Js天生支持非阻塞: 回调函数=事件循环+回调队列
    所有非阻塞的操作,返回的结果暂时在回调队列中等待
    由事件循环,自动依次取回到主程序中恢复执行
    回调队列在主程序之外存储回调函数,所以,不会干扰主程序执行
    非阻塞在Web服务器中:
    普通服务器端应用: 虽然可实现每个请求独立线程/进程, 但如果一个请求中,包含多个阻塞IO操作(访问数据库,网络,读写硬盘文件),该请求返回的时间就等于所有IO操作的时间总和——慢
    Node服务器端应用: 不但每个请求是一个独立的线程,且,每个请求内的每个IO操作,都是非阻塞的。

    一个包含多个IO操作的请求,返回的总响应时间,仅仅等于其中一个时间最长的IO操作的时间。
    Node.js vs javascript: 
    Javascript: 编程语言, 依照ECMAScript

    2种运行环境:

    1. 客户端浏览器: 由各种客户端浏览器中的js解释器执行
      扩展: DOM API 和 BOM API 主要目的是为了操作网页内容和浏览器窗口
    2. 独立的js解释器:Node.js 应用程序开发和运行的平台
      仅支持ECMAScript
      扩展: 各种专门的服务器模块: TCP, HTTP, 文件读写, MYSQL

      构建一个简单的node应用:
      创建一个新的node项目: 基本命令:
      mkdir 项目文件夹
      cd 项目文件夹
      npm init //负责在当前所在的项目目录下自动生成package.json配置文件
      运行:node 入口文件.js

2.module

Node应用都是由模块组成
模块就是组织程序功能的一种文件或文件夹
Node应用采用CommonJS模块规范
CommonJS规定:

  1. 每个文件就是一个模块,有自己的作用域——避免全局污染
    一个文件内定义的变量,函数,类都是该文件私有,对其它文件默认不可见
  2. 对象,方法和变量也可以从一个文件/模块中导出(exports),用在其它文件/模块中。

实际项目中,都是将各种功能/数据,划分为不同项目模块来管理
如何定义一个模块:2步:

  1. 在模块/文件中定义业务代码(对象,class,函数)
  2. 将内部的功能抛出,用于将来其它js文件调用

2种情况:

2.1面向对象的方式:

  1. 定义一种class或一个对象,包裹属性和功能
  2. 将class或对象直接赋值给module.exports
    其中: module,指当前模块对象/当前文件

       exports是当前module对象的一个属性
         本质上也是一个对象,保存将来要抛出的所有东西
         exports是当前模块对外的唯一接口

    今后,只要希望将模块内部的东西,抛出到外部,供其它文件使用时,都要添加到module.exports上
    其它文件要想使用当前模块的功能,就必须用require引入当前模块,而require的本质是找模块的exports.

2.2面向函数的方式:

  1. 在文件中,定义多个零散的方法
  2. 将多个零散的方法添加到module的exports上
    其实,可先将零散的方法,先集中定义在一个对象中,再将整个对象赋值给module.exports属性

引入模块: require() 专门负责加载模块文件
何时: 只要在另一个js文件中,引入自定义模块并获取内容时,都用require
本质: 找到js文件,并执行,返回module.exports对象
优化: 单例模式singleton: 始终保持项目中只有一个对象的实例

模块的引入和加载也是单例模式: 模块只在第一次被require时,创建。之后,缓存在内存中。反复require不会导致反复创建模块对象。

强调: 模块是同步加载:前一个加载完,后一个才能开始

 强烈建议: 所有require必须集中在顶部

路径: 以./开头,表示使用相对路径,相对于当前正在执行脚本所在路径——不能省略!

    以/开头,表示Linux系统根目录——绝对路径
    以自定义变量开头,表示在变量保存的地址下继续查找
    什么前缀也不加!只写模块名: 表示加载一个核心模块或项目引入的第三方模块
      路径查找顺序:
        /usr/local/lib/node/模块名.js
        /home/user/projects/node_modules/模块名.js
        /home/user/node_modules/模块名.js
        /home/node_modules/模块名.js
        /node_modules/模块名.js

坑: 简写: module.exports.fun=function(){…}

       可简写为: exports.fun=function(){…}
   exports其实是module.exports的别名
   var exports=module.exports;
问题: 给exports赋值,无法赋值给module.exports
   因为exports只是一个变量,临时保存module.exports的地址值。再次给exports赋任何新值,都导致exports与module.exports分道扬镳!
避免: 不要用简写exports

3.目录模块:

何时: 当一个模块代码,复杂到需要进一步细分时,一个模块,就可能由多个文件组成,保存在一个文件夹里。
如何:

  1. 创建文件夹,集中保存相关的多个js文件模块
  2. 在文件夹中添加一个主模块(index.js),主模块中,引入并组织好多个小模块一起导出
  3. 在文件夹中添加package.json文件,其中:

      {
        "name":"模块名",
        "main":"./主模块相对路径"
      }
    

    其实, 如果没有main甚至没有package.json,也行。
    会自动优先找文件夹下的index.js

引入目录模块: require("./目录名")
如果希望直接用目录名引用模块,不加相对路径:
将目录放入node_modules文件夹中

npm: 第三方模块的包管理工具: 查询,下载
除了核心模块和自定义本地模块,node生态系统还提供了大量优质第三方模块
如何:
查询模块:

模糊查找: npm search 模块名
精确查找: npm search /^模块名$/
  如果现实完整描述: npm search /^模块名$/ --parseable

安装模块: 2个位置:

    1. 全局安装: npm install -g 模块名
      路径: Linux: /usr/local/lib/node_modules

         Windows:
          C:\Users\用户名\AppData\Roaming\npm\node_modules
    2. 项目本地安装: npm install 模块名 -save
    1. 全局对象:
      全局作用域对象不是window,而是global
      ECMAScript标准中原本规定的就是global
      在浏览器中被window代替
      强调: 交互模式: 直接在命令行中测试node应用,所有全局变量/全局函数自动成为global的成员

       脚本模式: 通过加载js文件执行node应用,文件内的"全局变量/全局函数",仅当前文件所有,不会成为global的成员——避免了全局污染
      

      console对象:
      测试重要手段: 打桩: 在关键位置输出关键变量的值
      输出文本信息: 浏览器中4种输出,node中合并为2中:
      console.log/info() 输出普通的文本信息
      console.error/warn() 输出错误信息
      其实: console.xxx()都自带格式化功能
      Console.log vs console.error: .error可直接导出到文件日志中

      如何: node xxx.js 2> error-file.log
      其中:2>表示输出流,专门向硬盘文件写入内容

      输出耗时:
      Console.time("标签"); //预备,开始!
      正常程序逻辑
      Console.timeEnd("标签"); //完成! 自动输出与time之间的时间间隔
      单元测试:
      什么是: 对程序中最小的执行单元进行测试
      开发人员主动对自己的函数执行单元测试
      如何: console.assert(判断条件, "错误提示")

                     只有条件不满足时,才输出msg

      输出堆栈:
      console.trace()

    1. 全局对象: process:

    process.platform
    process.pid
    process.kill(pid);
    

    控制台输入输出:
    2步:

    1. 让控制台进入输入状态:

    process.stdin.resume()
    process.stdin.setEncoding("utf-8")

    1. 监听stdin的data事件:
      在控制台输入后,按回车,会触发stdin的data事件
      process.stdin.on("data",text=>{
          process.stdout.write( … text … )
        })
    

    控制台参数:
    2步: 1. 定义关联数组,保存参数名和参数对应的处理函数

    2. 启动时, process.argv数组可自动获得传入的所有参数,  根据参数调用不同的处理函数

    process.argv: ["node.exe","xxx.js","参数值1","参数值2",…]

    高精度计时:
    精确到纳秒, 优点: 不受系统时间影响
    如何: 2步: 1. 获得开始的时间戳: var start=process.hrtime();

          2. 获得结束时间戳: var diff=process.hrtime(start);

    diff: [秒数, 纳秒]

    获得秒差: diff[0]+diff[1]/1e9
    获得毫秒差: diff[0]*1000+diff[1]/1e6

    Vs console.time/timeEnd:
    time/timeEnd: 缺: 精度低, 优: 效率高
    hrtime: 优: 精度高,且不受系统时间影响

          缺点: 效率低
    

    非I/O的异步操作(定时器):
    何时: 要执行异步回调时
    如何:

    1. setTimeout/setInterval() 将回调函数添加到事件循环的timer阶段的队列中等待执行。
      Timer阶段是事件循环的第一阶段
      习惯上: setTimeout往往都会设置ms数
    2. setImmediate() 将回调函数添加到事件循环的check阶段的队列中等待执行。
      Check阶段比Timer要晚执行
      习惯上: 并不设置毫秒数,而是普通的追加到等待队列末尾即可。
    3. process.nextTick() 将回调函数加入nextTickQueue队列等待执行
      nextTickQueue不参与事件循环,而是在开始timer之前,就立刻执行nextTickQueue中的回调函数
      优点: 不会有延迟
    4. 自定义的EventEmiter

    5.EventEmitter类型:

    Node.js所有异步I/O操作完成时,都会发送一个事件到事件队列
    Node.js中许多对象都会触发事件:
    比如: http模块: 创建Server对象,监听http请求

       一旦收到一个http请求,则立刻触发事件,将处理函数放入事件队列
      fs模块: 在每次读写完文件时,也会触发事件,将处理函数放入事件队列

    什么是EventEmitter: 专门封装事件监听和事件触发的API的一种类型
    所有可以触发事件的对象,都是EventEmitter类型的子对象
    如何让一个对象可以监听并触发事件:

    1. 引入events模块: const events=require("events")
    2. 创建events.EventEmitter类型的子对象:
      var emitter=new events.EventEmitter();
    3. 用on,为对象添加事件监听:
      emitter.on("自定义事件名",function 处理函数(参数列表){

      … 获得参数, 执行操作 …

      })

    4. 在任何情况下,使用对象的emit方法,触发指定的事件:
      emitter.emit("自定义事件名",参数值,…)

    触发一次后,自动解绑:
    emitter.once("自定义事件名",处理函数)

    错误处理:
    问题: try catch无法捕获异步调用中的错误
    解决: Domain
    何时: 只要既希望捕获主程序错误,又希望捕获异步操作的错误时
    如何:

    1. 引入domain模块: const domain=require("domain")
    2. 创建domain对象: const mpDomain=domain.create();
    3. 为domain对象添加error事件监听
      mpDomain.on("error",err=>{

      console.log("出错啦!"+err);

      })

    4. 将可能出错的程序放入mpDomain中运行:
      mpDomain.run(()=>{

      musicPlayer.emit("play");

      })

    6.协议:

    什么是: 计算机之间通过网络实现通信时,事先达成的一种"约定"
    为什么: 约定使不同厂商的设备,不同操作系统之间,都可按照统一约定,任意通信

    7.分组交换方式:

    什么是: 将大数据分割为一个个叫做包(packet)的较小单元进行传输

    8.ISO/OSI模型:

    ISO(国际标准化组织)
    OSI(开放式通信系统互联参考模型)
    7层:

    1. 应用层: 规定应用程序中的通信细节
      包括: HTTP FTP TELNET SMTP DNS
    2. 表示层: 负责数据格式的转换
    3. 会话层: 建立连接
    4. 传输层: 控制总体数据传输
      包括:
      TCP(传输控制协议): 可靠传输
      优: 可靠,客户端和服务端可双向通信
      缺: 传输效率低
      何时: 要求可靠性时
      UDP(用户数据报协议):
      何时: 对可靠性要求不高,对传输效率要求高,且发送小数据(qq, 微信, 在线视频播放)
    5. 网络层: 将数据分组传输到目的地
    6. 数据链路层: 负责规划网络中节点间的路线
    7. 物理层: 负责通过以太网,蓝牙,光纤发送0/1的比特流

    9.TCP/IP: 互联网协议套件

    包含: TCP 传输控制协议

       IP 互联网协议

    TCP/IP不是ISO标准
    TCP/IP 只有四层:

    鄙视:

    1. TCP/IP四层协议,分别对应ISO/OSI中的哪一层: 图6
    2. 网络建立连接需要3次握手,断开连接需要4次握手,分别是:
      图7
    3. HTTP/1.0 1.1 2.0每次升级有哪些不同

    10.net模块:

    使用net模块:

    1. 可创建基于TCP的客户端与服务器端通信

    创建TCP服务器:
    引入net模块
    使用net.createServer方法创建服务端对象server

    接受一个回调函数作为参数:
     只要有客户端连接到当前服务端,就自动执行该回调函数
     回调函数接受一个socket参数对象,用于与客户端通信
     Socket对象: 是客户端在服务器端的一个代理对象
                可通过socket和真正的客户端发送和接受消息
     Socket对象的data事件,可监听客户端发来的消息
       回调函数中, data参数为消息的内容
     Socket对象的end事件,可监听客户端的断开
     Socket的write方法向客户端输出消息

    调用server的listen方法,绑定到一个端口,监听客户端发来的链接请求

    也接受一个回调函数参数,但仅在启动监听后执行一次

    创建TCP客户端:
    引入net模块
    使用net.connect()方法向服务器建立连接
    var client=net.connect(服务端端口,ip,function(){})
    回调函数在连接建立后,自动触发一次
    为client的data事件绑定处理函数,处理函数的data参数自动接收服务端发来的消息
    为client的end事件添加处理函数,当客户端断开连接时执行操作
    在任何位置可用client.write("消息内容")向服务端发送
    在任何位置可用client.end() 断开与服务端连接

    11.HTTP模块:

    使用HTTP模块:

    1. 实现WEB服务器,接受请求并返回响应(代替了apache,tomcat)
    2. 模拟客户端向一个指定的WEB服务器发送请求

    创建HTTP服务端:
    引入HTTP模块
    创建HTTP服务端server:
    var server=http.createServer(function(req,res){

     //只要有请求发送到该服务器,就自动触发该回调函数
     //其中: 
       //req对象,封装了发来的请求信息
       //res对象,专门用于向服务器端返回响应
        //res.writeHead(状态码,{ 属性:值, …:… ,…})
        //res.write("放入响应主体中")
        //res.end()

    })
    启动监听: server.listen(端口,function(){ … })

    创建HTTP请求:
    使用http.request()方法创建一个请求(连接),获得请求对象req
    接收2个参数:

    options对象参数:
     host
     port
     method
     path  /index.html?page=12
    回调函数: 在服务器端返回响应时执行
     参数res: 专门用于获得响应内容(响应头和响应主体)
      HTTP协议规定: 先发响应头部 用res.headers获得响应头部对象,用res.statusCode 获得状态码
      强调: 响应主题是稍后才发送过来
       必须用res.on("data",function(buffer){ … String(buffer) …})
      强调: 凡是从响应中获得的data,默认都是字符串

    req.end()结束并发送请求。
    强调:必须加req.end(),请求才能发送出去

    http.get()
    专门向服务器端发送get请求
    是http.request()的简化:

    1. 自动设置method为get;
    2. 自动调req.end

    但依然需要使用res.on("data",function(buffer){ … })来接受响应主体

    分块:
    问题: 如果响应主体过大,一次性传不过来
    解决:

       分块发送和接受,再拼接,再整体转换
       如果分块接受,res.on("data",function(buf){ … })每收到一块,就会反复触发。
       其中buf,仅是其中一块而已
    

    请求文件,保存在本地:
    引入fs模块:
    创建写入流,指向目标文件: var writable=fs.createWriteStream("相对路径")
    使用管道,将写入流writable连接到res对象上: res.pipe(writable)

    响应头部: res.writeHead(状态码,{ })
    允许跨域: "Access-Control-Allow-Origin":"请求来源的网站"
    指定内容类型:"Content-Type":"application/json" "text/css"

    req对象:
    请求头部: req.headers
    请求方法: req.method
    请求地址: req.url
    url的处理:

    引入url模块
    用url.parse(req.url,true)将req.url字符串转为对象
     其中true,表示将search中的参数也转为对象属性
     如何: var obj=url.parse(req.url, true)
      其中: obj.query中保存了所有参数及其值

    获得请求参数:
    Get: get方式的参数都通过url中的search传递

      obj=url.parse(req.url,true)
      obj.query

    Post: post方式的参数都是放在请求主体中,没有在url中

      问题:obj.query无法获得
      解决: req.on("data",function(buf){ … })
      问题: String(buf)获得的是参数的字符串
      解决: querystring模块
    
    
    

    12.https模块:

    问题: http协议是明文的
    危害: 1. 通信使用明文,内容可能被窃听

       2. 不验证身份,有可能遭遇伪装
       3. 无法证明消息的完整性,消息有可能被篡改

    网络嗅探器:

    13.解决: https协议

    https是更安全的http协议:

    1. 客户端和服务器端的双向认证
    2. 完整性检查
    3. 内容加密

    https=http+ssl

    ssl/tls: ssl 安全套接层,对传统socket进一步提供安全的保护

     tls 传输层安全, 其实是ssl的继任者
    

    14.提供三大服务:

    1. 客户端和服务器端的双向认证 ——可靠
    2. 完整性检查 ——完整
    3. 数据加密 ——机密性
      tls/ssl的执行过程:

    15.Step0: 获得服务器端证书, 3步:

    1. 在服务器端生成私钥
    2. 用私钥生成一个证书申请文件
    3. 将私钥和申请文件交给第三方CA,第三方CA经过审查,会生成并颁发证书给申请的服务器
      证书包含2样东西: 公钥+公司的信息
      Step1: 客户端请求https协议的web服务器
      Step2: 服务器返回证书给客户端
      Step3: 客户端拿到证书后,将证书交给CA。

        客户端利用CA中的公钥随机生成自己的私钥
        将私钥发给服务器端

      Step4: 服务器端获得客户端发来的客户端私钥
      到此,客户端和服务器端,拥有了相同的两个钥匙
      之后,服务器和客户端发送的所有消息,都用两个相同的私钥加密和解密

    16.如何实现https的web服务器应用:

    1. 申请https网站的认证证书:

    Step1: 用openssl生成服务器端私钥:
    openssl genrsa -out d:/privatekey.pem 1024
    Step2: 用私钥生成证书申请文件:

    openssl  req  -new  -key  d:/privatekey.pem  -out  d:/certificaterequest.csr

    Step3: 用私钥和证书申请文件共同生成证书文件

    openssl  x509  -req  -in  d:/certificaterequest.csr  -signkey  
    d:/privatekey.pem   -out  d:/certificate.pem
    

    2.使用node的https模块创建服务器
    Step1: 引入必须的模块:
    const https=require(“https”);
    const fs=require(“fs”);
    Step2:读取服务器私钥和证书文件,保存到服务器程序的变量中
    let privatekey=fs.readFileSync(“d:/privatekey.pem”);
    let certificate=fs.readFileSync(“d:/certificate.pem”);
    Step3: 用https创建服务器端应用程序,提供私钥和证书,并定义处理请求的回调函数

    https.createServer(
      {
        key: privatekey,
        cert: certificate
    },
    (req,res)=>{
      res.write(“…”)
      res.end();
    }
    ).listen(443)
    

    3.用https模块向https的服务器发送请求
    错误: http模块不支持向https服务器发送请求
    正确:

    var https=require(“https”);
    https.get(“https://...”, res=>{
    res.on(“data”,buf=>{
    buf…
    })
    })

    17.express

    什么是: 基于node的http模块和第三方的Connect框架的web框架
    Connect框架: 专门将各种各样的中间件函数粘合在一起,共同处理http请求中的req对象
    何时: 只要对req对象反复执行多种操作时,都要用connect组织多个中间件。
    如何:

    Step1: 安装connect模块: npm install connect –save

    Step2: 引入connect模块: var connect=require(“connect”)

    Step3: 用connect模块创建处理req对象的应用程序实例var app=connect();

    Step4: 向connect模块的应用程序实例中添加中间件函数

      app.use(function md1(req,res,next){
      //加工req对象
      … …
      next();
    })
    

    Step5: connect的应用程序实例,必须要放入createServer中用于处理服务器接收到的req对象
    http.createServer(app)
    总结: express是在connect基础上的进一步封装和简化,所以express也是采用中间件组合的方式,处理req对象
    安装express框架: 2种:

    1. 使用本地express模块,进能够提供服务支持,需要自定义添加复杂的程序结构

      Step1: npm install –save express
      Step2: 引入http和express
      const http=require(“http”);
      const express=require(“express”);
      Step3: 创建express应用实例对象:
      let app=express();
      Step4: 为app添加各种处理中间件函数
      app.use(function md(req,res,next){ … …})
      Step5: 将app和createServer相连
      http.createServer(app).listen(端口号);

    2. 使用脚手架, 简化生成项目的结构:

    Step1: 全局安装express生成器:
    npm install –g express-generator

    Step2: 用生成器,生成项目脚手架代码:
    express 项目文件夹名 –e //-e 表示用EJS作为前端页面模板
    强调: 只负责生成项目代码,并不负责下载依赖包

    Step3: 为脚手架代码下载所有依赖包
    cd 项目文件夹下
    npm install //根据package.json中的依赖项

    Step4: 用脚手架代码启动nodejs服务器端应用程序: npm start

    express项目结构:

    1. ./bin/www.js express项目的启动文件

    package.json中: npm start 时 自动执行 node ./bin/www

    2./app.js 对express框架的实例对象的配置
    要求: 对express实例对象app的所有配置必须放在一个独立的文件模块app.js中
    然后,在主程序www.js中引入app.js模块

    3../routes/xxx.js 路由模块
    每个子功能,都应该集中定义在一个路由模块文件中
    在app.js中引入路由文件模块,并将路由文件模块添加到app的中间件列表中,并设置上级路径
    在每个子路由模块文件中,创建路由对象,为路由对象添加不同请求方法和不同子路径下的处理函数
    强调: 子路由中的相对路径,都是在上级路径之下的相对路径

    改造脚手架项目结构:

    1. 补充缺失的模块:
      express-session 让express可以处理session
      connect-flash 强化自动维护session的功能
      passport 综合的用户验证解决方案

               ( 使用passport模块,实现qq,微信登录)
    2. 在app.js中添加对新模块的引用:
    3. 为项目添加mongodb支持

    Step1: 安装mongoose模块和promise模块
    mongoose: node js专用的简化操作mongodb数据库的模块

    Step2: 创建文件夹./config,在文件夹下添加config.js
    在config.js中定义对象模块,保存连接字符串
    module.exports={
    db:”mongodb://主机名或ip/数据库名”}

    Step3: 在./config文件夹下创建mongoose.js,保存创建连接对象的代码:
    var config=require('./config'),
    mongoose=require('mongoose');
    设置mongoose的promise属性,使用当前项目的promise模块
    mongoose.Promise=require(‘promise’);
    var db=mongoose.connect(config.db) module.exports=db;

    Step4: 根据业务需要,定义mongoose模型对象:
    创建./models文件夹, 在models内为每种业务对象创建专门的模型文件

    3步:

    1. 引入mongoose,获得Schema类型
    2. 用Schema创建UserSchema结构
    3. 将UserSchema编译为User模型,并抛出为User模块
      Step5: 回到mongoose.js中,在connect之后,引入User模块require('../models/user.model');
      Step6: 回到app.js中,在路由中间件之前,先请求并初始化mongoose.jsrequire("./config/mongoose");

    楷楷
    4k 声望10.5k 粉丝

    兴趣是最好的老师!