Web学习笔记-Vue3(用户动态页面设计)

  1. 1. 实现用户信息模块
  2. 2. 实现关注用户功能
  3. 3. 实现历史动态模块
  4. 4. 实现发动态模块

本文记录 Vue3 的学习过程,内容为用户动态页面设计。

1. 实现用户信息模块

用户动态页面可以划分为三个模块:用户信息部分、发动态部分(如果是自己才有发动态功能)、历史动态展示部分,因此我们可以用三个组件来实现(在 components 目录下创建 UserNewsInfo.vueUserNewsSend.vue 以及 UserNewsPosts.vue 三个组件)。

其中 UserNewsInfo 内容如下:

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
<template>
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-4">
<img
class="img-fluid"
src="https://cdn.acwing.com/media/user/profile/photo/82581_lg_e9bdbcb8aa.jpg"
/>
</div>
<div class="col-md-8">
<div class="username">AsanoSaki</div>
<div class="fans">粉丝:123</div>
<button type="button" class="btn btn-secondary btn-sm">+关注</button>
</div>
</div>
</div>
</div>
</template>

<script>
export default {
name: "UserNewsInfo",
};
</script>

<style scoped>
img {
border-radius: 50%;
}

button {
padding: 2px 4px;
font-size: 14px;
width: 70px;
}

.username {
font-size: 20px;
font-weight: bold;
}

.fans {
font-size: 14px;
color: gray;
}
</style>

UserNewsPosts 内容如下:

1
2
3
4
5
6
7
8
9
<template>
<div class="card">
<div class="card-body"></div>
</div>
</template>

<script></script>

<style scoped></style>

然后我们在 UserNewsView 中先将这两个组件添加进去:

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
<template>
<Card title="用户动态">
<div class="row">
<div class="col-md-3">
<UserNewsInfo />
</div>
<div class="col-md-9">
<UserNewsPosts />
</div>
</div>
</Card>
</template>

<script>
// @ is an alias to /src
import Card from "@/components/Card.vue";
import UserNewsInfo from "@/components/UserNewsInfo.vue";
import UserNewsPosts from "@/components/UserNewsPosts.vue";

export default {
name: "UserNewsView",
components: {
Card: Card,
UserNewsInfo: UserNewsInfo,
UserNewsPosts: UserNewsPosts,
},
};
</script>

现在我们的网页是静态的,用户头像、名字这些内容应该都是参数,每个用户是不一样的,且用户发动态后需要在历史动态组件中展示,因此这几个组件都是需要数据交互的。

一般情况下我们会将数据存到需要交互的这几个组件的最顶层组件中,即 UserNewsView。我们可以使用 setup() 函数初始化变量,当前页面的用户一般是不会变的,因此可以定义一个 reactive,注意 reactive 只能接收对象。未来我们需要在模板中用到的属性都需要从 setup() 函数中 return 出去。在 UserNewsView 中传递数据给 UserNewsInfo

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
<template>
<Card title="用户动态">
<div class="row">
<div class="col-md-3">
<UserNewsInfo :user="user" />
</div>
<div class="col-md-9">
<UserNewsPosts />
</div>
</div>
</Card>
</template>

<script>
// @ is an alias to /src
import Card from "@/components/Card.vue";
import UserNewsInfo from "@/components/UserNewsInfo.vue";
import UserNewsPosts from "@/components/UserNewsPosts.vue";
import { reactive } from "vue";

export default {
name: "UserNewsView",
components: {
Card: Card,
UserNewsInfo: UserNewsInfo,
UserNewsPosts: UserNewsPosts,
},
setup() {
const user = reactive({
username: "AsanoSaki",
fansCount: 1,
is_followed: false, // 是否关注这个用户
});

return {
user: user,
}
},
};
</script>

Vue 中传递数据的方式类似于 React,父组件通过 props 传递属性给子组件,子组件通过调用函数(事件)给父组件传递信息。可以使用 :v-bind: 给子组件绑定属性,其后是一个表达式而非普通字符串。子组件需要将接受的数据放在 props 中,接受的 user 数据类型为 Object,且是必填的 required

如果某个数据是动态被计算出来的可以使用 computed。在 UserNewsInfo 中接收并使用数据:

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
<template>
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-4">
<img
class="img-fluid"
src="https://cdn.acwing.com/media/user/profile/photo/82581_lg_e9bdbcb8aa.jpg"
/>
</div>
<div class="col-md-8">
<div class="username">{{ user.username }}</div>
<div class="fans">粉丝:{{ doubleFansCount }}</div>
<button type="button" class="btn btn-secondary btn-sm">+关注</button>
</div>
</div>
</div>
</div>
</template>

<script>
import { computed } from 'vue';

export default {
name: "UserNewsInfo",
props: {
user: {
type: Object,
required: true,
},
},
setup(props) {
let doubleFansCount = computed(() => props.user.fansCount * 2);

return {
doubleFansCount: doubleFansCount,
}
},
};
</script>

<style scoped>
...
</style>

2. 实现关注用户功能

当没有关注当前用户时才会显示加关注的按钮,否则应该显示取消关注按钮。我们可以使用 v-if 属性判断某个表达式是否成立,即可以判断 user.is_followed 是否为 true 来动态显示关注和取关按钮。

还有就是当我们点击按钮时需要更改 user.is_followed 的状态,需要定义一个事件处理函数并绑定(使用 v-on:click@click)到按钮的 click 属性上。

由于状态是从父组件 UserNewsView 中传过来的,因此不能直接在子组件中修改状态,而是需要在父组件中定义好函数并绑定(使用 @)到子组件上:

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
<template>
<Card title="用户动态">
<div class="row">
<div class="col-md-3">
<UserNewsInfo @follow="follow" @unfollow="unfollow" :user="user" />
</div>
<div class="col-md-9">
<UserNewsPosts />
</div>
</div>
</Card>
</template>

<script>
// @ is an alias to /src
import Card from "@/components/Card.vue";
import UserNewsInfo from "@/components/UserNewsInfo.vue";
import UserNewsPosts from "@/components/UserNewsPosts.vue";
import { reactive } from "vue";

export default {
name: "UserNewsView",
components: {
Card: Card,
UserNewsInfo: UserNewsInfo,
UserNewsPosts: UserNewsPosts,
},
setup() {
const user = reactive({
username: "AsanoSaki",
fansCount: 1,
is_followed: false, // 是否关注这个用户
});

const follow = () => {
if (user.is_followed) return;
user.is_followed = true;
user.fansCount++;
};

const unfollow = () => {
if (!user.is_followed) return;
user.is_followed = false;
user.fansCount--;
};

return {
user: user,
follow: follow,
unfollow: unfollow,
}
},
};
</script>

然后由子组件触发绑定的函数来修改父组件中的状态,其 API 为 context.emit()setup() 函数中可以接收第二个参数 context

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
<template>
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-4">
<img
class="img-fluid"
src="https://cdn.acwing.com/media/user/profile/photo/82581_lg_e9bdbcb8aa.jpg"
/>
</div>
<div class="col-md-8">
<div class="username">{{ user.username }}</div>
<div class="fans">粉丝:{{ user.fansCount }}</div>
<button @click="handleFollow" v-if="!user.is_followed" type="button" class="btn btn-secondary btn-sm">+关注</button>
<button @click="handleUnfollow" v-if="user.is_followed" type="button" class="btn btn-secondary btn-sm">取消关注</button>
</div>
</div>
</div>
</div>
</template>

<script>
import { computed } from 'vue';

export default {
name: "UserNewsInfo",
props: {
user: {
type: Object,
required: true,
},
},
setup(props, context) {
let doubleFansCount = computed(() => props.user.fansCount * 2);

const handleFollow = () => {
context.emit("follow");
};

const handleUnfollow = () => {
context.emit("unfollow");
};

return {
doubleFansCount: doubleFansCount,
handleFollow: handleFollow,
handleUnfollow: handleUnfollow,
}
},
};
</script>

<style scoped>
...
</style>

3. 实现历史动态模块

我们先在 UserNewsView 中创建几条动态并传给 UserNewsPosts 子组件:

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
<template>
<Card title="用户动态">
<div class="row">
<div class="col-md-3">
<UserNewsInfo @follow="follow" @unfollow="unfollow" :user="user" />
</div>
<div class="col-md-9">
<UserNewsPosts :posts="posts" />
</div>
</div>
</Card>
</template>

<script>
// @ is an alias to /src
import Card from "@/components/Card.vue";
import UserNewsInfo from "@/components/UserNewsInfo.vue";
import UserNewsPosts from "@/components/UserNewsPosts.vue";
import { reactive } from "vue";

export default {
...
setup() {
const user = reactive({
id: 1, // 给每个用户一个唯一编号
username: "AsanoSaki",
fansCount: 1,
is_followed: false, // 是否关注这个用户
});

const posts = reactive({
count: 3,
posts: [
{
id: 1,
userId: 1,
content: "今天天气真好!",
},
{
id: 2,
userId: 1,
content: "随便发个动态",
},
{
id: 3,
userId: 1,
content: "今天吃些什么?",
},
],
});

...

return {
user: user,
follow: follow,
unfollow: unfollow,
posts: posts,
}
},
};
</script>

在子组件 UserNewsPosts 中我们就需要循环渲染出每条动态,可以使用 v-for 实现,注意循环时类似于 React,需要绑定一个不重复的 key 属性:

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
<template>
<div class="card">
<div class="card-header text-center">历史动态</div>
<div class="card-body">
<div v-for="post in posts.posts" :key="post.id">
<div class="card post">
<div class="card-body">{{ post.content }}</div>
</div>
</div>
</div>
</div>
</template>

<script>
export default {
name: "UserNewsPosts",
props: {
posts: {
type: Object,
required: true,
},
},
};
</script>

<style scoped>
.post {
margin: 10px 0;
}
</style>

4. 实现发动态模块

Vue 可以使用 v-model 获取 <textarea> 的内容并绑定到某个变量上,我们先在父组件 UserNewsView 中导入子组件 UserNewsSend,然后创建发动态的函数传递给子组件:

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
72
73
74
75
76
<template>
<Card title="用户动态">
<div class="row">
<div class="col-md-3">
<UserNewsInfo @follow="follow" @unfollow="unfollow" :user="user" />
<hr />
<UserNewsSend @send="send" />
</div>
<div class="col-md-9">
<UserNewsPosts :posts="posts" />
</div>
</div>
</Card>
</template>

<script>
// @ is an alias to /src
import Card from "@/components/Card.vue";
import UserNewsInfo from "@/components/UserNewsInfo.vue";
import UserNewsPosts from "@/components/UserNewsPosts.vue";
import UserNewsSend from "@/components/UserNewsSend.vue";
import { reactive } from "vue";

export default {
name: "UserNewsView",
components: {
Card: Card,
UserNewsInfo: UserNewsInfo,
UserNewsPosts: UserNewsPosts,
UserNewsSend: UserNewsSend,
},
setup() {
...

const posts = reactive({
count: 3,
posts: [
{
id: 1,
userId: 1,
content: "今天天气真好!",
},
{
id: 2,
userId: 1,
content: "随便发个动态",
},
{
id: 3,
userId: 1,
content: "今天吃些什么?",
},
],
});

...

const send = (content) => {
posts.count++;
posts.posts.unshift({ // 在数组末尾加元素是push(),在首部加元素是unshift()
id: posts.count,
userId: 1,
content: content,
});
}

return {
user: user,
follow: follow,
unfollow: unfollow,
posts: posts,
send: send,
}
},
};
</script>

最后是实现 UserNewsSend

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
<template>
<div class="card">
<div class="card-body">
<label for="input" class="form-label">发一条动态吧~</label>
<textarea
v-model="content"
class="form-control"
id="input"
rows="3"
></textarea>
<div class="text-end">
<button
@click="handleSend"
type="button"
class="btn btn-outline-primary"
>
发送
</button>
</div>
</div>
</div>
</template>

<script>
import { ref } from "vue";

export default {
name: "UserNewsSend",
setup(props, context) {
let content = ref("");

const handleSend = () => {
if (content.value) { // 输入框不为空才发送动态
context.emit("send", content.value); // 调用父组件的send函数
content.value = ""; // 发送后输入框的内容应该清空
}
};

return {
content: content,
handleSend: handleSend,
};
},
};
</script>

<style scoped>
button {
margin-top: 15px;
border-radius: 20px;
width: 90px;
}
</style>

我们理一下执行过程,首先点击发送按钮后会触发绑定的 UserNewsSend 组件中的 handleSend 函数,该函数会调用父组件 UserNewsView 传递过来的 send 事件,参数是 content.value,父组件触发 send 事件后会调用其定义的 send 函数,这个函数会在父组件的 post 对象中添加数据,由于 post 对象是 reactive 类型的,因此当这个对象发生变化时,引用了该对象的组件就会重新渲染,即重新渲染 UserNewsPosts