网站建设顾问,上海网络科技有限公司有哪些,自己建设网站需要具备哪些条件,有没有专门做设计的网站了解Node.js
Node.js是一个基于ChromeV8引擎的JavaScript运行环境#xff0c;使用了一个事件驱动、非阻塞式I/O模型#xff0c;让JavaScript 运行在服务端的开发平台#xff0c;它让JavaScript成为与PHP、Python、Perl、Ruby等服务端语言平起平坐的脚本语言。Node中增添了很…了解Node.js
Node.js是一个基于ChromeV8引擎的JavaScript运行环境使用了一个事件驱动、非阻塞式I/O模型让JavaScript 运行在服务端的开发平台它让JavaScript成为与PHP、Python、Perl、Ruby等服务端语言平起平坐的脚本语言。Node中增添了很多内置的模块提供各种各样的功能同时也提供许多第三方模块。
模块的问题
为什么要有模块
复杂的前端项目需要做分层处理按照功能、业务、组件拆分成模块 模块化的项目至少有以下优点
便于单元测试便于同事间协作抽离公共方法, 开发快捷按需加载, 性能优秀高内聚低耦合防止变量冲突方便代码项目维护
几种模块化规范
CMD(SeaJS 实现了 CMD)AMD(RequireJS 实现了 AMD)UMD(同时支持 AMD 和 CMD)IIFE 自执行函数CommonJS Node 采用了 CommonJSES Module 规范 (JS 官方的模块化方案)
Node中的模块
Node中采用了 CommonJS 规范
实现原理
Node中会读取文件拿到内容实现模块化 Require方法 同步引用
tipsNode中任何js文件都是一个模块每一个文件都是模块
Node中模块类型
内置模块属于核心模块无需安装在项目中不需要相对路径引用 Node自身提供。文件模块程序员自己书写的js文件模块。第三方模块 需要安装 安装之后不用加路径。
Node中内置模块
fs filesystem
操作文件都需要用到这个模块
const path require(path); // 处理路径
const fs require(fs); // file system
// // 同步读取
let content fs.readFileSync(path.resolve(__dirname, test.js), utf8);
console.log(content);let exists fs.existsSync(path.resolve(__dirname, test1.js));
console.log(exists);
path 路径处理
const path require(path); // 处理路径// join / resolve 用的时候可以混用console.log(path.join(a, b, c, .., /))// 根据已经有的路径来解析绝对路径 可以用他来解析配置文件
console.log(path.resolve(a, b, /)); // resolve 不支持/ 会解析成根路径console.log(path.join(__dirname, a))
console.log(path.extname(1.js))
console.log(path.dirname(__dirname)); // 解析父目录
vm 运行代码
字符串如何能变成 JS 执行呢
1.eval
eval中的代码执行时的作用域为当前作用域。它可以访问到函数中的局部变量。
let test global scope
global.test1 123
function b(){test fn scopeeval(console.log(test)); //local scopenew Function(console.log(test1))() // 123new Function(console.log(test))() //global scope
}
b()
2.new Function
new Function()创建函数时不是引用当前的词法环境而是引用全局环境,Function中的表达式使用的变量要么是传入的参数要么是全局的值
Function可以获取全局变量所以它还是可能会有变量污染的情况出现
function getFn() {let value testlet fn new Function(console.log(value))return fn
}getFn()()global.a 100 // 挂在到全局对象global上
new Function(console.log(a))() // 100
3.vm
前面两种方式我们一直强调一个概念那就是变量的污染
VM的特点就是不受环境的影响也可以说他就是一个沙箱环境
在Node中全局变量是在多个模块下共享的所以尽量不要在global中定义属性
所以vm.runInThisContext可以访问到global上的全局变量但是访问不到自定义的变量。而vm.runInNewContext访问不到global也访问不到自定义变量他存在于一个全新的执行上下文
const vm require(vm)
global.a 1
// vm.runInThisContext(console.log(a))
vm.runInThisContext(a 100) // 沙箱独立的环境
console.log(a) // 1
vm.runInNewContext(console.log(a))
console.log(a) // a is not defined
Node模块化的实现
node中是自带模块化机制的每个文件就是一个单独的模块并且它遵循的是CommonJS规范也就是使用require的方式导入模块通过module.export的方式导出模块。
node模块的运行机制也很简单其实就是在每一个模块外层包裹了一层函数有了函数的包裹就可以实现代码间的作用域隔离。
我们先在一个js文件中直接打印arguments得到的结果如下图所示我们先记住这些参数。
console.log(arguments) // exports, require, module, __filename, __dirname Node中通过modules.export 导出require 引入。其中require依赖node中的fs模块来加载模块文件通过fs.readFile读取到的是一个字符串。
在javascrpt中可以通过eval或者new Function的方式来将一个字符串转换成js代码来运行。但是前面提到过他们都有一个致命的问题就是变量的污染。
实现require模块加载器
首先导入依赖的模块pathfs,vm, 并且创建一个Require函数这个函数接收一个modulePath参数表示要导入的文件路径
const path require(path);
const fs require(fs);
const vm require(vm);
// 定义导入类参数为模块路径
function Require(modulePath) {...
}
在Require中获取到模块的绝对路径使用fs加载模块这里读取模块内容使用new Module来抽象使用tryModuleLoad来加载模块内容Module和tryModuleLoad稍后实现Require的返回值应该是模块的内容也就是module.exports。
// 定义导入类参数为模块路径
function Require(modulePath) {// 获取当前要加载的绝对路径let absPathname path.resolve(__dirname, modulePath);// 创建模块新建Module实例const module new Module(absPathname);// 加载当前模块tryModuleLoad(module);// 返回exports对象return module.exports;
}
Module的实现就是给模块创建一个exports对象tryModuleLoad执行的时候将内容加入到exports中id就是模块的绝对路径。
// 定义模块, 添加文件id标识和exports属性
function Module(id) {this.id id;// 读取到的文件内容会放在exports中this.exports {};
}
node模块是运行在一个函数中这里给Module挂载静态属性wrapper里面定义一下这个函数的字符串wrapper是一个数组数组的第一个元素就是函数的参数部分其中有exportsmoduleRequire__dirname__filename, 都是模块中常用的全局变量.
第二个参数就是函数的结束部分。两部分都是字符串使用的时候将他们包裹在模块的字符串外部就可以了。
// 定义包裹模块内容的函数
Module.wrapper [(function(exports, module, Require, __dirname, __filename) {,})
]
_extensions用于针对不同的模块扩展名使用不同的加载方式比如JSON和javascript加载方式肯定是不同的。JSON使用JSON.parse来运行。
javascript使用vm.runInThisContext来运行可以看到fs.readFileSync传入的是module.id也就是Module定义时候id存储的是模块的绝对路径读取到的content是一个字符串使用Module.wrapper来包裹一下就相当于在这个模块外部又包裹了一个函数也就实现了私有作用域。
使用call来执行fn函数第一个参数改变运行的this传入module.exports后面的参数就是函数外面包裹参数exports, module, Require, __dirname, __filename。/
// 定义扩展名不同的扩展名加载方式不同实现js和json
Module._extensions {.js(module) {const content fs.readFileSync(module.id, utf8);const fnStr Module.wrapper[0] content Module.wrapper[1];const fn vm.runInThisContext(fnStr);fn.call(module.exports, module.exports, module, Require,__filename,__dirname);},.json(module) {const json fs.readFileSync(module.id, utf8);module.exports JSON.parse(json); // 把文件的结果放在exports属性上}
}
tryModuleLoad函数接收的是模块对象通过path.extname来获取模块的后缀名然后使用Module._extensions来加载模块。
// 定义模块加载方法
function tryModuleLoad(module) {// 获取扩展名const extension path.extname(module.id);// 通过后缀加载当前模块Module._extensions[extension](module); // 策略模式
}
到此Require加载机制基本就写完了。Require加载模块的时候传入模块名称在Require方法中使用path.resolve(__dirname, modulePath)获取到文件的绝对路径。然后通过new Module实例化的方式创建module对象将模块的绝对路径存储在module的id属性中在module中创建exports属性为一个json对象。
使用tryModuleLoad方法去加载模块tryModuleLoad中使用path.extname获取到文件的扩展名然后根据扩展名来执行对应的模块加载机制。
最终将加载到的模块挂载module.exports中。tryModuleLoad执行完毕之后module.exports已经存在了直接返回就可以了。
接下来我们给模块添加缓存。就是文件加载的时候将文件放入缓存中再去加载模块时先看缓存中是否存在如果存在直接使用如果不存在再去重新加载加载之后再放入缓存。
// 定义导入类参数为模块路径
function Require(modulePath) {// 获取当前要加载的绝对路径let absPathname path.resolve(__dirname, modulePath);// 从缓存中读取如果存在直接返回结果if (Module._cache[absPathname]) {return Module._cache[absPathname].exports;}// 创建模块新建Module实例const module new Module(absPathname);// 添加缓存Module._cache[absPathname] module;// 加载当前模块tryModuleLoad(module);// 返回exports对象return module.exports;
}
增加功能省略模块后缀名。
自动给模块添加后缀名实现省略后缀名加载模块其实也就是如果文件没有后缀名的时候遍历一下所有的后缀名看一下文件是否存在。
// 定义导入类参数为模块路径
function Require(modulePath) {// 获取当前要加载的绝对路径let absPathname path.resolve(__dirname, modulePath);// 获取所有后缀名const extNames Object.keys(Module._extensions);let index 0;// 存储原始文件路径const oldPath absPathname;function findExt(absPathname) {if (index extNames.length) {return throw new Error(文件不存在);}try {fs.accessSync(absPathname);return absPathname;} catch(e) {const ext extNames[index];findExt(oldPath ext);}}// 递归追加后缀名判断文件是否存在absPathname findExt(absPathname);// 从缓存中读取如果存在直接返回结果if (Module._cache[absPathname]) {return Module._cache[absPathname].exports;}// 创建模块新建Module实例const module new Module(absPathname);// 添加缓存Module._cache[absPathname] module;// 加载当前模块tryModuleLoad(module);// 返回exports对象return module.exports;
}
源代码调试
我们可以通过VSCode 调试Node.js
步骤
创建文件a.js
module.exports abc
1.文件test.js
let r require(./a)console.log(r)
1.配置debug本质是配置.vscode/launch.json文件而这个文件的本质是能提供多个启动命令入口选择。
一些常见参数如下
program控制启动文件的路径即入口文件name下拉菜单中显示的名称该命令对应的入口名称request分为 launch启动和 attach附加进程已经启动skipFiles指定单步调试跳过的代码runtimeExecutable设置运行时可执行文件默认是 node可以设置成 nodemonts-nodenpm 等
修改launch.jsonskipFiles指定单步调试跳过的代码 将test.js 文件中的require方法所在行前面打断点执行调试进入源码相关入口方法
梳理代码步骤
1.首先进入到进入到require方法Module.prototype.require 2.调试到Module._load 方法中该方法返回module.exportsModule._resolveFilename方法返回处理之后的文件地址将文件改为绝对地址同时如果文件没有后缀就加上文件后缀。 3.这里定义了Module类。id为文件名。此类中定义了exports属性 4.接着调试到module.load 方法该方法中使用了策略模式Module._extensions[extension](this, filename)根据传入的文件后缀名不同调用不同的方法 5.进入到该方法中看到了核心代码读取传入的文件地址参数拿到该文件中的字符串内容执行module._compile 6.此方法中执行wrapSafe方法。将字符串前后添加函数前后缀并用Node中的vm模块中的runInthisContext方法执行字符串便直接执行到了传入文件中的console.log代码行内容。 至此整个Node中实现require方法的整个流程代码已经调试完毕通过对源代码的调试可以帮助我们学习其实现思路代码风格及规范有助于帮助我们实现工具库提升我们的代码思路同时我们知道相关原理也对我们解决日常开发工作中遇到的问题提供帮助。 作者京东物流 乔盼盼 来源京东云开发者社区 自猿其说Tech 转载请注明来源