最近在准备面试,就初步整理了下nodejs在面试中常被问的点,后面我也会逐步补充,如有不全,欢迎指出,如有错误,欢迎指正呀~~~
相关面试题:
- express和koa区别,优缺点
- nodejs优缺点
- node中间层细节处理
- 事件循环机制,node和浏览器的事件循环机制区别
- node的多线程,高并发,安全
可连接部分本文都有详解,但是由于掘金的markdown不支持锚点语法,不知道怎样才能跳转到本页面对应位置,因此点击无反应,是心痛的感jio
nodejs
定义
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时
nodejs可以解析执行js代码,以前只有浏览器可以执行js代码,有了nodejs后js代码可以脱离浏览器执行
nodejs的特点
- event-driven 事件驱动
- non-blocking I/O model 非阻塞I/O模型
- lightweight and efficient 轻量和高效
node中的模块系统
- 模块化是一种将项目分解成独立功能部分的方法
- 在 Node 中,没有全局作用域,只有模块作用域
- 模块化的文件,具有模块作用域,外部访问不到内部变量,内部也访问不到外部变量,默认都是封闭的。每个文件的命名空间都是独立且封闭的,不同模块之间不会互相影响,不会有污染情况
- 只有通过
export
暴露出去,其他文件才能通过require
拿到
模块化的优缺点
- 优点
- 可维护性
- 多人协作互不干扰,解决了多人开发过程中可能出现的命名冲突问题
- 灵活架构,焦点分离
- 方便模块间组合、分解
- 方便单个模块调试、升级
- 可分单元测试
- 可维护性
- 缺点
- 性能损耗
- 系统分层,调用链会很长
- 模块间通信,模块间发送消息会很耗性能
- 性能损耗
commonjs
js 天生不支持模块化(es6才支持),nodejs 环境中对 js 做了特殊的模块化支持,即为 commonjs
加载 require
var fs = require('fs')
- 作用
- 加载并执行模块中的代码(同步)
- 得到被加载模块中导出的接口对象
导出 exports
- node 中是模块作用域,默认文件中所有成员只在当前文件模块有效,就算被加载了,也无法访问
- 只有挂载到
exports
接口对象中才可以被访问
exports
和 module.exports
区别
-
在 node 中,每个模块内部都有一个自己的
module
对象 -
module.exports
也是一个对象(默认是空对象) -
对外导出成员,只需要把导出的成员挂载到
module.exports
中 -
每次导出接口成员的时候都通过
module.exports.xxx = xxx
的方式很麻烦,Node 为了简化操作,专门提供了一个变量exports
。也就是说在模块中还有这么一句代码var exports = module.exports
-
当一个模块需要导出单个成员的时候,直接给
exports
赋值是不管用的。exports
赋值会断开和module.exports
之间的引用。同理,给module.exports
重新赋值也会断开 -
模块中最后 return 的是
module.exports
,不是exports
,所以给exports
重新赋值无作用// 向外暴露一个类用module.exports语法 module.exports = 构造函数名 // 一般情况下,某一个js文件中,提供了函数,供别人使用。 只需要暴露函数就行了 exports.msg = msg
模块加载规则
- 优先从缓存中加载
- 如果已经
require
过,不会重复执行加载,直接可以拿到里面的接口对象 - 目的是避免重复加载,提高模块加载效率
- 如果已经
- 判断模块标识符
require('模块标识符')
- 核心模块
- 自定义模块(路径形式的模块标识)
- ./,../或/xxx,d:/a/foo.js
- 首位的/是绝对路径,代表当前文件模块所属磁盘根目录c:/
- 第三方模块
核心模块
- 核心模块是由 Node 提供的一个个的具名的模块,它们都有自己特殊的名称标识
- 例如文件操作的 fs 模块
- http 服务构建的 http 模块
- path 路径操作模块
- os 操作系统模块
- 核心模块必须引入才能使用
第三方模块
- 凡是第三方模块都必须通过 npm 来下载
- 使用的时候就可以通过
require('包名')
的方式来进行加载才可以使用 - 不可能有任何第三方包名和核心模块重名,提交第三方包时不会允许,不然加载时会有冲突
- 既不是核心模块、也不是路径形式的模块,则先找到当前文件所处目录中的 node_modules 目录
- node_modules 下对应模块的 package.json 文件中的 main 属性中记录了
art-template
的入口模块,如果 package.json 文件不存在或者 main 指定的入口模块是也没有,则 node 会自动找该目录下的 index.js - 实际上最终加载的还是文件
- node_modules 下对应模块的 package.json 文件中的 main 属性中记录了
- 如果以上所有任何一个条件都不成立,则会进入上一级目录中的 node_modules 目录查找,如果上一级还没有,则继续往上上一级查找......如果直到当前磁盘根目录还找不到,最后报错
- 一个项目有且只有一个 node_modules,放在项目根目录中,这样项目中所有的子目录中的代码都可以加载到第三方包
自定义模块
- 加载文件时相对路径一定要加./,不然默认是加载核心模块
小扩展
require()
中的路径,是从当前这个 js 文件出发,找到目标文件- 而 fs 是从命令提示符找到目标文件
- fs 等其他的模块用到路径的时候,都是相对于 cmd 命令光标所在位置
- 所以,在 b.js 中想读 1.txt 文件,推荐用绝对路径:
fs.readFile(__dirname + "/1.txt",function(err,data){ if(err) { throw err; } console.log(data.toString()); })
npm
npm—node package manager: 世界上最大的开源库生态系统,绝大多数 javascript 相关的包都存放在了 npm 上,目的是为了让开发人员更方便的下载使用
- npm 是基于 nodejs 开发出来的包管理工具,所以用 npm 时,要先安装 node
- npm 的第二种含义是一个命令行工具
解决npm被墙问题
npm 存储包文件的服务器在国外,有时候会被墙,速度很慢
- 方法一:安装淘宝镜像 cnpm
# 后面安装的时候直接用cnpm install即可 npm install --global cnpm
- 方法二:不想安装 cnpm 又想通过淘宝服务器下载
npm install jquery --registry=http://registry.npm.taobao.org
- 方法三:每次手动写淘宝参数觉得麻烦,又不想是 cnpm ,那么配置如下(推荐)
npm config set registry http://registry.npm.taobao.org # 查看npm是否配置成功,成功后 npm install 就默认通过淘宝镜像 npm config list
package.json文件
作用是:
- 包描述文件(当前项目说明书)。创建方法:
npm init
- 保存第三方包的依赖信息,比 node_modules 清晰。package.json 文件相当于给他人使用时,提供了一份安装所有依赖包的自动下载索引
- dependencies:在生产环境中需要用到的依赖
- devDependencies:在开发、测试环境中用到的依赖
- 允许我们使用 “语义化版本规则”, 指明项目里依赖包的版本
- 让你的构建更好地与其他开发者分享,便于重复使用
npm 可以直接运行 package.json 中 scripts 指定的脚本
删除包时,如果要把依赖项信息删除,命令是
npm uninstall 包名 --save
package-lock.json文件
- npm5 之前没有 package-lock.json 这个文件,之后才加入
- npm5 之后的版本安装包不需要加
--save
参数,会自动保存依赖信息 - 安装包时,会自动创建或更新 package-lock.json,package.json不会自动生成
- package-lock.json 会保存 node_modules 中所有包的信息(版本、下载地址),这样重新
npm i
时候速度可以提升 - 从文件来看 lock 用来锁定版本,比如
1.1.1
。只用 package.json时,会出现^1.1.1
意思是1.1.1
版本以上都可以,npm i
会直接下载最新版本的包,但我们希望永远下载的是1.1.1
版本,不要下载最新版本,有时候版本更新会引发 API 等问题。可以锁定具体版本号,防止自动升级新版
- npm5 之后的版本安装包不需要加
package-lock.json作用
- 记住依赖信息,提升加载速度
- 为了系统的稳定性考虑,锁定版本号,防止自动升级
事件循环机制
- 什么是同步异步
- 同步
- 等待被调用方执行完毕才能继续执行
- 会阻塞后面代码的执行
- 异步
- 不需要一直等待被调用方响应,调用方的主动轮询和被调用方的主动通知
- 不会阻塞后面代码的执行
- 区别:调用过程中是主动等待还是被动通知,是否阻塞
- 同步
- 什么是阻塞非阻塞
- 区别:调用状态,调用方在获取结果的过程中是干等还是互不耽误
- 异步非阻塞是节约调用方时间的(nodejs 一大特点)
- 什么是异步IO
- 操作系统所提供的IO能力
- 生活中可见的IO能力:人机交互,数据的进出,鼠标键盘等物理接口为输入,显示器为输出。这些接口再向下会进入操作系统层面,操作系统会提供诸多能力(磁盘读写,DNS查询,数据库连接)+ 上层应用和下层系统之间的交互,同步阻塞则为同步IO,异步非阻塞则为异步IO
- nodejs 中 fs 模块里的 readFile 文件读写就是典型异步IO,readFileSync 就是同步IO
- 什么是单线程
- 同一时间只能做一件事情,两段js代码不能同时执行
- 原因是避免DOM渲染冲突。多行js同时执行时,可能会同时操作DOM,引发冲突
- 异步是js单线程的解决方案,是一种无奈的解决方案,还存在很多问题
- 没有按照书写方式执行,可读性差
- callback 中不容易模块化
- 实现方式 event-loop
浏览器的事件循环机制 event-loop
- js实现异步的具体解决方案
- 同步代码,在主进程中直接执行
- 异步函数先放在异步队列中
- 待同步函数执行完毕,轮询执行异步队列的任务
- 宏任务 macrotask
- script 整体代码
- setTimeout
- setInterval
- setImmediate
- I/O
- ui render
- 微任务 microtask
- process.nextTick
- promise.then
- async/await(实则就是promise)
- MutationObserver(h5新特性)
- 宏任务 macrotask
- js异步机制由事件循环和任务队列构成。JS本身是单线程语言,所谓异步依赖于浏览器或者操作系统等完成。JavaScript 主线程拥有一个执行栈以及一个任务队列,主线程会依次执行代码
- 遇到异步操作(例如:setTimeout, AJAX)时,异步操作会由浏览器(OS)执行,浏览器会在这些任务完成后,将事先定义的回调函数推入主线程的任务队列(task queue)中,当主线程的执行栈清空之后会读取任务队列中的回调函数,当任务队列被读取完毕之后,主线程接着执行,从而进入一个无限的循环,这就是事件循环
- 轮询监测异步队列中,当前时刻有函数,就拿到主进程中执行,执行完后又监测异步队列,有函数拿到主进程执行...如此轮询
- 每执行一次宏任务后,执行玩任务队列中的所有微任务,再进行下一个宏任务,如此循环
nodejs的事件循环机制
- 在Node中,事件循环的模型和浏览器相比大致相同,而最大的不同点在于Node中事件循环分不同的阶段
- 两者最主要的区别在于浏览器中的微任务是在每个相应的宏任务中执行的,而nodejs中的微任务是在不同阶段之间执行的
- libuv里面的uv_run函数实现了事件循环的整个过程(6个阶段)
- timer
- setTimeout
- setInterval
- IO callback 事件回调阶段
- eventEmitter
- idle,prepare 闲置阶段
- 仅系统内部使用
- poll 轮询阶段
- 检索新的 I/O 事件,执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
- incoming data 输入数据
- fs.readFile
- 检索新的 I/O 事件,执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
- check 检查阶段
- 直接执行setImmediate
- close callback 关闭事件的回调
- 如果一个socket或handle被突然关掉(比如socket.destroy()),close事件将在这个阶段被触发,否则将通过process.nextTick()触发
- timer
- 日常开发中的绝大部分异步任务都是在 poll、check、timers 这3个阶段处理的,简述poll阶段
- 如果当前已经存在定时器,而且有定时器到时间了,拿出来执行,eventLoop 将回到 timers 阶段
- 如果没有定时器, 会去看回调函数队列
- 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制
- 如果 poll 队列为空时,会有两件事发生
- 如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调
- 如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去,一段时间后自动进入 check 阶段
- setTimeout和setInterval
- 如果两者都在主模块中调用,那么执行先后取决于进程性能,也就是随机
- 如果两者都不在主模块调用(被一个异步操作包裹),那么setImmediate的回调永远先执行
nodejs的优缺点
(擅长I/O密集、不擅长计算密集....)
- 优点
- 处理高并发场景性能更高
- 适合I/O密集型应用
- 缺点
- 不适用用CPU密集型
- CPU使用率较重、IO使用率较轻的应用——如视频编码、人工智能等,Node.js的优势无法发挥
- nodejs是单线程的,只支持单核CPU,无法充分利用多核 CPU 的性能
- 可靠性低,一旦代码某个环节崩溃,整个系统都崩溃
- 不适用用CPU密集型
- 解决方案
- Nnigx反向代理,负载均衡,开多个进程,绑定多个端口
- 单线程变成多线程:开多个进程监听同一个端口,使用cluster模块
nodejs适合处理高并发的原因
- 单线程解决高并发的思路就是采用非阻塞,异步编程的思想。简单概括就是当遇到非常耗时的IO操作时,采用非阻塞的方式,继续执行后面的代码,并且进入事件循环,当IO操作完成时,程序会被通知IO操作已经完成。主要运用JavaScript的回调函数来实现。
- 多线程虽然也能解决高并发,但是是以建立多个线程来实现,其缺点是当遇到耗时的IO操作时,当前线程会被阻塞,并且把cpu的控制权交给其他线程,这样带来的问题就是要非常频繁的进行线程的上下文切换
1.异步I/O
node 具有异步 I/O 特性,每当有 I/O 请求发生时,node 会提供给该请求一个 I/O 线程。然后 node 就不管这个 I/O 的操作过程了,而是继续执行主线程上的事件,只需要在该请求返回回调时再处理即可。也就是 node 省去了许多等待请求的时间
2.事件驱动
主线程通过event loop事件循环触发的方式来运行程序
适用nodejs的场景
- RESTful API
- 统一Web应用的UI层
- 大量Ajax请求的应用
- 开发命令行工具
node
和apache
等服务器软件的区别
- 没有自己的语法,使用V8引擎,所以就是JS。Node就是将V8中的一些功能自己没有重写,移植到了服务器上。V8引擎解析JS的,效率非常高,并且V8中很多东西都是异步的。
- Node.js没有根目录的概念,因为它根本没有任何的web容器,就是安装配置完成之后,没有一个根目录。让node.js提供一个静态服务,需要自己封装
node作为中间层
为什么引入node中间层
说到这个问题,首先要理解前后端分离后,客户端渲染的缺陷
服务端渲染
- 说白了就是在服务端使用模板引擎,在发送给客户端之前,已经把数据请求渲染到模板上了
var 渲染结果 = template.render('模板字符串', { 解析替换对象 })
- 其实就是MVC模式
- 这种模式下,服务端压力非常大,服务端处理的业务很杂,因此需要前后端分离,保证后端只用响应数据
客户端渲染
- 第一次请求拿到页面
- 第二次请求拿到数据
- 在客户端拿到ajax响应结果并渲染数据
- MVVM模式
如何判断服务端和客户端渲染
- 查看网站源代码,如果动态请求的数据能搜索到,那就是服务端渲染
- 如果网页没刷新,但切换了页面,则一定是客户端渲染
客户端渲染的缺陷
- 首屏白屏时间长
- 服务端渲染是可以被爬虫抓取到的,客户端渲染不会被爬虫抓取到,不利于 SEO 搜索引擎优化
- 模板引擎解析时,全由浏览器完成,浏览器负担过重
- 客户端渲染有时会有接口跨域问题
为了解决客户端渲染的诸多问题,可以引入node作为中间层。用nodeJS搭一个中间层来渲染数据,可以弥补前端模板引擎和路由无法做到的SEO友好性工作
node作为中间层的细节处理
整个流程图如下:
- 将渲染的工作拿到nodejs中间层处理,客户端发送请求后,接收到的就是一个完整的html页面,利于搜索引擎优化
- node中间层可以完成服务端渲染,但是区别于传统的ssr
- node的高并发属性,更适用于大型项目,提高渲染效率
- node可以用redis将后端数据缓存下来
- node可以做请求合并和负载均衡,分担后端的高并发压力
在Node中使用模板引擎
- art-template 前后端都可以用
- ejs 是通过字符串方式,效率较低
- jade 效率高,但学习成本高
express
中间件
什么是中间件
- 处理请求的,本质是函数
- 用户从请求到响应过程中的处理环境分步骤处理,每个步骤调用一个方法完成,这个方法就是中间件(中间处理环节)
- 处理和封装(例如query,postBody,cookie,session),返回一个方法,挂载在req上
- 一定在路由前挂载,因为需要在路由里使用,接收三个参数(req,res,next)
express中的中间件
express中对中间件有几种分类,同一个请求中所有中间件都操作的是同一个req,res
1.应用程序级别中间件
万能匹配,不关心请求路径和请求方法的中间件
- 任何请求都会进入这个中间件
var express = require('express') var app = express() // 类型1:app.use app.use(function(req, res, next){ console.log('请求进来了') // 不调用next()不会进入第二个中间件 next() }) // 配置404中间件 app.use(function(req, res){ console.log('第二个中间件,404了') res.render('404. html') }) app.listen(8888, function() { console.log('running') })
关心请求路径的中间件
-
只要是'/a'开头的路径就可以进去,不关心子路径
-
'/a/b'可
-
'/ab'不可
app.use('/a',function(req, res, next){ // http://localhost:8888/a/b 可进 // http://localhost:8888/a 可进 // http://localhost:8888/ab 不可 // http://localhost:8888 不可 console.log('/a路径请求进来了') })
2.路由级别中间件
- 严格匹配请求路径和方法的中间件
- 严格匹配, '/a'才能匹配到
- '/a/b' 不可
app.get('/a', function(req, res, next){ // http://localhost:8888/a/b 不可 // http://localhost:8888/a 可进 // http://localhost:8888/ab 不可 // http://localhost:8888 不可 console.log(1) }) app.post('/', function(req, res, next){ console.log(2) }) app.pur('/', function(req, res, next){ console.log(3) }) app.delete('/user', function(req, res, next){ console.log(4) })
3.错误处理中间件
- 全局错误处理,一般放在404处理之后
app.use(function(err, req, res, next){ console.error(err.stack) res.status(500).send('error') })
4.内置中间件
- express.static
- express.json
- express.urlencoded
5.第三方中间件
- body-parser
- compression
- cookie-parser
- morgan
- response-time
- serve-static
- session
配置中间件
- 中间件要挂载后才能使用
app.use(session({ secret: 'coco', resave: false, saveUninitialized: false }) app.use(router)
koa2,koa1和express区别
-
koa1: 依赖
co
库并采用generator
函数,在函数内使用yield
语句 -
koa2: 增加了箭头函数,移除了
co
依赖,使用 Promise, 因此可以结合async await
使用,es6 语法,执行时间比 koa1 更快 -
koa和express区别
- express是大而全,koa是小而精
- koa是原生不绑定任何中间件的裸框架,需要什么加什么,扩展性非常好,组装几个中间件就可以和express匹敌
- api对比
- koa模板引擎和路由方面没有express提供的api丰富,koa将req,res都挂载到了ctx上,通过ctx既可以访问到req,也可以访问到res
- 虽然koa比express少集成了很多功能,但对应功能只需要require中间件即可,反而更灵活
- 中间件加载和执行机制
- 中间件模式区别的核心是next的实现
- koa请求与响应是洋葱进出模型,使用最新async代码,没有回调函数,代码运行非常清晰。当koa处理中间件遇到await next()的时候会暂停当前中间件进而处理下一个中间件,最后再回过头来继续处理剩下的任务(逻辑就是回调函数),递归存在栈溢出的问题,可能会把js引擎卡死,koa采用了尾调用的方式进行了性能优化
- express是直线型,只进不出,express本身是不支持洋葱模型的数据流入流出能力的,需要引入其他的插件
- app.use 就是往中间件数组中塞入新的中间件
- express中间件的执行则依靠私有方法 app.handle 进行处理
- koa通过 compose() 这个方法,就能将我们传入的中间件数组转换并级联执行,最后 callback() 返回this.handleRequest()的执行结果。
- 编程体验
- express是回调函数
- koa2是基于新的语法特性async function,实现了promise链传递,错误处理更友好
- 各自优缺点
- express
- 优点:历史更久,文档更完整,资料更多,深入人心
- 缺点:callback hell,没有默认的错误处理方式
- koa
- 优点:没有callback,有默认的错误处理方式
- 缺点:路由,模板,jsonp等中间件都需要开发者自己配置(但其实更灵活)
- express
- express是大而全,koa是小而精