在上一篇文章从Vue迁移到Nuxt实现服务端渲染:构建SEO友好的博客系统(一)中,我们介绍了将Vue博客项目迁移到Nuxt的背景、动机和基础架构设置。本文将继续深入探讨迁移过程中的实际操作、遇到的问题及解决方案,以及组件迁移的最佳实践。

目录

  1. 组件迁移策略
  2. 状态管理与持久化
  3. 服务端渲染中的API调用
  4. TypeScript适配
  5. 样式与资源处理
  6. 性能优化
  7. SEO实践

1. 组件迁移策略

在迁移Vue组件到Nuxt环境时,我们采用了以下策略:

基础组件优先

从底层基础组件开始迁移,如SvgIconPagination等,然后逐步迁移更复杂的组件。这种自下而上的方法使我们能够建立坚实的组件基础,再逐步构建更复杂的功能。

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组件被拆分为HeaderNavBarToggle三个子组件,每个子组件负责特定的功能。

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调用,我们主要使用useFetchuseAsyncData两个组合式函数。

页面数据获取

在页面组件中,我们使用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实践

元数据管理

每个页面组件中,我们使用useHeaduseSeoMeta设置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友好特性。

在下一篇文章中,我们将实现:

  1. 部分复杂交互组件的迁移
  2. 首页部分样式的渲染

通过这些步骤,我们将打造一个既具有良好用户体验,又能获得优秀搜索引擎表现的现代博客系统。

敬请期待下一篇文章从Vue迁移到Nuxt实现服务端渲染:构建SEO友好的博客系统(三),我们将继续深入探讨Nuxt开发的更多高级技巧。

评论
默认头像
评论
来发评论吧~