Skip to content
目录

Nuxt3 - 面经项目实战

简介

Nuxt 框架提供了一种基于 Node.js 的服务端渲染方案 SSR(Server Side Rendering),可以让 Vue 应用在服务器端进行渲染,从而提高页面的加载速度和 SEO。

Nuxt.js 服务器渲染的工作原理,包括:

  • 服务端渲染:Nuxt.js 在服务器端执行 Vue 组件的渲染过程,并生成初始 HTML。
  • 客户端激活:一旦初始 HTML 被发送到浏览器,Vue.js 会接管页面,并在客户端激活成一个交互式应用程序。

在使用 Nuxt3 进行开发时,需要注意以下基本概念:

  • 路由系统
  • 数据获取
  • 组件
  • 布局
  • 状态管理
  • 插件
  • 中间件

演示环境

  • 电脑 - Windows 10
  • Node.js - v16.15.0
  • Npm - 9.4.0
  • Nuxt - 3.5.2

初始化项目

Nuxt3 官网

下载命令

sh
npx nuxi init <project-name>

其中 <project-name> 换成自己的项目名称。

下载问题

由于大陆访问受限,所以下载可能会失败。

解决方案参考:修改 host 文件

Windows 系统

sh
C:\Windows\System32\drivers\etc

# 增加以下代码
185.199.108.133 raw.githubusercontent.com

映射关系为访问 raw.githubusercontent.com 映射到 IP 地址 185.199.108.133

注意:其中的 # 表示注释,映射关系行首不能有 # ,否者不生效。

Mac 系统

参考链接:

运行项目

  1. 进入项目目录,并安装项目依赖 npm install
  2. 启动项目,npm run dev
  3. ✨ 浏览器访问 http://localhost:3000

服务端渲染

SPA 和 SSR 是什么

  • SPA(Single Page Application)单页面应用,在客户端通过 JS 动态地构建页面。
  • SSR(Server-Side Rendering)服务器端渲染,在服务器端生成 HTML 页面并发送给客户端。

有什么不同?

  • SPA 的特点是页面切换流畅,动态渲染变化的部分,用户体验好,但是对搜索引擎的支持不够友好。
  • SSR 的特点是对搜索引擎友好,可以提高页面首次加载速度SEO,但是页面切换可能不够流畅,因为每次都是请求一个完整的 HTML 页面。

Nuxt 框架优势

  • Nuxt 采用了混合的架构模式,可以同时支持 SSR 和 SPA。

  • 在 Nuxt 中,首次访问页面是 SSR 方式,也就是在服务器端生成 HTML 页面并发送给客户端,但后续的页面切换则使用 SPA 的方式进行,从而兼顾了 SSR 和 SPA 的优点。

适用场景

企业网站、商品展示 等 C 端网站,对 SEO 搜索更友好,且页面切换流畅,用户体验更好。

路由案例

目录结构

我们先来认识 Nuxt 项目的目录结构。

sh
├─.nuxt              非工程代码,存放运行或发行的编译结果
├─node_modules       项目依赖
├─public             网站资源目录
├─server             接口目录
├─.gitignore         git 忽略文件
├─.npmrc             npm 配置文件
├─app.vue            根组件
├─nuxt.config.ts     nuxt 配置文件
├─package.json       项目配置文件
├─README.md          项目说明文件
└─tsconfig.json      ts 配置文件

案例练习

nuxt 有一些约定的目录,有特殊功能,如 pages 目录的 vue 文件会自动注册路由。

sh
├─ pages             页面目录,自动注册路由

Nuxt.js 自带路由功能,不需要额外安装和配置,无需安装 vue-router

参考目录

sh
├─ pages               页面目录,自动注册路由
  └─index.vue         主页
  └─user.vue          用户页
├─app.vue              根组件

参考代码

vue
<template>
  <!-- 路由链接 -->
  <NuxtLink to="/">首页</NuxtLink>
  <NuxtLink to="/user">用户页</NuxtLink>
  <!-- 路由占位 -->
  <NuxtPage />
</template>
  • <NuxtPage> 相当于 <RouterView>
  • <NuxtLink> 相当于 <RouterLink>

项目实战 - Nuxt 版面经

项目资料

项目收获

  • SEO 优化
  • 组件库 @vant/nuxt 使用
  • 移动端 vw 适配
  • 路由系统
  • 路由中间件
  • 组件复用
  • 布局复用

SEO 优化

通过设置网页 title 和 description 等 SEO 优化信息,由服务端渲染。

vue
<script setup lang="ts">
// SEO 优化
useSeoMeta({
  title: '黑马面经 - 找工作神器|笔试题库|面试经验',
  description:
    '求职之前,先上黑马面经,就业找工作一站解决。全面提升求职竞争力,找到好工作,拿到好offer。',
  author: '黑马程序员',
})
</script>

@vant/nuxt 组件库

  • 安装 nuxt 版 vant-ui
sh
pnpm i @vant/nuxt
  • 添加配置
ts
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: false },
  modules: ['@vant/nuxt'],
})
  • 使用
vue
<van-button type="primary">主要按钮</van-button>
<van-button type="info">信息按钮</van-button>

PS: 在 Nuxt 项目中,vant 组件会自动按需导入(需重启)。

项目中的 vw 适配

安装依赖

sh
pnpm i postcss-px-to-viewport

添加配置

ts
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: false },
  modules: ['@vant/nuxt'],
  // 移动端适配
  postcss: {
    plugins: {
      'postcss-px-to-viewport': {
        viewportWidth: 375,
      },
    },
  },
})

参考文档:

路由配置-自动路由

在 pages 目录的 vue 文件会自动注册路由。

面经移动端

路由设计

  • 登录页 login.vue
  • 注册页 register.vue
  • 文章详情页 detail.vue
  • 面经(TabBar)article.vue
  • 收藏(TabBar)collect.vue
  • 喜欢(TabBar)like.vue
  • 我的(TabBar)user.vue

路由中间件

新建 middleware 目录,通过 Nuxt 路由中间件实现路由重定向,包括导航守卫等路由功能。

ts
export default defineNuxtRouteMiddleware((to, from) => {
  if (to.path === '/') {
    return navigateTo('/article')
  }
})

参考链接

layouts 布局

通过 layout 实现布局的复用,新建文件 layouts/tabbar.vue

  • route 开启路由模式
  • to 跳转路由
vue
<template>
  <div class="layout-page">
    <!-- 插槽 -->
    <slot />
    <!-- tabbar -->
    <van-tabbar route>
      <van-tabbar-item to="/article" icon="notes-o">面经</van-tabbar-item>
      <van-tabbar-item to="/collect" icon="star-o">收藏</van-tabbar-item>
      <van-tabbar-item to="/like" icon="like-o">喜欢</van-tabbar-item>
      <van-tabbar-item to="/user" icon="user-o">我的</van-tabbar-item>
    </van-tabbar>
  </div>
</template>

页面中使用 layout 布局

vue
<template>
  <NuxtLayout name="tabbar">
    <h1>面经首页</h1>
  </NuxtLayout>
</template>

修改主题色

在 app.vue 的样式全局生效。

vue
<style>
:root {
  --van-primary-color: #fa6d1d !important;
}
</style>

参考链接

注册页

使用组件,在 Nuxt3 中无需导入。

  • van-nav-bar
  • van-form
  • van-field
  • van-button

静态结构

register.vue

vue
<script setup lang="ts">
// 表单数据
const form = reactive({
  username: 'itheima',
  password: '123456',
})

// 表单提交
const onSubmit = async () => {
  //
}
</script>

<template>
  <div class="register-page">
    <!-- 导航栏部分 -->
    <van-nav-bar title="面经注册" />

    <!-- 一旦form表单提交了,就会触发submit,可以在submit事件中
        根据拿到的表单提交信息,发送axios请求
    -->
    <van-form @submit="onSubmit">
      <!-- 输入框组件 -->
      <!-- \w 字母数字_   \d 数字0-9 -->
      <van-field
        v-model="form.username"
        name="username"
        label="用户名"
        placeholder="用户名"
        :rules="[
          { required: true, message: '请填写用户名' },
          { pattern: /^\w{5,}$/, message: '用户名至少包含5个字符' },
        ]"
      />
      <van-field
        v-model="form.password"
        type="password"
        name="password"
        label="密码"
        placeholder="密码"
        :rules="[
          { required: true, message: '请填写密码' },
          { pattern: /^\w{6,}$/, message: '密码至少包含6个字符' },
        ]"
      />
      <div style="margin: 16px">
        <van-button block type="primary" native-type="submit">注册</van-button>
      </div>
    </van-form>
    <NuxtLink class="link" to="/login">已注册,去登录</NuxtLink>
  </div>
</template>

<style scoped>
.link {
  color: #069;
  font-size: 12px;
  padding-right: 20px;
  float: right;
}
</style>

请求封装

安装 axios,登录请求可用 axios 发送。

sh
pnpm i axios

新建 utils/request.ts 封装 axios 模块

ts
/* 封装axios用于发送请求 */
import axios from 'axios'

// 创建一个新的axios实例
const request = axios.create({
  baseURL: 'http://interview-api-t.itheima.net/h5',
  timeout: 5000,
})

// 添加请求拦截器
request.interceptors.request.use(
  function (config) {
    // 在发送请求之前做些什么
    return config
  },
  function (error) {
    // 对请求错误做些什么
    return Promise.reject(error)
  },
)

// 添加响应拦截器
request.interceptors.response.use(
  function (response) {
    // 对响应数据做点什么
    return response.data
  },
  function (error) {
    // 对响应错误做点什么
    return Promise.reject(error)
  },
)

export default request

register.vue 表单提交调用接口

vue
<script setup lang="ts">
// 表单数据
const form = reactive({
  username: 'itheima',
  password: '123456',
})

// 表单提交
const onSubmit = async () => {
  // 注册请求
  await request({
    method: 'POST',
    url: '/user/register',
    data: form,
  })
  // 成功提示
  showSuccessToast('注册成功')
  // 跳转页面
  navigateTo('/login')
}
</script>

参考链接

登录页

登录页逻辑参考注册页。

静态结构

login.vue

vue
<script setup lang="ts">
// 表单数据
const form = reactive({
  username: 'itheima',
  password: '123456',
})

// 表单提交
const onSubmit = async () => {
  //
}
</script>

<template>
  <div class="login-page">
    <!-- 导航栏部分 -->
    <van-nav-bar title="面经登录" />

    <!-- 一旦form表单提交了,就会触发submit,可以在submit事件中
         根据拿到的表单提交信息,发送axios请求
     -->
    <van-form @submit="onSubmit">
      <!-- 输入框组件 -->
      <!-- \w 字母数字_   \d 数字0-9 -->
      <van-field
        v-model="form.username"
        name="username"
        label="用户名"
        placeholder="用户名"
        :rules="[
          { required: true, message: '请填写用户名' },
          { pattern: /^\w{5,}$/, message: '用户名至少包含5个字符' },
        ]"
      />
      <van-field
        v-model="form.password"
        type="password"
        name="password"
        label="密码"
        placeholder="密码"
        :rules="[
          { required: true, message: '请填写密码' },
          { pattern: /^\w{6,}$/, message: '密码至少包含6个字符' },
        ]"
      />
      <div style="margin: 16px">
        <van-button block type="info" native-type="submit">提交</van-button>
      </div>
    </van-form>
    <NuxtLink class="link" to="/register">注册账号</NuxtLink>
  </div>
</template>

<style scoped>
.link {
  color: #069;
  font-size: 12px;
  padding-right: 20px;
  float: right;
}
</style>

持久化存储-差异

Nuxt 服务器端不支持 localStorage 所以运行会报错,可通过 cookie 代替。

❌ 常见报错: localStorage is not defined ❌ 持久化存储失败 -> 使用 useCookie 代替。

ts
const KEY = 'hmmj-token'

// 获取
export const getToken = () => {
  return localStorage.getItem(KEY)
}

// 设置
export const setToken = (newToken: string) => {
  localStorage.setItem(KEY, newToken)
}

// 删除
export const delToken = () => {
  localStorage.removeItem(KEY)
}

✅ 使用 useCookie 代替。

ts
const KEY = 'hmmj-token'

// 获取
export const getToken = () => {
  return useCookie(KEY).value
}

// 设置
export const setToken = (newToken: string) => {
  useCookie(KEY, { maxAge: 60 * 60 * 24 * 14 }).value = newToken
}

// 删除
export const delToken = () => {
  useCookie(KEY).value = undefined
}

✅ 登录成功后,存储 token

ts
// 表单提交
const onSubmit = async () => {
  // 登录请求
  const { data } = await loginAPI(form)
  // 保存 token
  setToken(res.data.token)
  // 成功提示
  showSuccessToast('登录成功')
  // 跳转页面
  navigateTo('/')
}

参考链接

路由鉴权-导航守卫

Nuxt 有路由中间件,简化了导航守卫的实现。

ts
// 白名单列表,记录无需权限访问的所有页面
const whiteList = ['/login', '/register']

export default defineNuxtRouteMiddleware((to, from) => {
  // 首页重定向
  if (to.path === '/') {
    return navigateTo('/article')
  }
  // 获取 token
  const token = getToken()
  if (!token && !whiteList.includes(to.path)) {
    return navigateTo('/login')
  }
})

参考链接

面经列表-动态渲染列表

68295257216

  • 通过 axios 发送请求
  • 通过 useFetch 发送请求 - 推荐

article.vue

存储数据

jsx
<script setup lang="ts">
const list = ref<ArticleItem[]>([])

const pageParams = reactive({
  current: 40,
  sorter: 'weight_desc',
  pageSize: 10,
})

const getList = async () => {
  // 获取数据
  const { data } = await useFetch<any>('/interview/query', {
    baseURL: 'http://interview-api-t.itheima.net/h5/',
    headers: {
      Authorization: `Bearer ${getToken()}`,
    },
    params: pageParams,
  })
  // 列表追加
  list.value.push(...data.value.data.rows)
}

getList()
</script>

类型声明

ts
/** 面经列表项 */
export type ArticleItem = {
  /** 主键id */
  id: string
  /** 头像 */
  avatar: string
  /** 面经内容 */
  content: string
  /** 创建时间 */
  createdAt: string
  /** 创建人 */
  creator: string
  /** 点赞量 */
  likeCount: number
  /** 面经标题 */
  stem: string
  /** 浏览量 */
  views: number
}

v-for循环展示

jsx
<template>
  <div class="article-page">
    <nav class="my-nav van-hairline--bottom">
        <a href="javascript:;">推荐</a>
        <a href="javascript:;">最新</a>
        <div class="logo"><img src="@/assets/logo.png" alt="" /></div>
    </nav>
    <ArticleItem v-for="item in list" :key="item.id" :item="item" />
  </div>
</template>

<style lang="less" scoped>
.article-page {
  margin-bottom: 50px;
  margin-top: 44px;
  .my-nav {
    height: 44px;
    position: fixed;
    left: 0;
    top: 0;
    width: 100%;
    z-index: 999;
    background: #fff;
    display: flex;
    align-items: center;
    > a {
      color: #999;
      font-size: 14px;
      line-height: 44px;
      margin-left: 20px;
      position: relative;
      transition: all 0.3s;
      &::after {
        content: '';
        position: absolute;
        left: 50%;
        transform: translateX(-50%);
        bottom: 0;
        width: 0;
        height: 2px;
        background: #222;
        transition: all 0.3s;
      }
      &.active {
        color: #222;
        &::after {
          width: 14px;
        }
      }
    }
    .logo {
      flex: 1;
      display: flex;
      justify-content: flex-end;
      > img {
        width: 64px;
        height: 28px;
        display: block;
        margin-right: 10px;
      }
    }
  }
}
.article-item {
  .head {
    display: flex;
    img {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      overflow: hidden;
    }
    .con {
      flex: 1;
      overflow: hidden;
      padding-left: 10px;
      p {
        margin: 0;
        line-height: 1.5;
        &.title {
          width: 280px;
        }
        &.other {
          font-size: 10px;
          color: #999;
        }
      }
    }
  }
  .body {
    font-size: 14px;
    color: #666;
    line-height: 1.6;
    margin-top: 10px;
  }
  .foot {
    font-size: 12px;
    color: #999;
    margin-top: 10px;
  }
}
</style>

子组件接收渲染

vue
<script setup lang="ts">
import type { ArticleItem } from 'types/article'

defineProps<{
  item: ArticleItem
}>()
</script>

<template>
  <van-cell class="article-item" @click="navigateTo(`/detail/${item.id}`)">
    <template #title>
      <div class="head">
        <img :src="item.avatar" alt="" />
        <div class="con">
          <p class="title van-ellipsis">{{ item.stem }}</p>
          <p class="other">{{ item.creator }} | {{ item.createdAt }}</p>
        </div>
      </div>
    </template>
    <template #label>
      <div class="body van-multi-ellipsis--l2" v-html="item.content"></div>
      <div class="foot">点赞 {{ item.likeCount }} | 浏览 {{ item.views }}</div>
    </template>
  </van-cell>
</template>

<style lang="less" scoped>
.article-item {
  .head {
    display: flex;
    img {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      overflow: hidden;
    }
    .con {
      flex: 1;
      overflow: hidden;
      padding-left: 10px;
      p {
        margin: 0;
        line-height: 1.5;
        &.title {
          width: 280px;
        }
        &.other {
          font-size: 10px;
          color: #999;
        }
      }
    }
  }
  .body {
    font-size: 14px;
    color: #666;
    line-height: 1.6;
    margin-top: 10px;
  }
  .foot {
    font-size: 12px;
    color: #999;
    margin-top: 10px;
  }
}
</style>

封装 useFetch

  1. 添加 baseURL
  2. 添加 headers
  3. 简化响应结果
ts
import type { UseFetchOptions } from 'nuxt/app'

export const useRequest = async (url: string,options?: UseFetchOptions<any>) => {
  const { data, error } = await useFetch(url, {
    baseURL: 'http://interview-api-t.itheima.net/h5/',
    headers: {
      Authorization: `Bearer ${getToken()}`,
    },
    ...options,
  })

  if (error.value) {
    return Promise.reject(error)
  } else {
    return data.value.data
  }
}

面经列表-分页加载更多

vant-list 列表

jsx
<van-list
  v-model:loading="loading"
  :finished="finished"
  finished-text="没有更多了"
  @load="getList"
>
  <ArticleItemCom v-for="item in list" :key="item.id" :item="item" />
</van-list>

加载完成,重置 loading, 累加数据,处理 finished

jsx
<script setup lang="ts">
const list = ref<ArticleItem[]>([])

const pageParams = reactive({
  current: 40,
  sorter: 'weight_desc',
  pageSize: 10,
})
const loading = ref(false)
const finished = ref(false)

const getList = async () => {
  // 获取数据
  const res = await useRequest('/interview/query', {
    params: pageParams,
    watch: false,
  })
  // 列表追加
  list.value.push(...res.rows)
  // 页码累加
  pageParams.current++
  // 加载状态结束
  loading.value = false
  // 如果当前页码大于总页数,说明没有更多数据了
  if (pageParams.current > res.pageTotal) {
    finished.value = true
  }
}

getList()
</script>

面经列表-推荐和更新 (实战)

1.切换推荐和最新 获取不同的数据

2.切换推荐和最新 点击的tab页签应该高亮

面经详情-动态路由传参-请求渲染

跳转路由传参

修改面经详情的目录结构

jsx
pages/detail.vue  =>   pages/detail/[id].vue

其中 [id].vue  表示动态路由

点击跳转 article.vue

jsx
<template>
  <!-- 文章区域 -->
  <van-cell class="article-item" @click="navigateTo(`/detail/${item.id}`)">
    <template #title>
      ...
    </template>
    <template #label>
      ...
    </template>
  </van-cell>
</template>

页面中获取参数

jsx
// 获取页面参数
const { params } = useRoute()
console.log(params)

静态结构

jsx
<script setup lang="ts">
// 
</script>

<template>
  <div class="detail-page">
    <van-nav-bar
      left-text="返回"
      @click-left="$router.back()"
      fixed
      title="面经详情"
    />
    <header class="header">
      <h1>大标题</h1>
      <p>
        2050-04-06 | 300 浏览量 | 222 点赞数
      </p>
      <p>
        <img src="头像" alt="" />
        <span>作者</span>
      </p>
    </header>
    <main class="body">
      <p>我是内容</p>
      <p>我是内容</p>
      <p>我是内容</p>
      <p>我是内容</p>
    </main>
    <div class="opt">
      <van-icon class="active" name="like-o"/>
      <van-icon name="star-o"/>
    </div>
  </div>
</template>

<style lang="less" scoped>
.detail-page {
  margin-top: 44px;
  overflow: hidden;
  padding: 0 15px;
  .header {
    h1 {
      font-size: 24px;
    }
    p {
      color: #999;
      font-size: 12px;
      display: flex;
      align-items: center;
    }
    img {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      overflow: hidden;
    }
  }
  .opt {
    position: fixed;
    bottom: 100px;
    right: 0;
    > .van-icon {
      margin-right: 20px;
      background: #fff;
      width: 40px;
      height: 40px;
      line-height: 40px;
      text-align: center;
      border-radius: 50%;
      box-shadow: 2px 2px 10px #ccc;
      font-size: 18px;
      &.active {
        background: #FEC635;
        color: #fff;
      }
    }
  }
}
</style>

代码实现

detail.vue

jsx
<script setup lang="ts">

</script>

<template>
  <div class="detail-page">
    <van-nav-bar
      left-text="返回"
      @click-left="$router.back()"
      fixed
      title="面经详细"
    />
    <header class="header">
      <h1>{{ article.stem }}</h1>
      <p>
        {{ article.createdAt }} | {{ article.views }} 浏览量 |
        {{ article.likeCount }} 点赞数
      </p>
      <p>
        <img :src="article.avatar" alt="" />
        <span>{{ article.creator }}</span>
      </p>
    </header>
    <main class="body" v-html="article.content"></main>
    <div class="opt">
      <van-icon :class="{active:article.likeFlag}" name="like-o"/>
      <van-icon :class="{active:article.collectFlag}" name="star-o"/>
    </div>
  </div>
</template>

类型声明

ts
/** * 面经详情 */
export type ArticleDetail = {
  /** 头像 */
  avatar?: string
  /** 浏览量 */
  collectFlag?: number
  /** 面经内容 */
  content?: string
  /** 创建时间 */
  createdAt?: string
  /** 创建人 */
  creator?: string
  /** 主键id */
  id?: string
  /** 点赞量 */
  likeCount?: number
  /** 浏览量 */
  likeFlag?: number
  /** 面经标题 */
  stem?: string
  /** 浏览量 */
  views?: number
}

页面缓存

没有做页面缓存的话,切换页面时会重新发送请求,用户体验不佳,可通过 keepalive 优化体验。

vue
<template>
  <!-- keepalive 设置页面缓存 -->
  <NuxtPage :keepalive="{ max: 10 }" />
</template>

面经详情-点赞收藏 (实战)

detail.vue

调用接口实现点赞收藏

jsx
<template>
  <div class="detail-page">
    <van-nav-bar
      left-text="返回"
      @click-left="$router.back()"
      fixed
      title="面经详细"
    />
    <header class="header">
      <h1>{{ article.stem }}</h1>
      <p>
        {{ article.createdAt }} | {{ article.views }} 浏览量 |
        {{ article.likeCount }} 点赞数
      </p>
      <p>
        <img :src="article.avatar" alt="" />
        <span>{{ article.creator }}</span>
      </p>
    </header>
    <main class="body" v-html="article.content"></main>
    <div class="opt">
      <van-icon @click="toggleLike" :class="{active:article.likeFlag}" name="like-o"/>
      <van-icon @click="toggleCollect" :class="{active:article.collectFlag}" name="star-o"/>
    </div>
  </div>
</template>

<style lang="less" scoped>
.detail-page {
  margin-top: 44px;
  overflow: hidden;
  padding: 0 15px;
  .header {
    h1 {
      font-size: 24px;
    }
    p {
      color: #999;
      font-size: 12px;
      display: flex;
      align-items: center;
    }
    img {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      overflow: hidden;
    }
  }
  .opt {
    position: fixed;
    bottom: 100px;
    right: 0;
    > .van-icon {
      margin-right: 20px;
      background: #fff;
      width: 40px;
      height: 40px;
      line-height: 40px;
      text-align: center;
      border-radius: 50%;
      box-shadow: 2px 2px 10px #ccc;
      font-size: 18px;
      &.active {
        background: #FEC635;
        color: #fff;
      }
    }
  }
}
</style>

我的收藏 (实战)

提供api方法

  • page: 表示当前页
  • optType:2 表示获取我的收藏数据

collect.vue准备结构

jsx
<script setup lang="ts">
// 
</script>

<template>
  <div class="collect-page">
    <van-nav-bar fixed title="我的收藏" />
    <van-list
      v-model:loading="loading"
      :finished="finished"
      finished-text="没有更多了"
      @load="onLoad"
    >
      <ArticleItemCom v-for="item in list" :key="item.id" :item="item" />
    </van-list>
  </div>
</template>

<style lang="less" scoped>
.collect-page {
  margin-bottom: 50px;
  margin-top: 44px;
}
</style>

我的喜欢 (实战)

准备api函数

  • page: 表示当前页
  • optType:1 表示获取我的喜欢数据

Like.vue请求渲染

jsx
<script setup lang="ts">

</script>

<template>
  <div class="like-page">
    <van-nav-bar fixed title="我的点赞" />
    <van-list
      v-model:loading="loading"
      :finished="finished"
      finished-text="没有更多了"
      @load="onLoad"
    >
      <ArticleItemCom v-for="item in list" :key="item.id" :item="item" />
    </van-list>
  </div>
</template>

<style lang="less" scoped>
.like-page {
  margin-bottom: 50px;
  margin-top: 44px;
}
</style>

个人中心 (实战)

页面调用渲染

jsx
<script setup lang="ts">
// 
</script>

<template>
  <div class="user-page">
    <div class="user">
      <img :src="avatar" alt="" />
      <h3>{{ username }}</h3>
    </div>
    <van-grid clickable :column-num="3" :border="false">
      <van-grid-item icon="clock-o" text="历史记录" to="/" />
      <van-grid-item icon="bookmark-o" text="我的收藏" to="/collect" />
      <van-grid-item icon="thumb-circle-o" text="我的点赞" to="/like" />
    </van-grid>

    <van-cell-group class="mt20">
      <van-cell title="推荐分享" is-link />
      <van-cell title="意见反馈" is-link />
      <van-cell title="关于我们" is-link />
      <van-cell @click="logout" title="退出登录" is-link />
    </van-cell-group>
  </div>
</template>

<style lang="less" scoped>
.user-page {
  padding: 0 10px;
  background: #f5f5f5;
  height: 100vh;
  .mt20 {
    margin-top: 20px;
  }
  .user {
    display: flex;
    padding: 20px 0;
    align-items: center;
    img {
      width: 80px;
      height: 80px;
      border-radius: 50%;
      overflow: hidden;
    }
    h3 {
      margin: 0;
      padding-left: 20px;
      font-size: 18px;
    }
  }
}
</style>

打包发布

nuxt 脚手架只是开发过程中,协助开发的工具,当真正开发完了 => 脚手架不参与上线

参与上线的是 => 打包后的源代码

打包:

  • 将多个文件压缩合并成一个文件
  • 语法降级
  • less sass ts 语法解析, 解析成css
  • ....

打包后,可以生成,浏览器能够直接运行的网页 => 就是需要上线的源码!

打包命令

nuxt 脚手架工具已经提供了打包命令,直接使用即可。

bash
pnpm build

.output 中的文件就是打包后的文件,只需要放到服务器中即可。

部署上线

  • 部署公司的服务器
  • 部署到第三方平台,如:vercel

根据 MIT 许可证发布