前端打包路(1) - 基本原理&发展历史&webpack初介绍

346 阅读9分钟

前言

后记:由于webpack的知识体系太庞大了,而且webpack是一个很有深度的框架,所以我们拆分一下,这次先来讲一下打包的基本原理和历史,后面会尽可能深的介绍:(2)原理(3)实战&webpack优化 (4) Tapable (5) tree-shaking (6) sourceMap (7) HMR

前端打包、构建、gulp、grunt、webpack、rollup、一堆名词,之前没有好好的系统性学习过,这次抽空系统的捋一捋。。。
可以说随着node的出现,前端变得越来越复杂,因为js代码不再是只能运行在浏览器里面的弱鸡语言,随之带来的是同样在服务器上运行的能力。我认为带来最大的利好就是前端项目也可以“工程化”了,就像C一样,具备了:预处理、编译、汇编、链接的能力。当然javascript是一门解释型语言,所以就没有后面三步了,前端打包多少类似于预处理+模块化的过程。

理解前端模块化

为啥要模块化:难道都写在main.js里面?如何复用?如何协同开发?
但是js不像其他

作用域

全局作用域、局部作用域 
全局: window, global
<!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>Document</title>
</head>
<body>
  <script src="./moduleA.js"></script>
  <script src="./moduleB.js"></script>
  <script src="./moduleC.js"></script>
</body>
</html>
// moduleA.js
var a = 1;
// moduleB.js
var a = 2;
// moduleC.js
var b = a + 1;
console.log('b = ', b);
结果:b = 3

显然被覆盖了。怎么办呢?

命名空间

//moduleA
var a = {
  value: 1,
};
//moduleB
var b = {
  value: 1,
};
//moduleC
console.log('moduleA的value', a.value);
console.log('moduleB的value', b.value);
结果:
moduleA的value 1
moduleB的value 2

看上去解决了上面的问题,但是随之而来的问题:

a.value = 100;

这样很容易就改变内部的一个变量了 所以我们需要利用作用域和闭包来改造一把

  var moduleA = (function() {
    var name = 'Nolan';

    return {
      myNameIs: function() {
        console.log('请叫我', name);
      },
    };
  })();

这是一个立即执行函数。

moduleA.myNameIs();
// 请叫我Nolan

moduleA.name;
// undefined

很明显暴露了该暴露的、隐藏了该隐藏的。
接下来我们再优化一下写法

(function(window) {
  var name = 'Nolan';
  function myNameIs() {
    console.log('请叫我', name);
  }
  
  window.moduleA = { myNameIs };
})(window)

如果你撸过webpack打包后的代码,对比一下,是不是很像了?

总结一下

所以我们看到一个技术是循序渐进出来的,想想手写一个js继承是不是也是一步一步解决问题,遇到新的问题,再解决问题,最终产生的。
优点:

  • 作用域封装
  • 重用性
  • 解除耦合

模块化

History

  • AMD
  • COMMONJS
  • ES6 MODULE

AMD

define('moduleName', ['lodash'], function(_) {
  return function(a, b) {
    console.log(a, b);
  };
});

比如 requireJS 后来衍生出了 玉伯大神的成名作 sea.js(CMD)

COMMONJS

2009年推出,主要为了规范服务端开发,并不是针对浏览器的规范。所以后来Nodejs也引用了此标准。

const moduleA = require('./moduleA');

exports.getSum = function(a, b) {
  console.log(a + b);
}

与AMD相同,强调了引入的模块。

ES6 MODULE

与COMMONS很像

import moduleA from './moduleA';

export function getName(name) {
  return name;
}

期间诞生了很多可以打包的工具:
Gulp,Grunt是自动化构建工具,这里要强调自动化是因为他们不仅可以做打包,自动化是其核心目的。
而webpack的出现可以说是专注于打包。

webpack

先来看一个小例子

首先我们先创建一个webpack-test的工程 下面包括 index.html、src/index.js和src/util.js。 npm安装 webpack以及webpack-cli工具
目前使用的是4.x.x版本。v5对tree-shake进行了性能优化,所以构建出的结果会有所不同。后面我们会介绍tree-shake是什么。 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>My Webpack Test</title>
</head>
<body>
  <script src="./dist/main.js"></script>
</body>
</html>

index.js

const num = require('./util');

function test() {
  console.log('我是一个小测试!', num);
}

test();

util.js

exports.default = 123;

接下来在根目录执行 npx webpack 或者 ./node_module/.bin/webpack 会生成dist/main.js的文件,打开文件我们看下结构

! 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, {
            enumerable: !0,
            get: r
        })
    }, n.r = function(e) {
        "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, {
            value: "Module"
        }), Object.defineProperty(e, "__esModule", {
            value: !0
        })
    }, n.t = function(e, t) {
        if (1 & t && (e = n(e)), 8 & t) return e;
        if (4 & t && "object" == typeof e && e && e.__esModule) return e;
        var r = Object.create(null);
        if (n.r(r), Object.defineProperty(r, "default", {
                enumerable: !0,
                value: e
            }), 2 & t && "string" != typeof e)
            for (var o in e) n.d(r, o, function(t) {
                return e[t]
            }.bind(null, o));
        return r
    }, 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) {
    const r = n(1);
    console.log("我是一个小测试!", r)
}, function(e, t) {
    t.default = 123
}]);

前面那一大坨我们先不管

简化就是(function(module){})([index.js, util.js])

看看结构发现是不是就是一个立即执行函数!所以说,高大上的webpack也只不过是通过前面提到的立即执行函数来实现的。那前面那一大堆不是人写的代码是什么呢?为啥会变成这个鸟样?我们接下来先让代码变得可读一些。

webpack --help可以看到--mode这样一个参数,developmentproduction两个值。默认是production,我们在运行npx webpack的时候也可以看到这样的输出:

image.png

接下来我们执行一把看看吧~npx webpack --mode=development

(function(modules) { // webpackBootstrap
  // 定义一个缓存
  var installedModules = {};
  // 可以称之为webpack运行在浏览器上的require方法,参数就是立即执行函数的参数中的key值
  function __webpack_require__(moduleId) {
    // 有缓存就返回缓存中的数据
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // 开开心心放入缓存,注意这里定义了exports对象
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };
    // 执行函数
    // 思考一下,我们在IDE里面疯狂无脑写着import/require/exports/export这些模块
    // 跑到浏览器上运行的时候,浏览器哪儿知道这些玩意儿是干蛋的,但是浏览器知道啥?
    // 知道对象、知道函数,所以我们把模块的导出存在了module.exports里面,module.exports在前面刚被初始化干干净净的被call
    // 再遇到require不怕了,其实就是__webpack_require__这个方法嘛
    // 所以require()的参数是模块的路径也就是立即执行函数参数中的key
    // 而exports的就是个对象
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    // 不重要
    module.l = true;
    // require(一个文件路径), 这个文件exports的那些玩意儿
    return module.exports;
  }
  // 为内置的require对象添加依赖模块
  __webpack_require__.m = modules;
  // 为内置的require对象添加缓存
  __webpack_require__.c = installedModules;

  // exports对象添加一个getter方法
  __webpack_require__.d = function(exports, name, getter) {
    if(!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
  };
  // 下面的例子中讲
  __webpack_require__.r = function(exports) {
    if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      // Object.prototype.toString.call(exports)的时候返回的是Module
      // 感觉就是看上去好看
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', { value: true });
  };
  // 说实话,我也不太清楚这个方法是干啥的
  // 但是我翻了下github上有人提问,解释是:“ESM CJS interop. import("commonjs") will use it.”
  // 地址粘贴在下方: https://github.com/webpack/webpack/issues/11024
  __webpack_require__.t = function(value, mode) {
    if(mode & 1) value = __webpack_require__(value);
    if(mode & 8) return value;
    if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
    var ns = Object.create(null);
    __webpack_require__.r(ns);
    Object.defineProperty(ns, 'default', { enumerable: true, value: value });
    if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
    return ns;
  };
  // 下面的例子中讲
  __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;
  };
  // 包含属性否
  __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
  // 不重要
  __webpack_require__.p = "";
  // require入口文件吧
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})({
  // 入口方法
  "./src/index.js": (function(module, exports, __webpack_require__) {
    eval("const num = __webpack_require__(/*! ./util */ \"./src/util.js\");\n\nfunction test() {\n  console.log('我是一个小测试!', num);\n}\n\ntest();\n\n\n//# sourceURL=webpack:///./src/index.js?");
  }),
  // 被引入的方法
  "./src/util.js": (function(module, exports) {
    eval("exports.default = 123;\n\n//# sourceURL=webpack:///./src/util.js?");
  })
});

这次变得好阅读一些了。我把函数的作用写在注释上。

那我们再尝试一个ES6 MODULE和CJS混用的
我们修改一下index.js

import { num } from './util';

function test() {
  console.log('我是一个小测试!', num);
}

test();

还有util.js

exports.num = 123;

让我们再次打包看下结果,重复的部分我们就不说了,主要集中在不同上 __webpack_require__.n__webpack_require__.r还有eval里面,可以跟着标注顺序看

(function (modules) { // webpackBootstrap
  var installedModules = {};
  function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    module.l = true;
    return module.exports;
  }
  __webpack_require__.m = modules;
  __webpack_require__.c = installedModules;
  // 20.说白了就是以后在调用exports.a的时候用我们传进来的getter,这个例子里面也就是getDefault
  // 21.如果你在index.js里面这么用的 import X from './util';
  // 22.console.log(X)的话,其实就是.a的getter。打完收工。
  __webpack_require__.d = function (exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, {
        enumerable: true,
        get: getter
      });
    }
  };
  // 3.这个Symbol.toStringTag理解为当我们Object.prototype.toString.call(exports)时,返回[object Module]类型
  __webpack_require__.r = function (exports) {
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, {
        value: 'Module'
      });
    }
    // 4.为exports对象加了一个属性,标明这个文件是ES6MODULE的哟~
    // 5.回到下面eval
    Object.defineProperty(exports, '__esModule', {
      value: true
    });
  };
  // 11.传入的exports对象是不是ES6Module啊?
  // 12.如果是,我们是不是会export default X一个默认值,然后import X就完了
  // 13.在es6 module中我们export default X其实是这样一个操作 default = X;
  // 14.没错就是个赋值,所以我们这样是会报错的  export default const X;
  // 15.转化成default = const X绝逼有问题对吧
  // 16.所以这里我们就用getDefault方法默认帮你返回exports.default值了
  // 17.d方法其实就是重写getter方法。
  // 18.我们看下三个参数getter现在就是 getDefault()这个方法了,'a'是我们命名的一个参数名
  // 19.我们去.d方法里面瞅一眼
  __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;
  };
  __webpack_require__.o = function (object, property) {
    return Object.prototype.hasOwnProperty.call(object, property);
  };
  __webpack_require__.p = "";
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({
  "./src/index.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      // 1.由于我们在index.js中使用了import所以,webpack贴心的在eval前面插入了一行代码调用了r方法
      // 2.我们去看看r干了啥
      // 6.回来了继续看下
      // 7.调用了__webpack_require__.n方法,传递的是import进来的文件中的exports对象
      // 8.所以我们要去瞅一眼import的文件是谁,没错是下面的./src/util.js
      // 9.所以他的exports对象上并没有一个叫__esModule的属性,我们继续
      // 10.去看下.n方法
      eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./util */ \"./src/util.js\");\n/* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_util__WEBPACK_IMPORTED_MODULE_0__);\n\n\nfunction test() {\n  console.log('我是一个小测试!', _util__WEBPACK_IMPORTED_MODULE_0__[\"num\"]);\n}\n\ntest();\n\n\n//# sourceURL=webpack:///./src/index.js?");
    }),
  // 23.这个文件没有用es6 module所以并没有像这个方法一样插入__webpack_require__.r方法
  "./src/util.js": (function (module, exports) {
    eval("exports.num = 123;\n\n//# sourceURL=webpack:///./src/util.js?");
  })
});

完球了。。。这下清楚了吧~

聪明的你肯定知道webpack的按需加载,如果这时候让你去实现你会怎么做呢?有几种天然的分隔文件方式呢?多入口?import()?import(/* prefetch 还有 preload*/)了解下?文件分开了,根据分开的文件创建html<link as="script" src="...." rel="prefetch">是不是就可以呢?

原理

叽叽喳喳半天,通过上面的例子,其实我们已经知道了webpack的产物,我们也分析了打包后的产物,那么从原始文件到构建后的文件,这其中又经历了什么呢?


未完待续