首发于vue/nuxtjs
nuxt3 简易教程

nuxt3 简易教程

代码仓库

什么是 Nuxt

根据 Vue 官网的说法
而 Nuxt 是由 Vue 官方团队开发的 SSR 框架

创建项目

npx nuxi init todo
项目结构
创建完需要手动安装依赖
cd todo
npm i
# 启动
npm run dev

基本 html 和样式

<template>
  <div class="container">
    <div class="todos">
      <input type="text" placeholder="输入代办事项......" />
      <button>save</button>
    </div>
    <div class="items">
      <div class="item">
        <span class="item-todo">play game</span>
        <span class="x">x</span>
      </div>
      <div class="item">
        <span class="item-todo">play game</span>
        <span class="x">x</span>
      </div>
      <div class="item">
        <span class="item-todo done">play game</span>
        <span class="x">x</span>
      </div>
    </div>
    <div class="options">
      <span :class="['option', { active: option == 'all' ? true : false }]"
        >all</span
      >
      <span class="line">|</span>
      <span :class="['option', { active: option == 'done' ? true : false }]"
        >done</span
      >
      <span class="line">|</span>
      <span :class="['option', { active: option == 'todo' ? true : false }]"
        >todo</span
      >
    </div>
  </div>
</template>
<script setup>
const option = ref('all')
</script>
<style>
.container {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  margin-top: 60px;
}

.items {
  margin: 15px 0;
}
.options {
  margin: 15px 0;
}
.todos {
  margin: 15px 0;
}

.item {
  margin-bottom: 8px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.done {
  text-decoration: line-through;
  color: grey;
}
.x {
  margin-left: 190px;
  cursor: pointer;
  font-size: 18px;
}

.option {
  cursor: pointer;
  padding: 2px;
  color: grey;
}
.line {
  padding: 2px;
  color: grey;
}
.active {
  padding: 2px;
  color: black;
}

input {
  outline-style: none;
  border: 1px solid #ccc;
  border-radius: 3px;
  padding: 6px;
  width: 300px;
  /* margin: 0 15px; */
  font-size: 14px;
  font-family: 'Microsoft soft';
}
input:focus {
  border-color: #66afe9;
  outline: 0;
  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
    0 0 8px rgba(102, 175, 233, 0.6);
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
    0 0 8px rgba(102, 175, 233, 0.6);
}

button {
  background-color: #4caf50; /* Green */
  border: none;
  color: white;
  border-radius: 3px;
  padding: 8px 22px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  margin: 0 15px;
  font-size: 16px;
}
</style>

使用组件

nuxt 支持识别特定的文件夹
根目录下的 components 的 vue 文件会被自动识别为组件,使用时无需手动导入
设组件 components/test/A.vue,则该组件标签写法是:<TestA>
Item.vue
<template>
  <div class="item">
    <span :class="['item-todo', { done: isDone ? true : false }]">{{
      name
    }}</span>
    <span class="x" @click="handleClick">x</span>
  </div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
  isDone: {
    type: Boolean
  },
  name: {
    type: String
  }
})
const emits = defineEmits(['del'])
const handleClick = () => {
  emits('del', props.name)
}
</script>
Option.vue
<template>
  <span
    @click="changeOpt"
    :class="['option', { active: nowOption == option ? true : false }]"
  >
    {{ option }}</span
  >
  <span class="line" v-if="line">|</span>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
  option: {
    type: String
  },
  nowOption: {
    type: String
  },
  line: {
    type: Boolean,
    default: true
  }
})
const emits = defineEmits(['changeOpt'])
const changeOpt = () => {
  emits('changeOpt', props.option)
}
</script>
app.vue
<template>
  <div class="container">
    <div class="todos">
      <input type="text" placeholder="输入代办事项......" />
      <button>save</button>
    </div>
    <div class="items">
      <Item v-for="v in items" :name="v" @del="handleDel"></Item>
    </div>
    <div class="options">
      <Option
        v-for="v in [
          { opt: 'all', line: true },
          { opt: 'done', line: true },
          { opt: 'todo', line: false }
        ]"
        :option="v.opt"
        :nowOption="option"
        @changeOpt="handleChangeOption"
        :line="v.line"
      >
        ></Option
      >
    </div>
  </div>
</template>
<script setup>
const option = ref('all')
const handleChangeOption = value => {
  option.value = value
}
const items = ref(['play game', 'go to bed', 'fly'])
const handleDel = name => {
  items.value = items.value.filter(item => item !== name)
}
</script>

状态管理

nuxt 会将 composables 下的文件识别并自动添加到全局
composables/useOptionMode.ts
const useOptionMode = () => {
  const optionMode = useState('option', () => 'all')
  // 修改option
  const changeOptionMode = (name: string) => {
    optionMode.value = name
  }
  // 是否显示该todo项
  const isShow = (isDone: boolean) => {
    const val = optionMode.value
    if (val == 'all') {
      return true
    }
    if (val == 'done') {
      return isDone
    }
    if (val == 'todo') {
      return !isDone
    }
  }
  return {
    optionMode,
    changeOptionMode,
    isShow
  }
}

export default useOptionMode
在其他地方都可以使用
Item.vue
<template>
  <div class="item" v-if="isShow(isDone)">
    <span
      @click="handleToggleDone"
      :class="['item-todo', { done: isDone ? true : false }]"
      >{{ name }}</span
    >
    <span class="x" @click="handleClick">x</span>
  </div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const { isShow } = useOptionMode()
const props = defineProps({
  isDone: {
    type: Boolean
  },
  name: {
    type: String
  }
})

const emits = defineEmits(['del', 'toggleDone'])
const handleClick = () => {
  emits('del', props.name)
}
const handleToggleDone = () => {
  emits('toggleDone', props.name)
}
</script>
Option.value
<template>
  <span
    @click="changeOptionMode(option)"
    :class="['option', { active: optionMode == option ? true : false }]"
  >
    {{ option }}</span
  >
  <span class="line" v-if="line">|</span>
</template>
<script setup>
import { defineProps } from 'vue'
const { optionMode, changeOptionMode } = useOptionMode()
const props = defineProps({
  option: {
    type: String
  },
  line: {
    type: Boolean,
    default: true
  }
})
</script>
app.vue 做了一些修改
<template>
  <div class="container">
    <div class="todos">
      <input type="text" placeholder="输入代办事项......" />
      <button>save</button>
    </div>
    <div class="items">
      <Item
        v-for="v in items"
        :name="v.name"
        :isDone="v.isDone"
        @del="handleDel"
        @toggleDone="handleToggleDone"
      ></Item>
    </div>
    <div class="options">
      <Option
        v-for="v in [
          { opt: 'all', line: true },
          { opt: 'done', line: true },
          { opt: 'todo', line: false }
        ]"
        :option="v.opt"
        :line="v.line"
      >
        ></Option
      >
    </div>
  </div>
</template>
<script setup>
// const option = ref('all')
const items = ref([
  { name: 'play game', isDone: true },
  { name: 'go to bed', isDone: true },
  { name: 'fly', isDone: true }
])

const handleDel = name => {
  items.value = items.value.filter(item => item.name !== name)
}
const handleToggleDone = name => {
  items.value = items.value.map(item => {
    if (item.name == name) {
      item.isDone = !item.isDone
    }
    return item
  })
}
</script>

添加元数据

在 nuxt 项目里看不到 index.html,但是可以通过配置文件给 html 添加元数据
而元数据可以让SEO更好地搜索到我们
目前的 head 标签里没有我们自定义的内容
修改 nuxt.config.ts 添加内容
// 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'
        }
      ],
      title: 'Hello Nuxt'
    }
  }
})
可以看到多出了我们添加的内容

基于页面和文件的路由

nuxt 可以识别 pages 文件夹下的文件,将它们添加为页面
index.vue 会识别为首页(将 app.vue 的内容移动到 pages/index.vue 并删除 app.vue,然后要重启项目才能应用更改)
然后我们添加 about 页面
我们可以使用动态的路由
此时,访问/about 会显示 about/index.vue,而访问/about/xxx 则会显示/about/[name].vue
我们给 about 添加跳转逻辑,来测试 about/[name].vue
pages/about/index.vue
<template>
  <div>
    <h1>about</h1>
    <ul>
      <li v-for="v in ['a', 'b', 'c', 'd', 'e']">
        <!-- 跳转可以使用 <NuxtLink to="xxx" /> -->
        <a :href="`/about/${v}`">{{ v }}</a>
      </li>
    </ul>
  </div>
</template>
<style scoped>
div {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  margin-top: 50px;
}
</style>

提取路径参数

我们编写[name].vue,从路由中提取参数并显示
pages/about/[name].vue
<template>
  <div class="">
    <h1>{{ name }}</h1>
  </div>
</template>
<script setup>
const route = useRoute()
const name = route.params.name
</script>
<style scoped>
div {
  margin-top: 50px;
  text-align: center;
}
</style>

错误页面

Nuxt 支持自定义错误时显示的页面,在根目录新建 error.vue
<template>
  <div>
    <div class="container">
      <h1>Something be wrong!</h1>
      <NuxtLink to="/">Go Back</NuxtLink>
    </div>
  </div>
</template>
<style scoped>
.container {
  text-align: center;
  margin-top: 5rem;
}
</style>
我们将 pages/about/[name].vue 里的 fetch 故意修改为错误的
此时错误页面显示
添加对 404 的处理
<template>
  <div>
    <div class="container">
      <h1 v-if="error.statusCode === 404">Page not found!</h1>
      <h1 v-else>Something be wrong!</h1>
      <NuxtLink to="/">Go Back</NuxtLink>
    </div>
  </div>
</template>
<script setup>
import { defineProps } from 'vue'
const props = defineProps({
  error: {
    type: Object
  }
})
</script>

使用 layout 布局

我们可以添加 layouts/default.vue,它会把该文件的内容添加到所有组件
layouts/default.vue
<template>
  <div class="">
    <!-- 顶部导航 -->
    <nav>
      <span><a href="/">Home</a></span>
      <span><a href="/about">About</a></span>
    </nav>
    <!-- 页面内容 -->
    <slot />
  </div>
</template>
<style scoped>
nav {
  background-color: aquamarine;
  padding: 5px;
  display: flex;
  align-items: center;
  justify-content: end;
}
span {
  cursor: pointer;
  margin: 8px;
}
a {
  text-decoration: none;
}
</style>

自定义布局

我们可以自定义 layout,然后手动在需要的地方使用。假设我们要在页脚加个人信息
layouts/custom.vue
<template>
  <div>
    <!-- 页面内容 -->
    <slot />
    <div class="foot">
      <div>个人博客网站:<a href="//malred.github.io">malred.github.io</a></div>
    </div>
  </div>
</template>
<script setup></script>
<style scoped>
.foot {
  font-size: 18px;
  line-height: 50px;
  height: 50px;
  text-align: center;
  background-color: aquamarine;
  position: absolute;
  width: 100%;
  bottom: 0;
}
</style>
在首页添加
<template>
  <div class="">
    <!-- 通过name属性来指定,这里使用了layouts/custom.vue -->
    <NuxtLayout name="custom">
      <div class="container">
        ...
      </div>
    </NuxtLayout>
  </div>
</template>

获取数据

我们使用 json-server 模拟假数据
/db/db.json
{
  "mock": [
    {
      "id": 1,
      "name": "a",
      "desc": "骨干成员a"
    },
    {
      "id": 2,
      "name": "b",
      "desc": "骨干成员b"
    },
    {
      "id": 3,
      "name": "c",
      "desc": "骨干成员c"
    },
    {
      "id": 4,
      "name": "d",
      "desc": "骨干成员d"
    },
    {
      "id": 5,
      "name": "e",
      "desc": "骨干成员e"
    }
  ]
}
在 db.json 同级目录下新增 package.json
{
  "scripts": {
    "mock": "json-server -w -p 5000 db.json"
  }
}
启动
yarn mock

useFetch

pages/about/[name].vue
<template>
  <div class="">
    <h1>{{ mock[0].desc }}</h1>
  </div>
</template>
<script setup>
const route = useRoute()
const name = route.params.name

const { data: mock, error } = useFetch(
  // url变化会动态请求
  () => `http://localhost:5000/mock?name=${name}`
)
</script>

useAsyncData

pages/about/[name].vue
<template>
  <div class="">
    <h1>{{ mock[0].desc }}</h1>
  </div>
</template>
<script setup>
const route = useRoute()
const name = route.params.name

const { data: mock, error } = useAsyncData('mock', async () => {
  const response = await $fetch(`http://localhost:5000/mock?name=${name}`)

  // 可以对得到的数据进行一些操作
  response[0].desc += '~'

  return response
})
</script>
<style scoped>
div {
  margin-top: 50px;
  text-align: center;
}
</style>
useAsyncData 还可以通过 watch 来监听数据变化,动态发起请求

使用 cookie

Nuxt 提供了 useCookie 来操作 cookie
pages/about/[name].vue
<template>
  ...
</template>
<script setup>
const route = useRoute()
const name = route.params.name
// ...

const cookie = useCookie('name')
cookie.value = name
</script>

存储运行配置

一般前端项目都有一个.env 文件来存放一些信息,Nuxt 可以通过修改配置文件来实现
.env 文件
NUXT_PUBLIC_BASE_URL=http://localhost:5000
nuxt.config.ts 文件
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  // devtools: { enabled: true },
  app: {
    // ...
  },
  runtimeConfig: {
    // The private keys which are only available server-side
    shoeStoreApiSecret: 'my-secret-key',
    // Keys within public are also exposed client-side
    public: {
      baseUrl: process.env.NUXT_PUBLIC_BASE_URL
    }
  }
})
在 pages/about/[name].vue 中使用
<template>
  <div class="">
    <h1 @click="console.log(config)">{{ mock[0].desc }}</h1>
  </div>
</template>
<script setup>
// ...

// 读取环境变量
const config = useRuntimeConfig()
console.log(config.public.baseUrl)

const { data: mock, error } = useAsyncData('mock', async () => {
  const response = await $fetch(`${config.public.baseUrl}/mock?name=${name}`)

  // 可以对得到的数据进行一些操作
  response[0].desc += `~`

  return response
})

// ...
</script>

构建 API

Nuxt 支持定义接口

定义 Get

// server/db/index.ts
export const db = {
  todos: [
    { name: 'play game', isDone: true },
    { name: 'go to bed', isDone: true },
    { name: 'fly', isDone: true }
  ]
}
// server/api/todo/index.ts
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
  }
})
// pages/index.vue
<template>
  ...
</template>
<script setup>
// const option = ref('all')
// 使用api提供的数据
const { data } = useFetch('/api/todo')
const items = ref(data)

// ...
</script>

后面的懒得写了,待续……

编辑于 2023-08-12 10:35・IP 属地福建