1. 打包工具的历史
模块化很好的解决了复杂应用开发中的代码组织问题,但随着引入模块化,又会产生一些新的问题。所使用的ES Modules
本身就存在环境兼容问题,尽管现如今主流浏览器最新版本都已经支持这一特性。但是目前还没办法做到统一所有用户浏览器的使用情况,所以还需要解决兼容问题。其次通过模块化的方式,划分出的模块文件比较多,前端应用又是运行在浏览器当中的。应用中所需要的每一个文件,都需要从服务器中请求回来,这些零散的模块文件必将导致浏览器频繁请求,从而影响应用的工作效率。
对于整个开发过程而言,模块化肯定是有必要的,只是需要在原有的基础之上引入更好的方案或工具去解决上面几个问题,让开发者在应用的开发阶段可以享受模块化所带来的优势又不必担心模块化对生产环境所产生的一些影响。
首先希望有一个工具能够编译代码,就是将开发阶段包含新特性的代码直接转换为能够兼容绝大多数环境的代码,这样一来环境兼容问题也就不存在了。其次是能够将散落的模块文件打包到一起,这就解决了浏览器中频繁对模块文件发出请求的问题。
至于模块化文件划分,只是在开发阶段需要他,因为它能够更好的组织代码对于运行环境实际上是没有必要的,所以可以选择在开发阶段通过模块化的方式去编写。在生产阶段还是打包到同一个文件中,最后还需要支持不同种类的前端资源类型,这样就可以把前端开发过程当中所涉及到的样式、图片、字体等所有资源文件都当做模块使用,对于整个前端应用来讲就有了一个统一的模块化方案了。
前端领域目前有一些工具很好的解决了以上这几个问题,其中最为主流的就是webpack
,parcel
和rollup
。以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.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 .
可以看到正常的工作。下来引入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
,由于打包过程会把import
和export
转换掉,所以说已经不需要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 + k
和 ctrl + 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__
函数,此时传入的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
回去。
/* 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函数将创建完的元素拿到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
的作用。
打包的全部代码如下。
/******/ (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中配置css
的loader
,css-loader
和 style-loader
需要安装到项目中。然后将loader
需要配置到config
的module
中。
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
设置成文件,最后将元素append
到body
中。
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
,内容是一段包含h1
的html
代码。
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
默认就可以处理代码中的import
和export
,所以很自然的会有人认为,webpack
会自动编译ES6
的代码,实则不然,webpack
仅仅完成模块打包工作,会对代码中的import
和export
做一些相应的转换,除此之外它并不能转换代码中其他的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标签的src
,src/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
标签用到的资源已经参与了打包。