Compare commits
10 Commits
248eba5975
...
9341e46594
Author | SHA1 | Date | |
---|---|---|---|
9341e46594 | |||
![]() |
48167c6b62 | ||
![]() |
a829fbc089 | ||
![]() |
de425bd9e5 | ||
![]() |
b0221e1d21 | ||
![]() |
636db6b70e | ||
![]() |
d5fa3e0820 | ||
![]() |
68aca2f93d | ||
![]() |
5e7ac295af | ||
![]() |
f9566b416a |
10
README.md
10
README.md
@ -17,6 +17,8 @@ cp .env.example .env
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
First, we need to build the remote first. Check [this](https://github.com/originjs/vite-plugin-federation/issues/525) for why.
|
||||||
|
|
||||||
```
|
```
|
||||||
cd remote
|
cd remote
|
||||||
pnpm i
|
pnpm i
|
||||||
@ -46,8 +48,16 @@ pnpm build
|
|||||||
pnpm serve
|
pnpm serve
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- It's recommended to split the repository for Host and Remote, and deploy separately
|
||||||
|
- If you want to go with monorepo approach, make sure to setup a monorepo package manager (Pnpm workspace, Nx, etc.)
|
||||||
|
- This repo is still evolving, and might not be production-ready yet
|
||||||
|
- No SSR support (no plan for this)
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
|
- Pinia integration
|
||||||
- Build-mode works but Dev-mode not works (fixed)
|
- Build-mode works but Dev-mode not works (fixed)
|
||||||
- Enable CORS on remote JS assets (fixed)
|
- Enable CORS on remote JS assets (fixed)
|
||||||
- Scoped style still not works (fixed)
|
- Scoped style still not works (fixed)
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { NuxtError } from "#app"
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
error: Object as () => NuxtError
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<h1>Ooops... Something went wrong...</h1>
|
|
||||||
<NuxtLink to="/">Go back home</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
@ -14,11 +14,17 @@ export default defineNuxtConfig({
|
|||||||
vite: {
|
vite: {
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
"^/node_modules/.*": {
|
// "^/node_modules/.*": {
|
||||||
target: "http://localhost:3000",
|
// target: "http://localhost:3000",
|
||||||
|
// changeOrigin: true,
|
||||||
|
// rewrite: (path) =>
|
||||||
|
// path.replace(/^\/node_modules\//, "/_nuxt/node_modules/")
|
||||||
|
// },
|
||||||
|
"^/remote/*": {
|
||||||
|
target: "http://localhost:3005",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) =>
|
rewrite: (path) =>
|
||||||
path.replace(/^\/node_modules\//, "/_nuxt/node_modules/")
|
path.replace(/^\/remote\//, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -33,7 +39,7 @@ export default defineNuxtConfig({
|
|||||||
remotes: {
|
remotes: {
|
||||||
remote: `${MFE_HOST}/_nuxt/remoteEntry.js`
|
remote: `${MFE_HOST}/_nuxt/remoteEntry.js`
|
||||||
},
|
},
|
||||||
shared: ["vue"]
|
shared: ["vue", "pinia"]
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -41,6 +47,7 @@ export default defineNuxtConfig({
|
|||||||
plugins: []
|
plugins: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
plugins: ["~/plugins/pinia"],
|
||||||
experimental: {
|
experimental: {
|
||||||
asyncEntry: true
|
asyncEntry: true
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
"dev": " nuxt dev",
|
"dev": " nuxt dev --host",
|
||||||
"prebuild": "npm run clean",
|
"prebuild": "npm run clean",
|
||||||
"pregenerate": "npm run clean",
|
"pregenerate": "npm run clean",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
@ -15,13 +15,15 @@
|
|||||||
"serve": "serve .output/public -p 3000 --single"
|
"serve": "serve .output/public -p 3000 --single"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nuxt": "^3.13.0",
|
"@pinia/nuxt": "^0.11.2",
|
||||||
|
"nuxt": "^4.0.3",
|
||||||
|
"pinia": "^3.0.3",
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@originjs/vite-plugin-federation": "^1.3.5",
|
"@originjs/vite-plugin-federation": "^1.4.1",
|
||||||
"serve": "~14.2.4",
|
|
||||||
"rimraf": "6",
|
"rimraf": "6",
|
||||||
"vite-plugin-top-level-await": "~1.4.4"
|
"serve": "~14.2.4",
|
||||||
|
"vite-plugin-top-level-await": "^1.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="grid grid-cols-1 gap-4">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<template #default>
|
<template #default>
|
||||||
<RemoteContactRouter />
|
<RemoteContactRouter label="propsFromHost" @increment="handleLog" />
|
||||||
</template>
|
</template>
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<div>Loading remote component...</div>
|
<div>Loading remote component...</div>
|
||||||
@ -14,12 +14,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { defineAsyncComponent } from "vue"
|
import {
|
||||||
|
__federation_method_getRemote as getRemote,
|
||||||
|
__federation_method_setRemote as setRemote,
|
||||||
|
__federation_method_unwrapDefault as unwrapModule,
|
||||||
|
type IRemoteConfig
|
||||||
|
} from "virtual:__federation__";
|
||||||
|
|
||||||
const RemoteContactRouter = defineAsyncComponent(() =>
|
// 公共的 remote 配置
|
||||||
import("remote/RemoteContactRouter")
|
const commonRemoteConfig: Partial<IRemoteConfig> = {
|
||||||
)
|
format: "esm", // 取决于你的 remoteEntry.js 构建方式
|
||||||
|
from: "vite"
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadRemoteContactRouter = async () => {
|
||||||
|
// 运行时注册 remote
|
||||||
|
setRemote("remote", {
|
||||||
|
...commonRemoteConfig,
|
||||||
|
url: "http://localhost:3003/remote/_nuxt/remoteEntry.js"
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取远程模块
|
||||||
|
const remoteModule = await getRemote("remote", "./RemoteContactRouter");
|
||||||
|
|
||||||
|
// 解包 default 导出。
|
||||||
|
// 如果远程模块是命名导出,例如 `export const RemoteContactRouter = ...`
|
||||||
|
// 那么你应该使用 `return remoteModule.RemoteContactRouter;`
|
||||||
|
return await unwrapModule(remoteModule);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. 使用 defineAsyncComponent 来加载组件
|
||||||
|
const RemoteContactRouter = defineAsyncComponent(loadRemoteContactRouter);
|
||||||
|
|
||||||
|
const handleLog = (count: number) => {
|
||||||
|
console.log("count: ", count);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
@ -1,3 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>Index page</div>
|
<div>
|
||||||
|
Index page:
|
||||||
|
<p>Count: {{ counter.$state.count }}</p>
|
||||||
|
<button type="button" @click="() => counter.increment()">
|
||||||
|
increment+ pinia
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useCounter } from "@/stores/counter"
|
||||||
|
const counter = useCounter()
|
||||||
|
</script>
|
||||||
|
9
host/plugins/pinia.ts
Normal file
9
host/plugins/pinia.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { createPinia } from "pinia"
|
||||||
|
import { defineNuxtPlugin } from "#app"
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
const pinia = createPinia()
|
||||||
|
// @ts-expect-error need global
|
||||||
|
window.__mfeHostPinia = pinia
|
||||||
|
nuxtApp.vueApp.use(pinia)
|
||||||
|
})
|
1819
host/pnpm-lock.yaml
1819
host/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
24
host/stores/counter.ts
Normal file
24
host/stores/counter.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { defineStore } from "pinia"
|
||||||
|
|
||||||
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
|
||||||
|
export const useCounter = defineStore("counter", {
|
||||||
|
state: () => ({
|
||||||
|
count: 100
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
increment() {
|
||||||
|
this.count += 1
|
||||||
|
},
|
||||||
|
|
||||||
|
async asyncIncrement() {
|
||||||
|
console.log("asyncIncrement called")
|
||||||
|
await sleep(300)
|
||||||
|
this.count++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
getCount: (state) => state.count
|
||||||
|
}
|
||||||
|
})
|
1
remote/.env.example
Normal file
1
remote/.env.example
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Env
|
@ -1,24 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<component v-if="asyncPageComponent" :is="asyncPageComponent" />
|
<p>Label:{{ label }}</p>
|
||||||
|
<component
|
||||||
|
v-if="asyncPageComponent"
|
||||||
|
:is="asyncPageComponent"
|
||||||
|
@increment="(v: number) => emit('increment', v)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { defineAsyncComponent } from "vue"
|
import { defineAsyncComponent } from "vue"
|
||||||
|
import { useHostPinia } from "~/composables/useHostPinia"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
label: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "increment", count: number): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
useHostPinia()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo
|
* @todo
|
||||||
* it'll be nice if there is a way to dynamically match route and render..
|
* it'll be nice if there is a way to dynamically match route and render..
|
||||||
*/
|
*/
|
||||||
console.log(">>", window.location.pathname)
|
|
||||||
|
|
||||||
const Index = defineAsyncComponent(() => import("@/pages/contact/index.vue"))
|
const Index = defineAsyncComponent(() => import("@/pages/contact/index.vue"))
|
||||||
const Properties = defineAsyncComponent(() =>
|
const Properties = defineAsyncComponent(
|
||||||
import("@/pages/contact/properties.vue")
|
() => import("@/pages/contact/properties.vue")
|
||||||
)
|
)
|
||||||
|
|
||||||
let asyncPageComponent
|
let asyncPageComponent: unknown
|
||||||
if (/\/contact\/properties/.test(window.location.pathname)) {
|
if (/\/contact\/properties/.test(window.location.pathname)) {
|
||||||
asyncPageComponent = Properties
|
asyncPageComponent = Properties
|
||||||
} else {
|
} else {
|
||||||
|
10
remote/composables/useHostPinia.ts
Normal file
10
remote/composables/useHostPinia.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { onBeforeMount } from "vue"
|
||||||
|
import { setActivePinia } from "pinia"
|
||||||
|
|
||||||
|
export function useHostPinia() {
|
||||||
|
onBeforeMount(() => {
|
||||||
|
console.log("RMT: init pinia store from host: ")
|
||||||
|
// @ts-expect-error global pinia host
|
||||||
|
setActivePinia(window.__mfeHostPinia || createPinia())
|
||||||
|
})
|
||||||
|
}
|
17
remote/middleware/core.ts
Normal file
17
remote/middleware/core.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
export default defineEventHandler((event) => {
|
||||||
|
const allowedOrigin = 'http://localhost:3003'; // 替换为你的前端项目域名
|
||||||
|
|
||||||
|
|
||||||
|
console.log('event', event)
|
||||||
|
// 设置 CORS 响应头,允许来自特定源的请求
|
||||||
|
event.node.res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
|
||||||
|
event.node.res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||||
|
event.node.res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||||
|
|
||||||
|
// 如果请求是预检请求 (OPTIONS),则直接返回 204
|
||||||
|
if (event.node.req.method === 'OPTIONS') {
|
||||||
|
event.node.res.writeHead(204);
|
||||||
|
event.node.res.end();
|
||||||
|
}
|
||||||
|
});
|
@ -5,12 +5,14 @@ import topLevelAwait from "vite-plugin-top-level-await"
|
|||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: "2024-04-03",
|
compatibilityDate: "2024-04-03",
|
||||||
devtools: { enabled: false },
|
devtools: { enabled: false },
|
||||||
ssr: false,
|
ssr: true,
|
||||||
nitro: {
|
nitro: {
|
||||||
preset: "static"
|
// preset: "static",
|
||||||
},
|
},
|
||||||
vite: {
|
vite: {
|
||||||
|
|
||||||
$client: {
|
$client: {
|
||||||
|
base: "/",
|
||||||
plugins: [
|
plugins: [
|
||||||
topLevelAwait({
|
topLevelAwait({
|
||||||
promiseExportName: "__tla",
|
promiseExportName: "__tla",
|
||||||
@ -22,19 +24,20 @@ export default defineNuxtConfig({
|
|||||||
exposes: {
|
exposes: {
|
||||||
"./RemoteContactRouter": "./components/RemoteContactRouter.vue"
|
"./RemoteContactRouter": "./components/RemoteContactRouter.vue"
|
||||||
},
|
},
|
||||||
// shared: []
|
shared: ["vue", "pinia"]
|
||||||
shared: ["vue"]
|
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
$server: {
|
$server: {
|
||||||
plugins: []
|
plugins: [
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
// build: {
|
// build: {
|
||||||
// target: "esnext"
|
// target: "esnext"
|
||||||
// }
|
// }
|
||||||
},
|
},
|
||||||
|
modules: ["@pinia/nuxt"],
|
||||||
experimental: {
|
experimental: {
|
||||||
asyncEntry: true
|
asyncEntry: true
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
"dev": "HOST=0.0.0.0 PORT=3001 nuxt dev",
|
"dev": "nuxt dev --host",
|
||||||
"prebuild": "npm run clean",
|
"prebuild": "npm run clean",
|
||||||
"pregenerate": "npm run clean",
|
"pregenerate": "npm run clean",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
@ -12,16 +12,18 @@
|
|||||||
"postinstall": "nuxt prepare",
|
"postinstall": "nuxt prepare",
|
||||||
"clean": "rimraf .output dist",
|
"clean": "rimraf .output dist",
|
||||||
"clean2": "rimraf --glob node_modules",
|
"clean2": "rimraf --glob node_modules",
|
||||||
"serve": "serve .output/public -p 3001 --cors --single"
|
"serve": "PORT=3005 node .output/server/index.mjs --cors --single"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nuxt": "^3.13.0",
|
"@pinia/nuxt": "^0.11.2",
|
||||||
|
"nuxt": "^4.0.3",
|
||||||
|
"pinia": "^3.0.3",
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@originjs/vite-plugin-federation": "^1.3.5",
|
"@originjs/vite-plugin-federation": "^1.4.1",
|
||||||
"serve": "~14.2.4",
|
|
||||||
"rimraf": "6",
|
"rimraf": "6",
|
||||||
"vite-plugin-top-level-await": "~1.4.4"
|
"serve": "~14.2.4",
|
||||||
|
"vite-plugin-top-level-await": "^1.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,27 @@
|
|||||||
<h1>rmt/pages/contact/index.vue</h1>
|
<h1>rmt/pages/contact/index.vue</h1>
|
||||||
<div>Contact index</div>
|
<div>Contact index</div>
|
||||||
<div>
|
<div>
|
||||||
count: {{ count }}
|
count: {{ count }} / counterFromPinia: {{ counter.$state.count }} <br />
|
||||||
<button type="button" @click="count++">increment+</button>
|
<button type="button" @click="count++">increment+ local</button>
|
||||||
|
<button type="button" @click="handleIncrementPinia">
|
||||||
|
increment+ pinia
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
|
import { useCounter } from "@/stores/counter"
|
||||||
|
const counter = useCounter()
|
||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "increment", count: number): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const handleIncrementPinia = () => {
|
||||||
|
counter.increment()
|
||||||
|
emit("increment", counter.$state.count)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rmt {
|
.rmt {
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="rmt2">
|
||||||
<h1>rmt/pages/contact/properties.vue</h1>
|
<h1>rmt/pages/contact/properties.vue</h1>
|
||||||
<div>Contact prop</div>
|
<div>Contact prop</div>
|
||||||
<div>
|
<div>
|
||||||
<button type="button" @click="handleClick">Click: {{ counter }}</button>
|
<button type="button" @click="handleClick">Click: {{ counter }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="links">
|
||||||
|
<NuxtLink to="/">Go to home</NuxtLink>
|
||||||
|
<NuxtLink to="/about">About</NuxtLink>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -18,3 +22,13 @@ definePageMeta({
|
|||||||
name: "contact-properties"
|
name: "contact-properties"
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rmt2 {
|
||||||
|
background: pink;
|
||||||
|
}
|
||||||
|
.links {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
File diff suppressed because it is too large
Load Diff
24
remote/stores/counter.ts
Normal file
24
remote/stores/counter.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { defineStore } from "pinia"
|
||||||
|
|
||||||
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
|
||||||
|
export const useCounter = defineStore("counter", {
|
||||||
|
state: () => ({
|
||||||
|
count: 999
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
increment() {
|
||||||
|
this.count += 1
|
||||||
|
},
|
||||||
|
|
||||||
|
async asyncIncrement() {
|
||||||
|
console.log("asyncIncrement called")
|
||||||
|
await sleep(300)
|
||||||
|
this.count++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
getCount: (state) => state.count
|
||||||
|
}
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user