管理资源

如果是从一开始就沿用指南的示例,现在应该有一个可以显示“Hello webpack”的小项目。接下来尝试混合一些图像之类的其他资源,看看 webpack 如何处理。

在 webpack 出现之前,前端开发人员会使用 gruntgulp 等工具处理资源,并将它们从 /src 文件夹移动到 /dist/build 目录中。JavaScript 模块也遵循同样的方式。但是,像 webpack 这样的工具将 动态打包 所有依赖并创建所谓的 依赖图。这是极好的创举,因为现在每个模块都可以 明确表述它自身的依赖,以避免打包未使用的模块。

webpack 最出色的功能之一就是除了引入 JavaScript,还可以通过 loader 或内置的 资源模块 引入任何其他类型的文件。换言之,以上列出的那些 JavaScript 的优点(例如显式依赖),同样可以用来构建 web 站点或 web 应用程序中的所有非 JavaScript 内容。让我们从 CSS 开始起步,或许你可能已经熟悉了下面这些设置。

设置

在开始前对项目做一个小的修改:

dist/index.html

 <!DOCTYPE html>
 <html>
   <head>
     <meta charset="utf-8" />
-    <title>起步</title>
+    <title>管理资源</title>
   </head>
   <body>
-    <script src="main.js"></script>
+    <script src="bundle.js"></script>
   </body>
 </html>

webpack.config.js

 const path = require('path');

 module.exports = {
   entry: './src/index.js',
   output: {
-    filename: 'main.js',
+    filename: 'bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
 };

加载 CSS

要想在 JavaScript 模块中导入 CSS 文件,需要安装 style-loadercss-loader,并在 module 配置 中添加这些 loader:

npm install --save-dev style-loader css-loader

webpack.config.js

 const path = require('path');

 module.exports = {
   entry: './src/index.js',
   output: {
     filename: 'bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
+  module: {
+    rules: [
+      {
+        test: /\.css$/i,
+        use: ['style-loader', 'css-loader'],
+      },
+    ],
+  },
 };

module loader 可以链式调用。链中的每个 loader 都将对资源进行转换,不过链会逆序执行。第一个 loader 将其结果(被转换后的资源)传递给下一个 loader,依此类推。最后,webpack 期望链中的最后的 loader 返回 JavaScript。

请确保 loader 的先后顺序:'style-loader' 在前,而 'css-loader' 在后。如果不遵守此约定,webpack 可能会抛出错误。

这些配置可以帮助在依赖于此样式的 JavaScript 文件中 import './style.css'。现在,在此模块执行过程中,含有 CSS 字符串的 <style> 标签,将被插入到 HTML 文件的 <head> 中。

让我们来试试!现在在项目中添加一个新的 style.css 文件,并将其导入到 index.js 中:

project

  webpack-demo
  |- package.json
  |- package-lock.json
  |- webpack.config.js
  |- /dist
    |- bundle.js
    |- index.html
  |- /src
+   |- style.css
    |- index.js
  |- /node_modules

src/style.css

.hello {
  color: red;
}

src/index.js

 import _ from 'lodash';
+import './style.css';

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

   // lodash 现在使用 import 引入。
   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+  element.classList.add('hello');

   return element;
 }

 document.body.appendChild(component());

然后运行构建命令:

$ npm run build

...
[webpack-cli] Compilation finished
asset bundle.js 72.6 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 1000 bytes 5 modules
orphan modules 326 bytes [orphan] 1 module
cacheable modules 539 KiB
  modules by path ./node_modules/ 538 KiB
    ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
    ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js 6.67 KiB [built] [code generated]
    ./node_modules/css-loader/dist/runtime/api.js 1.57 KiB [built] [code generated]
  modules by path ./src/ 965 bytes
    ./src/index.js + 1 modules 639 bytes [built] [code generated]
    ./node_modules/css-loader/dist/cjs.js!./src/style.css 326 bytes [built] [code generated]
webpack 5.4.0 compiled successfully in 2231 ms

再次在浏览器中打开 dist/index.html,应该看到 Hello webpack 现在的样式是红色。请检查页面(不要查看页面源代码,它不会显示结果,因为 <style> 标签是由 JavaScript 动态创建的)并查看页面的 head 标签以观察 webpack 做了什么。可以发现,head 标签包含了原本不存在的 style 块元素,也就是在 index.js 中导入的 CSS 文件中的样式。

注意,在多数情况下也可以 压缩 CSS 以便在生产环境中节省加载时间。最重要的是,现有的 loader 可以支持任何可以想到的 CSS 样式 —— postcsssassless 等。

加载图像

假如现在正在下载 CSS,但是像背景图片和图标这样的图像应该如何处理呢?webpack5 可以使用内置的 资源模块 轻松地将这些内容混入系统中:

webpack.config.js

 const path = require('path');

 module.exports = {
   entry: './src/index.js',
   output: {
     filename: 'bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
   module: {
     rules: [
       {
         test: /\.css$/i,
         use: ['style-loader', 'css-loader'],
       },
+      {
+        test: /\.(png|svg|jpg|jpeg|gif)$/i,
+        type: 'asset/resource',
+      },
     ],
   },
 };

现在 import MyImage from './my-image.png' 将会处理图像,将其添加到 output 目录,并且 MyImage 变量将包含该图像在处理后的最终的 url。如前所示,在使用 css-loader 时,处理 CSS 中的 url('./my-image.png') 也会发生类似过程。loader 会识别这是一个本地文件,并将 './my-image.png' 路径替换为 output 目录中图像的最终路径。而 html-loader 也以相同方式处理 <img src="./my-image.png" />

试试向项目中添加一个图像,并观察它是如何工作的:

project

  webpack-demo
  |- package.json
  |- package-lock.json
  |- webpack.config.js
  |- /dist
    |- bundle.js
    |- index.html
  |- /src
+   |- icon.png
    |- style.css
    |- index.js
  |- /node_modules

src/index.js

 import _ from 'lodash';
 import './style.css';
+import Icon from './icon.png';

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

   // lodash 现在使用 import 引入。
   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
   element.classList.add('hello');

+  // 将图像添加到已经存在的 div 中。
+  const myIcon = new Image();
+  myIcon.src = Icon;
+
+  element.appendChild(myIcon);
+
   return element;
 }

 document.body.appendChild(component());

src/style.css

 .hello {
   color: red;
+  background: url('./icon.png');
 }

重新构建并再次打开 index.html 文件:

$ npm run build

...
[webpack-cli] Compilation finished
assets by status 9.88 KiB [cached] 1 asset
asset bundle.js 73.4 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 1.82 KiB 6 modules
orphan modules 326 bytes [orphan] 1 module
cacheable modules 540 KiB (javascript) 9.88 KiB (asset)
  modules by path ./node_modules/ 539 KiB
    modules by path ./node_modules/css-loader/dist/runtime/*.js 2.38 KiB
      ./node_modules/css-loader/dist/runtime/api.js 1.57 KiB [built] [code generated]
      ./node_modules/css-loader/dist/runtime/getUrl.js 830 bytes [built] [code generated]
    ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
    ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js 6.67 KiB [built] [code generated]
  modules by path ./src/ 1.45 KiB (javascript) 9.88 KiB (asset)
    ./src/index.js + 1 modules 794 bytes [built] [code generated]
    ./src/icon.png 42 bytes (javascript) 9.88 KiB (asset) [built] [code generated]
    ./node_modules/css-loader/dist/cjs.js!./src/style.css 648 bytes [built] [code generated]
webpack 5.4.0 compiled successfully in 1972 ms

如果一切顺利,现在应该看到图标成为了重复的背景图,并且 Hello webpack 文本旁边出现了 img 元素。检查此元素将看到实际的文件名已更改为像 29822eaa871e8eadeaa4.png 一样的名称。这意味着 webpack 在 src 文件夹中找到了我们的文件,并对其进行了处理!

加载字体

使用资源模块可以接收并加载任何文件,然后将其输出到构建目录。换言之,我们可以将它们用于任何类型的文件,也包括字体文件。更新 webpack.config.js 以处理字体文件:

webpack.config.js

 const path = require('path');

 module.exports = {
   entry: './src/index.js',
   output: {
     filename: 'bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
   module: {
     rules: [
       {
         test: /\.css$/i,
         use: ['style-loader', 'css-loader'],
       },
       {
         test: /\.(png|svg|jpg|jpeg|gif)$/i,
         type: 'asset/resource',
       },
+      {
+        test: /\.(woff|woff2|eot|ttf|otf)$/i,
+        type: 'asset/resource',
+      },
     ],
   },
 };

在项目中添加一些字体文件:

project

  webpack-demo
  |- package.json
  |- package-lock.json
  |- webpack.config.js
  |- /dist
    |- bundle.js
    |- index.html
  |- /src
+   |- my-font.woff
+   |- my-font.woff2
    |- icon.png
    |- style.css
    |- index.js
  |- /node_modules

配置好 loader 并将字体文件放在合适的位置后,可以通过 @font-face 声明将其混合。本地的 url(...) 指令会被 webpack 获取并处理,就像处理图片一样:

src/style.css

+@font-face {
+  font-family: 'MyFont';
+  src: url('./my-font.woff2') format('woff2'),
+    url('./my-font.woff') format('woff');
+  font-weight: 600;
+  font-style: normal;
+}
+
 .hello {
   color: red;
+  font-family: 'MyFont';
   background: url('./icon.png');
 }

现在重新构建试试,看看 webpack 是否处理了字体:

$ npm run build

...
[webpack-cli] Compilation finished
assets by status 9.88 KiB [cached] 1 asset
assets by info 33.2 KiB [immutable]
  asset 55055dbfc7c6a83f60ba.woff 18.8 KiB [emitted] [immutable] [from: src/my-font.woff] (auxiliary name: main)
  asset 8f717b802eaab4d7fb94.woff2 14.5 KiB [emitted] [immutable] [from: src/my-font.woff2] (auxiliary name: main)
asset bundle.js 73.7 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 1.82 KiB 6 modules
orphan modules 326 bytes [orphan] 1 module
cacheable modules 541 KiB (javascript) 43.1 KiB (asset)
  javascript modules 541 KiB
    modules by path ./node_modules/ 539 KiB
      modules by path ./node_modules/css-loader/dist/runtime/*.js 2.38 KiB 2 modules
      ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
      ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js 6.67 KiB [built] [code generated]
    modules by path ./src/ 1.98 KiB
      ./src/index.js + 1 modules 794 bytes [built] [code generated]
      ./node_modules/css-loader/dist/cjs.js!./src/style.css 1.21 KiB [built] [code generated]
  asset modules 126 bytes (javascript) 43.1 KiB (asset)
    ./src/icon.png 42 bytes (javascript) 9.88 KiB (asset) [built] [code generated]
    ./src/my-font.woff2 42 bytes (javascript) 14.5 KiB (asset) [built] [code generated]
    ./src/my-font.woff 42 bytes (javascript) 18.8 KiB (asset) [built] [code generated]
webpack 5.4.0 compiled successfully in 2142 ms

重新打开 dist/index.html 观察 Hello webpack 文本是否换上了新的字体。如果一切顺利,应该能看到已经发生了变化。

加载数据

此外,可以加载的有用资源还有数据,如 JSON 文件、CSV、TSV 和 XML。与 NodeJS 类似,对 JSON 的支持实际上也是内置的,即 import Data from './data.json' 默认将正常运行。可以使用 csv-loaderxml-loader 导入 CSV、TSV 与 XML。接下来试试加载这三类文件:

npm install --save-dev csv-loader xml-loader

webpack.config.js

 const path = require('path');

 module.exports = {
   entry: './src/index.js',
   output: {
     filename: 'bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
   module: {
     rules: [
       {
         test: /\.css$/i,
         use: ['style-loader', 'css-loader'],
       },
       {
         test: /\.(png|svg|jpg|jpeg|gif)$/i,
         type: 'asset/resource',
       },
       {
         test: /\.(woff|woff2|eot|ttf|otf)$/i,
         type: 'asset/resource',
       },
+      {
+        test: /\.(csv|tsv)$/i,
+        use: ['csv-loader'],
+      },
+      {
+        test: /\.xml$/i,
+        use: ['xml-loader'],
+      },
     ],
   },
 };

在项目中添加一些数据文件:

project

  webpack-demo
  |- package.json
  |- package-lock.json
  |- webpack.config.js
  |- /dist
    |- bundle.js
    |- index.html
  |- /src
+   |- data.xml
+   |- data.csv
    |- my-font.woff
    |- my-font.woff2
    |- icon.png
    |- style.css
    |- index.js
  |- /node_modules

src/data.xml

<?xml version="1.0" encoding="UTF-8"?>
<note>
  <to>Mary</to>
  <from>John</from>
  <heading>Reminder</heading>
  <body>Call Cindy on Tuesday</body>
</note>

src/data.csv

to,from,heading,body
Mary,John,Reminder,Call Cindy on Tuesday
Zoe,Bill,Reminder,Buy orange juice
Autumn,Lindsey,Letter,I miss you

现在可以导入这四种类型的数据(JSON, CSV, TSV, XML)中的任何一种了,所导入的 Data 变量,将包含可直接使用的已解析的 JSON:

src/index.js

 import _ from 'lodash';
 import './style.css';
 import Icon from './icon.png';
+import Data from './data.xml';
+import Notes from './data.csv';

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

   // lodash 现在使用 import 引入
   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
   element.classList.add('hello');

   // 将图像添加到已经存在的 div 中。
   const myIcon = new Image();
   myIcon.src = Icon;

   element.appendChild(myIcon);

+  console.log(Data);
+  console.log(Notes);
+
   return element;
 }

 document.body.appendChild(component());

重新执行 npm run build 命令,然后打开 dist/index.html。在开发者工具中的控制台应该能够看到成功打印了导入的数据!

// 没有警告
import data from './data.json';

// 显示警告,规范不允许这样做。
import { foo } from './data.json';

自定义 JSON 模块解析器

通过使用 自定义解析器 替代特定的 webpack loader,可以将任何 tomlyamljson5 文件作为 JSON 模块导入。

假设 src 文件夹下有 data.tomldata.yaml 以及 data.json5 文件:

src/data.toml

title = "TOML Example"

[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z

src/data.yaml

title: YAML Example
owner:
  name: Tom Preston-Werner
  organization: GitHub
  bio: |-
    GitHub Cofounder & CEO
    Likes tater tots and beer.
  dob: 1979-05-27T07:32:00.000Z

src/data.json5

{
  // comment
  title: 'JSON5 Example',
  owner: {
    name: 'Tom Preston-Werner',
    organization: 'GitHub',
    bio: 'GitHub Cofounder & CEO\n\
Likes tater tots and beer.',
    dob: '1979-05-27T07:32:00.000Z',
  },
}

首先安装 tomlyamljsjson5 对应的包:

npm install toml yamljs json5 --save-dev

并在 webpack 中配置它们:

webpack.config.js

 const path = require('path');
+const toml = require('toml');
+const yaml = require('yamljs');
+const json5 = require('json5');

 module.exports = {
   entry: './src/index.js',
   output: {
     filename: 'bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
   module: {
     rules: [
       {
         test: /\.css$/i,
         use: ['style-loader', 'css-loader'],
       },
       {
         test: /\.(png|svg|jpg|jpeg|gif)$/i,
         type: 'asset/resource',
       },
       {
         test: /\.(woff|woff2|eot|ttf|otf)$/i,
         type: 'asset/resource',
       },
       {
         test: /\.(csv|tsv)$/i,
         use: ['csv-loader'],
       },
       {
         test: /\.xml$/i,
         use: ['xml-loader'],
       },
+      {
+        test: /\.toml$/i,
+        type: 'json',
+        parser: {
+          parse: toml.parse,
+        },
+      },
+      {
+        test: /\.yaml$/i,
+        type: 'json',
+        parser: {
+          parse: yaml.parse,
+        },
+      },
+      {
+        test: /\.json5$/i,
+        type: 'json',
+        parser: {
+          parse: json5.parse,
+        },
+      },
     ],
   },
 };

src/index.js

 import _ from 'lodash';
 import './style.css';
 import Icon from './icon.png';
 import Data from './data.xml';
 import Notes from './data.csv';
+import toml from './data.toml';
+import yaml from './data.yaml';
+import json from './data.json5';
+
+console.log(toml.title); // 输出 `TOML Example`
+console.log(toml.owner.name); // 输出 `Tom Preston-Werner`
+
+console.log(yaml.title); // 输出 `YAML Example`
+console.log(yaml.owner.name); // 输出 `Tom Preston-Werner`
+
+console.log(json.title); // 输出 `JSON5 Example`
+console.log(json.owner.name); // 输出 `Tom Preston-Werner`

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

   // lodash 现在使用 import 引入。
   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
   element.classList.add('hello');

   // 将图像添加到已经存在的 div 中。
   const myIcon = new Image();
   myIcon.src = Icon;

   element.appendChild(myIcon);

   console.log(Data);
   console.log(Notes);

   return element;
 }

 document.body.appendChild(component());

重新执行 npm run build 命令,然后打开 dist/index.html 就应该能够看到导入的数据会被打印到控制台上!

全局资源

上述所有内容中最出色之处在于,以这种方式加载资源,可以以更直观的方式将模块和资源组合在一起。无需依赖于含有全部资源的 /assets 目录,而是将资源与代码组合在一起使用。例如,类似这样的结构会非常有用:

- |- /assets
+ |– /components
+ |  |– /my-component
+ |  |  |– index.jsx
+ |  |  |– index.css
+ |  |  |– icon.svg
+ |  |  |– img.png

这种配置方式会使代码更具备可移植性,因为现有的集中放置的方式会让紧密耦合所有资源。假如想在另一个项目中使用 /my-component,只需将其复制或移动到 /components 目录下。只要你已经安装过全部 外部依赖 ,并且 已经在配置中定义过相同的 loader ,那么项目应该能够良好运行。

但是,假如只能被局限在旧有开发方式,或者有一些在多个组件(视图、模板、模块等)之间共享的资源,仍然可以将这些资源存储在一个基本目录中,甚至配合 alias 可以更方便使用 import 导入。

回退处理

在下篇指南中无需使用本指南中所有用到的资源,因此需要进行一些清理工作,以便为下篇指南 管理输出 做好准备:

project

  webpack-demo
  |- package.json
  |- package-lock.json
  |- webpack.config.js
  |- /dist
    |- bundle.js
    |- index.html
  |- /src
-   |- data.csv
-   |- data.json5
-   |- data.toml
-   |- data.xml
-   |- data.yaml
-   |- icon.png
-   |- my-font.woff
-   |- my-font.woff2
-   |- style.css
    |- index.js
  |- /node_modules

webpack.config.js

 const path = require('path');
-const toml = require('toml');
-const yaml = require('yamljs');
-const json5 = require('json5');

 module.exports = {
   entry: './src/index.js',
   output: {
     filename: 'bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
-  module: {
-    rules: [
-      {
-        test: /\.css$/i,
-        use: ['style-loader', 'css-loader'],
-      },
-      {
-        test: /\.(png|svg|jpg|jpeg|gif)$/i,
-        type: 'asset/resource',
-      },
-      {
-        test: /\.(woff|woff2|eot|ttf|otf)$/i,
-        type: 'asset/resource',
-      },
-      {
-        test: /\.(csv|tsv)$/i,
-        use: ['csv-loader'],
-      },
-      {
-        test: /\.xml$/i,
-        use: ['xml-loader'],
-      },
-      {
-        test: /\.toml$/i,
-        type: 'json',
-        parser: {
-          parse: toml.parse,
-        },
-      },
-      {
-        test: /\.yaml$/i,
-        type: 'json',
-        parser: {
-          parse: yaml.parse,
-        },
-      },
-      {
-        test: /\.json5$/i,
-        type: 'json',
-        parser: {
-          parse: json5.parse,
-        },
-      },
-    ],
-  },
 };

src/index.js

 import _ from 'lodash';
-import './style.css';
-import Icon from './icon.png';
-import Data from './data.xml';
-import Notes from './data.csv';
-import toml from './data.toml';
-import yaml from './data.yaml';
-import json from './data.json5';
-
-console.log(toml.title); // 输出 `TOML Example`
-console.log(toml.owner.name); // 输出 `Tom Preston-Werner`
-
-console.log(yaml.title); // 输出 `YAML Example`
-console.log(yaml.owner.name); // 输出 `Tom Preston-Werner`
-
-console.log(json.title); // 输出 `JSON5 Example`
-console.log(json.owner.name); // 输出 `Tom Preston-Werner`

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

-  // lodash 现在使用 import 引入。
   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-  element.classList.add('hello');
-
-  // 将图像添加到已经存在的 div 中。
-  const myIcon = new Image();
-  myIcon.src = Icon;
-
-  element.appendChild(myIcon);
-
-  console.log(Data);
-  console.log(Notes);

   return element;
 }

 document.body.appendChild(component());

并移除之前添加的依赖:

npm uninstall css-loader csv-loader json5 style-loader toml xml-loader yamljs

下篇指南

我们将继续移步到 管理输出

延伸阅读

10 位贡献者

skipjackmichael-ciniawskyTheDutchCodersudarsangpchenxsanEugeneHlushkoAnayaDesignwizardofhogwartsastonizersnitin315