# turbo-qiankun-template
**Repository Path**: ydb_sir/turbo-qiankun-template
## Basic Information
- **Project Name**: turbo-qiankun-template
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 1
- **Created**: 2024-06-18
- **Last Updated**: 2024-12-31
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 基于Pnpm + Turborepo + QianKun的微服务+Monorepo实践
## 背景
微前端一般都会涉及多个代码库,很多时候要一个一个代码库地去开发维护和运行,很不方便,这种时候引入Monorepo搭配微前端就能很好地解决这种问题,一个代码库就可以完成整个微前端项目的维护,同时基于Monorepo的版本管理也有成熟的方案。
个人观点:一般是要兼容新旧项目的时候,提供一套插拔机制,在保证新功能可以使用新技术栈的同时,兼容旧项目平稳运行,这种时候使用微前端就比较合适,不然强行使用微前端的话,就是强行增加开发难度和心智损耗。
## 创建Turborepo项目
```
pnpm dlx create-turbo@latest
or
npx create-turbo@latest
```
第一步给项目命名,例如`turbo-qiankun-project`,第二步选Pnpm,其它的可一路回车。
## 项目整体结构
整个的turbo项目结构大致如下。
```less
├── 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文件夹里创建微前端应用,主要是以下几个部分。
```less
├── 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
```bash
pnpm i qiankun
```
2. 修改入口文件
```javascript
// 在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
```js
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传递参数
```js
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 文件配置
```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` 对象进行全局配置,所有的子应用都将实现这些生命周期钩子:
```js
// 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配置。
```bush
pnpm i react-app-rewired customize-cra -D
```
### 改造子应用
1.在src目录新增文件`public-path.js`
```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`文件,并新增如下配置
```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`文件
```jsx
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(
}
>
}
>
{/* 子应用一定不能写,否则会出现路由跳转bug */}
{/* */}
);
}
// 判断是否在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`
```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`
```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`
```js
export default {
base: '/sub-umi',
// plugins: ['@umijs/plugins/dist/qiankun'],
qiankun: {
slave: {},
}
};
```
完成上面两步就可以在基座中看到umi子应用的加载了。
3.修改入口文件
如果想在qiankun的生命周期中做些处理,需要修改下入口文件,在子应用的 `src/app.ts` 中导出 `qiankun` 对象,实现生命周期钩子。子应用运行时仅支持配置 `bootstrap`、`mount` 和 `unmount` 钩子:
```js
// 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()`方法并做相应的跳转逻辑
```js
// 重写函数
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`的方式来引入,减少加载重复包导致资源浪费,就是一个项目使用后另一个项目不必再重复加载。
方式:
- 主应用:将所有公共依赖配置`webpack` 的`externals`,并且在`index.html`使用外链引入这些公共依赖
- 子应用:和主应用一样配置`webpack` 的`externals`,并且在`index.html`使用外链引入这些公共依赖,注意,还需要给子应用的公共依赖的加上 `ignore` 属性(这是自定义的属性,非标准属性),qiankun在解析时如果发现`igonre`属性就会自动忽略
以axios为例:
```js
// 修改config-overrides.js
const { override, addWebpackExternals } = require('customize-cra')
module.exports = override(
addWebpackExternals ({
axios: "axios",
}),
)
```
```html
```
## 全局状态管理
一般来说,各个子应用是通过业务来划分的,不同业务线应该降低耦合度,尽量去避免通信,但是如果涉及到一些公共的状态或者操作,qiankun也是支持的。
qinkun提供了一个全局的`GlobalState`来共享数据,基座初始化之后,子应用可以监听到这个数据的变化,也能提交这个数据。
基座:
```js
// 基座初始化
import { initGlobalState } from 'qiankun';
const actions = initGlobalState(state);
// 主项目项目监听和修改
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
actions.setGlobalState(state);
```
子应用:
```js
// 子项目监听和修改
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

# 参考文章
《打造高效Monorepo:Turborepo、pnpm、Changesets实践》https://tech.uupt.com/?p=1185
《Qiankun官网》https://qiankun.umijs.org/zh/guide/tutorial#umi-qiankun-%E9%A1%B9%E7%9B%AE
《Umi官网》https://umijs.org/docs/max/micro-frontend
《用微前端 qiankun 接入十几个子应用后,我遇到了这些问题》https://juejin.cn/post/7202108772924325949#heading-5