Atom Shell入门

现在已经有很多框架可以让我们用JavaScript、HTML和CSS来编写跨平台桌面应用,Atom Shell便是其中之一。Atom Shell基于nodejs和Chromium实现,Github的开源编辑器Atom便是基于Atom shell和Coffee scrpit开发的。

Chromium浏览器可以分为两个部分:Browser端和Render端。Browser端负责与本地系统交互:创建窗口、控制托盘图标等等。Render端负责绘制页面。两者通过IPC交互。Atom Shell的实现是在Browser端和Render端分别嵌入了nodejs。这样Browser和Render两部分就都可以使用nodejs提供的api了,也可以在Browser端用javascript来调用本地系统相关的API。

Browser端

在Web app中我们通常会有两种javascript脚本:服务器端脚本和客户端端脚本。客户端端脚本运行于浏览器中,服务器端脚本运行于nodejs中。在Atom Shell中有类似的概念,运行于Render端页面中的脚本和运行于browser端中的脚本。分别可以称它们为:Render端脚本和Browser端脚本。

在传统的Web app中,服务器端脚本和客户端脚本通常使用web sockets来通信。在Atom Shell中,Render端脚本需要使用ipc模块来发送信息给rowser端。同时Atom Shell还提供了一个更易用的remote模块来支持通信。

Render端

普通的网页是无法操作浏览器以外的本地系统的。而在Atom Shell中nodejs api可以在网页中使用,所以开发者可以在网页中访问本地资源,就像Node-Webkit一样。

但是和Node-Webkit不一样的是:不能直接在网页中(Render端)操作本地资源,只能通过remote模块调用Browser端脚本操作本地资源。

DEMO

通常一个Atom Shell app的文件目录如下:

your-app/
├── package.json
├── main.js
└── index.html

package.json的格式和普通的Node模块中的一样。其中属性main的值表示app的入口脚本,这个脚本会运行在Browser端中。如下是package.json文件的样例:

{
    "name"    : "your-app",
    "version" : "0.1.0",
    "main"    : "main.js"
}

main.js中应该创建窗口并处理相应的系统事件,下面是一个典型的样例:

// 用于控制app的life circle
var app = require('app');
// 用于创建本地窗口的模块
var BrowserWindow = require('browser-window');    
// 把app的crash情况发送给服务器
require('crash-reporter').start();

// 保持一个全局的window对象引用,
// 如果你不这么做,window对象会在GC启动后被自动垃圾收集机制释放
var mainWindow = null;

// 在所有窗口关闭后关闭app
app.on('window-all-closed', function() {
  if (process.platform != 'darwin')
    app.quit();
});

// 这个方法会在Atom Shell初始化结束后调用回调函数
app.on('ready', function() {
  // 创建窗口
  mainWindow = new BrowserWindow({width: 800, height: 600});

  // 加载index.html
  mainWindow.loadUrl('file://' + __dirname + '/index.html');

  // 在窗口关闭时触发
  mainWindow.on('closed', function() {
      // 移除window对象的引用
      // 通常如果是多窗口的app,你需要把window对象存储到一个数组里面,而在close时把对应的window对象移除掉
    mainWindow = null;
  });
});

最后创建需要在窗口中显示的index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Hello World!</title>
  </head>
  <body>
    <h1>Hello World!</h1>
    We are using node.js <script>document.write(process.version)</script> and atom-shell <script>document.write(process.versions['atom-shell'])</script>.
  </body>
</html>

运行App

首先下载Atom Shell的可执行文件。然后运行如下命令执行app:

  1. windows:.\atom-shell\atom.exe your-app\
  2. Linux: ./atom-shell/atom your-app/
  3. Mac OS X: ./Atom.app/Contents/MacOS/Atom your-app/

打包

你只需要把app的文件夹命名为app,并把它放在Atom Shell的资源目录下。在OS X系统中为Atom.app/Contents/Resources/,在Linux和Windows上为resources/。然后执行Atom.app(在Linux中为atom,在windows中为atom.exe)即可。然后把atom-shell文件夹压缩打包分发给用户即可。

如果你用的打包工具是grunt,那么可以用grunt-download-atom-shell来自动下载对应平台的Atom Shell。

让你的站点支持触屏

从手机到桌面屏幕,越来越多的设备拥有了触摸屏。当用户使用你的界面时,你的app应该直观且优雅的支持触摸操作。

让页面元素响应触摸状态

从手机到桌面屏幕,越来越多的设备拥有了触摸屏。当用户使用你的界面时,你的app应该直观且优雅的支持触摸操作。

添加触摸状态的样式

你有没有触摸或点击一个页面上的元素,有没有奇怪为什么这个站点的页面元素可以检测这些状态?

当用户触摸你的界面中的元素时,仅仅是简单地修改元素的颜色就可以让用户感知你的站点可以正常工作。不只是这个作用,还可以让用户感受到页面的即时响应。

使用伪类来切换不同触摸状态下的样式

最快地支持触摸的方式就是在页面元素的触摸状态切换时修改其样式。

关键点:

  • 修改:hover:active:focus状态下的UI,让用户感觉你的站点是可以即时响应的。
  • 不要覆盖浏览器的默认touch和focus行为,除非你自己实现了对应的UI变化。
  • 对于用户可能进行触摸操作的页面元素,禁用其文字选择功能。除非用户有需要会拷贝或选择文字。

DOM元素可以处于以下四种状态:默认、focus、hover和active。如果你想要改变这些状态下的UI,我们需要使用这三种伪类::hover:focus:active。例如:

.btn {
  background-color: #4285f4;
}

.btn:hover {
  background-color: #296CDB;
}

.btn:focus {
  background-color: #0F52C1;

  /* The outline parameter surpresses the border
  color / outline when focused */
  outline: 0;
}

.btn:active {
  background-color: #0039A8;
}

查看样例

看下伪类所对应的触摸状态

覆盖浏览器默认的触摸状态样式

不同的浏览器实现了它们自己特有的触摸状态样式。当你想要实现自己的样式时,就需要同时覆盖掉浏览器的默认样式。

记住:

  • 只有当你实现了自己的样式时,才需要覆盖浏览器的默认样式。

覆盖Tap高亮样式

当移动设备刚出现的时候,很多站点都没有激活状态下的样式。结果很多浏览器在用户触摸浏览器的时候添加了高亮颜色或是其他样式。

Safari和Chrome添加了一个高亮颜色作为Tap高亮样式。这可以通过设置CSS样式-webkit-tap-highlight-color来修改默认样式。

/* Webkit / Chrome Specific CSS to remove tap
highlight color */
.btn {
  -webkit-tap-highlight-color: transparent;
}

Windows Phone上的Internet Explorer有一个类似的行为,但是它需要meta标签来重写:

<meta name="msapplication-tap-highlight" content="no" />

重写FirefoxOS按钮的状态样式

Firefox的伪类-moz-focus-inner为每个可触摸元素默认添加了一个outline样式。你可以通过设置border:0来移除outline样式。

如果你使用了<button>元素,FirefoxOS会其默认添加个渐变的背景。你可以通过设置background-image:none来覆盖这样式。

/* Firefox Specific CSS to remove button
differences and focus ring */
.btn {
  background-image: none;
}

.btn::-moz-focus-inner {
  border: 0;
}

重写Focus状态下的ouline样式

使用outline:0可以覆盖focused元素的outline颜色。

.btn:focus {
  outline: 0;

  // Add replacement focus styling here
}
禁用可触摸UI的user-select功能

在一些移动浏览器上,用户长按屏幕就可以选择文字。但当用户不小心按一个按钮时间太长,并不会触发点击事件而会触发按钮文字的选择,这并不是好的用户体验。

-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;

记住:

  • 如果页面元素中的信息对用户有用,例如电话号码、email地址等等的东西,你不应该轻易地禁用user-select

引用

触摸状态的伪类。

  • 伪类::focus
  • 例子:
  • 描述:当你通过tab键切换焦点时,Focus状态可以提示用户哪个元素处于激活状态

  • 伪类::active

  • 例子:
  • 描述:这个状态表示页面元素被选中,例如用户正在点击或触摸一个元素

  • 伪类::hover

  • 例子:
  • 描述:当用户的鼠标处于页面元素之上时,这个元素就会进入这个状态。在hover是改变UI可以鼓励用户与此元素交互

实现自定义手势

如果你想要为自己的站点实现一个自定义的交互和手势,那么有两点要记住:要支持哪些移动浏览器和如何保持高帧率。在这篇文章中我们讲一探究竟。

使用事件来响应触摸操作

根据你想要实现的touch操作,你就需要在如下阵营中选其一:

  • 用户同时只与一个特别的元素交互
  • 用户与多个元素在同一时间交互

两者必选其一。

如果用户只需要和一个元素交互,那么只要手势操作开始,你可能就需要把所有的touch事件放在那个元素上。例如,在其他元素上滑动也可以控制要移动的元素。

然后,如果你期望用户与多个元素在同一时间交互,你应该将touch操作限制到特定的元素上。

TL;DR

  • 要支持所有的设备,那就需要处理touch、mouse和MS Pointer事件
  • 永远将开始整个交互的事件监听器绑定在元素本身上
  • 如果你想让用户与一个指定的元素交互,那么将移动和结束的监听器绑定在document上面;确保在结束的监听器中解绑移动和结束的监听器
  • 如果你想要支持多点触摸,要么在各个元素上绑定与其对应的移动和结束touch事件,要么在一个元素上处理所有的touch事件

添加事件监听器

大多数移动浏览器都实现了Touch事件和鼠标事件。

你需要绑定的事件名是:touchstarttouchmovetouchendtouchcancel

在某些情况下,你可能也需要支持鼠标的交互;那么你可以使用这些事件:mousedownmousemovemouseup

对与Windows Phone的设备,你需要支持一系列Pointer Events。Pointer Events是鼠标和touch事件的合集。目前这只在IE 10+上支持,事件名分别是MSPointerDownMSPointerMoveMSPointerUp

Touch、鼠标和Pointer Events是在你的应用中增加手势操作的重要基础。(查看下Touch、鼠标和Pointer Events

使用addEventListener()方法可以注册这些事件,同时还要传递回调函数和一个布尔值。这个布尔值决定了是否使用捕获模式。为true时表示使用捕获模式时,你可以在其他元素之前捕获或打断事件。

// Check if pointer events are supported.
if (window.navigator.msPointerEnabled) {
  // Add Pointer Event Listener
  swipeFrontElement.addEventListener('MSPointerDown', this.handleGestureStart, true);
} else {
  // Add Touch Listener
  swipeFrontElement.addEventListener('touchstart', this.handleGestureStart, true);

  // Add Mouse Listener
  swipeFrontElement.addEventListener('mousedown', this.handleGestureStart, true);
}

这段代码一开始先检查window.navigator.msPointerEnabled来判断是否支持Pointer Events。如果不支持就在添加touch和鼠标事件的监控。

处理单个元素的交互

你可能已经注意到,在上面的代码片段中只是添加了开始手势的事件。这是故意这么写的。

一旦手势操作在元素上开始,就添加移动和结束的事件监听器。这样浏览器可以通过touch事件监听器来检查touch操作是否发生了。并且处理地更快,因为在平时(手势操作开始前)不需要运行额外的javascript。

实现的步骤如下:

  1. 添加开始事件监听器到指定元素上;
  2. 在开始事件的监听器中,绑定移动和结束事件的监听器到document上。之所以要绑定在document上,是因为我们需要监控所有的事件,不仅仅是那个指定的元素;(译者注:用户的手势操作有时很快,可能会超出指定的元素)
  3. 处理移动事件;
  4. 在结束事件的监听器中,移除移动和结束事件的监听器;

如下是handleGestureStart方法的代码片段,它添加了移动和结束的事件监听器到document上:

// Handle the start of gestures
this.handleGestureStart = function(evt) {
  evt.preventDefault();

  if(evt.touches && evt.touches.length > 1) {
    return;
  }

  // Add the move and end listeners
  if (window.navigator.msPointerEnabled) {
    // Pointer events are supported.
    document.addEventListener('MSPointerMove', this.handleGestureMove, true);
    document.addEventListener('MSPointerUp', this.handleGestureEnd, true);
  } else {
    // Add Touch Listeners
    document.addEventListener('touchmove', this.handleGestureMove, true);
    document.addEventListener('touchend', this.handleGestureEnd, true);
    document.addEventListener('touchcancel', this.handleGestureEnd, true);

    // Add Mouse Listeners
    document.addEventListener('mousemove', this.handleGestureMove, true);
    document.addEventListener('mouseup', this.handleGestureEnd, true);
  }

  initialTouchPos = getGesturePointFromEvent(evt);

  swipeFrontElement.style.transition = 'initial';
}.bind(this);

我们使用的结束事件的回调函数是handleGestureEnd。在手势操作结束后它移除了移动和结束事件的监听器。

// Handle end gestures
this.handleGestureEnd = function(evt) {
  evt.preventDefault();

  if(evt.touches && evt.touches.length > 0) {
    return;
  }

  isAnimating = false;

  // Remove Event Listeners
  if (window.navigator.msPointerEnabled) {
    // Remove Pointer Event Listeners
    document.removeEventListener('MSPointerMove', this.handleGestureMove, true);
    document.removeEventListener('MSPointerUp', this.handleGestureEnd, true);
  } else {
    // Remove Touch Listeners
    document.removeEventListener('touchmove', this.handleGestureMove, true);
    document.removeEventListener('touchend', this.handleGestureEnd, true);
    document.removeEventListener('touchcancel', this.handleGestureEnd, true);

    // Remove Mouse Listeners
    document.removeEventListener('mousemove', this.handleGestureMove, true);
    document.removeEventListener('mouseup', this.handleGestureEnd, true);
  }

  updateSwipeRestPosition();
}.bind(this);

鼠标事件也使用相同的处理方法,因为用户的鼠标很可能会不小心移动到指定元素的外面。如果只是将移动事件绑定在元素上,那么很容易会不触发事件。相反地如果绑定在document上面,移动事件将继续触发不论鼠标在页面的哪个地方。

你可以使用Chrome DevTool中的“Show potential scroll bottlenecks”功能来了解touch事件的实现:

处理多元素的交互

如果你期望用户在同一时间与多个页面元素交互,你可以将对应的移动和结束事件直接绑定到那些元素上。但是这只适用于touch事件。对于鼠标事件,你依旧需要将mousemovemouseup事件绑定到document上面。

如果我们只想要监控特定元素上的touch操作,那么我们可以把touch和pinter事件的移动和结束监听器直接绑定在元素上:

// Check if pointer events are supported.
if (window.navigator.msPointerEnabled) {
  // Add Pointer Event Listener
  elementHold.addEventListener('MSPointerDown', this.handleGestureStart, true);
  elementHold.addEventListener('MSPointerMove', this.handleGestureMove, true);
  elementHold.addEventListener('MSPointerUp', this.handleGestureEnd, true);
} else {
  // Add Touch Listeners
  elementHold.addEventListener('touchstart', this.handleGestureStart, true);
  elementHold.addEventListener('touchmove', this.handleGestureMove, true);
  elementHold.addEventListener('touchend', this.handleGestureEnd, true);
  elementHold.addEventListener('touchcancel', this.handleGestureEnd, true);

  // Add Mouse Listeners
  elementHold.addEventListener('mousedown', this.handleGestureStart, true);
}

handleGestureStarthandleGestureEnd函数中,添加和移除鼠标事件的监听器到document上。

// Handle the start of gestures
this.handleGestureStart = function(evt) {
  evt.preventDefault();

          var point = getGesturePointFromEvent(evt);
  initialYPos = point.y;

  if (!window.navigator.msPointerEnabled) {
    // Add Mouse Listeners
    document.addEventListener('mousemove', this.handleGestureMove, true);
    document.addEventListener('mouseup', this.handleGestureEnd, true);
  }
}.bind(this);

this.handleGestureEnd = function(evt) {
  evt.preventDefault();

  if(evt.targetTouches && evt.targetTouches.length > 0) {
    return;
  }

  if (!window.navigator.msPointerEnabled) {
    // Remove Mouse Listeners
    document.removeEventListener('mousemove', this.handleGestureMove, true);
    document.removeEventListener('mouseup', this.handleGestureEnd, true);
  }

  isAnimating = false;
  lastHolderPos = lastHolderPos + -(initialYPos - lastYPos);
}.bind(this);

Touch操作时保持60fps

现在我们已经处理好开始和结束事件,我们就可以真正地实现touch事件了。

获取和存储Touch事件的坐标

对于任何开始和移动事件,你可以轻松地提取出xy坐标。

如下代码片段中通过targetTouches来判断是否是一个touch事件对象。如果事件对象是鼠标或者pointer事件,那么直接获取事件对象的clientXclientY值。

function getGesturePointFromEvent(evt) {
    var point = {};

    if(evt.targetTouches) {
      // Prefer Touch Events
      point.x = evt.targetTouches[0].clientX;
      point.y = evt.targetTouches[0].clientY;
    } else {
      // Either Mouse event or Pointer Event
      point.x = evt.clientX;
      point.y = evt.clientY;
    }

    return point;
  }

每个touch事件都有三种TouchList属性(见touch列表属性):

  • touches:包含了所有当前屏幕上Touch对象,无论它们起始于哪个元素上
  • targetTouches:包含了所有起始于当前事件所绑定的元素的touch对象
  • changedTouches:包含了所有因为事件触发而状态发生改变的Touch对象(译者注:例如touchmove事件中,触摸点移动touch对象为changedTouches,不动则不是)

大多数情况下targetTouches属性就足够了。

Request Animation Frame

因为事件回调函数在主线程中触发,我们就需要让回调的运行时间尽量短,以保持高帧率,并且避免卡顿。

在事件回调中使用requestAnimationFrame来修改UI。它可以让你可以在浏览器绘制一帧时更新UI,也可以帮你把一些操作放在回调函数外面。

一个典型的实现在开始和移动事件中把xy坐标保存下来。然后在移动事件的回调函数中调用requestAnimationFrame

在我们的DEMO中,我们在handleGestureStart中存储touch的初始化位置:

// Handle the start of gestures
this.handleGestureStart = function(evt) {
  evt.preventDefault();

  if(evt.touches && evt.touches.length > 1) {
    return;
  }

  // Add the move and end listeners
  if (window.navigator.msPointerEnabled) {
    // Pointer events are supported.
    document.addEventListener('MSPointerMove', this.handleGestureMove, true);
    document.addEventListener('MSPointerUp', this.handleGestureEnd, true);
  } else {
    // Add Touch Listeners
    document.addEventListener('touchmove', this.handleGestureMove, true);
    document.addEventListener('touchend', this.handleGestureEnd, true);
    document.addEventListener('touchcancel', this.handleGestureEnd, true);

    // Add Mouse Listeners
    document.addEventListener('mousemove', this.handleGestureMove, true);
    document.addEventListener('mouseup', this.handleGestureEnd, true);
  }

  initialTouchPos = getGesturePointFromEvent(evt);

  swipeFrontElement.style.transition = 'initial';
}.bind(this);

handleGestureMove方法中,如果需要则会在requestAnimationFrame之前存储y位置,然后将onAnimFrame函数传递作为回调函数:

var point = getGesturePointFromEvent(evt);
lastYPos = point.y;

  if(isAnimating) {
    return;
  }

  isAnimating = true;
  window.requestAnimFrame(onAnimFrame);

onAnimFrame函数中,我们修改UI来移动元素。一开始我们先检查手势是否还在进行,来决定是否继续执行动画。如果需要执行动画,你们我们先计算出新的transform值。

一旦我们设置好transform,我们就将isAnimating变量设置为false,这样下一次touch事件中可以执行新的requestAnimationFrame

 function onAnimFrame() {
    if(!isAnimating) {
      return;
    }

    var newYTransform = lastHolderPos + -(initialYPos - lastYPos);

    newYTransform = limitValueToSlider(newYTransform);

    var transformStyle = 'translateY('+newYTransform+'px)';
    elementHold.style.msTransform = transformStyle;
    elementHold.style.MozTransform = transformStyle;
    elementHold.style.webkitTransform = transformStyle;
    elementHold.style.transform = transformStyle;

    isAnimating = false;
}

使用touch-action来控制滚动

CSS属性touch-action允许你在触摸时控制滚动行为。在我们的例子中,使用touch-action: none来禁用触摸滚屏功能。

/* Pass all touches to javascript */
-ms-touch-action: none;
touch-action: none;

如下是touch-action的所有可能值。

  • 属性名:touch-action: auto
    • 描述:滚动正常工作,只要浏览支持触摸操作依旧可以触发水平或是垂直的滚动。
  • 属性名:touch-action: none
    • 描述:触摸操作不能触发滚动
  • 属性名:touch-action: pan-x
    • 描述:触摸操作可以触发水平滚动;但垂直滚动被禁止;
  • 属性名:touch-action: pan-y
    • 描述:触摸操作可以触发垂直滚动;但水平滚动被禁止;

记住:

  1. 使用touch-action: pan-xtouch-action: pan-y更好,因为你的目的明确,用户只能在元素上水平或垂直的滚动。

引用

touch事件的标准定义可以通过w3 Touch Event来获取。

Touch事件、鼠标事件和MS Pointer事件

这些事件是在你的应用中增加手势操作的重要基础。

  • 事件名:touchstartmousedownMSPointerDown
    • 描述:当手指第一次触摸一个元素或者鼠标按下时触发。
  • 事件名:touchmovemousemoveMSPointerMove
    • 描述:当手指在屏幕上移动或者用鼠标拖动时触发。
  • 事件名:touchendmouseupMSPointerUp
    • 描述:当手指离开屏幕或者鼠标松开时触发。
  • 事件名:touchcancel
    • 描述:当浏览器取消手势操作是触发此事件。

Touch list对象

每个touch事件对象都包含三种touch list属性:

  • 属性:touches
    • 描述:包含了所有当前屏幕上Touch对象,无论它们起始于哪个元素上
  • 属性:targetTouches
    • 描述:包含了所有起始于当前事件所绑定的元素的touch对象。例如,如果你的事件绑定在一个<button>上,那么这个属性就只包含起始于这个按钮的touch对象。如果你的事件绑定在document上,那么这个属性就包含这个document上的所有touch对象。
  • 属性:changedTouches
    • 描述:包含了所有因为事件触发而状态发生改变的Touch对象
      1. 对于touchstart事件,这个属性包含了在当前事件中刚刚激活的touch对象
      2. 对于touchmove事件,这个属性包含了自从上一次move事件触发后位置移动的touch对象
      3. 对于touchendtouchcancel,这个属性包含了刚刚离开屏幕的touch对象。

[译]学习CSS布局

一直认为CSS布局是前端的精华所在。优秀的前端工程师不仅仅是精通javascript,更应该对CSS了如指掌。作为一个从后端转向前端的开发,我有过一段时间的状态是:刚学完了CSS的标准,但却不知道从何下手去真正地做一个网页。在这几年接触的前端同学中,发现这是一个比较普遍的现象。因为CSS真的相当灵活,并且它是如此的“不平易近人”。

举个比较简单例子,CSS中有个外边距样式是margin,它有个值是auto。它表示浏览器会自动计算外边距的宽度。如果不经别人提醒你很难想到可以用margin:auto来实现水平居中。当然肯定有很多聪明的同学会自己联想到这个用处,然后口口相传。

主要想说明的是:CSS是一个标准,而使用这标准的方法有很多很多。我想肯定有些用法是连CSS标准制定者都想不到,个人觉得用纯CSS绘制等腰三角形就属其中之一。说的有些远了,最近翻译了一篇入门级别的CSS布局教程,希望可以帮到初学者们。

欢迎指正其中的不妥之处~github

中文排版二三事

前段时间一直在折腾中文排版相关的事情,自认为结果还算不错。故开源之,即是Entry.css。这是一个可配置的、更适合阅读的中文文章样式库,可以用来快速搭建中文博客主题或是用于项目文档的样式。在这篇博文中会介绍下在做这个库过程中学到的一些中文排版知识,以及它的特色。

Read More

CSS Counters

CSS Counters是一个很有意思的特性,它配合 content 属性和伪元素可以实现自动编号的效果。它是CSS2.1提出的标准,主流浏览器对它的支持很好,即使是IE8都支持。利用CSS Counters可以实现“标题自动编号”、“复杂样式的有序列表”等等以前需要后端配合才能实现的样式。例如下面是由RED TEAM DESIGN提供的特殊有序列表样式:

屏幕快照 2013-05-11 上午8.10.21

counter-reset与counter-increment的使用


在CSS2.1中counter-resetcounter-increment两个属性负责控制Counters,然后通过content属性的counter()函数来显示。每个Counters都有一个名字,counter-reset就是用于重置Counters。它的值是一个或以上的Counter名字和对应的可选初始值。例如:

.ol1 {
    counter-reset: ol 2;
}
.ol2 {
    counter-reset: ol1 ol2 3;
}
.ol2 {
    counter-reset: ol1 ol1 3;
}

上例中第一种情况重置Counter ol初始值2,第二种情况重置Counter ol1的初始值为0、ol2的初始值为3,第三种情况重置了ol1的初始值为3。可见对于重复重置,CSS会默认覆盖前者的初始化值。counter-increment则用于控制Counters的增长,它的值是一个或以上的Counter名字和对应的可选增量值。例如:

.li1 {
    counter-increment: ol 2;
}
.li2 {
    counter-increment: ol ol 1;
}

上例中第一种情况设置了Counter ol增长了2,第二种情况Counter ol增长了2,可见对于重复的设置增长值,CSS会作为是增量处理。然后在CSS的content属性中调用counter()函数即可显示出当前计数器值,如下:

li:before {
  content: counter(ol) ".";
  counter-increment: ol;
}

counter()函数默认显示成数字的样式,它还可以设置第二个参数来修改输出。所有list-style-type支持的样式它都支持,例如:’disc’、’circle’、’square’、和’none’等等,例子如下:

li:before {
  content: counter(ol, disc) ".";
  counter-increment: ol;
}

总结一下,有几个注意点要留心:

  1. 如果一个元素即使用了reset/increment又使用了content函数,那么先reset/increment再显示;
  2. 如果一个元素同时使用了reset和increment,那么先reset
  3. 如果一个counter被reset或increment多次,则按照顺序做覆盖或增量处理;

看到目前为止,你已经可以使用Counters来作出自己的效果了。但是这篇博文想要谈的不仅仅如此,很显然如果仅仅凭借目前这些规范还没办法实现一个嵌套的有序列表。要模仿如下例一样的ol列表嵌套,目前的功能还不够。

<ol>
    <li>here’s one line from a numbered list</li>
    <li>here’s another
<ol>
    <li>Here's one inner ol tag</li>
    <li>another line</li>
    <li>Last line.</li>
</ol>
</li>
    <li>第三列</li>
</ol>

嵌套Counters与作用域


为了模拟上面的效果,CSS增加了嵌套Counters与作用域的支持。如果一个元素有counter-reset的样式,则它会生成一个Counters实例,这个实例的作用域包含了它的子孙元素、它的伪元素和它的兄弟元素。举个例子:

作用域例子

从上面的例子可以发现,“父元素”和“其他元素”都处于作用域之外,故它们都是以1(默认值0,且增加1)开始。而兄弟元素、伪元素和子元素都按照顺序计数(after伪元素在子元素之后,before伪元素在子元素之前)。另外如果之前没有Counter实例(即没有counter-reset样式),那么counter-increment也会创建一个实例。

除了作用域之外,还有个很重要的概念“嵌套”。所谓嵌套Counters是指“自我嵌套”。如果在一个Counter实例的作用域内再次重置相同名字的Counter实例,那么新的同名Counter会嵌套在其父Counter下。看下面的例子会比较清晰:

<style>
  ol {
    counter-reset: ol;
  }
  li {
    position: relative;
    display: block;
    padding-left: 34px;
  }
  li:before {
    content: counter(ol) ".";
    counter-increment: ol;
    position: absolute;
    top: 0;
    left: 0;
  }
</style>
<ol>                                             <!-- {item[0]=0 重置,作用域开始 -->
    <li>here’s one line from a numbered list</li><!--  item[0]++ (=1)    -->
    <li>here’s another                           <!--  item[0]++ (=2)    -->
        <ol>                                     <!--    {item[1]=0 嵌套作用域开始 -->
            <li>Here's one inner ol tag</li>     <!--    item[1]++ (=1)  -->
            <li>another line</li>                <!--    item[1]++ (=2)  -->
        </ol>                                    <!--    } 嵌套作用域结束  -->
    </li>
    <li>Last line.</li>                          <!--  item[0]++ (=3)    -->
</ol>                                            <!-- } 作用域结束         -->

嵌套Counters配合作用域,就可以模拟默认的嵌套ol列表效果了。这里面比较容易踩坑的点是伪元素的顺序问题。如果把上例里面的before换成after,则得到的效果会不一样。更奇特的事情是即使换成了after,在IE8下效果仍旧是正确的。个人猜测是因为IE8认为after也是和before伪元素一样,先于子元素来处理。目前还没在IE9下测试过,其他主流PC浏览器都符合标准。这里有个demo展示了这种情况。

2013-06-14补充:不仅仅counter-reset可以生成嵌套,未reset之前直接increment也会生成嵌套。这很容易导致出现意料之外地结果,所以确保increment之前一定要reset。

浏览器的overflow事件

Webkit和Firefox其实是原生支持探测元素overflow状态改变的事件。参看这个DEMO:

See the Pen Way to detect overflow event support and use it with graceful degradation by mzhou (@zmmbreeze) on CodePen.

Webkit使用的是overflowchanged事件,而Firefox则使用overflow和underflow这两个事件。虽然Webkit只提供了一个事件,但是我们可以通过event对象的属性来知道是overflow还是underflow,甚至知道是垂直方向还是水平方向。而Firefox的两个事件则没法提供溢出方向的信息。

重要地是IE和旧版的Opera(非webkit内核)是不支持的,也没有什么特别好的fallback方法。所以在使用overflow事件时一定要做好特征检测。在Webkit下,只需判断window有没有OverflowEvent即可。而在Firefox下比较麻烦了。

查看了Modernizr的源码,发现了使用setAtrribute来检测Firefox是否支持某个事件的方法。可惜尝试之下发现不能正确检测overflow事件。于是改用创建div并激活overflow事件的方法来判断是否支持。代码如下:

var element = document.createElement('div');
if (element.addEventListener) {
  element.style.cssText = 'overflow:scroll;height:1px;width:1px;';
  document.body.appendChild(element);

  var overflowSupport = false;
  // firefox(tested on 1.5+) support overflow/underflow event
  element.addEventListener('overflow', function () {
      overflowSupport = true;
  }, false);
  element.innerHTML = '&lt;div style=&quot;height:200px;width:1px;&quot;&gt;&lt;/div&gt;';

  var timeout;
  var end = function() {
      if (end.done) {
          return;
      }
      end.done = true;

      if (overflowSupport) {
          callback(function (element, type, cb) {
              element.addEventListener(type + 'flow', cb, false);
          });
      } else {
          callback();
      }

      clearTimeout(timeout);
      document.body.removeChild(element);
  };
  // Use scroll event to make sure it's right after overflow event.
  element.addEventListener('scroll', end, false);
  element.scrollTop = 1000;
  // Make sure callback was called, even browser not support scroll event.
  // For example 'opera 11.*'
  timeout = setTimeout(end, 250);
}

因为是异步的,所以一定要确保判断结果的动作是在执行过overflow事件回调之后。一个比较简单的方法是使用setTimeout延迟执行。为了保证执行顺序,时间就一定要设置长一点(250ms)。但是这样响应太慢了。幸运地是在Firefox中scroll事件是在overflow事件之后触发的,所以改为在scroll事件的回调函数中做判断。

如果你有更好的检测方法,请一定告知~ Github

纯CSS实现网页版PPT

Slider.css是我最近在写的一个项目,目标是用HTML和CSS实现网页版幻灯片。目前已经实现的差不多了,已经支持了:

  • 前进、后退
  • 多种切换动画
  • 进度调(目前只是Chrome支持,需要开启#enable-experimental-web-platform-features)
  • 最大最小化
  • 页码
  • 基本内容布局
    其中用到了一些比较有意思的CSS技术:target selector、counter、flow-into、3D变换等等。还是直接看代码DEMO比较实在,欢迎提意见和加星~

使用window.name来实现跨域

前端跨域请求算是这个领域老生常谈的必修课之一了。前辈们已经摸索出了很多有用的方法:

  1. JSONP
  2. 同根域名下的修改document.domain
  3. iframe + 修改location.hash
  4. iframe + proxy页面
  5. iframe + window.name方式
  6. HTML5 postMessage方式
  7. flash
  8. 发伪img请求,传输数据(很多日志类请求用此方法)

  9. 每个方法都有各自的优缺点。这里要谈的是“iframe + window.name”的方式。它利用了window.name属性的特性:“一旦被设置,即使在页面重新定位之后也不会改变”。如下是大概流程:

windowName

关键点在于数据页面会把数据转成字符串设置到window.name上,然后再把iframe定位到proxy页面。proxy页面和主页面是同一域名下,故主页面可以访问proxy页面的window.name。

这个方法有如下的优点:

  1. 支持POST和GET
  2. 所有主要浏览器都支持
  3. 支持传输的数据量大(2M)
  4. 无需任何插件
  5. 机制不算复杂(比起flash和location.hash之类来说要简单些)
    当然它也有些缺点:

  6. 需要同源的proxy页面

  7. 需要请求一次proxy页面
  8. 需要创建和销毁iframe,相比JSONP和img请求之类的方法,开销要大些
    我写了一个DEMO。一开始我以为这个方法没有什么兼容性问题,但是实际使用的时候还是发现了一个。

    IE10下,一旦设置iframe.name之后,再在iframe内部设置window.name时只能在iframe内部访问。在iframe外部调用iframe.contentWindow.name获取到的值是iframe.name而非window.name
    用GET提交数据时,这不会有问题。但如果需要使用POST,那就需要构造form来提交数据。这时一般会用到“form.targetiframe.name相同,来返回页面到指定的iframe”这一技术。于是一旦设置iframe.name之后,利用window.name来跨域的方法就会失效了。试了几种方法,都没找到合适的hack。最终只能把form构造在iframe内部,虽然麻烦些,但是可以做到主流浏览器都兼容。

网上还有window.name + postMessage互补的方式来解决这个问题,优点是在高级浏览器上可以减少一次请求(proxy文件),缺点是前后端代码都稍微变得复杂些。目前已经有了很多成熟的方案了,可以自行Google。

setMonth方法勿用导致地有趣bug

这不是什么新鲜的知识了,网上已有很多类似的文章说过这个点。这里纯粹记录下自己踩过地坑。言归正传,Javascript的Date实例有个方法setMonth,用于设置Date实例的月份和日期。如下:

new Date().setMonth(8, 1);

第一个参数为月份,整数区间为[0, 11]。第二个参数为日期,整数区间为[1, 31]。方法很简单,但是这个方法有个容易忽略的点。先看如下代码

var strToDateStr = function (str) {
var r = str.match(/(\d{4})-(\d{2})-(\d{2})/);
var d = new Date();
d.setFullYear(r[1]);
d.setMonth(r[2] - 1);
d.setDate(r[3]);
return d;
}

这个方法,用于将字符串转换成Date实例。不过它有一个bug,当你在2013年10月31号那天调用这个方法且输入为2013-09-02时,得到的输出是2013年10月02号。让我们来一行行分析下:

     var d = new Date();
// d 为10月31号
d.setFullYear(‘2013’);
// d 依旧为10月31号
d.setMonth(8);
// 9月没有31号,故d为10月1号
d.setDate(‘02’);
// d为10月2号

这本不是setMonth的bug,反而是它的feature。只是因为对方法内部原理不了解导致了奇怪的bug。其实这里应该这么写:

var strToDate = function (str) {
var r = str.match(/(\d{4})-(\d{2})-(\d{2})/);
return new Date(r[1], r[2] - 1, r[3], 0, 0, 0, 0);
};

直接在初始化实例的时候确定月份和日期,速度应该会更快些。