最近在公司探索Bundless构建工具的实现,尝试将一些已有的业务项目从Webpack迁移到Vite。由于中后端项目一般对浏览器兼容性要求不高,所以可以大胆引入一些前沿激进的方案。因此,公司找了一个业务中后台项目,初步尝试引入Vite。当然,在迁移的过程中,并没有直接使用Vite,而是在Vite的上层做了一层封装,以接入团队目前开发的构建工具的架构。项目构建配置会与原来的Vite配置不同。相同的。不过不影响后续步骤和原理介绍。我也会尽量把场景还原成原来的Vite配置,让大家看得懂。还有一些内部工具和业务相关的问题,这些细节没有公开,请理解。前言目前主流的前端打包工具主要以Webpack为代表,但随着项目规模的发展,构建痛点越来越突出。最大的感受就是太慢了。一方面,项目冷启动时,必须递归打包整个项目的依赖树,另一方面,JavaScript语言本身的局限性(解释、单线程执行),导致构建性能出现瓶颈.在此背景下,一些被称为Bundleless(或Unbundled)的构建工具应运而生,如Snowpack、Vite,其中Vite最近在社区中越来越受欢迎,在GitHub上的star高达30k+,甚至超过了vue3仓库星数之多(目前24.1k),可见其影响之大。与Webpack等传统打包工具相比,Vite有两大优势:利用浏览器内置ESModule的支持(只需在script标签中添加属性type="module"),浏览器直接请求devserver一个一个一个个的模块,而不需要提前打包所有的文件。借助esbuild超快的编译速度,预建第三方库。一方面,将分散的文件合并起来,减少网络请求。另一方面,它完全转换为ESM模块语法以适应浏览器内置的ESM支持。顾名思义,与传统的构建工具相比,Bundleless最大的特点就是不需要打包业务代码(虽然第三方库还是要打包,但没办法),尤其是当项目越来越大,可以大大提高构建效率和开发体验。公司业务项目实施后,底层从Webpack切换到Vite,冷启动速度提升400%以上。原本20秒启动的项目,现在可以3~4秒冷启动,第二次启动秒开,不得不感慨:太快了!迁移问题SVG组件报错Vite本身不支持svg组件的写法。默认情况下,如下写法会导致浏览器报错:importUpfrom'common/imgs/up.svg';functionHome(){return{<>//省略其他子组件>/>}}为了解决这个问题,在社区现有生态中找到vite-plugin-react-svg插件,将其添加到viteplugins数组中,实现引用SVG资源的能力一种组件方式,并通过以下方式导入svg文件:importUpfrom'common/imgs/up.svg?component';esbuild预构建第三方包内部bug时,会检查并报错相关第三方包依赖。在这个项目中,遇到了如下报错。react-virtualized中的esmproduct有问题://ThisvariableisnotexportedinWindowScroller.js!从“../WindowScroller.js”导入{bpfrpt_proptype_WindowScroller};在这个库的官方GitHub仓库,我发现友达提出了同样的问题(issue地址:https://github.com/bvaughn/react-virtualized/issues/1632),导入的代码中有问题报错的那句话,引入了一个不存在的变量,但是webpack/rollup并没有捕捉到这个问题,但是在esbuild中却报错了,一般是第三方库产品的bug。一般来说,解决node_modules中第三方库的bug大概有两种方式:第一种方式是复制一份第三方库中有问题的文件进行修复,放到项目目录下(不是node_modules),然后通过构建工具resolve.alias能力重定向到固定位置。另一种是通过patch-package记录node_modules变更记录,生成patches目录,然后通过项目的post-install脚本在团队中同步这个变更。这里使用后者来实现第三方库的临时补丁。当然,这也适用于其他第三方库问题的临时修复。//1。安装yarnaddpatch-packagepostinstall-postinstall//2。修改node_modules代码后执行:yarnpatch-packagereact-virtualized//3。在package.json中添加脚本:{"postinstall":"patch-package"}pre-build反复执行如果刚才的问题只是小事,那么接下来的bug简直就是终极折磨。完成基本配置后,项目确实可以正常显示,但是每次清空缓存重建时,pre-build阶段会出现以下很奇怪的现象:控制台一直显示类似newdependenciesxxx的日志,并且该服务频繁重新加载预建缓存目录。Vite中会不断产生新的依赖缓存文件。随着服务频繁重载,所有的缓存文件会被清空,又会产生更多新的依赖,也就是不断刷新缓存目录,大约需要20秒左右才稳定下来。预建和重新刷新目录的20多秒期间,页面无法访问,一直卡在卡死状态。放一张事故现场的图片给大家感受下:1.问题定位首先,我在正常情况下用demo工程试了一下。预构建情况如下:一次输出所有构建缓存文件。终端日志也很简单,如下图所示:正常情况下根据日志,在Vite源码中全局搜索Pre-bundling,反向追溯pre-build部分源码。总结流程如下:在ViteServer启动阶段,执行server.listen回调中的runOptimize逻辑,进入预构建阶段。在runOptimize中调用optimizeDeps,内部调用esbuild进行构建,并将自定义扫描插件传入esbuild,在esbuild构建过程中做依赖分析,将依赖分配给deps得到deps后,打印出上面的terminallog,第一个Apre-建造结束。函数调用流程如下:startServer->runOptimize->optimizeDeps->scanImports->esbuild.build终端输出的依赖信息来自deps变量,在optimizeDeps中执行scanImports得到:所以我们断点进入scanImports:可以看到这里会读取配置中的input配置。项目中的配置是./src/index.tsx。如果这样配置,会以root和input的拼接路径为入口搜索依赖。但是很遗憾,找不到这个入口路径,如下图:原因是配置文件是这样的:{input:'./src/index.js',root:path.resolve(__dirname,'src')}当时为了把html放到src目录下,随便设置了root参数,所以找不到和input拼接后的路径,中间出现了src/src,明显是一个多写了一层src,所以这种情况下如果没有找到entry,会直接返回一个空对象。Vite会认为找不到入口,无法进行预构建依赖。2.解决方案的核心是保证入口路径能够被Vite解析。有两种方式:去掉root配置,让process.cwd()默认作为root使用,最后才能找到入口路径。删除输入配置。此时Vite的行为是在根目录下搜索html文件,即使在项目根目录下配置了root为src,也能解析。3.问题回顾后,现在一切正常,但是回到原来的问题,为什么命令行里会出现那么多新的依赖和其他日志,buildcache目录会一遍又一遍的刷新,页面总是被卡住?虽然问题解决了,但是,还是要把这些诡异的问题探索清楚。查看源码后发现问题的根源在于Vitepre-build不仅在服务启动的时候进行,也有可能在请求进来的时候触发,也就是说pre的行为-build不仅仅在开始时触发一次,在浏览器访问项目时,可能会再次触发,甚至多次触发。在上面提到的第一个预构建过程中,运行时优化的逻辑会被注册到runOptimize函数中,如下图箭头所示:它返回一个闭包函数,主要是运行时优化的逻辑,它将再次调用optimizeDeps,然后逻辑执行scanImports+esbuild.build,最后重启ViteServer。这个闭包函数会在Vite的resolvePlugin中调用。简单来说,当浏览器发起请求,请求进入Vite服务器,首先执行一系列插件(顺序参考官网),其中会到resolvePlugin在比较高的位置,本插件会分析项目中的依赖项,如果发现有没有预构建的依赖,会执行_registerMissingImport预构建这个依赖,并重启ViteServer。_registerMissingImport调用后,会进行第二次预构建,但不会立即执行。相当于每100ms收集一次batch,然后一起build。其实是有一个拦截的过程,这样就不用每个依赖都去调用optimizeDeps了。提高预构建效率属于Vite中的细节优化,类似于Vue中的nextTick批量更新。在这里贴上_registerMissingImport返回的函数代码:(handle)clearTimeout(handle)//100ms后批量预构建依赖handle=setTimeout(()=>rerun(ssr),debounceMs)//重启ViteServerserver._pendingReload=newPromise((r)=>{pendingResolve=r})}}其实拦截的二次优化过程也可以解释上面log大的原因。每隔100ms就会发现新的依赖,ViteServer会重启预构建,这必然会导致产生大量的新依赖。这个时候服务器频繁重启,页面卡住是很正常的。4、以上分析过程的一些扩展,也算是找到了踩坑问题的根源。不过我也在Vite仓库中搜索了相关问题。事实上,这种二次预建过程在正常项目中其实是存在的,主要是处理项目中的一些动态导入场景。当通过动态引入依赖过多时,也会极大地影响构建性能。这种场景也可以使用antfu同学开发的vite-plugin-optimize-persist这个插件进行自动优化。后续思考迁移之后,项目可以正常运行,构建效率也得到了很好的提升,但是目前,还有两个问题让我陷入思考。一、实施前景首先,这个业务项目的结构相对来说没有那么复杂。如果涉及Monorepo、SSR构建或者更复杂的结构,Vite还能迁移到那里吗?Bundless能否在目前的中后台业务中大规模使用??这需要在近期通过不断的投入和实践来验证,将继续与大家分享。2.生产环境可以使用Vite吗?另一方面,是否应该让Vite接管生产环境的建设?要知道它可以显着提高开发环境的效率,但是真的适合生产环境吗?我个人认为开发环境和生产环境要分开看。前者的痛点是效率,后者的诉求是稳定性和质量。这个时候我觉得Webpack在生产环境是比Vite用来打包的Rollup更好的选择。但是这会带来新的问题——两者的配置差别很大,如何解决统一配置的问题呢?关于这个问题,我会在后面详细讨论。我个人对此持乐观态度,至少有一些可能实现。或许当Webpack和Vite之间的配置差异可以通过某种解决方案来消除时,Vite就可以应用于所有使用Webpack的项目。也就是说,当那一天真的到来的时候,在开发环境中,Vite完全可以替代Webpack。