webpack 模块加载原理

370

简单分析下webpack5的模块加载的原理。

测试文件如下:

login.js 提供方法

// login.js
export function login() {
    const ele = document.createElement('h1')
    ele.className = 'name example iconfont icon-linggan'

    ele.innerHTML = '这是测试内容啊啥的sodas啊实打实的'
    return ele
}

export function funB() {
    console.log('funB')
}

export const a = 'a'

index.js 调用login.js的方法

import login from './js/login.js'
document.body.appendChild(login())

tips: webpack不能同时支持 ES模块 和 CommonJS 模块

经过测试同时存在会报错:

Uncaught Error: ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: ./src/js/login.js

所以这里只考虑ES模块加载规则。

webpack配置:
const path = require('path')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
    entry: './src/index',
    output: {
            filename: 'index.js',
            path: path.resolve(__dirname, 'dist')
    },
    mode: 'development',
    devtool: false,
    target: 'web',
    module: {
            rules: [
                    {
                            test: /\.jsx?$/,
                            exclude: /node_modules/,
                            use: ['babel-loader']
                    },
            ]
    },
    optimization: {
            minimizer: [
              // 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 `terser-webpack-plugin`),将下一行取消注释
              // `...`,
              new CssMinimizerPlugin(),
              new TerserPlugin(),
            ],
      },
    plugins: [
            new  MiniCssExtractPlugin({
                    filename: 'css/[name].[hash:8].min.css',
            }),
            new CleanWebpackPlugin(),
            new HtmlWebpackPlugin({
                    title: '插件使用',
                    template: './index.html'
            }),
            new webpack.DefinePlugin({
                    BASE_URL: '"./"'
            }),
    ]
}

webpack的配置主要关注 mode: 'development',devtool: false 这2个配置 ,其他都是辅助能解析基本文件的可以忽略。

打包出来的删除注释后的js文件

tips: 里面的注释是为了增加理解手动所写

(() => {
	"use strict";
        // 声明一个全局存储的变量: id --> () => {}
            var __webpack_modules__ = ({

          "./src/js/login.js":
          ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

              __webpack_require__.r(__webpack_exports__);
              __webpack_require__.d(__webpack_exports__, {
                "login": () => (/* binding */ login),
                "funB": () => (/* binding */ funB),
                "a": () => (/* binding */ a)
              });
              function login() {
                var ele = document.createElement('h1');
                ele.className = 'name example iconfont icon-linggan';
                ele.innerHTML = '这是测试内容啊啥的sodas啊实打实的';
                return ele;
              }
              function funB() {
                console.log('funB');
              }
              var a = 'a';

          })
	});

        // 声明一个缓存变量
	var __webpack_module_cache__ = {};
    
        // 根据moduleId获取和存储 exports变量
	function __webpack_require__(moduleId) {
            var cachedModule = __webpack_module_cache__[moduleId];
            if (cachedModule !== undefined) {
                    return cachedModule.exports;
            }
            var module = __webpack_module_cache__[moduleId] = {
                    exports: {}
            };

            __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

            return module.exports;
	}
    
        // 三个自执行函数 在__webpack_require__函数上增加 _d, _o, _r 方法
	(() => {
		__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_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
	})();
	
	(() => {
            __webpack_require__.r = (exports) => {
                if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
                        Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
                }
                Object.defineProperty(exports, '__esModule', { value: true });
            };
	})();
	// 自执行结束--
 
  // 获取并执行  moduleId = "./src/js/login.js" 的模块
  var __webpack_exports__ = {};
  (() => {
      __webpack_require__.r(__webpack_exports__);
      var _js_login_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./js/login.js */ "./src/js/login.js");

      document.body.appendChild((0,_js_login_js__WEBPACK_IMPORTED_MODULE_0__["default"])());
  })();

})()
;
根据文件总结步骤如下:

    (moduleId: ./src/js/login.js)

  1. webpack_modules 存储全局模块数量,存储以后的形式如下:
{

    moduleId: (_, exports, require) => {

        __webpack_require__.r(exports)

        __webpack_require__.d(exports)
        // 见上面的源码
        ...

    }

}
  1. webpack_module_cache 缓存加载过的模块,缓存形式如下:
{
   moduleId: { exports: { a: 'a', login: function login() {}, ...} }
}
  1. function webpack_require 根据moduleId 获取导出的内容。如果没有则加入到__webpack_module_cache__中,存储形式如下:
// __webpack_modules__存储的内容。 
// 进过 __webpack_modules__[moduleId](module, module.exports, __webpack_require__)后
// __webpack_module_cache__ 中存储了 moduleId 的 exports
{
    moduleId: {
        exports :  {
            // 该文件导出的内容...
        }
    }
}

tips: 所以在__webpack_require__的时候根据根据moduleId 确定 是谁的 exports, require。

  1. webpack_require.o 判断是自己的属性并且不在原型上

  2. webpack_require.r 标记 exports.__esModule = true 即为es模块

  3. webpack_require._d 会把内容绑定到 exports属性上,经过处理以后得到:

exports: {
   login: function login() {},
   funb: ...
}
  1. webpack_require('./src/js/login.js') 找到并存储moduleId 为 './src/js/login.js'的 exports

  2. 执行 exports的 default 函数

最后总结下执行顺序:

_webpack_require_ ----> webpack_modules[moduleId] ---> _webpack_require_.r / _webpack_require._d

_webpack_require__传入moduleId以后就会获取webpack_modules中的箭头函数, 箭头函数主要就是调用__webpack_require_.d获取exports内容,并存储在cache中,下次直接获取cache中的内容。获取到exports内容后就可以调用啦。