webpack打包(上)

518 阅读10分钟

webpack打包

模块打包工具的由来

模块化解决了解决了开发中的代码组织问题,但模块化本身存在一些问题,如:

  • ESM(es module 下同) 本身存在环境兼容问题

    主流浏览器新版本以支持,但无法统一所有用户浏览器使用情况

  • 模块化的模块文件过多,网络请求频繁

    每个模块化的模块文件加载都需要从服务器中请求回来,因此会导致浏览器频繁向服务器发起请求,会影响应用的工作效率。

  • 所有的前端资源都需要模块化

    html、css、图片等资源都可以看做一个模块

毋容置疑,模块化是必要的,但如何解决这些问题呢?

想要解决这些问题,需要满足解决几个点:

  • 首先需要将开发阶段包含新特性的代码编译成大多数浏览器可以执行的兼容代码
  • 其次能将散落的模块文件打包到一起,解决模块资源频繁请求的问题
  • 最后需要支持前端资源的不同种类

为此,需要有一个前端模块打包工具

模块打包工具的概要

上一节介绍了模块打包工具的由来,现在我们先来看下市面主流的打包工具有哪些。

主流打包工具

  • webpack
  • parcel
  • rollup

以 webpack为例介绍:

作为模块打包工具,本身可以解决模块化JavaScript的打包问题;通过webpack可以将零散的代码打包到同一个js文件中,对于存在兼容问题的代码,通过模块加载器(Loader)进行编译转换。利用代码拆分(code Splitting)进行渐进式加载。

这里打包工具解决的是前端整体的模块化,并不单指JavaScript模块化

如此可以享受模块化在开发阶段带来的便捷,同时也不用担心在生产环境模块化带来的问题

了解了打包工具的意义,下面进入整体,开始进行打包工具实操:

webpack快速上手

在开始实践前,需要进行一些前期准备工作

// 创建工程目录、创建源码目录与相应文件
mkdir webpack-demos1
cd webpack-demos1
touch index.html
mkdir src
touch ./src/index.js
touch ./src/heading.js

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>webpack-demos-1</title>
</head>
<body>
    这里是webpack-demos示例

    <script src="dist/main.js"></script>
</body>
</html>

heading.js内容

// 默认导出了生成内容的函数
export default () => {
    const element = document.createElement('h2');

    element.textContent = 'Hello world';
    element.addEventListener('click', () => {
        alert('Hello webpack');
    })

    return element;
}

index.js内容

import createHeading from './heading.js'

const heading = createHeading()

document.body.append(heading)

安装

npm i webpack webpack-cli --dev // 默认会已最新版本安装,当前是webpack5,可以指定版本如 npm i webpack@4.40.2 --dev

安装完成可通过webpack命令将项目进行打包,默认情况webpack会从src目录下index.js作为入口开始工作

npx webpack

执行结束后根目录会生成dist目录

webpack配置文件

webpack默认的配置是将src/index.js进行打包,输出到dist/main.js当中。如果要从别的入口开始,或者打包到别的目录当中要怎么做呢?

这时需要为webpack添加配置文件,告知webpack要做的事情,和怎么去做事情

在项目根目录下添加webpack.config.js文件,此文件是运行在node环境中的文件,因此需要commonjs规范书写

const path = require('path')
module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'output')
    }
}
  • entry 属性为webpakc工作的入口,需要一个路径,如果是相对路径,那么./是不能省去的
  • output 属性为输出目录设置,值为一个对象
    • filename 输出的文件名称
    • path 输出文件的路径,且必须是一个绝对路径,可使用path模块

此时再次运行webpack打包命令,webpack将根据webpack.config.js配置文件的入口./src/main.js开始打包,最终输出到当前当前目录的outupt目录中,输出的文件名为bundle.js

webpack工作模式

在上节中,直接运行打包时,webpack会发出一个警告,提示未配置mode工作模式,未配置时webpack默认使用production模式,

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/
  • production模式会自动优化打包结果,如压缩代码等

  • 但是我们在开发阶段时,需要打包速度、资源映射等,此时就需要配置mode工作模式来完成

npx webpack --mode development
  • 同时还有一个none模式,打包时不会做任何额外的处理

目前模式为以上三种,具体模式的差异,在官方文档中查看

除了在命令行中增加mode参数指定工作模式,还可以在webpack配置文件中增加参数

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

webpack打包结果运行原理

webpack打包结束后,生成的bundle文件是一个匿名立即执行函数,接收一个modules的模块数组;

这里的modules是匿名函数执行时传入的模块列表,每个函数是拥有相同参数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);
console.log('_heading_js__WEBPACK_IMPORTED_MODULE_0__', _heading_js__WEBPACK_IMPORTED_MODULE_0__)

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

document.body.append(heading)

/***/ }),

匿名立即执行函数内部,首先声明了一个变量installedModules,来缓存已经加载过的模块

/******/ 	// The module cache
/******/ 	var installedModules = {};

接着定义了__webpack_require__ 函数,用于根据模块ID(这里是模块的序号,对应modules中序号)加载相应模块;

内部执行逻辑:加载模块时先从installedModules模块缓存中查找是否存在,存在则直接返回使用,否则创建当前模块并缓存到模块缓存中(此时module.l为为加载完毕状态),接着通过moduleID(数组下标,下同)在模块数组中找到模块函数,通过call方法来调用模块函数,并传入this指向和其他参数,调用后module对象上已绑定了当前模块的内容,此时改变标识module.l的状态为true,最后返回当前模块module的exports对象

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

定义__webpack_require__后,因函数也是对象,因此继续向__webpack_require__函数对象上添加__webpack_require__.m、__webpack_require__.c、__webpack_require__.r等功能函数,最后通过__webpack_require__加载第一个入口模块并返回,入口模块内部一次根据依赖进行加载

/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(__webpack_require__.s = 0);

代码基于webpack 4.X版本,在webpack5.X版本上,webpack打包后的打包代码做了变动,后续进行对比详解,但整体原理不变

webpack资源模块加载

webpack工具本身只处理js文件,对于类似css、image等资源模块无法处理。

webpack无法直接处理,但这些资源文件又是我们必须要用的,如此该怎么做呢?

webpack处理js文件使用的是默认的loader来处理,那么我们给webpack添加不同的loader,就可以处理其他文件了

首先来看css资源,需要安装css-loader:

npm install css-loader --dev

在业务内引用css样式

import createHeading from './heading.js'
import './main.css' // 引入css样式

const heading = createHeading()

document.body.append(heading)

接下来再webpack.config.js配置文件中增加css-loader相关配置:

// 与output同级的module对象
module: {
    rules: [
        {
            test: /.css$/,
            use: 'css-loader'
        }
    ]
}

rules:所有的loader在rules数组中进行配置,一个loader是一个对象;test:文件检测规则;use:loader列表,可以是字符串式的单个,也可以是数组类型的多个

此时再运行npx webpack打包命令,结果正常打包,且生成了最新的dist目录与bundle.js文件

css-loader 高版本后默认需要使用webpack5才可以正常生效,否则会报错 this.getOptions is not a funtion错误。

接下来我们看下bundle结果和页面展现

/* 2 */
/***/ ((module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "default": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ });
/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3);
/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0__);
// Imports

var ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0___default()(function(i){return i[1]});
// Module
___CSS_LOADER_EXPORT___.push([module.id, "body {\n    margin: 0;\n    padding: 0;\n}", ""]);
// Exports
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);


/***/ }),

因为css文件我们是在第2个引入的模块,因此模块Id(模块标识符2)为2,经过loader处理后将css处理为js模块,且在最后放入了变量中。

但这时候你再看页面会发现,what?明明正常打包了,为什么页面body元素的样式没生效呢?

仔细查看页面dom结构和思考后你就能发现了,虽然我们把css样式文件打包进去了,但却没有插入页面中渲染。所以,这里还需要一个插件来帮我们把样式插入到页面中。这个插件就是:style-loader.

style-loader 的作用是:将css-loader处理过的css模块信息,以style标签的形式将模块信息插入页面中。

安装style-loader

npm i style-loader --dev

现在我们再次修改webpack.config.js文件,将安装的style-loader配置上:

module: {
    rules: [
        {
            test: /.css$/,
            use: [
                'style-loader',
                'css-loader'
            ]
        }
    ]
}

刚才已经说过,use可以是一个也可是多个loader的数组,但注意的是,此数组的执行顺序是从后向前。也就是说,先执行的要放在数组末尾。

再次执行打包命令,发现css样式文件已经已style标签的形式插入了页面并生效了

<style>
    body {
        margin: 0;
        padding: 0;
    }
</style>

至此,我们的css样式文件加载已经完成了。

总结:

loader是webpack实现模块化的核心特性之一,我们借助于loader就可以加载任何类型的资源

webpack 导入资源模块

到这里可能有些小伙伴有一些疑惑:传统的做法中,我们将css和js分离开,单独维护、单独引入;但是webpack中却又建议我们要在js当中import引入css,这到底是为什么呢?

其实不仅仅是css,而是webpack建议我们在编写代码的过程中,引入任何当前代码所需要的资源类型。

真正需要资源的不是这个应用,而是此时正在编写的代码。JavaScript驱动整个前端应用,而在正常工作时,必须依赖资源

webpack文件资源加载器

我们用到的大部分loader都是将资源文件转换成js模块,但还有一部分特殊的文件类型,如图片类型、字体类型等,这些资源类型没有办法通过js的形式来表示,对于这类文件我们需要用到文件资源加载器:file-loader

我们在页面内先加载一个图片,然后再次执行打包

import acatr from './1.jpg'

const img = new Image();
img.src = acatr;

document.body.append(img);

这时候你会发现打包时出错了

ERROR in ./src/1.jpg 1:0
Module parse failed: Unexpected character '�' (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders

这时候webpack就告诉我们,模块分析失败,意外的字符,并且告诉我们需要安装一个加载器来处理。

我们接下来安装文件资源加载器:file-loader

npm i file-loader --dev

webpack.config.js中增加loader配置

module: {
    rules: [
        {
            test: /.css$/,
            use: [
                'style-loader',
                'css-loader'
            ]
        },
        {
            test: /.(jpg|png|gif)$/,
            use: 'file-loader'
        }
    ]
}

再打包时,webpack会对图片类型使用file-loader打包了。打包结果中可以看到,结果目录出现了一张图片,但名字是一长串字符。此时我们再来看下打包后的文件:

/* 11 */
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "default": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ });
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (__webpack_require__.p + "30abe97a302e168efd973cfc18ef435e.jpg");

/***/ })

webpack将图片打包后的路径和名称返回,且使用时也用的这里发挥的内容

webpack URL加载器

除了file-loader通过拷贝物理文件的形式意外,还有Data URLs的形式去表示文件,此方式在实际工作中也很常见。

此种形式,有特定格式,且文件内容以base64的形式表示,直接存储在js中,因此不需要再发送网络请求来获得此资源

安装

npm i url-loader --dev

修改webpack.config.js配置文件

module: {
    rules: [
        {
            test: /.css$/,
            use: [
                'style-loader',
                'css-loader'
            ]
        },
        {
            test: /.(jpg|png|gif)$/,
            use: 'url-loader'
        }
    ]
}

再次打包时,文件会自动转换为Data URLs的base64形式。但此时存在一个问题,如果图片过大,那么转换的base64会很大,那么我们最终打包的bundle就会很大,影响页面的加载和体验差,如何解决呢?

最佳实践:

  • 小文件使用Data URLs,减少请求的次数(一般是10kb以内)
  • 大文件单独提取存放,提高加载速度

此时修改url-loader的配置

module: {
    rules: [
        {
            test: /.css$/,
            use: [
                'style-loader',
                'css-loader'
            ]
        },
        {
            test: /.(jpg|png|gif)$/,
            use: {
                loader: 'url-loader',
                options: {
                    limit: 10 * 1024 // 10kB
                }
            }
        }
    ]
}

url-loader当资源超过10kB时,单独存放,会内部调用file-loader.当10kB以内时,自动转换为Data URLs

webpack常用加载器分类

  • 编译转换类

    将其他资源模块转换为js模块

  • 文件操作类

    将加载的资源模块拷贝到输出目录,且将目录导出

  • 代码检查类

    对加载的资源文件进行校验,目的是统一代码风格,保证代码质量