[webpack学习]你知道经webapck打包后的代码是怎么运行的吗?

771 阅读6分钟

前言

对Webpack的打包后的代码的运行逻辑蛮有兴趣,所以阅读了下代码学习总结一下。

Webpack4打包结果分析

先展示项目以及项目中的开发源码和webpack配置

项目目录结构如下:

src/heading.js

export default ()=>{
  const element=document.createElement('h2')

  element.textContent='Hello World'
  element.addEventListener('click',()=>{
    alert('Hello webpack')
  })

  return element
}

src/index.js

import createHeading from './heading.js'
const createLink =require('./link.js')

const heading = createHeading()
document.body.append(heading)

const link=createLink()
document.body.append(link)

src/link.js

module.exports=()=>{
  const element=document.createElement('a')
  element.textContent='click me'
  return element
}

webpack.config.js

const path=require('path')

module.exports={
  mode:'none', // 'none'模式是只负责打包整合文件,不做丑化和压缩处理,此模式有利于分析打包的代码。
  entry:'./src/index.js', //入口文件
   output:{
     filename:'bundle.js', //打包输出结果
     path:path.join(__dirname,'dist') //必须传入绝对路径
   }
}

打包后的代码结构如下,其实可以把代码分为模块数组webpack加载逻辑

(function(modules) {
	// .... webpack加载逻辑:声明且执行webpack加载模块的函数
})([
	//... 模块数组:源代码中的模块组成的数组modules:[index.js,heading.js,link.js]
]);

它是一个立即执行函数。通过把开发模块传入到modules形参,然后立即执行函数执行。我们先看一下模块数组的代码:

[ 
/* 0  对应src/index.js*/
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
//从modules中加载索引为1的模块,即heading.js
/* harmony import */ var _heading_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); 

const createLink =__webpack_require__(2) //从modules中加载索引为1的模块,即link.js

const heading = Object(_heading_js__WEBPACK_IMPORTED_MODULE_0__["default"])()
document.body.append(heading)

const link=createLink()
document.body.append(link)
}),
/* 1 对应heading.js*/
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
// __webpack_require__.r用于标记该模块通过ES6 export 导出。通过CommonJS module.exports 导出的模块不需要被处理
__webpack_require__.r(__webpack_exports__);
// __webpack_exports__相当于 module.exports。这里是把export default导出的模块放到module.exports 中default的属性值
/* harmony default export */ __webpack_exports__["default"] = (()=>{
  const element=document.createElement('h2')

  element.textContent='Hello World'
  element.addEventListener('click',()=>{
    alert('Hello webpack')
  })

  return element
});
}),
/* 2 对应link.js*/
(function(module, exports) {

module.exports=()=>{
  const element=document.createElement('a')
  element.textContent='click me'
  return element
}
})
]

从上可以总结出:

  1. 开发的所有模块会按照引入顺序依次放到modules数组中。入口文件中的代码会放到数组的首位
  2. 当模块需要引入别的模块时,会调用__webpack_require__函数,传入需要引入的模块所在modules中的位置
  3. 如果该模块是通过ES6 export 导出,则会做以下处理:
    • __webpack_require__.r(__webpack_exports__):标记该模块为ES6模块
    • __webpack_exports__["default"] = ...:把模块代码放到__webpack_exports__default属性中。相当于module.exports.default=...

接下来看webpack加载逻辑的代码(一些不涉及到分析的代码我会不展示):

(function(modules) { // webpackBootstrap
	// The module cache 用于存放已经加载了的模块,在再次调用模块时可以立即返回导出值,防止模块里的代码再次执行
	var installedModules = {};
    
	// The require function  在取代ES6 import和CommonJS require 的函数 
	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, // 表示模块在modules中的位置索引
			l: false,  // 表示模块已被加载的标志位,true代表已被加载
			exports: {} // 模块通过CommonJS module.exports或者ES6 export 导出的变量
		};
		// 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;
	}
    
	// define getter function for harmony exports
    // ES6使用。把export
	__webpack_require__.d = function(exports, name, getter) {
		if(!__webpack_require__.o(exports, name)) {
			Object.defineProperty(exports, name, { enumerable: true, get: getter });
		}
	};
    
	// define __esModule on exports
    // 用于处理通过ES6 export 导出的模块时,给exports变量定义 {Symbol(Symbol.toStringTag): "Module",__esModule: true}
	__webpack_require__.r = function(exports) {
		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
		}
		Object.defineProperty(exports, '__esModule', { value: true });
	};
    
    // Load entry module and return exports
    // 立即加载和执行入口文件
	return __webpack_require__(__webpack_require__.s = 0);
})

总结以上执行函数的内容,做了以下工作:

  1. 声明installedModules用于存放已加载的模块。
  2. 声明__webapck_require__函数用于引用模块。且给该函数添加各种属性以辅助引入。该函数运行逻辑如下:
    • 检查installedModules中是否存在要被加载的模块,有则直接return出去
    • 如果模块还没被加载。则声明module={i:moduleId,l:false,expoers:{}}且存放到installedModules
    • 通过call把执行上下文设为module.exports后执行模块代码
    • 最后把module.exports作为结果return出去
  3. 最后以__webapck_require__(0)加载入口文件且把模块作为立即执行函数的结果返回出去。

拓展: 在转换ES6模块时,Webpack会将其转换成与CommonJS模块兼容的模式。因此,在ES6模块中导出的变量我们也可以用CommonJS中的require加载。例如针对开头的ES6模块heading.js,可以通过const createHeading = require('./heading.js').default加载。至于为什么要把ES6模块向CommonJS模块兼容而不是反过来,估计是因为CommonJS出来比较早吧。

对比Webpack5的打包结果

同样是以开头的项目进行打包,不过从Webpack4换成Webpack5。打包后的代码结构如下,同样地代码也分模块数组webpack加载逻辑,先分析webpack加载逻辑:

(()=>{
	var __webpack_modules__ = ([
    	//... 模块数组:源代码中的模块组成的数组modules:[index.js,heading.js,link.js]
    ])
    
    // 以下是 webpack加载逻辑:声明且执行webpack加载模块的函数
    // The module cache 相当于webpack4中的installedModules
    var __webpack_module_cache__ = {};
    
    // The require function 相当于webpack4中的同名函数
    function __webpack_require__(moduleId) {
      // Check if module is in cache
      if(__webpack_module_cache__[moduleId]) {
          return __webpack_module_cache__[moduleId].exports;
      }
      // Create a new module (and put it into the cache)
      var module = __webpack_module_cache__[moduleId] = {
          // no module.id needed // 舍弃webpack4中的i和l属性
          // no module.loaded needed
          exports: {}
      };

      // Execute the module function
      // 对比于webpack4,不再设置执行上下文。因为每个把模块代码包含的函数都用了严格模式"use strict",严格模式下this的值为undefined(其实webpack4就已经对函数都用了严格模式....)
      __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

      // Return the exports of the module
      return module.exports;
  } 
  
  /* webpack/runtime/define property getters */
  (() => {
      // define getter functions for harmony exports 用于处理ES6模块,把ES6模块中所有导出的值通过该函数复制到module.exports下
      __webpack_require__.d = (exports, definition) => {
          for(var key in definition) {
              if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
                  Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
              }
          }
      };
  })();

  /* webpack/runtime/hasOwnProperty shorthand */
  (() => {
      __webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop)
  })();

  /* webpack/runtime/make namespace object */
  (() => {
      // define __esModule on exports
      __webpack_require__.r = (exports) => {
          if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
              Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
          }
          Object.defineProperty(exports, '__esModule', { value: true });
      };
  })();
  
  // startup
  // Load entry module
  // 直接引入入口文件,不在return加载后的入口文件(早就觉得没必要了....)
  __webpack_require__(0);
  // This entry module used 'exports' so it can't be inlined
})();

总结一下webpack加载逻辑中,与webpack4的不同之处:

  1. 首先是整个结构位置已变化,webpack4是(function(){webpack加载逻辑})(模块数组),webpack5是(()=>{模块数组;webpack加载逻辑})()
  2. 除去__webpack_require__.m,__webpack_require__.c,__webpack_require__.s等使用少的属性
  3. __webpack_require__.d 函数有所改变,用于赋值传入对象的所有属性到module.exports中。而不是复制传入对象指定的属性
  4. 最后只加载执行入口文件。其结果不会return出去

我们再看模块数组中的代码:

var __webpack_modules__ = ([
/* 0 index.js*/
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _heading_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);

const createLink =__webpack_require__(2)

// 此处圆括号用法:(any,...,fn2),执行单个或多个表达式,并返回最后一个表达式的值,多个表达式之间需要用逗号“,”分隔开
const heading = (0,_heading_js__WEBPACK_IMPORTED_MODULE_0__.default)()
document.body.append(heading)

const link=createLink()
document.body.append(link)
}),

/* 1 heading.js*/
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
__webpack_require__.r(__webpack_exports__);
// 通过__webpack_require__.d把第二个参数里的值用Object.defineProperty复制到__webpack_exports__中
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "default": () => __WEBPACK_DEFAULT_EXPORT__
/* harmony export */ });
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (()=>{
  const element=document.createElement('h2')

  element.textContent='Hello World'
  element.addEventListener('click',()=>{
    alert('Hello webpack')
  })

  return element
});
}),

/* 2 link.js*/
((module) => {

module.exports=()=>{
  const element=document.createElement('a')
  element.textContent='click me'
  return element
}
})
]);

总结一下模块数组中,与webpack4的不同之处:

  1. 在模块中加载别的模块时,webpack4是Object(_heading_js__WEBPACK_IMPORTED_MODULE_0__["default"])(),webpack5是(0,_heading_js__WEBPACK_IMPORTED_MODULE_0__.default)()。这里我看不出区别,甚至看不出为啥两者要这么写。
  2. 处理ES6模块时,使用__webpack_require__.d复制导出值到module.exports中。且通过Object.defineProperty使module.exports中的属性值不可被修改。

后记

Webpack自己接触和使用蛮久了,蛮多东西要记录的,这里先开一个头。之后关于源码和使用技巧都会写成文章记录下来。