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

 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——冷启动时vite做了什么(源码、middlewares)_第1张图片

模块解析

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

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

(四)什么是Vite——冷启动时vite做了什么(源码、middlewares)_第2张图片

(四)什么是Vite——冷启动时vite做了什么(源码、middlewares)_第3张图片

middlewares 中内容转换

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

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

(四)什么是Vite——冷启动时vite做了什么(源码、middlewares)_第4张图片

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 相关请求处理中间件

(四)什么是Vite——冷启动时vite做了什么(源码、middlewares)_第5张图片

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

如图下图所示。

(四)什么是Vite——冷启动时vite做了什么(源码、middlewares)_第6张图片

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

(四)什么是Vite——冷启动时vite做了什么(源码、middlewares)_第7张图片

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

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

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

(四)什么是Vite——冷启动时vite做了什么(源码、middlewares)_第8张图片

点击 进入函数(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 .

(四)什么是Vite——冷启动时vite做了什么(源码、middlewares)_第9张图片

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

(四)什么是Vite——冷启动时vite做了什么(源码、middlewares)_第10张图片

看图我们大概知道了有哪些区别。浏览器支持原生 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 方法,把控制权交给下一个中间件

(四)什么是Vite——冷启动时vite做了什么(源码、middlewares)_第11张图片

对 .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——冷启动时vite做了什么(源码、middlewares)_第12张图片

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

(四)什么是Vite——冷启动时vite做了什么(源码、middlewares)_第13张图片

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

(四)什么是Vite——冷启动时vite做了什么(源码、middlewares)_第14张图片

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

依赖预构建做了什么:

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

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

你可能感兴趣的:(vite,前端,vue,javascript,vite)