微前端简易版本实现

243 阅读18分钟

项目启动

根目录下的package.json配置如下

{
  "name": "micro-web-project",
  "version": "1.0.0",
  "description": "create micro project for myself",
  "main": "index.js",
  "scripts": {
    "start": "node ./build/run.js"
  },
  "author": "yancy",
  "license": "ISC",
  "devDependencies": {},
  "dependencies": {}
}

其中"start": "node ./build/run.js"文件创建多个子进程进行多个项目启动

const childProcess = require('child_process')
const path = require('path')

const filePath = {
  vue2: path.join(__dirname, '../vue2'),
  vue3: path.join(__dirname, '../vue3'),
  react15: path.join(__dirname, '../react15'),
  react16: path.join(__dirname, '../react16'),
  // 启动主应用;稍后主应用生命周期构建后取消注释
  // main: path.join(__dirname, '../main')
}
// cd 子应用的目录 npm start 启动项目
function runChild () {
  Object.values(filePath).forEach(item => {
    childProcess.spawn(`cd ${item} && npm start`, { stdio: "inherit", shell: true })
  })
}
runChild()

子应用打包配置

vue2

vue.config.js

const path = require('path');

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

const packageName = 'vue2';
const port = 9004;

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

vue3

const path = require('path');

const packageName = 'vue3'

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 库格式
      filename: 'vue3.js',
      library: `${packageName}`,
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_${packageName}`,
    },
  },
};

react15

webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  entry: {
    path: ['./index.js']
  },
  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'
    })
  ],
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'react15.js',
    library: 'react15',
    libraryTarget: 'umd',
    umdNamedDefine: true,
    publicPath: 'http://localhost:9002/'
  },
  devServer: {
    // 配置允许跨域
    headers: { 'Access-Control-Allow-Origin': '*' },
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 9002,
    historyApiFallback: true,
    hot: true,
  }
}

react16

webpack.config.js

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
  }
}

子应用入口

vue2的mian.js入口

import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false

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

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

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

export async function mount() {
  render()
}

export async function unmount(ctx) {
  const { container } = ctx
  if (container) {
    document.querySelector(container).innerHTML = ''
  }
}

vue3的mian.js入口

import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { setMain } from './utils/global'

let instance = null;

function render() {
  instance = createApp(App);
  instance
    .use(router)
    .mount('#app');
}

if (!window.__MICRO_WEB__) {
  render();
}
export async function bootstrap() {
  console.log('vue3.0 app bootstrap');
}

export async function mount(app) {
  setMain(app)
  render();
}

export async function unmount(ctx) {
  instance.unmount();
  instance = null;
  const { container } = ctx
  if (container) {
    document.querySelector(container).innerHTML = ''
  }
}

setMain所在文件代码如下

export let main = {}

export const setMain = (data) => {
  main = data
}

react15的index.js入口

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 const bootstrap = () => {
  console.log('bootstrap')
}

export const mount = () => {
  render()
}

export const unmount = () => {
  console.log('卸载')
}

react16和15一致

子应用注册

在主应用中注册子应用

主应用vue3的mian.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { subNavList } from './store/sub'
import { registerApp } from './util'

registerApp(subNavList)

createApp(App).use(router()).mount('#micro_web_main_app')

需要注册的导航菜单,注册到微前端框架

import { subNavList } from './store/sub'中的sub

import { loading } from '../store'
import * as appInfo from '../store'

export const subNavList = [
  {
    name: 'react15',// 唯一
    entry: '//localhost:9002/',  // 子应用的入口
    loading,
    container: '#micro-container',  // 子应用注册到的容器,即App.vue中放置微前端子应用的dom容器
    <!--     <template>
      <Header v-show="headerStatus"/>
      <MainNav v-show="navStatus"/>
      <div class="sub-container">
        <Loading v-show="loading"/>
        <div v-show="!loading" id="micro-container">子应用内容</div>
      </div>
    </template> -->
    activeRule: '/react15',  // 活动路由,即非hash前面的window.pathname。如www.baidu.com/react15/#/
    appInfo,
  },
  {
    name: 'react16',
    entry: '//localhost:9003/',
    loading,
    container: '#micro-container',
    activeRule: '/react16',
    appInfo,
  },
  {
    name: 'vue2',
    entry: '//localhost:9004/',
    loading,
    container: '#micro-container',
    activeRule: '/vue2',
    appInfo,
  },
  {
    name: 'vue3',
    entry: '//localhost:9005/',
    loading,
    container: '#micro-container',
    activeRule: '/vue3',
    appInfo,
  },
];

路由配置

import router from './router'

活动路由,即非hash前面的window.pathname。如www.baidu.com/react15/#/。其最终都从App.vue进入

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;

注册子应用(main/micro)微前端中处理

在主应用下创建micro文件夹用于微前端

registerMicroApps方法提供给主应用调用

micro/index.js

export { registerMicroApps, start } from './start'

启动微前端
micro/start.js

import { setList, getList } from './const/subApps'

export const registerMicroApps = (appList) => {
  setList(appList)
}

存储appList
micro/const/subApps.js

let list = []

export const getList = () => list

export const setList = appList => list = appList

注册子应用(main/src/main.js)主应用中处理,调用main/micro的注册

registerApp(subNavList)

import { registerApp } from './util'

util/index.js

import { registerMicroApps } from '../../micro'

export const registerApp = (list) => {
  // 注册到微前端框架里
  registerMicroApps(list)
}

路由拦截

micro/start.js

// 实现路由拦截
+ import { rewriteRouter } from './router/rewriteRouter'
import { setList, getList } from './const/subApps'

+ rewriteRouter()

export const registerMicroApps = (appList) => {
  setList(appList)
}

micro/router/rewriteRouter.js

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.addEventListener('micro_push', turnApp)
  window.addEventListener('micro_replace', turnApp)

  // 监听返回事件
  window.onpopstate = async function () {
    await turnApp()
  }
}

import { turnApp } from './routerHandle'
micro/router/routerHandle.js

export const turnApp = async () => {
  console.log('isrunning')
}

import { patchRouter } from 'micro/utils

// 给当前的路由跳转打补丁
export const patchRouter = (globalEvent, eventName) => {
  return function () {
    const e = new Event(eventName)
    globalEvent.apply(this, arguments)
    window.dispatchEvent(e)
  }
}

获取首个子应用

在主应用的src/util/index.js

+ import { registerMicroApps, start } from '../../micro'

export const registerApp = (list) => {
  // 注册到微前端框架里
  registerMicroApps(list)

  // 开启微前端框架
+  start()
}

micro/start.js

// 实现路由拦截
import { rewriteRouter } from './router/rewriteRouter'
+ import { currentApp } from './utils'
import { setList, getList } from './const/subApps'

rewriteRouter()

export const registerMicroApps = (appList) => {
  setList(appList)
}

+ // 启动微前端框架
+ 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
+ }

import { currentApp } from './utils'

micro/utils/index.js

+ 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 = () => {
    // window.location.pathname获取的www.baidu.com/react15#/xxx中的/react15
+   const currentUrl = window.location.pathname
+ 
+   return filterApp('activeRule', currentUrl)
+ }
+ 
+ export const filterApp = (key, value) => {
+   const currentApp = getList().filter(item => item[key] === + value)
+ 
+   return currentApp && currentApp.length ? currentApp[0] : {}
+ }

// 子应用是否做了切换
export const isTurnChild = () => {
  if(window.location.pathname === window.__CURRENT_SUB_APP__) {
      return false
  }
  return true;
}

micro/router/routerHandle.js

在routerHandle.js中控制路由切换,避免重复执行

+ import isTurnChild from 'micro/utils/index.js'

export const turnApp = async () => {
+  if(!isTurnChild()) {
+    retun;
+  }
  console.log('isrunning')
}

主应用生命周期

在主应用的src/util/index.js

import { registerMicroApps, start } from '../../micro'

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

  // 开启微前端框架
  start()
}

micro/start.js

// 实现路由拦截
import { rewriteRouter } from './router/rewriteRouter'
import { currentApp } from './utils'
import { setList, getList } from './const/subApps'
+ import { setMainLifecycle } from './const/mainLifeCycle'

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
 }

import { setMainLifecycle } from './const/mainLifeCycle'

let lifecycle = {}

export const getMainLifecycle = () => lifecycle

export const setMainLifecycle = data => lifecycle = data

通过生命周期控制loading

src/App.vue

<template>
  <Header v-show="headerStatus"/>
  <MainNav v-show="navStatus"/>
  <div class="sub-container">
    <Loading v-show="loading"/>
    <div v-show="!loading" id="micro-container">子应用内容</div>
  </div>
</template>

<script>
import Header from "./components/Header";
import MainNav from "./components/MainNav";
import Loading from "./components/Loading";

import { loading, header, nav } from './store'

export default {
  name: 'App',
  components: {
    Header,
    MainNav,
    Loading,
  },
  setup() {
    return {
      loading: loading.loadingStatus,
      headerStatus: header.headerStatus,
      navStatus: nav.navStatus,
    }
  }
}
</script>

<style>
html, body, #micro_web_main_app{
  width: 100%;
  /*height: 100%;*/
}
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.sub-container{
  min-height: 100%;
  position: relative;
}
#micro-container{
  min-height: 100%;
  width: 100%;
}
</style>

import { loading, header, nav } from './store'

src/store/loading.js

import { ref } from 'vue'

export let loadingStatus = ref(true)

export const changeLoading = type => loadingStatus.value = type

然后在生命周期中改变loading状态

src/util/index.js

import { registerMicroApps, start, createStore } from '../../micro'

+ import { loading } from '../store'

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

  // 开启微前端框架
  start()
}

micro/start.js

// 实现路由拦截
import { rewriteRouter } from './router/rewriteRouter'
import { currentApp } from './utils'
import { setList, getList } from './const/subApps'
+ import { setMainLifecycle } from './const/mainLifeCycle'

rewriteRouter()

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

  lifeCycle.beforeLoad[0]()

  setTimeout(() => {
    lifeCycle.mounted[0]()
  }, 3000)
  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
 }

微前端生命周期

micro/router/routerHandle.js

import { isTurnChild } from '../utils'
+ import { lifecycle } from '../lifeCycle'
export const turnApp = async () => {
  if (isTurnChild()) {
    // 微前端的生命周期执行
+    await lifecycle()
  }
}

import { lifecycle } from '../lifeCycle' 随后解释,先设置路由切换前后应用标志activeRole

micro/start.js

中const app = currentApp();window.CURRENT_SUB_APP = app.activeRule 设置当前使用的app唯一标识activeRule

micro/util/index.js

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)
+ }

export const filterApp = (key, value) => {
  const currentApp = getList().filter(item => item[key] === value)

  return currentApp && currentApp.length ? currentApp[0] : {}
}

// 子应用是否做了切换
export const isTurnChild = () => {
  const { pathname, hash } = window.location
  const url = pathname + hash

  // 当前路由无改变。
+  const currentPrefix = url.match(/(\/\w+)/g)

+  if (
+    currentPrefix &&
+    (currentPrefix[0] === window.__CURRENT_SUB_APP__) &&
+    hash === window.__CURRENT_HASH__
+  ) {
+    return false;
+  }
  // 保存上一步的路由状态window.__ORIGIN_APP__
+  window.__ORIGIN_APP__ = window.__CURRENT_SUB_APP__;

+  const currentSubApp = window.location.pathname.match(/(\/\w+)/)

  if (!currentSubApp) {
    return false
  }
  // 当前路由以改变,修改当前路由
  window.__CURRENT_SUB_APP__ = currentSubApp[0];

  // 判断当前hash值是否改变
  window.__CURRENT_HASH__ = hash

  return true;
}

micro/lifeCycle/index.js

import { lifecycle } from '../lifeCycle' 根据前后应用的切换过程中,获取的需要卸载的路由和需要加载的路由进行生命周期执行

+ import { findAppByRoute } from '../utils'
+ import { getMainLifecycle } from '../const/mainLifeCycle'
+ 
+ export const lifecycle = async () => {
+   // 获取到上一个子应用
+   const prevApp = findAppByRoute(window.__ORIGIN_APP__)
+ 
+   // 获取到要跳转到的子应用
+   const nextApp = findAppByRoute(window.__CURRENT_SUB_APP__)
+ 
+   if (!nextApp) {
+     return
+   }
+ 
+   if (prevApp) {
+     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 appContext = null;
+   return appContext
+ }
+ 
+ export const mounted = async (app) => {
+   app && app.mount && app.mount()
+ 
+   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()))
+ }

加载和解析html

在项目加载时加载html

即在 micro/lifeCycle/index.js下

 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) {
     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 appContext = null;
+   const appContext = await loadHtml(app);
   return appContext
 }
 
 export const mounted = async (app) => {
   app && app.mount && app.mount()
 
   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()))
 }

loadHtml方法存在如下文件下

micro/loader/index.js

之前启动的都是子应用并没有启动主应用;放开文章头部注释内容

获取子应用html文本的方法

micro/utils/fetchResource.js

+ export const fetchResource = url => fetch(url).then(async res => await res.text())

加载html

+ import { fetchResource } from 'micro/utils/fetchResource'
+ // 加载html的方法
+ export const loadHtml = async(app) => {
+   // 子应用需要显示在哪里
+   let container = app.container;  //#id内容
+   // 子应用的入口
+   let entry = app.entry;
+ 
+   const html = await parseHtml(entry)
+ 
+   const ct = document.querySelector(container)
+ 
+   if (!ct) {
+     throw new Error('容器不存在,请查看')
+   }
+   ct.innerHTML = html;
+ 
+   return app;
}

+ // 解析html
+ export const parseHtml = async (entry) => {
+   const html = await fetchResource(entry)
+   
+   const div = document.createElement('div',)
+   div.innerHTML = html
+   // 标签,link,script
+   const [dom, scriptUrl, script] = await getResource(div) 
+   console.log(dom, scriptUrl, script)
+   return html
+ }
+ 
+ export const getResource = async() => {
+   return ['', '', '']
+ }

loadHtml方法实际返回处理后的子应用。因此beforeLoad方法执行时传递的subApp

因此之前改写如下

在 micro/lifeCycle/index.js下

 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) {
     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.mount && app.mount()
 
   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()))
 }

getResource方法;获取js执行脚本

micro/loader/index.js

 import { fetchResource } from 'micro/utils/fetchResource'
 // 加载html的方法
 export const loadHtml = async(app) => {
   // 子应用需要显示在哪里
   let container = app.container;  //#id内容
   // 子应用的入口
   let entry = app.entry;
 
   const [dom, script] = await parseHtml(entry)
 
   const ct = document.querySelector(container)
 
   if (!ct) {
     throw new Error('容器不存在,请查看')
   }
   ct.innerHTML = html;
 
   return app;
 }

 // 解析html
 export const parseHtml = async (entry) => {
   const html = await fetchResource(entry)
   
+   let allScript = []
   const div = document.createElement('div',)
   div.innerHTML = html
   // 标签,link,script
-+   const [dom, scriptUrl, script] = await getResource(div, entry) 
+   console.log(dom, scriptUrl, script)
+   const fetchedScript = await Promise.all(scriptUrl.map(async item => fetchResource(item)))
+   allScript = script.concat(fetchedScript)
+   return [dom, allScript]
 }
 
+ export const getResource = async(root, entry) => {
+   const scriptUrl = [] // js 链接: src href
+   const script = []  // 卸载script中的js脚本内容
+   const dom = root.outerHTML
+
+   // 深度解析
+   function deepParse(element) {
+     const children = element.children
+     const parent = element.parentNode;
+     // 第一步处理位于 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]
 }

目前获取到dom和所有的script但是还未执行。运行js脚本 import { performScriptForFunction } from 'micro/sandBox/performScript.js'

 import { fetchResource } from 'micro/utils/fetchResource'
+ import { performScriptForFunction } from 'micro/sandBox/performScript.js'
 // 加载html的方法
 export const loadHtml = async(app) => {
   // 子应用需要显示在哪里
   let container = app.container;  //#id内容
   // 子应用的入口
   let entry = app.entry;
 
   const [dom, script] = await parseHtml(entry)
 
   const ct = document.querySelector(container)
 
   if (!ct) {
     throw new Error('容器不存在,请查看')
   }
   ct.innerHTML = html;

  // 执行脚本
+   scripts.forEach(item => {
+    performScriptForFunction(item)
+  })
 
   return app;
 }

执行js脚本

micro/sandBox/performScript.js

import { performScriptForFunction } from 'micro/sandBox/performScript.js'

export const performScriptForFunction = (script) => {
  // 方式一:new Function
  new Function(script).call(window, window)
}

export const performScriptForEval = (script) => {
  // 方式二:eval
  // eval(script) 会重复执行 内部执行触发history不会拦截 => isTrueChild再执行
  eval(script)
}

为了避免重复isTrueChild再执行执行,需要修改

micro/util/index.js

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)
 }

export const filterApp = (key, value) => {
  const currentApp = getList().filter(item => item[key] === value)

  return currentApp && currentApp.length ? currentApp[0] : {}
}

// 子应用是否做了切换
export const isTurnChild = () => {
  const { pathname, hash } = window.location
  const url = pathname + hash

  // 当前路由无改变。
+  const currentPrefix = url.match(/(\/\w+)/g)

+  if (
+    currentPrefix &&
+    (currentPrefix[0] === window.__CURRENT_SUB_APP__) &&
+    hash === window.__CURRENT_HASH__
+  ) {
+    return false;
+  }
  // 保存上一步的路由状态window.__ORIGIN_APP__
+  window.__ORIGIN_APP__ = window.__CURRENT_SUB_APP__;

+  const currentSubApp = window.location.pathname.match(/(\/\w+)/)

+  if (!currentSubApp) {
+    return false
+  }
+  // 当前路由以改变,修改当前路由
+  window.__CURRENT_SUB_APP__ = currentSubApp[0];

  // 判断当前hash值是否改变
  window.__CURRENT_HASH__ = hash

  return true;
}

微前端环境变量设置

  1. 替换执行方法为 micro/sandBox/index.js

micro/loader/index.js

 import { fetchResource } from 'micro/utils/fetchResource'
- import { performScriptForFunction } from 'micro/sandBox/performScript.js'
+ import { sandBox } from "../sandbox";
 // 加载html的方法
 export const loadHtml = async(app) => {
   // 子应用需要显示在哪里
   let container = app.container;  //#id内容
   // 子应用的入口
   let entry = app.entry;
 
   const [dom, script] = await parseHtml(entry)
 
   const ct = document.querySelector(container)
 
   if (!ct) {
     throw new Error('容器不存在,请查看')
   }
   ct.innerHTML = html;

  // 执行脚本
-   scripts.forEach(item => {
-    performScriptForFunction(item)
-  })
+  scripts.forEach(item => {
+    sandBox(app, item)
+  })
 
   return app;
 }

 // 解析html
 export const parseHtml = async (entry) => {
   const html = await fetchResource(entry)
   
   let allScript = []
   const div = document.createElement('div',)
   div.innerHTML = html
   // 标签,link,script
   const [dom, scriptUrl, script] = await getResource(div, entry) 
   console.log(dom, scriptUrl, script)
   const fetchedScript = await Promise.all(scriptUrl.map(async item => fetchResource(item)))
   allScript = script.concat(fetchedScript)
   return [dom, allScript]
 }
 
 export const getResource = async(root, entry) => {
   const scriptUrl = [] // js 链接: src href
   const script = []  // 卸载script中的js脚本内容
   const dom = root.outerHTML

   // 深度解析
   function deepParse(element) {
     const children = element.children
     const parent = element.parentNode;
     // 第一步处理位于 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]
 }
  1. 挂载生命周期和标志

micro/sandBox/index.js

import { sandBox } from "../sandbox";

+ import { performScriptForEval } from './performScript'
+ 
+ const isCheckLifeCycle = lifecycle => lifecycle &&
+   lifecycle.bootstrap &&
+   lifecycle.mount &&
+   lifecycle.unmount
+ 
+ // 子应用生命周期处理, 环境变量设置
+ export const sandBox = (app, script) => {
+   // 1. 设置环境变量
+   window.__MICRO_WEB__ = true
+ 
+   // 2. 运行js文件
+   const lifecycle = performScriptForEval(script, app.name)
+ 
+   // 生命周期,挂载到app上
+   if (isCheckLifeCycle(lifecycle)) {
+     app.bootstrap = lifecycle.bootstrap
+     app.mount = lifecycle.mount
+     app.unmount = lifecycle.unmount
+   }
+ }
  1. 修改执行js的方法,使其能够在window上获取bootstrap,mount,unmount生命周期,并将其返回

micro/sandBox/performScript.js

// 执行js脚本
export const performScriptForFunction = (script, appName) => {
  const scriptText = `
    ${script}
    return window['${appName}']
  `
  return new Function(scriptText).call(window, window)
}

export const performScriptForEval = (script, appName) => {
  // library window.appName
  const scriptText = `
    () => {
      ${script}
      return window['${appName}']
    }
  `
  return eval(scriptText).call(window, window)// app module mount
}

window.appName是来自src/store/sub.js
下的配置项。该配置项已经通过之前步骤注入到微前端框架。app中既是其中内容

export const subNavList = [
  {
    name: 'react15',// 唯一
    entry: '//localhost:9002/',
    loading,
    container: '#micro-container',
    activeRule: '/react15',
    appInfo,
  },
  {
    // 与webpack.config.js中的library一致
    name: 'react16',
    entry: '//localhost:9003/',
    loading,
    container: '#micro-container',
    activeRule: '/react16',
    appInfo,
  },
  {
    name: 'vue2',
    entry: '//localhost:9004/',
    loading,
    container: '#micro-container',
    activeRule: '/vue2',
    appInfo,
  },
  {
    name: 'vue3',
    entry: '//localhost:9005/',
    loading,
    container: '#micro-container',
    activeRule: '/vue3',
    appInfo,
  },
];

以上配置中的name要与各个子应用中的打包配置library一致
用react16为例:webpack.config.js

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配置项会将该子应用的js导出的全局属性暴露在window.react16上
    // 例如子应用的 export const bootstrap;export const mount;export const unmount
    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
  }
}

以上完成之后在

micro/lifeCycle/index.js

中的app.mount等生命周期才会执行

运行环境隔离 --- 快照沙箱

新建 micro/sandbox/snapShotSandbox.js

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]
      }
    }
  }
}

然后在环境变量设置与运行js文件时,由原来的window变为当前的proxy上

micro/sandbox/index.js

import { performScriptForEval } from './performScript'
+ import { SnapShotSandbox } from './snapShotSandbox'

const isCheckLifeCycle = lifecycle => lifecycle &&
  lifecycle.bootstrap &&
  lifecycle.mount &&
  lifecycle.unmount

// 子应用生命周期处理, 环境变量设置
export const sandBox = (app, script) => {

+  const proxy = new SnapShotSandbox()
+
+  if (!app.proxy) {
+    app.proxy = proxy
+  }

  // 1. 设置环境变量
  window.__MICRO_WEB__ = true

  // 2. 运行js文件
-  const lifecycle = performScriptForEval(script, app.name)
+  const lifecycle = performScriptForEval(script, app.name, app.proxy.proxy)

  // 生命周期,挂载到app上
  if (isCheckLifeCycle(lifecycle)) {
    app.bootstrap = lifecycle.bootstrap
    app.mount = lifecycle.mount
    app.unmount = lifecycle.unmount
  }
}

performScriptForEval(script, app.name, app.proxy.proxy)改造如下

micro/sandBox/performScript.js

// 执行js脚本
-+ export const performScriptForFunction = (script, appName, global) => {
  const scriptText = `
    ${script}
    return window['${appName}']
  `
-  return new Function(scriptText).call(window, window)
+  return new Function(scriptText).call(global, global)
}

-+ export const performScriptForEval = (script, appName, global) => {
  // library window.appName
  const scriptText = `
    () => {
      ${script}
      return window['${appName}']
    }
  `
  return eval(scriptText).call(global, global)// app module mount
}

执行卸载沙箱操作

在 micro/lifeCycle/index.js下

 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.unmount) {
+     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.mount && app.mount()
 
   await runMainLifeCycle('mounted')
 }
 
 export const destoryed = async (app) => {
   app && app.unmount && app.unmount()
 
   // 对应的执行以下主应用的生命周期
   await runMainLifeCycle('destoryed')
 }
 
 export const runMainLifeCycle = async (type) => {
   const mainlife = getMainLifecycle()
 
   await Promise.all(mainlife[type].map(async item => await item()))
 }

快照沙箱适用于老版本浏览器(性能不好)

运行环境隔离 --- 代理沙箱

micro/sandbox/proxySandBox.js

// 代理沙箱

let defaultValue = {} // 子应用的沙箱容器

export class ProxySandbox{
  constructor() {
    this.proxy = null;

    this.active()
  }
  // 沙箱激活
  active() {
    // 子应用需要设置属性,
    this.proxy = new Proxy(window, {
      get(target: window, key) {
        // window上的方法会存在非法操作
        if (typeof target[key] === 'function') {
          return target[key].bind(target)
        }
        return defaultValue[key] || target[key] // 当获取window本身的属性时。使用target[key]
      },
      set(target, key, value) {
        defaultValue[key] = value
        return true
      }
    })

  }

  // 沙箱销毁
  inactive () {
    defaultValue = {}
  }
}

micro/sandbox/index.js

import { performScriptForEval } from './performScript'
- import { SnapShotSandbox } from './snapShotSandbox'
+ import { proxySandBox } from './proxySandBox'

const isCheckLifeCycle = lifecycle => lifecycle &&
  lifecycle.bootstrap &&
  lifecycle.mount &&
  lifecycle.unmount

// 子应用生命周期处理, 环境变量设置
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)

  // 生命周期,挂载到app上
  if (isCheckLifeCycle(lifecycle)) {
    app.bootstrap = lifecycle.bootstrap
    app.mount = lifecycle.mount
    app.unmount = lifecycle.unmount
  }
}

performScriptForEval(script, app.name, app.proxy.proxy)改造如下

micro/sandBox/performScript.js

// 执行js脚本

// 执行js脚本
export const performScriptForFunction = (script, appName, global) => {
  window.proxy = global
  console.log(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
}

css样式隔离

  • css module
  • shadow dom
    • mode: attachShadow->shadow
  • minicss:webpack打包单独的css文件
  • css-in-js

主应用和子应用通信(props方式)

  • 好莱坞原则: 不用联系我,当我需要的时候会打电话给你
  • 依赖注入:主应用的显示隐藏,注入到子应用内部,通过子应用内部的方法进行调用

main/src/mian.js 注册(通过main.js的registerApp(subNavList)注册微前端框架,js的registerApp调用微前端micro/index.js的注册方法,微前端注册方法registerApp实现appList和生命周期的注册。而appList为主应用传递的subNavList内容)时携带子路由配置信息

import { subNavList } from './store/sub'

import { loading } from '../store'
+ import * as appInfo from '../store'

export const subNavList = [
  {
    name: 'react15',// 唯一
    entry: '//localhost:9002/',
    loading,
    container: '#micro-container',
    activeRule: '/react15',
+    appInfo,
  },
  {
    name: 'react16',
    entry: '//localhost:9003/',
    loading,
    container: '#micro-container',
    activeRule: '/react16',
+    appInfo,
  },
  {
    name: 'vue2',
    entry: '//localhost:9004/',
    loading,
    container: '#micro-container',
    activeRule: '/vue2',
+    appInfo,
  },
  {
    name: 'vue3',
    entry: '//localhost:9005/',
    loading,
    container: '#micro-container',
    activeRule: '/vue3',
+    appInfo,
  },
];

主应用appInfo信息

import * as appInfo from '../store'

main/src/store/index.js

// 暴露loading的方法
export * as loading from './loading'

// 暴露header的方法
export * as header from './header'

// 暴露nav的方法
export * as nav from './nav'

main/src/store/loading.js

main/src/store/header.js

main/src/store/nav.js

三个文件存放主路由存在的变量,变量用于控制头部header和导航栏nav控制
在登录的子应用时,控制隐藏

// 主应用为vue3的代码:设置显示和隐藏
import { ref } from 'vue';

export const headerStatus = ref(true)

export const changeHeader = type => headerStatus.value = type;

主应用中通过header和nav的状态控制显示隐藏

main/src/App.vue

<template>
+  // 状态控制显示隐藏
+  <Header v-show="headerStatus"/>
+  <MainNav v-show="navStatus"/>
  <div class="sub-container">
    <Loading v-show="loading"/>
    <div v-show="!loading" id="micro-container">子应用内容</div>
  </div>
</template>

<script>
import Header from "./components/Header";
import MainNav from "./components/MainNav";
import Loading from "./components/Loading";

import { loading, header, nav } from './store'

export default {
  name: 'App',
  components: {
    Header,
    MainNav,
    Loading,
  },
  setup() {
    return {
+      loading: loading.loadingStatus,
+      headerStatus: header.headerStatus,
+      navStatus: nav.navStatus,
    }
  }
}
</script>

<style>
html, body, #micro_web_main_app{
  width: 100%;
  /*height: 100%;*/
}
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.sub-container{
  min-height: 100%;
  position: relative;
}
#micro-container{
  min-height: 100%;
  width: 100%;
}
</style>

在子应用中改变显示隐藏状态实现对主应用的header和nav控制

main/micro/index.js中注册路由时(registerMicroApps携带appInfo)后续通过微前端生命周期,在mounted时将appInfo传入子应用

micro/lifeCycle.js

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.unmount) {
    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.mount && app.mount({
+    // 只需要subNavList的appInfo和entry即可
+    appInfo: app.appInfo,
+    entry: app.entry
  })

  await runMainLifeCycle('mounted')
}

export const destoryed = async (app) => {
  app && app.unmount && app.unmount()

  // 对应的执行以下主应用的生命周期
  await runMainLifeCycle('destoryed')
}

export const runMainLifeCycle = async (type) => {
  const mainlife = getMainLifecycle()

  await Promise.all(mainlife[type].map(async item => await item()))
}

然后react16的子应用入口中引入appInfo并改变值,并在登陆页面时实现header和nav隐藏

/react16/index.js

+ import { setMain } from './utils/main.js';
...省略代码

export const mount = (app) => {
  // 此处的app即为上述微前端注入子应用生命周期的内容

//   app.mount({
// +    // 只需要subNavList的appInfo和entry即可
// +    appInfo: app.appInfo,
// +    entry: app.entry
//   })

+  setMain(app)
  render();
}

...省略代码

/react16/utils/main.js

let main = null

export const setMain = (data) => {
  main = data
}

export const getMain = () => {
  return main
}

然后在进入login界面时实现header和nav隐藏

/react16/src/pages/login/ind.js

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

主应用和子应用通信(customEvent方式)

main/micro/customevent/index.js

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)
  }
}

然后在micro/start.js运行时添加事件监听

+ import { Custom } from './customevent'
+ 
+ const custom = new Custom()
+ custom.on('test', (data) => {
+   console.log(data)
+ })
+ 
+ window.custom = custom

最后子应用通过emit触发.触发后主应用事件监听获取值的变化。此时值可以控制显示隐藏

/react16/index.js

...省略代码

export const mount = (app) => {
  // 此处的app即为上述微前端注入子应用生命周期的内容

//   app.mount({
// +    // 只需要subNavList的appInfo和entry即可
// +    appInfo: app.appInfo,
// +    entry: app.entry
//   })

+  window.custom.emit('test', data: {
+   headerShow: false,
+   navShow: false
+ })
  render();
}

子应用间通信

viue3 ---> vue2

vue2/src/main.js

export const mount = () => {
   window.custom.emit('test1', {
     b: 2
   })

   window.custom.on('test2', (data) => {
    console.log(data)
  })

  render()
}

vue3/src/main.js

export const mount = () => {
  // vue3 ---> vue2
  // 先监听后触发
  window.custom.on('test1', () => {
    window.custom.emit('test2', {
      b: 2
    })
  })

  render()
}