初始化项目
使用官方脚手架搭建项目
技术栈使用 ts
+vue-router
+pinia
eslint+prettier 代码格式化可选,最好也勾上,保持代码风格
npm create vue@latest
Vue.js - The Progressive JavaScript Framework √ 请输入项目名称: ... vue3-test √ 是否使用 TypeScript 语法? ... 否 / 是 √ 是否启用 JSX 支持? ... 否 / 是 √ 是否引入 Vue Router 进行单页面应用开发? ... 否 / 是 √ 是否引入 Pinia 用于状态管理? ... 否 / 是 √ 是否引入 Vitest 用于单元测试? ... 否 / 是 √ 是否要引入一款端到端(End to End)测试工具? » 不需要 √ 是否引入 ESLint 用于代码质量检测? ... 否 / 是 √ 是否引入 Prettier 用于代码格式化? ... 否 / 是
|
配置别名,启动项目
进入目录属性目录文件
在vite.config.ts 配置别名
但是在这里配置的别名,ts和esliint识别不了ts文件
vite.config.ts
resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), '@assets': fileURLToPath(new URL('./src/assets', import.meta.url)), '@components': fileURLToPath(new URL('./src/components', import.meta.url)), '@utils': fileURLToPath(new URL('./src/utils', import.meta.url)) } }
|
在tsconfig.app.json
里配置别名,这样导入的时候就可以识别ts文件了
如果没有tsconfig.app.json
在tsconfig.json
里配置一样的
tsconfig.app.json
"compilerOptions": { "include": ["env.d.ts", "src/**/*.ts", "src/**/*.vue", "src/**/*.d.ts", "src/**/*.tsx"], "paths": { "@/*": ["./src/*"], "@utils/*":["./src/utils/*"] } }
|
配置端口和本地代理
在vite可以配置本地代理,这样在本地调试接口就不会出现跨域了
vite.config.ts
resolve:{... } server: { port: 8086, host: '0.0.0.0', proxy: { ['/api']: { target: 'http://127.0.0.1:3000/', changeOrigin: true, rewrite: (path) => path.replace('/api', ''), }
}, },
|
可以起一个后台服务测试一下,服务端口默认是3000
npm install koa-generator -g koa [demo-name] cd demo-name npm i npm run start
|
在前端请求后台服务
请求地址会通过 localhost:8086/api ->转发到后台服务-> localhost:3000
App.vue
<script setup lang="ts"> import { onMounted } from 'vue' import axios from 'axios' onMounted(async () => { const { data } = await axios({ method: 'get', url: '/api' }) console.log(data) }) </script>
|
全局ts文件类型声明
ts需要写各种类型声明,我们可以放到一个统一的**.d.ts
文件下,比如我在src目录下新建一个project.d.ts
project.d.ts
interface Window { refreshFlag: Boolean; taskList: axiosAgent.PendingTask[]; utils: { refreshToken: () => void } }
|
其中axiosAgent.PendingTask
需要用到另一个d.ts
文件
为什么不放入一个文件里写呢?
因为在类型声明里使用了import, 就会被当做成一个模块,而不是单纯的类型声明,不会被ts解析
所以需要单独写一个d.ts
,声明一个命名空间declare namespace name
,并导出
project_axios.d.ts
import { AxiosRequestConfig } from 'axios'; declare namespace axiosAgent { interface PendingTask { config: AxiosRequestConfig; resolve: Function; } } export = axiosAgent; export as namespace axiosAgent;
|
配置pinia
store基本配置
在stores里声明需要的状态管理器
通过definStore
声明状态管理器
pinia状态管理器只有3个属性: state、getters、actions
state对应状态变量,是个方法,返回一个对象
getters 是状态的计算属性, 是个对象,对象属性可以使用箭头函数
actions 可以同步或异步操作state或其他的actions方法,这里官网没有用箭头函数了
stores/someList.ts
import { defineStore } from 'pinia' type TState = { someList:SList, someCategory:CList } const useSomeList = defineStore('someList',{ state:():TState => { return { someList:[], someCategory: [] } }, getters: { GET_LIST: ({someList}) => { return someList } }, actions: { async getList(categoryId:number) { this.someList = await axios('/api/slist') }, async getCategory() { this.someCategory = await axios('/api/clist') }, async init() { await this.getCategory() await Promise.all(this.someCategory.map(async (item:any) => await this.getList(item.cid))) } } }) export default useSomeList
|
组件中使用
1.引入store的hooks并使用上面的属性
import {useSomeList} from '@/stores/index' const someStore = useSomeList()
someStore.GET_LIST onMounted(async () => { await someStore.init() })
|
store响应式问题
在组件中直接使用store上的state,是没有响应式的,需要通过import { storeToRefs } from 'pinia
转换成响应式,或者转换成computed
的也是可以监听的
组件能监听到这里的state变动
app.vue
import {useSomeList} from '@/stores/index' import { storeToRefs } from 'pinia' const someStore = useSomeList()
const { someList, someCategory } = storeToRefs(someStore) const listState = computed(() => someStore.someList);
|
组件不能监听到直接解构state
的的数据变动
app.vue
import {useSomeList} from '@/stores/index' const someStore = useSomeList()
const { increment } = store
|
页面测试效果
编写一个组件,遍历不同场景下someStore.someList
数据
components/StateTest.vue
<template> <div class="state-test" :style="`border:1px solid ${props.color}`"> <div>{{ props.title }}</div> <div class="test" v-for="(item, index) in props.someList" :key="index" :style="`height: 40px; width: 100px; color:${props.color} ;`"> <div>{{ item.sname }}</div> </div> </div> </template>
<script lang='ts' setup> const props = defineProps({ someList: { type: Object, default: () => { } }, title: { type: String, default: '' }, color: { type: String, default: '' } }) </script>
|
在onMounted
调用someStore.init()
初始化数据,但是数据需要等待五秒才会加载出来,因为一开始的token是错误的,会把请求放入window.taskList
,等待五秒之后更新为正确token重新获取数据,axios请求逻辑请观看下一节
组件参数some-list
分别传入computed
、storeToRefs
、直接解构
和直接使用store.state
操作后的数据,观察数据变化
app.vue
<template> <header> <div class="wrapper"> <state-test color="aquamarine" title="计算属性" :some-list="listState"></state-test> <state-test color="pink" title="直接解构" :some-list="someList"></state-test> <state-test color="orange" title="使用storeToRefs" :some-list="someListRef"></state-test> <state-test color="red" title="直接使用store.state" :some-list="someStore.someList"></state-test> </div> </header> <RouterView /> </template> <script setup lang="ts"> import { onMounted, computed } from 'vue' import { useSomeList } from '@/stores/index' import { storeToRefs } from 'pinia' import StateTest from '@/components/StateTest.vue' const someStore = useSomeList()
const { someList:someListRef } = storeToRefs(someStore) const { someList } = someStore const listState = computed(() => someStore.someList); onMounted(async () => { await someStore.init() }) </script>
|
配置axios
需求中有个重放请求的逻辑,当token失效的时候,如果会去更新token,当拿到新token的时候,重放刚刚的请求
token失效重试
如果后台返回了过期的状态码,那么就将全局过期标志refreshFlag
制为true
,通过refreshToken
方法重新获取新的token, 并将当前的请求放到taskList
队列里等待token更新后,重新请求
utils/request.ts
axios.interceptors.response.use(response => { if (code === RET_ENUM.EXPIRE) { window.refreshFlag = true console.error('登陆太过期') if (!window.taskList) window.taskList = [] window.utils.refreshToken() return new Promise(resolve => { window.taskList.push({ config, resolve }) }) } return response })
|
收集token失效后的请求
如果refreshFlag
标志为true
,则说明token过期将后续的请求全部收集放入taskList
,等待token更新后重新发起请求
utils/request.ts
axios.interceptors.response.use(response => { const {config} = response if(window.refreshFlag) { if(!window.taskList) window.taskList = [] return new Promosie(resolve => { window.taskList.push({ config, resolve }) }) } })
|
当token更新后,重放请求
refreshToken
方法模拟token
更新操作,从config.data
里取出请求数据,并更新config.data.token
为正确的token,然后从失败的请求队列taskList
里并重放刚刚的失败请求, 通过resolve
返回的结果
utils/winUtils
import axios from "axios"; window.utils = { refreshToken: () => { setTimeout(async () => { if (window.refreshFlag) { window.refreshFlag = false await Promise.all(window.taskList.map(async ({ config, resolve }) => { const newData = config.data instanceof Object ? JSON.parse(JSON.stringify(config.data)) : JSON.parse(config.data); newData.token = '123456'; config.data = newData; resolve(await axios(config)); }))
} }, 5000); } }
|
组件批量传参
有时候传参我们不想单个的传可以使用v-bind={propA:'value2', propB: 'value2'}
批量传参数
增加一个需求,每个软件名字旁边有一个按钮,不同cid
类型的软件,按钮风格不一样
将StateTest
组件再细化一下,里面的文案和按钮部分拆分为SoftButton
通过props传入ghost
和color
在getClass
方法中判断并改变buttonClass
,让button
获取对应的样式
components/SoftButton.vue
<template> <div class="sitem-contain"> <div>{{ props.sname }}</div> <button :style="buttonClass">下载</button> </div> </template>
<script lang='ts' setup> import { onMounted, ref } from 'vue'
const props = defineProps({ sname: { type: String, default: '' }, ghost: { type: Boolean, default: false }, color: { type: String, default: '' } }) const buttonClass = ref({}) const getClass = () => { if (props.ghost === true) { buttonClass.value = { backgroundColor: '#FFF', border: `1px solid ${props.color}`, color: `${props.color}` } return } buttonClass.value = { backgroundColor: `${props.color}`, border: `1px solid ${props.color}`, color: '#FFF' } } getClass() </script>
|
StateTest
将原来的文案部分替换为soft-button
组件,并通过v-bind="getTheme(item.cid)
判断对应的props值
components/StateTest.vue
<template> <div class="state-test" :style="`border:1px solid ${props.color}`"> <div>{{ props.title }}</div> <div class="test" v-for="(item, index) in props.someList" :key="index" :style="`height: 40px; width: 100px; color:${props.color} ;`"> <soft-button :sname="item.sname" v-bind="getTheme(item.cid)"></soft-button> </div> </div> </template>
<script lang='ts' setup> import SoftButton from '@/components/SoftButton.vue' const props = defineProps({ someList: { type: Object, default: () => { } }, title: { type: String, default: '' }, color: { type: String, default: '' } }) const getTheme = (cid:number) => { if(cid === 1) { return { ghost: true, color: props.color } } return { ghost: false, color: props.color } } </script>
|
props与toRefs的坑
在给子组件soft-button
传参使用的是一个函数,多少有点不雅观,于是想给每个someList
的item
增加ghost
和color
属性,这样就可以直接使用item.ghost
,item.color
传参了
添加属性的方式有很多,第一种直接替换整个数组
踩坑1
通过toRefs
解构props对象,然后再通过watch
,每次监听到props.someList
有改动的时候,替换该属性,在组件上遍历someListNew
@/components/StateTest.vue
<template> <div class="state-test" :style="`border:1px solid ${props.color}`"> <div class="test" v-for="(item, index) in someListNew" :key="index" :style="`height: 40px; width: 100px; color:${item.color} ;`"> <soft-button :sname="item.sname" v-bind="{ ghost: item.ghost, color: item.color }"></soft-button> </div> </div> </template> <script lang='ts' setup>
type Props = { someList: SList[], title: string, color: string } const props = defineProps<Props>()
const { someList: someListNew } = toRefs(props)
watch(() => props.someList, () => { getTheme() }, { deep: true })
const getTheme = () => { const newSList = someListNew.value.map((item: SList): any => { if (item.cid === 1) { return { ...item, ghost: true, color: props.color } } else { return { ...item, ghost: false, color: props.color } } }) someListNew.value = newSList } </script>
|
键盘啪啪一顿操作,结果发现页面展示效果和预期的不一样,打开控制台一看,原来props解构出来的都是readonly
属性,是不让修改的
踩坑2(不建议)
虽然可以直接去修改item上的属性,通过someListNew.value.map
出来的item
也是响应式的
但是这样会改动最原始的对象,pinia上的属性导致渲染出来的都是最后加载出来的红色
@/components/StateTest.vue
<template> <div class="state-test" :style="`border:1px solid ${props.color}`"> <div class="test" v-for="(item, index) in someListNew" :key="index" :style="`height: 40px; width: 100px; color:${item.color} ;`"> <soft-button :sname="item.sname" v-bind="{ ghost: item.ghost, color: item.color }"></soft-button> </div> </div> </template> <script lang='ts' setup>
const getTheme = () => { const newSList = someListNew.value.map((item: SList): any => { if (item.cid === 1) { item.ghost = true item.color = props.color return } else { item.ghost = false item.color = props.color return } }) } </script>
|
方法
重新通过ref
声明一个变量,去接收someListNew.map
返回的新数组,记得修改组件遍历对象为mySomeList
@/components/StateTest.vue
<template> <div class="state-test" :style="`border:1px solid ${props.color}`"> <div class="test" v-for="(item, index) in mySomeList" :key="index" :style="`height: 40px; width: 100px; color:${item.color} ;`"> <soft-button :sname="item.sname" v-bind="{ ghost: item.ghost, color: item.color }"></soft-button> </div> </div> </template> <script lang='ts' setup>
const mySomeList = ref<SList[]>([])
const getTheme = () => { const newSList = someListNew.value.map((item: SList): any => { if (item.cid === 1) { return { ...item, ghost: true, color: props.color } } else { return { ...item, ghost: false, color: props.color } } }) mySomeList.value = newSList } </script>
|
组件事件操作
通过上面操作已经可以正常展示组件内容了,现在增加一点交互事件,当点击下载的时候让每个软件item
变成灰色,并且按钮变为已下载
给soft-button
组件在增加一个down
下载监听事件,每次点击下载就触发changeColor
改变对应的软件的color
@/components/StateTest.vue
<soft-button :sid="item.sid" :sname="item.sname" v-bind="{ ghost: item.ghost, color: item.color }" @down="changeColor"></soft-button>
<script lang='ts' setup>
const changeColor = (sid:number) => { const sitem = mySomeList.value.find(item => item.sid === sid) if(!sitem) return sitem.color = 'gray' }
</script>
|
在soft-button
编写触发逻辑,当buttton
按钮点击时,监听 click
事件来触发down
事件
还需要监听props.color
,每次变动的时候需要更新样式
<template> <button :style="buttonClass" @click="changeColor">{{ downText }}</button> </template>
<script lang='ts' setup> import { computed, ref, watch } from 'vue'
const emits = defineEmits(['down']) const props = defineProps({ sid: { type: Number, default: null }, })
const downText = computed((() => props.color === 'gray' ? '已下载' : '下载'))
const changeColor = () => { if(props.color === 'gray') return emits('down', props.sid) } const getClass = () => { if (props.color === 'gray') { buttonClass.value = { backgroundColor: '#FFF', border: `1px solid ${props.color}`, color: `${props.color}` } return } }
watch(()=>props.color, ()=>{ getClass() },{ immediate: true }) </script>
|
setup外的store和router操作
在setup里面我们可以使用store
的hook和router
的hooks函数
<script setup lang="ts"> import StateTest from '@/components/StateTest.vue' import {useRouter } from 'vue-router' const router = useRouter()
const someStore = useSomeList() </script>
|
但是在setup外面,router
就不能使用hooks了,比如在axios请求的失败的时候需要跳转,就需要通过createRouter
创建的router
进行操作了
import router from '@/router/index' import {useSomeList} from '@/stores/index' try {
}catch(err){ router.push('/home') const store = useSomeList() console.log(store.someList) }
|
总结
通过上面的练习基本把vue3的流程跑了一遍,简单熟悉了一下props
,pinia
,axios
,router
和一些事件操作,由于项目太赶了都是按照以前的经验快速开发,后期还需要多看官方文档巩固vue3的基础概念
参考
vue.js
git仓库
vue-test
vue-test-serve