基于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,解析里面的jsactiveRule: "/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', // 唯一 identry: '//localhost:7001', // html entry},{name: 'app2', // 唯一 identry: '//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 entryname: "reactApp", // 子应用名称container: "#subapp", // 子应用挂载的 divactiveRule: "/sub-react",props: {// 子应用传值msg: {data: {mt: "you are one",},},historyMain: (value:any) => {history.push(value);},},},{entry: SUB_REACT_SECOND, // html entryname: "reactAppSecond",container: "#subapp", // 子应用挂载的divactiveRule: "/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 替换成 chunkLoadingGlobalconfig.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(<BrowserRouterbasename={window.__POWERED_BY_QIANKUN__ ? "/sub-react" : "/sub-react"}><Switch><Routepath="/"exactrender={(propsAPP) => <App {...propsAPP} propsMainAPP={props} />}></Route><Routepath="/main"exactrender={(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 = argumentswindow.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

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring Cloud Alibaba微服务从入门到进阶(三)(Spring Cloud Alibaba)

Spring Cloud Alibaba是spring Cloud的子项目 Spring Cloud Alibaba的主要组件&#xff08;红框内是开源的&#xff09; Spring Cloud是快速构建分布式系统的工具集&#xff0c; Spring Cloud提供了很多分布式功能 Spring Cloud常用子项目 项目整合 Spring Cloud Alibaba …

友塔游戏测试开发笔面经验

题目一 给定任意非负整数M&#xff0c;判断其能否表达为 M 2 ^a 2 ^b(a和b为非负整数)&#xff0c;若可以输出a和b&#xff0c;若不能输出-1&#xff1b; 例如&#xff1a; 输入&#xff1a;6 输出: “1 2” 分析&#xff1a; void findAB(int M){} 为解决问题的主函数 …

优选算法[1]

目录 1.双指针&#xff1b; 2.滑动窗口&#xff1b; 3.二分查找&#xff1b; 4.前缀和&#xff1b; 1.双指针&#xff1b; 包括对撞指针和快慢指针(一般用来循环&#xff09;&#xff1b; 题目类型&#xff1a;移动零&#xff0c;复写零&#xff0c;快乐数&#xff0c;盛…

【每日力扣】235. 二叉搜索树的最近公共祖先与39. 组合总和问题描述

&#x1f525; 个人主页: 黑洞晓威 &#x1f600;你不必等到非常厉害&#xff0c;才敢开始&#xff0c;你需要开始&#xff0c;才会变的非常厉害。 235. 二叉搜索树的最近公共祖先 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义…

Python中类方法和静态方法的区别你知道吗?

​1.类方法 通过 classmethod 装饰器修饰的方法就是类方法 类方法可以通过类名或对象名调用&#xff0c;但是一般情况下使用类名调用&#xff08;节省内存&#xff09; 类方法中没有self.在类方法中不可以使用其它对象的属性和方法 类方法中一般会有一个参数cls&#xff0c;…

Crc冗余校验码设计

串行电路的位置&#xff0c;有异或门的地方是1&#xff08;生成多项式&#xff09; 简单的来说&#xff0c;如果最高位Q4 为0 的话&#xff0c;那么直接和 0 进行异或的话&#xff0c;实现的也是自己本身&#xff0c;直接左移就可以了 如果最高是1的话&#xff0c;那么就要和生…

【数据结构与算法】:选择排序与快速排序

&#x1f525;个人主页&#xff1a; Quitecoder &#x1f525;专栏&#xff1a;数据结构与算法 我的博客即将同步至腾讯云开发者社区&#xff0c;邀请大家一同入驻&#xff1a;腾讯云 欢迎来到排序的第二个部分&#xff1a;选择排序与快速排序&#xff01; 目录 1.选择排序1.…

如何export windows中的环境变量

在大语言模型&#xff08;LLM&#xff09;学习过程中&#xff0c; 利用 jupyter 导入环境变量时出现以下问题&#xff0c; C:\Users\zhangxuantao>export SENSENOVA_SKxxxxxx export 不是内部或外部命令&#xff0c;也不是可运行的程序 或批处理文件。 原因是学习教程中用…

VTK安装(C++)并配置vs

准备工作&#xff1a; 1.VTK下载包(此教程使用VTK8.2.0) 2.CMAKE(此教程使用3.29.0) 在此不过多赘述&#xff0c;可在网上搜索cmake安装 3.visual studio(此教程使用vs2019) VTK下载及编译&#xff1a; 1、找到自己适合的VTK版本,我选择的是VTK8.2.0。 1.1 官网下载&#xff…

天水麻辣烫榜上有名!2024适合普通人的创业项目!2024最适合创业的三大行业!2024热门创业项目!

1、天水麻辣烫 最近济南6天开了4家甘肃麻辣烫天天爆满 有店日营业额破万元有店主飞甘肃天水学习5天回来迅速开店&#xff01;选择天水麻辣烫作为创业项目绝对是一个明智的选择。趁着现在的热度&#xff0c;开设一家门店&#xff0c;借助其已经积累的名气和口碑&#xff0c;创业…

Docker常用命令的使用及镜像的构建

1.docker的好处 在开发中可能会遇到一个问题&#xff0c;一个程序在自己电脑上能跑&#xff0c;但是换到服务器上就不行了。如果我们重新搭建环境&#xff0c;需要重新部署mysql,es,redis等组件很麻烦。有了docker之后&#xff0c;我们可以快速完成项目的部署。同时docker的隔…

L1-072 刮刮彩票 分数 20 (巧用一维数组,数组加和)

啊啊啊啊啊啊啊啊明明就想出来了&#xff0c;明明就&#xff0c;就差这2分为什么为什么啊&#xff01;&#xff01;&#xff01;忘记当 tt 大于3小于6时应该 - 3 了&#xff0c;哎呦喂&#xff0c;三位的数组哪有4&#xff0c;5&#xff0c;6啊啊啊啊啊忘记减了&#xff0c;忘了…