Skip to content
On this page

面经 H5 端 - Vant(下)

核心定位:

  1. 熟悉 项目架子: api 模块 (封装请求函数) request 请求封装(axios) storage 封装 路由练习
  2. 熟悉 组件库 使用: 复制文档内容,粘贴到合适位置,看文档改改

面经列表

静态结构

image-20220614074054380

注册组件:

  • van-cell
  • van-list
js
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

jsx
<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面, 不多说, 直接上题&nbsp;一面
        </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 提供接口函数

jsx
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",
    },
  });
};

页面中调用测试

jsx
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 标识前缀

jsx
// 封装接口,获取文章列表
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 更方便

jsx
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 组件

jsx
<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面, 不多说, 直接上题&nbsp;一面
      </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>

注册成全局组件使用

jsx
import ArticleItem from "@/components/article-item.vue";
Vue.component("ArticleItem", ArticleItem);

Article.vue页面中

vue
<template>
  <div class="article-page">
    ...

    <article-item></article-item>
  </div>
</template>

动态渲染列表

article.vue

存储数据

jsx
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 循环展示

jsx
<template>
  <div class="article-page">
    ...

    <article-item v-for="(item,i) in list" :key="item.id" :item="item"></article-item>
  </div>
</template>

子组件接收渲染

jsx
<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

image-20220614081410184

jsx
<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

jsx
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

jsx
<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

jsx
changeSorter (value) {
  this.sorter = value

  // 重置所有条件
  this.current = 1 // 排序条件变化,重新从第一页开始加载
  this.list = []
  this.finished = false // finished重置,重新有数据可以加载了
  // this.loading = false

  // 手动加载更多
  // 手动调用了加载更多,也需要手动将loading改成true,表示正在加载中(避免重复触发)
  this.loading = true
  this.onLoad()
}

面经详情

静态结构准备

核心知识点:跳转路由传参

准备动态路由

image-20220702082707782

页面中获取参数

jsx
this.$route.params.id;

点击跳转 ArticleItem.vue

diff
<template>

+  <van-cell class="article-item" @click="$router.push(`/detail/${item.id}`)">
    <!-- ...省略其它代码 -->
+ </van-cell>

</template>

其他准备代码:

main.js

js
import { Icon } from "vant";
Vue.use(Icon);

Detail.vue

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

js
export const getDetailByIdAPI = (id) => {
  return request({
    url: "/h5/interview/show",
    params: { id },
  });
};

Detaile.vue

diff
<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>

点赞文章、取消点赞

  1. 封装 API
js
/**
 * id: 面经id
 * optType: 1 喜欢 , 2 收藏
 */
export const updateDetailAPI = (id, optType) => {
  return request({
    url: "/h5/interview/opt",
    method: "post",
    data: {
      id,
      optType, // 1 喜欢 ,2  收藏
    },
  });
};
  1. 绑定点击事件
  2. 发送请求
  3. 提示用户
  4. 更新数据
diff
<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;
+    },
+  },
};

收藏文章、取消收藏

  1. 复用 API

  2. 绑定点击事件,复用更新方法, 传参 2

  3. 发送请求

  4. 提示用户

  5. 更新数据

diff
<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;

    },
  },
};

我的收藏

出于项目的完整性,这里会快速实现收藏,喜欢,详情~

  1. Collect.vue准备结构
jsx
<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>
  1. 封装 API:src/api/article.js
js
// 获取我的收藏
export const getCollectAndLikeAPI = (optType) => {
  return request({
    url: "/h5/interview/opt/list",
    params: {
      page: 1, // 当前页
      pageSize: 1000,
      optType: optType, // 2表示收藏, 1表示喜欢
    },
  });
};
  1. 请求数据,保存数据
diff
<script>
// ... 省略其它代码

+ import { getCollectAndLikeAPI } from "@/api/article";

export default {
    // ... 省略其它代码

+  async created() {
+    const res = await getCollectAndLikeAPI(2);
+    this.list = res.data.rows;
+  },
};
</script>

我的(个人中心)

静态结构

1 注册组件

jsx
import { Grid, GridItem, CellGroup } from "vant";

Vue.use(Grid);
Vue.use(GridItem);
Vue.use(CellGroup);
  1. 静态结构
vue
<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

  1. api/user.js
jsx
// 获取用户信息
export const getUserInfoAPI = () => {
  return request({ url: "/h5/user/currentUser" });
};
  1. 页面调用渲染
diff
<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 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。

js
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

jsx
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);
  }
);

我的喜欢

我的喜欢与我的收藏,几乎一模一样,自行实现

  1. 静态结构
vue
<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>
  1. 复用 API 函数 -不需要重复实现

api/article.js

jsx
// 获取我的收藏
export const getCollectAndLikeAPI = (optType) => {
  return request.get("/h5/interview/opt/list", {
    params: {
      page: 1, // 当前页
      pageSize: 1000,
      optType: optType, // 2表示收藏, 1表示喜欢
    },
  });
};
  1. 请求数据、保存数据
diff
<script>
// ... 省略其它代码

+ import { getCollectAndLikeAPI } from "@/api/article";

export default {
    // ... 省略其它代码

+  async created() {
+    const res = await getCollectAndLikeAPI(2);
+    this.list = res.data.rows;
+  },

};
</script>

Released under the MIT License.