本文已参与「新人创作礼」活动,一起开启掘金创作之路。
一、需求:首先CommonJS中我们主要实现的内容是:
1.模块加载器: 解析文件地址,通过node,这次我们直接给一段代码传入,先不写解析部分
2.模块解析器: 执行文件内容
二、代码
1.创建一个class,定义传参和变量等
class Module {
// 传入模块名称和文件内容(要执行的代码块)
constructor(moduleName, source) {
this.export = {}; // 定义模块要返回的export
this.moduleName = moduleName;
this.source = source;
}
}
2.Require
》创建模块
》执行文件内容
》返回export
require = (moduleName,source) => {
// 创建一个模块
const module = new Module(moduleName, source);
// 执行文件得到结果
const exports = compile(module, source);
// 通过require返回结果
return exports;
}
》缓存
class Module {
// 传入模块名称和文件内容(要执行的代码块)
constructor(moduleName, source) {
this.export = {}; // 定义模块要返回的export
this.moduleName = moduleName;
this.source = source;
this.$cacheModule = new Map(); // 定义一个缓存变量
}
require = (moduleName,source) => {
// 缓存中有这个模块的数据的话就返回缓存的exports
if($cacheModule.has(moduleName)){
return $cacheModule.get(moduleName).exports;
}
// 创建一个模块
const module = new Module(moduleName, source);
// 执行文件得到结果
const exports = compile(module, source);
// 把module存入缓存
this.$cacheModule.set(moduleName, module);
// 通过require返回结果
return exports;
}
}
》IIFE
声明完之后便直接执行的函数,这类函数通常是一次性使用的,因此没必要给这类函数命名,直接让它执行就好了 IIFE的作用就是防止变量全局污染,以及保证内部变量的安全 他是通过模仿一个私有作用域,用匿名函数作为一个“容器”,“容器”内部可以访问外部的变量,而外部环境不能访问“容器”内部的变量,所以 ( function(){…} )() 内部定义的变量不会和外部的变量发生冲突,俗称“匿名包裹器”或“命名空间”
通过IIFE让内部代码不会污染全局,但在IIFE中能访问到全局变量。
(function(arg1,arg2){...})(a,b)
$wrap = (code) => {
// 这里传入的参数就是想以后抛出到文件模块中用的接口
return `function(module, exports, require){${code}}`
}
》然后现在就是这个code到底怎么执行了
这里我们要创建一个沙箱环境执行code, 因为我们要保证code执行时:1.不能访问闭包的变量 ,2.不能访问全局的变量 ,3.只能访问我们传入的变量 这里我们通过new Function() 不能访问闭包,
const func1 = () => {
const a = 123;
return function() {
const b = 456;
console.log(a,b,window); // 123,456,window对象
const func = new Function('obj',
'console.log(obj.a + obj.b, window);'+ // 579, window对象
'console.log(a,b)' // a,b都不能访问(即不能访问)
);
return func({a:123,b:456});
}
}
func1()()
通过with()包裹的对象,会被放到原型链的顶部,而且底层是通过 in 操作符判断的.,所有变量都会通过in从传入的对象中去取,但也能取到全局变量。
const obj = {a: 123, b: 456}
with(obj){
console.log(obj) // {a: 123, b: 456}
console.log(a+b) // 579
}
通过proxy去拦截with的in操作符,让with在取变量的过程中(底层),所有除了白名单以外的变量都通过in操作,如果没有就返回underfined
// 代理
const proxiedObject = new Proxy(sandbox, {
// 专门处理 in 操作符的
has(target, key) {
if (!whiteList.includes(key)) {
return true;
}
},
get(target, key, receiver) {
if (key === Symbol.unscopables) {
return void 0;
}
return Reflect.get(target, key, receiver);
}
});
所以一个简单的沙箱如下
$runInThisContext = (code,whiteList=['console']) => {
const func = new Function('sandbox', `with(sandbox){${code}}`);
return function(sandbox){
if(!sandbox || typeof sandbox !== 'object') {
throw error("sandbox parameter must be a object")
}
const proxiedObject = new Proxy(sandbox, {
has(target,key) {
if(!whiteList.includes(key)) {
return true;
}
},
get(target, key, receiver) {
if(key === Symbol.unscopables) {
return void 0;
}
return Reflect.get(target, key, receiver)
}
})
return func(proxiedObject)
}
}
》最后在沙箱环境下传入code,执行代码
const compiler = this.$runInThisContext(this.$wrap(source))({});
compiler.call(module, module, module.exports, this.require)
return module.exports;
3.验证
const m = new Module();
// a.js
const aSourceCode = `
const b = require('b.js', 'const a = require("a.js"); console.log("module a:", a); exports.action = function() {console.log("excute action from B successfully!")}');
b.action();
exports.action = function() {
console.log("excute action from A successfully!")
}
`
m.require('a.js',aSourceCode)
// module a: {}
// excute action from B successfully!
4.完整代码
class Module {
constructor(moduleName, source) {
this.exports = {};
this.moduleName = moduleName;
this.$cacheModule = new Map();
this.$source = source;
}
/**
* @description: require方法
* @param {string} moduleName 其实就是路径信息
* @param {string} source 文件的源代码,因为省略了加载器解析路径得到源代码的过程
* @return {object} export出去的对象
*/
require = (moduleName, source) => {
if (this.$cacheModule.has(moduleName)) {
return this.$cacheModule.get(moduleName).exports;
}
// 创建模块
const module = new Module(moduleName, source);
// 执行文件
const exports = this.compile(module, source);
// 缓存
this.$cacheModule.set(moduleName, module);
return exports;
};
/**
* @description: IIFE 拼一个闭包
* @param {string} code 代码字符串
* @return {*}
*/
$wrap = (code) => {
const wrapper = [
'return (function(module,exports,require) {',
'\n})'
]
return wrapper[0] + code + wrapper[1]
}
/**
* @description: 简单实现一个能在浏览器跑的解释器 vm.runInThisContext
* @param {string} code
* @return {*}
*/
$runInThisContext = (code,whiteList=['console']) => {
const func = new Function('sandbox', `with(sandbox){${code}}`);
return function(sandbox){
if(!sandbox || typeof sandbox !== 'object') {
throw error("sandbox parameter must be a object")
}
const proxiedObject = new Proxy(sandbox, {
has(target,key) {
if(!whiteList.includes(key)) {
return true;
}
},
get(target, key, receiver) {
if(key === Symbol.unscopables) {
return void 0;
}
return Reflect.get(target, key, receiver)
}
})
return func(proxiedObject)
}
}
/**
* @description: 执行文件
* @param {*}
* @return {*}
*/
compile = (module, source) => {
const compiler = this.$runInThisContext(this.$wrap(source))({});
compiler.call(module, module, module.exports, this.require)
return module.exports;
};
}
const m = new Module();
// a.js
const aSourceCode = `
const b = require('b.js', 'const a = require("a.js"); console.log("module a:", a); exports.action = function() {console.log("excute action from B successfully!")}');
b.action();
exports.action = function() {
console.log("excute action from A successfully!")
}
`
m.require('a.js',code)