概述

webpack 作为目前最主流的代码打包工具,他提供了一整套的前端项目模块化方案,而不仅仅是局限于对JavaScript的模块化。

通过webpack提供的前端模块化方案,我们就可以很轻松的对我们前端项目开发过程中涉及到的所有的资源进行模块化,因为webpack的想法比较先进,而且早之前的使用过程也比较繁琐,再加上他的文档也比较晦涩难懂,所以说在最开始的时候他显得对我们开发者不是十分友好。

但是随着版本的迭代官网的文档也在不断的更新,目前webpack已经非常受欢迎,基本上可以说覆盖了绝大多数现代化的web开发应用项目。

接下来我们就通过一个小案例,先来了解一下webpack的一个基本使用。

我们有一个项目目录,目录中有个src文件夹,src中有两个文件 index.js 和 heading.js, 在src同级有一个index.html文件。

大致的作用也非常简单,我们在heading.js中默认导出一个用于创建元素的函数。

export default () => {
    const element = document.createElement('h2');
    element.textContent = 'Hello word';
    element.addEventListener('click', () => {

    })
    return element;
}

在index.js中去导入这个模块并且使用了他。

import createHeading from './heading.js';

const heading = createHeading();

document.body.append(heading);

除此之外呢,我们在项目的跟目录下有个index.html,在这个文件中我们通过script标签以模块化的方式去引入了index.js

<body>
    <script type="module" src="src/index.js"></script>
</body>

我们打开命令行,通过http-server . 的工具把他运行起来, (可以全局安装http-server模块)。

http-server .

我们可以看到可以正常的工作。没有什么问题。

接下来我们尝试引入webpack去处理我们的js模块。由于webpack是一个npm的工具模块,所以我们先通过yarn init的方式去初始化一个package.json

yarn init

完成过后我们再去安装一下webpack所需要的核心模块以及他对应的cli模块, 通过--dev把他指定为开发依赖。

yarn add webpack webpack-cli --dev

安装过后,webpack-cli程序所提供的cli程序就会出现在我们node_modules的bin目录中,可以通过yarn命令快速找到这样的一个cli并且去运行他。通过yarn webpack --version去执行一下。

yarn webpack --version

有了这样一个webpack之后呢,我们就可以通过他去帮我们打包我们在src下面的js代码了,具体的使用方式就是通过webpack命令去打包。执行yarn webpack命令之后webpack就会自动从src下面的index.js开始打包。

yarn webpack

完成过后控制台会提醒我们,顺着index.js有两个js文件被打包到了一起,与之对应的就是我们在项目的跟目录会多出一个dist目录,我们打包的结果就会存放在这个目录中的一个main.js当中。

这里我们回到index.html中,把js脚本文件的路径修改成dist/main.js。由于打包过程会把import和export转换掉,所以说我们已经不需要type="module"这样一种模块化的方式引入。

<body>
-    <script type="module" src="src/index.js"></script>
+    <script src="dist/main.js"></script>
</body>

完成过后,我们再次启动服务,回到浏览器刷新一下。

http-server .

这里我们的应用仍然可以正常工作。

当然如果每次都通过yarn去运行webpack会比较麻烦的话,可以把webpack这个命令放到package.json当中的script当中,这样就可以直接为我们项目去定义一个build的任务。

"script": {
    "build": "webpack"
}

build任务我们使用webpack去完成就可以了,在命令行执行 yarn build

yarn build

以上就是针对webpack的基本使用。

webpack 配置文件

webpack4.0以后的版本支持零配置的方式直接启动打包,整个打包过程会按照约定将src/index.Js作为打包入口最终打包的结果会存放在dist/main.js当中。

但很多时候我们都需要自定义这样的路径,例如我们的入口文件是src/main.js, 这时我们就需要去为webpack添加专门的配置文件,具体的做法就是在项目的跟目录下添加一个webpack.config.js的一个文件。

这个文件是一个运行在node环境的js文件,也就说我们需要按照CommonJS的方式去编写代码。

这个文件可以去导出一个对象, 我们通过导出对象的属性就可以完成相应的配置选项,例如我们添加一个叫做entry的属性,那这个属性的作用就是指定我们webpack打包入口文件的路径。我们将其设置为src下面的main.js。

这里需要注意的是这个entry的属性,如果是一个相对路径的话,这个前面的./ 是不可以省略的。

module.exports = {
    entry: './src/main.js'
}

这时候我们再去命令行运行yarn build查看效果

yarn build

我们还可以通过output配置我们输出文件的位置, 这个属性要求值是一个对象,可以通过对象中的filename去指定输出文件的名称。例如我们设置成bundle.js

再去运行yarn build, 就可以看到,dist/bundle.js已经正常生成。

我们还可以在output中添加一个path属性去指定我们输出文件所在的目录, 这个属性需要是一个绝对路径,我们需要借助node中的path模块帮我们转换。这里我们也说到了,这是node环境中运行的代码,所以我们可以直接去载入path模块,然后根据path.join(__dirname, 'dist'), 这就可以得到output的完整路径。我们再次运行yarn build就可以看到在项目中生成了output/bundle.js。

const path = require('path');

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
    }
}

webpack 工作模式

webpack新增了一个工作模式的用法,这种用法大大简化了webpack配置的复杂程度,可以理解成针对不用环境的几组预设的配置,我们回到开发环境中,重新去运行webpack的打包命令。

打包过程中命令行会报出一个命令行警告,大致的意思就是说我们没有去设置一个叫做mode的属性,webpack会默认使用production的模式去工作。

在这个模式下webpack会自动启动一些优化插件,例如自动帮我们代码压缩,这对实际生产环境是非常友好的,但是对打包的结果我们没办法去阅读。

我们可以通过cli去指定打包的模式,具体的用法就是给webpack的命令去传入一个--mode的参数,这个属性我们有三种取值,默认是production,默认production会自动的去启动优化,优化我们的打包结果。

这里我们再尝试一下development,也就是开发模式。开发模式webpack会自动去优化打包的速度,他会去添加一些调试过程需要的服务到我们的代码当中,这些我们后面在介绍调试的时候会单独再介绍。

yarn webpack --mode=development

除此之外还有一个node模式,在node模式下,webpack就是运行最原始状态的打包,他不会去做任何额外的处理,目前我们的工作模式只有这三种。具体这三种的差异我们可以在官方文档中找到。

yarn webpack --mode=none

除了我们通过cli参数去指定工作模式,我们还可以在webpack的配置文件当中去设置工作模式,具体做法就是我们在配置文件的配置目录中去添加一个mode属性,这样的话webpack命令就会根据我们配置中的模式,去工作了,我们就不需要再去指定--mode的参数了。

打包结果运行原理

我们来一起解读一下通过webpack打包过后的结果,为了可以更好的理解我们打包过后的代码,我们先将webpack的工作模式设置成node。

const path = require('path');

module.exports = {
    mode: 'node',
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
    }
}

这样的话就是以最原始的状态去打包,我们回到命令行终端,再次去运行webpack的命令。去执行打包。

yarn webpack

完成过后我们去打开生成的bundle.js文件, 我们先把我们整体的结构折叠起来以便于我们更好的对结构了解。快捷键是ctrl + k 和 ctrl + 0。

/******/ (function(modules) { // 接收参数位置
/******/ })
/******/ ([ // 调用位置
/******/ ]);

。我们可以看到整体生成的代码是一个立即执行函数。这个函数是webpack的工作入口。接收一个叫做modules的参数。调用的时候我们传入了一个数组。

/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
/***/ })
/******/ ]);

展开这个数组,数组中的每一个参数都是一个参数列表相同的函数。这里的函数对应的就是我们源代码中的模块,也就是说我们的每一个模块最终都会被包裹到这样的一个函数中,从而去实现模块的私有作用域。

/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _heading_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);


const heading = Object(_heading_js__WEBPACK_IMPORTED_MODULE_0__["default"])();

document.body.append(heading);

/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = (() => {
    const element = document.createElement('h2');
    element.textContent = 'Hello word';
    element.addEventListener('click', () => {

    })
    return element;
});

/***/ })
/******/ ]);

我们再来看一下webpack的工作入口函数,这个函数内部并不复杂,而且注释也非常清晰,最开始先定义了一个对象(installedModules),用于去存放或者叫缓存我们加载过的模块。

然后紧接着定义了一个webpack_require函数,顾名思义,这个函数就是用来加载模块的,在往后就是向webpack_require这个函数上挂载了一些数据,和一些工具函数。这些并不重要。

我们接着往后看,这个函数执行到最后调用了调用了require函数,传入了一个webpack_require.s = 0开始去加载我们的模块,这个地方的模块id呢实际上就是我们上面的模块数组当中的元素下标,也就是说这里才开始加载我们在源代码当中所谓的入口模块。

/******/ (function(modules) { // webpackBootstrap
/******/     // The module cache
/******/     var installedModules = {};
/******/
/******/     // The require function
/******/     function __webpack_require__(moduleId) {
/******/     }
/******/
/******/
/******/     // 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) {
/******/     };
/******/
/******/     // define __esModule on exports
/******/     __webpack_require__.r = function(exports) {
/******/     };
/******/
/******/     // create a fake namespace object
/******/     // mode & 1: value is a module id, require it
/******/     // mode & 2: merge all properties of value into the ns
/******/     // mode & 4: return value when already ns object
/******/     // mode & 8|1: behave like require
/******/     __webpack_require__.t = function(value, mode) {
/******/     };
/******/
/******/     // getDefaultExport function for compatibility with non-harmony modules
/******/     __webpack_require__.n = function(module) {
/******/     };
/******/
/******/     // 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 = 0);
/******/ })

为了可以更好的理解他的执行过程我们可以让他运行起来,然后我们通过浏览器的开发工具去单步调试一下。打开命令行终端,我们通过http-server去启动浏览器。

http-server ./

打开我们的开发工具,在source面板中找到bundle.js这样一个文件,在这个文件当中我们找到他的入口函数(installedModules位置),打上断点。刷新页面,启动调试那这个函数在一开始运行的时候接收到的参数实际上就是我们两个模块所对应的两个函数。我们直接向下执行,对于不重要的环节直接跳过。

在最后部分加载了我们id为0的模块,然后我们进入到这个webpack_require函数中。

webpack_require内部先判断了这个模块有没有被加载过,如果加载了就从缓存里面读,如果没有就创建一个新的对象。创建过后开始调用这个模块对应的函数,把我们刚刚创建的模块对象(module), 导出成员对象(module.exports),webpack_require函数作为参数传入进去。这样的话我们在我们模块的内部就可以使用module.exports去导出成员。通过webpack的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;
/******/     }

进入到模块中,发现,在模块内部先去调用了一个webpack_require.r函数,这个函数的作用非常简单,就是给我们导出对象上面添加一个标记。我们去看一下。

/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _heading_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);


const heading = Object(_heading_js__WEBPACK_IMPORTED_MODULE_0__["default"])();

document.body.append(heading);

/***/ }),

函数webpack_require.r,可以发现实际上就是在我们导出对象上定义了一个__esModule的标记,定义过后我们这个导出对象上就有了这样一个标记,用来对外界表明我们这是一个ES Module。

/******/     // define __esModule on exports
/******/     __webpack_require__.r = function(exports) {
/******/         if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/             Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/         }
/******/         Object.defineProperty(exports, '__esModule', { value: true });
/******/     };

然后再紧接着往下,这时候又调用了webpack_require函数,此时传入的id是1,也就是说用来去加载第一个模块。

/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _heading_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);


const heading = Object(_heading_js__WEBPACK_IMPORTED_MODULE_0__["default"])();

document.body.append(heading);

/***/ }),

这个模块就是我们在代码当中export的那个heading,然后再去以相同的道理去执行heading这个模块, 最后将heading模块导出的这个整体的对象通过return函数给他return回去。

/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = (() => {
    const element = document.createElement('h2');
    element.textContent = 'Hello word';
    element.addEventListener('click', () => {

    })
    return element;
});

/***/ })

module.exports是一个对象,因为我们ES Module里面默认导出是放在default里面,这时候我们把模块导出的对象拿到。然后访问里面的default,然后调用default函数,内部还是调用我们内部模块的那些代码,最终将我们创建完的元素拿到了,最后append到我们的body上面。

/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _heading_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);


const heading = Object(_heading_js__WEBPACK_IMPORTED_MODULE_0__["default"])();

document.body.append(heading);

/***/ }),

这就是一个大致的执行过程。有了这样一个运行过程其实我们应该了解了,webpack其实大包过后的代码并不会特别的复杂,只是说帮我们把我们所有的模块放到了同一个文件当中,除了放到同一个文件当中他还的提供一个基础代码让我们这么模块与模块之间相互依赖的关系还可以保持原有的那样一个状态,这个实际上就是我们前面所看到的webpack bootstrap的作用。

以上就是我们webpack打包过后代码的一个工作的原理,下面贴出全部代码

/******/ (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, { enumerable: true, get: getter });
/******/         }
/******/     };
/******/
/******/     // define __esModule on exports
/******/     __webpack_require__.r = function(exports) {
/******/         if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/             Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/         }
/******/         Object.defineProperty(exports, '__esModule', { value: true });
/******/     };
/******/
/******/     // create a fake namespace object
/******/     // mode & 1: value is a module id, require it
/******/     // mode & 2: merge all properties of value into the ns
/******/     // mode & 4: return value when already ns object
/******/     // mode & 8|1: behave like require
/******/     __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;
/******/     };
/******/
/******/     // 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 = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _heading_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);


const heading = Object(_heading_js__WEBPACK_IMPORTED_MODULE_0__["default"])();

document.body.append(heading);

/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = (() => {
    const element = document.createElement('h2');
    element.textContent = 'Hello word';
    element.addEventListener('click', () => {

    })
    return element;
});

/***/ })
/******/ ]);