工程化

188 阅读18分钟

webpack是啥?有啥作用

webpack是静态模块打包工具

作用:分析、压缩、打包代码

好处:减少文件体积、减少文件数量, 提高网页加载速度

功能:

  • 模块的打包:通过打包整合不同的模块文件保证各模块之间的引用和执行

  • 代码编译:通过丰富的loader可以将不同格式文件如.sass/.vue/.jsx转译为浏览器可以执行的文件

  • 扩展功能:通过社区丰富的plugin可以实现多种强大的功能,例如代码分割、代码混淆、代码压缩、按需加载.....等等

webpack构建流程

流程:

  • 初始化参数:从配置文件或者shell语句中读取合并参数

  • 开始编译:用参数初始化Compiler对象,加载所有配置的插件,执行run方法。

  • 确定入口:根据entry参数找到入口文件

  • 编译模块:从⼊⼝⽂件出发,调⽤所有配置的 Loader 对模块进⾏翻译,再找出该模块依赖的模块,再递归本步骤直到所有⼊⼝依赖的⽂件都经过了本步骤的处理;

  • 完成模块编译:在经过第4步使⽤ Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;

  • 输出资源:根据⼊⼝和模块之间的依赖关系,组装成⼀个个包含多个模块的 Chunk,再把每个 Chunk 转换成⼀个单独的⽂件加⼊到输出列表,这步是可以修改输出内容的最后机会;

  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和⽂件名,把⽂件内容写⼊到⽂件系统

总结就是三个阶段:

  • 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
  • 编译:从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
  • 输出:将编译后的 Module 组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中

从资源转换角度看:

  • compiler.make阶段

    • entry 文件以 dependence 对象形式加入 compilation 的依赖列表 ,dependence 对象记录了 entry 的相关信息
    • 根据 dependency 创建 对应的module 对象,之后读入 module 对应的文件内容, 调用 loader-runner对内容做转化, 转化结果若有对其他依赖则继续读入依赖资源, 重复此过程直到所有的依赖均被转换为 module
  • compilation.seal 阶段

    • 遍历 module 集合, 根据 entry配置以及引入资源的方式, 将 module 分配到不同的 Chunk
    • Chunk之间最终形成ChunkGraph结构
    • 遍历ChunkGraph 调用 compilation.emitAssets 方法标记 chunk 的输出规则, 及转换为 assets集合
  • compiler.emitAssets阶段

    • assets写入文件系统

webpack run过程

执行npm run dev时候最先执行的build/dev-server.js文件,该文件主要完成下面几件事情:

1、检查node和npm的版本、引入相关插件配置

2、webpack对源码进行编译打包并返回compiler对象

3、创建express服务器

4、配置开发中间件(webpack-dev-middleware)和热重载中间件(webpack-hot-middleware)

5、挂载代理服务和中间件

6、配置静态资源

7、启动服务器监听特定端口(8080)

8、自动打开浏览器并打开特定网址(localhost:8080)

Loader

什么是Loader

loader的作用是让webpack拥有了加载和解析非JavaScript文件的能力,其实就是用于解析文件的

常见Loader

  • image-loader : 加载并且压缩图片文件

  • css-loader : 加载 CSS,支持模块化、压缩、文件导入等特性

  • style-loader : 把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS

  • sass-loader: 将SCSS/SASS代码转换为CSS

当我们使用类似于 less 或者 scss 等预处理器的时候,通常需要多个 loader 的配合使用如test: /.less$/, use: ['style-loader', 'css-loader', 'less-loader']

  • eslint-loader : 通过 ESLint 检查 JavaScript 代码

  • tslint-loader : 通过 TSLint检查 TypeScript 代码

  • babel-loader : 把 ES6 转换成 ES5

  • file-loader:可以指定要复制和放置资源文件的位置,以及如何使用版本哈希命名以获得更好的缓存,并在代码中通过URL去引用输出的文件

  • url-loader:和file-loader功能相似,但是可以通过指定阈值来根据文件大小使用不同的处理方式(小于阈值则返回base64格式编码并将文件的 data-url内联到bundle中)

  • raw-loader:加载文件原始内容

webpack5自身内置了file-loader/ url-loader/ raw-loader等loader,所以我们不需要再显示引入loader 只需要指定对应的type即可实现相同的功能 如file-loader等价于 type= "asset/resource"'

  • awesome-typescirpt-loader: 将typescript转换为javaScript 并且性能由于ts-loader

  • source-map-loader: 加载额外的Source Map文件

  • cache-loader: 可以在一些开销较大的Loader之前添加可以将结果缓存到磁盘中,提高构建的效率

  • thread-loader: 多线程打包,加快打包速度

Loader编写

  • loader支持链式调用,上一个loader的执行结果会作为下一个loader的入参。

    • 根据这个特性,我们知道我们的loader想要有返回值,并且这个返回值必须是标准的JavaScript字符串或者AST代码结构,这样才能保证下一个loader的正常调用。
  • loader的主要职责就是将代码转译为webpack可以理解的js代码。

    • 根据这个特性,loader内部一般需要通过return / this.callback来返回转换后的结果
  • 单个loader一把只负责单一的功能。

    • 根据这个特性,我们的loader应该符合单一职责的原则,尽量别让单个loader执行太多职责
  • 善于利用开发工具

    • loader-utilsloader-utils 是一个非常重要的 Loader 开发辅助工具,为开发中提供了诸如读取配置、requestString的序列化和反序列化、getOptions/getCurrentRequest/parseQuery等核心接口....等等功能,对于loader的开发十分有用
    • schema--utilsschema-utils是用于校验用户传入loader的参数配置的校验工具,也是在开发中十分有用
  • loader是无状态的

    • 根据此特性,我们不应该在loader保存状态
  • webpack默认缓存loader的执行结果

    • webpack会默认缓存loader的执行结果直到资源/所依赖的资源发生变化 如果想要loader不缓存 可以通过this.cacheble 显式声明不做缓存
  • Loader接收三个参数

    • source: 资源输入 对于第一个执行的loader为资源文件的内容 后续执行的loader则为前一个loader的执行结果 也可能是字符串 或者是代码的AST结构
    • sourceMap: 可选参数 代码的sourcemap结构
    • data: 可选参数 其他需要在Loader链中传递的信息
  • 正确上报loader的异常信息

    • 一般尽量使用logger.error 减少对用户的干扰
    • 对于需要明确警示用户的错误 优先使用 this.emitError
    • 对于已经严重到不能继续往下编译的错误 使用 callback
  • loader函数中的this 由webpack提供 并且指向了loader-runtimeloaderContext 对象

    • 可以通过this来获取loader需要的各种信息 Loader Context提供了许多实用的接口,我们不仅可以通过这些接口获取需要的信息,还可以通过这些接口改变webpack的运行状态(相当于产生 Side Effect
  • loader由pitchnormal两个阶段

    • 根据此特性,我们可以在pitch阶段预处理一些操作

webpack会按照 use 定义的顺序从前往后执行Pitch Loader 从后往前执行Normal Loader 我们可以将一些预处理的逻辑放在Pitch

  • 正确处理日志 使用 Loader Context``的getLogger接口(支持verbose/log/info/warn/error 五种级别的日志 用户可以通过infrastructureLogging.level 配置项筛选不同日志内容 )

  • 充分调试你编写的loader

    • 创建出webpack实例 并运行laoder
    • 获取loader执行结果 对比、分析判断是否符合预期
    • 判断执行过程中是否出错

Loader编写顺序

loader 总是 从右到左被调用

可以通过enforce来强制控制Loader的执行顺序 (pre 表示在所有正常的loader执行之前执行,post则表示在之后执行)

Loader的执行有以下两个阶段:

  1. Pitching 阶段: loader 上的 pitch 方法,按照 后置(post)、行内(inline)、普通(normal)、前置(pre) 的顺序调用。更多详细信息,请查看 Pitching Loader
  2. Normal 阶段: loader 上的 常规方法,按照 前置(pre)、普通(normal)、行内(inline)、后置(post) 的顺序调用。模块源码的转换, 发生在这个阶段。

Plugin

什么是Plugin

Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果。其实就是插件就是拓展功能的

常见Plugin

  • define-plugin : 定义环境变量(webpack4之后可以通过指定mode:production/development实现同样效果)

  • html-webpack-plugin : 简化 HTML 文件创建

  • web-webpack-plugin:为单页面应用输出HTML 性能优于html-webpack-plugin

  • webpack-parallel-uglify-plugin : 多进程执行代码压缩,提升构建速度

  • webpack-bundle-analyzer : 可视化 Webpack 输出文件的体积

  • speed-measure-webpack-plugin : 可以看到每个 LoaderPlugin 执行耗时 (整个打包耗时、每个 PluginLoader 耗时),可用于性能分析

  • mini-css-extract-plugin : 分离样式文件,CSS 提取为独立文件,支持按需加载

  • clean-webpack-plugin: 每次打包时删除上次打包的产物, 保证打包目录下的文件都是最新的

  • webpack-merge: 用来合并公共配置文件,常用(例如分别配置webpack.common.config.js/ webpack.dev.config.js/webpack.production.config.js并将其合并)

  • ignore-plugin: 忽略指定的文件,可以加快构建速度

  • terser-webpack-plugin:压缩ES6的代码(tree-shaking)

  • uglifyjs-webpack-plugin: 压缩js代码

  • mini-css-extract-plugin: 将CSS提取为独立文件,支持按需加载

  • css-minimize-webpack-plugin:压缩CSS代码

css文件的压缩需要mini-css-extract-plugincss-minimize-webpack-plugin 的配合使用 即先使用mini-css-extract-plugin将css代码抽离成单独文件,之后使用 css-minimize-webpack-plugin对css代码进行压缩

  • serviceworker-webpack-plugin: 为离线应用增加离线缓存功能

  • ModuleconcatenationPlugin: 开启Scope Hositing 用于合并提升作用域, 减小代码体积

  • copy-webpack-plugin: 在构建的时候,复制静态资源到打包目录。

  • compression-webpack-plugin: 生产环境采用gzip压缩JS和CSS

-webpack-dashboard: 可以更友好地展示打包相关信息

Plugin编写

Plugin是通过监听webpack构建过程中发布的hooks来实施对应的操作从而影响更改构建逻辑以及生成的产物,而在这个过程中compilercompilation可以说是最核心的两个对象了,其中compiler可以暴露了整个构建流程的200+个hooks,而compilation则暴露了更细粒度的hooks

compiler对象是一个全局单例,代表了webpack从开启到关闭的整个生命周期,负责启动编译和监听文件,而compilation是每次构建过程的上下文对象,包含当次构建所需要的信息

每次热更新和重新编译都会创建一个新的compilation对象,compilation对象只代表当次编译

插件是通过监听webpack构建过程中发布的hooks从而在特定阶段去执行特定功能来达到改变构建逻辑以及产物的目的,而这些都离不开tapable (一个专门用于处理各种发布订阅的库 有同步异步、熔断、循环、瀑布流等钩子),关于了解tapable的使用,这里推荐这篇文章:Webpack tapable 使用研究 - 掘金 (juejin.cn)。 讲完plugin的前置知识,接下来就让我们正式开始学习如何开发插件

  • 插件是通过监听webpack发布的hooks来工作的

    • 根据这个特性,我们的plugin一定是一个函数或者一个包含apply() 的对象,这样才可以监听compiler 对象
  • 传递给插件的compiler compilation 都是同一个引用

    • 根据此特性,我们知道我们的插件是会影响到其他插件的,所以我们在编写插件的时候应该分析会对其他插件造成啥影响
  • 基于tapable来完成对hooks的复杂的订阅以及响应

    • 编译过程的特定节点会分发特定钩子,插件可以通过这些钩子来执行对应的操作
    • 通过 tapable的回调机制以参数形式传递上下文信息
    • 可以通过上下文的众多接口来影响构建流程
  • 监听一些具有特定意义的hook来影响构建

    • compiler.hooks.compilation:webpack刚启动完并创建compilation对象后触发
    • compiler.hooks.make:webpack开始构建时触发
    • compiler.hooks.done:webpack 完成编译时触发,此时可以通过stats对象得知编译过程中的各种信息
  • 善于使用开发工具

    • 使用schema-utils用于校验参数(关于schema-utils的使用方法读者可以自行查阅)
  • 正确处理插件日志信息以及插件信息

    • 使用 stats汇总插件的统计数据
    • 使用 ProgressPlugin插件的 reportProgress接口上报执行进度
    • 通过 compilation.getLogger获取分级日志管理器
    • 使用 compilation.errors/warining处理异常信息(eslint-webpack-plugin的做法)
  • 测试插件

    • 通过分析compilation.error/warn 数组来判断webpack是否运行成功 - 分析构建产物判断插件功能是否符合预期

以上便是如何编写plugin所需的知识和常规流程,建议可以阅读一些插件例如eslint-webpack-plugin / DefinePlugin 等插件的源码来更深入地学习插件开发的知识和流程

Loader和Plugin的区别

功能不同:

  • Loader本质是一个函数,它是一个转换器。webpack只能解析原生js文件,对于其他类型文件就需要用loader进行转换。
  • Plugin它是一个插件,用于增强webpack功能。webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果

用法不同

  • Loader的配置是在module.rules下进行。类型为数组,每⼀项都是⼀个 Object ,⾥⾯描述了对于什么类型的⽂件( test ),使⽤什么加载( loader )和使⽤的参数( options
  • Plugin的配置在plugins下。类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。

总结:

Loader主要负责将代码转译为webpack 可以处理的JavaScript代码,而 Plugin 更多的是负责通过接入webpack 构建过程来影响构建过程以及产物的输出,Loader的职责相对比较单一简单,而Plugin更为丰富多样

Webpack热更新

Webpack的热更新(Hot Module Replacement,简称HMR),在不刷新页面的前提下,将新代码替换掉旧代码

原理:

  • 使用 webpack-dev-server(WDS) 托管静态资源 同时以Runtime方式注入HMR客户端代码
  • 浏览器加载页面后 与WDS建立WebSocket连接
  • webpack监听到文件变化后 增量构建发生变更的模块 并通过WebSocket发送hash事件
  • 浏览器接收到 hash事件后 请求 manifest资源文件 确认增量变更范围
  • 浏览器加载发生变更的增量模块
  • webpack运行时触发变更模块的module.hot.accept回调 执行代码变更逻辑
  • done:构建完成,更新变化

总结:

HRM的原理实际上是 webpack-dev-server(WDS)和浏览器之间维护了一个websocket服务。当本地资源发生变化后,webpack会先将打包生成新的模块代码放入内存中,然后WDS向浏览器推送更新,并附带上构建时的hash,让客户端和上一次资源进行对比。客户端对比出差异后会向WDS发起Ajax请求获取到更改后的内容(文件列表、hash),通过这些信息再向WDS发起jsonp请求获取到最新的模块代码。

bundle,chunk,module是什么?

  • bundle 捆绑包: 它是构建过程的最终产物,由说有需要的chunkmodule组成。

  • chunk 代码块:一个chunk由多个模块组合而成,用于代码的合并和分割,在构建过程中一起被打包到一个文件中。

  • module 模块:是代码的基本单位,可以是一个文件、一个组件、一个库等。在编译的时候会从entry中递归寻找出所有依赖的模块。

Babel的原理是什么?

Babel 的主要工作是对代码进行转译。 (解决兼容, 解析执行一部分代码)

转译分为三阶段:

  • 解析(Parse),将代码解析⽣成抽象语法树 AST,也就是词法分析与语法分析的过程
  • 转换(Transform),对语法树进⾏变换方面的⼀系列操作。通过 babel-traverse,进⾏遍历并作添加、更新、删除等操作
  • ⽣成(Generate),通过 babel-generator 将变换后的 AST 转换为 JS 代码

文件指纹作用?怎么用?

概念:文件指纹是指文件打包后的一连串后缀

作用:

  • 版本管理:  在发布版本时,通过文件指纹来区分 修改的文件 和 未修改的文件。
  • 使用缓存:  浏览器通过文件指纹是否改变来决定使用缓存文件还是请求新文件。

种类:

  • Hash:和整个项目的构建相关,只要项目有修改(compilation实例改变),Hash就会更新
  • Contenthash:和文件的内容有关,只有内容发生改变时才会修改
  • Chunkhash:和webpack构架的chunk有关 不同的entry会构建出不同的chunk (不同 ChunkHash之间的变化互不影响)

如何使用:

  • JS文件:使用Chunkhash
  • CSS文件:使用Contenthash
  • 图片等静态资源: 使用hash

生产环境的output为了区分版本变动,通过Contenthash来达到清理缓存及时更新的效果,而开发环境中为了加快构建效率,一般不引入Contenthash

文件监听的原理

开启文件监听后,webpack会轮询访问文件的最后修改时间,当发现文件修改时间发生变化后,会先缓存起来等到aggregateTimeout再统一执行

开启文件监听方式:可以在构建时带上--watch 参数或者设置watch:true,而watchOptions则可以对监听的细节进行定制

watch: true,
watchOptions: {
    //不监听的文件或者文件夹 忽略一些大型的不经常变化的文件可以提高构建速度
    ignored: /node_modules/,
    //监听到变化会等多少时间再执行
    aggregateTimeout: 300,
    //判断文件是否发生变化是通过不断轮询指定文件有没有变化实现的
    poll: 1000
}

优化

打包速度

  • 利用缓存:利用Webpack的持久缓存功能,避免重复构建没有变化的代码。可以使用cache: true选项启用缓存。

  • 使用多进程/多线程构建 :使用thread-loaderhappypack等插件可以将构建过程分解为多个进程或线程,从而利用多核处理器加速构建。

  • 使用DllPlugin和HardSourceWebpackPluginDllPlugin可以将第三方库预先打包成单独的文件,减少构建时间。HardSourceWebpackPlugin可以缓存中间文件,加速后续构建过程。

  • 使用Tree Shaking: 配置WebpackTree Shaking机制,去除未使用的代码,减小生成的文件体积

  • 移除不必要的插件: 移除不必要的插件和配置,避免不必要的复杂性和性能开销

打包体积

  • 代码分割(Code Splitting) :将应用程序的代码划分为多个代码块,按需加载。这可以减小初始加载的体积,使页面更快加载。

  • Tree Shaking:配置WebpackTree Shaking机制,去除未使用的代码。这可以从模块中移除那些在项目中没有被引用到的部分。

  • 压缩代码:使用工具如UglifyJSTerser来压缩JavaScript代码。这会删除空格、注释和不必要的代码,减小文件体积。

  • 使用生产模式:在Webpack中使用生产模式,通过设置mode: 'production'来启用优化。这会自动应用一系列性能优化策略,包括代码压缩和Tree Shaking

  • 使用压缩工具:使用现代的压缩工具,如BrotliGzip,来对静态资源进行压缩,从而减小传输体积。

  • 利用CDN加速:将项目中引用的静态资源路径修改为CDN上的路径,减少图片、字体等静态资源等打包。

前端性能

  • 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤webpack的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩JS⽂件, 利⽤ cssnano (css-loader?minimize)来压缩css
  • 利⽤CDN加速: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤webpack对于 output 参数和各loader的 publicPath 参数来修改资源路径
  • Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动webpack时追加参数 --optimize-minimize 来实现
  • Code Splitting (自动): 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存
  • 提取公共第三⽅库: SplitChunksPlugin插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码

Code Splitting

本质:

代码分割的本质其实就是在源代码直接上线打包成唯一脚本main.bundle.js这两种极端方案之间的一种更适合实际场景的中间状态。

意义:

「用可接受的服务器性能压力增加来换取更好的用户体验。」

源代码直接上线:虽然过程可控,但是http请求多,性能开销大。

打包成唯一脚本:一把梭完自己爽,服务器压力小,但是页面空白期长,用户体验不好。

原理:

Code Splitting代码分割,是一种优化技术。它允许将一个大的chunk拆分成多个小的chunk,从而实现按需加载,减少初始加载时间,并提高应用程序的性能。

通常Webopack会将所有代码打包到一个单独的bundle中,然后在页面加载时一次性加载整个bundle。这样的做法可能导致初始加载时间过长,尤其是在大型应用程序中,因为用户需要等待所有代码加载完成才能访问应用程序。

Code Splitting 解决了这个问题,它将应用程序的代码划分为多个代码块,每个代码块代表不同的功能或路由。这些代码块可以在需要时被动态加载,使得页面只加载当前所需的功能,而不必等待整个应用程序的所有代码加载完毕。

Webpack中通过optimization.splitChunks配置项来开启代码分割。

Tree shaking

Tree Shaking 也叫摇树优化,是一种通过移除多于代码,从而减小最终生成的代码体积,生产环境默认开启

原理

  • ES6 模块系统Tree Shaking的基础是ES6模块系统,它具有静态特性,意味着模块的导入和导出关系在编译时就已经确定,不会受到程序运行时的影响。
  • 静态分析:在Webpack构建过程中,Webpack会通过静态分析依赖图,从入口文件开始,逐级追踪每个模块的依赖关系,以及模块之间的导入和导出关系。
  • 标记未使用代码: 在分析模块依赖时,Webpack会标记每个变量、函数、类和导入,以确定它们是否被实际使用。如果一个导入的模块只是被导入而没有被使用,或者某个模块的部分代码没有被使用,Webpack会将这些未使用的部分标记为"unused"
  • 删除未使用代码: 在代码标记为未使用后,Webpack会在最终的代码生成阶段,通过工具(如UglifyJS等)删除这些未使用的代码。这包括未使用的模块、函数、变量和导入。

必要条件:

一、静态模块结构(ES6 模块)

Tree shaking 依赖于静态导入 / 导出语法(ES6 Modules),因此必须满足:

  1. 使用 import/export 而非 require/module.exports

    • 动态模块系统(如 CommonJS)无法在编译时确定依赖关系,导致 Tree shaking 失效。

    • 正确示例

      javascript

      // ES6 模块
      export const add = (a, b) => a + b;
      export const subtract = (a, b) => a - b;
      
    • 错误示例

      javascript

      // CommonJS(不支持 Tree shaking)
      module.exports = {
        add: (a, b) => a + b,
        subtract: (a, b) => a - b,
      };
      
  2. 避免动态导入和副作用代码

    • 动态导入(如 import('./module.js').then(...))会导致依赖关系无法静态分析。
    • 模块顶层代码若有副作用(如修改全局变量、发起 API 请求),会被保留。
二、无副作用代码

Tree shaking 的核心是识别并移除未使用的纯代码,因此需确保:

  1. 纯函数与无副作用

    • 被导入的函数或模块不能有副作用,否则会被保留。

    • 有副作用的代码

      // 此模块有副作用(修改全局变量)
      export const value = 10;
      window.globalValue = 20; // 副作用代码,会被保留
      
    • 纯模块

      // 纯函数模块(无副作用)
      export const add = (a, b) => a + b;
      
  2. 使用 package.json 的 sideEffects 字段

    • 告知打包工具哪些文件有副作用,避免误删:

      {
        "sideEffects": false, // 所有文件无副作用
        "sideEffects": ["./src/globals.css", "./src/module-with-side-effects.js"] // 指定有副作用的文件
      }
      
三、正确的构建配置

构建工具需正确配置以启用 Tree shaking:

  1. 使用支持 Tree shaking 的打包工具

    • 推荐使用 Webpack 4+、Rollup 或 Vite,这些工具默认支持 ES6 模块的 Tree shaking。
  2. 启用生产模式

    • 生产模式会自动开启压缩和 Tree shaking:

      # Webpack
      webpack --mode production
      
      # Vite
      vite build
      
  3. 配置压缩工具

    • 使用 Terser 或 ESBuild 等压缩工具时,需确保以下选项开启:

      // webpack.config.js
      module.exports = {
        optimization: {
          usedExports: true, // 标记未使用的导出
          minimize: true, // 压缩代码
        },
      };
      
四、代码结构优化

编写可 Tree-shake 的代码:

  1. 避免命名空间导入

    • 不要一次性导入整个模块,而是按需导入:

      // 错误:导入整个模块,无法 Tree shake
      import * as utils from './utils';
      utils.add(1, 2);
      
      // 正确:按需导入,可 Tree shake
      import { add } from './utils';
      add(1, 2);
      
  2. 使用具名导出 / 导入

    • 具名导出比默认导出更易 Tree shake:

      // 推荐:具名导出
      export const add = (a, b) => a + b;
      
      // 不推荐:默认导出(可能导致整个模块被保留)
      export default function add(a, b) {
        return a + b;
      }
      
五、第三方库的兼容性

确保依赖的库支持 Tree shaking:

  1. 优先选择 ES6 模块格式的库

    • 检查库的 package.json 是否有 module 或 esnext 字段指向 ES6 模块版本。

    • 例如:

      {
        "main": "lib/cjs/index.js", // CommonJS 版本
        "module": "lib/esm/index.js" // ES6 模块版本
      }
      
  2. 避免整体导入大型库

    • 对于 Lodash 等工具库,使用按需导入:

      // 错误:导入整个 lodash
      import _ from 'lodash';
      _.debounce(...);
      
      // 正确:按需导入,支持 Tree shaking
      import { debounce } from 'lodash-es';
      
六、验证 Tree shaking 效果
  1. 分析打包结果

    • 使用 Webpack Bundle Analyzer 等工具查看哪些代码被保留:

      # 安装分析工具
      npm install --save-dev webpack-bundle-analyzer
      
      # 配置 webpack.config.js
      const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
      
      module.exports = {
        plugins: [new BundleAnalyzerPlugin()],
      };
      
  2. 检查生成的代码

    • 手动查看打包后的文件,确认未使用的代码是否被移除。
总结:关键条件清单
  1. 使用 ES6 模块语法import/export)。
  2. 确保代码 无副作用,或在 package.json 中声明。
  3. 启用 生产模式 和 压缩工具
  4. 编写 可 Tree-shake 的代码结构(按需导入、具名导出)。
  5. 选择 支持 ES6 模块的第三方库

工作流程

  1. 标记哪些导出值没有被使用;
  2. 使用Terser将这些没用到的导出语句删除

标记的流程如下:

  1. make阶段:收集模块导出变量并记录到模块依赖关系图中
  2. seal阶段:遍历模块依赖关系图并标记那些导出变量有没有被使用
  3. 构建阶段:利用Terser将没有被用到的导出语句删除

开发中如何利用Tree shaking?

  • 避免无作用的重复赋值
  • 使用 #pure标记函数无副作用(这种做法在开源项目的源码中经常出现,如pinia、reactive....等
  • 禁止转译 导入/导出语句(使用了babel-loader需要将 babel-preset-envmodules配置为false
  • 使用支持 Tree shaking的包
  • 优化导出值的粒度
//正确做法
const a = 'a';
const b = 'b';
export {
    a,
    b
}
//错误做法
export default {
    a: 'a',
    b: 'b'
}

长缓存?如何做到长缓存优化

1、什么是长缓存

浏览器在用户访问页面的时候,为了加快加载速度,会对用户访问的静态资源进行存储,但是每一次代码升级或者更新,都需要浏览器去下载新的代码,最方便的更新方式就是引入新的文件名称,只下载新的代码块,不加载旧的代码块,这就是长缓存。

2、具体实现

在Webpack中,可以在output给出输出的文件制定chunkhash,并且分离经常更新的代码和框架代码,通NameModulesPlugin或者HashedModulesPlugin使再次打包文件名不变

按需加载

在Webpack中,import不仅仅是ES6module的模块导入方式,还是一个类似require的函数,我们可以通过import('module')的方式引入一个模块,import()返回的是一个Promise对象;使用import()方式就可以实现 Webpack的按需加载

神奇注释

在import()里可以添加一些注释,如定义该chunk的名称,要过滤的文件,指定引入的文件等等,这类带有特殊功能的注释被称为神器注释。

Source Map

Source Map是一种文件,它建立了构建后的代码与原始源代码之间的映射关系。通常在开发阶段开启,用来调试代码,帮助找到代码问题所在。

可以在Webpack配置文件中的devtool选项中指定devtool: 'source-map'来开启。

使用:

source map是将编译打包后的代码映射回源码 可以通过devtool配置项来设置,还可以通过SourceMapDevToolPlugin实现更加精细粒度的控制

devtool配置项和 SourceMapDevToolPlugin不能同时使用,因为devtool选项已经内置了这些插件,如果同时使用相当于应用了两次插件

devtoolperformanceproductionqualitycomment
(none)build: fastest rebuild: fastestyesbundleRecommended choice for production builds with maximum performance.
evalbuild: fast rebuild: fastestnogeneratedRecommended choice for development builds with maximum performance.
eval-cheap-source-mapbuild: ok rebuild: fastnotransformedTradeoff choice for development builds.
eval-cheap-module-source-mapbuild: slow rebuild: fastnooriginal linesTradeoff choice for development builds.
eval-source-mapbuild: slowest rebuild: oknooriginalRecommended choice for development builds with high quality SourceMaps.
cheap-source-mapbuild: ok rebuild: slownotransformed
cheap-module-source-mapbuild: slow rebuild: slownooriginal lines
source-mapbuild: slowest rebuild: slowestyesoriginalRecommended choice for production builds with high quality SourceMaps.
inline-cheap-source-mapbuild: ok rebuild: slownotransformed
inline-cheap-module-source-mapbuild: slow rebuild: slownooriginal lines
inline-source-mapbuild: slowest rebuild: slowestnooriginalPossible choice when publishing a single file
eval-nosources-cheap-source-mapbuild: ok rebuild: fastnotransformedsource code not included
eval-nosources-cheap-module-source-mapbuild: slow rebuild: fastnooriginal linessource code not included
eval-nosources-source-mapbuild: slowest rebuild: oknooriginalsource code not included
inline-nosources-cheap-source-mapbuild: ok rebuild: slownotransformedsource code not included
inline-nosources-cheap-module-source-mapbuild: slow rebuild: slownooriginal linessource code not included
inline-nosources-source-mapbuild: slowest rebuild: slowestnooriginalsource code not included
nosources-cheap-source-mapbuild: ok rebuild: slownotransformedsource code not included
nosources-cheap-module-source-mapbuild: slow rebuild: slownooriginal linessource code not included
nosources-source-mapbuild: slowest rebuild: slowestyesoriginalsource code not included
hidden-nosources-cheap-source-mapbuild: ok rebuild: slownotransformedno reference, source code not included
hidden-nosources-cheap-module-source-mapbuild: slow rebuild: slownooriginal linesno reference, source code not included
hidden-nosources-source-mapbuild: slowest rebuild: slowestyesoriginalno reference, source code not included
hidden-cheap-source-mapbuild: ok rebuild: slownotransformedno reference
hidden-cheap-module-source-mapbuild: slow rebuild: slownooriginal linesno reference
hidden-source-mapbuild: slowest rebuild: slowestyesoriginalno reference. Possible choice when using SourceMap only for error reporting purposes.

这么多的选择,那么我们应该如何使用呢,根据我的实践,我觉得比较好的设置应该是下面这样

  • 开发环境:cheap-eval-source-map,生产这种source map速度最快,并且由于开发环境下没有代码压缩,所以不会影响断点调试
  • 生产环境:hidden-source-map,由于进行了代码压缩,所以并不会占用多大的体积

避免在生产中使用 inline-eval- 因为它们会增加 bundle 体积大小 并且降低整体性能

webpack 5有哪些更新?

Webpack 5 的核心更新聚焦于性能优化、长期缓存、模块化架构升级和现代前端生态兼容,以下是关键特性的深度解析:

一、构建性能与缓存机制的革命性升级

1. 持久化缓存(Persistent Caching)
  • 核心机制:通过文件系统缓存(cache.type: 'filesystem')将编译结果持久化存储到磁盘,二次构建时跳过解析和编译流程,构建速度平均提升 80%38。

  • 安全策略

    • 依赖追踪:自动识别文件依赖(fileDependencies)、上下文依赖(contextDependencies),确保缓存失效的准确性3。
    • 快照策略:通过 snapshot 配置(timestamps/contenthash)判断文件变更,避免误判导致的缓存污染38。
  • 配置示例

    module.exports = {
      cache: {
        type: 'filesystem',
        cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'),
        buildDependencies: {
          config: [__filename] // 配置文件变更时清除缓存
        }
      }
    };
    
2. 长期缓存(Long-Term Caching)
  • 确定性 ID 算法

    • 模块 ID(moduleIds :生产模式默认使用 deterministic 算法,根据文件路径生成短哈希(如 915),避免因模块顺序变化导致缓存失效289。
    • Chunk ID(chunkIds :同样采用 deterministic 算法,确保拆分后的代码块哈希稳定28。
  • 哈希优化

    • contenthash 基于文件内容生成,而非仅依赖内部结构,即使注释或变量名变更也能保持哈希一致24。

二、模块化架构的颠覆性创新

1. 模块联邦(Module Federation)
  • 核心价值:支持跨应用共享模块,实现微前端架构下的代码解耦与动态加载19。

  • 典型场景

    • 独立开发:多个团队独立开发子应用,通过模块联邦动态集成。
    • 库共享:共享 React、Vue 等框架实例,避免重复打包。
  • 配置示例

    // 宿主应用(Host)
    new ModuleFederationPlugin({
      remotes: {
        remoteApp: 'http://remote-app.com/remoteEntry.js'
      }
    });
    
    // 远程应用(Remote)
    new ModuleFederationPlugin({
      name: 'remoteApp',
      exposes: { './Button': './src/components/Button' }
    });
    
2. 资源模块(Asset Modules)
  • 功能整合

    • asset/resource 替代 file-loader,输出独立文件并导出 URL。
    • asset/inline 替代 url-loader,内联为 Data URI。
    • asset 自动根据文件大小选择输出方式(默认 ≤8kb 内联)89。
  • 配置示例

    module.exports = {
      module: {
        rules: [
          {
            test: /.(png|jpe?g|gif)$/i,
            type: 'asset',
            parser: { dataUrlCondition: { maxSize: 4 * 1024 } } // 调整内联阈值
          }
        ]
      }
    };
    

三、代码优化与现代前端生态兼容

1. Tree Shaking 深度增强
  • 静态分析升级

    • 作用域分析:不仅检测变量是否被引用,还能识别嵌套属性访问,例如 utils.math.add 会保留 add 方法89。
    • CommonJS 支持:处理 module.exports 和 require 时,能识别未使用的导出8。
  • 配置建议

    • 在 package.json 中声明 sideEffects,避免误删有副作用的文件8。
    • 启用 usedExports 和 minimize,通过 Terser 移除未使用代码15。
2. Node.js Polyfills 按需引入
  • 重大变化

    • 不再自动注入 cryptofs 等 Node.js 核心模块的 Polyfills,需手动配置 fallback289。
  • 兼容方案

    module.exports = {
      resolve: {
        fallback: {
          crypto: require.resolve('crypto-browserify'),
          buffer: require.resolve('buffer')
        }
      }
    };
    

四、开发体验与现代标准支持

1. 开发模式优化
  • 可读性增强

    • 开发模式默认使用 named Chunk ID,输出文件命名更直观(如 main.[name].js)29。
    • 进度插件(progress-webpack-plugin)支持多维度统计(模块、依赖、入口),减少控制台输出频率2。
2. 现代标准支持
  • WebAssembly 原生支持:直接导入 .wasm 文件,无需额外配置18。
  • 动态导入(Dynamic Import) :优化对 import() 的处理,支持更灵活的代码分割18。
  • ES 模块兼容:严格遵循 ESM 规范,对 JSON 模块的默认导出行为进行标准化28。

五、性能优化实战策略

1. 构建速度优化
  • 并行处理

    • 使用 thread-loader 并行执行耗时的 Loader(如 Babel)6。
    • 配置 parallel: true 启用 Terser 多线程压缩6。
  • 解析优化

    • 通过 resolve.mainFields 指定模块入口(优先 module 字段,即 ESM 版本)510。
    • 缩短 resolve.extensions 列表,减少文件后缀匹配耗时6。
2. 包体积优化
  • 代码分割

    • 配置 optimization.splitChunks 按路由、库类型拆分代码,避免单文件过大15。
    • 使用 prefetch 和 preload 优化资源加载时机6。
  • Tree Shaking 强化

    • 避免全局变量污染,确保函数无副作用58。
    • 使用 lodash-es 等支持 ESM 的库,替代整体导入5。

六、兼容性与迁移指南

1. 破坏性变更
  • 移除旧特性

    • 移除 require.includeDllPlugin 等废弃 API29。
    • 不再支持 Node.js 8 及以下版本28。
  • 配置调整

    • 若需保留 Webpack 4 的 ID 生成逻辑,需手动配置 moduleIds: 'size'chunkIds: 'size'28。
2. 迁移步骤
  1. 检查依赖:使用 depcheck 移除未使用的包5。
  2. 升级 Loader:确保 babel-loadercss-loader 等依赖适配 Webpack 5。
  3. 配置缓存:启用 cache.type: 'filesystem' 并优化 buildDependencies38。
  4. 测试构建:使用 webpack-bundle-analyzer 验证包体积变化510。

总结:Webpack 5 的核心价值

  • 性能飞跃:持久化缓存和长期缓存机制显著减少重复构建耗时。

  • 架构革新:模块联邦和资源模块为微前端和现代资源管理提供原生支持。

  • 生态兼容:强化 ESM 支持,与 TypeScript、React 等工具链无缝集成。

  • 开发体验:更友好的错误提示、进度反馈和调试工具提升开发效率。

通过以上更新,Webpack 5 为现代前端项目提供了更高效、灵活的构建解决方案,尤其适合大型应用和微服务架构。开发者需重点关注缓存配置、模块联邦集成和 Tree Shaking 优化,以充分释放其性能潜力。

vite比webpack快在哪里

他们都是前端构建工具,但vite构建速度相对于webpack还是有一些速度优势

  • 冷启动速度vite是利用浏览器的原生ES moudle,采用按需加载的当时,而不是将整个项目打包。而webpack是将整个项目打包成一个或多个bundle,构建过程复杂。
  • HMR热更新vite使用浏览器内置的ES模块功能,使得在开发模式下的热模块替换更加高效,那个文件更新就加载那个文件。它通过WebSocket在模块级别上进行实时更新,而不是像Webpack那样在热更新时重新加载整个包。
  • 构建速度: 在生产环境下,Vite的构建速度也通常比Webpack快,因为Vite的按需加载策略避免了将所有代码打包到一个大文件中。而且,Vite对于缓存、预构建等方面的优化也有助于减少构建时间。
  • 缓存策略Vite利用浏览器的缓存机制,将依赖的模块存储在浏览器中,避免重复加载。这使得页面之间的切换更加迅速。
  • 不需要预编译Vite不需要预编译或生成中间文件,因此不会产生大量的临时文件,减少了文件IO操作,进一步提升了速度。

vite与webpack的定位以及关系

那前端社区中常谈到的这些工具webpackrollupparcelesbuildvitevue-clicreate-react-appumi他们之间的关系是怎样的。

  • webpackrollupparcelesbuild都是打包工具,代码写好之后,我们需要对代码进行压缩合并转换分割打包等操作,这些工作需要打包工具去完成。
  • vue-clicreate-react-appumi 是基于webpack的上层封装,通过简单的配置就可以快速创建出一个项目,把更多的时间放在业务开发上。
  • vite开发环境依赖esbuild进行预构建,生产环境则依赖rollup进行打包,并且充分利用了现代浏览器的特性,比如http2ES modulevite是站在众多巨人肩膀上的一个产物, 类似webpack + webpack-dev-server的结合体,是一个非常棒的前端项目的构建工具。

运行原理

首先,我们从运行原理上分析一下,vite为什么比webpack快。

webpack运行原理

n1PETD

当我们使用webpack启动项目时,webpack会根据我们配置文件(webpack.config.js) 中的入口文件(entry),分析出项目项目所有依赖关系,然后打包成一个文件(bundle.js),交给浏览器去加载渲染。

这样就会带来一个问题,项目越大,需要打包的东西越多,启动时间越长。

关于ES module

在讲vite运行原理之前,我们先说一下ES module

目前,绝大多数现代浏览器都已经支持ES module了, 我们只需要在<script>标签中添加type="module",就可以使用ES module了。

下面这段代码是可以直接在浏览器中运行的。

javascript
 体验AI代码助手
 代码解读
复制代码
// test.js
export default function hello() {
  console.log('hello world');
}

// index.html
<script type="module">
  import hello from './test.js';

  hello(); // hello world
</scirpt>

vite运行原理

EmFj1C

<script type="module">中,浏览器遇到内部的import引用时,会自动发起http请求,去加载对应的模块。

vite也正是利用了ES module这个特性,使用vite运行项目时,首先会用esbuild进行预构建,将所有模块转换为es module,不需要对我们整个项目进行编译打包,而是在浏览器需要加载某个模块时,拦截浏览器发出的请求,根据请求进行按需编译,然后返回给浏览器。

这样一来,首次启动项目(冷启动)时,自然也就比webpack快很多了,并且项目大小对vite启动速度的影响也很小。

构建方式

我们再来看一下,vite与webpack在项目构建上有哪些区别。

webpack

webpack是基于nodejs运行的,但js只能单线程运行,无法利用多核CPU的优势,当项目越来越大时,构建速度也就越来越慢了。

vite

vite预构建按需编译的过程,都是使用esbuild完成的。

esbuild是用go语言编写的,可以充分利用多核CPU的优势,所以vite开发环境下的预构建按需编译速度,都是非常快的。

http2

vite充分利用了http2可以并发请求的优势,这也是速度快的一个主要原因。 接下来,我们了解一下http2的来龙去脉。

在之前http1的时候,浏览器对同一个域名的请求,是有并发限制的,一般为6个,如果并发请求6个以上,就会造成阻塞问题,所以在http1的时代,我们要减少打包产物的文件数量,减少并发请求,来提高项目的加载速度。

2015年以后,http2出现了,他可以并发发送多个请求,不会出现http1的并发限制。这时候,将打包产物分成多个小模块,并行去加载,反而会更快。

vite也充分利用了这一优势,对项目资源进行了合理的拆分,访问项目时,同时加载多个模块,来提升项目访问速度。

热更新

vite速度快的另一个原因是与webpack不同的热更新机制。

我们首先来了解一下什么是HMR。

模块热替换(hot module replacement - HMR),该功能可以实现应用程序运行过程中,替换、添加或删除模块,而无需重新加载整个页面,也就是我们常说的热更新

vite与webpack虽然都支持HMR,但两个工具的实现原理是不一样的。

webpack

webpack项目中,每次修改文件,都会对整个项目重新进行打包,这对大项目来说,是非常不友好的。

虽然webpack现在有了缓存机制,但还是无法从根本上解决这个问题。

vite

vite项目中,监听到文件变更后,会用websocket通知浏览器,重新发起新的请求,只对该模块进行重新编译,然后进行替换。

并且基于es module的特性,vite利用浏览器的缓存策略,针对源码模块(我们自己写的代码)做了协商缓存处理,针对依赖模块(第三方库)做了强缓存处理,这样我们项目的访问的速度也就更快了。

生产环境(为什么使用rollup打包)

vite生产环境下,为什么使用rollup打包呢?

Rollup 是一款 ES Module 打包器, 从作用上来看,RollupWebpack 非常类似。不过相比于 WebpackRollup要小巧的多,打包生成的文件更小。 因为小巧,自然在这种特定的打包环境下,Rollup的打包速度也要比 Webpack 快很多。

vite正是基于es module的特性实现的,所以使用rollup要更合适一些。

vite生产环境下,为什么不用esbuild打包呢?

尽管esbuild的打包速度比rollup更快,但 Vite 目前的插件 API 与使用 esbuild 作为打包器并不兼容,rollup插件api与基础建设更加完善,所以在生产环境vite使用rollup打包会更稳定一些。

如果后面esbuild基础建设与生态更加完善后,esbuild还是更有优势的。

所以使用vite可能会带来开发环境与生产环境打包结果不一致的问题。

使用成本

除了速度上的区别,我们再分析一下,vite与webpack的使用成本。

webpack

如果我们使用webpack自己去搭建项目脚手架时,需要配置比较多的东西, 比如:跨域代码压缩代码分割css预处理器的代码转换样式兼容性vue/react代码解析图片压缩代码热更新es降级ts转换等等,远不止这些。

概念和配置项太多,我们需要了解各种loader、plugin的使用,并且需要根据项目场景,对配置不断进行优化,心智负担太大。

所以就出现了一些基于webpack上层封装的脚手架,如:vue-clicreate-react-appumi等。

vite

vite对我们常用功能都做了内置,比如:css 预处理器html 预处理器hash 命名异步加载分包压缩HMR等等,我们可以很轻松的通过配置项去配置。

esbuild

blog.csdn.net/weixin_4386…

什么是单点登录?

单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一

SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统

SSO 一般都需要一个独立的认证中心(passport),子系统的登录均得通过passport,子系统本身将不参与登录操作

当一个系统成功登录以后,passport将会颁发一个令牌给各个子系统,子系统可以拿着令牌会获取各自的受保护资源,为了减少频繁认证,各个子系统在被passport授权以后,会建立一个局部会话,在一定时间内可以无需再次向passport发起认证

Monorepo的理解

Monorepo是一种将多个项目代码存储在一个仓库的代码管理方式,将不同的项目代码放在一个仓库中,优缺点如下: 优点:

  • 更好的实现代码复用,方便代码管理
  • 可以复用项目基础设施,不需要每个项目都建立一遍
  • 更好的CR
  • 子项目模块之间的关系更加透明,可以实现一次命令完成所有部署。
  • 管理依赖变得更加简单,可以在一个统一的环境中处理依赖库的版本和更新。
  • 一种开放,透明,共享的组织文化,这有利于开发者成长,代码质量的提升

缺点:

  • 代码权限管理变得很复杂
  • 项目上手学习成本高,需要了解子项目之间的依赖关系
  • 项目基础建设、依赖管理、代码搜索、分支模型等技术要求会很高
  • 所以代码放在一次,会导致项目仓库体积过大,clone等操作会变很慢

你在项目是怎么做Monorepo?

常见的Monorepo实现:

  • yarn/pnpm/npm包管理工具下使用workspace功能,在package.josn下声明workspace
  • lerna具备基本的任务调度能力,支持一键构建、发布等,但是不支持按需构建、不支持离线缓存
  • rush stack:微软开源的 monorepo 管理工具
  • nx
  • bazel

为什么pnpm快?

npm的问题

npm@3之前安装依赖时会出现依赖之间互相嵌套,就像树结构一样一层一层,过深的层级嵌套会带来大量重复的文件,有些依赖会重复安装,占用磁盘空间。

json
 代码解读
复制代码
node_modules
└─ a
   ├─ index.js
   ├─ package.json
   └─ node_modules
      └─ b
         ├─ index.js
         └─ package.json

而在npm@3时出现了yarn,它们的出现是为了解决2之前的问题,这时候开始引入扁平化处理依赖嵌套,也就是将所有的依赖都放在一个node_modules下,依赖在统一层级下互相引用,这样是解决了之前的一些问题,但也导致了新的问题出现就是幽灵依赖

json
 代码解读
复制代码
node_modules
├─ a
|  ├─ index.js
|  └─ package.json
└─ b
   ├─ index.js
   └─ package.json

幽灵依赖是指项目中使用了一些没有在package.json中定义的包。比如A库依赖B库,那么这两个库都会平铺到node_modules下。如果项目中使用了B库,然后在package.json定义了进行安装,所以可以直接访问。假如某天项目不需要A库或者将A库删除,此时B库就会因为找不到A库而跑不起来。

pnpm

pnpm 是一种替代 npmYarn 的包管理器,用于管理 JavaScript 项目中的依赖关系。与传统的 npmYarn 不同,pnpm 提供了一种更为创新的方式来管理依赖项。

在项目安装依赖时,pnpm会将所有的依赖包存储在磁盘的某一个位置简称pnpm store查看存储位置】,下次遇到相同的包时会,如果pnpm store中已经存在这个包,就会从pnpm store创建一个硬连接到node_modules/.pnpm下对应的依赖。这样即使多个项目都依赖一个包,也只会在本地存一份代码,不会占用额外的磁盘空间。

而且pnpmnode_module并不是平铺的,通过package.json安装的依赖通过软连接到./.pnpm中,并且在安装时会判断pnpm store中是否有相对应的包,如果有就创建硬连接到./.pnpm

json
 代码解读
复制代码
node_modules
├── a -> ./.pnpm/a@1.0.0/node_modules/a
└── .pnpm
    ├── b@1.0.0
    │   └── node_modules
    │       └── b -> <store>/b
    └── a@1.0.0
        └── node_modules
            ├── a -> <store>/foo
            └── b -> ../../bar@1.0.0/node_modules/bar

这样做带来的好处就是:

  • 节约磁盘空间pnpm 的链接依赖方式可以减少磁盘占用。相同的依赖包在不同项目中共享,不会造成重复存储。

  • 提升安装速度:由于依赖都是同一管理在pnpm store中,所以相同依赖不需要重复下载,这样使得pnpm 的安装和构建速度相对更快,特别是在项目中存在大量依赖项时。

  • 创建非扁平的node_modules目录:由于npmyarn安装依赖时所有包都会提升到根目录下,会造成幽灵依赖和依赖安全问题,而pnpm采用非扁平的形式,有效解决。