Comfortably Numb

反方向的钟

0%

es6 ~ 10

node

几种通用模式

  1. 单例模式
  2. 观察者模式
  3. 代理模式/装饰器模式

    作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 《JS二十年》
// ES2015

// 1. 三表达式内的作用域
// for (const p in x) {body} 的去糖后近似表示
// 循环中的每次迭代使用一个绑定,并在迭代之间传递值
// 对于 const p 来说,每次使用循环的唯一绑定就足够了,因此, p 不能被 for 头部或者循环体内的表达式修改
{ let $next;
for ($next in x) {
const p = $next;
{body}
}
}
// 2. 严格模式下禁止块级函数声明

其他示例代码

  1. reTryRequest: 接口轮询,一分钟内轮询直到成功,超时取消
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
const queryRequest = (data) => {
return new Promise((resolve, reject) => {
const res = data.map(v => v+'111')
reject(res)
})
}
const retry = (query, timeout = 30000, times = 5) => {
// 使用闭包保存 timer
let timer = null
return (data) => {
return new Promise((resolve, reject) => {
timer = setTimeout(() => {
query(data).then(res => {
clearTimeout(timer)
resolve(res)
}).catch(err => {
clearTimeout(timer)
reject(err)
})
}, timeout)
})
}
}
const longRequestRetry = async (data, timeout = 30000, times = 5) => {
try {
const res = await retry(queryRequest, timeout = 30000, times)(data)
console.log('res', res)
} catch(err) {
if (times > 0) {
console.log('times', times)
longRequestRetry(data, timeout, times - 1)
}
}
}
// 使用
const request = (data) => {
queryRequest(data).then(res => {

}).catch(err => {
longRequestRetry(data)
})
}
  1. generator or async/await 实现排序动画
  2. 设计一个 generator 状态机,和一个栈保留10个不同时间发出的异步任务,取消栈内除了栈顶的所有异步任务

debug

如何上报准确错误信息

不同类型的错误捕获

错误上报方式

错误处理

没有修改主题前的自动部署流程十分顺滑

1
2
3
4
5
6
# git:main 分支
$ hexo new "My New Post"
$ git add .
$ git commit msg
$ git push
# 剩下的交给travis.bot发布到gh-pages分支

一切变故来源于更换了更好看的主题

同样操作一遍,线上的直接404,在.travis.yml脚本中增加了 npm install,就此成功了一次,当第三次推送时又不行了,本地server完全OK,查看node版本,换成.travis.yml中的10.22.0,再clean掉,不行,第五次,第六次,尝试了多种办法还是不行,一气之下改成私有部署,本地1个命令,真香。

推荐私有部署 hexo-deployer-git 再无头秃烦恼

1
2
3
4
5
deploy:
type: git
repo: # your-git-repo-url
token: $GH_TOKEN # 你的token, 跟在travis.com上配置是同一个token,这里写token名:$GH_TOKEN
branch: gh-pages # 发布分支
1
2
3
4
5
6
# 配置完成后接下来命令行:直接一个命令
$ npx hexo clean && npx hexo deploy
# 接下来就是vscode-auth => github.com的认证过程,按照提示操作即可
# 如果碰到 关键字中有 LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443错误,更换网络,不要用流量
# 如果碰到 Authentication failed for 'https://github.com/xxx/xxx.github.io/',那就是travis上配置的$ $GH_TOKEN与.config.yml中的不一致
# 这两个问题都没碰到,基本成功

TroubleShooting

  1. 将主题换回默认主题,部署成功,说明问题出在 Next 主题包上
  2. .travis.yml 文件内 install 下增加一条安装主题的命令 git clone --branch v8.0.0 https://github.com/next-theme/hexo-theme-next themes/next 即可
  3. 重新在main分支上推一下代码即可安装成功

挑战类型体操答案合集,一部分是自己做的,一部分是自己抄的

热身运动

1
2
3
4
5
// 期望是一个 string 类型 👉 so eazy
// any -> string
type HelloWorld = string
// 你需要使得如下这行不会抛出异常
type test = Expect<Equal<HelloWorld, string>>

部分前置知识: keyof, typeof, extends 等,具体请移步到官方文档查阅详情

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
// typeof Type Operator
typeof 'hello' // string
function f(a?: string) {
return {x: 10, y: 1, ...a} // 返回具体值,而不是类型
}
type F0 = ReturnType<f> // ❌ 'f' refers to a value, but is being used as a type here, you should use 'typeof f'
type F1 = ReturnType<typeof f> // {x: number; y:number; a?: string}
// typeof 后面不能跟执行函数
// meant to use = ReturnType<typeof f>
let shouldcontinue: typeof f('aaaa') // ❌

// keyof Type Operator
type Point = {a: string, b: string}
type P0 = keyof Point // type P0 = 'a' | 'b'

// as 类型断言:key Remapping via as
type NewKeyType<P> = Capitalize<string & P>
type MappedTypeWithNewProperties<T> = {
[P in keyof T as NewKeyType<P>]: T[P]
}
// 条件类型通过never来过滤key
type RemoveKindField<T> = {
[P in keyof T as Exclude<P, 'kind'>]: T[P]
}
interface Circle {
kind: 'circle';
radius: number
}
type kindlessCircle = RemoveKindField<T> // type kindlessCircle = {radius: number}

// extends 继承,扩展,衍生,Class继承,非实例,继承单个Class,衍生多个interface,类型约束 K extends keyof T,K 是 T的子类型(类似于Class A extends Class B, A 是 B的子类一个意思)
// 类继承👉协变&逆变
// infer 待推断的类型

// InstanceType<Type> 提取构造函数的实例函数类型:Class
class C {
x = 0
}
// 类 & 枚举 横跨 类型空间 & 值空间,所以 C 也算一种类型,但是 `typeof C !== C`, 因为 `typeof C` 并非 `C` 的类型,而`InstanceType<typeof C>`才是, 而 `C` 是 构造函数 `constructor`
type C0 = InstanceType<typeof C> // type C0 = C

实现 Pick<T, Keys>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Todo {
title: string
description: string
completed: boolean
}

// Pick<Type, Keys> 👉 MyPick<Type, Keys> 使用方法一致

type MyPick<T, K extends keyof T> = {
[P in K]: T[P] // P in K, K = 'title' | 'description' | 'completed', `in` means for P in K
}

type TodoPreview = MyPick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
title: 'Clean room',
completed: false,
description: 'xxxx' // ❌,description 多余属性
}

Readonly<T>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Todo {
title: string
description: string
}
// ReadOnly<Type> 👉 MyReadOnly<Type>

type MyReadOnly<T> = {
readonly [P in keyof T]: T[P]
}

const todo: MyReadonly<Todo> = {
title: "Hey",
description: "foobar"
}

todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property

DeepReadonly<T> 🌟

1
2
3
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends {} ? DeepReadonly<T[P]> : T[P]
}

ReadonlyAndPick<T, Keys> 🌟

1
2
3
4
5
6
7
8
type ReadonlyAndPick<T, K extends keyof T> = {readonly [P in keyof T as P extends K ? P : never]: T[P]} & {[P in keyof T as P extends K ? never : P]:[P]}
// 使用 Exclude 替代 P extends K ? never : P && 使用 Pick 替代 P extends K ? P : never
type ReadonlyAndPick<T, K extends keyof T> = {readonly [P in keyof Pick<T, K>]: T[P]} & {[P in keyof T as Exclude<P, K>]: T[P]}
// or
type ReadonlyAndPick<T, K extends keyof T> = {readonly [P in keyof Pick<T, K>]: T[P]} & {[P in Exclude<keyof T, K>]: T[P]}
// 进一步 替换 readonly
type ReadonlyAndPick<T, K extends keyof T> = Readonly<Pick<T, K>> & {[P in Exclude<keyof T, K>]: T[P]}

Omit<T, Keys>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Todo {
title: string
description: string
completed: boolean
}

// Omit<Type, Keys> 👉 MyOmit<Type, Keys>
type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never: P]: T[P]
}
// 跟上面用 never 过滤属性的方式一致,所以还可以这么写
type MyOmit1<T, K extends keyof T> = {
[P in keyof T as Exclude<P, K>]: T[P];
}

type TodoPreview = MyOmit<Todo, 'description' | 'title'>

const todo: TodoPreview = {
completed: false,
}

Exclude<T, U>

1
2
3
4
// 从T中排除可分配给U的类型
// T, U 都是字符串类型
// MyExclude<'a' | 'b' | 'c', 'a'> // 输出 'b' | 'c'
type MyExclude<T, U> = T extends U ? never : T

ReturnType<T> 提取函数的返回类型

1
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any

Parameters<T> 提取函数的参数类型

1
type Parameters<T> = T extends (...args: infer P) => any ? P : T

First<T> of Array 实现一个泛型类型, 使得可以返回数组类型的第一项

1
2
3
4
5
6
type arr1 = ['a', 'b', 'c']
type arr2 = [1, 2, 3]
type head1 = First<arr1> // 'a'
type head2 = First<arr2> // 1

type First<T> = T extends any[] ? never : T[0]

Length<T> of Tuple

1
2
3
type tesla = ['tesla', 'model3', 'model X', 'model Y']
type Length<T> = T extends any[] ? T['length'] : never
type teslaLength = Length<tesla> // 4

Last<T> of Array 实现一个泛型类型, 使得可以返回数组类型的最后一项

1
2
3
4
5
6
type arr1 = ['a', 'b', 'c']
type arr2 = [1, 2, 3]
type tail1 = Last<arr1> // 'a'
type tail2 = Last<arr2> // 1
// F 只是一个变量,代表最后一项之前的所有项
type Last<T extends any[]> = T extends [...infer F, infer E]? E: never;

Concat<T, P>: 实现一个Array.concat 方法功能相同的类型

1
2
3
type Concat<T, P> =T extends any[] ? P extends any[] ? [...T, ...P] : never : never;
// or type Concat<T extends any[], P extends any[]> = [...T, ...P]
type Result = Concat<[1], [2]> // [1, 2]

Push<T, P>: Array.push 类型方法

1
2
type Push<T extends any[], P extends any> = [...T, P]
type Result = Push<[1, 2], '3'> // [1, 2, '3']

Unshift<T, P>: Array.unshift 类型方法

1
2
type Unshift<T extends any[], P extends any> = [P, ...T]
type Result = Unshift<[1, 2], '3'> // ['3', 1, 2]

Pop<T> 🌟

1
type Pop<T> = T extends [...infer P, infer E] ? P : never;  

Shift<T> 🌟

1
type Shift<T> = T extends [infer E, ...infer P] ? P : never;  

Promise.all 🌟🌟

1
2
3
4
5
6
7
8
9
10
type inferTupleT<T extends any[]> = {
[P in keyof T]: T[P] extends Promise<infer S> ? S : T[P]
}
declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<inferTupleT<T>>
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise<string>((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
const p = PromiseAll([promise1, promise2, promise3] as const) // Promise<[number, 42, string]>

If<truthy, T, F> 🌟🌟

If<true, T, F> => T, If<false, T, F> => F

这里还需要考虑 any, never, boolean 被视为 union type, 同时 never是union运算的幺元【可在 category-theory-for-programmers 这本书上找到相关解释】, 但是 unknown 却没有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type If<C, T, F> = C extends true ? T : C extends false ? F : never; 
type A = If<true, 'a', 'b'> // 'a'
type B = If<false, 'a', 'b'> // 'b'
type C = If<'c', 'a', 'b'> // never
// 考虑输入 `any` && `boolean`
type D = If<boolean, 'a', 'b'> // 'a' | 'b', boolean 是 union type => distributive conditional types
type E = If<any, 'a', 'b'> // 'a' | 'b', any 是 union type => distributive conditional types
// 第一步去除 T 的 naked type 特点,怎么办呢?和下面 IsNever 处理一致, 加入 array, record, function 结构
type If<C, T, F> = [C] extends [true] ? T : [C] extends [false] ? F : never;
// 此时 boolean 可以了,但是 any 还不行
type D = If<boolean, 'a', 'b'> // never
type E = If<any, 'a', 'b'> // 'a'
// any 还是不对,还需要破坏类型检查,处理方式就是加入 加入破坏条件: 破坏 T 作为 checked type 的条件 即可
// any extends C ? never : T
type If<C, T, F> = [C] extends [true] ? (any extends C ? never : T) : [C] extends [false] ? (any extends C ? never : F) : never;
// 此时
type D = If<boolean, 'a', 'b'> // never
type E = If<any, 'a', 'b'> // never

插播:conditional type & distributive conditional types

distributive conditional types 的3个前提条件:1. T = naked type; 2. T = checked type; 3. T 实例化为 union type;

conditional type
1
2
3
4
5
6
7
8
9
10
11
12
13
// T: checkedType 被检查的类型
// U: extendsType 判断条件
// 下面这个表达式的意思是:如果T能够assignable(这里不是subtype的意思)给U那么结果为X,否则结果为Y
// 根据官方文档的 assignable 图:
type checkTtype = T extends U ? X : Y
// 如果 T 是 naked type: 详见本文末尾解释
// 则表达式就是 distributive conditional types
// T = A | B | C // A,B,C是naked type
type unionTypes = string | number
type checkTtypeUnion<T> = T extends any ? T[] : T
type checkTtypeUnionI1 = checkTtypeUnion<unionTypes> // string[] | number[]
// 如何消除 distributive conditional types
// 破坏上述3个条件即可,然后用 never 过滤掉该分支

TupleToUnion<T> 元组转联合类型

1
type TupleToUnion<T extends any[]> = T[number]

TupleToObject<T> 元组转对象

1
2
3
4
5
6
7
8
9
type tuple = ['tesla', 'model 3', 'model X', 'model Y']
// or
// const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
type TupleToObject<T extends readonly any[]> = {
[P in T[number]]: P
}
type result = TupleToObject<tuple> // {tesla: 'tesla', ...}
// when tuple is const
// type result = TupleToObject<typeof tuple>

IsNever<T> 🌟

conditional type 判断 IsNever 在某种情况下有问题:如果这么写 type IsNever<T> = T extends never ? true : false , 当 type A = IsNever<never>type A = never 而不是 true or false, 查阅怎么理解 conditional type 中的联合类型与 never

1
2
3
// IsNever
type IsNever<T> = [T] extends [never] ? true : false
// 将输入变成 不可能是空列表的状态即可,[T] 🉑️, {a: T} 也 🉑️

Flatten<T>

1
type Flatten<T> = T extends Array<infer I> ? I : T;

类型体操链接

TroubleShooting

  1. naked type: The type parameter is present without being wrapped in another type, (ie, an array, or a tuple, or a function, or a promise or any other generic type)

特别说明:本项目只关注如何写可用 ts 类型,其他诸如:根路径配置类型文件、ts 类型查找顺序、ts 类型体操等均不涉及

项目中需要使用 ts 类型的文件及其结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 文件名及文件层级
|---- server # 请求
| |--- index.ts # 请求配置
| |--- api.ts # 具体api
|---- store # vuex
|------|--- modules
|------|--- index.ts
|---- composables # 逻辑复用组件
|------|--- *.ts # echarts.ts / 变量声明,使用,值更新
|---- router # vue-router
|------|--- index.ts
|---- directives # 自定义全局指令
|------|--- index.ts
|---- pages # 页面
|---- components # 组件(非全局)
|---- my.t.ts # 共用类型定义文件

先看项目中出现过的几个 ts 典型写法

*.vue 文件内

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
// some-detail.vue script.lang = ts
import { defineComponent, reactive, ref, toRefs } from 'vue'
import { useStore } from 'vuex'
import { key } from 'path/to/store'

export default defineComponent({
props: {
list: Array, // round 0: 不够准确
default() {
return []
}
},
// setup 内部
// round 1
setup: (props: any) => {
// round 2: ?
const leftType: any = reactive({
rightType: 0,
list: []
})
// round 3: ?
const newArray = leftType.list.map(v:any => {
return {
...v,
changeSomePropName: v.somePropName
}
})
// round 4: ?
const textFunc: void = () => {
// do something but do not return anything
}
// round 5: ?
const hasTrue = ref<boolean>(false)
const isStr = ref<string>('initStr')

// round 6: ?
const { list } = toRefs(props) // list: any
// round 7: ?
const store: any = useStore(key)
// round 8: ?
const getData = getDataByAxiosApi().then(res => {
const ret: any = res
})
})
}

总结

1. 频繁使用 ts 类型的几个地方

  1. server/api.ts 接口定义,接口通用 axios 实例类型返回动态类型
  2. store/modules state 定义,融合多个模块的类型
  3. router/mpdules routes 定义,扩展字段类型
  4. 全局类型定义:扩展原模块类型
  5. *.vue文件内 setup & composables/*.ts 逻辑组件, 内部:函数声明,变量定义,赋值操作等

2. 文件内频繁写错方向的类型:使用推导类型

  1. 凡是赋值操作,赋值右边带有类型推导的,类型一律由右边决定,不必在左边写 any
  2. 凡是简单类型赋值操作,ref内部的,都不必声明类型 const a = ref('some str'); // a 一定会被推导为 Ref<string>
  3. 函数操作(useStore/useRoute/数组操作(map,forEach,for(of,in))/自定义函数,此处只是执行操作的函数),类型由函数定义时返回的参数类型决定
1
// 以上写法可以改成?

3. 定义函数/变量时如何返回需要的类型

  1. 扩展全局模块类型: router/store/axios

    衍生问题,如何知道 router 上有 RouteMeta 类型?(或者说如何知道 Router 有哪些类型)

    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
    // 扩展全局模块类型router/store/axios,以router为例
    /**
    * 模块导出的2种方式
    */
    // 方法一:全局导出声明
    // 确保是模块
    // export {}

    // declare module 'vue-router' {
    // // 自定义元字段声明
    // interface RouteMeta {
    // // is optional
    // isAdmin?: boolean
    // requiresAuth?: boolean,
    // // must be declared by every route
    // title?: string
    // }
    // }

    // 方法二:模块导出声明
    declare interface RouteMeta {
    // is optional
    isAdmin?: boolean
    requiresAuth?: boolean
    // must be declared by every route
    title: string
    }
  2. composables/directives/server

Method 是什么类型?
如何修改 request 的定义,使得可以在使用 getData 是返回与后端确定好的结构?在修改过程中碰到哪些问题?请举例

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
// server/index
import axios, { AxiosRequestConfig, Method } from 'axios'
// 这种写法会导致什么问题?
function request(method: Method, url: string, config: AxiosRequestConfig) {
return new Promise((resolve, reject) => {
instance
.request({
method: method,
url: url,
...config
})
.then((res) => {
resolve(res)
})
.catch((err) => {
reject(err)
})
})
}
// 定义 get 请求方法
export function get(url: string, params?: any, responseType?: resType) {
return request('GET', url, { params, responseType })
}
// api.ts
const getData = async (data) => {
try {
const res = await get('path/to/get/some/data', data)
return res
} catch(err) {
console.error('getData error', error)
}

}
// *.vue 使用getData函数时能否拿到返回的res 类型??
import { getData } from 'path/to/api.ts'
const res = getData()

如何在快速迭代的项目中书写正确可用的类型

1. 渐进式类型更新:比 any 更好一点的写法

引用类型reactive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 一开始可能只知道某个变量的几个属性,大致结构,不确定后续属性的新增/减少,可以这样写
// 第一步
// 类型文件B.ts
export type typeAfromB = {
[k: string]: unknown
// [k: string]: any
} // unknown & any 的区别: unknown 类型的变量赋值可以赋值给 any 和 unknown, 有类型检查更安全, any 可以赋值给任意类型(包括null & undefined),没有类型检查

// 第二步
// 组件A.vue
import { reactive, ref } from 'vue'
import { typeAfromB } from 'path/to/B.ts'
const someTypeInSometimes = reactive<typeAfromB>({
propA: '123',
propB: 456,
propC: {},
propD: [],
propE: () => {}
})

原始类型:联合类型、交叉类型、字面量类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 字面量类型
export type typeCfromB = 'GET' | 'POST' | 'DELETE' | 'PUT'
// 联合类型
export type typeDfromB = string | number
// 交叉类型
export type typeEfromB = string & number // 此处的 typeEfromB 是什么?考虑一种类型既是 `string` 又是`number`的类型
export type typeEEfromB = string[] & number // 此处的 typeEfromB 是什么?考虑一种类型既是 `string[]` 又是`number`的类型

// 组件B.vue: 使用以上定义的类型
const cb = ref<typeCfromB>('oh my god!') // 这里正确吗?
const db = ref<typeDfromB>(124)
db.value = '124' // 这样写ok吗?
const copyDB = db.includes('1') // 这样行不行?
const eb = ref<typeEfromB>(['125', 123]) // 有没有问题?
const copyEB = ref<typeEEfromB>()
const eeb = copyEB?.value.length // 这样能行吗?eeb 的类型是什么,

把类型当作值的集合,理解 类型中的 union type - 交集 和 intersection type -并集

1
2
3
4
5
6
7
8
9
10
11
12
13
type AA = {aa: 'A'}
type BB = {bb: 'B'}
type ABor = AA | BB
type ABand = AA & BB
type AAkeys = keyof AA
type BBkeys = keyof BB
type ABandKeys = keyof ABand // 'aa' | 'bb'
type ABorKeys = keyof ABor // never

const aaa: ABand = {
aa: 'A',
bb: 'B'
}

2. 项目中需要的类型基本上可由:联合类型(|)、交叉类型(&)、 字面量类型、泛型 以及枚举覆盖,不需要类型体操

从以上例子中考虑:联合类型交叉类型的区别? 可以得出交叉类型的应用场景是什么?

实际上,项目中使用最常见的还是联合类型和单一的type/interface, 使用 交叉类型 较多的场景是 store,为什么?

项目中常见的需要利用泛型的几种类型:new Promise, new Map, new Set

1
2
3
4
new Promise<T>((resolve, reject) => {
// do something
})
const a = ref<Map<tring, string[]>>(new Map())

进阶:类型越来越多,每新增一个文件、每新增一个api、每新增一个函数都会面临类型不够用的情况

1. 梳理项目中的类型分类,文件结构

考虑众多的类型定义,引用,需要对所有类型进行划分,文件结构梳理,有哪几种方式?考虑以上涉及到的文件和使用地方,类型导出声明的几种方式,类型分布项目中的文件结构可能有哪几种?

2. 先提炼类型最多,最频繁使用的文件:server/api.ts, store/modules, components/*.vue

  1. 提炼相近的结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 以api.ts为例:前置条件为 基础方法都已经封装完毕,直接使用即可:get, post, put等
    import { post, get } from 'path/to/server/index'
    // 管理后台项目考虑众多的list接口返回的结构基本都是一致的,所以以下泛型其实并不需要
    const getList = <T>(data: {idList: string[]}) => {
    return get<T>('/path/to/get/list', data)
    }
    // 修改如下
    import { ListReturnType } from '/path/to/interfaceAPI'
    const getList = <ListReturnType>(data: {idList: string[]}) => {
    return get<ListReturnType>('/path/to/get/list', data)
    }
    // interfaceAPI.ts
    export interface ListReturnType {
    records: string[] | number[] | Record<string, unknown>[]
    page: number
    total: number
    }
  2. 使用泛型,先不考虑合理的问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 考虑一种情况,返回类型由参数类型决定,此时最简单的就是泛型
    import { post, get } from 'path/to/server/index'
    const getListTypeByParamsType = <T>(data: T) => {
    return get<T[]>('/path/to/get/list', data)
    }
    // 在组件A.vue中使用:一个异步函数中获取返回值的类型,以便后续的操作
    const getListFinallyA = async () => {
    // getListTypeByParamsType: number[]
    const getNumberList = await getListTypeByParamsType<number>()
    const handleNumList = getNumberList.filter(v => v > 1000)
    }
    // 在组件B.vue中使用
    const getListFinallyB = async () => {
    // getStringList: string[]
    const getStringList = await getListTypeByParamsType<string>()
    const handleStrList = getStringList.map(v => v.length)
    }
  3. 将所用类型分类拆分,大量使用联合类型、交叉类型

    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
    // 考虑一种实际业务情况,一位客户的公开信息由:基础信息 & 会员信息 & 订单信息组成
    // 对应的接口在不同页面或不同条件下返回的可能会只有其中1种或多种,而且只能在页面使用时才能确定类型,那么此时的接口返回类型怎么写比较好呢?
    // interfaceAPI.ts
    export type GenderType = 'f' | 'm' | 1 | -1 | 2; // 考虑兼容情况
    export interface GoodsType {
    name: string
    amount: number
    id: string
    type: string
    }
    export interface BaseInfo {
    nickName: string
    anyAge: number
    gender: GenderType
    children?: BaseInfo[]
    }
    export interface MemberInfo {
    level: 1 | 2 | 3 | 4 | 5
    numMember: string | number
    bonusPoint: number
    timesConnected: number
    }

    export interface OrderInfo {
    channels: 'jd' | 'tmall' | 'pdd' | 'amazon'
    totalAmount: number
    purchasedGoods: GoodsType[]
    }
    // api.ts
    import { get } from 'path/to/server/index'
    const getCustomerInfo = <T>(data: { id: string; type?: number }) => {
    return get<T>('/path/to/get/list', data)
    }

    const getCustomerInfoA = (id: string) => {
    return getCustomerInfo<BaseInfo>({id, type: 0})
    }

    const getCustomerInfoB = (id: string) => {
    return getCustomerInfo<BaseInfo & MemberInfo>({id, type: 1})
    }

    const getCustomerInfoC = (id: string) => {
    return getCustomerInfo<BaseInfo & MemberInfo & OrderInfo>({id, type: 2})
    }

    const resA = getCustomerInfoA('123').then((res) => {
    const { nickName } = res
    console.log(nickName)
    })

    const resB = getCustomerInfoB('123').then((res) => {
    const { nickName, level } = res
    console.log(nickName, level)
    })

    const resC = getCustomerInfoC('123').then((res) => {
    const { nickName, level, purchasedGoods } = res
    console.log(nickName, level, purchasedGoods)
    })
  4. 使用部分联合类型时,考虑用 Pick Omit 等方法

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
// 可选
// Partial<Type>
interface Todo {
title: string
dess: string
children: string[]
}
type PartialTodo = Todo = Partial<Todo>
// Equal to
type PartialTodo = {
title?: string
desc?: string
children?: string[]
}
// 必选
// Required<Type>
type RequiredPartialTodo = Required<PartialTodo>
// Equal to Todo

// 只读
// Readonly<Type>
function Freeze<T>(obj: T): Readonly<T>

// Pick<Type, Keys> 只使用部分属性 - Keys = Type里的字符串或者字符串集合(并集):'name', 'name' | 'title' | 'desc' 这2种形式
// Omit<Type, Keys> 忽略部分属性 - Keys = Type里的字符串或者字符串集合(并集):'name', 'name' | 'title' | 'desc' 与 Pick<Type, Keys> 正好相反
type partProps = 'title' | 'desc'
type TodoPreview = Pick<Todo, partProps>
const todoPreview: TodoPreview = {
titel: 'xxx',
desc: '555'
}

// Exclude<Type, ExcludeUnion> 排除联合类型内的某个或者某些类型
type T0 = Exclude<'a' | 'b' | 'c', 'a'> // type T0 = 'a' | 'b'
type T1 = Exclude<'a' | 'b' | 'c', 'a' | 'c'> // type T0 = 'b'

// Extract<Type, Union> 提取 Type 和 Union 的交集
type T2 = Extract<'a' | 'b' | 'c', 'a' | 'f'> // type T2 = 'a'

// 与函数相关的实用方法
// Parameters<Type> 提取函数参数类型
// ConstructorParameters<Type> 提取构造函数参数类型:ErrorConstructor, FunctionConstructor, RegExpConstructor 等
// ReturenType<Type> 提取函数返回类型


// 不常用到的实用方法
// ThisParameterType<Type> 提取`有this`的函数参数类型,或者提不到就是 unknown
// ThisType<Type> 不返回转换类型,只作为一个上下文相关的 this 类型 的标记,必须`tsconfig.ts`开启 `noImplicitThis`

// 内置字符串操作类型
// Uppercase<StringType> 全部大写
// Lowercase<StringType> 全部小写
// Capitalize<StringType> 首字母大写
// Uncapitalize<StringType> 非首字母大写

部分答案,仅供参考,如有错误,概不负责,请自行甄别

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
// some-detail.vue script.lang = ts
import { defineComponent, reactive, ref, toRefs, PropType } from 'vue'
import { useStore } from 'vuex'
import { key } from 'path/to/store'

export default defineComponent({
props: {
list: Array as PropType<string[]>, // round 0: props 使用 PropType 强制转换类型为具体类型
default() {
return []
}
},
// setup 内部
// round 1: 给props 提供具体的类型 { list: string[] }
setup: (props: {list: string[]}) => {
// round 2:
const leftType = reactive<{
rightType: number
list: {label: string;value: number | string; extraProps?: string}[]
}>({
rightType: 0,
list: []
})
// round 3: v: {label: '', value: 0, extraProps?: ''}
const newArray = leftType.list.map(v => {
return {
...v,
extraProps: v.label
}
})
// round 4: 只要没有明确 return 都是void, 特殊情况为 never
const textFunc = () => {
// do something but do not return anything
}
// round 5: 简单类型可以自动推导
const hasTrue = ref(false)
const isStr = ref('initStr')

// round 6: props在setup引入的时候就应该定义list类型,此处是 string[]
const { list } = toRefs(props) // list: string[]
// round 7: 在createStore 就已经定义完成,不必在左边加 any
export const key: InjectionKey<Store<LoginState & PermissionState & UserState>> = Symbol('index')
export const store = createStore<LoginState & UserState & PermissionState>({
modules
})
// store 类型即推导出的类型
const store = useStore(key)
// round 8: 参照 api.ts 的修改
const getData = getDataByAxiosApi().then(res => {
const ret = res
})
})
}

温馨提示: 先别替换 ant-design-vue 中的 moment 为 dayjs, 有bug, date-picker会坏掉,等作者修好再换

本项目的技术栈是:vite vue3 ant-desing-vue
项目中需要用到的ant-desing-vueUI 组件依赖moment,我看官网提示有插件可以改成dayjs,可以使整个包小一点,看了antd-dayjs-webpack-plugin的代码后发现没有针对vite ant-desing-vue的,就跃跃欲试想写一个vite-plugin-vue-ant-desing-vue-dayjs插件,专门用在vite vue3 ant-desing-vue项目中。后面其实发现没必要,当然,写完才发现-_-||

主要问题

叠了 3 个 debuff 导致运行时出问题

  1. 由于我 fork 了一部分antd-dayjs-webpack-plugin代码,内部是module.exports
  2. 同时入口文件里是直接export default vitePluginVueAntDVueDayjs(Options={}) {...}
  3. 而且是直接yarn add git+ssh://git@github.com:xxx/some-project.git --dev,这个some-project就是写的插件,package.json里暴露的直接是main: src/index.js,

第一次写插件,心情十分激动结果刚运行就报错,搜了一下原来是 node 环境里不能直接加载ES6模块。。。于是我重新看了一次 es6 文档node 环境如何加载es6模块,然后查了一下@vite/plugin-vue的解决办法

解决方案

  1. 完全采用 cjs 模块方式编写,一水儿的module.export -> 针对入口文件非打包后的插件。如package.json中的main: src/index.js
  2. 将 es6 模块打包编译成 node 模块(@vite/plugin-vue)就是这么做的:esbuild src/index.ts --bundle --platform=node --target=node12 --external:@vue/compiler-sfc --outfile=dist/index.js -> 完全使用 es6 模块编写插件,然后使用打包后的 cjs 模块, package.json中的main: dist/index.js

没必要写是因为直接配置即可

1
2
3
# 命令行
# npm i dayjs
yarn add dayjs
1
2
3
4
5
6
7
8
// vite.config.js文件
{
resolve: {
alias: {
moment: 'dayjs'
}
},
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// main.ts
import dayjs from 'dayjs'
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'
import advancedFormat from 'dayjs/plugin/advancedFormat'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import weekday from 'dayjs/plugin/weekday'
import weekOfYear from 'dayjs/plugin/weekOfYear'
import isMoment from 'dayjs/plugin/isMoment'
import localeData from 'dayjs/plugin/localeData'
import localizedFormat from 'dayjs/plugin/localizedFormat'
import 'dayjs/locale/zh-cn' // 导入本地化语言

dayjs.extend(isSameOrBefore)
dayjs.extend(isSameOrAfter)
dayjs.extend(advancedFormat)
dayjs.extend(customParseFormat)
dayjs.extend(weekday)
dayjs.extend(weekOfYear)
dayjs.extend(isMoment)
dayjs.extend(localeData)
dayjs.extend(localizedFormat)
dayjs.locale('zh-cn') // 使用本地化语言

插件步骤

如果要写也不是不行:看上面的配置步骤即可知道思路,可参考项目vite-plugin-vue-antd-dayjs

第一种: 使用非打包插件

有个直接的好处,安装后可以直接修改包文件来测试代码逻辑是否正确,即改即用,我当时采用的就是这种写法

1. 新建项目

1
2
3
4
5
# 项目结构
|-- src
|----|--index.js
|-- package.json
|-- README.md

2. package.json 配置

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
// packgae.json
{
"name": "vite-plugin-xxx",
"version": "1.0.0",
"description": "Description of the vite-plugin-xxx",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": ["vite-plugin", "vite-plugin-vue", "rollup-plugin", "xxx"],
"engines": {
"node": ">=12.0.0"
},
"devDependencies": {
"dayjs": "*"
},
"peerDependencies": {
"dayjs": "*"
},
"homepage": "",
"bugs": {
"url": ""
},
"repository": {
"type": "git",
"url": ""
},
"author": "your Name",
"license": ""
}

3. 入口文件

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
// src/index.js
module.exports = function VitePluginVueXXX(Options = {}) {
const virtualFile = "@virtual-file";
return {
name: "vite-plugin-vue-xxx",
enforce: "pre", // 加载vite内置插件之前加载
config: () => ({
"resolve.alias": {
moment: "dayjs", // set dayjs alias
},
}), // 在被解析之前修改 Vite 配置
configResolved(resolvedConfig) {
// 存储最终解析的配置
config = resolvedConfig;
}, // 在解析 Vite 配置后调用
resolveId(id) {
// 每个传入的模块请求时被调用
if (id === virtualFile) {
console.log("resolveid id test", id);
return id;
}
},
load(id) {
// 每个传入的模块请求时被调用
if (id === virtualFile) {
// 在这里return上面main.js引入的插件和依赖
// 如果需要知道哪些插件必须引入,直接查看dayjs官网的插件合集
// return 出去的是 `@virtual-file`的文件内容,这里写成 `const xxx = require('xxx')` or `import xxx from 'xxxx'`都是可以的,注意在`main.js`内的引用形式即可
// 例如:isMoment plugin
return `const isMoment = require('dayjs/plugin/isMoment'); dayjs.extend(isMoment);module.exports = {isMoment}`;
}
},
};
};

4. 安装 没有发布的包文件

1
2
3
4
# 安装 插件的 peerDependencies
yarn add dayjs
# 安没有发布的装插件包
yarn add git+ssh://git@github.com:xxx.git --dev
1
2
3
4
5
6
7
8
9
10
11
// 在 vite.config内配置
// 引入插件
const vitePluginVueForAntdDayjs = require('vite-plugin-vue-antd-dayjs')
{
plugins: [vue(),vitePluginVueForAntdDayjs()],
resolve: {
alias: {
moment: 'dayjs'
}
},
}

5. 在 main.js 里引入虚拟文件即可

1
2
3
4
5
// 以 isMoment 为例
// 在 mjs 内 引入 cjs 模块
import isMoment from "@virtual-file";
// 如果模块很多,一次引入很多模块, 直接引入文件
import "@virtual-file";

启动项目: yarn dev, vite 打包会发现有提示:[vite] new dependencies found: dayjs/plugin/isSameOrAfter, dayjs/plugin/advancedFormat, dayjs/plugin/customParseFormat, dayjs/plugin/weekday, dayjs/plugin/weekYear, dayjs/plugin/weekOfYear, dayjs/plugin/isMoment, dayjs/plugin/localeData, dayjs/plugin/localizedFormat, dayjs/locale/zh-cn, updating... 说明已经成功引入

6. 打包比较前后包的差异

第二种:使用打包后的插件

步骤跟上面的差不多,只是写的时候使用 es6 模块,多一步打包&入口文件有点不同,可参考项目vite-plugin-antd-vue-ts

  1. packgae.json 的配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    "main": "dist/index.js",
    "types": "dist/index.d.ts",
    "scripts": {
    "build:boundle": "`esbuild src/index.ts --bundle --platform=node --target=node12 --external:dayjs --outfile=dist/index.js"
    },
    "dependencies": {
    "esbuild": "*"
    },
    "devDependencies": {
    "dayjs": "*"
    },
    "peerDependencies": {
    "dayjs": "*"
    }
    }
  2. 入口文件

1
export default function VitePluginVueXXX(Options = {}) {}
  1. 执行打包命令
1
npm run build:boudle
  1. 配置 vite.config.js & 在main.js引入文件步骤同上

如果要实现在入口文件的引用方式为 import someplugin from 'path/to/all-plugin-file-name',在插件内使用 export defaultmodule.exports是一样的效果

步骤总览

开发前配置

  1. 命令行使用 vite 生成项目模板
  2. 配置 TypeScript:
    1. tsconfig 配置
    2. 安装 ts 类型检查
  3. 集中处理请求【Axios 库】
  4. 分支管理保护与自动化的2种方式,1个必要

开始开发

  1. 配置环境文件
  2. 配置别名【可选】
  3. 配置路由
  4. 选择&调研组件库
  5. 配置主题文件
  6. 安装 CSS 预处理器
  7. 开发过程中与 vue3 开发文档相关的一些知识点
  8. 开发过程中常见的问题
  9. 在项目中写出可维护可拓展的类型

结尾提供了其他相关链接

准备工作

使用 nvm 切换不同版本的 node 来适用不同项目

命令行使用 vite 生成项目模板

1
2
3
4
5
6
7
8
9
node -v
# v8.9.4
nvm use 12
# Now using node v12.19.0 (npm v6.14.8)
yarn create @vitejs/app
# 选择 vue-ts 预设模板
cd my-vue3-vite2-app
yarn
yarn dev

配置 TypeScript

tsconfig.json 文件 增加 2 项配置

1
2
3
4
5
6
{
"compilerOptions": {
"isolatedModules": true,
"types": ["vite/client"]
}
}

安装 ts 类型检查

打开 vite.config.ts 文件时 控制台就会报 failed to load the eslint library for the vite.config.ts 错误,这是因为本地 eslint 无法解析 ts 文件,需要安装 ts 依赖 & vite 不提供 ts 类型检查

1
2
3
4
5
6
7
8
# 安装ts eslint 依赖
# node version >= 12
# 注意:以下包不提供对 *.vue 文件的检查
yarn add -D eslint typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin
# 如需在开发过程中对 *.vue 文件进行类型检查,请增加以下包支持
# yarn add -D eslint-plugin-vue
# 安装node变量的类型声明
npm i --save-dev @types/node

下载额外的npm包需要添加其他类型定义:1.安装npm i --save-dev @types/xxx;2.在tsconfig.json中添加``types`的配置『 由于 vite2 创建的 ts 项目中已添加默认的 types 定义,标明只使用该类型定义,所以如果需要其他类型(不在@vite/client 中),需要增加配置

1
2
3
4
5
// tsconfig.json
// 在 node 后面增加一项xxx即可
{
"types": ["vite/client", "node", "xxx"]
}

本项目使用 eslint:recommended 规范,如需修改请参照 tslint 官方提示

  1. 工作目录下新增 .eslintrc.js 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // 不含  eslint-plugin-vue 插件的配置
    module.exports = {
    root: true,
    parser: "@typescript-eslint/parser",
    plugins: ["@typescript-eslint"],
    extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
    };
    // 包含 eslint-plugin-vue 插件的配置
    module.exports = {
    root: true,
    parser: "vue-eslint-parser",
    parserOptions: {
    parser: "@typescript-eslint/parser",
    ecmaVersion: 2020,
    sourceType: "module",
    ecmaFeatures: {
    jsx: true,
    tsx: true,
    },
    },
    plugins: ["@typescript-eslint"],
    extends: [
    "plugin:vue/vue3-recommended",
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    ],
    };
  2. 工作目录下新增 .eslintignore 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # don't ever lint node_modules
    node_modules
    # don't lint build output (make sure it's set to your correct build folder name)
    dist
    # don't lint nyc coverage output
    coverage
    # don't lint .vscode
    .vscode
    # don't lint .eslintrc.js
    .eslintrc.js

配置请求 Axios 集中处理请求

定义

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
// server/index.ts 文件
import axios, { AxiosRequestConfig, Method } from "axios";
import qs from "qs";
// 请求常量配置
const BASE_URL = import.meta.env.VITE_APP_BASE_URL as string;
const SEUCCESS_CODE = ["200"]; // 多个code可兼容历史api的成功返回值
const SPECIAL_CODE_403 = []; // 添加业务要求的 无权限的code码
const LOGOUT_CODE = []; // 添加业务要求登出的code码

export type resType = "json" | "blob";

const instance = axios.create({
baseURL: BASE_URL,
timeout: 30000,
withCredentials: true, // 表示跨域请求时是否需要使用凭证
validateStatus: function (status) {
return status >= 200 && status <= 304;
},
paramsSerializer: function (params) {
return qs.stringify(params);
},
});
// 创建一个基本函数,后续的 post & get方法都基于baseFunc
// 添加请求拦截器
instance.interceptors.request.use(
function (config) {
// 如需自定义config,可在调用api时传入,在此处修改、增加、覆盖
const options = {
...config,
headers: {
...config.headers,
// 'X-TOKEN': config.headers['X-TOKEN'] || 'default value or test token for testing' // 请求头自定义token, 重置 responseType || Content-Type
},
};
return options;
},
function (error) {
return Promise.reject(error);
}
);

// 添加响应拦截器
instance.interceptors.response.use(
function (response) {
const { status, data }: { status: number; data: any } = response;
// 特殊处理返回类型是 blob 类型的请求
if (status === 200 && response.config.responseType === "blob") {
return Promise.resolve(response);
}
const { code } = data;
if (SEUCCESS_CODE.includes(code + "")) {
// 符合请求成功标准
return Promise.resolve(data.data || {});
} else if (SPECIAL_CODE_403.includes(code + "")) {
// 403需要重新授权
return Promise.resolve(data);
} else if (LOGOUT_CODE.includes(code + "")) {
// 意外登出,重新登录
// reLogin
return Promise.reject(data);
// return Promise.resolve(data)
} else {
// 统一弹窗提示错误
return Promise.reject(data);
}
},
function (error) {
// 集中处理超时错误
// 或者 统一向上抛出
return Promise.reject(error);
}
);
// 基本配置
// 这里有2种方式可以返回 使用api时配置的返回类型
// 1. 直接在根目录下添加 Axios 的类型声明,改写返回类型
// 2. 引入Axios是点击查看 Axios 定义的类型,在使用 `instance.request` 时改成返回传入泛型即可
// 这里我们使用第二种方式
function request<T>(method: Method, url: string, config: AxiosRequestConfig) {
return new Promise<T>((resolve, reject) => {
instance
.request<T, T>({
method: method,
url: url,
...config,
})
.then((res) => {
resolve(res);
})
.catch((err) => {
reject(err);
});
});
}
// 后续根据具体参数类型返回
export function put<T>(url: string, data?: any) {
return request<T>("PUT", url, { data });
}
export function post<T>(url: string, data?: any, responseType?: resType) {
return request<T>("POST", url, { data, responseType });
}
export function del<T>(url: string, data?: any) {
return request<T>("DELETE", url, { data });
}
export function get<T>(url: string, params?: any, responseType?: resType) {
return request<T>("GET", url, { params, responseType });
}

export default instance;

使用定义好的 post, get等方法

1
2
3
4
5
6
7
8
9
10
// const-api-path.ts
export const POST_SOME_LIST = "/path/to/some/list";
// api.ts
import { post, get } from "./index";
import { POST_SOME_LIST } from "./const-api-path";
import { ListReturnType, ListParamType } from "./api-types";

export const getSomeList = (data: ListParamType) => {
return post<ListReturnType>(POST_SOME_LIST, data);
};

分支管理保护与自动化

测试环境的发布分支只有 2 个: dev, hotfix

分支管理、保护的2种方式,1个必要

基础仓库影响范围较大,分别对应多个项目的不同阶段,则需要对该项目进行分支管理和保护,通常只保护3个主要分支: main, dev,hotfix

第一种方式

一个新需求的全部工作流程

  1. 新建功能分支,以 feature- 开头
  2. 功能开发结束给 devmerge request
  3. 代码审核结束成功合并到 dev后,开始测试
  4. 通过测试后提交 merge requestmain分支准备发布
  5. 发布成功且线上验收成功后,在最新提交上打 tag, 并推送 tags
  6. 保留该功能分支 2 周删除该分支对应的远程分支,以防后续开发功能分支越来越多

一个紧急修复的全部流程

  1. git checkout hotfix
  2. git pull
  3. 开始修复,修复结束后提交代码发布到测试环境测试
  4. 通过测试后提交 mr 到 main分支准备发布
  5. 发布成功且线上验收成功后,在最新提交上打 tag, 并推送 tags
第二种方式

一个主仓库分别由各个开发fork到自己的group, 主仓库只保留3个分支:main(稳定版本-线上环境), hotfix(紧急修复-发布测试环境), dev(开发时-发布测试环境), fork仓库完全由开发者自行管理

1
2
3
4
5
6
7
8
9
10
11
12
# fork后的项目新增一个 remote
git remote add source-hub-name source-hub-ssh-path-or-https-path
# 拉取 主仓库所有分支
git fetch source-hub-name
# 建立开发进度对应分支
git checkout -b dev-source-hub-name
# 拉取 主仓库 dev 分支所有commit
git pull source-hub-name dev
# 回到dev分支,合并主仓库进度
git checkout dev
# 或者仅pick部分提交
git merge dev-source-hub-name

开发

配置环境文件

  • 构建包文件会通过环境文件来确定环境变量,在项目下新增:
    • .env.production文件
    • .env.development文件
    • .env.stagging文件

配置别名【可选】

1
2
3
4
5
6
7
8
9
10
11
12
13
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path from "path";

// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});

配置路由

1
yarn add vue-router@4

使用

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
import HelloWorld from "@/components/hello-world.vue";
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";

// 路由懒加载
const About = () => import("../components/about.vue");
const routes: RouteRecordRaw[] = [
{
path: "/",
component: HelloWorld, // 非懒加载
},
{
path: "/about",
component: About,
// meta类型需要额外声明,此声明在 src/typings/augmenation.d.ts内
meta: {
isAdmin: false,
title: "关于",
},
},
];
const router = createRouter({
history: createWebHashHistory(),
routes,
});
export default router;

添加额外的 meta 字段的类型声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// augmenation.d.ts
// Ensure this file is parsed as a module regardless of dependencies.
export {};
declare module "vue-router" {
// 自定义元字段声明
interface RouteMeta {
// is optional
isAdmin?: boolean;
// is optional
requiresAuth?: boolean;
// must be declared by every route
title: string;
}
}

添加路由元信息声明

选择组件库 ☞ 本项目使用 ant-design-vue, 所以选用 less, 方便覆盖默认主题

配置主题文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用插件转换文件
import lessToJS from 'less-vars-to-js'
const themeVariables = lessToJS(
fs.readFileSync(path.resolve(__dirname, './src/styles/variable.less'), 'utf8')
)
// vite.config.js
{
css: {
preprocessorOptions: {
// 在这里覆盖css全局变量, 可覆盖变量可在varible.less中查看
less: {
modifyVars: themeVariables,
javascriptEnabled: true,
}
}
},
}

安装 CSS 预处理器

请匹配 组件库 的选择

1
2
3
4
5
6
# .scss and .sass
npm install -D sass
# .less
npm install -D less
# .styl and .stylus
npm install -D stylus

替换 moment 为 dayjs

  1. yarn add dayjs or npm i dayjs

  2. vite.config中添加 alias

    1
    2
    3
    4
    5
    6
    7
    {
    resolve: {
    alias: {
    moment: 'dayjs'
    }
    },
    }
  3. main.ts文件里添加相关插件, 添加前后分别打包进行比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// main.ts
import dayjs from "dayjs";
import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
import advancedFormat from "dayjs/plugin/advancedFormat";
import customParseFormat from "dayjs/plugin/customParseFormat";
import weekday from "dayjs/plugin/weekday";
import weekOfYear from "dayjs/plugin/weekOfYear";
import isMoment from "dayjs/plugin/isMoment";
import localeData from "dayjs/plugin/localeData";
import localizedFormat from "dayjs/plugin/localizedFormat";
import "dayjs/locale/zh-cn"; // 导入本地化语言

dayjs.extend(isSameOrBefore);
dayjs.extend(isSameOrAfter);
dayjs.extend(advancedFormat);
dayjs.extend(customParseFormat);
dayjs.extend(weekday);
dayjs.extend(weekOfYear);
dayjs.extend(isMoment);
dayjs.extend(localeData);
dayjs.extend(localizedFormat);
dayjs.locale("zh-cn"); // 使用本地化语言

开发过程中与 vue3 开发文档相关的一些知识点

响应式 API

  • reaåctive(): 返回对象的响应式副本

SetUp & ref() || unRef() & toRefs()

SetUp(props, context) 逻辑块组合,可组合:生命周期函数/watch/computed,返回对象,对象可包含 data/methods/computed

生命周期映射关系

  • beforeCreate -> use setup()
  • created -> use setup()
  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeUnmount -> onBeforeUnmount
  • unmounted -> onUnmounted
  • errorCaptured -> onErrorCaptured
  • renderTracked -> onRenderTracked
  • renderTriggered -> onRenderTriggered

由于 props 是响应式的,所以不能使用ES6解构,因为 解构是浅复制,非引用类型属性拷贝后会跟原 prop 属性脱离,是一个新的变量,没有响应性,引用类型的属性(数组,对象,函数)还会保持引用,这 2 种情况下解构后的变量响应性不统一 所以会消除 prop 的响应性,如果需要解构 prop,使用 toRefs

若要获取传递给 setup() 的参数的类型推断,请使用 defineComponent

在 Setup 中使用 Router

请使用useRouter() || useRoute(), 模板中可以访问 $router$route,所以不需要在 setup 中返回 routerroute

  • userLink & RouterLink: 内部行为已经作为一个组合式 API 函数公开

ts 相关

  • setup() 函数中,不需要将类型传递给 props 参数,因为它将从 props 组件选项推断类型
  • refs() 1.初始值推断类型;2.传递一个泛型参数;3. 使用Ref<T>替代ref
  • readtive() 使用接口(Interface)
  • computed自动推断

与 Options api 一起使用

  • 复杂的类型或接口使用 as 强制转换

注解返回类型

  • computed 的类型难以推断 需要注解

注解 props

  • 使用 PropType 强制转换构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { defineComponent, PropType } from "vue";

// 这里是接口,也可以是type
interface Book {
title: string;
author: string;
year: number;
}

const Component = defineComponent({
props: {
name: String,
success: { type: String },
callback: {
type: Function as PropType<() => void>,
},
book: {
type: Object as PropType<Book>,
required: true,
},
},
});
  • 由于 ts 的设计限制:涉及到为了对函数表达式进行类型推理,你必须注意·对象和数组的validatorsdefault
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
import { defineComponent, PropType } from "vue";

interface Book {
title: string;
year?: number;
}

const Component = defineComponent({
props: {
bookA: {
type: Object as PropType<Book>,
// 请务必使用箭头函数
default: () => ({
title: "Arrow Function Expression",
}),
validator: (book: Book) => !!book.title,
},
bookB: {
type: Object as PropType<Book>,
// 或者提供一个明确的 this 参数
default(this: void) {
return {
title: "Function Expression",
};
},
validator(this: void, book: Book) {
return !!book.title;
},
},
},
});

注解 emit

  • 为触发的事件注解一个有效载荷。另外,所有未声明的触发事件在调用时都会抛出一个类型错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Component = defineComponent({
emits: {
addBook(payload: { bookName: string }) {
// perform runtime 验证
return payload.bookName.length > 0;
},
},
methods: {
onSubmit() {
this.$emit("addBook", {
bookName: 123, // 类型错误!
});
this.$emit("non-declared-event"); // 类型错误!
},
},
});

类型声明 refs

  • 有时我们可能需要为 ref 的内部值指定复杂类型。我们可以在调用 ref 重写默认推理时简单地传递一个泛型参数
1
2
3
const year = ref<string | number>("2020"); // year's type: Ref<string | number>
year.value = 2020; // ok!
// 如果泛型的类型未知,建议将 ref 转换为 Ref<T>

类型声明 reactive

ref 的区别: 当变量基础类型时使用ref, 引用类型-对象/数组等使用 reactive

  • 当声明类型 reactive property,我们可以使用接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { defineComponent, reactive } from "vue";

interface Book {
title: string;
year?: number;
}

export default defineComponent({
name: "HelloWorld",
setup() {
const book = reactive<Book>({ title: "Vue 3 Guide" });
// or
const book: Book = reactive({ title: "Vue 3 Guide" });
// or
const book = reactive({ title: "Vue 3 Guide" }) as Book;
},
});

开发过程中遇到的问题[TroubleShooting]

注意:vite2 自动构建 typescript 模板项目文件,部分同学会在 build 阶段报错,详情请移步

windows 系统的同学可将 build 打包命令由vuedx-typecheck . && vite build 改为 vite build && tsc,ci 脚本内加一条:run: npx --no-install vuedx-typecheck

ts文件内使用 const xxx = require('xxx')报错:require is not defined

已解决: vite@2.4.1已修复该错误,升级即可

UI 组件的问题

依赖强缓存

  • 通过浏览器 devtools 的 Network 选项卡暂时禁用缓存;
  • 重启 Vite dev server,使用 –force 标志重新打包依赖;
  • 重新载入页面。

esbuild 预构建

相关文档链接

熵(Entropy), 单位:bit, 信息熵的计算公式:H(X)= -

散列函数

1
hash(value: array<byte>) -> vector<byte, N> (N对于该函数固定)

SHA-1,git 中使用的

Vim「作为编辑器的使用功能」

编辑模式

  1. 正常:移动光标修改

  2. 插入文本: 键入 i 进入插入模式

  3. 替换文本:键入 R 进入替换模式

  4. 可视化: 选中文本块,键入 v 进入可视化一般模式,V 进入可视化行模式,Ctrl+v 可视化块模式

  5. 执行命令:键入 : 进入命令模式:

    • :q 退出关闭窗口
    • :w 保存(写)
    • :wq 保存并退出
    • :e {file-name} 打开要编辑的文件
    • :ls 显示打开的缓存
    • :help {标题} 打开帮助文档:
      • :help :w 打开:w命令的帮助文档
      • :help w 打开w移动的帮助文档

键入 ESC 切换为正常模式

vim「作为编程语言」

移动-在缓存中导航

  1. 基本移动:hjkl(左下右上-逆时针)
  2. 词:w-下一个词,b-词初,e-词尾
  3. 行:0行初,^-第一个非空格字符,$行尾
  4. 屏幕:H-屏幕首行,M-屏幕中间,L-屏幕底部
  5. 翻页:Ctrl+u上翻,Ctrl+d下翻
  6. 文件: gg-文件头,G文件尾部
  7. 杂项::{line number}<CR> or {line number}G
  8. 查找:f{character}, t{character}, F{character}, T{character}, (f/F = find, t/T = to, 小写是 forward, 大写是 backward)
    • find/to forward/backward {character} on the current line
    • , or ;用于导航匹配
  9. 搜索:/{regex}向后搜索,n or N用于导航匹配

编辑-动词

  • i - insert mode
  • o/O insert line below / above 向下、上插入行
  • d{motion}删除{移动命令},dw删除词,d$删除到行尾,d0-删除到行头
  • c{motion}改变{移动命令},cw改变词,d{motion}i
  • x删除字符,等同于dl
  • s替换字符,等同于xi
  • 可视化模式 + 操作
    • 选中文字,d删除或者c改变
  • u撤销,<C-r>重做
  • y to copy / “yank” (some other commands like d also copy)
  • p 粘贴
  • ~ 改变字符大小写

计数

3w向前移动 3 个词,5j向下移动 5 行,7dw删除 7 个词

修饰语:用修饰语改变语义

i, a表示在内部,在周围

  • ci(改变当前括号内容
  • ci[改变当前方括号内容
  • da'删除一个单引号字符串,包括周围的单引号

进阶

宏(macro):批处理,命令集合

  1. 搜索&替换::s替换

  2. 多窗口::sp or :vsp分割窗口,同一个缓存可以在多个窗口显示

    1
    2
    3
    4
    # 以下载的 vimrc 文件为例
    vim vimrc
    :sp
    # 此时会有2个窗口查看或编辑vimrc文件
  3. 宏:q{character}开始在寄存器{character}中录制宏,q停止录制,@重放宏

  4. 执行宏 {次数}@{字符},执行宏{次数}

  5. 宏递归:

    1. q{character}q清除宏
    2. 录制该宏,用@{character}来递归调用该宏
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 自动加末尾行注释宏**@t**
    touch new-practice-vimrc
    vim new-practice-vimrc
    # 录制宏t
    qt
    # i进入编辑模式,编辑器底部显示: --INSERT --Recording @t
    i
    # 输入通用行尾注释
    # esc退出到编辑模式,底部显示 --Recording @t
    # q停止宏录制
    q
    # :wq!保存退出编辑模式,结束

vscode 配置 vim 插件

  • 下载插件 Vim
  • 配置 setting.json
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
{
"vim.easymotion": true,
"vim.suround": true,
"vim.incsearch": true,
"vim.useSystemClipboard": true,
"vim.useCtrlKeys": true,
"vim.hlsearch": true,
"vim.insertModeKeyBindings": [
{
"before": ["j", "j"],
"after": ["<Esc>"]
}
],
"vim.normalModeKeyBindingsNonRecursive": [
{
"before": ["<leader>", "d"],
"after": ["d", "d"]
},
{
"before": ["<C-n>"],
"commands": [":nohl"]
}
],
"vim.leader": "<space>",
"vim.handleKeys": {
"<C-a>": false,
"<C-f>": false
}
}

练习-见missing-semester 文件夹

常用的VScode快捷键

基于 macOS,有些是自定义后的快捷键,请 command + k; command s 查看自己的键盘快捷方式

  • command + k; command s 查看键盘快捷方式
  • command + p 搜索文件
  • command + shift + p 打开命令面板
  • command + t 全局搜索文件内容
  • command + w 关闭当前文件
  • command + shift + n 新建窗口
  • command + shift + v 预览 markdown 文件
  • command + shift + k 删除光标所在的一行
  • command + / 切换单行注释
  • command + shift + / 切换块注释
  • command + j 切换终端
  • ctrl + ` 切换终端
  • command + b 切换侧边栏
  • command + , 打开设置
  • command + f2 + fn 选择当前文件内所有和光标所在位置相同的单词
  • command + 1 or 2 or 3 编辑区分区
  • command + l 选择当前行
  • command + [ or ] 缩进
  • command + k; command + o 全部折叠
  • command + k; command + l 切换折叠
  • command + option + [ or ] 折叠/展开代码块
  • command + k; command + [ or ] 递归折叠/展开代码块
  • command + shift + l 先执行一次 选择,再执行此命令可选择到所有相同文本或选项
  • command + option + shift + 上下或左右箭头 选择多行代码
  • option + shift + 拖动光标 选择多行代码
  • option + shift + 上下箭头 向上或向下复制行
  • option + shift + 左右箭头 向左或向右选择部分代码
  • option + 上下箭头 向上或向下移动行
  • option + 上下箭头 向上或向下移动行
  • f3 + fn 在结果中导航
  • command + d 选择多个结果
  • command + g 查找下一个
  • command + shift + g 查找上一个
  • option + enter 选择所有结果
  • f8 + fn 导航到错误或警告处

操作系统阅读笔记 - Three Easy Pieces

参考资料

  1. 免费书籍 PDF 版本
  2. Homework
  3. Projects
  4. 其余教辅资料请向contact@epubit.com.cn发送邮件获取
  5. ostep-Projects

第一章 - 虚拟化

第一节 - CPU 虚拟化: 时分共享-进程(和空分共享-磁盘)

进程: 操作系统为正在运行的程序提供的抽象

  1. 进程的机器状态组成:
  • 内存
  • 寄存器
    • 类型:程序计数器 & 栈指针 & 帧指针
    • 3 部分组成:状态,数据,指令
  1. 启动程序的过程:操作系统将程序的 代码静态数据从磁盘加载到内存中,同时为程序的运行时栈和动态内存分配内存,最后启动程序,通常是程序入口(以c为例通常是 main()函数)。

  2. 默认情况下进程都有3个文件描述符

  • 标准输入0
  • 标准输出1
  • 标准错误2
  1. 进程状态
  • initial: 创建进程时的状态
  • running
  • ready
  • blocked
  • final: 僵死状态,待回收,有时候会出现 parent progress 已经被回收但是 child progress 还在僵死状态时,此时有高一级的 init 进程回收,或者直接关机也行(有些垃圾程序只能关机。
1
2
3
4
5
# 进程主要状态
running <----------- 调度|取消调度 ----------> ready
| |
|---I/O: start---> blocked --I/O: finished--->|

  1. 上下文切换:操作系统保存和恢复该进程的寄存器内容的过程

进程 API(系统调用陷入内核模式,区别于用户模式)

暂时只考虑单 CPU 的情况,不管是什么进程,CPU 都有可能不断中断,切换任意进程,记住就算是亲子(进程)也是不存在尊老爱幼的,谁先运行取决于 CPU 调度程序,所以执行顺序是不确定的。⚠️ 这种不确定性贯穿全文

  1. fork():调用一次返回两次,争先恐后
  • fork()虽然复制了 parent progress,但是由于写时复制拥有私有虚拟内存(用户栈),但是调用时返回值却不相同,child progress 的返回值是 0,parent progress 的返回值是 child progress 的 pid。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 有必要理解一下返回2次, 示例代码(c)如下:
// 头文件省略
// 让我们先假定child progress先运行
int main(int argc, char *argv[])
{
printf("您好,当前运行进程pid:%d", int getpid());
int rc = fork()
// 不考虑fork失败
if (rc == 0) {
// child process, 这里根据 rc 的返回值运行一次 - !!返回一次
} else if (rc > 0) {
// parent process, 这里根据 rc 的返回值再运行一次 - !!返回两次
}
}

‼️ 注意:nodejs child_process.fork()虽然调用的是 fork(2),但是却有所不同,衍生的 child progress 不会克隆当前 parent progress,只能通过 IPC 通信。

1
2
3
4
5
# 命令行执行: node ./os-notes/chapter-5.js 输出结果:
此消息最新显示
衍生的child progress的 pid:95650
此进程的 pid 是 95649
^C
  1. exec():鸠占鹊巢,臭不要脸

其他变体:execl(), execle(), execlp(), execv(), execvp()等。‼️ 成功调用不会返回。

  • exec()会直接覆写当前进程的代码,堆,栈和其他内存空间等。所以新的程序的虚拟地址不变,进程 id 不变
  1. wait():parent progress 可以通过调用wait()来确保自己后运行,此时 2 种情况:
  • child progress 先运行,然后 parent progress 运行,天意如此。
  • parent progress 先运行,马上调用wait(),该系统调用会在 child progress 运行结束后返回,然后 parent progress 输出。
    总结:这 3 个系统调用可以用来实现 shell 的很多有用的功能,例如重定向输出,管道的实现方式等。

‼️ 问题:多进程程序是怎么抽象的?

进程是程序运行的一个实例,多进程就是程序运行的多个实例,fork()函数在新的 child progress 中运行相同的程序,child progress 是 parent progress 的复制品,进程上下文中将只有内数据不同(通过标记为私有的写时复制,复制时是完全相同的,运行后不同),代码中的数据完全相同,因为 fork 复制进程时只是重新分配了一段虚拟内存复制原进程内容,并且标记为只读,可以通过绘制进程图来了解 fork()。而通过exec()是在当前进程中的上下文中运行另一个新程序,将覆盖当前进程的地址空间(该进程所在的虚拟内存),但是PID(进程 id)不变且继承了调用该函数时已打开的所有文件描述符(即包含stdin,stdout,stderr3 个描述符)。更多内容请参考csapp第八章异常控制流一起食用。

机制: 受限直接执行(limited direct execution, 简称 LDE 协议)

LDE 协议的两个过程:1. 内核初始化陷阱表,并记住位置以便后续执行操作;2. 在进程中设置节点分配内存,用于保存陷入陷阱和返回的信息

此协议下需要解决 2 个问题:

  1. 受限制操作:引入用户模式(user mode)**,与之对应的是内核模式(kernel mode)**
  • 用户模式 和 系统交互 通过 内核模式暴露一些关键功能,如:访问文件系统,创建或销毁进程,或与其他进程通信以及分配更多内存等,称为系统调用。暴露功能的方式为在启动时设置 陷阱表 系统可以在 用户模式和内核模式中反复横跳
  • 例如:用户模式下进程不能发出 I/O 请求 ,否则会导致进程终止
  1. 进程切换:
  • 需要解决操作系统重新获取占据主动权的问题
    • 等待进程执行系统调用或者出错 - 该进程必须是个听话的好孩子才行
    • 时钟中断
  • 保存进程上下文:2 种可能,存储位置,存储内容和存储方式均不相同
    • 时钟中断时:用户寄存器有硬件隐式保存,使用该进程的内核栈
    • 调度程序切换进程:内核寄存器被软件 OS 保存,并存储在该进程的进程结构的内存中(‼️ 用户栈吗?)。
  1. 进程并发:锁?中断中禁止中断 - 禁止套娃?信号? => 并发章节会详细讨论
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 进程的地址空间:
-------------------------
| 内核栈(代码,数据,堆,栈)| ----> 用户代码不可见的内存
--------------------------
| 用户栈(运行时创建) |
--------------------------
| 共享库的内存映射区域 |
---------------------------
| 运行时堆 |
--------------------------
| 读/写段(.data,.bss) | ----> 从可执行文件加载
--------------------------
| 只读代码段 | ----> 从可执行文件加载
|(.init,.text,.rodata) |
--------------------------

进程调度:介绍

如何开发调度策略? 想一想什么情况下要使用调度策略?然后弄清楚评价调度策略的关键指标是什么?

工作负载假设:
1. 每一个进程运行相同的时间 -> 完全公平
2. 所有工作同时到达 -> 平均响应时间
3. 一旦开始,每个进程运行到完成 -> 运行时独占CPU
4. 所有进程不涉及I/O -> 进程任务单一
5. 已知每个进程的运行时间 -> 平均周转时间

调度指标:
1. 周转时间 = T完成时间 - T到达时间 => 性能指标
2. 公平
3. 响应时间 = T首次运行 - T到达时间
让我们设置一个时间函数 T(参与进程, 策略, [放开的假设条件])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 工作进程假设: 基于逐渐放开以上5个假设条件
# test1: 假定目前的进程为(随机排序执行):A(10s完成),B(10s完成),C(10s完成)
10s 10s 10s
-----|||||/////30s
A B C
# test2: 假定目前的进程为(随机排序执行):A(100s完成),B(10s完成),C(10s完成) - 假设1
100s 10s 10s
-------------------------------|||||/////120s
A B C
# test3: 假定目前的进程为(随机排序执行):A(100s完成)0s到达,B(10s完成)10s到达,C(10s完成)10s到达 - 假设1&2
100s 10s 10s
-------------------------------|||||/////120s
A B C
# test4: 假定目前的进程为(随机排序执行):A(100s完成)0s到达,B(10s完成)10s到达,C(10s完成)10s到达 - 假设1&2&3
10s 10s 10s 90s
-----|||||/////--------------------------120s
A B C A
  1. 先进先出FIFO:队列
  • 平均周转时间:
    • Tt(test1,FIFO,[]) = (10 + 20 + 30) / 3 = 20s
    • Tt(test2,FIFO,[1]) = (100 + 110 + 120) / 3 = 110s
  1. 最短任务优先SJF:FIFO 下一旦出现单个长任务周转时间就很长,改进一下策略
  • 平均周转时间:
    • Tt(test2,SJF,[1]) = (10 + 20 + 120) / 3 = 50s
    • Tt(test3,SJF,[1,2]) = (100 + 110-10 + 120-10 ) / 3 = 103.3s
  1. 最短完成时间优先STCF:SJF 下短任务后到达时,长任务的周转时间并没有改善
  • 平均周转时间:

    • Tt(test4,STCF,[1,2,3]) = (120 + 10 + 20) / 3 = 50s

    平均周转时间作为唯一指标的话,STCF表现良好。然鹅,引入分时系统后用户要求系统交互性好,所以引入响应时间作为新的指标
    平均响应时间:Tr(test4,STCF,[1,2,3]) = (0 + 10 - 10 + 20 - 10) / 3 = 3.3s;
    Tr(test1,FIFO,[]) = (0 + 10 - 10 + 20 - 10) / 3 = 3.3s;

  1. 轮转RR(Round-Robin): RR 在一个时间片内运行,然后切换到队列的下一个工作,反复切换执行直到所有任务完成。
  • 时间片(time slice)的长度为时钟中断周期的倍数: 以下示例假设 RR 时间片为 1s
  • 平均响应时间:Tr(test1,SJF,[3]) = (0 + 10 + 20) / 3 = 10
  • 平均响应时间:Tr(test1,RR,[3]) = (0 + 1 + 2) / 3 = 1
  • 假设 RR 时间片为 50ms: Tr(test1,RR,[3]) = (0 + 0.05 + 0.1) / 3 = 0.05s
  • RR 效率的关键就是时间片的大小,然鹅时间片太小会频繁切换进程上下文,影响整体性能
  • 摊销技术:将时间片的大小与切换上下文时间比较,1ms/10ms _ 100% = 10% => 太大;1ms/100ms _ 100% = 1% => 可以考虑
  • 平均周转时间:Tt(test1,RR,[3]) = (26 + 28 + 30) / 3 = 28s ‼️ 周转时间太可怕惹

两种调度程序:SJF,STCF 优化了周转时间,不利于响应时间;RR 优化了响应时间,不利于周转时间,接下来需要放开假设 4 和假设 5

结合I/O: 重叠,当一个进程被I/O阻塞,CPU可以切换其他进程运行
工作进程假设:A 50ms 运行10ms 发出I/O; B 50ms 没有I/O
1
2
3
4
5
6
7
# 没有重叠
A A A A A B B B B B
CPU: -- -- -- -- --||||||||||
I/O: -- -- -- --
# 重叠后
CPU: --||--||--||--||--||
I/O: -- -- -- --

实际上调度程序并不知道每个工作的长度,下一章将通过构建一个调度程序利用最近的使用情况预测未来从而解决未知工作长度的问题:多级反馈队列

多级反馈队列(Multi-level Feedback Queue)

这是我遇到的第二个多级,真是相当美妙的思想,第一个多级是多级页表(这本书它出现在第二章,我发誓我没有跳着看书 🐶)

多级反馈队列要解决 2 个问题:优化周转时间 && 降低响应时间 => 从历史中学习并预测

MLFQ基本规则:
MLFQ有许多独立的队列(queue),每个队列有不同的优先级(priority level)。一个进程只能在一个队列中,MLFQ总是优先执行较高优先级的进程,每个队列中的进程都拥有相同的优先级。
MLFQ的关键在于如何设置优先级。
规则1:若A的优先级 > B的优先级,运行A
规则2:如果A的优先级 == B的优先级,RR(轮转)运行

刚定下 2 个规则就出现一个问题:AB 两个进程在高优先级队列中,CD 两个进程在低优先级队列中,假设 AB 任务一直运行,CD 岂不是等到死都没法运行

如何改变优先级:
先考虑工作负载的类型:1.运行时间短,频繁放弃CPU的交互型工作;2.需要更多CPU时间,响应时间不重要的长时间计算密集型工作。
所以调整算法,增加规则:
规则3:工作进入系统,放到最高优先级 - 单个长工作(密集计算型)
规则4a:工作用完整个时间片后,降低到下一个优先级 - 长工作运行一段时间来了一个短工作(交互型)
规则4b:如果工作在时间片内动释放CPU,则优先级不变 - 交互型短工作执行大量I/O操作
以上实例皆运行良好,但是会出现长工作的饥饿问题:
系统中出现了大量的交互型短工作,导致长工作无法得到CPU
||
提升优先级:S太高长工作会饿死,太低交互型工作得不到合适的CPU时间比例
规则5:周期性提升所有工作的优先级,经过一段时间S,将系统中的素有工作重新加入最高优先级队列
更好的计时方式:修改规则4a & 4b:
规则4:一旦工作用来了某一层的时间配额(无论中间主动放弃了多少次CPU),就降低优先级 => 防止恶意程序愚弄CPU

MLFQ 调优及其他问题

  1. 配置多少优先级队列?

  2. 每一层队列的时间片设置为多大?

  3. 多久提升一次进程的优先级?

    大多数的 MLFQ 变体都支持不同队列可变的时间片长度。高优先级队列通常只有较短的时间片,因此这一层的交互工作可以更快的切换,相反,低优先级队列更多的是密集型工作

比例份额

多处理器调度

第二节 - 内存虚拟化

地址空间

内存操作 API

地址转换

  1. 分段
  2. 分页
  • 快速地址转换
  • 较小的表
  1. 交换空间
  • 虚拟化磁盘空间
  • 替换策略
  • 虚拟内存技巧及示例

空闲空间管理

第二章 - 并发

线程

基于并发的数据结构

条件变量

信号量

常见并发问题

基于事件的并发

事件循环:

1
2
3
4
5
6
# 伪代码
while(1) {
events = getEvents()
for (e in events)
processEvents(e)
}

基于事件的服务器如何决定事件的发生顺序?尤其是网络和 I/O

这里我们有一个例子,来首先了解一下select()函数:这个例子是从CSAPP上抄来的,select真是令人印象深刻
假设有一个echo服务器,它很强,一边接收网络请求,一边也可以响应用户的命令行键入请求。肿么办捏,先处理哪一个?更具体的代码示例请参考`《CSAPP》第12章第2节基于I/O多路复用的并发编程`
基于I/O多路复用技术的并发:使用select(),要求内核挂起进程,只有在一个或者多个I/O发生后才将控制权返回给应用程序(说人话:多个I/O请求注册到同一个select, select搞一个集合存储他们的状态,只要有I/O触发那么就由系统调用转回应用程序(用户模式))。

重要 API: select() or poll() 系统调用

select() 检查I/O描述符集合, 地址通过`readfds`,`writefds`, `errorfds`传入;
在每个集合中检查前nfds个描述符。
返回时,select()用给定请求操作准备好的描述符组成的子集替换给定的描述符集合,返回所有集合中就绪描述符的总数。

请注意 select()的超时参数,常见用法是设置为 NULL,但会导致无限期阻塞,直到有可用的就绪描述符。更好的做法是将超时设置为 0,因此让调用的 select()立即返回,这种方式提供了一种构建非阻塞(异步)事件循环的方法。

补充:阻塞与非阻塞接口
阻塞(同步)接口:在返回给调用者之前完成所有工作,非阻塞(异步)接口开始一些工作但是立即返回,从而让所有需要完成的工作都在后台完成。
通常阻塞调用的就是某种I/O。
非阻塞接口可用于任何类型的编程(例如使用线程),但在基于事件的方法中非常重要,因为阻塞的调用会阻塞所有进展。

‼️ 要分清楚并不是所有的非阻塞=异步,I/O 就不是,非阻塞 I/O!==异步 I/O

使用单个 CPU 和基于事件的并发服务器,线程并发的程序中存在的抢占锁释放锁等问题不复存在,因为只有一个线程,不会被其他线程中断,但是请务必记住‼️ 不要阻塞基于事件的服务器,即 node 中,小心使用同步 api.

阻塞系统的调用:I/O 大 boss 如何解决?

异步 I/O(Asyncchronous I/O):

  1. 发出异步读取
  2. 如何才能知道 I/O 已经完成,并且缓冲区(aio_buf)已经有了数据: aio_error 系统调用检查 aiocbp 引用的请求是否已经完成,如果完成则函数返回成功 (用 0 表示),否则返回 EINPROGRESS。对于每个未完成的 AIO,应用程序可以调用 aio_error 来周期性的轮询(poll)系统,以确定所述 I/O 是否完成。这条看着很累,看下一条,宝贝。
  3. poll 一个 I/O 害行,1000 个捏恐怕要累屎惹,所以某些系统提供了 基于中断(interrupt) 的方法。使用 UNIX 信号(signal)在异步 I/O 完成时通知一下程序,从而消除了轮询的痛苦,从今以后幸福快乐 🥰。
1
2
3
4
5
6
7
8
9
10
11
// AIO control block
struct aiocb {
int aio_fildes; /* file descriptor */
off_t aio_offser; /* file offset */
volatile void *aio_buf; /* location of buffer */
size_t aio_nbytes; /* length of transfer */
}
// AIO 第一步: 向文件发出异步读取,发出成功后会立即返回并且应用程序可以继续工作
int aio_read(struct aiocb * aiocbp);
// AIO 第二步:
int aio_error(const struct aiocb *aiocbp)
补充:UNIX信号
信号提供了一种与进程通信的方式,可以将信号传递给应用程序,进程将暂停当前工作开始运行信号处理程序。完成后,该进程就回复先前的行为。
每个信号都有名字,如:HUP(挂断),INT(中断),SEGV(段违规)
内核或者程序都可以发出信号
可以用kill命令行工具发出信号,例如:
prompt > ./main & [3] 36705
prompt > kill -HUP 36705
stop wakin' me up...

在没有异步 I/O 的系统中,纯基于事件的方法无法实现。然鹅可以使用某种混合方法,使用线程池来管理未完成的 I/O。参考《flash: an efficient and portable web server》来了解更多。

状态管理:基于事件的方法的另一个复杂问题

当事件处理程序(事件循环中待处理的一个事件)发出异步I/O时,必须打包一些程序状态,以便下一个事件处理程序在I/O完成完成时使用。而基于线程的工作是不需要的,因为状态都保存在线程栈内。
node是如何处理这个问题的?
据鄙人看完的《深入浅出nodejs》后的一些笔记来看,node只是JS运行在单线程上,异步I/O另有线程:
部分线程通过阻塞或非阻塞&轮询技术来完成数据获取,一个线程进行计算处理,通过线程间通信进行数据传递,实现异步I/O,也就是说node处理异步I/O是通过管理线程池来实现的。(没想到吧Σ(⊙▽⊙"a)

太难了

当系统是多核 CPU 时,基于事件的一些简单性(不用加锁,不存在线程中断)就没有了,为了利用多个 CPU,事件服务器必须并行运行多个事件处理程序,这时会出现「缓存一致性」?「加锁!必须加锁!!」,「发生缺页肿么办!被堵死了」等各种问题,现在先不要心塞,后面心塞的还多着呢,抽屉里放点硝酸甘油片啥的 ❤️。

思考:同步 I/O 包含非阻塞 I/O 吗?异步 I/O 呢?

I/O 包含两步:请求和数据复制到缓冲区

I/O 类型 请求 数据复制到缓冲区(aio_buf) 检查 I/O 是否完成的方法
同步 I/O 阻塞 阻塞 等前两步完成也就拿到了
非阻塞 I/O 异步 阻塞 轮询,CPU 很忙
I/O 多路复用 异步 阻塞 轮询,CPU 很忙(linux: epoll;)
异步 I/O 异步 异步 中断时发信号(windows:IOCP)

技术总结:组合使用用于网络的 select()接口和用于磁盘的 AIO 的调用(我猜 node 就是这样)

node 基于 I/O(AIO(通过线程池来实现异步 I/O))多路复用,所以是单线程(JS)、非阻塞、异步 I/O,JS 运行的单线程不会阻塞,但是 I/O 过程有可能是阻塞的,一旦遇到缺页就会卡住,就算主动去非洲客户也不会原谅你,OMG 生活真是苦涩。

思考: node 单线程是怎么实现并发的?

nodejs使用cluster(集群)创建多个共享服务器端口的child progress来利用多核CPU系统
原理是:child_process.fork()衍生的是独立的child progress,和 parent progress只通过IPC管道来通信,可以根据需要随时关闭或者创建,不影响其他进程。
cluster分发连接:主进程负责监听接口,然后循环分发给各child progress。

思考:多个进程为什么可以共享服务器端口?

首先要知道监听描述符和已连接描述的区别,是并发服务器的基础,每次一个请求到达监听描述符时,可以在此时派发fork()一个新进程来连接已连接描述符与客户端通信。服务器调用server.listen({fd:7})将消息发给主进程,parent progress将监听监听描述符并将句柄(handle)派发给child progress,child progress调用server.listen(handle)会显式监听handle而不是与 parent progress通信,child progress还会调用server.listen(0)会收到相同的「随机端口」,随机端口在第一次随机分配,而后都是已知的。
如果需要独立端口,可根据child progress的PID来生成。

思考 again: nodejs 主进程挂了肿么办?所有相关 child progress 会一起被回收还是移交给主进程挂掉时立即重启一个新的 parent progress 呢?

child progress死亡不会影响 parent progress, 不过child progress死亡时(线程组的最后一个线程,通常是“领头”线程死亡时),会向它的 parent progress发送死亡信号. 反之 parent progress死亡, 一般情况下child progress也会随之死亡, 但如果此时child progress处于可运行态、僵死状态等等的话, child progress将被进程1(init 进程,由内核创建,是所有进程的祖先)收养,从而成为孤儿进程. 另外, child progress死亡的时候(处于“终止状态”),parent progress没有及时调用 wait() 或 waitpid() 来返回死亡进程的相关信息,此时child progress还有一个 PCB (Process Control Block进程控制结构)残留在进程表中,被称作僵尸进程.

思考:让我们来想想 nodejs 进程管理 是怎么实现的?

第三章 - 持久性

I/O 设备

磁盘驱动器

RAID 冗余磁盘阵列

文件与目录

文件系统的实现

局部性和快速文件系统

崩溃一致性

日志结构文件系统

数据完整性和保护

思考:数据库

分布式系统

通信基础

Sun 的网络文件系统 NFS (Network File System)

Andrew 文件系统 AFS

思考:与计算机网络的联系

TCP 与 UDP 的比较

TCP 握手挥手机制

TCP 与 UDP 的丢包处理

实践:局部网络中的通信-wireless

Set up your Docker environment

  • 准备工作: 下载
  • Dockerfile: 项目中根目录下增加一个Dockerfile配置文件,以前端项目为例
  • docker 几个概念和常用命令:
    • 概念:
      • container: 容器,一个被隔离的且有自己的文件系统,网络的正常的操作系统进程,进程树独立于主机
      • image: 镜像,一个配置文件用来生成容器的
    • 常用命令:
      1. docker run your-Docker-image,
      2. docker ps --all
      3. Build and test: docker build --tag tagName:Vsesion .
      4. Run your image as a container: docker run --publish 8000:8080 --detach --name container-alias-name tagName:Vsesion
      5. Delete container: docker rm --force container-alias-name
1
2
3
4
5
6
7
8
9
10
# FORM: 从 node:12.18.1 这个线上的image继承
FROM node:12.18.1
# ENV 执行环境是生产环境还是开发环境
ENV NODE_ENV=production
# WORKDIR: 工作目录/app
WORKDIR /app
COPY ["package.json", "package-lock.json*", "./"]
RUN npm install --production
COPY . .
CMD ["node", "server.js"]

Build and run your image: In projects

1