Compare commits

...

10 Commits

Author SHA1 Message Date
9341e46594 首次提交 2025-08-14 09:14:55 +08:00
Antony Budianto
48167c6b62
chore: update dep (#2) 2025-02-18 07:40:06 +07:00
Antony Budianto
a829fbc089
chore: update dependency (#1)
* chore: update dependency

* chore: adjust base
2025-01-22 09:46:08 +07:00
Antony Budianto
de425bd9e5 init pinia 2025-01-03 17:37:54 +07:00
antony
b0221e1d21 chore: bump nuxt 3.15 2024-12-25 17:15:18 +07:00
Antony Budianto
636db6b70e
Update README.md 2024-12-23 18:43:20 +07:00
antony
d5fa3e0820 docs: add link 2024-12-23 18:41:45 +07:00
antony
68aca2f93d feat: add links 2024-12-23 18:25:50 +07:00
antony
5e7ac295af docs: update 2024-12-23 16:59:23 +07:00
Antony Budianto
f9566b416a
Update README.md 2024-12-23 16:39:48 +07:00
19 changed files with 2341 additions and 1585 deletions

View File

@ -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)

View File

@ -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>

View File

@ -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
} }

View File

@ -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"
} }
} }

View File

@ -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>

View File

@ -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
View 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)
})

File diff suppressed because it is too large Load Diff

24
host/stores/counter.ts Normal file
View 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
View File

@ -0,0 +1 @@
# Env

View File

@ -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 {

View 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
View 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();
}
});

View File

@ -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
} }

View File

@ -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"
} }
} }

View File

@ -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 {

View File

@ -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
View 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
}
})