在写这篇文章的一个多月前,本坑还不知道微前端是什么,大概从字面上的含义是比较小的前端项目。css
本坑开始实践它,是因为工做要求。改造一个运行多年,前端用jsp写的服务平台项目(如下简称该平台)。改造它是改造它的前端架构。改造它的缘由是比较多人反馈,其页面加载和渲染显得吃力,页面切换后首屏等待时间长的问题,交互体验温馨度不可避免的降低了,特别是在老式电脑面前。html
该平台业务比较多,因此组长但愿前端框架组能把平台中的前端部分分离出来,最好用当下满大街的Vue、可以按照各个一级菜单分红若干前端子项目,用户访问依然是总体的项目,同时这一改造实施过程不须要重作一个、而是整个500多个页面从局部开始、是逐步、兼容的,新旧同时运行,直至总体被替换。(ps:不重作?这......科学吗?)前端
其实大概知道慢在哪里,可是不知道究竟慢在具体哪一个部分。和其余一以贯之的相似管理平台布局并没有不一样。左边导航栏,上面顶栏,右侧内容栏,总体页面是一个index.jsp。上面提到的内容栏是一个iframe,里面经过切换src来切换页面。更多的业务造就更多页面,更多页面带来更多的加载。加上长时间没有作好资源加载的管理,致使渲染一次页面须要加载大量js,css或者屡次加载同一个文件的状况。该平台大量的配置页面生成,是经过easyui的来作的。经过数据来创造整个页面dom节点,也拖累了内容完整呈现的时间。vue
咱们使用谷歌浏览器performance能够最终追查到这个系统在哪些方面,哪一个方法存在着哪些延迟。结论是:node
一、混乱的项目资源管理致使大量的资源请求。jquery
二、easyui和项目中很多的dom操做带来大量的重排和重绘。ios
三、埋点,插件使用不当以及其余。web
它是什么呢?vue-router
微前端的概念来自于以前流行的微服务。它的来源很大程度是来自于这篇文章 。微服务系统使得后台服务架构可以比较好地规避愈来愈臃肿的体积带来的性能降低。根据业务合理拆分红一个个的服务,尽可能避免一个子服务影响整个项目运行的优点,有效的进行隔离。vuex
那么,前端也有一样的需求吗?答案是确定的。
今天,日益更新的前端技术,已经可以把一个个页面各个小元素打包成组件库,功能包,在多个项目中引入使用。此外,咱们不用再使用难受的iframe来聚合不一样的项目,而是导出一个个web component,只须要import 到页面就可使用。把一个个子项目打包成一个个web component,聚合在入口项目以内。这也许就是微前端如今比较时髦的样子。
若是一个大项目有如下特色,微前端能够在这些项目中运用:
一、大项目有统一的入口,子项目页面须要无刷新下切换,但是各个子项目在业务上和开发团队上是不一样的。
二、项目过大,打包、运行、部署效率出现显著降低的问题。这时但愿能根据业务拆分打包,部署。
回到本文开头,一开始面对这样的需求仍是有些想辞职的冲动,由于以为需求有点不是符合实际,实际上要实施改版也是须要过程的。
不过静下来想一想,搜搜,翻了翻当前项目的前端结构,隐隐约约彷佛浮现一些需求可行性的线索。
由于项目的最终目的是把整个jsp页面改为vue来写。而这一要求是逐步替换的过程,因此在改造过程当中,同时要保证项目兼容jsp的页面。咱们继续沿用了原来就有的iframe,借此把jsp融入整个微前端框架,而已经改造的micro则不须要iframe.
咱们的开发团队,分框架组和各个业务组。其中每一个业务组有3到8我的,他们大多数是后端背景,主要作的也是后端开发。框架组有前端和后端。为了应付庞大的业务开发需求。大部分后端人员都须要使用jsp,js等前端技术进行开发。
框架组为了减小他们的前端开发门槛,前端框架组会封装好easyui组件,提供业务组使用。因此,正如前文提到,后端人员是经过数据,结合框架组提供的组件来完成页面的开发的。从某种角度来讲,数据配置的页面对接下来的改造工做有必定的帮助,由于大部分页面能够同时改写。
咱们对整个项目进行了大体的分类。
一、portal 项目:该项目是整个微前端项目的入口。里面含有loader,用以加载各个项目模块。它也嵌入到子项目中,使得单独运行子项目和portal项目同样的界面要求。
二、permission 项目:该项目包含菜单组件,登陆页面,顶栏组件,权限控制等。在任何环境下,它都必须首先加载,为子项目模块挂载提供锚点。
三、common项目:该项目包含公共业务组件。好比封装好的页面,能够直接给不太可以掌握vue项目的后端人员更加友好的去使用。
四、业务项目:就是指业务组各个模块开发的前端项目。什么样的业务分为一个项目,这点由产品和技术人员一块儿来决定。相对于portal项目,业务项目至关于它的子项目。
前端框架组必须提供一套统一的业务项目的前端模板,能够在确认新建的子项目后迅速的加入到整个项目中,进行开发和部署,而这一过程不能影响其余项目的部署和运行。
除了上述方案浮出水面,还会在改造过程当中遇到一个个细节问题。 不过在大方向,框架组成上,前端结构上作好了,细节问题也会随耐心和时间被解决。
本坑根据以上的分类,大体进行说明其实现。这其中结合了很多前辈之经验,在文章结尾处鸣谢。
portal 项目是整个项目部署的入口,它的核心来自于single-spa
在整个项目结构中它将集成到每个子项目。集成的方式很粗暴简单,就是外联加载。
portal负责根据不一样的环境来对应的组件和app,同时也安装各个app,卸载各个app等,它负责app在single-spa的生命周期。好比集成模式下根据环境和路由加载对应的app,而在子项目运行时只加载公共组件和不一样业务的app。
那么protal是如何加载的呢?
protal维护了一个json里面包含了各个子项目的index.html的信息,经过匹配index.html里面的src 、link,加载各项资源。
module.exports = { common: { webName:'common', globalVarName: 'mfe:common', componentsTarget: '/common/release/components/web.html', resourcePatterns: ['/components.[0-9a-z]{8}.js/g'], loadType:'before' }, permission: { webName:'permission', globalVarName: 'mfe:permission', // URL 匹配模式 matchUrlHash: '', // 微前端地址 componentsTarget: '/permission/release/components/web.html', webTarget:'/permission/release/web/web.html', // 资源匹配模式 resourcePatterns: ['/common.[0-9a-z]{8}.css/g','/store.[0-9a-z]{8}.js/g', '/publicPath.[0-9a-z]{8}.js/g','/singleSpaEntry.[0-9a-z]{8}.js/g','/components.[0-9a-z]{8}.js/g'], //是否要在项目启动前加载,before为提早加载,after为hash变化后加载 loadType:'before' }, app4vue:{ webName:'repair-order', globalVarName: 'mfe:app4vue', matchUrlHash: '/layout/repair-order', webTarget: '/app4/release/web/web.html', resourcePatterns: ['/common.[0-9a-z]{8}.css/g','/store.[0-9a-z]{8}.js/g', '/singleSpaEntry.[0-9a-z]{8}.js/g'], loadType:'after' } }
async gatherResource () { const self = this // const spaEntry = 'portal' const web = self._webName //若是是微前端聚合模式 if (window._IS_SIGLE_PORTAL) { if (web !== 'mfe-permission') { await self.loadComponents(micros.common) await self.loadApp(micros.permission) } } else { if (web === 'mfe-permission') { await self.loadComponents(micros.common) } else { if (web !== 'mfe-common') { await self.loadComponents(micros.common) } await self.loadApp(micros.permission) } } // return new Promise(resolve => resolve('loader:all Finish!')) }
permission负责登陆页,layout中的菜单栏,顶栏。全部的子项目app都必须挂载到permission项目中的显示区块里。也就是说permssion会提供锚点给子项目挂载。
因此permission负责路由的控制,这里的路由有整体路由和app内路由切换。若是app切换的路由控制涉及到singer-spa,app的切换会触发single-spa:routing-event事件,portal监听该事件 unmounted和mounted app,若是app内部的路由切换,须要触发app内部路由切换。
本坑尝试监听permission的 hash,因为vue新版本,hash实际是监听不了的,因此监听hash是没办法的。
这看上去会干涉到子项目的代码,若是哪位大神有好方法能够在评论区贴上你的见解。
业务项目的独立运行只会发生在开发模式之下,在生产或者测试环境并不会独立运行。
集成环境下输出成三个周期,提供给single-spa
export var global = {}; export const bootstrap = () => { return Promise.resolve(); } export function mount (props) { Vue.mixin({ data: function () { return { props } } }) return Promise.resolve().then(() => { createDomElement(); global.instance = new Vue({ el: '#app4', router, render: h => h(App) }) }) } export function unmount () { return Promise.resolve().then(() => { global.instance.$destroy(); global.instance.$el.innerHTML = ''; delete global.instance }) } function createDomElement () { // Make sure there is a div for us to render into let node = document.getElementById('main-content'); let el = document.getElementById('app4'); if (!el) { el = document.createElement('div'); el.id = 'app4'; node.appendChild(el); } return el; }
开发模式独立运行
const init = async () => { //启动single-spa const loader = new Loader(process.env) await loader.startSingleSpa() Vue.mixin({ data () { return { loader } } }) Vue.config.productionTip = false; //permission渲染后再挂载本身上去 window.addEventListener('single-spa:main-content-mount', evt => { if (!window.vim) { window.vm = new Vue({ el: loader.createHookEle('app4'),//挂载本身 // store, router, render: h => h(App) }) } }) } init()
common 相似插件的打包,不赘述。
import './styles/vars.scss' import MButton from './components/button' const components = [MButton]; const install = function (Vue) { if (install.installed) return; components.map(component => { Vue.use(component); }); }; // 全局引用可自动安装 if (typeof window !== 'undefined' && window.Vue) { install(window.Vue); } export default { install, MButton }
几个主要源码采用外联形式
<link rel="stylesheet" href="/micro-frontend/common/release/components/common.css"> <script rel="preload" src="/micro-frontend/base/vue/2.6.10/vue.min.js" as="script"></script> <script rel="preload" src="/micro-frontend/base/element-ui/2.7.2/lib/index.js" as="script"></script> <script rel="preload" src="/micro-frontend/base/vue-router/3.0.6/vue-router.min.js" as="script"></script> <!-- <script rel="preload" src="/micro-frontend/base/redux/4.0.1/redux.min.js" as="script"></script> --> <script rel="preload" src="/micro-frontend/base/vuex/2.2.1/vuex.min.js" as="script"></script> <script rel="preload" src="/micro-frontend/base/axios/0.15.3/axios.min.js" as="script"></script> <script rel="preload" src="/micro-frontend/base/jquery/3.1.0/jquery.min.js" as="script"></script>
single-spa是怎么聚合各个独立的vue app的呢?本坑尝试理解它
Single-spa 把app聚合成三个周期 bootstrap mounted unmounted,这三个周期是须要本身去配置改写的,其实single-spa还有其余周期,不须要改写。
也就是对single-spa来讲app只有这三个东西须要特别的关心,app的卸载和加载。其余都是app本身的事。mounted、unmonuted和vue app 独立运行的mounted和destroy本质上没有区别,只是single-spa作了一层代理。代理完成app的挂载和销毁。
single-spa内部也保存了一个数组,负责维护内部注册的app.注册完后代理完成app的挂载。卸载后销毁之。
若是亲爱的客官,你也遇到这种问题,用这种改法是没有保证的。
本坑实践它很大的理由也是用本身的方法初探微前端实践方法的可行性。
这种大跨度的改变会带来很多不可预测的底层冲突,先后端的冲突。
第二呢,这种大跨度的改变几乎等同于重构。
第三呢,微前端方案也有自身的局限性,好比对库版本的管理,app样式的隔离没有作到很好等等。仍是要根据实际来衡量。
针对太过古老的系统好比jsp,能够先尝试把jsp转为html,在前端性能上多改进,再另行考虑综合性改版。
估计不少地方搞的很差,代码或者信息错误,欢迎各位指导。