Skip to content

问道移动端项目

接口文档:问道项目 -- h5移动端接口文档

第一章:项目搭建

一、初始化项目

安装 Vue CLI 脚手架:

bash
npm install -g @vue/cli

使用如下命令检查是否安装成功:

bash
vue --version

Vue CLI 创建新项目:

bash
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

等待安装,项目初始化完成。

运行项目:

bash
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节点中加入如下规则:

json
"rules": {
  "semi": ["error", "always"] # 控制代码中是否需要使用分号
}

2. 自动格式化代码

插件名:Prettier-Standard - JavaScript formatter

安装地址:Prettier-Standard - JavaScript formatter

在 VSCode 设置里添加如下内容,这样就可以使用 Prettier-Standard - JavaScript formatter 插件格式化代码了。

json
// 代码缩进 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)安装

bash
npm i vant@latest-v2

# 安装如果有报错,则换为
npm i vant@latest-v2 --legacy-peer-deps

2)导入所有组件 @/main.js

javascript
import Vue from 'vue';
import Vant from 'vant';
import 'vant/lib/index.css';

Vue.use(Vant);

2. vw 适配

官方说明:Vant 2 浏览器适配

bash
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

javascript
// 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)引入样式源文件

javascript
// 引入全部样式
import 'vant/lib/index.less'

2)修改样式变量

如果 vue-cli 搭建的项目,可以在 vue.config.js 中进行配置。

javascript
// 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)安装

bash
npm i axios --legacy-peer-deps

2)在 utils/request.js 封装 axios

javascript
// 这里克隆一份新的 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 配置路由

javascript
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 内使用

js
import { Toast } from 'vant'; // 按需导入需要使用这句

Toast('提示内容'); // 普通的提示
Toast.success('xxxxx') // 成功的提示
Toast.fail('xxxx') // 失败的提示

2)组件内,通过 this 直接调用

引入 Toast 组件后,会自动在 Vue 的 prototype 上挂载 $toast 方法,便于在组件内调用。

js
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

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

javascript
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。

javascript
// 按需导入 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

javascript
// 注册的请求方法
export const registerAPI = (data) => {
  return request.post('/h5/user/register', data)
}

2)@/views/Register.vue

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 实现。

vue
<!-- 下面的导航条,放到哪里都可以,因为它始终定位到底部 -->
<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

vue
<van-cell>
  <template #title>
    <!-- 内容 -->
  </template>
  <template #label>
    <!-- 内容 -->
  </template>
</van-cell>

把上面这个封装为组件到 @/components/ArticleItem.vue

props 接受传过来的数据,展示。

2)Article.vue 展示

@/views/Layout/Article.vue

vue
<ArticleItem v-for="item in articles" :key="item.id" :item="item" />

2. 网络请求

@/api/article.js

javascript
// 获取文章列表
export const getArticlesAPI = (params) => {
  return request.get('/h5/interview/query', { params })
}

3. 下拉加载

List 列表:https://vant-ui.github.io/vant/v2/#/zh-CN/list

vue
<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 即可。

javascript
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

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>
javascript
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()
    }
  }
}

第五章:详情页

一、页面布局

编写界面样式。

点我查看代码
vue
<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

vue
@click="$router.push(`detail?id=${item.id}`)"

2)Detail.vue

js
this.$route.query.id

三、封装 API,获取数据

@/api/article.js

js
// 获取文章详情
export const getArticleDetailAPI = (params) => {
  return request.get('/h5/interview/show', { params })
}

@views/Detail.vue

js
async created () { // 在 created 生命周期中完成网络请求
  const res = await getArticleDetailAPI({ id: this.$route.query.id })
  this.article = res.data.data
}

四、展示数据

vue
<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。

js
// 收藏与点赞
export const updateLikeAndCollect = (data) => {
  return request.post('/h5/interview/opt', data)
}

2)@views/Detail.vue 实现功能。

vue
<van-icon
  @click="toggleLike"
  :class="{ active: article.likeFlag }"
  name="like-o"
/>
<van-icon
  @click="toggleCollect"
  :class="{ active: article.collectFlag }"
  name="star-o"
/>
js
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。

js
// 获取收藏与点赞列表
export const collectAndLikeListAPI = (params) => {
  return request.get('/h5/interview/opt/list', {
    params: params
  })
}

2)@views/Collect.vue 实现功能。

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 即可。

Released under the MIT License.