项目介绍
该项目是一个电商平台,主要实现功能有:
针对用户:登录、注册、个人中心;
针对商品:展示、搜索展示、详情页;
针对用户和商品的交互:加入购物车、删除购物车、网上结账等。
开发一个前端模块的步骤:
- 写静态页面、拆分静态组件。
- 发请求。
- Vuex。
- 组件获取仓库数据,动态展示。
项目配置
-
项目运行,浏览器自动打开。
// package.json "scripts": { "serve": "vue-cli-service serve --open", "build": "vue-cli-service build", "lint": "vue-cli-service lint" },
-
关闭
eslint
检验工具。// vue.config.js module.exports = { lintOnSave: false }
-
src 文件夹配置别名。
// jsconfig.json { "compilerOptions": { "baseUrl": "./", "paths": { "@/*":[ "src/*" ] } }, "exclude": [ "node_modules", "dist" ] }
-
清除 Vue 页面默认样式。
vue是单页面开发,我们只需要修改public下的index.html文件。
<link rel="stylesheet" href="<%= BASE_URL %>reset.css">
-
代码改变时,实现页面实时刷新。
// vue.config.js module.exports = { lintOnSave: false, devServer: { // true 则热更新,false 则手动刷新,默认值为 true inline: true, // development server port 8000 port: 8001, } }
项目路由分析
- 上中下结构:头部Header + 内容 Content + 页脚 Footer。
- 路由组件:
- Home
- Login / Register 13700000000 | 111111
- Search / Detail / addCartSuccess / ShopCart / Trade / OrderId / PaySuccess
- Center / MyOrder
- 非路由组件:
- Header
- Footer [在首页、搜索页]
组件开发
1. Header & Footer
-
Footer组件显示与隐藏:
- v-show:只是操作样式
- v-if: 频繁操作DOM,消耗性能
- 在Home Search 等组件 显示 Footer组件
- 在Login Register 隐藏
-
解决办法:路由元信息
<Footer v-show='$route.meta.footerShow != false' ></Footer>
2. Home 组件
-
导航的三级联动组件
<TypeNav/>
。 -
Home 组件挂载的时候获取用户信息在 Header 组件展示。
mounted(){ // 获取用户信息在首页展示 this.$store.dispatch('getUserInfo') }
userName(){ return this.$store.state.user.userInfo.name; }
-
轮播图
3. TypeNav 三级联动组件
- 将该组件注册为全局组件。
- 编程式路由导航 + 事件委托实现路由跳转。
- 全局组件可以在任一页面中直接使用,不需要导入声明。
// main.js
import TypeNav from '@/components/TypeNav'
// (全局组件名字,哪一个组件)
Vue.component(TypeNav.name, TypeNav)
-
节流:固定时间间隔触发。
让一级导航的每隔固定时间间隔触发。
@mouseenter="changeIndex(index)" changeIndex: throttle(function(index){ this.currentIndex = index; }, 50)
-
transition 动画
<transition name="sort" ></transition>
.sort-enter-active{ transition: all .5s linear; }
4. 轮播图 Carouse
-
swiper插件
-
watch + nextTick 解决遗留问题。
5. axios 请求
-
为什么需要进行二次封装aixos?
请求拦截器、响应拦截器
-
统一接口管理。
-
nprogress 进度条。
// 对axios进行二次封装
import axios from 'axios'
// 引入进度条
import nprogress from 'nprogress';
import 'nprogress/nprogress.css'
// 1. 利用axios对象的方法create, 创建于给axios实例
const requests = axios.create({
// 配置对象
// 基础路径,发请求的时候,路径当中会出现api
baseURL: '/api',
// 代表请求超时时间
timeout: 5000
});
// 2. 请求拦截器:在发请求之前,请求拦截器可以检测到
requests.interceptors.request.use((config) => {
// config: 配置对象
nprogress.start();
return config;
});
// 3. 响应拦截器
requests.interceptors.response.use((res)=>{
// 成功的回调函数
nprogress.done();
return res.data
}, (error)=>{
// 响应失败的回调函数
return Promise.reject(new Error('fail'))
})
export default requests;
6. Mock.js 数据
mock用来拦截前端ajax请求,返回我么们自定义的数据用于测试前端接口。
// mockServer.js
import Mock from 'mockjs'
import banner from './banner.json'
import floor from './floor.json'
// webpack默认对外暴露的:图片、JSON数据格式
Mock.mock("/mock/banner", {
code: 200,
data: banner
})
Mock.mock("/mock/floor", {
code: 200,
data: floor
})
7. Search 组件
-
面包屑导航相关操作
两个删除逻辑:
- 当分类属性(query)删除时删除面包屑同时修改路由信息。
- 当搜索关键字(params)删除时删除面包屑、修改路由信息、同时删除输入框内的关键字。
-
处理排序等相关操作
通过计算属性处理。
1代表综合,2代表价格,asc代表升序,desc代表降序。
前台拼接好排序字符串,向后台请求。
-
分页。
8. 分页
<Pagination
:pageNo='searchParams.pageNo'
:pageSize="searchParams.pageSize"
:total="total"
:continues="5"
@getPageNo = "getPageNo" />
分页器展示,需要哪些数据?
- 一共多少条数据 total。
- 当前是第几页 pageNo。
- 每一页展示多少条数据 pageSize。
- 分页器连续的页码数:5|7(好看)continues。
- totalPage 一共多少页,totalPage = Math.ceil(total / continues) 。
// 计算连续页码的起始数据,结束数据。
startNumberAndEndNumber(){
let start = 0, end = 0;
// totalPage < continues
if(this.totalPage < this.$props.continues){
start = 1;
end = this.totalPage;
}else{
start = this.$props.pageNo - parseInt(this.$props.continues / 2);
end = this.$props.pageNo + parseInt(this.$props.continues / 2);
// 考虑负数 & 0情况!
if (start < 1){
start = 1;
end = this.continues;
}
// 考虑 end
if(end > this.totalPage){
end = this.totalPage;
start = end - this.continues + 1;
}
}
return {start, end}
}
如何实现当前页码数据显示:
- 通过自定义事件将当前页码传给 Search 组件。
- Search 组件通过发起服务器请求,返回数据。
滚动路由(vue2)
const router = createRouter({
history: createWebHashHistory(),
routes: [...],
scrollBehavior (to, from, savedPosition) {
return { top: 0 }
}
})
9. 游客登录
创建 utils
工具包,
import {v4 as uuid4} from 'uuid'
// 要生成一个随机字符串,且每次执行不能发生变化。
export const getUUID = () => {
let uuid_token = localStorage.getItem('UUIDTOKEN')
if(!uuid_token){
uuid_token = uuid4();
// 本地存储一次。
localStorage.setItem('UUIDTOKEN', uuid_token)
}
return uuid_token;
}
游客 uuid_token 设置在 Detail 组件的 store 中。
import {getUUID} from '@/utils/uuid_token'
const state = {
goodInfo: {},
uuid_token: getUUID()
}
requestAjax.js
中设置请求头。
// this.$store只能在组件中使用,不能再js文件中使用。如果要在js中使用,需要引入
import store from '@/store'
const requests = axios.create({
baseURL: '/api',
timeout: 5000
});
// 2. 请求拦截器:在发请求之前,请求拦截器可以检测到
requests.interceptors.request.use((config) => {
// 游客登陆
if(store.state.detail.uuid_token){
config.headers.userTempId = store.state.detail.uuid_token;
}
nprogress.start()
return config;
});
// 3. 响应拦截器
requests.interceptors.response.use((res)=>{
nprogress.done()
return res.data
}, (error)=>{
return Promise.reject(new Error('fail'))
})
export default requests;
10. 购物车相关
-
判断底部勾选框是否全部勾选代码部分。
//判断底部勾选框是否全部勾选 isAllCheck() { //every遍历某个数组,判断数组中的元素是否满足表达式,全部为满足返回true,否则返回false return this.cartInfoList.every(item => item.isChecked === 1) }
-
修改商品数量加入节流操作。
handler: throttle(async function(type, disNum, cart) { //type: 为了区分这三个元素 //disNum形参: +变化量(1) -变化量(-1)input最终的个数(并不是变化量) //cart: 哪一个产品【身上有id】 //向服务器发请求,修改数量 switch (type) { //加号 case "add": disNum = 1; break; case "minus": //判断产品的个数大于1,才可以传递给服务器-1 //如果出现产品的个数小于等于1,传递给服务器个数0(原封不动) disNum = cart.skuNum > 1 ? -1 : 0; break; case "change": // //用户输入进来的最终量,如果非法的(带有汉字|出现负数),带给服务器数字零 if (isNaN(disNum) || disNum < 1) { disNum = 0; } else { //属于正常情况(小数:取证),带给服务器变化的量 用户输入进来的 - 产品的起始个数 disNum = parseInt(disNum) - cart.skuNum; } break; } //派发action try { //代表的是修改成功 await this.$store.dispatch("addOrUpdateShopCart", { skuId: cart.skuId, skuNum: disNum, }); //再一次获取服务器最新的数据进行展示 this.getData(); } catch (error) { console.log(error); } }, 500)
-
购物车状态修改和商品删除。
-
删除多个商品是重复调用删除单个商品的函数。
11. 注册 & 登录
-
获取验证码:服务器随机返回。
-
用户登录成功的时候返回token存储。
export const setToken = (token) => {
localStorage.setItem("TOKEN", token)
}
export const getToken = ()=>{
return localStorage.getItem("TOKEN")
}
export const removeToke = ()=> {
localStorage.removeItem("TOKEN")
}
async userLogin({ commit }, data) {
let res = await reqLogin(data);
//服务器下发token,用户唯一标识符(uuid)
if (res.code == 200) {
//用户已经登录成功且获取到token
commit("USERLOGIN", res.data.token);
setToken(res.data.token)
return "ok";
} else {
return Promise.reject(new Error("faile"));
}
}
// 2. 请求拦截器:在发请求之前,请求拦截器可以检测到
requests.interceptors.request.use((config)=>{
// 用户登录
if(store.state.user.token){
config.headers.token = store.state.user.token;
}
nprogress.start()
return config;
});
路由懒加载
插件 vue-lazyload
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。
Vue Router 支持开箱即用的动态导入,这意味着你可以用动态导入代替静态导入:
import UserDetails from './views/UserDetails'
// 替换成
const UserDetails = () => import('./views/UserDetails')
组件通信
解决问题:让组件间数据共享。
组件间通信的分类可以分成以下:
- 父子组件之间的通信
- 兄弟组件之间的通信
- 祖孙与后代组件之间的通信
- 非关系组件间之间的通信
-
props 传递数据。
- 适用场景:父组件传递数据给子组件
- 子组件设置
props
属性,定义接收父组件传递过来的参数 - 父组件在使用子组件标签中通过字面量来传递值
-
$emit 触发自定义事件
- 适用场景:子组件传递数据给父组件
- 子组件通过
$emit触发
自定义事件,$emit
第二个参数为传递的数值 - 父组件绑定监听器获取到子组件传递过来的参数
// children.vue this.$emit('add', good);
// parents.vue <Children @add="cartAdd($event)" />
-
ref
- 父组件在使用子组件的时候设置
ref
- 父组件通过设置子组件
ref
来获取数据
<Children ref="foo" /> this.$refs.foo; // 获取子组件实例,通过子组件实例我们就能拿到对应的数据
- 父组件在使用子组件的时候设置
-
全局事件总线
EventBus
- 使用场景:兄弟组件传值
- 创建一个中央事件总线
EventBus
- 兄弟组件通过
$emit
触发自定义事件,$emit
第二个参数为传递的数值 - 另一个兄弟组件通过
$on
监听自定义事件
// 基于vue2的全局事件总线安装 new Vue({ el: '#app', render: h => h(App), beforeCreate(){ // 安装全局事件总线 Vue.prototype.$bus = this; } })
this.$bus.$on('foo', this.handle);
this.$bus.$emit('foo');
-
$parent 或 $root
- 通过共同祖辈
$parent
或者$root
搭建通信桥连.
this.$parent.on('add',this.add)
this.$parent.emit('add')
- 通过共同祖辈
-
$attrs 或 $listeners [看不下去了!!!!]
- 适用场景:祖先传递数据给子孙
- 设置批量向下传属性
$attrs
和$listeners
- 包含了父级作用域中不作为
prop
被识别 (且获取) 的特性绑定 ( class 和 style 除外)。 - 可以通过
v-bind="$attrs"
传⼊内部组件
-
provide 与 inject
- 在祖先组件定义
provide
属性,返回传递的值 - 在后代组件通过
inject
接收组件传递过来的值
// 祖先组件 provide(){ return { foo:'foo' } }
// 后代组件 inject:['foo'] // 获取到祖先组件传递过来的值
- 在祖先组件定义
相关问题
v-show 与 v-if 。
共同点:v-if 和 v-show 都能实现元素的显示隐藏
v-show的元素始终会被渲染并保存在DOM中,v-show只是简单的切换元素的CSS属性display。
v-if是真正的条件渲染,因为它会确保切换过程中条件块内的时间监听和子组件适当地被销毁和重建。
区别:
- v-show 只是简单的控制元素的 display 属性,而 v-if 才是条件渲染(条件为真,元素将会被渲染,条件为假,元素会被销毁);
- v-show 有更高的首次渲染开销,而 v-if 的首次渲染开销要小的多;
- v-if 有更高的切换开销,v-show 切换开销小;
- v-if 有配套的 v-else-if 和 v-else,而 v-show 没有;
- v-if 可以搭配 template 使用,而 v-show 不行。
路由跳转方式:
- 声明式导航
router-link
标签 ,可以把router-link
理解为一个a标签,它也可以加class修饰。 - 编程式导航 :声明式导航能做的编程式都能做,而且还可以处理一些业务。
编程式路由跳转到当前路由(参数不变),多次执行会抛出
NavigationDuplicated(导航重复)
的警告错误?
-
$router.push()
的返回值为promise
;且 push 也是一个promsie
。 -
抛出错误原因:
push是一个promise,promise需要传递成功和失败两个参数,我们的push中没有传递。
-
解决办法:
let originPush = VueRouter.prototype.push;
VueRouter.prototype.push = function(location, resolve, reject){
// console.log(this, location, resolve, reject);
if(resolve && reject){
originPush.call(this, location, resolve, reject)
}else{
originPush.call(this, location, ()=>{}, ()=>{})
}
}
手撕防抖节流。
防抖: 不管怎么触发事件,要执行的东西一定在事件触发n秒之后才执行。
如果在事件触发的n秒内又触发了,就以新的时间为准,n秒之后才执行。
function debounce(fn, delay) {
let timer;
return function() {
var context = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args);
}, delay)
}
}
节流:一段时间只做一件事情,能够实现性能较好的懒加载。
比如监听滚动条和顶部距离。
function throttle(fn, wait) {
let timer;
return function() {
let context = this;
let args = arguments;
if (!timer) {
timer = setTimeout(function() {
time = null;
fun.apply(context, args);
}, wait)
}
}
}
nextTick().
- 好文章
- 将回调推迟到下一个 DOM 更新周期之后执行。
- 在修改数据之后立即使用它,然后等待 DOM 更新。
对象的拷贝。
-
浅拷贝:
浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 。
-
直接赋值。
// 浅拷贝 let obj1 = {age: 12, name: "Danny"}; let obj2 = obj1;
-
Object.assign()
拷贝的是属性值。// 伪深拷贝,即浅拷贝。 let obj1 = {age: 12, name: "Danny"}; let obj2 = Object.assign({}, obj1);
-
Array.prototype.slice();
拷贝规则同Object.assign()
.let a1 = [1, 2, 3, 4]; let a2 = a1.slice(0, 2);
-
扩展运算符
-
-
深拷贝:
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
-
JSON.parse(JSON.stringify());
let obj2 = JSON.parse(JSON.stringify(obj1));
- 如果这个属性对象时function, 不会拷贝成功。
- 如果被拷贝的对象中某个属性的值为undefined,则拷贝之后该属性会丢失。
- 如果被拷贝的对象中有正则表达式,则拷贝之后的对象正则表达式会变成Object,但对象是{}。
-
递归赋值;
function deepClone(obj){ let objClone = Array.isArray(obj)?[]:{}; if(obj && typeof obj === "object"){ for(key in obj){ //判断是否为自身属性 if(obj.hasOwnProperty(key)){ //判断ojb子元素是否为对象,如果是,递归复制 if(obj[key] && typeof obj[key] ==="object"){ objClone[key] = deepClone(obj[key]); } else { //如果不是,简单复制 objClone[key] = obj[key]; } } } } return objClone; }
-