【4】Vue3 + Nuxt3 核心

803 阅读4分钟

路由中间件(middleware)

◼ Nuxt 提供了一个可定制的 路由中间件,用来监听路由的导航,包括:局部和全局监听(支持再服务器和客户端执行)

◼ 路由中间件分为三种:

  • 匿名(或内联)路由中间件
    • 在页面中使用definePageMeta 函数定义,可监听局部路由。当注册多个中间件时,会按照注册顺序来执行。
definePageMeta({
  //路由中间件
  middleware: [
    //第一个中间件
    function (to, from) {
      console.log(from, 'from');
      console.log(to, 'to');
      //如果返回时空值或者不返回,就会执行下一个中间件
      // 如果返回的是navigate,就会跳转到指定页面
      return navigateTo('/home')
    },
    //第二个中间件
    function (to, from) {
      console.log('第二个');
    },
    function (to, from) {
      console.log('第三个');
    }
  ]
})
  • 命名路由中间件
    • middleware 目录下定义,并会自动加载中间件。命名规范 kebab-case)

image.png

  • 全局路由中间件(优先级比前面的高,支持两端)
    • middleware 目录中,需带 .global后缀的文件,每次路由更改都会自动运行。

image.png

路由验证( validate )

◼ Nuxt支持对每个页面路由进行验证,我们可以通过definePageMeta中的validate属性来对路由进行验证。

  • validate属性接受一个回调函数,回调函数中以 route 作为参数
  • 回调函数的返回值支持:
    • 返回 bool 值来确定是否放行路由
    • true 放行路由
    • false 默认重定向到内置的 404 页面
  • 返回对象 { statusCode:401 } // 返回自定义的 401 页面,验证失败
//路由参数的校验
definePageMeta({
  validate: (route) => {
    return /^\d+$/.test(route.params.id as string)

    return {
      statusCode: 401
    }
  },
})

◼ 路由验证失败,可以自定义错误页面:

在项目根目录(不是pages目录)新建 error.vue

Layout 布局

Layout布局是页面的包装器,可以将多个页面共性东西抽取到 Layout布局 中。

例如:每个页面的页眉和页脚组件,这些具有共性的组件我们是可以写到一个Layout布局中。

Layout布局是使用组件来显示页面中的内容

Layout布局有两种使用方式:

方式一:默认布局

  • ✓ 在layouts目录下新建默认的布局组件,比如:layouts/default.vue
  • ✓ 然后在app.vue中通过内置组件来使用

image.png

在app.vue中使用

 <template>
  <NuxtLayout>
    <!-- 页面 -->
    <NuxtPage />
  </NuxtLayout>
</template>

image.png 方式二:自定义布局(Custom Layout)

  • ✓ 继续在layouts文件夹下新建 Layout 布局组件,比如: layouts/custom-layout.vue
  • ✓ 然后在app.vue中给内置组件 指定name属性 的值为:custom-layout
  // 自定义当前页面的layout
definePageMeta({
  layout: 'custom-layout'
})

也支持在页面中使用 definePageMeta 宏函数来指定 layout 布局

渲染模式

◼ 浏览器 和 服务器都可以解释 JavaScript 代码,都可以将 Vue.js 组件呈现为 HTML 元素。此过程称为渲染。

  • 在客户端将 Vue.js 组件呈现为 HTML 元素,称为:客户端渲染模式
  • 在服务器将 Vue.js 组件呈现为 HTML 元素,称为:服务器渲染模式

◼ 而Nuxt3是支持多种渲染模式,比如:

  • 客户端渲染模式(CSR): 只需将 ssr 设置为 false
  • 服务器端渲染模式(SSR):只需将 ssr 设置为 true
  • 混合渲染模式(SSR | CSR | SSG | SWR):需在 routeRules 根据每个路由动态配置渲染模式
  routeRules: {
    "/": { ssr: true }, //ssr
    "/category": { ssr: false }, //spa应用
    "/cart": { static: true }, // 只会在构建的时候生成一次静态页面
    "/profile": { swr: true }, // 只会生成多次静态页面(会自动重新校验页面的时候重新生成)
  },

Nuxt插件(Plugins)

Nuxt3支持自定义插件进行扩展,创建插件有两种方式:

  • 方式一:直接使用 useNuxtApp() 中的 provide(name, vlaue) 方法直接创建,比如:可在App.vue中创建
    • useNuxtApp 提供了访问 Nuxt 共享运行时上下文的方法和属性(两端可用):provide、hooks、callhook、vueApp等
    //创建插件
const nuxtApp = useNuxtApp()

nuxtApp.provide('formData', () => {
  return '2023-12-31'
})

console.log(nuxtApp.$formData());
  • 方式二:在 plugins 目录中创建插件(推荐)
    • 顶级和子目录index文件写的插件会在创建Vue应用程序时会自动加载和注册
    • .server 或 .client 后缀名插件,可区分服务器端或客户端,用时需区分环境

在 plugins 目录中创建插件

  • 1.在 plugins 目录下创建 plugins/price.ts 插件
  • 2.接着 defineNuxtPlugin 函数创建插件,参数是一个回调函数
  • 3.然后在组件中使用 useNuxtApp() 来拿到插件中的方法

image.png

注意事项:

插件注册顺序可以通过在文件名前加上一个数字来控制插件注册的顺序

✓ 比如:plugins/1.price.ts 、plugins/2.string.ts、plugins/3.date.ts

组件生命周期

监听App的生命周期的Hooks:

  • App Hooks 主要可由 Nuxt 插件 使用 hooks 来监听 生命周期,也可用于 Vue 组合 API 。
  • 但是,如在组件中编写hooks来监听,那 create和setup hooks就监听不了,因为这些hooks已经触发完了监听。
 export default defineNuxtPlugin((nuxtApp) => {
  //监听app的生命周期
  nuxtApp.hook("app:created", (vueApp) => {
    console.log("app:created");
    
  });
  nuxtApp.hook("app:beforeMount", (vueApp) => {
    console.log("app:beforeMount");
    
  });
  nuxtApp.hook("vue:setup", () => {
    console.log("vue:setup");
    
  });
  nuxtApp.hook("app:rendered", (renderContext) => {
    console.log("app:rendered");
    
  });
  nuxtApp.hook("app:mounted", (vueApp) => {
    console.log("app:mounted");
    
  });
});

语法:nuxtApp.hook(app:created, func)

image.png

客户端渲染

image.png

服务器渲染

  • beforeCreate -> setup
  • created

注意

image.png

获取数据

在Nuxt中数据的获取主要是通过下面4个函数来实现(支持Server和Client ):

  • 1.useAsyncData(key, func):专门解决异步获取数据的函数,会阻止页面导航。
    • 发起异步请求需用到 $fetch 全局函数(类似Fetch API)
    • $fetch(url, opts) 是一个类原生fetch的跨平台请求库
// 使用$fetch请求服务端客户端都会发请求
$fetch(BASE_URL + "/2", {
  method: "GET"
}).then(res => {
  console.log(res);
})
    
const data = await useAsyncData<IResultData>('postsInfo', () => {
  return $fetch(BASE_URL + "/2", {
    method: "GET"
  })
})
  • 2.useFetch(url, opts):用于获取任意的URL地址的数据,会阻止页面导航
    • 本质是 useAsyncData(key, () => $fetch(url, opts)) 的语法糖。
 const { data } = await useFetch<IResultData>(BASE_URL + "/2", { method: "GET" })
    
 const infos = await useFetch<IResultData>('/2', {
  method: 'GET',
  baseURL: BASE_URL,
    //请求拦截
  onRequest({ request, options }) {
    console.log(options.method);
    options.headers = {
      token: 'xxxx'
    }
  },
  onRequestError({ response, options, error }) {
    console.log('onRequestError');
  },
  //响应拦截
  onResponse({ response, options }) {
    console.log(response);
  },
  onResponseError({ response, options, error }) {
    console.log('onResponseError');
  }
})
  • 3.useLazyFetch(url, opts): 用于获取任意URL数据,不会阻止页面导航
    • 本质和 useFetch 的 lazy 属性设置为 true 一样
    const { data } = await useLazyFetch<IResultData>(BASE_URL + "/2", { method: "GET", })
  • 4.useLazyAsyncData(key, func):专门解决异步获取数据的函数。 不会阻止页面导航
    • 本质和useAsyncData的lazy属性设置为true一样

image.png

useFetch vs axios

获取数据Nuxt推荐使用 useFetch 函数,为什么不是 axios ?

  • useFetch 底层调用的是$fetch函数,该函数是基于unjs/ohmyfetch请求库,并与原生的Fetch API有者相同API
  • unjs/ohmyfetch 是一个跨端请求库: A better fetch API. Works on node, browser and workers
    • ✓ 如果运行在服务器上,它可以智能的处理对 API接口的直接调用。
    • ✓ 如果运行在客户端行,它可以对后台提供的 API接口 正常的调用(类似 axios),当然也支持第三方接口的调用
    • ✓ 会自动解析响应和对数据进行字符串化
  • useFetch 支持智能的类型提示和智能的推断 API 响应类型。
  • 在setup中用useFetch获取数据,会减去客户端重复发起的请求。
useFetch(url, options)语法

参数

  • url: 请求的路径
  • options:请求配置选项
    • ➢ method、query ( 别名 params )、body、headers、baseURL
    • ➢ onRequest、 onResponse、lazy....
  • 返回值: data, pending, error, refresh
全局封装fetch请求
 import type { AsyncData, UseFetchOptions } from "nuxt/app";

const BASE_URL = "http://jsonplaceholder.typicode.com/posts";
type Method = "GET" | "POST";

export interface IResultData<T> {
  code: number;
  data: T;
}

class MyRequest {
  request<T = any>(
    url: string,
    method: Method,
    data?: any,
    options?: UseFetchOptions<T>
  ): Promise<AsyncData<T, Error>> {
    const newOptions: UseFetchOptions<T> = {
      baseURL: BASE_URL,
      method: method,
      ...options,
    };
    if (method === "GET") {
      newOptions.query = data;
    }
    if (method === "POST") {
      newOptions.body = data;
    }
    return new Promise((resolve, reject) => {
      useFetch<T>(url, newOptions as any)
        .then((res) => {
          resolve(res as AsyncData<T, Error>);
        })
        .catch((err) => {
          reject(err);
        });
    });
  }

  get<T = any>(url: string, params?: any, options?: UseFetchOptions<T>) {
    return this.request(url, "GET", params, options);
  }
  post<T = any>(url: string, data?: any, options?: UseFetchOptions<T>) {
    return this.request(url, "POST", data, options);
  }
}

export default new MyRequest();

Server API

Nuxt3 提供了编写后端服务接口的功能,编写服务接口可以在server/api目录下编写

比如:编写一个 /api/homeinfo 接口

  • 1.在server/api目录下新建 homeinfo.ts
  • 2.接在在该文件中使用 defineEventHandler 函数来定义接口(支持 async)
  • 3.然后就可以用useFetch函数轻松调用: /api/homeinfo 接口了
 export default defineEventHandler((event) => {
  return {
    code: 200,
    data: {
      name: "luabu",
      age: "22",
    },
  };
});

调用 const { data } = await useFetch('/api/homeInfo')

全局状态共享

Nuxt跨页面、跨组件全局状态共享可使用 useState(支持Server和Client ):

useState<T>(init?: () => T | Ref<T>): Ref<T>

useState<T>(key: string, init?: () => T | Ref<T>): Ref<T>

参数:

  • init:为状态提供初始值的函数,该函数也支持返回一个Ref类型
  • key: 唯一key,确保在跨请求获取该数据时,保证数据的唯一性。为空时会根据文件和行号自动生成唯一key

返回值:Ref 响应式对象

useState 具体使用步骤如下:

  • 1.在 composables 目录下创建一个模块,如: composables/states.ts
  • 2.在states.ts中使用 useState 定义需全局共享状态,并导出
  • 3.在组件中导入 states.ts 导出的全局状态

useState 注意事项:

  • useState 只能用在 setup 函数 和 Lifecycle Hooks 中
  • useState 不支持classes, functions or symbols类型,因为这些类型不支持序列化

在composables/useCount.ts

 export default function () {
  return useState("counter", () => 100);
}

使用

<script lang="ts" setup>
const counter = useCounter()

function addCounter() {
  counter.value++
}
</script>

<template>
  <div>
    Page: foo
    <div>{{ counter }}</div>
    <button @click="addCounter">+1</button>
  </div>
</template>

Nuxt3 集成 Pinia

安装依赖

npm install @pinia/nuxt –-save

@pinia/nuxt 会处理state同步问题,比如不需要关心序列化或XSS 攻击等问题

npm install pinia –-save

如有遇到pinia安装失败,可以添加 --legacy-peer-deps 告诉 NPM 忽略对等依赖并继续安装。或使用yarn

Nuxt 应用接入 Pinia

在nuxt.config文件中添加: modules: [‘@pinia/nuxt‘]

image.png

Pinia 使用步骤:

  • 1.在store文件夹中定义一个模块,比如:store/counter.ts
  • 2.在 store/counter.ts 中使用defineStore函数来定义 store 对象
  • 3.在组件中使用定义好的 store对象
import { defineStore } from "pinia";
 export interface IState {
  counter: number;
}
export const useHomeStore = defineStore("home", {
  state: ():IState => {
    return {
      counter: 0,
    };
  },
  actions: {
    increment() {
      this.counter++;
    },
  },
});
<template>
  <div>
    <h2>Category</h2>
    <h3>{{ counter }}</h3>
    <button @click="addCounter">+1</button>
  </div>
</template>

<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useHomeStore } from '~/store/home';
const homeStore = useHomeStore()
const { counter } = storeToRefs(homeStore)

function addCounter() {
  homeStore.increment()
}

</script>

actions 中发起请求

  async fetchHome() {
      const { data } = await $fetch("/api/homeInfo");
      console.log(data);
      this.homeInfo = data;
    },

useState vs Pinia

Nuxt跨页面、跨组件全局状态共享,既可以使用 useState,也可以使用Pinia,那么他们有什么异同呢?

它们的共同点:

  • 都支持全局状态共享,共享的数据都是响应式数据
  • 都支持服务器端和客户端共享

但是 Pinia 比 useState 有更多的优势,比如:

  • 开发工具支持(Devtools)
  • 跟踪动作,更容易调试
  • store可以出现在使用它的组件中
  • 模块热更换
  • 无需重新加载页面即可修改store数据
  • 在开发时保持任何现有状态
  • 插件:可以使用插件扩展 Pinia 功能
  • 提供适当的 TypeScript 支持或自动完成

安装 Element Plus 2

Element Plus 官网:element-plus.org/zh-CN/guide…

安装 Element Plus 的具体步骤:

  • 第一步:
    • npm install element-plus --save
    • npm install unplugin-element-plus --save-dev
  • 第二步:
    • 配置Babel对EP的转译
    • 配置自动导入样式
  • 第三步:
    • 在组件中导入组件,并使用

注意事项:目前Nuxt3暂时还不支持EP组件自动导包,等后续EP的module

image.png