Vue实现个人空间

Vue3——网站整体布局、用户动态页面

1. 前端渲染逻辑

前端渲染:只有第一次打开页面的时候,向服务器发送请求,服务器返回所有js, 之后再打开页面,前端用返回的js文件将页面渲染出来。

2. vue文件

一个vue文件由三部分组成,html,js,css
css部分标签 ,加上scoped,不同组件之间的css选择器就不会相互影响到了。

vue1.png

3.组件化的框架

  • 可以拆分实现

vue2.png

  • 引入组件的方式(根组件App.vue)

项目初始化自带一个HelloWorld组件,引入方式如下

vue3.png

4.项目结构

网页分为导航栏NavBar和内容Content。总共要实现六个页面,每个页面都可以用一个组件来实现

  • NavBar
  • Content
    • 首页
    • 好友列表
    • 好友动态
    • 登录
    • 注册
    • 404

5.准备工作

引入bootstrap,在根组件app.vue中

1
2
3
4
<script>
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap/dist/js/bootstrap';
</script>

引入后提示需要再装一个模块

vue4.png

vue5.png

6.实现导航栏

  1. 实现导航栏组件——直接使用bootstrap选取需要的元素,将组件export出去

NavBar.vue

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
<template>
<!-- 导航栏开始 -->
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<!-- 容器,用于包裹导航栏内容 -->
<div class="container">
<!-- 品牌/logo -->
<a class="navbar-brand" href="#">Myspace</a>

<!-- 响应式导航栏的切换按钮 -->
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarText"
aria-controls="navbarText"
aria-expanded="false"
aria-label="Toggle navigation"
>
<!-- 切换按钮的图标 -->
<span class="navbar-toggler-icon"></span>
</button>

<!-- 导航栏内容,支持折叠 -->
<div class="collapse navbar-collapse" id="navbarText">
<!-- 导航栏左侧的链接 -->
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<!-- 首页链接 -->
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">首页</a>
</li>
<!-- 好友列表链接 -->
<li class="nav-item">
<a class="nav-link" href="#">好友列表</a>
</li>
<!-- 用户动态链接 -->
<li class="nav-item">
<a class="nav-link" href="#">用户动态</a>
</li>
</ul>

<!-- 导航栏右侧的链接 -->
<ul class="navbar-nav">
<!-- 登录链接 -->
<li class="nav-item">
<a class="nav-link" href="#">登录</a>
</li>
<!-- 注册链接 -->
<li class="nav-item">
<a class="nav-link" href="#">注册</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- 导航栏结束 -->
</template>

<script>
// 导出组件
export default {
// 组件名称
name: 'NavBar',
// 注册子组件(当前没有子组件,所以为空)
components: {},
};
</script>

<style scoped>
/* 这里可以定义组件的样式 */
/* scoped 表示样式仅作用于当前组件 */
</style>
  1. 在根组件(app.vue)将实现的NavBar引入——引入路径,在components对象中添加组件名,同时在template中引入组件

vue6.png

7.写内容里的六个小组件

对于一个比较大的组件,可以拆分为多个组件
首页组件(HomeView.vue)
可以在bootstrap中找个card组件,然后用container包起来,container是用来动态调位置的

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
<template>
<div class="home">
<div class="container">
<div class="card">
<div class="card-body">
首页
</div>
</div>
</div>
</div>
</template>

<script>
// @ is an alias to /src

export default {
name: 'HomeView',
components: {
}
}
</script>

<style scoped>
.container {
margin-top: 20px;
}
</style>

发现这一部分的html和css其实每个页面都一样,要将这块公共部分提取出来作为单独的组件,方便后期整体修改。

可以将内容渲染到 slot标签中(slot可以用来获取子元素):

写一个组件ContentBase.vue

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
<template>
<!-- 外层容器,用于包裹整个内容区域 -->
<div class="home">
<!-- Bootstrap 的容器,用于居中内容并添加响应式布局 -->
<div class="container">
<!-- 卡片组件,用于展示内容 -->
<div class="card">
<!-- 卡片的主体部分 -->
<div class="card-body">
<!-- 插槽,用于接收父组件传递的内容 -->
<slot></slot>
</div>
</div>
</div>
</div>
</template>

<script>
// 导出组件
export default {
// 组件名称
name: 'ContentBase',
};
</script>

<style scoped>
/* 自定义样式 */
.container {
/* 设置容器的上边距 */
margin-top: 20px;
}
</style>

然后修改HomeView.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<ContentBase>
首页
</ContentBase>
</template>

<script>
import ContentBase from '@/components/ContentBase.vue';

export default {
name: 'HomeView',
components: {
ContentBase,
}
}
</script>

<style scoped>

</style>

实现剩下的好友列表、好友动态、登录、注册、404,这里以好友列表为例,其他的都一样

UserList.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<ContentBase>
好友列表
</ContentBase>
</template>

<script>
import ContentBase from '@/components/ContentBase.vue';

export default {
name: 'UserList',
components: {
ContentBase,
}
}
</script>

<style scoped>

</style>

8.添加路由

router/index.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
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import UserList from '../views/UserList.vue'
import UserProfile from '../views/UserProfile.vue'
import LoginView from '@/views/LoginView.vue'
import RegisterView from '@/views/RegisterView.vue'
import NotFoundView from '@/views/NotFoundView.vue'

const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/userlist',
name: 'userlist',
component: UserList
},
{
path: '/userprofile',
name: 'userprofile',
component: UserProfile
},
{
path: '/login',
name: 'login',
component: LoginView
},
{
path: '/register',
name: 'register',
component: RegisterView
},
{
path: '/notfound',
name: 'notfound',
component: NotFoundView
},
]

const router = createRouter({
history: createWebHistory(),
routes
})

export default router

9.实现前端渲染的属性

想要实现前端渲染,将原本的a标签换成router-link标签,有特殊的属性 : to ,注意在vue中绑定属性需要用冒号: 传入name。name就是在路由里面定义的name

例如

1
<router-link class="navbar-brand" :to="{name: 'home'}">Myspace</router-link>

点击 <router-link> 时,Vue Router 会动态更新页面内容,而不会重新加载整个页面。此时点击就会跳转到home对应的路径

如果是下面的代码浏览器会重新加载整个页面

1
<a class="navbar-brand" href="/">Myspace</a>

现在将NavBar.vue全部用router-link

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>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container">
<router-link class="navbar-brand" :to="{name: 'home'}">Myspace</router-link>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<router-link class="nav-link active" aria-current="page" :to="{name: 'home'}">首页</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" :to="{name: 'userlist'}">好友列表</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" :to="{name: 'userprofile'}">用户动态</router-link>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<router-link class="nav-link" :to="{name: 'login'}">登录</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" :to="{name: 'register'}">注册</router-link>
</li>
</ul>
</div>
</div>
</nav>
</template>

<script>
export default {
name: 'NavBar',
components: {

}
}
</script>

<style scoped>

</style>

10. 用户动态页面的实现(三个组件)

vue7.png

新建三个组件UserProfileInfo.vue、UserProfilePosts.vue和UserProfileWrite.vue

可以用bootstrap的grid来布局

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<ContentBase>
<div class="row">
<div class="col-3">
用户信息
</div>
<div class="col-9">
帖子列表
</div>
</div>
</ContentBase>
</template>

现在来实现UserProfileInfo

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
<template>
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-3">
<img class="img-fluid" src="https://cdn.acwing.com/media/user/profile/photo/487342_lg_9a864c18d9.jpg" alt="">
</div>
<div class="col-9">
<div class="username">Lzh is coding</div>
<div class="fans">fans: 999</div>
<button type="button" class="btn btn-secondary btn-sm">follow</button>
</div>
</div>
</div>
</div>
</template>

<script>
export default{
name: 'UserProfileInfo',
}
</script>

<style scoped>
img {
border-radius: 50%;
}
.username {
font-weight: bold;
}
.fans {
font-size: 12px;
color: #666;
}
button{
padding: 2px,4px;

}
</style>

对于展示的个人信息、帖子列表都需要参数,这三个模块是相互交互的,对于这样的情况,需要将数据存到上层组件中。

上层组件UserProfileView.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
setup: function(){
const user = reactive({
id: 1,
username: 'LiuZihao',
lastName: 'Liu',
firstName: 'Zihao',
followers: 0,
is_followed: false,
});
return{
user,
}
}

使用方式

vue8.png

父组件传递

vue9.png

子组件接受

vue10.png

使用:

1
2
3
4
5
<div class="col-9">
<div class="username">{{fullName}}</div>
<div class="fans">{{user.followers}}</div>
<button type="button" class="btn btn-secondary btn-sm">follow</button>
</div>

实现关注按钮

逻辑: 如果没有关注,则显示 “关注”;如果已经关注,则显示 “取消关注”字样
实现: 使用template的v-if

1
2
<button v-if="!user.is_followed" type="button" class="btn btn-secondary btn-sm">follow me</button>
<button v-if="user.is_followed" type="button" class="btn btn-secondary btn-sm">unfollow me</button>

关注完以后,还需要更新user状态,此时需要定义事件处理函数。

vue11.png

将这两个函数绑定起来:

1
2
<button v-on:click="follow" v-if="!user.is_followed" type="button" class="btn btn-secondary btn-sm">follow me</button>
<button v-on:click="unfollow" v-if="user.is_followed" type="button" class="btn btn-secondary btn-sm">unfollow me</button>

子组件要向父组件传递消息:
父组件:

vue12.png

vue13.png

子组件:用context.emit可以出发父组件的事件

vue14.png

UserProfileInfo

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
<template>
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-3">
<img class="img-fluid" src="https://cdn.acwing.com/media/user/profile/photo/487342_lg_9a864c18d9.jpg" alt="">
</div>
<div class="col-9">
<div class="username">{{fullName}}</div>
<div class="fans">{{user.followers}}</div>
<button v-on:click="follow" v-if="!user.is_followed" type="button" class="btn btn-secondary btn-sm">follow me</button>
<button v-on:click="unfollow" v-if="user.is_followed" type="button" class="btn btn-secondary btn-sm">unfollow me</button>
</div>
</div>
</div>
</div>
</template>

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

export default{
name: 'UserProfileInfo',
props: {
user: {
type: Object,
required: true,
}
},
setup: function(props,context){
let fullName = computed(() => {
return props.user.lastName + ' ' + props.user.firstName;
});

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

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


return {
fullName,
follow,
unfollow,
}
}
}
</script>

<style scoped>
img {
border-radius: 50%;
}
.username {
font-weight: bold;
}
.fans {
font-size: 12px;
color: #666;
}
button{
padding: 2px,4px;

}
</style>

UserProfile

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
<template>
<ContentBase>
<div class="row">
<div class="col-3">
<UserProfileInfo @follow="follow" @unfollow="unfollow" v-bind:user="user"></UserProfileInfo>
</div>
<div class="col-9">
<UserProfilePosts></UserProfilePosts>
</div>
</div>
</ContentBase>
</template>

<script>
import ContentBase from '@/components/ContentBase.vue';
import UserProfileInfo from '@/components/UserProfileInfo.vue';
import UserProfilePosts from '@/components/UserProfilePosts.vue';
import { reactive } from 'vue';

export default {
name: 'UserProfile',
components: {
ContentBase,
UserProfileInfo,
UserProfilePosts,
},
setup: function(){
const user = reactive({
id: 1,
username: 'LiuZihao',
lastName: 'Liu',
firstName: 'Zihao',
followers: 0,
is_followed: false,
});

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

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

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

<style scoped>

</style>

实现帖子列表

UserProfilePosts

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
<template>
<div class="card">
<div class="card-body">
<div v-for="post in posts.posts" :key="post.id">
<div class="card single-post">
<div class="card-body">
<div>{{post.content}}</div>
</div>
</div>
</div>
</div>
</div>
</template>

<script>
export default {
name: 'UserProfilePosts',
props:{
posts:{
type:Object,
required:true,
}
}
}
</script>

<style scoped>
.single-post{
margin-top: 10px;
}
</style>

实现发帖功能

UserProfileWrite

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
<template>
<div class="card edit-field">
<div class="card-body">
<div class="mb-3">
<label for="edit-post" class="form-label">编辑</label>
<textarea v-model="content" class="form-control" id="edit-post" rows="3"></textarea>
<button v-on:click="submit_post" type="button" class="btn btn-primary btn-sm">发布</button>
</div>
</div>
</div>
</template>

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

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

const submit_post=()=>{
context.emit('submit_post',content.value);
content.value='';
}

return {
content,
submit_post,
}
}
}
</script>

<style scoped>
.edit-field{
margin-top: 20px;
}
button{
margin-top: 10px;
}
</style>

首先要获取textarea里的信息,利用v-model,v-model的标签与内容绑定起来

vue15.png

还需要一个触发函数。当click的时候,将textarea里的内容发成帖子。
父组件需要一个函数, 并把这个函数暴露给按钮页即可

1
2
3
4
5
6
7
8
const submit_post=(content)=>{
posts.posts.unshift({
id:posts.count,
userId:1,
content:content,
});
posts.count++;
}

vue16.png