用vue3实现一个低配版的QQ空间~
vue3的一些基本概念
vue3框架每个vue文件由三个部分构成:template
,script
,style
,template主要是实现HTML,script实现js,style定义格式,下面是几个部分用到的知识的一些详细介绍
script部分
export default对象的属性:
name
:组件的名称
components
:存储中用到的所有组件
props
:存储父组件传递给子组件的数据
watch()
:当某个数据发生变化时触发
computed
:动态计算某个数据
setup(props, context)
:初始化变量、函数
ref
定义变量,可以用.value属性重新赋值reactive
定义对象,不可重新赋值props
存储父组件传递过来的数据context.emit()
:触发父组件绑定的函数
template部分
<slot></slot>
:存放父组件传过来的children。
v-on:click
或@click
属性:绑定事件
v-if
、v-else
、v-else-if
属性:判断
v-for
属性:循环,:key循环的每个元素需要有唯一的key
v-bind:
或:
:绑定属性
style部分
<style>
标签添加scope属性后,不同组件间的css不会相互影响。
第三方组件
view-router
包:实现路由功能。
vuex
:存储全局状态,全局唯一。
state
: 存储所有数据,可以用modules属性划分成若干模块getters
:根据state中的值计算新的值mutations
:所有对state的修改操作都需要定义在这里,不支持异步,可以通过$store.commit()触发actions
:定义对state的复杂修改操作,支持异步,可以通过$store.dispatch()触发。注意不能直接修改state,只能通过mutations修改state。modules
:定义state的子模块
分析vue框架的主要模块构成
components
是用来实现页面用到各种组件的,比如导航栏等。
views
是实现各个页面的,一般来说,每一个页面一个.vue文件,比如个人空间,首页等页面。
router
是实现路由,下面有一个Index.js文件,用来import views里面实现的各个页面,添加到routes里面实现页面跳转。
APP.vue
就是主页面了
分析QQ空间的页面组成
首先第一步,实现整体效果
下面是一个简易的实现,也就是有一个导航栏,里面有首页、用户列表、好友动态、登录、主页五个单元,这每一个单元都应该是一个单独的页面,所以要在view里面实现五个.vue文件,同时,每个页面都会有自己的body部分,但这个body应该大小间距什么的差不多,所以可以用一个共同组件ContentBase来实现,但是里面显示不同的内容,也就是在components下面实现ContentBase.vue文件,最后在routes里面添加路由实现跳转。
完成上述工作后,文件列表如下:
其中,为了在ContentBase里面显示每个页面自己的body,我们用到了<slot></slot>
组件(存放父组件传过来的children),那么在template里面用到ContentBase组件时,我们把想要显示的内容放到ContentBase组件里面就行了。
实现跳转
刚刚添加的路由只能通过修改url来手动跳转,如果想点击导航栏条目跳转,需要在导航栏里面添加地址,但是与之前不同的是,vue里面不能用a标签的href实现跳转,要用router-link标签的:to
={name: ‘xx’}来实现(xx是对应的要跳转的name,也就是routes里面每个path的name)
比如:
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
]
<router-link class="navbar-brand" :to="{name: 'home'}">MySpace</router-link>
class="navbar-brand" :to="{name: 'home'}">MySpace</a>
给出整体实现的部分代码
NavBar.vue
组件的代码
<template>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<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" :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",
}
</script>
<style scoped>
</style>
ContentBase.vue
组件的代码
<template>
<div class="home">
<div class="container">
<div class="card">
<div class="card-body">
<slot></slot>
</div>
</div>
</div>
</div>
</template>
<script>
export default{
name: "ContentBase",
}
</script>
<style>
.container{
margin-top: 20px;
}
</style>
登录页面LoginView.vue
代码,其他类似,只有ContentBase组件内部不同
<template>
<ContentBase>
登录
</ContentBase>
</template>
<script>
import ContentBase from '../components/ContentBase';
export default {
name: 'LoginView',
components: {
ContentBase,
}
}
</script>
<style>
</style>
index.js
文件
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView';
import NotFoundView from '../views/NotFoundView';
import RegisterView from '../views/RegisterView';
import UserListView from '../views/UserListView';
import UserProfileView from '../views/UserProfileView';
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/userlist',
name: 'userlist',
component: UserListView
},
{
path: '/login',
name: 'login',
component: LoginView
},
{
path: '/404',
name: '404',
component: NotFoundView
},
{
path: '/register',
name: 'register',
component: RegisterView
},
{
path: '/userProfile',
name: 'userprofile',
component: UserProfileView
},
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
然后实现用户动态页面
用户动态页面的构成:
主要分为三个模块,每个模块完成各自的功
能,所以可以用三个组件来实现。
在components下面定义三个vue文件来实现三个部分
实现三个组件之前,可以先将用户动态页面布局,这里用bootstrap的grid system,可以定义每一列占多少格,一行分了十二格,这里用户信息3格,帖子列表9格。
用户基本信息部分的实现
用户基本信息这一栏,也就是UserProfileInfo.vue文件,细分也可以是左右两个部分,如下图:
所以还用grid system实现,依旧分为3:9,头像就是一张图片。
布局实现如下:
<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/108721_lg_b7a2a572c6.jpg">
</div>
<div class="col-9">
<div class="username">Ypp</div>
<div class="fnas">粉丝:111</div>
<button type="button" class="btn btn-secondary btn-sm">+关注</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;
}
button{
padding: 2px 4px;
font-size: 12px;
}
</style>
获取基本信息
每一个用户的url都应该是不一样的,而且我们希望点开某个人的动态页面能够获取一些基本信息显示,而且关注按钮和粉丝数应该是动态更新的。那么无论是用户信息还是动态发帖都应该能够获取这个页面用户的基本信息,所以将这些信息定义在父组件即UserProInfo中。
先显示用户基本信息
这里涉及到定义参数和传递参数的知识
- 在vue中,用setup()初始化变量、函数,因为用户信息是不变的,所以用reactive定义变量,最后将用到的属性return出去
- 父组件给子组件传递事件的时候,在子组件用v-bind:或:绑定事件,在子组件想要接收到参数就要将参数放到props里面
- 如果想要动态计算某些属性得到新的属性,要用compute计算,这个计算在setup()里面
以上实现主要修改UserProfileView.vue文件和UserProfileInfo.vue文件:
UserProfileView
:
<template>
<ContentBase>
<div class="row">
<div class="col-3">
<UserProfileInfo v-bind:user="user"/>
</div>
<div class="col-9">
<UserProfilePosts />
</div>
</div>
</ContentBase>
</template>
<script>
import ContentBase from '../components/ContentBase';
import UserProfileInfo from '../components/UserProfileInfo';
import UserProfilePosts from '../components/UserProfilePosts'
import { reactive } from 'vue';
export default {
name: 'UserProfile',
components: {
ContentBase,
UserProfileInfo,
UserProfilePosts,
},
setup(){
const user = reactive({
id: 1,
username: "头发没了还会再长",
lastName: "Y",
firstName: "pp",
followerCount: 0,
is_followed: false,
});
return{
user,
}
}
}
</script>
<style scoped>
</style>
UserProfileInfo
:
<template>
<ContentBase>
<div class="row">
<div class="col-3">
<UserProfileInfo v-bind:user="user"/>
</div>
<div class="col-9">
<UserProfilePosts />
</div>
</div>
</ContentBase>
</template>
<script>
import ContentBase from '../components/ContentBase';
import UserProfileInfo from '../components/UserProfileInfo';
import UserProfilePosts from '../components/UserProfilePosts'
import { reactive } from 'vue';
export default {
name: 'UserProfile',
components: {
ContentBase,
UserProfileInfo,
UserProfilePosts,
},
setup(){
const user = reactive({
id: 1,
username: "头发没了还会再长",
lastName: "Y",
firstName: "pp",
followerCount: 0,
is_followed: false,
});
return{
user,
}
}
}
</script>
<style scoped>
</style>
定义按钮
按钮是用来关注的,如果没有关注,按钮应该显示+关注,如果已经关注,应该显示取消关注,这个信息通过props
的is_followed
属性获取。所以设置两个按钮,在is_followed=true显示取消关注,反之,显示+关注,在vue里面有一个v-if
属性可以用来判断,直接写到button标签即可。
<button v-if="!user.is_followed" type="button" class="btn btn-secondary btn-sm">+关注</button>
<button v-if="user.is_followed" type="button" class="btn btn-secondary btn-sm">取消关注</button>
而且我们应该实时更新关注按钮的状态,当点完关注显示取消关注,也就是给按钮绑定一个事件,事件函数在setup()
里面定义实现,在button标签内用v-on:click
或@click
属性绑定。
事件函数需要做的事情,是修改is_followed
属性的值,但是button在UserProfileInfo
这个子组件内,而is_followed属性在UserProfileView
这个父组件内,所以我们想要修改is_followed属性就要在子组件给父组件传递信息——通过event传递。
vue里面通过context.emit()
:触发父组件绑定的函数,来修改属性,在子组件内使用该属性触发父组件内的事件,这个修改属性值的事件是在父组件内实现,但是在子组件内触发,子组件通过context.emit()
触发,触发的一个前提是子组件绑定了该事件,绑定用@
属性,这个绑定是在父组件的子组件标签内绑定。
实现效果如下:
代码修改的部分如下:
UserProfileInfo
:
setup(props, context){
let fullname = computed(() => props.user.lastName + ' ' +props.user.firstName)
let follow = () => {
context.emit('follow');
}
let unfollow = () => {
context.emit('unfollow');
}
return{
fullname,
follow,
unfollow,
}
},
UserProfileView
:
<UserProfileInfo v-bind:user="user" @follow="follow" @unfollow="unfollow"/>
setup(){
const user = reactive({
id: 1,
username: "头发没了还会再长",
lastName: "Y",
firstName: "pp",
followerCount: 1,
is_followed: true,
});
const follow = () => {
if(user.is_followed) return;
user.is_followed=true
user.followerCount++
}
const unfollow = () => {
if(!user.is_followed) return;
user.is_followed=false
user.followerCount--
}
return{
user,
follow,
unfollow,
}
}
用户动态的实现
这一部分和用户基本信息实现基本类似,用户基本信息这一栏是先在父组件内定义用户信息这一属性,然后通过props
将属性传递到用户基本信息组件内显示。
同样,用户动态这一组件的信息也定义在父组件内,用posts来定义。
但是因为posts是一个列表,不能直接显示,要用循环来实现,在vue里面用v-for
属性实现循环遍历列表的每一个item,然后再对每一个item单独操作。而且要为每一个item绑定一个key
,这个key是唯一的,可以取每个item的id。
然后将每个post放在一个卡片里,在设置一下style。
最后效果如下:
代码修改部分如下:
UserProfilePosts.vue
:
<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">
{{post.content}}
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default{
name: "UserProfilePosts",
props: {
posts: {
type: Object,
required: true,
}
}
}
</script>
<style>
.single-post{
margin-bottom: 10px;
}
</style>
UserProfileView.vue
:
<div class="col-9">
<UserProfilePosts v-bind:posts="posts"/>
</div>
setup(){
const user = reactive({
id: 1,
username: "头发没了还会再长",
lastName: "Y",
firstName: "pp",
followerCount: 1,
is_followed: true,
});
const posts = reactive({
count: 3,
posts:[
{
id: 1,
userId: 1,
content: "尽人事,听天命",
},
{
id: 2,
userId: 2,
content: "不矜不伐缓缓书",
},
{
id: 3,
userId: 3,
content: "道阻且长,行则将至",
},
]
})
const follow = () => {
if(user.is_followed) return;
user.is_followed=true
user.followerCount++
}
const unfollow = () => {
if(!user.is_followed) return;
user.is_followed=false
user.followerCount--
}
return{
user,
follow,
unfollow,
posts,
}
}
发帖模块的实现
发帖模块实现的功能是当编辑区的帖子编辑完点击发布按钮时,帖子能够显示在用户动态区域,要实现该功能,跟之前还是类似的,但是这次是将子组件的内容传递到父组件。由父组件将内容添加到另外一个组件的属性内。
要实现传递,首先应该获取到编辑区的内容才行,也就是动态获取,在vue里标签内使用v-model="xx"
属性可以将该标签的内容和xx绑定起来。xx可以是定义在setup()里的属性。
获取到内容之后绑定事件函数,当点击发布按钮时应该将content触发父组件绑定的事件,将content添加到帖子列表里。所以给按钮绑定@click函数,该函数使用context.emit()
触发父组件事件。
父组件事件被触发应该做的事情是增加帖子,所以被触发的事件要实现的功能是增加content到帖子列表。然后用@
将该事件在父组件(UserProfileView)内绑定到要触发的子组件(UserProfileWrite)。
代码修改如下:
UserProfileView
:
const post_a_post = (content) => {
posts.count++;
posts.posts.unshift({
id: posts.count,
userId: 1,
content: content,
})
}
return{
user,
follow,
unfollow,
posts,
post_a_post
}
UserProfileWrite
:
<template>
<div class="card edit-field">
<div class="card-body">
<label for="edit-post" class="form-label">编辑区</label>
<textarea v-model="content" class="form-control" id="edit-post" rows="3"></textarea>
<button @click="post_a_post" type="button" class="btn btn-success btn-sm">发布</button>
</div>
</div>
</template>
<script>
import { ref } from 'vue';
export default{
name: "UserProinfoWrite",
setup(props, context){
let content = ref('');
const post_a_post = () => {
context.emit('post_a_post', content.value);
content.value = ""
}
return{
content,
post_a_post
}
}
}
</script>
<style>
.edit-field{
margin-top: 20px;
}
button{
margin-top: 10px;
}
</style>