在应用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、 文件构造

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.jsconst title = require('./login')console.log('commonjs 文件打包剖析')// login.js// commonJS导出module.exports = "今天天气很冷!"

筹备工作做完后,执行打包命令删除无用的正文,相干代码都折叠,运行调试。

能够看到,其实打包后的文件就是个IIFE. 传入的对象就是咱们之前两个文件门路作为键名,以及各自一个函数作为对象的的对象汇合(实际上就是咱们的依赖汇合,键名是能够通过webpack.config.js进行配置的,默认是以后门路加文件名的形式命名的)。

进入到自执行函数中能够看到,__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导出的值。

commonJS标准的导入导出剖析实现。
论断:在应用commonJS导入,commonJS导出模块的时候,webpack会应用本人的__webpack_require__办法进行模块的加载。

2.2、应用commonJS标准导入模块,esModule导出模块打包

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

// index.jsconst object = require('./login')console.log('commonjs 文件打包剖析')console.log(object.default, 'default')console.log(object.user, 'user')// login.jsexport 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中的值,就是咱们须要的后果了

3、esModule 导入模块打包

3.1、应用esModule 导入模块,esmodule形式导出

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

// login.jsexport const user = {  name: '法外狂徒-张三',  age: 33}export default '今天天气很冷!'console.log('login.js 执行了')// index.jsimport 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.jsmodule.exports = "今天天气很冷!"// index.jsimport 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是会报错的

esModule导入commonjs模块

这就是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.jsmodule.exports = 'login.js value'console.log('loginjs 执行了')

点击加载按钮,而后点击 再次加载Login模块按钮,失去如下后果


能够看到,

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

看看webpack打包后的后果

能够看到,应用了懒加载,会呈现两个新的办法调用

  • __webpack_require.e 加载chunk的办法
  • __webpack_require.t 为以后module创立一个fake namespace
    同时在IIFE外面的函数体中也多了 一些其余的代码

    这里的性能次要是将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

触发再次加载Login模块后

动静加载login.js后,执行__wepack_require__办法时,就会找到之前的缓存,无需再次发动资源申请

总结/引申:

  1. webpack对于js文件的编译操作,会应用他本身的一个__webpack_require的办法,并依据不同的援用模块的形式,调用不同的办法,最终目标是达到将被导出的模块内容,通过exports对象去导出,以便咱们能获取失去正确的值
  2. 懒加载模块其实就是动静的创立了一个script标签,通过promise的包装,让咱们能够很优雅的获取到加载胜利后的module
  3. webpack怎么晓得咱们的引入模块的规定 以及导出模块的规定的呢? 通过 ast
  4. 比照Rollup,二者有啥区别?Rollup只对esmodule进行打包,而webpack则是新旧通吃

欢送留言探讨!