前端微应用-乾坤(qiankun)原理分析-single-spa

qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛 的构建一个生产可用 微前端架构系统。

本文宗旨是帮大家理解 qiankun 的原理,从而更好的使用 qiankun

single-spa

single-spa 可以把他看作状态管理机 ,他可以根据规则 匹配路由,启动对应的应用,并且对应的应用还有生命周期bootstrap, mount, unmount、update

使用如下:

js 复制代码
// single-spa-config.js
import { registerApplication, start } from "single-spa";

// 注册 app2
registerApplication(
  "app2",  // APP 名称 需要保证唯一
  () => import("src/app2/main.js"), // 入口文件
  (location) => location.pathname.startsWith("/app2"), // 规则
  { some: "value" },
);

// 注册 app1
registerApplication({
  name: "app1",
  app: () => import("src/app1/main.js"),
  activeWhen: "/app1", // 规则
  customProps: {
    some: "value",
  },
});

start();
  • registerApplication入参数app是需要返回3个状态如下:
js 复制代码
const application = {
  bootstrap: () => Promise.resolve(), //bootstrap function 初始化 loading
  mount: () => Promise.resolve(), //mount function // 渲染
  unmount: () => Promise.resolve(), //unmount function // 卸载
};
registerApplication("applicationName", application, '/app1');

// 在或者可以是多个数组形式

const application = {
  bootstrap: [() => Promise.resolve(), () => Promise.resolve()], //bootstrap function 初始化 loading
  mount: [() => Promise.resolve(), () => Promise.resolve()], //mount function // 渲染
  unmount: [() => Promise.resolve(), () => Promise.resolve()], //unmount function // 卸载
};
registerApplication("applicationName", application, '/app1');

匹配路由的相关原理代码如下:

js 复制代码
function sanitizeActiveWhen(activeWhen) {
    //1. 确保 activeWhen 是数组
  let activeWhenArray = Array.isArray(activeWhen) ? activeWhen : [activeWhen];
  // 2. 如果本身你 `activeWhen` 是函数就直接用你的函数返回值规则判断
  activeWhenArray = activeWhenArray.map((activeWhenOrPath) =>
    typeof activeWhenOrPath === "function"
      ? activeWhenOrPath
      : pathToActiveWhen(activeWhenOrPath) // 3. 如果是字符串就转成函数,`pathToActiveWhen`返回了一个函数
  );

  // 6. 调用函数即可判断是否匹配到路由
  return (location) =>
    activeWhenArray.some((activeWhen) => activeWhen(location));
}


export function pathToActiveWhen(path, exactMatch) {
// toDynamicPathValidatorRegex 方法先略过,可以理解是根据字符串生产正则表达式的
  const regex = toDynamicPathValidatorRegex(path, exactMatch);

  return (location) => {
    // compatible with IE10
    let origin = location.origin;
    if (!origin) {
      origin = `${location.protocol}//${location.host}`;
    }
    // 4. 去掉域名、以及参数只保留路由 例如:https://www.baidu.com/s?wd=123&ie=utf-8 --> /s
    const route = location.href
      .replace(origin, "")
      .replace(location.search, "")
      .split("?")[0];
    return regex.test(route); // 5. 判断路由是否匹配规则
  };
}

生成正则的toDynamicPathValidatorRegex表达式方法如下:

js 复制代码
function toDynamicPathValidatorRegex(path, exactMatch) {
  let lastIndex = 0,
    inDynamic = false,
    regexStr = "^";

  if (path[0] !== "/") {
    path = "/" + path;
  }

  for (let charIndex = 0; charIndex < path.length; charIndex++) {
    const char = path[charIndex];
    const startOfDynamic = !inDynamic && char === ":";
    const endOfDynamic = inDynamic && char === "/";
    if (startOfDynamic || endOfDynamic) {
      appendToRegex(charIndex);
    }
  }

  appendToRegex(path.length);
  return new RegExp(regexStr, "i");

  function appendToRegex(index) {
    const anyCharMaybeTrailingSlashRegex = "[^/]+/?";
    const commonStringSubPath = escapeStrRegex(path.slice(lastIndex, index));

    regexStr += inDynamic
      ? anyCharMaybeTrailingSlashRegex
      : commonStringSubPath;

    if (index === path.length) {
      if (inDynamic) {
        if (exactMatch) {
          // Ensure exact match paths that end in a dynamic portion don't match
          // urls with characters after a slash after the dynamic portion.
          regexStr += "$";
        }
      } else {
        // For exact matches, expect no more characters. Otherwise, allow
        // any characters.
        const suffix = exactMatch ? "" : ".*";

        regexStr =
          // use charAt instead as we could not use es6 method endsWith
          regexStr.charAt(regexStr.length - 1) === "/"
            ? `${regexStr}${suffix}$`
            : `${regexStr}(/${suffix})?(#.*)?$`;
      }
    }

    inDynamic = !inDynamic;
    lastIndex = index;
  }

  function escapeStrRegex(str) {
    // borrowed from https://github.com/sindresorhus/escape-string-regexp/blob/master/index.js
    return str.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
  }
}

toDynamicPathValidatorRegex方法可以直接在控制台运行去调试如下:

其实咱们一个微应用尽量是一个前缀开头的比如/app*、/search*这么用的话也比较规范。

以下是single-spa的路由监听相关的处理

js 复制代码
export function patchHistoryApi(opts) {
  if (historyApiIsPatched) {
    throw Error(
      formatErrorMessage(
        43,
        __DEV__ &&
          `single-spa: patchHistoryApi() was called after the history api was already patched.`
      )
    );
  }

  // True by default, as a performance optimization that reduces
  // the number of extraneous popstate events
  urlRerouteOnly =
    opts && opts.hasOwnProperty("urlRerouteOnly") ? opts.urlRerouteOnly : true;

  historyApiIsPatched = true;

  originalReplaceState = window.history.replaceState;

  // We will trigger an app change for any routing events.
  window.addEventListener("hashchange", urlReroute);
  window.addEventListener("popstate", urlReroute);

  // Monkeypatch addEventListener so that we can ensure correct timing
  const originalAddEventListener = window.addEventListener;
  const originalRemoveEventListener = window.removeEventListener;
  window.addEventListener = function (eventName, fn) {
    if (typeof fn === "function") {
      if (
        routingEventsListeningTo.indexOf(eventName) >= 0 &&
        !find(capturedEventListeners[eventName], (listener) => listener === fn)
      ) {
        capturedEventListeners[eventName].push(fn);
        return;
      }
    }

    return originalAddEventListener.apply(this, arguments);
  };

  window.removeEventListener = function (eventName, listenerFn) {
    if (typeof listenerFn === "function") {
      if (routingEventsListeningTo.indexOf(eventName) >= 0) {
        capturedEventListeners[eventName] = capturedEventListeners[
          eventName
        ].filter((fn) => fn !== listenerFn);
      }
    }

    return originalRemoveEventListener.apply(this, arguments);
  };

  window.history.pushState = patchedUpdateState(
    window.history.pushState,
    "pushState"
  );
  window.history.replaceState = patchedUpdateState(
    originalReplaceState,
    "replaceState"
  );
}

运转流程图如下:

single-spa 总结

可以从分享中看到single-spa并没有处理你的应用要怎么挂载到界面的问题(都是执行了对应的mount),你可以通过customProps传进来一个container来指定挂载到那个位置下。

Q: 那么对于vue、react框架来说应该怎么让single-spaloadApp呢?

A: 需要把vue、react整体通过webpack打包成一个lib并且导出对应的生命周期就行了。

比如:

js 复制代码
// 加载子应用的 js 脚本
async function loadScript(url) {
  return new Promise((resole, reject) => {
    let script = document.createElement("script");
    script.src = url;
    script.onload = resole; // 加载成功
    script.onerror = reject; //加载失败
    document.head.appendChild(script); //把script放在html的head标签里
  });
}

// 注册应用  
registerApplication(
  "app1",
  async () => {
    // 动态创建script标签 把这个模块引入进来
    await loadScript("./app1/bundle.js");
    return (window as any).singleApp1; //bootstrap mount unmount
  },
  '/app1',
  {
    container: "#single-spa-layout",
  }
);

// 开启应用
start();

Q: 如果single-spa没有匹配到路由会怎么样?

A: 需要你自己处理,比如:新注册一个404的微应用

js 复制代码
// 注册应用  
registerApplication(
  "app404",
  async () => {
    // 动态创建script标签 把这个模块引入进来
    await loadScript("./app404/bundle.js");
    return (window as any).singleApp1; //bootstrap mount unmount
  },
  () => {
    return true;
  },
  {
    container: "#single-spa-layout",
  }
);

第二种:

js 复制代码
// 通过single-spa 事件监听的方式处理
window.addEventListener('single-spa:no-app-change', () => {
  const mountedApps = singleSpa.getMountedApps();
  if (!mountedApps.length) {
    singleSpa.navigateToUrl('/default-route'); // 导航到默认路由
  }
});

Q:对比qiankun他们有什么区别?

A: qiankun是基于single-spa开发的,它提供了对框架更加便捷的挂载引入的方式,让你接入微应用像使用 iframe 一样简单。并且它内部做了css、js沙箱隔离,可以预加载子应用的等优化。

相关推荐
酷小洋26 分钟前
Ajax基础
前端·ajax·okhttp
小妖66627 分钟前
vue2 provide 后 inject 数据不是响应式的,不实时更新
java·服务器·前端
是代码侠呀1 小时前
HTTP 的发展史:从前端视角看网络协议的演进
前端·网络协议·http·开源·github·github star·github 加星
heyCHEEMS2 小时前
Vue 两种导航方式
前端·javascript·vue.js
我是哈哈hh2 小时前
【vue】vuex实现组件间数据共享 & vuex模块化编码 & 网络请求
前端·javascript·vue.js·前端框架·网络请求·vuex·模块化
想睡好2 小时前
圆角边框 盒子阴影 文字阴影
前端·css·html
fei_sun2 小时前
【数据结构】子串、前缀
java·前端·数据结构
zfyljx2 小时前
2048 html
前端·css·html
帮帮志2 小时前
如何启动vue项目及vue语法组件化不同标签应对的作用说明
前端·javascript·vue.js
森哥的歌2 小时前
深入解析Vue3中ref与reactive的区别及源码实现
前端·javascript·vue.js