node采用的是CommonJS规范。每个文件就是一个单独的模块,拥有属于自身的独立做用域,变量以及方法等。这些对其余模块都是不可见的。CommonJS规范规定,每一个模块内部,module表明当前模块。module是一个对象,它有一个exports属性,也就是module.exports。该属性是对外的接口,把须要导出的内容放到该属性上。外部能够经过require进行导入。require导入的就是exports中的内容。node
该篇文章就手动实现如下require方法,经过手写的require方法拿到另外一个文件中的exports中的内容。json
首先,咱们先看一下node环境中标准的require方法是如何引用模块的。
新建文件夹,在文件夹中新建b.js。经过module.exports将内容导出。
b.js:浏览器
let str = 'b.js导出的内容'; module.exports = str;
而后新建另外一个文件,my-require.js。在my-require.js中引入b.js中的str。
my-require.js:安全
let str = require('./b.js'); console.log(str);
运行代码,能够看到。打印出了b的内容:b.js导出的内容。
以上是标准CommonJS中require的引用,接下来手动实现它:
首先梳理如下逻辑,require函数中传递的参数是一个路径,有路径再加上node的fs模块,咱们就能够读取到该文件。那有了该文件的内容,从该文件中获取exports就不是什么难事了。
上代码:app
let path = require('path'); let fs = require('fs'); let vm = require('vm'); /** 定义本身的require方法 myrequire() */ function myrequire(modulePath){ let absPath = path.resolve(__dirname,modulePath); function find(absPath){ try{ fs.accessSync(absPath); return absPath; }catch(e){ console.log(e); } } absPath = find(absPath); let module = new Module(absPath); loadModule(module); return module.exports; } function Module(id){ this.id = id; this.exports = {} } function loadModule(module){ let extension = path.extname(module.id); Module._extensions[extension](module); } Module._extensions = { '.js'(module){ let content = fs.readFileSync(module.id, 'utf8'); let fnStr = Module.wrapper[0]+content+Module.wrapper[1]; let fn = vm.runInThisContext(fnStr); fn.call(module.exports,module.exports,module,myrequire); } } Module.wrapper = [ '(function(exports,module,require,__dirname,__dirname){', '})' ]; let str = myrequire('./b.js'); console.log(str);
阅读顺序从上至下。首先 引入了path fs和vm模块。path和fs都不用说了,都懂。vm模块是node的核心模块。核心功能官方解释的是:ide
意思大体是:vm能够使用v8的Virtual Machine contexts动态地编译和执行代码,而代码的执行上下文是与当前进程隔离的,可是这里的隔离并非绝对的安全,不彻底等同浏览器的沙箱环境。
其实vm模块在该本文中的做用就是执行字符串代码,这样理解就好。函数
首先,定义了一个myrequire的方法。该方法传入一个相对路径。在myrequire方法中第一步将相对路径转换为绝对路径。而后又经过一个find方法来校验该路径是否存在。接下来经过构造函数Module传入绝对路径,new出了实例module。
该构造函数Module传入了路径id,内部定义了属性exports={}。该属性就是文件导出的属性。ui
紧接着,经过loadModule方法传入了实例module,来加载该文件。在loadModule方法中,首先获取了文件名后缀.js。 把文件名后缀.js传给Module._extensions。在Module._extensions对象中,经过文件后缀名.js找到该文件类型的解析方法。并把实例module传递进去。
在该方法中,经过module.id路径和fs模块经过获取到该文件内容content。注意下一步。在该文件内容content的外面用(function(exports,modules,require,__dirname,__filename){})函数包裹了一层。这样作的目的是待会要执行该函数而且拿到其中的module.exports中导出的内容。可是咱们刚才经过fs读取到的文件内容仅仅是字符串,又包裹了一层空函数,仍是字符串。
接下来就要用到vm模块。该模块能够执行字符串代码。经过vm.runInthisContext()方法,将刚才获得的字符串传递进去。此时就获得了能够执行的方法fn。
那接下来就是执行该方法fn了。执行fn,把刚才的参数传递进去。注意当前this执行为module.exports。这样才能拿到module.exports中的内容。
最后在myrequire中末尾,返回了该exports内容。return module.exports。
好,接下来就是验证效果了。右键code run,或者浏览器中打开。能够看到:this
b.js导出的内容
拿到了文件b.js中的内容,而且打印了出来。
好,如今以及实现了最简单了require。但是,咱们并不知足于此。由于该require方法还有一些问题。好比说,还不能引用json文件,并且也没有考虑若是文件没有后缀的状况。接下来继续完善myrequire方法:code
let path = require('path'); let fs = require('fs'); let vm = require('vm'); /** 定义本身的require方法 myrequire() */ function myrequire(modulePath){ let absPath = path.resolve(__dirname,modulePath); let ext_name = Object.keys(Module._extensions); let index = 0; let old_absPath = absPath; function find(absPath){ try{ fs.accessSync(absPath); return absPath; }catch(e){ let ext = ext_name[index++]; let newPath = old_absPath+ext; return find(newPath); } } absPath = find(absPath); let module = new Module(absPath); loadModule(module); return module.exports; } function Module(id){ this.id = id; this.exports = {} } function loadModule(module){ let extension = path.extname(module.id); Module._extensions[extension](module); } Module._extensions = { '.js'(module){ let content = fs.readFileSync(module.id, 'utf8'); let fnStr = Module.wrapper[0]+content+Module.wrapper[1]; let fn = vm.runInThisContext(fnStr); fn.call(module.exports,module.exports,module,myrequire); }, '.json'(module){ let content = fs.readFileSync(module.id, 'utf8'); module.exports = content; } } Module.wrapper = [ '(function(exports,module,require,__dirname,__dirname){', '})' ]; let str = myrequire('./b'); console.log(str); console.log(myrequire('./a'));
在myrequire方法的第二行,先获取到Module._extensions中的全部后缀(目前有.js和.json),又声明了一个下标index,最后有保存了该路径old_absPath。 在find方法中,若是用户没有写文件后缀,就会自动拼接后缀。循环去查找,直到找到或者到最后也没找到。
在Module._extensions中新增了一个对象.json的方法。该方法较为简单。经过fs读取到文件并把文件内容放到module.exports中。ok,看下效果吧:
b.js导出的内容 { "name":"要引入的内容" }
能够看到。正常拿到了b.js中的内容并且也读取到了a.json中的内容。至此,咱们就实现了CommonJS中的require方法。写文章不易,喜欢就点个👍吧 thx~