Vite原理学习之预编译

2023-10-27

前言

Vite是下一代的前端开发与构建工具,为什么称为下一代?根本原因在于其基于原生ES Module。在目前的前端工程化生态中,webpack、rollup、esbuild等非常流行,而Vite真是构建在一些流行的技术上。Vite的出现实际上是前端模块规范发展到现在自然出现的产物,它并不是首个基于ES Module的构建工具,还有一些同类型的工具例如Snowpack、@web/dev-server等等。

Vite说明

Vite是基于ES Module的构建工具,可与webpack做对比来学习了解。从构建工具的角度来看前端发展,目前可以简单分为三个阶段:

  • 第一阶段:IIFE实现模块化,源码修改后手动刷新代码来查看最新的效果
  • 第二阶段:CommonJs、AMD、ES Module等模块化规范发展起来,出现了一些构建工具例如webpack,前后端分离促进前端工程化的发展
  • 第三阶段:ES Module支持程度增大,基于ES Module的构建工具例如vite、snowpack等渐渐发展

前端项目越来越大,你会发现webpack等传统构建工具的项目启动速度、HMR速度是越来越慢,Vite实际上就是来解决这两个问题的。基于ES Module,Vite是如何加快开发阶段项目启动和更新呢?

Vite将应用模块分为两类:依赖和源码:

  • 依赖是指第三方模块即node_modules中模块
  • 源码是指非node_modules中本项目源代码

在项目启动时,Vite使用esbuild来预编译代码中使用的依赖包,但是不会对源码做任何处理,一方面得益于Go语言编写的esbuild的编译速度,这个过程相对webpack的JS语言编写的acorn编译非常快,另一方面减少编译的代码量,从而大大提高了项目的启动速度。并且在预编译后还添加了缓存机制,避免再次启动的不必要编译。

在开发过程中,Vite才开始对源码进行必要的转换和动态更新。这个过程是按需进行的,只有当浏览器需要相关模块源码时才会执行,这个过程Vite会通过相关机制来实现。按需更新保证了在开发阶段HMR的速度问题,从而带来良好的开发体验。

实际上Vite背后的原理主要就是这两点实现的逻辑处理,当然还涉及到一些不同规范模块转换、CSS分割、ESM特点导致的其他兼容处理等等一系列工作。

Vite项目启动过程

vite官方提供了创建基于vite的项目开发的脚手架create-vite-app,其内置常见的MVVM框架的项目模板,例如vue、vue + typescript、react等。

"scripts": {
	"dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
}

基于Vite 2.6.7版本创建的,开发环境的命令直接就是执行vite命令。在源码中vite命令对应的逻辑总结如下:

// vite/bin/vite.js

// --debug调试参数,会在终端打印出vite相关细节
const debugIndex = process.argv.findIndex((arg) => /^(?:-d|--debug)$/.test(arg))
if (debufIndex > 0) { // 相关逻辑 }
// --profile统计分析参数,会生成profile文件
const profileIndex = process.argv.indexOf('--profile')
if (profileIndex > 0) {
	// 相关逻辑
} else {
	require('../dist/node/cli')
}

vite命令最核心的逻辑是执行相关目录下的cli文件,该文件逻辑主要如下:

const cac = require('cac)
...
const cli = cac('vite')
...
// dev命令
cli
  .command('[root]') // default command
  .alias('serve') // the command is called 'serve' in Vite's API
  .alias('dev') // alias to align with the script name
  .action(async() => {
  	// 命令对应的逻辑
  })
  
// build命令
cli
  .command('build [root]')
  .action(async() => {
  	// 命令对应的逻辑
  })
  
// preview命令
cli
  .command('preview [root]')
  .action(async() => {
  	// 命令对应的逻辑
  })

实际上就是使用cac库来创建CLI工具,定义了dev、build、preview命令具体的执行逻辑。项目启动和开发阶段更新都是在dev命令的逻辑下面,其action逻辑实际上主要就是创建服务器并进行监听:

const { createServer } = await import('./server')
const server = await createServer({
	root,
    base: options.base,
    mode: options.mode,
    configFile: options.config,
    logLevel: options.logLevel,
    clearScreen: options.clearScreen,
    server: cleanOptions(options)
})
if (!server.httpServer) {
	throw new Error('HTTP server not available')
}
await server.listen()

依赖预构建逻辑都在创建服务器的相关文件中,即调用node/server下文件对应的createServer函数来创建开发服务器。

createServer创建开发服务器

createServer函数主要的逻辑主要以下几点:

  • 合并配置文件和相关默认值生成最终的配置对象
  • 根据不同的配置应用不同的中间件
  • 重写开发服务器listen方法执行预编译
  • 使用chokidar库创建监听器来监听相关文件变动执行不同的逻辑
根据配置应用中间件

在Vite源码中相关配置对应的逻辑是通过中间件来实现的:

// 默认内置的中间件
const middlewares = connect() as Connect.Server

中间件实际上是Node的Connect(中间件框架)的实例, 通过Connect框架来实现应用自定义中间件或服务器。在源码中针对一些配置项应用相关内置的中间件,这里简单列举一些:

  // request timer
  if (process.env.DEBUG) {
    middlewares.use(timeMiddleware(root))
  }
  // cors (enabled by default)
  const { cors } = serverConfig
  if (cors !== false) {
    middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors))
  }
  // proxy
  const { proxy } = serverConfig
  if (proxy) {
    middlewares.use(proxyMiddleware(httpServer, config))
  }
  // base
  if (config.base !== '/') {
    middlewares.use(baseMiddleware(server))
  }

其中一些重要的中间件有:

  • servePublicMiddleware:处理public的
  • serveStaticMiddleware:处理serve root路径下静态文件相关
  • serveRawFsMiddleware:处理链接root之外的相关静态文件相关,例如多项目情况
  • spaFallbackMiddleware:html spa处理
  • indexHtmlMiddleware:html转换
  • transformMiddleware:源码转换,当HMR时处理执行
  • errorMiddleware:全局错误相关
servePublicMiddleware中间件逻辑

该中间件逻辑如下:

export function servePublicMiddleware(dir: string): Connect.NextHandleFunction {
  const serve = sirv(dir, sirvOptions)
  return function viteServePublicMiddleware(req, res, next) {
    // 跳过相关静态资源的请求 skip import request and internal requests `/@fs/ /@vite-client` etc
    if (isImportRequest(req.url!) || isInternalRequest(req.url!)) {
      return next()
    }
    serve(req, res, next)
  }
}

sirv是一个轻量级的中间件,用于处理对静态资源的请求,具体使用可以去看相关npm sirv文档

  // serve static files under /public
  // this applies before the transform middleware so that these files are served
  // as-is without transforms.
  if (config.publicDir) {
    middlewares.use(servePublicMiddleware(config.publicDir))
  }

publicDir默认是public,作为静态资源服务的文件夹,该文件夹下内容不会被编译转换。serveStaticMiddleware和serveRawFsMiddleware中间件虽然处理不同的静态资源获取的情况,但是底层都是使用sirv来实现的,这里就不展开说明了。

spaFallbackMiddleware中间件逻辑

该中间件的应用是有前提条件的,即:

  if (!middlewareMode || middlewareMode === 'html') {
    middlewares.use(spaFallbackMiddleware(root))
  }

middlewareMode支持布尔值和字符串,该属性是serve配置项:

以中间件模式创建 Vite 服务器。(不含 HTTP 服务器)

  • ‘ssr’ 将禁用 Vite 自身的 HTML 服务逻辑,因此你应该手动为 index.html 提供服务。
  • ‘html’ 将启用 Vite 自身的 HTML 服务逻辑。

当该属性值是ssr和是布尔值true是等价, 该属性可以不配置默认该值是false。spaFallbackMiddleware中间件实际上是针对index.html文件访问地址的处理:

import history from 'connect-history-api-fallback'

export function spaFallbackMiddleware(
  root: string
): Connect.NextHandleFunction {
  const historySpaFallbackMiddleware = history({
    // support /dir/ without explicit index.html
    rewrites: [
      {
        from: /\/$/,
        to({ parsedUrl }: any) {
          const rewritten =
            decodeURIComponent(parsedUrl.pathname) + 'index.html'

          if (fs.existsSync(path.join(root, rewritten))) {
            return rewritten
          } else {
            return `/index.html`
          }
        }
      }
    ]
  })

SPA项目使用History API的路由方式当刷新时对于不匹配的路由会找不到对应的文件,使用connect-history-api-fallback来解决相关文件,vite在该中间中对于访问index.html的一些路径支持。

indexHtmlMiddleware中间件逻辑
  if (!middlewareMode || middlewareMode === 'html') {
    // transform index.html
    middlewares.use(indexHtmlMiddleware(server))
  }

该中间的逻辑就是转换index.html的内容,应用针对htm内容的一些插件操作html对应的文本内容生成最终的内容,逻辑简单概括两步:

  • 同步读取html内容生成
  • 读取配置的插件列表并将HTML内容传递给它们,生成最后HTML内容

开发阶段的CSS插入、组件插入等等都是在此中间件执行的。

预编译

vite重写了Node服务器listen方法来在服务器启动完成前执行预编译动作,具体逻辑如下:

  const runOptimize = async () => {
    if (config.cacheDir) {
      server._isRunningOptimizer = true
      try {
        server._optimizeDepsMetadata = await optimizeDeps(
          config,
          config.server.force || server._forceOptimizeOnRestart
        )
      } finally {
        server._isRunningOptimizer = false
      }
      server._registerMissingImport = createMissingImporterRegisterFn(server)
    }
  }

  if (!middlewareMode && httpServer) {
    let isOptimized = false
    // overwrite listen to run optimizer before server start
    const listen = httpServer.listen.bind(httpServer)
    httpServer.listen = (async (port: number, ...args: any[]) => {
      if (!isOptimized) {
        try {
          await container.buildStart({})
          await runOptimize()
          isOptimized = true
        } catch (e) {
          httpServer.emit('error', e)
          return
        }
      }
      return listen(port, ...args)
    }) as any
  } else {
    await container.buildStart({})
    await runOptimize()
  }

从上面逻辑可知,核心逻辑是调用optimizeDeps,而该方法逻辑简单总结如下几点:

  • 创建缓存目录以及相关文件:默认缓存目录是node_modules/.vite,会创建_metadata.json、package.json、编译后的文件和其对应sourcemap文件等
  • 从入口文件扫描收集依赖
  • 使用esbuild预编译相关依赖为ESM格式
收集依赖

收集依赖就要有入口,默认情况下,Vite 会抓取你的 index.html 来检测需要预构建的依赖项。如果指定了config.optimizeDeps.entries、 build.rollupOptions.input,Vite 将转而去抓取这些入口点。
获取到入口之后就调用esbuild的build API来操作文件系统中文件,具体代码如下:

  await Promise.all(
    entries.map((entry) =>
      build({
        absWorkingDir: process.cwd(),
        write: false,
        entryPoints: [entry],
        bundle: true,
        format: 'esm',
        logLevel: 'error',
        plugins: [...plugins, plugin],
        ...esbuildOptions
      })
    )

从入口文件开始遍历所有文件找到符合要求的import语句,获取对应的依赖。并不是所有import语句对应的模块都是依赖,这个过程会筛选符合要求的import语句,具体是通过esbuildScanPlugin生成的插件逻辑来决定的。esbuildScanPlugin插件会定义相关的过滤条件,这边就涉及到esbuild插件相关知识。
具体的扫描过程相对复杂繁琐,不过有一个非常重要的工具库es-module-lexer,该库用于ES模块语法词法分析。

编译依赖

当获取到符合要求的依赖后,就逐个对依赖进行编译,并输出到缓存目录.vite中,这个过程涉及到esbuild、es-module-lexer相关使用。

总结

Vite预编译过程从源码来看还是非常清晰的,其中涉及到esbuild、es-module-lexer、fast-glob等相关库的使用逻辑相对又非常复杂,涉及到编译原理。作为可用于实际开发的工具,其细节还是非常多的,本文仅仅在流程上大概梳理下预编译阶段的主要过程点。预编译过程整体如下:

  • 处理命令支持的debug、profile参数
  • 使用cac定制cli对象,用于定制dev、serve、build、preview等命令的具体处理逻辑,默认vite就是执行dev命令
  • 执行dev命令,会调用createServer来创建开发服务器,并启动监听相关端口

开发服务器创建过程逻辑主要如下:

  • 合并配置文件和相关默认值生成最终的配置对象

  • 根据不同的配置添加不同的中间件,用于在更新阶段做相关处理

  • 重写开发服务器listen方法执行预编译方法,主要就是调用optimizeDeps方法

    • 创建.vite缓存目录并创建相关编译的依赖文件等等
    • 从入口文件扫描收集依赖,入口文件默认是index.html,可通过config.optimizeDeps.entries、 build.rollupOptions.input来指定,优先级大于默认的
    • 使用esbuild预编译相关依赖为ESM格式,所有依赖都是编译成ES Module规范的,依赖可能是IIFE、CommonJs等
  • 使用chokidar库创建监听器来监听相关文件变动执行不同的逻辑以及创建webscoket连接

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Vite原理学习之预编译 的相关文章

随机推荐

  • Mysql详解

    一 数据库的基本概念 数据库的英文单词 DataBase DB 数据库 用来存储和管理数据的仓库 数据库的特点 持久化存储数据 其实数据库就是一个文件系统 方便存储和管理数据 使用了统一的方式操作数据库 SQL 常见的数据库软件 MySQL
  • 两阶段最小二乘法_最小二乘法(Least Squares)简介

    最小二乘法简介 最小二乘法 Least Squares 是回归分析中的一种标准方法 它是用来近似超定系统 Overdetermined System 答案的一种方法 超定系统是指数学中的一种概念 一组包含未知数的方程组中 如果方程的数量大于
  • arm平台编译adb

    参考github https github com bonnyfone adb arm 本来的目的是在arm平台编译以后能够使用adb shell获取shell权限 然后就可以避开其他权限无法执行su的问题 最开始 先修改了android源
  • kodi刮削器 中文_手把手教您设置KODI播放器,3分钟打造家庭影院级媒体库,流畅播放NAS里的原盘电影!...

    创作立场声明 此文为比较基础的KODI播放器的保姆级安装和配置教程 希望可以帮助一些初入家庭影音的值友们 关于KODI KODI是一款播放器 也是一款媒体库管理软件 不仅在电视上可以安装 还可以在windows电脑上进行安装 当下载高清 高
  • Vue3记录

    Vue3快速上手 1 Vue3简介 2020年9月18日 Vue js发布3 0版本 代号 One Piece 海贼王 耗时2年多 2600 次提交 30 个RFC 600 次PR 99位贡献者 github上的tags地址 https g
  • 【1】TypeScript入门——基本认知

    一 基本认知 1 优点 TypeScript 更加可靠 它与使用 JavaScript 相比 不仅支持在任何地方直观地获取组件的接口定义 还能对属性 状态中的值是否为空进行自动检测并给出提示 容错处理 甚至还支持对 React JSX元素接
  • Java创建数组的方法

    最近学Java 一点小心得 希望和大家分享一下 第一次写文章 写的不好希望大家谅解 当然我也会尽力写好这篇文章 Java创建数组的方法大致有三种 说明 这里以int为数据类型 以arr为数组名来演示 一 声明并赋值 int arr 1 2
  • 嵌入式学习——c语言数据的输入输出

    嵌入式学习 c语言数据的输入输出 一 输入输出概念 1 2C语言本身不提供输入输出语句 1 3 include头文件放在程序中 二 printf输出数据 2 2格式字符 三 scanf输入数据 3 1scanf一般格式 四 输入输出函数 4
  • Spring Boot之分离测试和生产环境的应用配置

    多环境应用配置 将默认不变的配置 设置在application properties文件中 新建开发环境下的属性文件application dev properties 将开发中的配置 设置在该文件中 新建生产环境下的属性文件applica
  • “点两下”就能开发一个AI应用!百度砸亿元基金、千万算力要搞插件生态

    金磊 发自 武汉量子位 公众号 QbitAI 这年头 要想开发一个AI应用 怎么搞 只需要简单的 点击 动作就可以了 例如你想开发一个AI作画的App 只需要先点击与之相匹配的能力 简笔成画 根据手绘草图和语言 生成符合要求的图片 言语之美
  • fiddler设置好代理后不能上网(方法二)

    1 打开fiddler在工具栏找到Tools gt options Connections选项如下图 将Allow remote computers to connect 勾选上 然后记住默认8888这个端口 可更改 设置手机代理时会用到
  • 2023高教社数学建模国赛A题 - 定日镜场的优化设计 - 思路

    问题1 计算年平均光学效率和输出热功率 建立模型的坐标系 以圆形区域中心为原点 正东方向为x轴 正北方向为y轴 垂直地面向上为z轴 计算吸收塔和定日镜的位置 吸收塔建于圆形定日镜场中心 根据给定的数据确定定日镜的位置 计算每个定日镜的光学效
  • 使用阿里云OSS实现文件的上传、下载、删除及修改功能

    一 配置OSS相关配置信息 1 要配置 OSS 相关配置信息 您可以按照以下步骤操作 登录阿里云控制台 进入 OSS 控制台 创建一个新的 OSS Bucket 并记录下以下信息 Bucket 名称 Bucket 所属地域 AccessKe
  • 如何解决git上传文件出错[rejected] master -> master (fetch first) error: failed to push some refs to '

    rejected master gt master fetch first error failed to push some refs to git gitee co 上传到码云的时候 报了这个错误 rejected master gt
  • 使用sklearn学习多项式回归(三)

    目录 1 什么是线性 1 1 变量之间的线性关系 1 2 数据间的线性与非线性 1 3 线性模型与非线性模型 1 4 使用分箱处理非线性问题 2 多项式回归PolynomialFeatures 2 1 什么是多项式回归 2 2 多项式回归处
  • 使用 Openssl 验证自签名证书

    原文地址 http blog csdn net kmyhy article details 6546072 iOS的 security framework 框架前面已经介绍 这个框架提供有限的功能 使用它能做到的 比你想象的要少 笔者一直想
  • html input 禁用缓存

    多数浏览器默认会缓存input的值 只有使用ctl F5强制刷新的才可以清除缓存记录 如果不想让浏览器缓存input的值 有2种方法 input 的属性autocomplete 默认为on 其含义代表是否让浏览器自动记录之前输入的值 很多时
  • word编辑公式简单方法

    安装Python包 pix2tex 在终端输入 pip install pix2tex gui i https pypi doubanio com simple 就可以安装 安装好以后 在终端输入pix2tex gui就可以启动 第一次启动
  • Android多级树形选择列表案例 - 手把手教你快速实现

    公司项目中有一个选择联系人的界面 一看里面关系极其复杂 最多时有5层关系嵌套 层数还不一定 有的是第五级是人员 有的是第四级是人员 崩溃中 原来的实现方式是分了三个Activity去分别加载 个人觉得太过臃肿麻烦 选个人要调四次页面 太繁琐
  • Vite原理学习之预编译

    前言 Vite是下一代的前端开发与构建工具 为什么称为下一代 根本原因在于其基于原生ES Module 在目前的前端工程化生态中 webpack rollup esbuild等非常流行 而Vite真是构建在一些流行的技术上 Vite的出现实