什么是vite系列目录:
(一)什么是Vite——vite介绍与使用-CSDN博客
(二)什么是Vite——Vite 和 Webpack 区别(冷启动)-CSDN博客
(三)什么是Vite——Vite 主体流程(运行npm run dev后发生了什么?)-CSDN博客
未完待续。。。
vite 源码总体目录结构
|
-CHANGELOG.md
|-LICENSE.md
|-README.md
|-bin
| |-openChrome.applescript
| |-vite.js
|-client.d.ts
|-package.json
|-rollup.config.js #打包配置文件
|-scripts
| |-patchTypes.js
|-src
| |-client #客户端
| | |-client.ts
| | |-env.ts
| | |-overlay.ts
| | |-tsconfig.json
| |-node #服务端
| | |-build.ts
| | |-cli.ts #命令入口文件
| | |-config.ts
| | |-constants.ts #常量
| | |-importGlob.ts
| | |-index.ts
| | |-logger.ts
| | |-optimizer
| | | |-esbuildDepPlugin.ts
| | | |-index.ts
| | | |-registerMissing.ts
| | | |-scan.ts
| | |-plugin.ts #rollup 插件
| | |-plugins #插件相关文件
| | | |-asset.ts
| | | |-clientInjections.ts
| | | |-css.ts
| | | |-esbuild.ts
| | | |-html.ts
| | | |-index.ts
| | | |-...
| | |-preview.ts
| | |-server
| | | |-hmr.ts #热更新
| | | |-http.ts
| | | |-index.ts
| | | |-middlewares #中间件
| | | | |-...
| | | |-moduleGraph.ts #模块间关系组装(树形)
| | | |-openBrowser.ts #打开浏览器
| | | |-pluginContainer.ts
| | | |-send.ts
| | | |-sourcemap.ts
| | | |-transformRequest.ts
| | | |-ws.ts
| | |-ssr
| | | |-__tests__
| | | | |-ssrTransform.spec.ts
| | | |-ssrExternal.ts
| | | |-ssrManifestPlugin.ts
| | | |-ssrModuleLoader.ts
| | | |-ssrStacktrace.ts
| | | |-ssrTransform.ts
| | |-tsconfig.json
| | |-utils.ts
|-tsconfig.base.json
|-types
| |-...
运行npm run dev后发生了什么?
(初识 vite 原理,vite 是如何启动项目的)(源码调试:下一代前端构建工具 - Vite 2.x 源码级分析)
vite命令是在哪里注册的呢,在 node_modules/vite/package.json 中查看bin字段。"bin" 字段的作用是能让我们在命令窗口全局输入命令执行。vite启动的时候,会执行 bin 目录下的 vite.js 文件,在这个文件里面我们会看到获取了当前的电脑的 绝对路径 ,如果不包含 node_module 路径,就需要引入 source-map-support 这个包来处理,如果是相对路径则会进行路径补全,当然在 dev 环境下都会有 node_module 路径,在 prod 环境下, vite 会使用 rollup 来进行打包。
示例项目下:package-lock.json
再示例项目下bin文件下看到三个vite文件,分别是针对unity,windows,跨平台三个方向链接对应的环境变量:
server 核心方法:执行命令行脚本
打开项目中的vite.js看到,其中主要运行的是从入口文件 cli.js。
vite.js 中的核心内容就是执行了 start 方法,动态引入了 ../dist/node/cli.js ,这个地址是打包后的地址,在 vite 源码中,脚本地址在 packages/vite/src/node/cli.ts 。
这里的/dist/node/cli.js是打包后的文件,可能有点长,可以配合vite打包前的源码一起阅读:
在 /dist/node/cli.js 中,首先使用 cac 命令工具处理用户的输入。
然后命令执行的是 createServer,这个 createServer 从 ./chunks/dep-df561101.js 引入,这里也是打包后的代码,比较长。
cli.ts 的核心功能是解析命令行参数并启动本地项目,解析命令行参数通过 cac 库,这里我们主要看启动本地项目的命令。在 cac 库中,通过 command 定义基础命令,通过 alias 方法定于命令别名,通过 option 方法定义命令行参数,最后通过 action 方法执行具体的操作。在 action 方法中,最核心的部分就是引入了 createServer 方法,通过 listen 启动本地 server。
同时在 cli.js 文件中,可以看到四个命令对应了 4 个核心的文件&方法:
1. dev 命令
文件路径:./server/index.ts;
主要方法:createServer;
主要功能:项目的本地开发命令,基于 httpServer 启动服务,Vite 通过对请求路径的劫持获取资源的内容返回给浏览器,服务端将文件路径进行了重写。例如:
项目源码如下:
import { createApp } from 'vue';
import App from './index.vue';
经服务端重写后,node_modules 文件夹下的三方包代码路径也会被拼接完整。
import __vite__cjsImport0_vue from "/node_modules/.vite/vue.js?v=ed69bae0";
const createApp = __vite__cjsImport0_vue["createApp"];
import App from '/src/pages/back-sky/index.vue';
2. build 命令文件路径:./build.ts ;
主要方法:build;
主要功能:使用 rollup 打包编译
3.optimize 命令
文件路径:./optimizer/index.ts;
主要方法:optimizeDeps;
主要功能:主要针对第三方包,Vite 在执行 runOptimize 的时候中会使用 rollup 对三方包重新编译,将编译成符合 esm 模块规范的新的包放入 node_modules 下的 .vite 中,然后配合 resolver 对三方包的导入进行处理:使用编译后的包内容代替原来包的内容,这样就解决了 Vite 中不能使用 cjs 包的问题。
下面是 .vite 文件夹中的 _metadata.json 文件,它在预编译的过程中生成,罗列了所有被预编译完成的文件及其路径。例如:
{
"hash": "1848098d",
"browserHash": "f4266ce6",
"optimized": {
"./src/lib/index.js": {
"src": "../../../src/lib/index.js",
"file": "___src_lib_index__js.js",
"fileHash": "58a83bbc",
"needsInterop": false
},
"lodash-es": {
"src": "../../lodash-es/lodash.js",
"file": "lodash-es.js",
"fileHash": "915b5f31",
"needsInterop": false
},
"vue": {
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
"fileHash": "c91074d2",
"needsInterop": false
}
},
"chunks": {}
}
4. preview 命令
文件路径:./preview/index.ts;
主要方法:preview;
主要功能:本地预览构建产物。不要将其用作生产服务器,因为它不是为此而设计的。
createServer #
我们找到了前面大概执行的顺序后,这里回到源码,在 packages/vite/src/node/server/index.ts 里面找到 createServer:
export async function createServer(
inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
// Vite 配置整合
// resolveConfig 方法解析 vite 核心配置,包括来自命令行、vite.config 文件的配置参数
const config = await resolveConfig(inlineConfig, 'serve', 'development')
const root = config.root
const serverConfig = config.server
// 创建http服务,根目录的 index.html 就是服务器的入口
const httpServer = await resolveHttpServer(serverConfig, middlewares, httpsOptions)
// 创建ws服务,主要是用于实现热更新(HMR),当代码发生变化时,服务器通过 WebSocket 向客户端发送更新通知
const ws = createWebSocketServer(httpServer, config, httpsOptions)
// 创建watcher,设置代码文件监听,实时地响应文件的增删改操作,也是用于实现热更新功能
const watcher = chokidar.watch(path.resolve(root), {
ignored: [
'**/node_modules/**',
'**/.git/**',
...(Array.isArray(ignored) ? ignored : [ignored])
],
...watchOptions
}) as FSWatcher
// 创建server对象
const server: ViteDevServer = {
config, // 配置属性
middlewares, // 中间件
httpServer, // HTTP server 实例
watcher, // chokidar 文件监听实例
pluginContainer: container, // 插件容器
ws, // WebSocket 实例
moduleGraph, // 模块依赖图
listen,
...
}
// 文件监听变动,websocket向前端通信
watcher.on('change', async (file) => {
...
handleHMRUpdate()
})
// 服务 middleware,通过 use 方法添加启动项目阶段需要的中间件
middlewares.use(...)
// optimize: 预构建
await initDepsOptimizer(config, server)
// 监听端口,启动服务
httpServer.listen = (async (port, ...args) => { ... })
return server
}
可以看到 createServer 做了很多事情:
- 配置参数解析,包括 vite 核心配置、https 配置、chokidar 配置
- 创建 HTTP 和 WebSocket server,用于启动开发 server 和热更新通信:
- 通过 createHttpServer 创建一个 HTTP 服务器实例,根目录的 index.html 就是服务器的入口。
- 通过 createWebSocketServer 创建 WebSocket 服务器,主要是用于实现热更新(HMR),当代码发生变化时,服务器通过 WebSocket 向客户端发送更新通知
- 启动 chokidar 文件监听器,监听文件变化,实现热更新 HMR:
- chokidar 能够创建一个文件监听器,监听文件和目录的变化,实时地响应文件的增删改操作,也是用于实现热更新功能。
- 创建 ModuleGraph 实例,记录模块依赖关系:
- 通过 ModuleGraph class 创建一个模块依赖图实例,模块依赖图主要用于维护各个模块之间的依赖关系,主要有两个用处:
-
-
- 热更新过程中,通过模块依赖图获取所有相关依赖,保证正确完整的实现热更新。
- 打包过程中,根据模块之间的依赖关系进行优化,比如将多个模块合并为一个请求、按需加载模块等,提高打包速度和加载性能。
-
- 创建插件容器,管理插件生命周期、执行过程、插件之间传递上下文:
- 通过 createPluginContainer 方法创建插件容器,插件容器主要有三个功能:
-
-
- 管理的插件的生命周期;
- 根据插件的钩子函数,在特定的时机执行插件;
- 在插件之间传递上下文对象,上下文对象包含 vite 的内部状态和配置信息,这样插件通过上下文对象就能访问和修改 vite 内部状态。
-
- 定义 ViteDevServer 对象,包含核心配置和启动开发 server 核心方法:
- ViteDevServer 对象就是 createServer 方法最终返回的对象,主要包含前几步创建的对象实例和启动 server 相关的核心方法。
- 其中比较特殊的是 createDevHtmlTransformFn 方法,这个方法用于在开发环境下转换 index.html 文件,默认注入一段客户端代码 /@vite/client ,用于在客户端创建 WebSocket,接收服务端热更新传递的消息。
- 执行 configureServer 定义函数,创建自定义中间件:
- configureServer 主要用于配置开发服务器,比如在内部 connect 中添加自定义中间件。在这一步,从配置中获取所有 configureServer 钩子并放入 postHooks 钩子中,在内部中间中间件定义好之后,执行 postHooks 钩子。
- 注意到 postHooks 是在处理 index.html 中间件之前执行,目的是为了自定义的中间件能够在返回 index.html 之前处理请求。
- 定义内部中间件:
- 通过 connect 包创建 middlewares 中间件。中间件主要是用来处理 HTTP 请求和响应,通过定义一系列的中间件并且按照一定的顺序执行,每个中间件函数对请求和响应进行处理,然后将处理后的请求和响应传递给下一个中间件函数,直到最后一个中间件函数处理完毕并发送响应。定义好 middlewares 之后,通过 use 方法添加启动项目阶段需要的中间件。
最后总结一下,在开发过程中,vite 启动命令 npm run dev 执行后,实际执行的是 node_module/.bin 目录下的 vite 脚本,在解析命令行参数之后,通过执行 createServer.listen 方法启动 vite 。
Vite开发环境主体流程
下图是 Vite 在开发环境运行时加载文件的主体流程。
具体如下:
在项目根目录下有一个index.html文件,在其script标签设置type='module':
- 当声明一个 script 标签类型为 module 时:
<script type="module" src="/src/main.js"></script>
- 启动项目,vite会启动一个 dev server,然后加载各种中间件,进而监听对应的前端访问请求。github.com/vitejs/vite...同时,会在开发环境中注入 Vite 自身的 client 客户端代码,用于监听HMR等处理。github.com/vitejs/vite...
浏览器会向服务器发起一个GET请求,拿到main.js文件:
http://localhost:3000/src/main.js请求main.js文件:
// /src/main.js:
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
- 浏览器请求到了main.js文件,检测到内部含有import引入的包,又会对其内部的 import 引用发起 HTTP 请求获取模块的内容文件。如:GET http://localhost:3000/@modules/vue.js 如:GET http://localhost:3000/src/App.vue
- Vite 的主要功能就是通过劫持浏览器的这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再返回给浏览器,vite整个过程中没有对文件进行打包编译,所以其运行速度比原始的 webpack 开发编译速度快出许多!