My Journey of Learning VUE3🎄
前言
本篇文章偏个人记录向,都是之前做一个类博客网站的VUE3课程项目时顺手记下的。可能会有比较跳跃的地方,后续有空重新捋一遍的话,会再次调整。
- 项目源码地址:https://github.com/goatpang/Acwing-Vue-Project
- 课程地址:https://www.acwing.com/activity/content/introduction/1150/
VUE3
创建项目
vue3传统创建项目:
- npm install -g @vue/cli (安装或升级脚手架)
- vue create + 项目名
- 然后 cd 项目名
- npm run serve 即可启动项目
npm run serve – –port 8888可以指定端口号
vite创建项目
- 也可以用vite创建vue3
- 新一代的前端构建工具(npm用的好像是webpack,是传统构建方法)
- 开发环境中,无需打包,可快速冷启动
npm run dev
命令是针对vite的构建方式,还需要init,需要install什么的
图形界面
vue ui
可以打开图形化界面图形界面中可以看到几个板块
- 创建项目
- 插件模块
- vue router:多个页面的路由功能
- vuex:实现多个组件维护同一个数据
- ant design of vue:阿里的前端框架
- 依赖模块
- 各种依赖包,和插件应该都会有所对应的
- bootstrap前端框架插件
- 任务模块
- serve:调试环境
- build:打包文件
vue项目基本架构
文件夹component和views下面都可以存组件,component一般放组成页面的小组件,view下面放调用小组件组成页面的组件,每个组件就是一个vue文件
文件夹router下面存的index.js是路由,设计页面跳转的(如上面代码:path是
/about
就跳转到AboutView.vue
组件中)APP.vue是根组件,即主页面(调用其它组件,跳转到其它页面(通常由路由实现))
程序的入口是在main.js,会调用主页面App.vue
1
2
3
4
5
6import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
createApp(App).use(store).use(router).use(router).mount('#app')从APP.vue调用根组件App
并且从router文件夹调用路由
并且从store文件夹调用store(Vuex的全局变量和函数)
然后把这些东西挂载到
#app
这个元素上,这个元素在哪?(看下面)
public文件夹下有个index.html,这个html里面就有一个叫
app
的div元素,main.js就是负责把各种组件、各种JS文件挂载到这
vue3基本知识
vue3是一个前端渲染框架
- 后端渲染框架:每打开一个链接,就会向服务器发送一个请求
- 前端渲染框架:只有在第一次打开页面的时候,才会向服务器发送请求,将所有页面打包到一个JS文件里面返回,这时候如果我们打开这个网站的其它页面,就不会再向服务器发起请求,而是在本地完成页面的渲染
正常的前端三剑客,应该是一个html文件,然后靠html文件把js、css动态引入。而在vue文件中,每个vue文件通常对应着网站的一个页面(称为一个组件),包含了以上的所有东西
- 用
template
标签存放html - 然后就是用
script
存放js - 用
style
存放css
- 用
vue中的CSS样式可以指定
scoped
选项,这会使得每个CSS样式只影响当前组件的样式,不会影响到其它组件的样式VUE是一个组件化的框架,一个页面有多个部分,可以用多个不同的组件来完成这个页面,比如导航栏一个组件,内容一个组件,……
那要如何在一个页面引入组件呢
开发vue项目
APP.vue分析
导航栏组件
- 首先实现导航栏组件NavBar.vue
- 用一个组件实现,根组件App.vue的
- 首先在APP.vue的脚本区域import bootstrap组件
- 然后在bootstrap官网去找已经做好样式的导航栏,直接放到vue文件的template里面即可实现导航栏(这里其实ant design of vue是一样的)
- 这里还要把a标签全部改成router-link标签
内容基础组件
然后是内容组件ContentBase.vue
为什么要这样呢?因为我们预计的每个页面都是要一个这种框框的内容组件的,既然是每个页面公用的,那跟导航栏的性质一样,将其独立用组件表示更佳,这样调整样式时就可以一起调整
1 | <template> |
上面的代码,三个知识点,首先是:
<div class="container">
固定样式的div,用于把内容放在中间<div class="card">
固定样式的div,一个带外框的div<slot></slot>
占位符,相当于该组件文件的函数参数,可以在其它组件调用该组件时,往这里填内容
实现VUE路由
首先只需要在router文件夹下的JS文件添加对应组件的路由路径及名称
(添加完后,用路径即可实现跳转)
为实现导航栏NavBar.vue的路由功能,我们可以采用两种方式
一种是常规html里面
a
标签href="/login"
的方式,实现超链接,但这个是后端渲染,每次点击都会向服务器请求一次数据还有一种是前端渲染,利用vue提供的
router-link
标签,以及:to="{name:"login"}"
的方式指定路由位置,实现路由超链接(前端渲染)(这里需要把bootstrap复制过来的导航栏代码的a标签全改成router-link,可以点击alt键,实现同时修改)
(VUE里面
:to=""
中冒号内的内容不是字符串,是一个表达式)
用户动态界面
实现用户动态的界面,分成三个模块:
UserProfileInfo、UserProfilePosts、UserProfileWrite
- 布局,用bootstrap的grid system实现
1 | //这里就完成了3比9的行切分 |
图片自适应大小参数
1
<img class=".img-fluid" src="xxx" alt="">
图片圆框
1
2
3img {
border-radius: 50%;
}方框卡片card标签
1
2
3
4
5<div class="card" style="margin-top: 30px;">
<div class="card-body">
xxx
</div>
</div>div指定class类型,然后针对该类型使用css调整样式
button可以直接用bootstrap的button
补充知识,CSS中:
不加点
.
是对所有指定标签元素进行样式修改,例如
1
2
3
4
5
6
7 img{
xxx
}
div{
xxx
}加了
.
的是对指定的Class的标签进行样式修改,例如:
1
2
3
4
5 <div class="red">hello</div>
.red{
xxx
}
父组件传参子组件
每个vue组件具有一些参数(这些参数也可以由用户的交互得到)
这个东西保存在export default
里面:
export default对象的属性:
name
:组件的名称components
:存储<template>
中用到的所有组件props
:存储父组件传递给子组件的数据computed
:动态计算某个数据setup(props, context)
:初始化变量、函数ref
定义变量,可以用.value属性重新赋值reactive
定义对象,不可重新赋值props
存储父组件传递过来的数据
reactive
、computed
需要在vue中importimport {reactive} from ‘vue’;
例如在动态界面的主组件中的script就应该这样写:(定义变量对象并返回)
1 | export default { |
主组件的template应该这样写:(把变量对象传给子组件)
1 | <template> |
冒号传参,和前面的router-link的那个:to="{name:'home'}"
性质相同,此时双引号里面的内容是表达式的形式,而非字符串
子组件的script应该这样写:(用props
获取父组件传来的参数)
1 | export default { |
子组件的template应该这样写:(将参数填入页面中)
1 | <template> |
注意,这里的src前面有一个冒号,才能解析后面的对象返回值,
:src="user.avatar"
(这种冒号等号双引号的搭配在vue里面太经典了)
然后其它是在标签内的文本,就通过
{{user.username}}
的方式即可解析。
参数计算并使用
例如子组件想利用父组件传过来的参数计算,并在子组件中使用
在script中:
1 | //引入vue中计算参数所使用的函数computed |
()=>{return props.user.lastname + props.user.firstname;}
其实是一种匿名函数的写法
在template中:
- 直接使用setup中定义的变量
1 | <div class="username">{{ fullname }}</div> |
- 条件语句v-if
1 | <button v-if="user.isfollowed" type="button" class="btn btn-secondary">取消关注</button> |
触发事件及绑定
export default对象的属性:
props
:存储父组件传递给子组件的数据watch()
:当某个数据发生变化时触发setup(props, context)
:初始化变量、函数context.emit()
:触发父组件绑定的函数
定义函数和定义对象是一样的,都在setup函数里面定义。
当“关注”这个按钮被点击的时候会触发两种可能的事件:follow事件和unfollow事件,在script分别就可以写两个函数:
1 | export default { |
在template中就要对对应的标签绑定我们的函数,这里当然是关注button:
1 | <button @click="unfollow" v-if="user.isfollowed" type="button" class="btn btn-secondary">取消关注</button> |
核心点就是在button上加入:@click="follow
“
@click
等价于v-on:click
,"follow"
中的follow就是setup中定义的触发函数(事件)
子组件传信息给父组件
子组件触发事件绑定父组件触发事件,实现更改父组件
在父组件内,需要首先定义一些当子组件的某事件被触发时,父组件要触发的事件
script中:(定义父组件的触发事件)
1 | const follow = () => { |
template中:(绑定子组件的触发事件和父组件的触发事件)
1 | <UserProfileInfo @follow="follow" @unfollow="unfollow" :user="user"></UserProfileInfo> |
在子组件中,需要定义触发信号(button),并定义触发事件,子组件触发事件需要用context.emit()
触发所绑定的父组件的触发事件
1 | export default { |
帖子界面
- 帖子用对象存储,具体内容用数组存储
1 | const posts = reactive( |
- v-for,循环posts对象展示帖子
1 | 文章总数:{{ posts.count }} |
这里注意必须要有
:key
这个参数,要选择一个循环数据中的唯一值作为key
(用途是优化循环,对用户透明的)
编辑帖子界面
帖子这种类型其实就是表单form,即可以让用户填数据的组件
- textarea组件作为编辑区(bootstrap上找一个好看的复制过来)
- 用
v-model
将textarea的内容值与自定义变量content绑定
1 | <textarea v-model="content" class="form-control" id="edit-post" rows="3"></textarea> |
- 自定义变量
ref
类型用let
,取出ref类型的值需要用value属性
1 | setup(props, context) { |
- 设定button的触发事件post_a_post
- 并与父组件绑定,实现发帖
1 | <button @click="post_a_post" type="button" class="btn btn-primary">发帖</button> |
1 | setup(props, context) { |
- 父组件设置绑定子组件
1 | <UserProfileWrite @post_a_post="post_a_post"></UserProfileWrite> |
- 父组件定义接收信号的触发事件,接收数据,修改posts对象
1 | const post_a_post = (content) => { |
posts对象是响应式的reactive对象,在完成修改后,会自动在所有引用该对象的组件中动态更新值,即可实现发帖
在数组插入元素,尾插入push,头插入unshift
后端API
- acwing给了一些API给我们使用,我的理解就是这是后端开发人员实现的工作(访问数据库之类的),后端将一些功能封装成了接口,给前端人员使用。
- 前端开发人员就可以使用后端封装出来的API来实现功能,例如通过API从服务器获取数据,将数据呈现在页面上,或通过API向服务器发送请求以更新数据。
- 因此,API是前后端通信的桥梁,它们的实现是后端的内容,但是前端开发人员需要了解如何使用API来实现应用程序的功能。
- 这里用需要用到
jquery
插件引入$
,即可使用$.ajax
进行API的请求响应
用户列表页面实现
用ajax请求API,获取用户列表,将用户列表保存到自定义的ref变量users中,并展示到界面上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15export default {
name: 'UserList',
components: { ContentBase },
setup() {
let users = ref([]);
$.ajax({
url: 'https://app165.acapp.acwing.com.cn/myspace/userlist/',
type: 'GET',
success(data) {
users.value = data;
}
});
return { users }
}
}实现CSS的一些动效,用户鼠标悬浮在其它用户的卡片时:
首先把鼠标变一下
1
2
3
4.card {
cursor: pointer;
margin-top: 30px;
}周围可以有一些阴影动效,给用户反馈
或者是背景变灰,这个比较简单
1
2
3.card:hover {
background-color: #f5f5f5;
}
顺手完善路由,路由到没有指定的路径的时候,应该跳到404
1 | { |
- 顺手完善路由,不同的用户的用户界面是不一样的,需要用userid进行区分。且需要登陆以后才会显示用户界面
更改router下面的index.js,其中:userID
表示参数
1 | { |
更改NavBar.vue的路由,加入params,表示传入的参数值
1 | <router-link class="nav-link" :to="{ name: 'userprofile', params: { userID: 2 } }">用户动态</router-link> |
更改用户界面 UserProfile.vue,获取路由router中的userID参数
1 | import { useRoute } from 'vue-router'; |
登录界面
首先在boostrap里面找个登录表单复制过来
将登录界面缩小,用分区的做法,只给登录界面三份
1
2
3
4
5<div class="row">
<div class="col-3">
登录界面
</div>
</div>将登录界面居中:
row
后面加上justify-content-md-center
1
<div class="row justify-content-md-center">
将登录按钮放大:宽度100%
1
button{ width:100%}
错误信息error message
表单提交绑定触发事件login
1
2<form @submit="login">
</form>由于表单本身有个默认的触发事件,我们要将原本的默认触发事件阻止的话,写法如下:
1
2<form @submit.prevent="login">
</form>登录组件的表单用
v-model
绑定setup里面定义的几个变量,用于获取变量
登录机制
- session机制
当用户在Web应用程序中进行登录或进行其他需要身份验证的操作时,应用程序会创建一个唯一的session ID,并将其存储在用户的浏览器中,通常是通过Cookie来实现。会话ID允许应用程序在后续的请求中识别用户,并保存用户的状态和相关信息,例如购物车内容、用户个人信息等。
y总说这是传统机制,很难实现跨域,登录状态难维护,因为session id都放在cookie里面,而cookie JS是不允许访问的
- Token机制
Token机制是一种无状态的用户登录机制,它使用一些加密算法将用户的身份信息和其他相关信息打包成一个Token,并将Token存储在用户的浏览器中,通常是通过Cookie或本地存储来实现。每次请求时,应用程序会检查请求中是否包含有效的Token,如果包含,则可以识别用户,并读取和保存相关状态和信息。
- JWT机制(JSON web Token)
JWT机制是一种基于Token的身份验证机制,它使用JSON格式来存储用户的身份信息和其他相关信息,并使用一些加密算法将其打包成一个Token
- 其它机制:OAuth机制、Basic认证机制
- session vs JWT
Session ID是一串随机数,因此会存在数据库中,来比对用户的Session ID是否合法
而JWT认证是根据用户的信息和token值判断是否合法,token并不需要存储在服务器。具体如下:
token是怎么来的呢?
- 构造 header 部分:header 包含了 token 的类型(即 JWT)和加密算法(如 HMAC SHA256 或 RSA),需要将其进行 base64 编码。
- 构造 payload 部分:payload 包含了用户信息、权限信息、过期时间等相关信息,需要将其进行 base64 编码。
- 构造 signature 部分:signature 是由 header 和 payload 部分和密钥组成的签名值,用于验证 token 的真实性和完整性。生成 signature 需要对 header 和 payload 部分进行签名,并使用密钥(随机数)对签名值进行加密,通常使用 HMAC SHA256 或 RSA 签名算法。
- 将 header、payload 和 signature 部分组合成一个字符串,使用点号(.)进行连接,形成完整的 token。
1
2
3
4 graph LR
A[header] --- B[payload]
B --- C[signature]用户再次携带token访问时,将用户的个人信息拼接上存储在服务器的随机数密钥过一遍加密算法,如果得出的值就是用户携带的token值,则合法。如果用户的个人信息修改了,那就无法再通过验证,因此也叫签名
注意:由于JWT的header和payload部分并没有加密,所以可以直接根据access值进行base64解码,去得到一些用户的基本信息
- JWT的access和refresh值
JWT认证机制下,用户输入用户名和密码给服务器,服务器会返回两个值:
- 一个是access值,就是token值
- 另一个是refresh值,用于获取access值
access值通常只有5分钟有效,refresh值14天内有效,因此可以用refresh值取获取新的access值
JWT的access值header和payload部分是base64编码的,并没有加密,想要解码,将其序列化,获取具体的相关信息,还得安装一个解码包:jwt-decode
全局变量vuex
组件是一个树状的形式,如果要把信息依次传递,那就非常麻烦,很多时候所有组件可能都需要用同一些变量,即全局变量,那就得靠vuex来实现
1 | import { createStore } from 'vuex' |
【异步】:即操作需要等待一段时间的,如请求API
【同步】:能瞬间完成的
modules本质上还是state,具体实例如下:
1 | //index.js |
1 | //user.js |
这里如果要使用user这个全局变量:store.state.user.username
登录机制实现
- 登录组件的login函数利用
store.dispatch
去调用store的action里的login函数
1 | import {userStore} from 'vuex'; |
- 利用vuex在store的stata中以全局变量的方式存储用户的信息
- 在store的action里面写上登录的触发函数login
(1)获取用户的JWT,并将其解码,获取access和refresh
(2)
1 | import $ from "jquery"; |
实现登录成功后跳转到用户界面(loginview.vue)
=》利用路由实现
router.push({ name: "userlist" })
登录成功后:将导航栏中登录button改成用户名button,改成导航到用户列表界面,注册button改成退出button,改成导航到首页,并将JWT删除(删除登录状态)
首先要获取存在store中的
is_login
:$store.state.user.is_login
利用
v-if
加到button中进行判断is_login
的状态,设定不同的导航栏注意没有登陆的情况下,不应该有用户动态页面
自动跳转登陆界面
- 点击用户卡片,先判断是否登陆,如果未登陆,跳转到登陆界面
- 如果已登陆,就跳转到对应的用户界面,这里需要在template传参userid,才能跳转到对应用户的界面,而在vue里面,这里封装的很好,只需要:
@click="open_user_profile(user.id)"
这样即可传参,这是因为vue良好的封装,所以才如此简单
用户动态界面
修改用户动态界面(第一个视频时候的把用户动态内容写死了,现在需要响应式地请求对应用户的动态)
- 首先要请求API,获取用户信息,注意【当前查看用户】和【登陆用户】的区别
- 然后css样式,imge-field,使图片居中,flex
- 然后是请求API,获取帖子信息
发帖补充
- 需要做判断,只有自己的页面可以发帖(现在查看其它用户的帖子的时候仍有编辑发帖区)
1 | <UserProfileWrite v-if="is_me" @post_a_post="post_a_post"></UserProfileWrite> |
1 | import { computed } from 'vue'; |
添加帖子:在原来的添加帖子的基础上添加请求API修改数据库的功能即可,原来添加帖子的方式是前端修改,所以其实不需要数据库也可以实现添加帖子,现在API的方式即可永久添加帖子
Vue判重机制
在看别人的动态的时候,点【账号名按钮】或【用户动态】并不会跳转回自己的用户页面。这是因为:vue的判重机制默认不包括路由中的参数,判重的过程中,/userprofile/15
和/userprofile/36
是被认为相同的页面,这就会导致页面不会发生跳转
解决方案:指定router-view的key为完整路径(y总推荐都以后这么写)
1 | <template> |
保存登陆状态
现在的登陆状态是一刷新就没了,如果需要保持登录状态,就需要把access那些存在本地的localstorage 1:51:10分开始讲
删除帖子
首先增加删除button,并且调整到向右对齐
1 | button { |
然后是在profile页面用filter
进行前端删除,在profilewrite页面调用API进行后端数据库的删除,如果成功,才调用父组件的前端删除
编辑帖子:后续自己完成
注册功能
复制登录界面,再加一项“确认密码”
关注功能
本来只实现了前端关注,现在就是加入后端API调用,使关注可持久化
build打包部署
用build可以将项目打包为一个dist文件夹:里面会有两个JS文件和两个CSS文件,分别是打包我们自己编写的所有代码和第三方代码(如boostrap)
这个文件夹可以部署到服务器上
注意
关于ref数据类型
<template>
1 | <div v-if="Current === 'mail'"> |
<script>
1 | let Current = ref('mail'); |
这才是ref数据类型关于value的正确用法,在script里面都得用value,在template里面都不用value,这才能搞定,不然就寄
关于webpack和vite
npm默认的就是webpack打包,webpack是传统方式,vite是新方式,webpack对于不同数据类型的数据处理的并不好
babel就是一种webpack打包工具
ERROR in ./src/assets/Git.md?raw 1:1
Module parse failed: Unexpected character ‘ ‘ (1:1)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders这个错误通常发生在使用Webpack打包时,Webpack无法识别或处理某些文件类型的情况下。针对这种情况,你需要为Webpack添加一个loader来处理该文件类型
Ant Design of Vue
在启动项目前加入:npm i --save ant-design-vue
然后完成注册(这里只讲全局注册)
1 | import { createApp } from 'vue'; |