需求
目标:
我想搭建一个nuxt3的框架,实现一些基本的组件和路由、页面,方便后续遇到相关ssr项目直接复用。
同时:
记录关于nuxt3的使用介绍
关于Nuxt(详解以及周边)
Nuxt 框架
1、一种基于 Node.js 的服务端渲染方案 SSR(Server Side Rendering)
支持 Vue 应用、服务器端渲染、提高页面的加载速度和 SEO
工作原理:
1、服务端渲染:Nuxt.js 在服务器端执行 Vue 组件的渲染过程,并生成初始 HTML
2、客户端激活:一旦初始 HTML 被发送到浏览器,Vue.js 会接管页面,并在客户端激活成一个交互式应用程序。
Nuxt 优势
SPA 和 SSR 是什么?
1、SPA(Single Page Application)单页面应用,在客户端通过 JS 动态地构建页面。
优势:页面切换流畅,动态渲染变化,用户体验好,搜索引擎的支持不够友好。
适用企业内部项目,如管理平台。
2、SSR(Server-Side Rendering)服务器端渲染,在服务器端生成 HTML 页面并发送给客户端。
优势:对搜索引擎友好,页面首次加载速度快 和 SEO,页面切换可能不够流畅(每次都是请求一个完整的 HTML 页面)
适用官网,对 SEO 搜索友好。
3、Nuxt 框架
Nuxt 采用了混合的架构模式,可以同时支持 SSR 和 SPA。
首次访问页面是 SSR 方式,也就是在服务器端生成 HTML 页面并发送给客户端
后续的页面切换则使用 SPA 的方式进行,从而兼顾了 SSR 和 SPA 的优点。
场景:企业网站、商品展示 等 C 端网站,对 SEO 搜索更友好,且页面切换流畅,用户体验更好
项目搭建
1、安装项目命令和报错分析
npx nuxi init <project-name>
错误分析
1、安装报以上错误(其原因是大陆访问受限)
2、但是我开了代理也一样下载不下来
3、从官网issue159:原因是脚手架node程序没有走代理
错误解决方案
1、官网zip
2、在终端添加host代理,进行DNS域名映射
mac修改方式如下:
sudo vi /etc/hosts
输入 i 进入编辑模式,编辑完成后按 Esc后:wq 保存退出
添加
185.199.108.133 raw.githubusercontent.com
Windows修改host参考:修改本地host代理
运行
npm run dev
or
npm i
npm run dev
目录以及初始化
1、先来理解下目录
├─.nuxt 非工程代码,存放运行或发行的编译结果
├─node_modules 项目依赖
├─public 网站资源目录
├─server 接口目录
├─.gitignore git 忽略文件
├─.npmrc npm 配置文件
├─app.vue 根组件
├─nuxt.config.ts nuxt 配置文件
├─package.json 项目配置文件
├─README.md 项目说明文件
└─tsconfig.json ts 配置文件
2、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>
### SEO 优化
实现:
通过设置网页 title 和 description 等 SEO 优化信息,由服务端渲染
SEO and Meta
// 在app.vue中新增
<script setup lang="ts">
// SEO 优化
useSeoMeta({
title: 'demo - 找工作|博客|提升',
description:
'求职之前,需要有经验,技术。祝愿大家找到好工作,拿到好offer。',
author: '卡卡——程序员',
})
</script>
组件库和vw适配
安装 nuxt 版 vant-ui
npm i @vant/nuxt
nuxt.config.ts添加配置
// https://nuxt.com/docs/api/configuration/nuxt-config
// PS: 在 Nuxt 项目中,vant 组件会自动按需导入(需重启)
export default defineNuxtConfig({
devtools: { enabled: false },
modules: ['@vant/nuxt'],
})
index.vue测试
<van-button type="primary">主要按钮</van-button>
<van-button type="info">信息按钮</van-button>
vw 适配
安装
npm i postcss-px-to-viewport
nuxt.config.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,
},
},
},
})
页面效果px会转化为vw
路由配置
自动路由
在 pages 目录的 vue 文件会自动注册路由,创建以下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 布局
目标:我们想在首页底部实现TabBar导航栏
通过 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 布局
article.vue
<template>
<NuxtLayout name="tabbar">
<h1>文章首页</h1>
</NuxtLayout>
</template>
为这几个路由分别配置
修改主题色
在 app.vue 的样式全局生效。
<style>
:root {
--van-primary-color: #fa6d1d !important;
}
</style>
注册页
直接复用、一些基础vant组件使用
<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>
<script setup lang="ts">
// 表单数据
const form = reactive({
username: 'admin',
password: '123456',
})
// 表单提交
const onSubmit = async () => {
//
}
</script>
<style scoped>
.link {
color: #069;
font-size: 12px;
padding-right: 20px;
float: right;
}
</style>
请求封装axios
安装 axios,登录请求可用 axios 发送。
npm i axios
新建 utils/request.ts 封装 axios 模块
nuxt3 utils模块
/* 封装axios用于发送请求 */
import axios, { AxiosResponse } from 'axios'
const service = axios.create({
baseURL: 'http://interview-api-t.itheima.net/h5',
timeout: 90000,
})
const request = (config: any) => {
// if (getToken()) {
// config.headers['csrf-token'] = getToken()
// }
if (config.cType) {
config.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'
}
return config
}
const requestError = (error: any) => {
console.error(error)
return Promise.reject(error)
}
const response = (response: AxiosResponse) => {
return response.data
}
const responseError = async (error: any) => {
if (error.response.status === 403) {
showFailToast('登录过期')
navigateTo('/login')
return Promise.reject(error)
}
if (error.response.data.code === 400) {
showFailToast(error.response.data.message)
return Promise.reject(error)
}
if (error.response.status === 500) {
// No permission
return Promise.reject(error)
}
return Promise.reject(error)
}
service.interceptors.request.use(request, requestError)
service.interceptors.response.use(response, responseError)
export default service
// register.vue 表单提交调用接口
// 表单提交
const onSubmit = async () => {
// 注册请求
await request({
method: 'POST',
url: '/user/register',
data: form,
})
// 成功提示
showSuccessToast('注册成功')
// 跳转页面
navigateTo('/login')
}
持久化存储 use-cookie
注意:Nuxt 服务器端不支持 localStorage 所以运行会报错,可通过 cookie 代替。
而且Nuxt支持 useCookie API存储cookie
在util中新建cookie.ts
const KEY = 'key'
// 获取
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
}
登录页逻辑
<script setup lang="ts">
// 表单数据
const form = reactive({
username: 'admin111',
password: '123456',
})
// 表单提交
const onSubmit = async () => {
// 登录请求
const res = await request({
method:"post",
url:'/user/login',
data:form
})
// 保存 token
setToken(res.data.token)
// 成功提示
showSuccessToast('登录成功')
// 跳转页面
navigateTo('/', {replace: true})
}
</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 有路由中间件,简化了导航守卫的实现。use-router
// middleware下的route.global.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')
}
})
渲染页面-封装useFetch 通过 useFetch 发送请求
为什么封装useFetch发送请求?
1、服务端先渲染静态页面,axios请求接收到数据需要客户端进行渲染页面。
2、useFetch发送的请求直接在服务端处理好,随着页面一起回来。利于seo
// 注意、函数组件都不用引入,可以直接使用,但type类型需要导入才能使用
export const useRequest = async (url: string,options?: 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
}
}
补充
其中 [id].vue 表示动态路由为detail/:id的路由