如何从0开始搭建1个前端项目?第四章(代码打包篇 - webpack)

如何从0开始搭建1个前端项目?第四章(代码打包篇 - webpack)

第四章:代码打包篇(webpack

上一篇:第三章:《代码编译篇》Browserslist&PostCss&Babel

favicon.svg 所有的源码都在这里(如果觉得对你有帮助,求star支持,感谢!🙏)

9. webpack - 是一个用于现代JavaScript应用程序的静态模块打包工具

如果大家跟着教程来到这里的话,其实就已经完成了一个前端项目的大部分基础配置了!接下来就要进入前端工程化中最重要的一个内容了,那就是webpack

本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。

上面是webpack的官方解释,简单点说:它就是一个前端的打包工具,用于把我们项目所有的代码经过一系列的处理后打包成若干个文件。

1. 安装webpack

npm install webpack webpack-cli -D
复制代码

webpack-cli此工具用于在命令行中运行 webpack

2. 调整以下目录结构、文件和内容,如下所示:

  Demo
  |- package.json
  |- package-lock.json
+ |- /dist
+   |- index.html
  |- /src
    |- index.js
复制代码

一般在开发过程中,创建分发代码(./dist)文件夹用于存放分发代码,源代码(./src)文件夹仍存放源代码。源代码是指用于书写和编辑的代码。分发代码是指在构建过程中,经过最小化和优化后产生的输出结果,最终将在浏览器中加载。(其中index.html为创建一个基础的html文件)

3. npx webpack

为了演示webpack的打包功能,让我们先来下载一个library(npm install lodash -D

src/index.js

import _ from 'lodash';

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

  element.innerHTML = `hello ${_.uniqueId()}`;

  return element;
}

document.body.appendChild(component());
复制代码
dist/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Demo</title>
  </head>
  <body>
    <script src="../src/index.js"></script>
  </body>
</html>
复制代码

以上代码很简单,就是在html的body中插入一个div并显示一些内容(uniqueId是随机生成一个唯一ID)

这里大家可以思考一下,虽然现在ES2015 中的 import 和 export 语句已经被标准化,但是大多数浏览器还无法支持它们。所以如果不使用webpack的情况下,我们只能删除js文件中的import _ from 'lodash';,并在html文件中添加如<script src="https://unpkg.com/lodash"></script>这样的方式导入依赖才可以正常的在浏览器中运行上面的js脚本。用这种方式去管理 JavaScript 项目会有一些问题,让我们使用 webpack 来管理这些脚本

现在让我们执行npx webpack,可以看到会生成dist/main.js,这个文件就是转译后的结果。它看上去会比较复杂,因为它内部帮我们处理了lodash的导入等其它问题,感兴趣的话你可以研究一下 webpack 具体如何实现。

最后让我们把html文件的中的<script src="../src/index.js"></script>替换成<script src="main.js"></script>,在浏览器中打开 dist 目录下的 index.html,如果一切正常,你应该能看到以下文本:'hello XXX'

4. 配置文件 - webpack.config.js

  • 新建webpack.config.js
module.exports = () => {
  return {};
};
复制代码

因为webpack的配置较为复杂,所以这里先新建一个空的配置文件,后面会一步一步的往里面添加配置。这里使用js文件并且以一个函数返回配置是因为后面我们会在里面写一些逻辑

  • 编辑package.json
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "type-check": "tsc --noemit",
  "eslint": "eslint src/**/*.{js,ts}",
  "stylelint": "stylelint src/**/*.{css,less,scss}",
  "prettier": "prettier src/**/* --write",
  "lint": "npm run eslint && npm run stylelint && npm run prettier",
  "prepare": "husky install",
  "postcss": "postcss src/**/*.{css,less,scss} --base src --dir build",
  "babel": "babel src --extensions .js,.ts --out-dir lib",
+ "build": "webpack"
},
复制代码

因为每次通过命令行传递参数不方便,所以后面将会使用npm run build替代上面的npx webpack

5. 配置项

以下是webpack的详细配置项,建议大家跟着文章没完成一步都运行一下npm run build看下效果,这样才能搞清楚每一项配置是干什么的

构建目标(Targets)

告知 webpack 为目标(target)指定一个环境。默认值为 "browserslist",如果没有找到 browserslist 的配置,则默认为 "web"

module.exports = () => {
  return {
+   // 构建目标
+   target: 'browserslist',
  };
};
复制代码

这项配置比较简单,这里我们使用默认值或者指定为"browserslist"就好

模式(Mode)

提供 mode 配置选项,告知 webpack 使用相应模式的内置优化。如果没有设置,webpack 会给 mode 的默认值设置为 production

module.exports = () => {
  return {
    // 构建目标
    target: 'browserslist',
+   // 模式
+   mode: 'none',
  };
};
复制代码
package.json

"scripts": {
  ...,
- "build": "webpack",
+ "build": "webpack build --mode development"
},
复制代码

这项配置也比较简单,这里我们使用默认值或者不指定就好,然后通过脚本命令(命令行)传递mode参数,因为这个参数是根据情况动态变化的,大家可以试一下传递developmentproduction的编译结果有啥区别(webpack和webpack build是一样的,这里是为了和后面其它的脚本好区分)

入口(Entry)

开始应用程序打包过程的一个或多个起点

+ const path = require('path');

module.exports = () => {
  return {
    // 构建目标
    target: 'browserslist',
    // 模式
    mode: 'none',
+   // 入口
+   entry: path.resolve(__dirname, 'src/index.js'),
  };
};
复制代码

这项配置是指定webpack打包的入口,这里指定为src/index.js就好。这里用到了Node中的path模块,是对路径的一些处理,大家可以自行的去了解一下

出口(Output)

指示 webpack 如何去输出、以及在哪里输出你的「bundle、asset 和其他你所打包或使用 webpack 载入的任何内容」

const path = require('path');

module.exports = () => {
  return {
    // 构建目标
    target: 'browserslist',
    // 模式
    mode: 'none',
    // 入口
    entry: path.resolve(__dirname, 'src/index.js'),
+   // 出口
+   output: {
+     path: path.resolve(__dirname, 'dist'),
+     filename: '[name].bundle.js',
+     clean: true,
+     // publicPath: '/',
    },
  };
};
复制代码

这项配置和上面的entry差不多,是指定webpack打包的出口

  • path:这里指定为为dist就好
  • filename:此选项决定了每个输出 bundle 的名称,因为可能会有多个 bundle ,所以来赋予每个 bundle 一个唯一的名称(注意:我们需要手动更新dist/index.js<script src="main.js"></script>引用的路径,后面会解决这个问题)
  • clean:在生成文件之前清空 output 目录。(注意:指定为true后每次运行都会清空之前我们创建的html文件,所以可以先指定为false,后面会解决这个问题)
  • publicPath:对于按需加载(on-demand-load)或加载外部资源(external resources)(如图片、文件等)来说,output.publicPath 是很重要的选项。此选项指定在浏览器中所引用的「此输出目录对应的公开 URL」。你可以通过它来指定应用程序中所有资源的基础路径。在多数情况下,此选项的值都会以 / 结束。(注意:这里先不指定,后面可能会动态指定)

Devtool

此选项控制是否生成,以及如何生成 source map

const path = require('path');

module.exports = () => {
  return {
    // 构建目标
    target: 'browserslist',
    // 模式
    mode: 'none',
    // 入口
    entry: path.resolve(__dirname, 'src/index.js'),
    // 出口
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: '[name].bundle.js',
      clean: true,
      // publicPath: '/',
    },
+   // devtool
+   devtool: 'source-map',
  };
};
复制代码

这项配置是指定如何生成 source map,就是可以帮助我们将编译后的代码映射回原始源代码,这对于调试代码错误的时候特别重要,不同的值会明显影响到构建(build)和重新构建(rebuild)的速度。所以建议大家去官网试一下每种source map的效果,在开发过程中可以指定为'source-map'

解析(Resolve)

这些选项能设置模块如何被解析

const path = require('path');

module.exports = () => {
  return {
    // 构建目标
    target: 'browserslist',
    // 模式
    mode: 'none',
    // 入口
    entry: path.resolve(__dirname, 'src/index.js'),
    // 出口
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: '[name].bundle.js',
      clean: true,
      // publicPath: '/',
    },
    // devtool
    devtool: 'source-map',
+   // 解析
+   resolve: {
+     extensions: ['.js', '.ts'],
+   },
  };
};
复制代码

这项配置是指定如何去解析文件,这里主要配置extensions.js,webpack将尝试按顺序解析这些后缀名

DevServer

可用于快速开发应用程序,需下载 webpack-dev-servernpm i webpack-dev-server -D

const path = require('path');

module.exports = () => {
  return {
    // 构建目标
    target: 'browserslist',
    // 模式
    mode: 'none',
    // 入口
    entry: path.resolve(__dirname, 'src/index.js'),
    // 出口
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: '[name].bundle.js',
      clean: true,
      // publicPath: '/',
    },
    // devtool
    devtool: 'source-map',
    // 解析
    resolve: {
      extensions: ['.js', '.ts'],
    },
+   // DevServer
+   devServer: {
+     static: path.resolve(__dirname, 'public'),
+     open: true,
+     hot: true,
+     historyApiFallback: true,
+   },
  };
};
复制代码

这项配置在开发过程中很重要,也很常用,它主要是我们提供了一个本地的服务而不用每次都在浏览器中打开编译后的html文件,以便于我们开发。

官方解释:webpack-dev-server 为你提供了一个基本的 web server,并且具有 live reloading(实时重新加载) 功能。以上配置告知 webpack-dev-server,将 public 目录下的文件 serve 到 localhost:8080 下。webpack-dev-server 会从 output.path 中定义的目录中的 bundle 文件提供服务,即文件将可以通过 http://[devServer.host]:[devServer.port]/[output.publicPath]/[output.filename] 进行访问。webpack-dev-server 在编译之后不会写入到任何输出文件。而是将 bundle 文件保留在内存中,然后将它们 serve 到 server 中,就好像它们是挂载在 server 根路径上的真实文件一样。

  • static:该配置项允许配置从目录提供静态文件的选项(默认是 'public' 文件夹)。这里我们先新建一个public文件夹,并新建public/index.html,内容和dist/index.html保持一致。(注意:我们需要手动更新public/index.js<script src="main.js"></script>引用的路径,后面会解决这个问题)
  • open:告诉 dev-server 在服务器已经启动后打开浏览器。设置其为 true 以打开你的默认浏览器
  • hot:启用 webpack 的 热模块替换 特性:模块热替换(HMR - hot module replacement)功能会在应用程序运行过程中,替换、添加或删除 模块,而无需重新加载整个页面(注意:需要在入口文件即src/index.js中添加如下代码才可生效,否则每次都会重新刷新整个页面)
src/index.js

import _ from 'lodash';

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

  element.innerHTML = `hello ${_.uniqueId()}`;

  return element;
}

+ if (module.hot) {
+   module.hot.accept();
+ }

document.body.appendChild(component());
复制代码
  • historyApiFallback:使用 HTML5 History API 时,可能必须提供 index.html 页面来代替任何 404 响应。主要是在单页面项目且为历史路由模式下使用
package.json

"scripts": {
  ...,
+ "start": "webpack serve --mode development",
  "build": "webpack build --mode development",
},
复制代码

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

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

Module

这些选项决定了如何处理项目中的不同类型的模块。这里我们主要会用到其中的 rules:创建模块时,匹配请求的规则数组。这些规则能够修改模块的创建方式。 这些规则能够对模块(module)应用 loader,或者修改解析器(parser)

因为webpack默认只可以处理js资源,所以这里的核心是掌握 loader :loader用于对模块的源代码进行转换。loader 可以使你在 import 或 "load(加载)" 模块时预处理文件。比如帮我们处理js在内的css和图片等任何类型的资源

  • 处理图片和字体资源
const path = require('path');

module.exports = () => {
  return {
    ...,
+   // 模块
+   module: {
+     // 规则
+     rules: [
+       {
+         test: /\.(js|ts)$/,
+         exclude: /node_modules/,
+         use: {
+           loader: 'babel-loader',
+           // options: {},
+         },
+       },
+       {
+         test: /\.(png|svg|jpg|jpeg|gif)$/i,
+         type: 'asset/resource',
+       },
+       {
+         test: /\.(woff|woff2|eot|ttf|otf)$/i,
+         type: 'asset/resource',
+       },
+     ],
+   },
  };
};
复制代码

这里是运用了webpack内置的 Asset Modules去处理图片和字体

webpack 根据正则表达式,来确定应该查找哪些文件,并将其提供给指定的 loader。在这个示例中,所有以 .png 结尾的文件,都将被提供给 asset/resource

src/index.js

import { uniqueId } from 'lodash';
import img from './xxx.jpg';

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

- element.innerHTML = `hello ${_.uniqueId()}`;
+ element.src = img;

  return element;
}

document.body.appendChild(component());

if (module.hot) {
  module.hot.accept();
}
复制代码

现在来准备一张图片并导入,运行npm start,就可以看到照片可以正常在页面上显示了

  • 处理css
const path = require('path');

module.exports = () => {
  return {
    ...,
    // 模块
    module: {
      // 规则
      rules: [
+       {
+         test: /\.css$/,
+         use: [
+           'style-loader',
+           'css-loader',
+           'postcss-loader',
+         ],
+       },
        {
          test: /\.(png|svg|jpg|jpeg|gif)$/i,
          type: 'asset/resource',
        },
        {
          test: /\.(woff|woff2|eot|ttf|otf)$/i,
          type: 'asset/resource',
        },
      ],
    },
  };
};
复制代码

处理css文件需要用到 style-loader 和 css-loader 以及 postcss-loader

npm i style-loader css-loader postcss-loader -D
复制代码
  • style-loader:把 CSS 插入到 DOM 中
  • css-loader:会对 @import 和 url() 进行处理,就像 js 解析 import/require() 一样
  • postcss-loader:使用 PostCSS 处理 CSS 的 loader(在前面我们配置了PostCSS,就是为了在这里派上用场,因为最终我们的项目运行的是webpack编译出来的文件,上面通过PostCSS编译文件是让大家知道它的具体作用是什么。这里的postcss-loader会读取PostCSS的配置,然后会把css通过PostCSS编译后的结果交给下一个loader处理)

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

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

src/index.js

  import { uniqueId } from 'lodash';
- import img from './xxx.jpg';
+ import './index.css';

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

+ element.innerHTML = `hello ${_.uniqueId()}`;
- element.src = img;

  return element;
}

document.body.appendChild(component());

if (module.hot) {
  module.hot.accept();
}
复制代码
src/index.css

+ body {
+   color: red;
+ }

+ ::placeholder {
+   color: gray;
+ }
复制代码

现在来创建一个css文件并导入,运行npm start,就可以看到文字变成红色了,并且在html的style标签中可以看到一些样式的兼容性处理

  • 处理js
const path = require('path');

module.exports = () => {
  return {
    ...,
    // 模块
    module: {
      // 规则
      rules: [
        {
          test: /\.css$/,
          use: [
            'style-loader',
            'css-loader',
            'postcss-loader',
          ],
        },
+       {
+         test: /\.(js|ts)$/,
+         exclude: /node_modules/,
+         use: {
+           loader: 'babel-loader',
+           // options: {},
+         },
+       },
        {
          test: /\.(png|svg|jpg|jpeg|gif)$/i,
          type: 'asset/resource',
        },
        {
          test: /\.(woff|woff2|eot|ttf|otf)$/i,
          type: 'asset/resource',
        },
      ],
    },
  };
};
复制代码

处理js文件需要用到 babel-loader,此 package 允许你使用Babel转译 JavaScript 文件

npm i babel-loader -D
复制代码
  • test:按照定义的规则去匹配参与编译的文件
  • use:使用loader编译文件,options不设置会读取.babel.config的配置
  • exclude:排除不应参与编译的文件,如node_modules

其实js文件是不需要编译的,这里使用babel-loader的原因和postcss-loader差不多。它会读取Babel的配置去编译js,把结果返回给webpack

直接运行npm startnpm run build,仔细观察编译后的js文件可以发现babel是做了兼容性的处理,webpack是做了打包的处理(如果觉得编译后的js文件太复杂看不懂,可以把src/index.js改简单点,比如只写一行const a = 1;,然后再看编译后的效果)

HtmlWebpackPlugin

HtmlWebpackPlugin(npm i HtmlWebpackPlugin -D)简化了 HTML 文件的创建,以便为你的 webpack 包提供服务。这对于那些文件名中包含哈希值,并且哈希值会随着每次编译而改变的 webpack 包特别有用。你可以让该插件为你生成一个 HTML 文件。

为了解决loader无法实现的其它事,所以这里需要掌握 plugins :插件是 webpack 的 支柱 功能。用于以各种方式自定义 webpack 构建过程。比如打包优化,资源管理,注入环境变量

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

module.exports = () => {
  return {
    ...,
    // 插件
+   plugins: [
+     new HtmlWebpackPlugin({
+       template: path.resolve(__dirname, 'public/index.html'),
+     }),
+   ],
  };
};
复制代码

这是一个插件,所以要写在plugins下,且需要new调用。它就是帮我们解决在上面配置Output时提到的编译完后需要手动更新dist/index.html这个问题,它将为你生成一个 HTML5 文件, 在 body 中使用 script 标签引入你所有 webpack 生成的 bundle

  • template:指定生成html文件的模板。这里我们新建public/index.html,且提供html文件基础内容

运行npm startnpm run build,可以看到html文件中已经自动帮我们引入了编译后的资源

ProgressPlugin

ProgressPlugin 提供了一种自定义编译过程中如何报告进度的方法,这个插件很简单,就是给编译过程加了个进度条,直接开启就行

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

module.exports = () => {
  return {
    ...,
    // 插件
    plugins: [
+     new webpack.ProgressPlugin(),
      new HtmlWebpackPlugin({
        template: path.resolve(__dirname, 'public/index.html'),
      }),
    ],
  };
};
复制代码

webpack-bundle-analyzer

BundleAnalyzerPlugin(npm i webpack-bundle-analyzer -D) 一个 plugin 和 CLI 工具,它将 bundle 内容展示为一个便捷的、交互式、可缩放的树状图形式,这个插件很简单,就是给编译后的结果提供了一个可视化的页面,直接开启就行

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');
+ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer');

module.exports = () => {
  return {
    ...,
    // 插件
    plugins: [
      new webpack.ProgressPlugin(),
      new HtmlWebpackPlugin({
        template: path.resolve(__dirname, 'public/index.html'),
      }),
+     new BundleAnalyzerPlugin.BundleAnalyzerPlugin(),
    ],
  };
};
复制代码

DefinePlugin

DefinePlugin 允许在 编译时 将你代码中的变量替换为其他值或表达式。这在需要根据开发模式与生产模式进行不同的操作时,非常有用。例如,如果想在开发构建中进行日志记录,而不在生产构建中进行,就可以定义一个全局常量去判断是否记录日志。这就是 DefinePlugin 的发光之处,设置好它,就可以忘掉开发环境和生产环境的构建规则。

package.json

"scripts": {
  ...,
- "start": "webpack serve --mode development",
- "build": "webpack build --mode development",
+ "start": "webpack serve --mode development --env APP_ENV=dev",
+ "build": "webpack build --mode development --env APP_ENV=prod"
},
复制代码
  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer');

- module.exports = () => {
+ module.exports = (env, argv) => {
+   console.log(env, argv, process.env.NODE_ENV, process.env.APP_ENV);

    return {
      ...,
      // 插件
      plugins: [
        new webpack.ProgressPlugin(),
+       new webpack.DefinePlugin({
+         'process.env.APP_ENV': JSON.stringify(env.APP_ENV),
+       }),
        new HtmlWebpackPlugin({
          template: path.resolve(__dirname, 'public/index.html'),
        }),
        new BundleAnalyzerPlugin.BundleAnalyzerPlugin(),
      ],
    };
  };
复制代码
src/index.js

...
+ console.log(process.env.NODE_ENV, process.env.APP_ENV);
...
复制代码

这个插件主要用于自定义一些变量,然后根据这些变量进行一些不同的操作。比如上面的start和build都是指定modedevelopment环境下进行编译,这时候就可以通过环境变量 --envDefinePlugin来自定义一些变量(如APP_ENV)(它和上面配置的mode不一样,它可以是任何值)

  • 分别给startbuild指定--envAPP_ENV=devAPP_ENV=prod
  • 如上所示,当 webpack 配置导出为函数时,会接收到一个 "environment" 的参数
  • APP_ENV的值赋给'process.env.APP_ENV',使用JSON.stringify()会更严谨一点,process.env 是node中的用户环境对象,可以自行去了解一下

现在运行npm startnpm run build,node环境下的webpack.config.json和浏览器环境下的src/index.js就都可以访问到APP_ENV这个变量了,你应该看到console.log()在运行不同的脚本时正常的打印了不同的变量。后面会讲到如何根据这个变量在不同的环境下做一些不同的操作

MiniCssExtractPlugin

MiniCssExtractPlugin (npm i mini-css-extract-plugin -D)本插件会将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');
+ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer');

  module.exports = (env, argv) => {
    console.log(env, argv, process.env.NODE_ENV, process.env.APP_ENV);

    return {
      ...,
      // 模块
      module: {
        // 规则
        rules: [
          ...,
          {
            test: /\.css$/,
            use: [
-            'style-loader',
+             MiniCssExtractPlugin.loader,
              'css-loader',
              'postcss-loader',
            ],
          },
          ...,
        ],
      },
      // 插件
      plugins: [
        new webpack.ProgressPlugin(),
        new webpack.DefinePlugin({
          'process.env.APP_ENV': JSON.stringify(env.APP_ENV),
        }),
        new HtmlWebpackPlugin({
          template: path.resolve(__dirname, 'public/index.html'),
        }),
+       new MiniCssExtractPlugin(),
        new BundleAnalyzerPlugin.BundleAnalyzerPlugin(),
      ],
    };
  };
复制代码

这个插件主要用于把css文件提取成单独的一个文件在html文件中通过link标签引入,而style-loader是在html文件中动态生成style标签把css文件插进去

现在运行npm startnpm run build,你可以清楚的看到编译后的css文件现在已经是一个单独的文件了

CopyWebpackPlugin

CopyWebpackPlugin (npm i copy-webpack-plugin -D)将已存在的单个文件或整个目录复制到构建目录。

  const webpack = require('webpack');
  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');
  const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+ const CopyPlugin = require('copy-webpack-plugin');
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer');

  module.exports = (env, argv) => {
    console.log(env, argv, process.env.NODE_ENV, process.env.APP_ENV);

    return {
      ...,
      // 插件
      plugins: [
        new webpack.ProgressPlugin(),
        new webpack.DefinePlugin({
          'process.env.APP_ENV': JSON.stringify(env.APP_ENV),
        }),
        new HtmlWebpackPlugin({
          template: path.resolve(__dirname, 'public/index.html'),
        }),
        new MiniCssExtractPlugin(),
+       new CopyPlugin({
+         patterns: [
+           {
+             from: path.resolve(__dirname, 'public'),
+             to: path.resolve(__dirname, 'dist'),
+             toType: 'dir',
+             globOptions: {
+               ignore: ['**/index.html'],
+             },
+           },
+         ],
+       }),
        new BundleAnalyzerPlugin.BundleAnalyzerPlugin(),
      ],
    };
  };
复制代码

这个插件主要用于把指定目录下的内容拷贝到编译的目录下,比如如上配置就是把public目录下的内容拷贝到dist

  • patterns:为插件指定文件相关模式
  • from:要复制的文件路径,如'public'
  • to:要复制到什么路径,如'dist'
  • toType:确定为什么类型,'dir'为文件夹
  • globOptions:要忽略复制的文件的配置,排除public/index.html是因为HtmlWebpackPlugin会通过它做为模板在dist目录下自动生成html文件
public/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
+   <link rel="icon" href="favicon.ico" />
    <title>Demo</title>
  </head>
  <body>
    
  </body>
</html>
复制代码

比如我们可以准备一个网站图标并在public/index.html中引入,此时如果不配置CopyPlugin直接运行npm run build,然后打开dist/index.html会发现无法访问到这个图标,很简单,因为favicon.ico并没有拷贝到dist目录下(运行npm start是可以正常访问的,因为在开发环境下DevServer会把public目录托管到本地服务下,但是我们最终的目的是在生产环境中dist目录可以正常运行)

配置好CopyPlugin,现在运行npm startnpm run build,你应该可以看到这个图标已经被拷贝到dist目录下了,就可以正常访问了,页面的标签上也会显示出来

优化(Optimization)

这里主要配置一些优化的选项。从 webpack 4 开始,会根据你选择的 mode 来执行不同的优化,不过所有的优化还是可以手动配置和重写。

const path = require('path');

module.exports = () => {
  return {
    ...,
    // 优化
+   optimization: {
+     // 分块策略
+     splitChunks: {
+       chunks: 'all',
+     },
+   },
  };
};
复制代码

这里的配置其实有很多,都是跟优化相关的,我们先配置的简单一点,后面再按需添加

splitChunks:有关分割Chunks的配置,避免一些重复依赖。设置为chunks: 'all'意味着chunk 可以在异步和非异步 chunk 之间共享

运行脚本看效果,你会发现lodash已经被单独打包成一个js文件了,并被其它js文件引入了

6. 区分开发环境和生产环境

上面我们已经完成了诸多的配置,现在我们需要根据不同的环境对配置做一些整合

package.json

"scripts": {
  ...,
- "start": "webpack serve --mode development",
- "build": "webpack build --mode development",
+ "start": "webpack serve --mode development --env APP_ENV=dev",
+ "build": "webpack build --mode production --env APP_ENV=prod"
},
复制代码

先改一下脚本

  • start:开发环境(使用DevServer运行项目并开发)
  • build:生产环境(打包项目输出dist目录并运行)
  const webpack = require('webpack');
  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');
  const MiniCssExtractPlugin = require('mini-css-extract-plugin');
  const CopyPlugin = require('copy-webpack-plugin');
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer');

  module.exports = (env, argv) => {
    console.log(env, argv, process.env.NODE_ENV, process.env.APP_ENV);
    
+   const isEnvDevelopment = env.APP_ENV === 'dev';
+   const isEnvProduction = env.APP_ENV === 'prod';

+   const cssLoaders = [
+     (isEnvDevelopment && 'style-loader') ||
+     (isEnvProduction && MiniCssExtractPlugin.loader),
+     'css-loader',
+     'postcss-loader',
+   ];
  
    return {
      // 构建目标
      target: 'browserslist',
      // 模式
      mode: 'none',
      // 入口
      entry: path.resolve(__dirname, 'src/index.js'),
      // 出口
        output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].bundle.js',
        clean: true,
        // publicPath: '/',
      },
      // devtool
-     devtool: 'source-map',
+     devtool: (isEnvDevelopment && 'source-map') || (isEnvProduction && false),
      // DevServer
      devServer: {
        static: path.resolve(__dirname, 'public'),
        open: true,
        hot: true,
        historyApiFallback: true,
      },
      // 解析
      resolve: {
        extensions: ['.js'],
      },
      // 优化
      optimization: {
        // 分块策略
        splitChunks: {
          chunks: 'all',
        },
      },   
      // 模块
      module: {
        // 规则
        rules: [
          {
            test: /\.css$/,
-           use: [
-             MiniCssExtractPlugin.loader,
-             'css-loader',
-             'postcss-loader',
-           ],
+           use: cssLoaders,
          },
          {
            test: /\.(js)$/,
            exclude: /node_modules/,
            use: {
              loader: 'babel-loader',
              // options: {},
            },
          },
          {
            test: /\.(png|svg|jpg|jpeg|gif)$/i,
            type: 'asset/resource',
          },
          {
            test: /\.(woff|woff2|eot|ttf|otf)$/i,
            type: 'asset/resource',
          },
        ],
      },
      // 插件
      plugins: [
        new webpack.ProgressPlugin(),
        new webpack.DefinePlugin({
          'process.env.APP_ENV': JSON.stringify(env.APP_ENV),
        }),
        new HtmlWebpackPlugin({
          template: path.resolve(__dirname, 'public/index.html'),
        }),
        new MiniCssExtractPlugin(),
-       new CopyPlugin({
-         patterns: [
-           {
-             from: path.resolve(__dirname, 'public'),
-             to: path.resolve(__dirname, 'dist'),
-             toType: 'dir',
-             globOptions: {
-               ignore: ['**/index.html'],
-             },
-           },
-         ],
-       }),
+       (isEnvProduction && new CopyPlugin({
+         patterns: [
+           {
+             from: path.resolve(__dirname, 'public'),
+             to: path.resolve(__dirname, 'dist'),
+             toType: 'dir',
+             globOptions: {
+               ignore: ['**/index.html'],
+             },
+           },
+         ],
+       })) || (isEnvDevelopment && (() => { })),
-       new BundleAnalyzerPlugin.BundleAnalyzerPlugin(),
+       (isEnvProduction && new BundleAnalyzerPlugin.BundleAnalyzerPlugin()) ||
+       (isEnvDevelopment && (() => { })),
      ],
    };
  };
复制代码

开发环境和生产环境的区别

  • 声明变量isEnvDevelopmentisEnvProduction并通过APP_ENV赋值,方便后面用来区分环境使用
  • 生产环境下不使用devtool,以提高性能
  • 提取处理css的loaders并声明赋值变量cssLoaders,且开发环境下使用style-loader,生产环境下使用MiniCssExtractPlugin
  • 开发环境下不使用CopyPlugin
  • 开发环境下不使用BundleAnalyzerPlugin

7. 总结

到此为止,我们就基本上完成了webpack的配置了,就可以进行正常的开发了!

因为webpack的配置其实还是比较复杂的,所以这里我们只是完成了一些比较基础的配置。不过平常的开发已经够用了,后面在开发的过程中再通过查阅文档按需配置就好

favicon.svg 所有的源码都在这里(如果觉得对你有帮助,求star支持,感谢!🙏)

下一篇:第五章《支持其它语言篇》Less&Sass&&TypeScript&React

分类:
前端