通过一个小项目myspace学习vue框架
由于vue概念比较多,但都不是很难理解,所以yxc老师采用了项目驱动的讲法,这里记录一下学习这个项目过程中的vue的一个整理。
项目github地址 欢迎收藏
配置环境
终端
linux和mac上可以用自带的终端,Windows上使用powershell或者cmd,Git Bash有些指令不兼容。
安装nodejs
安装vue
安装vue router和vuex插件
需要安装router和vuex两个插件
这个页面是一个多页面的应用,是需要做路由的,因此需要使用router。
vuex可以使得我们在多个组件之间维护同一个数据。
vue框架的结构概述
views写各种界面
router:初始状态下有两个路由,/和about
components:存储组件
main.js 入口,整个组件挂在到app元素上。
注:后端渲染与前端渲染:
后端渲染:每打开一个页面,服务器发送请求并且返回回来
前端渲染;只有在第一次打开(无论是什么页面),服务器将所有元素返回,同时打包在js文件中,当打开第二个或第三个等页面后,用返回的js文件直接将新页面渲染出来
vue的基本概念
1.vue的文件组成部分
html js css
2.vue的特点
1)将所有的东西全部放在一起,同一个页面会包含所有的东西
css可以加scope特性:添加此特性,每个选择器在返回给前端的时候会有一个随机值,每个组件的随机值是不一样的,这样不同组件之间的css选择器就不会影响到
2).组件化框架
一个页面可能会有不同的部分,每一个部分都可以用一个单独的组件来实现
引入方式:输入如下代码:
myspace项目
整个vue项目的入口在main.js文件中,main.js文件中的代码如下:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
//store就是vuex
createApp(App).use(store).use(router).mount('#app')
最后将整个app挂载到app整个元素上,app整个元素在public文件夹下的index.html文件下:
vue是一个前端渲染框架
前端渲染框架与后端渲染框架
后端渲染框架每次打开一个新的页面client都会向server发送一次请求。
前端渲染框架第一次打开页面时发送请求(第一次无论打开什么页面),服务端将整个js文件全部返回回来,后面在打开别的页面的时候,直接由返回的js渲染就可以了,不用再去向服务端发送请求。
开发项目的时候,我们一般秉承从上到下的思路去开发,如果从下往上开发的话,可能会需要反复的重构代码。
一般原来写前端是html一个地方,css一个地方,js一个地方,而vue是将这三个统一在了一个页面中。
vue中每个界面都是一个.vue文件,.vue文件由三个部分组成。分别是html部分,放在template中,js部分,放在script中,还有css部分,放在style中。
style加了scoped属性后,不同组件之间的css选择器就不会影响到了。添加此特性,每个选择器在返回给前端的时候会有一个随机值,每个组件的随机值是不一样的,这样不同组件之间的css选择器就不会影响到。在写css样式的时候,不需要考虑不同组件是不是会相互影响到。
myspace项目整体设计
由于导航栏这个组件每一个界面都会用到,所以我们将导航栏这个组件抽象出来,可以在components这个文件夹下面创建一个Navbar.vue文件。
NavBar.vue代码
<template>//里面写导航栏的html
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<router-link a 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',params:{userId:2}}">用户动态</router-link>
</li> -->
</ul>
<ul class="navbar-nav" v-if="!$store.state.user.is_login">
<!--访问store是使用$store 就一直按着一层一层的路径去访问就可以了-->
<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>
<ul class="navbar-nav" v-else>
<li class="nav-item">
<router-link
class="nav-link"
:to="{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" @click="logout">退出</a>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
import { useStore } from 'vuex';
export default {
name :"NavBar",
setup(){
const store = useStore();
const logout = () =>{
store.commit('logout');//调用mutations里面的api的话,使用的是commit,调用的是actions里面的haul,使用的是dispatch
};
return {
logout,
}
}
}
</script >
使用bootstrap bootstrap地址
bootstrap可以帮助程序员达到一个美工的效果
在App.vue中引入bootstrap
的使用
如何实现访问不同的地址可以得到不同的界面呢?
通过在router文件夹下的index.js文件里加路由的方式实现,具体如下面的代码:
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
//首先需要把所有的组件都引入进来
import UserListView from '../views/UserListView.vue'
import UserProfileView from '../views/UserProfileView.vue'
import LoginView from '../views/LoginView.vue'
import RegisterView from '../views/RegisterView.vue'
import NotFoundView from '../views/NotFoundView.vue'
//第二部模仿这homeview写就行了
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/userlist/',
name: 'userlist',
component: UserListView//这个名字和上面的引入后起的别名的名字一样
},
{
// 加一个冒号说明是一个参数
path: '/userprofile/:userId/',
name: 'userprofile',
component: UserProfileView
},
{
path: '/login/',
name: 'login',
component: LoginView
},
{
path: '/register/',
name: 'register',
component: RegisterView
},
{
path: '/404/',
name: '404',
component: NotFoundView
},
{
// 当前面的都无法匹配的话,下面正则表达式表示可以匹配任意字符串
path:'/:catchAll(.*)',
redirect:"/404/"
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
学vue关键还是得写熟练,写项目80%用到的其实只是其中80%的知识点,我们只需要将这80%的知识点写熟练,剩下的变写变查就行了。
实现点击导航栏的每一个部分,跳到对应页面的功能。
使用router-link 和 :to 方法来实现
<template>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<router-link a 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',params:{userId:2}}">用户动态</router-link>
</li> -->
</ul>
<ul class="navbar-nav" v-if="!$store.state.user.is_login">
<!--访问store是使用$store 就一直按着一层一层的路径去访问就可以了-->
<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>
<ul class="navbar-nav" v-else>
<li class="nav-item">
<router-link
class="nav-link"
:to="{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" @click="logout">退出</a>
</li>
</ul>
</div>
</div>
</nav>
</template>
<!-- 加完scoped之后我们在这里写的所有的css选择器不会影响其他的组件 -->
<script>
import { useStore } from 'vuex';
export default {
name :"NavBar",
setup(){
const store = useStore();
const logout = () =>{
store.commit('logout');//调用mutations里面的api的话,使用的是commit,调用的是actions里面的haul,使用的是dispatch
};
return {
logout,
}
}
}
</script >
实现用户动态界面
可以看到这个界面比较复杂,我们使用三个组件来分别实现。
对于上面的三个模块,三个组件的命名如下:
- 个人信息模块 UserProfileInfo.vue
- 发帖区 UserProfileWrite.vue
- 帖子展示区 UserProfilePosts.vue
页面布局采用bootstrap的grid系统。
bootstrap的grid系统主要是外部是一个container,里面使用的是row和col。“row-x” row和col后面接的数字表示分开所占的份数。
图片如果太大或者太小,bootstrap里面提供了响应式的自适应图片大小的方法,在class后面加上"img-field"。将图片设置为圆形的方法:将border-radius设置为50%。
button控制大小的按钮class = "btn-lg"大图标 class = "btn-sm"小图标。
UserProfileInfo.vue
<template>
<!--card card-body这两个类是一个卡片-->
<div class="card">
<div class="card-body">//这行和上面一行代码表示用一个卡片将里面的内容括起来,这样会比较容易调节样式
<div class="row">
<div class="col-3 img-field">
<img :src ="user.photo" class="img-fluid">
<!--src前面加上冒号表示后面的是一个变量user.photo是一个变量而不是一个字符串-->
</div>
<div class="col-9">
<!--fullName是33行的fullName-->
<div class="username">{{user.username}}</div>
<div class="fans">粉丝:{{user.followerCount}}</div>
<button @click="follow" v-if="!user.is_followed" type="button" class="btn btn-secondary btn-sm">+关注</button>
<button @click="unfollow" v-if="user.is_followed" type="button" class="btn btn-secondary btn-sm">取消关注</button>
</div>
</div>
</div>
</div>
</template>
<script>
// import { computed } from 'vue';
import $ from 'jquery';
import { useStore } from 'vuex'
export default {
name : "UserProfileInfo",
props:{
user:{
type:Object,
required:true,
}
},
setup(props,context){
// let fullName = computed (() => props.user.lastname+ ' '+ props.user.firstname);
const store = useStore();
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,
//注意区别一下authorization和authorication的区别
},
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%;
/* max-width: 100%;
height: auto; */
}
.username{
font-weight: bold;
}
.fans{
font-size: 12px;
color:gray;
}
button{
padding:2px 4px;//上下是2个像素,左右是4个像素。
font-size: 12px;
}
.img-field{
display:flex;
flex-direction:column;
justify-content: center;
}
</style>
帖子列表模块
使用这个代码"div.card-div.card-body"生成一个卡片将内部括起来。
userProfilePosts.vue
<template>
<div class="card">
<div class="card-body"></div>
<!--每一个循环一定要加上一个key属性,保证每一个key属性不一样,这是vue内部的机制,平时我们用不到-->
<div v-for = "post in posts.posts" :key="post.id">
<div class="card single-post">
<div class="car-body">
{{post.content}}
<button @click="delete_a_post(post.id)" v-if = "is_me" type="button" class="btn btn-danger btn-sm">删除</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { computed } from 'vue'
import { useStore } from 'vuex'
import $ from 'jquery'
export default{
name:"UserProfilePosts",
props:{
posts:{
type: Object,
required: true,
},
user: {
type: Object,
required: true,
}
},
setup(props,context){
const store = useStore();
let is_me = computed(() => store.state.user.id === props.user.id);
const delete_a_post = post_id =>{
$.ajax({
url:"https://app165.acapp.acwing.com.cn/myspace/post/",
type:"DELETE",
data:{
post_id,
},
headers:{
'Authorization':"Bearer " + store.state.user.access,
},
success(resp) {
if(resp.result === "success"){
context.emit('delete_a_post',post_id);
}
}
})
}
return {
is_me,
delete_a_post
}
}
}
</script>
<style scoped>
.single-post{
margin-bottom:10px;
}
button{
float:right
}
</style>
父组件向子组件传递消息
目前的结构是UserProfileView.vue组件中使用到了UserProfileInfo.vue,UserProfilePosts.vue,UserProfileWrite.vue组件,这三个组件是要交互的,我们一般把数据存储到最顶层的组件里面,也就是存储到userProfileView.vue里面。
这里存储数据我们使用setup函数来实现存储数据,在setup函数里面定义一堆变量。
在setup里面定义的变量,未来如果需要在模板里面用到,那么需要return出去,类似下面这样。
上面这个是我们在UserProfileView.vue这个父文件中定义出来的变量,那么我们如何在子组件UserProfileInfo.vue这个文件中使用这个user变量呢?
v-bind:user = “user” 可以简写为 :user = "user"这样。
<template>
<ContentBase>
<!--这里看下视频是有比较容易的写法写下面的这些东西-->
<div class="row">
<div class="col-3">
<!--跨组件传递消息-->
<UserProfileInfo @follow="follow" @unfollow="unfollow" v-bind:user="user"/>
<UserProfileWrite v-if = "is_me" @post_a_post = "post_a_post"/>
</div>
<div class="col-9">
<UserProfilePosts :user="user" :posts = "posts" @delete_a_post = "delete_a_post"/>
</div>
</div>
</ContentBase>
</template>
在userProfileInfo.vue的script部分的props里面将user属性申明一下,就可以直接使用user这个变量了。
<script>
// import { computed } from 'vue';
export default {
name : "UserProfileInfo",
props:{
user:{
type:Object,# Object记得大写
required:true,
}
},
使用computed动态计算属性
vue中想要动态计算某一个属性的值的话,使用computed属性。
1.需要先在script中从vue中引入computed
import { computed } from 'vue';
2.在setup函数中 使用computed函数计算,computed函数中传入的参数是一个匿名函数,注意其写法。vue中没有this这个概念,所以props这个参数是必须要传进来的。
setup(props,context){
let fullName = computed (() => props.user.lastname+ ' '+ props.user.firstname);
return//注意要想在template中使用的话需要return出去。
{
fullname
}
}
接着就可以在template模块中使用fullname这个变量了。
实现关注按钮
基本逻辑:打开一个页面的时候,我们需要先判断一下是否关注这个用户,如果关注了我们就可以取消关注,如果没有关注的话,我们可以点一个关注。
那么就需要我们使用一个变量来判断一下我们是否关注,vue中使用v-if来实现判断的逻辑。
<button @click="follow" v-if="!user.is_followed" type="button" class="btn btn-secondary btn-sm">+关注</button>
<button @click="unfollow" v-if="user.is_followed" type="button" class="btn btn-secondary btn-sm">取消关注</button>
button绑定事件
我们按完关注这个按钮之后,需要更新一下关注的状态,所以这里需要给button绑定一个事件。
可以使用v-on:click = "follow"或者 v-on:click = “unfollow”。其中v-on可以简写为@
@click = "follow"函数来关注,@click = "unfollow"函数来取消关注。
其中follow函数和unfollow函数定义在setup函数里面,定义完之后需要在后面的return中将follow名称和unfollow名称返回。
这里使用了ajax技术,ajax技术后面再细讲。
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,
//注意区别一下authorization和authorication的区别
},
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
}
子组件向父组件传递信息
我们想要修改状态,这个状态定义在了user,而user来自父组件,所以要在父组件中修改。(哪里定义的在哪里修改)。所以这里需要子组件向父组件传递信息,子组件向父组件传递信息,我们采用绑定事件的方法。
第一步需要现在父组件中写好修改父组件的函数
例如在userProfilleView.vue中绑定follow和unfollow两个函数。context.emit(“函数名”);
<template>
<UserProfileInfo @follow="follow" @unfollow="unfollow" v-bind:user="user"/>
</template>
<script>
const follow= () =>{
if(user.is_followed)return;
user.is_followed = true;
user.followersCount ++ ;
}
const unfollow = () =>{
if(!user.is_followed)return;
user.is_followed = false;
user.followersCount -- ;
}
</script>
第二步:在子组件userProfileInfo.vue中调用父组件中的函数。
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,
//注意区别一下authorization和authorication的区别
},
success(resp){
if(resp.result === "success")
{
context.emit('follow');//触发父组件里面的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");//触发父组件里面的unfollow函数
}
}
})
}
vue中循环使用v-for,注意必须要写上key,每一个循环的key都是不一样的。这个key我们是用不到的,可以理解为vue内部用来做优化的。
<div v-for = "post in posts.posts" :key="post.id">
<div class="card single-post">
<div class="car-body">
{{post.content}}
<button @click="delete_a_post(post.id)" v-if = "is_me" type="button" class="btn btn-danger btn-sm">删除</button>
</div>
</div>
</div>
vue中如何获取textarea中的内容
需要用到setup()变量,来获取content的值,用到属性v-model:让某个标签内容和context内容相互绑定。
使用v-model实现一个前端元素中的值与一个值的绑定。
实现发送框,userProfileWrite.vue中的代码
<template>
<div class="card edit-field">
<div class="card-body">
<label for="edit-post" class="form-label">编辑</label>
<!--将textarea里面的内容和content的值绑定起来-->
<textarea v-model = "content" class="form-control" id="edit-post" rows="3"></textarea>
<button @click="post_a_post" type="button" class="btn btn-primary btn-sm">发帖</button>
</div>
</div>
</template>
<script>
import { ref } from 'vue';
import $ from 'jquery'
import { useStore } from 'vuex';
export default {
name :"UserProfileWrite",
setup(props,context){
const store = useStore();
let content = ref('');
const post_a_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"){
//注意这里要想取得ref定义的content的值话,一定要使用value
context.emit('post_a_post',content.value)
// console.log(content.value);
}
}
})
}
return {
content,
post_a_post,
}
}
}
</script>
<style scoped>
.edit-field {
margin-top:20px;
}
button{
margin-top:10px;
}
</style>
在数组最前面加元素的api unshift
reactive值的变量的含义:当reactive里面的变量变换的时候,会修改所有的引用该变量的变量。
从点击发帖按钮到实现右边框最新的帖子排在最前面的过程
userProfileWrite.vue中
1.发帖的button绑定了当前页面下面setup函数中的post_a_post函数
<button @click="post_a_post" type="button" class="btn btn-primary btn-sm">发帖</button>
2.当前页面的post_post函数会触发父组件里面的修改函数(为了方便,父组件里的修改函数我们也定义为post_a_post)
context.emit('post_a_post',content.value)
userProfileView.vue
这里UserProfileWrite组件绑定了userProfileView页面下的post_a_post函数
<UserProfileWrite v-if = "is_me" @post_a_post = "post_a_post"/>
这个页面下的post_a_post函数会修改posts变量
//其中posts是一个reactive变量,修改了之后所有引用posts变量的地方都会被立即修改并渲染。
const posts = reactive({
})
const post_a_post = (content)=>{
posts.count++;
//在数组最前面加元素的api
posts.posts.unshift({
id:posts.count,
userId:1,
content:content,
})
}
上面那里修改了posts变量后,下面这里的代码就会立即渲染出来变化后的界面。
<UserProfilePosts :user="user" :posts = "posts" @delete_a_post = "delete_a_post"/>
可以在vscode搜索栏目直接搜索post_a_post,找出所有引用了post_a_post的地方就可以把发送帖子的逻辑弄明白。
云端API调用以实现功能
好友列表界面
前言:从这里开始,本项目正式开始使用API,从云端将用户进行获取。
接下来需要调用后端提供的接口,需要用到ajax技术,这里需要用到jquerry,安装jquery的方法:
API地址
通过GET方法获取API,访问JSON信息,用rest framework与jwt 验证进行后端验证,用ajax实现数据的获取。首先使用下卖弄的方式装上jquery,引入$对象。
ajax的一个特点是同一个链接但是对应不同的方法对应不同的函数。
npm i jquery
引入ajax的$
import $ from 'jquery'
这里调用的后端是已经实现好的,采用django rest framework实现的。
axios方法也是类似ajax的一种方法来获取后端列表。
使用ajax写法调用后端返回userlist的方法如下:
$.ajax({
url:'https://app165.acapp.acwing.com.cn/myspace/userlist/',
type :"get",
success(resp){
//如果想要修改一个ref类型的变量,需要修改它的.value类型
users.value = resp;
}
});
如果想让冒号里面的东西不是一个字符串而是一个变量的话,那么我们在这个变量名称前面加上一个冒号就可以。
例如下面的key里面的user.id是一个变量,所以需要在key前面加上一个冒号。
<div class="card" v-for = "user in users" :key="user.id" @click="open_user_profile(user.id)">
鼠标放上去之后展现一个小手形状的css方法:
cursor:pointer
鼠标悬浮加上阴影效果
.card:hover{
box-shadow: 2px 2px 10px lightgrey;
/* 偏移量朝右朝下都是2像素,扩散十像素,扩散颜色是lg */
transform: 500ms;//调整阴影反映的周期
}
点击一个用户之后,链接后面加上ID,修改路由改成以下格式:
path: '/userprofile/:userId/',
name: 'userprofile',
component: UserProfileView
(建议路由链接一般用/来做结尾)UseRoute来访问参数。
利用正则表达式实现404界面
如果每一个界面的地址都没有满足的话(404界面的地址一般放在最后,执行到这一步表示前面所有的地址都没有满足),这里无论什么地址都应该满足404界面。
{
// 当前面的都无法匹配的话,下面正则表达式表示可以匹配任意字符串
path:'/:catchAll(.*)',
redirect:"/404/"
}
每一个人账号打开的界面应该是不一样的,而且有些界面也应该是登录之后才有权限看,所以这里在userProfile后面加上userid这个参数。
{
// 加一个冒号说明是一个参数
path: '/userprofile/:userId/',
name: 'userprofile',
component: UserProfileView
},
需要在userProfileView.vue界面中引入
import { useRoute } from 'vue-router';
NavBar.vue中导航栏中用户的id的参数
<ul class="navbar-nav" v-else>
<li class="nav-item">
<router-link
class="nav-link"
:to="{name:'userprofile',params:{userId: $store.state.user.id}}"
>{{$store.state.user.username}}
</router-link>
个人用户列表的个人判断
只有当登录之后,才能打开该页面,通过下面的语句,来决定登录。
const userId = parseInt(route.params.userId);
实现登录页面
动态获取用户名和密码
需要对两个变量username和password进行双向绑定,需要用到v-model,同时,发出错误信息也需要响应式变量(但不需要双向绑定)。
作为一个表单,需要手写一下提交操作。
const login =()=>{
console.log(username.value,password.value);
注意在访问变量时,需要加下.value
当提交在页面层输出时,会闪过又刷新一下信息:原因在于表单只绑定了一个提交前的事件,执行完之后依旧要执行提交后的事件,阻止方式:
@submit.prevent
实现真正登录
以上这些登录只是伪登录,前端页面的登录,要想实现真正的登录需要和后端进行全局交互才可,由于很多页面都需要使用用户信息,因此需要使用全局变量。
首先去bootstrap界面找一个登录的表单。
如果想登录的话,我们需要动态获取我们的用户名和密码,这里需要一个双向绑定的变量来实现。
注意:访问ref值的时候,需要加上.value
@submit.prevent 表示阻止掉它默认的行为。
looginView.vue
<template>
<ContentBase>
<div class="row justify-content-md-center">//居中
<div class="col-3">
<!-- submit.prevent中的prevent可以去掉是另一种情况 -->
<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>
// @ is an alias to /src
import ContentBase from '../components/ContentBase.vue'
// 响应式变量使用ref或者reactive
import { ref } from 'vue';//响应式变量
import { useStore } from 'vuex'
import router from '@/router/index'
//@这个符号一般默认定义到src目录
export default {
name: 'LoginView',
//在上面的template里面会用到哪些其他的组件
components: {
ContentBase,
},
setup(){
const store = useStore();
let username = ref('');
let password = ref('');
let error_message = ref('');
const login = () => {//提交之后的事件
error_message.value="";
// console.log(username.value,password.value);
store.dispatch("login",{
username:username.value,
password:password.value,
success(){
router.push({name:'userlist'});
},
error(){
error_message.value = "用户名或密码错误";
}
});
}
return {
username:username,//可以简写为 username
password:password,
error_message,
login
}
}
}
</script>
<style scoped>
button{
width:100%;
}
.error-message{
color:red;
}
</style>
在登录之后,可以看到要展示很多的个人信息在界面上,这时候可能需要我们来维护一些全局的信息,vue中有一个维护全局信息的方法:vuex。
vuex维护了一个状态树,以前加入两个组件需要通过父子关系传递信息的方式,现在只需要这两个组件都和vuex交互就可以得到维护的信息。
在store文件夹下面的index.js里面,可以发现这就是vuex创建的全局唯一的对象。vuex5个属性描述参考
一个完整的复杂的修改,我们应该放到action里面。而对于state的直接修改,全部放到mutations里面。
这里面有5个属性
import { createStore } from 'vuex'
import ModuleUser from './user'
// vuex,vue中实现一个去全局变量,有利于平行组件之间的通信
export default createStore({
// state存储所有数据
state: {
},
//获取state中的内容,但是不能直接获取,需要一些计算才能获取的时候
getters: {
},
//唯一的可以对state中的内容进行修改的
//mutations不能执行异步操作
mutations: {
},
//可以定义我们对state的各种操作,例如更新方式等
actions: {
},
//将state进行分割
modules: {
user:ModuleUser,
}
})
每一个modules里面的一个对象,它其实是维护state里面的一个对象,维护的时候,同样是具有getters,mutations,和actions属性。
例如在user.js中直接定义一个Moduleuser,然后在index.js中引入它:
user.js
import $ from 'jquery';
import jwt_decode from 'jwt-decode'
const ModuleUser = {
state:{
id:"",
username:"",
photo:"",
followerCount:0,
access:"",
refresh:"",
is_login:false,
},
getters:{
},
//mutations里面不支持异步调用,简单区分:凡是访问链接的
//就是异步,不访问链接的就是同步。
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.id="";
state.username="";
state.photo ="";
state.followerCount=0;
state.access="";
state.refresh="";
state.is_login=false;
}
},
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);
const {access,refresh} = resp;
const access_obj = jwt_decode(access);
// console.log(refresh)
//这里每五分钟直接调用一次
setInterval(()=>{
$.ajax({
url:"https://app165.acapp.acwing.com.cn/api/token/refresh/",
type:"POST",
data:{
refresh,
},
success(resp){
context.commit('updateAccess',resp.access);
}
});
},4.5*60*1000);
//这里本来是打算每5分钟访问一次,但是为了避免5分钟的时候刚好过期了,改成4.5分钟访问一次。
$.ajax({
url:"https://app165.acapp.acwing.com.cn/myspace/getinfo/",
type:"GET",
data:{
user_id:access_obj.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;
index.js
import { createStore } from 'vuex'
import ModuleUser from './user'
// vuex,vue中实现一个去全局变量,有利于平行组件之间的通信
export default createStore({
// state存储所有数据
state: {
},
//获取state中的内容,但是不能直接获取,需要一些计算才能获取的时候
getters: {
},
//唯一的可以对state中的内容进行修改的
//mutations不能执行异步操作
mutations: {
},
//可以定义我们对state的各种操作,例如更新方式等
actions: {
},
//将state进行分割
modules: {
user:ModuleUser,
}
})
后面如果想访问user的username的话,可以这么访问:
store.state.user.username
登录方式的演进
传统的登录方式,用户通过用户名和密码访问server,server会返回一个session_id,同时server会将这个session_id存储到数据库里面,后面用户每次访问的时候带上这个session_id。server会从服务器中读出来这个session_id代表的是哪一个用户。参考下图理解。
旧的登录方式如何判断登录:使用sesson_id,一般存在cookie,js不能访问到,当跨域访问时,很难维护登录状态,新的登录方式如下图:
jwt认证,又称JSON WEB Token JWT不会存在服务器中,但是可以验证,用户信息存在JWT里。里面有项内容为userId,验证的方法为:
这样保证了加密是安全的
真验证的编写
使用JWT验证,有两个返回值:
refresh:获取新令牌,post方法在http body,更加安全。access:令牌JWT,直接获取认证。
在user.js写下登录的action,是一个ajax,当外部要去调用文件的action的某个名字时,需要用到dispatch。
同样是客户端使用用户名和密码登录server,然后server会返回一个jwt验证,但是服务器并不会存储这个jwt,后面客户端每请求服务器的时候,会携带这个jwt,服务端这边会有例如加密解密的方式来验证这个jwt。
可以使用base64解析网址来将字符串进行一个base64解析,base64解析的好处是可以把一些特殊字符替换掉。
登录实际上是 客户端用用户名和密码登录服务器,返回一个access和refresh,只要有这个access,就表示我们一直处于登录状态。refresh每5分钟刷新一次,让我们获得一个新的access。
由于user_id编码在了jwt里面,这里要解码的话,我们需要装一个包,在terminal里面执行:
npm i jwt-decode
接着在user.js中引入这个包
import jwt_decode from 'jwt-decode'
使用jwt验证的代码写法在上面的user.js里面有写,记住这种写法就好。
异步:比如我们访问一个网址,访问这个网址可能会需要我们等待一段时间,在等待的这段时间里可能会执行其他的函数,这种可能会更改逻辑的访问顺序就是异步访问。
由于在actions里面不能直接修改state,在mutations里面不支持异步,所以在user.js里面,我们在mutations里面写了几个更新state的函数,然后在actions里面调用。
使用router来实现界面的跳转,在loginView.vue中在登录函数的回调success函数中,使用
router.push({name:‘userlist’)这个值和index.js中的路径的值是一样的。
获取用户名之后:
获取到用户名之后,需要将信息更新到state里面,action不能直接更新,需要在mutations下更新,当mutations创建完,用context.commmit调用,access由于每5分钟过期一次,因此需要定期更新。成功后,跳转到首页
LoginView部分代码的最终展示如下:
所有存在store里面的值,我们都是可以使用$直接访问的,例如在展示登录后,原登录按钮那里显示登录的用户名的逻辑里面来访问is_login这个变量的访问顺序。
实现退出逻辑
登录的话,登录状态本质上是获取了这个JWT,退出的话,将jwt删除就可以,不用通知服务器。在user.js中logout函数中,将state各个属性赋空值就可以。
logout(state){
state.id="";
state.username="";
state.photo ="";
state.followerCount=0;
state.access="";
state.refresh="";
state.is_login=false;
}
实现用户动态页面
无论是个人还是用户的页面,默认当个人用户登录才能点开,如果没有则直接跳转到登录页面。
在UserListView中写入参数open_user_profile,对于登录与不登录的两种情况,进行分别的判断与跳转。
注:由于在vue后台可以直接判断逻辑,对函数进行封装,来分辨是定义还是函数,因此函数传参数,直接加一个括号即可。
open_user_profile(user,id)
该页面必须是用户登录之后才有权限打卡,如果用户未登录就打开了这个页面,会让用户跳转到登录界面。逻辑实现在下面这个函数中。
const open_user_profile = userId =>{
if(store.state.user.is_login){
router.push({
name:"userprofile",
params:{
userId
}
})
}else{
router.push({
name:"login"
});
}
}
实现从云端拉取某个用户的信息,填充在用户信息界面
在前面写静态页面时,当前页面时写死的,因此无论目前点击任何用户都只会显示自己,现在要显示全部的用户的全部的页面信息,需要从云端拉下来,拉的信息包括:用户信息,历史帖子。
在UserPorfileView的文件中,用ajax获取api。
$.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){
user.id = resp.id;
user.username = resp.username;
user.photo = resp.photo;
user.followerCount = resp.followerCount;
user.is_followed = resp.is_followed;
//is_followed表示当前已经登录的用户是否关注了当前我们正在看的用户。
// console.log(resp);
}
})
将头像竖直方向居中。使用flex,将flex的主轴变成竖方向。
.img-field{
display:flex;//使用flex居中
flex-direction:column;//将flex的主轴变为竖向
justify-content: center;
}
获取每个用户发过的文章
使用ajax技术异步获取,然后将得到的内容存在posts.posts这个数组中。
$.ajax({
url:"https://app165.acapp.acwing.com.cn/myspace/post/",
type:"GET",
data:{
user_id :userId,
},
headers:{
'Authorization': "Bearer " + store.state.user.access,
},
success(resp){
posts.count = resp.length;
posts.posts = resp;
}
});
发布帖子
一定是只有自己的页面才可以添加,需要判断登录页面与当前页面是否是自己:
const is_me=computed(()=>userId===store.state.user.id)
<UserProfileWrite v-if="is_me" @post_a_post="post_a_post"/>
用v-if判断即可,记得注意数据类型,应该强制转换为int
出现的问题是:当打开他人的userProfile时,点击自己左上角没有反应。
原因在于:判断连接是否相同时,不包含参数判断。
在APP.vue的 里添加:key = "router fullpath"用完整路径来判重。
添加操作
引入一个api,加headers进行验证,更新有两种操作,在后端返回结果之后,再在前端更新,也可以在前端更新,后者可以让用户操作更加流畅,如果提交有bug,可能会有数据不一致发生。
实现删除帖子功能
众所周知:删除不是所有的时候都能删,比如删除别人的内容,也就是说,删除和修改只有在自己的页面才可以操作。修改在UserProfilePosts中,加上辅助计算属性is_me,同时,判断是不是自己,需要找到当前用户是谁,记得导入UseStore
const delete_a_post = post_id =>{
posts.posts = posts.posts.filter(post => post.id !== post_id);//将里面每条帖子不等于要删除帖子id的帖子都保存下来
posts.count = posts.posts.length;
}
const delete_a_post = post_id =>{
$.ajax({
url:"https://app165.acapp.acwing.com.cn/myspace/post/",
type:"DELETE",
data:{
post_id,
},
headers:{
'Authorization':"Bearer " + store.state.user.access,
},
success(resp) {
if(resp.result === "success"){
context.emit('delete_a_post',post_id);
}
}
})
}
实现注册功能
类似于登录功能,调用api,此api会返回如下信息:
success
用户名不能为空
两个密码不一致
用户名已存在
密码不存在
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(){
router.push({name:'userlist'});
},
error(){
error_message.value = "系统异常,请稍后重试";
}
});
}
else
{
error_message.value = resp.result;
}
// console.log(resp);
}
})
将关注逻辑存到数据库上实现持久化
服务器的api并没有区分取消关注还是添加关注,而是一个变换,双向的变换:
关注→取消
取消→关注
同样的也是调用api,用authorication进行授权,授权完成后,前端页面渲染关注操作,展示关注成功的渲染代码。
使用ajax返回的结果来判断是否关注,如果关注了就取消关注,如果没关注,就添加关注。
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,
//注意区别一下authorization和authorication的区别
},
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");
}
}
})
}
区别一下 认证(authentication)和授权(authorization)的区别:
以前一直分不清楚authentication和authorization,其实很简单,举个例子来说:
你要登机,你需要出示你的身份证和机票,身份证是为了证明你张三确实是你张三,这就是authentication;而机票是为了证明你张三确实买了票可以上飞机,这就是authorization。
也就是说张三上飞机分为两部确认:
第一个确认你是张三(认证) 第二个确认张三买了飞机票可以上飞机,有上飞机的权利(授权)。
在计算机领域的例子:
你要登录一个论坛,输入用户名张三,密码1234,密码正确,证明张三确实是张三,这就是authentication;另外张三还是一个版面的版主,所以有权利修改帖子,这就是authorization。
将项目部署到云服务器上
本项目的api的地址和项目的部署地址是完全没有任何关系的。
部署过程待更
参考博客
博客1
国内用vue比较多,国外使用react比较多,这里好处是比较容易找到解决问题的方法,坏处是方法的质量一般都不怎么高–yxc。