相对于PC端来讲,移动端设备分辨率百花齐放,千奇百怪,对于每个开发者来讲,移动端适配是咱们进行移动端开发第一个须要面对的问题。css
在移动端咱们常常能够在head标签中看到这段代码:html
<meta name='viewport' content='width=device-width,initial-scale=1,user-scale=no' /> 复制代码
经过meta标签对viewport的设置,定义了页面的缩放比例;要了解这些参数的意义,咱们须要先知道几个视口宽度的意义。vue
layoutviewport
布局宽度,就是网页的宽度visualviewport
但是宽度,就是浏览器窗口的宽度,这个值决定了咱们手机一屏能看到的内容;visualviewport
和layoutviewport
的大小关系,决定了是否会出现滚动条,当visualviewport
更大或者恰好等于layoutviewport
时是不会出现滚动条的。idealviewport
为浏览器定义的可完美适配移动端的viewport,固定不变,能够认为是设备视口宽度device-width
。meta的设置其实就是对layoutviewport
和visualviewport
进行设置。webpack
width=device-width
表示页面宽度layoutviewport
与设备视口宽度idealviewport
一致initial-scale=1
表示页面宽度和网页宽度与设备视口宽度的初始缩放比例,visualviewport
由这个比例决定,可是对于layoutviewport
来讲,它同时受到两个属性的影响,而后取其中较大的那个值。user-scale=no
禁止缩放因此如今咱们知道,这段在移动端常见的代码的意思,即将visualviewport
和layoutviewport
设置为idealviewport
的值;这样咱们在移动端就不会出现滚动条,网页内容能够比较好的展现出来,在这个前提下咱们再考虑页面的适配问题。git
UI出图的时候通常是有一个固定的宽度的,而咱们实际的移动端设备的宽度却都不太同样,可是若是页面元素的缩放比例和页面宽度的缩放比例一致,在不一样尺寸的设备下咱们网页的效果也将会是一致的。github
rem 是相对于根元素 html 的 font-size 来作计算。一般在页面初始化时加载时经过对document.documentElement.style.fontSize 设置来实现。通常咱们将根元素html的font-size设置为宽度的1/10,不一样设备的宽度不一样,可是一样数值的rem比例与设备的宽度比例是一致的。web
document.documentElement.style.fontSize = document.documentElement.clientWidth / 10 + 'px'; 复制代码
在实际项目中咱们无须在开发中本身进行转换,可使用pxtorem在输出的时候将px转换为rem。正则表达式
将视口宽度window.innerWidth
和视口高度window.innerHeight
(即layoutviewport
)等分为 100 份。vue-router
vw : 1vw 为视口宽度的 1% vh : 1vh 为视口高度的 1% vmin : vw 和 vh 中的较小值 vmax : 选取 vw 和 vh 中的较大值vue-cli
和rem相比较,视口单位不须要使用js对根元素进行设置,兼容性稍差,可是大部分设备都已经支持了,一样的无须再开发时进行单位换算,直接使用相关的插件postcss-px-to-viewport在输出的时候进行转换。
以前咱们提到了layoutviewport
布局宽度实际上不是一个固定值,而是经过meta设置属性,经过idealviewport
计算出来的值,咱们能够经过控制meta的属性来将layoutviewport
固定为某一个值。通常设计图的宽度为750px,如今咱们的目标就是将layoutviewport
设置为750px;layoutviewport
受到两个属性的影响,width属性咱们之间设置为750,initial-scale缩放比例应该为idealviewport
的宽度/750;当咱们未改变meta标签属性的时候,layoutviewport
的值其实就是idealviewport
的值,因此能够经过document.body.clientWidth
或者window.innerWidth
来获取。
;(function () { const width = document.body.clientWidth || window.innerWidth const scale = width / 750 const content = 'width=750, initial-scale=' + scale + ', minimum-scale=' + scale + ', maximum-scale=' + scale + ', viewport-fit=cover' document.querySelector('meta[name="viewport"]').content = content })() 复制代码
设置完成以后,layoutviewport
在不一样的设备中会始终保持为750px,咱们开发时能够直接使用设计稿尺寸。
布局的方式能够是各类各样的,可是出于兼容性的考虑,有些样式咱们最好避免使用,难以解决的问题,那就不去面对。
position:fixed
在平常的页面布局中很是经常使用,在许多布局中起到了关键的做用。它的做用是: position:fixed
的元素将相对于屏幕视口(viewport)的位置来指定其位置。而且元素的位置在屏幕滚动时不会改变。 可是,在许多特定的场合,position:fixed
的表现与咱们想象的截然不同。
position:fixed
的元素会相对于最近的而且应用了transform的祖先元素定位,而不是窗口。致使这个现象的缘由是使用了transform的元素将建立一个新的堆叠上下文。堆叠上下文(Stacking Context):堆叠上下文是 HTML 元素的三维概念,这些 HTML 元素在一条假想的相对于面向(电脑屏幕的)视窗或者网页的用户的 z 轴上延伸,HTML 元素依据其自身属性按照优先级顺序占用层叠上下文的空间。顺序以下图所示,总之堆叠上下文会对定位关系产生影响。想要进一步能够查看不受控制的 position:fixed。
键盘弹出与使用transform属性的状况在移动端是很常见的,因此须要谨慎使用position:fixed
。
flex-direction: column;
而且占据整个窗口的弹性盒子,而后里面的布局,应该是首尾为固定高度,中间内容区域为
flex: 1;
。
html, body {
padding: 0;
margin: 0;
}
.page {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
display: flex;
flex-direction: column;
.page-content {
flex: 1;
overflow-y: auto;
}
}
复制代码
而后再来实现底部标题栏,底部标题栏通常由居中标题和左右操做区域组成;为了实现中间区域标题居中,咱们左右两部分应该保持相同的宽度。
<template>
<div class="header">
<div class="header__left">
左
</div>
<div class="header__main">
<slot>Title</slot>
</div>
<div class="header__right">
<div>右</div>
</div>
</div>
</template>
<script>
export default {
name: 'HeadBar'
}
</script>
<style lang="scss" scoped>
.header {
display: flex;
width: 100%;
line-height: 88px;
height: 88px;
font-size: 36px;
background-color: #42b983;
z-index: 999;
color: #fff;
transition: background-color .5s ease;
.header__main {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.header__left, .header__right {
padding: 0 16px;
width: 120px;
}
.header__left {
text-align: left;
}
.header__right {
text-align: right;
}
}
</style>
复制代码
底部导航栏主体部分,实际上是单个导航选项平分导航栏;而每一个导航选项,是一个方向为flex-direction: column;
布局方式横向为align-items: center;
,竖向为justify-content: space-around;
<template>
<div class="taber">
<div class="taber-item">
<div class="icon"></div>
<span>选项1</span>
</div>
<div class="taber-item">
<div class="icon"></div>
<span>选项2</span>
</div>
<div class="taber-item">
<div class="icon"></div>
<span>选项3</span>
</div>
<div class="taber-item">
<div class="icon"></div>
<span>选项4</span>
</div>
</div>
</template>
<script>
export default {
name: 'BottomTaber',
data () {
return {
}
}
}
</script>
<style lang="scss" scoped>
.taber {
background-color: #42b983;
color: #fff;
height: 88px;
display: flex;
.taber-item {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
}
.icon {
width: 36px;
height: 36px;
background-color: #fff;
}
}
</style>
复制代码
在vue中咱们经过vue-router来管理路由,每一个路由跳转相似与在不一样的页面之间进行切换,从用户友好的角度来讲,每次切换页面的时候最好添加一个转场效果。若是转场动画不区分路由是打开新页面、仍是返回以前页面咱们只须要在<router-view>
外使用<transition>
添加一个动画效果便可;可是通常打开和返回是应用不一样的动画效果的,因此咱们须要在切换路由的时候区分路由是前进仍是后退。为了区分路由的动做,咱们在路由文件中设置meta为数字,meta表示其路由的深度,而后监听$route,根据to、from meta值的大小设置不一样的跳转动画。若是应用到多种跳转动画,能够根据详情,具体状况具体应用。
<template> <transition :name="transitionName"> <router-view></router-view> </transition> </template> <script> export default { name: 'app', data () { return { transitionName: 'fade' } }, watch: { '$route' (to, from) { let toDepth = to.meta let fromDepth = from.meta if (fromDepth > toDepth) { this.transitionName = 'fade-left' } else if (fromDepth < toDepth) { this.transitionName = 'fade-right' } else { this.transitionName = 'fade' } } } } </script> 复制代码
vue-navigation
来实现,更加方便,无须对router进行多余的设置。
npm i -S vue-navigation
安装,在main.js中导入:
import Navigation from 'vue-navigation' Vue.use(Navigation, {router}) // router为路由文件 复制代码
在App.vue中设置:
this.$navigation.on('forward', (to, from) => { this.transitionName = 'fade-right' }) this.$navigation.on('back', (to, from) => { this.transitionName = 'fade-left' }) this.$navigation.on('replace', (to, from) => { this.transitionName = 'fade' }) 复制代码
vue-navigation插件还有一个重要的功能就是保存页面状态,与keep-alive类似,可是keep-alive保存状态没法识别路由的前进后退,而实际应用中,咱们的需求是返回页面时,但愿页面状态保存,当进入页面时但愿获取新的数据,使用vue-navigation能够很好的实现这个效果。具体使用能够查看vue-navigation有详细使用说明与案例。另外也能够尝试vue-page-stack,两个项目都能实现咱们须要的效果,vue-page-stack
借鉴了vue-navigation
,也实现了更多的功能,而且最近也一直在更新。
PS: 这里的动画效果引用自animate.scss;
以前咱们已经实现了底部导航栏的基本样式,这里咱们再作一些说明。当页面路由路径与router-link
的路由匹配时,router-link
将会被设置为激活状态,咱们能够经过设置active-class
来设置路径激活时应用的类名,默认为router-link-active
,而激活的类名还有一个router-link-exact-active
,这个类名是由exact-active-class
来设置的,一样是设置路径激活时应用的类名;active-class
与exact-active-class
实际上是由路由的匹配方式决定的。
通常路由的匹配方式是包含匹配。 举个例子,若是当前的路径是 /a 开头的,那么 也会被设置 CSS 类名。按照这个规则,每一个路由都会激活 ,而使用exact
属性可使用“精确匹配模式”。精确匹配只有当路由彻底相同的时候才会被激活。
移动端的路由守卫通常不会太复杂,主要是登陆权限的判断,咱们设置一个路由白名单,将全部不须要登陆权限的路由放入其中;对于须要登陆的路由作判断,没有登陆就跳转登陆页面,要求用户进行登陆后在访问,若是登陆后须要返回原有路由就把目标页面的路由做为参数传递给登陆页面,再在登陆后进行判断,若是存在目标页面参数就跳转目标页面,没有就跳转首页。
若是你的应用涉及到权限,那须要标注每一个路由须要的权限,在meta中设置roles,roles是数组来保存须要的权限;从后台的接口中获取用户拥有的权限和roles进行对比就能够判断是否具备相关权限了。
const whiteList = ['/login'] router.beforeEach((to, from, next) => { const hasToken = store.getters.auth if (hasToken) { if (to.path === '/login') { next({ path: '/' }) } else { const needRoles = to.meta && to.meta.roles && to.meta.roles.length > 0 if (needRoles) { const hasRoles = store.state.user.roles.some(role => to.meta.roles.includes(role)) if (hasRoles) { next() } else { next('/403') } } else { next() } } } else { if (whiteList.includes(to.path)) { next() } else { next('/login') } } }) 复制代码
在咱们的项目中,每每会使用的许多组件,通常使用频率比较高的组件为了不重复导入的繁琐通常是做为全局组件在项目中使用的。而注册全局组件咱们首先须要引入组件,而后使用Vue.component
进行注册;这是一个重复的工做,咱们每次建立组件都会进行,若是咱们的项目是使用webpack构建(vue-cli也是使用webpack),咱们就能够经过require.context
自动将组件注册到全局。建立components/index.js
文件:
export default function registerComponent (Vue) { /** * 参数说明: * 1. 其组件目录的相对路径 * 2. 是否查询其子目录 * 3. 匹配基础组件文件名的正则表达式 **/ const modules = require.context('./', false, /\w+.vue$/) modules.keys().forEach(fileName => { // 获取组件配置 const component = modules(fileName) // 获取组件名称,去除文件名开头的 `./` 和结尾的扩展名 const name = fileName.replace(/^\.\/(.*)\.\w+$/, '$1') // 注册全局组件 // 若是这个组件选项是经过 `export default` 导出的, // 那么就会优先使用 `.default`, // 不然回退到使用模块的根。 Vue.component(name, component.default || component) }) } 复制代码
以后在main.js
中导入注册模块进行注册,使用require.context
咱们也能够实现vue插件和全局filter的导入。
import registerComponent from './components' registerComponent(Vue) 复制代码
v-model
是语法糖,它的本质是对组件事件进行监听和数据进行更新,是props和$on监听事件的缩写,v-model
默认传递value
,监听input
事件。如今咱们使用v-model
来实现下数字输入框,这个输入框只能输入数字,在组件中咱们只须要定义value来接受传值,而后在输入值知足咱们输入条件(输入为数字)的时候使用$emit
触发input
事件。
<template> <div> <input type="text" :value="value" @input="onInput"> </div> </template> <script> export default { name: 'NumberInput', props: { value: String }, methods: { onInput (event) { if (/^\d+$/.test(event.target.value)) { this.$emit('input', event.target.value) } else { event.target.value = this.value } } } } </script> 复制代码
使用的时候,咱们只须要使用v-model
绑定值就能够了。 v-model
默认会利用名为value
的prop
和名为input
的事件,可是不少时候咱们想使用不一样的prop
和监听不一样的事件,咱们可使用model
选项进行修改。
Vue.component('my-checkbox', { model: { prop: 'checked', event: 'change' }, props: { // this allows using the `value` prop for a different purpose value: String, // use `checked` as the prop which take the place of `value` checked: { type: Number, default: 0 } }, // ... }) 复制代码
<my-checkbox v-model="foo" value="some value"></my-checkbox> 复制代码
上述代码至关于:
<my-checkbox :checked="foo" @change="val => { foo = val }" value="some value"> </my-checkbox> 复制代码
在不少第三方组件库中,咱们常常看到直接使用插件的方式调用组件的方式,好比VantUI的Dialog弹出框组件,咱们不但可使用组件的方式进行使用,也能够经过插件的形式进行调用。
this.$dialog.alert({ message: '弹窗内容' }); 复制代码
将组件做为插件使用的原理其实并不复杂,就是使用手动挂载Vue组件实例。
import Vue from 'vue'; export default function create(Component, props) { // 先建立实例 const vm = new Vue({ render(h) { // h就是createElement,它返回VNode return h(Component, {props}) } }).$mount(); // 手动挂载 document.body.appendChild(vm.$el); // 销毁方法 const comp = vm.$children[0]; comp.remove = function() { document.body.removeChild(vm.$el); vm.$destroy(); } return comp; } 复制代码
调用create
传入组件和props
参数就能够获取组件的实例,经过组件实例咱们就能够调用组件的各类功能了。
<template> <div class="loading-wrapper" v-show="visible"> 加载中 </div> </template> <script> export default { name: 'Loading', data () { return { visible: false } }, methods: { show () { this.visible = true }, hide () { this.visible = false } } } </script> <style lang="css" scoped> .loading-wrapper { position: absolute; top: 0; bottom: 0; width: 100%; background-color: rgba(0, 0, 0, .4); z-index: 999; } </style> <!--使用--> const loading = create(Loading, {}) loading.show() // 显示 loading.hide() // 关闭 复制代码
移动端各类组件、插件已经相对完善,在项目开发中重复造轮子是一件很不明智的事情;开发项目时咱们能够借助第三方组件、插件提升咱们的开发效率。
VantUI是有赞开源的一套轻量、可靠的移动端Vue组件库;支持按需引入、主题定制、SSR,除了经常使用组件外,针对电商场景还有专门的业务组件,若是是开发电商项目的话,推荐使用。官方文档关于主题定制是在webpack.config.js
中进行设置的:
// webpack.config.js module.exports = { rules: [ { test: /\.less$/, use: [ // ...其余 loader 配置 { loader: 'less-loader', options: { modifyVars: { // 直接覆盖变量 'text-color': '#111', 'border-color': '#eee' // 或者能够经过 less 文件覆盖(文件路径为绝对路径) 'hack': `true; @import "your-less-file-path.less";` } } } ] } ] }; 复制代码
但咱们的项目多是使用vue-cli构建,这时咱们须要在vue.config.js
中进行设置:
module.exports = { css: { loaderOptions: { less: { modifyVars: { 'hack': `true; @import "~@/assets/less/vars.less";` } } } } } 复制代码
better-scroll是一个为移动端各类滚动场景提供丝滑的滚动效果的插件,若是在vue中使用能够参考做者的文章当 better-scroll 碰见 Vue。
swiper是一个轮播图插件,若是是在vue中使用能够直接使用vue-awesome-swiper,vue-awesome-swiper
基于Swiper4
,而且支持SSR。