webpack多入口打包文件

256 阅读6分钟

一、webpack如何多入口打包文件

// entry: string | array | object
module.exports = {
  entry: {
    main: './src/app.js',
    vendor: './src/vendor.js',
  },
output: {
    filename: '[name].[contenthash].bundle.js',
  },
};

二、多入口打包优势

(1)依赖关系优化

通过分离入口点,webpack可以更智能地处理共享依赖,使得公共模块单独打包,入口点只包含专属逻辑,避免重复依赖

// 场景:app.js 和 vendor.js 都使用 lodash 
optimization: { 
    splitChunks: { 
        chunks: 'all' // 自动提取公共依赖 
    } 
}

(2)缓存策略优化

多入口文件支持更精细化的缓存策略,比如vendor.js这种低频修改的文件可以长期缓存,app.js这种高频修改的文件可以短周期缓存,使得热更新更快

  • 按需加载的基础,多入口架构为后续的代码拆分和动态导入奠定了基础
// 后续可轻松改为动态导入
import(/* webpackChunkName: "vendor" */ './vendor.js')

(3)按需加载和动态导入的基础

// 后续可轻松改为动态导入
import(/* webpackChunkName: "vendor" */ './vendor.js')

三、多入口打包使用场景

(1) 主应用+第三方库分离

​项目结构:​

src/
├── app.js        // 业务逻辑
├── vendor.js     // 第三方库
└── utils.js      // 共享工具

​打包策略:​

  1. 将稳定依赖(如React、Lodash)放入vendor
  2. 高频变动的业务逻辑放入app
  3. 共享工具自动提取

​打包结果特征:​

  • vendor包变化频率低 → 适合持久缓存
  • app包体积精简 → 加快热更新速度

(2) 多页面应用(MPA)架构

对于传统多页面项目:

entry: {
  home: './src/home/index.js',
  about: './src/about/index.js',
  contact: './src/contact/index.js'
}

配合 html-webpack-plugin 自动生成各页面的HTML:

plugins: [
  new HtmlWebpackPlugin({
    template: './home.html',
    chunks: ['home']  // 只注入home相关的JS
  })
]

(3)微前端架构适配

在多团队协作的微前端场景中:

entry: {
  main: './src/main.js',
  'team-a': './src/team-a/bootstrap.js',
  'team-b': './src/team-b/bootstrap.js'
}

优势体现:

  • 独立团队拥有自己的入口
  • 共享依赖自动优化
  • 独立部署能力

四、通过插件分析入口依赖

性能监控集成

结合webpack-bundle-analyzer分析各入口:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static'
    })
  ]
};

五、webpack打包的完整过程

核心打包流程

输入 (源代码)  -> 依赖解析与模块转换 -> 构建依赖图 -> 封装与代码生成 -> 输出 (Bundle)

下面我们详细拆解每一步。


1. 初始化:读取配置与创建 Compiler

  • webpack 启动后,首先会读取你的 webpack.config.js 和命令行参数,合并得到最终的配置。
  • 然后,初始化 Compiler 对象。Compiler 是 webpack 的编译管理器,是贯穿整个打包周期的核心对象。它包含了完整的配置信息,并暴露了生命周期钩子。

javascript

// 伪代码,示意过程
const webpack = (options, callback) => {
  // 1. 创建 compiler 对象
  const compiler = new Compiler(options.context);
  compiler.options = options;

  // 2. 注册所有配置的插件
  if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      // 调用插件的 apply 方法,将 compiler 实例传入
      plugin.apply(compiler);
    }
  }

  // 3. 触发 environment, initialize 等生命周期钩子
  // ...
  return compiler;
};

2. 编译与构建模块图

这是最复杂也是最核心的一步。Compiler 会找到一个入口文件(如 ./src/index.js),开始编译。

a. 模块解析

  • 读取入口文件内容。
  • 调用 AST(抽象语法树)解析器(默认是 acorn)将源代码转换成 AST,便于分析。

b. 依赖收集

  • 遍历 AST,找到所有的 importrequiredefine 等模块导入语句。
  • 根据导入语句的路径,使用 enhanced-resolve 库解析出模块的绝对路径。这就是为什么 webpack 能处理各种模块规范(ES6, CommonJS, AMD)和文件类型。

c. 模块转换

  • 对于每一个解析出来的模块,根据配置中的 module.rules(loader 规则)进行处理。

  • Loader 的本质是一个函数,它接收源文件内容,返回转换后的内容。这个过程是链式的。

  • 例如,对于 ./style.css

    1. css-loader 处理 @import 和 url(),将其转换为 JS 可执行的模块。
    2. style-loader 接收 css-loader 返回的 JS 字符串,生成一段 JS 代码:通过 DOM API 将 <style> 标签插入到 HTML 中。

d. 构建依赖图

  • 递归地重复 a, b, c 步骤,处理入口文件的所有依赖,以及依赖的依赖,直到项目中所有用到的模块都被处理完毕。
  • 最终形成一个依赖图,其中节点是模块,边是模块间的依赖关系。

text

// 依赖图示例
Entry: ./src/index.js
  |-- ./src/utils/math.js
  |-- ./src/components/Button.jsx
      |-- ./src/styles/button.css
      |-- ./node_modules/react/index.js

3. 封装与代码生成:生成 Chunk

在得到完整的依赖图后,Compiler 进入 seal 阶段。

  • 根据配置的 入口点和代码分割规则(如 SplitChunksPlugin),将依赖图中的模块分到不同的 Chunk 中。
  • 一个 Chunk 对应一个或多个输出文件(如 .js.css)。
  • 然后,webpack 开始为每个 Chunk 生成最终的代码。它使用自己的模板系统来生成运行时代码。

关键:webpack 如何让模块在浏览器中运行?

webpack 实现了一个模块打包器 runtime。它不会直接执行 import/require,而是将所有模块封装在一个函数作用域内,并自己实现了一套模块加载和执行逻辑。

生成的 Bundle 代码结构大致如下:

javascript

// 一个简化版的 bundle.js
(function(modules) { // webpackBootstrap 自执行函数
  // 1. 模块缓存
  var installedModules = {};

  // 2. webpack 自己实现的 require 函数 (__webpack_require__)
  function __webpack_require__(moduleId) {
    // 检查缓存
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // 创建新模块并缓存
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false, // loaded?
      exports: {}
    };
    // 执行模块函数
    // 这里的 modules[moduleId] 就是下面传入的模块定义
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    module.l = true; // 标记为已加载
    return module.exports;
  }

  // 3. 从入口模块开始执行
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})({
  // 4. 传入的 modules 对象:一个包含所有模块定义的 Map
  // Key: 模块ID (通常是路径)
  // Value: 一个被包装的函数,包含了模块的源代码
  "./src/index.js": (function(module, exports, __webpack_require__) {
    // 这是由 index.js 转换而来的代码
    // 原来的 `import utils from './utils/math.js'` 被替换了
    const utils = __webpack_require__("./src/utils/math.js");
    console.log('Hello Webpack');
  }),
  "./src/utils/math.js": (function(module, exports) {
    // 模块自己的代码
    module.exports = {
      add: (a, b) => a + b
    };
  })
});

4. 输出 Assets

  • 在确定好输出内容后,根据配置的 output 路径和文件名,将 Chunk 转换成独立的文件。
  • 在这个过程中,还会进行文件内容优化(如代码压缩、Tree Shaking 等)。
  • 最后,通过 Node.js 的 fs 模块将文件写入到指定的输出目录。

核心原理总结

步骤核心工作关键工具/概念
1. 初始化合并配置,创建 Compiler,注册插件Compiler, 生命周期钩子
2. 编译从入口开始,递归解析依赖,构建模块图AST (acorn), enhanced-resolveLoader
3. 生成根据依赖图和规则,生成 Chunk 和最终代码ChunkTemplate (运行时代码)
4. 输出将 Chunk 写入文件系统,生成 Bundlefs 模块

一句话总结:Webpack 通过 AST 静态分析代码中的模块依赖关系,将所有模块包装成一个或多个自执行函数,并通过自己实现的 __webpack_require__ 方法在浏览器环境中模拟 CommonJS 模块系统,最终将所有模块组织成一个或多个 Bundle 文件。