webpack4.x 打包工具 | (一)

750 阅读12分钟

1. 打包工具的历史

模块化很好的解决了复杂应用开发中的代码组织问题,但随着引入模块化,又会产生一些新的问题。所使用的ES Modules本身就存在环境兼容问题,尽管现如今主流浏览器最新版本都已经支持这一特性。但是目前还没办法做到统一所有用户浏览器的使用情况,所以还需要解决兼容问题。其次通过模块化的方式,划分出的模块文件比较多,前端应用又是运行在浏览器当中的。应用中所需要的每一个文件,都需要从服务器中请求回来,这些零散的模块文件必将导致浏览器频繁请求,从而影响应用的工作效率。

对于整个开发过程而言,模块化肯定是有必要的,只是需要在原有的基础之上引入更好的方案或工具去解决上面几个问题,让开发者在应用的开发阶段可以享受模块化所带来的优势又不必担心模块化对生产环境所产生的一些影响。

首先希望有一个工具能够编译代码,就是将开发阶段包含新特性的代码直接转换为能够兼容绝大多数环境的代码,这样一来环境兼容问题也就不存在了。其次是能够将散落的模块文件打包到一起,这就解决了浏览器中频繁对模块文件发出请求的问题。

至于模块化文件划分,只是在开发阶段需要他,因为它能够更好的组织代码对于运行环境实际上是没有必要的,所以可以选择在开发阶段通过模块化的方式去编写。在生产阶段还是打包到同一个文件中,最后还需要支持不同种类的前端资源类型,这样就可以把前端开发过程当中所涉及到的样式、图片、字体等所有资源文件都当做模块使用,对于整个前端应用来讲就有了一个统一的模块化方案了。

前端领域目前有一些工具很好的解决了以上这几个问题,其中最为主流的就是webpackparcelrollup。以webpack为例,一些核心特性就很好的满足了上面所说的需求。

首先webpack作为一个模块打包工具(Module Bundler)他本身就可以解决模块化js代码打包的问题,通过webpack可以将一些零散的模块代码打包到同一个js文件中。对于代码中那些有环境兼容问题的代码可以在打包的过程中通过模块加载器(Loader)对其进行编译转换。其次,webpack还具备代码拆分(Code Splitting)的理念,能够将应用中所有的代码都按照需要进行打包。这样一来就不用担心代码全部打包到一起文件较大的问题了。

可以把应用加载过程中初次运行所必须的模块打包到一起,对于其他的那些模块单独存放。等应用工作过程中实际需要某个模块再异步加载这个模块从而实现增量加载或渐进式加载,这样就不用担心文件太碎或是文件太大这两个极端问题。

webpack支持在js中以模块化的方式载入任意类型的资源文件,例如在webpack当中可以通过js直接import一个css文件。这些css文件最终会通过style标签的形式工作,其他类型的文件也可以有类似的这种方式去实现。

2. 快速上手

webpack 作为目前最主流的代码打包工具提供了一整套的前端项目模块化方案而不仅仅局限于对js的模块化。通过webpack提供的前端模块化方案,可以很轻松的对前端项目涉及到的所有的资源进行模块化。

这里有一个项目,目录中有个src文件夹,src中有两个文件 index.jsheading.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 .

可以看到正常的工作。下来引入webpack处理js模块。首先以通过yarn init的方式去初始化一个package.json

yarn init

完成过后安装webpack所需要的核心模块以及对应的cli模块。

yarn add webpack webpack-cli --dev

有了webpack之后就可以打包src下面的js代码了。执行yarn webpack命令webpack会自动从src下面的index.js开始打包。

yarn webpack

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

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

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

再次启动服务,应用仍然可以正常工作。

http-server .

可以把webpack命令放到package.json中的script,通过yarn build打包。

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

3. 配置文件

webpack4.0后的版本支持零配置打包,整个打包过程会按约定将src/index.js作为入口结果存放在dist/main.js中。很多时候需要自定义路径,例如入口文件是src/main.js,这就需要为webpack添加配置文件,在项目的跟目录添加webpack.config.js文件即可。这个文件运行在node环境也就说需要按照Commonjs的方式编写代码。

文件导出一个对象, 通过导出对象的属性可以完成相应的配置选项,例如entry属性指定webpack打包入口文件的路径。可以将其设置为./src/main.js

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

可以通过output配置输出文件的位置,属性值是一个对象,对象中的filename指定输出文件的名称,path属性指定输出文件的目录需要是一个绝对路径。

const path = require('path');

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

运行yarn build就在项目中生成了dist/bundle.js

4. 工作模式

webpack新增了工作模式简化了webpack配置的复杂度,可以理解成针对不用环境的几组预设的配置,webpack可以设置一个mode属性,如不设置默认会使用production模式工作。在这个模式下webpack会自动启动一些优化插件,例如代码压缩。

可以在webpack启动时传入--mode的参数,这个属性有三种取值,默认是production,还有development也就是开发模式。开发模式webpack会自动优化打包的速度,会添加一些调试过程需要的服务到代码中。

yarn webpack --mode=development

node模式就是运行最原始状态的打包,不会去任何额外的处理。

yarn webpack --mode=none

除了通过cli参数指定工作模式,还可以在webpack的配置文件中设置工作模式,在配置文件的配置中添加mode属性就可以了。

const path = require('path');

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

5. 打包结果分析

首先先将webpack的工作模式设置成node。这样就是以最原始的状态打包。

const path = require('path');

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

完成过后打开生成的bundle.js文件,可以把整体结构折叠起来以便于对结构了解。快捷键是ctrl + kctrl + 0

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

/******/ (function(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__函数上挂载了一些数据和一些工具函数。

这个函数执行到最后调用了__webpack_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);
/******/ })

__webpack_require__内部先判断了这个模块有没有被加载过,如果加载了就从缓存里面读,如果没有就创建一个新的对象。创建过后开始调用这个模块对应的函数,把刚刚创建的模块对象(module),导出成员对象(module.exports),__webpack_require__函数作为参数传入进去。这样的话在模块的内部就可以使用module.exports导出成员,通过__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函数,这个函数的作用是给导出对象添加一个标记,用来对外界表明这是一个ES Module

/* 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函数。

/******/ 	// 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__函数,此时传入的id1,也就是说用来加载第一个模块。

/* 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);

/***/ }),

这个模块就是代码中exportheading,以相同的道理执行heading模块,将heading模块导出的对象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函数将创建完的元素拿到appendbody上面。

/* 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的作用。

打包的全部代码如下。

/******/ (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;
});

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

6. 模块依赖方式

css文件也可以作为打包的入口,不过webpack的打包入口一般还是js,打包入口从某种程度来说可以算是应用的运行入口。就目前而言前端应用中的业务是由js驱动的,可以在js代码当中通过import的方式引入css文件。

import createHeading from './heading.js';

import './style.css';

const heading = createHeading();

document.body.append(heading);

在webpack.config.js中配置cssloadercss-loaderstyle-loader 需要安装到项目中。然后将loader需要配置到configmodule中。

yarn add css-loader style-loader --dev
const path = require('path');

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            }
        ]
    }
}

运行打包命令启动项目后,样式是可以生效的。传统模式开发是将文件单独分开单独引入,webpack建议在js中去引入css,甚至编写代码中引入资源都可以在js中印日。因为真正需要资源的不是应用,而是正在编写的代码,代码想要正常工作就必须要加载对应的资源,这就是webpack的哲学。一开始可能不太容易理解,换种方式理解假设样式单独引入到页面中,如果代码更新了不再需要这个样式资源了,是不是需要手动的删除。通过js的代码引入文件或者建立js和文件之间的依赖关系是有明显优势的。

js代码本身是负责完成整个业务的功能,放大来看就是驱动了整个前端应用,在实现业务功能的过程当中可能需要用到样式或图片等一系列的资源文件。如果建立了这种依赖关系,一来逻辑上比较合理,因为js确实需要这些资源文件的配合才能实现对应的功能,二来可以保证上线时资源文件不缺失,而且每一个上线的文件都是必要的。

7. 文件资源加载器

webpack社区提供了非常多的资源加载器,基本上开发者能想到的合理需求都有对应的loader,接下来尝试一些非常有代表性的loader,首先是文件资源加载器。

大多数文件加载器都类似于css-loader,是将资源模块转换为js代码的实现方式进行工作,但是有一些经常用到的资源文件例如图片或字体这些文件是没办法通过js表示的。对于这类的资源文件,需要用到文件的资源加载器也就是file-loader

在项目中添加一张普通的图片文件,通过import 的方式导入这张图片。接收模块文件的默认导出也就是文件的资源路径,创建img元素把src设置成文件,最后将元素appendbody中。


import createHeading from './heading.js';

import './style.css';

import icon from './icon.png';

const heading = createHeading();

document.body.append(heading);

const img = new Image();

img.src = icon;

document.body.append(img);

这里导入了一个webpack不能识别的资源所以需要修改webpack配置。为png文件添加一个单独的加载规则配置,test属性设置.png结尾,use属性设置为file-loader,这样webpack打包的时候就会用file-loader处理图片文件了。

yarn add file-loader --dev
const path = require('path');

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: 'file-loader'
            }
        ]
    }
}

打包过后dist目录中会多出一个图片文件,这个文件就是代码中导入的图片,不过文件名称发生了改变。文件模块代码只是把生成的文件名称导出了。

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

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = (__webpack_require__.p + "e177e3436b8f0b3cfff0fd836ea3472c.png");

/***/ })

入口模块直接使用了导出的文件路径(__webpack_require__(6))img.src = _icon_png__WEBPACK_IMPORTED_MODULE_2__["default"];

/***/ (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);
/* harmony import */ var _style_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);
/* harmony import */ var _style_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_style_css__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var _icon_png__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6);

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

document.body.append(heading);

const img = new Image();

img.src = _icon_png__WEBPACK_IMPORTED_MODULE_2__["default"];

document.body.append(img);

/***/ })

启动应用发现图片并不能正常的加载,控制台终端可以发现直接加载了网站根目录的图片,而网站根目录并没有这个图片所以没有找到。图片应该在dist目录当中。这个问题是由于index.html并没有生成到dist目录,而是放在了项目的跟目录,所以这里把项目的跟目录作为了网站的跟目录,而webpack会认为所有打包的结果都会放在网站的跟目录下面,所以就造成了这样一个问题。

通过配置文件去webpack打包过后的文件最终在网站当中的位置,具体的做法就是在配置文件中output位置添加publicPath。这里设置为dist/斜线不能省略。

const path = require('path');

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: 'file-loader'
            }
        ]
    }
}

完成以后重新打包,这一次在文件名称前面拼接了一个变量。

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

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = (__webpack_require__.p + "e177e3436b8f0b3cfff0fd836ea3472c.png");

/***/ })

这个变量在webpack内部的代码提供的就是设置的publicPath(\__webpack_require__.p = "dist/";)


/******/ (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 = "dist/";
/******/
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(__webpack_require__.s = 0);
/******/ })

webpack在打包时遇到图片文件,根据配置文件中的配置,拼配到对应的文件加载器,此时文件加载器开始工作,先是将文件拷贝到输出的目录,然后再将文件拷贝到输出目录的路径作为当前模块的返回值返回,这样对于应用来说,所需要的资源就被发布出来了,同时也可以通过模块的导出成员拿到资源的访问路径。

8. url加载器

file-loader这种通过copy文件的形式处理文件资源外还有一种通过Data URLs的形式表示文件。Data URLs是一种特殊的url协议,可以直接表示文件,传统的url要求服务器上有对应的文件,然后通过地址,得到服务器上对应的文件。而Data URLs本身就是文件内容,在使用这种url的时候不会再去发送任何的http请求,比如常见的base64格式。

data:[mediatype][;base64],\<data>

data:表示协议,[mediatype][;base64]表示媒体类型和编码,\<data>则是具体的文件内容。例如下面给出的Data URLs,浏览器可以根据这个url解析出html类型的文件内容,编码是url-8,内容是一段包含h1html代码。

data:text/html;charset=UTF-8,<h1>html content</h1>

如果是图片或者字体这一类无法通过文本表示的2进制类型的文件,可以通过将文件的内容进行base64编码,以编码后的结果也就是字符串表示这个文件内容。这里url就是表示了一个png类型的文件,编码是base64,再后面就是图片的base64编码。

data:image/png;base64,iVBORw0KGgoAAAANSUhE...SuQmCC

当然一般情况下base64的编码会比较长,这就导致编码过后的资源体积要比原始资源大,不过优点是浏览器可以直接解析出文件内容,不需要再向服务器发送请求。

webpack在打包静态资源模块时,就可以使用这种方式去实现,通过Data URLs以代码的形式表示任何类型的文件,需要用到一个专门的加载器url-loader

yarn add url-loader --dev

webpack配置文件中找到之前的file-loader将其修改为url-loader

const path = require('path');

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: 'url-loader'
            }
        ]
    }
}

此时webpack打包时,再遇到.png文件就会使用url-loader将其转换为Data URLs的形式。打开bundle.js可以发现在最后的文件模块中导出的是一个完整的Data URLs

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

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("data:image/png;base64,iVBORw0KGgoAAAANSUh...AAAABJRU5ErkJggg==");

/***/ })

因为Data URLs中已经包含了文件内容,所以dist中也就不存在独立的.png物理文件了。

这种方式十分适合项目当中体积比较小的资源,如果体积过大会造成打包结果非常大从而影响运行速度。最佳的实践方式是对项目中的小文件通过url-loader转换成Data URLs然后在代码中嵌入,从而减少应用发送请求次数。对于较大的文件仍然通过传统的file-loader方式以单个文件方式存放,从而提高应用的加载速度。

url-loader支持通过配置选项的方式设置转换的最大文件,将url-loader字符串配置方式修改为对象的配置方式,对象中使用loader定义url-loader,然后额外添加options属性为其添加一些配置选项。这里为url-loader添加limit的属性,将其设置为 10kb(10 * 1024),单位是字节。

这样url-loader只会将10kb以下的文件转换成Data URLs,超过10kb的文件仍然会交给file-loader去处理。

const path = require('path');

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        limit: 10 * 1024
                    }
                }
            }
        ]
    }
}

9. babel-loader

webpack默认就可以处理代码中的importexport,所以很自然的会有人认为,webpack会自动编译ES6的代码,实则不然,webpack仅仅完成模块打包工作,会对代码中的importexport做一些相应的转换,除此之外它并不能转换代码中其他的ES6代码。如果需要webpack在打包过程中同时处理其他ES6特性,需要为js文件配置一个额外的加载器babel-loader

首先需要安装babel-loader,由于babel-loader需要依赖额外的babel核心模块,所以需要安装@babel/core模块和用于完成具体特性转换@babel/preset-env模块。

yarn add babel-loader @babel/core @babel/preset-env --dev

配置文件中为js文件指定加载器为babel-loader,这样babel-loader就会取代默认的加载器,在打包过程当中处理代码中的一些新特性。

const path = require('path');

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /.js$/,
                use: 'babel-loader'
            }
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: 'url-loader'
            }
        ]
    }
}

还需要为babel配置需要使用的插件,配置文件中给babel-loader传入相应的配置,们直接使用preset-env插件集合,这个集合当中就已经包含了全部的ES最新特性。

const path = require('path');

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /.js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                }
            }
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: 'url-loader'
            }
        ]
    }
}

10. 加载资源

webpack中提供了几种资源加载方式,首先第一个就是ES Module标准的import声明。

import heading from './heading.js';
import icon from './icon.png';

其次是遵循Commonjs标准的require函数,不过通过require函数载入ES Module的话,对于ES Module的默认导出需要通过require函数导入结果的default属性获取。

const heading = require('./heading.js').default;
const icon = require('./icon.png');

遵循AMD标准的define函数和require函数webpack也同样支持。

define(['./heading.js', './icon.png', './style.css'], (createHeading, icon) => {
    const heading = createHeading();
    const img = new Image();
    img.src = icon;
    document.body.append(heading);
    document.body.append(icon);
});

require(['./heading.js', './icon.png', './style.css'], (createHeading, icon) => {
    const heading = createHeading();
    const img = new Image();
    img.src = icon;
    document.body.append(heading);
    document.body.append(icon);
})

webpack兼容多种模块化标准,除非必要的情况否则不要在项目中去混合使用这些标准,每个项目使用一个标准就可以了。

除了js代码中的三种方式外还有一些加载器在工作时也会处理资源中导入的模块,例如css-loader加载的css文件(@import指令和url函数)

@import '';

html-loader加载的html文件中的一些src属性也会触发相应的模块加载。

main.js

import './main.css';

main.css

body {
    min-height: 100vh;
    background-image: url(background.png);
    background-size: cover;
}

webpack在遇到css文件时会使用css-loader进行处理,处理的时候发现css中有引入图片,就会将图片作为一个资源模块加入到打包过程。webpack会根据配置文件中针对于遇到的文件找到相应的loader,此时这是一张png图片就会交给url-loader处理。

reset.css

@import url(reset.css);
body {
    min-height: 100vh;
    background-image: url(background.png);
    background-size: cover;
}

html文件中也会引用其他文件例如img标签的srcsrc/footer.html

<footer>
    <img src="better.png" />
</footer>
yarn add html-loader --dev

配置文件中为扩展名为html的文件配置loader

const path = require('path');

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /.js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                }
            }
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: 'url-loader'
            },
            {
                test: /.html$/,
                use: 'html-loader'
            }
        ]
    }
}

html-loader默认只会处理img标签的src属性,如果需要其他标签的一些属性也能够触发打包可以额外做一些配置,具体的做法就是给html-loader添加attrs属性,也就是html加载的时候对页面上的属性做额外的处理。比如添加一个a:href属性,让他能支持a标签的href属性。

const path = require('path');

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /.js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                }
            }
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: 'url-loader'
            },
            {
                test: /.html$/,
                use: {
                    loader: 'html-loader',
                    options: {
                        attrs: ['img:src', 'a:href']
                    }
                }
            }
        ]
    }
}

完成以后运行打包,在打包的结果中可以看到a标签用到的资源已经参与了打包。