LJKのBlog

学无止境

Redux 是什么?

Redux 是一个使用叫做“action”的事件来管理和更新应用状态的模式和工具库 它以集中式 Store(centralized store)的方式对整个应用中使用的状态进行集中管理,其规则确保状态只能以可预测的方式更新。

Redux 库和工具

Redux 是一个小型的独立 JS 库。 但是,它通常与其他几个包一起使用:

React-Redux

Redux 可以集成到任何的 UI 框架中,其中最常见的是 React 。React-Redux 是 Redux 官方维护的。

Redux Toolkit

Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。

Redux DevTools 扩展

Redux DevTools 扩展 可以显示 Redux 存储中状态随时间变化的历史记录。这允许你有效地调试应用程序,包括使用强大的技术,如“时间旅行调试”。

单向数据流( one-way data flow )

基于 state 渲染 view,当发生某些 action(例如用户单击按钮)时,生成新的 state,基于新的 state 再重新渲染 view,这就是单向数据流。

然而,当我们有多个组件需要共享和使用相同 state时,可能会变得很复杂,尤其是当这些组件位于应用程序的不同部分时。

解决这个问题的一种方法是从组件中提取共享 state,并将其放入组件树之外的一个集中位置。这样,我们的组件树就变成了一个大“view”,任何组件都可以访问 state 或触发 action,无论它们在树中的哪个位置!

不可变性

“Mutable” 意为 “可改变的”,而 “immutable” 意为永不可改变。

JavaScript 的对象(object)和数组(array)默认都是 mutable 的。

如果想要不可变的方式来更新,代码必需先 复制 原来的 object/array,然后更新它的复制体

展开运算符(spread operator)可以实现这个目的。

Redux 期望所有状态更新都是使用不可变的方式

术语

action

action 是一个具有 type 字段的普通 JavaScript 对象。你可以将 action 视为描述应用程序中发生了什么的事件.

action creator

action creator 是一个创建并返回一个 action 对象的函数。它的作用是让你不必每次都手动编写 action 对象:

1
2
3
4
5
6
const addTodo = text => {
return {
type: "todos/todoAdded",
payload: text,
}
}

reducer

reducer 是一个函数,接收当前的 state 和一个 action 对象,必要时决定如何更新状态,并返回新状态。函数签名是:(state, action) => newState你可以将 reducer 视为一个事件监听器,它根据接收到的 action(事件)类型处理事件。

reducer 必须符合一下规则:

  • 仅使用 stateaction 参数计算新的状态值
  • 禁止直接修改 state。必须通过复制现有的 state 并对复制的值进行更改的方式来做 _不可变更新(immutable updates)_。
  • 禁止任何异步逻辑、依赖随机值或导致其他“副作用”的代码

reducer 函数内部的逻辑通常遵循以下步骤:

  • 检查 reducer 是否关心这个 action
    • 如果是,则复制 state,使用新值更新 state 副本,然后返回新 state
  • 否则,返回原来的 state 不变

Redux 中 真正 重要的是 reducer 函数,以及其中计算新状态的逻辑。

store

当前 Redux 应用的状态存在于一个名为 store 的对象中。

store 是通过传入一个 reducer 来创建的,并且有一个名为 getState 的方法,它返回当前state

dispatch

Redux store 有一个方法叫 dispatch更新 state 的唯一方法是调用 store.dispatch() 并传入一个 action 对象。 store 将执行所有 reducer 函数并计算出更新后的 state,调用 getState() 可以获取新 state。

**dispatch 一个 action 可以形象的理解为 “触发一个事件”**。发生了一些事情,我们希望 store 知道这件事。 Reducer 就像事件监听器一样,当它们收到关注的 action 后,它就会更新 state 作为响应。

selector

随着应用变得越来越大,会遇到应用程序的不同部分需要读取相同的数据,selector 函数可以从 store 状态树中提取指定的片段。

redux 数据流

早些时候,我们谈到了“单向数据流”,它描述了更新应用程序的以下步骤序列:

  • State 描述了应用程序在特定时间点的状况
  • 基于 state 来渲染 UI
  • 当发生某些事情时(例如用户单击按钮),state 会根据发生的事情进行更新
  • 基于新的 state 重新渲染 UI

具体来说,对于 Redux,我们可以将这些步骤分解为更详细的内容:

  • 初始启动:
    • 使用最顶层的 root reducer 函数创建 Redux store
    • store 调用一次 root reducer,并将返回值保存为它的初始 state
    • 当 UI 首次渲染时,UI 组件访问 Redux store 的当前 state,并使用该数据来决定要呈现的内容。同时监听 store 的更新,以便他们可以知道 state 是否已更改。
  • 更新环节:
    • 应用程序中发生了某些事情,例如用户单击按钮
    • dispatch 一个 action 到 Redux store,例如 dispatch({type: 'counter/increment'})
    • store 用之前的 state 和当前的 action 再次运行 reducer 函数,并将返回值保存为新的 state
    • store 通知所有订阅过的 UI,通知它们 store 发生更新
    • 每个订阅过 store 数据的 UI 组件都会检查它们需要的 state 部分是否被更新。
    • 发现数据被更新的每个组件都强制使用新数据重新渲染,紧接着更新网页

动画的方式来表达数据流更新:

总结

  • Redux 是一个管理全局应用状态的库
    • Redux 通常与 React-Redux 库一起使用,把 Redux 和 React 集成在一起
    • Redux Toolkit 是编写 Redux 逻辑的推荐方式
  • Redux 使用 “单向数据流”
    • State 描述了应用程序在某个时间点的状态,UI 基于该状态渲染
    • 当应用程序中发生某些事情时:
      • UI dispatch 一个 action
      • store 调用 reducer,随后根据发生的事情来更新 state
      • store 通知 UI state 发生了变化
    • UI 基于新 state 重新渲染
  • Redux 有这几种类型的代码
    • Action 是有 type 字段的纯对象,描述发生了什么
    • Reducer 是纯函数,基于先前的 state 和 action 来计算新的 state
    • 每当 dispatch 一个 action 后,store 就会调用 root reducer

大型应用中,适合将路由分布在多个元素中,这样可以更轻松的进行代码分拆。但是在较小的应用程序中,或者具有密切相关的嵌套组件,则适合将所有路由集中放在一个中,这样可以在一个地方查看所有路由,代码可读性更强。

集中路由

分散路由

官网:https://tanstack.com/query/latest/docs/react/overview

刷新缓存:使用refetch或者使缓存失效,如果是全部查询则使用前者,如果是分页查询则使用后者。

staleTimecacheTime

  • staleTime:可以理解为数据保质期,在保质期内遇到同 key 的请求,不会去再次获取数据,而是从缓存中获取。

    staleTime 默认 0

  • cacheTime :数据在内存中的缓存时间,当数据在缓存期时,会按照 key 进行存储,下次遇到同 key 获取数据,会直接从缓存中取,瞬间展示,但是否后台请求新数据,要看 staleTime 的配置,当不配置 staleTime 时,遇到同 key 获取数据,虽然瞬间切换至缓存数据展示,但此时后台获取新数据,待获取完毕后瞬间切换为新数据。

    cacheTime 默认5分钟

https://blog.csdn.net/qq_21567385/article/details/114171438

isLoadingisFetching

  • isLoading
  • isFetching

isLoading 是在 “硬” 加载时才会为 trueisFetching 是在每次请求时为 true,那么 isFetching 我们能通俗易懂的理解,就是每次请求时当做 loading 嘛。

那什么是 isLoading 的 “硬” 加载?其实 “硬” 加载就是没有缓存时的加载,那么缓存是什么?

Query Invalidation(查询失效)

QueryClientinvalidateQueries 方法主动让查询(query)失效。

1
2
3
4
// 让缓存中的所有查询失效
queryClient.invalidateQueries()
// 使用'todos'开头的键的查询都失效
queryClient.invalidateQueries({ queryKey: ['todos'] })

当使用invalidateQueries让一个查询失效时,会发生两件事:

  • 查询被标记为过期(stale),过期状态会覆盖staleTime配置。
  • 发起重新请求(refetch),如果当前页面正渲染查询的数据,也会重新请求。

查询匹配

使用invalidateQueriesremoveQueries这些API(支持查询配置的都可以),支持精确匹配,也可以通过前缀匹配多个查询。

关于过滤器的类型,参考Query Filters.

下面示例中,使用todos前缀使查询键中以todos开头的所有查询失效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useQuery, useQueryClient } from '@tanstack/react-query'

// Get QueryClient from the context
const queryClient = useQueryClient()

queryClient.invalidateQueries({ queryKey: ['todos'] })

// Both queries below will be invalidated
const todoListQuery = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
})
const todoListQuery = useQuery({
queryKey: ['todos', { page: 1 }],
queryFn: fetchTodoList,
})

传递具体的query key:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
queryClient.invalidateQueries({
queryKey: ['todos', { type: 'done' }],
})

// 会失效
const todoListQuery = useQuery({
queryKey: ['todos', { type: 'done' }],
queryFn: fetchTodoList,
})

// 不会失效
const todoListQuery = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
})

invalidateQueries API是非常灵活的,如果只想让todos查询失效,而不影响更多的子键,可以通过设置exact: true实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
queryClient.invalidateQueries({
queryKey: ['todos'],
exact: true, // 精确匹配,不会扩大范围
})

// 会失效
const todoListQuery = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
})

// 不会失效
const todoListQuery = useQuery({
queryKey: ['todos', { type: 'done' }],
queryFn: fetchTodoList,
})

如果要更精细的颗粒度,可以传入函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10, // version >= 10 返回true
})

// 会失效
const todoListQuery = useQuery({
queryKey: ['todos', { version: 20 }],
queryFn: fetchTodoList,
})

// 会失效
const todoListQuery = useQuery({
queryKey: ['todos', { version: 10 }],
queryFn: fetchTodoList,
})

// 不会失效
const todoListQuery = useQuery({
queryKey: ['todos', { version: 5 }],
queryFn: fetchTodoList,
})

小场面

状态提升

兄弟组件之间传值(共享状态),需要状态提升,示例:

Ceshi3 和 Ceshi4 之间需要共享 value 值,所以将 value 值提升到他俩的公共父组件 Ceshi2 中:

Ceshi2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Ceshi3 from "./ceshi3"
import Ceshi4 from "./ceshi4"
import { useState } from "react"

const Ceshi2 = () => {
const [value, setValue] = useState("") // 状态提升
return (
<div>
<Ceshi3 value={value} setValue={setValue} />
<Ceshi4 value={value} setValue={setValue} />
</div>
)
}
export default Ceshi2

Ceshi3:

1
2
3
4
5
6
7
8
9
10
11
import { Input } from "antd"

type InputType = {
value: string
setValue: (value: string) => void
}

const Ceshi3 = ({ value, setValue }: InputType) => {
return <Input value={value} onChange={e => setValue(e.target.value)} />
}
export default Ceshi3

Ceshi4:

1
2
3
4
5
6
7
8
9
10
import { Input } from "antd"

type InputType = {
value: string
setValue: (value: string) => void
}
const Ceshi4 = ({ value, setValue }: InputType) => {
return <Input value={value} onChange={e => setValue(e.target.value)} />
}
export default Ceshi4

组合组件

对于上面状态提升的示例,只是将需要共享的数据进行提升,可以将组件进行提升,也就是组合组件:

Ceshi2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Ceshi3 from "./ceshi3"
import Ceshi4 from "./ceshi4"
import { useState } from "react"
import { Input } from "antd"

const Ceshi2 = () => {
const [value, setValue] = useState("") // 状态提升
const InputCeshi = <Input value={value} onChange={e => setValue(e.target.value)} />
return (
<div>
<Ceshi3 input={InputCeshi} />
<Ceshi4 input={InputCeshi} />
</div>
)
}
export default Ceshi2

Ceshi3:

1
2
3
4
const Ceshi3 = (props: { input: JSX.Element }) => {
return props.input
}
export default Ceshi3

Ceshi4:

1
2
3
4
const Ceshi4 = ({ input }: { input: JSX.Element }) => {
return input
}
export default Ceshi4

缓存状态

react-query

swr

客户端状态

url

Context

react 框架原生提供

示例:

1

Redux

第三方,最经典的,如果是大型应用,需要比较复杂的状态管理,推荐使用。

需要维护 一个全局状态树。

数据类型

变量与常量

1
int a = 0;  // 变量,可以再赋值

C 语言的常量可以分为直接常量符号常量,符号常量一般习惯使用大写字母:

1
const int len = 256;  // 直接常量

宏定义 define

示例:

1
#define PI 3.14

define 和 const 的区别

  • define 是宏定义,程序在预处理阶段将用 define 定义的内容进行了 替换 。因此在程序运行时,常量表中并没有用 define 定义的常量,系统不为它分配内存。

    而 const 定义的常量,在程序运行时,存在常量表中,且系统为它分配内存。

  • define 定义的常量,预处理时只是直接进行了替换,因此在编译时不能进行数据类型检验。

    而 const 定义的常量,在编译时进行严格的类型检验,可以避免出错。

  • define 定义表达式时要注意“边缘效应”,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #define N 1+2
    float a = N/2.0;
    /*
    按照常规做法,可能会认为结果是3/2 = 1.5
    但是实际上,结果应该为1+2/2.0 = 2.0

    若想要实现3/2,则#define N (1+2)
    即为避免边缘效应,一定要加!括!号!
    */

用 define 定义函数

示例:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#define LEN(x) sizeof(x) / sizeof(x[0])
int main() {
int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
for (int i = 0, l = LEN(arr); i < l; i++) {
printf("%d\n", arr[i]);
}
return 0;
}

基本数据类型

注:int、short int、long int 是根据编译环境的不同,所取范围不同。而其中 short int 和 long int 至少是表中所写范围,但是 int 在表中是以 16 位编译环境写的取值范围。另外 c 语言 int 的取值范围在于他占用的字节数 ,不同的编译器,规定是不一样。ANSI 标准定义 int 是占 2 个字节,TC 是按 ANSI 标准的,它的 int 是占 2 个字节的。但是在 VC 里,一个 int 是占 4 个字节的。

  • 整型
    • short:两个字节,16 位,2^16
    • unsigned short:第一位是符号位,-2^15 - 2^15
    • int:4 个字节,32 位,2^32
    • long:8 个字节,64 位
  • 浮点
    • float
    • double:这个一般不用
  • 字符
    • char:c++有 string 类型,c 没有 string 类型,c 中表示字符串可以使用字符数组或者字符字符指针。
  • void:对类型不关心,不敏感

格式化输出 printf

  • %d:带符号十进制整数
  • %c:单个字符
  • %s:字符串
  • %f:6 位小数

自动类型转换

自动转换发生在不同数据类型运算时,在编译的时候自动完成。自动转换遵循的规则就好比小盒子可以放进大盒子里面一样,下图表示了类型自动转换的规则。

char 类型数据转换为 int 类型数据遵循 ASCII 码中的对应值,ASCII 参考:http://c.biancheng.net/c/ascii/

注:字节小的可以向字节大的自动转换,但字节大的不能向字节小的自动转换

强制类型转换

强制类型转换是通过定义类型转换运算来实现的。其一般形式为:

1
(数据类型)(表达式)

其作用是把表达式的运算结果强制转换成类型说明符所表示的类型,例如:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(int argc, const char *argv[])
{
float a = 6.77;
int b = (int)a;
printf("%d\n", b);
return 0;
}

// 6

转换为 go:

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
var a float32 = 6.77
var b int = int(a)
fmt.Println(b)
}

// 6
  1. 转换后不会改变原数据的类型及变量值,只在本次运算中临时性转换
  2. 强制转换后的运算结果不遵循四舍五入原则

C 语言关键字

32 个:

  • auto:声明自动变量
  • short:声明短整型变量或函数
  • int:声明整型变量或函数
  • long :声明长整型变量或函数
  • float:声明浮点型变量或函数
  • double:声明双精度变量或函数
  • char:声明字符型变量或函数
  • struct:声明结构体变量或函数
  • union:声明共用数据类型
  • enum:声明枚举类型
  • typedef:用以给数据类型取别名
  • const:声明只读变量
  • unsigned:声明无符号类型变量或函数
  • signed:声明有符号类型变量或函数
  • extern:声明变量是在其他文件正声明
  • register:声明寄存器变量
  • static:声明静态变量
  • volatile:说明变量在程序执行中可被隐含地改变
  • void:声明函数无返回值或无参数,声明无类型指针
  • if:条件语句
  • else:条件语句否定分支(与 if 连用)
  • switch:用于开关语句    case:开关语句分支
  • for:一种循环语句
  • do:循环语句的循环体
  • while:循环语句的循环条件
  • goto:无条件跳转语句
  • continue:结束当前循环,开始下一轮循环
  • break:跳出当前循环
  • default:开关语句中的“其他”分支
  • sizeof:计算数据类型长度
  • return:子程序返回语句(可以带参数,也可不带参数)循环条件

1999 年 12 月 16 日,ISO 推出了 C99 标准,该标准新增了 5 个 C 语言关键字:

  • inline
  • restrict
  • _Bool
  • _Complex
  • _Imaginary

2011 年 12 月 8 日,ISO 发布 C 语言的新标准 C11,该标准新增了 7 个 C 语言关键字:

  • _Alignas
  • _Alignof
  • _Atomic
  • _Static_assert
  • _Noreturn
  • _Thread_local
  • _Generic

运算符

  • 算术运算符
  • 赋值运算符
  • 关系运算符
  • 逻辑运算符
  • 三目运算符

算术运算符

除法运算中注意:

如果相除的两个数都是整数的话,则结果也为整数,小数部分省略,如 8/3 = 2;而两数中有一个为小数结果则为小数,如:9.0/2 = 4.500000。

取余运算中注意:

该运算只适合用两个整数进行取余运算,如:10%3 = 1;而 10.0%3 则是错误的;运算后的符号取决于被模数的符号,如(-10)%3 = -1;而 10%(-3) = 1。

注:C 语言中没有乘方这个运算符,也不能用 ×,÷ 等算术符号。

自增与自减运算符

自增运算符为“++”,其功能是使变量的值自增 1;自减运算符为“–”,其功能是使变量值自减 1。它们经常使用在循环中。自增自减运算符有以下几种形式:

注意:无论是 a++还是++a 都等同于 a=a+1,在表达式执行完毕后 a 的值都自增了 1,无论是 a–还是–a 都等同于 a=a-1,在表达式执行完毕后 a 的值都自减少 1。

赋值运算符

C 语言中赋值运算符分为简单赋值运算符复合赋值运算符,之前我们已经接触过简单赋值运算符“=”号了,下面讲一下复合赋值运算符:

复合赋值运算符就是在简单赋值符“=”之前加上其它运算符构成,例如+=、-=、*=、/=、%=。

关系运算符

关系表达式的值是“真”和“假”,在 C 程序用整数 1 和 0 表示。

逻辑运算符

在数学中我们见过 7<x<100 这样的公式,意思是 x 大于 7 并且 x 小于 100。

在程序中这样写一个变量的范围值是不行的,计算机是看不懂这样的算式的,那么怎样让计算机看懂呢?这里就要用到逻辑运算符了。

逻辑运算的值也是有两种分别为“真”和“假”,C 语言中用整型的 1 和 0 来表示。其求值规则如下:

  1. 与运算(&&)

    参与运算的两个变量都为真时,结果才为真,否则为假。例如:5>=5 && 7>5 ,运算结果为真;

  2. 或运算(||)

    参与运算的两个变量只要有一个为真结果就为真。 两个量都为假时,结果为假。例如:5>=5||5>8,运算结果为真;

  3. 非运算(!)

    参与运算的变量为真时,结果为假;参与运算量为假时,结果为真。例如:!(5>8),运算结果为真。

三目运算符

1
表达式1 ? 表达式2 : 表达式3

运算符 优先级

没必要去死记运算符的优先级顺序,记住最高优先级是()就可以了。

结构语句

分支结构 if / if-else

简单 if

1
2
3
4
if(表达式)
{
执行代码块;
}

简单 if-else 语句

1
2
3
4
5
6
7
8
if(表达式)
{
执行代码块1;
}
else
{
执行代码块2;
}

多重 if-else 语句

1
2
3
4
5
6
7
8
9
10
11
12
13
if(表达式1)
{
执行代码块1;
}
else if(表达式2)
{
执行代码块2;
}
...
else
{
执行代码块3;
}

循环结构 while / do-while

while

1
2
3
4
while(表达式)
{
执行代码块;
}

使用 while 语句应注意以下几点:

  1. while 语句中的表达式一般是关系表达或逻辑表达式,当表达式的值为假时不执行循环体,反之则循环体一直执行。
  2. 一定要记着在循环体中改变循环变量的值,否则会出现死循环(无休止的执行)。
  3. 循环体如果包括有一个以上的语句,则必须用{}括起来,组成复合语句。

do-while

1
2
3
4
do
{
执行代码块;
}while(表达式)

do-while 循环语句的语义是:它先执行循环中的执行代码块,然后再判断 while 中表达式是否为真,如果为真则继续循环;如果为假,则终止循环。因此,do-while 循环至少要执行一次循环语句

循环结构 for

1
2
3
4
for(表达式1; 表达式2; 表达式3)
{
执行代码块;
}

在 for 循环中,表达式 1是一个或多个赋值语句,它用来控制变量的初始值表达式 2是一个关系表达式,它决定什么时候退出循环;表达式 3是循环变量的步进值,定义控制循环变量每循环一次后按什么方式变化。这三部分之间用分号(;)分开

使用 for 语句应该注意

  1. for 循环中的“表达式 1、2、3”均可可以缺省,但分号(;)不能缺省

  2. 省略“表达式 1(循环变量赋初值)”,表示不对循环变量赋初始值。如:

    1
    2
    3
    4
    5
    int i = 1;
    for(; i<=10; i++)
    {
    printf("第%d遍书写: computer\n", i);
    }
  3. 省略“表达式 2(循环条件)”,不做其它处理,循环一直执行(死循环)。如:

    1
    2
    3
    4
    for(int i=0; ; i++)
    {
    printf("第%d遍书写: computer\n", i);
    }
  4. 省略“表达式 3(循环变量增量)”,不做其他处理,循环一直执行(死循环)。如:

    1
    2
    3
    4
    for(int i=0; i<=10;) // 省略循环变量的步进值
    {
    printf("第%d遍书写: computer\n", i);
    }

    注:死循环可以使用后面即将讲到的 break 解决

  5. 表达式 1 可以是设置循环变量的初值的赋值表达式,也可以是其他表达式。如:

    1
    2
    3
    4
    5
    6
    7
    int sum, num;
    num = 0;
    // 循环变量初始值可以换成其他表达式
    for(sum = 0; num<=10; num++)
    {
    sum += num;
    }
  6. 表达式 1 和表达式 3 可以是一个简单表达式也可以是多个表达式以逗号分割。如:

    1
    2
    3
    4
    for (int sum = 0, num = 0; num <= 3; num++, sum++) {
    sum += num;
    printf("num=%d,sum=%d\n", num, sum);
    }
  7. 表达式 2 一般是关系表达式或逻辑表达式,但也可是数值表达式或字符表达式,只要其值非零,就执行循环体。

    1
    2
    3
    4
    5
    for(int sum=0,num=0; num<=3 && sum<=5 && 1; num++, sum++)
    {
    sum += num;
    printf("num=%d,sum=%d\n", num, sum);
    }

循环中的 break 与 continue

break 用于结束循环,continue 用于跳过本次循环。

注:在多层循环中,一个break 语句只跳出当前循环

分支结构 switch

1
2
3
4
5
6
switch(表达式){
case 常量表达式1:执行代码块1 break;
...
case 常量表达式n:执行代码块n break;
default: 执行代码块n+1;
}

注意:

  • 在 case 子句后如果没有 break;会一直往后执行一直到遇到 break;才会跳出 switch 语句。
  • switch 后面的表达式语句只能是整型或者字符类型
  • 在 case 后,允许有多个语句,可以不用{}括起来。
  • 各 case 和 default 子句的先后顺序可以变动,而不会影响程序执行结果。
  • default 子句可以省略不用。

goto 语句

1
goto 语句标号;

语句标号是一个标识符,该标识符一般用英文大写并遵守标识符命名规则,这个标识符加上一个“:”一起出现在函数内某处,执行 goto 语句后,程序将跳转到该标号处并执行其后的语句。

示例:用 goto 语句和 if 语句构成循环求 10 以内的数之和。

应用场景:深层循环嵌套,调到循环外面需要多个 break,但是仅仅使用一次 goto 语句就可以实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 伪代码
for()
{
for()
{
for()
{
if(disaster)
{
goto error;
}
}
}
}
error:
if(disaster)
{}

函数

C 语言提供了大量的库函数,比如 stdio.h 提供输出函数,但是还是满足不了我们开发中的一些逻辑,所以这个时候需要自己定义函数,自定义函数的一般形式:

1
2
3
4
5
[数据类型] 函数名称([参数])
{
执行代码块;
return (表达式);
}

注意:

  1. 数据类型默认 int

  2. 自定义函数尽量放在 main 函数之前,如果要放在 main 函数之后的话,需要在 mian 函数之前声明自定义函数,声明格式为:

    1
    [数据类型] 函数名称([参数])

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

int sayLove();

int sayHello()
{
printf("hello word\n");
return 0;
}

int main()
{
sayHello();
sayLove();
return 0;
}

int sayLove()
{
printf("i love u\n");
return 0;
}

内部函数与外部函数

在 C 语言中不能被其他源文件调用的函数称谓内部函数 ,内部函数由 static 关键字来定义,因此又被称谓静态函数,形式为:

1
static [returnType] funcName([params])

外部函数使用 extern 关键字修饰,可以省略。

数组

声明:

1
itemType arrayName[length]

初始化:

1
2
itemType arrayName[n] = {item1, item2...,itemn}
itemType arrayName[] = {item1, item2...,itemn}
  • 数组的下标均以 0 开始;
  • 数组在初始化的时候,数组内元素的个数不能大于声明的数组长度;
  • 如果采用第一种初始化方式,元素个数小于数组的长度时,多余的数组元素初始化为 0;

数组的遍历

只能使用 for 遍历,注意 c 语言没有提供计算数组长度的方法

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#define LEN(x) sizeof(x) / sizeof(x[0])
int main() {
int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
for (int i = 0, l = LEN(arr); i < l; i++) {
printf("%d\n", arr[i]);
}

return 0;
}

数组作为函数参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#define LEN(x) sizeof(x) / sizeof(x[0])

void tmp(int arr[], int len) {
for (int i = 0, l = len; i < l; ++i) {
printf("%d\n", arr[i]);
}
}

int main() {
int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
tmp(arr, LEN(arr));
return 0;
}

注意:

  • 形参可以指定长度,也可以不指定
  • 数组作为参数传递,传递的是指针,所以:
    • 不能通过 sizeof 求长度
    • 如果修改,是通过指针修改,会修改数组本身

字符串函数

strlen

strlen()获取字符串的长度,在字符串长度中是不包括‘\0’而且汉字和字母的长度是不一样的。

strcmp

strcmp()在比较的时候会把字符串先转换成 ASCII 码再进行比较,返回的结果为0 表示 s1 和 s2 的 ASCII 码相等,返回结果为1 表示 s1 比 s2 的 ASCII 码大,返回结果为**-1 表示 s1 比 s2 的 ASCII 码小**。

strcpy

strcpy()拷贝之后会覆盖原来字符串且不能对字符串常量进行拷贝。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <string.h>

int main() {
char str[] = "hello";
strcpy(str, "word");
printf("%s", str); // word
return 0;
}

strcat

拼接字符串,strcat 在使用时 s1 与 s2 指的内存空间不能重叠,且 s1 要有足够的空间来容纳要复制的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <string.h>

int main() {
char s1[] = "hello ";
char s2[] = "word";
char s[strlen(s1) + strlen(s2)];
strcpy(s, s1);
strcat(s, s2);
printf("%s", s); // hello word
return 0;
}

多维数组

PHP 中的 Exception, Error, Throwable

  • PHP 中将代码自身异常(一般是环境或者语法非法所致)称作错误 Error,将运行中出现的逻辑错误称为异常 Exception
  • 错误是没法通过代码处理的,而异常则可以通过 try/catch 来处理
  • PHP7 中出现了 Throwable 接口,该接口由 ErrorException 实现,用户不能直接实现 Throwable 接口,而只能通过继承 Exception 来实现接口

PHP7 异常处理机制

过去的 PHP,处理致命错误几乎是不可能的。致命错误不会调用由 set_error_handler() 设置的处理方式,而是简单的停止脚本的执行。

在 PHP7 中,当致命错误和可捕获的错误(E_ERRORE_RECOVERABLE_ERROR)发生时会抛出异常,而不是直接停止脚本的运行。对于某些情况,比如内存溢出,致命错误则仍然像之前一样直接停止脚本执行。在 PHP7 中,一个未捕获的异常也会是一个致命错误。这意味着在 PHP5.x 中致命错误抛出的异常未捕获,在 PHP7 中也是致命错误。

注意:其他级别的错误如 warningnotice,和之前一样不会抛出异常,只有 fatalrecoverable 级别的错误会抛出异常。

fatalrecoverable 级别错误抛出的异常并非继承自 Exception 类。这种分离是为了防止现有 PHP5.x 的用于停止脚本运行的代码也捕获到错误抛出的异常。fatalrecoverable 级别的错误抛出的异常是一个全新分离出来的类 Error 类的实例。跟其他异常一样,Error 类异常也能被捕获和处理,同样允许在 finally 之类的块结构中运行。

Throwable

为了统一两个异常分支,ExceptionError 都实现了一个全新的接口:Throwable

PHP7 中新的异常结构如下:

1
2
3
4
5
6
7
8
9
interface Throwable
|- Exception implements Throwable
|- ...
|- Error implements Throwable
|- TypeError extends Error
|- ParseError extends Error
|- ArithmeticError extends Error
|- DivisionByZeroError extends ArithmeticError
|- AssertionError extends Error

如果在 PHP7 的代码中定义了 Throwable 类,它将会是如下这样:

1
2
3
4
5
6
7
8
9
10
interface Throwable{
public function getMessage(): string;
public function getCode(): int;
public function getFile(): string;
public function getLine(): int;
public function getTrace(): array;
public function getTraceAsString(): string;
public function getPrevious(): Throwable;
public function __toString(): string;
}

这个接口看起来很熟悉。Throwable 规定的方法跟 Exception 几乎是一样的。唯一不同的是 Throwable::getPrevious() 返回的是 Throwable 的实例而不是 Exception 的。ExceptionError 的构造函数跟之前 Exception 一样,可以接受任何 Throwable 的实例。

Throwable 可以用于 try/catch块中捕获 ExceptionError 对象(或是任何未来可能的异常类型)。记住捕获更多特定类型的异常并且对之做相应的处理是更好的实践。然而在某种情况下我们想捕获任何类型的异常(比如日志或框架中错误处理)。在 PHP7 中,要捕获所有的应该使用 Throwable 而不是 Exception

1
2
3
4
5
1 try {
2 // Code that may throw an Exception or Error.
3 } catch (Throwable $t) {
4 // Handle exception
5 }

用户定义的类不能实现 Throwable 接口。做出这个决定一定程度上是为了预测性和一致性——只有 ExceptionError 的对象可以被抛出。此外,异常需要携带对象在追溯堆栈中创建位置的信息,而用户定义的对象不会自动的有参数来存储这些信息。

Throwable 可以被继承从而创建特定的包接口或者添加额外的方法。一个继承自 Throwable 的接口只能被 ExceptionError 的子类来实现。

1
2
3
4
5
1 interface MyPackageThrowable extends Throwable {}
2
3 class MyPackageException extends Exception implements MyPackageThrowable {}
4
5 throw new MyPackageException();

Error

事实上,PHP5.x 中所有的错误都是 fatalrecoverable 级别的错误,在 PHP7 中都能抛出一个 Error实例。跟其他任何异常一样,Error 对象可以使用 try/catch 块来捕获。

1
2
3
4
5
6
$var = 1;
try {
$var->method(); // Throws an Error object in PHP 7.
} catch (Error $e) {
// Handle error
}

通常情况下,之前的致命错误都会抛出一个基本的 Error 类实例,但某些错误会抛出一个更具体的 Error 子类:TypeErrorParseError 以及 AssertionError

TypeError

当函数参数或返回值不符合声明的类型时,TypeError 的实例会被抛出。

1
2
3
4
5
6
7
8
9
10
11
function add(int $left, int $right){
return $left + $right;
}

try {
$value = add('left', 'right');
} catch (TypeError $e) {
echo $e->getMessage(), "\n";
}

//Argument 1 passed to add() must be of the type integer, string given

ParseError

include/require 文件或 eval() 代码存在语法错误时,ParseError 会被抛出。

1
2
3
4
5
1 try {
2 require 'file-with-parse-error.php';
3 } catch (ParseError $e) {
4 echo $e->getMessage(), "\n";
5 }

ArithmeticError

ArithmeticError 在两种情况下会被抛出。一是位移操作负数位。二是调用intdiv() 时分子是 PHP_INT_MIN 且分母是 -1 (这个使用除法运算符的表达式:PHP_INT_MIN / -1,结果是浮点型)。

1
2
3
4
5
1 try {
2 $value = 1 << -1;
3 catch (ArithmeticError $e) {
4 echo $e->getMessage();//Bit shift by negative number
5 }

DevisionByZeroError

intdiv() 的分母是 0 或者取模操作 (%) 中分母是 0 时,DivisionByZeroError 会被抛出。注意在除法运算符 (/) 中使用 0 作除数(也即 xxx/0 这样写)时只会触发一个 warning,这时候若分子非零结果是 INF,若分子是 0 结果是 NaN。

1
2
3
4
5
1 try {
2 $value = 1 % 0;
3 } catch (DivisionByZeroError $e) {
4 echo $e->getMessage();//Modulo by zero
5 }

AssertionError

assert() 的条件不满足时,AssertionError 会被抛出。

1
ini_set('zend.assertions', 1);
1
2
3
4
5
6
7
1 ini_set('assert.exception', 1);
2
3 $test = 1;
4
5 assert($test === 0);
6
7 //Fatal error: Uncaught AssertionError: assert($test === 0)

只有断言启用并且是设置 ini 配置的 zend.assertions = 1assert.exception = 1 时,assert()才会执行并抛 AssertionError

在你的代码中使用 Error

用户可以通过继承 Error 来创建符合自己层级要求的 Error 类。这就形成了一个问题:什么情况下应该抛出 Exception,什么情况下应该抛出 Error

Error 应该用来表示需要程序员关注的代码问题。从 PHP 引擎抛出的 Error 对象属于这些分类,通常都是代码级别的错误,比如传递了错误类型的参数给一个函数或者解析一个文件发生错误。Exception 则应该用于在运行时能安全的处理,并且另一个动作能继续执行的情况。

由于 Error 对象不应该在运行时被处理,因此捕获 Error 对象也应该是不频繁的。一般来说,Error 对象仅被捕获用于日志记录、执行必要的清理以及展示错误信息给用户。

编写代码支持 PHP5.x 和 PHP7 的异常

为了在同样的代码中捕获任何 PHP5.x 和 PHP7 的异常,可以使用多个 catch,先捕获 Throwable,然后是 Exception。当 PHP5.x 不再需要支持时,捕获 Exceptioncatch 块可以移除。

1
2
3
4
5
6
7
try {
// Code that may throw an Exception or Error.
} catch (Throwable $t) {
// Executed only in PHP 7, will not match in PHP 5.x
} catch (Exception $e) {
// Executed only in PHP 5.x, will not be reached in PHP 7
}

不幸的是,处理异常的函数中的类型声明不容易确定。当 Exception 用于函数参数类型声明时,如果函数调用时候能用 Error 的实例,这个类型声明就要去掉。当 PHP5.x 不需要被支持时,类型声明则可以还原为 Throwable

如何理解React的副作用?

说到React的副作用,我们先说下纯函数(Pure function)、纯组件(Pure Component)。

纯函数 和 纯组件

纯函数 (Pure Function) 是 函数式编程 里面非常重要的概念 。

如果一个函数是 纯函数 (Pure Function) ,它必须符合两个条件:

  1. 函数返回结果只依赖入参,入参固定,则返回值永远不变。
  2. 函数执行过程中不会产生对外可观察的变化。

示例:

1
2
3
function sqrt(a) {
return a * a
}

**纯组件 **就是纯函数:给一个component相同的props,永远会渲染出相同的视图,并且不会产生对外可观察的变化。

副作用

纯函数不会产生对外可观察的变化,这个对外可观察的变化就是副作用。

副作用包括但不限于:

  • 修改外部变量;
  • 调用另一个非纯函数(纯函数内调用纯函数,不会产生副作用,依然还是纯函数);
  • 发送HTTP请求;
  • 调用DOM API 修改页面;
  • 调用 window.reload 刷新页面,甚至console.log()往控制台打印数据;
  • Math.random()
  • 获取当前时间;

钩子 Hook

https://www.ruanyifeng.com/blog/2020/09/react-hooks-useeffect-tutorial.html

https://www.ruanyifeng.com/blog/2019/09/react-hooks.html

函数组件的主体只应该用来返回组件的 HTML 代码,所有的其他操作(副作用)都必须通过钩子(hook)引入。

由于副作用非常多,所以hook有许多种。React 为许多常见的副作用提供了专用的hook:

  • useState():保存状态
  • useContext():保存上下文
  • useRef():保存引用

上面这些hook,都是引入某种特定的副作用,而 useEffect()是通用的副作用hook 。找不到对应的hook时,就可以用它。其实,从名字也可以看出来,它跟副作用(side effect)直接相关。

useEffect

1
useEffect(didUpdate);

只要是副作用,都可以使用useEffect()引入。它的常见用途有下面几种:

  • 获取数据(data fetching)
  • 事件监听或订阅(setting up a subscription)
  • 改变 DOM(changing the DOM)
  • 输出日志(logging)

重点:

  1. 赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。

  2. 清除 effect:useEffect 函数可以返回一个清除函数,用以清除 effect 创建的诸如订阅或计时器 ID 等资源,示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    export const useDebounce = <V>(value: V, delay: number = 1000) => {
    const [debouncedValue, setDebouncedValue] = useState(value);
    useEffect(() => {
    // 每次在value变化以后,设置一个定时器
    const timeout = setTimeout(() => setDebouncedValue(value), delay);
    // 每个定时器的名称都是timeout,所以只有最后一个定时器能存活下来
    return () => clearTimeout(timeout); // 返回一个清除函数
    }, [value, delay]);
    return debouncedValue;
    };

    如果该 useEffect 只调用一次,清除函数会在组件卸载前执行;如果组件多次渲染(通常如此),则 在执行下一个 effect 之前,上一个 effect 就已被清除

  3. effect 的执行时机:传给 useEffect 的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用,虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。在开始新的更新前,React 总会先清除上一轮渲染的 effect。

  4. effect 的条件执行:给 useEffect 传递第二个参数,它是 effect 所依赖的值数组。

 react 中组件重新渲染是很频繁的,为了避免重复的网络请求,所以发送网络请求的操作一定要放在 useEffect 中  

useState

1
const [state, setState] = useState(initialState);

返回一个 state,以及更新 state 的函数。

在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。

setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。就是说每次使用setState都会重新渲染组件。

1
setState(newState);

在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state

注意:

React 会确保 setState 函数的标识是稳定的,并且不会在组件重新渲染时发生变化。这就是为什么可以安全地从 useEffectuseCallback 的依赖列表中省略 setState

useReducer

useReducer 是 useState 的替代方案。当 useState 不能很好的满足需要的时候,useReducer 可能会解决我们的问题。

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
import { useReducer } from "react";
import { Button, Space } from "antd";

type Action = {
type: "incr" | "decr" | "set";
present?: number;
};
const Test = () => {
const reducer = (state: number, action: Action) => {
switch (action.type) {
case "decr": {
return state - 1;
}
case "incr": {
return state + 1;
}
case "set": {
if (action.present) {
return action.present;
} else {
throw new Error("未设置有效值");
}
}
}
// return state
};
const initial = 0;
const [state, dispatch] = useReducer(reducer, initial);
return (
<Space>
{state}
<Button onClick={() => dispatch({ type: "incr" })}>+</Button>
<Button onClick={() => dispatch({ type: "decr" })}>-</Button>
<Button onClick={() => dispatch({ type: "set" })}>reset</Button>
</Space>
);
};

export default Test;

useContext

React.createContext

1
const MyContext = React.createContext(defaultValue);

创建一个 Context 对象。

Context.Provider

1
<MyContext.Provider value={/* 某个值 */}>

Provider 接收一个 value 属性,传递给 consumers 组件。一个 Provider 可以和多个 consumers 组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。

当 Provider 的 value 值发生变化时,它内部的所有 consumers 组件都会重新渲染。

通过新旧值检测来确定变化,使用了与 Object.is 相同的算法(基本等同于“===”)。

注意

当传递对象给 value 时,检测变化的方式会导致一些问题:详见注意事项

Class.contextType

Context.Consumer

Context.displayName

全局安装 create-react-app

1
2
3
4
5
# 配置npm国内源
yarn config set registry https://registry.npm.taobao.org/

# 全局安装 create-react-app
yarn global add create-react-app

初始化项目

1
2
# npx create-react-app my-app --template typescript
yarn create react-app my-app --template typescript

删除.git

因为是个人开发,不是团队开发,所以不需要 git

1
2
3
4
5
6
7
8
admin@admin MINGW64 /d/www
$ cd my-app/

admin@admin MINGW64 /d/www/my-app (master)
$ rm -rf .git

admin@admin MINGW64 /d/www/my-app
$

配置 import 的 baseurl

在 tsconfig.json 中添加 baseUrl 配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"compilerOptions": {
"baseUrl": "./src",
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}

配置格式化规则

在项目根目录下新建文件 .prettierrc.js ,定义格式化规则:

1
2
3
4
5
module.exports = {
printWidth: 100, //单行长度
tabWidth: 4, //缩进长度
semi: false, //句末使用分号
}

在项目根目录下新建文件 .prettierignore ,语法同.gitignore,定义忽略格式化的文件:

1
2
build
coverage

主要文件介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
admin@admin MINGW64 /d/www/my-app
$ ll src/
total 12
-rw-r--r-- 1 admin 197121 564 Jul 27 09:39 App.css
-rw-r--r-- 1 admin 197121 273 Jul 27 09:39 App.test.tsx
-rw-r--r-- 1 admin 197121 556 Jul 27 09:39 App.tsx # 描述app本身
-rw-r--r-- 1 admin 197121 366 Jul 27 09:39 index.css
-rw-r--r-- 1 admin 197121 500 Jul 27 09:39 index.tsx # 入口文件,准备工作
-rw-r--r-- 1 admin 197121 2632 Jul 27 09:39 logo.svg
-rw-r--r-- 1 admin 197121 41 Jul 31 18:05 react-app-env.d.ts # 引入预先定义好的typescript类型
-rw-r--r-- 1 admin 197121 425 Jul 27 09:39 reportWebVitals.ts # 埋点上报
-rw-r--r-- 1 admin 197121 241 Jul 27 09:39 setupTests.ts # 配置单元测试
admin@admin MINGW64 /d/www/my-app
$ ll public/ # public目录不参与打包
total 30
-rw-r--r-- 1 admin 197121 3870 Jul 27 09:39 favicon.ico
-rw-r--r-- 1 admin 197121 1721 Jul 27 09:39 index.html
-rw-r--r-- 1 admin 197121 5347 Jul 27 09:39 logo192.png
-rw-r--r-- 1 admin 197121 9664 Jul 27 09:39 logo512.png
-rw-r--r-- 1 admin 197121 492 Jul 27 09:39 manifest.json # pwa配置文件
-rw-r--r-- 1 admin 197121 67 Jul 27 09:39 robots.txt # 配置爬虫权限

安装各种包

json-server

1
yarn global add json-server  # 推荐全局安装

在项目根目录下新建目录 __json_server__,在目录下新建 db.json 文件。

package.json 中新增快捷方式:

1
2
3
4
5
6
7
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"json-server": "json-server --watch --host 0.0.0.0 --port 3001 ./__json_server__/db.json"
},

prettier

1
2
yarn add prettier --dev --exact
yarn add eslint-config-prettier --dev # 解决prettier和eslint的冲突

修改 package.json,新增”prettier”

1
2
3
4
5
6
7
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest",
"prettier"
]
},

qs

1
2
yarn add qs
yarn add @types/qs --dev # 给qs打补丁,加上类型,以适应ts的类型检查

emotion

1
yarn add @emotion/styled @emotion/react

dayjs

1
yarn add dayjs

.env 和 .env.development

npm start 的时候,调用 .envnpm run build 的时候,调用.env.development,create-react-app 会自动切换。注意:配置项一定是REACT_APP_开头。

.env 示例:

1
REACT_APP_API_URL=http://online.com

.env.development 示例:

1
REACT_APP_API_URL=http://172.16.2.1:3001

Ant Design

官网:https://ant.design/index-cn

1
yarn add antd

craco

1
2
yarn add @craco/craco
yarn add craco-less --dev

项目根目录下新建文件 craco.config.js :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const CracoLessPlugin = require("craco-less")

module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: {
"@primary-color": "#1890ff",
"@font-size-base": "16px",
},
javascriptEnabled: true,
},
},
},
},
],
}

配置 webstrom

汉化

这里已经安装过了

配置自动 import 的绝对路径

这里应勾选了

配置 css-in-js 语法高亮

这里已经安装过了

配置自动格式化

yarn v2

以上是 yarn v1 搭配 create-react-app 构建 react,下面介绍使用 yarn v2:

  1. 全局安装 yarn v2

    1
    npm i yarn@berry -g
  2. 初始化项目:

    1
    2
    3
    4
    5
    > yarn dlx create-react-app react-ts-antd-yarn2-dev --template typescript
    ...
    ➤ YN0000: └ Completed in 2s 148ms
    ➤ YN0000: Failed with errors in 5s 116ms
    `yarnpkg add @testing-library/jest-dom@^5.14.1 @testing-library/react@^12.0.0 @testing-library/user-event@^13.2.1 @types/jest@^27.0.1 @types/node@^16.7.13 @types/react@^17.0.20 @types/react-dom@^17.0.9 typescript@^4.4.2 web-vitals@^2.1.0` failed
  3. 初始化项目:

    1
    2
    3
    4
    5
    6
    7
    8
    # 1. 进入项目目录
    > cd react-ts-antd-yarn2-dev
    # 2. 升级yarn到v3,这一步是必须的
    > yarn set version stable # 或者:yarn set version berry
    # 2. 配置npm国内源
    > yarn config set npmRegistryServer https://registry.npmmirror.com
    # 3. 初始化
    > yarn
  4. 安装必要包

    1
    2
    3
    4
    5
    > yarn add --dev @testing-library/jest-dom @testing-library/react @testing-library/user-event @types/jest @types/node @types/react @types/react-dom@ typescript
    > yarn add web-vitals

    # 精简(去掉测试包),并删除src下测试相关的文件
    > yarn add --dev typescript @types/react-dom @types/react @types/jest @types/node
  5. 项目根目录下新建 tsconfig.json 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    {
    "compilerOptions": {
    "baseUrl": "./src",
    "target": "es2015",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
    },
    "include": ["src"]
    }
  6. src 目录下新建 react-app-env.d.ts 文件,如果缺少这个文件,已组件形式引入图片会报错

    1
    /// <reference types="react-scripts" />
  7. .gitignore 中新增以下内容:

    1
    2
    3
    4
    5
    6
    7
    .yarn/*
    !.yarn/cache
    !.yarn/patches
    !.yarn/plugins
    !.yarn/releases
    !.yarn/sdks
    !.yarn/versions
  8. 启动项目

    1
    > yarn start
  9. 配置 prettier

    1
    > yarn add --dev eslint-config-prettier prettier

    项目根目录下新增 .prettierrc.js 文件

    1
    2
    3
    4
    5
    6
    7
    8
    //此处的规则供参考,其中多半其实都是默认值,可以根据个人习惯改写
    module.exports = {
    printWidth: 100, //单行长度
    tabWidth: 2, //缩进长度
    semi: false, //句末使用分号
    jsxBracketSameLinte: false,
    arrowParens: "avoid",
    }
  10. 剩下的和 yarn 一致

1
2
> yarn add @emotion/react @emotion/styled antd @ant-design/icons dayjs qs react-query react-router react-router-dom
> yarn add --dev @craco/craco craco-less @types/qs

https://juejin.cn/post/6844904184894980104

泛型是什么

设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。

为了便于大家更好地理解上述的内容,我们来举个例子,在这个例子中,我们将一步步揭示泛型的作用。首先我们来定义一个通用的 identity 函数,该函数接收一个参数并直接返回它:

1
2
3
4
5
function identity(value) {
return value
}

console.log(identity(1)) // 1

现在,我们将 identity 函数做适当的调整,以支持 TypeScript 的 Number 类型的参数:

1
2
3
4
5
function identity(value: Number): Number {
return value
}

console.log(identity(1)) // 1

这里 identity 的问题是我们将 Number 类型分配给参数和返回类型,使该函数仅可用于该原始类型。但该函数并不是可扩展或通用的,很明显这并不是我们所希望的。

我们确实可以把 Number 换成 any,我们失去了定义应该返回哪种类型的能力,并且在这个过程中使编译器失去了类型保护的作用。我们的目标是让 identity 函数可以适用于任何特定的类型,为了实现这个目标,我们可以使用泛型来解决这个问题,具体实现方式如下:

1
2
3
4
5
function identity<T>(value: T): T {
return value
}

console.log(identity<Number>(1)) // 1

对于刚接触 TypeScript 泛型的读者来说,首次看到 `` 语法会感到陌生。但这没什么可担心的,就像传递参数一样,我们传递了我们想要用于特定函数调用的类型。

参考上面的图片,当我们调用 identity(1)Number 类型就像参数 1 一样,它将在出现 T 的任何位置填充该类型。图中 ``内部的T被称为类型变量,它是我们希望传递给 identity 函数的类型占位符,同时它被分配给value参数用来代替它的类型:此时T 充当的是类型,而不是特定的 Number 类型。

其中 T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:

  • K(Key):表示对象中的键类型;
  • V(Value):表示对象中的值类型;
  • E(Element):表示元素类型。

其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量 U,用于扩展我们定义的 identity 函数:

1
2
3
4
5
6
function identity<T, U>(value: T, message: U): T {
console.log(message)
return value
}

console.log(identity<Number, string>(68, "Semlinker"))

除了为类型变量显式设定值之外,一种更常见的做法是使编译器自动选择这些类型,从而使代码更简洁。我们可以完全省略尖括号,比如:

1
2
3
4
5
6
function identity<T, U>(value: T, message: U): T {
console.log(message)
return value
}

console.log(identity(68, "Semlinker"))

对于上述代码,编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T 和 U,而不需要开发人员显式指定它们。

如你所见,该函数接收你传递给它的任何类型,使得我们可以为不同类型创建可重用的组件。现在我们再来看一下 identity 函数:

1
2
3
4
function identity<T, U>(value: T, message: U): T {
console.log(message)
return value
}

相比之前定义的 identity 函数,新的 identity 函数增加了一个类型变量 U,但该函数的返回类型我们仍然使用 T。如果我们想要返回两种类型的对象该怎么办呢?针对这个问题,我们有多种方案,其中一种就是使用元组,即为元组设置通用的类型:

1
2
3
function identity<T, U>(value: T, message: U): [T, U] {
return [value, message]
}

虽然使用元组解决了上述的问题,但有没有其它更好的方案呢?答案是有的,你可以使用泛型接口。

泛型接口

为了解决上面提到的问题,首先让我们创建一个用于的 identity 函数通用 Identities 接口:

1
2
3
4
interface Identities<V, M> {
value: V
message: M
}

在上述的 Identities 接口中,我们引入了类型变量 VM,来进一步说明有效的字母都可以用于表示类型变量,之后我们就可以将 Identities 接口作为 identity 函数的返回类型:

1
2
3
4
5
6
7
8
9
10
11
function identity<T, U>(value: T, message: U): Identities<T, U> {
console.log(value + ": " + typeof value)
console.log(message + ": " + typeof message)
let identities: Identities<T, U> = {
value,
message,
}
return identities
}

console.log(identity(68, "Semlinker"))

以上代码成功运行后,在控制台会输出以下结果:

1
2
3
68: number
Semlinker: string
{value: 68, message: "Semlinker"}

泛型除了可以应用在函数和接口之外,它也可以应用在类中,下面我们就来看一下在类中如何使用泛型。

泛型类

Gird:网格布局

Flex:弹性布局

应用场景:

  • 一维布局用 flex;二维布局用 grid
  • 从内容出发,用 flex;从布局出发,用 grid

https://juejin.cn/post/6908962052727898125#comment

与 Javascript 一样,CSS 中也有一些计算函数,仅对与一些常用的和个人了解的做一次分享。

CSS 函数较多,从常用性和实际出发,按运用比重顺序排序

Color 颜色相关

rgb()、rgba()

这两个函数一定是不会陌生的,对于颜色属性值通过红绿蓝三原色来进行调整。

区别在于 rgba() 中 a 表示透明度,取值 0-1 之间。相对来说最为常用

除此之外,类似的CSS 颜色函数还有:

hsl()、hsla()

使用色相、饱和度、亮度、(透明度)来定义颜色

hwb()

使用色相、以及黑白混合度(white\black)如上方右图所示。

相对的,可能对于美术或设计师更能理解以色相为主的颜色选取方式,其实对于代码来说,我们使用 rgb()和 rgba()即可,兼容性也是良好的。

color-mod()

CSS Color Module Level 4 中提出一个新的 CSS

其是基于一个颜色值,在不同的条件下调整参数得到的新颜色,未作深入了解。可以通过colorme.io/这个网站来看下具体颜色值是如何变化的,同时大漠老师的博客中也有相关的文档解释,可以参考下。

使用 color-mod()函数修改颜色

Transform 变换相关

对于不同内核浏览器,可能需要加入私有前缀来进行对应的兼容性处理,而 3d 边换,IE 毒瘤则是一定不支持的

translate 位置变换:translate()、translateX()、translateY()、translate3d()

rotate 旋转变换:rotate()、rotate3d()、rotateX()、rotateY()、rotateZ()

skew 倾斜变换:skew()、skewX()、skewY()

scale 缩放变换:scale()、scale3d()、scaleX()、scaleY()、scaleZ()

Background 背景相关

linear-gradient() 线性渐变

代码实现如下:

radial-gradient() 径向渐变

代码实现如下:

conic-gradient() 角向渐变

代码实现如下:

element() 将网站中的某部分当作图片渲染

CSS Color Module Level 4 中提出一个新的 CSS,由于只能在 Firefox 浏览器内产生效果,可以参考大漠老师的另一篇文章,里面有更为详细的介绍

CSS element()函数

Math 计算相关

相对来说,计算相关的函数其实兼容性一般,对于个人项目或移动端项目可以做尝试

min() 取最小值

对于 dialog 这一类元素,其长度应当适应于不同分辨率下,以往可能需要使用 vw / % 等来渲染,但是会有一个问题,那就是在不同分辨率下可能 60vw / 60% 下,dialog 的宽度在 1920 分辨率下看着还算正常,但是在 1366 等比较小的分辨率下会出现宽度不足以容纳内部元素,或者做适配时不太优雅展示的时候,可以考虑结合这一类计算函数来进行。

1
2
3
4
5
.dialog {
width: min(700px,80vw)
/* 解析为宽度取 700px 和 80vw 中较小的那一个数值 */
}
复制代码

max() 取最大值

同 min() 取值逻辑

clamp() 取值范围

clamp(MIN, VAL, MAX) = max(MIN, min(VAL, MAX))

其中 MIN 表示最小值,VAL 表示首选值,MAX 表示最大值。意思是,如果 VAL 在 MIN 和 MAX 范围之间,则使用 VAL 作为函数返回值;如果 VAL 大于 MAX,则使用 MAX 作为返回值;如果 VAL 小于 MIN,则使用 MIN 作为返回值。

1
2
3
4
5
6
7
.dialog {
width: clamp(240px, 50vw, 600px)
/* 解析为当 50vw > 600px 时,.dialog width值等于600px */
/* 当 240px <= 50vw <= 600px 时,.dialog width值等于50vw */
/* 进一步缩小,当 50vw < 240px时,.dialog width值等于 240px */
}
复制代码

相关例子效果展示可以参考张鑫旭老师博客中的在线代码,地址如下:

www.zhangxinxu.com/study/20200…

calc() 动态计算

calc() 应该是最为常见的 css 计算函数了,可结合 四则运算来取部分的宽高等,可用于圣杯布局中主体内容的宽高计算等需要动态计算的地方

Attr() 属性函数

仅从官方的例子中只能看到些基础的应用,且容易理解,而张鑫旭老师博客中指出:

传统的 attr()语法只能让 HTML 属性作为字符串使用,且只能使用在伪元素中

全新的 attr()语法那可就完全不得了了,可以让 HTML 属性值转换成任意的 CSS 数据类型。

个人尝试代码直接报错了~😅,

还是建议想要了解的查看下原文章:

www.zhangxinxu.com/wordpress/2…

CSS 变量

**var()函数可以代替元素中任何属性中的值的任何部分。var()**函数不能作为属性名、选择器或者其他除了属性值之外的值。

CSS 变量声明

--* 用来声明变量,使用 var(--*) 来使用变量

  • CSS 变量声明只可用于属性值,不可用以属性名

  • CSS 变量不支持多个同时声明

  • CSS 变量使用的合法性

  • CSS 变量与属性单位结合需要使用 * 乘法

  • CSS 变量的声明时可相互调用声明的变量

    :root { –main-bg-color: pink; –ml: 20px; –mlv: 20; –primary-size: 20px; –base-font-size: var(–primary-size); /_ 变量声明时的调用 / } body { background-color: var(–main-bg-color); / 表现结果为:背景色为粉红 / } .ml20 { margin-left: var(–ml); margin-left: calc(var(–mlv) * 1px); / 表现结果都是左间距 20px,此处的变量运用需区别带单位和不带单位的使用差异 _/ }

CSS 变量作用域

1
2
3
4
5
6
7
8
9
10
11
12
:root { --color: purple; }
div { --color: green; }
#alert { --color: red; }
* { color: var(--color); }

<p>我的紫色继承于根元素</p>
<div>我的绿色来自直接设置</div>
<div id='alert'>
ID选择器权重更高,因此阿拉是红色!
<p>我也是红色,占了继承的光</p>
</div>
复制代码

上述代码中,显示出了 CSS 变量也是具有作用域的

: root 取全局作用域,div 取所有的 div 元素,#alert 取所有 id 为 alert 的元素使用,按照 css 解析顺序,上面案例代码的元素颜色即和文字表述一致

JS 读取 CSS 变量

1
2
3
4
5
6
var cssVarColor = getComputedStyle(box).getPropertyValue('--color');

// 输出cssVarColor
// 输出变量值是:#cd0000
console.log(cssVarColor);
复制代码

一、超链接

1
2
3
4
5
6
7
8
9
10
11
12
a:link {
color: red;
}
a:visited {
color: blue;
}
a:hover {
color: black;
}
a:active {
color: #6600cc;
}

注意:设置的顺序不能变,遵循爱恨(love/hate)原则。 ——此原则自己在网上看别人说的,便于记忆就写在这了

二、更多伪类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
:root  ------------------------------ 文档的根

:nth-child(n) --------------------- 作为其父元素的第n个孩子的一个元素

:nth-last-child(n) ---------------- 作为其父元素的第n个孩子的一个元素,从最后一个数起

:nth-of-type(n) ------------------ 作为其类型的第n个兄弟的一个元素

:nth-last-of-type(n) ----------- 作为其类型的第n个兄弟的一个元素,从最后一个数起

:first-child ---------------------- 作为其父元素的第1个孩子的一个元素

:last-child ----------------------- 作为其父元素的最后1个孩子的一个元素

:first-of-type -------------------- 作为其类型的第1个兄弟的一个元素

:last-of-type ------------------- 作为其类型的第1个兄弟的一个元素,从最后一个数起

:only-child -------------------- 作为其父元素的唯一1个孩子的一个元素

:only-of-type ------------------- 作为其类型的唯一1个兄弟的一个元素

:empty -------------------------- 没有孩子或文本的一个元素

三、一些伪元素

1
2
3
:first-letter  第一个字母
:after 选择元素的后面,允许在这些位置插入内容
:before 选择元素的前面,允许在这些位置插入内容

基础类型

string、number、boolean

array(数组)

ts 表示数组有两种形式,元素类型[] 或者 Array<元素类型>,示例:

1
2
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];

如果是只读数组,被赋值后就不能修改了,使用ReadonlyArray<元素类型>

1
let rolist: ReadonlyArray<number> = [1, 2, 3, 4, 5];

unknown

any

enum(枚举)

数字枚举

默认就是数字枚举,从 0 开始递增,如果给某个成员赋值,后面的在这个值的基础上自增。

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Direction {
up,
down,
left,
right,
} // up=0、down=1、left=2、right=3,从0开始依次递增

enum Direction {
up,
down = 2,
left,
right,
} // up=0、down=2、left=3、right=4,down后面的从2开始依次递增

字符串枚举

字符串枚举,因为不能自增,所以需要每个成员都进行初始化。

1
2
3
4
5
6
enum Direction {
up = "up",
down = "down",
left = "left",
right = "right",
}

异构枚举

混合字符串和数字,但是不推荐这么做。

1
2
3
4
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}

外部枚举

void(空值)

null 和 undefined

当打印一个变量时,这个变量没有定义就是undefined,定义了没有赋值就是null

ts 中,nullundefined各自有自己的类型,分别是 null 和 undefined,它们本身作用不大,默认情况下,null 和 undefined 类型是所有其他类型的子类型,就是说你可以把nullundefined赋值给number类型的变量。

不过,指定–strictNullChecks,nullundefined就只能赋值给 any 和它们各自的类型(有一个例外是undefined还可以赋值给 void 类型)

never

void 表示没有任何类型,never 表示永远不存在的值的类型。

void 没有返回值,never 返回的值没有类型。

object

类型断言

类型转换,有两种形式:

1
2
3
4
5
6
7
// “尖括号”语法:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

// as 语法:
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

在 TypeScript 里使用 JSX 时,只有 as 语法断言是被允许的。

关于 number、string、boolean、symbol、object

接口

接口的作用是规范、限制、约束。定义接口的关键字是 interface 和 type。

  • interface 可以重复定义(merge),type 不可以
  • interface 定义的接口格式固定,type 可以定义各种格式的接口
  • interface 可以实现接口的 extends/implements,type 不行,基于此,推荐约束变量类型的时候用 type,定义类接口的时候用 interface

属性类接口

1
2
3
4
5
6
7
8
interface LabeledValue {
label: string;
}
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

定义接口的关键字 interface 或者 type,上例还可以写作:

1
2
3
type LabeledValue {
label: string;
}

可选属性 ?

接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}

function paintShape(opts: PaintOptions) {
// ...
}

const shape = getShape();
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });

xPosyPos 均被视为可选

只读属性 readonly

一些对象属性在创建的时候定义其值,然后就再也不能被修改,这时候就可以在属性名前用 readonly 来指定只读属性:

1
2
3
4
5
6
7
8
9
// 定义接口
interface Point {
readonly x: number;
readonly y: number;
}

// p1和p2的x、y就再也不能被改变了
let p1: Point = { x: 1, y: 2 };
let p2: Point = { x: 3, y: 4 };

只读数组的类型是ReadonlyArray<元素类型>

1
2
3
4
5
6
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error! 可以看到ReadonlyArray不能赋值到普通数组,不过可以通过类型断言重写:a = ro as number[]
readonly 和 const

最简单判断该用 readonly 还是 const 的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const ,若做为属性则使用 readonly 。

额外的属性检查

示例一,正常:

1
2
3
4
5
6
7
8
interface LabeledValue {
label: string;
}
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

示例二,报错:

1
2
3
4
5
6
7
interface LabeledValue {
label: string;
}
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}
printLabel({ size: 10, label: "Size 10 Object" }); // error

prinLabel 函数期待的参数是 LabeledValue 类型的,所以 size 属性显然是多余的,示例一中,定义变量 myObject 时没有指定类型,这么写可以绕开检查,最后没有报错,此外还有两种方式也可以绕开检查:

1
2
// 使用类型断言
printLabel({ size: 10, label: "Size 10 Object" } as LabeledValue);
1
2
3
4
5
6
7
8
9
10
// 添加一个字符串索引签名
interface LabeledValue {
label: string;
[key: string]: any; // key是string类型,value是any类型
}
function printLabel(labeledObj: LabeledValue): void {
console.log(labeledObj.label);
console.log(labeledObl.size);
}
printLabel({ size: 100, label: "Size 10 Object" });

不提倡绕开检查,除非是包含方法和内部状态的复杂对象字面量,可能需要使用这些技巧。

函数类型接口

不常用

1
2
3
4
5
6
7
8
9
10
type Fn = {
(a: string, b: number): void; // 返回类型为void,则表示不限制返回类型
};

let fn: Fn = (x, y) => {
console.log(x);
console.log(y);
};

fn("string", 2);

可索引接口

不常用,用于描述哪些能够”通过索引得到”的类型(数组和对象),比如 a[10]、b[“name”]

1
2
3
4
5
6
7
type A = {
// 索引签名:index是索引,number是索引的类型,string是索引对应的值的类型
[index: number]: string;
};

let a: A = ["string1", "string2"];
console.log(a);

类类型接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface X {
name: string;
eat(food: string): string; // 虽然定义了参数,但是其实并不限制参数,只限制返回类型
}

class A implements X {
public name: string;
constructor(name: string) {
this.name = name;
}
eat() {
console.log(123);
return "food";
}
}

let a = new A("lujinkai");
a.eat();

接口继承接口

1
2
3
4
5
6
7
8
9
10
11
interface A {
name: string;
}

interface B extends A {
age: number;
}

let b: B = { name: "lujinkai", age: 23 };

console.log(b);

接口继承类

接口不仅可以继承接口,还可以继承类。

示例:

1
2
3
4
5
6
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}

类静态部分和实例部分

ts 中,类分静态部分实例部分,new 实例化对象后,对象中有的部分就是实例化的部分,对象中没有的部分就是静态部分,构造函数 constructor 就是静态部分。

类类型接口只检查实例部分,不检查静态部分,如果想要检查静态部分,可以使用类表达式

示例:

1
2
3
4
5
6
7
8
9
10
11
12
interface ClockConstructor {
new (hour: number, minute: number): void;
}
interface ClockInterface {
tick(): void;
}
const Clock: ClockConstructor = class Clock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("beep beep");
}
};

函数

TypeScript 为 JavaScript 函数添加了额外的功能,让我们可以更容易地使用。

可选参数

1
2
3
4
function buildName(firstName: string, lastName?: string) {
if (lastName) return firstName + " " + lastName;
else return firstName;
}

默认参数

1
2
3
function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}

剩余参数

1
2
3
4
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;

函数类型

1
2
3
4
// myAdd 是函数名
// (x:number, y:number) => number 是函数类型
// (x: number, y: number): number { return x + y; } 是函数
let myAdd:(x:number, y:number) => number = (x: number, y: number): number { return x + y; };

注意:不要混淆了 TypeScript 中的 => 和 ES6 中的 =>,在 TypeScript 的类型定义中,=> 用来表示函数的定义,=> 的左边是输入类型,需要用括号括起来,右边是输出类型

上例还可以写做:

1
2
3
type AddFunc =  (x:number, y:number) => number

let myAdd:AddFunc = (x: number, y: number): number { return x + y; };

或者:

1
2
3
4
5
interface AddFunc {
(x:number, y:number):number
}

let myAdd:AddFunc = (x: number, y: number): number { return x + y; };

this

略。。。

重载

略。。。

字面量类型

js 基础复习

什么是构造函数

在 JavaScript 中,用 new 关键字来调用的函数,称为构造函数。构造函数首字母一般大写。

es6 的 class 类本质上是对构造函数的封装,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// es6 class类
class Greeter {
constructor(message) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
console.log(greeter.greet());

// 以上写法本质上和下面是一样的
function Greeter(message) {
this.greeting = message;
}
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};

let greeter = new Greeter("world"); // Greeter就是构造函数
console.log(greeter.greet());

js 中的 this 指向

  • 函数中的 this:window
  • 方法中的 this:调用他的对象
  • 构造函数中的 this:通过构造函数创建的实例
  • 事件处理函数中的 this:触发事件的对象
  • 定时器执行的 function 中的 this:window

pubic、private、protected

1
2
class A {}
class B {}

B 继承 A,A 是基类,B 是派生类,基类通常被称为超类,派生类通常被称作子类,子类如果有构造函数,它必须要调用super()

  • public:类成员(属性和方法)默认是 publc
  • private:当成员被标记成 private 时,它就不能在声明它的类的外部访问
  • protected:protected 修饰符与 private 修饰符的行为很相似,但有一点不同, protected 成员在派生类中仍然可以访问

readonly 修饰符

可以使用 readonly 关键字将属性设置为只读。 只读属性必须在声明时或构造函数里被初始化。

参数属性

存取器

首先,存取器要求你将编译器设置为输出 ECMAScript 5 或更高。 不支持降级到 ECMAScript 3。 其次,只带有 get 不带有 set 的存取器自动被推断为 readonly 。

静态属性

静态属性存在于类本身上面而不是类的实例上,关键字 static 定义静态属性,实例属性使用 this 访问,静态属性通过类名调用。

抽象类

关键字 abstract 定义抽象类和在抽象类内部定义抽象方法。

示例:

1
2
3
4
5
6
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log("roaming the earth...");
}
}

高级技巧

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Greeter {
static standardGreeting = "Hello, there";
greet() {
return Greeter.standardGreeting;
}
}
let greeter1: Greeter = new Greeter();
console.log(greeter1.greet()); // Hello, there

// `typeof Greeter`取Greeter类的类型,而不是实例的类型。"告诉我Greeter标识符的类型",也就是构造函数的类型
// 这个类型包含了类的所有静态成员和构造函数
let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";

let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet()); // Hey there!

把类当做接口使用

1

直播协议

RTMP

HLS

HTTP-FLV

1
2
3
4
http://192.168.0.12:5056/udp/224.0.2.26:24012
http://192.168.0.12:5056/rtp/224.0.2.26:24012
http://192.168.0.15:5056/rtp/224.0.2.26:24055
http://192.168.0.15:5056/status/

metrics Server 提供核心监控指标,比如 Node 节点的 CPU 和内存使用率,其他的监控交由 Prometheus 组件完成

简介

官网:https://prometheus.io/docs/introduction/overview/
github:https://github.com/prometheus

prometheus 是基于 go 语言开发的一套开源的监控、报警和时间序列数据库的组合,是由 SoundCloud 公司开发的
开源监控系统,prometheus 是 CNCF(Cloud Native Computing Foundation,云原生计算基金会)继 kubernetes 之后毕业的第二个项目,prometheus 在容器和微服务领域中得到了广泛的应用,其特点主要如下:

1
2
3
4
5
6
7
- 使用 key-value 的多维度格式保存数据
- 数据不使用 MySQL 这样的传统数据库,而是使用时序数据库,目前是使用的 TSDB
- 支持第三方 dashboard 实现更高的图形界面,如 grafana(Grafana 2.5.0版本及以上)
- 功能组件化
- 不需要依赖存储,数据可以本地保存也可以远程保存
- 服务自动化发现
- 强大的数据查询语句功(PromQL:Prometheus Query Language)

TSDB

TSDB:Time Series Database,时间序列数据库,简称时序数据库

什么是时序数据库?顾名思义,就是存储与时间相关的数据,该数据是在时间上分布的一系列值

时序数据库被广泛应用于物联网和运维监控系统

这个网站可以查看各种时序数据库的排名:

1
2
3
4
5
6
7
小而精,性能高,数据量较小(亿级): InfluxDB
简单,数据量不大(千万级),有联合查询、关系型数据库基础:timescales
数据量较大,大数据服务基础,分布式集群需求: opentsdb、KairosDB
分布式集群需求,olap实时在线分析,资源较充足:druid
性能极致追求,数据冷热差异大:Beringei
兼顾检索加载,分布式聚合计算: elsaticsearch
如果你兼具索引和时间序列的需求。那么Druid和Elasticsearch是最好的选择。其性能都不差,同时满足检索和时间序列的特性,并且都是高可用容错架构。

阿里云版 TSDB,文档写的还不错:https://help.aliyun.com/product/54825.html

Prometheus 内置了 TSDB,目前是 V3.0 版本,是一个独立维护的 TSDB 开源项目;在单机上,每秒可处理数百万个样本

Prometheus TSDB 数据存储格式:

  • 以每 2 小时为一个时间窗口,并存储为一个单独的 block
  • block 会压缩、合并历史数据块,随着压缩合并,其 block 数量会减少
  • block 的大小并不固定,但最小会保存两个小时的数据

P137

PromQL

https://songjiayang.gitbooks.io/prometheus/content/promql/summary.html

PromQL:Prometheus Query Language,是 Prometheus 自己开发的数据查询 DSL 语言,语言表现力非常丰富,内置函数很多,在日常数据可视化以及 rule 告警中都会使用到它

系统架构

1
2
3
4
5
6
prometheus server:主服务,接受外部http请求,收集、存储与查询数据等
prometheus targets: 静态收集的目标服务数据
service discovery:动态发现服务
prometheus alerting:报警通知
push gateway:数据收集代理服务器(类似于zabbix proxy)
data visualization and export: 数据可视化与数据导出(访问客户端)

安装

三种安装方式:docker、二进制、operator

1
2
3
https://prometheus.io/download/ #官方二进制下载及安装,prometheus server的监听端口为9090
https://prometheus.io/docs/prometheus/latest/installation/ #docker镜像直接启动
https://github.com/coreos/kube-prometheus #operator部署

一般不使用 docker 安装,推荐 operator,但是 operator 安装会隐藏很多细节,所以要先学习二进制安装

1
2
3
4
5
6
7
8
9
10
11
https://github.com/prometheus/prometheus/releases/download/v2.25.2/prometheus-2.25.2.linux-amd64.tar.gz
https://github.com/prometheus/alertmanager/releases/download/v0.21.0/alertmanager-0.21.0.linux-amd64.tar.gz
https://github.com/prometheus/blackbox_exporter/releases/download/v0.18.0/blackbox_exporter-0.18.0.linux-amd64.tar.gz
https://github.com/prometheus/consul_exporter/releases/download/v0.7.1/consul_exporter-0.7.1.linux-amd64.tar.gz
https://github.com/prometheus/graphite_exporter/releases/download/v0.9.0/graphite_exporter-0.9.0.linux-amd64.tar.gz
https://github.com/prometheus/haproxy_exporter/releases/download/v0.12.0/haproxy_exporter-0.12.0.linux-amd64.tar.gz
https://github.com/prometheus/memcached_exporter/releases/download/v0.8.0/memcached_exporter-0.8.0.linux-amd64.tar.gz
https://github.com/prometheus/mysqld_exporter/releases/download/v0.12.1/mysqld_exporter-0.12.1.linux-amd64.tar.gz
https://github.com/prometheus/node_exporter/releases/download/v1.1.2/node_exporter-1.1.2.linux-amd64.tar.gz
https://github.com/prometheus/pushgateway/releases/download/v1.4.0/pushgateway-1.4.0.linux-amd64.tar.gz
https://github.com/prometheus/statsd_exporter/releases/download/v0.20.0/statsd_exporter-0.20.0.linux-amd64.tar.gz

prometheus server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@k8s-master src]$tar zxf prometheus-2.25.2.linux-amd64.tar.gz
[root@k8s-master src]$mv prometheus-2.25.2.linux-amd64 /usr/local/prometheus
[root@k8s-master src]$cd /usr/local/prometheus/

# service启动文件
[root@k8s-master prometheus]$vim /lib/systemd/system/prometheus.service
[Unit]
Description=Prometheus Server
Documentation=https://prometheus.io/docs/introduction/overview/
After=network.target
[Service]
Restart=on-failure
WorkingDirectory=/usr/local/prometheus/
ExecStart=/usr/local/prometheus/prometheus --
config.file=/usr/local/prometheus/prometheus.yml
[Install]
WantedBy=multi-user.target

[root@k8s-master prometheus]$systemctl daemon-reload
[root@k8s-master prometheus]$systemctl start prometheus.service

node exporter

收集各 k8s node 节点上的监控指标数据,监听端口为 9100

node exporter 和 metrics server

原理都是一样的,都是从 node 的 kublete 获取数据,但是 node exporter 从本机的 kubelet 获取数据,metrics server 从所有 node 的 kubelet 获取数据,所以 node exporter 需要在每个 node 都部署一份,而 metrics server 只部署一个就可以

安装:

在每个 node 都执行以下安装步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$tar zxf node_exporter-1.1.2.linux-amd64.tar.gz
$mv node_exporter-1.1.2.linux-amd64 /usr/local/node_exporter
$cd /usr/local/node_exporter/

$vim /etc/systemd/system/node-exporter.service
[Unit]
Description=Prometheus Node Exporter
After=network.target
[Service]
ExecStart=/usr/local/node_exporter/node_exporter
[Install]
WantedBy=multi-user.target

$systemctl daemon-reload
$systemctl start node-exporter.service

$ss -ntlp | grep 9100
LISTEN 0 32768 *:9100 *:* users:(("node_exporter",pid=115111,fd=3))

与 prometheus 集成:

在 prometheus server 中添加一个 job:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@k8s-master prometheus]$pwd
/usr/local/prometheus
[root@k8s-master prometheus]$grep -v "#" prometheus.yml | grep -v "^$"
global:
alerting:
alertmanagers:
- static_configs:
- targets:
rule_files:
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'promethues-node' # job名称
metrics_path: '/metrics' # 默认就是 /metrics
static_configs:
- targets: ['10.0.1.31:9100', '10.0.1.32:9100', '10.0.1.33:9100'] # 被采集的node

重启 prometheus ,等一会然后观察监控情况:

haproxy exporter

除了 node_exporter,官方还提供了监控 haproxy 的 exporter

  1. 需要将其部署在 haproxy 服务所在的节点

    1
    tar xvf haproxy_exporter-0.12.0.linux-amd64.tar.gz
  2. prometheus 添加 haproxy 数据采集

    1
    2
    3
    - job_name: "prometheus-haproxy"
    static_configs:
    - targets: ["192.168.7.108:9101"]
  3. grafana 添加模板

    367 2428

其他 exporter

如果官方没有提供监控相应服务的 exporter,需要使用第三方或自己开发

https://github.com/zhangguanzhang/harbor_exporter
https://github.com/nginxinc/nginx-prometheus-exporter/

Grafana

调用 prometheus 的数据,进行更专业的可视化

cAdvisor

https://www.oschina.net/p/cadvisor

cAdvisor 是由谷歌开源的 docker 容器性能分析工具,cAdvisor 可以对节点机器上的资源及容器进行实时监控和性能数据采集,包括 CPU 使用情况、内存使用情况、网络吞吐量及文件系统使用情况,cAdvisor 不仅可以搜集一台机器上所有运行的容器信息,还提供基础查询界面和 http 接口,方便其他组件如 prometheus 进行数据抓取

k8s1.12 之前 cadvisor 集成在 node 节点的上 kubelet 服务中,从 1.12 版本开始分离为两个组件,因此需要在 node 节点单独部署 cadvisor

官方 github 给出部署方法,可是需要 pull 国外的镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# cadvisor镜像准备
$docker load -i cadvisor_v0.36.0.tar.gz
$docker tag gcr.io/google_containers/cadvisor:v0.36.0 harbor.ljk.local/k8s/cadvisor:v0.36.0
$docker push harbor.ljk.local/k8s/cadvisor:v0.36.0

# 在每个节点 启动cadvisor容器
VERSION=v0.36.0
sudo docker run \
--volume=/:/rootfs:ro \
--volume=/var/run:/var/run:ro \
--volume=/sys:/sys:ro \
--volume=/var/lib/docker/:/var/lib/docker:ro \
--volume=/dev/disk/:/dev/disk:ro \
--publish=8080:8080 \
--detach=true \
--name=cadvisor \
--privileged \
--device=/dev/kmsg \
harbor.ljk.local/k8s/cadvisor:${VERSION}

prometheus 采集 cadvisor 数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@k8s-master prometheus]$grep -v "#" prometheus.yml | grep -v "^$"
global:
alerting:
alertmanagers:
- static_configs:
- targets:
rule_files:
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'promethues-node'
metrics_path: '/metrics'
static_configs:
- targets: ['10.0.1.31:9100', '10.0.1.32:9100', '10.0.1.33:9100']
- job_name: 'promethues-containers'
metrics_path: '/metrics'
static_configs:
- targets: ['10.0.1.32:8080', '10.0.1.33:8080'] # cadvisor采集的node

grafana 添加 pod 监控模板

395 893 容器模板 ID

prometheus 报警设置

prometheus 触发一条告警的过程:

1
2
3
4
5
prometheus-->触发阈值-->超出持续时间-->alertmanager-->分组|抑制|静默-->媒体类型-->邮件|钉钉|微信等

分组(group): 将类似性质的警报合并为单个通知
静默(silences): 是一种简单的特定时间静音的机制,例如:服务器要升级维护可以先设置这个时间段告警静默
抑制(inhibition): 当警报发出后,停止重复发送由此警报引发的其他警报即合并一个故障引起的多个报警事件,可以消除冗余告警

安装 alertmanager

1
2
3
[root@k8s-master src]$tar zxf alertmanager-0.21.0.linux-amd64.tar.gz
[root@k8s-master src]$mv alertmanager-0.21.0.linux-amd64 /usr/local/alertmanager
[root@k8s-master src]$cd /usr/local/alertmanager/

配置 alertmanager

官方配置文档:https://prometheus.io/docs/alerting/configuration/

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
[root@k8s-master alertmanager]$cat alertmanager.yml
global:
resolve_timeout: 5m
smtp_smarthost: "smtp.qq.com:465"
smtp_from: "441757636@qq.com"
smtp_auth_username: "441757636@qq.com"
smtp_auth_password: "udwthuyxjstcdhcj" # 授权码随便写的
smtp_hello: "@qq.com"
smtp_require_tls: false
route: #设置报警的分发策略
group_by: ["alertname"] #采用哪个标签来作为分组依据
group_wait: 10s #组告警等待时间。也就是告警产生后等待10s,如果有同组告警一起发出
group_interval: 10s #两组告警的间隔时间
repeat_interval: 2m #重复告警的间隔时间,减少相同邮件的发送频率
receiver: "web.hook" #设置接收人,这个接收人在下面会有定义
receivers: # 定义接收人
- name: "web.hook" # 定义接收人:web.hook,这里定义为接收人是一个邮箱
#webhook_configs:
#- url: 'http://127.0.0.1:5001/'
email_configs:
- to: "2973707860@qq.com"
inhibit_rules: #禁止的规则
- source_match: #源匹配级别
severity: "critical"
target_match:
severity: "warning"
equal: ["alertname", "dev", "instance"]

启动 alertmanager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 二进制启动
$./alertmanager --config.file=./alertmanager.yml

# service启动
$vim /lib/systemd/system/alertmanager.service
[Unit]
Description=Prometheus Server
Documentation=https://prometheus.io/docs/introduction/overview/
After=network.target
[Service]
Restart=on-failure
WorkingDirectory=/usr/local/prometheus/
ExecStart=/usr/local/prometheus/prometheus --config.file=/usr/local/prometheus/prometheus.yml
[Install]
WantedBy=multi-user.target

$lsof -i:9093 # 监听9093端口
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
alertmana 53151 root 9u IPv6 195688 0t0 TCP *:9093 (LISTEN)

报警规则文件示例

1
2
3
4
5
6
7
8
groups:
- name: # 警报规则组的名称
rules:
- alert: # 警报规则的名称
expr: # 使用PromQL表达式完成的警报触发条件,用于计算是否有满足触发条件
for: # 触发报警条件,等一段时间(pending)再报警
labels: # 自定义标签,允许自行定义标签附加在警报上
annotations: # 设置有关警报的一组描述信息,其中包括自定义的标签,以及expr计算后的值
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
[root@k8s-master prometheus]$pwd
/usr/local/prometheus
[root@k8s-master prometheus]$vim rule.yml
groups:
- name: linux_pod.rules
rules:
- alert: Pod_all_cpu_usage
expr: (sum by(name)(rate(container_cpu_usage_seconds_total{image!=""}[5m]))*100) > 75
for: 5m
labels:
severity: critical
service: pods
annotations:
description: 容器 {{ $labels.name }} CPU 资源利用率大于 75% , (current value is {{ $value }})
summary: Dev CPU 负载告警
- alert: Pod_all_memory_usage
expr: sort_desc(avg by(name)(irate(container_memory_usage_bytes{name!=""}[5m]))*100) > 1024*10^3*2
for: 10m
labels:
severity: critical
annotations:
description: 容器 {{ $labels.name }} Memory 资源利用率大于 2G , (current value is {{ $value }})
summary: Dev Memory 负载告警
- alert: Pod_all_network_receive_usage
expr: sum by (name)(irate(container_network_receive_bytes_total{container_name="POD"}[1m])) > 1024*1024*50
for: 10m
labels:
severity: critical
annotations:
description: 容器 {{ $labels.name }} network_receive 资源利用率大于 50M , (current value is {{ $value }})

配置 prometheus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$cat prometheus.yml
...
# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets:
- 10.0.1.31:9093 #alertmanager地址

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
# - "first_rules.yml"
- "/usr/local/prometheus/rule.yml" #指定规则文件

...

# 验证报警规则
[root@k8s-master prometheus]$./promtool check rules ./rule.yml
Checking ./rule.yml
SUCCESS: 3 rules found
# 重启prometheus
[root@k8s-master prometheus]$systemctl restart prometheus

部署

1
kubectl apply -f nginx.yaml --record=true # --record=true为记录执行的kubectl

滚动更新

滚动更新策略 spec.strategy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: Enter deployment name
spec:
strategy:
rollingUpdate:
maxSurge: 1 # 一次性最多添加多少pod
maxUnavailable: 1 # 不可用Pod的数量最多是多少
type: RollingUpdate # 指定更新策略:滚动更新
replicas: Enter the number of replicas
template:
metadata:
labels:
editor: vscode
spec:
containers:
- name: name
image: Enter containers image

更新策略有两种:

  • RollingUpdate :默认的更新策略,表示滚动更新
  • Recreate:重建,会终止所有正在运行的实例,然后用较新的版本来重新创建它们,即在创建新 Pods 之前,所有现有的 Pods 会被杀死,测试环境可以使用

Pod 有多个副本,滚动更新的过程就是轮流更新 pod,直至所有 pod 都更新成功,至于如何轮流更新,取决于 maxSurge 和 maxUnavailable 这两个参数,示例:

1
2
3
# 如果Pod有5个
maxSurge: 1 # 每次添加1个新版本Pod,启动成功后删除一个旧版本Pod
maxUnavailable: 0 # 不允许有不可用的Pod,即更新过程中始终保持5个Pod正常运行

升级命令 kubectl set

1
2
3
4
5
6
7
kubectl set image (-f FILENAME | TYPE NAME) CONTAINER_NAME_1=CONTAINER_IMAGE_1 ... CONTAINER_NAME_N=CONTAINER_IMAGE_N [options]

# 示例
kubectl set image deployment/nginx busybox=busybox nginx=nginx:1.9.1
kubectl set image deployments,rc nginx=nginx:1.9.1 --all
kubectl set image daemonset abc *=nginx:1.9.1
kubectl set image -f path/to/file.yaml nginx=nginx:1.9.1 --local -o yaml

除了使用 kubectl get 命令升级,还可以直接 kubectl apply -f 升级,但是需要先手动修改 yaml 文件,所以不推荐使用

回滚

回滚到上一个版本

1
kubectl rollout undo deployment/nginx-deployment -n linux36

回滚到指定版本

1
2
3
4
5
6
7
8
9
10
# 查看当前版本号
kubectl rollout history deployment/nginx-deployment -n linux36
deployment.extensions/nginx-deployment
REVISION CHANGE-CAUSE
1 kubectl apply --filename=nginx.yaml --record=true
3 kubectl apply --filename=nginx.yaml --record=true
4 kubectl apply --filename=nginx.yaml --record=true

# 回滚到指定的1版本
kubectl rollout undo deployment/nginx-deployment --to-revision=1 -n linux36

Jenkins 持续继承与部署

参考以下脚本:

1
2
3
4
5
6
$cat build-command.sh

#!/bin/bash
docker build -t harbor.magedu.local/pub-images/nginx-base-wordpress:v1.14.2 .
sleep 1
docker push harbor.magedu.local/pub-images/nginx-base-wordpress:v1.14.2
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
$cat linux36-ningx-deploy.sh

#!/bin/bash
#Author: ZhangShiJie
#Date: 2019-08-03
#Version: v1
#记录脚本开始执行时间
starttime=$(date +'%Y-%m-%d %H:%M:%S')
#变量
SHELL_DIR="/root/scripts"
SHELL_NAME="$0"
K8S_CONTROLLER1="192.168.7.101"
K8S_CONTROLLER2="192.168.7.102"
DATE=$(date +%Y-%m-%d_%H_%M_%S)
METHOD=$1
Branch=$2
if test -z $Branch; then
Branch=develop
fi
function Code_Clone() {
Git_URL="git@172.20.100.1:linux36/app1.git"
DIR_NAME=$(echo ${Git_URL} | awk -F "/" '{print $2}' | awk -F "." '{print $1}')
DATA_DIR="/data/gitdata/linux36"
Git_Dir="${DATA_DIR}/${DIR_NAME}"
cd ${DATA_DIR} && echo "即将清空上一版本代码并获取当前分支最新代码" && sleep 1 && rm -rf
${DIR_NAME}
echo "即将开始从分支${Branch} 获取代码" && sleep 1
git clone -b ${Branch} ${Git_URL}
echo "分支${Branch} 克隆完成,即将进行代码编译!" && sleep 1
#cd ${Git_Dir} && mvn clean package
#echo "代码编译完成,即将开始将IP地址等信息替换为测试环境"
#####################################################
sleep 1
cd ${Git_Dir}
tar czf ${DIR_NAME}.tar.gz ./*
}

#将打包好的压缩文件拷贝到k8s 控制端服务器
function Copy_File() {
echo "压缩文件打包完成,即将拷贝到k8s 控制端服务器${K8S_CONTROLLER1}" && sleep 1
scp ${Git_Dir}/${DIR_NAME}.tar.gz root@${K8S_CONTROLLER1}:/opt/k8s-
data/dockerfile/linux36/nginx/
echo "压缩文件拷贝完成,服务器${K8S_CONTROLLER1}即将开始制作Docker 镜像!" && sleep 1
}

#到控制端执行脚本制作并上传镜像
function Make_Image() {
echo "开始制作Docker镜像并上传到Harbor服务器" && sleep 1
ssh root@${K8S_CONTROLLER1} "cd /opt/k8s-data/dockerfile/linux36/nginx && bash build-
command.sh ${DATE}"
echo "Docker镜像制作完成并已经上传到harbor服务器" && sleep 1
}

#到控制端更新k8s yaml文件中的镜像版本号,从而保持yaml文件中的镜像版本号和k8s中版本号一致
function Update_k8s_yaml() {
echo "即将更新k8s yaml文件中镜像版本" && sleep 1
ssh root@${K8S_CONTROLLER1} "cd /opt/k8s-data/yaml/linux36/nginx && sed -i 's/image:
harbor.magedu.*/image: harbor.magedu.net\/linux36\/nginx-web1:${DATE}/g' nginx.yaml"
echo "k8s yaml文件镜像版本更新完成,即将开始更新容器中镜像版本" && sleep 1
}

#到控制端更新k8s中容器的版本号,有两种更新办法,一是指定镜像版本更新,二是apply执行修改过的yaml文件
function Update_k8s_container() {
#第一种方法
ssh root@${K8S_CONTROLLER1} "kubectl set image deployment/linux36-nginx-deployment linux36-nginx-container=harbor.magedu.net/linux36/nginx-web1:${DATE} -n linux36"
#第二种方法,推荐使用第一种
#ssh root@${K8S_CONTROLLER1} "cd /opt/k8s-data/yaml/web-test/tomcat-app1 && kubectl apply -f web-test.yam --record"
echo "k8s 镜像更新完成" && sleep 1
echo "当前业务镜像版本: harbor.magedu.net/linux36/nginx-web1:${DATE}"
#计算脚本累计执行时间,如果不需要的话可以去掉下面四行
endtime=$(date +'%Y-%m-%d %H:%M:%S')
start_seconds=$(date --date="$starttime" +%s)
end_seconds=$(date --date="$endtime" +%s)
echo "本次业务镜像更新总计耗时:"$((end_seconds - start_seconds))"s"
}

#基于k8s 内置版本管理回滚到上一个版本
function rollback_last_version() {
echo "即将回滚之上一个版本"
ssh root@${K8S_CONTROLLER1} "kubectl rollout undo deployment/linux36-nginx-deployment -n linux36"
sleep 1
echo "已执行回滚至上一个版本"
}

#使用帮助
usage() {
echo "部署使用方法为 ${SHELL_DIR}/${SHELL_NAME} deploy "
echo "回滚到上一版本使用方法为 ${SHELL_DIR}/${SHELL_NAME} rollback_last_version"
}

#主函数
main() {
case ${METHOD} in
deploy)
Code_Clone
Copy_File
Make_Image
Update_k8s_yaml
Update_k8s_container
;;
rollback_last_version)
rollback_last_version
;;
*)
usage
;;
esac
}

main $1 $2 $3

代码升级和回滚

代码升级:Jenkins 负责 pull 最新代码,编译成 war 包(如果需要),然后将编译后的包发送到 k8s 或者专门负责制作镜像的服务器,使用 dockerfile 制作完新镜像后,就上传到 harbor,最后再由 kubectl 执行更新镜像的操作

代码回滚:直接回滚镜像即可

Pod 的生命周期:https://kubernetes.io/zh/docs/concepts/workloads/pods/pod-lifecycle/

Pod 状态

  1. 第一阶段

    1
    2
    3
    4
    Pending  # 正在创建Pod但是Pod中的容器还没有全部被创建完成,处于此状态的Pod应该检查Pod依赖的存储是否有权限挂载、镜像是否可以下载、调度是否正常等
    Failed # Pod中有容器启动失败而导致pod工作异常
    Unknown # 由于某种原因无法获得pod的当前状态,通常是由于与pod所在的node节点通信错误
    Succeeded # Pod中的所有容器都被成功终止即pod里所有的containers均已terminated
  2. 第二阶段

    1
    2
    3
    4
    5
    6
    Unschedulable  # Pod不能被调度,kube-scheduler没有匹配到合适的node节点
    PodScheduled # pod正处于调度中,在kube-scheduler刚开始调度的时候,还没有将pod分配到指定的node,在筛选出合适的节点后就会更新etcd数据,将pod分配到指定的node
    Initialized # 所有pod中的初始化容器已经完成了
    ImagePullBackOff # Pod所在的node节点下载镜像失败
    Running # Pod内部的容器已经被创建并且启动
    Ready # 表示pod中的容器已经可以提供访问服务
  3. 第三阶段

    1
    2
    Succeeded
    Failed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Error                      #pod 启动过程中发生错误
NodeLost #Pod 所在节点失联
Unkown #Pod 所在节点失联或其它未知异常
Waiting #Pod 等待启动
Pending #Pod 等待被调度
Terminating: #Pod 正在被销毁
CrashLoopBackOff #pod,但是kubelet正在将它重启
InvalidImageName #node节点无法解析镜像名称导致的镜像无法下载
ImageInspectError #无法校验镜像,镜像不完整导致
ErrImageNeverPull #策略禁止拉取镜像,镜像中心权限是私有等
ImagePullBackOff #镜像拉取失败,但是正在重新拉取
RegistryUnavailable #镜像服务器不可用,网络原因或harbor宕机
ErrImagePull #镜像拉取出错,超时或下载被强制终止
CreateContainerConfigError #不能创建kubelet使用的容器配置
CreateContainerError #创建容器失败
PreStartContainer #执行preStart hook报错,Pod hook(钩子)是由 Kubernetes 管理的 kubelet 发起的,当容器中的进程启动前或者容器中的进程终止之前运行,比如容器创建完成后里面的服务启动之前可以检查一下依赖的其它服务是否启动,或者容器退出之前可以把容器中的服务先通过命令停止。
PostStartHookError #执行 postStart hook 报错
RunContainerError #pod运行失败,容器中没有初始化PID为1的守护进程等
ContainersNotInitialized #pod没有初始化完毕
ContainersNotReady #pod没有准备完毕
ContainerCreating #pod正在创建中
PodInitializing #pod正在初始化中
DockerDaemonNotReady #node节点decker服务没有启动
NetworkPluginNotReady #网络插件还没有完全启动

Pod 调度过程

参考:kube-scheduler

Pod 探针

https://kubernetes.io/zh/docs/concepts/workloads/pods/pod-lifecycle/#container-probes

探针是 kubelet 对容器执行的定期诊断,以保证 Pod 的状态始终处于运行状态,要执行诊断,kubelet 调用由容器实现的 Handler(处理程序),有三种类型的处理程序:

1
2
3
ExecAction      #在容器内执行指定命令,如果命令退出时返回码为0则认为诊断成功
TCPSocketAction #对指定端口上的容器的IP地址进行TCP检查,如果端口打开,则诊断被认为是成功的
HTTPGetAction #对指定的端口和路径上的容器的IP地址执行HTTPGet请求,如果响应的状态码大于等于200且小于 400,则诊断被认为是成功的

每次探测都将获得以下三种结果之一:

1
2
3
成功:容器通过了诊断
失败:容器未通过诊断
未知:诊断失败,因此不会采取任何行动

探针类型

1
2
3
livenessProbe # 存活探针,检测容器容器是否正在运行,如果存活探测失败,则kubelet会杀死容器,并且容器将受到其重启策略的影响,如果容器不提供存活探针,则默认状态为 Success,livenessProbe用于控制是否重启pod

readinessProbe # 就绪探针,如果就绪探测失败,端点控制器将从与Pod匹配的所有Service的端点中删除该Pod的IP地址,初始延迟之前的就绪状态默认为Failure(失败),如果容器不提供就绪探针,则默认状态为 Success,readinessProbe用于控制pod是否添加至service

livenessProbe 和 readinessProbe 的对比:

1
2
3
4
5
6
7
1. 配置参数一样
2. livenessProbe用于控制是否重启pod,readinessProbe用于控制pod是否添加至service
3. livenessProbe连续探测失败会重启、重建pod,readinessProbe不会执行重启或者重建Pod操作
4. livenessProbe连续检测指定次数失败后会将容器置于(Crash Loop BackOff)且不可用,readinessProbe不会
5. readinessProbe 连续探测失败会从service的endpointd中删除该Pod,livenessProbe不具备此功能,但是会将容器挂起livenessProbe

建议:两个探针都配置

探针配置

探针有很多配置字段,可以使用这些字段精确的控制存活和就绪检测的行为:

1
2
3
4
5
initialDelaySeconds: 120 # 初始化延迟时间,告诉kubelet在执行第一次探测前应该等待多少秒,默认是0秒,最小值是0
periodSeconds: 60 # 探测周期间隔时间,指定了kubelet应该每多少秒秒执行一次存活探测,默认是10秒。最小值是1
timeoutSeconds: 5 # 单次探测超时时间,探测的超时后等待多少秒,默认值是1秒,最小值是1
successThreshold: 1 # 从失败转为成功的重试次数,探测器在失败后,被视为成功的最小连续成功数,默认值是1,存活探测的这个值必须是1,最小值是 1
failureThreshold: 3 # 从成功转为失败的重试次数,当Pod启动了并且探测到失败,Kubernetes的重试次数,存活探测情况下的放弃就意味着重新启动容器,就绪探测情况下的放弃Pod 会被打上未就绪的标签,默认值是3,最小值是1

HTTP 探测器可以在 httpGet 上配置额外的字段:

1
2
3
4
5
host:                     #连接使用的主机名,默认是Pod的 IP,也可以在HTTP头中设置 "Host" 来代替
scheme: http #用于设置连接主机的方式(HTTP 还是 HTTPS),默认是 HTTP
path: /monitor/index.html #访问 HTTP 服务的路径
httpHeaders: #请求中自定义的 HTTP 头,HTTP 头字段允许重复
port: 80 #访问容器的端口号或者端口名,如果数字必须在 1 ~ 65535 之间

HTTP 探针示例

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
#apiVersion: extensions/v1beta1
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 1
selector:
matchLabels: #rs or deployment
app: ng-deploy-80
#matchExpressions:
# - {key: app, operator: In, values: [ng-deploy-80,ng-rs-81]}
template:
metadata:
labels:
app: ng-deploy-80
spec:
containers:
- name: ng-deploy-80
image: nginx:1.17.5
ports:
- containerPort: 80
#readinessProbe:
livenessProbe:
httpGet:
#path: /monitor/monitor.html
path: /index.html # 探测的url
port: 80
initialDelaySeconds: 5 # 等五秒才开始第一次检测
periodSeconds: 3 # 探测周期
timeoutSeconds: 5 # 单次探测超时时间
successThreshold: 1 # 处于失败中,探测成功一次就算成功
failureThreshold: 3 # 出于成功中,探测失败三次才算失败
---
apiVersion: v1
kind: Service
metadata:
name: ng-deploy-80
spec:
ports:
- name: http
port: 81
targetPort: 80
nodePort: 40012
protocol: TCP
type: NodePort
selector:
app: ng-deploy-80

TCP 探针示例

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
#apiVersion: extensions/v1beta1
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 1
selector:
matchLabels: #rs or deployment
app: ng-deploy-80
#matchExpressions:
# - {key: app, operator: In, values: [ng-deploy-80,ng-rs-81]}
template:
metadata:
labels:
app: ng-deploy-80
spec:
containers:
- name: ng-deploy-80
image: nginx:1.17.5
ports:
- containerPort: 80
livenessProbe: #或readinessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 5
periodSeconds: 3
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: ng-deploy-80
spec:
ports:
- name: http
port: 81
targetPort: 80
nodePort: 40012
protocol: TCP
type: NodePort
selector:
app: ng-deploy-80

ExecAction 探针

基于指定的命令对 Pod 进行特定的状态检查

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
#apiVersion: extensions/v1beta1
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-deployment
spec:
replicas: 1
selector:
matchLabels: #rs or deployment
app: redis-deploy-6379
#matchExpressions:
# - {key: app, operator: In, values: [redis-deploy-6379,ng-rs-81]}
template:
metadata:
labels:
app: redis-deploy-6379
spec:
containers:
- name: redis-deploy-6379
image: redis
ports:
- containerPort: 6379
#readinessProbe:
livenessProbe:
exec:
command: # 基于指定的命令对Pod进行特定的状态检查
#- /apps/redis/bin/redis-cli
- /usr/local/bin/redis-cli
- quit
initialDelaySeconds: 5
periodSeconds: 3
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: redis-deploy-6379
spec:
type: NodePort
ports:
- name: http
port: 6379
targetPort: 6379
nodePort: 40016
protocol: TCP
selector:
app: redis-deploy-6379

Pod 重启策略

k8s 在 Pod 出现异常的时候会自动将 Pod 重启,以恢复 Pod 中的服务

1
2
3
4
restartPolicy:
Always # 当容器异常时,k8s自动重启该容器,ReplicationController/Replicaset/Deployment
OnFailure # 当容器失败时(容器停止运行且退出码不为0),k8s自动重启该容器
Never # 不论容器运行状态如何都不会重启该容器,Job或CronJob

镜像拉取策略

配置最佳实践 | Kubernetes

1
2
3
4
imagePullPolicy:
IfNotPresent # node节点没有此镜像就去指定的镜像仓库拉取,node有就使用node本地镜像
Always # 每次重建pod都会重新拉取镜像
Never # 从不到镜像中心拉取镜像,只使用本地镜像

HPA:Horizontal Pod Autoscaler,pod 的自动水平伸缩,特别适合无状态服务

HPA 会自动完成 pod 的扩缩容,当资源需求过高时,会自动创建出 pod 副本;当资源需求低时,会自动收缩 pod 副本数。

注意:通过集群内的资源监控系统(metrics-server),来获取集群中资源的使用状态,所以必须确保集群中已经安装 metrics-server 的组件

HPA 版本:

1
2
3
4
$kubectl api-versions | grep auto
autoscaling/v1 # 只支持通过cpu为参考依据,来改变pod副本数
autoscaling/v2beta1 # 支持通过cpu、内存、连接数以及用户自定义的资源指标数据为参考依据
autoscaling/v2beta2 # 同上,小的变动

metrics-server

概念

Metrics Server 是 Kubernetes 集群核心监控数据的聚合器,Metrics Server 从 Kubelet 收集资源指标,并通过 Merics API 在 Kubernetes APIServer 中提供给缩放资源对象 HPA 使用。也可以通过 Metrics API 提供的 Kubectl top 查看 Pod 资源占用情况,从而实现对资源的自动缩放。

Metrics Server 是 Kubernetes 监控组件中的重要一部分,Metrics Server 主要分为 API 和 Server 两大部分。

  • Metrics API:通过 APIServer 对外暴露 Pod 资源使用情况。为 HPA、kubectl top、Kubernetes dashboard 等提供数据来源
  • Metrics Server :定期通过 Summary API 从 Kubelet 所在集群节点获取服务指标,然后将指标汇总、存储到内存中,仅仅存储指标最新状态,一旦重启组件数据将会丢失

部署

最新的 metrics-server 是 v4.0.2,但是其依赖的镜像需要翻墙,而阿里云镜像和 mirrorgooglecontainers 中都没有最新版本,最新的只有 v0.3.6 版本,勉强用吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$docker pull registry.aliyuncs.com/google_containers/metrics-server-amd64:v0.3.6
$docker tag registry.aliyuncs.com/google_containers/metrics-server-amd64 harbor.ljk.local/k8s/metrics-server-amd64:v0.3.6
$docker push harbor.ljk.local/k8s/metrics-server-amd64:v0.3.6

# 下载 components.yaml
$wget https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.3.6/components.yaml
$vim components.yaml
...
image: harbor.ljk.local/k8s/metrics-server-amd64:v0.3.6 #修改为本地镜像
...
# 安装
$kubectl apply -f components.yaml

# 测试
$nohup kubectl proxy &
$curl http://localhost:8001/apis/metrics.k8s.io/v1beta1/nodes
$curl http://localhost:8001/apis/metrics.k8s.io/v1beta1/pods
$kubectl top node
$kubectl top pod -A

HPA 示例

要求:我有个 deployment 叫 myapp 现在只有一个副本数,最多只能 8 个副本数,当 pod 的 cpu 平均利用率超过百分之 50 或内存平均值超过百分之 50 时,pod 将自动增加副本数以提供服务

  1. SVC、Deployment 资源清单:

    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
    apiVersion: v1
    kind: Service
    metadata:
    name: svc-hpa
    namespace: default
    spec:
    selector:
    app: myapp
    type: NodePort ##注意这里是NodePort,下面压力测试要用到。
    ports:
    - name: http
    port: 80
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: myapp
    namespace: default
    spec:
    replicas: 1
    selector:
    matchLabels:
    app: myapp
    template:
    metadata:
    name: myapp-demo
    namespace: default
    labels:
    app: myapp
    spec:
    containers:
    - name: myapp
    image: ikubernetes/myapp:v1
    imagePullPolicy: IfNotPresent
    ports:
    - name: http
    containerPort: 80
    resources:
    requests:
    cpu: 50m
    memory: 50Mi
    limits:
    cpu: 50m
    memory: 50Mi
  2. HPA 资源清单:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    apiVersion: autoscaling/v2beta1
    kind: HorizontalPodAutoscaler
    metadata:
    name: myapp-hpa-v2
    namespace: default
    spec:
    minReplicas: 1 ##至少1个副本
    maxReplicas: 8 ##最多8个副本
    scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
    metrics:
    - type: Resource
    resource:
    name: cpu
    targetAverageUtilization: 50 ##注意此时是根据使用率,也可以根据使用量:targetAverageValue
    - type: Resource
    resource:
    name: memory
    targetAverageUtilization: 50 ##注意此时是根据使用率,也可以根据使用量:targetAverageValue