从零开始搭建nuxt3项目记录开发知识点...

3,650 阅读6分钟

前言

之前用过nuxt2,nuxt3出来之后还没来得及看,最近终于有点空闲的时间,继续开始卷了,于是参考官网一点点搭建,最大的感受是框架就是用来解放人的双手的,绝大部分的场景框架都为你考虑到了,不再需要自己手动实现,反而有点不习惯了😑

nuxt3安装

  • node: 16.19.0
  • npmp && npx: 8.19.3

安装

npx nuxi init nuxt3-prod

# 进入nuxt-prod目录安装依赖

npm i

运行

npm run dev

😴 如果出现下面错误,说明安装依赖时出现了错误,先确定本地的npm源,然后重新安装

1.jpg

🍕 下面则是运行成功的界面

2.jpg

nuxt3配置

目录结构

nuxt3项目初始化后的目录结构可以说是相当的简单

3.jpg

  • .nuxt: nuxt自动编译生成的
  • app.vue: 主入口文件
  • nuxt.config.ts: nuxt配置文件
  • package.json: 包管理文件

配置文件

nuxt.config.ts

nuxt.config.js为项目默认的配置文件,配置的某些属性可以在应用中访问到

export default defineNuxtConfig({
  runtimeConfig: {
    // 服务器端可以获取使用
    apiSecret: "123456",
    // 客户端获取使用
    public: {
      apiBase: "/api",
    },
  },
});

访问方式app.vue

<script setup>
    const runtimeConfig = useRuntimeConfig();
    console.log(runtimeConfig);
</script>

app.config.ts

与nuxt.config.js类似的一个全局配置文件,但是需要手动创建,并且必须在根目录下面,里面定义的对象可以全局访问,相对于一个全局变量对象

export default defineAppConfig({
  title: "Hello world",
  theme: {
    colors: {
      primary: "#ff0000",
    },
  },
});

访问方式app.vue

<script setup>
    const appConfig = useAppConfig();
    console.log(appConfig);
</script>

视图界面

组件

在根目录创建components文件夹,然后再建立驼峰式的组件文件(也可以创建驼峰文件夹,子文件默认为index.vue),nuxt会自动引入创建的所有文件并全局注册,在任意地方都可以直接使用

路由

默认的项目模板是没有路由的,我们需要手动创建pages文件夹,然后在其下面建立文件,nuxt会把里面的文件当做路由文件进行配置,index.vue为默认路由入口文件,此时有两种方式可以进入到路由

第一种:修改app.vue代码

<template>
    <NuxtLayout>
        <NuxtPage />
    </NuxtLayout>
</template>

在模块中新增<NuxtPage />组件, 默认为路由的入口

第二种:根目录新增layouts文件夹,然后再建立子文件default.vue

<template>
    <slot></slot>
</template>

此方式需要删除app.vue文件, default的名称不可修改,nuxt会默认读取

布局

需要在根目录新建layouts文件夹,default.vue为其默认的布局组件,这样pages中所有路由都会被它包裹,通常我们把界面公共的布局区域放到此处,如头部导航,底部菜单等...

<template>
    <Header></Header>
    <slot></slot>
    <Footer></Footer>
</template>

并不是所有界面都用到默认的布局,如果还需要其他布局,可以在layouts目录下新增组件,比如custom.vue

<template>
    <div>我是新的自定义头部</div>
    <slot></slot>
</template>

当一个路由界面需要用此自定义布局时, 在路由中配置即可,没有配置的默认使用default.vue

<template>
    <div>我是新的界面,使用自定义布局</div>
</template>
<script setup>
    defineMetaPage({
        layout: 'custom'
    })
</script>

资源

public

public目录主要用作静态资源公共服务器,在代码中可以直接访问到静态资源文件, 此目录的文件不会被构建工具所编译

比如在public目录下有个img文件夹,放一个logo.png文件

<img src="/img/logo.png" />

assets

静态资源的管理目录,主要用在客户端,希望能被构建工具(Vite或webpack)处理的资源放在这个目录下

此目录不像layoust,components,它不会被nuxt自动处理,所以此目录下的文件没有严格的命令规范

比如在assets目录下有个img文件夹,放一个logo.png

<img src="~/assets/img/logo.png" />

全局样式

在配置文件中引入即可...

export default defineNuxtConfig({
    ...,
    css: ['~/assets/css/index.css', '~/assets/css/base.css']
});

路由

路由配置

在pages目录下新建路由文件,nuxt会自动的引入所有文件生成路由配置,跳转的路径就是文件名称

  • pages

    • index.vue
    • list.vue
const router = useRouter()
router.push('/')
router.push('/list')

嵌套路由

如果需要用到嵌套路由,则需创建一个与路由文件名称相同的文件夹, 如home.vue路由有嵌套路由,则创建一个同名的文件夹,在定义路由界面即可...

  • pages

    • home
      • index.vue
      • ohter.vue
    • index.vue
    • home.vue

在nuxt3中嵌套路由使用<NuxtChild />组件貌似不行了,需要使用<NuxtPage />

<template>
   <NuxtPage />
</template>
<script setup>
     const router = useRouter()
     router.push('/home)
     router.push('/home/other)
</script>

动态路由

1. 文件以中括号命名

在文件夹下建立以[id].vue命令的组件文件, 不能以下划线_id.vue命名,会访问不了

  • pages

    • posts
      • [id].vue
    • index.vue
<script setup>
   // 传递一个参数
    const router = useRouter()
    router.push('/posts/1)
    router.push('/posts/123)
</script>
<script setup>
   // 接收参数
   const route = useRoute()
   console.log(route.params.id)
</script>

2. 嵌套以中括号命令文件夹和文件

  • pages

    • posts
      • [type]
        • [id].vue
    • index.vue
 <script setup>
    // 传递两个参数
     const router = useRouter()
     router.push('/posts/a/1)
     router.push('/posts/b/123)
 </script>
<script setup>
    // 接收参数
    const route = useRoute()
    console.log(route.params.type, route.params.id)
</script>

3. 文件夹和文件以中括号命名

  • pages

    • posts-[type]
      • [id].vue
    • index.vue
<script setup>
    // 传递两个参数
    const router = useRouter()
    router.push('/posts-a/1)
    router.push('/posts-b/123)
</script>
<script setup>
    // 接收参数
    const route = useRoute()
    console.log(route.params.type, route.params.id)
</script>

4. 文件以[...]命令

  • pages

    • posts
      • [...all].vue
    • index.vue
 <script setup>
     // 此方式没有任何的参数限制
     const router = useRouter()
     router.push('/posts/a/1)
     router.push('/posts/b/123)
     router.push('/posts/b/123/32323)
 </script>
<script setup>
   // 接收参数
   const route = useRoute()
   console.log(route.params.all[0], route.params.all[1], route.params.all[2])
</script>

路由跳转

1. 组件跳转

<template>
    <nav>
      <ul>
        <li><NuxtLink to="/">home</NuxtLink></li>
        <li><NuxtLink to="/about">about</NuxtLink></li>
      </ul>
    </nav>
</template>

2. 方法跳转

<script>
    const router = useRouter()
    router.push('/about')
</script>

路由参数

通过?拼接参数

<script setup>
    // 路由跳转
    const router = useRouter()
    router.push('/detail?id=123&name=hello')
</script>

<script setup>
    // 接受参数
    const route = useRoute()
    console.log(route.query.id, route.query.name)
</script>

通过动态路由参数传递, 需要配置成动态路由

参考上面动态路由跳转和参数接收

路由中间件

1. 命令路由中间件

定义在middleware文件夹中,当页面每次使用时,会通过异步导入自动加载执行

middleware/auth.js

export default defineNuxtRouteMiddleware((to, from) => {
  console.log("要去那个页面:" + to.path);
  console.log("来自那个页面:" + from.path);
})
<script setup lang="ts">
definePageMeta({
  middleware: 'auth'
})
</script>

<template>
  <h1>我是首页使用了路由中间件</h1>
</template>

2. 全局路由中间件

定义在middleware文件夹中, 使用.global.js后缀, 在每次路由变化时自动执行。

middleware/auth.global.js

export default defineNuxtRouteMiddleware((to, from) => {
  console.log("要去那个页面:" + to.path);
  console.log("来自那个页面:" + from.path);
});

路由验证

Nuxt通过每个要验证界面中的definePageMeta()方法的vilidate属性进行验证,vildata方法接收route作用参数,可以返回一个boolean值来确定此路由是否为有效路由,如果为false,并且找不到其他匹配项,则会导致404

<script setup lang="ts">
    definePageMeta({
      validate: async (route) => {
        // 检查id是否由数字组成
        return /^\d+$/.test(route.params.id)
      }
    })
</script>

SEO配置

head全局配置

nuxt.config文件中全局配置

export default defineNuxtConfig({
  app: {
    head: {
      charset: "UTF-8",
      viewport: "width=device-width, initial-scale=1.0",
      title: "加速器",
      meta: [{ name: "description", content: "加速器" }],
    },
  },
})

动态配置

在路由界面中使用useHead函数配置

<script setup>
    const title = '首页'
    useHead({
      title,
      meta: [ { name: 'description', content: 'My amazing site.' } ]
    });
</script>

composables组合式

自动扫描

此目录下的所有顶层文件会被自动扫描,文件中导出的方法,在应用程序中可以被直接使用,不用再手动引入

创建文件composables/useDateTime.js

export const getYear = () => {
  return new Date().getFullYear()
}

app.vue中使用

<template>
    <div>
    {{ year }}
    </div>
</template>
<script setup>
    const year = getYear()
</script>

嵌套文件

如果composables中有嵌套的文件夹,默认情况下它里面的方法是不会被扫描到的,用两种方式可以实现

1. 对嵌套的文件重新导出

对于项目的目录:composables/index.js composables/myUtils/utils.js,我们可以在index中引入utils并直接导出, 这种方式不太优雅

export { utils } from './myUtils/utils.ts'

2. 在nuxt.config.ts文件中配置,会遍历查找

export default defineNuxtConfig({
  imports: {
    dirs: [
      // 扫描顶层模块
      'composables',
      // 扫描嵌套的模块,并且使用特定的名称和后缀的文件
      'composables/*/index.{ts,js,mjs,mts}',
      // 扫描给定目录内的所有模块
      'composables/**'
    ]
  }
})

useState状态管理

Nuxt3提供了可组合的useState,以创建跨组件和SSR友好的共享数据状态,利用此特性也可实现全局的状态管理(客户端不是可以持久化)

简单使用

在单个路由中使用的时候类似于ref的功能,创建的值可以作为全局的唯一标识,在任何其他的路由组件中使用

<template>
    <div>
        <div>{{ counter }}</div>
        <div @click="counter++">counter++</div>
        <div @click="counter--">counter--</div>
    </div>
</template>
<script setup>
const counter = useState('counter', () => Math.round(Math.random() * 1000))
</script>

其他路由组件中使用

<template>
    <div>
        <div>{{ counter }}</div>
    </div>
</template>
<script setup>
const counter = useState('counter')
</script>

状态共享

在componsables目录下创建文件userStore.js

export const useUserInfo = () =>
  useState("userInfo", () => {
    return {
      nickName: "tony",
    };
  });

在其他组件中使用

<template>
    <div>
        {{ userName }}
    </div>
    <div @click="changeName">改变值</div>
</template>
<script setup>
const userInfo = useUserInfo();
const changeName = () => {
    userInfo.userName = 'hi tony'
}
</script>

pinia状态管理

安装依赖

npm i pinia @pinia/nuxt @pinia-plugin-persistedstate/nuxt -S

pinia配置

nuxt.congfig中配置

modules: ["@pinia/nuxt", "@pinia-plugin-persistedstate/nuxt"],

pinia使用

在根目录创建pinia/user.js模块, 可以任意的创建多个模块

import { defineStore } from "pinia";

export default defineStore("user", {
  state() {
    return {
      userName: 'hello world',
    };
  },
  getters: {},
  actions: {
    setUserName(userName) {
      this.userName = userName;
    },
  }
});

路由中使用

<template>
    <div>{{ userName }}</div>
</template>
<script setup>
import { toRefs } from 'vue'
import userPinia from '~/pinia/user'
const userStore = userPinia()
const { userName } = toRefs(userStore)
const changeName = () => {
    userStore.setUserName('hi world')
}
</script>

数据持久化

安装依赖

npm i @pinia-plugin-persistedstate/nuxt -S

模块配置

import { defineStore } from "pinia";

export default defineStore("user", {
  state() {
    return {
      userName: 'hello world',
    };
  },
  getters: {},
  actions: {
    setUserName(userName) {
      this.userName = userName;
    },·
  },
  persist: {
    key: "user", // 存储的标识符,默认为store的值(第一个参数)
    paths: ["userName"], // 存储哪些数据
  },
});

sass配置

安装sass

cnpm i sass -D

sass全局变量

  vite: {
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: `@import "assets/css/var.scss";`,
        },
      },
    },
  },

数据请求

nuxt3中提供了4个方法来获取数据, 他们可以在路由,组件或插件中使用

注意:他们只能在setup或者是生命周期钩子中使用

useFetch

该方法实际上是对 useAsyncData 和$fetch 的封装,提供了一个更便捷的封装方法,在服务器端获取数据使用

<script setup>
    const headers = useRequestHeaders(["cookie"]);
    const { data: count } = await useFetch('/api/count',{headers})
</script>

<template>
  Page visits: {{ count }}
</template>

$fetch

主要用于在客户端用户交互时的数据请求, 此方法在pages的路由界面setup中不能被直接调用,在子组件的setup中可以被直接调用(客户端调用)

<script setup>
    const sendData = () => {
        const { data } = await $fetch('/api/send')
    }
</script>

<template>
 <div @click="sendData">发送数据</div>
</template>
<script setup>
    const { data } = await $fetch('/api/send')
    console.log(data)
</script>

<template>
</template>

useLazyFetch

相当于useFetch方法默认配置了lazy:true,执行异步函数时不会阻塞路由的执行。也意味着你必须处理数据为null的情况

<template>
  <div v-if="pending">
    Loading ...
  </div>
  <div v-else>
    <div v-for="post in posts">
      <!-- do something -->
    </div>
  </div>
</template>

<script setup>
const { pending, data: posts } = useLazyFetch('/api/posts')
watch(posts, (newPosts) => {
  // Because posts starts out null, you will not have access
  // to its contents immediately, but you can watch it.
})
</script>

useAsynaData

server/api/count.ts

let counter = 0
export default () => {
  counter++
  return JSON.stringify(counter)
}

app.vue

<script setup>
const { data } = await useAsyncData('count', () => $fetch('/api/count'))
</script>

<template>
  Page visits: {{ data }}
</template>

useLazyAsyncData

<template>
  <div>
    {{ pending ? 'Loading' : count }}
  </div>
</template>

<script setup>
const { pending, data: count } = useLazyAsyncData('count', () => $fetch('/api/count'))
watch(count, (newCount) => {
  // Because count starts out null, you won't have access
  // to its contents immediately, but you can watch it.
})
</script>

element-plus配置

安装

配置

插件

在根目录plugins下创建antd.client.js

import Antd from "ant-design-vue";
import "ant-design-vue/dist/antd.css";

export default defineNuxtPlugin((nuxtApp) => {
    nuxtApp.vueApp.use(Antd)
});

使用

<template>
    <a-button type="primary">按钮</a-button>
</template>

基础配置文件

export default defineNuxtConfig({
  app: {
    head: {
      charset: "UTF-8",
      viewport: "width=device-width, initial-scale=1.0",
      title: "首页",
      meta: [{ name: "description", content: "首页" }],
    },
  },
  modules: ["@pinia/nuxt", "@pinia-plugin-persistedstate/nuxt"],
  runtimeConfig: {
    // 服务器端可以获取
    apiSecret: "123",
    // 客户端获取
    public: {
      apiBase: "/api",
    },
  },
  css: [
    "~/assets/css/base.css",
    "~/assets/css/flex.css",
  ],
  vite: {
    css: {
      preprocessorOptions: {
        less: {
          additionalData: '@import "assets/css/var.less";',
        },
      },
    },
  },
});

开发构建命令

命令功能
npm run dev启动本地开发服务器
npm run build使用混合渲染模式创建一个.output目录,其中包含所有应用程序、服务器和依赖项,可用于生产
npm run genarate使用SPA方式预渲染应用程序的每个路由,将结果存储在纯 HTML 文件中,可以部署在任何静态托管服务上,注意:服务端渲染的内容都可正常访问(已知$fetch请求数据会异常);客户端渲染如果涉及到访问Nuxt的server api等情况会不正常,访问外部数据API正常,请上线前完整验证
npm run previewbuild构建打包后,启动本地预览服务
npm run postinstall在应用程序中创建.nuxt目录并生成类型(没用过)

部署

构建打包

npm run build

pm2部署

在根目录创建一个配置文件ecosystem.config.js

module.exports = {
  apps: [
    {
      name: 'nuxt3-prod',
      exec_mode: 'cluster',
      instances: 'max',
      script: './.output/server/index.mjs'
    }
  ]
}

把.output和配置文件上传到服务器,然后使用pm2启动,目录结构可以自己调整

pm2 start ecosystem.config.js // 启动服务

pm2 list // 查看是否启动成功

设置自动启动

当服务器挂了重新恢复后,之前的node服务还在

pm2 startup