微前端qiankun 监听子应用路由变化并加载子应用的原理解析

qiankun 内部通过监听浏览器的路由变化事件(如 popstatehashchange)来管理子应用的激活和加载。以下是 qiankun 如何实现这一功能的详细解释:

先了解下一些基本知识点:

popstatehashchange 是浏览器中用于监听 URL 变化的两个不同事件。它们分别用于不同的路由模式(history 模式和 hash 模式)。以下是这两个事件的详细解释和使用场景:

popstate 事件

用途

  • 主要用于监听浏览器历史记录的变化,通常在使用 history 模式时使用。
  • 当用户点击浏览器的前进或后退按钮,或者通过 JavaScript 调用 history.pushStatehistory.replaceState 时触发。

触发条件

  • 用户点击浏览器的前进或后退按钮。
  • 调用 history.pushStatehistory.replaceState 方法。

示例代码

javascript 复制代码
window.addEventListener('popstate', (event) => {
  console.log('popstate event triggered');
  console.log('Current URL:', window.location.href);
  // 处理路由变化逻辑
  checkActiveApps();
});

hashchange 事件

用途

  • 主要用于监听 URL 的 hash 部分的变化,通常在使用 hash 模式时使用。
  • 当 URL 的 hash 部分发生变化时触发。

触发条件

  • URL 的 hash 部分发生变化。
  • 例如,从 http://example.com/#/app1 变为 http://example.com/#/app2

示例代码

javascript 复制代码
window.addEventListener('hashchange', (event) => {
  console.log('hashchange event triggered');
  console.log('Current URL:', window.location.href);
  // 处理路由变化逻辑
  checkActiveApps();
});

startsWith(rule)是 JavaScript 字符串方法,用于检查一个字符串是否以指定的子字符串开头。它返回一个布尔值:如果字符串以指定的子字符串开头,则返回 true,否则返回 false。在 qiankun 中,startsWith(rule) 通常用于匹配当前路由路径和子应用的 activeRule。以下是一个简化的示例,展示如何在 qiankun 中使用 startsWith(rule) 来检查路由是否匹配子应用的 activeRule

基本用法

ini 复制代码
const str = "Hello, world!";
const rule = "Hello";

console.log(str.startsWith(rule)); // true

实现细节

  1. 注册事件监听器

    • qiankun 在启动时会注册 popstatehashchange 事件监听器。
    • 这些监听器会捕获路由变化,并根据 activeRule 来决定哪些子应用需要被激活。
  2. 检查 activeRule

    • 当路由变化时,qiankun 会遍历所有注册的子应用配置,检查当前路由是否匹配任何子应用的 activeRule
    • 如果匹配,则激活相应的子应用。
  3. 激活子应用

    • 激活子应用包括加载子应用的资源、调用子应用的生命周期钩子(如 bootstrapmount)等。

以下是 qiankun 内部监听路由变化的简化代码示例:

scss 复制代码
let activeApps = [];
let registeredApps = [];

function checkActiveApps() {
  const currentPath = window.location.pathname;

  registeredApps.forEach(app => {
    if (matchPath(currentPath, app.activeRule)) {
      if (!activeApps.includes(app)) {
        activateApp(app);
        activeApps.push(app);
      }
    } else {
      if (activeApps.includes(app)) {
        deactivateApp(app);
        activeApps = activeApps.filter(a => a !== app);
      }
    }
  });
}

function matchPath(path, rule) {
  // 使用 startsWith 检查路径是否以规则开头
  return path.startsWith(rule);
}

function activateApp(app) {
  // 加载子应用资源
  loadApp(app);
  // 调用子应用的 bootstrap 和 mount 生命周期钩子
  app.bootstrap();
  app.mount();
}

function deactivateApp(app) {
  // 调用子应用的 unmount 生命周期钩子
  app.unmount();
}

function loadApp(app) {
  fetch(app.entry)
    .then(response => response.text())
    .then(html => {
      const container = document.querySelector(app.container);
      container.innerHTML = html;
    });
}

function registerMicroApps(apps) {
  registeredApps = apps;
}

function start() {
  // 注册事件监听器
  window.addEventListener('popstate', checkActiveApps);
  window.addEventListener('hashchange', checkActiveApps);

  // 初始检查
  checkActiveApps();
}

// 示例注册子应用
registerMicroApps([
  {
    name: 'app1',
    entry: 'https://server1.com/app1',
    container: '#app1',
    activeRule: '/app1',
    bootstrap: () => console.log('app1 bootstraped'),
    mount: () => console.log('app1 mounted'),
    unmount: () => console.log('app1 unmounted'),
  },
  {
    name: 'app2',
    entry: 'https://server2.com/app2',
    container: '#app2',
    activeRule: '/app2',
    bootstrap: () => console.log('app2 bootstraped'),
    mount: () => console.log('app2 mounted'),
    unmount: () => console.log('app2 unmounted'),
  },
]);

start();

详细步骤

  1. 注册子应用

    • 使用 registerMicroApps 注册子应用配置。
    • 每个子应用的配置包括 nameentrycontaineractiveRule
    javascript 复制代码
    registerMicroApps([
      {
        name: 'app1',
        entry: 'https://server1.com/app1',
        container: '#app1',
        activeRule: '/app1',
        bootstrap: () => console.log('app1 bootstraped'),
        mount: () => console.log('app1 mounted'),
        unmount: () => console.log('app1 unmounted'),
      },
      {
        name: 'app2',
        entry: 'https://server2.com/app2',
        container: '#app2',
        activeRule: '/app2',
        bootstrap: () => console.log('app2 bootstraped'),
        mount: () => console.log('app2 mounted'),
        unmount: () => console.log('app2 unmounted'),
      },
    ]);
  2. 启动 qiankun

    • 调用 start 函数启动 qiankun
    • start 函数会注册 popstatehashchange 事件监听器,并进行初始检查。
    scss 复制代码
    function start() {
      // 注册事件监听器
      window.addEventListener('popstate', checkActiveApps);
      window.addEventListener('hashchange', checkActiveApps);
    
      // 初始检查
      checkActiveApps();
    }
    
    start();
  3. 检查激活的子应用

    • 当路由变化时,checkActiveApps 函数会被调用。
    • 该函数会遍历所有注册的子应用配置,检查当前路由是否匹配任何子应用的 activeRule
    ini 复制代码
    function checkActiveApps() {
      const currentPath = window.location.pathname;
    
      registeredApps.forEach(app => {
        if (matchPath(currentPath, app.activeRule)) {
          if (!activeApps.includes(app)) {
            activateApp(app);
            activeApps.push(app);
          }
        } else {
          if (activeApps.includes(app)) {
            deactivateApp(app);
            activeApps = activeApps.filter(a => a !== app);
          }
        }
      });
    }
  4. 路径匹配

    • matchPath 函数使用 startsWith(rule) 来检查当前路径是否以 activeRule 开头。
    javascript 复制代码
    function matchPath(path, rule) {
      // 使用 startsWith 检查路径是否以规则开头
      return path.startsWith(rule);
    }
  5. 激活子应用

    • 如果路由匹配某个子应用的 activeRuleactivateApp 函数会被调用。
    • 该函数会加载子应用的资源,并调用子应用的 bootstrapmount 生命周期钩子。
    scss 复制代码
    function activateApp(app) {
      // 加载子应用资源
      loadApp(app);
      // 调用子应用的 bootstrap 和 mount 生命周期钩子
      app.bootstrap();
      app.mount();
    }
  6. 加载子应用资源

    • loadApp 函数会根据子应用的 entry 配置加载子应用的资源。
    • 例如,加载 https://server1.com/app1/index.html 及其相关资源。
    ini 复制代码
    function loadApp(app) {
      fetch(app.entry)
        .then(response => response.text())
        .then(html => {
          const container = document.querySelector(app.container);
          container.innerHTML = html;
        });
    }

下面是对 fetch(app.entry).then(response => response.text()).then(html => { ... }) 这段代码的详细解析。

代码解析

ini 复制代码
fetch(app.entry)
  .then(response => response.text())
  .then(html => {
    const container = document.querySelector(app.container);
    container.innerHTML = html;
  });

详细步骤

  1. 发起 HTTP 请求

    • fetch(app.entry):使用 fetch API 发起一个 HTTP GET 请求,app.entry 是子应用的入口地址(例如 //localhost:8080)。
  2. 处理响应

    • .then(response => response.text()):将响应体解析为文本格式。response.text() 返回一个 Promise,解析后的结果是响应的文本内容。
  3. 处理 HTML 内容

    • .then(html => { ... }):将解析后的 HTML 内容传递给回调函数。
    • const container = document.querySelector(app.container):使用 document.querySelector 选择子应用的挂载容器。app.container 是子应用挂载的 DOM 节点选择器(例如 #container)。
    • container.innerHTML = html:将解析后的 HTML 内容设置为容器的 innerHTML,从而将子应用的 HTML 渲染到指定的容器中。

详细流程图

ini 复制代码
+-------------------+
| fetch(app.entry)  |
| - 发起 HTTP 请求  |
+-------------------+
          |
          v
+-------------------+
| response =>       |
| response.text()   |
| - 解析响应为文本  |
+-------------------+
          |
          v
+-------------------+
| html => {         |
|   const container = |
|   document.querySelector(app.container); |
|   container.innerHTML = html; |
| - 渲染 HTML 到容器 |
+-------------------+

示例

假设 app.entry//localhost:8080app.container#container,并且 //localhost:8080 返回以下 HTML 内容:

xml 复制代码
<!DOCTYPE html>
<html>
<head>
  <title>App 1</title>
</head>
<body>
  <h1>Hello from App 1</h1>
</body>
</html>

代码执行流程

  1. 发起 HTTP 请求

    scss 复制代码
    fetch('//localhost:8080')
  2. 处理响应

    ini 复制代码
    .then(response => response.text())
    • 假设响应体是上述 HTML 内容。
  3. 处理 HTML 内容

    ini 复制代码
    .then(html => {
      const container = document.querySelector('#container');
      container.innerHTML = html;
    });
    • container#container 元素。
    • container.innerHTML = html 将上述 HTML 内容设置为 #container 的内容。

可能出现的问题及其解决方案

  1. 网络请求失败

    • 问题fetch 请求失败,例如网络问题或服务器错误。

    • 解决方案

      • 使用 catch 处理错误。

      • 示例:

        ini 复制代码
        fetch(app.entry)
          .then(response => response.text())
          .then(html => {
            const container = document.querySelector(app.container);
            container.innerHTML = html;
          })
          .catch(error => {
            console.error('Fetch error:', error);
          });
  2. 响应格式不正确

    • 问题:响应体不是预期的 HTML 内容。

    • 解决方案

      • 检查响应状态码。

      • 示例:

        ini 复制代码
        fetch(app.entry)
          .then(response => {
            if (!response.ok) {
              throw new Error('Network response was not ok ' + response.statusText);
            }
            return response.text();
          })
          .then(html => {
            const container = document.querySelector(app.container);
            container.innerHTML = html;
          })
          .catch(error => {
            console.error('Fetch error:', error);
          });
  3. 容器选择器错误

    • 问题app.container 选择器不正确或容器不存在。

    • 解决方案

      • 确保选择器正确且容器存在。

      • 示例:

        ini 复制代码
        fetch(app.entry)
          .then(response => response.text())
          .then(html => {
            const container = document.querySelector(app.container);
            if (container) {
              container.innerHTML = html;
            } else {
              console.error('Container not found:', app.container);
            }
          })
          .catch(error => {
            console.error('Fetch error:', error);
          });
  4. 安全性问题

    • 问题 :直接设置 innerHTML 可能导致 XSS 攻击。

    • 解决方案

      • 使用安全的模板引擎或手动处理 HTML 内容。

      • 示例:

        ini 复制代码
        fetch(app.entry)
          .then(response => response.text())
          .then(html => {
            const container = document.querySelector(app.container);
            if (container) {
              // 使用安全的模板引擎或手动处理 HTML
              container.innerHTML = sanitizeHTML(html);
            } else {
              console.error('Container not found:', app.container);
            }
          })
          .catch(error => {
            console.error('Fetch error:', error);
          });
        
        function sanitizeHTML(input) {
          const div = document.createElement('div');
          div.textContent = input;
          return div.innerHTML;
        }

通过上述详细解析,你可以更好地理解 fetch(app.entry).then(response => response.text()).then(html => { ... }) 的工作原理和代码实现。

  1. 卸载子应用

    • 如果路由不再匹配某个子应用的 activeRuledeactivateApp 函数会被调用。
    • 该函数会调用子应用的 unmount 生命周期钩子。
    scss 复制代码
    function deactivateApp(app) {
      // 调用子应用的 unmount 生命周期钩子
      app.unmount();
    }

总结

  • startsWith(rule) :用于检查字符串是否以指定的子字符串开头,返回布尔值。
  • qiankun 中的使用 :在 matchPath 函数中使用 startsWith(rule) 来检查当前路由路径是否以子应用的 activeRule 开头。
  • 路由变化监听 :通过监听 popstatehashchange 事件,qiankun 能够自动检查路由变化,并根据 activeRule 动态激活和加载子应用。

通过这种方式,qiankun 能够有效地管理子应用的激活和加载,确保在路由变化时正确地加载和卸载子应用。

相关推荐
夜斗(dou)14 分钟前
css如何隐藏一个元素
前端·css
小远披荆斩棘40 分钟前
Mac中配置Node.js前端vscode环境(第二期)
前端·macos·node.js
m0_748251351 小时前
解决 Tomcat 跨域问题 - Tomcat 配置静态文件和 Java Web 服务(Spring MVC Springboot)同时允许跨域
java·前端·spring
孤水寒月1 小时前
uniapp下的手势事件
前端·javascript·uni-app
程序员_三木1 小时前
使用 Three.js 创建动态粒子效果
开发语言·前端·javascript·数码相机·webgl·three.js
时间sk1 小时前
CSS——17. nth-child选择器2
前端·css
时间sk1 小时前
CSS——16. nth—child序列选择器1
前端·css
夜斗(dou)1 小时前
CSS Grid 布局示例(基本布局+代码属性描述)
前端·css
远洋录1 小时前
Tailwind CSS 实战:深色模式设计与实现
前端·人工智能·react
前端熊猫2 小时前
css实现垂直文本
前端·css