AcWing Web应用课 (y总yyds)
文章目录
Vue3——网站整体布局、用户动态页面
1. 前端渲染逻辑
前端渲染:只有第一次打开页面的时候,向服务器发送请求,服务器返回所有js, 之后再打开页面,前端用返回的js文件将页面渲染出来。
2. vue文件
一个vue文件由三部分组成,html,js,css
css部分标签 ,加上scoped,不同组件之间的css选择器就不会相互影响到了。
3. 组件化的框架
- 可以拆分实现
- 引入组件的方式(根组件App.vue)
项目练习:
4. 一些准备
- 引入bootstrap
也是根组件app.vue中引入
<script>
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap/dist/js/bootstrap'
</script>
有一个模块需要单独装
5. 实现导航栏
- 实现导航栏组件——直接使用bootstrap选取需要的元素,将组件export出去
NavBar.vue
<template>
<nav class="navbar navbar-expand-lg 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="#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">
<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>
</ul>
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="#">登录</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">注册</a>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
export default{
name: "NavBar",
}
</script>
<style scoped>
</style>
- 在根组件将 实现的NavBar引入——引入路径,在components对象中添加组件名,同时在template中引入组件
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>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
</style>
6. 写六个小组件
对于一个比较大的组件,可以拆分为多个组件
首页组件
可以在bootstrap中找个card组件,然后用container包起来,container是用来动态调位置的
发现这一部分的html和css其实每个页面都一样,要将这块公共部分提取出来作为单独的组件,方便后期整体修改。
可以将内容渲染到 slot标签中(slot可以用来获取子元素):
contentElem.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: "contentElem",
}
</script>
<style scoped>
.container{
margin-top: 20px;
}
</style>
HomeView.vue
<template>
<contentElem>
首页
</contentElem>
</template>
<script>
import contentElem from "../components/contentElem.vue";
export default {
name: 'HomeView',
components: {
contentElem,
}
}
</script>
<style scoped>
</style>
图片也是可以渲染的:
<template>
<contentElem>
首页
<img src="https://cdn.acwing.com/media/user/profile/photo/108069_sm_fb7fff1e8d.jpg" alt="my head pic">
</contentElem>
</template>
7. 添加路由
-
引入所有组件(页面)
-
更新路由列表即可
import {
createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue'
import NotFoundView from '../views/NotFoundView.vue'
import RegisterView from '../views/RegisterView.vue'
import UserList from '../views/UserList.vue'
import UserProfile from '../views/UserProfile.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/login',
name: 'login',
component: LoginView
},
{
path: '/404',
name: '404',
component: NotFoundView
},
{
path: '/register',
name: 'register',
component: RegisterView
},
{
path: '/userlist',
name: 'userlist',
component: UserList
},
{
path: '/userprofile',
name: 'userprofile',
component: UserProfile
},
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
8. 实现前端渲染的属性
想要实现前端渲染,将原本的a标签换成router-link标签,有特殊的属性 : to
,注意在vue中绑定属性需要用冒号:
,传入name,param
这样就能跳到home对应的路径。
NavBar.vue
<template>
<nav class="navbar navbar-expand-lg bg-light">
<div class="container">
<router-link class="navbar-brand" :to="{name:'home',param:{}}">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" aria-current="page" :to="{name:'home',param:{}}">首页</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" :to="{name:'userlist',param:{}}">好友列表</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" :to="{name:'userprofile',param:{}}">用户动态</router-link>
</li>
</ul>
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<router-link class="nav-link" :to="{name:'login',param:{}}">登录</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" :to="{name:'register',param:{}}">注册</router-link>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
export default{
name: "NavBar",
}
</script>
<style scoped>
</style>
9. 用户动态页面的实现(三个组件)
userProfileInfo/userProfileWrite/userProfilePosts
使用bootstrap的grid
- 先对最大的一块进行划分
- 再实现用户信息组件,对用户信息这个vue进行划分;以此类推
- 对于图片:
Bootstrap 中的图像使用.img-fluid. 这适用于图像max-width: 100%;,height: auto;以便它随父宽度缩放。
图片变为圆形:
img{
border-radius: 50%;
}
然后把文字等元素放进去,调调样式,用卡片包起来,即可完成这个模块。
卡片:
<div class="card">
<div class="card-body">
This is some text within a card body.
</div>
</div>
- 帖子列表
对于展示的个人信息、帖子列表都需要参数,这三个模块是相互交互的,对于这样的情况,需要将数据存到上层组件中。
上层组件UserProfileView.vue:
// setup:()=>{
// }
// 当一个对象是函数时,可以简写为以下:
setup(){
const user =reactive({
id:1,
username:"ZhuJiaxuan",
lastName:"Zhu",
firstName:"Jiaxuan",
followerCountL:0,
is_followed:false,
});
//要用到的属性需要return
return {
user,
}
}
那么如何使用呢?(在不同组件之间传递信息)
子组件接收:(props)
使用:
如果想要的数值需要被计算,如果需要用到传过来的属性
使用computed,参数是一个函数
setup(props){
let fullname=computed(()=>props.user.lastName+" "+props.user.firstName);
return {
fullname
}
}
使用:直接使用{
{fullname}}
即可
- 实现关注按钮
逻辑: 如果没有关注,则显示 “关注”;如果已经关注,则显示 “取消关注”字样
实现: 使用template的v-if
关注完以后,还需要更新user状态,此时需要定义事件处理函数。
举例:
将这两个函数绑定起来:
子组件要向父组件传递消息:
父组件:
子组件:用context.emit可以出发父组件的事件
- 对于帖子列表,同理
v-for
- 发帖区也是同理:
UserProfileWrite.vue
<template>
<div class="card edit-field">
<div class="card-body">
<label for="edit-post" class="form-label">编辑帖子</label>
<textarea class="form-control" id="exampleFormControlTextarea1" rows="3"></textarea>
<button type="button" class="btn btn-outline-primary btn-sm">发帖</button>
</div>
</div>
</template>
<script>
export default {
name:"UserProfileWrite",
}
</script>
<style>
.edit-field{
text-align: left;
margin-top: 20px;
}
textarea{
margin-bottom: 15px;
}
button{
float:right;
padding:2px 4px;
font-size: 12px;
}
</style>
- 现在要将发帖区的内容显示在帖子列表区域
子组件向父组件传递信息,然后父向子传递信息?!
- 首先要获取textarea里的信息
利用v-model,v-model的标签与内容绑定起来
举例:
- 还需要一个触发函数。当click的时候,将textarea里的内容发成帖子。
父组件需要一个函数, 并把这个函数暴露给按钮页即可
const post_a_post =(content)=>{
posts.count++;
posts.posts.unshift({
id:posts.count,
userId:1,
content:content,
});
}
10. 用户列表页面实现
从云端将用户列表读进来
- 安装
npm i jquery
使用ajax - 页面中
import $ from 'jquery';
从云端动态获取用户(使用AJAX)
<template>
<contentElem>用户列表</contentElem>
</template>
<script>
import contentElem from "../components/contentElem.vue";
import $ from 'jquery';
import {
ref } from 'vue';
export default {
name: 'UserList',
components: {
contentElem,
},
setup(){
let users= ref([]);
$.ajax({
url:"https://app165.acapp.acwing.com.cn/myspace/userlist/",
type:"get",
success(resp){
console.log(resp);
}
});
return {
users
}
},
}
</script>
<style scoped>
</style>
使用bootstrap的 Grid system进行布局:
微调样式,
访问不存在的页面,则跳转到404:
给链接加上id
路由中需要添加参数,用:
对应的UserProfileView中,使用提供的useRoute接口,即可获取这个参数
11. 实现登录页面
需要双向绑定两个变量username,password
ref中的值需要.value访问
阻止默认行为
11.1 vuex
因为很多前端行为需要获得用户信息,所以要将登录的用户信息存到全局变量中——此时需要vuex
交互:
vuex创建的全局唯一对象:
/store/index.js
访问:
11.1.1 传统登录方式
存到cookie中,跨域时难处理。
11.1.2 JWT方式
json web token
11.2 实现登录
vuex:存储全局状态,全局唯一。
state: 存储所有数据,可以用modules属性划分成若干模块
getters:根据state中的值计算新的值
mutations:所有对state的修改操作都需要定义在这里,不支持异步,可以通过$store.commit()触发
actions:定义对state的复杂修改操作,支持异步,可以通过$store.dispatch()触发。注意不能直接修改state,只能通过mutations修改state。
modules:定义state的子模块
dispatch调用store中的事件
附注:使用ajax需要import $ from 'jquery';
\
从获得的access字符串中解码出用户信息,需要安装一个解码包
然后在user.js中引入
import jwt_decode from 'jwt-decode';
使用:
const access_obj=jwt_decode(access);
纯背过:用于授权
可以成功获得现在登录的用户信息:
有了这些信息后,要将这些信息存到state里面:
但是action里面是不能直接更新的,要通过mutations
完整版user.js
import $ from 'jquery';
import jwt_decode from 'jwt-decode';
const ModuleUser={
state: {
id:"",
username:"",
photo:"",
followerCount:"",
access:"",
refresh:"",
is_login:false,
},
getters: {
},
mutations: {
updateUser(state,user){
//第一个参数是state,第二个参数是自己定义的
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){
//context 传api,data传一些信息(dispatch的参数)
$.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=resp.access;
// const refresh=resp.refresh;
const {
access,refresh}=resp; //ES6语法
const access_obj=jwt_decode(access);
// console.log(access_obj,refresh);
//然后可以根据其中的user_id和我们的api去获取信息
$.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;
LoginView.vue
<template>
<contentElem>
<div class="row justify-content-md-center">
<div class="col-3">
<form @click.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>
</contentElem>
</template>
<script>
import contentElem from "../components/contentElem.vue";
import {
ref } from "vue";
import {
useStore } from "vuex";
export default {
name: 'UserList',
components: {
contentElem,
},
setup(){
let username=ref('');
let password=ref('');
let error_message=ref('');
const store=useStore();
const login=()=>{
// console.log(username.value);
// console.log(password.value);
//如果想调用外面action中的一个api,用dispatch
store.dispatch("login",{
username:username.value,
password:password.value,
success(){
console.log("success");
},
error(){
console.log("fail");
}
})
}
return {
username,
password,
error_message,
login
}
}
}
</script>
<style scoped>
.error-message{
color: red;
}
button{
float: right;
}
</style>
11.3 使用refresh刷新access
因为5分钟access会过期,所以需要用到refresh进行刷新。
方法1. 访问的时候发现access过期了,此时去获取一个新的access
方法2. 每隔五分钟获取一次access,调用刷线access的接口,每五分钟一次
setInterval(()=>{
$.ajax({
url:"https://app165.acapp.acwing.com.cn/api/token/refresh/",
type:"POST",
data:{
refresh:access_obj.refresh,
},
success(resp){
context.commit("updateAccess",resp.access);
},
error(){
data.error();
}
});
},4.5*60*1000);
12. 登录后跳转到用户列表页面
-
引入router
-
使用api
13. 登录后修改展示页面
NavBar修改一下。让这些页面都符合逻辑
想要获取store中的全局变量,可以使用$ xxx
14. 实现退出
只需要写一个事件即可。
凡是要修改全局state,要把事件写到action/mutations里面 (需要store)
15. 未登录时的跳转
userList.vue
点击时触发函数:
16. 用户动态从云端获取
用户动态页面根据userId的改变而改变。
还是ajax!
17. 判断发帖子模块是否需要
只有在自己的页面才能发帖子。
18. 用完整链接判断页面是否相同
App.vue
默认是用页面的name判断的,都叫userprofile,所以到别人页面,再点击自己的,就会出现不刷新不跳转的情况。
19. 从前端删除到真的删除
20. 部署
- build 打包成可以发布的版本
2. 打包
3. 传到服务器上