pnpm 包管理器-创建项目
优势:
比同类工具快两倍左右,节省磁盘空间
安装方式:npm install -g pnpm
创建项目:pnpm create vue
Eslint 配置代码风格
配置文件:.eslint.sjs
1. prettier风格配置
- 单引号
- 无分号
- 每行字符至多80字符
- 不加对象|数组最后逗号
- 换行符号不限制(win mac 不一致)
js
// ESlint插件 + Vscode配置 实现自动格式化修复
"editor.codeActionsOnSave": {
"source.fixAll": true
},
"editor.formatOnSave": false,
Eslint关注于规范,不符合规范会报错
prettier关注美观度
2. vue组件名称多单词组成(忽略index.vue)
3. props解构(关闭)
js
rules: {
'prettier/prettier': [
'warn',
{
singleQuote: true, // 单引号
semi: false, // 无分号
printWidth: 80, // 每行宽度至多80字符
trailingComma: 'none', // 不加对象|数组最后逗号
endOfLine: 'auto' // 换行符号不限制(win mac 不一致)
}
],
'vue/multi-word-component-names': [
'warn',
{
ignores: ['index'] // vue组件名称多单词组成(忽略index.vue)
}
],
'vue/no-setup-props-destructure': ['off'], // 关闭 props 解构的校验
// 💡 添加未定义变量错误提示,create-vue@3.6.3 关闭,这里加上是为了支持下一个章节演示。
'no-undef': 'error'
}
-
禁用格式化插件prettier format on save关闭
-
装Eslint插件,并配置保存时自动修复
配置代码检查工作流
- 初始化git仓库,执行git init即可
- 初始化husky工具配置, typicode.github.io/husky/
js
pnpm dlx husky-init && pnpm install
暂存区 eslint校验
- 安装lint-staged 包
pnpm i lint-staged-D
- packagejson配置lint-staged命令
js
{
// ... 省略 ...
"lint-staged": {
"*.{js,ts,vue}": [
"eslint --fix"
]
}
}
{
"scripts": {
// ... 省略 ...
"lint-staged": "lint-staged"
}
}
- .husky/pre-commit 文件修改
js
pnpm lint-staged
目录调整
默认生成的目录结构不满足我们的开发需求,所以这里需要做一些自定义改动。主要是两个工作:
- 删除初始化的默认文件
- 修改剩余代码内容
- 新增调整我们需要的目录结构
- 拷贝初始化资源文件,安装预处理器插件
-
删除文件
-
修改内容
src/router/index.js
jsx
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: []
})
export default router
src/App.vue
jsx
<script setup></script>
<template>
<div>
<router-view></router-view>
</div>
</template>
<style scoped></style>
src/main.js
jsx
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')
-
新增需要目录 api 和 utils
-
将项目需要的全局样式 和 图片文件,复制到 assets 文件夹中, 并将全局样式在main.js中引入
jsx
import '@/assets/main.scss'
- 安装 sass 依赖
jsx
pnpm add sass -D
vue-router4 路由解析
路由初始化
- 创建路由实例由createRouter实现
- 路由模式
- history 模式使用 createWebHistory()
- hash模式使用createWebHashHistory()
- 参数是基础路径,默认/ 基础代码解析
js
import { createRouter, createWebHistory } from 'vue-router'
// createRouter 创建路由实例,===> new VueRouter()
// 1. history模式: createWebHistory() http://xxx/user
// 2. hash模式: createWebHashHistory() http://xxx/#/user
// vite 的配置 import.meta.env.BASE_URL 是路由的基准地址,默认是 '/'
// https://vitejs.dev/guide/build.html#public-base-path
// 如果将来你部署的域名路径是:http://xxx/my-path/user
// vite.config.ts 添加配置 base: my-path,路由这就会加上 my-path 前缀了
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: []
})
export default router
import.meta.env.BASE_URL 是Vite 环境变量:cn.vitejs.dev/guide/env-a...
总结:
引入Element Plus 组件库
按需引入Element Plus
- 安装 pnpm add element-plus
- 配置按需导入
- 直接使用组件
步骤
- 安装插件
js
pnpm add -D unplugin-vue-components unplugin-auto-import
- 然后把下列代码插入到你的
Vite
或Webpack
的配置文件中
js
...
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
...
AutoImport({
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
})
]
})
- 直接使用
js
<template>
<div>
<el-button type="primary">Primary</el-button>
<el-button type="success">Success</el-button>
<el-button type="info">Info</el-button>
<el-button type="warning">Warning</el-button>
<el-button type="danger">Danger</el-button>
...
</div>
</template>
彩蛋:默认 components 下的文件也会被自动注册
Pinia - 构建用户仓库 和 持久化
官方文档:
prazdevs.github.io/pinia-plugi...
步骤:
- 安装插件 pinia-plugin-persistedstate
js
pnpm add pinia-plugin-persistedstate -D
- 使用 main.js
js
import persist from 'pinia-plugin-persistedstate'
...
app.use(createPinia().use(persist))
- 配置 stores/user.js
jsx
import { defineStore } from 'pinia'
import { ref } from 'vue'
// 用户模块
export const useUserStore = defineStore(
'big-user',
() => {
const token = ref('') // 定义 token
const setToken = (t) => (token.value = t) // 设置 token
return { token, setToken }
},
{
persist: true // 持久化
}
)
Pinia - 配置仓库统一管理
pinia 独立维护
现在:初始化代码在 main.js 中,仓库代码在 stores 中,代码分散职能不单一
优化:由 stores 统一维护,在 stores/index.js 中完成 pinia 初始化,交付 main.js 使用
仓库 统一导出
现在:使用一个仓库 import { useUserStore } from ./stores/user.js
不同仓库路径不一致
优化:由 stores/index.js 统一导出,导入路径统一 ./stores
,而且仓库维护在 stores/modules 中
数据交互 - 请求工具设计
创建 axios 实例
会使用 axios 来请求后端接口, 一般都会对 axios 进行一些配置 (比如: 配置基础地址等)
一般项目开发中, 都会对 axios 进行基本的二次封装, 单独封装到一个模块中, 便于使用
步骤
- 安装 axios
csharppnpm add axios
-
新建
utils/request.js
封装 axios 模块利用 axios.create 创建一个自定义的 axios 来使用
js
import axios from 'axios'
const baseURL = 'http://big-event-vue-api-t.itheima.net'
const instance = axios.create({
// TODO 1. 基础地址,超时时间
})
instance.interceptors.request.use(
(config) => {
// TODO 2. 携带token
return config
},
(err) => Promise.reject(err)
)
instance.interceptors.response.use(
(res) => {
// TODO 3. 处理业务失败
// TODO 4. 摘取核心响应数据
return res
},
(err) => {
// TODO 5. 处理401错误
return Promise.reject(err)
}
)
export default instance
完成 axios 基本配置
jsx
import { useUserStore } from '@/stores/user'
import axios from 'axios'
import router from '@/router'
import { ElMessage } from 'element-plus'
const baseURL = 'http://big-event-vue-api-t.itheima.net'
const instance = axios.create({
baseURL,
timeout: 100000
})
instance.interceptors.request.use(
(config) => {
const userStore = useUserStore()
if (userStore.token) {
config.headers.Authorization = userStore.token
}
return config
},
(err) => Promise.reject(err)
)
instance.interceptors.response.use(
(res) => {
if (res.data.code === 0) {
return res
}
ElMessage({ message: res.data.message || '服务异常', type: 'error' })
return Promise.reject(res.data)
},
(err) => {
ElMessage({ message: err.response.data.message || '服务异常', type: 'error' })
console.log(err)
if (err.response?.status === 401) {
router.push('/login')
}
return Promise.reject(err)
}
)
export default instance
export { baseURL }
整体路由设计
- 完成整体路由规划【搞清楚要做几个页面,它们分别在哪个路由下面,怎么跳转的.....】
- 通过观察, 点击左侧导航, 右侧区域在切换, 那右侧区域内容一直在变, 那这个地方就是一个路由的出口
- 我们需要搭建嵌套路由
目标:
- 把项目中所有用到的组件及路由表, 约定下来
约定路由规则:
path | 文件 | 功能 | 组件名 | 路由级别 |
---|---|---|---|---|
/login | views/login/LoginPage.vue | 登录&注册 | LoginPage | 一级路由 |
/ | views/layout/LayoutContainer.vue | 布局架子 | LayoutContainer | 一级路由 |
├─ /article/manage | views/article/ArticleManage.vue | 文章管理 | ArticleManage | 二级路由 |
├─ /article/channel | views/article/ArticleChannel.vue | 频道管理 | ArticleChannel | 二级路由 |
├─ /user/profile | views/user/UserProfile.vue | 个人详情 | UserProfile | 二级路由 |
├─ /user/avatar | views/user/UserAvatar.vue | 更换头像 | UserAvatar | 二级路由 |
├─ /user/password | views/user/UserPassword.vue | 重置密码 | UserPassword | 二级路由 |
登录注册页面【element-plus 表单 & 表单校验】
功能需求
- 注册登录 静态结构&基本切换
- 诸恶功能(校验+注册)
- 登录功能(校验+登录+存token)
登录注册
- 安装 element-plus 图标库
js
pnpm i @element-plus/icons-vue
- 静态结构准备
js
<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
const isRegister = ref(true)
</script>
<template>
<el-row class="login-page">
<el-col :span="12" class="bg"></el-col>
<el-col :span="6" :offset="3" class="form">
//注册相关表单
<el-form ref="form" size="large" autocomplete="off" v-if="isRegister">
<el-form-item>
<h1>注册</h1>
</el-form-item>
<el-form-item>
<el-input :prefix-icon="User" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item>
<el-input
:prefix-icon="Lock"
type="password"
placeholder="请输入密码"
></el-input>
</el-form-item>
<el-form-item>
<el-input
:prefix-icon="Lock"
type="password"
placeholder="请输入再次密码"
></el-input>
</el-form-item>
<el-form-item>
<el-button class="button" type="primary" auto-insert-space>
注册
</el-button>
</el-form-item>
<el-form-item class="flex">
<el-link type="info" :underline="false" @click="isRegister = false">
← 返回
</el-link>
</el-form-item>
</el-form>
<el-form ref="form" size="large" autocomplete="off" v-else>
<el-form-item>
<h1>登录</h1>
</el-form-item>
<el-form-item>
<el-input :prefix-icon="User" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item>
<el-input
name="password"
:prefix-icon="Lock"
type="password"
placeholder="请输入密码"
></el-input>
</el-form-item>
<el-form-item class="flex">
<div class="flex">
<el-checkbox>记住我</el-checkbox>
<el-link type="primary" :underline="false">忘记密码?</el-link>
</div>
</el-form-item>
<el-form-item>
<el-button class="button" type="primary" auto-insert-space
>登录</el-button
>
</el-form-item>
<el-form-item class="flex">
<el-link type="info" :underline="false" @click="isRegister = true">
注册 →
</el-link>
</el-form-item>
</el-form>
</el-col>
</el-row>
</template>
<style lang="scss" scoped>
.login-page {
height: 100vh;
background-color: #fff;
.bg {
background: url('@/assets/logo2.png') no-repeat 60% center / 240px auto,
url('@/assets/login_bg.jpg') no-repeat center / cover;
border-radius: 0 20px 20px 0;
}
.form {
display: flex;
flex-direction: column;
justify-content: center;
user-select: none;
.title {
margin: 0 auto;
}
.button {
width: 100%;
}
.flex {
width: 100%;
display: flex;
justify-content: space-between;
}
}
}
</style>
注:
-
el-row表示一行,一行分成24份
-
el-col表示列:span="12" 代表在一行中占12份(50%)
-
span="6" 表示在一行中占6份(25%)
-
offset="3" 表示在一行中,左侧margin份数
- el-form 整个表单组件
- el-form-item 表单的一行(一个表单域)
- 表单域彼此独立 互不影响
- el-input表单元素(输入框)
注册功能
【需求】注册页面基本校验
- 用户名非空,长度校验5-10位
- 密码非空,长度校验6-15位
- 再次输入密码,非空,长度校验6-15位
【进阶】再次输入密码需要自定义校验规则,和密码框值一致(可选)
校验相关
- el-form => :model="ruleForm" 绑定的整个form的数据对象{xxx,xxx,xxx}
- el-form => :rules="rules" 绑定的整个rules规则对象{xxx,xxx,xxx}
- 表单元素 =>v-model="ruleForm,xxx" 给表单元素绑定form的子属性
- el-model-item-props配置生效的是哪个校验规则
具体如下:
1. model 属性绑定 form 数据对象
js
//整个用于提交的form数据对象
const formModel = ref({
username: '',
password: '',
repassword: ''
})
<el-form :model="formModel" >
2. v-model 绑定 form 数据对象的子属性
js
<el-input
v-model="formModel.username"
:prefix-icon="User"
placeholder="请输入用户名"
></el-input>
...
(其他两个也要绑定)
3. rules 配置校验规则
js
<el-form :rules="rules" >
//整个表单的校验规则
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 5, max: 10, message: '用户名必须是5-10位的字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{
pattern: /^\S{6,15}$/,
message: '密码必须是6-15位的非空字符',
trigger: 'blur'
}
],
repassword: [
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{
pattern: /^\S{6,15}$/,
message: '密码必须是6-15的非空字符',
trigger: 'blur'
},
{
validator: (rule, value, callback) => {
//判度value和当前form中收集的password是否一致
if (value !== formModel.value.password) {
callback(new Error('两次输入密码不一致!'))
} else {
callback()
}
},
trigger: 'blur'
}
]
}
效果如下:
整个表单的校验规则
-
非空校验:required:true message 消息提示 trigger触发校验时机 blur change
-
长度校验 min:xx max:xx
-
正则校验 pattern:正则规则
-
自定义校验 =>自己写逻辑校验(校验函数)
- validator : (rule,value,callback)
- rule 当前校验相关的信息
- value 当前校验表单元素目前的表单值
- callback 无论成功还是失败,都需要callback回调
- callback() 校验成功
- callback(new Error(错误信息)) 校验失败
4. prop 绑定校验规则
js
<el-form-item prop="username">
<el-input
v-model="formModel.username"
:prefix-icon="User"
placeholder="请输入用户名"
></el-input>
</el-form-item>
...
(其他两个也要绑定prop)
注册前的预校验
需求:点击注册按钮,注册之前,需要先校验
1. 通过 ref 获取到 表单组件
js
const form = ref()
<el-form ref="form">
2. 注册之前进行校验
js
<el-button
@click="register"
class="button"
type="primary"
auto-insert-space
>
注册
</el-button>
const register = async () => {
await form.value.validate()
console.log('开始注册请求')
}
封装 api 实现注册功能
需求:封装注册api,进行注册,注册成功切换到登录
1. 新建 api/user.js 封装
js
import request from '@/utils/request'
export const userRegisterService = ({ username, password, repassword }) =>
request.post('/api/reg', { username, password, repassword })
2. 页面中调用
jsx
const register = async () => {
await form.value.validate()
await userRegisterService(formModel.value)
ElMessage.success('注册成功')
// 切换到登录
isRegister.value = false
}
3. eslintrc 中声明全局变量名, 解决 ElMessage 报错问题
jsx
module.exports = {
...
globals: {
ElMessage: 'readonly',
ElMessageBox: 'readonly',
ElLoading: 'readonly'
}
}
效果如下:
登录功能
实现登录校验
【需求说明】给输入框添加表单校验
- 用户名不能为空,用户名必须是5-10位的字符,失去焦点 和 修改内容时触发校验
- 密码不能为空,密码必须是6-15位的字符,失去焦点 和 修改内容时触发校验
操作步骤:
- model 属性绑定 form 数据对象,直接绑定之前提供好的数据对象即可
js
<el-form :model="formModel" >
- rules 配置校验规则,共用注册的规则即可
js
<el-form :rules="rules" >
- v-model 绑定 form 数据对象的子属性
js
<el-input
v-model="formModel.username"
:prefix-icon="User"
placeholder="请输入用户名"
></el-input>
<el-input
v-model="formModel.password"
name="password"
:prefix-icon="Lock"
type="password"
placeholder="请输入密码"
></el-input>
- prop 绑定校验规则
js
<el-form-item prop="username">
<el-input
v-model="formModel.username"
:prefix-icon="User"
placeholder="请输入用户名"
></el-input>
</el-form-item>
...
- 切换的时候重置
js
watch(isRegister, () => {
formModel.value = {
username: '',
password: '',
repassword: ''
}
})
预校验
请求参数:
【需求说明1】登录之前的预校验
- 登录请求之前,需要对用户的输入内容,进行校验
- 校验通过才发送请求
【需求说明2】登录功能
- 封装登录API,点击按钮发送登录请求
- 登录成功存储token,存入pinia 和 持久化本地storage
- 跳转到首页,给提示
实现步骤:
- 注册事件,进行登录前的预校验 (获取到组件调用方法)
js
<el-form ref="form">
const login = async () => {
await form.value.validate()
console.log('开始登录')
}
- 封装接口 API
js
export const userLoginService = ({ username, password }) =>
request.post('api/login', { username, password })
- 调用方法将 token 存入 pinia 并 自动持久化本地
js
const userStore = useUserStore()
const router = useRouter()
const login = async () => {
await form.value.validate()
const res = await userLoginService(formModel.value)
userStore.setToken(res.data.token)
ElMessage.success('登录成功')
//跳转到本地
router.push('/')
}
效果如下:
首页 layout 架子 [element-plus 菜单]
基本架子拆解
架子组件列表:
el-container
-
el-aside 左侧
- el-menu 左侧边栏菜单
-
el-container 右侧
- el-header 右侧头部
- el-dropdown
- el-main 右侧主体
- router-view
- el-header 右侧头部
代码
html
<script setup>
import {
Management,
Promotion,
UserFilled,
User,
Crop,
EditPen,
SwitchButton,
CaretBottom
} from '@element-plus/icons-vue'
import avatar from '@/assets/default.png'
</script>
<template>
<el-container class="layout-container">
<el-aside width="200px">
<div class="el-aside__logo"></div>
<el-menu
active-text-color="#ffd04b"
background-color="#232323"
:default-active="$route.path"
text-color="#fff"
router
>
<el-menu-item index="/article/channel">
<el-icon><Management /></el-icon>
<span>文章分类</span>
</el-menu-item>
<el-menu-item index="/article/manage">
<el-icon><Promotion /></el-icon>
<span>文章管理</span>
</el-menu-item>
<el-sub-menu index="/user">
//多级菜单的标题-具名插槽title
<template #title>
<el-icon><UserFilled /></el-icon>
<span>个人中心</span>
</template>
//展开的内容-默认插槽
<el-menu-item index="/user/profile">
<el-icon><User /></el-icon>
<span>基本资料</span>
</el-menu-item>
<el-menu-item index="/user/avatar">
<el-icon><Crop /></el-icon>
<span>更换头像</span>
</el-menu-item>
<el-menu-item index="/user/password">
<el-icon><EditPen /></el-icon>
<span>重置密码</span>
</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>
<el-container>
<el-header>
<div>黑马程序员:<strong>小帅鹏</strong></div>
<el-dropdown placement="bottom-end">
<span class="el-dropdown__box">
<el-avatar :src="avatar" />
<el-icon><CaretBottom /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile" :icon="User"
>基本资料</el-dropdown-item
>
<el-dropdown-item command="avatar" :icon="Crop"
>更换头像</el-dropdown-item
>
<el-dropdown-item command="password" :icon="EditPen"
>重置密码</el-dropdown-item
>
<el-dropdown-item command="logout" :icon="SwitchButton"
>退出登录</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-header>
<el-main>
<router-view></router-view>
</el-main>
<el-footer>大事件 ©2023 Created by 黑马程序员</el-footer>
</el-container>
</el-container>
</template>
<style lang="scss" scoped>
.layout-container {
height: 100vh;
.el-aside {
background-color: #232323;
&__logo {
height: 120px;
background: url('@/assets/logo.png') no-repeat center / 120px auto;
}
.el-menu {
border-right: none;
}
}
.el-header {
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
.el-dropdown__box {
display: flex;
align-items: center;
.el-icon {
color: #999;
margin-left: 10px;
}
&:active,
&:focus {
outline: none;
}
}
}
.el-footer {
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #666;
}
}
</style>
注:
-
el-menu 整个菜单
-
active-text-color:激活时文字颜色
-
:default-active="$route.path" 配置默认高亮的菜单项
-
router router选项开启,el-menu-item 的index就是点击跳转的路径
el-menu-item 菜单项
index="/article/channel"配置的是访问的跳转路径,配合default-active值实现高亮
登陆访问拦截 =>默认是放行的
根据返回值决定,是放行还是拦截
返回值:
-
undefined / 直接放行
-
false拦回from的地址是页面
-
具体路径或路径对象 拦截到对应的地址
- '/lohin'{ name:'login'}
如果没有token,且访问的是非登录页,拦截到登录,其他情况正常放行
登录访问拦截
需求:只有登录页,可以未授权的时候访问,其他所有页面,都需要先登录再访问
jsx
// 登录访问拦截
router.beforeEach((to) => {
const userStore = useUserStore()
if (!userStore.token && to.path !== '/login') return '/login'
})
用户基本信息获取&渲染
请求参数
步骤如下:
1. api/user.js
封装接口
js
export const userGetInfoService = () => request.get('/my/userinfo')
2. stores/modules/user.js 定义数据
js
const user = ref({})
const getUser = async () => {
const res = await userGetInfoService() // 请求获取数据
user.value = res.data.data
}
3. layout/LayoutContainer
页面中调用
js
import { useUserStore } from '@/stores'
const userStore = useUserStore()
onMounted(() => {
userStore.getUser()
})
4. 动态渲染
js
<div>
黑马程序员:<strong>{{ userStore.user.nickname || userStore.user.username }}</strong>
</div>
<el-avatar :src="userStore.user.user_pic || avatar" />
退出功能 [element-plus 确认框]
1. 注册点击事件
js
<el-dropdown placement="bottom-end" @command="onCommand">
<el-dropdown-menu>
<el-dropdown-item command="profile" :icon="User">基本资料</el-dropdown-item>
<el-dropdown-item command="avatar" :icon="Crop">更换头像</el-dropdown-item>
<el-dropdown-item command="password" :icon="EditPen">重置密码</el-dropdown-item>
<el-dropdown-item command="logout" :icon="SwitchButton">退出登录</el-dropdown-item>
</el-dropdown-menu>
2. 添加退出功能
js
const onCommand = async (command) => {
if (command === 'logout') {
await ElMessageBox.confirm('你确认退出大事件吗?', '温馨提示', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消'
})
//清楚本地的数据(token+user)
userStore.removeToken()
userStore.setUser({})
router.push(`/login`)
} else {
//跳转操作
router.push(`/user/${command}`)
}
}
3. pinia user.js 模块 提供 setUser 方法
js
const setUser = (obj) => (user.value = obj)