web中的拦截技巧

代码注入时机

  • 源码注入
  • 构建,推送服务注入
  • 网关注入,nginx 等修改响应文件
  • 浏览器插件注入

重写 api

通过 重写 fetch xhr 原型链,添加额外的功能

1
2
3
4
5
6
7
8
9
10
const _console = window.console;
window.console = new Proxy(_console, {
get(target, props) {
const fn = target[props];
return (...args) => {
fn.apply(null, args);
_console.info("other info");
};
},
});

拦截事件

1
2
3
4
5
6
7
8
9
10
11
12
13
document.addEventListener(
"click",
(e) => {
e.stopPropagation();
e.preventDefault();
},
{
// 捕获阶段执行
capture: true,
// 一个布尔值,设置为 true 时,表示 listener 永远不会调用 preventDefault()
passive: false,
}
);

监听 DOM 变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 选择需要观察变动的节点
const targetNode = document.getElementById("some-id");

// 当观察到变动时执行的回调函数
const callback = function (mutationsList, observer) {
// Use traditional 'for loops' for IE 11
for (let mutation of mutationsList) {
if (mutation.type === "childList") {
console.log("A child node has been added or removed.");
} else if (mutation.type === "attributes") {
console.log("The " + mutation.attributeName + " attribute was modified.");
}
}
};

// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
// 观察器的配置(需要观察什么变动)
const config = { attributes: true, childList: true, subtree: true };
// 以上述配置开始观察目标节点
observer.observe(targetNode, config);

// 之后,可停止观察
observer.disconnect();

监听对象属性变化

1
2
3
4
5
6
7
8
9
10
11
const obj = { a: 1 };

Object.defineProperty(obj, "a", {
get() {},
set(v) {},
});

const newObj = new Proxy(obj, {
get(target, props) {},
set(target, props, value) {},
});

service worker

service worker

1
this.addEventListener("fetch", function (event) {});

web container

webcontainers

整合远程调试方案

根据以上的拦截技巧可以整个一个远程调试的方案,可以实现以下的功能:

  • 实现共享域名的登录态 cookie
  • 在远程设备(手机、测试设备)调试本地开发中服务无需配置 Web 服务的 https 直接使用 https 协议访问开发服务,避免 http 协议导致许多 Web API 不可用仅限于安全上下文的特性 (opens new window)
  • 该服务是一个天然的中间层,可无感注入代码实现效率工具,比如:远程网络抓包、Mock 移动端控制台(eruda)远程代码调试(chii)切换后端接口环境、接口染色

思路:

  • 客户端发起 https 请求,并在请求路径中添加 ip port

  • nginx 拦截指定域名的所有请求
    如果请求的是 html 文件,则 注入客户端的 sdk.js

    1
    2
    3
    4
    5
    location / {
    sub_filter '</body>' '<script src="/sdk.js"/></body>';
    root html;
    index index.html;
    }

    同时把 ip port 写入 cookie

  • 如果是资源请求,直接从 cookie 中读取 ip port

  • sdk 中拦截所有的 a 标签的默认事件,在跳转路径上添加 ip port

  • socket 请求需要重写 socket api,让其携带域名和端口访问

v8引擎相关知识

作用域

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

全局作用域 函数作用域 块级作用域

块级作用域通过 词法环境(Lexical Environment) 实现,通过 const let 声明的变量具有块级作用域,只能在包含他们的代码块中访问。如果在顶级使用 let const,他么们不会被添加到 window 对象上,但是会在全局作用域中

词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。

作用域链

其实在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。

JavaScript 引擎首先会在当前的执行上下文中查找该变量,如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
var a = 1;
let b = 2;
{
let b = 3;
var c = 4;
let d = 5;
console.log(a);
console.log(b);
}
console.log(b);
console.log(c);
console.log(d);
}
foo();
  • 进行词法分析,a,c 加入到变量环境 Variable Environment = {a:undefined,c:undefined}
    b 会加入到词法环境,解析块级作用域中的代码,需要开辟新的词法环境 Lexical Environment = {b:不可达}=> {b:不可达,d:不可达}

    let 只会在执行的时候赋值,在词法分析阶段会被识别,但是不能访问,也就是暂时性死区(TDZ,Temporal Dead Zone)

  • 函数执行,a,b 被赋值,Variable Environment = {a:1,c:undefined}Lexical Environment = {b:2} => {b:3,d:5}
    会先查找词法环境,在查找变量环境

闭包

在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。

如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

执行上下文

执行上下文负责运行时代码的管理,包括作用域,变量,对象,函数生命周期。

  • 创建阶段,确定 this,创建变量环境,创建词法环境。
  • 执行阶段,函数内部代码开始执行,变量赋值,函数引用和执行。
  • 回收阶段,当函数执行完毕后,从相应的栈中弹出,资源被释放。

内存

JavaScript 是一种弱类型的、动态的语言.

  • 弱类型,意味着你不需要告诉 JavaScript 引擎这个或那个变量是什么数据类型,JavaScript 引擎在运行代码的时候自己会计算出来。
  • 动态,意味着你可以使用同一个变量保存不同类型的数据。

数据科技分为原始类型引用类型

原始类型的数据保存在环境变量或词法环境中,包含这两个区域的执行上下文又被压入调用栈中,所以可以说原始类型是保存在栈空间中的.

字符串,symbol,bigint 虽然是原始类型,实际还是存放在堆空间的。

如果是一个引用类型,会单独存放到堆空间中,在分配了引用类型数据之后会拿到一个堆中的地址,在把这个地址保存到环境变量中.

JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。JavaScript 引擎需要离开当前的执行上下文,只需要将指针下移到上个执行上下文的地址就可以了.

所以通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间。

原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。

从内存的角度来理解闭包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo() {
let name = "one";
const obj = {
getName() {
return name;
},
setName(_name) {
name = _name;
},
};
return obj;
}

var bar = foo();
bar.setName("two");
bar.getName();

当 foo 函数执行的时候,首先是编译的过程,当遇到对象的两个方法时还需要对两个方法进行词法分析, 发现引用了 name 变量.

JavaScript 引擎判断这是一个闭包,于是在堆空间创建换一个“closure(foo)”的对象(这是一个内部对象,JavaScript 是无法访问的),用来保存 name 变量。并把堆空间的地址保存在 foo 执行上下文中的环境变量中.

垃圾回收

分为栈内存回收,堆内存回收

栈内存回收需要用到一个记录当前执行状态的指针(称为 ESP),指向的就是当前的执行上下文,JavaScript 引擎会通过向下移动 ESP 来销毁该函数保存在栈中的执行上下文。当下移之后如果有新的函数调用,原来的内存位置就会写入新的执行上下文.

回收堆中的垃圾数据,就需要用到 JavaScript 中的垃圾回收器,垃圾回收的策略都是建立代际假说的基础之上的

代际假说有以下两个特点:

  • 第一个是大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问;
  • 第二个是不死的对象,会活得更久。

需要根据对象变量的不同生命周期长短使用不同的策略:

在 V8 中会把堆分为新生代老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。新生区通常只支持 1 ~ 8M 的容量.副垃圾回收器,主要负责新生代的垃圾回收。主垃圾回收器,主要负责老生代的垃圾回收。

垃圾回收大致都分为一下几个步骤:

  • 标记活动对象和非活动对象
  • 清楚非活动对象
  • 内存整理,并不是所有的回收策略都需要这一步

副垃圾回收器主要负责新生区的垃圾回收. 大多数小的对象都会被分配到新生区,这个区域虽然不大,但是垃圾回收还是比较频繁的。用 Scavenge[ˈskævɪndʒ] 算法来处理.原理是:

把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区.

新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。在垃圾回收过程中,首先要对对象区域中的垃圾做标记;标记完成之后,就进入垃圾清理阶段,副垃圾回收器会把这些存活的对象复制到空闲区域中,同时它还会把这些对象有序地排列起来,所以这个复制过程,也就相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了。

完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。

由于新生代中采用的 Scavenge 算法,所以每次执行清理操作时,都需要将存活的对象从对象区域复制到空闲区域。但复制操作需要时间成本,如果新生区空间设置得太大了,那么每次清理的时间就会过久,所以为了执行效率,一般新生区的空间会被设置得比较小。

也正是因为新生区的空间不大,所以很容易被存活的对象装满整个区域。为了解决这个问题,JavaScript 引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中。

主垃圾回收器 主要负责老生区中的垃圾回收。

除了新生区中晋升的对象,一些大的对象会直接被分配到老生区。因此老生区中的对象有两个特点,一个是对象占用空间大,另一个是对象存活时间长。

,若要在老生区中使用 Scavenge 算法进行垃圾回收,复制这些大的对象将会花费比较多的时间,从而导致回收执行效率不高,同时还会浪费一半的空间。因而,主垃圾回收器是采用标记 - 清除(Mark-Sweep)的算法进行垃圾回收的。

标记阶段就是从一组根元素(调用栈)开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。

因为清除部分内存会产生碎片,而碎片过多会导致大对象无法分配到足够的连续内存,于是又产生了另外一种算法——标记 - 整理(Mark-Compact),这个标记过程仍然与标记 - 清除算法里的是一样的,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

全停顿,内存回收过程过长,又因为 JS 为单线程而阻塞 JS 执行,这个问题叫全停顿.

对于新生代的内存回收影响不大,因为内存比较小,可以全停顿等待执行完成.对于老生代会将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking)算法

使用增量标记算法,可以把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,这样当执行上述动画效果时,就不会让用户因为垃圾回收任务而感受到页面的卡顿了。

JS 如何执行

由解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。

编译型语言在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了

字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。

如果有一段第一次执行的字节码,解释器 Ignition 会逐条解释执行。解释器 Ignition 除了负责生成字节码之外,它还有另外一个作用,就是解释执行字节码。在 Ignition 执行字节码的过程中,如果发现有热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器 TurboFan 就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。

字节码配合解释器和编译器是最近一段时间很火的技术,比如 Java 和 Python 的虚拟机也都是基于这种技术实现的,我们把这种技术称为即时编译(JIT)。

处理新任务

当一个线程在执行任务的时候,如何可以处理新任务,最简单的办法就是通过一个循环,不断检测是否有新的任务产生。

这样可以解决同一个线程中产生的新任务,但是无法解决其他线程中产生的新任务。因为没有办法直接检测其他线程是否有新任务的产生。

通用的模型就是消息队列,息队列是一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点,也就是说要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。

渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息,接收到消息之后,会将这些消息组装成任务发送给渲染主线程

任务类型

任务类型包括, 输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、JavaScript 定时器等等。

除此之外,消息队列中还包含了很多与页面相关的事件,如 JavaScript 执行、解析 DOM、样式计算、布局计算、CSS 动画等。

以上这些事件都是在主线程中执行的,所以在编写 Web 应用时,你还需要衡量这些事件所占用的时长,并想办法解决单个任务占用主线程过久的问题。

高优先级任务

一个典型的场景是监听 DOM 的改变做一些逻辑处理,如果不加入消息队列选择同步处理,在 DOM 频繁改变的时候,当前任务会被延长,导致后面的任务不能及时处理。

如果加入到消息队尾部,又可能影响效率,因为可能已经有很多任务在排队了。

针对这种情况微任务就产生了,通常我们把消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列,在执行宏任务的过程中,如果 DOM 有变化,那么就会将该变化添加到微任务列表中,等宏任务中的主要功能都直接完成之后,这时候,渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务,因为 DOM 变化的事件都保存在这些微任务队列中,这样也就解决了实时性问题。

事件循环

浏览器处理消息队列用到了事件循环系统,但这个事件循环与 nodejs 事件循环没有关系。

  • V8: V8 引擎自己实现了一个事件循环,但是 nodejs 和 浏览器都没有采用
  • 浏览器: 不同的厂商实现可能不同,chrome 浏览器使用 libevent 实现事件循环。
  • nodejs: 使用 libuv 实现事件循环。

setTimeout 如何实现

从使用方式上能感觉到,setTimeout 需要等待指定时间才能执行,而消息队列中的任务是立即执行的,所以 setTimeout 中的回调函数不能立即加入到消息队列中。

所以浏览器还维护着一个延时任务列表,包括定时器和内部一些延时任务,创建一个定时器时,渲染进程会将该定时器的回调任务添加到延迟队列中。这个回调任务包括,定义的回调函数,发起时间,延时时间。

在处理消息队列的时候,会调用出延时任务的方法 ProcessTimerTask 。这个方法的调用时机是当前事件循环中一个任务处理结束后开始执行。

ProcessDelayTask 函数会根据发起时间和延迟时间计算出到期的任务,然后依次执行这些到期的任务。等到期的任务执行完成之后,再继续下一个循环过程。

在 Chrome 中,定时器被嵌套调用 5 次以上,系统会判断该函数方法被阻塞了,在 5 次调用之后,如果定时器的调用时间间隔小于 4 毫秒,那么浏览器会将每次调用的时间间隔设置为 4 毫秒。

未被激活的页面中定时器最小值大于 1000 毫秒,也就是说,如果标签不是当前的激活标签,那么定时器最小的时间间隔是 1000 毫秒,目的是为了优化后台页面的加载损耗以及降低耗电量。

Chrome、Safari、Firefox 都是以 32 个 bit 来存储延时值的,32bit 最大只能存放的数字是 2147483647 毫秒,这就意味着,如果 setTimeout 设置的延迟值大于 2147483647 毫秒(大约 24.8 天)时就会溢出,那么相当于延时值被设置为 0 了,这导致定时器会被立即执行。

XMLHttpRequest 实现流程

渲染进程会将请求发送给网络进程,然后网络进程负责资源的下载,等网络进程接收到数据之后,就会利用 IPC 来通知渲染进程;渲染进程接收到消息之后,会将 xhr 的回调函数封装成任务并添加到消息队列中,等主线程循环系统执行到该任务的时候,就会根据相关的状态来调用对应的回调函数。

宏任务和微任务

前面已经介绍过微任务的由来。

宏任务:

  • 渲染事件(如解析 DOM、计算布局、绘制);
  • 用户交互事件(如鼠标点击、滚动页面、放大缩小等);
  • JavaScript 脚本执行事件;
  • 网络请求完成
  • 文件读写完成事件

WHATWG 规范中定义事件循环机制:

  • 先从多个消息队列中选出一个最老的任务,这个任务称为 oldestTask;
  • 然后循环系统记录任务开始执行的时间,并把这个 oldestTask 设置为当前正在执行的任务;
  • 当任务执行完成之后,删除当前正在执行的任务,并从对应的消息队列中删除掉这个 oldestTask;
  • 最后统计执行完成的时长等信息。

由于宏任务不能精细的控制执行的时机,因为两个红任务之间可能被插入了很多系统级的任务。

微任务:

微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。

  • MutationObserver 监控某个 DOM 节点,然后再通过 JavaScript 来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。
  • Promise 当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务。

在当前宏任务中的 JavaScript 快执行完成时,也就在 JavaScript 引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。WHATWG 把执行微任务的时间点称为检查点。

如果在执行微任务的过程中,产生了新的微任务,同样会将该微任务添加到微任务队列中,V8 引擎一直循环执行微任务队列中的任务,直到队列为空才算执行结束。也就是说在执行微任务过程中产生的新的微任务并不会推迟到下个宏任务中执行,而是在当前的宏任务中继续执行。

  • 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。
  • 微任务的执行时长会影响到当前宏任务的时长。
  • 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。

通过异步操作解决了同步操作的性能问题;通过微任务解决了实时性的问题。

W3C 最新解释:

  • 每一个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列,在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。

  • 浏览器必须准备好一个微队列,微队列中的任务优先所有其他任务执行。

因此现在 chrome 中至少有 3 个队列:

  • 延时队列: 用于存放定时器到达后的回调任务,优先级中
  • 交互任务: 用于存放用户交互产生的任务,优先级高
  • 微队列: 用于存放最快需要执行的任务,优先级最高

Promise.then 返回 Promise 的行为

当一个 promise.then 方法返回另一个 Promise 时,thenable 的状态会吸收返回的 promise 的状态,也就是说 thenable 的状态与返回的 promise 状态保持一致

但是这种状态并不是立即吸收的,返回的 promise 会被使用 then 方法,包装成新的 promise 对象,并添加到微队列中

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
Promise.resolve()
.then(() => {
//1 添加到微队列
console.log(0);

//3 打印0
//4 包装成 (Promise.resolve(4).then(res=>res)).then(res=>res) => p4.then(res=>res)添加到微队列
//7 res=>res 添加到微队列
return Promise.resolve(4).then((res) => res);
//10 完成状态
})
.then((res) => {
//11 添加到微队列
console.log(res);
//14 打印4
});

Promise.resolve()
.then(() => {
//2 添加到微队列
console.log(1);
//5 打印1
})
.then(() => {
//6 添加到微队列
console.log(2);
//8 打印2
})
.then(() => {
//9 添加到微队列
console.log(3);
//12 打印3
})
.then(() => {
//13 添加到微队列
console.log(5);
//15 打印3
});

代码美学(摘要)

如何给变量或类型起名字

不要使用以下的命名方式

  • 不要使用单字母:简洁的变量命名是来自数学中的命名习惯,数学家喜欢公式的精简和优美,但是会丢失大量的上下文信息,让你读代码的时间比写代码的时间还要长

  • 不要使用缩写:与单字母的方式一样,无法理解上下文的信息,让代码很难阅读

  • 不要在命名中添加类型说明: 新生的程序工作者可能不会接触到这个命名方式,它来自于匈牙利命名法在类型提示不完善的年代,增加对变量类型的说明 intTotal,strName

  • 不要再类型中添加类型描述: 最典型的就是对interface的定义使用 IBoxProps

  • 不要再类或类型中添加 BaseAbstract: 可能你会习惯性的把基础类命名中添加 Base, 但是这并不能说明他应该被继承或是被实现,只需要直接命名即可,例如 Box
    另外你的命名对子类如何使用并不会有影响,例如 (box:Box)=>void, 并不关心 Box 是不是抽象类
    当你不知道如何给基础类命名的时候,可以考虑是否需要对子类修改命名方式,添加更多信息, 例如子类实现了不同颜色的盒子 YellowBox

推荐的命名方式

  • 在命名中增加单位描述: delaySeconds , 对于一些强类型语言可以使用类型标注,例如 C# TimeSpan, 而对于弱类型语言可以在命名中添加单位描述

你可能不需要Utils文件

可能你正在用一个Utils方法过滤电影列表

1
2
3
class Utils {
filterMove(){}
}

但事实上这应该是电影类中的一部分

1
2
3
class Movie {
filterMove(){}
}

当你准备写一个utils方法的使用请考虑它是否足够抽象, 亦或者考虑将他放到更明确的类或功能模块中去。

使用组合而不是继承

继承最大的问题在于,无法找到一个完美的抽象,随着业务和需求的发展,总是需要调整被继承的类,这将会影响全局。

如果需要使用类,遵循以下的原则:

  • 需要实现的类中有大量重复的接口
  • 避免直接访问受保护的成员变量
  • 显式的为子类创建需要重写的API
  • 每个方法都需要标注行为 final/sealed/private, 避免修改时导致错误

如是使用继承,需要注意以下几点:

  • 组合会导致功能定义时产生大量的重复初始化代码
  • 如果想从类中暴露方法,需要写大量的包装函数
    1
    2
    3
    4
    5
    class Box{
    getName(){
    return this.type.getName()
    }
    }
  • 虽然会导致少量冗余,但是好处远远大于坏处,它可以让你的代码耦合程度更低。

vue/dev-server

demo 介绍

demo 展示在页面中直接引入 esModule 文件,并在文件中引入 vue 组件的渲染流程

渲染流程

sequenceDiagram autonumber participant Client participant Server Client->>Server: /main.js Note left of Server: 返回资源的时候处理node_modules中依赖的路径 Server->>Client: main.js文件内容 Note left of Server: __module/vue 和 test.vue Client->>Server: 浏览器分析分析并请求import资源 Server->>Client: 返回依赖资源或是打包后的js文件
  • 浏览器向服务器请求入口文件 main.js, 请求路径为 import 路径 .main.js

  • 服务端接收到请求,交给 middleware 处理,首先检查缓存,如果缓存存在直接返回缓存结果,如果不存在,通过请求路径读取本地资源文件,处理后加入缓存并返回

  • 如果文件是 js 结尾, 这里表示的是入口文件

    • 将文件内容转为 ast 语法树, 分析依赖模块包括 vue 和 text.vue,将 node_module 中的依赖路径转换为自定义路径用于资源请求时区分资源并可以加入缓存优化,例如__module/vue,将 ast 生成的代码返回给浏览器
    • 浏览器会自动请求 esMoudle 中 import 的文件
      http://localhost:3000/\_\_modules/vue
      http://localhost:3000/test.vue
  • 如果文件路径中包含 \_\_moudles, 则尝试加载 node_modules 中的文件
    通过 require.resolve("vue") 可以获取某个包在 node_modules 中的绝对路径

  • 如果文件路径是以 .vue 结尾, 需要使用 vue 提供的编译模块将文件编译成单个 js 文件

    • 首先通过 vueComplier compileToDescriptor 将 vue 文件处理 template, styles,scripts 的描述对象
    • 在使用 vueCompiler.assemble 将描述文件中的各部分组装成代码,返回给浏览器

middleware.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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
const vueCompiler = require("@vue/component-compiler");
const fs = require("fs");
const stat = require("util").promisify(fs.stat);
const root = process.cwd();
const path = require("path");
const parseUrl = require("parseurl");
const { transformModuleImports } = require("./transformModuleImports");
const readFile = require("util").promisify(fs.readFile);
const parseUrl = require("parseurl");
const defaultOptions = {
cache: true,
};

async function loadPkg(pkg) {
if (pkg === "vue") {
const dir = path.dirname(require.resolve("vue"));
const filepath = path.join(dir, "vue.esm.browser.js");
return readFile(filepath);
} else {
// TODO
// check if the package has a browser es module that can be used
// otherwise bundle it with rollup on the fly?
throw new Error("npm imports support are not ready yet.");
}
}
async function readSource(req) {
const { pathname } = parseUrl(req);
const filepath = path.resolve(root, pathname.replace(/^\//, ""));
return {
filepath,
source: await readFile(filepath, "utf-8"),
updateTime: (await stat(filepath)).mtime.getTime(),
};
}
function transformModuleImports(code) {
const ast = recast.parse(code);
recast.types.visit(ast, {
visitImportDeclaration(path) {
const source = path.node.source.value;
if (!/^\.\/?/.test(source) && isPkg(source)) {
path.node.source = recast.types.builders.literal(
`/__modules/${source}`
);
}
this.traverse(path);
},
});
return recast.print(ast).code;
}

const vueMiddleware = (options = defaultOptions) => {
let cache;
let time = {};
if (options.cache) {
const LRU = require("lru-cache");

cache = new LRU({
max: 500,
length: function (n, key) {
return n * 2 + key.length;
},
});
}

const compiler = vueCompiler.createDefaultCompiler();

function send(res, source, mime) {
res.setHeader("Content-Type", mime);
res.end(source);
}

function injectSourceMapToBlock(block, lang) {
const map = Base64.toBase64(JSON.stringify(block.map));
let mapInject;

switch (lang) {
case "js":
mapInject = `//# sourceMappingURL=data:application/json;base64,${map}\n`;
break;
case "css":
mapInject = `/*# sourceMappingURL=data:application/json;base64,${map}*/\n`;
break;
default:
break;
}

return {
...block,
code: mapInject + block.code,
};
}

function injectSourceMapToScript(script) {
return injectSourceMapToBlock(script, "js");
}

function injectSourceMapsToStyles(styles) {
return styles.map((style) => injectSourceMapToBlock(style, "css"));
}

async function tryCache(key, checkUpdateTime = true) {
const data = cache.get(key);

if (checkUpdateTime) {
const cacheUpdateTime = time[key];
const fileUpdateTime = (
await stat(path.resolve(root, key.replace(/^\//, "")))
).mtime.getTime();
if (cacheUpdateTime < fileUpdateTime) return null;
}

return data;
}

function cacheData(key, data, updateTime) {
const old = cache.peek(key);

if (old != data) {
cache.set(key, data);
if (updateTime) time[key] = updateTime;
return true;
} else return false;
}

async function bundleSFC(req) {
const { filepath, source, updateTime } = await readSource(req);
const descriptorResult = compiler.compileToDescriptor(filepath, source);
console.log(descriptorResult);
const assembledResult = vueCompiler.assemble(compiler, filepath, {
...descriptorResult,
script: injectSourceMapToScript(descriptorResult.script),
styles: injectSourceMapsToStyles(descriptorResult.styles),
});
return { ...assembledResult, updateTime };
}

return async (req, res, next) => {
if (req.path.endsWith(".vue")) {
const key = parseUrl(req).pathname;
let out = await tryCache(key);

if (!out) {
// Bundle Single-File Component
const result = await bundleSFC(req);
console.log(result);
out = result;
cacheData(key, out, result.updateTime);
}

send(res, out.code, "application/javascript");
} else if (req.path.endsWith(".js")) {
const key = parseUrl(req).pathname;
let out = await tryCache(key);

if (!out) {
// transform import statements
const result = await readSource(req);
out = transformModuleImports(result.source);
console.log(out);
cacheData(key, out, result.updateTime);
}

send(res, out, "application/javascript");
} else if (req.path.startsWith("/__modules/")) {
const key = parseUrl(req).pathname;
const pkg = req.path.replace(/^\/__modules\//, "");

let out = await tryCache(key, false); // Do not outdate modules
if (!out) {
out = (await loadPkg(pkg)).toString();
cacheData(key, out, false); // Do not outdate modules
}

send(res, out, "application/javascript");
} else {
next();
}
};
};

exports.vueMiddleware = vueMiddleware;

隐式转换

一元操作符

一元操作符包括,+, -, ~, !,其中 +, - 需要和加法运算符区分开。

ECMA 规定了 一元操作符 求值过程。

1
2
3
4
console.log(+true); // 1
console.log(+[]); // 0
console.log(+{}); //NaN
console.log(+"123"); // 123

对于 + 一元表达式求值,需要首先对操作数(V)取值(GetValue, 对取值结果进行 ToNumber 操作。

ToNumber:

  1. 如果 V 是 Number,返回 V
  2. 如果 V 是 SymbolBigInt 抛出错误 TypeError
  3. 如果 V undefined, 返回 NaN
  4. 如果 V 是 nullfalse 返回 +0
  5. 如果 V 是 true 返回 1 (案例 1)
  6. 如果 V 是 String 返回 StringToNumber(argument) StringToNumber 规定如果不能转成数字,返回 NaN 如果可以则返回 Number (案例 4)
  7. V 如果是一个 Object
  8. 将 V 转换为原始值 ToPrimitive 数组将会通过 toString 转换为 "", 再次执行第 6 步,最终数组被转换为 0 (案例 2),对象在经过 ToPrimitive 转换为 [Object object], 在执行 StringToNumber 由于不能转换为数字,最终返回 NaN (案例 3)
  9. 原始值不是对象,再次执行 ToNumber

修改 GPT/MBR 分区

添加新的磁盘,或在虚拟机中扩容磁盘,需要修改分区。

查看分区现状

1
2
3
4
5
6
lsblk
# 或者
fdisk -l /dev/sda

# 先使用 sudo apt install parted 安装 parted
parted /dev/sda print

可以看到磁盘容量变大,但是分区没有变化

1
2
3
4
5
6
7
8
9
10
11
12
Disk /dev/sda: 105 GiB, 112742891520 bytes, 220200960 sectors
Disk model: QEMU HARDDISK
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x94a3b99f

Device Boot Start End Sectors Size Id Type
/dev/sda1 * 2048 8484863 8482816 4G 83 Linux
/dev/sda2 8486910 10483711 1996802 975M 5 Extended
/dev/sda5 8486912 10483711 1996800 975M 82 Linux swap / Solaris

manjaro美化

添加 archlinux 源

mirrorlist-repo 官方仓库地址

1
2
3
4
# /etc/pacman.conf

[archlinuxcn]
Server = https://repo.archlinuxcn.org/$arch

设置源

  • 软件仓库中选择 Preferences, use mirrors 选择指定源

  • 通过命令行选择

    1
    2
    3
    4
    5
    # 手动选择镜像列表
    sudo pacman-mirrors -i -c China -m rank

    # 自动生成配置列表 -g 表示从活动池中选择镜像列表
    sudo pacman-mirrors -c China -g

更新系统

1
sudo pacman -Syyu

安装 yay 助手

1
sudo pacman -S yay

输入法

Fcitx5

1
sudo pacman -S fcitx5-im fcitx5-chinese-addons fcitx5-material-color

系统设置中添加拼音输入法

配置环境变量,让应用程序可以使用输入法

1
2
3
4
5
# /etc/environment

GTK_IM_MODULE=fcitx
QT_IM_MODULE=fcitx
XMODIFIERS=@im=fcitx

配置插件:

  • 点击 configure addons, 选择 classic user interface 勾选 use per screen DPI, 保证输入法随系统缩放。

设置联想:

  • 使用云拼音实现联想功能

    点击 configure addons, 选择 PinYin, 开启云拼音,并选择 Baidu

oh-my-zsh

oh-my-zsh

SSH

GNOME/Keyring

如果安装的是 KDE 桌面等桌面环境的版本,可能无法自动获取 SSH Key 的密码,导致每次使用都会提示输入密钥密码

安装 Cinnamon 它是一个独立的桌面环境,包含了 Keyring 所需要的工具包,首次认证会弹出一个 GUI 窗口填写密码,后续可以免密登录

1
yay cinnamon

开启 gcr 守护进程

1
systemctl --user enable --now gcr-ssh-agent.service

添加环境变量

1
export SSH_AUTH_SOCK="${XDG_RUNTIME_DIR}/gcr/ssh"

虚拟化环境配置显卡直通

X11 环境中强制 Xorg 使用 NVIDIA GPU, 需要注意 PCI设备不要填错, 通过命令lspci | grep -i nvidia 查看

1
2
sudo rm /etc/X11/xorg.conf
sudo nvidia-xconfig --busid=PCI:1:0:0 --force-generate

设置环境变量

1
2
3
echo 'export __NV_PRIME_RENDER_OFFLOAD=1' >> ~/.zshrc
echo 'export __GLX_VENDOR_LIBRARY_NAME=nvidia' >> ~/.zshrc
source ~/.zshrc

LVM模式安装manjaro

安装流程

首先对物理磁盘分区,对于 UEFI 引导方式,需要划分 efi 分区 并把磁盘挂载到 /boot/efi 路径

注意: 所有的分区和格式化操作都需要在命令行中执行, manjaro 的 GUI 只用来挂载分区

如果使用 window 环境下的虚拟机安装, 在 启用或关闭 windows 功能 配置中,关闭 windows 沙箱, Hyper-v 两个配置, 这功能可能导致虚拟死机。

  • 启动前将系统的引导方式修改为 UEFI,进入系统时选择专有驱动的选项启动方式

  • 使用 fdisk 查看磁盘信息

  • 格式化磁盘为 GPT 格式,划分出 500M 作为 efi 分区,剩余空间给 LVM 使用

  • 分区成功后将 efi 分区格式化为 FAT32 格式

  • 将剩余的空间转为 LVM, 创建出 /dev/vgdisk/lvhome 分区,并将 lvhome 分区格式化为 ext4

分配成功后使用以下命令查看分区信息:

1
2
3
4
5
6
7
8
9
sudo pvs
sudo vgs
sudo lvs

# 查看详细信息

sudo pvdisplay
sudo vgdisplay
sudo lvdisplay

删除分区

1
2
3
sudo pvremove /dev/something
sudo vgremove something
sudo lvremove /dev/something
  • 通过 GUI 界面将分区挂载到指定路径,挂载磁盘时一定不要重新格式化磁盘

错误处理

  • 开机时提示一下错误,可能是遗留的 bug,解决方案如下

1
2
3
4
# /etc/default/grub
GRUB_SAVDEFAULT=false

update-grub

手动挂载分区

系统安装成功后,可能需要挂载更多自定义分区, 首先创建一个 workspace lv分区

查看分区信息

1
2
3
4
sudo blkid

#指定分区
sudo blkid /dev/sda1

添加信息到 /etc/fstab, 实现自动挂载

扩容 lvm

  • 扩容虚拟磁盘, vmware 磁盘设置中增加磁盘容量。

  • 使用 gdisk 为剩余磁盘创建分区,创建成功后磁盘可能无法使用,使用以下命令刷新分区表

    1
    sudo partprobe
  • 使用 pvcreate 将新的分区创建为新的pv

  • 扩容 vg 将新的 pv 添加至 指定卷组

    1
    2
    3
    vgextend vgdisk /dev/sda3

    vgs #查看容量
  • 扩容 lv

    -l + :指定逻辑卷的LE个数,如 -l +200 一般一个为 4M
    -L + :表示增加多少空间,如 -L +15G ,单位有bBsSkKmMgGtTpPeE
    -l +100%FREE :表示增加vg的全部可用空间

    1
    lvextend -L +15G /dev/vgname/lvname
  • 扩展文件系统

    1
    resize2fs /dev/something

⑪控制访问-代理模式

代理模式

为另一个对象提供一个替身或占位符以控制对这个对象的访问。

代理模式非常灵活,可能不经意写的一行代码也是代理模式,例如:

  • 分发请求到到远程
  • 为初始化开销大的对象提供代理
  • 保护某些对方法不能访问

JS 中提供了 Proxy 这个代理对象,

图片加载

想象一下一个图片的加载过程是不是可以通过代理来实现

  • 发起加载图片并绘制在页面上的请求
  • 这个请求将发送给代理对象
  • 代理对象通过网络获取图片
  • 绘制图片并提供图片控制的接口

防火墙代理

控制网络资源的访问,保护主题免于侵害

只能引用代理

当主题被引用时,进行额外的动作,例如计算一个对象被引用的次数

缓存代理

为开销大的运算结果提供暂时存储,它也允许多个客户共享结果,以减少计算或网络延迟。

同步代理

在多线程的情况下,提供安全访问。

复杂隐藏代理

用来隐藏一个类的复杂度,并控制访问,有时候也称为 外观代理
复杂隐藏代理和外观模式不一样,因为代理控制访问,而外观模式提供另一组接口。

写入时复制代理

用来控制对象的复制,方法是延迟对象的复制,直到客户真的需要为止。这是虚拟代理的变体。

CSS 尺寸/位置/布局/元素

复选框与文本对齐

1
2
<p><input type="checkbox" /><span>文本</span></p>
<p><input type="radio" /> <span>文本</span></p>

当字号大于 input 尺寸时, input 会与文字中间对其

但是字号过小的时候,input 与文字底部并不能对其

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 方案1 */
input {
vertical-align: -3px;
}

/* 方案2 */
input {
vertical-align: top;
margin-top: 4px;
}

/* 方案3 */
input {
vertical-align: middle;
margin-top: -2px;
margin-bottom: 1px;
}

多行文字垂直居中

1
2
3
4
5
<div class="box">
<div class="child"
>这里显示多行文字。这里显示多行文字。这里显示多行文字。这里显示多行文字。这里显示多行文字。这里显示多行文字。这里显示多行文字。这里显示多行文字。</span
>
</div>
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
/* 方案1 */

.box {
width: 500px;
height: 500px;
display: flex;
align-items: center;
}

/* 方案2 */

.box {
width: 500px;
height: 500px;
display: flex;
}

.child {
margin: auto 0;
}

/* 方案3 */

.box {
width: 500px;
height: 500px;
line-height: 500px;
}

.child {
vertical-align: middle;
line-height: normal;
display: inline-block;
}

/* 方案4 */

.box {
width: 500px;
height: 500px;
display: table;
}

.child {
display: table-cell;
vertical-align: middle;
}

多行文本超出隐藏

1
2
3
4
5
.box {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}

三列布局

  • 绝对定位 1
1
2
3
4
5
<div class="box">
<div class="left"></div>
<div class="center"></div>
<div class="right"></div>
</div>
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
50
51
52
53
54
55
56
57
58
59
60
61
62
/* 绝对定位1 */

.left {
position: absolute;
width: 200px;
left: 0;
top: 0;
height: 100%;
background: goldenrod;
}

.right {
position: absolute;
width: 200px;
right: 0;
height: 100%;
background: thistle;
}

.center {
position: absolute;
right: 200px;
left: 200px;
height: 100%;
background: palegreen;
}

.box {
position: relative;
height: 100%;
}

/* 绝对定位2 */

.left {
position: absolute;
width: 200px;
left: 0;
top: 0;
height: 100%;
background: goldenrod;
}

.right {
position: absolute;
width: 200px;
right: 0;
top: 0;
height: 100%;
background: thistle;
}

.center {
margin: 0 200px;
height: 100%;
background: palegreen;
}

.box {
position: relative;
height: 100%;
}
  • 浮动 + 负 margin
1
2
3
4
5
6
7
<div class="box">
<div class="wrapper">
<div class="center"></div>
</div>
<div class="left"></div>
<div class="right"></div>
</div>
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
.left {
float: left;
width: 200px;
margin-left: -100%;
height: 100%;
background: goldenrod;
}

.center {
margin: 0 200px;
height: 100%;
background: palegreen;
}

.wrapper {
float: left;
height: 100%;
width: 100%;
}

.right {
float: left;
width: 200px;
height: 100%;
margin-left: -200px;
background: thistle;
}

.box {
position: relative;
height: 100%;
}
1
2
3
4
5
6
7
8
<!-- 推荐,元素与视觉保持一致 -->
<div class="box">
<div class="left"></div>
<div class="wrapper">
<div class="center"></div>
</div>
<div class="right"></div>
</div>
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
.left {
float: left;
width: 200px;
margin-left: -100%;
height: 100%;
background: goldenrod;
}

.center {
margin: 0 200px;
height: 100%;
background: palegreen;
}

.wrapper {
float: left;
height: 100%;
width: 100%;
}

.right {
float: left;
width: 200px;
height: 100%;
margin-left: -200px;
background: thistle;
}

.box {
position: relative;
height: 100%;
}
  • flex
1
2
3
4
5
<div class="box">
<div class="left"></div>
<div class="center"></div>
<div class="right"></div>
</div>
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
.left {
width: 200px;
height: 100%;
background: goldenrod;
}

.center {
flex: 1;
height: 100%;
background: palegreen;
}

.wrapper {
float: left;
height: 100%;
width: 100%;
}

.right {
width: 200px;
height: 100%;
background: thistle;
}

.box {
display: flex;
height: 100%;
}

尺寸百分比

  • 无定位元素: 相对于外层元素的内容(容纳)区域
  • 定位元素: 相对于最近的定位元素的 padding 区域
属性 百分比相对与
width 参考元素宽度
height 参考元素高度,需要指定参考元素高度
padding 参考元素宽度
border 参考元素宽度
margin 参考元素宽度

最大/最小 尺寸

可用于防止内部元素,超出容器尺寸

1
2
3
4
5
6
.box {
width: 300px;
}
img {
max-width: 100%;
}

表单

  • form 可以原生支持回车键提交表单, 会自动查找 form 中第一个有 type='submit' 的按钮, 并触发这个按钮的点击事件

  • 放在 label 中的 input 元素,即使没有使用 for 关联在一起,点击 label 中的任意元素,也会选中 input

    1
    2
    3
    4
    5
    <label>
    <input type="radio" />
    <p>name</p>
    ></label
    >

精灵图

spritesmith 用于合并图片,并生成生成样式

属性值计算过程

每一个元素的每一个属性都必须有值,属性值变为最终计算样式的过程就是属性值计算过程。

  • 确定声明值: 样式表中没有冲突的声明,作为最终样式

  • 层叠冲突: 对有冲突的声明使用层叠规则,确定 css 属性值

    • 比较重要性,作者样式表会覆盖浏览器默认样式
    • 比较特殊性,比较权重
    • 比较源次序,权重相同时,后写的样式覆盖先写的样式
  • 使用继承,对仍然没有值的属性,若可以继承,则继承父元素的值

  • 使用默认值,对仍然没有值的属性,使用默认值

  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信