目前,前端社区对Vite替代Webpack的呼声越来越高。但是对于长期维护的业务项目,很多同学上车可能还是有疑虑——Vite真的足以支持非玩具级别的项目吗?为此,本文将分享一个实际案例,介绍我们如何(相对)在公司的核心业务中落地Vite。起草一个web端业务的图文编辑器,已经进行了五年多的时间。作为一个被多人维护的前端项目,它具有一定的复杂性:小编使用基于Yarnworkspace和Lerna的宏仓库来管理源码。里面有近20个包,初始化时会加载400多个模块,并且有2GB多的node_modules依赖。编辑器模块最早使用Vue0.8和AMD模块语法,从Vue1.x和2.x时代开始一直维护。webpack也是从头开始,然后从1.x升级到现在的4.x版本。编辑器中的一些高级渲染功能使用了Worker和WASM的功能。编辑器整体以单个npm包的形式发布到公司私有仓库供业务访问,有独立的打包发布流程。小编2016年第一次投稿,基于Vue0.8和AMD语法,不敢说这是所谓的“大型企业级”项目,但至少肯定不是玩具项目。然而,出乎意料的是,“Vite的迁移成本甚至比升级Webpack和Babel大版本还要低。”仅仅一个下午,基于Vite的编辑器的最小可用MVP就启动并运行了。下面几点介绍相关的实践经验:如何规划基本的迁移思路,以及一些基础的知识储备。如何通过编写插件来解决Webpack加载器的一些问题。如何迁移常见的webpack配置。如何处理上游依赖问题。知识背景与思路我们知道,以Webpack为代表的主流前端打包器之所以速度慢,是因为冷启动时必须递归打包整个项目的依赖树,受限于JavaScript的特性(解释和执行和单线程模型)并且存在吞吐量瓶颈。为了解决这两个痛点,Vite另辟蹊径,换了个??路线:对于项目中的业务模块,Vite使用了现代浏览器内置的ESModule支持,浏览器直接请求devserver加载这些模块一个接一个——所以你经常可以看到本地环境中大量的HTTP请求刷新屏幕,这也是Vite最鲜明的特点。对于项目中的node_modules依赖,Vite使用esbuild等原生语言开发的高性能打包器将这些库中的非ESM标准(CommonJS或UMD)模块打包成一个整体的ESM,也就是所谓的依赖预捆绑。这个过程的打包结果被缓存起来,冷启动时重建缓存的效率也非常高。Vite的设计和webpack-dev-server的区别在它的文档中已经很清楚地展示出来了。一图抵千言:Webpack风格的经典bundler图Vite风格的No-bundler图基于这个区别,我们可以知道,要让Vite支持原生的Webpack项目,你需要保证的无外乎两点:确保业务模块的源代码符合ESM规范。确保所有依赖项都由esbuild正确处理。当然,这只是最简单的心智模型。实际前端项目中经常会引入一些奇怪的东西,比如CSS、JSON、Worker、WASM、HTML模板……虽然Vite对这些需求内置了很好的支持,但谁也不能保证一键搞定盒子-这不是Vite或Webpack的问题,而是将代码移植到构建环境时的常见困难。对于这类任务,“最难的永远是从零到一的‘打光’”。因此,这里的建议是:“充分熟悉每个组件从项目进入到渲染完成所经历的代码(子)树,确保这个最小的子集能够在新环境中正常运行。”所有其他代码都可以暂时彻底删除。一个架构设计合理的软件项目,一般很容易实现模块的精简和扩展。例如,在这个编辑器中,我们支持可以按需配置和加载的元素类型。现有的20多个业务元素,其对应的模块已经支持按需加载,只有遇到对应的数据才会通过import()导入。因此,在迁移时,只需要保留一些基本的元素模块实现用于测试即可。同样,在业务项目中,也可以通过精简路由配置等方式,自定义走主流程的最低可用版本。自定义插件实现上述代码简化过程无非就是创建一个干净的示例页面导入项目,注释掉一些代码,然后重复执行vite命令test,这里不再赘述。对于Vite迁移,可能很多同学最担心的就是Webpack插件的兼容性问题。我们刚遇到类似的问题,这里简单分享一下。2016年编辑器古版本代码截图中有一个细节,就是引入了editor.html作为组件的HTML模板。这种行为一直保留了很多年到现在——也就是说,这里没有使用SFC单文件组件,而是使用了一个text-element.html作为text-element.js等组件的模板,比如this://导入HTML源代码--codeSecretGardenimportTextElementTplfrom'./text-element.html'//Vue2.0经典配置--codeSecretGardenexportdefault{template:TextElementTpl,methods:{//...},created(){//...}}在Webpack配置中,我们一般使用HTMLloader来支持,那么Vite呢?这种需求好像没有内置,现在社区的vite-plugin-html是专为EJS模板设计的,star数量好像很少。。。不过真的要等社区做出来吗给你现成的?事实上,Vite的插件系统直接依赖于rollup。对于这个需求,只需在vite.config.js中编写几行插件,如下所示://使用rollup附带的插件工具--ConardLiconst{createFilter,dataToEsm}=require('@rollup/pluginutils');functioncreateMyHTMLPlugin(){//创建用于过滤模块的过滤器constfilter=createFilter(['**/*.html']);return{name:'vite-plugin-my-html',//startName--ConardLi//根据id过滤模块,遇到匹配的模块转换sourcetransform(source,id){if(!filter(id))返回;//这样就可以为其他JS模块默认导出HTML字符串returndataToEsm(source);},};}//这样就可以按照Vite的标准API使用插件了module.exports={plugins:[createMyHTMLPlugin()],}createMyHTMLPlugin不就是一个很简单的函数吗?但它实际上解决了一个实际问题。就个人而言,我认为一个用户友好的构建系统应该在大多数时候开箱即用,并且能够通过简单的逻辑进行自我扩展。在这一点上,可以说Vite做的还是相当不错的。此外,Vite与Snowpack的主要区别之一是其插件系统与Rollup有更深层次的集成,从而实现了开发和构建模式下通用的插件API。因此,在业务上,也有机会“脱壳”一些成熟的Rollup插件来满足需求。常见的Webpack配置迁移在本次实践中使用了很少的Vite配置。主要值得一提的是:通过resolve.alias配置,可以覆盖(或劫持)模块路径。注意这个配置最好尽量小,滥用它很容易降低代码模块结构对工具链的友好性。通过define配置,可以支持process.env.__DEV__等环境变量注入。请注意,Vite会直接将字符串注入到产品代码中的原始表达式中,因此如果您只想传递一个像true这样的简单常量,则需要额外的JSON.stringify包。通过vite-plugin-vue2可以支持Vue2.0SFC。这里的原因是虽然编辑器中的主要组件没有使用SFC,但是测试页面的demo入口是app.vue。有了这个插件,他们可以很好地共存。Less和CSS依赖于Vite的内置支持,无需引入额外的配置。当然,另一种解决方法是先执行独立打包CSS的命令,然后导入“./dist.css”。WebWorker可以通过importWorkerfrom"worker.js?worker"语法来支持。另外可以进一步结合resolve.alias配置继续兼容Webpack。对于WASM,除了importinitfrom"./a.wasm"等内置支持外,还有一种做法是让WASM的JS适配层支持传入可配置的WASM路径。这方面的典型示例可以在CanvasKit和其他包中找到。上游依赖问题处理根据上述实践,应该足以解决Vite加载各个业务模块的问题。但是最后还是有一个头疼的问题:node_modules中的依赖不能被esbuild正确打包怎么办?在这次迁移中,我们遇到了两个这样的问题,每个问题的原因各不相同:图像重采样库Pica依赖于一个简单的WebWorker转换库,它会直接读取模块代码顶层的参数数据,原因esbuild报错。字体解析库OpenType.js在ESM源代码中封装了几个require('fs')函数,以便同时兼容浏览器和Node。这也会导致错误。对于这两个问题,其实有一个通用的workaround方法:“创建一个third_party目录,把有问题的upstream模块复制进去,在这里修复问题,调整模块依赖。”比如把Pica库中的require('./a.js')代码复制到third_party目录下,模块导入路径改成require('pica/src/a.js'),这样它不需要完整复制整个上游依赖项。至于这里遇到的两个CommonJS问题,具体修复也很容易,比如将参数的读取放到exportdefault的函数体中,直??接去掉浏览器环境下不用的Node文件读取逻辑等。这样的third_party模式其实不算hack。在很多语言项目中被广泛使用,但有一些地方值得注意:建议在修改的位置加上//FIXME等注释,以便接收者确认修改的地方。如果需要集成较大的上游依赖,不建议直接放在代码库中,可以使用gitsubmodule或者CDN等形式。理想情况下,补丁应该向上游报告,问题解决后,相应的本地版本应该被移除。以上就是所有值得罗列的问题,最后放一张基于Vite的本地环境启动成功的截图:上图中的日志有问题,就是加载了两个不同的Vue版本。这是因为SFC部分和依赖HTML模板的代码误用了不同的Vue依赖。后来通过别名配置将vue改写为vue/dist/vue解决了这个问题。由于编辑器SDK原本是使用Babel独立发布的,不会影响原有的NPM发布流程,Vite整体侵入性不高。至于最终的效果,没有别的,直接踩油门加速:Webpack的devserver冷启动时间40多秒缩短为1.5秒,创建.vite目录缓存后,启动vite命令的时间只有300毫秒左右。修改单个文件后约2秒的增量编译时间被完全优化掉,浏览器加载页面的效率没有明显差异。通过这样做,这个历史项目通过即时反馈重新获得了开发经验水平,同时还实现了更高效的CI集成。这里还有很大的想象空间,期待Vite在未来发挥更大的作用。综上所述,Vite以较低的接入成本实现了开发体验的大幅提升,有望引领前端构建工具领域的下一波范式转变。根据ROI的说法,“其着陆的潜在好处远远超过成本。”实际业务中的代码应该尽可能符合标准,少用一些依赖工具链黑魔法的特性,换取更好的向后兼容性。对于代码移植,实践中其实有很多技巧(不一定摆在桌面上),比如定期替换,编写codemod,为下游业务提供deprecatedAPI检测脚本等——问问自己,copy代码中的var有你曾经用let搜索并替换过吗?这些方法没有区别,只要能简单方便地解决问题即可。即使JavaScript本身是一个编译产物,它仍然易于阅读,易于修改,也易于反馈给上游backport。主流编译型语言做到这一点并不容易——类似于你把DLL中函数符号的机器码或者Java类文件中的字节码改了之后,直接向上游提交PR就可以了根据差异库。.这就是黑魔法的来源,也可能是前端的一种“道路自信”。其实作为本文的作者,我之前也尝试过一些类似的代码移植。这种工作就像破解密室逃脱游戏一样,非常有趣。个人感觉这次Vite迁移在实践手段上与之前的经历颇为相似:1995年将世界上最早的JS引擎源代码编译回JavaScript,从Flutter中抽取DartVM,在iOS上单独运行.原项目中,针对国产掌上电脑搭建了嵌入式Linux工具链,移植了QuickJS引擎。所以最后强烈鼓励大家多尝试兴趣驱动的技术。或许未来的某一天,折腾他们的经历可以帮你找到一个切入点,为你的业务赋能,形成一个闭环,打出一套组合拳
