微前端实现原理剖析--上

202 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情。 前言: Iframe?Single-Spa?QianKun? 微前端很多,技术栈百花齐放,我们自己实现一波?

一、环境准备

1.1 使用rollup

npm init -y
npm i rollup rollup-plugin-serve

1.2 新建一个rollup.config.js

import serve from 'rollup-plugin-serve'

// rollup可以帮我们打包 es6的模块化语法
export default {
  input: './src/single-spa.js',
  output: {
    file: './lib/umd/single-spa.js',
    format: 'umd',
    name: 'singleSpa',
    sourcemap:true
  },
  plugins: [
    serve({
      openPage: '/index.html',
      contentBase: '',
      port: 3000
    })
  ]
    
}

1.3 编写package.json脚本

-c 走rollup.config.js -w 热更新

"dev": "rollup -c -w"

1.4 测试运行rollup

npm run dev

image.png rollup 环境运行成功

1.5 测试热更新

在single-spa.js中导出一个变量,lib/umd/single-spa.js会自动生成global的变量

export const a =1

截屏2022-04-14 下午7.55.08.png

二、注册应用的方法

2.1 新建一个index.html, 引入rollup打包好后的js

script src="/lib/umd/single-spa.js"></script>

2.2 调用注册微应用的方法和启动方法

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script src="/lib/umd/single-spa.js"></script>
  <script>
    // 参数1 注册应用的名字 参数2 加载app的方法必须返回一个pormise方法
    singleSpa.registerApplication('app1',async () => {
        // 这个函数需要返回结果
        return {
          bootstrap: async() => {

          },
          mount: async() => {

          },
          unmount: async() =>{

          }
        }
    }, 
      location => location.hash.startsWith('#/app1'),
      {store: {name:'zf',age:10}}
    )

    // 启动这个应用
    singleSpa.start() 
  </script>
</body>
</html>

image.png

2.3 新建src/appications/app.js

/**
 * 
 * @param {*} appName 应用名字 
 * @param {*} loadApp   加载引用
 * @param {*} activeWhen  当激活时会调用 loadApp
 * @param {*} customProps   自定义属性
 */

const apps = [] // 用来存放所有的应用

export function registerApplication (appName, loadApp, activeWhen, customProps) { 
  apps.push({
    name: appName,
    loadApp,
    activeWhen,
    customProps
  }) 

  console.log(apps)
}

2.4 新建src/start.js

export function start () { 
  
}

2.5 最后在src/single-spa中导出registerApplication方法 和 start方法

export { registerApplication} from './applications/app'
export { start} from './start'
测试调用registerApplication方法的应用保存到啊数组中了

image.png

三、 微应用生命周期流程图

未命名文件.jpg

3.1 新建src/application/app.helper.js工具文件

// 描述应用的整个状态
export const NOT_LOADED = 'NOT_LOADED' // 
export const LOADING_SOURCE_CODE = 'LOADING_SOURCE_CODE'
export const NOT_BOOTSTRAPPED = 'NOT_BOOTSTRAPPED'
export const BOOTSTRAPPING = 'BOOTSTRAPPING'
export const NOT_MOUNTED = 'NOT_MOUNTED'
export const MOUNTING = 'MOUNTING'
export const MOUNTED = 'MOUNTED'
export const UPDATING = 'UPDATING'
export const UNMOUNTING = 'UNDATING'
export const UNLOADING = 'UNLOADING'
export const LOAD_ERR = 'LOAD_ERR'
export const SKIP_BECAUSE_BROKEN = 'SKIP_BECAUSE_BROKEN'

// 当前应用是否被激活
export function isActive (app) { 
  return app.status === 'MOUNTED'
} 

// 当前应用是否要被激活
export function shouldBeAcitve (app) { // true 引用就开始初始化等一系列操作
  return app.activeWhen(window.location)
} 

3.3 新建src/navigations/reroute.js重新加载路由

import { started } from "../start";

export function reroute () { 
  if (started) {
    console.log('调用start方法');
  }  else { 
    console.log('调用register');
  }
  
}

3.4 修改start.js

import { reroute } from "./navigations/reroute";

export let started = false

export function start () { 
    // 需要挂载应用
  started = true
  reroute() // 重新加载路由
}

3.5 修改app.js 中注册完应用后重新加载路由

// 维护应用所有的状态 状态机
export function registerApplication (appName, loadApp, activeWhen, customProps) { 
  apps.push({
    name: appName,
    loadApp,
    activeWhen,
    customProps,
    status: NOT_LOADED
  }) 

  console.log(apps)
  reroute()   // 加载应用
  // vue 一系列的生命周期
}
测试先register后start ok!

image.png

四、 封装whenActive 路由改变加载微应用

4.1 src/app.js 暴露方法

export function getAppChanges () { 
  const appsTouUmount = [] // 要卸载的app
  const appsToLoad = [] //要加载的app
  const appsToMount = [] // 需要挂载的app
  apps.forEach(app => { 
    // 需不需要加载
    const appShouldBeActive = app.status !== SKIP_BECAUSE_BROKEN && shouldBeAcitve(app)
    switch (app.status) {
      case NOT_LOADED:
      case LOADING_SOURCE_CODE:
        if (appShouldBeActive) {
          appsToLoad.push(app)
        }
        break
      case NOT_BOOTSTRAPPED:
      case BOOTSTRAPPING:
      case NOT_MOUNTED:
        if (appShouldBeActive) {
          appsToMount.push(app)
        }
        break;
      case MOUNTED:
        if (!appShouldBeActive) { 
          appsTouUmount.push(app)
        }
      default:
        break;
    }
  })
  return { appsToLoad, appsToMount,appsTouUmount }
}

4.2 reroute.js 去调用方法


export function reroute () { 

  // 需要获取加载的应用

  // 需要获取要被挂载的应用

  // 哪些应用需要被卸载

  const { appsToLoad, appsToMount, appsTouUmount } = getAppChanges()
  console.log(appsToLoad, appsToMount, appsTouUmount);

  if (started) {
    console.log('调用start方法');
    // app 装载
    return performaAppChanges() // 根据路径来装载应用
  } else { 
    // 注册应用时 需要预先加载  
    console.log('调用注册方法');
    return loadApps()
    console.log('调用register');
  }

  // 预加载应用
  async function loadApps () { 

  }

  // 根据路径来装载应用
  async function performaAppChanges () { 

  }

  
}
Tips: 别忘了index.html中的location看路由是否需要别激活的回调方法

image.png

4.3 当输入#/app1的时候,应用被加载进来了

截屏2022-04-14 下午10.43.41.png