Webpack 入门

一、什么是Webpack?

作为其核心,webpack是一个静态模块捆绑器。在特定项目中,webpack将所有文件和资产视为模块。在后台,它依赖于依赖图。依赖图描述了模块之间如何使用文件之间的引用(require和import语句)相互关联。这样,webpack会静态遍历所有模块以构建图,并使用它生成单个捆绑包(或多个捆绑包)-一个JavaScript文件,其中包含所有模块以正确顺序组合而成的代码。“静态地”表示,当webpack构建其依赖关系图时,它不执行源代码,而是将模块及其依赖关系缝合在一起。然后可以将其包含在HTML文件中。

二、概念介绍

Entry point(入口)

Webpack的entry point是收集前端项目的所有依赖项的起点。 实际上,这是一个简单的JavaScript文件。

这些依赖关系形成依赖关系图。

Webpack的默认entry point(从版本4开始)是src / index.js,它是可配置的。 webpack可以有多个entry point

Output(输出)

output是在构建过程中收集生成的JavaScript和静态文件的位置。
Webpack的默认output文件夹(自版本4起)为dist /,也可以配置。
生成的JavaScript文件是所谓的bundle的一部分。

Loaders(加载程序)

loader是第三方扩展程序,可帮助webpack处理各种文件扩展名。 例如,有用于CSS,图像或txt文件的loader
loaders的目标是在module中转换文件(JavaScript以外的文件)。 文件成为module后,webpack可以将其用作项目中的依赖项。

Plugins(插件)

Plugins是第三方扩展,可以更改webpack的工作方式。 例如,有一些用于提取HTML,CSS或设置环境变量的Plugin

Mode(模式)

webpack有两种操作模式:开发和生产。 它们之间的主要区别是生产模式自动将最小化和其他优化应用于JavaScript代码。

Code splitting(代码拆分)

代码拆分或延迟加载是一种避免较大bundle产生的优化技术。
通过代码拆分,开发人员可以决定仅加载响应某些用户交互(例如单击或路由更改(或其他条件))的整个JavaScript代码块。
被拆分的一段代码变成了一个代码块(chunk)。

三、动手实践

要开始使用webpack,请创建一个新文件夹并在命令行中进入那个文件夹,初始化NPM项目:

mkdir webpack-tutorial && cd $_
npm init -y

接着安装webpack, webpack-cli, 和 webpack-dev-server

npm i webpack webpack-cli webpack-dev-server --save-dev

如果要用NPM脚本运行webpack,打开package.json并配置一个“ dev”脚本:

"scripts": {
    "dev": "webpack --mode development"
  },

上述脚本,我们指定webpack在开发模式下工作,便于本地调试。

运行webpack的步骤

让webpack在开发模式下运行:

npm run dev

可能会有以下错误:

ERROR in Entry module not found: Error: Can't resolve './src'

webpack在这里寻找默认 entry point src / index.js 创建src文件夹,并在src内创建一个简单的JavaScript文件:

mkdir src
echo 'console.log("Hello webpack!")' > src/index.js

现在再次运行npm run dev,应该不会再看到错误。 运行输出结果在一个叫dist/的新文件夹,其中包含一个名为main.js的JavaScript文件:

dist
└── main.js

这是我们生成的第一个Webpack bundle,也称为output。

配置webpack

对于简单的任务,webpack无需配置就可以工作,但是很快就会遇到一些限制。 通过文件配置webpack,需在项目文件夹中创建webpack.config.js:

touch webpack.config.js

Webpack用JavaScript编写,并在无头JavaScript环境(例如Node.js)上运行。 在这个文件中,至少需要定义module.exports,这是Node.js的Common JS导出:

module.exports = {
  //
};

在webpack.config.js中,我们可以更改webpack的行为:

  • entry point
  • output
  • loaders
  • plugins
  • code splitting

例如,要改变entry point的路径:

const path = require("path");

module.exports = {
  entry: { index: path.resolve(__dirname, "source", "index.js") }
};

现在,webpack将在source / index.js中查找要加载的第一个文件。 更改包的输出路径:

const path = require("path");

module.exports = {
  output: {
    path: path.resolve(__dirname, "build")
  }
};

现在webpack会将生成的bundle输出到build文件夹而不是dist中。 (为简单起见,我们还是用默认输出路径)。

webpack 与 HTML

没有HTML页面的Web应用程序几乎没有用。 要在webpack中使用HTML,我们需要安装一个插件html-webpack-plugin:

npm i html-webpack-plugin --save-dev

插件装好后,我们可以在配置中使用他:

const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "src", "index.html")
    })
  ]
};

这里是让webpack 从src / index.html加载HTML。

html-webpack-plugin的最终目标有两个:

  • 它加载我们的HTML文件
  • 它将bundle注入到包含这些bundle文件的html文件中

现在,我们在src / index.html中创建一个简单的HTML页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Webpack tutorial</title>
</head>
<body>

</body>
</html>

然后,这个应用程序就会被webpack的开发服务器运行。

webpack的开发服务器

在前面我们已经安装了webpack-dev-server。如果没有安装,先安装:

npm i webpack-dev-server --save-dev

webpack-dev-server是用于开发的软件包。 配置完成后,我们可以启动本地服务器来提供文件。
要配置webpack-dev-server,打开package.json并添加一个“start”脚本:

 "scripts": {
    "dev": "webpack --mode development",
    "start": "webpack-dev-server --mode development --open",
  },

有了这个配置,我们就可以启动服务器了:

npm start

您的默认浏览器应打开。 在浏览器的console中,您还应该看到一个script标签,其中插入了我们的main.js bundle:

image.png

webpack 与 loaders

loaders是第三方扩展程序,可帮助webpack处理各种文件扩展名。 例如,有用于CSS,图像或txt文件的loader。
在配置方面,webpack loader的配置如下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.filename$/,
        use: ["loader-b", "loader-a"]
      }
    ]
  },
  //
};

相关配置以module开头。 在module内,我们在rules内配置一个或者多个loader。

对于每个我们要视为Module的文件,我们使用test 和use 进行配置:

{
    test: /\.filename$/,
    use: ["loader-b", "loader-a"]
}

test 告诉webpack: 请把这个文件当成一个module,use 则说明那些Loader 将被运用到这个文件

webpack 与CSS

要在webpack中处理CSS,我们需要安装至少两个loader。loaders 帮助webpack知道如何处理CSS
测试webpack中的CSS, 先定义一个简单的css文件src/style.css:

h1 {
    color: orange;
}

然后定义一个html文件src/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Webpack tutorial</title>
</head>
<body>
<h1>Hello webpack!</h1>
</body>
</html>

最后在在src/index.js中加载这个CSS:

import "./style.css";
console.log("Hello webpack!");

在测试之前,我们先安装处理css的loader:

  • css-loader, 处理运用import来加载css文件
  • style-loader,处理在DOM中加载css
npm i css-loader style-loader --save-dev

然后在webpack.config.js中配置这些loader:

const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"]
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "src", "index.html")
    })
  ]
};

相关的配置在module里面
现在跑npm start就可以在浏览器里面看到那些css在html的head里面加载了:

image.png

如果css被minified, 可以通过MiniCssExtractPlugin展开css

Webpack中loaders的顺序很重要

在webpack中,加载程序在配置中出现的顺序非常重要。 以下配置无效:

//

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["css-loader", "style-loader"]
      }
    ]
  },
  //
};

此处,“ style-loader”出现在“ css-loader”之前。 但是style-loader用于在页面中注入样式,而不是用于加载实际的CSS文件。

相反,以下配置有效:

//

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"]
      }
    ]
  },
  //
};

webpack加载程序从右到左加载(或从上到下考虑)。

Webpack与SASS

要在Webpack中使用SASS,我们至少需要安装适当的loader。
此处的loader对于帮助webpack了解如何处理.scss文件是必需的。
要在webpack中测试SASS,请在src / style.scss中创建一个简单的样式表:

@import url("https://fonts.googleapis.com/css?family=Karla:weight@400;700&display=swap");

$font: "Karla", sans-serif;
$primary-color: #3e6f9e;

body {
  font-family: $font;
  color: $primary-color;
}

另外,在src / index.html中的HTML模板中添加更多HTML元素:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Webpack tutorial</title>
</head>
<body>
<h1>Hello webpack!</h1>
<p>Hello sass!</p>
</body>
</html>

最后,将SASS文件加载到src / index.js中:

import "./style.scss";
console.log("Hello webpack!");

在测试页面之前,我们需要安装loader(以及Node.js的sass软件包):
sass-loader 处理import加载 SASS 文件
css-loader 处理加载CSS文件作为模块
style-loader 用于在DOM中加载样式表
安装 loaders:

npm i css-loader style-loader sass-loader sass --save-dev

然后在webpack.config.js中配置他们:

const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");

module.exports = {
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: ["style-loader", "css-loader", "sass-loader"]
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "src", "index.html")
    })
  ]
};

相关的配置仍然是以module开头:

const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");

module.exports = {
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: ["style-loader", "css-loader", "sass-loader"]
      }
    ]
  },
  //
};

注意loader的顺序(从右往左), 第一个是sass-loader,第二个是css-loader,最后是style-loader.
现在运行npm-start,可以在html的head里面看到记载的样式:


image.png

一旦安装了SASS和CSS loader,就可以使用MiniCssExtractPlugin展开CSS文件。

Webpack与JavaScript

webpack本身并不知道如何转换JavaScript代码。 该任务已外包给带有babel的第三方loader,特别是babel-loader. babel是一个JavaScript编译器和“转译器”。 给定现代JavaScript语法作为输入,babel可以将其转换为可以在(几乎)任何浏览器中运行的兼容代码。

在继续之前,我们需要安装一堆软件包:

  • babel core,实际引擎
  • babel preset env,用于将现代Javascript编译为ES5
  • babel loader

安装依赖项:

npm i @babel/core babel-loader @babel/preset-env --save-dev

然后通过创建一个新文件babel.config.json配置babel。 在这里,我们将babel配置 use preset-env:

{
  "presets": [
    "@babel/preset-env"
  ]
}

最后,配置webpack使用loader来转换JavaScript文件:

const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");

module.exports = {
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: ["style-loader", "css-loader", "sass-loader"]
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: ["babel-loader"]
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "src", "index.html")
    })
  ]
};

要测试转换,请在src / index.js中编写一些现代语法:

import "./style.scss";
console.log("Hello webpack!");

const fancyFunc = () => {
  return [1, 2];
};

const [a, b] = fancyFunc();

现在运行npm run dev来查看dist中转换后的代码。 打开dist / main.js并搜索“ fancyFunc”:

\n\nvar fancyFunc = function fancyFunc() {\n  return [1, 2];\n};\n\nvar _fancyFunc = fancyFunc(),\n    _fancyFunc2 = _slicedToArray(_fancyFunc, 2),\n    a = _fancyFunc2[0],\n    b = _fancyFunc2[1];\n\n//# sourceURL=webpack:///./src/index.js?"

没有babel,代码将不会被转译:

\n\nconsole.log(\"Hello webpack!\");\n\nconst fancyFunc = () => {\n  return [1, 2];\n};\n\nconst [a, b] = fancyFunc();\n\n\n//# sourceURL=webpack:///./src/index.js?"); 

注意:即使没有babel,webpack也可以正常工作。 仅在运输ES5时才需要进行代码转换过程。

Webpack 中JavaScript模块

webpack将一些文件视为一个模块。 但是,我们不要忘记它的主要目的:加载ES模块。
直到2015年,JavaScript仍没有标准的代码重用机制。 已经进行了很多尝试来标准化这方面,这导致多年来混乱的碎片化。
您可能听说过AMD模块,UMD或Common JS。 没有明确的获胜者。 最后,在ECMAScript 2015中,ES模块以该语言发布。 现在,我们有了一个“官方”模块系统。
webpack中使用ES模块和模块化代码还是很方便的。
要试用webpack中的ES模块,使用以下代码在src / common / usersAPI.js的新文件中创建一个模块:

const ENDPOINT = "https://jsonplaceholder.typicode.com/users/";

export function getUsers() {
  return fetch(ENDPOINT)
    .then(response => {
      if (!response.ok) throw Error(response.statusText);
      return response.json();
    })
    .then(json => json);
}

现在,在src / index.js中,可以加载模块并使用以下功能:

import { getUsers } from "./common/usersAPI";
import "./style.scss";
console.log("Hello webpack!");

getUsers().then(json => console.log(json));

ES 模块介绍

Production 模式

如上所述,webpack具有两种操作模式:开发和生产。 到目前为止,我们仅在开发模式下工作。
在开发模式下,webpack接受我们编写的几乎所有原始JavaScript代码,并将其加载到浏览器中。
没有minify过。 这样可以更快地重新加载开发中的应用程序。

相反,在生产模式下,webpack进行了许多优化:

  • 使用TerserWebpackPlugin进行minify以减小bundle包的大小
  • 使用ModuleConcatenationPlugin提升范围

process.env.NODE_ENV设置为“production”, 此环境变量对于在development或production模式中执行操作很有用。
要在生产模式下配置webpack,打开package.json并添加一个“ build”脚本:

"scripts": {
    "dev": "webpack --mode development",
    "start": "webpack-dev-server --mode development --open",
    "build": "webpack --mode production"
  },

现在,在执行 "npm run build" 时, webpack将产生一个缩小的包。

使用webpack进行代码拆分

代码拆分是指针对以下方面的优化技术:

  • 避免产生很大的bundle 文件
  • 避免依赖项重复

webpack社区决定应用程序的初始bundle包的最大大小的限制为:200KB。 要了解为什么让bundle包的size小很重要,Google "The Cost of JavaScript"

激活webpack中的代码拆分的主要方法有以下三种:

  • 定义多个entry point
  • 使用 optimization.splitChunks
  • 动态import

第一个基于多个entry point的技术适用于较小的项目,但从长远来看却无法扩展。 在这里,我们将仅关注optimization.splitChunks和动态import。

使用Optimization.splitChunks进行代码拆分

假设一个使用Moment.js的JavaScript应用程序,Moment.js是当前流行的一个关于日期和时间的JS库。

将这个library安装在项目文件夹中:

npm i moment

现在清除src / index.js的内容,并在那里导入库:

import moment from "moment";

运行npm run build,查看输出:

 main.js    350 KiB       0  [emitted]  [big]  main

整个库都bundle在我们应用程序的main entry point 中, 这样不好。 通过optimization.splitChunks,我们可以从main bundle中移出moment.js。

要配置代码拆分,打开webpack.config.js并将optimization添加到配置中,配置如下:

const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");

module.exports = {
  module: {
  // omitted for brevity
  },
  optimization: {
    splitChunks: { chunks: "all" }
  },
  // omitted for brevity
};

运行** npm run build**,查看输出:

        main.js   5.05 KiB       0  [emitted]         main
vendors~main.js    346 KiB       1  [emitted]  [big]  vendors~main

现在,我们有了一个带有moment.js的vendors〜main.js,而main entry point 的大小更合理。

注意:即使进行代码拆分,moment.js仍然是一个巨大的库。 还有更好的选择,如luxon或date-fns。

使用动态import进行代码拆分

一种更强大的代码拆分技术使用动态import有条件地加载代码。 在ECMAScript 2020中提供此功能之前,webpack就提供了动态导入。
这种方法在Vue和React之类的现代前端库中得到了广泛使用(React有其自己的方式,但是概念是相同的)。

代码拆分可能用在:

  • 在module级别
  • 在route级别

例如,可以有条件地加载一些JavaScript模块,以响应用户的交互(例如单击或鼠标移动)。 或者,您可以在响应路由更改时加载代码的相关部分。

要开始动态导入,清除src / index.html的内容,然后将以下html的内容放入其中:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Dynamic imports</title>
</head>
<body>
<button id="btn">Load!</button>
</body>
</html>

src/common/usersAPI.js加入以下fetch模块代码:

const ENDPOINT = "https://jsonplaceholder.typicode.com/users/";

export function getUsers() {
  return fetch(ENDPOINT)
    .then(response => {
      if (!response.ok) throw Error(response.statusText);
      return response.json();
    })
    .then(json => json);
}

然后在src/index.html中加入以下代码:

const btn = document.getElementById("btn");

btn.addEventListener("click", () => {
  //
});

如果运行npm run start,然后单击界面中的按钮,则没有任何反应。
现在想象一下,我们想在某人单击按钮后加载用户列表。 “幼稚”的方法可以使用静态导入从src / common / usersAPI.js中加载函数:

import { getUsers } from "./common/usersAPI";

const btn = document.getElementById("btn");

btn.addEventListener("click", () => {
  getUsers().then(json => console.log(json));
});

问题在于ES模块是静态的,这意味着我们无法在运行时更改导入。
通过动态导入,我们可以选择何时加载代码:

const getUserModule = () => import("./common/usersAPI");

const btn = document.getElementById("btn");

btn.addEventListener("click", () => {
  getUserModule().then(({ getUsers }) => {
    getUsers().then(json => console.log(json));
  });
});

在这里,我们创建一个函数来动态加载模块:

const getUserModule = () => import("./common/usersAPI");

然后在event listener中,我们将then()链接到动态导入:

btn.addEventListener("click", () => {
  getUserModule().then(/**/);
});

这样就可以通过对象解构来提取getUsers函数:

btn.addEventListener("click", () => {
  getUserModule().then(({ getUsers }) => {
    //
  });
});

最后,我们照常使用函数:

//

btn.addEventListener("click", () => {
  getUserModule().then(({ getUsers }) => {
    getUsers().then(json => console.log(json));
  });
});

现在,当第一次使用npm run start加载页面时,会看到控制台中已加载的main bundle:


image.png

现在,仅在单击按钮时才加载“ ./common/usersAPI”

image.png

懒加载的chunk是0.js
通过在导入路径前面加上/ * webpackChunkName:“ name_here” * /,我们还可以控制这个chunk的名称:

const getUserModule = () =>
  import(/* webpackChunkName: "usersAPI" */ "./common/usersAPI");

const btn = document.getElementById("btn");

btn.addEventListener("click", () => {
  getUserModule().then(({ getUsers }) => {
    getUsers().then(json => console.log(json));
  });
});

0.js 现在将具有如下名称, usersAPI.js:


image.png

动态import 与prefetch和preload

webpack 4.6.0+增加了对prefetch和preload的支持。

在声明您的导入时使用这些内联指令可以使webpack输出“ Resource Hint”,它告诉浏览器:

  • prefetch:将来可能需要一些导航资源
  • preload:当前导航期间可能需要资源

一个简单的prefetch示例: 有一个HomePage组件,该组件呈现一个LoginButton组件,然后按需在单击后加载LoginModal组件。
LoginButton.js

//...
import(/* webpackPrefetch: true */ 'LoginModal');

这将使<link rel =“ prefetch” href =“ login-modal-chunk.js”>附加在页面顶部,指示浏览器在空闲时间预取login-modal-chunk.js 文件。
一旦父块被加载,webpack将添加prefetch提示。

与prefetch相比,Preload指令有很多区别:

  • preload的块开始并行于父块加载。 父块完成加载后,prefetch 才开始。
  • preload的块具有中等优先级,可以立即下载。 浏览器空闲时,才会下载prefetch的块。
  • 父块应立即请求preload的块。 prefetch的块可以在将来的任何时候使用。
  • 浏览器支持不同。

一个简单的预加载示例: 有一个组件,该组件始终依赖于应放在单独块中的大库。
让我们想象一个需要巨大ChartingLibrary的组件ChartComponent。 他将显示一个LoadingIndicator,并即时按需导入ChartingLibrary:
ChartComponent.js

//...
import(/* webpackPreload: true */ 'ChartingLibrary');

当请求使用ChartComponent的页面时,也会通过<link rel =“ preload”>请求charting-library-chunk。 假设页面块较小并且完成得比较快,该页面将显示一个LoadingIndicator,直到已经请求的制图库块完成为止。 这将增加一点加载时间,因为它只需要一个往返而不是两个。 特别是在高延迟环境中。

错误地使用webpackPreload实际上会影响性能,因此使用时务必小心。

缓存

我们使用webpack bundle了模块化应用程序,从而产生了可部署的/ dist目录。 将/ dist的内容部署到服务器后,客户端(通常是浏览器)将访问该服务器以抢占该站点及其资产。 最后一步可能很耗时,这就是为什么浏览器使用缓存技术的原因。 这使站点能够以更少的不必要的网络流量更快地加载。 但是,当需要下载新代码时,它也会引起问题。

下面重点介绍确保除非打包文件的内容已更改,否则确保由webpack编译生成的文件可以保持缓存的配置。

输出文件名

我们可以使用output.filename替换设置来定义输出文件的名称。 webpack提供了一种使用方括号括起来的字符串来替代文件名的模板方法。 [contenthash]替换将基于resource的内容添加唯一的哈希。 当resource的内容更改时,[contenthash]也将更改。

让我们使用示例从输出管理插件开始设置项目,因此我们不必手动维护index.html文件:
project

webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
|- /node_modules

webpack.config.js

  const path = require('path');
  const { CleanWebpackPlugin } = require('clean-webpack-plugin');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      // new CleanWebpackPlugin(['dist/*']) for < v2 versions of CleanWebpackPlugin
      new CleanWebpackPlugin(),
      new HtmlWebpackPlugin({
       title: 'Output Management',
       title: 'Caching',
      }),
    ],
    output: {
     filename: 'bundle.js',
     filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist'),
    },
  };

使用此配置运行我们的构建脚本npm run build,将产生以下输出:

...
                       Asset       Size  Chunks                    Chunk Names
main.7e2c49a622975ebd9b7e.js     544 kB       0  [emitted]  [big]  main
                  index.html  197 bytes          [emitted]
...

如图所见,bundle包的名称现在反映了其内容(通过hash)。 如果我们在不进行任何更改的情况下运行另一个构建,我们希望该文件名保持不变。 但是,如果我们再次运行它,我们可能会发现情况并非如此:

...
                       Asset       Size  Chunks                    Chunk Names
main.205199ab45963f6a62ec.js     544 kB       0  [emitted]  [big]  main
                  index.html  197 bytes          [emitted]
...

这是因为webpack在条目块中包含某些样板,特别是runtime和manifest。
输出可能会有所不同,具体取决于您当前的Webpack版本。 较新的版本可能没有与某些较旧的版本相同的哈希问题,但是为了安全起见,仍然建议执行以下步骤。

提取样板

正如我们在代码拆分中所了解的那样,SplitChunksPlugin可用于将模块拆分为单独的包。 webpack提供了优化功能,可以使用optimization.runtimeChunk选项将运行时代码拆分为单独的块。 将其设置为single可以为所有块创建单个运行时bundle包:
webpack.config.js

  const path = require('path');
  const { CleanWebpackPlugin } = require('clean-webpack-plugin');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      // new CleanWebpackPlugin(['dist/*']) for < v2 versions of CleanWebpackPlugin
      new CleanWebpackPlugin(),
      new HtmlWebpackPlugin({
        title: 'Caching',
      }),
    ],
    output: {
      filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist'),
    },
   optimization: {
     runtimeChunk: 'single',
   },
  };

让我们运行另一个构建,以查看提取的运行时bundle包:

Hash: 82c9c385607b2150fab2
Version: webpack 4.12.0
Time: 3027ms
                          Asset       Size  Chunks             Chunk Names
runtime.cc17ae2a94ec771e9221.js   1.42 KiB       0  [emitted]  runtime
   main.e81de2cf758ada72f306.js   69.5 KiB       1  [emitted]  main
                     index.html  275 bytes          [emitted]
[1] (webpack)/buildin/module.js 497 bytes {1} [built]
[2] (webpack)/buildin/global.js 489 bytes {1} [built]
[3] ./src/index.js 309 bytes {1} [built]
    + 1 hidden module

将第三方库(例如lodash或react)提取到单独的供应商块中也是一个好习惯,因为与我们的本地源代码相比,第三方供应商块的更改可能性较小。 此步骤将允许客户端向服务器请求更少的请求以保持最新。 这可以通过使用SplitChunksPlugin的如下示例中演示的SplitChunksPlugin的cacheGroups选项来完成。

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
};
这可能会导致包含所有外部程序包的较大块。 建议仅包括您的核心框架和实用程序,并动态加载其余依赖项。

让我们使用带有下一个参数的cacheGroups添加optimization.splitChunks并构建:
webpack.config.js

  const path = require('path');
  const { CleanWebpackPlugin } = require('clean-webpack-plugin');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      // new CleanWebpackPlugin(['dist/*']) for < v2 versions of CleanWebpackPlugin
      new CleanWebpackPlugin(),
      new HtmlWebpackPlugin({
        title: 'Caching',
      }),
    ],
    output: {
      filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist'),
    },
    optimization: {
      runtimeChunk: 'single',
     splitChunks: {
       cacheGroups: {
         vendor: {
           test: /[\\/]node_modules[\\/]/,
           name: 'vendors',
           chunks: 'all',
         },
       },
     },
    },
  };

运行另一个build,查看我们的新供应商bundle包:

...
                          Asset       Size  Chunks             Chunk Names
runtime.cc17ae2a94ec771e9221.js   1.42 KiB       0  [emitted]  runtime
vendors.a42c3ca0d742766d7a28.js   69.4 KiB       1  [emitted]  vendors
   main.abf44fedb7d11d4312d7.js  240 bytes       2  [emitted]  main
                     index.html  353 bytes          [emitted]
...

现在我们可以看到我们的 main bundle包不包含来自node_modules目录的供应商代码,并且大小减小到240个字节!

模块标识符

让我们向项目添加另一个模块print.js:
project

webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
 |- print.js
|- /node_modules

print.js

 export default function print(text) {
   console.log(text);
 };

src/index.js

  import _ from 'lodash';
 import Print from './print';

  function component() {
    const element = document.createElement('div');

    // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
   element.onclick = Print.bind(null, 'Hello webpack!');

    return element;
  }

  document.body.appendChild(component());

运行build,我们希望仅更改main bundle的哈希,但是...

...
                           Asset       Size  Chunks                    Chunk Names
  runtime.1400d5af64fc1b7b3a45.js    5.85 kB      0  [emitted]         runtime
  vendor.a7561fb0e9a071baadb9.js     541 kB       1  [emitted]  [big]  vendor
    main.b746e3eb72875af2caa9.js    1.22 kB       2  [emitted]         main
                      index.html  352 bytes          [emitted]
...

...我们可以看到这三个都有改变。 这是因为默认情况下,每个module.id都会根据解析顺序递增。 意思是当resolve顺序改变时,ID也将改变。 因此,回顾一下:

  • main bunle包由于其新内容而发生了变化。
  • vendor bundle 软件已更改,因为其module.id已更改。
  • 而且,runtme bundle发生了变化,因为它现在包含了对新模块的引用。

第一个和最后一个改变是预料的,我们要修复vendor的hash。 让我们将optimization.moduleIds的值改成“ hashed”
webpack.config.js

 const path = require('path');
  const { CleanWebpackPlugin } = require('clean-webpack-plugin');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      // new CleanWebpackPlugin(['dist/*']) for < v2 versions of CleanWebpackPlugin
      new CleanWebpackPlugin(),
      new HtmlWebpackPlugin({
        title: 'Caching',
      }),
    ],
    output: {
      filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist'),
    },
    optimization: {
      moduleIds: 'hashed',
      runtimeChunk: 'single',
      splitChunks: {
        cacheGroups: {
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            chunks: 'all',
          },
        },
      },
    },
  };

现在,尽管有任何新的本地依赖关系,我们的供应商哈希值仍应在各个版本之间保持一致:

...
                          Asset       Size  Chunks             Chunk Names
   main.216e852f60c8829c2289.js  340 bytes       0  [emitted]  main
vendors.55e79e5927a639d21a1b.js   69.5 KiB       1  [emitted]  vendors
runtime.725a1a51ede5ae0cfde0.js   1.42 KiB       2  [emitted]  runtime
                     index.html  353 bytes          [emitted]
Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.216e852f60c8829c2289.js
...

让我们修改src / index.js来临时删除额外的依赖项:
src/index.js

  import _ from 'lodash';
 -  import Print from './print';
 + // import Print from './print';

  function component() {
    const element = document.createElement('div');

    // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  -  element.onclick = Print.bind(null, 'Hello webpack!');
  +  // element.onclick = Print.bind(null, 'Hello webpack!');

    return element;
  }

  document.body.appendChild(component());

再build一下:

...
                          Asset       Size  Chunks             Chunk Names
   main.ad717f2466ce655fff5c.js  274 bytes       0  [emitted]  main
vendors.55e79e5927a639d21a1b.js   69.5 KiB       1  [emitted]  vendors
runtime.725a1a51ede5ae0cfde0.js   1.42 KiB       2  [emitted]  runtime
                     index.html  353 bytes          [emitted]
Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.ad717f2466ce655fff5c.js
...

我们可以看到,两个版本在vendor bundle包的文件名中都产生了55e79e5927a639d21a1b。

缓存可能很复杂,但是对应用程序或站点用户的好处使其值得付出努力

总结-资源

在这篇文章中,我们介绍了webpack的基础知识:代码拆分,配置,加载程序,插件,prefetch preload, 缓存。 当然还有更多。

参考文献
初学者入门
webpack 介绍

其他重要资
webpack文档
Survive JS-Webpack

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351