1
头图

在使用webpack中的项目的时候,我们可以使用esModule,也可以使用commonJS,还可以使用import(moduleName)进行模块的懒加载,那么这一切webpack是怎么做到的呢?

1、准备工作

1.1、 使用webapck@4 webpack-cli@3

"html-webpack-plugin": "4",
"webpack": "4",
"webpack-cli": "3"

1.2、 文件结构

image.png

1.3、 webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

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

  plugins: [
      new CleanWebpackPlugin(),
      new HtmlWebpackPlugin({
        template: './src/index.html',
      })
  ],

  optimization: {
      chunkIds: 'named',
      splitChunks: {
          cacheGroups: {
            commons: {
              test: /[\\/]node_modules[\\/]/,
              name: 'vendors',
              chunks: 'all',
            },
        },
      }
  }
}

1.4、打包命令

npx webpack 或者 在package.json中添加

"scripts": {
      "build": "webpack"
}

然后执行 npm run build

2、CommonJS模块打包

2.1、使用commonJS导出模块, commonJS导入模块

// index.js
const title = require('./login')
console.log('commonjs 文件打包分析')

// login.js
// commonJS导出
module.exports = "今天天气很冷!"

准备工作做完后,执行打包命令删除无用的注释,相关代码都折叠,运行调试。
image.png
可以看到,其实打包后的文件就是个IIFE. 传入的对象就是我们之前两个文件路径作为键名,以及各自一个函数作为对象的的对象集合(实际上就是我们的依赖集合,键名是可以通过webpack.config.js进行配置的,默认是当前路径加文件名的方式命名的)。
image.png
进入到自执行函数中可以看到,__webpack_require__这个上面挂载了很多的方法和属性

  • __webpack_require__.m 导出的modules对象
  • __webpack_require__.c 导出的module缓存
  • __webpack_require__.d 为exports定义 getter方法
  • __webpack_require__.r 为即将导出的exports定义 __esModuleSymbol.toStringTag(esModule)
  • __webpack_require__.t
  • __webpack_require__.n n方法 为module定义获取模块值的getter方法, 非esmodule则返回当前module
  • __webpack_require__.o o方法 判断对象是否拥有某个属性,工具函数
  • __webpack_require__.p public_path 配置文件中配置,默认为空字符串 在使用jsonp动态引入时会被使用到

我们继续往下运行,当代码走到最后的return时,这时候会调用__webpack_require__(这个方法就是核心方法。其实做的事情很简单。),这时候就是程序在加载入口文件(此是加载的是index.js

function __webpack_require__(moduleId) {
  // 检查是否有缓存
  if (installedModules[moduleId]) {
    return installedModules[moduleId].exports;
  }
  // 创建一个module对象,并将其保存在缓存中,一遍下次使用
  var module = (installedModules[moduleId] = {
    i: moduleId, // 模块ID
    l: false, // 是否已经加载
    exports: {}, // 对外返回的exports对象, 模块导出的东西都挂载在该对象上面
  });
  // 调用call方法,执行当前的module
  modules[moduleId].call(
    module.exports,
    module,
    module.exports,
    __webpack_require__
  );
  // 标记模块已加载
  module.l = true;
  // 返回模块的exports对象
  return module.exports;
}

__webpack_require__内部,会执行当前module中的代码,第一次执行的时候,我们看到index.js中有对login.js的引用,这里被webpack包装成了__webpack_require__这个方法,无论是require还是import语法都将会执行这个方法。

// dits/main.js
{
    "./src/index.js": function (module, exports, __webpack_require__) {
      const title = __webpack_require__(/*! ./login */ "./src/login.js");
      console.log("commonjs 文件打包分析");
      console.log(title, "title");
    },
    "./src/login.js": function (module, exports) {
      module.exports = "今天天气很冷!";
      console.log("login.js 执行了");
    },
  }

运行到__webpack_require__(/*! ./login */ "./src/login.js")这里时,会去加载login.js的内容。login.js内容很少,就是module.exports = '今天天气很冷',由上面的分析可知,被__webpack_require__加载的代码都会返回其exports.所以加载了login.js后,title的值就应该是我们module.exports导出的值。
image.png
commonJS规范的导入导出分析完成。
结论:在使用commonJS导入,commonJS导出模块的时候,webpack会使用自己的__webpack_require__方法进行模块的加载。

2.2、使用commonJS规范导入模块,esModule导出模块打包

更改index.js和.login.js, 执行打包

// index.js
const object = require('./login')
console.log('commonjs 文件打包分析')
console.log(object.default, 'default')
console.log(object.user, 'user')

// login.js
export const user = {
  name: '法外狂徒-张三',
  age: 33
}
export default '今天天气很冷!'
console.log('login.js 执行了')

导出结果, 这里我们只看IIFE中的modules,因为上面的内容都是一样的。

{
  "./src/index.js":
    /*! no static exports found */
    function (module, exports, __webpack_require__) {
      const object = __webpack_require__(/*! ./login */ "./src/login.js");
      console.log("commonjs 文件打包分析");
      console.log(object.default, "default");
      console.log(object.user, "user");
    },
  "./src/login.js":
    /*! exports provided: user, default */
    function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      /* harmony export (binding) */ __webpack_require__.d(
        __webpack_exports__,
        "user",
        function () {
          return user;
        }
      );
      const user = {
        name: "法外狂徒-张三",
        age: 33,
      };
      /* harmony default export */ __webpack_exports__["default"] =
        "今天天气很冷!";
      console.log("login.js 执行了");
    },
}

可以看见在使用esModule进行导出的时候,多了这些内容。

  • __webpack_require__.r 为exports对象 定义用于标识esModule标识

    __webpack_require__.r = function (exports) {
      // 判断当前环境是否是es6的环境,如果是,则对exports设置一个 `Module`属性
      if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
      }
      // 为exports 设置一个默认的`__esModule`值, 用于标识esModule
      Object.defineProperty(exports, "__esModule", { value: true });
    };
  • __webpack_require__.d 为exports对象上的属性,定义getter

    __webpack_require__.d = function (exports, name, getter) {
      // 判断当前exports对象是否有某个属性值,如果没有, 则重新定义这个属性,并设置getter方法
      if (!__webpack_require__.o(exports, name)) {
        Object.defineProperty(exports, name, { enumerable: true, get: getter });
      }
    };
    // 工具函数,判断对象是否拥有某个属性值
    __webpack_require__.o = function (object, property) {
      return Object.prototype.hasOwnProperty.call(object, property);
    };

    可以看到,在login.js中,对user对象,设置了一个getter方法,返回的是定义在login.js代码块中的user对象,这就是我们定义在该文件中的user对象。同时export defaults返回的数据,也通过__webpack_exports__["default"]进行了赋值,因此,index.js在执行const object = __webpack_require__(/*! ./login */ "./src/login.js");这个代码时,object中的值,就是我们需要的结果了
    image.png

3、esModule 导入模块打包

3.1、使用esModule 导入模块,esmodule方式导出

更改index.js和login.js的代码,然后执行打包操作

// login.js
export const user = {
  name: '法外狂徒-张三',
  age: 33
}
export default '今天天气很冷!'
console.log('login.js 执行了')

// index.js
import title, { user } from './login'
console.log('commonjs 文件打包分析')
console.log(title, 'default')
console.log(user, 'user')

打包结果

{
    "./src/index.js":
      /*! no exports provided */
      function (module, __webpack_exports__, __webpack_require__) {
        "use strict";
        __webpack_require__.r(__webpack_exports__);
        var _login__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/login.js");
        console.log("commonjs 文件打包分析");
        console.log(_login__WEBPACK_IMPORTED_MODULE_0__["default"], "default");
        console.log(_login__WEBPACK_IMPORTED_MODULE_0__["user"], "user");
      },
    "./src/login.js":
      /*! exports provided: user, default */
      function (module, __webpack_exports__, __webpack_require__) {
        "use strict";
        __webpack_require__.r(__webpack_exports__);
        __webpack_require__.d(__webpack_exports__, "user", function () {
          return user;
        });
        const user = {
          name: "法外狂徒-张三",
          age: 33,
        };
        __webpack_exports__["default"] = "今天天气很冷!";
        console.log("login.js 执行了");
      },
  }

同上面一样,这个文件是个IIFE,只是里面的内容发生了变化。同4.2进行比较,只是获取值的方式不同而已。

3.2、使用esModule导入 commonJS导出

更改文件,然后执行打包

// login.js
module.exports = "今天天气很冷!"

// index.js
import title from './login.js'
console.log('esmodule 文件打包分析')

打包结果:

{
  "./src/index.js":
    /*! no exports provided */
    function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      var _login__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/login.js");
      var _login__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_login__WEBPACK_IMPORTED_MODULE_0__);
      console.log(_login__WEBPACK_IMPORTED_MODULE_0___default.a, "default");
    },
  "./src/login.js":
    /*! no static exports found */
    function (module, exports) {
      module.exports = "今天天气很冷!";
      console.log("login.js 执行了");
    },
}

可以看到,使用这种方式的导出,在导入该文件的地方,多了一个n方法的调用
,下面来看看这个n方法干了什么事情

__webpack_require__.n = function (module) {
  var getter = module && module.__esModule ? // 当前module是否是先前标记的__esModule
    function getDefault() {return module['default'];} : // 是esmodule就返回默认
    function getModuleExports() {return module;}; // 否则返回改module
  __webpack_require__.d(getter, 'a', getter); // 为该module调用d方法,重新定义getter, 重新定义属性 a, 用于后面获取该值
  return getter;
};

继续往下看,执行到获取title的代码时,回去调用之前定义d方法定义的getter方法,获取到默认导出的值,这样在使用commonjs导出,使用 esModule的方式导入的时候,就能正确的获取到值了,否则按正常来讲,esModule导入commonjs是会报错的
image.png
esModule导入commonjs模块
image.png

这就是webpack的加持下, 我们可以在项目中混合使用commonjs esModule规范的代码进行npm 包的引入。


4、模块懒加载

webpack中,允许我们使用import('module')来进行模块的懒加载。

4.1、准备工作

还是使用上面的配置,只是更改我们的被打包的文件内容

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>测试打包文件分析</title>
</head>
<body>
  <button id="btn">加载</button>
</body>
</html>

在index.js文件中,我们创建一个按钮用于触发加载login.js内容,然后在import导入模块成功后,再次新建一个新的按钮,这个按钮在进行一次动态导入。

// index.js 入口文件,通过注册一个点击事件完成异步加载login.js的内容
const btn = document.getElementById('btn')

btn.addEventListener('click', () => {
  import(/*webpackChunkName: "login"*/'./login.js').then(login => {
    console.log(login, 'login -------->')
    // 创建一个新的按钮,再次加载一次login模块
    const button = document.createElement('button')
    button.innerHTML = '再次加载Login模块'
    button.onclick = () => {
      import(/*webpackChunkName: "login"*/'./login.js').then(module => {
        console.log('<<<<<<loaded login again ------')
        console.log(module, 'module')
        console.log('loaded login again ------->>>>>>')
      })
    }
    document.body.appendChild(button)
  })
})

console.log('index js 文件执行')
// login.js
module.exports = 'login.js value'

console.log('loginjs 执行了')

点击加载按钮,然后点击 再次加载Login模块按钮,得到如下结果
image.png
image.png
可以看到,

  1. import方法是返回的一个promise,返回的是被webpack处理过的一个对象,这个对象就是上述 __webpack_require__处理过并返回的exports对象
  2. network中有login.js的网络请求,head标签中多了一个script的脚本文件

看看webpack打包后的结果
image.png
可以看到,使用了懒加载,会出现两个新的方法调用

  • __webpack_require.e 加载chunk的方法
  • __webpack_require.t 为当前module创建一个fake namespace
    同时在IIFE里面的函数体中也多了 一些其他的代码
    image.png
    这里的功能主要是将window['webpackJsonp'] 调用push方法的时候,直接调用webpackJsonpCallback方法,
    声明一个jsonpArraywindow['webpackJsonp']共享一个数组空间
    然后将webpackJsonpCallback方法赋值给jsonpArray.push, 这样就将window['webpackJsonp']webpackJsonpCallback建立起了链接,即调用window['webpackJsonp'].push方法就会执行webpackJsonpCallback方法
    因为是第一次加载,jsonpArray数组为空数组, 所以不会执行下面的for循环
    然后我们再看看需要异步加载的模块被webpack打包成什么内容了

    (window['webpackJsonp'] = window['webpackJsonp'] || []).push([
    ['login'],
    {
      './src/login.js': function (module, exports) {
          module.exports = 'login.js value';
          console.log('loginjs 执行了');
      },
    },
    ]);
    

    这个文件中,当执行login.js模块时,会调用push方法,实际上就会调用webpackJsonpCallback方法

    function webpackJsonpCallback(data) {
      var chunkIds = data[0];
      var moreModules = data[1];
    
      // add "moreModules" to the modules object,
      // then flag all "chunkIds" as loaded and fire callback
      var moduleId,
        chunkId,
        i = 0,
        resolves = [];
      for (; i < chunkIds.length; i++) {
        chunkId = chunkIds[i];
        if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
          resolves.push(installedChunks[chunkId][0]);
        }
        installedChunks[chunkId] = 0;
      }
      for (moduleId in moreModules) {
        if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
          modules[moduleId] = moreModules[moduleId];
        }
      }
      if (parentJsonpFunction) parentJsonpFunction(data);
    
      while (resolves.length) {
        resolves.shift()();
      }
    }

第二次加载login.js
image.png
触发再次加载Login模块后
image.png
动态加载login.js后,执行__wepack_require__方法时,就会找到之前的缓存,无需再次发起资源请求
image.png

总结/引申:

  1. webpack对于js文件的编译操作,会使用他自身的一个__webpack_require的方法,并根据不同的引用模块的方式,调用不同的方法,最终目的是达到将被导出的模块内容,通过exports对象去导出,以便我们能获取得到正确的值
  2. 懒加载模块其实就是动态的创建了一个script标签,通过promise的包装,让我们可以很优雅的获取到加载成功后的module
  3. webpack怎么知道我们的引入模块的规则 以及导出模块的规则的呢? 通过 ast
  4. 对比Rollup,二者有啥区别?Rollup只对esmodule进行打包,而webpack则是新旧通吃

欢迎留言讨论!


路飞的笑
119 声望3 粉丝

人活着就要做有意义的事情。


« 上一篇
Redux