nuxt初体验

基础概念

SSR:Server side Render,服务器端渲染。

SPA:Client side Render,客户端渲染。

新建项目

命令:npx nuxi init <project-name>

创建项目报错

新建项目运行报错信息:

Failed to download template from registry: Failed to download https://raw.githubusercontent.com/nuxt/starter/templates/templates/v3.json: TypeError: fetch failed

原因:DNS 对 Nuxt 服务器域名会解析失败

解决办法:在 hosts 文件中直接将主机映射到对应 IP 上。

步骤:

  1. 查询IP 的网址查询 raw.githubusercontent.com 的 IP 地址。

  2. windows 将 上面的信息添加到 hosts 文件中。

    • windows 的路径:C:\Windows\System32\drivers\etc
    • Mac 的路径:/etc/hosts

    185.199.108.133  raw.githubusercontent.com
    # 185.199.109.133 raw.githubusercontent.com
    # 185.199.110.133 raw.githubusercontent.com
    # 185.199.111.133 raw.githubusercontent.com
  3. 重新打开一个终端进行项目创建。如果不成功则注释掉换下一个 ip 映射测试。我这里第三个成功了。

目录结构

  • pages/ – 目录是可选的,

    • 如果不存在,则不包含 vue-router 依赖。

    • 如果存在,请使用 <NuxtPage> 组件

      <template>
      <div>
      <NuxtLayout>
      <NuxtPage/>
      </NuxtLayout>
      </div>
      </template>

      由于 <NuxtPage> 在内部使用的是 Vue 的 <Suspense> 组件,因此无法将其设置为根组件。

内置组件

因为这些组件名称与本机 HTML 元素匹配,所以它们在模板中必须大写。

欢迎页面组件

<NuxtWelcome> 欢迎页面组件,该组件是 @nuxt/ui 的一部分。

页面占位组件

<NuxtPage> 是 Nuxt 自带的页面占位组件。是对 <router-view> 的封装。

页面布局组件

<NuxtLayout> 是 Nuxt 自带的页面布局组件,可以将多个页面共性东西抽取到 Layout 布局中。比如:每个页面的页眉和页脚组件,这些具有共性的组件我们是可以写到一个 Layout 布局中。

<NuxtLayout> 是使用 <slot> 组件来显示页面中的内容。

<NuxtLayout> 有两种使用方式:

  • 默认布局
    • 在 layouts 目录下新建默认的布局组件,比如:layouts/default.vue
    • 然后在 app.vue 中通过 <NuxtLayout> 内置组件来使用。
  • 自定义布局
    • 在 layouts 文件夹下新建 Layout 布局组件,比如: layouts/custom-layout.vue
    • 然后在 app.vue中给 <NuxtLayout> 内置组件指定 name 属性的值为:custom-layout
    • 也支持在页面中使用 definePageMeta 宏函数来指定 layout 布局。

默认布局

目录结构
  • layouts
    • default.vue
  • pages
    • detail.vue
    • index.vue
  • app.vue
layouts/default.vue
<template>
<div>
<div class="base header">header</div>
<slot></slot>
<div class="base footer">footer</div>
</div>
</template>

<style scoped>
.base {
width: 100%;
height: 80px;
line-height: 80px;
text-align: center;
font-size: 20px;
}
.header { background-color: red; }
.footer { background-color: skyblue; }
</style>
app.vue
<template>
<NuxtLayout>
<div>
<NuxtLink to="/">
<button>Home</button>
</NuxtLink>
<NuxtLink to="/detail">
<button>detail</button>
</NuxtLink>
</div>
<NuxtPage></NuxtPage>
</NuxtLayout>
</template>

<style>
a { margin-right: 10px; }
button { cursor: pointer; }
.router-link-active button { color: red; }
</style>
pages/index.vue
<template>
<div>Page: index</div>
</template>
pages/detail.vue
<template> <div>Page: detail</div> </template>

自定义布局

目录结构
  • layouts
    • custom.vue
    • default.vue
  • pages
    • detail.vue
    • index.vue
    • login.vue
  • app.vue

pages/detail.vue 和 pages/index.vue 内容和上面的默认布局内容一样

layouts/custom.vue
<template>
<div> <slot></slot> </div>
</template>
layouts/default.vue
<template>
<div>
<div class="base header">
<div class="header-content">
<h1>header</h1>
<div>
<NuxtLink to="/"><button>Home</button></NuxtLink>
<NuxtLink to="/detail"><button>detail</button></NuxtLink>
</div>
<div class="login-btn" @click="loginHandler">登录</div>
</div>
</div>
<slot></slot>
<div class="base footer">footer</div>
</div>
</template>

<script setup>
const loginHandler = () => { navigateTo('/login') }
</script>

<style scoped>
.base {
width: 100%;
height: 80px;
line-height: 80px;
text-align: center;
font-size: 20px;
}
.header { background-color: red; }
.header-content {
margin: 0 auto;
width: 1200px;
height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-content .login-btn {
cursor: pointer;
color: #eee;
}
.header-content .login-btn:hover { color: #333; }
.footer { background-color: skyblue; }
</style>
layouts/login.vue
<script setup>
definePageMeta({ layout: 'custom' })
</script>

<template>
<div>
<div style="margin-bottom: 30px">Page: login</div>
<NuxtLink to="/">go Home page</NuxtLink>
</div>
</template>
app.vue
<template>
<NuxtLayout> <NuxtPage></NuxtPage> </NuxtLayout>
</template>

<style>
a { margin-right: 10px; }
button { cursor: pointer; }
.router-link-active button { color: red; }
</style>

注意

这里将 导航移动到 default 页面是因为,login 页面的内容也是显示到 <NuxtPage> 组件中,如果继续放在这里,那么在 login 页面中也会显示之前的导航信息。

当然也可以不这样,直接判断信息在 app.vue 中隐藏菜单信息。

  • 通过 $route.path === '/login' 来动态显示
  • 通过 $route.meta.layout !== 'custom' 来动态显示

方式一

<template>
<NuxtLayout>
<div :style="{ display: $route.path === '/login' ? 'none' : 'block' }">
<NuxtLink to="/"><button>Home</button></NuxtLink>
<NuxtLink to="/detail"><button>detail</button></NuxtLink>
</div>
<NuxtPage></NuxtPage>
</NuxtLayout>
</template>

<style>
a { margin-right: 10px;}
button { ursor: pointer;
.router-link-active button { olor: red;
</style>

方式二

<template>
<NuxtLayout>
<div v-if="$route.meta.layout !== 'custom'">
<NuxtLink to="/"><button>Home</button></NuxtLink>
<NuxtLink to="/detail"><button>detail</button></NuxtLink>
</div>
<NuxtPage></NuxtPage>
</NuxtLayout>
</template>

<style>
a { margin-right: 10px;}
button { ursor: pointer;
.router-link-active button { olor: red;
</style>

客户端渲染组件

基础

<ClientOnly> 该组件中的默认插槽内容只在客户端渲染

<template>
<div>
<ClientOnly><div>我只会在客户端渲染</div>/ClientOnly>
<div>
<h1>Home Page</h1>
<p>Home page content goes here</p>
</div>
</div>
</template>

fallback 属性

可以给 <ClientOnly> 组件添加 fallback 属性,该属性的内容在组件还未加载出来之前进行占位和提示。

<ClientOnly fallback="正在加载..."><div>我只会在客户端渲染</div></ClientOnly>

fallback-tag 属性

fallback-tag 属性可以修改 fallback 中内容的 HTML 标签。它默认是 <span> 标签,我们可以使用 fallback-tag 来设置其标签。

<ClientOnly fallback-tag="div" fallback="正在加载...">

插槽用法

<ClientOnly> 组件中的默认插槽的内容只会在客户端渲染,而 fallback 插槽中的内容只在服务器端渲染

<ClientOnly>
<div>客户端中显示</div>
<template #fallback>
<div>服务器端中显示</div>
</template>
</ClientOnly>

页面导航组件

<NuxtLink> 是 Nuxt 内置组件,是对 RouterLink 的封装,用来实现页面的导航。

NuxtLink 组件属性:

  • to —- 支持路由路径、路由对象、URL
  • href —- to 的别名
  • active-class —- 激活连接的类名
  • target —- 和 a 标签的 target 一样,指定何种方式显示新页面

外部链接会加上 rel=”noopener noreferrer”,表示不携带 opener 和 referrer 信息。也可以手动指定 external,当然只要是外部连接就会自动添加上 rel=”noopener noreferrer”。

<template>
<div>
<div class="page">
<NuxtLink to="/">首页</NuxtLink>
<NuxtLink to="/home">home</NuxtLink>
<!-- href 是 to 的别名 -->
<NuxtLink href="/detail" replace>detail index 有 replace</NuxtLink>
<NuxtLink to="/detail/2" active-class="active-red">detail [id]</NuxtLink>
<!-- <NuxtLink to="/user-admin?username=123&password=123123">user-[type]</NuxtLink> -->
<NuxtLink :to="{ path: '/user-admin', query: { username: 123, password: 123123 } }">
user-[type]
</NuxtLink>
<!-- 外部链接会加上 rel="noopener noreferrer" 不携带 opener 和 referrer 信息 -->
<!-- 也可以手动指定 external,当然外部连接会自动添加上 rel="noopener noreferrer" -->
<NuxtLink to="https:www.jd.com" external target="_blank">京东</NuxtLink>
</div>
<NuxtPage></NuxtPage>
</div>
</template>
<style>
.page {
display: flex;
gap: 0 20px;
}
.active-red {
color: red;
}
</style>

还可以使用编程导航 navigateTo 函数。通过编程导航可以轻松实现动态导航,但动态导航不利于 SEO。

navigateTo 函数在服务器端和客户端都可以使用,也可以用于插件、中间件等,还可以直接用来执行页面导航。

navigateTo(to, options) 函数:

  • to —- 可以是纯字符串、外部 URL、路由对象。
  • options —- 导航配置,可选。
    • replace —- false(默认)、true,为 true 时替换当前路由页面
    • external —- false(默认)、true,不允许导航到外部链接,true 则允许。
    • redirectCode —- 数字,重定向代码
<template>
<div>
<div class="page">
<button @click="goToPage('/')">首页</button>
<button @click="goToPage('/home')">home</button>
<button
@click="goToPage({ path: '/detail/3', query: { username: 123, password: 123123 } })"
>
detail
</button>
<button @click="goToPage('https://www.jd.com')">京东</button>
</div>
<NuxtPage></NuxtPage>
</div>
</template>

<script setup>
const goToPage = url => {
if (/^http/i.test(url)) {
// 这里不加 external 是跳不过去的
// return navigateTo(url, { external: true })
return navigateTo(url, {
open: {
target: '_blank',
// 窗口特性,设置窗口大小
windowFeatures: {
width: 600,
height: 500
}
}
})
}

navigateTo(url)
}
</script>

<style>
.page {
display: flex;
gap: 0 20px;
}
</style>

abortNavigation 函数

中止导航,并显示可选的错误消息。

abortNavigation(Error | string)

  • Error —- 错误对象

  • string —- 错误字符串

useRouter

除了可以通过 navigateTo 函数来实现编程导航,也可以使用 useRouter ( 或 Options API 的 this.$router )。

  • back —- 页面返回,router.go(-1)
  • forward —- 页面前进,router.go(1)
  • go —- 页面返回或前进
  • push —- 建议用 navigateTo
  • replace — 建议用 navigateTo
  • beforeEach —- 路由守卫钩子,每次导航前执行
  • afterEach —- 路由守卫钩子,每次导航后执行

useRouter 与 navigateTo

假如现在有两个页面 pages/index.vuepages/lazy.vue 我们在 lazy 组件中设置路由跳转到 index 组件中,以此对比 useRouter 和 navigateTo 的区别。

// pages/index.vue
useRouter().push('/lazy') // 页面有很明显的切换过程

// 页面直接在服务端渲染时就切换完毕了,在浏览器中无明显的切换过程
navigateTo('/lazy')

// 下面的 111 会在浏览器中输出
useRouter().push('/lazy')
console.log('111')

// 下面的 111 会在编辑器终端输出
navigateTo('/lazy')
console.log('111')

SEO 组件

<Title>, <Base>, <NoScript>, <Style>, <Meta>, <Link>, <Body>, <Html> and <Head>

<Head><Body> 可以接受嵌套的 Meta 标记(出于美观原因),但这不会影响嵌套的 Meta 标记在最终 HTML 中的呈现位置

nuxt.config.js

关闭 ssr

ssr: false 关闭 SSR,开启 SPA。值:false/true,true 为默认值。

运行时配置

// nuxt.config.ts
runtimeConfig: {
count: 1, // 只能在服务端访问
public: { // 两端都可以访问
baseURL: 'localhost:8080'
}
}

// app.vue
const runtimeConfig = useRuntimeConfig()
console.log(runtimeConfig.count)
console.log(runtimeConfig.public.baseURL)

路由为哈希模式

服务器的路由只能是 history 模式,客户端的路由可以是 history 或 hash 模式。

若需要将路由模式切换为 hash 模式,只能在 SPA 模式下才能启用哈希模式,也就是说需要将 SSR 关闭后再进行设置。启动后,URL 永远不会发送到服务器,并且不支持 SSR。

解释一下 为什么说URL永远不会发送到服务器,并且不支持SSR

总结:SSR 的核心是 在服务器生成完整的 HTML 内容,如果使用 Hash 模式则无法知道客户端实际需要渲染的路由。因为在 Hash 模式下,URL 的路径会被 # 符号分隔,# 前的部分会发送到服务器,# 后的部分会留在客户端。这会导致服务器永远接收的都是根路径,无法知道前端具体的路由,导致无法正确进行服务端渲染。

# 符号最初设计用于 页面内锚点跳转,前端框架通过监听 hashchange 事件动态更新页面内容,无需像服务器发起新请求。

export default defineNuxtConfig({
ssr: false, // 关闭 SSR,开启 SPA
router: {
options: {
hashMode: true // 路由为 hash 模式
}
}
})

runtimeConfig vs app.config

它们都用于应用程序公开变量。

  • runtimeConfig:定义环境变量。运行时需要指定的私有或公有 token。
  • app.config:定义公共变量。在构建时确定的公共 token、网站配置。

全局样式

  1. 在 app.vue 文件中编写
  2. 在外部文件中编写(推荐)
    1. 在 assets/style 中编写全局样式,如 global.css。
    2. 去 nuxt.config 中的 css 选型中进行配置
// assets/styles/global.css -- 全局样式
body {
background-color: pink;
}

// nuxt.config.js -- css 配置
export default defineNuxtConfig({
css: ['@/assets/styles/global.css']
})

也可以使用 SCSS。

// assets/styles/main.scss
$primary-bg: skyblue;
$primary-font: 20px;
// app.vue
<template>
<div class="page">
<h1>Home Page</h1>
<p>Home page content goes here</p>
</div>
</template>

<style lang="scss">
@use '@/assets/styles/main.scss' as main;

.page {
background-color: main.$primary-bg;
font-size: main.$primary-font;
}
</style>
// nuxt.config.js -- css 配置
export default defineNuxtConfig({
css: ['@/assets/styles/global.css', '@/assets/styles/main.scss']
})

上面的是手动导入 scss 文件,我们可以配置使其自动导入。

修改 app.vue 和 nuxt.config.js 的代码。

// app.vue
<template>
<div class="page">
<h1>Home Page</h1>
<p>Home page content goes here</p>
</div>
</template>

<style lang="scss">
.page {
background-color: $primary-bg;
font-size: $primary-font;
}
</style>
// nuxt.config.js -- css 配置
export default defineNuxtConfig({
css: ['@/assets/styles/global.css'],
vite: {
css: {
preprocessorOptions: {
scss: {
additionalData: '@use "@/assets/styles/main.scss" as *;' // 这里配置后就不需要在上面的 css 选项中再配置了
}
}
}
}
})

这种就会在 .scss 和 使用了 lange=”scss” 的文件的首行自动添加导入设置的 .scss 文件配置。

判断目前是什么端

方法一

nuxt.config.ts 中的 runtimeConfig 中随便写点代码,然后去控制台输出,看哪个端的控制端有内容。

// nuxt.config.ts
runtimeConfig: {
isServer: true
}

// app.vue
const runtimeConfig = useRuntimeConfig()
if (runtimeConfig.isServer) {
console.log('server 端')
}

方法二

// app.vue
if (import.meta.server) {
console.log('服务端')
} else {
console.log('客户端')
}

资源导入

图片资源

public 目录:静态资源的公共服务器,可以直接通过 URL 访问。如:/images/xx.png

assets 目录: 存放样式表、字体等,可以使用 @/assets 等路径访问目录中的内容。

<template>
<div class="page">
<div>
<h2>public</h2>
<div>
<h3>通过img标签引入</h3>
<img src="/public/星空.jpg" alt="" />
<img src="@/public/星空.jpg" alt="" />
</div>
<div>
<h3>通过背景引入</h3>
<div class="public-bg public-bg1"></div>
<div class="public-bg public-bg2"></div>
</div>
</div>
<div>
<h2>assets</h2>
<div>
<h3>通过img标签引入</h3>
<img src="@/assets/images/R-C.jpg" alt="" />
</div>
<div>
<h3>通过背景引入</h3>
<div class="assets-bg"></div>
</div>
</div>
</div>
</template>

<style>
.page {
display: flex;
gap: 0 50px;
}
img {
margin: 10px;
width: 200px;
}
.public-bg {
display: inline-block;
margin: 10px;
width: 200px;
height: 110px;
background-size: contain;
}
.public-bg1 {
background-image: url(/public/星空.jpg);
}
.public-bg2 {
background-image: url(@/public/星空.jpg);
}
.assets-bg {
margin: 10px;
width: 200px;
height: 110px;
background-image: url(@/assets/images/R-C.jpg);
background-size: contain;
}
</style>

字体图标

可以将字体图标文件放在 assets 目录中,通过 @/assets 等路径进行获取。之后在 nuxt.config.js 的 css 选项中配置让其全局生效。

// nuxt.config.js -- css 配置
export default defineNuxtConfig({
css: ['@/assets/iconfont/iconfont.css'], // 字体图标
})

新建页面

nuxt 项目的页面在 pages 目录下创建。nuxt 会根据该目录的目录结构和其文件名自动生成对应的路由。

可以使用命令来便捷创建页面。

  • npx nuxi add page xxx —- 创建页面
  • npx nuxi add page xxx/xx —- 创建xxx目录并在内部创建xx页面
  • npx nuxi add page xxx/[id] —- 创建xxx目录并在内部创建动态路由页面

路由

动态路由

动态路由也是根据目录结构和文件的名称自动生成。

动态路由语法:页面组件目录 或 页面组件文件都 支持 [ ] 方括号语法,[ ] 方括号中编写动态路由参数。

例如:

  • pages/detail/[id].vue —- /detail/:id
  • pages/detail/user-[type].vue —- /detail/user-:type
  • pages/detail/[role]/[id].vue —- /detail/:role/:id
  • pages/detail-[role]/[id].vue —- /detail-:role/:id

动态路由和 index.vue 可以同时存在。

可选路由

文件夹使用双括号 [[]] 命名,支持使用或不使用。

pages/[[test]]/testRoute.vue,这里浏览器访问:http://localhost:3000/test/testRoutehttp://localhost:3000/testRoute 都可以。

路由参数

通过 [ ] 方括号语法定义的动态路由,如目录结构为 pages/detail/[id].vue,可以 URL 路径传递动态路由参数 /detail/12?name=zhangsan,在 [id].vue 中通过 $route.params.id$route.query.name 获取,也可以使用 useRoute 函数。

全局路由(404 page)

用户可能会手动修改 URL 或 我们忘记创建某个页面,导致页面跳转出错,虽然 Nuxt 给我们定制了一个 404 页面,但不符合每个项目的需求,可能需要自定义。

我们可以通过 [ ] 方括号,并在内加入三个点,如:[…slug].vue。其中的 slug 可以是其它的字符串。

我们可以在 pages 根目录下创建该页面,也可以在任意子目录下创建该页面。需要注意的是子目录下的是该目录下输入的 URL 不存在则使用该目录下的 404 页面;pages 根目录下的是捕获全局的错误 URL。

路由匹配机制

预定义路由优先于动态路由,动态路由优先于捕获所有路由。先找预定义路由,后动态路由,最后捕获所有路由(404页面)。

  • 预定路由:pages/detail/create.vue
    • 匹配 /detail/create
  • 动态路由:pages/detail/[id].vue
    • 匹配 /detail/1、/detail/abc、…
    • 不匹配 /detail/、/detail/create、/detail/1/1、…
  • 捕获所有路由:pages/detail/[…slug].vue
    • 匹配 /detail/1/1、/detail/a/b、…
    • 不匹配 /detail/、…

嵌套路由

嵌套路由也是根据目录结构和文件的名称自动生成。

步骤

  1. 在 pages 目录下创建一个一级路由,如:parent.vue 文件。
  2. 在 pages 目录下创建一个一级路由同名的文件夹,如:parent 文件夹。
  3. 二级路由在 parent 文件夹中创建即可。
  4. 在 app.vue 和 parent.vue 文件中编写页面占位,使用 <NuxtLink>
  5. app.vue 中的页面占位是为了显示 parent.vue 等一级路由;parent.vue 中的页面占位是为了显示 parent 目录下的二级路由。

简单目录结构

  • pages
    • parent
      • child1.vue —- 二级路由,child1 页面
      • child2.vue —- 二级路由,child2 页面
      • index.vue —- 二级路由默认页面
    • index.vue —- 一级路由默认页面
    • parent.vue —- 一级路由,parent 页面
  • app.vue

代码编写

app.vue
<template>
<div>
<div>
<NuxtLink to="/" :active-class="$route.path === '/' ? 'active' : ''">
<button>Home</button>
</NuxtLink>
<NuxtLink to="/parent" :active-class="$route.path.includes('parent') ? 'active' : ''">
<button>Parent</button>
</NuxtLink>
</div>
<NuxtPage></NuxtPage>
</div>
</template>

<style>
a {
margin-right: 10px;
}
button {
cursor: pointer;
}
.router-link-active.router-link-exact-active button,
.active button {
color: red;
}
</style>
pages/index.vue
<template>
<div>
<div>Page: index</div>
</div>
</template>
pages/parent.vue
<template>
<div>
<div>Page: parent</div>
<div>
<NuxtLink to="/parent">
<button>parent home</button>
</NuxtLink>
<NuxtLink to="/parent/child1">
<button>parent child1</button>
</NuxtLink>
<NuxtLink to="/parent/child2">
<button>parent child2</button>
</NuxtLink>
</div>
<NuxtPage></NuxtPage>
</div>
</template>
pages/parent/index.vue
<template>
<div>
<div>Page: parent/index</div>
</div>
</template>
pages/parent/child1.vue
<template>
<div>
Page: parent/child1
</div>
</template>
pages/parent/child2.vue
<template>
<div>
Page: parent/child2
</div>
</template>

路由中间件

概念

Nuxt 提供了一个可定制的 路由中间件,可以在整个应用程序中使用,非常适合在导航到特定路由之前提取我们想要运行的代码。用来监听路由的导航,包括:局部和全局监听(支持在服务器和客户端执行)。

路由中间件分三类:

  • 匿名(或内联)路由中间件:在页面中使用 definePageMeta 函数定义,可监听局部路由。当注册多个中间件时,会按照注册顺序来执行。
  • 命名路由中间件:在 middleware 目录下定义,并且会自动加载中间件。命名通常使用短横线。也需要在 definePageMeta 函数定义。
  • 全局路由中间件:在 middleware 目录中,需要带 .global 后缀的文件,每次路由更改会自动运行。

全局路由中间件的优先级最高,支持服务端和客户端。

路由中间件执行顺序:

  1. 全局路由中间件
  2. 页面定义的路由中间件顺序(多个路由中间件使用数组语法)

如果有多个全局路由中间件,则根据目录的顺序依次执行。

若想手动指定全局路由中间件的执行顺序,则可以在文件名前面加上字母编号。

注意:文件名是按照字母排序的,而不是按照数值排序。如:10.new.global.ts 将位于 2.new.global.ts 之前。

匿名(或内联)路由中间件

匿名(或内联)路由中间件,只能用在单文件中,如:index.vue、parent.vue 等文件中。不能用在 app.vue 中。

<!-- pages/index.vue -->
<script setup>
definePageMeta({
middleware: [
function (to, from) {
console.log('我是匿名中间件')
}
]
})
</script>

<template>
<div>
<div>Page: index</div>
</div>
</template>

命名路由中间件

middleware/home.ts
export default defineNuxtRouteMiddleware((to, from) => {
console.log('我是命名中间件,是第一个中间件')
})

export default defineNuxtRouteMiddleware((to, from) => {
if (to.path === '/about') {
return navigateTo('/login')
}
})
pages/index.vue
<script setup>
definePageMeta({
// middleware: ['home'] // 单个
// middleware: 'home' // 单个
middleware: [ // 多个
'home',
function (to, from) {
console.log('我是匿名中间件,也是第二个中间件')
}
]
})
</script>

<template>
<div>
<div>Page: index</div>
</div>
</template>

全局路由中间件

// middleware/main.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
console.log('我是全局中间件,是优先级最高的中间件')
})

路由验证(validate)

Nuxt支持对每个页面路由进行验证,我们可以通过 definePageMeta 中的 validate 属性来对路由进行验证。

validate 属性接受一个回调函数,回调函数中以 route 作为参数。

回调函数的返回值支持:

  • 返回 boolean 值来确定是否放行路由
    • true 放行路由
    • false 默认重定向到内置的 404 页面。可以自定义错误页面,在项目根目录(不是pages目录)新建 error.vue
  • 返回对象:{ statusCode:401 } 返回自定义的 401 页面,验证失败

返回 boolean

目录结构
  • pages
    • detail
      • [id].vue
      • index.vue
    • index.vue
  • app.vue
  • error.vue —- 路由验证错误页面
pages/detail/[id].vue
<script setup>
definePageMeta({
validate: route => {
return /^\d+$/i.test(route.params.id)
}
})
</script>

<template>
<div>Page: detail/[id]</div>
</template>
pages/detail/index.vue
<template>
<div>
Page: detail/index
</div>
</template>
pages/index.vue
<template>
<div>
<div>Page: index</div>
</div>
</template>
error.vue
<template>
<div>
<div>error</div>
<div style="margin-top: 30px">{{ $route }}</div>
<div style="margin-top: 30px">
<button @click="goToHomeHandler">返回首页</button>
</div>
</div>
</template>

<script setup>
const goToHomeHandler = () => {
clearError({ redirect: '/' }) // 清除错误,返回首页
}
</script>
app.vue
<template>
<div>
<div>
<NuxtLink to="/">
<button>Home</button>
</NuxtLink>
<NuxtLink to="/detail">
<button>detail</button>
</NuxtLink>
<NuxtLink to="/detail/1">
<button>detail/1</button>
</NuxtLink>
<NuxtLink to="/detail/abc">
<button>detail/abc</button>
</NuxtLink>
</div>
<NuxtPage></NuxtPage>
</div>
</template>

<style>
a {
margin-right: 10px;
}
button {
cursor: pointer;
}
.router-link-active button {
color: red;
}
</style>

返回对象

其他页面和上面一样。

修改 [id].vue
definePageMeta({
validate: route => {
return { statusCode: 302 }
}
})
修改 error.vue
<template>
<div>
<div>error</div>
<div style="margin-top: 30px">{{ $route }}</div>
<div style="margin-top: 30px">statusCode: {{ props.error.statusCode }}</div>
<div style="margin-top: 30px">
<button @click="goToHomeHandler">返回首页</button>
</div>
</div>
</template>

<script setup>
const props = defineProps(['error']) // 接收传递的错误信息
const goToHomeHandler = () => {
clearError({ redirect: '/' }) // 清除错误,返回首页
}
</script>

渲染模式

浏览器和服务器都可以解释 JavaScript 代码,将 Vue.js 组件转换为 HTML 元素。这一步称为渲染

  • 在客户端将 Vue.js 组件呈现为 HTML 元素,称为:客户端渲染模式。
  • 在服务器将 Vue.js 组件呈现为 HTML 元素,称为:服务器渲染模式 或 通用渲染。

而Nuxt3是支持多种渲染模式:

  • 客户端渲染模式(CSR):将 srr 设置为 false。

    • 优点:
      • 开发速度快。不必担心代码的服务器兼容性,仅用于浏览器的 API。
      • 便宜。只需要在任何带有 HTML、CSS 和 JavaScript 文件的静态服务器上托管仅限客户端应用程序即可。
      • 离线。代码完全在浏览器中运行,可以在互联网不可用时很好地保持工作。
    • 缺点:
      • 性能不好。需要等浏览器下载、解析和运行 JS 文件。
      • 搜索引擎优化不好。爬虫在第一次索引页面时不会等待界面完全呈现。
  • 服务器渲染模式(SSR):将 ssr 设置为 true。

    • 优点:
      • 性能好。用户可以立即访问页面内容。浏览器显示静态内容比 JS 生成的内容快,且在水合过程中保留了程序交互性。
      • 搜索引擎优化很好。渲染是将整个页面返回,爬虫能直接索引页面内容。
    • 缺点:
      • 开发受限。服务器和浏览器的环境不同,提供的 API 也不同。
      • 成本问题。服务器运行才能动态显示页面,每月成本增加;但浏览器接管了客户端导航的通用渲染,服务器的调用会大大减少。
    • 适用于任何面向内容的网站:博客、营销网站、投资组合、电子商务网站和市场。
  • 混合渲染模式(CSR | SSR | SSG | SWR):允许每个路由使用不同的缓存规则,并决定服务器应该如何响应给定URL上的新请求。需要再 routeRules 根据每个路由动态配置渲染模式。

    • SSG:静态站点生成。在构建时预先生成所有静态 HTML 文件,直接托管在 CDN 上。首次加载极快,SEO 支持非常好。不过当数据需要更新时,需要重新构建。适合内容固定的页面,比如:文档、博客等。

    • SWR:过时数据优先更新策略。在数据获取时优先返回缓存(可能过期的数据),同时在后台静默更新最新数据。

      • 工作流程:

        用户->>客户端: 发起请求
        客户端->>缓存: 检查是否存在旧数据
        缓存-->>客户端: 立即返回旧数据
        客户端->>服务器: 后台发起验证请求
        服务器-->>客户端: 返回新数据
        客户端->>缓存: 更新缓存
        客户端->>UI: 静默更新界面
      • 优势:

        • 零等待加载:用户立即看到内容。
        • 带宽优化:减少重复请求。
        • 自动更新:保持数据新鲜度。
        • 离线支持:配合 Service Worker 实现。
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true }, // 构建时预渲染
'/detail': { ssr: true }, // 服务器端渲染
'/news': { swr: 3600 }, // 混合渲染模式,每隔 1h 重新获取最新数据
'/order/**': { ssr: false } // 客户端渲染
}
})

插件

Nuxt 支持自定义插件进行扩展,创建插件有两种方式:单个文件中使用 useNuxtApp(name, value) 创建、在 plugins 目录中创建。

Nuxt 会自动读取 plugins/ 目录中的文件,并在创建 Vue 应用程序时加载它们。即里面的所有插件都是自动注册的,不需要单独将它们添加到nuxt.config.ts中。

注意:只有目录顶层的文件(或任何子目录中的索引文件)才会自动注册为插件。

单文件中创建

useNuxtApp() 中的 provide(name, value) 方法直接创建。useNuxtApp() 提供了访问 Nuxt 共享运行时上下文的方法和属性(两端可用):provide、hooks、callhook、vueApp 等。

感觉在 app.vue 中创建查询,pages 目录下的文件创建后切换路由控制台会报错。

<template>
<NuxtPage></NuxtPage>
</template>

<script setup>
const price = ref(78)
const nuxtApp = useNuxtApp()
nuxtApp.provide('doublePrice', () => price.value * 2)
nuxtApp.provide('increment', ++price.value)
console.log(nuxtApp.$doublePrice(), nuxtApp.$increment) // 158 79
</script>

plugins 目录中创建

  • 在 plugins 目录中创建插件(推荐)
    • 顶级和子目录 index 文件写的插件会创建 Vue 应用程序时自动加载和注册。
    • 如果带有 .server 或 .client 后缀的插件名,那么可以区分服务器端或客户端的运行环境。

步骤:

  1. 在 plugins 目录中创建插件文件。
  2. 接着在 defineNuxtPlugin 函数创建插件,参数是一个回调函数。
  3. 然后在组件中使用 useNuxtApp() 拿到插件中的方法。

注意:插件注册顺序可以通过在文件名前加上一个数字来控制插件注册的顺序。有助于实现插件间的相互依赖。如:plugins/1.price.ts、``plugins/ 2.string.ts`…

注意:文件名是按字符串排序的,而不是按数值排序的。也就是:10.new.ts 将排在 2.new.ts 前面。可以用 0作为 个位数的前缀

创建帮助函数

使用组合 API

需要注意的是,使用组合在插件间互相依赖情况下可能无法工作;插件依赖 vue.js 生命周期的情况下无法工作。

plugins/price.ts
// 定义插件
export default defineNuxtPlugin(nuxtApp => {
return {
provide: {
formatPrice (value: number) {
return value.toFixed(2)
}
}
}
})
pages/detail.vue
<script setup lang="ts">
// 使用插件中的方法
const { $formatPrice } = useNuxtApp()
</script>

<template>
<div>Page: detail</div>
<div>34.5678 => {{ $formatPrice(34.5678) }}</div>
</template>

.server 或 .client 后缀

// plugins/time.client.ts ---- 定义
export default defineNuxtPlugin((nuxtApp) => {
return {
provide: {
currentTime () {
return Date.now()
},
status: 1,
userInfo: { name: '张三' }
}
}
})
// app.vue ---- 使用
const { $currentTime, $status, $userInfo } = useNuxtApp()
if (process.client) {
console.log(1, $currentTime(), $status, $userInfo)
}

app 生命周期

使用 Hooks 监听App的生命周期。

nuxtApp.hook(hook api, func)

Hook Arguments Environment Description
app:created vueApp Server & Client Called when initial vueApp instance is created.
app:error err Server & Client Called when a fatal error occurs.
app:error:cleared { redirect? } Server & Client Called when a fatal error occurs.
app:data:refresh keys? Server & Client (internal)
vue:setup - Server & Client (internal)
vue:error err, target, info Server & Client Called when a vue error propagates to the root component. Learn More.
app:rendered renderContext Server Called when SSR rendering is done.
app:redirected - Server Called before SSR redirection.
app:beforeMount vueApp Client Called before mounting the app, called only on client side.
app:mounted vueApp Client Called when Vue app is initialized and mounted in browser.
app:suspense:resolve appComponent Client On Suspense resolved event.
app:manifest:update { id, timestamp } Client Called when there is a newer version of your app detected.
link:prefetch to Client Called when a <NuxtLink> is observed to be prefetched.
page:start pageComponent? Client Called on Suspense pending event.
page:finish pageComponent? Client Called on Suspense resolved event.
page:loading:start - Client Called when the setup() of the new page is running.
page:loading:end - Client Called after page:finish
page:transition:finish pageComponent? Client After page transition onAfterLeave event.
dev:ssr-logs logs Client Called with an array of server-side logs that have been passed to the client (if features.devLogs is enabled).
page:view-transition:start transition Client Called after document.startViewTransition is called when experimental viewTransition support is enabled.

使用插件来监听(推荐)

plugins/lifecycle.ts

export default defineNuxtPlugin(nuxtApp => {
// client && server
nuxtApp.hook('app:created', vueApp => {
console.log('app:created')
})
// client && server
nuxtApp.hook('app:error', err => {
console.log('app:error')
})
// client && server
nuxtApp.hook('vue:setup', () => {
console.log('vue:setup')
})
// client && server
nuxtApp.hook('vue:error', (err, target, info) => {
console.log('vue:error')
})
// server
nuxtApp.hook('app:rendered', renderContext => {
console.log('app:rendered')
})
// client
nuxtApp.hook('app:beforeMount', vueApp => {
console.log('app:beforeMount')
})
// client
nuxtApp.hook('app:mounted', vueApp => {
console.log('app:mounted')
})
/*
client 执行顺序
app:created
app:beforeMount
vue:setup
app:mounted
*/
/*
server 执行顺序
app:created
vue:setup
app:rendered
*/
})

在 app.vue 中监听

<script setup>
const nuxtApp = useNuxtApp()
nuxtApp.hook('app:created', vueApp => {
console.log('app:created')
})
// client && server
nuxtApp.hook('app:error', err => {
console.log('app:error')
})
// client && server
nuxtApp.hook('vue:setup', () => {
console.log('vue:setup')
})
// client && server
nuxtApp.hook('vue:error', (err, target, info) => {
console.log('vue:error')
})
// server
nuxtApp.hook('app:rendered', renderContext => {
console.log('app:rendered')
})
// client
nuxtApp.hook('app:beforeMount', vueApp => {
console.log('app:beforeMount')
})
// client
nuxtApp.hook('app:mounted', vueApp => {
console.log('app:mounted')
})
/*
client 执行顺序
app:mounted
*/
/*
server 执行顺序
app:rendered
*/
</script>

上面的执行结果只有一个,是因为我们在 setup 里进行的,所以只能执行 vue:setup 后面的生命周期。

注意:如果不在 setup 中会报错的。

组件生命周期

客户端渲染

options API composition API
beforeCreate setup()
created setup()
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeDestory onBeforeUnmount
destoryed onUnmounted
errorCaptured onErrorCaptured

服务器端渲染

因为没有任何动态更新,所以像 mountedupdated 这样的生命周期钩子不会在 SSR 期间被调用,只会在客户端运行。

只有 beforeCreatecreated 这两个钩子会在 SSR 期间被调用。

应该避免在 beforeCreatecreated 使用期间编写副作用代码,比如使用定时器 setInterval 等。我们可能会在客户端对这些代码在 beforeUnmoutunMounted 中清理。但 SSR 中没有这些钩子。所以在 SSR 中我们需要避免这种情况,将副作用的代码放到 mounted 中。

options API composition API
beforeCreate setup()
created setup()

获取数据

在 Nuxt 中数据的获取主要是通过下面5个函数来实现(支持 Server 和 Client):$fetch()、useAsyncData()、useLazyAsyncData()、useFetch()、useLazyFetch()

$fetch() 刷新会在客户端和服务端都会发起请求,浪费服务器资源,不推荐。后面几个是 hooks api,刷新页面服务端请求数据,客户端不会请求;路由切换客户端请求数据,服务端不会请求;服务端请求数据会通过 hydration 水合到客户端。

useAsyncData()、useFetch() 会阻塞页面导航,也就是需要等数据请求回来后才显示页面信息。可以设置 lazy:true 使其不阻塞页面导航,然后通过 watch 监听数据从而确保数据一定能拿到。

useLazyAsyncData()、useLazyFetch() 不会阻塞页面,相当于设置了 lazy: true,上面函数的简写方式。

注意:上面的函数只能在 setup 或 lifecycle hooks 中使用。

$fetch()

$fetch(url, opts) 是一个类原生 fetch 的跨平台请求库。

不过,$fetch 函数在刷新页面时 client 和 server 都会发起一次请求,浪费服务器资源。

<script setup>
const BASE_URL = 'http://codercba.com:9060/juanpi/api'

// 刷新页面时 client 和 server 都会发起一次请求,浪费服务器资源
$fetch(BASE_URL + '/homeInfo', {method: 'GET' })
.then(res => { console.log(res) })
</script>

useAsyncData()

useAsyncData(key, func) 专门解决异步获取数据的函数,会阻止页面导航。发起异步请求需用到 $fetch 全局函数,当然也可以用其他的,比如 axios。

使用 useAsyncData 函数,在刷新页面时可以减少客户端发起的一次请求。

刷新页面时服务端会发起请求,客户端不会发起。每次切换路由后回到此页面 或 修改代码保存后, 客户端会发起请求,服务端不会发起。 也就是客户端和服务端同时只有一方发起请求。

<script setup>
const BASE_URL = 'http://codercba.com:9060/juanpi/api'
const { data: homeInfo } = await useAsyncData('homeInfo', () => {
return $fetch(BASE_URL + '/homeInfo', { method: 'GET' })
})
console.log('homeInfo:', homeInfo.value?.data)
</script>

注意:多个 useAsyncData 函数时,需要保证 key 的唯一性,不然请求结果会重复。

下面代码因为两个函数的请求 key 值不唯一,所以在当前路由进行刷新时会出现重复请求,输出相同的结果。(路由切换不会出现重复请求,只有刷新才会)

<script setup>
const BASE_URL = 'http://codercba.com:9060/juanpi/api'
const { data: homeInfo } = await useAsyncData('homeInfo', () => {
return $fetch(BASE_URL + '/homeInfo', { method: 'GET' })
})
console.log('homeInfo:', homeInfo.value?.data)

const { data: goods } = await useAsyncData('homeInfo', () => {
return $fetch(BASE_URL + '/goods', { method: 'POST' })
})
console.log('goods:', goods.value?.data)
</script>

useFetch() 与 useLazyFetch()

useFetch()

useFetch(url, opts) 用于获取任意的 URL 地址的数据,会阻止页面导航。是 useAsyncData 使用 $fetch 的简写。

<script setup>
const BASE_URL = 'http://codercba.com:9060/juanpi/api'
const { data: homeInfo } = await useFetch(BASE_URL + '/homeInfo', { method: 'GET' })
console.log(homeInfo.value)
</script>

验证阻塞页面导航

下面的代码需要等数据返回后才会进行挂载,也就是输出’onMounted’。

<script setup>
const BASE_URL = 'http://codercba.com:9060/juanpi/api'

const { data: homeInfo } = await useFetch(BASE_URL + '/homeInfo', { method: 'GET' })
console.log(homeInfo.value)

onMounted(() => {
console.log('onMounted')
})
</script>

<template>
<div>Page: lazy</div>
</template>

取消获取数据阻塞页面

当页面数据需要获取很多时或网络波动较大时,那我们不可能让用户等待很久。如何解决呢?可以设置 lazy: true,这样就不会阻塞页面的导航了。同时设置 watch 去监听数据,当 data 数据更新后 watch 会自动去赋值那些。

<script setup>
const BASE_URL = 'http://codercba.com:9060/juanpi/api'

const { data: homeInfo } = await useFetch(BASE_URL + '/homeInfo', { method: 'GET', lazy: true })
console.log(homeInfo.value)

onMounted(() => {
console.log('onMounted')
})

// 确保数据一定能拿到
watch(homeInfo, () => {
console.log(homeInfo.value)
})
</script>

<template>
<div>Page: lazy</div>
</template>

当然我们也可以不用 watch 去监听,当我们切换到其他路由后回到此页面一样能拿到数据。但是不能保证一定能拿到数据,比如网络波动较大,超时那些。

useLazyFetch()

上面的写法有一种简写形式,就是使用 useLazyFetch(),效果与 useFetch(api, { lazy: true }) 一样。

<script setup>
const BASE_URL = 'http://codercba.com:9060/juanpi/api'

const { data: homeInfo } = await useLazyFetch(BASE_URL + '/homeInfo', { method: 'GET' })
console.log(homeInfo.value)

onMounted(() => {
console.log('onMounted')
})

watch(homeInfo, () => {
console.log(homeInfo.value)
})
</script>

<template>
<div>Page: lazy</div>
</template>

客户端刷新按钮

如果需要在客户端进行刷新,我们可以解构出 refresh。

<script setup>
const BASE_URL = 'http://codercba.com:9060/juanpi/api'

const { data: homeInfo, refresh } = await useLazyFetch(BASE_URL + '/homeInfo', { method: 'GET' })
console.log(homeInfo.value)

const updateHandler = () => {
refresh()
}
onMounted(() => {
console.log('onMounted')
})

watch(homeInfo, () => {
console.log(homeInfo.value)
})
</script>

<template>
<div>
<div>Page: lazy</div>
<button @click="updateHandler">获取最新数据</button>
</div>
</template>

useFetch vs axios

获取数据 Nuxt 推荐使用 useFetch(),为什么不是 axios?

  • 因为 useFetch() 底部使用的是 $fetch(),该函数基于 unjs/ohmyfetch 请求库,并于原生的 Fetch API 有着相同 API。
  • unjs/ohmyfetch 是一个跨端请求库。运行在服务器,可以智能的处理对 API 接口的直接调用。运行在客户端,可以对后台提供的 API 接口正常的调用(类似 axios),当然也支持第三方接口的调用。会自动解析响应和对数据进行字符串化。
  • useFetch 支持之鞥呢的类型提示和智能的推断 API 响应式类型。
  • 在 setup 中用 useFetch 获取数据,会减去客户端重复发起的请求。

useFetch 的封装

步骤:

  1. 定义 request 类并导出。
  2. 在类中定义 request、get、post 方法。
  3. 在 request 中使用 useFetch 发起网络请求。
  4. 添加 TS 类型声明。
import type { AsyncData, UseFetchOptions } from "#app"

const BASE_URL = 'http://codercba.com:9060/juanpi/api'
export type Methods = 'GET' | 'POST'

class Request {
request<T> (url: string, method: Methods, data?: any, options?: UseFetchOptions<T>):Promise<AsyncData<T,Error>> {
// 整合请求配置
let newOptions = {
baseURL: BASE_URL,
method: method || 'GET', // 没传则默认为 GET
...options
}
// 根据请求类型设置请求体
if (method === 'GET') {
newOptions.params = data || {}
} else {
newOptions.body = data || {}
}
return new Promise((resolve, reject) => {
useFetch<T>(url, newOptions as any)
.then(res => {
resolve(res as AsyncData<T, Error>)
}).catch(err => {
reject(err)
})
})
}

post<T> (url: string, data?: any, options?: UseFetchOptions<T>) {
return this.request<T>(url, 'POST', data, options)
}

get<T> (url: string, params?: any, options?: UseFetchOptions<T>) {
return this.request<T>(url, 'GET', params, options)
}
}

export default new Request()

Server API

编写后端服务接口可以在 server/api 目录下编写。

编写一个 /api/homeinfo 接口:

  1. 新建 server/api/homeinfo.ts。
  2. 在该文件中使用 defineEventHandler 函数定义接口。
  3. 使用 useFetch 函数调用编写的接口。
// server/api/homeinfo.ts
export default defineEventHandler(async event => {
const { context } = event
const { req, res } = event.node

const method1 = req.method
const url = req.url
const params = context.params
console.log('method1=', method1)
console.log('url=', url)
console.log('params=', params)

// 获取查询字符串
const query = getQuery(event)
const method2 = getMethod(event) // 废弃
const header = getHeaders(event)
const body = await readBody(event)
const rawBody = await readRawBody(event)
console.log('query=', query)
console.log('method2=', method2)
console.log('header=', header)
console.log('body=', body)
console.log('rawBody=', rawBody)

return {
code: 200,
data: {
token: 'test token',
query,
method2,
header,
body,
rawBody
}
}
})

post 请求

// 终端输出
console.log('method1=', method1) // POST
console.log('url=', url) // /api/homeinfo
console.log('params=', params) // {}

// 获取查询字符串
const query = getQuery(event)
const method2 = getMethod(event) // 废弃
const header = getHeaders(event)
const body = await readBody(event)
const rawBody = await readRawBody(event)
console.log('query=', query) // {}
console.log('method2=', method2) // POST
console.log('header=', header) // { ... }
console.log('body=', body) // { name: 'zhangsan', sex: '男', age: 18 }
console.log('rawBody=', rawBody) // {"name":"zhangsan","sex":"男","age":18}

get请求

全局状态共享

跨页面、跨组件全局状态共享可使用 useState(支持Server和Client )。

  • useState<T>(init?: () => T | Ref<T>): Ref<T>
  • useState<T>(key: string, init?: () => T | Ref<T>): Ref<T>
  • 参数:
    • init:为状态提供初始值的函数,该函数也支持返回一个Ref类型。
    • key: 唯一key,确保在跨请求获取该数据时,保证数据的唯一性。为空时会根据文件和行号自动生成唯一key。
  • 返回值:Ref 响应式对象。

使用:

  1. 在 composables 目录下编写,新建 composables/states.ts。
  2. 在该文件中使用 useState 定义全局共享状态并导出。
  3. 在组件中导入 states.ts 导出的全局状态。
// composables/states.ts
export const useUserInfo = function () {
return useState('userInfo', () => {
return {
name: 'zhangsan',
age: 18
}
})
}
// pages/index.vue
const userInfo = useUserInfo()
console.log(userInfo.value) // { name: 'zhangsan', age: 18 }

注意:

  • useState 只能在 setup 函数中 和 lifecycle 函数中使用。
  • useState 不支持 classes, functions or symbols类型,因为这些类型不支持序列化。

集成 Pinia

安装依赖

安装依赖:npm i pinia @pinia/nuxt

如有遇到 pinia 安装失败,可以添加 --legacy-peer-deps 告诉 NPM 忽略对等依赖并继续安装。

配置 nuxt.config.ts

export default defineNuxtConfig({
modules: [ '@pinia/nuxt' ]
})

使用

  1. 在 store 目录中编写代码,如:store/counter.ts
  2. 在该文件中使用功能 defineStore 函数来定义 store 对象。
  3. 在组件中使用定义好的 store 对象。

store/counter.ts

import { defineStore } from "pinia"
import { ref } from "vue"

export const useCounterStore = defineStore('counter', () => {
const number = ref(10)
const doubleNumber = () => {
number.value *= 2
}
return {
number, doubleNumber
}
})

pages/index.vue

<template>
<div>
<div>Page: index {{ number }}</div>
<button @click="counterStore.doubleNumber()">double number</button>
</div>
</template>

<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '~/store/counter'

// 使用 pinia
const counterStore = useCounterStore()
const { number } = storeToRefs(counterStore)
</script>

集成 ElementPlus

https://nuxt.com/modules/element-plus

安装

命令:npm i element-plus @element-plus/nuxt

配置

在 nuxt.config.ts 中配置 element-plus 模块

export default defineNuxtConfig({
css: [ 'element-plus/dist/index.css' ],
modules: [ '@element-plus/nuxt' ]
})

国际化

新建 plugins/element-plus.client.js 文件

import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'

export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.use(ElementPlus, {
locale: zhCn
})
})

修改 nuxt.config.ts 文件

export default defineNuxtConfig({
css: [ 'element-plus/dist/index.css' ],
plugins: [ '@/plugins/element-plus.client.js' ],
modules: [ '@element-plus/nuxt' ]
})