深入浅出vite(一)--vite的优点及原理、性能优化

news2024/11/19 7:42:37

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
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vite demo</title>
  <link rel="stylesheet" href="./index.css">
</head>
<script src="./index.js" type="module"></script>
<body>
  <h1>Hello Vite</h1>
</body>
</html>

// index.js
import { name } from './test'
console.log(name)
// test.js
import _ from 'lodash'
export const 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的处理,提高了编译性能。

对于其他静态资源文件,除了svgvite都是做到了开箱即用,引入即可使用(svg会对路径进行处理,需要区分是按照图片加载,还是按照svg加载)。

npm run dev的源码分析

本来想先写热更新等其他内容,想了想,还是先把源码分析写完,方便后面进行解释。
首先先说一下调试的步骤,这里直接在已有的项目中,配置了调试文件launch.jsonprogram指向了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入口文件

async function createServer(inlineConfig = {}) {
    return _createServer(inlineConfig, { ws: true });
}
async function _createServer(inlineConfig = {}, options) {
    const config = await resolveConfig(inlineConfig, 'serve');
    // ...
}

故一路分析下来,可以从resolveConfig开始看源码了:

async function resolveConfig(inlineConfig, command, defaultMode = 'development', defaultNodeEnv = 'development') {
  // ...
  
  /**
  * 先加载项目目录的配置文件,
  * 即vite.config.js、vite.config.mjs、vite.config.ts、vite.config.cjs其中一个
  * 如果找不到会报错
  * 如果找到了会对自定义配置和用户配置做合并
  */ 
  const loadResult = await loadConfigFromFile(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 = await runConfigHook(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方法:

async function _createServer(inlineConfig = {}, options) {
  // 拿到了配置项
  const config = await resolveConfig(inlineConfig, 'serve');
  
  // ...以下讲解忽略服务端渲染
  
  // 如果不是服务渲染,会创建一个http server用户本地开发调试,同时创建一个webscoket用于热重载
  const httpServer = middlewareMode
        ? null
        : await resolveHttpServer(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);
        await onHMRUpdate(file, false);
    });
    
  // 在不同的生命周期指调用不同的插件...
  
  // 对内部中间件的处理...通过 connect 库提供开发服务器,通过中间件机制实现多项开发服务器配置
  
  // 最后就是进行预构建依赖,使用esbuild预构建它们,并将 CommonJS / UMD 转换为 ESM 格式
  const initServer = async () => {
    // ..
    if (isDepsOptimizerEnabled(config, false)) {
      await initDepsOptimizer(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.js
export default defineConfig({
  ...
  build:{ 
    rollupOptions:{ 
      output:{ 
        assetFileNames:"[name]-[hash].[ext]", 
        manualChunks:(id)=>{ 
          if(id.indexOf('node_modules') !== -1){
            return 'vendor' 
          }
        }
      }
    },
  },
})



gzip压缩策略

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

import viteCompression from 'vite-plugin-compression';

export default defineConfig ({
    ...
    plugins: [viteCompression()],
}

cdn加速

cdn策略简单讲就是多台服务器具有该资源,用户请求的时候会优先请求距离你距离最近的一台资源。跟webpack的优化策略一样,这里的前提是公司比较有钱哈哈哈,不然靠免费的cdn可能会遇到“挂掉”的风险。

总结

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

参考

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/669969.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DevExpress WinForms功能区组件,让业务应用创建更轻松!(下)

DevExpress WinForms的Ribbon&#xff08;功能区&#xff09;组件灵感来自于Microsoft Office&#xff0c;并针对WinForms开发人员进行了优化&#xff0c;它可以帮助开发者轻松地模拟当今最流行的商业生产应用程序。 在上文中&#xff08;点击这里回顾>>&#xff09;&am…

Axure教程—中继器菜单

本文将教大家如何用AXURE中的中继器制作菜单&#xff08;不自动折叠其他菜单&#xff09; 一、效果 预览地址&#xff1a;https://8ao8gl.axshare.com 二、功能 1、点击菜单出现相应的子菜单 2、子菜单如果想折叠&#xff0c;点击相应的菜单 三、制作 &#xff08;1&#xff…

Java-@Transactional注解超详细

本文已收录于专栏 《Java》 目录 本文前言概念说明使用说明底层实现注意事项注解扩展总结提升 本文前言 Transactional注解是Spring框架中用于声明式事务管理的关键注解。本文将深入探讨Transactional注解的作用、使用方式和常见属性&#xff0c;并结合代码实例演示其在实际项目…

Adobe PS 2023、Adobe Photoshop 2023下载教程、安装教程

最后附下载地址 Adobe Photoshop 简介&#xff1a; Adobe Photoshop是一款广泛使用的图像处理软件&#xff0c;由Adobe公司开发。它提供了许多强大的工具和功能&#xff0c;可以用于图像编辑、合成、修饰、设计等各个领域。用户可以使用Photoshop来调整图像的亮度、对比度、色…

《水经注地图服务》如何快速发布墨卡托DAT缓存

《水经注地图服务》的快速发布功能是一个能够帮助用户实现快速发布地图服务的功能&#xff0c;并且提供常规情况下大多数用户所需的默认配置&#xff0c;让用户在发布地图时更加便捷。 前面为大家分享了《水经注地图服务》快速发布经纬度DAT缓存以及如何在水经微图中加载&…

项目管理甘特图,怎么做才能更高效?(附甘特图详细制作教程和模板)

如何制作项目管理的甘特图&#xff1f;给大家放几个模板感受下&#xff1a; 01 项目管理Excel套表 02 工程项目流程甘特图 03 项目进度横道图 04 生产制造排程规划图 05 项目日程表 06 项目进度计划表 甘特图制作教程&#xff0c;一共两种方法&#xff0c;大家按需选择&#x…

android注入so或者dex

本程序分为32位和64位&#xff0c;以及so中加载apk&#xff08;或者dex都可以&#xff09;。 代码地址&#xff1a;点击下载 &#xff08;一&#xff09;so注入 32位和64位so注入代码几乎相同&#xff0c;因此仅以32位为例说明so注入的过程。 arm64-v8a架构可以兼容armeabi…

SLF4J门面日志框架源码探索 | 京东云技术团队

1 SLF4J介绍 SLF4J即Simple Logging Facade for Java&#xff0c;它提供了Java中所有日志框架的简单外观或抽象。因此&#xff0c;它使用户能够使用单个依赖项处理任何日志框架&#xff0c;例如&#xff1a;Log4j&#xff0c;Logback和JUL(java.util.logging)。通过在类路径中…

单片机Hard fault 产生原因和错误跟踪的方法

一、单片机 Hard fault产生的原因 Hard fault产生的原因有两方面&#xff0c;硬件方面和软件方面。 ①硬件方面常见原因&#xff1a; 电源设计有错误&#xff0c;造成器件供电不稳&#xff1b; 电源质量不好&#xff0c;纹波&#xff0c;噪声过大&#xff1b; 器件接地不良&…

干货分享|HOOPS Web平台和Polygonica进行增材制造的云CAM服务示例

这篇文章提供了一个示例项目&#xff0c;展示了使用 Machineworks Polygonica 和 HOOPS Web 平台进行增材制造的云 CAM 服务。该项目作为一个示例&#xff0c;说明了如何在服务器端使用 Polygonica 与 HOOPS Communicator 和 Exchange 来开发云服务。 它涵盖了增材制造 CAM 的…

Android问题笔记-Android Studio编译报错:2 files found with path.....

点击跳转>Unity3D特效百例点击跳转>案例项目实战源码点击跳转>游戏脚本-辅助自动化点击跳转>Android控件全解手册点击跳转>Scratch编程案例点击跳转>软考全系列 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&…

看物联网技术ZETA如何帮助场馆实现数智化管理升级?

背景介绍&#xff1a; 江宁足球训练基地位于南京江宁区上坊镇境内,是江苏省女足及青少年足球发展基地。该基地总占地面积为333300平方米&#xff0c;其中房屋建筑面积有19000平方米&#xff0c;健身房350平方米&#xff0c;拥有9个标准足球场&#xff0c;曾承办多场甲级足球赛…

Java多线程与并发

1、JDK版本的选择 选择JDK8、JDK11进行讲解的原因&#xff1a;Oracle长期支持 2、进程和线程的区别 进程和线程的由来 3、进程与线程的区别 进程是资源分配的最小单位,线程是cpu调度的最小单位. 所有与进程相关的资源&#xff0c;都被记录在PCB(进程控制块)中。进程是抢占…

day08 教你用英语过海关

前言 &#x1f3e0;个人主页&#xff1a;我是沐风晓月 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是沐风晓月&#xff0c;阿里云社区博客专家 &#x1f609;&#x1f609; &#x1f495; 座右铭&#xff1a; 先努力成长自己&#xff0c;再帮助更多的人,一起加油进…

基于深度学习FasterRCNN模型Restnet50 的生活垃圾智能分类(准确率达84%)-含python工程全源码

目录 前言总体设计系统整体结构图系统流程图 运行环境1. 硬件环境2. Python 环境 模块实现1. 数据预处理2. 数据加载3. 模型构建4. 模型训练及保存5. 模型加载与调用 系统测试1. 模型准确率2. 分类别准确率 工程源代码下载其它资料下载 前言 本项目基于Faster R-CNN模型&#…

mmrotate框架基本使用

1、如何将类交给mmrotate框架容器管理 容器&#xff1a;框架中现有基本容器包括DATASETS, BACKBONES, LOSSES, DETECTORS。初始化容器&#xff1a;Registry(‘backbone’)中’backbone’为容器初始化配置文件。#/mmdet/models/builder.py 部分代码 from mmcv.utils import Re…

【Lisp】【Python】在CAD中用插件获取选中字块的文字,在rhino中批量生成图层

文章目录 1 get_selected_text.lsp1.1 使用方法LISP代码解析1.2 动图 2 Rhino中使用PythonScript批量建立图层.py2.1 直接生成2.2 带颜色生成 2.3 动图展示 1 get_selected_text.lsp 1.1 使用方法 用记事本复制以下代码&#xff0c;改文件名为get_selected_text.lsp (defun c:…

springboot高校宿舍报修管理系统-计算机毕设 附源码83946

springboot高校宿舍报修管理系统 摘 要 科技进步的飞速发展引起人们日常生活的巨大变化&#xff0c;电子信息技术的飞速发展使得电子信息技术的各个领域的应用水平得到普及和应用。信息时代的到来已成为不可阻挡的时尚潮流&#xff0c;人类发展的历史正进入一个新时代。在现实…

【AI绘图 丨 Stable_diffusion 系列教程四】— Window 环境 | Stable Diffusion入门教程 及安装(全篇)

&#x1f449;腾小云导读 最近&#xff0c;AI图像生成引人注目&#xff0c;它能够根据文字描述生成精美图像&#xff0c;这极大地改变了人们的图像创作方式。Stable Diffusion作为一款高性能模型&#xff0c;它生成的图像质量更高、运行速度更快、消耗的资源以及内存占用更小&a…

MATLAB+JAVA的混合开发

近期项目中需要使用matlab跟java做混合开发。主要记录一下&#xff0c;此次开发遇到的问题点。 环境&#xff1a;使用的matlab版本是 R2018b。 当前状况&#xff1a;MATLAB代码已经编写好&#xff0c;且运行成功。需要打成jar包才可以被java调用。 步骤一&#xff1a; 按照…