irpas技术客

下一代前端开发利器——Vite(原理源码解析)_奇舞周刊

网络 7685

本文作者是360奇舞团前端开发工程师

前言

Hi,大家好!

前段时间用Vue3搭建项目时看到同时推出的Vite,只当它是一个新打包工具或者vue-cli的升级版,仍然选择了用Webpack构建项目。最近看了尤雨溪在VueConf上的演讲视频:《Vue3 生态进展和计划》[1],感觉它确实解决了现阶段前端工程化的一些痛点,也能体会到尤雨溪对Vite的重视和大力推广的决心,再加上Vue本身的庞大用户基数,Vite确实有可能成为下一代前端构建工具的突破口。

本文将讨论下Vite出现的背景,解决的痛点,核心功能的实现,存在的意义和预期的未来。Vite本身并不复杂。中文官方文档非常清晰简洁,建议大家使用前仔细读下文档。


大纲

背景

什么是Vite?

基本用法

实现原理

源码分析

优势与不足

与传统构建工具对比

兼容性

未来


背景

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

共存的模块化标准

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

CommonJS:现主要用于Node.js(Node@13.2.0开始支持直接使用ES Module)

AMD:require.js 依赖前置,市场存量不建议使用

CMD:sea.js 就近执行,市场存量不建议使用

ES Module:ES语言规范,标准,趋势,未来

对模块化发展史感兴趣的可以看下《前端模块化开发那点历史》@玉伯[2],而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[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密集下更具性能优势,编译速度更快,以下摘自官网的构建速度对比:浏览器:“开始了吗?”服务器:“已经结束了。”开发者:“好快,好喜欢!!”

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 代码是服务端逻辑,用于处理代码的构建与页面模块的请求。

简单看了下源码(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[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]) ????], ????ignoreInitial:?true, ????ignorePermissionErrors:?true, ????disableGlobbing:?true, ????...watchOptions ??})?as?FSWatcher

通过 ws[6] 来创建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-server与Vite-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将成为主流。

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://·/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://·/package/chokidar

[6]

ws: https://·/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://·/vueconf/mkwv0c/xqyxix

[10]

Vite源码解析: http://vite.ssr-fc.com/

[11]

Develop with Vite | Vite快速入门 - Anthony Fu ? Vue北京聚会 Day 13: https://·/watch?v=xx8gEHet6n8

?-?END?-

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。