import('@/pages/home')} />,
15 | },
16 | ],
17 | },
18 | {
19 | path: '*',
20 | element: 404
,
21 | },
22 | ])
23 |
24 | return (
25 | <>
26 | }>{elements}
27 | >
28 | )
29 | }
30 |
31 | export default () => (
32 |
33 |
34 |
35 | )
36 |
--------------------------------------------------------------------------------
/packages/shared/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@packages/shared",
3 | "version": "0.0.1",
4 | "dependencies": {
5 | "@ant-design/icons": "^4.7.0",
6 | "@packages/shared": "^0.0.1",
7 | "@types/md5": "^2.3.2",
8 | "ahooks": "^3.3.10",
9 | "antd": "^4.20.1",
10 | "axios": "^0.27.2",
11 | "hox": "1.0.2",
12 | "lodash": "^4.17.21",
13 | "md5": "^2.3.0",
14 | "qiankun": "^2.7.0",
15 | "qs": "^6.10.3",
16 | "react": "^17.0.0",
17 | "react-dom": "^17.0.0",
18 | "react-router-dom": "^6.3.0",
19 | "vite-plugin-qiankun": "^1.0.14"
20 | },
21 | "devDependencies": {
22 | "@types/react": "^17.0.0",
23 | "@types/react-dom": "^17.0.0",
24 | "@vitejs/plugin-react": "^1.3.2",
25 | "@vitejs/plugin-vue": "^2.3.2",
26 | "ip": "^1.1.5",
27 | "less-plugin-import-node-modules": "^1.0.0",
28 | "vite": "^2.9.8",
29 | "vite-plugin-imp": "^2.1.8"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/scripts/afterBuild.js:
--------------------------------------------------------------------------------
1 | const rimraf = require('rimraf')
2 | const fs = require('fs-extra')
3 |
4 | rimraf.sync('./dist')
5 |
6 | try {
7 | fs.copySync('./modules/app/dist', './dist', {
8 | overwrite: true,
9 | })
10 | } catch (err) {
11 | console.error(err)
12 | }
13 |
14 | // const legacyModules = {
15 | // vue: 'dist',
16 | // react: 'build',
17 | // }
18 |
19 | // Object.entries(legacyModules).forEach(([name, distDir]) => {
20 | // try {
21 | // fs.copySync(`./legacies/${name}/${distDir}`, `./dist/${name}`, {
22 | // overwrite: true,
23 | // })
24 | // } catch (err) {
25 | // console.error(err)
26 | // }
27 | // })
28 |
29 | const microModules = {
30 | 'demo-react-1': 'dist',
31 | 'demo-react-2': 'dist',
32 | 'demo-vue-1': 'dist',
33 | 'demo-vue-2': 'dist',
34 | }
35 |
36 | Object.entries(microModules).forEach(([name, distDir]) => {
37 | try {
38 | fs.copySync(`./modules/${name}/${distDir}`, `./dist/${name}`, {
39 | overwrite: true,
40 | })
41 | } catch (err) {
42 | console.error(err)
43 | }
44 | })
45 |
--------------------------------------------------------------------------------
/modules/app/src/layouts/Common/style.scss:
--------------------------------------------------------------------------------
1 | .common-layout-wrapper {
2 | height: 100vh;
3 | width: 100vw;
4 |
5 | .trigger {
6 | padding: 0 24px;
7 | font-size: 18px;
8 | line-height: 64px;
9 | cursor: pointer;
10 | transition: color 0.3s;
11 | }
12 |
13 | .trigger:hover {
14 | color: #1890ff;
15 | }
16 |
17 | .logo {
18 | height: 32px;
19 | margin: 16px;
20 | background: rgba(255, 255, 255, 0.3);
21 | }
22 |
23 | .ant-layout {
24 | height: 100vh;
25 | width: 100vw;
26 | }
27 |
28 | .ant-layout-content {
29 | overflow: auto;
30 | }
31 |
32 | .common-layout-header {
33 | border-bottom: solid 1px #f2f2f2;
34 |
35 | &-content {
36 | display: flex;
37 | align-items: center;
38 | justify-content: space-between;
39 | }
40 | }
41 |
42 | .common-layout-header,
43 | .common-layout-content {
44 | background: #fff;
45 | }
46 |
47 | .common-layout-content {
48 | padding: 24px;
49 | min-height: 100%;
50 | }
51 |
52 | .common-layout-header-content-right {
53 | padding: 0 24px;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/modules/demo-react-1/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@modules/demo-react-1",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "tsc && vite build",
7 | "serve": "vite preview"
8 | },
9 | "dependencies": {
10 | "@ant-design/icons": "^4.7.0",
11 | "@packages/shared": "^0.0.1",
12 | "@types/md5": "^2.3.2",
13 | "ahooks": "^3.3.10",
14 | "antd": "^4.20.1",
15 | "axios": "^0.27.2",
16 | "hox": "1.0.2",
17 | "md5": "^2.3.0",
18 | "react": "^17.0.0",
19 | "react-dom": "^17.0.0",
20 | "react-router-dom": "^6.3.0",
21 | "react-i18next": "^11.16.9",
22 | "vite-plugin-qiankun": "^1.0.14"
23 | },
24 | "devDependencies": {
25 | "@packages/build": "^0.0.1",
26 | "@types/react": "^17.0.0",
27 | "@types/react-dom": "^17.0.0",
28 | "@vitejs/plugin-react": "^1.3.2",
29 | "autoprefixer": "^10.4.7",
30 | "less": "^4.1.2",
31 | "postcss": "^8.4.14",
32 | "sass": "^1.51.0",
33 | "tailwindcss": "^3.0.24",
34 | "typescript": "^4.3.2",
35 | "vite": "^2.9.8",
36 | "vite-plugin-imp": "^2.1.8"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/modules/demo-react-2/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@modules/demo-react-2",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "tsc && vite build",
7 | "serve": "vite preview"
8 | },
9 | "dependencies": {
10 | "@ant-design/icons": "^4.7.0",
11 | "@packages/shared": "^0.0.1",
12 | "@types/md5": "^2.3.2",
13 | "ahooks": "^3.3.10",
14 | "antd": "^4.20.1",
15 | "axios": "^0.27.2",
16 | "hox": "1.0.2",
17 | "md5": "^2.3.0",
18 | "react": "^17.0.0",
19 | "react-dom": "^17.0.0",
20 | "react-router-dom": "^6.3.0",
21 | "react-i18next": "^11.16.9",
22 | "vite-plugin-qiankun": "^1.0.14"
23 | },
24 | "devDependencies": {
25 | "@packages/build": "^0.0.1",
26 | "@types/react": "^17.0.0",
27 | "@types/react-dom": "^17.0.0",
28 | "@vitejs/plugin-react": "^1.3.2",
29 | "autoprefixer": "^10.4.7",
30 | "less": "^4.1.2",
31 | "postcss": "^8.4.14",
32 | "sass": "^1.51.0",
33 | "tailwindcss": "^3.0.24",
34 | "typescript": "^4.3.2",
35 | "vite": "^2.9.8",
36 | "vite-plugin-imp": "^2.1.8"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/templates/micro-react-vite/package.json.tpl:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@modules/{{ name }}",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "tsc && vite build",
7 | "serve": "vite preview"
8 | },
9 | "dependencies": {
10 | "@ant-design/icons": "^4.7.0",
11 | "@packages/shared": "^0.0.1",
12 | "@types/md5": "^2.3.2",
13 | "ahooks": "^3.3.10",
14 | "antd": "^4.20.1",
15 | "axios": "^0.27.2",
16 | "hox": "1.0.2",
17 | "md5": "^2.3.0",
18 | "react": "^17.0.0",
19 | "react-dom": "^17.0.0",
20 | "react-router-dom": "^6.3.0",
21 | "react-i18next": "^11.16.9",
22 | "vite-plugin-qiankun": "^1.0.14"
23 | },
24 | "devDependencies": {
25 | "@packages/build": "^0.0.1",
26 | "@types/react": "^17.0.0",
27 | "@types/react-dom": "^17.0.0",
28 | "@vitejs/plugin-react": "^1.3.2",
29 | "autoprefixer": "^10.4.7",
30 | "less": "^4.1.2",
31 | "postcss": "^8.4.14",
32 | "sass": "^1.51.0",
33 | "tailwindcss": "^3.0.24",
34 | "typescript": "^4.3.2",
35 | "vite": "^2.9.8",
36 | "vite-plugin-imp": "^2.1.8"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/modules/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@modules/app",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "tsc && vite build",
7 | "serve": "vite preview"
8 | },
9 | "dependencies": {
10 | "@ant-design/icons": "^4.7.0",
11 | "@packages/assets": "^0.0.1",
12 | "@packages/shared": "^0.0.1",
13 | "@types/lodash": "^4.14.182",
14 | "@types/md5": "^2.3.2",
15 | "ahooks": "^3.3.10",
16 | "antd": "^4.20.1",
17 | "axios": "^0.27.2",
18 | "history": "^5.2.0",
19 | "hox": "1.0.2",
20 | "lodash": "^4.17.21",
21 | "md5": "^2.3.0",
22 | "qiankun": "^2.7.0",
23 | "react": "^17.0.0",
24 | "react-dom": "^17.0.0",
25 | "react-router-dom": "^6.3.0"
26 | },
27 | "devDependencies": {
28 | "@packages/build": "^0.0.1",
29 | "@types/react": "^17.0.0",
30 | "@types/react-dom": "^17.0.0",
31 | "@vitejs/plugin-react": "^1.3.2",
32 | "autoprefixer": "^10.4.7",
33 | "less": "^4.1.2",
34 | "postcss": "^8.4.14",
35 | "sass": "^1.51.0",
36 | "tailwindcss": "^3.0.24",
37 | "typescript": "^4.3.2",
38 | "vite": "^2.9.8",
39 | "vite-plugin-imp": "^2.1.8"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/modules/demo-vue-1/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import {
3 | renderWithQiankun,
4 | qiankunWindow,
5 | } from 'vite-plugin-qiankun/dist/helper'
6 |
7 | import App from './App.vue'
8 | import './index.scss'
9 |
10 | const appName = 'demo-vue-1'
11 |
12 | // @ts-ignore
13 | window.__POWERED_BY_QIANKUN__ = qiankunWindow.__POWERED_BY_QIANKUN__
14 |
15 | let app: any
16 | async function start(props: any = {}) {
17 | const { container } = props
18 | app = createApp(App)
19 | app.mount(
20 | container
21 | ? container.querySelector(`#${appName}-app`)
22 | : document.querySelector(`#${appName}-app`)
23 | )
24 | }
25 |
26 | function applyProps(props: any) {}
27 |
28 | renderWithQiankun({
29 | bootstrap() {
30 | console.log(`[${appName}] bootstrap`)
31 | },
32 | mount(props: any) {
33 | console.log(`[${appName}] mount`, props)
34 | applyProps(props)
35 | start(props)
36 | },
37 | update(props: any) {
38 | console.log(`[${appName}] update`, props)
39 | applyProps(props?.props ?? props)
40 | },
41 | unmount() {
42 | console.log(`[${appName}] unmount`)
43 | app.unmount()
44 | },
45 | })
46 |
47 | // @ts-ignore
48 | if (!window.__POWERED_BY_QIANKUN__) {
49 | start()
50 | }
51 |
--------------------------------------------------------------------------------
/modules/demo-vue-2/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import {
3 | renderWithQiankun,
4 | qiankunWindow,
5 | } from 'vite-plugin-qiankun/dist/helper'
6 |
7 | import App from './App.vue'
8 | import './index.scss'
9 |
10 | const appName = 'demo-vue-2'
11 |
12 | // @ts-ignore
13 | window.__POWERED_BY_QIANKUN__ = qiankunWindow.__POWERED_BY_QIANKUN__
14 |
15 | let app: any
16 | async function start(props: any = {}) {
17 | const { container } = props
18 | app = createApp(App)
19 | app.mount(
20 | container
21 | ? container.querySelector(`#${appName}-app`)
22 | : document.querySelector(`#${appName}-app`)
23 | )
24 | }
25 |
26 | function applyProps(props: any) {}
27 |
28 | renderWithQiankun({
29 | bootstrap() {
30 | console.log(`[${appName}] bootstrap`)
31 | },
32 | mount(props: any) {
33 | console.log(`[${appName}] mount`, props)
34 | applyProps(props)
35 | start(props)
36 | },
37 | update(props: any) {
38 | console.log(`[${appName}] update`, props)
39 | applyProps(props?.props ?? props)
40 | },
41 | unmount() {
42 | console.log(`[${appName}] unmount`)
43 | app.unmount()
44 | },
45 | })
46 |
47 | // @ts-ignore
48 | if (!window.__POWERED_BY_QIANKUN__) {
49 | start()
50 | }
51 |
--------------------------------------------------------------------------------
/templates/micro-vue-vite/src/main.ts.tpl:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import {
3 | renderWithQiankun,
4 | qiankunWindow,
5 | } from 'vite-plugin-qiankun/dist/helper'
6 |
7 | import App from './App.vue'
8 | import './index.scss'
9 |
10 | const appName = '{{ name }}'
11 |
12 | // @ts-ignore
13 | window.__POWERED_BY_QIANKUN__ = qiankunWindow.__POWERED_BY_QIANKUN__
14 |
15 | let app: any
16 | async function start(props: any = {}) {
17 | const { container } = props
18 | app = createApp(App)
19 | app.mount(
20 | container
21 | ? container.querySelector(`#${appName}-app`)
22 | : document.querySelector(`#${appName}-app`)
23 | )
24 | }
25 |
26 | function applyProps(props: any) {}
27 |
28 | renderWithQiankun({
29 | bootstrap() {
30 | console.log(`[${appName}] bootstrap`)
31 | },
32 | mount(props: any) {
33 | console.log(`[${appName}] mount`, props)
34 | applyProps(props)
35 | start(props)
36 | },
37 | update(props: any) {
38 | console.log(`[${appName}] update`, props)
39 | applyProps(props?.props ?? props)
40 | },
41 | unmount() {
42 | console.log(`[${appName}] unmount`)
43 | app.unmount()
44 | },
45 | })
46 |
47 | // @ts-ignore
48 | if (!window.__POWERED_BY_QIANKUN__) {
49 | start()
50 | }
51 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "root",
3 | "engines": {
4 | "node": ">=12.17.0"
5 | },
6 | "private": true,
7 | "scripts": {
8 | "init": "npx pnpm@6 i --ignore-scripts --filter=!./legacies && npx pnpm@6 run --filter=./legacies --parallel init",
9 | "clean": "rimraf ./**/node_modules",
10 | "kill-dev": "kill-port --port 8080,8081,8082,8083,8084",
11 | "dev": "npm run kill-dev && npx pnpm@6 run --filter=!root --parallel dev",
12 | "dev:no-legacy": "npx pnpm@6 run --filter=!root --filter=!@legacies/* --parallel dev",
13 | "patch": "npx pnpm@6 run --filter=!root --parallel patch",
14 | "build": "npx pnpm@6 run --filter=!root --parallel build && npm run after-build",
15 | "serve": "serve ./dist",
16 | "after-build": "node ./scripts/afterBuild.js",
17 | "create": "node ./scripts/create/index.js"
18 | },
19 | "devDependencies": {
20 | "@umijs/utils": "^3.5.23",
21 | "ip": "^1.1.5",
22 | "kill-port": "^1.6.1",
23 | "prettier": "^2.5.1",
24 | "rimraf": "^3.0.2",
25 | "yargs": "^17.5.0"
26 | },
27 | "dependencies": {
28 | "fs-extra": "^10.1.0",
29 | "serve": "^13.0.2"
30 | },
31 | "stackblitz": {
32 | "installDependencies": false,
33 | "startCommand": "npm run init && npm run dev"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/modules/demo-vue-1/README.md:
--------------------------------------------------------------------------------
1 | # Vue 3 + TypeScript + Vite
2 |
3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `
8 |
9 |
10 | {{ msg }}
11 |
12 |
13 | Recommended IDE setup:
14 | VS Code
15 | +
16 | Volar
17 |
18 |
19 |
20 | See
21 | README.md
22 | for more information.
23 |
24 |
25 |
26 |
27 | Vite Docs
28 |
29 | |
30 | Vue 3 Docs
31 |
32 |
33 |
40 |
41 | Edit
42 | components/HelloWorld.vue
43 | to test hot module replacement.
44 |
45 |
46 |
47 |
64 |
--------------------------------------------------------------------------------
/modules/demo-vue-2/src/components/HelloWorld.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 | {{ msg }}
11 |
12 |
13 | Recommended IDE setup:
14 | VS Code
15 | +
16 | Volar
17 |
18 |
19 |
20 | See
21 | README.md
22 | for more information.
23 |
24 |
25 |
26 |
27 | Vite Docs
28 |
29 | |
30 | Vue 3 Docs
31 |
32 |
33 |
40 |
41 | Edit
42 | components/HelloWorld.vue
43 | to test hot module replacement.
44 |
45 |
46 |
47 |
64 |
--------------------------------------------------------------------------------
/templates/micro-vue-vite/src/components/HelloWorld.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 | {{ msg }}
11 |
12 |
13 | Recommended IDE setup:
14 | VS Code
15 | +
16 | Volar
17 |
18 |
19 |
20 | See
21 | README.md
22 | for more information.
23 |
24 |
25 |
26 |
27 | Vite Docs
28 |
29 | |
30 | Vue 3 Docs
31 |
32 |
33 |
40 |
41 | Edit
42 | components/HelloWorld.vue
43 | to test hot module replacement.
44 |
45 |
46 |
47 |
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 微前端 monorepo 项目样例
2 |
3 | 
4 |
5 | ### 在线预览
6 |
7 | [](https://stackblitz.com/edit/mono-micro-project)
8 | [](https://app.codeanywhere.com/#https://github.com/edit/mono-micro-project)
9 |
10 | [StackBlitz](http://stackblitz.com/) 对 pnpm 支持不太好所以换用 lerna 来支持 workspace 功能
11 |
12 | ## 启动方式
13 |
14 | ```bash
15 | pnpm run init # 初始化,安装依赖
16 | pnpm run dev # 启动 dev 命令,默认基座在 8080 端口
17 | pnpm run build # 构建
18 | pnpm run serve # 产物预览
19 | ```
20 |
21 | ## 给特定子项目安装依赖
22 |
23 | ```bash
24 | npm install -g pnpm # 全局安装 pnpm
25 | pnpm add xxx --filter @modules/xxx-1 --filter @modules/xxx-2
26 | ```
27 |
28 | ## 创建子项目
29 |
30 | ```bash
31 | npm run create -- --name=<模块名> --template=<模板名>
32 | ```
33 |
34 | 目前可用模板有:
35 |
36 | 1. `micro-react-vite` **(默认)**: 基于 vite 的 react(17) 子项目
37 | 2. `micro-vue-vite`:基于 vite 的 vue3 子项目
38 | 3. 待增加
39 |
40 | ### 踩坑记录
41 |
42 | 1. React.lazy 组件在 vite 中无法热更新
43 |
44 | 相关资料:
45 | https://github.com/vitejs/vite/issues/2719
46 | https://github.com/facebook/react/issues/21181
47 |
48 | 解决方式
49 |
50 | - 组件文件中仅保留 export default 部分,不要有 export const ... 导出
51 | - export default 导出具名函数,不要匿名导出,避免 export default () => ...
52 |
53 | 2. 路由建议
54 |
55 | 基座用 Browser 路由,子应用使用 Hash 路由,互不冲突,若存在多应用协同共存且有各自路由需求,建议使用 Memory 路由
56 |
57 | 3. 父子路由、子子路由冲突
58 |
59 | 要点:让子路由的 history 操作仅限在特定地址条件下生效,可能需要改 node_modules 中关键处的源码,用 patch-package 保存改动
60 |
--------------------------------------------------------------------------------
/modules/demo-react-1/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { renderWithQiankun } from 'vite-plugin-qiankun/dist/helper'
4 | import useAccount from '@packages/shared/hooks/useAccount'
5 | import useAppConfig from '@packages/shared/hooks/useAppConfig'
6 | import App from '@/routes'
7 |
8 | import './index.scss'
9 |
10 | const appName = 'demo-react-1'
11 |
12 | export default function start(props: any = {}) {
13 | const { container } = props
14 |
15 | ReactDOM.render(
16 | ,
17 | container
18 | ? container.querySelector(`#${appName}-root`)
19 | : document.querySelector(`#${appName}-root`)
20 | )
21 | }
22 |
23 | function applyProps(props: any) {
24 | useAccount.data?.setAccount(props?.account)
25 | useAppConfig?.data?.setLocale(props?.locale)
26 | }
27 |
28 | renderWithQiankun({
29 | bootstrap() {
30 | console.log(`[${appName}] bootstrap`)
31 | },
32 | mount(props: any) {
33 | console.log(`[${appName}] mount`, props)
34 | applyProps(props)
35 | start(props)
36 | },
37 | update(props: any) {
38 | console.log(`[${appName}] update`, props)
39 | applyProps(props?.props ?? props)
40 | },
41 | unmount(props: any) {
42 | console.log(`[${appName}] unmount`)
43 | const { container } = props
44 | ReactDOM.unmountComponentAtNode(
45 | container
46 | ? container.querySelector(`#${appName}-root`)
47 | : document.querySelector(`#${appName}-root`)
48 | )
49 | },
50 | })
51 |
52 | // @ts-ignore
53 | if (!window.__POWERED_BY_QIANKUN__) {
54 | start()
55 | }
56 |
57 | // @ts-ignore
58 | if (process.env.NODE_ENV === 'development') {
59 | import('@/hmr.fix')
60 | }
61 |
--------------------------------------------------------------------------------
/modules/demo-react-2/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { renderWithQiankun } from 'vite-plugin-qiankun/dist/helper'
4 | import useAccount from '@packages/shared/hooks/useAccount'
5 | import useAppConfig from '@packages/shared/hooks/useAppConfig'
6 | import App from '@/routes'
7 |
8 | import './index.scss'
9 |
10 | const appName = 'demo-react-2'
11 |
12 | export default function start(props: any = {}) {
13 | const { container } = props
14 |
15 | ReactDOM.render(
16 | ,
17 | container
18 | ? container.querySelector(`#${appName}-root`)
19 | : document.querySelector(`#${appName}-root`)
20 | )
21 | }
22 |
23 | function applyProps(props: any) {
24 | useAccount.data?.setAccount(props?.account)
25 | useAppConfig?.data?.setLocale(props?.locale)
26 | }
27 |
28 | renderWithQiankun({
29 | bootstrap() {
30 | console.log(`[${appName}] bootstrap`)
31 | },
32 | mount(props: any) {
33 | console.log(`[${appName}] mount`, props)
34 | applyProps(props)
35 | start(props)
36 | },
37 | update(props: any) {
38 | console.log(`[${appName}] update`, props)
39 | applyProps(props?.props ?? props)
40 | },
41 | unmount(props: any) {
42 | console.log(`[${appName}] unmount`)
43 | const { container } = props
44 | ReactDOM.unmountComponentAtNode(
45 | container
46 | ? container.querySelector(`#${appName}-root`)
47 | : document.querySelector(`#${appName}-root`)
48 | )
49 | },
50 | })
51 |
52 | // @ts-ignore
53 | if (!window.__POWERED_BY_QIANKUN__) {
54 | start()
55 | }
56 |
57 | // @ts-ignore
58 | if (process.env.NODE_ENV === 'development') {
59 | import('@/hmr.fix')
60 | }
61 |
--------------------------------------------------------------------------------
/templates/micro-react-vite/src/main.tsx.tpl:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { renderWithQiankun } from 'vite-plugin-qiankun/dist/helper'
4 | import useAccount from '@packages/shared/hooks/useAccount'
5 | import useAppConfig from '@packages/shared/hooks/useAppConfig'
6 | import App from '@/routes'
7 |
8 | import './index.scss'
9 |
10 | const appName = '{{ name }}'
11 |
12 | export default function start(props: any = {}) {
13 | const { container } = props
14 |
15 | ReactDOM.render(
16 | ,
17 | container
18 | ? container.querySelector(`#${appName}-root`)
19 | : document.querySelector(`#${appName}-root`)
20 | )
21 | }
22 |
23 | const applyProps = throttle(function applyProps(props: any) {
24 | useAccount.data?.setAccount(props?.account)
25 | useAppConfig?.data?.setLocale(props?.locale)
26 | })
27 |
28 | renderWithQiankun({
29 | bootstrap() {
30 | console.log(`[${appName}] bootstrap`)
31 | },
32 | mount(props: any) {
33 | console.log(`[${appName}] mount`, props)
34 | applyProps(props)
35 | start(props)
36 | },
37 | update(props: any) {
38 | console.log(`[${appName}] update`, props)
39 | applyProps(props?.props ?? props)
40 | },
41 | unmount(props: any) {
42 | console.log(`[${appName}] unmount`)
43 | const { container } = props
44 | ReactDOM.unmountComponentAtNode(
45 | container
46 | ? container.querySelector(`#${appName}-root`)
47 | : document.querySelector(`#${appName}-root`)
48 | )
49 | },
50 | })
51 |
52 | // @ts-ignore
53 | if (!window.__POWERED_BY_QIANKUN__) {
54 | start()
55 | }
56 |
57 | // @ts-ignore
58 | if (process.env.NODE_ENV === 'development') {
59 | import('@/hmr.fix')
60 | }
61 |
--------------------------------------------------------------------------------
/packages/build/vite.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const { mergeConfig } = require('vite')
3 | const qiankun = require('vite-plugin-qiankun')
4 | const { default: react } = require('@vitejs/plugin-react')
5 | const { default: vue } = require('@vitejs/plugin-vue')
6 | const { default: vitePluginImp } = require('vite-plugin-imp')
7 | // const LessNodeModules = require('less-plugin-import-node-modules')
8 | const nodeIP = require('ip')
9 | const ip = nodeIP.address()
10 |
11 | // https://vitejs.dev/config/
12 | const getConfig = ({
13 | type = 'react',
14 | micro = false,
15 | moduleName = '',
16 | dirname = process.cwd(),
17 | } = {}) => {
18 | const serverConfig = {
19 | strictPort: true,
20 | proxy: {},
21 | }
22 |
23 | const initialPlugins = {
24 | vue: [vue()],
25 | react: [
26 | react(),
27 | vitePluginImp({
28 | libList: [
29 | {
30 | libName: 'antd',
31 | style: (name) => `antd/es/${name}/style/css`,
32 | },
33 | {
34 | libName: '@ant-design/icons',
35 | libDirectory: '',
36 | camel2DashComponentName: false,
37 | },
38 | ],
39 | }),
40 | ],
41 | }[type]
42 |
43 | const sharedViteConfig = {
44 | root: dirname,
45 | server: serverConfig,
46 | preview: serverConfig,
47 | resolve: {
48 | alias: [
49 | // fix less import by: @import ~
50 | // less import no support webpack alias '~' · Issue #2185 · vitejs/vite
51 | // https://github.com/vitejs/vite/issues/2185
52 | { find: /^~/, replacement: '' },
53 | { find: '@', replacement: path.resolve(dirname, './src') },
54 | ],
55 | },
56 | plugins: initialPlugins,
57 | css: {
58 | preprocessorOptions: {
59 | less: {
60 | javascriptEnabled: true,
61 | // plugins: [new LessNodeModules()],
62 | },
63 | },
64 | },
65 | }
66 |
67 | if (!micro) {
68 | return sharedViteConfig
69 | }
70 |
71 | const microViteConfig = mergeConfig(sharedViteConfig, {
72 | base: `/${moduleName}`,
73 | plugins: [qiankun(moduleName, {})],
74 | build: {
75 | rollupOptions: {
76 | external: ['@/hmr.fix'],
77 | },
78 | },
79 | })
80 |
81 | return microViteConfig
82 | }
83 |
84 | module.exports = {
85 | ip,
86 | getConfig,
87 | }
88 |
--------------------------------------------------------------------------------
/modules/app/src/components/MicroApp.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect, memo, lazy } from 'react'
2 | import { loadMicroApp, FrameworkConfiguration, initGlobalState } from 'qiankun'
3 | import { useLocation } from 'react-router-dom'
4 |
5 | const Lazy = lazy(() => new Promise(() => null))
6 |
7 | const microAppEntryCache: any = {}
8 | const actions = initGlobalState({ hash: '' })
9 |
10 | function run(fn: any, ...params: any[]) {
11 | if (typeof fn === 'function') {
12 | return fn(...params)
13 | }
14 |
15 | return undefined
16 | }
17 |
18 | interface MicroAppProps extends FrameworkConfiguration {
19 | name: string
20 | entry?: string
21 | props?: any
22 | [key: string]: any
23 | }
24 |
25 | let prevAppUnmountPromise: Promise = Promise.resolve()
26 |
27 | // https://qiankun.umijs.org/zh/api#loadmicroappapp-configuration
28 | const MicroApp = memo(function MicroApp({
29 | name,
30 | entry,
31 | sandbox = true,
32 | props = {},
33 | }: MicroAppProps) {
34 | const location = useLocation()
35 | const [ready, setReady] = useState(false)
36 | const microApp = useRef()
37 | const container = useRef()
38 |
39 | // const { account } = useAccount()
40 | // const { locale } = useAppConfig()
41 |
42 | useEffect(() => {
43 | // debugger
44 | async function mount() {
45 | // debugger
46 | await prevAppUnmountPromise
47 | // debugger
48 | window[name as any] = microAppEntryCache[name] ?? window[name as any]
49 |
50 | microApp.current = loadMicroApp(
51 | {
52 | name,
53 | entry: entry!,
54 | container: container.current,
55 | props,
56 | },
57 | {
58 | sandbox,
59 | }
60 | )
61 |
62 | microApp.current.mountPromise.then(() => {
63 | if (window[name as any]) {
64 | microAppEntryCache[name] = window[name as any]
65 | }
66 |
67 | setReady(true)
68 | })
69 | }
70 | mount()
71 |
72 | return () => {
73 | // debugger
74 | prevAppUnmountPromise = Promise.resolve(run(microApp.current?.unmount)).then(
75 | () => {
76 | // debugger
77 | }
78 | )
79 | }
80 | }, [])
81 |
82 | useEffect(() => {
83 | if (!microApp.current) {
84 | return
85 | }
86 | run(microApp.current?.update, {
87 | container: container.current,
88 | props: {
89 | ...props,
90 | location,
91 | },
92 | })
93 | }, [location, Object.values(props)])
94 |
95 | useEffect(() => {
96 | actions.setGlobalState(location)
97 | }, [location])
98 |
99 | return (
100 | <>
101 |
102 | {!ready && }
103 | >
104 | )
105 | })
106 |
107 | export default function MicroAppWrapper(props: MicroAppProps) {
108 | return
109 | }
110 |
--------------------------------------------------------------------------------
/modules/app/src/pages/login/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useNavigate } from 'react-router-dom'
3 | import { Form, Input, Button, Space, message, Avatar } from 'antd'
4 | import { LockOutlined, UserOutlined } from '@ant-design/icons'
5 | import useAccount from '@packages/shared/hooks/useAccount'
6 |
7 | import styles from './style.module.scss'
8 |
9 | export default function Login() {
10 | const [form] = Form.useForm()
11 | const { setAccount } = useAccount()
12 | const navigate = useNavigate()
13 |
14 | return (
15 |
16 |
17 |
22 |
23 |
24 | Mono Micro Project
25 |
26 |
27 |
95 |
96 |
97 | )
98 | }
99 |
--------------------------------------------------------------------------------
/modules/app/src/routes/config.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import BlankLayout from '@packages/shared/layouts/Blank'
3 | import Lazy from '@packages/shared/components/Lazy'
4 | import useAccount from '@packages/shared/hooks/useAccount'
5 | import useAppConfig from '@packages/shared/hooks/useAppConfig'
6 |
7 | import MicroApp from '@/components/MicroApp'
8 | import CommonLayout from '@/layouts/Common'
9 | import Home from '@/pages/home'
10 | import { Navigate } from 'react-router-dom'
11 |
12 | export default function useRoutesConfig() {
13 | const { account } = useAccount()
14 | const { locale } = useAppConfig()
15 |
16 | const configs: any[] = [
17 | {
18 | element: ,
19 | children: [
20 | {
21 | children: [
22 | {
23 | index: true,
24 | title: '首页',
25 | element: ,
26 | menu: true,
27 | },
28 | {
29 | path: 'any',
30 | title: '404 页',
31 | element: ,
32 | menu: true,
33 | },
34 | { path: '*', element: '404' },
35 | ],
36 | },
37 | {
38 | path: 'micro',
39 | children: [
40 | {
41 | menu: true,
42 | title: 'demo-react-1',
43 | path: 'demo-react-1',
44 | element: (
45 |
55 | ),
56 | },
57 | {
58 | menu: true,
59 | title: 'demo-vue-1',
60 | path: 'demo-vue-1',
61 | element: (
62 |
72 | ),
73 | },
74 | {
75 | menu: true,
76 | title: 'demo-react-2',
77 | path: 'demo-react-2',
78 | element: (
79 |
89 | ),
90 | },
91 | {
92 | menu: true,
93 | title: 'demo-vue-2',
94 | path: 'demo-vue-2',
95 | element: (
96 |
106 | ),
107 | },
108 | ],
109 | },
110 | ],
111 | },
112 | {
113 | element: ,
114 | children: [
115 | {
116 | path: 'login',
117 | element: import('@/pages/login')} />,
118 | },
119 | { path: '*', element: '404' },
120 | ],
121 | },
122 | ]
123 | return configs
124 | }
125 |
--------------------------------------------------------------------------------
/modules/app/src/layouts/Common/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo, useState, Suspense } from 'react'
2 | import {
3 | Outlet,
4 | useLocation,
5 | useMatch,
6 | useNavigate,
7 | useOutlet,
8 | matchRoutes,
9 | // useOutletContext
10 | } from 'react-router-dom'
11 | import { flatten } from 'lodash'
12 | import {
13 | Menu,
14 | Layout,
15 | Dropdown,
16 | Spin,
17 | Avatar,
18 | Space,
19 | Modal,
20 | message,
21 | } from 'antd'
22 | import {
23 | MenuUnfoldOutlined,
24 | MenuFoldOutlined,
25 | UserOutlined,
26 | VideoCameraOutlined,
27 | UploadOutlined,
28 | GlobalOutlined,
29 | LogoutOutlined,
30 | LockOutlined,
31 | } from '@ant-design/icons'
32 |
33 | import useRoutesConfig from '@/routes/config'
34 | import useAppConfig from '@packages/shared/hooks/useAppConfig'
35 | import useAccount from '@packages/shared/hooks/useAccount'
36 |
37 | import './style.scss'
38 |
39 | const { Header, Sider, Content } = Layout
40 |
41 | const languageLabels: any = {
42 | 'zh-CN': '简体中文',
43 | 'en-US': 'English',
44 | }
45 |
46 | export default function CommonLayout() {
47 | const navigate = useNavigate()
48 | const { account } = useAccount()
49 | const { locale, setLocale } = useAppConfig()
50 | const configs = useRoutesConfig()
51 | const [collapsed, setCollapsed] = useState(false)
52 |
53 | const menuConfig = useMemo(() => {
54 | const targetConfig = configs.find((item: any) => {
55 | return item?.element?.type?.displayName === 'CommonLayout'
56 | })
57 |
58 | return filterNav(targetConfig?.children ?? [])
59 | }, [configs])
60 |
61 | // @ts-ignore
62 | window.navigate = navigate
63 |
64 | return (
65 |
66 |
67 |
76 |
77 |
91 |
92 |
165 |
170 |
171 | }>
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 | )
180 | }
181 |
182 | CommonLayout.displayName = 'CommonLayout'
183 |
184 | function filterNav(list: any[], parents: any[] = []): any[] {
185 | return flatten(
186 | [...list]
187 | .map(({ ...item }: any) => {
188 | if (Array.isArray(item.children)) {
189 | item.children = filterNav(item.children, [...parents, item])
190 |
191 | if (item.children.length === 0) {
192 | return undefined
193 | }
194 |
195 | if (item.children.length === 1) {
196 | return item.children[0]
197 | }
198 |
199 | if (!(item.label ?? item.title)) {
200 | return item.children
201 | }
202 | } else {
203 | if (!item.menu) {
204 | return undefined
205 | }
206 | }
207 |
208 | return item
209 | })
210 | .flat(Infinity)
211 | )
212 | .filter(Boolean)
213 | .map(({ ...item }: any) => {
214 | if (Array.isArray(item.children)) {
215 | item.key = item.key ?? item.path
216 | delete item.path
217 | } else {
218 | item.key =
219 | item.key ??
220 | `/${[...parents, item]
221 | .map((item) => item.path)
222 | .filter(Boolean)
223 | .join('/')
224 | .replace(/\/\//g, '/')}`
225 | }
226 |
227 | item.label = item.label ?? item.title
228 |
229 | delete item.title
230 | delete item.index
231 | delete item.element
232 | delete item.menu
233 |
234 | return item
235 | })
236 | }
237 |
--------------------------------------------------------------------------------