一.Vue新特性
(1)defineOptions:主要是用来定义Options API的选项
背景说明:有< script setup >之前,如果定义props,emits可以轻而易举地添加一个与setup平级
的属性。但是用了< script setup >后,就没法这么干了setup属性已经没有了,自然无法添加与其平级的属性
为了解决这一问题,引入defineProps与defineEmits这两个宏。但这只解决了Props与Emits这两个属性。
<!-- <script>
export default {name: 'LoginIndex'
}
</script> --><script setup>
defineOptions({name: 'LoginIndex'
})
</script><template>
<div>我是登录页
</div>
</template>
(2)defineModel
在vue3中,自定义组件上使用v-model,相当于传递一个modelValue属性,同时触发update:modelValue事件
import { fileURLToPath, URL } from 'node:url'import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'// https://vitejs.dev/config/
export default defineConfig({plugins: [vue({script: {defineModel: true}}),],resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url))}}
})
<script setup>
import { defineModel } from 'vue'
const modelValue = defineModel()
</script><template>
<div><input type="text" :value="modelValue" @input="e => modelValue = e.target.value">
</div>
</template>
二.Pinia快速入门
(1)什么是Pinia
Pinia 是 Vue 的最新 状态管理工具 ,是 Vuex 的 替代品
1.提供更加简单的API (去掉了 mutation )
2.提供符合,组合式风格的API (和 Vue3 新语法统一)
3.去掉了 modules 的概念,每一个 store 都是一个独立的模块
4.配合 TypeScript 更加友好,提供可靠的类型推断
(2)手动添加Pinia到Vue项目
在实际开发项目的时候,关于Pinia的配置,可以在项目创建时自动添加
现在我们初次学习,从零开始:
1.使用 Vite 创建一个空的 Vue3 项目
npm create vue@latest
2.按照官方文档 安装 pinia 到项目中
import { createApp } from 'vue'
import { createPinia } from 'pinia'
// 导入持久化的插件
import persist from 'pinia-plugin-persistedstate'import App from './App.vue'
const pinia = createPinia() // 创建Pinia实例
const app = createApp(App) // 创建根实例
app.use(pinia.use(persist)) // pinia插件的安装配置
app.mount('#app') // 视图的挂载
(3)Pinia基础使用 - 计数器案例
1.定义store
↓
2.组件使用store
定义Store(state + action) 组件使用Store
import { createApp } from 'vue'
import { createPinia } from 'pinia'
// 导入持久化的插件
import persist from 'pinia-plugin-persistedstate'import App from './App.vue'
const pinia = createPinia() // 创建Pinia实例
const app = createApp(App) // 创建根实例
app.use(pinia.use(persist)) // pinia插件的安装配置
app.mount('#app') // 视图的挂载
(4)getters实现
Pinia中的 getters 直接使用 computed函数 进行模拟, 组件中需要使用需要把 getters return出去
// 定义store
// defineStore(仓库的唯一标识, () => { ... })
export const useCounterStore = defineStore('counter', () => {// 声明数据 state - countconst count = ref(100)// 声明操作数据的方法 action (普通函数)const addCount = () => count.value++const subCount = () => count.value--// 声明基于数据派生的计算属性 getters (computed)const double = computed(() => count.value * 2)// 声明数据 state - msgconst msg = ref('hello pinia')return {count,double,addCount,subCount,msg,}
}
(5)action异步实现
编写方式:异步action函数的写法和组件中获取异步数据的写法完全一致
接口地址:http://geek.itheima.net/v1_0/channels
需求:在Pinia中获取频道列表数据并把数据渲染App组件的模板中
<script setup>
import { storeToRefs } from 'pinia'
import Son1Com from '@/components/Son1Com.vue'
import Son2Com from '@/components/Son2Com.vue'
import { useCounterStore } from '@/store/counter'
import { useChannelStore } from './store/channel'
const counterStore = useCounterStore()
const channelStore = useChannelStore()
</script><template><div><h3>App.vue根组件 - {{ count }}- {{ msg }}</h3><Son1Com></Son1Com><Son2Com></Son2Com><hr><button @click="getList">获取频道数据</button><ul><li v-for="item in channelList" :key="item.id">{{ item.name }}</li></ul></div>
</template><style scoped></style>
(6)storeToRefs工具函数
使用storeToRefs函数可以辅助保持数据(state + getter)的响应式解构
// 此时,直接解构,不处理,数据会丢失响应式
const { count, msg } = storeToRefs(counterStore)
const { channelList } = storeToRefs(channelStore)
const { getList } = channelStore
(7)Pinia的调试
Vue官方的 dev-tools 调试工具 对 Pinia直接支持,可以直接进行调试多一句没有,少一句不行,用更短时间,教会更实用的技术!
高级软件人才培训专家
(8)Pinia持久化插件
官方文档:https://prazdevs.github.io/pinia-plugin-persistedstate/zh/
1.安装插件 pinia-plugin-persistedstate
npm i pinia-plugin-persistedstate
2.main.js 使用
import persist from ‘pinia-plugin-persistedstate’
…
app.use(createPinia().use(persist))
3.store仓库中,persist: true 开启
// persist: true // 开启当前模块的持久化persist: {key: 'hm-counter', // 修改本地存储的唯一标识paths: ['count'] // 存储的是哪些数据}
import { defineStore } from 'pinia'
import { ref } from 'vue'
import axios from 'axios'export const useChannelStore = defineStore('channel', () => {// 声明数据const channelList = ref([])// 声明操作数据的方法const getList = async () => {// 支持异步const { data: { data }} = await axios.get('http://geek.itheima.net/v1_0/channels')channelList.value = data.channels}// 声明getters相关return {channelList,getList}
})
三.大事件管理系统
(1)Eslint 配置代码风格
配置文件 .eslintrc.cjs
1.prettier 风格配置 https://prettier.io
①单引号
②不使用分号
③宽度80字符
④不加对象|数组最后逗号
⑤换行符号不限制(win mac 不一致)
2.vue组件名称多单词组成(忽略index.vue)
3. props解构(关闭)
提示:安装Eslint且配置保存修复,不
要开启默认的自动保存格式化
(2)配置代码检查工作流
提交前做代码检查
1.初始化 git 仓库,执行 git init 即可
2. 初始化 husky 工具配置,执行 pnpm dlx husky-init && pnpm install 即可
https://typicode.github.io/husky/
3. 修改 .husky/pre-commit 文件
问题:pnpm lint 是全量检查,耗时问题,历史问题。
暂存区 eslint 校验
1.安装 lint-staged 包 pnpm i lint-staged -D
2. package.json 配置 lint-staged 命令
3…husky/pre-commit 文件修改
(3)vue-router4 路由代码解析
1.创建路由实例由 createRouter 实现
2. 路由模式
①history 模式使用 createWebHistory()
② hash 模式使用 createWebHashHistory()
③参数是基础路径,默认
import { createRouter, createWebHistory } from 'vue-router'// createRouter 创建路由实例
// 配置 history 模式
// 1. history模式:createWebHistory 地址栏不带 #
// 2. hash模式: createWebHashHistory 地址栏带 #// vite 中的环境变量 import.meta.env.BASE_URL 就是 vite.config.js 中的 base 配置项
const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [ ]}]
})export default router
<script setup>
// 在 Vue3 CompositionAPI 中
// 1. 获取路由对象 router useRouter
// const router = useRouter()
// 2. 获取路由参数 route useRoute
// const route = useRoute()
import { useRoute, useRouter } from 'vue-router'
import { useUserStore, useCountStore } from '@/stores'
const router = useRouter()
const route = useRoute()const goList = () => {router.push('/list')console.log(router, route)
}
const userStore = useUserStore()
const countStore = useCountStore()
</script>
(4)按需引入 Element Plus
import { fileURLToPath, URL } from 'node:url'import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
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: [vue(),AutoImport({resolvers: [ElementPlusResolver()]}),Components({resolvers: [ElementPlusResolver()]})],base: '/',resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url))}}
})
(5)Pinia 构建用户仓库 和 持久化
import { defineStore } from 'pinia'
import { ref } from 'vue'// 用户模块 token setToken removeToken
export const useUserStore = defineStore('big-user',() => {const token = ref('')const setToken = (newToken) => {token.value = newToken}const removeToken = () => {token.value = ''}return {token,setToken,removeToken}},{persist: true}
)
(6)Pinia 仓库统一管理
pinia 独立维护
- 现在:初始化代码在 main.js 中,仓库代码在 stores 中,代码分散职能不单一
- 优化:由 stores 统一维护,在 stores/index.js 中完成 pinia 初始化,交付 main.js 使用
仓库 统一导出
- 现在:使用一个仓库 import { useUserStore } from
./stores/user.js
不同仓库路径不一致 - 优化:由 stores/index.js 统一导出,导入路径统一
./stores
,而且仓库维护在 stores/modules 中
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'const pinia = createPinia()
pinia.use(persist)export default pinia
export * from './modules/user'
export * from './modules/counter'// import { useUserStore } from './modules/user'
// export { useUserStore }
// import { useCountStore } from './modules/counter'
// export { useCountStore }
import { defineStore } from 'pinia'
import { ref } from 'vue'// 数字计数器模块
export const useCountStore = defineStore('big-count', () => {const count = ref(100)const add = (n) => {count.value += n}return {count,add}
})
(7)数据交互 - 请求工具设计
import axios from 'axios'
import { useUserStore } from '@/stores'
import { ElMessage } from 'element-plus'
import router from '@/router'
const baseURL = 'http://big-event-vue-api-t.itheima.net'const instance = axios.create({// TODO 1. 基础地址,超时时间baseURL,timeout: 10000
})// 请求拦截器
instance.interceptors.request.use((config) => {// TODO 2. 携带tokenconst useStore = useUserStore()if (useStore.token) {config.headers.Authorization = useStore.token}return config},(err) => Promise.reject(err)
)// 响应拦截器
instance.interceptors.response.use((res) => {// TODO 4. 摘取核心响应数据if (res.data.code === 0) {return res}// TODO 3. 处理业务失败// 处理业务失败, 给错误提示,抛出错误ElMessage.error(res.data.message || '服务异常')return Promise.reject(res.data)},(err) => {// TODO 5. 处理401错误// 错误的特殊情况 => 401 权限不足 或 token 过期 => 拦截到登录if (err.response?.status === 401) {router.push('/login')}// 错误的默认情况 => 只要给提示ElMessage.error(err.response.data.message || '服务异常')return Promise.reject(err)}
)export default instance
export { baseURL }
(8)首页整体路由设计
path: '/',component: () => import('@/views/layout/LayoutContainer.vue'),redirect: '/article/manage',children: [{path: '/article/manage',component: () => import('@/views/article/ArticleManage.vue')},{path: '/article/channel',component: () => import('@/views/article/ArticleChannel.vue')},{path: '/user/profile',component: () => import('@/views/user/UserProfile.vue')},{path: '/user/avatar',component: () => import('@/views/user/UserAvatar.vue')},{path: '/user/password',component: () => import('@/views/user/UserPassword.vue')}]
四.具体业务功能实现
(1)登录注册页面 [element-plus 表单 & 表单校验]
功能需求说明:
1.注册登录 静态结构 & 基本切换
2.注册功能 (校验 + 注册)
3.登录功能 (校验 + 登录 + 存token)
import request from '@/utils/request'// 注册接口
export const userRegisterService = ({ username, password, repassword }) =>request.post('/api/reg', { username, password, repassword })// 登录接口
export const userLoginService = ({ username, password }) =>request.post('/api/login', { username, password })// 获取用户基本信息
export const userGetInfoService = () => request.get('/my/userinfo')
<script setup>
import { userRegisterService, userLoginService } from '@/api/user.js'
import { User, Lock } from '@element-plus/icons-vue'
import { ref, watch } from 'vue'
import { useUserStore } from '@/stores'
import { useRouter } from 'vue-router'
const isRegister = ref(false)
const form = ref()// 整个的用于提交的form数据对象
const formModel = ref({username: '',password: '',repassword: ''
})
// 整个表单的校验规则
// 1. 非空校验 required: true message消息提示, trigger触发校验的时机 blur change
// 2. 长度校验 min:xx, max: xx
// 3. 正则校验 pattern: 正则规则 \S 非空字符
// 4. 自定义校验 => 自己写逻辑校验 (校验函数)
// validator: (rule, value, callback)
// (1) rule 当前校验规则相关的信息
// (2) value 所校验的表单元素目前的表单值
// (3) callback 无论成功还是失败,都需要 callback 回调
// - callback() 校验成功
// - callback(new Error(错误信息)) 校验失败
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() // 就算校验成功,也需要callback}},trigger: 'blur'}]
}const register = async () => {// 注册成功之前,先进行校验,校验成功 → 请求,校验失败 → 自动提示await form.value.validate()await userRegisterService(formModel.value)ElMessage.success('注册成功')isRegister.value = false
}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('/')
}// 切换的时候,重置表单内容
watch(isRegister, () => {formModel.value = {username: '',password: '',repassword: ''}
})
</script><template><!-- 1. 结构相关el-row表示一行,一行分成24份 el-col表示列 (1) :span="12" 代表在一行中,占12份 (50%)(2) :span="6" 表示在一行中,占6份 (25%)(3) :offset="3" 代表在一行中,左侧margin份数el-form 整个表单组件el-form-item 表单的一行 (一个表单域)el-input 表单元素(输入框)2. 校验相关(1) el-form => :model="ruleForm" 绑定的整个form的数据对象 { xxx, xxx, xxx }(2) el-form => :rules="rules" 绑定的整个rules规则对象 { xxx, xxx, xxx }(3) 表单元素 => v-model="ruleForm.xxx" 给表单元素,绑定form的子属性(4) el-form-item => prop配置生效的是哪个校验规则 (和rules中的字段要对应)--><el-row class="login-page"><el-col :span="12" class="bg"></el-col><el-col :span="6" :offset="3" class="form"><!-- 注册相关表单 --><el-form:model="formModel":rules="rules"ref="form"size="large"autocomplete="off"v-if="isRegister"><el-form-item><h1>注册</h1></el-form-item><el-form-item prop="username"><el-inputv-model="formModel.username":prefix-icon="User"placeholder="请输入用户名"></el-input></el-form-item><el-form-item prop="password"><el-inputv-model="formModel.password":prefix-icon="Lock"type="password"placeholder="请输入密码"></el-input></el-form-item><el-form-item prop="repassword"><el-inputv-model="formModel.repassword":prefix-icon="Lock"type="password"placeholder="请输入再次密码"></el-input></el-form-item><el-form-item><el-button@click="register"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:model="formModel":rules="rules"ref="form"size="large"autocomplete="off"v-else><el-form-item><h1>登录</h1></el-form-item><el-form-item prop="username"><el-inputv-model="formModel.username":prefix-icon="User"placeholder="请输入用户名"></el-input></el-form-item><el-form-item prop="password"><el-inputv-model="formModel.password"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@click="login"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>