使用Nuxt重构博客

分类
技术
标签
前端开发

最近在学习Nuxt,所以就想着把博客重构一下,学习一下Nuxt的使用。
目标是将博客系统的前端部分重构为Nuxt.js应用,包括文章列表、展示内容功能。

技术栈选型

这半年以来我与团队一齐开发了泡泡树洞项目,项目中我主要负责后端开发与CICD等运维工作,前端部分则由团队的前端工程师负责。在这个过程中,我对Nuxt.js以及其生态有一些了解。因此,我决定使用Nuxt.js重构我的博客,提升博客质量的同时也加深对Nuxt的理解。
可以说这是我第一次尝试使用Nuxt.js开发项目,我希望通过这次重构,更深入地了解Nuxt.js的特性与优势,同时也希望能够为其他对Nuxt感兴趣的开发者提供一些参考。
下面是我在重构博客过程中选用的技术栈:
1- 依赖管理:pnpm 2- 前端框架:Vue.js 3- 服务端渲染与静态站点生成框架:Nuxt.js 4- 内容管理:@nuxt/content 5- CSS 框架:Tailwind CSS (含 Typography 插件) 6- UI 组件库:DaisyUI 7- 图标库:nuxt-icon
作为博客项目,其核心是内容管理与展示,因此我选择了@nuxt/content作为内容管理系统,以便更方便地管理博客文章、分类、标签等静态内容。同时,为了提升博客的视觉效果与用户体验,我选用了Tailwind CSS与DaisyUI,以及nuxt-icon提供的图标库。

技术栈安装与配置

安装Node.js与pnpm:
1# https://github.com/nvm-sh/nvm 2curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash 3nvm use 20 4# https://pnpm.io/ 5npm install -g pnpm
初始化Nuxt项目:
1pnpm dlx nuxi@latest init nuxt-cunoe-blog 2cd nuxt-cunoe-blog
在这里遇到了本次重构第一个问题
1ERROR Error: Failed to download template from registry: Failed to download https://raw.githubusercontent.com/nuxt/starter/templates/templates/v3.json: TypeError: fetch failed
我意识到是因为我在国内,无法访问GitHub的原因,如果在 WSL 中我会直接使用 proxychains 来解决这个问题,但是我现在是在 Mac 环境下,由于 Proxychains 无法在 Mac 上使用,故我选择直接找到了 Nuxt 的模板地址,然后直接下载了模板文件。
1curl -o- https://raw.githubusercontent.com/nuxt/starter/main/templates/v3.json > v3.json 2# v3.json 中有一个字段是 "tar" ,这个字段的值就是模板的地址 3wget https://codeload.github.com/nuxt/starter/tar.gz/refs/heads/v3 -O nuxt-starter-v3.tar.gz 4tar -xzf nuxt-starter-v3.tar.gz 5mv nuxt-starter nuxt-cunoe-blog 6cd nuxt-cunoe-blog 7# 安装依赖,初始化项目 8pnpm i 9pnpm dev
这样就完成了项目的初始化,后续的安装与配置过程就比较顺利了,详情可以参考 Nuxt 以及各技术栈的官方文档。

内容迁移策略

我之前的Blog是基于Hexo搭建的,Hexo是一个静态博客框架,它使用Markdown文件来管理文章内容,这与Nuxt的@nuxt/content插件的使用方式有一定的相似性,因此我将文章从Hexo直接复制到Nuxt的content目录下,并稍作调整,即可完成内容迁移。
在我原来的Blog中,文章 Header 部分是这样的:
1// 2018.md 2--- 3title: 2018年度总结 4date: 2018-12-31 09:00:00 5urlname: 2018 6categories: 7- 笔记 8tags: 9- 总结 10---
@nuxt/content会自动解析文件头,并转换为对象
在项目中可以使用@nuxt/content插件来获取文章数据,例如:
1<!-- ./pages/blogs/[urlname].vue --> 2<script setup> 3const path = useRoute() 4const { data: doc } = await useAsyncData('blog-data', () => queryContent(path).findOne()) 5</script>
即可获取到文章的数据,然后在页面中展示。
在页面中展示时,可以使用ContentRenderer组件来渲染文章内容,例如:
1<!-- ./pages/blogs/[urlname].vue --> 2<template> 3 <div> 4 <div class="container max-w-3xl mx-auto"> 5 <ContentDoc v-slot="{ doc }"> 6 <h2 class="my-4 text-4xl font-semibold">{{ doc.title }}</h2> 7 <p class="my-4 text-gray-500"> 8 by CUNOE, {{ convertDate(doc.date) }} 9 </p> 10 <ContentRenderer class="prose" :value="doc"/> 11 </ContentDoc> 12 </div> 13 </div> 14</template>
为了实现prose样式,需要在tailwind.config.js中添加配置:
此处省略了一些配置,具体配置可以参考 Tailwindcss/typography 文档:https://github.com/tailwindlabs/tailwindcss-typography
1pnpm i @tailwindcss/typography
1// tailwind.config.jsexport default { 2 // ... plugins: [ 3 require('@tailwindcss/typography'), ] 4}

部署与发布

我选择了Vercel作为博客的部署与发布平台,Vercel是一个提供Serverless部署服务的平台,它支持Nuxt.js项目的部署,并提供了CI/CD功能,可以方便地将项目部署到云端。
在Vercel中,只需要将项目与GitHub仓库关联,然后设置好CI/CD配置,即可实现自动部署。
具体的部署流程可以参考Vercel官方文档。
11. 在Vercel中创建一个新项目 22. 将Vercel安装到GitHub仓库并允许访问 33. 修改 settings -> domains 中的域名,绑定自己的域名即可

总结

通过这次重构,我对Nuxt.js有了更深入的了解。现在前端的开发方式已经发生了很大的变化,Nuxt.js提供了一种全新的前端开发方式,使得前端开发更加高效、简单,让前端开发变成像搭积木一样的体验。
在这次重构中,我学到了很多新的知识,也解决了一些问题,同时也发现了一些新的问题,这些问题将成为我下一步学习的方向。
前端的开发相比后端来说,更加灵活,更加有趣,但也更加复杂,需要不断学习,不断提升自己的技术水平。很多时候,前端开发不仅仅是技术问题,更多的是设计问题、用户体验问题,需要我们不断思考、不断尝试。
今天刚做完又看到了一些前端大佬的博客,好看的设计、炫酷的动画,让我感觉自己还有很长的路要走,也希望这个博客成为我前端学习的一个记录,也希期能够帮助到其他人。前端开发简单可以很简单,但是想要做到很好,还是需要花费很多的时间和精力,去学习、去实践、去尝试。
现在这个博客还有很多地方需要改进,比如SEO优化、性能优化、用户体验优化等等,这些都是我接下来要做的事情,不知道还有多少坑要踩,但是我会继续努力,不断学习,不断提升自己的技术水平。
以后每次更新博客,都会记录一下自己的学习经历,希望能够帮助到其他人,也希望能够帮助自己更好地学习前端开发。
前端开发永无止境,做好了一个功能,下一个功能就在等着你,属实是痛苦。
总的来说,这次重构是一次很好的学习经历,我希望通过这次重构,能够提升自己的技术水平,也希望能够为其他对Nuxt感兴趣的开发者提供一些参考。

项目布局与源码

下面是一些项目的基本结构,这里只列出了一些关键文件,具体文件以及目录结构参考 Nuxt 官方文档。
1$ tree 2. 3├── README.md 4├── app.vue 5├── assets 6│   └── css 7│   └── main.css 8├── components 9│   └── main 10│   ├── AppFooter.vue 11│   └── AppNavbar.vue 12├── content 13│   ├── about 14│   │   └── index.md 15│   └── blogs 16│   └── youtrack-with-testlink.md 17├── layouts 18│   ├── 404.vue 19│   └── default.vue 20├── nuxt.config.ts 21├── package.json 22├── pages 23│   ├── [...slug].vue 24│   ├── about.vue 25│   ├── blogs 26│   │   └── [blog].vue 27│   └── index.vue 28├── pnpm-lock.yaml 29├── public 30│   └── favicon.ico 31├── tailwind.config.js 32└── tsconfig.json
app.vue
1<!-- ./app.vue --> 2<template> 3 <NuxtLoadingIndicator /> 4 <NuxtLayout> 5 <NuxtPage /> 6 </NuxtLayout> 7</template>
布局文件
1<!-- ./layouts/default.vue --> 2 <template> 3 <div class="flex flex-col font-spacegrotesk h-screen"> 4 <header class="w-full bg-[#F1F2F4] dark:bg-slate-950 z-10"> 5 <AppNavbar /> 6 </header> 7 <main class="flex-1 flex flex-col justify-start m-4"> 8 <div class="p-2"></div> 9 <slot /> 10 <div class="p-2"></div> 11 </main> 12 <footer class="w-full"> 13 <AppFooter /> 14 </footer> 15 </div> 16</template> 17 18<script setup lang="ts"> 19import AppNavbar from '~/components/main/AppNavbar.vue'; 20import AppFooter from '~/components/main/AppFooter.vue'; 21</script>
导航栏以及页脚组件
1<!-- ./components/main/AppNavbar.vue --> 2<template> 3 <div> 4 <div class="bg-base-300"> 5 <div class="container mx-auto p-2 flex justify-between items-center"> 6 <!-- 左侧链接部分 --> 7 <div> 8 <nuxt-link to="/" class="btn btn-ghost text-xl">Home</nuxt-link> 9 <nuxt-link to="/about" class="btn btn-ghost text-xl">About</nuxt-link> 10 </div> 11 <!-- 右侧头像部分 --> 12 <div> 13 <img src="/favicon.ico" alt="Avatar" class="w-10 h-10 rounded-full"> 14 </div> 15 </div> 16 </div> 17 <div class="bg-auto h-64 relative bg-center" style="background-image: url(https://s3.cunoe.com/files/background/bg-3.jpg)"> 18 <div class="absolute bottom-0 left-0 right-0 bg-opacity-50 text-white py-4 px-6"> 19 <div class="container mx-auto"> 20 <nuxt-link to="/" class="text-white text-3xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl"> 21 <strong>CUNOE&DIARY</strong> 22 </nuxt-link> 23 </div> 24 </div> 25 </div> 26 <div class="bg-base-300"> 27 <div class="container mx-auto text-3xl p-2"> 28 <!-- 添加 flex 和 justify-end 类来实现靠右对齐 --> 29 <div class="flex justify-end flex-1 space-x-4"> 30 <nuxt-link to="mailto:[email protected]" class=""><Icon name="ic:baseline-mail-outline" /></nuxt-link> 31 <nuxt-link to="https://github.com/cunoe" target="_blank"><Icon name="uil:github" /></nuxt-link> 32 </div> 33 </div> 34 </div> 35 </div> 36</template>
1<!-- ./components/main/AppFooter.vue --> 2<script> 3import { ref, defineComponent } from 'vue'; 4const year = ref(new Date().getFullYear()); 5export default defineComponent({ 6 setup() { 7 return { 8 year 9 }; 10 } 11}); 12</script> 13<template> 14 <footer> 15 <div class="flex flex-col justify-center items-center space-y-1 bg-base-300 h-40 mx-auto"> 16 <div class="container mx-auto"> 17 <div class="space-x-2 text-gray-600"> 18 <a href="https://icp.gov.moe/?keyword=20232394" rel="noreferrer" target="_blank"> 19 萌ICP备20232394号 20 </a> 21 </div> 22 <div class="p-1"></div> 23 <div> 24 <span class="text-gray-400">Copyright © 2017 - {{year}} - By Cunoe</span> 25 </div> 26 </div> 27 </div> 28 </footer> 29</template>
首页
1<!-- ./pages/index.vue --> 2<script setup> 3const articles = await queryContent('blogs').where({ published: {$exists: false} }).sort({date:-1}).find() 4const convertDate = (date) => { 5 return new Date(date).toLocaleDateString('en-US', { 6 year: 'numeric', 7 month: 'long', 8 day: 'numeric' 9 }) 10} 11onMounted(() => { 12 document.title = 'CUNOE&DIARY' 13}) 14</script> 15<template> 16 <div> 17 <div class="container max-w-3xl mx-auto"> 18 <div class="grid grid-cols-1 gap-4"> 19 <div v-for="(article, index) in articles" :key="index"> 20 <nuxt-link :to="`/blogs/${article.urlname}`"> 21 <div class="p-4 rounded-badge"> 22 <h2 class="text-2xl font-bold mb-2">{{ article.title }}</h2> 23 <p class="my-4 text-gray-500"> 24 by CUNOE, {{ convertDate(article.date) }} 25 </p> 26 <div class="prose" v-html="article.description" /> 27 </div> 28 </nuxt-link> 29 <!-- 分割线 --> 30 <hr class="my-4 border-t border-gray-600 opacity-50"> 31 </div> 32 </div> 33 </div> 34 </div> 35</template>
文章详情页
1<!-- ./pages/blogs/[urlname].vue --> 2<script setup> 3const { path } = useRoute() 4const { data: doc } = await useAsyncData('blog-data', () => queryContent(path).findOne()) 5const convertDate = (date) => { 6 return new Date(date).toLocaleDateString('en-US', { 7 year: 'numeric', 8 month: 'long', 9 day: 'numeric' 10 }) 11} 12</script> 13<template> 14 <div> 15 <div class="container max-w-3xl mx-auto"> 16 <ContentDoc v-slot="{ doc }"> 17 <h2 class="my-4 text-4xl font-semibold">{{ doc.title }}</h2> 18 <p class="my-4 text-gray-500"> 19 by CUNOE, {{ convertDate(doc.date) }} 20 </p> 21 <ContentRenderer class="prose" :value="doc"/> 22 </ContentDoc> 23 </div> 24 </div> 25</template>

页面切换添加过渡

为了让页面切换更加流畅美观,我们需要对页面切换时的加载进行优化,添加加载指示器以及页面切换时的动画。
参考:https://nuxt.com/docs/getting-started/transitions
1<!-- app.vue --> 2<template> 3 <!-- 加载指示器 --> 4 <NuxtLoadingIndicator /> 5 <NuxtLayout> 6 <NuxtPage /> 7 </NuxtLayout> 8</template> 9<!-- 过渡 --> 10<style> 11 .page-enter-active, 12 .page-leave-active { 13 transition: all 0.4s; 14 } 15 .page-enter-from, 16 .page-leave-to { 17 opacity: 0; 18 filter: blur(1rem); 19 } 20 21 .layout-enter-active, 22 .layout-leave-active { 23 transition: all 0.4s; 24 } 25 .layout-enter-from, 26 .layout-leave-to { 27 opacity: 0; 28 filter: blur(1rem); 29 } 30 31 html.dark{ 32 color-scheme: dark; 33 } 34</style>

Emoji移动

为了让博客更加生动,我在博客首页添加了一个Emoji移动效果。实现方式是通过@formkit/auto-animate以及nuxt-swiper实现的。
1pnpm add @formkit/auto-animate 2pnpm add nuxt-swiper
1// nuxt.config.tsexport default defineNuxtConfig({ 2 // ... modules: ['@formkit/auto-animate/nuxt', 'nuxt-swiper'],})
1<!-- pages/index.vue --> 2<script> 3import { ref } from 'vue' 4const items = ref(["😏","😐","😑","😒","😕"]) 5function moveItem(toRemove) { 6 items.value = items.value.filter((item) => item !== toRemove) 7 if (Math.random() < 0.5) { 8 items.value.unshift(toRemove); 9 } else { 10 items.value.push(toRemove); 11 } 12} 13// ... 14</script> 15<template> 16 <swiper class="max-w-screen-md"> 17 <ul v-auto-animate class="container flex flex-row text-2xl justify-center space-x-0.5"> 18 <li 19 v-for="item in items" 20 :key="item" 21 @click="moveItem(item)" 22 > 23 {{ item }} 24 </li> 25 </ul> 26 </swiper> 27 <!-- ... --> 28</template>

网页SEO

添加meta

参考:https://nuxt.com/docs/getting-started/seo-meta
1// nuxt.config.tsexport default defineNuxtConfig({ 2 // ... app: { 3 head: { 4 charset: 'utf-8', viewport: 'width=device-width,initial-scale=1', title: 'CUNOE&DIARY', meta: [{name: 'description', content: 'CUNOE&DIARY'}], }, },})

添加sitemap

1pnpm add @nuxtjs/sitemap
1// nuxt.config.tsexport default defineNuxtConfig({ 2 // ... modules: ['@nuxtjs/sitemap'],})