将固定尺寸对象转换为响应式元素

Avatar of Philip Kiely
Philip Kiely

DigitalOcean 为您的旅程各个阶段提供云产品。立即开始使用 200 美元的免费信用额度!

我最近遇到一个情况,需要在网站上展示 iPhone。我希望用户能够与这个“模拟”手机上的应用程序演示进行交互,因此它必须用 CSS 渲染,而不是图像。我发现了一个很棒的库,叫做 marvelapp/devices.css。该库使用纯 CSS 实现了我需要的设备,并且看起来很棒,但有一个问题:它提供的设备没有响应式(即无法缩放)。一个 已打开的问题 列出了几个选项,但每个选项都存在浏览器兼容性问题或其他问题。我开始修改该库以使设备响应式。

这是 最终的可调整大小的库。下面,我们将逐步介绍创建它所涉及的代码。

原始库是用 Sass 编写的,并使用具有固定像素尺寸的元素来实现设备。作者还为每个设备提供了一个简单的 HTML 示例,包括我们将在本文中一直使用的 iPhone X。以下是原始库的示例。请注意,它渲染的设备虽然详细,但相当大,而且尺寸不会改变。

以下是如何实现

我使用了三个 CSS 技巧来使设备可调整大小

  1. calc()一个 CSS 函数,它可以执行计算,即使输入具有不同的单位
  2. --size-divisor,一个与 var() 函数一起使用的 CSS 自定义属性
  3. @media 查询,以 min-width 分隔

让我们来看看它们中的每一个。

calc()

CSS 的 calc() 函数 允许我们更改设备各个方面的尺寸。该函数以表达式作为输入,并将函数的计算结果作为输出返回,并带有适当的单位。如果我们想将设备的大小缩减一半,则需要将每个像素测量值除以 2。

之前

width: 375px;

之后

width: calc(375px / 2);

第二个代码段的长度是第一个代码段长度的一半。原始库中的每个像素测量值都需要用 calc() 函数包装,才能调整整个设备的大小。

var(–size-divisor)

必须先在文件开头声明 CSS 变量,然后才能在任何地方使用它。

:root {
  --size-divisor: 3;
}

有了它,这个值就可以通过 var() 函数在整个样式表中使用。这将非常有用,因为我们希望在调整设备大小时,所有像素计数都除以相同的数字,同时避免使用魔数并简化使设备响应式所需的代码。

width: calc(375px / var(--size-divisor));

定义了上面的值后,它将返回原始宽度除以三,以像素为单位。

@media

为了使设备响应式,它们必须响应屏幕尺寸的变化。我们使用 媒体查询 来实现这一点。这些查询监视屏幕的宽度,并在屏幕尺寸超过给定阈值时触发。我根据 Bootstrap 的 xssm 等尺寸 选择了断点。

无需在最小宽度为零像素时设置断点;相反,文件开头的 :root 声明处理了这些超小型屏幕。这些媒体查询必须放在文档末尾,并按 min-width 升序排列。

@media (min-width: 576px) {
  :root {
    --size-divisor: 2;
  }
}
@media (min-width: 768px) {
  :root {
    --size-divisor: 1.5;
  }
}
@media (min-width: 992px) { 
  :root {
    --size-divisor: 1;
  }
}
@media (min-width: 1200px) { 
  :root {
    --size-divisor: .67;
  }
}

更改这些查询中的值将调整设备进行调整大小的幅度。请注意,calc() 既可以处理浮点数,也可以处理整数。

使用 Python 预处理预处理器

有了这些工具,我需要一种方法将我的新方法应用于这个包含数千行的库。结果文件将以一个变量声明开头,包含整个库,每个像素测量值都用 calc() 函数包装,最后以上面的媒体查询结尾。

为了避免手动准备这些更改,我创建了一个 Python 脚本,它可以自动转换所有像素测量值。

def scale(token):
  if token[-2:] == ';\n':
    return 'calc(' + token[:-2] + ' / var(--size-divisor));\n'
  elif token[-3:] == ');\n':
    return '(' + token[:-3] + ' / var(--size-divisor));\n'
  return 'calc(' + token + ' / var(--size-divisor))'

这个函数在给定包含 NNpx 的字符串时,将返回 calc(NNpx / var(--size-divisor));。在整个文件中,幸运的是只有三个像素匹配项:NNpxNNpx;以及 NNpx);。其余的只是字符串连接。但是,这些标记已经通过空格分隔每一行来生成。

def build_file(scss):
  out = ':root {\n\t--size-divisor: 3;\n}\n\n'
  for line in scss:
    tokens = line.split(' ')
    for i in range(len(tokens)):
      if 'px' in tokens[i]:
        tokens[i] = scale(tokens[i])
    out += ' '.join(tokens)
  out += "@media (min-width: 576px) {\n  \
    :root {\n\t--size-divisor: 2;\n  \
    }\n}\n\n@media (min-width: 768px) {\n \
    :root {\n\t--size-divisor: 1.5;\n  \
    }\n}\n\n@media (min-width: 992px) { \
    \n  :root {\n\t--size-divisor: 1;\n  \
    }\n}\n\n@media (min-width: 1200px) { \
    \n  :root {\n\t--size-divisor: .67;\n  }\n}"
  return out

这个函数构建了新的库,它首先声明了 CSS 变量。然后,它遍历整个旧库,搜索要缩放的像素测量值。对于它找到的数百个包含 px 的标记中的每一个,它都会缩放该标记。随着迭代的进行,该函数通过重新连接标记来保留原始行结构。最后,它追加必要的媒体查询,并将整个库作为字符串返回。运行这些函数和从文件读取和写入的少量代码完成了这项工作。

if __name__ == '__main__':
  f = open('devices_old.scss', 'r')
  scss = f.readlines()
  f.close()
  out = build_file(scss)
  f = open('devices_new.scss', 'w')
  f.write(out)
  f.close()

这个过程在 Sass 中创建了一个新的库。要创建最终使用的 CSS 文件,请运行

sass devices_new.scss devices.css

这个新的库提供了相同的设备,但它们是响应式的!这是它的示例

要阅读实际的输出文件,该文件包含数千行,请 在 GitHub 上查看。

其他方法

虽然这个过程的结果非常引人注目,但要获得这些结果需要一些工作。为什么我没有选择更简单的方法?以下是三种具有优缺点的其他方法。

缩放

一种最初很有希望的方法是使用缩放来调整设备大小。这将统一缩放设备,并且可以与我的解决方案一样与媒体查询配对,但它可以在没有麻烦的 calc() 和变量的情况下运行。

这行不通,原因很简单:缩放是一个非标准属性。除了其他限制之外,它在 Firefox 中不受支持。

将 px 替换为 em

另一种查找和替换方法建议将所有 px 实例替换为 em。然后,设备会根据字体大小缩小和放大。但是,要使设备小到足以适应移动显示屏,可能需要使用微小的字体大小,小于 Chrome 等浏览器强制执行的最小大小。如果访问您网站的访客正在使用会增大字体大小的辅助技术,这种方法也会遇到麻烦。

这可以通过将所有值缩小 100 倍,然后应用标准字体大小来解决。但是,这需要与本文中的方法一样多的预处理,我认为直接执行这些计算比操纵字体大小更优雅。

scale()

The scale() function can change the size of entire objects. The function returns a <transform-function>, which can be given to a transform attribute in a style. Overall, this is a strong approach, but does not change the actual measurement of the CSS device. I prefer my approach, especially when working with complex UI elements rendered on the device’s “screen.”

Chris Coyier prepared an example using this approach.

In conclusion

Hundreds of calc() calls might not be the first tool I would reach for if implementing this library from scratch, but overall, this is an effective approach for making existing libraries resizable. Adding variables and media queries makes the objects responsive. Should the underlying library be updated, the Python script would be able to process these changes into a new version of our responsive library.