Nuxt3 如何进行状态管理和异常捕获

607 阅读9分钟

内部状态管理

  • Nuxt 内置的状态管理模块 useState()
  • 整合全局状态管理库:Pinia。

useState

看见useState很容易想到react 设置状态变量的 那个 useState hook ,但是在Nuxt中除了设置数据状态以外,还可以设置全局变量。

useState 用法

下面是 一个使用useState定义一个状态的案例。

<template>
  <div>
    <h2>用户信息</h2>
    <p>用户名:{{ username }}</p>
    <p>邮箱:{{ email }}</p>
    <Button @click="editUserInfo">编辑</Button>
    <div v-if="isEditing">
      <input v-model="newUsername" placeholder="新用户名" />
      <input v-model="newEmail" placeholder="新邮箱" />
      <Button @click="saveUserInfo">保存</Button>
    </div>
  </div>
</template>

<script setup lang="ts">
// 初始状态
const username = useState('username', () => '初始用户名');
const email = useState('email', () => '初始邮箱');
const isEditing = useState('isEditing', () => false);
const newUsername = ref('');
const newEmail = ref('');
const editUserInfo = () => {
  isEditing.value = true;
};

const saveUserInfo = () => {
  username.value = newUsername.value;
  email.value = newEmail.value;
  isEditing.value = false;
};
</script>

从这个案例看useState和ref好像没什么区别,定义一个响应式数据,ref也可以做到和useState一样的事情。但是实际上它们是有一定差异的。

首先useState 如果传入第一个参数'key' 那么就具有缓存性,在key不发生改变的情况下,引用的是同一个useState

<template>
  <div>
    <p>Value: {{ state }}</p>
    <button @click="updateValue">Update Value</button>
    <button @click="reloadValue">Reload Value</button>
  </div>
</template>

<script setup lang="ts">
import { useState } from '#app';

const key = 'myValueKey';
const initialValue = Math.random();
const state = useState(key, () => initialValue);

const updateValue = () => {
  state.value++;
};

const reloadValue = () => {
  const newState = useState(key, () => initialValue);
  console.log('Reloaded state value:', newState.value);
};
</script>

点击 “Reload Value” 按钮时,再次调用 useState(key, init)。由于 key 不变,且初始化函数仅在首次调用时执行,后续调用会返回之前已经创建的状态,而不会重新执行初始化函数生成新的随机数作为初始值。控制台会输出重新加载后的状态值,可以看到它与之前的状态值相同,而不是一个新的随机数。

由于 useState 的缓存性,即使初始值是随机的不稳定值,在前端注水时,只要 key 不变,就会返回之前在服务端创建的状态,保证了前后端状态的一致性。

<template>
  <div>
    <p>随机数: {{ state }}</p>
    <p>随机数Ref:{{ stateRef }}</p>
  </div>
</template>

<script setup lang="ts">
const key = 'randomValueKey';
const getRandomValue = () => Math.random();
const state = useState(key, getRandomValue);
const stateRef = ref(getRandomValue())
</script>

定义一个用ref生成的变量,我们观察控制台发现出现了注水时不一致的警告。同样打印出现可以看见useState 定义的服务端渲染和客户端渲染保持一致,但是ref的变量出来的就会不一致。

image.png

useState 定义全局状态

除了上面在组件或者页面中使用useState以外,还可以使用useState 定义全局状态。 刚才我们在第一个案例使用的例子里面定义的username

const username = useState('username', () => '初始用户名');

我们换一个页面打印

//index.vue
console.log(useState('username').value);

arv.gif

就算跨页面也可以获得useState定义的状态值,当然大家也可以将内容定义在composables 中使用。但是和我们使用vuex 以及pinia一样,useState 保存的全局变量也是刷新就会丢失的,

pinia

和我们之前教给大家下载模块的方法一致,大家可以直接在devtool里面寻找需要的模块下载。 我们在composables 目录下定义一个store.ts

//store.ts
export const useStore = defineStore("userStore", () => {
  const state = ref<{ [key: string]: any }>({ token: '123' })
  const setState = (newState: any) => {
    state.value = { ...state.value, ...newState };
  };
  const removeState = (key: string) => {
    delete state.value[key];
  }
  return { state, setState, removeState }
});

//state.vue
console.log(store.state);
<Button @click="setStoreValue">setStore</Button>
<Button @click="delStoreValue">delStore</Button>

const store = useStore()
const setStoreValue = () => {
  store.setState({ [username.value]: email.value })
  console.log(store.state);
}
const delStoreValue = () => {
  store.removeState(username.value)
  console.log(store.state);
}

当然大家也可以自定义目录去使用store 不过这样定义出来的内容需要先导入才可以在页面使用。

pinia 持久化

和vue内一样使用pinia实现持久化需要加入持久化插件,在nuxt里面里面我们同样需要下载插件

image.png

Nuxt中使用该插件需要多配置一项build。

//nuxt.config.ts
  build: {
    transpile: ['pinia-plugin-persistedstate']
  },

我们去配置持久化保存方式为localStorage

export const useStore = defineStore("userStore", () => {
//store 内容
}, {
  persist: {
    storage: piniaPluginPersistedstate.localStorage(),
  }
});

这样就会自动把我们useStore放到localStorage 里面去了

补充 pinia 持久化 默认cookie模式

上面我们使用localStorage 实现持久化,在我们使用jwt的项目这种普通的客户端渲染项目里面是没问题的。 但是在ssr的情况下,如果我们在首屏渲染的时候去拿这种数据,比如我们保存用户token在localStorage。 前文我们写的请求拦截器

//request.ts
    onRequest({ options }) {
      // 从本地存储中获取 token
      const token = localStorage.getItem('token');
      }

或者我们之前写的路由守卫一旦有这种浏览器api的调用,那么在我们服务端渲染的时候就一定会发生渲染错误。

所以为了解决这种问题我们在做ssr项目的时候一般使用cookie 来保存我们的登录信息。

  persist: {
    storage: piniaPluginPersistedstate.cookies(),
  }
  //或者直接设置为true 默认是cookie保存
    persist: {
    storage: true,
  }

那我们同样要对之前的请求封装做调整

//request.ts
    onRequest({ options }) {
      // 从本地存储中获取 token
      const token = useStore().state.value.token;
      }

这样就是在我们使用已有的后端,不改变后端鉴权的最小成本改动,如果后端也换位cookie的话那成功会过大。

当然也有的同学会选择将操作浏览器api的行为全部

if(import.meta.client){
//操作浏览器api/dom 等
}

这样也是一种解决思路,只是要特别注意渲染时机的问题

Nuxt错误处理

在项目中我们需要为可能出现的错误做统一的捕获处理,可以是导向一个错误展示页,或者是弹出一些错误提示信息,在前文中,我们在路由中间件或者是请求拦截器都做了这样的事情,比如在接口返回不为200 的时候跳转到error页面 实际上Nuxt本身就给我们提供了一个公共的error页面。

error.vue页面

如果大家对第一篇文章的目录结构有印象就知道 和我们自己新建的error页面放在pages不同,nuxt提供的error页面必须设置在项目根目录下,且名字不能修改。

而error.vue的应用场景又是什么。

我们在页面的template 上定义一个不存在的变量

  <div>{{ userInfo.name }}</div>

那么在刚进入页面的服务端渲染阶段就会发生报错,

image.png

我们的error页面就是为我们自定义错误页面,当前页面显示500说明是一个服务端错误。

//error.vue
<script setup lang="ts">
import type { NuxtError } from '#app';

const props = defineProps({
  error: Object as () => NuxtError
})
</script>

<template>
  <div>
    <h1>{{ error?.statusCode }}</h1>
    <NuxtLink to="/">Go back home</NuxtLink>
  </div>
</template>
<style></style>

报错页面就会换成我们

image.png

换成我们自定义的内容。

那么我们当我们在逻辑代码中发生错误并希望手动抛出错误

//服务端渲染
throw createError({status: 404, message: 'Page not found'})
//客户端渲染
showError({status: 404, message: 'Page not found'})

关于error.vue的使用就是这样了,这部分内容比较少,最多可以在NuxtError这里还有一些其它属性的应用大家可以查阅文档详情。

Nuxt plugins

在说到其它捕获错误之前需要先对我们Nuxt的plugin 做一个简单 的介绍。

和nuxt 中的其它功能一样,只需要在plugins 定义内容就可以自动注册为插件。

这里简单的对插件注册做一点补充。

注册插件可以通过设置序号来设置插件的执行顺序,不过这个序号是按照字符串来的,不是按照数字大小。 之所以设置插件的执行顺序是因为有些插件依赖于其它插件这种情况下就显得很有必要了。

plugins/
| - 01.hello.ts
| - 02.hello.ts

和中间件一样插件也是区分客户端插件和服务端插件的

plugins/
| - 01.hello.server.ts
| - 02.hello.client.ts

定义插件

插件支持函数式和对象式,一般我们推荐使用对象式定义插件

export default defineNuxtPlugin(nuxtApp => {
  // Doing something with nuxtApp
})

export default defineNuxtPlugin({
  name: 'my-plugin-one',
  // enforce 可以是 'pre'(在其他插件之前执行)或者 'post'(在其他插件之后执行)
  enforce: 'pre', // 或者 'post'
  async setup(nuxtApp) {
    // 这里相当于一个普通的函数式插件
  },
  hooks: {
    // 你可以在这里直接注册 Nuxt 应用的运行时钩子
    'app:created'() {
      const nuxtApp = useNuxtApp()
      // 在这个钩子中执行一些操作
    }
  },
})

plugins 捕获vue 渲染问题

export default defineNuxtPlugin({
  name: 'handle-vue-error-plugin',
  enforce: 'pre',
  async setup(nuxtApp) {
   nuxtApp.vueApp.config.errorHandler = (..._args) => {
    console.log('vue error handler')
  }
  },
})

plugins 使用 Nuxt hook 捕获vue问题

我们实现一个插件,该插件的功能是在vue渲染发生错误的时候捕获到错误信息,并提交到错误日志后台,

export default defineNuxtPlugin({
  name: 'enhanced-error-handling-plugin',
  enforce: 'pre',
  hooks: {
    'vue:error'(error, instance, info) {
      // 打印 Vue 错误信息
      console.error(`Vue Error: ${error.message}`);
      // 打印发生错误的组件实例信息
      console.error(`Component Instance:`, instance);
      // 打印错误详细信息
      console.error(`Error Info:`, info);

      async function sendErrorToServer({ error, instance, info }) {
        try {
          // 使用 $fetch 向服务器的 /error-logging-endpoint 发送 POST 请求,携带错误信息、组件名称和详细信息
          await $fetch('/error-logging-endpoint', {
            method: 'POST',
            body: { error: error.message, component: instance.$options.name || 'Unknown Component', info }
          });
          // 打印成功发送错误信息到服务器的日志
          console.log('Error sent to server successfully.');
        } catch (fetchError) {
          // 打印发送错误信息到服务器时出现的错误
          console.error('Error sending error to server:', fetchError);
        }
      }

      sendErrorToServer({ error, instance, info });

      function showNetworkErrorNotification() {
        // 显示网络错误通知,这里使用了 alert,实际应用中可使用更友好的通知方式
        alert('网络出现问题,请检查你的网络连接。');
      }

      console.log(error.message, 'message');

      if (error.message.includes('Network Error')) {
        // 如果错误信息包含“Network Error”,则显示网络错误通知
        showNetworkErrorNotification();
      }
    },
  },
});

NuxtErrorBoundary

<NuxtErrorBoundary> 组件处理在其默认插槽中发生的客户端错误 。

我们可以把 <NuxtErrorBoundary> 作为容器组件将内容包起来,其默认插槽中发生的错误会被捕获,避免向上冒泡,并且渲染 error 插槽。


<template>
  <NuxtErrorBoundary @error="logSomeError">
    <!-- 默认插槽放置要渲染的内容 -->
    <!-- ... -->
    <!-- error 插槽处理错误,接收 error 为错误信息 -->
    <template #error="{ error }">
      这里显示错误信息
      <button @click="error = null">
        设置 error = null 清除错误,显示内容
      </button>
    </template>
  </NuxtErrorBoundary>
</template>

在 Nuxt 中,我们可以通过多种方式来进行状态管理和错误处理。useState()提供了一种简单的内置状态管理方式,同时 Pinia 为我们带来了更强大的全局状态管理能力。

在错误处理方面,Nuxt 提供了error.vue页面用于自定义错误展示,以及通过插件和<NuxtErrorBoundary>组件来捕获和处理各种错误。

这些功能使得我们在开发 Nuxt 应用时能够更好地管理状态和处理错误,提高应用的稳定性和用户体验。希望你在使用 Nuxt 进行开发时,能够充分利用这些特性,打造出高质量的应用程序。

本篇是nuxt入门的第四篇,大家可以前往专栏查看更多关于Nuxt入门的相关知识。

Nuxt 入门日记(一) juejin.cn/post/740737…

Nuxt 入门日记(二)juejin.cn/post/741142…

Nuxt 入门日记(三)juejin.cn/post/741322…