nuxtjs学习-目录、请求、状态管理

374 阅读13分钟

视频教程 www.bilibili.com/video/BV11W…

官网 nuxt.com.cn/

目录

  • .nuxt 临时文件
  • .output 打包输出
  • assets 静态资源 assets/ 目录用于添加构建工具将处理的所有网站资源。
  • components 组件目录 会自动注册为全局组件
  • composables使用 composables/ 目录将您的 Vue 组合式函数自动导入到您的应用程序中。
  • content
  • layouts 布局页面
  • middleware 中间件
  • modules
  • node_modules 依赖
  • pages路由
  • plugins插件
  • public 公共资源 里面的文件不会进行打包,在访问的时候不需要添加public前缀
  • server 服务器
  • utils 在整个应用程序中自动导入您的实用程序函数
  • .env
  • .gitignore
  • .nuxtignore
  • app.vue 入口文件 主页面
  • app.config.ts
  • error.vue 错误处理页面
  • nuxt.config.ts nuxt配置文件
  • package.json 安装依赖
  • tsconfig.json

composables

使用 composables/ 目录将您的 Vue 组合式函数自动导入到您的应用程序中。

在这个目录导出的内容,在使用的时候不需要引入

在根目录创建composables目录,在目录下面创建myCom.js文件,代码如下

// 导出一个add函数
export const add = (x, y) => {
  return x + y;
};
​
// 使用默认的方式
export default function (x, y) {
  return x + y;
}

导出的方法可以直接使用,使用默认导出的方式是直接使用文件名

console.log('计算',add(1,2));
console.log('默认方式',myCom(1,2));

nuxt只会扫描一层目录,如果composables里面还有子目录,是不会自动导入的

如果想要扫描多层,需要修改配置文件

地址 composables/ · Nuxt 目录结构 - Nuxt 中文 (nuxtjs.org.cn)

imports: {
 dirs: [
   // Scan top-level modules 扫描顶层模块
   "composables",
   // ... or scan modules nested one level deep with a specific name and file extension
   // 用特定名称和文件扩展名扫描嵌套在一级深处的模块
   "composables/*/index.{ts,js,mjs,mts}",
   // ... or scan all modules within given directory
   // 扫描给定目录中的所有模块
   "composables/**"
 ]
},

utils使用方式和composables一样

$fetch和useAsyncData

Nuxt 使用 ofetch 在全局暴露 $fetch 助手,用于进行 HTTP 请求。

$fetch是使用Promise封装的

编写接口

在根目录的server目录下面创建api/myIp.js,代码如下

export default defineEventHandler(event => {
  console.count('次数')
​
  
  // 通过return 的方式进行返回数据  可以直接返回json对象
  return {
    code: 200,
    ip: "171.95.220.50",
    country: "中国",
    region: "四川",
    city: "南充",
    asn: "AS4134",
    isp: "电信",
    latitude: "",
    longitude: "",
    LLC: "Chinanet"
  };
});
​

也可以直接使用在线的公共接口地址 UApi-免费公益开放的API接口平台 (uapis.cn)

$fetch

在vue文件中使用

<!-- \pages\index.vue -->
<script setup lang="ts">
const res = await $fetch('/api/myIp')
console.log("🚀 ~ res:", res)
</script>
<template>
  <div>首页</div>
</template>

查看控制台可以看出来是在服务端执行了一次,然后在客户端执行了一次,接口也执行了两次,如果是操作数据库会存在问题,需要只在客户端执行,因此需要修改一下

<!-- \pages\index.vue -->
<script setup lang="ts">
  if (import.meta.client) {
    const res = await $fetch("/api/myIp");
    console.log("🚀 ~ res:", res);
  }
</script>
<template>
  <div>首页</div>
</template>

这样写法是不正确的,因此官方提供了useAsyncData

useAsyncData

将上面代码修改为如下,返回的数据是响应式数据。

<!-- \pages\index.vue -->
<script setup lang="ts">
  // if (import.meta.client) {
  //   const res = await $fetch("/api/myIp");
  //   console.log("🚀 ~ res:", res);
  // }
  // 第一个参数是一个key  第二个参数是一个函数
  const res = useAsyncData("test", () => {
    return $fetch("/api/myIp");
  });
  console.log("🚀 ~ res ~ res:", res.data.value);
</script>
<template>
  <div>首页</div>
</template>

此时在服务端数据的是null,在客户端是返回的数据,原因是useAsyncData也是异步的,需要在useAsyncData前面添加await,这样服务端和客户端都是打印出数据,接口只会执行一次

useAsyncData只会在服务端执行一次

通过事件调用

在页面中创建一个按钮,在按钮上添加一个事件,在事件中获取接口的数据

<!-- \pages\index.vue -->
<script setup lang="ts">
  // if (import.meta.client) {
  // const res = await $fetch("/api/myIp");
  // console.log("🚀 ~ res:", res);
  // }
  // 第一个参数是一个key  第二个参数是一个函数
  // const res =await  useAsyncData("test", () => {
  //   return $fetch("/api/myIp");
  // });
  // console.log("🚀 ~ res ~ res:", res.data.value);
​
  const test1 = async () => {
    const res = await useAsyncData("test", () => {
      console.log('打印');
      
      return $fetch("/api/myIp");
    });
    console.log("🚀 ~ res ~ res:", res.data.value);
  };
</script>
<template>
  <div>首页</div> <br>
  <el-button type="primary" size="default" @click="test1">test1</el-button>
  
</template>

image-20240923164108858

这样就变成在客户端进行调用执行了,同样是执行了一次接口

useAsyncData在服务端只会执行一次

useAsyncData函数的key在客户端和服务端执行的期间需要是唯一的,如果key不一样,那么会调用两次

useFetch,lazy,refresh

useFetch

useFetch实际上是useAsyncData$fetch的语法糖,返回数据和useAsyncData是一样的

useFetch自动添加了一些属性,例如:refresherrorpending(已经废弃,使用status代替)

refresh

作用是重新发送一次请求,只会在服务端执行一次

一般用于刷新token

status

请求的状态

<!-- \pages\index.vue -->
<script setup lang="ts">

// 第一个参数是url
//  没有useAsyncData中的key   是自动维护的
const {refresh,status} = await useFetch('/api/myIp')
console.log("🚀 ~ res:",status.value)
// refresh()

const test1 = () => {}
</script>
<template>
  <div>首页</div> <br>
<div v-if="status === 'pending'">
    正在加载...
  </div>
  <div v-if="status === 'success'">
    加载完成
  </div>
  <el-button type="primary" size="default" @click="test1">test1</el-button>
  
</template>

status是返回的success,原因是接口请求是异步的,只会返回结果,去掉await,会先返回pending,然后返回success,通过这个状态可以在页面显示过度效果

在服务端只会显示成功的状态,但是在客户端会有两个状态

  • 服务端:刷新的时候、直接进入页面都是服务端,页面是服务端返回的
  • 客户端:在其他页面跳转到当前页面,status会显示默认值pending,成功之后就显示success

lazy

useFetch前面添加await就无法展示过度效果了,如果想要两者都展示,那么就需要使用lazy

<!-- \pages\index.vue -->
<script setup lang="ts">

// 第一个参数是url
//  没有useAsyncData中的key   是自动维护的
const {refresh,status,data} =  useFetch('/api/myIp',{
  lazy:true
})
console.log("🚀 ~ res:",status.value,data.value)
// refresh()

const test1 = () => {}
</script>
<template>
  <div>首页</div> <br>
  <div v-if="status === 'pending'">
    正在加载...
  </div>
  <div v-if="status === 'success'">
    加载完成
  </div>
  <el-button type="primary" size="default" @click="test1">test1</el-button>
  
</template>

lazy设置成true,就会先渲染标签

官方还提供了useLazyFetch的命令,可以实现上面的效果

<!-- \pages\index.vue -->
<script setup lang="ts">

// 第一个参数是url
//  没有useAsyncData中的key   是自动维护的
// const {refresh,status,data} =  useFetch('/api/myIp',{
//   lazy:true
// })
// console.log("🚀 ~ res:",status.value,data.value)
// refresh()


const {refresh,status,data} = await useLazyFetch('/api/myIp')
console.log("🚀 ~ res:",status.value,data.value)


const test1 = () => {}
</script>
<template>
  <div>首页</div> <br>
  <div v-if="status === 'pending'">
    正在加载...
  </div>
  <div v-if="status === 'success'">
    加载完成
  </div>
  <el-button type="primary" size="default" @click="test1">test1</el-button>
  
</template>

useFetch封装

为什么不用axios,因为他没有处理两次请求的问题

composables中创建api.js进行封装useFetch

请求拦截器

export const apiCore = (url, opt) => {
  // 获取全局变量
  // 环境变量 nuxt.config.ts 文件的 runtimeConfig中定义
  const config = useRuntimeConfig();

  return useFetch(url, {
    baseURL: config.public.baseUrl,
    // 请求拦截器
    onRequest: ({ options }) => {
      // 获取token
      let token = "";
      //   只能在客户端获取token
      if (import.meta.client) {
        token = localStorage.getItem("token");
      }
      //   添加 headers
      options.headers = {
        // 添加认证
        Auth: `token ${token}`,
        // 原本的headers
        ...options.headers
      };
    },
    ...opt
  });
};

// 封装get
export const getApi = (url, opt = {}) => {
  // console.log("🚀 ~ getApi ~ url, opt:", url, opt);
  // return apiCore(url, {
  //   method: "GET"
  // });
  return new Promise((resolve, reject) => {
    return apiCore(url, {
      method: "GET",
      ...opt
    }).then(res => {
      // 直接将真正的数据返回
      resolve(res.data.value)
    }).catch(err => {
      reject(err)
    })
  });
};

在页面中使用

<!-- \pages\index.vue -->
<script setup lang="ts">
  const test0 = async () => {
    const res = await getApi("/uapi/api/weather?name=北京市");
    // const res = await getApi('/api/myIp')
    console.log("服务端请求:", res);
  };
  test0();
  const test1 = async () => {
    const res = await getApi("/uapi/api/say");
    // const res = await getApi('/api/myIp')
    console.log("随机一言:", res);
  };
  const test2 = async () => {
    const res = await getApi("/uapi/api/weather?name=北京市");
    // const res = await getApi('/api/myIp')
    console.log("天气查询:", res);
  };
  const login = async () => {
    localStorage.setItem("token", "123456789");
  };
</script>
<template>
  <div>首页</div>
  <br />

  <el-button type="primary" size="default" @click="test1">随机一言</el-button>
  <el-button type="primary" size="default" @click="test2">天气查询</el-button>
  <el-button type="primary" size="default" @click="login">模拟登录</el-button>
</template>

在客户端和服务端都可以正常运行

响应拦截器

axiosonResponsehttp状态码2XX之内的,onRequestErrorhttp状态码2xx之外的,在 useFetch 中不会区分

onResponseonRequestErrorresponse参数里面会有一个ok属性 ,如果oktrue,就在onResponse中执行,如果是false,则两个都会执行

http的状态码是2xx,那么ok就是truehttp的状态码是4xx500,那么ok就是false

export const apiCore = (url, opt) => {
  // 获取全局变量
  // 环境变量 nuxt.config.ts 文件的 runtimeConfig中定义
  const config = useRuntimeConfig();

  const nuxtApp = useNuxtApp();

  return useFetch(url, {
    baseURL: config.public.baseUrl,
    // 请求拦截器
    onRequest: ({ options }) => {
      // 获取token
      let token = "";
      //   只能在客户端获取token
      if (import.meta.client) {
        token = localStorage.getItem("token");
      }
      //   添加 headers
      options.headers = {
        // 添加认证
        Auth: `token ${token}`,
        // 原本的headers
        ...options.headers
      };
    },
    /**
     * 在axios中 onResponse 是http状态码2XX之内的,onRequestError是http状态码2xx之外的,在 useFetch 中不会区分
     */
    // onResponse和onRequestError的response参数里面会有一个 ok 属性 ,如果ok是true,就在onResponse中执行,否则在onRequestError中执行

    // 相应拦截器
    onResponse({ response }) {
      // http状态码是2xx 的情况
      if (response.status >= 200 && response.status < 300) {
        // 只有状态码是2xx的时候才能执行
        // 根据后端返回的code值设置成功,例如 后端返回的code是200表示成功
        // 这里的code值是后端返回的,上面的200是http的状态码
        if (response._data.code !== 200) {
          // 这里表示后端返回错误
          // ElMessage.error(response._data.code + "" + response._data.message);
          // elmessage 在服务端没有  因此需要添加客户端限制
          if (import.meta.client) {
            ElMessage.error(response._data.code + "" + response._data.message);
          } else {
            // 在服务端跳转到错误页面
            console.log("服务端");

            nuxtApp.runWithContext(() => {
              // 注意  这个页面需要再导航守卫中放行才可以
              navigateTo({
                path: "/error",
                query: {
                  code: response._data.code,
                  message: response._data.message
                }
              });
            });
          }
        }
      } else {
        // http状态码不是2xx的情况
        if (import.meta.client) {
          ElMessage.error(response._data.code + "" + response._data.message);
        } else {
          // 在服务端跳转到错误页面

          nuxtApp.runWithContext(() => {
            // 注意  这个页面需要再导航守卫中放行才可以
            navigateTo({
              path: "/error",
              query: {
                code: response._data.code,
                message: response._data.message
              }
            });
          });
        }
      }
    },
    // 错误处理部分  代码没有生效
    onRequestError({ response }) {
      if (import.meta.client) {
        ElMessage.error(response._data.code + "" + response._data.message);
      } else {
        // 在服务端跳转到错误页面

        nuxtApp.runWithContext(() => {
          // 注意  这个页面需要再导航守卫中放行才可以
          navigateTo({
            path: "/error",
            query: {
              code: response._data.code,
              message: response._data.message
            }
          });
        });
      }
    },
    ...opt
  });
};

// 封装get
export const getApi = (url, opt = {}) => {
  return new Promise((resolve, reject) => {
    return apiCore(url, {
      method: "GET",
      ...opt
    })
      .then(res => {
        // 直接将真正的数据返回
        resolve(res.data.value);
      })
      .catch(err => {
        reject(err);
      });
  });
};

在视频教程中错误处理使用的是onRequestError,但是在我的代码中没有执行,因此将处理方式放在了onResponse

处理跨域

在文件nuxt.config.ts添加nitro

nitro: {
    devProxy: {
      // 被代理的路径
      "/uapi": {
        // 代理的目标地址
        target: "https://uapis.cn",
        // 表示在代理请求时更改源域名
        changeOrigin: true,
        // 表示在转发请求时,会在目标 URL 前添加原始请求的路径
        prependPath: true,
      },
    },
  },

状态处理

nuxt自带了一个状态处理是state

state

<!-- \pages\index.vue -->
<script setup lang="ts">
  //  定义一个数据
let res = useState('num',() => {
  return 0
})
// 会执行两次  分别是在服务端执行和客户端执行
res.value++
console.log("🚀 ~ res ~ res:", res.value)// 1 2
console.log('数据',useState('num').value)//1 2



</script>
<template>
  <div>首页</div>
  <br />

  <el-button type="primary" size="default" >随机一言</el-button>
</template>

useState是定义一个数据,在服务端和客户端都会执行,可以通过俩个种方式获取到结果

image-20240924103517080

创建的state在服务端和客户端都可以访问到,是共享的

在服务端创建的数据:在客户端和服务端都可以获取到

在客户端创建的数据:只能在客户端获取到数据,在服务端获取不到数据,因为直接在服务端获取数据相当于是刷新页面,刷新页面之后数据就丢失了

点击页面跳转还是能够获取到数据,因为这是客户端渲染

如果直接在地址栏输入地址,是后去不到数据的,因为这是服务端渲染,是会刷新页面的

pinia

使用pinia

安装pinia,nuxt.com.cn/modules/pin…

npm i pinia @pinia/nuxt

安装完成之后在nuxt.config.ts文件的modules进行引入

  modules: ["@element-plus/nuxt",'@pinia/nuxt'],

接下来就可以使用了

composables中创建一个userStore.js

import { defineStore } from "pinia";

// 第一个参数是名称  第二个参数是回调函数
export const userStore = defineStore("userStore", () => {
  // 定义内容
  const token = ref("");

  // 设置token
  const setToken = val => {
    token.value = val;
  };

  // 获取token
  const getToken = () => {
    return token.value;
  };
  //   将数据和方法暴漏
  return { token, setToken, getToken };
});

在页面中使用,上面导出的是一个函数

<!-- \pages\index.vue -->
<script setup lang="ts">
  const login = () => {
    userStore().setToken("123");
  };
</script>
<template>
  <div>首页</div>
  <br />

  <el-button type="primary" size="default" @click="login">设置token</el-button>
</template>

在刷新页面之后,pinia中的数据就丢失了

在服务端设置的数据,在客户端能够获取到;在客户端设置的数据,在服务端也是获取不到,和state一样

持久化

引入pinia持久化插件,nuxt.com.cn/modules/pin…

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

安装之后进行引入

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

在教程中还需要在nuxt.config.ts文件中配置build

build:{
 transpile:["pinia-plugin-persistedstate"]
},

教程的nuxt版本是3.12.3,学习时的版本是3.13.0

然后对userStore进行配置持久化

import { defineStore } from "pinia";

// 第一个参数是名称  第二个参数是回调函数
export const userStore = defineStore("userStore", () => {
  // 定义内容
  const token = ref("");

  // 设置token
  const setToken = val => {
    token.value = val;
  };

  // 获取token
  const getToken = () => {
    return token.value;
  };
  //   将数据和方法暴漏
  return { token, setToken, getToken };
},{
    persist:{
        key:"user", // 存储的名称 非必须
        storage:persistedState.localStorage
    }
});

这样就可以在客户端和服务端进行互通数据,但是在提示工具中是看不到的,需要在客户端打印一下才能看到,因为客户端去获取store,那么服务端的数据就会被同步过来,但是数据不是持久化的,之后在客户端修改之后才会进行持久化。

如果不打印可以在控制台输入window.$pinia.state.value查看数据

即时设置的持久化插件也不能实现客户端设置数据,服务端获取数据,只能是服务端设置数据,客户端获取

保存到cookie实现客户端和服务端共享数据

如果将持久化数据保存到cookie中,在客户端设置数据,服务端是可以获取到的

持久化插件默认是保存到cookie中的

import { defineStore } from "pinia";

// 第一个参数是名称  第二个参数是回调函数
export const userStore = defineStore("userStore", () => {
  // 定义内容
  const token = ref("");

  // 设置token
  const setToken = val => {
    token.value = val;
  };

  // 获取token
  const getToken = () => {
    return token.value;
  };
  //   将数据和方法暴漏
  return { token, setToken, getToken };
},{
    // persist:{
    //     key:"user", // 存储的名称 非必须
    //     storage:persistedState.localStorage
    // }
    persist:true
});

修改拦截器和导航守卫

修改导航守卫

修改token的获取

export default defineNuxtRouteMiddleware((to, from) => {
  // 白名单
  let passURL = ["/login","/about", "/error"];
  if (!passURL.includes(to.path)) {
    let token = "";
    if (import.meta.client) {
        // 客户端在pinia中获取token
      token = userStore().getToken();
      if (!token) {
        return navigateTo({
          path: "/login",
          query: {
            code: 401,
            message: "请先登录"
          }
        });
      }
    }
    
  }
});

修改拦截器

获取token同样替换

export const apiCore = (url, opt) => {
  // 获取全局变量
  // 环境变量 nuxt.config.ts 文件的 runtimeConfig中定义
  const config = useRuntimeConfig();

  const nuxtApp = useNuxtApp();

  return useFetch(url, {
    baseURL: config.public.baseUrl,
    // 请求拦截器
    onRequest: ({ options }) => {
      // 获取token
      let token = userStore().getToken();

      //   添加 headers
      options.headers = {
        // 添加认证
        Auth: `token=${token}`,
        // 原本的headers
        ...options.headers
      };
    },
    /**
     * 在axios中 onResponse 是http状态码2XX之内的,onRequestError是http状态码2xx之外的,在 useFetch 中不会区分
     */
    // onResponse和onRequestError的response参数里面会有一个 ok 属性 ,如果ok是true,就在onResponse中执行,否则在onRequestError中执行

    // 相应拦截器
    onResponse({ response }) {
      console.log("🚀 ~ onResponse ~ response:", response);
      // http状态码是2xx 的情况
      if (response.status >= 200 && response.status < 300) {
        // 只有状态码是2xx的时候才能执行
        // 根据后端返回的code值设置成功,例如 后端返回的code是200表示成功
        // 这里的code值是后端返回的,上面的200是http的状态码
        if (response._data.code !== 200) {
          // 这里表示后端返回错误
          // ElMessage.error(response._data.code + "" + response._data.message);
          // elmessage 在服务端没有  因此需要添加客户端限制
          if (import.meta.client) {
            ElMessage.error(response._data.code + "" + response._data.msg);
          } else {
            // 在服务端跳转到错误页面
            console.log("服务端");

            nuxtApp.runWithContext(() => {
              // 注意  这个页面需要再导航守卫中放行才可以
              navigateTo({
                path: "/error",
                query: {
                  code: response._data.code,
                  message: response._data.msg
                }
              });
            });
          }
        }
      }
      // else {
      //   // http状态码不是2xx的情况
      //   if (import.meta.client) {
      //     ElMessage.error(response._data.code + "" + response._data.msg);
      //   } else {
      //     // 在服务端跳转到错误页面

      //     nuxtApp.runWithContext(() => {
      //       // 注意  这个页面需要再导航守卫中放行才可以
      //       navigateTo({
      //         path: "/error",
      //         query: {
      //           code: response._data.code,
      //           message: response._data.msg
      //         }
      //       });
      //     });
      //   }
      // }
    },
    // 错误处理部分  代码没有生效
    onRequestError({ response }) {
      console.log("🚀 ~ onRequestError ~ response:", response);
      if (import.meta.client) {
        console.log("7777+++--+-");

        ElMessage.error(response._data.code + "" + response._data.message);
      } else {
        console.log("888---");
        // 在服务端跳转到错误页面

        nuxtApp.runWithContext(() => {
          // 注意  这个页面需要再导航守卫中放行才可以
          navigateTo({
            path: "/error",
            query: {
              code: response._data.code,
              message: response._data.message
            }
          });
        });
      }
    },
    ...opt
  });
};

// 封装get
export const getApi = (url, opt = {}) => {
  return new Promise((resolve, reject) => {
    return apiCore(url, {
      method: "GET",
      ...opt
    })
      .then(res => {
        // 直接将真正的数据返回
        resolve(res.data.value);
      })
      .catch(err => {
        reject(err);
      });
  });
};

出现问题:在网络里面看不到请求,控制台也没有报错

原因:设置的token是中文,有可能是编码的问题造成的,正常情况下token不可能是汉字

nuxt错误处理

nuxt提供了一个错误处理页面叫做error,在根目录下面创建error.vue文件

error.vue页面,nuxt动态绑定了一个叫errorprops属性

<script setup lang="ts">
  defineProps({
    // nuxt动态绑定了一个叫error的属性
    error: Object
  });

//   清除错误 并跳转到某个页面
clearError({redirect:"/login"})
</script>

<template>
  <div>错误处理</div>
  <!-- 错误码 -->
  <div>{{ error.statusCode }}</div>
  <!-- 错误信息 -->
  <div>{{ error.message }}</div>
</template>
<style lang="scss"></style>
<!-- \pages\index.vue -->
<script setup lang="ts">
  // 手动抛出错误 只能在服务端触发
  // throw createError({ statusCode: 404, message: "自定义错误" });

  // 客户端触发
  const toError = () => {
    showError({ statusCode: 404, message: "自定义错误" });
  }
</script>
<template>
  <div>首页</div>
  <br />
  <!-- <div>{{ aa.cc }}</div> -->

  <el-button type="primary" size="default" @click="toError">错误</el-button>
  

</template>

seo优化

自定义标题是在页面中通过useHead定义

<script setup>
let title = ref('测试标题');
  useHead({
    title: "首页",
    meta: [
      {
        name: "keywords",
        content: "vue3,nuxt3,nuxt"
      },
      {
        name: "description",
        content: "vue3+nuxt3"
      }
    ],
    // 参数是当前显示的标题
    // titleTemplate(titleChunk){
    //   console.log("🚀 ~ titleTemplate ~ titleChunk:", titleChunk)
    //   return titleChunk ? `${titleChunk} - vue3+nuxt3` : "vue3+nuxt3";
    // }
    //  %s 是占位符  显示的是 titleChunk 数据
    // titleTemplate:"%s 首页"
    // titleTemplate:`%s ${title.value}`

  });

</script>

如果app.vue中定义了,那么所有没有设置过的页面都会显示app.vue中的,查看源代码可以看到设置的meta

可以直接设置,也可以通过titleTemplate设置

layout

在根目录创建layouts文件夹,下面创建用于布局的页面

默认布局

创建default.vue文件,将app.vue中的布局代码挪过来

<script setup lang="ts">
console.log('默认布局');

</script>

<template>
  <header>
    <NuxtLink to="/">首页</NuxtLink>
    <NuxtLink to="/about">关于我们</NuxtLink>
    <NuxtLink to="/api">api</NuxtLink>
  </header>
  <main>
    <!-- 需要使用插槽 -->
     <slot></slot>
  </main>
  <footer>尾部</footer>
</template>
<style scoped lang="scss">
  header,
  footer,
  main {
    border: 1px solid #ccc;
  }
</style>

app.vue文件代码

<script setup>
  useHead({
    title: "app",
    meta: [
      {
        name: "keywords",
        content: "vue3,nuxt3,nuxt"
      },
      {
        name: "description",
        content: "vue3+nuxt3"
      }
    ]
  });
</script>

<template>
  <!-- 使用布局 -->
  <!-- 
nuxt-layout标签就是   layouts/default.vue组件

 -->
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>
<style scoped lang="scss"></style>

不适用布局

如果不想使用布局,例如login页面,那么添加如下代码即可

  definePageMeta({
    layout: false
  });

命名布局

layouts文件夹,下面创建custom.vue

<script setup lang="ts">
console.log('默认布局');

</script>

<template>
  <header>
    <NuxtLink to="/">首页</NuxtLink>
    <NuxtLink to="/about">关于我们</NuxtLink>
    <NuxtLink to="/api">api</NuxtLink>
  </header>
  <main>
    <!-- 需要使用插槽 -->
     <slot></slot>
  </main>
  <footer>自定义布局</footer>
</template>
<style scoped lang="scss">
  header,
  footer,
  main {
    border: 1px solid #ccc;
  }
</style>

aboput页面使用上面的布局,layout: 'custom'设置布局样式,custom是文件名称

<script setup lang="ts">
  definePageMeta({
    path: "/about1",
    layout: "custom"
  });
</script>

<template>
  <div>about</div>
</template>
<style lang="scss"></style>

或者在NuxtLayout标签上添加name属性,如果app.vue父组组价中设置了layout,那么需要在definePageMeta中设置layoutfalse

<script setup lang="ts">
  definePageMeta({
    path: "/about1",
    layout: false
  });
</script>

<template>
  <NuxtLayout name="custom">
    <div>about</div>
  </NuxtLayout>
</template>
<style lang="scss"></style>

打包发布

在文件nuxt.config.ts可以配置生产环境开发环境的配置

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  compatibilityDate: "2024-04-03",
  devtools: { enabled: true },
  // 生产环境的配置
  $production: {},

  // 开发环境的配置
  $development: {},
// ……
});

打包命令是npm run build,打包目录是.output目录,打包结束之后执行命令node .output/server/index.mjs就可以了

附录

vue和nuxt对照

vueNuxt含义
router-viewNuxtPage路由入口
router-linkNuxtLink页面跳转
router.pushnavigateTo编程式路由
路由1. /pages目录 2.命名路由 3. 可选路由 4. 自定义路由 5. 嵌套路由vue的路由变成了多种方式,不需要在编写路由表,直接写在/pages
动态路由1. 命名路由 2. 可选路由 3. 全局路由1. 命名路由:创建[id].vue文件,路径不能省略 2. 可选路由:创建[[id]]目录,在目录下创建对应的文件,路径中可以省略 3. 全局路由:相当于是404路由,当所有的路由匹配不到时执行<br /
路由中的name自定义路由在文件中编写,相当于是vue路由的name属性
路由守卫中间件/middleware目录vue中需要写到路由表中,在nuxt中是在middleware目录下,在使用的地方进行引入,也可以直接在页面中编写;全局中间件是名称.global.js,不需要进行引入
axiosuseFetchnuxt提供的方法,可以设置请求拦截器响应拦截器
vuexstate数据管理,statenuxt自带的
piniapiniavuex中数据一般是存到localStorage,在nuxt中推荐放在cookie中,因为这样客户端和服务端都可以设置或获取数据