一、UMD模块加载

这里不讨论 CommonJs 与 requireJS(AMD)以及 UMD 的区别,可以查看:https://webpack.toobug.net/zh-cn/chapter2/amd.html 了解更多。

主要讨论 webpack 是如何依赖 UMD 的模式去打包一个简单的纯 vue js(无 loader)。

首先看下 UMD 是如何兼容 AMD 和 CommonJs 的:

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['b'], factory);
    } else if (typeof module === 'object' && module.exports) {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory(require('b'));
    } else {
        // Browser globals (root is window)
        root.returnExports = factory(root.b);
    }
}(this, function (b) {
    //use b in some fashion.

    // Just return a value to define the module export.
    // This example returns an object, but the module
    // can return a function as the exported value.
    return {};
}));

首先,模块加载定义的时候就是定义了一个匿名方法然后立即执行这个方法,并传递参数 (function(){}(param));

比如下面这个方法会直接输出 postbird:

(function (name){console.log(name)}('postbird'))

1.jpg

而上面的 UMD 定义中,定义的匿名方法有两个参数,分别是 rootfactory,立即执行函数在执行的时候,传入了 this 以及 一个 function 参数,其中,function 返回了一个对象。

在立即执行函数中,首先会去兼容 AMD 的规范,去判断是否支持 AMD typeof define === 'function' && define.amd,如果支持 则直接使用 AMD 的规范加载模块。 如果存在 module 则说明支持 commonJS,会通过 module.exports = factoru(require('b')) 的 commonJS 形式加载模块。

否则会直接挂载到 root 的一个对象上,如果 root 是 window 的话,会挂载到 window上,并且会直接 factory,只需要执行后的对象。

二、webpack 打包一个极简的 vue 项目

不涉及 loader,因此不适用 .vue 文件的打包,所有的 vue 均使用 js 来实现

因为涉及模块,因此组织下项目的目录:

|-- 
  |-- node_modules/
  |-- app.js
  |-- index.html
  |-- main.js
  |-- package.json
  |-- build.js

index.html

vue 的 html template 文件,其中里面依赖的是 ./build.js,这是打包出来的目标文件,初始化中还不存在该文件。

为了减少打包出来的 bundle 的大小,我通过 script 的方式引入了生产环境的 vue。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>webpack vue</title>
  <!-- 生产环境版本,优化了尺寸和速度 -->
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
</head>
<body>
  <div id="app"><!--  --></div>
  <script src="./build.js"></script>
</body>
</html>

main.js

webpack 入口文件,也是拉取其他依赖的入口,这里直接创建了一个 vue 的实例,而在这个文件中,通过 import 引入了app.js

import App from './app.js';

new Vue({
  el: '#app',
  components: {App},
  template: `<div> <App/> </div>`
});

app.js

App 组件,实际上就是一个对象,这个对象也是一个 vue 的组件对象,有 template 属性:

export default {
  template: `<div> <h1>App.js</h1> </div>`
}

运行命令

我全局装了 webpack (4.x) 如果没装的话需要全局或者依赖装一下。

webpack main.js -o build.js // 注意是 4.x 版本

因为需要分析产物,所以不能选择 production 的打包结果,需要 development:

webpack main.js -o build.js --mode development

运行结果

通过 live-server 开启本地服务,然后运行 index.html,发现运行结果是预期的结果:

3.jpg

三、webpack 打包产物分析:

非 production 的打包结果还是蛮大的,整个文件有98行。

/******/ (function(modules) { // webpackBootstrap
/******/     // The module cache
/******/     var installedModules = {};
/******/
/******/     // The require function
/******/     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,
/******/             l: false,
/******/             exports: {}
/******/         };
/******/
/******/         // 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;
/******/     }
/******/
/******/
/******/     // expose the modules object (__webpack_modules__)
/******/     __webpack_require__.m = modules;
/******/
/******/     // expose the module cache
/******/     __webpack_require__.c = installedModules;
/******/
/******/     // define getter function for harmony exports
/******/     __webpack_require__.d = function(exports, name, getter) {
/******/         if(!__webpack_require__.o(exports, name)) {
/******/             Object.defineProperty(exports, name, {
/******/                 configurable: false,
/******/                 enumerable: true,
/******/                 get: getter
/******/             });
/******/         }
/******/     };
/******/
/******/     // define __esModule on exports
/******/     __webpack_require__.r = function(exports) {
/******/         Object.defineProperty(exports, '__esModule', { value: true });
/******/     };
/******/
/******/     // getDefaultExport function for compatibility with non-harmony modules
/******/     __webpack_require__.n = function(module) {
/******/         var getter = module && module.__esModule ?
/******/             function getDefault() { return module['default']; } :
/******/             function getModuleExports() { return module; };
/******/         __webpack_require__.d(getter, 'a', getter);
/******/         return getter;
/******/     };
/******/
/******/     // Object.prototype.hasOwnProperty.call
/******/     __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/     // __webpack_public_path__
/******/     __webpack_require__.p = "";
/******/
/******/
/******/     // Load entry module and return exports
/******/     return __webpack_require__(__webpack_require__.s = "./main.js");
/******/ })
/************************************************************************/
/******/ ({

/***/ "./app.js":
/*!****************!*\
  !*** ./app.js ***!
  \****************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\r\n  template: `<div> <h1>App.js</h1> </div>`\r\n});\n\n//# sourceURL=webpack:///./app.js?");

/***/ }),

/***/ "./main.js":
/*!*****************!*\
  !*** ./main.js ***!
  \*****************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _app_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./app.js */ \"./app.js\");\n\r\n\r\nnew Vue({\r\n  el: '#app',\r\n  components: {App: _app_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"]},\r\n  template: `<div> <App/> </div>`\r\n});\n\n//# sourceURL=webpack:///./main.js?");

/***/ })

/******/ });

虽然代码有点多,但是本质上也就是一个立即执行函数:

4.jpg

首先看参数部分,参数部分是一个对象,其中,对象的每个属性的 key 实际上就是文件名,比如项目中的 app.jsmain.js,说是项目名,不如说是路径更准确些,其中每个 key(文件路径)的 value 则是定义的一个方法。

最终, webpack 将传入的参数通过下面代码执行:

modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

moduleId 则是立即执行函数的 key,而 key 对应的 value 则通过 call 传参调用:

以 app.js 举例, app.js 对应的属性值是:

(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\r\n  template: `<div> <h1>App.js</h1> </div>`\r\n});\n\n//# sourceURL=webpack:///./app.js?");
/***/ })

第一个参数 module 就是 call 里面的 module,需要注意的是 call 里面的 module.exports 是 this 指向(这个不明白的可以去看 javascript 的 call / apply / bind 部分)。而从 module 开始,参数是一一对应的。

实际上就是在 function __webpack_require__(moduleId) {} 这个方法中,通过 moduleId 拿到属性值,因为属性值是一个定义的函数,然后在通过 call 去执行这个函数,并且将 module/module.exports/__webpack_require__ 传入给属性值定义的方法,从而能够在这个方法中起作用。

而形参和实参的对象关系是:

形参实参
modulemodule
__webpack_exports__module.exports
__webpack_require____webpack_require__

webpack_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,
/******/             l: false,
/******/             exports: {}
/******/         };
/******/
/******/         // 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;
/******/     }

这个方法是在最外层的立即执行函数中调用的, 是 return __webpack_require__(__webpack_require__.s = "./main.js"); 这行代码调用了,虽然 __webpack_require__.s = "./main.js" 这个很长,但实际上就是把 ./main.js 这个入口文件传进去了而已,而 __webpack_require__.s 也没使用,s 应该是 start 的意思,这里应该只是语义上让人更好理解。

执行了 return __webpack_require__(__webpack_require__.s = "./main.js"); 之后,就已经走进了最外层立即执行函数传入参数中通过 ./main.js 拿到的 value 方法,然后去执行。

eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _app_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./app.js */ \"./app.js\");\n\r\n\r\nnew Vue({\r\n  el: '#app',\r\n  components: {App: _app_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"]},\r\n  template: `<div> <App/> </div>`\r\n});\n\n//# sourceURL=webpack:///./main.js?");

可以发现,这里面又有 __webpack_require('./app,js') 说明依赖了 app.js,因此又会继续去走,知道所有的依赖都能够拉出来,这时候我是需要先走 __webpack_require__ 拉出依赖,才能保证代码运行依赖是存在的。

四、生产环境的打包结果:

生产环境就简单太多了,他会把一些内容直接拉出来,然后塞进去,比如立即执行函数可能只传入一个实参:

! function (e) {
  var t = {};

  function n(r) {
    if (t[r]) return t[r].exports;
    var o = t[r] = {
      i: r,
      l: !1,
      exports: {}
    };
    return e[r].call(o.exports, o, o.exports, n), o.l = !0, o.exports
  }
  n.m = e, n.c = t, n.d = function (e, t, r) {
    n.o(e, t) || Object.defineProperty(e, t, {
      configurable: !1,
      enumerable: !0,
      get: r
    })
  }, n.r = function (e) {
    Object.defineProperty(e, "__esModule", {
      value: !0
    })
  }, n.n = function (e) {
    var t = e && e.__esModule ? function () {
      return e.default
    } : function () {
      return e
    };
    return n.d(t, "a", t), t
  }, n.o = function (e, t) {
    return Object.prototype.hasOwnProperty.call(e, t)
  }, n.p = "", n(n.s = 0)
}([function (e, t, n) {
  "use strict";
  n.r(t), new Vue({
    el: "#app",
    components: {
      App: {
        template: "<div> <h1>App.js</h1> </div>"
      }
    },
    template: "<div> <App/> </div>"
  })
}]);