# 微前端

几个核心价值:技术栈无关,独立开发、独立部署,增量升级,独立运行时。

single-spa 问题

问题

  • 1 没有处理父子应用之间的样式隔离问题。

  • 2 每个应用都使用 window 对象可能会存在冲突。 父应用切换子应用,每次切换都是同一个window。

  • 3 仅仅实现了 路由劫持,和应用加载

  • 4 single-spa不够灵活。不能动态加载js文件。 即父应用如果想要加载子应用,需要创建script把子应用的脚本添加进去,在切换到子应用时,把脚本添加到父应用head中。

# SPA VS MPA

微前端是由多个 spa 组成的 mpa。通过上下文的管理,生命周期的管理和通信机制的管理。让这些 spa 形成统一的整体。

  • spa 单页应用,先加载一个统一的壳,在站内路由来形式转发。虽然初始加载比较慢,但是内部跳转非常快。
  • mpa 多页应用,每次的页面是全加载的。

# 为什么不用 iframe

iframe 最大的特性就是提供了 浏览器原生的硬隔离方案 不论是样式隔离、js 隔离这类问题统统都能被完美解决。 问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享。

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

# Single-spa+Vue实践

# 父应用中主要过程(parent-vue)

  • 1 registerApplication注册应用,当父应用切换到 '/vue'目录时。父应用动态加载子应用脚本(动态标签把app.js和chunk-vendors.js插入到父应用head中 )。
  • 2 当加载子应用脚本,会去调用子应用的( bootstrap mount unmount),调用子应用mount时候就会走子应用 appOptions方法,把子应用放在应用的el节点上。
  • 2 父应用start()启动。

# 子应用中主要完成以下过程(children-vue)

  • 1 引入(single-spa-vue,single-spa-react) 把子应用实例挂到singleSpaVue上。并返回 子应用生命周期( bootstrap mount unmounted )
  • 2 在vue.config.js中配置 把子应用打包成lib库在父应用中加载。

# 样式隔离方案

# 子应用之间样式隔离

  • 当切换到当前子应用,加载当前子应用样式。当切换其他子应用,删除当前子应用样式,并切换子应用。

# 父子应用样式隔离方案

  • BEM 约定项目前缀
  • CSS-Modules 打包生成不冲突的选择器名。
  • Shdow DOM 真正意义的隔离,

子应用 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

new Vue({
  router,
  render: h => h(App)
}).$mount("#app");

const appOptions = {
    el: "#childrenVue",
    router,
    render: h => h(App)
};

// 支持应用独立运行、部署,不依赖于基座应用
console.log('----window-',window)
if (!window.singleSpaNavigate) {
    delete appOptions.el
    new Vue(appOptions).$mount('#app')
}else{
    //在页面跳转前拼绝对路径
    __webpack_public_path__ = 'http://localhost:2000/'
}

// singleSpaVue包装一个vue微前端服务对象
const vueLifecycle = singleSpaVue({
    Vue,
    appOptions
});
// 导出生命周期对象
// 启动时
export const bootstrap = vueLifecycle.bootstrap;
// 挂载时
export const mount  = vueLifecycle.mount;
// 卸载时
export const unmount = vueLifecycle.unmount;
    const package  = require('./package');
    module.exports = {
        devServer:{
            port:2000
        },
        configureWebpack:{
            output:{
                //library的值在所有子应用中需要唯一
                library:package.name,
                //导出umd格式的包,在全局对象上挂载属性package.name,基座应用需要通过这个全局对象获取一些信息,比如子应用导出的生命周期函数
                libraryTarget:'umd'
            },
        }
    }
    


子应用vue.config.js

const package  = require('./package');
module.exports = {
    devServer:{
        port:2000
    },
    configureWebpack:{
        output:{
            //library的值在所有子应用中需要唯一
            library:package.name,
            //导出umd格式的包,在全局对象上挂载属性package.name,基座应用需要通过这个全局对象获取一些信息,比如子应用导出的生命周期函数
            libraryTarget:'umd'
        },
    }
}


父应用main.js文件配置

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import {registerApplication,start} from "single-spa";

//获取子应用url,插入到父应用的head中。
async function loadScript(url){
    return  new Promise(function (resolve,reject) {
        let script = document.createElement('script');
        script.src = url;
        script.onload = resolve;
        script.onerror = reject;
        document.head.append(script);
    })

}
//当父应用切换到 '/vue'目录。父应用动态加载子应用,并把子应用插入到父应用head中。
registerApplication('childrenVue',async ()=>{
    //子应用通过打包生成类库(app.js,chunk-vendors.js)
    //父应用加载子应用文件,需要自己构建script标签。动态插入到head中
    console.log('加载模块')
    await loadScript('http://localhost:2000/js/chunk-vendors.js')
    await loadScript('http://localhost:2000/js/app.js');
    return window.childrenVue;//bootstrap mount unmount
    //用户切换到/vue,执行async方法。
},location => location.pathname.startsWith('/vue'))

//父应用启动
start();
//父应用注册挂载
createApp(App).use(router).mount('#app')