Nodejs 一些面试经验
知识点总结——NODE.JS
针对网络应用开发的平台
主要特征:
- 基于Google的JavaScript运行时引擎V8
- 扩展了Node标准类库: TCP,同步或异步文件管理,HTTP
为什么使用Node:
- 可以在服务器端运行js: 现有前端团队可直接参与后端js开发
-
js天生支持非阻塞IO:
IO: 代表一切数据进出程序的操作:
包括: 文件读写, 数据库操作, 网络操作
问题: 有延迟
传统阻塞IO: IO操作会阻塞当前主线程,直到本次IO操作完成,才能执行后续代码。
非阻塞IO: 即使处理较慢的IO操作时,主进城仍然能处理其他请求
Js天生支持非阻塞: 回调函数=事件循环+回调队列
所有非阻塞的操作,返回的结果暂时在回调队列中等待
由事件循环,自动依次取回到主程序中恢复执行
回调队列在主程序之外存储回调函数,所以,不会干扰主程序执行
非阻塞在Web服务器中:
普通服务器端应用: 虽然可实现每个请求独立线程/进程, 但如果一个请求中,包含多个阻塞IO操作(访问数据库,网络,读写硬盘文件),该请求返回的时间就等于所有IO操作的时间总和——慢
Node服务器端应用: 不但每个请求是一个独立的线程,且,每个请求内的每个IO操作,都是非阻塞的。一个包含多个IO操作的请求,返回的总响应时间,仅仅等于其中一个时间最长的IO操作的时间。 Node.js vs javascript: Javascript: 编程语言, 依照ECMAScript
2种运行环境:
- 客户端浏览器: 由各种客户端浏览器中的js解释器执行
扩展: DOM API 和 BOM API 主要目的是为了操作网页内容和浏览器窗口 - 独立的js解释器:Node.js 应用程序开发和运行的平台
仅支持ECMAScript
扩展: 各种专门的服务器模块: TCP, HTTP, 文件读写, MYSQL构建一个简单的node应用:
创建一个新的node项目: 基本命令:
mkdir 项目文件夹
cd 项目文件夹
npm init //负责在当前所在的项目目录下自动生成package.json配置文件
运行:node 入口文件.js
- 客户端浏览器: 由各种客户端浏览器中的js解释器执行
2.module
Node应用都是由模块组成
模块就是组织程序功能的一种文件或文件夹
Node应用采用CommonJS模块规范
CommonJS规定:
- 每个文件就是一个模块,有自己的作用域——避免全局污染
一个文件内定义的变量,函数,类都是该文件私有,对其它文件默认不可见 - 对象,方法和变量也可以从一个文件/模块中导出(exports),用在其它文件/模块中。
实际项目中,都是将各种功能/数据,划分为不同项目模块来管理
如何定义一个模块:2步:
- 在模块/文件中定义业务代码(对象,class,函数)
- 将内部的功能抛出,用于将来其它js文件调用
2种情况:
2.1面向对象的方式:
- 定义一种class或一个对象,包裹属性和功能
-
将class或对象直接赋值给module.exports
其中: module,指当前模块对象/当前文件exports是当前module对象的一个属性 本质上也是一个对象,保存将来要抛出的所有东西 exports是当前模块对外的唯一接口
今后,只要希望将模块内部的东西,抛出到外部,供其它文件使用时,都要添加到module.exports上
其它文件要想使用当前模块的功能,就必须用require引入当前模块,而require的本质是找模块的exports.
2.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.目录模块:
何时: 当一个模块代码,复杂到需要进一步细分时,一个模块,就可能由多个文件组成,保存在一个文件夹里。
如何:
- 创建文件夹,集中保存相关的多个js文件模块
- 在文件夹中添加一个主模块(index.js),主模块中,引入并组织好多个小模块一起导出
-
在文件夹中添加package.json文件,其中:
{ "name":"模块名", "main":"./主模块相对路径" }
其实, 如果没有main甚至没有package.json,也行。
会自动优先找文件夹下的index.js
引入目录模块: require("./目录名")
如果希望直接用目录名引用模块,不加相对路径:
将目录放入node_modules文件夹中
npm: 第三方模块的包管理工具: 查询,下载
除了核心模块和自定义本地模块,node生态系统还提供了大量优质第三方模块
如何:
查询模块:
模糊查找: npm search 模块名
精确查找: npm search /^模块名$/
如果现实完整描述: npm search /^模块名$/ --parseable
安装模块: 2个位置:
-
全局安装: npm install -g 模块名
路径: Linux: /usr/local/lib/node_modulesWindows: C:\Users\用户名\AppData\Roaming\npm\node_modules
- 项目本地安装: npm install 模块名 -save
-
-
全局对象:
全局作用域对象不是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()
- 全局对象: process:
process.platform
process.pid
process.kill(pid);
控制台输入输出:
2步:
- 让控制台进入输入状态:
process.stdin.resume()
process.stdin.setEncoding("utf-8")
- 监听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的异步操作(定时器):
何时: 要执行异步回调时
如何:
- setTimeout/setInterval() 将回调函数添加到事件循环的timer阶段的队列中等待执行。
Timer阶段是事件循环的第一阶段
习惯上: setTimeout往往都会设置ms数 - setImmediate() 将回调函数添加到事件循环的check阶段的队列中等待执行。
Check阶段比Timer要晚执行
习惯上: 并不设置毫秒数,而是普通的追加到等待队列末尾即可。 - process.nextTick() 将回调函数加入nextTickQueue队列等待执行
nextTickQueue不参与事件循环,而是在开始timer之前,就立刻执行nextTickQueue中的回调函数
优点: 不会有延迟 - 自定义的EventEmiter
5.EventEmitter类型:
Node.js所有异步I/O操作完成时,都会发送一个事件到事件队列
Node.js中许多对象都会触发事件:
比如: http模块: 创建Server对象,监听http请求
一旦收到一个http请求,则立刻触发事件,将处理函数放入事件队列
fs模块: 在每次读写完文件时,也会触发事件,将处理函数放入事件队列
什么是EventEmitter: 专门封装事件监听和事件触发的API的一种类型
所有可以触发事件的对象,都是EventEmitter类型的子对象
如何让一个对象可以监听并触发事件:
- 引入events模块: const events=require("events")
- 创建events.EventEmitter类型的子对象:
var emitter=new events.EventEmitter(); -
用on,为对象添加事件监听:
emitter.on("自定义事件名",function 处理函数(参数列表){… 获得参数, 执行操作 …
})
- 在任何情况下,使用对象的emit方法,触发指定的事件:
emitter.emit("自定义事件名",参数值,…)
触发一次后,自动解绑:
emitter.once("自定义事件名",处理函数)
错误处理:
问题: try catch无法捕获异步调用中的错误
解决: Domain
何时: 只要既希望捕获主程序错误,又希望捕获异步操作的错误时
如何:
- 引入domain模块: const domain=require("domain")
- 创建domain对象: const mpDomain=domain.create();
-
为domain对象添加error事件监听
mpDomain.on("error",err=>{console.log("出错啦!"+err);
})
-
将可能出错的程序放入mpDomain中运行:
mpDomain.run(()=>{musicPlayer.emit("play");
})
6.协议:
什么是: 计算机之间通过网络实现通信时,事先达成的一种"约定"
为什么: 约定使不同厂商的设备,不同操作系统之间,都可按照统一约定,任意通信
7.分组交换方式:
什么是: 将大数据分割为一个个叫做包(packet)的较小单元进行传输
8.ISO/OSI模型:
ISO(国际标准化组织)
OSI(开放式通信系统互联参考模型)
7层:
- 应用层: 规定应用程序中的通信细节
包括: HTTP FTP TELNET SMTP DNS - 表示层: 负责数据格式的转换
- 会话层: 建立连接
- 传输层: 控制总体数据传输
包括:
TCP(传输控制协议): 可靠传输
优: 可靠,客户端和服务端可双向通信
缺: 传输效率低
何时: 要求可靠性时
UDP(用户数据报协议):
何时: 对可靠性要求不高,对传输效率要求高,且发送小数据(qq, 微信, 在线视频播放) - 网络层: 将数据分组传输到目的地
- 数据链路层: 负责规划网络中节点间的路线
- 物理层: 负责通过以太网,蓝牙,光纤发送0/1的比特流
9.TCP/IP: 互联网协议套件
包含: TCP 传输控制协议
IP 互联网协议
TCP/IP不是ISO标准
TCP/IP 只有四层:
鄙视:
- TCP/IP四层协议,分别对应ISO/OSI中的哪一层: 图6
- 网络建立连接需要3次握手,断开连接需要4次握手,分别是:
图7 - HTTP/1.0 1.1 2.0每次升级有哪些不同
10.net模块:
使用net模块:
- 可创建基于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模块:
- 实现WEB服务器,接受请求并返回响应(代替了apache,tomcat)
- 模拟客户端向一个指定的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()的简化:
- 自动设置method为get;
- 自动调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协议:
- 客户端和服务器端的双向认证
- 完整性检查
- 内容加密
https=http+ssl
ssl/tls: ssl 安全套接层,对传统socket进一步提供安全的保护
tls 传输层安全, 其实是ssl的继任者
14.提供三大服务:
- 客户端和服务器端的双向认证 ——可靠
- 完整性检查 ——完整
- 数据加密 ——机密性
tls/ssl的执行过程:
15.Step0: 获得服务器端证书, 3步:
- 在服务器端生成私钥
- 用私钥生成一个证书申请文件
-
将私钥和申请文件交给第三方CA,第三方CA经过审查,会生成并颁发证书给申请的服务器
证书包含2样东西: 公钥+公司的信息
Step1: 客户端请求https协议的web服务器
Step2: 服务器返回证书给客户端
Step3: 客户端拿到证书后,将证书交给CA。客户端利用CA中的公钥随机生成自己的私钥 将私钥发给服务器端
Step4: 服务器端获得客户端发来的客户端私钥
到此,客户端和服务器端,拥有了相同的两个钥匙
之后,服务器和客户端发送的所有消息,都用两个相同的私钥加密和解密
16.如何实现https的web服务器应用:
- 申请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种:
- 使用本地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(端口号); - 使用脚手架, 简化生成项目的结构:
Step1: 全局安装express生成器:
npm install –g express-generator
Step2: 用生成器,生成项目脚手架代码:
express 项目文件夹名 –e //-e 表示用EJS作为前端页面模板
强调: 只负责生成项目代码,并不负责下载依赖包
Step3: 为脚手架代码下载所有依赖包
cd 项目文件夹下
npm install //根据package.json中的依赖项
Step4: 用脚手架代码启动nodejs服务器端应用程序: npm start
express项目结构:
- ./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的中间件列表中,并设置上级路径
在每个子路由模块文件中,创建路由对象,为路由对象添加不同请求方法和不同子路径下的处理函数
强调: 子路由中的相对路径,都是在上级路径之下的相对路径
改造脚手架项目结构:
-
补充缺失的模块:
express-session 让express可以处理session
connect-flash 强化自动维护session的功能
passport 综合的用户验证解决方案( 使用passport模块,实现qq,微信登录)
- 在app.js中添加对新模块的引用:
- 为项目添加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步:
- 引入mongoose,获得Schema类型
- 用Schema创建UserSchema结构
- 将UserSchema编译为User模型,并抛出为User模块
Step5: 回到mongoose.js中,在connect之后,引入User模块require('../models/user.model');
Step6: 回到app.js中,在路由中间件之前,先请求并初始化mongoose.jsrequire("./config/mongoose");