一个蘑菇街电商设计页面,使用了vue路由,vuex,轮播图,购物车计算,内容联动,封装思想,插件封装,事件总线,防抖函数,接口使用等技术,vue的技能点基本全部运用,同时使用第三方插件betterScroll来提高滚动流畅度,并解决了使用betterScroll的相关问题
- network
- components -> common/content
- pages -> 路由分层
- common
- assets
- router
- store
- initialize.css
- base.css
- 封装HYTabbar
- 封装HYTabbarItem
- 响应点击切换的设计
- 封装完成后,使用时对HYTabbar重新包装
- 创建axios实例
- 拦截响应,返回.data数据
- 根据传入的options发送请求,并且调用对应resolve和reject
- 封装navbar包含三个插槽:left、center、right
- 设置navbar相关的样式
- 使用navbar实现首页的导航栏
- 封装请求首页更多数据
- 将banner数据放在banners变量中
- 将recommend数据放在recommends变量中
- 使用Swiper和SwiperItem
- 传入banners进行展示
- 传入recommends数据,进行展示
- 展示一张图片即可
- 基本结构的封装
- 监听点击
- 定义goodsList变量,用于存储请求到的商品数据
- 根据type和page请求商品数据
- 将商品数据保存起来
- 展示商品列表,封装GoodsList
- 列表中每一个商品,封装GoodsListItem
- 注意CSS属性的设置即可
- 学习BetterScroll的使用
- 安装better-scroll
- 封装一个独立的组件,用于作为滚动组件:Scroll
- 组件内代码的封装:
- 1.创建BetterScroll对象,并且传入DOM和选项(probeType、click、pullUpLoad)
- 2.监听scroll事件,该事件会返回一个position
- 3.监听pullingUp事件,监听到该事件进行上拉加载更多
- 4.封装刷新的方法:this.scroll.refresh()
- 5.封装滚动的方法:this.scroll.scrollTo(x, y, time)
- 6.封装完成刷新的方法:this.scroll.finishedPullUp
- 通过Scroll监听上拉加载更多。
- 在Home中加载更多的数据。
- 请求数据完成后,调动finishedPullUp
- 封装BackTop组件
- 定义一个常量,用于决定在什么数值下显示BackTop组件
- 监听滚动,决定BackTop的显示和隐藏
- 监听BackTop的点击,点击时,调用scrollTo返回顶部
- 重新添加一个tabControl组件(需要设置定位,否则会被盖住)
- 在updated钩子中获取tabControl的offsetTop
- 判断是否滚动超过了offsetTop来决定是否显示新添加的tabControl
- 创建远程仓库,无需选配置README文件,开源协议选MIT。直接克隆下来
- 然后再在webstorm中创建项目,项目会提示文件名已存在,选择merge合并即可
- 这样就免去了还要链接远程仓库的麻烦
如果你已经创建好了项目想链接远程仓库,执行以下指令
- 链接远程仓库 git remote add origin 仓库地址
- 把当前分支push到远程分支 git push -u origin master
css初始化可以直接引用initialize.css。在github上
这个文件在cli3中被移除了。但这个统一标准还是很重要的,如果作为项目组长,一定要把这个copy过来使用
处理封装一个统一管理文件之外。还要再封装一个单独面向文件。如home.vue封装home.js网络请求文件,request->home.js->home.vue
可以降低模块耦合度,便于管理。现在项目小,等以后大项目便会了解这么做的好处
页面结构有变化用路由,只变数据不用路由;组件内结构有变化用插槽,只变内容则不用
我们一般不在声明周期函数内直接处理。 而是给它封装在methods中
注意事项:
- 使用声明周期函数调用BScroll时要在DOM创建后使用,如使用module()生命周期函数
- wrapper中只能包含一个元素content。然后挂载wrapper。如果想添加内容在content中添加
- 父视窗为100vh,子视窗设为position:absolute;top: 44px;bottom: 49px;overflow: hidden;
top和bottom是上下边框留的边距。不要可以删除,其他别乱改
滚动侦测:
-
当 probeType 为 1 的时候,会非实时(屏幕滑动超过一定时间后)派发scroll 事件;
-
当 probeType 为 2 的时候,会侦测当前滚动距离,但滚动惯性不侦测;
-
当 probeType 为 3 的时候,滚动和惯性全部侦测
let bscroll = new BScroll(document.querySelector('.wrapper'),{ //配置参数 probeType:2 }); //绑定事件 bscroll.on('scroll',position =>{ console.log(position); })
上拉加载:
- 配置参数为 pullUpLoad:true
- 绑定事件pullingUp
- 加载完成后 标识上拉加载结束finishPullUp() bscroll.on('pullingUp',() =>{ console.log('上拉加载'); //发送网络请求,请求页面数据 //数据展示完成 setTimeout(()=>{ //标识上拉加载结束 bscroll.finishPullUp() //刷新组件 bscroll.refresh(); },2000)
click参数
在BScroll中,点击事件默认会被阻止。(<button>除外) 需要设为true才支持点击事件
不管axios还是BScroll,只要是第三方插件,一般都会面临不会维护或bug等风险。到时候出问题每个引入页面都要修改
我们只要做一个封装,然后再在组件中导入使用。到时只需修改封装组件即可
<template>
<div class="wrapper" ref="wrapper">
<div class="content">
<slot></slot>
</div>
</div>
</template>
<script>
import BScroll from 'better-scroll'
export default {
name: "Scroll",
props:{
probeType: {
type:Number,
default:0
}
},
data(){
return {
bscroll:null
}
},
mounted() {
this.bscroll = new BScroll(this.$refs.wrapper,{
probeType:this.probeType,
});
this.bscroll.on('scroll',position =>{
this.$emit('scroll',position)
});
},
methods:{
//传入默认time=500
scrollTo(x,y,time=500){
this.bscroll.scrollTo(x,y,time);
}
}
}
</script>
使用方法:
- 注册组件
- 把要滚动的内容嵌套在组件中即可
- 通过父传子把配置参数传递,在emit发射到父组件中使用
在SCroll中,高度计算出来之前,图片还有加载完,会导致即使图片加载出来了无法向下滚动的问题
解决办法,通过@load监听每个图片的加载,每加载完成一个图片刷新重新计算高度
- 在vue中通过原型挂载$bus并new Vue。通过vue的事件总线来发射到父组件中 Vue.prototype.$bus = new Vue()
- 在图片监听事件中发射事件总线 this.$bus.$emit('aaa')
- 在父组件生命周期函数中监听事件总线,进行刷新 this.$bus.$on('aaa',()=>{ this.$refs.scroll.refresh(); })
当我们在父组件刷新BScroll时,提醒refresh没有找到的问题
解决办法:
//一、判断是否获取到bscroll再执行
refresh(){
this.bscroll && this.bscroll.refresh()
}
//二、在mounted生命周期函数中使用,而不是created中
在实际开发中我们可能每当用户输入一个字符就请求一次数据,这时一个很大的性能浪费, 可以当用户在300ms内持续输入时,取消数据请求,过了时间后再发送数据,缓解服务器压力。 这种操作就叫做防抖处理
在BScroll中为了解决高度问题,进行监听每个图片加载而频繁刷新,会浪费性能 我们也可以进行防抖处理。
debounce(fun,delay){
let timer = null;
return function (...args) {
if(timer) clearTimeout(timer);
timer = setTimeout(()=>{
fun.apply(this,args)
},delay)
}
}
防抖函数讲解: 知识点:seTimeout会返回一个ID池,通过clearTimeout内传入ID值来取消timeout
- 第一次执行,没有timer跳过,直接执行setTimeout
- 第二次执行,拿到了setTimeout第一次返回的timer,在delay时间内被清空。然后继续执行setTimeout
- 第三十次,重复到第29次timer清空,执行setTimeout。这时没有第31次了,直接执行seTimeout内的函数
- 通过apply指向当前function,是因为fun是形参,不是属于Vue实例的方法,要用Vue实例调用就得改变this指向
使用方法
- 传入函数和延时const backFun = debounce(fun,delay)。 //这里fun不要加()
- 调用返回函数backFun()
- args是如果在调用backFun()时,里面可以传参数,(...args)时es6数组解构,可以传多个参数
在vue中无法直接获取组件在实例中的距离顶部的高度
可以通过$el,再获取offsetTop
this.$refs.tabControl.$el.offsetTop
但在图片为加载完成时获取的高度不准确,所以我们判断影响高度的是轮播图的高度。
获取img的加载。我们只需获取一次即可。不需要多次获取,所以无需用到防抖函数
//这里设置isLoad=false
imgLoad(){
if(!this.isLoad){
this.$emit('swiperImgLoad')
this.isLoad = true;
}
}
方案一。
一开始思路是 记录高度值,在高度到达position时设置fixed,
但BScroll的translate与fixed效果冲突
方案二。
移形换影,复制当前组件,脱离BScroll,使组件定位脱离文档流,
当到达高度,使用v-show显示复制的组件
bug解决。因复制组件所以显示被选中不一致
通过ref 1和2 获取组件,把当前index赋值(ref1和ref2)的currentIndex即可。
index相等时class效果生效
在app中,使用keep-alive有时无法回到原处。保险起见,使用activated和deactivated 在离开时记录scroll.y位置。回到组件使用scrollTo()回到原位置
获取公共组件进行插槽复用
然后在goodsItem页面监听点击发送路由和params的id,请求后在Detail页面再通过路由获取到id。
利用id来发送网络请求,并单独封装一个网络请求返回数据。利用返回数据做轮播图
在详情页请求id时,因为App主页做过keep-alive,所以我们请求到的都是一个id。
解决办法就是给 keep-alive设置exclude="Detail"属性,把Detail排除在外。
这里也可以用activated生命周期函数
当我们获取接口时,需要同时获取多个接口的数据,可以定义一个类。
这样获取数据时只需把类传递过去即可,不用传递多个接口
class Person(name,age){
this.name = name,
age = age
}
//我们想要保存一个数据,直接new一个对象传过去
const g = new Person('vicer',18);
//使用时直接获取即可
g.name;
g.age;
在vue中导入用法,其他用法不变
export class Goods{
constructor(a,b){
this.a=a;
this.b=a;
}
}
Object.keys(obj).length !== 0
解释: Object.keys用来遍历对象的属性,参数是一个对象,并返回数组
在项目中前面可以加上v-if ,这样就可以做到有数据时显示
之前使用防抖函数,判断img加载获取高度。 还可以监听img的list数量。当数量等于接口的length时,刷新页面重新计算scroll高度。
- 使用watch监听数据的变化
- 在wathc函数内获取图片length保存为current。
- 再定义变量count保存图片加载次数,每次加载+1
- 当count===current时emit发送数据,刷新页面
这个方法适用于简单获取图片次数来计算高度 还有其他操作的话建议使用防抖函数。
把debouce返回的函数用data的属性来保存,不要用const或let,
this.backFun = debounce(fun,delay);
//这里要用data保存是因为,我们使用防抖函数时把使用方法封装到了mixin中,再通过methods使用, ,通过methods保存的变量函数使用完会被销毁,每次都要重新创建。所以要在mixin使用data保存。
methods:{
imgLoad(){
this.backFun()
}
}
在后台给出的一般都是时间戳。 我们需要进行时间格式化
这里我们有两种方法。
export function formatDate(date, fmt) {
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
}
let o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
};
for (let k in o) {
if (new RegExp(`(${k})`).test(fmt)) {
let str = o[k] + '';
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str));
}
}
return fmt;
};
function padLeftZero (str) {
return ('00' + str).substr(str.length);
};
将代码引入,将时间戳*1000,并转换成Date对象
filters:{
showDate(value){
let date = new Date(value*1000) ;
return formatDate(date,'yyyy-MM-dd');
}
}
在实例中使用过滤器即可
安装
npm install moment --save
引入
import moment from 'moment'
使用过滤器定义函数
filters:{
formatDate(time) {
const date = new Date(time);
return moment(time).format('YYYY-MM-DD hh:mm:ss');
}
}
我们需要获取哪个就把格式写上,无需则省略
格式:年-月-日 时:分:秒
yyyy-MM-dd hh:mm:ss
使用时我们做了加载图片刷新页面。但我们在detail页面加载的时候home页面也在刷新
解决办法一: 判断路由
if(this.$rout.path.indexOf('/home')){
this.$bus.$emit('homeItemImgLod');
}else if(this.$rout.path.indexOf('/detail')){
this.$bus.$emit('detailItemImgLod');
}
解决办法二: 使用混入,导入防抖函数
这里不用继承,而是用混入。继承是继承原来的变量,混入是返回一个新的变量
- 在使用防抖函数中的回调方法抽取出来,赋值给一个data属性。
- 把data和使用防抖回调一起导入进mixin中,然后再导入mixin data(){ return{imgLoadListener:null} }, mounted(){ let refresh = debounce(this.$refs.scroll.refresh,50); this.imgLoadListener = ()=> {refresh()}; this.$bus.$on('itemImgLoad', this.imgLoadListener); }
需求:点击标题时滚动到指定内容
问题点,在哪里获取正确的offsetTop
- created不行,没有获取到元素
- mounted也不行,数据还没获取到
- 数据回调也不行,DOM还没渲染完
- $nextTick也不行,图片高度没有计算在内
- 在图片加载完成后获取,才能获取正确高度
做法,使用ref获取每个组件的高度,然后保存在数组。利用点击事件切换index来切换高度
需求:滚动内容切换标题样式。
做法,判断高度区间来切换数组标题的下标
for(let i=0;i<length;i++){
if( this.currentIndex !== i && ((i<length-1 && positionY > this.themeTopYs[i] && positionY < this.themeTopYs[i+1]) ||
( i === length-1 && positionY >= this.themeTopYs[i])) ){}
}
上面的做法太繁琐,让我们hack一点。
在数组末尾push中数组最大值
this.themeTopYs.push(Number.MAX_VALUE);
只需判断区间内的高度即可,如i>0 && i<100。因为添加了最大数,遍历时记得length-1
for(let i=0;i<length-1;i++){
if(this.currentIndex !== i && (positionY >= this.themeTopYs[i] && positionY < this.themeTopYs[i+1])){
this.currentIndex =i;
this.$refs.navBar.currentIndex = this.currentIndex;
}
}
把methods,data,components一起封装进mixin,然后直接导入使用。 记得需要调用封装好的backTopFun函数传入position。
把需要展示的数据列出,并组成对象。 使用vuex,对比购物车cartList中商品的id和传入的新商品id,有的话把商品的count+=1, 没有的话直接设置为1 ,push进cartList中
for循环方法
// let oldGoods = null;
// for(let item in state.cartList){
// if(item.iid = payload.idd){
// oldGoods = item
// }
// }
利用find数组方法,代替for。
//如果函数内判断为true,则返回item。
let oldGoods = state.cartList.find( item => payload.iid === item.iid);
购物车商品应封装一个模块,点击按钮也进行一个封装。 默认在加入购物车时为选中,所以在vuex中push时把checked设为true
选中按钮动态绑定class,并通过props传入checked的布尔值。 监听组件点击使用click.native,点击时进行布尔取反即可
获取vuex中cartList,通过filter进行判断,item.checked为true则返回item。 然后再通过链式编程,继续把item通过reduce进行累加,价格*数量+之前的返回值。 最后还可以确定小数点后两位。
totalPrice(){
return this.$store.state.cartList.filter( item =>{
return item.checked;
}).reduce( (preValue,item) =>{
return preValue+item.price*item.count;
},0).toFixed(2)
}
选中的数量就非常简单了,在filter后面加个length即可
在bottomBar中的按钮,使用父传子,把isChecked的布尔值传过去即可。
使用计算属性计算
方法1
//every无法判断空数组情况,所以要先判断length
if(this.cartList.length === 0) return false;
return this.cartList.every( item =>{
return item.checked;
})
方法2
//当没有商品时length返回的是0,需要返回布尔值返回0会报错,所以我们这里用双!!,进来类型转换
return !!(this.cartList.length && this.cartList.every( item => item.checked ))
监听点击,如果按钮为全部选中状态,点击则全部取消。 如果为部分选中,点击则全部选中
//由于之前已经计算过全选状态,直接if判断即可
if(this.isSelectAll){
this.cartList.forEach(item => item.checked = false)
}else{
this.cartList.forEach(item => item.checked = true)
}
Toast:商品添加成功弹窗
toast不要再商品push后面直接显示,而是要监听数据是否有添加。
在actions中,使用new promise的回调,把添加成功信息回调出来,再用then做显示操作
在vue中如果我们导入组件还需要再特定位置引用和设置参数等,我们可以把一个组件封装成一个插件, 直接一行代码就可以使用封装的组件
//3.导入Toast组件
import Toast from "./Toast";
//1.创建index文件,封装对象并导出
const obj = {};
//2.当使用Vue.use时自动执行install,并且会把Vue传递进去
obj.install = function (Vue) {
//4.创建组件构造器
const toastConstructor = Vue.extend(Toast);
//5.通过new的方式,根据组件构造器可以创建出来一个组件对象
const toast = new toastConstructor();
//6.将组件对象,手动挂载到某一个元素上
toast.$mount(document.createElement('div'));
//7.这里toast.$el对应的就是div
document.body.appendChild(toast.$el);
//8.把toast挂载到vue原型上,通过vue的$toast方法来调用toast对象
Vue.prototype.$toast = toast;
};
export default obj
- 在main.js中导入,并使用vue进行安装 import toast from './toast' Vue.use(toast)
- 通过this.$toast来调用对象(组件)toast的方法把~ this.$toast.show();
步骤:
- 安装fastclick插件 npm install fastclick --save
- 导入 import FastClick from 'fastclick'
- 调用attach函数 FastClick.attach(document.body);
如果使用fastclick报错,是因为处理函数touchstart默认的passive: true,忽略掉preventDefault() 就可以了。
也可以为容器设置css属性touch-action,去除滑动时默认现象产生,但不影响事件触发
body{
touch-action: none;
}
Vue-Lazyload
-
安装 npm install vue-lazyload --save
-
导入 import VueLazyLoad from 'vue-lazyload' //里面的参数可传可不传 Vue.use(VueLazyLoad,{ //占位图 loading:require('./img/placeholder.png') })
-
使用:把:scr 换成v-lazy
移动端适配方案postcss-px-to-viewport
一般设计稿都是按照iPhone6(750*1334)来设计的
所以视窗的宽高一般是375*667
-
安装 npm install postcss-px-to-viewport --save-dev
-
配置文件
配置在根目录下postcss.config.js
module.exports = {
plugins: {
autoprefixer: {},
"postcss-px-to-viewport": {
viewportWidth: 375, // 视窗的宽度,对应的是我们设计稿的宽度.
viewportHeight: 667, // 视窗的高度,对应的是我们设计稿的高度.(也可以不配置)
unitPrecision: 5, // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除)
viewportUnit: 'vw', // 指定需要转换成的视窗单位,建议使用vw
selectorBlackList: ['ignore', 'tab-bar', 'tab-bar-item','button-bar','bottom-bar'], // 指定不需要转换的类,后面再讲.
minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位.
mediaQuery: false, // 允许在媒体查询中转换`px`
exclude:[/^TabBar/,/^tab-control/,/^TabControl/] //排除文件的转换,数组中放正则表达式
},
}
}
服务器->操作系统->window(.net)/Linux->tomcat/nginx(软件/反向代理)
一:将自己的电脑作为服务器 -> window ->nginx
- 下载Stable version稳定的window版本
- 解压到英文目录下
二、配置文件
- 粗暴:可以直接把dist内的文件复制到html文件夹中。
- 优雅:在conf文件夹中找到nginx.conf配置文件,把locations的root html注释并改成dist,更新配置(无需停止再重启) location / { #root html; root dist; index index.html index.htm; }
三、nginx命令
注意:不要直接双击启动
启动命令 start nginx
更新配置 nginx -s reload
退出命令 nginx -s quit
强行停止 nginx -s stop
四、启动服务查看网页
在网址输入,无需输入端口,默认80
localhost
买服务器之后安装如下软件
secureCRT 在命令行下部署 winsco 使用文件方式部署
Xshell B站弹幕推荐 FinalShell B站弹幕推荐
- 如何理解生命周期
- 如何进行非父子组件通信
不要认为数据发生改变,页面跟着更新是理所当然的
const obj = {
name:'vicer',
age:18
}
//在对象中,无法直接监听对象的改变,
//1.通过defineProperty来监听对象属性的改变
//2.当数据发生改变,vue要通知哪些人,界面发生刷新
Object.keys(obj).forEach(key =>{
//遍历下标获取value
let value = obj[key]
Object.defineProperty(obj,key,{
set(newValue){
//当更新数据自动执行set
//我们如何在监听到数据发生改变,要通知哪些人呢??
//谁在用通知谁。通过解析html代码,获取哪些人在用属性,
console.log('设置了'+key+'的数据');
//通知数据发生改变。
dep.notify();
value = newValue
},
get(){
//当获取函数自定执行set
//当获取的时候会自动调用get,get便会记录谁调用了。
console.log('获取了'+key+'的数组');
return value;
}
})
})
//发布订阅者模式
//发布者
class Dep{
constructor(){
//记录谁订阅了信息更新
this.subs = []
}
addSub(wacther){
this.subs.push(wacther)
}
//发送通知
notify(){
this.subs.forEach(item =>{
//当发送通知时更新数据
item.updata();
})
}
}
//订阅者
class Wacther{
constructor(name){
this.name = name
}
updata(){
console.log(this.name + '更新了数据');
}
}
const dep = new Dep();
//多个订阅者
const w1 = new Wacther('张三')
dep.addSub(w1);
const w2 = new Wacther('李四')
dep.addSub(w2);
const w3 = new Wacther('王五')
dep.addSub(w3);
- 从new Vue() => Observe 这里Observe其实就是defineProperty,来监听data改变
- Observe => Dep 一个属性对应一个Dep。如name对应一个,age对应一个。然后把订阅者添加到subs里。
- 从new Vue() => Complie => Watcher 这里通过{{}}语法解析模板,并通过Watcher订阅数据变化,绑定更新函数到Dep中
- Dep => Watcher =>View Dep通知订阅者Wathcer,Wathcer再updata更新View,就完成了一整套的响应式
在接收发射事件内,使用scrollTo回到tab-control的高度即可
配置两个接口,使用axios的promise的catch回调函数,调用接口二并传入参数