从零开始构建Nuxt项目

2025-11-29 13:59:06   浏览: 14次

AI评分:88

原因:文章内容全面,从Nuxt项目初始化、工程结构、代码逻辑到部署步骤均有详细覆盖,配有清晰截图和实用代码示例,语言流畅易懂。缺点是过度依赖外部图片链接(可能不稳定),部署部分稍简略,未涉及错误处理或SEO深度优化,适合初学者但缺乏高级技巧。

    本文主要介绍如何快速构建一个NUXT项目,调用线上接口,并且最终部署。

    为什么突发奇想的想要构建一个Nuxt的项目?其实也并非是突发奇想,在几年前就有计划做一个对搜索引擎友好的网站了,但是碍于Vue是单页面应用,历史逻辑错综复杂,想要转化到另外一个框架上需要很大的积极性来实现。


开始构建

    NUXT官网:nuxt.com 。创建Nuxt4.0项目:

npm create nuxt@latest <project-name>

    在这一步可能因为网络问题需要重试多次,当然如果指令有问题,可以考虑升级node,官方的建议是20.x+ 。 

   (Node的版本可以使用NVM:node version manager 进行管理,这样就可以在本机切换多个node环境进行开发了)



    选择包管理器,我这边就用默认的包管理器npm了。等待安装和初始化,期间可能询问使用git初始化,以及是否安装一些官方的模块,直接默认回车就行。



    我们跟随指引启动项目,先 cd test 进行项目根路径,再npm run dev启动项目。



    打开网址,发现就初始化好了。


工程结构

    通过上面的方式进行安装,我们可以得到一个非常干净的项目,结构如下:


    很显然,这里缺少了很多目录结构,这些结构并没有直接创建,而是需要有Nuxt的基础结构认知。通过我线上的一个Nuxt项目进行目录结构解析:

    

    首先这里几个被.gitignore过滤掉的文件夹不用管,即上图被灰色的文件夹及文件(.env是环境配置文件,如更改端口什么的)。我们聚焦于 app文件夹、public文件夹、nuxt.config.ts文件 即可。


    先说 nuxt.config.ts文件,这是Nuxt的配置文件,可以对项目进行基础的配置。比如我的项目加入了sass进行css预处理语言设置(加入依赖直接 npm install <依赖名称> 即可),设置了全局css样式,并且对vite的css预处理中加入了scss的变量配置文件。其次还配置了路由规则,把所有的/api/**路径全部代理到了我的线上项目路径,以便本地测试。


    public文件夹,和其他前端框架一样,就是一个公有静态文件配置目录,可以加入一些静态文件,如robot.txt、静态图片、验证用文件等。


    项目最核心的目录就是app了,基本上99%的时间都在这里进行编码。对于该文件,一个最基础的结构如下图所示。

    首先是assests文件夹,用来管理css、js、img或者其他的资源文件使用,可以依次再创建子文件夹表示资源类型,我这边只有scss文件。components文件夹,表示组件目录,默认情况下,组件会以文件相对文件夹命名自动引入(详见Nuxt components指南)。核心的核心是pages文件夹,这里直接决定了页面的逻辑关系。index表示主页,blog表示博客页,在blogs文件夹下的文件用[id]命名的文件为动态页面。



    至于app.vue,则是网站应用的启动入口页。


代码逻辑

    对于全局css样式,我常用的配置项就下面这些。

*{
  padding: 0;
  margin: 0;
  box-sizing: border-box;
    // font-family: serif;
}

// html, body{
//   width: 100%;
//   height: 100%;
//   overflow: hidden;
// }

a {
  color: black;
  text-decoration: none;
}

ul, li {
  list-style: none;
}

*::-webkit-scrollbar{
  width: 8px;
  height: 8px;
  background-color: rgb(189, 189, 189);
}
*::-webkit-scrollbar-thumb{
  background-color: rgb(114, 116, 112);
}


    入口页,配置上导航页签,以及内容呈现页。对于NuxtLink里配置的to路径,会在pages文件夹下寻找对应的.vue文件,/自动对应index.vue 。注意,NuxtPage要设置跟随路由刷新,否则可能导致页面不刷新

<template>
  <div class="app">
    <div class="nav">
      <NuxtLink to="/">HOME</NuxtLink>
      <NuxtLink to="/blog">BLOG</NuxtLink>
    </div>
    <div class="page-container">
      <NuxtPage class="page" :key="$route.fullPath"/>
    </div>
  </div>
</template>


    默认的该app.vue会使用pages/index.vue进行初始主页的渲染。可以是简单的欢迎页面。这里需要注意的是,因为是SSR服务端渲染页面,为了SEO考虑,支持配置html的head标签,只要使用useHead即可。

<template>
  <div class="index">
    <!-- <hr/> -->
    <h1 class="title">Welcome 2 Dreamcenter!</h1>
    <br/>
    <p>网站开发中, <a href="https://www.dreamcenter.top" style="color: coral;" target="_blank">点击前往旧站点</a></p>
  </div>
</template>

<script setup>
useHead({
  title: '时光潜流 | 首页',
  meta: [
    { name: 'description', content: '时光潜流, 妹控的中二君' },
    { name: 'keywords', content: '时光潜流,主页,妹控' },
    { name: 'author', content: '时光潜流' }
  ],
  htmlAttrs: {
    lang: 'zh-CN'
  }
})
</script>

<style scoped lang="scss">
.index {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: calc(100vh - 80px);
  .title{
    font-family:'Courier New', Courier, monospace;
  }
}
</style>


    这样一个简单的首页就做好了。



    博客列表页主要会涉及到发送请求的逻辑。我们先来看template部分。

<template>
  <div class="blog">
    <!-- <p>Count: {{ countRes?.data }}</p> -->
    <ul class="blogList">
      <li v-for="item in listRes?.data" :key="item.id" class="blogItem">
        <NuxtLink :to="`/blogs/${item.id}`" class="title">
          {{ item.title }}
        </NuxtLink>
        <p class="time">{{ item.time }}</p>
        <div class="content">{{ item.content + '...' }}</div>
      </li>
    </ul>
    <div class="pagination">
      <a class="last" @click="last" :style="{color: (pageInfo.pageNo == 1 ? '#aaa' : 'black')}">上一页</a>
      <span>{{  pageInfo.pageNo  + ' / ' + Math.ceil(countRes.data / pageInfo.pageSize) }}</span>
      <!-- <b class="split">/</b> -->
      <a class="next" @click="next" :style="{color: (pageInfo.pageNo == Math.ceil(maxPage) ? '#aaa' : 'black')}">下一页</a>
    </div>
  </div>
</template>


    其实就是简单的对列表for循环,然后博客标题点击即可跳转到对应的动态页面。效果如下图所示。




    该页面的script部分,使用useAsyncData函数,来装载异步数据,使用$fetch发送请求。注意,这里的数据都是响应式引用,使用ref定义。

<script setup>
useHead({
  title: '时光潜流 | 博客列表',
  meta: [
    { name: 'description', content: '陈列博客列表' },
    { name: 'keywords', content: '博客,列表,时光潜流' },
    { name: 'author', content: '时光潜流' }
  ],
  htmlAttrs: {
    lang: 'zh-CN'
  }
})

const pageInfo = ref({ pageNo: 1, pageSize: 7 })


const { data: countRes } = await useAsyncData('blog-count', () =>
  $fetch('/api/blog/count')
)

const maxPage = computed(() => Math.ceil(countRes.value.data / pageInfo.value.pageSize))

const { data: listRes, refresh } = await useAsyncData('blog-list', async () => {
  return await $fetch('/api/blog/page', {
    method: 'POST',
    params: pageInfo.value
  })
})

const last = async  () => {
  if (pageInfo.value.pageNo > 1) {
    pageInfo.value.pageNo--
    await refresh()
  }
}

const next = async () => {
  console.log(maxPage)
  if (pageInfo.value.pageNo < maxPage.value) {
    pageInfo.value.pageNo++
    await refresh()
  }
  /*const { data: newData } = await refresh()
  if (!newData.value?.list.length) {
    pageInfo.value.pageNo--
    await refresh()
  }*/
}

</script>


    最后是动态页面部分。首先从route获取到当前路由的参数部分的id值,通过id获取到页面数据简单转化(便于后续获取数据)。接着要配置动态页面的head部分,使用useSeoMeta函数来初始化配置动态页面的head。样式部分,单位vw、vh表示视窗宽高百分比,使用:deep可以穿透scoped限制,来改变元素的样式。

<template>
  <article class="blogItem">
    <header>
      <h1 class="title">{{ blogInfo?.title }}</h1>
      <p class="time">{{ blogInfo?.time }}</p>
    </header>
    <div v-html="blogInfo?.content" class="content"></div>
  </article>
</template>

<script setup> 
const route = useRoute()
const id = computed(() => route.params.id)

const { data: blogInfo } = await useAsyncData('blog', async () => {
  const result = await $fetch('/api/blog/id', {
    method: 'POST',
    params: { id: id.value }
  })
  return result
}, {
  transform: (result) => result?.data // 直接将 result.data 提取为返回值
})

// if (import.meta.server) {
  useSeoMeta({
    title: blogInfo.value.title,
    description: blogInfo.value.content.substring(0, 100) + '...',
    keywords: blogInfo.value.keywords
  })

// }

</script>

<style scoped lang="scss">
.blogItem{
  width: 50vw;
  display: flex;
  // justify-content: center;
  justify-self: center;
  flex-direction: column;
  .title{
    font-family: serif;
    text-align: center;
    margin-top: 60px;
    font-size: 36px;
  }
  .time{
    color: #999;
    text-align: center;
    font-size: 14px;
    margin: 16px 0 36px 0;
    font-family: 'Noto Serif SC', '楷体', serif;
  }
  .content {
    font-family: serif;
    :deep(p) {
      font-size: 20px;
      padding-bottom: 20px;
      line-height: 1.8;
    }
    :deep(pre) {
      border: 2px solid rgb(152, 221, 160);
      padding: 4px 10px;
    }
    :deep(p img) {
      // border: 1px solid black;
      box-shadow: 0 0 2px black;
    }
  }
}

@media (max-width: 768px) {
  .blogItem {
    width: 85vw;
  }
}
</style>


    动态页面效果如下。至此一些基础的功能就全部实现了。

    

部署

    当我们在本地环境把想要的功能都实现后,就要部署项目了。使用如下指令: nuxt build

    生成好后,可以得到一个.output的文件夹,里面包含了node服务端代码以及public静态代码,可以分开部署,也可以合并部署。node启动指令为 node server/index.mjs  。

   

    在服务器端部署时,如果3000端口被占用,需要临时修改。可以先在命令行输入 set PORT=33330 (目标端口号),来设置当前项目的端口号,再使用node来启动。


结语

    至此,就已经实现了一个基础的NUXT页面构建与部署啦!通过部署NUXT,可以实现将页面进行服务器端渲染,增加搜索引擎的收录率。




#Nuxt#Seo#SSR#教学