Nuxt3笔记

2,854 阅读13分钟

Nuxt3介绍

Nuxt 3 - The Hybrid Vue Framework (nuxtjs.org)

Home | Nuxt3中文文档 (57code.github.io)

image-20211228141454184

Nuxt是一个基于Vue3的混合开发框架,下面特性表明,这是一个体系完备的通用开发框架,他能提供良好的代码组织和服务端渲染/静态网站生成(SSR/SSG)能力

image-20220106123121754

安装

确保安装了 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

项目目录

项目目录只有这些

image-20211228161334348

路由&视图

约定路由

运行服务器

 npm run dev

image-20211228164039291

根目录已经存在了一个入口文件app.vue,按照下方提示,创建一个pages/index.vue来开始项目

image-20211228163816507

为什么说nuxt是一个开发起来很简洁的框架,因为所有的东西都基于配置,如果只有一个页面那就是app.vue,如果有很多页面,需要路由导航,根据约定,根目录创建一个pages,所有的单个页面都放在这里,创建pages/index.vue后,终端显示了更新,页面显示404

image-20211228164821319

显然,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会被识别为 / 路由,如果是其他组件名,则是被注册为同名路由

image-20211228171902402

接下来尝试一下添加一个子路由组件

image-20211228185511962

index.vue添加路由跳转链接

 <template>
   <div>
     <h3>Index Page</h3>
     <NuxtLink to="/detail">Detail Page</NuxtLink>
   </div>
 </template>

路由生效了

image-20211228185608234image-20211228185623866

上面初步的了解,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')
   },
 ]

动态路由

如果需要传参使用动态路由,可以在文件夹或者组件名上添加方括号

image-20211229114452920

[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,但相比原生还是非常简便的)

image-20211229115211584image-20211229115235783

生成的路由是这样的

	{
    "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')
	}

嵌套路由

文件夹和组件同名时,就会被注册为嵌套路由

image-20211229120152196

// 上级路由
<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>

添加链接,点击后成功跳转

image-20211229121801778image-20211229122244716

如果只访问/parent,会发现child内容没有了,导致父页面内容缺失,如需要显示child缺失的默认子组件的路由,可以在parent目录下新建一个index.vue

<template>
  <div>
    <h3>Parent/Index Page</h3>
  </div>
</template>

访问/parent路由访问到了默认子路由内容 ( 啊对,又重启服务器了

image-20211229153806391

产生的路由会是这样

{
    "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,当前文档搜索这个id,当前文档搜索这个id

image-20211229233743577

第二个结果,就是nuxt动态生成的路由配置

image-20211229234022570

问题

以上配置不能实现的方面,比如别名、重定向、路由守卫等需求,在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的结合有问题,等待后续修复后面笔记的内容但凡修改没有动态生效,都重启服务器不在重复说明

image-20211230001349852image-20211230001544991

给这组件内元素加类名打上标记,可以看到,这个组件实际上会作为app.vue子元素,替换掉NuxtPage的位置,然后将NuxtPage放入default.vue的slot插槽内

image-20211230001840118

自定义布局

如果需要自定义布局,就不能使用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的路由链接,点击跳转,可以看到布局组件变了

image-20211230004119128image-20211230004136019

使用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>

image-20211230010308952

甚至 NuxtLayout 可以不止一个,对页面进行任意的组合控制

配合

就像上面的备注一样,想要使用

<script>
export default {
  layout: false
}
</script>

<script setup>
// 使用setup语法糖就另起一个script标签

</script>

组件导入

组件自动导入

在使用vue2开发的时候,总是会遇到先import引入组件,然后components注册组件,再到template中使用,vue3帮我们省去了注册组件,但是如果一个页面引入的子组件过多,数排的import也是感觉碍眼,

这一点,现在也给解决了,把组件放入components目录,nuxt会帮助自动导入组件,所有的组件声明完成直接使用,不再需要import,有效提高开发体验

首先创建两个组件

image-20220101002005805

然后打开父组件default.vue,直接使用

<template>
  <div class="layouts">
    <TheHeader/>
    通用布局页,default.vue:
    <slot></slot>
    <TheFooter/>
  </div>
</template>

可以看到页面上已经生效了

image-20220101002421221

组件名称约定

没有嵌套的组件会以原文件名自动导入,使用的时候要与组件名写法一致,不可以换成-分隔符写法

如果是文件夹嵌套的话,组件名将会基于路径和文件名的拼合注册到全局,比如base/foo/Button.vue,注册后的组件名会是BaseFooButton

image-20220101004012865image-20220101004245248

组件懒加载

如果在组件名前面加上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

可以看到这个组件被单独打包了出回来

image-20220101010421228

数据获取

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的渲染

image-20220103224107781

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);
})

终端和浏览器控制台会打印这个实例对象,从中可以看到各种接口

image-20220105164714420

应用:自动提供帮助方法

一个常见的应用是给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())

image-20220105170703581

获取 useNuxtApp实例后打印可以看到这个方法被注入到了实例上并被添加了$前缀

image-20220105171153639

应用:访问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>

image-20220105192452137

扩展

尝试引入其他组件库结果:

  • 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)
    }
    

    vant-nuxt3-demo (github.com)

  • 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)
    })
    

    element-plus-nuxt-starter(github.com)

  • Ant Design:只能全量引入

    npm i ant-design-vue@next
    
    import { 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设计了另一个模块机制,实现一组功能的集合

modules.nuxtjs.org/

image-20220105203818097

社区模块相当丰富,总共有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>

\