基于Pnpm + Turborepo + QianKun的微前端+Monorepo实践

基于Pnpm + Turborepo + QianKun的微前端+Monorepo实践

背景

微前端一般都会涉及多个代码库,很多时候要一个一个代码库地去开发维护和运行,很不方便,这种时候引入Monorepo搭配微前端就能很好地解决这种问题,一个代码库就可以完成整个微前端项目的维护,同时基于Monorepo的版本管理也有成熟的方案。

个人观点:一般是要兼容新旧项目的时候,提供一套插拔机制,在保证新功能可以使用新技术栈的同时,兼容旧项目平稳运行,这种时候使用微前端就比较合适,不然强行使用微前端的话,就是强行增加开发难度和心智损耗。

创建Turborepo项目

pnpm dlx create-turbo@latest
or 
npx create-turbo@latest

第一步给项目命名,例如turbo-qiankun-project,第二步选Pnpm,其它的可一路回车。

项目整体结构

整个的turbo项目结构大致如下。

├── turbo-qiankun-project 
├─── apps     // 应用代码存放目录
├──── micro-base     // 基座
├──── sub-react       // react子应用,create-react-app创建的react应用,使用webpack打包
├──── sub-vue  // vue子应用,vite创建的子应用
├──── sub-umi    // umi脚手架创建的子应用
├─── packages // 公共库代码存放目录
└─── package.json   

现统一在apps文件夹里创建微前端应用,主要是以下几个部分。

├── micro-base     // 基座
├── sub-react       // react子应用,create-react-app创建的react应用,使用webpack打包
├── sub-vue  // vue子应用,vite创建的子应用
└── sub-umi    // umi脚手架创建的子应用
  • 基座(主应用):主要负责集成所有的子应用,提供一个入口能够访问你所需要的子应用的展示,尽量不写复杂的业务逻辑
  • 子应用:根据不同业务划分的模块,每个子应用都打包成umd模块的形式供基座(主应用)来加载

创建基架应用

非umi的基架应用

基座用的是create-react-app脚手架加上antd组件库搭建的项目,也可以选择vue或者其他框架。

  • 创建项目:npx create-react-app micro-base
  • 打开项目: cd micro-base
  • 启动项目:npm start
  • 暴露配置项(可选):npm run eject

以上就是一些常规的react项目创建的步骤,接下来开始引入Qiankun。

  1. 安装qiankun
pnpm i qiankun
  1. 修改入口文件
// 在src/index.tsx中增加如下代码
import { start, registerMicroApps } from 'qiankun';

// 1. 要加载的子应用列表
const apps = [
  {
    name: "sub-react", // 子应用的名称
    entry: '//localhost:8080', // 默认会加载这个路径下的html,解析里面的js
    activeRule: "/sub-react", // 匹配的路由
    container: "#sub-app" // 加载的容器
  },
]

// 2. 注册子应用
registerMicroApps(apps, {
  beforeLoad: [async app => console.log('before load', app.name)],
  beforeMount: [async app => console.log('before mount', app.name)],
  afterMount: [async app => console.log('after mount', app.name)],
})

start() // 3. 启动微服务

当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。

主要用到的两个API:

  • registerMicroApps(apps, lifeCycles?)

    注册所有子应用,qiankun会根据activeRule去匹配对应的子应用并加载

  • start(options?)

    启动 qiankun,可以进行预加载和沙箱设置

至此基座就改造完成,如果是老项目或者其他框架的项目想改成微前端的方式也是类似。

基于umi的基架应用

1.创建项目

1.安装插件plugin-qiankun

pnpm i @umijs/plugin-qiankun -D

2.配置.umirc.ts

defineConfig({
  ...... ,
  qiankun: {
    master: {
      // 注册子应用信息
      apps: [
        {
          name: 'app1', // 唯一 id
          entry: '//localhost:7001', // html entry
        },
        {
          name: 'app2', // 唯一 id
          entry: '//localhost:7002', // html entry
        },
      ],
    },
  },
});

3.app.js 文件配置

以下详细配置可写在app.ts 文件中作为在.umirc.ts 文件中注册之后的补充

在app.ts中补充的原因:.umirc.ts 文件中注册时不能使用props传递参数

import { SUB_REACT, SUB_REACT_SECOND } from "@/utils/proxy";
// 子应用传递参数使用
export const qiankun = {
    master: {
      // 注册子应用信息
      apps: [
        {
          entry: SUB_REACT, // html entry
          name: "reactApp", // 子应用名称
          container: "#subapp", // 子应用挂载的 div
          activeRule: "/sub-react",
          props: {
            // 子应用传值
            msg: {
              data: {
                mt: "you are one",
              },
            },
            historyMain: (value:any) => {
              history.push(value);
            },
          },
        },
        {
          entry: SUB_REACT_SECOND, // html entry
          name: "reactAppSecond",
          container: "#subapp", // 子应用挂载的div
          activeRule: "/sec_sub",
           props: {
            // 子应用传值
            msg: {
              data: {
                mt: "you are one",
              },
            },
            historyMain: (value:any) => {
              history.push(value);
            },
          },
        },
      ],
    },
  }

4.router.js 文件配置

  {
      title: "sub-react",
      path: "/sub-react",
      component: "../layout/index.js",
      routes: [
        {
          title: "sub-react",
          path: "/sub-react",
          microApp: "reactApp",
          microAppProps: {
            autoSetLoading: true, // 开启子应用loading
            // className: "reactAppSecond", // 子应用包裹元素类名
            // wrapperClassName: "myWrapper",
          },
        },
      ],
    },
    {
      title: "sec_sub",
      path: "/sec_sub",
      component: "../layout/index.js",
      routes: [
        {
          title: "sec_sub",
          path: "/sec_sub",
          microApp: "reactAppSecond",
          microAppProps: {
            autoSetLoading: true, // 开启子应用loading
            // className: "reactAppSecond",
            // wrapperClassName: "myWrapper",
          },
        },
      ],
    },

5.父应用配置生命周期钩子

在父应用的 src/app.ts 中导出 qiankun 对象进行全局配置,所有的子应用都将实现这些生命周期钩子:

// src/app.ts
export const qiankun = {
  lifeCycles: {
    // 所有子应用在挂载完成时,打印 props 信息
    async afterMount(props) {
      console.log(props);
    },
  },
};

React子应用

创建子应用

使用create-react-app脚手架创建,webpack进行配置,为了不eject所有的webpack配置,可以选择用react-app-rewired工具来改造webpack配置。

pnpm i react-app-rewired customize-cra -D

改造子应用

1.在src目录新增文件public-path.js

if (window.__POWERED_BY_QIANKUN__) {
  // 动态设置 webpack publicPath,防止资源加载出错
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}

2.修改webpack配置文件

在根目录下新增config-overrides.js文件,并新增如下配置

const { name } = require('./package');

module.exports = {
  webpack: (config) => {
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = 'umd';
    // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
    config.output.jsonpFunction = `webpackJsonp_${name}`; 
    config.output.globalObject = 'window';

    return config;
  },

  devServer: (_) => {
    const config = _;

    config.headers = {
      'Access-Control-Allow-Origin': '*',
    };
    config.historyApiFallback = true;
    config.hot = false;
    config.watchContentBase = false;
    config.liveReload = false;

    return config;
  },
};

3.修改package.json文件

{
  // ...
  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject"
  },
  // ...
}

4.改造主入口index.js文件

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import Main from "./Main";
import Home from "./Home";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter, Switch, Route, Redirect } from "react-router-dom";
import "./public-path.js";

let root;

// 将render方法用函数包裹,供后续主应用与独立运行调用
function render(props) {
  const { container } = props;
  const dom = container ? container.querySelector('#root') : document.getElementById('root')
  root = createRoot(dom)
  root.render(
    <BrowserRouter
      basename={
        window.__POWERED_BY_QIANKUN__ ? "/sub-react" : "/sub-react"
      }
    >
      <Switch>
        <Route
          path="/"
          exact
          render={(propsAPP) => <App {...propsAPP} propsMainAPP={props} />}
        ></Route>
        <Route
          path="/main"
          exact
          render={(propsAPP) => <Main {...propsAPP} propsMainAPP={props} />}
        ></Route>
        <Route path="/home" exact component={Home}></Route>
        {/* 子应用一定不能写,否则会出现路由跳转bug */}
        {/* <Redirect from="*" to="/"></Redirect> */}
      </Switch>
    </BrowserRouter>
	);
}

// 判断是否在qiankun环境下,非qiankun环境下独立运行
if (!window.__POWERED_BY_QIANKUN__) {
  console.log("独立运行时");
  render({});
}

// 各个生命周期
// bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
export async function bootstrap() {
  console.log("[react16] react app bootstraped");
}

// 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
export async function mount(props) {
  // props.onGlobalStateChange((state, prev) => {
  //   // state: 变更后的状态; prev 变更前的状态
  //   console.log(state, prev);
  // });
  // props.setGlobalState({ username: "11111", password: "22222" });
  // console.log("[react16] props from main framework", props);
  // console.log(props.singleSpa.getAppStatus());
  render(props);
}

// 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
export async function unmount(props) {
  const { container } = props;
  root.unmount();
}
reportWebVitals();

通过上面几步,即可完成React子应用的改造。

Vite + Vue3子应用

创建子应用

选择vue3+vite

pnpm create vite@latest

改造子应用

1.安装qiankun依赖

pnpm i vite-plugin-qiankun

2.修改vite.config.js

import qiankun from 'vite-plugin-qiankun';

defineConfig({
    base: '/sub-vue', // 和基座中配置的activeRule一致
    server: {
      port: 3002,
      cors: true,
      origin: 'http://localhost:3002'
    },
    plugins: [
      vue(),
      qiankun('sub-vue', { // 配置qiankun插件
        useDevMode: true
      })
    ]
})

3.修改main.js

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper';

let app;
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  createApp(App).mount('#app');
} else {
  renderWithQiankun({
    // 子应用挂载
    mount(props) {
      app = createApp(App);
      app.mount(props.container.querySelector('#app'));
    },
    // 只有子应用第一次加载会触发
    bootstrap() {
      console.log('vue app bootstrap');
    },
    // 更新
    update() {
      console.log('vue app update');
    },
    // 卸载
    unmount() {
      console.log('vue app unmount');
      app && app.unmount();
    }
  });
}

umi子应用

创建子应用

使用最新的umi4去创建子应用,创建好后只需要简单的配置就可以跑起来。

pnpm dlx create-umi@latest

改造子应用

1.安装插件

pnpm i @umijs/plugins

2.配置.umirc.ts

export default {
  base: '/sub-umi',
  // plugins: ['@umijs/plugins/dist/qiankun'],
  qiankun: {
    slave: {},
  }
};

完成上面两步就可以在基座中看到umi子应用的加载了。

3.修改入口文件

如果想在qiankun的生命周期中做些处理,需要修改下入口文件,在子应用的 src/app.ts 中导出 qiankun 对象,实现生命周期钩子。子应用运行时仅支持配置 bootstrapmountunmount 钩子:

// src/app.ts
export const qiankun = {
  // 应用加载之前
  async bootstrap(props) {
    console.log('app1 bootstrap', props);
  },
  // 应用 render 之前触发
  async mount(props) {
    console.log('app1 mount', props);
  },
  // 应用卸载之后触发
  async unmount(props) {
    console.log('app1 unmount', props);
  },
};

注意点

样式隔离

qiankun实现了各个子应用之间的样式隔离,但是基座和子应用之间的样式隔离没有实现,所以基座和子应用之前的样式还会有冲突和覆盖的情况。

解决方法:

  • 每个应用的样式使用固定的格式
  • 通过css-module的方式给每个应用自动加上前缀

子应用间的跳转

  • 主应用和微应用都是 hash 模式,主应用根据 hash 来判断微应用,则不用考虑这个问题。
  • history模式下微应用之间的跳转,或者微应用跳主应用页面,直接使用微应用的路由实例是不行的,原因是微应用的路由实例跳转都基于路由的 base。有两种办法可以跳转:
    1. history.pushState()
    2. 将主应用的路由实例通过 props 传给微应用,微应用这个路由实例跳转。

具体方案:在基座中复写并监听history.pushState()方法并做相应的跳转逻辑

// 重写函数
const _wr = function (type: string) {
  const orig = (window as any).history[type]
  return function () {
    const rv = orig.apply(this, arguments)
    const e: any = new Event(type)
    e.arguments = arguments
    window.dispatchEvent(e)
    return rv
  }
}

window.history.pushState = _wr('pushState')

// 在这个函数中做跳转后的逻辑
const bindHistory = () => {
  const currentPath = window.location.pathname;
  setSelectedPath(
  	routes.find(item => currentPath.includes(item.key))?.key || ''
  )
}

// 绑定事件
window.addEventListener('pushState', bindHistory)

公共依赖加载

场景:如果主应用和子应用都使用了相同的库或者包(antd, axios等),就可以用externals的方式来引入,减少加载重复包导致资源浪费,就是一个项目使用后另一个项目不必再重复加载。

方式:

  • 主应用:将所有公共依赖配置webpackexternals,并且在index.html使用外链引入这些公共依赖

  • 子应用:和主应用一样配置webpackexternals,并且在index.html使用外链引入这些公共依赖,注意,还需要给子应用的公共依赖的加上 ignore 属性(这是自定义的属性,非标准属性),qiankun在解析时如果发现igonre属性就会自动忽略

以axios为例:

// 修改config-overrides.js
const { override, addWebpackExternals } = require('customize-cra')

module.exports = override(
  addWebpackExternals ({
    axios: "axios",
  }),
)
<!-- 注意:这里的公共依赖的版本必须一致 -->
<script ignore="true" src="https://unpkg.com/axios@1.1.2/dist/axios.min.js"></script>

全局状态管理

一般来说,各个子应用是通过业务来划分的,不同业务线应该降低耦合度,尽量去避免通信,但是如果涉及到一些公共的状态或者操作,qiankun也是支持的。

qinkun提供了一个全局的GlobalState来共享数据,基座初始化之后,子应用可以监听到这个数据的变化,也能提交这个数据。

基座:

// 基座初始化
import { initGlobalState } from 'qiankun';
const actions = initGlobalState(state);
// 主项目项目监听和修改
actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log(state, prev);
});
actions.setGlobalState(state);

子应用:

// 子项目监听和修改
export function mount(props) {
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log(state, prev);
  });
  props.setGlobalState(state);
}

父子应用通信

一种方法是使用GlobalState

如果是使用umi,还有两种方式:

  • 基于 useModel() 的通信。这是 Umi 推荐的解决方案。
  • 基于配置的通信。

具体可在Umi官网查阅。

项目代码地址

https://github.com/brucecat/turbo-qiankun-template

image.png

参考文章

《打造高效Monorepo:Turborepo、pnpm、Changesets实践》https://tech.uupt.com/?p=1185

《Qiankun官网》https://qiankun.umijs.org/zh/guide/tutorial#umi-qiankun-项目

《Umi官网》https://umijs.org/docs/max/micro-frontend

《用微前端 qiankun 接入十几个子应用后,我遇到了这些问题》https://juejin.cn/post/7202108772924325949#heading-5

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/461760.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AI赋能写作:AI大模型高效写作一本通

❤️作者主页&#xff1a;小虚竹 ❤️作者简介&#xff1a;大家好,我是小虚竹。2022年度博客之星评选TOP 10&#x1f3c6;&#xff0c;Java领域优质创作者&#x1f3c6;&#xff0c;CSDN博客专家&#x1f3c6;&#xff0c;华为云享专家&#x1f3c6;&#xff0c;掘金年度人气作…

文件包含例子

一、常见的文件包含函数 php中常见的文件包含函数有以下四种&#xff1a; include() require() include_once() require()_once() include与require基本是相同的&#xff0c;除了错误处理方面: include()&#xff0c;只生成警告&#xff08;E_WARNING&#xff09;&#x…

linux之source.list解析

众所周知&#xff0c;linux可以通过apt命令安装软件&#xff0c;那么apt又是从哪里获取软件包呢并安装呢&#xff1f;这里就绕不开一个文件source.list&#xff0c;该文件定义了软件源相关的信息。下面以实际例子&#xff0c;详细的介绍下这个文件。 文件作用 定义软件源的信…

MySQL-HMA 高可用故障切换

本章内容&#xff1a; 了解MySQL MHA搭建MySQL MHAMySQL MHA故障切换 1.案例分析 1.1.1案例概述 目前 MySQL 已经成为市场上主流数据库之一&#xff0c;考虑到业务的重要性&#xff0c;MySQL 数据库 单点问题已成为企业网站架构中最大的隐患。随着技术的发展&#xff0c;MHA…

【C++庖丁解牛】List容器的介绍及使用 | 深度剖析 | list与vector的对比

&#x1f341;你好&#xff0c;我是 RO-BERRY &#x1f4d7; 致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f384;感谢你的陪伴与支持 &#xff0c;故事既有了开头&#xff0c;就要画上一个完美的句号&#xff0c;让我们一起加油 目录 1. list的介绍1.1 list的…

每周一算法:双向深搜

题目描述 达达帮翰翰给女生送礼物&#xff0c;翰翰一共准备了 N N N 个礼物&#xff0c;其中第 i i i 个礼物的重量是 G [ i ] G[i] G[i]。 达达的力气很大&#xff0c;他一次可以搬动重量之和不超过 W W W的任意多个物品。 达达希望一次搬掉尽量重的一些物品&#xff0c;请…

冲动是魔鬼,工作不顺心时不要把坏脾气带给家人

今天与一个跟踪了很久的客户准备签合同了&#xff0c;客户突然反悔&#xff0c;为此与他周旋了一整天&#xff0c;忙碌得一口水都没有喝。回到小区坐在车里抽着烟&#xff0c;久久不愿回家&#xff0c;只想一个人坐着&#xff0c;疲惫、无奈。这个月的奖金似乎又将成为泡影。 …

mov格式视频怎么做二维码?视频在线做二维码的方法

如何将mov格式视频转二维码以后分享呢&#xff1f;视频二维码是现在手机获取视频内容很常用的一种方式&#xff0c;通过二维码生成器工具就可以快速在线生成二维码图片&#xff0c;使用手机扫码就可以播放视频。但是视频的格式有很多种&#xff0c;当我们需要将mov格式的视频生…

网络安全——关于防火墙

网络安全防火墙是很重要的部分&#xff0c;关于防火墙我们要知道&#xff0c;他默认所有流量都是黑名单&#xff0c;只有开启允许通过才可以。 我们通过一个实验来学防火墙命令。 防火墙要登录才能使用&#xff0c;用户名是admin,默认密码是Admin123&#xff0c;在第一次登录…

Spring AI Chat 简单示例

官方文档地址&#xff1a; https://docs.spring.io/spring-ai/reference/index.html Spring AI 可以方便 Java 开发者在代码中集成 AI 的功能&#xff0c;通过 Spring 提供的抽象&#xff0c;可以方便的切换不同的AI提供商&#xff0c;Spring AI 是对 AI 的使用&#xff0c;并…

Android 地图SDK 绘制点 删除 指定

问题 Android 地图SDK 删除指定绘制点 详细问题 笔者进行Android 项目开发&#xff0c;对于已标记的绘制点&#xff0c;提供撤回按钮&#xff0c;即删除绘制点&#xff0c;如何实现。 解决方案 新增绘制点 private List<Marker> markerList new ArrayList<>…

没有公式,不要代码,让你理解 RCNN:目标检测中的区域卷积神经网络

⭐️ 导言 在计算机视觉领域&#xff0c;目标检测是一项关键任务&#xff0c;它涉及识别图像中感兴趣的物体&#xff0c;并定位它们的位置。而RCNN&#xff08;Region-based Convolutional Neural Network&#xff09;是一种经典的目标检测算法&#xff0c;它以区域为基础进行…

BMP280 arduino调试

终于成功了。 #include <SPI.h> //定义数据类型 #define s32_t long signed int #define u32_t long unsigned int #define u16_t unsigned short #define s16_t signed short // 定义从设备选择引脚 const int chipSelectPin 10; //定义BMP280寄存器/// unsigned int …

R语言:如何基于地球外辐射(Ra)和相对日照(n/N)计算太阳辐射Rs?

正在编写相关软著&#xff0c;借此机会了解R语言的基本语法和一些处理流程&#xff0c;所以解释稍微繁琐。 Note&#xff1a; 使用的R语言版本是 R version 4.3.2 (2023-10-31 ucrt) 使用的RStudio编辑器版本是&#xff1a; 01 基于随机森林的插值填补缺失值 这是目前处理…

电子供应链的未来:电子元器件采购商城的洞察

电子供应链的未来将受到数字化技术、智能化制造和全球化贸易等趋势的深刻影响。在这一背景下&#xff0c;电子元器件采购商城将发挥越来越重要的作用&#xff0c;并提供以下洞察&#xff1a; 数字化转型&#xff1a; 电子元器件采购商城将更加注重数字化转型&#xff0c;通过引…

【计算机系统结构】重叠方式

&#x1f4dd;本文介绍 本文主要内容位计算机系统结构的重叠方式 &#x1f44b;作者简介&#xff1a;一个正在积极探索的本科生 &#x1f4f1;联系方式&#xff1a;943641266(QQ) &#x1f6aa;Github地址&#xff1a;https://github.com/sankexilianhua &#x1f511;Gitee地址…

不可变集合

2. 3. 如果键值对超过10个的话 优化之后 要生成不可变的集合直接使用copyof就可以

Python XML处理实战指南:从基础到高级技巧

Python XML处理实战指南&#xff1a;从基础到高级技巧 介绍XML基础XML的定义和特点XML结构组成命名空间&#xff08;Namespaces&#xff09;小结 Python中处理XML的库ElementTreeminidomlxml 使用ElementTree解析XML读取XML文件遍历XML元素查找特定元素修改XML文件 使用lxml处理…

除了「au revoir」,「再见」还能怎么说?柯桥成人学外语来银泰附近

1. Je dois y alle#15857575376r I have to go there Y there&#xff0c;意思是“我要走了”。 例如&#xff0c;”Moi, je dois y aller.” 对不起&#xff0c;我该走了。 如果你和同伴都要离开&#xff0c;那就可以说"On y va"&#xff0c;它相当于英语里…

C#集合和数据结构,随笔记录

C#集合和数据结构 System.Collections命名空间包含接口和类&#xff0c;这些接口和类定义各种对象&#xff08;如列表/链表、位数组、哈希表、队列和堆栈&#xff09;的集合 System.Collections.Generic命名空间&#xff1a; 所有集合都直接或间接基于ICollection接口 列表类集…