学习vue框架

通过一个小项目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
![在这里插入图片描述](https://img-blog.csdnimg.cn/8680e468daff42a3a545155aa0e13abb.png

import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap/dist/js/bootstrap';

实现导航栏的过程

app.vue中不但可以当做是网站首页,也可以写所有页面中公共需要的动画或者样式。不在上面写代码也可以。app.vue是主组件,是页面入口文件,是vue页面资源的首加载项。所有的页面都是在app.vue中进行切换的。可以理解为所有的路由都是app.vue的子组件。
在NavBar.vue中写好了tamplate中的部分后,由于NavBar每个界面都需要用,我们将其放在app.vue中,具体实现方法如下:

第一步:在script中将NavBar export出去。

在这里插入图片描述

<script>
export default {
    name :"NavBar",
}
</script >
第二步,在App.vue中引入Navbar.vue

要写在template中的组件需要放在components中

<script>
import NavBar from './components/NavBar';

export default {
  name :"App",
  components:{
    NavBar
  },
}

</script>

在这里插入图片描述

第三步,将引入的NavBar组件写入template部分中
<template>
  <NavBar />
</template>

这时候看前端界面,就已经显示出了导航栏
在这里插入图片描述
vue中main.js、index.html、App.vue中的app是什么关系?
在这里插入图片描述参考资料1
参考资料2
参考资料3
bootstrap中有两种container,分别是container-fluid 和container,container会更靠中间一点。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述直接写页面可能会发现比较丑,我们可以使用bootstrap 中的一个cards组件把它括起来。

   <div class="card">
      <div class="card-body">
         <slot></slot>
      </div>
     </div>  

如果发现太靠左同样可以使用一个container把这个组件括起来。
container是bootstrap中实现好的一个类,可以使得我们响应式的调整中间内容区域的大小大小。

 <div class="container">
      <div class="card">
      <div class="card-body">
         <slot></slot>
      </div>
     </div>  
    </div>

小技巧:在vscode中只需要写 div.container 再按回车可以自动生成下面的代码:

 <div class="container">
   </div>

调整与顶部的距离


<style scoped>
  /* 调整与顶部的距离 */
  .container{
    margin-top: 20px;
  }

</style>

当多个组件都用到同样一个公共的内容时,我们应该用一个单独的文件把这个文件抽象出来,这样后面如果需要调整样式的话会比较容易调整。
例如我们抽象出来一个ContentBase.vue

<template>
    <div class="home">
    <!-- container是一个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中引入这个公共组件

<script>
// @ is an alias to /src
import ContentBase from '../components/ContentBase'

export default {
  name: 'HomeView',
  //在上面的template里面会用到哪些其他的组件
  components: {
    ContentBase,

  }
}
</script>

在HomeView.vue中的template部分中使用这个组件

<template>
   <!--Content是一个共用组件,放到外面components/ContentBase下面-->
    <ContentBase>
      首页
    </ContentBase>
</template>

注意到这里ContentBase组件里面是有**“首页”**这两个字的,如何实现通过组件传递信息呢?
可以看到在ContentBase.vue中应用了这个标签,相当于一个占位符。
vue中插槽(slot)的使用
在这里插入图片描述

如何实现访问不同的地址可以得到不同的界面呢?

通过在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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值