一步步实现一个简单的 CMJ 模块加载器

一步步实现一个简单的 COMMONJS 模块加载器

1. 使用es6的class语法

    // 使用 `es6` class 语法
    class Module {
        constructor() {
            
        }
    }

2.初始化exports,保存模块信息

    class Module {
        constructor(moduleName) {
            // 暴露数据
            this.exports = {};

            //保存一下模块的信息
            this.moduleName = moduleName;
        }
    }

3.实现require函数

TODO: 由于模块的加载,要使用到node的fs模块,应用相应的规则(先根据相对路径找,再到对应的node_modules中查找,以及最后在全局global_node_modules中查找)找到文件具体位置,最后的目的其实就是获取文件的源代码,较为复杂,不在此实现。现采用直接将文件源代码传入require方法替代

class Module {
    constructor(moduleName, source) {
        // ...

        // 源代码
        this.$source = source;
    }

    /**
     * require
     * 
     * useage: require('./a.js')
     * 
     * @param {string} moduleName  模块的名称,文件路径
     * @param {string} source 文件的源代码
     * 
     * @return {object} require 返回的结果就是 exports 的引用
     */
    require = (moduleName, source) => {

         /**
         * 省略加载器查找文件的过程
         * 
         * 否则这里应该执行加载器方法,获取文件源代码
         */

        // 创建模块对象
        const module = new Module(moduleName, source);

        // 执行文件内容
        // TODO
        const exports = this.compile(module, source);

        //缓存
        this.$cacheModule.set(moduleName, module);

        return module.exports;
    }
}

4.实现一个简单的隔离沙箱环境

问题:为什么要创建一个隔离的沙箱环境去执行文件源代码?

可以想象一下,如果文件内容的执行过程可以访问全局变量,那么意味着文件的执行会出现意想不到的问题;比如:有一个全局变量 case,在文件 a.js 写到 if(case != null){ //... },但其实变量 casea.js 中是没有定义的,按照道理文件执行会出错。但如果文件内容的执行过程可以访问全局变量,就会出现错误,导致文件会正常执行。

实现沙箱方案:

eval:可以访问全局/闭包,但实际需要解释执行 ❌

new Function:不可以访问闭包,可以访问全局,只编译一次 1 ✅

proxy:可以拦截属性的获取,保证不能访问全局变量 2 ✅

with:with 包裹的对象,会被放到原型链的顶部,而且是通过 in 操作符判断的;通过 with 塞入我们传入的数据对象 3 ✅

    /**
     * 简单实现一个能在浏览器中运行的解释器 vm.runInThisContext
     * 目标:
     * 创建一个隔离的沙箱环境,来执行我们的代码字符串
     * 
     * 隔离:1、不能访问闭包的标量,2、不能访问全局的变量,3、只能访问我们传入的变量
     * 
     * 
     * 
     * @param {string} code 代码字符串
     */

    $runInThisContext = (code, whiteList=['console']) => {

        // 使用 with 保证可以通过我们传入的 sandbox 对象取数据
        const wrapper = `with(sandbox) {${code}}`
        // new Function 不能访问闭包
        const func = new Function('sandbox', wrapper);

        return function(sandbox) {
            if(!sandbox || typeof sandbox !== 'object') {
                throw Error('sandbox parameter must be an object');
            }

            // 代理

            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);
                }
            });

            return func(proxiedObject);
        }

    }

5.实现一个IIFE函数

    /**
     * 
     * 拼接一个闭包,使用自执行函数 IIFE
     * 
     * @param {string} code 代码字符串
     */

    $wrap = (code) => {
        const wrapper = [
            'return (function (module, exports, require){',
            '\n});'
        ]

        return wrapper[0] + code + wrapper[1];
    }

6.实现require函数中的compile方法, 向文件中注入 module exports require

    /**
     * 
     * 执行文件的内容,将执行结果挂载在module的exports属性上,并且返回 exports 的引用
     * 
     * function (proxiedSandbox) {
     *     // const wrapper = `with(sandbox) {${code}}` 的结果
     *     with(proxiedSandbox) {
     *         // this.$wrap(source) 的结果
     *         return (function (module, exports, require) {
     *             // 文字内容字符串
     *         }) 
     *     }
     * }
     * 
     * @param {string} module 模块对象
     * @param {string} source 文件源代码
     * 
     * @return {object} exports 的引用
     */

    compile = (module, source) => {
        //TODO
        // 拼接iife
        const iifeString = this.$wrap(source);
        // 创建沙箱的执行环境
        const compiler = this.$runInThisContext(iifeString)({});
        // 注入 module, exports, require 执行
        compiler.call(module, module, module.exports, this.require);
        // 返回 exports 的引用
        return module.exports;
    }

7.实现require的缓存

    constructor(moduleName, source) {
        // ...

        // 缓存
        this.$cacheModule = new Map();
        
        // ...
    }

    /**
     * require
     * 
     * useage: require('./a.js')
     * 
     * @param {string} moduleName  模块的名称,文件路径
     * @param {string} source 文件的源代码,省略了使用node的fs模块,根据规则(一层一层往外找,node_modules,global_node_modules))查找源代码的过程,直接传入文件源代码
     * 
     * @return {object} require 返回的结果就是 exports 的引用
     */
    require = (moduleName, source) => {
        // 每一次 reuqire 都执行文件内容的话,开销太大,所以加缓存
        if(this.$cacheModule.has(moduleName)) {
            // 注意返回的是执行之后的 exports对象
            return this.$cacheModule.get(moduleName).exports;
        }
        
        // ...

        //返回module的exports
        return exports;
    }

完整版代码

    /**
     * 实现一个简单版本的 commonjs 模块加载器,偏浏览器端
     * 
     * 指导准则:COMMONJS 规范
     * 
     * 主要 2 个部分:
     * 
     * 1、模块加载器 (解析文件地址,获得文件源代码;一层一层往外找,node_modules,global_node_modules)
     * 2、模块解析  (执行文件内容,Node 里面是使用了 V8 执行)
     */

    class Module {
        constructor(moduleName, source) {
            // 暴露数据
            this.exports = {};

            //保存一下模块的信息
            this.moduleName = moduleName;

            // 缓存
            this.$cacheModule = new Map();

            // 源代码
            this.$source = source;
        }

        /**
         * require
         * 
         * useage: require('./a.js')
         * 
         * @param {string} moduleName  模块的名称,文件路径
         * @param {string} source 文件的源代码,省略了使用node的fs模块,根据规则(一层一层往外找,node_modules,global_node_modules))查找源代码的过程,直接传入文件源代码
         * 
         * @return {object} require 返回的结果就是 exports 的引用
         */
        require = (moduleName, source) => {
            // 每一次 reuqire 都执行文件内容的话,开销太大,所以加缓存
            if(this.$cacheModule.has(moduleName)) {
                // 注意返回的是执行之后的 exports对象
                return this.$cacheModule.get(moduleName).exports;
            }

            /**
             * 省略加载器查找文件的过程
             * 
             * 否则这里应该执行加载器方法,获取文件源代码
             */

            // 创建模块对象
            const module = new Module(moduleName, source);

            // 执行文件内容
            const exports = this.compile(module, source);

            //缓存
            this.$cacheModule.set(moduleName, module);

            //返回module的exports
            return exports;
        }

        /**
         * 
         * 拼接一个闭包,使用自执行函数 IIFE
         * 
         * @param {string} code 代码字符串
         */

        $wrap = (code) => {
            const wrapper = [
                'return (function (module, exports, require){',
                '\n});'
            ]

            return wrapper[0] + code + wrapper[1];
        }
        /**
         * 简单实现一个能在浏览器中运行的解释器 vm.runInThisContext
         * 目标:
         * 创建一个隔离的沙箱环境,来执行我们的代码字符串
         * 
         * 隔离:1、不能访问闭包的标量,2、不能访问全局的变量,3、只能访问我们传入的变量
         * 
         * 
         * 
         * @param {*} code 代码字符串
         */

        $runInThisContext = (code, whiteList=['console']) => {

            // 使用 with 保证可以通过我们传入的 sandbox 对象取数据
            const wrapper = `with(sandbox) {${code}}`
            // new Function 不能访问闭包
            const func = new Function('sandbox', wrapper);

            return function(sandbox) {
                if(!sandbox || typeof sandbox !== 'object') {
                    throw Error('sandbox parameter must be an object');
                }

                // 代理

                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);
                    }
                });

                return func(proxiedObject);
            }

        }

        /**
         * 
         * 执行文件的内容,将执行结果挂载在module的exports属性上,并且返回 exports 的引用
         * 
         * function (proxiedSandbox) {
         *     // const wrapper = `with(sandbox) {${code}}` 的结果
         *     with(proxiedSandbox) {
         *         // this.$wrap(source) 的结果
         *         return (function (module, exports, require) {
         *             // 文字内容字符串
         *         }) 
         *     }
         * }
         * 
         * @param {string} module 模块对象
         * @param {string} source 文件源代码
         * 
         * @return {object} exports 的引用
         */

        compile = (module, source) => {
            //TODO
            // 拼接iife
            const iifeString = this.$wrap(source);
            // 创建沙箱的执行环境
            const compiler = this.$runInThisContext(iifeString)({});
            // 传入 module, exports, require 执行
            compiler.call(module, module, module.exports, this.require);
            // 返回 exports 的引用
            return module.exports;
        }
    }

    const m = new Module();

    // a.js
    const sourceCodeFromA = `
        const b = require('b.js', 'exports.action = function() { console.log("successfully 🎉")}');

        b.action();
    `

    m.require('a.js', sourceCodeFromA);
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值