基础篇九 Nuxt4 插件系统:扩展 Nuxt 能力

文章目录

Nuxt 的强大离不开插件系统。想用第三方库?需要全局注册组件?想监听应用生命周期?都需要插件。今天深入聊聊 Nuxt 插件系统的用法和最佳实践。

一、插件基础

插件放在 plugins 目录,自动注册:

ts 复制代码
// plugins/my-plugin.ts
export default defineNuxtPlugin((nuxtApp) => {
  console.log('插件执行了!')
  
  // 这里的代码在应用创建时执行
})

nuxtApp 对象包含:

  • vueApp - Vue 应用实例
  • provide - 提供全局方法
  • hook - 监听生命周期
  • runWithContext - 在应用上下文中执行

二、注入全局方法

ts 复制代码
// plugins/utils.ts
export default defineNuxtPlugin(() => {
  return {
    provide: {
      // 注入全局方法
      formatDate: (date: Date) => {
        return date.toLocaleDateString('zh-CN')
      },
      
      formatPrice: (price: number) => {
        return `¥${price.toFixed(2)}`
      }
    }
  }
})

使用:

vue 复制代码
<script setup lang="ts">
const { $formatDate, $formatPrice } = useNuxtApp()
</script>

<template>
  <p>{{ $formatDate(new Date()) }}</p>
  <p>{{ $formatPrice(99.9) }}</p>
</template>

⚠️ 全局方法以 $ 开头,这是 Nuxt 的约定。

三、引入第三方库

示例:引入 dayjs

bash 复制代码
pnpm add dayjs
ts 复制代码
// plugins/dayjs.ts
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
import relativeTime from 'dayjs/plugin/relativeTime'

dayjs.locale('zh-cn')
dayjs.extend(relativeTime)

export default defineNuxtPlugin(() => {
  return {
    provide: {
      dayjs
    }
  }
})

使用:

vue 复制代码
<script setup lang="ts">
const { $dayjs } = useNuxtApp()
</script>

<template>
  <p>{{ $dayjs().format('YYYY-MM-DD') }}</p>
  <p>{{ $dayjs('2024-01-01').fromNow() }}</p>
</template>

示例:引入 lodash

bash 复制代码
pnpm add lodash-es
pnpm add -D @types/lodash-es
ts 复制代码
// plugins/lodash.ts
import { debounce, throttle, cloneDeep } from 'lodash-es'

export default defineNuxtPlugin(() => {
  return {
    provide: {
      debounce,
      throttle,
      cloneDeep
    }
  }
})

使用:

vue 复制代码
<script setup lang="ts">
const { $debounce, $throttle } = useNuxtApp()

const handleSearch = $debounce((keyword: string) => {
  console.log('搜索:', keyword)
}, 300)
</script>

四、生命周期钩子

插件可以监听 Nuxt 的生命周期:

ts 复制代码
// plugins/lifecycle.ts
export default defineNuxtPlugin((nuxtApp) => {
  // 应用创建完成
  nuxtApp.hook('app:created', () => {
    console.log('App created')
  })
  
  // 挂载前
  nuxtApp.hook('app:beforeMount', () => {
    console.log('Before mount')
  })
  
  // 挂载完成
  nuxtApp.hook('app:mounted', () => {
    console.log('Mounted')
  })
  
  // 页面切换前
  nuxtApp.hook('page:start', () => {
    console.log('Page start')
  })
  
  // 页面切换完成
  nuxtApp.hook('page:finish', () => {
    console.log('Page finish')
  })
  
  // 发生错误
  nuxtApp.hook('app:error', (error) => {
    console.error('App error:', error)
  })
  
  // Vue 错误
  nuxtApp.hook('vue:error', (error, instance, info) => {
    console.error('Vue error:', error)
  })
})

五、全局组件注册

虽然 Nuxt 支持组件自动导入,但有些库需要手动注册:

ts 复制代码
// plugins/components.ts
import MyButton from '~/components/MyButton.vue'
import MyModal from '~/components/MyModal.vue'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.component('MyButton', MyButton)
  nuxtApp.vueApp.component('MyModal', MyModal)
})

六、全局指令

ts 复制代码
// plugins/directives.ts
export default defineNuxtPlugin((nuxtApp) => {
  // 点击外部指令
  nuxtApp.vueApp.directive('click-outside', {
    mounted(el, binding) {
      el._clickOutside = (event: MouseEvent) => {
        if (!(el === event.target || el.contains(event.target))) {
          binding.value(event)
        }
      }
      document.addEventListener('click', el._clickOutside)
    },
    unmounted(el) {
      document.removeEventListener('click', el._clickOutside)
    }
  })
  
  // 防抖指令
  nuxtApp.vueApp.directive('debounce', {
    mounted(el, binding) {
      const delay = binding.arg ? parseInt(binding.arg) : 300
      let timer: ReturnType<typeof setTimeout>
      
      el._debounceHandler = () => {
        clearTimeout(timer)
        timer = setTimeout(() => {
          binding.value()
        }, delay)
      }
      
      el.addEventListener('click', el._debounceHandler)
    },
    unmounted(el) {
      el.removeEventListener('click', el._debounceHandler)
    }
  })
})

使用:

vue 复制代码
<template>
  <div v-click-outside="handleClickOutside">
    点击外部关闭
  </div>
  
  <button v-debounce="handleSubmit">提交</button>
  <button v-debounce:500="handleSubmit">500ms 后执行</button>
</template>

七、插件执行顺序

插件按文件名字母顺序执行。需要控制顺序时:

ts 复制代码
// plugins/01-first.ts
export default defineNuxtPlugin(() => {
  console.log('第一个执行')
})

// plugins/02-second.ts
export default defineNuxtPlugin(() => {
  console.log('第二个执行')
})

或者指定依赖:

ts 复制代码
// plugins/second.ts
export default defineNuxtPlugin({
  name: 'second',
  dependsOn: ['first'],  // 依赖 first 插件
  setup() {
    console.log('second 在 first 之后执行')
  }
})

八、仅客户端/服务端执行

ts 复制代码
// 仅客户端执行
export default defineNuxtPlugin({
  name: 'client-only',
  mode: 'client',  // 或 environment: 'client'
  setup() {
    console.log('只在客户端执行')
  }
})

// 仅服务端执行
export default defineNuxtPlugin({
  name: 'server-only',
  mode: 'server',
  setup() {
    console.log('只在服务端执行')
  }
})

九、TypeScript 类型扩展

让 TypeScript 认识你注入的方法:

ts 复制代码
// plugins/utils.ts
export default defineNuxtPlugin(() => {
  return {
    provide: {
      formatDate: (date: Date) => date.toLocaleDateString('zh-CN')
    }
  }
})

// 声明类型
declare module '#app' {
  interface NuxtApp {
    $formatDate: (date: Date) => string
  }
}

或者单独创建类型文件:

ts 复制代码
// types/plugins.d.ts
declare module '#app' {
  interface NuxtApp {
    $dayjs: typeof import('dayjs')
    $formatDate: (date: Date) => string
    $formatPrice: (price: number) => string
  }
}

export {}

十、页面切换进度条

使用插件实现全局 loading:

ts 复制代码
// plugins/loading.ts
export default defineNuxtPlugin((nuxtApp) => {
  const loading = ref(false)
  
  nuxtApp.hook('page:start', () => {
    loading.value = true
  })
  
  nuxtApp.hook('page:finish', () => {
    setTimeout(() => {
      loading.value = false
    }, 200)
  })
  
  return {
    provide: {
      loading
    }
  }
})
vue 复制代码
<!-- app.vue -->
<script setup lang="ts">
const { $loading } = useNuxtApp()
</script>

<template>
  <div>
    <div v-if="$loading" class="loading-bar">
      <div class="progress"></div>
    </div>
    <NuxtPage />
  </div>
</template>

总结

插件系统核心用法:

功能 实现方式
注入全局方法 provide: { xxx }
引入第三方库 在插件中导入并 provide
生命周期监听 nuxtApp.hook('xxx', fn)
注册组件 nuxtApp.vueApp.component()
注册指令 nuxtApp.vueApp.directive()
控制执行顺序 文件名前缀或 dependsOn
仅客户端/服务端 mode: 'client' / 'server'

基础篇到此结束!下一篇开始进入进阶篇,聊聊 SSR 原理、服务端渲染、Nitro 引擎等高级话题。

相关文章

入门篇三:Nuxt4组件自动导入:写代码少敲一半字

入门篇二:Nuxt 4路由自动生成:告别手动配置路由的日子

延伸阅读

nuxt4完整系列,持续更新中。。,欢迎来逛逛


内容有帮助?点赞、收藏、关注三连!评论区等你 💪

相关推荐
brien921 分钟前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance36 分钟前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M42 分钟前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写44 分钟前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李1 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易1 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马1 小时前
前沿视界
前端
kyriewen1 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易2 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员
IT_陈寒2 小时前
Vue的这个响应式陷阱,差点让我加班到天亮
前端·人工智能·后端