从0彻底梳理,2024年webpack5最佳实践(附demo示例)

1,665 阅读24分钟

How to configure Webpack in a ReactJS project - Swapps

前言

webpack 被称为是一个模块打包机🐔,用来做前端代码构建,可以说是一只功劳赫赫的🐔,在过去的十年里,弥补了传统前端领域的短板,是现代前端发展真正的基石。

webpack 是前端构建工具链中的一部分,但是所谓【构建】,其实已经脱离传统前端技术的范畴了(可以说是现在广义前端的一部分,毕竟万物皆可大前端),虽然感觉上与前端开发密不可分,但事实上已经算是另外一个技术领域了。

所以它需要你有一些相关的基础知识才能理解,如果你还没了解过相关概念,没有服务端操作系统最基本的知识,没用过nodejs或其他服务端语言,那建议你可以从更基础的概念类的技术文章看起。

现在已经快到2024年,本篇文章主要介绍下,对照webpack5最新的文档,重新梳理下 webpack 的最新的最佳实践。

我会从头用 webpack 配置一个 React 项目,满足大多数情况下全面而灵活的项目构建,相关 demo 上传到 github 上,地址:webpack_demo

我将尽力确保这是一份最简洁明了的配置,标注每一项配置出现的原因,不会有冗余、模糊、过时的部分

本文篇幅较长,希望你有所收获。

注:虽然 node 常使用 commonJs ,但本篇代码将采用 ESM(import) 模块化规则编写

大纲

  • 前言
  • 大纲
  • 学 webpack 的必要性
  • 第一步:npm run 什么?
  • 构建流程
  • 基本配置
  • entry入口 & output出口
  • Loader 配置
  • Plugin 配置
  • Babel 编译
  • Dev Server 开发服务器
  • 资源路径引入问题
  • 总共用到了多少个包
  • 总结

学 webpack 的必要性

webpack 作为现代前端工具链的核心,功能是极为丰富、灵活和开放的,但同时 webpack 的文档也是极为繁杂的,虽然 webpack5 的集成已经非常强大,基本上已经可以做到开箱即用,但是构建的产物将直接用于前端成果的交付,如果你是一个技术负责人,那理清从业务代码到构建产物这个过程 webpack 都做了什么,是非常有必要的。

并且,一个项目如何构建这其实与具体的业务场景息息相关,很难做到开箱即用百分百合适,技术负责人根据业务特点去自定义一套符合团队规范的构建流程,是很有必要的。

同时,熟悉 webpack 构建也便于你调试在构建过程中出现的一些问题。所以即使并不影响你日常开发,最好你也要了解相关知识。

第一步:npm run 什么?

开始搭建 webpack 第一件事并不是配置 entry 而是你要知道,每次启动项目时,到底在 npm run ...什么?

我觉得从这里开始,对于并不是很熟悉 webpack 的人来说,心智负担就开始体现出来了,因为 webpack 提供了既黑盒又开放的多种执行方式,这里主要涉及到三个 webpack 最常用的包 webpackwebpack-cliwebpack-dev-server

  • webpack 是所有构建的核心逻辑,必不可少
  • webpack-cli 是基于 webpack 提供的一套便捷式命令执行工具,没有更多的功能,可用可不用,推荐不用
  • webpack-dev-server 是在 webpack 之上专门用于开发环境的包,他一部分功能依赖 webpack ,一部分功能依赖 webpack-dev-middleware,既提供了node api 也提供了命令执行,通常情况也是必不可少的。

所以我们经常打开一个项目的package.json,会看到 npm run start/build命令后面执行的逻辑可能是 webpack 命令,可能是 webpack-dev-server 命令,可能是 node js文件的命令。其实无论用什么写法,最终都是执行的 node.js 方法。

所以,既然我们选择自己构建一套 webpack 配置了,我们在 package.json 配置命令的时候就直接,用最简单的方式写一个 node.js 的执行入口,传一个参数区分生产和开发环境,所有的执行逻辑统一写在 node.js 中就好了,不要交叉使用,看起来会有点恶心。

总之,如果不能使用完全集成的功能,我觉得就没必要用cli命令。

可以写成:

// package.json
"scripts": {
   "start": "NODE_ENV=development node build",
   "build": "NODE_ENV=production node build"
 }

而不要:

// package.json
"scripts": {
   "start": "cross-env NODE_ENV=development webpack-dev-server --progress --config build/dev.js",
   "build": "cross-env NODE_ENV=production webpack build --config ./webpack.config.js --stats verbose"
 }

构建流程

目录划分

development(开发环境)production(生产环境) 这两个环境下的构建目标存在着巨大差异,他们的配置原则上其实应该是两份,所以我们应该创建两个文件来分别构建开发 webpack.dev.js 和生产webpack.prod.js两种环境。

同时 webpack 配置也会有很多重复的部分,所以我们抽离一个公共配置文件 webpack.common.js,去写通用的配置。

webpack 配置并非一个普通对象,所以我们会用到一个 webpack 配置合并的包 webpack-merge,来专门将公共配置合并起来。

我们创建build/目录,用于放构建相关的代码,创建src/目录,用于放被构建的业务代码。

image-20231213184803935

执行入口

当执行npm run start/build 时,统一执行 build/index.js 文件,通过传递的环境变量 NODE_ENV 确定是执行开发环境构建还是生产环境构建

// build/index.js
import devServer from './webpack.dev.js'
import build from './webpack.prod.js'

const { NODE_ENV = 'development' } = process.env;

console.log('当前环境:' + NODE_ENV)

NODE_ENV === 'development' ? devServer() : build()

执行 build 代码,不使用 webpack-cli的 webpack 命令,webpack 在 node 中的 api 也同样很简单,就是直接执行 webpack 方法,第一个参数是配置,第二个参数是执行结果的回调,只有传入第二个参数,build 才会立即执行。

// build/webpack.prod.js
import { merge } from 'webpack-merge'
import common from './webpack.common.js'
import webpack from 'webpack'

const webpackConfig = merge(common, {
  mode: 'production'
});
export default function () {
  // 执行build方法,开始构建
  webpack(webpackConfig, (err, stats) => {
    if (err || stats.hasErrors()) {
      console.error('编译出错')
    } else {
      console.log('webpack 编译成功')
    }
  });
}

执行 devServer 方法,我们这里采用 webpack-dev-server,来构建开放环境,执行复杂一点需要先创建 devServer 对象,如下代码

执行后webpack-dev-server会在本地启一个开发服务器,实现项目的构建、自动渲染与热更新

// build/webpack.dev.js
import { merge } from 'webpack-merge'
import common from './webpack.common.js'
import webpack from 'webpack'
import WebpackDevServer from 'webpack-dev-server'

const webpackConfig = merge(common, {
  mode: 'development',
  devServer: {
			....
  }
});
export default function () {
  // 创建webpack对象
  const compiler = webpack(webpackConfig)
  // 创建devServer对象
  const devServer = new WebpackDevServer({ ...webpackConfig.devServer }, compiler);

  const runServer = async () => {
    console.log('Starting server...');
    await devServer.start();
  };
  runServer()
}

到此,一个简单的 webpack 构建流程的基本框架就搭建起来了。

基本配置

  • mode,当前环境,分为 development 开发环境和 production 生产环境两种,在 webpack 诸多默认构建行为中,两种环境下的构建方式差异极大
  • devtool:选择一种 source map 风格来增强调试过程,提示越详细打包越慢,产物越大,很明显你需要根据开发和生产两种环境,分别配置 source map 风格,你需要根据你所在的业务特点来做平衡,比如你是一个很计较性能的C端业务,那打包产物要尽可能小,如果没那么敏感可以适当增加 source-map 细节,便于调试。
// build/webpack.dev.js
export default {
	mode: 'development',
  devtool: 'inline-source-map',
}

// build/webpack.prod.js
export default {
	mode: 'production',
  devtool: 'source-map',
}

entry入口 & output出口

入口和出口配置,以多页应用为例,有几个需要自己配置的字段

  • entry:两个页面的入口,entry下路径对应的 key 值(main page2),是有用处的,在出口 output 中自定义输出路径时,可以通过 [name] 拿到这个 key 值,出口的路径就和多入口关联起来了。

  • **output.path:**打包后产物的输出路径。

  • output.filename:打包后产物的名称,通过 [name] 可以取到当前打包产物来自的入口 entry 名称,path 与 filename 最终组合成完整的打包产物输出路径。

  • output.publicPath

    • 这是很重要且不容易理解的配置,是因为它会影响多个地方,它被用作资源路径的处理,而资源包括打包后的 .js、.css,包括引入的图片、媒体、文件,而每种资源的处理方式是不同的,有些 webpack 内部处理,有些需要借助一些 plugin 插件,所以资源路径的处理过程会有点混乱。

    • 比如你将 publicPath 设置成 /dist/,那么打包后 html 中引入 js css 的路径就会以 /dist/ 绝对路径开头,但它并不一定会生效,因为你可能使用了 html-webpack-plugin插件来生成 html ,这个插件中同样有 publicPath 配置,就会覆盖掉默认 output.publicPath 的配置。

    • 总而言之,output.publicPath 是最初始的资源引入路径的配置,是否会生效,取决于你对项目内各种资源的打包方式和放置位置,还要看最终服务是如何部署,一般我们将静态资源部署在域名 / 目录能直接访问的位置,所以通常配置为”/“(这种方式在本地直接打开 html 文件是访问不到的,需要你在本地起一个访问服务,或者配置成”./“相对路径才行)

  • output.clean:表示每次输出前先清空下目录,很实用,代替以前的clean-webpack-plugin插件。

// webpack.config.js
import path from 'path'
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const { NODE_ENV = 'development' } = process.env;

export default {
  entry: {
    main: path.resolve(__dirname, '../src/main/index.jsx'),
    page2: path.resolve(__dirname, '../src/page2/index.jsx'),
  },
  output: {
    path: path.resolve(__dirname, '../dist/'),
    filename: 'js/[name]/[name].[contenthash].js',
    publicPath: '/',
    clean: true
  }
}

Loader 配置

webpack Loader 的配置这么多年没什么太大的变化,在 react 项目中就更简洁了

  • js&jsxbabel-loader处理js和jsx,babel这一套也是很繁琐,后面有专门章节介绍

  • css:现在我们通常会用一个 css 预处理工具加强开发体验,我这里用了 less ,所以本次项目中按顺序css编译依次经过 less-loader编译 less,css-loader编译 css,最终经过style-loader注入到 html 的 中,我这里加了个判断开发环境下直接注入 ,生产环境单独打包出来(生产环境的一项 css 分离优化,配合 mini-css-extract-plugin 插件使用)

  • **资源处理:**资源处理是需要注意的,webpack5 专门引入了 asset 模块,在这之前你可能使用file-loader url-loader raw-loader来处理资源。asset提供了四种类型完全替代旧方案。

    我这里直接用 type: 'asset' 最灵活的类型来处理所有图片资源编译和路径引入问题,maxSize 配置表示小于 4kb 直接编译进代码里(通常以base64的方式),大于4kb,统一存放到 generator.filename 配置的路径,注意 filename 只配置了一部分路径,最终打包后在代码里引入的路径是要拼接在 output.publicPath 基础之上的,比如 output.publicPath 配置为 /dist/ ,最终代码引入的路径就是 <img src="/dist/images/***.png" />

一个 React 项目最基本的 Loader 配置这三个就足够了,如果你是 vue 项目应还需要 vue 提供的 vue-loader因为 .vue 文件是单独的语法,需要先编译成 js 文件,而 React jsx 本质上还是 js (只需要在 babel 中引入 @babel/preset-react 的预设用于 编译 jsx),并不需要专门的 Loader。

如果你是 html 项目,还可以使用html-loader来解析 html 内部标签内的图片路径编译问题,因为 js 内资源路径的编译,是依靠 ESM(import)之类的模块引入才能识别的,无法直接识别 src="./img.png" 这种字符串的。

export default {
  module: {
    rules: [
      {
        // 资源类处理与路径
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        type: 'asset',
        generator: {
          filename: 'images/[hash][ext][query]'
        },
        parser: {
          dataUrlCondition: {
            maxSize: 4 * 1024 // 4kb
          }
        }
      },
      {
        // js jsx babel编译,细节通过babel.config.js单独配置
        test: /\.m?js|jsx$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader'
        },
      },
      {
        // css 编译
        test: /\.css|less/,
        use: [
          NODE_ENV === 'development' ? 'style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          'less-loader',
        ],
      },
    ],
  },
}

Plugin 配置

plugin 是 webpack 开放生态的核心,官方和社区都有丰富的 plugin 资源,webpack 构建流程虽然很复杂,网络上能看到各种插件,但很多最佳实践都已经被验证过了,现在来到了 webpack 的 v5 版本,很多插件功能都已集成到 webpack 中开箱即用,所以你需要关注一下,你是不是还在用过时的插件。

外部插件需要先 npm install,官方插件可直接通过 webpack.pluginName 调用

html-webpack-plugin 编译html

仍然是最常用的插件之一,webpack 只能围绕着 js 做编译,通常情况下,使用 html-webpack-plugin 做 html 的编译是很有必要的,此次项目是个多页应用示例,所以 html-webpack-plugin 需要执行两次,配置如下:

其中 publicPath 表示打包后的 html 中引入 js css 的路径,如果不设置,将使用 output.publicPath 配置的资源路径,如何设置取决于如何部署,比如设置成 ”./“ 相对路径需要保证 .html 与引入资源在同一目录,设置成 ”/“ 需要在部署服务器根目录下能访问到该资源。

export default {
	plugins: [
   new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../src/main/index.html'),
      filename: 'main.html',
      chunks: ['main'],
      publicPath: '/'
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../src/page2/index.html'),
      filename: 'page2.html',
      chunks: ['page2'],
      publicPath: '/'
    }),
	]
}

webpack.DefinePlugin 构建时替换代码变量

可以通过它在构建过程向代码中替换代码中的变量,可以替换任何类型的数据,在实际开发中可能会很有用,比如在开发和生产环境注入不同的数据。

注意:构建时的替换与运行时的代码没有关系,只是纯粹的字符串替换,本质是在编译时强制更换了代码内容,替换后的内容会在运行时执行,请保证替换内容的百分百安全可靠,并注意替换后的代码不会在运行时出现语法错误

export default {
  plugins: [
    // 代码中所有 _WEBPACK_MODE_ 字符都将被替换成 JSON.stringify('development')
    new webpack.DefinePlugin({
      _WEBPACK_MODE_: JSON.stringify('development'),
    })
  ]
}

webpack.ProgressPlugin 构建进度

webpack 自己提供了构建进度的插件,可以在回调中拿到构建进度,输出控制台提示,便于观察。

我试了下发现这个插件有两个小问题,它在回调中返回的进度,总是从0.03开始,并进度1会执行两次(可能他的进度有更细的划分),这使我每次编译成功都会打印两遍,不知道为什么。

注意:ProgressPlugin 属于增强构建体验,对于编译过程没有任何帮助,并且会增加编译时间,可根据需要决定是否引入

// build/webpack.common.js
let progressStartTime = 0
export default {
  plugin: [
    new webpack.ProgressPlugin({
      handler(percentage, message, ...args) {
        // dev-server每次进度从0.03开始,不知道为啥
        if (percentage <= 0.03) {
          progressStartTime = Date.now();
        }else if (percentage > 0.03 && percentage < 1){
          console.log(`进度:${(percentage * 100).toFixed(0) + '% '}${message}${args.join(' ')}`)
        }else if (percentage === 1) {
          const cost = Date.now() - progressStartTime;
          process.stdout.write('\n');
          vconsole.log(`编译完成,耗时:${cost}ms`);
        }
      },
    })
  ]
}

mini-css-extract-plugin 提取css到单独文件

上面说可以通过style-loader将打包后的 css 插入到中,在生产环境下,为了优化体验拆分 css 到单独文件是非常有必要的,便于浏览器缓存,需要配合 MiniCssExtractPlugin.loader 使用。

注意:在 webpack 以前的版本,可能通过 extract-text-webpack-plugin做类似工作,该方案已被废弃

// build/webpack.prod.ja
export default {
  plugins:[
    new MiniCssExtractPlugin({
      filename: 'css/[name]/[name].css',
      chunkFilename: 'css/[name]/[id].css',
    })
  ]
}

SplitChunksPlugin 编译分块(内部集成)

分块是 webpack 很重要的一项优化方式,它会在多种情况下选择将一部分代码拆出成一个单独的js文件:

  • 新的 chunk 可以被共享,或者模块来自于 node_modules 文件夹
  • 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积)
  • 当按需加载 chunks 时,并行请求的最大数量小于或等于 30
  • 当加载初始化页面时,并发请求的最大数量小于或等于 30

拆分后可以有效减小构建产物体积与增强浏览器缓存,该插件内部集成,使用时只需要通过 optimization.splitChunks 直接配置即可

注意:webpack 默认配置已经符合 web 性能最佳实践,当你确认真的需要时再去更改,通常情况你无需更改配置。

// build/webpack.prod.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

webpack.HotModuleReplacementPlugin 热更新HMR(内部集成)

devServer 下的模块热替换,已经在 webpack-dev-server 内部集成,通常无需使用。

terser-webpack-plugin 压缩编译(内部集成)

webpack5 内部集成了该插件,并会在生产环境下自动压缩代码,通常情况下这是比较好的压缩方案,无需更改。

注意:webpack5 无需安装、引入、配置该插件,如果你想自定义一些压缩配置,则需要 ``npm install terser-webpack-plugin --save-dev`安装后再配置

copy-webpack-plugin 复制目录

可以在构建结束后将某些文件直接复制到构建后的目录中

注意:复制过程不参与编译流程

Babel 编译

在 webpack 真正编译的环节,最主要的就是 js 编译,webpack 通常使用 Babel 做编译(你也可以选择其他编译器比如最近因 vite 比较火的 esbuild),如果你能接受 Babel 的编译速度的话,建议还是用 Babel ,实践案例最为丰富。

Babel 的编译比较复杂,而且需要用到很多相关的包,建议将相关配置单独封装到babel.config.js,而不是写在 webpack 或者写在 package.json 里。

这又是一堆很糟心的配置,我都不知道要怎么讲清楚这玩意,可以专门出一篇文章,网上充斥各种混乱过时的文章,提示一下如果你在网上查找相关文章,如果里面提到的任何包不是 @babel 开头,那这篇文章就不要看了,肯定过时了

注意:Babel 现在最新 v7 版本,发版到现在有很多废弃的包,最新的 babel 包全部是以 @babel 开头,不要再用旧的包了。

@babel/core

  • babel 核心功能,必引入

@babel/preset-env

  • Babel 最重要的有两个机制预设presets插件plugins,而预设就是一组插件的集合,因为在生产环境我们需要根据不同的浏览器,来决定引入不同的插件(提升性能),所以官方推出了一个智能的presets@babel/preset-env,这是一大堆插件的集合,具体需要引入哪些,在运行时根据浏览器做最优的决定。

@babel/preset-react

  • 本次项目使用了 React,虽然 jsx 本质上是 js ,但是仍有很大不同,所以需要引入@babel/preset-react编译jsx 文件

@babel/plugin-transform-runtime 与 @babel/runtime-corejs3

重点来了,最容易让人迷惑的包,官方推出了两个 runtime 插件,主要为了解决两个问题

  1. 打包后每个文件开头都会引入补充文件,用于处理当前文件的兼容问题,比如某个浏览器不支持 js 某个方法或者压根缺少某个数据类型,就需要先声明这个方法或者引入相关的垫片(polyfill),每个打包后的文件都会引入当前文件缺失的部分,会有很多重复引入。
  2. 垫片(polyfill)的引入会造成全局污染,在业务项目中还好,如果是开发一些类库这个问题就严重了。

所以为了解决上述问题,就有了这两个包,@babel/plugin-transform-runtime安装在开发依赖项(devDependencies),``@babel/runtime-corejs3`需要在运行时使用所以要安装在生产依赖项(dependencies),@babel/runtime 有多个版本你可以理解为对应不同程度的语法兼容,corejs-3兼容的语法最多,所以我们选择corejs-3

image-20231214215628139

所以我们最终决定引入这5个包,编译结果基本兼顾性能与全面,可满足多数场景,具体配置如下:

// babel.config.js
export default {
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "browsers": ["last 2 versions", "not ie <= 10"]
        }
      }
    ],
    '@babel/preset-react'
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3,
      }
    ]
  ]
}

还有一个值得说一下就是@babel/preset-stage-?这个包在之前也经常被用到,经常配置"presets": ["stage-0"]用于在编译时可以选择转义到 TC39 新语法的哪个阶段的提案,但在 babel v7 中被完全废弃了,具体可看 Removing Babel's Stage Presets

Dev Server 开发服务器

Webpack 提供 Dev Server 功能,将 mode 设置为 'development',有三种方式可以来构建开放环境:

  1. webpack-dev-server,集成开箱即用的构建工具,会自动搭建一个 devServer 服务,集成 HMR ,监听文件变化,无刷新热更新页面。
  2. 用 webpack 自带的 watch 和 watchOptions 配置项去监听,就只是简单的监听文件变化,然后重新走 build ,再手动刷新页面
  3. webpack-dev-middleware中间件,这个需要你自己搭建 devServer 服务器。webpack-dev-server业务基于它实现的

通常情况下,在正常代码构建中,我们会采用webpack-dev-server来构建开发环境,这是最合适的选择。

你可以向 Dev Server 添加一些常用配置,绝大多数配置使用默认即可

  • open:Dev Server 启动后会自动打开浏览器,并可设置要打开的路由

  • port:设置打开的端口,默认 auto 会自动打开一个闲置端口

  • hot:热更新,默认开启

  • host:设置'0.0.0.0',可以让之外的服务器访问你的 dev server,否则是只允许你本地访问的

  • proxy:是我们最常用的设置,用于设置 Dev Server 的请求代理,很多时候我们打包后的前端代码会随着服务端代码一起部署到服务器上,所以我们经常在前端代码中直接通过请求当前服务器的 /api/add接口路由来请求数据,我们在本地开发时不可能每次把服务器代码也在本地启动,所以我们可以通过 Dev Server 将当前的请求,代理到一台部署了服务端代码的服务器上

    如下示例,我们在前端代码直接请求/api/add接口,会自动代理到http://api.test.com/api/add,我们可以代理到服务端的测试环境实现联调,亦或者直接代理到线上服务,免去本地搭建服务端环境。

// build/webpack.dev.com
export default {
  devServer: {
    open: ['/main.html'],
    port: 'auto',
    host: '0.0.0.0',
    hot: true,
    proxy: {
      '/api': 'http://api.test.com',
    },
  },
}

Dev Server 构建的产物并不会直接输出到文件中,只会保存到内存里,是看不到的,因为我们通常无需使用开发环境的构建产物,也减少了磁盘写入的时间,提升响应速度。

注意:其实 Dev Server 构建的开发环境和最终生产环境的构建产物是有巨大差异的,我们可以看到这两者所构建的过程,使用的工具都有很大不同,原则上这是有很大风险的,不过 webpack 很好的兼容了这一点,而且历经时间的验证,但作为一个项目的技术负责人,在引入一些新的构建工具时,我们应该更加关注这一点。

从构建流程上,webpack 开发生产两种环境的构建原理起码是一致的,而像近几年比较火的vite更为极端,在开发和生产环境的构建的原理、编译流程都完全不同,甚至编译 js 都是分别用 babel 和 esbuild 两种编译器,注定构建产物的差异更加大,这也是在考虑选择构建工具时不可忽视的因素。

资源路径引入问题

单独拿一节来说下资源路径问题是因为这个问题对于不太熟悉 webpack 的人来说确实很容易遇到,归根结底其实 webpack 只是一个 js 打包的工具,至于打包出来的各种资源要如何引用,webpack 并不太关心,配置不清楚容易出现 404 的情况。

【资源】的定义就是指 js 代码之外,需要单独引入的部分,包括 css、js、图片、视频等

html中的 js css 路径引入

  • output.publicPath 定义了最初的资源引入路径,通常将它设置成”/“即可,这是因为通常我们会将前端代码部署到服务器请求的根目录上(并不一定是服务器的根目录,通常被 nginx 或服务端转发),就是说我们通常访问www.test.com/js/main.js就可以拿到这个js文件,此时在 html 中 script 引入路径就是 /js/main.js,这个路径是这台服务器的根目录,html 文件你可以放在任何位置。

    但是你设置成”/“在本地打开 html 文件肯定是 404 的,因为你本地电脑没有服务端的这些环境和转发逻辑,这个绝对路径在你电脑上是拿不到 js 文件的,这种需要和服务端一起部署才是完整的逻辑。

  • 你也可以将 output.publicPath 设置为 ”./“ ,此时在 html 中引入的就是 ./js/main.js相对路径,那此时你在本地打开 html 是可以访问到这个 js 文件的,它既然是相对路径,那就要求部署时,html 与 js 在同一目录下。

  • 所以无论你怎么配置,你应该要了解最终的打包产物要如何与服务端配合部署到服务器上。

图片等媒体资源路径引入

  • 图片资源的打包也一样,webpack 编译时可以将引入的图片统一存放到某个文件夹下,引入的路径还是需要你来配置,如果仍然使用 output.publicPath 的配置,如果你配置的是相对路径那代码和图片就需要保证相对位置是对的,这种情况设置成 ”/“ 绝对路径是最方便不易出错的,所以通常情况 output.publicPath 最好设置成绝对路径。

总共用到了多少个包

在梳理了构建所需要用到的基本配置后,我们数一下一共用到了多少个 npm 包,这几乎已经是我们构建一个标准项目用到最少包的极限了,除去 react 两个包,纯构建用到了 16 个 npm 包,这还是在 webpack5 已经内置了很多常用功能的基础上。

所以说项目构建确实是一件非常繁琐的事。

// package.json 
{
  "devDependencies": {
    "@babel/core": "^7.23.5",
    "@babel/plugin-transform-runtime": "^7.23.4",
    "@babel/preset-env": "^7.23.5",
    "@babel/preset-react": "^7.23.3",
    "babel-loader": "^9.1.3",
    "cross-env": "^7.0.3",
    "css-loader": "^6.8.1",
    "html-loader": "^4.2.0",
    "html-webpack-plugin": "^5.5.3",
    "less": "^4.2.0",
    "less-loader": "^11.1.3",
    "mini-css-extract-plugin": "^2.7.6",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "style-loader": "^3.3.3",
    "webpack": "^5.89.0",
    "webpack-dev-server": "^4.15.1",
    "webpack-merge": "^5.10.0"
  },
  "dependencies": {
    "@babel/runtime-corejs3": "^7.23.6"
  }
}

总结

  • 关于为什么要写一篇 webpack5 的配置实践呢,webpack 发展至今,经历了很多阶段,有过各种最佳实践不停地从出现到废弃,网络上有大量的 webpack 相关技术文章,但大多都过时或者语焉不详,webpack 本身不是特别难,但是涉及非常广的基础知识,操作系统、网络、编译、部署,所以我回想以前在其中遇到的各种问题,结合最新的 webpack5 文档想写一篇更容易理解的最佳实践。

  • 编译是一个很专业的领域,webpack 做了很多工作让复杂的编译流程变的简单,但我们应该清楚要负责一个项目的构建,你仍然需要懂得整个编译流程的方方面面,这是在之后根据业务特点去优化生产代码,增强开发体验的前提。

  • 构建对一个项目来说经常是一写定终身,业务成熟之后我们再去更改和优化构建的方式和策略,成本和风险都会很大,所以我们在项目之初做构建时还是要谨慎。

  • 想写一节关于 webpack 构建的优化策略,比较难,内容也会很多,还需要多调研下,所以下次再专门写一篇构建优化策略的文章。优化属于 webpack 的进阶使用了,实际上 webpack 默认的优化已经是非常到位了,绝对可以满足大多数项目的构建需求了。

  • 对于 webpack 的优化无非是构建过程、开发体验、构建产物三个方面,我们一定要清楚程序质量一定是大于构建过程和开发体验的,不要为了减少构建时间而影响构建产物的质量。

  • 了解了 webpack 基本原理,全部掌握了本文所说的基本配置,我认为你已经是一名合格的 【webpack 配置工程师】 了,如果业务在分包、优化、编译环节有更多需求,那就需要你慢慢探索了,定制化的需求是很难有最佳实践的。

  • 文章涉及内容较广,如果问题可在评论区指出,谢谢。

本文demo地址:webpack_demo

参考文献

原创声明

原创文章,转载请注明作者和文章原链接,关注公众号看更多文章哦!