Code Monkey home page Code Monkey logo

blog's Introduction

Hi 老铁 👋

这是我的博客

曾经苍老,现在风华正茂

🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙 🤙

blog's People

Contributors

strongcode9527 avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar

blog's Issues

消息队列,宏任务与微任务

背景

每一个页面都有一个渲染主线程,会处理很多任务,比如:DOM、样式计算、布局等等。如此多的任务就需要一个消息队列来进行管理。这些任务的类型,就是我们通常所说的宏任务以及微任务。

两种任务的关系,以及他们和UI线程渲染的关系

其实宏任务以及微任务的概念在前端已经是很普及的了,相关文章链接。但是有以下几个问题一直困扰着我:

  • 宏任务以及微任务的关系
  • 这两种任务和GUI渲染线程的关系

所以研究总结如下关系:

截屏2021-03-17 上午8.49.58.png
宏任务和他所产生的微任务是绑定的,一个宏任务执行完成后,这个宏任务所产生的微任务,以及微任务产生的微任务全部执行完后。才会执行下一个宏任务。如果这些任务耗时不长,那么一帧16ms内,可以执行多个宏任务。

截屏2021-03-17 上午8.49.01.png

如果一个宏任务以及宏任务产生的微任务耗时过长,超过16ms,那么就会造成UI线程渲染阻塞。其中如果一个宏任务耗时过长,也会等待其所产生的微任务执行完成后再进行UI线程渲染页面

为什么要分两种任务?

每一个任务的执行当中,有可能会产生新的任务,那么这些新的任务有两种插入消息队列的方式:

image (1).png

这也主要是宏任务和微任务的区别,在任务执行过程中:

  • 产生的宏任务直接插入消息队列尾部依次执行。
  • 产生的微任务直接插入当前任务的微任务队列中,在此任务执行完成后,直接执行此任务的微任务队列。

可以看出微任务的存在主要是保证任务执行的时效性,而宏任务就是正常的直接插入消息队列尾部。

总结

  • 宏任务和微任务是绑定关系,宏任务执行完成后会执行它所产生的微任务。
  • 一帧内 (大多是16ms) ,可执行多个宏任务以及他们的微任务。
  • 如果宏任务以及微任务执行时间过长,那么会阻塞UI线程的工作,导致页面卡顿、掉帧。
  • 宏任务产生的微任务直接插入当前宏任务的微任务队列,这么做是为了保证任务执行的时效性。
  • 宏任务执行过程中产生的宏任务,会直接放入消息队列的尾部,依次执行,新任务的执行时效性也就无法保证。

vite-esbuild

这篇文章对 esbuild 在 vite 中的应用做一个总结

vite 中 esbuild 的应用

esbuild 是 vite 性能快的关键。esbuild 在 vite 中主要被使用在以下场景:

  1. 通过入口分析依赖树,收集 node_modules 中的依赖,提前打包处理,并在磁盘持久化缓存处理过后的文件。
  2. 对部分业务文件 使用 esbuild 处理转化,比如 ts、jsx、tsx。注意 js 类型文件并不会被处理

node_modules 处理

需要处理 node_modules 的原因:

  1. 第三方库代码不会经常变动,缓存处理,提高响应速度。
  2. 打包第三方库,使 lodash 这种文件较多的库,减少网络请求。
  3. 代码兼容处理:比如有的依赖代码模块格式并不是 esm 而是 commonjs,利用 esbuild 进行模块格式转换。

难点一:vite 如何从 index.html 入口分析依赖的组成?他怎么知道需要处理哪些包?

  1. esbuild 天生支持 index.html 作为入口打包
require('esbuild').buildSync({
  entryPoints: ['index.html'], // is ok
  bundle: true,
  write: true,
  outdir: 'out',
})


if (resolved.includes('node_modules') || include?.includes(id)) {
    if (OPTIMIZABLE_ENTRY_RE.test(resolved)) {
        // esbuild 插件中直接判断引入的包是否是 node_modules 中的依赖
        depImports[id] = resolved
    }
}

难点二:esbuild 如何处理 vue 等非 javascript 文件,并无官方 vue-loader

const regex = isHtml ? scriptModuleRE : scriptRE

const scriptModuleRE =
    /(<script\b[^>]*type\s*=\s*(?:"module"|'module')[^>]*>)(.*?)<\/script>/gims
export const scriptRE = /(<script\b(?:\s[^>]*>|>))(.*?)<\/script>/gims

vite 内部通过编写一个简单的 esbuild 插件,直接利用正则表达式将 script 标签内部内容截取出来,作为 vue 文件的内容。这样 esbuild 就可以将 vue 文件当成 js 文件处理依赖树了。

编译业务文件

当用户第一次请求业务文件的时候,浏览器以及 vite 应用内部均没有缓存,这个时候就要倚靠 esbuild 对部分业务文件进行编译。

esbuild 默认会编译 ts、tsx、jsx 文件。

其中 js 文件默认不会被编译。所以当选择一些新语法开发时,要慎重

语法兼容问题

浏览器兼容

就像刚才说的问题,普通的 js 文件以及 vue 文件中的 js script 内容,是不会被 esbuild 处理编译的,所以不稳定的新语法是不能在 vite 环境下使用的。

esbuild 兼容

esbuild 作为新起的打包工具,他对部分语法是不支持的,比如 js 文件中使用装饰器。这个场景 esbuild 就是不兼容的。

所以语法兼容问题要考虑两个问题:

  1. 你开发时用的浏览器最好是比较新的 chrome 或者火狐浏览器,因为 vite 并不会默认编译 js 文件,如果有兼容问题,处理起来比较复杂。
  2. 代码兼容也要考虑 esbuild 的语法支持程度。如果有 esbuild 不支持的语法,在 vite 提前打包 node_modules 文件时会直接报错。(vite 会在启动服务的时候,默认从入口查找所有需要打包的 node_modules 文件,如果你的业务代码有兼容问题,会导致 esbuild 分析文件直接报错。)

作用域以及作用域链

作用域的简单定义

作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。

词法作用域和动态作用域

词法作用域又称静态作用域(lexical scoping),与之对应的是静态作用域,这是编程语言常见的两种作用域,javascript采用的是静态作用域,也就是词法作用域。

静态作用域的概念对于理解javascript的作用域以及闭包等概念非常重要。下面就用两个例子说明静态作用域以及动态作用域的区别。

静态作用域:

const result = 1;

function sub() {
  console.log(result);
}

function func() {
	const result = 2;
  sub();
}

func();

// 输出结果 1

动态作用域

#!/bin/bash

result=1
function sub () {
    echo $result;
}
function func () {
  local $result=2;
  foo;
}
bar
// 输出结果 2

其实这就是静态作用域与动态作用域的区别:

  • 静态作用域是由代码中的函数声明的位置来决定的。

作用域链

听到链这个词,估计很多前端同学都很熟悉,因为javascript不仅有作用域链还有原型链等等,作用域链其实比较简单,下面我们就用几个图,来说明作用域链的知识点。

截屏2021-05-11 上午8.35.21.png

上方是简单的作用域链的示意图,可以看到变量的访问是根据函数定义时就确定的。

闭包函数

function out() {
    const out = 3;
    return function inner() {
        debugger;
        const inner = 4;
        return out + inner;
    }
}


截屏2021-05-10 下午8.11.27.png

我们可以看到闭包函数作用域链是 Local => Closure => Global

https的理解

学习笔记来自这两篇博客

图解 HTTPS:Charles 捕获 HTTPS 的原理

聊聊对称/非对称加密在HTTPS中的应用

对称加密算法

发送方和接收方需要持有同一把密钥,发送消息和接收消息均使用该密钥。

相对于非对称加密,对称加密具有更高的加解密速度,但双方都需要事先知道密钥,密钥在传输过程中可能会被窃取,因此安全性没有非对称加密高。

非对称加密算法

接收方在发送消息前需要事先生成公钥和私钥,然后将公钥发送给发送方。发送放收到公钥后,将待发送数据用公钥加密,发送给接收方。接收到收到数据后,用私钥解密。

在这个过程中,公钥负责加密,私钥负责解密,数据在传输过程中即使被截获,攻击者由于没有私钥,因此也无法破解。

非对称加密算法的加解密速度低于对称加密算法,但是安全性更高。

https请求的过程

image

1.客户端发起HTTPS请求

这个没什么好说的,就是用户在浏览器里输入一个https网址,然后连接到server的443端口。

2.服务端的配置

采用HTTPS协议的服务器必须要有一套数字证书,可以自己制作,也可以向组织申请。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面(startssl就是个不错的选择,有1年的免费服务)。这套证书其实就是一对公钥和私钥。如果对公钥和私钥不太理解,可以想象成一把钥匙和一个锁头,只是全世界只有你一个人有这把钥匙,你可以把锁头给别人,别人可以用这个锁把重要的东西锁起来,然后发给你,因为只有你一个人有这把钥匙,所以只有你才能看到被这把锁锁起来的东西。

3.传送证书

这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等等。

4.客户端解析证书

这部分工作是有客户端的TLS来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等等,如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随即值。然后用证书对该随机值进行加密。就好像上面说的,把随机值用锁头锁起来,这样除非有钥匙,不然看不到被锁住的内容。

5.传送加密信息

这部分传送的是用证书加密后的随机值,目的就是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。

6.服务段解密信息

服务端用私钥解密后,得到了客户端传过来的随机值(私钥),然后把内容通过该值进行对称加密。所谓对称加密就是,将信息和私钥通过某种算法混合在一起,这样除非知道私钥,不然无法获取内容,而正好客户端和服务端都知道这个私钥,所以只要加密算法够彪悍,私钥够复杂,数据就够安全。

7.传输加密后的信息

这部分信息是服务段用私钥加密后的信息,可以在客户端被还原

8.客户端解密信息

客户端用之前生成的私钥解密服务段传过来的信息,于是获取了解密后的内容。整个过程第三方即使监听到了数据,也束手无策。

对称加密和非对称加密同时使用的原因

非对称加密算法的性能是非常低的,原因在于寻找大素数、大数计算、数据分割需要耗费很多的CPU周期,所以一般的HTTPS连接只在第一次握手时使用非对称加密,通过握手交换对称加密密钥,在之后的通信走对称加密。

tcp抓包学习

tcp建立连接(三次握手)

tcp连接

当你的页面仅支持http1.x的时候,浏览器进程可能会和服务器建立多个tcp连接。用不同的端口号区分tcp连接。

TCP报头中的源端口号和目的端口号同IP数据报中的源IP与目的IP唯一确定一条TCP连接。

tcp断开连接

tcp断开连接,服务器主动断开

杀死浏览器进程,浏览器主动断开

断开连接分两种情况:

  • 一种情况是正常浏览网页,提出结束请求的是服务器,这种情况下一般都是进行正常的keep-alive,然后服务器提起正常的请求断开连接,四次挥手。
  • 另一种情况就是用户主动杀死浏览器进程,可能就只有三次挥手了。服务器被动断开连接的时候,并且没有可传输的数据的时候,可能会将Server对Client的FIN报文的确认和Server的FIN一起发,这可能是三次挥手的原因

https建立连接

https进行连接

首先还是正常的tcp进行连接。

然后进行的就是tls的握手:

  • client hello 这一步主要是向服务器发送客户端支持的http版本,包括http1.x 或者http2等,还有就是浏览器支持的加密的各种信息供服务端选择。
  • Server Hello 选择client hello支持的各种协议,当然只是选择一个。
  • certificate 服务器向浏览器发送证书链。
  • Sever Hello Done 服务器向浏览器发送完毕,等待浏览器发送。
  • Client Key Exchange 向服务器发送加密的相关方式

上面只是对各种包的一个大概的说明。

参考

http2

http/2 学习

自己对于http/2 的理解还停留在面试死记硬背的水平,所以最近对自己心中一些疑问以及自己的理解做一个总结,这些总结可能会有错误。如果有大佬觉得哪里有什么问题,可以直接说出。

http/2 的完整介绍很多。珠玉在前:http/2 简介 ,就不在赘述了。

问题一: http/2 为什么要使用二进制分帧层

二进制协议的理解:

对于这个问题,我们首先就要搞清楚什么http1.1的文本协议和htt/p2的二进制协议的不同了。其实我作为一个基础并不牢固的搬砖工,当看到这个二进制的时候我就在想:在计算机世界当中所有的内容不都是二进制吗?为什么还要有二进制以及文本协议之分呢?

带着这个问题,我在网上找到了一些回复,就直接搬下来吧

Binary protocol versus text protocol isn't really about how binary blobs are encoded. The difference is really whether the protocol is oriented around data structures or around text strings. Let me give an example: HTTP. HTTP is a text protocol, even though when it sends a jpeg image, it just sends the raw bytes, not a text encoding of them.

四级低空略过水平的翻译:

二进制协议和文本协议的区别并不是关于二进制blob是如何编码的。关键在于协议是以数据结构为导向还是以文本字符串为导向。

文本协议请求:其中所有的内容都是文字

二进制请求:

struct request {
  int requestType;
  int protocolVersion;
  char path[1024];
  char user_agent[1024];
  char host[1024];
  long int accept_bitmask;
  long int language_bitmask;
  long int charset_bitmask;
};

当我们看到http文本协议以及二进制请求的结构之后:

  • 二进制协议比文本协议更加紧凑。
  • 文本协议解析是需要特殊字符的,比如\r\n,一方面造成了文本协议更加冗长,另一方面使用这种特殊字符解析一段文字的方式,没有二进制解析方便快捷。
  • 二进制协议中的数字比文本中的数字占用空间更小:比如http中状态码200,在文本协议中是 "2" "0" "0",这三个数字要分别用编码表示的,但是在二进制协议中,不用将数字编码成文本格式,直接用二进制表示数字就可以了: 11001000 。

http/2中的二进制分帧层

参考文章

At the core of all performance enhancements of HTTP/2 is the new binary framing layer, which dictates how the HTTP messages are encapsulated and transferred between the client and server.

_
在许多文章中,都说http/2中的二进制分帧层,是http/2性能提升的关键,它给一个tcp链接同时发送多个请求提供了可能。

http.png

HTTP/1.1和HTTP/2都是基于TCP的协议,TCP模型是双向数据流,任何在一个TCP连接上处理超过一个请求的协议都需要解决这样两个问题:

  1. 分片——如何将流中的多个请求和响应拆分成独立的消息
  2. 对应——如何将请求和响应对应起来

在这两个问题中,我觉得第二个往往会让人忽略,就是在http1.1 中,一个tcp请求只能同时完成一个请求响应过程,所以请求以及响应天生一一对应,不存在响应和请求无法匹配的问题。但是当我们打开抓包工具查看相应的相关http报文的时候,会发现其实响应是没有一个字段去和请求一一对应的。这就给http/2的多路复用带来了问题,一个tcp连接中多个请求,多个响应,那么如何将这些响应和请求一一对应呢?只能在二进制分帧层添加字段了,所以在http/2帧的报文格式中,有一个字段是Stream Identifier,这个流ID可以将响应和请求一一对应。这个根本解决了多路复用,请求以及响应匹配的问题。

所以给这个问题来一个总结吧:

  • 二进制协议比文本协议更加紧凑,减少占用空间。
  • 分帧层相当于将http切分,更加灵活,比如可以对header帧做单独的特殊处理。
  • 分帧层有着属于自己的报文头,其中的Stream Identifier 使得操作系统具备将多个响应以及请求一一匹配的能力,这个是http/2 性能提升的关键,也就是多路复用。
  • 二进制分帧层存在的意义是将请求或响应切分,可以更加灵活处理。分帧层定义的报文格式,例如Stream Identifier等,直接关乎http/2 的各种优化方案的实现。

问题二:tcp与http/2的帧的关系

在http/2中帧是最小通信单位,我的问题是http/2 中的帧和最小的tcp数据包是什么关系,一一对应的吗?在http/2 的请求中,tcp是如何拆分以及组装消息的?

很多面试题中多路复用的解释

其中的解释如下:

  • 同域名下所有通信都在单个连接上完成,消除了因多个 TCP 连接而带来的延时和内存消耗。
  • 单个连接上可以并行交错的请求和响应,之间互不干扰

下面的解释当中,并行交错的请求和响应。其实这个并行请求应该具体为并行http请求,那么tcp可以将请求拆分并行请求吗?

这个就要从tcp如何组装以分拆http报文说起了,

TCP 数据包在 IP 数据包的负载里面。它的头信息最少也需要20字节,因此 TCP 数据包的最大负载是 1480 - 20 = 1460 字节。由于 IP 和 TCP 协议往往有额外的头信息,所以 TCP 负载实际为1400字节左右。因此,一条1500字节的信息需要两个 TCP 数据包

我们假设有两个post请求并行进行请求:

截屏2021-02-27 上午10.32.35.png

如果TCP协议层面并行请求,并将顺序打乱,这样可以吗?
截屏2021-02-27 上午10.22.02.png

在我看来其实这是不可以的,因为tcp协议是完全按照顺序组装数据包的,如果并行请求并将数据包顺序打乱,那么计算机将不知道如何组装这些数据包。

所以上面的并行请求是不准确的,在计算机内部一定是串行请求,发完一个请求的数据包,在发送下一个请求的数据包,这样才能保证数据包组合正确。

所以http/2中的帧,是按照顺序串行请求的,如果不是这样,数据将无从组装,一一对应。
截屏2021-02-27 上午10.30.21.png

总结一下吧:

  • http/2中的帧是最小通信单位,但这仅仅是在http/2这一协议层。一个http/2中的帧,如果信息过多,可能是由多个tcp拆分组成,所以http/2中的帧概念,并不是一一对应网络协议中的一个数据包。
  • 一个或多个tcp报文组成一个帧,一个或多个帧组成一个http/2的请求或者响应。
  • http/2 一个tcp链接不存在并行传输,是串行传输,发送完一个帧的数据包,再发送下一个帧的相关数据包。

如果有哪些地方理解不到位,请不吝赐教

2

2

vite 学习笔记

参考文章

背景

Vite是一个构建工具,旨在为现代web项目提供更快、更精简的开发体验。

vite主要分两个模块:

  • 通过native ES module实现的本地开发服务,可以提供极快的热更新服务。
  • 通过rollup对生产环境打包,可以极大地优化打包生产环境的代码。

vite 和 webpack 开发环境最大的区别就是vite 在开发环境抛弃了打包这一个理念,直接在开发环境使用Javascript module,减少打包带来的时间损耗,极大地方便了本地开发。

对于vite的学习,我主要总结了以下四个模块进行总结。

  • 模块路径解析
  • 不同格式文件处理
  • 热更新
  • 预打包

模块路径解析

对于一个native es module服务系统而言,不同模块的路径解析非常重要,这里面有以下几个问题:

  • node_modules等特殊路径内容如何处理
  • 相对路径不便于记录唯一文件路径

vite的解决方式:

  • 将裸模块(node_modules)做转换 "vue.js" --> "/@modules/vue.js"
  • 将相对路径转为绝对路径,便于vite统一文件路径识别。 import '../../a.js' --> '/src/a.js'

不同格式文件处理

通过不同格式的处理,我们可以理解类似于webpack loader对于不同文件是如何处理的, 了解vite工作机制。

  • vue
  • css
  • json
  • html

对于不同格式的文件,vite统一都处理成javascript格式,在返回的response 中添加
Content-Type: application/javascript; charset=utf-8

vue:

vue 的组件是一个单文件组件的机制。一个vue组件的定义基本分三个部分:

<template></template>
<script></script>
<style></style>

编译器会将一个vue组件的三部分分别处理。在vite中,请求一个组件的资源:

截屏2021-03-18 下午7.53.28.png

Helloworld.vue script 逻辑部分编译:

// 此文件可以理解为一个组件的script逻辑部分
import string from '/src/string.js'
const __script = {
    name: 'HelloWorld',
    props: {
        msg: String
    },
    data() {
        return {
  				age: 123
        }
    }
}
// 这里引入组件的template部分
import "/src/components/HelloWorld.vue?type=style&index=0"
// 这里引入组件的style部分
import {render as __render} from "/src/components/HelloWorld.vue?type=template"
__script.render = __render
__script.__hmrId = "/src/components/HelloWorld.vue"
__script.__file = "/Users/lizhuang/gitcode/vite-test/src/components/HelloWorld.vue"
export default __script

HelloWorld.vue?type=style 样式部分编译:

import { updateStyle } from "/vite/client"
const css = "\nh1 {\n  background: red;\n}\n"
updateStyle("62a9ebed-0", css)
export default css

HelloWorld.vue?type=template 结构部分编译:

import {
  toDisplayString as _toDisplayString,
  createVNode as _createVNode,
  createTextVNode as _createTextVNode,
  Fragment as _Fragment,
  openBlock as _openBlock,
  createBlock as _createBlock
} from "/@modules/vue.js"

const _hoisted_1 = /*#__PURE__*/
_createVNode("p", null, "string1", -1 /* HOISTED */
)
const _hoisted_2 = /*#__PURE__*/
_createVNode("p", null, [/*#__PURE__*/
_createTextVNode("Edit "), /*#__PURE__*/
_createVNode("code", null, "components/HelloWorld.vue"), /*#__PURE__*/
_createTextVNode(" to test hot module replacement.")], -1 /* HOISTED */
)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
    return (_openBlock(),
    _createBlock(_Fragment, null, [_createVNode("h1", null, _toDisplayString($props.msg), 1 /* TEXT */
    ), _createVNode("button", {
        onClick: _cache[1] || (_cache[1] = $event=>($data.count++))
    }, "count is: " + _toDisplayString($data.count), 1 /* TEXT */
    ), _hoisted_1, _createVNode("p", null, _toDisplayString($data.string), 1 /* TEXT */
    ), _hoisted_2], 64 /* STABLE_FRAGMENT */
    ))
}

JSON 文件格式处理

通过 rollup-pluginutils 的dataToEsm方法

{
  custom: 'data',
  to: ['treeshake']
}

转变为:

export const custom = 'data';
export const to = ['treeshake'];
export default { custom, to };

CSS 文件格式处理

其实在vue的样式部分已经有所涉及。

import { updateStyle } from "/vite/client"
const css = "\nh1 {\n  background: red;\n}\n"
updateStyle("62a9ebed-0", css)
export default css

其中 updateStyle 是更新样式的关键函数,我们进行分析:

/**
* content: css文件内容
*/
function updateStyle(id, content) {
  ...
		if (!style) {
      style = new CSSStyleSheet()
      style.replaceSync(content)
      document.adoptedStyleSheets = [...document.adoptedStyleSheets, style]
    } else {
      style.replaceSync(content)
    }
  ...
}    
		

vite利用** CSSStyleSheet **代表一个样式表,利用javascript的接口编辑或者添加相关的样式。

当然还有一个特殊的情况就是css文件中有@import 等操作, 这种特殊的情况,vite直接使用style标签进行样式插入。

cosnt style = document.createElement('style')
style.setAttribute('type', 'text/css')
style.innerHTML = content
document.head.appendChild(style)

热更新

vite1 代码较少,这可以让我们低成本的学习一个开发环境热更新的具体细节

image.png

其中第四部处理不同文件的方式,列在了下方:

async function handleMessage(payload: HMRPayload) {
  const { path, changeSrcPath, timestamp } = payload as UpdatePayload
  switch (payload.type) {
    case 'connected':
      console.log(`[vite] connected.`)
      break
    case 'vue-reload':
      queueUpdate(
        import(`${path}?t=${timestamp}`)
          .catch((err) => warnFailedFetch(err, path))
          .then((m) => () => {
            __VUE_HMR_RUNTIME__.reload(path, m.default)
            console.log(`[vite] ${path} reloaded.`)
          })
      )
      break
    case 'vue-rerender':
      const templatePath = `${path}?type=template`
      import(`${templatePath}&t=${timestamp}`).then((m) => {
        __VUE_HMR_RUNTIME__.rerender(path, m.render)
        console.log(`[vite] ${path} template updated.`)
      })
      break
    case 'style-update':
      // check if this is referenced in html via <link>
      const el = document.querySelector(`link[href*='${path}']`)
      if (el) {
        el.setAttribute(
          'href',
          `${path}${path.includes('?') ? '&' : '?'}t=${timestamp}`
        )
        break
      }
      // imported CSS
      const importQuery = path.includes('?') ? '&import' : '?import'
      await import(`${path}${importQuery}&t=${timestamp}`)
      break
      .... 还有很多,就不一一列举了
  }
}

预打包

vite的预打包优化手段其实和小程序页面预加载技术,以及网页的prefetch,preload等的原理是基本一致的,当我们尽量少的打包过后,那么预打包那些没有处理的文件就是优化的手段之一。

vite 会去分析package.json 当中的依赖项,会将依赖进行打包并缓存:

截屏2021-03-21 下午3.40.26.png

其中lodash较为特殊,因为其文件众多,如果不进行预打包的话,开发项目将会请求很多相关文件,造成网页reload时性能衰减。所以vite预打包的另外一个重要的功能就是通过rollup或者esbuild(vite 不同版本实现不同),将过于零散的文件打包,减少网络请求,提高页面reload性能

截屏2021-03-21 下午3.42.57.png

调用栈、执行上下文、变量环境以及词法环境

文章参考:

https://time.geekbang.org/column/article/126339
mqyqingfeng/Blog#4

执行上下文

**定义:**当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。

调用栈

调用栈是javascript引擎追踪函数执行的一个机制,通过调用栈就能够了解函数之间的调用关系。javascript利用栈这种数据结构管理执行上下文。

var a = 0;

function add(a + b) {
	returan a + b;
}

function sum(c) {
	return c + add(2, 3);
}

sum(a);

以上代码执行调用栈如下:
调用栈

可以看到调用栈如果不能有序退出那么就会造成栈溢出,这种情况一般会发生在递归调用结束条件有问题情况等等。

块级作用域

作用域决定了代码区块中变量和其他资源的可访问性。

ES6 之前javascript没有块级作用域,只有全局作用域和函数作用域。var、let、const是js定义变量的三个关键词,其中var和let、const有本质不同。let 和 const 都是es6语法。两者都支持块级作用域,并没有变量提升现象,即:不会再编译阶段将声明放置到代码顶部。而在javascript为了加入块级作用域,引入了词法环境这一概念。我们可以简单地认为,var以及function声明的变量加入到环境变量,而let以及const声明的变量加入到词法环境当中。

我们可以通过一个函数的创建执行来分析这两种变量的不同。

function strong(){
  var a = 1;
  let b = 2;
  {
    var c = 3;
    let d = 4;
    console.log(c)
    console.log(d)
  }
  console.log(a)
  console.log(b)
  console.log(c)
}

foo()

当函数创建并执行的时候,会产生如下的调用栈:
调用栈

当执行到console.log(c) 的时候,调用栈如下:
调用栈

可以看到 let , const 等块级作用域变量会直接放到词法环境中,首先在这里寻找变量,如果没有再去变量环境中寻找。块级代码执行完后,这些变量会被词法环境栈直接弹出。

cookie

cookie 概念

cookie 的英文意思有两个一个是饼干,另一个是坚强的人。将数据信息命名为 cookie 的原因可以看这个

cookie 是服务器产生的数据信息(部分 cookie 也可能是由网页 javascript 代码注入),存储在浏览器中,常用来存储用户信息等。

cookie 的存储是通过一个字符串来完成的

"name=xxx;age=123"

既然 cookie 这个概念代表一部分数据,那么下面我们将讨论数据的来源、存储以及应用

cookie 的来源

  • 服务器: 服务器可以通过设置 cookie 直接在浏览器中存储相关数据
  • javascript 可以新增、修改(部分 cookie)

cookie 的存储

cookie 的存储以及修改会涉及到 cookie 的配置问题,下面对相关配置进行总结

Expires

cookie 的最长有效时间,如果没有设置这个属性,浏览器关闭时 cookie 会被清除

Max-Age

经过多少秒 cookie 过期,如果也存在 Expires 属性,已 Max-Age 为准。也就是说 Max-Age 比 Expires 权重要高。

Domain

域名规定 cookie 可被发送的域名,如果不设置,默认为当前页面的 host。如果设置了,子域名都是允许的。

比如 Domain = a.com,那么域名 b.a.com 就是允许访问这个 cookie 的。

Path

页面路径和域名的设置原理基本一致,/ 字符被认为是文件名的分隔符。 如果你设定 Path=/docs 那么

  • /docs
  • /docs/web
    都是可以访问 cookie 的

Secure

设定之后,只有是 http 协议才能访问这个 cookie

HttpOnly

设置后 javascript 无法访问这个 cookie

SameSite

  • Strict 严格模式,禁止所有跨域的 cookie 发送
  • None 完全允许,允许所有跨域的 cookie 发送
  • Lax 宽松模式,允许部分跨域 cookie 发送

cookie 的跨域使用

在浏览器存储由接口返回的 cookie 后,此后的每一个请求,只要是满足 cookie 的设置,cookie 都会被 http 请求携带。

我在这一部分会重点对跨域请求的 cookie 携带做一个总结。

cors 接口跨域请求

对于cookie 的使用,一个主动地使用场景就是请求跨域的接口,如果这个跨域的接口需要携带 cookie 有以下两个方面需要考虑

withCredentials

跨域请求接口,浏览器默认是不会携带 cookie 的

在调用接口后,我们需要指定一个属性 withCredentials

const invocation = new XMLHttpRequest();
const url = 'https://bar.other/resources/credentialed-content/';

function callOtherDomain() {
  if (invocation) {
    invocation.open('GET', url, true);
    // 在指定这个属性后,接口调用才有可能携带 cookie
    invocation.withCredentials = true;
    invocation.onreadystatechange = handler;
    invocation.send();
  }
}

cors 跨域请求分两种一种是简单请求,一种是复杂请求 (不理解请自行baidu)

  • 如果是简单请求,当设置此属性后,跨域请求会直接携带相关的 cookie
  • 复杂请求会等待 option 预请求回来后,才会携带相关 cookie

SameSite

SameSite 属性是浏览器除了 withCredentials 判断跨域请求是否可以携带此 cookie 的另一个机制。

浏览器的跨域请求必须同时满足这两个机制,才能将 cookie 发出

除了我们在 javascript 中使用 XMLHttpRequest 以及 fetch。我们还会在 html 中引入一下标签,常用的比如 img、iframe 等。其中这些资源也会涉及到 cookie 发送的问题,这些问题往往被我们所忽视:

请求类型 示例 strict None lax
链接 < a href="">
预加载 < link ref="prerender" href="" />
get 表单 < form method="get">
post 表单 < form method="post">
iframe < iframe src=""> </iframe>
AJAX get("")
image < img src="">

其中需要特别注意的就是 a 标签,当点击标签后,如果 same-site 属性为 strict,跳转过去的页面是不会携带 cookie 的。所以会造成很多网页的登录状态失效,这个要特别注意。

所以我觉得默认的 lax 其实是比较合适的,在安全性以及易用性上保持平衡

cookie 与安全

在学习使用 egg 的过程中,会涉及到一些关于 cookie 安全的问题。下面对这些 cookie的安全问题做一个总结

csrf-token

csrf (cross-site request forgery) 跨站请求伪造。一句话解释就是,攻击者会利用 cookie 信息,在第三方网页上伪造真实请求,进行网络攻击。

特点:

  • 攻击一般发生在第三方网站,而不是被攻击的网站。
  • 攻击者利用受害者 cookie 信息,冒充受害者身份,进行网络交互。

解决方式

第一种就是 same-site 属性。当然只有支持这个属性的浏览器才可以。如果是版本较老的浏览器就不行了。

第二种方式就是 csrf-token

csrf-token 简单说就是在 cookie 中存储一个新的字段,通过网络请求页面时服务器注入一个加密的字符串,在之后的网络请求时,都会携带这个字符串,如果服务器判断字符串不合法,就不会返回真实数据以及数据交互。

csrf-token 的实现也有两种方式。

  1. 第一种就是 session 存储, 服务器会在内存或者数据库存储 session 数据。
  2. 第二种方式是规定所有接口都要携带一个新的请求头携带 csrf-token 数值,并且浏览器也会自动在 cookie 字段中携带 csrf-token 数值。服务器会在收到请求时比对这两个 token 是否一致来判断是否来自于第三方页面。

我比较喜欢第二种方式,因为可以减轻服务器的压力,实现也比较简单。

cookie 防篡改

cookie 存储在浏览器中,javascript、用户手动都是可以进行修改的。

为了防止 cookie 被篡改,可以在服务器下发 cookie 的时候,同时下发一个根据内容加密的 cookie 字段。


set-cookie name=008; path=/; httponly
set-cookie name.sig=BKz_FtEld6gVjNwSzdNZAXZCq3n2Vf7VcHISiEBp7oc; path=/; httponly

其中 name.sig 就是防止篡改的字段。当服务器接收到 name 时会同时和 name.sig 进行比对,如果不一致,就意味着 cookie 被篡改了

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.