以下是 Les James 的客座文章。与我们许多人一样,Les 一直在寻找适合他的响应式图像解决方案。在这篇文章中,他分享了一种他发现的技术,他可以在其中将“命名”媒体查询从 CSS 传递到 JavaScript,JavaScript 使用这些名称来替换适合该媒体查询的相应图像。甚至可以使用 Sass 自动执行此过程。HTML 纯粹主义者可能会对图像上缺少真正的 src
感到反感,但 到目前为止,我们必须做我们必须做的事情。
当前 提出的响应式图像解决方案 要求您将媒体查询值内联到 HTML 标签中。
<picture>
<source src="large.jpg" media="(min-width: 45em)" />
<source src="med.jpg" media="(min-width: 18em)" />
<source src="small.jpg" />
<img src="small.jpg" alt="" />
</picture>
这对我有问题。我喜欢将媒体查询放在一个地方,即 CSS。上面的例子对我来说感觉不可维护,这可能与我使用 RWD 的方法有关。我使用 Frameless grid 方法来创建布局。我告诉 Sass 我想要布局中的列数,它会生成一个媒体查询来适应它。这意味着我只考虑列数,实际上我不知道媒体查询的实际值。
在将此方法应用于响应式图像时,我需要一种方法来提供有关断点周围的元数据。因此,当我创建一个断点时,我给它一个标签。我最近喜欢 small、medium 和 large 这样的标签,但标签可以是 任何你想要的。关键是使用有意义的东西来命名断点。目标是将这些名称与我 HTML 中定义的匹配源配对。第一步是将我们的标签格式化为 JavaScript 可以解析的内容。
Sass 到 JSON
当我创建一个断点时,我会调用我创建的 Sass 混合器。列数创建一个 min-width
媒体查询来适应该列数。标签为我们的媒体查询命名。
@include breakpoint(8, $label: "medium") {
/* medium size layout styles go here */
}
断点混合器将该标签传递给一个函数,该函数将其格式化为 JSON 字符串。
@function breakpoint-label($label) {
@return '{ "current" : "#{$label}" }';
}
JSON 到 CSS
现在我们已经将标签转换为 JSON,如何将其添加到 CSS 中呢?字符串的自然匹配是 CSS 生成的内容。我使用 body::before
来保存我的字符串,因为它是我最不可能在前端实际使用的地方。以下是标签从断点混合器进入 CSS 的方式。
@if($label) { body::before { content: breakpoint-label($label); } }
不幸的是,我必须支持旧浏览器,它们在使用 JavaScript 读取我们的 CSS 生成的内容时会有问题。因此,我们必须将 JSON 放在另一个地方,以获得更广泛的兼容性。为此,我将把我们的 JSON 添加到 head 中作为字体系列。
@if($label) {
body::before { content: breakpoint-label($label); }
.lt-ie9 head { font-family: breakpoint-label($label); }
}
CSS 到 JS
我们的布局标签现在位于 CSS 中的 JSON 字符串中。要使用 JavaScript 读取它,我们需要使用 getComputedStyle
这个朋友。让我们创建一个函数来获取我们的 JSON,然后对其进行解析。
function getBreakpoint() {
var style = null;
if ( window.getComputedStyle && window.getComputedStyle(document.body, '::before') ) {
style = window.getComputedStyle(document.body, '::before');
style = style.content;
}
return JSON.parse(style);
}
对于不支持 getComputedStyle
的浏览器,我们需要添加一个小补丁并获取 head 字体系列。
function getBreakpoint() {
var style = null;
if ( window.getComputedStyle && window.getComputedStyle(document.body, '::before') ) {
style = window.getComputedStyle(document.body, '::before');
style = style.content;
} else {
window.getComputedStyle = function(el) {
this.el = el;
this.getPropertyValue = function(prop) {
var re = /(\-([a-z]){1})/g;
if (re.test(prop)) {
prop = prop.replace(re, function () {
return arguments[2].toUpperCase();
});
}
return el.currentStyle[prop] ? el.currentStyle[prop] : null;
};
return this;
};
style = window.getComputedStyle(document.getElementsByTagName('head')[0]);
style = style.getPropertyValue('font-family');
}
return JSON.parse(style);
}
我们的函数现在存在一个主要问题。我们的 JSON 作为字符串传递,这意味着它被括在引号中,但使用哪种引号取决于你使用的浏览器。WebKit 将字符串包装在单引号中传递。Firefox 使用双引号将字符串包装起来,这意味着它会转义 JSON 中的双引号。IE8 做了一些很奇怪的事情,在字符串末尾添加了 ; }
。为了解决这些不一致问题,我们需要另一个函数在解析之前规范化我们的 JSON。
function removeQuotes(string) {
if (typeof string === 'string' || string instanceof String) {
string = string.replace(/^['"]+|\s+|\\|(;\s?})+|['"]$/g, '');
}
return string;
}
现在,在我们 getBreakpoint
函数的返回值中解析 JSON 之前,我们只需将字符串传递给 removeQuotes
函数。
return JSON.parse( removeQuotes(style) );
图像源匹配
JavaScript 现在可以读取我们为每个断点定义的标签。在这一点上,将该标签与响应式图像源匹配是微不足道的。以以下图像为例。
<img data-small="small.jpg" data-large="large.jpg" />
当活动媒体查询为 small
时,我们可以让 JavaScript 将其与 data-small
匹配并将图像的源设置为 small.jpg
。如果您为每个断点都声明了一个源,这非常有效,但正如您在我们的示例中看到的,我们没有为 medium
定义源。这是一个非常常见的情况。较小的图像通常可以在较大的布局中使用。也许中等的布局只是添加了一个侧边栏,而我们的图像大小没有改变。那么 JavaScript 如何知道在布局为中等时拉取较小的源呢?为此,我们需要排序。
Sass 列表到 JavaScript 数组
每次创建断点时,我们都可以将该标签存储在 Sass 列表中。在我们的断点混合器中,我们可以将标签附加到我们的列表中。
$label-list: append($label-list, $label, comma);
假设我们的列表默认为预填充的移动标签 small
,以下断点将创建一个 small, medium, large
列表。
@include breakpoint(8, $label: "medium");
@include breakpoint(12, $label: "large");
在 Sass 中声明断点的顺序将决定标签的顺序。让我们将这个 Sass 列表作为数组添加到我们的 JSON 函数中。
@function breakpoint-label($label) {
$label-list: append($label-list, $label, comma);
@return '{ "current" : "#{$label}", "all": [#{$label-list}] }';
}
现在,除了 JavaScript 知道当前断点标签之外,它还可以查看我们传递的数组并了解它们的顺序。因此,如果布局为 medium
并且在我们的图像上没有找到匹配的数据属性,JavaScript 可以找到我们标签数组中的 medium
,并在该数组中向后遍历,直到找到匹配的源。
总结
这个响应式图像解决方案对我来说非常重要。通过用标签标记媒体查询值,我创建了灵活性简便性。如果我更改断点的大小,我只需要在一个地方更改它,即我的 Sass。我的 HTML 图像源不依赖于媒体查询的值,只依赖于它们的名称。
虽然我使用 Sass 实现了这一点,但这实际上不是必需的。您可以手动将 JSON 字符串写入媒体查询,Sass 只会帮助自动执行该过程。这是一个 简单的 Pen,它使用纯 CSS。
Check out this Pen!
要查看此技术的更强大、生产质量的实现,请查看我的框架 Breakpoint 的代码。
请注意,如果您希望您的图像标签有效,那么请使用诸如 src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D"
之类的源来对其进行填充,并且请确保您提供了一个
我希望分享这篇文章能激发更多关于抽象响应式图像的复杂性和可维护性的讨论和想法。我很想知道您的想法,以及您是否有改进它的方法。
我们都站在巨人的肩膀上。Jeremy Keith 的文章 Conditional CSS 和 Viljami Salminen 的 detectMQ 为此提供了很多功劳和灵感。
这是一种非常巧妙的技术,伙计。似乎要让它工作需要很多工作,要为不同的浏览器处理 JSON,真是让人头疼。没有人有时间做这些!哈哈
只是开玩笑,很棒的技术。
感谢佩德罗。在项目中使用它真的很容易。不过,要达到这一步,付出了很多努力。
很棒的技术。我喜欢 SASS 部分。为响应式图像技术提供了更多燃料。
我使用 **类似的技术** 开发了 Responsive-Fragments(在 Github 上),但它使用 Ajax 加载与特定 MediaQuery 断点“关键字”或标签匹配的内容(HTML 片段)。使用与 HTML 片段 URL 匹配的 HTML data-attribute。
<div data-device-tablet="/fragments/sidebar.html"></div>
它还使用一个回退(类似数组)系统,在桌面设备上加载后续的 HTML 片段(加载智能手机、平板电脑和桌面 HTML 片段)。这在根据设备 MediaQuery 加载不同的 HTML 以及优化带宽/图像等时非常有用。
这也受到了 Jeremy Keith 文章的启发。
来自荷兰的亲切问候。
这真是一个令人印象深刻的技术。感谢分享。
确实很有趣!
我们在 Github 上使用类似的方法让我们的 Css/Sass/Less 在 Javascript 中触发断点,并在其基础上构建了一个库:https://github.com/14islands/js-breakpoints
我是不是漏掉了什么,或者不支持生成内容的浏览器也不 **支持** Media Queries?(我说的是 IE8)。
由于我创建的桌面布局都包装在 media queries 中,我创建了一个使用
lt-ie9
类沙盒化的重复布局。<head>
font-family 代码被添加到lt-ie9
类中。啊哈,我也有同样的疑问。感谢你的信息 - 因此你拥有桌面布局的无响应版本,JavaScript 也需要能够找出这些旧浏览器中应用的样式,对吗?这很有道理,我想我自己可能不会想到这一点。
(顺便说一句,很棒的技术。我一直苦恼于如何告知浏览器已应用两个或多个 media queries,虽然你的文章没有直接解决这个问题,但它给了我一个很棒的想法。我从未想过使用 JSON。)
另一个想法:只使用
head { font-family: ... }
hack 用于所有浏览器,而不是同时使用它和body::before
,这样做有什么问题吗?在 Opera 中,使用 head font-family 技巧不起作用。它不会返回声明的值,而是返回实际值。因此,无论你在其中输入什么,Opera 都将返回“Times New Roman”。一旦 Opera 完全切换到 Webkit,可能值得重新审视使用 font-family 传递字符串的兼容性。
内容属性怎么样?这可行吗?
通常情况下,使用这种方法(使用
content
向 body 添加一些标记),你受到 media queries 必须相互排斥的限制。然而,你通过生成一个 JSON 数组完全避开了这个问题,因此查询可以级联。非常聪明,非常棒的方法。太棒了!
我知道;当我阅读这篇文章时,我也突发奇想。我认为他的技术仍然只允许在一个时间点激活一个查询,但允许你提供一个回退列表。我认为,如果你想同时激活两个 media queries,你必须进行一些非常复杂的检查,并为每个查询组合设置排列。以下是一个想法的雏形,因为我需要构建它
没有显示
permute-queries()
函数,它会找出所有不同的“标签查询”组合来输出。当然,随着你添加查询,排列的数量会呈指数级增长 - 你将有 n2 个排列,准确地说。这意味着,对于三个查询,你将有九个标签查询,其中一些查询没有意义 -@media (max-width: 320px) and (min-width: 321px)
?!我们不必担心混淆浏览器,因为该查询不会应用,但它确实会使样式表变得臃肿。直接在 Sass 代码中输入
@media
查询解析,以剔除无意义的查询。这听起来越来越像是一场噩梦了。似乎它在 < IE8 上不起作用,我在 BrowserStack 上进行了测试,但你始终使用条件 IE 注释或服务器端检测。
如果你正在测试 Codepen 演示,它在 IE8 中将不起作用。对于可靠的跨浏览器解决方案,请查看我为 jQuery 插件 编写的 JavaScript 代码。
太酷了!
我一直在思考如何将信息从 CSS 发送到 JS。我尝试了另一种方法,你的 Sass 实际上输出 JSON 或 JavaScript 文件以及 CSS - https://github.com/edwardoriordan/sass-attributes。
如果我们能找到将数据从 CSS 发送到 JS 的方法(除了在运行时使用 Javascript 解析所有 CSS - 这在大多数情况下可能太慢),我们可以开始开发对 CSS in JS 友好的 polyfills。
根据规范(AFAIK,但如果我错了请纠正我),以下内容应该可以工作,如果真的可以工作,会简单得多
http://codepen.io/jjenzz/pen/jKsom
关键是传递给
attr()
的url
类型或单位 参数。也许有一种方法可以为它创建一个 ployfill,直到浏览器支持与规范匹配?抱歉,所有链接都坏了……
1) http://codepen.io/jjenzz/pen/jKsom
2) http://www.w3.org/TR/css3-values/#attr-notation
当我阅读 Christian Heilmann 的这篇文章 时,我想尝试同样的事情。似乎有一个无 JavaScript 的解决方案潜藏其中。
我之前构建了一个小型 JavaScript 对象和 Sass mixin 来实现这一点。非常希望有人能贡献一些代码让它变得更好。 https://github.com/danott/breakpoints
这确实是一个巧妙的解决方案,但它不会影响你的网站 SEO,因为这与图像有关吗?搜索爬虫会在 JS 完成其工作后读取图像的动态添加的 src 属性吗?据我所知,爬虫不会读取 data-* 属性。
也许 Web 应用更适合使用这种情况,因为 SEO 不那么重要?
是的,SEO 是响应式图像 hack 的一个问题。我不确定机器人是否会索引
<noscript>
回退中的图像。更重要的是,我们需要一个标准。我开始编写一些 Sass 函数来创建列表并将它们字符串化为 JSON。目前它只有一维,但这是一个开始。
http://codepen.io/lesjames/pen/ulcFp
调整大小处理程序应进行防抖处理,因为浏览器在用户操作期间会连续(每隔几毫秒)触发此事件。例如,在我的 测试 中,手动缩小浏览器窗口会产生大约 20 个调整大小事件。由于处理程序内部执行了“繁重”的操作(查询所有
<img>
元素等),因此不应该强制此操作在单个用户触发的调整大小操作期间执行多次。参见:事件防抖
嗯,我不认为这段
getComputedStyle
代码是一个好方法。以跨浏览器方式检索 CSS 样式的功能已经由通用的第三方 JS 库(如 jQuery)提供。手动检测getComputedStyle
并实现自定义 polyfill 只会使本文比应有的复杂和混乱。JS 在此模式中的工作很简单:1. 从body
的 CSS 样式中检索当前标签名称,2. 查询所有图像,3. 循环遍历图像并相应地设置src
属性。这可以使用 JS 库用几行代码编写。尝试手动实现这些内容只会降低我们解决方案的可靠性,因为你的代码更容易出现错误。