Web学习(十一) Vue

Web学习(十一) Vue

1.配置环境

1.1 安装Nodejs

1.2 安装@vue/cli

npm i -g @vue/cli

1.3 启动vue自带的图形化项目管理界面

vue ui

常见问题1:Windows上运行vue,提示无法加载文件,表示用户权限不足。
解决方案:用管理员身份打开终端,输入set-ExecutionPolicy RemoteSigned,然后输入y

2.基本概念

script部分

export default对象的属性:

  • name:组件的名称
  • components:存储中用到的所有组件
  • props:存储父组件传递给子组件的数据
  • watch():当某个数据发生变化时触发
  • computed:动态计算某个数据
  • setup(props, context):初始化变量、函数
    • ref定义变量,可以用.value属性重新赋值
    • reactive定义对象,不可重新赋值
    • props存储父组件传递过来的数据
    • context.emit():触发父组件绑定的函数

template部分

  • <slot></slot>:存放父组件传过来的children。
  • v-on:click或@click属性:绑定事件
  • v-if、v-else、v-else-if属性:判断
  • v-for属性:循环,:key循环的每个元素需要有唯一的key
  • v-bind:或::绑定属性

style部分

  • <style>标签添加scope属性后,不同组件间的css不会相互影响。

第三方组件

  • view-router包:实现路由功能。
  • vuex:存储全局状态,全局唯一。
    • state: 存储所有数据,可以用modules属性划分成若干模块
    • getters:根据state中的值计算新的值
    • mutations:所有对state的修改操作都需要定义在这里,不支持异步,可以通过$store.commit()触发
    • actions:定义对state的复杂修改操作,支持异步,可以通过$store.dispatch()触发。注意不能直接修改state,只能通过mutations修改state。
    • modules:定义state的子模块

项目组成

  • views:用于写各个页面,每个页面对应一个 view,里面也可以写组件。 components:组件。
  • router:路由。
  • main.js:整个的入口,根组件,将根组件挂载到 index.html 里。
  • .vue:每个页面是一个 .vue 文件,是 vue 自定义的文件类型,每个 .vue 文件都有三部分组成,分别是 html,css 和
    js。每个 .vue 文件都会 export 一个对象。
    在这里插入图片描述

3.Vue3实战

安装插件后,启动项目
在这里插入图片描述
在输入栏找到运行地址后打开网页。
在这里插入图片描述

3.1 实现导航栏

在App.vue中添加需要导入的包

<template>
  <NavBar />
  <router-view/>
</template>

<script>
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap/dist/js/bootstrap';
import NavBar from './components/NavBar';

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

</script>

<style>
</style>

在component中添加NavBar.vue,html部分直接利用bootstrap:

<template>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <div class="container-fluid">
    <a class="navbar-brand" href="#">MySpace</a>
    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarNav">
      <ul class="navbar-nav">
        <li class="nav-item">
          <a class="nav-link active" aria-current="page" href="#">首页</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="#">好友列表</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="#">用户动态</a>
        </li>
        <li class="nav-item">
          <a class="nav-link disabled">登陆</a>
        </li>
        <li class="nav-item">
          <a class="nav-link disabled">注册</a>
        </li>
      </ul>
    </div>
  </div>
</nav>
</template>


<script>
export default {
    
    
    name: "NavBar",
}
</script>

<style scoped>

</style>

3.2 实现contentBase组件

由于多个页面存在相似的部分,所以外面在component中创建contentBase.vue,这样在其他页面直接调用countentBase组件即可:

<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>

在homeview.vue中使用contentBase:

<template>
  <CountentBase>
    首页
  </CountentBase>
</template>

<script>
import CountentBase from '@/components/CountentBase.vue'

export default {
    
    
  name: 'HomeView',
  components: {
    
    
    CountentBase
  }
}
</script>

3.3 编写LoginView.vue,notFoundView.vue,RegisterView.vue等,并添加路由

在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';

const routes = [
  {
    
    
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    
    
    path: '/userlist',
    name: 'userlist',
    component: UserListView
  },
  {
    
    
    path: '/userprofile',
    name: 'userprofile',
    component: UserProfileView
  },
  {
    
    
    path: '/login',
    name: 'login',
    component: LoginView
  },
  {
    
    
    path: '/register',
    name: 'register',
    component: RegisterView
  },
  {
    
    
    path: '/404',
    name: '404',
    component: NotFoundView
  },
]

const router = createRouter({
    
    
  history: createWebHistory(),
  routes
})

export default router

添加路由后已经可以按网址目录实现网页的跳转。
将NavBar中a标签全部换为router-link,在页面的NavBar上实现链接的跳转。

<template>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <div class="container">
    <router-link class="navbar-brand" :to="{name: 'home'}">Myspace</router-link>
    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarText">
      <ul class="navbar-nav me-auto mb-2 mb-lg-0">
        <li class="nav-item">
          <router-link class="nav-link" :to="{name: 'home'}">首页</router-link>
        </li>
        <li class="nav-item">
          <router-link class="nav-link" :to="{name: 'userlist'}">好友列表</router-link>
        </li>
        <li class="nav-item">
          <router-link class="nav-link" :to="{name: 'userprofile'}">用户动态</router-link>
        </li>
      </ul>
      <ul class="navbar-nav">
        <li class="nav-item">
          <router-link class="nav-link" :to="{name: 'login'}">登录</router-link>
        </li>
        <li class="nav-item">
          <router-link class="nav-link" :to="{name: 'register'}">注册</router-link>
        </li>
      </ul>
    </div>
  </div>
</nav>
</template>


<script>
export default {
    
    
    name: "NavBar",
}
</script>

<style scoped>

</style>

在这里插入图片描述

3.4 实现UserProfileInfo,UserProfilePosts与UserProfileWrite:

在这里插入图片描述
用户动态主页面UserProfileView.vue:

<template>
  <ContentBase>
      <div class="row">
        <div class="col-3">
          <UserProfileInfo @follow123="follow" @unfollow123="unfollow" :user="user" />
          <UserProfileWrite @post_a_post123="post_a_post" />
        </div>
        <div class="col-9">
          <UserProfilePosts :posts="posts" />
        </div>
      </div>
  </ContentBase>
</template>

<script>
import ContentBase from '../components/ContentBase'
import UserProfileInfo from '../components/UserProfileInfo';
import UserProfilePosts from '../components/UserProfilePosts';
import UserProfileWrite from '../components/UserProfileWrite';
import {
    
     reactive } from 'vue';

export default {
    
    
  name: 'UserList',
  components: {
    
    
      ContentBase,
      UserProfileInfo,
      UserProfilePosts,
      UserProfileWrite
  },

  // 初始化变量、函数
  setup() {
    
    
    const user = reactive({
    
    
      id: 1,
      username: "YanYuchen",
      lastName: "Yan",
      firstName: "Yuchen",
      followerCount: 0,
      is_followed: false,
    });

    const posts = reactive({
    
    
      count: 3,
      posts: [
        {
    
    
          id: 1,
          userId: 1,
          content: "今天上了web课真开心",
        },
        {
    
    
          id: 2,
          userId: 1,
          content: "今天上了算法课,更开心了",
        },
        {
    
    
          id: 3,
          userId: 1,
          content: "今天上了acwing ,开心极了",
        },
      ]
    });

    // 关注函数
    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 -- ;
    };

    //发送文本信息
    const post_a_post = (content) => {
    
    
      posts.count ++ ;
      posts.posts.unshift({
    
      //最前面添加元素
        id: posts.count,
        userId: 1,
        content: content,
      })
    };

    return {
    
    
      user,
      follow,
      unfollow,
      posts,
      post_a_post
    }
  }
}
</script>

<style scoped>
</style>

UserProfileInfo.vue:

<template>
    <div class="card">
        <div class="card-body">
            <div class="row">
                <div class="col-3">
                    <img class="img-fluid" src="https://cdn.acwing.com/media/user/profile/photo/29150_lg_1f2ac240fe.jpg" alt="">
                </div>
                <div class="col-9">
                    <div class="username">{
    
    {
    
     fullName }}</div>
                    <div class="fans">粉丝:{
    
    {
    
     user.followerCount }}</div>
                    <!-- v-if判断,没有关注的话就是关注按钮,已经关注的话就是取消关注按钮 -->
                    <!-- v-on:click或@click属性:绑定事件 -->
                    <button @click="follow1234" v-if="!user.is_followed" type="button" class="btn btn-secondary btn-sm">+关注</button>
                    <button @click="unfollow1234" v-if="user.is_followed" type="button" class="btn btn-secondary btn-sm">取消关注</button>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
import {
    
     computed } from 'vue';

export default {
    
    
    name: "UserProfileInfo",
    props: {
    
    
        user: {
    
    
            type: Object,
            required: true,
        },
    },

    // computed:动态计算某个数据
    // props:存储父组件传递给子组件的数据
    setup(props, context) {
    
    
        // 从UserProfileView中读取user数据
        let fullName = computed(() => props.user.lastName + ' ' + props.user.firstName);

        // 关注函数
        const follow1234 = () => {
    
    
            context.emit('follow123');   //context.emit():触发父组件绑定的函数
        };

        // 取消关注函数
        const unfollow1234 = () => {
    
    
            context.emit("unfollow123"); // 触发父组件UserProfileView中unfollow函数
        }

        return {
    
    
            fullName,
            follow1234,
            unfollow1234,
        }
    }
}
</script>


<style scoped>
img {
    
    
    border-radius: 50%;
}

.username {
    
    
    font-weight: bold;
}

.fans {
    
    
    font-size: 12px;
    color: gray;
}

button {
    
    
    padding: 2px 4px;
    font-size: 12px;
}

</style>

UserProfilePosts.vue:

<template>
    <div class="card">
        <div class="card-body">
            <!-- v-for是循环,相当于有多少个post建多少个div -->
            <div v-for="post in posts.posts" :key="post.id">
                <div class="card single-post">
                    <div class="card-body">
                        {
    
    {
    
     post.content }}
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
export default {
    
    
    name: "UserProfilePosts",
    props: {
    
    
        posts: {
    
    
            type: Object,
            required: true,
        },
    }
}
</script>


<style scoped>
.single-post {
    
    
    margin-bottom: 10px;
}
</style>

UserProfileWrite.vue;

<template>
  <div class="card edit-field">
    <div class="card-body">
      <label for="edit-post" class="form-label">编辑帖子</label>
      <textarea v-model="content" class="form-control" id="edit-post" rows="3"></textarea>
      <!-- @click绑定函数post_a_post,点击调用函数post_a_post -->
      <button @click="post_a_post" type="button" class="btn btn-primary btn-sm">发帖</button>
    </div>
  </div>
</template>


<script>
import {
    
     ref } from 'vue';

export default {
    
    
    name: "UserProfileWrite",
    setup(props, context) {
    
    
        let content = ref('');

        const post_a_post = () => {
    
    
            context.emit('post_a_post123', content.value);  // 触发父组件中的post_a_post
            content.value = "";
        };

        return {
    
    
            content,
            post_a_post,
        }
    }
}
</script>


<style scoped>
.edit-field {
    
    
    margin-top: 20px;
}

button {
    
    
    margin-top: 10px;
}
</style>

3.5 编写用户列表userlist

利用ajax从API中获取用户信息

<template>
  <ContentBase>
      <div class="card" v-for="user in users" :key="user.id" @click="open_user_profile(user.id)">
        <div class="card-body">
          <div class="row">
            <div class="col-1 img-field">
              <img class="img-fluid" :src="user.photo" alt="">
            </div>
            <div class="col-11">
              <div class="username">{
    
    {
    
     user.username }}</div>
              <div class="follower-count">{
    
    {
    
     user.followerCount }}</div>
            </div>
          </div>
        </div>
      </div>
  </ContentBase>
</template>

<script>
import ContentBase from '../components/ContentBase'
import $ from 'jquery';
import {
    
     ref } from 'vue';
import router from '@/router/index';
import {
    
     useStore } from 'vuex';

export default {
    
    
  name: 'UserList',
  components: {
    
    
      ContentBase,
  },
  setup() {
    
    
    const store = useStore();
    let users = ref([]);

    // 获取用户信息
    $.ajax({
    
    
      url: 'https://app165.acapp.acwing.com.cn/myspace/userlist/',
      type: "get",
      success(resp) {
    
    
        users.value = resp;
      }
    });

    // 打开用户信息
    const open_user_profile = userId => {
    
    
      if (store.state.user.is_login) {
    
    
        router.push({
    
    
          name: "userprofile",
          params: {
    
    
            userId
          }
        })
      } else {
    
    
        router.push({
    
    
          name: "login"
        });
      }
    }

    return {
    
    
      users,
      open_user_profile
    };
  }
}
</script>

<style scoped>
img {
    
    
  border-radius: 50%;
}

.username {
    
    
  font-weight: bold;
  height: 50%;
}

.follower-count {
    
    
  font-size: 12px;
  color: gray;
  height: 50%;
}

.card {
    
    
  margin-bottom: 20px;
  cursor: pointer;
}

.card:hover {
    
    
  box-shadow: 2px 2px 10px lightgrey;
  transition: 500ms;
}

.img-field {
    
    
    display: flex;
    flex-direction: column;
    justify-content: center;
}
</style>

3.6 编写登陆页面

<template>
  <ContentBase>
    <div class="row justify-content-md-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'
import {
    
     ref } from 'vue';
import {
    
     useStore } from 'vuex';
import router from '@/router/index';

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中login函数
      store.dispatch("login", {
    
    
        username: username.value,
        password: password.value,
        //成功回调函数
        success() {
    
       
          router.push({
    
    name: 'userlist'});  //登陆成功则跳转userlist页面
        },
        //失败回调函数
        error() {
    
        
          error_message.value = "用户名或密码错误";
        }
      });
    };

    return {
    
    
      username,
      password,
      error_message,
      login,
    }
  }
}
</script>

<style scoped>
button {
    
    
  width: 100%;
}

.error-message {
    
    
  color: red;
}
</style>

3.7 利用vuex存储数据

在store文件夹下:
index.js:

import {
    
     createStore } from 'vuex'
import ModuleUser from './user';

export default createStore({
    
    
  state: {
    
    
  },
  getters: {
    
    
  },
  mutations: {
    
    
  },
  actions: {
    
    
  },
  modules: {
    
    
    user: ModuleUser,
  }
});

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: {
    
    
      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({
    
    
            // 先获取token
            url: "https://app165.acapp.acwing.com.cn/api/token/",
            type: "POST",
            data: {
    
    
                username: data.username,
                password: data.password,
            },
            // 成功回调函数
            success(resp) {
    
    
                const {
    
    access, refresh} = resp;
                // jwt解码
                const access_obj = jwt_decode(access);

                // 每隔4.5分钟刷新一次
                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);

                // 获取用户信息
                $.ajax({
    
    
                    url: "https://app165.acapp.acwing.com.cn/myspace/getinfo/",
                    type: "GET",
                    data: {
    
    
                        user_id: access_obj.user_id,
                    },
                    // jwt授权
                    headers: {
    
    
                        'Authorization': "Bearer " + access,
                    },
                    success(resp) {
    
    
                        // 成功登陆则调用updateuser
                        context.commit("updateUser", {
    
     
                            ...resp,
                            access: access,
                            refresh: refresh,
                            is_login: true,
                        });
                        data.success();
                    },
                })
            },
            error() {
    
    
                data.error();
            }
        });
      },
  },
  modules: {
    
    
  }
};

export default ModuleUser;

3.8 部署项目

在vue的ui界面点击build运行,打包完成后的结果在dist文件夹中。
如果打包后打开index出现空白页面,则在vue.config.js文件中,将路径修改为相对路径’./',即添加一行publicPath: ‘./’,
vue.config.js:

const {
    
     defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
    
    
  publicPath: './',
  transpileDependencies: true
})

猜你喜欢

转载自blog.csdn.net/weixin_44026026/article/details/126098080