问道移动端项目
接口文档:问道项目 -- h5移动端接口文档
第一章:项目搭建
一、初始化项目
安装 Vue CLI 脚手架:
npm install -g @vue/cli使用如下命令检查是否安装成功:
vue --versionVue CLI 创建新项目:
vue create wd-mobile-vue2-vant2 -m npm之后会询问如下信息:

1️⃣ 手动选择功能

2️⃣ 选择 vue 的版本

3️⃣ 是否使用 history 模式

4️⃣ 选择 css 预处理

5️⃣ 选择 eslint 的风格(eslint 代码规范的检验工具,检验代码是否符合规范)
比如:const age = 18; => 报错!多加了分号!后面有工具,一保存,全部格式化成最规范的样子。

6️⃣ 选择校验的时机 (直接回车)

7️⃣ 选择配置文件的生成方式 (直接回车)

8️⃣ 是否保存预设,下次直接使用? => 不保存,输入 N

等待安装,项目初始化完成。
运行项目:
npm run serve目录结构

二、ESLint
1. 代码检查
使用 VSCode 插件,在编写代码的时候就检查格式,而不是 webpack 打包的时候。

是什么
官方概念:ESLint 是可组装的 JavaScript 和 JSX 检查工具。
通俗理解:一个工具,用来约束团队成员的代码风格。
当通过 @vue/cli 脚手架工具安装项目后,默认已经将 eslint 相关的包安装并配置好了。
我们将使用 vue 的 eslint 插件规定的默认规则进行代码检查。
如果需要查看规则,则可以查看 https://eslint.nodejs.cn/docs/latest/rules/。
怎么增加配置
关于 ESLint 的配置,需要放到配置文件 .eslintrc.js 中。
打开 .eslintrc.js 文件,找到里面的 rules 节点,这个 rules 节点,用于自定义 ESLint 规则。
比如,我们配置每条语句结束,必须加分号,则需要在 rules节点中加入如下规则:
"rules": {
"semi": ["error", "always"] # 控制代码中是否需要使用分号
}2. 自动格式化代码
插件名:Prettier-Standard - JavaScript formatter
安装地址:Prettier-Standard - JavaScript formatter
在 VSCode 设置里添加如下内容,这样就可以使用 Prettier-Standard - JavaScript formatter 插件格式化代码了。
// 代码缩进 2 个空格
"editor.tabSize": 2,
// 保存(包括自动保存),自动格式化
"editor.formatOnSave": true,
// 按Ctrl + S保存时,自动修复
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
// 编辑器窗口失去焦点时,自动保存代码【可选】
"files.autoSave": "onFocusChange",
// 配置vue默认的格式化程序【必须的配置】
"[vue]": {
"editor.defaultFormatter": "numso.prettier-standard-vscode"
},
// 设置JS文件的默认格式化程序【必须的配置】
"[javascript]": {
"editor.defaultFormatter": "numso.prettier-standard-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "numso.prettier-standard-vscode"
},三、使用 vant
1. 引入
参考文档:Vant 2
1)安装
npm i vant@latest-v2
# 安装如果有报错,则换为
npm i vant@latest-v2 --legacy-peer-deps2)导入所有组件 @/main.js
import Vue from 'vue';
import Vant from 'vant';
import 'vant/lib/index.css';
Vue.use(Vant);2. vw 适配
官方说明:Vant 2 浏览器适配
npm i postcss-px-to-viewport@1.1.1 -D
# 安装报错,则:
npm i postcss-px-to-viewport@1.1.1 -D --legacy-peer-deps项目根目录, 新建 postcss 的配置文件 postcss.config.js
// postcss.config.js
module.exports = {
plugins: {
'postcss-px-to-viewport': {
// 设计稿如果是2倍图,宽是750,则 750/2 = 375,下面就写375
// 设计稿如果是3倍图,宽是 1080,则 1080/3 = 360,下面就写360
viewportWidth: 375
}
}
}3. 定制主题
Vant 文档:定制主题
1)引入样式源文件
// 引入全部样式
import 'vant/lib/index.less'2)修改样式变量
如果 vue-cli 搭建的项目,可以在 vue.config.js 中进行配置。
// vue.config.js
module.exports = {
css: {
loaderOptions: {
less: {
// 若 less-loader 版本小于 6.0,请移除 lessOptions 这一级,直接配置选项。
lessOptions: {
modifyVars: {
/*
// 直接覆盖变量
'text-color': '#111',
'border-color': '#eee',
// 或者可以通过 less 文件覆盖(文件路径为绝对路径)
hack: `true; @import "your-less-file-path.less";`,
*/
blue: 'orange'
}
}
}
}
}
};四、引入 axios

1)安装
npm i axios --legacy-peer-deps2)在 utils/request.js 封装 axios
// 这里克隆一份新的 axios,并配置它,最后导出
import axios from 'axios'
// 克隆一份新的 axios
const request = axios.create({
baseURL: 'http://interview-api-t.itheima.net',
timeout: 5000 // 超时时间,超过5秒,如果请求还没有完成,则取消
})
// 还可以给 request 配置拦截器等等
// 最后导出
export default request五、路由配置

1)在 @/router/imdex.js 配置路由
const routes = [
{
path: '/',
component: () => import('@/views/Home.vue'),
redirect: '/article',
children: [
// 面经列表页
{ path: 'article', component: () => import('@/views/Layout/Article') },
// 我的收藏页
{ path: 'collect', component: () => import('@/views/Layout/Collect') },
// 我的喜欢页
{ path: 'like', component: () => import('@/views/Layout/Like') },
// 用户中心页
{ path: 'user', component: () => import('@/views/Layout/User') }
]
},
// 面经详情页
{ path: '/detail', component: () => import('@/views/Detail.vue') },
// 登录页
{ path: '/login', component: () => import('@/views/Login.vue') },
// 注册页
{ path: '/register', component: () => import('@/views/Register.vue') }
]2)在 App.vue 中和 Home.vue 中,分别加入 <router-view></router-view>。
第二章:登录 & 注册
一、登录
1. toast 轻提示
Toast 轻提示:https://vant-ui.github.io/vant/v2/#/zh-CN/toast#quan-ju-fang-fa
两种使用方式
1)js 内使用
import { Toast } from 'vant'; // 按需导入需要使用这句
Toast('提示内容'); // 普通的提示
Toast.success('xxxxx') // 成功的提示
Toast.fail('xxxx') // 失败的提示2)组件内,通过 this 直接调用
引入 Toast 组件后,会自动在 Vue 的 prototype 上挂载 $toast 方法,便于在组件内调用。
this.$toast('提示内容') // 普通的提示
this.$toast.success('登录成功') // 成功的提示
this.$toast.fail('xxxx') // 失败的提示2. 页面布局
NavBar 导航栏:https://vant-ui.github.io/vant/v2/#/zh-CN/nav-bar
Form 表单:https://vant-ui.github.io/vant/v2/#/zh-CN/form
<template>
<div class="login-page">
<van-nav-bar title="问道登录" />
<van-form @submit="onSubmit">
<van-field v-model="username" name="username" label="用户名" placeholder="请输入用户名"
:rules="userRules" />
<van-field v-model="password" type="password" name="password" label="密码" placeholder="请输入密码"
:rules="[{ required: true, message: '必填项' }, { pattern: /^\S{6,20}$/, message: '长度在 6 到 20 个字符' }]" />
<div style="margin: 16px;">
<van-button block type="info" native-type="submit">提交</van-button>
</div>
</van-form>
<div class="link">
还没有账号?<router-link to="/register">去注册</router-link>
</div>
</div>
</template>
<script>
export default {
name: 'login-page',
data () {
return {
userRules: [
{ required: true, message: '必填项' },
{ pattern: /^\w{3,12}$/, message: '长度在 3 到 6 个字符' }
],
username: '',
password: ''
}
},
methods: {
onSubmit (values) {
console.log('submit', values)
}
}
}
</script>
<style lang="less" scoped>
.link {
margin: 20px 20px 0 0;
font-size: 14px;
text-align: end;
}
</style>3. 网络请求
1)统一管理请求路径
@/api/user.js
import request from '@/utils/request'
// 下面封装各个请求
// 登录的请求方法
export const loginAPI = (data) => {
// 发送ajax请求,以前是 axios.post(),现在改为 request.post()
// return request.post('接口地址', '提交的数据')
return request.post('/h5/user/login', data)
}2)在登录组件(Login.vue)编写业务逻辑
修改两个输入框的 name,改为 username 和 password。
// 按需导入 API 方法
import { loginAPI } from '@/api/user'
export default {
name: 'login-page',
data () {
return {
username: '',
password: ''
}
},
methods: {
// 点击登录的时候,执行此方法
async onSubmit (values) {
try {
const { data: res } = await loginAPI(values)
localStorage.setItem('mobile-token', res.data.token) // 存储token
this.$toast('登录成功') // 提示
this.username = this.password = '' // 清空输入框
this.$router.push('/article') // 跳转到面经列表页
} catch (err) {
if (err.response) {
this.$toast(err.response.data.message)
} else {
this.$toast('登录失败')
}
}
}
}
}二、注册
1)@/api/user.js
// 注册的请求方法
export const registerAPI = (data) => {
return request.post('/h5/user/register', data)
}2)@/views/Register.vue
<template>
<div class="register">
<van-nav-bar title="问道用户注册" />
<van-form @submit="onSubmit">
<van-field v-model="username" name="username" label="用户名" placeholder="请输入用户名" :rules="userRules" />
<van-field v-model="password" type="password" name="password" label="密码" placeholder="请输入密码"
:rules="[{ required: true, message: '必填项' }, { pattern: /^\S{6,20}$/, message: '长度在 6 到 20 个字符' }]" />
<div style="margin: 16px;">
<van-button block type="info" native-type="submit">提交</van-button>
</div>
</van-form>
<div class="link">
已有账号?<router-link to="/login">去登录</router-link>
</div>
</div>
</template>
<script>
import { registerAPI } from '@/api/user'
export default {
name: 'register-page',
data () {
return {
userRules: [
{ required: true, message: '必填项' },
{ pattern: /^\w{3,12}$/, message: '长度在 3 到 6 个字符' }
],
username: '',
password: ''
}
},
methods: {
// 点击注册的时候,执行。
async onSubmit (values) {
try {
// values ==== {username: 'laotang', password: '123123'}
// 调用 registerAPI 发送请求
await registerAPI(values)
this.$toast('注册成功') // 提示
this.username = this.password = '' // 重置表单
this.$router.push('/login') // 跳转到登录
} catch (err) {
// 注册失败,提示信息
if (err.response) {
// 如果有响应结果,则提示响应结果中的信息
this.$toast(err.response.data.message)
} else {
this.$toast('注册失败') // 如果没有响应结果,则笼统的提示一下
}
}
}
}
}
</script>
<style lang="less" scoped>
.link {
margin: 20px 20px 0 0;
font-size: 14px;
text-align: end;
}
</style>第三章:首页
底部导航栏使用 Vant 2 的 Tabbar
观察 tabbar 的用法介绍,发现有路由模式。
- 去掉
<van-tabbar>的v-model属性 - 给
<van-tabbar>加入route属性 - 给
<van-tabbar-item>加入to="/xxx"属性
图标,也使用 vant 提供的 icon 实现。
<!-- 下面的导航条,放到哪里都可以,因为它始终定位到底部 -->
<van-tabbar route>
<van-tabbar-item to="/article" icon="notes-o">面经</van-tabbar-item>
<van-tabbar-item to="/collect" icon="star-o">收藏</van-tabbar-item>
<van-tabbar-item to="/like" icon="like-o">喜欢</van-tabbar-item>
<van-tabbar-item to="/user" icon="user-o">我的</van-tabbar-item>
</van-tabbar>第四章:面经列表
一、文章展示
1. 页面布局
1)封装 ArticleItem.vue
Cell 单元格:https://vant-ui.github.io/vant/v2/#/zh-CN/cell
<van-cell>
<template #title>
<!-- 内容 -->
</template>
<template #label>
<!-- 内容 -->
</template>
</van-cell>把上面这个封装为组件到 @/components/ArticleItem.vue
props 接受传过来的数据,展示。
2)Article.vue 展示
@/views/Layout/Article.vue
<ArticleItem v-for="item in articles" :key="item.id" :item="item" />2. 网络请求
@/api/article.js
// 获取文章列表
export const getArticlesAPI = (params) => {
return request.get('/h5/interview/query', { params })
}3. 下拉加载
List 列表:https://vant-ui.github.io/vant/v2/#/zh-CN/list
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<ArticleItem v-for="item in articles" :key="item.id" :item="item" />
</van-list>List 组件通过 loading 和 finished 两个变量控制加载状态。当组件滚动到底部时,会触发 load 事件并将 loading 设置成 true。此时可以发起异步操作并更新数据,数据更新完毕后,将 loading 设置成 false 即可。若数据已全部加载完毕,则直接将 finished 设置成 true 即可。
export default {
data () {
return {
current: 1,
sorter: 'weight_desc',
articles: [],
loading: false,
finished: false
}
},
methods: {
async onLoad () {
// 异步更新数据
const articles = await getArticlesAPI({
current: this.current,
sorter: this.sorter
})
this.articles.push(...articles.data.data.rows)
// 数据全部加载完成
this.loading = false
this.current++
if (this.current > articles.data.data.pageTotal) {
this.finished = true
}
}
}
}二、切换类别
@/views/Layout/Article.vue
<nav class="my-nav van-hairline--bottom">
<a
@click="changeSorter('weight_desc')"
:class="{ active: sorter === 'weight_desc' }"
href="javascript:;"
>推荐</a
>
<a
@click="changeSorter(null)"
:class="{ active: sorter === null }"
href="javascript:;"
>最新</a
>
<div class="logo"><img src="@/assets/logo.png" alt="" /></div>
</nav>export default {
data () {
return {
current: 1,
sorter: 'weight_desc',
articles: [],
loading: false,
finished: false
}
},
methods: {
changeSorter (sorter) {
this.sorter = sorter
this.current = 1
this.articles = []
this.loading = true // 避免同时发送两次请求
this.onLoad()
}
}
}第五章:详情页
一、页面布局
编写界面样式。
点我查看代码
<template>
<div class="detail-page">
<van-nav-bar
left-text="返回"
@click-left="$router.back()"
fixed
title="面经详细"
/>
<header class="header">
<h1>标题</h1>
<p>创建时间 | 432 浏览量 | 45 点赞数</p>
<p>
<img src="123.jpg" alt />
<span>作者</span>
</p>
</header>
<main class="body">内容</main>
<div class="opt">
<van-icon name="like-o" />
<van-icon name="star-o" />
</div>
</div>
</template>
<script>
export default {
name: 'detail-page'
}
</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>二、详情页拿到 ID
1)ArticleItem.vue
@click="$router.push(`detail?id=${item.id}`)"2)Detail.vue
this.$route.query.id三、封装 API,获取数据
@/api/article.js
// 获取文章详情
export const getArticleDetailAPI = (params) => {
return request.get('/h5/interview/show', { params })
}@views/Detail.vue
async created () { // 在 created 生命周期中完成网络请求
const res = await getArticleDetailAPI({ id: this.$route.query.id })
this.article = res.data.data
}四、展示数据
<template>
<div class="detail-page">
<van-nav-bar
left-text="返回"
@click-left="$router.back()"
fixed
title="面经详细"
/>
<header class="header">
<h1>{{ article.stem }}</h1>
<p>
{{ article.createdAt }} | {{ article.views }} 浏览量 |
{{ article.likeCount }} 点赞数
</p>
<p>
<img :src="article.avatar" alt />
<span>{{ article.creator }}</span>
</p>
</header>
<main class="body" v-html="article.content"></main>
<div class="opt">
<van-icon
:class="{ active: article.likeFlag }"
name="like-o"
/>
<van-icon
:class="{ active: article.collectFlag }"
name="star-o"
/>
</div>
</div>
</template>四、收藏与点赞
1)@/api/article.js 添加网络请求 API。
// 收藏与点赞
export const updateLikeAndCollect = (data) => {
return request.post('/h5/interview/opt', data)
}2)@views/Detail.vue 实现功能。
<van-icon
@click="toggleLike"
:class="{ active: article.likeFlag }"
name="like-o"
/>
<van-icon
@click="toggleCollect"
:class="{ active: article.collectFlag }"
name="star-o"
/>async toggleLike () {
await updateLikeAndCollect({ id: this.article.id, optType: 1 })
this.article.likeFlag = !this.article.likeFlag
if (this.article.likeFlag) {
this.article.likeCount++
this.$toast.success('点赞成功')
} else {
this.article.likeCount--
this.$toast.success('取消点赞')
}
},
async toggleCollect () {
await updateLikeAndCollect({ id: this.article.id, optType: 2 })
this.article.collectFlag = !this.article.collectFlag
if (this.article.collectFlag) {
this.$toast.success('收藏成功')
} else {
this.$toast.success('取消收藏')
}
}第六章:收藏页与喜欢页
1)@/api/article.js 添加网络请求 API。
// 获取收藏与点赞列表
export const collectAndLikeListAPI = (params) => {
return request.get('/h5/interview/opt/list', {
params: params
})
}2)@views/Collect.vue 实现功能。
<template>
<div class="collect-page">
<van-nav-bar fixed title="我的收藏" />
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<article-item v-for="(item, i) in list" :key="i" :item="item" />
</van-list>
</div>
</template>
<script>
import ArticleItem from '@/components/ArticleItem.vue'
import { collectAndLikeListAPI } from '@/api/article'
export default {
name: 'collect-page',
components: {
ArticleItem
},
data () {
return {
list: [],
loading: false,
finished: false,
page: 1
}
},
methods: {
async onLoad () {
// 异步更新数据
const { data: res } = await collectAndLikeListAPI({
page: this.page,
optType: 2
})
this.list.push(...res.data.rows)
this.loading = false
if (this.page === res.data.pageTotal || !res.data.rows.length) {
this.finished = true
} else {
this.page++
}
}
}
}
</script>
<style lang="less" scoped>
.collect-page {
margin-bottom: 50px;
margin-top: 44px;
}
</style>喜欢页直接复制【我的收藏】代码。修改请求参数 optType: 1 即可。