返回顶部
首页 > 资讯 > 前端开发 > html >如何手动实现一个 JavaScript 模块执行器
  • 791
分享到

如何手动实现一个 JavaScript 模块执行器

2024-04-02 19:04:59 791人浏览 独家记忆
摘要

今天就跟大家聊聊有关如何手动实现一个 javascript 模块执行器,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。如果给你下面这样一个代码片段(动

今天就跟大家聊聊有关如何手动实现一个 javascript 模块执行器,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。

如果给你下面这样一个代码片段(动态获取的代码字符串),让你在前端动态引入这个模块并执行里面的函数,你会如何处理呢?

module.exports = {    name : 'ConardLi',   action : function(){     console.log(this.name);   } };

node 环境的执行

如果在 node 环境,我们可能会很快的想到使用 Module 模块, Module 模块中有一个私有函数  _compile,可以动态的加载一个模块:

export function getRuleFromString(code) {   const myModule = new Module('my-module');   myModule._compile(code,'my-module');   return myModule.exports; }

实现就是这么简单,后面我们会回顾一下 _compile 函数的原理,但是需求可不是这么简单,我们如果要在前端环境动态引入这段代码呢?

嗯,你没听错,最近正好碰到了这样的需求,需要在前端和 Node 端抹平动态引入模块的逻辑,好,下面我们来模仿 Module 模块实现一个前端环境的  JavaScript 模块执行器。

首先我们先来回顾一下 node 中的模块加载原理。

node Module 模块加载原理

node.js 遵循 Commonjs 规范,该规范的核心思想是允许模块通过 require 方法来同步加载所要依赖的其他模块,然后通过 exports  或 module.exports 来导出需要暴露的接口。其主要是为了解决 JavaScript  的作用域问题而定义的模块形式,可以使每个模块它自身的命名空间中执行。

再在每个 nodejs 模块中,我们都能取到 module、exports、__dirname、__filename 和 require  这些模块。并且每个模块的执行作用域都是相互隔离的,互不影响。

其实上面整个模块系统的核心就是 Module 类的 _compile 方法,我们直接来看 _compile 的源码

Module.prototype._compile = function(content, filename) {   // 去除 Shebang 代码   content = internalModule.stripShebang(content);    // 1.创建封装函数   var wrapper = Module.wrap(content);     // 2.在当前上下文编译模块的封装函数代码   var compiledWrapper = vm.runInThisContext(wrapper, {      filename: filename,     lineOffset: 0,     displayErrors: true   });    var dirname = path.dirname(filename);   var require = internalModule.makeRequireFunction(this);    var depth = internalModule.requireDepth;      // 3.运行模块的封装函数并传入 module、exports、__dirname、__filename、require    var result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);   return result; };

整个执行过程我将其分为三步:

创建封装函数

第一步即调用 Module 内部的 wrapper 函数对模块的原始内容进行封装,我们先来看看 wrapper 函数的实现:

Module.wrap = function(script) {   return Module.wrapper[0] + script + Module.wrapper[1]; };  Module.wrapper = [   '(function (exports, require, module, __filename, __dirname) { ',   '\n});' ];

CommonJS 的主要目的就是解决 JavaScript  的作用域问题,可以使每个模块它自身的命名空间中执行。在没有模块化方案的时候,我们一般会创建一个自执行函数来避免变量污染:

(function(global){   // 执行代码。。 })(window)

所以这一步至关重要,首先 wrapper  函数就将模块本身的代码片段包裹在一个函数作用域内,并且将我们需要用到的对象作为参数引入。所以上面的代码块被包裹后就变成了:

(function (exports, require, module, __filename, __dirname) {   module.exports = {      name : 'ConardLi',     action : function(){      console.log(this.name);    }   }; });

编译封装函数代码

NodeJs 中的 vm 模块提供了一系列 api 用于在 V8 虚拟机环境中编译和运行代码。JavaScript  代码可以被编译并立即运行,或编译、保存然后再运行。

vm.runInThisContext() 在当前的 global 对象的上下文中编译并执行  code,最后返回结果。运行中的代码无法获取本地作用域,但可以获取当前的 global 对象。

var compiledWrapper = vm.runInThisContext(wrapper, {    filename: filename,   lineOffset: 0,   displayErrors: true });

所以以上代码执行后,就将代码片段字符串编译成了一个真正的可执行函数:

(function (exports, require, module, __filename, __dirname) {   module.exports = {      name : 'ConardLi',     action : function(){      console.log(this.name);    }   }; });

运行封装函数

最后通过 call 来执行编译得到的可执行函数,并传入对应的对象。

var result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);

所以看到这里你应该会明白,我们在模块中拿到的 module,就是 Module 模块的实例本身,我们直接调用的 exports 实际上是  module.exports 的引用,所以我们既可以使用 module.exports 也可以使用 exports 来导出一个模块。

实现 Module 模块

如果我们想在前端环境执行一个 CommonJS 模块,那么我们只需要手动实现一个 Module  模块就好了,重新梳理上面的流程,如果只考虑模块代码块动态引入的逻辑,我们可以抽象出下面的代码:

export default class Module {   exports = {}   wrapper = [     'return (function (exports, module) { ',     '\n});'   ];    wrap(script) {     return `${this.wrapper[0]} ${script} ${this.wrapper[1]}`;   };    compile(content) {     const wrapper = this.wrap(content);     const compiledWrapper = vm.runInContext(wrapper);     compiledWrapper.call(this.exports, this.exports, this);   } }

这里有个问题,在浏览器环境是没有 VM 这个模块的,VM  会将代码加载到一个上下文环境中,置入沙箱(sandbox),让代码的整个操作执行都在封闭的上下文环境中进行,我们需要自己实现一个浏览器环境的沙箱。

实现浏览器沙箱

eval

在浏览器执行一段代码片段,我们首先想到的可能就是 eval, eval 函数可以将一个 Javascript 字符串视作代码片段执行。

但是,由 eval() 执行的代码能够访问闭包和全局作用域,这会导致被称为代码注入 code injection 的安全隐患, eval  虽然好用,但是经常被滥用,是 JavaScript 最臭名昭著的功能之一。

所以,后来又出现了很多在沙箱而非全局作用域中的执行字符串代码的值的替代方案。

new Function()

Function 构造器是 eval() 的一个替代方案。new Function(...args, 'funcBody') 对传入的  'funcBody' 字符串进行求值,并返回执行这段代码的函数。

fn = new Function(...args, 'functionBody');

返回的 fn 是一个定义好的函数,最后一个参数为函数体。它和 eval 有两点区别:

  • fn 是一段编译好的代码,可以直接执行,而 eval 需要编译一次

  • fn 没有对所在闭包的作用域访问权限,不过它依然能够访问全局作用域

但是这仍然不能解决访问全局作用域的问题。

with 关键词

如何手动实现一个 JavaScript 模块执行器

with 是 JavaScript 一个冷门的关键字。它允许一个半沙箱的运行环境。with  代码块中的代码会首先试图从传入的沙箱对象获得变量,但是如果没找到,则会在闭包和全局作用域中寻找。闭包作用域的访问可以用new Function()  来避免,所以我们只需要处理全局作用域。with 内部使用 in 运算符。在块中访问每个变量,都会使用 variable in sandbox  条件进行判断。若条件为真,则从沙箱对象中读取变量。否则,它会在全局作用域中寻找变量。

function compileCode(src) {   src = 'with (sandbox) {' + src + '}'   return new Function('sandbox', src) }

试想,如果 variable in sandbox  条件永远为真,沙箱环境不就永远也读取不到环境变量了吗?所以我们需要劫持沙箱对象的属性,让所有的属性永远都能读取到。

Proxy

如何手动实现一个 JavaScript 模块执行器

es6 中提供了一个 Proxy 函数,它是访问对象前的一个拦截器,我们可以利用 Proxy 来拦截 sandbox  的属性,让所有的属性都可以读取到:

function compileCode(code) {   code = 'with (sandbox) {' + code + '}';   const fn = new Function('sandbox', code);   return (sandbox) => {     const proxy = new Proxy(sandbox, {       has() {         return true;        }     });     return fn(proxy);   } }

Symbol.unscopables

Symbol.unscopables 是一个著名的标记。一个著名的标记即是一个内置的 JavaScript  Symbol,它可以用来代表内部语言行为。

Symbol.unscopables 定义了一个对象的 unscopable(不可限定)属性。在 with 语句中,不能从 Sandbox 对象中检索  Unscopable 属性,而是直接从闭包或全局作用域检索属性。

所以我们需要对 Symbol.unscopables 这种情况做一次加固,

function compileCode(code) {   code = 'with (sandbox) {' + code + '}';   const fn = new Function('sandbox', code);   return (sandbox) => {     const proxy = new Proxy(sandbox, {       has() {         return true;        },       get(target, key, receiver) {         if (key === Symbol.unscopables) {           return undefined;          }         Reflect.get(target, key, receiver);       }     });     return fn(proxy);   } }

全局变量白名单

但是,这时沙箱里是执行不了浏览器默认为我们提供的各种工具类和函数的,它只能作为一个没有任何副作用的纯函数,当我们想要使用某些全局变量或类时,可以自定义一个白名单:

const ALLOW_LIST = ['console'];  function compileCode(code) {   code = 'with (sandbox) {' + code + '}';   const fn = new Function('sandbox', code);   return (sandbox) => {     const proxy = new Proxy(sandbox, {       has() {         if (!ALLOW_LIST.includes(key)) {             return true;         }       },       get(target, key, receiver) {         if (key === Symbol.unscopables) {           return undefined;          }         Reflect.get(target, key, receiver);       }     });     return fn(proxy);   } }

最终代码:

好了,总结上面的代码,我们就完成了一个简易的 JavaScript 模块执行器:

const ALLOW_LIST = ['console'];  export default class Module {    exports = {}   wrapper = [     'return (function (exports, module) { ',     '\n});'   ];    wrap(script) {     return `${this.wrapper[0]} ${script} ${this.wrapper[1]}`;   };    runInContext(code) {     code = `with (sandbox) { ${code}  }`;     const fn = new Function('sandbox', code);     return (sandbox) => {       const proxy = new Proxy(sandbox, {         has(target, key) {           if (!ALLOW_LIST.includes(key)) {             return true;           }         },         get(target, key, receiver) {           if (key === Symbol.unscopables) {             return undefined;           }           Reflect.get(target, key, receiver);         }       });       return fn(proxy);     }   }    compile(content) {     const wrapper = this.wrap(content);     const compiledWrapper = this.runInContext(wrapper)({});     compiledWrapper.call(this.exports, this.exports, this);   } }

测试执行效果:

function getModuleFromString(code) {   const scanModule = new Module();   scanModule.compile(code);   return scanModule.exports; }  const module = getModuleFromString(` module.exports = {    name : 'ConardLi',   action : function(){     console.log(this.name);   } }; `);  module.action(); // ConardLi

看完上述内容,你们对如何手动实现一个 JavaScript 模块执行器有进一步的了解吗?如果还想了解更多知识或者相关内容,请关注编程网html频道,感谢大家的支持。

--结束END--

本文标题: 如何手动实现一个 JavaScript 模块执行器

本文链接: https://lsjlt.com/news/82331.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 如何手动实现一个 JavaScript 模块执行器
    今天就跟大家聊聊有关如何手动实现一个 JavaScript 模块执行器,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。如果给你下面这样一个代码片段(动...
    99+
    2024-04-02
  • Python如何实现以主程序的形式执行模块
    今天小编给大家分享一下Python如何实现以主程序的形式执行模块的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。前言:这个先来...
    99+
    2023-07-02
  • JavaScript如何手动实现instanceof
    这篇文章将为大家详细讲解有关JavaScript如何手动实现instanceof,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。1. instanceof的用法instanceof运算符用于检测构造函数的p...
    99+
    2023-06-25
  • JavaScript如何实现自执行函数
    这篇文章将为大家详细讲解有关JavaScript如何实现自执行函数,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。自执行函数 ( function(){…} )() 和( function(){…} () ...
    99+
    2023-06-03
  • php如何实现几秒后执行一个函数
    本篇内容介绍了“php如何实现几秒后执行一个函数”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!实现方法:1、使用“sleep(延迟秒数)”语...
    99+
    2023-06-30
  • springboot如何实现多模块项目添加一新模块
    这篇文章主要介绍了springboot如何实现多模块项目添加一新模块,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。springboot多模块项目添加一新模块选择Maven M...
    99+
    2023-06-29
  • 如何实现进行Python测试模块
    小编给大家分享一下如何实现进行Python测试模块,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!为了更容易实现这种Python 测试模块,避免多次复制并粘贴测试函...
    99+
    2023-06-17
  • 原生JavaScript中如何用关键字import和export来实现一个模块接口
    这篇文章将为大家详细讲解有关原生JavaScript中如何用关键字import和export来实现一个模块接口,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。...
    99+
    2024-04-02
  • php如何实现代码自动执行
    本文小编为大家详细介绍“php如何实现代码自动执行”,内容详细,步骤清晰,细节处理妥当,希望这篇“php如何实现代码自动执行”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。一、PHP的自动执行功能PHP提供了多种方...
    99+
    2023-07-06
  • 怎么在Matplotlib中利用animation模块实现一个动态图
    本篇文章为大家展示了怎么在Matplotlib中利用animation模块实现一个动态图,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。matplotlib 画图功能非常强大,目前也只能根据官网 提供...
    99+
    2023-06-06
  • Nodejs+express模块如何创建一个服务器
    这篇文章给大家分享的是有关Nodejs+express模块如何创建一个服务器的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。使用express模块创建一个服务器新建一个文件夹,文件夹名字非中文,名字也不要和模块名字...
    99+
    2023-06-14
  • 如何实现一个简单的区块链
    这篇文章将为大家详细讲解有关如何实现一个简单的区块链,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。区块链的基础概念很简单:一个分布式数据库,...
    99+
    2024-04-02
  • 如何通过JavaScript实现组件化和模块化
    今天小编给大家分享的是如何通过JavaScript实现组件化和模块化,相信很多人都不太了解,为了让大家更加了解,所以给大家总结了以下内容,一起往下看吧。一定会有所收获的哦。组件化和模块化为什么会有组件化和模块化?当我们的项目复杂度不断上升,...
    99+
    2023-07-06
  • php如何实现一个条件成立时执行脚本外的另一个php脚本
    在php开发中,我们经常需要在满足一定条件时执行某个脚本,这时候我们可以使用php的条件语句和系统命令,实现一个条件成立时执行脚本外的另一个php脚本。一、条件语句php中经常用到的条件语句有if、else语句和switch语句。根据不同的...
    99+
    2023-05-24
  • Python PyQt5模块实现一个浏览器的示例代码
    目录1. 首先是环境的安装 (本人使用的是PyCharm,python3.6)2. 实现代码3. 运行结果4. Tips1. 首先是环境的安装 (本人使用的是PyCharm,pyth...
    99+
    2024-04-02
  • 如何在Python中使用Tqdm模块实现一个进度条功能
    本文章向大家介绍如何在Python中使用Tqdm模块实现一个进度条功能,主要包括如何在Python中使用Tqdm模块实现一个进度条功能的使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。Pytho...
    99+
    2023-06-06
  • 如何实现一台机器运行两个JDK
    这篇文章主要为大家展示了“如何实现一台机器运行两个JDK”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“如何实现一台机器运行两个JDK”这篇文章吧。一台机器运行两个JDK由于系统的原因,现在要在一...
    99+
    2023-06-03
  • php如何实现一个单列模式
    小编给大家分享一下php如何实现一个单列模式,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!php实现一个单列模式的方法:1、创建私有变量保存该对象;2、禁止使用n...
    99+
    2023-06-15
  • JavaScript如何实现异步任务循环顺序执行
    今天小编给大家分享一下JavaScript如何实现异步任务循环顺序执行的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。需求场景...
    99+
    2023-07-05
  • Python Logging 模块的动手实践:构建一个完整的日志记录系统
    Logging、日志纪录、调试、应用程序、开发 Logger 的建立与使用 Logging 模块的核心是 Logger 类。要开始使用,您需要创建一个 Logger 实例: import urs logger = ...
    99+
    2024-02-20
    本指南将带您逐步使用 Logging 模块来建立一个全面高效的日志纪录。我们将探究模块的基本概念 并使用动手示例来展示其工作原理。准备好动手体验 Logging 模块的非凡力量 在您的应用程
软考高级职称资格查询
推荐阅读
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作