移动端适配
相对于PC端来讲,移动端设备分辨率百花齐放,千奇百怪,对于每个开发者来讲,移动端适配是咱们进行移动端开发第一个须要面对的问题。css
在移动端咱们常常能够在head标签中看到这段代码:html
<meta name='viewport' content='width=device-width,initial-scale=1,user-scale=no' />
经过meta标签对viewport的设置,定义了页面的缩放比例;要了解这些参数的意义,咱们须要先知道几个视口宽度的意义。前端
-
layoutviewport布局宽度,就是网页的宽度 -
visualviewport但是宽度,就是浏览器窗口的宽度,这个值决定了咱们手机一屏能看到的内容;visualviewport和layoutviewport的 -
大小关系,决定了是否会出现滚动条,当visualviewport更大或者恰好等于layoutviewport时是不会出现滚动条的。 -
idealviewport为浏览器定义的可完美适配移动端的viewport,固定不变,能够认为是设备视口宽度device-width。
meta的设置其实就是对layoutviewport和visualviewport进行设置。vue
-
width=device-width表示页面宽度layoutviewport与设备视口宽度idealviewport一致 -
initial-scale=1表示页面宽度和网页宽度与设备视口宽度的初始缩放比例,visualviewport由这个比例决定,可是对于layoutviewport来讲,它同时受到两个属性的影响,而后取其中较大的那个值。
user-scale=no禁止缩放
因此如今咱们知道,这段在移动端常见的代码的意思,即将visualviewport和layoutviewport设置为idealviewport的值;这样咱们在移动端就不会出现滚动条,网页内容能够比较好的展现出来,在这个前提下咱们再考虑页面的适配问题。webpack
UI出图的时候通常是有一个固定的宽度的,而咱们实际的移动端设备的宽度却都不太同样,可是若是页面元素的缩放比例和页面宽度的缩放比例一致,在不一样尺寸的设备下咱们网页的效果也将会是一致的。web
使用相对单位
rem
rem 是相对于根元素 html 的 font-size 来作计算。一般在页面初始化时加载时经过对document.documentElement.style.fontSize 设置来实现。通常咱们将根元素html的font-size设置为宽度的1/10,不一样设备的宽度不一样,可是一样数值的rem比例与设备的宽度比例是一致的。面试
document.documentElement.style.fontSize = document.documentElement.clientWidth / 10 + 'px';
在实际项目中咱们无须在开发中本身进行转换,可使用pxtorem在输出的时候将px转换为rem。正则表达式
视口单位
将视口宽度window.innerWidth和视口高度window.innerHeight(即layoutviewport)等分为 100 份。算法
vw : 1vw 为视口宽度的 1% vh : 1vh 为视口高度的 1% vmin : vw 和 vh 中的较小值 vmax : 选取 vw 和 vh 中的较大值vue-router
和rem相比较,视口单位不须要使用js对根元素进行设置,兼容性稍差,可是大部分设备都已经支持了,一样的无须再开发时进行单位换算,直接使用相关的插件postcss-px-to-viewport在输出的时候进行转换。
修改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,咱们开发时能够直接使用设计稿尺寸。
布局样式
布局的方式能够是各类各样的,可是出于兼容性的考虑,有些样式咱们最好避免使用,难以解决的问题,那就不去面对。
须要谨慎对待的fixed
position:fixed在平常的页面布局中很是经常使用,在许多布局中起到了关键的做用。它的做用是:position:fixed的元素将相对于屏幕视口(viewport)的位置来指定其位置。而且元素的位置在屏幕滚动时不会改变。可是,在许多特定的场合,position:fixed的表现与咱们想象的截然不同。
-
iOS弹出键盘;软键盘唤起后,页面的 fixed元素将失效(iOS认为用户更但愿的是元素随着滚动而移动,也就是变成了 absolute定位),既然变成了absolute,因此当页面超过一屏且滚动时,失效的 fixed 元素就会跟随滚动了。 -
当元素祖先的 transform 属性非 none时,定位容器由视口改成该祖先。说的简单点,就是position:fixed的元素会相对于最近的而且应用了transform的祖先元素定位,而不是窗口。致使这个现象的缘由是使用了transform的元素将建立一个新的堆叠上下文。堆叠上下文(Stacking Context):堆叠上下文是 HTML 元素的三维概念,这些 HTML 元素在一条假想的相对于面向(电脑屏幕的)视窗或者网页的用户的z 轴上延伸,HTML元素依据其自身属性按照优先级顺序占用层叠上下文的空间。顺序以下图所示,总之堆叠上下文会对定位关系产生影响。想要进一步能够查看不受控制的position:fixed。
键盘弹出与使用transform属性的状况在移动端是很常见的,因此须要谨慎使用position:fixed。
推荐使用flex
flex,即弹性布局,移动端兼容性较好,可以知足大部分布局需求。如今咱们使用flex来实现h5中常见的顶部标题栏+中部滚动内容+底部导航栏的布局
页面跳转
转场动画
在vue中咱们经过vue-router来管理路由,每一个路由跳转相似与在不一样的页面之间进行切换,从用户友好的角度来讲,每次切换页面的时候最好添加一个转场效果。若是转场动画不区分路由是打开新页面、仍是返回以前页面咱们只须要在外使用添加一个动画效果便可;可是通常打开和返回是应用不一样的动画效果的,因此咱们须要在切换路由的时候区分路由是前进仍是后退。为了区分路由的动做,咱们在路由文件中设置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>
登陆跳转
虽然这样可以实现跳转效果,可是须要在编写router时添加设置,比较麻烦;咱们可使用开源项目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绑定数据
v-model是语法糖,它的本质是对组件事件进行监听和数据进行更新,是props和 o n 监 听 事 件 的 缩 写 , v − m o d e l 默 认 传 递 v a l u e , 监 听 i n p u t 事 件 。现 在 我 们 使 用 v − m o d e l 来 实 现 下 数 字 输 入 框 , 这 个 输 入 框 只 能 输 入 数 字 , 在 组 件 中 我 们 只 需 要 定 义 v a l u e 来 接 受 传 值 , 然 后 在 输 入 值 满 足 我 们 输 入 条 件 ( 输 入 为 数 字 ) 的 时 候 使 用 on监听事件的缩写,v-model默认传递value,监听input事件。如今咱们使用v-model来实现下数字输入框,这个输入框只能输入数字,在组件中咱们只须要定义value来接受传值,而后在输入值知足咱们输入条件(输入为数字)的时候使用 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";`
}
}
}
}
}
另外vux、mint-ui也是很好的选择。
经常使用插件
better-scroll是一个为移动端各类滚动场景提供丝滑的滚动效果的插件,若是在vue中使用能够参考做者的文章当 better-scroll 碰见 Vue。
最后

本文分享自微信公众号 - 前端瓶子君(pinzi_com)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。