【25-qiankun】手写微前端qiankun集成react&vue子项目-bysking

一、背景

通过实践,理解微前端的核心逻辑

二、概念

微前端涉及主应用和子应用,能很好的隔离和自由组合多个子应用形成完整的产品形态,子应用能独立开发部署维护升级,自由选择技术react/vue。

  • 主应用: qiankun-base, 负责管理子应用列表,匹配路由后选择对子应用进行加载,卸载等操作
  • 子应用: qianku-react,基于react搭建的乾坤子应用
  • 子应用: qiankun-vue, 基于vue搭建的乾坤子应用

子应用的接入要求:

  • 导出三个必要的生命周期钩子函数
  • bootstrap 渲染之前
  • mount 渲染函数
  • unmount 卸载函数
  • 生命周期函数必须返回promise

三、实践

3.1 新建子应用qianku-react

新建一个react项目,部署在http://localhost:40000/ 改造以支持主应用的调用,改动前的react项目入口逻辑是直接就渲染了:

js 复制代码
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';


ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.querySelector('#root')
);

现在为了能让微前端进行自由管控渲染的执行,需要将之前的逻辑封装成一个函数

js 复制代码
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

function render(props) {
  const { container } = props;

  // 主应用渲染的时候,会传入一个容器id进来,有的话要优先使用,没有的话,就默认子应用独立渲染
  const containerDom = container
    ? container.querySelector('#root')
    : document.querySelector('#root');

  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    containerDom
  );
}

// 如果是在乾坤里面加载,则子应用应该跳过默认的加载逻辑,让主应用来控制当前应用的加载(匹配到子应用的路由标识)
if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}

/** 暂时保持空函数 */
export async function bootstrap() {}

/**
 * 挂着子应用的函数入口
 * @param {*} props
 */
export async function mount(props) {
  render(props);
}

/**
 * 卸载子应用的函数
 * @param {*} props
 */
export async function unmount(props) {
  const { container } = props;
  const containerDom = container
    ? container.querySelector('#root')
    : document.querySelector('#root');

  ReactDOM.unmountComponentAtNode(containerDom);
}

3.2 新建子应用qiankun-vue

新建一个vue项目,部署在http://localhost:5000/

如上,默认的vue项目入口文件 main.js,渲染逻辑:

js 复制代码
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import './public-path';

Vue.config.productionTip = false;

new Vue({
  router,
  render: (h) => h(App),
}).$mount('#app');

改造后

js 复制代码
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import './public-path';

Vue.config.productionTip = false;

let instance = null;
function render(props) {
  const { container } = props;

  // 主应用渲染,会指定子应用的挂载位置container,需要兼容
  const containerDom = container
    ? container.querySelector('#app')
    : document.querySelector('#app');

  // 这里需要保存一下实例,方便后续对这个实例进行销毁操作
  instance = new Vue({
    router,
    render: (h) => h(App),
  }).$mount(containerDom);
}

// 如果子应用独立运行,则手动调用渲染, 否则如果是qiankun使用到了,则会动态注入路径,解决webpack的资源加载问题
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
} else {
  mount({});
}

// 根据 qiankun 的协议需要导出 bootstrap/mount/unmount
export async function bootstrap(props) {}

/**
 * 子应用加载函数,主应用会在合适的时机调用(匹配到路由规则时)
 * @param {*} props
 */
export async function mount(props) {
  render(props);
}

/**
 * 卸载函数
 * @param {*} props
 */
export async function unmount(props) {
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
}

3.3 新建主应用qiankun-base

现在我们把上面的两个子应用分别运行起来, vue子应用运行在40000端口,react子应用运行在50000端口,现在我们直接新建主应用,运行在3000端口。

  • 在主应用的入口处,我们能维护一份子应用列表的配置
js 复制代码
const apps = [
  {
    name: 'vueApp',
    entry: 'http://localhost:40000/',
    container: '#vue', // 要渲染到的容器名id
    activeRule: '/vue' // 我们自定义通过哪一个路由来激活这个子应用的渲染

  },
  {
    name: 'reactApp', 
    entry: 'http://localhost:50000/',
    container: '#react',
    activeRule: '/react',  // 我们自定义通过哪一个路由来激活这个子应用的渲染
  }
];
  • 在主应用中需要留位置给子应用渲染
js 复制代码
<template>
  <div>
    <el-menu :router="true" mode="horizontal">
      <el-menu-item index="/">首页</el-menu-item>

      <!-- 微前端主应用中 -->
      <el-menu-item index="/vue">vue应用</el-menu-item>
      <el-menu-item index="/react">react应用</el-menu-item>
    </el-menu>


    <!-- 主应用自己的路由渲染 -->
    <router-view v-show="$route.name"></router-view>

    <!-- 微前端主应用为 vue 和 react 两个子应用渲染预留位置 -->
    <div id="vue"></div>
    <div id="react"></div>
  </div>
</template>
  • 现在到了核心流程环节了 核心思路是:
  • 监听路由变化
  • 匹配路由
  • 根据路由去处理当前子应用的加载和渲染,和上一个子应用的卸载

那么我们如何用代码实现上面的逻辑呢?请接着往看下文进行分析。新建如下的目录结构

模块的使用:模块导出两个函数,registerMicroApps 用于注册子应用,start用于启动微前端渲染流程,使用如下

js 复制代码
import { registerMicroApps, start } from './micro-base/index';

const apps = [
  {
    name: 'vueApp',
    entry: 'http://localhost:40000/',
    container: '#vue', // 要渲染到的容器名id
    activeRule: '/vue' // 我们自定义通过哪一个路由来激活这个子应用的渲染

  },
  {
    name: 'reactApp', 
    entry: 'http://localhost:50000/',
    container: '#react',
    activeRule: '/react',  // 我们自定义通过哪一个路由来激活这个子应用的渲染
  }
];

// 根据应用列表,注册应用
registerMicroApps(apps); 

// 启动微前端渲染流程
start();

回忆一下,微前端要做的事:1)监视路由变化,2)根据路由匹配子应用 3)加载子应用 4)渲染子应用

  • index.js
js 复制代码
import { handleRouter } from './handleRouter';
import { rewriteRouter } from './rewriteRouter';
let _qiankunSubApps = []; // 记录子应用列表

export const getApps = () => _qiankunSubApps; // 提供获取接口

export const registerMicroApps = (apps) => {
    
  // 初始化的时候,存一份apps配置
  _qiankunSubApps = apps;
};

export const start = () => {
  // 重写路由,因为我们要拦截监听全部的路由变化,根据路由去筛选让哪一个app进行加载以及卸载
  rewriteRouter();

  //默认执行一次初始执行的子应用渲染逻辑
  handleRouter();
};

现在我们研究下rewriteRouter的逻辑,qiankun-base/src/micro-base/rewriteRouter.js

js 复制代码
import { handleRouter } from './handleRouter';

// 因为假设已经渲染子应用了,切换应用的时候,要对上一个应用进行卸载,对当前应用进行加载,所以要维护上一次的路由
let prevRoute = '';
let nextRoute = window.location.pathname;
export const getPrevRoute = () => prevRoute;
export const getNextRoute = () => nextRoute;

window.getNextRoute = getNextRoute;
window.getPrevRoute = getPrevRoute;

export const rewriteRouter = () => {
  // 拦截pushState,给我们一个机会在路由切换前后做一些事情
  const rawPushState = window.history.pushState;
  window.history.pushState = (...args) => {
    // 导航前
    prevRoute = window.location.pathname;
    rawPushState.apply(window.history, args); // 真正的改变历史记录
    nextRoute = window.location.pathname;
    // 导航后
    handleRouter();
  };

  // 拦截replaceState 也要做同样的事情,核心是为了正确追踪上一个路由和当前路由
  const rawReplaceState = window.history.replaceState;
  window.history.replaceState = (...args) => {
    prevRoute = window.location.pathname;
    rawReplaceState.apply(window.history, args);
    nextRoute = window.location.pathname;
    handleRouter();
  };

  // 监听popstate事件,这个事件触发的时候,说明路由发生了切换
  window.addEventListener('popstate', () => {
    // popstate 触发的时候,路由已经完成导航
    prevRoute = nextRoute; 
    nextRoute = window.location.pathname;
    handleRouter();
  });
};

上面的逻辑,我们能正确最终路由的变化了,然后我们根据变化前后的路由来处理微前端的加载和卸载逻辑,调用handleRouter这个方法,qiankun-base/src/micro-base/handleRouter.js

js 复制代码
import { importHTML } from './importHtml';
import { getPrevRoute, getNextRoute } from './rewriteRouter';
import { getApps } from './index';

export const handleRouter = async () => {
  // 根据路由获取上一个应用和下一个即将加载的应用,切换之前的应用需要卸载,当前应用需要加载
  const apps = getApps();
  const prevApp = apps.find((item) => {
    return getPrevRoute().startsWith(item.activeRule);
  });
  if (prevApp) {
    await unmount(prevApp); // 如果有上一个应用,卸载上一个路由应用
  }

  // 获取下一个路由应用配置
  const app = apps.find((item) => getNextRoute().startsWith(item.activeRule));
  if (!app) return;

  // 注入微前端的配置全局环境变量
  window.__POWERED_BY_QIANKUN__ = true;
  window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = app.entry;

  // 加载子应用,通过app的入口文件,获取子应用的模版文件和js文件
  const { template, execScripts } = await importHTML(app.entry);

  // 将子应用的模版文件挂载到主应用的容器中
  const container = document.querySelector(app.container);
  container.appendChild(template);

  // 获取并保存子应用导出的微前端的生命周期函数
  const appExports = await execScripts();
  app.bootstrap = appExports.bootstrap;
  app.mount = appExports.mount;
  app.unmount = appExports.unmount;

  // 启动子应用的加载逻辑
  await bootstrap(app);
  await mount(app);
};

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

上面的代码,处理了根据路由变化,找到对应的子应用并加载,整个流程其实已经完了,现在只需要看一下细节,比如importHTML这个函数做了些什么qiankun-base/src/micro-base/importHtml.js

js 复制代码
import { loadResource } from './LoadResource';
export const importHTML = async (url) => {
  const html = await loadResource(url); // 根据子应用的入口,获取子应用的模版文件
  const template = document.createElement('div');
  template.innerHTML = html;

  // 获取模版文件中的所有 script 标签
  const scripts = template.querySelectorAll('script');
  // 获取所有 script 标签的代码
  function getExternalScripts() {
    return Promise.all(
      Array.from(scripts).map((script) => {
        const src = script.getAttribute('src');
        if (!src) {
          return Promise.resolve(script.innerHTML);
        } else {
          return loadResource(src.startsWith('http') ? src : `${url}${src}`);
        }
      })
    );
  }

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

    //手动构造一个 CommonJS 模块环境
    const module = { exports: {} };
    const exports = module.exports;
    scripts.forEach((code) => {
      // eval执行的代码可以访问外部变量
      eval(code);
    });
    return module.exports;
  }

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

最后还有一个资源加载函数 qiankun-base/src/micro-base/LoadResource.js

js 复制代码
export const loadResource = (url) => fetch(url).then((res) => res.text());

如上,就是整个微前端的核心逻辑。

todo:

  • js沙箱隔离机制
  • 样式隔离
相关推荐
xhuarui3 小时前
React-Router-dom的二次封装,增加路由守卫等功能
前端·react.js
大鸡腿最好吃5 小时前
打造高性能的react
react.js
小满zs14 小时前
React第三十章(css原子化)
前端·react.js
sorryhc16 小时前
解读Ant Design X API流式响应和流式渲染的原理
前端·react.js·ai 编程
1024小神16 小时前
vue/react前端项目打包的时候加上时间,防止后端扯皮
前端·vue.js·react.js
不能只会打代码18 小时前
六十天前端强化训练之第十七天React Hooks 入门:useState 深度解析
前端·javascript·react.js
鱼樱前端20 小时前
React完整学习指南:从入门到精通(从根class->本hooks)16-19完整对比
前端·react.js
juenanfeng20 小时前
基于懒加载的 antd4 大数据量表格实践
react.js