下一代前端开发利器——Vite(原理源码解析)

2022-12-05 0 629

责任编辑作者是360奇舞蹈团后端开发技师

序言

Hi,我们好!

前几日用Vue3构筑工程项目时看到同时面世的Vite,木患它是两个新装箱辅助工具或者vue-cli的改良版,依然选择了用Webpack构筑工程项目。最近看了尤雨溪在VueConf上的演说音频:《Vue3 自然生态进展和计划》[1],感觉它的确化解了眼下后端产业化的许多关键点,也能感受到尤雨溪对Vite的倚重和力推的决心,再加上Vue这类的巨大用户绝对值,Vite的确有可能正式成为新一代后端构筑辅助工具的突破点。

责任编辑将讨论下Vite出现的大背景,化解的关键点,核心理念功能的同时实现,存在的意义和预期的今后。Vite这类并不复杂。英文非官方文件格式非常明晰简约,提议我们采用前仔细读下文件格式。

概要

大背景什么是Vite?基本用语同时实现基本原理源代码分析优势与不足与传统构筑辅助工具对照相容性今后

大背景

这里的大背景介绍会从与Vite密切相关的两个基本概念的文化史讲起,两个是JavaScript的模组化国际标准,另两个是后端构筑辅助工具。

并存的模组化国际标准

为什么JavaScript会有多种不同并存的模组化国际标准?因为js在设计Hathras并没有模组化的基本概念,随着后端业务维数急速提高,模组化越来越受到开发人员的倚重,街道社区已经开始不断涌现多种不同模组化化解方案,它们相互先进经验,也争辩急速,形成多个政治势力,从CommonJS已经开始,到ES6正式面世ES Modules规范化结束,所有争辩,实乃产业发展史,ES Modules也正式成为后端重要的基础建设。

CommonJS:现主要用于Node.js([email protected]已经开始支持直接采用ES Module)AMD:require.js 倚赖后置,市场增量不提议采用CMD:sea.js 设点执行,市场增量不提议使用ES Module:ES语言规范化,国际标准,趋势,今后

对模组化文化史钟爱的可以看下《后端模组化开发司佥产业发展史》@玉伯[2],而Vite的核心理念正是依靠应用程序对ES Module规范化的同时实现。

产业发展中的构筑辅助工具

近些年来后端产业化产业发展迅速,各种构筑辅助工具不断不断涌现,目前Webpack依然占据统治地位,npm 每星期用户数达到五百万次。下面是我按 npm 发版时间线列举的开发人员比较津津乐道的许多构筑辅助工具。

下一代前端开发利器——Vite(原理源码解析)

当前产业化关键点

现在常用的构筑辅助工具如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(原理源码解析)

Vite直接将源代码交给应用程序,同时实现dev server秒开,应用程序显示页面需要相关模块时,再向dev server发起请求,服务器简单处理后,将该模块返回给应用程序,同时实现真正意义的按需加载。下一代前端开发利器——Vite(原理源码解析)

基本用语

创建vite工程项目

$ npm create vite@latest

选取模板

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

JavaScriptTypeScriptvanillavanilla-tsvuevue-tsreactreact-tspreactpreact-tslitlit-tssveltesvelte-ts

启动

{

  “scripts”

: {

    “dev”“vite”// 启动开发服务器,别名:`vite dev`,`vite serve`    “build”“vite build”// 为生产环境构筑产物    “preview”“vite preview” // 本地预览生产构筑产物

  }

}

同时实现基本原理

ESbuild 编译

esbuild 采用go编写,cpu密集下更具性能优势,编译速度更快,以下摘自官网的构筑速度对照:

应用程序:“已经开始了吗?”服务器:“已经结束了。”开发人员:“好快,好喜欢!!”下一代前端开发利器——Vite(原理源码解析)image.png

倚赖预构筑

模组化兼容:如开头大背景所写,现仍并存多种不同模组化国际标准代码,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 代码是服务端逻辑,用于处理代码的构筑与页面模块的请求。

简单看了下源代码([email protected]),核心理念功能主要是以下几个方法(以下为源代码截取,部分逻辑做了删减):

命令行启动服务npm run dev后,源代码执行cli.ts,调用createServer方法,创建http服务,监听开发服务器端口。// 源代码位置 vite/packages/vite/src/node/cli.tsconst{ 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.tsexport async function createServer(

inlineConfig: InlineConfig = {}

): Promise<ViteDevServer

{

    // Vite 配置整合    const config = await resolveConfig(inlineConfig, servedevelopment

)

    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(changeasync

 (file) => {

        …

        handleHMRUpdate()

    })

    // 非常多的 middleware

    middlewares.use(…)

    // optimize    const runOptimize = async

 () => {…}

    return

 server

}

采用chokidar[5]监听文件变化,绑定监听事件。// 源代码位置 vite/packages/vite/src/node/server/index.ts  const

watcher = chokidar.watch(path.resolve(root), {

    ignored

: [

      **/node_modules/**

,

      **/.git/**

,

      …(Array

.isArray(ignored) ? ignored : [ignored])

    ],

    ignoreInitialtrue

,

    ignorePermissionErrorstrue

,

    disableGlobbingtrue

,

    …watchOptions

})as

 FSWatcher

通过 ws[6] 来创建WebSocket服务,用于监听到文件变化时触发热更新,向客户端发送消息。// 源代码位置 vite/packages/vite/src/node/server/ws.tsexport function createWebSocketServer()

{

    let

 wss: WebSocket

    const

hmr = isObject(config.server.hmr) && config.server.hmr

    const

 wsServer = (hmr && hmr.server) || server

    if

 (wsServer) {

        wss = newWebSocket({noServertrue

 })

        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({ typeconnected

 }))

        …

    })

    // 失败    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.tsasync 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, 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-server与Vite-dev-server的对照:

到目前很长时间以来Webpack在后端工程领域占统治地位,Vite面世以来备受关注,街道社区活跃,GitHub star 数量激增,目前达到37.4KWebpack配置丰富采用极为灵活但上手成本高,Vite开箱即用配置高度集成Webpack启动服务需装箱构筑,速度慢,Vite免编译可秒开Webpack热更新需装箱构筑,速度慢,Vite毫秒响应Webpack成熟稳定、资源丰富、大量实践案例,Vite实践较少Vite采用esbuild编译,构筑速度比webpack快几个数量级

相容性

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

今后探索

传统构筑辅助工具性能已到瓶颈,主打开发体验的Vite,可能会受到欢迎。主流应用程序基本支持ESM,ESM将正式成为主流。Vite在Vue3.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 为装箱后的文件提供传统应用程序相容性支持

UI组件库

Element UI[7]:支持 vite 引入

相关链接

Vite官网[8]Vue3 自然生态进展和计划-尤雨溪[9]Vite源代码导出[10]Develop with Vite | Vite快速入门 – Anthony Fu • Vue北京聚会 Day 13[11]

参考资料

[1]

《Vue3 自然生态进展和计划》: https://www.yuque.com/vueconf/mkwv0c/xqyxix

[2]

《后端模组化开发司佥产业发展史》: https://github.com/seajs/seajs/issues/588

[3]

StackBlitz: https://vite.new/

[4]

街道社区维护模板: https://github.com/vitejs/awesome-vite#templates

[5]

chokidar: https://www.npmjs.com/package/chokidar

[6]

ws: https://www.npmjs.com/package/ws

[7]

Element UI: https://element-plus.gitee.io/zh-CN/guide/quickstart.html#%E6%8C%89%E9%9C%80%E5%AF%BC%E5%85%A5

[8]

Vite官网: https://cn.vitejs.dev/

[9]

Vue3 自然生态进展和计划-尤雨溪: https://www.yuque.com/vueconf/mkwv0c/xqyxix

[10]

Vite源代码导出: http://vite.ssr-fc.com/

[11]

Develop with Vite | Vite快速入门 – Anthony Fu • Vue北京聚会 Day 13: https://www.youtube.com/watch?v=xx8gEHet6n8

END

360W3CECMATC39Leader

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务