我们知道,Vite 在开发环境下,会打开一个 Dev Server 用于预览开发的页面,那么这个 Dev Server 到底做了什么呢?它是怎么做到将我们的代码展示成页面的,接下来我们就来一探究竟。

构造项目

我们构造一个最简单的项目,项目中没有用到 npm 包、css 等功能,就只有一个 index.html 和一个 typescript 文件。

目的:剥离出复杂的内容,用最简单的例子去说明最核心的内容

代码放在该GitHub 仓库链接

├─ index.html
├─ index.ts

index.html 代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body><div id="app"></div>
</body><script type="module" src="./index.ts"></script>
</html>

index.ts 代码如下:

const app = document.getElementById('app');
app!.innerHTML = 'helloworld';

项目有了,接下来我们从用户侧,看看 Vite Server 做了什么?

用户侧视觉

在项目目录,运行 vite 命令,我们会看到如下输入:

 vite v3.0.0-alpha.0 dev server running at:> Local: http://localhost:5173/> Network: use `--host` to exposeready in 551ms.

可以看到 vite 创建了一个 dev server,用于访问页面。

访问页面,页面展示出 helloworld,请求如下:

这里可以看到有 5 个请求(如果有多的,可能是浏览器插件的请求,建议使用无痕模式查看),他们的嵌套关系如下:

  • 拉取 index.html* Vite 的热更新相关脚本:/@vite/client* /client/env.mjs* ws://localhost:5173/* 我们写的 ts 代码:/index.ts> 为什么我们明明只写了 index.htmlindex.ts,但这里却还会有其他的资源请求?

我们查看 index.html 的代码:

<!DOCTYPE html>
<html lang="en">
<head>
+<script type="module" src="/@vite/client"></script><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<div id="app"></div>
</body><script type="module" src="./index.ts"></script>
</html>

这里可以看出,index.html 已经被修改了,插入了一段名为 client 代码,这段代码其实是用于 Vite 热更新的,它开启了一个 websocket。client 还依赖了其他脚本,因此浏览器还会继续发起请求,所以会看到有多个请求。

再看看 index.ts :

const app = document.getElementById("app");
- app!.innerHTML = 'helloworld';
+ app.innerHTML = "helloworld";+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIkQ6L3RlbmNlbnQvYXBwL3doYXQtdml0ZS1kby9wYWNrYWdlcy9zaW1wbGUvaW5kZXgudHMiXSwic291cmNlc0NvbnRlbnQiOlsiY29uc3QgYXBwID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ2FwcCcpO1xuYXBwIS5pbm5lckhUTUwgPSAnaGVsbG93b3JsZCc7XG4iXSwibWFwcGluZ3MiOiJBQUFBLE1BQU0sTUFBTSxTQUFTLGVBQWUsS0FBSztBQUN6QyxJQUFLLFlBQVk7IiwibmFtZXMiOltdfQ==

index.ts 的代码已经被编译成 js 了,并且拼接上了 sourcemap。

浏览器是不能运行 ts 代码的,为什么浏览器能运行 index.ts?

其实浏览器要怎么处理一个请求,是看它的响应 Header 中的 Content-Type 的

我们可以看到,虽然请求的是 index.ts,但 Content-Type 却是 application/javascript,这就代表了,浏览器会将这段代码,当做 JavaScript 脚本去处理

这个与文件后缀是无关的,在我们实际开发中,很多请求是 ts、tsx、vue,但无论什么后缀都是没有关系的,它们的 Content-Type 都是 application/javascript,因此浏览器能够正确的运行处理。

到目前为止,用户侧所看到的 Vite Server 的行为,已经明确了:

  • 修改 index.html,在 head 标签中加入了 client 脚本。
  • 编译 index.ts,并拼接上 sourcemap。
  • 连接 websocket

为了简单起见,我们本篇文章不讲述热更新的内容,如果感兴趣,可以查看《Vite 热更新的主要流程》,该文章同样是用了最简单的例子,讲述 Vite 热更新的核心流程,建议阅读。

Server 的中间件机制

我们从用户侧可以看出,Vite Server 对不同的请求的文件做了特殊的处理,然后进行响应返回给客户端

那一个 Server 要如何处理请求的呢?答案是,使用中间件

中间件机制

Vite 用 connect 包来创建一个 DevServer。其简单的用法如下:

var connect = require('connect');
var http = require('http');var app = connect();// 使用一个中间件
app.use(function(req, res){res.end('Hello from Connect!\n');
});// 创建 nodejs http server,并监听 3000 端口
http.createServer(app).listen(3000);

connect 的中间件机制,可以用如下图表示:

当一个请求发送到 server 时,会经过一个个的中间件,中间件本质是一个回调函数,每次请求都会执行回调。

connect 的中间件机制有如下特点:

  • 每个中间件可以分别对请求进行处理,并进行响应。
  • 每个中间件可以只处理特定的事情,其他事情交给其他中间件处理
  • 可以调用 next 函数,将请求传递给下一个中间件。如果不调用,则之后的中间件都不会被执行

由于 htmlTS 文件的处理方式完全不同,因此要做成两个不同的中间件。

  • html 处理中间件
  • 代码转化中间件

html 处理中间件

中间件的部分代码实现如下:

async function viteIndexHtmlMiddleware(req, res, next) {// 去掉 url 中的 hash 和 queryconst url = req.url && cleanUrl(req.url)// 只处理 html 的请求,否则调用 next 传递请求给下个中间件if (url?.endsWith('.html')) {// 从 url 中获取 html 文件路径const filename = getHtmlFilename(url, server)if (fs.existsSync(filename)) {try {// 读取文件,拿到 html 的代码字符串let html = fs.readFileSync(filename, 'utf-8')// 转换 html 代码,返回转换后的代码字符串html = await server.transformIndexHtml(url, html, req.originalUrl)// 响应请求return send(req, res, html, 'html', {headers: server.config.server.headers})} catch (e) {return next(e)}}}next()
}

该中间件只处理 html 请求。如果不是 html 请求,就直接调用 next,将请求交给后续的中间件处理了。

中间件核心流程就是:

  • 读取 html 文件
  • 执行 transform 转换/修改内容
  • 响应请求

我们从用户侧视觉中,也可以看出,transform 就是加上了让的热更新代码,但要是认为它只有这个作用,那就小看 Vite 啦!

Vite 有非常高的可扩展性,加上热更新代码,只不过是 Vite 一个小小的内部插件实现的功能。

我们来看看 Vite 的 transformIndexHtml 插件钩子,它可以index.html 进行修改,可以插入任何的内容

通过在 transformIndexHtml 钩子中,直接修改 html 代码,或者设置 transformIndexHtml 钩子的返回值的方式,对 html 插入内容。

根据 hook 的返回值,做不同的处理,返回结果的类型如下:

type IndexHtmlTransformResult =| string| HtmlTagDescriptor[]| {html: stringtags: HtmlTagDescriptor[]}interface HtmlTagDescriptor {tag: stringattrs?: Record<string, string>children?: string | HtmlTagDescriptor[]/** * 默认: 'head-prepend' */injectTo?: 'head' | 'body' | 'head-prepend' | 'body-prepend'
}

可以看出,返回结果,可以是 string、数组、对象

  • 字符串 —— 则直接替换成转换后 html 代码
  • 对象和数组 —— 需要注入 html 标签,通过 HtmlTagDescriptor 进行配置

HtmlTagDescriptor 的配置内容分为两类:

  • 注入内容
  • 注入的位置

配置方式如下图:

例如 Vite 热更新的返回值为以下配置:

{tag: 'script',attrs: { type: 'module', src: '/@vite/client'},injectTo: 'head-prepend'
}

就是在 <head> 标签内的最前面,拼接上 <script src="/@vite/client" type="module"></script>

代码转换中间件

transformMiddleware 中间件的实现如下:

async function viteTransformMiddleware(req, res, next) {// 只处理 GET 请求,其他不处理if (req.method !== 'GET') {return next()}const url: string = req.url// 只处理部分的请求if (// 用正则表达式判断:/\.((j|t)sx?|mjs|vue|marko|svelte|astro)($|\?)/// ts、vue 都算作是 js 请求isJSRequest(url)) {const result = await transformRequest(url, server, {html: req.headers.accept?.includes('text/html')})if (result) {return send(req, res, result.code)}}next()
}

可以发现,其实中间件的大致框架/写法,都是差不多的,只处理部分请求,其他的调用 next 函数,将请求交给下一个中间件处理。

TS/JStransform 就复杂一点了,因为这里其实不仅仅要处理 TS、JS,其实还可能要处理 Vue、TSX 等组件代码,那 Vite 是怎么实现的呢?

答案是:使用 Vite 插件去扩展这些转换、编译代码的能力

框架是越来越多的,Vite 不可能把这些框架的后缀都内置到 Vite 中,这时候就需要插件提供的扩展能力了,这又是 Vite 扩展性的一大体现

我们来看看一个文件模块到底经历了哪些的处理过程?

  • resolveId,输出是一个本地的实际的路径,npm 包则会指向 node_modules 中的实际位置。
  • load,输出是文件模块的代码字符串,默认就是直接读取文件内容并返回。
  • transform,对代码进行转换。默认行为是不处理。

三个流程分别对应了三个插件钩子:resolveIdloadtransform,这三个钩子,在开发环境中,由 Vite 提供,在生产环境打包时,则由 Rollup 提供。

模块的处理代码如下(有删减):

async function doTransform( url: string,server: ViteDevServer,options: TransformOptions,timestamp: number ) {// 存放代码字符串let code: string | null = null// 存放 sourcemaplet map: SourceDescription['map'] = null// 解析出本地的实际路径const id = (await pluginContainer.resolveId(url))?.id || url// 加载出模块的代码字符串const loadResult = await pluginContainer.load(id, { ssr })code = loadResult.code// 转换代码const transformResult = await pluginContainer.transform(code, id, {inMap: map,ssr})code = transformResult.codemap = transformResult.mapreturn {code,map,}
}

我在 《Vite 是如何兼容 Rollup 插件生态的》中详细介绍过 PluginContainer 的作用,感兴趣的可以看一下,这里大概总结一下:

PluginContainer 的作用是在 Vite 中模拟 Rollup 的插件机制,它在内部实现 Rollup 的钩子,pluginContainer.load 实际上会调用的所有 Vite 插件的 load 钩子。

我们用户侧看到的 index.ts 插件被转换,也是 Vite 的内置插件,用 transform 钩子进行编译转换的。实际上 Vite 是使用了 esbuild,对单个文件进行转译:

export function esbuildPlugin(options: ESBuildOptions = {}): Plugin {const filter = createFilter(options.include || /\.(tsx?|jsx)$/,options.exclude || /\.js$/)return {name: 'vite:esbuild',async transform(code, id) {// 只处理 ts/tsx/jsx,不处理 jsif (filter(id)) {const result = await transformWithEsbuild(code, id, options)return {code: result.code,map: result.map}}}}
}

transformWithEsbuild 函数,则是使用 esbuild 对代码进行转译。

经过转译之后,就是我们用户侧看到的 js 代码了。

总结

本篇文章首先构造出一个最简单的项目,这样便于只关注 Vite 的核心流程;然后简单地介绍了 Connect 的中间件机制,以及说明,Vite Server 的请求处理能力,是通过中间件实现的;然后我们分别介绍了 html 处理插件和 TS 处理中间件。

  • html 处理中间件,通过调用插件的 transformIndexHtmlhtml 页面进行处理。
  • TS 处理中间件,通过调用插件的 resolveIdloadtransform 这三个钩子,对代码进行处理的

从中我们也可以看出,Vite 通过插件,实现了非常高的可扩展性

处理过后的代码,会作为请求的响应值,返回到浏览器,浏览器会根据 Content-type 对响应内容,进行相应的处理。经过这些步骤,一个简单的页面就能够展示出来了。

可以看出,Vite 的核心流程其实非常简单,当然本篇文章,有很多内容其实也是没有说到的,Vite 内部有很多内置的中间件、插件没有介绍,同时 Vite 有很多内部逻辑,也是被忽略的,例如配置的解析、依赖预构建、缓存、优化等等,但其实也不影响我们做出一个简单版本的 Vite。

本篇文章,主要从概念上说明 Vite Server 的行为,下篇文章,我会手写一个简单的 Vite Server,并用它来跑我们这次构造的简单项目,敬请期待~

Vite Server 是如何处理页面资源的?相关推荐

  1. android调用h5预加载图片,使用HTML5的页面资源预加载(Link prefetch)功能加速你的页面加载速度...

    不管是浏览器的开发者还是普通web应用的开发者,他们都在做一个共同的努力:让Web浏览有更快的速度感觉.有很多已知的技术都可以让你的网站速度变得更快:使用CSS sprites,使用图片优化工具,使用 ...

  2. 浏览器页面资源加载过程与优化

    评价页面性能好坏的核心之一就是页面的加载速度,而页面加载速度的关键就是页面资源的加载.本文将从浏览器浏览器页面资源加载过程展开分析,来引出页面关键请求路径的概念,并给出如何优化该关键请求路径的一些方法 ...

  3. Vite 配置 cdn 加载资源

    一.介绍 上篇文章我们从零配置 Vite + Vue3.0 开发环境.生产环境,本篇文章我们配置 CDN 加载.因为 Vite 不会重写从外部文件导入的内容,我们需要使用支持 ESM 编译的 CDN. ...

  4. 根据url一键爬取前端页面资源文件,恐怖如斯-----小飞兔

    前言 有一天你在网上发现一个很好看的前端页面,你想要弄下来在自己的项目上使用,于是你去查看源码,复制html代码和资源文件,过程非常的麻烦,而且很可能缺胳膊少腿,这里我给大家推荐一款可以一键爬取前端页 ...

  5. Nginx实现 内网访问外网https页面资源的解决方案

    项目场景: 在开发过程中,有遇到在内网环境下 需要访问外网 https页面.遇到这个需求也是比较不好做.经过查询资料和调试最终完成功能. 问题描述 解决思路 : 通过 nginx 反向代理来实现 原因 ...

  6. CSS垂直翻转/水平翻转提高web页面资源重用性

    一.CSS下兼容性的元素水平/垂直翻转实现 随着现代浏览器对CSS3的支持愈发完善,对于实现各个浏览器兼容的元素的水平翻转或是垂直翻转效果也就成为了可能.相关的CSS代码如下: /*水平翻转*/ .f ...

  7. 解析浏览器访问服务器 Servlet 应用程序的交互过程(Servlet 容器如何处理请求资源路径)

    案例 1: 请求资源路径:http://localhost:8080/web01/greeting?name=zs 浏览器通过 localhost:8080 连接服务器: 服务器在 webapps 目 ...

  8. AngularJs通过路由传参解决多个页面资源浪费问题

    在实际开发中会遇到很多类似模块界面大体都一致只是极少的细节部分不一样,这时不管是在html页面还有js及数据交互的时候我们就没必要因为这些不同的页面分出不同的文件,这样很浪费内存及效率,于是我在开发中 ...

  9. 不加载执行js_前端性能优化:preload 预加载页面资源

    网上看到一篇来自蚂蚁金服数据体验团队的文章,觉得不错,分享给大伙:https://juejin.im/post/5a7fb09bf265da4e8e785c38 本文主要介绍preload的使用,以及 ...

最新文章

  1. synchronized同步对象锁
  2. 【Android 安全】DEX 加密 ( Application 替换 | 分析 Activity 组件中获取的 Application | ActivityThread | LoadedApk )
  3. JAVA 设计模式 : 状态模式
  4. 【队列】队列的基本操作总结
  5. [WPF系列]-DynamicResource与StaticResource的区别
  6. Go微服务报错protoc-gen-go: unable to determine Go import path for
  7. Linux用户管理(五)Linux系统的启动
  8. Windows Mobile下使用CppUnitLite输出测试结果
  9. cookie与session详解
  10. 计算机组成原理随笔(一)
  11. sysfs接口函数的建立_DEVICE_ATTR
  12. python语音信号时频分析_librosa-madmom:音频和音乐分析
  13. java09数组的使用
  14. 图神经网络(GNN)综述
  15. js获取页面宽度给JS div设宽度
  16. 贪心字典序最小问题poj3617
  17. 猴子吃桃问题java思路_java编程题猴子吃桃问题答案
  18. BS和CS的区别有哪些:
  19. python whl文件_python whl是什么文件
  20. 解决Hexo无法显示图片的几种方案

热门文章

  1. 软件开发中的著名定律
  2. 用PS调出古装人像复古感色调
  3. 金蝶云星空添加基础资料属性
  4. 学习资料分享平台系统
  5. python输入秒数输出分钟小时_Python函数将秒到分钟,小时,天问题,怎么解决
  6. 联想计算机主机哪个是独立显卡,联想显卡怎么切换_联想双显卡怎么切换-系统城...
  7. Java版本飞机大战
  8. 经典微博爱情语录:;有一种智慧叫低调...
  9. meanshift 的跟踪原理解析
  10. 华为荣耀4c_华为荣耀4C详细评测:再次刷新安卓手机性价比