• Webpack 5 超详细解读(五)


    41.代码拆分方式

    通过Webpack实现前端项目整体模块化的优势很明显,但是它同样存在一些弊端,那就是项目当中所有的代码最终都会被打包到一起,试想一下,如果说应用非常复杂,模块非常多的话,那打包结果就会特别的大,很多时候超过两三兆也是非常常见的事情。而事实情况是,大多数时候在应用开始工作时,并不是所有的模块都是必须要加载进来的,但是,这些模块又被全部打包到一起,需要任何一个模块,都必须得把整体加载下来过后才能使用。而应用一般又是运行在浏览器端,这就意味着会浪费掉很多的流量和带宽。

    更为合理的方案就是把的打包结果按照一定的规则去分离到多个bundle.js当中,然后根据应用的运行需要,按需加载这些模块,这样的话就可以大大提高应用的响应速度以及它的运行效率。可能有人会想起来在一开始的时候说过Webpack就是把项目中散落的那些模块合并到一起,从而去提高运行效率,那这里又在说它应该把它分离开,这两个说法是不是自相矛盾?其实这并不是矛盾,只是物极必反而已,资源太大了也不行,太碎了更不行,项目中划分的这种模块的颗粒度一般都会非常的细,很多时候一个模块只是提供了一个小小的工具函数,它并不能形成一个完整的功能单元,如果不把这些散落的模块合并到一起,就有可能再去运行一个小小的功能时,会加载非常多的模块。而目前所主流的这种HTTP1.1协议,它本身就有很多缺陷,例如并不能同时对同一个域名下发起很多次的并行请求,而且每一次请求都会有一定的延迟,另外每次请求除了传输具体的内容以外,还会有额外的header请求头和响应头,当大量的这种请求的情况下,这些请求头加在一起,也是很大的浪费。

    综上所述,模块打包肯定是有必要的,不过像应用越来越大过后,要开始慢慢的学会变通。为了解决这样的问题,Webpack支持一种分包的功能,也可以把这种功能称之为代码分割,它通过把模块,按照所设计的一个规则打包到不同的bundle.js当中,从而去提高应用的响应速度,目前的Webpack去实现分包的方式主要有两种:

    • 第一种就是根据业务去配置不同的打包入口,也就是会有同时多个打包入口同时打包,这时候就会输出多个打包结果;

    • 第二种是多入口文件,单独打包依赖包的形式;

    • 第三种就是采用ES Module的动态导入的功能,去实现模块的按需加载,这个时候Webpack会自动的把动态导入的这个模块单独输出的一个bundle.js当中。

    41.1 多入口文件打包

    多入口打包一般适用于传统的“多页”应用程序。最常见的划分规则是一个页面对应一个打包入口,对于不同页面之间公共的部分再去提取到公共的结果中。

    目录结构

    一般Webpack.config.js配置文件中的entry属性只会一个文件路径(打包入口),如果需要配置多个打包入口,则需要将entry属性定义成为一个对象(注意不是数组,如果是数组的话,那就是将多个文件打包到一起,对于整个应用来讲依然是一个入口)。一旦配置为多入口,输出的文件名也需要修改**“[name].bundle.js**”,[name]最终会被替换成入口的名称,也就是index和album。

    const { CleanWebpackPlugin } = require('clean-Webpack-plugin')
    const HtmlWebpackPlugin = require('html-Webpack-plugin')
    
    module.exports = {
      mode: 'none',
      entry: {
        index: './src/index.js',  // 多入口
        album: './src/album.js'
      },
      output: {
        filename: '[name].bundle.js'  // [name]占位符,最终被替换为入口名称index和album
      },
      optimization: {
        splitChunks: {
          // 自动提取所有公共模块到单独 bundle
          chunks: 'all'
        }
      },
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              'style-loader',
              'css-loader'
            ]
          }
        ]
      },
      plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
          title: 'Multi Entry',
          template: './src/index.html',
          filename: 'index.html',
        }),
        new HtmlWebpackPlugin({
          title: 'Multi Entry',
          template: './src/album.html',
          filename: 'album.html',
        })
      ]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    命令行运行yarn Webpack命令,打开dist目录发现已经有两个js文件。

    41.2 多入口依赖包单独打包

    多入口打包本身非常容易理解,也非常容易使用,但是它也存在一个小小的问题,就是在不同的打包入口当中,它一定会有那么一些公共的部分,按照目前这种多入口的打包方式,不同的打包结果当中就会出现相同的模块,例如在我们这里index入口和album入口当中就共同使用了global.css和fetch.js这两个公共的模块,因为实例比较简单,所以说重复的影响不会有那么大,但是如果共同使用的是jQuery或者Vue这种体积比较大的模块,那影响的话就会特别的大,所以说需要把这些公共的模块去。提取到一个单独的bundle.js当中,Webpack中实现公共模块提取的方式也非常简单,只需要在优化配置当中去开启一个叫splitChunks的一个功能就可以了,回到配置文件当中,配置如下:

    const { CleanWebpackPlugin } = require('clean-Webpack-plugin')
    const HtmlWebpackPlugin = require('html-Webpack-plugin')
    
    module.exports = {
      mode: 'none',
      entry: {
        index: './src/index.js',
        album: './src/album.js'
        // 或者使用下面的写法
        // index: { import : './src/index.js', dependOn: 'shared' },
        // album: { import : './src/album.js', dependOn: 'shared' },
        // shared: ['jquery', 'lodash']
      },
      output: {
        filename: '[name].bundle.js'
      },
      optimization: {
        splitChunks: {
          // 自动提取所有公共模块到单独 bundle
          chunks: 'all'  // 表示会把所有的公共模块都提取到单独的bundle.js当中
        }
      },
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              'style-loader',
              'css-loader'
            ]
          }
        ]
      },
      plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
          title: 'Multi Entry',
          template: './src/index.html',
          filename: 'index.html',
          chunks: ['index']
        }),
        new HtmlWebpackPlugin({
          title: 'Multi Entry',
          template: './src/album.html',
          filename: 'album.html',
          chunks: ['album']
        })
      ]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    打开命令行运行yarn Webpack后发现,公共模块的部分被打包进album~index.bundle.js中去了。

    41.3 动态导入的形式打包

    按需加载是开发浏览器应用当中一个非常常见的需求,一般常说的按需加载指的是加载数据,这里所说的按需加载指的是在应用运行过程中需要某个模块时才去加载这个模块,这种方式可以极大的节省带宽和流量。Webpack支持使用动态导入的这种方式来去实现模块的按需加载,而且所有动态导入的模块都会被自动提取到单独的bundle.js当中,从而实现分包,相比于多入口的方式,动态导入更为灵活,因为通过代码的逻辑去控制,需不需要加载某个模块,或者是时候加的某个模块。而分包的目的中就有很重要的一点就是:让模块实现按需加载,从而去提高应用的响应速度。

    具体来看如何使用,这里已经提前设计好了一个可以发挥按需加载作用的场景,在这个页面的主体区域,如果访问的是文章页的话,得到的就是一个文章列表,如果访问的是相册页,显示的就是相册列表。

    项目目录:

    动态导入使用的就是ESM标准当中的动态导入,在需要动态导入组件的地方,通过这个函数导入指定的路径,这个方法返回的就是一个promise,promise的方法当中就可以拿到模块对象,由于网站是使用的默认导出,所以说这里需要去解构模块对象当中的default,然后把它放到post的这个变量当中,拿到这个成员过后,使用mainElement.appendChild(posts())创建页面元素,album组件也是如此。完成以后再次回到浏览器,此时页面仍然可以正常工作的。

    // import posts from './posts/posts'
    // import album from './album/album'
    
    const render = () => {
      const hash = window.location.hash || '#posts'
      console.log(hash)
      const mainElement = document.querySelector('.main')
    
      mainElement.innerHTML = ''
    
      if (hash === '#posts') {
        // mainElement.appendChild(posts())
        // 这个方法返回的就是一个promise,promise的方法当中就可以拿到模块对象,由于网站是使用的默认导出,所以说这里需要去解构模块对象当中的default,然后把它放到post的这个变量当中
        import('./posts/posts').then(({ default: posts }) => {
          mainElement.appendChild(posts())
        })
      } else if (hash === '#album') {
        // mainElement.appendChild(album())
        import('./album/album').then(({ default: album }) => {
          mainElement.appendChild(album())
        })
      }
    }
    
    render()
    
    window.addEventListener('hashchange', render)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    这时再回到开发工具当中,然后重新去运行打包,然后去看看此时打包的结果是什么样子的,打包结束,打开dist目录,此时dist目录下就会多出3个js文件,那这三个js文件,实际上就是由动态导入自动分包所产生的。这3个文件的分别是刚刚导入的两个模块index.js/album.js,以及这两个模块当中公共模块fetch.js。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1IMyFHpN-1669024724129)(http://5coder.cn/img/1668693735_0abaef9539abad0d157664b059ba6660.png)]

    动态导入整个过程无需配置任何一个地方,只需要按照ESM动态导入成员的方式去导入模块就可以,内部会自动处理分包和按需加载,如果说你使用的是单页应用开发框架,比如react或者Vue的话,在你项目当中的路由映射组件,就可以通过这种动态导入的方式实现按需加载

    42.splitchunks 配置

    最初,chunks(以及内部导入的模块)是通过内部 Webpack 图谱中的父子关系关联的。CommonsChunkPlugin 曾被用来避免他们之间的重复依赖,但是不可能再做进一步的优化。

    从 Webpack v4 开始,移除了 CommonsChunkPlugin,取而代之的是 optimization.splitChunks

    默认值

    开箱即用的 SplitChunksPlugin 对于大部分用户来说非常友好。

    默认情况下,它只会影响到按需加载的 chunks,因为修改 initial chunks 会影响到项目的 HTML 文件中的脚本标签。

    Webpack 将根据以下条件自动拆分 chunks:

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

    当尝试满足最后两个条件时,最好使用较大的 chunks。

    配置

    Webpack 为希望对该功能进行更多控制的开发者提供了一组选项。

    选择了默认配置为了符合 Web 性能最佳实践,但是项目的最佳策略可能有所不同。如果要更改配置,则应评估所做更改的影响,以确保有真正的收益。

    optimization.splitChunks

    下面这个配置对象代表 SplitChunksPlugin 的默认行为。

    Webpack.config.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,
            },
          },
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    当 Webpack 处理文件路径时,它们始终包含 Unix 系统中的 / 和 Windows 系统中的 \。这就是为什么在 {cacheGroup}.test 字段中使用 [\\/] 来表示路径分隔符的原因。{cacheGroup}.test 中的 /\ 会在跨平台使用时产生问题。

    从 Webpack 5 开始,不再允许将 entry 名称传递给 {cacheGroup}.test 或者为 {cacheGroup}.name 使用现有的 chunk 的名称。

    splitChunks.automaticNameDelimiter
    string = '~'
    
    • 1

    默认情况下,Webpack 将使用 chunk 的来源和名称生成名称(例如 vendors~main.js)。此选项使你可以指定用于生成名称的分隔符。

    splitChunks.chunks
    string = 'async'` `function (chunk)
    
    • 1

    这表明将选择哪些 chunk 进行优化。当提供一个字符串,有效值为 allasyncinitial。设置为 all 可能特别强大,因为这意味着 chunk 可以在异步和非异步 chunk 之间共享。

    Note that it is applied to the fallback cache group as well (splitChunks.fallbackCacheGroup.chunks).

    Webpack.config.js

    module.exports = {
      //...
      optimization: {
        splitChunks: {
          // include all types of chunks
          chunks: 'all',
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    或者,你也可以提供一个函数去做更多的控制。这个函数的返回值将决定是否包含每一个 chunk。

    module.exports = {
      //...
      optimization: {
        splitChunks: {
          chunks(chunk) {
            // exclude `my-excluded-chunk`
            return chunk.name !== 'my-excluded-chunk';
          },
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    你可以将此配置与 HtmlWebpackPlugin 结合使用。它将为你注入所有生成的 vendor chunks。

    splitChunks.maxAsyncRequests
    number = 30
    
    • 1

    按需加载时的最大并行请求数。

    splitChunks.maxInitialRequests
    number = 30
    
    • 1

    入口点的最大并行请求数。

    splitChunks.defaultSizeTypes
    [string] = ['javascript', 'unknown']
    
    • 1

    Sets the size types which are used when a number is used for sizes.

    splitChunks.minChunks
    number = 1
    
    • 1

    拆分前必须共享模块的最小 chunks 数。

    splitChunks.hidePathInfo
    boolean
    
    • 1

    为由 maxSize 分割的部分创建名称时,阻止公开路径信息。

    splitChunks.minSize
    number = 20000` `{ [index: string]: number }
    
    • 1

    生成 chunk 的最小体积(以 bytes 为单位)。

    splitChunks.minSizeReduction
    number` `{ [index: string]: number }
    
    • 1

    生成 chunk 所需的主 chunk(bundle)的最小体积(以字节为单位)缩减。这意味着如果分割成一个 chunk 并没有减少主 chunk(bundle)的给定字节数,它将不会被分割,即使它满足 splitChunks.minSize

    为了生成 chunk,splitChunks.minSizeReductionsplitChunks.minSize 都需要被满足。

    splitChunks.enforceSizeThreshold
    splitChunks.cacheGroups.{cacheGroup}.enforceSizeThreshold
    number = 50000
    
    • 1

    强制执行拆分的体积阈值和其他限制(minRemainingSize,maxAsyncRequests,maxInitialRequests)将被忽略。

    splitChunks.minRemainingSize
    splitChunks.cacheGroups.{cacheGroup}.minRemainingSize
    number = 0
    
    • 1

    在 Webpack 5 中引入了 splitChunks.minRemainingSize 选项,通过确保拆分后剩余的最小 chunk 体积超过限制来避免大小为零的模块。 ‘development’ 模式 中默认为 0。对于其他情况,splitChunks.minRemainingSize 默认为 splitChunks.minSize 的值,因此除需要深度控制的极少数情况外,不需要手动指定它。

    splitChunks.minRemainingSize 仅在剩余单个 chunk 时生效。

    splitChunks.layer
    splitChunks.cacheGroups.{cacheGroup}.layer
    RegExp` `string` `function
    
    • 1

    按模块层将模块分配给缓存组。

    splitChunks.maxSize
    number = 0
    
    • 1

    使用 maxSize(每个缓存组 optimization.splitChunks.cacheGroups[x].maxSize 全局使用 optimization.splitChunks.maxSize 或对后备缓存组 optimization.splitChunks.fallbackCacheGroup.maxSize 使用)告诉 Webpack 尝试将大于 maxSize 个字节的 chunk 分割成较小的部分。 这些较小的部分在体积上至少为 minSize(仅次于 maxSize)。 该算法是确定性的,对模块的更改只会产生局部影响。这样,在使用长期缓存时就可以使用它并且不需要记录。maxSize 只是一个提示,当模块大于 maxSize 或者拆分不符合 minSize 时可能会被违反。

    当 chunk 已经有一个名称时,每个部分将获得一个从该名称派生的新名称。 根据 optimization.splitChunks.hidePathInfo 的值,它将添加一个从第一个模块名称或其哈希值派生的密钥。

    maxSize 选项旨在与 HTTP/2 和长期缓存一起使用。它增加了请求数量以实现更好的缓存。它还可以用于减小文件大小,以加快二次构建速度。

    maxSizemaxInitialRequest/maxAsyncRequests 具有更高的优先级。实际优先级是 maxInitialRequest/maxAsyncRequests < maxSize < minSize

    设置 maxSize 的值会同时设置 maxAsyncSizemaxInitialSize 的值。

    splitChunks.maxAsyncSize
    number
    
    • 1

    maxSize 一样,maxAsyncSize 可以为 cacheGroups(splitChunks.cacheGroups.{cacheGroup}.maxAsyncSize)或 fallback 缓存组(splitChunks.fallbackCacheGroup.maxAsyncSize )全局应用(splitChunks.maxAsyncSize

    maxAsyncSizemaxSize 的区别在于 maxAsyncSize 仅会影响按需加载 chunk。

    splitChunks.maxInitialSize
    number
    
    • 1

    maxSize 一样,maxInitialSize 可以对 cacheGroups(splitChunks.cacheGroups.{cacheGroup}.maxInitialSize)或 fallback 缓存组(splitChunks.fallbackCacheGroup.maxInitialSize)全局应用(splitChunks.maxInitialSize)。

    maxInitialSizemaxSize 的区别在于 maxInitialSize 仅会影响初始加载 chunks。

    splitChunks.name
    boolean = false` `function (module, chunks, cacheGroupKey) => string` `string
    
    • 1

    每个 cacheGroup 也可以使用:splitChunks.cacheGroups.{cacheGroup}.name

    拆分 chunk 的名称。设为 false 将保持 chunk 的相同名称,因此不会不必要地更改名称。这是生产环境下构建的建议值。

    提供字符串或函数使你可以使用自定义名称。指定字符串或始终返回相同字符串的函数会将所有常见模块和 vendor 合并为一个 chunk。这可能会导致更大的初始下载量并减慢页面加载速度。

    如果你选择指定一个函数,则可能会发现 chunk.namechunk.hash 属性(其中 chunkchunks 数组的一个元素)在选择 chunk 名时特别有用。

    如果 splitChunks.nameentry point 名称匹配,entry point 将被删除。

    splitChunks.cacheGroups.{cacheGroup}.name can be used to move modules into a chunk that is a parent of the source chunk. For example, use name: "entry-name" to move modules into the entry-name chunk. You can also use on demand named chunks, but you must be careful that the selected modules are only used under this chunk.

    main.js

    import _ from 'lodash';
    
    console.log(_.join(['Hello', 'Webpack'], ' '));
    
    • 1
    • 2
    • 3

    Webpack.config.js

    module.exports = {
      //...
      optimization: {
        splitChunks: {
          cacheGroups: {
            commons: {
              test: /[\\/]node_modules[\\/]/,
              // cacheGroupKey here is `commons` as the key of the cacheGroup
              name(module, chunks, cacheGroupKey) {
                const moduleFileName = module
                  .identifier()
                  .split('/')
                  .reduceRight((item) => item);
                const allChunksNames = chunks.map((item) => item.name).join('~');
                return `${cacheGroupKey}-${allChunksNames}-${moduleFileName}`;
              },
              chunks: 'all',
            },
          },
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    使用以下 splitChunks 配置来运行 Webpack 也会输出一组公用组,其下一个名称为:commons-main-lodash.js.e7519d2bb8777058fa27.js(以哈希方式作为真实世界输出示例)。

    在为不同的拆分 chunk 分配相同的名称时,所有 vendor 模块都放在一个共享的 chunk 中,尽管不建议这样做,因为这可能会导致下载更多代码。

    splitChunks.usedExports
    splitChunks.cacheGroups{cacheGroup}.usedExports
    boolean = true
    
    • 1

    弄清哪些 export 被模块使用,以混淆 export 名称,省略未使用的 export,并生成有效的代码。 当它为 true 时:分析每个运行时使用的出口,当它为 "global" 时:分析所有运行时的全局 export 组合)。

    splitChunks.cacheGroups

    缓存组可以继承和/或覆盖来自 splitChunks.* 的任何选项。但是 testpriorityreuseExistingChunk 只能在缓存组级别上进行配置。将它们设置为 false以禁用任何默认缓存组。

    Webpack.config.js

    module.exports = {
      //...
      optimization: {
        splitChunks: {
          cacheGroups: {
            default: false,
          },
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    splitChunks.cacheGroups.{cacheGroup}.priority
    number = -20
    
    • 1

    一个模块可以属于多个缓存组。优化将优先考虑具有更高 priority(优先级)的缓存组。默认组的优先级为负,以允许自定义组获得更高的优先级(自定义组的默认值为 0)。

    splitChunks.cacheGroups.{cacheGroup}.reuseExistingChunk
    boolean = true
    
    • 1

    如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块。这可能会影响 chunk 的结果文件名。

    Webpack.config.js

    module.exports = {
      //...
      optimization: {
        splitChunks: {
          cacheGroups: {
            defaultVendors: {
              reuseExistingChunk: true,
            },
          },
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    splitChunks.cacheGroups.{cacheGroup}.type
    function` `RegExp` `string
    
    • 1

    允许按模块类型将模块分配给缓存组。

    Webpack.config.js

    module.exports = {
      //...
      optimization: {
        splitChunks: {
          cacheGroups: {
            json: {
              type: 'json',
            },
          },
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    splitChunks.cacheGroups.test
    splitChunks.cacheGroups.{cacheGroup}.test
    function (module, { chunkGraph, moduleGraph }) => boolean` `RegExp` `string
    
    • 1

    控制此缓存组选择的模块。省略它会选择所有模块。它可以匹配绝对模块资源路径或 chunk 名称。匹配 chunk 名称时,将选择 chunk 中的所有模块。

    {cacheGroup}.test 提供一个函数:

    Webpack.config.js

    module.exports = {
      //...
      optimization: {
        splitChunks: {
          cacheGroups: {
            svgGroup: {
              test(module) {
                // `module.resource` contains the absolute path of the file on disk.
                // Note the usage of `path.sep` instead of / or \, for cross-platform compatibility.
                const path = require('path');
                return (
                  module.resource &&
                  module.resource.endsWith('.svg') &&
                  module.resource.includes(`${path.sep}cacheable_svgs${path.sep}`)
                );
              },
            },
            byModuleTypeGroup: {
              test(module) {
                return module.type === 'javascript/auto';
              },
            },
          },
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    为了查看 module and chunks 对象中可用的信息,你可以在回调函数中放入 debugger; 语句。然后 以调试模式运行 Webpack 构建 检查 Chromium DevTools 中的参数。

    {cacheGroup}.test 提供 RegExp

    Webpack.config.js

    module.exports = {
      //...
      optimization: {
        splitChunks: {
          cacheGroups: {
            defaultVendors: {
              // Note the usage of `[\\/]` as a path separator for cross-platform compatibility.
              test: /[\\/]node_modules[\\/]|vendor[\\/]analytics_provider|vendor[\\/]other_lib/,
            },
          },
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    splitChunks.cacheGroups.{cacheGroup}.filename
    string` `function (pathData, assetInfo) => string
    
    • 1

    仅在初始 chunk 时才允许覆盖文件名。 也可以在 output.filename 中使用所有占位符。

    也可以在 splitChunks.filename 中全局设置此选项,但是不建议这样做,如果 splitChunks.chunks 未设置为 'initial',则可能会导致错误。避免全局设置。

    Webpack.config.js

    module.exports = {
      //...
      optimization: {
        splitChunks: {
          cacheGroups: {
            defaultVendors: {
              filename: '[name].bundle.js',
            },
          },
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    若为函数,则:

    Webpack.config.js

    module.exports = {
      //...
      optimization: {
        splitChunks: {
          cacheGroups: {
            defaultVendors: {
              filename: (pathData) => {
                // Use pathData object for generating filename string based on your requirements
                return `${pathData.chunk.name}-bundle.js`;
              },
            },
          },
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    通过提供以文件名开头的路径 'js/vendor/bundle.js',可以创建文件夹结构。

    Webpack.config.js

    module.exports = {
      //...
      optimization: {
        splitChunks: {
          cacheGroups: {
            defaultVendors: {
              filename: 'js/[name]/bundle.js',
            },
          },
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    splitChunks.cacheGroups.{cacheGroup}.enforce
    boolean = false
    
    • 1

    告诉 Webpack 忽略 splitChunks.minSizesplitChunks.minChunkssplitChunks.maxAsyncRequestssplitChunks.maxInitialRequests 选项,并始终为此缓存组创建 chunk。

    Webpack.config.js

    module.exports = {
      //...
      optimization: {
        splitChunks: {
          cacheGroups: {
            defaultVendors: {
              enforce: true,
            },
          },
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    splitChunks.cacheGroups.{cacheGroup}.idHint
    string
    
    • 1

    设置 chunk id 的提示。 它将被添加到 chunk 的文件名中。

    Webpack.config.js

    module.exports = {
      //...
      optimization: {
        splitChunks: {
          cacheGroups: {
            defaultVendors: {
              idHint: 'vendors',
            },
          },
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    Examples

    Defaults: Example 1
    // index.js
    
    import('./a'); // dynamic import
    // a.js
    import 'react';
    
    //...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    结果: 将创建一个单独的包含 react 的 chunk。在导入调用中,此 chunk 并行加载到包含 ./a 的原始 chunk 中。

    为什么:

    • 条件1:chunk 包含来自 node_modules 的模块
    • 条件2:react 大于 30kb
    • 条件3:导入调用中的并行请求数为 2
    • 条件4:在初始页面加载时不影响请求

    这背后的原因是什么?react 可能不会像你的应用程序代码那样频繁地更改。通过将其移动到单独的 chunk 中,可以将该 chunk 与应用程序代码分开进行缓存(假设你使用的是 chunkhash,records,Cache-Control 或其他长期缓存方法)。

    Defaults: Example 2
    // entry.js
    
    // dynamic imports
    import('./a');
    import('./b');
    // a.js
    import './helpers'; // helpers is 40kb in size
    
    //...
    // b.js
    import './helpers';
    import './more-helpers'; // more-helpers is also 40kb in size
    
    //...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    结果: 将创建一个单独的 chunk,其中包含 ./helpers 及其所有依赖项。在导入调用时,此 chunk 与原始 chunks 并行加载。

    为什么:

    • 条件1:chunk 在两个导入调用之间共享
    • 条件2:helpers 大于 30kb
    • 条件3:导入调用中的并行请求数为 2
    • 条件4:在初始页面加载时不影响请求

    helpers 的内容放入每个 chunk 中将导致其代码被下载两次。通过使用单独的块,这只会发生一次。我们会进行额外的请求,这可以视为一种折衷。这就是为什么最小体积为 30kb 的原因。

    Split Chunks: Example 1

    创建一个 commons chunk,其中包括入口(entry points)之间所有共享的代码。

    Webpack.config.js

    module.exports = {
      //...
      optimization: {
        splitChunks: {
          cacheGroups: {
            commons: {
              name: 'commons',
              chunks: 'initial',
              minChunks: 2,
            },
          },
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    此配置可以扩大你的初始 bundles,建议在不需要立即使用模块时使用动态导入。

    Split Chunks: Example 2

    创建一个 vendors chunk,其中包括整个应用程序中 node_modules 的所有代码。

    Webpack.config.js

    module.exports = {
      //...
      optimization: {
        splitChunks: {
          cacheGroups: {
            commons: {
              test: /[\\/]node_modules[\\/]/,
              name: 'vendors',
              chunks: 'all',
            },
          },
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这可能会导致包含所有外部程序包的较大 chunk。建议仅包括你的核心框架和实用程序,并动态加载其余依赖项。

    Split Chunks: Example 3

    创建一个 custom vendor chunk,其中包含与 RegExp 匹配的某些 node_modules 包。

    Webpack.config.js

    module.exports = {
      //...
      optimization: {
        splitChunks: {
          cacheGroups: {
            vendor: {
              test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
              name: 'vendor',
              chunks: 'all',
            },
          },
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这将导致将 reactreact-dom 分成一个单独的 chunk。 如果你不确定 chunk 中包含哪些包,请参考 Bundle Analysis 部分以获取详细信息。

    43.import 动态导入配置

    Webpack打包过程中利用动态导入的方式对代码进行拆包。之前使用import './title'的同步的方式进行导入,可以选择splitChunks选项进行配置。

    此时,我们更改导入的方式为异步导入,即使用import('./title')的方式进行导入,并且将splitChunks配置项删除,观察打包后的结果。

    可以看到打包后的结果为198.bundle.js文件。这是Webpack自身就会配置好的属性,无需进行其他配置。基于这个特点,对其周边进行补充。

    44.1 chunkIds

    根据官网介绍,chunkIds有几个配置的值,这里只针对naturalnameddeterministic进行测试。

    optimization.chunkIds

    boolean = false` `string: 'natural' | 'named' | 'size' | 'total-size' | 'deterministic'
    
    • 1

    告知 Webpack 当选择模块 id 时需要使用哪种算法。将 optimization.chunkIds 设置为 false 会告知 Webpack 没有任何内置的算法会被使用,但自定义的算法会由插件提供。optimization.chunkIds 的默认值是 false

    • 如果环境是开发环境,那么 optimization.chunkIds 会被设置成 'named',但当在生产环境中时,它会被设置成 'deterministic'
    • 如果上述的条件都不符合, optimization.chunkIds 会被默认设置为 'natural'

    下述选项字符串值均为被支持:

    选项值描述
    'natural'按使用顺序的数字 id。
    'named'对调试更友好的可读的 id。
    'deterministic'在不同的编译中不变的短数字 id。有益于长期缓存。在生产模式中会默认开启。
    'size'专注于让初始下载包大小更小的数字 id。
    'total-size'专注于让总下载包大小更小的数字 id。
    44.1.1 natural

    按使用顺序的数字 id。一般不使用,在多个导入的过程中,例如同时导入了title.jsa.js,打包过后会生成1.bundle.js2.bundle.js,但是当我们不再需要title.js时,再次进行打包,会生成1.bundle.js,这是浏览器就会存在缓存问题。

    打包结果:

    44.1.2 named

    对调试更友好的可读的 id。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1WiAFHds-1669024724134)(http://5coder.cn/img/1668724505_b01c65ada36e259e5eed23f863e6d506.png)]

    这里很明确的知道src_title_js.bundle.js是title.js打包生成后的结果。对于开发阶段没有任何影响,但是对于生产阶段,就会并不需要进行任何调试,就不需要更好的阅读。

    44.1.3 deterministic

    在不同的编译中不变的短数字 id。有益于长期缓存。在生产模式中会默认开启。

    在设置chunkIdsdeterministic时,发现就回到了最初的状态198.bundle.js,因为这是Webpack5中默认提供的。

    44.2 chunkFilename

    在动态导入中,还可以配置chunkFilename选项,对打包的结果进行重命名文件名。

    此时js/chunk_[name].js中的name与js/chunk_[name]_[id].js中的id指向的都是198。可以使用魔法注释的功能对打包结果的文件名进行重置。

    这样就可以很好的识别某个打包文件对应的源文件。

    44.runtimeChunk 优化配置

    针对Webpack中的optimization的优化过程中,还有一个runtimeChunk的配置。

    optimization.runtimeChunk

    object` `string` `boolean
    
    • 1

    optimization.runtimeChunk 设置为 true'multiple',会为每个入口添加一个只含有 runtime 的额外 chunk。此配置的别名如下:

    Webpack.config.js

    module.exports = {
    //...
    optimization: {
    runtimeChunk: {
    name: (entrypoint) => `runtime~${entrypoint.name}`,
    },
    },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    "single" 会创建一个在所有生成 chunk 之间共享的运行时文件。此设置是如下设置的别名:

    Webpack.config.js

    module.exports = {
    //...
    optimization: {
    runtimeChunk: {
    name: 'runtime',
    },
    },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    通过将 optimization.runtimeChunk 设置为 object,对象中可以设置只有 name 属性,其中属性值可以是名称或者返回名称的函数,用于为 runtime chunks 命名。

    默认值是 false:每个入口 chunk 中直接嵌入 runtime。

    Warning

    对于每个 runtime chunk,导入的模块会被分别初始化,因此如果你在同一个页面中引用多个入口起点,请注意此行为。你或许应该将其设置为 single,或者使用其他只有一个 runtime 实例的配置。

    Webpack.config.js

    module.exports = {
    //...
    optimization: {
    runtimeChunk: {
    name: (entrypoint) => `runtimechunk~${entrypoint.name}`,
    },
    },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    runtimeChunk,直观翻译是运行时的chunk文件,其作用是啥呢,通过调研了解了一波,在此记录下。

    44.1 何为运行时代码?

    形如import('abc').then(res=>{})这种异步加载的代码,在Webpack中即为运行时代码。在VueCli工程中常见的异步加载路由即为runtime代码。

    {
        path: '/about',
        name: 'About',
        // route level code-splitting
        // this generates a separate chunk (about.[hash].js) for this route
        // which is lazy-loaded when the route is visited.
        component: () => import(/* WebpackChunkName: "about" */ '../views/About.vue')
        // component: About
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    44.2 搭建工程测试功效

    1、搭建简单的vue项目,使用vuecli新建一个只需要router的项目,脚手架默认路由配置了一个异步加载的about路由,如上图所示

    2、不设置runtimeChunk时,查看打包文件,此时不需要做任何操作,因为其默认是false,直接yarn build,此时生成的主代码文件的hash值为7d50fa23

    3、接着改变about.vue文件的内容,再次build,查看打包结果,发现app文件的hash值发生了变化。

    发现app文件的hash值发生了变化。

    设置runtimeChunk是将包含chunks 映射关系的 list单独从 app.js里提取出来,因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,所以每次改动都会影响它,如果不将它提取出来的话,等于app.js每次都会改变。缓存就失效了。设置runtimeChunk之后,Webpack就会生成一个个runtime~xxx.js的文件。
    然后每次更改所谓的运行时代码文件时,打包构建时app.js的hash值是不会改变的。如果每次项目更新都会更改app.js的hash值,那么用户端浏览器每次都需要重新加载变化的app.js,如果项目大切优化分包没做好的话会导致第一次加载很耗时,导致用户体验变差。现在设置了runtimeChunk,就解决了这样的问题。所以这样做的目的是避免文件的频繁变更导致浏览器缓存失效,所以其是更好的利用缓存。提升用户体验。

    4、新建vue.config.js,配置runtimeChunk,第一次打包,然后修改about,在打包一次,查看2次打包之后app文件的hash值的变化。

    // vue.config.js
    module.exports = {
      productionSourceMap: false,
      configureWebpack: {
         runtimeChunk: true
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    通过截图看到2次打包生成的app文件的hash值没有改变。和上面说的作用一致。

    44.3 你以为这就完了?

    1、查看下runtime~xxx.js文件内容:

    function a(e){return i.p+"js/"+({about:"about"}[e]||e)+"."+{about:"3cc6fa76"}[e]+".js"}f
    
    • 1

    发现文件很小,且就是加载chunk的依赖关系的文件。虽然每次构建后app的hash没有改变,但是runtime~xxx.js会变啊。每次重新构建上线后,浏览器每次都需要重新请求它,它的 http 耗时远大于它的执行时间了,所以建议不要将它单独拆包,而是将它内联到我们的 index.html 之中。这边我们使用script-ext-html-Webpack-plugin来实现。(也可使用html-Webpack-inline-source-plugin,其不会删除runtime文件。)

    // vue.config.js
    const ScriptExtHtmlWebpackPlugin = require('script-ext-html-Webpack-plugin')
    module.exports = {
      productionSourceMap: false,
      configureWebpack: {
        optimization: {
          runtimeChunk: true
        },
        plugins: [
          new ScriptExtHtmlWebpackPlugin({
            inline: /runtime~.+\.js$/  //正则匹配runtime文件名
          })
        ]
      },
      chainWebpack: config => {
        config.plugin('preload')
          .tap(args => {
            args[0].fileBlacklist.push(/runtime~.+\.js$/) //正则匹配runtime文件名,去除该文件的preload
            return args
          })
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    重新打包,查看index.html文件

    DOCTYPE html>
    <html lang=en>
    
    <head>
        <meta charset=utf-8>
        <meta http-equiv=X-UA-Compatible content="IE=edge">
        <meta name=viewport content="width=device-width,initial-scale=1">
        <link rel=icon href=/favicon.ico>
        <title>runtime-chunktitle>
        <link href=/js/about.cccc71df.js rel=prefetch>
        <link href=/css/app.b087a504.css rel=preload as=style>
        <link href=/js/app.9f1ba6f7.js rel=preload as=script>
        <link href=/css/app.b087a504.css rel=stylesheet>
    head>
    
    <body><noscript><strong>We're sorry but runtime-chunk doesn't work properly without JavaScript enabled. Please enable it
                to continue.strong>noscript>
        <div id=app>div>
        <script>(function (e) { function r(r) { for (var n, a, i = r[0], c = r[1], l = r[2], f = 0, s = []; f < i.length; f++)a = i[f], Object.prototype.hasOwnProperty.call(o, a) && o[a] && s.push(o[a][0]), o[a] = 0; for (n in c) Object.prototype.hasOwnProperty.call(c, n) && (e[n] = c[n]); p && p(r); while (s.length) s.shift()(); return u.push.apply(u, l || []), t() } function t() { for (var e, r = 0; r < u.length; r++) { for (var t = u[r], n = !0, a = 1; a < t.length; a++) { var c = t[a]; 0 !== o[c] && (n = !1) } n && (u.splice(r--, 1), e = i(i.s = t[0])) } return e } var n = {}, o = { "runtime~app": 0 }, u = []; function a(e) { return i.p + "js/" + ({ about: "about" }[e] || e) + "." + { about: "cccc71df" }[e] + ".js" } function i(r) { if (n[r]) return n[r].exports; var t = n[r] = { i: r, l: !1, exports: {} }; return e[r].call(t.exports, t, t.exports, i), t.l = !0, t.exports } i.e = function (e) { var r = [], t = o[e]; if (0 !== t) if (t) r.push(t[2]); else { var n = new Promise((function (r, n) { t = o[e] = [r, n] })); r.push(t[2] = n); var u, c = document.createElement("script"); c.charset = "utf-8", c.timeout = 120, i.nc && c.setAttribute("nonce", i.nc), c.src = a(e); var l = new Error; u = function (r) { c.onerror = c.onload = null, clearTimeout(f); var t = o[e]; if (0 !== t) { if (t) { var n = r && ("load" === r.type ? "missing" : r.type), u = r && r.target && r.target.src; l.message = "Loading chunk " + e + " failed.\n(" + n + ": " + u + ")", l.name = "ChunkLoadError", l.type = n, l.request = u, t[1](l) } o[e] = void 0 } }; var f = setTimeout((function () { u({ type: "timeout", target: c }) }), 12e4); c.onerror = c.onload = u, document.head.appendChild(c) } return Promise.all(r) }, i.m = e, i.c = n, i.d = function (e, r, t) { i.o(e, r) || Object.defineProperty(e, r, { enumerable: !0, get: t }) }, i.r = function (e) { "undefined" !== typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(e, "__esModule", { value: !0 }) }, i.t = function (e, r) { if (1 & r && (e = i(e)), 8 & r) return e; if (4 & r && "object" === typeof e && e && e.__esModule) return e; var t = Object.create(null); if (i.r(t), Object.defineProperty(t, "default", { enumerable: !0, value: e }), 2 & r && "string" != typeof e) for (var n in e) i.d(t, n, function (r) { return e[r] }.bind(null, n)); return t }, i.n = function (e) { var r = e && e.__esModule ? function () { return e["default"] } : function () { return e }; return i.d(r, "a", r), r }, i.o = function (e, r) { return Object.prototype.hasOwnProperty.call(e, r) }, i.p = "/", i.oe = function (e) { throw console.error(e), e }; var c = window["WebpackJsonp"] = window["WebpackJsonp"] || [], l = c.push.bind(c); c.push = r, c = c.slice(); for (var f = 0; f < c.length; f++)r(c[f]); var p = l; t() })([]);script>
        <script src=/js/chunk-vendors.1e5c55d3.js>script>
        <script src=/js/app.9f1ba6f7.js>script>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    index.html中已经没有对runtime~xxx.js的引用了,而是直接将其代码写入到了index.html中,故不会在请求文件,减少http请求。

    runtimeChunk作用是为了线上更新版本时,充分利用浏览器缓存,使用户感知的影响到最低。

    45.代码懒加载

    https://www.jianshu.com/p/6fc86fa8ee81

    模块懒加载本身与Webpack没有关系,Webpack可以让懒加载的模块代码打包到单独的文件中,实现真正的按需加载。Webpack会自动对异步代码进行分割。

    示例代码如下:

    function getComponent() {
        return import(/* WebpackChunkName: "lodash" */ 'lodash').then(({default: _})=>{
            var element = document.createElement('div')
            element.innerHTML = _.join(['a','b'],'-')
            return element
        })
    }
    
    document.addEventListener('click', ()=>{
        getComponent().then(element => {
            document.body.appendChild(element)
        })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    需要配置@babel/preset-env"useBuiltIns": "usage"

    {
       "presets": [
         ["@babel/preset-env",{
           "targets": {
              "chrome": "67"
            },
            "useBuiltIns": "usage",
            "corejs": "3"
         }
         ],
         "@babel/preset-react"
       ],
        "plugins": [
          "@babel/plugin-syntax-dynamic-import"
        ]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    执行打包指令,打包后的文件如下:

    生成了vendors~lodash.js文件。
    浏览器打开打包后的html文件,查看Network如下:

    点击后才会加载vendors~lodash.js

    12665637-7023b80413d6c86e

    实现了模块按需加载。

    异步函数的方式:

    async function getComponent(){
        const { default: _} = await import(/* WebpackChunkName: "lodash" */ 'lodash')
        const element = document.createElement('div')
        element.innerHTML = _.join(['a','b'],'-')
        return element
    }
    
    
    document.addEventListener('click', ()=>{
        getComponent().then(element => {
            document.body.appendChild(element)
        })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    46.prefetch 与 preload

    Webpack v4.6.0+ 增加了对预获取和预加载的支持。

    在声明 import 时,使用下面这些内置指令,可以让 Webpack 输出 “resource hint(资源提示)”,来告知浏览器:

    • prefetch(预获取):将来某些导航下可能需要的资源
    • preload(预加载):当前导航下可能需要资源

    下面这个 prefetch 的简单示例中,有一个 HomePage 组件,其内部渲染一个 LoginButton 组件,然后在点击后按需加载 LoginModal 组件。

    LoginButton.js

    //...
    import(/* WebpackPrefetch: true */ './path/to/LoginModal.js');
    
    • 1
    • 2

    这会生成 并追加到页面头部,指示着浏览器在闲置时间预取 login-modal-chunk.js 文件。

    Tips:只要父 chunk 完成加载,Webpack 就会添加 prefetch hint(预取提示)。

    与 prefetch 指令相比,preload 指令有许多不同之处:

    • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
    • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
    • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
    • 浏览器支持程度不同。

    下面这个简单的 preload 示例中,有一个 Component,依赖于一个较大的 library,所以应该将其分离到一个独立的 chunk 中。

    我们假想这里的图表组件 ChartComponent 组件需要依赖一个体积巨大的 ChartingLibrary 库。它会在渲染时显示一个 LoadingIndicator(加载进度条) 组件,然后立即按需导入 ChartingLibrary

    ChartComponent.js

    //...
    import(/* WebpackPreload: true */ 'ChartingLibrary');
    
    • 1
    • 2

    在页面中使用 ChartComponent 时,在请求 ChartComponent.js 的同时,还会通过 请求 charting-library-chunk。假定 page-chunk 体积比 charting-library-chunk 更小,也更快地被加载完成,页面此时就会显示 LoadingIndicator(加载进度条) ,等到 charting-library-chunk 请求完成,LoadingIndicator 组件才消失。这将会使得加载时间能够更短一点,因为只进行单次往返,而不是两次往返。尤其是在高延迟环境下。

    Tips:不正确地使用 WebpackPreload 会有损性能,请谨慎使用。

    有时你需要自己控制预加载。例如,任何动态导入的预加载都可以通过异步脚本完成。这在流式服务器端渲染的情况下很有用。

    const lazyComp = () =>
      import('DynamicComponent').catch((error) => {
        // 在发生错误时做一些处理
        // 例如,我们可以在网络错误的情况下重试请求
      });
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果在 Webpack 开始加载该脚本之前脚本加载失败(如果该脚本不在页面上,Webpack 只是创建一个 script 标签来加载其代码),则该 catch 处理程序将不会启动,直到 chunkLoadTimeout 未通过。此行为可能是意料之外的。但这是可以解释的 - Webpack 不能抛出任何错误,因为 Webpack 不知道那个脚本失败了。Webpack 将在错误发生后立即将 onerror 处理脚本添加到 script 中。

    为了避免上述问题,你可以添加自己的 onerror 处理脚本,将会在错误发生时移除该 script。

    <script
      src="https://example.com/dist/dynamicComponent.js"
      async
      onerror="this.remove()"
    >script>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这种情况下,错误的 script 将被删除。Webpack 将创建自己的 script,并且任何错误都将被处理而没有任何超时。

    47.第三方扩展设置 CDN

    47.1 什么是CDN

    传送门

    cdn全称是内容分发网络。其目的是让用户能够更快速的得到请求的数据。简单来讲,cdn就是用来加速的,他能让用户就近访问数据,这样就更更快的获取到需要的数据。举个例子,现在服务器在北京,深圳的用户想要获取服务器上的数据就需要跨越一个很远的距离,这显然就比北京的用户访问北京的服务器速度要慢。但是现在我们在深圳建立一个cdn服务器,上面缓存住一些数据,深圳用户访问时先访问这个cdn服务器,如果服务器上有用户请求的数据就可以直接返回,这样速度就大大的提升了。

    cdn的整个工作过程

    47.2 如何设置CDN

    Webpack中我们引入一个不想被打包的第三方包,可能是由于该包的体积过大或者其他原因,这对与Webpack打包来说是有优势的,因为减少第三包的打包会提高Webpack打包的速度。比如在实际使用中,我们使用到了lodash第三方包,我们又没有自己的CDN服务器,这是就需要借助别人的CDN服务器进行对该包的引入(一般是官方的CDN服务)。

    47.2.1 有自己的CDN服务器

    如果有自己的CDN服务器,我们可以在Webpack配置文件中的output中设置publicPath目录,其中写入CDN的服务器地址,如下:

    这样在打包过后,打开index.htm可以看到,我们对所有的资源都会从该CDN服务器下查找。

    47.2.2 使用第三方资源官方的CDN服务

    在Webpack官网中,可以看到,设置externals属性,可以选择我们不需要打包的第三方资源,具体配置如下:

    防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)

    例如,从 CDN 引入 jQuery,而不是把它打包:

    index.html

    <script
      src="https://code.jquery.com/jquery-3.1.0.js"
      integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
      crossorigin="anonymous"
    >script>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Webpack.config.js

    module.exports = {
      //...
      externals: {
        jquery: 'jQuery',
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这样就剥离了那些不需要改动的依赖模块,换句话,下面展示的代码还可以正常运行:

    import $ from 'jquery';
    
    $('.my-element').animate(/* ... */);
    
    • 1
    • 2
    • 3

    上面 Webpack.config.jsexternals 下指定的属性名称 jquery 表示 import $ from 'jquery' 中的模块 jquery 应该从打包产物中排除。 为了替换这个模块,jQuery 值将用于检索全局 jQuery 变量,因为默认的外部库类型是 var,请参阅 externalsType

    虽然我们在上面展示了一个使用外部全局变量的示例,但实际上可以以以下任何形式使用外部变量:全局变量、CommonJS、AMD、ES2015 模块,在 externalsType 中查看更多信息。

    48.打包 Dll 库

    在Webpack4往后或者Webpack5,本身其打包的速度已经足够优化,因此在高版本Vue脚手架、React脚手架中已经移除了DLL库的使用。 但是从打包内容的多少以及打包的速度上来讲,如果使用了DLL库,它的确可以提高构建速度。

    48.1 DLL库是什么

    DllPluginDllReferencePlugin 用某种方法实现了拆分 bundles,同时还大幅度提升了构建的速度。“DLL” 一词代表微软最初引入的动态链接库(有一些东西可以进行共享,共享的东西可以提前准备好,将其变为一个库。将来在不同的项目中,对其进行使用的时候,只需要将该库导入即可)。

    48.2 打包DLL库

    这里已React和React Dom为例。

    项目目录以及package.json

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nqA1uxSP-1669024724151)(http://5coder.cn/img/1668918984_0dcd78e5d8d2ae6a4a558f111765fa19.png)]

    Webpack.config.js

    const path = require('path')
    const Webpack = require('Webpack')
    const TerserPlugin = require('terser-Webpack-plugin')
    
    module.exports = {
      mode: "production",
      entry: {
        react: ['react', 'react-dom']
      },
      output: {
        path: path.resolve(__dirname, 'dll'),
        filename: 'dll_[name].js',
        library: 'dll_[name]'
      },
      optimization: {
        minimizer: [
          new TerserPlugin({
            extractComments: false
          }),
        ],
      },
      plugins: [
        new Webpack.DllPlugin({
          name: 'dll_[name]',
          path: path.resolve(__dirname, './dll/[name].manifest.json')
        })
      ]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    执行yarn dll后,发现dll目录中生成两个文件dll_react.js和react.manifest.json。其中在其他项目中使用该dll库时,会先引入react.manifest.json文件,根据其中的引用路径,再对应找到js文件进行打包。

    react.manifest.json

    {
      "name": "dll_react",
      "content": {
        "./node_modules/react/index.js": {
          "id": 294,
          "buildMeta": {
            "exportsType": "dynamic",
            "defaultObject": "redirect"
          },
          "exports": [
            "Children",
            "Component",
            "Fragment",
            "Profiler",
            "PureComponent",
            "StrictMode",
            "Suspense",
            "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED",
            "cloneElement",
            "createContext",
            "createElement",
            "createFactory",
            "createRef",
            "forwardRef",
            "isValidElement",
            "lazy",
            "memo",
            "useCallback",
            "useContext",
            "useDebugValue",
            "useEffect",
            "useImperativeHandle",
            "useLayoutEffect",
            "useMemo",
            "useReducer",
            "useRef",
            "useState",
            "version"
          ]
        },
        "./node_modules/react-dom/index.js": {
          "id": 935,
          "buildMeta": {
            "exportsType": "dynamic",
            "defaultObject": "redirect"
          },
          "exports": [
            "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED",
            "createPortal",
            "findDOMNode",
            "flushSync",
            "hydrate",
            "render",
            "unmountComponentAtNode",
            "unstable_batchedUpdates",
            "unstable_createPortal",
            "unstable_renderSubtreeIntoContainer",
            "version"
          ]
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    49.使用 Dll 库

    目录结构

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JMSUhuNy-1669024724153)(http://5coder.cn/img/1668920895_3af3c0f0ea9e322dcd02a0c0a7fa20b3.png)]

    Webpack.config.js

    const resolveApp = require('./paths')
    const HtmlWebpackPlugin = require('html-Webpack-plugin')
    const { merge } = require('Webpack-merge')
    const TerserPlugin = require('terser-Webpack-plugin')
    const Webpack = require('Webpack')
    const AddAssetHtmlPlugin = require('add-asset-html-Webpack-plugin')
    
    // 导入其它的配置
    const prodConfig = require('./Webpack.prod')
    const devConfig = require('./Webpack.dev')
    
    // 定义对象保存 base 配置信息
    const commonConfig = {
      entry: {
        index: './src/index.js'
      },
      optimization: {
        minimizer: [
          new TerserPlugin({
            extractComments: false
          })
        ],
        runtimeChunk: false,
        splitChunks: {
          chunks: 'all',
          minSize: 20000,
          maxSize: 20000,
          minChunks: 1,
          cacheGroups: {
            reactVendors: {
              test: /[\\/]node_modules[\\/]/,
              filename: 'js/[name].vendor.js'
            }
          }
        }
      },
      resolve: {
        extensions: ['.js', '.json', '.wasm', '.jsx', '.ts', '.vue'],
        alias: {
          '@': resolveApp('./src')
        }
      },
      output: {
        filename: 'js/[name].[contenthash:8]._bundle.js',
        path: resolveApp('./dist'),
    
      },
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              'style-loader',
              {
                loader: 'css-loader',
                options: {
                  importLoaders: 1,
                  esModule: false
                }
              },
              'postcss-loader'
            ]
          },
          {
            test: /\.less$/,
            use: [
              'style-loader',
              'css-loader',
              'postcss-loader',
              'less-loader'
            ]
          },
          {
            test: /\.(png|svg|gif|jpe?g)$/,
            type: 'asset',
            generator: {
              filename: "img/[name].[hash:4][ext]"
            },
            parser: {
              dataUrlCondition: {
                maxSize: 30 * 1024
              }
            }
          },
          {
            test: /\.(ttf|woff2?)$/,
            type: 'asset/resource',
            generator: {
              filename: 'font/[name].[hash:3][ext]'
            }
          },
          {
            test: /\.jsx?$/,
            use: ['babel-loader']
          }
        ]
      },
      plugins: [
        new HtmlWebpackPlugin({
          title: 'copyWebpackPlugin',
          template: './public/index.html'
        }),
        new Webpack.DllReferencePlugin({
          context: resolveApp('./'),
          manifest: resolveApp('./dll/react.manifest.json')
        }),
        new AddAssetHtmlPlugin({
          outputPath: 'js',
          filepath: resolveApp('./dll/dll_react.js')
        })
      ]
    }
    
    module.exports = (env) => {
      const isProduction = env.production
    
      process.env.NODE_ENV = isProduction ? 'production' : 'development'
    
      // 依据当前的打包模式来合并配置
      const config = isProduction ? prodConfig : devConfig
    
      const mergeConfig = merge(commonConfig, config)
    
      return mergeConfig
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125

    打包后的index.html

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kk36w3rU-1669024724157)(http://5coder.cn/img/1668921259_41811dba52c9da64fdd5486cc1d94224.png)]

    50.CSS 抽离和压缩

    CSS抽离和压缩

    Webpack中,如果正常在js中引入css文件样式,在Webpack打包时会将改css文件也打包进入jsbundle中。我们希望在js中引入的css样式文件单独抽离出来并且打包和压缩,这里需要使用Webpack提供的MiniCssExtractPlugin来实现。

    代码目录

    由于将css单独抽离打包的需求在开发阶段并不需要,且不适合,所以需要区分环境进行使用。在webpack.common.js和webpack。prod.js中分别进行单独配置。

    const resolveApp = require('./paths')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const { merge } = require('webpack-merge')
    const TerserPlugin = require("terser-webpack-plugin");
    const MiniCssExtractPlugin = require("mini-css-extract-plugin")
    
    // 导入其它的配置
    const prodConfig = require('./webpack.prod')
    const devConfig = require('./webpack.dev')
    
    // 定义对象保存 base 配置信息
    const commonConfig = (isProduction) => {
      return {
        entry: {
          index: './src/index.js'
        },
        resolve: {
          extensions: [".js", ".json", '.ts', '.jsx', '.vue'],
          alias: {
            '@': resolveApp('./src')
          }
        },
        output: {
          filename: 'js/[name].[contenthash:8].bundle.js',
          path: resolveApp('./dist'),
        },
        optimization: {
          runtimeChunk: true,
          minimizer: [
            new TerserPlugin({
              extractComments: false,
            }),
          ]
        },
        module: {
          rules: [
            {
              test: /\.css$/,
              use: [
                isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
                {
                  loader: 'css-loader',
                  options: {
                    importLoaders: 1,
                    esModule: false
                  }
                },
                'postcss-loader'
              ]
            },
            {
              test: /\.less$/,
              use: [
                'style-loader',
                'css-loader',
                'postcss-loader',
                'less-loader'
              ]
            },
            {
              test: /\.(png|svg|gif|jpe?g)$/,
              type: 'asset',
              generator: {
                filename: "img/[name].[hash:4][ext]"
              },
              parser: {
                dataUrlCondition: {
                  maxSize: 30 * 1024
                }
              }
            },
            {
              test: /\.(ttf|woff2?)$/,
              type: 'asset/resource',
              generator: {
                filename: 'font/[name].[hash:3][ext]'
              }
            },
            {
              test: /\.jsx?$/,
              use: ['babel-loader']
            }
          ]
        },
        plugins: [
          new HtmlWebpackPlugin({
            title: 'copyWebpackPlugin',
            template: './public/index.html'
          })
        ]
      }
    }
    
    module.exports = (env) => {
      const isProduction = env.production
    
      process.env.NODE_ENV = isProduction ? 'production' : 'development'
    
      // 依据当前的打包模式来合并配置
      const config = isProduction ? prodConfig : devConfig
    
      const mergeConfig = merge(commonConfig(isProduction), config)
    
      return mergeConfig
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105

    在webpack.common.js中将配置文件作为function的导出数据使用,可以使用传参的方式来判断当前的环境(生产或者开发)。

    在webpack.prodd.js中

    const CopyWebpackPlugin = require('copy-webpack-plugin')
    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    const MiniCssExtractPlugin = require("mini-css-extract-plugin")
    const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")
    
    module.exports = {
      mode: 'production',
      optimization: {
        minimizer: [
          new CssMinimizerPlugin()
        ]
      },
      plugins: [
        new CleanWebpackPlugin(),
        new CopyWebpackPlugin({
          patterns: [
            {
              from: 'public',
              globOptions: {
                ignore: ['**/index.html']
              }
            }
          ]
        }),
        new MiniCssExtractPlugin({
          filename: 'css/[name].[hash:8].css'
        })
      ]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mmewl0lZ-1669024724160)(http://5coder.cn/img/1668934227_b7c50ebe030466eb472723cd56921e9f.png)]

    执行yarn build打包后,发现在dist目录中单独抽离出来了css目录及文件。并且在使用yarn serve开发环境时,样式文件也可以正常加载。

    并且使用新的插件css-minimizer-webpack-plugin对css文件进行压缩。

    [MiniCssExtractPlugin官方文档](

  • 相关阅读:
    Talk | 纽约州立宾汉姆顿大学博士生丁琰:开放环境中机器人的任务与动作规划
    Java代码基础算法练习---2024.3.14
    4.2 实现基于栈的表达式求值计算器(难度4/10)
    数据结构与算法之LeetCode-513. 找树左下角的值 - 力扣(DFS,BFS)
    何为驱动(详解)
    Pix4Dmapper空间三维模型的应用实例:GIS选址分析
    前端和后端是Web开发中的两个不同的领域,你更倾向于哪一种?
    Environment Modules工具
    计算机网络 第二节
    IDEA 2022.2.1 Beta 2发布:新增支持Java 18、增强JUnit 5的支持
  • 原文地址:https://blog.csdn.net/weixin_42122355/article/details/127969669