微前端使用第二章

发布时间 2024-01-11 18:42:18作者: 一路繁花似锦绣前程

二、微前端自研框架

1、子应用接入
a、vue2
const path = require('path');
const {name} = require('./package');

function resolve(dir) {
  return path.join(__dirname, dir);
}

const port = 9004;

module.exports = {
  outputDir: 'dist', // 打包的目录
  assetsDir: 'static', // 打包的静态资源
  filenameHashing: true, // 打包出来的文件,会带有hash信息
  publicPath: 'http://localhost:9004',
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    hot: true,
    disableHostCheck: true,
    port,
    headers: {
      'Access-Control-Allow-Origin': '*', // 本地服务的跨域内容
    },
  },
  // 自定义webpack配置
  configureWebpack: {
    resolve: {
      alias: {
        '@': resolve('src'),
      },
    },
    output: {
      // 把子应用打包成 umd 库格式 commonjs 浏览器,node环境
      libraryTarget: 'umd',
      filename: 'vue2.js',
      library: 'vue2', // window.vue2
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
};
import Vue from 'vue'
import App from './App.vue'
import router from './router';
import store from './store';

Vue.config.productionTip = false

let instance = null;
const render = () => {
  instance = new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount('#app-vue')
}

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

// 开始加载结构
export async function beforeLoad() {
  console.log('beforeLoad');
}

export async function mounted() {
  window.custom.on("test2", data => {
    console.log(data)
  })
  window.custom.emit("test1", {
    a: 1
  })
  render()
  console.log('mounted')
}

export async function destoryed() {
  console.log('destoryed', instance)
}
b、vue3
const path = require('path');
const {name} = require('./package');

function resolve(dir) {
  return path.join(__dirname, dir);
}

const port = 9005;

module.exports = {
  outputDir: 'dist',
  assetsDir: 'static',
  filenameHashing: true,
  publicPath: 'http://localhost:9005',
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    hot: true,
    disableHostCheck: true,
    port,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  // 自定义webpack配置
  configureWebpack: {
    resolve: {
      alias: {
        '@': resolve('src'),
      },
    },
    output: {
      // 把子应用打包成 umd 库格式
      libraryTarget: 'umd',
      filename: 'vue3.js',
      library: 'vue3',
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
};
import {createApp} from 'vue';
import App from './App.vue';
import router from './router';

let instance = null;
const render = () => {
  instance = createApp(App);
  instance.use(router).mount('#app');
}

if (!window.__MICRO_WEB__) {
  render();
}

export async function beforeLoad() {
  console.log('beforeLoad');
}

export async function mounted() {
  const storeData = window.store.getStore()
  window.store.update({
    ...storeData,
    a: 11
  })
  // vue3 vue2 先有监听,再有触发
  window.custom.on("test1", () => {
    window.custom.emit("test2", {
      a: 2
    })
  })
  render();
  console.log('mounted');
}

export async function destoryed() {
  console.log('destoryed', instance);
}
c、react15
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  entry: {
    path: ['./index.js']
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'react15.js',
    library: 'react15',
    libraryTarget: 'umd',
    umdNamedDefine: true,
    publicPath: 'http://localhost:9002/'
  },
  module: {
    rules: [
      {
        test: /\.js(|x)$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react']
          }
        }
      },
      {
        test: /\.(c|sc)ss$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: {
          loader: 'url-loader',
        }
      }
    ]
  },
  optimization: {
    splitChunks: false,
    minimize: false
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html'
    }),
    new MiniCssExtractPlugin({
      filename: '[name].css'
    })
  ],
  devServer: {
    headers: {'Access-Control-Allow-Origin': '*'},
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 9002,
    historyApiFallback: true,
    hot: true,
  }
}
import React from 'react'
import ReactDOM from 'react-dom'
import BasicMap from './src/router/index.jsx';
import "./index.scss"

const render = () => {
  ReactDOM.render((
    <BasicMap/>
  ), document.getElementById('app-react'))
}

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

export async function beforeLoad() {
  console.log('beforeLoad')
}

export async function mounted() {
  render()
  console.log('mounted')
}

export async function destoryed() {
  console.log('destoryed')
}
d、react16
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  entry: {path: ['regenerator-runtime/runtime', './index.js']},
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'react16.js',
    library: 'react16',
    libraryTarget: 'umd',
    umdNamedDefine: true,
    publicPath: 'http://localhost:9003'
  },
  module: {
    rules: [
      {
        test: /\.js(|x)$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react']
          }
        }
      },
      {
        test: /\.(cs|scs)s$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
      },
    ]
  },
  optimization: {
    splitChunks: false,
    minimize: false
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html'
    }),
    new MiniCssExtractPlugin({
      filename: '[name].css'
    })
  ],
  devServer: {
    headers: {'Access-Control-Allow-Origin': '*'},
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 9003,
    historyApiFallback: true,
    hot: true
  }
}
import React from 'react'
import "./index.scss"
import ReactDOM from 'react-dom'
import BasicMap from './src/router';
import {setMain} from "./src/utils/main";

export const render = () => {
  ReactDOM.render(<BasicMap/>, document.getElementById('app-react'))
}

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

export async function beforeLoad() {
  console.log("beforeLoad")
}

export async function mounted(app) {
  setMain(app)
  render()
}

export async function destoryed() {
  console.log("destoryed")
}
2、主应用注册子应用
import {createApp} from 'vue'
import App from './App.vue'
import router from './router'
import {subNavList} from "./store/sub"
import {registerApp} from './utils';

// 注册、加载、启动子应用
registerApp(subNavList);

createApp(App).use(router()).mount('#micro_web_main_app')
import * as appInfo from "../store"

export const subNavList = [
  {
    name: "react15",
    activeRule: "/react15",
    container: "#micro-container",
    entry: '//localhost:9002/',
    appInfo
  },
  {
    name: "react16",
    activeRule: "/react16",
    container: "#micro-container",
    entry: '//localhost:9003/',
    appInfo
  },
  {
    name: "vue2",
    activeRule: "/vue2",
    container: "#micro-container",
    entry: '//localhost:9004/',
    appInfo
  },
  {
    name: "vue3",
    activeRule: "/vue3",
    container: "#micro-container",
    entry: '//localhost:9005/',
    appInfo
  }
]
import {createRouter, createWebHistory} from 'vue-router';

const routes = [
  {
    path: '/',
    component: () => import('../App.vue'),
  },
  {
    path: '/react15',
    component: () => import('../App.vue'),
  },
  {
    path: '/react16',
    component: () => import('../App.vue'),
  },
  {
    path: '/vue2',
    component: () => import('../App.vue'),
  },
  {
    path: '/vue3',
    component: () => import('../App.vue'),
  },
];

const router = (basename = '') => createRouter({
  history: createWebHistory(basename),
  routes,
});

export default router;
export const NAV_LIST = [
  {
    name: '首页',
    status: true,
    value: 0,
    url: '/vue3#/index',
    hash: '',
  },
  {
    name: '资讯',
    status: false,
    value: 1,
    url: '/react15#/information',
  },
  {
    name: '视频',
    status: false,
    value: 2,
    url: '/react15#/video',
    hash: '',
  },
  {
    name: '选车',
    status: false,
    value: 3,
    url: '/vue3#/select',
    hash: '',
  },
  {
    name: '新能源',
    status: false,
    value: 4,
    url: '/vue2#/energy',
    hash: '',
  },
  {
    name: '新车',
    status: false,
    value: 5,
    url: '/react16#/new-car',
    hash: '',
  },
  {
    name: '排行',
    status: false,
    value: 6,
    url: '/react16#/rank',
    hash: '',
  },
]
<template>
  <div class="main-nav-container">
    <div class="main-nav-content">
      <!-- logo内容 -->
      <div class="main-nav-logo">
        <img src="" alt="">
      </div>

      <!-- 导航列表详情 -->
      <div class="main-nav-list">
        <div
            v-for="(item, index) in NAV_LIST"
            :class="{ 'main-nav-active': currentIndex === index }"
            :key="index"
            @click="setCurrentIndex(item, index)"
        >
          {{ item.name }}
        </div>
      </div>

      <!-- 搜索 -->
      <div class="main-nav-search">
        <div class="main-nav-search-icon">
          <img src="../../assets/blue-search.png" alt="">
        </div>
        <div class="main-nav-search-input">
          <input
              type="text"
              id="main-nav-search"
              v-if="searchStatus"
              @blur="setSearchStatus(false)"
          >
          <div class="main-nav-search-input-fake" v-else @click="setSearchStatus(true)">
            快速搜索
          </div>
        </div>
        <div class="main-nav-search-button">
          搜索
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import {ref, watch} from 'vue'
import {NAV_LIST} from '../../const'
import {useRouter, useRoute} from 'vue-router'

export default {
  name: 'nav',
  setup() {
    const currentIndex = ref(0)
    const searchStatus = ref(true)
    const router = useRouter();
    const route = useRoute();

    watch(route, (val) => {
      NAV_LIST.forEach((item, index) => {
        if (val.fullPath.indexOf(item.url) !== -1) {
          currentIndex.value = index
        }
      })
    }, {deep: true})
    const setCurrentIndex = (data, index) => {
      if (data.url === route.fullPath) {
        return
      }
      currentIndex.value = index
      router.push(data.url)
    }
    const setSearchStatus = (type) => {
      searchStatus.value = type
    }

    return {
      NAV_LIST,
      currentIndex,
      setCurrentIndex,
      searchStatus,
      setSearchStatus
    }
  }
};
</script>
<template>
  <div class="micro-main-body-container">
    <Loading v-show="loadingStatus"></Loading>

    <!-- 子应用容器 -->
    <div id="micro-container" v-show="!loadingStatus"></div>
  </div>
</template>

<script>
import Loading from './loading.vue';
import {loading} from '@/store';

export default {
  name: 'micro-body',
  components: {
    Loading,
  },
  setup() {
    return {
      loadingStatus: loading.loadingStatus,
    };
  },
};
</script>
3、主应用路由拦截
export {registerMicroApps, start} from "./start"
export {createStore} from "./store"
import {registerMicroApps, start, createStore} from "../../micro"
import {loading} from "../store"

const store = createStore()
window.store = store
store.subscribe((newValue, oldValue) => {
  console.log(newValue, oldValue)
})

export const registerApp = (list) => {
  // 注册到微前端框架里
  registerMicroApps(list, {
    beforeLoad: [
      () => {
        loading.changeLoading(true)
        console.log("开始加载")
      }
    ],
    mounted: [
      () => {
        loading.changeLoading(false)
        console.log("渲染完成")
      }
    ],
    destoryed: [
      () => {
        console.log("卸载完成")
      }
    ]
  })

  // 开启微前端框架
  start()
}
import {setList, getList} from "./const/subApps";
import {currentApp} from "./utils";
import {rewriteRouter} from "./router/rewriteRouter";
import {setMainLifeCycle} from "./const/mainLifeCycle";
import {Custom} from "./customeevent";
import {prefetch} from "./loader/prefetch";

const custom = new Custom()
custom.on("test", (data) => {
  console.log(data)
})
window.custom = custom

rewriteRouter()
export const registerMicroApps = (appList, lifeCycle) => {
  setList(appList)
  setMainLifeCycle(lifeCycle)
}

// 启动微前端框架
export const start = () => {
  // 首先验证当前子应用列表是否为空
  const apps = getList()
  if (!apps.length) {
    // 子应用列表为空
    throw Error("子应用列表为空,请正确注册")
  }
  // 有子应用的内容,查找到符合当前路由的子应用
  const app = currentApp()
  if (app) {
    const {pathname, hash} = window.location
    const url = pathname + hash
    window.history.pushState("", "", url)
    window.__CURRENT_SUB_APP__ = app.activeRule
  }
  // 预加载-加载接下来的所有子应用,但是不显示
  prefetch()
}
import {patchRouter} from "../utils";
import {turnApp} from "./routerHandle";

// 重写window的路由跳转
export const rewriteRouter = () => {
  window.history.pushState = patchRouter(window.history.pushState, "micro_push")
  window.history.replaceState = patchRouter(window.history.replaceState, "micro_replace")
  // 监听返回事件
  window.onpopstate = function () {
    turnApp()
  }

  window.addEventListener("micro_push", turnApp)
  window.addEventListener("micro_replace", turnApp)
}
let list = []
export const getList = () => list
export const setList = appList => list = appList
import {getList} from "../const/subApps";
// 给当前的路由跳转打补丁
export const patchRouter = (globalEvent, eventName) => {
  return function () {
    const e = new Event(eventName)
    globalEvent.apply(this, arguments)
    window.dispatchEvent(e)
  }
}
export const currentApp = () => {
  const currentUrl = window.location.pathname
  return filterApp("activeRule", currentUrl)
}

export const findAppByRoute = (router) => {
  return filterApp("activeRule", router)
}

const filterApp = (key, value) => {
  const currentApp = getList().filter(item => item[key] === value)
  return currentApp && currentApp.length ? currentApp[0] : {}
}

// 子应用是否做了切换
export const isTurnChild = () => {
  window.__ORIGIN_APP__ = window.__CURRENT_SUB_APP__
  if (window.__CURRENT_SUB_APP__ === window.location.pathname) {
    return false
  }
  const currentApp = window.location.pathname.match(/\/\w+/)
  if (!currentApp) {
    return
  }
  window.__CURRENT_SUB_APP__ = currentApp[0]
  return true
}
import {isTurnChild} from "../utils";
import {lifecycle} from "../lifeCycle";

export const turnApp = async () => {
  if (isTurnChild()) {
    // 微前端的生命周期执行
    await lifecycle()
  }
}
4、主应用生命周期
import {findAppByRoute} from "../utils";
import {getMainLifeCycle} from "../const/mainLifeCycle";
import {loadHtml} from "../loader";

export const lifecycle = async () => {
  // 获取到上一个子应用
  const prevApp = findAppByRoute(window.__ORIGIN_APP__)
  // 获取到要跳转到的子应用
  const nextApp = findAppByRoute(window.__CURRENT_SUB_APP__)
  if (!nextApp) {
    return
  }
  if (prevApp && prevApp.destoryed) {
    if (prevApp.proxy) {
      prevApp.proxy.inactive() // 将沙箱销毁
    }
    await destoryed(prevApp)
  }
  const app = await beforeLoad(nextApp)
  await mounted(app)
}

export const beforeLoad = async (app) => {
  await runMainLifeCycle("beforeLoad")
  app && app.beforeLoad && app.beforeLoad()
  const subApp = await loadHtml(app) // 获取子应用的内容
  subApp && subApp.beforeLoad && subApp.beforeLoad()
  return subApp
}
export const mounted = async (app) => {
  app && app.mounted && app.mounted({
    appInfo: app.appInfo,
    entry: app.entry
  })
  await runMainLifeCycle("mounted")
}
export const destoryed = async (app) => {
  app && app.destoryed && app.destoryed()
  // 对应的执行以下主应用的生命周期
  await runMainLifeCycle("destoryed")
}

export const runMainLifeCycle = async (type) => {
  const mainlife = getMainLifeCycle()
  await Promise.all(mainlife[type].map(async item => await item()))
}
let lifeCycle = {}
export const getMainLifeCycle = () => lifeCycle
export const setMainLifeCycle = data => lifeCycle = data
import {ref} from 'vue';
export const loadingStatus = ref(false);
export const changeLoading = type => loadingStatus.value = type
export * as loading from './loading';
export * as header from './header';
export * as nav from './nav';
5、加载和解析html
import {fetchResource} from "../utils/fetchResource";
import {sandbox} from "../sandbox";
// 加载html的方法
export const loadHtml = async (app) => {
  // 第一个,子应用需要显示在哪里
  let container = app.container // #id 内容
  // 子应用的入口
  let entry = app.entry
  const [dom, scripts] = await parseHtml(entry, app.name)
  const ct = document.querySelector(container)
  if (!ct) {
    throw new Error("容器不存在,请查看")
  }
  ct.innerHTML = dom
  scripts.forEach(item => {
    sandbox(app, item)
  })
  return app
}

const cache = {} // 根据子应用的name来做缓存

export const parseHtml = async (entry, name) => {
  if (cache[name]) {
    return cache[name]
  }
  const html = await fetchResource(entry)
  let allScript = []
  const div = document.createElement("div")
  div.innerHTML = html
  const [dom, scriptUrl, script] = await getResources(div, entry)
  const fetchScripts = await Promise.all(scriptUrl.map(async item => fetchResource(item)))
  allScript = script.concat(fetchScripts)
  cache[name] = [dom, allScript]
  return [dom, allScript]
}

export const getResources = async (root, entry) => {
  const scriptUrl = []
  const script = []
  const dom = root.outerHTML

  // 深度解析
  function deepParse(element) {
    const children = element.children
    const parent = element.parent
    // 第一步处理位于 script 中的内容
    if (element.nodeName.toLowerCase() === "script") {
      const src = element.getAttribute("src")
      if (!src) {
        script.push(element.outerHTML)
      } else {
        if (src.startsWith("http")) {
          scriptUrl.push(src)
        } else {
          scriptUrl.push(`http:${entry}/${src}`)
        }
      }
      if (parent) {
        parent.replaceChild(document.createComment("此 js 文件已经被微前端替换", element))
      }
    }
    // link 也会有js的内容
    if (element.nodeName.toLowerCase() === "link") {
      const href = element.getAttribute("href")
      if (href.endsWith(".js")) {
        if (href.startsWith("http")) {
          scriptUrl.push(href)
        } else {
          scriptUrl.push(`http:${entry}/${href}`)
        }
      }
    }
    for (let i = 0; i < children.length; i++) {
      deepParse(children[i])
    }
  }

  deepParse(root)
  return [dom, scriptUrl, script]
}
// 执行js脚本
export const performScriptForFunction = (script, appName, global) => {
  window.proxy = global
  const scriptText = `
    return ((window) => {
      ${script}
      return window['${appName}']
    })(window.proxy)
  `
  return new Function(scriptText)()
}
export const performScriptForEval = (script, appName, global) => {
  // library window.appName
  window.proxy = global
  const scriptText = `
    ((window) => {
      ${script}
      return window['${appName}']
    })(window.proxy)
  `
  return eval(scriptText) // app module mount
}
export const fetchResource = url => fetch(url).then(async res => await res.text())
// import {performScriptForEval} from "./performScript";
import {performScriptForFunction} from "./performScript";
// import {SnapShotSandbox} from "./snapShotSandbox";
import {ProxySandbox} from "./proxySandbox";

const isCheckLifeCycle = lifecycle => lifecycle &&
  lifecycle.beforeLoad &&
  lifecycle.mounted &&
  lifecycle.destoryed
// 子应用生命周期处理,环境变量设置
export const sandbox = (app, script) => {
  // const proxy = new SnapShotSandbox()
  const proxy = new ProxySandbox()
  if (!app.proxy) {
    app.proxy = proxy
  }
  // 1、设置环境变量
  window.__MICRO_WEB__ = true
  // 2、运行js文件
  // const lifecycle = performScriptForEval(script, app.name, app.proxy.proxy)
  const lifecycle = performScriptForFunction(script, app.name, app.proxy.proxy)
  // 生命周期,挂载到app上
  if (isCheckLifeCycle(lifecycle)) {
    app.beforeLoad = lifecycle.beforeLoad
    app.mounted = lifecycle.mounted
    app.destoryed = lifecycle.destoryed
  }
}
6、运行环境隔离
// 快照沙箱
// 应用场景:比较老版本的浏览器
export class SnapShotSandbox {
  constructor() {
    // 1、代理对象
    this.proxy = window
    this.active()
  }

  // 沙箱激活
  active() {
    // 创建一个沙箱快照
    this.snapshot = new Map()
    // 遍历全局环境
    for (const key in window) {
      this.snapshot[key] = window[key]
    }
  }

  // 沙箱销毁
  inactive() {
    for (const key in window) {
      if (window[key] !== this.snapshot[key]) {
        // 还原操作
        window[key] = this.snapshot[key]
      }
    }
  }
}
// 代理沙箱
const defaultValue = {} // 子应用的沙箱容器
export class ProxySandbox {
  constructor() {
    this.proxy = null
    this.active()
  }

  // 沙箱激活
  active() {
    // 子应用需要设置属性
    this.proxy = new Proxy(window, {
      get(target, key) {
        if (typeof target[key] === "function") {
          return target[key].bind(target)
        }
        return defaultValue[key] || target[key]
      },
      set(target, key, value) {
        defaultValue[key] = value
        return true
      }
    })
  }

  // 沙箱销毁
  inactive() {

  }
}
7、css样式隔离
module.exports = {
  module: {
    rules: [
      {
        test: /\.(cs|scs)s$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              module: true
            }
          },
          'sass-loader'
        ]
      }
    ]
  }
}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>shadow dom</title>
</head>
<body>
<div id="box1"></div>
<div id="box2"></div>
<script>
  const box1 = document.getElementById("box1")
  // 开启shadow dom模式
  const shadow1 = box1.attachShadow({mode: "open"})
  const one = document.createElement("div")
  one.className = "one"
  one.innerText = "第一个内容"
  const style1 = document.createElement("style")
  style1.textContent = `
    .one{
      color: red;
    }
  `
  shadow1.appendChild(one)
  shadow1.appendChild(style1)

  const box2 = document.getElementById("box2")
  // 开启shadow dom模式
  const shadow2 = box2.attachShadow({mode: "open"})
  const two = document.createElement("div")
  two.className = "one"
  two.innerText = "第二个内容"
  const style2 = document.createElement("style")
  style2.textContent = `
    .one{
      color: blue;
    }
  `
  shadow2.appendChild(two)
  shadow2.appendChild(style2)
</script>
</body>
</html>
8、应用间通信
import {ref} from "vue";

export const headerStatus = ref(true)
export const changeHeader = type => headerStatus.value = type
import {ref} from "vue";

export const navStatus = ref(true)
export const changeNav = type => navStatus.value = type
<template>
  <!-- 用户信息 -->
  <UserInfo v-show="headerStatus"/>
  <!-- 导航 -->
  <MainNav v-show="navStatus"/>
</template>

<script>
import MainNav from './main-nav'
import UserInfo from './userInfo.vue'
import {header, nav} from "../../store"

export default {
  name: 'MainHeader',
  components: {
    MainNav,
    UserInfo
  },
  setup() {
    return {
      headerStatus: header.headerStatus,
      navStatus: nav.navStatus
    }
  }
};
</script>
import React, {useEffect} from 'react';
import globalConfig from "../../config/globalConfig";
import LoginPanel from "./components/LoginPanel.jsx";
import {getMain} from "../../utils/main";

import "./index.scss"

const Login = () => {

  useEffect(() => {
    const main = getMain()
    main.appInfo.header.changeHeader(false)
    main.appInfo.nav.changeNav(false)
  }, [])

  return (
    <div className="login">
      <img className="loginBackground" src={`${globalConfig.baseUrl}/login-background.png`}/>
      <LoginPanel/>
    </div>
  )
}

export default Login
let main = null
export const setMain = (data) => {
  main = data
}
export const getMain = () => {
  return main
}
export class Custom {
  // 事件监听
  on(name, cb) {
    window.addEventListener(name, (e) => {
      cb(e.detail)
    })
  }

  // 事件触发
  emit(name, data) {
    const event = new CustomEvent(name, {
      detail: data
    })
    window.dispatchEvent(event)
  }
}
9、全局状态管理
export const createStore = (initData = {}) => (() => {
  let store = initData
  const observers = [] // 管理所有的订阅者,依赖

  // 获取store
  const getStore = () => store
  // 更新store
  const update = (value) => {
    if (value !== store) {
      // 执行store的操作
      const oldValue = store
      // 将store更新
      store = value
      // 通知所有的订阅者,监听store的变化
      observers.forEach(async item => await item(store, oldValue))
    }
  }
  // 添加订阅者
  const subscribe = (fn) => {
    observers.push(fn)
  }
  return {
    getStore,
    update,
    subscribe
  }
})()
10、提高加载性能
import {getList} from "../const/subApps";
import {parseHtml} from "./index";

export const prefetch = async () => {
  // 1、获取到所有子应用列表-不包括当前正在显示的
  const list = getList().filter(item => !window.location.pathname.startsWith(item.activeRule))
  // 2、预加载剩下的所有子应用
  await Promise.all(list.map(async item => await parseHtml(item.entry, item.name)))
}