微前端Qiankun的不佳实践--从试试到逝世

image

微前端Qiankun的不佳实践--从试试到逝世

image

微前端是什么

⼀种类似于微服务的架构,是⼀种由独⽴交付的多个前端应⽤组成整体的架构⻛格,将前端应⽤分解成⼀些更⼩、更简单的能够独⽴开发、测试、部署的应⽤,⽽在⽤户看来仍然是内聚的单个产品。

qiankun 是蚂蚁金服推出的微前端框架,基于 single-spa 进行二次开发。

特性:
  • 📦 基**于 single-spa **封装,提供了更加开箱即用的 API。
  • 📱 技**术栈无关,**任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
  • 💪 H**TML Entry 接入方式,**让你接入微应用像使用 iframe 一样简单。
  • 🛡​ 样**式隔离,**确保微应用之间样式互相不干扰。
  • 🧳 J**S 沙箱,**确保微应用之间 全局变量/事件 不冲突。
  • ⚡️ 资**源预加载,**在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
  • 🔌 u**mi 插件,**提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。
HTML Entry接入方式。

直接引用请求一个试试。

image

看看控制台输入的内容

image

  • assetPublicPath 静态资源的公共路径
  • execScripts 执行脚本的函数
  • getExternalScripts 获取所有外部脚本的函数,返回脚本路径
  • getExternalStyleSheets 获取所有外部样式的函数,返回样式文件的路径
  • template 处理后的 HTML 模板

image

获取到 templateexecScripts 这些信息以后,基于 template 生成 render](https://github.com/umijs/qiankun/blob/master/src/loader.ts#L296) [用于渲染子应用的页面。之后会根据需要生成沙盒,并将沙盒对象传给 execScripts 来获取子应用导出的声明周期函数。调用 single-spa 的 API 启动子应用。

这是大概的一个工作原理。

从0搓一个环境

简单概括主要的操作就两步:

  • 配置主应用基座
  • 配置子应用

这边分别搓三个不同的子应用。别问为什么不搓angular,本来想搓一个svelte的,因为懒放弃了。

image

image

基座应用

  1. 初始化基座应用
pnpm create vite ./  

一路看着选择

  1. 安装tdesign,撸个框框,安装qiankun
pnpm add tdesign-vue-next
pnpm add qiankun
  1. 改造入口,如果应用暂时没有的时候,先注释掉。
import { registerMicroApps, start } from 'qiankun';

// 注册子应用
registerMicroApps([
    {
        name: 'app-vue', // app name registered
        entry: '//localhost:5174',
        container: '#main',
        activeRule: '/sub-vue',
    },
    {
        name: 'app-react', // app name registered
        entry: '//localhost:5175',
        container: '#main',
        activeRule: '/sub-react',
    },
    {
        name: 'app-html', // app name registered
        entry: '//localhost:8080',
        container: '#main',
        activeRule: '/sub-html',
    }
]);

// 启动
start();
  1. 增加子应用的挂载点
// main 是我的挂载节点
<t-layout>
  <main id="main">
  </main>
  <RouterView />
</t-layout>

为了防止app ID与里面子应用的ID冲突,这里把挂点的节点修改一下

createApp(App).use(router).use(TDesign).mount('#base-app')
// 对应的html文件也要调整
<div id="base-app"></div>
主应用处理完成,测试主应用的两个路由是否正常。

image

image

VUE子应用

初始化项目略
安装qiankun插件
pnpm add vite-plugin-qiankun

修改vite.config.js

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx';
import qiankun from 'vite-plugin-qiankun'

// https://vitejs.dev/config/
export default ({mode}) =>{
  const __DEV__ = mode === 'development';
  return defineConfig({
    base: __DEV__ ? '/' : '//localhost:5174',
    plugins: [vue(),vueJsx({}),
      qiankun('sub-vue',{
        useDevMode:true,
      })],
  })
}
改造main.js(or ts)文件
import { createApp } from 'vue'
import { renderWithQiankun, qiankunWindow } from "vite-plugin-qiankun/es/helper.js";
import './style.css'
import App from './App.vue'

let app

// 判断是否在 qiankun 环境中运行
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
    // 如果不是在 qiankun 环境中运行,直接创建 Vue 应用并挂载到 #app 元素上
    createApp(App).mount('#app')
} else {
    // 如果在 qiankun 环境中运行,使用 renderWithQiankun 函数注册微应用的生命周期
    renderWithQiankun({
        // 挂载函数,在微应用挂载时调用
        mount(props) {
            // 创建 Vue 应用
            app = createApp(App);
            // 将应用挂载到 qiankun 提供的容器中的 #app 元素上
            app.mount(props.container.querySelector('#app'))
            console.log('vue mount')
        },
        // 引导函数,在微应用启动时调用
        bootstrap() {
            console.log('vue bootstrap')
        },
        // 更新函数,在微应用更新时调用
        update() {
            console.log('vue update')
        },
        // 卸载函数,在微应用卸载时调用
        unmount() {
            console.log('vue unmount')
            // 卸载 Vue 应用
            app?.unmount()
        }
    })
}
测试一下。对应的路由是否成功。注意查看控制台的DOM结构

image

REACT子应用

初始化项目略
还是在vite.config.js里面配置
import { defineConfig } from 'vite';
import qiankun from 'vite-plugin-qiankun';

export default ({ mode }) => {
  // 判断是否为开发模式
  const __DEV__ = mode === 'development';

  return defineConfig({
    server: {
      // 服务器端口号
      port: 5175,
      // 服务器源地址
      origin: '//localhost:5175',
    },
    // 应用基础路径,开发模式下为根路径,生产模式下为服务器地址
    base: __DEV__ ? '/' : '//localhost:5175',
    plugins: [
      // 使用 qiankun 插件,配置微应用
      qiankun('sub-react', {
        // 开发模式下使用 dev 模式
        useDevMode: true,
      }),
    ],
  });
};

注意这里默认plugins里面会有react(),删除掉,如果控制台报错,改为在具体的页面上引用。

修改入口文件main.jsx

ReactDOM.createRoot是React 18的用法,如果是之前的版本,ReactDOM.render方法实现

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper';

let root = null;

// 渲染函数,用于渲染应用
function render(props) {
    const { container } = props;
    // 获取目标容器,优先使用传入的容器,否则使用默认的根元素
    const targetContainer = container ? container.querySelector('#root') : document.getElementById('root');
    // 创建 React 根元素
    root = ReactDOM.createRoot(targetContainer);
    // 渲染应用
    root.render(
        <React.StrictMode>
            <App />
        </React.StrictMode>
    );
}

// 使用 qiankun 的渲染函数进行渲染
renderWithQiankun({
    // 挂载函数,在微应用挂载时调用
    mount(props) {
        console.log('sub-vite2-react mount');
        render(props);
    },
    // 引导函数,在微应用启动时调用
    bootstrap() {
        console.log('bootstrap');
    },
    // 卸载函数,在微应用卸载时调用
    unmount(props) {
        console.log('sub-vite2-react unmount');
        root.unmount();
    },
});

// 如果不是在 qiankun 环境下运行,直接渲染应用
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
    render({});
}

运行,测试,成功!

image

引入jquery的HTML页面

忽然发现找不到一个jquery的页面了,网上下载个模板到处都是收费的。。。

刚好昨天想给blog换个程序,测试了hexo打包了一个默认页面。

image

编辑一个入口的js文件 entry.js
// 定义渲染函数,接收 jQuery 对象作为参数
const render = ($) => {
    // 使用 jQuery 选择 #purehtml-container 元素,并设置其内容为 "Hello, render with jQuery"
    $('#purehtml-container').html('Hello, render with jQuery');
    // 返回一个已解决的 Promise,表示渲染完成
    return Promise.resolve();
};

// 立即执行函数,接收全局对象 window 作为参数
((global) => {
    // 在全局对象上定义 purehtml 对象,包含微应用的生命周期函数
    global['purehtml'] = {
        // 引导函数,在微应用启动时调用
        bootstrap: () => {
            console.log('purehtml bootstrap');
            // 返回一个已解决的 Promise,表示引导完成
            return Promise.resolve();
        },
        // 挂载函数,在微应用挂载时调用
        mount: () => {
            console.log('purehtml mount');
            // 调用渲染函数,传入 jQuery 对象,并返回渲染结果
            return render($);
        },
        // 卸载函数,在微应用卸载时调用
        unmount: () => {
            console.log('purehtml unmount');
            // 返回一个已解决的 Promise,表示卸载完成
            return Promise.resolve();
        },
    };
})(window);

在首页的html最下面引入该js
</body>
<script src="./entry.js" entry></script>
</html>
用http-server启了测试一下

提示cros的时候,加上启动命令 --cros

image

整体演示

image

待办事件:

不整了

  • 样式隔离
  • 子应用间通信
  • 微应用之间的状态共享

预到的问题:

Q:打开基座应用的时候,没有问题,切换到其它子应用之后,基座的内容无法正常打开。

A:问题产生的原因是,当子应用被激活的时候会将子应用挂载到 ID 为 reactAppContainer 的元素上,那自然就替换了该元素的子元素,所以 <router-view />就不存在了,再回到主应用的时候路由就会报错。

// 错误写法
<main id="main">
   <RouterView />
</main>
      
// 正确写法
<main id="main">
</main>
<RouterView />

Q:如果加载react的报错。

[import-html-entry]: error occurs while executing normal script import { injectIntoGlobalHook } from "/@react-refresh";

修改vite.config.js 里面把plugins的react删除。在页面上引用

plugins: [
react(), // 删除
    qiankun('sub-react', {
      useDevMode: true,
    }),
  ],
算彩蛋吗

看着正打开的qiankun的网站,心想,要不试了一下直接引用qiankun的网站。

除了样式有点乱之外,还都能使用。

image

image

image

image