从0实现破烂版webpack

332 阅读3分钟

webpack是模块化打包工具,可以加载多种资源的模块,我们这里只实现加载js模块的功能(主要其他的我不会)。

实现commonjs模块加载

先看下流程图,这是我们要实现的功能,把main.js模块依赖的子模块打包到bundle.js ,最后执行bundle文件就能访问到各个模块依赖的资源。


首先先定义一下我们的配置文件

const path = require('path')
module.exports = {
    entry:"./main.js",
    output: {
        path:path.resolve(__dirname,"dist"),
        filename: "bundle.js"
    }
}

接着实现构建的逻辑

收集modules依赖

首先我们需要通过入口文件分析依赖的其他模块,然后有子模块继续进行分析,并定义一个modules收集filename和module的关系,方便我们在执行代码的时候require(filename)能找到对应的module。

但分析依赖的过程需要我们借助babel的  transformFileSync可以把js代码转成AST树,然后通过traverse遍历ast树找到require依赖的子模块。

    getAstAndCode(path) {
        let { code, ast } = transformFileSync(path)
        return {
            ast,
            code
        }
    },
    getDependencies(ast) {
        let dependencies = []
        traverse(ast, {
            CallExpression: ({ node: { callee, arguments } }) => {
                if (callee.name === "require") {
                    dependencies.push(arguments[0].value)
                }
            }
        })
        return dependencies
    }

有了dependencies后,我们就可以遍历生成modules(filename到modue的映射)

    buildModule(dependencies) {
        let modules = {}
        while (dependencies.length) {
            let filename = dependencies.pop()
            let absolutePath = path.resolve(__dirname, '../src', filename)
            const {ast,code} = getAstAndCode(absolutePath)
            dependencies.push(...getDependencies(ast))
            modules[filename] = code
        }
        return modules
    }

生成bundle

有了modules我们就可以去生成bundle了,bundle.js是我们最终要执行的文件,怎样把模块都通过正确的方式调用呢?

首先我们思考一个问题。在main.js中我们require去依赖其他的模块然后拿到一个对象,这很像我们的函数,只要我们传进去一个路径就返回我们想要的结果,但是根据commonjs 模块的规范,想要的结果不是通过return返回的而是在module.export上。所以我们可以对函数进行包装,在子模块加载的时候,传进去一个参数当做module.export,在子模块中通过module.export收集要返回的内容,最后在包装函数中把收集好的module.export返回。

    emitFile(modules) {
        const code = `(function(modules){
            function require(filename){
                const code = modules[filename]
                let module = {};
                (function(module){
                    eval(code)
                })(module)
                return module.exports
            }
            require('${this.entry}')
        })(${ JSON.stringify(modules)})`
        let output = path.resolve(this.output.path, this.output.filename)
        fs.exists(this.output.path, (exists) => {
            if (!exists) {
                fs.mkdirSync(this.output.path)
            }
            fs.writeFileSync(output, code, 'utf-8')
        });
    }

到这一个简单实现commonjs模块打包已经完成了。当然我不能容忍不能加载我们熟悉的ejs模块

实现ejs模块加载

首先得配置一下我们.babellr使用babel-presets-env翻译es6语法。

{
    "presets":["env"]
}

我们在看一下es6的模块翻译成es5是个啥样,babel转换地址:www.babeljs.cn/repl



我们发现import转成了require我们就不需要特别处理bundle的方法了,只需要再支持exports方法记录返回结果就可以了

    emitFile(modules) {
        const code = `(function(modules){
            function require(filename) {
                const code = modules[filename]
                let module = {};
                (function (module,exports) {  //增加exports收集结果
                    eval(code)
                })(module,module)
                return module.exports||module
            }
            require('${this.entry}')
        })(${ JSON.stringify(modules)})`
        ...
    }

简单的处理,支持了es6模块的加载

完整的代码 github:https://github.com/13866368297/webpack.git

到这webpack模块加载的功能就实现了,还是那句话,主要让大家了解下webpack加载模块的大致过程,具体的实现细节就不扣了。

看到这的小伙伴谢谢大家的支持!!!