Nuxt3 入门日记(三)

1,463 阅读8分钟

如何在 Nuxt 项目中引入 UI 库、引入 Tailwindcss、运用全局样式以及深入了解 Nuxt 的请求数据方法。

全局样式

Nuxt主要使用两种方式可以配全局样式:

  • app.vue 中引入。
  • 配置文件 nuxt.config.ts;

由于一般我们开发都会使用css预处理器,所以我会先安装sass,原生css的引用方法和其并无区别,大家按照内容操作就可以。

app.vue 引入css:

app.vue中引入我们可以直接在app.vuestyle里面引入css 或者直接在script里面导入

app.vue
<style>
@import url(~/assets/css/other.css);
</style>

<script>
import '~/assets/css/main.css'
</script>

nuxt.config.ts 配置全局样式:

export default defineNuxtConfig({
  css: ["assets/css/global.scss"]
  })
export default defineNuxtConfig({
  vite: {
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: '@use "~/assets/css/global.scss" as *;',
        },
      },
    },
  },
  })

当然除了上述两种方式nuxt还支持使用useHead hook 实现link引入第三方css,具体实现根据大家业务需求选择。

Nuxt里面使用组件库

这就不得不提到Nuxt的一个叫做模块的概念了,Nuxt 模块是在开发模式下使用 Nuxt 或构建用于生产的项目时按顺序运行的函数。 使用模块,可以将自定义解决方案封装、正确测试并共享为 npm 包,而无需向项目添加不必要的样板,也不需要更改 Nuxt 本身。

所以在现阶段使用的UI库基本都是被打成模块提供给nuxt 开发者。我们在一般常用的UI库文档可以找到Nuxt 项目的引入方法,同样我们可以在Nuxt Modules找到相应的模块。

而同时nuxt项目默认给我们加入了一个插件,也就是vite-plugin-vue-devtools

image.png

我们可以直接在插件中直接找到模块安装,会自动将依赖下载并且配置好nuxt.config.ts

this.gif

如果通过指令安装就需要手动新增这个modules。

export default defineNuxtConfig({
  modules: ['shadcn-nuxt']
  })

大家也可以在自己的vue项目里面使用该插件,插件可以提供其它内容。

这样我们就引入了shadcn的组件库,但是shadcn是比较特殊的,不仅仅需要引入同时它依赖于TypeScriptTailwindcss

整合Tailwindcss

原子化 css 已经非常流行了,例如 Tailwindcss、Windicss 等,能够提升开发效率和体验,因为我们使用shadcn需要用到Tailwindcss所以我们这里选择Tailwindcss

我们和上面引入shadcn 的方法一样去下载模块,模块下载完之后,由于shadcn强依赖于Tailwindcss所以我们无需做更多操作 根据官网内容配置

export default defineNuxtConfig({
  modules: ['@nuxtjs/tailwindcss', 'shadcn-nuxt'],
  shadcn: {
    /**
     * Prefix for all the imported component
     */
    prefix: '',
    /**
     * Directory that the component lives in.
     * @default "./components/ui"
     */
    componentDir: './components/ui'
  }
})

执行命令生成 components.json配置文件,同时会将我们所需要的Tailwindcss文件一应设置好,大家可以根据自己需求选择不同的文件名称等等

npx shadcn-vue@latest init

接下来在遇到需要使用的组件就执行引入命令。

npx shadcn-vue@latest add button

就可以直接在页面和租价内容使用该组件了,不需要再在页面做导入

//----[[more]]/[id].vue
<template>
  <div class="title">参数</div>
  <div>query {{ route.query.id }} age{{ route.query.age }}</div>
  <div>params {{ route.params.id }} </div>
  <Button>Button</Button>
  <div class="one two" v-for="item in 10">
    这里是内容
  </div>
</template>

上面主要告知大家如何引入第三方模块,由于有些模块的特殊性,不能仅仅安装依赖以及简单配置nuxt.config.tsmodelus就可以使用,具体还需要大家查看模块官方文档。

nuxt的网络请求方法

在网络请求方面Nuxt为我们提供了五种数据请求方法,他们各自有各自的特点接下来我会介绍这五种方法的使用和特点以及如何封装请求方便大家使用。

如果对服务端渲染不了解的同学可能会觉得,我不可以直接用axios或者原生的fetch来请求吗,为什么要用$fetch

因为我们需要考虑调用时机的问题,比如我这次项目的需求虽然是官网,但是我要求里面请求接口的动态内容,比如热点,要支持SEO收录,那么这个动态内容必须要在服务端渲染就已经获取到最新值并完成服务端渲染,返回一个动态的html,那么这部分网络请求也必须要在服务端渲染的时候已经完成,所以我们需要使用Nuxt的服务端渲染,这里就涉及到请求的调用时机问题,是首屏渲染阶段还是其它阶段,$fetch帮我们自动处理了这部分判断。

  • $fetch;
  • useAsyncData;
  • useLazyAsyncData;
  • useFetch;
  • useLazyFetch。

$fetch

我们来看下如何使用$fetch 来获取后端数据,第一个参数仅需填入url就可以了默认为get请求。

//request.vue
const res = await $fetch('/api/provideData')
console.log(res);
//其它方法根据内容配置
//const res = await $fetch('/api/provideData', {  
//method: 'POST',
//body: {
//      //other data
//    }
//  }
//)

我们观察控制台打印

image.png

以及服务端打印 其中count是我在接口中定义的全局变量模拟数据库数据,当接口发生调用就会+1。初始值为1。

image.png

我们发现count的值确实增加了两次说明发生了两次请求。

那我们很容易发现一个问题,有些场景我们需要记录接口请求的次数作为一个数据指标,而且有些接口请求是要对数据库做操作的,那么多请求了这一次就会有问题了。并且如果说这次请求操作了数据库内容,导致客户端请求的内容和服务端请求的内容不一致,那么客户端渲染的内容肯定也就会和服务端渲染内容不一致,造成水合错误。

所以如何避免这个问题Nuxt给我们提供了一个办法

useAsyncData

我们可以使用 useAsyncData 搭配$fetch 使用,实现只有服务端请求一次的效果,避免重复请求。

我们将它配合$fetch 共同使用

useAsyncData使用传入两个参数,第一个参数是请求的key,第二个则是逻辑的回调函数。

第一个key是可以不传的,当key不传的时候,Nuxt会帮助我们自动生成这个key。

// let response = await useAsyncData('data', () => {
//   console.log(11111111111);

//   return $fetch('/api/provideData')
// })
// console.log(response.data.value);

我们在方法中间打印一下记录,并观察服务端打印内容。

image.png

我们发现客户端渲染的部分是没有打印方法内部的记录,说明请求只在服务端渲染发生了一次

image.png

观察服务端打印,发现count确实只增加1,执行一次。

使用useAsyncData 的情况,Nuxt就会将服务端请求得到的内容先保存,然后根据useAsyncDatakey在客户端渲染阶段再拿回来,如果发现key存在的话,就不会在请求了。但是如果key不存在那么依旧会请求。

如果我们要触发一个事件再请求方法

 <div @click="handleClick">
    请求内容页{{ data?.name }}
  </div>
<script setup lang="ts">
const handleClick= async ()=>{
 let response = await useAsyncData('data', () => {
   return $fetch('/api/provideData')
 })
return response
}
</script>

image.png 我们会发现它就会正常调用,每次请求都会重新发起

这里补充一点使用useAyncData请求的到的数据

let { data, refresh } = await useAsyncData('data', () => {
  return $fetch('/api/provideData')
})
console.log(data);

image.png

是一个由ref包裹的响应式变量。

useFetch

useFetch是对useAsyncData$fetch的封装,和我们上面使用useAsyncData处理内容达成的效果一致。所以使用useFetch一样可以避免我们上面所说的$fetch重复请求的问题。


let { data, refresh } = await useFetch('/api/provideData');
refresh();

大家可以注意到这里我解构获得了一个refresh方法,该方法的作用就是刷新请求。

那么查看上面的代码假设从进入首屏开始我们会请求几次 provideData这个接口?

很容易理解的就是useFetch只会在服务端请求一次,而refresh()也肯定会在服务端请求一次,所以这里就是两次。

refresh实际上和useFetch特性是一致的,如果服务端渲染已经请求了,客户端渲染不会再重复请求,实际上refresh请求也就是一次。

useLazyAsyncData 和useLazyFetch

我们观察这两个方法名称,很容易得到的信息就是和它们上文提到的useAsyncData 以及useFetch 有一定联系。

实际上我们在直接使用上面两个api可以达成和这两个api一样的效果。

//等价
let { data, refresh } = await useFetch('/api/provideData', { lazy: true });
let { data, refresh } = await useLazyFetch('/api/provideData');

同理useAyncData 也可以设置lazytrue

而设置lazy的作用就是不会阻塞路由导航。

let { data, refresh, status } = useFetch('/api/provideData');
console.log(status.value);

useFetch 可以导出一个status反应请求当前的状态。

image.png

可以发现我们服务端请求的时候statuspending,而在客户端的时候status已经变为了success。 我们利用该特性,尝试做一个过渡状态。

<div>
   <div v-if="status === 'success'"> 请求内容页{{ data?.name }}</div>
   <div v-else> 加载中............</div>
   </div>

但是我们会发现无论怎么刷新页面都不会有任何变化,根本不存在过渡状态。

打发.gif

原因大家应该很容易理解,根据前面讲的服务端渲染的内容,我们知道实际上我们看见的时候服务端已经请求完成得到的是success的内容。

页面已经完成了请求结束后的内容渲染所以当然不会有pending状态。

但是如果在客户端渲染阶段,比如说我从首页到/request 页面那么就可以看见效果了。

打.gif

我们再观察一下代码,会发现我这次发起请求和之前发起请求有一点差异。

之前我请求使用了await 而这次没有, 我们知道await是将异步的代码变为同步,如果我们把await加回去。

我们会发现客户端渲染的过渡效果也不见了,因为页面渲染会等待我请求完成之后执行。

打发达瓦.gif

那我们还希望可以先渲染页面dom而不是被异步请求阻塞的话要怎么办

let { data, refresh, status } = await useFetch('/api/provideData', { lazy: true });

这样就实现了,按照上文所说的等价原则就能理解useLazyFetchuseAyncData的使用了。

请求封装

下面是我对useFetch做的一个简单封装案例,对请求和响应做一些简单的处理。

import { useToast } from "~/components/ui/toast";

// 定义一个名为 useRequest 的函数,接收 url 和 options 两个参数
export const useRequest = (url: string, options: any) => {
// 获取运行时配置
  const config = useRuntimeConfig();
  // 获取 Nuxt 应用实例
  const nuxtApp = useNuxtApp();
  // 从 useToast 中获取 toast 对象,用于显示提示信息
  const { toast } = useToast();

  // 使用 useFetch 发送请求,并配置各种处理函数和选项
  return useFetch(url, {
    // 设置请求的基础 URL,从运行时配置中获取
    baseURL: config.public.apiBase,
    // 请求拦截处理函数
    onRequest({ options }) {
      // 从本地存储中获取 token
      const token = localStorage.getItem('token');
      // 如果有 token,则在请求头部添加 Authorization 字段
      options.headers = {
        ...options.headers,
        Authorization: token ? `Bearer ${token}` : '',
      };
      // 打印请求的 URL 和头部信息,方便调试
      console.log(`Requesting ${url} with headers ${JSON.stringify(options.headers)}`);
    },
    // 响应成功处理函数
    onResponse({ response }) {
      // 获取响应的状态码和数据
      const { status, _data } = response;
      // 如果响应状态码在 200 到 300 之间(表示成功)
      if (status >= 200 && status < 300) {
        // 但如果数据中的 code 不为 200(表示业务逻辑上的错误)
        if (_data.code !== 200) {
          // 如果在客户端环境
          if (import.meta.client) {
            // 使用 toast 显示错误提示信息
            toast({
              title: '请求出现问题',
              description: _data.message,
              variant: 'destructive',
            });
          } else {
            // 如果不在客户端环境,使用 Nuxt 应用实例进行页面导航到错误页面,并传递错误信息
            nuxtApp.runWithContext(() => {
              navigateTo({ path: '/error', query: { message: _data.message, code: _data.code } });
            });
          }
        }
      }
      // 打印响应状态码和数据,方便调试
      console.log(`Response status: ${status}, data: ${JSON.stringify(_data)}`);
    },
    // 响应错误处理函数
    onResponseError({ error, response }) {
      // 获取错误信息,如果错误对象中有 message 则使用,否则使用响应数据中的 message
      let message = error?.message;
      message = response._data.message;
      // 如果在客户端环境
      if (import.meta.client) {
        // 使用 toast 显示错误提示信息
        toast({
          title: '请求出现错误',
          description: message,
          variant: 'destructive',
        });
      } else {
        // 如果不在客户端环境,使用 Nuxt 应用实例进行页面导航到错误页面,并传递错误信息
        nuxtApp.runWithContext(() => {
          navigateTo({ path: '/error', query: { message, code: response._data.code } });
        });
      }
      // 打印错误信息、响应状态码和数据,方便调试
      console.error(`Error: ${message}, response status: ${response.status}, data: ${JSON.stringify(response._data)}`);
    },
    // 扩展传入的 options 参数
    ...options
  });
};

以供大家参考,大家可以根据自己需要做更符合业务的封装比如加入 retry选项来设置重试次数和重试间隔时间

//onResponseError
 retry: {
       retries: 3,
       delay: 1000,
     },
     //...options

那么Nuxt的请求相关介绍就到这里了,更多内容大家可以自己去文档和社区查看详情。下一次我将对Nuxt 的内部状态管理,以及Nuxt的异常处理。以及配置信息做一些介绍。也会回头对该篇的部分内容做一个补充