webpack面试解析

参考:
上一篇webpack相关的系列:webpack深入学习,搭建和优化react项目
爪哇教育@字节面试官解析webpack-路白

1、Webpack中的module是什么?

通常来讲,一个 module 模块就是指一个文件中导出的内容,webpack 会将这些 module 打包成一个或多个 chunk 。

在webpack中module就是一个个打包模块,它通过模块之间的依赖关系构建出打包依赖路径,最终构建出一个bundle。

webpack支持ESModule、CommonJS、AMD、各种资源模块(image\Json\font等等)的打包。

  1. ESM
    import导入 & export导出,主要在浏览器环境中使用。
  2. commonJS
    主要在nodeJS中使用,module.exports暴露模块,require导入模块。
  3. AMD
    define定义模块 require导出模块
  4. css / less
    @import

2、chunk和bundle的区别是什么?

chunk是【webpack打包过程】中的Module集合,是打包过程中module集合的概念。

webpack打包是从一个入口模块开始,入口模块引用其他模块,然后其他的模块再引用更多模块,构成了一个引用关系集合,这些module就形成了一个chunk。

chunk会根据webpack的配置进行拆分,一般包含三种情况

  1. 项目入口(entry)。如果存在多个入口模块,可能会出现多条打包路径,每一条路径都会形成一个chunk。
  2. 通过import()动态引入的代码
  3. 通过splitChunks拆分出来的代码

Bundle是webpack最终输出的一个或者多个打包好的文件。

chunk是webpack的打包过程概念,bundle是打包完成后的结果,也就是说大部分的chunk在打包完成后,就成了bundle。

Chunk和Bundle的关系是什么?

大多数情况下,一个entry入口会生成一个chunk,一个bundle,但是也有例外,比如如果加入了devtool:sourcemap的配置,一个入口,一个chunk生成了两个Bundle。

Chunk和Bundle的关系探究 从一个空文件夹编写一个可使用webpack打包的项目。
  1. 空文件夹中npm init,在src文件夹下简单写几个文件。(先只写几个简单的js文件

  2. 安装webpack / webpack-cli

  3. 编写webpack配置。

    module.exports = {
    mode:'production', // webpack要求必填
    entry: {
    index: './src/index.js,
    }, // 打包入口
    output: {
    path: path.join(__dirname, 'dist'), // 打包输出的路径
    filename: '[name].js' , // 打包输出的bundle名称。
    }
    }

    // 上面的配置方式会输出一个chunk(index.js) / 对应一个Bundle(index.js).

但是如果在配置中多加一行:devtool: "source-map" // 生成代码调试可用的源码-打包后代码映射关系,打包出来的bundle就会多生成一个index.js.map。但是chunk还是只有一个,也就是说一个chunk对应了两个Bundle。

多入口的打包情况
  1. 入口为一个key对应一个入口文件数组,仍然只会输出一个chunk。

    module.exports = {
    // ...
    entry: {
    index: ['./src/index.js','./src/add.js'],
    }, // 打包入口
    }

    // 上面的配置方式会输出一个chunk(index.js) / 对应一个Bundle(index.js).

  2. 入口为多个key,一个入口对应一个chunk

    module.exports = {
    // ...
    entry: {
    index: './src/index.js',
    common: './src/common.js'
    },
    }

下载了一个lodash依赖,然后加入其他的优化配置 会出现几个chunk 、 bundle
module.exports = {
    mode:'production', // webpack要求必填
    entry: {
        index: './src/index.js, 
        other: './src/multiple.js'
    }, // 打包入口
    output: {
        path: path.join(__dirname, 'dist'), // 打包输出的路径
        filename: '[name].js' , // 打包输出的bundle名称。
    },
    optimization: { // 优化持久缓存的配置,比如动态引入导致的hashName变动 、 代码分包等。
        runtimeChunk: 'single', // runtime
        splitChunks: {
            cacheGroups: {
                commons: {
                    chunks: 'initial',
                    minChunks: 2,
                    minSize:0
                },
                vendor: {
                    test: /node_modules/,
                    chunks: 'initial',
                    name: 'vendor',
                    enforce: true
                }
            }
        }
    }
}

出现了5个包

  1. 两个entry
  2. runtimeChunk
    官网说明
    webpack之optimization.runtimeChunk作用
    runtimeChunk:single会生成一个在所有生成chunk之间运行的文件,文件中包含chunk映射关系清单等,如果没有设置runtime,这个映射关系清单信息就会放在入口文件打包出来的chunk内部,假如我们修改了一个模块,那这个模块的hash值变动后,所有相关的chunk hash值都会变动,导致缓存失效,配置了runtime后就只有runtime和被修改的模块的hash值才会变动,其他模块的hash值不变,浏览器缓存仍然存在。

目的:为了更好地利用浏览器缓存,不至于每一次打包都导致主包的hash值变动。

  1. SplitChunks common包和 SplitChunks vendor包
    commons包的内容:只要被两个chunk同时引用的公共代码,就提取到common里面。
    vendor包的内容:顾名思义,提取node_module的依赖到这个包里。
SplitChunks

官网说明
splitChunks探索
如何使用 splitChunks 精细控制代码分割
webpack splitChunks配置(一)chunks属性的使用

SplitChunks是 Webpack 中一个可以配置代码分割规则,提取或分离代码的插件,主要作用是提取公共代码,防止代码被重复打包,以及拆分过大的js文件,合并零散的js文件。

SplitChunks插件配置选项

意思是只要符合下面的条件就会强制分块。

// webpack4的默认配置(开箱即用)

splitChunks: {
    // 表示选择哪些 chunks 进行分割,可选值有:async,initial和all
    chunks: "async",
    // 表示新分离出的chunk必须大于等于minSize,默认为30000,约30kb。
    minSize: 30000,
    // 表示一个模块至少应被minChunks个chunk所包含才能分割。默认为1。
    minChunks: 1,
    // 表示按需加载文件时,并行请求的最大数目。默认为5。
    maxAsyncRequests: 5,
    // 表示加载入口文件时,并行请求的最大数目。默认为3。
    maxInitialRequests: 3,
    // 表示拆分出的chunk的名称连接符。默认为~。如chunk~vendors.js
    automaticNameDelimiter: '~',
    // 设置chunk的文件名。默认为true。当为true时,splitChunks基于chunk和cacheGroups的key自动命名。
    name: true,
    // cacheGroups 下可以可以配置多个组,每个组根据test设置条件,符合test条件的模块,就分配到该组。模块可以被多个组引用,但最终会根据priority来决定打包到哪个组中。默认将所有来自 node_modules目录的模块打包至vendors组,将两个以上的chunk所共享的模块打包至default组。
    cacheGroups: {
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10
        },
        // 
    default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true
        }
    }
}

// 分包条件
// 模块在代码中被复用或者来自 node_modules 文件夹
// 模块的体积大于等于30kb(压缩之前)
// 当按需加载 chunks 时,并行请求的最大数量不能超过5
// 页面初始加载时,并行请求的最大数量不能超过3
理解 chunks的三个选项
  • chunks: "initial"
    initial表示 同时打包异步同步,但是异步内部的引入将不再拆分,直接打包一起进一个文件中。也就是说每一次遇到一个异步加载的模块就相当于多开了一个入口,接下来只会根据这个入口内的依赖路径生成chunk。如果一个文件既被异步引入又被同步引入,它们将分别被打包到两个文件。
  • chunks: "async"
    async表示只拆分异步加载的模块,如果异步加载的模块内有同步加载的模块,则会将同步加载的模块并入main.js的入口文件里。
  • chunks: "all"
    all表示以上两者都包括,如果一个文件被同步引入又被异步引入,会被提取出来打包到一个文件,其他的文件引入的都是这个被打包出来的同一份文件。 也就是说chunk 可以在异步和非异步 chunk 之间共享。

Plugin和Loader分别是什么,如何工作

  1. Loader

    由于webpack 本身只能打包commonjs规范的js文件,对css,图片等格式的文件没法打包,因此引入了Loader来帮助打包,Loader可以看作是一个各种类型的模块翻译器,将非js模块转化为webpack能识别的js模块,比如css-loader会把css文件最后打包成css.js。

    loader运行在打包文件之前(loader为在模块加载时的预处理文件)

  2. Plugin

    可以完成各种loader不能完成的功能。plugin扩展了webpack的功能,webpack基于事件流框架 Tapable,它会在运行的各个阶段广播出事件,插件会监听对应的事件,在打包过程中对打包代码做相应处理。。从打包优化和压缩,到重新定义环境变量,还可以用来处理各种各样的任务。因此webpack可以做到针对各种情况灵活处理。

    Plugin在整个编译周期中都可以使用。

其他的webpack打包过程概念:webpack编译会创建的两个核心对象
  1. Compiler

    是一个包含了webpack环境的所有配置信息的对象,可以理解为webpack的实例,是全局唯一的,它内部包括options / loader / plugin的信息和 webpack 整个生命周期相关的钩子。

  2. Compliation

    包含了当前的模块资源和编译生成资源,webpack在开发模式下运行的时候,每当检测到一个文件变化,就会创建一次新的Compliation。

事件节点:

run:开始编译

make:从entry开始递归分析依赖并对依赖进行build

build-moodule:使用loader加载文件并build模块

normal-module-loader:对loader加载的文件用acorn编译,生成抽象语法树AST

program:开始对AST进行遍历,当遇到require时触发call require事件

seal:所有依赖build完成,开始对chunk进行优化(抽取公共模块、加hash等)

optimize-chunk-assets:压缩代码

emit:把各个chunk输出到结果文件

如何实现一个Plugin / loader

web前端高级webpack - webpack常见面试题及手写loader和plugin

实现loader

在编写 loader 前,我们首先需要了解 loader 的本质

其本质为函数,函数中的 this 作为上下文会被 webpack 填充,因此我们不能将 loader设为一个箭头函数

函数接受一个参数,为 webpack 传递给 loader 的文件源内容

函数中 this 是由 webpack 提供的对象,能够获取当前 loader 所需要的各种信息

函数中有异步操作或同步操作,异步操作通过 this.callback返回,返回值要求为 string 或者 Buffer

一般在编写loader的过程中,保持功能单一,避免做多种功能

代码如下所示:

// 导出一个函数,source为webpack传递给loader的文件源内容 
module.exports = function(source) { 
    const content = doSomeThing2JsString(source); 
     
    // 如果 loader 配置了 options 对象,那么this.query将指向 options 
    const options = this.query; 
     
    // 可以用作解析其他模块路径的上下文 
    console.log('this.context'); 
     
    /* 
     * this.callback 参数: 
     * error:Error | null,当 loader 出错时向外抛出一个 error 
     * content:String | Buffer,经过 loader 编译后需要导出的内容 
     * sourceMap:为方便调试生成的编译后内容的 source map 
     * ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,进而省去重复生成 AST 的过程 
     */ 
    this.callback(null, content); // 异步 
    return content; // 同步 
} 

实现一个能够删除源码中的console语句的Loader

const parser =  require('@babel/parser') ;// 可以把js源码转成 ast语法树
const traverse = require('@babel/traverse').default // 可以递归遍历 ast节点
const generator = require('@babel/generator').default; // 把修改好的ast语法树 再转成源码字符串
const types = require('@babel/types') // 操作节点的增删改

module.exports = function(source){
  const ast = parser.parse(source,{sourceType:'module'})
  // console.log(ast.program.body);
  traverse(ast,{
    CallExpression(path){
      // console.log(path)
      if(types.isMemberExpression(path.node.callee) && types.isIdentifier(path.node.callee.object,{name:'console'})){
        path.remove()
      }  
    }
  })
  const output = generator(ast,{},source);
  return output.code
}
实现plugin

如果自己要实现plugin,需要遵循一定的规范:

  1. 插件必须是一个函数或者是一个包含 apply 方法的对象,这样才能访问compiler实例

  2. 传给每个插件的 compiler 和 compilation 对象都是同一个引用,因此不建议修改

  3. 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住

    class MyPlugin {
    // Webpack 会调用 MyPlugin 实例的 apply 方法给插件实例传入 compiler 对象
    apply (compiler) {
    // 找到合适的事件钩子,实现自己的插件功能
    compiler.hooks.emit.tap('MyPlugin', compilation => {
    // compilation: 当前打包构建流程的上下文
    console.log(compilation);

         // do something...
     })
    

    }
    }

打包的时候生成一个显示当前项目文件列表的myfile.md说明文件。

class Myplugin{
	// 可以再打包的时候产生一个新的文件
	constructor(options){
	    // new 这个插件是传进来的参数
	   this.options = options
	}
	apply(compiler){
    const hooks = compiler.hooks;
    // 监听事件
	if(hooks){
	    hooks.emit.tap('myplugin',function(complication,callback){
	    var str = '文件列表是:\n';
	    for(let k in complication.assets){
	        str += `文件名:${k},,,大小是 ${ complication.assets[k].size()} \n\n`
	    }
	    complication.assets['myfile.md'] = {
	        source(){
	            return str
	        },
	        size(){
	           return str.length
	        }
	       }
	       callback&&callback()
	    })
	}else{
	    compiler.plugin('emit',function(complication,callback){
	        var str = '文件列表是:\n';
	        for(let k in complication.assets){
	          str += `文件名:${k} , 大小是 ${ complication.assets[k].size()} \n\n`
	        }
	        complication.assets['myfile.md'] = {
	          source(){
	            return str
	          },
	          size(){
	            return str.length
	          }
	        }
	        callback&&callback()
	    })
	}
	}
}
module.exports = Myplugin

简单描述一下webpack打包过程?

流程可以大致划分为 以下7个阶段

  1. 初始化参数:从配置文件webpack.config.js和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  3. 确定入口:根据配置中的 entry 找出所有的入口文件,一个入口就会形成一个依赖图路径,最后打成一个包。
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会,一般对应compiler的emit事件。
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

webpack热更新

Webpack HMR 原理解析

Webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。 HMR的核心就是客户端从webpack服务端拉取更新后的文件信息,准确的说是chunk 需要更新的部分的模块哈希值。

webpack-dev-server(WDS) 与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。

客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash值的JSON串),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。

后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理。像react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。

如何优化webpack打包?

在淘宝优化了一个大型项目,分享一些干货
「吐血整理」再来一打Webpack面试题

一般来说我们的优化有两个方向:

  1. 优化用户体验
    减少首屏加载时间
    提升各项交互的流畅度,如表单验证和页面切换
  2. 优化开发体验
    优化打包速度,减少构建耗时

在webpack中可以使用按需加载、异步加载分包、提取公共代码等方式去优化。

  1. 利用optimization.splitChunks分包。
    提取依赖包,把分包的正则属性配置路径为node_modules。
    提取重复模块,把重复的模块尽量提取到同一个chunk里,比如说设置一个common包的规则,一旦达到这个规则的公共代码就会被分出来一个包,一般来说设置chunks:'all'的属性即可。

为了保证首屏渲染时间,需要使用懒加载,也就是异步引入的方式优化,把首屏暂时用不到的包异步引入,这样分包的时候就会自动把这些文件提取到别的chunk里了。如果担心分的包太细碎会发起太多请求,可以使用MinSize和限制最高分包数等属性来限制。

  1. 开启 optimization.runtimeChunk 属性,将模块依赖关系单独拉出来一个分包,这样如果一个模块变动,与它相关的其他模块内容就不会跟着连锁变动了,名字hash值也不会变,就可以继续利用之前的缓存设置。

  2. 配置css和js文件的代码压缩:css-minimizer-webpack-plugin 和 webpack-parallel-uglify-plugin 插件。

  3. 可以通过多线程来提升Webpack打包速度的工具:HappyPack(不维护了)、thread-loader,一般用在babel-loader之类转译文件多、时间长的Loader上效果会比较明显。

  4. webpack5还提出了一个Module Federation(模块联邦)的属性配置,有利于加快应用启动和编译速度。 比如UMI的项目就可以直接在config里面配置MFSU之后,编译速度大大加快。

相关推荐
桃园码工6 分钟前
15_HTML5 表单属性 --[HTML5 API 学习之旅]
前端·html5·表单属性
百万蹄蹄向前冲1 小时前
2024不一样的VUE3期末考查
前端·javascript·程序员
轻口味1 小时前
【每日学点鸿蒙知识】AVCodec、SmartPerf工具、web组件加载、监听键盘的显示隐藏、Asset Store Kit
前端·华为·harmonyos
alikami1 小时前
【若依】用 post 请求传 json 格式的数据下载文件
前端·javascript·json
吃杠碰小鸡2 小时前
lodash常用函数
前端·javascript
emoji1111112 小时前
前端对页面数据进行缓存
开发语言·前端·javascript
泰伦闲鱼2 小时前
nestjs:GET REQUEST 缓存问题
服务器·前端·缓存·node.js·nestjs
m0_748250032 小时前
Web 第一次作业 初探html 使用VSCode工具开发
前端·html
一个处女座的程序猿O(∩_∩)O2 小时前
vue3 如何使用 mounted
前端·javascript·vue.js
m0_748235952 小时前
web复习(三)
前端