day1
接口文档地址:https://www.apifox.cn/apidoc/project-934563/api-20384515
一、项目功能演示
1.目标
启动准备好的代码,演示移动端面经内容,明确功能模块
2.项目收获
二、项目创建目录初始化
vue-cli 建项目
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 选自定义
- 手动选择功能
- 选择vue的版本
3.x > 2.x
- 是否使用history模式
- 选择css预处理
- 选择eslint的风格 (eslint 代码规范的检验工具,检验代码是否符合规范)
- 比如:const age = 18; => 报错!多加了分号!后面有工具,一保存,全部格式化成最规范的样子
- 选择校验的时机 (直接回车)
- 选择配置文件的生成方式 (直接回车)
- 是否保存预设,下次直接使用? => 不保存,输入 N
- 等待安装,项目初始化完成
- 启动项目
npm run serve
三、ESlint代码规范及手动修复
代码规范:一套写代码的约定规则。例如:赋值符号的左右是否需要空格?一句结束是否是要加;?…
没有规矩不成方圆
ESLint:是一个代码检查工具,用来检查你的代码是否符合指定的规则(你和你的团队可以自行约定一套规则)。在创建项目时,我们使用的是 JavaScript Standard Style 代码风格的规则。
1.JavaScript Standard Style 规范说明
建议把:https://standardjs.com/rules-zhcn.html 看一遍,然后在写的时候, 遇到错误就查询解决。
下面是这份规则中的一小部分:
- 字符串使用单引号 – 需要转义的地方除外
- 无分号 – 这没什么不好。不骗你!
- 关键字后加空格
if (condition) { ... }
- 函数名后加空格
function name (arg) { ... }
- 坚持使用全等
===
摒弃==
一但在需要检查null || undefined
时可以使用obj == null
- …
2.代码规范错误
如果你的代码不符合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 是来帮助你的。心态要好,有错,就改。
3.手动修正
根据错误提示来一项一项手动修正。
如果你不认识命令行中的语法报错是什么意思,你可以根据错误代码(func-call-spacing, space-in-parens,…)去 ESLint 规则列表中查找其具体含义。
打开 ESLint 规则表,使用页面搜索(Ctrl + F)这个代码,查找对该规则的一个释义。
四、通过eslint插件来实现自动修正
- eslint会自动高亮错误显示
- 通过配置,eslint会自动帮助我们修复错误
- 如何安装
- 如何配置
// 当保存的时候,eslint自动帮我们修复错误 "editor.codeActionsOnSave": { "source.fixAll": true }, // 保存代码,不自动格式化 "editor.formatOnSave": false
- 注意:eslint的配置文件必须在根目录下,这个插件才能才能生效。打开项目必须以根目录打开,一次打开一个项目
- 注意:使用了eslint校验之后,把vscode带的那些格式化工具全禁用了 Beatify
settings.json 参考
{ "window.zoomLevel": 2, "workbench.iconTheme": "vscode-icons", "editor.tabSize": 2, "emmet.triggerExpansionOnTab": true, // 当保存的时候,eslint自动帮我们修复错误 "editor.codeActionsOnSave": { "source.fixAll": true }, // 保存代码,不自动格式化 "editor.formatOnSave": false }
五、调整初始化目录结构
强烈建议大家严格按照老师的步骤进行调整,为了符合企业规范
为了更好的实现后面的操作,我们把整体的目录结构做一些调整。
目标:
- 删除初始化的一些默认文件
- 修改没删除的文件
- 新增我们需要的目录结构
1.删除文件
- src/assets/logo.png
- src/components/HelloWorld.vue
- src/views/AboutView.vue
- src/views/HomeView.vue
2.修改文件
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>
3.新增目录
- src/api 目录
- 存储接口模块 (发送ajax请求接口的模块)
- src/utils 目录
- 存储一些工具模块 (自己封装的方法)
目录效果如下:
六、vant组件库及Vue周边的其他组件库
组件库:第三方封装好了很多很多的组件,整合到一起就是一个组件库。
比如日历组件、键盘组件、打分组件、登录组件等
组件库并不是唯一的,常用的组件库还有以下几种:
pc: element-uielement-plusiviewant-design
移动:vant-uiMint UI (饿了么) Cube UI (滴滴)
七、全部导入和按需导入的区别
目标:明确 全部导入 和 按需导入 的区别
区别:
1.全部导入会引起项目打包后的体积变大,进而影响用户访问网站的性能
2.按需导入只会导入你使用的组件,进而节约了资源
八、全部导入
- 安装vant-ui
yarn add vant@latest-v2 // 或者 npm i vant@latest-v2
- 在main.js中
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提供了很多的组件,全部导入,会导致项目打包变得很大。
九、按需导入
- 安装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>
- 把引入组件的步骤抽离到单独的js文件中比如
utils/vant-ui.js
import { Button, Icon } from 'vant' Vue.use(Button) Vue.use(Icon)
main.js中进行导入
// 导入按需导入的配置文件 import '@/utils/vant-ui'
十、项目中的vw适配
官方说明:https://vant-contrib.gitee.io/vant/v2/#/zh-CN/advanced-usage
yarn add postcss-px-to-viewport@1.1.1 -D
- 项目根目录, 新建postcss的配置文件
postcss.config.js
// postcss.config.js module.exports = { plugins: { 'postcss-px-to-viewport': { viewportWidth: 375, }, }, };
viewportWidth:设计稿的视口宽度
- vant-ui中的组件就是按照375的视口宽度设计的
- 恰好面经项目中的设计稿也是按照375的视口宽度设计的,所以此时 我们只需要配置375就可以了
- 如果设计稿不是按照375而是按照750的宽度设计,那此时这个值该怎么填呢?
十一、路由配置-一级路由
但凡是单个页面,独立展示的,都是一级路由
路由设计:
- 登录页 (一级) Login
- 注册页(一级) Register
- 文章详情页(一级) Detail
- 首页(一级) Layout
- 面经(二级)Article
- 收藏(二级)Collect
- 喜欢(二级)Like
- 我的(二级)My
一级路由
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>
十二、路由配置-tabbar标签页
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>
十五、登录静态布局
使用组件
- van-nav-bar
- van-form
- van-field
- van-button
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>
十六、登录表单中的细节分析
- @submit事件:当点击提交按钮时会自动触发submit事件
- v-model双向绑定:会自动把v-model后面的值和文本框中的值进行双向绑定
- name属性:收集的key的值,要和接口文档对应起来
- label:输入的文本框的title
- :rules: 表单的校验规则
- placeholder: 文本框的提示语
十七、注册静态布局
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>
十八、request模块 - axios封装
接口文档地址:https://apifox.com/apidoc/project-934563/api-20384515
基地址:http://interview-api-t.itheima.net/h5/
目标:将 axios 请求方法,封装到 request 模块
我们会使用 axios 来请求后端接口, 一般都会对 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接口 - 注册功能
1.目标:将请求封装成方法,统一存放到 api 模块,与页面分离
2.原因:
以前的模式:
- 页面中充斥着请求代码,
- 可阅读性不高
- 相同的请求没有复用请求没有统一管理
3.期望:
- 请求与页面逻辑分离
- 相同的请求可以直接复用请求
- 进行了统一管理
4.具体实现
新建 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') } }
二十、toast 轻提示
https://vant-contrib.gitee.io/vant/v2/#/zh-CN/toast
两种使用方式
- 组件内或js文件内 导入,调用
import { Toast } from 'vant'; Toast('提示内容');
- **组件内 **通过this直接调用
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接口 - 登录功能
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('/') } }
二十三、local模块 - 本地存储
新建 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('/') } }
day2
一、全局前置守卫-语法认识
这个 面经移动端 项目,只对 登录用户 开放,如果未登录,一律拦截到登录
- 如果访问的是 首页, 无token, 拦走
- 如果访问的是 列表页,无token, 拦走
- 如果访问的是 详情页,无token, 拦走
…
分析:哪些页面,是不需要登录,就可以访问的! => 注册 和 登录 (白名单 - 游客可以随意访问的)
路由导航守卫 - 全局前置守卫
- 访问的路径一旦被路由规则匹配到,都会先经过全局前置守卫
- 只有全局前置守卫放行,才会真正解析渲染组件,才能看到页面内容
router/index.js
router.beforeEach((to, from, next) => { // 1. to 往哪里去, 到哪去的路由信息对象 // 2. from 从哪里来, 从哪来的路由信息对象 // 3. next() 是否放行 // 如果next()调用,就是放行 // next(路径) 拦截到某个路径页面 })
二、全局前置守卫-访问拦截处理
拦截或放行的关键点? → 用户是否有登录权证 token
核心逻辑:
- 判断用户有没有token, 有token, 直接放行 (有身份的人,想去哪就去哪~)
- 没有token(游客),如果是白名单中的页面,直接放行
- 否则,无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') } } })
三、面经列表-认识Cell组件-准备基础布局
1.认识静态结构
2.注册组件:
- van-cell
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>
四、封装 ArticleItem 组件
说明:每个文章列表项,其实就是一个整体,封装成一个组件 → 可阅读性 & 复用性
步骤:
- 新建 components/ArticleItem.vue 组件,贴入内容
- 注册成全局组件
- Article.vue 页面中应用
新建 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>
五、封装 api 接口-获取文章列表数据
接口: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}` } }) }
六、请求拦截器-携带 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过期
说明: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() }
十二、面经详情-动态路由传参-请求渲染
1.跳转路由传参
核心知识点:跳转路由传参
准备动态路由 (已准备)
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
2.动态渲染 (页面代码准备)
准备代码:
导入图标组件:
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.代码实现
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方法
- page: 表示当前页
- optType:2 表示获取我的收藏数据
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函数
- page: 表示当前页
- optType:1 表示获取我的喜欢数据
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脚手架只是开发过程中,协助开发的工具,当真正开发完了 => 脚手架不参与上线
参与上线的是 => 打包后的源代码
打包:
- 将多个文件压缩合并成一个文件
- 语法降级
- less sass ts 语法解析, 解析成css
- …
打包后,可以生成,浏览器能够直接运行的网页 => 就是需要上线的源码!
打包命令
vue脚手架工具已经提供了打包命令,直接使用即可。
yarn build
在项目的根目录会自动创建一个文件夹dist
,dist中的文件就是打包后的文件,只需要放到服务器中即可。
配置publicPath
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 进行展示