微前端--single-spa

微前端

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
使用微前端的挑战: 子应用切换,应用相互隔离,互补干扰,子应用之前的通信,多个子应用并存,用户状态的存储,免登。

常用技术方案

路由分发式微前端

通过http服务的反向代理

http {
    server {
        listen 80;
        server_name xxx.xxx.com;
        location /api/ {
            proxy_pass http://localhost:3001/api    
        }
        location /web/admin {
            proxy_pass http://localhost:3002/api
        }
        location / {
            proxy_pass /;
        }
    }
}

实现简单,不需要对现有应用进行改造,和技术栈无关。
切换应用的时候,浏览器都需要重新加载页面。

iframe

html的标签
实现简单,css和js隔离,互不干扰。全局上下文完全隔离,内存变量不共享,子应用之间的通信,数据同步过程比较复杂,对seo不友好。切换应用的时候,浏览器都需要重新加载页面。

single-spa

在single-spa方案中,应用被分为两类:基座应用和子应用。
single-spa 会在基座应用中维护一个路由注册表,每个路由对应一个子应用。基座应用启动以后,当我们切换路由时,如果是一个新的子应用,会动态获取子应用的 js 脚本,然后执行脚本并渲染出相应的页面;如果是一个已经访问过的子应用,那么就会从缓存中获取已经缓存的子应用,激活子应用并渲染出对应的页面。

// 基座
import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router';
const  { registerApplication, start } =  require('single-spa');

Vue.use(VueRouter)

Vue.config.productionTip = false

// 接入 single-spa 的标志
window.__SINGLE_SPA__ = true

const router = new VueRouter({
  mode: 'history',
  routes: []
});

// 远程加载子应用
function createScript(url) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script')
    script.src = url
    script.onload = resolve
    script.onerror = reject
    const firstScript = document.getElementsByTagName('script')[0]
    firstScript.parentNode.insertBefore(script, firstScript)
  })
}
// 加载子应用
function loadApp(url, globalVar, entrypoints) {
  return async () => {
    for(let i = 0; i < entrypoints.length; i++) {
      await createScript(url + entrypoints[i])
    }
    return window[globalVar]
  }
}
// 子应用路由注册表
const apps = [
  {
    // 子应用名称
    name: 'app1',
    // 子应用加载函数
    app: loadApp('http://localhost:8081', 'app1', [ "/js/chunk-vendors.js", "/js/app.js" ]),
    // 当路由满足条件时(返回true),激活(挂载)子应用
    activeWhen: location => location.pathname.startsWith('/app1'),
    // 传递给子应用的对象
    customProps: {}
  },
  {
    name: 'app2',
    app: loadApp('http://localhost:8082', 'app2', [ "/js/chunk-vendors.js", "/js/app.js" ]),
    activeWhen: location => location.pathname.startsWith('/app2'),
    customProps: {}
  },
  {
    // 子应用名称
    name: 'app3',
    // 子应用加载函数
    app: loadApp('http://localhost:3000', 'app3', ["/main.js"]),
    // 当路由满足条件时(返回true),激活(挂载)子应用
    activeWhen: location => location.pathname.startsWith('/app3'),
    // 传递给子应用的对象
    customProps: {}
  }
]

// 注册子应用
for (let i = apps.length - 1; i >= 0; i--) {
  registerApplication(apps[i])
}

new Vue({
  router,
  render: h => h(App),
  mounted() {
    // 启动
    start()
  },
}).$mount('#app')
  • name: 子应用的唯一表示
  • activeWhen: 子应用激活的条件,当url发生变化的升级后,会遍历执行注册的子应用的activeWhen方法,当activeWhen返回的是true,对应的子应用就会被激活
  • app: 用户获取子应用提供给基座应用的生命周期,bootstrap mount unmount等。基座应用切换子应用时,也是同样的操作,即先执行上一个子应用的 unmount 操作,然后再执行下一个子应用的 mount 操作。因此就需要子应用提供 mount、unmount 等生命周期方法,供基座应用调用。和单页应用的懒加载一样,基座应用在激活子应用时,如果子应用是首次激活,就会执行 app 方法,动态去加载子应用的入口 js 文件,然后执行,得到子应用的生命周期方法。
  • customProps: 子应用激活的时候,可以传递给子应用的自定义属性,是一个对象
// index.js

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

const appOptions = {
  render: (h) => h(App)
};

let vueInstance;

// 子应用没有接入 single-spa
if (!window.__SINGLE_SPA__) {
  new Vue(appOptions).$mount('#app')
}

// 提供 bootstrap 生命周期方法
export function bootstrap () {
  console.log('app1 bootstrap')
  return Promise.resolve().then(() => {

  });
}
// 提供 mount 生命周期方法
export function mount (props) {
  console.log('app1 mount', props)
  return Promise.resolve().then(() => {
    vueInstance = new Vue(appOptions)
    vueInstance.$mount('#microApp')
  })
}

// 提供 unmount 生命周期方法
export function unmount () {
  console.log('app1 unmount')
  return Promise.resolve().then(() => {
    if (!vueInstance.$el.id) {
      vueInstance.$el.id = 'microApp'
    }
    vueInstance.$destroy()
    vueInstance.$el.innerHTML = ''
  })
}

// 提供 update 生命周期方法
export function update () {
  console.log('app1 update');
}

通常通过webpack构建工具生成的js脚本,表现形式都是iiff,就是立即执行函数表达式。各子应用对应的脚本执行的时是相互隔离的,如果是这样,基座应用在激活子应用的时候,是无法获取到子应用的生命周期方法的,也无法挂载子应用。添加libaray,libarayTarget配置项,将子应用入口文件的返回值就是生命周期方法暴露给window,这样基座应用就可以从window中获取子应用的生命周期的方法。

// 项目的构建脚本
module.exports = {
    configureWebpack: {
        ...
        publicPath: 'http://localhost:8081'
        output: {
            library: 'app1',   
            libraryTarget: 'var'
        }    
    }
}
  • 单页应用的路由切换功能是基于window.history(window.location.hash)实现。在单页面应用中,会给window对象注册popstate(hashchange)事件,在callback中,添加页面切换的逻辑,当通过执行 pushState(replaceState) 方法、修改 hash 值、使用浏览器前进后退(go、back、forward)功能改变 url 时,会触发 popstate(hashchange) 事件,然后切换页面。
  • 基座应用加载执行 single-spa 时,也会给 window 对象注册 popstate (hashchange) 事件, popstate(hashchange) 的 calback 中,就是激活子应用的逻辑。当基座应用通过执行pushState(replaceState)、修改 hash、使用浏览器前进后退(go、back、forward)功能的方式修改 url 时,popstate(hashchange) 就会触发,相应的子应用的激活逻辑就会执行。
// 通过原生构造函数 - popStateEvent 创建一个popstate事件对象
function createPopStateEvent(state, originalMethodName) {
    var evt;
    try {
        evt = new PopStateEvent("popstate", {
            state: state    
        })
    } catch(err) {
        evt = document.createEvent('popstateevent')
        evt.initPopStateEvent("popstate", false, false, state)
    }
    evt.singleSpa = true
    evt.singleSpaTrigger = originalMethodName
    return evt
}
// 重写 updateState、replaceState 方法,通过 window.dispatchEvent 方法,手动触发 popstate 事件
function patchedUpdateState(updateState, methodName) {
    return function () {
      var urlBefore = window.location.href;
      var result = updateState.apply(this, arguments);
      var urlAfter = window.location.href;

      if (!urlRerouteOnly || urlBefore !== urlAfter) {
        window.dispatchEvent(createPopStateEvent(window.history.state, methodName));
      }
      return result;
    };
}
// 重写 pushState 方法
window.history.pushState = patchedUpdateState(window.history.pushState, "pushState");
// 重写 replaceState 方法
window.history.replaceState = patchedUpdateState(window.history.replaceState, "replaceState");
...
const router = new VueRouter({
  mode: 'history',
  base: '/app1',
  routes: [{
    path: '/foo',
    name: 'foo',
    component: {
      ...
    }
  }, {
    path: '/bar',
    name: 'bar',
    component: {
      ...
    }
  }]
})
...

application 模式下,single-spa 的工作流程,application 模式下,我们需要先通过registerApplication 注册子应用,然后在基座应用挂载完成以后执行 start 方法, 这样基座应用就可
以根据 url 的变化来进行子应用切换,激活对应的子应用。
在这里插入图片描述

parcel模式下,single-spa的工作流程。mountRootParcel 方法会返回一个parcel实例对象,内部包含update、unmount 方法。当我们需要更新组件时,直接调用parcel对象的update方法,就可以触发组件的update生命周期方法;当我们需要卸载组件时,直接调用parcel对象的unmount方法。在执行mountRootParcel 方法时,传入的第二个参数,会作为组件 mount 生命周期方法的入参;在执行 parcel.update 方法时,传入的参数,会作为组件 update 生命周期方法的入参。

子应用是否被挂载

  • NOT_LOADED 未加载/待加载
  • LOAD_SOURCE_CODE 加载源代码
  • NOT_SOURCE_CODE 未启动/待启动
  • BOOTSTRAPPING 子应用启动中
  • NOT_MOUNTRED 为挂载/待挂载
  • MOUNTING 子应用挂载中
  • UNMOUNTING 需要卸载
  • UNMOUNTED 已经卸载
  • LOAD_ERROR 子应用加载失败
传参

父组件和parcel组件的通信
mount 阶段,父组件在执行 mountRootParcel 时,可以将要传递给 parcel 组件的值作为第二个参数,这个参数会作为 parcel 组件 mount 方法执行时的入参,这样 parcel 组件就可以拿到父组件传递的值。update 阶段也一样,父组件执行 parcel.update 时,传入的参数会作为 parcel 组件 update 方法执行时的入参。
基座应用和子组件之间的通信
基座应用在定义路由注册表的时候,会给每个子应用定义一个customProps,这个customoProps会作为子应用mount方法的入参,在子应用中, customProps(或者 customProps 里面的某个值) 可以作为子应用的共享状态(使用 vuex、mobx、redux 等)。这样,当基座应用修改 customProps 时,子应用就可接受到通知,然后更新。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值