Vue3 项目
配置环境
安装Nodejs
安装LTS
版本即可
安装@vue/cli
脚手架
打开终端 执行:npm i -g @vue/cli
如果执行后面的操作有bug,可能是最新版有问题,可以尝试安装早期版本,比如:npm i -g @vue/cli@4
启动vue
自带的图形化项目管理界面
在powershell
或CMD
上运行:vue ui
常见问题1:Windows
上运行vue
,提示无法加载文件,表示用户权限不足。
解决方案:用管理员身份打开终端,输入set-ExecutionPolicy RemoteSigned
,然后输入y
安装问题杂谈
一些参考blog
:
#安装nodejs 安装完毕后,用管理员启动cmd
#1.检查
node -v
npm -v
#2.创建两个目录:
D:\nodejs\nodejs\node_cache
D:\nodejs\nodejs\node_global
#3.设置global cache路径
npm config set prefix "D:\nodejs\nodejs\node_global"
npm config set cache "D:\nodejs\nodejs\node_cache"
#4.修改用户的环境变量
Path中->D:\nodejs\nodejs\node_global
#5.修改系统变量
添加:NODE_PATH D:\nodejs\nodejs\node_global\node_modules
添加:Path D:\nodejs\nodejs\node_global
D:\nodejs\nodejs
#6.测试安装
npm install express -g
#7.修改镜像
npm config get registry
npm config set registry https://registry.npm.taobao.org/
npm config get registry
npm install -g cnpm --registry=https://registry.npm.taobao.org
#8.安装vue
cnpm install -g @vue/cli
#9.启动
vue ui
# nodejs 安装目录是
D:\nodejs\nodejs
# nodejs版本是最新的
node-v18.12.0-x64.msi
# 详细解释 参考地址
https://blog.csdn.net/qq_48485223/article/details/122709354
卸载vue
:
npm uninstall -g vue
npm uninstall -g vue-cli
npm uninstall -g @vue-cli
基本概念
基本结构
views
文件夹一般用来写各种页面的
router
文件夹一般用来存放网页路由
默认的路由使用
createWebHashHistory
,这会使我们网页的url
存在#
,若将#
删除,需把其改成createWebHistory
vue const router = createRouter({ history: createWebHistory(), routes })
components
文件夹一般是存各种组件的,和views
文件夹差不多,可以根据个人习惯来存放。
整个vue
项目的入口在main.js
文件里
createApp(App).use(store).use(router).mount('#app')
这句话的意思是新创建一个App,其根组件为App.vue
,使用依赖store
(之前安装的vuex
)、router
(之前安装的router
),并挂载渲染到#app
上,这个#app
表示在/public/index.html
中id="app"
的div
标签,即该项目存在一个网页文件index.html
,后面写的组件都是打包挂载到这个文件上面对应的<div id="app"></div>
里的。
前端渲染
vue
框架和react
一样也是通过前端渲染的。
前端渲染与后端渲染的区别:
后端渲染:在客户端每打开一个页面、链接,都会向服务器发送请求,服务器将所用到的页面返回给客户端。
前端渲染;只有在客户端第一次打开页面(无论是什么页面),客户端才会向服务器端发送请求,服务器端将页面所需要的元素全部返回给客户端,同时打包在js文件中,当打开第二个或第三个等页面后,就不需要向服务器发送请求了,直接用返回的js文件将新页面渲染出来。
熟悉vue
在.vue
里同时包含了html
、css
、js
分别对应template
、style
、script
<style scoped><style>
表示当前页面的css
样式只会影响自己,而不会影响其他页面。
我们写的网页一般都是由不同的模块组装而成的,因此我们可以将某些模块单独写成组件,这样就可以实现组件的复用。可以通过以下写法将当前文件的组件模块导出
<script>
export default {
name: '组件名',如'HelloWorld'
props: { //该组件需要传递的信息
键 : 值
msg: String
}
}
<script>
通过一下写法,将组件挂载到对应的页面:
<HelloWorld msg="Welcome to your vue!">
项目实现
页面设计
我们用vue
实现网站时,一般是从上往下设计的,将页面的各个部分拆分成不同的组件,若组件中的元素过多,也可以继续拆分成不同的组件。
设计图如下:
App.vue
App.vue
是我们项目的根组件。我们需要用到的依赖(如bootstrap
)、需要挂载的组件都是需要引用到这里。
我们先来看看项目初建的App.vue
长什么样:
<router-view/>
表示会根据我们的网页网址不同展示不同的网页。
<template>
<nav>
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
</nav>
<router-view/>
</template>
<script>
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
nav {
padding: 30px;
}
nav a {
font-weight: bold;
color: #2c3e50;
}
nav a.router-link-exact-active {
color: #42b983;
}
</style>
这个原装的导航栏有点丑,我们可以把它删掉,自己实现一个新的。
导航栏
我们利用bootstrap
来制作我们的导航栏,首先需要在App.vue
中引入bootstrap
文件。
<template>
<nav-bar></nav-bar>
<router-view :key="$route.fullPath"></router-view>
</template>
<script>
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap/dist/js/bootstrap';
import navBar from './components/navBar.vue';
export default {
name: "App",
components: {
navBar,
}
}
</script>
<style>
然后编写导航栏组件:
/components/navbar.vue
<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 active"
:to="{ name: 'home' }">Home</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link"
:to="{ name: 'userlist' }">Friends</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link"
:to="{ name: 'userprofile', params: { ID: 2 } }">Activity</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: 'login' }">Sign
in</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" :to="{ name: 'register' }">Sign
up</router-link>
</li>
</ul>
<ul class="navbar-nav" v-else>
<li class="nav-item">
<router-link class="nav-link"
:to="{ name: 'userprofile', params: { ID: $store.state.user.id } }">
{{ $store.state.user.username }}</router-link>
</li>
<li class="nav-item">
<a class="nav-link" @click="logout" style="cursor: pointer">Sign
out</a>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
import { useStore } from 'vuex';
export default {
name: "navBar",
setup() {
const store = useStore();
const logout = () => {
store.dispatch("logout");
};
return {
logout,
}
}
}
</script>
<style scoped></style>
导航栏的前端渲染:
<router-link class="navbar-brand"
:to="{ name: 'home' }">Myspace</router-link>
在
Vue
里如果想给某个标签绑定属性要用:
+属性名称
,:
是v-bind:
的缩写。举个例子:
vue <UserProfileInfo :user="user"/>
这里加了
:
后双引号里面的字符串就不是一个普通的字符串,而是一个表达式,可以传一个表达式,表示对双引号里面的字符串内容取值。
由于每个页面都需要导航栏,因此要把导航栏组件放到根组件App.vue
里面。
当我们在当前组件里需要用到其他的组件时,要把这个组件放在当前组件的export default{}
中的components
里,components
属性是一个对象,要定义key
与value
,其中value
是引入的组件名,是它export default
中定义的name
。key
是在当前组件中自己定义的外部组件的别名。当key
与value
同名时,可以统一简写成一个名字即key=value
;若不同名时,要规范地写成key: value
,这样在当前组件中就可以用key
来引用外部的组件了。
如下:
<template>
<navBar/>
</template>
<script>
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap/dist/js/bootstrap';
import navBar from './components/navBar.vue';
export default {
name: "App",
components: {
navBar,
}
}
</script>
ContentBase组件
利用bootstrap
中的card
组件将页面中的内容框起来
<div class="card">
<div class="card-body">
</div>
</div>
利用bootstrap
中的container
组件,响应式地调整中间内容区域的大小。
<div class="container">
<div class="card">
<div class="card-body">
</div>
</div>
</div>
由于不同组件的布局方式类似,即存在公共部分,因此,我们可以将上面的写的card
与container
布局写成一个ContentBase
组件,引入到各个组件页面中去。这样做的好处是,当我们想修改布局或者添加一些公共元素的时候,可以直接修改这个ContentBase
基类组件:
components/ContentBase.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>
在其他页面中引入此组件,以HomeView
为例:
<template>
<ContentBase>
<img src="../assets/2.jpg" class="card-img-top" alt="..." >
<div class="card-body">
<h5 class="card-title">Lucky day</h5>
<p class="card-text">have a fun</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</ContentBase>
</template>
<script>
import ContentBase from '../components/ContentBase';
export default {
name: 'HomeView',
components: {
ContentBase
}
}
</script>
<style scoped>
</style>
这里有一个知识点:如何在ContentBase
中获取到HomeView
中以<ContentBase><ContentBase>
包裹着的内容。
HomeView
是父组件,ContentBase
是其子组件;
ContentBase
负责渲染外部样式,定义布局效果,而其内部的内容由父组件HomeView
提供。
这里需要用到插槽<slot></slot>
在子组件ContentBase
中渲染父组件HomeView
的内容。
关于插槽内容与插槽出口,用例子来展示:
HomeView.vue
<ContentBase>
这里的是插槽内容
<ContentBase>
ContentBase.vue
<template>
<div class="home">
<div class="container">
<div class="card">
<div class="card-body">
<slot></slot> 插槽出口
</div>
</div>
</div>
</div>
</template>
<slot>
元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
通过使用插槽,<ContentBase>
组件更加灵活和具有可复用性。现在组件可以用在不同的地方渲染各异的内容,但同时还保证都具有相同的样式。
React
中是用this.props.children[]
在子组件中获取父组件中的子节点内容的,与Vue
的插槽<slot>
效果类似。
更多用法可以参考官方文档插槽slot
其他的页面也类似滴引入ContentBase
组件。
创建路由
/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import UserList from '../views/UserList.vue'
import UserProfile from '../views/UserProfile.vue'
import LoginView from '../views/LoginView.vue'
import RegisterView from '../views/RegisterView.vue'
import NotFoundView from '../views/NotFoundView.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/userlist/',
name: 'userlist',
component: UserList
},
{
path: '/userprofile/:ID/',
name: 'userprofile',
component: UserProfile
},
{
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
实现用户动态页面
该页面的主要功能为展示当前用户信息、展示当前用户发过的帖子、动态发帖。
因此该页面可以由三个组件实现:
UserProfileinfo
:展示个人信息UserProfilePost
:展示帖子UserProfileWrite
:发帖
页面布局
可以用bootstrap
的Grid System
先展示一下大致的样子:
/views/UserProfile.vue
<template>
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-5">
<img :src="user.photo" class="img-fluid" alt="...">
</div>
<div class="col-9">
<div class="Username">{{ user.Username }}</div>
<div class="fans">followers:{{ user.Followers }}</div>
<button @click="follow" v-if="!user.is_followed" type="button"
class="btn btn-primary btn-sm">Follow</button>
<button @click="unfollow" v-if="user.is_followed" type="button"
class="btn btn-primary btn-sm">Unfollow</button>
</div>
</div>
</div>
</div>
</template>
组件中定义数据变量
我们需要在url
上添加参数来区别当前的动态页面是哪个用户的动态页面,可以用userid
来充当url
的参数。
因此这三个组件都需要进行数据的交互,我们可以把用户数据存放在这三个组件的根组件UserProfile.vue
中。
我们在组件的setup
函数里定义变量存储数据:
setup函数可以代替之前的data,methods,computed,watch,Mounted等对象,但是props声明还是在外面
setup(props, context):
初始化变量、函数ref
定义变量,可以用.value
属性重新赋值reactive
定义对象,不可重新赋值props
存储父组件传递过来的数据context.emit()
:触发父组件绑定的函数
关于
ref
与reactive
:Vue3中实现响应式数据的方法是
ref和reactive
reactive:
- reactive的参数一般是对象或者数组,他能够将复杂数据类型变为响应式数据。
- reactive的响应式是深层次的,底层本质是将传入的数据转换为Proxy对象
- 而
reactive()
不可重新赋值(会丢失响应性),只能通过.属性名=xx
修改对应的属性。
ref
:
ref
的参数一般是基本数据类型,也可以是对象类型;ref
的本质还是react
,如ref(1) ===> reactive({value:1})
如果参数是对象类型,其实底层的本质还是
reactive
,系统会自动将ref
转换为reactive
在模板(
template
)中访问ref
中的数据,系统会自动帮我们添加.value
,在JS中访问ref中的数据,需要手动添加.value
ref
的底层原理同reactive
一样,都是Proxy
template
中引用ref```vue
[HTML_REMOVED]
import { ref } from ‘vue’const ref1 = ref(0)
[HTML_REMOVED]
[HTML_REMOVED]
[HTML_REMOVED]{{ ref1 }}[HTML_REMOVED]
[HTML_REMOVED]
```
reactive vs ref
:
- reactive参数一般接受对象或数组(一般情况下都是对象),是深层次的响应式。ref参数一般接收简单数据类型,若直接定义
a = reactive([])
,则只能通过a.push(...[1,2,3])
的形式重新赋值,不能直接通过=
赋值,或者直接改成ref;若ref接收对象为参数,本质上会转变为reactive方法- 在JS中访问ref的值需要手动添加
.value
,访问reactive不需要- 响应式的底层原理都是Proxy
- ref开销reactive大
总结:
- 如果你需要一个响应式原始值,那么使用
ref()
是正确的选择,要注意是原始值- 如果你需要一个响应式对象,层级不深,那么使用
ref
也可以- 如果您需要一个响应式可变对象,并且对象层级较深,需要深度跟踪,那么使用
reactive
你可以把
reactive
看成ref
的子集,ref
可以解决一切烦恼,
在不同组件里传递信息
父组件向子组件传递信息是通过props
属性
子组件向父组件传递信息是通过绑定事件传递的
子组件里接受父组件传过来的参数,要在export default{}
中添加props
属性。
如子组件UserProfileinfo.vue
中接收其父组件UserProfile.vue
传递过来的参数:
UserProfile.vue
传递参数user
:
<ContentBase>
<div class="row">
<div class="col-3">
<UserProfileInfo :user="user" />
</div>
<div class="col-9">
<UserProfilePost >
</UserProfilePosts>
</div>
</div>
</ContentBase>
定义user
UerProfileinfo.vue
:子组件中接收参数
然后就可以在UserProfileInfo
中愉快地用父组件传来的user
了
利用computed()
函数动态计算值
引用:import {computed} from 'vue'
setup()
中是没有this
的,所以必须传入props
调用父组件传来的数据。
实现关注按钮
需要用到v-if
,关注与不关注显示的按钮不同。
template 属性
<slot></slot>
:存放父组件传过来的children
。v-on:click
或`@click
属性:绑定点击事件v-on:函数名
或@函数名
: 绑定对应的函数事件v-if、v-else、v-else-if
属性:判断v-for
属性:循环,`:key
循环的每个元素需要有唯一的key
v-bind:
或:
绑定属性
子组件触发父组件的函数
父组件中定义的数据是不能直接在子组件里进行修改的,只能通过绑定函数事件的方式,在子组件触发父组件中的函数,在父组件相应的函数中进行数据的修改。
利用context.emit(函数名,参数)
可以触发父组件的函数。
这里的函数名指的是父组件中@
后面跟着的函数名,在父组件中这个@函数名
的函数名并不一定是跟父组件中实现函数的函数名相同,只不过在子组件中通过emit
触发时,要对应这个@函数名
。举个例子:
在父组件UserProfile
中绑定函数:
@follow123
不一定要与follow
同名,follow
为该组件中在setup()
中实现的一个函数。
而在子组件触发时一定与@
后的函数对应:
基本逻辑如下:当在子组件中触发context.emit('follow123')
时,会在父组件中寻找@follow123
,并且执行父组件中@follow123
对应的函数follow
,达到更新数据的目的。
实现UserProfilePost
这里都是先写死数据,后面再调成从后端获取数据:
首先在父组件中定义posts
帖子列表:
传入参数给UserProfilePosts
组件:
<UserProfilePosts :posts="posts" />
与UserProfileInfo
一样,在UserProfilePosts
中要定义props
将父组件传来的参数获取;
export default {
...
props: {
posts: {
type: Object,
required: true, //表示必须传
}
}
}
在template
中循环渲染列表:
注意:在v-if
循环中要加上一个key
,这个key
需要保证是唯一的
也可以改写成取下标的形式:
<div v-for="(post,index) in posts.posts" :key="index" >
...
</div>
如果数组不变的话可以用下标,变的话不建议用下标。
实现UserProfileWrite
这里当懒狗了,没有用到新的知识,直接贴代码吧
<template>
<div class="card edit-field">
<div class="card-body">
<div class="mb-3">
<label for="EditPost" class="form-label">Write Your Post</label>
<textarea v-model="content" class="form-control" id="EditPost" rows="3"></textarea>
<button @click="postApost" type="button" class="btn btn-success">Send</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: "UserProfileWrite",
setup() {
let content = ref('');
const postApost = () => {
console.log(content.value);
content.value = "";
}
return {
content // 这里其实是content:content的缩写
}
}
}
</script>
<style scoped>
.edit-field {
margin-top: 20px;
}
button {
margin-top: 10px;
}
</style>
在vue
里获取textarea
的信息: v-model
双向绑定
<textarea v-model="content" class="form-control" id="EditPost" rows="3"></textarea>
//将textarea里的信息双向绑定到content里
将帖子发到页面右边,需要用到子组件向父组件传递信息,即要触发父组件的函数。
在父组件中定义:
...
<template>
...
<UserProfileWrite @postApost="postApost" />
</div>
</template>
...
<script>
...
const postApost =(content) => {
posts.count ++;
posts.posts.unshift({
id: posts.count,
userId: 1,
content: content
}) //在数组最前面加元素
}
...
return {
postApost,
}
</script>
UserProfileWrite.vue
...
<script>
export default {
name: "UserProfileWrite",
setup(props,context) {
let content = ref('');
const postApost = () => {
context.emit("postApost",content.value);
content.value = "";
}
return {
content // 这里其实是content:content的缩写
}
}
}
</script>
...
实现登录页面
登录页面就是我自己的祖传模板了懒得重新设计,所以在这里只是记录一下登录的逻辑。
首先要用两个双向绑定(v-model
)的响应式变量记录用户名与密码,还要有一个变量记录报错信息:
let username = ref('');
let password = ref('');
let error_message = ref('');
编写login
函数
const login = () => {
error_message.value = "";
store.dispatch("login", {
username: username.value,
password: password.value,
success() {
router.push({ name: 'userlist' });
},
error() {
error_message.value = "Password or Username error!";
}
});
};
阻止表单的默认提交事件:.prevent
<form @submit.prevent="login"> </form>
由于访问很多页面都需要用到用户的信息,因此用户的信息需要放到一个全局变量里,使得所有页面都能访问得到。一般来说子组件给父组件传递信息是用emit
触发函数的形式,父组件给子组件传递信息是通过props
来传递,子组件可以利用props
来调用父组件的数据。这种传递方式是仅限于父子的,若是想给兄弟结点传递信息,则需要先上传到它们的LCA
再下传到对应的兄弟结点,这样做太麻烦了,我们这里引入全新的定义:vuex
.
Vuex
vuex
负责维护全局的变量,类似于react
中的redux
。
vuex
会在全局维护一个对象,这个对象是一个状态树(state),我们所有的信息都会存储在这个状态树里(只会存一份)。这样的话,我们所有页面结点都只需要和这个状态树进行数据交互即可,就不需要先传给LCA
再往下传递了。
由于当前的用户信息在所有页面都用得到,所以我们将用户信息存储到vuex
里。
基本概念
vuex
:存储全局状态,全局唯一state:
存储所有数据,可以用modules
属性划分成若干模块。getters:
根据state
中的值计算新的值,只能通过编写函数的方式返回计算后的新的state
中的值,不能直接修改state
的值。mutations
:所有对state
的修改操作都需要定义在这里,不支持异步,可以通过$store.commit()
触发。举个例子,当我们需要从云端获取数据信息,获取后再进行更新数据信息即修改state
时,是不能在mutations
中修改的,因为从云端获取数据信息的过程是异步的。actions
:定义对state
的复杂修改操作,支持异步,可以通过$store.dispatch()
触发。注意不能直接修改state
,只能通过mutations
修改state
。这里可以简单理解为:若只需要简单的修改state
,如state.username = "xxx"
这种直接赋值的话就可以放在mutations
里修改,若需要进行复杂操作,包括从云端获取数据信息等异步操作,就需要放在actions
里。modules
:定义state
的子模块,对state
进行分割。每个modules
里的对象实际上都是维护state
里的一个对象,同时具有getters、mutations、actions
属性。
vuex实践
在vue的可视化界面装完vuex插件后,我们会在项目文件下得到一个sotre
文件夹,里面的index.js
存放的就是vuex
创建的全局唯一的对象。
上面这些模块本质上是一个字典,只不过当字典里面套字典(对象),且值为函数体时,字典里的键可以省略简写,直接写函数体就好了:
```js
actions: {
login: () => {}
}
可简写成
actions: {
login() => {}
}
```
actions
里的函数有两个传入参数:1. context
,context
里存在着一些API
方法,如context.commit("函数名",需要传入的参数)
可以调用mutations
中对应的函数;2. 自己传的data
参数,自定义传入一些需要用到的数据。
在modules
中定义一个user
模块,并将其放到/store/index.js
的modules
中
这个user
模块和原始的state
模型本质上是一样的,只不过将有关“用户信息”的这些数据抽象出一个新的模块,然后再引入进/store/index.js
主文件中。
/store/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: {
},
actions: {
modules: {},
};
export default ModuleUser;
/store/index.js
import { createStore } from 'vuex'
import ModuleUser from './user'
export default createStore({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
user: ModuleUser,
}
})
这样就使得我们的代码更加简洁可观了,不用全都缩在一个文件里。
以后访问user
模块中的数据只需要通过store.state.user.属性名
访问。
特别注意:在template
中访问store是用$store
JWT验证登录
JWT(json web token)
是一种比较新的登录验证方式,与传统登录方式不同,这种方式支持跨域访问,具体的话可以参考我之前写的文章,这里就不再阐述了。 简单来说就是客户端输入用户名和密码,点击登录就会向服务端发送请求,服务端验证身份后会返回一个jwt token
(可以理解为由用户信息与私钥进行加密后得到的字符串 + 用户信息拼接成的新字符串)给客户端,客户端可以将其存在浏览器的local Storage
里,以后每次访问需要验证身份的页面,客户端都需要携带token
向服务端发送请求,服务端是不会存储token
的(这点与传统的传递seesion_id
登陆方式不同,传统的方式是将用户信息与session
存到服务端的数据库里,jwt
方式是不用将token
存到数据库里的,会将用户信息封装在这个token
里,只会将私钥存储到数据库里,方便对用户传回来的信息进行加密验证),但是会对这个token
用加密算法进行验证,若验证成功则返回信息,若不成功则不返回。
简单逻辑就是:用户在客户端输入用户账号与密码,向服务端申请jwt token
,服务端会向客户端返回一个access
与refresh
字符串。access
就是我们上面所说的用来验证用户身份信息的token
,也叫做令牌,一般有效期比较短;而refresh
是用来刷新access
的有效期的,其本身的有效期比较长。当access
过期后需要利用refresh
调用新的刷新令牌API
来获取新的access
令牌。为什么要这样设计呢?因为有些API
调用用的是GET
方法,GET
方法是会直接将用户信息作为参数放到url
上的,这种方法会将数据全部暴露出来,不太安全,可能会被中间的一些恶意用户直接从url
上截取了你的令牌access
,所以access
的有效期一般要设计得比较短。
refresh
获取新的令牌的API
使用POST
方法,POST
方法里的参数是存在HTTP body
里面的,不会暴露在url
里,一般来说更加安全,有效期更长。用refresh
刷新access
令牌的方式更能保证用户信息的安全。
当access
过期后就代表登出状态了。
获取token
/store/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: {
},
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_obj = jwt_decode(access);
},
error() {
data.error();
},
});
},
},
modules: {},
};
export default ModuleUser;
在views/LoginView.vue
中调用上面的login
函数:
使用store.dispatch("函数名",需要传入的参数)
来调用actions
里定义的函数
...
import { useStore } from 'vuex';
import router from '@/router';
export default {
name: 'LoginView',
components: {
},
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() {
router.push({ name: 'userlist' }); //路由跳转
},
error() {
error_message.value = "Password or Username error!";
}
});
};
const GotoRegister = () => {
router.push({
name: "register",
});
}
return {
username,
password,
error_message,
login,
GotoRegister,
}
}
}
获取某个用户的信息
想要获取某个用户的信息,则需要获取他的user_id
,这个user_id
是编码在上面获取的token
里的,也就是access
,我们需要将它解码出来。(注意,这里user_id
是后端命名传给前端的)
安装解码包:在项目终端上输入:npm i jwt-decode
引入: import jwt_decode from "jwt-decode";
继续完善上面的login
函数:
当获取token
成功后,将其解码,获得user_id
然后就可以根据user_id
去获取用户信息了
从后端获取用户信息后,要以此更新state
中存储的用户信息,在mutations
里更新(mutations
可以直接用state.属性名 = xx
赋值 )
通过context.commit("函数名",需要传入的参数)
可以调用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;
},
updateLogout(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) {
// 获取token
$.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_obj = jwt_decode(access);
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); // 利用refresh刷新access
// 获取用户信息
$.ajax({
url: "https://app165.acapp.acwing.com.cn/myspace/getinfo/",
type: "GET",
data: {
user_id: access_obj.user_id,
},
headers: { //jwt 验证
Authorization: "Bearer " + access, //这里是后端人为定义的Authorization
},
success(resp) {
context.commit("updateUser", {
...resp, //resp是后端传回来的用户信息,这里是将其解构
access: access,
refresh: refresh,
is_login: true,
});
data.success();
},
});
},
error() {
data.error();
},
});
},
});
},
}
利用refresh定期刷新access
有一种傻瓜方法是,写一个定期函数,每隔一段时间就用refresh
刷新aaccess
,这个间隔时间一般取略小于access
的有效期,这里是5分钟有效期,因此设定每隔4.5分钟就刷新一次。上面的代码也已经实现了,这里将它单独抽出来
setInterval(() => {
$.ajax({
url: "https://app165.acapp.acwing.com.cn/api/token/refresh/",
type: "POST",
data: {
refresh, //key与value一样可以简写
},
success(resp) {
context.commit("updateAccess", resp.access);
},
});
}, 4.5 * 60 * 1000);
实现登出功能
登出功能十分简单,直接将token
和用户信息一起清除掉就好了,相当于把通行证给撕了。。
由于可以直接修改state
的值,因此将其写在mutations
里
/store/user.js
...
mutations {
logout(state) {
state.id = "";
state.username = "";
state.photo = "";
state.followerCount = 0;
state.access = "";
state.refresh = "";
state.is_login = false;
}
}
...
直接在组件里调用mutations
里的函数:
如果是调用
mutations
里的函数用commit()
调用
actions
里的函数用dispatch()
点击导航栏的Sign out
按钮实现退出功能
/components/NavBar.vue
...
export default {
name: "NavBar",
setup() {
const store = useStore();
const logout = () => {
store.commit("logout"); //在组件中直接调用mutations里的函数
}
}
}
实现用户列表页面
这里考虑从后端获取数据,就不写死数据了。
在后端获取数据需要用到jQuery
中的ajax
或者是axios
,这里选择jQuery
中的ajax
.
首先安装jquery
:
在VScode
的终端中输入:npm i jquery
然后在项目中引入import $ from 'jquery'
ajax
中的同一个url
可以对应不同的type
方法
UserList.vue
<template>
<ContentBase>
<div class="card" v-for="user in users" :key="user.id"
@click="OpenUserProfile(user.id)">
<div class="card-body">
<div class="row">
<div class="col-1">
<img :src="user.photo" class="img-fluid" alt="">
</div>
<div class="col-10">
<div class="cc">
<div class="username">{{ user.username }}</div>
<div class="followers">
<span class="text"> {{ user.followerCount }} </span> followers
</div>
</div>
</div>
</div>
</div>
</div>
</ContentBase>
</template>
<script>
import ContentBase from '@/components/ContentBase'
import router from '@/router';
import $ from 'jquery';
import { ref } from 'vue';
import { useStore } from 'vuex';
export default {
name: 'UserList',
components: {
ContentBase,
},
setup() {
let users = ref([]);
const store = useStore();
$.ajax({
//url: 'https://api.bilibili.com/x/relation/followers?vmid=2884629&pn=1&ps=20&order=desc&jsonp=jsonp',
url: 'https://app165.acapp.acwing.com.cn/myspace/userlist/',
type: "get",
success(resp) {
users.value = resp;
}
});
const OpenUserProfile = userID => {
if (store.state.user.is_login) {
router.push({
name: "userprofile",
params: {
ID: userID,
}
});
}
else {
router.push({
name: "login"
});
}
}
return {
users,
OpenUserProfile,
}
}
}
</script>
<style scoped>
img {
border-radius: 50%;
}
.cc {
float: left;
height: 50%;
}
.username {
font-weight: bold;
}
.text {
font-weight: bold;
color: black;
}
.followers {
font-size: 15px;
color: rgb(180, 162, 170);
}
.card {
margin-bottom: 20px;
cursor: pointer;
}
.card:hover {
box-shadow: 2px 2px 10px lightgrey;
transition: 500ms;
}
</style>
页面效果大致如下:
url添加参数
我们在用户列表中点开不同的用户理应展示不同用户对应的userprofile
个人信息。因此要在url
上添加id
参数以引导去不同的对应id
的用户个人信息页面。
如:http://localhost:8080/userprofile/18/
表示访问id=18
的用户个人信息页面。
在路由上的userprofile
添加参数:
表示通过ID
来访问对应ID
的用户的userprofile
页面。
{
path: '/userprofile/:ID/',
name: 'userprofile',
component: UserProfile
},
然后在UserProfile.vuie
里引入userRoute
import {useRoute} from 'vue-router'
setup() {
const route = useRoute();
// route.params.参数 params后面可以获取我们添加到路由url后面的参数
console.log(route.params.ID);
}
注意,要在NavBar.vue
里对应的链接也加上参数params
<router-link class="nav-link"
:to="{ name: 'userprofile', params: { ID: 19 } }">Activity</router-link>
修改用户动态页面
添加只有登录后才能访问用户动态页面
在template
调用带参数的函数直接像正常函数调用即可,如下面的@click='OpenUserProfile(user.id)'
,相当于React
中的通过匿名函数调用相应的带参数函数,即前面等效于
@click='() => {OpenUserProfile(user.id)}'
,vue
在底层已经帮我们做好了很多轮子了,因此不用写这么麻烦。React: 勿Q
/views/UserList.vue
<template>
<ContentBase>
<div class="card" v-for="user in users" :key="user.id"
@click="OpenUserProfile(user.id)">
<div class="card-body">
<div class="row">
<div class="col-1">
<img :src="user.photo" class="img-fluid" alt="">
</div>
<div class="col-10">
<div class="cc">
<div class="username">{{ user.username }}</div>
<div class="followers">
<span class="text"> {{ user.followerCount }} </span> followers
</div>
</div>
</div>
</div>
</div>
</div>
</ContentBase>
</template>
<script>
import ContentBase from '@/components/ContentBase'
import router from '@/router';
import $ from 'jquery';
import { ref } from 'vue';
import { useStore } from 'vuex';
export default {
name: 'UserList',
components: {
ContentBase,
},
setup() {
let users = ref([]);
const store = useStore();
$.ajax({
//url: 'https://api.bilibili.com/x/relation/followers?vmid=2884629&pn=1&ps=20&order=desc&jsonp=jsonp',
url: 'https://app165.acapp.acwing.com.cn/myspace/userlist/',
type: "get",
success(resp) {
users.value = resp;
}
});
const OpenUserProfile = userID => { //打开指定用户的userprofile
if (store.state.user.is_login) { //若已经登录了
router.push({
name: "userprofile",
params: {
ID: userID,
}
});
}
else {
router.push({ //未登录就跳到登录页面
name: "login"
});
}
}
return {
users,
OpenUserProfile,
}
}
}
</script>
<style scoped>
img {
border-radius: 50%;
}
.cc {
float: left;
height: 50%;
}
.username {
font-weight: bold;
}
.text {
font-weight: bold;
color: black;
}
.followers {
font-size: 15px;
color: rgb(180, 162, 170);
}
.card {
margin-bottom: 20px;
cursor: pointer;
}
.card:hover {
box-shadow: 2px 2px 10px lightgrey;
transition: 500ms;
}
</style>
修改个人信息页面
之前我们写个人信息页面的时候是写死数据在里面的,这里我们修改一下,将其改成从云端获取数据。
需要修改的地方:1. 用户个人信息、2. 用户发的帖子。
需要用到的API
: 1. 获取用户信息、2. 获取用户的帖子。
/views/UserProfile.vue
<template>
<ContentBase>
<div class="row">
<div class="col-3">
<UserProfileInfo @checkfollow="follow" @unfollow="unfollow" :user="info" />
<UserProfileWrite v-if="isMe" @postApost="postApost" />
</div>
<div class="col-9">
<UserProfilePosts :user="info" :posts="posts" @deletepost="DeletePost" >
</UserProfilePosts>
</div>
</div>
</ContentBase>
</template>
<script>
import ContentBase from '@/components/ContentBase.vue'
import UserProfileInfo from '@/components/UserProfileinfo.vue'
import UserProfilePosts from '@/components/UserProfilePosts.vue'
import { reactive } from 'vue'
import { useRoute } from 'vue-router'
import { useStore } from 'vuex'
import UserProfileWrite from '../components/UserProfileWrite.vue'
import $ from 'jquery'
import { computed } from '@vue/reactivity'
export default {
name: 'UserProfile',
components: {
ContentBase,
UserProfileInfo,
UserProfilePosts,
UserProfileWrite
},
setup() {
const route = useRoute();
const userID = route.params.ID;
const store = useStore();
const user = reactive({});
const posts = reactive({});
//获取用户信息
$.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.Followers = resp.followerCount;
user.is_followed = resp.is_followed;
}
});
//获取用户帖子
$.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;
//console.log(resp);
posts.posts = resp;
}
});
const follow = () => {
if (user.is_followed) return;
user.is_followed = true;
user.Followers++;
}
const unfollow = () => {
if (!user.is_followed) return;
user.is_followed = false;
user.Followers--;
}
const postApost = content => {
posts.count++;
posts.posts.unshift({
id: posts.count,
content: content,
})
}
const DeletePost = postID => {
posts.posts = posts.posts.filter(post => post.id !== postID);
posts.count = posts.posts.length;
}
const isMe = computed(() => userID == store.state.user.id)
return {
info: user,
follow,
unfollow,
posts,
postApost,
isMe,
DeletePost,
}
}
}
</script>
<style scoped>
</style>
修改发帖功能的展示形式
只有当前页面是自己的时候,个人信息页面才会有发帖框。
因此要记录一下当前的个人信息页面是否是为自己。
可以定义一个辅助函数用computed()
函数来动态计算当前页面是否为自己:
/views/UserProfile.vue
const userID = route.params.ID; //获取url路径上的参数,这里默认是字符串类型,若要在下面与store.state.user.id用===比较需转化为整型,或者使用== 非严谨判定
...
const isMe = computed(() => userID == store.state.user.id)
//////
const userID = parseInt(route.params.ID);
const isMe = computed(() => userID === store.state.user.id)
修改<router-view>
我们写到这里时,还有一单小bug,就是当我们在访问他人的个人信息页面时,无法通过点击自己右上角的用户名跳回自己的个人信息页面。原因是按照我们之前设置路由的方法,他判断页面跳转的时候是不考虑判断url
的参数的,因此会卡在同一个userprofile
个人信息页面里,我们要在<router-view>
中添加key
值来绑定参数。
App.vue
<template>
<NavBar />
<router-view :key="$router.fullPath" /> 表示按照完整路径判断
</template>
增加发帖功能
将通过发帖框发送的帖子存到后端的数据库中,并展示在我们页面右边的发帖区
/components/UserProfileWirte.vue
<template>
<div class="card edit-field">
<div class="card-body">
<div class="mb-3">
<label for="EditPost" class="form-label">Write Your Post</label>
<textarea v-model="content" class="form-control" id="EditPost"
rows="3"></textarea>
<button @click="postApost" type="button"
class="btn btn-success">Send</button>
</div>
</div>
</div>
</template>
<script>
import { ref } from 'vue';
import $ from 'jquery'
import { useStore } from 'vuex';
export default {
name: "UserProfileWrite",
setup(props, context) {
let content = ref('');
const store = useStore();
const postApost = () => {
$.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("postApost", content.value)
content.value = "";
}
}
});
/* context.emit("postApost", content.value)
content.value = "";*/
}
return {
content,
postApost,
}
}
}
</script>
<style scoped>
.edit-field {
margin-top: 20px;
}
button {
margin-top: 10px;
}
</style>
增加删帖功能
添加删除按钮,并只有在自己的页面才能显示这个按钮。
UserProfile
页面中会获取不同用户对应的个人信息,
为了判断当前页面是否为自己的,需要从父组件UserProfile.vue
中传一个对方用户的userId
给子组件UserProfilePosts
,这样当渲染到userProfilePost
模块时,可以通过store.state.user.id
当前用户的id
与props.user.id
父组件传来的用户id
是否相同,判断是否该展示删除按钮。
编写删帖逻辑,实现前端与后端同步删除帖子。
/views/UserProfile.vue
...
const DeletePost = postID => {
posts.posts = posts.posts.filter(post => post.id !== postID); //过滤
posts.count = posts.posts.length;
}
...
/components/UserProfilePosts.vue
<template>
<div class="card">
<div class="card-body">
<div v-for="post in posts.posts" :key="post.id">
<div class="card OnePost">
<div class="card-body">
{{ post.content }}
<button @click="delete_a_post(post.id)" v-if="isMe"
type="button"
class="btn btn-danger btn-sm">Delete</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { computed } from '@vue/reactivity'
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();
const isMe = computed(() => store.state.user.id === props.user.id);
const delete_a_post = postID => {
$.ajax({
url: "https://app165.acapp.acwing.com.cn/myspace/post/",
type: "DELETE",
data: {
post_id: postID,
},
headers: {
"Authorization": "Bearer " + store.state.user.access,
},
success(resp) {
if (resp.result == "success") {
context.emit("deletepost", postID);
}
}
});
}
return {
isMe,
delete_a_post,
}
}
}
</script>
<style scoped>
.OnePost {
margin-bottom: 10px;
}
button {
float: right;
}
</style>
实现注册功能
和登陆功能类似,这里就直接贴代码了QAQ
/views/RegisterView.vue
<template>
<div class="homeBox">
<div class="container">
<div class="sign-box">
<div class="apple-btn sign-apple">
<li class="red-btn"></li>
<li class="yellow-btn"></li>
<li class="green-btn"></li>
</div>
<div class="title">Sign</div>
<form @submit.prevent="sign">
<div class="input">
<input v-model="username" type="text" id="sign-user"
placeholder="Have A Good Name?">
</div>
<div class="input">
<input v-model="password" type="password" id="sign-password"
placeholder="Keep Secret">
</div>
<div class="input">
<input v-model="password_confirm" type="password"
id="password_confirm" placeholder="Confirm Your Password">
<div class="error-message">{{ error_message }}</div>
</div>
<button type="submit" class="btn sign-btn">Sign up</button>
</form>
<div class="change-box sign-change">
<div class="change-btn toLogin" @click="GotoLogin">
<span>Login</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref } from 'vue';
import { useStore } from 'vuex';
import router from '@/router';
import $ from "jquery"
export default {
name: 'RegisterView',
components: {
},
setup() {
const store = useStore();
let username = ref('');
let password = ref('');
let error_message = ref('');
let password_confirm = ref('');
const sign = () => {
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) {
console.log(resp);
if (resp.result === "success") {
store.dispatch("login", {
username: username.value,
password: password.value,
success() {
router.push({ name: 'userlist' });
},
error() {
error_message.value = "System Error";
}
});
} else error_message.value = resp.result;
//console.log(resp);
}
});
// console.log(store);
//console.log(username.value,password.value,password_confirm.value);
};
const GotoLogin = () => {
router.push({
name: "login",
});
}
return {
username,
password,
password_confirm,
error_message,
sign,
GotoLogin,
}
}
}
</script>
<style scoped>
* {
padding: 0px;
margin: 0px;
}
.error-message {
display: flex;
font-weight: 800;
color: red;
font-size: 1em;
justify-content: flex-start;
}
.homeBox {
position: fixed;
width: 100%;
height: 100%;
top: 60px;
background: -webkit-linear-gradient(130deg, #ffc72c, #14248b);
}
.container {
position: absolute;
height: 430px;
width: 600px;
background-color: rgba(255, 255, 255, .9);
left: 50%;
top: 30%;
transform: translate(-50%, -30%);
border-radius: 10px;
box-shadow: 0px 0px 10px rgba(17, 39, 59, 0.8);
border: 1px solid rgba(17, 39, 59, 1);
box-sizing: border-box;
}
.container:hover .title {
font-size: 20px;
/* transform: translate(0,-30%); */
}
.container:hover input {
transform: translate(0, -30%);
}
.container:hover .btn {
height: 30px;
width: 90px;
transform: translate(0, -30%);
font-size: 12px;
}
.container:hover .change-box {
height: 200px;
}
.container:hover .change-btn {
display: block;
}
.title {
margin-top: 10px;
position: relative;
height: 40px;
width: 100%;
font-size: 30px;
display: flex;
justify-content: center;
align-items: center;
text-transform: uppercase;
font-weight: 500;
letter-spacing: 3px;
transition: .4s;
}
.input {
width: 400px;
height: 45px;
position: relative;
margin: 40px auto;
/* border-radius: 45px;
overflow: hidden; */
}
input {
position: relative;
width: 100%;
height: 100%;
border: none;
outline: none;
/* box-sizing: border-box; */
padding-left: 15px;
font-size: 16px;
background-color: rgba(255, 255, 255, 0.4);
border-radius: 45px;
transition: .4s;
}
.btn {
height: 50px;
width: 160px;
position: relative;
margin: -10px auto;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 20px;
color: rgba(255, 255, 255, .4);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: .4s;
}
.change-box {
position: absolute;
height: 0px;
width: 100%;
clip-path: polygon(100% 50%, 50% 100%, 100% 100%);
bottom: 0px;
transition: .4s;
}
.change-btn {
position: absolute;
bottom: 30px;
right: 40px;
font-size: 20px;
display: none;
font-weight: 500;
}
.change-btn:hover {
text-shadow: 0px 0px 3px rgba(200, 200, 200, .8);
cursor: pointer;
}
.login-box input {
caret-color: white;
color: rgba(255, 255, 255, 0.8);
}
.sign-change {
background-color: rgba(17, 39, 59, 0.8);
}
.toLogin {
color: white;
}
.sign-box input {
box-shadow: 0 0 3px black;
}
.sign-box .btn {
color: #1e90ff;
background-color: rgba(200, 200, 200, .1);
transition: .5s;
}
.sign-box .btn:hover {
color: white;
background-color: #1e90ff;
}
/* Mac 按钮样式及特效 */
.apple-btn {
position: absolute;
height: 15px;
width: 65px;
top: 3px;
}
.apple-btn li {
list-style: none;
position: relative;
height: 15px;
width: 15px;
border-radius: 15px;
opacity: 0;
}
.sign-apple li {
right: 5px;
float: right;
}
.sign-apple {
right: 5px;
}
li:nth-child(2) {
margin: 0px 2px
}
.red-btn {
background-color: red;
transition: .3s;
transform: translate(0, -50%);
}
.yellow-btn {
background-color: orange;
/* transition-delay: .2s; */
transition: .6s;
transform: translate(0, -50%);
}
.green-btn {
background-color: rgb(15, 136, 15);
/* transition-delay: .3s; */
transition: .9s;
transform: translate(0, -50%);
}
.container:hover .red-btn {
opacity: 1;
transform: translate(0, 0);
}
.container:hover .yellow-btn {
opacity: 1;
transform: translate(0, 0);
}
.container:hover .green-btn {
opacity: 1;
transform: translate(0, 0);
}
</style>
实现关注功能
完善之前在/components/UserProfileInfo.vue
中写的两个follow
与unfollow
函数
与服务端数据库的交互,实现功能的持久化。
前端与后端的交互是通过API
交互的,每次从前端修改之前,先通过API
调用后端,根据后端的结果来修改前端的内容。
/copmponents/UserProfileInfo.vue
<template>
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-5">
<img :src="user.photo" class="img-fluid" alt="...">
</div>
<div class="col-9">
<div class="Username">{{ user.Username }}</div>
<div class="fans">followers:{{ user.Followers }}</div>
<button @click="follow" v-if="!user.is_followed" type="button"
class="btn btn-primary btn-sm">Follow</button>
<button @click="unfollow" v-if="user.is_followed" type="button"
class="btn btn-primary btn-sm">Unfollow</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.FirstName + ' ' + props.user.LastName);
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,
},
success(resp) {
if (resp.result === "success") {
context.emit("checkfollow"); //触发父组件函数以更新父组件里的值
}
}
});
};
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%;
}
.Username {
font-weight: bold;
}
.fans {
font-size: 12;
color: rgb(180, 162, 170);
}
button {
padding: 2px 5px;
font-size: 12px;
}
</style>
项目部署
可以参考下面的博客:博客