node的应用是模块组成的,Node遵循commonjs的模块规范,用来隔离每一个模块的做用域,使每个模块在自身的命名空间中执行。node
commonjs的主要内容:json
模块必须经过module.exports导出对外的变量或接口,经过require()来导入其余模块的输出到当前模块做用域中。数组
commonjs模块特色:浏览器
一、全部代码运行在当前模块做用域中,不会污染全局做用域。缓存
二、模块同步加载,根据代码的顺序加载。函数
三、模块能够屡次加载,只会在第一次加载时运行一次,而后运行结果会被缓存,之后再加载,就直接从缓存中读取结果,如若想要模块再次运行,必须清除缓存。性能
咱们先来 看一下简单的例子ui
编写一个demo-exports.jsspa
let name = 'saucxs'; let getName = function (name) { console.log(name) }; module.exports = { name: name, getName: getName }
咱们再来编写一个demo-require.js3d
let person = require('./demo-export') console.log(person, '-------------') // { name: 'saucxs', getName: [Function: getName] } console.log(person.name, '===========') // saucxs person.getName('gmw'); // gmw person.name = 'updateName' console.log(person, '22222222') // { name: 'updateName', getName: [Function: getName] } console.log(person.name, '3333333') // updateName person.getName('gmw') // gmw
node demp-require.js,结果如上所示。
commonjs规范,每个文件就是一个模块,每个模块中都有一个module对象,这个对象就指向当前模块。module对象具备以下属性:
(1)id:当前模块id。
(2)exports:表示当前模块暴露给外部的值。
(3)parent:是一个对象,表示调用当前模块的模块。
(4)children:是一个对象,表示当前模块调用的模块。
(5)filename:模块的绝对路径
(6)paths:从当前模块开始查找node_modules目录,而后依次进入到父目录,查找父目录下的node_modules目录;依次迭代,直到根目录下的node_modules目录。
(7)loaded:一个布尔值,表示当前模块是否被彻底加载。
咱们来看一下栗子
module.js
module.exports = { name: 'saucxs', getName: function (name) { console.log(name) } } console.log(module)
node module.js
咱们知道了module对象有一个exports属性,该属性用来对外暴露变量,方法或者整个模块。当其余文件须要require该模块的时候,实际上读取的是module对象中的exports属性。
既然都有了module.exports就能知足全部的需求,为啥还有一个exports对象呢?
咱们如今来看一下二者的关系:
(1)exports对象和module.exports都是引用类型变量,指向同一个内存地址,在node中,二者一开始都是指向一个空对象的。
exports = module.exports = {}
(2)其次,exports对象是经过形参的方式传入,直接赋值给形参的引用,可是并不能改变做用域外的值。
var module = { exports: {} }; var exports = module.exports; function change(exports) { /*为形参exports添加属性name,会同步到外部的module.exports对象*/ exports.name = 'saucxs' /*在这里修改wxports的引用,并不会影响到module.exports*/ exports = { age: 18 } console.log(exports) // {age: 18} } change(exports); console.log(module.exports); // {exports: {name: 'saucxs'}}
分析上述代码:
直接给exports赋值,会改变当前模块内部的形参exports的对象应用。说明当前的exports对象已经跟外部的module.exports对象没有任何关系,因此改变exports对象不会影响到module.exports。
注意:module.exports就是为了解决上述exports直接赋值的问题,会致使抛出不成功的问题而产生的。
//这些操做都是合法的 exports.name = 'saucxs'; exports.getName = function(){ console.log('saucxs') }; //至关于下面的方式 module.exports = { name: 'saucxs', getName: function(){ console.log('saucxs') } } //或者更常规的写法 let name = 'saucxs'; let getName = function(){ console.log('saucxs') } module.exports = { name: name, getName: getName }
这样就能够不用每次都把要抛出对象或者方法直接赋值给exports属性,直接采用对象字面量的方式更加方便。
require是模块的引入规则,经过exports或者module.exports抛出一个模块,经过require方法传入模块标识符,而后node根据必定的规则引入该模块,咱们就可使用模块中定义的方法和属性。
(1)路径分析
(2)文件定位
(3)编译执行
(1)node提供的模块,例如http模块,fs模块等,称为核心模块。核心模块在node源代码编译过程当中就有编译了二进制文件,在node进程启动的时候,部分核心模块就直接加载进内存中,所以这部分模块是不用经历上述的(2),(3)步骤,并且在路径分析中优先判断,所以加载速度是最快的。
(2)用户本身编写的模块,称为文件模块。文件模块是须要按需加载的,须要经历上述的三个步骤,速度较慢。
浏览器会缓存静态脚本文件以提升页面性能同样,Node对引入过的模块也会进行缓存。与浏览器不一样的是:Node缓存的是编译执行以后的对象而不是静态文件。咱们举个例子看一下
requireA.js
console.log('模块requireA开始加载...') exports = function() { console.log('Hi') } console.log('模块requireA加载完毕') init.js var mod1 = require('./requireA') var mod2 = require('./requireA') console.log(mod1 === mod2)
执行node init.js
虽然咱们两次引入requireA这个模块,可是模块中的代码其实只执行了一遍。而且mod1和mod2指向了同一个模块。
Module._load = function(request, parent, isMain) { // 计算绝对路径 var filename = Module._resolveFilename(request, parent); // 第一步:若是有缓存,取出缓存 var cachedModule = Module._cache[filename]; if (cachedModule) { return cachedModule.exports; // 第二步:是否为内置模块 if (NativeModule.exists(filename)) { return NativeModule.require(filename); } // 第三步:生成模块实例,存入缓存 var module = new Module(filename, parent); Module._cache[filename] = module; // 第四步:加载模块 try { module.load(filename); hadException = false; } finally { if (hadException) { delete Module._cache[filename]; } } // 第五步:输出模块的exports属性 return module.exports; };
对应的流程以下:
模块标识符分析:
(1)核心模块,如http,fs模块。
(2)以 . 或者 ../ 开始的相对路径文件模块。
(3)以 / 开始的绝对路径模块。
(4)非路径形式的文件模块。
分析:
(1)核心模块:优先级仅次于缓存,加载速度最快。若是自定义模块和核心模块名称相同,加载会失败。若想成功,必须修改自定义模块的名称或者换个路径。
(2)路径形式的文件模块:以 . 或者 .. 或者 / 开始的标识符,都会被当作文件模块来处理。加载过程当中,require方法将路径转换为真实的路径,加载速度仅次于核心模块。
(3)非路径形式的自定义模块:这是一种特殊的文件模块,多是一个文件或者包的形式。查找这类模块的策略相似于js做用域链,node会逐个尝试模块路径中的路径,知道找到目标文件为止。
注意:这是node定位文件模块的具体文件的时候的查找策略,具体表现为一个路径的组成的数组。
能够在REPL环境中输出Module对象,查看其path属性的方式查看上述数组,文章开始的paths数组:
(1)文件拓展名分析
require()分析的标识符能够不包含扩展名,node会按.js、.node、.json的次序补足扩展名,依次尝试
(2)目标分析和包
若是在扩展名分析的步骤中,查找不到文件而是查找到相应目录,此时node会将目录当作包来处理,进行下一步分析查找当前目录下package.json中的main属性指定的文件名,若查找不成功则依次查找index.js,index.node,index.json。
若是目录分析的过程当中没有定位到任何文件,则自定义模块会进入下一个模块路径继续查找,直到全部的模块路径都遍历完毕,依然没找到则抛出查找失败的异常。
(3)参考源码
在Module._load方法的内部调用了Module._findPath这个方法,这个方法是用来返回模块的绝对路径的,源码以下:
Module._findPath = function(request, paths) { // 列出全部可能的后缀名:.js,.json, .node var exts = Object.keys(Module._extensions); // 若是是绝对路径,就再也不搜索 if (request.charAt(0) === '/') { paths = ['']; } // 是否有后缀的目录斜杠 var trailingSlash = (request.slice(-1) === '/'); // 第一步:若是当前路径已在缓存中,就直接返回缓存 var cacheKey = JSON.stringify({request: request, paths: paths}); if (Module._pathCache[cacheKey]) { return Module._pathCache[cacheKey]; } // 第二步:依次遍历全部路径 for (var i = 0, PL = paths.length; i < PL; i++) { var basePath = path.resolve(paths[i], request); var filename; if (!trailingSlash) { // 第三步:是否存在该模块文件 filename = tryFile(basePath); if (!filename && !trailingSlash) { // 第四步:该模块文件加上后缀名,是否存在 filename = tryExtensions(basePath, exts); } } // 第五步:目录中是否存在 package.json if (!filename) { filename = tryPackage(basePath, exts); } if (!filename) { // 第六步:是否存在目录名 + index + 后缀名 filename = tryExtensions(path.resolve(basePath, 'index'), exts); } // 第七步:将找到的文件路径存入返回缓存,而后返回 if (filename) { Module._pathCache[cacheKey] = filename; return filename; } } // 第八步:没有找到文件,返回false return false; };
根据上述的模块引入机制咱们知道,当咱们第一次引入一个模块的时候,require的缓存机制会将咱们引入的模块加入到内存中,以提高二次加载的性能。可是,若是咱们修改了被引入模块的代码以后,当再次引入该模块的时候,就会发现那并非咱们最新的代码,这是一个麻烦的事情。如何解决呢?
require(): 加载外部模块 require.resolve():将模块名解析到一个绝对路径 require.main:指向主模块 require.cache:指向全部缓存的模块 require.extensions:根据文件的后缀名,调用不一样的执行函数
解决办法:
//删除指定模块的缓存 delete require.cache[require.resolve('/*被缓存的模块名称*/')] // 删除全部模块的缓存 Object.keys(require.cache).forEach(function(key) { delete require.cache[key]; })
而后咱们再从新require进来须要的模块就能够了。