前端分享会--微前端改造初探

在写这篇文章的一个多月前,本坑还不知道微前端是什么,大概从字面上的含义是比较小的前端项目。

本坑开始实践它,是由于工作要求。改造一个运行多年,前端用jsp写的服务平台项目(以下简称该平台)。改造它是改造它的前端架构。改造它的原因是比较多人反馈,其页面加载和渲染显得吃力,页面切换后首屏等待时间长的问题,交互体验舒适度不可避免的下降了,特别是在老式电脑面前。

该平台业务比较多,所以组长希望前端框架组能把平台中的前端部分分离出来,最好用当下满大街的Vue、能够按照各个一级菜单分成若干前端子项目,用户访问依然是整体的项目,同时这一改造实施过程不需要重做一个、而是整个500多个页面从局部开始、是逐步、兼容的,新旧同时运行,直至整体被替换。(ps:不重做?这......科学吗?)

看看,慢在哪里

其实大概知道慢在哪里,但是不知道究竟慢在具体哪个部分。和其他一以贯之的类似管理平台布局并无不同。左边导航栏,上面顶栏,右侧内容栏,整体页面是一个index.jsp。上面提到的内容栏是一个iframe,里面通过切换src来切换页面。更多的业务造就更多页面,更多页面带来更多的加载。加上长时间没有做好资源加载的管理,导致渲染一次页面需要加载大量js,css或者多次加载同一个文件的情况。该平台大量的配置页面生成,是通过easyui的来做的。通过数据来创造整个页面dom节点,也拖累了内容完整呈现的时间。

我们使用谷歌浏览器performance可以最终追查到这个系统在哪些方面,哪个方法存在着哪些延迟。结论是:

1、混乱的项目资源管理导致大量的资源请求。

2、easyui和项目中不少的dom操作带来大量的重排和重绘。

3、埋点,插件使用不当以及其他。

系统截图
微前端

它是什么呢?

微前端的概念来自于之前流行的微服务。它的来源很大程度是来自于这篇文章 。微服务系统使得后台服务架构能够比较好地规避越来越臃肿的体积带来的性能下降。根据业务合理拆分成一个个的服务,尽量避免一个子服务影响整个项目运行的优势,有效的进行隔离。

那么,前端也有同样的需求吗?答案是肯定的。

今天,日益更新的前端技术,已经能够把一个个页面各个小元素打包成组件库,功能包,在多个项目中引入使用。此外,我们不用再使用难受的iframe来聚合不同的项目,而是导出一个个web component,只需要import 到页面就可以使用。把一个个子项目打包成一个个web component,聚合在入口项目之内。这也许就是微前端现在比较时髦的样子。
如果一个大项目有以下特点,微前端可以在这些项目中运用:
1、大项目有统一的入口,子项目页面需要无刷新下切换,可是各个子项目在业务上和开发团队上是不同的。

2、项目过大,打包、运行、部署效率出现显著下降的问题。这时希望能根据业务拆分打包,部署。

方案与实施

回到本文开头,一开始面对这样的需求还是有些想辞职的冲动,因为觉得需求有点不是符合实际,实际上要实施改版也是需要过程的。

不过静下来想想,搜搜,翻了翻当前项目的前端结构,隐隐约约似乎浮现一些需求可行性的线索。

因为项目的最终目的是把整个jsp页面改成vue来写。而这一要求是逐步替换的过程,所以在改造过程中,同时要保证项目兼容jsp的页面。我们继续沿用了原来就有的iframe,借此把jsp融入整个微前端框架,而已经改造的micro则不需要iframe.
我们的开发团队,分框架组和各个业务组。其中每个业务组有3到8个人,他们大多数是后端背景,主要做的也是后端开发。框架组有前端和后端。为了应付庞大的业务开发需求。大部分后端人员都需要使用jsp,js等前端技术进行开发。
框架组为了减少他们的前端开发门槛,前端框架组会封装好easyui组件,提供业务组使用。所以,正如前文提到,后端人员是通过数据,结合框架组提供的组件来完成页面的开发的。从某种角度来说,数据配置的页面对接下来的改造工作有一定的帮助,因为大部分页面可以同时改写。

我们对整个项目进行了大致的分类。

1、portal 项目:该项目是整个微前端项目的入口。里面含有loader,用以加载各个项目模块。它也嵌入到子项目中,使得单独运行子项目和portal项目一样的界面要求。
2、permission 项目:该项目包含菜单组件,登录页面,顶栏组件,权限控制等。在任何环境下,它都必须首先加载,为子项目模块挂载提供锚点。
3、common项目:该项目包含公共业务组件。比如封装好的页面,可以直接给不太能够掌握vue项目的后端人员更加友好的去使用。
4、业务项目:就是指业务组各个模块开发的前端项目。什么样的业务分为一个项目,这点由产品和技术人员一起来决定。相对于portal项目,业务项目相当于它的子项目。

前端框架组必须提供一套统一的业务项目的前端模板,可以在确认新建的子项目后迅速的加入到整个项目中,进行开发和部署,而这一过程不能影响其他项目的部署和运行。

除了上述方案浮出水面,还会在改造过程中遇到一个个细节问题。 不过在大方向,框架组成上,前端结构上做好了,细节问题也会随耐心和时间被解决。

分别阐述

本坑根据以上的分类,大致进行说明其实现。这其中结合了不少前辈之经验,在文章结尾处鸣谢。

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!'))
  }
permisson:

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是没办法的。

permission监控路由发出事件,app内部监听,从而触发路由
permission监控路由发出事件,app内部监听,从而触发路由

这看上去会干涉到子项目的代码,如果哪位大神有好方法可以在评论区贴上你的看法。

业务项目

业务项目的独立运行只会发生在开发模式之下,在生产或者测试环境并不会独立运行。

集成环境下输出成三个周期,提供给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

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

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,在前端性能上多改进,再另行考虑综合性改版。

估计很多地方搞的不好,代码或者信息错误,欢迎各位指导。

鸣谢

single-spa官网

微前端实践

前端微服务化解决方案

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • Rekit 2.0 出了好用的新特性!文章翻译来自:http://rekit.js.org/著作权归作者所有。商业...
    moofyu阅读 9,074评论 1 16
  • 记得我小的时候,每年元宵节,可以凭票到街上买到元宵:用牛皮纸袋包装的,一袋半斤。元宵大小和狮子头差不多,只有黑芝麻...
    helloKimmy阅读 215评论 2 8
  • 打开它的包装,里面有一个金黄色的饼干,上面涂有香浓的黑巧巧克力,巧克力上还画着可爱的小熊猫,它有一双明亮的...
    张文哲阅读 194评论 0 3
  • 生活在这个世上,每个人都有自己的难言之隐和苦处,尤其人到中年。有人选择沉默,有人善于倾诉,无论哪种,给自己最好的解...
    立夏至阅读 161评论 0 1