webpack 学习

254 阅读7分钟

Compiler 对象包含了当前运行Webpack的配置,包括entry、output、loaders等配置,这个对象在启动Webpack时被实例化,而且是全局唯一的。

Compilation对象代表了一次资源版本构建

一、webpack怎么拆包

webpack 会根据模块依赖图的内容组织分包 —— Chunk 对象,默认的分包规则有:
  • 同一个 entry 下触达到的模块组织成一个 chunk
    
  • 异步模块单独组织为一个 chunk
    
  • entry.runtime 单独组织成一个 chunk
    

    默认规则集中在 compilation.seal 函数实现,seal 核心逻辑运行结束后会生成一系列的 Chunk、ChunkGroup、ChunkGraph 对象,后续如 SplitChunksPlugin 插件会在 Chunk 系列对象上做进一步的拆解、优化,最终反映到输出上才会表现出复杂的分包结果。

    拆包的方式:

  1. entry设置多入口文件

重点:seal 阶段遍历 entry 对象,为每一个 entry 单独生成 chunk,之后再根据模块依赖图将 entry 触达到的所有模块打包进 chunk 中。

初始化完毕后,Webpack 会读取 ModuleDependencyGraph 的内容,将 entry 所对应的内容塞入对应的 chunk (发生在 webpack/lib/buildChunkGrap.js 文件);

a842770d983f4d42a265cf859a63e3ac_tplv-k3u1fbpfcp-zoom-1.png

Main.js 以同步方式直接或间接引用了 a/b/c/d 四个文件,分析 ModuleDependencyGraph 过程会逐步将 a/b/c/d 模块逐步添加到 chunk[main] 中,

  1. 按需加载

重点:分析 ModuleDependencyGraph 时,每次遇到异步模块都会为之创建单独的 Chunk 对象,单独打包异步模块。

Webpack 4 之后,只需要用异步语句 require.ensure("./xx.js") 或 import("./xx.js") 方式引入模块,就可以实现模块的动态加载,这种能力本质也是基于 Chunk 实现的。

  1. runtime分包

重点: Webpack 5 之后还能根据 entry.runtime 配置单独打包运行时代码。

webpack 5之后还支持基于 runtime 的分包规则

编译时,Webpack 会根据业务代码决定输出那些支撑特性的运行时代码(基于 Dependency 子类),例如:

  • 需要 __webpack_require__.f、__webpack_require__.r 等功能实现最起码的模块化支持
    
  • 如果用到动态加载特性,则需要写入 __webpack_require__.e 函数
    
  • 如果用到 Module Federation 特性,则需要写入 __webpack_require__.o 函数
    
  • 等等
    
module.exports = {
  entry: {
    index: { import: "./src/index", runtime: "solid-runtime" },
  }
};

Webpack 执行完 entry、异步模块分包后,开始遍历 entry 配置判断是否带有 runtime 属性,如果有则创建以 runtime 值为名的 Chunk,因此,上例配置将生成两个chunk:chunk[index.js] 、chunk[solid-runtime],

  1. Bundle splitting: 为了更好的缓存,可以将一个大文件分割成更多,更小的文件。

    通过设置optimization中的splitchunks来实现

    module.exports = {
          entry: path.resolve(__dirname, 'src/index.js'),
          output: {
            path: path.resolve(__dirname, 'dist'),
            filename: '[name].[contenthash].js'
          },
          optimization: {
            splitChunks: {
              chunks: 'all',
            }
          }
    };
    

    增加的optimization.splitChunks.chunks = 'all',会把引用的第三方模块(node_modules)全部打包到vendor.js。

    vendors.js同样会遇到与main.js文件相同的问题,对其一部分进行更改意味着重新下载整个vendor.js。

    const path = require('path');
    const webpack = require('webpack');
    
    module.exports = {
      entry: path.resolve(__dirname, 'src/index.js'),
      plugins: [
        new webpack.HashedModuleIdsPlugin(), // so that file hashes don't change unexpectedly
      ],
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[contenthash].js',
      },
      optimization: {
        runtimeChunk: 'single',
        splitChunks: {
          chunks: 'all',
          maxInitialRequests: Infinity,
          minSize: 0,
          cacheGroups: {
            vendor: {
              test: /[\\/]node_modules[\\/]/,
              name(module) {
                //获取每个npm包的名称
                const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
    
               //对npm的包名子添加前缀,并去掉@
                return `npm.${packageName.replace('@', '')}`;
              }
            }
          }
        }
      }
    }
    

    一个vue-cli生成的项目的打包结果将会是:

      dist/npm.vue.44c71c1a.js           
      dist/npm.vue-router.0290a1da.js   
      dist/npm.core-js.028dc51e.js       
      dist/npm.vuex.6946f3d5.js          
      dist/app.e76cff0a.js               
      dist/runtime.4e174d8a.js          
      dist/npm.vue-loader.611518c6.js    
      dist/about.16c4e81c.js             
      dist/npm.webpack.034f3d3d.js      
      dist/css/app.ab586865.css 
    

    下面重点介绍一下cacheGroups

    cacheGroups是splitChunks里面最核心的配置。splitChunks根据cacheGroups拆分模块,之前说的chunks以及其他属性都是对缓存组进行配置的。splitChunks默认有两个缓存组,vendor-加载内容来源node_modules,另一个是default。

二、webpack的动态加载原理

v2-e79216a9f4cbf7e5acda474312c7efcf_r.jpeg webpack 按需加载.png

感谢: https://zhuanlan.zhihu.com/p/159216534 https://juejin.cn/post/6952703369135800350#heading-1

三、webpack的tree-shaking

理解:tree-shaking指的是消除没被引用的模块代码,减少代码体积大小,以提高页面的性能。

原理: ES Module 最大的特点就是静态化,在编译时就能确定模块的依赖关系,以及输入和输出的值,这意味着什么?意味着模块的依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,正是基于这个基础,才使得 Tree-Shaking 成为可能

条件

  • 模块系统必须为ESmodule。
    

    原因:

    ESModule是静态的,依赖关系在编译时就确定了。而commonjs是node环境默认的模块系统,是动态的。而webpack只会在编译时做处理,所以只有es模块才行

    但是,在实际项目中,在进入webpack优化前,我们一般会用一系列loader对源码进行处理,其中包括神一样的babel-loader。我们按照以下的babel配置: 同样是上面的源码,webpack打包后,我们可以看到tree-shaking又失效了

    问题是preset为env时,模块转换默认为cjs。我们把babel配置为以下即可:

{
  "presets": [
    [ "env",{modules:false}]
  ]
}

对于ts项目,一般我们都会使用ts-loader来处理ts文件,对于ts,同样,我们需要在tsconfig.json中的module设置为'esnext'或者'es2015':

{
 "compilerOptions": {
    module:'esnext'
    }
}
  • package.json文件标识sideEffect字段。
    

    webpack对有副作用的模块,都不会进行tree-shaking,甚至是代码混淆! 。

    那么在webpack中,还有哪些模块会被认为是有副作用的呢?

    1. 含有eval也被认为是不纯的

    2. 在顶级作用域中对用一个属性赋值两次

  • 把webpack设置为生产环境
    

    生产环境下,webpack默认开启了tree-shaking.有关tree-shaking的配置如下:

    optimization: {
            providedExports: false,//会在webpack编译过程中标识模块的哪些方法是被导出的
            usedExports: true,//会在webpack编译过程中标识模块的哪些方法是被导出的
            sideEffects: true,//允许在package.json中标志没有作用的文件
            minimizer: [
                new TerserPlugin({ //把无用的代码remove掉。
                    cache: true,
                    parallel: true
                })
            ]
        },
    
  • 存在的问题:
    

    1、tree-shaking对第三方库不管用

    2、无法shake掉class中没被引用的方法

感谢: https://juejin.cn/post/6956522989810614308 https://juejin.cn/post/6955383260759195678

四、webpack的性能优化

五、怎么写loader

六、怎么写plugin

https://juejin.cn/post/6955421936373465118#heading-31

七、webpack5 VS webpack4

八、webpack的热替换原理

https://juejin.cn/post/6973825927708934174

九、webpack的打包原理

c8afb702663549b0a0e8d1657b6affb8_tplv-k3u1fbpfcp-watermark.png

感谢:

https://juejin.cn/post/6960924869307400200#heading-10

十、webpack的模块化原理

  • 通过一个webpack_module对象来存储模块化代码
1. key为文件名称
2. value为文件代码
  • 通过webpack_module_cache来缓存模块化代码
  • 通过webpack_require来从webpack_module_cache或webpack_module中读取并从引入代码

十一、source-map的原理

定义:保存源代码映射关系的文件

source Map 文件大概长:

{
  "version": 3,
  "sources": [//转换前的文件
    "webpack://webpack5-template/./src/index.js"
  ],
  "names": [],
  "mappings": ";;;;;AAAA;AACA;AACA;AACA;AACA,eAAe;AACf;AACA;AACA,mB",//记录位置信息的字符串。这个的话,用到了 VLQ 编码相关
  "file": "main.bundle.js",
  "sourcesContent": [
    "console.log('Interesting!!!')\n// Create heading node\nconst heading = document.createElement('h1')\nheading.textContent = 'Interesting!'\nconsole.log(a); // 这一行会报错\n// Append heading node to the DOM\nconst app = document.querySelector('#root')\napp.append(heading)"
  ],
  "sourceRoot": ""
}
重要的字段:version,sources,mappings
  • source map。产生 .map 文件(配合 eval 或者 inline 使用的时候,会不生成 source map 文件,具体要看哪个模式)
  • eval。使用 eval 包裹块代码
  • cheap。不生成列信息
  • inline。将 .map 作为 DataURI 嵌入,不单独生成一个 .map 文件
  • module。包含 loader 的 source map

对于开发环境,eval, eval-source-map, eval-cheap-source-map, eval-cheap-module-source-map 等都是可以的。个人推荐使用 eval-cheap-module-source-map

开发环境:

将你的服务器配置为,不允许普通用户访问 source map 文件!你不应将 source map 文件部署到 web 服务器。而是只将其用于错误报告工具。

大致流程:

  • webpack 构建的时候将原始 JS 和 source map 文件都上传到我们的监控平台
  • js 错误堆栈收集,通过 window.onerror 来捕获 js 报错,然后上报到服务器,以用来收集用户使用时候发生的 bug
  • 解析 JS 错误,映射源文件堆栈
  • 通过 sourcemap 查找原始报错信息,可以使用source-map
  • 监控平台展示

十一、webpack5(Module Federation)实现微前端

十二、webpack打包的babel处理

感谢: https://juejin.cn/post/6968027732504477726

非常感谢一下链接的作者,本文单纯为了自身学习总结。参考链接:

https://juejin.cn/post/6964642182183518215

https://juejin.cn/post/6961724298243342344#heading-2