Vite的原理

news2024/11/30 0:34:47

背景

这里的背景介绍会从与Vite紧密相关的两个概念的发展史说起,一个是JavaScript的模块化标准,另一个是前端构建工具。


共存的模块化标准

为什么JavaScript会有多种共存的模块化标准?因为js在设计之初并没有模块化的概念,随着前端业务复杂度不断提高,模块化越来越受到开发者的重视,社区开始涌现多种模块化解决方案,它们相互借鉴,也争议不断,形成多个派系,从CommonJS开始,到ES6正式推出ES Modules规范结束,所有争论,终成历史,ES Modules也成为前端重要的基础设施。

  • CommonJS:现主要用于Node.js(Node@13.2.0开始支持直接使用ES Module)
  • AMDrequire.js 依赖前置,市场存量不建议使用
  • CMDsea.js 就近执行,市场存量不建议使用
  • ES Module:ES语言规范,标准,趋势,未来

对模块化发展史感兴趣的可以看下《前端模块化开发那点历史》@玉伯,而Vite的核心正是依靠浏览器对ES Module规范的实现。


发展中的构建工具

近些年前端工程化发展迅速,各种构建工具层出不穷,目前Webpack仍然占据统治地位,npm 每周下载量达到两千多万次。下面是我按 npm 发版时间线列出的开发者比较熟知的一些构建工具。


当前工程化痛点

现在常用的构建工具如Webpack,主要是通过抓取-编译-构建整个应用的代码(也就是常说的打包过程),生成一份编译、优化后能良好兼容各个浏览器的的生产环境代码。在开发环境流程也基本相同,需要先将整个应用构建打包后,再把打包后的代码交给dev server(开发服务器)。

Webpack等构建工具的诞生给前端开发带来了极大的便利,但随着前端业务的复杂化,js代码量呈指数增长,打包构建时间越来越久,dev server(开发服务器)性能遇到瓶颈:

  • 缓慢的服务启动: 大型项目中dev server启动时间达到几十秒甚至几分钟。

  • 缓慢的HMR热更新: 即使采用了 HMR 模式,其热更新速度也会随着应用规模的增长而显著下降,已达到性能瓶颈,无多少优化空间。

缓慢的开发环境,大大降低了开发者的幸福感,在以上背景下Vite应运而生。


什么是Vite?

基于esbuild与Rollup,依靠浏览器自身ESM编译功能, 实现极致开发体验的新一代构建工具!

概念

先介绍以下文中会经常提到的一些基础概念:

  • 依赖: 指开发不会变动的部分(npm包、UI组件库),esbuild进行预构建。
  • 源码: 浏览器不能直接执行的非js代码(.jsx、.css、.vue等),vite只在浏览器请求相关源码的时候进行转换,以提供ESM源码。

开发环境

  • 利用浏览器原生的ES Module编译能力,省略费时的编译环节,直给浏览器开发环境源码,dev server只提供轻量服务。
  • 浏览器执行ESM的import时,会向dev server发起该模块的ajax请求,服务器对源码做简单处理后返回给浏览器。
  • Vite中HMR是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块失活,使得无论应用大小如何,HMR 始终能保持快速更新。
  • 使用esbuild处理项目依赖,esbuild使用go编写,比一般node.js编写的编译器快几个数量级。

生产环境

  • 集成Rollup打包生产环境代码,依赖其成熟稳定的生态与更简洁的插件机制。

处理流程对比

Webpack通过先将整个应用打包,再将打包后代码提供给dev server,开发者才能开始开发。

Vite直接将源码交给浏览器,实现dev server秒开,浏览器显示页面需要相关模块时,再向dev server发起请求,服务器简单处理后,将该模块返回给浏览器,实现真正意义的按需加载。 


基本用法

创建vite项目

$ npm create vite@latest

选取模板

Vite 内置6种常用模板与对应的TS版本,可满足前端大部分开发场景,可以点击下列表格中模板直接在 StackBlitz 中在线试用,还有其他更多的 社区维护模板可以使用。

JavaScriptTypeScript
vanillavanilla-ts
vuevue-ts
reactreact-ts
preactpreact-ts
litlit-ts
sveltesvelte-ts

启动

{
  "scripts": {
    "dev": "vite", // 启动开发服务器,别名:`vite dev`,`vite serve`
    "build": "vite build", // 为生产环境构建产物
    "preview": "vite preview" // 本地预览生产构建产物
  }
}

实现原理

ESbuild 编译

esbuild 使用go编写,cpu密集下更具性能优势,编译速度更快,以下摘自官网的构建速度对比:
浏览器:“开始了吗?”
服务器:“已经结束了。” 

依赖预构建

  • 模块化兼容: 如开头背景所写,现仍共存多种模块化标准代码,Vite在预构建阶段将依赖中各种其他模块化规范(CommonJS、UMD)转换 成ESM,以提供给浏览器。
  • 性能优化: npm包中大量的ESM代码,大量的import请求,会造成网络拥塞。Vite使用esbuild,将有大量内部模块的ESM关系转换成单个模块,以减少 import模块请求次数。

按需加载

  • 服务器只在接受到import请求的时候,才会编译对应的文件,将ESM源码返回给浏览器,实现真正的按需加载。

缓存

  • HTTP缓存: 充分利用http缓存做优化,依赖(不会变动的代码)部分用max-age,immutable 强缓存,源码部分用304协商缓存,提升页面打开速度。
  • 文件系统缓存: Vite在预构建阶段,将构建后的依赖缓存到node_modules/.vite ,相关配置更改时,或手动控制时才会重新构建,以提升预构建速度。

重写模块路径

浏览器import只能引入相对/绝对路径,而开发代码经常使用npm包名直接引入node_module中的模块,需要做路径转换后交给浏览器。

  • es-module-lexer 扫描 import 语法
  • magic-string 重写模块的引入路径
// 开发代码
import { createApp } from 'vue'

// 转换后
import { createApp } from '/node_modules/vue/dist/vue.js'

源码分析

Webpack-dev-server类似Vite同样使用WebSocket与客户端建立连接,实现热更新,源码实现基本可分为两部分,源码位置在:

  • vite/packages/vite/src/client client(用于客户端)
  • vite/packages/vite/src/node server(用于开发服务器)

client 代码会在启动服务时注入到客户端,用于客户端对于WebSocket消息的处理(如更新页面某个模块、刷新页面);server 代码是服务端逻辑,用于处理代码的构建与页面模块的请求。

简单看了下源码(vite@2.7.2),核心功能主要是以下几个方法(以下为源码截取,部分逻辑做了删减):

命令行启动服务npm run dev后,源码执行cli.ts,调用createServer方法,创建http服务,监听开发服务器端口。

// 源码位置 vite/packages/vite/src/node/cli.ts
const { createServer } = await import('./server')
try {
    const server = await createServer({
        root,
        base: options.base,
        ...
    })
    if (!server.httpServer) {
        throw new Error('HTTP server not available')
    }
    await server.listen()
}

createServer方法的执行做了很多工作,如整合配置项、创建http服务(早期通过koa创建)、创建WebSocket服务、创建源码的文件监听、插件执行、optimize优化等。下面注释中标出。

// 源码位置 vite/packages/vite/src/node/server/index.ts
export async function createServer(
    inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
    // Vite 配置整合
    const config = await resolveConfig(inlineConfig, 'serve', 'development')
    const root = config.root
    const serverConfig = config.server

    // 创建http服务
    const httpServer = await resolveHttpServer(serverConfig, middlewares, httpsOptions)

    // 创建ws服务
    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,
        watcher,
        ws,
        moduleGraph,
        listen,
        ...
    }

    // 文件监听变动,websocket向前端通信
    watcher.on('change', async (file) => {
        ...
        handleHMRUpdate()
    })

    // 非常多的 middleware
    middlewares.use(...)
    
    // optimize
    const runOptimize = async () => {...}

    return server
}

使用chokidar监听文件变化,绑定监听事件。

// 源码位置 vite/packages/vite/src/node/server/index.ts
  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

通过 ws 来创建WebSocket服务,用于监听到文件变化时触发热更新,向客户端发送消息。

// 源码位置 vite/packages/vite/src/node/server/ws.ts
export function createWebSocketServer(...){
    let wss: WebSocket
    const hmr = isObject(config.server.hmr) && config.server.hmr
    const wsServer = (hmr && hmr.server) || server

    if (wsServer) {
        wss = new WebSocket({ noServer: true })
        wsServer.on('upgrade', (req, socket, head) => {
            // 服务就绪
            if (req.headers['sec-websocket-protocol'] === HMR_HEADER) {
                wss.handleUpgrade(req, socket as Socket, head, (ws) => {
                    wss.emit('connection', ws, req)
                })
            }
        })
    } else {
        ...
    }
    // 服务准备就绪,就能在浏览器控制台看到熟悉的打印 [vite] connected.
    wss.on('connection', (socket) => {
        socket.send(JSON.stringify({ type: 'connected' }))
        ...
    })
    // 失败
    wss.on('error', (e: Error & { code: string }) => {
        ...
    })
    // 返回ws对象
    return {
        on: wss.on.bind(wss),
        off: wss.off.bind(wss),
        // 向客户端发送信息
        // 多个客户端同时触发
        send(payload: HMRPayload) {
            const stringified = JSON.stringify(payload)
            wss.clients.forEach((client) => {
                // readyState 1 means the connection is open
                client.send(stringified)
            })
        }
    }
}

在服务启动时会向浏览器注入代码,用于处理客户端接收到的WebSocket消息,如重新发起模块请求、刷新页面。

//源码位置 vite/packages/vite/src/client/client.ts
async function handleMessage(payload: HMRPayload) {
  switch (payload.type) {
    case 'connected':
      console.log(`[vite] connected.`)
      break
    case 'update':
      notifyListeners('vite:beforeUpdate', payload)
      ...
      break
    case 'custom': {
      notifyListeners(payload.event as CustomEventName<any>, payload.data)
      ...
      break
    }
    case 'full-reload':
      notifyListeners('vite:beforeFullReload', payload)
      ...
      break
    case 'prune':
      notifyListeners('vite:beforePrune', payload)
      ...
      break
    case 'error': {
      notifyListeners('vite:error', payload)
      ...
      break
    }
    default: {
      const check: never = payload
      return check
    }
  }
}

优势

  • 快!快!非常快!!
  • 高度集成,开箱即用。
  • 基于ESM急速热更新,无需打包编译。
  • 基于esbuild的依赖预处理,比Webpack等node编写的编译器快几个数量级。
  • 兼容Rollup庞大的插件机制,插件开发更简洁。
  • 不与Vue绑定,支持React等其他框架,独立的构建工具。
  • 内置SSR支持。
  • 天然支持TS。

不足

  • Vue仍为第一优先支持,量身定做的编译插件,对React的支持不如Vue强大。
  • 虽然已经推出2.0正式版,已经可以用于正式线上生产,但目前市场上实践少。
  • 生产环境集成Rollup打包,与开发环境最终执行的代码不一致。

与 webpack 对比

由于Vite主打的是开发环境的极致体验,生产环境集成Rollup,这里的对比主要是Webpack-dev-serverVite-dev-server的对比:

  • 到目前很长时间以来Webpack在前端工程领域占统治地位,Vite推出以来备受关注,社区活跃,GitHub star 数量激增,目前达到37.4K 

  • Webpack配置丰富使用极为灵活但上手成本高,Vite开箱即用配置高度集成
  • Webpack启动服务需打包构建,速度慢,Vite免编译可秒开
  • Webpack热更新需打包构建,速度慢,Vite毫秒响应
  • Webpack成熟稳定、资源丰富、大量实践案例,Vite实践较少
  • Vite使用esbuild编译,构建速度比webpack快几个数量级

兼容性

  • 默认目标浏览器是在script标签上支持原生 ESM 和 原生 ESM 动态导入
  • 可使用官方插件 @vitejs/plugin-legacy,转义成传统版本和相对应的polyfill

未来探索

  • 传统构建工具性能已到瓶颈,主打开发体验的Vite,可能会受到欢迎。
  • 主流浏览器基本支持ESM,ESM将成为主流。
  • ViteVue3.0代替vue-cli,作为官方脚手架,会大大提高使用量。
  • Vite2.0推出后,已可以在实际项目中使用Vite
  • 如果觉得直接使用Vite太冒险,又确实有dev server速度慢的问题需要解决,可以尝试用Vite单独搭建一套dev server

相关资源

官方插件

除了支持现有的Rollup插件系统外,官方提供了四个最关键的插件

  • @vitejs/plugin-vue 提供 Vue3 单文件组件支持
  • @vitejs/plugin-vue-jsx 提供 Vue3 JSX 支持(专用的 Babel 转换插件)
  • @vitejs/plugin-react 提供完整的 React 支持
  • @vitejs/plugin-legacy 为打包后的文件提供传统浏览器兼容性支持

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/85011.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

js将后端返回的文件流导出为excel,并自定义下载文件名

一. 需求 后台管理系统&#xff0c;常会出现导出excel表格功能&#xff1b;点击导出按钮&#xff0c;请求后端接口&#xff0c;需要将接口返回的文件流导出为excel&#xff0c;实现前端下载excel文件。 但是&#xff0c;返回的数据获取 response 时出现乱码&#xff0c;如图&a…

CSS复合样式id选择器class选择器

1、CSS复合样式 单一样式&#xff1a;一个CSS属性只控制一种样式&#xff0c;叫做单一样式。复合样式&#xff1a; 一个CSS属性控制多种样式&#xff0c;叫做复合样式。 background、border、font 无序&#xff1a;background、border有序&#xff1a;font&#xff08;先size、…

用JS实现:图片压缩、图片加密

本文将用JavaScript实现两个颇有技术含量的功能&#xff1a;图片压缩、图片加密。 最终效果&#xff1a;可实现将任意图片加密、压缩&#xff0c;并保存到一个独立的html页面中&#xff0c;输入正确的密码&#xff0c;才能看到原图。 第一步、压缩图片 技术原理 将图片读入c…

SDL 软件安全开发周期 Security Development Lifecycle

本文参考多篇文章写作而成&#xff0c;出处在文末注明。 SDL的是安全开发生命周期&#xff0c;Security Development Lifecycle。由微软最早提出&#xff0c;是一种专注于软件开发的安全保障流程。为实现保护最终用户为目标&#xff0c;它在软件开发流程的各个阶段引入安全和隐…

C语言中如何逆序任意输出的字符串

C语言中如何逆序任意输出的字符串的三种方法&#xff1a; 一 设置两个指针&#xff0c;分别指向字符串的头部和尾部&#xff0c;然后交换两个指针所指的字符&#xff0c;并向中间移动指针直到交叉。 char *Reverse(char *s){ // p指向字符串头部 char *p s ; // q指向字符串尾…

扩展函数工具类篇(Kotlin)

引言&#xff1a;本文仅记录自己项目中使用到的扩展类、则需使用&#xff01;&#xff08;均为kt文件&#xff09; 一、ActivityExpand /*** 启动Activity*/ fun AppCompatActivity.startPager(actClass: Class<*>) {startActivity(Intent(this, actClass)) }/*** 启动…

微软推出适用于 Linux 的 Windows 子系统

导读微软宣布正式推出适用于 Linux 的 Windows 子系统 (WSL)&#xff0c;现在可以在 Microsoft Store 中以完整形式购买。据微软称&#xff0c;WSL 以前以预览形式提供&#xff0c;允许开发人员使用他们最喜欢的笔记本电脑进行编程&#xff0c;以运行 GNU/Linux 环境&#xff0…

拓扑排序(Topological Sorting)

题目描述 题目链接&#xff1a;剑指 Offer II 113. 课程顺序 - 力扣&#xff08;Leetcode&#xff09; 现在总共有 numCourses 门课需要选&#xff0c;记为 0 到 numCourses-1。 给定一个数组 prerequisites &#xff0c;它的每一个元素 prerequisites[i] 表示两门课程之间的…

前端给后端发送数据,后端接收

在具体的前后端分离项目中&#xff0c;我们经常会遇到点击表格中的某一行的按钮&#xff0c;获取这一行的某一项值的需求&#xff0c;前端点击拿到这个值&#xff0c;再传给后端使用 例如&#xff1a;在此例中点击同步的按钮&#xff0c;需要获取到表格中的ip地址的内容&#…

GD32F303固件库开发(17)----内部Flash读写

概述 本例程主要讲解如何对芯片自带Flash进行读写&#xff0c;用芯片内部Flash可以对一些需要断电保存的数据进行保存&#xff0c;无需加外部得存储芯片&#xff0c;本例程采用的是GD32F303ZET6主控&#xff0c;512K大小的Flash。 最近在弄ST和GD的课程&#xff0c;需要GD样片…

web前端期末大作业:基于HTML+CSS+JS外卖服务平台10页 带购物车 (web课程设计与实现)

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

Yolo v5源码解析笔记

一、Yolo v5文件目录树结构 二、train.py文件介绍 从main()方法&#xff0c;首先读取参数&#xff1b;其次建立YOLONet&#xff1b;然后读取训练数据&#xff1b;最后进行训练。 2.1 读取参数 部分截图 2.2 建立YOLONet YOLONet的建立是通过 yolo文件夹中的yolo_net.py文件…

游戏开发58课 性能优化

6. 卡顿优化 相信很多研发者或玩家&#xff0c;都遇到这种情况&#xff1a;游戏大部时间运行都很流畅&#xff0c;但在战斗的某些时刻或者打开某些界面会卡一下&#xff0c;甚至卡很久。这个现象就是卡顿。引发卡顿的原因有很多&#xff0c;但主要有&#xff1a; 突发大量IO。…

银河麒麟桌面V10SP1版本系统休眠唤醒鼠标键盘失效解决方法

使用银河麒麟桌面系统有时会出现系统休眠后再唤醒&#xff0c;鼠标键盘失效的问题&#xff0c;可尝试以下方法解决。 1.检查是否安装 laptop-mode-tools 工具 执行命令: dpkg -l | grep laptop-mode-tools如果执行命令无结果输出&#xff0c;表示未安装(如果已安装&#xff0…

【从零开始学习深度学习】21. 卷积神经网络(CNN)之二维卷积层原理介绍、如何用卷积层检测物体边缘

目录1. 二维互相关运算2. 自定义二维卷积层3. 卷积层的应用----图像中物体边缘检测4. 通过数据学习核数组5. 互相关运算和卷积运算6. 特征图和感受野总结卷积神经网络&#xff08;convolutional neural network&#xff09;是含有卷积层&#xff08;convolutional layer&#x…

欧克科技在深交所上市:客户集中度较高,胡坚晟为控股股东

12月12日&#xff0c;欧克科技股份有限公司&#xff08;下称“欧克科技”&#xff0c;SZ:001223&#xff09;在深圳证券交易所主板上市。本次上市&#xff0c;欧克科技的发行价格为65.58元/股&#xff0c;发行数量为1668万股&#xff0c;募资总额约为10.94亿元&#xff0c;募资…

印制电路板(PCB)设计原则和抗干扰措施

印制电路板(PCB)是电子产品中电路元件和器件的支撑件&#xff0e;它提供电路元件和器件之间的电气连接。随着电于技术的飞速发展&#xff0c;PCB的密度越来越高。PCB 设计的好坏对抗干扰能力影响很大&#xff0c;因此&#xff0c;在进行PCB 设计时&#xff0e;必须遵守PCB设计的…

Nginx 防盗链

Nginx 防盗链 本篇主要介绍一下 nginx 中 防盗链的具体配置 , 以及http 的 referer 头 概述 防盗链其实就是 防止别的站点来引用你的 资源, 占用你的流量 在了解nginx 防盗链之前 我们先了解一下 什么是 HTTP 的头信息 Referer,当浏览器访问网站的时候,一般会带上Referer,告…

【TS】TypeScript声明文件(.d.ts)的使用

前言 当我们在TS文件中需要引入外部库时&#xff0c;编译时是无法判断传入参数的类型的&#xff0c;所以我们需要在引入前加入一个声明文件来帮助ts判断类型。 当然现在大部分库都自带有自己的声明文件&#xff0c;一般在types目录下。 使用场景 在ts文件中对引用的外部库做…

汇编逻辑运算指令

目录 逻辑运算指令 一&#xff1a;取反指令NOT 二&#xff1a;逻辑与运算AND&#xff08;重点&#xff09; 屏蔽某些位. 把小写字母转换成大写字母 判断一个数的状态 三&#xff1a;逻辑或运算指令OR 用于拼字 把大写字母转换为小写字母 四&#xff1a;逻辑异或指令XOR…