Webpack 主要概念、功能以及体验优化

329 阅读4分钟

一、前言

随着 Web 业务日益复杂化和多元化以及前端模块化的流行,前端也进入了工程化的领域,而 Webpack 是当前阶段最流行的构建打包工具(bundler),没有之一(Vite 正在崛起)。

二、工程化

提到构建打包工具,就不得不提前端工程化,工程化给构建工具提供理论指导,构建工具是工程化的实现。 工程化可以分为模块化、组件化、规范化、自动化。而 Webpack 提供了这些问题的解决方案。

三、Webpack

Webpack 是一个静态模块打包器(bundler) 在 Webpack 看来,前端的所有资源(js/ts/json/css/less/jpg/gif/mp4/...)都会作为静态模块处理,src/index.js 作为默认入口文件(Webpack 分析入口),分析依赖关系,生成代码块(chunks),然后编译打包成静态资源(bundle)。

将程序员写完的源代码加工生成具有良好兼容性,且可以让浏览器高效稳定运行的的代码

Webpack 打包过程

Webpack 配置文件

Webpack 中,有五个核心属性,默认定义在webpack.config.js,它们分别是:

1、Entry

  1. 入口起点(入口点)指示 Webpack 应该使用该模块,来作为构建其内部依赖图(依赖关系图)的。进入入口起点后,Webpack 会发现有模块和库是入口起点(直接和间接)依赖的。
  2. 默认值是./src/index.js,但您可以通过在 Webpack 配置中配置 entry 属性,来指定一个(或多个)不同的入口起点。例如:
module.exports = {
  entry: './path/to/my/entry/file.js'
};

2、Output

  1. output 属性告诉 Webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。主要输出文件的默认值是 ./dist/main.js,其他生成文件默认放置在 ./dist 文件夹中。
  2. 以通过在配置中指定一个 output 字段,来配置这些处理过程:
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'
  }
};

在上面的示例中,我们通过 output.filenameoutput.path 属性,来告诉 webpack bundle 的名称,以及我们想要 bundle 生成(emit)到哪里。 tips: path 模块是一个 Node.js 核心模块,用于操作文件路径。

3、Loader

  1. webpack 只能理解 JavaScriptJSON 文件,这是 Webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。实际上,loader 只是一个普通的 funciton,接受匹配文件的字符串内容(借助资源模块完成),进行转换。

    loader 例子:

    /**
     * loader Function
     * @param {String} content 文件内容
     */
    module.exports = function(content){
        return "{};" + content
    }
    

    在更高层面,在 webpack 的配置中,loader 有两个属性:

  2. test 属性,识别出哪些文件会被转换。

  3. use 属性,定义出在进行转换时,应该使用哪个 loader。

const path = require('path');
module.exports = {
  output: {
    filename: 'my-first-webpack.bundle.js'
  },
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  }
}

以上配置中,对一个单独的 module 对象定义了 rules 属性,里面包含两个必须属性:testuse。这告诉 Webpack 编译器(compiler)如下信息:

“嘿,webpack 编译器,当你碰到「在require()/import语句中被解析为'.txt' 的路径」时,在你对它打包之前,先 use(使用)raw-loader转换一下。”

tips: 请记住,使用正则表达式匹配文件时,你不要为它添加引号。也就是说,/\.txt$/'/\.txt$/'"/\.txt$/"不一样。前者指示 Webpack 匹配任何以 .txt 结尾的文件,后者指示 webpack 匹配具有绝对路径 '.txt' 的单个文件。

4、Plugin

loader 用于转换某些类型的模块,而 plugin 则可以用于执行范围更广的任务。包括:打包优化资源管理注入环境变量

想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用new操作符来创建一个插件实例。

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

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

在上面的示例中,html-webpack-plugin 为应用程序生成一个 HTML 文件,并自动注入所有生成的bundle。

5、Mode

通过选择 development, productionnone 之中的一个,来设置mode 参数,以启用 webpack内置在相应环境下的优化。默认值为production。或者从 CLI 参数中传递:webpack --mode=development

module.exports = {
  mode: 'production'
};
选项描述
development会将DefinePluginprocess.env.NODE_ENV的值设置为development。为模块和chunk启用有效的名。
production会将DefinePluginprocess.env.NODE_ENV的值设置为production。为模块和chunk启用确定性的混淆名称,FlagDependencyUsagePluginFlagIncludedChunksPluginModuleConcatenationPluginNoEmitOnErrorsPluginTerserPlugin
none不使用任何默认优化选项

四、Webpack 构建性能

  1. 先使用 speed-measure-webpack-plugin 插件对项目打包耗时进行评估。

    // webpack.config.js
    
    const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
    const smp = new SpeedMeasurePlugin();
    const webpackConfig = smp.wrap(webpackConfig);
    
  2. 输出如下:

    image.png

  3. 再根据分析出的报告进行针对性的优化。

根据代码执行环境,Webpack 性能优化分为三个部分,分别是通用环境开发环境生产环境

通用环境

Dll

使用 DllPlugin 为更改不频繁的代码生成单独的编译结果。这可以提高应用程序的编译速度,尽管它增加了构建过程的复杂度。

Loader

将 loader 应用于最少数量的必要模块。而非如下:

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /.js$/,
        loader: 'babel-loader',
      },
    ],
  },
};

通过使用 include 字段,仅将 loader 应用在实际需要将其转换的模块:

const path = require('path');

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /.js$/,
        include: path.resolve(__dirname, 'src'),
        loader: 'babel-loader',
      },
    ],
  },
};

解析

以下步骤可以提高解析速度:

  • 减少 resolve.modulesresolve.extensionsresolve.mainFilesresolve.descriptionFiles 中条目数量,因为他们会增加文件系统调用的次数。
  • 如果你不使用 symlinks(例如 npm link 或者 yarn link),可以设置 resolve.symlinks: false
  • 如果你使用自定义 resolve plugin 规则,并且没有指定 context 上下文,可以设置 resolve.cacheWithContext: false

worker 池(worker pool)

thread-loader 可以将非常消耗资源的 loader 分流给一个 worker pool。在耗时的 Loader 前开启多进程。

Warning:不要使用太多的 worker,因为 Node.js 的 runtime 和 loader 都有启动开销。最小化 worker 和 main process(主进程) 之间的模块传输。进程间通讯(IPC, inter process communication)是非常消耗资源的。

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /.js$/,
        include: path.resolve('src'),
        use: [
          "thread-loader",
          // 耗时的 loader (例如 babel-loader)
        ],
      },
    ],
  },
};

持久化缓存

在 webpack 配置中使用 cache 选项。使用 package.json 中的 "postinstall" 清除缓存目录。

oneOf

当规则匹配时,只使用第一个匹配规则,并跳出匹配数组。

webpack.config.js

module: {
  rules: [{ 
    oneOf: [{
      resourceQuery: /raw/,
      type: 'asset/source',
      },
      {
      test: /.m?js$/,
      use: [ ... ]
    }]
  }]
},

开发环境

增量编译

使用 webpack 的 watch mode(监听模式)。而不使用其他工具来 watch 文件和调用 webpack 。内置的 watch mode 会记录时间戳并将此信息传递给 compilation 以使缓存失效。

在某些配置环境中,watch mode 会回退到 poll mode(轮询模式)。监听许多文件会导致 CPU 大量负载。在这些情况下,可以使用 watchOptions.poll 来增加轮询的间隔时间。

在内存中编译

下面几个工具通过在内存中(而不是写入磁盘)编译和 serve 资源来提高性能:

  • webpack-dev-server
  • webpack-hot-middleware
  • webpack-dev-middleware

HMR(hot module replacement)模块热替换

HMR 功能会在应用程序运行过程中,替换、添加或删除模块,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:

  • 保留在完全重新加载页面期间丢失的应用程序状态。
  • 只更新变更内容,以节省宝贵的开发时间。
  • 在源代码中 CSS/JS 产生修改时,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。

Webpack 配置方式:

// webpack.config.js
module.exports = {
    devServer: {
      hot: true,
    },
}
  1. Css 文件:可以使用 HMR 功能,因为 style-loader 内部实现了
  2. JS 文件:默认不能使用 HMR 功能,需要修改 js 代码,添加支持 HMR 功能的代码
  3. HTML 文件:默认不能使用 HRM 功能,同时会导致问题:html 文件不能热更新了

Devtool

需要注意的是不同的 devtool 设置,会导致性能差异。

  • "eval" 具有最好的性能,但并不能帮助你转译代码。
  • 如果你能接受稍差一些的 map 质量,可以使用 cheap-source-map 变体配置来提高性能
  • 使用 eval-source-map 变体配置进行增量编译。

Tip:在大多数情况下,最佳选择是 eval-cheap-module-source-map

避免在生产环境下才会用到的工具

某些 utility, plugin 和 loader 都只用于生产环境。例如,在开发环境下使用 TerserPlugin 来 minify(压缩) 和 mangle(混淆破坏) 代码是没有意义的。通常在开发环境下,应该排除以下这些工具:

  • TerserPlugin
  • [fullhash]/[chunkhash]/[contenthash]
  • AggressiveSplittingPlugin
  • AggressiveMergingPlugin
  • ModuleConcatenationPlugin

生产环境

Source-Map

一种提供源代码到构建后代码映射技术(如果构建后代码错了,通过映射可以追踪源代码错误) 建议配置为精简项,减少 CPU 压力。

// webpack.config.js

module.exports = {
devtool:'nosources-source-map'
...
}

可选项:[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

  1. source-map:生成外部调试文件 提供错误代码准确信息 和 源代码的错误位置
  2. inline-source-map:以base64方式内联进打包文件,提供错误代码准确信息和源代码的错误位置
  3. hidden-source-map:提示错误代码错误原因,没有错误位置,不能追踪到源代码错误
  4. eval-source-map:每一个文件都生成对应的 source-map,都包裹在 eval 文件,提供错误代码准确信息和源代码的错误位置
  5. nosource-source-map:提示错误代码错误原因,但是没有任何源代码信息
  6. cheap-source-map:提示错误代码错误原因和源代码错误位置,但只能精确到行
  7. cheap-module-source-map:提示错误代码错误原因和源代码错误位置,module 会将loader 的 source-map 加入

Babel 缓存

通过使用 cacheDirectory 将转译的结果缓存到文件系统中。

// webpack.config.js
module: {
  rules: [
    {
      test: /.m?js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env'],
          cacheDirectory:true, // 缓存
          cacheCompression:false // 取消缓存压缩
        }
      }
    }
  ]
}

五、Webpack 优化静态资源

CssMinimizerPlugin

压缩 CSS 文件

// webpack.config.js
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
  optimization: {
    minimizer: [
      // '...',
      new CssMinimizerPlugin(),
    ],
  },
};

TerserWebpackPlugin

压缩 JS 文件

// webpack.config.js
const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
    // '...',
    new TerserPlugin()
    ],
  },
};

Hash 优化

减少服务器部署文件的更新,合理利用浏览器缓存机制,提高静态资源加载速度。

  1. hash: 每次 Webpack 打包构建时都会生成不同的 hash 值
  2. chunkhash:根据 chunk 生成的 hash 值,如果打包来源同一个chunk,那么 hash 值就一样
  3. contenthash:根据文件内容生产的 hash 值,不同文件 hash 值不同。如果文件未更改,则每次生成的 hash 值相同

Tree shaking

去除无用代码。

前提:

  • 使用 ES2015 模块语法(即 import 和 export)。
  • 确保没有编译器将您的 ES2015 模块语法转换为 CommonJS 的(顺带一提,这是现在常用的 @babel/preset-env 的默认行为,详细信息请参阅文档)。
  • 在项目的 package.json 文件中,添加 "sideEffects" 属性。
  • 使用 mode 为 "production" 的配置项以启用 tree shaking。

参考:

webpack.docschina.org/guides/buil…