Skip to content

webpack的编译&构建 #4

@ixlei

Description

@ixlei

webpack的编译&构建

上一篇文章webpack详解中介绍了webpack基于事件流编程,是个高度的插件集合,整体介绍了webpack 的编译流程。本文将单独聊一聊最核心的部分,编译&构建。

webpack的编译

重要的构建节点

webpack的构建中总会经历如下几个事件节点。

  • before-run 清除缓存
  • run 注册缓存数据钩子
  • compile 开始编译
  • make 从入口分析依赖以及间接依赖模块,创建模块对象
  • build-module 模块构建
  • seal 构建结果封装, 不可再更改
  • after-compile 完成构建,缓存数据
  • emit 输出到dist目录

其中make是整个构建中最核心的部分编译,通过模块工厂函数创建模块,然后对模块进行编译。

在make钩子的编译

Alt text
上图中提到的*ModuleFactory是指模块工厂函数,之所以会有模块工厂这样的函数,还要从webpack中entry的配置说起,在webpack的配置项中entry支持如下类型:

  • 字符类型string
  • 字符数组类型[string]
  • 多页面对象key-value类型object { <key>: string | [string] }
  • 也支持一个函数,返回构建的入口(function: () => string | [string] | object { <key>: string | [string] })

为了处理以后不同类型的入口模块,所以就需要个模块工厂来处理不同的入口模块类型。

  • singleEntry: string|object { <key>: string }
  • multiEntry: [string]|object { <key>: [string] }
  • dynamicEntry: (function: () => string | [string] | object { <key>: string | [string] })

上图中为了简单说明构建的流程,就以最直接的singleEntry类型说起,对于此类入口模块,webpack均使用NormalModuleFactory来创建模块,这个创建的模块的类型叫NormalModule,在NormalModule中实现了模块的构建方法build,使用runLoaders对模块进行加载,然后利用进行解析,分析模块依赖,递归构建。

构建封装seal

到构建封装阶段时候,代码构建已经完毕,但是如何将这些代码按照依赖引用逻辑组织起来,当浏览器将你构建出来的代码加载到浏览器的时候,仍然能够正确执行。在webpack中通过Manifest记录各个模块的详细要点,通过Runtime来引导,加载执行模块代码,特别是异步加载。

###Runtime
如上所述,我们这里只简略地介绍一下。runtime,以及伴随的 manifest 数据,主要是指:在浏览器运行时,webpack 用来连接模块化的应用程序的所有代码。runtime 包含:在模块交互时,连接模块所需的加载和解析逻辑。包括浏览器中的已加载模块的连接,以及懒加载模块的执行逻辑。

###Manifest
那么,一旦你的应用程序中,形如 index.html 文件、一些 bundle 和各种资源加载到浏览器中,会发生什么?你精心安排的 /src 目录的文件结构现在已经不存在,所以 webpack 如何管理所有模块之间的交互呢?这就是 manifest 数据用途的由来……
当编译器(compiler)开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 "Manifest",当完成打包并发送到浏览器时,会在运行时通过 Manifest 来解析和加载模块。无论你选择哪种模块语法,那些 import 或 require 语句现在都已经转换为 webpack_require 方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够查询模块标识符,检索出背后对应的模块。

定义了一个立即执行函数,声明了__webpack_require__,对各种模块进行加载。

(function(modules) { // webpackBootstrap
    var installedModules = {}; // cache module
    function __webpack_require__(moduleId) { // 模块加载
        // Check if module is in cache
        if (installedModules[moduleId]) {
            return installedModules[moduleId].exports;
        }
        // Create a new module (and put it into the cache)
        var module = installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {}
        };
        // Execute the module function
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        // Flag the module as loaded
        module.l = true;
        // Return the exports of the module
        return module.exports;
    }

    // expose the modules object (__webpack_modules__)
    __webpack_require__.m = modules;

    // expose the module cache
    __webpack_require__.c = installedModules;

    // define getter function for harmony exports
    __webpack_require__.d = function(exports, name, getter) {
        if (!__webpack_require__.o(exports, name)) {
            Object.defineProperty(exports, name, {
                configurable: false,
                enumerable: true,
                get: getter
            });
        }
    };

    // getDefaultExport function for compatibility with non-harmony modules
    __webpack_require__.n = function(module) {
        var getter = module && module.__esModule ?
            function getDefault() { return module['default']; } :
            function getModuleExports() { return module; };
        __webpack_require__.d(getter, 'a', getter);
        return getter;
    };

    // Object.prototype.hasOwnProperty.call
    __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

    // __webpack_public_path__
    __webpack_require__.p = "";

    // Load entry module and return exports
    return __webpack_require__(__webpack_require__.s = 0);
})([/**modules*/])

上面提到的代码片段便是webpack构建后在浏览器中执行的引导代码。也就是上面提到的runtime。它是个立即执行函数,那么入参modules便是上面的Manifest,组织各个模块的依赖逻辑。

(function(modules){
	// ...
	// runtime
	function __webpack_require__(moduleId) {
		// 加载逻辑
	}
	// ...
})([function (module, exports, __webpack_require__) {

    var chunk1 = __webpack_require__(1);
    var chunk2 = __webpack_require__(2);
   
}, function (module, exports, __webpack_require__) {

    __webpack_require__(2);
    var chunk1 = 1;
    exports.chunk1 = chunk1;
}, function (module, exports) {

    var chunk2 = 1;
    exports.chunk2 = chunk2;
}])

上面说到了runtimemanifest就是在seal阶段注入的

class Compilation extends Tapable {
   
    seal(callback) {
        this.hooks.seal.call();
        // ...
        if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
            this.hooks.beforeChunkAssets.call();
            this.createChunkAssets();
        }
        // ...
    }
  
    createChunkAssets() {
	    // ...
		for (let i = 0; i < this.chunks.length; i++) {
			const chunk = this.chunks[i];
			// ...
				const template = chunk.hasRuntime() 
					? this.mainTemplate
					: this.chunkTemplate; // 根据是否有runTime选择模块,入口文件是true, 需要异步加载的文件则没有
				const manifest = template.getRenderManifest({ 
					// 生成manifest
					chunk,
					hash: this.hash,
					fullHash: this.fullHash,
					outputOptions,
					moduleTemplates: this.moduleTemplates,
					dependencyTemplates: this.dependencyTemplates
				}); // [{ render(), filenameTemplate, pathOptions, identifier, hash }]
				// ...
    }
}

通过template最后将代码组织起来,上面看到的构建后的代码就是mainTemplate生成的。

写在最后

通过template生成最后代码,构建已经完成,接下来就是将代码输出到dist 目录。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions