vczhan

vczhan 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

vczhan 收藏了文章 · 4月18日

TypeScript Interface vs Type知多少

接口和类型别名非常相似,在大多情况下二者可以互换。在写TS的时候,想必大家都问过自己这个问题,我到底应该用哪个呢?希望看完本文会给你一个答案。知道什么时候应该用哪个,首先应该了解二者之间的相同点和不同点,再做出选择。

接口 vs 类型别名 相同点

1. 都可以用来描述对象或函数

interface Point {
  x: number
  y: number
}

interface SetPoint {
  (x: number, y: number): void;
}
type Point = {
  x: number;
  y: number;
};

type SetPoint = (x: number, y: number) => void;

2. 都可以扩展

两者的扩展方式不同,但并不互斥。接口可以扩展类型别名,同理,类型别名也可以扩展接口。

接口的扩展就是继承,通过 extends 来实现。类型别名的扩展就是交叉类型,通过 & 来实现。

// 接口扩展接口
interface PointX {
    x: number
}

interface Point extends PointX {
    y: number
}
// 类型别名扩展类型别名
type PointX = {
    x: number
}

type Point = PointX & {
    y: number
}
// 接口扩展类型别名
type PointX = {
    x: number
}
interface Point extends PointX {
    y: number
}
// 类型别名扩展接口
interface PointX {
    x: number
}
type Point = PointX & {
    y: number
}

接口 vs 类型别名不同点

1. 类型别名更通用(接口只能声明对象,不能重命名基本类型)

类型别名的右边可以是任何类型,包括基本类型、元祖、类型表达式(&|等类型运算符);而在接口声明中,右边必须为结构。例如,下面的类型别名就不能转换成接口:

type A = number
type B = A | string

2. 扩展时表现不同

扩展接口时,TS将检查扩展的接口是否可以赋值给被扩展的接口。举例如下:

interface A {
    good(x: number): string,
    bad(x: number): string
}
interface B extends A {
    good(x: string | number) : string,
    bad(x: number): number // Interface 'B' incorrectly extends interface 'A'.
                           // Types of property 'bad' are incompatible.
                           // Type '(x: number) => number' is not assignable to type '(x: number) => string'.
                           // Type 'number' is not assignable to type 'string'.
}

但使用交集类型时则不会出现这种情况。我们将上述代码中的接口改写成类型别名,把 extends 换成交集运算符 &,TS将尽其所能把扩展和被扩展的类型组合在一起,而不会抛出编译时错误。

type A = {
    good(x: number): string,
    bad(x: number): string
}
type B = A & {
     good(x: string | number) : string,
     bad(x: number): number 
}

3. 多次定义时表现不同

接口可以定义多次,多次的声明会合并。但是类型别名如果定义多次,会报错。

interface Point {
    x: number
}
interface Point {
    y: number
}
const point: Point = {x:1} // Property 'y' is missing in type '{ x: number; }' but required in type 'Point'.

const point: Point = {x:1, y:1} // 正确
type Point = {
    x: number // Duplicate identifier 'A'.
}

type Point = {
    y: number // Duplicate identifier 'A'.
}

到底应该用哪个

如果接口和类型别名都能满足的情况下,到底应该用哪个是我们关心的问题。感觉哪个都可以,但是强烈建议大家只要能用接口实现的就优先使用接口,接口满足不了的再用类型别名。

为什么会这么建议呢?其实在TS的wiki中有说明。具体的文章地址在这里

以下是Preferring Interfaces Over Intersections的译文:

大多数时候,对于声明一个对象,类型别名和接口表现的很相似。

interface Foo { prop: string }

type Bar = { prop: string };

然而,当你需要通过组合两个或者两个以上的类型实现其他类型时,可以选择使用接口来扩展类型,也可以通过交叉类型(使用 & 创造出来的类型)来完成,这就是二者开始有区别的时候了。

  • 接口会创建一个单一扁平对象类型来检测属性冲突,当有属性冲突时会提示,而交叉类型只是递归的进行属性合并,在某种情况下可能产生 never 类型
  • 接口通常表现的更好,而交叉类型做为其他交叉类型的一部分时,直观上表现不出来,还是会认为是不同基本类型的组合
  • 接口之间的继承关系会缓存,而交叉类型会被看成组合起来的一个整体
  • 在检查一个目标交叉类型时,在检查到目标类型之前会先检查每一个组分

上述的几个区别从字面上理解还是有些绕,下面通过具体的列子来说明。

interface Point1 {
    x: number
}

interface Point extends Point1 {
    x: string // Interface 'Point' incorrectly extends interface 'Point1'.
              // Types of property 'x' are incompatible.
              // Type 'string' is not assignable to type 'number'.
}
type Point1 = {
    x: number
}

type Point2 = {
    x: string
}

type Point = Point1 & Point2 // 这时的Point是一个'number & string'类型,也就是never

从上述代码可以看出,接口继承同名属性不满足定义会报错,而相交类型就是简单的合并,最后产生了 number & string 类型,可以解释译文中的第一点不同,其实也就是我们在不同点模块中介绍的扩展时表现不同。

再来看下面例子:

interface PointX {
    x: number
}

interface PointY {
    y: number
}

interface PointZ {
    z: number
}

interface PointXY extends PointX, PointY {
}

interface Point extends PointXY, PointZ {
   
}
const point: Point = {x: 1, y: 1} // Property 'z' is missing in type '{ x: number; y: number; }' but required in type 'Point'
type PointX = {
    x: number
}

type PointY = {
    y: number
}

type PointZ = {
    z: number
}

type PointXY = PointX & PointY

type Point = PointXY & PointZ

const point: Point = {x: 1, y: 1} // Type '{ x: number; y: number; }' is not assignable to type 'Point'.
                                  // Property 'z' is missing in type '{ x: number; y: number; }' but required in type 'Point3'.

从报错中可以看出,当使用接口时,报错会准确定位到Point。
但是使用交叉类型时,虽然我们的 Point 交叉类型是 PointXY & PointZ, 但是在报错的时候定位并不在 Point 中,而是在 Point3 中,即使我们的 Point 类型并没有直接引用 Point3 类型。

如果我们把鼠标放在交叉类型 Point 类型上,提示的也是 type Point = PointX & PointY & PointZ,而不是 PointXY & PointZ

这个例子可以同时解释译文中第二个和最后一个不同点。

结论

有的同学可能会问,如果我不需要组合只是单纯的定义类型的时候,是不是就可以随便用了。但是为了代码的可扩展性,建议还是优先使用接口。现在不需要,谁能知道后续需不需要呢?所以,让我们大胆的使用接口吧~

查看原文

vczhan 收藏了文章 · 3月24日

有意思!强大的 SVG 滤镜

想写一篇关于 SVG 滤镜的文章已久,SVG 滤镜的存在,让本来就非常强大的 CSS 如虎添翼。让仅仅使用 CSS/HTML/SVG 创作的效果更上一层楼。题图为袁川老师使用 SVG 滤镜实现的云彩效果 -- CodePen Demo -- Cloud (SVG filter + CSS)

什么是 SVG 滤镜

SVG 滤镜与 CSS 滤镜类似,是 SVG 中用于创建复杂效果的一种机制。很多人看到 SVG 滤镜复杂的语法容易心生退意。本文力图使用最简洁明了的方式让大家尽量弄懂 SVG 滤镜的使用方式。

本文默认读者已经掌握了一定 SVG 的基本概念和用法。

SVG 滤镜的种类

SVG 滤镜包括了:

feBlend
feColorMatrix
feComponentTransfer
feComposite
feConvolveMatrix
feDiffuseLighting
feDisplacementMap
feFlood
feGaussianBlur
feImage
feMerge
feMorphology
feOffset
feSpecularLighting
feTile
feTurbulence
feDistantLight
fePointLight
feSpotLight

看着内容很多,有点类似于 CSS 滤镜中的不同功能:blur()contrast()drop-shadow()

SVG 滤镜的语法

我们需要使用 <defs><filter> 标签来定义一个 SVG 滤镜。

通常所有的 SVG 滤镜元素都需要定义在 <defs> 标记内。

现在,基本上现代浏览器,即使不使用 <defs> 包裹 <filter>,也能够定义一个 SVG 滤镜。

这个 <defs> 标记是 definitions 这个单词的缩写,可以包含很多种其它标签,包括各种滤镜。

其次,使用 <filter> 标记用来定义 SVG 滤镜。 <filter> 标签需要一个 id 属性,它是这个滤镜的标志。SVG 图形使用这个 id 来引用滤镜。

看一个简单的 DEMO:

<div class="cssFilter"></div>
<div class="svgFilter"></div>

<svg>
    <defs>
        <filter id="blur">
            <feGaussianBlur in="SourceGraphic" stdDeviation="5" />
        </filter>
    </defs>
</svg>
div {
    width: 100px;
    height: 100px;
    background: #000;
}
.cssblur {
    filter: blur(5px);
}
.svgFilter{
    filter: url(#blur);
}

这里,我们在 defsfilter 标签内,运用了 SVG 的 feGaussianBlur 滤镜,也就是模糊滤镜, 该滤镜有两个属性 instdDeviation。其中 in="SourceGraphic" 属性指明了模糊效果要应用于整个图片,stdDeviation 属性定义了模糊的程度。最后,在 CSS 中,使用了 filter: url(#blur) 去调用 HTML 中定义的 id 为 blur 的滤镜。

为了方便理解,也使用 CSS 滤镜 filter: blur(5px) 实现了一个类似的滤镜,方便比较,结果图如下:

image

CodePen Demo - SVG 滤镜

嘿,可以看到,使用 SVG 的模糊滤镜,实现了一个和 CSS 模糊滤镜一样的效果。

CSS filter 的 url 模式

上文的例子中使用了 filter: url(#blur) 这种模式引入了一个 SVG 滤镜效果,url 是 CSS 滤镜属性的关键字之一,url 模式是 CSS 滤镜提供的能力之一,允许我们引入特定的 SVG 过滤器,这极大的增强 CSS 中滤镜的能力。

相当于所有通过 SVG 实现的滤镜效果,都可以快速的通过 CSS 滤镜 URL 模式一键引入。

多个滤镜搭配工作

和 CSS 滤镜一样,SVG 滤镜也是支持多个滤镜搭配混合使用的。

所以我们经常能看到一个 <filter> 标签内有大量的代码。很容易就懵了~

再来看个简单的例子:

<div></div>

<svg>
    <defs>
        <!-- Filter declaration -->
        <filter id="MyFilter">

            <!-- offsetBlur -->
            <feGaussianBlur in="SourceAlpha" stdDeviation="5" result="blur" />
            <feOffset in="blur" dx="10" dy="10" result="offsetBlur" />

            <!-- merge SourceGraphic + offsetBlur -->
            <feMerge>
                <feMergeNode in="offsetBlur" />
                <feMergeNode in="SourceGraphic" />
            </feMerge>
        </filter>
    </defs>
</svg>
div {
    width: 200px;
    height: 200px;
    background: url(xxx);
    filter: url(#MyFilter);
}

我们先来看看整个滤镜的最终结果,结果长这样:

image

CSS 可能一行代码就能实现的事情,SVG 居然用了这么多代码。(当然,这里 CSS 也不好实现,不是简单容器的阴影,而是 PNG 图片图形的轮廓阴影)

分解步骤

首先看这一段:

<!-- offsetBlur -->
<feGaussianBlur in="SourceAlpha" stdDeviation="5" result="blur" />
<feOffset in="blur" dx="10" dy="10" result="offsetBlur" />

首先 <feGaussianBlur in="SourceAlpha" stdDeviation="5" result="blur" /> 这一段,我们上面也讲到了,会生成一个模糊效果,这里多了一个新的属性 result='blur',这个就是 SVG 的一个特性,不同滤镜作用的效果可以通过 result 产出一个中间结果(也称为 primitives 图元),其他滤镜可以使用 in 属性导入不同滤镜产出的 result,继续操作。

紧接着,<feOffset> 滤镜还是很好理解的,使用 in 拿到了上一步的结果 result = 'blur',然后做了一个简单的位移。

这里就有一个非常重要的知识点:在不同滤镜中利用 resultin 属性,可以实现在前一个基本变换操作上建立另一个操作,比如我们的例子中就是添加模糊后又添加位移效果。

结合两个滤镜,产生的图形效果,其实是这样的:

image

实际效果中还出现了原图,所以这里我们还使用了 <feMerge> 标签,合并了多个效果。也就是上述这段代码:

<!-- merge SourceGraphic + offsetBlur -->
<feMerge>
    <feMergeNode in="offsetBlur" />
    <feMergeNode in="SourceGraphic" />
</feMerge>

feMerge 滤镜允许同时应用滤镜效果而不是按顺序应用滤镜效果。利用 result 存储别的滤镜的输出可以实现这一点,然后在一个 <feMergeNode> 子元素中访问它。

  • <feMergeNode in="offsetBlur" /> 表示了上述两个滤镜的最终输出结果 offsetBlur ,也就是阴影的部分
  • <feMergeNode in="SourceGraphic" /> 中的 in="SourceGraphic" 关键词表示图形元素自身将作为 <filter> 原语的原始输入

整体再遵循后输入的层级越高的原则,最终得到上述结果。示意流程图如下:

image

至此,基本就掌握了 SVG 滤镜的工作原理,及多个滤镜如何搭配使用。接下来,只需要搞懂不同的滤镜能产生什么样的效果,有什么不同的属性,就能大致对 SVG 滤镜有个基本的掌握!

关于 SVG 滤镜还需要知道的

上面大致过了一下 SVG 滤镜的使用流程,过程中提到了一些属性,可能也漏掉了一些属性的讲解,本章节将补充说明一下。

滤镜标签通用属性

有一些属性是每一个滤镜标签都有,都可以进行设置的。

属性作用
x, y提供左上角的坐标来定义在哪里渲染滤镜效果。 (默认值:0)
width, height绘制滤镜容器框的高宽(默认都为 100%)
result用于定义一个滤镜效果的输出名字,以便将其用作另一个滤镜效果的输入(in)
in指定滤镜效果的输入源,可以是某个滤镜导出的 result,也可以是下面 6 个值

in 属性的 6 个取值

SVG filter 中的 in 属性,指定滤镜效果的输入源,可以是某个滤镜导出的 result,也可以是下面 6 个值:

in 取值作用
SourceGraphic该关键词表示图形元素自身将作为 <filter> 原语的原始输入
SourceAlpha该关键词表示图形元素自身将作为 <filter> 原语的原始输入。SourceAlphaSourceGraphic 具有相同的规则除了 SourceAlpha 只使用元素的非透明部分
BackgroundImage与 SourceGraphic 类似,但可在背景上使用。 需要显式设置
BackgroundAlpha与 SourceAlpha 类似,但可在背景上使用。 需要显式设置
FillPaint将其放置在无限平面上一样使用填充油漆
StrokePaint将其放在无限平面上一样使用描边绘画
后 4 个基本用不上~

更多 SVG 滤镜介绍讲解

上面已经提到了几个滤镜,我们简单回顾下:

  • <feGaussianBlur > - 模糊滤镜
  • <feOffset > - 位移滤镜
  • <feMerge> - 多滤镜叠加滤镜

接下来再介绍一些比较常见,有意思的 SVG 滤镜。

feBlend 滤镜

<feBlend> 为混合模式滤镜,与 CSS 中的混合模式相类似。

在 CSS 中,我们有混合模式 mix-blend-modebackground-blend-mode 。我有过非常多篇关于 CSS 混合模式相关的一些应用。如果你还不太了解 CSS 中的混合模式,可以先看看这几篇文章:

SVG 中的混合模式种类比 CSS 中的要少一些,只有 5 个,其作用与 CSS 混合模式完全一致:

  • normal — 正常
  • multiply — 正片叠底
  • screen — 滤色
  • darken — 变暗
  • lighten— 变亮

简单一个 Demo,我们有两张图,利用不同的混合模式,可以得到不一样的混合结果 :

<div></div>

<svg>
    <defs>
        <filter id="lighten" x="0" y="0" width="200" height="250">
            <feImage width="200" height="250" xlink:href="image1.jpg" result="img1" />
            <feImage width="200" height="250" xlink:href="image2.jpg" result="img2" />
            <feBlend mode="lighten" in="img1" in2="img2"/>
        </filter>
    </defs>
</svg>
.container {
    width: 200px;
    height: 250px;
    filter: url(#lighten);
}

这里还用到了一个 <feImage> 滤镜,它的作用是提供像素数据作为输出,如果外部来源是一个 SVG 图像,这个图像将被栅格化。

image

上述运用了 feBlend 滤镜中的 mode="lighten" 后的结果,两个图像叠加作用了 lighten 混合模式:

image

看看全部 5 中混合模式的效果:

image

CodePen Demo -- SVG Filter feBlend Demo

feColorMatrix

<feColorMatrix> 滤镜也是 SVG 滤镜中非常有意思的一个滤镜,顾名思义,它的名字中包含了矩阵这个单词,表示该滤镜基于转换矩阵对颜色进行变换。每一像素的颜色值(一个表示为[红,绿,蓝,透明度] 的矢量) 都经过矩阵乘法 (matrix multiplated) 计算出的新颜色。

这个滤镜稍微有点复杂,我们一步一步来看。

<feColorMatrix> 滤镜有 2 个私有属性 typevalues,type 它支持 4 种不同的类型:saturate | hueRotate | luminanceToAlpha | matrix,其中部分与 CSS Filter 中的一些滤镜效果类似。

type 类型作用values 的取值范围
saturate转换图像饱和度0.0 - 1.0
hueRotate转换图像色相0.0 - 360
luminanceToAlpha阿尔法通道亮度(不知道如何翻译 :sad)只有一个效果,无需改变 values 的值
matrix使用矩阵函数进行色彩变换需要应用一个 4 x 5 的矩阵

在这里,我做了一个简单的关于 <feColorMatrix> 前 3 个属性 saturate | hueRotate | luminanceToAlpha 的效果示意 DEMO -- CodePen - feColorMatrix Demo,可以感受下它们的具体的效果:

1gif

saturate、hueRotate 滤镜和 CSS 中的 filter 中的 saturate、hue-rotate 的作用是一模一样的。

feColorMatrix 中的 type=matrix

feColorMatrix 中的 type=matrix 理解起来要稍微更复杂点,它的 values 需要传入一个 4x5 的矩阵。

像是这样:

<filter id="colorMatrix">
  <feColorMatrix type="matrix" values="1 0 0 0 0, 0 1 0 0 0, 0 0 1 0 0, 0 0 0 1 0"/>
</filter>

要理解如何运用这些填写矩阵,就不得不直面另外一个问题 -- 图像的表示。

数字图像的本质是一个多维矩阵。在图像显示时,我们把图像的 R 分量放进红色通道里,B 分量放进蓝色通道里,G 分量放进绿色通道里。经过一系列处理,显示在屏幕上的就是我们所看到的彩色图像了。

而 feColorMatrix 中的 matrix 矩阵,就是用来表示不同通道的值每一个分量的值,最终通过计算得到我们熟知的 rgba() 值。

计算逻辑为:

/* R G B A 1 */ 
1 0 0 0 0 // R = 1*R + 0*G + 0*B + 0*A + 0 
0 1 0 0 0 // G = 0*R + 1*G + 0*B + 0*A + 0 
0 0 1 0 0 // B = 0*R + 0*G + 1*B + 0*A + 0 
0 0 0 1 0 // A = 0*R + 0*G + 0*B + 1*A + 0

中文的文章,对 feColorMatrix 的 matrix 讲解最好的应该就是大漠老师的这篇 -- 详解feColorMatrix,对具体的表示法感兴趣的可以看看。

仅仅是使用的话,这里还有一个可视化的 DEMO -- CodePen - feColorMatrix Demo,帮助大家理解记忆:

2


到目前为止,大部分 SVG 滤镜的展示讲解都是 CSS 现有能力能够实现的,那 SVG 滤镜的独特与魅力到底在哪呢?有什么是 CSS 能力无法做到的么?下面来看看另外几个有意思的 SVG 滤镜。

feSpecularLighting/feDiffuseLighting 光照滤镜

feSpecularLighting 与 feDiffuseLighting 都意为光照滤镜,使用它们可以照亮一个源图形,不同的是,feSpecularLighting 为镜面照明,而 feDiffuseLighting 为散射光照明。

  • feDiffuseLighting:来自外部光源,适合模拟太阳光或者灯光照明
  • feSpecularLighting:指定从反射面反射的二次光

简单看其中一个 Demo,代码看着有点多,但是一步一步也很好理解:

<div></div>
<div class="svg-filter"></div>
<svg>
    <defs>
        <filter id="filter">
            <!--Lighting effect-->
            <feSpecularLighting in="SourceGraphic" specularExponent="20" specularConstant="0.75" result="spec">
              <fePointLight x="0" y="0" z="200" />
            </feSpecularLighting>
            <!--Composition of inputs-->
            <feComposite in="SourceGraphic" in2="spec" operator="arithmetic" k1="0" k2="1" k3="1" k4="0" />
        </filter>
    </defs>
</svg>
div {
    background: url(avator.png);
}
.svg-filter {
    filter: url(#filter);
}

左边是原图,右边是应用了光照滤镜之后的效果。

image

CodePen - feSpotLight SVG Light Source

feMorphology 滤镜

feMorphology 为形态滤镜,它的输入源通常是图形的 alpha 通道,用来它的两个操作可以使源图形腐蚀(变薄)或扩张(加粗)。

使用属性 operator 确定是要腐蚀效果还是扩张效果。使用属性 radius 表示效果的程度,可以理解为笔触的大小。

  • operator:erode 腐蚀模式,dilate 为扩张模式,默认为 erode
  • radius:笔触的大小,接受一个数字,表示该模式下的效果程度,默认为 0

我们将这个滤镜简单的应用到文字上看看效果:

<div class="g-text">
    <p>Normal Text</p>
    <p class="dilate">Normal Text</p>
    <p class="erode">Normal Text</p>
</div>

<svg width="0" height="0">
    <filter id="dilate">
        <feMorphology in="SourceAlpha" result="DILATED" operator="dilate" radius="3"></feMorphology>
    </filter>
    <filter id="erode">
        <feMorphology in="SourceAlpha" result="ERODE" operator="erode" radius="1"></feMorphology>
    </filter>
</svg>
p {
    font-size: 64px;
}
.dilate {
    filter: url(#dilate);
}
.erode {
    filter: url(#erode);
}

效果如下:最左边的是正常文字,中间的是扩张的模式,右边的是腐蚀模式,看看效果,非常好理解:

image

当然,我们还可以将其运用在图片之上,这时,并非是简单的让图像的笔触变粗或者变细,

  • 对于 erode 模式,会将图片的每一个像素向更暗更透明的方向变化,
  • dilate 模式,则是将每个向像素周围附近更亮更不透明的方向变化

简单看个示例动画 DEMO,我们有两张图,分别作用 operator="erode"operator="dilate",并且动态的去改变它们的 radius,其中一个的代码示意如下:

<svg width="450" height="300" viewBox="0 0 450 300">
    <filter id="morphology">
        <feMorphology operator="erode" radius="0">
            <animate attributeName="radius" from="0" to="5" dur="5s" repeatCount="indefinite" />
        </feMorphology>
    </filter>

    <image xlink:href="image.jpg" width="90%" height="90%" x="10" y="10" filter="url(#morphology)"></image>
</svg>

3

上图左边是扩张模式,右边是腐蚀模式:

CodePen Demo -- SVG feMorphology Animation

feTurbulence 滤镜

turbulence 意为湍流,不稳定气流,而 SVG <feTurbulence> 滤镜能够实现半透明的烟熏或波状图像。 通常用于实现一些特殊的纹理。滤镜利用 Perlin 噪声函数创建了一个图像。噪声在模拟云雾效果时非常有用,能产生非常复杂的质感,利用它可以实现了人造纹理比如说云纹、大理石纹的合成。

有了 feTurbulence,我们可以自使用 SVG 创建纹理图形作为置换图,而不需要借助外部图形的纹理效果,即可创建复杂的图形效果。

这个滤镜,我个人认为是 SVG 滤镜中最有意思的一个,因为它允许我们自己去创造出一些纹理,并且叠加在其他效果之上,生成出非常有意思的动效。

feTurbulence 有三个属性是我们特别需要注意的:typebaseFrequencynumOctaves

  • type:实现的滤镜的类型,可选fractalNoise 分形噪声,或者是 turbulence 湍流噪声。

    • fractalNoise:分形噪声更加的平滑,它产生的噪声质感更接近云雾
    • turbulence:湍流噪声
  • baseFrequency: 表示噪声函数的基本频率的参数,频率越小,产生的图形越大,频率越大,产生的噪声越复杂其图形也越小越精细,通常的取值范围在 0.02 ~ 0.2
  • numOctaves:表示噪声函数的精细度,数值越高,产生的噪声更详细。 默认值为1

这里有一个非常好的网站,用于示意 feTurbulence 所产生的两种噪声的效果:http://apike.ca/ - feTurbulence

两种噪声的代码基本一致,只是 type 类型不同:

<filter id="fractal" >
  <feTurbulence id="fe-turb-fractal" type="fractalNoise" baseFrequency="0.00025" numOctaves="1"/>
</filter>
<filter id="turbu">
  <feTurbulence id="fe-turb-turbulence" type="fractalNoise" baseFrequency="0.00025" numOctaves="1"/>
</filter>

我们通过改变 baseFrequencynumOctaves 参数看看实际产生的两种噪声的效果:

同时,baseFrequency 允许我们传入两个值,我们可以只改变某一方向上的频率,具体的你可以戳这个 Demo 看看:CodePen -- feTurbulence baseFrequency & numOctaves

单单一个 <feTurbulence> 滤镜其实是比较难搞懂这滤镜想干什么的,需要将这个滤镜作为纹理或者输入,和其他滤镜一起搭配使用,实现一些效果,下面我们来看看:

使用 feTurbulence 滤镜实现文字流动的效果

首先,尝试将 feTurbulence 所产生的纹理和文字相结合。

简单的代码如下:

<div>Coco</div>
<div class="turbulence">Coco</div>

<svg>
    <filter id="fractal" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">
        <feTurbulence id="turbulence" type="fractalNoise" baseFrequency="0.03" numOctaves="1" />
        <feDisplacementMap in="SourceGraphic" scale="50"></feDisplacementMap>
    </filter>
</svg>
.turbulence {
    filter: url(#fractal);
}

左边是正常的效果,后边是应用了 <feTurbulence> 的效果,你可以试着点进 Demo,更改 baseFrequencynumOctaves 参数的大小,可以看到不同的效果:

image

CodePen Demo -- feTurbulence text demo

feDisplacementMap 映射置换滤镜

上面的 Demo 还用到了 feDisplacementMap 滤镜,也需要简单的讲解下。

feDisplacementMap 为映射置换滤镜,想要用好这个滤镜不太容易,需要掌握非常多的关于 PhotoShop 纹理创建或者是图形色彩相关的知识。该滤镜用来自图像中从 in2 的输入值到空间的像素值置换图像从 in 输入值到空间的像素值。

说人话就是 feDisplacementMap 实际上是用于改变元素和图形的像素位置的。该滤镜通过遍历原图形的所有像素点,使用 feDisplacementMap 重新映射到一个新的位置,形成一个新的图形。

在上述的 feTurbulence 滤镜与文字的结合使用中,我们通过 feTurbulence 噪声得到了噪声图形,然后通过 feDisplacementMap 滤镜根据 feTurbulence 所产生的噪声图形进行形变,扭曲,液化,得到最终的效果。

MDN 上有这个滤镜转化的一个公式(感兴趣的可以研究下,我啃不动了):

P'(x,y) ← P( x + scale * (XC(x,y) - 0.5), y + scale * (YC(x,y) - 0.5))

使用 feTurbulence 滤镜实现褶皱纸张的纹理

好,我们继续 feTurbulence ,使用这个滤镜,我们可以生成各种不同的纹理,我们可以尝试使用 feTurbulence 滤镜搭配光照滤镜实现褶皱的纸张纹理效果,代码也非常少:

<div></div>
<svg>
    <filter id='roughpaper'>
        <feTurbulence type="fractalNoise" baseFrequency='0.04' result='noise' numOctaves="5" />

        <feDiffuseLighting in='noise' lighting-color='#fff' surfaceScale='2'>
            <feDistantLight azimuth='45' elevation='60' />
        </feDiffuseLighting>
    </filter>
</svg>
div {
    width: 650px;
    height: 500px;
    filter: url(#roughpaper);
}

效果如下:

image

CodePen Demo -- Rough Paper Texture with SVG Filters

你可以在 Sara Soueidan 的一次关于 SVG Filter 的分享上,找到制作它的教程:Youtube -- SVG Filters Crash Course

使用 feTurbulence 滤镜实现按钮hover效果

使用 feTurbulence 滤镜搭配 feDisplacementMap 滤镜,还可以制作一些非常有意思的按钮效果。

尝试实现一些故障风格的按钮,其中一个按钮的代码如下:

<div class="fe1">Button</div>
<div class="fe2">Button</div>

<svg>
    <defs>
        <filter id="fe1">
            <feTurbulence id="animation" type="fractalNoise" baseFrequency="0.00001 9.9999999" numOctaves="1" result="warp">
                <animate attributeName="baseFrequency" from="0.00001 9.9999" to="0.00001 0.001" dur="2s" repeatCount="indefinite"/>
            </feTurbulence>
            <feOffset dx="-90" dy="-90" result="warpOffset"></feOffset>
            <feDisplacementMap xChannelSelector="R" yChannelSelector="G" scale="30" in="SourceGraphic" in2="warpOffset"></feDisplacementMap>
        </filter>
    </defs>
</svg>
.fe1 {
    width: 200px;
    height: 64px;
    outline: 200px solid transparent;
}

.fe1:hover {
    filter: url(#fe1);
}

通过 hover 按钮的时候,给按钮添加滤镜效果,并且滤镜本身带有一个无限循环的动画:

完整的代码你可以戳这里:CodePen Demo - SVG Filter Button Effects

使用 feTurbulence 滤镜实现云彩效果

最后,我们回到题图上的云彩效果,使用 feTurbulence 滤镜,我们可以非常逼真的使用 SVG 模拟出真实的云彩效果。

首先,通过随机生成的多重 box-shadow,实现这一一个图形:

<div></div>
div {
    width: 1px;
    height: 1px;
    box-shadow: rgb(240 255 243) 80vw 11vh 34vmin 16vmin, rgb(17 203 215) 33vw 71vh 23vmin 1vmin, rgb(250 70 89) 4vw 85vh 21vmin 9vmin, rgb(198 241 231) 8vw 4vh 22vmin 12vmin, rgb(198 241 231) 89vw 11vh 31vmin 19vmin, rgb(240 255 243) 5vw 22vh 38vmin 19vmin, rgb(250 70 89) 97vw 35vh 33vmin 16vmin, rgb(250 70 89) 51vw 8vh 35vmin 14vmin, rgb(17 203 215) 75vw 57vh 40vmin 4vmin, rgb(250 70 89) 28vw 18vh 31vmin 11vmin, rgb(250 70 89) 8vw 89vh 31vmin 2vmin, rgb(17 203 215) 13vw 8vh 26vmin 19vmin, rgb(240 255 243) 98vw 12vh 35vmin 5vmin, rgb(17 203 215) 35vw 29vh 27vmin 18vmin, rgb(17 203 215) 67vw 58vh 22vmin 15vmin, rgb(198 241 231) 67vw 24vh 25vmin 7vmin, rgb(17 203 215) 76vw 52vh 22vmin 7vmin, rgb(250 70 89) 46vw 86vh 26vmin 20vmin, rgb(240 255 243) 50vw 20vh 25vmin 1vmin, rgb(250 70 89) 74vw 14vh 25vmin 16vmin, rgb(240 255 243) 31vw 100vh 29vmin 20vmin
}

这个工作,你可以交给 SASS、LESS 或者 JavaScript 这些能够有循环函数能力的语言去生成,它的效果大概是这样:

image

紧接着,通过 feTurbulence 产生分形噪声图形,使用 feDisplacementMap 进行映射置换,最后给图形叠加上这个滤镜效果。

<svg width="0">
  <filter id="filter">
    <feTurbulence type="fractalNoise" baseFrequency=".01" numOctaves="10" />
    <feDisplacementMap in="SourceGraphic" scale="240" />
  </filter>
</svg>
div {
    filter: url(#filter);
}

即可得到这样的云彩效果:

image

完整的代码,你可以戳这里到袁川老师的 CodePen 观看:Cloud (SVG filter + CSS)

总结一下

关于 SVG 滤镜入门的第一篇总算差不多了,本文简单的介绍了一下 SVG 滤镜的使用方式以及一些常见的 SVG 滤镜并给出了最简单的一些使用效果,希望大家看完能对 SVG 滤镜有一个简单的认识。

本文罗列的滤镜效果更多的是单个效果或者很少几个组合在一起的效果,实际的使用或者应用到应用场景下其实会是更多滤镜的的组合产生出的一个效果。

后面的文章将会更加细致的去探讨分析多个 SVG 滤镜组合效果,探讨更复杂的排列组合。

文章的题目叫SVG 滤镜从入门到放弃因为 SVG 滤镜学起来确实太繁琐太累了,它不像 CSS 滤镜或者混合模式那么容易上手那么简单。当然也由于 SVG 滤镜的功能非常强大,定制化能力强以及它已经存在了非常之久有关。SVG 滤镜的兼容性也很好,它们其实是早于 CSS3 一些特殊效果之前就已经存在的。

CSS 其实一直在向 SVG 的一些特殊能力靠拢,用更简单的语法让人更易上手,不过 SVG 滤镜还是有其独特的魅力所在。后续将会有更多关于 SVG 滤镜的文章。也希望读到这里的同学不要放弃!

参考资料

最后

好了,本文到此结束,希望对你有帮助 :)

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

查看原文

vczhan 关注了用户 · 3月24日

chokcoco @chokcoco

坎坷切图仔

关注 290

vczhan 赞了文章 · 3月2日

手写一个基于 Proxy 的缓存库

两年前,我写了一篇关于业务缓存的博客 前端 api 请求缓存方案, 这篇博客反响还不错,其中介绍了如何缓存数据,Promise 以及如何超时删除(也包括如何构建修饰器)。如果对此不够了解,可以阅读博客进行学习。

但之前的代码和方案终归还是简单了些,而且对业务有很大的侵入性。这样不好,于是笔者开始重新学习与思考代理器 Proxy。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。关于 Proxy 的介绍与使用,建议大家还是看阮一峰大神的 ECMAScript 6 入门 代理篇

项目演进

任何项目都不是一触而就的,下面是关于 Proxy 缓存库的编写思路。希望能对大家有一些帮助。

proxy handler 添加缓存

当然,其实代理器中的 handler 参数也是一个对象,那么既然是对象,当然可以添加数据项,如此,我们便可以基于 Map 缓存编写 memoize 函数用来提升算法递归性能。

type TargetFun<V> = (...args: any[]) => V

function memoize<V>(fn: TargetFun<V>) {
  return new Proxy(fn, {
    // 此处目前只能略过 或者 添加一个中间层集成 Proxy 和 对象。
    // 在对象中添加 cache
    // @ts-ignore
    cache: new Map<string, V>(),
    apply(target, thisArg, argsList) {
      // 获取当前的 cache
      const currentCache = (this as any).cache
      
      // 根据数据参数直接生成 Map 的 key
      let cacheKey = argsList.toString();
      
      // 当前没有被缓存,执行调用,添加缓存
      if (!currentCache.has(cacheKey)) {
        currentCache.set(cacheKey, target.apply(thisArg, argsList));
      }
      
      // 返回被缓存的数据
      return currentCache.get(cacheKey);
    }
  });
}
  

我们可以尝试 memoize fibonacci 函数,经过了代理器的函数有非常大的性能提升(肉眼可见):

const fibonacci = (n: number): number => (n <= 1 ? 1 : fibonacci(n - 1) + fibonacci(n - 2));
const memoizedFibonacci = memoize<number>(fibonacci);

for (let i = 0; i < 100; i++) fibonacci(30); // ~5000ms
for (let i = 0; i < 100; i++) memoizedFibonacci(30); // ~50ms

自定义函数参数

我们仍旧可以利用之前博客介绍的的函数生成唯一值,只不过我们不再需要函数名了:

const generateKeyError = new Error("Can't generate key from function argument")

// 基于函数参数生成唯一值
export default function generateKey(argument: any[]): string {
  try{
    return `${Array.from(argument).join(',')}`
  }catch(_) {
    throw generateKeyError
  }
}

虽然库本身可以基于函数参数提供唯一值,但是针对形形色色的不同业务来说,这肯定是不够用的,需要提供用户可以自定义参数序列化。

// 如果配置中有 normalizer 函数,直接使用,否则使用默认函数
const normalizer = options?.normalizer ?? generateKey

return new Proxy<any>(fn, {
  // @ts-ignore
  cache,
  apply(target, thisArg, argsList: any[]) {
    const cache: Map<string, any> = (this as any).cache
    
    // 根据格式化函数生成唯一数值
    const cacheKey: string = normalizer(argsList);
    
    if (!cache.has(cacheKey))
      cache.set(cacheKey, target.apply(thisArg, argsList));
    return cache.get(cacheKey);
  }
});

添加 Promise 缓存

在之前的博客中,提到缓存数据的弊端。同一时刻多次调用,会因为请求未返回而进行多次请求。所以我们也需要添加关于 Promise 的缓存。

if (!currentCache.has(cacheKey)){
  let result = target.apply(thisArg, argsList)
  
  // 如果是 promise 则缓存 promise,简单判断! 
  // 如果当前函数有 then 则是 Promise
  if (result?.then) {
    result = Promise.resolve(result).catch(error => {
      // 发生错误,删除当前 promise,否则会引发二次错误
      // 由于异步,所以当前 delete 调用一定在 set 之后,
      currentCache.delete(cacheKey)
    
      // 把错误衍生出去
      return Promise.reject(error)
    })
  }
  currentCache.set(cacheKey, result);
}
return currentCache.get(cacheKey);

此时,我们不但可以缓存数据,还可以缓存 Promise 数据请求。

添加过期删除功能

我们可以在数据中添加当前缓存时的时间戳,在生成数据时候添加。

// 缓存项
export default class ExpiredCacheItem<V> {
  data: V;
  cacheTime: number;

  constructor(data: V) {
    this.data = data
    // 添加系统时间戳
    this.cacheTime = (new Date()).getTime()
  }
}

// 编辑 Map 缓存中间层,判断是否过期
isOverTime(name: string) {
  const data = this.cacheMap.get(name)

  // 没有数据(因为当前保存的数据是 ExpiredCacheItem),所以我们统一看成功超时
  if (!data) return true

  // 获取系统当前时间戳
  const currentTime = (new Date()).getTime()

  // 获取当前时间与存储时间的过去的秒数
  const overTime = currentTime - data.cacheTime

  // 如果过去的秒数大于当前的超时时间,也返回 null 让其去服务端取数据
  if (Math.abs(overTime) > this.timeout) {
    // 此代码可以没有,不会出现问题,但是如果有此代码,再次进入该方法就可以减少判断。
    this.cacheMap.delete(name)
    return true
  }

  // 不超时
  return false
}

// cache 函数有数据
has(name: string) {
  // 直接判断在 cache 中是否超时
  return !this.isOverTime(name)
}

到达这一步,我们可以做到之前博客所描述的所有功能。不过,如果到这里就结束的话,太不过瘾了。我们继续学习其他库的功能来优化我的功能库。

添加手动管理

通常来说,这些缓存库都会有手动管理的功能,所以这里我也提供了手动管理缓存以便业务管理。这里我们使用 Proxy get 方法来拦截属性读取。

 return new Proxy(fn, {
  // @ts-ignore
  cache,
  get: (target: TargetFun<V>, property: string) => {
    
    // 如果配置了手动管理
    if (options?.manual) {
      const manualTarget = getManualActionObjFormCache<V>(cache)
      
      // 如果当前调用的函数在当前对象中,直接调用,没有的话访问原对象
      // 即使当前函数有该属性或者方法也不考虑,谁让你配置了手动管理呢。
      if (property in manualTarget) {
        return manualTarget[property]
      }
    }
   
    // 当前没有配置手动管理,直接访问原对象
    return target[property]
  },
}


export default function getManualActionObjFormCache<V>(
  cache: MemoizeCache<V>
): CacheMap<string | object, V> {
  const manualTarget = Object.create(null)
  
  // 通过闭包添加 set get delete clear 等 cache 操作
  manualTarget.set = (key: string | object, val: V) => cache.set(key, val)
  manualTarget.get = (key: string | object) => cache.get(key)
  manualTarget.delete = (key: string | object) => cache.delete(key)
  manualTarget.clear = () => cache.clear!()
  
  return manualTarget
}

当前情况并不复杂,我们可以直接调用,复杂的情况下还是建议使用 Reflect

添加 WeakMap

我们在使用 cache 时候,我们同时也可以提供 WeakMap ( WeakMap 没有 clear 和 size 方法),这里我提取了 BaseCache 基类。

export default class BaseCache<V> {
  readonly weak: boolean;
  cacheMap: MemoizeCache<V>

  constructor(weak: boolean = false) {
    // 是否使用 weakMap
    this.weak = weak
    this.cacheMap = this.getMapOrWeakMapByOption()
  }

  // 根据配置获取 Map 或者 WeakMap
  getMapOrWeakMapByOption<T>(): Map<string, T> | WeakMap<object, T>  {
    return this.weak ? new WeakMap<object, T>() : new Map<string, T>()
  }
}

之后,我添加各种类型的缓存类都以此为基类。

添加清理函数

在缓存进行删除时候需要对值进行清理,需要用户提供 dispose 函数。该类继承 BaseCache 同时提供 dispose 调用。

export const defaultDispose: DisposeFun<any> = () => void 0

export default class BaseCacheWithDispose<V, WrapperV> extends BaseCache<WrapperV> {
  readonly weak: boolean
  readonly dispose: DisposeFun<V>

  constructor(weak: boolean = false, dispose: DisposeFun<V> = defaultDispose) {
    super(weak)
    this.weak = weak
    this.dispose = dispose
  }

  // 清理单个值(调用 delete 前调用)
  disposeValue(value: V | undefined): void {
    if (value) {
      this.dispose(value)
    }
  }

  // 清理所有值(调用 clear 方法前调用,如果当前 Map 具有迭代器)
  disposeAllValue<V>(cacheMap: MemoizeCache<V>): void {
    for (let mapValue of (cacheMap as any)) {
      this.disposeValue(mapValue?.[1])
    }
  }
}

当前的缓存如果是 WeakMap,是没有 clear 方法和迭代器的。个人想要添加中间层来完成这一切(还在考虑,目前没有做)。如果 WeakMap 调用 clear 方法时,我是直接提供新的 WeakMap 。

clear() {
  if (this.weak) {
    this.cacheMap = this.getMapOrWeakMapByOption()
  } else {
    this.disposeAllValue(this.cacheMap)
    this.cacheMap.clear!()
  }
}

添加计数引用

在学习其他库 memoizee 的过程中,我看到了如下用法:

memoized = memoize(fn, { refCounter: true });

memoized("foo", 3); // refs: 1
memoized("foo", 3); // Cache hit, refs: 2
memoized("foo", 3); // Cache hit, refs: 3
memoized.deleteRef("foo", 3); // refs: 2
memoized.deleteRef("foo", 3); // refs: 1
memoized.deleteRef("foo", 3); // refs: 0,清除 foo 的缓存
memoized("foo", 3); // Re-executed, refs: 1

于是我有样学样,也添加了 RefCache。

export default class RefCache<V> extends BaseCacheWithDispose<V, V> implements CacheMap<string | object, V> {
    // 添加 ref 计数
  cacheRef: MemoizeCache<number>

  constructor(weak: boolean = false, dispose: DisposeFun<V> = () => void 0) {
    super(weak, dispose)
    // 根据配置生成 WeakMap 或者 Map
    this.cacheRef = this.getMapOrWeakMapByOption<number>()
  }
  

  // get has clear 等相同。不列出
  
  delete(key: string | object): boolean {
    this.disposeValue(this.get(key))
    this.cacheRef.delete(key)
    this.cacheMap.delete(key)
    return true;
  }


  set(key: string | object, value: V): this {
    this.cacheMap.set(key, value)
    // set 的同时添加 ref
    this.addRef(key)
    return this
  }

  // 也可以手动添加计数
  addRef(key: string | object) {
    if (!this.cacheMap.has(key)) {
      return
    }
    const refCount: number | undefined = this.cacheRef.get(key)
    this.cacheRef.set(key, (refCount ?? 0) + 1)
  }

  getRefCount(key: string | object) {
    return this.cacheRef.get(key) ?? 0
  }

  deleteRef(key: string | object): boolean {
    if (!this.cacheMap.has(key)) {
      return false
    }

    const refCount: number = this.getRefCount(key)

    if (refCount <= 0) {
      return false
    }

    const currentRefCount = refCount - 1
    
    // 如果当前 refCount 大于 0, 设置,否则清除
    if (currentRefCount > 0) {
      this.cacheRef.set(key, currentRefCount)
    } else {
      this.cacheRef.delete(key)
      this.cacheMap.delete(key)
    }
    return true
  }
}

同时修改 proxy 主函数:

if (!currentCache.has(cacheKey)) {
  let result = target.apply(thisArg, argsList)

  if (result?.then) {
    result = Promise.resolve(result).catch(error => {
      currentCache.delete(cacheKey)
      return Promise.reject(error)
    })
  }
  currentCache.set(cacheKey, result);

  // 当前配置了 refCounter
} else if (options?.refCounter) {
  // 如果被再次调用且当前已经缓存过了,直接增加       
  currentCache.addRef?.(cacheKey)
}

添加 LRU

LRU 的英文全称是 Least Recently Used,也即最不经常使用。相比于其他的数据结构进行缓存,LRU 无疑更加有效。

这里考虑在添加 maxAge 的同时也添加 max 值 (这里我利用两个 Map 来做 LRU,虽然会增加一定的内存消耗,但是性能更好)。

如果当前的此时保存的数据项等于 max ,我们直接把当前 cacheMap 设为 oldCacheMap,并重新 new cacheMap。

set(key: string | object, value: V) {
  const itemCache = new ExpiredCacheItem<V>(value)
  // 如果之前有值,直接修改
  this.cacheMap.has(key) ? this.cacheMap.set(key, itemCache) : this._set(key, itemCache);
  return this
}

private _set(key: string | object, value: ExpiredCacheItem<V>) {
  this.cacheMap.set(key, value);
  this.size++;

  if (this.size >= this.max) {
    this.size = 0;
    this.oldCacheMap = this.cacheMap;
    this.cacheMap = this.getMapOrWeakMapByOption()
  }
}

重点在与获取数据时候,如果当前的 cacheMap 中有值且没有过期,直接返回,如果没有,就去 oldCacheMap 查找,如果有,删除老数据并放入新数据(使用 _set 方法),如果都没有,返回 undefined.

get(key: string | object): V | undefined {
  // 如果 cacheMap 有,返回 value
  if (this.cacheMap.has(key)) {
    const item = this.cacheMap.get(key);
    return this.getItemValue(key, item!);
  }

  // 如果 oldCacheMap 里面有
  if (this.oldCacheMap.has(key)) {
    const item = this.oldCacheMap.get(key);
    // 没有过期
    if (!this.deleteIfExpired(key, item!)) {
      // 移动到新的数据中并删除老数据
      this.moveToRecent(key, item!);
      return item!.data as V;
    }
  }
  return undefined
}


private moveToRecent(key: string | object, item: ExpiredCacheItem<V>) {
  // 老数据删除
  this.oldCacheMap.delete(key);
  
  // 新数据设定,重点!!!!如果当前设定的数据等于 max,清空 oldCacheMap,如此,数据不会超过 max
  this._set(key, item);
}

private getItemValue(key: string | object, item: ExpiredCacheItem<V>): V | undefined {
  // 如果当前设定了 maxAge 就查询,否则直接返回
  return this.maxAge ? this.getOrDeleteIfExpired(key, item) : item?.data;
}
  
  
private getOrDeleteIfExpired(key: string | object, item: ExpiredCacheItem<V>): V | undefined {
  const deleted = this.deleteIfExpired(key, item);
  return !deleted ? item.data : undefined;
}
  
private deleteIfExpired(key: string | object, item: ExpiredCacheItem<V>) {
  if (this.isOverTime(item)) {
    return this.delete(key);
  }
  return false;
}  

整理 memoize 函数

事情到了这一步,我们就可以从之前的代码细节中解放出来了,看看基于这些功能所做出的接口与主函数。

// 面向接口,无论后面还会不会增加其他类型的缓存类
export interface BaseCacheMap<K, V> {
  delete(key: K): boolean;

  get(key: K): V | undefined;

  has(key: K): boolean;

  set(key: K, value: V): this;

  clear?(): void;

  addRef?(key: K): void;

  deleteRef?(key: K): boolean;
}

// 缓存配置
export interface MemoizeOptions<V> {
  /** 序列化参数 */
  normalizer?: (args: any[]) => string;
  /** 是否使用 WeakMap */
  weak?: boolean;
  /** 最大毫秒数,过时删除 */
  maxAge?: number;
  /** 最大项数,超过删除  */
  max?: number;
  /** 手动管理内存 */
  manual?: boolean;
  /** 是否使用引用计数  */
  refCounter?: boolean;
  /** 缓存删除数据时期的回调 */
  dispose?: DisposeFun<V>;
}

// 返回的函数(携带一系列方法)
export interface ResultFun<V> extends Function {
  delete?(key: string | object): boolean;

  get?(key: string | object): V | undefined;

  has?(key: string | object): boolean;

  set?(key: string | object, value: V): this;

  clear?(): void;

  deleteRef?(): void
}

最终的 memoize 函数其实和最开始的函数差不多,只做了 3 件事

  • 检查参数并抛出错误
  • 根据参数获取合适的缓存
  • 返回代理
export default function memoize<V>(fn: TargetFun<V>, options?: MemoizeOptions<V>): ResultFun<V> {
  // 检查参数并抛出错误
  checkOptionsThenThrowError<V>(options)

  // 修正序列化函数
  const normalizer = options?.normalizer ?? generateKey

  let cache: MemoizeCache<V> = getCacheByOptions<V>(options)

  // 返回代理
  return new Proxy(fn, {
    // @ts-ignore
    cache,
    get: (target: TargetFun<V>, property: string) => {
      // 添加手动管理
      if (options?.manual) {
        const manualTarget = getManualActionObjFormCache<V>(cache)
        if (property in manualTarget) {
          return manualTarget[property]
        }
      }
      return target[property]
    },
    apply(target, thisArg, argsList: any[]): V {

      const currentCache: MemoizeCache<V> = (this as any).cache

      const cacheKey: string | object = getKeyFromArguments(argsList, normalizer, options?.weak)

      if (!currentCache.has(cacheKey)) {
        let result = target.apply(thisArg, argsList)

      
        if (result?.then) {
          result = Promise.resolve(result).catch(error => {
            currentCache.delete(cacheKey)
            return Promise.reject(error)
          })
        }
        currentCache.set(cacheKey, result);
      } else if (options?.refCounter) {
        currentCache.addRef?.(cacheKey)
      }
      return currentCache.get(cacheKey) as V;
    }
  }) as any
}

完整代码在 memoizee-proxy 中。大家自行操作与把玩。

下一步

测试

测试覆盖率不代表一切,但是在实现库的过程中,JEST 测试库给我提供了大量的帮助,它帮助我重新思考每一个类以及每一个函数应该具有的功能与参数校验。之前的代码我总是在项目的主入口进行校验,对于每个类或者函数的参数没有深入思考。事实上,这个健壮性是不够的。因为你不能决定用户怎么使用你的库。

Proxy 深入

事实上,代理的应用场景是不可限量的。这一点,ruby 已经验证过了(可以去学习《ruby 元编程》)。

开发者使用它可以创建出各种编码模式,比如(但远远不限于)跟踪属性访问、隐藏属性、阻止修改或删除属性、函数参数验证、构造函数参数验证、数据绑定,以及可观察对象。

当然,Proxy 虽然来自于 ES6 ,但该 API 仍需要较高的浏览器版本,虽然有 proxy-pollfill ,但毕竟提供功能有限。不过已经 2021,相信深入学习 Proxy 也是时机了。

深入缓存

缓存是有害的!这一点毋庸置疑。但是它实在太快了!所以我们要更加理解业务,哪些数据需要缓存,理解那些数据可以使用缓存。

当前书写的缓存仅仅只是针对与一个方法,之后写的项目是否可以更细粒度的结合返回数据?还是更往上思考,写出一套缓存层?

小步开发

在开发该项目的过程中,我采用小步快跑的方式,不断返工。最开始的代码,也仅仅只到了添加过期删除功能那一步。

但是当我每次完成一个新的功能后,重新开始整理库的逻辑与流程,争取每一次的代码都足够优雅。同时因为我不具备第一次编写就能通盘考虑的能力。不过希望在今后的工作中,不断进步。这样也能减少代码的返工。

其他

函数创建

事实上,我在为当前库添加手动管理时候,考虑过直接复制函数,因为函数本身是一个对象。同时为当前函数添加 set 等方法。但是没有办法把作用域链拷贝过去。

虽然没能成功,但是也学到了一些知识,这里也提供两个创建函数的代码。

我们在创建函数时候基本上会利用 new Function 创建函数,但是浏览器没有提供可以直接创建异步函数的构造器,我们需要手动获取。

AsyncFunction = (async x => x).constructor

foo = new AsyncFunction('x, y, p', 'return x + y + await p')

foo(1,2, Promise.resolve(3)).then(console.log) // 6

对于全局函数,我们也可以直接 fn.toString() 来创建函数,这时候异步函数也可以直接构造的。

function cloneFunction<T>(fn: (...args: any[]) => T): (...args: any[]) => T {
  return new Function('return '+ fn.toString())();
}

鼓励一下

如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。

博客地址

参考资料

前端 api 请求缓存方案

ECMAScript 6 入门 代理篇

memoizee

memoizee-proxy

查看原文

赞 38 收藏 29 评论 2

vczhan 收藏了文章 · 2月26日

fiddler+willow使用和配合真机测试调试

本文最早发布于csdn

背景介绍:

Fiddler是一个http协议调试代理工具,它能够记录并检查所有你的电脑和互联网之间的http通讯,设置断点,查看所有的“进出”Fiddler的数据(指cookie,html,js,css等文件,这些都可以让你胡乱修改的意思)。 Fiddler 要比其他的网络调试器要更加简单,因为它不仅仅暴露http通讯还提供了一个用户友好的格式。Fiddler是用C#写出来的,它包含一个简单却功能强大的基于JScript.NET事件脚本子系统,它的灵活性非常棒,可以支持众多的http调试任务,并且能够使用.net框架语言进行扩展。

注:Fiddler因为设置代理的原因,在使用中可能会出现网络问题,直接关闭或者点击关掉左下角的capture就好了

Fiddler及Willow插件的安装

Fiddler有两个版本,下载地址 http://www.telerik.com/download/fiddler ,针对Fiddler2和Fiddler4,对应的Willow插件版本也是不一样的。本文使用和针对的主要是Fiddler4

官网介绍的主要功能如下:
fiddler官网

  • Web调试,调试PC、Mac或Linux系统和移动设备的通信。确保适当的cookies,headers和缓存指令是客户端和服务器之间传输。支持任何框架,包括.NET,Java,Ruby等等。

  • 性能测试,Fiddler让你一眼就看到加载页面权重-请求的数量和传输的字节,http缓存和压缩技术。也可以用Fiddler的自定义规则来关注潜在的性能问题.比如,你可以标记所有大于25KB的响应(Fiddler4要安装Fiddeler script)

  • HTTP/HTTPS流量监控,Fiddler是一个免费的web调试代理,记录了所有在你的电脑和互联网之间的HTTP(s)的流量,可以用它来调试几乎任何支持代理的应用程序比如IE,Chrome,Safari等

  • Web会话操作,容易操作和编辑web会话。你所需要做的就是设置一个暂停正在处理的会话和允许变更的请求/响应的断点。你也可以在Fiddler中编写自己的HTTP请求(也就是伪造请求)

  • 安全性测试,使用Fiddler对你的web应用程序进行安全性测试——解密HTTPS流量,用man-in-the-middler解密技显示和修改请求。可以配置Fiddler解码所有流量或者特定会话的流量

  • 定制Fiddler,受益于丰富的扩展模型,从简单的FiddlerScript到强大的扩展,可以使用任何.net语言来扩展,点击查看已有的扩展

Fiddler原理:

在本机开启一个http的代理服务器,然后它会转发所有的http请求和响应到最终的服务器,如图所示
fiddler原理
打开Fiddler后,Fiddler会自动篡改代理,打开ie的internet选项->连接->局域网设置->高级可以看到下图
fiddler代理
通过更改浏览器的代理服务地址,Fiddler就可以截获所有发出的请求

Fiddler两种代理模式

流模式:(streaming) 实时传送给客户端(更接近于浏览器本身真实的行为)
缓冲模式: (buffering) 等http请求所有东西都准备好后才返回给应用程序(可控制最后的服务器响应)
在Fiddler的工具栏的Stream可以进行两种模式的切换,默认是缓冲模式

Fiddler使用场景

  • 开发环境host配置,Tools->Hosts

  • 前后端接口调试

  • 线上bugfix,将线上项目代理到本地进行修改调试(AutoResponder,Willow)

  • 性能分析和优化

工具栏常用功能介绍

  • Replay,回放会话,选中会话并按R键即可回放会话(可多条)

  • 清空监控面板,快捷键ctrl+x

  • go 断点调试

  • stream切换代理模式

  • Decode 解压请求

  • keep all session选项可选保存会话的数量,默认的保存所有,保存的会话越多,fiddler占用的内存越大,可以设置下,而且调试也不希望看到太多会话,可以根据需要清空监控面板或过滤请求

  • All Process,可以用来控制如只捕获chrome浏览器的请求

  • Find 可以查找会话并选择颜色高亮标明

  • TextWizard 解码/编码功能,可选选项很多,避免去网上找解码工具

状态栏

状态栏功能较少,但也很重要

Capture用来控制Fiddler是否工作,点击即可切换状态
All Process控制请求来源
旁边的数字代表当前会话数量

命令行

  • select命令。选择所有相应类型(指content-type)为指定类型的HTTP请求,如选择图片,使用命令select image.而select css则可以选择所有相应类型为css的请求,select html则选择所有响应为HTML的请求,选中的部分会高亮显示,如图执行select image
    select image

  • allbut命令。
    allbut命令用于选择给定类型的HTTP请求(删除其他类型请求),该命令还有一个别名keeponly.

    Eg:只保留image会话:allbut image
    
  • ?text命令
    选择所有 URL 匹配问号后的字符的全部 session

    Eg:?qq.com
    
  • 小于/大于size命令
    选择响应大小大于某个大小或者小于某个大小的所有HTTP请求

    Eg:选择响应大小小于10k的请求:<10k
    
  • =status命令
    选择响应状态等于给定状态的所有HTTP请求。

    Eg:选择所有状态为200的HTTP请求:=200
    
  • @host命令
    选择包含指定 HOST 的全部 HTTP请求。

    Eg:选择所有host包含csdn.net的请求:@csdn.net
    
  • Bpafter, Bps, bpv, bpm, bpu

    这几个命令主要用于批量设置断点
    
    Bpafter xxx: 中断 URL 包含指定字符的全部 session 响应
    
    Bps xxx: 中断 HTTP 响应状态为指定字符的全部 session 响应。
    
    Bpv xxx: 中断指定请求方式的全部 session 响应
    
    Bpm xxx: 中断指定请求方式的全部 session 响应。等同于bpv xxx
    
    Bpu xxx:与bpafter类似。
    
    当这些命令没有加参数时,会清空所有设置了断点的HTTP请求。
    
  • help
    输入help会弹出这个页面 http://docs.telerik.com/fiddler/knowledgebase/quickexec,是fiddler的官方命令行文档

会话图标类型(此图为Fiddler2)

右侧窗口功能

  • Stastics:统计选中的一个或多个请求相关数据,大小、耗时

    最下方会有一个不太容易发现的功能show charts,点击会对请求进行可视化处理,如图

  • Inspectors:多种方式查看Request或者Response的详细消息,如图:

  • AutoResponder: 设置一些规则将符合规则的请求重定向到本地。

  • Composer:创建发送HTTP请求/前后端接口联调

  • Filters:设置会话过滤规则

  • Log:日志

  • Timeline:性能优化和分析

  • Willow的使用:请求重定向(模拟响应)

    右键添加项目,规则,host

    注意:Match中的URL必须以"/"结尾,Action中的URL必须以"\"结尾
    其他使用:

Fiddeler+Willow+全民wifi 配合真机调试

  1. 确保fiddler相关选项勾选如下,tools->fiddler options ->connections

这里默认的只会抓取HTTP请求包,如果也要抓取HTTPS包,则在HTTPS标签栏里设置,把Capture HTTPS CONNECTs选项    勾上。

2.确保capture开启,我的fiddler有时候会出现这种情况

此时capture会关闭,需要点击黄色区域才能重新开启capture,不然无法抓到请求

3.手机和电脑连接在同一个局域网中,全民wifi插入电脑USB口(我用过360随身wifi等其他设备,因为公司网络设置的原因都不行)。如果是第一次使用全民wifi,会有提示下载驱动,成功安装驱动后会在电脑右下角提示安装成功和开始体验。电脑右下角出现全民wifi图标,可以通过手机扫描二维码或者在手机端wifi设置里面直接输入帐号密码来连接共享pc网络,接着配置代理,一般fiddler默认的端口是8888,如有不同可以在connection中查看,配置主机名,两种方法:

 方法一:windows下是win+r  运行cmd  接着运行ipconfig
![](http://7xteia.com1.z0.glb.clouddn.com/4.png)
方法二:鼠标移上fiddler右上角的online会出现提示如下:
![](http://7xteia.com1.z0.glb.clouddn.com/5.png)

4.创建重定向规则,如果文件较少,可以用fiddler自带的AutoResponsder,如果是文件夹,就用willow添加,因为手q使用了离线包机制吗,所以willow要配置host禁用离线包并在手机端清除qq缓存,右键会出现添加项目规则host等的工具栏

配置host如图

5.开始抓包,打开项目对应的区域,抓到的请求在fiddler里面会是橙色区域如图

即代表抓包成功,有时候会出现404或者代码报错,所以一般要先在浏览器端测试观察控制台有没有报错
如果发现404,可能是因为使用了不同的域名,也要进行添加规则和匹配

6.浏览器端的调试如下:
复制抓到的请求地址,在浏览器端打开即可,如果要调试一个项目下另外的业务,更改url结构即可

我遇到过因为需要看外网的情况关闭了手机的代理后再开结果无法抓包的问题,重启手机就好了,有时候请求很多有很多不想看的请求需要观察当前的业务时,可以设置filter

过滤掉请求后点击action选择run filterset now即可生效

7.清空面板方法如下:
方法一:在命令窗口里输入clear即可
方法二:上方有移除请求的选项
点击会有很多选项,可以移除所有请求,移除img请求和移除请求码为非200的等等,根据实际需要选择不同选项
方法三:清除所有记录的快捷键:ctrl+x

一般调试的时候都要禁用缓存
在fiddler2里是Tools->Performance->Disable Caching,在fiddler4里是Rules->Performance->Disable Caching

Fiddler的高级使用

网速限速

我们为什么要限速。限速对于web前端研发是非常重要的,由于开发者的机器一般配置都很高,并且是在localhost下来调试程序,所以很难模拟到用户的真实使用情 况,如正在下载JS,css等静态资源的时候,页面的一个渲染情况。当网速很慢的时候,我们更希望看到的是先渲染出用户界面,而不是让用户看到一片空白。那么这个时候,网络限速就能很方便在localhost针对类似的情况来做性能调试与优化。

方法一:Fiddler script(自定义延时)
需要的插件:Fiddler script,下载地址:http://www.telerik.com/download/fiddler/fiddlerscript-editor
下载完直接安装就行了,安装之前必须关闭fiddler,再打开fiddler就会在右侧tab看到fiddler script选项
fiddler script原理:把请求完全代码化
Eg:模拟延时3s发送请求:
选中会话,在fiddler-script——>Go to->OnBeforeRequest添加代码如下:
oSession["request-trickle-delay"] = "3000"

点击save script保存,再replay会话就会发现会话延迟了三秒才发送
延时响应同理

方法二:
Rules → Performances → Simulate Modem Speeds

Fiddler替换HTTP Request Host
替换的方法有两种,一种是暂时的,一种是永久的,暂时的方法是在Fiddler 左下角输入:

urlreplace www.demo.com www.dev.demo.com

要清除转发,请在同一位置输入:

urlreplace

按Enter 就可以了。

更详细的说明请参考Fiddler官方说明文件- QuickExec Reference 。 可以发现urlreplace 做的是整个网址字串的取代,所以可以动手脚的地方不只于此。

永久的方法是修改Fiddler的CustomRules.js ,注意是.js ! 点下Fiddler 上方的Rules ,再点Customize Rules :

如果有安装FiddlerScript Editor ,会用FiddlerScript Editor开启CustomRules.js ,否则会用笔记本开启。 或者也可以到「我的文件 Fiddler2 Scripts 」直接编辑CustomRules.js 。

//请先在CustomRules.js 找到:
  static function OnBeforeRequest ( oSession : Session ) {
   // ...
   //在函式中加入:
  if ( oSession . HostnameIs ( 'www.demo.com' ) )
   oSession . hostname = 'www.dev.demo.com' ;
 }

将CustomRules.js 存档, Fiddler 会自动重新载入CustomRules.js ,原先发到www.demo.com 的HTTP Request 就会自动转发到www.dev.demo.com 。
更详细的说明请参考Fiddler官方说明文件- Script Samples 。

Fiddler断点调试

设置断点有两种方法
第一种:打开Fiddler 点击Rules-> Automatic Breakpoint ->Before Requests(这种方法会中断所有的会话)
如何消除命令呢? 点击Rules-> Automatic Breakpoint ->Disabled

第二种: 在命令行中输入命令: bpu www.baidu.com (这种方法只会中断www.baidu.com)

如何消除命令呢? 在命令行中输入命令 bpu

看个实例,模拟QQ邮箱的登录,输入错误的用户名和密码,用Fiddler中断会话,修改成正确的用户名密码。这样就能成功登录

  1. 登录qq邮箱,输入错误的密码

  2. 打开Fiddler, 在命令行中输入bpu

  3. 输入错误的用户名和密码 点击登录

  4. Fiddler 能中断这次会话,选择被中断的会话,点击Inspectors tab下的WebForms tab 修改用户名密码,然后点击Run to Completion。

  5. 结果是正确地登录了qq邮箱

禁用缓存

两种方法,一种暂时的,一种永久的(通过fiddler script)
暂时的方法:
Tools->Performance->Disable Caching

永久的方法:
在fiddler script里查找
> var m_DisableCaching: boolean = false;

把值改成true并保存就可以了

扩展Fiddler script的一些用法

实例 让所有qq的会话都显示红色。

把这段脚本放在OnBeforeRequest(oSession: Session) 方法下,并且点击"Save script"

     if (oSession.HostnameIs("www.cnblogs.com")) {
            oSession["ui-color"] = "red";
    }

效果如图

这样所有的cnblogs的会话都会显示红色

如何在Fiddler Script中修改Cookie

cookie其实就是request 中的一个header.

// 删除所有的cookie

oSession.oRequest.headers.Remove("Cookie");

// 新建cookie
oSession.oRequest.headers.Add("Cookie", "username=testname;testpassword=P@ssword1");

注意: Fiddler script不能直接删除或者编辑单独的一个cookie, 你需要用replace方法或者正则表达式的方法去操作cookie的string

复制代码

static function OnBeforeRequest(oSession: Session) 
{ 
     if (oSession.HostnameIs('www.example.com') && 
          oSession.uriContains('pagewithCookie') && 
oSession.oRequest.headers.Contains("Cookie")) 
     { 

     var sCookie = oSession.oRequest["Cookie"]; 

     //  用replace方法或者正则表达式的方法去操作cookie的string
     sCookie = sCookie.Replace("cookieName=", "ignoreme="); 

     oSession.oRequest["Cookie"] = sCookie; 
    } 

一点小的tips:

  • 我在使用Fiddler的过程中碰到过无法抓包的情况,原因是之前因为测试配置了autoresponder或者filter等没有改回去,如果碰到这个情况请确保Fiddler的选项都配置正确

  • chrome和firefox浏览器无法被监听
    fiddler安装之后,默认会在IE浏览器中安装一个fiddler的插件,所以它对IE及国内基于IE内核的各类浏览器都能实现监听,但其他内核的浏览器无法被监听。

    解决办法:禁用chrome和firefox中具有代理功能的插件,比如chrome如果安装了switchSharp,禁用它或选择“使用系统代理设置”,或在switchSharp中新配置一个代理项(比如名为fiddler,用于指向代理127.0.0.1,端口8888,如下图),即可实现监听。
    
查看原文

vczhan 赞了文章 · 2月5日

CSS奇思妙想 -- 使用 background 创造各种美妙的背景

本文属于 CSS 绘图技巧其中一篇,系列文章:

将介绍一些利用 CSS 中的 backgroundmix-blend-modemask 及一些相关属性,制作一些稍微复杂、酷炫的背景。

通过本文,你将会了解到 CSS background 中更为强大的一些用法,并且学会利用 background 相关的一些属性,采用不同的方式,去创造更复杂的背景图案。在这个过程中,你会更好的掌握不同的渐变技巧,更深层次的理解各种不同的渐变。

同时,借助强大的 CSS-Doodle,你将学会如何运用一套规则,快速创建大量不同的随机图案,感受 CSS 的强大,走进 CSS 的美。

背景基础知识

我们都知道,CSS 中的 background 是非常强大的。

首先,复习一下基础,在日常中,我们使用最多的应该就是下面 4 种:

  • 纯色背景 background: #000

  • 线性渐变 background: linear-gradient(#fff, #000) :

  • 径向渐变 background: radial-gradient(#fff, #000) :

  • 角向渐变 background: conic-gradient(#fff, #000) :

背景进阶

当然。掌握了基本的渐变之后,我们开始向更复杂的背景图案进发。我最早是在《CSS Secret》一书中接触学习到使用渐变去实现各种背景图案的。然后就是不断的摸索尝试,总结出了一些经验。

在尝试使用渐变去制作更复杂的背景之前,列出一些比较重要的技巧点:

  • 渐变不仅仅只能是单个的 linear-gradient 或者单个的 radial-gradient,对于 background 而言,它是支持多重渐变的叠加的,一点非常重要;
  • 灵活使用 repeating-linear-gradeintrepeating-radial-gradeint),它能减少很多代码量
  • transparent 透明无处不在
  • 尝试 mix-blend-modemask,创建复杂图案的灵魂
  • 使用随机变量,它能让一个 idea 变成无数美丽的图案

接下来,开始组合之旅。

使用 mix-blend-mode

mix-blend-mode ,混合模式。最常见于 photoshop 中,是 PS 中十分强大的功能之一。在 CSS 中,我们可以利用混合模式将多个图层混合得到一个新的效果。

关于混合模式的一些基础用法,你可以参考我的这几篇文章:

然后,我们来尝试第一个图案,先简单体会一下 mix-blend-mode 的作用。

我们使用 repeating-linear-gradient 重复线性渐变,制作两个角度相反的背景条纹图。正常而言,不使用混合模式,将两个图案叠加在一起,看看会发生什么。

额,会发生什么就有鬼了 。显而易见,由于图案不是透明的,叠加在一起之后,由于层叠的关系,只能看到其中一张图。

好,在这个基础上,我们给最上层的图案,添加 mix-blend-mode: multiply,再来一次,看看这次会发生什么。

可以看到,添加了混合模式之后,两张背景图通过某种算法叠加在了一起,展现出了非常漂亮的图案效果,也正是我们想要的效果。

CodePen Demo - Repeating-linear-gradient background & mix-blend-mode

尝试不同的 mix-blend-mode

那为什么上面使用的是 mix-blend-mode: multiply 呢?用其他混合模式可以不可以?

当然可以。这里仅仅只是一个示例,mix-blend-mode: multiply 在 PS 中意为正片叠底,属于图层混合模式的变暗模式组之一。

我们使用上面的 DEMO,尝试其他的混合模式,可以得到不同的效果。

可以看到,不同的混合模式的叠加,效果相差非常之大。当然,运用不同的混合模式,我们也就可以创造出效果各异的图案。

CodePen Demo - Repeating-linear-gradient background & mix-blend-mode

借助 CSS-Doodle 随机生成图案

到这,就不得不引出一个写 CSS 的神器 -- CSS-Doodle,我在其他非常多文章中也多次提到过 CSS-doodle,简单而言,它是一个基于 Web-Component 的库。允许我们快速的创建基于 CSS Grid 布局的页面,并且提供各种便捷的指令及函数(随机、循环等等),让我们能通过一套规则,得到不同 CSS 效果。

还是以上面的 DEMO 作为示例,我们将 repeating-linear-gradient 生成的重复条纹背景的颜色、粗细、角度随机化、采用的混合模式也是随机选取,然后利用 CSS-Doodle,快速随机的创建各种基于此规则的图案:

可以点进去尝试一下,点击鼠标即可随机生成不同的效果:

CodePen Demo -- CSS Doodle - CSS MIX-BLEND-MODE Background

尝试使用径向渐变

当然,上面使用的是线性渐变,同样,我们也可以使用径向渐变运用同样的套路。

我们可以使用径向渐变,生成多重的径向渐变。像是这样:

给图片应用上 background-size,它就会像是这样:

像上文一样,我们稍微对这个图形变形一下,然后叠加两个图层,给最上层的图形,添加 CSS 样式 mix-blend-mode: darken

CodePen Demo -- radial-gradient & mix-blend-mode Demo

借助 CSS-Doodle 随机生成图案

再来一次,我们使用 CSS-Doodle,运用上述的规则在径向渐变,也可以得到一系列有意思的背景图。

可以点进去尝试一下,点击鼠标即可随机生成不同的效果:

CodePen Demo -- CSS Doodle - CSS MIX-BLEND-MODE Background 2

当然,上述的叠加都是非常简单的图案的叠加,但是掌握了这个原理之后,就可以自己尝试,去创造更复杂的融合。

上述的叠加效果是基于大片大片的实色的叠加,当然 mix-blend-mode 还能和真正的渐变碰撞出更多的火花。

在不同的渐变背景中运用混合模式

在不同的渐变背景中运用混合模式?那会产生什么样美妙的效果呢?

运用得当,它可能会像是这样:

umm,与上面的条纹图案完全不一样的风格。

你可以戳进 gradienta.io 来看看,这里全是使用 CSS 创建的渐变叠加的背景图案库。

使用混合模式叠加不同的渐变图案

下面,我们也来实现一个。

首先,我们使用线性渐变或者径向渐变,随意创建几个渐变图案,如下所示:

接着,我们两两之间,从第二层开始,使用一个混合模式进行叠加,一共需要设定 5 个混合模式,这里我使用了 overlay, multiply, difference, difference, overlay。看看叠加之后的效果,非常的 Nice:

CodePen Demo -- Graideint background mix

由于上面动图 GIF 的压缩率非常高,所以看上去锯齿很明显图像很模糊,你可以点进上面的链接看看。

然后,我们可以再给叠加后的图像再加上一个 filter: hue-rotate(),让他动起来,放大一点点看看效果,绚丽夺目的光影效果:

CodePen Demo -- Graideint background mix 2

借助 CSS-Doodle 随机生成图案

噔噔噔,没错,这里我们又可以继续把 CSS-Doodle 搬出来了。

随机的渐变,随机的混合模式,叠加在一起,燥起来吧。

使用 CSS-Doodle 随机创建不同的渐变,在随机使用不同的混合模式,让他们叠加在一起,看看效果:

当然,由于是完全随机生成的效果,所以部分时候生成出来的不算太好看或者直接是纯色的。不过大部分还是挺不错的

CodePen Demo -- CSS Doodle Mix Gradient


感谢坚持,看到这里。上述上半部分主要使用的混合模式,接下来,下半部分,将主要使用 mask,精彩继续。


使用 mask

除去混合模式,与背景相关的,还有一个非常有意思的属性 -- MASK

mask 译为遮罩。在 CSS 中,mask 属性允许使用者通过遮罩或者裁切特定区域的图片的方式来隐藏一个元素的部分或者全部可见区域

对 mask 的一些基础用法还不太熟悉的,可以先看看我的这篇文章 -- 奇妙的 CSS MASK

简单而言,mask 可以让图片我们可以灵活的控制图片,设定一部分展示出来,另外剩余部分的隐藏。

使用 mask 对图案进行切割

举个例子。假设我们使用 repeating-linear-gradient 渐变制作这样一个渐变图案:

它的 CSS 代码大概是这样:

:root {
    $colorMain: #673ab7;
}
{
    background: 
        repeating-linear-gradient(0, $colorSub 0, $colorSub 3px, transparent 3px, transparent 10px),
        repeating-linear-gradient(60deg, $colorSub 0, $colorSub 3px, transparent 3px, transparent 10px),
        repeating-linear-gradient(-60deg, $colorSub 0, $colorSub 3px, transparent 3px, transparent 10px);
}

如果我们给这个图案,叠加一个这样的 mask :

{
    mask: conic-gradient(from -135deg, transparent 50%, #000);
}

上述 mask 如果是使用 background 表示的话,是这样 background: conic-gradient(from -135deg, transparent 50%, #000), 图案是这样:

两者叠加在一起,按照 mask 的作用,背景与 mask 生成的渐变的 transparent 的重叠部分,将会变得透明。将会得到这样一种效果:

CodePen Demo -- mask & background Demo

我们就完成了 background 与 mask 的结合。运用 mask 切割 background 的效果,我们就能制作出非常多有意思的背景图案:

CodePen Demo -- mask & background Demo

mask-composite OR -webkit-mask-composite

接下来,在运用 mask 切割图片的同时,我们会再运用到 -webkit-mask-composite 属性。这个是非常有意思的元素,非常类似于 mix-blend-mode / background-blend-mode

-webkit-mask-composite: 属性指定了将应用于同一元素的多个蒙版图像相互合成的方式。

通俗点来说,他的作用就是,当一个元素存在多重 mask 时,我们就可以运用 -webkit-mask-composite 进行效果叠加。

注意,这里的一个前提,就是当 mask 是多重 mask 的时候(类似于 background,mask 也是可以存着多重 mask),-webkit-mask-composite 才会生效。这也就元素的 mask 可以指定多个,逗号分隔。

假设我们有这样一张背景图:

:root {
    $colorMain: #673ab7;
    $colorSub: #00bcd4;
}
div {
    background: linear-gradient(-60deg, $colorMain, $colorSub);
}

我们的 mask 如下:

{
    mask: 
            repeating-linear-gradient(30deg, #000 0, #000 10px, transparent 10px, transparent 45px),
            repeating-linear-gradient(60deg, #000 0, #000 10px, transparent 10px, transparent 45px),
            repeating-linear-gradient(90deg, #000 0, #000 10px, transparent 10px, transparent 45px);
}

mask 表述成 background 的话大概是这样:

如果,不添加任何 -webkit-mask-composite,叠加融合之后的效果是这样:

如果添加一个 -webkit-mask-composite: xor,则会变成这样:

可以看到,线条的交汇叠加处,有了不一样的效果。

CodePen Demo -- background & -webkit-mask-composite

借助 CSS-Doodle 随机生成图案

了解了基本原理之后,上 CSS-Doodle,我们利用多重 mask 和 -webkit-mask-composite,便可以创造出各式各样的美妙背景图案:

是不是很类似万花筒?

借助了 CSS-Doodle,我们只设定大致的规则,辅以随机的参数,随机的大小。接着就是一幅幅美妙的背景图应运而生。

下面是运用上述规则的尝试的一些图案:

CodePen Demo -- CSS Doodle - CSS MASK Background

当然,可以尝试变换外形,譬如让它长得像个手机壳。

下面两个 DEMO 也是综合运用了上述的一些技巧的示例,仿佛一个个手机壳的图案。

CodePen Demo -- CSS Doodle - CSS MASK Background 2

CodePen Demo -- CSS Doodle - CSS MASK Background 3

总结一下

背景 background 不仅仅只是纯色、线性渐变、径向渐变、角向渐变。混合模式、滤镜、遮罩也并不孤独。

background 配合混合模式 mix-blend-modebackground-blend-mode、滤镜 filter、以及遮罩 mask 的时候,它们就可以组合变幻出各种不同的效果。

到目前为止,CSS 已经越来越强大,它不仅仅可以用于写业务,也可以创造很多有美感的事物,只要我们愿意去多加尝试,便可以创造出美妙的图案。

最后

好了,本文到此结束,看到这里,你是不是也跃跃欲试?想自己亲手尝试一下?

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

查看原文

赞 24 收藏 16 评论 2

vczhan 关注了用户 · 2月5日

民工哥 @jishuroad

10多年IT职场老司机的经验分享,坚持自学一路从技术小白成长为互联网企业信息技术部门的负责人。

我的新书:《Linux系统运维指南》

微信公众号:民工哥技术之路

民工哥:知乎专栏

欢迎关注,我们一同交流,相互学习,共同成长!!

关注 3597

vczhan 收藏了文章 · 2020-12-15

全面拥抱React-Hooks

丑话说在前面
强烈建议至少刷一遍《官方文档》,反复研读《Hooks FAQ》
这里主要以本人关注点聚合,方便理解用于实践

一、React-Hooks要解决什么?

以下是上一代标准写法类组件的缺点,也正是hook要解决的问题

  • 大型组件很难拆分和重构,也很难测试。
  • 业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
  • 组件类引入了复杂的编程模式,比如 Render props 和高阶组件

设计目的

  • 加强版函数组件,完全不使用"类",就能写出一个全功能的组件
  • 组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来

二、如何用好React-Hooks?

明确几点概念

  • 所有的hook,在默认没有依赖项数组每次渲染都会更新
  • 每次 Render 时Props、State、事件处理、Effect等hooks都遵循 Capture Value 的特性
  • Render时会注册各种变量,函数包括hooks,N次Render就会有N个互相隔离的状态作用域
  • 如果你的useEffect依赖数组为[],那么它初始化一次,且使用的state,props等永远是他初始化时那一次Render保存下来的值
  • React 会确保 setState,dispatch,context 函数的标识是稳定的,可以安全地从 hooks 的依赖列表中省略

Function Component中每次Render都会形成一个快照并保留下来,这样就确保了状态可控,hook默认每次都更新,会导致重复请求等一系列问题,如果给[]就会一尘不变,因此用好hooks最重要就是学会控制它的变化

三、一句话概括Hook API

  • useState 异步设置更新state
  • useEffect 处理副作用(请求,事件监听,操作DOM等)
  • useContext 接收一个 context 对象并返回该 context 的当前值
  • useReducer 同步处理复杂state,减少了对深层传递回调的依赖
  • useCallback 返回一个 memoized 回调函数,避免非必要渲染
  • useMemo 返回一个 memoized 值,使得控制具体子节点何时更新变得更容易,减少了对纯组件的需要,可替代shouldComponentUpdate
  • useRef 返回一个在组件的整个生命周期内保持不变 ref 对象,其 .current 属性是可变的,可以绕过 Capture Value 特性
  • useLayoutEffect 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect
  • useImperativeHandle 应当与 forwardRef 一起使用,将 ref 自定义暴露给父组件的实例值
  • useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签

四、关注异同点

useState 与 this.setState

  • 相同点:都是异步的,例如在 onClick 事件中,调用两次 setState,数据只改变一次。
  • 不同点:类中的 setState 是合并,而useState中的 setState 是替换。

useState 与 useReducer

  • 相同点:都是操作state
  • 不同点:使用 useState 获取的 setState 方法更新数据时是异步的;而使用 useReducer 获取的 dispatch 方法更新数据是同步的。
  • 推荐:当 state 状态值结构比较复杂时,使用useReducer

useLayoutEffect 与 useEffect

  • 相同点:都是在浏览器完成布局与绘制之后执行副作用操作
  • 不同点:useEffect 会延迟调用,useLayoutEffect 会同步调用阻塞视觉更新,可以使用它来读取 DOM 布局并同步触发重渲染
  • 推荐:一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffect

useCallback 与 useMemo

  • 相同点:都是返回memoized,useCallback( fn, deps) 相当于 useMemo( ( ) => fn, deps)
  • 不同点:useMemo返回缓存的变量,useCallback返回缓存的函数
  • 推荐:不要过早的性能优化,搭配食用口味更佳(详见下文性能优化)

五、性能优化

在大部分情况下我们只要遵循 React 的默认行为,因为 React 只更新改变了的 DOM 节点,不过重新渲染仍然花费了一些时间,除非它已经慢到让人注意了

react中性能的优化点在于:

  • 1、调用setState,就会触发组件的重新渲染,无论前后的state是否不同
  • 2、父组件更新,子组件也会自动的更新

之前的解决方案

基于上面的两点,我们通常的解决方案是:

  • 使用immutable进行比较,在不相等的时候调用setState;
  • 在 shouldComponentUpdate 中判断前后的 props和 state,如果没有变化,则返回false来阻止更新。
  • 使用 React.PureComponent

使用hooks function之后的解决方案

传统上认为,在 React 中使用内联函数对性能的影响,与每次渲染都传递新的回调会如何破坏子组件的 shouldComponentUpdate 优化有关, 使用useCallback缓存函数引用,再传递给经过优化的并使用引用相等性去避免非必要渲染的子组件时,它将非常有用

  • 1、使用 React.memo等效于 PureComponent,但它只比较 props,且返回值相反,true才会跳过更新
const Button = React.memo((props) => {
  // 你的组件
}, fn);// 也可以自定义比较函数
  • 2、用 useMemo 优化每一个具体的子节点(详见实践3)
  • 3、useCallback Hook 允许你在重新渲染之间保持对相同的回调引用以使得 shouldComponentUpdate 继续工作(详见实践3)
  • 4、useReducer Hook 减少了对深层传递回调的依赖(详见实践2)

如何惰性创建昂贵的对象?

  • 当创建初始 state 很昂贵时,我们可以传一个 函数 给 useState 避免重新创建被忽略的初始 state
function Table(props) {
  // ⚠️ createRows() 每次渲染都会被调用
  const [rows, setRows] = useState(createRows(props.count));
  // ...
  // ✅ createRows() 只会被调用一次
  const [rows, setRows] = useState(() => createRows(props.count));
  // ...
}
  • 避免重新创建 useRef() 的初始值,确保某些命令式的 class 实例只被创建一次:
function Image(props) {
  // ⚠️ IntersectionObserver 在每次渲染都会被创建
  const ref = useRef(new IntersectionObserver(onIntersect));
  // ...
}
function Image(props) {
  const ref = useRef(null);
  // ✅ IntersectionObserver 只会被惰性创建一次
  function getObserver() {
    if (ref.current === null) {
      ref.current = new IntersectionObserver(onIntersect);
    }
    return ref.current;
  }
  // 当你需要时,调用 getObserver()
  // ...
}

六、注意事项

Hook 规则

  • 在最顶层使用 Hook
  • 只在 React 函数中调用 Hook,不要在普通的 JavaScript 函数中调用
  • 将条件判断放置在 hook 内部
  • 所有 Hooks 必须使用 use 开头,这是一种约定,便于使用 ESLint 插件 来强制 Hook 规范 以避免 Bug;
useEffect(function persistForm() {
  if (name !== '') {
    localStorage.setItem('formData', name);
  }
});

告诉 React 用到了哪些外部变量,如何对比依赖

useEffect(() => {
  document.title = "Hello, " + name;
}, [name]); // 以useEffect为示例,适用于所有hook

直到 name 改变时的 Rerender,useEffect 才会再次执行,保证了性能且状态可控

不要在hook内部set依赖变量,否则你的代码就像旋转的菊花一样停不下来

useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, [count]);// 以useEffect为示例,适用于所有hook

不要在useMemo内部执行与渲染无关的操作

  • useMemo返回一个 memoized 值,把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值,避免在每次渲染时都进行高开销的计算。
  • 传入 useMemo 的函数会在渲染期间执行,请不要在这个函数内部执行与渲染无关的操作。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

七、实践场景示例

实际应用场景往往不是一个hook能搞定的,长篇大论未必说的清楚,直接上例子(来源于官网摘抄,网络收集,自我总结)

1、只想执行一次的 Effect 里需要依赖外部变量

【将更新与动作解耦】-【useEffect,useReducer,useState】

  • 1-1、使用setState的函数式更新解决依赖一个变量

该函数将接收先前的 state,并返回一个更新后的值

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);
  • 1-2、使用useReducer解决依赖多个变量
import React, { useReducer, useEffect } from "react";

const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {
  const { count, step } = state;
  if (action.type === 'tick') {
    return { count: count + step, step };
  } else if (action.type === 'step') {
    return { count, step: action.step };
  } else {
    throw new Error();
  }
}
export default function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { count, step } = state;
  console.log(count);
  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        });
      }} />
    </>
  );
}

2、大型的组件树中深层传递回调

【通过 context 往下传一个 dispatch 函数】-【createContext,useReducer,useContext】

/**index.js**/
import React, { useReducer } from "react";
import Count from './Count'
export const StoreDispatch = React.createContext(null);
const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {
  const { count, step } = state;
  switch (action.type) {
    case 'tick':
      return { count: count + step, step };
    case 'step':
      return { count, step: action.step };
    default:
      throw new Error();
  }
}
export default function Counter() {
  // 提示:`dispatch` 不会在重新渲染之间变化
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <StoreDispatch.Provider value={dispatch}>
      <Count state={state} />
    </StoreDispatch.Provider>
  );
}

/**Count.js**/
import React, { useEffect,useContext }  from 'react';
import {StoreDispatch} from '../index'
import styles from './index.css';

export default function(props) {
  const { count, step } = props.state;
  const dispatch = useContext(StoreDispatch);
  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);
  return (
    <div className={styles.normal}>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        });
      }} />
    </div>
  );
}

3、代码内聚,更新可控

【层层依赖,各自管理】-【useEffect,useCallback,useContext】

function App() {
  const [count, setCount] = useState(1);
  const countRef = useRef();// 在组件生命周期内保持唯一实例,可穿透闭包传值

  useEffect(() => {
    countRef.current = count; // 将 count 写入到 ref
  });
  // 只有countRef变化时,才会重新创建函数
  const callback = useCallback(() => {
    const currentCount = countRef.current //保持最新的值
    console.log(currentCount);
  }, [countRef]);
  return (
    <Parent callback={callback} count={count}/>
  )
}
function Parent({ count, callback }) {
  // count变化才会重新渲染
  const child1 = useMemo(() => <Child1 count={count} />, [count]);
  // callback变化才会重新渲染,count变化不会 Rerender
  const child2 = useMemo(() => <Child2 callback={callback} />, [callback]);
  return (
    <>
      {child1}
      {child2}
    </>
  )
}

八、自定义 HOOK

实现this.setState的callback

function useStateCallback(init) {
  const [state, setState] = useState(init)
  const ref = useRef(init)

  const handler = useCallback((value, cb) => {
    setState(value)
    if(cb) {
      ref.current = value
      cb(ref.current)
    }
  }, [setState])

  return [state, handler]
}

获取上一轮的 props 或 state

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

只在更新时运行 effect

function useUpdate(fn) {
  const mounting = useRef(true);
  useEffect(() => {
    if (mounting.current) {
      mounting.current = false;
    } else {
      fn();
    }
  });
}

组件是否销毁

function useIsMounted(fn) {
  const [isMount, setIsMount] = useState(false);
  useEffect(() => {
    if (!isMount) {
      setIsMount(true);
    }
    return () => setIsMount(false);
  }, []);
  return isMount;
}

惰性初始化useRef

function useInertRef(obj) { // 传入一个实例 new IntersectionObserver(onIntersect)
  const ref = useRef(null);
  if (ref.current === null) {
  // ✅ IntersectionObserver 只会被惰性创建一次
    ref.current = obj;
  }
  return ref.current;
}

参考文章

  1. 精读《useEffect 完全指南》
  2. 精读《Function VS Class 组件》
  3. 精读《怎么用 React Hooks 造轮子》
  4. 《React Hooks 使用详解》
  5. 《useMemo与useCallback使用指南》
查看原文

vczhan 赞了文章 · 2020-12-15

全面拥抱React-Hooks

丑话说在前面
强烈建议至少刷一遍《官方文档》,反复研读《Hooks FAQ》
这里主要以本人关注点聚合,方便理解用于实践

一、React-Hooks要解决什么?

以下是上一代标准写法类组件的缺点,也正是hook要解决的问题

  • 大型组件很难拆分和重构,也很难测试。
  • 业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
  • 组件类引入了复杂的编程模式,比如 Render props 和高阶组件

设计目的

  • 加强版函数组件,完全不使用"类",就能写出一个全功能的组件
  • 组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来

二、如何用好React-Hooks?

明确几点概念

  • 所有的hook,在默认没有依赖项数组每次渲染都会更新
  • 每次 Render 时Props、State、事件处理、Effect等hooks都遵循 Capture Value 的特性
  • Render时会注册各种变量,函数包括hooks,N次Render就会有N个互相隔离的状态作用域
  • 如果你的useEffect依赖数组为[],那么它初始化一次,且使用的state,props等永远是他初始化时那一次Render保存下来的值
  • React 会确保 setState,dispatch,context 函数的标识是稳定的,可以安全地从 hooks 的依赖列表中省略

Function Component中每次Render都会形成一个快照并保留下来,这样就确保了状态可控,hook默认每次都更新,会导致重复请求等一系列问题,如果给[]就会一尘不变,因此用好hooks最重要就是学会控制它的变化

三、一句话概括Hook API

  • useState 异步设置更新state
  • useEffect 处理副作用(请求,事件监听,操作DOM等)
  • useContext 接收一个 context 对象并返回该 context 的当前值
  • useReducer 同步处理复杂state,减少了对深层传递回调的依赖
  • useCallback 返回一个 memoized 回调函数,避免非必要渲染
  • useMemo 返回一个 memoized 值,使得控制具体子节点何时更新变得更容易,减少了对纯组件的需要,可替代shouldComponentUpdate
  • useRef 返回一个在组件的整个生命周期内保持不变 ref 对象,其 .current 属性是可变的,可以绕过 Capture Value 特性
  • useLayoutEffect 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect
  • useImperativeHandle 应当与 forwardRef 一起使用,将 ref 自定义暴露给父组件的实例值
  • useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签

四、关注异同点

useState 与 this.setState

  • 相同点:都是异步的,例如在 onClick 事件中,调用两次 setState,数据只改变一次。
  • 不同点:类中的 setState 是合并,而useState中的 setState 是替换。

useState 与 useReducer

  • 相同点:都是操作state
  • 不同点:使用 useState 获取的 setState 方法更新数据时是异步的;而使用 useReducer 获取的 dispatch 方法更新数据是同步的。
  • 推荐:当 state 状态值结构比较复杂时,使用useReducer

useLayoutEffect 与 useEffect

  • 相同点:都是在浏览器完成布局与绘制之后执行副作用操作
  • 不同点:useEffect 会延迟调用,useLayoutEffect 会同步调用阻塞视觉更新,可以使用它来读取 DOM 布局并同步触发重渲染
  • 推荐:一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffect

useCallback 与 useMemo

  • 相同点:都是返回memoized,useCallback( fn, deps) 相当于 useMemo( ( ) => fn, deps)
  • 不同点:useMemo返回缓存的变量,useCallback返回缓存的函数
  • 推荐:不要过早的性能优化,搭配食用口味更佳(详见下文性能优化)

五、性能优化

在大部分情况下我们只要遵循 React 的默认行为,因为 React 只更新改变了的 DOM 节点,不过重新渲染仍然花费了一些时间,除非它已经慢到让人注意了

react中性能的优化点在于:

  • 1、调用setState,就会触发组件的重新渲染,无论前后的state是否不同
  • 2、父组件更新,子组件也会自动的更新

之前的解决方案

基于上面的两点,我们通常的解决方案是:

  • 使用immutable进行比较,在不相等的时候调用setState;
  • 在 shouldComponentUpdate 中判断前后的 props和 state,如果没有变化,则返回false来阻止更新。
  • 使用 React.PureComponent

使用hooks function之后的解决方案

传统上认为,在 React 中使用内联函数对性能的影响,与每次渲染都传递新的回调会如何破坏子组件的 shouldComponentUpdate 优化有关, 使用useCallback缓存函数引用,再传递给经过优化的并使用引用相等性去避免非必要渲染的子组件时,它将非常有用

  • 1、使用 React.memo等效于 PureComponent,但它只比较 props,且返回值相反,true才会跳过更新
const Button = React.memo((props) => {
  // 你的组件
}, fn);// 也可以自定义比较函数
  • 2、用 useMemo 优化每一个具体的子节点(详见实践3)
  • 3、useCallback Hook 允许你在重新渲染之间保持对相同的回调引用以使得 shouldComponentUpdate 继续工作(详见实践3)
  • 4、useReducer Hook 减少了对深层传递回调的依赖(详见实践2)

如何惰性创建昂贵的对象?

  • 当创建初始 state 很昂贵时,我们可以传一个 函数 给 useState 避免重新创建被忽略的初始 state
function Table(props) {
  // ⚠️ createRows() 每次渲染都会被调用
  const [rows, setRows] = useState(createRows(props.count));
  // ...
  // ✅ createRows() 只会被调用一次
  const [rows, setRows] = useState(() => createRows(props.count));
  // ...
}
  • 避免重新创建 useRef() 的初始值,确保某些命令式的 class 实例只被创建一次:
function Image(props) {
  // ⚠️ IntersectionObserver 在每次渲染都会被创建
  const ref = useRef(new IntersectionObserver(onIntersect));
  // ...
}
function Image(props) {
  const ref = useRef(null);
  // ✅ IntersectionObserver 只会被惰性创建一次
  function getObserver() {
    if (ref.current === null) {
      ref.current = new IntersectionObserver(onIntersect);
    }
    return ref.current;
  }
  // 当你需要时,调用 getObserver()
  // ...
}

六、注意事项

Hook 规则

  • 在最顶层使用 Hook
  • 只在 React 函数中调用 Hook,不要在普通的 JavaScript 函数中调用
  • 将条件判断放置在 hook 内部
  • 所有 Hooks 必须使用 use 开头,这是一种约定,便于使用 ESLint 插件 来强制 Hook 规范 以避免 Bug;
useEffect(function persistForm() {
  if (name !== '') {
    localStorage.setItem('formData', name);
  }
});

告诉 React 用到了哪些外部变量,如何对比依赖

useEffect(() => {
  document.title = "Hello, " + name;
}, [name]); // 以useEffect为示例,适用于所有hook

直到 name 改变时的 Rerender,useEffect 才会再次执行,保证了性能且状态可控

不要在hook内部set依赖变量,否则你的代码就像旋转的菊花一样停不下来

useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, [count]);// 以useEffect为示例,适用于所有hook

不要在useMemo内部执行与渲染无关的操作

  • useMemo返回一个 memoized 值,把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值,避免在每次渲染时都进行高开销的计算。
  • 传入 useMemo 的函数会在渲染期间执行,请不要在这个函数内部执行与渲染无关的操作。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

七、实践场景示例

实际应用场景往往不是一个hook能搞定的,长篇大论未必说的清楚,直接上例子(来源于官网摘抄,网络收集,自我总结)

1、只想执行一次的 Effect 里需要依赖外部变量

【将更新与动作解耦】-【useEffect,useReducer,useState】

  • 1-1、使用setState的函数式更新解决依赖一个变量

该函数将接收先前的 state,并返回一个更新后的值

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);
  • 1-2、使用useReducer解决依赖多个变量
import React, { useReducer, useEffect } from "react";

const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {
  const { count, step } = state;
  if (action.type === 'tick') {
    return { count: count + step, step };
  } else if (action.type === 'step') {
    return { count, step: action.step };
  } else {
    throw new Error();
  }
}
export default function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { count, step } = state;
  console.log(count);
  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        });
      }} />
    </>
  );
}

2、大型的组件树中深层传递回调

【通过 context 往下传一个 dispatch 函数】-【createContext,useReducer,useContext】

/**index.js**/
import React, { useReducer } from "react";
import Count from './Count'
export const StoreDispatch = React.createContext(null);
const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {
  const { count, step } = state;
  switch (action.type) {
    case 'tick':
      return { count: count + step, step };
    case 'step':
      return { count, step: action.step };
    default:
      throw new Error();
  }
}
export default function Counter() {
  // 提示:`dispatch` 不会在重新渲染之间变化
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <StoreDispatch.Provider value={dispatch}>
      <Count state={state} />
    </StoreDispatch.Provider>
  );
}

/**Count.js**/
import React, { useEffect,useContext }  from 'react';
import {StoreDispatch} from '../index'
import styles from './index.css';

export default function(props) {
  const { count, step } = props.state;
  const dispatch = useContext(StoreDispatch);
  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);
  return (
    <div className={styles.normal}>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        });
      }} />
    </div>
  );
}

3、代码内聚,更新可控

【层层依赖,各自管理】-【useEffect,useCallback,useContext】

function App() {
  const [count, setCount] = useState(1);
  const countRef = useRef();// 在组件生命周期内保持唯一实例,可穿透闭包传值

  useEffect(() => {
    countRef.current = count; // 将 count 写入到 ref
  });
  // 只有countRef变化时,才会重新创建函数
  const callback = useCallback(() => {
    const currentCount = countRef.current //保持最新的值
    console.log(currentCount);
  }, [countRef]);
  return (
    <Parent callback={callback} count={count}/>
  )
}
function Parent({ count, callback }) {
  // count变化才会重新渲染
  const child1 = useMemo(() => <Child1 count={count} />, [count]);
  // callback变化才会重新渲染,count变化不会 Rerender
  const child2 = useMemo(() => <Child2 callback={callback} />, [callback]);
  return (
    <>
      {child1}
      {child2}
    </>
  )
}

八、自定义 HOOK

实现this.setState的callback

function useStateCallback(init) {
  const [state, setState] = useState(init)
  const ref = useRef(init)

  const handler = useCallback((value, cb) => {
    setState(value)
    if(cb) {
      ref.current = value
      cb(ref.current)
    }
  }, [setState])

  return [state, handler]
}

获取上一轮的 props 或 state

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

只在更新时运行 effect

function useUpdate(fn) {
  const mounting = useRef(true);
  useEffect(() => {
    if (mounting.current) {
      mounting.current = false;
    } else {
      fn();
    }
  });
}

组件是否销毁

function useIsMounted(fn) {
  const [isMount, setIsMount] = useState(false);
  useEffect(() => {
    if (!isMount) {
      setIsMount(true);
    }
    return () => setIsMount(false);
  }, []);
  return isMount;
}

惰性初始化useRef

function useInertRef(obj) { // 传入一个实例 new IntersectionObserver(onIntersect)
  const ref = useRef(null);
  if (ref.current === null) {
  // ✅ IntersectionObserver 只会被惰性创建一次
    ref.current = obj;
  }
  return ref.current;
}

参考文章

  1. 精读《useEffect 完全指南》
  2. 精读《Function VS Class 组件》
  3. 精读《怎么用 React Hooks 造轮子》
  4. 《React Hooks 使用详解》
  5. 《useMemo与useCallback使用指南》
查看原文

赞 37 收藏 19 评论 4

vczhan 赞了文章 · 2020-10-14

Webpack 5 正式发布

Webpack简介

随着前端发展如日冲天,前端项目也越来越复杂,得益于Nodejs的发展,前端模块化、组件化、工程化也大势所趋。伴随着前端的模块化和工程化,Grunt、Gulp到Webpack等项目构建和打包工具也随之出现。

前端工程化的早期,主要是解决重复任务的问题。Grunt、Gulp就是其中代表,他们的主要功能是完成文件压缩、编译less、sass、地址添加hash、替换等。不过,随着前端工程化的发展,Webpack出现了,与其说是一个工程构建工具,Webpack更像是一套前端工程化解决方案。

根据官方资料介绍,Webpack 是一个前端资源加载和打包工具。所谓的模块就是在平时的前端开发中,用到一些静态资源,如JavaScript、CSS、图片等文件,Webpack就将这些静态资源文件称之为模块。 Webpack支持AMD和CommonJS,以及其他的一些模块系统,并且兼容多种JS书写规范,可以处理模块间的依赖关系,所以具有更强大的JS模块化的功能,它能对静态资源进行统一的管理以及打包发布。作为一款 Grunt和Gulp的替代产品,Webpack受到大多数开发者的喜爱,因为它能够编译打包CSS,做CSS预处理,对JS的方言进行编译,打包图片,代码压缩等等。

Webpack 5

自2018 年 2 月Webpack4 发布以来,Webpack 就一直没有更进一步的重大更新,为了保持 API 的一致性,旧的架构没有做太多改变,遗留了很多的包袱。或许是沉寂太久,2020 年 10 月 10 日,Webpack 正式发布了 5.0 版本。Webpack 5带来了哪些新的特性呢?

  • 尝试用持久性缓存来提高构建性能。
  • 尝试用更好的算法和默认值来改进长期缓存。
  • 尝试用更好的 Tree Shaking 和代码生成来改善包大小。
  • 尝试改善与网络平台的兼容性。
  • 尝试在不引入任何破坏性变化的情况下,清理那些在实现 v4 功能时处于奇怪状态的内部结构。
  • 试图通过现在引入突破性的变化来为未来的功能做准备,尽可能长时间地保持在 v5 版本上。

下面就让我们来看一下,Webpack 5带来的一些新的特性。

1, 清除过期功能

1.1 清理已弃用的功能

所有在 Webpack 4 标记即将过期的功能,都已在该版移除。因此在迁移到 Webpack 5 之前,请确保你在 Webpack 4 运行的构建不会有任何的功能过期警告,否则迁移到Webpack 5之后就会报错。

1.2 不再为Node.js 模块自动引用Polyfills

在 Webpack 4 或之前的版本中,任何项目引用 Node.js 内置模块都会自动添加 Polyfills,Polyfills是一个语法检查的模版工具。不过,Webpack 5不再为 Node.js 内置模块自动添加 Polyfills,Webpack 5会投入更多的精力到前端模块的兼容性工作中。

如果你的代码中有引用这些 Node.js 的模块,当需要升级到 Webpack 5版本时, 请将尽量使用前端的模块,或者自行手动添加适合的 Polyfills。

2. 针对长期缓存的优化

2.1 确定的 Chunk、模块 ID 和导出名称

Webpack 5新增了长期缓存的算法,这些算法在生产模式下是默认启用的,语法格式如下。

chunkIds: "deterministic" 
moduleIds: "deterministic"
mangleExports: "deterministic"

该算法以确定性的方式为模块和分块分配短的(3 或 5 位)数字 ID,这是包大小和长期缓存之间的一种权衡。由于这些配置将使用确定的 ID 和名称,这意味着生成的缓存失效不再更频繁。

2.2 真正的内容哈希

当使用[contenthash]时,Webpack 5 将使用真正的文件内容哈希值。之前它 "只 "使用内部结构的哈希值。当只有注释被修改或变量被重命名时,这对长期缓存会有积极影响。这些变化在压缩后是不可见的。

3. 更好的开发支持

在开发模式下,默认启用的新命名代码块 ID 算法为模块(和文件名)提供了人类可读的名称。
模块 ID 由其路径决定,相对于 context。代码块 ID 由代码块的内容决定。

所以你不再需要使用import(/* webpackChunkName: "name" */ "module")来调试。但如果你想控制生产环境的文件名,还是有意义的。

可以在生产环境中使用 chunkIds: "named" 在生产环境中使用,但要确保不要不小心暴露模块名的敏感信息。如果你不喜欢在开发中改变文件名,你可以通过 chunkIds: "natural" 来使用旧的数字模式。

3.2 模块联邦

Webpack 5 增加了一个新的功能 "模块联邦",它允许多个 Webpack 一起工作。从运行时的角度来看,多个构建的模块将表现得像一个巨大的连接模块图。从开发者的角度来看,模块可以从指定的远程构建中导入,并以最小的限制来使用。

4. 对Web 平台功能的全新支持

4.1 JSON 模块

比如对 JSON 模块,会与现在的提案保持一致,并且要求进行默认的导出,否则会有警告信息。即使使用默认导出,未使用的属性也会被 optimization.usedExports 优化丢弃,属性会被 optimization.mangleExports 优化打乱。

如果想用自定义的 JSON 解析器,可以在 Rule.parser.parse 中指定一个自定义的 JSON 解析器来导入类似 JSON 的文件(如toml、yaml、json5 等)。

4.2 资源模块

Webpack 5 现在已经对表示资源的模块提供了内置支持。这些模块可以向输出文件夹发送一个文件,或者向 Javascript 包注入一个 DataURI。 无论哪种方式,它们都会给出一个 URL 来让程序正常工作。

import url from "./image.png" //老方法
new URL("./image.png", import.meta.url)  //新方法

选择 "新的方式 "语法是为了允许在没有打包工具的情况下运行代码。这种语法也可以在浏览器中的原生 ECMAScript 模块中使用。

4.3 原生 Worker 支持

当把资源的 new URLnew Worker/new SharedWorker/navigator.serviceWorker.register 结合起来时,Webpack 会自动为 Web Worker 创建一个新的入口点(entrypoint),如下所示。

new Worker(new URL("./worker.js", import.meta.url))

选择这种语法也是为了允许在没有打包工具的情况下运行代码。这种语法在浏览器的原生 ECMAScript 模块中也可以使用。

4.4 URIs

Webpack 5 支持在请求中处理网络协议,比如:

  • 支持data:支持 Base64 或原始编码。Mimetype 可以在module.rule中被映射到加载器和模块类型。例如:import x from "data:text/javascript,export default 42"。
  • 支持file:支持file文件类型。

支持http(s):需要通过new webpack.experiments.s schemesHttp(s)UriPlugin()选择加入。

4.5 异步模块

Webpack 5 支持所谓的 "异步模块"。这些模块并不是同步解析的,而是基于异步和 Promise 的。通过import 导入它们会被自动处理,不需要额外的语法,而且几乎看不出区别。通过require()导入它们会返回一个解析到导出的 Promise。在 Webpack 中,有多种方式来拥有异步模块,常见的方式如下:

  • 异步的外部资源(async externals)
  • 新规范中的 WebAssembly 模块
  • 使用顶层 Await 的 ECMAScript 模块。

4.6 外部资源

Webpack 5 增加了更多的外部类型来支持更多的应用。
promise: 一个评估为 Promise 的表达式,外部模块是一个异步模块,解析值作为模块导出使用。
import:原生的 import() 用于加载指定的请求,外部模块是一个异步模块,解析值作为模块导出,外部模块是一个异步模块。
module:尚未实现,但计划通过 import x from "..." 加载模块。
script:通过 <script> 标签加载一个 url,并从一个全局变量(以及它的可选属性)中获取输出。外部模块是一个异步模块。

5. 开发体验上的提升

5.1 经过优化的构建目标(target)

Webpack 5 允许传递一个目标列表,并且支持目标的版本。例如 :

target: "node14" 
target: ["web", "es2020"]。

这是一个简单的方法,为 webpack 提供它需要确定的所有信息:代码块加载机制,以及支持的语法,如箭头函数。

5.2 统计格式

改进了统计测试格式的可读性和冗余性。改进了默认值,使其不那么冗长,也适合大型构建。

5.3 进度

ProgressPlugin插件也做了一些优化,现在不仅可以统计模块编译的进度,也可以统计 入口 和 依赖。并且,之前展示进度可能会对构建性能有一定的影响,这次的升级也做了一些性能方面的优化。

5.4 自动添加唯一命名

在 Webpack 4 中,多个 Webpack 同时运行时可能会在同一个 HTML 页面上发生冲突,因为它们使用同一个全局变量进行代码块加载。为了解决这个问题,需要为 output.jsonpFunction 配置提供一个自定义的名称。

同时,Webpack 5 会从 package.json name 中自动推断出一个唯一的构建名称,并将其作为 output.uniqueName 的默认值。由于 package.json 中有唯一的名称,可将 output.jsonpFunction删除。

5.5 自动添加公共路径

Webpack 5 会在可能的情况下自动确定 output.publicPath,无需开发者手动确认。

5.6 Typescript 类型

Webpack 5 可以从源码中生成 typescript 类型,并通过 npm 包暴露它们。如果要迁移到Webpack 5版本,需要删除 @types/webpack

6. 构建优化

6.1 嵌套的 tree-shaking

现在,Webpack能够跟踪对导出的嵌套属性的访问,因此可以改善重新导出命名空间对象时的 Tree Shaking(清除未使用的导出和混淆导出),如下所示。

// inner.js
export const a = 1;
export const b = 2;

// module.js
export * as inner from './inner';
// 或 
import * as inner from './inner'; export { inner };

// user.js
import * as module from './module';
console.log(module.inner.a);

6.2 内部模块 tree-shaking

Webpack 4 没有分析模块的导出和引用之间的依赖关系,Webpack 5 有一个新的选项 optimization.innerGraph,在生产模式下是默认启用的,它可以对模块中的标志进行分析,找出导出和引用之间的依赖关系。

import { something } from './something';

function usingSomething() {
  return something;
}

export function test() {
  return usingSomething();
}

内部依赖图算法会找出 something 只有在使用 test 导出时才会使用。这允许将更多的出口标记为未使用,并从代码包中省略更多的代码。

当设置"sideEffects": false时,可以省略更多的模块。在这个例子中,当 test 导出未被使用时,./something 将被省略。要获得未使用的导出信息,需要使用 optimization.unusedExports。要删除无副作用的模块,需要使用optimization.sideEffects。可以分析以下标记:

  • 函数声明
  • 类声明
  • 默认导出export default 或定义变量以下的
    1,函数表达式

2,类表达式
3,顺序表达式
4,/#__PURE__/ 表达式
5,局部变量
6,引入的捆绑(bindings)

使用 eval() 将为一个模块放弃这个优化,因为经过 eval 的代码可以引用范围内的任何标记。这种优化也被称为深度范围分析。

6.3 CommonJs Tree Shaking

曾经,Webpack不支持对 CommonJs进行 导出和 require() 调用时的导出使用分析。现在,Webpack 5 增加了对一些 CommonJs 构造的支持,允许消除未使用的 CommonJs 导出,并从 require() 调用中跟踪引用的导出名称。

支持的构造如下:

  • exports|this|module.exports.xxx = ...
  • exports|this|module.exports = require("...") (reexport)
  • exports|this|module.exports.xxx = require("...").xxx (reexport)
  • Object.defineProperty(exports|this|module.exports, "xxx", ...)
  • require("abc").xxx
  • require("abc").xxx()
  • 从 ESM 导入
  • require() 一个 ESM 模块
  • 被标记的导出类型 (对非严格 ESM 导入做特殊处理)
  • 未来计划支持更多的构造

6.4 开发与生产的一致性问题

Webpack正在通过改善开发模式很晚生产模式的相似性,并在开发模式上提升构建性能,避免仅在生产模式的产生的问题之间找到一个很好的平衡点。

Webpack 5 默认在两种模式下都启用了 "sideEffects "优化。在 Webpack 4 中,由于 package.json 中的"sideEffects"标记不正确,这种优化导致了一些只在生产模式下出现的错误。r如果在开发过程中启用这个优化,可以更快更容易地发现这些问题。

在很多情况下,开发和生产都是在不同的操作系统上进行的,文件系统的大小写敏感度不同,所以 Webpack 5 增加了一些奇怪的大小写的警告/错误。

6.5 改进 target 配置

在 Webpack 4 中,"target "是在 "web" 和 "node" 之间的一个粗略的选择(还有一些其他的)。Webpack 5 给开发者留下了更多的选择。target选项现在比以前影响了更多关于生成代码的事情,比如代码块加载方法、代码块的格式、externals 是否默认被启用等等。

此外,对于其中的一些情况,在 "web" 和 "node" 之间的选择过于粗略,我们需要更多的信息。因此,我们允许指定最低版本,例如 "node10.13",并推断出更多关于目标环境的属性。

现在Webpack也允许用一个数组组合多个目标,webpack 将确定所有目标的最小属性。使用数组也很有用,当使用像 "web" 或 "node" 这样没有提供完整信息的目标时(没有版本号)。例如,["web", "es2020"] 结合了这两个部分目标。

有一个目标 "browserslist",它将使用 browserslist 类库的数据来确定环境的属性。当项目中存在可用的 browserslist 配置时,这个目标也会被默认使用。当没有可用的配置时,默认使用 "web" 目标。

6.6 代码块拆分与模块大小

现在,Webpack 支持对模块按照大小进行拆分。SplitChunksPlugin 插件知道如何处理这些不同模块的大小,并为它们设置 minSize 和 maxSize,如下所示。

module.exports = {
  optimization: {
    splitChunks: {
      minSize: {
        javascript: 30000,
        webassembly: 50000,
      },
    },
  },
};

7.性能优化

7.1 持久缓存

现在有一个文件系统缓存,它是可选的,可以通过以下配置启用。

module.exports = {
  cache: {
    // 1. 将缓存类型设置为文件系统
    type: 'filesystem',

    buildDependencies: {
      // 2. 将你的config 添加为 buildDependency,以便在改变 config 时获得缓存
      config: [__filename],
      // 3. 如果有其他的东西需要被构建依赖,可以在这里添加它们
      // 注意,webpack、加载器和所有从你的配置中引用的模块都会被自动添加
    },
  },
};

说明:
默认情况下,Webpack 假定 Webpack 所在的 node_modules 目录只被包管理器修改,对 node_modules 来说,哈希值和时间戳会被跳过。出于性能考虑,只使用包名和版本。只要不指定resolve.symlinks: falseSymlinks(即npm/yarn link)就没有问题(无论如何都要避免)。不要直接编辑 node_modules 中的文件,除非你用 snapshot.managedPaths: []以剔除该优化。当使用 Yarn PnP 时,webpack 假设 yarn 缓存是不可改变的(通常是这样),此时可以使用 snapshot.immutablePaths: [] 来退出这个优化。

缓存将默认存储在 node_modules/.cache/webpack(当使用 node_modules 时)或 .yarn/.cache/webpack(当使用 Yarn PnP 时)中。当所有的插件都正确处理缓存时,你可能永远都不需要手动删除它。

许多内部插件也会使用持久性缓存。例如 SourceMapDevToolPlugin (缓存 SourceMap 的生成)或 ProgressPlugin (缓存模块数量)

持久性缓存将根据使用情况自动创建多个缓存文件,以优化对缓存的读写访问。默认情况下,时间戳将用于开发模式的快照,而文件哈希将用于生产模式。文件哈希也允许在 CI 中使用持久性缓存。

7.2 编译器闲置和关闭

编译器现在需要在使用后关闭。编译器现在会进入和离开空闲状态,并且有这些状态的钩子。插件可能会使用这些钩子来做不重要的工作。(即将持久缓存缓慢地将缓存存储到磁盘上)。在编译器关闭时--所有剩余的工作应该尽可能快地完成。一个回调标志着关闭完成。

插件和它们各自的作者应该预料到,有些用户可能会忘记关闭编译器。所以,所有的工作最终也应该在空闲状态下完成。当工作正在进行时,应该防止进程退出。webpack() 用法在被传递回调时自动调用close。

7.3 文件生成

Webpack 过去总是在第一次构建时发出所有的输出文件,但在增量(观察)构建时跳过了写入未更改的文件。假设在 Webpack 运行时,没有任何其他东西改变输出文件。
增加了持久性缓存后,即使在重启 Webpack 进程时,也会有类似监听的体验。所以,现在Webpack 会检查输出目录中现有的文件,并将其内容与内存中的输出文件进行比较,只有当文件被改变时,它才会执行写入文件操作。
这只在第一次构建时进行。任何增量构建都会在运行中的 webpack 进程中生成新的资产时写入文件。

8. 重大问题

8.1 单一文件目标的代码分割

只允许启动单个文件的目标(如 node、WebWorker、electron main)现在支持运行时自动加载引导所需的依赖代码片段。因此,允许对这些目标使用 chunks: "all"optimization.runtimeChunk
请注意,如果目标的代码块加载是异步的,这使得初始评估也是异步的。当使用 output.library 时,这可能是一个问题,因为现在导出的值是一个 Promise处理后的值。

8.2 新的解析器

enhanced-resolve 更新到了 v5,有以下改进:

  • 追踪更多的依赖关系,比如丢失的文件。
  • 别名可能有多种选择
  • 现在可以别名为 false 了。
  • 支持 exports 和 imports 字段等功能。
  • 性能提高

8.3 没有 JS 的代码块

不包含 JS 代码的块,将不再生成 JS 文件。这就允许有只包含 CSS 的代码块。

9.未来计划

9.1 实验特性

在 webpack 5 中,有一个新的 experiments 配置选项,允许启用实验性功能。这使得哪些功能被启用/使用变得很清楚。

虽然 webpack 遵循语义版本化,但它会对实验性功能进行例外处理。实验性功能可能会在 webpack 的次要版本中包含破坏性的变化。当这种情况发生时,我们会在变更日志中添加一个明确的注释。这将使我们能够更快地迭代实验性功能,同时也使我们能够在主要版本上为稳定的功能停留更长时间。

并且,以下的实验功能也会随 Webpack 5 一起发布。

  • 旧的 WebAssembly 支持,就像 Webpack 4 一样 (experiments.syncWebAssembly)
  • 根据更新的规范(experiments.asyncWebAssembly),新增 WebAssembly 支持。这使得一个 WebAssembly 模块成为一个异步模块。
  • 顶层的 Await第三阶段提案(experiments.topLevelAwait)在顶层使用 await 使该模块成为一个异步模块。
  • 以模块的形式生成代码包 (experiments.outputModule)这就从代码包中移除了包装器 IIFE,执行严格模式,通过 <script type="module"> 进行懒惰加载,并在模块模式下最小化压缩。

9.2 最小 Node.js 版本

最低支持的 Node.js 版本从 6 增加到 10.13.0(LTS)。

10.内部架构变更

下面是一些Webpack 5架构方面的变更:

10.1 新的插件运行顺序

现在 webpack 5 中的插件在应用配置默认值之前就会被应用。这使得插件可以应用自己的默认值,或者作为配置预设。但这也是一个突破性的变化,因为插件在应用时不能依赖配置值的设置。
迁移:只在插件钩子中访问配置。或者最好完全避免访问配置,并通过构造函数获取选项。

10.2 运行时模块

大部分的运行时代码被移到了所谓的"运行时模块"中。这些特殊模块负责添加运行时代码。它们可以被添加到任何块中,但目前总是被添加到运行时块中。
"运行时需求"控制哪些运行时模块(或核心运行时部件)被添加到代码包中。这确保了只有使用的运行时代码才会被添加到代码包中。未来,运行时模块也可以添加到按需加载的块中,以便在需要时加载运行时代码。

在大多数情况下,核心运行代码时允许内联入口模块,而不是用 __webpack_require__ 来调用它。如果代码包中没有其他模块,则根本不需要使用__webpack_require__。这与模块合并很好地结合在一起,即多个模块被合并成一个模块。在最好的情况下,根本不需要运行时代码。

迁移:如果你在插件中注入运行时代码到 Webpack 运行时,可以考虑使用 RuntimeModules 来代替。

10.3 序列化

我们添加了一个序列化机制,以允许在 webpack 中对复杂对象进行序列化。它有一个可选的语义,所以那些应该被序列化的类需要被明确地标记出来(并且实现它们的序列化)。大多数模块、所有的依赖关系和一些错误都已经这样做了。

迁移:当使用自定义模块或依赖关系时,建议将它们实现成可序列化的,以便从持久化缓存中获益。

10.4 用于缓存的插件

增加了一个带有插件接口的 Cache 类。该类可用于写入和读取缓存。根据配置的不同,不同的插件可以为缓存添加功能。MemoryCachePlugin 增加了内存缓存功能。FileCachePlugin 增加了持久性(文件系统)缓存。FileCachePlugin 使用序列化机制将缓存项目持久化到磁盘上或从磁盘上恢复。

10.5 Tapable 插件升级

Webpack 3 插件的 compat 层已经被移除,并且它在 Webpack 4 中已经被取消了,一些较少使用的 tapable API 被删除或废弃。

10.6 Main/Chunk/ModuleTemplate 废弃

打包模板已经重构。MainTemplate/ChunkTemplate/ModuleTemplate 被废弃,现在 JavascriptModulesPlugin 负责 JS 模板。

在那次重构之前,JS 输出由 Main/ChunkTemplate 处理,而另一个输出(即 WASM、CSS)则由插件处理。重构后这一点被改变了,所有的输出都由他们的插件处理。

依然可以侵入部分模板。钩子现在在 JavascriptModulesPlugin 中,而不是 Main/ChunkTemplate 中。(是的,插件也可以有钩子,我称之为附加钩子。)有一个兼容层,所以 Main/Chunk/ModuleTemplate 仍然存在,但只是将 tap 调用委托给新的钩子位置。

10.7 入口文件的新增配置

在 Webpack 5 中,入口文件除了字符串、字符串数组,也可以使用描述符进行配置了,如下所示。

module.exports = {
  entry: {
    catalog: {
      import: './catalog.js',
    },
  },
};

此外,我们也可以定义输出的文件名,之前都是通过 output.filename 进行定义的,如下所示。

module.exports = {
  entry: {
    about: { import: './about.js', filename: 'pages/[name][ext]' },
  },
};

另外,入口文件的配置,新增了文件依赖定义、生成类库的格式类型(commonjs 或 amd),也可以设置运行时的名字,以及代码块加载的方式,更多细节可以参考完整的发布记录。

10.8 排序与ID

Webpack 曾经在编译阶段以特定的方式对模块和代码块进行排序,以递增的方式分配 ID。现在不再是这样了。顺序将不再用于 ID 的生成,取而代之的是,ID 的生成完全由插件进行控制,并且优化模块和代码块顺序的钩子已经被移除。

10.9 从数组到集合(Set)

  • Compilation.modules 现在是一个集合
  • Compilation.chunks 现在是一个集合
  • Chunk.files 现在是一个集合

10.10 文件系统与信息变更

在Webpack 5 中,一个是需要使用 Compilation.fileSystemInfo 替代 file/contextTimestamps,获取文件的时间戳信息,另一个是新增Compiler.modifiedFiles 以便更容易引用更改后的文件。
另外,还新增了一个类似于 compiler.inputFileSystemcompiler.outputFileSystem 的新 API compiler.intermediateFileSystem,用于所有不被认为是输入或输出的 fs 操作,如写入 records,缓存或输出 profiling。

10.11 模块热替换

HMR 运行时已被重构为运行时模块。HotUpdateChunkTemplate 已被合并入 ChunkTemplate 中。ChunkTemplates 和 plugins 也应处理 HotUpdateChunk 了。

HMR 运行时的 Javascript 部分已从核心 HMR 运行时钟分离了出来。其他模块类型现在也可以使用它们自己的方式处理 HMR。在未来,这将使得 HMR 处理诸如 mini-css-extract-plugin 或 WASM 模块。

迁移:此为新功能,无需迁移。
import.meta.webpackHot 公开了与 module.hot 相同的 API。当然可以在 ESM 模块(.mjs,package.json 中的 type: "module")中使用,这些模块不能访问 module。

10.12 工作队列

Webpack 曾经通过函数调用函数的形式来进行模块处理,还有一个 semaphore 选项限制并行性。Compilation.semaphore 已被移除,现在可以使用异步队列处理,每个步骤都有独立的队列:

  • Compilation.factorizeQueue:为一组 dependencies 调用模块工厂。
  • Compilation.addModuleQueue:将模块添加到编译队列中(可以使用缓存恢复模块)
  • Compilation.buildQueue:必要时构建模块(可将模块存储到缓存中)
  • Compilation.rebuildQueue:如需手动触发,则会重新构建模块
  • Compilation.processDependenciesQueue:处理模块的 dependencies。

这些队列会有一些 hook 来监听并拦截工作的进程。未来,多个编译器会同时工作,可以通过拦截这些队列来进行编译工作的编排。

10.13 模块和 chunk 图

Webpack 曾经在依赖关系中存储了已解析的模块,并在 chunk 中存储引入的模块。但现已发生变化。所有关于模块在模块图中如何连接的信息,现在都存储在 ModulGraph 的 class 中。所有关于模块与 chunk 如何连接的信息现在都已存储在 ChunkGraph 的 class 中。依赖于 chunk 图的信息也存储在相关的 class 中。

以下列举一些模块的信息已被移动的例子:

  • Module connections -> ModuleGraph
  • Module issuer -> ModuleGraph
  • Module optimization bailout -> ModuleGraph (TODO: check if it should ChunkGraph instead)

当从缓存中恢复模块时,Webpack 会将模块从图中断开。现在已无需这么做。一个模块不存储图形的任何信息,技术上可以在多个图形中使用。这会使得缓存变得更加容易。这部分变化中大多数都有一个适配层,当使用时,它会打印一个弃用警告。

10.14 模块 Source Types

现在,模块必须通过 Module.getSourceTypes() 来定义它们支持的源码类型。根据这一点,不同的插件会用这些类型调用 source()。对于源类型为 Javascript 的 JavascriptModulesPlugin 会将源代码嵌入到 bundle 中。源类型 Webassembly 的 WebAssemblyModulesPlugin 会 emit 一个 wasm 文件。同时,也支持自定义源类型,例如,mini-css-extract-plugin 会使用源类型为 stylesheet 将源码嵌入到 css 文件中。

模块类型与源类型间没有关系。即使模块类型为 Json,也可以使用源类型为 Javascript 和模块类型为 webassembly/experimental 的 Javascript 和 Webassembly。

10.15 全新的观察者

Webpack 所使用的观察者已重构。它之前使用的是 chokidar 和原生依赖 fsevents(仅在 OSX 上)。现在它在只基于原生的 Node.js 中的 fs,这意味着在 webpack 中已经没有原生依赖了。

它还能在监听时捕捉更多关于文件系统的信息。目前,它还可以捕获 mtimes 和监视事件时间,以及丢失文件的信息。为此,WatchFileSystem API 做了一点小改动。在修改的同时,我们还将 Arrays 转换为 Sets,Objects 转换为 Maps。

10.16 SizeOnlySource after emit

Webpack 现在使用 SizeOnlySource 替换 Compilation.assets 中的 Sources,以减少内存占用情况。

10.17 ExportsInfo

重构了模块导出信息的存储方式。ModuleGraph 现在为每个 Module 提供了一个 ExportsInfo,它用于存储每个 export 的信息。如果模块仅以副作用的方式使用,它还存储了关于未知 export 的信息,并且每个 export都会存储以下信息:

  • 是否使用 export? 是否使用并不确定。
  • 是否提供 export? 是否提供并不确定。
  • 能否重命名 export 名? 是否重命名,也不确定
  • 如果 export 已重新命名,则为新名称。
  • 嵌套的 ExportsInfo,如果 export 是一个含有附加信息的对象,那么它本身就是一个对象。

10.18 代码生成阶段

编译的代码生成功能作为单独的编译阶段。它不再隐藏在 Module.source() 和 Module.getRuntimeRequirements() 中运行了。这应该会使得流程更加简洁。它还运行报告该阶段的进度。并使得代码生成在剖析时更加清晰可见。

迁移时,Module.source()Module.getRuntimeRequirements() 已弃用,然后使用 Module.codeGeneration() 代替。

10.19 依赖关系参考

Webpack 曾经有一个单一的方法和类型来表示依赖关系的引用(Compilation.getDependencyReference 会返回一个 DependencyReference)该类型用于引入关于该引用的所有信息,如 被引用的模块,已经引入了哪些 export,如果是弱引用,还需要订阅一些相关信息。把所有这些信息构建在一起,拿到参考的成本就很高,而且很频繁(每次有人需要一个信息)。

在 Webpack5 中,这部分代码库被重构了,方法进行了拆分:

  • 引用的模块可以从 ModuleGraphConnection 中读取
  • 引入的导出名,可以通过 Dependency.getReferencedExports() 获取
  • Dependency 的 class 上会有一个 weak 的 flag
  • 排序只与 HarmonyImportDependencies 相关,可以通过 sourceOrder 属性获取

10.20 Presentational Dependencies

这是 NormalModules 的一种新 Dependencies 类型:Presentational Dependencies。这些 dependencies 只在代码生成阶段使用,但在模块图构建过程中未使用。所以它们永远不能引用模块或影响导出/导入。这些依赖关系的处理成本较低,Webpack 会尽可能地使用它们。

10.21 弃用 loaders

null-loader已被弃用,可以使用下面的写法进行替换。

module.exports = {
  resolve: {
    alias: {
      xyz$: false,
    },
  },
};
// 或者使用绝对路径
module.exports = {
  resolve: {
    alias: {
      [path.resolve(__dirname, '....')]: false,
    },
  },
};

总的来说,Webpack 5 的大部分工作都围绕优化进行展开,去除了 Webpck 4 中有废弃的内容,新增了长期缓存,优化了内核等内容。

参考文档:中文文档:webpack.docschina.org

查看原文

赞 13 收藏 7 评论 0

认证与成就

  • 获得 326 次点赞
  • 获得 12 枚徽章 获得 1 枚金徽章, 获得 4 枚银徽章, 获得 7 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2013-02-03
个人主页被 3.6k 人浏览