写点什么

Javascript 实现一个 Module

作者:Jeannette
  • 2021 年 12 月 12 日
  • 本文字数:2557 字

    阅读完需:约 8 分钟

 Node.js 中使用的 Commonjs 模块已经很广泛了,但是应该怎么去实现一个 Module 呢

/** *  实现一个commonjs模块加载器 (同步的方式),所以不适合浏览器,会造成卡顿 *  Commonjs的规范的准则 *  1. 模块加载器 解析方件地址, 有一个寻找的规则,目的找到文件 *   src/xx/xxx/xxx/xx.js *  2. 模块解析器: 执行文件的内容,Node里面使用V8执行*/
// exapmle: ---// exports.a = 'a';// require('./a.js')
class Module { constructor(moduleName, source) { // 暴露数据 this.exports = {} // 保存模块信息 this.moduleName = moduleName; // 缓存 this.$cacheModule = new Map(); // 源代码 this.$source = source } /** * * @param {模块名称} moduleName * @param {文件的源代码} source */ 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 } // 拼一个闭包出来, IIFE $wrap = (code) => { // 拼的一个闭包函数,然后拼接起来 const wrapper = [ 'return (function (module, exports, require) {', '\n});' ]; return wrapper[0] + code + wrapper[1]; }
/** * 实现一个浏览器运行的解析器 * 核心是创建是一个隔离的沙箱环境,来执行代码 * 隔离: 不能访问闭包的变量, 1. 不能访问全局变量, 只能访问传入的变量 * eval: 可以访问全局/闭包,但需要解析执行, es6后, 如果是间接使用eval, 不可以访问闭包 * -> (0, eval) ('var a = b + 1'); * new Funciton: 不可以访问闭包,可以访问全局, 只能编译一次 * with: with包裹的对象, 会被放到原型链的顶部, 而且通过in 操作符判断的 * 如果通过with 塞入传入的数据,只能访问传入的变量,任何属性,取不到返回undefined, 永远不会访问全局的域 * unscopable: 这个对象不能够被with处量 * @param {*} code */ $runInThisContext = (code, whiteList=['console']) => { // 使用with保证可以通过我们传入的sanbox对象数据 const wrapper = `with(sandbox) {${code}}`; const func = new Function('sandbox', wrapper) return function (sandbox) { // 塞到源代码中的变量 if (!sandbox || typeof sandbox !== 'object') { throw Error('sandbox parameter must be an object') } // 限制变量的查找,当怎么找,都是从sanbox里找 const proxedObject = new Proxy(sandbox, { // 传门处理in的操作符,因为这里的in是with里的特点, 这样就不会从原型链不停的往上找 has(target, key) { if (!whiteList.includes(key)) return true }, get(target, key, receiver) { if (key === Symbol.unscopables) { return void 0; } // Reflect 可以用于获取目标对象的行为,它与 Object 类似,但是更易读,为操作对象提供了一种更优雅的方式。它的方法与 Proxy 是对应的。 return Reflect.get(target, key, receiver) } }); return func(proxedObject) } } /** * 执行文件内容,入参数是文件源代码字符串 * * IIFE: (function() {})(xxx, yyy); * * function (proxiedSandbox) { * with (proxiedSandbox) { * return (function (module, exports, require) { * // 文件内容字符串 * }) * } * } */ compile = (module, source) => { console.log(this) // return (function(module, exports, require) { //xxxx }); ⚠️ const iifeString = this.$wrap(source); const compiler = this.$runInThisContext(iifeString)({}) compiler.call(module, module, module.exports, this.require); return module.exports; }}
const m = new Module();
// a.jsconst sourceCodeFromAModule = ` const b = require('b.js', 'const a = require("a.js"); console.log("a module: ", a); exports.action = function() { console.log("execute action from B module successfully 🎉") }');
b.action();
exports.action = function() { console.log("execute action from A module!"); }`m.require('a.js', sourceCodeFromAModule);
复制代码


这里面主要的重点是使用

  1. IIFE + new Function + Proxy + with 实现沙箱环境

 

这里重点在于,new Function 不能访问闭包 + with 可以访问某个对象的属性, 加上 Proxy 可以约束它一直找上层的作用域 , 结合 IIFE 可以闭包的方式进行作用域的保护包括可以通过字符串的方式生成一个模块的函数

 

关于 IIFE:

https://developer.mozilla.org/zh-CN/docs/Glossary/IIFE

 

关于 new Function https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function

 

关于 Proxy

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy

 

关于 with

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/with


关于源码 Commonjs 的源码

https://github1s.com/nodejs/node/blob/HEAD/lib/internal/modules/cjs/loader.js

发布于: 3 小时前阅读数: 7
用户头像

Jeannette

关注

一位小码农…… 2019.07.19 加入

科学技术是第一生产力

评论

发布
暂无评论
Javascript实现一个Module