面经 H5 端 - Vant(下)
核心定位:
- 熟悉 项目架子: api 模块 (封装请求函数) request 请求封装(axios) storage 封装 路由练习
- 熟悉 组件库 使用: 复制文档内容,粘贴到合适位置,看文档改改
面经列表
静态结构
注册组件:
- van-cell
- van-list
import Vue from 'vue'
import {
Cell,
List,
Tab,
Tabs
} from 'vant'
Vue.use(Tab)
Vue.use(Tabs)
Vue.use(Cell)
Vue.use(List)
静态结构 article.vue
:
<template>
<div class="article-page">
<van-nav-bar fixed placeholder>
<template #left>
<van-tabs color="#000" title-inactive-color="#aaa" line-width="12px">
<van-tab title="推荐"></van-tab>
<van-tab title="最新"></van-tab>
</van-tabs>
</template>
<template #right>
<div class="logo">
<img src="@/assets/logo.png" alt="" />
</div>
</template>
</van-nav-bar>
<van-cell class="article-item" >
<template #title>
<div class="head">
<img src="http://teachoss.itheima.net/heimaQuestionMiniapp/%E5%AE%98%E6%96%B9%E9%BB%98%E8%AE%A4%E5%A4%B4%E5%83%8F%402x.png" alt="" />
<div class="con">
<p class="title van-ellipsis">宇宙头条校招前端面经</p>
<p class="other">不风流怎样倜傥 | 2022-01-20 00-00-00</p>
</div>
</div>
</template>
<template #label>
<div class="body van-multi-ellipsis--l2">
笔者读大三, 前端小白一枚, 正在准备春招, 人生第一次面试, 投了头条前端, 总共经历了四轮技术面试和一轮hr面, 不多说, 直接上题 一面
</div>
<div class="foot">点赞 46 | 浏览 332</div>
</template>
</van-cell>
</div>
</template>
<script>
export default {
name: 'article-page',
data () {
return {
}
},
methods: {
}
}
</script>
<style lang="less" scoped>
.article-page {
.logo {
flex: 1;
display: flex;
justify-content: flex-end;
> img {
width: 64px;
height: 28px;
display: block;
margin-right: 10px;
}
}
}
.article-item {
.head {
display: flex;
img {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
}
.con {
flex: 1;
overflow: hidden;
padding-left: 10px;
p {
margin: 0;
line-height: 1.5;
&.title {
width: 280px;
}
&.other {
font-size: 10px;
color: #999;
}
}
}
}
.body {
font-size: 14px;
color: #666;
line-height: 1.6;
margin-top: 10px;
}
.foot {
font-size: 12px;
color: #999;
margin-top: 10px;
}
}
</style>
封装 api 接口
接口:https://www.apifox.cn/apidoc/project-934563/api-20384521
新建 api/article.js
提供接口函数
import request from "@/utils/request";
// 1.
export const getArticlesAPI = (obj) => {
return request({
url: "/h5/interview/query",
params: {
current: obj.current,
pageSize: 10,
sorter: "weight_desc",
},
});
};
页面中调用测试
import { getArticles } from "@/api/article";
export default {
name: "article-page",
data() {
return {};
},
async created() {
const res = await getArticles({
current: 1,
});
console.log(res);
},
};
发现 401 错误, 通过 headers 携带 token
注意:这个 token,需要拼上前缀 Bearer
token 标识前缀
// 封装接口,获取文章列表
export const getArticles = (obj) => {
const token = getToken();
return request.get("/interview/query", {
params: {
current: obj.current, // 当前页
pageSize: 10, // 每页条数
sorter: obj.sorter, // 排序字段 => 传"weight_desc" 获取 推荐, "不传" 获取 最新
},
headers: {
// 注意 Bearer 和 后面的空格不能删除,为后台的token辨识
Authorization: `Bearer ${token}`,
},
});
};
请求拦截器-携带 token
utils/request.js
每次自己携带 token 太麻烦,通过请求拦截器统一携带 token 更方便
import { getToken } from "./storage";
// 添加请求拦截器
request.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
const token = getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error);
}
);
封装 ArticleItem 组件
新建 components/article-item.vue
组件
<template>
<van-cell class="article-item">
<template #title>
<div class="head">
<img
src="http://teachoss.itheima.net/heimaQuestionMiniapp/%E5%AE%98%E6%96%B9%E9%BB%98%E8%AE%A4%E5%A4%B4%E5%83%8F%402x.png"
alt=""
/>
<div class="con">
<p class="title van-ellipsis">宇宙头条校招前端面经</p>
<p class="other">不风流怎样倜傥 | 2022-01-20 00-00-00</p>
</div>
</div>
</template>
<template #label>
<div class="body van-multi-ellipsis--l2">
笔者读大三, 前端小白一枚, 正在准备春招, 人生第一次面试, 投了头条前端,
总共经历了四轮技术面试和一轮hr面, 不多说, 直接上题 一面
</div>
<div class="foot">点赞 46 | 浏览 332</div>
</template>
</van-cell>
</template>
<script>
export default {
name: 'ArticleItem'
}
</script>
<style lang="less" scoped>
.article-item {
.head {
display: flex;
img {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
}
.con {
flex: 1;
overflow: hidden;
padding-left: 10px;
p {
margin: 0;
line-height: 1.5;
&.title {
width: 280px;
}
&.other {
font-size: 10px;
color: #999;
}
}
}
}
.body {
font-size: 14px;
color: #666;
line-height: 1.6;
margin-top: 10px;
}
.foot {
font-size: 12px;
color: #999;
margin-top: 10px;
}
}
</style>
注册成全局组件使用
import ArticleItem from "@/components/article-item.vue";
Vue.component("ArticleItem", ArticleItem);
Article.vue
页面中
<template>
<div class="article-page">
...
<article-item></article-item>
</div>
</template>
动态渲染列表
article.vue
存储数据
data () {
return {
list: [],
current: 1,
sorter: 'weight_desc'
}
},
async created () {
const { data } = await getArticles({
current: this.current,
sorter: this.sorter
})
this.list = data.rows
},
v-for 循环展示
<template>
<div class="article-page">
...
<article-item v-for="(item,i) in list" :key="item.id" :item="item"></article-item>
</div>
</template>
子组件接收渲染
<template>
<van-cell class="article-item" @click="$router.push(`/detail/${item.id}`)">
<template #title>
<div class="head">
<img :src="item.avatar" alt="" />
<div class="con">
<p class="title van-ellipsis">{{ item.stem }}</p>
<p class="other">{{ item.creator }} | {{ item.createdAt }}</p>
</div>
</div>
</template>
<template #label>
<div class="body van-multi-ellipsis--l2">{{ clearHtmlTag(item.content) }}</div>
<div class="foot">点赞 {{ item.likeCount }} | 浏览 {{ item.views }}</div>
</template>
</van-cell>
</template>
<script>
export default {
name: 'article-item',
props: {
item: {
type: Object,
default: () => ({})
}
},
methods: {
clearHtmlTag (str) {
return str.replace(/<[^>]+>/g, '')
}
}
}
</script>
<style lang="less" scoped>
.article-item {
.head {
display: flex;
img {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
}
.con {
flex: 1;
overflow: hidden;
padding-left: 10px;
p {
margin: 0;
line-height: 1.5;
&.title {
width: 280px;
}
&.other {
font-size: 10px;
color: #999;
}
}
}
}
.body {
font-size: 14px;
color: #666;
line-height: 1.6;
margin-top: 10px;
}
.foot {
font-size: 12px;
color: #999;
margin-top: 10px;
}
}
</style>
分页加载更多
https://vant-contrib.gitee.io/vant/v2/#/zh-CN/list
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<article-item v-for="(item,i) in list" :key="i" :item="item"></article-item>
</van-list>
data () {
return {
list: [],
current: 1,
sorter: 'weight_desc',
loading: false,
finished: false
}
},
methods: {
async onLoad () {
const { data } = await getArticles({
current: this.current,
sorter: this.sorter
})
this.list = data.rows
}
}
加载完成,重置 loading, 累加数据,处理 finished
async onLoad () {
const { data } = await getArticles({
current: this.current,
sorter: this.sorter
})
this.list.push(...data.rows)
this.loading = false
this.current++
if (this.current > data.pageTotal) {
this.finished = true
}
}
修改筛选条件
article.vue
<a
@click="changeSorter('weight_desc')"
:class="{ active: sorter === 'weight_desc' }"
href="javascript:;"
>推荐</a
>
<a
@click="changeSorter(null)"
:class="{ active: sorter === null }"
href="javascript:;"
>最新</a
>
提供 methods
changeSorter (value) {
this.sorter = value
// 重置所有条件
this.current = 1 // 排序条件变化,重新从第一页开始加载
this.list = []
this.finished = false // finished重置,重新有数据可以加载了
// this.loading = false
// 手动加载更多
// 手动调用了加载更多,也需要手动将loading改成true,表示正在加载中(避免重复触发)
this.loading = true
this.onLoad()
}
面经详情
静态结构准备
核心知识点:跳转路由传参
准备动态路由
页面中获取参数
this.$route.params.id;
点击跳转 ArticleItem.vue
<template>
+ <van-cell class="article-item" @click="$router.push(`/detail/${item.id}`)">
<!-- ...省略其它代码 -->
+ </van-cell>
</template>
其他准备代码:
main.js
import { Icon } from "vant";
Vue.use(Icon);
Detail.vue
<template>
<div class="detail-page">
<van-nav-bar
left-text="返回"
@click-left="$router.back()"
fixed
title="面经详细"
/>
<header class="header">
<h1>面经标题</h1>
<p>创建时间 | {{ 0 }} 浏览量 | {{ 0 }} 点赞数</p>
<p>
<img src="" alt="" />
<span>作者</span>
</p>
</header>
<main class="body">面经内容</main>
<div class="opt">
<van-icon class="active" name="like-o" />
<van-icon class="active" name="star-o" />
</div>
</div>
</template>
<script>
export default {};
</script>
<style lang="less" scoped>
.detail-page {
margin-top: 44px;
overflow: hidden;
padding: 0 15px;
.header {
h1 {
font-size: 24px;
}
p {
color: #999;
font-size: 12px;
display: flex;
align-items: center;
}
img {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
}
}
.opt {
position: fixed;
bottom: 100px;
right: 0;
> .van-icon {
margin-right: 20px;
background: #fff;
width: 40px;
height: 40px;
line-height: 40px;
text-align: center;
border-radius: 50%;
box-shadow: 2px 2px 10px #ccc;
font-size: 18px;
&.active {
background: #fec635;
color: #fff;
}
}
}
}
</style>
请求数据,渲染界面
api/article.js
export const getDetailByIdAPI = (id) => {
return request({
url: "/h5/interview/show",
params: { id },
});
};
Detaile.vue
<script>
+ import { getDetailByIdAPI } from "@/api/article";
export default {
+ // 2. 声明数据
+ data() {
+ return {
+ detail: {},
+ };
+ },
+ // 3. 创建后,请求数据,保存数据
+ async created() {
+ const res = await getDetailByIdAPI(this.$route.params.id);
+ this.detail = res.data;
+ },
};
</script>
<template>
<div class="detail-page">
// ... 省略其它代码
<header class="header">
+ <!-- 4. 插值显示 -->
+ <h1>{{ detail.stem }}</h1>
<p>
+ <!-- 4. 插值显示 -->
+ {{ detail.createdAt }} | {{ detail.views }} 浏览量 |
+ {{ detail.likeCount }} 点赞数
</p>
<p>
+ <img :src="detail.avatar" alt="" />
+ <span>{{ detail.creator }}</span>
</p>
</header>
+ <!-- 5. 后台返回的是html字符串, 使用v-html显示 -->
+ <main class="body" v-html="detail.content"></main>
<div class="opt">
+ <!-- 6. 动态class控制高亮 -->
+ <van-icon :class="{ active: detail.likeFlag }" name="like-o" />
+ <van-icon :class="{ active: detail.collectFlag }" name="star-o" />
</div>
// ... 省略其它代码
</div>
</template>
点赞文章、取消点赞
- 封装 API
/**
* id: 面经id
* optType: 1 喜欢 , 2 收藏
*/
export const updateDetailAPI = (id, optType) => {
return request({
url: "/h5/interview/opt",
method: "post",
data: {
id,
optType, // 1 喜欢 ,2 收藏
},
});
};
- 绑定点击事件
- 发送请求
- 提示用户
- 更新数据
<template>
+ <!-- 2. 绑定事件, 传参类型:1 -->
<van-icon
:class="{ active: detail.likeFlag }"
name="like-o"
+ @click="update(1)"
/>
</template>
<script>
+ import { getDetailByIdAPI, updateDetailAPI } from "@/api/article";
export default {
// ... 省略其它代码
+ methods: {
+ // 3. 封装方法, 调用API
+ async update(optType) {
+ await updateDetailAPI(this.$route.params.id, optType);
+ // 4. 提示用户
+ this.$toast(this.detail.likeFlag ? "取消点赞" : "点赞成功");
+ // 5.1 修改数据
+ this.detail.likeCount = this.detail.likeFlag
+ ? this.detail.likeCount - 1
+ : (this.detail.likeCount = 1);
+ // 5.2 修改数据
+ this.detail.likeFlag = !this.detail.likeFlag;
+ },
+ },
};
收藏文章、取消收藏
复用 API
绑定点击事件,复用更新方法, 传参 2
发送请求
提示用户
更新数据
<template>
+ <!-- 2. 绑定事件, 复用方法,传参类型:2 -->
<van-icon
:class="{ active: detail.collectFlag }"
name="star-o"
+ @click="update(2)"
/>
</template>
<script>
// ... 省略其它代码
export default {
// ... 省略其它代码
methods: {
async update(optType) {
await updateDetailAPI(this.$route.params.id, optType);
+ // 2. 根据optType区分逻辑
+ if (optType === 1) {
this.$toast(this.detail.likeFlag ? "取消点赞" : "点赞成功");
this.detail.likeCount = this.detail.likeFlag
? this.detail.likeCount - 1
: (this.detail.likeCount = 1);
this.detail.likeFlag = !this.detail.likeFlag;
+ // 3. 加个return 区分不同逻辑
+ return;
+ }
+ // 4. 增加收藏的逻辑
+ this.$toast(this.detail.collectFlag ? "取消收藏" : "收藏成功");
+ this.detail.collectFlag = !this.detail.collectFlag;
},
},
};
我的收藏
出于项目的完整性,这里会快速实现收藏,喜欢,详情~
Collect.vue
准备结构
<template>
<div class="collect-page">
<van-nav-bar fixed title="我的收藏" />
<article-item v-for="item in list" :key="item.id" :item="item" />
</div>
</template>
<script>
import ArticleItem from "@/components/ArticleItem.vue";
export default {
components: { ArticleItem },
data() {
return {
list: [],
};
},
};
</script>
<style lang="less" scoped>
.collect-page {
margin-bottom: 50px;
margin-top: 44px;
}
</style>
- 封装 API:
src/api/article.js
// 获取我的收藏
export const getCollectAndLikeAPI = (optType) => {
return request({
url: "/h5/interview/opt/list",
params: {
page: 1, // 当前页
pageSize: 1000,
optType: optType, // 2表示收藏, 1表示喜欢
},
});
};
- 请求数据,保存数据
<script>
// ... 省略其它代码
+ import { getCollectAndLikeAPI } from "@/api/article";
export default {
// ... 省略其它代码
+ async created() {
+ const res = await getCollectAndLikeAPI(2);
+ this.list = res.data.rows;
+ },
};
</script>
我的(个人中心)
静态结构
1 注册组件
import { Grid, GridItem, CellGroup } from "vant";
Vue.use(Grid);
Vue.use(GridItem);
Vue.use(CellGroup);
- 静态结构
<template>
<div class="user-page">
<div class="user">
<img src="" alt="" />
<h3>用户名</h3>
</div>
<van-grid clickable :column-num="3" :border="false">
<van-grid-item icon="clock-o" text="历史记录" to="/" />
<van-grid-item icon="bookmark-o" text="我的收藏" to="/collect" />
<van-grid-item icon="thumb-circle-o" text="我的点赞" to="/like" />
</van-grid>
<van-cell-group class="mt20">
<van-cell title="推荐分享" is-link />
<van-cell title="意见反馈" is-link />
<van-cell title="关于我们" is-link />
<van-cell title="退出登录" is-link />
</van-cell-group>
</div>
</template>
<script>
export default {};
</script>
<style lang="less" scoped>
.user-page {
padding: 0 10px;
background: #f5f5f5;
height: calc(100vh - 50px);
.mt20 {
margin-top: 20px;
}
.user {
display: flex;
padding: 20px 0;
align-items: center;
img {
width: 80px;
height: 80px;
border-radius: 50%;
overflow: hidden;
}
h3 {
margin: 0;
padding-left: 20px;
font-size: 18px;
}
}
}
</style>
请求数据、渲染界面
准备 api
api/user.js
// 获取用户信息
export const getUserInfoAPI = () => {
return request({ url: "/h5/user/currentUser" });
};
- 页面调用渲染
<template>
<div class="user-page">
<div class="user">
+ <img :src="avatar" alt="" />
+ <h3>{{ username }}</h3>
</div>
<van-grid clickable :column-num="3" :border="false">
<van-grid-item icon="clock-o" text="历史记录" to="/" />
<van-grid-item icon="bookmark-o" text="我的收藏" to="/collect" />
<van-grid-item icon="thumb-circle-o" text="我的点赞" to="/like" />
</van-grid>
<van-cell-group class="mt20">
<van-cell title="推荐分享" is-link />
<van-cell title="意见反馈" is-link />
<van-cell title="关于我们" is-link />
<van-cell title="退出登录" is-link />
</van-cell-group>
</div>
</template>
<script>
+ import { getUserInfo } from '@/api/user'
export default {
+ data () {
+ return {
+ username: '',
+ avatar: ''
+ }
+ },
+ async created () {
+ const { data } = await getUserInfo()
+ this.username = data.username
+ this.avatar = data.avatar
+ },
}
</script>
实现退出登录
路由懒加载 & 异步组件
路由懒加载 & 异步组件, 不会一上来就将所有的组件都加载,而是访问到对应的路由了,才加载解析这个路由对应的所有组件
官网链接:https://router.vuejs.org/zh/guide/advanced/lazy-loading.html#使用-webpack
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
const Layout = () => import("@/views/Layout");
const Detail = () => import("@/views/Detail");
const Register = () => import("@/views/Register");
const Login = () => import("@/views/Login");
const Article = () => import("@/views/Article");
const Collect = () => import("@/views/Collect");
const Like = () => import("@/views/Like");
const User = () => import("@/views/User");
自主实现
优化-响应拦截器-处理 token 过期
utils/request.js
import { delToken, getToken } from "./storage";
// ... 省略代码
// 添加响应拦截器
request.interceptors.response.use(
function (response) {
// 对响应数据做点什么
return response.data;
},
function (error) {
if (error.response) {
// 有错误响应, 提示错误提示
if (error.response.status === 401) {
delToken();
router.push("/login");
} else {
Toast(error.response.data.message);
}
}
// 对响应错误做点什么
return Promise.reject(error);
}
);
我的喜欢
我的喜欢与我的收藏,几乎一模一样,自行实现
- 静态结构
<template>
<div class="like-page">
<van-nav-bar fixed title="我的收藏" />
<article-item v-for="item in list" :key="item.id" :item="item" />
</div>
</template>
<script>
import ArticleItem from "@/components/ArticleItem.vue";
export default {
components: { ArticleItem },
data() {
return {
list: [],
};
},
};
</script>
<style lang="less" scoped>
.like-page {
margin-bottom: 50px;
margin-top: 44px;
}
</style>
- 复用 API 函数 -不需要重复实现
api/article.js
// 获取我的收藏
export const getCollectAndLikeAPI = (optType) => {
return request.get("/h5/interview/opt/list", {
params: {
page: 1, // 当前页
pageSize: 1000,
optType: optType, // 2表示收藏, 1表示喜欢
},
});
};
- 请求数据、保存数据
<script>
// ... 省略其它代码
+ import { getCollectAndLikeAPI } from "@/api/article";
export default {
// ... 省略其它代码
+ async created() {
+ const res = await getCollectAndLikeAPI(2);
+ this.list = res.data.rows;
+ },
};
</script>