(四)什么是Vite——冷启动时vite做了什么(源码、middlewares)

news2025/1/23 2:22:06

 vite分享ppt,感兴趣的可以下载:

​​​​​​​Vite分享、原理介绍ppt

什么是vite系列目录:

(一)什么是Vite——vite介绍与使用-CSDN博客

(二)什么是Vite——Vite 和 Webpack 区别(冷启动)-CSDN博客

(三)什么是Vite——Vite 主体流程(运行npm run dev后发生了什么?)-CSDN博客

(四)什么是Vite——冷启动时vite做了什么(源码、middlewares)-CSDN博客

(五)什么是Vite——冷启动时vite做了什么(依赖、预构建)-CSDN博客

未完待续。。。

冷启动时vite做了什么:

vite主要遵循的是使用 ESM ( Es modules 模块)的规范来执行代码,由于现代浏览器基本上都支持了 ESM 规范,所以在开发阶段并不需要将代码打包编译成 es5 模块即可在浏览器上运行。我们只需要从入口文件出发, 在遇到对应的 import 语句时,将对应的模块加载到浏览器中就可以了(按需加载)。同时 ts/jsx 等文件的转译工作也会借助了 esbuild 来提升速度。Vite在内部实现上,会启动一个 dev server , 并接受独立模块的HTTP请求,并让浏览器自身去解析和处理模块加载

模块解析

对于引用的模块,vite将其分为了 依赖 和 源码 两类,对其进行不同的处理。

先来看一下对于 源码 的处理。我们打开请求的App.vue文件,可以看到里面的内容已经不是源码了,而是经过处理后的代码。

middlewares 中内容转换

Vite 中源文件的转换是在 dev server 启动以后通过 middlewares 实现的。

当浏览器发起请求以后,dev sever 会通过相应的 middlewares (transformMiddleware 、indexHtmlMiddleware)对请求做处理,然后将处理以后的内容返回给浏览器。

middlewares 对源文件的处理,分为 resolve、load、transform、parser 四个过程:

  1. resolve - 解析 url,找到源文件的绝对路径;
  2. load - 加载源文件。如果是第三方依赖,直接将预构建内容返回给浏览器;如果是业务代码,继续 transform、parser。
  3. transfrom - 对源文件内容做转换,即 ts -> js, less -> css 等。转换完成的内容可以直接返回给浏览器了。
  4. parser - 对转换以后的内容做分析,找到依赖模块,对依赖模块做预转换 - pre transform 操作,即重复 1 - 4。
    1. pre transform 是 Vite 做的一个优化点。预转换的内容会先做缓存,等浏览器发起请求以后,如果已经完成转换,直接将缓存的内容返回给浏览器。

Vite 在处理步骤 3 时,是通过 esbuild.transform 实现的,对比 Webpack 使用各个 loader 处理源文件,那是非常简单、快捷的。

模块请求加载过程:一个请求的 Vite 之旅

当浏览器一个请求到vite服务时,发生了什么?

主要由以下两个中间件来统一处理请求的内容,并在中间件处理的流程中调用 vite 插件容器的相关钩子函数

  • transformMiddleware:核心中间件处理代码
  • indexHtmlMiddleware:html 相关请求处理中间件

GET localhost :实际 GET / => /index.html

当访问页面的时候,实际是有一个 GET / => /index.html 的重定向进入 indexHtmlMiddleware 这个过程,主要做了一件事情,注入 dev 环境需要的一些依赖,@vite/client 主要用来和服务器进行 ws 通信并处理一些 hmr 相关的工作。

GET client :实际 GET /@vite/client

这个请求会直接进入 transformMiddleware 中间件中,进入中间件的处理过程:中间件会调用 transformRequest(url, server, options = {}) 函数

  1. @vite/client 是如何映射到对应的内容呢,在调用 pluginContainer.resolveId 的过程中会遇到 aliasPlugin 插件的钩子,执行名称替换,最终替换成 vite/dist/client/client.mjs
  1. 继续将改写过的路径传给下一个插件,最终进入 resolvePlugin 插件的 tryNodeResolve 函数,最终解析获得该文件的 id 为/lib/node_modules/vite/dist/client/client.mjs
  2. 最终通过 pluginContainer.load 获取加载本地文件,然后通过 pluginContainer.transform 进行代码转换,将转换后的代码通过 send 方法发送给浏览器。

有无 middlewares 的影响:例子—— vue-dev-server-analysis

(尤雨溪开发vue dev server理解vite原理 - 编程宝库)

# 克隆官方仓库
git clone https://github.com/vuejs/vue-dev-server.git
cd vue-dev-server
# npm i -g yarn
# 安装依赖
yarn

 一般来说,我们看源码先从package.json文件开始:

// vue-dev-server/package.json
{
  "name": "@vue/dev-server",
  "version": "0.1.1",
  "description": "Instant dev server for Vue single file components",
  "main": "middleware.js",
  // 指定可执行的命令
  "bin": {
    "vue-dev-server": "./bin/vue-dev-server.js"
  },
  "scripts": {
    // 先跳转到 test 文件夹,再用 Node 执行 vue-dev-server 文件
    "test": "cd test && node ../bin/vue-dev-server.js"
  }
}

根据 scripts test 命令。我们来看 test 文件夹。

 test 文件夹:

vue-dev-server/test 文件夹下有三个文件,代码不长。

  • index.html
  • main.js
  • text.vue

如图下图所示。

接着我们找到 vue-dev-server/bin/vue-dev-server.js 文件,代码也不长。

主要是启动了端口 3000 的服务,重点在 vueMiddleware 中间件。接着我们来调试这个中间件。

vue-dev-server/bin/vue-dev-server.js 文件中这行 app.use(vueMiddleware()) 打上断点。

找到 vue-dev-server/package.json 的 scripts,把鼠标移动到 test 命令上,会出现运行脚本和调试脚本命令。如下图所示,选择调试脚本。

点击 进入函数(F11)按钮可以进入 vueMiddleware 函数。如果发现断点走到不是本项目的文件中,不想看,看不懂的情况,可以退出或者重新来过。可以用浏览器无痕(隐私)模式(快捷键Ctrl + Shift + N,防止插件干扰)打开 http://localhost:3000,可以继续调试 vueMiddleware 函数返回的函数。

有无 vueMiddleware 中间件对比:

先看结果,在具体分析:

不在调试情况状态下,我们可以在 vue-dev-server/bin/vue-dev-server.js 文件中注释 app.use(vueMiddleware()),执行 npm run test 打开  http://localhost:3000 .

再启用中间件后,如下图。

看图我们大概知道了有哪些区别。浏览器支持原生 type=module 模块请求加载。vue-dev-server 对其拦截处理,返回浏览器支持内容,因为无需打包构建,所以速度很快。

具体分析

接下来,我们来具体分析:

我们可以找到vue-dev-server/middleware.js,查看这个中间件函数的概览。

// vue-dev-server/middleware.js

const vueMiddleware = (options = defaultOptions) => {
  // 省略
  return async (req, res, next) => {
    // 省略
    // 对 .vue 结尾的文件进行处理
    if (req.path.endsWith('.vue')) {
    // 对 .js 结尾的文件进行处理
    } else if (req.path.endsWith('.js')) {
    // 对 /__modules/ 开头的文件进行处理
    } else if (req.path.startsWith('/__modules/')) {
    } else {
      next()
    }
  }
}
exports.vueMiddleware = vueMiddleware

vueMiddleware 最终返回一个函数。这个函数里主要做了四件事:

  • 对 .vue  结尾的文件进行处理
  • 对 .js 结尾的文件进行处理
  • 对 /__modules/  开头的文件进行处理
  • 如果不是以上三种情况,执行 next 方法,把控制权交给下一个中间件

对 .vue 结尾的文件进行处理

if (req.path.endsWith('.vue')) {
const key = parseUrl(req).pathname
let out = await tryCache(key)
if (!out) {
  // Bundle Single-File Component
  const result = await bundleSFC(req) // 具体见下文,bundleSFC 编译单文件组件
  out = result
  cacheData(key, out, result.updateTime)
}
send(res, out.code, 'application/javascript')
}

bundleSFC 编译单文件组件

这个函数,根据 @vue/component-compiler 转换单文件组件,最终返回浏览器能够识别的文件。

const vueCompiler = require('@vue/component-compiler')
async function bundleSFC (req) {
const { filepath, source, updateTime } = await readSource(req) // 看下文,readSource 读取文件资源
const descriptorResult = compiler.compileToDescriptor(filepath, source)
const assembledResult = vueCompiler.assemble(compiler, filepath, {
  ...descriptorResult,
  script: injectSourceMapToScript(descriptorResult.script),
  styles: injectSourceMapsToStyles(descriptorResult.styles)
})
return { ...assembledResult, updateTime }
}

接着来看 readSource 函数实现。

readSource 读取文件资源

这个函数主要作用:根据请求获取文件资源。返回文件路径 filepath、资源 source、和更新时间 updateTime。

const path = require('path')
const fs = require('fs')
const readFile = require('util').promisify(fs.readFile)
const stat = require('util').promisify(fs.stat)
const parseUrl = require('parseurl')
const root = process.cwd()
async function readSource(req) {
const { pathname } = parseUrl(req)
const filepath = path.resolve(root, pathname.replace(/^\//, ''))
// 根据请求获取文件资源。返回文件路径 filepath、资源 source、和更新时间 updateTime。
return {
  filepath,
  source: await readFile(filepath, 'utf-8'),
  updateTime: (await stat(filepath)).mtime.getTime()
}
}
exports.readSource = readSource

接着我们来看对 .js 文件的处理

对 .js 结尾的文件进行处理

if (req.path.endsWith('.js')) {
const key = parseUrl(req).pathname
let out = await tryCache(key)
if (!out) {
  // transform import statements
  // 转换 import 语句 
  // import Vue from 'vue'
  // => import Vue from "/__modules/vue"
  const result = await readSource(req)
  out = transformModuleImports(result.source) // 转换 import 引入
  cacheData(key, out, result.updateTime)
}
send(res, out, 'application/javascript')
}

 针对 vue-dev-server/test/main.js 转换

import Vue from 'vue'
import App from './test.vue'
new Vue({
  render: h => h(App)
}).$mount('#app')

 转换:

import Vue from "/__modules/vue"
import App from './test.vue'
new Vue({
  render: h => h(App)
}).$mount('#app')

main.js 中的 import 语句import Vue from 'vue'通过 recast 生成 ast 转换成 import Vue from "/__modules/vue" 而最终返回给浏览器的是 vue-dev-server/node_modules/vue/dist/vue.esm.browser.js

main.js 中的引入 .vue 的文件,import App from './test.vue' 则用 @vue/component-compiler 转换成浏览器支持的文件,具体见下文。

对 /__modules/ 开头的文件进行处理

import Vue from "/__modules/vue"

 这段代码最终返回的是读取路径 vue-dev-server/node_modules/vue/dist/vue.esm.browser.js 下的文件。

if (req.path.startsWith('/__modules/')) {
// 
const key = parseUrl(req).pathname
const pkg = req.path.replace(/^\/__modules\//, '')
let out = await tryCache(key, false) // Do not outdate modules
if (!out) {
  out = (await loadPkg(pkg)).toString() // loadPkg 加载包(这里只支持Vue文件)
  cacheData(key, out, false) // Do not outdate modules
}
send(res, out, 'application/javascript')
}

loadPkg 加载包(这里只支持Vue文件)

目前只支持 Vue 文件,也就是读取路径 vue-dev-server/node_modules/vue/dist/vue.esm.browser.js 下的文件返回。

// vue-dev-server/loadPkg.js
const fs = require('fs')
const path = require('path')
const readFile = require('util').promisify(fs.readFile)
async function loadPkg(pkg) {
if (pkg === 'vue') {
  // 路径
  // vue-dev-server/node_modules/vue/dist
  const dir = path.dirname(require.resolve('vue'))
  const filepath = path.join(dir, 'vue.esm.browser.js')
  return readFile(filepath)
}
else {
  // TODO
  // check if the package has a browser es module that can be used
  // otherwise bundle it with rollup on the fly?
  throw new Error('npm imports support are not ready yet.')
}
}
exports.loadPkg = loadPkg

 至此,我们就基本分析完毕了主文件和一些引入的文件。对主流程有个了解。

Vite 提供了哪些常用的构建插件和中间件?如何使用它们?

上文讲到了对于不同的资源会有不同的插件去处理。Vite 提供了一些常用的构建插件和中间件,以帮助你在项目中进行开发和构建。

vite的内置插件:

vite在返回源码时,已经对源码做了一层处理,编译为浏览器可以运行的代码。实际上,对于不同的文件后缀,比如.ts、.vue、.tsx、.jsx,vite都会对其做不同的处理:

.vue文件

vite 默认支持 Vue 项目,并提供了一些 Vue 相关的插件,如 @vitejs/plugin-vue,用于解析和编译 Vue 单文件组件。

  • Vue 3 单文件组件支持:@vitejs/plugin-vue
  • Vue 3 JSX 支持:@vitejs/plugin-vue-jsx
  • Vue 2 支持:underfin/vite-plugin-vue2

npm install @vitejs/plugin-vue

 在 vite.config.js 中使用 Vue 插件:

import { createVuePlugin } from 'vite-plugin-vue'

export default {
  plugins: [
    createVuePlugin(/* options */)
  ]
}

.ts文件

(packages/vite/src/node/plugins/esbuild.ts)

Vite 内置支持 TypeScript,无需额外的插件,其内部使用esbuild将TypeScript 转译到 JavaScript。可以直接在项目中使用 TypeScript。

.jsx文件、.tsx文件

(packages/vite/src/node/plugins/esbuild.ts)

JSX 的转译同样是通过 esbuild,默认为 React 16 风格。

.css文件

(packages/vite/src/node/plugins/css.ts)

Vite 支持处理 CSS 相关的插件,比如 vite-plugin-css,用于处理 CSS 文件和样式。导入 .css 文件将会把内容插入到标签中。

npm install vite-plugin-css

 在 vite.config.js 中使用 CSS 插件:

import { createCssPlugin } from 'vite-plugin-css'

export default {
  plugins: [
    createCssPlugin(/* options */)
  ]
}

裸模块重写

(packages/vite/src/node/plugins/resolve.ts)(packages/vite/src/node/plugins/importAnalysis.ts)

从下面这张图中,我们可以看到, vuex.js、vue-router.js 这种依赖模块文件名后面是有后缀的,而 main.js 等这种源代码文件是没有的。为什么会有这样的不同呢?这是因为 vuex.js、vue-router.js 是裸模块,而浏览器在加载文件时,只能加载相对地址,类似于 import Vuex from 'vuex'; 这种裸模块的加载,浏览器是不支持的,所以 vite 会对其做一层裸模块重写的处理,例如将引入 vuex 的url改写为 /node_modules/.vite/deps/vuex.js?v=bc6c6eed

由于 import vue 这种模块引入方式,使用的是 Nodejs 特有的模块查找算法(到 node_modules 中取查找),浏览器无法使用,因此 Vite 会将 vue 替换成一个另一个路径,当浏览器解析到这行 import 语句时,会发送一个 /node_modules/.vite/deps/vuex.js?v=bc6c6eed, Vite Server 会到该目录下,拿到 vue 预构建之后的产物代码。

你会发现,引入的 vuex 是在.vite文件夹里头,而不是在 node_modules 的 vuex 文件夹中,这是因为 vite 做了一个优化 —— 依赖预构建

依赖预构建做了什么:

  • 扫描入口文件
  • 扫描所有用到的依赖
  • 将多个依赖进行打包
  • 修改这些模块的引入路径

简单来说,vite会对 package.json 中的 dependencies 部分先进行构建,然后把构建后的文件换存在 node_modules/.vite 文件夹中,当启动项目时,直接请求该缓存内容。

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

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

相关文章

IO流回顾与装饰者模式

目录 代码图示原理 代码 // 初始化服务器System.out.println("初始化服务器");ServerSocket serverSocket new ServerSocket(8087);System.out.println("服务器初始化成功");// 监听服务器Socket accept serverSocket.accept();System.out.println("…

vue-vuex详解

​🌈个人主页:前端青山 🔥系列专栏:Vue篇 🔖人终将被年少不可得之物困其一生 依旧青山,本期给大家带来vue篇专栏内容:vue-vuex详解 目录 Vuex 1.vuex是什么? 2.vuex核心概念 3.vuex模块化 4. Vuex中ac…

【Java 进阶篇】JQuery 动画:为页面添彩的魔法

在现代的Web开发中,用户体验的提升是至关重要的一环。而动画作为页面交互中的重要组成部分,更是为用户带来了全新的感官体验。本篇博客将深入探讨 JQuery 中动画的应用,带你进入一个充满活力的前端世界。 前言 动画是网页设计的一种重要手段…

YOLO目标检测——树叶检测数据集下载分享【含对应voc、coco和yolo三种格式标签】

实际项目应用:生物多样性研究、林业管理、环境监测和教育科研等方面数据集说明:树叶分类检测数据,真实场景的高质量图片数据,数据场景丰富,总共十个类别。标签说明:使用lableimg标注软件标注,标…

力扣刷题篇之数与位1

系列文章目录 目录 系列文章目录 前言 一、进制转换 总结 前言 本系列是个人力扣刷题汇总,本文是数与位。刷题顺序按照[力扣刷题攻略] Re:从零开始的力扣刷题生活 - 力扣(LeetCode) 一、进制转换 67. 二进制求和 - 力扣&…

(免费领源码)Java#SpringBoot#mysql高校实验室资产管理系统85189-计算机毕业设计项目选题推荐

摘 要 随着计算机技术的发展,特别是计算机网络技术与数据库技术的发展,使人们的生活与工作方式发生了很大的改观。本课题研究的高校实验室资产管理系统,主要功能模块包括后台首页,轮播图,公告管理,资源管理…

一看就会的jni,不会你来打我!

环境配置 Android Studio,这个不多说了。 简单说一下NDK的下载和环境变量,方便在Terminal里使用命令(mac版)。 下载 1.可以通过Android Studio内置的Settings-Android SDK-SDK Tools安装NDK,下载目录为 /Users/mac-xxx(Username)/Library…

服务号可以升级为订阅号吗

服务号和订阅号有什么区别?服务号转为订阅号有哪些作用?在推送频率上来看,服务号每月能推送四条消息,而订阅号可以每天(24小时)推送一条消息。如果企业开通公众号的目的是提供服务,例如售前资讯…

WPS文件丢失如何恢复?4个方法教你快速恢复!

“在WPS里保存了一些比较重要的文档,由于某些原因导致文件丢失了,这种情况下,还有办法成功恢复WPS文件吗?” WPS Office是许多用户在电脑上创建和编辑文档的首选工具。然而,有时文件会意外丢失,可能是由于误…

Linux_在命令行中以树状结构显示目录_tree

1、安装tree命令 使用tree命令,可以在命令行中以树状结构显示目录,当你想知道一个路径下文件的结构时十分方便,还有别的选项功能,下面会介绍其中的一些,完整的介绍Linux命令 - tree—LZL在线工具。 sudo apt updates…

头部厂商Q3交付量环比下滑!激光雷达,现实很骨感

由于中国自主品牌车企在高阶智驾赛道上的激进策略,全球激光雷达行业的走势,也无疑受到中国市场的影响。 本周,禾赛科技发布2023年度三季报,季度激光雷达交付量为47,440台,同比增长125.5%,其中ADAS激光雷达交…

mysql---主从复制和读写分离

主从复制 主从复制,修改,表里的数据:主mysql上的数据,新增都会同步到从mysql上面试题:mysql的主从复制的模式: 1、异步复制:mysql的默认复制就是异步复制。只要执行完之后,客户端提…

使用 Redis 构建轻量的向量数据库应用:图片搜索引擎(二)

本篇文章我们来继续聊聊轻量的向量数据库方案:Redis,如何完成整个图片搜索引擎功能。 写在前面 在上一篇文章《使用 Redis 构建轻量的向量数据库应用:图片搜索引擎(一)》中,我们聊过了构建图片搜索引擎的…

如何在10亿级别用户中检查用户名是否存在?

题目 不知道大家有没有留意过,在使用一些app注册的时候,提示你用户名已经被占用了,需要更换一个,这是如何实现的呢?你可能想这不是很简单吗,去数据库里查一下有没有不就行了吗,那么假如用户数量…

windows usbip(瑞芯微windows开发)

Rockchip RK3588 windows开发 安装usbipd 除usbipd之外&#xff0c;还有一个usbip仓库可以参考usbip-win&#xff0c;但是相对麻烦一些 windows install winget install usbipdShare Devices usbipd --help usbipd list usbipd bind --busid<BUSID>Remote Connectin…

mysql统计整个数据库记录条数

SELECTSUM(TABLE_ROWS) FROM(SELECTTABLE_NAME,TABLE_ROWSFROMINFORMATION_SCHEMA.TABLESWHERETABLE_SCHEMA 数据名&#xff0c;其他不变) t;效果如下&#xff1a;

算法-贪心算法-简单-买卖股票的最佳时机

记录一下算法题的学习4 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票&#xff0c;并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。 返回你可以从这…

【设计一个缓存--针对各种类型的缓存】

设计一个缓存--针对各种类型的缓存 1. 设计顶层接口2. 设计抽象类 -- AbstractCacheManager3. 具体子类3.1 -- AlertRuleItemExpCacheManager3.2 -- AlertRuleItemSrcCacheManager 4. 类图关系 1. 设计顶层接口 // 定义为一个泛型接口,提供给抽象类使用 public interface Cach…

Linux命令(123)之mail

linux命令之mail 1.mail介绍 linux命令mail是用来发送邮件 2.mail用法 mail -s "Subject" EmailAddress < EmailMessage 参数说明-s指定邮件主题SubjectEmailAddress指定邮件地址EmailMessage指定邮件内容3.实例 3.1.配置QQ邮件发送 1.安装sendmail/mailx …

ThreadLocal这点牛角尖总算是给我钻明白了

前言 这个问题算是我的一个羞耻点&#xff0c;起源于一次面试中&#xff0c;面试官问ThreadLocal的底层实现是啥&#xff0c;我那时候一直以为ThreadLocal是一个类似于Redis一样的独立于线程外的第三方存储容器&#xff0c;如何底层维护了一个Map结构&#xff0c;以线程ID为Key…