Nodejs 一些面试经验

针对网络应用开发的平台

主要特征:

  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");