Webpack面试题汇总

109 阅读7分钟

1、说说你对webpack的理解

(1)背景

webpack最初的目标是实现前端项目模块化,旨在更高效的管理和维护项目中的每一个资源。

模块化

全局模式

最早的时候,我们会通过文件划分的形式实现模块化,也就是将每个功能及其相关状态数据各自单独放在不同的JS文件中。

约定每个文件是一个独立的模块,然后再将这些JS文件引入到页面,一个script标签对应一个模块。

<script src="module-a.js"></script>
<script src="module-b.js"></script>
  • 全局变量被污染了,很容易引起命名冲突
  • 模块没有私有空间
  • 依赖模糊,难以维护

单例模式

规定每个模块只暴露一个全局对象,然后模块的内容都挂载在这个对象上,这种方式也并没有解决第一种方式的依赖等问题。

window.moduleA = {
  method1: function () {
    console.log('moduleA#method1')
  }
}
  • 解决全局变量污染问题

IIFE 模式

再后来,我们使用立即执行函数为模块提供私有空间,通过参数的形式作为依赖声明。

// module-a.js
(function ($) {
  var name = 'module-a'

  function method1 () {
    console.log(name + '#method1')
    $('body').animate({ margin: '200px' })
  }

  window.moduleA = {
    method1: method1
  }
})(jQuery)
  • 解决模块私有空间问题

上述方式都是早期模块化的方式,但仍旧存在一些问题

  • 一个页面引入多个script标签,模块的加载并不受代码的控制
  • 依赖模糊,难以维护(看不出来谁依赖谁,内部依赖关系混乱导致难以维护)

(2)webpack

webpack是一个用于现代JavaScript应用程序的静态模块打包工具。

静态模块指的是开发阶段,可以直接被webpack引用的资源。

当webpack处理应用程序时,会在内部从一个或多个入口点构建一个依赖图,此依赖图对应映射到项目所需的每个模块,并生成一个或多个 bundle,可以由浏览器直接加载。

webpack的能力:

  • 编译代码能力:能够将es6编译成es5,解决浏览器兼容问题
  • 模块整合能力:将多个不同的es模块,整合成一个bundle,解决浏览器频繁请求文件的问题
  • 万物皆可模块能力:支持各种类型的前端模块

webpack有很多新特性:

  • 对commonJs、AMD、ES6语法实现了兼容
  • 对JavaScript、CSS、图片等资源文件都支持打包
  • 可以将代码切割成不同的块,实现按需加载,缩短初始化时间
  • 支持sourceMaps,易于调试
  • 具有强大的plugin机制,提供各种各样灵活的功能

2、webpack的构建流程

webpack的运行流程是一个串行的过程,从启动到结束依次会执行以下流程:

  • 初始化参数:从配置文件和shell语句中读取并合并参数,得到最终的参数
  • 开始编译:用上一步得到的参数初始化Compiler对象,加载所有配置的插件,执行对象的run方法开始执行编译

plugin的本质是带有apply方法的对象,将Compiler对象传入apply方法并调用,在apply方法中,我们可以注册钩子函数,并实现自己的逻辑。

  • 确定入口:根据配置中的Entry找到所有的入口文件
  • 编译模块:从入口文件出发,调用所有的loader对模块进行编译,在找出模块的依赖模块,递归编译模块
  • 完成模块编译:loader编译完所有的模块之后,得到了每个模块被翻译后的最终内容,以及他们之间的依赖关系
  • 输出资源:根据入口和模块之间的依赖关系,组成一个个包含多个模块的chunk,再把每个chunk转换成一个单独的文件加入到输出列表,这一步是可以修改输出内容的最后机会
  • 输出完成:根据配置确定输出的路径和文件名,把文件内容写入到文件系统中

一般情况下,一个入口对应一个chunk,异步模块单独组织为一个 chunk。

浅析webpack打包过程

3、webpack中常见的loader

(1)loader是什么

  • 实现对不同格式文件的处理:比如将SASS转换为CSS,或将TypeScript转化为JavaScript,通过eslint检查JS代码
  • 将资源转换成JS文件以添加到模块依赖中。webpack通过生成AST来分析文件的依赖关系,因而只能处理JS文件。webpack的模块不仅仅指的是JS,可以是任何类型的资源,因而我们使用loader来将资源转换成JS文件。

(2)配置方式

rules是一个数组的形式,我们可以针对不同的文件配置不同的规则。

module.exports = {
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          { loader: 'style-loader' },
          {
            loader: 'css-loader',
            options: {
              modules: true
            }
          },
          { loader: 'sass-loader' }
        ]
      }
    ]
  }
};

(3)loader的特性

  • loader支持链式调用,顺序为从右到左、从下到上
  • loader可以是同步的,也可以是异步的

(4)常见的loader

  • css-loader:解析css文件中的@import和url语句,处理css-modules,并将结果作为一个js模块返回

可以理解为将具有依赖关系的css模块以字符串的形式拼接在一起,并将其作为js模块的导出内容

  • style-loader:把css代码注入js中,并通过dom操作加载css

css-loader返回的是一个js模块的代码,将这些代码直接放进style标签是不行的,还需要进行特殊处理,最终将css的代码通过style标签插入到页面中

  • less-loader/sass-loader:将less/sass转化成css
  • postcss-loader:处理css代码,可以进行css自动化前缀添加、css变量处理、css压缩等操作
  • file-loader:图片字体等资源的处理loader,将文件导入地址替换为访问地址,并把文件输出到相应的位置

使用图片时,可以使用相对路径,file-loader会进行路径的转换处理

  • url-loader:file-loader的增强版,可以将图片转换为base64字符串,更快的加载图片,一旦图片过大,依旧使用file-loader加载图片
  • babel-loader:es6及以上版本转化为es5
  • eslint-loader:通过eslint检查JavaScript代码

4、webpack中常见的plugin

(1)什么是plugin

Plugin是一种计算机应用程序,它和主应用程序互相交互,以提供特定的功能。

webpack中的plugin赋予其各种灵活的功能,能够解决loader无法处理的问题。

plugin本质是一个具有apply方法的JavaScript对象。apply方法会被webpack的Compiler调用,并且在整个编译生命周期都可以访问Compiler对象。

const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
  apply(compiler) {
    compiler.hooks.run.tap(pluginName, (compilation) => {
      console.log('webpack 构建过程开始!');
    });
  }
}

module.exports = ConsoleLogOnBuildWebpackPlugin;

(2)常见的plugin

  • HtmlWebpackPlugin:简化html文件的创建

当使用webpack打包时,创建一个html文件,并把webpack打包后的静态文件自动插入到这个html文件中

  • DefinePlugin:定义环境变量
  • UglifyJSWebpackPlugin:压缩es6代码,单线程压缩代码
  • WebpackBundleAnalyzer:可视化输出文件的体积
  • MiniCssExtractPlugin:将CSS提取到单独的文件中,按需加载
  • WebpackParallelUglifyPlugin:多核压缩
  • CleanWebpackPlugin:清除构建目录

5、loader和Plugin的区别

不同的作用

  • loader:加载器,Webpack将一切文件视为模块,但是只能加载js文件,loader可以让Webpack具备加载解析非js文件的能力。
  • Plugin:插件,Plugin可以扩展Webpack的功能,让Webpack具有更多的灵活性。在Webpack运行的生命周期过程中会广播出很多事件,插件会监听这些事件,在合适的时候加入到Webpack运行机制中,改变Webpack的行为。

不同的用法

  • loader:在module.rules中配置,作为模块的解析规则而存在。类型为数组,每一项为对象,里面描述了对于不同的文件类型,采用什么样的加载器以及加载器的参数。
  • Plugin:在module.plugins中配置。类型为数组,每一项是插件的实例,参数通过构造函数传入。

6、webpack、rollup、parcel

  • webpack:适用于大型复杂的前端项目构建,具有强大的loader和插件生态。

打包后的文件实际上就是一个立即执行函数,这个立即执行函数接受一个参数。这个参数是模块对象,键为各个模块的路径,值为模块内容。立即执行函数内部则处理模块间的引用与执行,这种情况更适合文件依赖复杂的应用开发。

  • rollup:适用于基础库的开发

rollup就是将各个模块打包进一个文件中,并且通过tree-shaking来删除无用的代码,可以最大程度降低代码体积。

但是rollup没有webpack诸如代码分割、按需加载等功能,更聚焦于库的打包

  • parcel:适用于简单的实验性项目

可以满足低门槛的快速看到效果,但是生态差、报错信息不够全面,正式项目不建议使用

7、如何用Webpack优化前端性能

用Webpack优化前端性能指的是优化Webpack的输出结果,提高页面的运行速度。

  • 压缩代码:删除多余的代码、注释、简化代码的写法

利用Webpack的UglifyJsPlugin和ParallelUglifyPlugin压缩JS

css-loader的minimize来压缩css

使用HtmlWebpackPlugin插件来生成HTML的模板时候,通过配置属性minify进行html优化

  • CDN加速:将静态资源路径修改为CDN的路径

可以利用webpack的output/loader的publicPath参数来修改路径

  • Tree Shaking:将代码中永远不会走到的片段删除掉

可以通过在启动webpack时追加参数--optimize-minimize来实现

  • Code Splitting:将代码按照一定规则进行分割,实现按需加载

  • 提取公共第三方库:SplitChunksPlugin插件来进行公共模块抽取,利用浏览器缓存可以长期缓存这些无需频繁变动的公共代码

  • 优化图片大小:使用像image-webpack-loader这样的loader来优化图像文件,以减小文件大小

8、提升webpack的构建速度

  • 升级Webpack版本:每个新版本都可能包含一些性能优化

  • HappyPack:利用进程并行编译loader,利用缓存使得rebuild更快

但是作者表示不会继续开发这个项目,类似的替代者是thread-leader

  • externals(外部扩展):将不怎么需要更新的第三方库使用script加载,脱离webpack的打包,减少打包的时间

  • dll(动态链接库):采用webpack的DllPlugin和DllReferencePlugin引入Dll,让一些基本不会改动的代码先被编译成静态资源,避免反复编译浪费时间

  • 利用缓存:webpack.cache、babel-loader.cacheDirectory、HappyPack.cache

  • 缩小文件的搜索范围:缩小loader插件的搜索范围,这种情况下提升的性能有限

  • 使用更轻量的插件,避免引入过多不必要的功能

9、webpack的热更新是如何做到的?说明其原理

HMR原理

热更新

webpack的热更新又称热替换(Hot Module Relacement)HMR,这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

配置热更新

  • devServer的hot设置为true
  • 添加HotModuleReplacementPlugin插件

或者在package.json的运行语句中使用webpack-dev-server --hot,可以告诉webpack,自动引入热更新插件

热更新原理

  • dev-server启动本地服务

    • 启动webpack,生成Compiler实例。Compiler实例上有很多方法,比如可以启动webpack的编译工作、监听本地文件的变化等
    • 使用express框架启动本地server,让浏览器可以请求本地的静态资源
    • 本地服务启动之后,再去启动websocket服务,实现本地服务和浏览器的双向通信
  • 如何编译并监听本地代码

    • 主要利用webpack-dev-middleware库
    • 调用compiler.watch
      • 首先对本地文件进行编译打包,也就是一系列webpack编译流程
      • 首次编译结束后,开启对本地文件的监听,当文件发生变化之后,重新编译
      • 监听本地文件的变化主要是通过判断文件的生成时间是否有变化
    • 执行setFs方法,将编译后的文件打包到内存,而不是dist目录
      • 这是因为访问内存比访问文件系统更快,也减少了代码写入文件的开销,这一切归功于memory-fs

webpack-dev-server只负责启动服务以及前置准备工作,所有文件相关的操作都抽离到middleware库了,主要是本地文件的编译、输出、监听。职责划分更加清晰。

在dev-server初始化的时候,启动的是本地服务端的websocket,那么作为客户端的浏览器,该如何实现与服务端的通信,实际上这些通信代码是我们偷偷塞到浏览器的。

启动本地服务器之前,调用了updateCompiler方法,获取了两个路径:websocket客户端代码路径(webpack-dex-server/clint/index.js)、webpack热更新代码路径,并把这两个路径作为webpack的入口,这样就可以把这两个文件的代码放到浏览器上

  • 如何与浏览器双向通信
    • 监听文件编译成功之后,socket服务端会向客户端浏览器发送通知,执行ok和hash事件,这样浏览器可以拿到最新的hash值,做检查更新逻辑
    • webpack-dex-server/clint/index.js:被放在浏览器的websocket客户端代码,注册了两个监听事件
      • hash事件:更新最新一次打包后的hash值
      • ok事件:进行热更新检查
    • webpack/hot/dev-server.js:进行热更新检查具体操作,主要利用HotModuleReplacementPlugin插件
      • 当我们设置devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload
      • 模块热更新:携带最新的hash值,向本地服务器发起请求,服务器返回json,json中包含所有要更新的模块hash,获取更新列表之后,再次向服务器请求最新的模块代码,之后进行模块对比,决定是否更新

10、Babel的原理是什么

Babel的转义过程主要分为三个阶段:

  • 解析Parse:将代码解析生成抽象语法树(AST),即词法分析和语法分析的过程
  • 转换Transfrom:Babel接收得到的AST并通过babel-traverse对其进行遍历,在此过程中进行添加、更新、移除等操作
  • 生成Generate:将变换后的AST再转换为JS代码,使用的模块是babel-generator

AST:源代码的抽象语法结构的树状表现形式,简单来说就是一个深度嵌套对象,用来描述代码的所有信息

11、前后端分离是什么

前后端分离,顾名思义就是前端和后端分开,分开开发和部署。

为什么要前后端分离

  • 项目变大之后难以维护,前后端代码耦合在一起,复用难度高,版本迭代更新对于开发人员来说是一项挑战
  • 需要打包编译整个项目,耗费时间长
  • 不管代码的改量是多少,都需要重新部署所有的代码

前后端分离的好处

  • 提升前后端的开发效率,专业的人做专业的事
  • 提高代码复用率,降低代码耦合,使得代码更容易维护
  • 提高前端静态页面的加载速度
  • 前后端分开部署

前后端分离带来的问题

  • 跨域问题

12、介绍一下Tree-Shaking及其工作原理

import是静态执行的,在编译阶段就能够确定所导入的模块,在运行时快速加载这些模块。import返回的值是对模块实例的引用,多次导入同一个模块,各个模块间共享实例。

require是动态执行的,在运行时才能够确定所需的模块,需要动态地加载这些模块。require返回的值相当于模块实例的复制,多次导入同一个模块,各自有不同的实例。

CommonJS规范中,我们无法确定在实际运行前是否需要引入的模块,所以并不适合Tree-Shaking,直到ES6的静态导入语法的出现,可以在编译的过程中分析出不需要的代码。

Tree-Shaking的逻辑停留在静态代码分析层面,只能浅显的判断:

  • 变量导出的模块是否被其他模块引用
  • 引用模块的主体代码有没有出现这个变量

没有进一步从语义能够分析是否真的有效使用了变量,因而开发者要有意识的规避无意义的赋值操作。

import foo from './foo';

const f = foo; // 实际上f并没有使用,无意义赋值

13、webpack和vite的区别

  • 构建速度:Vite的构建速度比Webpack更快
    • Vite在开发过程中没有打包,直接将ES模块源码输出给浏览器,使用浏览器原生支持的ES模块加载(<script module>)
    • Webpack使用打包后的文件进行模块加载(bundle能够在浏览器中运行的原因在于输出文件中通过__webpack_require__函数定义了可以在浏览器中执行的加载函数来模拟Node中的require语句)
    • 并且Vite的每个模块都可以独立的进行编译和缓存
  • 配置复杂度:
    • Vite的配置相对简单,无需大量配置,只要指定一些基本的选项
    • Webpack的配置略复杂,涉及各种loader和Plugin
  • 生态环境
    • Webpack的生态环境更加成熟,在社区中拥有广泛的支持和丰富的插件库
    • Vite尚处于发展阶段,生态系仍然不太完善
  • 功能特性:
    • Webpack是一个功能更加全面的打包工具,支持各种loader和插件
    • Vite的设计初衷是专注于开发环境下的快速构建,因此对于一些高级特性的支持相对较少

综上,Vite适用于开发环境下的快速构建,Webpack适用于生产环境下的复杂应用程序打包,使用哪种工具需要根据具体项目需求进行评估。

14、说说你对SSG的理解

SSG(Static Site Generation,静态网站生成)指在构建时预先生成静态页面,并将页面部署到CDN或者其他存储服务中,以提升Web应用的性能和用户体验。

优势

  • 加载速度快:不需要动态请求数据渲染页面
  • 安全性高:没有后台代码和数据库
  • 成本低:不需要动态服务器设备,降低网站的运维成本和服务器负担。

SSG不适用于频繁更新的内容和动态交互场景,但对于内容较为稳定和更新较少的网站是一个性能优化的好选择。

15、谈谈你对前端工程化的理解

前端工程化指的是将前端开发中的设计、开发、测试、部署等环节进行标准化和自动化,以提高开发效率和代码质量,并降低维护成本。

  • 模块化:使用模块化的思想可以将复杂的代码拆分成小的可重用得模块。使得不同模块之间的依赖关系更加清晰
  • 自动化构建:通过构建工具,自动化的完成代码编译、压缩、打包、等任务,提高开发效率(Webpack、Gulp、Rollup)
  • 自动化测试:自动化完成单元测试、集成测试、UI测试等任务,从而提高代码质量并减少故障(Jest、Mocha、Chai、Selenium)
  • 自动化部署:自动化完成代码上传、服务器部署、数据库更新等任务,减少手动操作产生的错误和漏洞(GitLab CI/CD )
  • 规范化管理:使用代码规范(Eslint)和版本控制系统(Git),可以规范开发流程和代码风格,提高代码的可读性和可维护性

16、什么是CI/CD

CI/CD是持续集成(Continuous Integration)和持续交付/持续部署(Continuous Delivery/Continuous Deployment)的缩写。

  • 持续集成:CI是一种开发实践,通过频繁地将代码集成到共享的版本控制库中,并自动进行构建、测试和静态代码分析等过程,以期望早发现和解决代码集成问题。主要目标是减少集中冲突和快速反馈,提高开发团队的协作效率和代码质量。

  • 持续交付/持续部署:CD是在CI的基础上进一步自动化整个软件交付流程的实践。

    • 持续交付:将软件交付到可部署环境的过程,包括自动化构建、自动化测试、文档生成和打包等,以确保每次交付都是可靠和可重复的
    • 持续部署:更进一步,将软件自动部署到生产环境,从而减少人工干预和降低发布的风险。

CI/CD的目标是通过自动化和持续的流程来提高软件的开发和交付效率、质量和可靠性。它帮助团队集中精力于开发新功能,并能够快速、频繁地将这些功能交付给最终用户。

CI/CD在现代软件开发中被广泛采用,为团队提供了一种更加可靠高效的软件交付方式。

17、Webpack的module、chunk、bundle分别指的是什么

Module(模块)

  • 在Webpack中,每个文件都被视为一个独立的模块,可以通过import、require等方式导入和导出
  • 代表了应用程序的组成部分

Chunk(代码块)

  • Webpack在构建过程中生成的代码块,这是一种逻辑上的概念,表示一组相互依赖的模块
  • Webpack在构建时,会根据依赖关系将模块组织成不同的代码块
  • 一般情况下,一个入口点会生成一个chunk,代码分割会生成分割代码块,但是实际上生成的chunk数和entry的配置有关
    • 字符串:单入口,一个chunk,一个bundle
    • 数组:多入口,所一个chunk,一个 bundle(HMR中使用了数组入口)
    • 对象:多入口,有多少个 key 就会形成多少个chunk,也就输出多少个 bundle 文件

Bundle(捆绑包)

  • Webpack根据模块依赖关系生成的最终输出文件。将多个模块打包成一个或多个捆绑包
  • Bundle是浏览器加载执行的最终文件,包含了应用程序所需的所有代码和资源

18、babel的stage

Babel是一个广泛使用的JavaScript编辑器,他可以将新版本的JavaScript代码转换为向后兼容的旧版本代码。

Babel通过使用不同的插件集合来支持ECMAScript提案的不同阶段,这些阶段被称为stage

以下是常见的Babel stage及其代表的意思:

  • Stage 0 - Strawman(展示阶段)

    • 提案最初的阶段,可能只是一个想法或草案,并没有正式进入ES规范的流程中
  • Stage 1 - Proposal(建议阶段)

    • 提案已经成为正式的ES提案,已经有了详细的规范和设计说明,并且正在讨论和收集反馈
  • Stage 2 - Draft(草案阶段)

    • 提案已经比较成熟,在语言规范中进行了初步定义,并且正在进行实验和实现
  • Stage 3 - Candidate(候选阶段)

    • 提案基本成熟,规范已经稳定,并且已经有了多个浏览器或环境的实验和测试
  • Stage 4 - Finished(完成阶段)

    • 提案已经准备好被纳入下一个版本的ES规范中,并且已经通过了所有必要的测试和审查

不是所有的提案都会按照这个阶段流程发展。一些重要的提案可能直接进入较高的阶段,有些提案可能在某个阶段被废弃。

Babel提供了一系列插件的集合,用于转义各个不同阶段的ES提案。在Babel配置文件中可以选择不同的插件集合,以支持希望使用的ES特性。

19、Webpack loader和Plugin实现原理

(1)Webpack打包基本原理

Webpack的一个核心功能就是把我们写的模块化代码,编译打包之后,生成可以在各浏览器中运行的代码。

打包的基本流程

  • 读取入口文件的内容
  • 分析入口文件,递归读取模块依赖的文件内容,生成依赖图
  • 根据依赖图,生成浏览器最终能够运行的代码

具体流程

  • fs读取文件内容
  • 借助babel实现入口文件的分析
    • babel/parser:将文件代码转换为AST
    • babel/traverse:遍历AST,收集依赖
    • babel/core、babel/preset-env:语法转换 es6 --> es5
  • 递归获取所有模块信息
    • 最终返回一个包含所有模块信息的对象
  • 生成最终浏览器可以运行的代码,写入bundle中
    • 对exports和require做一些处理
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

const getModuleInfo = file => {
    const body = fs.readFileSync(file, 'utf-8')
    console.log(body)
    const ast = parser.parse(body, {
       sourceType: 'module' 
    })
    // console.log(ast.program.body)
    const deps = {}
    traverse(ast, {
        ImportDeclaration({ node }) {
            const dirname = path.dirname(file);
            const absPath = './' + path.join(dirname, node.source.value)
            deps[node.source.value] = absPath
        }
    })
    const { code } = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]
    })
    const moduleInfo = { file, deps, code }
    return moduleInfo
}

const parseModules = file => {
    // 定义依赖图
    const depsGraph = {}
    // 首先获取入口的信息
    const entry = getModuleInfo(file)
    const temp = [entry]
    for (let i = 0; i < temp.length; i++) {
        const item = temp[i]
        const deps = item.deps
        if (deps) {
            // 遍历模块的依赖,递归获取模块信息
            for (const key in deps) {
                if (deps.hasOwnProperty(key)) {
                    temp.push(getModuleInfo(deps[key]))
                }
            }
        }
    }
    temp.forEach(moduleInfo => {
        depsGraph[moduleInfo.file] = {
            deps: moduleInfo.deps,
            code: moduleInfo.code
        }
    })
    // console.log(depsGraph)
    return depsGraph
}


// 生成最终可以在浏览器运行的代码
const bundle = file => {
    const depsGraph = JSON.stringify(parseModules(file))
    return `(function(graph){
        function require(file) {
            var exports = {};
            function absRequire(relPath){
                return require(graph[file].deps[relPath])
            }
            (function(require, exports, code){
                eval(code)
            })(absRequire, exports, graph[file].code)
            return exports
        }
        require('${file}')
    })(${depsGraph})`
}


const build = file => {
    const content = bundle(file)
    // 写入到dist/bundle.js
    fs.mkdirSync('./dist')
    fs.writeFileSync('./dist/bundle.js', content)
}

build('./src/index.js')

(2)手写loader

loader本质上就是一个函数,这个函数会在我们在我们加载一些文件时执行。

一般来说,我们会去使用官方推荐的loader-utils包去完成更加复杂的loader的编写。

// webpack.config.js
const path = require('path')

module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    },
    resolveLoader: {
        // loader路径查找顺序从左往右
        modules: ['node_modules', './']
    },
    module: {
        rules: [
            {
                test: /.js$/,
                use: {
                    loader: 'syncLoader',
                    options: {
                        message: '升值加薪'
                    }
                }
            }
        ]
    }
}

// syncLoader.js
const loaderUtils = require('loader-utils')
module.exports = function (source) {
    const options = loaderUtils.getOptions(this)
    console.log(options) // { message: '升值加薪' }
    source += options.message
    // 可以传递更详细的信息
    this.callback(null, source)
}

(3)手写Plugin

Plugin通常是在webpack打包的某个时间节点做一些操作,使用Plugin时,一般通过new Plugin(),我们可以明确的是,Plugin是一个类。

Webpack打包的时候,会调用plugin的apply方法来执行plugin的逻辑,这个方法接收一个compiler作为参数,compiler实际上就是webpack实例。

plugin的核心在于:apply执行时,可以调用webpack实例的生命周期钩子函数,在不同的时间点做一些操作。

// webpack.config.js 

const path = require('path')
const DemoWebpackPlugin = require('./demo-webpack-plugin')
module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    },
    plugins: [
        new DemoWebpackPlugin()
    ]
}

// demo-webpack-plugin.js
class DemoWebpackPlugin {
    constructor () {
        console.log('plugin init')
    }
    // compiler是webpack实例
    apply (compiler) {
        // 一个新的编译(compilation)创建之后(同步)
        // compilation代表每一次执行打包,独立的编译
        compiler.hooks.compile.tap('DemoWebpackPlugin', compilation => {
            console.log(compilation)
        })
        // 生成资源到 output 目录之前(异步)
        compiler.hooks.emit.tapAsync('DemoWebpackPlugin', (compilation, fn) => {
            console.log(compilation)
            compilation.assets['index.md'] = {
                // 文件内容
                source: function () {
                    return 'this is a demo for plugin'
                },
                // 文件尺寸
                size: function () {
                    return 25
                }
            }
            fn()
        })
    }
}

module.exports = DemoWebpackPlugin

20、webpack的proxy工作原理,为什么可以解决跨域

(1)使用

// webpack.config.js
devServer: {
    hot:true, // 它是热更新:只更新改变的组件或者模块,不会整体刷新页面
    open: true, // 是否自动打开浏览器
    proxy: { // 配置代理(只在本地开发有效,上线无效)
      "/x": { // 这是请求接口中要替换的标识
        target: "https://api.bilibili.com", // 被替换的目标地址,即把 /api 替换成这个
        pathRewrite: {"^/api" : ""}, 
        secure: false, // 若代理的地址是https协议,需要配置这个属性
      },
      '/api': {
        target: 'http://localhost:3000', // 这是本地用node写的一个服务,用webpack-dev-server起的服务默认端口是8080
        pathRewrite: {"/api" : ""}, // 后台在转接的时候url中是没有 /api 的
        changeOrigin: true, // 加了这个属性,那后端收到的请求头中的host是目标地址 target
      },
    } 
  }

(2)工作原理

利用http-proxy-middleware这个http代理中间件,实现请求转发给其他服务器。

在开发阶段, webpack-dev-server 会启动一个本地开发服务。

当本地发送请求的时候,代理服务器响应该请求,并将请求转发到目标服务器,目标服务器响应数据后再将数据返回给代理服务器,最终再由代理服务器将数据响应给本地。

代理服务器与本地浏览器显示的页面同源,不存在跨域问题。

21、webpack5的升级点

升级说明
持久缓存webpack5引入了更好的持久缓存机制,利用了更稳定的HashedModuleIdsPlugin和NamedChunksPlugin,以改善构建性能
Tree-Shaking改进提供了更好的代码优化,以便删除未使用的代码
支持WebAssembly(WASM)对WASM提供原生支持,可以在项目中使用
支持ES6模块导入对动态导入语法有更好的支持,可以更轻松的进行代码分割
模块联邦这是webpack5中的一项重大功能,允许多个独立的webpack构建连接在一起,实现模块共享,更好的支持微服务架构
缓存组新的缓存组概念被引入,可以更细粒度的控制模块的缓存策略
内置代码分割优化Webpack 5通过optimization.splitChunks进行了重新设计,提供了更灵活的配置选项,使得代码分割更为强大和易用

22、devDependencies和dependencies区别

dependencies

  • 指定项目在生产环境中运行所需要的依赖项
  • 依赖项通常包括运行时需要的库、框架、工具等。

devDependencies

  • 指定在开发过程中所需要的依赖项。
  • 依赖项通常包括开发、测试、构建、部署等过程中所需的工具、库等。
    • 例如,测试框架、构建工具、代码检查工具等通常属于 devDependencies