最近实现的简单、透明、组件化的微前端方案,整体感觉不错,得到了很多人的反馈,很有学习参考价值。但是很多朋友在这个方案的打包和配置中遇到了一些问题。事情要从头做到尾,挖的坑要完善。今天和大家分享一下Vite微应用解决方案插件的开发过程。可以参考文章:问题点总结。在Vite中使用该微前端方案时,您会遇到以下问题:Vite的打包资源默认使用HTML作为入口,而我们的微前端方案需要以JS为入口,以JS为切入点。importscheme打包导出代码去掉import.meta语句打包翻译成{}空objectchunk分隔的CSS文件,Vite在main.js中默认使用document.head.appendChild处理打包后的CSS文件没有引用资源路径在里面。手动编写新的URL(图片、import.meta.url)太麻烦了。通过配置解决问题。首先,前三个问题都可以通过Vite来解决。vite兼容rollup配置问题1,修改JS入口,需要修改vite配置。将build.rollupOptions.input设置为src/main.tsx,这样Vite会默认使用自定义配置的main.tsx作为打包的入口文件。不再生成index.html。问题2,rollup的一个特性默认会清理入口文件的导出模块,可以配置preserveEntrySignatures:'allow-extension'保证打包后导出模块不会被移除。问题三,看了Vite的Issue,很多人遇到了这个问题。起初,他们认为Vite默认处理它。看了Vite源码后,并没有找到处理的逻辑。它应该由esbuild翻译。所以将build.target设置为esnext即可解决问题,即import.meta属于es2020,也可以设置为具体的es2020。Configuration:exportdefaultdefineConfig({build:{//es2020支持import.meta语法target:'es2020',rollupOptions:{//用于控制Rollup尽量保证entryblock和baseentrymodule有相同的exportpreserveEntrySignatures:'allow-extension',//入口文件input:'src/main.tsx',},},});写一个Vite插件我们可以写一个插件来封装上面的配置。一个普通的Vite插件很简单defineConfig({plugins:[{//可以使用Vite提供的hook和rollup},],});plugins可以做很多事情,通过Vite和rollup提供的hooks来自定义解析、编译、打包输出的整体流程。插件一般不会直接写在vite.config.ts中。您可以定义一种方法来导出此插件。这里可以使用confighook提供默认的Vite配置,封装自定义配置:exportfunctionmicroWebPlugin():Plugin{//Pluginhookreturn{name:'vite-plugin-micro-web',config(){return{build:{target:'es2020',rollupOptions:{preserveEntrySignatures:'allow-extension',input:'src/main.tsx',},},};},};}这样一个简单的插件就完成了。Vite特有的钩子config-在解析Vite配置之前调用,它可以返回一个部分配置对象,将深度合并到现有配置中,或者直接更改配置configResolved-在解析Vite配置之后调用,使用这个钩子来读取和存储最终的已解析配置configureServer-用于配置开发服务器的挂钩transformIndexHtml-用于转换index.html的专用挂钩。Hook接收当前HTML字符串和转换上下文handleHotUpdate-执行自定义HMR更新处理。rolluphookrolluphook有很多,分为两个stageCompilationstage:Outputstage:我们这里会用到的hook有:transform-用来转换加载的模块内容generateBundle-编译代码块生成阶段styleinsertionnode处理问题4、document.head.appendChild使用transformhook替换Vite默认的document.head.appendChild作为自定义节点。cssCodeSplit被打包成一个CSS文件。我们默认使用cssCodeSplit将其打包成一个CSS文件,免去了用插件transform修改Vite的逻辑。问题5,即打包后的CSS没有引用。要用hash得到这个CSS,我们可以有多种方案。使用HTML打包方式,将index.html中的JS和CSS文件提取出来单独处理,不添加样式文件名hash,协议固定样式名,通过hook提取文件名。权衡之下,最终使用generateBundle阶段提取Vite编译生成的CSS文件名,通过修改入口代码插入。但是generateBundle已经在输出阶段,不会经过transformhook。找到了两全其美的方法:创建一个非常小的入口文件main.js,它也可以用哈希和主应用程序时间戳进行缓存。asyncgenerateBundle(options,bundle){//主入口文件letentry:string|不明确的;//所有CSS模块constcssChunks:string[]=[];//为(constchunkNameofObject.keys(bundle))找到入口文件和CSS文件{if(chunkName.includes('main')&&chunkName.endsWith('.js')){entry=chunkName;}if(chunkName.endsWith('.css')){//使用相对路径避免后续ESM无法解析模块cssChunks.push(`./${chunkName}`);}}//继续下面的代码}生成一个新的入口文件,通过bundle提取文件向上获取带hash的JS和CSS入口。现在需要编写一个新文件main.js。rollup中有一个APIemitFile可以触发资源文件的创建。接下来进行处理://继续上面的代码if(entry){constcssChunksStr=JSON.stringify(cssChunks);//创建一个很小的入口文件,配合hash和主应用时间戳缓存处理this.emitFile({fileName:'main.js',type:'asset',source:`//取microAppEnv参数,使用相对路径避免错误importdefineAppfrom'./${entry}?microAppEnv';//创建链接标签functioncreateLink(href){constlink=document.createElement('link');link.rel='stylesheet';link.href=href;returnlink;}//入口文件导出一个方法将打包好的css文件通过link的方式插入到对应的节点defineApp.styleInject=(parentNode)=>{${cssChunksStr}.forEach((css)=>{//import.meta.url保持路径正确,方括号中的值避免被rollup转换Dropconstlink=createLink(newURL(css,import.meta['url']));parentNode.prepend(link);});};exportdefaultdefineApp;`,});}插件需要应用入口配合exportstyleInject方法提供样式插入,我们通过enc解决封装输入方法。封装一个调用应用入口的方法:exportfunctiondefineMicroApp(callback){constdefineApp=(container)=>{constappConfig=callback(container);//处理样式本地插入constmountFn=appConfig.mount;//获取constinject=defineApp.styleInject中的插件方法;if(mountFn&&inject){appConfig.mount=(props)=>{mountFn(props);//加载后,插入样式inject(container);};}返回应用配置;};returndefineApp;}现在build之后会生成一个没有hash的main.js文件,主应用可以正常加载打包好的资源。进一步优化,main.js的压缩混乱可以用vite编译js',类型:'资产',来源:result.code,});在子应用路径问题之前,我们需要手动添加新的URL(image,import.meta.url)来修复子应用路径问题。此逻辑由转换挂钩自动处理。在此插件之前,Vite会将所有资源文件转换为路径importlogofrom'./logo.svg';//转换为:exportdefault'/src/logo.svg';因此,我们只需要将exportdefault的"resourcepath"替换为exportdefaultnewURL("resourcepath",import.meta['url']).href。constimagesRE=newRegExp(`\\.(png|webp|jpg|gif|jpeg|tiff|svg|bmp)($|\\?)`);transform(code,id){//纠正图片资源使用绝对地址if(imagesRE.test(id)){return{code:code.replace(/(export\s+default)\s+(".+")/,`$1新URL($2,import.meta['url']).href`),map:null,};}returnundefined;},完成,一个比较完整的Vite微应用方案就诞生了。看效果:多带插件,可以玩出意想不到的东西。本微前端方案未实现以下隔离方式,不保证未来一定会实现。你可以发挥更多的想象力。CSS样式隔离通过插件在主应用节点添加修改idCSS.name{color:red;}/*转换为*/#id.name{color:red;}但前提是你需要设置每个id都是唯一的。样式性能会受到影响,CSSModules方案会更好。JS沙箱虽然ESM中的运行时沙箱目前还没有现成的解决方案,但是运行时沙箱的性能很差。换一种思路,可以从编译时沙箱入手。使用transformhook将应用程序的所有窗口都转换成sandboxfakeWindows,从而达到隔离的效果。学习插件仓库可以克隆代码示例:https://github.com/MinJieLiu/micro-app/tree/main/packages/micro-vite-plugin微前端示例:https://github.com/MinJieLiu/micro-app-demo
