(译)A mostly complete guide to Webpack 5

222 阅读12分钟

原文地址:A mostly complete guide to webpack 5 (2020)

什么是Webpack

作为一个 JS 开发者,应该比较熟悉 module,比如 AMD、UMD、CommonJS 和 ES Module。

Webpack 是一个模块捆绑器,并且它对模块有更加广泛的定义,特别对于 Webpack 来说,模块是:

  • 常见的JS模块
  • AMD模块
  • 导入的CSS
  • 图片URL
  • ES模块

也就是说,Webpack 是能够从这些来源中提取依赖项

Webpack的最终目的就是用一种可以导入JS代码中所有内容的方式来统一这些不同的资源和模块类型,并最终生成可交付的输出。

Entry point

对于 Webpack,Entry point 是已经收集了前端项目的所有依赖项的起点

实际上,它就是一个简单的JS文件

这些依赖关系形成了一个依赖关系图

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

module.exports = {
  entry: './path/to/my/entry/file.js',
};

Output

Output 是在构建过程中收集所得到的 JS 和静态文件的地方

Webpack(从版本4开始)默认输出文件夹是dist/,它也是可以配置的

之后产生的 JS 文件就是所谓 bundle 的一部分。

const path = require('path');

module.exports = {
  entry: './path/to/my/entry/file.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'my-first-webpack.bundle.js',
  },
};

Loaders

因为Webpack只能理解 JS 和 JSON 文件。所以就需要loaders来处理其他文件。

Loaders是第三方扩展。来帮助 Webpack 处理各种各样的文件扩展名。比如:CSS、图像或者 txt 文件。

Loader 的目的是在模块中转化文件(除了 JavaScript)。一旦文件成为了模块,Webpack 就可以将其作为项目中的依赖项了。

Loader 有两个属性

  1. test属性,识别出哪些文件会被转换
  2. use属性,定义出在进行转换时,应该使用哪个 loader。
const path = require('path');

module.exports = {
  output: {
    filename: 'my-first-webpack.bundle.js',
  },
  module: {
    // 遇到文件扩展名为 txt 的就使用 raw-loader 转换一下
    rules: [{ test: /\.txt$/, use: 'raw-loader' }],
  },
};

Plugins

Plugins 是可以改变 Webpack 工作方式的第三方扩展。比如有提取 HTML、CSS 或设置环境变量的插件。

(Plugins可以扩展Webpack的能力)

const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 用于访问内置插件

module.exports = {
  module: {
    rules: [{ test: /\.txt$/, use: 'raw-loader' }],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ]
};

Mode

Webpack有两种模式:developmentproduction

它们之间的主要区别就是在于 production 模式会自动对 JS 代码进行缩小和其他优化

默认是为 production

Code splitting

Code splitting或者Lazy loading是一种避免较大的bundles的优化技术

通过 Code splitting,开发人员可以决定在响应某些用户交互时来加载整个 JavaScript 块,比如单击或者路由改变。

一段被分裂的代表变成了一个 chunk

开始使用 Webpack

首先创建一个新的文件夹并进入来初始化 NPM 项目,再安装 webpack 相关包:

npm init -y

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

之后,再打开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 正在寻找默认的入口点src/index.js。创建 src 文件夹,在文件夹中创建 index.js 文件

src/index.js中输入一段代码:

console.log('Hello webpack');

现在再次运行npm run dev。你可以看到一个名为dist/的新文件夹,里面有个名为main.js的JS文件:

dist
└── main.js

这就是你的第一个 webpack 包,也称为 output。

配置 webpack

对于更简单的任务,webpack 可以在不进行配置的情况下工作,但是这很快就会遇到限制。要通过文件配置webpack,在项目文件夹中创建一个webpack.config.js

Webpack是用JavaScript编写的,你可以运行在 NodeJS 中。如下:

module.exports = {
  //
};

比如要改变入口路径,可以:

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 中

使用 HTML

一个没有 HTML 页面的 web 应用程序几乎是毫无用处的。为使用 webpack 中的 HTML,需要安装html-webpack-plugin插件:

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

安装完成后,就可以对其进行配置:

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

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

这里的意思就是 webpack src/index.html加载一个HTML模板

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

  • 它加载我们的HTML文件
  • 它将包注入同一个文件中

src/index.html中创建:

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

</body>
</html>

马上,我们将在 webpack 的开发服务器中运行这个 “app”

webpack 的开发服务器

之前我们安装了 webpack-dev-server,若未安装,则安装它:

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

webpack-dev-server 是一个便于开发的软件包。一旦配置好,就可以启动一个本地服务器来服务我们的文件。

先在package.json添加一个“start”脚本:

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

有了这个脚本,可以很容易地运行服务器:

npm start

之后你的默认浏览器应该打开。而且你还可以看见一个 script tag,表示 JS 包被注入:

image.png

如果跑不起来,在webpack.config.json中需要mode: 'development'这段代码。

与 webpack 的 loaders 一起工作

加载器是第三方扩展帮助webpack处理各种文件扩展名。比如 CSS 和图像都有加载器。

对 webpack Loader 的剖析,从配置上来说

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

相关配置从module这个键开始。这个键中我们在 rules 内部配置每个加载程序组或者单个加载程序

对于每个文件都视为一个模块,并且test键和use键来配置一个对象作用于它们

{
  test: /\.filename$/,
  use: ["loader-b", "loader-a"]
}
  • test告诉 webpack:将这些文件名视作一个模块
  • use则定义这些 loaders 应用于文件

使用 CSS

为了在 webpack 中使用 CSS,我们需要安装至少两个加载器。

这里的加载器对于帮助 webpack 理解如何处理.css文件是必要的。

为了在 webpack 中测试 CSS,用src/style.css创建一个简单的样式表:

h1 {
  colro: orange;
}

之后,在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>
</body>

</html>

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

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

在测试页面之前,需要安装 loaders

  • css-loader用于通过import来加载 CSS 文件
  • style-loader用于在DOM中加载stylesheet

安装 loaders:

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

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

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

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

现在运行npm start,你就可以看到样式表被加载到 HTML 的头部:

image.png

webpack的loaders加载顺序很重要

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

//

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["css-loader", "style-loader"] // 顺序反过来了
      }
    ]
  },
  //
};

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

webpack 的 loaders 是从右向左进行装载

使用 SASS

要在 webpack 中使用 SASS,至少需要安装适当的加载器

在 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>
<script defer src="main.js"></script></head>

<body>
  <h1>Hello webpack!</h1>
  <p>Hello sass!</p>
</body>

</html>

最后在src/index.js中加载 SASS 文件

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

现在就来安装需要的 loaders:

  • sass-loader用于通过import来加载 SASS 文件

其他的css-loaderstyle-loader上面已经介绍了

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

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

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

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

注意loader出现的顺序:sass-loader → css-loader → style-loader

现在再次运行npm start,可以看到样式表被加载到 HTML 的头部:

image.png

使用现代 JavaScript

webpack并不知道如何转换 JavaScript 代码。这个任务外包给第三方 loader,比如 babel-loaderbabel

babel是一个JavaScript编译器和“运输器”。以现代 JavaScript 语法输入,babel 能够将其转换为兼容的代码,这些代码可以在(几乎)任何浏览器中运行

现在需要安装一系列软件包:

  • babel core真正的引擎
  • babel preset env 用于将现代JS代码编译成ES5代码
  • babel loader 用于 webpack
npm i @babel/core babel-loader @babel/preset-env --save-dev

然后通过创建一个新文件 babel.config.json 来配置 babel:

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

最后,配置 webpack 并使用 loader 来转换 JS 文件。

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

module.exports = {
  mode: 'development',
  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中转换后的代码,搜索"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?"); 

如何从头开始建立 React、webpack 5 和 Babel

在 Webpack 中使用 React 组件,你还需要用于 React 的 babel preset:

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

完成之后,还需要更改babel.config.json的配置:

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

现在就可以开始安装 React:

npm i react react-dom

最后在src/index.js中编组件:

import React, { useState } from 'react';
import { render } from 'react-dom';

function App() {
  const [state, setState] = useState("CLICK ME")

  return (
    <button onClick={() => setState("CLICKED")}>{state}</button>
  )
}

render(<App />, document.getElementById('root'));

记得还要在HTML页面中添加<div id="root" />

image.png

在 webpack 中使用 JS 的模块

webpack 将一整套文件视作模块,但别忘记它的主要用途:加载ES模块

直到 2015 年,JS 还没有一个标准的代码重用机制。曾经有很多尝试标准化这个方面,导致了这些年的分裂。

你可能听说过 AMD 模块、UMD 或 CommonJS。没有明确的赢家。最后 ES2015 出现,现在就有了一个“官方”模块系统。

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))

控制台输出如下: image.png

生产模式

正如前面介绍的,webpack有两个操作模式:开发和生产。目前我们也只在开发模式下工作。

在开发模式下,webpack 会将我们编写的代码,几乎是原始地加载到浏览器中

在使用的时候没有压缩,这使得在开发中重新加载应用程序更快。

在生产模式下,webpack 进行了一些优化:**

  • 使用 TerserWebpackPlugin 压缩包的大小
  • 使用 ModuleConcatenationPlugin 作用域提升

它还设置process.env.NODE_ENV为“production”,这个环境变量对于在生产或开发中有条件地执行任务非常有用。

要在生产模式下配置 webpack,打开package.json并添加一个“build”脚本:

"scripts": {
  "dev": "webpack --mode development",
  "start": "webpack serve --open",
  "build": "webpack --mode production",
  "test": "echo \"Error: no test specified\" && exit 1"
},

现在,运行npm run build将生成一个小包。

使用 webpack 进行代码分割

代码分割是一种优化技术,它的目标是:

  • 避免过大的包
  • 避免依赖关系的重复

Webpack社区认为应用程序初始包的最大大小有一个限制:200KB。

为了理解为什么捆绑销售保持小规模是至关重要的,可以在Google上搜索 “The Cost of JavaScript”。

在webpack中有三种主要的方法来激活代码分割:

  • 有多个entry入口
  • optimization.splitChunks
  • 用动态导入

第一种基于多 entry 对于小型项目很有效,但从长远来看是不可扩展的。这里只关注第二种和第三种

使用 optimization.splitChunks 进行拆分

考虑一个使用 Moment.JS 的 JS 应用程序,此 JS 库用于计算时间和日期。

首先安装这个库:

npm i moment

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

import moment from "moment";

使用npm run build并查看输出后的结果:

image.png

整个库被捆绑在我们的应用程序的主要入口点。效果并不会很好。

使用optimization.splitChunks,我们可以将 moment.js 从主包中移除。

webpack.config.js中进行配置:

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并查看输出结果:

image.png

这样的话,主 entry 文件的大小就更加合理。

即使将 moment.js 代码分割后仍然是一个巨大的库,还有更好的选择,比如 luxon 或 date-fns。

使用动态导入进行代码拆分

更强大的代码拆分技术就是使用动态导入有条件地加载代码。在 ECMAScript 2020 发布之前很久,webpack 就提供了动态导入的功能

这种方式在 Vue 和 React 等现代前端库中得到广泛的应用(React 有自己的方式,不过概念是相同的)

代码分割可能用于:

  • 在模块层面

  • 在路由层面

例如,你可以在响应用户交互的时候有条件的加载 JS 模块,比如单击或鼠标移动。或者你可以在响应路由更改时加载代码的相关部分。

开始进行动态导入,更改src/index.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.js中创建以下逻辑:

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");

然后在事件监听器汇总,我们将then()链接到动态导入,并通过对象析构来提取getUsers函数,最后像往常一样使用我们的函数:

// omitted for brevity

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

当你第一次使用npm run start运行加载页面时,会看到控制台中加载的主包:

image.png

现在只有点击按钮才能加载./common/usersAPI

image.png

你还可以通过导入路径前加上/* webpackChunkName: "name_herer" */来控制块的名称:

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

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

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

现在这个块会有一个期望的名称:

image.png

更多话题

其他有趣的值得一看的有:

若有不当之处,欢迎评论指出