0%

深克隆

  • 对于数组和对象做不同的处理
  • 为了解决循环引用的问题,缓存已经访问过的引用类型,如果已经存在,使用缓存值
  • Object.keys() 遍历的是对象自身的属性
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
function deepCopy(obj, cache = []) {
if (obj === null || typeof obj !== "object") {
return obj;
}
const hit = cache.filter((c) => c.original === obj)[0];
if (hit) {
return hit.copy;
}
const copy = Array.isArray(obj) ? [] : {};
cache.push({
original: obj,
copy,
});
Object.keys(obj).forEach((key) => {
copy[key] = deepCopy(obj[key], cache);
});
return copy;
}

let a = {
a: 10,
};
a.self = a;

let b = deepCopy(a);
console.log(b);

bind

  • 异常处理 如果调用者不是函数,抛出异常
  • 如果是构造调用,则返回构造调用的结果
  • 否则,使用 apply 改变函数绑定的 this
1
2
3
4
5
6
7
8
9
10
11
12
13
Function.prototype.mybind = function (context) {
if (typeof this !== "function") {
throw new Error("error");
}
let _this = this;
let arg = [...arguments].slice(1);
return function F() {
if (this instanceof F) {
return new _this(...arg, ...arguments);
}
return _this.apply(context, arg.concat(...arguments));
};
};

call/apply

  • 异常处理 如果调用者不是函数,抛出异常
  • 在 context 上添加 fn 为当前函数,进行调用得到结果,之后需要删除 context 上的 fn
  • 返回结果
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
Function.prototype.mycall = function (context) {
if (typeof this !== "function") {
throw new TypeError("error");
}
context = context || window;
context.fn = this;
let result;
let args = [...arguments].slice(1);
result = context.fn(...args);
delete context.fn;
return result;
};

Function.prototype.myapply = function (context) {
if (typeof this !== "function") {
throw new TypeError("error");
}
context = context || window;
context.fn = this;
let result;

if (arguments[1]) {
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
delete context.fn;
return result;
};

console.log(Array.prototype.concat.mycall([], [1], [2]));
console.log(Array.prototype.concat.myapply([], [[1], [2]]));

防抖函数

  • 引入 immediate ,immediate 为 true,需要在 timer = null 时执行函数,而 immediate 为 false 时,需要在延时回调函数中执行函数,因此还需要在闭包中保存 context 和 传参信息
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
function debounce(fn, wait = 50, immediate = true) {
let timer, context, args;
const later = () =>
setTimeout(() => {
timer = null;
if (!immediate) {
fn.apply(context, args);
context = args = null;
}
}, wait);

return function (...params) {
if (!timer) {
timer = later();
if (immediate) {
fn.apply(this, params);
} else {
context = this;
args = params;
}
} else {
clearTimeout(timer);
timer = later();
}
};
}

const dfn = debounce((arg) => console.log(arg), 500, true);

dfn(1);
dfn(2);
dfn(3);

setTimeout(() => {
dfn("aa");
}, 1000);

节流函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function throttle(fn, gutter) {
let prev = null;

return function (...params) {
let now = Date.now();
if (now - prev > gutter) {
fn.apply(this, params);
prev = now;
}
};
}

const tfn = throttle((arg) => console.log(arg), 5000);

setInterval(() => {
tfn("a");
}, 10);

数组扁平化

  • 简单的递归过程
1
2
3
4
const flattenDeep = (arr) =>
Array.isArray(arr)
? arr.reduce((prev, cur) => [...prev, ...flattenDeep(cur)], [])
: [arr];

实现 new

  • 创建一个对象,其 __proto__ 指向构造函数的原型
  • 调用该函数,函数的 this 绑定上一步创建的对象
  • 只要函数的返回值不为对象,返回之前创建的对象,否则返回函数返回的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function A() {
this.a = 10;
}

function New(func) {
let res = {};
if (func.prototype !== null) {
res.__proto__ = func.prototype;
}
let ret = func.apply(res, [...arguments].slice(1));
if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
return ret;
}
return res;
}

console.log(New(A));

instanceOf

  • 沿着原型链循环判断
1
2
3
4
5
6
7
8
9
10
11
12
13
function instanceOf(left, right) {
let proto = left.__proto__;
let prototype = right.prototype;
while (true) {
if (proto === null) {
return false;
}
if (proto === prototype) {
return true;
}
proto = proto.__proto__;
}
}

柯里化

柯里化函数接收到足够参数后,就会执行原函数,那么我们如何去确定何时达到足够的参数呢?

  • 柯里化函数需要记住你已经给过他的参数,如果没给的话,则默认为一个空数组。
  • 接下来每次调用的时候,需要检查参数是否给够,如果够了,则执行 fn,没有的话则返回一个新的 curry 函数,将现有的参数塞给他。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let sum = (a, b, c, d) => a + b + c + d;

let curry = (fn, ...arr) => {
let len = fn.length;
return (...args) => {
const combineArgs = [...arr, ...args];
if (len <= combineArgs.length) {
return fn(...combineArgs);
} else {
return curry(fn, ...combineArgs);
}
};
};

let cfn = curry(sum);

console.log(cfn(1)(2)(3)(4));

eventbus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class EventBus {
all = new Map();
on(type, handler) {
const handlers = this.all.get(type);
const added = handlers && handlers.push(handler);
if (!added) {
this.all.set(type, [handler]);
}
}
off(type, handler) {
const handlers = this.all.get(type);
if (handlers) {
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
}
}

emit(type, evt) {
(this.all.get(type) || []).slice().map((handler) => {
handler(evt);
});
}
}

当我们在项目中需要多处使用开源组件,且开源组件的默认配置行为不满足需求,需要我们手动配置大量选项时,采用二次封装方式应用一些默认选项是一种比较优雅的方式。

二次封装主要是要理解 $props,$attrs,$listeners,inheritAttrs 这几个 api。在二次封装时,我们希望原有 UI 组件的配置项在封装的组件上同样有效,我们只是对一些配置项进行了默认的配置,或者进行一些功能的增强,那问题的关键就是如何实现 prop 和 listeners 的透传。对于 listeners 的透传 我们可以使用 v-on="$listeners"。对于 prop 的透传,一个可行的方式是将被封装组件的 prop 在,然后通过 v-bind="$props" 的方式透传 prop,但是这种方式要求我们必须定义与被封装组件相同的 prop,繁琐不说,组件更新增加 prop,我们也要同步添加。

以下是官方文档对于 inheritAttrs 的解释

默认情况下父作用域的不被认作 props 的 attribute 绑定 (attribute bindings) 将会“回退”且作为普通的 HTML attribute 应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置 inheritAttrs 到 false,这些默认行为将会被去掉。

所有不被认作 props 的 attribute 可以通过 $attrs 访问。通过 $attrsinheritAttrs:false 是一种更好地方式。

对于对部分选项需要设置默认值得需求,vue2.x 中对于 v-bind 绑定的属性和单独设置的属性处理方式是默认单独设置的属性会覆盖 v-bind 绑定的属性。如果需要实现可以手动设置覆盖二次封装默认配置的需求,可以采用下面的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<componentsA v-bind="propsWithDefault" />
</template>
<script>
export default {
.......
computed:{
propsWithDefault(){
return {
defaultOption:'defaultValue'
...this.$attrs
}
}
}
}
</script>

如果采用 jsx + render 函数的方式,则可以利用 js 的完全编程能力。
可以参考我下面封装的上传组件,在上传类型是图片时,对 accept 设置了默认值。

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
<script>
import { message, Upload, Button, Icon, Modal } from "ant-design-vue";

function getBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
}

/**
* 基于 a-upload 二次封装的文件上传组件。
* @displayName SeekerUpload
*/
export default {
inheritAttrs: false,
name: "SeekerUpload",
components: {
"a-upload": Upload,
"a-button": Button,
"a-icon": Icon,
"a-modal": Modal,
},
props: {
/**
* 上传文件类型
* @values file image
*/
uploadType: {
type: String,
default: "file",
validator: (x) => ["file", "image"].includes(x),
},
/** 上传文件数量限制 */
number: {
type: Number,
default: 1,
},
/** 文件上传按钮说明文字 */
btnText: {
type: String,
default: "选择文件",
},
/** 图片上传说明文字 */
tipText: {
type: String,
default: "选择图片",
},
alwaysShowBtn: {
type: Boolean,
default: false,
},
},
computed: {
listType() {
if (this.uploadType === "image") {
return "picture-card";
}
return "text";
},
},
data() {
return {
previewImage: "",
previewVisible: false,
};
},
methods: {
beforeUpload() {
// 使用自定义上传方法
if (typeof this.$attrs.customRequest === "function") {
return true;
}
return false;
},
handleCancel() {
this.previewVisible = false;
},
async handlePreview(file) {
if (this.uploadType === "file") {
return;
}
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj);
}
this.previewImage = file.url || file.preview;
this.previewVisible = true;
},
handleChange({ fileList }) {
const files = fileList.slice(-this.number);
// 对于上传图片 限制图片大小在 800KB 以内
if (this.uploadType === "image" && files.length) {
const limit = 800 * 1024;
// 判断最后添加的图片
const latestAdd = files[files.length - 1];
if (latestAdd.size > limit) {
message.error("图片大小超过限制,最大不得超过800KB!");
files.pop();
}
}
this.$emit("update:fileList", files);
this.$emit("listChange", files);
},
},
render() {
const {
listType,
uploadType,
$attrs,
$listeners,
handleCancel,
previewVisible,
previewImage,
handlePreview,
handleChange,
tipText,
btnText,
beforeUpload,
number,
alwaysShowBtn,
} = this;
const bind = {
props: {
accept: uploadType === "image" ? "image/*" : undefined,
...$attrs,
},
on: {
preview: handlePreview,
change: handleChange,
...$listeners,
},
};

const uploadControl =
uploadType === "image" ? (
<div>
<a-icon type="plus" />
<div class="ant-upload-text">{tipText}</div>
</div>
) : (
<a-button disabled={$attrs.disabled}>
<a-icon type="upload" />
{btnText}
</a-button>
);

return (
<div>
<a-upload list-type={listType} beforeUpload={beforeUpload} {...bind}>
{($attrs.fileList && $attrs.fileList.length < number) || alwaysShowBtn
? uploadControl
: null}
</a-upload>
<a-modal
visible={previewVisible}
footer={null}
width={600}
{...{ on: { cancel: handleCancel } }}
>
<img alt="example" style="width: 100%" src={previewImage} />
</a-modal>
</div>
);
},
};
</script>

本篇笔记是 计算机网络-自顶而下分析 第六版 第三章的学习笔记。

基本概念

运输层协议是运行在端系统而不是在路由器中实现的。在发送端,运输层将从发送应用程序进程接收到的报文转换成运输层分组,用因特网术语来讲该分组称为运输层报文段 segment。实现的方法(可能)是将应用报文划分为较小的块,并为每块
加上一个运输层首部以生成运输层报文段 然后,在发送端系统中,运输层将这些报文段传递给网络层,网路层将其封装成网络层分组(即数据报)并向目的地发送。:网络路由器仅作用于该数据报的网络层字段;即它们不检查封装在该数据报的运输层报文段的字段 在接收端,网络层从数据报中提取运输层报文段,并将该报文段向上交给运输层 运输层则处理接收到的报文段,使该报文段中的数据为接收应用进程使用。

与网络层的关系

网络层提供了主机之间的逻辑通信,而运输层为运行在不同主机上的进程之间提供了逻辑通信。然而,即使底层网络协议不能在网络层提供相应的服务,运输层协议也能提供某些服务。即使底层网络协议是不可靠的,也就是说网络层协议会使分组丢失、篡改和冗余,运输协议也能为应用程序提供可靠的数据传输服务。

概述运输层服务

因特网网络层协议有一个名字叫 IP ,即网际协议 IP 主机之间提供了逻辑通信 IP 的服务模型是尽力而为交付服务( besl- e[[ort delivery service)。这意味着 IP 尽它”最大的努力”在通信的主机之间交忖报文段,但它并不做任何确保
特别是,它不确保报文段的交付 不保证报文段的按序交付,不保证报文段数据的完整。由于这些原因, IP 被称为不可靠服务 (unreliable service) 在此还要指出的是,每台主机至少有一个网络层地址,即所谓的 IP 地址。

UDP TCP 最基本的责任是,将两个端系统间 的交付服务扩展为运行在端系统上的两个进程之间的交付服务 将主机间交付扩展到进程间交付被称为运输层的多路复用( transport -layer multiplexing) 与多路分解( demultiplexing) 。

与 IP 一样, UDP 也是一种不可靠的服务,即不能保证一个进程所发送的数据能够完整无缺地(或全部! )到达目的进程。
TCP 为应用程序提供了几种附加服务 首先,它提供可靠数据传输( reliable dala tTansfer) 通过使用流量控制、序号、确认和定时器(本章将详细介绍这些技术), TCP 确保正确地、按序地将数据从发送进程交付给接收进程。
TCP 还提供拥塞控制( congestion conlrol) 。TCP 力求为每个通过一条拥塞网络链路的连接平等地共享网络链路带宽 这可以通过调节 TCP 连接的发送端发送进网络的流量速率来做到 在另一方面, UDP 流量是不可调节的 使用 UDP 传输的应用程序可以根据其需要以其愿意的任何速率发送数据。

多路复用和多路分解

如何实现

多路复用的要求是

  • 套接宇有唯一标识符
  • 每个报文段有特殊字段来指示该报文段所要交付到的套接字

这些特殊字段是源端口号字段 (source port oumber field) 和目的端口号字段 (destination port nurnber field)。端口号’是一个 16 比特的数,其大小在 0-65535。
0-1023 范围的端口号称为周知端口号( well- known porl number) ,是受限制的,这是指它们保留给诸如 HTTP (它使用端口号 80) 和盯’p (它使用端口号 1)之类的周知应用层协议来使用。

假设主机 A B 进行 UDP 通信,主机 能够运行多个进程,每个进程有自己的 UDP 套接字及相应的端口号。当从网络到达 UDP 报文段时,主机 通过检查该报文段中的目的端口号,将每个报文段定向(分解)到相应的套接字。

一个 UDP 套接字是由一个二元组来全面标识的,该二元组包含一个目的 地址和一个目的端口号。

服务器主机可以支持很多并行的 TCP 套接字,每个套接字与一个进程相联系,并由其四元组(源地址,源端口,目的地址,目的端口)来标识每个套接字。

无连接服务 UDP

报文结构

UDP 首部只有 4 个字段(源端口号,目的端口号,长度,检验和),每个字段由两个字节组成。长度字 32 比特段指示了在 UDP 报文段中的字节数(首部加数据) 。因为数据字段在一个 UDP 段中不同于另一个段中,需要一个明确的长度。接收方使用检验和来检查该报文段中是否出现了差错。

UDP 检验和

发送方的 UDP 对报文段中的所有 16 比特字的和进行反码运算,求和时遇到的任何溢出都被回卷。得到的结果被放在 UDP 报文段中的检验和字段。在接收方,如果没有差错,全部的数据段 16 比特字加上检验和的和将是 1111111111111111。

为什么 UDP 提供了检验和。许多链路层协议,包括流行的以太计算机网协议,也提供了差错检测,其原因是不能保证源和目的之间的所有链路都提供插座检测。这就是说,也许这些链路中的一条可能使用没有差错检测的协议。此外,即使报文段经链路正确地传输,当报文段存储在某台路由器的内存中时,也可能引入比特差错。在既无法确保连链路的可靠性,又无法确保内存中的差错检测的情况下,如果端到端数据传输服务要提供差错检测, UDP 就必须在端到端基础上在运输层提供差错检测。这是 个在系统设计巾被称颂的端到端原则( end - end principle)。

因为假定 IP 是可以运行在任何第二层协议之上的,运输层提供差错检测作为 种保险措施是非常有用的 虽然 UDP 提供差错检测,但它对差错恢复无能为力。 UDP 的某种实现只是丢弃受损的报文段; 其他实现是将受损的报文段交给应用程序并给出警告。

可靠数据传输原理

本节一步步研究一系列协议,逐步构建一个无错、可靠的数据传输协议。

经完全可靠信道的可靠传输 rdt 1.0

首先考虑最简单的情况,即底层信道是完全可靠的。
rdt1.0

在这个简单的协议中,一个单元数据与一个分组没差别。,所有计算机#### 经具有比特差错信道的可靠数据传输 rdt 2.0

底层信道更为实际的模型是分组中的比特可能受损 在分组的传输、传播或缓存的过程中,这种比特差错通常会出现在网络的物理部件中 我们眼下还将继续假定所有发送的分组(虽然有些比特可能受损)将按其发送的顺序被接收。

协议使用了肯定确认否定确认,这种形式使得接收方可以让发送方知道那些内容被正确接收,哪些内容接收有误并因此需要重复。在计算机网络环境中,基于这种重传机制的可靠传输协议被称为 自动重传请求(Automatic Repeat reQuest,ARQ)协议

ARQ 协议还需要另外三种协议功能来处理存在比特差错的情况。

  • 差错检测 需要一种机制以使接收方检测到何时出现了比特差错
  • 接收方反馈 因为发送方和接收方通常在不同端系统上执行,可能相隔数千英里,发送方要了解接收方情况(此时为分组是再被正确接收)的唯一途径就是让接收方提供明确的反馈信息给发送方
  • 重传 接收方收到有差错的分组时,发送方将重传该分组文

rdt2.0

目前该协议有一个致命缺陷,没有考虑到 ACK 或者 NAK 分组受损的可能性。

可以在发送方收到受损的 ACK 或 NAK 分组时,重传当前数据分组,但是这种方法引入了 冗余分组,其根本困难在于接收方不知道它上次所发送的 ACK 或者 NAK 是否被发送方正确地收到,因此它无法事先知道接收到的分组是新的还是一次重传。

解决这个问题的一个简单方法,是在数据分组增加一个新字段,让发送方对其数据分组编号,即将发送数据的分组的序号放在该字段,接收方只需要检查序号即可确定收到的分组是否一次重传。对于停等协议,1 比特序号就足够了,因为它可以让接收方知道发送方是否正在重传前一个发送分组(接收到的分组序号与最近收到的分组序号相同),或是一个新分组(序号变化了)。

具有比特差错的丢包信道的可靠数据传输 rdt 3.0

问题是怎么检测丢包以及发生丢包后该做什么。为了解决检测丢包的问题,还需要增加新的协议机制。

假定发送方传输一个数据分组,该分组或者接收方对该分组的 ACK 发生了丢失,如果发送方愿意等待足够长的时间以便确定分组已丢失,则它只需要重传该数据分组即可。这个时间至少应当是发送方和接受方之间的一个往返时延,加上接收方处理一个分组所需要的时间。

为了实现基于时间的重传机制,需要一个倒计数定时器。发送方需要做到:1. 每次发送一个分组时,启动一个定时器 2. 响应定时器中断 3. 终止定时器

流水线可靠数据传输协议

上述 rdt 3.0 协议的问题在于它是一个停等协议,对于信道的利用率太低。如果每次可以发送多个分组,那么需要增加以下机制:

  • 必须增加序号范围,每个输送中的分组必须有一个唯一的序号,而且也许有多个输送中未确认的报文
  • 协议的发送方和接收方也许必须缓存多个分组
  • 所需序号范围和对缓冲的要求取决于数据传输协议如何处理丢失、损坏和延时过大的分组。解决流水线的差错恢复有两种基本方法:回退 N 步选择重传

回退 N 步

那些已被发送但还未被确认的分组的许可序号范围可以被看成是一个在序号范围内长度为 的窗口 随着协议的运行,该窗口在序号空间向前滑动。因此 N 常被称为窗口长度 (window size) , GBN 协议也常被称为滑动窗口协议。
为什么要限制这些被发送的、未被确认的分组的数目为 N 呢?流量控制是对发送发施加限制的原因之一,拥塞控制则是另一个原因。

GBN

基本概念

网络层是实现主机到主机的通信服务的。每一台主机和路由器都有一个网络层部分。网络层协议是协议栈中最具挑战性的部分。

转发涉及分组在单一的路由器中从一条入链路到一条出链路的传送。路由选择涉及一个网络的所有路由器,他们经路由选择协议共同交互,以决定分组从源到目的地节点所采用的路径。

最近一周一直在研究在公司的 rancher k8s 环境下搭建前端错误监控平台 sentry。首先介绍一下 sentry,这是一个非常强大的错误收集框架,可以收集众多语言的运行错误日志,其在前端错误的监控上应用的比较多。配合上传 sourceMap可以快速准确定位到源代码错误位置。

为什么它来监控前端错误呢?我们的测试无法覆盖到应用的方方面面,很多线上错误,往往是用户反馈到了客服人员,然后技术人员才能知晓,同时由于前端运行环境复杂,问题可能很难复现,技术人员定位错误需要不少的时间,这段时间就可能造成不小的损失了,如果解决问题花费的时间很久,用户对于这家公司的技术力也会产生质疑。应用前端错误监控之后,当线上有高频错误发生时,可以立即通过钉钉或者邮件的方式通知到开发人员,开发人员通过错误日志信息可以快速定位到错误点,这带来的好处是不言而喻的。

sentry 提供 saas 的服务,同时也提供开源的私有化部署版本,提供相同的服务体验。官方维护的是 docker-compose 的一键式部署方式,但是公司的云服务器都在 k8s 的集群,因此我考虑采用的是基于 k8s 的安装方式。然而公司并没有专业的运维人员,想折腾,只能自己动手了。最为一个一周前对 k8s 还是一窍不通的小白,踩坑是避免不了的,写这篇文章就是记录下踩的那些坑。

错误方式 rancher ui 商店方式安装

一开始知道可以在 rancher 上安装 sentry,是在 v 站看到的一个回复,说是起个部署 sentry 会方便很多。然后知道了 rancher 的商店里可以找到 sentry,然后一键安装。这里需要开启 rancher 商店的 helm 官方仓库,结果商店死活刷不出来应用列表,后来才知道国内的云主机是无法访问 google 的 helm 仓库的,需要使用国内加速的仓库 http://mirror.azure.cn/kubernetes/charts。

换了加速地址后,可以顺利使用 rancher 商店启动应用了,工作负载一堆爆红,web,db和redis都没起来,这是因为没有设置默认存储类,导致 pvc 没有 绑定到 pv。刚一开始我可不知道 pv 和 pvc 的概念,更不用说默认存储类,总之是又百度又google又看官方文档的,算是稍稍搞明白了这块。这个在安装要求里已经说明了,k8s集群应该有一个默认存储类,用来动态分配pv。rancher ui 里像下图一样。

默认存储类

配置好了这个,按理说通过 rancher ui 的商店应该可以正常安装了吧,然而答案是并不能。这时候所有组件都能正常创建,除了一个 pending 的 loadbalance Service,这个我一开始没管它,自己又创建了一个 Ingress 配置,总算是可以访问 sentry 的 web 页面了。但是我按照文档的方法,输入用户名和密码,一直提示登录失败。我重装了几次,一直如此,折腾了几个小时,才发现了原因。

rancher ui 也是通过 helm 工具安装 chart 的,helm 默认的等待事件是 5分钟,超过了就认为安装失败了。而 sentry 的安装是包含了两个 job 的,db_init 和 user_create。db_init 用时很长,已经超过了5分钟,helm 认为安装失败了,就不继续调用 user_create 的 job了,导致初始用户根本没有被初始化进去。

正确方式 helm 方式安装

rancher ui中并没有方式去设置 helm 的等待时间,因此只能采用 helm 的方式进行安装了。这里感谢这位仁兄的文章 Sentry实时应用错误跟踪系统在Kubernetes中私有化部署,给予了我很大的帮助。

先简单介绍一下 helm,helm 是一个 k8s 的应用管理工具。helm 的 chart 我理解和 docker-compose 的 compose.yaml 类似,但是比后者更强大,通过预先配置的一组文件,来一键式部署k8s应用。helm现在有helm2和helm3两个版本,其中helm2需要在k8s中安装 helm2 服务端 Tiller,麻烦不说,还有潜在的后门隐患。helm3取消了这一步,因此这里使用的是helm3。

这里还踩了一个坑,一开始 helm 方式,连 db_init 的 job 都不运行。检查了以下各个组件,只有名为 sentry 的 service 处于 pending状态。它的 serviceType 是 loadbalance。尝试删掉它,helm 直接返回错误,我感觉是helm要等待所有组件就绪,才会调度job。我看了下文档,service 主要是负责外网连接功能的,它有两种类型 loadbalance 和 nodeport,loadbalance 只有使用 gce(谷歌的云服务) 的服务才能正常工作。后来把他改成 nodeport 果然正常了。db_init 后,user_create 也正常调度了。
最后摸索出来的命令是这样的。

1
2
helm --kubeconfig  D:\bins\kubeconfig install sentry stable/sentry -n sentry --set persistenc
e.enabled=true --set user.email=你的用户名 --set user.password=你的密码 --set email.host=你的邮箱host --set email.port=25 --set email.user=你的邮箱服务用户 --set email.password=你的邮箱服务密码 --set service.type=NodePort --set email.use_tls=false --wait --timeout=800s

稍微等待个10分钟作用, sentry服务就部署完成了,确实是很简单。。。

明天研究一下将 sentry 引入项目,然后在下篇博客再总结一下 sentry 的前端监控应用。

这几天发现多端上存在一些重合的业务逻辑代码,在每个项目中都写一遍逻辑是很没有效率的事情,也不利于 bug 的管控与治理。一般在 vue 项目中,同一个项目中的重复代码可以通过封装组件,封装 mixin,抽离 utils 函数的方式来复用,对于分散在多个项目中的重复代码,可以通过制作为 npm 包的方式来实现复用。

公司的前端基础设施还是非常不完善的,并没有自己的 npm 私仓。首先要把 npm 私仓建立起来,经过一番查找资料最终决定采用 verdaccio 进行搭建。

npm 私仓搭建

verdaccio 是一个轻量级的 npm 私有仓库软件。它的搭建过程非常简便,无需数据库,唯一的要求就是需要有 node 环境。

windows 搭建私服 verdaccio(内外网安装)
使用 verdaccio 搭建 npm 私有库 pm2 守护进程
搭建主要过程主要参考了这两篇博文,唯二需要注意的是 配置文件末尾应该加上 listen:0.0.0.0:4873 还有 使用 pm2 启动的时候,应该填写 verdaccio 的绝对路径

1
2
3
pm2 start C:\Users\Administrator\AppData\Roaming\npm\node_modules\verdaccio\bin\verdaccio
# 如果是linux,就用下面这个命令就好了
pm2 start `which verdaccio`

使用私仓

nrm 方式

1
2
3
4
5
6
安装nrm
npm install -g nrm
添加仓库
nrm add seeker http://{地址}:4873/
使用仓库
nrm use seeker

npmrc

直接添加
registry=http://{地址}:4873/

npm 包制作发布流程

要制作一个 npm 包,包含代码编写,本地测试,文件打包等关键点
首先需要搭建的是打包的工作流,这里采用的是 rollup + typescript 搭建的。

为什么要用 typescript 编写包呢?如果你是用的 vscode 写代码,一定会觉得 vscode 的代码提示非常好用,当你引用一个包时,包函数的使用方法都以 ts 函数定义的方式显示出来,这个是靠 typescript 的 .d.ts 文件做到的。而用 typescript 编写类库,就可以使用 tsc 自动生成 .d.ts 文件,让我们自己编写的库也能获得 vscode 代码提示的加持。这样来编写库,对库使用人员是非常友好的。

rollup工作流搭建

rollup 使用非常简单。安装rollup后,只需要编写一个简单的配置文件,就可以工作了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import typescript from "@rollup/plugin-typescript";
import pkg from "./package.json";

export default {
input: "src/index.ts",
output: [
{
// 出口文件
file: pkg.main,
format: "umd",
name: "seeker-fe-utils",
},
{
// 出口文件
file: pkg.module,
format: "es",
name: "seeker-fe-utils",
},
],
plugins: [typescript()],
};

我这里引用了 rollup 的 typescript 插件,可以看到我输出了两种模块规范的目标文件。注意这里我直接引入了 package.json,以其中的 main 字段和 module 字段直接作为输出路径,保证了入口文件路径的准确性,可以算是一个小技巧。

目前我使用的 @rollup/plugin-typescript 插件版本是 6.0.0,这个版本有一个 bug,即输出多个文件与自动生成 .d.ts 的配置是冲突的。要生成 .d.ts需要在 tsconfig 文件中设置 outDir,然后就会导致很蛋疼的冲突问题。参考了 stackoverflow 上的解决方法是,不使用 tsconfig配置,而是通过命令行传参的方式单独生成 .d.ts 文件。把以下命令配置到 package.json 的 script 中。

1
2
3
4
5
"scripts": {
"gen-type": "npx tsc -d --emitDeclarationOnly --outDir types",
"build": "npx rollup -c",
"pub": "npx rollup -c && npm publish"
}

package.json 配置简要说明

如果要发布 npm 包,以下三个字段是必须的。
name - 包名
version - 版本号,每次发布的版本必须与之前的版本不同
main - 指定 cjs 模块入口文件的路径
module - 指定 esm 模块入口文件的路径,用来和webpack搭配只用
broswer - 指定 umd 模块入口文件的路径

版本迭代

npm 提供了 npm version 系列命令用来修改 package.json 中的版本号。
README 文件正常会在 npm 包发布的时候更新。

安装测试

在本地测试一般使用 npm link,具体方法是

在包项目根目录下使用npm link,这将会把包软连接到全局 npm 包中。
在项目目录下使用 npm link 包名,这将会把在全局包下的包软连接到项目的 node_modules 文件夹中。
在开发时可以指定 package.json 中的 main 为源文件的路径,这样就可以边修改包源码边测试了。

还可以使用 npm install {npm 包绝对路径} 在本地安装 npm 包测试

包发布

npm 同名包以版本区分,发布新的版本,旧的版本依旧存在
npm publish 发布该版本的包
npm unpublish 从 registry 中删除该版本的包

typescript 深入理解装饰器

这篇文章总结的很好

装饰器使用一种特殊的 @expression 声明使用,它必须被解析为一个函数,以在运行时调用

装饰器工厂

如果你想要更灵活的使用装饰器,可以使用装饰器工厂方式,装饰器工厂返回一个装饰器函数,以在运行时被调用。使用装饰器工厂意味着你可以向装饰器工厂函数传入参数,以生成不同行为的装饰器函数

1
2
3
4
5
6
7
function color(value: string) {
// this is the decorator factory
return function (target) {
// this is the decorator
// do something with 'target' and 'value'...
};
}

多个装饰器

装饰器调用顺序
实例属性的方法参数装饰器,方法装饰器,存取装饰器,属性装饰器
静态属性的方法参数装饰器,方法装饰器,存取装饰器,属性装饰器
构造函数的参数装饰器
类装饰器


  1. Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each instance member.
  2. Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each static member.
  3. Parameter Decorators are applied for the constructor.
  4. Class Decorators are applied for the class.

定义(工厂方法):相同装饰器按从上到下方式执行工厂方法返回装饰器函数前的语句
执行:相同装饰器在运行时按照从下到上的顺序执行

  1. The expressions for each decorator are evaluated top-to-bottom.
  2. The results are then called as functions from bottom-to-top.

PropertyDescriptor

允许对一个属性的描述进行检索,在 js 中属性由一个字符串类型的名字和一个属性描述符构成

  1. value 该属性的值
  2. writable 当且仅当属性的值可以被改变时为 true。(仅针对数据属性描述有效)
  3. get 获取该属性的访问器函数(getter)。如果没有访问器, 该值为 undefined。(仅针对包含访问器或设置器的属性描述有效)
  4. set 获取该属性的设置器函数(setter)。 如果没有设置器, 该值为 undefined。(仅针对包含访问器或设置器的属性描述有效)
  5. configurable 当且仅当指定对象的属性描述可以被改变或者属性可被删除时,为 true。
  6. enumerable 当且仅当指定对象的属性可以被枚举出时,为 true。

类装饰器

被应用于类的构造函数并且可以被用来观察,修改或者替换类的定义。类修饰器不能被用在声明文件中。

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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
/**

- 属性装饰器
- @param targetPrototype 构造函数的原型
- @param propName 属性名称
*/
function propDecorator1(targetPrototype: any, propName: string) {
console.log("属性装饰器 1:", targetPrototype, propName);
}

function propDecorator2(params: string) {
console.log("属性装饰器 2 before:", params);
return function (targetPrototype: any, propName: string) {
// targetPrototype[propName]是在原型链上查找实例属性,永远为 undefined
console.log(
"属性装饰器 2:",
targetPrototype,
propName,
targetPrototype[propName]
);
targetPrototype[propName] = "会在原型上面添加该属性并赋值,不是实例对象";
};
}

function propDecorator3(params: string) {
console.log("属性装饰器 3 before:", params);
return function (targetPrototype: any, propName: string) {
console.log("属性装饰器 3:", targetPrototype, propName);
};
}

/**

- 方法装饰器
- @param targetPrototype 构造函数的原型
- @param methodName 方法名称
- @param descr 方法的描述
*/
function methodDecorator1(
targetPrototype: any,
methodName: string,
descriptor: PropertyDescriptor
) {
console.log("方法装饰器 1:", targetPrototype, methodName, descriptor);
}

let methodDecorator2: any = function (params: string) {
console.log("方法装饰器 2 before:", params);
return function (
targetPrototype: any,
methodName: string,
descriptor: PropertyDescriptor
) {
console.log("方法装饰器 2:", targetPrototype, methodName, descriptor);
};
};

let methodDecorator3: any = function (params: string) {
console.log("方法装饰器 3 before:", params);
return function (
targetPrototype: any,
methodName: string,
descriptor: PropertyDescriptor
) {
console.log("方法装饰器 3:", targetPrototype, methodName, descriptor);
let originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
args = args.map((item) => (item += " test"));
return originalMethod.apply(this, args);
};
return descriptor;
};
};

/**

- 方法参数装饰器
- @param targetPrototype 构造函数的原型
- @param methodName 方法名称
- @param paramIndex 参数在 arguments 中的下标
*/
function paramDecorator1(
targetPrototype: any,
methodName: string,
paramIndex: number
) {
console.log("方法参数装饰器 1:", targetPrototype, methodName, paramIndex);
}

function paramDecorator2(params: string) {
console.log("方法参数装饰器 2 before:", params);
return function (
targetPrototype: any,
methodName: string,
paramIndex: number
) {
console.log("方法参数装饰器 2:", targetPrototype, methodName, paramIndex);
};
}

function paramDecorator3(params: string) {
console.log("方法参数装饰器 3 before:", params);
return function (
targetPrototype: any,
methodName: string,
paramIndex: number
) {
console.log("方法参数装饰器 3:", targetPrototype, methodName, paramIndex);
};
}

/**

- 静态属性修饰器
- @param targetPrototype
- @param propName
*/
function staticPropDecorator(param: string) {
console.log("静态属性修饰器 before:", param);
return function (targetPrototype: any, propName: string) {
console.log(
"静态属性修饰器:",
targetPrototype,
propName,
targetPrototype[propName]
);
targetPrototype[propName] = "静态属性初始值被修改了!";
};
}

/**

- 静态方法修饰器
- @param targetPrototype
- @param methodName
- @param descriptor
*/
function staticMethodDecorator(param: string) {
console.log("静态方法修饰器 before:", param);
return function (
targetPrototype: any,
methodName: string,
descriptor: PropertyDescriptor
) {
console.log("静态方法修饰器:", targetPrototype, methodName, descriptor);
};
}

/**

- 类装饰器
- @param targetClass
*/
function classDecorator1(constructor: Function) {
console.log("类装饰器 1:", constructor);
}

function classDecorator2(params: string) {
console.log("类装饰器 2 before:", params);
return function (constructor: Function) {
console.log("类装饰器 2:", constructor);
};
}

function classDecorator3(params: string) {
console.log("类装饰器 3 before:", params);
return function (constructor: Function) {
console.log("类装饰器 3:", constructor);
};
}

/**

- 存取器装饰器
- @param target
- @param propertyKey
- @param PropertyDescriptor
*/
function accessorDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.get = () => "存取器修饰后的值"
}

@classDecorator1
@classDecorator2("params2")
@classDecorator3("params3")
class Test {
@propDecorator1
@propDecorator2("param2")
@propDecorator3("param3")
public msg: string = "属性初始值";

@staticPropDecorator("静态属性")
static title: string = "静态属性初始值";

constructor(msg: string) {
this.msg = msg;
}

@methodDecorator1
@methodDecorator2("param2")
@methodDecorator3("param3")
toString(
@paramDecorator1 str1: string,
@paramDecorator2("param2") str2: string,
@paramDecorator3("param3") str3: string
) {
console.log("toString:", str1, str2, str3);
}

@staticMethodDecorator("静态方法")
static staticToString() {
console.log(this.title);
}

@accessorDecorator
get msg1() {
return this.msg
}

}

let t: any = new Test("this is a msg.");
t.toString("ss", "dd", "ff"); //methodDecorator3 装饰器对该方法进行了重写
console.log(t.msg, t.proto, t.msg, t.msg1);
Test.staticToString();
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
属性装饰器2 before: param2
属性装饰器3 before: param3
属性装饰器3: Test { toString: [Function] } msg
属性装饰器2: Test { toString: [Function] } msg undefined
属性装饰器1: Test { toString: [Function], msg: '会在原型上面添加该属性并赋值,不是实例对象' } msg
方法装饰器2 before: param2
方法装饰器3 before: param3
方法参数装饰器2 before: param2
方法参数装饰器3 before: param3
方法参数装饰器3: Test { toString: [Function], msg: '会在原型上面添加该属性并赋值,不是实例对象' } toString 2
方法参数装饰器2: Test { toString: [Function], msg: '会在原型上面添加该属性并赋值,不是实例对象' } toString 1
方法参数装饰器1: Test { toString: [Function], msg: '会在原型上面添加该属性并赋值,不是实例对象' } toString 0
方法装饰器3: Test { toString: [Function], msg: '会在原型上面添加该属性并赋值,不是实例对象' } toString { value: [Function],
writable: true,
enumerable: true,
configurable: true }
方法装饰器2: Test { toString: [Function], msg: '会在原型上面添加该属性并赋值,不是实例对象' } toString { value: [Function],
writable: true,
enumerable: true,
configurable: true }
方法装饰器1: Test { toString: [Function], msg: '会在原型上面添加该属性并赋值,不是实例对象' } toString { value: [Function],
writable: true,
enumerable: true,
configurable: true }
静态属性修饰器 before: 静态属性
静态属性修饰器: function Test(msg) {
this.msg = "属性初始值";
this.msg = msg;
} title 静态属性初始值
静态方法修饰器 before: 静态方法
静态方法修饰器: function Test(msg) {
this.msg = "属性初始值";
this.msg = msg;
} staticToString { value: [Function],
writable: true,
enumerable: true,
configurable: true }
类装饰器2 before: params2
类装饰器3 before: params3
类装饰器3: function Test(msg) {
this.msg = "属性初始值";
this.msg = msg;
}
类装饰器2: function Test(msg) {
this.msg = "属性初始值";
this.msg = msg;
}
类装饰器1: function Test(msg) {
this.msg = "属性初始值";
this.msg = msg;
}
toString: ss test dd test ff test
this is a msg. undefined this is a msg. 存取器修饰后的值
静态属性初始值被修改了!

通配符路由

阅读源码发现, vue router 源码中对通配符路由做了特殊处理,保证其始终在匹配列表最后,非嵌套的路由必须以 slash 开头 (/)

一些限制

path 属性是必须的,component 必须是一个实际的组件

单页面多路由区域

单页面多路由区域 看了源码才知道有这种操作
router-view 可以设置 name,如果不设定会匹配到默认的

1
2
3
4
5
6
7
8
9
10
11
12
13
// 路由配置
{
path: '/Hi',
components: {
default:Hello,
left:Hi2,
right:Hi1
}
}
// 页面里这么写
<router-view></router-view>
<router-view name="left"></router-view>
<router-view name="right"></router-view>

跳转原理

  1. 首先通过 createRouteMap 创建路由映射,然后使用映射创建 matcher 函数
  2. 路由跳转的核心是 transitionTo 方法,该方法首先判定是不是相同的路由,相同的路由不进行跳转
    • path,hash,query 相同 path 相同隐含了 param 相同
    • 路由名,hash,query,param 相同

即将去一家 vue 技术栈的公司干活了,其实对于 vue2 的掌握,我还是有不少欠缺的,自己通读 vue2 的文档还是大一的时候,后面 vue2 的版本经历了多次更新,这些更新的内容我基本就没怎么看过了,这次就来查漏补缺一下,这份笔记只记录自己不熟悉的内容。

不同构建版本

这里有几个概念

  • 完整版,同时包含运行时和编译器的版本
  • 编译器,如果需要使用 template 选项,需要使用带编译器的完整版。如果你仍然希望使用完整版,则需要在打包工具里配置一个别名:
  • 运行时,除去编译器的一切
  • UMD,统一模块规范,提供了一个 AMD,CMD,ES Module 的兼容层
  • CMD,这是 nodejs 的模块化规范,配合老的打包工具如 Browserify 或 webpack 1。
  • ES Module 从 2.6 开始 Vue 会提供两个 ES Modules (ESM) 构建文件:
    • 为打包工具提供的 ESM:为诸如 webpack 2 或 Rollup 提供的现代打包工具。ESM 格式被设计为可以被静态分析,所以打包工具可以利用这一点来进行“tree-shaking”并将用不到的代码排除出最终的包。为这些打包工具提供的默认文件 (pkg.module) 是只有运行时的 ES Module 构建 (vue.runtime.esm.js)。
    • 为浏览器提供的 ESM (2.6+):用于在现代浏览器中通过 script type=”module” 直接导入。

CSP 环境

在某些环境,如 Google Chrome Apps,会强制应用内容安全策略 CSP,不能使用 new Function() 对表达式求值。这时可以用 CSP 兼容版本。运行时版本是完全兼容 CSP 的,当通过 webpack + vue-loader 或者 Browserify + vueify 构建时,模板将被预编译为 render 函数,可以在 CSP 环境中完美运行。

Vue 实例

Object.freeze

这里唯一的例外是使用 Object.freeze(),这会阻止修改现有的属性,也意味着响应系统无法再追踪变化。这里 Vue 3 直接提供了一个 api markRaw

v-once 可以执行一次性的插值,当数据改变时,插值处的内容不会更新。

模板中的全局变量

模板中的表达式都被放在沙盒中,只能访问全局变量的一个白名单

1
2
3
4
5
6
const allowedGlobals = makeMap(
"Infinity,undefined,NaN,isFinite,isNaN," +
"parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent," +
"Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl," +
"require" // for Webpack/Browserify
);

动态参数

动态参数,2.6.0 新增,可以使用 方括号括起来的 js 表达式作为一个指令的参数

1
2
<a v-bind:[attributeName]="url"> ... </a>
<a v-on:[eventName]="doSomething"> ... </a>

对动态参数值的约束

动态参数预期会求出一个字符串,异常情况下值为 null。这个特殊的 null 值可以被显性地用于移除绑定。任何其它非字符串类型的值都将会触发一个警告。

对动态参数表达式的约束

动态参数表达式有一些语法约束,因为某些字符,如空格和引号,放在 HTML attribute 名里是无效的。例如:

1
2
<!-- 这会触发一个编译警告 -->
<a v-bind:['foo' + bar]="value"> ... </a>

变通的办法是使用没有空格或引号的表达式,或用计算属性替代这种复杂表达式。

在 DOM 中使用模板时 (直接在一个 HTML 文件里撰写模板),还需要避免使用大写字符来命名键名,因为浏览器会把 attribute 名全部强制转为小写:

1
2
3
4
5
<!--
在 DOM 中使用模板时这段代码会被转换为 `v-bind:[someattr]`
除非在实例中有一个名为“someattr”的 property,否则代码不会工作。
-->
<a v-bind:[someAttr]="value"> ... </a>

计算属性和侦听器

计算属性

计算属性是基于它们的响应式依赖进行缓存的,只在相关响应式依赖发生改变时它们才会重新求值。这也同样意味着下面的计算属性将不再更新,因为 Date.now() 不是响应式依赖。

1
2
3
4
5
computed: {
now: function () {
return Date.now()
}
}

计算属性的 setter

计算属性默认只有 getter,不过在需要时你也可以提供一个 setter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
computed: {
fullName: {
// getter
get: function () {
return this.firstName + ' ' + this.lastName
},
// setter
set: function (newValue) {
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}

侦听器

当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

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
var watchExampleVM = new Vue({
el: "#watch-example",
data: {
question: "",
answer: "I cannot give you an answer until you ask a question!",
},
watch: {
// 如果 `question` 发生改变,这个函数就会运行
question: function (newQuestion, oldQuestion) {
this.answer = "Waiting for you to stop typing...";
this.debouncedGetAnswer();
},
},
created: function () {
// `_.debounce` 是一个通过 Lodash 限制操作频率的函数。
// 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
// AJAX 请求直到用户输入完毕才会发出。想要了解更多关于
// `_.debounce` 函数 (及其近亲 `_.throttle`) 的知识,
// 请参考:https://lodash.com/docs#debounce
this.debouncedGetAnswer = _.debounce(this.getAnswer, 500);
},
methods: {
getAnswer: function () {
if (this.question.indexOf("?") === -1) {
this.answer = "Questions usually contain a question mark. ;-)";
return;
}
this.answer = "Thinking...";
var vm = this;
axios
.get("https://yesno.wtf/api")
.then(function (response) {
vm.answer = _.capitalize(response.data.answer);
})
.catch(function (error) {
vm.answer = "Error! Could not reach the API. " + error;
});
},
},
});

class 与 style 绑定

当在一个自定义组件上使用 class 属性时,这些 class 将被添加到该组件的根元素上面。这个元素上已经存在的 class 不会被覆盖。

当 v-bind 使用需要添加浏览器引擎前缀的 CSS 属性值时,Vue.js 会自动侦测并添加响应的前缀。

多重值

从 2.3.0 起你可以为 style 绑定中的属性提供一个包含多个值的数组,常用于提供多个带前缀的值,例如:

1
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

条件渲染

v-if vs v-show

v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。

v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。

一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。

列表渲染

v-for 中使用对象

你也可以用 v-for 来遍历一个对象的属性。
你也可以提供第二个的参数为 property 名称 (也就是键名):
还可以用第三个参数作为索引:

1
2
3
<div v-for="(value, name, index) in object">
{{ index }}. {{ name }}: {{ value }}
</div>

在遍历对象时,会按 Object.keys() 的结果遍历,但是不能保证它的结果在不同的 JavaScript 引擎下都一致。

维护状态

当 Vue 正在更新使用 v-for 渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key 属性。建议尽可能在使用 v-for 时提供 key attribute,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。

不要使用对象或数组之类的非基本类型值作为 v-for 的 key。请用字符串或数值类型的值。

数组更新检测

Vue 将被侦听的数组变异方法进行了包裹,所以它们也会触发视图更新,这些方法包括:

1
2
3
4
5
6
7
push();
pop();
shift();
unshift();
splice();
sort();
reverse();

替换数组

filter()、concat() 和 slice()。它们不会改变原始数组,而总是返回一个新数组。当使用非变异方法时,可以用新数组替换旧数组。

1
2
3
example1.items = example1.items.filter(function (item) {
return item.message.match(/Foo/);
});

由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。

显示过滤/排序后的结果

有时,我们想要显示一个数组经过过滤或排序后的版本,而不实际改变或重置原始数据。在这种情况下,可以创建一个计算属性,来返回过滤或排序后的数组。
在计算属性不适用的情况下 (例如,在嵌套 v-for 循环中) 你可以使用一个方法:

v-for 里使用值范围

v-for 也可以接受整数。在这种情况下,它会把模板重复对应次数。

1
2
3
<div>
<span v-for="n in 10">{{ n }} </span>
</div>

template 上使用 v-for

类似于 v-if,你也可以利用带有 v-for 的 template 来循环渲染一段包含多个元素的内容。比如:

1
2
3
4
5
6
<ul>
<template v-for="item in items">
<li>{{ item.msg }}</li>
<li class="divider" role="presentation"></li>
</template>
</ul>

组件上使用 v-for

在 2.2.0+的版本里,在组件上使用 v-for 时,key 现在是必须的

事件处理

当需要访问原始 DOM 事件时,可以用特殊变量 $event 把它传入方法:

1
2
3
<button v-on:click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>

事件修饰符

1
2
3
4
5
6
.stop
.prevent
.capture
.self
.once 事件只会触发一次,可以被用到自定义的组建事件上
.passive 对应于 addEventListener 的 passive 属性,表明这个函数不能使用 preventDefault。如果和 .prevent一起使用,浏览器会给予警告

按键修饰符

vue 允许为 v-on 在监听键盘事件时添加修饰符。

直接将 KeyboardEvent.key 暴露的任意有效按键名转换为 kebab-case 来作为修饰符。

可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器。

1
2
3
4
.ctrl
.alt
.shift
.meta

.exact 修饰符允许你控制由精确的系统修饰符组合触发的事件。

1
2
3
4
5
6
7
8
<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button v-on:click.ctrl="onClick">A</button>

<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button v-on:click.ctrl.exact="onCtrlClick">A</button>

<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button v-on:click.exact="onClick">A</button>

鼠标按钮修饰符

1
2
3
.left
.right
.middle

表单输入绑定

v-model

可以用 v-model 指令在表单 input、textarea 及 select 元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。v-model 本质上是语法糖,他负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。

text 和 textarea 元素使用 value 属性和 input 事件;
checkbox 和 radio 使用 checked 属性和 change 事件;
select 字段将 value 作为 prop 并将 change 作为事件。

在 textarea 处使用插值语法不会生效。
单个复选框,绑定到布尔值,多个复选框,应绑定到数组。
单选按钮,选项应绑定到同一个值。

select 单选时,如果 v-model 表达式的初始值未能匹配任何选项,select 元素将被渲染为“未选中”状态。在 iOS 中,这会使用户无法选择第一个选项。因为这样的情况下,iOS 不会触发 change 事件。因此,更推荐像上面这样提供一个值为空的禁用选项。

1
2
3
4
5
6
<select v-model="selected">
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>

select 多选时,应绑定到一个数组。

修饰符

.lazy 默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步 (除了上述输入法组合文字时)。你可以添加 lazy 修饰符,从而转为在 change 事件之后进行同步。

.number 自动将用户输入转为 number 类型,如果这个值无法被 parseFloat 解析,就会返回原始值。

.trim 自动过滤用户输入的首尾空白字符

在组件上使用 v-model

默认会使用组件的 value 和 input 事件,可以使用 model 属性修改默认行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Vue.component("base-checkbox", {
model: {
prop: "checked",
event: "change",
},
props: {
checked: Boolean,
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`,
});

组件基础

为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。这里有两种组件的注册类型:全局注册和局部注册。至此,我们的组件都只是通过 Vue.component 全局注册的:

1
2
3
Vue.component("my-component-name", {
// ... options ...
});

动态组件

上述内容可以通过 Vue 的 component 元素加一个特殊的 is attribute 来实现:

1
<component v-bind:is="currentTabComponent"></component>

请留意,这个 attribute 可以用于常规 HTML 元素,但这些元素将被视为组件,这意味着所有的 attribute 都会作为 DOM attribute 被绑定。对于像 value 这样的 property,若想让其如预期般工作,你需要使用 .prop 修饰器。

dom property 和 attribute 的区别

当写 html 代码的时候,你可以在 html 标签上定义 attr,当浏览器解析你的 html 代码的时候,一个相应的 dom 节点会被创建,这个节点是一个对象,因此他具有他自己的属性。对于一个给定的 DOM 节点对象,属性是对象的属性,attributes 是对象的 attributes 属性里的元素。

当一个 DOM 节点被创建,dom 对象的许多 preperty 和 它的 attributes 里面的属性是相关联的

1
<input id="the-input" type="text" value="Name:" />

相应的 dom 节点会有 id,type 和 value 属性

id 属性是 id attribute 的反射,对其进行读写会直接改变 attribute 中的 id 属性。
type 属性 对其进行读写操作会改变 attribute 中的 type,但是它并不是一个纯的映射,因为它的值被限定在运行的列表内。
value 属性则不是一个映射,它是 input 控件元素的当前值,对其进行写入不会更改 attribute 中的 value。

解析 dom 模板的注意事项

有些 HTML 元素,诸如 ul、ol、table 和 select,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 li、tr 和 option,只能出现在其它某些特定的元素内部。
使用以下来源的模板,这种限制是不存在的。
字符串 (例如:template: ‘…’)
单文件组件 (.vue)
script type=”text/x-template”

深入了解组件

prop 类型检查

type 可以是下列原生构造函数中的一个

String
Number
Boolean
Array
Object
Date
Function
Symbol

额外的,type 还可以是一个自定义的构造函数,并且通过 instanceof 来进行检查确认。

非 prop 的 attribute

一个非 prop 的 attribute 是指传向一个组件,但是该组件并没有相应 prop 定义的 attribute。这些 attribute 会被添加到这个组件的根元素上。

替换/合并已有的 Attribute

对于大多数 attribute 来说,从外部提供给组件的值会替换掉组件内部设置好的值。class 和 style attribute 会稍微智能一些,即两边的值会被合并起来。

禁用 Attribute 继承

如果不希望组件的根元素继承 attribute,你可以在组件的选项中设置 inheritAttrs: false。
这尤其适合配合实例的 $attrs 属性使用,该属性包含了传递给一个组件的 attribute 名和 attribute 值,例如:

1
2
3
4
{
required: true,
placeholder: 'Enter your username'
}

自定义事件

如果希望监听原生事件,需要使用.native 修饰符。

.sync 语法糖 是以下方式的简写,带有.sync 修饰符的 v-bind 不能和表达式一起使用。

1
2
3
4
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>

插槽 编译作用域

1
2
3
4
5
6
7
8
<navigation-link url="/profile">
Clicking here will send you to: {{ url }}
<!--
这里的 `url` 会是 undefined,因为 "/profile" 是
_传递给_ <navigation-link> 的而不是
在 <navigation-link> 组件*内部*定义的。
-->
</navigation-link>

后备内容

有时为一个插槽设置具体的后备 (也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染。例如在一个 submit-button 组件中:

1
2
3
<button type="submit">
<slot>Submit</slot>
</button>

具名插槽

在向具名插槽提供内容的时候,我们可以在一个 template 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>

<template v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>

<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>

注意 v-slot 只能添加在 template 上

作用域插槽

绑定在 slot 元素上的 attribute 被称为插槽 prop。

1
2
3
<span>
<slot v-bind:user="user"> {{ user.lastName }} </slot>
</span>

为了让 user 在父级的插槽内容中可用,我们可以将 user 作为 slot 元素的一个 attribute 绑定上去:

1
2
3
<span>
<slot v-bind:user="user"> {{ user.lastName }} </slot>
</span>

在上述情况下,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot 直接用在组件上:

1
2
3
<current-user v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</current-user>

还可以更简单

1
<current-user v-slot="slotProps"> {{ slotProps.user.firstName }} </current-user>

只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template> 的语法:

1
2
3
4
5
6
7
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>

<template v-slot:other="otherSlotProps"> ... </template>
</current-user>

解构插槽 prop

插槽 prop 可以使用解构语法

1
<current-user v-slot="{ user }"> {{ user.firstName }} </current-user>

可以定义默认值

1
2
3
<current-user v-slot="{ user = { firstName: 'Guest' } }">
{{ user.firstName }}
</current-user>

动态插槽名

和动态属性名一样

具名插槽缩写

使用 #

动态组件 & 异步组件

使用动态组件的时候,有时会向保持这些组件的状态

1
2
3
4
<!-- 失活的组件将会被缓存!-->
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>

处理边界情况

程式化的事件侦听器

如此可以避免在实例上添加不必要的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mounted: function () {
this.attachDatepicker('startDateInput')
this.attachDatepicker('endDateInput')
},
methods: {
attachDatepicker: function (refName) {
var picker = new Pikaday({
field: this.$refs[refName],
format: 'YYYY-MM-DD'
})

this.$once('hook:beforeDestroy', function () {
picker.destroy()
})
}
}

递归组件

可以在自己的模板中调用自身,只能通过 name 选项来做这件事

模板定义的替代品

当 inline-template 这个特殊的 attribute 出现在一个子组件上时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容。这使得模板的撰写工作更加灵活。

1
2
3
4
5
6
<my-component inline-template>
<div>
<p>These are compiled as the component's own template.</p>
<p>Not parent's transclusion content.</p>
</div>
</my-component>

X-Template

另一个定义模板的方式是在一个 script 元素中,并为其带上 text/x-template 的类型,然后通过一个 id 将模板引用过去。例如:

1
2
3
<script type="text/x-template" id="hello-world-template">
<p>Hello hello hello</p>
</script>
1
2
3
Vue.component("hello-world", {
template: "#hello-world-template",
});

强制更新

使用 forceUpdate 来做这件事,只有很少的情况下才需要做这件事

过渡

css过渡

初始渲染的过渡

可以在节点上设置 appear,也可以设置自定义的类名和钩子函数

1
2
3
<transition appear>
<!-- ... -->
</transition>

过渡模式

默认是旧元素的离开和新元素的进入同时发生,可以设置 mode

in-out 新元素先进性过渡,完成后当前元素过渡离开
out-in 当前元素先进行过渡,完成后新元素过渡进入

多个组件的过渡,使用动态组件

1
2
3
<transition name="component-fade" mode="out-in">
<component v-bind:is="view"></component>
</transition>

什么是 rxjs

一组可以用来处理非同步或事件的 js 函数库

非同步

  • ajax/XHR/fetch
  • Service Worker/ Node Stream
  • setTimeout /setInterval
  • Promise

事件

  • DOM 事件 click dbclick keyup
  • CSS 动画事件
  • HTML5 Geolocation /websockets/server send event

Observable 可观察的物件

代表一组未来即将产生的事件资料

Observer 观察者物件

代表一个用来接收观察结果的物件
观察者物件包含 3 个含有回调函数的属性 next error complete

Subscription 订阅物件

代表正在执行 Observable/Observer 的执行个体 可用来取消订阅

Operators 运算子

必须拥有函数编程所定义的纯函数特性
主要用来处理一系列的事件资料合集
常见的运算子包含 map,filter,concat,flatMap,switchMap

Subject 主题物件

如同 EventEmitter 一样,主要用来广播收到的事件资料给多为 Observer (观察者)

schedulers 排程调度器

用来集中管理与调度多重事件之间的资料,一控制事件并发情况