学习前提
会基本的vue,会使用AI助手,学习本课本的人必须长得帅。
使用脚手架初始化搭建项目
安装UI组件库
参考最新的UI框架
自动引入配置
npm install -D unplugin-vue-components unplugin-auto-import
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})
安装icon
npm install @element-plus/icons-vue
注册所有图标
您需要从 @element-plus/icons-vue
中导入所有图标并进行全局注册。
// main.ts
// 如果您正在使用CDN引入,请删除下面一行。
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
这样我们就可以直接在vue文件使用icon
<template>
<el-icon>
<avatar />
</el-icon>
</template>
构建登录页面 UI 结构
这里不做多说,体力活。
样式处理
创建全局的 style
,我们使用sass。我们安装一下
npm install -D sass-embedded
找的代码库里面的vue-admin/src/assets/main.css 改成 mian.scss然后在main里面引入。
输入下面代码
html,
body {
height: 100%;
margin: 0;
padding: 0;
background-color: red;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB,
Microsoft YaHei, Arial, sans-serif;
}
#app {
height: 100%;
}
*,
*:before,
*:after {
box-sizing: inherit;
margin: 0;
padding: 0;
}
a:focus,
a:active {
outline: none;
}
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}
div:focus {
outline: none;
}
.clearfix {
&:after {
visibility: hidden;
display: block;
font-size: 0;
content: ' ';
clear: both;
height: 0;
}
}
然后遇到报错了
问了AI之后,可能是下面的问题
我们先从新启动项目。但是没解决。
那解决一下vite配置呢,先官网找一下。
一不小心就被找到了。
查询到vite本身就支持scss,我们只需要安装scss的包就行。
npm i sass
然后对vite进行配置
页面出现了红色背景,说明样式已经OK了。
接下来就是完善login页面,详情看代码库文件 src/views/LoginView.vue
Icon 图标处理方案:SvgIcon
在我们的项目中所使用的 icon
图标,一共分为两类:
element-plus
的图标- 自定义的
svg
图标
这也是通常情况下企业级项目开发时,所遇到的一种常见情况。
对于 element-plus
的图标我们可以直接通过 el-icon
来进行显示,但是自定义图标的话,我们暂时还缺少显示的方式,所以说我们需要一个自定义的组件,来显示我们自定义的 svg
图标。
那么这种自定义组件处理 自定义 svg
图标的形式,就是我们在面临这种问题时的通用解决方案。
那么对于这个组件的话,它就需要拥有两种能力:
- 显示外部
svg
图标 - 显示项目内的
svg
图标
基于以上概念,我们可以创建出以下对应代码:
创建 components/SvgIcon/index.vue
:
<template>
<div
v-if="isExternal"
:style="styleExternalIcon"
class="svg-external-icon svg-icon"
:class="className"
/>
<svg v-else class="svg-icon" :class="className" aria-hidden="true">
<use :xlink:href="iconName" />
</svg>
</template>
<script setup>
import { isExternal as external } from '@/utils/validate'
import { defineProps, computed } from 'vue'
const props = defineProps({
// icon 图标
icon: {
type: String,
required: true
},
// 图标类名
className: {
type: String,
default: ''
}
})
/**
* 判断是否为外部图标
*/
const isExternal = computed(() => external(props.icon))
/**
* 外部图标样式
*/
const styleExternalIcon = computed(() => ({
mask: `url(${props.icon}) no-repeat 50% 50%`,
'-webkit-mask': `url(${props.icon}) no-repeat 50% 50%`
}))
/**
* 项目内图标
*/
const iconName = computed(() => `#icon-${props.icon}`)
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover !important;
display: inline-block;
}
</style>
创建 utils/validate.js
:
/**
* 判断是否为外部资源
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
在 views/login/index.vue
中使用 外部 svg
(https://res.lgdsunday.club/user.svg
):
<span class="svg-container">
<svg-icon icon="https://res.lgdsunday.club/user.svg"></svg-icon>
</span>
外部图标可正常展示。
配置@绝对路径引入
直接在vite里面配置就行,下面的AI的回答。
import path from 'path'
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, '/src'), // 假设您的源代码在 src 目录
},
},
})
处理内部 svg 图标显示
本地图标需要在vue全局注册,因为我们使用的是vite。下面的方案,同样是AI给我的。
npm install vite-plugin-svg-icons -D
vite文件里面配置
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'
export default () => {
return {
plugins: [
createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
symbolId: 'icon-[dir]-[name]',
}),
],
}
}
配置 main.ts
import 'virtual:svg-icons-register'
-
在
login/index.vue
中使用SvgIcon
引入本地svg
// 用户名 <svg-icon icon="user" /> // 密码 <svg-icon icon="password" /> // 眼睛 <svg-icon icon="eye" />
-
此时 处理内容
svg
图标的代码 已经完成。
完善登录表单校验
- 密码框小眼睛
对于密码框存在两种状态,密文状态,明文状态。点击眼睛可以进行切换。
该功能实现为通用的处理方案,只需要动态修改input的type类型即可,其中: password为密文显示,text为明文显示 根据以上理论,即可得出以下代码: 写一个变量+点击事件控制就完事。
<el-form-item prop="password">
<span class="svg-container">
<svg-icon icon="password" />
</span>
<el-input
v-model="form.password"
placeholder="password"
name="password"
:type="passwordType"
/>
<span class="show-pwd">
<svg-icon
:icon="passwordType === 'password' ? 'eye' : 'eye-open'"
@click="onChangePwdType"
/>
</span>
</el-form-item>
import { reactive, ref } from 'vue'
const passwordType = ref('password')
const onChange
PwdType = () => {
passwordType.value = passwordType.value === 'password' ? 'text' : 'password'
}
配置环境变量封装 axios 模块
npm i axios
安装起来,宝贝。
首先我们先去完成第一步:封装 axios
模块。
在当前这个场景下,我们希望封装出来的 axios
模块,至少需要具备一种能力,那就是:根据当前模式的不同,设定不同的 BaseUrl
,因为通常情况下企业级项目在 开发状态 和 生产状态 下它的 baseUrl
是不同的。
对于 @vue/cli
来说,它具备三种不同的模式:
development
test
production
具体可以点击 这里 进行参考。
根据我们前面所提到的 开发状态和生产状态 那么此时我们的 axios
必须要满足:在 开发 || 生产 状态下,可以设定不同 BaseUrl
的能力
那么想要解决这个问题,就必须要使用到 @vue/cli
所提供的 环境变量 来去进行实现。
我们可以在项目中根目录文件夹下面,创建两个文件:
.env.development
.env.production
它们分别对应 开发状态 和 生产状态。
我们可以在上面两个文件中分别写入以下代码:
.env.development
:
# 标志
# just a flag
ENV = 'development'
# base api
VITE_APP_BASE_API = '/api'
.env.production
:
# 标志
ENV = 'production'
# base api
VITE_APP_BASE_API = '/prod-api'
有了这两个文件之后,我们就可以创建对应的 axios
模块
创建 utils/request.js
,写入如下代码:
import axios from 'axios'
const service = axios.create({
baseURL: process.env.VITE_APP_BASE_API,
timeout: 5000
})
export default service
但是会出现ReferenceError: process is not defined
的错误通常意味着你的代码在尝试访问process
对象,但是该对象在你当前的执行环境中并不可用。
要解决这个问题,你可以尝试以下步骤:
确保.env
文件存在并正确配置: 在项目根目录下,确保你有一个.env
文件(或者.env.development
、.env.production
等,具体取决于你的环境),并且它包含了VUE_APP_BASE_API
这个环境变量。
检查Vite
配置: 由于你的项目使用的是Vite
,而不是Vue CLI
,你需要在Vite
配置中确保环境变量被正确加载。在Vite
中,只有以VITE_
开头的环境变量才会被暴露给你的应用程序。因此,你可能需要将VUE_APP_BASE_API
更名为VITE_APP_BASE_API
,并在你的代码中相应地更新它。
使用import.meta.env
代替process.env
: 在Vite
项目中,你应该使用import.meta.env
来访问环境变量,而不是process.env
。因此,你的代码应该更改为:
import axios from 'axios'
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API, // 注意这里的更改
timeout: 5000,
})
export default service
封装请求动作
有了 axios
模块之后,接下来我们就可以
- 封装接口请求模块
- 封装登录请求动作
封装接口请求模块:
创建 api
文件夹,创建 sys.js
:
import request from '@/utils/request'
/**
* 登录
*/
export const login = data => {
return request({
url: '/sys/login',
method: 'POST',
data,
})
}
/**
* 获取用户信息
*/
export const getUserInfo = () => {
return request({
url: '/sys/profile',
})
}
封装登录请求动作:
该动作我们期望把它封装到 pinia
的 action
中。我也是第一次使用pinia
,现学现用就行。
在 store
下创建 auth.js
文件,用于处理所有和 用户相关 的内容(此处需要使用第三方包 md5
), 同时使用localStorage
进行数据存储处理:
import { defineStore } from 'pinia'
import { login, getUserInfo } from '@/api/sys'
export const useAuthStore = defineStore('auth', {
state: () => ({
token: null,
user: null,
}),
actions: {
async login(username, password) {
try {
const response = await login({ username, password })
this.token = response.data.token
this.user = response.data.user
localStorage.setItem('token', this.token)
return true
} catch (error) {
console.error('Login failed:', error)
return false
}
},
logout() {
this.token = null
this.user = null
localStorage.removeItem('token')
},
},
getters: {
isLoggedIn: state => !!state.token,
},
})
登录触发动作
在login
中,触发上面定义的action
<template>
<el-button
type="primary"
style="width: 100%; margin-bottom: 30px"
@click="onSubmit"
>登录</el-button
>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import { useAuthStore } from '@/stores/auth'
// do not use same name with ref
const form = reactive({
name: '',
password: '',
})
const authStore = useAuthStore()
const onSubmit = async () => {
console.log('submit!', form.name, form.password)
if (await authStore.login(username.value, password.value)) {
alert('Login successful!')
} else {
alert('Login failed!')
}
}
</script>
触发之后会得到以下错误:
该错误表示,我们当前请求的接口不存在。
出现这个问题的原因,是因为我们在前面配置环境变量时指定了 开发环境下,请求的 BaseUrl
为 /api
,所以我们真实发出的请求为:/api/sys/login
。
这样的一个请求会被自动键入到当前前端所在的服务中,所以我们最终就得到了 http://192.168.18.42:8081/api/sys/login
这样的一个请求路径。
而想要处理这个问题,那么可以通过指定 vite DevServer
代理 的形式,代理当前的 url
请求。
而指定这个代理非常简单,是一种近乎固定的配置方案。
配置代理: 在 vite.config.js 文件中,添加或修改 proxy 选项,如下所示:
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
// 匹配所有以 /api 开头的请求
'/api': {
target: 'https://api.imooc-admin.lgdsunday.club/', // 你的后端服务地址
changeOrigin: true, // 必须设置为true,否则会请求到代理服务器
rewrite: (path) => path.replace(/^\/api/, '') // 重写路径,去掉路径中的 /api
}
}
}
})
在这个配置中,所有发送到 /api
的请求都会被代理到 http://192.168.18.42:8081
,并且会自动去掉请求路径中的 /api
前缀。
保存 vite.config.js
文件后,重启 Vite
开发服务器以使配置生效。
现在,当你在前端代码中发送请求到 /api/sys/login
时,Vite
会自动将其代理到 http://192.168.18.42:8081/sys/login
。
这样配置后,你就可以在开发环境下避免跨域问题,并且能够正确地发送请求到你的后端服务。如果你遇到任何问题,确保检查代理配置是否正确,以及后端服务是否运行并能够响应请求。
响应数据的统一处理
我们保存了服务端返回的 token
。但是有一个地方比较难受,那就是在 vuex 的 user 模块
中,我们获取数据端的 token
数据,通过 data.data.data.token
的形式进行获取。
一路的 data.
确实让人比较难受,如果有过 axios
拦截器处理经验的同学应该知道,对于这种问题,我们可以通过 axios 响应拦截器 进行处理。
在 utils/request.js
中实现以下代码:
import axios from 'axios'
import { ElMessage } from 'element-plus'
...
// 响应拦截器
service.interceptors.response.use(
response => {
const { success, message, data } = response.data
// 要根据success的成功与否决定下面的操作
if (success) {
return data
} else {
// 业务错误
ElMessage.error(message) // 提示错误消息
return Promise.reject(new Error(message))
}
},
error => {
// TODO: 将来处理 token 超时问题
ElMessage.error(error.message) // 提示错误信息
return Promise.reject(error)
}
)
export default service
登录后操作
那么截止到此时,我们距离登录操作还差最后一个功能就是 登录鉴权 。
只不过在进行 登录鉴权 之前我们得先去创建一个登录后的页面,也就是我们所说的登录后操作。
- 在
HomeView.vue
,写入以下代码:
<template>
<div class="">Layout 页面</div>
</template>
<script setup>
import {} from 'vue'
</script>
<style lang="scss" scoped></style>
- 在
router/index
中,指定对应路由表:
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
})
- 在登录成功后,完成跳转
// src/views/LoginView.vue
import { useRouter } from 'vue-router'
const router = useRouter()
const onSubmit = async () => {
console.log('submit!', form.name, form.password)
if (await authStore.login(form.name, form.password)) {
alert('Login successful!')
// 登录后操作
router.push('/')
} else {
alert('Login failed!')
}
}
登录鉴权解决方案
在处理了登陆后操作之后,接下来我们就来看一下最后的一个功能,也就是 登录鉴权
首先我们先去对 登录鉴权 进行一个定义,什么是 登录鉴权 呢?
当用户未登陆时,不允许进入除
login
之外的其他页面。用户登录后,
token
未过期之前,不允许进入login
页面
而想要实现这个功能,那么最好的方式就是通过 路由守卫 来进行实现。
那么明确好了 登录鉴权 的概念之后,接下来就可以去实现一下。
在此处我们使用到了 pinia 中的 getters
,此时的 getters
被当作 快捷访问 的形式进行访问。
getters: {
isLoggedIn: state => !!state.token,
}
在 router/index.js
文件
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/login',
name: 'login',
// route level code-splitting
// this generates a separate chunk (login.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/loginView.vue'),
},
],
})
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
if (to.name === 'login' && authStore.isLoggedIn) {
// 如果用户已经登录,尝试访问登录页面,则重定向到首页
next({ name: 'home' })
} else if (to.name !== 'login' && !authStore.isLoggedIn) {
// 如果用户未登录,尝试访问非登录页面,则重定向到登录页面
next({ name: 'login' })
} else {
// 其他情况,正常放行
next()
}
})
export default router
源码已同步到 vue-admin 包里,感兴趣的可以自行下载了解。
总结
本次学到了以下知识
-
登录方案相关的业务代码
-
element-plus
相关el-form
表单- 密码框状态处理
-
后台登录解决方案
- 封装
axios
模块 - 封装 接口请求 模块
- 封装登录请求动作
- 保存服务端返回的
token
- 登录鉴权
- 封装
-