Chor

Chor 查看完整档案

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

个人动态

Chor 发布了文章 · 2月17日

Tailwind CSS (可能)是名过其实的

Tailwind CSS 是一个工具集 CSS 框架,网上很多文章已对其有详尽的介绍。本文不是官方文档的复述,也不是系列优点的罗列,作者 Gerard 会从另一个角度出发,在尽力保持客观的前提下,立足于实际开发的场景,指出 Tailwind CSS 存在的一些问题。事实上,除了文中提及的,Tailwind CSS 还存在着不少缺点,比如对高度定制化的支持程度不足、记忆大量预定义类名带来的心智负担等。

友情提醒,你不一定会赞同这篇文章的看法,毕竟我们的看法总会受到自身认知和使用体验的影响,但更重要的是可能是作者对新兴技术的态度,用他的原话说,就是:“When everyone is shouting that it’s awesome, it’s usually a good moment to sit down and have a good look at it”

下面正文开始。

引言

在过去的两年间,Tailwind CSS广受青睐(从每周 30K 的下载量到如今的 600K)。毫无疑问,这个流行的实用优先 CSS 框架具备诸多优点。很可能你对它的惊艳和强大早有耳闻,因为很多开发者正是这么想的。

但关于这个框架,我们还有很多要说的。

什么是 Tailwind CSS?

如果你从来没见过 Tailwind 的实际应用,可以看这个:

<div class="bg-gray-100 rounded-xl p-8">Hello World</div>

这里的类名就反映了 Tailwind 的定义:一个包含多个预定义类(所谓的工具类)的集合。你并不需要编写基础的 CSS 样式规则,只需要直接在 HTML 中应用已经事先定义好的类名。

这样的类名还有很多。下面这个列表展示了部分类别和对应的例子:

  • 背景 (bg-gray-200, bg-gradient-to-bl)
  • 弹性布局 (flex-1, flex-row)
  • 网格布局 (grid-cols-1, col-span-4)
  • 内边距 (p-0, p-1)
  • 尺寸 (w-1, h-1)

之前有人将这个预定义类的集合比作可以在代码中使用的“乐高积木”。当然,它与传统的 CSS 有很多重复之处,但却不止于此:比如它还包括了预定义的范围(bg-red-100, bg-red-200 等)。Tailwind 旨在让我们的开发事半功倍,从某个角度来说,它也确实做到了。

同时,我很喜欢这个名字:Tailwind(顺风)。我闲下来的时候会驾船出海,顺风可是个不错的东西。

语法

正如上面所展示的,我们直接在 HTML 中书写工具类名。我们会很快想到这和内联 CSS 是非常相似的,这或许也能解释为什么 Tailwind 的开发者会在文档的开头部分就提及这个问题。

虽然他们极力解释,称 Tailwind 瑕不掩瑜(我不否认它确实有诸多优点),但我还是不太认可它的语法。我不想用一大堆类名污染 HTML 结构中的每一个元素,也不想每天都面对这样的代码:

Image for post

注意:上面这段代码来自 Tailwind 的文档,所做的事情是渲染一个简单的卡片。事实上,它最后呈现的效果非常漂亮,甚至还是响应式的。但如果放眼于我们的日常开发,这种情况就会急速恶化:如果我正在开发一个比卡片复杂更多的组件呢?如果我必须遵循设计师提出的某种设计风格,以及忍受他的一些“小怪癖”呢?

我尝试去应付这种情况,结果也在意料之中 —— 每一个 HTML 元素都充斥着一大堆 Tailwind 的工具类名。官方文档和教学视频并不会告诉你这些,但在我们着手开发大型应用的时候,这会是一个真实摆在我们面前的问题:

// 一大堆类名
<div class="sm:w-4 md:w-6 lg:w-10 xl:w-full sm:text-sm md:text-base lg:text-base xl:text-2xl flex-1 sm:flex-none bg-black sm:bg-white rounded-md sm:rounded-none">Hello World</div>

这也是实际开发中不可避免的。

上面这个例子可不夸张,我甚至可以说它是一个最简化的例子了 —— 至少对于那些有明确要求、明确设计风格(基于不同屏幕尺寸作出的响应式变化和样式调整)的应用来说,是这样的。

那么要怎么组织这些类名呢?也许我们要创建并遵循某个排序规则,但这样实在太复杂了。另一种做法是允许模板设计者和开发者使用任意一种具体的排序,但这样一来,为了找到要修改的目标类名。我们就不得不水平扫视甚至是滚动查看代码。

我可不想像找威利一样去找元素的字号(译者注:威利是儿童书籍《威利在哪里》中的人物,读者需要在一张人山人海的图片中找到威利)

我的观点是,部分 HTML 元素会使用非常多的样式,这种情况下应该考虑将样式与 HTML 标签进行分离,单独放到某个文件里。这样,我们就可以组织样式并增强其可读性。你不能把 CSS 的所有功能”塞到“ class 这一个 HTML 标签属性里,Tailwind 也不能。这样做只会让 HTML 结构越发臃肿。

@apply

针对上面提到的问题,Tailwind 允许我们在单个 CSS 文件中使用它们的类名:

.header {
  @apply bg-red-200 w-4 text-gray-400 rounded-sm border-red-400 border-2;
}

但比起传统编写 CSS(或者 SASS 等其它预处理器)的方式,我看不出这样做有什么优点。你可以说我是“老古董”,但我确实更喜欢下面这种编写方式:

.header {
  background-color: #FECACA;
  width: 200px;
  color: #444;
  border-radius: 5px;
  border: 2px solid #F87171;
}

再次强调,在真实开发中,元素可能会应用非常多的样式。

我并没有对 Tailwind 的优点避而不谈,其提供的部分工具类一定有更多用处亟待探索。但谈及语法的时候,我还是希望标记语言(HTML) 和样式规则可以进行明确的分离。我想,这是一个主观的看法。

清除无用代码

在项目中引入 Tailwind 之后,所有的类名都是可用的。但在构建和打包项目的时候,我们显然并不需要用上所有类名。因此,Tailwind 使用了 PurgeCSS 这个工具:

这就是 PurgeCSS 发挥作用的地方。PurgeCSS 会分析你的内容和 css 文件,首先它将 css 文件中使用的选择器与内容文件中的选择器进行匹配,然后它会从 css 中删除未使用的选择器,从而生成更小的 css 文件。

简单总结一下:首先,我们为项目引入大量的工具类名,接着,在准备构建并发布项目的时候,使用一个工具扫描代码并找出所有未使用的类名,以确保它们不会随其它代码一起打包。其实现依赖于下面这个正则表达式:

Image for post

引入并使用 Tailwind 会给我们的项目平添一层复杂性,复杂性带来的是一定的风险,会给我们的开发造成麻烦。比如说:

render(
  myItems.map(item => (
    <div className={`item level-${item.level}`}>
      {item.text}
    </div>
  ));
);

像这样动态生成类名的操作是做不到的。这意味着 Tailwind 对我们的开发造成了限制。关于这一点,文档也有提到,但很容易被开发者忽略:

Image for post

字符串拼接的操作是不允许的。

开发上的限制是一方面,还有一个问题是:给项目增加一层复杂性,通常会给项目带来风险。

这最终变成了一个关乎判断的问题,即 Tailwind 是否利大于弊?项目不同,对这个问题的回答也不同,但我们至少得留意到它存在的问题。关于 Tailwind 带来的限制性,上面提到的问题只是冰山一角。可以再举一个例子,那就是给 Tailwind 项目添加额外的(自定义的)CSS 并不那么简单直接

替代品

在阅读了 Tailwind 的文档并上手开发了几天之后,我忍不住在想:作者并没有意识到我们中的大多数人已经在日常开发中使用其它工具来简化样式编写了。

文档说过,使用 Tailwind 的一个好处在于可以避免魔数(译者注:魔数指的是缺乏解释或命名的独特数值,出现多次,且可以被有名字的常量取代)。确实如此,这是它的一个优点:我们定义一个诸如 bg-red-200 的颜色工具类,之后可以在代码各处使用,并在一个地方(Tailwind 的配置文件)集中修改它的实际值。这还是挺香的,我相信你也同意这种做法。

但今天的工具,比如说 SASS (周下载量超过五百万),早就可以轻松创建工具类和变量并在代码中重用了。甚至原生的 CSS 也已经支持使用变量。

当我们使用 SASS 或者原生 CSS 的时候,我们不需要面对额外的一层复杂性,在编写 CSS 样式规则的时候,也不需要改变既已形成的习惯和语法。

使用 Tailwind 是有成本的。花费时间和精力学习 Tailwind 的语法和类名,你会逐渐忘记其背后的语法:也即原生 CSS 的语法。如果我的开发者在一个更大的项目中使用 Tailwind 长达一年,他们将会逐渐忘记原生 CSS。这种事态真的乐观吗?我不太确定。

后序

Tailwind 很流行,它的吸引力和追捧者与日俱增。我能理解这其中的原因,毕竟使用它真的可以让我们受益匪浅。对其优点我也表示认可,它的一些工具类可以发挥很大的作用。

我撰写本文的目的,在于向各位展示一个事实:故事总是有两面性的。

一些人会从这个框架中受益,但还有一些人则会受限,他们会在开发的过程中不断发现这些限制 —— 或者更糟,在开发后才发现。

在适应新框架的时候,请保持你的批判性。当“每个人”都高声惊呼其惊艳的时候,也许正是冷静坐下、仔细端详的最佳时机。

感谢你花费时间阅读本文!

查看原文

赞 3 收藏 1 评论 1

Chor 发布了文章 · 2月9日

30 个案例教你用纯 CSS 实现常见的几何图形

本文会介绍一些常见几何图形的 CSS 绘制方案,思路参考自 The shapes of CSS 一文以及网上的其它文章,部分地方会做适当的修改和补充。

1. 三角形

传统 border 实现

我们知道,如果设置一个盒子的宽高为 0,盒子就会变成一个点。此时再给上下左右四个 border 一定的宽度和不同的颜色,那么单纯由 border 填充的盒子看起来是这样的:

但内容盒有宽高的时候,四个 border 的对接处就不是一个点,而是一个矩形(图中白色区域),这时候的border 看起来就会和现实中的边框差不多:

因此,要绘制三角形,核心就是设置盒子的宽高为 0,让 border 表现为一个三角形:

.delta {
    width: 0px;
    height: 0px;
    border: 50px solid;
    border-color:  #43cea2 #185a9d #ff6b6b #83a4d4;
}

当然,也可以给盒子一个宽高,只不过这时候我们就要将其设置为 IE 盒模型(box-sizing:border-box),以确保在始终保持盒子原尺寸的前提下,通过增加 border 的宽度使 border 向盒子内部聚拢:

.delta {
    width: 100px;
    height: 100px;
    box-sizing: border-box;
    border: 50px solid;
    border-color:  #43cea2 #185a9d #ff6b6b #83a4d4;
}

上面两种方法效果一样,接着根据实际情况隐藏其中三个三角形即可。我们这里只想要显示底下的三角形,所以其它三角形通过透明色隐藏,顶部的三角形则设置 border 宽度为 0 ,避免占用空间。

HTML:

<div class="delta"></div>

CSS:

.delta {
    width: 0px;
    height: 0px;
    border: 50px solid transparent;
    border-bottom-color: #ff6b6b;
    border-top-width: 0px;
}

效果如下:

源代码:codepen1

渐变实现

这种方法适合绘制直角三角形。假设要绘制一个红色的直角三角形,其实可以把它看作是一个矩形的一半,矩形从透明色渐变到红色:

div {
    width: 200px;
    height: 200px;
    background: linear-gradient(to right bottom,transparent 0%,transparent 50%,red 50%,red 100%);
}
svg 实现
<svg width="100" height="100">
  <polygon points="50,0 100,100 0,100" style="fill: red;"/>
</svg>
clip-path 实现
div {
    width: 100px; 
    height: 100px;
    background: red;
    clip-path: polygon(50% 0, 0 100%, 100% 100%);
}
transform 和 overflow 实现

假设要绘制这个三角形:

先试着把他的另一半补齐:

可以想象成这其实是由两个盒子上下堆叠而成的,下面的是绿色盒子,上面的是蓝色盒子,这个蓝色盒子倾斜摆放,并且超出绿色盒子的部分被隐藏了。因此最初可能是类似这样的:

那么反过来想一下,假设我们能够实现上图这个样式,接着设法把蓝色盒子超出绿色盒子的部分隐藏,并把绿色盒子设置为透明色,是不是就实现了最初的那个三角形了呢?

从布局上,我们考虑绿色盒子是相对定位的父元素,蓝色盒子是子元素(用伪元素来做),并且在绝对定位和 transform 的作用下,实现图中的效果。接下来我们要解决几个问题:

  • 蓝色盒子和绿色盒子在宽高上的关系?从图中可以看出,蓝色盒子的边长基本等于绿色盒子的对角线长度。绿色盒子宽高都是 100px,因此对角线是 100√2 px,越等于 140px,因此蓝色盒子的边长就是 140px,也就是父盒子宽高的 140%。
  • 蓝色盒子的绝对定位的 left 偏移多少?在绝对定位的设置上,我们可以让蓝色盒子并排放在绿色盒子左边,并且两者底边在同一条线上
  • 蓝色盒子旋转多少度?从图中的几何关系很容易看出,经过上面绝对定位的设置之后,蓝色盒子应该再旋转45 度才能到达图中位置。不过这里要注意,蓝色盒子不是绕着自己的中心旋转的,而是绕着自己的右下角顶点旋转的,因此这里还得修改 transform-origin 的值

最后,还要把超出绿色盒子的部分隐藏,并且把绿色盒子的颜色设置为透明色。因此最终代码如下:

div {
    width: 100px; 
    height: 100px;
    background: transparent;
    position: relative;
    overflow: hidden;
}
div::after {
    content: '';
    width: 140%;
    height: 140%;
    background-color: #185a9d;
    position: absolute;
    left: -140%;
    bottom: 0;
    transform-origin: right bottom;
    transform: rotate(45deg) ;
}    

源代码:codepen2

字符实现
div::after {
    content:'\25b3'
    color:red
    font-size: 60px    
}

基本不实用,因为无法设置背景颜色,也无法设置三角形的形状。

多矩形堆叠实现

HTML:

<div class="container">
  <div style="width: 1px"></div>
  <div style="width: 2px"></div>
  <div style="width: 3px"></div>
  <div style="width: 4px"></div>
  <div style="width: 5px"></div>
  <div style="width: 6px"></div>
</div>

CSS:

.container {
    display:flex;
    flex-direction: column;
    align-items: center;
    div {
        height:1px;
        background: red;
    }    
}

三角形可以看作是多个宽度递增、高度极小的矩形自上而下堆叠在一起。这段代码绘制的图形接近于正三角形,如果要绘制直角三角形,可以设置 align-items: start,让矩形一致往左边靠拢。

这个方法基本也不实用,首先它会增加多余的 dom 结构,其次是这样的图形存在锯齿,不够美观。—— 要减少锯齿,我们可以尝试继续压缩矩形的高度,但这样意味着需要使用更多的 dom 来绘制出同等高度的三角形。

2. 椭圆形

普通椭圆

椭圆的实现依靠的是 border-radius,因此有必要搞懂 border-radius 的具体含义。

通常情况下,想要设置一个 div 的圆角,我们可能会写出类似这样的代码:

border-radius: 12px

它实际上等价于下面的代码:

border-top-left-radius: 12px 12px
border-top-right-radius: 12px 12px
border-bottom-left-radius: 12px 12px
border-bottom-left-radius: 12px 12px

也就是说,我们把 div 的左上角、右上角、左下角、右下角的某两个值都设置为 12px,那么这两个值是什么呢?它们其实指的是这四个角各自的水平半径和垂直半径。在这个例子中,我们的四个圆角,实际上都是一个半径为 12px 的圆的 1/4 弧。

这样我们也能理解圆的形成了。对于一个 100px * 100px 的 div,设置 border-radius:50%,就等于设置 div 四个角的水平半径为 div 宽的一半,垂直半径为 div 高的一半,这样形成的图形一定会是一个圆形。

同理,对于一个 200px * 100px 的 div,设置 border-radius:50%,就等于设置四个角的水平半径为 div 宽的一半,垂直半径为 div 高的一半,这样形成的图形一定会是一个椭圆形。

代码如下:

div {
  width: 200px; 
  height: 100px;
  background: #185a9d;
  border-radius: 50%;
}
特殊椭圆(鸡蛋形)

实现方式和普通椭圆类似。特点在于上半部分比下半部分要更加扁平,因此左上角和右上角圆角的垂直半径要更长,这里取整体高度的 60%,剩余的 40% 作为左下角和右下角圆角的垂直半径。代码如下:

HTML:

<div class="shape"></div>

CSS:

.shape {
  width: 126px;
  height: 180px;
  background: #36d792;
  border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
}

源代码:codepen3

3. 扇形

半圆

  • border-radius 实现:

    先画一个长度为宽度两倍的矩形,再给左上角和右上角设置圆角即可(圆角半径等于宽度)

    .shape {
      width: 200px;
      height:100px;
      background: #ff6b6b;
      border-radius:100px 100px 0 0;
    }
  • overflow:hidden 实现:

    让圆形的一半超出父元素并给父元素设置溢出隐藏

    .shape {
      width: 200px;
      height: 100px;  
      overflow: hidden;
      position: relative;
    }
    .shape::after {
      content:'';
      position: absolute;
      width: 200px;
      height: 200px;
      background: #ff6b6b;
      border-radius: 50%;
    }
  • 背景渐变实现(看上去是半圆):

    可以将半圆看作是一个圆形从有颜色渐变到透明色

    .shape {
      width: 200px;
      height:200px;
      border-radius: 50%;
      background-image: linear-gradient(to bottom,#ff6b6b 50%,transparent 50%) ;  
    }
  • border-left 实现(看上去是半圆):

    .shape {
      width: 200px;
      height: 100px;  
      border-top: 100px solid #ff6b6b;
      border-radius: 50%;
    }
1/4 圆

  • border-radius实现:

    .shape {
      width: 120px;
      height: 120px;  
      background: #ff6b6b;
      border-top-left-radius: 100%;
    }
  • border 实现:

    类似于三角形的实现

    .shape {
      width: 0px;
      height: 0px;  
      border: 100px solid transparent;
      border-top-color: #ff6b6b;
      border-radius: 100px;
      transform: rotate(-45deg);
    }
  • overflow:hidden 实现

    .shape {
      width: 100px;
      height: 100px;  
      overflow: hidden;
      position: relative;
    }
    .shape::after {
      content:'';
      position: absolute;
      width: 200px;
      height: 200px;
      background: #ff6b6b;
      border-radius: 50%;
    }
任意扇形
  • 三角形 + 圆形 + 溢出隐藏 实现:

    利用之前 border 实现三角形的方法,我们可以实现如下图所示、与圆心对准的的矩形。给圆形设置透明色和溢出隐藏,并且消除掉矩形不想显示的 border,就能得到扇形了。扇形的圆心角大小由 border-left-widthborder-right-width 共同控制,这两者越大,圆心角也就越大 —— 但最大也只能是 180 度。

    代码如下:

    HTML:

    <div class="shape"></div>

    CSS:

    .shape {
      width: 200px;
      height: 200px;
      border-radius: 50%;
      overflow: hidden;
      position: relative;
    }
    .shape::after {
      content:'';
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%,-50%);
      width: 0;
      height: 0;
      border-left: 200px solid transparent;
      border-right: 200px solid transparent;
      border-top: 100px solid #ff6b6b;
      border-bottom: 100px solid transparent;  
    }

    源代码:codepen4

  • 矩形 + 半圆 + 溢出隐藏 实现:

    想象一下有一个绿色矩形,下面有一个直径与矩形长度相等的红色半圆,让半圆绕着圆心旋转,在这个过程中,绿色区域里面是不是就有一个角度不断变化的扇形呢?如下图所示:

    因此,我们只要把绿色矩形设置为透明色,同时加上溢出隐藏的效果,就能通过改变半圆旋转的角度,在矩形内部形成一个扇形了。代码如下:

    HTML:

    <div class="shape"></div>

    CSS:

    .shape {
      width: 200px;
      height: 100px;
      position: relative;
      overflow: hidden;
    }
    .shape::after {
      content:'';
      position: absolute;
      width: 100%;
      height: 100%;
      top: 100%;
      background: #ff6b6b;
      border-radius: 0 0 50% 50% / 0 0 100% 100%;
      transform-origin: top center;
      transform: rotate(30deg); 
    }

    效果如下:

    源代码:codepen5

    不过有个问题:这种方法实现的扇形,圆心角不能超过 180 度,那么对于圆心角大于 180 度的扇形怎么实现呢?我们改用两个半圆实现。

  • 两个半圆实现:

    一开始两个半圆是叠放在一起的,通过让上面的半圆绕着圆心旋转,就可以形成圆心角大于 180 度的扇形。如下图所示:

代码如下:

HTML:

<div class="shape"></div>

CSS:

.shape {
  width: 200px;
  height: 100px;
  background: #36d793;
  border-radius: 50% 50% 0 0 / 100% 100% 0 0;
  position: relative;
}
.shape::before{
  content:'';
  position: absolute;
  background: #36d793;
  width: 200px;
  height: 100px;
  border-radius: 50% 50% 0 0 / 100% 100% 0 0;
  transform-origin: bottom center;
  transform: rotate(30deg);
}

源代码:codepen6

4. 梯形

border 实现

前面说过,如果设置一个盒子宽高为 0,并给一定的 border,那么这个盒子看起来是这样的:

在此基础上,如果给这个盒子一个宽度,会发生什么事呢?盒子就会变成这样:

实际上这也是符合我们的直觉的,可以想象成是四个三角形的交接点被横向拉长了,三角形也跟着变化。现在这个图形已经包含梯形了,那么我们接下来的事情就很简单了,只需要把没用到的分块设置成透明色即可,因此最终代码如下:

HTML:

<div class="shape"></div>

CSS:

.delta {
    width: 100px;
    height: 0px;
    border: 50px solid transparent;
    border-bottom-color: #ff6b6b;
}

最终效果如下:

源代码:codepen7

  • 梯形的大小:

    通过设置 div.delta 的宽度,可以同时修改梯形的上下底长度;通过设置 border-left-widthborder-right-width 可以修改底角大小,border 越宽,底角越小。另外还可以设置 border-bottom-width,从而控制梯形的高度。有时候,我们可能希望一切的变化都是在确保原 div 大小不变的情况下进行的,这时候可以给 div 设置 box-sizing:border-box

  • 梯形的方向:

    现在我们的梯形是朝上或者朝下的,如果想要设置方向为朝左或者朝右,可以纵向拉长对接点,也即保持 div 宽度为 0 的同时,给它一定的高度。

  • 直角梯形:

    这里只以一个方向为例进行介绍。对于下面这张图:

    如果我们把它的 border-left-width 进行压缩,那么红色梯形的顶角就会往左边拖动,趋近于九十度:

    当压缩到为 0 的时候,原先的等腰梯形就变成了如下图所示的直角梯形:

矩形+三角形实现

等腰梯形也可以看作是由一个矩形 + 两个直角三角形组成(其它梯形同理):

代码如下:

.shape {
    width: 200px;
    height: 120px;
    background-color: #36d792;
    position: relative;
}
.shape::before,.shape::after {
    content:'';
    position: absolute;
    width: 0;
    height: 0;
    border-top: 60px solid white;            
    border-bottom: 60px solid transparent;
}
.shape::before {
    border-left: 20px solid white;
    border-right: 20px solid transparent;
    left: 0;
}
.shape::after {
    border-left: 20px solid transparent;
    border-right: 20px solid white;
    right: 0;
}    

源代码:codepen8

5. 平行四边形

这个比较简单,直接通过 transform:skew() 对矩形做倾斜处理即可。代码如下:

.shape {
    width: 200px;
    height: 120px;
    background-color: #36d792;
    transform: skew(20deg);
}

6. 五角星

实际上,五角星可以看作是由三个三角形拼接而成的:

长度的设置:

  • 为了方便后续计算,这里设法让顶部三角形的腰长为 130px,底长为 100px。即:设置顶部三角形的 border-bottom-width为 120px,,border-left-widthborder-right-width 都为 50px。
  • 下面两个三角形也都是等腰三角形,需要利用几何关系计算各边长度。显然,只要知道等腰三角形的面积和底边,那么就能推算出它的高,而高其实就是 border-bottom-width 。三角形各个边的边长为 130+50*2130+50*2(130+50)*2,根据海伦公式,可以由三边计算出一个三角形的面积,又由于底边已知,所以算出高为 143px,也即 border-bottom-width 为 143px,而 border-leftborder-right 的宽度则为三角形底边的一半,也就是 180px。这样,两个三角形都能绘制出来了。
  • 剩下的工作就是调整绝对定位的偏移量以及两个三角形旋转的角度。由于计算的偏差问题,这里得到的并不是标准的五角星,但总体思路是这样。

最终代码如下:

.shape {
    width: 0;
    height: 0;
    border-left: 50px solid transparent;
    border-right: 50px solid transparent;
    border-bottom: 120px solid #36d792;
    position: relative;
}
.shape::before {
    content: '';
    position: absolute;
    width: 0;
    height: 0;
    border-left: 200px solid transparent;
    border-right: 200px solid transparent;
    border-bottom: 143px solid #36d792;    
    top: 106px;
    left: -192px;
    transform: rotate(36deg);        
}
.shape::after {
    content: '';
    position: absolute;
    width: 0;
    height: 0;
    border-left: 200px solid transparent;
    border-right: 200px solid transparent;
    border-bottom: 143px solid #36d792;    
    top: 106px;
    left: -206px;
    transform: rotate(-36deg);        
}

源代码:codepen9

7. 六角星

六角星用一个正三角形和一个倒三角形来做即可:

代码如下:

.shape {
    width: 0;
    height: 0;
    border: 50px solid transparent;
    border-bottom: 80px solid #36d792;
    position: relative;
}
.shape::after {
    content: '';
    position: absolute;
    width: 0;
    height: 0;
    border: 50px solid transparent;
    border-top: 80px solid #36d792;
    top: 30px;
    left: -50px;
}

源代码:codepen10

8. 八角星

用两个矩形来做即可,其中一个矩形绕中心旋转 45 度就可以形成八角星。代码如下:

HTML:

<div class="shape"></div>

CSS:

.shape {
  width:80px;
  height:80px;
  background: #36d792;
  position: relative;
}
.shape::after {
  content:'';
  position: absolute;
  width:80px;
  height:80px;
  background: #36d792;
  transform: rotate(45deg);
}

源代码:codepen11

9. 十二角星

十二角星的做法和八角星类似,只是多了一个矩形以及旋转的角度不同而已。代码如下:

HTML:

<div class="shape"></div>

CSS:

.shape {
  width:80px;
  height:80px;
  background: #36d792;
  position: relative;
}
.shape::before,.shape::after {
  content:'';
  position: absolute;
  width:80px;
  height:80px;
  background: #36d792;
}
.shape::before {
    transform: rotate(30deg);
}
.shape::after {
    transform: rotate(-30deg);
}

源代码:codepen12

10. 五边形

为了避免繁琐的运算,这里采用口诀“九五顶五九,八五两边分”设置五边形的相关长度:

五边形看作是一个等腰三角形 + 等腰梯形即可,最终代码如下:

HTML:

<div class="shape"></div>

CSS:

.shape {
  width:0;
  height:0;
  border: 80px solid transparent;
  border-top-width: 0px;
  border-bottom: 59px solid #36d792;
  position: relative;
}
.shape::after {
  content:'';
  position: absolute;
  width:100px;
  height:0;
  border: 30px solid transparent;
  border-bottom-width:0px;
  border-top: 95px solid #36d792;
  left:-80px;
  top:59px;
}

源代码:codepen13

11. 六边形

六边形可以看作是由两个等腰三角形 + 一个矩形组成,也可以看作由两个等腰梯形组成,这里选择第二种。代码如下:

HTML:

<div class="shape"></div>

CSS:

.shape {
  width:120px;
  height:0;
  border: 60px solid transparent;
  border-top-width: 0px;
  border-bottom: 104px solid #36d792;
  position: relative;
}
.shape::after {
  content:'';
  position: absolute;
  width:120px;
  height:0;
  border: 60px solid transparent;
  border-bottom-width:0px;
  border-top: 104px solid #36d792;
  left:-60px;
  top:104px;
}

源代码:[codepen14]()

12. 八边形

正八边形可以看作由两个等腰梯形 + 一个矩形组成,代码如下:

HTML:

<div class="shape"></div>

CSS:

.shape {
  width:102px;
  height:42px;
  background: #36d792;
  position: relative;
}
.shape::before,.shape::after {
  content:'';
  position: absolute;
  width:42px;
  height:0px;
  border: 30px solid transparent;
}
.shape::before {
  border-top-width:0px;
  border-bottom: 30px solid #36d792;
  top:-30px;
}
.shape::after {
  border-bottom-width:0px;
  border-top: 30px solid #36d792;
  top:42px;
}

源代码:codepen15

13.菱形

普通菱形

普通菱形可以看作由两个三角形构成,代码如下:

HTML:

<div class="shape"></div>

CSS:

.shape {
  width:0px;
  height:0px;
  border: 40px solid transparent;
  border-top-width:0px;
  border-bottom: 70px solid #36d792;
  position: relative;
}
.shape::before {
  content:'';
  position: absolute;
  width:0px;
  height:0px;
  border: 40px solid transparent;
  border-bottom-width:0px;
  border-top: 70px solid #36d792;
  left:-40px;
  top:70px;
}

源代码: codepen16

特殊菱形(九十度角)

仍然可以采用上面的方法,但更简单的方法是绕中心旋转一个正方形。代码如下:

HTML:

<div class="shape"></div>

CSS:

body {
  margin:80px;
}
.shape {
  width:100px;
  height:100px;
  background: #36d792;
  transform: rotate(45deg);
}

源代码:codepen17

14. 盾形

HTML:

<div class="shape"></div>

CSS:

.shape {
  width:0;
  height:0;
  border:50px solid transparent;
  border-top-width:0;
  border-bottom:20px solid #36d792;
  position: relative;
}
.shape::after {
  content:'';
  position:absolute;
  width:0;
  height:0;
  border:50px solid transparent;
  border-bottom-width:0;
  border-top: 70px solid #36d792;
  left:-50px;
  top: 20px;
}

源代码:codepen18

15.钻石形

HTML:

<div class="shape"></div>

CSS:

.shape {
  width:35px;
  height:0;
  border:25px solid transparent;
  border-top-width:0;
  border-bottom:25px solid #36d792;
  position: relative;
}
.shape::after {
  content:'';
  position:absolute;
  width:0;
  height:0;
  border:42px solid transparent;
  border-bottom-width:0;
  border-top: 70px solid #36d792;
  left:-25px;
  top: 25px;
}

源代码:codepen19

16. 太极图

这是最终要实现的效果:

虽然它似乎是由不规则的几何图形构成的,但实际上,我们可以用规则的几何图形堆叠形成太极图。简单地说,可以把它拆解成下面这样:

先用第三小节提到的背景渐变实现一个黑白对半圆:

.taiji {
    width:200px;
    height:200px;
    border-radius:50%;
    background-image: linear-gradient(to right,black 50%,white 50%);
}

再实现两个黑白同心圆,其内圆半径和外环(外环可以用 border 来做)宽度根据几何关系求出即可,接着将同心圆分别定位到对半圆的最上面和最下面。

最终代码如下:

.taiji {
    width:200px;
    height:200px;
    border-radius:50%;
    background: linear-gradient(to right,black 50%,white 50%);
    position: relative;
}
.taiji::before,.taiji::after {
    content:'';
    position: absolute;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    left: -50%;
}
.taiji::before {
    background-color: white;
    border:40px solid black;
    top: 0;
}
.taiji::after {
    background-color: black;
    border:40px solid white;
    bottom: 0;
}

让太极图动起来:

现在,可以设置在鼠标移入的时候让太极图旋转起来:

.taiji {
       animation: rotate 1s linear infinite;    
    animation-play-state:paused;
}
.taiji:hover {
    animation-play-state:running;
}
@keyframes rotate {
  0%{
    transform: rotate(0deg);
  }
  100%{
    transform: rotate(360deg);
  }
}

效果如下:

源代码:codepen20

17.爱心

爱心可以看作是由两个这样的形状经过旋转后形成的:

设置左图的 transform-origin 为右下角顶点,让其绕点顺时针旋转 45 度,右图的 transform-origin 为左下角顶点,让其绕点逆时针旋转 45 度,即可形成爱心(旋转 45 度是因为爱心的底角是 90 度)。代码如下:

HTML:

<div class="heart"></div>

CSS:

.heart {
  position: relative;
}
.heart::before,.heart::after {
  content:'';
  position: absolute;
  width:50px;
  height:80px;
  border-radius: 25px 25px 0 0;
  background: red;
}
.heart::before {
  transform-origin: right bottom;
  transform: rotate(45deg);
}
.heart::after {
  left:50px;
  transform-origin: left bottom;
  transform: rotate(-45deg);
}

源代码:codepen21

18.无穷符号

无穷符号 可以看作是由下面两个图形经过旋转构成的:

先画出两个圆环,取消第一个圆环右下角圆角,并将其逆时针旋转 45 度;取消第二个圆环左下角圆角,并将其顺时针旋转 45 度。代码如下:

HTML:

<div class="shape"></div>

CSS:

.shape {
  position: relative;
}
.shape::before,.shape::after {
  content:'';
  position: absolute;
  width: 60px;
  height: 60px;
  border: 20px solid #36d792;
}
.shape::before {
  border-radius: 50% 50% 0 50%;
  transform: rotate(-45deg);
}
.shape::after {
  border-radius: 50% 50% 50% 0;
  transform: rotate(45deg);
}

得到的图形是这样的:

可以看到,圆环部分重叠了,所以需要将 ::after 伪元素右移,那么应该偏移多少呢?这里需要稍微计算一下。为了方便观察,我们修改两个图形的颜色和层级,并作适当的标注,得到下面这个图形:

对照开头的那张图可以看出,只要将 .shape::after 从 A 点右移到 B 点,就能形成一个 的形状。AB 边这段距离是由两条斜边组成的,并且斜边都位于一个等腰直角三角形中,因此只要分别算出两个三角形的直角边(a 和 b),就能算出斜边。从图中可以看出,a 是 30 + 20 = 50,对应的斜边为 50√2,约为 70;b 是 30,对应的斜边为 30√2,约为 42,所以 AB 边长为 112,即 .shape::after 应该右移 112 px。修改代码如下:

.shape::after {
  border-radius: 50% 50% 50% 0;
  left: 112px;
  transform: rotate(45deg);
}

这样,我们就能得到一个 形状了。

源代码:codepen22

19.吃豆人

吃豆人实际上是一个圆心角为 270 度的扇形,可以采用之前绘制 1/4 圆的方法来实现。代码如下:

HTML:

<div class="shape"></div>

CSS:

.shape {
  width: 0;
  height: 0;
  border: 100px solid #f5d54d;
  border-right-color: transparent;
  border-radius: 100px;
  position: relative;
}
.shape::before,.shape::after {
  content:'';
  position: absolute;
  width: 24px;
  height: 24px;
  border-radius: 50%;
}
.shape::before {
  background: #000;
  top: -70px;
  left: 5px;
}
.shape::after {
  background: #f5d54d;
  top: -12px;
  left: 60px;
}

源代码:codepen23

20. 弯尾箭头

下图是一个常见的弯尾箭头图标:

这个图标可以看作由两个图形组成:一个是三角形,一个是弧线,弧线是通过 border + 圆角实现的。

先来绘制三角形:

HTML:

<div class="shape"></div>

CSS:

.shape {
    width: 0;
    height: 0;
    border: 20px solid transparent;
    border-left-color: #198bf6;
       position: relative;
    transform: rotate(40deg);
}

.shape 设置一个伪元素,将其 border-top 作为弧线,并设置圆角:

.shape::after {
    content: '';
    position: absolute;
    width: 40px;
    height: 40px;
    border-top: 10px solid #198bf6;
    border-radius-top-left: 100px;
}

弯曲程度,可以通过 widthborder-radius 进行调节。

现在我们看到的就是这样的图形:

弧线是相对于三角形顶点定位的,需要相对于顶点左移 40+20 = 60px,再上移 10/2 = 5px:

.shape::after {
    left: -60px;
    top: -5px;
}

最后就能得到刚开始的那个图标了。

源代码:codepen24

21.聊天气泡

类型1:

这是一个类似微信的聊天气泡。观察到三角形部分是带有圆角的,所以我们不采用三角形 + 矩形的做法,而是用旋转的正方形 + 矩形来做 —— 即让正方形相对矩形定位在中间后,旋转 45 度。代码如下:

HTML:

<div class="shape"></div>

CSS:

.shape {
  width: 200px;
  height: 100px;
  background: #21d86d;
  position: relative;
  border-radius: 10px;
}
.shape::after {
  content:'';
  position: absolute;
  width: 20px;
  height:20px;
  background: #21d86d;
  border-radius: 2px;
  top:50%;
  transform: translate(-35%,-50%) rotate(45deg);
}

源代码:codepen25

类型2:

这种类型的气泡带有边框和背景颜色,我们仍然可以采用上面的做法即可,但要注意去掉正方形的两个边框。代码如下:

HTML:

<div class="shape"></div>

CSS:

.shape {
  width: 200px;
  height: 100px;
  border-radius: 8px;
  background: #faf8f4;
  border: 1px solid #e6d9b3;
  position: relative;
}
.shape::after {
  content:'';
  position: absolute;
  top: 50%;
  transform: translate(-50%,-50%) rotate(45deg);
  width: 15px;
  height: 15px;
  background: #faf8f4;
  border: 1px solid #e6d9b3;
  border-style: none none solid solid
}

源代码:codepen26

类型3:

这种是类似 QQ 的聊天气泡,这里弯曲的尾巴我们用 20 小节介绍的方法来做即可。代码如下:

HTML:

<div class="shape"></div>

CSS:

.shape {
  width: 200px;
  height: 100px;
  background: #01a0ff;
  border-radius: 20px;
  position: relative;
}
.shape::after {
  content:'';
  position: absolute;
  left: -20px;
  width: 40px;
  height: 40px;
  border-bottom: 20px solid #01a0ff;
  border-bottom-left-radius: 100px;
}

源代码:codepen27

22. RSS 订阅

这是一个常见的 RSS feed 图标,圆角矩形和内部的白色圆点都是容易实现的。那么两段白色圆弧应该怎么实现呢?我们可能很容易想到,两段白色圆弧都分别是一个 1/4 红色圆形的边框,所以可以用下面的方式来做:

但这种方式无疑是很麻烦的,事实上,我们用 CSS3 的 box-shadow 内阴影来做会更加简单:

/ x偏移量 | y偏移量 | 阴影模糊半径 | 阴影扩散半径 | 阴影颜色 | 内阴影 /
box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.2) inset;

可以先在圆角矩形内画一个 1/4 圆,然后利用内阴影往圆里放三段相间的弧线(白色弧线、红色弧线和白色弧线),x 偏移量和 y 偏移量控制弧线的坐标,阴影扩散半径控制弧线的宽度。这种方式显然比第一种要简单得多。

最终代码如下:

HTML:

<div class="shape"></div>

CSS:

.shape {
  width: 165px;
  height: 165px;
  padding: 35px;
  border-radius: 30px;
  background-color: #ff4802;
  position: relative;
}
.shape::before {
  content:'';
  position: absolute;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background-color: #fff;
  bottom: 40px;
}
.shape::after {
  content:'';
  position: absolute;
  width: 165px;
  height: 165px;
  border-top-right-radius: 165px;
  box-shadow:
    -14px 14px 0 14px #fff inset,
    -28px 28px 0 28px #ff4802 inset,
    -42px 42px 0 42px #fff inset;    
}

源代码:codepen28

23. 徽章缎带

徽章缎带可以看作是由一个圆形 + 两个三角形经过如下变换得到的:

HTML:

<div class="shape"></div>

CSS:

.shape {
  position: relative;
  background: #00a1fb;
  height: 100px;
  width: 100px;
  border-radius: 50%;
}
.shape::before,.shape::after {
  content: '';
  position: absolute;
  border: 40px solid transparent;
  border-bottom: 70px solid #00a1fb;
  border-top: none;
  top: 70px; 
}
.shape::before {
  left: -10px;
  transform: rotate(-140deg);
}
.shape::after {
  right: -10px;
  transform: rotate(140deg);
}

源代码:codepen29

24. TV 电视机

TV 电视机可以看作是由下面两个图形叠加在一起构成的:

两个图形的做法类似,都是给矩形设置一个水平半径和垂直半径相差很大的圆角。代码如下:

HTML:

<div class="shape"></div>

CSS:

.shape {
  position: relative;
  width: 200px;
  height: 150px;
  margin: 20px 0;
  background: #00a1fb;
  border-radius: 50% / 10%;
}
.shape::after {
  content:'';
  position: absolute;
  background-color: #00a1fb;
  top: 10%;
  bottom: 10%;
  left: -5%;
  right: -5%;
  border-radius: 5% / 50%;
}

源代码:codepen30

25. 放大镜

放大镜的实现也很简单。原文的做法是将放大镜的把手定位到右侧再进行旋转,其实我们可以直接将把手定位到正下方,然后去旋转 .shape 而不是 .shape::after,这样就可以方便地控制把手的朝向。

HTML:

<div class="shape"></div>

CSS:

.shape {
  width: 80px;
  height: 80px;
  border: 20px solid #01a0fe;
  border-radius: 50%;
  position: relative;
  transform: rotate(-45deg);
}
.shape::after {
  content:'';
  position: absolute;
  left: 50%;
  top: 90px;
  transform: translateX(-50%);
  width: 18px;
  height: 65px;
  background: #01a0fe;
}

源代码:codepen31

26. Facebook

Facebook 的图标由三个元素构成:蓝色方块、横线和弧线。横线是个等腰梯形,用前面介绍的方法来做即可;弧线可以看作是圆角矩形的一部分,那怎么才能做到只在蓝色方块中显示这一部分呢?我们可以先画好一个蓝底白边的圆角矩形,只把它的一部分定位到蓝色方块中,再给蓝色方块设置溢出隐藏。如下图所示:

为了让白色字母 f 在还没接触蓝色方块右边缘的时候就产生溢出隐藏的效果,这里要给蓝色方块加上蓝色边框。

代码如下:

HTML:

<div class="shape"></div>

CSS:

.shape {
    width: 100px;
    height: 110px;
    background-color: #3b589c;
    border-radius: 6px;
    border: 15px solid #3b589c;
    border-bottom: none;
    position: relative;
    overflow: hidden;
}
.shape::before {
    content: '';
    position: absolute;
    width: 100px;
    height: 100px;
    background-color: #3b589c;
    border: 20px solid #fff;
    border-radius: 27px;
    left: 56px;
}
.shape::after {
    content: '';
    position: absolute;
    top: 37px;
    left: 37px;
    width: 58px;
    height: 0;
    border-right: 4px solid transparent;    
    border-top: 20px solid #fff;        
}

源代码:codepen32

27. 月亮

月亮其实可以看作是由两个半径相同的圆不完全重叠后形成的:

那么实际实现中真的需要画两个圆吗?其实不需要,底下的圆用 CSS3 的 box-shadow 来做会更方便。

/ x偏移量 | y偏移量 | 阴影模糊半径 | 阴影扩散半径 | 阴影颜色 /
box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.2);

我们可以先画好一个圆,再给它设置阴影。x 偏移量和 y 偏移量共同控制月亮的形状和角度:

由于不需要模糊效果,所以模糊半径设置为 0;扩散半径可以控制月亮大小,若设置为 0 则表示与另一个圆大小相同;最后的阴影颜色属性则是控制月亮的颜色。

最终代码如下:

HTML:

<div class="shape"></div>

CSS:

.shape {
  width:100px;
  height: 100px;
  border-radius: 50%;
  box-shadow: 40px 0px 0 0 #f4bd2e;
}

源代码:codepen33

28. 指示箭头

指示箭头可以有两种做法:

原文采用的是左图的做法,用一个矩形 + 两个三角形来实现,但我们无法确定指示箭头所处背景的颜色,所以无法确定第一个三角形应该采用什么颜色;如果采用右图的做法,则无需考虑背景颜色的问题,实现起来也更加简单(两个矩形 + skew 来实现即可)。

HTML:

<div class="shape"></div>

CSS:

.shape {
  position: relative;
}
.shape::before,.shape::after {
  content:'';
  position: absolute;
  width: 200px;
  height: 20px;
  background: #00a1fb;
}
.shape::before {
  transform: skew(45deg);
}
.shape::after {
  transform: skew(-45deg);
  top:20px;
}

源代码:codepen34

29. 锁

HTML:

<div class="shape"></div>

CSS:

body {
  margin: 100px;
}
.shape {
  width: 140px;
  height: 100px;
  background: #01a0ff;
  border-radius: 15px;
  position: relative;
}
.shape::before {
  content:'';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%,-50%);
  width: 20px;
  height: 40px;
  border-radius: 10px;
  background: #fff;  
}
.shape::after {
  content:'';
  position: absolute;
  top: -63px;
  left: 50%;
  transform: translateX(-50%);
  width: 60px;
  height: 45px;
  border-radius: 48px 48px 0 0 / 48px 48px 0 0;
  border: 18px solid #01a0ff;
  border-bottom: none;  
}

源代码:codepen35

30. 书签 / 旗帜

这是一个常见的书签 / 旗帜图标,用矩形 + 三角形实现即可。代码如下:

HTML:

<div class="flag">
    <div class="text">采纳</div>
</div>

CSS:

.flag {
    width: 56px;
    background-color: #009961;
    border-top-left-radius: 4px;
    border-top-right-radius: 4px;
}
.text {
    position: relative;
    padding: 9px 6px;
    text-align: center;
    color: white;
}
.text::after {
    content:'';
    position: absolute;
    left: 0;
    top: 36px;
    border-width: 16px 28px;
    border-color: #009961;
    border-style: solid;
    border-bottom: 16px solid transparent;
}

需要注意,矩形 .text::after是长方形而不是正方形,这需要通过 border-width 进行相关设置:由于整体宽度是需要和父盒子保持一致的,因此左右两个 border 的宽度都应该是整体宽度的一半。最后再通过绝对定位进行微调,就能得到上图的效果了。

源代码:codepen36

参考:

The shapes of CSS

三角形的 N 种画法与浏览器的开放世界

查看原文

赞 31 收藏 22 评论 0

Chor 关注了用户 · 1月30日

ConardLi @conardli

Reading makes a full man, conference a ready man, and writing an exact man.

关注 2346

Chor 发布了文章 · 1月29日

为什么需要在 JavaScript 中使用顶层 await?

作为一门非常灵活和强大的语言,JavaScript 对现代 web 产生了深远的影响。它之所以能够在 web 开发中占据主导地位,其中一个主要原因就是频繁更新所带来的持续改进。

顶层 await(top-level await)是近年来提案中涉及的新特性。该特性可以让 ES 模块对外表现为一个 async 函数,允许 ES 模块去 await 数据并阻塞其它导入这些数据的模块。只有在数据确定并准备好的时候,导入数据的模块才可以执行相应的代码。

有关该特性的提案目前仍处于 stage 3 阶段,因此我们不能直接在生产环境中使用。但鉴于它将在不久的未来推出,提前了解一下还是大有好处的。

听起来一头雾水没关系,继续往下阅读,我会和你一起搞定这个新特性的。

以前的写法,问题在哪里?

在引入顶层 await 之前,如果你试图在一个 async 函数外面使用 await 关键字,将会引起语法错误。为了避免这个问题,开发者通常会使用立即执行函数表达式(IIFE)

await Promise.resolve(console.log('❤️'));
//报错

(async () => {
 await Promise.resolve(console.log('❤️'));
  //❤️
})();

然而这只是冰山一角

在使用 ES6 模块化的时候,经常会遇到需要导入导出的场景。看看下面这个例子:

//------ library.js ------
export const sqrt = Math.sqrt;
export function square(x) {
    return x * x;
}
export function diagonal(x, y) {
    return sqrt(square(x) + square(y));
}


//------ middleware.js ------
import { square, diagonal } from './library.js';

console.log('From Middleware');

let squareOutput;
let diagonalOutput;

// IIFE
 (async () => {
     await delay(1000);
     squareOutput = square(13);
     diagonalOutput = diagonal(12, 5);
 })();

function delay(delayInms) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(console.log('❤️'));
    }, delayInms);
  });
}

export {squareOutput,diagonalOutput};

在这个例子中,我们在library.jsmiddleware.js 之间进行变量的导入导出 (文件名随意,这里不是重点)

如果仔细阅读,你会注意到有一个 delay 函数,它返回的 Promise 会在计时结束之后被 resolve。因为这是一个异步操作(在真实的业务场景中,这里可能会是一个 fetch 调用或者某个异步任务),我们在 async IIFE 中使用 await 以等待其执行结果。一旦 promise 被 resolve,我们会执行从 library.js 中导入的函数,并将计算得到的结果赋值给两个变量。这意味着,在 promise 被 resolve 之前,两个变量都会是 undefined

在代码最后面,我们将计算得到的两个变量导出,供另一个模块使用。

下面这个模块负责导入并使用上述两个变量:

//------ main.js ------
import { squareOutput, diagonalOutput } from './middleware.js';

console.log(squareOutput); // undefined
console.log(diagonalOutput); // undefined
console.log('From Main');

setTimeout(() => console.log(squareOutput), 2000);
//169

setTimeout(() => console.log(diagonalOutput), 2000);
//13

运行上面代码,你会发现前两次打印得到的都是 undefined,后两次打印得到的是 169 和 13。为什么会这样呢?

这是因为,在 async 函数执行完毕之前,main.js 就已经访问了 middleware.js 导出的变量。记得吗?我们前面还有一个 promise 等待被 resolve 呢 ……

为了解决这个问题,我们需要想办法通知模块,让它在准备好访问变量的时候再将变量导入。

解决方案

针对上述问题,有两个广泛使用的解决方案:

1.导出一个 Promise 表示初始化

你可以导出一个 IIFE 并依靠它确定可以访问导出结果的时机。async 关键字可以异步化一个方法,并相应返回一个 promise。因此,下面的代码中,async IIFE 会返回一个 promise。

//------ middleware.js ------
import { square, diagonal } from './library.js';

console.log('From Middleware');

let squareOutput;
let diagonalOutput;

//解决方案
export default (async () => {
    await delay(1000);
    squareOutput = square(13);
    diagonalOutput = diagonal(12, 5);
})();

function delay(delayInms) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(console.log('❤️'));
    }, delayInms);
  });
}

export {squareOutput,diagonalOutput};

当你在 main.js 中访问导出结果的时候,你可以静待 async IIFE 被 resolve,之后再去访问变量。

//------ main.js ------
import promise, { squareOutput, diagonalOutput } from './middleware.js';

promise.then(()=>{
     console.log(squareOutput); // 169
     console.log(diagonalOutput); // 13
     console.log('From Main');

     setTimeout(() => console.log(squareOutput), 2000);// 169

     setTimeout(() => console.log(diagonalOutput), 2000);// 13
})

尽管这个方案可以生效,但它也引入了新的问题:

  • 大家都必须将这种模式作为标准去遵循,而且必须要找到并等待合适的 promise;
  • 倘若有另一个模块依赖 main.js 中的变量 squareOutputdiagonalOutput,那么我们就需要再次书写类似的 IIFE promise 并导出,从而让另一个模块得以正确地访问变量。

为了解决这两个新问题,第二个方案应运而生。

2.用导出的变量去 resolve IIFE promise

在这个方案中,我们不再像之前那样单独导出变量,而是将变量作为 async IIFE 的返回值返回。这样的话,main.js 只需简单地等待 promise 被 resolve,之后直接获取变量即可。

//------ middleware.js ------
import { square, diagonal } from './library.js';

console.log('From Middleware');

let squareOutput;
let diagonalOutput;

export default (async () => {
    await delay(1000);
    squareOutput = square(13);
    diagonalOutput = diagonal(12, 5);
    return {squareOutput,diagonalOutput};
})();

function delay(delayInms) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(console.log('❤️'));
    }, delayInms);
  });
}

//------ main.js ------

import promise from './middleware.js';

promise.then(({squareOutput,diagonalOutput})=>{
    console.log(squareOutput); // 169
    console.log(diagonalOutput); // 13
    console.log('From Main');

    setTimeout(() => console.log(squareOutput), 2000);// 169

    setTimeout(() => console.log(diagonalOutput), 2000);// 13
})

但这个方案有其自身的复杂性存在。

根据提案的说法,“这种模式的不良影响在于,它要求对相关数据进行大规模重构以使用动态模式;同时,它将模块的大部分内容放在 .then() 的回调函数中,以使用动态导入。从静态分析、可测试性、工程学以及其它角度来讲,这种做法相比 ES2015 的模块化来说是一种显而易见的倒退”。

顶层 Await 是如何解决上述问题的?

顶层 await 允许我们让模块系统去处理 promise 之间的协调关系,从而让我们这边的工作变得异常简单。

//------ middleware.js ------
import { square, diagonal } from './library.js';

console.log('From Middleware');

let squareOutput;
let diagonalOutput;

//使用顶层 await 
await delay(1000);
squareOutput = square(13);
diagonalOutput = diagonal(12, 5);

function delay(delayInms) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(console.log('❤️'));
    }, delayInms);
  });
}

export {squareOutput,diagonalOutput};

//------ main.js ------
import { squareOutput, diagonalOutput } from './middleware.js';

console.log(squareOutput); // 169
console.log(diagonalOutput); // 13
console.log('From Main');

setTimeout(() => console.log(squareOutput), 2000);// 169

setTimeout(() => console.log(diagonalOutput), 2000); // 13

middleware.js 中的 await promise 被 resolve 之前, main.js 中任意一条语句都不会执行。与之前提及的解决方案相比,这个方法要简洁得多。

注意

必须注意的是,顶层 await 只在 ES 模块中生效。 此外,你必须要显式声明模块之间的依赖关系,才能让顶层 await 像预期那样生效。提案仓库中的这段代码就很好地说明了这个问题:

// x.mjs
console.log("X1");
await new Promise(r => setTimeout(r, 1000));
console.log("X2");
// y.mjs
console.log("Y");
// z.mjs
import "./x.mjs";
import "./y.mjs";
//X1
//Y
//X2

这段代码打印的顺序并不是预想中的 X1,X2,Y。这是因为 xy 是独立的模块,互相之间没有依赖关系。

推荐你阅读一下 文档问答 ,这样会对这个顶层 await 这个新特性有更加全面的了解。

试用

V8

你可以按照文档所说的,尝试使用顶层 await 特性。

我使用的是 V8 的方法。找到你电脑上 Chrome 浏览器的安装位置,确保关闭浏览器的所有进程,打开命令行运行如下命令:

chrome.exe --js-flags="--harmony-top-level-await"

这样,Chrome 重新打开后将开启对于顶层 await 特性的支持。

当然,你也可以在 Node 环境测试。阅读这个指南 获取更多细节。

ES 模块

确保在 script 标签中声明该属性:type="module"

<script type="module" data-original="./index.js" >
</script>

需要注意的是,和普通脚本不一样,声明模块化之后的脚本会受到 CORS 策略的影响,因此你需要通过服务器打开该文件。

应用场景

以下是提案中讲到的相关用例:

动态的依赖路径

const strings = await import(`/i18n/${navigator.language}`);

允许模块使用运行时的值去计算得到依赖关系。这对生产/开发环境的区分以及国际化工作等非常有效。

资源初始化

const connection = await dbConnector();

这有助于把模块看作某种资源,同时可以在模块不存在的时候抛出错误。错误可以在下面介绍的后备方案中得到处理。

依赖的后备方案

下面的例子展示了如何用顶层 await 去加载带有后备方案的依赖。如果 CDN A 无法导入 jQuery,那么会尝试从 CDN B 中导入。

let jQuery;
try {
  jQuery = await import('https://cdn-a.example.com/jQuery');
} catch {
  jQuery = await import('https://cdn-b.example.com/jQuery');
}

抨击

针对顶层 await 的特性,Rich Harris 提出了不少抨击性的问题

  • 顶层 await 会阻塞代码的执行
  • 顶层 await 会阻塞资源的获取
  • CommonJS 模块没有明确的互操作方案

而 stage 3 的提案已经直接解决了这些问题:

  • 由于兄弟模块能够执行,所以不存在阻塞;
  • 顶层 await 在模块图的执行阶段发挥作用,此时所有的资源都已经获取并链接了,因此不存在资源被阻塞的风险;
  • 顶层await 只限于在 ES6 模块中使用,本身就不打算支持普通脚本或者 CommonJS 模块

我强烈推荐各位读者阅读提案的 FAQ 来加深对这个新特性的理解。

看到这里,想必你对这个酷炫的新特性已经有了一定的了解。是不是已经迫不及待要使用看看了呢?在评论区留言一起交流吧。

查看原文

赞 11 收藏 9 评论 1

Chor 赞了回答 · 1月18日

解决javascript中从一个对象取部分属性给另一个对象

const obj2 = (({a, d, e}) => ({a, d, e}))(obj)

关注 6 回答 4

Chor 发布了文章 · 1月14日

Chor 的 2020 年度总结 | 2020,再见;2021,你好

当意识到需要为 2020 年写一个年终总结的时候,已经是 2021 年 1 月 4 号了。不过没关系,年终总结虽迟但到。我的年度总结可能和别人不太一样,不会有文采飞扬的标题和笔墨,也不会谈太多技术相关的事情,关键词只有两个:经历想法

学业 | 学校和课程

上网课的体验

无论谁也不会想到,开年不久就爆发了疫情。今年的大部分时间都是在家里度过的,上课也改为采用网课的方式,一开始很难适应,但过了一段时间之后我发现自己很喜欢这种线上教学的形式,因为它带给我某种程度上的自由,我有更多时间去决定自己学习的方式。此外,线下教学的缺陷在于它是一次性的,有些知识点错过了就很难再补全,但线上教学因为有录播的存在,我们可以反复地回顾上课的内容。

今年上半年基本都是各种计算机专业课:操作系统、数据库、编译原理 ...... 说是网课,但大部分都是自学为主。一个学期的时间其实是很难将这些体系化的东西讲全、讲清楚的,因此我们所接受到的实际上是经过“删减”的知识,而且有些老师提供的网课视频就是照着 ppt 读稿,没啥意思,不如自己课后去找资料、查漏补缺来得高效。上半年大概写了十来篇笔记,都是和课程相关的,逐渐喜欢上了这种电子笔记的形式,就呈现效果和后期修改的便利程度来看,手写笔记无法与之相比。其实也多亏了网课这样的形式,给了我更多充足的时间去进行学习方式上的探索。

下半年的课程听起来很高大上:机器学习、计算智能、创业基础……实则很多是在浪费时间罢了,我不是很感兴趣,这里就不谈了。

疫情期间顺便把素拓分水完了。虽然这东西没啥用,不过不修完没法毕业,于是马上开启了“疯狂蹭分”模式 ……

告别淼城

最遗憾的事情,也许是疫情正好赶上了我们在三水的最后一个学期。当 9 月份再次回到三水的宿舍时,已经是打包行李准备搬校区的时间了,突然就要离开生活了两年的校区,内心还是挺不舍得的。虽然学校地理位置颇为偏僻,人烟罕至,但我很喜欢学校的氛围,在这里能够静下心来去思考一些事情。如果让我说学校最喜欢的一个地方,应该是图书馆前的长椅。有时候经过那里,会在长椅上坐一会,看着眼前一片平静的湖水,听着耳畔传来的鸭叫声,感觉再焦躁的心情也会慢慢变得平和。这样让人可以放松的“公共空间”,在广州校区这边并不多见。

图书馆我去得比较多,一直喜欢坐在靠窗的位置,有风吹过的时候,很舒服。

写作 | 公众号和技术写作

今年输出的文章

我的公众号是 19 年 10 月份开通的,由于开通时间太晚,没有留言功能了。我能理解这是进行舆论管控的一种手段,但没办法和读者进行互动还是挺蛋疼的。微信官方在 8 月份的时候曾推出“读者讨论”的功能,我一度以为这是某种程度上的补偿措施,可惜的是该功能在 11 月份的时候又悄无声息地消失了。

19 年的时候,公众号只发了一篇文章,之后就一直没有打理。今年上半年决定把之前的博客慢慢迁移过来,截止到目前,公众号大概是发了 49 篇文章,这期间也做了一些宣传工作(其实就是在一些推文群发发文章而已)。粉丝数量则是 150 人,这个数量自然是很少,不及那些知名公众号的百分之一,不过“万丈高楼平地起”,所有的东西都是慢慢累积起来的,要学会耐得住性子。

贴一下今年的文章汇总:

除了几篇技术性没那么强,大部分文章我都是投入很多时间和精力去创作并完善的,力求做到结构清晰、语言通俗易懂。一开始我还怕有的地方写得不够好,比如用词是否准确?表述是否精简?不过有几个网友表示从我的文章里得到了不少收获之后,我也就放心了。这是对我的文章很大的肯定,这里很感谢他们。总的来说,今年在技术写作上的输出比我想象的要多很多,没什么遗憾了,很满意。

写文章的原则

我的公众号基本是不发转载的(目前只转了一篇)。比起自己辛辛苦苦地去写,转载有时候真的要容易得多,但东西始终是别人的,拿别人的东西喂饱自己,我不喜欢这种方式。我做公众号也有自己一直在恪守的原则:

  • 我所选择的创作类型:坚持以原创和翻译为主,不管是哪一种类型,我都会倾注时间和精力去创作;
  • 对于“原创”的理解:所有翻译的技术外文都是二次加工的东西,一律不会且不应该标注为原创。我并不认为你参与翻译了,这就是你的东西;
  • 对于“标题”的选择:我的标题一直是平平无奇的,但能直观反映主题,今后也会是这个风格。至于原因:我反感所有的标题党 —— 无论是谁,并且我不想成为这样的人

当然,还有很多不足之处需要改进。比如我翻译的文章基本都是没有征得原作者许可的,不是因为作者拒绝了(从以往的经验来看,大部分的国外作者是很友善的),单纯就是因为我懒、没有去要授权而已,这个事情我觉得可以做得更好。另外我的文章里有不少都是早期学习做的笔记,不排除会有大大小小的错误,这些也是以后写文章需要需要注意的事情。

今年也加了不少推文群,发现无论是在哪里,都有各种标题党存在:夸大事实、蹭热度、贩卖焦虑 …… 不同的只是这部分人所占的比例。综合下来还是腾讯云社区的氛围比较好 —— 千万不要认为这是在打广告,我只是在陈述事实。腾讯云社区有更多用心做公众号的人,不少技术圈大佬是坚持在输出原创优质内容的,非常之勤奋。另外官方对于文章版权的保护也相当重视 —— 印象比较深的一次,有好几个作者的文章在未授权的情况下被某个站点搬过去了,经过社区交涉后当天就被撤了下来。

网络 | 网上趣事记录

1024 圆桌会议

10 月份的时候掘金社区有一个《1024 圆桌会议:JavaScript 的过去、现在和未来》的线上直播,主要聊到了 JavaScript 的发展历程、TC39、技术翻译等话题。当时在飞书群问了一个困惑已久的问题:“在进行技术翻译的时候无法完全理解作者原意,这种情况下如何避免翻译可能造成的误导?”很幸运得到了红宝书(《JavaScript高级程序设计》)译者李松峰老师的解答,颇有收获,这里也做一下记录。

  • 老师给的第一个建议是直接问作者本人,这可以说是最简单有效的,并且足以打消一切由于对翻译结果不确定而产生的疑虑;
  • 第二个建议是咨询更专业的人。其实我有时候在翻译的时候拿捏不当,也会问群里的朋友,有掘金翻译计划的大佬,也有教英语的老师,还有本身就是职业做翻译的。多聆听别人的见解,多几个揣摩和理解作者的角度,翻译的时候卡壳了,一定不要一个人在死胡同里打转;
  • 第三个建议我认为是最受用的,偏向于方法论,实操性更强,那就是寻找平行文本。平行文本指的是与原文内容接近的任何参考资料(包括译入语材料和译出语材料),当不确定用词、表达甚至是结构的时候,求助权威文章,汲取长处,从而去完善译文,所谓的“他山之石,可以攻玉”讲的就是这个。简而言之,将平行文本应用在我们的译文中可以实现“内化”。

其实有不少已经在实践中用上了,比如在碰到一些陌生的领域内术语时,参考其它文章来获得翻译上的灵感,这就是平行文本的应用。

比较意外的是,贺老(Hax)也借着这个问题的契机解释了“饱受诟病”的 Promise 翻译的由来。对于将 Promise 翻译为“期约”这件事,社群里听到更多的是反对的声音,甚至正在阅读这篇文章的你,也可能无法接受这个译名。“用了这么多年的叫法,为什么突然就要改了?” 但很多时候,这种反对不是源于内心对原则性错误的抵触,仅仅只是因为拒绝改变自己先入为主的观念罢了。正是因为看到了这一点,所以作为译者,是有责任在众人的不理解中去推动这种改变的,这是属于他们的责任感。

稳固不变并非就是好事,很多事物是需要经历一个从无到有的过程的 —— 正如贺老所说的,“这么多年过去了,Promise 也应该有一个属于它的译名了”(原话忘记了,但差不多是这个意思)。

后面也拿到了李松峰老师亲笔签名的红宝书。第四版我是早早就买了,但是这一本赠送的会更有纪念意义 —— 不仅是技术上的,也是翻译上的。

《互联网中“讨论”的消亡》线上沙龙

之前收到了机核网 RSS 推送的一篇文章:中文互联网中“讨论”的消亡,文章很长,但耐心读完后,感觉一针见血,剖析得很到位。原文来自公众号@沙丘研究所,后来报名参加了他们在 Zoom 上开的一期线上沙龙,主要谈到了“互联网”、“结构搭建”、“公共领域”、“优质内容传播”等话题。整场沙龙听下来,我属于比较懵逼的状态,可能有太多领域内的名词都听不懂了。提前补充相关知识很重要,这能让我们在聆听演讲的时候有更加完好的体验。不过好在有文字稿,油管上也有录播,可能以后再回过头来品味这期沙龙,又是不一样的感觉。

掘金翻译计划

除了自己翻译感兴趣的文章之外,今年也依旧积极地投入掘金翻译计划的校对工作中,校对了 22 篇译文。目前来看,掘金翻译计划不如之前有序和高效,issue 堆积未处理的问题虽然有所改善,但多少还是存在,而且有不少译者或者校对者领取任务之后人就消失了,虽说翻译全是出自自愿,但这样的行为毫无疑问是会给别人带来麻烦的。再者就是我进行校对的时候,发现有不少人为了求快,在语句的斟酌以及资料的考证上不假思索,造成的后果就是读起来不够流畅,且增加了读者理解的成本。希望这些问题能重视起来并逐步改进,这样才能真正成为一个最懂读者和译者的翻译平台。

一些有趣的网友

这一年和 @情封(可能大家都认识了,公众号“前端早读课”的负责人)有过几次印象深刻的聊天,我们谈论的话题还挺多,包括产品设计、社区建设、用户体验、公众号运营等。中文互联网环境在我眼里一直是浮躁且忙碌的,而在这样的环境下,两个素未谋面的网友仍可以腾出片刻的时间进行一次讨论,互相交流意见、启发对方,我一直认为这是一件相当难得且值得庆幸的事情。情封是一个很有想法也很有态度的人,和我遇到的大部分网友都不一样,和这样的人聊天是能够学到很多东西的。

此外也认识了一个正在做 UGC 社区的朋友。他正在做的是类似掘金或者思否那样的技术文章创作社区,社区仍处于起步阶段,因此我们几个内容创作者就加入了相关讨论中,互相贡献自己的一些 idea。聊到的话题包括长内容与短内容的创作形式/编辑方式,markdown 编辑器使用体验的优化等。在讨论的过程中,我能明显地感受到决策的过程比想象中的要更加复杂和漫长,不过这也正说明他是一个思考问题相当全面的人,这样的人行事是非常谨慎的,会系统地评估风险、权衡利弊,绝不会一拍脑瓜子就冒然地作出决定。

这件事情也让我体会到社区运营者的不易。一般来说,内容创作社区会有三种核心的角色:运营者、创作者和读者,而大部分时候,我们只会充当其中一种角色,并站在这个角色的角度去发言,但要想对社区作出相对客观的评价,我们就需要学会切换不同的身份去看待同一个问题。简单地说就是要有同理心,心里不要只装着自己。

社区(腾讯云/掘金)的一些礼物

参加活动的时候腾讯云社区送的一些礼物,主要是鼠标垫、文具套装和颈枕(这个超实用)等,好像还有一个腾讯视频会员卡,不过前几天看的时候发现已经过期了 ← ←。希望 2021 年也能时来运转。

参加掘金翻译计划兑换的T恤和卫衣,以及一些贴纸:

卫衣真心很好看,改版后的 T 恤质感也很舒服,后面送了朋友一件。

新的尝试:第一次发视频

起因是在 b 站发现了一个讲解 Event Loop 的视频,原视频是 JSconf EU 大会上的一个演讲,很有名,各位应该看过。有 up 主做了搬运和翻译,前半部分还挺好,后面明显虎头蛇尾,机翻现象明显。看底下有很多人吐槽,突发奇想,为什么不自己做一个字幕呢?好视频不应该被埋没,于是用 Arctime 做了一个中文字幕,过程比想象的繁琐很多,需要反复调整时间轴,非常需要耐心,好在最后效果还是不错的。视频地址在这里:Philip Roberts:到底什么是Event Loop呢?(JSConf EU 2014),如果喜欢的话可以来个一键三连。很多技术大会还是很有意思的,国内的比如 QConf,国外的比如 JSconf、CSSconf 等,后面如果有时间,可能会尝试再翻译几个技术大会视频。

网络安全:记一次网络勒索事件

虽然大标题是“网上趣事记录”,但这件事并不十分有趣。上半年算是“经历了”一回网络勒索事件,而当事人是我的一位朋友,详情可以看这篇文章。这种好像段子一样的事情就发生在自己周边,我的第一反应是满脸的问号:???好在我们都算冷静,看出了这是 fake spam,事情最后有惊无险地解决了。我们在网上真的得注意保护自己的隐私,比如填某些表单的时候如无必要,就别泄露个人联系方式,群里发图问问题的时候关键信息也应该打码(有的人心太大了,自己姓名和手机号码都暴露在截图里还浑然不知),毕竟你不知道群里都是什么人。反正在互联网上一定要多个心眼,避免让自己陷入不必要的麻烦。

娱乐 | 书籍、电影、音乐和游戏

看了几本书

我不是一个很喜欢看书的人,属于一年能看一两本书就不错了的类型。不过比较意外的是,疫情在家期间,我竟然可以静下心来读几本书。或许是因为面对电脑屏幕的时间更长了,看纸质书可以在一定程度上缓解那种疲劳感。

《奥斯维辛:一部历史》

发现我还是比较喜欢看历史类型的书籍。读完《奥斯维辛:一部历史》,最大的感受就是:没有人真的了解自己,人能有多邪恶,不敢想象。在书末有这么一段话:

“人类从内心深处需要这个世界有公道存在,需要无辜的人最终得到补偿,有罪的人最终得到惩罚。但奥斯维辛的历史没有给我们这样的慰藉。”

似乎无论是以前还是现在,正邪善恶都远比常识所理解的复杂很多,尤其是,他们往往不像人们期望的那样有应得的归宿。历史上,那些前脚刚逃离奥斯维辛的人,后脚又陷入另一个更加可怕的地狱 —— 古拉格群岛,而那些曾犯下暴行的人,却有不少掩饰了自己的罪行,从此在人生上平步青云。

《深入解析 CSS》

技术类书籍。无论是《深入解析 CSS》,还是《CSS in depth》,这本书都不像它的书名所说的那样,特别深入,但我认为值得一读,因为它对于一些 CSS 的细节做了非常到位的剖析和讲解,作者会讲到很多我们在实际项目中经常遇到的小问题,并且给出优雅的解决方案,看完之后你经常会有这种感觉:”这个作者知道我疑惑的地方在哪里,他给出了解释,而且解释得通俗易懂“。简而言之,这本书很实在,很接地气。

《JavaScript 高级程序设计》第四版

个人还是更喜欢 Nicholas·C·Zakas 的文笔风格,无论是他写的红宝书第三版,还是《深入理解 ES6》,在阅读的时候你都能感觉到他的逻辑是非常缜密且清晰的,读起来津津有味。当然现在的作者 Matt 写得也挺好,但是一些问题可能会让读者在阅读第四版的时候不是很舒服:比如 Matt 并不是完全按照循序渐进的风格进行讲解,有很多知识点实际上是在后面才涉及,但会突然出现在前面,对此他并没有进行相应的说明,新手阅读的时候可能会有点懵;另一个问题就是一些代码的拼写错误,不是只有一两个,有很多。在翻译上实际也存在一些大大小小的问题,当然,我不认为 Promise 的翻译算是一个问题,正如上面所说的,只是立场不同而已,而且在中文世界里恐怕找不到比“期约”更加贴切的译名了。豆瓣的评论里有不少人说翻译水平一般,估计都是在吐槽 Promise 的翻译,只能说根本没吐槽到点上。

具体的大大小小的问题可以去图灵社区的勘误区查看,估计再版的时候这些问题都会纠正过来。预购的读者可能吃亏点,但是发现问题也是一个进步的过程。

《程序员修炼之道》

相当经典且值得一看的好书,书里讲的不少东西已经成为了公认的最佳实践。在做了一定的项目量之后再去看会有更大的收获,有很多地方可以引起共鸣。

一些纪录片和电影

看了几期《一席》的节目,这个系列给我的感觉是人文气息很浓厚。印象比较深刻的是作家郑执的那期演讲,讲了自己的父亲、作家之路、在东北的一些经历,很让人动容。演讲虽有 43 分钟,但我看完后还觉得不过瘾。作家讲故事的能力真的是一流,简单的语言,却能触碰我们内心最柔软的地方。后面又看了《杀马特我爱你》、《纸工厂》,都是比较出名的几期。

网剧看了比较火的《隐秘的角落》和《沉默的真相》,多少有些牵强的成分,但总体观感很不错。两部都属于短剧,节奏是比较紧凑的,没有什么拖泥带水的地方。想起以前还有一些动不动就七八十集的电视剧,真是想想都觉得头皮发麻。

电影看了姜文的《让子弹飞》(一直听说很好看),杜琪峰的《黑社会》系列,两部电影都属于比较值得玩味的,豆瓣因此还有衍生的“黑社会学”。另外就是“镖客三部曲”,昆汀的《无耻混蛋》,之所以看这几部作品,很大程度上是奔着莫里康内的配乐去的。《无耻混蛋》中,一袭红裙的女主中枪的那一瞬间,血花四溅,却宛如鲜花盛开,再搭配《Un Amico》的配乐,场面非常凄美,可以说是影史上相当惊艳的一幕。昆汀在这部电影中大量使用了莫里康内的音乐(多达八首),虽然莫里康内本人对此颇有微词(曾经吐槽过),但不得不说昆汀确实是将配乐运用得出神入化。

今年听歌的收获

莫里康内

B 站 up 主“培根悖论唠唠嗑”纪念莫里康内的一期视频《用音符谱写电影的诗人,传奇作曲家莫里康内》让我彻底粉上了这位神级配乐大师。先说这期视频,它有多好看呢?我前前后后应该看了有二十几遍,而且每一次看的时候都能收获同等程度的感动。

比较推荐的几首:《Un Amico》《The Surrender》《L' Estasi Delll'oro》《Sacco e Vanzetti》。如果你有玩过小岛秀夫的《合金装备》系列,对《Sacco e Vanzetti》这首歌一定不陌生。

The Stone Roses | 石玫瑰

今年比较大的一个惊喜,又发现了一支宝藏级别的英伦摇滚乐队:The Stone Roses,石玫瑰。看 B 站的纪录片《石玫瑰再临 The Stone Roses: Made of Stone》入的坑,后面听了几首歌,用两个字形容就是:牛逼。在英伦摇滚的历史上,石玫瑰登场的时间比 Oasis (绿洲,我最喜欢的乐队,没有之一)要更早,但是这两支乐队在有些地方我觉得蛮像的。首先,他们都来自英国曼彻斯特(曼彻斯特,永远滴神),然后在曲风上都有一种浪漫的气息,两个主唱发型也比较相似,而且在演出时都喜欢拿着铃鼓。纪录片末尾是石玫瑰重组后开的一次演唱会,我一时之间竟有点分不出台上站着的是石玫瑰的主唱 Ian Brown,还是绿洲的主唱 Liam Gallagher。

重点来了,一向“桀骜不驯,怼天怼地怼空气”的 Liam Gallagher 是怎么评价石玫瑰的呢?看图说话:

顺便一提,这段话是 Liam 在演唱会后台说的,当时的他是台下的听众。这场演唱会有多火呢?在粉丝的眼里,甚至比他们的工作还要重要:有一位老哥直接翘班跑来买票,现场还有一位工作到一半跑过来的粉刷工,表示自己要听完演唱会再去忙其它的。门口排的长队九曲十八弯、一眼望不到头。当时的场地并不足以容纳那么多的观众,能买到票的人可以说是“天选之子”。

今年玩的一些游戏

今年玩的游戏并不多。一个是《Borderland 3》,epic 打折的时候入的,操作手感比上一作要好很多,也加入了非常地道的中文配音。不过我并不是很喜欢这一代的剧情,另外角色也不是很讨喜,所以打到一半就没玩了,至今没通关;另一个是 《GTA5》(想到一张梗图:是谁还没买 GTA5?),和高中舍友一起打线上模式,做各种任务,互相坑对方,非常之好玩。R 星做的开放世界游戏,质量还是很顶的。当然,下半年开学后也就没玩了,最近好像又出了新的 dlc,可能有空会约几个小伙伴玩一下。

《JOJO》和《进击的巨人》

现在我对单纯的热血漫画并不是很感兴趣,最多只是逛逛贴吧看看剧情是否有什么进展,不会再有从前那样的热情。目前来说,《JOJO》系列(尤其是 SBR)以及《进击的巨人》是我心中唯二两部神作,在我看来,荒木和谏山创是属于真正有思想并且会将这些思想在漫画中传达出来的作者,他们的作品,绝非是那种看一遍就能看懂的。

《JOJO:SBR》中,乔尼为了救下自己的挚友杰洛,用好不容易得到的“遗体”换来了敌人手里一瓶尚未喝完的葡萄酒,于是有了名场面:雪中对饮。这一刻,我深刻感受到了荒木通过艺术作品所散发出来的那种浪漫:

至于《进击的巨人》,虽然已连载超过 10 年,不过我是在最近几个月才入坑的。目前漫画差不多要收尾了,虽然多半不会是 happy ending,但仍然非常期待谏山创会带给我们什么样的结局。

项目 | 在图书馆的“搬转”日常

学习新的技术栈:小程序开发

上半年学期末看到了图书馆的一篇推文,正在招学生参与图书馆项目的开发。于是我就赶在截止日期之前报名了,顺带附上不太像样的简历。在学期快结束的时候收到了老师的邮箱回复,大概意思是让我暑假学一下小程序,开学后会有个面试。

当时我还没接触过小程序,不过我知道它有很多地方和 Vue 是很像的,而且考虑到小程序对前端工程师来说基本是必学的,所以第一反应是:学起来。既然要学习一个新的技术栈,肯定少不了两个东西,一个是权威的官方文档,一个是质量上乘的课程。微信社区的文档已经很完备了,至于课程,我选择的是慕课上的《微信小程序云开发-从0打造云音乐全栈小程序》,300+ 的价格还是蛮贵的(心疼一下自己),但是很值。学校的课程结束后就进入了暑假,我开始马不停蹄地学习小程序,那种每天学习一点新东西,解决一个小 bug,慢慢去完善一个项目的感觉真的很棒,至今仍然记忆犹新。而且讲师的风格我个人比较喜欢,他的讲法是由浅入深、循序渐进的,经常会抛出一个问题引导你去思考。这个课程能学到的不仅是技术,还有编码规范以及解决问题的思路。

项目大概写了一个多月就完工了。期间不仅学到了小程序的相关知识,也对它周遭的生态尤其是云开发有了一定的了解,不得不说,云开发真的很香,前端在一定程度上能够独当一面。此外也接触到了一点 Koa 相关的知识,写了一些简单的后台接口(虽然因为太久没写,现在忘得差不多了)。

总的来说,小程序的开发体验还算可以,很多东西都是现成的,只需要去调用即可,遇到的很多问题在社区也有解决方案。但开发过程依旧有不少痛点,比如:

  • setData 蛋疼的赋值方式;
  • 不支持 less、sass 等 css 预处理器;
  • 缺少原生的状态管理方案。虽然官方推出的westore 足够简洁和轻量,但是对代码有一定的侵入性:需要用其提供的 api 构建组件和页面,修改状态之后需要手动调用 this.update() 进行更新……这些都在一定程度上增加了项目对其的依赖,加上我们无法像使用 Axios 那样做一层中间的封装,万一哪一天要更换状态管理方案,代码修改起来想必会相当痛苦。
  • 另外,开发者工具的使用体验也比较差,偶尔会有一些奇葩的 bug,卡死、黑屏的概率尤其高,最可怕的是这玩意疯狂吃 CPU:之前有个群友晒了一张图,开发者工具的 nwjs 进程占用 CPU 直接飙到了 127%,看着属实吓人。可能大家也不会直接在上面编码,都是用 VS Code 或者 WebStorm 居多;

当然,这里吐槽的基本都是原生小程序的毛病 —— 很多问题在小程序框架出现之后都迎刃而解了。为了摆脱这种不太舒服的开发体验,后面得找时间学习一些跨端框架了。

学校的项目:年度账单小程序

下半年开学后就是面试,过程比较顺利,进入部门后不久就开始了“搬砖”的工作。在老师的带领下,和学校的几个小伙伴一起做了一个年度账单小程序,一开始以为花一两个月就能写完了,没想到由于各种各样的原因,最后忙活了一个学期才搞定。目前项目已经上线,在公众号和朋友圈的宣传下,截止到现在已有 1w+ 用户量,反响挺不错的。

不知道大家是否有留意到,今年(2020 年)做年度账单的 app / 网站更多了,像 Gitee、飞书、CSDN 等都推出了自己的用户年度账单,可以说是遍地开花。我想老师可能是预见到了这一点,所以打算推出一个面向师生的年度账单,不得不说是个很棒的 idea。当然,做这个项目实在是踩了无数的坑:

  • 缺乏专业可靠的美工,后面严重耽误了项目的开发进度。想和老师说一声,“明年招人真的得招个学 UI 或者产品设计的同学了”,虽然前端也需要关注用户体验,但让专业的人来做这件事,一定可以考虑到更多细节;
  • 项目中后期的时候重构了两次核心代码,这源于初期需求和用户群体定位的不明确。这部分代码的逻辑也是整个项目最复杂的,需要考虑到各种不同类型的用户。在知道要重写逻辑之后,心态有点炸裂,好在还是坦然接受了,就当作是一次挑战吧。而且以后工作中也肯定会有这样的事情;
  • 技术方案选择以及评估工作做得还不够到位。我们做的小程序最初是有提供一键“保存图片”功能的,这个功能只能用 canvas 来做,因此选择了海报图绘制库 Painter。后面实际开发的时候,整体来说是比较顺利的,但还是有不少痛苦的地方:

    • 不能实时修改以查看效果,每次修改都要等待绘制,严重影响开发效率;
    • 需要手动声明每个元素的位置。包含变量的文案是最麻烦的,基本上需要把一句简单的话拆成不同的字块,再计算彼此之间的距离以进行正确的定位和排列
    • 页面全部都是静态的,动效什么的别想了
    • 灵活性差,往往一个地方发生改动,后面的都要跟着改,就好像多米诺骨牌一样

      后面又出现了两个致命的问题:一个是 iPhoneX 绘制的时候始终会出现大量空白区,另一个则是出现概率较高的页面绘制失败问题(可能是绘制的页面太多了)。两个问题对于用户体验都是破坏性的,我们只好放弃这个方案。

  • 及时沟通非常重要。做一些想当然的假设会给自己带来很多麻烦,举个例子:我们的小程序会展示师生进图书馆和借阅书籍的数据,当时我认为“如果有借书,那么一定是有进图书馆的”,这个逻辑乍看确实没啥问题,你不进图书馆,怎么借书呢?但事实是,还真有这种情况 —— 比如学生 A 用自己的校园卡刷卡进馆,然后用学生 B 的校园卡刷卡借了一本书,那么,学生 B 不就是没有进馆,但是却借了书吗?如果有事先和其他人确认这种情况,这种疏忽是可以避免的。
  • 还有其它千奇百怪的问题,再说下去可能篇幅就不够了,后面再写一篇文章总结一下这个项目。

由于宣传排期的问题,项目没有如期在 29 号上线,而是推迟到了 1 月 6 号,但从大家的留言以及访问量来看,小程序还是挺受欢迎的。我觉得我们的目标也达到了,给自己和小伙伴们点个赞。

2021 年的一些计划

实际上,今年做的事情也并不是很多,尤其是技术方面,在后半段就有点乏力了,明显没有一两年前学习时的那种冲劲。原因有很多,但无论如何,必须得尽早摆脱这种状态。2021 年的具体计划,其实还没想好,但大概会做下面这些事:

  • 学习 | 写作:从上面的文章清单可以看出,自 9 月 18 号之后,我就没有再发布新的文章了,也就是说,下半年那学期的文章产出几乎为 0。忙于项目是一方面,另一方面也说明了我很长时间没有学习新知识了,2021 年要回归正常状态,持续学习并输出文章
  • 书籍阅读 | 付费课程:看完红宝书第四版,以及其它的几本技术书籍;之前在极客时间和拉勾上买了一些课程,也要慢慢消化掉
  • 基础巩固:JS 算法搞起来,一些偏底层的东西可能要重拾;CSS 会整理并积累一些优秀的代码、开发技巧,常见的特效最好自己都能手写实现一遍
  • 学习新的技术:可能是 uniapp 或其它框架
  • 其它:继续深入学习 Vue 技术栈;另外会看看浏览器、webpack 方面的东西

最后,祝阅读到这里的各位,2021 年诸事顺利。

本文参与了 SegmentFault 思否征文「2020 总结」,欢迎正在阅读的你也加入
查看原文

赞 0 收藏 0 评论 0

Chor 关注了用户 · 2020-11-06

芒果果 @wangying_5ea4fb9de961c

一路走走看看,顺便留下点什么。

关注 60

Chor 赞了文章 · 2020-10-18

微信小程序之登录态的探索

上一篇:开发微信小程序必须要知道的事

登录,几乎什么项目都会用到,其重要性不言而喻,而小程序的登录却一直是为人头疼的一件事,这里我分享下我们在小程序登录上的探索

通常的登录都是通过一个表单,这很正常,但如果在小程序里你也这么做那就有点不可思议了,微信的一键登录对用户体验有多好你难道不知道?不用是不是脑子有坑?最主要——你要利用微信的生态必须需要用微信的登录,以获取相关信息来和微信交互,OK,我们进入正题

用户在小程序、小游戏中需要点击组件后,才可以触发登录授权弹窗、授权自己的昵称头像等数据

友情提示一下:wx.login并不需要点击组件,需要的是wx.getUserInfo,但通常我们都会用到UnionID、encryptedData、iv等信息完成完整的登录流程,本文主要聚焦的也是这种场景

所以之前直接通过调用API的方式就行不通了,那么问题来了——这个点击按钮要放到哪里?

  • 放到首页,一进小程序就必须先登录。这样显然很粗暴,而且问题并没有解决,反而会把用户直接拒之门外,毕竟你不是用小程序做后台系统,什么场景都需要授权,先授权也是必须的
  • 在需要授权的时候跳到登陆页面。这样就解决了上面遇到的不需要授权的时候也被强制授权,可是这样好吗?

    • 体验上不好,操作被打断,尤其整个页面都不需要授权只有在一个地方需要授权的,例如:你正在读一篇文章,读罢深有感触,想评论一番,洋洋洒洒几十字写完正准备点击呢,他妈的跳转了!跳转了!
    • 又一个漏斗,增加用户流失率。还TM要登录!很多用户心里一定这么想

那就直接放在需要登录的页面上(这不是漏斗吗?很多读者一定这么想。但想想看上面那个场景,点评论时只是需要点击下弹出的登录按钮,而且还假模假样的以微信的口吻提醒你需要登录,那你会不会登录?最起码你很愿意登录,而且来的很突然,我控几不住自己的手就点了!点了!)

可是这种方式有一个问题

怎么在需要的页面都能弹出登录按钮

应该很多人都能想到:抽离出组件,那怎么保证在需要的页面都有这个组件呢?错杀一千也不能放过一个!把登录组件集成到共用的父组件,然后在每个页面都使用。我也建议这么做,因为这个共用的父组件其实又很多用处,例如iPhoneX适配等

等等,什么都准备好了,什么时候需要登录呢?XX,这个肯定是你自己控制的啦。嗯~好吧,我们来理一理

在哪里校验是否需要鉴权

请求接口的时候,嗯~这是大家的共识

BOSS来了

怎么鉴权

Image text

官方的这张图已经做了很详尽的说明,这里不做赘述
但是看到session_key了吗?看到官方同时说的了吗

clipboard.png

所以问题又来了

怎么保证session_key的有效性

诚如上图

  • 要保证调用接口时后端session_key不失效,只能在每次调用前先使用wx.checkSession检查是否有效
  • 实践中也发现wx.checkSeesion非常耗时,大约200ms,所以也不能每次接口调用前都使用wx.checkSession检查是否有效
  • 同时要注意⚠️前端不能随便重新执行wx.login,因为可能导致正在进行的其它后端任务session_key失效

天啦噜,怎么办?!
通过实践和偶然的发现——官方的示例代码

clipboard.png

得知:在使用小程序期间session_key是不会失效的,so,你想到了什么?

  • 在每个请求前去校验有效性
  • 将校验有效性的结果存储起来
  • 通过async/await和刚才存储起来的结果来保证不过多调用wx.checkSession

先问个问题:你准备用什么方式来存储校验的结果?
。。。
让思考先飞一会
。。。。。。
。。。。。。。。。
。。。。。。。。。。。。
storage吗?当然可以,不过不够完美,为什么?因为storage是永久的存储,而session_key的有效期却只是在使用小程序期间,所以你需要在小程序结束后手动重置该状态以重新校验其有效性,那是不是在app的onUnload里重置呢?不是!开发过小程序的应该都知道,那就是结束使用小程序的方式太多,不能保证每种方式都会触发onUnload,例如用户直接销毁了微信进程?(其实你也可以在app的onShow里搞)那用什么呢?直接用内存啊,借助内存的自动管理来智能管理,所以最终代码应该是这样的

// doRequest.js
let wxSessionValid = null // 微信session_key的有效性
// 带鉴权的请求封装
async function doRequestWithCheckAuth() {
  ...
  if (typeof wxSessionValid !== 'boolean') {
    wxSessionValid = await checkWxSession() // 检查微信session是否有效
  }
  if (!wxSessionValid) {
    await reLogin() // 重新登录
  }
  wxSessionValid = true // 重新登陆后session_key一定有效
  ...
}

这样是不是看起来比较完美了?嗯~

不知道有没有同学着急问业务侧的session(自定义的登录态)怎么没讲?嗯,那现在讲吧

怎么校验完整的认证体系

其实很简单,都不想把它作为一部分来讲,但既然讲了就必然有我想强调的

  • 校验微信端的session_key略有麻烦,但不应该把它抛给服务端

    • 服务端不能直接校验session_key的有效性而是通过调用接口发现错误了才知道失效了,这是被动的
    • 服务端需要同时维护两个session

而放在前端我们只需要校验两个session的有效性即可,任何一个失效就重新登录,这是积极主动有效的操作,应该被提倡

贯通

OK,基本上梳理的差不多了,就差弹登录按钮了,这个简单,调用刚才封装的组件的方法就行了嘛,bingo,可是,点完允许后呢?怎么继续用户的操作呢?怎么能让用户的体验不被打断呢?先回放下刚才reLogin的代码

async function reLogin() {
  // 确保有用户信息
  await new Promise(resolve => { // ⚠️注意开头有await!!!
    wx.getSetting({
      success: (res) => {
        // 如果用户没有授权或者没有必要的用户信息
        if (!res.authSetting['scope.userInfo'] || !_.isRealTrue(wx.getStorageSync('userInfoRes').userInfo)) {
          navToLogin(resolve) // 去提示用户点击登录按钮,⚠️注意:并把当前的resolve带过去
        } else {
          resolve() // 静默登录
        }
      }
    })
  })
  return new Promise((resolve) => {
    wx.login({
      success: res => {
        login(res.code).then((jwt) => {
          resolve(jwt) // resolve jwt
        }) // 通过code进行登录
      },
      fail(err) {
        wx.showToast({
          title: err.errMsg,
          icon: 'none',
          duration: 2000
        })
      }
    })
  })
}
function navToLogin(resolve) {
  /* eslint-disable no-undef */
  const pages = getCurrentPages()
  const page = pages[pages.length - 1] // 当前page
  page.openLoginModal(resolve) // 打开登录按钮弹框,并把当前的resolve带过去
}

上面的代码注释里有两个⚠️注意看到没?是的,通过回调的方式?当用户同意授权了就继续余下的逻辑,如果被拒绝了,则安利他,再拒绝就终止操作,下次需要授权也会继续弹出授权

有不明白欢迎评论留言指出,我再做说明修改
下一篇文章里会有代码呈现,完整源码以后会放出的,通过wepy搭建的一个框架
下一篇:对api请求封装的探索和总结
谢谢

查看原文

赞 65 收藏 47 评论 2

Chor 赞了文章 · 2020-09-27

浅谈script标签的defer和async

1. 什么鬼

今天在做一个小需的时候,忽然看到前辈一句吊炸天的代码

    <script data-original="#link("xxxx/xx/home/home.js")" type="text/javascript" async defer></script>

卧槽,竟然同时有asyncdefer属性,心想着肯定是前辈老司机的什么黑科技,两个一块儿肯定会发生什么神奇化学反应,于是赶紧怀着一颗崇敬的心去翻书翻文档,先复习一下各自的定义。

2. 调查一番

先看看asyncdefer各自的定义吧,翻开红宝书望远镜,是这么介绍的

2.1 defer

这个属性的用途是表明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在<script>元素中设置defer属性,相当于告诉浏览器立即下载,但延迟执行。

HTML5规范要求脚本按照它们出现的先后顺序执行,因此第一个延迟脚本会先于第二个延迟脚本执行,而这两个脚本会先于DOMContentLoaded事件执行。在现实当中,延迟脚本并不一定会按照顺序执行,也不一定会在DOMContentLoad时间触发前执行,因此最好只包含一个延迟脚本。

2.2 async

这个属性与defer类似,都用于改变处理脚本的行为。同样与defer类似,async只适用于外部脚本文件,并告诉浏览器立即下载文件。但与defer不同的是,标记为async的脚本并不保证按照它们的先后顺序执行。

第二个脚本文件可能会在第一个脚本文件之前执行。因此确保两者之间互不依赖非常重要。指定async属性的目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容。

概括来讲,就是这两个属性都会使script标签异步加载,然而执行的时机是不一样的。引用segmentfault上的一个回答中的一张图图片描述蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表 HTML 解析。

也就是说async是乱序的,而defer是顺序执行,这也就决定了async比较适用于百度分析或者谷歌分析这类不依赖其他脚本的库。从图中可以看到一个普通的<script>标签的加载和解析都是同步的,会阻塞DOM的渲染,这也就是我们经常会把<script>写在<body>底部的原因之一,为了防止加载资源而导致的长时间的白屏,另一个原因是js可能会进行DOM操作,所以要在DOM全部渲染完后再执行。

2.3 really?

然而,这张图(几乎是百度搜到的唯一答案)是不严谨的,这只是规范的情况,大多数浏览器在实现的时候会作出优化。

来看看chrome是怎么做的

《WebKit技术内幕》:

  1. 当用户输入网页URL的时候,WebKit调用其资源加载器加载该URL对应的网页。

  2. 加载器依赖网络模块建立连接,发送请求并接受答复。

  3. WebKit接收到各种网页或者资源的数据,其中某些资源可能是同步或异步获取的。

  4. 网页被交给HTML解释器转变成一系列的词语(Token)。

  5. 解释器根据词语构建节点(Node),形成DOM树。

  6. 如果节点是JavaScript代码的话,调用JavaScript引擎解释并执行。

  7. JavaScript代码可能会修改DOM树的结构。

  8. 如果节点需要依赖其他资源,例如图片、CSS、视频等,调用资源加载器来加载他们,但是他们是异步的,不会阻碍当前DOM树的继续创建;如果是JavaScript资源URL(没有标记异步方式),则需要停止当前DOM树的创建,直到JavaScript的资源加载并被JavaScript引擎执行后才继续DOM树的创建。

所以,通俗来讲,chrome浏览器首先会请求HTML文档,然后对其中的各种资源调用相应的资源加载器进行异步网络请求,同时进行DOM渲染,直到遇到<script>标签的时候,主进程才会停止渲染等待此资源加载完毕然后调用V8引擎对js解析,继而继续进行DOM解析。我的理解如果加了async属性就相当于单独开了一个进程去独立加载和执行,而defer是和将<script>放到<body>底部一样的效果。

3. 实验一发

3.1 demo

为了验证上面的结论我们来测试一下

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
        <link href="http://libs.baidu.com/bootstrap/3.0.3/css/bootstrap.css" rel="stylesheet">
        <link href="http://cdn.staticfile.org/foundation/6.0.1/css/foundation.css" rel="stylesheet">
        <script data-original="http://lib.sinaapp.com/js/angular.js/angular-1.2.19/angular.js"></script>
        <script data-original="http://libs.baidu.com/backbone/0.9.2/backbone.js"></script>
        <script data-original="http://libs.baidu.com/jquery/2.0.0/jquery.js"></script>
    </head>
    <body>
        ul>li{这是第$个节点}*1000
    </body>
    </html>

一个简单的demo,从各个CDN上引用了2个CSS3个JS,在body里面创建了1000个li。通过调整外部引用资源的位置和加入相关的属性利用chrome的Timeline进行验证。

3.2 放置在<head>

图片描述
异步加载资源,但会阻塞<body>的渲染会出现白屏,按照顺序立即执行脚本

3.3 放置在<body>底部

图片描述
异步加载资源,等<body>中的内容渲染完毕后且加载完按顺序执行JS

3.3 放置在<head>头部并使用async

图片描述
异步加载资源,且加载完JS资源立即执行,并不会按顺序,谁快谁先上

3.4 放置在<head>头部并使用defer

图片描述
异步加载资源,在DOM渲染后之后再按顺序执行JS

3.5 放置在<head>头部并同时使用asyncdefer

图片描述
表现和async一致,开了个脑洞,把这两个属性交换一下位置,看会不会有覆盖效果,结果发现是一致的 = =、

综上,在webkit引擎下,建议的方式仍然是把<script>写在<body>底部,如果需要使用百度谷歌分析或者不蒜子等独立库时可以使用async属性,若你的<script>标签必须写在<head>头部内可以使用defer属性

4. 兼容性

那么,揣摩一下前辈的心理,同时写上的原因是什么呢,兼容性?

上caniuse,async在IE<=9时不支持,其他浏览器OK;defer在IE<=9时支持但会有bug,其他浏览器OK;现象在这个issue里有描述,这也就是“望远镜”里建议只有一个defer的原因。所以两个属性都指定是为了在async不支持的时候启用defer,但defer在某些情况下还是有bug。

The defer attribute may be specified even if the async attribute is specified, to cause legacy Web browsers that only support defer (and not async) to fall back to the defer behavior instead of the synchronous blocking behavior that is the default.

5. 结论

其实这么讲来,最稳妥的办法还是把<script>写在<body>底部,没有兼容性问题,没有白屏问题,没有执行顺序问题,高枕无忧,不要搞什么deferasync的花啦~

目前只研究了chrome的webkit的渲染机制,Firefox和IE的有待继续研究,图片和CSS以及其他外部资源的渲染有待研究。

更多信息在 这里

参考

查看原文

赞 62 收藏 87 评论 13

Chor 赞了回答 · 2020-09-27

解决defer和async的区别

先来试个一句话解释仨,当浏览器碰到 script 脚本的时候:

  1. <script data-original="script.js"></script>

    没有 deferasync,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。

  2. <script async data-original="script.js"></script>

    async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。

  3. <script defer data-original="myscript.js"></script>

    defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。

然后从实用角度来说呢,首先把所有脚本都丢到 </body> 之前是最佳实践,因为对于旧浏览器来说这是唯一的优化选择,此法可保证非脚本的其他一切元素能够以最快的速度得到加载和解析。

接着,我们来看一张图咯:

请输入图片描述

蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表 HTML 解析。

此图告诉我们以下几个要点:

  1. deferasync 在网络读取(下载)这块儿是一样的,都是异步的(相较于 HTML 解析)
  2. 它俩的差别在于脚本下载完之后何时执行,显然 defer 是最接近我们对于应用脚本加载和执行的要求的
  3. 关于 defer,此图未尽之处在于它是按照加载顺序执行脚本的,这一点要善加利用
  4. async 则是一个乱序执行的主,反正对它来说脚本的加载和执行是紧紧挨着的,所以不管你声明的顺序如何,只要它加载完了就会立刻执行
  5. 仔细想想,async 对于应用脚本的用处不大,因为它完全不考虑依赖(哪怕是最低级的顺序执行),不过它对于那些可以不依赖任何脚本或不被任何脚本依赖的脚本来说却是非常合适的,最典型的例子:Google Analytics

关注 70 回答 7

认证与成就

  • 获得 509 次点赞
  • 获得 17 枚徽章 获得 0 枚金徽章, 获得 4 枚银徽章, 获得 13 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

注册于 2018-10-03
个人主页被 9.8k 人浏览