webpack学习[二]:基本使用

121 阅读13分钟

前面的文章webpack学习[一]中,我们介绍了webpack的一些基本概念,例如:入口、输出、loader、插件、模式等,本文将介绍如何使用webpack管理一个前端项目。

一、构建项目

首先我们创建一个目录webpack-study,并切换到该目录下

mkdir webpack-study
cd webpack-study

使用 npm 对项目进行初始化,生成package.json文件。

npm init 

package.json 文件初始内容如下:

{
  // 项目名称
  "name": "webpack-study", 
  // 项目版本
  "version": "1.0.0",
  // 项目描述
  "description": "",
  // 入口文件
  "main": "index.js",
  // 脚本命令
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  // 作者
  "author": "ag_dubs <dali@npmjs.com>",
  // 版本信息
  "license": "ISC"
}

然后安装 webpack:

npm install webpack webpack-cli --save-dev

安装后可以看到package.json文件中多了如下内容:

"devDependencies": {
    "webpack": "^5.88.2",
    "webpack-cli": "^5.1.4"
}

创建 ./src 目录,并创建入口文件index.js,该文件夹中存放项目源代码(即:用于书写和编辑的代码)。
创建 ./dist 目录,该文件夹用于存放分发代码(即:构建过程中,经过最小化和优化后产生的输出结果,最终将在浏览器中加载)

创建完成后,目录结构如下:

image.png

紧接着,我们在入口文件 index.js 中书写如下代码:

const helloDiv = document.createElement('div');

helloDiv.innerHTML = 'Hello Webpack';

document.body.appendChild(helloDiv);

书写完成后,我们在 cmd 终端执行npx webpack命令,该命令默认会将 src/index.js 作为入口起点(entry),生成 dist/main.js 作为输出(output)。

npx webpack

当我们没有设置配置文件时,上述命令是如下命令的简写形式:

npx webpack --entry <entry> --output-path <output-path>

npx webpack --entry src/index.js --output-path dist

其中npx 可以运行在初次安装的 webpack package 中的 webpack 二进制文件。该命令的主要作用可参考该博客.

该命令执行成功后,可以看到 ./dist 目录下会生成一个 main.js 文件。

image.png

然后我们在 ./dist 目录下创建一个index.html文件,书写如下代码来验证我们是否打包成功:

<!DOCTYPE html>
<html lang="en">
<head>  
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="main.js"></script>
</body>
</html>

如果打包成功,我们在浏览器中打开 index.html 文件可以看到 "Hello Webpack" 了:

image.png

创建配置文件

大多数项目会根据实际需求进行复杂的配置,所以webpack支持使用配置文件webpack.config.js进行打包配置,这比终端输入命令参数更加高效。

我们可以在项目根目录创建一个 webpack.config.js 文件,并添加如下内容:

const path = require('path');

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

这时候就可以在终端运行如下命令进行打包

npx webpack --config webpack.config.js

如果 webpack.config.js 存在,则 webpack 命令将默认选择使用它。我们在这里使用 --config 选项只是向你表明,可以传递任何名称的配置文件。(即:配置文件为 webpack.config.js 时可直接运行 npx webpack 进行打包)

package.json 中设置快捷方式

此外,我们可以在 package.json 文件的 scripts 中添加命令,为 webpack 命令设置一个快捷方式。

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack"
},

配置完成后,我们可以直接在命令行执行如下命令进行打包:

npm run build

二、资源管理

2.1 加载CSS

若想在JavaScript模块中引入CSS文件,需要安装 styly-loadercss-loader

  • styly-loader:把 CSS 插入到 DOM 中。
  • css-loader:加载 CSS 文件并解析 import 的 CSS 文件,最终返回 CSS 代码
npm install --save-dev style-loader css-loader  

然后在 webpack.config.js中添加如下配置:

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

注意:style-loadercss-loader的先后顺序不能写错,因为模块 loader 链式调用(从下往上或从右往左),链中的每个 loader 都将对资源进行转换。链会逆序执行。第一个 loader 将其结果(被转换后的资源)传递给下一个 loader,依此类推。最后,webpack 期望链中的最后的 loader 返回 JavaScript。

我们在/src目录下新建一个index.css文件,添加如下样式:

.hello {
    color: red
}

修改index.js内容如下:

import './index.css';

const helloDiv = document.createElement('div');
helloDiv.innerHTML = 'Hello Webpack';
helloDiv.classList.add('hello');
document.body.appendChild(helloDiv);

然后执行npm run build进行打包,然后在浏览器中打开index.html,可以看到字体颜色变成红色了,这也说明CSS文件成功加载了。

image.png

除了 CSS 文件之外,现有的loader支持各种CSS风格,例如:postcsssassless等。

2.2 资源模块(asset module)

资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader。主要包含以下四种类型:

  • asset/resource 发送一个单独的文件并导出 URL。
  • asset/inline 导出一个资源的 data URI
  • asset/source 导出资源的源代码
  • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择

(1)resource 资源(asset/resource)

我们以加载图片资源为例,来展示asset/resourceasset/inline两者之间的区别。

首先我们在配置文件中加入如下配置:

module: {
    rules: [
      ...
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        type: 'asset/resource'
      }
    ]
}

使用webpack内置资源模块时,配置的是 type ,而不是 use。

然后我们在index.js文件中引入图片资源:

import './index.css';
import skyImg from './sky.jpg';

const helloDiv = document.createElement('div');
helloDiv.innerHTML = 'Hello Webpack';
helloDiv.classList.add('hello');
document.body.appendChild(helloDiv);

const img = document.createElement('img');
img.src = skyImg;
document.body.appendChild(img);

执行 npm run build 进行打包。可以看到我们的图片文件被发送到 dist文件夹下。

image.png

这种资源模块类型下,我们可以通过如下两种配置方式自定义输出的文件名:

  • 在 output 中配置 assetModuleFilename
output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
    assetModuleFilename: 'images/[hash][ext][query]'
},
  • 配置 generator(如果两种方式都配置了,该种方式优先级更高
module: {
    rules: [
      ...
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        type: 'asset/resource',
        generator: {
          filename: 'static/[hash][ext][query]'
        }
      }
    ]
}

(2)inline 资源(asset/inline)

首先我们修改配置文件如下:

module: {
    rules: [
      ...
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        type: 'asset/inline'
      }
    ]
}

执行打包命令后,我们在浏览器中查看网页源代码,可以看到图片被打包成 data URI 的形式。并且dist文件夹中没有任何文件输出。

uTools_1690703663378.png

webpack 输出的 data URI,默认是呈现为使用 Base64 算法编码的文件内容。如果要使用自定义编码算法,则可以指定一个自定义函数来编码文件内容:

const svgToMiniDataURI = require('mini-svg-data-uri');

module.exports = {
  ...
  module: {
    rules: [
      {
        test: /.svg/,
        type: 'asset/inline',
        generator: {
          dataUrl: content => {
            content = content.toString();
            return svgToMiniDataURI(content);
          }
        }
      }
    ]
  },
};

(3)source 资源(asset/source)

该类资源会导出文件的源代码。例如我们新建一个 txt 文件,输入如下内容:

hello webpack

添加如下配置:

module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.txt/,
        type: 'asset/source'
      }
    ]
  },
};

修改 index.js 文件内容如下:

import './index.css';
import text from './text.txt';

const helloDiv = document.createElement('div');
helloDiv.innerHTML = text;
helloDiv.classList.add('hello');
document.body.appendChild(helloDiv);

打包之后,在浏览器打开 index.html 可以看到 txt 文件中的内容。

image.png

(4)通用资源(asset)

当资源类型为 asset 时,webpack 将按照默认条件,自动地在 resource 和 inline 之间进行选择:小于 8kb 的文件,将会视为 inline 模块类型,否则会被视为 resource 模块类型。

可以通过在 webpack 配置的 module rule 层级中,设置 Rule.parser.dataUrlCondition.maxSize选项来修改此条件:

module.exports = {
  ...
  module: {
    rules: [
      {
        test: /.txt/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 4 * 1024 // 4kb
          }
        }
      }
    ]
  },
};

此外,也可以通过传递一个函数来指定是否使用 inline 模块,例如:

module.exports = {
  //...
  module: {
    rules: [
      {
        //...
        parser: {
          dataUrlCondition: (source, { filename, module }) => {
            const content = source.toString();
            return content.includes('some marker');
          },
        },
      },
    ],
  },
};

2.3 加载图像(image)

加载图像资源,我们可以使用上面介绍的资源模块来处理。例如我们进行了如下配置后:

module: {
    rules: [
      ...
      {
        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" />

当然对于一些较小的图像我们可以使用asset/inline进行加载。或者也可以使用asset根据图片大小选择如何打包。

2.4 加载字体(font)

对于字体这类资源我们也可以使用webpack内置的资源模块来加载,我们可进行如下配置:

module: {
    rules: [
      ...
      {
        test: /.(woff|woff2|eot|ttf|otf)$/i,,
        type: 'asset/resource'
      }
    ]
}

然后我们在CSS文件中定义相关字体:

@font-face {
    font-family: "Bitstream Vera Serif Bold";
    src: url("https://mdn.github.io/css-examples/web-fonts/VeraSeBd.ttf");
    font-size: 14px;
    font-weight: 500;
}

.hello {
    color: red;
    font-family: "Bitstream Vera Serif Bold"
}

打包后,在浏览器我们可以看到文字字体如下:

image.png

2.5 加载数据

对于一些数据文件,如 JSON 文件,CSV、TSV 和 XML,webpack也是可以加载的。其中json文件,webpack 自身就可以加载。如下json文件:

{
    "title": "hello world"
}

我们使用import导入:

import jsonData from './data.json'
console.log(jsonData);

可以看到,json数据会被加载为一个JavaScript对象:

image.png

而对于 CSV、TSV 和 XML 我们需要安装csv-loaderxml-loader

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

然后添加如下配置:

module: {
    rules: [
      ...
      {
        test: /\.(csv|tsv)$/i,
        use: 'csv-loader'
      },
      {
        test: /\.xml$/i,
        use: 'xml-loader'
      }
    ]
}

假设项目中有如下数据文件data.csvdata.xml

to,from,heading,body
Mary,John,Reminder,Call Cindy on Tuesday
Zoe,Bill,Reminder,Buy orange juice
Autumn,Lindsey,Letter,I miss you
<?xml version="1.0" encoding="UTF-8"?>
<note>
  <to>Mary</to>
  <from>John</from>
  <heading>Reminder</heading>
  <body>Call Cindy on Tuesday</body>
</note>

webpack打包格式如下图所示:

image.png

2.6 自定义 JSON 模块 parser

通过使用自定义 parser 替代特定的 webpack loader,可以将任何 tomlyaml 或 json5 文件作为 JSON 模块导入。

首先安装 tomlyamljs 和 json5 的 packages:

npm install toml yamljs json5 --save-dev

然后添加如下配置:

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

module: {
    rules: [
      ...
      {
        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/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',
  },
}

webpack打包后数据格式如下图所示:

image.png

正如 webpack 官方文档而言,有了这些加载资源的方法,就无需依赖于含有全部资源的 /assets 目录,而是将资源与代码组合在一起使用。例如我们要开发某个组件,那么我们就可以将与该组件相关的所有的资源放在同一目录,这样使得代码更具有移植性。

image.png

三、管理输出

上面示例中我们都是单文件入口,而且打包结果都输出到main.js中。假设我们进行如下修改:

首先创建一个新文件 src/print.js

export default function printMe() {
    console.log('I get called from print.js!');
}

然后我们在 index.js 文件中引入 printMe

import './index.css';
import printMe from './print';

const helloDiv = document.createElement('div');
helloDiv.innerHTML = 'Hello world';
helloDiv.classList.add('hello');
document.body.appendChild(helloDiv);

const btn = document.createElement('button');
btn.innerHTML = 'Click me and check the console!';
btn.onclick = printMe;

helloDiv.appendChild(btn);

修改配置如下:

module.exports = {
  mode: 'development',
  entry: {
    index: './src/index.js',
    print: './src/print.js'
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  }
}

然后执行npm run build 进行打包,我们可以看到dist 目录下会生成 print.bundle.jsindex.bundle.js 文件:

image.png

这时我们在浏览器打开 index.html 文件,我们并不能看到所作的改变,这是因为我们还需要在index.html文件中修改引入的 js 文件:

<!DOCTYPE html>
<html lang="en">
<head>  
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="print.bundle.js"></script>
    <script src="index.bundle.js"></script>
</body>
</html>

这时候我们在浏览器中访问 index.html 中时才会显示新的内容:

image.png

例如上述这种情况,当我们修改了入口文件或者新增了入口文件,导致输出文件发生改变,这个时候我们需要在index.html 文件中手动引入新的文件。对于此,我们可以使用 HtmlWebpackPlugin

3.1 HtmlWebpackPlugin

首选安装 HtmlWebpackPlugin

npm install --save-dev html-webpack-plugin

然后在配置文件中添加如下配置:

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

module.exports = {
  ...
  plugins: [
    new HtmlWebpackPlugin()
  ]
}

然后我们执行npm run build 命令进行打包,会自动在dist目录下生成一个新的index.html文件,该文件引入了print.bundle.jsindex.bundle.js

该插件还有很多配置项,可参考该插件git仓库,这里介绍一些常用的配置:

配置项类型默认值描述
title{String}Webpack App生成的HTML文件的标题(<title>) 。如果配置了 template 会优先使用模板文件的title。
filename{String|Function}'index.html'生成的HTML文件名。可以使用 [name] 替换 entry name 。也可以传一个函数,例如: (entryName) => entryName + '.html'
template{String}模板文件的绝对路径或相对路径。 默认使用 src/index.ejs(如果存在)
templateContent{string|Function|false}false模板内容
templateParameters{Boolean|Object|Function}false允许覆盖模板中使用的参数
inject{Boolean|String}true将所有资源注入到模板或模板内容中。 'body':所有 javascript 资源都将放置在 body 元素的底部。 'head':将把脚本放在 head 元素中  true: 会根据 scriptLoading 选项将其添加到 head/body 中。 false:将禁用自动注入
publicPath{String|'auto'}'auto'用于脚本和链接标记的公共路径
scriptLoading{'blocking'|'defer'|'module'}'defer'<script>标签的type
favicon{String}图标路径
meta{Object}{}允许注入 meta 标签 例如: meta: {viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no'}

通常我们配置如下:

plugins: [
    new HtmlWebpackPlugin({
      template: 'src/index.html',
      inject: 'body',
    })
],

3.2 清理 dist 文件夹

我们每次生成的文件都会存放在 dist 文件夹,当我们多次打包后,dist文件夹会变得杂乱,而且有些文件是无用的,对此我们可以配置output.clean在每次构建之前清理 dist 文件夹。

 const path = require('path');

 module.exports = {
   ...
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
     clean: true,
   },
 };

这样每次打包后 dist 文件夹中只有本次构建的文件。

四、开发环境配置

上述介绍了webpack的基本输入输出管理以及资源加载,下面将介绍开发环境中的一些配置,使得我们开发过程中更加方便。

以下配置仅适用于开发环境,所有首先配置 modedevelopment

4.1 使用 source map

当 webpack 打包源代码时,可能会很难追踪到 error(错误) 和 warning(警告) 在源代码中的原始位置。

例如:如果将三个源文件(a.jsb.js 和 c.js)打包到一个 bundle(bundle.js)中,而其中一个源文件包含一个错误,那么堆栈跟踪就会直接指向到 bundle.js。而我们可能需要准确地知道错误来自于哪个源文件,所以这种提示这通常不会提供太多帮助。

为了更容易地追踪 error 和 warning,JavaScript 提供了 source maps 功能,可以将编译后的代码映射回原始源代码。如果一个错误来自于 b.js,source map 就会明确的告诉你。

源映射 提供了一种将压缩文件中的代码映射回源文件中的原始位置的方法

webpack 中通过devtool选项开启 source map。source map 有很多选项,不同的值会明显影响到构建(build)和重新构建(rebuild)的速度。

这里以 inline-source-map 形式来展示 source map 的作用。例如,我们将上述的 print.js 文件内容修改如下:

export default function printMe() {
    // 这里 console 拼写错误
    consle.log('I get called from print.js!');
}

执行 npm run build 打包后,我们在浏览器打开 index.html,点击按钮可以看到控制台报错如下图:

image.png

可以看到,该报错中包含有发生错误的文件(print.js)和行号(2)的引用。这对于我们调试来说还是非常友好的。

注意:生产模式下,建议关闭 source map。

4.2 使用开发工具

前面例子中,每次编译代码时,都需要手动执行 npm run build 进行构建,比较繁琐。对此,webpack 提供了几种方式,能够在代码发生变换后自动编译代码。

(1)观察模式(watch mode)

通过 webpack watch 命令实现。当其中一个文件被更新,代码将被重新编译。

可在 package.json 添加一个用于启动 webpack watch mode 的 npm scripts:

{
    "scripts": {
        "watch": "webpack --watch",
    },
}

然后我们就可以执行npm run watch 开启观察模式,命令执行后并不会退出命令行,当我们修改某个文件内容并保存时,webpack 会自动重新编译修改后的模块。

但是该种方式的唯一缺点在于,每次修改后,需要重新刷新浏览器才能看到修改后的实际结果。

(2)webpack-dev-server

webpack-dev-server 为你提供了一个基本的 web server,并且具有 live reloading(实时重新加载) 功能。

首先,我们需要安装 web server:

npm install --save-dev webpack-dev-server

然后在配置文件中添加如下配置:

module.exports = {
    mode: 'development',
    ...
    // 告知 `webpack-dev-server`,将 `dist` 目录下的文件 serve 到 `localhost:8080` 下
    devServer: {
     static: './dist',
    },
    ...
};

webpack-dev-server 会从 output.path 中定义的目录中的 bundle 文件提供服务,即文件将可以通过 http://[devServer.host]:[devServer.port]/[output.publicPath]/[output.filename] 进行访问。

webpack-dev-server 在编译之后不会写入到任何输出文件。而是将 bundle 文件保留在 内存 中,然后将它们 serve 到 server 中,就好像它们是挂载在 server 根路径上的真实文件一样。如果你的页面希望在其他不同路径中找到 bundle 文件,则可以通过 dev server 配置中的 devMiddleware.publicPath 选项进行修改。

接着添加一个可以直接运行 dev server 的 script:

{
    "scripts": {
        "start": "webpack serve --open,
    },
}

在命令行中运行 npm start,我们会看到浏览器自动加载页面。如果你更改任何源文件并保存它们,web server 将在编译代码后自动重新加载

(3)webpack-dev-middleware

webpack-dev-middleware 是一个封装器(wrapper),它可以把 webpack 处理过的文件发送到一个 server。webpack-dev-server 在内部使用了它,然而它也可以作为一个单独的 package 来使用,以便根据需求进行更多自定义设置。

官网上展示了一个webpack-dev-middleware 配合 express server 的示例。

首先,安装 express 和 webpack-dev-middleware

npm install --save-dev express webpack-dev-middleware

然后添加如下配置:

 module.exports = {
   ...
   devServer: {
     static: './dist',
   },
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
     clean: true,
     publicPath: '/',
   },
 };

我们将会在 server 脚本使用 publicPath,以确保文件资源能够正确地 serve 在 http://localhost:3000 下。

然后自定义一个 server,即在根目录下添加 server.js 文件:

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');

const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);

// 告知 express 使用 webpack-dev-middleware,
// 以及将 webpack.config.js 配置文件作为基础配置。
app.use(
  webpackDevMiddleware(compiler, {
    publicPath: config.output.publicPath,
  })
);

// 将文件 serve 到 port 3000。
app.listen(3000, function () {
  console.log('Example app listening on port 3000!\n');
});

然后中终端执行node server.js 启动服务器。然后打开浏览器,访问 http://localhost:3000,即可看到webpack应用在运行了。