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
初始化项目
下载命令
npx nuxi init <project-name>
其中 <project-name>
换成自己的项目名称。
下载问题
由于大陆访问受限,所以下载可能会失败。
解决方案参考:修改 host 文件
Windows 系统
C:\Windows\System32\drivers\etc
# 增加以下代码
185.199.108.133 raw.githubusercontent.com
映射关系为访问 raw.githubusercontent.com
映射到 IP 地址 185.199.108.133
。
注意:其中的 # 表示注释,映射关系行首不能有 # ,否者不生效。
Mac 系统
参考链接:
运行项目
- 进入项目目录,并安装项目依赖
npm install
。 - 启动项目,
npm run dev
。 - ✨ 浏览器访问 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 项目的目录结构。
├─.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 文件会自动注册路由。
├─ pages 页面目录,自动注册路由
Nuxt.js 自带路由功能,不需要额外安装和配置,无需安装 vue-router 。
参考目录
├─ pages 页面目录,自动注册路由
│ └─index.vue 主页
│ └─user.vue 用户页
├─app.vue 根组件
参考代码
<template>
<!-- 路由链接 -->
<NuxtLink to="/">首页</NuxtLink>
<NuxtLink to="/user">用户页</NuxtLink>
<!-- 路由占位 -->
<NuxtPage />
</template>
<NuxtPage>
相当于<RouterView>
<NuxtLink>
相当于<RouterLink>
项目实战 - Nuxt 版面经
项目资料
项目收获
- SEO 优化
- 组件库 @vant/nuxt 使用
- 移动端 vw 适配
- 路由系统
- 路由中间件
- 组件复用
- 布局复用
SEO 优化
通过设置网页 title 和 description 等 SEO 优化信息,由服务端渲染。
<script setup lang="ts">
// SEO 优化
useSeoMeta({
title: '黑马面经 - 找工作神器|笔试题库|面试经验',
description:
'求职之前,先上黑马面经,就业找工作一站解决。全面提升求职竞争力,找到好工作,拿到好offer。',
author: '黑马程序员',
})
</script>
@vant/nuxt 组件库
- 安装 nuxt 版 vant-ui
pnpm i @vant/nuxt
- 添加配置
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: false },
modules: ['@vant/nuxt'],
})
- 使用
<van-button type="primary">主要按钮</van-button>
<van-button type="info">信息按钮</van-button>
PS: 在 Nuxt 项目中,vant 组件会自动按需导入(需重启)。
项目中的 vw 适配
安装依赖
pnpm i postcss-px-to-viewport
添加配置
// 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 路由中间件实现路由重定向,包括导航守卫等路由功能。
export default defineNuxtRouteMiddleware((to, from) => {
if (to.path === '/') {
return navigateTo('/article')
}
})
参考链接
layouts 布局
通过 layout 实现布局的复用,新建文件 layouts/tabbar.vue
。
- route 开启路由模式
- to 跳转路由
<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 布局
<template>
<NuxtLayout name="tabbar">
<h1>面经首页</h1>
</NuxtLayout>
</template>
修改主题色
在 app.vue 的样式全局生效。
<style>
:root {
--van-primary-color: #fa6d1d !important;
}
</style>
参考链接
注册页
使用组件,在 Nuxt3 中无需导入。
- van-nav-bar
- van-form
- van-field
- van-button
静态结构
register.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 发送。
pnpm i axios
新建 utils/request.ts
封装 axios 模块
/* 封装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 表单提交调用接口
<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
<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 代替。
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 代替。
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
// 表单提交
const onSubmit = async () => {
// 登录请求
const { data } = await loginAPI(form)
// 保存 token
setToken(res.data.token)
// 成功提示
showSuccessToast('登录成功')
// 跳转页面
navigateTo('/')
}
参考链接
路由鉴权-导航守卫
Nuxt 有路由中间件,简化了导航守卫的实现。
// 白名单列表,记录无需权限访问的所有页面
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')
}
})
参考链接
面经列表-动态渲染列表
- 通过 axios 发送请求
- 通过 useFetch 发送请求 - 推荐
article.vue
存储数据
<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>
类型声明
/** 面经列表项 */
export type ArticleItem = {
/** 主键id */
id: string
/** 头像 */
avatar: string
/** 面经内容 */
content: string
/** 创建时间 */
createdAt: string
/** 创建人 */
creator: string
/** 点赞量 */
likeCount: number
/** 面经标题 */
stem: string
/** 浏览量 */
views: number
}
v-for循环展示
<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>
子组件接收渲染
<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
- 添加 baseURL
- 添加 headers
- 简化响应结果
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
}
}
面经列表-分页加载更多
<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
<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页签应该高亮
面经详情-动态路由传参-请求渲染
跳转路由传参
修改面经详情的目录结构
pages/detail.vue => pages/detail/[id].vue
其中 [id].vue 表示动态路由
点击跳转 article.vue
<template>
<!-- 文章区域 -->
<van-cell class="article-item" @click="navigateTo(`/detail/${item.id}`)">
<template #title>
...
</template>
<template #label>
...
</template>
</van-cell>
</template>
页面中获取参数
// 获取页面参数
const { params } = useRoute()
console.log(params)
静态结构
<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
<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>
类型声明
/** * 面经详情 */
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 优化体验。
<template>
<!-- keepalive 设置页面缓存 -->
<NuxtPage :keepalive="{ max: 10 }" />
</template>
面经详情-点赞收藏 (实战)
detail.vue
调用接口实现点赞收藏
<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
准备结构
<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
请求渲染
<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>
个人中心 (实战)
页面调用渲染
<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 脚手架工具已经提供了打包命令,直接使用即可。
pnpm build
.output 中的文件就是打包后的文件,只需要放到服务器中即可。
部署上线
- 部署公司的服务器
- 部署到第三方平台,如:vercel