发布于

vite源码学习

Authors
  • avatar
    Name
    田中原
    Twitter

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.jsonscript中添加

{
  "scripts": {
    "debug": "node --inspect-brk=5858 ./node_modules/vite/dist/node/cli.js"
  }
}

在vscode的同一工作区中添加vitevite-project,按下F5即可开始调试。

vite处理

vite快的基础: esmodule

浏览器原生模块支持程度:

浏览器原生模块支持程度

基本上现代浏览器都已支持

基础示例: 访问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与标准脚本不同的点

  1. 禁止通过本地加载 通过本地加载html文件,会遇到CORS错误

例如: file://xxx/index.html

如果要使用浏览器的esmodule必须运行一个web服务器

  1. 自动使用严格模式

  2. 自带defer效果,js会等到HTML文档解析完毕才执行

  3. 全局无法获取

由于浏览器只会对用到的模块发起 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
}
浏览器请求的结果 css生成

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

后续的处理与之前处理逻辑相同。