Webpack4.x 学习笔记(下)

date
Mar 10, 2020
slug
webpack4.x-note-2
status
Published
tags
Webpack
summary
webpack深入学习笔记,内含 tree shaking、caching、shimming、code splitting等
type
Post

浏览器 Caching

当我们在打包资源的时候资源名以文件名命名,修改资源,接着普通刷新资源是不会改变的(其实是浏览器将上一次的资源缓存了)。解决方法就是在资源命名处加上 [contenthash] 来解决这个问题。先来看几个概念:
  1. runtime
    1. 在浏览器运行过程中,webpack 用来连接模块化应用程序所需的所有代码。它包含:在模块交互时,连接模块所需的加载和解析逻辑。包括:已经加载到浏览器中的连接模块逻辑,以及尚未加载模块的延迟加载逻辑。在未使用 optimization: { runtimeChunk: { name: 'runtime' } } 时,runtime 被存放在入口的 main.js 中,只有使用后才会被单独打包出来。
  1. manifest
    1. 存在于 runtime 中,当 webpack compiler 开始执行、解析和映射应用程序时,manifest 会保留形如 index.html 文件、各个 bundle 和各种资源模块的详细要点,这个数据集合称为 "manifest",当完成打包并发送到浏览器时,runtime 会通过 manifest 来解析和加载模块。无论你选择哪种 模块语法,那些 import 或 require 语句现在都已经转换为 __webpack_require__ 方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够检索这些标识符,找出每个标识符背后对应的模块。
      在稍低版本的 webpack 中加上了 contenthash 即使内容没变化,contenthash 也会改变。那这又是为什么呢?原因是因为在 webpack 中每个资源都是 chunk,每个 chunk 都会有对应的 chunk id(模块标识符 module identifier),chunk id 的顺序是排列好的,新创建一个 js 资源然后在 index.js 中引用都会引起 chunk id 的顺序变化,顺序变化了就会导致 manifest 文件的变化,又因为 runtime 包含 manifest,所以 runtime 变化。 具体事例请参考: https://webpack.docschina.org/guides/caching 只需要将 runtime 提出即可解决该问题:optimization: { runtimeChunk: { name: 'runtime' } }

Tree Shaking

描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如 import 和 export。 这里注意只支持 es module 的语法,commonjs 不支持。具体原因请参考:深入 CommonJs 与 ES6 Module
// webpack.config.js
module.exports = {
    mode: 'development',
    devtools: 'cheap-module-eval-source-map',
    // ...
    optimization: {
        usedExports: true
    }
}

// package.json
{
    // ...
    sideEffects: false // 这里指全部文件都要进行 tree-shaking
    // sideEffects: ["@babel/polyfill", "*.css"] // 指除了 polyfill css文件其他都进行 tree-shaking
}
开发环境中即使配置了 optimization 但是在打包后的 js 中代码还是保持原状,因为在开发环境中我们需要经常调试,如果直接使用 tree-shaking 会导致一些难以追踪的问题,所以 webpack 中在生产环境即使不配置 optimization 也会自动开启 tree-shaking,默认的 sideEffects: false , 如下:
// webpack.config.js
module.exports = {
    mode: 'production',
    devtools: 'cheap-module-source-map',
    // ...
}

// package.json
{
    // ...
    sideEffects: false // 这里指全部文件都要进行 tree-shaking
}

Code Splitting

代码分割这个概念很早就出现了,只不过都是我们人为的拆分、分离代码。这并不是 webpack 独有的。
常用的代码分离方法有三种:
  • 入口起点:使用 entry 配置手动地分离代码。
  • 静态导入:使用 SplitChunksPlugin 去重和分离 chunk。
  • 动态导入:通过模块中的内联函数调用来分离代码。
配置多入口,会将入口处的文件依次放入 index.html 中。
通过在文件头部 import _ from 'lodash' 我们可以在 webpack 配置中添加 optimization: {splitChunks: { chunks: 'all' }} 即可。
异步代码(动态导入)文件无需配置 webpack 文件,webpack 会自动进行代码分离。

split chunks

当我们在 optimization: { splitChunks: {} } 没有配置任何参数的时候这里其实执行了 webpack 的默认配置,配置如下:(具体请参考 SplitChunksPlugin
// webpack 默认配置
module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
};
  1. chunks: all | async | initial , 当为 async 时只支持异步引入的资源打包,当为 all 时异步、同步资源均可被打包。需要注意的是当为 all 时 cacheGroups 必须配置,不设置其中的属性 filename 的话打包出来的资源名为 vendors~main.js 意思是在 vendors 组下,资源入口为 main.js 我们可以设置 filename: 'vendors.js'
  1. minSize 是指当资源大于该值才向下执行,小于则不执行
  1. maxSize 是指当资源大于该值时 webpack 尝试将资源用该值来分割为多份,一般不配置或为0即可
  1. minChunks 是指当一个资源(jquery lodash...)被用了至少几次后才分割
  1. maxAsyncRequests 是指同时加载资源的个数,假如这里设置5,当 webpack 分割第5块资源完毕后剩下的资源不会再分割
  1. maxInitialRequests 指网站首页(入口文件)最多分割模块数
  1. automaticNameDelimiter 指的是当没配置 filename 时打包出来的文件 `vendors~lodash.js` 中的~连接符
  1. name 指的是是否让 cacheGroups 中配置的 filename 生效
  1. cacheGroups 顾名思义是缓存组的意思,假设此时我同步导入
    1. import _ from 'lodash'
      import $ from 'jquery'
      如果没有这一项则会打包出来俩个 vendor 文件,有了该项后先打包 lodash 放入 vendros 组中,在打包 jquery 发现满足配置,再放入 vendors 中,生成一个 vendors 文件。那么 default 是干什么的呢?
      import _ from 'lodash'
      import $ from 'jquery'
      import Header from './Header.js'
      lodash 与 jquery 均放入 vendors 组中,而 Header 并不在 node_modules 中所以放入 default 组中。如果此时没有 default 选项那么 Header 并不会被分割出来。 这里需要注意的是 vendors 中的 test 并不是只能匹配 node_modules,default 同理。
  1. priority 指打包优先级,在上面的默认配置代码中 default 并没有配置 test,所以此时如果我们引入 import _ from 'lodash' 是既满足 vendors 组又满足 default 组,那么 lodash 该去哪呢?此时比较 priority 的大小,放入数值大的组中。
  1. reuseExistingChunk 指的是如果出现资源互相的情况那么如果被引用资源已经被打包过就直接使用,避免重复打包

提示

  1. 在进行代码分割后打包文件会出现 `vendors~lodash.js` 这样的 vendor 前缀,如果想要去掉的话需要配置 `vendors: false, default: false`
  1. ⚠️ 处理同步引入资源打包(即 chunks 为 all 或者 innital)时不配置 cacheGroups 是不会将符合 cacheGroups 之前条件的资源分割打包出来

lazy loadinng

先看俩段代码:
import $ from 'jquery'

const el = $('<div>i7eo</div>')
$('body').append(el)
const getJquery = () => import(/* webpackChunkName: "jquery" */ 'jquery').then(({ default: $ }) => {
    return $
})

document.addEventListener('click', () => {
    getJquery().then($ => {
        const el = $('<div>i7eo</div>')
        $('body').append(el)
    })
})
第一段代码是同步执行,而第二段我们使用了异步加载的方式,虽然打包的时候 jquery 也被打包了出来但是进入页面第一时间并不会加载,只有当单击页面时才会加载,这就是异步加载(lazy loading)的好处。三大框架中动态路由也是运用了这样的技巧。第二段代码可以使用 async await 改写具体如下:
async function getJquery() {
    const { default: $ } = await import(/* webpackChunkName: "jquery" */ 'jquery')
    return $
}

document.addEventListener('click', () => {
    getJquery().then($ => {
        const el = $('<div>i7eo</div>')
        $('body').append(el)
    })
})

pre-fetch/pre-loading

除了利用缓存来提高页面加载速度,那有什么办法在代码层面还提高呢?我们可以在 chrome 查看 coverage(commande+shift+p),观察代码使用率的情况从而找出未使用的代码做成异步加载来实现。

问题

思考这样一个场景,有一个管理后台,右上角有一个登陆按钮,单击后出现一个弹窗。
这个时候我们可以把单击事件中的弹窗逻辑使用 import 改为异步代码,首次进入页面不加载,单击时再加载。但是如果逻辑较多或网络不稳定就会出现单击后弹窗延迟出现的可能。那么这个时候我们就可以使用 pre-fetch/pre-loading 来解决这个问题。实现方式也很简单,在 import(/* webpackPrefetch:true */ './Popup.js') 改写魔法注释就可以实现。 pre-fetch/pre-loading 的区别在于 fetch 是在页面加载完毕后加载,loading 是与页面的主逻辑资源一同加载。(注意浏览器对这二者的兼容性)

shimming

根据官网的解释来看 shimming 是依赖预置的意思。举个例子:
试想现在有一个老项目,里面有上千个页面,每个页面都得用到 juqery,现在我们要对该项目利用 webpack 改造,那么 juqery 的依赖应该怎么处理?
方法1: 在入口文件处将 jquery 挂载到 window 上。`window.$ = $`
方法2: 配置 shimming,具体如下:
module.exports = {
    // ...
    plugins: [
        new webpack.ProvidePlugin({
            $: 'jquery'
        })
    ]
}
官方推荐的方式是方法后2,具体可参考 https://webpack.docschina.org/guides/shimming/
方法2中对 webpack 配置后其实也是在需要使用 $ 的文件头部自动注入 import $ from 'jquery'。配合 split code 使用效果更佳

问题

在 es module 文件中 this 是指向当前文件(module)的,如何使 this 指向 window ?
  1. 安装 npm i imports-loader -D
  1. 配置 loader
    1. module.exports = {
          // ...
          module: {
              rules: [
                  {
                      test: /\.js$/,
                      exclude: /node_modules/,
                      use: [
                          {loader: 'babel-loader'},
                          {loader: 'imports-loader?this=>window'}
                      ],
                      // loader: 'babel-loader'
                  }
              ]
          }
      }

提示

  1. import 中注释的写法称作 magic comment,具体请参考:
    1. 这里想使注释生效的话请安装 @babel/plugin-syntax-dynamic-import
  1. chunk 就是指的就是 dist 下的 js 资源
  1. 前端性能的提升从缓存角度来看提升是很有限的,着重还是要从代码角度来考虑。提一个点,对资源缓存只是针对非首次访问提速,那么第一次放访问应该怎么办?还是要从代码的角度多考虑

© i7eo 2017 - 2022