The Nuxt 3 Bootcamp - The Complete Developer Guide

1. Introduction

1. Introduction to Nuxt and it’s Benefits




状态管理的


2. Client-Side vs Server-Side vs Universal Rendering




UR(通用渲染) 服务器返回渲染好的 html,然后再发送 js,此时 js 和页面发生水合作用,使网页变成可交互的



3. Pros and Cons of Client-Side and Universal Rendering


csr
csr
ur
ur

4. Course Prerequisite

2. Diving into Nuxt

1. App Overview


2. Creating a Nuxt App

npx nuxi init 01-Baby-Name-Generator
cd 01-Baby-Name-Generator
npm i

3. Exploring the Boilerplate

npm run dev

4. Building the Boilerplate HTML

// app.vue
<template>
  <div>
    <h1>Baby Name Generator</h1>
    <p>Choose your options and click the "Find Names" button below</p>
    <div class='options-container'>
      <div class='option-container'>
        <h4>1) Choose a gender</h4>
        <div class='option-buttons'>
          <button>Boy</button>
          <button>Unisex</button>
          <button>Girl</button>
        </div>
      </div>
      <div class='option-container'>
        <h4>2) Choose the name's popularity</h4>
        <div class='option-buttons'>
          <button>Trendy</button>
          <button>Unique</button>
        </div>
      </div>
      <div class='option-container'>
        <h4>3) Choose the name's length</h4>
        <div class='option-buttons'>
          <button>Long</button>
          <button>All</button>
          <button>Short</button>
        </div>
      </div>
    </div>
  </div>
</template>

5. Styling Our App

<style scoped>
.container {
  font-family: Arial, Helvetica, sans-serif;
  color: rgb(27, 60, 138);
  max-width: 50rem;
  margin: 0 auto;
  text-align: center;
}
h1 {
  font-style: 3rem;
}
.options-container {
  background-color: rgb(255, 238, 236);
  border-radius: 2rem;
  padding: 1rem;
  width: 95%;
  margin: 0 auto;
  margin-top: 4rem;
  position: relative;
}
.option-container {
  margin-bottom: 2rem;
}
.option {
  background: white;
  outline: 0.15rem solid rgb(249, 87, 89);
  border: none;
  padding: 0.75rem;
  width: 12rem;
  font-size: 1rem;
  color: rgb(27, 60, 138);
  cursor: pointer;
  font-weight: 200;
}
.option-left {
  border-radius: 1rem 0 0 1rem;
}
.option-right {
  border-radius: 0 1rem 1rem 0;
}
</style>

6. Managing State

<script setup>
const options = reactive({
  gender: '',
  popularity: '',
  length: ''
})
</script>
<template>
  <div class="container">
    <h1>Baby Name Generator</h1>
    <p>Choose your options and click the "Find Names" button below</p>
    <div class="options-container">
      <div class="option-container">
        <h4>1) Choose a gender</h4>
        <div class="option-buttons">
          <button
            class="option option-left"
            :class="options.gender === 'Boy' && 'option-active'"
          >
            Boy
          </button>
          <button
            class="option"
            :class="options.gender === 'Unisex' && 'option-active'"
          >
            Unisex
          </button>
          <button
            class="option option-right"
            :class="options.gender === 'Gril' && 'option-active'"
          >
            Girl
          </button>
        </div>
      </div>
      <div class="option-container">
        <h4>2) Choose the name's popularity</h4>
        <div class="option-buttons">
          <button
            class="option option-left"
            :class="options.popularity === 'Trendy' && 'option-active'"
          >
            Trendy
          </button>
          <button
            class="option option-right"
            :class="options.popularity === 'Unique' && 'option-active'"
          >
            Unique
          </button>
        </div>
      </div>
      <div class="option-container">
        <h4>3) Choose the name's length</h4>
        <div class="option-buttons">
          <button
            class="option option-left"
            :class="options.length === 'Long' && 'option-active'"
          >
            Long
          </button>
          <button class="option">All</button>
          <button
            class="option option-right"
            :class="options.length === 'Short' && 'option-active'"
          >
            Short
          </button>
        </div>
      </div>
    </div>
  </div>
</template>
<style scoped>
/* ... */
</style>

7. A Little Bit of TypeScript

<script setup lang="ts">
enum Gender {
  GIRL = 'Girl',
  BOY = 'Boy',
  UNISEX = 'Unisex'
}
enum Popularity {
  TRENDY = 'Trendy',
  UNIQUE = 'Unique'
}
enum Length {
  SHORT = 'Short',
  LONG = 'Long',
  ALL = 'All'
}
interface OptionsState {
  gender: Gender
  popularity: Popularity
  length: Length
}

const options = reactive<OptionsState>({
  gender: Gender.UNISEX,
  popularity: Popularity.TRENDY,
  length: Length.ALL
})
</script>
<template>
  <div class="container">
    <h1>Baby Name Generator</h1>
    <p>Choose your options and click the "Find Names" button below</p>
    <div class="options-container">
      <div class="option-container">
        <h4>1) Choose a gender</h4>
        <div class="option-buttons">
          <button
            class="option option-left"
            :class="options.gender === Gender.BOY && 'option-active'"
          >
            Boy
          </button>
          <button
            class="option"
            :class="options.gender === Gender.UNISEX && 'option-active'"
          >
            Unisex
          </button>
          <button
            class="option option-right"
            :class="options.gender === Gender.GIRL && 'option-active'"
          >
            Girl
          </button>
        </div>
      </div>
      <div class="option-container">
        <h4>2) Choose the name's popularity</h4>
        <div class="option-buttons">
          <button
            class="option option-left"
            :class="options.popularity === Popularity.TRENDY && 'option-active'"
          >
            Trendy
          </button>
          <button
            class="option option-right"
            :class="options.popularity === Popularity.UNIQUE && 'option-active'"
          >
            Unique
          </button>
        </div>
      </div>
      <div class="option-container">
        <h4>3) Choose the name's length</h4>
        <div class="option-buttons">
          <button
            class="option option-left"
            :class="options.length === Length.LONG && 'option-active'"
          >
            Long
          </button>
          <button
            class="option"
            :class="options.length === Length.ALL && 'option-active'"
          >
            All
          </button>
          <button
            class="option option-right"
            :class="options.length === Length.SHORT && 'option-active'"
          >
            Short
          </button>
        </div>
      </div>
    </div>
  </div>
</template>
<style scoped>
/* ... */
</style>

8. Updating Our State After a Click Event

<script setup lang="ts">
// ...
</script>
<template>
  <div class="container">
    <h1>Baby Name Generator</h1>
    <p>Choose your options and click the "Find Names" button below</p>
    <div class="options-container">
      <div class="option-container">
        <h4>1) Choose a gender</h4>
        <div class="option-buttons">
          <button
            class="option option-left"
            :class="options.gender === Gender.BOY && 'option-active'"
            @click="options.gender = Gender.BOY"
          >
            Boy
          </button>
          <button
            class="option"
            :class="options.gender === Gender.UNISEX && 'option-active'"
            @click="options.gender = Gender.UNISEX"
          >
            Unisex
          </button>
          <button
            class="option option-right"
            :class="options.gender === Gender.GIRL && 'option-active'"
            @click="options.gender = Gender.GIRL"
          >
            Girl
          </button>
        </div>
      </div>
      <div class="option-container">
        <h4>2) Choose the name's popularity</h4>
        <div class="option-buttons">
          <button
            class="option option-left"
            :class="options.popularity === Popularity.TRENDY && 'option-active'"
            @click="options.popularity = Popularity.TRENDY"
          >
            Trendy
          </button>
          <button
            class="option option-right"
            :class="options.popularity === Popularity.UNIQUE && 'option-active'"
            @click="options.popularity = Popularity.UNIQUE"
          >
            Unique
          </button>
        </div>
      </div>
      <div class="option-container">
        <h4>3) Choose the name's length</h4>
        <div class="option-buttons">
          <button
            class="option option-left"
            :class="options.length === Length.LONG && 'option-active'"
            @click="options.length = Length.LONG"
          >
            Long
          </button>
          <button
            class="option"
            :class="options.length === Length.ALL && 'option-active'"
            @click="options.length = Length.ALL"
          >
            All
          </button>
          <button
            class="option option-right"
            :class="options.length === Length.SHORT && 'option-active'"
            @click="options.length = Length.SHORT"
          >
            Short
          </button>
        </div>
      </div>
    </div>
  </div>
</template>
<style scoped>
/* */
</style>

9. Adding the Names Array

// app.vue
<script setup lang="ts">
import { Gender, Popularity, Length, names } from './data'
interface OptionsState {
  gender: Gender
  popularity: Popularity
  length: Length
}

const options = reactive<OptionsState>({
  gender: Gender.UNISEX,
  popularity: Popularity.TRENDY,
  length: Length.ALL
})

const selectedNames = ref<string[]>([])
</script>
<template>
  <div class="container">
    <h1>Baby Name Generator</h1>
    <p>Choose your options and click the "Find Names" button below</p>
    <div class="options-container">
      // ...
      <button class="primary">Find Names</button>
    </div>
    {{ selectedNames }}
  </div>
</template>
<style scoped>
/*  */
.primary {
  background-color: rgb(249, 87, 89);
  color: white;
  border-radius: 6.5rem;
  border: none;
  padding: 0.75rem 4rem;
  font-size: 1rem;
  cursor: pointer;
}
</style>
// data.ts
interface Name {
    id: number;
    name: string;
    gender: Gender;
    popularity: Popularity;
    length: Length;
}

export enum Gender {
    GIRL = "Girl",
    BOY = "Boy",
    UNISEX = "Unisex",
}

export enum Popularity {
    TRENDY = "Trendy",
    UNIQUE = "Unique",
}

export enum Length {
    SHORT = "Short",
    LONG = "Long",
    ALL = "All",
}
export const names: Name[] = [
    {
        id: 1,
        name: "Laith",
        gender: Gender.BOY,
        popularity: Popularity.UNIQUE,
        length: Length.SHORT,
    },
    {
        id: 2,
        name: "Jake",
        gender: Gender.BOY,
        popularity: Popularity.TRENDY,
        length: Length.SHORT,
    },
    {
        id: 3,
        name: "Lamelo",
        gender: Gender.BOY,
        popularity: Popularity.UNIQUE,
        length: Length.SHORT,
    },
    {
        id: 4,
        name: "Abraham",
        gender: Gender.BOY,
        popularity: Popularity.TRENDY,
        length: Length.LONG,
    },
    {
        id: 5,
        name: "Bartholomew",
        gender: Gender.BOY,
        popularity: Popularity.UNIQUE,
        length: Length.LONG,
    },
    {
        id: 6,
        name: "Noah",
        gender: Gender.BOY,
        popularity: Popularity.TRENDY,
        length: Length.SHORT,
    },
    {
        id: 7,
        name: "Benjamin",
        gender: Gender.BOY,
        popularity: Popularity.TRENDY,
        length: Length.LONG,
    },
    {
        id: 8,
        name: "William",
        gender: Gender.BOY,
        popularity: Popularity.TRENDY,
        length: Length.LONG,
    },
    {
        id: 9,
        name: "Lucus",
        gender: Gender.BOY,
        popularity: Popularity.TRENDY,
        length: Length.SHORT,
    },
    {
        id: 10,
        name: "Harrison",
        gender: Gender.BOY,
        popularity: Popularity.UNIQUE,
        length: Length.LONG,
    },
    {
        id: 11,
        name: "Selma",
        gender: Gender.BOY,
        popularity: Popularity.UNIQUE,
        length: Length.SHORT,
    },
    {
        id: 12,
        name: "Asher",
        gender: Gender.BOY,
        popularity: Popularity.UNIQUE,
        length: Length.SHORT,
    },
    {
        id: 13,
        name: "Tucker",
        gender: Gender.BOY,
        popularity: Popularity.TRENDY,
        length: Length.SHORT,
    },

    {
        id: 14,
        name: "Arya",
        gender: Gender.GIRL,
        popularity: Popularity.UNIQUE,
        length: Length.SHORT,
    },
    {
        id: 15,
        name: "Olivia",
        gender: Gender.GIRL,
        popularity: Popularity.TRENDY,
        length: Length.SHORT,
    },
    {
        id: 16,
        name: "Fay",
        gender: Gender.GIRL,
        popularity: Popularity.UNIQUE,
        length: Length.SHORT,
    },
    {
        id: 17,
        name: "Brooklyn",
        gender: Gender.GIRL,
        popularity: Popularity.TRENDY,
        length: Length.LONG,
    },
    {
        id: 18,
        name: "Genevieve",
        gender: Gender.GIRL,
        popularity: Popularity.UNIQUE,
        length: Length.LONG,
    },
    {
        id: 19,
        name: "Zoe",
        gender: Gender.GIRL,
        popularity: Popularity.TRENDY,
        length: Length.SHORT,
    },
    {
        id: 20,
        name: "Valentina",
        gender: Gender.GIRL,
        popularity: Popularity.TRENDY,
        length: Length.LONG,
    },
    {
        id: 21,
        name: "Josephine",
        gender: Gender.GIRL,
        popularity: Popularity.TRENDY,
        length: Length.LONG,
    },
    {
        id: 22,
        name: "Maya",
        gender: Gender.GIRL,
        popularity: Popularity.TRENDY,
        length: Length.SHORT,
    },
    {
        id: 23,
        name: "Everleigh",
        gender: Gender.GIRL,
        popularity: Popularity.UNIQUE,
        length: Length.LONG,
    },
    {
        id: 24,
        name: "Poppy",
        gender: Gender.GIRL,
        popularity: Popularity.UNIQUE,
        length: Length.SHORT,
    },
    {
        id: 25,
        name: "Maia",
        gender: Gender.GIRL,
        popularity: Popularity.UNIQUE,
        length: Length.SHORT,
    },
    {
        id: 26,
        name: "Ivy",
        gender: Gender.GIRL,
        popularity: Popularity.TRENDY,
        length: Length.SHORT,
    },

    {
        id: 27,
        name: "Jude",
        gender: Gender.UNISEX,
        popularity: Popularity.UNIQUE,
        length: Length.SHORT,
    },
    {
        id: 28,
        name: "Adrian",
        gender: Gender.UNISEX,
        popularity: Popularity.TRENDY,
        length: Length.SHORT,
    },
    {
        id: 29,
        name: "Sunny",
        gender: Gender.UNISEX,
        popularity: Popularity.UNIQUE,
        length: Length.SHORT,
    },
    {
        id: 30,
        name: "Channing",
        gender: Gender.UNISEX,
        popularity: Popularity.TRENDY,
        length: Length.LONG,
    },
    {
        id: 31,
        name: "Tennessee",
        gender: Gender.UNISEX,
        popularity: Popularity.UNIQUE,
        length: Length.LONG,
    },
    {
        id: 32,
        name: "Dallas",
        gender: Gender.UNISEX,
        popularity: Popularity.TRENDY,
        length: Length.SHORT,
    },

    {
        id: 33,
        name: "Zephyr",
        gender: Gender.UNISEX,
        popularity: Popularity.UNIQUE,
        length: Length.LONG,
    },

    {
        id: 34,
        name: "Teri",
        gender: Gender.UNISEX,
        popularity: Popularity.TRENDY,
        length: Length.SHORT,
    },
];

10. Computing Names Based on Options

<script setup lang="ts">
// ...

const computedSelectedNames = () => {
  const filterNames = names
    .filter(name => name.gender === options.gender)
    .filter(name => name.popularity === options.popularity)
    .filter(name => {
      if (options.length === Length.ALL) return true
      else return name.length === options.length
    })

  selectedNames.value = filterNames.map(name => name.name)
}

const selectedNames = ref<string[]>([])
</script>

11. Creating the Name Cards

<script setup lang="ts">
// ...

const computedSelectedNames = () => {
  const filterNames = names
    .filter(name => name.gender === options.gender)
    .filter(name => name.popularity === options.popularity)
    .filter(name => {
      if (options.length === Length.ALL) return true
      else return name.length === options.length
    })

  selectedNames.value = filterNames.map(name => name.name)
}

const selectedNames = ref<string[]>([])
</script>
<template>
  <div class="container">
    <h1>Baby Name Generator</h1>
    <p>Choose your options and click the "Find Names" button below</p>
    <div class="options-container">
      ...
    </div>
    <div class="cards-container">
      <div v-for="name in selectedNames" :key="name" class="card">
        <h4>{{ name }}</h4>
        <p>x</p>
      </div>
    </div>
  </div>
</template>
<style scoped>
/* */
.cards-container {
  display: flex;
  margin-top: 3rem;
  flex-wrap: wrap;
}
.card {
  background-color: rgb(27, 60, 138);
  width: 28%;
  color: white;
  border-radius: 1rem;
  padding: 0.1rem;
  margin-right: 0.5rem;
  margin-bottom: 1rem;
  position: relative;
}
.card p {
  position: absolute;
  top: -29%;
  left: 92.5%;
  cursor: pointer;
  color: rgba(255, 255, 255, 0.178);
}
</style>

12. Working with Components

// app.vue
<script setup lang="ts">
// ...

const selectedNames = ref<string[]>([])

const optionsArray = [
  {
    title: '1) Choose a gender',
    category: 'gender',
    buttons: [Gender.GIRL, Gender.BOY, Gender.UNISEX]
  },
  {
    title: `2) Choose the name's popularity`,
    category: 'popularity',
    buttons: [Popularity.TRENDY, Popularity.UNIQUE]
  },
  {
    title: `3) Choose the name's length`,
    category: 'length',
    buttons: [Length.ALL, Length.LONG, Length.SHORT]
  }
]
</script>
<template>
  <div class="container">
    <h1>Baby Name Generator</h1>
    <p>Choose your options and click the "Find Names" button below</p>
    <div class="options-container">
      <Option
        v-for="option in optionsArray"
        :key="option.title"
        :option="option"
        :options="options"
      />
      <button class="primary" @click="computedSelectedNames">Find Names</button>
    </div>
    <div class="cards-container">
      <div v-for="name in selectedNames" :key="name" class="card">
        <h4>{{ name }}</h4>
        <p>x</p>
      </div>
    </div>
  </div>
</template>
<style scoped>
/*  */
</style>

13. Passing Props to the Components

// Option.vue
<script setup lang="ts">
import { Gender, Popularity, Length } from '../data'
interface OptionProps {
  option: {
    title: string
    category: string
    buttons: Gender[] | Popularity[] | Length[]
  }
  options: {
    gender: Gender
    popularity: Popularity
    length: Length
  }
}

const props = defineProps<OptionProps>()
</script>
<template>
  <div class="option-container">
    <h4>{{ option.title }}</h4>
    <div class="option-buttons">
      <button
        class="option"
        :class="options[option.category] === value && 'option-active'"
        @click="options[option.category] = value"
        v-for="value in option.buttons"
        :key="value"
      >
        {{ value }}
      </button>
    </div>
  </div>
</template>
<style>
.option-container {
  margin-bottom: 2rem;
}
.option {
  background: white;
  outline: 0.15rem solid rgb(249, 87, 89);
  border: none;
  padding: 0.75rem;
  width: 12rem;
  font-size: 1rem;
  color: rgb(27, 60, 138);
  cursor: pointer;
  font-weight: 200;
}
.option-left {
  border-radius: 1rem 0 0 1rem;
}
.option-right {
  border-radius: 0 1rem 1rem 0;
}
.option-active {
  background-color: rgb(249, 87, 89);
  color: white;
}
</style>

14. Computing the Class Names

// option.vue
<script setup lang="ts">
import { Gender, Popularity, Length } from '../data'
interface OptionProps {
  option: {
    title: string
    category: string
    buttons: Gender[] | Popularity[] | Length[]
  }
  options: {
    gender: Gender
    popularity: Popularity
    length: Length
  }
}

const props = defineProps<OptionProps>()

const computedButtonClasses = (value, index) => {
  const classNames: string[] = []
  if (props.options[props.option.category] === value) {
    classNames.push('option-active')
  }
  if (index === 0) classNames.push('option-left')
  if (index === props.option.buttons.length - 1) classNames.push('option-right')
  return classNames.join(' ')
}
</script>
<template>
  <div class="option-container">
    <h4>{{ option.title }}</h4>
    <div class="option-buttons">
      <button
        class="option"
        :class="computedButtonClasses(value,index)"
        @click="options[option.category] = value"
        v-for="(value, index) in option.buttons"
        :key="value"
      >
        {{ value }}
      </button>
    </div>
  </div>
</template>
<style>
/*  */
</style>

15. Dealing with Nested Components

// CardName.vue
<script setup lang="ts">
interface NameProps {
  name: string
}
const props = defineProps<NameProps>()
</script>
<template>
  <div class="card">
    <h4>{{ name }}</h4>
    <p>x</p>
  </div>
</template>
<style>
.card {
  background-color: rgb(27, 60, 138);
  width: 28%;
  color: white;
  border-radius: 1rem;
  padding: 0.1rem;
  margin-right: 0.5rem;
  margin-bottom: 1rem;
  position: relative;
}
.card p {
  position: absolute;
  top: -29%;
  left: 92.5%;
  cursor: pointer;
  color: rgba(255, 255, 255, 0.178);
}
</style>

16. Emitting Events to the Parent Component

// Card/Name.vue
<script setup lang="ts">
interface NameProps {
  name: string
  index: number
}
const props = defineProps<NameProps>()

const emit = defineEmits(['remove'])

const removeName = () => {
  emit('remove', props.index)
}
</script>
<template>
  <div class="card">
    <h4>{{ name }}</h4>
    <p @click="removeName">x</p>
  </div>
</template>
<style>
/*  */
</style>
// app.vue
<script setup lang="ts">
// ...

const removeName = (index: number) => {
  const filteredNames = [...selectedNames.value]
  filteredNames.splice(index, 1)
  selectedNames.value = filteredNames
}
</script>
<template>
  <div class="container">
    ...
    <div class="cards-container">
      <CardName
        v-for="(name, index) in selectedNames"
        :key="name"
        :name="name"
        :index="index"
        @remove="() => removeName(index)"
      />
    </div>
  </div>
</template>
<style scoped>
/*  */
</style>

3. Pages and File-Based Routing

1. App Overview



2. Adding Bootstrap Globally

npx nuxi init 02-Top-Restaurants
cd 02-Top-Restaurants
npm i

在 nuxt 里,我们看不到 html 文件,但是可以通过 nuxt.config.ts 配置, 这里引入 bootstrap

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: true },
  app: {
    head: {
      meta: [],
      link: [
        {
          rel: 'stylesheet',
          href: 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css'
        }
      ]
    }
  }
})


3. Creating the Landing Page

// app.vue
<template>
  <div class="">
    <Nav />
    <div class="container">
      <h1>Welcome to Restaurantly</h1>
      <a href="/restaurants">Go to restaurants</a>
    </div>
  </div>
</template>
<style>
.container {
  text-align: center;
  margin-top: 5rem;
}
</style>
// components/Nav.vue
<template>
  <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
    <div class="container-fluid">
      <a class="navbar-brand" href="/">Restaurantly</a>
      <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav ml-auto mb-2 mb-lg-0">
          <li class="nav-item">
            <a class="nav-link active" aria-current="page" href="/">Home</a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="/restaurants">Restaurants</a>
          </li>
        </ul>
      </div>
    </div>
  </nav>
</template>
<style>
.ml-auto {
  margin-left: auto;
}
</style>

4. Adding Pages into Our App

// page/restaurants.vue
import { Nav } from '../.nuxt/components'
;<template>
  <div class=''>
    <Nav />
    <h1>Restaurants</h1>
  </div>
</template>

app.vue 的内容移到 page/index.vue

5. Building the Restaurant Page

// components/restaurant/Table.vue
<script setup lang="ts">
import restaurants from '@/data.json'

const restaurantsOrganized = {
  first: [...restaurants].splice(0, 25),
  second: [...restaurants].splice(25, 25)
}
</script>

<template>
  <div class="table">
    <h1>TOP 50: THE RANKING</h1>
    <div class="table-container">
      <div class="table-col">
        <RestaurantRow
          v-for="restaurant in restaurantsOrganized.first"
          :key="restaurant.id"
        />
      </div>
      <div class="table-col">
        <RestaurantRow
          v-for="restaurant in restaurantsOrganized.second"
          :key="restaurant.id"
        />
      </div>
    </div>
  </div>
</template>

<style scoped>
.table {
  margin: 3rem 0;
}
.table h1 {
  margin-bottom: 2rem;
}
.table-container {
  display: flex;
  justify-content: space-between;
}
.table-col {
  width: 48%;
}
</style>
// components/restaurant/Row.vue
<script setup lang="ts"></script>

<template>
  <div class="row">
    <h4 class="header rank">1</h4>
    <a href="/" class="header link">McDonalds</a>
  </div>
</template>

<style scoped>
.row {
  border-top: 1px solid rgba(128, 128, 128, 0.336);
  display: flex;
  align-items: center;
}
.header {
  font-size: 1.25rem;
  color: rgb(10, 31, 148);
  margin: 0;
  margin-right: 2rem;
  padding: 0.5rem 1rem;
  font-weight: 700;
  width: 4rem;
}
.rank {
  color: black;
}
.link {
  text-decoration: none;
  color: rgb(228, 157, 27);
  width: auto;
  font-weight: 500;
}
</style>
// pages/restaurants.vue
import { Nav } from '../.nuxt/components'
;<template>
  <Nav />
  <div class='container'>
    <RestaurantTable />
  </div>
</template>

6. Adding the Restaurant Page Logic

// components/restaurant/Row.vue
<script setup lang="ts">
interface RowProps {
  rank?: number
  name?: string
  index?: number
  isHeader?: boolean
}
const props = defineProps<RowProps>()
</script>

<template>
  <div class="row" v-if="isHeader">
    <h4 class="header">Rank</h4>
    <h4 class="header">Chain</h4>
  </div>
  <div
    v-else
    class="row"
    :style="index % 2 === 0 ? { background: 'rgba(128,128,128,0.15)' } : null"
  >
    <h4 class="header rank">{{ rank }}</h4>
    <a :href="`/restaurants/${name}`" class="header link">{{ name }}</a>
  </div>
</template>

<style scoped>
/*  */
</style>
// components/restaurant/Table.vue
<script setup lang="ts">
// ...
</script>

<template>
  <div class="table">
    <h1>TOP 50: THE RANKING</h1>
    <div class="table-container">
      <div class="table-col">
        <RestaurantRow :isHeader="true" />
        <RestaurantRow
          v-for="(restaurant, index) in restaurantsOrganized.first"
          :key="restaurant.id"
          :name="restaurant.name"
          :rank="restaurant.rank"
          :index="index"
        />
      </div>
      <div class="table-col">
        <RestaurantRow :isHeader="true" />
        <RestaurantRow
          v-for="(restaurant, index) in restaurantsOrganized.second"
          :key="restaurant.id"
          :name="restaurant.name"
          :rank="restaurant.rank"
          :index="index"
        />
      </div>
    </div>
  </div>
</template>

<style scoped>
/*  */
</style>

7. Dynamic and Nest Routes


// pages/restaurants/index.vue
<template>
  <Nav />
  <div class="container">
    <RestaurantTable />
  </div>
</template>
// pages/restaurants/[name].vue
<template>
  <div class="">This is the restaurant data</div>
</template>

8. Extracting Path Parameters

<script setup lang=ts>
const route = useRoute()
const name = route.params.name
</script>

9. Catching the Not Found Error

// nuxt3.6.2 -> /error.vue
// nuxt3.?(视频中的) -> /pages/404.vue
<template>
  <div class="">
    <Nav />
    <div class="container">
      <h1>Page not found</h1>
      <NuxtLink to="/">Go Back</NuxtLink>
    </div>
  </div>
</template>
<style scoped>
.container {
  text-align: center;
  margin-top: 5rem;
}
</style>

4. Formatting Pages with Layouts

1. Defining the Default Layout

把 Nav 组件用在 layouts/default.vue,就可以给所有页面加上

<template>
  <div class="">
    <!-- 顶部导航 -->
    <Nav />
    <!-- 页面内容 -->
    <slot />
  </div>
</template>

2. Creating a Custom Page Layout


// pages/restaurants/[name].vue
<script setup lang="ts">
// ...
</script>
<template>
  <div>
    <!-- 使用custom.vue包装 -->
    <NuxtLayout name="custom" v-if="restaurant">
      <div class="restaurant-container">
        <div class="image-container">
          <img :src="restaurant?.imageUrl" alt="" />
        </div>
        <div class="info-container">
          <h1>{{ restaurant?.name }}</h1>
          <div class="stats-container">
            <h5>Revenue (in billions)</h5>
            <p>${{ restaurant?.revenue }}</p>
          </div>
          <div class="stats-container">
            <h5>Number of Stores</h5>
            <p>{{ restaurant?.numberOfStores }}</p>
          </div>
          <p class="content">{{ restaurant?.content }}</p>
        </div>
      </div>
    </NuxtLayout>
    <div class="restaurant-not-found" v-else>
      <h1>Restaurant not found</h1>
      <button
        class="btn btn-primary btn-lg"
        @click="$router.push('/restaurants')"
      >
        Go Back
      </button>
    </div>
  </div>
</template>

<style scoped>
/* */
</style>

3. Injecting Custom Elements to Layout Page





5. Defining Page Meta Data for Better SEO

1. Option 1 Using Components


2. Option 2 Using Composables

没用

6. Global State and Composables

1. App Overview


2. Approaches to State Management

这种方式,传递属性给很多组件,并且这个属性和组件本身并不是高度相关

使用统一的状态管理

3. Creating Our First Composable

根目录下的 composables 是存放全局状态的文件夹

// composables/useDarkMode.ts
const useDarkMode = () => {
  // ref可能导致内存泄漏
  // const isDarkMode = ref(false);

  const isDarkMode = useState('darkMode', () => false)

  return {
    isDarkMode
  }
}

export default useDarkMode
<script setup lang="ts">
const { isDarkMode } = useDarkMode()
</script>

<template>
  <div>
    <Nav />
    <Heading />
    <Cards />
  </div>
</template>

<style>
* {
  margin: 0;
  padding: 0;
  font-family: Georgia;
}
</style>

4. Using the Compasable State

<script setup lang="ts">
const { isDarkMode } = useDarkMode()
</script>

<template>
  <div :style="isDarkMode ? { backgroundColor: 'black' } : null">
    <Nav />
    <Heading />
    <Cards />
  </div>
</template>

<style>
* {
  margin: 0;
  padding: 0;
  font-family: Georgia;
}
</style>
// nav.vue
<script setup lang="ts">
const { isDarkMode, toggleDarkMode } = useDarkMode();
</script>

<template>
  <nav :style="isDarkMode ? { backgroundColor: 'rgb(73,72,72)' } : null">
    <div :style="isDarkMode ? { color: 'white' } : null">
      <h1>Artikle</h1>
      <label class="switch">
        <input type="checkbox" />
        <span class="slider round"></span>
      </label>
    </div>
  </nav>
</template>

<style scoped>
nav {
  border-bottom: 1px solid rgba(73, 72, 72, 0.274);
  padding: 20px;
  box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.253);
}

nav div {
  width: 50%;
  margin: 0 auto;
  display: flex;
  justify-content: space-between;
}

h1 {
  font-size: 30px;
}

/* The switch - the box around the slider */
.switch {
  position: relative;
  display: inline-block;
  width: 60px;
  height: 34px;
}

/* Hide default HTML checkbox */
.switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

/* The slider */
.slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: #ccc;
  -webkit-transition: 0.4s;
  transition: 0.4s;
}

.slider:before {
  position: absolute;
  content: "";
  height: 26px;
  width: 26px;
  left: 4px;
  bottom: 4px;
  background-color: white;
  -webkit-transition: 0.4s;
  transition: 0.4s;
}

input:checked + .slider {
  background-color: #2196f3;
}

input:focus + .slider {
  box-shadow: 0 0 1px #2196f3;
}

input:checked + .slider:before {
  -webkit-transform: translateX(26px);
  -ms-transform: translateX(26px);
  transform: translateX(26px);
}

/* Rounded sliders */
.slider.round {
  border-radius: 34px;
}

.slider.round:before {
  border-radius: 50%;
}
</style>
// header.vue
<script setup lang="ts">
const { isDarkMode } = useDarkMode();
</script>

<template>
  <div class="container">
    <div class="content-container">
      <div class="text-container">
        <h4>Based on your reading history</h4>
        <h2 :style="isDarkMode ? { color: 'white' } : null">
          Designing search for mobile apps
        </h2>
        <p :style="isDarkMode ? { color: 'white' } : null" class="date">
          Aug 26th, 2021, 4pm
        </p>
        <p :style="isDarkMode ? { color: 'white' } : null" class="snippet">
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, in.
          Obcaecati in iusto minima impedit assumenda perferendis natus tempore
          modi ducimus. Blanditiis, quis. Maxime delectus ducimus assumenda vel
          accusantium, eligendi nobis accusamus fuga, hic, natus ab odit. Earum,
          eveniet magnam perspiciatis similique odit cupiditate optio aut fugit,
          inventore, dolorem id?
        </p>
      </div>
      <img
        src="https://media.istockphoto.com/photos/mobile-responsive-website-development-wireframe-design-preview-on-picture-id844419966?b=1&k=20&m=844419966&s=170667a&w=0&h=Dfps34xqMI2CaLkKMznJnESU4G_XY5evvlC9ui9XEJk="
        alt=""
      />
    </div>
  </div>
</template>

<style scoped>
h4 {
  text-transform: uppercase;
  font-size: 20px;
  color: grey;
}

h2 {
  margin-top: 15px;
  font-size: 40px;
}

.date {
  margin-top: 15px;
}

.content-container {
  display: flex;
}

.text-container {
  padding-right: 50px;
}

.snippet {
  margin-top: 30px;
  font-size: 20px;
  line-height: 30px;
}

.container {
  width: 50%;
  margin: 0 auto;
  padding: 50px 0;
}
</style>
// cards.vue
<script setup lang="ts">
const { isDarkMode } = useDarkMode();
const cards = [
  {
    img: "https://images.unsplash.com/photo-1511919884226-fd3cad34687c?ixid=MnwxMjA3fDB8MHxzZWFyY2h8OHx8Y2FyfGVufDB8fDB8fA%3D%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60",
    title: "Sports Cars are Just Not Worth it",
    author: "Laith Harb",
  },
  {
    img: "https://images.unsplash.com/photo-1509316785289-025f5b846b35?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8ZGVzZXJ0fGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=800&q=60",
    title: "How to Survive in the Desert",
    author: "Joe Doe",
  },
  {
    img: "https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8M3x8c2hvZXxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60",
    title: "Why Shoes are Turning More Red",
    author: "Donald Brown",
  },
  {
    img: "https://images.unsplash.com/photo-1520763185298-1b434c919102?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTR8fGZsb3dlcnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60",
    title: "People are Eating this Deadly Flower",
    author: "Mike Anderson",
  },
  {
    img: "https://images.unsplash.com/photo-1559480671-12ceff3e511d?ixid=MnwxMjA3fDB8MHxzZWFyY2h8NHx8a29iZXxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60",
    title: "How to get the Mamba Mentality",
    author: "Kobe Bean Bryant",
  },
  {
    img: "https://images.unsplash.com/photo-1470229722913-7c0e2dbbafd3?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=3570&q=80",
    title: "Worst Concert Ever",
    author: "Gavin Scott",
  },
];
</script>

<template>
  <div
    class="container"
    :style="isDarkMode ? { backgroundColor: 'rgb(73,72,72)' } : null"
  >
    <div class="content-container">
      <h3 :style="isDarkMode ? { color: 'white' } : null">Reading List</h3>
      <div class="cards-container">
        <Card v-for="(card, index) in cards" :key="index" :card="card" />
      </div>
    </div>
  </div>
</template>

<style scoped>
.container {
  margin: 30px 0;
  background: rgba(250, 250, 194, 0.637);
}

.content-container {
  width: 50%;
  margin: 0 auto;
  padding: 40px 0;
}

.cards-container {
  margin-top: 30px;
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
}

h3 {
  font-size: 25px;
}
</style>
// cart.vue
<script setup lang="ts">
interface CardProps {
  card: {
    img: string;
    title: string;
    author: string;
  };
}
const { isDarkMode } = useDarkMode();
const props = defineProps<CardProps>();
</script>

<template>
  <div
    class="card"
    :style="isDarkMode ? { backgroundColor: 'black', color: 'white' } : null"
  >
    <img :src="card.img" alt="" />
    <div class="content">
      <h3>{{ card.title }}</h3>
      <p>{{ card.author }}</p>
    </div>
  </div>
</template>

<style scoped>
.card {
  box-shadow: 1px 1px 5px black;
  background: white;
  width: 30%;
  margin-bottom: 30px;
  border-radius: 5px;
  overflow: hidden;
}

img {
  width: 100%;
  height: 200px;
}

.content {
  padding: 10px;
}

p {
  margin-top: 10px;
}
</style>

5. Mutating Our Global State

const useDarkMode = () => {
  // ref可能导致内存泄漏
  // const isDarkMode = ref(false);

  const isDarkMode = useState('darkMode', () => false)

  const toggleDarkMode = () => {
    isDarkMode.value = !isDarkMode.value
  }

  return {
    isDarkMode,
    toggleDarkMode
  }
}

export default useDarkMode

7. Fetching Data and HTTP Requests

1. App Overview


2. Integrating Tailwind

npm i tailwindcss @tailwindcss/aspect-ratio @tailwindcss/forms @tailwindcss/line-clamp @tailwindcss/typography
// assets/css/tailwind.css
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
// nuxt.config.ts
import { defineNuxtConfig } from 'nuxt3'

// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
export default defineNuxtConfig({
  publicRuntimeConfig: {
    WEATHER_APP_SECRET: process.env.WEATHER_APP_SECRET
  },
  privateRuntimeConfig: {
    HELLO: 'world in the server not the client'
  },
  css: ['~/assets/css/tailwind.css'],
  build: {
    postcss: {
      postcssOptions: {
        plugins: {
          tailwindcss: {},
          autoprefixer: {}
        }
      }
    }
  }
})
// tailwind.config.js
const defaultTheme = require('tailwindcss/defaultTheme')

module.exports = {
  mode: 'jit',
  content: {
    files: [
      './components/**/*.{vue,js}',
      './layouts/**/*.vue',
      './pages/**/*.vue',
      './app.vue',
      './plugins/**/*.{js,ts}',
      './nuxt.config.{js,ts}'
    ]
  },
  theme: {
    extend: {
      fontFamily: {
        sans: ['"Inter var"', ...defaultTheme.fontFamily.sans]
      }
    }
  },
  variants: {
    extend: {}
  },
  plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/typography'),
    require('@tailwindcss/line-clamp'),
    require('@tailwindcss/aspect-ratio')
  ]
}

3. Writing the HTML Structure

<script setup lang="ts"></script>

<template>
  <div class="h-screen relative overflow-hidden">
    <img />
    <div class="absolute w-full h-full top-0 overlay" />
    <div class="absolute w-full h-full top-0 p-48">
      <div class="flex justify-between">
        <div>
          <h1 class="text-7xl text-white">111</h1>
          <p class="font-extralight text-2xl mt-2 text-white">111</p>
          <img />
        </div>
        <div>
          <p class="text-9xl text-white font-extralight">21°</p>
        </div>
      </div>
      <div class="mt-20">
        <input type="text" class="w-1/2 h-10" placeholder="Search a city..." />
        <button class="bg-sky-400 w-20 text-white h-10">Search</button>
      </div>
    </div>
  </div>
</template>

<style scoped>
.overlay {
  background-color: rgba(0, 0, 0, 0.5);
}

.icon {
  margin-left: -2.75rem;
  margin-top: -2.75rem;
}
</style>

4. The Basics of HTTP Requests


{
  "city": [
    {
      "id": 1,
      "name": "shanghai",
      "temp": 37
    },
    {
      "id": 2,
      "name": "beijing",
      "temp": 21
    }
  ]
}
// app.vue
<script setup lang="ts">
const { data: city, error } = useFetch('http://localhost:4000/city/1')
</script>

<template>
  <div class="h-screen relative overflow-hidden">
    <img />
    <div class="absolute w-full h-full top-0 overlay" />
    <div class="absolute w-full h-full top-0 p-48">
      <div class="flex justify-between">
        <div>
          <h1 class="text-7xl text-white">{{ city.name }}</h1>
          <p class="font-extralight text-2xl mt-2 text-white">111</p>
          <img :src="`http://localhost:4000/大雪.png`" class="w-56" />
        </div>
        <div>
          <p class="text-9xl text-white font-extralight">{{ city.temp }}°</p>
        </div>
      </div>
      <div class="mt-20">
        <input type="text" class="w-1/2 h-10" placeholder="Search a city..." />
        <button class="bg-sky-400 w-20 text-white h-10">Search</button>
      </div>
    </div>
  </div>
</template>

<style scoped>
.overlay {
  background-color: rgba(0, 0, 0, 0.5);
}
</style>

5. The useFetch Composable

6. Refetching Data with useFetch

<script setup lang="ts">
const search = ref('beijing')
const input = ref('')

const { data: city, error } = useFetch(
  // url变化会动态请求
  () => `http://localhost:4000/city?name=${search.value}`
)

const handleClick = () => {
  const formatedSearch = input.value.trim().split(' ').join('')
  search.value = formatedSearch
  input.value = ''
}
</script>

<template>
  <div class="h-screen relative overflow-hidden">
    <img />
    <div class="absolute w-full h-full top-0 overlay" />
    <div class="absolute w-full h-full top-0 p-48">
      <div class="flex justify-between">
        <div>
          <h1 class="text-7xl text-white">{{ city[0].name }}</h1>
          <p class="font-extralight text-2xl mt-2 text-white">{{ new Date() }}</p>
          <img :src="`http://localhost:4000/大雪.png`" class="w-56" />
        </div>
        <div>
          <p class="text-9xl text-white font-extralight">{{ city[0].temp }}°</p>
        </div>
      </div>
      <div class="mt-20">
        <input
          type="text"
          class="w-1/2 h-10"
          placeholder="Search a city..."
          v-model="input"
        />
        <button class="bg-sky-400 w-20 text-white h-10" @click="handleClick">
          Search
        </button>
      </div>
    </div>
  </div>
</template>

<style scoped>
.overlay {
  background-color: rgba(0, 0, 0, 0.5);
}

.icon {
  margin-left: -2.75rem;
  margin-top: -2.75rem;
}
</style>

7. The useAsyncData Composable

<script setup lang="ts">
const search = ref('beijing')
const input = ref('')
const background = ref('')

// const { data: city, error } = useFetch(
//   () => `http://localhost:4000/city?name=${search.value}`
// )

const { data: city, error } = useAsyncData('city', async () => {
  const response = await $fetch(
    `http://localhost:4000/city?name=${search.value}`
  )
  // return {
  //   name:''
  // }
  const temp = response[0].temp

  if (temp <= -10) {
    background.value =
      'https://bpic.588ku.com/back_list_pic/21/07/09/3821a250db9129c2e38061e849701efa.jpg!/fw/640/quality/90/unsharp/true/compress/true'
  } else if (temp > -10 && temp <= 0) {
    background.value =
      'https://img.tukuppt.com/ad_preview/00/08/01/5d19ea0aa424b.jpg!/fw/980'
  } else if (temp > 0 && temp <= 10) {
    background.value =
      'https://file.51pptmoban.com/d/file/2019/03/10/b18e181988ea2afe4e0c67701c743ab7.jpg'
  } else {
    background.value =
      'https://img.51miz.com/preview/element/00/01/16/69/E-1166981-2B217F5C.jpg'
  }

  return response
})

const handleClick = () => {
  const formatedSearch = input.value.trim().split(' ').join('')
  search.value = formatedSearch
  input.value = ''
}
</script>

<template>
  <div class="h-screen relative overflow-hidden">
    <img :src="background" class="w-full h-full"/>
    <div class="absolute w-full h-full top-0 overlay" />
    <div class="absolute w-full h-full top-0 p-48">
      <div class="flex justify-between">
        <div>
          <h1 class="text-7xl text-white">{{ city[0].name }}</h1>
          <p class="font-extralight text-2xl mt-2 text-white">
            {{ new Date() }}
          </p>
          <img :src="`http://localhost:4000/大雪.png`" class="w-56" />
        </div>
        <div>
          <p class="text-9xl text-white font-extralight">{{ city[0].temp }}°</p>
        </div>
      </div>
      <div class="mt-20">
        <input
          type="text"
          class="w-1/2 h-10"
          placeholder="Search a city..."
          v-model="input"
        />
        <button class="bg-sky-400 w-20 text-white h-10" @click="handleClick">
          Search
        </button>
      </div>
    </div>
  </div>
</template>

<style scoped>
.overlay {
  background-color: rgba(0, 0, 0, 0.5);
}

.icon {
  margin-left: -2.75rem;
  margin-top: -2.75rem;
}
</style>

8. Refetching Data with useAsyncData

<script setup lang="ts">
const search = ref('beijing')
const input = ref('')
const background = ref('')

// const { data: city, error } = useFetch(
//   () => `http://localhost:4000/city?name=${search.value}`
// )

const {
  data: city,
  error,
  refresh
} = useAsyncData(
  'city',
  async () => {
    const response = await $fetch(
      `http://localhost:4000/city?name=${search.value}`
    )
    // return {
    //   name:''
    // }
    const temp = response[0].temp

    if (temp <= -10) {
      background.value =
        'https://bpic.588ku.com/back_list_pic/21/07/09/3821a250db9129c2e38061e849701efa.jpg!/fw/640/quality/90/unsharp/true/compress/true'
    } else if (temp > -10 && temp <= 0) {
      background.value =
        'https://img.tukuppt.com/ad_preview/00/08/01/5d19ea0aa424b.jpg!/fw/980'
    } else if (temp > 0 && temp <= 10) {
      background.value =
        'https://file.51pptmoban.com/d/file/2019/03/10/b18e181988ea2afe4e0c67701c743ab7.jpg'
    } else {
      background.value =
        'https://img.51miz.com/preview/element/00/01/16/69/E-1166981-2B217F5C.jpg'
    }

    return response
  },
  {
    // 监听数据变化,变化了就重新获取数据
    watch: [search]
  }
)

const handleClick = () => {
  const formatedSearch = input.value.trim().split(' ').join('')
  search.value = formatedSearch
  input.value = ''
  // 重新获取数据
  // refresh()
}
</script>

<template>
  <div class="h-screen relative overflow-hidden">
    <img :src="background" class="w-full h-full" />
    <div class="absolute w-full h-full top-0 overlay" />
    <div class="absolute w-full h-full top-0 p-48">
      <div class="flex justify-between">
        <div>
          <h1 class="text-7xl text-white">{{ city[0].name }}</h1>
          <p class="font-extralight text-2xl my-2 text-white">
            {{ new Date() }}
          </p>
          <img :src="`http://localhost:4000/大雪.png`" class="w-56" />
        </div>
        <div>
          <p class="text-9xl text-white font-extralight">{{ city[0].temp }}°</p>
        </div>
      </div>
      <div class="mt-20">
        <input
          type="text"
          class="w-1/2 h-10"
          placeholder="Search a city..."
          v-model="input"
        />
        <button class="bg-sky-400 w-20 text-white h-10" @click="handleClick">
          Search
        </button>
      </div>
    </div>
  </div>
</template>

<style scoped>
.overlay {
  background-color: rgba(0, 0, 0, 0.5);
}

.icon {
  margin-left: -2.75rem;
  margin-top: -2.75rem;
}
</style>

8. Cookies and Runtime Config

2. The useCookie Composable

<script setup lang="ts">
const cookie = useCookie('city')
if (!cookie.value) cookie.value = '北京'

const search = ref(cookie.value)
const input = ref('')
const background = ref('')

const {
  data: city,
  error,
  refresh
} = useAsyncData(
  'city',
  async () => {
    let response

    try {
      response = await $fetch(
        `https://www.tianqiapi.com/api?version=v61&appid=23224653&appsecret=VnZ9foZ7&city=${search.value}`
      )
      if (response.city) {
        cookie.value = search.value
      } else {
        cookie.value = '北京'
      }

      const temp = response.tem

      if (temp <= -10) {
        background.value =
          'https://bpic.588ku.com/back_list_pic/21/07/09/3821a250db9129c2e38061e849701efa.jpg!/fw/640/quality/90/unsharp/true/compress/true'
      } else if (temp > -10 && temp <= 0) {
        background.value =
          'https://img.tukuppt.com/ad_preview/00/08/01/5d19ea0aa424b.jpg!/fw/980'
      } else if (temp > 0 && temp <= 10) {
        background.value =
          'https://file.51pptmoban.com/d/file/2019/03/10/b18e181988ea2afe4e0c67701c743ab7.jpg'
      } else {
        background.value =
          'https://img.51miz.com/preview/element/00/01/16/69/E-1166981-2B217F5C.jpg'
      }
    } catch (e) {}

    return response
  },
  {
    // 监听数据变化,变化了就重新获取数据
    watch: [search]
  }
)

const handleClick = () => {
  const formatedSearch = input.value.trim().split(' ').join('')

  search.value = formatedSearch
  input.value = ''
  // 重新获取数据
  // refresh()
}
</script>

<template>
  <div class="h-screen relative overflow-hidden">
    <img :src="background" class="w-full h-full" />
    <div class="absolute w-full h-full top-0 overlay" />
    <div class="absolute w-full h-full top-0 md:p-48 py-24 px-4">
      <div class="flex md:justify-between">
        <div>
          <h1 class="md:text-7xl text-3xl text-white">{{ city.city }}</h1>
          <p class="font-extralight md:text-2xl text-md my-2 text-white">
            {{ city.date }} - {{ city.wea }}
          </p>
          <img
            :title="`${city.wea}`"
            :src="`/${city.wea}.png`"
            class="w-24 md:w-56"
          />
        </div>
        <div>
          <p class="text-4xl md:text-9xl text-white font-extralight">
            {{ city.tem }}°C
          </p>
        </div>
      </div>
      <div class="mt-20">
        <input
          type="text"
          class="w-1/2 h-10"
          placeholder="输入想查看的城市..."
          v-model="input"
        />
        <button
          class="bg-sky-400 md:w-20 w-12 text-white h-10"
          @click="handleClick"
        >
          搜索
        </button>
      </div>
    </div>
  </div>
</template>

<style scoped>
.overlay {
  background-color: rgba(0, 0, 0, 0.5);
}
</style>

3. Storing Runtime Configs

import { defineNuxtConfig } from 'nuxt3'

// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
export default defineNuxtConfig({
  publicRuntimeConfig: {
    WEATHER_APP_SECRET: process.env.WEATHER_APP_SECRET,
    WEATHER_APP_ID: process.env.WEATHER_APP_ID
  },
  // 服务器可以访问,但是客户端不行
  privateRuntimeConfig: {
    HELLO: 'world in the server not the client'
  },
  css: ['~/assets/css/tailwind.css'],
  build: {
    postcss: {
      postcssOptions: {
        plugins: {
          tailwindcss: {},
          autoprefixer: {}
        }
      }
    }
  }
})
<script setup lang="ts">
const cookie = useCookie('city')
const config = useRuntimeConfig()
if (!cookie.value) cookie.value = '北京'

const search = ref(cookie.value)
const input = ref('')
const background = ref('')

const {
  data: city,
  error,
  refresh
} = useAsyncData(
  'city',
  async () => {
    let response

    try {
      response = await $fetch(`https://www.tianqiapi.com/api`, {
        params: {
          appsecret: config.WEATHER_APP_SECRET,
          appid: config.WEATHER_APP_ID,
          city: search.value,
          version: 'v61'
        }
      })

      if (response.city) {
        cookie.value = search.value
      } else {
        cookie.value = '北京'
      }

      const temp = response.tem

      if (temp <= -10) {
        background.value =
          'https://bpic.588ku.com/back_list_pic/21/07/09/3821a250db9129c2e38061e849701efa.jpg!/fw/640/quality/90/unsharp/true/compress/true'
      } else if (temp > -10 && temp <= 0) {
        background.value =
          'https://img.tukuppt.com/ad_preview/00/08/01/5d19ea0aa424b.jpg!/fw/980'
      } else if (temp > 0 && temp <= 10) {
        background.value =
          'https://file.51pptmoban.com/d/file/2019/03/10/b18e181988ea2afe4e0c67701c743ab7.jpg'
      } else {
        background.value =
          'https://img.51miz.com/preview/element/00/01/16/69/E-1166981-2B217F5C.jpg'
      }
      return response
    } catch (e) {}
  },
  {
    // 监听数据变化,变化了就重新获取数据
    watch: [search]
  }
)

const handleClick = () => {
  const formatedSearch = input.value.trim().split(' ').join('')

  search.value = formatedSearch
  input.value = ''
  // 重新获取数据
  // refresh()
}
</script>

<template>
  <div class="h-screen relative overflow-hidden">
    <img :src="background" class="w-full h-full" />
    <div class="absolute w-full h-full top-0 overlay" />
    <div class="absolute w-full h-full top-0 md:p-48 py-24 px-4">
      <div class="flex md:justify-between">
        <div>
          <h1 class="md:text-7xl text-3xl text-white">{{ city.city }}</h1>
          <p class="font-extralight md:text-2xl text-md my-2 text-white">
            {{ city.date }} - {{ city.wea }}
          </p>
          <img
            :title="`${city.wea}`"
            :src="`/${city.wea}.png`"
            class="w-24 md:w-56"
          />
        </div>
        <div>
          <p class="text-4xl md:text-9xl text-white font-extralight">
            {{ city.tem }}°C
          </p>
        </div>
      </div>
      <div class="mt-20">
        <input
          type="text"
          class="w-1/2 h-10"
          placeholder="输入想查看的城市..."
          v-model="input"
        />
        <button
          class="bg-sky-400 md:w-20 w-12 text-white h-10"
          @click="handleClick"
        >
          搜索
        </button>
      </div>
    </div>
  </div>
</template>

<style scoped>
.overlay {
  background-color: rgba(0, 0, 0, 0.5);
}
</style>

4. Error Handling

<script setup lang="ts">
const cookie = useCookie('city')
const config = useRuntimeConfig()
if (!cookie.value) cookie.value = '北京'

const search = ref(cookie.value)
const input = ref('')
const background = ref('')

const {
  data: city,
  error,
  refresh
} = useAsyncData(
  'city',
  async () => {
    let response

    try {
      response = await $fetch(`https://www.tianqiapi.com/api`, {
        params: {
          appsecret: config.WEATHER_APP_SECRET,
          appid: config.WEATHER_APP_ID,
          city: search.value,
          version: 'v61'
        }
      })

      if (response.city) {
        cookie.value = search.value
      } else {
        throw new Error('no find')
      }

      const temp = response.tem

      if (temp <= -10) {
        background.value =
          'https://bpic.588ku.com/back_list_pic/21/07/09/3821a250db9129c2e38061e849701efa.jpg!/fw/640/quality/90/unsharp/true/compress/true'
      } else if (temp > -10 && temp <= 0) {
        background.value =
          'https://img.tukuppt.com/ad_preview/00/08/01/5d19ea0aa424b.jpg!/fw/980'
      } else if (temp > 0 && temp <= 10) {
        background.value =
          'https://file.51pptmoban.com/d/file/2019/03/10/b18e181988ea2afe4e0c67701c743ab7.jpg'
      } else {
        background.value =
          'https://img.51miz.com/preview/element/00/01/16/69/E-1166981-2B217F5C.jpg'
      }
      return response
    } catch (e) {}
  },
  {
    // 监听数据变化,变化了就重新获取数据
    watch: [search]
  }
)

// let today = new Date()
// today = today.toLocaleDateString('en-US', {
//   weekday: 'long',
//   year: 'numeric',
//   month: 'long',
//   day: 'numeric'
// })

const handleClick = () => {
  const formatedSearch = input.value.trim().split(' ').join('')

  search.value = formatedSearch
  input.value = ''
  // 重新获取数据
  // refresh()
}

const goBack = () => {
  search.value = cookie.value
}
</script>

<template>
  <div v-if="city" class="h-screen relative overflow-hidden">
    <img :src="background" class="w-full h-full" />
    <div class="absolute w-full h-full top-0 overlay" />
    <div class="absolute w-full h-full top-0 md:p-48 py-24 px-4">
      <div class="flex md:justify-between">
        <div>
          <h1 class="md:text-7xl text-3xl text-white">{{ city.city }}</h1>
          <p class="font-extralight md:text-2xl text-md my-2 text-white">
            {{ city.date }} - {{ city.wea }}
          </p>
          <img
            :title="`${city.wea}`"
            :src="`/${city.wea}.png`"
            class="w-24 md:w-56"
          />
        </div>
        <div>
          <p class="text-4xl md:text-9xl text-white font-extralight">
            {{ city.tem }}°C
          </p>
        </div>
      </div>
      <div class="mt-20">
        <input
          type="text"
          class="w-1/2 h-10"
          placeholder="输入想查看的城市..."
          v-model="input"
        />
        <button
          class="bg-sky-400 md:w-20 w-12 text-white h-10"
          @click="handleClick"
        >
          搜索
        </button>
      </div>
    </div>
  </div>
  <div v-else class="p-10">
    <h1 class="text-7xl">No! 找不到该地区的数据!</h1>
    <button class="mt-5 bg-sky-400 px-10 w-50 text-white h-10" @click="goBack">
      Go Back
    </button>
  </div>
</template>

<style scoped>
.overlay {
  background-color: rgba(0, 0, 0, 0.5);
}

9. Building a REST API

1. App Overview

2. Our Endpoints

3. Creating Our Server

npx nuxi init todo
cd
npm i
npm run dev
// app.vue
<template>
  <div class="">
    {{ data }}
  </div>
</template>
<script setup lang="ts">
const { data } = useFetch('/api/todo')
</script>
// index.ts
export default defineEventHandler(() => {
  return 'Hello this is data'
})

4. GET Requests

import { db } from '~~/server/db'
// server/api/todo/index.ts
export default defineEventHandler(e => {
  const method = e.req.method
  if (method === 'GET') {
    return db.todos
  }
})
// server/db/index.ts
export const db = {
  todos: []
}

5. POST Requests and Body

npm i uuid
import { db } from '~~/server/db'
import { v4 as uuid } from 'uuid'

// /api/todo/
export default defineEventHandler(async e => {
  const method = e.req.method
  switch (method) {
    case 'GET':
      return db.todos
    case 'POST':
      const body = await useBody(e)
      // console.log({ body });
      if (!body.item) throw new Error()
      const newTodo = {
        id: uuid(),
        item: body.item,
        completed: false
      }

      db.todos.push(newTodo)
      return newTodo
    default:
      console.log('default')
      return
  }
})


6. PUT & DELETE with Dynamic Endpoints

// server/api/todo/[id].ts
import { db } from '~~/server/db'
export default defineEventHandler(e => {
  const method = e.req.method
  const context = e.context
  const { id } = context.params

  const findById = id => {
    let index
    const todo = db.todos.find((t, i) => {
      if (t.id === id) {
        index = i
        return true
      }
      return false
    })
    if (!todo) throw new Error()
    return { todo, index }
  }

  const { todo, index } = findById(id)

  switch (method) {
    case 'PUT':
      const uptTodo = {
        ...todo,
        completed: !todo.completed
      }

      db.todos[index] = uptTodo
      return uptTodo
    case 'DELETE':
      db.todos.splice(index, 1)

      return { msg: 'item deleted' }
  }
})

7. Error Handling

import { db } from '~~/server/db'
import { createError, sendError } from 'h3'

export default defineEventHandler(e => {
  const method = e.req.method
  const context = e.context
  const { id } = context.params

  const TodoNotFoundError = createError({
    statusCode: 404,
    statusMessage: 'Todo not found',
    data: {}
  })

  const findById = id => {
    let index
    const todo = db.todos.find((t, i) => {
      if (t.id === id) {
        index = i
        return true
      }
      return false
    })
    if (!todo) {
      sendError(e, TodoNotFoundError)
    }
    return { todo, index }
  }

  // ...
})
import { db } from '~~/server/db'
import { v4 as uuid } from 'uuid'
import { createError, sendError } from 'h3'

// /api/todo/
export default defineEventHandler(async e => {
  const method = e.req.method
  switch (method) {
    case 'GET':
      return db.todos
    case 'POST':
      const body = await useBody(e)
      // console.log({ body });
      if (!body.item) {
        const BadReqError = createError({
          statusCode: 400,
          statusMessage: 'No item provided',
          data: {}
        })
        sendError(e, BadReqError)
      }
    // ...
  }
})

8. Building the HTML

npm i @nuxt/ui

<template>
  <div class="container">
    <NCard class="cards">
      <h1>My Todos</h1>
      <div class="add-todo">
        <input type="text" placeholder="Add a new todo..." />
        <NButton>Add</NButton>
      </div>
      <NCard class="card" v-for="todo in todos" :key="todo.id">
        <h4>
          {{ todo.item }}
        </h4>
        <p>x</p>
      </NCard>
    </NCard>
  </div>
</template>
<script setup lang="ts">
const { data: todos } = useFetch('/api/todo')
</script>
<style scoped>
.container {
  padding: 2rem;
  margin: 0 auto;
  max-width: 50%;
}
.cards {
  padding: 2rem;
}
.card {
  padding: 0.5rem;
  margin-top: 1rem;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
}
input {
  outline: none;
}
.add-todo {
  display: flex;
  justify-content: space-between;
}
</style>

9. Making POST, PUT, DELETE Requests From the Frontend

<template>
  <div class="container">
    <NCard class="cards">
      <h1>My Todos</h1>
      <div class="add-todo">
        <input v-model="input" type="text" placeholder="Add a new todo..." />
        <NButton @Click="addTodo">Add</NButton>
      </div>
      <NCard
        @Click="() => uptTodo(todo.id)"
        class="card"
        v-for="todo in todos"
        :key="todo.id"
      >
        <h4 :class="todo.completed ? 'completed' : null">
          {{ todo.item }}
        </h4>
        <p @Click="() => delTodo(todo.id)">x</p>
      </NCard>
    </NCard>
  </div>
</template>
<script setup lang="ts">
const { data: todos } = useFetch('/api/todo')
const input = ref('')

const addTodo = async () => {
  if (!input.value) return
  await $fetch('/api/todo', {
    method: 'POST',
    body: {
      item: input.value
    }
  })
}

const uptTodo = async id => {
  await $fetch(`/api/todo/${id}`, { method: 'PUT' })
}

const delTodo = async id => {
  await $fetch(`/api/todo/${id}`, { method: 'DELETE' })
}
</script>
<style scoped>
.container {
  padding: 2rem;
  margin: 0 auto;
  max-width: 50%;
}
.cards {
  padding: 2rem;
}
.card {
  padding: 0.5rem;
  margin-top: 1rem;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
}
.completed {
  text-decoration: line-through;
}
input {
  outline: none;
}
.add-todo {
  display: flex;
  justify-content: space-between;
}
</style>

10. Organizing Code in Composable

// composables/useTodos.ts
const useTodos = () => {
  const { data: todos, refresh } = useAsyncData('todos', async () => {
    return $fetch('/api/todo')
  })

  const addTodo = async item => {
    if (!item) return
    await $fetch('/api/todo', {
      method: 'POST',
      body: { item }
    })
    refresh()
  }

  const uptTodo = async id => {
    await $fetch(`/api/todo/${id}`, { method: 'PUT' })
    refresh()
  }

  const delTodo = async id => {
    await $fetch(`/api/todo/${id}`, { method: 'DELETE' })
    refresh()
  }

  return {
    todos,
    addTodo,
    uptTodo,
    delTodo
  }
}
export default useTodos
<template>
  <div class="container">
    <NCard class="cards">
      <h1>My Todos</h1>
      <div class="add-todo">
        <input v-model="input" type="text" placeholder="Add a new todo..." />
        <NButton @Click="handleClick">Add</NButton>
      </div>
      <NCard
        @Click="() => uptTodo(todo.id)"
        class="card"
        v-for="todo in todos"
        :key="todo.id"
      >
        <h4 :class="todo.completed ? 'completed' : null">
          {{ todo.item }}
        </h4>
        <p @Click="() => delTodo(todo.id)">x</p>
      </NCard>
    </NCard>
  </div>
</template>
<script setup lang="ts">
const { todos, addTodo, uptTodo, delTodo } = useTodos()
const input = ref('')
const handleClick = () => {
  addTodo(input.value)
  input.value = ''
}
</script>
// ...

10. Integrating with Supabase

1. App Overview



2. Building the Authentication Card

// components/Auth/Card.vue
<template>
  <div class="">
    <NCard class="card">
      <h3>Login</h3>
      <div class="input-container">
        <input placeholder="Email" />
        <input placeholder="Password" />
      </div>
      <NButton>Login</NButton>
    </NCard>
  </div>
</template>
<script setup></script>
<style>
.card {
  padding: 1rem;
  width: 25rem;
}
.card h3 {
  font-size: 1.75rem;
}
.input-container {
  margin: 0.3rem 0;
  display: flex;
  flex-direction: column;
}
.input-container input {
  /* width: 10rem; */
  margin-bottom: 0.5rem;
  padding: 0.2rem;
  outline: none;

  border: 0.1rem solid rgba(0, 0, 0, 0.1);
  border-radius: 0.2rem;
}
</style>
// pages/index.vue
<template>
  <div class="">
    <div class="container">
      <AuthCard />
    </div>
  </div>
</template>
<script setup></script>
<style scoped>
.container {
  max-width: 50%;
  margin: 0 auto;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}
</style>

3. Conditionally Displaying Signup or Signin Form

<template>
  <div class="">
    <NCard class="card">
      <h3>{{ authState }}</h3>
      <div class="input-container">
        <input placeholder="Email" />
        <input placeholder="Password" />
      </div>
      <NButton>Submit</NButton>
      <p @Click="toggleAuthState">
        {{
          authState === 'Login'
            ? "Don't have an account? Create one now"
            : 'Already have an account? Go ahead an log in'
        }}
      </p>
    </NCard>
  </div>
</template>
<script setup lang="ts">
const authState = ref<'Login' | 'Signup'>('login')

const toggleAuthState = () => {
  if (authState.value === 'Login') authState.value = 'Signup'
  else authState.value = 'Login'
}
</script>
<style>
.card {
  padding: 1rem;
  width: 25rem;
}
.card h3 {
  font-size: 1.75rem;
  text-transform: capitalize;
}
.input-container {
  margin: 0.3rem 0;
  display: flex;
  flex-direction: column;
}
.input-container input {
  /* width: 10rem; */
  margin-bottom: 0.5rem;
  padding: 0.2rem;
  outline: none;

  border: 0.1rem solid rgba(0, 0, 0, 0.1);
  border-radius: 0.2rem;
}
p {
  color: blue;
  font-size: 0.5rem;
  cursor: pointer;
}
</style>

4. Creating a Supabase Account and Project


supabase

5. Connect Nuxt to Supabase

npm install @supabase/supabase-js
// composables/useSupabase.ts
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = 'https://dfhsbkwiamyvtpzrkvgg.supabase.co'
const supabaseKey = ''

const useSupabase = () => {
  const supabase = createClient(supabaseUrl, supabaseKey)

  return {
    supabase
  }
}

export default useSupabase
<template>
  <div class="">
    <div class="container">
      <AuthCard />
      {{ supabase }}
    </div>
  </div>
</template>
<script setup lang="ts">
const { supabase } = useSupabase()
</script>
// ...

11. Handling Authentication


 上一篇
gRPC [Node.js] Master Class Build Modern API & Microservices gRPC [Node.js] Master Class Build Modern API & Microservices
01 - gRPC Course Overview01.01 gRPC Introduction 01.02 Course Objective 03 - [Theory] gRPC Internals Deep Dive 介绍 grpc 的
2023-07-07
下一篇 
Online Japanese N5 Course(All 15 lessons) Online Japanese N5 Course(All 15 lessons)
2 - Lesson 1   I am a student わたしわ がくせいです 这里的 ta 发的音像 da 7 - 語彙 Vocabulary 自己不能称呼自己为 san,可以称呼别人(假设叫 xx yy)xx yy san
2023-07-05
  目录