main
- main.ts
import { createApp } from "vue"
import App from "./App"
import router from "./router"
import "@/micro"
import { store } from "@/store"
createApp(App).use(router).use(store).mount("#app")
- micro/app.ts
import actions from "@/shared/action"
import { store } from "@/store"
const getActiveRule = (hash: string) => (location: { hash: string }) =>
location.hash.startsWith(hash)
const microApps: {
name: string
entry: string
activeRule: string
}[] = [
{
name: "subapp",
entry: "http://127.0.0.1:8082",
activeRule: "#/subapp",
},
]
export const apps = microApps.map((item) => {
return {
...item,
container: "#subapp", // 子应用挂载的div
activeRule: getActiveRule(item.activeRule),
props: {
routerBase: item.activeRule, // 下发基础路由
// getGlobalState: actions.getGlobalState, // 下发getGlobalState方法
store,
},
}
})
- 注意上面的name 和activeRule 必须和 router 里的一级路由的path 和name 保持一致
{
name: "subapp",
entry: "http://127.0.0.1:8082",
activeRule: "#/subapp",
},
正确用法
// 主应用
{
path: "/subapp", // path 和 activeRule 保持一致
name: "subapp", // name 和上面的name保持一致
children: [{
{
path: "/subapp/a",
...
}
}]
}
错误用法
{
path: "/test", // path 和 activeRule 不一样
name: "test", // name 和上面的name不一样
children: [{
{
path: "/test/a",
...
}
}]
}
- micro/index.ts
import {
addGlobalUncaughtErrorHandler,
registerMicroApps,
start,
} from "qiankun"
import Nprogress from "nprogress"
import "nprogress/nprogress.css"
import { ElLoading } from "element-plus"
import { apps } from "./app"
let loadingInstance: ReturnType<typeof ElLoading.service>
registerMicroApps(apps, {
beforeLoad: (app) => {
Nprogress.start()
loadingInstance = ElLoading.service()
console.log("before load", app.name)
return Promise.resolve()
},
afterMount: (app) => {
Nprogress.done()
loadingInstance.close()
console.log("after mount", app.name)
return Promise.resolve()
},
})
addGlobalUncaughtErrorHandler(() => {
loadingInstance.close()
console.log("子应用加载失败,请检查应用是否可运行")
})
export { start }
- router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
export const routes: RouteRecordRaw[] = [
// 基座应用自己的路由
{
path: '/login',
component: () => import('../views/Login'),
},
{
path: "",
component: () => import('../views/Layout'),
redirect: "/home",
children: [
{
path: "/home",
name: "首页",
component: () => import('../views/Home'),
},
{
path: "/subapp",
component: () => import('../views/SubApp'),
name: "子应用",
children: [
{
path: "/subapp/child1",
name: "child1",
// 这里的二级菜单组件是没有意义的,但是 ts 必须要传一个 component,所以随便写一个就行,为了解决 ts 报错
component: import("../views/Portal")
},
{
path: "/subapp/child2",
name: "child2",
component: import("../views/Portal")
}
]
}
]
},
];
const router = createRouter({
history: createWebHashHistory(),
routes,
});
export default router;
- store/index.ts
import actions from "@/shared/action"
import { createStore } from "vuex"
export const store = createStore({
state() {
return {
token: "111",
name: "",
}
},
mutations: {
setToken(state, token) {
state.token = token
actions.setGlobalState({ ...state, token })
actions.offGlobalStateChange()
},
setName(state, name) {
state.name = name
},
},
})
- App.tsx
import { defineComponent } from "vue"
import "element-plus/dist/index.css"
import "./App.less"
import { useStore } from "vuex"
const App = defineComponent({
setup: (props, context) => {
const store = useStore()
return () => (
<>
<div>token: {store.state.token}</div>
<router-view />
</>
)
},
})
export default App
这里需要注意如果你的子应用是二级菜单那么你的容器 id,必须在根 Layout 组件或者一级路由组件里声明,而且容器id和子应用调用必须写在同一个组件里,否则打包后无效,因为我们可能有多个子应用,所以我直接在 Layout 里写,这样就避免我每次都要写一遍容器id和调用一次start
- views/Layout
import { defineComponent, onMounted } from "vue"
import { ElContainer, ElAside, ElMain, ElHeader } from "element-plus"
import s from "./index.module.less"
import { SlideBar } from "../../components/SliderBar"
import { start } from "qiankun"
import { store } from "@/store"
const Layout = defineComponent({
setup: (props, context) => {
const handleClick = () => {
store.commit("setToken", "2222")
}
onMounted(() => {
if (!window.qiankunStarted) {
window.qiankunStarted = true
start({
sandbox: {
experimentalStyleIsolation: true,
},
})
}
})
return () => (
<div class={[s.layoutWrapper, "common-layout"]} onClick={handleClick}>
<ElContainer>
<ElHeader class={s.header}>header</ElHeader>
<ElContainer>
<ElAside width='240px' class={s.aside}>
<SlideBar />
</ElAside>
<ElMain class={s.main}>
<div id="subapp" />
<router-view />
</ElMain>
</ElContainer>
</ElContainer>
</div>
)
},
})
export default Layout
- SlideBar/index.tsx
import { ElMenu, ElMenuItem, ElSubMenu } from 'element-plus';
import { defineComponent } from 'vue';
import { RouteRecordRaw } from 'vue-router';
import { routes } from '../../router';
import s from './index.module.less'
export const SlideBar = defineComponent({
setup: (props, context) => {
const filterRoutes = routes.filter((route)=> route.path === "")[0]?.children
const renderItem = (route: RouteRecordRaw) => {
return (
<>
{route.path ? (
<ElMenuItem index={route.path}>{route.name}</ElMenuItem>
) : null}
</>
)
}
return () => (
<div class={s.slideBarWrapper}>
<ElMenu
activeTextColor='#ffd04b'
backgroundColor='#545c64'
class={"el-menu-vertical-demo"}
textColor="#fff"
router
>
{
filterRoutes!.map(route => (
<>
{
route.children && !!route.children.length ? (
<ElSubMenu index={route.path}
v-slots={
{
title: () => (<span>{route.name}</span>)
}
}
>
{
route.children.map(child => renderItem(child))
}
</ElSubMenu>
) : renderItem(route)
}
</>
))
}
</ElMenu>
</div>
)
}
});
- views/Portal/index.tsx
import { start } from "@/micro"
import { defineComponent, onMounted } from "vue"
const Portal = defineComponent({
setup: (props, context) => {
return () => <div />
},
})
export default Portal
- views/SubApp/index.tsx
import { start } from 'qiankun';
import { defineComponent, onMounted } from 'vue';
const SubApp = defineComponent({
setup: (props, context) => {
return () => (
<>
<router-view />
</>
)
}
});
export default SubApp;
- vite.config.ts
import { defineConfig } from "vite"
import vue from "@vitejs/plugin-vue"
import vueJsx from "@vitejs/plugin-vue-jsx"
import { resolve } from "path"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx({
transformOn: true,
mergeProps: true,
}),
],
resolve: {
extensions: [".ts", ".tsx", ".json", ".js", ".vue"],
alias: {
"@": resolve("src"),
},
},
})
子应用
- main.ts
import { createPinia } from "pinia"
import {
renderWithQiankun,
qiankunWindow,
QiankunProps,
} from "vite-plugin-qiankun/dist/helper"
import { createApp, App as VueApp } from "vue"
import App from "./App"
import { router } from "./router"
import { useUserStore } from "./store/user"
let app: VueApp<Element>
const render = (props: QiankunProps | { container: string }) => {
const { container } = props
app = createApp(App)
app
.use(router)
.use(createPinia())
.mount(
container
? container instanceof HTMLElement
? container.querySelector("#app")!
: container
: document.getElementById("app")!
)
}
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
render({ container: "#app" })
} else {
renderWithQiankun({
mount(props) {
console.log(props, "sss")
const { store: parentStore, onGlobalStateChange, setGlobalState } = props
console.log(1)
onGlobalStateChange((state, prev) => {
console.log(state, prev)
store.setToken(state.token)
})
console.log("--mount")
render(props)
const store = useUserStore()
store.setToken(parentStore.state.token)
},
bootstrap() {
console.log("--bootstrap")
},
update() {
console.log("--update")
},
unmount() {
console.log("--unmount")
app?.unmount()
},
})
}
- vite.config.ts
iimport vueJsx from "@vitejs/plugin-vue-jsx"
import { defineConfig, loadEnv } from "vite"
import { resolve } from "path"
import vue from "@vitejs/plugin-vue"
import qiankun from "vite-plugin-qiankun"
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
// base: "http://localhost:3002/",
const root = process.cwd()
const env = loadEnv(mode, root)
const { VITE_PUBLIC_PATH, VITE_DROP_CONSOLE } = env
return {
base: mode !== "production" ? "/" : "http://127.0.0.1:8082/", // 生产环境需要指定域名
server: {
port: 8082,
cors: true,
headers: {
"Access-Control-Allow-Origin": "*"
},
hmr: true
},
resolve: {
extensions: [".ts", ".tsx", ".json", ".js", ".vue"],
alias: {
"@": resolve("src"),
},
},
plugins: [
vue(),
vueJsx({
transformOn: true,
mergeProps: true,
}),
qiankun("subapp", {
useDevMode: mode !== "production",
}),
],
}
})
- router
import { qiankunWindow } from "vite-plugin-qiankun/dist/helper"
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router"
const routes: RouteRecordRaw[] = [
{
path: "/subApp",
component: () => import("@/views/Layout"),
name: "Layout",
children: [
{
path: "a",
component: () => import("@/views/A"),
},
],
},
]
export const router = createRouter({
history: createWebHashHistory(
qiankunWindow.__POWERED_BY_QIANKUN__ ? "/subapp" : "/"
),
routes,
})