Vue.js vs Next.js:模态框路由 —— 并排拆解
原文链接:dev.to/heba_allah/…
原文作者:Heba Allah Hashim
让我们并排拆解这两个框架,以便理解这种巨大的思维转变。
项目 1:Vue.js 做法(代码驱动逻辑)
步骤 1:初始化项目
npm create vite@latest vue-modal-app -- --template vue
cd vue-modal-app
npm install vue-router@4
步骤 2:配置路由路径(src/router.js)
import { createRouter, createWebHistory } from 'vue-router'
import GalleryView from './views/GalleryView.vue'
export const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: GalleryView,
children: [
{
path: 'photo/:id',
component: () => import('./components/PhotoModal.vue'),
props: true
}
]
}
]
})
步骤 3:把路由器接到应用上
更新应用入口文件 src/main.js:
import { createApp } from 'vue'
import App from './App.vue'
import { router } from './router'
createApp(App).use(router).mount('#app')
替换根组件文件 src/App.vue:
<template>
<router-view />
</template>
步骤 4:创建图库列表页(src/views/GalleryView.vue)
<template>
<div style="padding: 20px; font-family: sans-serif;">
<h1>Vue Photo Gallery</h1>
<div style="display: flex; gap: 20px;">
<div
v-for="photoId in ['sunset', 'ocean', 'mountains']"
:key="photoId"
@click="openPhoto(photoId)"
style="width: 150px; height: 100px; background: #3b82f6; color: white; display: flex; align-items: center; justify-content: center; cursor: pointer; border-radius: 8px;"
>
View {{ photoId }}
</div>
</div>
<!-- 在网格上方渲染子级弹层组件 -->
<router-view v-if="showModal" @close="closeModal" />
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const showModal = ref(false)
// 关键:若硬刷新时 URL 中仍有 ID,Watcher 会强制打开模态框
watch(
() => route.params.id,
(newId) => {
showModal.value = !!newId
},
{ immediate: true }
)
function openPhoto(id) {
router.push(`/photo/${id}`)
}
function closeModal() {
router.push('/')
}
</script>
步骤 5:创建弹层模态框组件(src/components/PhotoModal.vue)
<template>
<div style="position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 9999;">
<div style="background: white; padding: 30px; border-radius: 12px; text-align: center; width: 300px;">
<h2>Vue Modal Layout</h2>
<p style="font-size: 24px; font-weight: bold; text-transform: uppercase; color: #3b82f6;">
🌌 {{ id }}
</p>
<button @click="$emit('close')" style="margin-top: 20px; padding: 8px 16px; cursor: pointer;">Close Overlay</button>
</div>
</div>
</template>
<script setup>
defineProps({ id: String })
defineEmits(['close'])
</script>
通过 npm run dev 运行,即可查看正在运行的应用。
项目 2:Next.js 做法(文件夹驱动架构)
步骤 1:初始化项目
# 配置选项:TypeScript(是)、ESLint(否)、src/(否)、App Router(是)
npx create-next-app@latest next-modal-app
cd next-modal-app
步骤 2:创建文件文件夹
在编辑器的文件侧栏中,手动创建与上方 Hierarchy Map 完全一致的文件夹,以避免系统转义错误。
步骤 3:搭建主图库页(app/gallery/page.tsx)
import Link from 'next/link';
export default function GalleryPage() {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>Next.js Photo Gallery</h1>
<div style={{ display: 'flex', gap: '20px' }}>
{['sunset', 'ocean', 'mountains'].map((photoId) => (
<Link
key={photoId}
href={`/gallery/photo/${photoId}`}
style={{
width: '150px',
height: '100px',
background: '#ef4444',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textDecoration: 'none',
borderRadius: '8px'
}}
>
View {photoId}
</Link>
))}
</div>
</div>
);
}
步骤 4:配置布局插槽(app/gallery/layout.tsx)
export default function GalleryLayout({
children,
modal
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<div>
{children}
{modal}
</div>
);
}
步骤 5:创建插槽回退(app/gallery/@modal/default.tsx)
export default function DefaultModal() {
// 通过返回基线 null,修复未挂载插槽不匹配导致的崩溃
return null;
}
步骤 6:创建拦截式弹层视图 (app/gallery/@modal/(.)photo/[id]/page.tsx)
'use client';
import { useRouter, useParams } from 'next/navigation';
export default function InterceptedPhotoPopup() {
const router = useRouter();
const params = useParams();
return (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9999 }}>
<div style={{ background: 'white', padding: '30px', borderRadius: '12px', textAlign: 'center', width: '300px' }}>
<h2>Next.js Intercepted Popup</h2>
<p style={{ fontSize: '24px', fontWeight: 'bold', textTransform: 'uppercase', color: '#ef4444' }}>
📸 {params.id}
</p>
<button onClick={() => router.back()} style={{ marginTop: '20px', padding: '8px 16px', cursor: 'pointer' }}>
Close Overlay
</button>
</div>
</div>
);
}
步骤 7:创建独立回退页 (app/gallery/photo/[id]/page.tsx)
import Link from 'next/link';
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function StandalonePhotoPage({ params }: PageProps) {
// 现代 Next.js 规则:params 必须异步解包
const { id } = await params;
return (
<div style={{ padding: '40px', fontFamily: 'sans-serif', textAlign: 'center' }}>
<h1>Standalone Full-Screen View</h1>
<div style={{ margin: '40px auto', width: '400px', height: '250px', background: '#ef4444', color: 'white', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '32px', borderRadius: '12px' }}>
🌌 {id.toUpperCase()}
</div>
<Link href="/gallery" style={{ color: '#ef4444' }}>← Back to Gallery List</Link>
</div>
);
}
通过 npm run dev 运行,并在浏览器中打开 http://localhost:3000/gallery。