当前位置: 首页 > 科技观察

vite微前端实践,实现组件化解决方案

时间:2023-03-14 19:10:18 科技观察

本文转载自微信公众号《前端之星》,作者Melody。转载请联系锋星公众号。什么是微前端?微前端是多个团队通过独立发布功能共同构建现代Web应用的技术手段和方法策略。微前端借鉴了微服务的架构理念,将一个庞大的前端应用拆分成多个独立且灵活的小应用。应用程序。微前端不仅可以将多个项目集成为一个,还可以降低项目之间的耦合度,提高项目的可扩展性。相对于整个前端仓库,微前端架构下的前端仓库更趋向于更小、更灵活。特点技术栈独立主框架不限制接入应用的技术栈,子应用可自主选择技术栈独立开发/部署各团队间仓库独立,独立部署,相互独立增量升级当应用庞大时,技术升级或重构相当繁琐,而微应用具有渐进升级的特点。独立运行时,微应用运行时互不依赖,具有独立的状态管理,提高效率。应用越大,越难维护,协作效率越低。微应用可以很好的拆分,提高效率目前可用的微前端解决方案目前微前端的解决方案有以下几种:基于iframe的完全隔离方案作为前端开发,我们对iframe已经非常熟悉了,在一个应用程序中可以独立运行另一个应用程序。它有显着的优点:非常简单,不需要任何改造,完美隔离,JS、CSS是独立的运行环境,无限使用,页面可以放置多个iframe组合服务当然缺点也很突出:路由状态无法维护,刷新后路由状态丢失。完全隔离使得与子应用程序交互变得极其困难。iframe中的弹窗无法突破自身对整个应用的全资源加载,加载太慢。这些明显的缺点也催生了其他的解决方案。基于single-spa的路由劫持方案single-spa通过劫持路由实现子应用之间的切换,但是接入方式需要集成自身的路由,有一定的局限性。qiankun孵化自蚂蚁金服基于微前端架构的云产品统一接入平台。它封装了single-spa。主要解决single-spa的一些痛点和不足。通过import-html-entry包解析HTML获取资源路径,然后解析加载资源。通过修改执行环境,实现JS沙箱、样式隔离等特性。京东小程序解决方案京东小程序没有沿用single-spa的思想,而是借鉴了WebComponent的思想,通过CustomElement结合一个自定义的ShadowDom,将微前端封装成一个类似webComponents的组件,从而实现微前端组件的优化渲染。在Vite上使用微前端我们从UmiJS迁移到Vite之后,微前端也势在必行,当时我们研究了很多解决方案。为什么没用?qiankunqiankun是目前社区主流的微前端解决方案。虽然很完美也很流行,但是最大的问题就是不支持Vite。它根据import-html-entry解析HTML获取资源。由于qiankun是通过eval来执行这些js的内容,而Vite中的script标签type是type="module",里面包含了import/export等模块代码,所以会报错:Importisnotallowedinscriptsotherthantype=“模块”。退一步说,我们采用的是single-spa的方式,使用systemjs的方式进行微前端加载方案,踩了不少坑。single-spa没有友好的教程可以访问。文献虽多,但大多是概念性的,当时让人觉得深奥。看了它的源码,才知道这是什么东西。。。里面的大部分代码都是围绕着路由劫持展开的,完全没有文档中那种高大上的感觉。而我们又不能使用它的路由劫持功能,那我们为什么要使用它呢?从组件化的角度来看,single-spa的实现一点都不优雅。它劫持了路由,这与react-router和组件化思想不相容。接入方式中有很多复杂的配置单实例方案,即同一时间只显示一个子应用。思考完single-spa的不足,我们可以自己实现一个组件化的微前端方案。如何实现简单、透明、组件化的解决方案通过组件化思维实现一个微应用非常简单:一个子应用导出一个方法,主应用加载子应用并调用该方法,传入一个Element节点参数,子应用获取Element节点,appendChild自己的组件到Element节点。类型约定在这之前,我们需要约定一个主应用和子应用之间的交互方式。三个钩子主要用于保证应用程序的正确执行、更新和卸载。类型定义:exportinterfaceAppConfig{//Mountmount?:(props:unknown)=>void;//更新render?:(props:unknown)=>ReactNode|void;//卸载unmount?:()=>void;}子应用导出通过类型的约定,我们可以导出子应用:mount、render、unmount是主要的钩子。React子应用实现:exportdefault(container:HTMLElement)=>{lethandleRender:(props:AppProps)=>void;//包装一个新组件用于更新处理functionMain(props:AppProps){const[state,setState]=React.useState(props);//提取setState方法到render函数调用,保持父子应用触发更新handleRender=setState;return;}return{mount(props:AppProps){ReactDOM.render(,container);},render(props:AppProps){handleRender?.(props);},unmount(){ReactDOM.unmountComponentAtNode(container);},};};Vue子应用实现:import{createApp}from'vue';importAppfrom'./App.vue';exportdefault(container:HTMLElement)=>{//Createconstapp=createApp(App);return{mount(){//加载app.mount(container);},unmount(){//卸载app.unmount();},};};主应用实现了React,核心代码只有十几行,主要处理子应用交互(为了可读性,隐藏了错误处理代码):exportfunctionMicroApp({entry,...props}:MicroAppProps){//传递给子应用的节点constcontainerRef=useRef(null);//子应用ConfigurationconstconfigRef=useRef();useLayoutEffect(()=>{import(/*@vite-忽略*/条目).then((res)=>{//将div传递给子应用进行渲染constconfig=res.default(containerRef.current);//调用子应用的加载方法config.mount?.(props);configRef.current=config;});return()=>{//调用子应用的unmount方法configRef.current?.unmount?.();configRef.current=undefined;};},[entry]);return{configRef.current?.render?.(props)}

;}完成,至此主应用和子应用已经完成加载、更新、卸载。现在,它是一个可以同时渲染多个不同子应用程序的组件。这比单水疗中心优雅得多。入口子应用的地址,当然真实情况会根据dev和prod模式给出不同的地址:Vue实现如何让子应用可以运行single-spa独立等等,很多方案都是挂载一个变量到window上,判断这个变量是否在微前端环境,很不优雅。在ESM中,我们可以通过import.meta.url传入的参数来判断:>,document.getElementById('root'),);}入口导入修改://添加环境参数和当前时间,避免被缓存import(/*@vite-ignore*/`${entry}?microAppEnv&t=${Date.now()}`);BrowserCompatibilityIE浏览器已经逐渐退出了我们的视野。基于Vite,我们只需要支持引入特性的浏览器即可。当然,如果考虑IE浏览器,也不是不可以,很简单:把上面代码的import换成System.import,也就是systemjs,这也是single-spa推荐的用法。浏览器ChromeEdgeFirefoxInternetExplorerSafariimport611660否10.1动态导入637967否11.1import.meta647962否11.1Modulepublic我们的子组件是否必须使用挂载和卸载模式?答案不一定,如果我们的技术栈都是关于React的。我们的子应用程序只导出一个渲染就足够了。这样,同样的React被用于渲染。好处是子应用可以消费父应用的Provider。但是有一个前提,两个应用之间的React必须是同一个实例,否则会报错。我们可以预先将react、react-dom、styled-components等常用模块打包成ESM模块,然后在文件服务中使用。更改Vite配置以添加别名:defineConfig({resolve:{alias:{react:'//localhost:8000/react@17.js','react-dom':'//localhost:8000/react-dom@17.js',},},});所以你可以愉快地使用相同的React代码。它还可以提取主应用程序和子应用程序之间的公共模块,使应用程序的总体积更小。当然,如果你不是http2,就需要考虑粒度的问题。在线CDN解决方案:https://esm.sh也有importmap解决方案,兼容性不是很好,但是以后会是一个趋势:父子通信组件式微应用,可以通过传递参数进行通信,正是React组件通信的模型。资源路径importlogofrom'./images/logo.svg';;在Vite的dev模式下,子应用中的静态资源一般是这样导入的:importlogofrom'./images/logo.svg';;图片路径:/basename/src/logo.svg,在主应用中显示时为404。因为路径只存在于子应用中。我们需要将它与URL模块一起使用,这样源前缀将被带到路径前面:constlogoURL=newURL(logo,import.meta.url);;当然这样使用起来比较麻烦,我们可以将其封装为一个Vite插件来自动处理这种场景。路由同步项目使用的是react-router,所以可能会出现路由不同步的问题,因为不是同一个react-router实例。即,路由之间没有链接。在react-router支持自定义历史库,我们可以创建:import{createBrowserHistory}from'history';exportconsthistory=createBrowserHistory();//主要应用:路由入口{children};//主应用:传递给子应用}/>;//子应用:路由入口{children};最终的子应用程序使用相同的历史模块。当然,这不是唯一的实现方式,也不是优雅的方式。我们可以通过路由实例navigate给子应用,这样也可以实现路由交互。注意:子应用的basename必须和主应用的路径名一致。这里也需要修改Vite的配置base字段:exportdefaultdefineConfig({base:'/child-app/',server:{port:3002,},plugins:[react()],});JS沙箱因为沙箱在ESM下是不支持的,因为执行环境中的模块窗口对象不能动态改变,不能注入新的全局对象。一般React和Vue项目很少修改全局变量,最重要的是做好代码规范检查。CSS样式隔离自动CSS样式隔离是有代价的。一般我们建议子应用使用不同的CSS前缀,配合CSSModules基本可以满足需求。PackageDeploymentDeployment可以根据子应用的基础放在不同的目录下,名字要对应。配置nginx转发规则。我们可以统一子应用的路由前缀,让nginx区分主应用,配置通用规则。比如把主应用放在系统目录下,子应用放在app-开头的目录下:location~^\/app-.*(\..+)${root/usr/share/nginx/html;}location/{try_files$uri$uri//index.html;root/usr/share/nginx/html/system;indexindex.htmlindex.htm;}优点1.简单核心不到100行代码,并且不需要冗余文档2.通过协议实现灵活3.透明无任何劫持方案,逻辑更透明4.组件化组件化渲染和参数通信5.支持基于ESM的Vite,面向未来6.向下兼容可选的SystemJS方案有兼容低版本浏览器的例子吗?示例代码在Github上。有兴趣的朋友可以克隆学习。由于我们的技术栈是React,所以本例中主应用的实现使用的是React。微前端组件(React):https://github.com/MinJieLiu/micro-app微前端示例:https://github.com/MinJieLiu/micro-app-demoEpilogue微前端端解决方案最适合团队场景,创建一个团队可以控制的计划尤为重要。参考资料:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/import.metahttps://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/报表/导入