Vue框架技術詳解——項目驅動概念理解【前端】【Vue】

Vue3框架

  • 是前端渲染框架
  • 瀏覽器向服務器第一次發送請求,就會將所有頁面的樣式全部返回到瀏覽器
  • vue中會將所有js文件最后打包成一個js文件,當前訪問其中一個頁面時,其他頁面的樣式也已經返回到瀏覽器中了,下次切換頁面時,就不會再向服務器發送請求
    在這里插入圖片描述

1.配置環境

1)vue3的安裝

  • Windows上推薦使用powershell或者cmd

  • 1.Node.js安裝:官網地址, 具體的安裝細節可以參考下面的安裝教程
    參考Nodejs安裝教程

  • 2.vue3安裝:Vue官網, 具體的安裝細節可以參考下面的安裝教程
    Vue3 v5.0.8的安裝教程

  • 3.安裝@vue/cli:vue的腳手架

打開終端執行:

npm i -g @vue/cli

安裝較慢可以配置鏡像:

npm config set registry https://registry.npmmirror.com/

如果執行后面的操作有bug,可能是最新版有問題,可以嘗試安裝早期版本,比如:npm i -g @vue/cli@4

  • 4.啟動vue自帶的圖形化項目管理界面:vue ui
    • 常見問題1:Windows上運行vue,提示無法加載文件,表示用戶權限不足。
    • 解決方案:用管理員身份打開終端,輸入set-ExecutionPolicy RemoteSigned,然后輸入y

2.創建項目

  • 1.選擇文件夾,創建項目名稱:
    在這里插入圖片描述
  • 2.選擇vue3框架:
    在這里插入圖片描述
  • 3.添加必要插件:
    在這里插入圖片描述
  • 4.安裝依賴:bootstrap
    在這里插入圖片描述
  • 5.運行項目:
    在這里插入圖片描述
  • 6.使用vscode打開項目文件夾
  • 推薦的一個vscode插件,用于vue文件的代碼高亮等顯示:Vetur

3.Vue的優勢

  • 1.寫不同組件的css樣式時不需要考慮不同組件之間的樣式互相影響:在渲染時,vue會自動給每個css類自動生成一個隨機的值,從而確保不同組件的樣式不會互相影響
    在這里插入圖片描述

4.系統設計

在這里插入圖片描述

1)NavBar——導航欄

  • 負責所有頁面的路由跳轉
  • 通過在BootStrap官網找到自己需要的樣式
    比如:

在這里插入圖片描述

  • 1.將樣式代碼直接復制,然后根據自己的頁面設計稍加修改即可:

NavBar.vue:

<template><nav class="navbar navbar-expand-lg bg-body-tertiary"><div class="container"><a class="navbar-brand" href="#">SharingSpace</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="/">Home</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",
}</script><style scoped></style>
  • 2.將NavBar添加到展示組件上:

App.vue:

<template><NavBar/><router-view/>
</template>
<script>
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap/dist/js/bootstrap.js';
import NavBar from './components/NavBar.vue';
export default {name: "App",components: {NavBar: NavBar,}
}
</script><style>
</style>
  • 3.頁面效果:
    在這里插入圖片描述

2)ContentBase——頁面統一容器組件

  • 作用是將所有頁面展示在同一個規范下,比如一些共通性的樣式,每個頁面距離NavBar組件的margin-top等
  • 使用<slot></slot>存放父組件傳過來的children
  • 當需要修改整個頁面布局的展示形式時,只需要統一修改ContentBase就能完成全部頁面的修改

ContenBase.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 scoped>
.container {margin-top: 20px;
}
</style>

3)Views——頁面組件

  • 由于初期的頁面布局只是名字不同,這里只展示HomeView的構造,其他頁面結構完全相同
    HomeView.vue:
<template><ContentBase>首頁</ContentBase>
</template><script>
import ContentBase from '@/components/ContentBase.vue'export default {name: 'HomeView',components: {ContentBase}
}
</script><style scoped>
</style>

4)router——頁面路由

  • 添加不同頁面的路由路徑
  • 將所有頁面組件導入,并通過定義router: path: '/xxx', name: 'xxxx', component: 'xxxxx',來指定不同路徑對應的頁面組件
  • 重定向:通過catchAll(.*)將除上述連接外的所有連接重定向到404

index.js:

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue';
import LoginView from '../views/LoginView.vue';
import RegisterView from '../views/RegisterView.vue';
import NotFoundView from '../views/NotFoundView.vue';
import UserListView from '../views/UserListView.vue';
import UserProfileView from '../views/UserProfileView.vue';const routes = [{path: '/',name: 'home',component: HomeView},{path: '/login/',name: 'name-login',component: LoginView},{path: '/register/',name: 'name-register',component: RegisterView},{path: '/userlist/',name: 'name-userlist',component: UserListView},{path: '/userprofile/',name: 'name-userprofile',component: UserProfileView},{path: '/404/',name: 'name-notfound',component: NotFoundView},{path: '/:catchAll(.*)',redirect: '/404/'}
]const router = createRouter({history: createWebHistory(),routes
})
export default router

5)<router-link>——頁面跳轉

  • 當添加router后,就能根據對應的地址加載對應的頁面組件進行展示
  • 在NavBar中綁定對應的路徑實現頁面跳轉:

NavBar.vue:

<template><nav class="navbar navbar-expand-lg bg-body-tertiary"><div class="container"><a class="navbar-brand" href="#">SharingSpace</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="/">Home</a></li><li class="nav-item"><a class="nav-link" href="/userlist">好友列表</a></li><li class="nav-item"><a class="nav-link" href="/userprofile">好友動態</a></li></ul><ul class="navbar-nav"><li class="nav-item"><a class="nav-link" href="/login">登錄</a></li><li class="nav-item"><a class="nav-link" href="register">注冊</a></li></ul></div></div></nav>
</template><script>
export default {name: "NavBar",
}</script>
<style scoped>
</style>
  • 但是這種形式每次會向服務器發送請求,刷新對應的頁面,屬于后端渲染
  • vue中提供了前端渲染的標簽:<router-link>:每次切換頁面時就不會在刷新了,因為第一次請求時已經將所有頁面樣式發送過來了。
    NavBar.vue:
<template><nav class="navbar navbar-expand-lg bg-body-tertiary"><div class="container"><router-link class="navbar-brand" :to="{name: 'home'}">SharingSpace</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'}">Home</router-link></li><li class="nav-item"><router-link class="nav-link" :to="{name: 'name-userlist'}">好友列表</router-link></li><li class="nav-item"><router-link class="nav-link" :to="{name: 'name-userprofile'}">好友動態</router-link></li></ul><ul class="navbar-nav"><li class="nav-item"><router-link class="nav-link" :to="{name: 'name-login'}">登錄</router-link></li><li class="nav-item"><router-link class="nav-link" :to="{name: 'name-register'}">注冊</router-link></li></ul></div></div></nav>
</template><script>
export default {name: "NavBar",
}</script>
<style scoped>
</style>

5.頁面功能實現

1)UserProfileView——用戶個人空間

  • 用戶個人空間總共包含三個模塊: 用戶信息,用戶歷史的動態,編輯動態模塊
  • 使用bootstarp的grid布局來規劃頁面:用戶信息占3份,歷史動態占9份,編輯動態模塊也是占3份,緊跟在用戶信息下面
  • 首先是實現<UserInfo> 和<UserProfileContent>組件,將其添加到當前個人空間組件中:
1.UserProfileView: 用戶個人空間總組件
<template><ContentBase><div class="row"><div class="col-3"><UserInfo /></div><div class="col-9"><UserProfileContent /></div></div></ContentBase>
</template><script>
import ContentBase from '@/components/ContentBase.vue';
import UserInfo from '@/components/UserInfo.vue';
import UserProfileContent from '@/components/UserProfileContent.vue';export default {name: 'UserProfileView',components: {ContentBase,UserInfo,UserProfileContent},
}
</script><style scoped></style>
2.UserInfo:用戶個人信息子組件
  • 包含用戶名稱、頭像、粉絲數、點贊數、收藏數
<template><div class="row"><div class="col-3"><div class="user-card"><div class="user-card-head"></div><div class="user-card-body"><div class="user-card-body-left"><img src="https://cdn.acwing.com/media/user/profile/photo/450127_md_da0516e621.jpg" alt=""></div><div class="user-card-body-right"><div class="user-card-body-right-text"><div class="user-card-body-right-text-username">Liubin Zhong<span>LV5</span></div><div class="user-card-body-right-text-reputation"><span class="user-card-body-right-text-reputation-item"><span>58</span><span>收藏</span></span><span class="user-card-body-right-text-reputation-item"><span>32</span><span>粉絲</span></span><span class="user-card-body-right-text-reputation-item"><span>15</span><span></span></span></div><div class="user-card-body-right-text-discreption">這個人很懶~什么也沒留下</div></div><div class="user-card-body-right-button"><button>+關注</button><button>發消息</button></div></div></div></div></div><div class="col-9"></div></div>
</template><script>export default {name: "UserInfo",
}
</script><style scoped>img {border-radius: 50%;
}.user-card {width: 300px;height: 200px;box-shadow: 2px 2px 5px lightgrey;border-radius: 5px;
}.user-card-head {background-image: url('/src/assets/mountain.jpg');background-size: cover;width: 100%;height: 80px;
}.user-card-body {width: 300px;height: calc(100% - 85px);box-sizing: border-box;padding-top: 10px;
}.user-card-body-left {width: 65px;height: 100%;float: left;text-align: center;
}.user-card-body-left>img {border-radius: 50%;width: 48px;height: 48px;
}.user-card-body-right {width: calc(100% - 70px);height: 100%;float: left;
}.user-card-body-right-text {width: 100%;height: 70%;
}.user-card-body-right-text-username {font-size: 16px;color: #222222;font-weight: bold;padding: 0px 0px 10px 0px;
}.user-card-body-right-text-username>span {font-size: 12px;color: #8BD29B;font-style: italic;
}.user-card-body-right-text-reputation {width: 276px;height: 22px;padding: 0px 0px 10px 0px;font-size: 12px;color: #9499A0;box-sizing: border-box;line-height: 12px;
}.user-card-body-right-text-reputation-item {margin-right: 18px;
}.user-card-body-right-text-reputation-item>span:nth-child(1) {font-size: 12px;color: #18191c;padding: 0px 3px 0px 0px;
}.user-card-body-right-text-reputation-item>span:nth-child(2) {font-size: 12px;color: #9299A0;
}.user-card-body-right-text-discreption {margin-top: 1px;width: 200px;height: 10px;font-size: 12px;line-height: 10px;
}.user-card-body-right-button>button {width: 100px;height: 30px;font-size: 14px;border-radius: 5px;border: none;cursor: pointer;
}.user-card-body-right-button>button:nth-child(1) {color: white;background-color: #00A1D6;padding: 1px 6px;float: left;margin-right: 5px;
}.user-card-body-right-button>button:nth-child(1):hover {background-color: #00B5E5;transition: 700ms;
}.user-card-body-right-button>button:nth-child(2) {padding: 1px 6px;color: #6D757A;float: left;border: #6D757A solid 1px;
}.user-card-body-right-button>button:nth-child(2):hover {border-color: #00B5E5;color: #00B5E5;transition: 700ms;
}
</style>
3.UserProfileContent:用戶歷史動態
  • 展示用戶的歷史動態信息內容
<template><div class="card"><div></div></div>
</template><script>export default {name: "UserProfileContent",
}
</script><style scoped></style>

2)組件之間的數據交互

  • setup(props, context):初始化變量、函數

    • ref:定義變量,可以用.value屬性重新賦值
    • reactive:定義對象,不可重新賦值
    • props:存儲父組件傳遞過來的數據
    • context.emit():觸發父組件綁定的函數
  • 在父組件中通過setup(),定義一個user,封裝用戶的個人信息,方便傳遞給UserInfo子組件獲取數據

  • 父組件給子組件傳遞數據是通過props傳遞,而子組件給父組件傳遞數據是通過觸發父組件的函數event,來修改父組件中的數據,因為數據全部是定義在了父組件中。

1.父組件給子組件傳遞信息——子組件中展示用戶的個人信息
  • 父組件:用戶個人空間
  • 傳遞信息是通過給子組件綁定一個封裝的對象::user="user",這里的: == v.bind:,是一個簡寫。

UserProfile.vue

<template><ContentBase><div class="row"><div class="col-3"><UserInfo :user="user"/></div><div class="col-9"><UserProfileContent /></div></div></ContentBase>
</template><script>
import ContentBase from '@/components/ContentBase.vue';
import UserInfo from '@/components/UserInfo.vue';
import UserProfileContent from '@/components/UserProfileContent.vue';
import { reactive } from 'vue';export default {name: 'UserProfileView',components: {ContentBase,UserInfo,UserProfileContent},setup() {const user = reactive({id: 1,username: "vue hello",lastName: "Vue",firstName: "hello",followCount: 666,fans: 2333,favoriaties: 1234,is_followed: true,});return {user: user,}}
}
</script>
<style scoped></style>
  • 子組件通過user獲取到封裝的值
  • 如果需要對數據進行拼接等運算操作,則需要將user放到props中通過setup()函數重新定義一個值作為運算的返回值:

UserInfo.vue:

<template><div class="row"><div class="col-3"><div class="user-card"><div class="user-card-head"></div><div class="user-card-body"><div class="user-card-body-left"><img src="https://cdn.acwing.com/media/user/profile/photo/450127_md_da0516e621.jpg" alt=""></div><div class="user-card-body-right"><div class="user-card-body-right-text"><div class="user-card-body-right-text-username">{{fullName}}<span>LV5</span></div><div class="user-card-body-right-text-reputation"><span class="user-card-body-right-text-reputation-item"><span>{{ user.followCount }}</span><span>關注</span></span><span class="user-card-body-right-text-reputation-item"><span>{{ user.fans }}</span><span>粉絲</span></span><span class="user-card-body-right-text-reputation-item"><span>{{ user.favoriaties}}</span><span></span></span></div><div class="user-card-body-right-text-discreption">這個人很懶~什么也沒留下</div></div><div class="user-card-body-right-button"><button @click="follow" v-if="!user.is_followed">+關注</button><button @click="unfollow" v-if="user.is_followed">取消關注</button><button>發消息</button></div></div></div></div></div><div class="col-9"></div></div>
</template><script>
import { computed } from 'vue';
export default {name: "UserInfo",props: {user: {type: Object,required: true,},},setup(props) {let fullName = computed(() => props.user.lastName + ' ' + props.user.firstName);const follow = () => {}const unfollow = () => {}return {fullName,follow,unfollow,}}
}
</script><style scoped>
//此處先省略,避免重復篇幅
</style>
2.子組件通過觸發父組件的函數操作數據——子組件中點擊關注按鈕需要將關注數進行+ 1
  • 1.UserProfile.vue:在父組件中定義函數,傳遞到子組件中
<template><ContentBase><div class="row"><div class="col-3"><UserInfo @follow="follow" @unfollow="unfollow" :user="user"/></div><div class="col-9"><UserProfileContent /></div></div></ContentBase>
</template><script>
import ContentBase from '@/components/ContentBase.vue';
import UserInfo from '@/components/UserInfo.vue';
import UserProfileContent from '@/components/UserProfileContent.vue';
import { reactive } from 'vue';export default {name: 'UserProfileView',components: {ContentBase,UserInfo,UserProfileContent},setup() {const user = reactive({id: 1,username: "hello vue",lastName: "Vue",firstName: "Hello",followCount: 666,fans: 2333,favoriaties: 1234,is_followed: false,});const follow = () => {if (user.is_followed) return;user.is_followed = true;user.fans ++;};const unfollow = () => {if (!user.is_followed) return;user.is_followed = false;user.fans --;};return {user: user,follow,unfollow,}}
}
</script><style scoped></style>
  • 2.UserInfo.vue:子組件中通過context獲取到父組件傳遞過來的函數,從而在自定義的函數中通過context.emit()觸發對應的父組件函數進行數據的操作
  • 將子組件中自定義的函數綁定到按鈕上,點擊按鈕觸發自定義函數,而自定義函數中會觸發父組件傳遞過來的函數,從而對父組件中存儲的數據進行修改。
<template><div class="row"><div class="col-3"><div class="user-card"><div class="user-card-head"></div><div class="user-card-body"><div class="user-card-body-left"><img src="https://cdn.acwing.com/media/user/profile/photo/450127_md_da0516e621.jpg" alt=""></div><div class="user-card-body-right"><div class="user-card-body-right-text"><div class="user-card-body-right-text-username">{{fullName}}<span>LV5</span></div><div class="user-card-body-right-text-reputation"><span class="user-card-body-right-text-reputation-item"><span>{{ user.followCount }}</span><span>關注</span></span><span class="user-card-body-right-text-reputation-item"><span>{{ user.fans }}</span><span>粉絲</span></span><span class="user-card-body-right-text-reputation-item"><span>{{ user.favoriaties}}</span><span></span></span></div><div class="user-card-body-right-text-discreption">這個人很懶~什么也沒留下</div></div><div class="user-card-body-right-button"><button @click="follow" type="button"  v-if="!user.is_followed">+關注</button><button v-on:click="unfollow" type = "button" v-if="user.is_followed">取消關注</button><button>發消息</button></div></div></div></div></div><div class="col-9"><button v-on:click="follow" type="button">去哪</button></div></div>
</template><script>
import { computed } from 'vue';
export default {name: "UserInfo",props: {user: {type: Object,required: true,},},setup(props, context) {let fullName = computed(() => props.user.lastName + ' ' + props.user.firstName);const follow = () => {console.log("follow");context.emit('follow');};const unfollow = () => {console.log('unfollow');context.emit('unfollow');};return {fullName,follow,unfollow}}
}
</script><style scoped>.user-card-body-right-button>button {width: 100px;height: 30px;font-size: 14px;border-radius: 5px;border: none;cursor: pointer;position: relative;z-index: 9999;border: 2px solid;
}
</style>
  • Vue中綁定一個事件使用v-on:click="xxx",可以簡寫成@click="xxx"
  • 觸發父組件傳遞的函數使用context.emit()
  • 總的來說就是:數據在哪定義的在哪修改

遇到的一個小問題:
  • 我是直接將之前寫的一個模仿B站的小卡片直接作為個人信息的樣式展示的
  • 但是獲取不到按鈕的點擊事件觸發:
  • 調了半天:最后發現是由于之前的樣式中
    .user-card-body-right 里用了 float: left; 和一些固定高度的子元素(例如 .user-card-body-right-text 高度寫了 70%),這樣會導致 .user-card-body-right-text 的容器區域蓋住了下面的按鈕。雖然它是透明的,但實際上它在按鈕上方,占據了點擊事件。所以看到按鈕在頁面上,鼠標點擊的時候,事件卻落在透明的文字容器上 → 按鈕沒反應。
  • 解決方案:我這里使用的是簡單方法:通過position: relative + z-index。只有在元素是定位元素(position: relative/absolute/fixed/sticky)時,z-index 才會生效。
    設置了 position: relative; z-index: 9999;,把按鈕抬到了“最上層”,高于之前擋住它的元素。所以按鈕又能被鼠標點到了。
  • 較優的解決方案是將float布局改成flex布局,就不需要靠z-index來壓層了

  • 目前的頁面布局:
    在這里插入圖片描述

3)數據對象添加展示——帖子展示

  • 1.在父組件中將帖子封裝成一個數組對象:

UserProfile.vue:

    const posts = reactive({count: 3,posts: [{id: 1,userId: 1,content: "今天上了web應用課真開心",},{id: 2,userId: 1,content: "今天學了Vue真開心",},{id: 3,userId: 1,content: "今天天氣不錯",},]});
  • 2.展示帖子:
  • 使用v-for="post in posts.posts" :key="post.id"來遍歷傳過來的帖子對象數組,只展示其中post的content部分:

UserProfileContent.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: "UserProfileContent",props: {posts: {type: Object,required: true,},}
}
</script><style scoped>
.single-post {margin-bottom: 10px;
}
</style>

*3.完善UserEditProfile.vue發帖編輯區:
在bootstrap官網找到對應的編輯樣式,直接復制過來修改:

<template><div class="card edit-field"><div class="card-body"><div class="mb-3"><label for="edit-title" class="form-label">標題</label><input type="email" class="form-control" id="exampleFormControlInput1"></div><div class="mb-3"><label for="edit-post" class="form-label">編輯帖子</label><textarea v-model="content" class="form-control" id="exampleFormControlTextarea1" rows="3"></textarea><button  type="button" class="btn btn-primary btn-sm" >發帖</button></div></div></div>
</template><script>
import { ref } from 'vue';export default {name: "UserEditProfile",setup(props, context) {let content = ref('');return {content,}}
}
</script><style scoped>.edit-field {margin-top: 20px;
}button {margin-top: 10px;
}
</style>
  • 頁面展示:
    在這里插入圖片描述

4)動態對象添加——發帖并展示

  • 在發帖編輯區編輯好帖子之后,點擊發帖會上傳到帖子展示區
  • 這就涉及到UserEditProfile區需要添加一個帖子對象到帖子數組中
  • 屬于子組件觸發父組件傳遞的函數來修改數據
  • 1.首先通過v-model="content"獲取到帖子編輯區的內容,作為參數傳遞給子組件中的submit_post函數
  • 2.點擊發帖按鈕綁定子組件的submit_post函數,點擊時,觸發子組件函數并傳入content表示帖子的內容,從而觸發父組件傳遞的submit_post函數,并傳入content,而父組件中,如果該函數觸發則會將content封裝成一個貼子對象,通過unshift()添加到帖子數組的頂部,完成最新的動態展示

UserEditProfile.vue:

<template><div class="card edit-field"><div class="card-body"><div class="mb-3"><label for="edit-title" class="form-label">標題</label><input type="email" class="form-control" id="exampleFormControlInput1"></div><div class="mb-3"><label for="edit-post" class="form-label">編輯帖子</label><textarea v-model="content" class="form-control" id="exampleFormControlTextarea1" rows="3"></textarea><button @click="submit_post" type="button" class="btn btn-primary btn-sm" >發帖</button></div></div></div>
</template>
<script>
import { ref } from 'vue';export default {name: "UserEditProfile",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>

UserProfileView.vue:

<template><ContentBase><div class="row"><div class="col-3"><UserInfo @follow="follow" @unfollow="unfollow" :user="user"/><UserEditProfileVue @submit_post="submit_post"/></div><div class="col-9"><UserProfileContent :posts="posts" /></div></div></ContentBase>
</template><script>
import ContentBase from '@/components/ContentBase.vue';
import UserInfo from '@/components/UserInfo.vue';
import UserProfileContent from '@/components/UserProfileContent.vue';
import { reactive } from 'vue';
import UserEditProfileVue from '@/components/UserEditProfile.vue';export default {name: 'UserProfileView',components: {ContentBase,UserInfo,UserProfileContent,UserEditProfileVue},setup() {const user = reactive({id: 1,username: "zhongliubin",lastName: "Zhong",firstName: "Liubin",followCount: 666,fans: 2333,favoriaties: 1234,is_followed: false,});const posts = reactive({count: 3,posts: [{id: 1,userId: 1,content: "今天上了web應用課真開心",},{id: 2,userId: 1,content: "今天學了Vue真開心",},{id: 3,userId: 1,content: "今天天氣不錯",},]});const follow = () => {if (user.is_followed) return;user.is_followed = true;user.fans ++;};const unfollow = () => {if (!user.is_followed) return;user.is_followed = false;user.fans --;};const submit_post = (content) => {if (content == '') return;posts.count ++;posts.posts.unshift({id: posts.count,userId: 1,content: content,});};return {user: user,follow,unfollow,posts,submit_post,}}
}
</script>
<style scoped>
</style>
  • 頁面展示:標題區并未完成,后續如果有時間再完善
    在這里插入圖片描述

5)好友列表展示——模擬從云端獲取好友數據

  • 模擬從云端獲取好友數據并展示
  • 這里直接使用課程提供的API從云端獲取數據,如果有興趣,可以自己在服務器中實現該API
  • setup()中使用ref定義一個user數組,用于接收請求返回的user列表
  • 然后將user列表中的信息通過v-for="user in users" :key="user.id"獲取到每個user的頭像、用戶名、粉絲數用于信息展示

UserListProfile.vue:

<template><ContentBase><div class="card" v-for="user in users" :key="user.id"><div class="card-body"><div class="row"><div class="col-1"><img class="img-fluid" v-bind:src="user.photo" alt=""></div><div class="col-11"><div class="username">{{ user.username }} </div><div class="followcount">{{ user.followerCount }}</div></div></div></div></div></ContentBase>
</template><script>
import ContentBase from '@/components/ContentBase.vue'
import $ from 'jquery';
import { ref } from 'vue';export default {name: 'UserListView',components: {ContentBase},setup() {let users = ref([]);$.ajax({url: 'https://app165.acapp.acwing.com.cn/myspace/userlist/',type: "GET",success(resp) {users.value = resp;}});return {users,};}
}
</script><style scoped>
img {border-radius: 50%;
}.username {font-weight: bold;height: 50%;
}.followcount {font-size: 12px;height: 50%;color: gray;
}.card {margin-bottom: 20px;cursor: pointer;
}.card:hover {box-shadow: 2px 2px 10px lightsalmon;transition: 500ms;
}
</style>
  • 頁面效果:
    在這里插入圖片描述

6)訪問不同好友空間——鏈接添加參數訪問不同id的空間頁面

  • 在訪問空間頁面時,是通過userId來訪問,于是路徑中需要添加對應的參數
  • router.js添加:
  {path: '/userprofile/:userId/',name: 'name-userprofile',component: UserProfileView},
如何取得鏈接中的參數
  • 1.NavBar中,訪問時需要用到參數,則需要設置訪問路徑的參數:params: {}
<li class="nav-item"><router-link class="nav-link" :to="{name: 'name-userprofile', params: {userId: 2}}">好友動態</router-link>
</li>
  • 2.使用useRoute從而獲取到鏈接中的參數:
    const route = useRoute();console.log(route.params.userId);

7)登錄

  • 表單提交:@submit.prevent="login",表單提交事件為login,并且阻止默認的表單提交行為
  • 前端模擬:
<template><ContentBase><div class="row justify-content-center"><div class="col-3"><form @submit.prevent="login"><div class="mb-3"><label for="username" class="form-label">用戶名</label><input v-model="username" type="text" class="form-control" id="username"></div><div class="mb-3"><label for="password" class="form-label">密碼</label><input v-model="password" type="password" class="form-control" id="password"></div><div class="error-message">{{ error_message }}</div><button type="submit" class="btn btn-primary">登錄</button></form></div></div></ContentBase>
</template><script>
import ContentBase from '@/components/ContentBase.vue'
import { ref } from 'vue';export default {name: 'LoginView',components: {ContentBase},setup() {let username = ref('');let password = ref('');let error_message = ref('');const login = () => {console.log(username.value, password.value);}return {username: username,password: password,error_message: error_message,login,}}
}
</script><style scoped>button {width: 100%;}
</style>
1. 登錄狀態的維護
1.傳統Session的登錄維護方式:
* 用戶輸入用戶名+密碼去登錄時,會訪問服務器的登錄驗證
* 如果驗證通過,服務器會通過`cookie`返回一個`session_id`給客戶端,并將該`session_id`保存在數據庫中,用于判斷是屬于哪個用戶的登錄驗證
* 登錄成功之后,用戶每次向服務器發送請求,都會攜帶`session_id`,而服務器會去驗證該`session_id`是否存在并且是屬于該用戶,通過則可以繼續訪問
* Session 過期與銷毀:`Session` 有一個有效期限,一般通過設置一個固定的時間,或者在一定時間內沒有用戶活動時會將 `Session `標記為過期。當` Session `過期時,服務器會銷毀對應的 `Session` 數據,釋放內存或其他資源。

在這里插入圖片描述

  • 傳統的cookie維護登錄方式,在面對跨域訪問時,由于js限制無法訪問不同源的的資源,cookie是有跨域問題的,詳細介紹參考Cookie、Session、Token的區別
2.jwt維護跨域的登錄狀態(JSON Web Token)
  • 1.用戶輸入用戶名+密碼進行登錄時,首先會驗證用戶名和密碼是否正確

  • 2.驗證通過則由服務器將用戶的一些信息info+ 服務器本地配置的私鑰(一個字符串)通過加密算法得到一個新的Token,而這個Token會與info一起返回給客戶端

  • 3.當客戶端向服務器發送請求時,每次需要攜帶該Token和info,接收到請求后,服務器會將info與本地的私鑰進行拼接,再通過同一個加密算法求出此次請求返回的加密值是多少,將該加密值與客戶端傳回的token進行比對

  • 4.如果一致則允許訪問,如果不一樣則說明該信息被修改了,不合法,不允許訪問。

  • 過期與銷毀:可以在info中設置有效期,設置公鑰頒發的有效期,如果超過時間則該token過期,需要重新登錄并頒發新的token

  • 該狀態的安全性是由加密算法保證的:info+私鑰 -> token是很容易的,但是如果想通過token + info -> 私鑰是幾乎無法在短時間內突破的,如果我們修改了info的信息,由于無法獲取私鑰的值,那么加密產生的token必然是不同的。
    在這里插入圖片描述

  • 通過postman查看后端服務器提供的api返回的jwt:
    在這里插入圖片描述

  • 通過jwt的解析網站查看jwt中包含的信息:
    在這里插入圖片描述

  • access: 訪問令牌,有效期5分鐘, 直接獲取服務器端的認證jwt
    refresh: 刷新令牌,有效期14天

  • 為什么還要有refresh令牌,GET請求的api會將參數添加到路徑上,不安全,所以access的令牌有效期較短,而refresh的令牌是通過POST請求獲取的,將refresh的令牌傳入就能獲取到一個新的access令牌,每次access令牌過期,就會將refresh令牌傳到refreshapi中,重新獲取一個新的access令牌

補充知識點一、什么是跨域

跨域是指瀏覽器出于安全考慮,限制網頁腳本訪問不同源(協議、域名、端口)的資源。兩個URL的協議、域名或端口任意一個不相同時,就屬于不同源,瀏覽器會阻止腳本請求從一個源加載的文檔與另一個源的資源進行交互。

補充知識點二、跨域產生的原因

跨域問題產生的根本原因是瀏覽器的同源策略(Same-Origin Policy)。同源策略是瀏覽器實現的一種安全協議,它限制了一個源的文檔或腳本如何與另一個源的資源進行交互。如果沒有同源策略,惡意網頁可能會讀取另一個網頁的敏感信息,如用戶輸入的密碼、銀行賬號等,從而進行非法操作。

2.維護全局變量
  • 用戶信息需要維護成一個全局變量,便于各個組件獲取
  • Vue中使用vuex維護state,存儲全局變量
  • 為了使各個部分的信息獨立,新增module:user.js:主要存放user的個人信息
  • 這樣就可以通過store.state.user獲取user里面的相關信息

store.index.js

import { createStore } from 'vuex'
import ModuleUser from './user'export default createStore({state: {},getters: {},mutations: {},actions: {},modules: {user: ModuleUser,}
})

store.user.js

const ModuleUser = ({state: {id: "",username: "",photo: "",followerCount: 0,},getters: {},mutations: {},actions: {},modules: {}
});
export default ModuleUser;
  • state:主要維護變量
  • getters:讀取state的信息,然后進行運算等
  • actions:定義對state信息的異步更新操作
  • mutations:對state信息的同步更新操作
3.登錄的實現
1.獲取jwt-token令牌
  • 定義異步請求方法login:通過用戶名和密碼以POST請求形式訪問后端api(https://app165.acapp.acwing.com.cn/api/token/),成功的話會返回令牌通過resp參數接收

user.js:

import $ from 'jquery';const ModuleUser = ({state: {id: "",username: "",photo: "",followerCount: 0,},getters: {},mutations: {},actions: {login(context, data) {$.ajax({url: "https://app165.acapp.acwing.com.cn/api/token/",type: "POST",data: {username: data.username,password: data.password,},success(resp) {console.log(resp);}});}},modules: {}
});export default ModuleUser;
  • LoginView中通過調用store.dispathc("login")來觸發登錄令牌的請求,傳入參數為從登錄文本框和密碼文本框獲取的用戶名和密碼,并且添加了登錄成功和失敗的回調函數
    LoginView.vue:
<script>
import ContentBase from '@/components/ContentBase.vue'
import { ref } from 'vue';
import { useStore } from 'vuex';export default {name: 'LoginView',components: {ContentBase},setup() {const store = useStore();let username = ref('');let password = ref('');let error_message = ref('');const login = () => {store.dispatch("login", {username: username.value,password: password.value,success() {console.log("success");},error() {console.log("failed");}});};return {username: username,password: password,error_message: error_message,login,}}
}
</script>
  • 為了直觀只展示script部分
  • 請求結果展示:
    在這里插入圖片描述
2.解析jwt-token得到用戶在數據庫中的user_id
  • 使用jwtDecode函數來解析獲取到的jwt-token: import { jwtDecode } from “jwt-decode”;

user.js:

import $ from 'jquery';
import { jwtDecode } from 'jwt-decode';const ModuleUser = ({state: {id: "",username: "",photo: "",followerCount: 0,},getters: {},mutations: {},actions: {login(context, data) {$.ajax({url: "https://app165.acapp.acwing.com.cn/api/token/",type: "POST",data: {username: data.username,password: data.password,},success(resp) {const {refresh, access} = resp;const access_info = jwtDecode(access);console.log(access_info, refresh);}});}},modules: {}
});
export default ModuleUser;
  • 獲取結果:
    在這里插入圖片描述
3.通過user_id獲取用戶信息
  • 用戶的詳細信息一般是需要jwt驗證才能獲取
  • 所以當得到user_id后,通過該user_id向后端發送獲取用戶信息的請求(需要攜帶jwt-token)

user.js:

import $ from 'jquery';
import { jwtDecode } from 'jwt-decode';const ModuleUser = ({state: {id: "",username: "",photo: "",followerCount: 0,},getters: {},mutations: {},actions: {login(context, data) {$.ajax({url: "https://app165.acapp.acwing.com.cn/api/token/",type: "POST",data: {username: data.username,password: data.password,},success(resp) {const {access} = resp;const access_info = jwtDecode(access);$.ajax({url: "https://app165.acapp.acwing.com.cn/myspace/getinfo/",type: "GET",data: {user_id: access_info.user_id,},headers: {'Authorization': "Bearer " + access,},success(resp) {console.log(resp);}});}});}},modules: {}
});export default ModuleUser;
  • 用戶信息獲取結果:

在這里插入圖片描述

4.將獲取的用戶信息更新到當前全局中
  • mutations中定義對user的更新操作
  • 而當通過登錄請求獲取到user_id,并通過user_id發送獲取用戶信息請求成功后,觸發在mutations中定義的updateUser,將全局中的user更新成登錄的user
  • 調用作為參數傳遞過來的data.success(),證明根據輸入的用戶名和密碼,登錄成功
  • 如果整個過程失敗了,則說明賬戶和密碼是不對的,無法獲取到jwt-token

user.js:

import $ from 'jquery';
import { jwtDecode } from 'jwt-decode';const ModuleUser = ({state: {id: "",username: "",photo: "",followerCount: 0,access: "",refresh: "",is_login: false,},getters: {},mutations: {updateUser(state, user) {state.id = user.id;state.username = user.username;state.photo = user.photo;state.followerCount = user.followerCount;state.access = user.access;state.refresh = user.refresh;state.is_login = user.is_login;},},actions: {login(context, data) {$.ajax({url: "https://app165.acapp.acwing.com.cn/api/token/",type: "POST",data: {username: data.username,password: data.password,},success(resp) {const {access, refresh} = resp;const access_info = jwtDecode(access);$.ajax({url: "https://app165.acapp.acwing.com.cn/myspace/getinfo/",type: "GET",data: {user_id: access_info.user_id,},headers: {'Authorization': "Bearer " + access,},success(resp) {context.commit("updateUser", {...resp,access: access,refresh: refresh,is_login: true,});data.success(); },});},error() {data.error();}});}},modules: {}
});
export default ModuleUser;
  • 登錄成功展示:
    在這里插入圖片描述
  • 登錄失敗展示:
    在這里插入圖片描述
5.維護jwt-token的有效期
  • 因為access令牌有效期是五分鐘,而整體的refresh令牌有效期是14天
  • 為了維護14天的有效登錄,需要每隔五分鐘通過refresh請求,獲取一個新的access令牌
  • 于是我們需要定義函數使得每五分鐘能夠發送refresh請求,獲取新的access令牌
  • 而為了防止出現邊界,比如剛好五分鐘時,瀏覽器卡住,導致請求發送失敗,access令牌刷新失敗,登錄狀態失效,我們可以設置為四分半發送一次refersh請求。
    *使用setInterval()設置周期函數每四分半發送一次refresh請求
import $ from 'jquery';
import { jwtDecode } from 'jwt-decode';const ModuleUser = ({state: {id: "",username: "",photo: "",followerCount: 0,access: "",refresh: "",is_login: false,},getters: {},mutations: {updateUser(state, user) {state.id = user.id;state.username = user.username;state.photo = user.photo;state.followerCount = user.followerCount;state.access = user.access;state.refresh = user.refresh;state.is_login = user.is_login;},updateAccess(access) {state.access = access;}},actions: {login(context, data) {$.ajax({url: "https://app165.acapp.acwing.com.cn/api/token/",type: "POST",data: {username: data.username,password: data.password,},success(resp) {const {access, refresh} = resp;const access_info = jwtDecode(access);setInterval(() => {$.ajax({url: "https://app165.acapp.acwing.com.cn/api/token/refresh/",type: "POST",data: {refresh: refresh,},success(resp) {console.log(resp);context.commit("updateAccess", resp.access);},});}, 4.5 * 60 * 1000);$.ajax({url: "https://app165.acapp.acwing.com.cn/myspace/getinfo/",type: "GET",data: {user_id: access_info.user_id,},headers: {'Authorization': "Bearer " + access,},success(resp) {context.commit("updateUser", {...resp,access: access,refresh: refresh,is_login: true,});data.success(); },});},error() {data.error();}});}},modules: {}
});
export default ModuleUser;
  • 當登錄成功之后,每四分半發送一次refresh請求,請求到新的access之后觸發mutationsupdateAccess(access),將全局存放的access替換成最新的access,以此來維護長期的登錄有效狀態
  • 通過refresh刷新access展示:
    在這里插入圖片描述
6.登錄成功跳轉到好友列表
  • 通過router.push({name: "name-userlist"});: 登錄成功則跳轉到name-userlist
  • 如果登錄失敗,則將erro_message = '用戶名或者密碼錯誤'
export default {name: 'LoginView',components: {ContentBase},setup() {const store = useStore();let username = ref('');let password = ref('');let error_message = ref('');const login = () => {error_message.value = "";store.dispatch("login", {username: username.value,password: password.value,success() {console.log("success");router.push({name: 'name-userlist'});},error() {console.log("failed");error_message.value = "用戶名或者密碼錯誤";}});};return {username: username,password: password,error_message: error_message,login,}}
}
7.NavBar右上角的登錄信息變更
  • 當登錄成功之后,NavBar右上角的登錄和注冊需要變成用戶名和退出進行展示
  • 這里使用v-if 和v-else

NavBar.vue:

<template><nav class="navbar navbar-expand-lg bg-body-tertiary"><div class="container"><router-link class="navbar-brand" :to="{name: 'home'}">SharingSpace</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'}">Home</router-link></li><li class="nav-item"><router-link class="nav-link" :to="{name: 'name-userlist'}">好友列表</router-link></li><li class="nav-item"><router-link class="nav-link" :to="{name: 'name-userprofile', params: {userId: 2}}">好友動態</router-link></li></ul><ul class="navbar-nav" v-if="!$store.state.user.is_login"><li class="nav-item"><router-link class="nav-link" :to="{name: 'name-login'}">登錄</router-link></li><li class="nav-item"><router-link class="nav-link" :to="{name: 'name-register'}">注冊</router-link></li></ul><ul class="navbar-nav" v-else><li class="nav-item"><router-link class="nav-link" :to="{name: 'name-userprofile', params: {userId: $store.state.user.id}}">{{ $store.state.user.username }}</router-link></li><li class="nav-item"><a class="nav-link" style="cursor: pointer">退出</a></li></ul></div></div></nav>
</template><script>
export default {name: "NavBar",
}</script><style scoped></style>
  • 頁面展示:
    在這里插入圖片描述
8.退出登錄
  • 當需要退出登錄時,直接將jwt令牌刪除即可
  • 簡單比喻就是:給你發了一張通行證,當你不想再進行通行時,直接將通行證撕毀即可,不用通知發證人
  • 由于jwt令牌存放在全局變量access中,所以需要在user.js中定義刪除令牌的邏輯
vue知識點
  • 1.調用mutations里面的api使用commit()
  • 2.調用actions里面的api使用dispatch()

store.user.js:主要展示mutations里面的logout邏輯

import $ from 'jquery';
import { jwtDecode } from 'jwt-decode';const ModuleUser = ({state: {id: "",username: "",photo: "",followerCount: 0,access: "",refresh: "",is_login: false,},getters: {},mutations: {updateUser(state, user) {state.id = user.id;state.username = user.username;state.photo = user.photo;state.followerCount = user.followerCount;state.access = user.access;state.refresh = user.refresh;state.is_login = user.is_login;},updateAccess(state, access) {state.access = access;},logout(state) {state.access = "";state.id = "";state.username = "";state.photo = "";state.followerCount = 0;state.refresh = "";state.is_login = false;},},
});
export default ModuleUser;

NavBar.vue: 主要展示退出的綁定邏輯

<template><ul class="navbar-nav" v-else><li class="nav-item"><router-link class="nav-link" :to="{name: 'name-userprofile', params: {userId: $store.state.user.id}}">{{ $store.state.user.username }}</router-link></li><li class="nav-item"><a class="nav-link" @click="logout" style="cursor: pointer">退出</a></li></ul>
</template><script>import { useStore } from 'vuex';export default {name: "NavBar",setup() {const store = useStore();const logout = () => {store.commit("logout");}return {logout,}}
}
</script>
  • 頁面效果:當登錄成功之后會跳轉到好友列表,右上角展示用戶名以及退出,點擊退出則展示登錄注冊
    在這里插入圖片描述

7)權限控制——只有登錄狀態才能查看自己和好友的個人空間以及發帖

  • 1.先將用戶動態頁面從導航欄中刪除
  • 2.在登錄后,點擊好友列表的其中一個,跳轉到對應的好友個人空間/userprofile/userid: router.push({name: "name-userprofile", params: {userId}});
  • 3.如果未登錄則跳轉到登錄界面:router.push({name: "name-login"});
    UserListView.vue:
<template><ContentBase><div class="card" v-for="user in users" :key="user.id" @click="click_userprofile(user.id)"><div class="card-body"><div class="row"><div class="col-1"><img class="img-fluid" v-bind:src="user.photo" alt=""></div><div class="col-11"><div class="username">{{ user.username }}</div><div class="followcount">{{ user.followerCount }}</div></div></div></div></div></ContentBase>
</template><script>
import ContentBase from '@/components/ContentBase.vue'
import $ from 'jquery';
import { ref } from 'vue';
import router from '@/router/index';
import { useStore } from 'vuex';export default {name: 'UserListView',components: {ContentBase},setup() {let users = ref([]);const store = useStore();$.ajax({url: 'https://app165.acapp.acwing.com.cn/myspace/userlist/',type: "GET",success(resp) {users.value = resp;}});const click_userprofile = userId => {if (store.state.user.is_login) {router.push({name: "name-userprofile",params: {userId: userId,},});} else {router.push({name: "name-login",});}};return {users,click_userprofile,};}
}
</script>
  • 將該事件綁定到好友列表的card上,這樣當點擊其中一個card時,就會觸發該click_userprofile(userId),根據card分配的user.id,傳入到路徑中,從而完成對應個人空間的跳轉

8)從云端動態拉取個人空間信息——點擊對應的好友跳轉到它的個人空間,展示是否關注以及粉絲數等信息

  • 跳轉到每個用戶對應的userprofile后,需要根據userId去向服務器發送獲取用戶信息請求

UserProfileView.vue: 主要展示拉取用戶信息的請求編寫

<script>
export default {name: 'UserProfileView',components: {ContentBase,UserInfo,UserProfileContent,UserEditProfileVue},setup() {const route = useRoute();const userId = route.params.userId;console.log(userId);const user = reactive({});const posts = reactive({});const store = useStore();$.ajax({url: "https://app165.acapp.acwing.com.cn/myspace/getinfo/",type: "GET",data: {user_id: userId,},headers: {'Authorization': "Bearer " + store.state.user.access,},success(resp) {console.log(resp);user.id = resp.id;user.username = resp.username;user.photo = resp.photo;user.followerCount = resp.followerCount;user.is_followed = resp.is_followed;},});}
}
</script>
  • 頁面效果:將對應的用戶信息拉取之后,就將里面的信息讀取到頁面對應的位置,由于這里的信息只包含username和關注數以及是否關注,粉絲數和獲贊數沒有,我就寫死為666和888了

在這里插入圖片描述

9)發帖區如果是訪問的是個人空間則需要展示,好友空間則不展示

  • 使用一個函數判斷:登錄的id:store.state.user.id是否與當前訪問的id:userId 是一樣的
  • 如果一樣,展示發帖區,不一樣則不展示
    UserProfileView.vue: 主要展示判斷邏輯以及使用v-if="is_me"邏輯
<template><ContentBase><div class="row"><div class="col-3"><UserInfo @follow="follow" @unfollow="unfollow" :user="user"/><UserEditProfileVue v-if="is_me" @submit_post="submit_post"/></div><div class="col-9"><UserProfileContent :posts="posts" /></div></div></ContentBase>
</template><script>
export default {name: 'UserProfileView',components: {ContentBase,UserInfo,UserProfileContent,UserEditProfileVue},setup() {const route = useRoute();const userId = parseInt(route.params.userId);console.log(userId);const user = reactive({});const posts = reactive({});const store = useStore();console.log(userId, store.state.user.id);const is_me = computed(() => userId === store.state.user.id);return {user: user,follow,unfollow,posts,submit_post,is_me,}}
}
</script>
  • 頁面效果:
    在這里插入圖片描述
    在這里插入圖片描述
  • 當點擊的是登錄者時,展示發貼區,其他好友空間則不展示

小bug:當進入好友空間點擊個人空間時,則不會跳轉到用戶的個人空間
  • 這是因為在路徑相同,參數不同時,會默認認為是同一個路徑,不會進行判重,比如:userprofile/22userprofile/65,默認為是同一個路徑,所以不跳轉
  • 在route上加入:key="$route.fullPath",使得他以全路徑名來判重,這樣參數不同,他就會識別為不同的路徑,從而完成跳轉
<template><NavBar/><router-view :key="$route.fullPath"/>
</template><script>
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap/dist/js/bootstrap.js';
import NavBar from './components/NavBar.vue';export default {name: "App",components: {NavBar: NavBar,}
}
</script>
<style>
</style>

10)動態添加帖子——編輯帖子之后發送POST請求,完成帖子的添加

  • 在自己的空間中編輯一個貼子,然后點擊發布,則需要向服務器發送發帖請求
  • 請求通過之后則完成編輯區內容賦值給content并清空編輯區

UserEditProfile.vue:

<template><div class="card edit-field"><div class="card-body"><div class="mb-3"><label for="edit-title" class="form-label">標題</label><input type="email" class="form-control" id="exampleFormControlInput1"></div><div class="mb-3"><label for="edit-post" class="form-label">編輯帖子</label><textarea v-model="content" class="form-control" id="exampleFormControlTextarea1" rows="3"></textarea><button @click="submit_post" type="button" class="btn btn-primary btn-sm" >發帖</button></div></div></div>
</template><script>
import { ref } from 'vue';
import $ from 'jquery';
import { useStore } from 'vuex';export default {name: "UserEditProfile",setup(props, context) {const store = useStore();let content = ref('');const submit_post = () => {$.ajax({url: "https://app165.acapp.acwing.com.cn/myspace/post/",type: "POST",data: {content: content.value,},headers: {'Authorization': "Bearer " + store.state.user.access, },success(resp) {if (resp.result === "success") {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>
  • 發帖展示:
    在這里插入圖片描述

11)刪除帖子——進入自己的空間之后,可以刪除對應的帖子

  • 增加刪除按鈕,用于刪除對應的帖子
  • 需要特判:如果是自己的空間才展示刪除按鈕,如果不是則不展示

UserProfileContent.vue: 需要傳入user用于獲取當前登錄的用戶id

<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 }}<button type="button" v-if="is_myself" class="btn btn-danger btn-sm">刪除</button></div></div></div></div></div>
</template><script>import { useStore } from 'vuex';
import { computed } from 'vue';export default {name: "UserProfileContent",props: {posts: {type: Object,required: true,},user: {type: Object,required: true,}},setup(props) {const store = useStore();let is_myself = computed(() => store.state.user.id === props.user.id);return {is_myself,}}
}
</script>
<style scoped>
.single-post {margin-bottom: 10px;
}button {float: right;
}
</style>
  • 自己的空間展示刪除按鈕:
    在這里插入圖片描述
  • 不是自己的空間則不展示:
    在這里插入圖片描述
1.前端刪除邏輯——點擊帖子,獲取帖子的id,然后從帖子列表中將該帖子id對應的帖子刪除
  • 在因為帖子數組posts是定義在UserProfile中,需要修改帖子的話需要在這個組件中完成帖子的實際刪除
  • 但是具體的帖子是由UserProfileContent管理并展示的,這就需要父組件UserProfile將刪除函數傳遞給子組件UserProfileContent,使得子組件能夠觸發該函數
  • 所以我們的刪除邏輯就是在UserProfileContent組件中,點擊某個帖子的刪除鍵時,獲取到該帖子的post.id,并觸發刪除帖子的函數,傳入刪除的該帖子id
  • 而具體的帖子刪除是在父組件的delete_post中,于是我們在子組件UserProfileContent中定義它的delete_post,當點擊刪除時,觸發該函數,在該函數觸發后,會觸發父組件傳過來的真正的delete_post函數

UserProfileView.vue: 具體的帖子刪除

<template><ContentBase><div class="row"><div class="col-3"><UserInfo @follow="follow" @unfollow="unfollow" :user="user"/><UserEditProfileVue v-if="is_me" @submit_post="submit_post"/></div><div class="col-9"><UserProfileContent :user="user" :posts="posts" @delete_post="delete_post"/></div></div></ContentBase>
</template><script>export default {name: 'UserProfileView',components: {ContentBase,UserInfo,UserProfileContent,UserEditProfileVue},setup() {const route = useRoute();const userId = parseInt(route.params.userId);console.log(userId);const user = reactive({});const posts = reactive({});const store = useStore();//點擊一個post時,獲取到它的post_id,將該post從posts中過濾掉const delete_post = post_id => {//返回true則保留,否則過濾掉,當傳入的post_id跟數組中的post.id相同時過濾console.log("dddddd");posts.posts = posts.posts.filter(post => post.id !== post_id);posts.count = posts.posts.length;};const is_me = computed(() => userId === store.state.user.id);return {user: user,follow,unfollow,posts,submit_post,is_me,delete_post,}}
}
</script>
<style scoped>
</style>

UserProfileContent.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 }}<button @click="delete_post(post.id)" type="button" v-if="is_myself" class="btn btn-danger btn-sm">刪除</button></div></div></div></div></div>
</template><script>import { useStore } from 'vuex';
import { computed } from 'vue';export default {name: "UserProfileContent",props: {posts: {type: Object,required: true,},user: {type: Object,required: true,}},setup(props, context) {const store = useStore();let is_myself = computed(() => store.state.user.id === props.user.id);const delete_post  = post_id => {context.emit('delete_post', post_id);};return {is_myself,delete_post,}}
}
</script><style scoped>
.single-post {margin-bottom: 10px;
}button {float: right;
}
</style>
  • 頁面效果:點擊刪除時能夠從前端展示中將該帖子刪除
    在這里插入圖片描述
2.刪除時向后端發送刪除請求——從數據庫中真正刪除該帖子
  • 通過發送刪除帖子的請求,將該帖子真正從數據庫刪除
    UserProfileContent.vue:
<script>
import { useStore } from 'vuex';
import { computed } from 'vue';
import $ from 'jquery';export default {name: "UserProfileContent",props: {posts: {type: Object,required: true,},user: {type: Object,required: true,}},setup(props, context) {const store = useStore();let is_myself = computed(() => store.state.user.id === props.user.id);const delete_post  = post_id => {$.ajax({url: "https://app165.acapp.acwing.com.cn/myspace/post/",type: "DELETE",data: {post_id: post_id,},headers: {'Authorization': "Bearer " + store.state.user.access,},success(resp) {if (resp.result === "success") {context.emit('delete_post', post_id);}}});};return {is_myself,delete_post,}}
}
</script>

12)注冊功能

  • 與登錄功能類似,直接向服務器發送注冊請求,傳入用戶名、密碼和確認密碼
    在這里插入圖片描述

  • 注冊請求發送成功之后就跟登錄是一樣的邏輯,于是套用login的代碼:

RegisterView.vue:

<template><ContentBase><div class="row justify-content-center"><div class="col-3"><form @submit.prevent="register"><div class="mb-3"><label for="username" class="form-label">用戶名</label><input v-model="username" type="text" class="form-control" id="username"></div><div class="mb-3"><label for="password" class="form-label">密碼</label><input v-model="password" type="password" class="form-control" id="password"></div><div class="mb-3"><label for="password_confirm" class="form-label">確認密碼</label><input v-model="password_confirm" type="password" class="form-control" id="password_confirm"></div><div class="error-message">{{ error_message }}</div><button type="submit" class="btn btn-primary">注冊</button></form></div></div></ContentBase>
</template><script>
import ContentBase from '@/components/ContentBase.vue'
import { ref } from 'vue';
import { useStore } from 'vuex';
import router from '@/router/index';
import $ from 'jquery';export default {name: 'RegisterView',components: {ContentBase},setup() {const store = useStore();let username = ref('');let password = ref('');let password_confirm = ref('');let error_message = ref('');console.log(store, router);const register = () => {error_message.value = "";$.ajax({url: "https://app165.acapp.acwing.com.cn/myspace/user/",type: "POST",data: {username: username.value,password: password.value,password_confirm: password_confirm.value,},success(resp) {if (resp.result === "success") {store.dispatch("login", {username: username.value,password: password.value,success() {console.log("success");router.push({name: 'name-userlist'});},error() {error_message.value = "服務器異常,請稍后再試";},});} else {error_message.value = resp.result;}}});};return {username: username,password: password,password_confirm,error_message,register,}}
}
</script>
<style scoped>button {width: 100%;}.error-message {color: red;}
</style>
  • 注冊成功之后,會直接跟登錄一樣,右上角顯示注冊的用戶名以及退出登錄選項
    在這里插入圖片描述

13)關注功能——點擊關注按鈕更新數據庫的關注狀態

  • 每次在前端修改之前,先向后端發送請求,根據后端返回結果修改前端的數據展示,使得前后端是同步
  • 在子組件中點擊關注和取消關注是向后端發送請求,如果后端修改成功則觸發前端在父組件的關注狀態修改邏輯
    UserInfo.vue:
<template><div class="row"><div class="col-3"><div class="user-card"><div class="user-card-head"></div><div class="user-card-body"><div class="user-card-body-left"><img :src="user.photo" alt=""></div><div class="user-card-body-right"><div class="user-card-body-right-text"><div class="user-card-body-right-text-username">{{ user.username }}<span>LV5</span></div><div class="user-card-body-right-text-reputation"><span class="user-card-body-right-text-reputation-item"><span>{{ user.followerCount }}</span><span>關注</span></span><span class="user-card-body-right-text-reputation-item"><span>666</span><span>粉絲</span></span><span class="user-card-body-right-text-reputation-item"><span>888</span><span></span></span></div><div class="user-card-body-right-text-discreption">這個人很懶~什么也沒留下</div></div><div class="user-card-body-right-button"><button @click="follow" type="button"  v-if="!user.is_followed">+關注</button><button v-on:click="unfollow" type = "button" v-if="user.is_followed">取消關注</button><button>發消息</button></div></div></div></div></div><div class="col-9"></div></div>
</template><script>
import { computed } from 'vue';
import $ from 'jquery';
import { useStore } from 'vuex';export default {name: "UserInfo",props: {user: {type: Object,required: true,},},setup(props, context) {const store = useStore();let fullName = computed(() => props.user.lastName + ' ' + props.user.firstName);const follow = () => {$.ajax({url: "https://app165.acapp.acwing.com.cn/myspace/follow/",type: "POST",data: {target_id: props.user.id,},headers: {'Authorization': "Bearer " + store.state.user.access,},success(resp) {if (resp.result === "success") {context.emit('follow');}}});};const unfollow = () => {$.ajax({url: "https://app165.acapp.acwing.com.cn/myspace/follow/",type: "POST",data: {target_id: props.user.id,},headers: {'Authorization': "Bearer " + store.state.user.access,},success(resp) {if (resp.result === "success") {context.emit('unfollow');}}});};return {fullName,follow,unfollow}}
}
</script><style scoped>img {border-radius: 50%;
}.user-card {width: 260px;height: 200px;box-shadow: 2px 2px 5px lightgrey;border-radius: 5px;
}.user-card-head {background-image: url('/src/assets/mountain.jpg');background-size: cover;width: 100%;height: 80px;
}.user-card-body {width: 260px;height: calc(100% - 85px);box-sizing: border-box;padding-top: 10px;
}.user-card-body-left {width: 65px;height: 100%;float: left;text-align: center;
}.user-card-body-left>img {border-radius: 50%;width: 48px;height: 48px;
}.user-card-body-right {width: calc(100% - 70px);height: 100%;float: left;
}.user-card-body-right-text {width: 100%;height: 70%;
}.user-card-body-right-text-username {font-size: 16px;color: #222222;font-weight: bold;padding: 0px 0px 10px 0px;
}.user-card-body-right-text-username>span {font-size: 12px;color: #8BD29B;font-style: italic;
}.user-card-body-right-text-reputation {width: 250px;height: 22px;padding: 0px 0px 10px 0px;font-size: 12px;color: #9499A0;box-sizing: border-box;line-height: 12px;
}.user-card-body-right-text-reputation-item {margin-right: 18px;
}.user-card-body-right-text-reputation-item>span:nth-child(1) {font-size: 12px;color: #18191c;padding: 0px 3px 0px 0px;
}.user-card-body-right-text-reputation-item>span:nth-child(2) {font-size: 12px;color: #9299A0;
}.user-card-body-right-text-discreption {margin-top: 1px;width: 200px;height: 10px;font-size: 12px;line-height: 10px;
}.user-card-body-right-button>button {width: 80px;height: 30px;font-size: 14px;border-radius: 5px;border: none;cursor: pointer;position: relative;z-index: 9999;border: 2px solid;
}.user-card-body-right-button>button:nth-child(1) {color: white;background-color: #00A1D6;padding: 1px 6px;float: left;margin-right: 5px;
}.user-card-body-right-button>button:nth-child(1):hover {background-color: #00B5E5;transition: 700ms;
}.user-card-body-right-button>button:nth-child(2) {padding: 1px 6px;color: #6D757A;float: left;border: #6D757A solid 1px;
}
</style>
  • 在父組件中真正執行關注和取消關注的前端數據修改:
    UserProfileView.vue:
<script>
export default {name: 'UserProfileView',components: {ContentBase,UserInfo,UserProfileContent,UserEditProfileVue},setup() {const route = useRoute();const userId = parseInt(route.params.userId);console.log(userId);const user = reactive({});const posts = reactive({});const store = useStore();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: user,follow,unfollow,posts,submit_post,is_me,delete_post,}}
}
</script>
  • 關注和取消關注持久化到數據庫展示:

在這里插入圖片描述
在這里插入圖片描述

  • 至此整個項目前端全部完成,所有數據交互都是通過訪問后端api進行的
  • 功能包括:好友列表、登錄注冊、發布帖子(日常分享)、刪除自己發布的帖子、查看好友的空間以及歷史動態、關注和取消關注

*注:以上內容來自acwing的web應用課vue框架的學習應用,僅作為學習交流,不用作商業用途,如果感興趣可以去該網站查看,y總nb!!!。以上內容如有侵權,聯系刪除。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/921823.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/921823.shtml
英文地址,請注明出處:http://en.pswp.cn/news/921823.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

HTML 網頁靜態托管 API 接口文檔(可集成到智能體Agent)

HTML 網頁靜態托管 API 接口文檔&#xff08;可集成到智能體Agent&#xff09; 接口概述 本接口用于將HTML代碼轉換為可訪問的網頁&#xff0c;支持通過API密鑰進行身份驗證。 API 密鑰申請地址&#xff1a; https://www.cuobiezi.net/user/api_keys/apply API接口信息 接…

springboot vue sse消息推送,封裝系統公共消息推送前后端方法

概述 1、封裝springboot全局的消息推送接口&#xff1b; 注&#xff1a;1&#xff09;由于原生HTML5 EventSource 不支持添加header&#xff0c;所以要把連接創建接口加入身份驗證白名單&#xff0c;并在接口內添加自己校驗token2&#xff09;后臺需定時心跳&#xff0c;保證鏈…

LeetCode 每日一題 2025/9/1-2025/9/7

記錄了初步解題思路 以及本地實現代碼&#xff1b;并不一定為最優 也希望大家能一起探討 一起進步 目錄9/1 1792. 最大平均通過率9/2 3025. 人員站位的方案數 I9/3 3027. 人員站位的方案數 II9/4 3516. 找到最近的人9/5 2749. 得到整數零需要執行的最少操作數9/6 3495. 使數組元…

小迪安全v2023學習筆記(八十講)—— 中間件安全WPS分析WeblogicJenkinsJettyCVE

文章目錄前記服務攻防——第八十天中間件安全&HW2023-WPS分析&Weblogic&Jetty&Jenkins&CVE應用WPS - HW2023-RCE&復現&上線CS介紹漏洞復現中間件 - Weblogic-CVE&反序列化&RCE介紹利用中間件 - Jenkins-CVE&RCE執行介紹漏洞復現CVE-20…

各webshell管理工具流量分析

哥斯拉哥斯拉是一個基于流量、HTTP全加密的webshell管理工具 特點 1.內置了3種Payload以及6種加密器&#xff0c;6種支持腳本后綴&#xff0c;20個內置插件 2.基于java&#xff0c;可以跨平臺使用 3.可以自己生成webshell&#xff0c;根據管理來生成一些payload&#xff0c;然后…

pytest(1):fixture從入門到精通

pytest&#xff08;1&#xff09;&#xff1a;fixture從入門到精通前言1. Fixture 是什么&#xff1f;為什么我們需要它&#xff1f;2. 快速上手&#xff1a;第一個 Fixture 與基本用法3. 作用域 (Scope)&#xff1a;控制 Fixture 的生命周期4. 資源管理&#xff1a;Setup/Tear…

Java17 LTS 新特性用例

基于 Java 17 LTS 的 實用示例 以下是基于 Java 17 LTS 的 30 個實用示例,涵蓋語言新特性、API 改進及常見場景。所有代碼均兼容 Java 17 語法規范。 文本塊(Text Blocks) String json = """{"name": "Java 17","type": &qu…

SpringBoot-Web開發-內容協商——多端內容適配內容協商原理HttpMessageConverter

其它篇章&#xff1a; 一&#xff1a;SpringBoot3-日志——日志原理&日志格式&日志級別&日志分組&文件輸出&文件歸檔&滾動切割 二&#xff1a;SpringBoot3-Web開發-靜態資源——WebMvcAutoConfiguration原理&資源映射&資源緩存&歡迎頁&…

Spring MVC 類型轉換與參數綁定:從架構到實戰

在 Spring MVC 開發中&#xff0c;“前端請求數據” 與 “后端 Java 對象” 的格式差異是高頻痛點 —— 比如前端傳的String類型日期&#xff08;2025-09-08&#xff09;要轉成后端的LocalDate&#xff0c;或者字符串male要轉成GenderEnum.MALE枚舉。Spring 并非通過零散工具解…

Spark提交任務的資源配置和優化

Spark 提交任務時主要可調的資源配置參數包括 Driver 資源&#xff08;內存、CPU&#xff09;、Executor 資源&#xff08;數量、內存、CPU&#xff09;以及 集群管理相關參數。配置和優化時一般結合集群硬件資源、數據規模、作業類型和作業復雜度&#xff08;SQL / 機器學習&a…

機器學習06——支持向量機(SVM核心思想與求解、核函數、軟間隔與正則化、支持向量回歸、核方法)

上一章&#xff1a;機器學習05——多分類學習與類別不平衡 下一章&#xff1a;機器學習07——貝葉斯分類器 機器學習實戰項目&#xff1a;【從 0 到 1 落地】機器學習實操項目目錄&#xff1a;覆蓋入門到進階&#xff0c;大學生就業 / 競賽必備 文章目錄一、間隔與支持向量&…

AI集群全鏈路監控:從GPU微架構指標到業務Metric關聯

點擊 “AladdinEdu&#xff0c;同學們用得起的【H卡】算力平臺”&#xff0c;H卡級別算力&#xff0c;80G大顯存&#xff0c;按量計費&#xff0c;靈活彈性&#xff0c;頂級配置&#xff0c;學生更享專屬優惠。 引言&#xff1a;AI算力時代的監控挑戰 隨著深度學習模型規模的指…

K8s Ingress Annotations參數使用指南

Kubernetes Ingress Annotations 是與特定 Ingress 控制器&#xff08;如 Nginx、Traefik、HAProxy 等&#xff09;配合使用&#xff0c;用于擴展和定制 Ingress 資源行為的關鍵配置項。它們通常以鍵值對的形式添加在 Ingress 資源的 metadata部分。Ingress Annotations參數速查…

CodeBuddy Code深度實戰:從零構建智能電商推薦系統的完整開發歷程

項目背景與挑戰作為一名有著多年全棧開發經驗的技術人員&#xff0c;我最近接手了一個具有挑戰性的項目&#xff1a;為某中型服裝電商平臺開發一套智能商品推薦系統。該系統需要在2個月內完成&#xff0c;包含以下核心功能&#xff1a;前端&#xff1a;React TypeScript構建的…

Day 19: 算法基礎與面試理論精通 - 從思想理解到策略掌握的完整體系

Day 19: 算法基礎與面試理論精通 - 從思想理解到策略掌握的完整體系 ?? 課程概述 核心目標:深度理解算法設計思想和核心原理,掌握面試高頻算法概念,建立完整的算法知識體系 學習重點: ? 核心數據結構的本質理解和應用場景分析 ? 經典算法設計模式的思想精髓和解題策…

AI與AR融合:重塑石化與能源巡檢的未來

在石化企業和新能源電站的巡檢工作中&#xff0c;傳統模式正被一場技術革命所顛覆。AI與AR&#xff08; www.teamhelper.cn &#xff09;的深度融合&#xff0c;不僅提升了巡檢效率&#xff0c;更將巡檢工作從被動響應轉變為預測預防&#xff0c;開啟了智能運維的新篇章。一、透…

滴滴二面(準備二)

手寫防抖函數并清晰闡述其價值&#xff0c;確實是前端面試的常見考點。下面我將為你直接呈現防抖函數的代碼&#xff0c;并重點結合滴滴的業務場景進行解釋&#xff0c;幫助你向面試官展示思考深度。 這是防抖函數的一個基本實現&#xff0c;附帶注釋以便理解&#xff1a; func…

Kubernetes(四):Service

目錄 一、定義Service 1.1 typeClusterIP 1.2 typeNodePort 1.3 typeLoadBalancer 1.4 typeExternalName 1.5 無標簽選擇器的Service 1.6 Headless Service 二、Kubernetes的服務發現 2.1 環境變量方式 2.2 DNS方式 Kubernetes 中 Service 是 將運行在一個或一組 Pod 上的應用…

在 Python 中實現觀察者模式的具體步驟是什么?

在 Python 中實現觀察者模式可以遵循以下具體步驟&#xff0c;這些步驟清晰地劃分了角色和交互流程&#xff1a; 步驟 1&#xff1a;定義主題&#xff08;Subject&#xff09;基類 主題是被觀察的對象&#xff0c;負責管理觀察者和發送通知。需實現以下核心方法&#xff1a; 存…

分布式方案 一 分布式鎖的四大實現方式

Java分布式鎖實現方式詳解 什么是分布式鎖 基于數據庫的分布式鎖基于Redis的分布式鎖基于ZooKeeper的分布式鎖基于Etcd的分布式鎖 各種實現方式對比最佳實踐建議多節點/線程調用結果展示 基于數據庫的分布式鎖 - 多線程測試基于Redis的分布式鎖 - 多節點測試基于ZooKeeper的分…