Nuxt3基本尝试 | 青训营笔记

937 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 13 天

本文章只介绍基本的使用,一些 API 的复杂使用不在此笔记中

1. 两种渲染模式

1.1 客户端渲染

优点:速度快、用户体验好

缺点:

  1. 最开始拿到的一个空的 HTML 文件和 JS 文件,首屏渲染慢。
  2. 最开始拿到的文件没有实质内容,对于 SEO (搜索引擎)不友好

1.2 同构渲染(服务端渲染)

SSR 会消耗服务器性能,所以一些后台管理系统不需要使用 SSR 构建

2. Nuxt Configuration

2.1 nuxt.config.ts

nuxt.config.ts 文件是 Nuxt 项目的配置入口文件,可以

控制或扩展应用程序。

最开始的代码只是包含导出 defineNuxtConfig 函数,在函数内可以自定义自己的配置,defineNuxtConfig 函数是在全局生效的,无需 import

// nuxt.config.ts
export default defineNuxtConfig({
    // My Nuxt Config
})

这个文件经常会在文档中被提及,比如添加一些脚本、注册模块或者改变渲染模式。

2.2 环境变量和 token

runtimeConfig API 可以向外暴露一些数据,像是环境变量在你的其他应用中。默认值只能在服务端访问,如果需要客户端也能访问,则需要 runtimeConfig.public API

// nuxt.config.ts
export default defineNuxtConfig({
    runtimeConfig: {
        // only server-side
        apiSecret: '123',
        // client-side and server-side
        public: {
            apiBae: '/api'
        }
    }
})

当然,也可以在跟 nuxt.config.ts 同目录下新建 .env 文件

# 这个会覆盖 nuxt.config.ts 配置的 apiSecret
NUXT_API_SECRET=555

当这些变量配置好后,就可以在其他组件中使用了,使用方式:useRuntimeConfig

客户端只能访问 apiBaseuseFetch 只是测试客户端是否能同时访问 apiSecretapiBase

// pages/index.vue
<script setup lang="ts">
const runtime = useRuntimeConfig()
// 客户端只能访问 public 中的属性
const apiBase = runtimeConfig.public.apiBase
// useFetch('/api/count') // 只是为了访问一下
</script>
<template>
    <div>apiBase:{{apiBase}} </div>
</template>

服务端可以同时访问 apiSecretapiBase

// server/api/count.ts
let count = 0
export default () => {
    count++
    const runtimeConfig = useRuntimeConfig()
    console.log('apiSecret', runtimeConfig.apiSecret)
  console.log('apiBase', runtimeConfig.public.apiBase)
  return JSON.stringify(count)
}

2.3 App Configuration

app.config.ts 文件也是在项目根目录,不过它暴露出的都是公共变量并且在构建的时候就已经确立。对比 runtimeConfig,这些变量不能被环境变量覆盖。

defineAppConfig 包含一个对象,且是全局注册,无需import,示例:

// app.config.ts
export default defineAppConfig({
    title: 'Hello Nuxt',
    theme: {
        dark: true,
        colors: {
            highlight: '#ff0000'
        }
    }
})

使用方法:组件中使用 useAppConfig API

// pages/appconfig.vue
<script setup lang="ts">
const appConfig = useAppConfig()
const { title, theme } = appConfig
</script><template>
  <div>
    <div>title: {{ title }}</div>
    <div>theme: {{ theme.dark ? 'dark' : 'light' }}</div>
    <div :style="`color: ${theme.colors.highlight}`">Test Text</div>
  </div>
</template>

2.4 runtimeConfig 和 app.config

runtimeConfigapp.config 都可以用来暴露变量给我们的应用程序,但是它们的应用场所不同

  • runtimeConfig:在构建之后需要被使用的私密的或者公开的 token
  • app.config :在构建时确定的公共 token、网站配置(如主题变量、标题和任何不敏感的项目配置)。

以下是官网的一些解释,我翻译不出来()

FeatureruntimeConfigapp.config
Client SideHydratedBundled
Environment Variables✅ Yes❌ No
Reactive✅ Yes✅ Yes
Types support✅ Partial✅ Yes
Configuration per Request❌ No✅ Yes
Hot Module Replacement❌ No✅ Yes
Non primitive JS types❌ No✅ Yes

同时还有其他构建工具的配置信息,参考:Configuration · Get Started with Nuxt

3. Views

Nuxt 提供了几个组件层来实现应用程序的用户界面

3.1 app.vue

Nuxt 默认会把 app.vue 当做是入口文件,并且渲染每个路由的内容

<template>
  <div>
   <h1>Welcome to the homepage</h1>
  </div>
</template>

3.2 Components

大多数的组件都是可循环利用的,比如按钮和菜单组件,在 Nuxt 中,在根目录的 components 文件夹创建组件,这样的组件是全局的,意味着在其他组件使用无需导入

// components/AppAlert.vue
<template>
    <div>
        <div>这是 AppAlert 组件 </div>
        <slot />
    </div>
</template>
// app.vue
<template>
  <div>
    <h1>Welcome to the homepage</h1>
    <AppAlert>
      这个组件是自动导入的,无需 import
    </AppAlert>
  </div>
</template>

3.3 pages

pages 下面的文件采用了一种特殊的路由模式,每一个文件都代表着不同的路由信息。

为了使用 pages,创建 pages/index.vue 文件并且在 app.vue 添加 <NuxtPage /> 标签(和 view-router 类似,不必一定只在 app.vue 中添加)

// app.vue
<template>
  <div>
    <h1>Welcome to the homepage</h1>
    <AppAlert>
      这个组件是自动导入的,无需 import
    </AppAlert>
    <NuxtPage />
  </div>
</template>

pages/index.vue 表示的路由地址为 ‘/’,以 index 文件名前缀开头的,默认为跟路由,例如 pages/users/index.vue 代表的路由地址为 /user/

<template>
  <div>
    pages 子路由
  </div>
</template>

3.4 Layouts

Layouts 是包含多个页面的通用用户界面(如页眉和页脚显示)的页面的包装器。布局是Vue文件,使用

<slot /> 组件去展示 page 页的内容。layouts/default.vue 文件会被认为是默认入口。

// layouts/default.vue
<template>
  <div>
    <AppHeader />
    <slot />
    <AppFooter />
  </div>
</template>

这时去掉 app.vue 便会展示以上内容

4. Assets

Nuxt 使用两个文件夹去处理 stylesheetsfonts 或者 images

  • public 内的文件会原封不动保留在根目录
  • assets 内的文件会被编译工具进一步处理

4.1 public

public/ 目录提供静态资源并且这些资源可以通过浏览器地址访问到。

访问 public/ 目录下的内容只需要使用 ‘/’ 即可

示例:

// pages/public.vue
<template>
  <img src="/4.png" alt="">
</template>

同时访问 http://localhost:3000/4.png 地址,也可以访问到图片

4.2 assets

Nuxt 使用 Vite 或者 webpack 为打包工具,它们主要的功能就是生产优化 JS 文件,但是它们可以通过创建扩展(Vite)或者装在(webpack)去生产另一些静态资源,像是 stylesheets、fonts 或者 SVG。此步骤转换原始文件主要用于性能或缓存目的(例如样式表缩小或浏览器缓存无效)。

可以使用 ~/assets/ 来访问 assets/ 目录下的文件。

// pages/assets.vue
<template>
  <img src="~/assets/2.png" alt="">  
</template>

4.3 Global Styles Imports

为了全局引用 Nuxt 组件样式,可以在 nuxt.config.ts 中使用 Vite 的选项

示例:

// assets/_colors.sass
$primary: #49240F
$secondary: #E4A79D
// nuxt.config.ts
export default defineNuxtConfig({
    vite: {
        css: {
            preprocessorOptions: {
                sass: {
                    additionalata: `@use "@/assets/_colors.sass" as *\n`
                }
            }
        }
    }
})

5. Routing

Nuxt 采用文件系统路由,在这里不再细讲

5.1 Navigation

<NuxtLink> 组件(双标签),用来指定 Nuxt 中路由的跳转,它最终会被渲染为 a 标签,但是它的跳转不会经由服务器。

// components/Navigation.vue
<template>
    <ul>
      <li><NuxtLink href="/">Home</NuxtLink></li>
      <li><NuxtLink href="/appconfig">appconfig</NuxtLink></li>
      <li><NuxtLink href="/assets">assets</NuxtLink></li>
      <li><NuxtLink href="/pages">pages</NuxtLink></li>
      <li><NuxtLink href="/public">public</NuxtLink></li>
    </ul>
</template>

5.2 Route Parameters

Nuxt[] 这种形式创建动态路由,同时使用 useRoute 方法获取动态路由的参数

// /pages/[id].vue
<script setup lang="ts">
const route = useRoute()
const id = route.params.id
</script>
​
<template>
  id: {{ id }}
</template>

路由跳转

<template>
    <ul>
      <li><input type="text" v-model="id" /></li>
      <li><NuxtLink :href="`/${route}`">id</NuxtLink></li>
    </ul>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { RouteLocationRaw } from 'vue-router';
const id = ref(0)
const route = <RouteLocationRaw>(id)
</script>

5.3 Route Middleware

Nuxt 提供了可定制的路由中间件,你可以编写合适的代码,在路由跳转之前做一些工作。

注意: 这里说的路由中间件跟后端的中间件完全不同

三种不同的路由中间件:

  1. 匿名路由中间件:直接在 pages 文件夹对应的路由使用
  2. 命名路由中间件:放置在 middleware/ 文件夹中,会同过一部导入自动加载到 page 中(注意: 这种路由中间件命名方式一般为 kebab-case,例如 someMiddleware 应当命名为 some-middleware)
  3. 全局路由中间件:也是放置在 middleware/ 文件夹(带有 .global 后缀),并且在路由跳转时会自动执行
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
    // to 代表去了哪里,from 代表去哪里来
  console.log('to: ', to)
  console.log('from: ', from)
})
// pages/middleware.vue
<template>
  <button @click="router.back()">回退</button>
</template><script setup lang="ts">
const router = useRouter()
definePageMeta({
  middleware: 'auth' // 与文件名字相同
})
</script>

6. SEO and Meta

通过 head configcomposablescomponents 改善产品的 SEO

6.1 App Head

nuxt.config.ts 中提供了 app.head 属性,允许你定制化整个项目的 head 头部标签

这种方式不允许你使用响应式的数据,如果想用响应式数据全局设置 head 头部,可以在 app.vue 中使用 useHead

在盒子内容之外,Nuxt3 设置了合适的默认头部信息,这些可以自己手动设置覆盖

  • charset: utf-8
  • viewport: width=device-width, initial-scale=1
// nuxt.config.ts
export default defineNuxtConfig({
    app: {
        head: {
            charset: 'utf-16',
            viewport: 'width=500, initial-scale=1',
            title: 'My Learn',
            meta: [
                { name: 'description', content: 'My amazing site.' }
            ]
        }
    }
})

6.2 组合式API:useHead

useHead 组合式方法允许你使用更加具体和响应式的方法管理头部标签

// pages/head.vue
<script setup lang="ts">
import { ref } from 'vue';
const title = ref('My App')
useHead({
  title: title,
  meta: [
    { name: 'description', content: 'learn Nuxt' }
  ],
  bodyAttrs: {
    class: 'learn'
  },
  script: [{ children: 'console.log('Hello World')' }]
})
</script>
​
<template>
  <div>
    <input type="text" v-model="title">
  </div>
</template>

6.3 Components

Nuxt 还提供了 <Title>, <Base>, <NoScript>, <Style>, <Meta>, <Link>, <Body>, <Html><Head> 标签,可以直接书写在 template 标签内

// pages/componentHead
<template>
  <div>
    <Head>
      <title>{{ title }}</title>
      <Meta name="desciption" :content="title" />
      <Style type="text/css" children="body { background-color: green; }" />
    </Head>
    <h1>{{ title }}</h1>
  </div>
</template><script setup lang="ts">
import { ref } from 'vue';
const title = ref('hello')
</script>

6.4 Tpes

以下是对 useHead 以及 app.head 中各个属性类型的定义

interface MetaObject {
  title?: string
  titleTemplate?: string | ((title?: string) => string)
  base?: Base
  link?: Link[]
  meta?: Meta[]
  style?: Style[]
  script?: Script[]
  noscript?: Noscript[];
  htmlAttrs?: HtmlAttributes;
  bodyAttrs?: BodyAttributes;
}

6.5 titleTemplate

在上述例子中,我们全局定义了 title: My Learn,这个定义的全局 title,可以在 titleTemplate 中的参数访问到:

// pages/titleTemplate
<script setup lang="ts">
useHead({
    titleTemplate: (titleChunk) => {
        return titleChunk ? `${titleChunk} - Site Title` : 'Site Title'
    }
})
</script>

6.6 Body Tags

<script setup lang="ts">
useHead({
  script: [
    {
      src: 'https://third-party-script.com',
      body: true
    }
  ]
})
</script>

6.6 Add External CSS

此内容不在文件中展示

useHead

<script setup lang="ts">
useHead({
  link: [
    {
      rel: 'preconnect',
      href: 'https://fonts.googleapis.com'
    },
    {
      rel: 'stylesheet',
      href: 'https://fonts.googleapis.com/css2?family=Roboto&display=swap',
      crossorigin: ''
    }
  ]
})
</script>

components

<template>
<div>
  <Link rel="preconnect" href="https://fonts.googleapis.com" />
  <Link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" crossorigin="" />
</div>
</template>

7. Data Fetching

跳过了 Transitions 章节,想要了解可以自行观看:Transitions · Get Started with Nuxt

Nuxt 提供了 useFetch、useLazyFetch、useAsyncData 和 useLayAsyncData 去处理项目中数据请求

useFetch, useLazyFetch, useAsyncData 和 useLazyAsyncData 只能工作在 setup 或者生命周期狗子中

在介绍如何获取服务端数据前,我们需要知道,Nuxt 可以被称为全栈框架,因为在 Nuxt 中,我们可以编写服务端代码。

7.1 Server

在根目录中创建 server 文件,API 请求的地址也是采用文件系统。

API 接口示例:

// server/api/count.ts
let count = 0
export default () => {
    const runtimeConfig = useRuntimeConfig()
    count ++
    return JSON.stringify(count)
}

此文件在上文涉及过,不过是演示 runtimeConfig

7.2 useFetch

// pages/fetch.vue
<template>
  <div>
    count: {{ count }}
  </div>
</template><script setup lang="ts">
const { data: count } = await useFetch('/api/count')
</script>

7.3 useLazyFetch

这个方法也可以使用 useFetch 实现,只需要在 useFetch 中的配置项添加 lazy: true 即可。

顾名思义,就是懒加载数据的意思,有时候请求数据的速度并不如我们想象的快,如果这个请求数据的方法堵塞了接下来代码的运行,那么用户的体验会很差。

不好演示,就直接写官方给的例子了

<template>
  <!-- you will need to handle a loading state -->
  <div v-if="pending">
    Loading ...
  </div>
  <div v-else>
    <div v-for="post in posts">
      <!-- do something -->
    </div>
  </div>
</template>
<script setup>
const { pending, data: posts } = useLazyFetch('/api/posts')
watch(posts, (newPosts) => {
  // Because posts starts out null, you will not have access
  // to its contents immediately, but you can watch it.
})
</script>

7.4 useAsyncData

useAsyncDatauseFetch 实现的功能基本一致,useFetch(url) 与 useAsyncData(url, () => $fetch(url)) 非常相近,不一样的地方在于 useAsyncData 可以拥有更复杂的逻辑

<script setup>
const { data: count } = await useAsyncData('count', () => $fetch('/api/count'))
</script>
<template>
  count: {{ count }}
</template>

7.5 useLazyAsyncData

useLazyFetchuseFetch 之间的关系一样,由于演示麻烦,这里直接用官方代码

<template>
  <div>
    {{ pending ? 'Loading' : count }}
  </div>
</template>
<script setup>
const { pending, data: count } = useLazyAsyncData('count', () => $fetch('/api/count'))
watch(count, (newCount) => {
  // Because count starts out null, you won't have access
  // to its contents immediately, but you can watch it.
})
</script>

7.6 Refreshing Data

有时候用户需要在页面中重新请求数据,这时候就需要重新获取数据的方法,useFetch 就提供了这样的方法,帮助我们重新获取数据

// pages/refresh.vue
<template>
  <div>
    <div>count: {{ count }}</div>
    <button @click="refresh()">重新获取数据</button>
  </div>
</template><script setup lang="ts">
const { data: count, refresh } = useFetch('/api/count')
</script>

默认情况下,refresh() 会取消所有正在 pending 的请求,他们的结果不会更新,在这个 refresh() 新请求解决之前,任何先前等待的 Promise 都不会解决。不过可以设置 dedupe 选项来取消这一种行为

refresh({ dedupe: true })

7.7 refreshNuxtData

使 Nuxt3 获取数据的方法(useFetchuseLazyAsyncDatauseFetchuseLazyFetch)缓存无效并且重新触发 refetch

这个方法适用于你想要重新获取当前页面的所有数据

// pages/lazyAsyncData.vue
<template>
  <div>
    <div>count: {{ count }}</div>
    <div>
      <button @click="refresh()">refresh(cache)</button>
      <button @click="newRefresh">refresh(invalidateCache)</button>
    </div>
  </div>
</template><script setup>
const { data: count, refresh } = await useAsyncData('count', () => $fetch('/api/count'))
const newRefresh = () => refreshNuxtData('count')
</script>

7.8 clearNuxtData

删除已经缓存的数据、错误信息还有 useAsyncDatauseFetch 未返回的 promises

如果前往另一个页面并且想要使本页面的数据无效,这个方法很有效

<template>
  <div>
    <div>count: {{ count }}</div>
    <button @click="clearNuxtData('count')">清空数据</button>
  </div>
</template><script setup lang="ts">
const { data: count } = await useAsyncData('count', () => $fetch('/api/count'))
</script>

7.9 最佳实践

// pages/dataBestPractices
<template>
  <h1>{{ mountain?.title }}</h1>
  <p>{{ mountain?.description }}</p>
</template><script setup lang="ts">
const { data: mountain } = await useFetch(
  '/api/mountains/everest',
  { pick: ['title', 'description'] }
)
</script>
// server/api/mountains/everest.ts
const mountainData = {
  title: "Mount Everest",
  description: "Mount Everest is Earth's highest mountain above sea level, located in the Mahalangur Himal sub-range of the Himalayas. The China–Nepal border runs across its summit point",
  height: "8,848 m",
  countries: [
    "China",
    "Nepal"
  ],
  continent: "Asia",
  image: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f6/Everest_kalapatthar.jpg/600px-Everest_kalapatthar.jpg"
}
export default () => {
  return mountainData
}

8. State Management

Nuxt 提供了 useState 组合方式去创建响应式、对 SSR 友好的共享数据。

useState 对 SSR 是非常用好的,是 ref 的替代者。它的值将在服务器端渲染后(在客户端水混合期间)保留,并使用唯一密钥在所有组件之间共享。

useState 只会在 setup 期间或者生命周期钩子中起作用

因为数据在 useState 的存储将会被转换为 JSON。所以请不要使用某些不能被转换的关键字,例如 classesfunctions 或者 symbols

8.1 最佳实践

不要讲 const state = ref() 定义在 <script setup> 或者 setup() 函数外

这样的做法会导致当用户访问你的网站时, state 一直存在,容易导致内存泄漏

作为替代,请使用 const useX = () => useState(‘x’)

基本示例

<template>
  <div>
    counter: {{ counter }}
    <button @click="counter++">+</button>
    <button @click="counter--">-</button>
  </div>
</template><script setup lang="ts">
const counter = useState('counter', () => Math.round(Math.random() * 1000))
</script>

8.2 数据共享

// composables/states.ts
export const useCounter = () => useState<number>('couter', () => 0)
export const useColor = () => useState<string>('color', () => 'pink')
// pages/sharedState.vue
<template>
  <div>
    <div>color: {{ color }}</div>
    <div>count: {{ count }}</div>
  </div>
</template><script setup lang="ts">
const color = useColor()
const count = useCounter()
</script>