webpack是怎么实现js模块化的?

前言

博主最近一直在学习算法相关的内容,因此挺长一段时间没有更新技术文章了,正好最近有个朋友问了我一个问题,webpack是怎么实现模块化的?我也就顺便把这块相关的内容写成一篇掘文,分享给那些对这块内容不太清楚的同窗。前端

经过本文,你会搞清楚下面这些问题:webpack

  • 1.webpack的模块化实现
  • 2.import会被webpack编译成什么?
  • 3.为何你可使用import引入commonjs规范的模块?为何反向引用也能够?

前端模块化

对于前端的模块化,相信你们都很熟悉。在如今的前端开发中,由于三大前端框架以及webpack等一系列打包工具的普及,模块化的应用已是屡见不鲜。咱们再也不须要像之前用对象来定义js模块,或者使用AMDCMDjs规范。如今在浏览器端,使用模块的方法就一个,import。随着时代发展,如今已经有不少浏览器原生支持了import语法,可是为了兼容性,咱们仍是须要经过webpack来处理import语法。web

PS:前不久尤大的vite2.0已经正式发布了,构建速度真是快到飞起,相信这也是将来的主流打包构建方式。算法

import会被编译成什么

咱们先来写个最简单的例子,来让webpack编译一下。本文的例子使用的webpack5编译,部分命名可能跟webpack4有些许差别,可是模块化的思想是一致的。浏览器

// index.js
import { read } from './a';
import run from './b';
read();
run();

// a.js
export const read = () => {
  console.log('阅读');
};

// b.js
export default run = () => {
  console.log('跑步');
};
复制代码

代码很简单,如今咱们来看下,webpack编译出来的代码是什么样的。`(去掉了不少注释)缓存

(() => {
  "use strict";
  var __webpack_modules__ = ({
    "./a.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"read\": () => (/* binding */ read)\n/* harmony export */ });\nconst read = () => {\r\n console.log('阅读');\r\n};\n\n//# sourceURL=webpack://my-leetcode/./a.js?");
    }),
    "./b.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (run = () => {\r\n console.log('跑步');\r\n});\n\n//# sourceURL=webpack://my-leetcode/./b.js?");
    }),
    "./index.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a */ \"./a.js\");\n/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./b */ \"./b.js\");\n\r\n\r\n(0,_a__WEBPACK_IMPORTED_MODULE_0__.read)();\r\n(0,_b__WEBPACK_IMPORTED_MODULE_1__.default)();\n\n//# sourceURL=webpack://my-leetcode/./index.js?");
    })
  });
  
  var __webpack_module_cache__ = {};

  function __webpack_require__(moduleId) {
    if(__webpack_module_cache__[moduleId]) {
        return __webpack_module_cache__[moduleId].exports;
    }
    var module = __webpack_module_cache__[moduleId] = {
        exports: {}
    };

    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
    return 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_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 });
    };
  })();

  // 执行入口的index.js
  var __webpack_exports__ = __webpack_require__("./index.js");
 })();
复制代码

首先编译出来的这个代码就是一个自执行函数,里面的内容能够分为三部分。性能优化

  • 1.modules对象
  • 2.__webpack_require__方法以及子方法的定义
  • 3.经过__webpack_require__方法运行入口的index.js文件

modules对象

这个对象里存放了全部你代码里写的做为一个个模块的js,它以js的文件路径做为key,值为一个可执行的函数。前端框架

__webpack_require__方法以及子方法的定义

__webpack_require__是一个关键的方法,负责实际的模块加载并执行这些模块内容,返回执行结果。它的子方法都是用来帮助模块的加载和执行。markdown

运行index.js文件

经过__webpack_require__方法运行入口文件index.jsapp

webpack模块化实现

咱们如今从入口index.js开始,一步步跟随代码。

__webpack_require__("./index.js");
复制代码

咱们先来看看__webpack_require__方法

// 模块缓存
var __webpack_module_cache__ = {};

// 传入引用模块的路径
function __webpack_require__(moduleId) {
  // 若是引用的模块存在缓存,直接返回缓存内容
  if(__webpack_module_cache__[moduleId]) {
      return __webpack_module_cache__[moduleId].exports;
  }
  // 定义一个module对象,再给它初始化一个exports对象
  var module = __webpack_module_cache__[moduleId] = {
      exports: {}
  };
  // 运行__webpack_modules__里的相关模块,传入相关参数
  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
  return module.exports;
}
复制代码

__webpack_require__方法其实就是运行__webpack_modules__里的相关模块。咱们如今来看看index.js模块的可执行函数。

"./index.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
  eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a */ \"./a.js\");\n/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./b */ \"./b.js\");\n\r\n\r\n(0,_a__WEBPACK_IMPORTED_MODULE_0__.read)();\r\n(0,_b__WEBPACK_IMPORTED_MODULE_1__.default)();\n\n//# sourceURL=webpack://my-leetcode/./index.js?");
})
// 把eval里的代码提取出来,等价于
(__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
  __webpack_require__.r(__webpack_exports__);
  // 定义一个变量,经过__webpack_require__加载a.js文件
  var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./a.js");
  // 定义一个变量,经过__webpack_require__加载b.js文件
  var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./b.js");
  // 经过以前定义的变量,来运行相关的方法
  (0,_a__WEBPACK_IMPORTED_MODULE_0__.read)();
  (0,_b__WEBPACK_IMPORTED_MODULE_1__.default)();
}
复制代码

里面的方法其实很简单,就是经过__webpack_require__加载a.jsb.js,经过返回值来运行a.jsb.js模块里的方法。

咱们如今来看看,__webpack_require__是怎么加载a.jsb.js模块,并把它们内部的方法返回出来使用的。咱们先从eval中提取出相关函数。

// a.js
(__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
   __webpack_require__.r(__webpack_exports__);
   __webpack_require__.d(__webpack_exports__, { "read": () => read });
   const read = () => { console.log('阅读'); };
}

// b.js
(__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      __webpack_require__.r(__webpack_exports__);
      __webpack_require__.d(__webpack_exports__, { "default": () =>__WEBPACK_DEFAULT_EXPORT__ });
      const __WEBPACK_DEFAULT_EXPORT__ = (run = () => { console.log('跑步') });
    }
复制代码

由于一个是read方法是export导出的,run方法是export default导出的,可是二者除了在命名上稍微有所区别,其余都一致。

首先,函数里,都存在咱们写在模块里的业务代码,readrun。而后咱们先重点来看下__webpack_require__.d方法。

__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] });
    }
  }
};
//这里的重点其实就是一句话,把key的内容,定义到exports的get方法中
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
复制代码

关于Object.defineProperty的内容不在本文讨论范围内,若是你不清楚这个方法,请先去了解一下它的使用。

咱们再把a.js__webpack_require__.d结合一下。

// a.js
(__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
   __webpack_require__.r(__webpack_exports__);
   // 这里的__webpack_exports__其实就是__webpack_require__里定义的module.exports。
   // 这里就是把read方法定义到module.exports.read上
   Object.defineProperty(__webpack_exports__, "read", { enumerable: true, get: read });
   const read = () => { console.log('阅读'); };
}
复制代码

这样定义以后

// index.js
// 这里__webpack_require__返回出来的module.exports.read上就定义了一个read方法
var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./a.js");
// 后面天然就可使用a.js里定义的read方法了。b.js也是相同的道理
_a__WEBPACK_IMPORTED_MODULE_0__.read()
复制代码

其实就是至关于,webpack将每个模块暴露出来的方法,都定义在了各自的module.exports对象上,而后返回出来,给其余的模块使用。经过这种方法,webpack就实现了js的模块化。

这不但跟Commonjs的导出方法命名同样,实现上也是相似。Commonjs中,每一个js文件一建立,也会生成一个 var exports = module.exports = {}, 开发者定义的方法,都会定义到exports或者module.exports

import懒加载实现

懒加载是前端很是经常使用的一种性能优化手段,使用上也很简单,只要import('xxx.js')就行,如今咱们来看下webpack是怎么实现懒加载的。咱们稍微改下以前的代码,而后再从新编译一下。

// index.js
import('./a.js').then(res => {
  res.read();
})

// a.js
export const read = () => {
  console.log('阅读');
};
复制代码

编译以后,咱们会发现除了主的js文件以外,还会生成一个懒加载的时候须要加载的js文件。 主文件步骤跟以前一致,仍是经过__webpack_require__加载index.js文件。

这里的代码量比较大,详细的流程,我也不在这里贴代码了,总的来讲,当用户触发其加载的动做时,会经过__webpack_require__.l方法动态的在head标签中建立一个script标签,而后加载模块,经过script标签的onloadonerror事件监听模块加载状态,若是完成,自动执行其中的代码。

commonjs的文件加载

接下来,咱们看下commonjs规范的文件会被webpack编译成什么样,改造一下代码

// index.js
const a = require('./a');
const run = require('./b');
a.read();
run();
// a.js
exports.read = () => {
  console.log('阅读');
};
// b.js
module.exports = run = () => {
  console.log('跑步');
};
复制代码

别的代码都一致,主要就来看下__webpack_modules__对象中各个模块的key对应的函数

// index.js
(__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
  const a = __webpack_require__("./a.js");
  a.read();
  const run = __webpack_require__("./b.js");
  run();
}
// a.js
(__unused_webpack_module, exports) => {
  exports.read = () => { console.log('阅读') };
}
// b.js
(module) => {
  module.exports = run = () => { console.log('跑步'); };
},
复制代码

编译以后的index.js文件跟原来的文件,只是把require换成了__webpack_require__,其余没有变化。而a.jsb.js跟原来的代码是如出一辙的。可是这里的exportsmodule__webpack_require__调用时候传入的。至关于,a.jsb.js都直接在__webpack_require__module.exports上定义了相关的方法。那index.js天然也就能够调用到这些方法了。

这也说明了,为何可使用import引入commonjs规范的模块,反向引用也能够。

总结

webpack的模块化主要是经过__webpack_require__方法,将各个模块里定义的方法,esm定义的方法使用Object.definePropertycommonjs定义的方法直接定义,最终都会统一加到本身定义的module.exports对象上,而后返回出来,给其余的模块引用。

import进来的文件通过webpack打包之后会存放在一个对象里,key为模块路径,value为模块的可执行函数。import懒加载会单独打成一个包,在须要加载的时候,动态进行加载。

由于webpack会把import的方法都会转换成__webpack_require__方法,使用相似commonjs规范的方式,获取其余模块里的方法。因此可使用import引入commonjs规范的模块, 反向引用也能够。

感谢

本文若是对你有所帮助,请帮忙点个赞,感谢。

相关文章
相关标签/搜索