手写qiankun微前端

52 阅读3分钟

qiankun微前端运行原理:

  1. 监听路由变化(两种路由方式:hash/history)
  2. 匹配子应用
  3. 加载子应用
  4. 渲染子应用

手写核心源代码

index.js

import { handelRouter } from './handle-router';
import { rewriteRouter } from './rewrite-router';

let _apps = [];

// 用于获取注册的子应用信息
export const getApps = () => _apps;

// 注册子应用
export const registerMicroApps = (app) => {
  _apps = app;
};

// 启动微前端
export const start = () => {
  // 微前端运行原理:
  // 1.监听路由变化(两种路由方式:hash/history,此项目用history方式)
  // 2.匹配子应用
  // 3.加载子应用
  // 4.渲染子应用
  rewriteRouter();
  handelRouter();
};

  1. 监听路由变化(以history模式为例)
rewrite-router.js

import { handelRouter } from './handle-router';

// 自己维护一个路由的历史记录。浏览器出于安全考虑没有提供历史记录
let prevRoute = ''; //上一个路由
let nextRoute = window.location.pathname; //下一个路由

export const getPreveRoute = () => prevRoute;
export const getNextRoute = () => nextRoute;

// 采用history路由示例
// hisotry.go ,history.back, history.forward 使用popstate事件监听
// pushState/replaceState 需要重写方法,添加路由变化处理逻辑
export const rewriteRouter = () => {
  // 不能通过window.popstate = function(){}去覆写,会覆盖以前的监听,而是通过如下方式,添加监听
  window.addEventListener('popstate', () => {
    // popstate触发的时候,路由已经完成导航了
    // 利用nextRoute和prevRoute记录路由变化
    prevRoute = nextRoute; //之前的
    nextRoute = window.location.pathname; // 最新的
    handelRouter();
  });

  // 先做备份
  const rowPushState = window.history.pushState;
  window.history.pushState = (...args) => {
    prevRoute = window.location.pathname;
    rowPushState.apply(window.history, args);
    nextRoute = window.location.pathname;

    handelRouter();
  };

  const rawReplaceState = window.history.replaceState;
  window.history.replaceState = (...args) => {
    prevRoute = window.location.pathname;
    rawReplaceState.apply(window.history, args);
    nextRoute = window.location.pathname;

    handelRouter();
  };
};

  1. 匹配子应用
handle-router

import { getApps } from '.';
import { importHTML } from './import-html';
import { getNextRoute, getPreveRoute } from './rewrite-router';

/**
 * 处理路由变化
 */
export const handelRouter = async function () {
  // 1.匹配子应用
  const apps = getApps();

  // 1.1获取当前的路由路由
  const prevRoute = apps.find((ele) =>
    getPreveRoute().startsWith(ele.activeRule)
  );

  if (prevRoute) {
    await unmount(prevRoute);
  }

  // 1.2 获取当前路由对应的子应用
  const app = apps.find((ele) => getNextRoute().startsWith(ele.activeRule));
  if (!app) {
    return;
  }

  // 2.加载子应用
  const { template, execScripts } = await importHTML(app.entry);
  const container = document.querySelector(app.container);
  container.appendChild(template);

  //  配置全局环境变量
  window.__POWERED_BY_QIANKUN__ = true;
  window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = app.entry + '/';

  // 执行子应用的代码,获取子应用导出的生命周期函数(bootstrap/mount/unmount)
  // 子应用打包出来的格式是umd格式。
  const appExport = await execScripts();
  app.bootstrap = appExport.bootstrap;
  app.mount = appExport.mount;
  app.unmount = appExport.unmount;

  await bootstrap(app);
  await mount(app);

  // const html = await fetch(app.entry).then((res) => {
  //   return res.text();
  // });
  // console.log(html);
  // const container = document.querySelector(app.container);
  // // 浏览器处于安全考虑,不会加载innerHTML中的js
  // // 故手动加载js
  // container.innerHTML = html;

  // 3.渲染子应用
};

async function bootstrap(app) {
  if (app.bootstrap) {
    await app.bootstrap();
  }
}
async function mount(app) {
  if (!app.mount) return;
  await app.mount({
    container: document.querySelector(app.container),
  });
}
async function unmount(app) {
  if (!app.unmount) return;
  await app.unmount();
}

  1. 加载并渲染子应用
import { fetchResource } from './fetch-resource';

// 注:qiankun采用的是 import-entry-html 库
export const importHTML = async (url) => {
  const html = await fetchResource(url);
  const template = document.createElement('div');
  template.innerHTML = html;

  const scripts = template.querySelectorAll('script');

  // 获取所有script标签的代码:
  // 返回的是文本组成的数组,形式:【代码,代码】
  function getExternalScripts() {
    return Promise.all(
      Array.from(scripts).map((script) => {
        const src = script.getAttribute('src');
        // 内联脚本,不需要src
        if (!src) {
          return Promise.resolve(script.innerHTML);
        } else {
          // 外部脚本,需要src
          // 分情况是因为src可能没有域名,只有pathname ,如<script src="/js/app.js"></script>
          return fetchResource(src.startsWith('http') ? src : `${url}/${src}`);
        }
      })
    );
  }

  // 获取并执行所有的script脚本代码
  // 用eval执行script中的代码
  async function execScripts() {
    const scripts = await getExternalScripts();

    //  手动构造一个commonjs的环境,以获取子应用导出的接口
    const module = { exports: {} };
    const exports = module.exports;

    scripts.forEach((script) => {
      eval(script);
    });

    // module.exports 就是子应用暴露出来的接口
    // 因为子应用打包出来的umd格式的内容,导出了生命周期函数:bootstrap/mount/unmount
    // 注:子应用构建后的umd代码大致形式如下:
    /*
      (function (global, factory) {
        typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
        typeof define === 'function' && define.amd ? define(['exports'], factory) :
        (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global['sub-app2'] = {}));
      })(window, (function () {
      // 内部的代码
        function bootstrap() { ... }
        function mount() { ... }
        function unmount() { ... }
        exports.bootstrap = bootstrap;
        exports.mount = mount;
        exports.unmount = unmount;
      }));
    */
    return module.exports;
  }

  return {
    template,
    getExternalScripts,
    execScripts,
  };
};

fetch-resource.js

export const fetchResource = (url) => fetch(url).then((res) => res.text());