Vue实现个人空间
Vue3——网站整体布局、用户动态页面
1. 前端渲染逻辑
前端渲染:只有第一次打开页面的时候,向服务器发送请求,服务器返回所有js, 之后再打开页面,前端用返回的js文件将页面渲染出来。
2. vue文件
一个vue文件由三部分组成,html,js,css
css部分标签 ,加上scoped,不同组件之间的css选择器就不会相互影响到了。

3.组件化的框架

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

4.项目结构
网页分为导航栏NavBar和内容Content。总共要实现六个页面,每个页面都可以用一个组件来实现
5.准备工作
引入bootstrap,在根组件app.vue中
1 2 3 4
| <script> import 'bootstrap/dist/css/bootstrap.css'; import 'bootstrap/dist/js/bootstrap'; </script>
|
引入后提示需要再装一个模块


6.实现导航栏
- 实现导航栏组件——直接使用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>
|
- 在根组件(app.vue)将实现的NavBar引入——引入路径,在components对象中添加组件名,同时在template中引入组件

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. 用户动态页面的实现(三个组件)

新建三个组件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, } }
|
使用方式

父组件传递

子组件接受

使用:
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状态,此时需要定义事件处理函数。

将这两个函数绑定起来:
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>
|
子组件要向父组件传递消息:
父组件:


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

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的标签与内容绑定起来

还需要一个触发函数。当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++; }
|
