Nuxt3介绍
Nuxt 3 - The Hybrid Vue Framework (nuxtjs.org)
Home | Nuxt3中文文档 (57code.github.io)
Nuxt是一个基于Vue3的混合开发框架,下面特性表明,这是一个体系完备的通用开发框架,他能提供良好的代码组织和服务端渲染/静态网站生成(SSR/SSG)能力
安装
确保安装了 npx(npx 在 NPM 版本 5.2.0 默认安装了),确认node.js版本为v14.16.0以上,输入命令行后回去github拉取项目模板,暂时不提供任何配置选项
我遇到了npm版本过高pinia模块安装时语法有限制导致安装报错,node版本降到了14.16.0解决
前排提示,如果遇到less保存,终端显示Vite热更新,但是页面没有刷新,可以尝试npm run dev 再次启动服务器
npx nuxi init nuxt3-app
在vscode新窗口中打开nuxt3-app
code nuxt3-app
安装依赖
npm install
项目目录
项目目录只有这些
路由&视图
约定路由
运行服务器
npm run dev
根目录已经存在了一个入口文件app.vue,按照下方提示,创建一个pages/index.vue来开始项目
为什么说nuxt是一个开发起来很简洁的框架,因为所有的东西都基于配置,如果只有一个页面那就是app.vue,如果有很多页面,需要路由导航,根据约定,根目录创建一个pages,所有的单个页面都放在这里,创建pages/index.vue后,终端显示了更新,页面显示404
显然,Nuxt根据文件创建刷新了服务器,index.vue内写下内容保存,仍然是404
<template>
<h3>Hello Nuxt3!</h3>
</template>
很明显app.vue还需要配置路由出口,终端显示热更新后,页面仍然是404,尝试重启服务器,页面显示了index.vue组件
<template>
<div>
<NuxtPage />
</div>
</template>
可以发现,并没有配置vue-router,这是因为Nuxt会注册pages文件夹内的组件为路由,index会被识别为 / 路由,如果是其他组件名,则是被注册为同名路由
接下来尝试一下添加一个子路由组件
index.vue添加路由跳转链接
<template>
<div>
<h3>Index Page</h3>
<NuxtLink to="/detail">Detail Page</NuxtLink>
</div>
</template>
路由生效了
上面初步的了解,nuxt是将router和views整合到了一起,放入pages内,只要有pages这个文件夹,nuxt就会把vue-router引进来,内部的组件会被按照目录结构自动注册到路由, 这一点用起来非常的简便,他们在最终生成的路由配置表中是下面这样,这个在哪里后面再讲
[ { "name": "index", "path": "/", "file": "K:/nuxt3-app/pages/index.vue", "children": [],
"component": () => __vite_ssr_dynamic_import__('/pages/index.vue')
},
{
"name": "detail",
"path": "/detail",
"file": "K:/nuxt3-app/pages/detail.vue",
"children": [],
"component": () => __vite_ssr_dynamic_import__('/pages/detail.vue')
},
]
动态路由
如果需要传参使用动态路由,可以在文件夹或者组件名上添加方括号
[id].vue 页面内获取两个路由参数
<template>
<div>
<h1>user page</h1>
<p>group : {{ $route.params.group }}</p>
<p>id: {{ $route.params.id }}</p>
</div>
</template>
index.vue添加跳转链接
<template>
<div>
<h3>Index Page</h3>
<NuxtLink to="/detail">Detail Page</NuxtLink> |
<NuxtLink to="/user-admin/1">User Page</NuxtLink>
</div>
</template>
新页面打开,获取到了路由参数 (如果跳转失败,重启一下服务器,感觉用起来还是会有bug,但相比原生还是非常简便的)
生成的路由是这样的
{
"name": "user-group-id",
"path": "/user-:group/:id",
"file": "K:/nuxt3-app/pages/user-[group]/[id].vue",
"children": [],
"component": () => __vite_ssr_dynamic_import__('/pages/user-[group]/[id].vue')
}
嵌套路由
文件夹和组件同名时,就会被注册为嵌套路由
// 上级路由
<template>
<div>
<h2>Parent Page</h2>
<nuxt-child />
</div>
</template>
// 子路由
<template>
<div>
<h3>Child Page</h3>
</div>
</template>
// index.vue
<template>
<div>
<h3>Index Page</h3>
<NuxtLink to="/detail">Detail Page</NuxtLink> |
<NuxtLink to="/user-admin/1">User Page</NuxtLink> |
<NuxtLink to="/parent/child">Child Page</NuxtLink>
</div>
</template>
添加链接,点击后成功跳转
如果只访问/parent,会发现child内容没有了,导致父页面内容缺失,如需要显示child缺失的默认子组件的路由,可以在parent目录下新建一个index.vue
<template>
<div>
<h3>Parent/Index Page</h3>
</div>
</template>
访问/parent路由访问到了默认子路由内容 ( 啊对,又重启服务器了
产生的路由会是这样
{
"path": "/parent",
"file": "K:/nuxt3-app/pages/parent.vue",
"children": [
{
"name": "parent-child",
"path": "child",
"file": "K:/nuxt3-app/pages/parent/child.vue",
"children": [],
"component": () => __vite_ssr_dynamic_import__('/pages/parent/child.vue')
},
{
"name": "parent",
"path": "",
"file": "K:/nuxt3-app/pages/parent/index.vue",
"children": [],
"component": () => __vite_ssr_dynamic_import__('/pages/parent/index.vue')
}
]
}
原理
nuxt动态生成的路由是在.nuxt/dist/server.mjs里面的,搜索routes,第一个结果得到后面的id
第二个结果,就是nuxt动态生成的路由配置
问题
以上配置不能实现的方面,比如别名、重定向、路由守卫等需求,在Nuxt2中的可用方案:
- router-extras-module 在页面中自定义路由参数
- @nuxt/router 覆盖Nuxt路由并编写自己的 router.js 文件
- 在配置文件 nuxt.config.js 中使用 router.extendRoutes 选项 (Nuxt3无效)
在Nuxt3中已全部失效,需要等官方的以上两个扩展更新
布局系统
通过自定义布局页,可以提取一些通用UI或代码到可重用的布局组件中,提高代码复用性,非常便捷高效
默认布局
那些放在 layouts/ 目录下的SFC会被自动加载进来,如果创建的SFC名为 default.vue,将会作为布局的模板,被用于项目中几乎所有的页面
接下来创建目录layouts和组件default.vue,然后重启服务器
实测在nuxt2内路由更新是不需要重启服务器的,可能是Nuxt3和Vite的结合有问题,等待后续修复后面笔记的内容但凡修改没有动态生效,都重启服务器不在重复说明
给这组件内元素加类名打上标记,可以看到,这个组件实际上会作为app.vue子元素,替换掉NuxtPage的位置,然后将NuxtPage放入default.vue的slot插槽内
自定义布局
如果需要自定义布局,就不能使用default.vue命名了,比如 custom.vue,定义之后想要正常使用,就必须在某个页面中设置页面属性layout
接下来在layouts下新建一个custom.vue
<template>
<div class="layouts">
自定义布局页,custom.vue:
<slot></slot>
</div>
</template>
pages/other.vue 作为需要在自定义布局中显示的组件,要进行页面配置
<template>
<div>
<h1>Other Page</h1>
</div>
</template>
<script>
export default {
layout: "custom"
}
</script>
pages/index.vue添加一个到other的路由链接,点击跳转,可以看到布局组件变了
使用NuxtLayout
以上两种方式不是特别的灵活,Nuxt则提供了 NuxtLayout组件结合slots来获得完全的控制力
首先custom.vue添加一个具名插槽比如title
<template>
<div class="layouts">
自定义布局页,custom.vue:
<slot name="title"></slot>
<slot></slot>
</div>
</template>
pages/other.vue,在NuxtLayout标签上绑定布局组件,通过template命名来插到布局组件对应的插槽内,注意需要设置组件选项 layout: false
<template>
<NuxtLayout name="custom">
<template #title>
<div class="title">
<h1>this is title</h1>
</div>
</template>
<template #default>
<div class="content">
<h1>some content</h1>
</div>
</template>
</NuxtLayout>
</template>
<script>
export default {
layout: false
}
</script>
<script setup>
// 使用setup语法糖就另起一个script标签
</script>
甚至 NuxtLayout 可以不止一个,对页面进行任意的组合控制
配合
就像上面的备注一样,想要使用
<script>
export default {
layout: false
}
</script>
<script setup>
// 使用setup语法糖就另起一个script标签
</script>
组件导入
组件自动导入
在使用vue2开发的时候,总是会遇到先import引入组件,然后components注册组件,再到template中使用,vue3帮我们省去了注册组件,但是如果一个页面引入的子组件过多,数排的import也是感觉碍眼,
这一点,现在也给解决了,把组件放入components目录,nuxt会帮助自动导入组件,所有的组件声明完成直接使用,不再需要import,有效提高开发体验
首先创建两个组件
然后打开父组件default.vue,直接使用
<template>
<div class="layouts">
<TheHeader/>
通用布局页,default.vue:
<slot></slot>
<TheFooter/>
</div>
</template>
可以看到页面上已经生效了
组件名称约定
没有嵌套的组件会以原文件名自动导入,使用的时候要与组件名写法一致,不可以换成-分隔符写法
如果是文件夹嵌套的话,组件名将会基于路径和文件名的拼合注册到全局,比如base/foo/Button.vue,注册后的组件名会是BaseFooButton
组件懒加载
如果在组件名前面加上Lazy前缀,则可以实现按需加载该组件,打包的时候会单独分块,优化打包尺寸,考虑到路由已经让打包分块了,使用懒加载的可能性为这个路由内还有较大的组件,但并不是必要的,只有在用户用的时候才加载,这时候就可以使用Lazy的方式进行懒加载,这个组件会在打包的时候单独分块儿出去
<template>
<div>
<h1>Mountains</h1>
<LazyMountainsList v-if="show" />
<button v-if="!show" @click="show = true">显示列表</button>
</div>
</template>
<script setup>
import { ref } from "vue"
const show = ref(false)
</script>
运行打包命令
npm run build
可以看到这个组件被单独打包了出回来
数据获取
nuxt3 中提供的数据获取函数有以下四个,需要注意,他们必须在 setup 和 生命周期钩子中使用
- useFetch
- useLazyFetch
- useAsyncData
- useLazyAsyncData
useAsyncData
在页面、组件或插件中都可以使用 useAsyncData 获取那些异步数据,比如:
const {
data: Ref(DataT), // 返回的数据
pending: Ref(boolean), // 加载状态指示器
refresh: (force?: boolean) => Promise<viod>, // 强制刷新数据的函数
error?: any // 请求失败的错误信息
} = useAsyncData (
key: string, // 唯一键用于多次请求结果去重
fn: () => Object, // 返回数值的异步函数
// lazy - 是否在路由跳转之后请求数据,默认为false,接收到服务器返回的数据才开始渲染页面,这样体验不好,
// 可以设为true,渲染页面后接收数据并显示数据,这时就需要用到pending判断来显示加载指示器
// server - 是否在服务端请求数据,如果页面是首屏,默认为true,这样数据会在服务端进行请求并渲染dom直接发送给用户
// 用户看到的直接就是页面,体验友好,如果是SPA,要在客户端发起请求,则设为false
options?: { lazy: boolean, server: boolean }
)
useLazyAsyncData
这个方法等效于 useAsyncData,仅仅是默认设置了lazy选项为true,即不会阻塞路由导航,对于服务器返回的数据需要处理null的情况(或者通过default对数据设置默认值)
todos实例
创建server/api/todo.ts
server下的内容会被nuxt自动注册,可以直接在组件内/api/todo访问
type todo = {
id: number;
title: string;
completed: boolean;
}
let todos: todo[] = [
{ id: 1, title: 'nuxt3', completed: false },
{ id: 2, title: 'vue3', completed: true },
]
export default () => {
return todos
}
index.vue
<template>
<div>
<div v-for="todo in todos" :key="todo.id">
<input type="checkbox" v-model="todo.completed">
<strong>{{ todo.title }}</strong>
</div>
</div>
</template>
<script setup>
const { data: todos } = await useAsyncData('todos', () => $fetch('/api/todo'))
</script>
useAsyncData() 可以直接使用
第一个参数为key,作为一个唯一的id,对多次请求返回的数据去重,
第二个参数为处理函数,调用官方封装的$fetch的全局变量,它类似与Fetch API
由于setup script 就是一个顶级的Async方法,所以可以直接在setup内 await
页面刷新后,获取数据todo,完成了dom的渲染
useFetch
页面、组件或者插件中可以使用 useFetch 获取任意URL资源
useFetch【使用参考ohmyfetch】是对 useAsyncData的包装,自动生成key同时推断响应类型,用起来更简单 ,基本一致
const {
data: Ref(DataT), // 返回的数据
pending: Ref(boolean), // 加载状态指示器
refresh: (force?: boolean) => Promise<viod>, // 强制刷新数据的函数
error?: any // 请求失败的错误信息
} = useFetch (url: string, options?)
useLazyFetch
这个方法等效于 useFetch,仅仅是默认设置了lazy选项为true,即不会阻塞路由导航,对于服务器返回的数据需要处理null的情况(或者通过default对数据设置默认值)
gql请求例子:
const gqlData = ref({
articles: "empty"
})
const gqlPending = ref(false)
const page = ref({
offset: 3,
limit: 5
})
const query = `
query CurrentUser($offset: Int, $limit: Int) {
articles(offset: $offset, limit: $limit) {
articles {
title
description
body
}
articlesCount
}
}
`;
const getData = async() => {
const { data, pending, error} = await useLazyFetch('http://192.168.31.107:32774/graphql',{
method: "POST",
body:{
query,
page
}
})
gqlData.value = data.value
gqlPending.value = pending.value
}
创建友好的跨组件共享状态
Nuxt3提供了 useState 创建响应式且服务端友好的跨组件状态共享管理能力,它是服务端友好的 ref 替换,它的值在服务端渲染(客户端注水过程中)将被保留并通过唯一的key在组件间共享
方法签名
useState<T>(key: string, init?: () => T): Ref<T>
- key:唯一键用于去重
- init:提供初始值的函数
useState() 的使用
这样创建的状态就可以直接给模板使用了,导出之后也可以在其他页面使用
<template>
<div>
<button @click="counter++">+</button>
<span> [ {{ counter }} ] </span>
<button @click="counter--">-</button>
</div>
</template>
<script setup>
export const counter = useState('counter', () => 1)
</script>
但这样在其他组件import使用也有些不便,可以使用Nuxt的另一个特性 composables (复合/组合)
composables
复用逻辑都写在这个目录内,Nuxt约定所有在这个目录内的可复用逻辑都会被自动导入
创建composables/counter.ts,因为useState是以一个函数的返回值来确定,所以抛出的需要是一个函数,为了便于特征分辨,命名为useCounter
export const useCounter = () => useState("counter", () => 1)
接下来在任意组件内,无需import导入,就可以直接获取到这个状态
<template>
<div>
<button @click="counter++">+</button>
<span> [ {{ counter }} ] </span>
<button @click="counter--">-</button>
</div>
</template>
<script setup>
const counter = useCounter()
</script>
官方实例
locale.ts,用于获取用户端的语言
import type { Ref } from "vue"
export const useLocale = () => useState<string>('locale', () => useDefaultLocale().value)
export const useDefaultLocale = (fallback = 'en-US') => { //默认返回en-US
const locale = ref(fallback)
// 如果是服务端运行
if (process.server) {
// learn more about nuxtApp interface on https://v3.nuxtjs.org/docs/usage/nuxt-app/#nuxtapp-interface-advanced
// 获取Nuxt上下文
const nuxtApp = useNuxtApp()
//Nuxt上下文中获取请求头中的accept-language
const reqLocale = nuxtApp.ssrContext?.req.headers['accept-language'].split(',')[0]
// 如果存在则设置这个为当前语言,不存在则下一个判断
if (reqLocale ) {
locale.value = reqLocale
}
// 如果是在客户端运行
} else if (process.client) {
// 获取浏览器的语言,如果存在则设置当前这个语言
const navLang = navigator.language
if (navLang) {
locale.value = navLang
}
}
// 获取当前语言后返回,如果以上都不存在则返回参数默认值'en-US'
return locale
}
// 抛出一个对象,当前浏览器语言和可供选择的语言
export const useLocales = () => {
const locale = useLocale()
const locales = ref([
'en-US',
'en-GB',
'ko-KR',
'ar-EG',
'fa-IR',
'zh-CN',
])
}
Plugins机制
Nuxt会自动读取plugin目录下的文件并加载他们,在文件名上添加 .server 或 .client 后缀使它仅作用于服务端或客户端
查看实例
创建plugins/test-plugin.ts,引入 defineNuxtPlugin,传一个回调函数进去,这个函数接收唯一参数nuxtApp,nuxtApp是nuxt应用程序的实例,从中获取nuxt暴露的各种接口,包括vue实例,插件会在页面渲染之前注册
import { defineNuxtPlugin } from "nuxt3"
//唯一的参数,nuxt实例
export default defineNuxtPlugin(nuxtApp => {
console.log(nuxtApp);
})
终端和浏览器控制台会打印这个实例对象,从中可以看到各种接口
应用:自动提供帮助方法
一个常见的应用是给NuxtApp实例提供一些额外的帮助方法,可以通过编写一个插件,返回一个对象,在里面设置 provide key,比如:
返回一个对象,provide内注入一个对象,这个对象内有一个函数hello,函数的作用为返回 hello world!
// #app是官方提供的模块,可以从里面导出很多有用的工具函数
import { defineNuxtPlugin } from "#app"
export default defineNuxtPlugin(nuxtApp => {
return {
provide: {
hello: () => {
return 'hello world!'
}
}
}
})
在组件内使用这个helper
const { $hello } = useNuxtApp()
console.log($hello())
获取 useNuxtApp实例后打印可以看到这个方法被注入到了实例上并被添加了$前缀
应用:访问vue实例
如果想要扩展vue,通常要访问到 vue实例,再引入 vue插件,在nuxt3中可以通过 nuxtApp.vueApp.use(xxx) 添加全局插件,扩展vue
这里引入 vue-devui 测试手动导入
npm i vue-devui
创建一个插件 vue-devui-plungin.ts,安装完成之后要按需引入组件
import { defineNuxtPlugin } from "#app"
import { Button } from "vue-devui"
import "vue-devui/button/style.css"
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp
.use(Button)
})
接下来就可以在组件中使用了
<d-button>DevUI</d-button>
扩展
尝试引入其他组件库结果:
-
naiveUI:引入后报错,评论区解决方案禁用vite换webpack
-
vant:可以使用,但样式暂时无法按需引入,import 'vant/lib/button/style/less' 会500报错,编写插件如下
import { defineNuxtPlugin } from "#app" import { Button } from "vant" import "vant/lib/index.css" // 这里 import 'vant/lib/button/style/less' 会500报错 export default defineNuxtPlugin (nuxtApp => { nuxtApp.vueApp.use(Button) } -
element-plus:ssr有问题,仍在解决,只能全量引入,明确暂时不支持自动引入
import { defineNuxtPlugin } from '#app' // 只能全量引入 import ElementPlus from 'element-plus/lib' // 官方正在尝试探索按需导入组件, 但是 @nuxt/components 似乎不支持 components/ComponentA/index.js // https://github.com/nuxt/components#library-authors export default defineNuxtPlugin((nuxtApp) => { nuxtApp.vueApp.use(ElementPlus) }) -
Ant Design:只能全量引入
npm i ant-design-vue@nextimport { defineNuxtPlugin } from '#app' import AntD from "ant-design-vue/lib" import 'ant-design-vue/dist/antd.css'; export default defineNuxtPlugin((nuxtApp) => { nuxtApp.vueApp.use(AntD) })需要注意,如果引入的UI库打开页面500报错,需要在路径后面加 /lib !!!!!!!!!!!!!!!!!!!!!!!
需要注意,如果引入的UI库打开页面500报错,需要在路径后面加 /lib !!!!!!!!!!!!!!!!!!!!!!!
需要注意,如果引入的UI库打开页面500报错,需要在路径后面加 /lib !!!!!!!!!!!!!!!!!!!!!!!
社区模块
介绍
使用plugins机制可以获取Nuxt实例,为项目注入一些全局通用实例方法,同时获取vue实例后引入vue扩展插件,比如引入UI库等
但是如果没做一个项目都要配置所有plugin也很繁琐,所以Nuxt设计了另一个模块机制,实现一组功能的集合
社区模块相当丰富,总共有187个,只是暂时适配nuxt3的仅有6个,如需其他扩展只能使用plugin配置
使用模块演示
使用pinia演示,这是一个项目状态管理的插件
npm i pinia @pinia/nuxt
接下来只需在 nuxt.config.ts 添加模块配置
import { defineNuxtConfig } from 'nuxt3'
export default defineNuxtConfig({
buildModules: [
'@pinia/nuxt',
]
})
创建 stores/users.ts
import { defineStore } from "pinia"
// 第一个参数key必须为整个store的唯一值
export const useUsers = defineStore('users', {
state() {
return {
allUser: 100
}
}
})
组件中使用,先输入useUsers( ),组件就会自动导入,会自动识别这个函数从哪个模块抛出的,与文件名无关,然后输入前面的 const users=
通过 users.allUser 访问,users.$patch( ) 来更改这个对象内对应属性的值
<template>
<div>
<button @click="users.$patch({allUser: users.allUser + 1})">+</button>
<span> [ {{ users.allUser }} ] </span>
<button @click="users.$patch({allUser: users.allUser - 1})">-</button>
</div>
</template>
<script setup>
import { useUsers } from '~~/stores/users';
const users = useUsers()
</script>
\