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 四个过程:
- resolve - 解析 url,找到源文件的绝对路径;
- load - 加载源文件。如果是第三方依赖,直接将预构建内容返回给浏览器;如果是业务代码,继续 transform、parser。
- transfrom - 对源文件内容做转换,即 ts -> js, less -> css 等。转换完成的内容可以直接返回给浏览器了。
- parser - 对转换以后的内容做分析,找到依赖模块,对依赖模块做预转换 - pre transform 操作,即重复 1 - 4。
- 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 = {}) 函数
- @vite/client 是如何映射到对应的内容呢,在调用 pluginContainer.resolveId 的过程中会遇到 aliasPlugin 插件的钩子,执行名称替换,最终替换成 vite/dist/client/client.mjs
- 继续将改写过的路径传给下一个插件,最终进入 resolvePlugin 插件的 tryNodeResolve 函数,最终解析获得该文件的 id 为/lib/node_modules/vite/dist/client/client.mjs
- 最终通过 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 文件夹中,当启动项目时,直接请求该缓存内容。