跳到主要内容

历时两个月!Nuxt3从入门到实战!你值得收藏!

img

杨村长lv-5

2021年12月03日 13:03 · 阅读 22855

关注

历时两个月!Nuxt3从入门到实战!你值得收藏!

前言

大家好,我是村长,下面这篇长文是我历时1个多月做的 Nuxt3学习成果的输出,由于精力和水平有限,难免有错漏,欢迎大家指正。后续我还有继续更新该系列的计划和想法,希望大家多多点赞支持一下,谢谢各位朋友啦! 欢迎关注我的公众号「村长学前端」,新视频和文章会第一时间在公众号发送,另外大家可以输入“加群”和200+前端大咖讨论学习!

小右13号在微博给 Nuxt 3带货了:

img

23号的直播分享再一次给 Nuxt 3带货,这次独占一页PPT:

image-2021102110658127

看来这个 Nuxt3一定不简单!那这个框架是做啥的呢?村长就带大家来一探究竟!

字面意思看是一款基于 Vue3的混合开发框架。

image-20211023220905838

那什么是混合(Hybrid)开发呢?继续看官方介绍,Hybrid状态还是soon,表示增量的静态生成ISG以及一些其他可能的高级模式,静态生成Nuxt2就有,但是每次都全量更新整个网站肯定是不好的,这个增量更新真的值得期待!下面的很多新特性:整合vite + vue3 + composition api + ts、CLI、DevTools、Nuxt Kit表明,这是一个体系完备的通用开发框架,能提供良好的代码组织、极高的开发效率、开发体验和服务端渲染/静态网站生成(SSR/SSG)能力,这才是硬核!

image-20211023221902922

配套视频

我专门录制了Nuxt3从入门到实战系列视频,爱看视频学习的小伙伴千万不要错过!

space.bilibili.com/480140591/c…

尝鲜Nuxt3,尤雨溪力荐的框架怎么样?

创建项目

打开 Visual Studio Code , 打开内置终端并输入下面命令创建一个 nuxt项目:

npx nuxi init nuxt3-app

踩坑指南:node版本需要高于v14.16.0

安装依赖

yarn install

启动

使用 yarn dev以 开发模式启动nuxt:

yarn dev

✨浏览器会自动打开:http://localhost:3000

image-20211023235415460

最小应用

在nuxt3中如果没有 pages/目录,则表示不会包含vue-router依赖。如果我们不需要路由或者就是一个落地页时就可以这么搞。

下面删除app.vue中 <NuxtWelcome />,随便添点内容看看效果:

<template>
<div>
<h1>nuxt3 app</h1>
</div>
</template>

image-20211024003609545

下面我们加个页面试试,创建layouts/index.vue:

<template>
<div>index page</div>
</template>

别忘了添加路由出口,app.vue:

<template>
<div>
<h1>nuxt3 app</h1>
<!-- 路由出口 -->
<NuxtPage></NuxtPage>
</div>
</template>

那如果我有另一个页面 detail.vue想要跳转过去哪?

可以像下面这样,添加一个 NuxtLink,index.vue:

<template>
<div>index page</div>
<!--跳转链接-->
<NuxtLink to="/detail">Detail Page</NuxtLink>
</template>

现在可以自由的跳转了!

img

这个约定路由用起来可太方便了,但是大家需要知道很多规则才能用好,比如:

  • 嵌套路由怎么搞?
  • 动态路由怎么搞?

等等问题,下面让村长给大家一一道来!快给我点个赞吧这会让我更新的更快!😊

约定路由,用起来可真爽啊!

前面写了最小nuxt3应用,我们试用了一下多页面写法,关于是否引入vue-router,nuxt3的行为是:如果只有app.vue不创建pages目录,将不会引入vue-router,则打包体积更小,反之则引入路由库,相当智能吧!

页面路径

nuxt3会自动整合vue-router,并且映射 pages/目录到应用的routes配置中。就像上一讲演示的 index.vuedetail.vue,它们在最终生成的路由配置表中大概是下面这样:

[
{
path: '/',
component: '~/pages/index.vue',
name: 'index',
},
{
path: '/detail',
component: '~/pages/detail.vue',
name: 'detail',
}
]

动态路由

如果我们在文件名或者文件夹名称里面包含了 方括号,它们将被转换为 动态路由参数。

比如下面这样的文件结构:

-| pages/
---| users-[group]/
-----| [id].vue

上面案例我们可以在组件 [id].vue中访问 groupid这两个参数:

<template>
{{ $route.params.group }}
{{ $route.params.id }}
</template>

通过 /users-admins/123 导航即可:

<NuxtLink to="/users-admins/123">管理员123</NuxtLink>

嵌套路由

目录和文件同名,就制造了嵌套路由。

比如下面目录结构:

-| pages/
---| parent/
------| child.vue
---| parent.vue

child.vue

<template>
<div>
<h1>child page</h1>
</div>
</template>

父组件中使用NuxtChild组件显示嵌套子组件内容,parent.vue:

<template>
<div>
<h1>parent page</h1>
<!-- 子组件出口 -->
<NuxtChild></NuxtChild>
</div>
</template>

试一下,index.vue

<NuxtLink to="/parent/child">Parent</NuxtLink>

产生的路由会像下面这样:

{
path: '/parent',
children: [
{
path: 'child'
}
]
}

那如果只使用 /parent会怎么样?发现内容没有了,显然需要一个 {path: '/parent/'}子路由

解决方法也很简单,在 parent/目录下加一个 index.vue即可。

原理

动态路由的原理可以简单在 .nuxt目录中一探究竟

有时候需要在不同页面间共用模板页:这就要用到模板功能,接下来将带大家一起看看怎么做到这一点。

写的头晕,大家快给村长点个赞吧,这会让我快速缓解😊!

巧用布局模板,高效开发从这里开始!

页面布局

前面我们试用了两个重要功能:动态路由嵌套路由

体验便捷的同时,当然也会有另一些重要需求,比如:别名、重定向和路由守卫等,我考查了以下v2中的可用方案:

经测试已经全部失效了!所以小伙伴们等以上两个扩展更新: )

接下来我们研究一下nuxt的布局系统,通过这个自定义的布局页,我们可以提取一些通用UI或代码到可重用的布局组件中,非常便捷,下面我们开始吧!

默认布局

那些放在 layouts/目录下的SFC会被自动加载进来,如果我们创建的SFC名为 default.vue,将会被用于项目所有页面中作为布局模板。

layouts/default.vue:

<template>
<div>
通用布局页,default.vue:
<slot />
</div>
</template>

效果如下:可见还是嵌套在app.vue中

image-20211107104358408

自定义布局文件

如果我们的布局文件名不叫default,而是别的,比如 custom.vue,想要使用它们,就必须在某个页面中设置页面属性 layout

custom.vue:

<template>
<div>
内容来自自定义布局页custom.vue!
<slot />
</div>
</template>

可以在helloworld.vue中试试custom这个布局,helloworld.vue:

<script>
export default {
layout: "custom"
}
</script>

试了一下,嵌套路由中是没有效果的,这可能是有意为之的

使用NuxtLayout

可以使用NuxtLayout组件结合slots获得完全控制力,同时需要设置组件选项 layout: false

helloworld.vue

<template>
<NuxtLayout name="custom">
<template #header>
<h1>hello page</h1>
</template>
some content...
</NuxtLayout>
</template>
<script>
export default {
layout: false,
};
</script>

修改一下custom.vue

<template>
<div>
内容来自自定义布局页custom.vue!
<slot name="header"/>
<slot />
</div>
</template>

我们甚至能组合多个布局页:

<template>
<div>
<NuxtLayout name="custom">
<template #header>
<h1>hello page</h1>
</template>
some content...
</NuxtLayout>

<NuxtLayout name="default">
some content...
</NuxtLayout>
</div>
</template>

配合由于需要设置layout选项,所以在这个script标签旁边同时使用 <script> export default { layout: "custom", }; </script> <script setup> // your setup script </script>

组件自动导入,用就完了!开发体验杠杠的

组件 Components

我估计大家应该挺烦每次使用组件时的各种导入和注册操作,这点nuxt中早就解决了,用就完了!

组件自动导入,开发体验杠杠的!

自动导入组件

我们把Vue组件放在 components/目录,这些组件可以被用在页面和其他组件中,以往我们使用这些组件需要导入并注册它们,但Nuxt会自动导入 components/目录中的任意组件。比如:

| components/
--| TheHeader.vue
--| TheFooter.vue

layouts/default.vue:

<template>
<div>
<TheHeader />
<slot />
<TheFooter />
</div>
</template>

组件名称约定

没有嵌套的组件会以文件名直接导入,但如果存在嵌套关系哪?例如下面的路径:

| components/
--| base/
----| foo/
------| Button.vue

那么组件名称将会基于路径和文件名连起来,比如上面的 base/foo/Button.vue注册名称将会是 BaseFooButton,将来用起来会像下面这样:

<BaseFooButton />

组件懒加载

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

前面都是nuxt3架构上解决的一些问题相关,下面我们开始演示nuxt3在业务编写方面的功能,比如数据获取、可复用业务逻辑、状态管理等等。写的手都酸了,大家给点点赞鼓励一下村长呗!

数据获取,就是这么轻松惬意

上一篇写了组件自动导入,通过 nuxt的组件系统,我们使用组件时不需要导入和注册组件,用就完了,开发体验杠杠的!但我突然想到,我们平时开发还需要用到第三方组件库,那么组件库能自动导入吗?

相关的文档参见:v3.nuxtjs.org/docs/direct…

所以如果严格按照 nuxt要求定义我们组件库并添加 nuxt.js文件,自动引入自然没有问题。然而在我尝试的两个组件库:vue-devuinaive-ui中,都不是很贴合它的要求,所以均以失败告终!希望有识之士可以折腾一下,如果你有解决方案,还请不吝赐教!

那么如何引入它们哪?其实通过插件体系获取 app实例就能手动注册,所以这并不是大问题。

本篇我们写一写组件逻辑,首先想到的是获取数据。

nuxt3中提供的数据获取函数有以下四个:

  • useFetch
  • useLazyFetch
  • useAsyncData
  • useLazyAsyncData

注意:它们都必须在 setup生命周期钩子中使用

useAsyncData

在页面、组件或插件中都可以使用 useAsyncData获取那些异步数据。比如:

const {
data: Ref<DataT>, // 返回的数据
pending: Ref<boolean>, // 加载状态指示器
refresh: (force?: boolean) => Promise<void>, // 强制刷新函数
error?: any // 请求失败的错误信息
} = useAsyncData(
key: string,// 唯一键用于多次请求结果去重
fn: () => Object,// 返回数值的异步函数
// lazy - 是否在路由之后才请求数据,server - 是否在服务端请求数据
options?: { lazy: boolean, server: boolean }
)

获取待办事项数据,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 lang="ts">
const { data: todos } = await useAsyncData(
'count', () => $fetch('/api/todos'))
</script>

$fetch使用参考ohmyfetch

useLazyAsyncData

该方法等效于 useAsyncData,仅仅设置了 lazy选项为true,也就是它不会阻塞路由导航,这意味着我们需要处理data为null的情况(或者通过default选购给data设置一个默认值)

useFetch

页面、组件或者插件中可以使用 useFetch获取任意URL资源。

useFetch是对 useAsyncData包装,自动生成key同时推断响应类型,用起来更简单。

看下面方法签名,基本完全相同:

const {
data: Ref<DataT>,
pending: Ref<boolean>,
refresh: (force?: boolean) => Promise<void>,
error?: any
} = useFetch(url: string, options?)

useLazyFetch

该方法等效于 useFetch,仅设置了 lazy选项为true,所以它不会阻塞路由导航,这意味着我们需要处理data为null的情况(或者通过default选购给data设置一个默认值)

最佳实践

只选取需要的数据

由于请求回来的数据会存储在页面payload中,甚至包括那些没有用到的字段,所以文档中明确建议大家只选择那些组件用到的数据,我们可以设置$fetch的 pick选项。

比如,下面的用法:

const { data: mountain } = await useFetch('/api/mountains/everest', { 
pick: ['title', 'description']
})

创建SSR友好的跨组件共享状态

上一篇写了数据获取的各种姿势,最终大意了没有闪,以至于 pick部分举例欠妥:

const { data: todos } = await useFetch("/api/todo", {
pick: ["id", "title", "completed"],
});

导致了 500错误,这里需要说明的是,pick期望的数据结构应该是对象,而我提供的是数组:

const todos = [
{ id: 1, title: 'nuxt3', completed: false },
{ id: 2, title: 'vue3', completed: true },
]
export default () => {
return todos
}

所以如果修改为对象:

export default () => {
return {
code: 1,
data: todos
}
}

则可以正常 pick

const { data: todos } = await useFetch("/api/todo", {
pick: ['data']
});

pick的行为是,摘出来这些部分做个新对象:

// 型如 { data: todos }
// 所以我之前代码:const { data: todos } = ...
// todos并非是数组而是{ data: todos }

所以页面中使用也要对应:

<div v-for="todo in todos.data" :key="todo.id">

有人问如果有层级pick需求该当如何?我估计pick选项没这个能力。如果想要对数据做强力控制,还得是 transform

const { data: todos } = await useFetch("/api/todo", {
transform(input) {
return input.data;
},
});

此时页面也可以直接使用 todos了!

状态共享

Nuxt3提供了 useState 创建响应式且服务端友好的跨组件状态共享能力。

useState 是服务端友好的 ref替换。它的值在服务端渲染(客户端注水的过程中)将被保留并通过唯一key在组件间共享。

方法签名

useState<T>(key: string, init?: () => T): Ref<T>

  • key:唯一键用于去重
  • init:提供初始值的函数

useState实践

声明一个状态,index.vue

const counter = useState("counter", () => Math.round(Math.random() * 1000))

<button @click="counter++">+</button>
{{ counter }}
<button @click="counter--">-</button>

共享状态

我们的全局状态当然想要在组件之间共享,此时可以利用nuxt的composables自动导入特性,把它们定义在composables目录中,这样他们将成为全局类型安全的状态。

composables/useCounter.ts

export const useCounter = () =>
useState("counter", () => Math.round(Math.random() * 1000));

配套视频

我专门录制了Nuxt3从入门到实战系列视频,爱看视频学习的小伙伴不要错过!

space.bilibili.com/480140591/c…

小伙们别忘了动动小手,三连一波鼓励一下村长啊!

有了数据和状态,下一步是进一步强化框架的能力,nuxt中有 pluginsmodules两个重要的扩展方式,下面我会带大家一起研究一下,手又酸了,大家点个关注心理按摩一下☺️~

巧用插件机制,强化Nuxt的利器!

上一篇写了nuxt状态共享,本篇我们研究nuxt3的插件系统。通过插件系统,我们可以获取nuxt实例以及vue实例,这样我们有机会扩展nuxt或vue,比如引入一个UI库。

plugins目录

Nuxt3会自动读取plugins目录下的文件并加载它们。我们可以在文件名上使用.server或者.client前缀使他们仅作用域服务端或者客户端。

创建插件

使用defineNuxtPlugin()注册一个插件:

import { defineNuxtPlugin } from '#app'
// 唯一的参数是nuxt实例
export default defineNuxtPlugin(nuxtApp => {
// Doing something with nuxtApp
})

插件用例:自动提供帮助方法

一个常见应用是给NuxtApp实例提供一些额外的帮助方法,我们可以通过编写一个插件,返回一个对象,在里面设置 providekey,比如:

import { defineNuxtPlugin } from '#app'

export default defineNuxtPlugin(() => {
return {
provide: {
hello: () => 'world'
}
}
})

使用这个helper,index.vue:

// 会自动加上$前缀
const { $hello } = useNuxtApp();
console.log($hello())

插件用例:访问Vue实例

如果想要扩展Vue,我们通常要访问Vue实例,引入Vue插件。在nuxt3中可以通过插件访问 nuxtApp.vueApp.use(xxx)做到。

我们引入 vue-devui试一下,前面我们曾经试图自动导入失败了,这里我们手动导入:

npm i vue-devui

创建一个插件vue-devui-plugin.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>按钮</d-button>

扩展

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

  • naive-ui按官方方式在SFC中直接使用组件 引入就报错,参见:github.com/TuSimple/na… 又找到了这个回答:github.com/TuSimple/na… 不过这个解决方案关闭了 vite,不是我喜欢的风格,仅供大家可以参考!
import { NButton } from 'naive-ui'

<n-button>button</n-button>

  • vant是可以的,不过暂时不知道样式如何按需引入,编写如下插件: 可以看一下有赞官方有计划做一个next3+vant的demo
import { defineNuxtPlugin } from "#app";
import { Button } from 'vant';
import 'vant/lib/index.css'
// 这里如果引入 vant/lib/button/index.css是没有效果的
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(Button)
});

  • element-plus官方有个starter项目:

    github.com/element-plu…

    只可惜也是全量引入,按需引入没交代,也明确了自动引入暂时支持不了。

实在写不动了,大家三连鼓励一下村长吧~下次一定继续更新!