注:华润APP不能使用新框架开发,目前在原基础上优化,不使用新的框架推倒重来。
华润APP开发时间比较早,当时的技术选型也比较老,引用很多第三方包,导致整体项目过重,做到后面发现,页面文件,样式文件,js文件越来越多,且命名不规范,导致版本迭代过程中,改了一个小功能,其他地方也需要跟着改,代码不易维护,每一个页面引用的静态文件基本一致,从a.html 跳转到b.html是整个页面刷新,需要重新加载静态资源,每一次跳转都会创建新的vue实例,造成资源浪费,影响页面渲染速度,页面之间的跳转动画不好开发,消息提示不统一,页面没有做到屏幕适配,请求嵌套过多等,导致后面的问题接踵而至。
由于APP端不能更换现有框架,现在南宁项目启动,我以一南宁项目为优化试点,采用一个大模块一个页面,大模块之间跳转采用Mpa模式,每个大模块页面中的功能使用模板的形式,静态资源只加载一次,以路由的方式跳转,增加路由切换动画,列表过渡效果,每一步点击(点击按钮/列表点击/Tab切换)增加水波纹点击效果,提示信息统一从顶部弹出,将项目中的ajax请求方式替换为promise+axios的形式,使用这个形式主要解决请求多层嵌套,合并发送请求的问题。
优化过程中,删除用不到的第三方库,图片,css,js等静态资源,优化APP工程目录,压缩css以及JS文件,减少包的体积,提升用户体验。
登录,首页,南宁报修,南宁货物放行,日常巡检
一个功能一个页面,如add.html、edit.html、list.html,一个完整的功能模块有6到7个页面左右
每一个大模块是一个HTML界面,这个HTML界面里包含该模块下的所有功能,整个项目的页面数量,与大的模块数量基本一致,大模块之间跳转,例如:点击登录跳转到主页,使用Mpa模式,模块中的功能,例如:点击报修列表跳转到详情,使用Spa模式。
注意:根据设计稿实际情况调整算法。
1、scss文件添加中添加如下代码
@function px2rem($px) { $rem: 37.5px; @return ($px/ $rem) + rem; }
2、全局的JS中添加如下代码
window.onload = function() { //获取屏幕宽度 let htmlwidth = document.documentElement.clientWidth || document.body.clientWidth; // 获取html的dom let htmldom = document.getElementsByTagName('html')[0]; //设置html的font-size htmldom.style.fontSize = htmlwidth / 10 + 'px'; window.addEventListener('resize', e => { let htmlwidth = document.documentElement.clientWidth || document.body.clientWidth; htmldom.style.fontSize = htmlwidth / 10 + 'px'; }); };
注:动画效果可自行更改,下面只是示例说明。
1、css代码示例如下
/* 点击列表或按钮水波纹效果 */ .ripple { position: relative; //隐藏溢出的径向渐变背景 overflow: hidden; } .ripple:after { content: ''; display: block; position: absolute; width: 100%; height: 100%; top: 0; left: 0; pointer-events: none; //设置径向渐变 background-image: radial-gradient(circle, #666 10%, transparent 10.01%); background-repeat: no-repeat; background-position: 50%; transform: scale(10, 10); opacity: 0; transition: transform 0.3s, opacity 0.5s; } .ripple:active:after { transform: scale(0, 0); opacity: 0.3; //设置初始状态 transition: 0s; }
2、页面中在对应的标签上添加class
<button type="button" class="ripple" @click="searchParam">搜索</button>
1、css代码示例如下
/* 路由跳转动画 */ .slide-right-enter-active, .slide-right-leave-active, .slide-left-enter-active, .slide-left-leave-active { will-change: transform; transition: all 250ms; position: absolute; } .slide-right-enter { opacity: 0; transform: translate3d(-100%, 0, 0); } .slide-right-leave-active { opacity: 0; transform: translate3d(100%, 0, 0); } .slide-left-enter { opacity: 0; transform: translate3d(100%, 0, 0); } .slide-left-leave-active { opacity: 0; transform: translate3d(-100%, 0, 0); }
2、js代码如下
$route(to, from) { if (to.meta.keepAlive == true) { if (from.name === "workorderlist") { this.transitionName = 'slide-left'; } else { this.transitionName = 'slide-right'; } } //如果to索引大于from索引,判断为前进状态,反之则为后退状态 console.log(to.meta.index > from.meta.index) if (to.meta.index > from.meta.index) { //设置动画名称 this.transitionName = 'slide-left'; } else { this.transitionName = 'slide-right'; } }
css代码如下
body { background: #f2f2f2; padding-bottom: env(safe-area-inset-bottom); -webkit-user-select: none; /* 禁止选中文本(如无文本选中需求,此为必选项) */ user-select: none; font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', STHeiti, Tahoma, Simsun, sans-serif, Roboto, Lato !important; } @supports (bottom: env(safe-area-inset-bottom)) { .menu_wkBox.back_box { padding-bottom: env(safe-area-inset-bottom); } }
属性
属性名 | 类型 | 默认值 | 说明 |
---|---|---|---|
offset | Number | 80 | 默认高度 |
代码如下
Vue.component("base-scroll", { template: `<div class="yo-scroll"> <section class="inner" :style="{ transform: 'translate3d(0, ' + top + 'px, 0)'}"> <slot> </slot> </section> </div>`, props: { offset: { type: Number, default: 80 //默认高度 } }, data() { return { top: 0, state: 0, startX: 0, startY: 0, touching: false, infiniteLoading: false, downFlag: false, //用来显示是否加载中 } } })
属性
属性名 | 类型 | 默认值 | 说明 |
---|---|---|---|
offset | Number | 80 | 默认高度 |
enableInfinite | Boolean | true | 是否启用上拉加载 |
enableRefresh | Boolean | true | 是否启用下拉刷新 |
dataList | Boolean | false | 滑到底部是否显示“暂无更多数据” |
onRefresh | Boolean | true | 下拉刷新 |
onInfinite | Boolean | true | 上拉加载 |
方法
方法名 | 方法描述 |
---|---|
touchStart | 触屏开始 |
touchMove | 触屏过程中 |
touchEnd | 触屏结束 |
refresh | 下拉刷新 |
refreshDone | 下拉刷新结束 |
infinite | 上拉加载 |
infiniteDone | 上拉加载结束 |
代码如下
/* 上拉加载下拉刷新 */ Vue.component("v-scroll", { template: `<div class="yo-scroll" :class="{'down':(state===0),'up':(state==1),refresh:(state===2),touch:touching}" @touchstart="touchStart($event)" @touchmove="touchMove($event)" @touchend="touchEnd($event)"> <section class="inner" :style="{ transform: 'translate3d(0, ' + top + 'px, 0)' }"> <div class="pull-refresh"> <slot name="pull-refresh"> <span class="down-tip">下拉更新</span> <span class="up-tip">松开刷新数据</span> <span class="refresh-tip">加载中……</span> </slot> </div> <slot> </slot> <footer class="load-more"> <slot name="load-more"> <span v-show="downFlag === false">上拉加载更多</span> <span v-show="downFlag === true">加载中……</span> </slot> </footer> <div class="nullData" v-show="dataList.noFlag">暂无更多数据</div> </section> </div>`, props: { offset: { type: Number, default: 80 //默认高度 }, enableInfinite: { type: Boolean, default: true }, enableRefresh: { type: Boolean, default: true }, dataList: { default: false, required: false }, onRefresh: { type: Function, default: undefined, required: false }, onInfinite: { type: Function, default: undefined, require: false } }, data() { return { top: 0, state: 0, startX: 0, startY: 0, touching: false, infiniteLoading: false, downFlag: false, //用来显示是否加载中 } }, methods: { touchStart(e) { this.startY = e.targetTouches[0].pageY; this.startX = e.targetTouches[0].pageX; this.startScroll = this.$el.scrollTop || 0; this.touching = true; //留着有用,不能删除 this.dataList.noFlag = false; this.$el.querySelector('.load-more').style.display = 'block'; }, touchMove(e) { if (!this.enableRefresh || this.dataList.noFlag || !this.touching) { return } let diff = e.targetTouches[0].pageY - this.startY - this.startScroll if (diff > 0) e.preventDefault() this.top = Math.pow(diff, 0.8) + (this.state === 2 ? this.offset : 0) if (this.state === 2) { // in refreshing return } if (this.top >= this.offset) { this.state = 1 } else { this.state = 0 } let more = this.$el.querySelector('.load-more'); if (!this.top && this.state === 0) { more.style.display = 'block'; } else { more.style.display = 'none'; } }, touchEnd(e) { //alert(JSON.stringify(e)) if (!this.enableRefresh) { return } this.touching = false if (this.state === 2) { // in refreshing this.state = 2 this.top = this.offset return } if (this.top >= this.offset) { // do refresh this.refresh() } else { // cancel refresh this.state = 0 this.top = 0 } //用于判断滑动是否在原地 ----begin let endX = e.changedTouches[0].pageX, endY = e.changedTouches[0].pageY, dy = this.startY - endY, dx = endX - this.startX; //如果滑动距离太短 if (Math.abs(dx) < 2 && Math.abs(dy) < 2) { this.$el.querySelector('.load-more').style.display = 'none'; this.downFlag = false; return; } //--------end-------- let outerHeight = this.$el.clientHeight, innerHeight = this.$el.querySelector('.inner').clientHeight, scrollTop = this.$el.scrollTop, ptrHeight = this.onRefresh ? this.$el.querySelector('.pull-refresh').clientHeight : 0, bottom = innerHeight - outerHeight - scrollTop - ptrHeight; if (bottom <= this.offset && this.state === 0) { this.downFlag = true; this.infinite(); } else { this.$el.querySelector('.load-more').style.display = 'none'; this.downFlag = false; } }, refresh() { this.state = 2; this.top = this.offset; setTimeout(() => { this.onRefresh(this.refreshDone) }, 1000); }, refreshDone() { this.state = 0 this.top = 0 }, infinite() { this.infiniteLoading = true setTimeout(() => { this.onInfinite(this.infiniteDone); }, 2000); }, infiniteDone() { this.infiniteLoading = false } } })
属性
属性名 | 类型 | 默认值 | 说明 |
---|---|---|---|
display | Boolean | / | 是否打开 |
title | String | 标题 | 标题 |
closable | Boolean | true | 是否显示关闭按钮 |
mask | Boolean | true | 是否显示遮罩 |
maskClosable | Boolean | true | 是否点击遮罩关闭 |
width | String | 400px | 宽度 |
inner | Boolean | false | 是否在父级元素中打开 |
方法
方法名 | 说明 |
---|---|
closeByMask | 关闭遮罩 |
closeByButton | 点击右上角关闭 |
maskClass | 控制遮罩显示隐藏 |
mainClass | 控制侧边栏显示隐藏 |
mainStyle | 控制侧边栏样式 |
代码如下
/* 侧边栏组件 */ Vue.component("drawer", { template: `<div class="drawer"> <div :class="maskClass" @click="closeByMask"></div> <div :class="mainClass" :style="mainStyle" class="main"> <div class="drawer-head"> <span>{{ title }}</span> <span class="close-btn" v-show="closable" @click="closeByButton">X</span> </div> <div class="drawer-body"> <slot/> </div> </div> </div>`, props: { // 是否打开 display: { type: Boolean }, // 标题 title: { type: String, default: '标题' }, // 是否显示关闭按钮 closable: { type: Boolean, default: true }, // 是否显示遮罩 mask: { type: Boolean, default: true }, // 是否点击遮罩关闭 maskClosable: { type: Boolean, default: true }, // 宽度 width: { type: String, default: '400px' }, // 是否在父级元素中打开 inner: { type: Boolean, default: false } }, computed: { maskClass: function() { return { 'mask-show': (this.mask && this.display), 'mask-hide': !(this.mask && this.display), 'inner': this.inner } }, mainClass: function() { return { 'main-show': this.display, 'main-hide': !this.display, 'inner': this.inner } }, mainStyle: function() { return { width: this.width, right: this.display ? '0' : `-${this.width}`, borderLeft: this.mask ? 'none' : '1px solid #eee' } } }, mounted() { if (this.inner) { let box = this.$el.parentNode box.style.position = 'relative' } }, methods: { closeByMask() { this.maskClosable && this.$emit('update:display', false) }, closeByButton() { this.$emit('update:display', false) } } })