一、简述
一个js文件就是一个模块
会自动把写的代码块套一层闭包
浏览器不兼容CommonJS的根本原因,在于缺少四个Node.js环境的变量.(module,export,require,global)
既然没有,我们就手写一个吧,这里先普及下备用的基础知识,请往下看
二、源码中用到的备用知识
fs.accessSync方法
判断文件是否存在,不存在则报错
let fs = require("fs"); fs.accessSync("./x.js") //当前目录中没有该文件则会报错

path.extname方法
判断文件的扩展名
let path = require("path"); console.log(path.extname("./c.js"))
vm.runInThisContext()方法
会创建一个独立的沙箱环境,以执行对参数code的编译,运行并返回结果。vm.runInThisContext()方法运行的代码没有权限访问本地作用域,但是可以访问Global全局对象。
var vm = require('vm'); var localVar = 'initial value'; //在runInThisContext创建的沙箱环境中执行 var vmResult = vm.runInThisContext('localVar = "vm";'); console.log('vmResult: ', vmResult); //vmResult: vm console.log('localVar: ', localVar); //localVar: initial value //在eval中执行 var evalResult = eval('localVar = "eval";'); console.log('evalResult: ', evalResult); //evalResult: eval console.log('localVar: ', localVar); //localVar: eval
在上面示例中,分在vm.runInThisContext()方法创建的沙箱环境中和eval()中执行了一段JavaScript代码。 vm.runInThisContext创建的沙箱环境无法访问本地作用域,因此localVar没有被改变。而eval可以访问本地作用域,因此localVar被改变。
有了这些基础知识的储备,已经迫不及待的开始手写源代码了
三、模块化实现
我们想的是构建一个module实例,类似这样{ loaded: false,filename: 绝对路径, exports: 引入模块的结果}
loaded检测如果require多次,会进行缓存,
filename我们需要把输入的路径变成绝对路径
export存放我们加载模块后的结果,至于怎么实现,我们会对应load方法就行实现
最后把export返回,就实现了加载模块
有了这么点想法,我们就一步一步开始实现吧
1、准备工作做好,声明构造函数Module
let path = require('path'); let fs = require('fs'); let vm = require('vm'); // 声明构造函数Module function Module(filename){ this.loaded = false; //用于检测是否被缓存过 this.filename = filename; //文件的绝对路径 this.exports = {} //模块对应的导出结果 } //存放模块的扩展名 Module._extensions = ['.js','.json']; //检测是否有缓存 Module._cache = {}; //拼凑成闭包的数组 Module.wrapper = ['(function(exports,require,module){','\r\n})'];
已经把准备声明的变量和模块声明好,没有使用的先不用理会,下面用到的时候就明白了
2、实现一个require方法,实现加载模块
function req(path) { //自己实现require方法,实现加载模块 // 根据输入的路径 变出一个绝对路径 let filename = Module._resolveFilename(path); // 通过这个文件名创建一个模块 let module = new Module(filename); module.load(); //让这个模块进行加载 根据不同的后缀加载不同的内容 return module.exports }
3、有了这个架构想法,先实现 Module._resolveFilename方法
//如果没写扩展名,我们给它默认添加扩展名 Module._resolveFilename = function (p) { p = path.join(__dirname,p); if(!/\.\w+$/.test(p)){ //如果没写扩展名,尝试添加扩展名 for(let i=0;i<Module._extensions.length;i++){ let filePath = p + Module._extensions[i];//拼接出一个路径 // 判断文件是否存在 try{ fs.accessSync(filePath); return filePath; }catch (e) { throw new Error('module not found') } } }else { return p } }
4、此时的filename是存在这个文件的绝对路径,接下来对不同后缀名加载不同的内容
Module.prototype.load = function () { // 加载模块本身 js按照js加载 json按照json加载 let extname = path.extname(this.filename); //判断后缀名 Module._extensions[extname](this); //把module实例传过去 }
5、先实现加载.json后缀的方法,直接文件读取,并赋值给module.export
Module._extensions['.json'] = function (module) { let content = fs.readFileSync(module.filename,'utf8'); module.exports = JSON.parse(content); };
写了这么多,我们先来测试下.json结尾的文件,有没有被写入到module.exports中
6、实现以.js结尾的后缀名的方法
// 后缀名为js的加载方法 Module._extensions['.js'] = function (module) { //取出加载模块的内容 let content = fs.readFileSync(module.filename,'utf8'); // 形成闭包 let script = Module.wrap(content); //让js代码执行 let fn = vm.runInThisContext(script); fn.call(module,module.exports,req,module) };
7、我们来看下Module.wrap(content)如何进行代码解析的
Module.wrap = function(content){ return Module.wrapper[0] + content +Module.wrapper[1]; };
是不是很简单,只是简单的js字符串拼接成闭包的函数,然后让其进行执行。到目前为止,基本的功能已经实现了,我们先来测试下。但是我们还没实现缓存,接下来继续
8、最后我们在处理下缓存,就完美结束啦
//检测是否有缓存 Module._cache = {}; //改造下req方法 function req(path) { //自己实现require方法,实现加载模块 // 根据输入的路径 变出一个绝对路径 let filename = Module._resolveFilename(path); if(Module._cache[filename]){ //有缓存直接取缓存中的 return Module._cache[filename].exports; } // 通过这个文件名创建一个模块 let module = new Module(filename); module.load(); //让这个模块进行加载 根据不同的后缀加载不同的内容 Module._cache[filename] = module; //第一次加载先缓存 return module.exports }
这样一来就完美实现了,共不到50行代码,开不开心,愉不愉快。最后贴上源码供大家参考,别忘记喜欢哦
9、源码贴上(仅供参考)
let path = require('path'); let fs = require('fs'); let vm = require('vm'); // 声明构造函数Module function Module(filename){ this.loaded = false; //用于检测是否被缓存过 this.filename = filename; //文件的绝对路径 this.exports = {} //模块对应的导出结果 } //存放模块的扩展名 Module._extensions = ['.js','.json']; //检测是否有缓存 Module._cache = {}; //拼凑成闭包的数组 Module.wrapper = ['(function(exports,require,module){','\r\n})']; //如果没写扩展名,我们给它默认添加扩展名 Module._resolveFilename = function (p) { p = path.join(__dirname,p); if(!/\.\w+$/.test(p)){ //如果没写扩展名,尝试添加扩展名 for(let i=0;i<Module._extensions.length;i++){ let filePath = p + Module._extensions[i];//拼接出一个路径 // 判断文件是否存在 try{ fs.accessSync(filePath); return filePath; }catch (e) { throw new Error('module not found') } } }else { return p } } Module.wrap = function(content){ return Module.wrapper[0] + content +Module.wrapper[1]; }; // 加载模块本身 Module.prototype.load = function () { let extname = path.extname(this.filename); // js按照js加载,json按照json加载 Module._extensions[extname](this); }; // 后缀名为json的加载方法 Module._extensions['.json'] = function (module) { let content = fs.readFileSync(module.filename,'utf8'); module.exports = JSON.parse(content); }; // 后缀名为js的加载方法 Module._extensions['.js'] = function (module) { let content = fs.readFileSync(module.filename,'utf8'); // 形成闭包 let script = Module.wrap(content); let fn = vm.runInThisContext(script); fn.call(module,module.exports,req,module) }; function req(path) { //自己实现require方法,实现加载模块 // 根据输入的路径 变出一个绝对路径 let filename = Module._resolveFilename(path); if(Module._cache[filename]){ return Module._cache[filename].exports; } // 通过这个文件名创建一个模块 let module = new Module(filename); module.load(); //让这个模块进行加载 根据不同的后缀加载不同的内容 Module._cache[filename] = module; return module.exports } let str = req('./b'); console.log(str);
作者:言sir
链接:https://juejin.im/post/5b308972f265da595534f00a
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。