0


【前端工程化】深入浅出vite(一)--vite的优点及原理、性能优化

Vite 需要 Node.js 版本 14.18+,16+。然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本。

背景

webpack

支持多种模块化,将不同模块的依赖关系构建成依赖图来进行统一处理,当构建的项目越来越大时,需要处理的

JS

代码也越来越多,通常需要很长时间才可以启动开发服务器,即使使用模块热替换(

HMR

),修改文件也需要几秒钟才能在浏览器中反映出来,影响了开发效率和幸福感。

Vite

可以解决上述问题,它支持

ESM

规范,所以并不需要遍历依赖图,而是按需加载各种文件。

vite和webpack开发环境下的差异

初体验

mkidr vite-demo
cd vite-demo
npm init -y
npm i lodash
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>Vite demo</title><linkrel="stylesheet"href="./index.css"></head><scriptsrc="./index.js"type="module"></script><body><h1>Hello Vite</h1></body></html>
// index.jsimport{ name }from'./test'
console.log(name)
// test.jsimport _ from'lodash'exportconst name ='Armouy';
console.log(_)
html, body{margin: 0;padding: 0;}

如果借助

vscode

中的

live server

插件启动这个项目,会报错:

GET http://127.0.0.1:5500/test net::ERR_ABORTED 404(Not Found)

借助

vite

npm i vite --save-dev

修改

package.json

:

"scripts":{"dev":"vite","build":"vite build"},

执行

npm run dev

,控制台输出成功~

Vite

组成

vite

主要有以下两个功能:

  • 一个开发服务器,基于原生ESM模块,省略了编译耗时,提供高效的模块热更新;
  • 使用Rollup打包,支持配置,可输出用于生成环境的高度优化过的静态资源。

特点

vite

的最大的优点就是快,主要体现在以下两个方面:启动时间快和请求效率高。

启动时间快的原因

  • 无需在启动前构建依赖关系图:vite采用ESM规范方式提供源码,只有在浏览器请求时才会进行转换,即按需提供;
  • 预构建:vite在启动服务时会先预构建源码,将遇到的CommonJS或者UMD等模块化代码转为ESM规范,并保存在node_module/.vite/deps文件夹中;遇到多个ESM模块,会转为一个模块,比如lodash-es,不进行预构建,会一次性请求600次,预构建之后只会请求一次。

p.s. 对于

node_moduls

,浏览器时不支持

ESM

规范去请求它们的,如果支持的话会带来很大的网络性能问题,对于

ESM

里面具有其他依赖

ESM

的话,那么浏览器将会无限制地请求依赖库。
p.s. 在

vite

中也可以借助

optimizeDeps.exclude

字段来忽视某些内容的预构建。

请求效率高的原因

  • 前面提到过,预构建的时候会将构建好结果缓存在node_module/.vite/deps文件夹中,vite中利用了HTTP头来优化加载效率,源码模块的请求会根据304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求;
  • HMR热更新:(跟webpack一样,vite也有热更新机制,做法有些不一样,我们后面再讲)当编辑一个文件时,vite只需要精确地使已编辑模块与最近的HMR边界直接的链失活,就可以保持快速更新。

Vite

如何识别文件

webpack

中通过借助

loader

去读取各种文件。而

vite

对于

css

的处理有自己的一套方式,在

vite

搭建

vue+ts

的项目中,

css

的引入最后都会转为这种模式:
在这里插入图片描述
在这里插入图片描述
首先

vite

会使用

fs

模块读取.

css

文件的内容,然后创建一个

style

标签,将内容都怼到

style

标签内,再将这个标签插入到

index.html

中,最后还会将

.css

文件的内容转为

JS

脚本,便于

css

模块化和热更新,还避免了第三方工具对css的处理,提高了编译性能。

对于其他静态资源文件,除了

svg

vite

都是做到了开箱即用,引入即可使用(

svg

会对路径进行处理,需要区分是按照图片加载,还是按照

svg

加载)。

npm run dev

的源码分析

本来想先写热更新等其他内容,想了想,还是先把源码分析写完,方便后面进行解释。
首先先说一下调试的步骤,这里直接在已有的项目中,配置了调试文件

launch.json

program

指向了

vite

所在的路径(是的我用的

vscode

):

{// 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387"version":"0.2.0","configurations":[{"type":"node","request":"launch","name":"启动程序","skipFiles":["<node_internals>/**"],"program":"${workspaceFolder}\\node_modules\\vite\\bin\\vite.js","outFiles":["${workspaceFolder}/**/*.js"]}]}

然后打断点开始调试,指向

npm run dev

的时候会指向

vite/bin/vite.js

,其中又指向了

dist/node/cli.js

,根据源码可知,入口文件是哪个了~
npm run dev入口文件

asyncfunctioncreateServer(inlineConfig ={}){return_createServer(inlineConfig,{ws:true});}asyncfunction_createServer(inlineConfig ={}, options){const config =awaitresolveConfig(inlineConfig,'serve');// ...}

故一路分析下来,可以从

resolveConfig

开始看源码了:

asyncfunctionresolveConfig(inlineConfig, command, defaultMode ='development', defaultNodeEnv ='development'){// .../**
  * 先加载项目目录的配置文件,
  * 即vite.config.js、vite.config.mjs、vite.config.ts、vite.config.cjs其中一个
  * 如果找不到会报错
  * 如果找到了会对自定义配置和用户配置做合并
  */const loadResult =awaitloadConfigFromFile(configEnv, configFile, config.root, config.logLevel);if(loadResult){
      config =mergeConfig(loadResult.config, config);
      configFile = loadResult.path;
      configFileDependencies = loadResult.dependencies;}// .../**
  * 调整插件顺序,在vite中可以通过enforce: 'pre | post'来指定顺序,一般插件的顺序如下
  * 1. alias
  * 2. 带有enforce: pre的用户插件
  * 3. vite核心插件
  * 4. 没有带enforce值的用户插件
  * 5. vite构建用的插件
  * 6. 带有enforce: post的用户插件
  * 7. vite后置构建插件(最小化、manifest、报告等)
  */const[prePlugins, normalPlugins, postPlugins]=sortUserPlugins(rawUserPlugins);// 这里的config字段,可以在自定义插件的时候改写vite的一些配置
  config =awaitrunConfigHook(config, userPlugins, configEnv);// If there are custom commonjsOptions, don't force optimized deps for this test// even if the env var is set as it would interfere with the playground specs.if(!config.build?.commonjsOptions &&
        process.env.VITE_TEST_WITHOUT_PLUGIN_COMMONJS){
        config =mergeConfig(config,{optimizeDeps:{disabled:false},ssr:{optimizeDeps:{disabled:false}},});
        config.build ??(config.build ={});
        config.build.commonjsOptions ={include:[]};}// .../**
  * 处理alias,比如我配置了@指向src的配置,那么最后结果就是
  * find:
  * '@' replacement: 'D:\\code\\new\\vue-admin\\src'
  */const resolvedAlias =normalizeAlias(mergeAlias(clientAlias, config.resolve?.alias ||[]));// .../**
  * 设置环境变量:
  * 读取环境变量,读取的优先级分别是 .env.[mode].local、.env.[mode]
  * 如果不存在对应 mode 的配置文件,则会尝试去寻找 .env.local、.env 配置文件
  * 读取到配置文件后,使用 doteenv 将环境变量写入到项目中;如果这些环境变量配置文件都不存在的话,则会返回一个空对象
  */const userEnv = inlineConfig.envFile !==false&&loadEnv(mode, envDir,resolveEnvPrefix(config));// ...// 整理构建配置const resolvedBuildOptions =resolveBuildOptions(config.build, logger, resolvedRoot);// ...// resolvedConfig 是最后要导出的配置项const resolvedConfig ={/** 省略 */}const resolved ={...config,...resolvedConfig,};// ...return resolved;}

分析完

resolveConfig

可以回到

_createServer

方法:

asyncfunction_createServer(inlineConfig ={}, options){// 拿到了配置项const config =awaitresolveConfig(inlineConfig,'serve');// ...以下讲解忽略服务端渲染// 如果不是服务渲染,会创建一个http server用户本地开发调试,同时创建一个webscoket用于热重载const httpServer = middlewareMode
        ?null:awaitresolveHttpServer(serverConfig, middlewares, httpsOptions);const ws =createWebSocketServer(httpServer, config, httpsOptions);// ...// 监听本地项目文件的变动const watcher = chokidar.watch([root,...config.configFileDependencies, config.envDir], resolvedWatchOptions);// 创建了一个server对象const server ={/** 可以启动本地开发服务,也可以负责热重载  */}/**
  * 当文件发生改变的时候就会被wather监听到,然后调用onHMRUpdate方法
  * 而onHMRUpdate内部调用了handleHMRUpdate
  * handleHMRUpdate会触发插件热更新的钩子,去编译更新文件
  * 如果是需要全量更新则发送full-reload,否则发送update
  * 当客户端接收到full-reload则启动本地刷新,通过http加载全部资源(这里做了协商缓存)
  * 如果客户端接收的是update,则表示启动hmr,浏览器按需加载对饮的模块就行
  */
  watcher.on('change',async(file)=>{
        file =normalizePath$3(file);// invalidate module graph cache on file change
        moduleGraph.onFileChange(file);awaitonHMRUpdate(file,false);});// 在不同的生命周期指调用不同的插件...// 对内部中间件的处理...通过 connect 库提供开发服务器,通过中间件机制实现多项开发服务器配置// 最后就是进行预构建依赖,使用esbuild预构建它们,并将 CommonJS / UMD 转换为 ESM 格式constinitServer=async()=>{// ..if(isDepsOptimizerEnabled(config,false)){awaitinitDepsOptimizer(config, server);}// ..})();return initingServer;};// ...}

关于

npm run build

,其实流程也差不多,源码就不逐行分析了(有兴趣自行查看~):

  • 使用resolveConfig收集了命令行配置、读取配置文件并合并配置;
  • 处理插件的的顺序、合并插件并设置环境变量;
  • 生成rollup构建配置;
  • 使用rollup编译产物并输出到指定目录。

预构建原理

vite

借助

esbuild

进行了预构建,转换

ts、jsx、tsx

等,预构建有两种操作方式:

  1. 可以通过命令行vite optimize手动预解析
  2. createServernpm run dev会执行)中也会进行预构建

从源码上可以看出,主要流程简单概括即:

  • 对项目中的依赖进行扫描,并存放在一个deps中;
  • 先把deps拍平成一维数组,调用build.context进行打包,输出打包的映射对象result
  • 最后得到的依赖结果会被缓存起来,并存放在node_module/.vite/deps目录下。

node_module/.vite/deps

下有一个

_metadata.json

,这个文件可以根据请求路径,找到对应的预构建之后的文件:

{"hash":"2266f73a","browserHash":"20efa1a5","optimized":{"@element-plus/icons-vue":{"src":"../../@element-plus/icons-vue/dist/index.js","file":"@element-plus_icons-vue.js","fileHash":"32dcf906","needsInterop":false},"axios":{"src":"../../axios/index.js","file":"axios.js","fileHash":"2061b9bb","needsInterop":false},"element-plus":{"src":"../../element-plus/es/index.mjs","file":"element-plus.js","fileHash":"123c11eb","needsInterop":false},//...},"chunks":{"chunk-5OBJFL24":{"file":"chunk-5OBJFL24.js"},// ...}}

热更新(

HMR

)原理

热更新指的是自动对页面上更改的模块进行替换,以达到刷新页面数据的效果,这个效果甚至是无感的。由上面的源码不难看出:

  • vite先执行createWebSocketServer创建一个webscoket服务端,并监听change事件;
  • vite在创建client.mjs文件时,会合并合并UserConfig配置,通过transformIndexHtml钩子函数,在转换index.html,这时会将client的代码注入到index.html中,这样浏览器访问index.html就会加载client生成代码,创建clientwebscoket的的链接,便于接收webscoket服务端消息;
  • 当服务端监听文件变化的时候,就会给client发送消息,同时调用服务端调用onHMRUpdate函数,该函数会根据此次修改文件的类型,通知客户端是要刷新还是重新加载文件。

打包不选择

Esbuild

的原因

由于生产环境中,

ESM

的机制会导致额外的网络往返导致效率低下,故最好还是将代码进行进行

 tree-shaking

、懒加载和

chunk

分割(以获得更好的缓存)。

Vite

目前的插件

API

与使用

esbuild

作为打包器并不兼容。尽管

esbuild

速度更快,但

Vite

采用了

Rollup

灵活的插件

API

和基础建设,

Rollup

提供了更好的性能与灵活性方面的权衡。

性能优化

分包策略

正常情况下我们项目打包的时候,业务代码会跟第三方代码糅合咋一起,如果业务代码经常变,就会导致每次打包都会重新打包所有文件,会造成浏览器缓存失效,如果使用分包策略,将第三方代码单独打包,那么就可以解决上述问题:

// vite.config.jsexportdefaultdefineConfig({...build:{rollupOptions:{output:{assetFileNames:"[name]-[hash].[ext]",manualChunks:(id)=>{if(id.indexOf('node_modules')!==-1){return'vendor'}}}},},})

gzip

压缩策略

服务端对资源进行压缩,浏览器对资源进行解压。但是要注意,如果如果文件不是很大的话,开启这个插件,反而会由于浏览器解压文件,浪费时间。

import viteCompression from'vite-plugin-compression';exportdefault defineConfig ({...plugins:[viteCompression()],})

cdn

加速

cdn

策略简单讲就是多台服务器具有该资源,用户请求的时候会优先请求距离你距离最近的一台资源。跟

webpack

的优化策略一样,这里的前提是公司比较有钱哈哈哈,不然靠免费的cdn可能会遇到“挂掉”的风险。

总结

本文学习了

vite

的优点、简单分析了优点背后实现的原理,以及还有性能优化的方案。

参考

  • vite官网
  • vite源码
  • 晒兜斯(他的Vite系列很干货)
  • 前端构建工具vite进阶系列

如有错误,欢迎指出,感谢阅读~

标签: 前端

本文转载自: https://blog.csdn.net/qq_34086980/article/details/131322813
版权归原作者 Armouy 所有, 如有侵权,请联系我们删除。

“【前端工程化】深入浅出vite(一)--vite的优点及原理、性能优化”的评论:

还没有评论