微前端实践初试

—— 一个基于single-spa的vue2升级至vue3的项目
Author 柯雨 Date 2021-01-21

一、 项目背景

随着2020年9月vue3的正式发布,为了在之后的工作之中能够逐步全面的使用vue3替代vue2,之前所开发的系统项目升级的需求也就提上了日程。考虑到第一,vue3的使用是新技术的尝试,第二,vue3的相关生态还在进一步完善之中,因此,我们使用了一个部门内部项目来作为我们的实践对象,在将其成功升级并等待Vue3相关生态完善稳定后再逐步推广至其它业务项目之中。

二、前期调研

经过调研,vue2升级至vue3的主流升级方式为更改侵入式修改项目代码,将vue3中不兼容的特性及代码进行手动的替换。这种方式的优势是升级技术难度较低,只需了解vue3与vue2的区别即可。但于此同时,它的缺点也是相当明显的:首先,由于在升级过程中对代码逻辑做了一定修改,导致项目可能会出现新的bug;其次,需要逐个页面逐个组件检查,在项目较为庞大时需要耗费大量人力;最后,这样的升级只能解决vue2至vue3的升级问题,如果想更近一步的让项目支持TS,或者项目使用了element想更换为其它支持vue3的组件库(目前element没有任何支持vue3的意思,不过可以尝试使用element plus),我们还需要付出大量额外的努力,甚至可能不亚于重写整个项目。

这时,一种完全不同的方式就进入了我们的考虑范围之中,那就是使用微前端(Micro-Frontends)作为我们的升级方式。这种方式不仅可以用在vue2至vue3升级之中,实际上它更可以作为使用不同技术栈的团队(React, Angular等)所开发前端项目的整合之中。

三、微前端

3.1什么是微前端

微前端这一说法最早是由THOUGHTWORKS技术雷达在2016年中收录(Micro frontends),为了解决大型前端项目(多团队,多技术,新老技术共存)的问题,通过借鉴后端的微服务概念所提出的。微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。与此同时,各个前端应用还可以独立运行、独立开发、独立部署。

3.2微前端的优势(Micro Frontends By Cam Jackson)

  • 渐进式升级
    对于许多团队来说,这是微前端之旅的起始。由于历史的技术局限或者交付时间的压力等原因,存在着很多庞大陈旧的前端项目。对于这些项目而言,重构是一个迫在眉睫的选项。相较于全部推翻重写,我们更希望一点一点慢慢把老旧的部分翻新,与此同时,持续的为客户提供新的功能。微前端架构能为我们实践这种想法提供了可能性。我们只需要对旧项目做一些修改,就可以在添加新功能时选择是否继续修改老的项目,或者使用新技术来开发。

  • 简单、解藕的代码库
    每一个微前端项目相比之前的整体而言代码量都是大大减少的。简单独立的代码库不仅使得我们更容易理解自己所需开发维护的项目,也减少了组件之间的不当耦合。于此同时,微前端架构也迫使我们明确了不同部分之间数据以及事件的流向。

  • 独立开发部署
    与微服务一样,能够独立部署是微前端的关键所在。无论你的前端代码在哪里,每一个微前端项目都需要能够独立运行、独立开发、独立部署。这样,维护该微前端的团队就可以独自决定他们的开发方向。

    独立开发部署.png

  • 团队自治
    得益于分离了代码库和部署过程,我们向着团队自治迈出了至关重要的一步,每个团队对于想要开发的业务以及如何快速高效开发项目都拥有了独立的决定权。当然,为了这一目标,我们的团队应该按照所负责的业务纵向划分,而非通过技术能力横向划分,下图详细展示了了这一划分方式。

    团队结构.png

3.3微前端的实现方案(微前端-最容易看懂的微前端知识)

单纯根据对概念的理解,很容易想到实现微前端的重要思想就是将应用进行拆解和整合,通常是一个父应用加上一些子应用,那么使用类似Nginx配置不同应用的转发,或是采用iframe来将多个应用整合到一起等等这些其实都属于微前端的实现方案,他们之间的对比如下:

方案 描述 优点 缺点
Nginx路由转发            通过Nginx配置反向代理来实现不同路径映射到不同应用,例如www.abc.com/app1对应app1,www.abc.com/app2对应app2,这种方案本身并不属于前端层面的改造,更多的是运维的配置 简单,快速,易配置 在切换应用时会触发浏览器刷新,影响体验
iframe嵌套 父应用单独是一个页面,每个子应用嵌套一个iframe,父子通信可采用postMessage或者contentWindow方式 实现简单,子应用之间自带沙箱,天然隔离,互不影响 iframe的样式显示、兼容性等都具有局限性;子应用间通信困难
Web Components 每个子应用需要采用纯Web Components技术编写组件,是一套全新的开发模式 每个子应用拥有独立的script和css,也可单独部署 对于历史系统改造成本高,子应用通信较为复杂易踩坑
组合式应用路由分发             每个子应用独立构建和部署,运行时由父应用来进行路由管理,应用加载,启动,卸载,以及通信机制 纯前端改造,体验良好,可无感知切换,子应用相互隔离                           需要设计和开发,由于父子应用处于同一页面运行,需要解决子应用的样式冲突,变量对象污染,通信机制等技术点

根据上面的对比,我们最终采用了组合式应用路由分发这种方案。

3.4微前端框架

诚然我们可以自己处理路由的分发,不过目前业内已经有了多种框架来帮助我们更轻松快速的集成微前端架构:

Mooa:基于Angular的微前端服务框架

Single-Spa:最早的微前端框架,兼容多种前端技术栈。

Qiankun:基于Single-Spa,阿里系开源微前端框架。

Icestark:阿里飞冰微前端框架,兼容多种前端技术栈。

Ara Framework:由服务端渲染延伸出的微前端框架。

我们这里采用single-spa来实现该项目的微前端架构。Single-spa借鉴了组件生命周期的思想,它为微应用设置了针对路由的生命周期。当微应用匹配路由处于激活状态时,微应用会把自身的内容挂载到页面上,反之则卸载。single-spa 又约定应用应包含以下生命周期:bootstrap 引导函数(应用内容首次挂载到页面前调用)、mount 挂载函数、unmount 卸载函数(须移除事件绑定等内容)以及Update更新函数(非必要)。

四、微前端实践

4.1项目总览

该系统为部门内部工具类项目,项目基于Vue2框架开发,目前已经完成了十余个页面的开发工作。下图是其首页截图,可以看出,我们的页面主要分为两个部分,所有页面通用的侧边栏部分和主体页面部分。因此,我们可以初步将项目分为三个微前端应用,继续使用vue2开发的侧边栏部分,使用vue2开发的老页面以及使用vue3开发的新页面。


项目首页截图.png

4.2项目整体结构

在实践中,因为我们的主要目的是将项目从vue2升级至vue3,该项目代码在当下以及可见的未来都将由我们团队甚至说我个人单独维护,因此我们将全部相关代码放入了一个代码库中(当然,如果你的场景不同的话,也完全可以将其放入几个互不相关的代码库中并对它们单独部署)。下图展示了我们所设计的微前端项目的整体结构,其中包含了一个基座项目root和两个分别使用vue2 (JS, Element)和vue3 (TS, AntDesign)的微应用。

其中最重要的就是基座项目,通过引入并配置single-spa,实现了通过监听URL变化的前缀(这里我们所有vue2应用以pre2/...为前缀,vue3应用以pre3/…为前缀),从而加载不同的微应用的目的。


项目结构

4.3基座项目(root)

我们基座项目的主要作用是将页面上的DOM在不同的URL前缀下分配给不同的微应用使用,这里先来看一下root/App.vue文件,这里我们除了将布局以及将侧边栏组件引入外,更重要的是我们创建了一个id为main的空div,这个div在项目中将根据URL被不同的微服务所使用,渲染出所需的主体页面部分。


项目中路由分发以及微服务加载的实现主要是由single-spa框架帮助我们完成,我们在其配置文件single-spa-config.js中引入single-spa并通过registerApplication方法来注册微应用vue2与vue3 。

singleSpa.registerApplication( //注册微前端服务
    're2', 
    async () => {
        if (process.env.NODE_ENV === 'development') {
            await runScript('http://127.0.0.1:4000/re2/app.js'); 
            return window.singleVue
        } else {
            let singleVue = null
            await getManifest('/re2/manifest.json', 'app').then(() => {
                singleVue = window.singleVue
            });
            return singleVue;       
        }
       
    },
    location => location.pathname.startsWith('/pre2') // 配置微前端模块前缀
)

registerApplication方法在这里接收了三个参数(以vue2微服务为例,vue3同理)。第一个参数是注册的微服务名称;第二个参数是一个加载时方法,该方法会在相应微服务第一次加载时调用,这个方法需要返回加载后的微服务(这个对象中存储了相应微服务的生命周期函数)。第三个参数是一个判断何时加载该微服务的方法,这个方法接收了window.location作为参数,通过返回boolean值来确定是否加载此微服务。

下面详细说明一下第二个参数,也就是加载时方法所做的事情。可以看到,我们区分了开发环境和正式环境,这是考虑到微服务模块在dev模式下所有代码都打包进了一个app.js文件中,而打包后的代码可能会分为多个js文件。所以在正式环境中一方面我们需要在微前端项目中通过stats-wbpack-plugin生成一个资源清单文件,另一方面在我们在基座中获取这个清单文件并引入相应的资源。此外,读者可能对于这里所返回的加载后的微服务window.singleVue从哪里来的有所困惑,对于这点,我们将在之后讲解。

最后,我们需要配置侧边栏菜单项以便用户点击不同菜单时展示不同页面,这里我们不需要在基座中注册vue-router,只需要根据用户所点菜单更改URL即可,具体的router注册放在相应的微应用中完成。不过需要注意的是,在更改URL时不要使用直接修改location.href等会导致前端页面刷新的方法,而是应该使用history.pushState(HTML5新特性)等单纯改变URL的方法。这是因为,页面刷新会导致应用基座以及微服务全部重新加载。

4.4 vue2微应用项目(vue2)

这个vue2项目与传统的vue2项目结构并无什么不同。只是在vue的入口文件main.js以及webpack的打包上略有不同。

import Vue from 'vue'
import routers from '@/router'
import store from './store'
import singleSpaVue from 'single-spa-vue'

const vueOptions = {
    el: '#main',
    router: routers,
    store,
    render: (h) => h(App),
}

if (!window.singleSpaNavigate){
    new Vue(vueOptions)
}

/* eslint-disable no-new */
const vueLifecycles = singleSpaVue({
    Vue,
    appOptions: vueOptions
})

export const bootstrap = vueLifecycles.bootstrap
export const mount = vueLifecycles.mount
export const unmount = vueLifecycles.unmount

在main.js中,我们需要引入single-spa-vue,这个库可以帮助我们直接实现微服务注册中所必须的生命周期函数,也就是bootstap,mount,unmount这三个方法。这个文件中,需要关注的有以下几点:

  1. 我们的vue挂载对象是在基座项目中准备好的id为main的div。
  2. 通过检查window.singleSpaNavigate(引入single-spa后会将singleSpaNavigate添加在windows对象上)是否存在来判断当前项目是独立部署运行的还是与作为一个微应用向外提供服务的。如果是独立运行的,那么与传统的开发方式一至,直接挂载于页面上即可。
  3. 使用single-spa-vue库索提供的方法,传入VUE及其相应的配置,生成vueLifecycles这个包含single-spa所需生命周期方法的对象并将相应方法导出供外部使用。
  4. 这里我们进行了vue-router的注册,router的注册使得页面可以根据URL的改变挂载并渲染微服务下不同的组件。

接下来我们来看一下该微应用的webpack配置。这里需要注意两个部分的改造。首先是在output中,我们通过library:singleVue以及libraryTarget: window将入口文件中所导出的生命周期函数对象以singleVue的名称挂载在window上。

module.exports = {
  context: path.resolve(__dirname, '../'),
  entry: {
    app: './src/main.js'
  },
  output: {
    library: "singleVue", // 导出名称
    libraryTarget: "window", //挂载目标
    path: config.build.assetsRoot,
    filename: '[name].js',
    publicPath: process.env.NODE_ENV === 'production'
      ? config.build.assetsPublicPath
      : config.dev.assetsPublicPath
  }
//......

其次,在plugins中我们使用stats-webpack-plugin导出项目的资源清单manifest.json供基座项目读取并引入微应用相关资源使用。

const StatsPlugin = require('stats-webpack-plugin')

//......
plugins: [
    new StatsPlugin('manifest.json', {
        chunkModules: false,
        entrypoints: true,
        source: false,
        chunks: false,
        modules: false,
        assets: false,
        children: false,
        exclude: [/node_modules/]
    })
  ]
//......

4.5 vue3微应用项目(vue3)

这里的配置与vue2微应用项目改造思路如出一辙,只是在项目入口文件中因为vue3的新挂载方式而略有不同。这里就不再详细阐述了。

import {createApp, h} from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Antd from 'ant-design-vue'
import singleSpaVue from 'single-spa-vue'

if(!window.singleSpaNavigate){
    createApp(App).use(store).use(router).use(Antd).mount('#app')
}

const vueLifecycles = singleSpaVue({
    createApp,
    appOptions: {
      render(): any {
        return h(App)
      },
    },
    handleInstance: (app: any) => {
        app.use(store).use(router).use(Antd).mount('#main')
    }
})

export const bootstrap = vueLifecycles.bootstrap
export const mount = vueLifecycles.mount
export const unmount = vueLifecycles.unmount

五、项目部署

项目在整体打包后会生成一个dist文件夹,可以看到基座项目以及两个微应用分别打包在了不同的目录之下(再次重申,这只是我们的做法,微前端架构完全支持你将这几个微应用部署在不同的服务器上)。


打包后的项目目录.png

在部署时,我们需要配置nginx的server配置,将/pre2以及/pre3前缀去除,所有的相关请求都指向基座项目,再由基座项目负责加载其余微应用所对应的资源,不然应用项目请求时会发生无法找到所需资源的状况。

server {
        listen       9090;
        server_name  localhost.com;

        location / {
            root   /Users/zhangkeyu/Desktop/project/Remonitor-UI/dist;
            index  root/index.html;
        }

        location /pre2/ {
            rewrite ^/pre2/(.*) /;
        }

        location /pre3/ {
            rewrite ^/pre3/(.*) /;
        }

六、总结

整个微前端项目的改造过程中,我遇到了很多的阻碍,主要的问题来自于对微前端概念不熟悉以及网上繁杂多样的微前端实现方案。虽然这个项目最终得以完成并上线运行,但是其自身仍然存在一些问题有待进一步调研解决:

  1. 不同微应用间切换时,由于应用的初次加载导致的短暂白屏问题。(根据需要采用预加载或增加loading动画提升用户体验)
  2. 不同微应用css样式相互影响的问题。(加入项目前缀)
  3. 添加新页面时需要同时修改基座项目及微应用项目本身。(引入路由配置文件,菜单根据配置文件生成)
  4. Keep-alive组件在微服务切换时失效。(修改single-spa-vue unmout生命周期行为,阻止vue destroy)

在这个项目中,我们只是尝试性的使用路由转发式微前端来解决vue2至vue3的升级问题,当然微前端的应用场景远不止这一种,vue2升级vue3的方式也远不止这一种。但是作为一个还算不错的解决方案,不妨趁着团队技术栈升级的过程将微前端应用于自己的项目之中,为之后遇到其它更复杂场景的使用打下坚实的基础。毕竟微应用可独立开发部署的特性使得你不必担心自己努力开发的代码由于微前端架构在未来的不适用而白白浪费,哪怕真的在未来某一天发现这个架构开始不适用于你的项目之中了,那么我们也只需稍微修改一下项目结构即可重新回到传统的项目架构。

最后,写这篇文章的目的一方面是记录分享自己在微前端开发过程中所获得的经验,另一方面也是抛砖引玉,希望与大家多多交流。

你可能感兴趣的:(微前端实践初试)