- 发布于
vite源码学习
- Authors
- Name
- 田中原
vite源码学习
vite是法语的很快的意思。
官方简介:Vite is an opinionated web dev build tool that serves your code via native ES Module imports during dev and bundles it with Rollup for production.
vite和webpack不同
开发模式不同
webpack
: 启动服务 => webpack打包 => 浏览器访问 => dev-server 返回对应文件
vite
: 启动服务=> 浏览器访问 => vite拦截请求,转换文件 => 找到文件返回
vite对比webpack最主要的不同是在开发模式依赖浏览器对esmodule的支持。提供了按需编译的能力,这样相比webpack减少了编译的时间。
在生产环境,vite通过rollup进行打包,和webpack没什么区别。
如何在本地debug Vite源码
# 下载源码并link
git clone https://github.com/vitejs/vite.git
cd vite
yarn
yarn link
# 切换到vite web项目
cd vite-project
yarn
yarn link "vite"
touch .vscode/launch.json
vite-project/.vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"stopOnEntry": true,
"request": "launch",
"name": "Launch via NPM",
"runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "debug"],
"port": 5858,
"skipFiles": ["<node_internals>/**"]
}
]
}
在vite-project
项目package.json
的script
中添加
{
"scripts": {
"debug": "node --inspect-brk=5858 ./node_modules/vite/dist/node/cli.js"
}
}
在vscode的同一工作区中添加vite
和vite-project
,按下F5即可开始调试。
vite处理
esmodule
vite快的基础:浏览器原生模块支持程度:
基本上现代浏览器都已支持
基础示例: 访问http://localhost:8080/index.html
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>Basic JavaScript module example</title>
<style>
canvas {
border: 1px solid black;
}
</style>
<script type="module">
import { sum } from './main.js'
</script>
</head>
<body></body>
</html>
浏览器会对import
的文件发起请求http://localhost:8080/main.js
export sum = (a, b) => a+b
浏览器module与标准脚本不同的点
- 禁止通过本地加载 通过本地加载html文件,会遇到CORS错误
例如: file://xxx/index.html
如果要使用浏览器的esmodule必须运行一个web服务器
自动使用严格模式
自带
defer
效果,js会等到HTML文档解析完毕才执行全局无法获取
由于浏览器只会对用到的模块发起 HTTP 请求,所以 Vite 没必要对项目里所有的文件先打包后返回,而是只编译浏览器发起 HTTP 请求的模块即可。vite会在服务端,对请求进行判断,执行对应的处理逻辑后再返回。
下面我们看vite是如何处理的
1. 拦截请求
vite启动了一个koa
作为web
服务,在vite/src/node/server/index.ts
中注册了很多plugin 当有请求进来后,会按顺序去执行各个plugin,在plugin中匹配到后,处理对应逻辑
const resolvedPlugins = [
sourceMapPlugin,
moduleRewritePlugin, // 路径重写
htmlRewritePlugin,
envPlugin,
moduleResolvePlugin, // node_modules处理
proxyPlugin,
clientPlugin,
hmrPlugin, // 热更新处理
vuePlugin, // Vue文件处理
cssPlugin,
jsonPlugin,
assetPathPlugin,
webWorkerPlugin,
wasmPlugin,
serveStaticPlugin,
// ...其他
]
resolvedPlugins.forEach((m) => m && m(context))
2. 文件路径重写
当浏览器请求http://localhost:3000/src/main.js
源文件src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
createApp(App).mount('#app')
浏览器收到的结果
import { createApp } from '/@modules/vue.js'
import App from '/src/App.vue'
import '/src/index.css?import'
createApp(App).mount('#app')
因为是浏览器的请求是不符合我们项目文件路径的,所以vite会将源文件中的路径进行转换。 例如: 源码中依赖的包文件,会被转为/@modules
项目中的文件会被添加对应的路径/src
请求是从js而非本地资源请求导入的会添加?import
比如/src/index.css
是从import '/src/index.css'
导入的, 不是<link rel =“ stylesheet” href =“/src/index.css”>
转换实现:vite/src/node/server/serverPluginModuleRewrite.ts
app.use(async (ctx, next) => {
await next()
if (ctx.status === 304) {
return
}
const publicPath = ctx.path
if (
ctx.body &&
ctx.response.is('js') &&
!isCSSRequest(ctx.path) &&
!ctx.url.endsWith('.map') &&
!resolver.isPublicRequest(ctx.path) &&
// skip internal client
publicPath !== clientPublicPath &&
// need to rewrite for <script>\<template> part in vue files
!((ctx.path.endsWith('.vue') || ctx.vue) && ctx.query.type === 'style')
) {
// 读取文件
const content = await readBody(ctx.body)
const cacheKey = publicPath + content
const isHmrRequest = !!ctx.query.t
// 有缓存直接返回缓存
if (!isHmrRequest && rewriteCache.has(cacheKey)) {
debug(`(cached) ${ctx.url}`)
ctx.body = rewriteCache.get(cacheKey)
} else {
// ...省略处理动态引入的代码
// 返回重写过路径的文件
ctx.body = rewriteImports(root, content!, importer, resolver, ctx.query.t)
if (!isHmrRequest) {
rewriteCache.set(cacheKey, ctx.body)
}
}
} else {
debug(`(skipped) ${ctx.url}`)
}
})
对导入语句的处理在rewriteImports函数中,用的是 es-module-lexer 来进行的语法分析获取 imports 数组,然后再做的替换。
浏览器运行到这一步会产生三个请求: http://localhost:3000/@modules/vue.js
http://localhost:3000/src/App.vue
http://localhost:3000/src/index.css?import
3.1 node_module文件
如何处理 http://localhost:3000/@modules/vue.js
文件vite/src/node/server/serverPluginModuleResolve.ts
// 通过Map标记是否缓存
export const moduleIdToFileMap = new Map()
export const moduleFileToIdMap = new Map()
export const moduleRE = /^\/@modules\//
app.use(async (ctx, next) => {
if (!moduleRE.test(ctx.path)) {
return next()
}
// 读取缓存
const serve = async (id: string, file: string, type: string) => {
moduleIdToFileMap.set(id, file)
moduleFileToIdMap.set(file, ctx.path)
await ctx.read(file)
return next()
}
//...省略缓存,优化,.map等逻辑
// 读取package.json获取入口,再读取node_modules中的对应文件
const nodeModuleInfo = resolveNodeModule(root, id, resolver)
if (nodeModuleInfo) {
return serve(id, nodeModuleInfo.entryFilePath!, 'node_modules')
}
// 直接读取node_modules中的对应文件
const nodeModuleFilePath = resolveNodeModuleFile(importerFilePath, id)
if (nodeModuleFilePath) {
return serve(id, nodeModuleFilePath, 'node_modules')
}
ctx.status = 404
})
resolveNodeModule
中读取package.json顺序: 'module', 'jsnext', 'jsnext:main', 'browser', 'main',只要含有此字段且为string就会作为入口文件
以vue3的package.json为例
{
"name": "vue",
"version": "3.0.2",
"description": "vue",
"main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js",
"types": "dist/vue.d.ts"
}
服务端会返回node_modules/vue/dist/vue.runtime.esm-bundler.js
3.2 vue文件
如何处理 http://localhost:3000/src/App.vue
源文件/src/App.vue
<template>
<HelloWorld msg="Hello Vue 3.0 + Vite" />
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld,
},
}
</script>
<style scoped>
h1 {
color: red;
}
</style>
.vue
文件中的<template>
转为type=template的import
import { render as __render } from '/src/components/HelloWorld.vue?type=template'
.vue
文件中的<style>
转为type=style的请求import
import '/src/components/HelloWorld.vue?type=style&index=0'
具体处理逻辑vite/src/node/server/serverPluginVue.ts
//处理不带type参数的文件。 例如 import App from '/src/App.vue'
if (!query.type) {
// ...省略
ctx.type = 'js'
const { code, map } = await compileSFCMain(descriptor, filePath, publicPath, root)
ctx.body = code
ctx.map = map
return etagCacheCheck(ctx)
}
//处理不带type=template的请求
if (query.type === 'template') {
ctx.type = 'js'
// ...省略
const { code, map } = compileSFCTemplate(
root,
templateBlock,
filePath,
publicPath,
descriptor.styles.some((s) => s.scoped),
bindingMetadata,
vueSpecifier,
config
)
ctx.body = code
ctx.map = map
return etagCacheCheck(ctx)
}
//处理不带type=style的请求
if (query.type === 'style') {
// ...省略
const result = await compileSFCStyle(root, styleBlock, index, filePath, publicPath, config)
ctx.type = 'js'
ctx.body = codegenCss(`${id}-${index}`, result.code, result.modules)
return etagCacheCheck(ctx)
}
3.3 css处理
如何处理http://localhost:3000/src/index.css?import
app.use(async (ctx, next) => {
await next()
if (isCSSRequest(ctx.path) && ctx.body) {
const id = JSON.stringify(hash_sum(ctx.path))
if (isImportRequest(ctx)) {
//
const { css, modules } = await processCss(root, ctx)
ctx.type = 'js'
//带有`?import`的css会重写为一个js,通过客户端的updateStyle插入到页面中去
ctx.body = codegenCss(id, css, modules)
}
}
})
function codegenCss(id: string, css: string, modules?: Record<string, string>): string {
let code =
`import { updateStyle } from "${clientPublicPath}"\n` +
`const css = ${JSON.stringify(css)}\n` +
`updateStyle(${JSON.stringify(id)}, css)\n`
if (modules) {
code += dataToEsm(modules, { namedExports: true })
} else {
code += `export default css`
}
return code
}
3.4 后续
在上一步处理完成后会再产生对子组件的请求
http://localhost:3000/src/components/HelloWorld.vue
http://localhost:3000/src/components/HelloWorld.vue?type=style&index=0
http://localhost:3000/src/components/HelloWorld.vue?type=template
后续的处理与之前处理逻辑相同。