从零开始构建Nuxt项目
2025-11-29 13:59:06 浏览: 14次
AI评分:88
本文主要介绍如何快速构建一个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,可以实现将页面进行服务器端渲染,增加搜索引擎的收录率。