原生js已载入就执行函数_手写CommonJS 中的 require函数
前言
来自于圣松大佬的文章
《手写CommonJS 中的 require函数》
什么是 CommonJS ?
node.js 的应用采用的commonjs模块规范。
每一个文件就是一个模块,拥有自己独立的作用域,变量,以及方法等,对其他的模块都不可见。CommonJS规范规定:每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。require方法用于加载模块。
CommonJS模块的特点:
所有代码都运行在模块作用域,不会污染全局作用域。
模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
模块加载的顺序,按照其在代码中出现的顺序。
如何使用?
假设我们现在有个a.js文件,我们要在main.js 中使用a.js的一些方法和变量,运行环境是nodejs。这样我们就可以使用CommonJS规范,让a文件导出方法/变量。然后使用require函数引入变量/函数。
示例:
// a.jsmodule.exports = '这是a.js的变量'; // 导出一个变量/方法/对象都可以
// main.js
let str = require('./a'); // 这里如果导入a.js,那么他会自动按照预定顺序帮你添加后缀console.log(str); // 输出:'这是a.js的变量'
手写一个require函数
前言
我们现在就开始手写一个 精简版的 require函数,这个require函数支持以下功能:
导入一个符合CommonJS规范的JS文件。支持自动添加文件后缀(暂时支持JS和JSON文件) 现在就开始吧!
1. 定义一个req方法
我们先自定义一个req方法,和全局的require函数隔离开。这个req方法,接受一个名为ID的参数,也就是要加载的文件路径。
// main.js
function req(id){}
let a = req('./a')console.log(a)
2. 新建一个Module 类
新建一个module类,这个module将会处理文件加载的全过程。
function Module(id) { this.id = id; // 当前模块的文件路径 this.exports = {} // 当前模块导出的结果,默认为空}
3. 获取文件绝对路径
刚才我们介绍到,require 函数支持传入一个路径。这个路径可以是相对路径,也可以是绝对路径,也可以不写文件后缀名。
我们在Module类上添加一个叫做“_resolveFilename”
的方法,用于解析用户传进去的文件路径,获取一个绝对路径。
// 将一个相对路径 转化成绝对路径Module._resolveFilename = function (id) {}
继续添加一个 “extennsions” 的属性,这个属性是一个对象。key是文件扩展名,value就是扩展名对应的不同文件的处理方法。
我们通过debugger nodejs require源码看到,原生的require函数支持四种类型文件:
js文件json文件node文件mjs文件
由于篇幅,这里我们就只支持两个扩展名:.js 和.json。
我们分别在extensions对象上,添加两个属性,两个属性的值分别都是一个函数。方便不同文件类型分类处理。
// main.js Module.extensions['.js'] = function (module) {}Module.extensions['.json'] = function (module) {}
接着,我们导入nodejs原生的“path”模块和“fs”模块,方便我们获取文件绝对路径和文件操作。
我们处理一下 Module._resolveFilename 这个方法,让他可以正常工作。
Module._resolveFilename = function (id) { // 将相对路径转化成绝对路径 let absPath = path.resolve(id);
// 先判断文件是否存在如果存在了就不要增加了 if(fs.existsSync(absPath)){ return absPath; } // 去尝试添加文件后缀 .js .json let extenisons = Object.keys(Module.extensions); for (let i = 0; i let ext = extenisons[i]; // 判断路径是否存在 let currentPath = absPath + ext; // 获取拼接后的路径 let exits = fs.existsSync(currentPath); // 判断是否存在 if(exits){ return currentPath } } throw new Error('文件不存在')}
在这里,我们支持接受一个名id的参数,这个参数将是用户传来的路径。
首先我们先使用 path.resolve()获取到文件绝对路径。接着用 fs.existsSync 判断文件是否存在。如果没有存在,我们就尝试添加文件后缀。
我们会去遍历现在支持的文件扩展对象,尝试拼接路径。如果拼接后文件存在,返回文件路径。不存在抛出异常。
这样我们在req方法内,就可以获取到完整的文件路径:
function req(id){ // 通过相对路径获取绝对路径 let filename = Module._resolveFilename(id);}
4. 加载模块 —— JS的实现
这里就是我们的重头戏,加载common.js模块。
首先 new 一个Module实例。传入一个文件路径,然后返回一个新的module实例。
接着定义一个 tryModuleLoad 函数,传入我们新建立的module实例。
function tryModuleLoad(module) { // 尝试加载模块 let ext = path.extname(module.id); Module.extensions[ext](module)}function req(id){ // 通过相对路径获取绝对路径 let filename = Module._resolveFilename(id); let module = new Module(filename); // new 一个新模块 tryModuleLoad(module); }
tryModuleLoad 函数 获取到module后,会使用 path.extname 函数获取文件扩展名,接着按照不同扩展名交给不同的函数分别处理。
处理js文件加载.
第一步,传入一个module对象实例。
使用module对象中的id属性,获取文件绝对路径。拿到文件绝对路径后,使用fs模块读取文件内容。读取编码是utf8。
Module.extensions['.js'] = function (module) { // 1) 读取 let script = fs.readFileSync(module.id, 'utf8'); }
第二步,伪造一个自执行函数。
这里先新建一个wrapper 数组。数组的第0项是自执行函数开头,最后一项是结尾。
let wrapper = [ '(function (exports, require, module, __dirname, __filename) {\r\n', '\r\n})'];
这个自执行函数需要传入5个参数:exports对象,require函数,module对象,dirname路径,fileame文件名。
我们将获取到的要加载文件的内容,和自执行函数模版拼接,组装成一个完整的可执行js文本:
Module.extensions['.js'] = function (module) { // 1) 读取 let script = fs.readFileSync(module.id, 'utf8'); // 2) 内容拼接 let content = wrapper[0] + script + wrapper[1];}
第三步:创建沙箱执行环境
这里我们就要用到nodejs中的 “vm” 模块了。这个模块可以创建一个nodejs的虚拟机,提供一个独立的沙箱运行环境。
具体介绍可以看:vm模块的官方介绍
我们使用vm模块的 runInThisContext函数,他可以建立一个有全局global属性的沙盒。用法是传入一个js文本内容。我们将刚才拼接的文本内容传入,返回一个fn函数:
const vm = require('vm');
Module.extensions['.js'] = function (module) { // 1) 读取 let script = fs.readFileSync(module.id, 'utf8'); // 2) 内容拼接 let content = wrapper[0] + script + wrapper[1]; // 3)创建沙盒环境,返回js函数 let fn = vm.runInThisContext(content); }
第四步:执行沙箱环境,获得导出对象。
因为我们上面有需要文件目录路径,所以我们先获取一下目录路径。这里使用path模块的dirname 方法。
接着我们使用call方法,传入参数,立即执行。
call 方法的第一个参数是函数内部的this对象,其余参数都是函数所需要的参数。
Module.extensions['.js'] = function (module) { // 1) 读取 let script = fs.readFileSync(module.id, 'utf8'); // 2) 增加函数 还是一个字符串 let content = wrapper[0] + script + wrapper[1]; // 3) 让这个字符串函数执行 (node里api) let fn = vm.runInThisContext(content); // 这里就会返回一个js函数 let __dirname = path.dirname(module.id); // 让函数执行 fn.call(module.exports, module.exports, req, module, __dirname, module.id)}
这样,我们传入module对象,接着内部会将要导出的值挂在到module的export属性上。
第五步:返回导出值
由于我们的处理函数是非纯函数,所以直接返回module实例的export对象就ok。
function req(id){ // 没有异步的api方法 // 通过相对路径获取绝对路径 let filename = Module._resolveFilename(id); tryModuleLoad(module); // module.exports = {} return module.exports;}
这样,我们就实现了一个简单的require函数。
let str = req('./a');// str = req('./a');console.log(str);// a.jsmodule.exports = "这是a.js文件"
5. 加载模块 —— JSON文件的实现
json文件的实现就比较简单了。使用fs读取json文件内容,然后用JSON.parse转为js对象就ok。
Module.extensions['.json'] = function (module) { let script = fs.readFileSync(module.id, 'utf8'); module.exports = JSON.parse(script)}
6. 优化
文章初,我们有写:commonjs会将我们要加载的模块缓存。等我们再次读取时,就去缓存中读取我们的模块,而不是再次调用fs和vm模块获得导出内容。
我们在Module对象上新建一个_cache属性。这个属性是一个对象,key是文件名,value是文件导出的内容缓存。
在我们加载模块时,首先先去_cache属性上找有没有缓存过。如果有,直接返回缓存内容。如果没有,尝试获取导出内容,并挂在到缓存对象上。
Module._cache = {}
function req(id){ // 通过相对路径获取绝对路径 let filename = Module._resolveFilename(id); let cache = Module._cache[filename];
if(cache){ // 如果有缓存,直接将模块的结果返回 return cache.exports } let module = new Module(filename); // 创建了一个模块实例 Module._cache[filename] = module // 输入进缓存对象内
// 加载相关模块 (就是给这个模块的exports赋值) tryModuleLoad(module); // module.exports = {} return module.exports;}
完整实现
const path = require('path');const fs = require('fs');const vm = require('vm');
function Module(id) { this.id = id; // 当前模块的id名 this.exports = {}; // 默认是空对象 导出的结果}Module.extensions = {};
// 如果文件是js 的话 后期用这个函数来处理Module.extensions['.js'] = function (module) { // 1) 读取 let script = fs.readFileSync(module.id, 'utf8'); // 2) 增加函数 还是一个字符串 let content = wrapper[0] + script + wrapper[1]; // 3) 让这个字符串函数执行 (node里api) let fn = vm.runInThisContext(content); // 这里就会返回一个js函数 let __dirname = path.dirname(module.id); // 让函数执行 fn.call(module.exports, module.exports, req, module, __dirname, module.id)}
// 如果文件是jsonModule.extensions['.json'] = function (module) { let script = fs.readFileSync(module.id, 'utf8'); module.exports = JSON.parse(script)}
// 将一个相对路径 转化成绝对路径Module._resolveFilename = function (id) { // 将相对路径转化成绝对路径 let absPath = path.resolve(id);
// 先判断文件是否存在如果存在 if(fs.existsSync(absPath)){ return absPath; } // 去尝试添加文件后缀 .js .json let extenisons = Object.keys(Module.extensions); for (let i = 0; i let ext = extenisons[i]; // 判断路径是否存在 let currentPath = absPath + ext; // 获取拼接后的路径 let exits = fs.existsSync(currentPath); // 判断是否存在 if(exits){ return currentPath } } throw new Error('文件不存在')}
let wrapper = [ '(function (exports, require, module, __dirname, __filename) {\r\n', '\r\n})'];// 模块独立 相互没关系
function tryModuleLoad(module) { // 尝试加载模块 let ext = path.extname(module.id); Module.extensions[ext](module)}
Module._cache = {}
function req(id){ // 没有异步的api方法 // 通过相对路径获取绝对路径 let filename = Module._resolveFilename(id); let cache = Module._cache[filename]; if(cache){ // 如果有缓存直接将模块的结果返回 return cache.exports } let module = new Module(filename); // 创建了一个模块 Module._cache[filename] = module; // 加载相关模块 (就是给这个模块的exports赋值) tryModuleLoad(module); // module.exports = {} return module.exports;}
let str = req('./a');console.log(str);
结束总结
这样,我们就手写实现了一个精简版的CommonJS require函数。
让我们回顾一下,require的实现流程:
- 拿到要加载的文件绝对路径。没有后缀的尝试添加后缀
- 尝试从缓存中读取导出内容。如果缓存有,返回缓存内容。没有,下一步处理
- 新建一个模块实例,并输入进缓存对象
- 尝试加载模块
- 根据文件类型,分类处理
- 如果是js文件,读取到文件内容,拼接自执行函数文本,用vm模块创建沙箱实例加载函数文本,获得导出内容,返回内容
- 如果是json文件,读取到文件内容,用JSON.parse 函数转成js对象,返回内容 获取导出返回值。
挂个招聘
我们是码云Gitee私有化部门,正在招聘阿里p6级别的前端开发。要求:统招本科学历及以上,4年以上前端开发经验,25-35k。坐标北京西三旗,不打卡,不996。有意者请发送简历至:wangshengsong@oschina.cn
在线笔记
最近花了点时间把笔记整理到语雀上了,方便同学们阅读:公众号回复笔记或者简历
最后
1.看到这里了就点个在看支持下吧,你的「点赞,在看」
是我创作的动力。
2.关注公众号前端壹栈
,回复「1」加入前端交流群
!「在这里有好多前端开发者,会讨论前端知识,互相学习」!
3.也可添加公众号【前端壹栈】
,一起成长
原生js已载入就执行函数_手写CommonJS 中的 require函数相关推荐
- matlab中floor函数,floor函数_怎么在excel中使用floor函数
floor函数即上取整函数,是计算机C语言中的数学函数,与ceil函数相对应.但是它在excel中却是另一种含义,FLOOR函数是向下舍入为最接近指数基数的倍数,下面小编就教你怎么在excel中使用f ...
- mounted钩子函数_怎样实现Vue中mounted钩子函数获取节点高度
这次给大家带来怎样实现Vue中mounted钩子函数获取节点高度,实现Vue中mounted钩子函数获取节点高度的注意事项有哪些,下面就是实战案例,一起来看一下. 遇到的问题 最近在开发一个Vue的项 ...
- python中index函数_详解python中的index函数用法
1.函数的创建 def fun(): #定义 print('hellow') #函数的执行代码 retrun 1 #返回值 fun() #执行函数 2.函数的参数 普通参数 :要按照顺序输入参数 de ...
- python hasattr函数_浅谈python中的getattr函数 hasattr函数
hasattr(object, name) 作用:判断对象object是否包含名为name的特性(hasattr是通过调用getattr(ojbect, name)是否抛出异常来实现的). 示例: & ...
- python中add函数_如何使用python中的add函数?
之前向大家介绍过python中的求和函数sum函数,numpy中的sum函数,对于数组可以指定维度进行相加.numpy中还有另一种求和运算方法,即add函数.add函数不仅作用于numpy中加法运算, ...
- python grid函数_详解numpy中的meshgrid函数用法
numpy中的meshgrid函数的使用 numpy官方文档meshgrid函数帮助文档https://docs.scipy.org/doc/numpy/reference/generated/num ...
- python numpy sum函数_如何使用Python中的sum函数?
之前小编向大家介绍过python中的sum函数(https://www.py.cn/jishu/jichu/22025.html).在python中sunm函数使用分为两种情况,一种是python自带 ...
- python中从小到大排序的函数_深入理解Python中的排序函数
由于 Python2 和 Python3 中的排序函数略有区别,本文以Python3为主. Python 中的排序函数有 sort , sorted 等,这些适用于哪些排序,具体怎么用,今天就来说一说 ...
- 尝试引用已删除的函数_如何在Excel中使用ROW函数
一.ROW函数介绍 1. ROW函数是用来得到指定单元格的行号.比如"=ROW(B1)",得到的就是B1的行号为"1". 2. 如果括号里面为空,什么都不引用, ...
最新文章
- Eclipse下编译Android自带联系人应用
- 小学计算机笔记,小学信息技术教师读书笔记
- asiox 多个baseurl_vue添加axios,并且指定baseurl
- 把存储过程结果集SELECT INTO到临时表
- python selenium爬虫_详解基于python +Selenium的爬虫
- Golang 词法分析器浅析
- cude的__ldg使用
- js如何在当前页面加载springmvc返回的页面_手写SpringMVC学习
- vue component created没有触发_面试!面试!面试!vue常见面试题。
- 虚拟化技术--桌面虚拟化(VDI)
- HTTP传递数据的几种方法
- 计算机磁盘修复工具,电脑自带chkdsk磁盘修复工具使用教程
- c语言编程贪吃蛇的不同功能,贪吃蛇C语言代码实现(难度可选)
- 70 行 Python 代码写春联,行书隶书楷书随你选
- kitti数据集label解析和可视化教程
- IDEA toString方法输出JSON格式
- JPEGView(图片浏览编辑器)绿色版 v1.0.37
- 使用freemarker引擎动态生成word文件
- 抖音美妆账号一条视频涨粉14.2w,合适刚玩短视频的你丨国仁网络
- “68道 Redis+168道 MySQL”精品面试题(带解析),你背废了吗?
热门文章
- Java防止Xss注入json_浅谈 React 中的 XSS 攻击
- oracle端口号为什么有三个,oracle安装时出现一个端口号
- sed 删除windows下的CR/LF
- 肝,Python3中RPC实践
- 技高一筹!Python奶爸的鸡娃日常!
- 微软 VS Code 有 1400 万用户,而全球开发者才 2400 万
- 45页的NAS神经网络搜索的综述,请查收!
- 地图容器自适应浏览器是什么意思_Web移动端实现自适应缩放界面的方法汇总
- python去重复的数据_Python中mysql查询重复数据并删除重复数据
- python打印倒等腰梯形,Linux使用shell脚本做的菱形等一些益智题