基础准备
技术选型
1). 前台数据处理/交互/组件化
2). 前后台交互
3). 模块化
4). 项目构建/工程化
5). css预编译器
6). 其它
接口相关理解
1). 接口、API
- 简单理解:前后端通信的地址
- 复杂理解:包含请求地址、请求方式、请求参数、响应的结果等所有内容才是一个完整接口
- vue接口文档:vue向外暴露的功能方法、属性等
2). 接口文档
3). 对/调/测接口 / 联调
4). 前后台分离
5). mock数据/接口
git 版本控制的基本操作
1). 创建本地仓库(代码在本地仓库中)
创建.gitignore文本, 并配置好
git init
git add .
git commit -m "init app"
2). 创建远程仓库
New Repo
指定仓库名
创建
3). 将本地仓库的代码推送到远程仓库
git remote add origin url (在本地记录远程仓库的地址)
git push origin master
4). 如果本地代码有修改, 要提交到本地仓库, 推送到仓库
git add .
git commit -m "xxx"
git push origin master
git config --global credential.helper store (记住用户和密码)
5). 如果远程代码有修改, 要拉取到本地仓库
git pull origin master
6). 将远程仓库的代码clone到本地(生成仓库)
git clone url
分支操作
1). 创建本地分支, 并推送到远程
git checkout -b dev
git push origin origin dev
2). 拉取远程新分支到本地
git pull (如果分支是在clone后创建的才需要执行)
git checkout -b dev origin/dev
3). 本地dev分支代码修改
git add .
git commit -m "xxx"
git push origin dev
4). 将dev分支合并到master
git checkout master
git merge dev
使用脚手架创建项目并运行
1). 使用vue-cli4
2). 开发环境运行 npm run serve
启动一个开发服务器,运行代码
3). 生产环境打包运行 npm run build
构建打包输出文件(将来用来项目上线的文件)
通常情况下:map文件只用来调试、测试,调试完成可以删除
运行打包输出的文件:
npm i serve -g
serve -s dist -p 5000
eslint 配置
1). package.json 中 `rules: { xxx: 0 }`,可以禁用某一个 eslint 规则
2). 源代码位置输入 `/* eslint-disable xxx */` 禁止代码下一行eslint 的 xxx 规则
3). vue.config.js 中 lintOnSave: false,禁用所有 eslint 规则
引入 vue-router
1). 下载vue-router
2). 确定整体路由结构:
上: Header
中: router-view
下: Footer
3). 定义一级路由组件: Home/Search/Register/Login (要有基本结构)
4). 创建路由器, 配置路由, 配置路由器
5). 组件中路由相关的2个对象 (面试问题)
$router: 路由器对象, 包含一些用于路由跳转的方法: push()/replace()/back()
$route: 当前路由信息对象, 包含当前路由相关数据的对象: path/name/query/params/meta
Header 组件
路由跳转/导航的2种方式
1). 声明式/路由链接跳转: <router-link to="/xxx">
2). 编程式跳转/导航: 点击监听 + router.push()/replace()
如果只需要跳转,用第一种
如果需要发送请求(做些其他事),再跳转,用第二种
样式的处理
- 样式命名
- 组件名称-xxx-xxx
- 样式使用
- 尽量用单个类名选择器,不要嵌套
- 优点:代码少、解析速度快
- scoped
- 让组件样式只在当前组件生效,不会影响其他组件
- 原理:给当前组件所有元素添加一个唯一的属性 data-v-xxx
- 所有样式选择器都会添加上属性选择器,所以样式只在当前组件生效
路由参数总结
params
路由配置
/xxx/:xxx 必选 params 参数
/xxx/:xxx? 可选 params 参数
使用/传递 params
this.
r
o
u
t
e
r
.
p
u
s
h
(
′
/
x
x
x
/
x
x
x
′
)
t
h
i
s
.
router.push('/xxx/xxx') this.
router.push(′/xxx/xxx′)this.router.push({ name: ‘必须使用命名路由’, params: {} })
可选参数:需要 params 就加上 params 选项,不需要 params 就去掉
query
使用/传递 query
this.
r
o
u
t
e
r
.
p
u
s
h
(
′
/
x
x
x
?
k
e
y
=
v
a
l
u
e
′
)
t
h
i
s
.
router.push('/xxx?key=value') this.
router.push(′/xxx?key=value′)this.router.push({path: ‘/xx’, query: {}})
meta
路由配置
meta: { key: value }
使用 meta 参数
this.$route.meta.xxx
props
以上方式可以 props 方式传递到组件中
路由配置
props(route) { return { …route.params, …route.meta, …route.query } }
组件声明接受
props: [‘xxx’]
公共路由组件参数
配置
组件声明接受
props: [‘xxx’]
vuex/导航/mock数据
vuex 的模块化
-
模块化优点:
- 尽可能避免将来合并文件出现的冲突问题
- store/modules/index.js 这个文件将来会出现冲突(保留双方代码)
- 每个页面的数据会在一个文件中,方便维护
- 尽可能避免将来合并文件出现的冲突问题
-
配置 modules
-
vuex 管理的数据结构
{
testCount: 0, // 总数据
home: { // 分模块数据
categoryList: [],
banners: []
},
login: {}
}
- 组件得到状态数据
computed: {
...mapState({
categoryList: (state) => {
return state.home.categoryList
},
banners: (state) => state.home.banners,
testCount: (state) => state.testCount
})
}
优化减少组件对象数量: 使用编程式导航代替声明式导航
需求: 点击某个分类项, 跳转到Search路由, 并携带categoryName & category1Id/category2Id/category3Id
实现: 使用声明路由导航<router-link>
问题: 每个分类都要创建一个RouterLink组件对象(非常多), 显示缓慢
解决: 使用编程式路由导航, 不用创建RouterLink组件对象, 显示更快
绑定点击监听, 在回调函数中通过push()/replace()来跳转
如何标识3分分类? 通过不同的属性名
优化事件处理效率: 利用事件委托
问题: 每个分类项都需要绑定点击监听, 监听数量太多了, 效率不高
解决: 利用事件委托, 绑定一个点击监听来搞定所有分类项的点击响应
利用标签自定义属性携带动态数据
问题: 如何得到对应的分类项的数据
解决: 利用标签的data自定义属性 (H5的语法)
给a标签指定data自定义属性: <a :data-categoryName="c1.categoryName">
在事件回调函数读取data自定义属性值: const {categoryname} = event.target.dataset
控制一级列表的显示与隐藏
设计状态数据: isHomeShow isSearchShow
isHomeShow:决定home路由组件显示 this.$route.path === '/'
isSearchShow: 决定Search路由组件显示
初始值: 是false
当移入移出时改变其值,从而决定显示隐藏
一级列表显示隐藏的过渡效果
给显示隐藏的元素包上一个<transition name="xxx">
在显示/隐藏过程的类名下指定: transition样式
在隐藏时的类名下指定: 隐藏的样式
优化请求执行的位置, 减少请求次数
问题: 从首页跳转到搜索页, 还会请求三级分类列表
原因: 在TypeNav组件的mounted()中分发给异步action请求的 ==> 每个TypeNav组件对象都会发请求
解决: mounted先判断是否有数据(this.categoryList.length),有数据return,没有数据在发送请求
合并分类 query 参数与搜索的关键字 params 参数
问题:
当根据分类跳转search时, 丢了keyword的params参数
当根据keyword跳转search时, 丢了categoryName/cateory1Id/cateory2Id/cateory3Id的query参数
解决:
当根据分类跳转search时, 同时携带上keyword的params参数
当根据keyword跳转search时,携带上categoryName/cateory1Id/cateory2Id/cateory3Id的query参数
mock 数据接口
问题: 当前首页只有分类的接口写好, 其它数据的接口还没有写好
场景: 后台还没写好接口,开发者不能等,要去模拟数据(注意:清楚数据结构)
解决: 前端工程师自己mock/模拟接口数据
原理:
搭建一个mock服务器,设置路由
当客户端访问mock服务器的指定路由时,返回一个模拟数据回去
理解 JSON 数据结构
a.结构: 名称, 数据类型 ==> 用于读取数据值
b.value: 会显示到界面上
c.真实接口返回的数据与mock的数据的关系: value可以变, 但结构不能变
注意: 如果有变化 ==> 需要修改模板中读取显示的代码 ==> 真实情况是多少会有些不同, 变化越小需要修改的代码就越少
使用 mockjs 来 mock 接口
下载mockjs, 引入使用
mockjs: 生成随机数据,拦截 Ajax 请求, 返回生成的随机数据
定义mock json数据: 使用mockjs的随机语法
mockServer中: 通过Mock.mock()来定义mock接口
main.js中: 引入mockServer
必须写ajax请求代码访问:
utils/mockRequest中: 封装针对mock接口的axios封装封装
utils/request中: 定义对应的接口请求函数
组件中: 调用接口请求函数
注意: 浏览器并不会发对mock接口的请求
使用 vuex 管理 mock 接口返回的 banners 与 floors 数据
state: banners / floors
mutation: GET_BANNERS() / GET_FLOORS()
action: getBanners() / getFloors()
banners 和 floors 数据遍历展示
轮播图
- 在哪里去 new Swiper
在 mounted 去 new Swiper,为了保证 DOM 结构生成了在 new Swiper
此时 Swiper 才能获取到 DOM 元素,才能生成轮播图
- 但是 mounted 直接 new Swiper,此时还没有获取到 banners 数据
因为此时在 mounted 中刚刚发送请求加载轮播图图片数据,所以轮播图数据还未生成,所以失败
- 要等轮播图图片数据请求回来,在 new Swiper
- await this.getBanners() --> 等待 vuex 将数据更新完毕,再执行后面代码
action 函数如果返回一个 promise 对象,await 就能等待 action 和 mutation 函数全部做完直到数据更新(可以通过打印调试看现象)
- 此时虽然轮播图数据有了,但是 DOM 结构没有
因为更新用户界面都是异步的,所以要等同步全部执行完,在去更新 - 方案一:定时器
通过定时器将 new Swiper 添加宏任务队列,而更新用户界面是微任务队列
所以是先更新用户界面,此时就有 DOM 结构
再 new Swiper,此时就 OK - 方案二:
this.$nextTick(() => {})
Vue.nextTick(() => {})
等当前用户界面更新完毕,在触发其中的回调函数
将其中的回调函数放到更新完成 DOM 后在触发
其中的回调函数可以近似看做实在 updated 中执行(但是只会执行一次) - 注意:Swiper6 需要手动引入其他插件才可以使用
import Swiper, { Navigation, Pagination, Autoplay } from "swiper";
import "swiper/swiper-bundle.min.css";
// https://swiperjs.com/get-started/
Swiper.use([Navigation, Pagination, Autoplay]);
- 因为轮播图由多个地方用,封装轮播图组件,此时又有新的问题,轮播图不能正常生成
轮播图组件会渲染两次,第一个 banners 数据还是[], 第二次才有数据
而轮播图组件第一次就会触发 mounted,此时 banners 数据还未生成,是[], 渲染失败
-
通过 watch 解决,watch 监听 carouselList 的变化
第一次 banners 数据是[],是数据初始化,不会触发
第二次 banners 才有数据,会触发 watch,此时在这里初始化 Swiper 就有数据 -
数据有了还需要等数据渲染成 DOM 元素才行,所以需要加 this.$nextTick
-
其他组件 Floor 中使用轮播图出现问题
原因:Floor 组件是等待数据加载成功再进行遍历渲染的,所以轮播图一上来就有数据
第一次 banners 数据就有了,不会触发 watch,所以不能 new Swiper -
此时需要在 mounted 中来判断数据是否存在,然后 new Swiper
mounted 代表数据已经渲染完成 DOM 元素,所以不需要加 this.$nextTick -
最后所有轮播图都使用 .swiper-container 这个容器去 new Swiper
导致所有轮播图会出现一些问题,切换显示时会一闪一闪
所以将容器改成 this.$refs.swiper
vue-router
概念
前端路由: 用来开发单页面应用
单页面应用 SPA:
- 整个应用只有一个完整页面
- 点击链接不会发送请求,不会刷新页面
- 会更新地址和局部页面
两种模式
-
hash
- 路径带#, 没法用锚点功能,不美观
- 兼容性较好 ie8
- 原理:
- 跳转路由:window.location.hash
- 监视路径:window.onhashchange 事件
-
history
-
不带#,可以用锚点功能,美观
-
兼容性较差 ie10
-
原理:
- 跳转路由:window.history.pushState
- 监视路径:window.popstate 事件
-
问题 history 模式出现 404 问题
-
原因:开发服务器处理 / 根路径请求,别的路径是处理不了
-
所以:访问 /home 就会出现 404
-
解决:
- 开发模式(webpack):
devServer: { historyApiFallback: true, // 一旦请求404,就默认返回index.html页面 },
- 上线模式(nginx):
- 开发模式(webpack):
-
-
问题 hash 模式不出现 404 问题
- 因为 hash 模式的路径都在 # 后面
- 而 # 后面的参数发送请求是不会携带,
- 所以每次请求请求地址都是 / ,自然没有 404
-
项目总结
2. 项目主要功能
首页、搜索页面、购物车页面、订单页面、支付页面、商品详情、登陆页面、注册页面等多个功能页面
3. 项目主要技术
Vue 全家桶技术(Vue、Vue-Router、Vuex), swiper, uuid, qrcode, mockjs, nprogress, vee-validate, vue-lazyload
4。项目难点功能
- 项目优化
- 项目封装组件
- 项目功能
- 购物车+支付
- 点击商品可以加入购物车(收集商品 id 和商品数量发送请求,添加购物车)
- 来到购物车页面(发送请求,获取购物车页面数据,遍历展示)
- 在购物车页面可以对商品进行操作(增加、减少数量、删除等操作)
- 点击结算按钮(发送请求,跳转到订单页面)
- 订单页面需要选择(收货地址、优惠券等功能)
- 点击提交订单会收集所有数据发送请求生成订单号,来到下一个支付页面
- 支付页面选择支付方式,选择微信支付(后续就会调用微信支付接口来付款)
- 付款成功/失败都会重定向到支付结果页面(根据参数来提示成功或失败)
- 所有结果都可以在我的订单页面查询到
- 登陆&注册
- 收集数据
- 进行表单验证
- 提交表单
- 购物车+支付
5. 项目优化
- 在首页的三级分类列表中 - 使用事件委托来绑定点击事件跳转页面
- 对所有图片使用 vue-lazyload 进行图片懒加载
- 进行路由懒加载(1. 会将路由组件差分成单独 js 打包出来 2. 按需加载(需要的时候才会加载当前路由组件))
- 封装复用组件:Carousel、TypeNav…
- 减少请求数量:登陆按钮、注册按钮、订单按钮使用开关在请求完成之前让其只能点击一次
- 封装了一个 Button 组件来做这个事
- webpack 优化…
6. 项目封装组件
-
Carousel
- 接受了要展示的图片数据
- 注意:
- Swiper6 改动较大,将部分功能提取成单独组件,要用就要通过 Swiper.use 使用(Paganition、AutoPlay)
- Swiper 生效必须等数据加载完成且渲染成 DOM 元素
- 通过 watch 来监视数据,等待数据加载完成
- 通过 this.$nextTick 方法等待 DOM 元素渲染完成
- 再 new Swiper 才行
- 再某些组件应用时,数据一上来就有了,watch 反而监视不到数据的变化,所以 mounted 中做
- 为了 Swiper 组件冲突,用了 ref
-
Paganition
- 接受了 total 等参数
- 注意:
- 计算中间显示按钮开始到结束的值会比较麻烦
-
Button
-
…