node采用的是CommonJS规范。每一个文件就是一个单独的模块,拥有属于自身的独立作用域,变量以及方法等。这些对其他模块都是不可见的。CommonJS规范规定,每个模块内部,module代表当前模块。module是一个对象,它有一个exports属性,也就是module.exports。该属性是对外的接口,把需要导出的内容放到该属性上。外部可以通过require进行导入。require导入的就是exports中的内容。该篇文章就手动实现以下require方法,通过手写的require方法拿到另一个文件中的exports中的内容。首先,我们先看一下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就不是什么难事了。上代码: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._extensionsextension;}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的核心模块。核心功能官方解释的是:The vm module provides APIs for compiling and running code within V8 Virtual Machine contexts. The vm module is not a security mechanism. Do not use it to run untrusted code. The term “sandbox” is used throughout these docs simply to refer to a separate context, and does not confer any security guarantees.意思大致是:vm可以使用v8的Virtual Machine contexts动态地编译和执行代码,而代码的执行上下文是与当前进程隔离的,但是这里的隔离并不是绝对的安全,不完全等同浏览器的沙箱环境。其实vm模块在该本文中的作用就是执行字符串代码,这样理解就好。首先,定义了一个myrequire的方法。该方法传入一个相对路径。在myrequire方法中第一步将相对路径转换为绝对路径。然后又通过一个find方法来校验该路径是否存在。接下来通过构造函数Module传入绝对路径,new出了实例module。该构造函数Module传入了路径id,内部定义了属性exports={}。该属性就是文件导出的属性。紧接着,通过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,或者浏览器中打开。可以看到:b.js导出的内容拿到了文件b.js中的内容,并且打印了出来。好,现在以及实现了最简单了require。可是,我们并不满足于此。因为该require方法还有一些问题。比如说,还不能引用json文件,而且也没有考虑如果文件没有后缀的情况。接下来继续完善myrequire方法: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._extensionsextension;}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~