滑向未来(现代 JavaScript 与 CSS 滚动实现指南)

2018/05/14 · CSS,
JavaScript ·
滚动

原文出处: Evil
Martians   译文出处:众成翻译-sea_ljf   

一些(网站)滚动的效果是如此令人着迷但你却不知该如何实现,本文将为你揭开它们的神秘面纱。我们将基于最新的技术与规范为你介绍最新的
JavaScript 与 CSS
特性,(当你付诸实践时,)将使你的页面滚动更平滑、美观且性能更好。

大多数的网页的内容都无法在一屏内全部展现,因而(页面)滚动对于用户而言是必不可少的。对于前端工程师与
UX
设计师而言,跨浏览器提供良好的滚动体验,同时符合设计(要求),无疑是一个挑战。尽管
web
标准的发展速度远超从前,但代码的实现往往是落后的。下文将为你介绍一些常见的关于滚动的案例,检查一下你所用的解决方案是否被更优雅的方案所代替。

使用box-shadow进行画图(性能优化终结者)

2018/06/01 · CSS ·
box-shadow

原文出处: 贾顺名   

最近突然想做一些好玩的东西,找来找去,想到了之前曾经在网上看到过有人用box-shadow画了一副蒙娜丽莎出来
感觉这个挺有意思,正好趁着周末,自己也搞一波

前言

消逝的滚动条

在过去的三十年里,滚动条的外观不断改变以符合设计的趋势,设计师们为(滚动条的)颜色、阴影、上下箭头的形状与边框的圆角实验了多种风格。以下是
Windows 上的变化历程:

图片 1(Windows
上的滚动条)

在2011年,苹果设计师从 ios
上获得灵感,为如何定义“美观的”滚动条确定了方向。所有滚动条均从 Mac
电脑中消失,不再占据任何页面空间,只有在用户触发滚动时(滚动条)才会重新出现(有些用户会设置不隐藏滚动条)。

图片 2(Mac 上的滚动条)

滚动条安静地消逝并未引起苹果粉丝的不满,已经习惯了 iPhone 与 iPad
上滚动方式的用户很快地习惯了这一设计。大多数开发人员与设计师都认为这是一个“好消息”,因为计算滚动条的宽度可真是件苦差事。

然而,我们生活在一个拥有众多操作系统与浏览器的世界中,它们(对于滚动)的实现各不相同。如果你和我们一样是一名
Web 开发者,你可不能把“滚动条问题”置之不理。

以下将为你介绍一些小技巧,使你的用户在滚动时有更好的体验。

前言

在上一篇博文中介绍了Vue.js的常用指令,今天总结归纳一下弹窗Dialog的使用,弹窗经常被使用在一些表单的增删改查啊,或者弹出一些提示信息等等,在保留当前页面状态的情况下,告知用户并承载相关操作。

隐藏但可滚动

先来看看一个关于模态框的经典例子。当它被打开的时候,主页面应该停止滚动。在
CSS 中有如下的快捷实现方式:

body { overflow: hidden; }

1
2
3
body {
  overflow: hidden;
}

但上述代码会带来一点不良的副作用:

图片 3 (注意红色剪头)

在这个示例中,为了演示目的,我们在 Mac
系统中设置了强制显示滚动条,因而用户体验与 Windows 用户相似。

我们该如何解决这个问题呢?如果我们知道滚动条的宽度,每次当模态框出现时,可在主页面的右边设置一点边距。

由于不同的操作系统与浏览器对滚动条的宽度不一,因而获取它的宽度并不容易。在Mac
系统中,无论任何浏览器(滚动条)都是统一15px,然而 Windows
系统可会令开发者发狂:

图片 4(“百花齐放”的宽度)

注意,以上仅是 Windows
系统下基于当前最新版浏览器(测试所得)的结果。以前的(浏览器)版本(宽度)可能有所不同,也没人知道未来(滚动条的宽度)会如何变化。

不同于猜测(滚动条的宽度),你可以通过 JavaScript
计算它的宽度(译者注:实测以下代码仅能测出原始的宽度,通过 CSS
改变了滚动条宽度后,以下代码也无法测出实际宽度):

const outer = document.createElement(‘div’); const inner =
document.createElement(‘div’); outer.style.overflow = ‘scroll’;
document.body.appendChild(outer); outer.appendChild(inner); const
scrollbarWidth = outer.offsetWidth – inner.offsetWidth;
document.body.removeChild(outer);

1
2
3
4
5
6
7
const outer = document.createElement(‘div’);
const inner = document.createElement(‘div’);
outer.style.overflow = ‘scroll’;
document.body.appendChild(outer);
outer.appendChild(inner);
const scrollbarWidth = outer.offsetWidth – inner.offsetWidth;
document.body.removeChild(outer);

尽管仅仅七行代码(就能测出滚动条的宽度),但有数行代码是操作 DOM
的。(为性能起见,)如非必要,尽量避免进行 DOM 操作。

解决这个问题的另一个方法是在模态框出现时仍保留滚动条,以下是基于这思路的纯
CSS 实现:

html { overflow-y: scroll; }

1
2
3
html {
  overflow-y: scroll;
}

尽管“模态框抖动”问题解决了,但整体的外观却被一个无法使用的滚动条影响了,这无疑是设计中的硬伤。

在我们看来,更好的解决方案是完全地隐藏滚动条。纯粹用 CSS
也是可以实现的。该方法(达到的效果)和 macOS
的表现并不是完全一致,(当用户)滚动时滚动条仍然是不可见的。滚动条总是处于不可见状态,然而页面是可被滚动的。对于Chrome,Safari
和 Opera 而言,可以使用以下的 CSS:

.container::-webkit-scrollbar { display: none; }

1
2
3
.container::-webkit-scrollbar {
  display: none;
}

IE 或 Edge 可用以下代码:

.container { -ms-overflow-style: none; }

1
2
3
.container {
  -ms-overflow-style: none;
}

至于 Firefox,很不幸,没有任何办法隐藏滚动条。

正如你所见,并没有任何银弹。任何解决方案都有它的优点与缺点,应根据你项目的需要选择最合适的。

在线地址:

优化前的版本
优化后的版本
源码仓库地址

不建议上传大图片。。喜欢听电脑引擎声的除外


首先,并不打算单纯的实现某一张图片(这样太没意思了),而是通过上传图片,来动态生成box-shadow的数据。
所以,你需要了解这些东西:

  1. box-shadow
  2. canvas

之前做了表格的增删改查任务,其中用到了dialog弹窗,今天总结归纳一下Vue.js中几种弹窗Dialog的使用

外观争议

需要承认的是,滚动条的样子在部分操作系统上并不好看。一些设计师喜欢完全掌控他们(所设计)应用的样式,任何一丝细节也不放过。在 GitHub
上有上百个库借助
JavaScript 取代系统滚动条的默认实现,以达到自定义的效果。

但如果你想根据现有的浏览器定制一个滚动条呢?(很遗憾,)并没有通用的
API,每个浏览器都有其独特的代码实现。

尽管5.5版本以后的 IE
浏览器允许你修改滚动条的样式,但它只允许你修改滚动条的颜色。以下是如何重新绘制(滚动条)拖动部分与箭头的代码:

body { scrollbar-face-color: blue; }

1
2
3
body {
  scrollbar-face-color: blue;
}

但只改变颜色对提高用户体验而言帮助不大。据此,WebKit
的开发者在2009年提出了(修改滚动条)样式的方案。以下是使用 -webkit 前缀在支持相关样式的浏览器中模拟
macOS 滚动条样式的代码:

::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-thumb {
background-color: #c1c1c1; border-radius: 4px; }

1
2
3
4
5
6
7
::-webkit-scrollbar {
  width: 8px;
}
::-webkit-scrollbar-thumb {
  background-color: #c1c1c1;
  border-radius: 4px;
}

Chrome、Safari、Opera 甚至于 UC
浏览器或者三星自带的桌面浏览器都支持(上述
CSS)。Edge 也有计划实现它们。但三年过去了,该计划仍在中等优先级中(而尚未被实现)。

当我们讨论滚动条的定制时,Mozilla
基金会基本上是无视了设计师的需求。(有开发者在)17年前就已经提出了一个希望修改滚动条样式的请求。而就在几个月前,Jeff
Griffiths(Firefox 浏览器总监)终于为这个问题作出了回答:

“除非团队中有人对此有兴趣,否则我对此毫不关心。”

公平地说,从 W3C 的角度看来,尽管 WebKit
的实现得到广泛的支持,但它仍然不是标准。现有的为滚动条修改样式的草案,是基于
IE 的:仅能修改它的颜色。

伴随着请求如同 WebKit
一样支持滚动条样式修改 issue 的提交,争议仍在继续。如果你想影响
CSS 工作小组,是时候参与讨论了。也许这不是优先级最高的问题,但(如同
WebKit
一样修改滚动条样式)得到标准化后,能使很多前端工程师与设计师轻松很多。

box-shadow

box-shadow可以让我们针对任意一个html标签生成阴影,我们可以控制阴影的偏移量、模糊半径、实际半径、颜色等一系列属性。
语法如下:

selector { /* offset-x | offset-y | color */ box-shadow: 60px -16px
teal; /* offset-x | offset-y | blur-radius | color */ box-shadow: 10px
5px 5px black; /* offset-x | offset-y | blur-radius | spread-radius |
color */ box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.2); /* inset |
offset-x | offset-y | color */ box-shadow: inset 5em 1em gold; /* Any
number of shadows, separated by commas */ box-shadow: 3px 3px red, -1em
0 0.4em olive; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
selector {
  /* offset-x | offset-y | color */
  box-shadow: 60px -16px teal;
 
  /* offset-x | offset-y | blur-radius | color */
  box-shadow: 10px 5px 5px black;
 
  /* offset-x | offset-y | blur-radius | spread-radius | color */
  box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.2);
 
  /* inset | offset-x | offset-y | color */
  box-shadow: inset 5em 1em gold;
 
  /* Any number of shadows, separated by commas */
  box-shadow: 3px 3px red, -1em 0 0.4em olive;
}

这里是MDN的box-shadow描述,里边有一些示例。

基本用法

流畅的操作体验

对于滚动而言,最常见的任务是登录页的导航(跳转)。通常,它是通过锚点链接来完成的。只需要知道元素的 id 即可:

<a href=”#section”>Section</a>

1
<a href="#section">Section</a>

点击该链接会  到(该锚点对应的)区块上,(然而) UX
设计师一般会坚持认为该过程应是平滑地运动的。GitHub
上有大量造好的轮子(帮你解决这个问题),然而它们或多或少都用到
JavaScript。(其实)只用一行代码也能实现同样的效果,最近DOM API
中的 Element.scrollIntoView() 可以通过传入配置对象来实现平滑滚动:

elem.scrollIntoView({ behavior: ‘smooth’ });

1
2
3
elem.scrollIntoView({
  behavior: ‘smooth’
});

然而该属性兼容性较差且仍是通过脚本(来控制样式)。如有可能,应尽量少用额外的脚本。

幸运的是,有一个全新的 CSS
属性(仍在工作草案中),可以用简单的一行代码改变整个页面滚动的行为。

html { scroll-behavior: smooth; }

1
2
3
html {
  scroll-behavior: smooth;
}

结果如下:

图片 5

(从一个区块跳到另一个)

图片 6

(平滑地滚动)

你可以在 codepen 上试验这个属性。在撰写本文时,scroll-behavior 仅在
Chrome、 Firefox 与 Opera 上被支持,但我们希望它能被广泛支持,因为使用
CSS (比使用
JavaScript)在解决页面滚动问题时优雅得多,并更符合“渐进增强”的模式。

canvas

是的,我们还需要canvas,因为我们需要将图片资源转存到canvas中,再生成我们实际需要的数据格式。
在这里并不会拿canvas去做渲染之类的,单纯的是要利用canvas的某些API。

dialog弹出对话框

粘性 CSS

另一个常见的需求是根据滚动方向动态地定住元素,即有名的“粘性(即 CSS
中的position: sticky)”效应。

图片 7 (一个粘性元素)

在以前的日子里,要实现一个“粘性”元素需要编写复杂的滚动处理函数去计算元素的大小。(然而)该函数较难处理元素在“黏住”与“不黏住”之间微小的延迟,(通常会)导致(元素)抖动的出现。通过
JavaScript 来实行(“粘性”元素)也有性能上的问题,特别是在(需要)调用
[Element.getBoundingClientRect() ]时(https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect)。%E3%80%82)

不久之前,CSS
实现了 position: sticky 属性。只需通过指定(某方向上的)偏移量即可实现我们想要的效果。

.element { position: sticky; top: 50px; }

1
2
3
4
.element {
  position: sticky;
  top: 50px;
}

(编写上述代码后,)剩下的就交由浏览器实现即可。你可以在 codepen 上试验一下。撰写本文之时,position: sticky 在各式浏览器(包括移动端浏览器)上支持良好,所以如果你还在使用
JavaScript 去解决这个问题的话,是时候换成纯 CSS 的实现了。

首版规划

刚开始的规划大致是这样的:

  1. 我们上传一张图片
  2. 创建一个Image对象接收上传的图片资源
  3. Image对象放入canvas
  4. 通过canvas生成图片文件对应的rgba数据
  5. 处理rgba数据转换为box-shadow属性
  6. done

图片 8图片 9

全面使用函数节流

从浏览器的角度看来,滚动是一个事件,因此在 JavaScript
中是使用一个标准化的事件监听器 addEventListener 去处理它: ,

window.addEventListener(‘scroll’, () => { const scrollTop =
window.scrollY; /* doSomething with scrollTop */ });

1
2
3
4
window.addEventListener(‘scroll’, () => {
  const scrollTop = window.scrollY;
  /* doSomething with scrollTop */
});

用户往往高频率地滚动(页面),但如果滚动事件触发太频繁的话,会导致性能上的问题,可以通过使用函数节流这一技巧去优化它。

window.addEventListener(‘scroll’, throttle(() => { const scrollTop =
window.scrollY; /* doSomething with scrollTop */ }));

1
2
3
4
window.addEventListener(‘scroll’, throttle(() => {
  const scrollTop = window.scrollY;
  /* doSomething with scrollTop */
}));

你需要定义一个节流函数包装原来的事件监听函数,(节流函数是)减少被包装函数的执行次数,只允许它在固定的时间间隔之内执行一次:

function throttle(action, wait = 1000) { let time = Date.now(); return
function() { if ((time + wait – Date.now()) < 0) { action(); time =
Date.now(); } } }

1
2
3
4
5
6
7
8
9
function throttle(action, wait = 1000) {
  let time = Date.now();
  return function() {
    if ((time + wait – Date.now()) < 0) {
        action();
        time = Date.now();
    }
  }
}

为了使(节流后的)滚动更平滑,你可以通过使用 window.requestAnimationFrame() 来实现函数节流:

function throttle(action) { let isRunning = false; return function() {
if (isRunning) return; isRunning = true; window.requestAnimationFrame(()
=> { action(); isRunning = false; }); } }

1
2
3
4
5
6
7
8
9
10
11
function throttle(action) {
  let isRunning = false;
  return function() {
    if (isRunning) return;
    isRunning = true;
    window.requestAnimationFrame(() => {
      action();
      isRunning = false;
    });
  }
}

当然,你可以通过现有的开源轮子来实现,就像 Lodash 一样。你可以访问 codepen 来看看上述解决方案与
Lodash 中的 _.throttle 之间的区别。

使用哪个(开源库)并不重要,重要的是在需要的时候,记得优化你(页面中的)滚动处理函数。

如何接收图片文件数据

我们在监听input[type="file"]change事件时,可以在target里边拿到一个files的对象。
该对象为本次上传传入的文件列表集合,一般来说我们取第一个元素就是了。
我们拿到了一个File类型的对象,接下来就是用Image来接收这个File对象了。

这里会用到一个浏览器提供的全局对象URLURL提供了一个createObjectURL的方法。
方法接收一个Blob类型的参数,而File则是继承自Blog,所以我们直接传入就可以了。
然后再使用一个Image对象进行接收就可以了:

$input.addEventListener(‘change’, ({target: {files: [file]}}) => {
let $img = new Image() $img.addEventListener(‘load’, _ => {
console.log(‘we got this image’) }) $img.src = URL.createObjectURL(file)
})

1
2
3
4
5
6
7
8
9
$input.addEventListener(‘change’, ({target: {files: [file]}}) => {
  let $img = new Image()
 
  $img.addEventListener(‘load’, _ => {
    console.log(‘we got this image’)
  })
 
  $img.src = URL.createObjectURL(file)
})

MDN关于URL.createObjectURL的介绍

<el-button type="text" @click="dialogVisible = true">点击打开 Dialog</el-button>

<el-dialog
  title="提示"
  :visible.sync="dialogVisible"
  width="30%"
  :before-close="handleClose">
  这是一段信息

    <el-button @click="dialogVisible = false">取 消</el-button>
    <el-button type="primary" @click="dialogVisible = false">确 定</el-button>

</el-dialog>

在视窗中显示

当你需要实现图片懒加载或者无限滚动时,需要确定元素是否出现在视窗中。这可以在事件监听器中处理,最常见的解决方案是使用 lement.getBoundingClientRect() :

window.addEventListener(‘scroll’, () => { const rect =
elem.getBoundingClientRect(); const inViewport = rect.bottom > 0 &&
rect.right > 0 && rect.left < window.innerWidth && rect.top <
window.innerHeight; });

1
2
3
4
5
6
window.addEventListener(‘scroll’, () => {
  const rect = elem.getBoundingClientRect();
  const inViewport = rect.bottom > 0 && rect.right > 0 &&
                     rect.left < window.innerWidth &&
                     rect.top < window.innerHeight;
});

上述代码的问题在于每次调用 getBoundingClientRect 时都会触发回流,严重地影响了性能。在事件处理函数中调用( getBoundingClientRect )尤为糟糕,就算使用了函数节流(的技巧)也可能对性能没多大帮助。
(回流是指浏览器为局部或整体地重绘某个元素,需要重新计算该元素在文档中的位置与形状。)

在2016年后,可以通过使用 Intersection
Observer 这一
API
来解决问题。它允许你追踪目标元素与其祖先元素或视窗的交叉状态。此外,尽管只有一部分元素出现在视窗中,哪怕只有一像素,也可以选择触发回调函数:

const observer = new IntersectionObserver(callback, options);
observer.observe(element);

1
2
3
const observer = new IntersectionObserver(callback, options);
 
observer.observe(element);

(点击这里,查看触发回流的
DOM 属性和方法。)

此 API
被广泛地支持,但仍有一些浏览器需要 polyfill。尽管如此,它仍是目前最好的解决方案。

通过canvas获取我们想要的数据

canvas可以直接渲染图片到画布中,可以是一个Image对象、HTMLImageElement及更多媒体相关的标签对象。
所以我们上边会把数据暂存到一个Image对象中去。
我们在调用drawImage时需要传入xywidthheight四个参数,前两个必然是0了,关于后边两个属性,正好当我们的Image对象加载完成后,直接读取它的widthheight就是真实的数据:

let context = $canvas.getContext(‘2d’) $img.addEventListener(‘load’, _
=> { context.drawImage($img, 0, 0, $img.width, $img.height) })

1
2
3
4
let context = $canvas.getContext(‘2d’)
$img.addEventListener(‘load’, _ => {
  context.drawImage($img, 0, 0, $img.width, $img.height)
})

当我们把图片渲染至canvas后,我们可以调用另一个API获取rgba相关的数据。

dialog弹出对话框html部分

滚动边界问题

如果你的弹框或下拉列表是可滚动的,那你务必要了解连锁滚动相关的问题:当用户滚动到(弹框或下拉列表)末尾(后再继续滚动时),整个页面都会开始滚动。

图片 10 (连锁滚动的表现)

当滚动元素到达底部时,你可以通过(改变)页面的 overflow 属性或在滚动元素的滚动事件处理函数中取消默认行为来解决这问题。

如果你选择使用 JavaScript
(来处理),请记住要处理的不是“scroll(事件)”,而是每当用户使用鼠标滚轮或触摸板时触发的“wheel(事件)”:

function handleOverscroll(event) { const delta = -event.deltaY; if
(delta< 0 && elem.scrollHeight – elem.scrollTop) { elem.scrollTop =
elem.scrollHeight; event.preventDefault(); return false; } if (delta
> elem.scrollTop) { elem.scrollTop = 0; event.preventDefault();
return false; } return true; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function handleOverscroll(event) {
  const delta = -event.deltaY;
  if (delta< 0 &&  elem.scrollHeight – elem.scrollTop) {
    elem.scrollTop = elem.scrollHeight;
    event.preventDefault();
    return false;
  }
  if (delta > elem.scrollTop) {
    elem.scrollTop = 0;
    event.preventDefault();
    return false;
  }
  return true;
}

不幸的是,这个解决方案不太可靠。同时可能对(页面)性能产生负面影响。

过度滚动对移动端的影响尤为严重。Loren
Brichter 在 iOS 的
Tweetie 应用上创造了一个“下拉刷新”的新手势,这在 UX
社区中引起了轰动:包括 Twitter 与 Facebook
在内的各大应用纷纷采用了(相同的手势)。

当这个特性出现在安卓端的 Chrome
浏览器中时,问题出现了:它会刷新整个页面而不是加载更多的内容,成为开发者在他们的应用中实现“下拉刷新”时的麻烦。

CSS
通过 overscroll-behavior 这个新属性解决问题。它通过控制元素滚动到尽头时的行为来解决下拉刷新与连锁滚动所带来的问题,(它的属性值中)也包含针对不同平台特殊值:安卓的 glow 与
苹果系统中的 rubber band

现在,上面 GIF 中的问题,在 Chrome、Opera 或 Firefox
中可以通过以下一行代码来解决:

.element { overscroll-behavior: contain; }

1
2
3
.element {
  overscroll-behavior: contain;
}

公平地说,IE 与 Edge
实现了(它独有的) -ms-scroll-chaining 属性来控制连锁滚动,但它并不能处理所有的情况。幸运的是,根据这消息,微软的浏览器已经准备实现 overscroll-behavior 这一属性了。

getImageData

我们调用getImageData会返回如下几个参数:

  1. data
  2. width
  3. height

data为一个数组,每相邻的四个元素为一个像素点的rgba描述。
一个类似这样结构的数组:[r, g, b, a, r, g, b, a]

MDN关于context.drawImage的介绍
MDN关于context.getImageData的介绍

图片 11图片 9

触屏之后

触屏设备上的滚动(体验)是一个很大的话题,深入讨论需要另开一篇文章。然而,由于很多开发者忽略了这方面的内容,这里需要提及一下。

(滚动手势无处不在,令人沉迷,以至于想出了如此疯狂的主意去解决“滚动上瘾”的问题。)

周围的人在智能手机屏幕上上下移动他们的手指的频率是多少呢?经常这样对吧,当你阅读本文时,你很可能就在这么做。

当你的手指在屏幕上移动时,你期待的是:页面内容平滑且流畅地移动。

苹果公司开创了“惯性”滚动并拥有它的专利 。它讯速地成为了用户交互的标准并且我们对此已习以为常。

但你也许已经注意到了,尽管移动端系统会为你实现页面上的惯性滚动,但当页面内某个元素发生滚动时,即使用户同样期待惯性滚动,但它并不会出现,这令人沮丧。

这里有一个 CSS 的解决方案,但看起来更像是个 hack:

.element { -webkit-overflow-scrolling: touch; }

1
2
3
.element {
  -webkit-overflow-scrolling: touch;
}

为什么这是个 hack
呢?首先,它只能在支持(webkit)前缀的浏览器上才能工作。其次,它只适用于触屏设备。最后,如果浏览器不支持的话,你就这样置之不理吗?但无论如何,这总归是一个解决方案,你可以试着使用它。

在触屏设备上,另一个需要考虑的问题是开发者如何处理 touchstart 与 touchmove 事件触发时可能存在的性能问题,它对用户滚动体验的影响非常大。这里详细描述了整个问题。简单来说,现代的浏览器虽然知道如何使得滚动变得平滑,但为确认(滚动)事件处理函数中是否执行了 Event.preventDefault() 以取消默认行为,有时仍可能需要花费500毫秒来等待事件处理函数执行完毕。

即使是一个空的事件监听器,从不取消任何行为,鉴于浏览器仍会期待 preventDefault 的调用,也会对性能造成负面影响。

为了准确地告诉浏览器不必担心(事件处理函数中)取消了默认行为,在 WHATWG 的
DOM
标准中存在着一个不太显眼的特性(能解决这问题)。(它就是)Passive
event
listeners,浏览器对它的支持还是不错的。事件监听函数新接受一个可选的对象作为参数,告诉浏览器当事件触发时,事件处理函数永远不会取消默认行为。(当然,添加此参数后,)在事件处理函数中调用 preventDefault 将不再产生效果。

element.addEventListener(‘touchstart’, e => { /* doSomething */ },
{ passive: true });

1
2
3
element.addEventListener(‘touchstart’, e => {
  /* doSomething */
}, { passive: true });

针对不支持该参数的浏览器,这里也有一个 polyfill 。这视频清晰地展示了此改进带来的影响。

处理rgba数据并转换为box-shadow

在上边我们拿到了一个一维数组,接下来就是将它处理为更合理的结构。
P.S. 一维数组是从左到右从上到下排列的,而不是从上到下从左到右

我们可以发现,widthheight相乘正好是data数组的length
而数组的顺序则是先按照x轴进行增加的,所以我们这样处理得到的数据:

function getRGBA (pixels) { let results = [] let {width, height, data}
= pixels for (let i = 0; i < data.length / 4; i++) { results.push({
x: i % width | 0, y: i / width | 0, r: data[i * 4], g: data[i * 4 +
1], b: data[i * 4 + 2], a: data[i * 4 + 3] }) } return results }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getRGBA (pixels) {
  let results = []
  let {width, height, data} = pixels
  for (let i = 0; i < data.length / 4; i++) {
    results.push({
      x: i % width | 0,
      y: i / width | 0,
      r: data[i * 4],
      g: data[i * 4 + 1],
      b: data[i * 4 + 2],
      a: data[i * 4 + 3]
    })
  }
 
  return results
}

我们将length除以4作为循环的最大长度,然后在生成每个像素点的描述时
通过当前下标对图片宽度取余得到当前像素点在图片中的x轴下标
通过当前下标对图片宽度取商得到当前像素点在图片中的y轴下标
同时塞入rgba四个值,这样我们就会拿到一个类似这样结构的数据:

[{ x: 0, y: 0, r: 255, g: 255, b: 255, a: 255 }]

1
2
3
4
5
6
7
8
[{
  x: 0,
  y: 0,
  r: 255,
  g: 255,
  b: 255,
  a: 255
}]
<script>
  export default {
    data() {
      return {
        dialogVisible: false
      };
    },
    methods: {
      handleClose(done) {
        this.$confirm('确认关闭?')
          .then(_ => {
            done();
          })
          .catch(_ => {});
      }
    }
  };
</script>

旧技术运行良好,为何还要改动?

在现代互联网中,过渡地依赖 JavaScript
在各浏览器上实现相同的交互效果不再是合理的,“跨浏览器兼容性”已经成为过去式,更多的
CSS 属性与 DOM API 方法正逐步被各大浏览器所支持。

在我们看来,当你的项目中,有特别酷炫的滚动效果时,渐进增强是最好的做法。

你应该提供(给用户)所有(你能提供的)基础用户体验,并逐步在更先进的浏览器上提供更好的体验。

必要时使用 polyfill,它们不会产生(不必要的)依赖,一旦(某个 polyfill
所支持的属性)得到广泛地支持,你就可以轻松地将它删掉。

六个月之前,在本文尚未成文之时,之前我们描述的属性只被少量的浏览器所支持。而到了本文发表之时,这些属性已被广泛地支持。

也许到了现在,当你上下翻阅本文之时,(之前不支持某些属性的)浏览器已经支持了该属性,这使得你编程更容易,并使你的应用打包出来体积更小。


感谢阅读至此!查阅浏览器的更新日志,积极参与讨论,有助于 web
标准驶向正确的方向。祝大家一帆风顺,顺利滑(滚)向未来!

2 赞 3 收藏
评论

图片 13

将数据生成为box-shadow格式的数据

box-shadow是支持多组属性的,两组属性之间使用,进行分割。
所以,我们拿到上边的数据以后,直接遍历拼接字符串就可以生成我们想要的结果:

let boxShadow = results.map(item => `${item.x}px ${item.y}px
rgba(${item.r}, ${item.g}, ${item.b}, ${item.a})` ).join(‘,’)

1
2
3
let boxShadow = results.map(item =>
  `${item.x}px ${item.y}px rgba(${item.r}, ${item.g}, ${item.b}, ${item.a})`
).join(‘,’)

效果图:
图片 14

虽说这样就做出来了,但是对浏览器来说太不友好了。因为是每一个像素点对应的一个box-shadow属性。
好奇的童鞋可以选择F12检查元素查看该div(反正苹果本是扛不住)
所以为了我们能够正常使用F12,我们下一步的操作就是合并相邻同色值的box-shadow,减少box-shadow属性值的数量。

dialog弹出对话框JS部分

合并相邻的单元格

虽说图片可能是由各种颜色不规则的组合而成,但毕竟还是会有很多是重复颜色的。
所以我们要计算出某一种颜色可合并的最大面积。
针对某一种颜色,用表格表示可能是这样的:
图片 15
就像在图中所示,我们最理想的合并方式应该是这样的 (radius的取值意味着我们只能设置一个正方形)
图片 16
于是。。如果计算出来这一块面积就成为了一个问题-.-

目前的思路是,将数组转换为二维数组,而不是单纯的在对象中用xy标识。
所以,我们对处理数组的函数进行如下修改:

function getRGBA (pixels) { let results = [] let {width, height, data}
= pixels for (let i = 0; i < data.length / 4; i++) { let x = i %
width | 0 let y = i / width | 0 let row = results[y] = results[y] ||
[] row[x] = { rgba: `${data.slice(i * 4, i * 4 + 4)}` //
为了方便后续的对比相同颜色,直接返回一个字符串 } } return results }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getRGBA (pixels) {
  let results = []
  let {width, height, data} = pixels
  for (let i = 0; i < data.length / 4; i++) {
    let x = i % width | 0
    let y = i / width | 0
    let row = results[y] = results[y] || []
    row[x] = {
      rgba: `${data.slice(i * 4, i * 4 + 4)}` // 为了方便后续的对比相同颜色,直接返回一个字符串
    }
  }
 
  return results
}

这时我们就能得到一个按照xy排列的二维数组,下一步的操作就是以任意点为原点,进行匹配周围的cell
参考上边的表格示例,我们会拿到一个类似这样的数据 (仅作示例)

[ [1, 1, 1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1], [1, 1, 1, 1, 1],
[1, 1, 1, 1], [1, 1], [1, 1, 1, 1, 1, 1], ]

1
2
3
4
5
6
7
8
9
[
  [1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1],
  [1, 1, 1],
  [1, 1, 1, 1, 1],
  [1, 1, 1, 1],
  [1, 1],
  [1, 1, 1, 1, 1, 1],
]

在上面的代码当中,一个dialog弹窗需要设置visible属性,它接收Boolean,当为true时显示
Dialog。Dialog
分为两个部分:bodyfooterfooter需要具名为footerslottitle属性用于定义标题,它是可选的,默认值为空。最后,本例还展示了before-close的用法,before-close 仅当用户通过点击关闭图标或遮罩关闭
Dialog 时起效。如果你在 footer 具名 slot 里添加了用于关闭 Dialog
的按钮,那么可以在按钮的点击回调函数里加入 before-close 的相关逻辑。

获取可合并的最大半径

目前采用的是递归的方式,从0,0原点处开始搜索,获取当前原点的色值,然后与周围进行比较,获取一个最大半径的正方形:

/** * 根据给定范围获取匹配当前节点的正方形 * @param {Array} matrix
二维矩阵数组 * @param {Object} tag 当前要匹配的节点 * @param {Number}
[startRowIndex=0] 开始的行下标,默认为1 * @param {Number}
[startColIndex=0] 开始的列下标,默认为1 * <a
href=”;
{Number} 返回一个最小范围 */ function range (matrix, tag, startRowIndex
= 0, startColIndex = 0) { let results = [] rows: for (let rowIndex =
startRowIndex; rowIndex < matrix.length; rowIndex++) { let row =
matrix[rowIndex] for (let colIndex = startColIndex; colIndex <
row.length; colIndex++) { let item = row[colIndex] if (item.rgba !==
tag.rgba) { if (colIndex === startColIndex) { break rows //
这个表示在某一行的第一列就匹配失败了,没有必要再进行后续的匹配,直接`break`到最外层
} else { results.push(colIndex – startColIndex) break //
将当前下标放入集合,终止当前循环 } } else if (colIndex === row.length –
1) { results.push(colIndex – startColIndex) //
这里表示一整行都可以与当前元素匹配 } } } //
对所有的x、y轴的值进行比较获取最小的值 let count = Math.min.apply(Math,
[results.length].concat(results)) return count }

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
/**
* 根据给定范围获取匹配当前节点的正方形
* @param  {Array}  matrix            二维矩阵数组
* @param  {Object} tag               当前要匹配的节点
* @param  {Number} [startRowIndex=0] 开始的行下标,默认为1
* @param  {Number} [startColIndex=0] 开始的列下标,默认为1
* <a href="http://www.jobbole.com/members/wx1409399284">@return</a> {Number}                   返回一个最小范围
*/
function range (matrix, tag, startRowIndex = 0, startColIndex = 0) {
  let results = []
  rows:
  for (let rowIndex = startRowIndex; rowIndex < matrix.length; rowIndex++) {
    let row = matrix[rowIndex]
    for (let colIndex = startColIndex; colIndex < row.length; colIndex++) {
      let item = row[colIndex]
 
      if (item.rgba !== tag.rgba) {
        if (colIndex === startColIndex) {
          break rows
          // 这个表示在某一行的第一列就匹配失败了,没有必要再进行后续的匹配,直接`break`到最外层
        } else {
          results.push(colIndex – startColIndex)
          break
          // 将当前下标放入集合,终止当前循环
        }
      } else if (colIndex === row.length – 1) {
        results.push(colIndex – startColIndex)
        // 这里表示一整行都可以与当前元素匹配
      }
    }
  }
 
  // 对所有的x、y轴的值进行比较获取最小的值
  let count = Math.min.apply(Math, [results.length].concat(results))
 
  return count
}

函数会从起点开始按顺序遍历所有的元素,在遇到不匹配的节点后,就会break进入下次循环,并将当前的下标存入数组中。
在遍历完成后,我们将数组所有的item以及数组的长度(可以认为是y轴的值)一同放入Math.min获取一个最小的值。
这个最小的值就是我们以当前节点为原点时可以生成的最大范围的正方形了。
P.S. 这个计算方式并不是很好,还不够灵活

自定义弹窗内容

递归计算剩余面积

因为上边也只是合并了一个正方形,还会剩下很多面积没有被查看。
所以我们用递归的方式来计算剩余面积,在第一次匹配结束后,是大概这个样子的:
图片 17

所以我们在递归处拆分出了两块会有重复数据的面积:
图片 18图片 19

以及之后的递归也是参照这个样子来的,这样能保证所有的节点都会被照顾到,不会漏掉。(如果有更好的方式,求回复)。

这样配合着前边拿到的半径数据,很轻松的就可以组装出合并后的集合,下一步就是将其渲染到DOM中了。

Dialog
组件的内容可以是任意的,甚至可以是表格或表单,下面分别举例嵌套表单和表格

渲染到box-shadow中

现在我们已经拿到了想要的数据,关于生成box-shadow属性处我们也要进行一些修改,之前因为是一个像素对应一个属性值,但是现在做了一些合并,所以,生成属性值的操作大概是这个样子的:

$output.style.boxShadow = results.map(item => `${item.x}px
${item.y}px 0px ${item.radius}px rgba(${item.target.rgba})` ).join(‘,’)

1
2
3
$output.style.boxShadow = results.map(item =>
  `${item.x}px ${item.y}px 0px ${item.radius}px rgba(${item.target.rgba})`
).join(‘,’)

P.S. xy的值必须要加上半径的值,否则会出现错位,因为box-shadow是从中心开始渲染的,而不是左上角

嵌套表格

完成后的效果对比

原图&两种实现方式的效果对比:
图片 20

我们拿合并前后生成的CSS存为了文件,并查看了文件大小,效果在一些背景不是太复杂的图片上还是很明显的,减少了2/3左右的体积。
如果将rgba替换为hex,还会再小一些
图片 21

现在再进行检查元素不会崩溃了,但是依然会卡:)

eg:

参考资料

  • box-shadow
  • drawImage
  • getImageData
  • createObjectURL

    1 赞 收藏
    评论

图片 13

图片 23图片 9

<el-button type="text" @click="dialogTableVisible = true">打开嵌套表格的 Dialog</el-button>

<el-dialog title="球员信息" :visible.sync="dialogTableVisible">
  <el-table :data="gridData">
    <el-table-column property="number" label="球衣号码" width="150"></el-table-column>
    <el-table-column property="name" label="姓名" width="200"></el-table-column>
    <el-table-column property="position" label="所打位置"></el-table-column>
  </el-table>
</el-dialog>

dialog弹窗嵌套表格html部分

图片 25图片 9

发表评论

电子邮件地址不会被公开。 必填项已用*标注