# 分析webpack打包原理

webpack的目的是通过打包生成一个依赖图,然后执行。

所以我们执行的目的就是形成模块依赖图,然后执行主文件。

# 新建模版文件

入口文件src/index.js

import bar from './bar';

bar();

src/bar.js

export default function bar() {
  document.write('hello webpack!')
}

配置文件webpack.config.js

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

打包文件 bundle.js 通过 node bundle.js会执行打包

const options = require('./webpack.config')

# 执行流程

bundle.js

const options = require('./webpack.config')
const Compiler = require('./lib/compiler')

new Compiler(options).run(); // 执行编译 目标是返回output指定的文件

compiler.js

const {getAst, getDependencies, getCode} = require('./parser');
const fs = require('fs')
const path = require('path')
module.exports = class Compiler {
    // 需要持有 方便后面用
    constructor(options) {
        const {entry, output} = options;
        this.entry = entry;
        this.output = output;
        this.moduleInfo = [];
    }
    run() {
        let info= this.build(this.entry);
        this.moduleInfo.push(info);
        // 广度优先搜索 所有指定的模块都对应上
        for(let i = 0; i < this.moduleInfo.length; i++) {
            const {dependencies} = this.moduleInfo[i];
            if(dependencies) {
                for(let j in dependencies) {
                    this.moduleInfo.push(this.build(dependencies[j]));
                }
            }
        }
        // 数组转换为obj
        const obj = {};
        this.moduleInfo.forEach(item => {
            obj[item.filename] = {
                dependencies: item.dependencies,
                code: item.code
            }
        })
        this.file(obj);
    }
    // 分析
    build(filename) {
        let ast = getAst(filename);
        let dependencies = getDependencies(ast, filename);
        let code = getCode(ast);
        return {
            code,
            dependencies,
            filename
        }
    }
    // 文件生成
    file(code) {
        const filePath = path.join(this.output.path, this.output.filename);
        const newCode = JSON.stringify(code);
        const bundle = `
        (function(graph){
            function require(module) {
                function localrequire(relativePath) {
                    return require(graph[module].dependencies[relativePath]);
                }
                var exports = {};
                (function(require, exports, code) {
                    eval(code)
                })(localrequire, exports, graph[module].code)
                return exports;
            }
            require('${this.entry}')
        })(${newCode})`

        fs.writeFileSync(filePath, bundle, 'utf-8');
    }
}

parser.js

const fs = require('fs')
const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const path = require('path')
const babel = require('@babel/core')
module.exports = {
    getAst(filename) {
        const content = fs.readFileSync(filename, 'utf-8');
        // 获取抽象语法树
        const Ast = parser.parse(content, {
            sourceType: "module"
        });
        return Ast;
    },
    getDependencies(Ast, filename) {
        const dependencies = {};
        // 解析语法树 获取依赖的模块
        traverse(Ast, {
            ImportDeclaration({node}) {
                const newPath = path.join(path.dirname(filename), node.source.value);
                dependencies[node.source.value] = newPath;
            }
        })
        return dependencies;
    },
    getCode(Ast) {
        const { code } = babel.transformFromAst(Ast, null, {
            presets: ["@babel/preset-env"]
        });
        return code;
    }
}

这里用到了3个模块包都是由babel提供的,需要使用 npmyarn 安装

  • @babel/parser 解析js返回ast
  • @babel/traverse 解析语法树 获取依赖的模块
  • @babel/core 解析语法树 编译成指定版本的代码

最后输出的文件 dist/main.js

(function(graph){
    function require(module) {
        function localrequire(relativePath) {
            return require(graph[module].dependencies[relativePath]);
        }
        var exports = {};
        (function(require, exports, code) {
            eval(code)
        })(localrequire, exports, graph[module].code)
        return exports;
    }
    require('./src/index.js')
})({"./src/index.js":{"dependencies":{"./hello.js":"src/hello.js"},"code":"\"use strict\";\n\nvar _hello = _interopRequireDefault(require(\"./hello.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\n(0, _hello[\"default\"])();"},"src/hello.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = bar;\n\nfunction bar() {\n  document.write('hello webpack!');\n}"}})