这个笔记是基于《深入浅出nodeJs》的,这本书出版较早是基于v0.6.0版本的,而现在node已经更新到v10的版本了,所以很多东西可能在新的版本都已经不适用了,但这本书偏理论居多,这些思想应该不会变的,所以do it吧。
node诞生于2009年基于谷歌V8引擎,它并不是一个框架,而是一个JavaScript运行环境。
在node中绝大部分的操作都是以异步方式进行调用的,如网络请求、文件写入/读取等。
node是基于事件驱动的。
JavaScript是单线程的,单线程有一些弱点:
node采用了child_process子线程来解决这些问题,类似于浏览器端的web worker。
node与底层操作系统之间有一层libuv,libuv在操作系统与node上层模块之间构建了一层平台架构,得益于这层架构node可以轻松实现跨平台。
从单线程的角度来讲,node处理I/O的能力是非常强的,I/O密集的优势主要在于node利用事件循环的能力,而不是启动每一个线程为每一个请求服务,资源暂用较少。
首先V8执行JavaScript的效率是非常高的。由于JavaScript是单线程,如果有长时间运行的计算将会导致CPU时间片不能释放,使得后续I/O无法发起。但是适当调整和分解大型运算任务为多个小任务,使得运算能适时释放,不阻塞I/O调用的发起,这样既可以享受并行异步I/O的好处,又能充分利用CPU。
当前普遍环境下,工具类应用普遍较为广泛,如:webpack、babel、grunt等。
CommonJS的美好愿景:希望JavaScript能运行在任何地方。
早期JavaScript主要有几个大问题:
CommonJS的出现,致力于让JavaScript能够编写以下应用:
CommonJS规范已经涵盖了模块、二进制、Buffer、字符集编码、I/O流、进程环境、文件系统、套接字、单元测试、web服务器网关接口、包管理等。
主要分为模块引用、模块定义、模块标识3个部分。
1、模块引用
调用require()方法来引入一个模块。如:
const fs = require("fs");
2、模块定义
对于引入的模块,上下文提供了exports对象用于导出当前模块的方法或变量,并且是唯一导出的出口。在模块中,存在一个module对象代表模块自身,而exports是module的属性。在node中,一个文件就是一个模块,将方法或属性挂载在exports对象上作为属性即可定义导出的方式。
exports.sayHello = function(){
console.log("hello world");
};
3、模块标识
模块标识就是传递给require()的参数,必须是符合小驼峰命名的字符串,或者以..、.开头的相对路径,或者绝对路径。可以不包含文件名后缀.js。
在node中引入模块需要经历3个步骤:
在node中模块分为2类:一类是node提供的模块,称为核心模块;另一类是用户编写的模块,称为文件模块。
核心模块编译进了二进制执行文件,在node进程启动时,部分核心模块就被直接加载在内存中,所以这部分核心模块引入时,文件定位和编译执行这2个步骤可以省略,并且在路径分析中优先判断,所以加载速度是最快的。
文件模块在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢。
node对引入过的模块都会进行缓存,以减少二次引入时的开销。不同于前端的文件缓存,node缓存的是编译和执行之后的对象。
无论是核心模块还是文件模块,对相同模块的二次加载一律采用缓存优先的方式,这是第一优先级,不同之处在于核心模块的缓存检查会先于为文件模块的缓存检查。
标识符有几种形式,对于不同的标识符,模块的查找和定位有不同程度上的差异。
1、模块标识符分析
模块标识符在node中有这么几类:
1) 核心模块
核心模块的优先级仅次于缓存加载,在node源代码编译过程中已经编译为二进制代码,其加载过程最快。
试图加载一个与核心模块相同标识符的自定义模块是不会成功的。
2)路径形式的文件模块
以.或..开始的标识符都会当作文件模块来处理。分析文件模块时,require()方法会将路径转为真实路径,并以真实路径作为索引,将编译后的结果存放到缓存中,以使二次加载更快。
3)自定义模块
首先自定义模块是非核心模块,也不是路径形式的标识符。它是一种特殊的文件模块,可能是一个文件或包的形式,这类模块的查找是最慢的。
模块路径是node定位文件的制定的查找策略,表现为一个路径组成的数组。
windows下:
exports.sayPath = function(){
console.log(module.paths);
};
//打印结果
[
'D:\\myProject\\node-project\\server\\node_modules',
'D:\\myProject\\node-project\\node_modules',
'D:\\myProject\\node_modules',
'D:\\node_modules'
];
可以看出模块路径生成规则如下:
文件路径越深,模块查找耗时越久,所以自定义模块的查找是最慢的。
2、文件定位
缓存加载的优化策略,使得二次加载不需要进行路径分析、文件定位和编译执行的过程,大大提高了再次加载模块时的效率。
文件的定位主要包括文件扩展名的分析、目录和包处理。
1)文件扩展名分析
标识符是可以不追加后缀名的,这种情况下,node会按.js、.json、.node的次序补足扩展名,依次尝试。
尝试的过程需要调用fs模块同步阻塞式的判断文件是否存在,这会导致略微的性能问题,所以对于.json、.node文件最好带上扩展名。
2)目录分析和包
分析标识符的过程中,可能没有找到对应的文件,但却得到一个目录,此时node会将这个目录当中包处理。
首先node会查找目录下的package.json文件,通过JSON.parse()解析包描述对象,从中取出main属性执行的文件名进行定位。如果文件名缺少扩展名,则进入扩展名分析的步骤。
如果main指定的文件名错误或压根没有package.json,node会将index当中默认文件名,依次查找index.js、index.json、index.node。
如果在目录分析的过程中没有定位到文任何文件,则自定义模块会进入下一个模块路径进行查找,如果路径数组都遍历完依然没有找到目标文件,则抛出查找失败的异常。
以下提到的模块编译都是文件模块。
在node中,每个文件模块都是一个对象。编译和执行是引入文件模块的最后一个阶段。定位到具体的文件后,node会新建一个模块对象,然后根据路径载入并编译。对于不同的文件扩展名,载入的方法也有不同。
每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache对象上,以提高二次引入的性能。
通过require.extensions可以知道系统中已有的扩展加载方式,如:
console.log(require.extensions);
//打印
{ '.js': [Function], '.json': [Function], '.node': [Function] }
1、JavaScript模块的编译
CommonJS模块规范中,每个模块文件中存在着require、exports、module这3个变量,同时每个模块还有__filename、__dirname这2个变量,模块中没有定义又是从何而来的呢?
这些变量不是全局定义的,实际上,在编译的过程中,node会对获取到的JavaScript文件内容进行头尾包装,所以一个正常的JavaScript文件被包裹后的样子:
(function(exports,require,module,__filename,__dirname){
exports.sayHello = function(){
console.log("hello world");
}
});
这样每个文件之间都做了作用域隔离。包装之后的代码会通过vm原生模块的runInThisContext()方法执行(类似eval),返回一个具体的function对象。最后,将当前模块对象的exports属性、require()方法、module以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function()执行。
执行之后,模块的exports属性被返回给了调用方,exports属性上的任何方法和属性都可以被外部调用。
exports对象是通过形参的形式传入的,直接赋值形参会改变形参的引用,所以以下写法是错误的:
exports = function(){
};
这样写则是正确的:
module.exports = function(){
};
2、C/C++模块的编译
node调用process.dlopen()方法进行加载和执行。node架构下,dlopen方法在windows和*nix平台下有不同的实现,通过liuv兼容层进行了封装。
事实上,.node文件并不需要编译,这些文件是编写C/C++模块之后编译产生的,所以这里只有加载和执行的结果。执行过程中,exports对象与.node模块产生联系,然后返回给调用者。
C/C++模块的运行效率更高,但编写门槛也比较高。
3、JSON文件的编译
node利用fs模块同步读取JSON文件的内容之后,调用JSON.parse()方法得到对象,然后将其赋值给模块对象的exports,以供外部引用。
JSON文件通常用作项目的配置文件,对于JSON文件的读取直接调用require()方法即可。
node核心模块在编译成可执行文件的过程中被编译进了二进制文件。核心模块包括:C/C++编写的和JavaScript编写的两部分。
编译所有C/C++文件之前,编译程序需要将所有的JavaScript模块文件编译成C/C++代码,但并没有将其直接编译成可执行代码。
1、转存为C/C++代码
node采用一些工具,将所有内置JavaScript代码转换成C++里的数组,这个过程中,JavaScript代码以字符串的形式存储在node命名空间中,是不可执行的。
启动node进程时,JavaScript代码直接加载到内存。在加载的过程中,JavaScript核心模块经历标识符分析后直接定位到内存中。
2、编译JavaScript核心模块
在引入核心模块的过程中,也经历了头尾包装的过程,然后才执行和导出了exports对象。与文件模块不同的是,核心模块是从内存中加载的。
核心模块编译成功的模块会缓存到NativeModule._cache上,文件模块则缓存到Module.__cache对象上。
核心模块中,有些模块全部由C/C++编写,有些模块由C/C++完成核心功能其它部分由JavaScript实现包装和对外导出。
静态语言的性能强于脚本语言,脚本语言的开发速度要优于静态语言。
整个过程:
对于用户而言,直接调用require()即可。
CommonJS的包规范主要由2部分组成:包结构、包描述。
完全符合CommonJS规范的的包目录应该包含以下这些文件:
CommonJS为package.json定义了如下一些必要的字段:
包规范的定义可以帮助node解决依赖包安装的问题,npm正是基于该规范进行了实现。
通常一些npm包还包含了author、bin、main、scripts、
有很多模块是可以实现前后端共用的,但实际情况,前后端环境是略有差异的。
CommonJS规范并不适合于前端,所以AMD规范最终在前端应用场景中胜出。
AMD规范是CommonJS规范的一个延伸。定义如下:
define(id?,dependencies?,factory);
模块id和依赖是可选的,factory内容就是实际代码的内容。
define(function(){
const exports = {};
exports.sayHello = function(){
console.log("hello world");
};
return exports;
});
AMD模块需要用define来明确定义一个模块,CommonJS则是隐式包装的,二者的目的都是为了进行作用域隔离。AMD规范的内容需要通过返回的方式实现导出。
与AMD规范的主要区别在于定义模块和依赖的引入部分。AMD需要在声明模块的时候指定所有依赖,并通过形参传递依赖到模块内容中:(Angular1.x和AMD规范很像)
define(["dep1","dep2"],function(dep1,dep2){
const exports = {};
exports.sayHello = function(){
console.log("hello world");
};
return exports;
});
CMD模块更接近于node对CommonJS规范的定义,CMD支持动态引入:
define(function(require,exports,module){
});
require、exports、module通过形参传递给模块。
为了让一个模块可以运行在前后端,在写作中需要考虑环境问题。为了保持前后端的一致性,需要将代码包裹在一个闭包内:
(function(name,definition){
const hasDefine = typeof define === "function";
const hasExports = typeof module !== "undefined" && module.exports;
if(hasDefine){
//AMD或CMD规范
define(definition);
}else if(hasExports){
//node模块
module.exports = definition();
}else{
//挂在在window变量下
this[name] = definition();
}
})("hello",function(){
return function(){
console.log("hello world");
}
});
CommonJS提出的规范十分简单,但现实意义却十分强大。
node通过模块规范,组织了自身的原生模块,弥补了JavaScript弱结构性的问题,形成了稳定的结构,并向外提供服务。
npm通过对包规范的支持,有效组织了第三方模块,这使得项目开发中的依赖问题得到很好的解决。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。