相信大家平时写代码都使用过require,那么今天我们简单的写写这个原理。
首先先了解下前端有几种模块分别是干什么的:前端模块规范有三种:CommonJs,AMD和CMD。
2.AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。
3.CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。
4.AMD:提前执行(异步加载:依赖先执行)+延迟执行
5.CMD:延迟执行(运行到需加载,根据顺序执行)
先看看 commonjs 规范
let str = 'hello world';
module.exports = str;
开始使用第一个包,这个里面要注意的一点就是如果是我们自己写的文件模块要写路径
let str = require('./str.js');
console.log(str);
好了,自己的模块现在就写出来了,那么我们现在看看这个里面都发生了些什么。
如果是第一次加载,首先是Module._load模块加载,Module._resolveFilename 模块解析文件名解析出一个绝对路径出来使用tryModuleLoad()尝试加载模块,先看有没有后缀名,没有后缀名则加上后缀名去目录底下寻找这个文件,找到了就开始读取文件内容执行文件代码(本质上就是创建一个匿名函数创建一个沙箱,沙箱里面执行代码并返回结果当模块被加载的时候),并创建这个模块没找到或者代码异常不合法则会抛出错误,并把这个模块加入到Module._cache模块进行缓存,多次require只会走一次这个这个读取流程,如果不是第一次加载那么会读Module._cache模块的缓存。
不过代码执行的时候会有些问题,比如常见的eval和new Function,但是这个会有问题,因为eval是不干净的执行,eval是依赖于上下文环境可能会污染变量。
let a = 'aaa';
eval('console.log(a)')
new function会把模块变成匿名函数,缺点也是不干净执行,依赖于上下文关系,容易变成互相引用。所以node里面执行代码就使用了vm这个沙箱,这个沙箱不依赖于外部环境,他的原理就是创建一个闭包开始执行函数并返回结果。
我们按照这个思路来写下:
let fs = require('fs'); let vm = require('vm'); let path = require('path'); function Module(id) { this.id = id; this.exports = {} } Module.wrapper = [ "(function (exports, require, module, __filename, __dirname) {", "})" ] Module.wrap = function (script) { return Module.wrapper[0] + script+ Module.wrapper[1]; } Module._extensions = { '.js':function (module) { let content = fs.readFileSync(module.id, 'utf8'); let funcStr = Module.wrap(content); let fn = vm.runInThisContext(funcStr); fn.call(module.exports,module.exports,req,module); // exports = {} }, '.json':function (module) { module.exports = JSON.parse(fs.readFileSync(module.id, 'utf8')); } } // 解析文件名 Module._resolveFilename = function (p) { if((/\.js$|\.json$/).test(p)){ // 以js或者json结尾的 return path.resolve(__dirname, p); }else{ // 没有后后缀 自动拼后缀 let exts = Object.keys(Module._extensions); let realPath; for (let i = 0; i < exts.length; i++) { let temp = path.resolve(__dirname, p + exts[i]); try { fs.accessSync(temp); // 存在的 realPath = temp break; } catch (e) { } } if(!realPath){ throw new Error('module not exists'); } return realPath } } Module._cache = {}; function tryModuleLoad(module){ let ext = path.extname(module.id);//扩展名 // 如果扩展名是js 调用js处理器 如果是json 调用json处理器 Module._extensions[ext](module); // exports 上就有了数组 } Module._load = function (p) { // 相对路径,可能这个文件没有后缀,尝试加后缀 let filename = Module._resolveFilename(p); // 获取到绝对路径 let cache = Module._cache[filename]; if(cache){ // 第一次没有缓存 不会进来 return cache.exports; } let module = new Module(filename); // 没有模块就创建模块 Module._cache[filename] = module;// 每个模块都有exports对象 {} //尝试加载模块 tryModuleLoad(module); return module.exports } function req(p) { return Module._load(p); // 加载模块 } let str1 = req('./str.js'); str2 = req('./str.js'); // 缓存靠的就是绝对路径来缓存的
ok,这个就是require原理,学了包那么就自己写一个包发布到npm上开始试(作)验(死)之旅吧。
最后是整体node加载包的流程图,有兴趣的同学可以写写试试看(*^▽^*)。
node模块加载策略。
文件模块查找规则
首先你得有个npm账号,这个去npm官网上注册一个就可以了,选个文件夹npm init然后开始写代码,写完代码之后npm login 输入你的npm信息之后npm publish这样就完事了,如果你想删除自己的包npm --force unpublish 包名。
最后推荐一个包nrm,这个包可以切换下载包的源,里面不用配置直接使用,安装方法npm i nrm -g,查看包源nrm ls ,切换包源nrm use cnpm