如何手动实现一个 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 关键词

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

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 模块执行器的详细内容,更多关于JavaScript 模块执行器的资料请关注我们其它相关文章!

(0)

相关推荐

  • JavaScript 模块化开发实例详解【seajs、requirejs库使用】

    本文实例讲述了JavaScript 模块化开发.分享给大家供大家参考,具体如下: JS开发的问题 冲突 依赖 JS引入的文件,产生依赖. 使用命名空间解决: 命名空间的弊端 调用的时候 名字比较长. 只能减低冲突,不能完全避免 SeaJs使用 引入sea.js的库 如何变成模块? define 如何调用模块? exports 和 seajs.use 如何依赖模块? require //html: <script src="js/sea.js" type="text/ja

  • 通过实例了解Nodejs模块系统及require机制

    一.简介 Nodejs 有一个简单的模块加载系统.在 Nodejs 中,文件和模块是一一对应的(每个文件被视为一个独立的模块),这个文件可能是 JavaScript 代码,JSON 或编译过的C/C++ 扩展,例如: /** *foo.js *将这个js文件导出为模块 */ exports.hello = function() { console.log("hello Nodejs!"); } /** *main.js *main.js和foo.js在同一目录下 *在控制台中将会输出:

  • JavaScript 几种循环方式以及模块化的总结

    小小最近学习到了js的几种循环方式,对这几种循环方式进行总结.以及对模块化的相关知识点进行总结, 循环方式 循环方式分为好几种循环方式,分别是for循环,forEach循环,map循环,for..in循环,for-of循环,jquery的循环. 小小将会依次对这几种循环方式进行介绍. 一般数组遍历循环 这里使用常用的数组遍历方式. 一般来说,常用的数组遍历如下 for (var index = 0; index < myArray.length; index++) { console.log(m

  • Node.js中文件系统fs模块的使用及常用接口

    fs是filesystem的缩写,该模块提供本地文件的读写能力,基本上是POSIX文件操作命令的简单包装.但是,这个模块几乎对所有操作提供异步和同步两种操作方式,供开发者选择. JavaScript 的是没有操作文件的能力,但是 Node 是可以做到的,Node 提供了操作文件系统模块,是 Node 中使用非常重要和高频的模块,是绝对要掌握的一个模块系统. fs 模块提供了非常多的接口,这里主要说一下一些常用的接口. 1.常用API快速复习 fs.stat 检测是文件还是目录 const fs

  • Node.js API详解之 dgram模块用法实例分析

    本文实例讲述了Node.js API详解之 dgram模块用法.分享给大家供大家参考,具体如下: Node.js API详解之 dgram dgram模块提供了 UDP 数据包 socket 的实现. 使用以下方式引用: const dgram = require('dgram'); dgram.createSocket(options[, callback]) 说明: 创建一个 dgram.Socket 对象. 一旦创建了套接字,调用 socket.bind() 会指示套接字开始监听数据报消息

  • 详解Node.JS模块 process

    process 模块是 nodejs 提供给开发者用来和当前进程交互的工具,它的提供了很多实用的 API.从文档出发,管中窥豹,进一步认识和学习 process 模块: 如何处理命令参数? 如何处理工作目录? 如何处理异常? 如何处理进程退出? process 的标准流对象 深入理解 process.nextTick 如何处理命令参数? 命令行参数指的是 2 个方面: 传给 node 的参数.例如 node --harmony script.js --version 中,--harmony 就是

  • Python是怎样处理json模块的

    首先,了解下什么是JSON? JSON:JavaScript Object Notation [JavaScript 对象表示法] JSON 是一种轻量级的数据交换格式,完全独立于任何程序语言的文本格式.一般,后台应用程序将响应数据封装成JSON格式返回. JSON的基本语法如下:JSON名称/值对.JSON 数据的书写格式是:名称/值对.名称/值对包括字段名称(在双引号中),然后着是一个冒号(:),最后是值. JSON最常用的格式是对象的键值对:key只能是string, value可以是 o

  • Python json模块与jsonpath模块区别详解

    JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,它使得人们很容易的进行阅读和编写.同时也方便了机器进行解析和生成.适用于进行数据交互的场景,比如网站前台与后台之间的数据交互. JSON和XML相比较可谓不相上下. Python 3.X中自带了JSON模块,直接import json就可以使用了. 官方文档:http://docs.python.org/library/json.html Json在线解析网站:http://www.json.cn/ JS

  • 谈谈node.js中的模块系统

    Node.js 的模块 JavaScript 做为一门为网页添加交互功能的简单脚本语言问世,在诞生时并不包含模块系统,随着 JavaScript 解决问题越来越复杂,把所有代码写在一个文件内,用 function 区分功能单元已经不能支撑复杂应用开发了,ES6 带来了大部分高级语言都有的 class 和 module,方便开发者组织代码 import _ from 'lodash'; class Fun {} export default Fun; 上面三行代码展示了一个模块系统最重要的两个要素

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

    如果给你下面这样一个代码片段(动态获取的代码字符串),让你在前端动态引入这个模块并执行里面的函数,你会如何处理呢? module.exports = { name : 'ConardLi', action : function(){ console.log(this.name); } }; node 环境的执行 如果在 node 环境,我们可能会很快的想到使用 Module 模块, Module 模块中有一个私有函数 _compile,可以动态的加载一个模块: export function g

  • 使用RequireJS库加载JavaScript模块的实例教程

    js通过script标签的默认加载方式是同步的,即第一个script标签内的js加载完成后,才开始加载第二个,以此类推,直至js文件全部加载完毕.且js的依赖关系必须通过script的顺序才能确保:而在js加载期间,浏览器将停止响应,这大大影响了用户体验,基于此,很多解决js以来和加载的方案出现,require js就是其中之一. requirejs加载的模块,一般为符合AMD标准的模块,即用define定义,用ruturn返回暴露方法.变量的模块:requirejs也可以加载飞AMD标准的模块

  • 如何开发出更好的JavaScript模块

    不少人都曾经在 npm 上发布过自己开发的 JavaScript 模块,而在使用一些模块的过程中,我经常产生"这个模块很有用,但如果能 xxx 就更好了"的想法.所以,本文将站在模块使用者的角度总结一下,如何能让模块变得更好用. 提供 ES6 模块的入口 webpack 和 rollup 都支持对 ES6 模块做一些静态优化(例如 Tree Shaking 和 Scope Hoisting),它们都会优先读取 package.json 中的 module 字段作为 ES6 模块的入口,

  • JavaScript模块模式实例详解

    本文实例讲述了JavaScript模块模式.分享给大家供大家参考,具体如下: 在JS中没有Class的概念,那么如何体现Object的Public和Private属性呢,答案就是模块模式(Module Pattern). JS中有一个显著的特性: 匿名函数(anonymous function),通过匿名函数的建立和执行,匿名函数里的代码就形成了一个闭包(closure),从而形成,封装和控制一个对象的Private和Public的特性,避免了全局变量的泛滥和与其他脚本的冲突. (functio

  • 关于javascript模块加载技术的一些思考

    前不久有个网友问我在前端使用requireJs和seajs的问题,我当时问他你们公司以前有没有自己编写的javascript库,或者javascript框架,他的回答是什么都没有,他只是听说像requirejs和seajs是新东西新技术,很有价值所以想用它. 这位网友的问题引起了我对javascript模块加载技术的思考,上篇文章我给出了自己写的一个javascript库的基本结构,其实写这篇文章的一个起因就是因为我想使用requirejs或者seajs这样的技术来重新设计我写javascrip

  • JavaScript模块详解

    本文是关于JavaScript模块的基础讲解内容,对每个模块进行了分析和代码用法的讲解,以下是全部内容: JavaScript的模块介绍 模块通常是指编程语言所提供的代码组织机制,利用此机制可将程序拆解为独立且通用的代码单元.所谓模块化主要是解决代码分割.作用域隔离.模块之间的依赖管理以及发布到生产环境时的自动化打包与处理等多个方面. 模块的优点 可维护性. 因为模块是独立的,一个设计良好的模块会让外面的代码对自己的依赖越少越好,这样自己就可以独立去更新和改进. 命名空间. 在 JavaScri

  • javascript 模块依赖管理的本质深入详解

    本文实例讲述了javascript 模块依赖管理的本质.分享给大家供大家参考,具体如下: 模块模式定义 模块是'javascript'的一种设计模式,它为函数定义一个包装函数,并且该包装函数的返回值与模块的API保持一致: function createModule() { function hello(name) { console.log(name + '帅哥你好!'); } return { hello: hello } } // 这里调用 createModule 来创建一个模块实例 v

  • 构建一个JavaScript插件系统

    本文译自 https://css-tricks.com/designing-a-javascript-plugin-system/ 插件是库和框架的常见功能,并且有一个很好的使用它的理由:它们允许开发人员以安全,可扩展的方式添加功能.这就使核心项目更具价值,这种开放形势可以帮助项目建立社区,并且不会为我们增加额外的维护负担. 本文就使用 JavaScript 来构建一个我们自己的插件系统. 这里我使用的是 "pluginn" 一词,但这些东西有时也称为其他名称,例如"exte

  • Javascript模块导入导出详解

    笔者开始学习Javascript的时候,对模块不太懂,不知道怎么导入模块,导出模块,就胡乱一通试 比如 import xx from 'test.js' 不起作用,就加个括号 import {xx} from 'test.js' 反正总是靠蒙,总有一种写法是对的,其实还是没有理解,还是不懂 尤其是在当初写 www.helloworld.net 网站的时候,一遇到这种问题,就懵逼了,尤其是引入第三方库的时候 这种情况下更多,此篇文章也是为了怕以后忘记,自查用的,也希望能帮助更多的朋友,此篇文章只是

  • 第一个JavaScript入门基础 document.write输出

    如果你有编程基础,学习Javascript是一件很容易的事情,如果你没有编程基础,也不要担心,我们会为你解释每一行代码. 复制代码 代码如下: <html> <body> <script type="text/javascript"> document.write("This is my first javascript"); </script> </body> </html> 我们将跳过HTM

随机推荐