网站免费下载app,注册公司地址虚拟地址怎么申请,重庆巫山网站设计公司,厦门淘宝网站设计公司前面的话 Node在实现中并非完全按照CommonJS规范实现#xff0c;而是对模块规范进行了一定的取舍#xff0c;同时也增加了少许自身需要的特性。本文将详细介绍NodeJS的模块实现 引入 nodejs是区别于javascript的#xff0c;在javascript中的顶层对象是window#xff0c;而在…前面的话 Node在实现中并非完全按照CommonJS规范实现而是对模块规范进行了一定的取舍同时也增加了少许自身需要的特性。本文将详细介绍NodeJS的模块实现 引入 nodejs是区别于javascript的在javascript中的顶层对象是window而在node中的顶层对象是global [注意]实际上javascript也存在global对象只是其并不对外访问而使用window对象指向global对象而已 在javascript中通过var a 100是可以通过window.a来得到100的 但在nodejs中是不能通过global.a来访问得到的是undefined 这是因为var a 100;这个语句中的变量a只是模块范围内的变量a而不是global对象下的a 在nodejs中一个文件就是一个模块每个模块都有自己的作用域。使用var来声明的一个变量它并不是全局的而是属于当前模块下 如果要在全局作用域下声明变量则如下所示 概述 Node中模块分为两类一类是Node提供的模块称为核心模块另一类是用户编写的模块称为文件模块 核心模块部分在Node源代码的编译过程中编译进了二进制执行文件。在Node进程启动时部分核心模块就被直接加载进内存中所以这部分核心模块引入时文件定位和编译执行这两个步骤可以省略掉并且在路径分析中优先判断所以它的加载速度是最快的 文件模块则是在运行时动态加载需要完整的路径分析、文件定位、编译执行过程速度比核心模块慢 接下来我们展开详细的模块加载过程 模块加载 在javascript中加载模块使用script标签即可而在nodejs中如何在一个模块中加载另一个模块呢 使用require()方法来引入 【缓存加载】 再展开介绍require()方法的标识符分析之前需要知道与前端浏览器会缓存静态脚本文件以提高性能一样Node对引入过的模块都会进行缓存以减少二次引入时的开销。不同的地方在于浏览器仅仅缓存文件而Node缓存的是编译和执行之后的对象 不论是核心模块还是文件模块require()方法对相同模块的二次加载都一律采用缓存优先的方式这是第一优先级的。不同之处在于核心模块的缓存检查先于文件模块的缓存检查 【标识符分析】 require()方法接受一个标识符作为参数。在Node实现中正是基于这样一个标识符进行模块查找的。模块标识符在Node中主要分为以下几类[1]核心模块如http、fs、path等[2].或..开始的相对路径文件模块[3]以/开始的绝对路径文件模块[4]非路径形式的文件模块如自定义的connect模块 根据参数的不同格式require命令去不同路径寻找模块文件 1、如果参数字符串以“/”开头则表示加载的是一个位于绝对路径的模块文件。比如require(/home/marco/foo.js)将加载/home/marco/foo.js 2、如果参数字符串以“./”开头则表示加载的是一个位于相对路径跟当前执行脚本的位置相比的模块文件。比如require(./circle)将加载当前脚本同一目录的circle.js 3、如果参数字符串不以“./“或”/“开头则表示加载的是一个默认提供的核心模块位于Node的系统安装目录中或者一个位于各级node_modules目录的已安装模块全局安装或局部安装 [注意]如果是当前路径下的文件模块一定要以./开头否则nodejs会试图去加载核心模块或node_modules内的模块 //a.js
console.log(aaa);//b.js
require(./a);//aaa
require(a);//报错 【文件扩展名分析】 require()在分析标识符的过程中会出现标识符中不包含文件扩展名的情况。CommonJS模块规范也允许在标识符中不包含文件扩展名这种情况下Node会先查找是否存在没有后缀的该文件如果没有再按.js、.json、.node的次序补足扩展名依次尝试 在尝试的过程中需要调用fs模块同步阻塞式地判断文件是否存在。因为Node是单线程的所以这里是一个会引起性能问题的地方。小诀窍是如果是.node和.json文件在传递给require()的标识符中带上扩展名会加快一点速度。另一个诀窍是同步配合缓存可以大幅度缓解Node单线程中阻塞式调用的缺陷 【目录分析和包】 在分析标识符的过程中require()通过分析文件扩展名之后可能没有查找到对应文件但却得到一个目录这在引入自定义模块和逐个模块路径进行查找时经常会出现此时Node会将目录当做一个包来处理 在这个过程中Node对CommonJS包规范进行了一定程度的支持。首先Node在当前目录下查找package.json(CommonJS包规范定义的包描述文件)通过JSON.parse()解析出包描述对象从中取出main属性指定的文件名进行定位。如果文件名缺少扩展名将会进入扩展名分析的步骤 而如果main属性指定的文件名错误或者压根没有package.json文件Node会将index当做默认文件名然后依次查找index.js、index.json、index.node 如果在目录分析的过程中没有定位成功任何文件则自定义模块进入下一个模块路径进行查找。如果模块路径数组都被遍历完毕依然没有查找到目标文件则会抛出查找失败的异常 访问变量 如何在一个模块中访问另外一个模块中定义的变量呢 【global】 最容易想到的方法把一个模块定义的变量复制到全局环境global中然后另一个模块访问全局环境即可 //a.js
var a 100;
global.a a;//b.js
require(./a);
console.log(global.a);//100 这种方法虽然简单但由于会污染全局环境不推荐使用 【module】 而常用的方法是使用nodejs提供的模块对象Module该对象保存了当前模块相关的一些信息 function Module(id, parent) {this.id id;this.exports {};this.parent parent;if (parent parent.children) {parent.children.push(this);}this.filename null;this.loaded false;this.children [];
} module.id 模块的识别符通常是带有绝对路径的模块文件名。
module.filename 模块的文件名带有绝对路径。
module.loaded 返回一个布尔值表示模块是否已经完成加载。
module.parent 返回一个对象表示调用该模块的模块。
module.children 返回一个数组表示该模块要用到的其他模块。
module.exports 表示模块对外输出的值。 【exports】 module.exports属性表示当前模块对外输出的接口其他文件加载该模块实际上就是读取module.exports变量 //a.js
var a 100;
module.exports.a a;//b.js
var result require(./a);
console.log(result);//{ a: 100 } 为了方便Node为每个模块提供一个exports变量指向module.exports。造成的结果是在对外输出模块接口时可以向exports对象添加方法 console.log(module.exports exports);//true [注意]不能直接将exports变量指向一个值因为这样等于切断了exports与module.exports的联系 模块编译 编译和执行是模块实现的最后一个阶段。定位到具体的文件后Node会新建一个模块对象然后根据路径载入并编译。对于不同的文件扩展名其载入方法也有所不同具体如下所示 js文件——通过fs模块同步读取文件后编译执行 node文件——这是用C/C编写的扩展文件通过dlopen()方法加载最后编译生成的文件 json文件——通过fs模块同步读取文件后用JSON.parse()解析返回结果 其余扩展名文件——它们都被当做.js文件载入 每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache对象上以提高二次引入的性能 根据不同的文件扩展名Node会调用不同的读取方式如.json文件的调用如下 // Native extension for .json
Module._extensions[.json] function(module, filename) {var content NativeModule.require(fs).readFileSync(filename, utf8); try {module.exports JSON.parse(stripBOM(content));} catch (err) {err.message filename : err.message;throw err;}
}; 其中Module._extensions会被赋值给require()的extensions属性所以通过在代码中访问require.extensions可以知道系统中已有的扩展加载方式。编写如下代码测试一下 console.log(require.extensions); 得到的执行结果如下 { .js: [Function], .json: [Function], .node: [Function] } 在确定文件的扩展名之后Node将调用具体的编译方式来将文件执行后返回给调用者 【JavaScript模块的编译】 回到CommonJS模块规范我们知道每个模块文件中存在着require、exports、module这3个变量但是它们在模块文件中并没有定义那么从何而来呢甚至在Node的API文档中我们知道每个模块中还有filename、dirname这两个变量的存在它们又是从何而来的呢如果我们把直接定义模块的过程放诸在浏览器端会存在污染全局变量的情况 事实上在编译的过程中Node对获取的JavaScript文件内容进行了头尾包装。在头部添加了(function(exports, require, module, filename, dirname) {\n在尾部添加了\n}); 一个正常的JavaScript文件会被包装成如下的样子 (function (exports, require, module, filename, dirname) {var math require(math);exports.area function (radius) {return Math.PI * radius * radius;};
}); 这样每个模块文件之间都进行了作用域隔离。包装之后的代码会通过vm原生模块的runInThisContext()方法执行(类似eval只是具有明确上下文不污染全局)返回一个具体的function对象。最后将当前模块对象的exports属性、require()方法、module(模块对象自身)以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function()执行 这就是这些变量并没有定义在每个模块文件中却存在的原因。在执行之后模块的exports属性被返回给了调用方。exports属性上的任何方法和属性都可以被外部调用到但是模块中的其余变量或属性则不可直接被调用 至此require、exports、module的流程已经完整这就是Node对CommonJS模块规范的实现 【C/C模块的编译】 Node调用process.dlopen()方法进行加载和执行。在Node的架构下dlopen()方法在Windows和*nix平台下分别有不同的实现通过libuv兼容层进行了封装 实际上.node的模块文件并不需要编译因为它是编写C/C模块之后编译生成的所以这里只有加载和执行的过程。在执行的过程中模块的exports对象与.node模块产生联系然后返回给调用者 C/C模块给Node使用者带来的优势主要是执行效率方面的劣势则是C/C模块的编写门槛比JavaScript高 【JSON文件的编译】 .json文件的编译是3种编译方式中最简单的。Node利用fs模块同步读取JSON文件的内容之后调用JSON.parse()方法得到对象然后将它赋给模块对象的exports以供外部调用 JSON文件在用作项目的配置文件时比较有用。如果你定义了一个JSON文件作为配置那就不必调用fs模块去异步读取和解析直接调用require()引入即可。此外你还可以享受到模块缓存的便利并且二次引入时也没有性能影响 CommonJS 在介绍完Node的模块实现之后回过头来再学习下CommonJS规范相对容易理解 CommonJS规范的提出主要是为了弥补当前javascript没有标准的缺陷使其具备开发大型应用的基础能力而不是停留在小脚本程序的阶段 CommonJS对模块的定义十分简单主要分为模块引用、模块定义和模块标识3个部分 【模块引用】 var math require(math); 在CommonJS规范中存在require()方法这个方法接受模块标识以此引入一个模块的API到当前上下文中 【模块定义】 在模块中上下文提供require()方法来引入外部模块。对应引入的功能上下文提供了exports对象用于导出当前模块的方法或者变量并且它是唯一导出的出口。在模块中还存在一个module对象它代表模块自身而exports是module的属性。在Node中一个文件就是一个模块将方法挂载在exports对象上作为属性即可定义导出的方式 // math.js
exports.add function () {var sum 0, i 0,args arguments, l args.length;while (i l) {sum args[i];}return sum;
}; 在另一个文件中我们通过require()方法引入模块后就能调用定义的属性或方法了 // program.js
var math require(math);
exports.increment function (val) {return math.add(val, 1);
}; 【模块标识】 模块标识其实就是传递给require()方法的参数它必须是符合小驼峰命名的字符串或者以.、..开头的相对路径或者绝对路径。它可以没有文件名后缀.js 模块的定义十分简单接口也十分简洁。它的意义在于将类聚的方法和变量等限定在私有的作用域中同时支持引入和导出功能以顺畅地连接上下游依赖。每个模块具有独立的空间它们互不干扰在引用时也显得干净利落