组合式函数

在vue3中,不同的组件有时候会存在一些相同的逻辑和,比如一些特定的字符串转换操作,数据的清洗。这个时候我们通常会将这一部分的逻辑封装成一个函数或者工具类,以便后续开发的对齐进行一个复用。
以下是一个简单的日期格式化函数示例

1
2
3
4
5
// dateFormatter.js
export function formatDate(date, format) {
const options = {year: 'numeric', month: '2-digit', day: '2-digit'};
return new Date(date).toLocaleDateString(undefined, options);
}

在vue组件中使用这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//vue

<template>
<div>{{formattedDate}}</div>
</template>

<script>
import {formatDate} from './dateFormatter';

export default {

data() {
return {
date: new Date(),
};
},
computed: {
formattedDate() {
return formatDate(this.date, 'YYYY-MM-DD');
},
},
};
</script>

理解无状态逻辑与有状态逻辑

  • 无状态逻辑:formatDate 函数是无状态的,因为它不依赖于外部状态,只根据传入的日期和格式返回格式化后的日期字符串。它的行为是确定的,给定相同的输入,总是会返回相同的输出。
  • 有状态逻辑:如果函数或组件需要管理和维护随时间变化的状态(例如,跟踪用户输入、管理组件的内部状态等),则为有状态逻辑。

vue3能更好的复用有状态的逻辑

  • 响应式核心api的抽离,诸如ref(),computed(),watch(),生命周期钩子等核心api的抽离,对于复用有状态的逻辑变得更加的容易。
  • 组合式api允许我们在组合函数中使用生命周期钩子。

这种方式赋予了组件更加细粒度的控制。而在vue2里面要达到这样的效果是比较麻烦的,而且代码难以去维护。

分析与实践

通过对以下效果的分析,基于vue3组织代码逻辑并且编码

左边与右边把它们分别看成独立的组件,因为呈现的布局与样式都不一样,但是都是基于同一份数据进行显示的。

启动项目

采用基于 vite 的脚手架 crate vue ,这里我使用的包管理工具是 yarn

1
yarn create vue
  • 删除无用的静态资源文件与模板组件,并且创建对应的文件,整体的目录目录如下
1
2
3
4
5
6
7
8
src
├── App.vue
├── components
│ ├── Bar1.vue
│ └── Bar2.vue
├── main.js
└── compositions
└── useGdp.js

相关文件内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<!--vue3-->
<template>
<h1>2024 GDP Top 5</h1>
<div class="container">
<Bar1 :gdp="gdp"/>
<Bar2 :gdp="gdp"/>
</div>
<div class="controls">
<div class="item" v-for="item in gdp" :key="item.country">
<label>{{ item.country }}</label>
<input type="number" step="0.001" min="0" v-model="item.value"/>
</div>
</div>
</template>

<script>
import {ref} from "vue";
import Bar1 from "@/components/Bar1.vue";
import Bar2 from "@/components/Bar2.vue";

export default {
components: {Bar2, Bar1},
setup() {
const gdp = ref([])

async function fetchGDP() {
gdp.value = await fetch("/api/gdp.json").then(res => res.json())
console.log(gdp.value)
}

fetchGDP()
return {
gdp
}
},
}
</script>

<style scoped>
.container {
display: flex;
justify-content: center;
flex-wrap: wrap;
}

.controls {
margin: 1em;
display: flex;
justify-content: center;
flex-wrap: wrap;
}

.item {
margin: 1em;
}

.item label {
margin: 0 1em;
}

.item input {
height: 26px;
font-size: 14px;
}

h1 {
text-align: center;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<!--vue3-->
<template>
<div class="bar1">
<div class="item" v-for="item in bars" :key="item.country">
<label>{{ item.country }}</label>
<div class="bar" :style="{background: item.color,width: item.size + 'px'}"></div>
<div class="value">{{ item.value }}万亿</div>
</div>
</div>
</template>

<script>
import useGdp from "@/composition/useGdp.js";
import {computed} from "vue";

export default {
props: ["gdp"],
setup(props) {
const gpd = computed(() => props.gdp)
return {
...useGdp(gpd, 400)
}
}
}

</script>


<style scoped>
.bar1 {
width: 500px;
box-sizing: border-box;
margin: 3em;
border-left: 1px solid #333;
}

.item {
display: flex;
height: 35px;
line-height: 35px;
margin: 1em 0;
position: relative;
}

.bar {
width: 100px;
height: 100%;
margin-right: 1em;
flex: 0 0 auto;
}

.item label {
position: absolute;
left: -50px;
}

.value {
flex: 0 0 auto;
}

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<!--vue3-->
<template>
<div class="bar2">
<div class="item" v-for="item in bars" :key="item.country">
<label>{{ item.country }}</label>
<div class="bar" :style="{background: item.color,width: item.size + 'px'}"></div>
<div class="value">{{ item.value }}万亿</div>
</div>
</div>
</template>

<script>
import useGdp from "@/composition/useGdp.js";
import {computed} from "vue";

export default {
props: ["gdp"],
setup(props) {
const gpd = computed(() => props.gdp)
return {
...useGdp(gpd, 400)
}
}
}

</script>


<style scoped>
.bar2 {
width: 600px;
box-sizing: border-box;
margin: 3em;
position: relative;
}

.bar2::before {
content: "";
display: block;
width: 1px;
height: 100%;
position: absolute;
background: #666;
left: 50%;
}

.item {
display: flex;
height: 35px;
line-height: 35px;
margin: 1em 0;
position: relative;
justify-content: center;
}

.bar {
width: 100px;
height: 100%;
margin-right: 1em;
flex: 0 0 auto;
}

.item label {
transform: translateX(-50%);
text-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}

.item .value {
color: #2c3e50;
}
</style>

这里使用到了一个第三方库,gsap,用于动画效果的展示。
这里简要说明一下,bars和barsTarget的关系,bars是一个ref,barsTarget是一个computed,当barsTarget变化的时候,我们通过watch监听barsTarget的变化,然后将bars变化到barsTarget,这里使用到了gsap,让数据逐步变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import {computed, ref, watch} from "vue";
import {gsap} from "gsap";

/**
* 自定义钩子 useGdp,用于处理 GDP 数据
* 该钩子接受一个引用(gdpRef)和一个最大尺寸(maxSize)作为参数
* 主要功能是根据提供的 GDP 数据和最大尺寸限制来调整数据的显示方式
*
* @param {Object} gdpRef - 一个引用对象,通常包含 GDP 数据
* @param {number} maxSize - 允许显示的最大尺寸
* @returns {Object} - 返回一个对象,包含处理后的 GDP 数据信息
*/
export default function useGdp(gdpRef, maxSize) {
const colors = ["#334552", "#B34335", "#6E9FA5", "#A2C3AC", "#C8846C"]
const maxValue = computed(() => {
if (gdpRef.value.length) {
return Math.max(...gdpRef.value.map(it => it.value))
}
})
const bars = ref([])
const barsTarget = computed(() => {
console.log("computed")
return gdpRef.value.map((it, i) => ({
...it,
color: colors[i % colors.length],
size: (it.value / maxValue.value) * maxSize
}))
})
watch(barsTarget, () => {
// 将bars变化到barsTarget
barsTarget.value.forEach((item, i) => {
bars.value[i] = {
...barsTarget.value[i],
size: 0,
value: 0,
}
// 逐步变化
gsap.to(bars.value[i], {
...barsTarget.value[i],
duration: 1,
})
})


}, {deep: true})

return {
bars
}
}
1
2
3
4
5
import {createApp} from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

代理配置,基于vite的代理配置

通过代理配置,我们可以将请求代理到本地的json文件,这样我们就可以在本地数据的基础上进行开发。对于这部分的知识可以在官方文档中进行了解vite代理配置
本站也有相关的学习记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server:{
proxy:{
'/api': {
target: 'http://localhost:5173/src/api/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
}
}
})