我的微前端实践之路

862 阅读5分钟

src=http___pic1.zhimg.com_v2-8596645b87919b31c14a136d496a92a3_1440w.jpg_source=172ae18b&refer=http___pic1.zhimg.jpg

背景

有天leader满脸笑容看着我,我就知道没什么好事情。果然,一坐下来,leader就说,最近会有个新项目,这个项目里面会有低代码,可视化模块,同时还需要对接其它的系统(N个系统),公司这边希望我作为这个项目的推动者。我当时的心里就凉溲溲的,我当时就发出了疑问:

  1. 我发凉的原因并不是项目本身,而是如何去对接这么多个系统?
  2. 这么多的系统如何去做单点对接?
  3. 如何去做多系统中的风格统一?

使用Iframe

对接N个系统,我初始的想法就是通过iframe的形式来进行操作,然后通过对url参数进行加密,再有业务系统进行url解密,拿到想要的token。但是随着使用iframe越来越多,它所暴露的问题也更多了,问题基本如下:

  1. url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
  2. UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
  3. 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
  4. 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。

关于如何在应用使用iframe,我推荐这篇文章frame架构微前端实战

singleSpa

现在在社区中使用singleSpa来实现微前端也算是比较火的一个方案。现在国内许多大厂微前端的框架都是基于sinleSpa二次封装的。

  1. 字节 garfish
  2. 阿里 qiankun
  3. 京东 micro-app

singleSpa实现思路与过程

src=http___static.alili.tech_images_micro_current.png&refer=http___static.alili.jpg

主要的思路为: 分为两种应用类别,一个为主应用,另一个为子应用。而主应用通过路由分发来访问每个子应用。 在这里选择的是vue作为主应用的技术栈。使用singleSpa核心点还是在于动态加载js文件和静态资源。

主应用中的main.js


import Vue from 'vue'
import App from './App.vue'
import router from './router'
import {registerApplication,start} from "single-spa"
async function loadScript(url){
  return new Promise((resolve,reject)=>{
    let script = document.createElement("script")
    script.src = url
    script.onload = resolve
    script.onerror = reject
    document.head.appendChild(script)
  })
}

registerApplication("child1",async ()=>{
  await loadScript('http://192.168.21.108:9974/js/chunk-vendors.js') // 子项目运行环境
  await loadScript('http://192.168.21.108:9974/js/app.js') // 子项目运行环境
  return window.child1 // bootstrap mount unmount
},
  location =>{
    return location.pathname.startsWith("/vue")
  },
)
registerApplication("child2",async ()=>{
  await loadScript('http://192.168.21.108:9975/js/chunk-vendors.js') // 子项目运行环境
  await loadScript('http://192.168.21.108:9975/js/app.js') // 子项目运行环境
  return window.child2 // bootstrap mount unmount
},
  location =>location.pathname.startsWith("/child2"),
)
start()
new Vue({
  router,
  render: h => h(App),
  store
}).$mount('#app')

分别建立两个子应用,为child1和child2,子应用需要记得要导出bootstrap,mount,unmount这三个方法。

child1中main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from "single-spa-vue"
Vue.config.productionTip = false

const appOptions = {
  el:"#microApp",
  router,
  render: h => h(App)
}
const lifeCycle = singleSpaVue({
  Vue,
  appOptions
})

if(window.singleSpaNavigate){
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = `//localhost:9974/`;
} else {
  delete appOptions.el
  new Vue(appOptions).$mount("#app")
}
export const bootstrap = lifeCycle.bootstrap
export const mount = lifeCycle.mount
export const unmount = lifeCycle.unmount

由于singleSpa会远程feach加载静态资源所以需要在vue.config.js中做些配置:


module.exports = {
    outputDir: "../dist/child1",
    configureWebpack:{
        output:{
            library:"child1",
            libraryTarget:"umd"
        },
        devServer:{
            port:9974
        }
    }
}

最后在router/index.js中修改base即可:

const router = new VueRouter({
  mode: 'history',
  base: '/child1',
  routes
})

如果有多个子应用的话,这种写法就稍微有点臃肿,所以我们通过定义list形式来进行代码优化 demo地址多个子应用情况

singleSpa数据通信

数据如何进行通信也是一大难点,在主应用中普通的使用方式如下:

registerApplication("child1",async ()=>{
  await loadScript('http://192.168.21.108:9974/js/chunk-vendors.js') // 子项目运行环境
  await loadScript('http://192.168.21.108:9974/js/app.js') // 子项目运行环境
  return window.child1 // bootstrap mount unmount
},
  location =>{
    return location.pathname.startsWith("/vue")
  },
  {token:"456781"}
)

子应用在mount中接收,然后通过store传递给各模块。

import store from "./store"
export function mount (props) {
  store.commit("setParentData",props.data)
  return lifeCycle.mount(() => {})
}

那么这种方式,是存在弊端的,我假设在子应用需要去修改主应用的数据,就很有难度了。那么,我们就思考如果主应用传递给子应用是当前的主应用vuex中的store呢?是不是就会具备了dispatch和commit方法了,那么这个时候就能修改主应用的值了。

主应用 main.js

registerApplication("child1",async ()=>{
  await loadScript('http://192.168.21.108:9974/js/chunk-vendors.js') // 子项目运行环境
  await loadScript('http://192.168.21.108:9974/js/app.js') // 子项目运行环境
  return window.child1 // bootstrap mount unmount
},
  location =>{
    return location.pathname.startsWith("/vue")
  },
  {data:store}
)

子应用:

import store from "./store"
export function mount (props) {
  store.commit("setParentData",props.data)
  return lifeCycle.mount(() => {})
}

子应用store:

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
  state: {
    parentData:{}
  },
  mutations: {
    setParentData(state,data){
      state.parentData = data
    }
  },
  actions: {
  },
  modules: {
  }
})

子应用其它模块使用:

<template>
  <div class="home">
    {{this.parentData.state.name}}
    <button @click="change">按钮</button>
  </div>
</template>
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    parentData:{}
  },
  mutations: {
    setParentData(state,data){
      state.parentData = data
    }
  },
  actions: {
  },
  modules: {
  }
})

QianKun

qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。

qiankun 孵化自蚂蚁金融科技基于微前端架构的云产品统一接入平台,在经过一批线上应用的充分检验及打磨后,我们将其微前端内核抽取出来并开源,希望能同时帮助社区有类似需求的系统更方便的构建自己的微前端系统。

km0cv8vn_w500_h500.png

主应用中main.js:

import { registerMicroApps, start  } from 'qiankun'
const microApps = [
    {
      name: "sub01",
      entry: 'http://localhost:9957',
      activeRule: "/sub01",
      container:"#vue",
       props: {
            globalState: initialState
          }
    },
    {
      name: "sub02",
      entry: 'http://localhost:9958',
      activeRule: "/sub02",
      container:"#vue",
       props: {
            globalState: initialState
          }
    },
  ];
  registerMicroApps(microApps)
  start({
    sandbox: { strictStyleIsolation: true }, // 可选,是否开启沙箱,默认为 true。// 从而确保微应用的样式不会对全局造成影响。
    singular: true // 可选,是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为 true。
  })

子应用导出钩子main.js


import "./public-path"
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.config.productionTip = false

function render(props = {}) {
  const { container } = props;
 new Vue({
    router,
    store,
    render: (h) => h(App),
  }).$mount(container ? container.querySelector('#app') : '#app');
}

if(!window.__POWERED_BY_QIANKUN__){
  render()
}

export async function bootstrap(){

}
export async function mount(props){
  render(props)
}
export async function unmount(){
  
}

public-path

    if (window.__POWERED_BY_QIANKUN__) {
      if (process.env.NODE_ENV === "development") {
        // eslint-disable-next-line no-undef
        __webpack_public_path__ = `//localhost:9957/`;
        return;
      }
      // eslint-disable-next-line no-undef
      __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
  })();
  

qiankun通信方式

qiankun中提供了initGlobalState用于初始化数据。

import { initGlobalState  } from 'qiankun'
const initialState = {
    // 当前登录用户
    userInfo: "55555"
  }
 const qiankunActions = initGlobalState(initialState)
 qiankunActions.onGlobalStateChange((state) => {
    for (const key in state) {
      if (key === 'userInfo') {
        console.log("state",state[key])
      }
    }
  })

子应用中使用的时候需要安装qiankun-vue2-common插件,默认的会把给当前store挂上主应用传递过来的数据。

import common from "qiankun-vue2-common";
export async function mount(props){
  common.initGlobalState(store, props);
  render(props)
}

Home.vue

import { mapState, mapActions } from "vuex";
 computed: {
    ...mapState("global", {
      userInfo: (state) => {
        return JSON.stringify(state.userInfo);
      }
    })
  },
  methods: {
    ...mapActions("global", ["setGlobalState"]),
    update() {
      this.setGlobalState({ userInfo: "666" });
    },
  },