什么是 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>
<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 做了一些修改
// 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 添加元数据
目前的 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>
在首页添加
// pages/index.vue
<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>
后面的懒得写了,待续……