【记一忘三二】nuxt3学习笔记

331 阅读4分钟

项目搭建

生成目录结构

pnpm dlx nuxi@latest init  <project-name>

添加Eslint配置

下载依赖

pnpm i @antfu/eslint-config -D

配置文件

  1. 创建配置文件eslint.config.js

    
    import antfu from '@antfu/eslint-config'
    
    
    export default antfu(
      /**
       * 第一层配置选项,用于启用 UnoCSS、Vue 和 TypeScript 的格式化处理。
       */
      {
        unocss: true, // 启用 UnoCSS 支持
        // 对 vue 文件进行格式化处理
        vue: true,
        // 对 ts 文件进行格式化处理
        typescript: true,
        formatters: {
          // 对 css 文件进行格式化处理
          css: true,
          // 对 html 文件进行格式化处理
          html: true,
        },
      },
      /**
       * 第二层配置选项,用于指定文件匹配模式和 Vue 特定的规则。
       */
      {
        // 指定匹配所有 .vue 文件
        files: ['**/*.vue'],
        rules: {
          'vue/html-self-closing': ['error', {
            html: {
              // 要求 HTML 空元素始终自闭合
              void: 'always',
            },
          }],
        },
      },
      /**
       * 第三层配置选项,用于定义 ESLint 忽略检查的文件和目录。
       */
      {
        ignores: [
          // 忽略 assets 目录下的 font 文件夹
          'assets/font',
          // 忽略 node_modules 目录
          'node_modules',
          // 忽略 .output 目录
          '.output',
          // 忽略 .nuxt 目录
          '.nuxt',
          // 忽略 dist 目录
          'dist',
        ],
      },
    )
    
  2. package.json添加启动脚本

{
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
}

添加.husky配置

生成项目.git

git init

自动配置husky

npx husky-init
  • 生成.husky目录

=

  • 创建scripts命令行

    {
        "prepare": "husky install",
    }
    

    这个脚本的作用是在 npm 安装过程结束时自动安装和配置 Husky,以便在 Git 事件发生时自动执行预设的脚本

    具体来说,"prepare": "husky install" 这个脚本的作用如下:

    1. 触发时机:当执行 npm installnpm ci 命令时,npm 会在安装完所有依赖后自动执行 prepare 脚本。
    2. 执行 Husky 安装husky install 命令会在项目中安装 Husky,并设置必要的 Git 钩子。这通常包括:
      • 在项目的 .git 目录下创建一个 husky 子目录。
      • husky 目录中创建钩子文件,如 pre-commitpre-push 等。
      • 将 Husky 的可执行文件路径添加到这些钩子文件中,以便在相应的 Git 事件发生时执行 Husky。

安装依赖

pnpm i

Lint-staged 增量检测提交代码

下载依赖

yarn add lint-staged -D

配置文件

  1. 创建lint-staged.config.js
/**
 * Lint-staged 配置文件,用于在 Git 提交前自动格式化和修复代码。
 * 这个配置文件通常命名为 .lintstagedrc 或在 package.json 中作为 lint-staged 属性。
 */

/**
 * 定义对不同文件类型执行的 linting 规则。
 * 每个规则都指定了一组 glob 模式,用于匹配文件,以及一个命令列表,
 * 这些命令将在匹配的文件被暂存(staged)时执行。
 */
export default {
  /**
   * 对 JavaScript、TypeScript、JSX 和 TSX 文件执行 ESLint 修复。
   * 这些文件在被暂存时会自动修复 ESLint 可自动修复的问题。
   */
  '*.{js,ts,jsx,tsx}': ['eslint --fix'],

  /**
   * 对 JSON 文件执行 ESLint 修复。
   * 这些文件在被暂存时会自动修复 ESLint 可自动修复的问题。
   */
  '*.json': ['eslint --fix'],

  /**
   * 对 Vue 文件执行 ESLint 修复。
   * 这些文件在被暂存时会自动修复 ESLint 可自动修复的问题。
   */
  '*.vue': ['eslint --fix']
}
  1. 创建scripts命令行
{
    "lint:lint-staged": "lint-staged"
}
  1. 修改.husky/pre-commit提交前钩子命令
pnpm run lint:lint-staged --allow-empty

Commitlint

下载依赖

pnpm i @commitlint/cli @commitlint/config-conventional -D

配置文件

  1. 添加commitlint.config.js文件

    module.exports = {
      // 继承的规则
      extends: ['@commitlint/config-conventional'],
      // @see: https://commitlint.js.org/#/reference-rules
      rules: {
        'subject-case': [0], // subject大小写不做校验
    
        // 类型枚举,git提交type必须是以下类型
        'type-enum': [
          2,
          'always',
          [
            'feat', // 新增功能
            'fix', // 修复缺陷
            'docs', // 文档变更
            'style', // 代码格式(不影响功能,例如空格、分号等格式修正)
            'refactor', // 代码重构(不包括 bug 修复、功能新增)
            'perf', // 性能优化
            'test', // 添加疏漏测试或已有测试改动
            'build', // 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)
            'ci', // 修改 CI 配置、脚本
            'revert', // 回滚 commit
            'chore' // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)
          ]
        ]
      }
    }
    
    1. 修改.husky/commit-msg 钩子用于 git 提交信息校验
    npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1"
    

    image-20241219104906118

SSR原理

c

注意:只有在首次地址请求的时候,会请求到SSR服务端,后续的路由跳转都是前端跳转。因为首次的请求不仅会返回HTML结构,还会返回Vue等数据

nuxt.config.ts配置

export default defineNuxtConfig({
  /**
   * compatibilityDate 是一个配置项,用于指定 Nuxt 应该兼容的日期。
   * 这个日期用于确定 Nuxt 应该使用哪些特性和行为。
   * 通常,这个日期越新,Nuxt 会使用更多的新特性和优化,但也可能意味着对旧版本浏览器的支持会减少。
   * 在这个例子中,'2024-11-01' 表示 Nuxt 将尽可能使用 2024 年 11 月 1 日之后引入的特性和优化。
   */
  compatibilityDate: '2024-11-01',
  /**
   * devtools 是一个配置对象,用于控制 Nuxt DevTools 的行为。
   * Nuxt DevTools 是一个浏览器扩展,提供了 Nuxt 应用的调试工具。
   */
  devtools: {
    /**
     * true 表示启用 Nuxt DevTools。
     * 当这个选项设置为 true 时,如果你已经安装了 Nuxt DevTools 浏览器扩展,它将自动连接到你的 Nuxt 应用,允许你在开发过程中进行调试。
     */
    enabled: true,
  },
	
  /**
   * 引入全局的css文件
   */
  css: [
    '@/styles/global.css',
  ]
})

添加unocss

下载依赖

pnpm add -D unocss @unocss/nuxt

配置文件

  1. 创建uno.config.js配置文件

    import {
      defineConfig,
      presetUno,
    } from 'unocss'
    
    export default defineConfig({
      presets: [
        presetUno(),
      ],
    })
    
    
  2. nuxt.config.js文件中引入模块

    export default defineNuxtConfig({
        modules: [
            '@unocss/nuxt',
        ],
    })
    

添加Element Plus

下载依赖

pnpm i element-plus @element-plus/nuxt -D

配置文件

  1. nuxt.config.js文件中引入模块
export default defineNuxtConfig({
  modules: [
    '@element-plus/nuxt'
  ],
  elementPlus: { /** Options */ }
})

scss全局导入

下载依赖

pnpm add -D sass-embedded

引入全局文件

vite: {
    css: {
        preprocessorOptions: {
            scss: {
                additionalData: `@use "@/styles/global.scss" as *;`,
            },
            },
        },
    },

工具方法

会自动导入composablesutils目录内的所有向外暴露的工具函数

export function showToast() {
  console.log('展示提示框')
}

export function hideToast() {
  console.log('隐藏提示框')
}

可以在任意位置使用

<template>
  <div @click="showToast">
    展示
  </div>
  <div @click="hideToast">
    隐藏
  </div>
</template>

插件

plugins目录内定义插件,会自动第一层目录下文件内容

  • 定义全局指令、组件

  • 设置全局生命周期钩子

  • 安装第三方插件、依赖包

import { defineNuxtPlugin } from '#app'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('page:loading:start', () => {
    console.log('新页面加载开始')
  })

  nuxtApp.hook('app:mounted', () => {
    console.log('app mounted')
  })

  nuxtApp.vueApp.directive('toast', {
    mounted() {
      console.log('渲染了toast指令')
    },
  })

  nuxtApp.vueApp.use({
    install() {
      console.log('安装插件')
    },
  })
})

注意:插件内容只会在首次加载服务端运行,后续的路由跳转都不会执行

项目路由

路由地址

  1. 创建pages目录,会根据目录中文件名称,生成路由跳转规则

  2. app.vue添加页面入口

    <template>
      <NuxtPage />
    </template>
    

基础路由

image-20241219135255982

生成/home/login/路由

注意:index.vue被转换的路由地址是/,而不是/index

嵌套路由

image-20241219135224132

任意路由

文件名称:[...任意名称].vue,比如[...404].vue

image-20241219140929181

在所有路由都不满足的情况,跳转到该路由

image-20241219140950146

多层路由

image-20241219141940495

app.vue

<template>
  <header>app页面头部</header>
  <NuxtPage />
  <footer>app页面底部</footer>
</template>

news.vue

<template>
  <header>新闻页面头部</header>
  <NuxtPage />
  <footer>新闻页面底部</footer>
</template>

add.vue

<template>
  <h1>新闻添加页面</h1>
</template>

image-20241219142001433

路由参数

动态路由

img

<template>
  <h1>新闻详情页面</h1>
  <h2>新闻ID: {{ useRoute().params.id }}</h2>
</template>           

image-20241219135158269

可选路由

image-20241219140837079

/user/settings/settings访问的页面是一样的

<template>
  <h1>用户设置页面</h1>
  <h2>{{ useRoute().params.user }}</h2>
</template>

image-20241219145806741

*注意:[]和[[]]*都代表了可选参数,不通的点是,参数是否能为空

路由配置

image-20241219150401446

<script setup lang='ts'>
definePageMeta({
  // 路由路径
  path: '/home.html',
  // 路由别名
  alias: ['/home'],
})
</script>

<template>
  <h1>home页面</h1>
</template>

路由导航

标签式

<template>
  <NuxtLink to="/home">
    home
  </NuxtLink>
</template>

编程式

useRouter
<script setup>
useRouter().push('/user')
</script>

<template>
  <h1>home页面</h1>
</template>
navigateTo
<script setup>
navigateTo('/user')
</script>

<template>
  <h1>home页面</h1>
</template> 

useRouternavigateTo都能实现页面跳转

useRouter在服务端不会被识别,直接会被略过执行,也就是useRouter跳转只会发生在客户端

navigateTo在服务端和客户端都会发生,在服务端就是重定向至跳转页面,客户端和useRouter功能相同

image-20241219174639847

路由守卫

所有的路由守卫都创建在middleware目录中,Nuxt会自动导入守卫

页面路由

// toast.js

import { defineNuxtRouteMiddleware } from '#app'

export default defineNuxtRouteMiddleware((to) => {
  console.log('路由中间件', to.fullPath)
})
<script setup>
definePageMeta({
  path: '/index',
	
  // 使用路由守卫
  middleware: [
    'toast',
  ],
})
</script>

<template>
  <h2>Index页面</h2>
</template>

全局路由

middleware目录下,创建包含.global的文件名称,那么就会创建全局路由守卫

// token.global.js

import { defineNuxtRouteMiddleware, navigateTo } from '#app'
import { tokenStore } from '@/composables/use-token'
import { ElMessage } from 'element-plus'

export default defineNuxtRouteMiddleware((to) => {
  const fullPath = to.fullPath
  const noLogin = ['/login']

  if (import.meta.client) {
    if (!noLogin.includes(fullPath)) {
      if (!tokenStore().isLogin) {
        ElMessage({
          message: '请先登录',
          type: 'warning',
        })
        return navigateTo('/login')
      }
    }
  }
})

全局数据

runtimeConfig

export default defineNuxtConfig({
  // 定义全局数据
  runtimeConfig: {
    // 只有服务端能访问
    isServer: true,
    // public内的数据,客户端和服务端都能访问
    public: {
      baseUrl: 'http://127.0.0.1:3000',
    },
  },
})
const config = useRuntimeConfig()

/**
 * 服务端:true
 * 客户端:undefined
 */
console.log(config.isServer)

/**
 * 服务端:http://127.0.0.1:3000
 * 客户端:http://127.0.0.1:3000
 */
console.log(config.public.baseUrl)

服务端和客户端判断

runtimeConfig

在客户端只能访问runtimeConfig.public内部的数据

<script setup>
const config = useRuntimeConfig()

if (config.isServer) {
  console.log('当前是服务端输出')
}
else {
  console.log('当前是客户端输出')
}
</script>

<template>
  <h1>home页面</h1>
</template>

import.meta.serverimport.meta.client

<script setup>
if (import.meta.server) {
  console.log('当前是服务端输出')
}
if (import.meta.client) {
  console.log('当前是客户端输出')
}
</script>

<template>
  <h1>home页面</h1>
</template>

SSR数据共享

<script setup>
const count = useState(() => 0)

count.value++

console.log(count.value)
</script>

<template>
  <h1>home页面</h1>
  <NuxtLink to="/user">
    home
  </NuxtLink>
</template>

如果使用const count = ref(0)定义数据,那么在服务端和客户端输出的都是0,这是因为两服务端和客户端count 变量都进行重新初始化

通过useState不仅可以多路由共享数据,还可以服务端和客户端共享数据

pinia

集成

pnpm i pinia @pinia/nuxt
export default defineNuxtConfig({
    modules: ['@pinia/nuxt'],
})

使用

composables目录定义user-count.js文件

import { defineStore } from 'pinia'

export const countStore = defineStore('count', {
  state: () => {
    return {
      count: 0,
    }
  },
  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
  },
})

任意页面使用

<template>
  <h2>
    count: {{ countStore().count }}
  </h2>
  <button @click="countStore().increment">
    增加
  </button>
  <button @click="countStore().decrement">
    减少
  </button>
</template>

数据持久化

pnpm add pinia-plugin-persistedstate
export default defineNuxtConfig({
  modules: [
    '@pinia/nuxt',
    'pinia-plugin-persistedstate/nuxt',
  ],
})

初始化数据

import { createPinia, defineStore } from 'pinia'

import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

export const countStore = defineStore('count', {
  state: () => {
    return {
      count: 0,
    }
  },
  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
  },
  persist: {
    storage: import.meta.client ? localStorage : null,
  },
})

当想把storage设置为localStoragesessionStorage配置时

  • 需要判断是在客户端环境,因为服务端环境没有这些配置
  • 只有把保存到cookie,客服端和服务端才能共享数据

多页面监听

watchStorageChange() {
    if (import.meta.client) {
        window.addEventListener('storage', (event) => {
            if (event.key === 'count') {
                const newValue = JSON.parse(event.newValue)
                if (newValue?.count) {
                    this.count = newValue.count
                    }
            }
        })
    }
},

actions中加入监听,在任意页面执行就可以了

网络请求

接口定义

server目录下,可以创建测试接口

image-20241222134600358

// 注意:这里来在 "h3",而不是 "#app"
import { defineEventHandler } from 'h3'

export default defineEventHandler(() => {
  return {
    code: 200,
    message: 'success',
    data: {
      name: 'John Doe',
      email: 'john@doe.com',
    },
  }
})

该文件的路由地址是get请求/api/user-info,文件名称指定请求方式

发起请求

$fetch

初始情况下:客户端和服务端都会发送请求

前端路由切换:客户端会发送请求

客户端函数执行:客户端会发送请求

<script setup>
const infoData = await $fetch('/api/info')
console.log(infoData)
</script>

<template>
  <h1>Index页面</h1>
</template>

useAsyncData

初始情况下:只有服务端会发送请求

前端路由切换:客户端会发送请求

客户端函数执行:客户端会发送请求

<script setup>
const infoData = await useAsyncData(() => $fetch('/api/info'))
console.log(infoData)

async function loadInfoData() {
  const infoData = await useAsyncData(() => $fetch('/api/info'))
  console.log(infoData)
}
</script>

<template>
  <el-button @click="loadInfoData">
    请求接口
  </el-button>
</template>

useFetch

功能和useAsyncData使用同样的

<script setup>
const infoData = await useFetch('/api/info')
console.log(infoData)

async function loadInfoData() {
  const infoData = await useFetch('/api/info')
  console.log(infoData)
}
</script>

<template>
  <el-button @click="loadInfoData">
    请求接口
  </el-button>
</template>