Webpack的打包原理

Webpack的打包原理

Webpack简介

Webpack是一个打包模块化Javascript的工具,在Webpack里一切文件皆模块,通过Loader转换文件,通过Plugin注入钩子,最后输出由多个模块组合成的文件。Webpack专注于构建模块化项目。 ——《深入浅出Webpack》

其官网的首页图很形象地展示了Webpack的定义 如下图所示

Webpack的定义

分析打包后的文件 (webpack版本: 4.41.4)

配置文件: webpack.config.js

const path = require('path');

module.exports = {
    
    
  mode: 'development',
  devtool: 'none',
  entry: './src/index.js',
  output: {
    
    
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  }
}

入口文件: ./src/index.js

let title = require('./title');
console.log(title);

被入口文件导入的文件: ./src/title.js

module.exports = '标题';

打包后的文件: main.js

(function (modules) {
    
    
  // 模块缓存
  var installedModules = {
    
    };

  function __webpack_require__(moduleId) {
    
    
    // 检查模块是否在缓存中
    if (installedModules[moduleId]) {
    
    
      return installedModules[moduleId].exports;
    }
    // 创建一个新的模块,并放到缓存中
    var module = (installedModules[moduleId] = {
    
    
      i: moduleId,
      l: false,
      exports: {
    
    },
    });
    // 执行模块函数
    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );
    // 模块已加载成功
    module.l = true;
    // 返回此模块的导出对象
    return module.exports;
  }

  // 把modules对象放在__webpack_require__.m属性上
  __webpack_require__.m = modules;

  // 把模块的缓存对象放在__webpack_require__.c属性上
  __webpack_require__.c = installedModules;

  // 导出定义getter函数
  __webpack_require__.d = function (exports, name, getter) {
    
    
    if (!__webpack_require__.o(exports, name)) {
    
    
      Object.defineProperty(exports, name, {
    
     enumerable: true, get: getter });
    }
  };

  // 声明es6模块
  __webpack_require__.r = function (exports) {
    
    
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
    
    
      Object.defineProperty(exports, Symbol.toStringTag, {
    
     value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', {
    
     value: true });
  };

  // 包装es6模块
  __webpack_require__.t = function (value, mode) {
    
    
    if (mode & 1) value = __webpack_require__(value);
    if (mode & 8) return value;
    if (mode & 4 && typeof value === 'object' && value && value.__esModule)
      return value;
    var ns = Object.create(null);
    __webpack_require__.r(ns);
    Object.defineProperty(ns, 'default', {
    
     enumerable: true, value: value });
    if (mode & 2 && typeof value != 'string')
      for (var key in value)
        __webpack_require__.d(
          ns,
          key,
          function (key) {
    
    
            return value[key];
          }.bind(null, key)
        );
    return ns;
  };

  // 获取默认导出的函数,为了兼容非harmony模块
  __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.prototye.hasOwnProperty
  __webpack_require__.o = function (object, property) {
    
    
    return Object.prototype.hasOwnProperty.call(object, property);
  };

  // 把公开访问路径放在__webpack_require__.p属性上
  __webpack_require__.p = '';

  // 加载入口模块并且返回exports (__webpack_require__.s: 指定入口模块ID)
  return __webpack_require__((__webpack_require__.s = './src/index.js'));
})({
    
    
  './src/index.js': function (module, exports, __webpack_require__) {
    
    
    let title = __webpack_require__('./src/title.js');
    console.log(title);
  },
  './src/title.js': function (module, exports) {
    
    
    module.exports = '标题';
  },
});

打包后文件中常用的几个方法

__webpack_require__.r (声明es6模块)

__webpack_require__.r = function (exports) {
    
    
    /**
     * Symbol.toStringTag 是一个内置 symbol,它通常作为对象的属性键使用
     * 对应的属性值应该为字符串类型,这个字符串用来表示该对象的自定义类型标签
     * 通常只有内置的 Object.prototype.toString() 方法会去读取这个标签并把它包含在自己的返回值里
     */
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
    
    
      Object.defineProperty(exports, Symbol.toStringTag, {
    
     value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', {
    
     value: true });
  };
  
  -------------------------------------------------------------------------------------------
  
  实际可以理解成如下代码:
  
  __webpack_require__.r = function (exports) {
    
    
      exports[Symbol.toStringTag] = 'Module';
      exports.__esModule = true;
    }
    
 let exports = {
    
    };
 __webpack_require__.r(exports);
 console.log(Object.prototype.toString.call(exports));
 // 控制台打印结果:"[object Module]"

__webpack_require__.o (Object.prototye.hasOwnProperty)

// 实际就是对Object.prototype.hasOwnProperty.call做了一个包装
__webpack_require__.o = function (object, property) {
    
    
    return Object.prototype.hasOwnProperty.call(object, property);
};

__webpack_require__.d (导出定义getter函数)

__webpack_require__.d = function (exports, name, getter) {
    
    
    // 检查形参name是否是形参exports的私有属性
    if (!__webpack_require__.o(exports, name)) {
    
    
      Object.defineProperty(exports, name, {
    
     enumerable: true, get: getter });
    }
};

__webpack_require__.n (获取默认导出的函数,为了兼容非harmony模块)

__webpack_require__.n = function (module) {
    
    
var getter =
  module && module.__esModule
    ? function getDefault() {
    
     // ES6 modules
        return module['default'];
      }
    : function getModuleExports() {
    
     // common.js
        return module;
      };
__webpack_require__.d(getter, 'a', getter);
return getter;
};

__webpack_require__.t (包装es6模块)

// 懒加载的时候用的
// require.t 一般来说核心用法是用来把一个任意模块都变成一个es模块
// import('./xxx').then(result => console.log(result))..不管懒加载的是一个common.js还是es6模块,都会变成es6模块的格式

// create a fake namespace object 创建一个模拟的命名空间对象,不管什么模块都转成es6模块
// mode & 1: value is a module id, require it value是一个模块ID,需要用require加载
// mode & 2: merge all properties of value into the ns // 将值的所有属性合并到ns中
// mode & 4: return value when already ns object 如果已经是ns对象了,则直接返回
// mode & 8 | 1: behave like require 等同于require方法
__webpack_require__.t = function (value, mode) {
    
    
    // 1转为二进制 0b0001
    if (mode & 1) value = __webpack_require__(value);
    // 8转为二进制 0b1000
    if (mode & 8) return value;
    // 4转为二进制 0b0100 value已经是es模块了,可以直接返回
    if (mode & 4 && typeof value === 'object' && value && value.__esModule)
      return value;
    var ns = Object.create(null);
    __webpack_require__.r(ns); // ns.__esModule=true
    Object.defineProperty(ns, 'default', {
    
     enumerable: true, value: value });
    if (mode & 2 && typeof value != 'string')
      // 把值拷贝到ns上
      for (var key in value)
        __webpack_require__.d(
          ns,
          key,
          function (key) {
    
    
            return value[key];
          }.bind(null, key)
        );
    return ns;
};

harmony

顾名思义,harmony中文是和谐的意思。webpack_require.n方法就是为了兼容非harmony的模块。

harmony


common.js加载 common.js

入口文件: ./src/index.js

let title = require('./title');
console.log(title.name);
console.log(title.age);

被入口文件导入的文件: ./src/title.js

exports.name = 'title_name';
exports.age = 'title_age';

打包后的文件: main.js (自执行函数实参部分)

{
    
    
    './src/index.js': function (module, exports, __webpack_require__) {
    
    
      let title = __webpack_require__('./src/title.js');
      console.log(title.name);
      console.log(title.age);
    },
    './src/title.js': function (module, exports) {
    
    
      exports.name = 'title_name';
      exports.age = 'title_age';
    }
}

common.js加载 ES6 modules

入口文件: ./src/index.js

let title = require('./title');
console.log(title);

被入口文件导入的文件: ./src/title.js

export default 'title_name'; // 默认导出
export const age = 'title_age'; // 单个导出

打包后的文件: main.js (自执行函数实参部分)

{
    
    
    './src/index.js': function (module, exports, __webpack_require__) {
    
    
      let title = __webpack_require__('./src/title.js');
      console.log(title);
    },
    './src/title.js': function (module, __webpack_exports__, __webpack_require__) {
    
    
      'use strict';
      __webpack_require__.r(__webpack_exports__);
      __webpack_require__.d(__webpack_exports__, 'age', function () {
    
    
        return age;
      });
      __webpack_exports__['default'] = 'title_name'; // 默认导出
      const age = 'title_age'; // 单个导出
    }
}

ES6 modules 加载 ES6 modules

入口文件: ./src/index.js

import name, {
    
     age } from './title';
console.log(name);
console.log(age);

被入口文件导入的文件: ./src/title.js

export default 'title_name'; // 默认导出
export const age = 'title_age'; // 单个导出

打包后的文件: main.js (自执行函数实参部分)

{
    
    
    './src/index.js': function (module, __webpack_exports__, __webpack_require__) {
    
    
      'use strict';
      __webpack_require__.r(__webpack_exports__);
      var _title__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
        './src/title.js'
      );
      console.log(_title__WEBPACK_IMPORTED_MODULE_0__['default']);
      console.log(_title__WEBPACK_IMPORTED_MODULE_0__['age']);
    },

    './src/title.js': function (module, __webpack_exports__, __webpack_require__) {
    
    
      'use strict';
      __webpack_require__.r(__webpack_exports__);
      __webpack_require__.d(__webpack_exports__, 'age', function () {
    
    
        return age;
      });
      __webpack_exports__['default'] = 'title_name'; // 默认导出
      const age = 'title_age'; // 单个导出
    }
}

ES6 modules 加载 common.js

入口文件: ./src/index.js

import name, {
    
     age } from './title';
console.log(name);
console.log(age);

被入口文件导入的文件: ./src/title.js

exports.name = 'title_name';
exports.age = 'title_age';

打包后的文件: main.js (自执行函数实参部分)

{
    
    
    './src/index.js': function (module, __webpack_exports__, __webpack_require__) {
    
    
      'use strict';
      __webpack_require__.r(__webpack_exports__);
      var _title__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
        './src/title.js'
      );
      var _title__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(
        _title__WEBPACK_IMPORTED_MODULE_0__
      );

      console.log(_title__WEBPACK_IMPORTED_MODULE_0___default.a);
      console.log(_title__WEBPACK_IMPORTED_MODULE_0__['age']);
    },
    './src/title.js': function (module, exports) {
    
    
      exports.name = 'title_name';
      exports.age = 'title_age';
    }
}

针对harmony的简单总结
  • 如果模块是用common.js写的,则不需要做任何转换
  • 如果模块里有export或者import或者都有
    • webpack_require.r(webpack_exports); 先表明是ES6模块
  • 如果是ES6 modules 加载 common.js,并且有默认导入
    • 需要通过__webpack_require__.n(title__WEBPACK_IMPORTED_MODULE_0_);得到默认导入,_title__WEBPACK_IMPORTED_MODULE_0___default.a就是默认导入了

异步加载

入口文件: ./src/index.js

let importBtn = document.getElementById('import');
importBtn.addEventListener('click', () => {
    
    
  import(/* webpackChunkName: 'title' */'./title').then(result => {
    
    
    console.log(result);
  })
})

被入口文件导入的文件: ./src/title.js

exports.name = 'title_name';
exports.age = 'title_age';

打包后的文件: main.js (自执行函数实参部分)

{
    
    
  './src/index.js': function (module, exports, __webpack_require__) {
    
    
    let importBtn = document.getElementById('import');
    importBtn.addEventListener('click', () => {
    
    
      // 下面代码就是 import('./title')
      __webpack_require__
        .e('title')
        .then(
          __webpack_require__.t.bind(null, /*! ./title */ './src/title.js', 7)
        )
        .then((result) => {
    
    
          // result就是这个title的导出对象
          console.log(result);
        });
    });
  },
}

打包后的文件(chunk):title.js

(window['webpackJsonp'] = window['webpackJsonp'] || []).push([
  ['title'],
  {
    './src/title.js': function (module, exports) {
      exports.name = 'title_name';
      exports.age = 'title_age';
    },
  },
]);
针对懒加载的总结
  • 首先会执行__webpack_require__.e(‘title’), 并传入chunkId
  • webpack_require.e(‘title’)方法会通过JSONP的方式,向页面的head添加一个script标签,并且script标签的src为chunkId + ‘.js’
  • 此刻会执行打包后的title.js中的代码,也就是会执行webpackJsonpCallback这个方法
  • webpackJsonpCallback方法会把title这个代码块设置为已加载成功状态,并依次让resolves执行,把promise变成成功状态
  • 进入__webpack_require__.t.bind(‘./src/title.js’, 7)方法
    • 先执行__webpack_require__方法
    • 创建ns对象,并为它赋值 ns.__esModule = true; 和 ns.default = value;
    • 把vaule上的属性拷贝一份到ns上,返回ns。也就是最终结果

编译流程

基本概念

  • Entry 入口,webpack执行构建的第一步将从Entry开始,可抽象成输入
  • Module 模块,在webpack里一切皆模块,一个模块对应着一个文件。webpack会从配置的Entry开始递归找出所有依赖的模块。
  • Chunk 代码块,一个Chunk由多个模块组合而成,用于代码合并与分割。
  • Loader 模块转换器,用于把模块原内容按照需求转换成新内容。
  • Plugin 扩展插件,在webpack构建流程中特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。

理解事件流机制 Tapable

  • webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable。
  • Tapable 事件流机制保证了插件的有序性,将各个插件串联起来, Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条webapck机制中,去改变webapck的运作,使得整个系统扩展性良好。

理解Compiler

Compiler 对象包含了当前运行Webpack的配置,包括entry、output、loaders等配置,这个对象在启动Webpack时被实例化,而且是全局唯一的。Plugin可以通过该对象获取到Webpack的配置信息进行处理。

理解Compilation

Compilation对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 Compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,简单来讲就是把本次打包编译的内容存到内存里。Compilation 对象也提供了插件需要自定义功能的回调,以供插件做自定义处理时选择使用拓展。
简单来说,Compilation的职责就是构建模块和Chunk,并利用插件优化构建过程。

编译流程

针对编译流程的简单总结

Webpack的运行是个串行的过程,从启动到结束会依次执行以下流程:

  • 初始化参数:从配置文件和shell语句中读取与合并参数,得出最终参数。
  • 开始编译:用得到的参数初始化compiler对象,加载所有配置的插件,执行对象的Run方法开始编译。
  • 确定入口:根据配置中的entry找出所有入口文件。
  • 编译模块:从入口文件出发,调用所有配置的loader对模块进行翻译,再找出该模块所依赖的模块,再- 递归本步骤。直到所有入口依赖文件都经过了本步骤的处理。(此处为深度优先遍历)
  • 完成模板编译:使用loader翻译完所有模块后,得到每个模块被翻译后的最终内容,以及它们之间的依赖关系。
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

猜你喜欢

转载自blog.csdn.net/kang_k/article/details/126873507