【译】在本地存储中保存图片和文件

原文地址:http://hacks.mozilla.org/2012/02/saving-images-and-files-in-localstorage/

你可能已经对本地存储有所了解,本地存储在浏览器中快速存储数据的时候特别强大,并且已经在浏览器中存在多时。但是如何才能在本地存储中保存文件呢?

首先,推荐你先阅读Storing images and files in IndexedDB

使用JSON实现强大的本地存储控制

首先,我们先了解一些基本的本地存储相关的知识。你可以使用键值对的方式往本地存储中存储数据,就像这样:

localStorage.setItem("name", "Robert");

而从本地存储中读取数据的方式如下:

localStorage.getItem("name");

这样的存取方式非常不错,而且最多可以存储5M的数据,给你更多选择的空间。但是由于本地存储是基于字符串的存储,存储一串没有结构的字符串并不是一个理想的选择。因此,我们可以利用浏览器中原生的JSON支持来将JavaScript对象转化成字符串,从而保存到本地数据中,在读取的时候也可以将其转换回JavaScript对象。

图片的存储

我们的想法是做到将已经当前页面中已缓存的图片保存到本地存储中。不过就像我们之前已经确定的,本地存储只支持字符串的存取,那么我们要做的就是将图片转换成Data URI。其中一种实现方式就是用canvas元素来加载图片。然后你可以以Data URI的形式从canvas中读取出当前展示的内容。

让我们看一个例子。

//当图片加载完成的时候触发回调函数
elephant.addEventListener("load", function () {
 var imgCanvas = document.createElement("canvas"),
 imgContext = imgCanvas.getContext("2d");

 // 确保canvas元素的大小和图片尺寸一致
 imgCanvas.width = elephant.width;
 imgCanvas.height = elephant.height;

 // 渲染图片到canvas中
 imgContext.drawImage(elephant, 0, 0, elephant.width, elephant.height);

 // 用data url的形式取出
 var imgAsDataURL = imgCanvas.toDataURL("image/png");

 // 保存到本地存储中
 try {
 localStorage.setItem("elephant", imgAsDataURL);
 }
 catch (e) {
 console.log("Storage failed: " + e);
 }
}, false);

如果我们想要考虑地更长远一些,那么还可以利用JavaScript对象并做一些数据检查。在这个例子中,第一次我们从服务端读取图片,之后每一次页面加载时,我们就可以直接从本地存储中读取已读取过的图片。

HTML部分

<figure>
 <img id="elephant" src="about:blank" alt="A close up of an elephant">
 <noscript>
 <img src="elephant.png" alt="A close up of an elephant">
 </noscript>
 <figcaption>A mighty big elephant, and mighty close too!</figcaption>
</figure>

JavaScript部分

//在本地存储中保存图片
var storageFiles = JSON.parse(localStorage.getItem("storageFiles")) || {},
 elephant = document.getElementById("elephant"),
 storageFilesDate = storageFiles.date,
 date = new Date(),
 todaysDate = (date.getMonth() + 1).toString() + date.getDate().toString();
// 检查数据,如果不存在或者数据过期,则创建一个本地存储
if (typeof storageFilesDate === "undefined" || storageFilesDate < todaysDate) {
 // 图片加载完成后执行
 elephant.addEventListener("load", function () {
 var imgCanvas = document.createElement("canvas"),
 imgContext = imgCanvas.getContext("2d");
// 确保canvas尺寸和图片一致
 imgCanvas.width = elephant.width;
 imgCanvas.height = elephant.height;
// 在canvas中绘制图片
 imgContext.drawImage(elephant, 0, 0, elephant.width, elephant.height);
// 将图片保存为Data URI
 storageFiles.elephant = imgCanvas.toDataURL("image/png");

 storageFiles.date = todaysDate;
// 将JSON保存到本地存储中
 try {
 localStorage.setItem("storageFiles", JSON.stringify(storageFiles));
 }
 catch (e) {
 console.log("Storage failed: " + e);
 }
 }, false);
// 设置图片
 elephant.setAttribute("src", "elephant.png");
}
else {
 // Use image from localStorage
 elephant.setAttribute("src", storageFiles.elephant);
}

注意:此处需要注意本地存储的容量,最好使用try…catch来控制异常。

保存任意格式的文件

使用canvas将图片转换成Data URI并保存到本地存储中的方式非常好,但是如果我们希望能找到一个可以保存任意格式文件的方式。

那么,这个过程就显的比较有趣了,我们需要用到:

  • XMLHttpRequest Level 2
  • BlobBuilder(提供接口来构建Blob对象,Blob对象是BLOB (binary large object),二进制大对象,是一个可以存储二进制文件的容器。在计算机中,BLOB常常是数据库中用来存储二进制文件的字段类型。BLOB是一个大文件,典型的BLOB是一张图片或一个声音文件,由于它们的尺寸,必须使用特殊的方式来处理(例如:上传、下载或者存放到一个数据库)。)
  • FileReader

基本方法是:

  1. 用XMLHttpRequest请求文件,然后将响应头设置为”arraybuffer”。
  2. 将返回数据存放到BlobBuilder中
  3. 获取blob,也就是文件内容
  4. 使用FileReader对象读取文件并加载到文件中,最后保存到本地存储。
// 获取文件
var rhinoStorage = localStorage.getItem("rhino"),
 rhino = document.getElementById("rhino");
if (rhinoStorage) {
 //如果已经存在则直接重用已保存的数据
 rhino.setAttribute("src", rhinoStorage);
}
else {
 // 创建XHR, BlobBuilder 和FileReader 对象
 var xhr = new XMLHttpRequest(),
 blobBuilder = new (window.BlobBuilder || window.MozBlobBuilder || window.WebKitBlobBuilder || window.OBlobBuilder || window.msBlobBuilder),
 blob,
 fileReader = new FileReader();
 xhr.open("GET", "rhino.png", true);
 //将响应头类型设置为“arraybuffer”,也可以使用"blob",这样就不需要使用BlobBuilder来构建数据,但是"blob"的支持程度有限。
 xhr.responseType = "arraybuffer";
 xhr.addEventListener("load", function () {
 if (xhr.status === 200) {
 // 将响应数据放入blobBuilder中
 blobBuilder.append(xhr.response);
 // 用文件类型创建blob对象
 blob = blobBuilder.getBlob("image/png");
 // 由于Chrome不支持用addEventListener监听FileReader对象的事件,所以需要用onload
 fileReader.onload = function (evt) {
 // 用Data URI的格式读取文件内容
 var result = evt.target.result;
 // 将图片的src指向Data URI
 rhino.setAttribute("src", result);
 //保存到本地存储中
 try {
 localStorage.setItem("rhino", result);
 }
 catch (e) {
 console.log("Storage failed: " + e);
 }
 };
 // 以Data URI的形式加载blob
 fileReader.readAsDataURL(blob);
 }
 }, false);
 // 发送异步请求
 xhr.send();
}

使用“blob”作为响应头类型

在上面的例子中,我们使用的是“arraybuffer”作为响应头类型,然后使用BlobBuilder来创建可以由FileReader读取的数据。然而,”blob”作为响应头类型后,会直接返回一个blob对象,从而可以直接由FileReader读取。上面的例子可以改成这样:

// Getting a file through XMLHttpRequest as an arraybuffer and creating a Blob
var rhinoStorage = localStorage.getItem("rhino"),
 rhino = document.getElementById("rhino");
if (rhinoStorage) {
 // Reuse existing Data URL from localStorage
 rhino.setAttribute("src", rhinoStorage);
}
else {
 // Create XHR, BlobBuilder and FileReader objects
 var xhr = new XMLHttpRequest(),
 fileReader = new FileReader();
 xhr.open("GET", "rhino.png", true);
 // Set the responseType to arraybuffer. "blob" is an option too, rendering BlobBuilder unnecessary, but the support for "blob" is not widespread enough yet
 xhr.responseType = "blob";
 xhr.addEventListener("load", function () {
 if (xhr.status === 200) {
 // onload needed since Google Chrome doesn't support addEventListener for FileReader
 fileReader.onload = function (evt) {
 // Read out file contents as a Data URL
 var result = evt.target.result;
 // Set image src to Data URL
 rhino.setAttribute("src", result);
 // Store Data URL in localStorage
 try {
 localStorage.setItem("rhino", result);
 }
 catch (e) {
 console.log("Storage failed: " + e);
 }
 };
 // Load blob as Data URL
 fileReader.readAsDataURL(xhr.response);
 }
 }, false);
 // Send XHR
 xhr.send();
}

浏览器支持情况

  • 本地存储——大部分主流浏览器都支持(在国外=。=), including IE8.
  • 原生的JSON支持——支持情况和本地存储类似
  • canvas元素——大部分主流浏览器都支持,从IE9开始
  • XMLHttpRequest Level 2—— Firefox,Google Chrome, Safari 5+ 并计划在IE10和Opera 12中实现
  • BlobBuilder——Firefox ,Google Chrome,并计划在IE10中实现. Safari和Opera情况不明.
  • FileReader——Firefox ,Google Chrome,Opera 11.1之后的版本, 计划在IE10中实现. Safari情况不明.
  • responseType “blob”——只在Firefox中支持. Google Chrome即将支持,IE10计划支持.  Safari和Opera情况不明。

【译】CSS media queries在JavaScript中的应用(二)

原文链接:http://www.nczonline.net/blog/2012/01/19/css-media-queries-in-javascript-part-2/

在我的前一篇文章中,我介绍了CSS media query在JavaScript中的应用,包括一个自制的实现函数和CSSOM Views里的matchMedia()方法。在CSS和JavaScript中,Media query都是那么实用,以至于我继续对其进行了一些研究,探索更好的利用方式。最后我发现,matchMedia()方法还有一些我在写第一篇文章时没有意识到的有趣特性。

matchMedia()和它的有趣特性

再次调用matchMedia()会返回一个MediaQueryList对象,这个对象可以让你确定给出的media类型是否和浏览器当前状态匹配。MediaQuery对象的matches属性会使用一个布尔型数据来表明匹配的结果。很明显,在每一次调用marches属性的时候,它都会获取一次浏览器的状态:

var mql = window.matchMedia("screen and (max-width:600px)");
console.log(mql.matches);
//resize the browser
console.log(mql.matches); //requeries

这显然非常实用,因为这样你就可以保持对MediaQueryList对象的引用,从而可以重复检查query与页面之间的状态。

Chrome和safari还有一个怪异的行为。即使matches属性的初始值是正确的,默认情况下还是不会更新matches的值,除非页面含有一个对应相同的query和至少一个规则定义的media块。比如说,为了让一个表现形式为”“screen and (max-width:600px)““的MediaQueryList对象正确显示出来(包括能正确触发事件),你必须在你的CSS中包含一些内容:

@media screen and (max-width:600px) {
 .foo { }
}

media块中必须含有至少一个CSS规则,但是该规则是可以为空的。只要这个规则存在于页面上,那么MediaQueryList对象就可以正确更新,并且所有通过addListener()绑定的事件都可以正确触发。

你可以使用JavaScript来修复这个问题:

var style = document.createElement("style");
style.appendChild(document.createTextNode("@media screen and (max-width:600px) { .foo {} }"));
document.head.appendChild(style); //WebKit支持document.head

当然你或许会需要为每一个使用matchMedia()访问的media query应用这个修复,这可能会显得有些繁杂。

Firefox中的实现也有一些怪异的特性。理论上,你可以注册一个处理器监听query状态的改变,这样你就不需要一直保持对MediaQUeryList对象的引用了,如下:

//在Firefox中无效
window.matchMedia("screen and (max-width:600px)").addListener(function(mql) {
 console.log("Changed!");
});

在Firefox中,即使media query是有效的,监听器或许仍然不会被调用。在我的测试中,它可能会被触发0到3次,然后再也不会被触发。Firefox团队已经了解了这个bug,并且应该很快会修复这个问题。所以,在使用这个方案的同时,你还是需要继续引用MediaQueryList对象来确保监听器被触发了:

//fix for Firefox
var mql = window.matchMedia("screen and (max-width:600px)");
mql.addListener(function(mql) {
 console.log("Changed!");
});

因为mql的存在,监听器就一直可以被触发。

监听器相关的更多内

在我的上一篇文章中,由于对一部分内容的误解,我对media query的描述有一些错误。监听器会在两个情景下被触发:

  1. media query一开始是有效的。就像前一个例子中,屏幕宽度变成600px或者更低。
  2. media query一开始是无效的。例如屏幕宽度大于600px。

这就是为什么需要将MediaQueryList对象传入到监听器中,这样你可以通过检查matches属性来确定media query是否有效。例如:

mql.addListener(function(mql) {
 if (mql.matches) {
 console.log("Matches now!");
 } else {
 console.log("Doesn't match now!");
 }
});

通过这样的代码,你就可以监控一个web应用的状态,从而可以做一些适当的调整。

是否需要对matchMedia()进行模拟(不支持的浏览器中)

在我在了解matchMedia()的时候,我试图创建一个函数模拟matchMedia()(原文称为polyfill,作为一种不支持matchMedia()方法情况下的替代措施)。Paul Irish使用类似我最近一篇文章里描述的技术实现了一个polyfill。Paul Hayes之后开设了一个分支来创建polyfill,使用了一个基于简单CSS transition的监听器来检测改变。然而,由于它依赖于CSS transitions,监听器的兼容性受到CSS transition兼容性的限制。加上调用matches不能再次获取浏览器状态与Firefox和webkit中的bug,让我坚信创建一个polyfill并不是一个正确的方向。毕竟,当真实环境中存在那么多明显的bug时,polyfill又怎么能正确执行。

我选择的方式是创建一个容器来包裹API的行为,从容器处消除这些问题。当然,我选择将这个API作为YUI Gallery的一个模块实现,叫做gallery-media。这个API很简单,并由两个方法组成。第一个是Y.Media.matches(),调用media query字符串并返回匹配结果。不需要记录任何对象,只需要直接这样获取:

var matches = Y.Media.matches("screen and (max-width:600px)");

第二个方法是Y.Media.on(),它允许你来指定一个media query和一个监听器,当media query生效或者失效时执行。监听器的参数是一个含有matches和media属性的对象,提供media query的信息。例如:

var handle = Y.Media.on("screen and (max-width:600px)", function(mq) {
 console.log(mq.media + ":" + mq.matches);
});
//detach later
handle.detach();

取代使用CSS transitions来监控改变,我使用了一个简单的onresize事件处理器。在桌面系统中,浏览器的尺寸是最有可能被改变的(在移动设备中,方向也有可能被该改变),那么我为旧的浏览器做了这么一个假设。API使用了原生的matchMedia()函数,在WebKit和Chrome中都有效并进行了一些修复,从而可以得到稳定的行为。

总结

JavaScript中的CSS media query比我想象的复杂一些,但是非常实用。我不觉得通过polyfill的方式修复marchMedia()是一个好主意,它让你不能在多个浏览器中使用相同的方式编码。好处是,它将你从bug和改变中隔离出来,从而看起来会更加先进一些。现在让我们使用JavaScript和CSS media query创造更多的可能。

参考

  1. CSS media queries in JavaScript, Part 1
  2. Rob Flaherty’s tweet
  3. matchMedia() MediaQueryList not updating
  4. matchMedia() listeners lost
  5. matchMedia polyfill
  6. matchMedia polyfill
  7. YUI 3 Gallery Media module

前一篇:【译】CSS media queries在JavaScript中的应用(一)

【译】CSS media queries在JavaScript中的应用(一)

原文地址:http://www.nczonline.net/blog/2012/01/03/css-media-queries-in-javascript-part-1/

在2011年初,我参与了一个关于JavaScript特性检测的项目。一些问题的出现让我觉得使用CSS media query或许会更好,所以我花了一些时间编写一个使用JavaScript操作CSS media queries的函数。我的思路很简单:如果我只需要基于media query就可以控制特定的CSS,那么,我也可以基于media query执行特定的JavaScript。

var isMedia = (function(){
 var div;
 return function(query){
 //如果<div>不存在, 则创建一个并让其隐藏
 if (!div){
 div = document.createElement("div");
 div.id = "ncz1";
 div.style.cssText = "position:absolute;top:-1000px";
 document.body.insertBefore(div, document.body.firstChild);
 }
 div.innerHTML = "_<style media=\"" + query + "\"> #ncz1 { width: 1px; }</style>";
 div.removeChild(div.firstChild);
 return div.offsetWidth == 1;
 };
})();

这个函数的思路很简单。我创建了一个<style>节点,在里面设置了media属性来对应我正在测试的样式。这个样式会应用在<div>标签上,我需要做的就是检查这个样式是否已经应用到对应的节点。我觉得应该避免一些浏览器探测,所以我没有使用currentStyle或者getComputedStyle(),而是选择改变一个元素的宽度,然后用offsetWidth来检查这个改变。

很快,我们就创建了一个函数,并且它可以在几乎所有的浏览器中运行。和你猜的一样,在IE6和IE7上会有一些问题。在这两个个浏览器上,<style>元素是一个NoScope元素(比如文本节点,注释节点或者script)。NoScope意味着当页面受到innerHTML或者其他类似的操作注入时,会发生一些可怕的事情。如果NoScope元素是作为HTML字符串的最前面部分被添加的,那么所有这些元素都会被丢弃。为了能正常使用NoScope元素,你必须确定这个元素不是HTML字符串的第一部分。因此,我在<style>元素签名增加了一条下划线,然后再将其删除,这样就能保证在IE6和IE7下保持正常。其他浏览地没有这个NoScope问题,但是使用这个技术并不是一个完美的方案(我之前说过我尽量要避免浏览器探测)。

最后,你可以这样使用这个函数:

if (isMedia("screen and (max-width:800px)"){
 //在screen模式下这样使用
}
if (isMedia("all and (orientation:portrait)")){
 //portrait模式下
}

isMedia()函数在所有浏览器里都工作地很好,它可以很好地检测media query在浏览器中的有效性。也就是说传入一个无效的media query会返回一个false。比如在IE6下,传入”screen”会返回true,但是更复杂的参数传入的话会返回false。这个结果是可以接收的,因为其他没有匹配到的media query中的任意CSS样式都不应该在对应的浏览器中应用。

CSSOM View

CSS Object Model(CSSOM)Views规范增加了对JavaScript操作CSS media query的原生支持,它在window对象下增加了matchMedia()方法。你可以传入一个CSS media query然后返回一个MediaQueryList对象。这个对象包括两个属性:matches,布尔值数据,表示CSS media query是否与当前的显示状态匹配;media对应传入的参数字符串。例如:

var match = window.matchMedia("screen and (max-width:800px)");
console.log(match.media); //"screen and (max-width:800px)"
console.log(match.matches); //true or false

到这里,API没有提供的支持和我之前的想法差不多。你或许会考虑为什么matchMedia()会返回一个对象?毕竟,如果没有匹配成功,返回的对象也毫无用处。答案就在addListener()和removeListener()上。

这两个方法允许你基于CSS media query与视图状态进行交互。例如,当模式转换到“portrait”模式时,或许你会想要有一个提示。你可以这么做:

var match = window.matchMedia("(orientation:portrait)");
match.addListener(function(match){
 if (match.media == "(orientation:portrait)") {
 //do something
 }
});

这段代码对media query进行了监听。当query值和当前的视图状态对应时,监听器对应的函数就会执行,而对应的MediaQueryList对象也会传入。用这个方式吗,你可以让你的JavaScript可以很快地响应布局变化,并且不需要用轮询的方式。那么,这里就和我的想法不太一样了,这个API允许你监听视图状态的改变,并响应调整界面的行为。

matchMeida()方法在Chrome,Safari5.1+,FireFox 9+和IOS 5+上的Safari上都已生效。这些是我了解并已核实的浏览器。IE和Opera依然没有在最近的几个版本中实现这个方法。

注意:WebKit上的实现有一个BUG,当MediaQueryList对象创建后,matches不会更新,query监听器也不会触发。希望这个问题能早点修复。

总结

CSS media query带来了一个在CSS和JavaScript中都适用的特性检测语法。我期望media query可以在未来成为JavaScript代码中的重要部分,在界面变化发生时,开发者能检测得到。实在没有理由说web应用的行为不应该响应布局的变化,而CSS media让我们变得更加强大。

参考

A function for detecting if the browser is in a given media mode
MSDN: innerHTML Property
CSS Object Model View
matchMedia() MediaQueryList is not updating

解码jQuery系列4 – 函数作用域、jQuery连缀模式和jQuery.fn


石川著, 李松峰编译

“OOP与jQuery”是“解码jQuery”系列中的一个子系列,主要讨论 jQuery 的内部构成及相关的OOP(Object Oriented Programming,面向对象编程)概念。

在这篇文章中,我们会讨论函数作用域、jQuery连缀模式和jQuery.fn。

1.函数作用域

我们都看到了,jQuery对象被包装在了很多层函数中。为什么我们用函数来定义作用域呢?因为JavaScript不能用花括号定义作用域。JavaScript变量只有全局作用域和函数作用域。

举例来说吧,为了把下面这个jQuery变量变成私有变量,必须把它包装在一个函数中:

function a() {
  var jQuery = "hellow world";
}
console.log(jQuery); // undefined

JavaScript函数具有词法作用域。什么意思呢?就是说,函数的作用域是在定义的时候创建的,而不是在执行的时候创建的。

var jQuery = 'hi there';
var f = function() {
  console.log(jQuery); // "undefined"
  var jQuery = "hello world";
};
f();
// undefined

所以,如果运行上面的代码,返回的值会是`undefined`,而不是`’hi there’`。 就是说,在同一个变量作用域或者同一个函数内,只要有使用var声明变量jQuery的语句,就可以在函数中的任何位置访问它,包括在var语句之前。所以上面的等同于:

var jQuery = 'hi there';
var f = function() {
  var jQuery; // same as -> var jQuery = undefined;
  console.log(jQuery); // "undefined"
  jQuery = "hello world";
};
f();
// undefined

就是说,函数f有自己的作用域,它没有读var jQuery = ‘hi there’。但在函数f内部,确实定义了jQuery变量,只不过调用console.log()时,jQuery还没有定义。

2.jQuery连缀模式

看看init方法,不知道你是否注意到了,这个方法返回this。

var jQuery = function( selector, context ) {
  // jQuery对象实际上就是一个“增强版的”init构造函数
  return new jQuery.fn.init( selector, context, rootjQuery );
}
jQuery.fn = jQuery.prototype = {
  init: function( selector, context, rootjQuery ) {
    //...
    return this;
    //...
  }
}

在jQuery中,可以在一个方法调用后面再连缀另一个方法调用。之所以能够如此,是因为每个方法都像这里一样返回this对象。

还是不清楚?我们来解释一下。首先,来看看对象中的this关键字有什么用。

var city = {
  name: 'beijing',
  getName: function() {
    return this.name;
  }
}
console.log(city.getName());

以上代码输出的城市名是beijing,说明this引用的是city对象。

再进一步,如果我们返回的是整个对象呢?

var num = {
  value: 1,
  minus: function (n) {
   this.value -= n;
   return this;
  },
  plus: function (n) {
    this.value += n;
    return this;
  },
  getVal: function () {
    console.log(this.value);
  }
};

num.minus(2).plus(5).getVal();

这样,就可以把方法调用连缀起来了。

3.jQuery.fn

在core.js中,我们可以看到这行代码:

jQuery.fn = jQuery.prototype = {
  //...
}

在我们开发jQuery插件时,经常要用到$.fn,也就是jQuery.fn。现在知道了吧,jQuery插件其实就是添加到jQuery.prototype的方法。正因为如此,插件里也不要忘了返回this噢。

(function( $ ){
  $.fn.fontcolor = function() {
    return this.each(function() {
      $(this).css("color", "orange");
    });
  };
})( jQuery );

系列其他文章