Vue3 食用指南
本文记录了Yukiii在学习y总Vue课程的过程中个人心得和学到内容,方便以后开发时查阅,如果发现有误欢迎指正,请多多指教~
可以通过文章目录快速定位到想查看的内容,每个部分的内容概述我都列在了目录中 :3
项目创建
vue ui
之后在图形化界面中选择 项目路径
及初始化配置。
之后安装插件 router
和 vuex
,安装依赖 bootstrap
。
router.js
: 路由,实现页面之间的转换
vuex
:store
,实现多个组件之间维护同一个数据。
bootstrap
: 帮助程序员完成美工工作,包装好一系列的div
。
说明
文件夹说明
router/index.js
记录各个页面的路由。
把
createWebHashHistory
改成createWebHistory
可以让路由不包含/#/
。
-
views/
文件夹用来保存各个页面。 -
components/
文件夹用来保存各个组件。 -
整个程序的入口是
main.js
文件,createApp(App).use(router).mount('#app')
,程序挂载到index.html
中的#app
。
文件说明
每个页面都是个 .vue
文件,由三个部分组成: html, javascript, css
。
不需要考虑不同组件之间的
css
会不会互相影响到,因为<style scoped>
会给每个样式返回一个随机值。
<template>
</template>
<script>
</script>
<style scoped>
</style>
导入 bootstrap
在 main.js
中:
import 'bootstrapp/dist/css/bootstrap.min.css';
import 'bootstrap/dist/js/bootstrap'
网页开发思想
一般开发网页的想法是从上往下开发,从而不用重构代码。每个页面由若干个组件构成。
Vue使用指南
组件的使用及 export default 对象的属性
每个 vue
组件都会导出一个对象 export default
,该组件要用到的子组件都需要在 components
中引入:
export default {
name: 'xxx',
components: {
// 包含的所有该页面用到组件
}
}
其中 export default
对象的属性:
name
: 组件的名称components
: 存储<template>
中用到的所有组件props
: 存储父组件传递给子组件的数据computed
: 动态计算某个数据setup(props, context)
: 定义初始化变量、函数ref
定义变量,可以用.value
属性重新赋值或获取值reactive
定义对象,不可重新赋值,当该类型变量值改变的时候会改变所有用到该变量的组件props
存储父组件传递过来的数据context.emit()
触发父组件绑定的函数return
在<template>
需要用到的变量,事件,函数都需要return
Card 组件的引入
在设计网页时,我们倾向于给每个网页先设置container, card
,将主要内容显示在卡片中。
对于这种每个页面基本都会用到的部分,推荐写一个 component
,这样以后要改配置的时候只需要修改该 component
即可。
<slot></slot>
存放父组件传过来的children
。
ContBase.vue
:
<template>
<div class = "container">
<div class = "card">
<div class = "card-body">
<slot></slot>
</div>
</div>
</div>
</template>
<script>
export default {
name: "ContentBase",
}
</script>
<style scoped>
.container {
margin-top: 20px;
}
</style>
HomeView.vue
:
<template>
<ContentBase>
首页
</ContentBase>
</template>
<script>
import ContentBase from '../components/ContentBase';
export default {
name: "HomeView",
components: {
ContentBase,
}
}
</script>
<style scoped>
</style>
路由的使用
route/index.js
当不存在 routes
前面的路径时,要重定向到 404
页面。
所有链接养成好习惯,以 /
结尾。
import HomeView from '../views/HomeView.vue'
import RegisterView from '../views/RegisterView';
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/userprofile/',
name: 'userprofile',
component: UserProfileView
},
{
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
提供 router-link
实现前端渲染方式,表现为页面跳转时不会刷新。
其中
:
是v-bind:
的缩写,:to = "{name: 'xxxx'}"
,表示绑定哪个页面,{}
框住一个对象,其属性中的name
需要和router.js
中的name
保存一致。
<li class = "nav-item">
<router-link class = "nav-link" :to = "{name: 'userlist'}">好友列表</router-link>
</li>
服务器请求方式
这种方式每次都会刷新一下,不倾向于使用这种方式。
<li class = "nav-item">
<a class = "nav-link" href = "/userlist">好友列表</a>
</li>
实现具体页面的思路
对页面进行分块
利用 bootstrap
中的 grid
可以对页面进行划分,比如要划分成下图所示的两大部分。
<template>
<ContentBase>
<div class = "row">
<div class = "col-3">
<UserProfileInfo />
</div>
<div class = "col-9">帖子列表</div>
</div>
</ContentBase>
</template>
划分成多个组件分别实现
以左上角的组件为例,显示头像,用户名,粉丝数及关注按钮,通过给 div
设置 class
从而在 css
中修改样式:
<template>
<div class = "card">
<div class = "card-body">
<div class = "row">
<div class = "col-3">
<img class = "img-fluid" src = "<图片地址>" alt = "">
</div>
<div class = "col-9">
<div class = "username">Yukiii</div>
<div class = "fans">粉丝:xxx</div>
<button type = "button" class = "btn btn-secondary btn-sm">+关注</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
img {
border-radius: 50%;
}
.username {
font-weight: bold;
}
.fans {
font-size: 12px;
color: grey;
}
button {
padding: 2px 4px;
font-size: 12px;
}
</style>
为什么要存储数据,如何存储数据,组件之间如何传递信息
以头像,粉丝数,用户名为例,每个用户都有各自的信息,这些数据通常会在顶层的组件中保存,之后传递给各个子组件。
在我们的例子中会把数据存在 UserProfileView.vue
(父组件) 中,所有在 <template>
中用到的数据都需要 return
。
其中
return
的user
其实是user: user,
的缩写。
<script>
import { reactive } from 'vue';
export default {
setup() {
const user = reactive({
id: 1,
username: "yukiii",
lastName: "Minato",
firstName: "yukiii",
followerCount: 0,
is_followed: false,
});
return {
user,
}
}
}
</script>
之后,我们需要在不同的组件之间传递信息,父组件传给子组件通常使用 props
的方式,而子组件传给父组件使用 emit
的方式。
父组件传给子组件
其中 ""
引起来的不是普通字符串,而是一个表达式,这里把定义的 user
对象传递给 UserProfileInfo
子组件。
<UserProfileInfo :user="user" />
在子组件UserProfileInfo
中:
props
存储父组件传递给子组件的数据,其中传入的user
中的required
表示必须传入该参数。之后可以在
<template>
中直接使用该变量{{ var.attr }}
。
<template>
<div class = "username">{{ user.username }}</div>
<div class = "fans">粉丝: {{ user.followerCount }}</div>
</template>
<script>
export default {
name: "UserProfileInfo",
props: {
user: {
type: Object,
required: true,
},
}
}
</script>
如果希望显示的数值是动态计算出来的:
<template>
<div class = "username">{{ fullName }}</div>
</template>
<script>
import { computed } from 'vue';
setup(props) {
let fullName = computed(() => props.user.lastName + ' ' + props.user.firstName);
return {
fullName,
}
}
</script>
子组件传给父组件
通常状态在哪定义的,我们就在哪修改。比如已经关注了用户,则 is_followed
的值就要修改为 true
。而关注按钮在子组件中,user
在父组件中。因此我们需要子组件传递信息给父组件。
子组件传给父组件信息是通过子组件触发父组件绑定的事件的方式进行 @事件名称="绑定的事件"
。
子组件在触发事件的同时也可以传递参数给父组件,方法在 获取 textarea 中的信息
中有提到。
父组件中:
<template>
<div class = "col-3">
<UserProfileInfo @follow = "follow" @unfollow = "unfollow" :user = "user" />
</div>
</template>
<script>
export default {
setup(props) {
const follow = () => {
if (user.is_followed) return;
user.is_followed = true;
user.followerCount ++;
};
const unfollow = () => {
if (!user.is_followed) return;
user.is_followed = false;
user.followerCount --;
}
return {
user,
follow,
unfollow,
}
}
}
</script>
子组件中:
其中绑定了按钮触发
follow
函数,具体见为按钮绑定函数
。
<script>
export default {
setup(props, context) {
const follow = () => {
context.emit("follow");
}
const unfollow = () => {
context.emit("unfollow");
}
return {
follow,
unfollow,
}
}
}
</script>
为按钮绑定函数
在 setup()
中定义函数事件,并且为按钮绑定上该事件,其中 @click
是 v-on:click
的缩写,用于绑定事件。
<template>
<div>
<bottom @click = "follow" type = "button">关注</bottom>
</div>
</template>
<script>
export default {
setup(props) {
const follow = () => {
console.log("follow");
};
return {
follow,
}
}
}
</script>
Vue中用循环和判断来显示内容
v-if 判断
v-if
判断如果成立则显示该标签,否则不显示。
<template>
<button v-if = "!user.is_followed" type = "button" class = "btn btn-secondary btn-sm">+关注</button>
<button v-if = "user.is_followed" type = "button" class = "btn btn-secondary btn-sm">取消关注</button>
</template>
v-for 循环
v-for
对于每个 posts
中的 post
我们都构造一个 div
,所有循环的对象都需要绑定一个保证唯一的 key
属性,通常是其id
。
在下面的例子中用到的
posts
对象包含count
和posts
属性,其中posts
是一个数组,存放各个post
对象。const posts = reactive({ count: 1, posts: [ { id: 1, userId: 1, content: "xxxx", } ] });
<div v-for = "post in posts.posts" :key = "post.id">
<div class = "card">
<div class = "card-body">
{{ post.content }}
</div>
</div>
</div>
获取textarea中的信息
通过v-model="绑定的变量"
把 content
变量 和编辑区中的内容绑定起来,从而实时获取编辑内容。
下面例子中绑定了个按钮触发
post_a_post
函数。在
post_a_post
函数中触发父组件中post_a_post
事件,并且传入参数content.value
,由于content
变量是用ref
类型 修饰的,因此在修改或调用它的值的时候需要加上.value
。
<template>
<textarea v-model = "content" class = "form-control" id = "edit-post" rows = "3"></textarea>
</template>
<script>
import { ref } from 'vue';
export default {
name: "UserProfileWrite",
setup(props, context) {
let content = ref('');
const post_a_post = () => {
context.emit('post_a_post', content.value);
content.value = "";
}
return {
content,
post_a_post,
}
}
}
</script>
在父组件中接收到 post_a_post
触发后调用 post_a_post
函数,之后由于 posts
是 reactive
类型变量,因此它修改后会重新渲染所有用到它的组件。
unshift
表示在数组前面加上一个元素。
<template>
<ContentBase>
<div class = "row">
<div class = "col -3">
<UserProfileInfo @follow = "follow" @unfollow = "unfollow" :user = "user" />
<UserProfileWrite @post_a_post = "post_a_post" />
</div>
<div class = "col-9">
<UserProfilePosts :posts = "posts" />
</div>
</div>
</ContentBase>
</template>
<script>
export default {
setup () {
const post_a_post = (content) => {
posts.count ++;
posts.posts.unshift({
id: posts.count,
userId: 1,
content: content;
})
}
return {
post_a_post,
}
}
}
</script>
获取后端url信息
在下面的例子中已经提供了一个后端 url
用于获取所有用户信息,返回一个 json
对象到 users
变量中。
之后可以在 <template>
中用该变量信息,见 通过 v-bind: 实现变量取值
。
url
: 要调用的api
的url
地址。
type
: 方法的类型。
data
: 传入的相关参数。
<script>
import $ from 'jquery';
import { ref } from 'vue';
export default {
name: 'UserList',
components: {
ContentBase,
},
setup() {
let users = ref([]);
$.ajax({
url: '<后端url>',
type: "get",
success(resp) {
users.value = resp;
}
});
return {
users,
}
}
}
</script>
全局变量的使用
可以发现很多地方都需要用到用户的信息,导航栏显示用户名,其他页面也要获取当前用户等等,此时我们就需要维护一个用户信息作为全局变量。
vuex
可以用来维护全局变量,它维护一个 state
状态树,当两个非父子组件需要交互的时候可以通过分别和 state
树交互的方式实现组件之间的交互。vuex
创建的全局唯一的对象在 store/index.js
中。
下面以一个用户登录状态验证的实现来说明如何使用。
用户登录状态的验证
传统方式
在用户第一次访问的时候,浏览器会将 username, password
信息传给 Server
,此时 Server
会生成一个 session_id
传给 Client
,浏览器会把 session_id
存放在 Cookie
之中,同时 Server
还会把 user, sesson_id
放到数据库之中。
当用户下次访问的时候会带上 session_id
进行访问,Server
会先查看数据库中是否存在传来的 session_id
,如果存在则找到该 user
,否则就需要重新登陆。
然而 Cookie
为了保证安全,不允许 js
进行访问,因此用 ajax
跨域访问则访问不到,很难维护登录状态。
跨域: 在当前的域名下访问另一个域名的
API
。
jwt 方式 json web token
jwt
不仅仅用来维护登录状态,它还可以传很多信息。
在用户第一次登录时,会将用户名和密码传给 Server
,之后 Server
会传给 Client
一个 jwt
。
之后用户如果发送的请求需要验证的话,则需要带上 jwt
,jwt
就是一个字符串,也可以理解为 json
对象,用户信息就存在 jwt
中。Server
虽然没有保存 jwt
,但可以验证 jwt
是否合法。
简单介绍一下 jwt
的验证原理: 首先会把一些信息存下来(比如 jwt
过期时间,user_id
等等),再和 Server
中的随机字符串(称为私钥
)进行拼接,之后通过某种加密算法生成一个新的字符串(称为签名
)。Server
之后会把存下的信息 info
和 签名
拼接成公钥 jwt
。
之后用户请求验证的时候,把 jwt
传给Server
,Server
会把 info
和 签名
提取出来,之后重复生成的过程,看生成的 签名
和 jwt
的 签名
是否相同,如果相同则说明验证成功。
全局变量的定义与使用
这里我们使用 jwt
方式来实现登录状态的验证。
首先在登录页面提交表单,把 username, password
传给 user.js
。
user.js
是在store
文件夹下定义的存储用户全局信息的文件,最后会在store/index.js
的modules
属性中引入。
下面的代码是登录页面的部分代码,其中
username, password
和表单的内容绑定在了一起,在提交表单之后调用login
函数,该函数会调用store
中的login
函数。
<script>
import { useStore } from 'vuex';
import router from '@/router/index'
export default {
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: 'namelist'});
},
error() {
error_message.value = "用户名或密码错误";
}
})
}
}
}
</script>
store/user.js
具体代码如下,简单介绍一下下面的用法:
state
存储用户的具体字段信息,后面要调用就用 store.state.user.is_login
(在 html
中用要在前面加上 $
)。
mutations
中用于改变 state
中的字段数据,只有在 mutations
可以修改。
actions
定义用到或修改 state
中信息的函数,在上面的用户登录页面提交表单后触发其 login
函数,在 login
函数中调用 store
中的 login
,传入参数 context, data
,其中 context
用来调用其他的 api
,比如例子中调用了 mutations
中的 updateUser
函数,另一个参数 data
用来接收传入的参数,比如例子中接收到 username, password, success(), error()
。
resp
通常是个json
对象,可以通过key
来获取value
。
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;
}
},
actions: {
login(context, data) {
$.ajax({
url: "<获取jwt>",
type: "POST",
data: {
username: data.username,
password: data.password,
},
success(resp) {
const {access, refresh} = resp;
const access_obj = jwt_decode(access);
$.ajax({
url: "<获取用户信息>",
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: {
}
}
当然,在其他组件中也可以调用 store
中的函数,调用 actions
就用 dispatch
,调用 mutations
就用 commit
。
下面以导航栏中的 logout
退出为例,调用了 mutations
中定义的 logout
函数,具体代码就不展示了,只是把 state
中变量清空为初始状态。
<script>
export default {
name: "NavBar",
setup() {
const store = useStore();
const logout = () => {
store.commit('logout');
};
return {
logout,
}
}
}
</script>
小技巧
头像设置圆形和自适应大小
<tmplate>
<div class = "col-3">
<img class = "img-fluid" src = "<图片路径>" alt = "">
</div>
</tmplate>
<style scoped>
img {
border-radius: 50%;
}
</style>
定义卡片并修改样式
div.card>div.card-body
<template>
<div class = "card single-post">
<div class = "card-body">
{{ post.content }}
</div>
</div>
</template>
<style scoped>
.single-post {
margin-bottom: 10px;
}
</style>
通过 v-bind: 实现变量取值
在下面的例子中,通过 :src
把 user
对象中的 photo
属性取出来。
<template>
<div class = "card" v-for = "user in users" :key = "user.id">
<div class = "card-body">
<img :src = "user.photo" alt = "">
</div>
</div>
</template>
卡片鼠标悬浮效果和阴影
当卡片中内容可交互时,如果我们把鼠标放到卡片上,鼠标的图样应该发生变化,变成"可点击"的样式,同时为卡片浮现一个阴影动画。
<style scoped>
.card {
margin-bottom: 20px;
cursor: pointer;
}
.card:hover {
box-shadow: 2px 2px 10px lightgrey;
transition: 500ms;
}
</style>
函数中的页面跳转
通过 router
中的 router.push({name: '<页面name>'})
实现。
<script>
import router from '@/router/index'
export default {
setup() {
const login = () => {
store.dispatch("login", {
username: username.value,
password: password.value,
success() {
router.push({name: 'namelist'});
},
})
}
}
}
</script>
再举一个例子,下面的函数用来判断如果用户处于登录状态,则可以跳转到其他页面,否则跳转到登录页面。
在 html
中的点击函数传入参数的方法: @click = open(user.id)
。
<script>
export default {
setup() {
const open = userId => {
if (store.state.user.is_login) {
router.push({
name: "<跳转页面_name>",
params: {
userId
}
})
} else {
router.push({
name: "login"
});
}
}
}
}
</script>