距离vue2学习已经一年度了,现在开始vue3的学习。
一、webpack
(1)创建列表隔行变色项目及webpack使用:
新建项目空白目录,并运行npm init -y命令,初始化包管理配置文件package.json;
新建src源代码目录,新建src -> index.html首页和src -> index.js脚本文件,初始化首页基本的结构
运行npm install jquery -s命令,安装jQuery,通过ES6模块化的方式导入jQuery,实现列表隔行变色效果。其中ul-li快速创建:ul>li{这是第$个li}*9回车。
webpack可以帮助用户将兼容性代码转为非兼容性代码
在代码文件根目录执行命令:
bash
cnpm i webpack-dev-server webpack-cli webpack -D
(2)webpack使用
①在项目根目录创建名为webpack.config.js的webpack配置文件,并初始化如下的配置文件:
javascript
module.exports = {
mode: 'development' // mode 用来制定构建模式,可选值有development 和 production
}
②在package.json的scripts节点下,添加dev脚本如下:
XML
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack"
},
③在终端中运行npm run dev命令,启动webpack进行项目的打包构建。会生成dist文件夹(里面是main.js),index.html引入的js替换为../dist/main.js,这样html就可以正常运行了。
(3)webpack.config.js 文件的作用
webpack.config.js 是 webpack的配置文件。webpack在真正开始打包构建之前,会先读取这个配置文件,从而基于给定的配置,对项目进行打包。
注意:由于webpack是基于node.js开发出来的打包工具,因此在它的配置文件中,支持使用node.js 相关的语法和模块进行webpack的个性化配置。
(4)webpack中的默认约定
》在webpack中有如下的默认约定:
①默认的打包入口文件为src -> index.js ②默认的输出文件路径为dist -> main.js
注意:可以在webpack.config.js中修改打包的默认约定
》自定义打包的入口与出口
在webpack.config.js配置文件中,通过entry节点指定打包的入口。通过output节点指定打包的出口。示例代码如下:
javascript
const path = require('path')
module.exports = {
mode: 'development', // mode 用来制定构建模式,可选值有development 和 production
entry: path.join(__dirname, './src/index.js'), // 打包入口文件的路径
output: {
path: path.join(__dirname, './dist'), //输出文件的存放路径
filename: 'bundle.js' // 输出文件的名称
}
}
(5)webpack插件的作用
通过安装和配置第三方的插件,可以拓展webpack的能力,从而让webpack用起来更方便。最常用的webpack插件有如下两个:
①webpack-dev-server
●类似于node.js阶段用到的nodemon工具
●每当修改了 源代码, webpack会自动进行项目的打包和构建,即package.json的scripts节点的dev值改为"webpack serve"
②html-webpack-plugin
●webpack 中的HTML插件( 类似于于一个模板引擎插件)
●可以通过此插件自定制index.html页面的内容
bash
cnpm i html-webpack-plugin -D
配置方式:
效果如下:
javascript
const path = require('path')
// 导入插件,得到构造函数
const HtmlPlugin = require('html-webpack-plugin')
// 创建插件的实例对象
const htmlPlugin = new HtmlPlugin({
template: './src/index.html',
filename: './index.html'
})
module.exports = {
mode: 'development', // mode 用来制定构建模式,可选值有development 和 production
entry: path.join(__dirname, './src/index.js'), // 打包入口文件的路径
output: {
path: path.join(__dirname, './dist'), //输出文件的存放路径
filename: 'bundle.js' // 输出文件的名称
},
plugins: [htmlPlugin] // 挂载插件的实例对象
}
注意:通过HTML插件复制到项目根目录中的index.html页面,也被放到了内存中;HTML插件在生成的index.html页面的底部,自动注入了打包的bundle.js 文件。
(6)devServer节点
在webpack.config.js配置文件中,可以通过devServer节点对webpack-dev-server插件进行更多的配置,open(初次打包之后,直接打开浏览器)、host(实时打包所用的主机地址)、port(d端口号)
(7)loader
在实际开发过程中,webpack默认只能打包处理以.js后缀名结尾的模块。其他非.js后缀名结尾的模块,webpack 默认处理不了,需要调用loader加载器才可以正常打包,否则会报错!
7.1 处理css的loader
在index.js里面导入css:import './css/index/css'
①:npm i style-loader css-loader -D,安装css的loader;②在webpack.config.js的module->rules数组里面添加规则,其中test表示匹配的文件烈性,use表示要调用的loader,loader顺序不能错。
javascript
module: {
rules: [{ test: /\.css$/, use: ['style-loader', 'css-loader'] }]
}
7.2 处理less的loader
①:运行npm i less-loader less -D命令;②在webpack.config.js的moudle->rules数组添加规则:
bash
rules: [
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
{ test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] }
]
其中安装的less是less-loader的内置依赖项。
7.3 打包处理样式表中与url相关的文件
在webpack5中url-loader、file-loader已经弃用。
二、vue3简介
1. vue3的优势
更容易维护:组合式API,更好的TypeScript支持;更快的速度:重写diff算法,模版变异优化,更高效的组件初始化;更小的提价:良好的TreeShaking,按需引入;更优的数据响应式:Proxy
2. 认识create-vue
creat-vue是Vue官方新的脚手架工具,底层切换到了vite(下一代构建工具)。
前提环境条件:node 版本高于16.0。创建一个vue应用:npm init vue@lastest,这一指令将会安装并执行Create-vue。安装慢的话需要用到:npm 使用快速的安装源(nrm)npm 使用快速的安装源(nrm) - 编程教程
按照绿字的提示输入命令,这样就可以打开一个vue3的项目了。使用vscode打开该项目,查看项目结构和目录。
vue2项目的main.js使用new vue()创建一个应用实例,vue3使用createApp() / createRouter() createStore(),将创建实例进行了封装,保证每个实例的独立封闭性,mount设置挂载点,#app(id魏app的盒子,即index.html里面id为app的div)。vue3需要vscode安装volar插件,具体如下:
3. 组合式api
3.1 setup
3.1.1、setup选项的执行时机:beforeCreate钩子之前自动执行
3.1.2.、setup写代码的特点:定义数据+函数然后以对象方式return
3.1.3、<script setup>解决了什么问题?经过语法糖的封装更简单的使用组合式API
3.1.4、setup中的this还指向组件实例吗?指向undefined
3.2 reactive和ref函数
推荐:声明数据,统一使用ref。
3.2.1 reactive()
作用:接受对象函数类型数据的参数传入并返回一个响应式的对象
步骤:从vue包导入reactive函数;在<script setup>中执行reactive函数并传入类型为对象的初始值,并使变量接收返回的值。
3.2.2 ref()
作用:接受简单类型或复杂类型,返回一个响应式的对象,本质上是在原有传入数据的基础上,外层包了一层对象,包成了复杂类型。
注意点:script访问数据,需要通过.value;template里面不需要.value
3.3、computed计算属性函数
计算属性基本思想和Vue2的完全一致,组合式API下的计算属性只是修改了写法。
核心步骤:1.导入computed函数;2.执行函数在回调参数中return基于响应式数据做计算的值,用变量接收。
注意:(1)计算属性中不应该有"副作用",比如异步请求/修改dom;(2)避免直接修改计算属性的值,计算属性应该是只读的,特殊情况可以配置get set,参见响应式 API:核心 | Vue.js。
3.4、watch
3.4.1 侦听单个数据
(1)导入watch函数;(2)执行watch函数传入要侦听的响应式数据(ref对象 )和回调函数。
3.4.2 侦听多个数据
同时侦听多个响应式数据的变化,不管哪个数据变化都要触发回调。
3.4.3 immediate
说明:在侦听器创建时立即触发回调,响应式数据变化之后继续执行回调。
3.4.4 deep:true
说明:深度监视,默认watch进行的是浅层监视;const ref1 = ref(简单类型)可以直接监视;const ref2 = ref(复杂类型)监视不到复杂类型内部数据的变化。
如果不开启deep的前提,要精确侦听对象的某个属性,如下写法:
3.4.5 总结
(1)作为watch函数的第一个参数,ref对象要添加.value吗?不要,第一个参数就是传ref 对象;
(2)watch只能侦听单个数据吗?单个或者多个;
(3)不开启deep,直接监视复杂类型,修改属性能触发回调吗?不能,默认是浅层侦听。
(4)不开启deep,精确侦听对象的某个属性?可以把第一个参数写成函数的写法,返回要监听的具体属性。
3.5、生命周期函数
比如:
3.6、父子通信
3.6.1 父传子
基本思想:父组件给子组件绑定属性,子组件内部通过props选项接收。
子组件由于写了setup,所以无法直接配置 props选项,所以此处需要借助于"编译器宏"函数接收子组件传递的数据;对于props传递过来的数据,模板中可以直接使用。
3.6.2 子传父
基本思想:父组件中给子组件标签通过@绑定事件;子组件内部通过emit方法触发事件。
3.6.3 总结
3.6.3.1 父传子
(1)父传子的过程中通过什么方式接收props?
defineProps({属性名:类型})
(2)setup语法糖中如何使用父组件传过来的数据?
const props = defineProps({属性名:类型}) props.xxx
3.6.3.2 子传父
(1)子传父的过程中通过什么方式得到emit方法?
defineEmits(['事件名称])
(2)怎么触发事件
emit('自定义事件名',参数)
3.7、模板引用
通过ref标识获取真实的dom对象或者组件实例对象。
通过ref函数生成一个ref对象,名称要和模版元素ref一致;通过ref标识绑定ref对象到标签。
默认情况下在<script setup>语法糖下组件内部的属性和方法是不开放给父组件访问的,可以通过defineExpose编译宏指定哪些属性和方法允许访问。
总结:
获取模板引用的时机是什么?组件挂载完毕
defineExpose编译宏的作用是什么?显式暴露组件内部的属性和方法
3.8、provide和inject
跨层传递普通数据:1.顶层组件通过provide函数提供数据;2.底层组件通过inject函数获取数据。
跨层传递响应式数据:在调用provide函数时,第二个参数设置为ref对象。
跨层传递方法:顶层组件可以向底层组件传递方法,底层组件调用方法修改顶层组件中的数据
4、vue3.3新特性
背景说明:
有<script setup>之前,如果要定义props,emits 可以轻而易举地添加一个与setup平级的属性。但是用了<script setup>后,就没法这么干了setup属性已经没有了,自然无法添加与其平级的属性。
为了解决这一问题,引入了defineProps 与defineEmits 这两个宏。但这只解决了props与emits这两个属性。如果我们要定义组件的name或其他自定义的属性,还是得回到最原始的用法------再添加一个普通的<script>标签。这样就会存在两个<script>标签。让人无法接受。
所以在Vue 3.3中新引入了defineOptions宏。顾名思义,主要是用来定义Options API的选项。可以用defineOptions定义任意的选项,props, emits, expose, slots 除外(因为这些可以使用defineXXX来做到)
4.1、defineOptions
4.2、defineModel
在Vue3中,自定义组件上使用v-model,相当于传递一个modelvalue属性,同时触发 update:modelvalue 事件。我们需要先定义props,再定义emits。其中有许多重复的代码。如果需要修改此值,还需要手动调用emit 函数。
如果使用defineModel,代码如下,需要修改vite.config.js,开启defineModel,重启:
5、Pinia
Pinia使Vue3的官方状态管理工具,是Vuex的替代品。
(1)提供更加简单的API(去掉了mutation )
(2)提供符合,组合式风格的API(和Vue3新语法统一)
(3)去掉了modules的概念,每一个store都是一个独立的模块
(4)配合TypeScript更加友好,提供可靠的类型推断
5.1、使用
按照官方文档安装pinia到项目。Pinia | Pinia
5.1.1 开始
javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia).mount('#app')
5.1.2 定义store
javascript
// @/store/counter.js
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
const double = computed(() => count.value * 2)
const msg = ref('hello pinia')
return { count, msg, double, increment, decrement }
})
javascript
// @/store/channels.js 支持异步
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
console.log(data.channels)
}
// 声明getters相关
return { channelList, getList }
})
5.1.3 使用store
html
<!-- App.vue,子组件可忽略 -->
<script setup>
import Son1 from '@/components/SonView1.vue'
import Son2 from '@/components/SonView2.vue'
import { useCounterStore } from '@/store/counter'
const counterStore = useCounterStore()
import { useChannelStore } from '@/store/channel.js'
const channelStore = useChannelStore()
</script>
<template>
<div>
<h3>APP.vue根组件 -{{ counterStore.count }} -{{ counterStore.msg }} - {{ counterStore.double }}</h3>
<Son1 />
<Son2 />
<hr />
<button type="button" @click="channelStore.getList">channel</button>
<ul>
<li v-for="item in channelStore.channelList" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
<style scoped></style>
另一种写法, 为了从 store 中提取属性时保持其响应性,你需要使用 storeToRefs()
。它将为每一个响应式属性创建引用。当你只使用 store 的状态而不调用任何 action 时,它会非常有用。请注意,你可以直接从 store 中解构 action,因为它们也被绑定到 store 上。
javascript
// 官网代码
<script setup>
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// `name` 和 `doubleCount` 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
const { name, doubleCount } = storeToRefs(store)
// 作为 action 的 increment 可以直接解构
const { increment } = store
</script>
5.1.4 pinia持久化插件
官方文档:快速开始 | pinia-plugin-persistedstate
(1)安装插件:npm i pinia-plugin-persistedstate
(2)main.js使用:
javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia.use(persist)).mount('#app')
(3)store仓库中,pereist:true开启
5.1.5 总结
(1)Pinia是用来做什么的?新一代的状态管理工具,替代vuex
(2)Pinia中还需要mutation吗?不需要,action既支持同步也支持异步
(3)Pinia如何实现getter?computed计算属性函数
(4)Pinia产生的Store如何解构赋值数据保持响应式?storeToRefs
(5)Pinia 如何快速实现持久化?pinia-plugin-persistedstate
6. 项目:大事件管理系统
简介:使用vue3 compositionAPI,Pinia/Pinia持久化处理,Element Plus(表单校验、表格处理、组件封装),pnpm包管理升级,Eslint+prettier更规范的配置,husky(Git hooks工具)代码提交之前进行校验,球球模块设置,VueRouter4路由设计,AI大模型开发一整个项目模块(掌握最新的开发方式)
6.1 pnpm包管理器
一些优势:比同类工具快2倍左右、节省磁盘空间... https://www.pnpm.cn/
安装方式::npm install -g pnpm 创建项目:pnpm create vue。注意不要在磁盘根目录创建项目,否则可能报权限不足。
6.2 Eslint配置代码风格
配置文件.eslintrc.cjs
(1)prettier风格配置:https://pretties.io。禁用prettier code format插件,安装Eslint插件, 修改.eslintrc.cjs:
javascript
module.exports = {
...
rules: {
// 禁用格式化插件,prettier format on save,
// 安装Eslint插件,并配置保存时自动修复。
// prettier关注代码美观度
'prettier/prettier': [
'warn',
{
singleQuote: true, // 单引号
semi: false, // 无分号
printWidth: 80, // 每行宽度至多80字符
trailingComma: 'none', // 不加对象|数组最后逗号
endOfLine: 'auto' // 换行符号不限制(win mac 不一致)
}
],
// eslint关注规范
'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'
}
}
(2)修改vscode的setting.json:
javascript
...
"editor.formatOnSave": false, // #值设置为true时,每次保存的时候自动格式化;
"editor.codeActionsOnSave": {
"source.fixAll": true
},
...
6.3 提交前做代码检查
6.3.1 husky
husky是一个git hooks工具( git的钩子工具,可以在特定时机执行特定的命令)
(1)初始化git仓库,执行git init即可。
(2)初始化husky工具配置,执行pnpm dlx husky-init && pnpm install 即可、https://typicode.github.io/husky/
(3)修改.husky/pre-commit文件。
javascript
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# npm test
pnpm lint
pnpm lint检验的是全部代码。
6.3.2 暂存区eslint校验
-
安装lint-staged包:pnpm i lint-staged -D
-
package.json配置lint-staged命令
javascript
```jsx
{
// ... 省略 ...
"lint-staged": {
"*.{js,ts,vue}": [
"eslint --fix"
]
}
}
{
"scripts": {
// ... 省略 ...
"lint-staged": "lint-staged"
}
}
```
- .husky/pre-commit文件修改
javascript
```jsx
pnpm lint-staged
```
6.4 目录调整
默认生成的目录结构不满足我们的开发需求,所以这里需要做一些自定义改动。主要是以下工作:
1.删除一些初始化的默认文件;主要是components、assets、router/index.js
2.修改剩余代码内容;主要是App.vue
3.新增调整我们需要的目录结构;比如api文件夹
4.拷贝全局样式和图片,安装预处理器支持:pnpm add sass -D
6.5 Vue-Router4 路由代码解析
6.5.1 路由初始化
以上为Vue2 => Vue3的router变化
(1)创建路由实例由createRouter 实现
(2)路由模式:history模式使用createwebHistory();hash模式使用createWebHashHistory();参数是基础路径,默认/
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
// createRouter创建路由实例
// 配置history模式
// 1. history模式:createwebHistory:地址栏不带#
// 2. hash模式: createwebHashHistory:地址栏带#
// vite环境变量:import.meta.env.BASE_URL
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: []
})
export default router
修改import.meta.env.BASE_URL需要在vite.config.js添加base参数,参看:环境变量和模式 | Vite 官方中文文档
javascript
<script setup>
import { useRoute, useRouter } from 'vue-router'
const router = useRouter()
const route = useRoute()
const goList = () => {
router.push('/list')
console.log(router, route)
}
</script>
<template>
<div>APP</div>
<button @click="$router.push('/home')">跳首页</button>
<button @click="goList">跳列表页</button>
</template>
<style scoped></style>
一种是直接在html里面使用$router.push(),一种是在script里面用useRouter函数。
6.6 按需引入Element Plus
6.6.1 官网地址
6.6.2 步骤
javascript
// 1. 安装
pnpm install element-plus
// 2. 插件
pnpm add -D unplugin-vue-components unplugin-auto-import
// 3.修改vite.config.js的引入项以及plugins参数
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
// 4. 重启服务,button直接改成el-button即可,无须导入。
<el-button @click="$router.push('/home')">跳首页</el-button>
<el-button @click="goList">跳列表页</el-button>
默认components 下的文件也会被自动注册。
6.7 pinia构建用户仓库和持久化
6.7.1 示例目标:
6.7.2 步骤
(1)新建stores仓库
javascript
// stores/user.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
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 // 持久化
}
)
(2)安装持久化插件
javascript
pnpm add pinia-plugin-persistedstate -D
(3)使用main.js
```jsx
import persist from 'pinia-plugin-persistedstate'
...
app.use(createPinia().use(persist))
```
(4)配置 stores/user.js
添加persist参数(最终看上面的代码):
...
{ persist: true // 持久化 }
...
6.7.3 pinia仓库统一管理
(1)pinia独立维护:由 stores 统一维护,在 stores/index.js 中完成 pinia 初始化,交付 main.js 使用
(2)仓库统一导出:由 stores/index.js 统一导出,导入路径统一 `./stores`,而且仓库维护在 stores/modules 中。
新建stores/index.js
javascript
// stores/index.js
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'
const pinia = createPinia().use(persist)
pinia.use(persist)
export default pinia
export * from './modules/user'
export * from './modules/counter'
修改main.js
javascript
// main.js
import { createApp } from 'vue'
import pinia from './stores/index.js'
import App from './App.vue'
import router from './router'
import '@/assets/main.scss'
const app = createApp(App)
app.use(pinia)
app.use(router)
app.mount('#app')
修改app.vue
javascript
<script setup>
...
import { useUserStore, useCounterStore } from '@/stores'
...
const userStore = useUserStore()
const counterStore = useCounterStore()
</script>
6.8 数据交互-请求工具设计
使用 axios 来请求后端接口, 一般都会对 axios 进行一些配置 (比如: 配置基础地址等),一般项目开发中, 都会对 axios 进行基本的二次封装, 单独封装到一个模块中, 便于使用。
新建 `utils/request.js` 封装 axios 模块,利用 axios.create 创建一个自定义的 axios 来使用,axios中文文档|axios中文网 | axios
javascript
import { useUserStore } from '@/stores'
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({
// TODO 1. 基础地址,超时时间
baseURL,
timeout: 100000
})
// 请求拦截器
instance.interceptors.request.use(
(config) => {
// TODO 2. 携带token
const userStore = useUserStore()
if (userStore.token) {
config.headers.Authorization = userStore.token // 根据实际使用修改
}
return config
},
(err) => Promise.reject(err)
)
// 响应拦截器
instance.interceptors.response.use(
(res) => {
// TODO 3. 处理业务失败
// TODO 4. 摘取核心响应数据
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)
// TODO 5. 处理401错误 权限不足或token过期
if (err.response?.status === 401) {
router.push('/login')
}
return Promise.reject(err)
}
)
export default instance
export { baseURL }
6.9 整体路由设计
6.9.1 实现目标
-
完成整体路由规划【搞清楚要做几个页面,它们分别在哪个路由下面,怎么跳转的.....】
-
通过观察, 点击左侧导航, 右侧区域在切换, 那右侧区域内容一直在变, 那这个地方就是一个路由的出口
-
我们需要搭建嵌套路由
-
把项目中所有用到的组件及路由表, 约定下来。
该项目的路由结构如下:
javascript
import { createRouter, createWebHistory } from 'vue-router'
// createRouter创建路由实例
// 配置history模式
// 1. history模式:createwebHistory:地址栏不带#
// 2. hash模式: createwebHashHistory:地址栏带#
// vite环境变量:import.meta.env.BASE_URL
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{ path: '/login', component: () => import('@/views/login/loginPage.vue') },
{
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')
}
]
}
]
})
export default router
6.9.2 登录注册
6.9.2.1 使用element-plus编写模型
(1)结构相关
el-row表示一行,一行分成24份
el-col表示列
(1.1) :span="12" 代表在一行中,占12份 (50%)
(1.2) :span="6" 表示在一行中,占6份 (25%)
(1.3) :offset="3" 代表在一行中,左侧margin份数
el-form 整个表单组件
el-form-item 表单的一行 (一个表单域)
el-input 表单元素(输入框)
(2)校验相关
(2.1) el-form => :model="ruleForm" 绑定的整个form的数据对象 { xxx, xxx, xxx }
(2.2) el-form => :rules="rules" 绑定的整个rules规则对象 { xxx, xxx, xxx }
(2.3) 表单元素 => v-model="ruleForm.xxx" 给表单元素,绑定form的子属性
(2.4) el-form-item => prop配置生效的是哪个校验规则 (和rules中的字段要对应)
6.9.2.2 script编写规则
(1)整个表单的校验规则
(3.1)非空校验 required: true message消息提示, trigger触发校验的时机 blur change
(3.2)长度校验 min:xx, max: xx
(3.3)正则校验 pattern: 正则规则 \S 非空字符
(3.4)自定义校验 => 自己写逻辑校验 (校验函数)
(2) validator: (rule, value, callback)
(4.1) rule 当前校验规则相关的信息
(4.2) value 所校验的表单元素目前的表单值
(4.3) callback 无论成功还是失败,都需要 callback 回调
-
callback() 校验成功
-
callback(new Error(错误信息)) 校验失败
6.9.2.3 注册功能
封装注册api,进行注册,注册成功切换到登录
(1)新建 api/user.js 封装
javascript
import request from '@/utils/request'
export const userRegisterService = ({ username, password, repassword }) =>
request.post('/api/reg', { username, password, repassword })
(2)页面中调用
javascript
const register = async () => {
await form.value.validate()
await userRegisterService(formModel.value)
ElMessage.success('注册成功')
// 切换到登录
isRegister.value = false
}
(3)eslintrc 中声明全局变量名, 解决 ElMessage 报错问题
html
module.exports = {
...
globals: {
ElMessage: 'readonly',
ElMessageBox: 'readonly',
ElLoading: 'readonly'
}
}
6.9.2.4 登录前的预校验
(1)登录请求之前,需要对用户的输入内容,进行校验,校验通过才发送请求。
(2)封装登录API,点击按钮发送登录请求,登录成功存储token,存入pinia 和 持久化本地storage,跳转到首页,给提示
html
...
import { useUserStore } from '@/stores/'
import { useRouter } from 'vue-router'
...
...
const userStore = useUserStore()
const router = useRouter()
...
6.9.3 登录访问拦截
只有登录页,可以未授权的时候访问,其他所有页面,都需要先登录再访问
javascript
// 登录访问拦截 => 默认是直接放行的
// 根据返回值决定,是放行还是拦截
// 返回值:
// 1. undefined / true 直接放行
// 2. false 拦回from的地址页面
// 3. 具体路径或路径对象,拦截到对应的地址
// return '/login' 或 {name: 'login'}
router.beforeEach((to) => {
const userStore = useUserStore()
if (!userStore.token && to.path !== '/login') return '/login'
})
6.9.4 用户基本信息获取&渲染
(1)`api/user.js`封装接口
javascript
export const userGetInfoService = () => request.get('/my/userinfo')
(2)stores/modules/user.js 定义数据
javascript
const user = ref({})
const getUser = async () => {
const res = await userGetInfoService() // 请求获取数据
user.value = res.data.data
}
(3)`layout/LayoutContainer`页面中调用
javascript
import { useUserStore } from '@/stores'
const userStore = useUserStore()
onMounted(() => {
userStore.getUser()
})
6.9.5 退出功能 [element-plus 确认框]
html
<!-- 注册点击事件 -->
<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>
javascript
// 添加退出功能
const onCommand = async (command) => {
if (command === 'logout') {
await ElMessageBox.confirm('你确认退出大事件吗?', '温馨提示', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消'
})
userStore.removeToken()
userStore.setUser({})
router.push(`/login`)
} else {
router.push(`/user/${command}`)
}
}
javascript
// pinia user.js 模块 提供 setUser 方法
const setUser = (obj) => {user.value = obj}