Webpack 打包基本原理

141 阅读4分钟

Webpack 是一个现代 JavaScript 应用程序的静态模块打包器。它的核心功能是将各种资源(JavaScript、CSS、图片等)作为模块进行打包,并生成优化后的输出文件。本文将结合具体例子和部分经典源码,详细讲解 Webpack 的打包原理。

基本概念

在深入了解打包原理之前,我们需要先了解一些基本概念:

  • Entry(入口):Webpack 从一个或多个入口文件开始构建依赖图。
  • Output(输出):Webpack 将打包后的文件输出到指定的目录。
  • Loaders(加载器):Webpack 使用加载器来处理非 JavaScript 文件(如 CSS、图片)。
  • Plugins(插件):Webpack 插件用于执行范围更广的任务(如打包优化、资源管理)。

打包流程

Webpack 的打包流程可以分为以下几个步骤:

  1. 初始化:读取配置文件,合并默认配置,初始化 Compiler 对象。
  2. 构建模块依赖图:从入口文件开始,递归解析所有依赖模块,生成依赖图。
  3. 转换模块:使用 Loaders 转换模块内容。
  4. 生成代码块:将模块分组为代码块(Chunks)。
  5. 输出文件:将代码块转换为文件,并输出到指定目录。

image.png

具体例子

我们通过一个简单的例子来演示 Webpack 的打包过程。

1. 项目结构
my-webpack-project/
├── src/
│   ├── index.js
│   └── module.js
├── dist/
├── webpack.config.js
└── package.json
2. 代码示例

src/index.js:

import { greet } from './module';

greet('World');

src/module.js:

export function greet(name) {
    console.log(`Hello, ${name}!`);
}

webpack.config.js:

const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            }
        ]
    }
};
3. 打包过程

运行以下命令进行打包:

npm install webpack webpack-cli babel-loader @babel/core @babel/preset-env --save-dev
npx webpack

Webpack 源码解析

我们通过分析部分经典源码,进一步理解 Webpack 的打包原理。

1. 初始化

Webpack 通过 webpack-cli 读取配置文件,并初始化 Compiler 对象:

const webpack = require('webpack');
const config = require('./webpack.config.js');

const compiler = webpack(config);
2. 构建模块依赖图

Compiler 对象调用 run 方法,开始构建模块依赖图:

compiler.run((err, stats) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log(stats.toString({
        chunks: false,
        colors: true
    }));
});

run 方法中,Webpack 从入口文件开始,递归解析所有依赖模块:

const entry = './src/index.js';
const modules = [];

function buildModule(filename) {
    const content = fs.readFileSync(filename, 'utf-8');
    const dependencies = parseDependencies(content);
    modules.push({ filename, content, dependencies });

    dependencies.forEach(dep => {
        buildModule(dep);
    });
}

buildModule(entry);

关于parseDependencies的基本原理,可见 juejin.cn/post/743622…

3. 转换模块

Webpack 使用 Loaders 转换模块内容,例如使用 babel-loader 转换 ES6 代码:

const babel = require('@babel/core');

function transform(content) {
    return babel.transform(content, {
        presets: ['@babel/preset-env']
    }).code;
}
4. 生成代码块

Webpack 将模块分组为代码块(Chunks),并生成最终的输出文件:

const bundle = modules.map(module => {
    return `
        // ${module.filename}
        function(module, exports, require) {
            ${module.content}
        }
    `;
}).join(',');

const result = `
    (function(modules) {
        function require(filename) {
            const fn = modules[filename];
            const module = { exports: {} };
            fn(module, module.exports, require);
            return module.exports;
        }
        require('${entry}');
    })({${bundle}})
`;

fs.writeFileSync('./dist/bundle.js', result);

自己实现一个简单的打包器

为了更好地理解 Webpack 的打包原理,我们可以尝试自己实现一个简单的打包器。

1. 项目结构
my-bundler/
├── src/
│   ├── index.js
│   ├── add.js
│   └── minus.js
├── dist/
├── bundle.js
└── package.json
2. 代码示例

src/add.js:

export default (a, b) => {
    return a + b;
}

src/minus.js:

export const minus = (a, b) => {
    return a - b;
}

src/index.js:

import add from './add.js';
import { minus } from './minus.js';

const sum = add(1, 2);
const division = minus(2, 1);
console.log('sum>>>>>', sum);
console.log('division>>>>>', division);

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>demo</title>
</head>
<body>
    <script src="./dist/bundle.js"></script>
</body>
</html>
3. 实现打包器

我们首先在项目根目录下创建 bundle.js,这个文件用来对我们刚刚写的模块化 JS 代码文件进行打包。

3.1 处理单个模块

我们需要读取文件内容,并将其转换为 AST(抽象语法树),然后提取依赖关系并转换代码。

const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');

const getModuleInfo = file => {
    const body = fs.readFileSync(file, 'utf-8');
    const ast = parser.parse(body, {
        sourceType: 'module'
    });
    const deps = {};
    traverse(ast, {
        ImportDeclaration({ node }) {
            const dirname = path.dirname(file);
            const absPath = './' + path.join(dirname, node.source.value);
            deps[node.source.value] = absPath;
        }
    });
    const { code } = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]
    });
    return { file, deps, code };
};
3.2 递归获取所有模块的信息

我们需要递归地获取所有模块的信息,构建依赖图。

const parseModules = file => {
    const depsGraph = {};
    const entry = getModuleInfo(file);
    const temp = [entry];
    for (let i = 0; i < temp.length; i++) {
        const item = temp[i];
        const deps = item.deps;
        if (deps) {
            for (const key in deps) {
                if (deps.hasOwnProperty(key)) {
                    temp.push(getModuleInfo(deps[key]));
                }
            }
        }
    }
    temp.forEach(moduleInfo => {
        depsGraph[moduleInfo.file] = {
            deps: moduleInfo.deps,
            code: moduleInfo.code
        };
    });
    return depsGraph;
};
3.3 生成最终代码

我们需要根据依赖图生成最终的代码,并写入到 dist/bundle.js 文件中。

const bundle = file => {
    const depsGraph = JSON.stringify(parseModules(file));
    return `(function(graph){
        function require(file) {
            var exports = {};
            function absRequire(relPath){
                return require(graph[file].deps[relPath]);
            }
            (function(require, exports, code){
                eval(code);
            })(absRequire, exports, graph[file].code);
            return exports;
        }
        require('${file}');
    })(${depsGraph})`;
};

const build = file => {
    const content = bundle(file);
    fs.mkdirSync('./dist');
    fs.writeFileSync('./dist/bundle.js', content);
};

build('./src/index.js');