每日优鲜供应链前端团队微前端改造

文 掘金号 Terry豆 @每日优鲜html

著做权归做者全部。商业转载请联系本帐号得到受权,非商业转载请注明每日优鲜大前端团队以及原文地址。前端

1、需求以及成果

我所在团队是作toB业务的,技术栈是Vue,团队目前有十多个典型的toB业务(菜单+内容布局),这些业务都是服务于一个大平台的,由于历史缘由,每一个业务都是独立的,都有一个html入口,因此当用户在这个大平台上使用这十多个业务的时候,每当切换系统时,页面都会刷新,体验不好;在开发层面,这十多个业务又有太多共同之处,每次修改为本都很高。
vue

最近有一个很重要的需求X,内容是这样的:从十多个项目中,每一个项目抽取若干功能组成一个新项目,基于现有架构的话,每当点击来自不一样系统的功能页面就要刷新一次,这是不可接受的。为了新需求X重复开发一遍这些业务功能又不现实,因此从技术角度来看,架构改造不可避免。
node

通过一番调研比对,咱们决定使用当下比较火的 SingleSpa 来完成改造(iframe方案尝试过,不太适合咱们的场景),目前改造已完成,咱们实现了如下效果:react

  • 只有一个不包含子项目(子项目指的是那十多个业务)资源的主项目,主项目只有一个html入口,子项目经过主项目来按需加载,子系统间切换再也不刷新;
  • 菜单栏、登陆、退出等功能都从子项目剥离,写在主项目里,再有相关改动只需修改主项目,包括错误监控、埋点等行为,只需处理一个主项目,十几个子项目再也不须要处理;
  • 子项目本来须要加载的公共部分(如vue、vuex、vue-router、ivew/element、私有npm包等),所有由主项目调度,配合webpack的externals功能经过外链的方式按需加载,一旦有一个子项目加载过,下一个子项目就不须要再加载,这样一来每一个子项目的dist文件里就只有子项目本身的业务代码(最终子项目包的体积缩小了80%,只有几十k),项目实际加载速度快了不少,肉眼可见;
  • 子项目并无从新开发,只是进行了一些改造,接入了微前端这套架构,因此新需求X的开发成本也极大的下降了,接入功能同时可供将来新增子项目使用;
  • 咱们的项目有本身的tab系统(相似浏览器的tab页签),这些tab页签经过keep-alive和一系列对缓存的处理,使其体验接近原生浏览器tab。

2、展现以及技术点

图1:项目外观示意图:

作微前端改造以前,蓝色系区域都是用公共包的方式由每一个子项目引入,因此子项目运行的时候展现的蓝色系部分都是相同的,给人一种在使用同一个系统的错觉,实际上切换系统的时候整个页面都要从新载入。
webpack

微前端改造后,只有橘红色区域是变化的,页面也再也不刷新。ios

图2:局部效果动图

图2展现了图1中的tab页签区以及子项目展现区。信息作了马赛克处理。
git

乍一看没什么特别的,但若是我说这些tab分别来自于不一样git仓库的独立vue项目呢?
这就是这套微前端架构的强大之处,让不一样单页vue项目能够随意组合成一个项目,而这些项目本身又是独立的vue项目
github

仔细看图2中路由的变化,hash路由的第一级决定了要加载哪一个子项目(work、sms、tms是三个不一样的git工程),不一样子项目间的切换也彻底没有刷新😁
web

为了让tab切换不刷新,这里使用了keep-alive去缓存页面,考虑到内存性能,在关闭tab页签时经过一些方法(主要是keep-alive的exclude属性)去除了keep-alive缓存,同时为了让子项目间的tab切换也不刷新,对图3下面提到的包装器也进行了不小的改造。让tab切换不刷新只是为了提高用户体验,这一步不是必要的,有必定的成本。

图3:部署架构示意图

实现一套微前端架构,能够把其分红四部分(参考:alili.tech/archive/110…

  • 加载器:也就是微前端架构的核心,图3中的“加载器JS文件”就是由加载器打包压缩出来的,这是原始的加载器:github.com/Fantasy9527… —— 能够把它理解成电源
  • 包装器:有了加载器,咱们要把现有的vue项目包装一下,使得加载器可使用它们,这是原始的包装器:github.com/CanopyTax/s… —— 若是想改造,建议改造这个部分,它至关于电源适配器
  • 主项目:通常是包含全部项目公共部分的项目—— 它至关于电器底座
  • 子项目:众多展现在主项目内容区的项目—— 它至关于你要使用的电器

因此是这么个概念:电源(加载器)→电源适配器(包装器)→️电器底座(主项目)→️电器(子项目)️

主项目和子项目都须要用包装器包装,只不过主项目的配置写法有不一样

加载器和包装器须要根据本身的需求作一些二次开发

总的来讲是这样一个流程:用户访问index.html后,浏览器运行加载器的js文件,加载器去读取图4中的配置文件,而后注册配置文件中配置的各个项目后,首先加载主项目(菜单等),再经过路由断定,动态远程加载子项目。

这里有个vue微前端版demo,包含最基础的效果与源码,务必研究一下这个demo再结合以上理论来帮助理解
*远程加载的子项目资源要在chrome的network中的xhr那一栏才能看到

图4:图3中的apps.config.js

用户访问index.html后,js加载器会加载apps.config.js。
不管路由是什么,每次必会首先加载主项目,再根据路由来匹配要加载哪一个子项目。
apps.config.js的生成如图3的绿色部分所示:

在资源服务器上起一个监听服务(我使用的是nodejs脚本+pm2守护),原有子项目的部署方式彻底不变(先后端彻底分离,资源带hash),当监听服务检测到文件改动时,去子项目部署文件夹里找它的index.html,把入口js用以下正则匹配出来,写入apps.config.js。

// content[i]为子项目文件夹名称。这段代码是nodejs脚本片断。
const reg = new RegExp(`src="\(\\/${content[i]}\\/index\\.\\w{8}.js\)`) // 对应图中的 /brain/index.3c4b55cf.js 复制代码

图4中的brain便是主项目,它的base属性为true,其他子项目的base属性为false

3、一些技术细节

这里说的的项目打包都是基于webpack。

System.js

它是实现远程加载子项目的核心。
咱们使用的是0.21版本的:github.com/systemjs/sy…
由于要动态经过http引入外部js,又不影响在开发的时候使用import、require方法,因此找到了systemjs来作这件事。
根据systemjs文档说明,咱们只须要把子项目打成umd格式(umd糅合了AMD和CommonJS)的包便可动态外部加载。

// 每一个子项目的webpack.config.js
output: {
    path: xxx,
    publicPath: xxx,
    filename: '[name].[chunkhash:8].js',
    chunkFilename: 'js/[name].[chunkhash:8].chunk.js',
    libraryTarget: 'umd', // 这里必定要写成umd,否则打出来的包system.js没法读取
    library: xxx, //模块的名称
},
复制代码

Webpack Externals

文档:www.webpackjs.com/configurati…
这么多同类型的vue项目,必定有大量的重复代码、重复引用,因此这是一块巨大的性能优化点,经过配置externals能够极大减少子项目打包出来的体积。

我并无彻底按照文档说明的方式来从CDN引入,缘由是这样的:入口index.html只有一个,若是按文档来作,一次引入全部CDN资源,可能子项目A用获得这些,但子项目B用不到这些,而我只访问了子项目B而已,这样不就多加载了无用的资源吗?
通过一番调研,一样利用systemjs解决了这个问题

// 每一个子项目本身的webpack.config.js,根据使用状况设置externals
 externals: {
      'axios': 'axios',
      'vue': 'Vue',
      'vue-router': 'VueRouter',
      'vuex': 'Vuex',
      'iview': 'iview',
      'moment': 'moment',
      'echarts': 'echarts',
      '@mfb/pc-utils-micro':'@mfb/pc-utils-micro', // 私有公共方法包
      '@mfb/pc-components-micro':'@mfb/pc-components-micro', // 私有公共组件包
      // '@mfb/pc-components-micro':'@mfb/pc-components-micro-0.2.1', // 若是须要指定版本,则用这一行替换上一行 
      ...
  },
复制代码
// index.html 整个微前端的惟一入口
<script src="system.js"></script>
<script>
  SystemJS.config({
    map: {
      "Vue": "//xxx.cdn.cn/static/vue/2.5.17/vue.min.js",
      "vue": "//xxx.cdn.cn/static/vue/2.5.17/vue.min.js", // 由于iview前置须要vue,是小写的,就又声明了一次
      "Vuex": "//xxx.cdn.cn/static/vuex/3.0.1/vuex.min.js",
      "VueRouter": "//xxx.cdn.cn/static/vueRouter/3.0.1/vue-router.min.js",
      "iview": "//xxx.cdn.cn/static/iview/3.3.2/iview.min.js",
      "moment": "//xxx.cdn.cn/static/moment/2.22.2/moment.min.js",
      "axios": "//xxx.cdn.cn/static/axios/0.15.3/axios.min.js",
      "echarts": "//xxx.cdn.cn/static/echarts/4.2.1/echarts.min.js",
      "@mfb/pc-utils-micro": "//xxx.cdn.cn/static/mfb-pc-utils-micro/mfb-pc-utils-micro-0.0.6.js",
      "@mfb/pc-components-micro": "//xxx.cdn.cn/static/mfb-pc-components-micro/mfb-pc-components-micro-0.0.42.js",
      "@mfb/pc-components-micro-0.2.1": "//xxx.cdn.cn/static/mfb-pc-components-micro/mfb-pc-components-micro-0.2.1.js" // 若是须要指定版本
    }
  })
</script>
复制代码

如此一来,systemjs只是在加载index.html时注册了这些CDN地址,不会直接去加载,当子项目里用到的时候,systemjs会接管模块引入,systemjs会去上面注册的map中查找匹配的模块,就再动态去加载资源。这样就避免了不一样子项目在这套架构下产生的多余加载。

按咱们的配置,webpack打包后,externals配置的模块不会打包进bundle,会被摘出来按umd规范经过requre/define方式去加载。

看systemjs源码会发现它从新定义了require和define方法,因此它能接管externals的外部引入过程。

4、总结体会

我最直白的感觉是实现了项目级别的模块化,把不一样项目变成了一个个模块来拼装组合,也就是说模块化从项目内提高到了项目自己

总结一下使用这套架构收到的好处,分为如下几点:

  • 缩小项目打包体积(平均每一个子项目bundle不到100k),而整合后的公共资源只需加载一次,性能获得很大提高 (技术角度)
  • 用户体验更好,用户感知不到本身在使用多个不一样的项目,更加平顺流畅 (产品角度)
  • 不一样git的项目通过改造后,能够随意以项目内每一个路由页面为单元拼装成一个新项目,产品灵活性本质上获得提高 (产品/技术角度)
  • 技术尝新,使用业界比较先进的微前端理念,几十个项目,成千上百个功能也能很好的分模块管理。 (管理角度)

也是有不少麻烦之处,须要消耗必定成本:

  • 由于多个vue实例在同一个document里,须要避免全局变量污染、全局监听污染、样式污染等,须要制定接入规范。
  • 使用了external抽离公共模块(好比Vue、Vue-router等)后,构造函数(或者Class)的污染也须要避免,好比Vue.mixin、Vue.components、Vue .use等等都须要作一些额外的工做去避免它们产生冲突。
  • 若是你也想要tab切换不刷新(使用keep-alive),那须要作的工做更多,主要是处理缓存,防止堆内存溢出(用chrome自带的performance monitor查看),还有项目间切换时路由钩子等等的处理。

不过跟收益比起来,这些成本就不算什么了~

最后要说一下,并非全部场景都适合微前端,尤为是项目规模小、数量少的场景不建议使用。
什么样的场景适合这套架构呢?通常有如下特征:

  • 项目很,规模很,都是每一个项目独立使用git此类仓库维护的、技术栈为vue/react/angular的这类应用
  • 须要整合到统一平台上,你正在寻找可能比iframe更合适的替代方案
  • 项目A有功能A一、A二、A3,项目B有功能B一、B二、B3,产品经理要你把A二、B一、B3组合成一个包含这些功能的新项目

可能你会问:为何不一开始就把全部须要整合的功能用一个git来维护?
答:理想是美好的,谁也没有先知能力,随着公司业务发展亦或是组织架构的改变、人员更迭,以上场景是几乎不可避免的;我很难想象十多个项目的好几百个功能都在一个git里管理起来有多困难。
可能你还会问,那我把须要整合的业务整合成到一个git仓库呢?
答:这固然是一个解决办法,前提是整合的成本你能接受;而且未来还有这类需求呢?每次都要手动整合业务代码到同一个git仓库吗?假设全部人都只维护这个整合完的git仓库,并行的需求线多了,上线时间会不会拥挤?一个功能产生了致命错误,会不会全部功能跟着出问题?

最后我想说:

咱们作这套框架的初衷是解决眼前的问题,然而发现它附带的潜力价值却比想象的多得多。

招贤纳士

招前端贤士,隶属每日优鲜前端团队,更多小伙伴期待与你一块儿浪。若是你觉的一技之长没地方施展,若是你以为想玩更潮的技术,若是你厌倦了一成不变,若是你觉的到了瓶颈没法突破,若是你以为干了这么多年前端突然迷茫了,若是你有股创业劲却没机会,若是若是,那是时候做出改变和尝试了,期待你跟咱们聊聊,写点什么发给咱们。大前端TL邮箱:mazhuang@missfresh.cn 坐标:北京

相关文章
相关标签/搜索