0%

peanutMallFollowCoderwhy工程日志

项目工程概述

项目github仓库地址:peanutMallFollowCoderwhy

talk is cheap, show me your code

一下记录我跟随coderwhy老师开发mall商城应用中碰到的一些问题和需要注意的点。

碰到的问题

创建项目

在创建项目的时候,不能使用大写或者一些其他特殊字符

git remove add origin respostity url:将远程仓库和本地仓库联系起来

划分目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
assets				// 资源
--img
--css
--normalize.css // 对不同浏览器的标签样式统一(在github下载)
--base.css // 初始化css设置
views // 视图
componets // 公共组件
--common // 完全公共的组件,可以给别的项目使用
--content // 和当前组件业务相关的组件
router // 路由相关
store // vuex公共状态管理
network // 网络相关封装
common // 公共的一些常量,或者工具类

在根目录下创建vue.config.js配置别名。

在根目录下创建代码规范文件.editorConfig

开发

tabBar

如果你配置了别名,那么dom中引用url的时候,一定要加上~

1
<img src="~assets/img/tabbar/home.svg" slot="img-common-slot"/>

要不然就获取不到相应的图片。

设置点击之后active的样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<template>
<div class="tab-item" @click="itemClick">
<slot v-if="!isActive" name="img-common-slot"></slot>
<slot v-else name="img-active-slot"></slot>
<div :style="isTextActive">
<slot name="textSlot"></slot>
</div>
</div>
</template>

<script>
export default {
name: "TabBarItem",
props: {
path: {
type: String
},
// 父组件给子组件传值
textColor: {
type: String,
default: "red"
}
},
methods: {
itemClick(){
this.$router.replace(this.path);
}
},
computed: {
isActive(){
return this.$route.path.indexOf(this.path) != -1;
},
isTextActive(){
return this.isActive ? {color: this.textColor}: {}
}
}
}
</script>

<style scoped>
.tab-item{
height: 49px;
text-align: center;
flex: 1;
}
.tab-item img{
width: 24px;
height: 24px;
vertical-align: middle;
margin: 4px 0 2px 0;
}
.active{
color: red
}
</style>

注意:插槽如果有多个,要使用具名插槽,在使用的时候也要指定替换哪一个插槽,效果图如下

封装网络请求

我们知道axios是第三方框架,如果有那一天这个第三方框架不更新的话,我们如果在项目中多处使用了axios,代码的重构就会带来很多不必要的麻烦。

①下载axios插件

1
npm install axios

②使用的时候不需要Vue.use命令install

直接导入axios即可使用,详情课参照官方文档 axios官方文档

github xowen

出现的问题:在传回axios实例之后,发现调用then方法失败,原因是:应当返回axiosInstance(config)配置过后的axiosInstance

错误代码:

1
2
3
4
5
6
7
8
9
10
11
12
import Axios from "axios";


export function request(config){
// 创建axios实例,设置baseConfig
const axiosInstance = Axios.create({
baseURL: "http://123.207.32.32:8000",
timeout: 5000
});
axiosInstance(config);
return axiosInstance;
}

​ 原因是因为axios实例配置过后的实例返回的是一个promise,所以可以调用then方法

正确代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Axios from "axios";

export function request(config){
// 创建axios实例,设置baseConfig
const axiosInstance = Axios.create({
baseURL: "http://123.207.32.32:8000",
timeout: 5000
});
// 拦截器
axiosInstance.interceptors.request.use(config => {
console.log(config);
return config;
});
return axiosInstance(config);
}

封装轮播图

注意:在每次滚动之后,都要验证位置,如果位置超过了图片的长度

开发FeatureView

点击FeatureView图片之后跳转的超链接

https://act.mogujie.com/zzlx67

TabControl

如果只是文字不一样的话,就没有必要用插槽

取而代之:

当需要设置点击active的样式,使用currentIndex标记当前所处于的标签,使用循环中的index和currentIndex判断是不是相等,如果相等,就是处于活跃的标签

其中定义的变量

--color-high-text:文字高亮颜色

--color-tint:下划线高亮

如果希望滚动的时候,停留在页面的顶端,可以设置css

在没有达到top:xxpx之前,position的属性默认是一个static,当达到xxpx时,浏览器会自动将posiotion的属性改成fixed

(如果需要适配ie浏览器,这个属性就不能随便用了)

1
2
3
4
{
position: sticky;
top: xxpx;
}

商品列表页

create函数中些主要逻辑,主要是实现的方法在methods中执行

push方法可以传入可变参数,如果希望把一个数组push到另一个数组中,可以使用数组1.push(…数组2)

关于样式调整:

flex-wrap:wrap根据子元素的宽度适配每一行占多少个子元素。

justify-content:space-around:子元素占据每一行并均等分,但是中间的宽度是左右宽度的2倍

切换点击的类型对应现实的商品

注意的问题是,子组件向父组件传递参数的方法:

子组件:

1
2
3
4
5
6
setActive(index){
this.currentIndex = index;
// 传回父组件,监听当前index的点击,对应修改展示的商品信息
this.$emit('tabClick',index);
}

父组件(其中,父组件用来接收子组件传递的方法的那个方法中不需要携带index形参):

1
<tab-control class="tab-control" :titles="['流行','新款','精选']" @tabClick="tabClick"/>

better-scoll的安装和使用

使用原生的滚动(内容多余整个屏幕的时候),在移动端滚动的时候会非常的卡顿,以前为了适配移动端滚动的问题,使用了iscoll框架,但是现在他没有更新了,所以我们使用国内开发的一个better-scoll

安装

1
npm install better-scroll --save

如果想实现原生的局部js滚动,可以使用如下方式,但是前提是必须给父标签一个固定的高度才行。

第三方的框架, 要注意封装,万一他不更新,就凉凉了,下次如果项目中有很多地方使用到了这个框架,改起来很要命的

使用

上述使用是错误的,因为这个方法会在创建组建的时候被调用,这时候还没有挂载template,所以一直拿不到content。

在使用的时候,根据官方文档的说明,必须在管理的标签外面加一层wrapper,并且管理的内容必须是一个标签

如果向监听用户滚动到哪一个位置了:并对当前状态添加一些业务功能

但是默认情况下,Bscroll是不能实时监听滚动的位置的,如果想要实时监听滚动的化,必须给他传参数

参数:

①probeType:是否侦测。0,1都是不侦测;2表示在手指滚动的过程中侦测,手指离开后的惯性滚动过程不侦测;3表示只要是滚动都侦测。(配合scroll事件使用)

②click:默认值位false,在wrapper中管理的content如果有监听click事件,默认是不会生效的,如果将click属性改为true,才会传派事件。

③pullUpload:上拉加载更多(配合pullingUp使用),但是当前页面只能调用一次上拉加载的回调函数,需要在最后调用一个finishPullUp方法,结束当前上拉加载,才能监听下一次上拉加载。

封装better-scroll:封装一个vue文件,这个vue文件对better-scroll有依赖,其他的vue文件只要引用这个vue文件就可以了。

使用document.query获取wrapper的方式不是特别好,如果在其他组件中也有class为wrapper的标签,那么获取的时候就会有一些问题(query获取的是同一个页面上的第一个查询到的元素),在vue开发的时候经常出现类似的这种问题,值得注意,推荐的方法:

在vue中给标签绑定一个ref属性

1
<div ref="aaa"></div>

之后再下面就可以通过this.$refs.aaa拿到上述标记的标签,但是在前面所讲的ref是在父组件中给子组件标记,然后拿到相应的子组件的方法。这里标记普通的标签也是可行的。

在给组件中的元素添加样式的时候,如果style标签中没有加上scoped属性,那么这个style中设置的样式就是全局的,例如

1
2
3
<style scoped>

</style>

为某个div设置高度

1
2
3
4
{
其中vh是 视口的高度100vh == 100 %
height: 100vh
}

可以动态计算高度,也可以使用定位

出现的问题

然而在我加入better-scroll之后,页面上的元素都动不了了,就好像里面没有东西一样,这就很奇怪了,我封装的scroll应该没有问题(因为我在分类中测试了),问题应该处在home页的css上面,但是并不知道哪里的问题。

上百度看了,滚动不了的问题无非是wrapper的高度要比content的低,content下只能有一个子标签,这些问题我都注意了,但是还是不管用,无能为力。

但是反复测试,在pc端换设备测试的时候发现,换来换去怎么又好使了?这也太玄学了吧。看来better-scroll插件还不是很稳定。

想到可能是版本太高?所以就换了个低版本,1.15.2(和coderwhy一样的版本测试),果然,换了版本之后就好用了。

backtop

如果想要监听组件的点击,需要添加修饰符native

给子组件添加ref属性,可以直接访问子组件中的内容。

上拉加载更多

在完成上拉加载更多之前,还需要解决滚动的bug,在滚动的时候,因为图片是动态加载的,所以vue的template会先计算出一个高度,这时候因为图片还没有加载,所以高度没有加载了图片的时候那么高,所以当图片加载完成之后,可滚动的区域就比之前的更小,所以滚动到某个区域发现会滚动不了了,所以这里先解决一些这个问题(有时候能滚动到底部,有时候不能,用户不友好,这是因为图片加载的是异步的)

better-scroll对象中有一个属性记录了可滚动的高度有多高,这个计算出来的高度就放在这个属性中

解决:当加载完所有的图片之后,最后在调用scroll对象的refresh方法,但是问题就在于,我怎么知道什么时候加载完了图片?

监听每一张图片是否加载完成。只要有一张图片加载完了,就更新scroll(refresh方法)

原生的监听图片时候加载完成使用的是

1
img.onload = function(){}

但是vue已经帮我们封装了,只要给出一个imageLoad方法并实现就可以了

注意的是:我们这个img标签是在goodItem中的,这里我们不能直接拿到scroll对象:

①可以添加一个Vuex对象,在goodListItem中监听某个状态,如果这个状态改变,就在主页中执行refresh方法

②事件总线:和Vuex很像,但是他不是管理状态的,而是管理事件的:通过this.$bus.$emit(“funName”)发送一个事件给事件总线,然后再别的组件中通过this.$bus.$on(“funName”,function(){})监听别的组件中是否发射了一个叫funName的事件,但是默认情况下,程序中是没有$bus的,这个需要自己添加进来,使用vue原型添加进来

这样的话,就可以通过这个Vue实例发射事件

注意:

做完一次上拉加载更多,加载数据之后,不要忘了调用scroll对象中的finishPullUp方法。

有时候会出现报错的情况,调用refresh的方法的时候出错,原因可能是请求图片数据的方法在home.vue中的created中执行的,这时候可能请求太快了,但是goodListItem中的scroll没有创建出来,可能goodListItem中的组件还没有挂载,解决方法:在使用scroll之前,先判断scroll是不是为null,如果不为null才执行后面的代码

当然,改了之后还是有问题,之后又把this.$bus.$on方法放在了mounted中监听,但是发现,在分类和首页快速切换的时候还是会有问题.解决

每次在首页使用scroll的时候,在前面加上this.$refs.scroll的判断

优化

因为图片加载几乎是同时的,所以scroll.refresh方法执行的频率特别高,一秒钟之内如果你多次调用的话,执行一次就可以了.

防抖动(debounce):比如输入框,根据你每次输入的文字展示相应的内容,如果每次输入一个单词就向服务器发送一次请求,对服务器的压力就太大了,比如在输入一个字符之后,等待500ms,如果有下一次输入,那么就取消上一次的请求,将下一次输入和上一次输入加在一起……依次类推.这里就可以使用防抖动。

当不传入delay参数的时候,函数也没有执行三十次,这是为什么?

setTimeout函数在下一次事件循环的时候才执行,所以setTimeout中的函数永远是放在后面执行的

并且,防抖动的功能性函数,不要写在某一个组件里面,单独放在某个位置

增加防抖动之后,发现在内部调用apply方法的时候,浏览器报错apply未定义?

tabControl

加入better-scroll之后,原来tabControl的吸顶效果就不起作用了

首先获取tabControl的offsetTop,之后根据这个offsetTop来实现滚动到什么位置之后,开始吸顶(改变样式为fixed)

但是组件本身没有offsetTop属性,所以需要拿到template里面的标签才能获取 ,所有的组件都有一个$el属性,用来获取组件中的元素

通过这样的方式,发现它的高度远远小于我们想要的offsetTop,原因是图片还没有加载,获取的这个高度,是图片还没有撑起来的高度,所以就非常的矮,所以需要等图片全部加载完之后,在计算出来一个最终的offsetTop才是正确的。

在better-scroll中设置tabControl的样式是行不通的,所以使用了第二种方法:

增加一个一摸一样的better-scroll在顶部,实时监听滚动,如果滚动到了某个位置,就将顶部的tabControl显示,并且要注意的是,两个tabControl是独立的,所以在监听tabControl点击的时候,还要同步他们的currentIndex

路由状态保持

在跳转到其他页面的时候,首页的组件会销毁,这时候我们不希望他销毁。

在路由的上面设置标签

详情页开发

标题

标题因为已经封装了组件,所以直接使用就可以,点击标签为active,使用currentIndex

轮播图

因为轮播图中使用了query方法,所以组件外部的class不能和swiper里面的相同

在跳转的时候,关于keep-alive的问题,需要排除Detail页面,需要注意的是,exculde和include两个属性都是vue2.1+添加进来的属性,所以需要保证vue的版本符合要求。并且在exculde中设置的值要和各个组件中的name保持一致,要不然会找不到组件

商品信息

v-for() :可以遍历数字n,从1遍历到10

1
2
3
<div v-for="n in 10">
{{n}}
</div>

如何判断对象是空的?

1
Object.keys(obj).length === 0

​ 如果两个组件中有相同代码,想做抽取的话,需要用到mixin

因为接口问题,所以这里只以商品的详情和参数为例

标题和内容的联动

父组件给子组件传值的时候,不是值到了,就渲染,而是会等一下执行一个update生命周期函数,如果想在数组到了之后就拿到组件中的offsetTop是不可能的,可以使用

1
2
3
this.$nextTick(() => {

})

this.$nextTick传进去的回调函数会在子组件被渲染完成之后被回调

但是在这个回调函数回调的时候,当前的DOM中是不包含图片的,所以就会出现高度不正确的问题(一般offsetTop值不对的时候,都是因为图片没有加载完)

可以在图片一加载就设置最新的值,但是调用的太频繁了,所以使用防抖

最终解决:

但是这里的话,我只有商品的参数信息和goods信息,对于上述所说的防抖函数都用不上,因为图片在goods的下面加载,然而我又没有添加goods的titleNavtext

1
2
3
4
5
6
this.$nextTick(() =>{
this.detailOffsetY.push(this.$refs.detailParam.$el.offsetTop - 44)
// 插入一个最大值,使之能够在区间内判断
this.detailOffsetY.push(Number.MAX_VALUE)
})
})
1
2
3
4
5
6
7
8
9
// 根据滚动改变标题的active
detalScroll(position){
for(let i = 0 ; i < this.detailOffsetY.length -1 ; i++){
if(this.currentIndex != i && -position.y >= this.detailOffsetY[i] && -position.y < this.detailOffsetY[i+1]){
this.currentIndex = i
this.$refs.nav.currentIndex = this.currentIndex
}
}
}

底部工具栏

​ 封装组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<template>
<div id="detail_bottom_bar">
<div class="bottom_left">
<div class="service">
<i class="icon"></i>
<span>客服</span>
</div>
<div class="shop">
<i class="icon"></i>
<span>店铺</span>
</div>
<div class="collect">
<i class="icon"></i>
<span>收藏</span>
</div>
</div>
<div class="bottom_right">
<div class="cart" @click="addToCart">
加入购物车
</div>
<div class="buy">
购买
</div>
</div>
</div>
</template>
<script>
export default {
methods: {
addToCart() {
this.$emit("addEvent");
}
}
};
</script>
<style scoped>
#detail_bottom_bar {
font-size: 0.65rem;
display: flex;
position: fixed;
background-color: #fff;
bottom: 0px;
left: 0;
right: 0;
height: 2.09rem;
text-align: center;
box-shadow: 0 -0.04rem 0.4rem gray;
}
.bottom_left {
display: flex;
flex: 1;
}
.bottom_left > div {
flex: 1;
border-right: 0.04rem solid rgba(128, 128, 128, 0.2);
}
.bottom_left .icon {
display: block;
background: url("~assets/img/detail/detail_bottom.png") 0 0/100%;
width: 1rem;
height: 1rem;
margin: 0.12rem auto;
}
.service .icon {
background-position: 0 -2.4rem;
}
.shop .icon {
background-position: 0 -4.5rem;
}
.bottom_right {
display: flex;
flex: 1;
}
.bottom_right > div {
flex: 1;
line-height: 2.09rem;
}
.cart {
background-color: rgb(255, 174, 0);
}
.buy {
background-color: var(--color-tint);
color: white;
}
</style>

backTop混入封装

​ 使用mixins,直接默认导出文件,到时候在某个组件中使用就可以了

购物车

这里使用到了vuex插件,如果点击的是同一个商品,这时候不希望添加多次,只需要增加这个商品在cartList中的数量即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const store = new Vuex.Store({
state: {
cartList: []
},
mutations: {
addCart(state,payload){
let product = state.cartList.find(item => item.iid === payload.iid);
if(product){
product.count++;
}else{
payload.count = 1;
state.cartList.push(payload)
}
}
}
})

mutations的作用就是为了修改state状态,尽可能实现的功能单一一点,上面的解决方案没有达到单一的实现原则,上述又判断逻辑的东西,最好放在actions中完成(不止是异步操作可以放在action中)

注意:在封装mutations中的常量出去的时候,不能使用对象的形式,应该使用下面的形式

1
2
3
export const ADD_COUNTER = 'add_counter'
export const ADD_TO_PUSH = 'add_to_push'

导航栏

mapGetters的使用:

购物车页面滚动的问题,在添加完新的商品信息之后,回到商品信息页面应该对scroll刷新一遍,要不然scroll计算出来的高度仍然是添加之前的高度

修改item的状态

item的checked属性应该默认是放在对象模型中实现的,一旦用户点击按钮,取消选中状态(默认是选中的)

toast的封装

将封装的toast组件封装到一个插件中,然后通过安装插件的方式来使用toast

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Toast from "./Toast";

const obj = {}

obj.install = function (Vue) {
console.log(Vue)
//1.创建组件构造器
const con = Vue.extend(Toast)
const toast = new con()
toast.$mount(document.createElement('div'))
document.body.appendChild(toast.$el)
Vue.prototype.$toast = Toast
}

export default obj

补充细节

300ms延迟

移动端点击事件的300ms延迟,使用fastClick插件减少延迟

1
2
3
4
5
6
7
8
9
10
11
   // 安装
    npm install fastclick -S
    // 引入
    import FastClick from 'fastclick'
    // 使用
    FastClick.attach(document.body);

作者:有田春雪
链接:https://www.jianshu.com/p/67bae6dfca90
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

图片懒加载

github地址: vue-lazyload

px2vw-css单位转换插件

为了适配某些设备,需要做到固定的效果,这时候就可以使用别的单位,但是项目中往往会有很多地方需要调整单位,这时候就可以使用插件

安装插件:postcss-px-to-viewport,并且在vue.config中并不需要做额外的配置(其中px2rem也有相关的插件)

一般项目的设计稿都是基于iPhone6(750*1334),并且一个retina 1个点,相当于两个像素,适配的话,就是相对于iPhone6做适配