# 模块机制
node采用模块化结构,按照CommonJS规范定义和使用模块,模块与文件是一一对应关系,即加载一个模块,实际上就是加载对应的一个模块文件
node 的基础中毫无疑问的应该是有关于模块机制的方面的,也即require这个内置功能的一些原理,关于模块互相引用的推荐先好好读读官方文档,在
这里就不给大家详细解释了
# 热更新
从面试官的角度看, 热更新 是很多程序常见的问题,对客户端而言,热更新意味着不用换包,当然也包含着md5校验/差异更新等。复杂问题:
对服务端而言,热更新意味着服务不用重启,这样可用性较高。
在node中做热更新代码,牵扯到的知识点可能主要是require会有一个cache,有这个cache在,即使你更新了 .js文件,在
代码中再次require还是会拿到之前的编译好缓存在v8内存(code space)中的旧代码,但是如果只是淡出的清除掉require中的cache,再次
require确实能拿到新的代码,但是这时候很容易碰到各地维持旧的引用依旧跑的旧的代码的问题。
不过热更新json之类的配置文件的话,还是可以简单的实现,更新require的cache就行,不会
有持有旧引用的问题,但是旧的引用一直被持有很容易出现内从泄露,而要热更新配置的话,为什么不存数据库?或者使用zookeeper之类
的服务?通过更新文件还要在发布一次,但是存数据库直接写个接口配个页面多爽
# 上下文
对于node.js而言,正常情况下只有一个上下文,甚至于内置的很多方面例如 require的实现只是在启动的时候运行了内置的函数
每个单独的.js文件并不意味着单独的上下文,在某个.js文件中污染了全局的作用域一样能影响到其它的地方
而目前的node.js将VM的接口暴露了出来,可以让你自己创建一个新的js上下文,这一点上跟前端还是区别挺大的,在执行外部代码的
时候,通过创建新的上下文沙盒(sandbox)可以避免上下文被污染
'use strict';
const vm = require('vm');
let code =
`(function(require) {
const http = require('http');
http.createServer( (request, response) => {
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end('Hello World\\n');
}).listen(8124);
console.log('Server running at http://127.0.0.1:8124/');
})`;
vm.runInThisContext(code)(require);
问题:既然可以通过新的上下文来避免污染,那么为什么 node.js不给每一个.js文件以独立的上下文来避免作用域被污染
问题其实有点下套,其实Node有给每个js文件独立的上下文,但是这避免不了全局的作用域污染,实际上这是为了功能的妥协。
Node.js 模块正常情况对作用域不会造成污染,意外创建全局变量是一种例外,可以采用严格模式来避免
# 包管理
------------------------------------------------------
1. a.js和b.js两个文件互相require是否会死循环?双方是否能导出变量?如何从设计上避免这种问题?
答:不会,先执行的导出空对象,通过导出工厂函数让对方从函数去拿比较好避免。
模块在导出的只是 var module = { exports: {}};中的exports,以从a.js启动为例,a.js还没执行完exports就是{} 在b.js的开头拿到的就是 {} 而已。
另外还有非常基础和常见的问题,比如:module.exports和exports的区别这里也能一并解决了,exports只是module.exports的一个引用
2. 如果a.js require了b.js,那么在b中定义的全局变量t=11能否在a中直接打印出来?
答:每个.js能独立一个环境只是因为node帮你在外层包了一圈自执行,所以你使用 t=11 定义全局变量在其它地方当然能拿到,情况如下:
//b.js
(function (exports,require,module,__filename,__dirname){
t=11;
})();
//a.js
(function (exports,require,nodule,__filename,__dirname){
console.log(t);//11
})();
3. 如何在不重启node进程的情况下热更新一个js/json文件?这个问题本身是否有问题?
答:可以清除掉require的缓存 重新require(),视具体情况还可以用VM模块重新执行。当然这个问题可能是典型的X-Y Problem,使用js实现热更新很容易碰到v8优化之后各地拿到缓存的引用导致热更新js没意义,当然热更新json还是可以简单一点比如用读取文件的方式来热更新,但是这样也不如从redis之类的数据库中读取比较合理
4. 比较 AMD, CMD, CommonJS 三者的区别?
AMD,CMD,CommonJS是目前最常用的三种模块化书写规范。
commonjs是用在服务器端的,同步的,如nodejs
amd, cmd是用在浏览器端的,异步的,如requirejs和seajs
其中,amd先提出,cmd是根据commonjs和amd基础上提出的。
根据CommonJS规范,一个单独的文件就是一个模块。加载模块使用require方法,该方法读取一个文件并执行,最后返回文件内部的exports对象。 CommonJS 加载模块是同步的,所以只有加载完成才能执行后面的操作。像Node.js主要用于服务器的编程,加载的模块文件一般都已经存在本地硬盘,所以加载起来比较快,不用考虑异步加载的方式,所以CommonJS规范比较适用。但如果是浏览器环境,要从服务器加载模块,这是就必须采用异步模式。所以就有了 AMD CMD 解决方案。
AMD 是 RequireJS 在推广过程中对模块定义的规范化产出
AMD异步加载模块。它的模块支持对象 函数 构造器 字符串 JSON等各种类型的模块。
适用AMD规范适用define方法定义模块。
CMD是SeaJS 在推广过程中对模块定义的规范化产出
CMD和AMD的区别有以下几点:
1.对于依赖的模块AMD是提前执行,CMD是延迟执行。不过RequireJS从2.0开始,也改成可以延迟执行(根据写法不同,处理方式不通过)。
2.CMD推崇依赖就近,AMD推崇依赖前置。
3.AMD的api默认是一个当多个用,CMD严格的区分推崇职责单一。例如:AMD里require分全局的和局部的。CMD里面没有全局的 require,提供 seajs.use()来实现模块系统的加载启动。CMD里每个API都简单纯粹。
5. 关于 node 中 require 的实现原理等
答:require原生入口代码里面调用了__load方法用于加载文件,继续看__load方法原生代码里面调用了_resolveFilename方法,顾名思义,这应该是一个解析需要require的文件名的方法,继续看_resolveFilename方法中又调用了_findPath方法。
可以看到,这里完整的显示了node是如何根据require传入的名称来定位具体的文件的,他们的顺序依次是:
1、先从缓存中读取,如果没有则继续往下
2、判断需要模块路径是否以/结尾,如果不是,则要判断
a. 检查是否是一个文件,如果是,则转换为真实路径
b. 否则如果是一个目录,则调用tryPackage方法读取该目录下的package.json文件,把里面的main属性设置为filename
c. 如果没有读到路径上的文件,则通过tryExtensions尝试在该路径后依次加上.js,.json和.node后缀,判断是否存在,若存在则返回加上后缀后的路径
3、如果依然不存在,则同样调用tryPackage方法读取该目录下的package.json文件,把里面的main属性设置为filename
4、如果依然不存在,则尝试在该路径后依次加上index.js,index.json和index.node,判断是否存在,若存在则返回拼接后的路径。
5、若解析成功,则把解析得到的文件名cache起来,下次require就不用再次解析了,否则若解析失败,则返回false
------------------------
# Promise
Promise是异步编程的一种解决方案,比传统的解决方案--回调函数和事件--更合理和更前大
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果,从语法上说,Promise是一个对象,从他可以获取异步操作的消息,Promise提供统一的API,各种异步操作都可以用同样的方法进行处理
有了Promise对象,就可以将异步操作以同步操作的方式表达出来,避免层层嵌套的回调函数
Promise对象的两个特点:
1.对象的状态不受外界影响
Promise对象代表一个异步操作,有三种状态:pending(进行中),fulfilled(已成功),rejected(已失败),只有异步操作的结果,可以决定当前是哪一种状态,任何其它操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其它手段无法改变
2.一旦状态改变,就不会再变,任何时候都可以得到这个结果
resolved(已定型)
promise对象的状态改变只有两种可能:从pending变为fulfilled和从pending变为rejected,只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时称为已定型
缺点:
首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。
其次,如果不设置回调函数,promise内部抛出的错误,不会反应到外部
当处于pending状态时,无法得知目前进展到哪一个阶段
# Events(事件)
# Timers(定时器)
# 阻塞/异步
# 并行/并发
1. Promise 中 .then的第二个参数与.catch 有什么区别?
没什么多大的区别,全都是用来处理错误函数的
详情可参考:http://es6.ruanyifeng.com/#docs/promise
2. Eventemitter的emit是同步还是异步
Node.js 中 Eventemitter 的 emit 是同步的
按监听器的注册顺序,同步地调用每个注册到名为 eventName 事件的监听器,并传入提供的参数。
如果事件有监听器,则返回 true ,否则返回 false。
3. 如何判断接口是否异步?是否只要有回调函数就是异步?
异步:发出指令,然后去做别的事情了,所有操作完成后再执行回调
异步I/O:异步的发出I/O请求
看文档
console.log 打印看看
看是否有 IO 操作
单纯使用回调函数并不会异步, IO 操作才可能会异步, 除此之外还有使用 setTimeout 等方式实现异步.
4. nextTick,setTimeout以及setImmediate三者的区别?
5. 如何实现一个sleep函数?
function sleep(ms) {
var start = Date.now(), expire = start + ms;
while (Date.now() < expire) ;
return;
}
6. 如何实现一个异步的reduce?(在Es5中出现的数组方法)
需要了解 reduce 的情况, 是第 n 个与 n+1 的结果异步处理完之后, 在用新的结果与第 n+2 个元素继续依次异步下去
# 进程
## Process(进程)
## Child Processes(子进程)
## Cluster(集群)
## 进程间通信
## 守护进程
1. 进程的当前工作目录是什么?有什么作用?
2. child_process.fork与POSIX的fork有什么区别?
cluster是常见的node利用多核的方法,它是基于child_process.fork()实现的,所以cluster产生的进程之间是通过IPC来通信的,并且它也没有拷贝父进程的空间,而是通过加入cluster.isMaster这个标识,来区分父进程以及子进程,达到类似POSIX的fork效果
3. 父进程或子进程的死亡是否回影响对方?什么是孤儿进程?
子进程的死亡不会影响父进程,不过子进程死亡时(线程组的最后一个线程,通常是‘领头线程死亡时’)会向他的父进程发送死亡信号。反之父进程死亡,一般情况下子进程也会随之死亡,但如果此时子进程处于可运状态,僵死状态等等的话,子进程将被进程1(init进程)收养,从而成为孤儿进程。
子进程死亡的时候(处于‘终止状态’),父进程没有及时调用wait()或waitpid()来返回死亡进程的相关信息,此时子进程还有一个PCB残留在进程表中,被称为僵尸进程。
4. cluster是如何保证负载均衡的?
5. 什么是守护进程?如何实现守护进程?
守护进程,是服务端方面一个很基础的概念。很多人只知道通过pm2之类的工具可以将进程以守护进程的方式启动,却不了解什么是守护进程,为什么要使用守护进程
普通的进程,在用户退出终端之后就会直接关闭,通过&启动到后台的进程,之后会由于会话(session组)被回收而终止进程,守护进程是不依赖终端(tty)的进程,不会因为用户退出终端而停止运行的进程
# IO
# Buffer
# String Decoder(字符串解码)
# Stream (流)
# Console (控制台)
# File System (文件系统)
# Readline
# REPL
1.Buffer一般处理什么数据?其长度能否动态变化?
Buffer是node.js中用于处理二进制数据的类,其中与IO相关的操作(网络/文件等)均基于Buffer,Buffer类的实例非常类似整数数组
但其大小是固定不变的,并且其内存在V8堆栈外分配原始内存空间。Buffer类的实例创建之后,其所占用的内存大小不能再进行调整
2.Stream的highWaterMark与drain事件是什么?二者之间的关系是?
3.Stream的pipe的作用是?在pipe的过程中数据是引用传递还是拷贝传递?
将一个可写流附到可读流上,同时将可写切换到流模式,并把所有数据推给可写流,在pipe传递数据的过程中,objectMode是传递引用,非objectMode则是拷贝一份数据传递下去
4.什么是文件描述符?输入流,输出流,错误流是什么?
5.console.log是同步还是异步?如何实现一个console.log?
console.log正常情况下是异步的,除非你使用new Console(stdout[,stderr])指定了一个文件的目的地
let print = (str) => process.stdout.write(str + '\n')
print('hello world')
6.如何同步的获取用户的输入?
node中,获取用户的输入其实就是读取node进程中的输入流的数据
而要同步读取,则是不用异步的read接口,而是用同步的readSync接口去读取stdin的数据可实现
7.Readline是如何实现的?
readline模块提供了一个用于从Readble的stream中一次读取一行的接口,当然你也可以用于读取文件或net,http,stream.
实现上,readline在读取TTY的数据时,是通过input.on('keypress','onkeypress')时发现用户按下了回车键来判断是新的line的,而读取一般的stream时,则是通过缓存数据然后用正则,test来判断是否为new line
8.REPL
Read--Eval--Print--Loop(交互式解释器)
repl模块提供了一种‘读取--求值-输出-循环的实现’它可以作为一个独立的程序或嵌入到其它应用中
9.Cluster模块
node默认单线程进行,对于32位系统最高可以使用512MB内存,对于64位最高可以使用1GB内存。对于多核CPU的计算机来说,这样做效率很低,因为只有一个核在运行,其它核都在闲置。cluster模块就是为解决这个问题而提出的
Cluster模块允许建立一个主进程和若干个worker进程,由主进程监控和协调worker进程的运行,worker之间采用进程间通信交换信息,cluster模块内置一个负载均衡器,采用Round-robin算法协调各个worker进程之间的负载,运行时,所有新建立的链接都有主进程完成,然后主进程再把TCP连接分配给指定的worker进程
10. Events模块
回调函数模式让node可以处理异步操作,但是,为了适应回调函数,异步操作只能有两个状态:开始和结束。对于那些多状态的异步操作,回调函数就会无法处理,你不得不将异步操作拆开,分成多个阶段,每个阶段结束时,调用下一个回调函数。
为了解决这个问题,node提供Event Emitter接口。通过事件,解决多状态异步操作的响应问题
11. child_process模块
用于新建子进程,子进程的运行结果储存在系统缓存之中(最大200KB),等到子进程运行结束后,主进程在调用回调函数读取子进程的运行结果
#小题型
1. 什么是error-first回调模式?
错误优先处理回调函数,应用error-first回调模式是为了更好的进行错误和数据的传递,第一个参数保留给一个错误error对象,一旦出现错误,错误将通过第一个参数error返回。其余的参数将用作数据的传递。
2. 如何避免回调地狱?
模块化设计:将回调函数拆分成几个独立的函数
使用流程控制库,比如async
组合使用generators和Promises
使用async/await函数
3. 什么是Promises?
在 then 后没有 catch ,没有捕捉异常。这样做会造成故障沉默,不会抛出异常。
如果你调试一个巨大的代码库,并且比不知道哪个 Promise 函数有潜在的问题, 你可以使用
unhandledRejection 这个工具。它将会打印出所有未处理的 reject 状态的 Promise。
4. 什么工具统一团队的代码风格?为什么统一的代码风格很重要?
ESLint和Standard可以用来统一代码
因为团队协作时,代码风格一致,团队成员可以更轻松的构建项目,可以通过静态分析排除代码问题
5. 什么时候用npm?什么时候应当用yarn?
6. 什么是桩代码(stub)?请描述一下应用场景?
桩代码就是在某些组件或模块中,模拟某些功能的代码。桩代码的作用是占位,让代码在测试过程中顺利进行,测试时,stub可以为函数调用返回模拟的结果,比如,当我们写文件时,实际上并不需要真正去写
7. 什么是测试金字塔?请举例说明?
测试金字塔反映了单元测试,集成测试,端到端测试在测试中占的比例
8. 你最欣赏的HTTP框架是什么?为什么?
9. 如何保证你的cookie安全?如何阻止XSS攻击?
XSS攻击是指攻击者向html页面里面插入恶意JavaScript代码
HttpOnly 这个属性帮助防止跨站脚本攻击,它禁止通过JavaScript访问cookie
为了防止攻击,可以对HTTP header里的set-cookie进行处理set-cookie:sid=;HttpOnly.
如果使用express框架,可以使用express-cookie session,它会默认做出上述防止XSS攻击的设置
10. 如何确认项目的相关依赖安全?
自动的更新你的依赖: npm outdated
NSP
11. 什么时候应该在后台进程中使用消息服务,怎样处理工作想成的任务/怎么给worker安排任务?
消息队列适用于后台数据传输服务,比如发送邮件和数据图像处理
消息队列有很多解决方案,比如RabbitMQQ
12. 这段代码有什么问题?
new Promise((resolve,reject) => {
throw new Error('error')
})
.then(console.log)
then之后没有catch。这样的话,错误会被忽略,会造成故障沉默
后边跟上 .catch(console.error)
调试一个大型项目时,可以使用监控unhandleRejection事件来捕获所有未处理的Promise错误
process.on('unhandledRejection',(err) => {
console.log(err)
})
13. 这段代码输出什么?
Promise.resolve(1)
.then((x) => x + 1)
.then((x) => { throw new Error('My Error') })
.catch(() => 1)
.then((x) => x + 1)
.then((x) => console.log(x))
.catch(console.error)
答案是2,逐行解释如下:
1.创建promise,resolve的值为1
2.x 的值为1,加1 返回2
3.x 的值为2,但是没有用到,抛出一个错误
4.捕获错误,但是没有处理,返回1
5.x 的值为1,加1 返回2
6.x 的值为2,打印为2
7.不会执行,因为没有错误抛出
14.为什么要用node?
简单强大,轻量可扩展
简单体现在node使用JavaScript,json来进行编码,人人都会
强大体现在非阻塞IO,可以适应分块传输数据,较慢的网络环境,尤其擅长高并发访问。
轻量体现在node本身既是代码,又是服务器,前后端使用统一语言。
可扩展体现在可以轻松应对多实例,多服务架构,同时有海量的第三方应用组件
node的优点:I/O密集型处理是node的强项,因为node的I/O请求都是异步的
node的缺点:不擅长CPU密集型的操作
CPU密集型操作(复杂的运算,图片的操作)
15. node的架构是什么样子的?
主要分为三层,应用app >> v8及node内置架构 >> 操作系统
v8是node运行的环境,可以理解为node虚拟机
node内置架构又可分为三层:
核心模块(javascript实现) >> c++绑定 >> libuv + CAes + http
16. node有哪些核心模块?
EventEmitter,Stream,FS,Net和全局对象
17. node在企业中常用到?
静态资源服务器,代码本地构建,单元测试,UI测试
工作中应该注意:精确版本号,测试,使用debug,保持代码精简
多请教,保持独立思考,使用现有的库,保持简单
良好的文档,配置文件,使用pm2
libuv:提供异步功能的C库。它在运行是负责一个事件循环(Event loop),一个线程池,文件系统I/O,DNS相关和网络I/O,以及其它一些功能
18. node异常处理
node是单线程运行环境,一旦抛出的异常没有被捕获就会引起整个进程的崩溃。
使用throw语句抛出一个错误对象,即抛出异常
将错误对象传递给回调函数,由回调函数负责发出错误
通过EventEmitter接口,发出一个error事件
try...catch结构无法捕获异步运行的代码抛出的错误
19. node关于高并发问题?
原理:非阻塞事件驱动实现异步开发,通过事件驱动的I/O来操作完成跨平台数据密集型实时应用
传统的server每个请求生成一个线程,node是一个单线程的,使用libuv保持数万并发
libuv原理:
c语言编写的基础库实现主循环,文件,网络即可
libuv的改进:
回传上下文信息
其它线程不能访问缺省主循环,loop不支持多线程
数据库高并发实现
var proxy = new EventProxy();
var status = "ready";
var select = function(callback){
proxy.once("selected",callback);
if(status == "ready"){
status = "pending";
db.select("SQL", function(results){
proxy.emit("selected",results);
status = "ready";
});
}
20. 什么是异步?
异步就是发出操作指令(把回调函数加入异步队列),然后就可以去做别的事情去了,所有操作完成后再执行回调
异步I/O就是异步的发出I/O请求
虽然node可以异步发出I/O请求,但nodejs不支持多线程,为啥就可以支持高并发呢?
因为nodejs的I/O操作,底层是开启了多线程的
当同时有多个I/O请求时,主线程会创建多个eio线程,以提高IO请求的处理速度
虽然nodeJS的IO操作开启了多线程,但是所有线程都是基于主线程开启的只能跑在一个进程当中还是不能充分利用CPU资源
pm2进程管理器可以解决这个问题,pm2是一个带有负载均衡功能的node应用的进程管理器
cluster模块
21. 代码可读性维护改进
async:async.waterfall([getcatalog,getaticle,getTigle])
promise的方法
koa写法
es6写法使用yield
22.优化问题
前端优化问题:移除iscorll,合并请求,tcp优化,HTTP优化,localstorate,html5离线缓存
api优化:restfulapi,标准输入输出
ui优化:使用同一的框架,前端组件化
异常处理:log监控,避免大文件处理,retry处理