前端学习笔记整理(持续更新)

Table of Contents

读书的时候对前端热情不高所以敷衍过去了,等要用的时候才发现给自己留了坑,这就很憋屈了,于是静下心来了解一下,主要是了解前端的系统是怎么样的.

HTML,CSS和JavaScript是怎么协作,三者怎么学?

HTML元素这么多,能否分类或者怎么分类?

CSS同样也是有很多属性,如何掌握?能否分类或者怎么分类?

JavaScript又是怎么回事,语言部分要有哪些要点?APIs部分有没有系统而完善的文档?

这篇笔记主要是对所需要的概念进行理解并且整理,然后把日后开发所需的参考文档整理出来.

由于是笔记,所以会一之直更新.

P.S: 我接触过一些后端开发人员很抗拒前端,大部份认为,"前端都不涉及逻辑的,有什么意思?".这是一个错误的认识,以前的我不太清楚,但是看到现在的网站页面这么复杂还能说出这种话的人明显是没有经过思考的.

HTML

  • 元素分类

    一般来说有两类:块级元素(block-level elements)和行内元素(inline elements).

    • 块级元素: 这类元素会在新的一行开始并且占据父类元素(容器)的最大可用宽度.
    • 行内元素/内联元素: 这类元素不需要在新的一行开始并且只占据它所需要的宽度.
  • 两者的区别, block-level vs inline

    表现在两个方面

    • 内容模型(Content model)

      内容模型也就是说一个元素所期望/接受的内容.块级元素可以包含块级元素以及行内元素,而行内元素只能够包含数据以及其它行内元素.

    • 格式(Formatting)

      默认情况下,块级元素必须要在新的一行开始,而行内元素在任何地方都可以开始.

      可以通过CSS改变它们的display属性来改变它们渲染的格式,不能改变它们的内容模型,也就是说该接受的内容是不会变的,

      但是可以改变元素的一些属性,比如display为inline元素是没有height和weight属性的,改为block之后就有了.

      "display: inline"变为行内格式, "display: block"变为块级格式.

  • 新的分类方案

    目前这套分类已经是快要过时了,有一套根据内容模型进行分类进行替代-Content categories.

    其中有两个分类分别跟块级元素和行内元素粗略相同,flow content和phrasing content.

    Flow content是指用于body元素里面的元素,phrasing content是指用于文本段落中的元素.

    下面还是会用到block-level elements和inline elements分类.

CSS

盒子模型(CSS Box Model)

盒子模型是布局的基础,每一个元素都是用一个矩形盒子表示,每个盒子读有内容(content),内部填充/内边距(padding),边界(border)和外边距(margin),按照这些顺序从里到外被包起来.

box-model.png

关于 marginpadding,很多人一开始容错误使用两者,特别是 padding, padding 可是会增加盒子的宽和高,这个要记住,如果只是为了增加元素与其它之间的距离,请用 margin.

P.S: 现在应该清楚为什么十六夜咲夜被叫PAD长了吧?

盒子类型(Types of CSS boxes)

有3种最常见的盒子类型.

  • 块盒子(block box),与块级元素有一样格式(formatting)的盒子,默认的块级元素和display为block或table的元素都是块盒子.
  • 行内盒子(inline box),与行内元素有一样格式(formatting)的盒子,默认的行内元素和display为inline的元素都是行内盒子.
  • 行内块盒子(inline-block box),上面两种的混合体,既有行内盒子的格式,也有块盒子的属性,可以把盒子理解为一个结尾带 \n 的文本,行内块盒子去掉了这个 \n 符号.

下面大概介绍关于盒子的常见CSS属性,注意并不是所有盒子都可以用里面的属性,等一下解释.

  • 相关的CSS属性:
    1. box的宽高(内容的宽高)以及行高

      width

      height

      min-width

      max-width

      min-height

      max-height

      line-height

    2. 内边距

      padding

      padding-(top|right|bottom|left)

    3. 边界

      border

      border-(top|right|bottom|left)

      border-width

      border-style

      border-color

      border-(top|right|bottom|left)-width

      border-(top|right|bottom|left)-style

      border-(top|right|bottom|left)-color

    4. 外边距

      margin

      margin-(top|right|bottom|left)

有一个点要注意一下,那就是刚刚提到的"并不是所有盒子都可以用里面的属性",盒子类型的不同在于元素的格式(formatting)和可用的CSS属性不同.

这也是为什么说display属性改变的是元素的属性.有个例子在上面提到过,行内元素就不能设置width和height属性(其实并不全对,像是 <img> 这种可替换元素就是例外),但是如果display改为block后就可以设置了,行内元素的宽和高是由它的内容决定的,不能设置.

CSS选择器(CSS selectors)

给元素设置元素的第一步就是选择元素,可以理解为用来匹配文本的正则表达式.由于HTML被解析为DOM Tree,那么自然就不需要正则表达式了.为了胜任工作,选择器被划分5类来满足任务中的不同需求.

选择器类型

  • 元素选择器(Type selectors)

    通过元素名字选择,比如选择页面的所有<p>元素.

    p {
        height: 100px;
    }
    
  • 类选择器(Class selectors)

    通过元素的class属性选择,比如选择页面所有class为kls的元素.

    .kls {
        height: 100px;
    }
    
  • ID选择器(ID selectors)

    通过元素的id属性选择,比如选择页面所有id为eid的元素(一般来说id是唯一的,不太可能存在多个相同的id).

    #eid {
        height: 100px;
    }
    
  • 通配选择器(Universal selectors)

    匹配所有元素,可以添加限定条件,比如选择所有class为kls的元素.

    *.kls {
        height: 100px
    }
    

    再比如选择id为eid的元素.

    *#eid {
        height: 100px
    }
    

    不过这个选择器性能不好,所以不推荐使用

  • 属性选择器(Attribute selectors)

    选择已经设定的属性或者属性值匹配的所有元素,比如选择所有设定了class属性的div元素.

    div[class] {
        height: 100px
    }
    

    再比如选择class以outline-text开头的div元素,是的包括class为outline-text-1,outline-text-2等等所有元素.

    div[class^=outine-text] {
        height: 100px;
    }
    

    一种有 种类用法(值的双引号不加也没有关系)

    • [attr]: 设定了attr属性的元素
    • [attr=value]: attr属性为"value"的元素.

      比如,匹配class为"outline"的div元素,

      <div class="outline"></div>
      

      div[class=outline]

    • [attr~=value]: attr属性为一个以空格分割的值列表,并且该表包含了"value"的元素.

      比如,匹配class的值列表包含"outline"的div元素

      <div class="outline outline-container"></div>
      <div class="outline-container"></div>
      <div class="outline"></div>
      

      div[class~=outline]

    • [attr|=value]: attr属性为"value"或者"value-"开头的div元素.

      比如,匹配class以"outline"或者"outline-"开头的div属性,

      <div class="outline-container"></div>
      <div class="outline"></div>
      

      div[class|=outline]

    • [attr^=value]: attr属性是以"value"开头的元素.
      <div class="outline-text"></div>
      <div class="outline-text outline"></div>
      <div class="outline-text-bottom"></div>
      

      比如,匹配class以outline-text开头的div元素,

      div[class^=outline-text]

    • [attr$=value]: attr属性是以"value"结尾的元素.

      比如,匹配class以"outline-text"结尾的div元素,

      <div class="outline-text"></div>
      <div class="outline outline-text"></div>
      <div class="start-outline-text"></div>
      

      div[class$=outline-text]

    • [attr*=value]: attr属性包含"value"的元素.

      比如,匹配class包含"outline-text"字眼的div元素,

      <div class="outline-text"></div>
      <div class="start-outline outline-text"></div>
      <div class="start-outline-text"></div>
      <div class="start-outline-text-end"></div>
      

      div[class*="outline-text"]

      • [attr operator value i]: 忽略大小写匹配
      operator ::= =
                 | |=
                 | ~=
                 | ^=
                 | $=
                 | *=
      

      i代表intensive,表示不区分大小写.

      比如,不区分大小写地匹配class为"outline-text"的div元素,

      <div class="outline-text"></div>
      <div class="outline-Text"></div>
      

      div[class = OUTLINE-TEXT i]

  • 选择器组合

    首先要区别什么是兄弟节点,父子节点和后代节点.

    • 相邻兄弟选择器(adjacent sibling combinator): elm1 + elm2,elm1与elm2处于同一个父节点下,elm2必须紧跟elm1才能成功选择 一个 elm2.
    • 通用兄弟选择器(general sibling combinator): elm1 ~ elm2,elm1与elm2处于同一个父节点下,只要elm2在elm1之后才能成功选择 所有 elm2(不用紧跟其后).
    • 子选择器(child combinator): elm1 > elm2,elm1是elm2的父节点才能成功选择 所有 elm2.
    • 后代选择器(descendant combinator): elm1 elm2,elm2必须是elm1的后代节点才能成功现在 所有 elm2.
    • 多个选择器: elm1, elm2, elm3, ..., elmn, 选择elm1,elm2,elm3,…,elmn.

      别忘了,多个选择器可以通过逗号分割来采用同一个样式,比如,

      .outline-text-1, .outline-text-2, p, div[class=outline] {
          color: yellow;
      }
      
  • CSS伪类(Pseudo-classes)

    伪类表示元素的某一个状态,根据元素的状态匹配.

    比如鼠标放在div元素上面的时候就变蓝,

    div:hover {
        background-color: blue;
    }
    
  • CSS伪元素(Pseudo-elements)

    伪元素可以表示元素的某一部分,根据元素的某一部分匹配.

    比如选择p元素的第一行字体变红色,

    p::first-line {
        color: red;
    }
    

CSS的值和单位

这个直接看文档比较快

CSS属性继承

简单总结一下,CSS属性有继承顺序: 重要性 > 专用性 > CSS选择器定义顺序.

首先根据CSS选择器定义顺序来决定是否继承父节点的属性,也就是覆盖.

可以通过多个不同的CSS选择器定位到同样的目标,不过不同的选择器的专用性会不一样,专用性最高的被采用,不管定义顺序.

最后,如果有的属性设定后面跟了"!important",比如"border: none !important;",就会采用这个属性,当然如果一个目

标被多个CSS选择器选中并且每个选择器都对同一个属性设定了"!important",那么会选择这些选择器中专用性最高的那个.

专用性有一个计算规则:

  1. 内联样式,也就是元素 style 属性(没有选择器)为 1000 分;
  2. 每个 ID 选择器得 100 分;
  3. 每个类/属性/伪类选择器得 10 分;
  4. 每个元素/伪元素选择得 1 分.

按照对应得分规则对一个选择器进行专用性计算.其中通用选择器 (*), 组合器 (+>~''), 以及否定伪类 (:not) 不影响该规则.

详细请看MDN的文章.

可继承属性以及不可继承属性

概念很简单,如果父/祖先元素指定了某个属性,子/后代元素没有指定这个元素,自动继承了父元素的这个属性的值,那么这个属性就是可继承属性.

反之就是不可继承属性.在文档上一般用 inherited:yes 表示可继承.

at-rule

  • @charset 定义样式表的编码,一般是"utf-8"
  • @import 导入别的样式文件
  • @namespace 告诉 CSS 引擎必须考虑 XML 命名空间
  • @page

    打印文档时修改某些 CSS 属性,只能修改 margin, orphans, window 和 page breaks of documents.

    (现在没用上,先跳过,日后更新)

  • @font-face

    下载在线字体资源,消除对用户电脑字体的依赖.

    比如,在 http://developer.mozilla.org/@api/deki/files/2934/=VeraSeBd.ttf 下载 Bitstream Vera Serif Bold 字体.

    @font-face {
        font-family: "Bitstream Vera Serif Bold";
        src: url("http://developer.mozilla.org/@api/deki/files/2934/=VeraSeBd.ttf");
    }
    
    body {font-framily: "Bitstream Vera Serif Bold", serif}
    
  • @keyframes

    定义关键帧动画.

    下面例子,鼠标停留在图片上然后播放缩放动画.

    @keyframes zoom-out-in {
        from {
            width: 10%;
            height: 10%;
        }
    
        50% {
            width: 15%;
            height: 15%;
        }
    
        to {
            width: 10%;
            height: 10%;
        }
    }
    
    img {
        height: 10%;
        width: 10%;
    }
    
    img:hover {
        animation-name: zoom-out-in;
        animation-duration: 3s;
        animation-timing-function: ease-in-out;
    }
    

    其中, fromto 分别是 0%100% 的别名,它们分别表示在 0%100% 的时候的帧.

    还可以设定更多帧.这个例子有设定了3个帧.然后在 img 元素调用这个动画,并且设定播放时间为3秒,以 ease-in-out 方式播放.

  • 嵌套@规则,既可以像上面的几个rules一样做为一个语句,也可以用在条件规则组里面.

    所谓条件规则组就是表达: "条件是否等于true或false,为true那么它们里面的语句就生效".

    条件规则只包含 3 个, @media, @supports 和 @document

    • @media

      媒体查询,响应式的关键元素.

      @media screen and (min-width: 900px) {
          article {
              padding: 1rem 3rem;
           }
      }
      

      @media screen and (min-width: 900px) 就是条件,这个条件是"设备是否彩色的电脑屏幕并且文档的宽度大于或等于900px".条件为真就应用样式.

    • @supports

      支持查询,用来判断某个属性/某个属性设定某个值/某种选择器是否被浏览器支持.

      这里有例子.

    • @document

      (之后补充)

CSS布局

以下是几种常见的布局技术

正常流(normal flow)

默认布局方式,每个元素会按照源码先后次序垂直显示.position属性为static,float为none,并且没有对display属性进行设置就会保持正常流布局.

文档会按照源代码的元素顺序上下排列,称做文档流,是一个层,之后会发现文档不只是只有一个层.

浮动布局

元素一旦设置为浮动就会脱离正常文档流(实际上仍然是文档的一部分),它会向左或者右偏移,直到碰触到容器的边缘或者另外一个浮动元素.

通过设置 float 属性让元素浮动.多列浮动要注意优先级,按照源码的定义顺序,先定义的级别高,那么浮动时候就先设定级别高的,

比如代码

<h1>3 column layout example</h1>
<div>First column</div>
<div>Second column</div>
<div>Tnird column</div>

优先级: First column > Second column > Third column

body {
    width: 90%;
    max-width: 900px;
    margin: 0 auto;
}

div:nth-of-type(1) {
    width: 36%;
    float: right;
}

div:nth-of-type(2) {
    width: 30%;
    float: right;
    margin-left: 4%;
}

div:nth-of-type(3) {
    width: 26%;
    float: right;
}

显示顺序为"Third column" "Second column" "First column".

如果浮动元素前面定义的元素 alpha 是块盒子,那么浮动元素就会在 alpha 的下面进行浮动;如果 alpha 是行内盒子,浮动元素就会无视 beta 进行浮动,两者可能会在同一行内,也就是 alpha 会给浮动元素让位.

如果浮动元素后面定义了一个元素 beta,那么 beta 很可能会与浮动元素处于同一行,不论盒子类型,要保证 beta 在浮动元素的下面,也就是 beta 不受浮动影响,可以通过给 beta 设置 clear 属性消除浮动元素带来的影响.

假设浮动元素为右浮动 float: leftdiv, betadiv, 那么 beta 就会贴近浮动元素的右侧,那么可以给 beta 设置 clear: left 清楚 beta 左边带来的浮动.

clear 属性不仅可以用在非浮动(non-float)元素上面,也可以用在浮动元素上面,用来消除别的浮动元素带来的浮动影响.

定位布局

通过 position 属性设置元素的位置.

  • 静态定位

    值为 static. 定位的元素就是按照普通的正常流布局.

  • 相对定位

    值为 relative, 定位的元素跟 static 一样处于正常的文档流中,但是可以通过配合 top, bottom, left, right 属性修改元素位置,并且不会影响任何其它元素的定位.

  • 绝对定位

    值为 absolute, 定位的元素不再处于正常的文档流中,它到了另外一个层上面,默认高于文档流一层.也可以通过配合 top, bottom, left, right 属性修改元素位置,和 relative 不一样, absolute 会影响其他元素的定位.

    • z-index

      z-index 属性可以设置元素的层级,之所以叫 z-index 是因为对z轴参照.值是整数,值越大处于的位置就越高.=z-order= 也可以打破这个计算方式.

  • 固定定位

    值为 fixed, 定位的元素不再处于正常的文档流中,跟 absolute 一样到了另外一个层上面,默认高于文档流的一层.也可以通过配合 top, bottom, left, right 属性修改元素位置.

    但是不同于 abosulte, 它的位置是坐于 viewport 中,也就是说不管页面拉动到文档的哪个位置,定位的元素会一直都会定在那里(可以想一下右下角烦人的页面的广告),进行这个元素会跟着滚动一样.

  • Position: sticky

    relativefixed 的混合体,允许元素像 relative 一样动作,直到滚动到某一个阈值点,之后变得 fixed.

  • 调整元素位置

    上面的这么多定位中,只有相对定位,绝对定位,固定定位以及 sticky 可以使用 top, right, bottom, left 这个4个属性来调整定位元素(positioned elements)的位置.

    • 对于绝对定位,它们是指定元素和容器元素之间(容器元素需要设置 position: relative)边界的距离,比如容器元素A包含一个 left: 10% 的子元素a,那么a的左边到A左边的距离为A宽度的10%.
    • 对于固定定位,它们是指定元素和 viewport 之间的边界距离.
    • 对于相对定位,它们是指元素和相邻元素之间的边界距离,如果没有相邻元素,那就是和容器元素之间的距离.
    • 对于sticky,当指定元素在 viewport 内,那么就表现和 relative 一样,当在 viewport 外就表现和 fixed 一样.

    如果元素的 topbottom 属性都设定了,并且没有指定 height 属性或者设定为 auto 或100,那么 topbottom 之间的距离就是元素的高度;假如 height 属性固定了,那么会优先处理 top 属性并且无视 bottom 属性.

    如果元素的 leftright 属性都设定了,并且没有指定 width 属性或者设定为 auto 或者100,那么 leftright 之间的距离就是元素的宽度;假设固定了 width,那么就要考虑两种情况,假设容器方向是从左往右(可以通过 direction 属性进行设定),那么会优先处理 left 属性并且无视 right,反之亦然.

    但是实际上有一个利用它们来居中元素的例子: https://stackoverflow.com/a/18106475

Flex布局

  • 概念

    是一个种一维的布局模型,一次只能处理一列或者一行. Flexbox 会涉及到两根轴(axes),主轴(main axis)和交叉轴(cross axis),交叉轴垂直于主轴.

    实际开发中只能设定主轴,交叉轴就是概念上的东西.

    主轴通过 CSS的 flex-direction 属性定义,它有4个值:

    • row
    • row-reverse
    • column
    • column-reverse
  • 关于起始线和终止线

    Flexbox 不会假设文档的书写模式(the writing mode of the document).以前的 CSS 布局把文档书写模式认为是水平和从左到右的书写模式.

    不同语言的方向会不一样,比如英语是从左到右,阿拉伯语是从右到左.

    两条轴都有起点和终点(起始线和终止线).

    假如主轴的方向是 row ,文本是英语的话,起始线在左边,终止线在右边.如果是阿拉伯语就相反.交叉轴的起始线和终止线都一样,分别是上边和下边.

  • Flex 容器
    .elm {
        display: flex;
        flex-direction: row;
        flex-basis: auto;
        flex-wrap: nowrap;
    }
    

    上面都是一个 flex 容器的默认属性.

    容器里面的元素从主轴的起始线开始.元素不会在主轴方向延伸,如果元素的宽度大于容器,

    容器会自动缩小(缩小是有限度的).但会延伸填充交叉轴.

    • 多行显示的 flex 容器, flex-wrap

      如果元素太多无法一行显示,那么可以设定 flex-wrapwrap 来换行.

    • 其它的一些属性
      • align-content
      • align-items

        沿着交叉轴对齐容器内的元素.4个可选值:

        1. stretch
        2. flex-start
        3. flex-end
        4. center
      • justify-content

        沿着主轴对齐容器内的元素.6个可选值:

        1. stretch
        2. flex-start
        3. flex-end
        4. center
        5. space-around
        6. space-between
      • flex-flow

        flex-directionflex-wrap 的混合.

        .elm {
             display: flex;
             flex-flow: row wrap;
        }
        
  • Flex 项
    • align-self

      设置沿着交叉轴对齐,默认继承父元素的 align-items 的值,可以用于单独设置项的对齐.

    • flex 容器里元素的属性

      默认值是0,是 flex-grow 的值.

      参考资料,这个属性是掌握 flexbox 布局的关键.

      flex 是下面3个的 shorthand(按顺序).

      1. flex-grow, 一个项需要多少正自由空间(positive free space)
      2. flex-shrink, 一个项需要被移除多少负自由空间(negative free space)
      3. flex-basis, 在增长和缩减发生之前项的基本大小

        默认值是auto,以该项的本身 width (flex-direction 为 row)或者 height (flex-direction 为 height)作为大小.

        如果为0则表示后面的可用空间被自动分配.

      关于正负自由空间概念可以看以下的两个例子,

      1. 每个项的大小均为 100px,正自由空间为 200px,如果需要让这三个项填充满 container, 这个自由空间可以用来分配给各个项.

        Basics7.png

      2. 每个项的大小均为 200px,那么三个项的总长度 600px 超出 container 100px,那么负自由空间为 100px,为了让三个项目填充满 container ,可以删除这个负自由空间.

        ratios1.png

      flex-growflex-shrink 的值可以是任何整数,它们是如何计算?

      其实很简单,用上面的第二个例子来说,如果三个项的 flex-grow 分别为 x, y, z.

      第一个项的长度为 (x / (x + y + z)) * width-or-height-of-container,如此类推.同样 flex-shrink 也是同样道理.如果每个项的 flex-grow 的值相等(任何正整数),它们就是等长.

      只有正/负操作自由空间的情况下才会发生增长/缩减.除了通过这两个属性来控制自由空间,还可以通过 justify-content 来控制,差别在于前者填充,后者不填充,还可以通过使用 margin 来控制项之间的空隙.

      使用 flexbox 注意处理跨浏览器的问题.

  • 教程推荐

    当你了解上面的东西以后是时候要实践以下了,油管上有很多优秀的前端教学视频,这里有一个关于 Flexbox 使用的教程,直奔重点毫无废话,个人非常推荐:https://www.youtube.com/watch?v=k32voqQhODc.

Grid布局

  • 概念

    是由水平线(horizontal lines)和垂直线(vertical lines)组成的网格(Grid),两条水平线组成一行,两条垂直线组成一列,行与列交叉点是一格,叫做 grid cell.

    1_diagram_numbered_grid_lines.png

    Figure 1: grid lines

    1_Grid_Cell.png

    Figure 2: grid cells

    任意两条线之间的空间叫做一个网格轨道(grid track),分为行轨道(row track)和列轨道(column track).

    1_Grid_Track.png

    Figure 3: row tracks

    线之间的距离叫做轨道大小(track size),分为固定轨道大小(fixed track size)和灵活轨道大小(flexible track size),用固定单位定义的大小就是固定轨道大小,比如像素;用百分比或者专门的 fr 单位定义的大小就是灵活轨道大小.

    可以通过 grid-template-rowsgrid-template-columns 定义行和列.

    一般来说,只需要定义 grid-template-columns 属性就可以,浏览器会根据内容来自动创建行,这些被创建的行就是隠式网格中.

    显式网格是定义了 grid-template-columnsgrid-template-rows 属性的网格,同样如果显式网格中的内容需要更多网格轨道时候就会自动创建隠式网格.

    按照默认,隠式网格的轨道会根据它里面的内容定义尺寸.可以通过 grid-auto-rows 和 grid-auto-columns 设定隠式网格中轨道大小,可以结合 minmax() 给隠式网格设定最小最大高度.

    关于 grid 的灵活单位的计算方式,实际上和 flexboxflex 属性的处理过程是一样的.比如要求一个行轨道有3个等宽的cells,那么可以这么设置 1fr 1fr 1fr 或者 repeat(3, 1fr) 1可以换成任何正整数,比例上相等(都是1/3)就是宽度相同.

    还有要注意,每个grid项都可以设置 display: flex 成为子 grid,也就是说 grid 可以嵌套 grid.

  • 关于Grid项的属性

    上面已经介绍大部份的 grid 的属性了,现在介绍一些修饰网格项的一些属性.

    通过指定线来定位项,线的系统可以参考上面的线图,这些属性为 grid-(column|row)-(start|end),给grid项这4个属性分别设置线的编号就可以指定它多大以及位置.

    通过这4个属性可以让一个grid项占据多个 grid cells,项占据的地方叫做grid区域(grid areas).其中 grid-(column|row) 是前面几个的简写.

    1_Grid_Area.png

    Figure 4: 网格区域

    比如

    .box {
        grid-column-start: 1;
        grid-column-end: 2;
        grid-row-start: 1;
        grid-row-end: 4;
    }
    

    等于

    .box {
        grid-column: 1 / 2;
        grid-row: 1 / 4;
    }
    

    事实上, grid-(column|row)-end 可以不用设置,浏览器会自动分配,当然这要看容器里面其它容器的 grid-(column|row)-start 的设置.

    更多关于默认span的可以看这里.

    grid-area 这是 grid-(row|column)-startgrid-(row|column)-end 顺序缩写,

    比如上面的例子可以写成,

    .box {
        grid-area: 1 / 1 / 4 / 2;
    }
    
    

    事实上,可以使用线编号的负数来倒数布局,可以看这里.

    grid-area 还有更好玩,更抽象的玩法,具体就不写了,看这里.网格线除了可以通过线编号来布局,还可以通过命名线来布局.

  • 关于Grid的一些其它属性
    • (column|row)-gap

      这两个属性和 grid-(column|row)-gap 是一样的, grid- 开头的版本以后会被移除,考虑到旧的代码,它们会做为前者的别名使用下去.

      由于有一些浏览器没有支持前者,所以最好两个版本都用上.

      这个两个属性是设置项之间的间距,可以理解为粗线(fat line).在给 fr 轨道分配长度之前会先给 gaps 分配空间,并且这些空间内不能放置任何东西.

      grid-gapgrid-row-gapgrid-column-gap 的顺序简写.

Flexbox 和 Grid

两种布局方式看似差不多,分别是一维和二维布局, Flexbox 会自动根据内容来换行,同样 Grid 布局也是一样,不过后者比前者多了一个关于行的控制权.

如何在 FlexboxGrid 之间选择?

  1. 是否需要同时控制行和列?如果是就选择 Grid,否则 Flexbox.
  2. 从内容出发.如果有一组元素并且希望它们可以平均分布在容器中,或者让内容大小来决定如何每一个项占据多少空间,如果需要换行,浏览器就会根据剩余的内容大小和当前行的可用空间来分配空间.
  3. 从布局出发.先创建网格再放置元素到网格内容,或者让元素按照网格排列,我们可以创建根据内容改变大小的网格轨道,不过整个轨道都会随之改变.
  4. 如果使用 Flexbox 时候发现需要禁用一些弹性特性,那么可能需要使用 Grid 布局.比如需要给一个 flex 项设置百分比宽度来让它换到上一行,那么 Grid 可能是一个更好的选择.

Floating, Positioning, Flexbox 和 Grid.

Flexbox 是第一个拥有合适的对齐控制的布局技术.

更深入盒子模型以及布局

  • Visual formatting model

    处理和显示文档的算法,具体请阅读视觉格式化模型,由于涉及多个术语,因此推荐看已经额外整理好术语的中文文档.

    还有一些别的术语,viewport, viewport是指可以浏览器中文档的可视区域,假如一个页面需要滚动才可以看到其它部分,那么没有被看到的部分就不是viewport,当前看到的就是viewport.

CSS 属性分类索引

由于 MDN 没有给出一个根据分类来划分的 CSS 索引,所以我就根据它的关键字索引来自己整理一份方便记忆,顺便混个眼熟.

JavaScript

首先浏览器这个 JavaScript 解析器构成比较复杂,它由三个部分组成,每个部分或许由自己的标准,每个部分由不同组织维护.

这三个部分分别是 BOM(Browser Object Model), DOM(Document Object Model) 以及 ECMAScript, 三者中只有 BOM 是没有标准的,

DOM 又是属于 BOM 的一部分, BOM 是浏览器厂商所实现的,每个厂商都会有自己独特功能,所以这套 API 在不同浏览器上是不一样的;

DOM 的标准则是由 W3C 维护的,由浏览器厂商实现;

ECMAScript 是一门语言的规范,我们口中说的 JavaScript 就是以它作为语言规范来实现的,它是由 ECMA 这个组织维护的,同样也是由浏览器厂商实现,

虽然说有一套标准,但不同浏览器厂商的实现进度不一样,即便在新版的浏览器中也需要考虑兼容问题.

浏览器API的层次和结构

浏览器的API是有层次结构的,这有一个好处就是方便我们去了解和记忆.最顶层的API类别是BOM(Browser Object Model),由于没有一个标准,所以BOM这个概念没怎么在MDN上面被提及到.

不过大部份浏览器的BOM APIs都是差不多的,不过的不过,实际开发还是注意做好适配.以下就是BOM的大概模型图.

BOM.png

针对图里面出现的几个进行一些说明.

window Window对象,公开所有浏览器指定的信息 window.navigator Navigator对象,关于浏览器本身的信息,比如版本,厂商,插件等 window.screen Screen对象,用户的屏幕 window.history History对象,用户浏览历史 window.location Location对象,当前的URL window.document Document对象,当前显示的页面 window.localStorage和window.sessionStorage Storage对象,浏览器的本地储存 window.XMLHttpRequest XMLHttpRequest对象,在早期称为,Asynchronous JavaScript and XML(Ajax),它倾向于使用XMLHttpRequest异步发送XML请求数据.在今天仍然用Ajax描述使用XMLHttpRequest或者fetch发送数据格式为XML,JSON等等数据格式的请求.

每一种对象都是一个类别,其中Document对象的API就是非常有名的Document Object Model,简称DOM.它也有自己的一套标准,可以看出它有多重要.

window对象是全局的,也就是说上面的API可以不写 window..

操作文档

浏览器会把HTML文档解析成树(Tree/DOM Tree)结构,树的话就不能不提到节点(Node/DOM Node),树是由多个节点组成的.还需要了解节点之间不同的关系,什么父节点,子节点,兄弟节点,根节点,后代节点等等,这个请参考上面给出MDN的文档.

利用DOM API获取Node

var node_div = document.querySelector('div');
// 获取第一个div元素,如果不存在div节点,node_div为null
var nodes_div = document.querySelector('div');
// 获取所有div元素,如果不存在div节点,nodes_div为长度为0的NodeList

其中,document.querySelector的参数是CSS Selector字符串,可以说新生的 document.querySelectordocument.getElementBy* 方法的替代品了(不过别忘了旧浏览器).

接下来新增加一个p节点,并且把它添加到第一个div节点里面,最后把p节点移除掉.

var node_new = document.createElement('p');
// 新建一个新的p节点
node_new.textContent = 'I am the new node to be append to the first div';
// 设定文本
node_div.appendChild(node_new);
// 添加到第一个div节点里面
// 如果直接把出现在页面的节点appendChild到别的节点上,就是移动节点了
node_new.parentNode.removeChild(node_new);
// 等同于 node_div.removeChild(node_new);

修改节点样式

node_div.style.backgroundColor = 'black';
// 把div节点的背景颜色改为黑色
// 给div节点添加样式 div.setAttribute

常用的方法参考

GlobalEventHandlers是一个mixin,是HTMLElement,Document,Window和WorkerGlobalScope类的公共接口.

包含一些十分有用的事件接口.比如设定鼠标/点击事件,直接就可以调用,如果没有想要的事件类型那么可以用下面的事件.

关于事件

Document,Window和Element对象都可以调用addEventListener方法添加事件回调,removeEventListener方法移除.

node_div.addEventListener('mouseenter', event => {console.log(event)});
// 如果想获取回调的调用者本身,那么就要把箭头函数换成普通的函数,因为箭头函数没有自己的this,arguments,super和new.target.
// 当然也可以利用词法作用域来把this绑定到里面.
node_div.addEventListener('mouseenter', function(event){console.log(this.textContent)});

// 关于removeEventListener,如果要移除回调的话需要添预先定义好的函数,忘了上面两句,重新添加
function mouse_enter_event(event){
    console.log(this.textContent);
}
node_div.addEventListener('mouseenter', mouse_enter_event);
// 开始移除
node_div.removeEventListener('mouseenter', mouse_enter_event);

关于addEventListener的详细用法请看这个链接

https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#The_event_listener_callback

关于事件类型请看这里

https://developer.mozilla.org/en-US/docs/Web/Events

从服务器获取数据

可以用Python3的http.server模块或者Emacs的simple-httpd快速搭建本地服务器

假设前端文件在 /path/to/htmls

用Python3

cd /path/to/htmls
python3 -m http.server

用simple-httpd,在 scratch 输入以下语句并执行或者执行 M-: 输入以下语句

(setq httpd-root "/path/to/htmls")

然后执行 M-x httpd-start 运行服务器.

假设访问'api/things/1'获取数据打印出来.

XMLHttpRequest

var request = new window.XMLHttpRequest();
request.open('GET', "/api/things/1");
request.responseType = 'text';
request.onload = function() {
    console.log(request.response);
}
request.send();

Fetch

window.fetch("/api/things/1").then(function(response) {
    response.text().then(function(text) {
        console.log(text);
    });
});

fetch 返回的是一个 window.Promise 的对象,它的 then 方法返回的也是一个 window.Promise 对象,关于 promise协议 请参考这份API文档(我记得Python的Celery的 AsyncResult 对象也是遵守这种协议).

对于fetch的回调里面的response,可以参考下面给出的文档链接.

关于 promise协议 我以后会单独写一片文章或者直接在这里补充上.

画图

教程就要是围绕 canvas2D apis 来绘图和做动画,还有使用 three.js3D 动画的教程.

整个教程十分长,所以具体不怎么贴代码,大概总结一下 api 就可以,还有关于 three.js 的使用可能需要一点 OpenGL 的概念,

由于我学这个教程之前了解过 WebGL 渲染线管的概念,所以学起来还是挺自然的,但是由于 3D 这一块是大话题并且教程使用的不是原生 WebGL,

所以 3D 这块我不在这里总结,我目前也在准备学 OpenGL (等我复习一下C++),以后我会单独做笔记.

获取页面上的 canvas 并且生成 2D 上下文

var canvas = document.querySelector(".myCanvas");
var ctx = canvas.getContent('2d');

简单的矩形

ctx.fillStyle = 'rgb(0, 0, 0)'; // 设置填充样式,rgb或者rgba
ctx.fillRect(x, y, width, height); // 以(x,y)坐标左上角顶点绘制宽为width和高为height的矩形
ctx.clearReact(x, y, width, height); // 清除刚刚绘制的矩形

笔划和线条宽度

ctx.strokeStyle = 'rgb(255, 255, 255)'; // 设置笔划线(内部不是实体的线条)的样式
ctx.strokeRect(x, y, width, height); // 用笔划线绘制矩形
ctx.lineWidth = 5; // 设置线条的宽度

绘制路径

ctx.beginPath();  // 设置画笔当前的点为绘画起点,新canvas的坐标是(0,0)
ctx.moveTo(x, y); // 把画笔移动大别的点上
ctx.lineTo(z, y); // 在(x,y)到(z,y)之间连接一条线
ctx.fill();       // 根据前面的连线进行绘制,如果想用笔划线绘制,可以用 ctx.stroke();

绘制圆

ctx.beginPath();
// (x,y)做为起点,r是半径,0是开始的角度,1*Math.PI是结束时候的角度,两者都是用弧度表示,true表示以逆时针方向绘画
// 得到的路径是上半边的圆弧
ctx.arc(x, y, r, 0, 1*Math.PI, true);
ctx.lineTo(2*r+x, y); // 把半圆弧底部连接起来
ctx.stroke();         // 如果是 ctx.fill();那么半圆就会变成实体

绘制文本

ctx.strokeStyle = "white"; // 设置笔划线样式,实体线 ctx.fillStyle = "white";
ctx.lineWidth = 2;
ctx.font = "36px arial";   // 设置字体
// 在(x,y)上用笔划线绘制 "Canvas text" 字样, 实体线 ctx.fillText('Canvas Text', x, y);
ctx.strokeText('Canvas text', x, y);

在画布上绘制图像

var image = new Image();
image.src = 'image.png';
image.onload = function() {
    // 第二个到第五个参数设定截取源图片的起点坐标和长宽
    // 最后四个参数设定截取后的图片放置的起点和长宽
    ctx.drawImage(image, 20, 20, 300, 400, 0, 0, 300, 400);
    // drawImage方法还有其它用法的,自己看文档
};

这个可以用来实现截图.

循环和动画

主要是讲了制作二维动画的原理:循环和重新绘图.具体看文档,提一下 api .

里面用到的 api 有:

  1. ctx.rotate(1*Math.PI)

    180度旋转画布.

  2. ctx.translate(-(width/2), -(height/2))

    移动坐标轴系统, x轴 移动 -(width/2) 距离, y轴 移动 -(height/2).

  3. window.requestAnimationFrame(draw)

    draw 是绘图的函数,它里面调用 window.requestAnimationFrame(draw),递归形成循环播放.

    window.requestAnimationFrame 的作用就是执行动画,它接受一个绘图函数来更新(重新绘制)动画的下一帧.

    不用这个方法的画是没有动画效果的,所以不能把它改成 draw().

视频与音频API

主要演示如何利用 window.HTMLMediaElement 对象的 API 自定义视频的播放控制板,实现快进和快退.

页面的视频元素 <video><audio> 就是整个项目的上下文.

还演示了 setIntervalclearInterval 两个函数分别用于设置定时循环事件和取消事件.

// 200 毫秒执行一次
var id = setInterval(
    function(){var date = new Date(); console.log(date.toTimeString());}, 200);
// 取消事件
clearInterval(id);

还有一个值得一提的事件 timeupdate ,可以用于 <video> 或者 <audio> 监听当前播放时间的变化.

客户端储存

传统方法: cookies

优点:支持非常旧的浏览器

缺点:过时,安全性差,无法储存复杂数据

// 获取cookie,但是值是一个字符串,不能很自然地处理
window.document.cookie

Web Storage API

有两种类型, sessionStoragelocalStorage.

前者保存的数据只存活于浏览器没被关闭的时间里面;而后者会保留的数据会一直存在,哪怕是浏览器关闭了.

两者都是 window.Storage 的实例,所以API都是一样的.

与cookies一样,Storage也是为每个域名储存的.

// 储存数据,数据会被转成字符串
window.localStorage.setItem('count', 1);
// 获取数据
console.log(window.localStorage.getItem('count'));
// 删除数据
window.localStorage.removeItem('count');

IndexedDB API

IndexedDB 是一个完整的数据库,按照分类来说是 NoSQL 数据库.

用法不是一两句代码可以演示完,所以直接给出官方文档教程链接.

https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API

https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API/Using_IndexedDB

以后有时间再单独写一片文章或者原地补充

未来: Cache API

一个简单的例子,把网站的首页离线保存起来,这个例子利用了 service worker,

可以浏览下面的 Service Worker API 来了解更多,它不是缓存的一部分,可是又脱离不了关系.

Service worker 还是试行中的功能,所以要判断浏览器是否支持 ServiceWorkerContainer.

if('serviceWorker' in window.navigator) {
    window.navigator.serviceWorker
                    .register('/js/sw.js')
                    .then(function() {
                        console.log('Service Worker Registered');
                    });
}

同样也要判断是否支持 Cache API.

if ('caches' in window) {
    // register 'js/sw.js'
}

其中 '/js/sw.js' 表示 service worker 要注册的动作(一个JavaScript文件),这个例子的主要是缓存首页资源.

// sw.js
self.addEventListener(
    'install',
    function(e) {
        e.waitUntil(
            caches.open('cachename').then(
                cache => cache.addAll(
                    [
                        "/js/index.js",
                        "/css/style.css",
                        "/"
                    ]
                )
            )
        );
    }
);

cache.addAll 就是把要缓存的资源添加进去.除了缓存页面资源,还可以缓存整个响应(response).下面给出的 Offline Cookbook 有更多的例子.

JavaScript API(语言本身)

写于 2019/11/21

这里是 JavaScript 本身的 API 阅读笔记,不涉及任何语法相关话题,主要是内置对象(builtin objects)(只讲有构造器的对象)的笔记,

这些对象在其它 JavaScript 的实现(比如node.js)中也是存在的,也就是说这是由 ECMAScript 定义的(不过这套标准也在不断改变完善).

其实内置对象不多,但内容也不少,所以这部分笔记会分段更新(是个带工程).还有就是笔记的目标都是不怎么常见,像 Date 这种就老生常谈的就不记录了.

主要对象在这里: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects.

关于某个东西(语法,内置对象以及内置对象的属性)能否在 ECMAScript 某个版本中使用可以到 MDN 相关页面的底部查看,比如 get 关键字:

(在 MDN 的页面)上找到 Specifications,那个表格就可以看到 get 在某个版本的 ECMAScript 中的具体状态(草稿/标准),还要注意旁边的备注说明.

除了 MDN 外还可以在 ECMA 的 Github 账号上去阅读语言的规范: https://github.com/tc39.

之后的计划: 完成之后,也打算去比较深入的去了解一些比较有趣的 BOM & DOM & CSSOM API,也就是重写上面的内容,

也是一个大工程,不过我尽量在保持对知识点有着比较清晰的理解下用最大速度更新(也是为了和干掉自己的拖延绝症,这是我今年的计划之一).

Object

写于 2020/4/27

几乎所有对象都是 Object 的实例: 从 Object.prototype 继承属性以及方法.

只要是涉及对象的操作都脱离不了 Object 的方法,所以如果想和对象打好"关系" Object 可以说是一个必须掌握的东西(Kira!).

  • 创建对象 Object.create

    说到

  • 复制若干个对象的属性到一个对象上 Object.assign
  • 防止对象添加/修改/删除属性的 Object.freeze 以及阻止对象添加/删除属性 Object.seal

BOM API

DOM API

关于事件 Event

事件可以划分为两种类型

CSSOM API

JavaScript 严格模式 (strict mode)

ECMAScript 5 引入了严格模式这个概念,目前所有主流浏览器已经实现了,IE 系列从 IE 10 开始就支持了.

与严格模式相对的,浏览器甚至一些 JavaScript 解析器都默认为非严格模式(non-strict mode/sloppy mode),

两者虽然看着一样,但是语义是有差别的,简单点说就是同一种语法(syntax)在不同模式的运行时会出现两种不同的结果.

严格模式名副其实的对开发人员编码要求严格,也就是不能为所欲为的编码了,这有不少好处,代码执行效率更高,代码更加规范,更加容读.

严格模式可以有效限制 JavaScript 的"奇技淫巧",本人是比较支持的.

WEB性能分析

开发用参考资料和后续学习

上面的都是用来学习/复习概念的,这部分是开发参考,方便以后开发.还有一些后续学习路径.

一些工具和开发技巧

在合适的环境下使用合适工具也是一个开发人员的必备技能,这里记录着我的一些实践.

让更改后的静态文件自动更新

写于 2018/9/4

由于浏览器的缓存问题,静态文件每次修改后都要 Ctrl-F5 强制刷新浏览器才会更新,

这有一个问题那就是用户不知道静态文件更新了,不能指望用户会及时 Ctrl-F5.

可以在静态文件的资源地址加上事件戳或者别的东西让浏览器认为这是新资源从而进行请求,

然而这由有一个问题,爬虫也会认为这是新的链接,对于搜索引擎的爬虫就不太友好.

最近找到一个不错的工具 live.js, 只要在 <head> 元素中加入以下就可以自动更新静态文件,

现改现更新,不用刷新,而且该脚本的代码量不大,想自己研究原理也不难.

<script type="text/javascript" src="http://livejs.com/live.js"></script>

给所有页面元素添设定事件

写于 2018/9/5

有两种解决方法,一种就是遍历元素设定事件,这样有两个问题,性能和资源占用会有问题,另外一个问题就是新增的元素没有被设定事件.

另外一种就是利用事件流.

浏览器处理 DOM 的事件是这样的一个过程:

当触发某一个事件的时候,比如在页面某个元素点了一下,触发了 onclick 事件.

  1. 事件捕获阶段:事件对象(Event)通过目标的祖先元素,由 window 对象到目标的父元素传播.
  2. 目标查找阶段:事件对象到达触发事件的目标(Event.target)上面后就开始执行事件,如果事件对象指定了事件不冒泡,事件对象就会在这个阶段完成后停止.
  3. 事件冒泡阶段:事件对象从目标的父元素到 window 进行传播,这个过程中,目标的祖先元素相应的监听事件也会执行.

这里有个优秀的图表可以看出完整的事件结构流程: https://www.w3.org/TR/DOM-Level-3-Events/#event-flow.

比如

<html>
    <body>
        <div class="parent">
            <div class="child">
                <p>I am the child</p>
            </div>
        </div>
        <script>
            var parent = document.querySelector(".parent");
            var child = document.querySelector(".child");
            parent.onclick = function() {alert("Parent");};
            child.onclick = function() {alert("Child");};
        </script>
    </body>
</html>

点击 "I am the child" 会触发两次 alert, 先是 Child, 后是 Parent.

现在使用事件代理( delegate )可以完美解决这些问题,同时取消事件冒泡.

document.onclick = function(event) {
    // about the interface of Event object
    // https://dom.spec.whatwg.org/#event
    var event = event || window.event;
    var target = event.target || event.srcElement;
    var is_event_set = target.onclick;
    if (target.tagName == 'DIV' && target.className == 'child') {
         target.onclick = function(evt) {
         /*
           可以根据元素地属性判断是否你想要地元素,如果符合条件就执行动作.
           比如,对class为outline的div元素添加事件,这就是在需要的时候添加事件,可以节省资源
         */
         /*
           如果要取消冒泡,则点击事件必须要传入event对象,然后设定 event 对象的 cancelBubble 为 true 或者调用 event.stopPropagation(),否则会导致事件发生两次;
           如果对象是类似于 a 这种有默认动作的标签,就要使用 event.preventDefault() 来阻止默认动作(除非你确实有这种奇怪的需求)
         */
             evt.preventDefault();
             evt.stopPropagation();
             console.log(target);
          }
         };
    }
    // 第一次点击是设定事件,要触发就要点击第二次,为了第一次就触发要做判断
    target.onclick && target.onclick(event);
};

注意,一旦使用了事件代理,同一个页面上没有被代理的事件注意要取消冒泡,否者会被执行两次.

还需要注意的一个东西就是 <iframe> 元素了,不能通过 <iframe> 监听到它引用的子页面的事件,正如上面说到,最多冒泡到 window 对象就停止了,

而子页面本身也是有一个 window 对象的,可以通过 HTMLIFrameElement.contentWindow 获取到.

解决事件执行次数过于频繁: 防抖(debounce),节流(throttle)和rAF(requestAnimationFrame)

写于 2018/10/20

写这一节是因为在无意中看到这篇参考资料.

防抖和节流可以用来控制函数在多长时间内可以执行一次,也就是控制函数的执行速率.

防抖的做法是,如果连续接受执行函数并且请求之间的间隔没有超过规定的时间,那么在最后一次执行请求停止后执行一次/第一次接受到执行请求的时候执行.

节流的做法是,如果连续执行函数,第一次执行函数时候记录当前时间,执行下一次的函数前先计算当前时间和上一次执行时间间隔有没有达到更到规定时间,达到才能执行,否则无视执行请求.

简单地想像一下,在100秒内执行90次函数,这100秒分为10段,在每一段前9秒中的每1秒执行一次函数(就像把100根排列好的棒子中的第10*n根抽掉).

在防抖中,设置间隔时间为1秒,那么就会执行9次函数,执行的函数处于第 n*10(1 < n < 10) 秒前一点点.

在节流中,设置间隔时间为10秒,那么就会执行9次函数,执行的函数处于第 n*10+1 秒中.

如果100秒执行100次,同样的时间设定,那么防抖就只是执行1次,节流会执行10次.(不清楚有没有算错数,懂我的意思就好).

接下来简单地实现一下如何对滚动事件进行防抖和节流.上篇提到的文章在评论区也有相应实现,同时我个人也参考这篇文章的实现.

Debounce 的实现思路

function debounce(func, wait, immediate) {
    var delayed;
    return function() {
        var context = this, args = arguments;
        var callNow = immediate && !delayed;
        delayed && clearTimeout(delayed);
        delayed = setTimeout(
            () => (delayed = null) || (!immediate && func.apply(context, args)),
            wait);
        callNow && func.apply(context, args);
    };
};

var scrollFn = debounce(function(){console.log(new Date());}, 500, true);

window.addEventListener('scroll', scrollFn);

immediate 决定是否在第一次请求就执行调用,否的话就只执行最后一次请求.

应用场景: 等用户不再进行输入的时候检测输入(这需要把 immediate 参数传入 false).

Throttle 的实现思路

function throttle(func, wait, interval) {
    var delayed,
        lastExcTime = new Date();
    return function() {
        var context = this, args = arguments, curTime = new Date();
        delayed && clearTimeout(delayed);
        if((new Date() - lastExcTime) >= interval){
            func.apply(context, args);
            lastExcTime = new Date();
        } else {
            delayed = setTimeout(func, wait);
        }
    };
};

function scrollFn(){
    console.log("Success");
}

var scrollFn = throttle(function() {console.log(new Date())}, 500, 1000);

window.addEventListener('scroll', scrollFn);

和防抖的"对所有执行请求进行去重"相比,节流就是"每多少个执行请求去重一次".

应用场景: 滚动加载图片.

利用 requestAnimationFrame 控制执行速率

function fnRAFedMaker(fn) {

    var ticking = false;

    var fnRAFed = function () {
        var context = this, args = arguments;
        var wrapper = function() {
            fn.apply(context, args);
            ticking = false;
        };
        ticking = ticking || requestAnimationFrame(wrapper);
    };

    return fnRAFed;
};

var scrollFn = fnRAFedMaker(function(){console.log(new Date());});

window.addEventListener('scroll', scrollFn, false);

关于 requestAnimationFrame 的执行速率可以看文档这里,大概就是一般时候是每秒60次,总的来说可浏览器的刷新率有关.

按照每秒60次来说,那么每执行一次就需要隔 (1000ms / 60) ms,大概相当于 throttle(fn, 500, 1000/16),当然 rAF 的精准度比后者高.

缺点就是性能不好,适合用于需要重绘的场景.

如何给定绝对定位元素居中

如何防止网站被别人iframe

这要针对不同情况来解决,解决的问题的角度不一样,分为前端和后端.

直接在HTML里面使用iframe标签

这种很好解决,我们只需要判断页面是否有一个 parent,当它的 parent 是自己就证明不存在 parent,那么就没有被别人 iframe 引用.

如果有,那么可以这样来让它强行跳转到我们的页面.详细内容请看这里.

window.top != window.self && window.top.location.replace("http://yoursite.com");

通过JavaScript写入iframe标签

对方可能会给 iframe 标签设置空 sandbox 属性来禁止 iframe 里面的脚本运行,这个时候上面的解决办法就失效了.关于 iframe 可以看这里.

这个时候可以通过设置后端的 XFO 头来禁止别的网站通过 iframe 盗用. HTTP headers 的参考可以参考这里.

比如 Nginx 可以在 server block 里面添加头,如下

server {

    add_header X-Frame-Options Deny;

    # the rest of the conf is up to you

}

当然要根据实际后端来自行配置.后端是彻底防止别人通过 iframe 标签来盗用你的网站,上面通过 JavaScript 判断的解决方法可以不用了.

响应式布局

什么是自适应布局(Adaptive Web Layout), 什么是响应式布局(Responsive Web Layout)

早期移动设备还没有流失的时候, 所有网站的页面都是只有一个尺寸的, 每个人的电脑屏幕大小不一样, 页面大小也不会适应电脑屏幕的大小.

随着移动设备的流行, 人们开始关注在小屏幕上的网页浏览体验.

早期开发者尽可能为不同类型的设备开发不同布局的页面(不同设备的尺寸是不一样的), 服务器通过浏览器的请求中的 User-Agent 头判断用户使用的是什么类型的设备返回对应页面.

早期如果观察仔细的话, 你会发现在不同设备上访问一些网站时会发现在不同设备上它们的域名是不同的, 比如百度在桌面浏览器访问时的域名是 www.baidu.com,

但在移动端浏览器上输入 www.baidu.com 会自动重定向到 m.baidu.com 上, 也有一些网站隐藏的很好的, 不会进行重定向, 而是服务器动态渲染, 返回不同页面.

这样虽然保证了不同设备的浏览体验, 但也增加了开发人员的工作量, 而且难以保证覆盖到所有尺寸可能性, 因为设备在不停的更新, 往往会出现一些以往不存在的尺寸的设备, 还不利于网站的 SEO.

随着 HTMLCSS 的发展, 开发者可以不需要根据 User-Agent 来判断设备的尺寸了, 媒体查询可以直接根据视口(viewport, 浏览器的可视内容区域)大小来动态设置样式.

媒体查询就像是编程语言里面的 if-then 条件语句, 以下是来自于 Fluid type with pixels 的例子,

.fluid-type {
  font-size: 14px;
}

@media screen and (min-width: 320px) {
  .fluid-type {
    font-size: calc(14px + 8 * ((100vw - 320px) / (1280 - 320)));
  }
}

@media screen and (min-width: 1280px) {
  .fluid-type {
    font-size: 22px;
  }
}

换成 JavaScript 就是类似于:

function getCurrentViewportWidth() {
  // 获取当前 viewport 的尺寸(px), 也就是 100vw 的大小.
  // 参考: https://developer.mozilla.org/en-US/docs/Web/CSS/Viewport_concepts
  return document.documentElement.clientWidth;
}

var fontSizeOfFluidType = 14;

if (getCurrentViewportWidth() >= 320) { // 相当于 @media screen and (min-width: 320px)
  fontSizeOfFluidType = 14 + 8 * ((getCurrentViewportWidth() - 320) / (1280 - 320));
}

if (getCurrentViewportWidth() >= 1280) { // 相当于 @media screen and (min-width: 1280px)
  fontSizeOfFluidType = 22;
}

以上的 CSS 代码考虑了 "小于等于319px/320px到1279px/大于等于1280px" 三种尺寸范围的情况, 人们称一个媒体查询为一个断点.

有了这个技术就可以让开发者不需要开发多个页面了, 直接通过 CSS 为不同大小的设备进行布局, 减少了工作量, 同时解决了自适应无法适配所有设备的问题, 哪怕是非典型尺寸设备也可以轻松覆盖,

提高了网站在不同设备上的体验一致性, 同时还解决了不利于 SEO 的问题.

总体上来说, 响应式布局是自适应布局的升级版布局方案, 也是目前主流的在跨端布局方案.

响应式布局这个概念也并不限定于 WEB 前端, 其它方向的前端开发上也有响应式布局的概念.

移动端优先和桌面端优先

如你所见, 上面的 .fluid-type 中的尺寸范围是从小到大的顺序编写的, 可以看出这套样式是优先为小尺寸的设备布局的, 这叫做移动端优先(mobile first).

如果反过来优先为大尺寸设备进行布局, 那就是桌面端优先(Desktop first). 桌面端优先是让媒体查询从大到小进行编码.

这里就以 .fluid-type 为例, 对其修改成桌面端优先.

以大到小的顺序思考范围: "大于等于1280px/1279到320px/小于等于320px",

.fluid-type {
  font-size: 22px;
}

@media screen and (max-width: 1280px) {
  .fluid-type {
    font-size: calc(22px - 8 * ((1280px - 100vw) / (1280 - 320)));
  }
}

@media screen and (max-width: 320) {
  .fluid-type {
    font-size: 14px;
  }
}

流式字体(Fluid Typography)

在定好媒体查询后, 就要根据自身实际情况来在媒体查询里进行布局编码.

通常来说, div 这类的盒子元素要实现随着 viewport 大小变化而变化是一件很容易的事情, CSS 本身就有有很多现成的解决方案,

比如百分比单位, vw 单位, Flexbox 布局, grid 布局等等.

但是字体要实现同样的效果可就没有盒子元素的要来得直接.

如果字体在盒子变化的过程中固定大小不变, 那么就会破坏盒子内容的布局.

这个时候需要名为流式字体的解决方法, 让字体在两个媒体查询断点之间随着 viewport 大小变化而变化, 而不是固定不变.

其实前面的 .fluid-type 就已经是流式字体了.

它的计算方法很简单, 以桌面优先版本的 .fluid-type 为例子.

第一个断点是 max-width: 1280px, 它的字体大小应该在 22px ~ 14px 这个范围内进行线性变化,

计算方法是: 字体起始值 + (字体起始值 - 字体结束值) * (abs(当前viewport大小 - 当前断点值) / abs(当前断点值 - 下一个断点值)).

其中 (abs(当前viewport大小 - 当前断点值) / abs(当前断点值 - 下一个断点值)) 表示了字体在区间范围内变化的百分比, (字体起始值 - 字体结束值) 表示了区间分为的最大值,

这两个相乘就可以得到字体变化的具体值.

这个计算方法适用于其他单位, 比如 remem; 也适用于字体大小以外的计算, 比如也可以计算字体行高(line-height), 也可以计算盒子 margin/padding/border 等等.

响应式布局所需要关注其它的方面

响应式布局是一个技术集合, 因此除了前面的媒体查询和流式字体以外, 还有很多技术, 比如响应式图片(Responsive Images), 弹性布局(Flexbox Layout), 网格布局(Grid Layout)等等.

这些东西在互联网上有很多资料. 专门针对于响应式布局编写的书也有不少, 为了知识的系统性, 个人推荐看书.

但这些书好多都没提及流式字体的内容, 所以我这里写的笔记基本上是为了自己针对这些书补充.

并且我相信流式字体的问题也同样困扰着很多刚学习响应式布局的前端开发者.

在中文互联网上, 关于响应式布局的文章好多都是不成体系的, 甚至有的连概念的解释都错, 根本无法支撑起对新手的教学作用.

有点想起之前网络上有人说到一句话: 中国互联网和国外互联网已经是两条路了.

互联网的发达也解决不了因为语言而产生的信息差, 学外语收益还是很大的.

以我个人经验, 这里推荐 Responsive Web Design with HTML5 and CSS Build Future Proof 这本书的最新版,

推荐它是因为在众多同类书籍中, 它是少数在不停出新版的存在, 毕竟前端标准也是在不停地发展的.

推荐它的另外一个理由是这本书非常偏向实践, 这是学习响应式布局的关键, 毕竟响应式布局也没有什么高大上的理论.

移动端适配

什么是移动端适配

首先要明白适配分两种,PC端和移动端.

由于历史原因,各个厂商的浏览器对 WEB 标准处理都不一样,最为著名的就是 IE 系列,可能是前端开发人员的恶梦.

它们的浏览器接口不一样,这也就是为什么 BOM 会没有标准了(希望有朝一日有一个标准),当然了页面操作类的API是一样的,也就是著名的 DOM, 没有 DOM 的话那就 DOOM 了.

幸运地是诞生出了 polyfill 这一个概念,翻译过来就是垫片, polyfill 可以减少开发人员兼容 JavaScript 的工作量,这只要求开发人员运行工具就可以了,比如 WebPackBabel.

除了 JavaScript 外, 不同浏览器的 CSS 的支持度不同和处理方式不一样也是问题,由于 CSS 并没有 polyfill 这一个说法,因此只能手动兼容.

幸运的是有支持查询 @supports 这一个 at-rule 可以判断该属性是否支持来减轻负担,不幸的是 at-rule 还只是处于实验阶段的功能,尽管如此使用属性时候查看兼容列表是必做的事情.

PC端只要做好上述两方面的兼容就可以了,对于那些 IE, Firefox, Chrome, Safari 等以外的浏览器,比如什么360浏览器,2345浏览器,要先调查出它们的内核(基本都是换了皮的旧版Chrome)再来查询文档.

单纯只是做移动端的话,上面的兼容可以不用怎么考虑,因为大多手机浏览器内容都是 webkit,但如果PC和移动端是同一套代码那就得做好上述的兼容,除此以外,移动端还要针对不同手机的屏幕大小做好兼容.

这就是适配移动端的主题,适配 JS,CSS 和在不同屏幕大小上的显示问题.在了解如何做适配工作之前要求先了解几个概念.

概念

  • Physical Pixels And Logical Pixels And Device Pixel Ratio
    • 像素(Pixels)

      人们普遍的认识就是电子图像/屏幕上一个点的常见的基本单位.像素在不同上下文中有不同的含意.

    • 物理像素(Physical Pixels)

      就是物理层面的屏幕的像素,屏幕是摸的到的设备,物理是就这个意思,物理像素就是屏幕上的点.简单点就是硬件方面的概念.

    • 逻辑像素(Logical Pixels)

      跟物理像素相对的话还可以叫做 digital pixels,之所以说是逻辑那是因为它摸不到,是一个抽象的概念,也就是电子图像的点.简单点就是软件方面的概念.

    • 设备像素比(Device Pixel Ratio)

      简写为 DPR/dpr.

      在有一个方向上,X轴或者Y轴上, Device Pixel Ratio : Physical Pixels / Logical Pixels.

      比方说,一张 (3 X 4)px 的图片 -- 逻辑像素,如果在 dpr = 2 的情况下,物理像素为 (6 X 8)px.

      也就是说 X * Y 的逻辑像素需要 dpr * X * dpr * Y 的物理像素来进行显示,这样有一个问题,那就是同一张图片在不同大小分辨率的屏幕上显示大小会不一样,分辨率大的屏幕显得图片小.

      (不知道各位有没有给过游戏设置窗口化以及不同分辨率,就是那种感觉,只不过这个例子变化的是逻辑像素).

  • Pixels/Dots Per Inch (PPI/DPI)

    像素密度, PPIAndroid 中的叫法,而 DPI 就是 IOS 的叫法.

    DPI/PPI 是物理像素,表示每英寸多少像素.业内用屏幕的对角线长度表示屏幕大小,同样大小的屏幕 DPI/PPI 越大就越清晰.

    屏幕对角线就是利用勾股定理计算出来的,同样在知道屏幕分辨率的情况下可以同样利用勾股定理计算出对角线占用的物理像素.

    那么 DPI/PPI 等于 "屏幕对角线占用的物理像素" 除以 "屏幕对角线".

    举个例子, 5.2 inch 1920 * 1080 的主屏, ppi/dpi = (sqrt(1920^2 + 1080^2)) / 5.2 = 294,然后我们就可以说这屏幕为 294 ppi 或者 294 dpi.

  • Density-independent Pixels (DP/DIP)

    
    早期 =DIP= 是 =Device/Density Independent Pixel=, =DP= 叫做 =Density Pixel=,后来直接用 =DP= 表示 =DIP=,
    
    所以说现在的 =DP= 和 =DIP= 是同一个东西,而现在的 =DP= 全称叫做 =Density-independent Pixels=.
    
    大概根据时间来排列上面的文档,其中 =id dp the same as dip= 和 =what is the difference between px dp dip and sp in Android= 两个回答更加说明了 =DP= 和 =DIP= 是同一个东西.
    
    

    密度无关像素,这是 Google 提出来用于适配 Android 屏幕的概念,正如上面说到的不同 dpi 的屏幕显示同一个像素大小的图片会有不同大小.

    这个单位就是解决这个问题的,在 160dpi 的屏幕中, 1dip = 1px = 1/160in;在 240dpi 的屏幕中, 1dip = 1.5px=, 所以 1dip 不等于 1px.

    具体计算公式为 px = dip * (dpi / 160).有了这条公式就可以知道图片用在不同屏幕上需要设置多少逻辑像素了,通过这样达到适配.

  • 视觉设计稿

    视觉设计稿有时候会被叫做设计稿或者视觉稿,设计稿有一种参数,比如 960px 的设计稿, 960px 指的是屏幕的宽度的物理像素为 960px.

具体的适配工作

适配内容主要针对三个方面: 元素大小,元素之间的间距和元素在页面中的位置,目的就是让页面在不同大小的屏幕上成比例显示,对于元素大小的问题来说,要求开发人员计算好在不同屏幕下元素的逻辑像素是多少,举个例子,现在有一个 Design-width px 的设计稿,设计稿上面有一个宽度为 Width-of-element-in-design pxlogo,要求在宽度为 Device-width px 的显示屏上进行显示,那么计算公式为,

Width-of-element-in-device = Width-of-element-in-design / Design-width * Device-width

这条公式对于元素的间距还有页面布局是同样适用,是适配的核心原理,具体来讲适配的内容有以下:

  • 图片大小
  • 文字大小
  • 图片和图片之间的距离
  • 图片和文字之间的距离
  • 文字和文字之间的距离
  • 元素的布局

主要难题在于同时考虑字体大小,图片清晰度和元素间距的问题,这就是为什么不能单纯通过百分比设计元素大小来做适配.对于布局,只要熟练掌握布局手段就没问题了.实现适配的手段有很多,经过一段时间的观察基本是离不开 CSS 相对单位的应用.上面的几个参考链接已经非常全面了,就不再赘述了.

实际开发中有两种方法,使用淘宝的 flexible.js 或者 viewport 单位做开发,这两者的原理其实都是一样的,前者实际上就是模拟 viewport 单位的工作方式,后者由于兼容问题而一直没有被采用,不过现在已经比较成熟了, flexible.js 的使命也差不多到头了.

https://github.com/amfe/lib-flexible 上的页面也建议采用 viewport 单位,并且该项目的源代码从 16 年起就没更新了,所以我觉得可以大胆使用 viewport 单位了.

REM方案

rem 是一个长度单位,不同于 px, rem 是一个相对单位, 1rem 就是相当于文档根节点的字体大小,也就是 <html> 元素的字体大小,比如 <html> 元素的 font-size16px,那么 1rem 就是 16px.

聪明的人们利用这个特性来使用 rem 做为元素长度单位,再接合 JS 动态调整 <html> 的字体大小来实现了自适应.这也就是所谓的 REM 方案.

假设现在要在 PC 端上做等比例缩放,拿到一个宽度为 1920px 的设计稿,也就是说这个设计稿是专门为 1920px 的屏幕(准确点是网页可视宽度,也叫做布局视口)产生的.使用 REM 方案要先都定好 1rem 为多少 px.

目前比较常用的方案是把设计稿划分为 100 等分,也就是说 1rem 等于网页可视宽度的 1%,那么 1rem 就要等于 (1920px / 100).

但是由于设计稿的宽度可能会很小,除以 100 得到的字体大小可能会很小,比如 Chrome 默认的最小字体大小是 12px.

所以需要稍微转变一下思路,划分 100 等分改为 10 等分,也就是 1rem 等于网页可视宽度的 10%,那么 1rem 就要等于 (1920px / 10), 10rem 也就是 100% 的网页可视的宽度.

1rem 定好后就可以是用 rem 作为元素长度单位进行布局了,现在还有一个问题,把 px 转换为 rem 又是如何计算?其实就是算出元素宽度占据设计稿宽度的比例再计算出这元素占了多少 rem,

这就是为什么划分为 100 等分的原因之一了,假设现在设计稿上面有一个 宽:高400px:300px 的元素,从宽度开始, (400px / 1920px * 10rem) 就是该元素用 rem 作为单位时的长度大小,同样高度也是这么计算.

除了元素的高度和宽度外,字体大小也可以这么做,不过因为浏览器都有自己的默认最小的字体大小,所以 rem 方案也不是银弹,字体大小要额外做处理,一般是结合媒体查询加上 px 单位来设置字体大小.

如果项目中允许使用 calc,那么你可以放心的把所有计算工作都交给它来处理,像这样,

.element {
    width: calc(400 / 1920 * 10rem);
    height: calc(300 / 1920 * 10rem);
}

如果还允许使用构建工具,那么我建议用构建工具来进行编译,毕竟 calc 有兼容的问题以及写起来不美观,具体我就不多写了.

最后还需要的就是动态改变 <html> 的字体大小了.这一步很简单,也很容易理解. 1rem 大小是根据设计稿的宽度的 1% 或者 10% 来使用的,

设计稿是按照屏幕大小来设计的,也就是说实际上 1rem 是按照网页可视的宽度来进行计算的,该宽度可以通过 document.documentElement.clientWidth 获得.

// 初始化根节点的font-size
var root = document.documentElement; // root就是<html>元素
root.style.fontSize = root.clientWidth / 10 + 'px';
// 监听window对象的resize事件来获取网页可视宽度,并且重新计算设置html的font-size
window.addEventListener('resize', function(event){
    root.style.fontSize = root.clientWidth / 10 + 'px';
});

到这里位置,最简单的自适应已经完成了.阅读到这里最好自己动手写一个简单的页面熟悉一下.

VW/VH方案

VW/VHviewport 单位, 1vw 就是占 viewport 的宽度 1%, 1vhviewport 高度的 1%. 1vw 和上面划分 100 等分时候的 1rem 是一样的!.

也就是说 100vw 等于上面的 10 等分的 10rem 以及 100 等分的 100rem,就是 rem 方案和本方案是可以互换的.这也是为什么 rem 方案中要以 100 等分来划分了,

原因是要兼容 vw 单位.使用该方案可以免去通过 JS 动态设定根接点的字体大小以及构建工具,除此以外和 rem 方案的用法以及计算方法都是一样的,

上面的例子中使用 VW 方案的话就是把上面的 100rem 或者 10rem 换成 100vw. vh 单位其实很少用上,因为设计稿一般不会指定屏幕高度的,所以元素的高度单位也是用 vw.

使用 vh 的情况一般就是规定要占满屏幕高度的多少才会使用.具体就不说了.

如何快速检测某个CSS属性的兼容性

写于 2019/3/1

MDN 的文档虽然关于这一块很齐全,但检索起来会比较麻烦,所以找到这个网站: https://caniuse.com/,值得注意的是该网站连一些国产浏览器的信息都有.

使用很简单,比如我要查自定义变量的支持程度,在搜索框输入 var 就可以看到关于 var 相关关键字的信息.

JavaScript里面的bind()方法

写于 2019/3/5

JavaScriptthis 我们都知道怎么用,但是你见过这么写的吗?

window.addEventListener('load', this.render.bind(this), false);

这里有一个关于 bind() 解释十分简单易懂,具体我就不多说了.

JavaScript的this关键字

如何判断一个元素是否可以滚动

写于 2019/3/19

根据 MDN 描述,当元素不能滚动的时候,它的 scrollTop 属性是0,并且设置任何值都是0,根据这个特点可以写出一个函数来判断元素是否可以滚动.

function isScrollable(element) {
    let origin_value = element.scrollTop;
    if (element.scrollTop > 0) return true;
    element.scrollTop = 1;
    if (element.scrollTop == 1) {
        element.scrollTop = origin_value;
        return true;
    }
    return false;
}

如何判断一个可滚动的元素是否滚动到底部

写于 2019/3/19

这要根据元素的三个属性来计算了,分别是 scrollTop, scrollHeightclientHeight.

  • scrollTop: 元素顶边到元素顶边之间的距离,换句话说就是元素已经滚动到多远了;
  • scrollHeight: 元素可以滚动的高度,也就是元素内容的高度(可见内容高度 + 未见内容高度,一些细节的处理和 clientHeight 一样,下面说);
  • clientHeight: 元素的高,计算公式为: CSS 高度 + Padding 高度 - 水平滚动条的高度(如果有的话).

这里不难发现如果元素滚动到底部的时候 (scrollHeight = scrollTop + clientHeight)=,那么可以开始编码了,

function isHittingBottom(element) {
    return element.scrollTop + element.clientHeight == element.scrollHeight;
}

Service Worker 的一些容易令人疑惑的地方

参照 MDN 上面的 PWA 教程的话,如果是按照完全按照上面的步骤来做是完全没有问题的,但是给我的 blog 添加 service worker 来实现首页离线功能,情况就不一样了,

所以发现了一个这个容易使人疑惑的地方,那就是 navigator.serviceWorker.register 的第二个参数以及缓存资源的时候如何正确写出路径.

我的 blog 的源代码以及编译后的页面是放在 GitHub 上的,大概结构是这样的,

.
├── index.html
│   ├── darksalt.conf
│   └── passwd
├── docker-compose.yml
├── Dockerfile
├── docs
│   │   ...
│   │   index.html
│   ├── └── main.js
│   ├── posts
└── src
|   ├── ...
|   ├── js
│   │   index.org
    ├── └── main.js
    └── posts
        └── index.org

因为 GitHub 的缘故,我的首页要重定向到 /docs/ 目录下的,因为 /docs/- 才是应该被访问的根目录.

main.js 是页面的唯一的 js 脚本(不包括第三方脚本),所以就用这个脚本来注册 service worker 了.

问题就处在这里了, 将要被注册 sw.js 文件要放在哪里呢,教程上面(貌似)没提到.其实很简单,最好放在根目录,

在这里就是 /docs/ (不是docs的父级目录),因为 sw.js 里面指定的缓存文件只能位于与 sw.js 一样的层级或者该层级之下,

不能超出 sw.js 的层级,所以如果把 /docs/sw.js 改为 /docs/js/sw.js,那么就不能加载与 js 同级目录 css 下的文件,

这就是 sw.js 的作用域, navigator.serviceWorker.register 的第二个参数里面的 scope 属性是指定 sw.js 的作用域,

这里没有设置,所以默认就是 /docs/.根据前面的说明可以了解到,设置的作用域是不能大于 sw.js 的,虽然教程上没说,但 registerAPI 说明还是有提到的.

剩下的就是 sw.js 指定位于该作用域下的文件来进行缓存了,可以是用相对以及绝对路径,比如 /docs/index.html, /docs/ 以及 index.html 都是一样的,

不能写 /src/js/main.js. 当然可以通过设置头来突破作用域的限制,但不推荐.

完成后 https://blog.darksalt.me/docs/ 的页面就可以在第一次浏览后离线浏览了.

各个厂商所支持的 Meta Tags

除了 W3C 标准的 meta tags 需要了解以外,还要了解每个厂商自己特有的 tags,在移动端开发以及兼容考虑中十分重要.

AppleSafari on IOS: https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html#//apple_ref/doc/uid/TP40008193-SW1.

Microsofthttps://docs.microsoft.com/en-us/openspecs/ie_standards/ms-iedoco/380e2488-f5eb-4457-a07a-0cb1b6e4b4b5

Author: saltb0rn (asche34@outlook.com)

Date: 2018-09-01

Emacs 29.3 (Org mode 9.6.15)

Validate