面经 H5 端 - Vant(上)
项目创建目录初始化
vue-cli 建项目
创建项目
vue create hm-vant-h5
- 选项
Vue CLI v5.0.4
? Please pick a preset: (Use arrow keys)
Default ([Vue 3] babel, eslint)
Default ([Vue 2] babel, eslint)
> Manually select features 选自定义
- 手动选择功能-回车确定
- 选择 vue 的版本
3.x
> 2.x
- 是否使用 history 模式
- 选择 css 预处理
- 选择配置文件的生成方式 (直接回车)
- 是否保存预设,下次直接使用? => 不保存,输入 N
- 等待安装,项目初始化完成
- 启动项目
npm run serve
调整初始化目录结构
强烈建议大家严格按照老师的步骤进行调整,为了符合企业规范
为了更好的实现后面的操作,我们把整体的目录结构做一些调整。
目标:
- 删除初始化的一些默认文件
- 修改没删除的文件
- 新增我们需要的目录结构
修改文件
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>
删除文件
src/views/AboutView.vue
src/views/HomeView.vue
src/components/HelloWorld.vue
src/assets/logo.png
新增目录
- src/api 目录
- 存储接口模块 (发送 ajax 请求接口的模块)
- src/utils 目录
- 存储一些工具模块 (自己封装的方法)
- src/assets 目录
- 项目使用的素材
目录效果如下:
vant 按需加载
vant-ui 组件库的引入
组件库:第三方封装好了很多很多的组件,整合到一起就是一个组件库。
组件库并不是唯一的
pc: element-ui iview ant-design
移动:vant-ui
按需加载
- 安装 vant-ui
npm i 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>
项目中的 vw 适配
官方说明:https://vant-contrib.gitee.io/vant/v2/#/zh-CN/advanced-usage
npm i postcss-px-to-viewport@1.1.1 -D
- 项目根目录, 新建 postcss 的配置文件
postcss.config.js
// postcss.config.js
module.exports = {
plugins: {
"postcss-px-to-viewport": {
viewportWidth: 375,
},
},
};
路由设计配置
但凡是单个页面,独立展示的,都是一级路由
路由设计:
- 登录页 (一级) login
- 注册页(一级) register
- 文章详情页(一级) detail
- 首页(一级) layout
- 面经(二级)article
- 收藏(二级)collect
- 喜欢(二级)like
- 我的(二级)my
一级路由
router/index.js
配置一级路由, 一级 views
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: "/", component: Layout },
{ path: "/login", component: Login },
{ path: "/register", component: Register },
{ path: "/detail/:id", component: Detail },
],
});
export default router;
清理 App.vue
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
async created() {},
};
</script>
tabbar 标签页
https://vant-contrib.gitee.io/vant/v2/#/zh-CN/tabbar
main.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>
二级路由
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;
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: 'login-page',
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: '请填写用户名' }]"
/>
<van-field
v-model="password"
type="password"
name="password"
label="密码"
placeholder="密码"
:rules="[{ required: true, message: '请填写密码' }]"
/>
<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://www.apifox.cn/apidoc/project-934563/api-19465917
我们会使用 axios 来请求后端接口, 一般都会对 axios 进行一些配置 (比如: 配置基础地址等)
一般项目开发中, 都会对 axios 进行基本的二次封装, 单独封装到一个模块中, 便于使用
- 安装 axios
npm i axios
新建
utils/request.js
封装 axios 模块利用 axios.create 创建一个自定义的 axios 来使用
/* 封装axios用于发送请求 */
import axios from "axios";
// 创建一个新的axios实例
const request = axios.create({
baseURL: "http://interview-api-t.itheima.net/",
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;
- 测试 (可以先注册后登录)
<script>
import request from '@/utils/request'
export default {
async created () {
const res = await request({
url: '/user/login',
body: {
username: 'shuaipeng',
password: '123456'
}
});
console.log(res)
}
}
</script>
toast 轻提示
将来无论注册成功,还是失败,都是需要给用户提示的
https://vant-contrib.gitee.io/vant/v2/#/zh-CN/toast
两种使用方式
- 无需注册,导入,调用
import { Toast } from "vant";
Toast("提示内容");
- 组件内,通过 this 直接调用 👍
注册
import { Toast } from "vant";
Vue.use(Toast);
调用
this.$toast("提示内容");
封装 api 接口 - 注册功能
新建 api/user.js
提供注册 Api 函数
import request from "@/utils/request";
// 注册接口
export const registerAPI = (data) => {
return request.post("/user/register", data);
};
register.vue
页面中调用测试
methods: {
async onSubmit (values) {
// 往后台发送注册请求了
await registerAPI(values)
this.$toast.success('注册成功')
this.$router.push('/login')
}
}
request.js
响应拦截器,统一处理错误提示
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 loginAPI = (data) => {
return request.post("/h5/user/login", data);
};
login.vue
登录功能
import { loginAPI } from '@/api/user'
import { setToken } from '@/utils/storage'
methods: {
async onSubmit (values) {
const { data } = await loginAPI(values)
setToken(data.token)
this.$toast.success('登录成功')
this.$router.push('/')
}
}
local 模块 - 本地存储
新建 utils/storage.js
// 以前 token 令牌,如果存到了本地,每一次都写这么长,太麻烦
// localStorage.setItem(键, 值)
// localStorage.getItem(键)
// localStorage.removeItem(键)
const KEY = "my-token-vant-mobile";
// 获取
export const getToken = () => {
return localStorage.getItem(KEY);
};
// 设置
export const setToken = (token) => {
localStorage.setItem(KEY, token);
};
// 删除
export const delToken = () => {
localStorage.removeItem(KEY);
};
导航守卫-页面访问拦截
这个 面经移动端 项目,只对 登录用户 开放,如果未登录,一律拦截到登录
如果访问的是 首页, 无 token, 拦走
如果访问的是 列表页,无 token, 拦走
如果访问的是 详情页,无 token, 拦走
....
分析:哪些页面,是不需要登录,就可以访问的! => 注册 和 登录 (白名单 - 游客可以随意访问的)
核心逻辑:
- 判断用户有没有 token, 有 token, 直接放行 (有身份的人,想去哪就去哪~)
- 没有 token(游客),如果是白名单中的页面,直接放行
- 否则,无 token(游客),且在访问需要权限访问的页面,直接拦截到登录
src/router/index.js
// ... 省略其它代码
import { getToken } from "@/utils/storage";
// 全局前置守卫:
// 1. 所有的路由一旦被匹配到,在真正渲染解析之前,都会先经过全局前置守卫
// 2. 只有全局前置守卫放行,才能看到真正的页面
// 任何路由,被解析访问前,都会先执行这个回调
// 1. from 你从哪里来, 从哪来的路由信息对象
// 2. to 你往哪里去, 到哪去的路由信息对象
// 3. next() 是否放行,如果next()调用,就是放行 => 放你去想去的页面
// next(路径) 拦截到某个路径页面
const whiteList = ["/login", "/register"]; // 白名单列表,记录无需权限访问的所有页面
router.beforeEach((to, from, next) => {
const token = getToken();
// 如果有token,直接放行
if (token) {
next();
return;
}
// 没有token的人, 看看你要去哪
// (1) 访问的是无需授权的页面(白名单),也是放行
// 就是判断,访问的地址,是否在白名单数组中存在 includes
if (whiteList.includes(to.path)) {
next();
} else {
// (2) 否则拦截到登录
Toast("没有登录, 请先登录!");
next("/login");
}
});
export default router;