• 微前端学习(下)


    一、课程目标

    1. qiankun 整体运行流程
    2. 微前端实现方案

    二、课程大纲

    1. qiankun 整体流程
    2. 微前端方案实现
    3. DIY微前端核心能力

    1、微前端方案实现

    • 基于 iframe 完全隔离的方案、使用纯的Web Components构建应用
    • EMP基于webpack module federation
    • qiankun、icestark 自己实现JS以及样式隔离

    2、qiankun 整体运行流程

    总流程
    注册子应用
    启动主应用
    是否激活子应用
    初始化子应用-只触发1次
    挂载子应用-可能触发多次
    卸载子应用-可能触发多次

    3、DIY 微前端核心能力

    3.1 应用注册 registerMicroApps(apps, lifeCycles?)

    • 参数
      • apps - Array - 必选,微应用的一些注册信息
      • lifeCycles - LifeCycles - 可选,全局的微应用生命周期钩子
    • 类型
      • RegistrableApp
        • name - string - 必选,微应用的名称,微应用之间必须确保唯一。
        • entry - string - 必选,微应用的入口。
        • container - string | HTMLElement - 必选,微应用的容器节点的选择器或者 Element 实例
        • activeRule - string | (location: Location) => boolean | Array boolean> - 必选,微应用的激活规则
      • LifeCyclest
        • type Lifecycle = (app: RegistrableApp) => Promise;
        • beforeMount - Lifecycle | Array - 可选
        • beforeUnmount - Lifecycle | Array - 可选
        • afterUnmount - Lifecycle | Array - 可选

    3.2 监听路由变化

    hash模式 | history模式

    如何实现前端路由?

    要实现前端路由 需要解决两个核心问题:

    • 如何改变 URL 却不引起页面刷新?
    • 如何检测 URL 变化了?
      下面分别使用 hash 和 history 两种实现方式回答上面的两个核心问题。
    1、hash 实现

    hash 是 URL 中 hash (#) 及后面的那部分,常用作锚点在页面内进行导航,改变 URL 中的 hash 部分不会引起页面刷新。
    通过 hashchange 事件监听 URL 的变化,改变 URL 的方式只有这几种:

    • 通过浏览器前进后退改变 URL
    • 通过标签改变 URL
    • 通过window.location改变URL
      这几种情况改变 URL 都会触发 hashchange 事件
    // 监听路由变化
    window.addEventListener('hashchange', onHashChange)
    
    • 1
    • 2
    2、history 实现

    history 提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新。
    history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:

    • 通过浏览器前进后退改变 URL 时会触发 popstate 事件,
    • 通过pushState/replaceState或标签改变 URL 不会触发 popstate 事件。好在我们可以拦截 pushState/replaceState的调用和标签的点击事件来检测 URL 变化,所以监听 URL 变化可以实现,只是没有 hashchange 那么方便。
    // 监听浏览器前进后退改变URL
    window.addEventListener("popstate", onPopState);
    
    • 1
    • 2

    3.3 路由劫持

    • 路由变化时匹配子应用
    • 执行子应用生命周期
    • 加载子应用

    3.4 生命周期

    • 主应用
      • beforeLoad: 挂载子应用前。
      • mounted: 挂载子应用后。
      • ummounted: 卸载子应用后。
    • 子应用
      • bootstrap: bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
      • mount: 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法。
      • unmount: 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例。

    3.5 资源加载

    • 加载样式表
    • 加载js资源
    • 执行js代码

    3.6 预加载

    具体实现

    依赖包

    "import-html-entry": "^1.12.0",
    "path-to-regexp": "^6.2.1",
    "qiankun": "^2.7.4"
    
    • 1
    • 2
    • 3

    start入口

    import { IAppInfo, ILifeCycle } from './types';
    
    import { setAppList, getAppList } from './appList/index';
    import { setLifeCycle } from './lifeCycle/index';
    import { hackRoute, reRoute } from './route/index';
    
    export const registerMicroApps = (
      appList: IAppInfo[],
      lifeCycle?: ILifeCycle
    ) => {
      appList && setAppList(appList);
      lifeCycle && setLifeCycle(lifeCycle);
    };
    
    export const start = () => {
      const list = getAppList();
      if (!list.length) {
        throw new Error('请先注册应用');
      }
    
      hackRoute();
      reRoute(window.location.href);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    存储appList

    // appList/index
    import { IAppInfo } from '../types';
    let appList: IAppInfo[] = [];
    
    export const setAppList = (list: IAppInfo[]): void => {
      appList = list;
    };
    
    export const getAppList = () => {
      return appList;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    存储lifeCycle 以及生命周期方法的实现

    import { ILifeCycle, IInternalAppInfo, IAppInfo } from '../types';
    import { EAppStatus } from '../enum';
    import { loadHTML } from '../loader'
    let lifeCycle: ILifeCycle = {};
    
    export const setLifeCycle = (lifeCycles: ILifeCycle): void => {
      lifeCycle = lifeCycles;
    };
    
    export const getLifeCycle = () => {
      return lifeCycle;
    };
    
    // 存储全局生命周期
    // 卸载
    export const runUnMounted = async (app: IInternalAppInfo) => {
      app.status = EAppStatus.UNMOUNTING;
      await app.unmounted?.(app);
      app.status = EAppStatus.NOT_MOUNTED;
      await runLifeCycle('unmounted', app);
    };
    
    // 初始化 只执行一次
    export const runBootstrap = async (app: IInternalAppInfo) => {
      if (app.status !== EAppStatus.LOADED) {
        return app;
      }
      app.status = EAppStatus.BOOTSTRAPING;
      await app.bootstrap?.(app);
      app.status = EAppStatus.NOT_MOUNTED;
    };
    // 挂载 可多次执行
    export const runMounted = async (app: IInternalAppInfo) => {
      app.status = EAppStatus.MOUNTING;
      await app.mounted?.(app);
      app.status = EAppStatus.MOUNTED;
      // 处理对应子应用生命周期
      await runLifeCycle('mounted', app);
    };
    
    // 加载前
    export const runBeforeLoad = async (app: IInternalAppInfo) => {
      app.status = EAppStatus.LOADING;
      await runLifeCycle('beforeLoad', app);
      // 加载子应用资源
      // app = await loadHTML(app)
      app.status = EAppStatus.LOADED;
    };
    
    const runLifeCycle = async (name: keyof ILifeCycle, app: IAppInfo) => {
      // lifeCycles - LifeCycles - 可选,全局的微应用生命周期钩子
      const fn = lifeCycle[name];
      if (fn instanceof Array) {
        await Promise.all(fn.map((item) => item(app)));
      } else {
        await fn?.(app);
      }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    TS相关类型-枚举

    export enum EAppStatus {
       NOT_FOUND = 'NOT_FOUND',
      NOT_LOADED = 'NOT_LOADED',
      LOADING = 'LOADING',
      LOADED = 'LOADED',
      BOOTSTRAPPING = 'BOOTSTRAPPING',
      NOT_MOUNTED = 'NOT_MOUNTED',
      MOUNTING = 'MOUNTING',
      UNMOUNTED = 'UNMOUNTED',
      MOUNTED = 'MOUNTED',
      UNMOUNTING = 'UNMOUNTING',
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    TS相关类型

    export interface IAppInfo {
      name: string
      entry: string
      container: string
      activeRule: string
    }
    
    export type Lifecycle = (app: IAppInfo) => Promise<any>
    
    export interface ILifecycle {
      beforeLoad?: Lifecycle | Lifecycle[]
      mounted?: Lifecycle | Lifecycle[]
      unmounted?: Lifecycle | Lifecycle
    }
    
    export interface IInternalAppInfo extends IAppInfo {
      status: EAppStatus
      bootstrap?: Lifecycle
      mount?: Lifecycle
      unmount?: Lifecycle
      proxy: any
    }
    
    export type EventType = 'hashchange' | 'popstate'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    路由拦截实现

    import { EventType } from '../types'
    import {
      runBoostrap,
      runBeforeLoad,
      runMounted,
      runUnmounted,
    } from '../lifeCycle'
    import { getAppListStatus } from '../utils'
    
    const capturedListeners: Record<EventType, Function[]> = {
      hashchange: [],
      popstate: [],
    }
    
    // 劫持和 history 和 hash 相关的事件和函数
    // 然后我们在劫持的方法里做一些自己的事情
    // 比如说在 URL 发生改变的时候判断当前是否切换了子应用
    
    const originalPush = window.history.pushState
    const originalReplace = window.history.replaceState
    
    let historyEvent: PopStateEvent | null = null
    
    let lastUrl: string | null = null
    
    export const reroute = (url: string) => {
      if (url !== lastUrl) {
        const { actives, unmounts } = getAppListStatus()
        Promise.all(
          unmounts
            .map(async (app) => {
              await runUnmounted(app)
            })
            .concat(
              actives.map(async (app) => {
                await runBeforeLoad(app)
                await runBoostrap(app)
                await runMounted(app)
              })
            )
        ).then(() => {
          callCapturedListeners()
        })
      }
      lastUrl = url || location.href
    }
    
    const handleUrlChange = () => {
      reroute(location.href)
    }
    
    export const hackRoute = () => {
      window.history.pushState = (...args) => {
        originalPush.apply(window.history, args)
        historyEvent = new PopStateEvent('popstate')
        args[2] && reroute(args[2] as string)
      }
      window.history.replaceState = (...args) => {
        originalReplace.apply(window.history, args)
        historyEvent = new PopStateEvent('popstate')
        args[2] && reroute(args[2] as string)
      }
    
      window.addEventListener('hashchange', handleUrlChange)
      window.addEventListener('popstate', handleUrlChange)
    
      window.addEventListener = hackEventListener(window.addEventListener)
      window.removeEventListener = hackEventListener(window.removeEventListener)
    }
    
    const hasListeners = (name: EventType, fn: Function) => {
      return capturedListeners[name].filter((listener) => listener === fn).length
    }
    
    const hackEventListener = (func: Function): any => {
      return function (name: string, fn: Function) {
        if (name === 'hashchange' || name === 'popstate') {
          if (!hasListeners(name, fn)) {
            capturedListeners[name].push(fn)
            return
          } else {
            capturedListeners[name] = capturedListeners[name].filter(
              (listener) => listener !== fn
            )
          }
        }
        return func.apply(window, arguments)
      }
    }
    
    export function callCapturedListeners() {
      if (historyEvent) {
        Object.keys(capturedListeners).forEach((eventName) => {
          const listeners = capturedListeners[eventName as EventType]
          if (listeners.length) {
            listeners.forEach((listener) => {
              // @ts-ignore
              listener.call(this, historyEvent)
            })
          }
        })
        historyEvent = null
      }
    }
    
    export function cleanCapturedListeners() {
      capturedListeners['hashchange'] = []
      capturedListeners['popstate'] = []
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109

    loader 加载器

    import { IInternalAppInfo } from '../types'
    import { importEntry } from 'import-html-entry'
    import { ProxySandbox } from './sandbox'
    
    export const loadHTML = async (app: IInternalAppInfo) => {
      const { container, entry } = app
    
      const { template, getExternalScripts, getExternalStyleSheets } =
        await importEntry(entry)
      const dom = document.querySelector(container)
    
      if (!dom) {
        throw new Error('容器不存在')
      }
    
      dom.innerHTML = template
    
      await getExternalStyleSheets()
      const jsCode = await getExternalScripts()
    
      jsCode.forEach((script) => {
        const lifeCycle = runJS(script, app)
        if (lifeCycle) {
          app.bootstrap = lifeCycle.bootstrap
          app.mount = lifeCycle.mount
          app.unmount = lifeCycle.unmount
        }
      })
    
      return app
    }
    
    const runJS = (value: string, app: IInternalAppInfo) => {
      if (!app.proxy) {
        app.proxy = new ProxySandbox()
        // @ts-ignore
        window.__CURRENT_PROXY__ = app.proxy.proxy
      }
      app.proxy.active()
      const code = `
        return (window => {
          ${value}
          return window['${app.name}']
        })(window.__CURRENT_PROXY__)
      `
      return new Function(code)()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    ProxySandbox

    export class ProxySandbox {
      proxy: any
      running = false
      constructor() {
        const fakeWindow = Object.create(null)
        const proxy = new Proxy(fakeWindow, {
          set: (target: any, p: string, value: any) => {
            if (this.running) {
              target[p] = value
            }
            return true
          },
          get(target: any, p: string): any {
            switch (p) {
              case 'window':
              case 'self':
              case 'globalThis':
                return proxy
            }
            if (
              !window.hasOwnProperty.call(target, p) &&
              window.hasOwnProperty(p)
            ) {
              // @ts-ignore
              const value = window[p]
              if (typeof value === 'function') return value.bind(window)
              return value
            }
            return target[p]
          },
          has() {
            return true
          },
        })
        this.proxy = proxy
      }
      active() {
        this.running = true
      }
      inactive() {
        this.running = false
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    预加载

    export const prefetch = async (app: IInternalAppInfo) => {
      requestIdleCallback(async () => {
        const { getExternalScripts, getExternalStyleSheets } = await importEntry(
          app.entry
        )
        requestIdleCallback(getExternalStyleSheets)
        requestIdleCallback(getExternalScripts)
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    4、qiankun 和 wujie 对比

    https://qiankun.umijs.org/zh
    https://wujie-micro.github.io/doc/guide/

    当谈到微前端中的 wujie 和 qiankun 时,它们都是目前国内比较流行的微前端框架。
    下面是它们的优缺点对比:

    wujie 的优点:

    1. 简单易用:wujie 提供了简单易用的 API,使得构建微前端应用变得更加容易。
    2. 灵活性:wujie 允许开发团队使用不同的技术栈来构建独立的微前端应用,从而提供了更大的灵活性。
    3. 性能优化:wujie 通过按需加载和缓存机制来优化性能,减少了加载时间和带宽消耗。

    wujie 的缺点:

    1. 社区支持相对较少:相对于 qiankun,wujie 的社区支持相对较少,这可能会导致在解决问题时遇到一些困难。
    2. 文档相对不完善:wujie 的文档相对不完善,可能需要开发者花费更多的时间来理解和使用该框架。

    qiankun 的优点:

    1. 成熟稳定:qiankun 是一个成熟稳定的微前端框架,已经在许多生产环境中得到验证。
    2. 强大的生态系统:qiankun 有一个庞大的社区支持和活跃的开发者社区,提供了丰富的插件和工具,使得开发更加便捷。
    3. 性能优化:qiankun 通过资源预加载、按需加载和缓存机制来优化性能,提供了更好的用户体验。

    qiankun 的缺点:

    1. 学习曲线较陡峭:相对于 wujie,qiankun 的学习曲线可能较陡峭,需要开发者花费一些时间来学习和理解该框架。
    2. 对主应用的侵入性较大:qiankun 对主应用的侵入性较大,需要在主应用中进行一些特定的配置和修改。

    综上所述,wujie 和 qiankun 都有各自的优点和缺点。选择哪个框架取决于具体的项目需求、团队技术栈和开发者的偏好。

    拓展

    除了 wujie 和 qiankun,还有其他一些好用的微前端框架可供选择。以下是其中几个:

    1. Single-SPA:

    Single-SPA 是一个非常流行的微前端框架,它允许开发团队使用不同的技术栈来构建独立的前端应用,并将它们组合成一个整体的应用。它具有灵活性和可扩展性,并且有一个活跃的社区支持。

    2. Piral:

    Piral 是一个基于 Web Components 的微前端框架,它提供了一种模块化的方式来构建和组合前端应用。它具有良好的可扩展性和性能,并且支持多种技术栈。

    3. Luigi:

    Luigi 是一个用于构建微前端应用的开源框架,它提供了一种可插拔的方式来组合和集成不同的前端应用。它具有良好的可扩展性和灵活性,并且支持多种技术栈。

    4. Mosaic:

    Mosaic 是一个基于 Web Components 的微前端框架,它提供了一种模块化的方式来构建和组合前端应用。它具有良好的可扩展性和性能,并且支持多种技术栈。

    这些框架都有各自的特点和优势,选择适合自己项目需求和团队技术栈的框架是很重要的。建议在评估这些框架时,考虑其文档质量、社区支持、可扩展性和性能等因素。

  • 相关阅读:
    java导出word表格 行列合并
    【软考】系统集成项目管理工程师(九)项目成本管理
    main函数中两个参数的作用
    创建对象在Heap堆区中如何分配内存
    软件质量保护与测试(第2版)学习总结第十三章 集成测试
    对卷积的一点具象化理解
    翻译软件排行榜-免费翻译软件排行榜-翻译软件推荐排行榜
    go的iris框架进行接收post请求参数获取与axios(vue)的数据传入
    课程设计-天天象棋作弊软件判别
    文举论金:黄金原油全面走势分析策略独家指导
  • 原文地址:https://blog.csdn.net/m0_37577465/article/details/132422627