¿Por qué Vite es rápido, ve y estúdialo?

Al construir un proyecto con vite recientemente , la mayor sensación es que es muy rápido y el proceso de desarrollo es muy cómodo. ¿Por qué te sientes así? Creo que es principalmente por estos dos puntos que le dan a la gente un sentimiento nuevo.

  • El proyecto comienza rápidamente
  • Actualización rápida del proyecto

He querido averiguarlo y he leído algunos artículos. Lo resumiré yo mismo y miraré el código fuente de vite. Después de todo, siempre he tenido curiosidad por las actualizaciones importantes.

puesta en marcha

rápidamente

Dado que los navegadores modernos admiten módulos ES, pero no admiten la importación de módulos desnudos

<script type="module">
    import { createApp } from "/node_modules/.vite/vue.js?v=c260ab7b";

    // 不支持
    import { createApp } from 'vue';
</script>
复制代码

vite ejecuta la compilación previa a través de esbuild , convierte CommonJS/UMD al formato ESM y lo almacena en caché en node_modules/.vite del proyecto actual:

imagen.png

Luego, reescriba el enlace, por ejemplo /node_modules/.vite/vue.js?v=c260ab7b, cuando el navegador solicite el recurso, secuestre la solicitud http del navegador, transforme archivos que no sean de JavaScript (como JSX, CSS o componentes Vue/Svelte) y devuélvalos al navegador.

imagen.png

Comparar paquete web

En comparación con webpack, analiza las dependencias de los módulos, empaqueta y genera paquetes, e inicia el servidor, mientras que vite carga los módulos a través de ES. Al enviar solicitudes en el navegador, el código fuente se proporciona a pedido y el navegador se hace cargo de parte de el trabajo del empaquetador, por lo que se sentirá rápido.

proceso de envasado de alimentos

actualización caliente

Invite, las actualizaciones activas se realizan en el ESM nativo. Cuando cambia el contenido de un módulo, también se siente más rápido que el navegador vuelva a solicitar el módulo, en lugar de volver a compilar todas las dependencias del módulo como un paquete web.

Al mismo tiempo, vite usa encabezados HTTP para acelerar la recarga de toda la página. Las solicitudes de módulos de origen 304 Not Modifiedse , mientras que las solicitudes de módulos dependientes se Cache-Control: max-age=31536000,immutablealmacenarán fuertemente en caché, por lo que una vez que se almacenen en caché, se almacenarán en caché. no es necesario volver a solicitarlo.

Obtenga más información sobre las últimas actualizaciones desde el código fuente:

Al WebSocketcrear un navegador para comunicarse con el servidor, usar chokidarlos cambios de archivo del monitor, cuando se modifica el contenido del módulo, enviar un mensaje para notificar al cliente y solo recargar el módulo modificado.

imagen.png

Servidor (nodo)

在node文件夹中,cli.ts中通过createServer, 启动服务。 首先通过 cac (一个JavaScript库,用于构建应用的CLI),创建命令行交互,当在项目中执行npm run dev时,会执行cli.action中的回调函数:

// dev
cli
  .command('[root]', 'start dev server') // default command
  .alias('serve') // the command is called 'serve' in Vite's API
  .alias('dev') // alias to align with the script name
  .option('--host [host]', `[string] specify hostname`)
  ...
  .action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {
    // output structure is preserved even after bundling so require()
    // is ok here
    const { createServer } = await import('./server')
    try {
      // 创建服务
      const server = await createServer({
        root,
        base: options.base,
        mode: options.mode,
        configFile: options.config,
        logLevel: options.logLevel,
        clearScreen: options.clearScreen,
        server: cleanOptions(options)
      })
      ...
      await server.listen()
      ...
      // 输出启动信息
      server.printUrls()
    } catch (e) {
      ...
    }
复制代码

createServer 的时候vite做了一些工作,包括启动服务、监听文件变化、生成模块依赖关系、拦截浏览器请求、对返回文件进行处理等。

export async function createServer(
  inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
  // 生成所有配置项,包括vite.config.js、命令行参数等
  const config = await resolveConfig(inlineConfig, 'serve', 'development')

  // 初始化connect中间件
  const middlewares = connect() as Connect.Server
  const httpServer = middlewareMode
    ? null
    : await resolveHttpServer(serverConfig, middlewares, httpsOptions)
  const ws = createWebSocketServer(httpServer, config, httpsOptions)

  // 初始化文件监听
  const watcher = chokidar.watch(path.resolve(root), {
    ignored: [
      '**/node_modules/**',
      '**/.git/**',
      ...(Array.isArray(ignored) ? ignored : [ignored])
    ],
    ignoreInitial: true,
    ignorePermissionErrors: true,
    disableGlobbing: true,
    ...watchOptions
  }) as FSWatcher

  // 生成模块依赖关系,快速定位模块,进行热更新
  const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) =>
    container.resolveId(url, undefined, { ssr })
  )

  // 生成所有插件配置
  const container = await createPluginContainer(config, moduleGraph, watcher)

  // 监听修改文件内容
  watcher.on('change', async (file) => {
    file = normalizePath(file)
    if (file.endsWith('/package.json')) {
      return invalidatePackageDjianata(packageCache, file)
    }
    // invalidate module graph cache on file change
    moduleGraph.onFileChange(file)
    if (serverConfig.hmr !== false) {
      try {
        // 执行热更新
        await handleHMRUpdate(file, server)
      } catch (err) {
        ws.send({
          type: 'error',
          err: prepareError(err)
        })
      }
    }
  })

  // 监听新增文件
  watcher.on('add', (file) => {
    handleFileAddUnlink(normalizePath(file), server)
  })

   // 监听删除文件
  watcher.on('unlink', (file) => {
    handleFileAddUnlink(normalizePath(file), server, true)
  })

  // 主要中间件,请求文件转换,返回给浏览器可以识别的js文件
  middlewares.use(transformMiddleware(server))

  ...

  return server
}
复制代码

当监听到文件内容变化 change 时,先执行 moduleGraph.onFileChange(file)

onFileChange(file: string): void {
    const mods = this.getModulesByFile(file)
    if (mods) {
      const seen = new Set<ModuleNode>()
      mods.forEach((mod) => {
        this.invalidateModule(mod, seen)
      })
    }
}

invalidateModule(
    mod: ModuleNode,
    seen: Set<ModuleNode> = new Set(),
    timestamp: number = Date.now()
  ): void {
    // 修改时间戳
    mod.lastInvalidationTimestamp = timestamp
    // 使转换结果无效,确保下次请求时重新处理该模块
    mod.transformResult = null
    mod.ssrTransformResult = null
    invalidateSSRModule(mod, seen)
}
复制代码

接着执行handleHMRUpdate 函数,通过moduleGraph.getModulesByFile(file),获取需要更新的模块,调用updateModules函数,此时会对一些文件特殊处理,比如是 .env 配置文件、html文件等情况,ws发送full-reload,页面刷新。

  • handleHMRUpdate
const isEnv = config.inlineConfig.envFile !== false && (file === '.env' || file.startsWith('.env.'))
...

// 通过文件获取所包含模块
const mods = moduleGraph.getModulesByFile(file)
...

if (!hmrContext.modules.length) {
    if (file.endsWith('.html'){
    ...
        ws.send({
            type: 'full-reload',
            path: config.server.middlewareMode
              ? '*'
              : '/' + normalizePath(path.relative(config.root, file))
        })
    }
}
复制代码
  • updateModules
if (needFullReload) {
    config.logger.info(colors.green(`page reload `) + colors.dim(file), {
      clear: true,
      timestamp: true
    })
    ws.send({
      type: 'full-reload'
    })
    } else {
    config.logger.info(
      updates
        .map(({ path }) => colors.green(`hmr update `) + colors.dim(path))
        .join('\n'),
      { clear: true, timestamp: true }
    )
    ws.send({
      type: 'update',
      updates
    })
}
复制代码

客户端(client)

在client文件夹中,在客户端 ws 接收到更新类型,执行相应操作

async function handleMessage(payload: HMRPayload) {
  switch (payload.type) {
    // 通信连接
    case 'connected':
      ...
      setInterval(() => socket.send('ping'), __HMR_TIMEOUT__)
      break
    // 更新部分代码
    case 'update':
      ...
      break
    // 自定义事件
    case 'custom': {
      ...
      break
    }
    // 全更新
    case 'full-reload':
      ...
      location.reload()
      ...
      break
    // 热更新后清除
    case 'prune':
      ...
      break
    // 错误
    case 'error': {
      ...
      break
    }
    default: {
      const check: never = payload
      return check
    }
  }
}
复制代码

在部分更新时会调用fetchUpdate函数

async function fetchUpdate({ path, acceptedPath, timestamp }: Update) {
  ...
  await Promise.all(
    Array.from(modulesToUpdate).map(async (dep) => {
      const disposer = disposeMap.get(dep)
      if (disposer) await disposer(dataMap.get(dep))
      const [path, query] = dep.split(`?`)
      try {
        // import更新文件,浏览器发送get请求,返回新结果
        const newMod = await import(
          /* @vite-ignore */
          base +
            path.slice(1) +
            `?import&t=${timestamp}${query ? `&${query}` : ''}`
        )
        moduleMap.set(dep, newMod)
      } catch (e) {
        warnFailedFetch(e, dep)
      }
    })
  )
  ...
}
复制代码

imagen.png

问题

在此有个问题,启动服务过程中,浏览器请求'App.vue',vite是怎么处理该组件的呢,比如

  • 怎么利用浏览器缓存来做优化的?
  • 怎么请求和处理组件里面的样式文件呢?

我们来看下在启动时提到的 transformMiddleware 里面有哪些动作:

export function transformMiddleware(
  server: ViteDevServer
): Connect.NextHandleFunction {
  const {
    config: { root, logger },
    moduleGraph
  } = server
  ...
  // 返回一个执行函数
  return async function viteTransformMiddleware(req, res, next) {

    // 不是get请求直接跳过
    if (req.method !== 'GET' || knownIgnoreList.has(req.url!)) {
      return next()
    }
    ...
    // 判断url,正则匹配,其中isJSRequest的正则为:
    // const knownJsSrcRE = /\.((j|t)sx?|mjs|vue|marko|svelte|astro)($|\?)/
    if (
      isJSRequest(url) ||
      isImportRequest(url) ||
      isCSSRequest(url) ||
      isHTMLProxy(url)
    ) {
      ...
      // http协商缓存:
      // 通过比对if-none-match的上一次etag值,如果变化返回一个完整响应内容,在响应头上添加新的etag值,否则返回 304,使用浏览器缓存
      const ifNoneMatch = req.headers['if-none-match']
      if (
        ifNoneMatch &&
        (await moduleGraph.getModuleByUrl(url, false))?.transformResult
          ?.etag === ifNoneMatch
      ) {
        isDebug && debugCache(`[304] ${prettifyUrl(url, root)}`)
        res.statusCode = 304
        return res.end()
      }

      // 依赖vite插件进行解析转换,返回code
      // 如果是npm依赖,会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求
      const result = await transformRequest(url, server, {
        html: req.headers.accept?.includes('text/html')
      })
      if (result) {
        const type = isDirectCSSRequest(url) ? 'css' : 'js'
        const isDep = DEP_VERSION_RE.test(url) || isOptimizedDepUrl(url)
        return send(req, res, result.code, type, {
          etag: result.etag,
          // allow browser to cache npm deps!
          cacheControl: isDep ? 'max-age=31536000,immutable' : 'no-cache',
          headers: server.config.server.headers,
          map: result.map
        })
      }
      ...
      
      next()
    }

复制代码

在上一步处理请求时,调用 transformRequest 函数,比如vue文件,会通过 @vite/plugin-vue 插件对templatestyle分别做处理

imagen.png

export function transformRequest(
  url: string,
  server: ViteDevServer,
  options: TransformOptions = {}
): Promise<TransformResult | null> {
  ...
  const request = doTransform(url, server, options, timestamp)
  ...
  return request
}

async function doTransform(
  url: string,
  server: ViteDevServer,
  options: TransformOptions,
  timestamp: number
) {
  ...
  // 插件命中
  const id =
    (await pluginContainer.resolveId(url, undefined, { ssr }))?.id || url
  ...
  // 加载
  const loadResult = await pluginContainer.load(id, { ssr })
  ...
  // 转换
  const transformResult = await pluginContainer.transform(code, id, {
    inMap: map,
    ssr
  })
  ...
  // 返回处理结果
  // 启用协商缓存,在响应头中带上etag
  const result = ssr
    ? await ssrTransform(code, map as SourceMap, url)
    : ({
        code,
        map,
        etag: getEtag(code, { weak: true })
      } as TransformResult)

  return result
}
复制代码

@vite/plugin-vue 插件中命中vue文件执行转换

export async function transformMain(
  code: string,
  filename: string,
  options: ResolvedOptions,
  pluginContext: TransformPluginContext,
  ssr: boolean,
  asCustomElement: boolean
) {
  ...
  // script
  const { code: scriptCode, map } = await genScriptCode(
    descriptor,
    options,
    pluginContext,
    ssr
  )

  // template
  const hasTemplateImport =
    descriptor.template && !isUseInlineTemplate(descriptor, !devServer)

  let templateCode = ''
  let templateMap: RawSourceMap | undefined
  if (hasTemplateImport) {
    ;({ code: templateCode, map: templateMap } = await genTemplateCode(
      descriptor,
      options,
      pluginContext,
      ssr
    ))
  } 

  // styles
  const stylesCode = await genStyleCode(
    descriptor,
    pluginContext,
    asCustomElement,
    attachedProps
  )
  ...
}
复制代码

将他们转换成es模块引入,浏览器发送请求,在通过中间件处理返回给浏览器。

总结

Aprendí principalmente sobre el proceso de ejecución del procesamiento de archivos y la actualización en caliente durante el proceso de inicio de vite. Hay muchos detalles en el código fuente. Si está interesado, puede estudiarlo en profundidad. Durante este proceso, aprendí sobre algunos npm paquetes y sus funciones, como cac, chokidaretc., así como httpla aplicación de almacenamiento en caché y el uso de muchas map、setestructuras de datos en el código encontrado, incluida la adición, eliminación, modificación, existencia, etc. de datos, continúe explorando con preguntas.

Artículo de referencia

Supongo que te gusta

Origin juejin.im/post/7078262306743779336
Recomendado
Clasificación