目录
项目首页开发
项目准备✌️
样式开发
防抖
底部横条✌️
登陆注册功能开发
样式编写
路由守卫实现基础登录校验功能☝️
使用 axios 发送登录 Mock 请求
请求函数的封装
通过代码拆分增加逻辑可维护性
本项目的源代码在文章末尾哦
我们先看一下在本文中我们这个项目要做的页面,分别是首页,登录和注册页面:
项目首页:
登录页面:
注册页面和登录页面相似,这里就不展示了。在本文中我们会完成京东到家项目首页和登录注册页面的样式开发,其中会用到 element-plus 组件库,登录注册会使用 axios 发送 Mock 请求来实现,贴近真实项目开发。
我们通过脚手架已经构建好了项目,如果还不会怎么搭建vue3项目的同学,可以看看我的这篇博客,对vue3有个大概的了解:
Vue3全家桶入门 (通过vue-cli脚手架搭建todolist项目环境,深入vue3.0核心知识)https://blog.csdn.net/qq_49900295/article/details/124726599?spm=1001.2014.3001.5501首先,在 main.js 中引入项目需要的依赖:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import 'normalize.css'
import './style/base.scss'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(store)
app.use(router)
app.use(ElementPlus)
app.mount('#app')
这里 normalize.css 和 element-plus 需要我们先安装:
npm install normalize.css --save
npm install element-plus --save
normalize.css 是css的初始化文件,它在默认的HTML元素样式上提供了跨浏览器的高度一致性,总之按装它就完事了,element-plus在我们的项目中可以用到一些弹窗,引入它会非常方便。
除了 normalize.css 外,在 main.js 里我们还引入了 base.scss,在这里我们设置了 html 与 body 的字号,方便在样式中使用rem来实现响应式的字体:
html {
font-size: 100px;
}
body {
font-size: .12rem;
}
下面是项目的 style 目录:
在 viriables.scss 文件中我们主要来定义一些颜色变量:
$content-fontcolor: #333;
$content-bgcolor: #F1F1F1;
因为在各个组件中使用这两个颜色特别多,所以我们可以把它单独拿出来,通过变量的形式我们就可以统一更改颜色,这样就非常方便。
下面是 App.vue 中的代码,我们把里面的内容都删掉,我们并不需要 router-link,我们只需要 router-view 渲染子路由就行:
这是首页的目录结构:
这里HomeView相当于首页的根组件,也就是相当于一个容器里面放着 FooTer,NearBy,StaticView三个子组件,从下图可以看出这三个子组件负责的区域。他们共同构建出了首页的样式。
我们看一下 HomeView 中的代码:
我们在 HomeView 中引入三个子组件然后在视图模板中使用引入的子组件。记得要在父组件的 components 中声明引入的子组件。wrapper是 StartView与NearBy组件的容器。
在 wrapper 的样式中有一个 overflow-y ,如果不加这个样式的话,首页在拖动滚动条时,Footer 底部就会变成这样:
加上 overflow-y 后表示可以在y轴放心滚动。
在修改 FooTer 组件样式时,我们想给底部字体10px大小,但是浏览器里默认我们的最小字号是12px,强制修改10px也只会显示12px,我们应该这么修改:
.docker__title {
font-size: .2rem;
transform: scale(.5,.5);
transform-origin: center top;
}
通过 transform 让元素缩放的方式改变大小,最后要设置它的变换中心点,我们设置水平居中,垂直靠着顶部就正好
当我们在做到顶部的一个轮播图的时候,因为图片要从服务器中读取,所以加载速度很慢,就会产生一个抖动的现象。比如在图片下面加个文字,看一下网页的加载过程:
因为图片加载的太慢,所以下面的内容会产生这种抖动,我们通过css的方法去解决它:
.banner {
height: 0;
overflow: hidden;
padding-bottom: 25.4%;
&__img {
width: 100%;
}
}
我们先计算一下图片的高宽比,得出是0.254,那这里给个底部内边距百分之25.4指的就是屏幕宽度的百分之25.4,又因为图片的宽度和屏幕宽度一样,所以这就相当于图片的高度。所以我们这么想,就在图片没加载出来之前就会把位置占住。这样就实现了防抖效果。
现在我们想给一个底部的条条这个该怎么做呢?这里的难点是我们最外层的盒子有一个padding值,所以我们的横条就不会占满宽度,这个问题应该怎么解决呢?很简单,把这个条条的外边距设置为负的就行:
.gap {
margin: 0 -.18rem;
height: .1rem;
background: #F1F1F1;
}
在 views 目录下的LoginView和RegisterView分别表示登录和注册的页面。
先编写一下基础代码:
hello world
我们先写一下它的视图模板:
立即注册
下面我们再写登陆页面的样式:
.wrapper {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
right: 0;
&__img {
display: block;
margin: 0 auto .4rem auto;
width: .66rem;
height: .66rem;
}
&__input {
box-sizing: border-box;
height: .48rem;
margin: 0 .4rem .16rem .4rem;
background: #F9F9F9;
padding: 0 .16rem;
border: 1px solid rgba(0, 0, 0, .1);
border-radius: 6px;
&__content {
width: 100%;
border: 0;
background: none;
outline: none;
line-height: .48rem;
font-size: .16rem;
color: rgba(0, 0, 0, .5);
&::placeholder {
color: rgba(0, 0, 0, .5);
}
}
}
&__login-button {
height: .48rem;
line-height: .48rem;
margin: .32rem .4rem .16rem .4rem;
text-align: center;
background: #0091FF;
box-shadow: 0 .04rem .08rem 0 rgba(0,145,255,0.32);
border-radius: .04rem;
color: #fff;
}
&__login-link {
text-align: center;
font-size: .14rem;
color: rgba(0, 0, 0, .5);
}
}
现在我们做一些路由跳转,先修改一下 router 里的 index.js 中的内容:
import { createRouter, createWebHashHistory } from 'vue-router'
import HomeView from '../views/home/HomeView.vue'
import LoginView from '../views/login/LoginView.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/login',
name: 'login',
component: LoginView
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
现在我们想实现一个功能,只有当登陆的时候才能访问首页,否则禁止访问首页
我们在 index.js 中通过 beforeEach 实现这个功能,先做一下简单的输出:
router.beforeEach((to, from, next) => {
console.log(to, from)
next()
})
这里 to 指的是要跳转到哪个页面的信息,from 指的是从哪个页面跳转的信息。router.beforEach 的意思就是每次在路由跳转前都要执行这个方法。
我们每次在跳转前先判断是否登录,如果用户之前登录过就跳转到对应的页面,否则就跳转到登陆页面。但是如果用户没有登录他跳转到登录页面的时候还会 router.beforEach 进行判断,这样他又会跳到登陆页面,这样会一直循环,所以我们在 if 中还得加一个条件,如果跳转的是登陆页面的话就允许跳转。
router.beforeEach((to, from, next) => {
const isLogin = false
if (isLogin || to.name === 'login') {
next()
} else {
next({ name: 'login' })
}
})
现在我们的页面默认就会跳转到登陆页面,就算在浏览器路径中让它跳到首页,他也不会跳转。
现在我们就去实现当用户点击登陆时候的跳转功能,在 LoginView 里,先给按钮一个点击事件:
当点击登陆时,会先判断输入的电话号码是否符合格式,符合与不符合都会有相应的弹窗(这里弹窗是element-plus的组件),当符合格式时就让 isLogin 为 ture,表示在登陆状态:
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
export default {
name: 'LoginView',
setup () {
const router = useRouter()
let number = ref('')
let password = ref('')
let handleLogin = () => {
let reg = /^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$/
if (reg.test(number.value)) {
open2()
localStorage.isLogin = true
setTimeout(() => {
router.push({ name: 'home' })
}, 2000)
} else {
open()
}
}
let open = () => {
ElMessage({
message: '您输入的电话号码格式错误',
type: 'error',
duration: 2000
})
}
let open2 = () => {
ElMessage({
message: '登陆成功欢迎您',
type: 'success',
duration: 2000
})
}
return {
handleLogin,
number,
password,
open,
open2,
router
}
}
}
然后我们完善一下 router 下 登陆路由的配置:
path: '/login',
name: 'login',
component: LoginView,
beforeEnter (to, from, next) {
const isLogin = localStorage.isLogin
if (isLogin) {
next({ name: 'home' })
} else {
next()
}
}
路由的 beforeEnter 事件就是当跳转路由之前执行的,如果我们当前已经登录成功,跳转到首页了。如果我们想跳转回登录页面的话就会来到 beforeEnter 判断,登陆成功后 isLogin 这个登录状态就是 true,这样就不会再退回到登录页面,还会跳转到首页。
在完成登录页面后,我们照猫画虎继续完成注册页面,把登录页面的内容复制一份,注册页面就比登录页面多了个确认密码,然后去 index.js 中配置路由信息。
只不过我们还得完成登录与注册页面之间的切换,在登录页面点击立即注册后跳转到注册页面:
let handleRegisterClick = () => {
ElNotification({
title: '尊敬的用户您好',
message: h('i', { style: 'color: teal' }, '正在跳转到注册页面'),
duration: 800
})
setTimeout(() => {
router.push({ name: 'register' })
}, 900)
}
在登录页面单击立即注册时,通过 element-plus 弹框提示,然后跳转路由,注册页面也是相同的操作。
现在我们的登录注册都是前端模拟出来的,现在我们要学习如何在前端调用接口,和后端做交互。
在我们的登录组件中,如果用户点击登录了,我们就让登录状态为 true,显然这是不合理的,我们应该先向后端发送请求,如果用户名和密码匹配,才允许登录将状态改为 true。
那我们首先安装一下 axios:
npm install axios ---save
把他引入到登录组件中:
import axios from 'axios'
这里的后端接口是我们通过 fastmock 模拟的后端接口,大家可以通过这个 url 来得到我们这个项目需要用到的数据,下面是我们这个项目的接口文档,可以看到登录是通过发送 post 请求来实现的,后面跟着的有登录的接口地址:
下面是我们这个项目的接口文档的地址:
https://www.fastmock.site/mock/ae8e9031947a302fed5f92425995aa19/jd/
我们给登录按钮加一个点击事件,然后在这个函数里实现我们想要的功能,这里加上 setTimeout 是因为 element-plus 的弹窗有两秒的时间,然后两秒后我们再实现登录跳转就更贴近实际一些。
post 请求中我们还需要把请求的数据返给后端接口, 下面就是登录事件的代码:
let handleLogin = () => {
axios.post('https://www.fastmock.site/mock/ae8e9031947a302fed5f92425995aa19/jd/api/user/login', {
username: username,
password: password
}).then(() => {
open2()
localStorage.isLogin = true
setTimeout(() => {
router.push({ name: 'home' })
}, 2000)
}).catch(() => {
open()
})
}
现在当我们点击登录时,就模拟了一个post请求,把输入的内容返回给后端接口,这里的接口只是用来模拟请求过程,并不是我们项目的真实后端接口。
注意要把返回内容的格式设置为 json ,因为 fastmock 是这么要求的。
vue3 已经支持 async 和 await 这样的语法,我们重新写一下上一节的代码,输出一下返回的结果:
let handleLogin = async () => {
const result = await axios.post('https://www.fastmock.site/mock/ae8e9031947a302fed5f92425995aa19/jd/api/user/login', {
username: username,
password: password
// }).then(() => {
// open2()
// localStorage.isLogin = true
// setTimeout(() => {
// router.push({ name: 'home' })
// }, 2000)
// }).catch(() => {
// open()
// })
})
console.log(result)
}
启动项目,点击登录,result就在控制台中输出出来了:
这里data中的 errno 表示返回的错误个数,如果是0就表示请求发送成功,那么我们继续完善代码:
let handleLogin = async () => {
const result = await axios.post('https://www.fastmock.site/mock/ae8e9031947a302fed5f92425995aa19/jd/api/user/login', {
username: username,
password: password
})
if (result.data.errno === 0) {
localStorage.isLogin = true
open2()
setTimeout(() => {
router.push({ name: 'home' })
}, 2000)
} else {
open()
}
}
现在我们故意把 url 地址写错:
启动项目,看看有什么效果:
控制台的网络这块就报错了,但是并没有弹窗,这是为什么呢?
因为异常会在 await 那里抛出,不会走下面的代码了,自然不会执行弹窗语句,那我们通过 try catch就能解决这个问题:
let handleLogin = async () => {
try {
const result = await axios.post('https://www.fastmck.site/mock/ae8e9031947a302fed5f92425995aa19/jd/api/user/login', {
username: username,
password: password
})
if (result.data.errno === 0) {
localStorage.isLogin = true
open2()
setTimeout(() => {
router.push({ name: 'home' })
}, 2000)
} else {
open()
}
} catch (e) {
open3()
}
}
这样在输入错误的 url 时,他就会提示我们请求失败了:
现在我们每发一个请求都要写一段很长的请求地址,后面请求别的接口还得再写这样一段代码,那我们就把它封装一下。
我们在 src 目录下新建一个 utils 文件,在 request 里封装 post 请求:
我们对 post 请求做了一个封装:
import axios from 'axios'
export const post = (url, data = {}) => {
return new Promise((resolve, reject) => {
axios.post(url, data, {
baseURL: 'https://www.fastmock.site/mock/ae8e9031947a302fed5f92425995aa19/jd',
headers: {
'Content-Type': 'application/json'
}
}).then((response) => {
resolve(response)
}, err => {
reject(err)
})
})
}
这样在 Login 中我们就不需要引入 axios 了,直接把 post 方法引入进来就行:
import { post } from '../../utils/request'
现在我们的弹窗用的是 element plus 提供的组件,但是用的多了的时候就会有大量冗余的代码,下面我们对他也进行一下封装:
let alertmessage = (thecontent, thetype, theduration) => {
ElMessage({
message: thecontent,
type: thetype,
duration: theduration
})
}
这样当我们用到弹窗这个功能时,直接指定参数就行了:
在 register 组件里我们点击注册的时候也是发送 post 请求,和登录页面的实现逻辑相同,只是请求的接口不一样,接口地址在上面的接口文档中有,大家可以自行查看,在这一节中我们封装了请求函数,然后在登录页面和注册页面中使用我们封装的这个函数实现了数据的请求。
现在我们的登录注册页面的功能就大致完成了,启动项目看看最终的效果:
现在我们把各种函数都放在了 setup 中,这样做肯定没有出错,但是这样会让我们的 setup 函数非常长,如果项目做到后面我们要在里面找某一个函数或者变量的时候,都很麻烦,如果把关于登录逻辑的数据和方法都放在 setup 外面的一个函数中,关于注册逻辑的数据和方法放在另一个函数中,这样再把这些函数在 setup 中接收,在 setup 中我们只关心整个页面的实现逻辑就行,这样整个代码的维护性和可读性都大大提高了。
在登录页面我们把向后端发送登录请求的相关逻辑从 setup 中抽离出来:
const useLoginEffect = () => {
const router = useRouter()
let username = ref('')
let password = ref('')
let handleLogin = async () => {
if (username.value === '' || password.value === '') {
alertmessage('输入内容不能为空', 'warning', 1500)
return
}
try {
const result = await post('/api/user/login', {
username: username,
password: password
})
if (result.data.errno === 0) {
localStorage.isLogin = true
alertmessage('登录成功欢迎您', 'success', 2000)
setTimeout(() => {
router.push({ name: 'home' })
}, 2000)
} else {
alertmessage('登录失败', 'error', 2000)
}
} catch (e) {
alertmessage('请求失败', 'error', 2000)
}
}
return { username, password, handleLogin }
}
这里我们重新定义了一个useLoginEffect 函数,然后把需要用到的数据和方法都放进来,最后通过 return 把数据和方法返回出来,以便在 setup 中接收。
我们再把点击注册这个函数的相关逻辑抽离出来:
const useRegisterEffect = () => {
const router = useRouter()
let handleRegisterClick = () => {
ElNotification({
title: '尊敬的用户您好',
message: h('i', { style: 'color: teal' }, '正在跳转到注册页面'),
duration: 800
})
setTimeout(() => {
router.push({ name: 'register' })
}, 900)
}
return { handleRegisterClick }
}
这样我们就把登录页面相关的功能都从 setup 里面抽离了出来,现在再看 setup 里的代码就优雅了许多,浅显易懂:
setup () {
const { username, password, handleLogin } = useLoginEffect()
const { handleRegisterClick } = useRegisterEffect()
return {
handleLogin,
username,
password,
handleRegisterClick
}
}
在 setup 里我们很清晰的知道这个页面的实现逻辑,如果想修改跳转登录这个函数就去对应的函数里修改就可以,方便了很多。
在注册页面的代码拆分和登录页面的相同,这里就不过多阐述。在本文中我们暂时完成了项目首页和登陆注册页面的样式,实现了登陆注册时向后端发送请求获取数据的功能,最后通过代码拆分增加逻辑可维护性。下一篇文章我们会实现商家展示功能的开发,大家记得关注哦!
项目代码地址:
https://gitee.com/jie_shao1112/jingdong-homehttps://gitee.com/jie_shao1112/jingdong-home