从Vue迁移到Nuxt实现服务端渲染:构建SEO友好的博客系统(二)

在上一篇文章从Vue迁移到Nuxt实现服务端渲染:构建SEO友好的博客系统(一)中,我们介绍了将Vue博客项目迁移到Nuxt的背景、动机和基础架构设置。本文将继续深入探讨迁移过程中的实际操作、遇到的问题及解决方案,以及组件迁移的最佳实践。
目录
- 组件迁移策略
- 状态管理与持久化
- 服务端渲染中的API调用
- TypeScript适配
- 样式与资源处理
- 性能优化
- SEO实践
1. 组件迁移策略
在迁移Vue组件到Nuxt环境时,我们采用了以下策略:
基础组件优先
从底层基础组件开始迁移,如SvgIcon
、Pagination
等,然后逐步迁移更复杂的组件。这种自下而上的方法使我们能够建立坚实的组件基础,再逐步构建更复杂的功能。
vue
<!-- packages/blog-nuxt/components/SvgIcon/index.vue -->
<template>
<svg aria-hidden="true" class="svg-icon" :width="size" :height="size">
<use :xlink:href="symbolId" :fill="color" />
</svg>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
prefix: {
type: String,
default: 'icon'
},
iconClass: {
type: String,
required: false
},
color: {
type: String,
},
size: {
type: String,
default: '1rem'
}
});
const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`);
</script>
组件重构
对于一些依赖第三方库的组件,我们选择重新实现而非直接迁移。例如,我们用纯HTML和CSS重新实现了原本依赖NaiveUI的Pagination
组件:
vue
<!-- packages/blog-nuxt/components/Pagination/index.vue -->
<template>
<div class="pagination">
<button
class="pagination-btn prev"
:disabled="currentPage <= 1"
@click="changePage(currentPage - 1)"
>
<svg-icon icon-class="angle-left"></svg-icon>
</button>
<!-- 页码按钮 -->
<!-- ... 省略部分代码 ... -->
<button
class="pagination-btn next"
:disabled="currentPage >= total"
@click="changePage(currentPage + 1)"
>
<svg-icon icon-class="angle-right"></svg-icon>
</button>
</div>
</template>
功能解耦
对于复杂组件,我们采用了功能解耦的方式,将其拆分为多个小组件。例如,Header
组件被拆分为Header
、NavBar
和Toggle
三个子组件,每个子组件负责特定的功能。
2. 状态管理与持久化
Nuxt对Pinia的支持非常友好,我们很容易将原有的状态管理迁移过来。
Store模块设计
我们将原有的状态存储拆分为三个核心模块:
- user.ts: 管理用户信息、登录状态和Token
- blog.ts: 管理博客配置、文章统计等全局博客信息
- app.ts: 管理应用状态,如主题、UI展示状态等
typescript
// packages/blog-nuxt/stores/user.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import Cookies from 'js-cookie';
export const useUserStore = defineStore('user', () => {
// 用户信息状态
const userInfo = ref<UserInfo | null>(null);
// Token状态
const token = ref<string | null>(null);
// 登录状态
const isLogin = computed(() => !!token.value);
// 其他方法...
return {
userInfo,
token,
isLogin,
// ...其他导出
};
});
状态持久化
在Vue项目中,我们使用localStorage进行状态持久化。而在Nuxt中,需要考虑服务端渲染环境下没有localStorage的问题。我们通过pinia-plugin-persistedstate插件和特殊处理解决了这个问题:
typescript
// packages/blog-nuxt/plugins/pinia-persist.ts
import { defineNuxtPlugin } from '#app';
import { createPersistedState } from 'pinia-plugin-persistedstate';
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.$pinia.use(createPersistedState({
storage: typeof window !== 'undefined' ? window.localStorage : undefined,
}));
});
处理服务端渲染中的状态同步
为了解决服务端和客户端状态不同步的问题,我们在应用启动时检查并初始化用户状态:
typescript
// packages/blog-nuxt/plugins/auth.ts
export default defineNuxtPlugin((nuxtApp) => {
// 在客户端初始化用户状态
if (process.client) {
const userStore = useUserStore();
userStore.initToken();
// 如果有token但没有用户信息,尝试获取用户信息
if (userStore.token && !userStore.userInfo) {
// 获取用户信息的逻辑
}
}
});
3. 服务端渲染中的API调用
Nuxt提供了多种方式处理API调用,我们主要使用useFetch
和useAsyncData
两个组合式函数。
页面数据获取
在页面组件中,我们使用useAsyncData
获取页面所需的数据:
vue
<script setup>
// 文章详情页
const route = useRoute();
const articleId = route.params.id;
// 获取文章详情
const { data: article } = await useAsyncData(
`article-${articleId}`,
() => $fetch(`/api/articles/${articleId}`)
);
// 文章不存在时重定向
if (!article.value) {
return navigateTo('/404');
}
// 设置SEO元数据
useHead({
title: article.value.title,
meta: [
{ name: 'description', content: article.value.summary },
{ property: 'og:title', content: article.value.title },
{ property: 'og:description', content: article.value.summary },
{ property: 'og:image', content: article.value.cover }
]
});
</script>
API代理配置
为了解决跨域问题,我们在nuxt.config.ts中配置了API代理:
typescript
// nuxt.config.ts
export default defineNuxtConfig({
// ...其他配置
nitro: {
devProxy: {
'/api': {
target: process.env.VITE_SERVICE_BASE_URL || 'http://localhost:3000',
changeOrigin: true,
prependPath: true
}
}
}
});
4. TypeScript适配
Nuxt对TypeScript的支持非常好,但在自动导入功能与TypeScript的集成上需要一些额外处理。
类型声明
为了让TypeScript识别Nuxt的自动导入,我们创建了类型声明文件:
typescript
// types/nuxt.d.ts
declare global {
// Nuxt Composables
const useAsyncData: any;
const useFetch: any;
const useHead: any;
const useNuxtApp: any;
const useRuntimeConfig: any;
const useSeoMeta: any;
// Vue Router
const useRoute: any;
const useRouter: any;
// Nuxt Helpers
const defineNuxtPlugin: any;
// Nuxt Types
namespace process {
const client: boolean;
const server: boolean;
}
}
export {};
tsconfig.json配置
我们还调整了tsconfig.json,让它继承Nuxt生成的配置:
json
{
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"types": ["node"],
"strict": true,
"skipLibCheck": true
}
}
5. 样式与资源处理
SCSS支持
在Nuxt中使用SCSS需要安装相关依赖并配置:
bash
pnpm add -D sass
然后在组件中直接使用:
vue
<style lang="scss" scoped>
.component {
&-title {
color: var(--color-primary);
&:hover {
text-decoration: underline;
}
}
}
</style>
CSS变量与主题系统优化
在迁移过程中,我们重新设计了CSS变量系统,解决了一些之前存在的问题。在原Vue项目中,我们混用了SASS变量和CSS变量:
scss
// 原Vue项目中的混合使用方式
$primary-color: #ed6ea0;
$text-color: #333;
:root {
--color-primary: #{$primary-color};
--text-color: #{$text-color};
// ...其他变量
}
[theme="dark"] {
--color-primary: darken($primary-color, 10%);
--text-color: #eee;
// ...暗色模式变量
}
在Nuxt项目中,我们完全改用CSS变量,简化了维护工作并提高了性能:
scss
// assets/styles/variables.css
:root {
--primary-color: #ed6ea0;
--secondary-color: #ec8c69;
--border-color: #eee;
--card-bg: #fff;
--bg-color: #f5f5f5;
--text-color: #333;
--grey-0: #fff;
--grey-1: #f5f5f5;
--header-text-color: #fff;
--nav-bg: rgba(255, 255, 255, 0.8);
// ...其他变量
}
html[theme="dark"] {
--primary-color: #ec8c69;
--secondary-color: #ed6ea0;
--border-color: #333;
--card-bg: #222;
--bg-color: #1a1a1a;
--text-color: #eee;
--grey-0: #1a1a1a;
--grey-1: #222;
--header-text-color: #eee;
--nav-bg: rgba(30, 30, 30, 0.8);
// ...暗色主题变量
}
主题切换与服务端渲染兼容
实现主题切换时,我们需要确保它在SSR环境中正常工作。我们使用VueUse的useDark
工具:
vue
<script setup>
import { useDark, useToggle } from '@vueuse/core';
import { useAppStore } from '~/composables/useStores';
const app = useAppStore();
const isDark = useDark({
selector: 'html',
attribute: 'theme',
valueDark: 'dark',
valueLight: 'light',
});
const toggle = useToggle(isDark);
// 同步主题状态到Pinia
watch(isDark, (value) => {
app.switchTheme(value ? 'dark' : 'light');
});
// 初始化时确保主题一致
onMounted(() => {
if (process.client) {
// 如果存储的主题与当前不一致,进行同步
if (app.theme === 'dark' && !isDark.value) {
isDark.value = true;
} else if (app.theme === 'light' && isDark.value) {
isDark.value = false;
}
}
});
</script>
这种方式解决了一个常见的SSR问题:服务端渲染的HTML可能使用默认主题,而客户端期望使用用户之前选择的主题,导致水合不匹配。
全局样式组织
我们重新组织了全局样式文件结构,使其更易于维护:
assets/styles/
├── main.scss # 主样式入口
├── variables.css # CSS变量
├── reset.scss # 样式重置
├── transitions.scss # 过渡动画
└── components/ # 组件共享样式
├── button.scss
├── card.scss
└── ...
在nuxt.config.ts中引入:
typescript
export default defineNuxtConfig({
// ...其他配置
css: [
'~/assets/styles/variables.css',
'~/assets/styles/reset.scss',
'~/assets/styles/main.scss',
'~/assets/styles/transitions.scss'
]
})
6. 性能优化
图片优化
使用Nuxt的图片模块优化图片加载:
bash
pnpm add -D @nuxt/image
vue
<template>
<NuxtImg
src="/images/blog-cover.jpg"
width="800"
height="400"
format="webp"
alt="博客封面"
loading="lazy"
/>
</template>
懒加载与代码分割
Nuxt默认提供了组件懒加载和自动代码分割功能:
vue
<template>
<!-- 自动懒加载 -->
<LazyArticleContent v-if="showContent" />
<!-- 手动导入懒加载组件 -->
<ClientOnly>
<LazyChatWidget />
</ClientOnly>
</template>
7. SEO实践
元数据管理
每个页面组件中,我们使用useHead
和useSeoMeta
设置SEO元数据:
vue
<script setup>
useHead({
title: '博客首页 - 技术博客',
meta: [
{ name: 'description', content: '一个使用Nuxt.js实现的技术博客,提供高质量的前端、后端和全栈开发文章' },
{ name: 'keywords', content: '博客,技术,前端,Vue,Nuxt,JavaScript' }
],
link: [
{ rel: 'canonical', href: 'https://yourblog.com/' }
]
});
</script>
动态SEO
对于动态内容页面,我们根据页面数据动态生成SEO信息:
vue
<script setup>
const { data: article } = await useAsyncData('article', () => $fetch(`/api/articles/${id}`));
useSeoMeta({
title: article.value.title,
ogTitle: article.value.title,
description: article.value.summary,
ogDescription: article.value.summary,
ogImage: article.value.cover,
twitterCard: 'summary_large_image',
});
</script>
结构化数据
我们还实现了JSON-LD结构化数据,帮助搜索引擎更好地理解页面内容:
vue
<template>
<div>
<!-- 页面内容 -->
<!-- 结构化数据 -->
<script type="application/ld+json">
{{
JSON.stringify({
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": article.title,
"image": article.cover,
"datePublished": article.publishTime,
"dateModified": article.updateTime,
"author": {
"@type": "Person",
"name": article.author.nickname
}
})
}}
</script>
</div>
</template>
总结与下一步
到目前为止,我们已经完成了从Vue到Nuxt的核心组件迁移、状态管理设置、API调用适配和SEO优化等关键工作。项目已经具备了基本的服务端渲染能力和SEO友好特性。
在下一篇文章中,我们将实现:
- 部分复杂交互组件的迁移
- 首页部分样式的渲染
通过这些步骤,我们将打造一个既具有良好用户体验,又能获得优秀搜索引擎表现的现代博客系统。
敬请期待下一篇文章从Vue迁移到Nuxt实现服务端渲染:构建SEO友好的博客系统(三),我们将继续深入探讨Nuxt开发的更多高级技巧。
- 本文链接:undefined/article/10
- 版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明文章出处!