接口文档地址:https://www.apifox.cn/apidoc/project-934563/api-20384515
启动准备好的代码,演示移动端面经内容,明确功能模块
1.安装脚手架 (已安装)
npm i @vue/cli -g
2.创建项目
vue create hm-vant-h5
Vue CLI v5.0.8
? Please pick a preset:
Default ([Vue 3] babel, eslint)
Default ([Vue 2] babel, eslint)
> Manually select features 选自定义
3.x
> 2.x
npm run serve
代码规范:一套写代码的约定规则。例如:赋值符号的左右是否需要空格?一句结束是否是要加;?…
没有规矩不成方圆
ESLint:是一个代码检查工具,用来检查你的代码是否符合指定的规则(你和你的团队可以自行约定一套规则)。在创建项目时,我们使用的是 JavaScript Standard Style 代码风格的规则。
建议把:https://standardjs.com/rules-zhcn.html 看一遍,然后在写的时候, 遇到错误就查询解决。
下面是这份规则中的一小部分:
if (condition) { ... }
function name (arg) { ... }
===
摒弃 ==
一但在需要检查 null || undefined
时可以使用 obj == null
如果你的代码不符合standard的要求,eslint会跳出来刀子嘴,豆腐心地提示你。
下面我们在main.js中随意做一些改动:添加一些空行,空格。
import Vue from 'vue'
import App from './App.vue'
import './styles/index.less'
import router from './router'
Vue.config.productionTip = false
new Vue ( {
render: h => h(App),
router
}).$mount('#app')
按下保存代码之后:
你将会看在控制台中输出如下错误:
eslint 是来帮助你的。心态要好,有错,就改。
根据错误提示来一项一项手动修正。
如果你不认识命令行中的语法报错是什么意思,你可以根据错误代码(func-call-spacing, space-in-parens,…)去 ESLint 规则列表中查找其具体含义。
打开 ESLint 规则表,使用页面搜索(Ctrl + F)这个代码,查找对该规则的一个释义。
- eslint会自动高亮错误显示
- 通过配置,eslint会自动帮助我们修复错误
// 当保存的时候,eslint自动帮我们修复错误
"editor.codeActionsOnSave": {
"source.fixAll": true
},
// 保存代码,不自动格式化
"editor.formatOnSave": false
settings.json 参考
{
"window.zoomLevel": 2,
"workbench.iconTheme": "vscode-icons",
"editor.tabSize": 2,
"emmet.triggerExpansionOnTab": true,
// 当保存的时候,eslint自动帮我们修复错误
"editor.codeActionsOnSave": {
"source.fixAll": true
},
// 保存代码,不自动格式化
"editor.formatOnSave": false
}
强烈建议大家严格按照老师的步骤进行调整,为了符合企业规范
为了更好的实现后面的操作,我们把整体的目录结构做一些调整。
目标:
main.js
不需要修改
router/index.js
删除默认的路由配置
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
]
const router = new VueRouter({
routes
})
export default router
App.vue
<template>
<div id="app">
<router-view/>
</div>
</template>
目录效果如下:
组件库:第三方封装好了很多很多的组件,整合到一起就是一个组件库。
https://vant-contrib.gitee.io/vant/v2/#/zh-CN/
比如日历组件、键盘组件、打分组件、登录组件等
组件库并不是唯一的,常用的组件库还有以下几种:
pc: element-ui element-plus iview ant-design
移动:vant-ui Mint UI (饿了么) Cube UI (滴滴)
目标:明确 全部导入 和 按需导入 的区别
区别:
1.全部导入会引起项目打包后的体积变大,进而影响用户访问网站的性能
2.按需导入只会导入你使用的组件,进而节约了资源
yarn add vant@latest-v2
// 或者 npm i vant@latest-v2
import Vant from 'vant';
import 'vant/lib/index.css';
// 把vant中所有的组件都导入了
Vue.use(Vant)
<van-button type="primary">主要按钮</van-button>
<van-button type="info">信息按钮</van-button>
vant-ui提供了很多的组件,全部导入,会导致项目打包变得很大。
npm i vant@latest-v2 或 yarn add vant@latest-v2
npm i babel-plugin-import -D
babel.config.js
中配置module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
style: true
}, 'vant']
]
}
main.js
import { Button, Icon } from 'vant'
Vue.use(Button)
Vue.use(Icon)
app.vue
中进行测试<van-button type="primary">主要按钮</van-button>
<van-button type="info">信息按钮</van-button>
<van-button type="default">默认按钮</van-button>
<van-button type="warning">警告按钮</van-button>
<van-button type="danger">危险按钮</van-button>
utils/vant-ui.js
import { Button, Icon } from 'vant'
Vue.use(Button)
Vue.use(Icon)
main.js中进行导入
// 导入按需导入的配置文件
import '@/utils/vant-ui'
官方说明:https://vant-contrib.gitee.io/vant/v2/#/zh-CN/advanced-usage
yarn add postcss-px-to-viewport@1.1.1 -D
postcss.config.js
// postcss.config.js
module.exports = {
plugins: {
'postcss-px-to-viewport': {
viewportWidth: 375,
},
},
};
viewportWidth:设计稿的视口宽度
但凡是单个页面,独立展示的,都是一级路由
路由设计:
router/index.js
配置一级路由, 一级views组件于准备好的中直接 CV 即可
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '@/views/Login'
import Register from '@/views/Register'
import Detail from '@/views/Detail'
import Layout from '@/views/Layout'
Vue.use(VueRouter)
const router = new VueRouter({
routes: [
{ path: '/login', component: Login },
{ path: '/register', component: Register },
{ path: '/article/:id', component: Detail },
{
path: '/',
component: Layout
}
]
})
export default router
清理 App.vue
<template>
<div id="app">
<router-view/>
</div>
</template>
<script>
export default {
created () {
}
}
</script>
https://vant-contrib.gitee.io/vant/v2/#/zh-CN/tabbar
vant-ui.js
引入组件
import { Button, Icon, Tabbar, TabbarItem } from 'vant'
Vue.use(Tabbar)
Vue.use(TabbarItem)
layout.vue
<template>
<div class="layout-page">
首页架子 - 内容区域
<van-tabbar>
<van-tabbar-item icon="notes-o">面经</van-tabbar-item>
<van-tabbar-item icon="star-o">收藏</van-tabbar-item>
<van-tabbar-item icon="like-o">喜欢</van-tabbar-item>
<van-tabbar-item icon="user-o">我的</van-tabbar-item>
</van-tabbar>
</div>
</template>
整体网站风格,其实都是橙色的,可以通过变量覆盖的方式,制定主题色
https://vant-contrib.gitee.io/vant/v2/#/zh-CN/theme
babel.config.js
制定样式路径
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
// 指定样式路径
style: (name) => `${name}/style/less`
}, 'vant']
]
}
vue.config.js
覆盖变量
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
css: {
loaderOptions: {
less: {
lessOptions: {
modifyVars: {
// 直接覆盖变量
'blue': '#FA6D1D',
},
},
},
},
},
})
重启服务器生效!
1.router/index.js
配置二级路由
在准备好的代码中去复制对应的组件即可
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '@/views/Login'
import Register from '@/views/Register'
import Detail from '@/views/Detail'
import Layout from '@/views/Layout'
import Like from '@/views/Like'
import Article from '@/views/Article'
import Collect from '@/views/Collect'
import User from '@/views/User'
Vue.use(VueRouter)
const router = new VueRouter({
routes: [
{ path: '/login', component: Login },
{ path: '/register', component: Register },
{ path: '/article/:id', component: Detail },
{
path: '/',
component: Layout,
redirect: '/article',
children: [
{ path: 'article', component: Article },
{ path: 'like', component: Like },
{ path: 'collect', component: Collect },
{ path: 'user', component: User }
]
}
]
})
export default router
2.layout.vue
配置路由出口, 配置 tabbar
<template>
<div class="layout-page">
//路由出口
<router-view></router-view>
<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>
</div>
</template>
使用组件
vant-ui.js
注册
import Vue from 'vue'
import {
NavBar,
Form,
Field
} from 'vant'
Vue.use(NavBar)
Vue.use(Form)
Vue.use(Field)
Login.vue
使用
<template>
<div class="login-page">
<!-- 导航栏部分 -->
<van-nav-bar title="面经登录" />
<!-- 一旦form表单提交了,就会触发submit,可以在submit事件中
根据拿到的表单提交信息,发送axios请求
-->
<van-form @submit="onSubmit">
<!-- 输入框组件 -->
<!-- \w 字母数字_ \d 数字0-9 -->
<van-field
v-model="username"
name="username"
label="用户名"
placeholder="用户名"
:rules="[
{ required: true, message: '请填写用户名' },
{ pattern: /^\w{5,}$/, message: '用户名至少包含5个字符' }
]"
/>
<van-field
v-model="password"
type="password"
name="password"
label="密码"
placeholder="密码"
:rules="[
{ required: true, message: '请填写密码' },
{ pattern: /^\w{6,}$/, message: '密码至少包含6个字符' }
]"
/>
<div style="margin: 16px">
<van-button block type="info" native-type="submit">提交</van-button>
</div>
</van-form>
</div>
</template>
<script>
export default {
name: 'LoginPage',
data () {
return {
username: 'zhousg',
password: '123456'
}
},
methods: {
onSubmit (values) {
console.log('submit', values)
}
}
}
</script>
login.vue
添加 router-link 标签(跳转到注册)
<template>
<div class="login-page">
<van-nav-bar title="面经登录" />
<van-form @submit="onSubmit">
...
</van-form>
<router-link class="link" to="/register">注册账号</router-link>
</div>
</template>
login.vue
调整样式
<style lang="less" scoped>
.link {
color: #069;
font-size: 12px;
padding-right: 20px;
float: right;
}
</style>
Register.vue
<template>
<div class="login-page">
<van-nav-bar title="面经注册" />
<van-form @submit="onSubmit">
<van-field
v-model="username"
name="username"
label="用户名"
placeholder="用户名"
:rules="[
{ required: true, message: '请填写用户名' },
{ pattern: /^\w{5,}$/, message: '用户名至少包含5个字符' }
]"
/>
<van-field
v-model="password"
type="password"
name="password"
label="密码"
placeholder="密码"
:rules="[
{ required: true, message: '请填写密码' },
{ pattern: /^\w{6,}$/, message: '密码至少包含6个字符' }
]"
/>
<div style="margin: 16px">
<van-button block type="primary" native-type="submit"
>注册</van-button
>
</div>
</van-form>
<router-link class="link" to="/login">有账号,去登录</router-link>
</div>
</template>
<script>
export default {
name: 'Register-Page',
data () {
return {
username: '',
password: ''
}
},
methods: {
onSubmit (values) {
console.log('submit', values)
}
}
}
</script>
<style lang="less" scoped>
.link {
color: #069;
font-size: 12px;
padding-right: 20px;
float: right;
}
</style>
接口文档地址:https://apifox.com/apidoc/project-934563/api-20384515
基地址:http://interview-api-t.itheima.net/h5/
我们会使用 axios 来请求后端接口, 一般都会对 axios 进行一些配置 (比如: 配置基础地址,请求响应拦截器等等)
一般项目开发中, 都会对 axios 进行基本的二次封装, 单独封装到一个模块中, 便于使用
npm i axios
新建 utils/request.js
封装 axios 模块
利用 axios.create 创建一个自定义的 axios 来使用
http://www.axios-js.com/zh-cn/docs/#axios-create-config
/* 封装axios用于发送请求 */
import axios from 'axios'
// 创建一个新的axios实例
const request = axios.create({
baseURL: 'http://interview-api-t.itheima.net/h5/',
timeout: 5000
})
// 添加请求拦截器
request.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error)
})
// 添加响应拦截器
request.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response.data
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error)
})
export default request
// 监听表单的提交,形参中:可以获取到输入框的值
async onSubmit (values) {
console.log('submit', values)
const res = await request.post('/user/register', values)
console.log(res)
}
以前的模式:
新建 api/user.js
提供注册 Api 函数
import request from '@/utils/request'
// 注册接口
export const register = (data) => {
return request.post('/user/register', data)
}
register.vue
页面中调用测试
methods: {
async onSubmit (values) {
// 往后台发送注册请求了
await register(values)
alert('注册成功')
this.$router.push('/login')
}
}
https://vant-contrib.gitee.io/vant/v2/#/zh-CN/toast
两种使用方式
import { Toast } from 'vant';
Toast('提示内容');
main.js
import { Toast } from 'vant';
Vue.use(Toast)
this.$toast('提示内容')
代码演示
this.$toast.loading({
message:'拼命加载中...',
forbidClick:true
})
try{
await register(values)
this.$toast.success('注册成功')
this.$router.push('/login')
}catch(e){
this.$toast.fail('注册失败')
}
响应拦截器是咱们拿到数据的第一个“数据流转站”
import { Toast } from 'vant'
...
// 添加响应拦截器
request.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response.data
}, function (error) {
if (error.response) {
// 有错误响应, 提示错误提示
Toast(error.response.data.message)
}
// 对响应错误做点什么
return Promise.reject(error)
})
api/user.js
提供登录 Api 函数
// 登录接口
export const login = (data) => {
return request.post('/user/login', data)
}
login.vue
登录功能
import { login } from '@/api/user'
methods: {
async onSubmit (values) {
const { data } = await login(values)
this.$toast.success('登录成功')
localStorage.setItem('vant-mobile-exp-token', data.token)
this.$router.push('/')
}
}
新建 utils/storage.js
const KEY = 'vant-mobile-exp-token'
// 直接用按需导出,可以导出多个
// 获取
export const getToken = () => {
return localStorage.getItem(KEY)
}
// 设置
export const setToken = (newToken) => {
localStorage.setItem(KEY, newToken)
}
// 删除
export const delToken = () => {
localStorage.removeItem(KEY)
}
登录完成存储token到本地
import { login } from '@/api/user'
import { setToken } from '@/utils/storage'
methods: {
async onSubmit (values) {
const { data } = await login(values)
setToken(data.token)
this.$toast.success('登录成功')
this.$router.push('/')
}
}
这个 面经移动端 项目,只对 登录用户 开放,如果未登录,一律拦截到登录
如果访问的是 首页, 无token, 拦走
如果访问的是 列表页,无token, 拦走
如果访问的是 详情页,无token, 拦走
…
分析:哪些页面,是不需要登录,就可以访问的! => 注册 和 登录 (白名单 - 游客可以随意访问的)
路由导航守卫 - 全局前置守卫
访问的路径一旦被路由规则匹配到,都会先经过全局前置守卫
只有全局前置守卫放行,才会真正解析渲染组件,才能看到页面内容
router/index.js
router.beforeEach((to, from, next) => {
// 1. to 往哪里去, 到哪去的路由信息对象
// 2. from 从哪里来, 从哪来的路由信息对象
// 3. next() 是否放行
// 如果next()调用,就是放行
// next(路径) 拦截到某个路径页面
})
拦截或放行的关键点? → 用户是否有登录权证 token
核心逻辑:
// 全局前置守卫:
// 1. 所有的路由一旦被匹配到,在真正渲染解析之前,都会先经过全局前置守卫
// 2. 只有全局前置守卫放行,才能看到真正的页面
// 任何路由,被解析访问前,都会先执行这个回调
// 1. from 你从哪里来, 从哪来的路由信息对象
// 2. to 你往哪里去, 到哪去的路由信息对象
// 3. next() 是否放行,如果next()调用,就是放行 => 放你去想去的页面
// next(路径) 拦截到某个路径页面
import { getToken } from '@/utils/storage'
const whiteList = ['/login', '/register'] // 白名单列表,记录无需权限访问的所有页面
router.beforeEach((to, from, next) => {
const token = getToken()
// 如果有token,直接放行
if (token) {
next()
} else {
// 没有token的人, 看看你要去哪
// (1) 访问的是无需授权的页面(白名单),也是放行
// 就是判断,访问的地址,是否在白名单数组中存在 includes
if (whiteList.includes(to.path)) {
next()
} else {
// (2) 否则拦截到登录
next('/login')
}
}
})
1.认识静态结构
2.注册组件:
import Vue from 'vue'
import { Cell } from 'vant'
Vue.use(Cell)
3.静态结构 Article.vue
<template>
<div class="article-page">
<nav class="my-nav van-hairline--bottom">
<a
href="javascript:;"
>推荐</a
>
<a
href="javascript:;"
>最新</a
>
<div class="logo"><img src="@/assets/logo.png" alt=""></div>
</nav>
<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 {
margin-bottom: 50px;
margin-top: 44px;
.my-nav {
height: 44px;
position: fixed;
left: 0;
top: 0;
width: 100%;
z-index: 999;
background: #fff;
display: flex;
align-items: center;
> a {
color: #999;
font-size: 14px;
line-height: 44px;
margin-left: 20px;
position: relative;
transition: all 0.3s;
&::after {
content: '';
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 0;
width: 0;
height: 2px;
background: #222;
transition: all 0.3s;
}
&.active {
color: #222;
&::after {
width: 14px;
}
}
}
.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>
说明:每个文章列表项,其实就是一个整体,封装成一个组件 → 可阅读性 & 复用性
步骤:
新建 components/ArticleItem.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/ArticleItem.vue'
Vue.component('ArticleItem', ArticleItem)
Article.vue
页面中
<template>
<div class="article-page">
...
<ArticleItem></ArticleItem>
</div>
</template>
接口:https://apifox.com/apidoc/project-934563/api-20384521
1.新建 api/article.js
提供接口函数
import request from '@/utils/request'
export const getArticles = (obj) => {
return request.get('/interview/query', {
params: {
current: obj.current,
sorter: obj.sorter,
pageSize: 10
}
})
}
2.页面中调用测试
import { getArticles } from '@/api/article'
export default {
name: 'article-page',
data () {
return {
}
},
async created () {
const res = await getArticles({
current: 1,
sorter: 'weight_desc'
})
console.log(res)
},
methods: {
}
}
3.发现 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}`
}
})
}
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)
})
说明:token 是有过期时间的 (6h),一旦 过期 或 失效 就无法正确获取到数据!
utils/request.js
// 添加响应拦截器
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)
})
article.vue
存储数据
import {getArticles} from '@/api/article'
data () {
return {
list: [],
current: 1,
sorter: 'weight_desc'
}
},
async created () {
const { data } = await getArticles({
current: this.current,
sorter: this.sorter
})
this.list = data.data.rows
},
v-for循环展示
<template>
<div class="article-page">
...
<ArticleItem v-for="(item,i) in list" :key="item.id" :item="item"></ArticleItem>
</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" v-html="item.content"></div>
<div class="foot">点赞 {{ item.likeCount }} | 浏览 {{ item.views }}</div>
</template>
</van-cell>
</template>
<script>
export default {
name: 'ArticleItem',
props: {
item: {
type: Object,
default: () => ({})
}
}
}
</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>
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response.data
}, function (error) {
// console.log(error)
// 有错误响应,后台正常返回了错误信息
if (error.response) {
if (error.response.status === 401) {
// 清除掉无效的token
delToken()
// 拦截到登录
router.push('/login')
} else {
// 有错误响应,提示错误消息
// this.$toast(error.response.data.message)
Toast(error.response.data.message)
}
}
// 对响应错误做点什么
return Promise.reject(error)
})
Login.vue
setToken(data.token)
Article.vue
async created () {
// 获取推荐的,第1页的10条数据
const res = await getArticles({
current: this.current,
sorter: this.sorter
})
this.list = res.data.rows
},
https://vant-contrib.gitee.io/vant/v2/#/zh-CN/list
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<ArticleItem v-for="(item,i) in list" :key="i" :item="item"></ArticleItem>
</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
}
}
1.切换推荐和最新 获取不同的数据
2.切换推荐和最新 点击的tab页签应该高亮
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()
}
核心知识点:跳转路由传参
准备动态路由 (已准备)
const router = new VueRouter({
routes: [
...,
{ path: '/article/:id', component: Detail },
{
path: '/',
component: Layout,
redirect: '/article',
children: [
...
]
}
]
})
点击跳转 article.vue
<template>
<!-- 文章区域 -->
<van-cell class="article-item" @click="$router.push(`/detail/${item.id}`)">
<template #title>
...
</template>
<template #label>
...
</template>
</van-cell>
</template>
页面中获取参数
this.$route.params.id
准备代码:
导入图标组件:
Vue.use(Icon)
静态结构:
<template>
<div class="detail-page">
<van-nav-bar
left-text="返回"
@click-left="$router.back()"
fixed
title="面经详情"
/>
<header class="header">
<h1>大标题</h1>
<p>
2050-04-06 | 300 浏览量 | 222 点赞数
</p>
<p>
<img src="头像" alt="" />
<span>作者</span>
</p>
</header>
<main class="body">
<p>我是内容</p>
<p>我是内容</p>
<p>我是内容</p>
<p>我是内容</p>
</main>
<div class="opt">
<van-icon class="active" name="like-o"/>
<van-icon name="star-o"/>
</div>
</div>
</template>
<script>
export default {
name: 'detail-page',
data () {
return {
article: {}
}
},
async created () {
},
methods: {
}
}
</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>
3.1封装api接口函数
api/article.js
export const getArticleDetail = (id) => {
return request.get('interview/show', {
params: {
id
}
})
}
3.2动态渲染
Detail.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>
<script>
import { getArticleDetail } from '@/api/article'
export default {
name: 'detail-page',
data () {
return {
article: {}
}
},
async created () {
this.article = {}
const { data } = await getArticleDetail(this.$route.params.id)
this.article = data
},
methods: {
}
}
</script>
封装准备接口
api/article.js
export const updateLike = (id) => {
return request.post('interview/opt', {
id,
optType: 1 // 喜欢
})
}
export const updateCollect = (id) => {
return request.post('interview/opt', {
id,
optType: 2 // 收藏
})
}
Detail.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 @click="toggleLike" :class="{active:article.likeFlag}" name="like-o"/>
<van-icon @click="toggleCollect" :class="{active:article.collectFlag}" name="star-o"/>
</div>
</div>
</template>
<script>
import { getArticleDetail, updateCollect, updateLike } from '@/api/article';
export default {
name: 'detail-page',
data() {
return {
article: {}
};
},
async created() {
this.article = {}
const { data } = await getArticleDetail(this.$route.params.id)
this.article = data;
},
methods: {
async toggleLike () {
await updateLike(this.article.id)
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 updateCollect(this.article.id)
this.article.collectFlag = !this.article.collectFlag
if ( this.article.collectFlag ) {
this.$toast.success('收藏成功')
} else {
this.$toast.success('取消收藏')
}
}
}
};
</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方法
api/article.js
// 获取我的收藏
export const getArticlesCollect = (obj) => {
return request.get('/interview/opt/list', {
params: {
page: obj.page, // 当前页
pageSize: 5, // 可选
optType: 2 // 表示收藏
}
})
}
collect.vue
准备结构
<template>
<div class="collect-page">
<van-nav-bar fixed title="我的收藏" />
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<ArticleItem v-for="(item, i) in list" :key="i" :item="item" />
</van-list>
</div>
</template>
<script>
import { getArticlesCollect } from '@/api/article'
export default {
name: 'collect-page',
data () {
return {
list: [],
loading: false,
finished: false,
page: 1
}
},
methods: {
async onLoad () {
// 异步更新数据
const { data } = await getArticlesCollect({ page: this.page })
this.list.push(...data.rows)
this.loading = false
this.page++
if (this.page > data.pageTotal) {
this.finished = true
}
}
}
}
</script>
<style lang="less" scoped>
.collect-page {
margin-bottom: 50px;
margin-top: 44px;
}
</style>
准备api函数
api/article.js
// 获取我的喜欢
export const getArticlesLike = (obj) => {
return request.get('/interview/opt/list', {
params: {
page: obj.page, // 当前页
pageSize: 5, // 可选
optType: 1 // 表示喜欢
}
})
}
Like.vue
请求渲染
<template>
<div class="like-page">
<van-nav-bar fixed title="我的点赞" />
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<ArticleItem v-for="(item,i) in list" :key="i" :item="item" />
</van-list>
</div>
</template>
<script>
import { getArticlesLike } from '@/api/article'
export default {
name: 'like-page',
data () {
return {
list: [],
loading: false,
finished: false,
page: 1
}
},
methods: {
async onLoad () {
// 异步更新数据
const { data } = await getArticlesLike({ page: this.page })
this.list.push(...data.rows)
this.loading = false
this.page++
if (this.page > data.pageTotal) {
this.finished = true
}
}
}
}
</script>
<style lang="less" scoped>
.like-page {
margin-bottom: 50px;
margin-top: 44px;
}
</style>
准备代码:
1 注册组件
import {
Grid,
GridItem,
CellGroup
} from 'vant'
Vue.use(Grid)
Vue.use(GridItem)
Vue.use(CellGroup)
2 准备api
api/user.js
// 获取用户信息
export const getUserInfo = () => {
return request('/user/currentUser')
}
3 页面调用渲染
<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 @click="logout" title="退出登录" is-link />
</van-cell-group>
</div>
</template>
<script>
import { getUserInfo } from '@/api/user'
import { delToken } from '@/utils/storage'
export default {
name: 'user-page',
data () {
return {
username: '',
avatar: ''
}
},
async created () {
const { data } = await getUserInfo()
this.username = data.username
this.avatar = data.avatar
},
methods: {
logout () {
delToken()
this.$router.push('/login')
}
}
}
</script>
<style lang="less" scoped>
.user-page {
padding: 0 10px;
background: #f5f5f5;
height: 100vh;
.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>
vue脚手架只是开发过程中,协助开发的工具,当真正开发完了 => 脚手架不参与上线
参与上线的是 => 打包后的源代码
打包:
打包后,可以生成,浏览器能够直接运行的网页 => 就是需要上线的源码!
vue脚手架工具已经提供了打包命令,直接使用即可。
yarn build
在项目的根目录会自动创建一个文件夹dist
,dist中的文件就是打包后的文件,只需要放到服务器中即可。
module.exports = {
// 设置获取.js,.css文件时,是以相对地址为基准的。
// https://cli.vuejs.org/zh/config/#publicpath
publicPath: './'
}
路由懒加载 & 异步组件, 不会一上来就将所有的组件都加载,而是访问到对应的路由了,才加载解析这个路由对应的所有组件
官网链接:https://router.vuejs.org/zh/guide/advanced/lazy-loading.html#%E4%BD%BF%E7%94%A8-webpack
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
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')
PS: 如果想要手机上看到效果,可以将打包后的代码,上传到 gitee,利用 git pages 进行展示