新接触

最近接触了一下vue.js,这个框架非常强大,它跟以往我们前端的常规开发很不一样,它基于node.js,使用数据驱动的方式,避免直接去操作DOM,使用它来构建移动端的单页应用十分方便。项目中我采用vue.js + vuex + vue-router + vue-resource + vux+ webpack,来实现一个完整应用的创建,幸好的是,我所说到的这些,都是为了服务于vue.js的,所以它们与vue.js搭配起来用真的十分方便,简化了许多繁琐的DOM操作。

关于vue.js

vue.js它是一个MVVM的框架,即Model-View-ViewModel,引用官方的一张图:

vue.js是一个构建数据驱动的web界面的库。Vue.js的目标是通过尽可能简单的API实现响应的数据绑定组合的视图组件。它意味着我们在普通 HTML 模板中使用特殊的语法将 DOM “绑定”到底层数据。一旦创建了绑定,DOM 将与数据保持同步。每当修改了数据,DOM 便相应地更新。这样我们应用中的逻辑就几乎都是直接修改数据了,不必与DOM更新搅在一起。这让我们的代码更容易撰写、理解与维护。这与Angular.js有点类似。

安装

如果是急需创建一个大型应用的话,使用命令行vue-cli十分方便,它直接创建好了一个基于vue的框架
有两个极力推荐的参考项目:
cnodejs-vue
vue-zhihu-daily

Vue.js 提供一个官方命令行工具,可用于快速搭建大型单页应用。该工具提供开箱即用的构建工具配置,带来现代化的前端开发流程。只需一分钟即可启动带热重载、保存时静态检查以及可用于生产环境的构建配置的项目:vue官网

1
2
3
4
5
6
7
8
# 全局安装 vue-cli
$ npm install -g vue-cli
# 创建一个基于 "webpack" 模板的新项目
$ vue init webpack my-project
# 安装依赖,走你
$ cd my-project
$ npm install
$ npm run dev(需要一直开着)

执行完最后一步后,使用npm run build来打包项目,然后在默认的http://localhost:8080/就可以看到项目主页了。

接下来,你可以参考其他的文档安装配套使用的库:

1
2
3
4
5
6
7
8
9
10
11
12
13
安装vue-resource:
$ npm install vue-resource

安装vue-router:
$ npm install vue-router

安装vuex:
$ npm install --save vuex

安装vux样式库:
npm install vux
#安装less-loader, vuejs-templates模板默认不安装less less-loader
npm install less less-loader --save-dev

一些说明

可能很多人在开始使用vue的时候都一脸懵逼,因为这和我们平常做的常规项目确实是很不一样,踩坑是很正常的事情,那么我想分享几点可能大家在开始上手的时候遇到的疑惑点。

安装的这些库分别是干什么?

  1. 首先讲vuex,它是用来进行状态管理的,将store里的数据和页面绑定起来,并通过action进来重新分发到页面,实现数据实时改变。参考官方的图:

    Vuex 的四个核心概念分别是:
    The state tree:Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态。至此它便作为一个『唯一数据源(SSOT)』而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。
    Getters:用来从 store 获取 Vue 组件数据。
    Mutators:事件处理器用来驱动状态的变化。
    Actions:可以给组件使用的函数,以此用来驱动事件处理器 mutations

  2. vue-router
    顾名思义,主要用来路由页面使用,通过配置map函数,来定义单页应用路由的地址,避免了单页应用去到某一个页面的时候刷新而回到首页的问题,这个对于单页应用是个很大的优化。

  3. vue-resource
    这个类似于JQuery中的$.ajax,用来请求服务器端的数据。
  4. vux
    Vux = Vue + WeUI + A Bunch of Components,这个库是基于vue和微信样式开发的,所以使用起来很搭配。

vue的小分享

1.vue组件的特点是可插拔、独立作用域、观察者模式、完整的生命周期。

在Vue.js中,父子之间的通信主要通过事件来完成,这种就是我们熟悉的观察者模式(或叫订阅-发布模式),很多框架也都使用了这种设计模式,比如Angular。父组件通过Vue内置的$broadcast()向下广播事件和传递数据,子组件通过$dispatch()向上派发事件和传递数据,双方都可以在events对象内接收自定义事件,并且处理各自的业务逻辑。
引用官方模板:
子组件传给父组件:

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
<!-- 子组件模板 -->
<template id="child-template">
<input v-model="msg">
<button v-on:click="notify">Dispatch Event</button>
</template>

<!-- 父组件模板 -->
<div id="events-example">
<p>Messages: {{ messages | json }}</p>
<child></child>
</div>

-----------------------------------------------

// 注册子组件
// 将当前消息派发出去
Vue.component('child', {
template: '#child-template',
data: function () {
return { msg: 'hello' }
},
methods: {
notify: function () {
if (this.msg.trim()) {
this.$dispatch('child-msg', this.msg)
this.msg = ''
}
}
}
})

// 初始化父组件
// 将收到消息时将事件推入一个数组
var parent = new Vue({
el: '#events-example',
data: {
messages: []
},
// 在创建实例时 `events` 选项简单地调用 `$on`
events: {
'child-msg': function (msg) {
// 事件回调内的 `this` 自动绑定到注册它的实例上
this.messages.push(msg)
}
}
})

或者父组件使用:
<child v-on:child-msg="handleIt"></child>
再在methods中标明handleIt函数。

父组件传给子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 默认为单向绑定 -->
<child :msg="parentMsg"></child>

<!-- 双向绑定 -->
<child :msg.sync="parentMsg"></child>

<!-- 单次绑定 -->
<child :msg.once="parentMsg"></child>


子组件接受参数:
Vue.component('example', {
props: {
msg: {
type: String,
required: false
}
}
})

2.关于样式:
我们给按钮定义了一些基本的样式,但是我们用的css选择器就是一个.button类名,可能会和别的组件中的.button样式冲突,所以我们加入了一个scoped属性,让App.vue中的style样式只作用于这个组件内部。
注意:scoped并不会影响css的作用优先级,使用scoped不代表不会被外部样式表覆盖

1
2
3
<style scpped> 
...
</style>

3.vue动态组件
例如我们要实现一个tab切换的效果,我们设计两个tab,每点击一个tab是切换一个子组件,然后再把事件添加到两个tab上;

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
//父组件
<nav>
<ul class="nav nav-pills pull-right">
<li role="presentation">
<a href="#" @click="currentView='manage-posts'">Manage Posts</a>
</li>
<li role="presentation">
<a href="#" @click="currentView='create-post'">Create Post</a>
</li>
</ul>
</nav>

<div class="container">
<!-- render the currently active component/page here -->
<component :is="currentView" keep-alive></component>
</div>

Vue.component('manage-posts', {
template: '#manage-template',
data: function() {
return {
posts: [
'Vue.js: The Basics',
'Vue.js Components',
'Server Side Rendering with Vue',
'Vue + Firebase'
]
}
}
})

Vue.component('create-post', {
template: '#create-template'
})

new Vue({
el: 'body',
data: {
currentView: 'manage-posts'
}
})

如果把切换出去的组件保留在内存中,可以保留它的状态或避免重新渲染。为此可以添加一个 keep-alive 指令参数

4.不要将HTML的Attributes和Vue的Model混用
比如最终要实现的代码是:

1
2
3
<img src=123.jpg>
<a href="index.html?id=123"></a>
<div id="id-123"></div>

比如Vue实例为:

1
2
3
4
5
6
new Vue({
el: 'body',
data: {
id: 123
}
})

错误 的使用是:

1
2
3
<img src={{ id }}.jpg>
<a href="index.html?id={{ id }}"></a>
<div id="id-{{ id }}"></div>

正确 的使用是:

1
2
3
<img :src=id + '.jpg'>
<a :href="'index.html?id=' + id"></a>
<div :id="'id-' + id "></div>

5.使用组件的时候传的参数要用小写不能用大小写混合,或者用横杠-连接:userinfo=”userinfo”

1
2
3
4
<div>
<cmp-account :userinfo="userinfo"></cmp-account>
<cmp-task :task="task"></cmp-task>
</div>

6.理解异步更新
默认情况下, Vue 的 DOM 更新是异步执行的。理解这一点非常重要。当侦测到数据变化时, Vue 会打开一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。假如一个 watcher 在一个事件循环中被触发了多次,它只会被推送到队列中一次。然后,在进入下一次的事件循环时, Vue 会清空队列并进行必要的 DOM 更新。在内部,Vue 会使用 MutationObserver 来实现队列的异步处理,如果不支持则会回退到 setTimeout(fn, 0)。

举例来说,当你设置 vm.someData = ‘new value’,DOM 并不会马上更新,而是在异步队列被清除,也就是下一个事件循环开始时执行更新。如果你想要根据更新的 DOM 状态去做某些事情,就必须要留意这个细节。尽管 Vue.js 鼓励开发者用 “数据驱动” 的方式想问题,避免直接操作 DOM ,但有时候你可能就是想要使用某个熟悉的 jQuery 插件。这种情况下怎么办呢?你可以在数据改变后,立刻调用 Vue.nextTick(callback),并把你要做的事情放到回调函数里面。当Vue.nextTick 的回调函数执行时,DOM 将会已经是更新后的状态了。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="example">{{msg}}</div>

var vm = new Vue({
el: '#example',
data: {
msg: '123'
}
})
vm.msg = 'new message' // change data
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
vm.$el.textContent === 'new message' // true
})

除此之外,也有一个实例方法 vm.$nextTick()。这个方法和全局的 Vue.nextTick 功能一样,但更方便在组件内部使用,因为它不需要全局的 Vue 变量,另外它的回调函数的 this 上下文会自动绑定到调用它的 Vue 实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Vue.component('example', {
template: '<span>{{msg}}</span>',
data: function () {
return {
msg: 'not updated'
}
},
methods: {
updateMessage: function () {
this.msg = 'updated'
console.log(this.$el.textContent) // => 'not updated'
this.$nextTick(function () {
console.log(this.$el.textContent) // => 'updated'
})
}
}
})

总结

相对于react native来说的话,vue.js是比较轻量级的,而且相对比较容易上手,对于单页应用来说无疑是带来了福音,做完项目后也是学到了很多知识,得到了很多经验,对于组件化的开发又有了深刻的认识,站在巨人的肩膀上又看远了那么一点。

极力推荐参考文章:

  1. 【翻译】Vue动态组件
  2. Vue+Webpack开发可复用的单页面富应用教程
  3. 使用 Vuex + Vue.js 构建单页应用
  4. Vue构建单页应用最佳实战
  5. 使用 Vuex + Vue.js 构建单页应用

背景

这里的背景是实现腾讯视频与爱奇艺视频的一个视频切换功能,爱奇艺那边提供的是使用object标签实现视频的播放效果,一开始想和腾讯视频一样使用iframe标签,但是发现iframe在爱奇艺上面不能全屏,无奈之下,只好用回object。

iframe与object参数

iframe标签:

1
'<iframe id="videof" frameborder="0" width="100%"  height="'+(videoHeight)+'" src="'+src+'" allowfullscreen="true"></iframe>'

object标签:

1
2
3
4
5
6
7
8
9
10
11
 '<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0" width="100%" height="'+(videoHeight)+'" align="middle">'
+'<param name="allowscriptaccess" value="always">'
+'<param name="quality" value="high">'
+'<param name="autoStart" value="0" />'
+'<param name="wmode" value="opaque">'
+'<param name="allowfullscreen" value="true">'
+'<param name="bgcolor" value="#000000">'
+'<param name="movie" value="'+src2+'">'
+'<param name="flashvars" value="vid='+ params0[3] +';tvid='+ params0[2] +'&amp;autoPlay=1&amp;autoChainPlay=1&amp;showSearch=0&amp;showSearchBox=0&amp;autoHideControl=1&amp;showFocus=0&amp;showShare=0&amp;showLogo=0&amp;coop=coop_227_qsbk&amp;cid=qc_100001_300089&amp;bd=1&amp;showDock=0">'
+'<embed width="100%" height="'+(videoHeight)+'" allowscriptaccess="always" wmode="opaque" allowfullscreen="true" bgcolor="#000000" align="middle" flashvars="vid='+ params0[3] +'&amp;tvid='+ params0[2] +'&amp;autoPlay=1&amp;autoChainPlay=1&amp;showSearch=0&amp;showSearchBox=0&amp;autoHideControl=1&amp;showFocus=0&amp;showShare=0&amp;showLogo=0&amp;coop=coop_227_qsbk&amp;cid=qc_100001_300089&amp;bd=1&amp;showDock=0" src="'+src2+'" name="mvFlash" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer">'
+'</object>'

具体的参数我就不多说了,网上也有很多资源,我主要想讲一下浏览器对他们的兼容性。

兼容性

浏览器之间的兼容

首先IE只支持对Object的解析。
火狐,谷歌,Safari只支持对Embed的解析。
上面的例子中同时使用 object 和 embed 标签来嵌入,细心的人会发现,object 的很多参数和 embed 里面的很多属性是重复的,为什么这样做?为了浏览器兼容性,有的浏览器支持 object,有的支持 embed,这也是为什么要修改 Flash 的参数时两个地方都要改的原因。这种方法是 Macromedia 一直以来的官方方法,最大限度的保证了 Flash 的功能,没有兼容性问题。

视频平台的兼容

兼容性 web touch端 ipad
腾讯视频 iframe iframe iframe
爱奇艺 object iframe iframe(不能全屏)

因为在ipad中仍然用的是web中样式,但是ipad中是没有flash插件的,所以不能用object,只能用iframe标签,但是也可以尝试去用新出的video标签来实现。

一般实现一些>,<或者^,\/箭头的话,大多直接用图片或者图片文字,其实也可以用css来实现。

1
2
3
<div class="tvChoesn-box">
<b class="arrow-tip"><i class="arrow-tip-outer"></i><i class="arrow-tip-inner"></i></b>
</div>

对应的css:

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
.tvChoesn-box{
width:100px;
height:100px;
margin:0 auto;
position:absolute;
background: #1b1838;
}
.arrow-tip{
width:12px;
height:12px;
position:absolute;
left: 50px;
top: -12px;
}
.arrow-tip-outer,.arrow-tip-inner{
width:0px;
height:0px;
display:block;
position:absolute;
left:0px;
top:0px;
border-left:8px transparent dashed;
border-right:8px transparent dashed;
border-bottom:8px transparent dashed;
border-top:8px white solid;
overflow:hidden;
}
.arrow-tip-outer{
top:2px;
border-top:8px white solid;
}
.arrow-tip-inner{
border-top:8px #1b1838 solid;
}

实现的效果如下:

原理

实际上就是利用border来控制箭头的方向,实心箭头其实不需要.arrow-tip-inner,而这里显示的是空心箭头,实际上就是在.arrow-tip-outer上下滑2px,所以效果看起来就像是一个空心箭头一样,实际上是两个实心箭头叠在一起,只是位置有交错而已。

需求是实现一个上传封面的功能。

为什么使用参数formdata?

一般当你想把复杂的数据从一个网页(文件,非ASCII编码的内容)发送到服务器,则必须使用multipart/form-data内容类型的form,例如:

1
2
3
4
<form method="post" enctype="multipart/form-data" action="http://xxxx">
<input type="file" name="media"/>
<input type="submit" value="upload"/>
</form>

这就是我们平时做的上传文件的表单。

也许你想使用XMLHttpRequest发送文件。你想复制这种形式,这真的很难,因为你必须在JavaScript中创建multipart/form-data内容。

这时参数formdata就有用了:他重现在JavaScript中form的提交机制,XMLHttpRequest level 2(编者草案)增加了新的参数formdata接口的支持。 参数formdata对象提供了一种方法来轻松地构建一组键/值对表示表单字段及其值,然后可以使用XMLHttpRequest的send()方法以“multipart/form-data”的格式发送。

1
2
3
4
5
6
7
8
9
<div class="y-topic-info" id="bg-set" style="background-image: url({{topic_info.pic_url}});">
<div class="y-topic-info-content" data-tid="{{topic_info['topic_id']}}">
<div class="change-bg-block">
<input type="file" name="change-bg" id="change-bg" class="upload-bg" data-sel="bg-file">
<a href="javascript:;" class="change-bg-btn">上传封面</a>
</div>
</div>
<input type="hidden" id="uploadtoken" value="{{token}}"/>
</div>

这里的token字段是用户的一个信息

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
;(function () {
$('[data-sel=bg-file]').change(function(e){
var token = $('#uploadtoken').val()
var file = document.getElementById('change-bg').files[0]

//创建一个空的FormData对象,然后再用append方法逐个添加键值对:
var form = new FormData()
form.append('file', file)
form.append('token', token)

var xhr = new XMLHttpRequest()
//上传
xhr.open("POST", "http://xxxx.com", true);
//进度条
xhr.upload.addEventListener("progress", function(){
tool.showTip('上传中...',false)
}, false);

//下载,内嵌ajax
xhr.addEventListener("load", function(){
var postStr = JSON.parse(xhr.responseText).key;
var outerSrc = 'xxxx/'+ postStr

$.ajax({
url : 'xxxx',
type : 'POST',
data : {
'icon' : postStr
},
dataType: 'json',
success : function(data) {
document.getElementById('bg-set').style.backgroundImage="url("+outerSrc+")";
},
error : function(data) {
alert('上传封面失败');
}
});

}, false);

//错误信息
xhr.addEventListener("error", function(){
alert('上传封面失败');
}, false);
//取消
xhr.addEventListener("abort", function(){
//...
}, false);

//发送请求
xhr.send(form);
})
})()

除了用原生的js实现ajax上传,还可以使用jquery的ajax上传:

1
2
3
4
5
6
7
8
9
10
11
12
13
$.ajax({
type:'post',
url:"http://xxxx",
//上传格式为formData格式
data:formData,
async: false,
cache: false,
contentType: false,
processData: false,//好像不可缺!
success:function(resultStr){
//...
}
});

总结

利用Formdata对象,我们可以使用原生js通过ajax实现异步上传图片,当然,现在已经有jquery的批量上传插件了,实现原理就是利用了Formdata。

背景

讲讲今天遇到的这个bug,背景是我要做一个在移动端上上滑显示导航栏,下滑隐藏导航栏的效果,于是,我的基本思路是通过手机端的touchstarttouchend事件来计算手指触摸屏幕的横纵坐标,从而计算它的角度,用来判断是往上的方向还是往下的方向。

过程

嗯,想象很美好,于是,噼里啪啦地做好后,在苹果手机上一测,各种顺利通过,我对着手机会心一笑,需求完成。

然而,现实很残酷,产品经理跑来跟我说,安卓上的什么UC浏览器,百度浏览器,对这个效果都不显示,QQ浏览器是一会显示,一会又傻傻的。

然后,我开始找问题,终于,发现在安卓上检测不出滑动时touchend事件的执行,问题找到了。于是我上网百度,到处一片哀怨,原来这个是安卓本身的bug,安卓的浏览器用的是所下载的浏览器本身的内核,所以各种类型的内核都有,上述几种浏览器都是有基于Tridient来做的,简单来说,就是IE的内核,安卓一直在更新版本,却还是没有解决这个问题。相比之下,iphone就可爱很多,在iphone下的阿鸡阿狗浏览器,用的都是webkit内核,对各种特性支持度都十分好,这就是差距啊!

在安卓上,滑动手机屏幕的触发事件过程是这样的:

1
touchstart->touchmove*n,无touchend

在iphone上,滑动手机屏幕的触发事件过程是这样的:

1
touchstart->touchmove*n->touchend

素以,在安卓上,滑动不能用touchend事件了(这里并不是说本身不能用touchend事件,只是滑动时无这个事件而已),经过查资料,发现之前很多人说zepto中的touch.JS在微信以及安卓上用swipe的时候失效,其实就是因为用了这个touchend事件,故没有效果。

解决办法

解决办法也不是没有的,先说明一下在移动端的事件:

  1. touchstart:手指触摸手机屏幕的触发,即使已经有一个手指放在屏幕上也会触发。
  2. touchmove:手指在手机屏幕时滑动时连续触发,发生期间,调用event.preventDefault()可以阻止滚动
  3. touchend;手机触摸屏幕离开后触发
  4. touchcancel:当系统停止跟踪触摸的时候触发

解决办法一

如果屏幕不考虑滚动的话,可以在touchstart中添加event.preventDefault(),意思是取消了touchstart的默认事件,但是这样做就没有了滚动手机屏幕这个效果,这个适用于局部使用。但我觉得这种效果并不好。

解决办法二

使用touchcancel事件,在js中同时绑定touchend和touchcancel事件,这里我截取了UC浏览器中的一段描述:

我认为这种办法是比较好的,当然这也不是最好的解决办法,拿来顶替一下还是可以的。

今天突然觉得分页是个很有逻辑的东西,所以想拎出来讲讲。

需求

分页规则

根据页面内容数量,在页面底部居中显示页码数据。点击页码数字,跳转到相应页数加载内容。
1)不足10条时:不显示该页码栏
2)不足70条时:显示 上一页 1-最大页码 下一页
3)超过70条时:显示 上一页 1 2 3 4 5 … 最大页码 下一页

交互规则

1)【上一页】按钮始终不高亮,【下一页】按钮始终高亮;
2)当内容超过70条时,点击页码数X时做如下判断:
· 若X-1≤3,则显示:1至5 … 最大页码数 。如下图:

· 若X-1>3并且最大页数Y-X>3,则将该页数按钮居中,显示:1 … X-1 X X+1 … Y。如下图:

· 若Y-X≤3,则依次显示:1 … Y-4 Y-3 Y-2 Y-1 Y。如下图:

实现

此处用的是后台分页处理,没有使用ajax来请求,只用来说明分页的逻辑,后台使用的语言为python。
代码如下:

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
{% set count = page['count']%}     #页数
{% set cur = int(page['cur'])%} #当前页
{% set path = page['base_path']%} #根路径

{%if count > 1%} #当页码大于一页的时候
<div class="container">

{%if cur > 1%} #当前页码大于一的时候,可以点击上一页,否则上一页不可点击
<a class="page" href="{{path}}/{{cur - 1}}">上一页</a>
{%else%}
<a class="page" href="javascript:;">上一页</a>
{%end%}

{%if count <= 7%} #当页码小于7页的时候,展示所有的页数
{%for p in range(1, count+1)%}
<a class="page {%if p == cur%}page-active{%end%}" href="{{path}}/{{p}}">{{p}}</a>
{%end%}

{%elif count > 7%} #当页码大于7页的时候

{%if cur <= 4%} #当前页码小于4页的时候,显示1至5 ... 最大页码数
{%for p in range(1, 6)%}
<a class="page {%if p == cur%}page-active{%end%}" href="{{path}}/{{p}}">{{p}}</a>
{%end%}
<a class="page-no" href="javascript:;">...</a>
<a class="page" href="{{path}}/{{count}}">{{count}}</a>

{%elif cur >= (count - 3)%} #当前页码在最后4页的时候,显示1 ... Y-4 Y-3 Y-2 Y-1 Y
<a class="page" href="{{path}}/1">1</a>
<a class="page-no" href="javascript:;">...</a>
{%for p in range(count-4, count+1)%}
<a class="page {%if p == cur%}page-active{%end%}" href="{{path}}/{{p}}">{{p}}</a>
{%end%}

{%else%} #否则,页数在中间的时候,显示 1 ... X-1 X X+1 ... Y
<a class="page" href="{{path}}/1">1</a>
<a class="page-no" href="javascript:;">...</a>
{%for p in range(cur-1, cur+2)%}
<a class="page {%if p == cur%}page-active{%end%}" href="{{path}}/{{p}}">{{p}}</a>
{%end%}
<a class="page-no" href="javascript:;">...</a>
<a class="page" href="{{path}}/{{count}}">{{count}}</a>
{%end%}

{%end%}


{%if cur < count%} #当前页码小于页码的时候,可以点击下一页,否则下一页不可点击
<a class="page page-active" href="{{path}}/{{cur+1}}">下一页</a>
{%else%}
<a class="page page-active" href="javascript:;">下一页</a>
{%end%}

<div style="clear: both"></div>
</div>
{%end%}

分页逻辑就讲到这里吧,有空我自己再用php实现了一个用ajax请求分页的代码哈。

需求

有个需求是要求实现屏幕内容下滑隐藏导航栏,上滑显示导航栏的效果。

分析

对于移动端来说,手指在屏幕上的事件主要通过触摸事件来触发,判断手指上滑还是下滑,主要是判断手指进入屏幕到离开屏幕的角度,在js中,就利用到Math.atan2(y,x)函数,这个函数的作用是返回从x轴正方向通过逆时针旋转到达坐标点(x, y)所经过的角度(单位为弧度),返回值介于 [-π, π] 之间。这里需要脑筋急转弯一下,大家还记得弧度是怎么求来的吗?弧度的公式是:

1
atan2(y,x) = 角度*π/180度,例如60度 = 3/π

好了,知道这个函数怎么求后,就可以根据这个函数求到角度,然后就可以判断:

  1. 当 0<角度<180 时,表示上滑。
  2. 当 -180<角度<0 时,表示下滑。

其实也可以不用角度,直接用弧度也是可以的,那就变成:

  1. 当 0<角度<π 时,表示上滑。
  2. 当 -π<角度<0 时,表示下滑。

其实,这样就可以根据角度或弧度表示上右下左四哥方向了,来一张示意图:

1
2
3
4
5
6
7
8
9
       Y/\
\ 上 | /
\ | /
左 \ | / 右
________|_________>X
|
/ | \
/ | \
/ 下 | \

好,思路有了,那就开干吧,函数里的y,x怎么来的,其实就是手指进入屏幕到离开屏幕的纵横坐标差,当然这个差是分正负值的。

实现

基本代码就是下面那样了:

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
;(function () {
//返回角度
function GetSlideAngle(dx, dy) {
return Math.atan2(dy, dx) * 180 / Math.PI;
}

//根据起点和终点返回方向 1:向上,2:向下,0:未滑动
function GetSlideDirection(startX, startY, endX, endY) {
var dy = startY - endY;
var dx = endX - startX;
var result = 0;

//如果滑动距离太短
if(Math.abs(dx) < 2 && Math.abs(dy) < 2) {
return result;
}

var angle = GetSlideAngle(dx, dy);

if(angle >= 0 && angle < 180) {
result = 1;
}else if (angle >= -180 && angle < 0) {
result = 2;
}

return result;
}

var handler = function (ev) {
var endX, endY;
endX = ev.changedTouches[0].pageX;
endY = ev.changedTouches[0].pageY;
var direction = GetSlideDirection(startX, startY, endX, endY);
switch(direction) {
case 0:
//alert("没滑动");
break;
case 1:
//向上隐藏
//如果文本的高度小于屏幕可见区域的高度
if(document.documentElement.clientHeight >= document.body.clientHeight){
//
}else{

var scrollTop=0;
if(document.documentElement&&document.documentElement.scrollTop)
{
scrollTop=document.documentElement.scrollTop;
}
else if(document.body)
{
scrollTop=document.body.scrollTop;
}

//如果滚动的高度小于导航栏的高度
if(scrollTop<=$('[data-sel=nav-header]').height()){
//
}else{
$('[data-sel=nav-header]').addClass('nav-slide-up');
}
}

break;
case 2:
//向下显示
$('[data-sel=nav-header]').removeClass('nav-slide-up');
break;
default:
}
}

//滑动处理
var startX, startY;
document.addEventListener('touchstart',function (ev) {
startX = ev.touches[0].pageX;
startY = ev.touches[0].pageY;
}, false);

document.addEventListener('touchend',handler, false);
document.addEventListener('touchcancel',handler, false);

})();

每个触摸事件都包括了三个触摸列表:

  1. touches :当前位于屏幕上的所有手指的一个列表。
  2. targetTouches :位于当前DOM元素上的手指的一个列表。
  3. changedTouches :涉及当前事件的手指的一个列表。

用一个手指接触屏幕,触发事件,此时这三个属性有相同的值。

用第二个手指接触屏幕,此时,touches有两个元素,每个手指触摸点为一个值。当两个手指触摸相同元素时,targetTouches和touches的值相同,否则targetTouches 只有一个值。changedTouches此时只有一个值,为第二个手指的触摸点。

用两个手指同时接触屏幕,此时changedTouches有两个值,每一个手指的触摸点都有一个值

手指滑动时,三个值都会发生变化

一个手指离开屏幕,touches和targetTouches中对应的元素会同时移除,而changedTouches仍然会存在元素。
手指都离开屏幕之后,touches和targetTouches中将不会再有值,changedTouches还会有一个值,此值为最后一个离开屏幕的手指的接触点。
来自http://blog.sina.com.cn/s/blog_468530a60102wzkw.html

代码中为何同时用touchendtouchcancel事件呢,那是为了兼容安卓的一些浏览器,详情可以看博客中后面讲到的安卓移动端滑动不支持touchend事件

这篇主要是讲一下在开发中遇到的一些有趣的解决办法,以及一些技巧。

1. input中设置cursor:pointer无效

背景是当我要实现一个上传封面功能时,我把input标签放在上层,a标签放在下层,这时我想优化一下交互效果,当鼠标移到input上时变成手型,但是事实怎么这样做失效了,所以经过查阅资料,最终加上font-size:0;,问题解决。
font-size:0;在解决相邻的display:inline-block;元素之间存在的默认间隙也是十分有用。
例如,有很多人抱怨为什么我将两个a标签设置成display:inline-block;width:50%,而最终他们不是在同一行中,第二个a标签被挤到下一行,这是因为display:inline-block;默认有一个间距,消除这个间距的办法有很多,常见的就有加上font-size:0;

2. 编辑内容使用contenteditable=”true”

以前的提交表单内容的思想都是用到input,textarea等等表单标签,却没有想到html元素中的属性contenteditable="true"也是一个不错的选择。

contenteditable属性兼容所有浏览器(IE6之前的版本是否兼容未测试)
在有些时候我们完全可以用DIV去替代input或者textarea来达到同样的效果,例如,在使用ajax的时候,在提交表单时我们可以获取DIV的内容。

1
<div contenteditable="true">可以编辑里面的内容</div>

我们可以编辑div里面的内容,再通过ajax把内容提交给后台。

git作为代码版本控制开发工具,实在是太方便了,非常实用,本文主要围绕在项目开发过程中git的实际操作,以此总结。

git操作流程

开发前的配置

首先和别人开始一个项目前,当然是需要在github或者其他git项目管理平台上创建一个项目,每个项目都会有它的SSHhttp的项目地址,通常会使用SSH地址。

使用

1
git clone git@github.com:xurna/animation-effect.git

先将远程代码复制下来,然后你还需要在本地创建你的公钥(SSH KEY),git平台上会有相关操作,不同平台的操作可能也不一样,按照提示操作即可(一般在setting中找到ssh key就可以设置)。

而这两者的区别在于:
在管理Git项目上,很多时候都是直接使用https url克隆到本地,当然也有有些人使用SSH url克隆到本地。这两种方式的主要区别在于:使用https url克隆对初学者来说会比较方便,复制https url然后到git Bash里面直接用clone命令克隆到本地就好了,但是每次fetch和push代码都需要输入账号和密码,这也是https方式的麻烦之处。而使用SSH url克隆却需要在克隆之前先配置和添加好SSH key,因此,如果你想要使用SSH url克隆的话,你必须是这个项目的拥有者。否则你是无法添加SSH key的,另外ssh默认是每次fetch和push代码都不需要输入账号和密码,如果你想要每次都输入账号密码才能进行fetch和push也可以另外进行设置。

生成SSH key后,一开始还会让你配置你的用户名称和e-mail地址。这是非常重要的,因为每次Git提交都会使用该信息。它被永远的嵌入到了你的提交中:

1
2
3
4
git config --global user.name [username]
git config --global user.email [email]

使用git config --list查看已设配置

配完这些后你就可以对项目进行操作并提交你的代码了。

开发中的操作

实际开发中其实我用到的git命令也不算太多,但基本上满足了开发的需求。
多人开发的项目通常需要建分支来管理代码,创建一个分支(这里需要注意的是,如果你已经创建了一个分支,又想再创建一个分支,那么最好先切换到master分支,然后再创建):

1
git branch subName

创建好分支后,你就可以“为所欲为”了,因为这不影响主分支(master)上面的代码。操作前先切换到自己的分支上:

1
git checkout subName

上面的新建一个分支并切换到新的分支这两步可以一步到位,效果一样:

1
git checkout -b subName

然后如果你对自己的分支代码进行操作,如果你改了某一部分代码,可以先提交到暂存区:

1
2
3
git add [filename]
或者全部修改过的文件提交
git add .

可以再提交到仓库:

1
git commit -m "提交代码的注释"

这个过程中你可以使用

1
git status

来查看每一步的状态,它会提示你下一步需要做什么。
如果你要提交到远程分支上的话,输入

1
2
git push origin subName(或者git push)
如果你是第一次提交,他会让你先设置一个操作,会有代码提示,按照它的操作完成就好,具体我忘了。。。

这样你的代码就提交到了远程分支上了。

合并分支与冲突处理

如果自己的代码没有问题,到了发布的时候,就可以和master合并分支了,先切换到master分支:

1
git checkout master

合并分支:

1
git merge subName

情况1. 如果在你提交之前,原来master上的代码其他人没有修改过,那么这个过程就进行地很顺利。
情况2. 如果master上的代码已经修改过了,git bash会提示你先更新的你的代码再合并分支:

1
git pull origin master

这里注意一点,git pull操作是可以在本分支上拉下最新的代码的,但是如果你要拉其他分支的代码,例如,你需要在subName分支上拉master的代码,那么,在subName分支上,不能单是执行git pull,而是执行git pull origin master,如果是在master分支上,则可以直接git pull,因为他本身对应的就是master分支。

情况3. 然后再使用上面的合并分支的命令,如果这个过程中提示有代码冲突,即别人修改了和你修改的一样的地方,这个时候,你需要手动删除,例如,在文件中会有:

1
2
3
4
5
6
7
8
9
<<<<<<< HEAD

test in master

=======

test in dev

>>>>>>> dev

其中“======”代表冲突区域分隔符,分隔符上下两段代码为你和别人都修改过的代码,你需要和别人协商最后要哪段代码,然后再删去那些符号。删掉后,再git commit代码,最后在git push上去。但是这样做不好,因为别人也需要在master上提交东西,自己直接在master上处理冲突的话,对于项目处理可能会出现问题,所以这个时候,可以考虑回退当前版本,在自己分支上处理冲突,最后再提交:

1
2
3
4
5
git reset --hard HEAD
git checkout subName
git pull origin master
然后手动处理冲突,处理完再commit上去,最后push上去。
最后再git checkout master,git pull,git merge subName,git push

所有情况解决后都需要在master分支上git push才算是真正提交合并了自己修改的代码。

特殊情况

如果你现在在做的一个需求还没有做完,然后产品经理那边又出了一个修改需求,需要先上线,那么此时你并不想提交你在做的这个需求代码,因为还没有完成,这种情况,你就可以在master上新建另一个分支去操作。注意,是切换到master上新建另一个分支,新建完后,如果它还有用处,那么可以保留它,注意,你并不需要再一次git clone一个项目下来,当你切换到不同分支的时候,代码会自动变到你的那个分支上,十分方便。
如果你用完觉得这个分支再也没有用处了,那么可以删掉本地分支:

1
git branch -d subName2

可以查看本地分支和远程分支:

1
2
git branch (查看本地分支)
git branch -a (查看远程分支)

如果你修改了某个文件,但是又放弃对这个文件的修改,你可以使用:

1
git checkout text.txt

如果你想回退到某个版本号的话,可以使用:

1
git reset --hard {版本号}

使用:

1
2
3
git log
或者
git reflog

可以查看历史版本号,Git reflog 可以查看所有分支的所有操作记录(包括(包括commit和reset的操作),包括已经被删除的commit记录,git log则不能察看已经删除了的commit记录。

好了,平时操作大多是这几类操作,如果后续有新的操作,再补进来吧。

对了,在master上合并完分支后切记要切回自己的分支,不然改的就直接是master的代码了!

利用fiddler调试

其实我主要想讲的就是怎样利用fiddler工具进行页面js,css,html等文件的调试,fiddler的下载和安装都很简单,此处不说了。

为什么需要用到fiddler来调试,它主要是为了调试手机或者页面存在缓存或者需要在线下修复bug而又不影响线上表现的情况下使用。而对于调试手机以及网页上的具有缓存的文件来说,fiddler无疑是给开发者带来了福音。

调试过程

手机调试

  1. 首先设置一下fiddler,打开tool->fidder options->connection,按照下图配置:
  2. 然后在手机端设置一下http代理,找到与电脑服务器连接同一wifi的ip地址,可在cmd中ipconfig中找到wifi的ip,电脑与手机需要在同一网段中才能成功。
    手机的配置如下图,输入服务器端口

  3. 成功后在fiddler页面的左边可以看到所在进行的网络访问记录,找到你需要调试的页面,在autoRespondoradd rules,可以看到再下面出现你所选的页面,在下一个文本框中选择find a file,找到本地你需要进行调试的文件,然后点击save,如下图,最后就可以进行本地文件的修改,可以直接看到线上的效果,但是,这只是调试,最后文件调试好后还要上传你所更改过的文件。

    pc端调试

    pc端的调试不用配置手机调试中的第一步与第二部,只要完成上面的第三步即可。

抓包

fiddler除了可以调试网页外,还可以进行抓包,查看http的头部信息等,这里没怎么用到我就不说了,点到为止。