先占个坑,后期会把 JavaScript权威指南第七版和JavaScript高级程序设计第四版两本书读过一遍以后,写一下
可能会一章一章的仔细写,也可能只会写不明白的重点、难点,毕竟这两本书太厚了(笑哭)

浏览器渲染流程

image-20210122101059973

  1. 触发视觉变化(不仅仅是js控制还有可能是css)
  2. 浏览器对样式进行重新计算
  3. 计算出新的元素布局
  4. 浏览器进行重新绘制
  5. 渲染层的合成

浏览器初次加载会完整的走过整个流程,但是之后我们可以对这个流程进行优化,避免非必要的流程

布局(layouts)与绘制(paint)

详细的回流重绘见下一篇文章

  • 渲染树只包含网页需要的节点(不包括meta、display:none)
  • 布局计算每个节点接触的位置和大小
  • 绘制是像素化每个节点的过程

重新布局被称为reflow 回流,重新绘制被称为repaint 重绘

  1. 回流(reflow), 布局引擎为frame计算图形, 以确定对象位置, 浏览器根据各种样式来计算结果放在它该出现的位置.
    • YaHoo!性能小组总结了一些导致回流发生的一些因素:
      1. 调整窗口大小
      2. 改变字体
      3. 增加或者移除样式表
      4. 内容变化,比如用户在 input 框中输入文字, CSS3 动画等
      5. 激活 CSS 伪类,比如 :hover
      6. 操作class属性
      7. 脚本操作DOM
      8. 计算offsetWidthoffsetHeight属性
      9. 设置 style 属性的值
    • 当不可避免的出现回流时,应该避免 layout thrashing 布局抖动
      • 避免回流(减少次数)
      • 读写分离(可以借助fastdom)
  2. 重绘(repaint), 当计算好盒子模型的位置, 大小以及其他属性后, 浏览器就根据各自的特性进行绘制一遍, 显现出来给用户看
    • 重绘则是视觉效果变化引起的重新绘制。比如 color 或者 background 发生了变化,那就该给触发重绘的元素化化妆,化成它想要的样子。

回流与重绘两者之间的联系在于: 触发回流一定会触发重绘, 而触发重绘却不一定会触发回流

复合线程(compositor thread)与图层(layers)

详细介绍见 https://segmentfault.com/a/1190000014520786

淘宝团队的文章 https://fed.taobao.org/blog/taofed/do71ct/performance-composite/

提升到合成层的最好方法就是 用 transfrom + opacity 搭配 will-change

京东和淘宝的轮播容器都是采用opacity + transform 3d

1
2
3
opacity: 1;
transform: translate3d(0px, 0px, 0px);
transition: none 0s ease 0s;

优化相关的函数

一帧的声明周期

image-20210122121308752

requestAnimationFrame

防抖应用,按照实际帧数触发

1
2
3
4
5
6
7
8
9
10
11
12
13
let flag = false;
window.addEventListener("pointermove", () => {
if (flag) {
return;
}
flag = true;
window.requestAnimationFrame(() => {
/*
* 处理事件
*/
flag = false;
})
})

requestIdleCallback

mdn上的requestIdleCallback

案例: react 16 通过rAF 模拟 rIF 进行事件调度

下图是rAF 和 rIF 的不同

image-20210122142533699

简单记录工具的本地化,可以跳过

测试工具

  • Chrome DevTools 开发调试、性能评测
  • Lighthouse 网站整体质量评估
  • WebPackTest 多测试地点、全面性能报告
    • 地址
    • 主要是三个指标,waterfall 、first view、repeat view

lighthouse 使用

  • 可以使用npm 全局安装
1
2
3
4
npm install -g lighthouse

Lighthouse http://www.bilibili.com
会生成一个本地测试报告
  • 直接用chrome浏览器

WebPageTest本地部署说明

  1. 拉取镜像

    1
    2
    3
    docker pull webpagetest/server

    docker pull webpagetest/agent
  2. 运行实例

    1
    2
    3
    docker run -d -p 4000:80 --rm webpagetest/server

    docker run -d -p 4001:80 --network="host" -e "SERVER_URL=http://localhost:4000/work/" -e "LOCATION=Test" webpagetest/agent

mac 用户自定义镜像

  1. 创建server目录

    1
    2
    mkdir wpt-mac-server
    cd wpt-mac-server
  2. 创建Dockerfile,添加内容

    1
    2
    3
    4
    vim Dockerfile

    FROM webpagetest/server
    ADD locations.ini /var/www/html/settings/
  3. 创建locations.ini配置文件,添加内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    vim locations.ini

    [locations]
    1=Test_loc
    [Test_loc]
    1=Test
    label=Test Location
    group=Desktop
    [Test]
    browser=Chrome,Firefox
    label="Test Location"
    connectivity=LAN
  4. 创建自定义server镜像

    1
    docker build -t wpt-mac-server .
  5. 创建agent目录

    1
    2
    mkdir wpt-mac-agent
    cd wpt-mac-agent
  6. 创建Dockerfile,添加内容

    1
    2
    3
    4
    5
    vim Dockerfile

    FROM webpagetest/agent
    ADD script.sh /
    ENTRYPOINT /script.sh
  7. 创建script.sh, 添加内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    vim script.sh

    #!/bin/bash
    set -e
    if [ -z "$SERVER_URL" ]; then
    echo >&2 'SERVER_URL not set'
    exit 1
    fi
    if [ -z "$LOCATION" ]; then
    echo >&2 'LOCATION not set'
    exit 1
    fi
    EXTRA_ARGS=""
    if [ -n "$NAME" ]; then
    EXTRA_ARGS="$EXTRA_ARGS --name $NAME"
    fi
    python /wptagent/wptagent.py --server $SERVER_URL --location $LOCATION $EXTRA_ARGS --xvfb --dockerized -vvvvv --shaper none
  8. 修改script.sh权限

    1
    chmod u+x script.sh
  9. 创建自定义agent镜像

    1
    docker build -t wpt-mac-agent .
  10. 用新镜像运行实例 (注意先停掉之前运行的containers)

    1
    2
    3
    4
    docker ps 查看docker实例 
    docker stop 实例id 实例id 即可停止
    docker run -d -p 4000:80 --rm wpt-mac-server
    docker run -d -p 4001:80 --network="host" -e "SERVER_URL=http://localhost:4000/work/" -e "LOCATION=Test" wpt-mac-agent
  11. m1平台构建有问题 应该是需要 设置–platform 暂时不会

RAIL的概念

  1. Response 响应
  2. Animation 动画
  3. Idle 空闲
  4. Load 加载

RAIL评估标准

  1. 响应: 处理事件应在50ms以内完成
  2. 动画:每10ms产生一帧(1/60 = 16.66)剩余毫秒值 浏览器需要渲染
  3. 空闲: 尽可能增加空闲事件
  4. 加载:在5s内完成内容加载,并可以交互

chrome 浏览器调试工具的使用

network性能测试

image-20210118103539289

打开调试工具 -> network 注意打钩选项 然后按住刷新,选择清空缓存并硬性重新加载

image-20210118104323123

network底部有 transferred over network和 resources loaded by the page. 这两个大小有何区别?后边那个指的是解压后的大小,前边那个指的是源文件

下面重点研究瀑布图

image-20210118104749352

瀑布图由长短不一的条状图构成,处于同一竖线起点的请求,就是并行请求,并行请求数量浏览器有上限,其他的请求必须等上一个请求完成后,才可以发出,瀑布图上可以看到

​ 瀑布图上蓝色竖线表示dom加载完成的时间,红色表示所有资源加载完成的时间。

​ 把鼠标放在到瀑布图上,还可以展示具体的时间构成

image-20210118105340061

TTFB(Time To First Byte)

可以把结果保存至本地

image-20210118110952135

lighthouse

主要看两个属性

image-20210118110414575

帧数

command+shift+p 输入frame 选择展示帧数

转载于知乎

背景

说起鉴权大家应该都很熟悉,不过作为前端开发来讲,鉴权的流程大头都在后端小哥那边,本文的目的就是为了让大家了解一下常见的鉴权的方式和原理。

认知:HTTP 是一个无状态协议,所以客户端每次发出请求时,下一次请求无法得知上一次请求所包含的状态数据。

一、HTTP Auth Authentication

简介

HTTP 提供一个用于权限控制和认证的通用框架。最常用的HTTP认证方案是HTTP Basic Authentication

img

鉴权流程

img

加解密过程

1
2
3
4
5
6
7
8
9
10
// Authorization 加密过程
let email = "postmail@test.com"
let password = "12345678"
let auth = `${email}:${password}`
const buf = Buffer.from(auth, 'ascii');
console.info(buf.toString('base64')); // cG9zdG1haWxAdGVzdC5jb206MTIzNDU2Nzg=

// Authorization 解密过程
const buf = Buffer.from(authorization.split(' ')[1] || ''), 'base64');
const user = buf.toString('ascii').split(':');

其他 HTTP 认证

通用 HTTP 身份验证框架有多个验证方案使用。不同的验证方案会在安全强度上有所不同。

IANA 维护了一系列的验证方案,除此之外还有其他类型的验证方案由虚拟主机服务提供,例如 Amazon AWS ,常见的验证方案包括:

  • Basic (查看 RFC 7617, Base64 编码凭证. 详情请参阅下文.),
  • Bearer (查看 RFC 6750, bearer 令牌通过OAuth 2.0保护资源),
  • Digest (查看 RFC 7616, 只有 md5 散列 在Firefox中支持, 查看 bug 472823 用于SHA加密支持),
  • HOBA (查看 RFC 7486 (草案), HTTP Origin-Bound 认证, 基于数字签名),
  • Mutual (查看 draft-ietf-httpauth-mutual),
  • AWS4-HMAC-SHA256 (查看 AWS docs)

二、Cookie + Session

注册流程

img

思考:为什么要在密码里加点“盐”?

鉴权流程

img

Session 存储

最常用的 Session 存储方式是 KV 存储,如Redis,在分布式、API 支持、性能方面都是比较好的,除此之外还有 mysql、file 存储。

如果服务是分布式的,使用 file 存储,多个服务间存在同步 session 的问题;高并发情况下错误读写锁的控制。

Session Refresh

我们上面提到的流程中,缺少 Session 的刷新的环节,我们不能在用户登录之后经过一个 expires 时间就把用户踢出去,如果在 Session 有效期间用户一直在操作,这时候 expires 时间就应该刷新。

以 Koa 为例,刷新 Session 的机制也比较简单: 开发一个 middleware(默认情况下所有请求都会经过该 middleware),如果校验 Session 有效,就更新 Session 的 expires: 当前时间+过期时间。

img

优化:

  1. 频繁更新 session 会影响性能,可以在 session 快过期的时候再更新过期时间。
  2. 如果某个用户一直在操作,同一个 sessionID 可能会长期有效,如果相关 cookie 泄露,可能导致比较大的风险,可以在生成 sessionID 的同时生成一个 refreshID,在 sessionID 过期之后使用 refreshID 请求服务端生成新的 sessionID(这个方案需要前端判断 sessionID 失效,并携带 refreshID 发请求)。

单设备登录

有些情况下,只允许一个帐号在一个端下登录,如果换了一个端,需要把之前登录的端踢下线(默认情况下,同一个帐号可以在不同的端下同时登录的)。

这时候可以借助一个服务保存用户唯一标识和 sessionId 值的对应关系,如果同一个用户,但 sessionId 不一样,则不允许登录或者把之前的踢下线(删除旧 session )。

三、JWT

简介

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

JWT 组成

JWT 由三部分组成,分别是 header(头部),payload(载荷),signature(签证) 这三部分以小数点连接起来。

例如使用名为 jwt-token 的cookie来存储 JWT 例如:

1
jwt-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibHVzaGlqaWUiLCJpYXQiOjE1MzI1OTUyNTUsImV4cCI6MTUzMjU5NTI3MH0.WZ9_poToN9llFFUfkswcpTljRDjF4JfZcmqYS0JcKO8;

使用.分割值可以得到三部分组成元素,按照顺序分别为:

  • header

      • 值:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    • Base64 解码: {"alg": "HS256", "type": "JWT"}
  • payload

      • 值:eyJuYW1lIjoibHVzaGlqaWUiLCJpYXQiOjE1MzI1OTUyNTUsImV4cCI6MTUzMjU5NTI3MH0
    • Base64 解码:
1
2
3
4
5
{       
"name": "lushijie",
"iat": 1532595255, // 发布时间
"exp": 1532595270 // 过期时间
}
  • signature

      • 值:WZ9_poToN9llFFUfkswcpTljRDjF4JfZcmqYS0JcKO8
    • 解码:
1
2
3
const headerEncode = base64Encode(header);     
const payloadEncode = base64Encode(payload);
let signature = HMACSHA256(headerEncode + '.' + payloadEncode, '密钥');

特点

  • 防CSRF(主要是伪造请求,带上cookie)
  • 适合移动应用 app

鉴权流程

img

Token 校验

对于验证一个 JWT 是否有效也是比较简单的,服务端根据前面介绍的计算方法计算出 signature,和要校验的JWT中的 signature 部分进行对比就可以了,如果 signature 部分相等则是一个有效的 JWT。

Token Refresh

为了减少 JWT Token 泄露风险,一般有效期会设置的比较短。 这样就会存在 JWT Token 过期的情况,我们不可能让用户频繁去登录获取新的 JWT Token。

解决方案:

可以同时生成 JWT Token 与 Refresh Token,其中 Refresh Roken 的有效时间长于 JWT Token,这样当 JWT Token 过期之后,使用 Refresh Token 获取新的 JWT Token 与 Refresh Token,其中 Refresh Token 只能使用一次

四、OAuth

阮一峰通俗介绍OAuth

OAuth 2.0 的四种方式 - 阮一峰的网络日志www.ruanyifeng.com图标

下面是转载,防止链接失效

RFC 6749

OAuth 2.0 的标准是 RFC 6749 文件。该文件先解释了 OAuth 是什么。

OAuth 引入了一个授权层,用来分离两种不同的角色:客户端和资源所有者。……资源所有者同意以后,资源服务器可以向客户端颁发令牌。客户端通过令牌,去请求数据。

这段话的意思就是,OAuth 的核心就是向第三方应用颁发令牌。然后,RFC 6749 接着写道:

(由于互联网有多种场景,)本标准定义了获得令牌的四种授权方式(authorization grant )。

也就是说,OAuth 2.0 规定了四种获得令牌的流程。你可以选择最适合自己的那一种,向第三方应用颁发令牌。下面就是这四种授权方式。

  • 授权码(authorization-code)
  • 隐藏式(implicit)
  • 密码式(password):
  • 客户端凭证(client credentials)

注意,不管哪一种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。

第一种授权方式:授权码

授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。

这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。

第一步,A 网站提供一个链接,用户点击后就会跳转到 B 网站,授权用户数据给 A 网站使用。下面就是 A 网站跳转 B 网站的一个示意链接。

1
2
3
4
5
https://b.com/oauth/authorize?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL&
scope=read

上面 URL 中,response_type参数表示要求返回授权码(code),client_id参数让 B 知道是谁在请求,redirect_uri参数是 B 接受或拒绝请求后的跳转网址,scope参数表示要求的授权范围(这里是只读)。

img

第二步,用户跳转后,B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户表示同意,这时 B 网站就会跳回redirect_uri参数指定的网址。跳转时,会传回一个授权码,就像下面这样。``

1
https://a.com/callback?code=AUTHORIZATION_CODE

上面 URL 中,code参数就是授权码。

img

第三步,A 网站拿到授权码以后,就可以在后端,向 B 网站请求令牌。

1
2
3
4
5
6
https://b.com/oauth/token?
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=CALLBACK_URL

上面 URL 中,client_id参数和client_secret参数用来让 B 确认 A 的身份(client_secret参数是保密的,因此只能在后端发请求),grant_type参数的值是AUTHORIZATION_CODE,表示采用的授权方式是授权码,code参数是上一步拿到的授权码,redirect_uri参数是令牌颁发后的回调网址。

img

第四步,B 网站收到请求以后,就会颁发令牌。具体做法是向redirect_uri指定的网址,发送一段 JSON 数据。

1
2
3
4
5
6
7
8
9
{    
"access_token":"ACCESS_TOKEN",
"token_type":"bearer",
"expires_in":2592000,
"refresh_token":"REFRESH_TOKEN",
"scope":"read",
"uid":100101,
"info":{...}
}

上面 JSON 数据中,access_token字段就是令牌,A 网站在后端拿到了。

img

第二种方式:隐藏式

有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)”隐藏式”(implicit)。

第一步,A 网站提供一个链接,要求用户跳转到 B 网站,授权用户数据给 A 网站使用。

1
2
3
4
5
https://b.com/oauth/authorize?
response_type=token&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL&
scope=read

上面 URL 中,response_type参数为token,表示要求直接返回令牌。

第二步,用户跳转到 B 网站,登录后同意给予 A 网站授权。这时,B 网站就会跳回redirect_uri参数指定的跳转网址,并且把令牌作为 URL 参数,传给 A 网站。

1
https://a.com/callback#token=ACCESS_TOKEN

上面 URL 中,token参数就是令牌,A 网站因此直接在前端拿到令牌。

注意,令牌的位置是 URL 锚点(fragment),而不是查询字符串(querystring),这是因为 OAuth 2.0 允许跳转网址是 HTTP 协议,因此存在”中间人攻击”的风险,而浏览器跳转时,锚点不会发到服务器,就减少了泄漏令牌的风险。

img

这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。

第三种方式:密码式

如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为”密码式”(password)。

第一步,A 网站要求用户提供 B 网站的用户名和密码。拿到以后,A 就直接向 B 请求令牌。

1
2
3
4
5
https://oauth.b.com/token?
grant_type=password&
username=USERNAME&
password=PASSWORD&
client_id=CLIENT_ID

上面 URL 中,grant_type参数是授权方式,这里的password表示”密码式”,usernamepassword是 B 的用户名和密码。

第二步,B 网站验证身份通过后,直接给出令牌。注意,这时不需要跳转,而是把令牌放在 JSON 数据里面,作为 HTTP 回应,A 因此拿到令牌。

这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。

第四种方式:凭证式

最后一种方式是凭证式(client credentials),适用于没有前端的命令行应用,即在命令行下请求令牌。

第一步,A 应用在命令行向 B 发出请求。

1
2
3
4
https://oauth.b.com/token?
grant_type=client_credentials&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET

上面 URL 中,grant_type参数等于client_credentials表示采用凭证式,client_idclient_secret用来让 B 确认 A 的身份。

第二步,B 网站验证通过以后,直接返回令牌。

这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。

令牌的使用

A 网站拿到令牌以后,就可以向 B 网站的 API 请求数据了。

此时,每个发到 API 的请求,都必须带有令牌。具体做法是在请求的头信息,加上一个Authorization字段,令牌就放在这个字段里面。

1
2
curl -H "Authorization: Bearer ACCESS_TOKEN" \
"https://api.b.com"

上面命令中,ACCESS_TOKEN就是拿到的令牌。

更新令牌

令牌的有效期到了,如果让用户重新走一遍上面的流程,再申请一个新的令牌,很可能体验不好,而且也没有必要。OAuth 2.0 允许用户自动更新令牌。

具体方法是,B 网站颁发令牌的时候,一次性颁发两个令牌,一个用于获取数据,另一个用于获取新的令牌(refresh token 字段)。令牌到期前,用户使用 refresh token 发一个请求,去更新令牌。

1
2
3
4
5
https://b.com/oauth/token?
grant_type=refresh_token&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
refresh_token=REFRESH_TOKEN

上面 URL 中,grant_type参数为refresh_token表示要求更新令牌,client_id参数和client_secret参数用于确认身份,refresh_token参数就是用于更新令牌的令牌。

B 网站验证通过以后,就会颁发新的令牌。

写到这里,颁发令牌的四种方式就介绍完了。下一篇文章会编写一个真实的 Demo,演示如何通过 OAuth 2.0 向 GitHub 的 API 申请令牌,然后再用令牌获取数据。

https://link.zhihu.com/?target=https%3A//blog.csdn.net/maxchenBug/article/details/88791514)

五、总结对比

没有最好,只有最合适!!!

HTTP Auth Authentication

  • 梳理总结:

    • 通用 HTTP 身份验证框架有多个验证方案使用。不同的验证方案会在安全强度上有所不同。HTTP Auth Authentication 是最常用的 HTTP认证方案,为了减少泄露风险一般要求 HTTPS 协议。
  • 适用场景

    • 一般多被用在内部安全性要求不高的的系统上,如路由器网页管理接口
  • 问题:

    • 请求上携带验证信息,容易被嗅探到
    • 无法注销
  • 梳理总结:

    • 服务端存储 session ,客户端存储 cookie,其中 cookie 保存的为 sessionID
    • 可以灵活 revoke(撤销) 权限,更新信息后可以方便的同步 session 中相应内容
    • 分布式 session 一般使用 redis(或其他KV) 存储
  • 使用场景:

    • 适合传统系统独立鉴权

JWT

  • 梳理总结:

    • 服务器不再需要存储 session,服务器认证鉴权业务可以方便扩展
    • JWT 并不依赖 cookie(防范CSRF),也可以使用 header 传递
    • 为减少盗用(中间人),要使用 HTTPS 协议传输
  • 适用场景:

    • 适合做简单的 RESTful API 认证
    • 适合一次性验证,例如注册激活链接
  • 问题:

    • 使用过程中无法废弃某个 token,有效期内 token 一直有效
    • payload 信息更新时,已下发的 token 无法同步

OAuth

  • 梳理总结:

    • OAuth是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容。
    • GitHub OAuth 文档 Identifying and authorizing users for GitHub Apps
  • 适用场景:OAuth 分为下面四种模式

    • 简化模式,不安全,适用于纯静态页面应用
    • 授权码模式,功能最完整、流程最严密的授权模式,通常使用在公网的开放平台中
    • 密码模式,一般在内部系统中使用,调用者是以用户为单位。
    • 客户端模式,一般在内部系统之间的 API 调用。两个平台之间调用,以平台为单位。

转载于知乎

延伸阅读

单点登录注销_Faker_Wang的博客-CSDN博客blog.csdn.net

[《手机扫码登录内网怎么实现的?》blog.csdn.net](

层叠上下文

文档中的层叠上下文由满足以下任意一个条件的元素形成:

小总结

z-index相关:

  • z-index不为auto
    • position 为absolute relative fixed sticky(后两种z-index无限制)
    • flex grid容器的子元素
  • 新属性css3相关
    • transform 变换
    • filter 过滤器
  • 和性能方面有关系
    • will-change
    • contain

详细介绍

  • 文档根元素(<html>);
  • position 值为 absolute(绝对定位)或 relative(相对定位)且 z-index 值不为 auto 的元素;
  • position 值为 fixed(固定定位)或 sticky(粘滞定位)的元素(沾滞定位适配所有移动设备上的浏览器,但老的桌面浏览器不支持);
  • flex (flex) 容器的子元素,且 z-index 值不为 auto
  • grid (grid) 容器的子元素,且 z-index 值不为 auto
  • opacity 属性值小于 1 的元素(参见 the specification for opacity);
  • mix-blend-mode 属性值不为 normal 的元素;
  • 以下任意属性值不为none的元素:
  • isolation 属性值为 isolate 的元素;
  • will-change 值设定了任一属性而该属性在 non-initial 值时会创建层叠上下文的元素(参考这篇文章);
  • contain 属性值为 layoutpaint 或包含它们其中之一的合成值(比如 contain: strictcontain: content)的元素。在层叠上下文中,子元素同样也按照上面解释的规则进行层叠。 重要的是,其子级层叠上下文的 z-index值只在父级中才有意义。子级层叠上下文被自动视为父级层叠上下文的一个独立单元。

总结:

  • 层叠上下文可以包含在其他层叠上下文中,并且一起创建一个层叠上下文的层级。
  • 每个层叠上下文都完全独立于它的兄弟元素:当处理层叠时只考虑子元素。
  • 每个层叠上下文都是自包含的:当一个元素的内容发生层叠后,该元素将被作为整体在父级层叠上下文中按顺序进行层叠。

Tips: 层叠上下文顺序

更完整的7阶层叠顺序图

BFC

块格式化上下文(Block Formatting Context,BFC) 是Web页面的可视CSS渲染的一部分,是块盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区域。

下列方式会创建块格式化上下文

  • 根元素(<html>)
  • 浮动元素(元素的 float 不是 none
  • 绝对定位元素(元素的 positionabsolutefixed
  • 行内块元素(元素的 displayinline-block
  • 表格单元格(元素的 displaytable-cell,HTML表格单元格默认为该值)
  • 表格标题(元素的 displaytable-caption,HTML表格标题默认为该值)
  • 匿名表格单元格元素(元素的 displaytable、``table-rowtable-row-group、``table-header-group、``table-footer-group(分别是HTML table、row、tbody、thead、tfoot 的默认属性)或 inline-table
  • overflow 计算值(Computed)不为 visible 的块元素
  • display 值为 flow-root 的元素
  • contain 值为 layoutcontent或 paint 的元素
  • 弹性元素(displayflexinline-flex元素的直接子元素)
  • 网格元素(displaygridinline-grid 元素的直接子元素)
  • 多列容器(元素的 column-countcolumn-width (en-US) 不为 auto,包括 ``column-count1
  • column-spanall 的元素始终会创建一个新的BFC,即使该元素没有包裹在一个多列容器中(标准变更Chrome bug)。

块格式化上下文包含创建它的元素内部的所有内容.

块格式化上下文对浮动定位(参见 float)与清除浮动(参见 clear)都很重要。浮动定位和清除浮动时只会应用于同一个BFC内的元素。浮动不会影响其它BFC中元素的布局,而清除浮动只能清除同一BFC中在它前面的元素的浮动。外边距折叠(Margin collapsing)也只会发生在属于同一BFC的块级元素之间。

几个重要的应用:

  • BFC 可以包含浮动的元素(清除浮动,防止高度塌陷)

  • 同一个 BFC 下外边距会发生折叠

  • BFC 可以阻止元素被浮动元素覆盖(左侧固定,右侧自适应布局)

    img

可以参考知乎

相关概念

image-20210428205046181

redis特点

  • 高性能,可持久化
  • key-value结构,支持多种数据类型
  • 支持事务,数据的原子性

应用场景

  • 缓存(读写性能优异)
  • 计数&消息系统(高并发、发布/订阅阻塞队列功能)
  • 分布式回话session&分布式锁(秒杀)

redis vs mongo

  • 储存方式不一样:key-value vs document
  • 使用方式&可靠性不一样: MongoDB SQL & ACDI支持
  • 应用场景不一样:高性能缓存 vs 海量数据分析

使用docker 安装redis

Docker-compose配置

在linux服务器中,新建/home/redistest/docker-compose.yml,并书写以下内容

1
2
3
4
5
6
7
8
9
10
11
version: "3"
services:
redis-test:
image: "redis"
restart: always
container_name: "redis-test"
ports:
- 15001:6379
volumes:
- /home/redistest:/data // 持久化
command: ["redis-server","--requirepass","123456"] // 设置密码

然后在当前目录运行docker-compose up -d 会自动抓取redis 并运行在15001端口

然后运行linux端口放行firewall-cmd --zone=public --add-port=15001/tcp --permanent

使用redis-cli

redis命令

  • 进入容器交互终端 docker exec -it redis-test /bin/bash
  • 输入 redis-cli 回车
  • 输入 auth 123456 // 密码登陆
  • 输出OK表示成功

想从镜像终端中出来,输入exit

常用的redis命令
  • ping ,如果运行正确 会返回pong

  • quit 断开当前redis 服务

  • auth 123456 // 登陆

  • select number 切换自动数据库,默认有0 - 15

  • 设置数据

    • set key value 设置键值
    • get key 获取键值
    • incr 变量自增 例如 set index 0 ;incr inde ;// 1 计数器
    • decr 递减
    • keys 正则 (*所有)查看符合条件的键值
    • exists key […key] 看键值是否存在 1 存在 0 不存在
    • del key 删除键值 1成功 0失败
  • 设置过期时间

    • set key value ‘EX’ seconds
  • Hash

    • hset obj key value

    • 就像给一个对象设置键值对一样 
      hset brian name "brian"
      hset brian age 19
      brian = {
       name: "brian",
        age: 20
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9

      - hgetall obj 返回对象的key value对

      - ```
      hgetall brian
      name
      brian
      age
      19
    • Hmset 设置多对键值对

    • hmset brian name xxx age 19 email ueih@122
      
      1
      2
      3
      4
      5
      6
      7

      - Hmget 获取多个键值

      - ```
      hmget brian name age
      xxx
      19
  • list list操作

  • pub/sub 发布订阅

  • server命令

    • client list 可以查看redis-cli连接的哪些redis-client服务 通过idle空闲时间判断当前使用的
    • client kill host:port 断开
    • flushdb 清空当前数据库的数据
    • flushall 清空所有数据库
  • slowlog slow log是用来记录执行时间的日子系统

redis备份

save 命令备份(同步任务,会阻塞,使用bgsave) 会生成一个dump.rdb,quit停止服务

CONFIG get dir 找到数据存在位置,把dump.rdb放入,重启服务就可以了

Redis GUI

安装一个破解版的redis desktop manage,可以修改语言为中文,通过gui中的终端服务,可以很轻松的使用

Nodejs 集成 redis

首先安装redis npm install redis --save

redis配置示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import redis from "redis"
const { promisify } = require("util");
const options = {
host: "39.106.100.189", // 切记不需要写http之类的
port: 15001,
password: "123456",
detect_buffers: true, // 不转换二进制
retry_strategy: function(options) { // 重连错误处理
if (options.error && options.error.code === "ECONNREFUSED") {
// End reconnecting on a specific error and flush all commands with
// a individual error
return new Error("The server refused the connection");
}
if (options.total_retry_time > 1000 * 60 * 60) {
// End reconnecting after a specific timeout and flush all commands
// with a individual error
return new Error("Retry time exhausted");
}
if (options.attempt > 10) {
// End reconnecting with built in error
return undefined;
}
// reconnect after
return Math.min(options.attempt * 100, 3000);
}
}
// 创建一个redis实例
const client = redis.createClient(options)

// set方法
const setValue = (key, value) => {
return client.set(key, value)
}

// 创建promise
const getAsync = promisify(client.get).bind(client);

// get方法
const getValue = (key) => {
return getAsync(key)
}

export {
client,
setValue,
getValue
}

测试

1
2
3
4
5
setValue("ceshi", 122333)
getValue("ceshi").then(res => {
console.log(res);
})
// 122333 成功

get 、set方法生产模式再次封装一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/ set方法
const setValue = (key, value) => {
if (typeof value === "undefined" || value == null || value === "") {
// == null 包含null 和 undefined
return;
}
if (typeof value === "string") {
return client.set(key, value)
} else if (typeof value === "object") {
// 对象层级应该只有能一层,不能多级嵌套
Object.keys(value).forEach((keyC) => {
client.hset(key, keyC, value[keyC], redis.print)
})
}
}

// 创建promise
const getAsync = promisify(client.get).bind(client);
// get方法
const getValue = (key) => {
return getAsync(key)
}
// hash get方法
const getHValue = (key) => {
return promisify(client.hgetall).bind(client)(key)
}

mongoose中文网

mongoose 基础用法记录

基础用法

  • 首先在mongoDB里新建一个数据库,例如test1,借助Navicat工具

image-20210428170647238

  • 然后书写test.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
var mongoose = require('mongoose');
// 后面跟随的配置是安全配置,防止控制台输出警告
mongoose.connect('mongodb://39.106.100.189:10050/test1', {
useUnifiedTopology: true,
useNewUrlParser: true
});
// 如果有用户名 密码 可以使用
// mongoose.connect('mongodb://name:pwd@39.106.100.189:10050/test1', {
useUnifiedTopology: true,
useNewUrlParser: true
});
var db = mongoose.connection;
db.on('error', function () {
console.log('error');
});
db.once('open', function() {
console.log('success');
var kittySchema = mongoose.Schema({
name: String
});

kittySchema.methods.speak = function () {
var greeting = this.name
? "Meow name is " + this.name
: "I don't have a name";
console.log(greeting);
}

// 简历表
var Kitten = mongoose.model('Kitten', kittySchema);

var fluffy = new Kitten({ name: 'fluffy' });
fluffy.speak(); // "Meow name is fluffy"

fluffy.save(function (err, fluffy) {
if (err) return console.error(err);
fluffy.speak();
});
});
// 还有一种方式 更加简洁
var User = mongoose.model('User', { // 建立表
name: String,
age: Number
});
var fluffy = new User({ name: 'ming', age: 15 });

fluffy.save(function (err, fluffy) { // 保存一条数据
if (err) return console.error(err);
});
  • 新增了一条fluffy数据,数据库显示如下

image-20210428170922882

项目中按功能拆分

一般会把mongoose初始化,Schema,和操作表分离

image-20210428181713717

mongoose初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
DBHelper.js

import mongoose from 'mongoose';
import config from './index';

// 创建链接
mongoose.connect(config.DB_URL, {
useUnifiedTopology: true,
useNewUrlParser: true
})

// 连接成功
mongoose.connection.on("connected", () => {
console.log('success' + config.DB_URL);
})
// 连接异常
mongoose.connection.on("error", (error) => {
console.log('error');
})

// 断开连接
mongoose.connection.on("disconnected", () => {
console.log('断开连接');
})

export default mongoose

Schema文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
model/User.js // 对应数据库 users表

import mongoose from '../config/DBHelpler';
const Schema = mongoose.Schema;

const UserSchema = new Schema({
name: {
type: String
},
age: {
type: Number
},
email: {
type: String
}
})

const UserModel = mongoose.model('User', UserSchema)

export default UserModel

controller文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 模拟
// 增删改查基础操作
import User from './User';

// 增
const user = {
name: "xiaoming",
age: 30,
email: "46546746"
}

const run = async () => {
const data = new User(user)
const result = await data.save() // promise
console.log(result);
}
run();
// 查
const search = async () => {
const result = await User.find() // promise
console.log(result);
}
search();
// 改
const update = async () => {
// User.updateMany
const result = await User.updateOne({
// filter
name: "xiaoming"
}, {
email: "safasfas"
})
console.log(result);
}
update();
// 删
const delete_ = async () => {
// User.deleteMany
const result = await User.deleteOne({
// filter
name: "ming"
})
console.log(result);
}
delete_();

静态方法

添加 Model 的静态方法也十分简单,继续用 animalSchema 举例:

1
2
3
4
5
6
7
8
9
// assign a function to the "statics" object of our animalSchema
animalSchema.statics.findByName = function(name, cb) {
return this.find({ name: new RegExp(name, 'i') }, cb);
};

var Animal = mongoose.model('Animal', animalSchema);
Animal.findByName('fido', function(err, animals) {
console.log(animals);
});

同样不要在静态方法中使用 ES6 箭头函数

五大设计原则

js常用前两种原则,语言特性和后三种原则关联不大。

  • 单一职责原则
    • 一个程序只做好一件事
    • 如果功能复杂就拆分开,每个部分保持独立
  • 开放封闭原则
    • 对扩展开放,对修改封闭
    • 增加需求时,扩展新代码,而非修改已有代码
    • 这是软件设计的终极目标
  • 李氏置换原则
  • 接口独立原则
  • 依赖导致原则

设计模式的基本-面向对象

为何使用面向对象

  • 程序执行:顺序、判断、循环 —- 结构化
  • 面对对象 —- 数据结构化
  • 对于计算机,结构化的才是最简单的
  • 变成应该 简单&抽象

js常用设计模式简介及举例

工厂模式

实际例子

jQuery

1
2
3
4
5
6
7
8
class jQuery {
construct(selector){
.....
}
}
window.$ = function(selector){
return new jQuery(selector)
}

React.createElement

1
2
3
4
5
6
class Vnode(tag, attrs, children) {
....
}
React.createElement = function(tag, attrs, children) {
return new Vnode(tag, attrs, children)
}

单例模式

js利用闭包和立即执行函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SingleObj{
login() {
console.log('111');
}
}
SingleObj.getInstance = (function () {
let instance = null;
return function () {
if (!instance) {
instance = new SingleObj();
}
return instance;
}
})()

const test1 = SingleObj.getInstance();
const test2 = SingleObj.getInstance();

console.log(test1 === test2); // true

// 无法防止错误用法,例如:
const test3 = new SingleObj();

适配器模式

应用场景

  • 旧接口格式和使用者不兼容
  • 中间加一个适配转换接口

实际例子

封装旧接口

解决新旧代码不兼容的问题,尽量避免全局替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 自己封装的ajax,使用方式如下
ajax({
url:'/getData',
type:'post',
data: {
id: 2
}
}).done(function(){})
// 但是由于历史原因,代码全是 $.ajax({...})

// 解决方案,做一层适配器
var $ = {
ajax: function(option) {
return ajax(option);
}
}

vue computed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="example">
<p>Original message: "{{ message }}"</p>
<p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
----------------------------------
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// 计算属性的 getter
reversedMessage: function () {
// `this` 指向 vm 实例
return this.message.split('').reverse().join('')
}
}
})

装饰器模式

应用场景

  • 为对象添加新功能
  • 不改变其原有的结构和功能

实现方式

es7装饰器提案

1
2
3
4
5
6
7
8
9
10
@testable
class MyTestableClass {
// ...
}

function testable(target) {
target.isTestable = true;
}

MyTestableClass.isTestable // true

上面代码中,@testable就是一个装饰器。它修改了MyTestableClass这个类的行为,为它加上了静态属性isTestabletestable函数的参数targetMyTestableClass类本身。

基本上,装饰器的行为就是下面这样。

1
2
3
4
5
6
7
@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A;

也就是说,装饰器是一个对类进行处理的函数。装饰器函数的第一个参数,就是所要装饰的目标类。

装饰器实现 mixin 功能演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list)
}
}
const Foo = {
foo() {
console.log('foo')
}
}
@mixins(Foo)
class MyClass {}

const obj = new MyClass();
obj.foo() // 'foo'

core-decorators

第三方类库,提供了一些常用装饰器
npm地址
只读

1
2
3
4
5
6
7
8
9
10
import { readonly } from 'core-decorators';

class Meal {
@readonly
entree = 'steak';
}

var dinner = new Meal();
dinner.entree = 'salmon';
// Cannot assign to read only property 'entree' of [object Object]

即将废弃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { deprecate } from 'core-decorators';

class Person {
@deprecate
facepalm() {}

@deprecate('We stopped facepalming')
facepalmHard() {}

@deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
facepalmHarder() {}
}

let person = new Person();

person.facepalm();
// DEPRECATION Person#facepalm: This function will be removed in future versions.

person.facepalmHard();
// DEPRECATION Person#facepalmHard: We stopped facepalming

person.facepalmHarder();
// DEPRECATION Person#facepalmHarder: We stopped facepalming
//
// See http://knowyourmeme.com/memes/facepalm for more details.
//

常用使用场景

类的装饰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Circle{
constructor(){

}
draw(){
console.log('画一个圆形')
}
}
// 装饰器
class Decorator() {
constructor(circle) {
this.circle = circle
}
draw() {
this.circle.draw()
this.setRedBorder(this.circle)
}
setRedBorder(circle){
console.log('设置红色边框')
}
}

方法的装饰

例子1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function readonly(target, name, descriptor) {
// 属性描述符
descriptor.writable = false;
return descriptor;
}

class Person {
constructor() {
this.first = 'A'
this.last = 'B'
}

@readonly
name() {
return `${this.first} ${this.last}`
}
}

p.name = () => { // 报错
console.log(1111)
}

例子2 函数改造,重点在于 call 和 this 的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function log(target, name, descriptor){
const oldFnc = descriptor.value;
descriptor.value = function(){
console.log(target, name, descriptor);
// return 是为了确保不丢失函数返回值
return oldFnc.call(this, arguments);
}
}

class Math {
@log
add(a, b) {
return a + b
}
}

代理模式

代理类和目标类分离,隔离开目标类和使用者
常用场景

  • 网页事件代理
  • jQuery $.proxy
  • es6 proxy
  • set get
    代码示意
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 本体
class RealImg{
constructor(fileName) {
this.fileName = fileName;
this.loadFromDisk() // 初始化从硬盘加载文件
}
diaplay() {
console.log('...diaplay' + this.fileName);
}
loadFromDisk() {
console.log('...loading' + this.fileName);
}
}
// 代理
class ProxyImg{
constructor(fileName) {
this.realImg = new RealImg(fileName);
}
display() {
this.realImg.diaplay();
}
}
// 演示
const img1 = new ProxyImg('1.png');
img1.display();

外观模式

使用场景

  • 为子系统中的一组接口提供了一个高层接口
  • 使用者使用这个高层接口

外观模式

观察者模式

前端最常用、最重要的模式
代码示意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Subject {
constructor() {
this.state = 0;
this.observers = [];
}
getState() {
return this.state;
}
setState(state) {
this.state = state;
this.notifyAllObservers();
}
notifyAllObservers() {
this.observers.forEach(observer => {
observer.update();
console.log(`${observer.name} -- updata --- ${this.getState()}`);
})
}
attach(observer) {
this.observers.push(observer);
}
}
// 观察者
class Observer {
constructor(name, subject) {
this.name = name;
this.subject = subject;
this.subject.attach(this);
}
update() {
console.log('更新');
}
}

const subject = new Subject();
const ob1 = new Observer('ob1', subject);
const ob2 = new Observer('ob2', subject);
const ob3 = new Observer('ob3', subject);
subject.setState(1);
subject.setState(2);
subject.setState(3);

应用场景

  • 网页事件绑定
  • promise
  • jQuery callback
  • nodejs 自定义事件

迭代器模式

使用场景

  • 顺序访问一个集合
  • 使用者无需知道集合的内部结构
    代码演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Interator{
index = 0
constructor(container) {
this.list = container.list
}
next() {
if (this.hasNext()) {
// 先返回,后加加
return this.list[this.index++]
}
}
hasNext() {
if (this.index >= this.list.length) {
return false
}
return true
}
}
class Container{
constructor(list) {
this.list = list;
}
getInterator() {
return new Interator(this);
}
}

实例

  • iterator & for of

状态模式

使用场景

  • 一个对象有状态变化
  • 每个状态变化都会触发一个逻辑
  • 不能中使用if-else来控制

实际应用

  • 状态机 例如html标签解析
  • promise 状态

其他模式

不常用,暂时略过

0%