helios741 / myblog Goto Github PK
View Code? Open in Web Editor NEW觉得好请点小星星,欢迎有问题交流(issue/email)
觉得好请点小星星,欢迎有问题交流(issue/email)
昨天(2018/04/21)参加城市格斗联赛75KG的自由搏击比赛。周六比赛,教练周一告诉我去打75KG的,这是第一次登台难免会紧张。周二是拳馆的实战课,和一个初次见面的七十公斤级的打,暴露了我身上的很多的问题,在这里记录一下:
我也在脑子中想了一下上面的问题,在几天的训练中也有所改变,对于这次比赛暴露出来的问题我觉得还是很严重的,我以为迈上拳台是克服自己内心恐惧的第一步,其实不然,在比赛的过程中对手用的三次后蹬腿,第二次的时候结结实实的蹬到了我的肋骨,我当时就感觉有点呼吸困难,但是还是忍了下来,在后面中就开始害怕,我敢向前,也害怕对手再出后蹬把我KO,在拳台上恐惧对手这就已经输了,这次也是以失败结尾。比赛完教练说,对方就是胜在拳打的准,而我主要是靠腿得分,我的拳基本就是王八拳乱抡。总结一下这次暴露出来的问题:
这次可能是因为对手出腿太慢了,每一次都被我挡住了,就主要是几个后蹬没有应对的策略,其实当时如果看出来是后蹬,踢一下他的前腿也就没事了。
原文: Interface vs Type alias in TypeScript 2.7
译者注:
经常有人在网上,在工作中,甚至在滑板公园询问我,在Typescript中定义编译时类型的类型别名和interface有什么区别。
我以前做的第一件事就是让他们去看Typescript的手册。。。
不幸的是在大多数时候,他们不能找到他们想要找到的东西(隐藏在“高级类型”部分)。即使你能找到它,这个信息描述的是过时的(描述的行为指针对[email protected]版本)。
好消息各位!你不必在看了,这篇文章是关于何时使用interface或类型别名的最新描述和风格指南。
"类型别名 可以和interface关键字一样,然而他们有一些细微的差别。"
这是对的
*interface*能够创建一个新的类型,*类型别名*不能创建一个新的类型
--例如:错误信息不能使用*类型别名*
这是不对的!(自从Typescript 2.1开始)
我们分别通过类型别名和interface定义编译时类型Point,并且实现使用interface和类型别名的类型参数实现两种getRectangleSquare
。
类型别名 和 interface 声明的Point
使用类型别名和interface定义getRectangleArea函数的参数
类型别名 和 interface声明的有相同的错误
两个相同的错误如下:
// TS Error:
// Interface:
Argument of type '{ x: number; }' is not assignable to parameter of type 'PointInterface'. Property 'y' is missing in type '{ x: number; }'.
// Type alias:
Argument of type '{ x: number; }' is not assignable to parameter of type 'PointType'. Property 'y' is missing in type '{ x: number; }'.
"第二个更加重要的重要的区别是类型别名不能被继承或者实现"
这个同样也是错的
我们能够通过一个interface继承类型别名:
interface 继承类型别名
类实现类型别名。
ThreeDimension 继承了PointType。PointType是类型别名声明的。
class 实现了interface和类型别名
"3. 类型别名 不能继承/实现 其他的类型别名"
当然这个也是错误的
类型别名能通过交叉类型运算符**&**扩展interface或者任意有效的Typescript类型(它类似字典或者javascript对象,非原始类型)。
类实现了具有交叉类型的*类型别名*
我们也能利用映射类型实现interface和类型别名的各种类型转换。
让我们通过Partial映射类型把Shape和Perimeter变为可选的。
类实现了通过交叉运算符和类型映射定义的*类型别名*。`perimeter()`和`area()`是可选的以至于我们没必要在类中去实现他们。
弱类型检测也正常工作
弱类型检测按期望的一样工作。
你可能偶尔想要定义一个可以充当一个具有额外属性对象或函数的对象。
我们这里讨论的是定义函数(可执行对象),和该函数的静态属性。
当与第三方库进行交互时,可以看到这种模式,这充分描述了类型的“全貌”
混合类型的定义和实现。
它和类型别名一样工作!
通过类型别名定义混合类型。
但是有一个非常微妙的不同。你将要在IDE中得到具体的类型信息去代替Counter类型。
使用类型别名和混合类型的interface的区别。
通常一个好的实践,是将我们的混合定义分为两个部分:
可调用对象(函数)类型别名
静态属性对象描述
这将要在编译的时候触发一个错误:
第一点不同——联合运算符定义的类型不能被实现
这完全有道理!一个图纸不能实现两个结构类型中的一个,所以在这方面没有什么好惊讶的。
类型别名联合使用用于定义对象是有意义并且有效的。所以下面会在编译时报一个错误,因为对象必须去定义perimeter()
和area()
两个中的一个。
联合类型——正确的使用对象字面量
第二个不同——联合定义类型不能被`interface`继承
同样,这个类的实现相似, interface
是一个“静态”图纸——它不能实现两个结构中的一个,所以它不能被联合类型合并继承。
interface有声明合并,但是类型别名没有。
什么是声明合并?
你能定义多次相同的interface,这些定义将要合并为一个。
声明合并
这种方式对于类型别名就不成立,因为类型别名是独一无二的实体(对于全局和模块域)。
第三个不同——类型别名不支持声明合并。
当我们为没有使用Typescript创作的库写第三方环境定义的时候,通过interface的声明合并是非常重要的,如果一些定义没有的话,使用者可以选择性的扩展它。
如果我们的库是使用Typescript写的,并且自动生成环境定义则同样使用。
这是唯一的用例,你应该总是使用interface而不是类型别名。
一般的,你要使用的一致就可以(无论使用类型别名还是interface),就我个人而言,我还是推荐使用类型别名:
type Props = {}
// BAD
interface Props extends OwnProps, InjectedProps, StoreProps {}
type OwnProps = {...}
type StoreProps = {...}
// GOOD
type Props = OwnProps & InjectedProps & StoreProps
type OwnProps = {...}
type StoreProps = {...}
在这遍文章中我们学到了在Typescript 2.7中interface和类型别名的不同。
有了这个,我们得出在特定的场景中应该如何去定义编译类型的结论。
让我们来回顾一下:
本文已经默认你已经知道generator
是什么以及for...of
和数据类型map
怎么用了。
先来一道思考题:
通过下面的变量
const students = {
xiaohong: {
age: '22',
fav: ['sleep', 'basketball'],
teachers: {
english: 'daming',
chinense: 'helios',
math: ['helios2', 'helios3']
}
},
xiaoming: {
age: '22',
fav: ['sleep', 'basketball', 'football'],
teachers: {
english: 'daming',
chinense: 'helios',
math: ['helios2', 'helios3']
}
},
}
对于第一个问题来说,我们可以使用各种循环语句:
for/while
for (let i =0 ;i < students[xiaoming].fav.length; i++) {
if (students[xiaoming].fav[i] === 'basketball') console.log(true)
}
let i = 0;
while(i++ < students[xiaoming].fav.length) {
if (students[xiaoming].fav[i] === 'basketball') console.log(true)
}
for...of
for (let item of students[xiaoming].fav ) {
if (item === 'basketball') console.log(true)
}
那么对于第二个问题来说,因为for/while
是不能遍历对象的,所以行不通,但是对象有一个专属的遍历方法
for...in
我们来看一下怎么通过for...in来遍历:
for (let stu in students) {
console.log(stu)
}
你可能会想了,通过for...in
去遍历数组会怎样呢?
我们看一下通过for...in
去遍历:
for (let item in students[xiaoming].fav) {
console.log(item)
// 或者去判断
}
哎呀,通过for...in
不也照样能实现数组的遍历么,那为什么不归结到数组的遍历里面去呢!
这里面还有一些细节需要去了解(这也是上面的“对象有一个专属的遍历方法”为什么加粗),我们通过一段代码去解释:
const num = [5, 6, 7]
for (let i in num) {console.log(i + 1)}
// 01
// 11
// 21
这是因为for-in
是为普通对象({key: value})设计的,所以只能遍历到字符串类型的键。
还有下面这个虽然不常用,但是也是不得不说的:
const arr = [5, 6, 7]
arr.foo = function() {}
for (let i in arr) {
console.log(i)
}
// 0
// 1
// 2
// foo !!!
foo
属于arr上面的方法,被遍历出来是说的过去的。
那么用for...of
我们来看看会怎么样
for (let stu of students){}
// Uncaught TypeError: students is not iterable
is not iterable,这个iterable
是神马东西,我们接下来下面一步步的看。
iterable是ES6对iteration(迭代/遍历)引入的接口。
如果一个对象被视为iterable(可迭代)那么它一定有一个Symbol.iterator
属性,这个属性返回一个iterator(迭代器)方法,这个方法返回一个规定的对象(这个后面会说)。也就是说iterable
是iterator
的工厂,iterable
能够创建iterator
。iterator
是用于遍历数据结构中元素的指针。
数据消费者: javascript本身提供的消费语句结构,例如for...of循环和spread operator (...)
数据源: 数据消费者能够通过不同的源(Array,string)得到供数据消费者消费的值;
让数据消费者支持所有的数据源这是不可以行的,因为还可能增加新的数据消费者和数据源。因此ES6引入了Iterable
接口数据源去实现,数据消费者去使用
可迭代协议(iterable protocol) 是允许js对象能够自定义自己的迭代行为。
简单的说只要对象有Symbol.iterator
这个属性就是可迭代的,我们可以通过重写(一些对象实现了iterable,比如Array,string)/添加(对于没有实现iterable的对象比如object,可以添加这个属性,使之变为可迭代的)该熟悉使之变为可迭代的。
当一个对象需要被迭代(for...of 或者 spread operator )的时候,他的Symbol.iterator
函数被调用并且无参数,然后返回一个迭代器。
迭代器协议定义了一种标准的方式来产生一个有限或无限序列的值。
当一个对象被认为是一个迭代器的时候,它会至少实现next()
方法,next()
方法返回两个属性value
(d迭代器返回的值)和done
(迭代时候已经结束)。
还有几个可选的方法可以被实现,具体请看:sec-iterator-interface
再文章的最开始我们已经说了再前ES6的时候,如何去遍历。
现在我们说说ES6新增的for...of
的作用。
在前面也已经说了,在ES6之前遍历object的时候用for...in
循环,for...in
会遍历对象上所有可枚举的值(包括原型(prototype)上的属性),比如下面这样:
function foo() {
this.name = 'helios'
}
foo.prototype.sayName = function() {
return this.name;
}
var o = new foo();
for (var i in o) {
console.log(i)
}
// name
// sayName
如果我们只想遍历对象自身的属性,可以使用hasOwnProperty
,如下:
function foo() {
this.name = 'helios'
}
foo.prototype.sayName = function() {
return this.name;
}
var o = new foo();
for (var i in o) {
if (!o.hasOwnProperty(i)) continue;
console.log(i)
}
如果我们不想让一个对象的属性在for...in
中被遍历出来,可是使用Object.defineProperty
来定义对象上的属性是否可别枚举(更多的属性请看:Object.defineProperty()),具体如下面代码:
var obj = {name: 'helios'}
Object.defineProperty(obj, 'age', {
enumerable: false
})
for (var i in obj) {
console.log(i)
}
在这一小节的最后我们来说说for...in
中的in操作符的含义:
prop in obj
:
我们来看一组例子:
var o = {
name: 'heliso'
}
console.log('name' in o) // true
console.log(Symbol.iterator in o) // false
console.log('toString' in o) // true
这个操作符虽然也适用于数组,但是尽量还是不要用在数组中,因为会比较奇怪,如下代码:
var arr = [6, 7,8]
console.log(7 in arr) // false
console.log(1 in arr) // true
console.log('length' in arr) // true
主要是前两个比较奇怪对不对,因为对于数组prop
代表的是数组的索引而为其存在的值。
按照这样的思路,正在看的读者你能思考一下in
操作符在字符串中是怎么的模式么?
只要是实现了Interable
接口的数据类型都能被遍历。
javascript内部实现的有:
并不是所有的iterable内容都来源于数据结构,也能通过在运行中计算出来,例如所有ES6的主要数据结构有三个方法能够返回iterable对象。
那就数据结构(数据源)去实现iterable就可以了。
用通俗的话说就是,你如果要遍历一个对象的话,有一下几个步骤:
Symbol.iterator
那就去实现Symbol.iterator
函数要返回一个iterator
iterator
返回一个对象,对象中至少要包含一个next方法来获取value
和done
上面我们已经铺垫了这么多了,我们说了javascript中object是不能被迭代了,因为没有实现iterable
,现在让我们来实践一下让object变的可迭代。
下面这样写肯定是不行的
const obj = {
name: 'helios',
age: 23
}
for (let it of obj) {
console.log(it)
}
// TypeError: obj is not iterable
const obj = {
name: 'helios',
age: 23,
[Symbol.iterator]: function() {
let age = 23;
const iterator = {
next() {
return {
value: age,
done: age++ > 24
}
}
}
return iterator
}
}
如果iterable
和iterator
是一个对象的话,上面的代码可以简化为:
function iterOver() {
let age = 23;
const iterable = {
[Symbol.iterator]() {return this},
next() {
return {
value: age,
done: age++ > 24
}
}
}
return iterable
}
for (let i of iterOver()) {
console.log(i)
}
我们如果每次想把一个不能迭代的对象变为可迭代的对象,在实现Symbol.iterator
的时候,每次都要写返回一个对象,对象里面有对应的next方法,next方法必须返回valua和done两个值。
这样写的话每次都会很繁,好在ES6提供了generator(生成器)能生成迭代器,我们来看简化后的代码:
const obj = {
name: 'helios',
age: 23,
[Symbol.iterator]: function* () {
while (this.age <= 24) yield this.age++
}
}
for (let it of obj) {
console.log(it)
}
知乎的这个回答是很有水平的了:为什么es6里的object不可迭代?
在stackoverflow中也有很高质量的回答:Why are Objects not Iterable in JavaScript?
在上面的回答中从技术方面说了为什么Object不能迭代(没有实现iterable),还说了以什么样的方式去遍历Object是个难题,所以把如何迭代的方式去留给了开发者。
但是还是要思考的一个问题就是:我们真有必要去迭代对象字面量么?
想一下我们要迭代对象字面量的什么呢?是keys
还是values
亦或者是entries
,这三种方式在ES6提供的新的数据类型map里面都有呀,完全是可以代替object的。在这里不说object
和map
的区别,只是说说在ES6以后我们想把两个事物关联起来的时候,不一定要非得是用对象字面量
了,map
支持的更好一下。
对于什么时候用对象字面量(object)什么时候使用map我们可以做一下总结:
对象字面量(object)应该是静 态的, 也就是说我们应该已经知道了里面有多少个,和对象的属性有什么
使用对象字面量(object)的一般场景有:
JSON.stringify
和JSON.parse
时候其他的情况用map,其他的情况包括:
也并不说是map
就肯定比对象字面量(object)好,map
也有如下的缺点:
JSON.stringify
/JSON.parse
ESC + b
ESC + f
Control + d
Control + k
Control + u
Control + w
Control + i
实体:
参考文章:
到2019年1月1日前应该完成的事情:
如果拿汽车来做比:
Docker好比汽车引擎,
Dockerfile相当于汽车蓝图,
Docker image(镜像)就是汽车样板,
Docker container(容器)类似于汽车的零部件,
Docker Registry可以看作是4s店,
Docker Compose就像老司机,
Docker Volume就像是汽车的油箱, 如果把容器间内的io数据流比喻成汽油,
Docker Swarm(或者K8s)就是交通枢纽。
这是这个系列博客中的第一篇,目的在解决‘每个博士生都应该知道的52个问题’以便更好的学习密码学。这系列问题已经被编写完成,目的是让博士候选人知道在第一年结束的时候他们应该知道自己应该知道什么-那些早起对博士生提醒的制度最后能尽快的废除。无论如何,我们将要在明年呈现问题的答案,我已经自愿完成了这个博客系列的第一件‘思考’。这第一个问题是讨论计算机体系结构和引出了下面几个问题
下面的几个有什么区别
通用处理器没有严格的定义,人们普遍的认为如果处理器是图灵完备的那么这个处理器就是通用的。这就捕获了能够计算任何可以计算内容的处理器(例如: 能够解决图灵机器解决的问题)。我不想钻研图灵机器的定义,如果你觉得我这样不求甚解不好那么我建议你去研究计算理论[1]。注意到这个定义中没有性能和指令集合的概念,事实上,一些研究者已经经过复杂的证明过程证明过了,你可能只需要一条指令就能完成图灵计算[2]。在现代处理器的背景下,大多数可编程的CPU被认为是通用的。
通用的目的往往带来性能上的损失。特别的,通用处理器能够计算任何能够计算的东西,但是它从来不能在重复并且复杂的任务中表现出色。鉴于在各个应用的通用处理器上跑定期重复的任务,处理器的设计者可能整合指令集的扩展到微架构为了适应任务。从功能上讲,微架构功能可能没有区别但是最终用户可能会收获到巨大的性能提升。
因为在座的都是密码学的研究者,我将依照一个密码学的例子去讲指令集扩展。考虑到具有AES加密的台式机。从二级存储读取任何东西都需要CPU中断才能缓存之前解密的数据。鉴于从为命中缓存的硬盘访问被认为是比较可怕的,在顶部增加加密程序和你的瓶颈值得你再次考虑你的硬盘加密程序。在这里应该非常清楚AES是复杂的可重复任务,拥有一个带有简单指令集的通用处理器CPU,我们没有选择只能将解密实现为线性操作。Intel和AMD都认识到对磁盘加密的需求以及AES增加二级存储访问的负担,并且已经生产了AES-NI x86指令集扩展来加速在台式机上磁盘的加密速度。
如果你想完全加速各种运算,最佳的解决方案就是使用专用处理器或者使用专用集成电路(ASIC)。我们失去了通用处理器授权的灵活性这一特征,以便换取性能上的提升。这些类型的处理器经常紧密耦合在通用处理器中,因此称为协处理器。注意到,一个协处理器经常与通用处理器封装在同一个包当中,但不一定要集成到处理器的架构中。如果我们转向现代处理器的架构,Intel和AMD已经把声卡,图形处理器以及DSP引擎集成到CPU中一段时间了。附加功能通过专用寄存器公开,协处理器作为通用处理器必须管理的独立组件。
最后我们转向介绍现场可编程门阵列(FPGAs),这个是专用集成电路(ASIC)和通用处理器的中间选择。如果一个应用需要高性能的吞吐了量并且需要频繁的进行变更,那么FPGA是一个很好的选择。为了更好的理解FPGA的工作原理,考虑到有上千个逻辑门的面包板(breadboard),这个面包板上放置着数千个逻辑门和查找表。如果你将应用描述为一组门和时间约束,那么你可以在面包板上将他们连接在一起,并生成一个能够评估你应用程序的电路。一个FPGA得到了可再次编程的便利性,同时提供专用逻辑去评价目标应用。FPGA与通用程序的关键区别在于如何设计和构建你的应用。为了更加充分的利用你的硬件,你必须将你的应用程序描述为一组硬件组件和用硬件描述语言的事件(Verilog or VHD)。在成产之前,这个过程长用在FPGA上对通用和专用处理器进行原型设计。然而,这并不是没有任何缺点。使用低级构建块设计程序对于大型应用来说变得非常麻烦。除此之外,与通用嵌入式IC相比,能源消耗和硬件成本变得更高。最近,FPGA制造商Xilinx开始向提供有ARM通用内核并且集成在一个包中的FPGA[3]。现在,FPGA可用于ARM内核就像协处理器一样灵活。因此,你能够构建专用的逻辑去评价你的加密原语,因此能够加速加密应用程序。
总结,通用处理器是具有能计算任何可以计算的东西的能力。类似地,对于具有指令集扩展的通用处理器,能够在特定的应用中拥有很好的表现。专用处理器或者协处理器对于特定的任务是非常快的,但是它不能计算其他的任何东西。FPGA可用于构建上述硬件,但是牺牲了ASIC解决方案的灵活性。
test issue
这周末就实习结束了,也没有什么事情了,就对这次实习进行一下总结,实习时间是从2017年6月到2017年十月底,四个多月的时间。
刚开始以为每周或者每个月写一次实习总结,发现还是有点懒惰再有就是工作后来渐渐的忙了起来,剩余的时间有点少了。所以就这次集中起来写一次吧。
刚来的时候发现百度这里环境真的是不错,不是几个人坐在一个长排的桌子上,而是每个人都有一个自己的小独立空间,简单如下图所示:
然后部门的领导给我分配了导师叫swf(现在也成为好朋友了),我放下包收拾了一下,他带我去休息区7788的和我说了一些我听不懂话,我当时心里想:“怎么写代码之前要这么多事情,还什么卡片乱七八糟的,头好大”,但是现在也基本熟悉了流程。
因为是十点上班,所以这一套完了之后基本就快十二点了,所以就去吃饭了。发现食堂的环境还是很nice呀,各种风味都有,但是对于我这种穷酸学生来说还是比较贵的了,但是度美食还是比较实惠的,所以一直到现在我都去那里吃。这时候也上一张图,免得以后会忘记。
工作是不打卡制的,有的人来的早,早早的把工作做完了,在六点就可以走了。
第一周主要是学习工作中要用到的技术以及公司的规范。按照新人wiki中流程走,最后要写一个新人作业,要技术厉害的人去review。
做了几次的新人作业,给我的感受就是公司用的技术还是相对比较落后,好多的东西都是用的内部的轮子,不是用的市面上比较流行的东西,这一点是让我当时比较不容易理解的,后来也慢慢的理解了,因为大公司的业务比较多,开发的时间也是很长的了,所以重构起来还是比较困难,所以要慢慢重构,我把前端代码拉下来的时候发现,光代码竟然有600+M。
因为工作前也知道了在工作中使用的是react,所以在学校也学了一周左右的react,在工作用了react还是比较容易上手,适应工作又是另一部分了。
管理React
组件通信数据的不使用的Redux/Flux
,还是用的内部管理的数据的工具叫React-ER
。关于React-ER
和Redux
的区别,我已经在前面的文章提到过了工作中的ER和Redux的区别
用的babel
的版本比较低,所以好多ES6的方法还是不能用的,所以经常会用的underscore
。
发送请求也不是用的当前比较流行的fetch
,也是用的部门内部自己封装的ajax请求的一个库。
https://blog.csdn.net/donahue_ldz/article/details/17139361
)
想下移动一个段(一般是一个大括号)
(
同上
gx
比如g0就代表移动到屏幕的第一行
fc
把光标移动到同一行的下一个字符c处
Fc
把光标移动到同一行的上一个字符c处
0
:x
gg
https://blog.csdn.net/donahue_ldz/article/details/17139361
G
control + b
control + f
control + d
control + u
M
容器就是一个进程,已经在宿主机上对容器进行了隔离,对容器资源进行限制,那么容器的文件系统呢?
容器的文件系统也应该和network/PID一样和宿主机进行隔离,这个就要用到了Mount Namespace
了。
看一下在DOCKER基础技术:LINUX NAMESPACE(上)的一个程序:
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
/* 定义一个给 clone 用的栈,栈大小1M */
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];
char* const container_args[] = {
"/bin/bash",
NULL
};
int container_main(void* arg)
{
printf("Container - inside the container!\n");
/* 直接执行一个shell,以便我们观察这个进程空间里的资源是否被隔离了 */
execv(container_args[0], container_args);
printf("Something's wrong!\n");
return 1;
}
int main()
{
printf("Parent - start a container!\n");
/* 调用clone函数,其中传出一个函数,还有一个栈空间的(为什么传尾指针,因为栈是反着的) */
int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWNS | SIGCHLD, NULL);
/* 等待子进程结束 */
waitpid(container_pid, NULL, 0);
printf("Parent - container stopped!\n");
return 0;
}
我们通过clone系统调用创建了一个新的子进程container_main
,并且声明要为它启动Mount Namespace(CLONE_NEWNS)。
小知识🈯️:
为什么Mount Namespace对应的宏叫做CLONE_NEWNS
而不是CLONE_NEWMOUNT
的呢,这是因为Mount Namespace是Linux中的第一个namespace,当时以为就这一个了,所以也就这样了。
编译:
# gcc -o ns ns.c
# ./ns
Parent - start a container!
Container - inside the container!
这时候我们的shell就在container_main这个进程里面,我们可以通过ps看到只有这个namespace的进程。但是我们通过ls /或者ls /tmp发现文件系统并没有做隔离,还都是宿主机的文件系统。
这个是因为容器进程会默认继承父进程的文件系统,所以创建新进程的时候还有显示去声明那些目录要重新mount,比如我们要重新挂在/tmp这个目录,所以子进程的代码可以改为:
int container_main(void* arg)
{
printf("Container - inside the container!\n");
mount("none", "/tmp", "tmpfs", 0, ""); // 告诉容器以tmpfs(内存盘)的格式重新挂在/tmp目录
/* 直接执行一个shell,以便我们观察这个进程空间里的资源是否被隔离了 */
execv(container_args[0], container_args);
printf("Something's wrong!\n");
return 1;
}
编译之后我们通过ls /tmp
查看这个目录下面就没有什么文件了。
我们每次启动容器,大多时候不是想继承父进程的文件系统,我们首先想到的就是直接把根目录(/)重新挂在了不就完了么。
在Linux中的chroot就能帮助搞定这个事情,chroot的用法比较简单,
通过chroot就能把ubuntu/centos的文件系统挂在到容器的根目录。这个就是rootfs了。
由于rootfs打包的不仅仅是应用程序,而是将整个操作系统的文件和目录一起打包进去,这也就意味着应用和它的依赖,这也就解决了云端和本地的配置不一样的问题了。
分为三层:只读层(readonly + whiteout)
,init层(ro + wh)
,可读写层(rw)
ubuntu和centos用得不一样
docker的volume在这里面说一说
POD的恢复过程,永远是发生在当前节点上的。事实上,一旦一个POD和一个node绑定,除非绑定发生了变化(修改pod.spec.node),否则就算这台机器挂了,也不会调度到其他的机器上。
而如果你想让 Pod 出现在其他的可用节点上,就必须使用 Deployment 这样的“控制器”来管理
Pod,哪怕你只需要一个 Pod 副本。
目前在项目中还没有使用任何数据管理工具,目前的状态都是通过放在父组件的state里面。(前面的两篇文章也介绍了项目的主要结构:编写不用redux的React代码,Typescript配合React实践)。
一直不舍得在项目中使用数据管理工具的原因有下面两点:
不用数据管理工具能解决问题,但是解决的不优雅
我们一般会有一个组件,把一个项目所有的路由都放在里面进行配置,没有过多的逻辑。但是项目要求在展开详情的时候显示面包屑,如下图一下:
先看一下路由组件的现在代码:
<Fragment>
<Route exact path="/user" component={User} />
<Route path="/user/:id" render={(props: IProps) =>
<UserDetailWrapper
{...props}
getBCPaths={(bcPaths: IPath[]) => this.getBCPaths(bcPaths)}
/>
} />
<Route path="/ldap" render={(props: IRouterProps) =>
<Ldap
{...props}
getBCPaths={(bcPaths: IPath[]) => this.getBCPaths(bcPaths)}
/>} exact />
<Route path="/ldap/:id" render={(props: IProps) =>
<LdapDetailWrapper
{...props}
getBCPaths={(bcPaths: IPath[]) => this.getBCPaths(bcPaths)}
/>} />
<Switch>
<Route path="/k8s-queue" component={K8sQueue} exact />
<Route path="/k8s-queue/create" component={K8sFormWrapper} />
<Route path="/k8s-queue/:name" render={(props: IK8sProps) =>
<K8sDetailWrapper
{...props}
getBCPaths={(bcPaths: IPath[]) => this.getBCPaths(bcPaths)}
/>}
/>
</Switch>
<Switch>
<Route path="/yarn" component={YarnList} exact />
<Route path="/yarn/create" component={YarnFromWrapper} />
<Route path="/yarn/:id" render={(props: IProps) =>
<YarnDetailWrapper
{...props}
getBCPaths={(bcPaths: IPath[]) => this.getBCPaths(bcPaths)}
/>} />
</Switch>
<Route path="/updateLdap/:id" component={LdapForm} />
<Route path="/createUser" component={CreateUser} />
<Route path="/monitor" component={ModuleStatusModule} />
<Route path="/node-list" component={NodeListModule} />
<Route path="/job-list" component={JobListModule} />
<Route path="/config" component={ConfigCenter} />
<Route path="/license" component={LicenseList} />
</Fragment>
上面只是其中一部分,能明显的看到好多组件中多了个getBCPaths
这个方法,这个方法就是在通过父子组件通信的方式得到具体组件的面包屑路径,我们就以上面代码中的UserDetailWrapper
这个组件来说,这个组件是个有状态组件,按理说不应该承担这种业务,但是还是不得不在组件的componentDidMount
中去回调这个方法来把面包屑的路径传递出去。UserDetailWrapper
的代码如下:(本来想都贴上,后来有一百多行就贴个简化版本的把)
import React, { ReactNode, PureComponent } from 'react';
import { RouteComponentProps } from 'react-router';
export interface IProps extends RouteComponentProps<{ id: number }> {
getBCPaths?: (bcPaths: IPath[]) => void;
}
class UserDetailWrapper extends PureComponent<IProps>{
componentDidMount() {
const { getBCPaths } = this.props;
const bcPaths: IPath[] = [
{
name: '用户列表',
path: '/user',
},
{
name: '详情',
},
];
getBCPaths && getBCPaths(bcPaths);
}
render(): ReactNode {
const { user, loading, namespaceList, yarnList, workspace, complex } = this.state;
return (
<UserDetail
user={user}
loading={loading}
/>
);
}
}
export default UserDetailWrapper;
如果引入了数据管理工具之后,这些面包屑的地址,都放在配置文件中,然后在在管理路由的中进行简单匹配就可以了,这样就解决了组件和路由之间的强耦合。
比如这样的一个场景:在管理员创建用户的时候,可能会给该用户分配了一个资源A,这个资源列表接口和当前的场景无关的。 当我们在查看用户详情的时候,可能会给这个用户修改这个资源,可能把A资源改为B。这个时候在不同的路由页面(创建用户页面和用户详情页面)都会请求一遍获取资源列表这个接口。
可能这样场景会有很多。当然我们也可以通过上面在面包屑的方式,把数据存在最顶层组件(配置路由组件),但是这样带来危害和不可维护性可能比调用多次接口的影响要大。
因为这个场景很有画面感,就不上代码了。
当组件的之间的嵌套层级很深的时候,可能父组件的数据要到子子子组件才能用,中间有过多的透传代码是没必要的。这个也是很有画面感的。
比如说管理用户这个操作(不知道叫操作合不合适,也可以说把操作改为界面),可以由多个地方去管理,在A这个地方能进行管理,在B这个地方也能进行管理。暂且不说这种方式好不好,或者说这样的产品设计的好不好,但是就真的是遇到了场景如下:
先解释以下工作区的概念,可以认为是多个用户共同使用同一份东西。
现在不一样了,不仅从这个地方能进如工作区管理,还在只有管理员能进的控制台增加了个一模一样的,如下图:
这样堪比Apple的设计,随之而来的就带来了以下两个问题:
对待第一个问题,我的方案就是瞎设计。以前叫workspaces现在叫ws-list
对待第二个问题:
只能把以前的路由copy一份改改,放在现在的路由里面咯:
以前的路由:
<Route key="list-active" path="/workspaces" component={WorkspaceList} exact />
<Route
path="/workspaces/create"
render={(props: IRouterProps) =>
<WorkspaceForm
{...props}
getBCPaths={(bcPaths: IPath[]) => this.getBCPaths(bcPaths)}
/>
}
/>
<Route
path="/workspaces/:id"
render={(props: IRouterProps) =>
<WorkspaceDetailWrapper
{...props}
getBCPaths={(bcPaths: IPath[]) => this.getBCPaths(bcPaths)}
/>
}
exact
/>
现在变了这样,只是多了一个参数:
<Route path="/ws-list" render={(props: IProps) =>
<WorkspaceList
{...props}
baseUrl="ws-list"
/>
} exact />
<Route
path="/ws-list/create"
render={(props: IRouterProps) =>
<WorkspaceForm
{...props}
baseUrl="ws-list"
getBCPaths={(bcPaths: IPath[]) => this.getBCPaths(bcPaths)}
/>
} />
<Route path="/ws-list/:id" render={(props: IProps) =>
<WorkspaceDetailWrapper
{...props}
baseUrl="ws-list"
getBCPaths={(bcPaths: IPath[]) => this.getBCPaths(bcPaths)}
/>
} />
然后在相应的组件里面只用this.props.baseUrl
默认值是workspaces
。
当前也就只能这样解决了,如果使用redux虽然不能解决大问题,但是还是能解决解决在配置路由的组件中减少逻辑代码。
最近也看了一下比较流行的mbox
是基于响应式编程的,引入的概念也不像redux那样很多,总的来说是同不错的。但是还是不像做公司第一个吃螃蟹的,还是安安稳稳的使用redux了。
这玩意儿要啥方案,直接干就完了
使用redux-action可以有效的简化actionCreators和reducers的代码。具体了解请看使用 Redux 打造你的应用
为了防止redux的代码平铺使得开发人员变得烦躁,所有都一个文件夹目录下然后通过index导出,如下:
home
├── components
│ ├── list.js
│ ├── recommend.js
│ ├── topic.js
│ └── writer.js
├── containers
│ └── header
│ └── index.jsx
├── index.js
├── store
│ ├── actionCreators.js
│ ├── actionTypes.js
│ ├── index.js
│ └── reducer.js
└── style.js
store目录下面的index.js
如下:
import reducer from './reducer'
import * as actionTypes from './actionTypes'
import * as actionCreators from './actionCreators'
export { reducer, actionCreators, actionTypes }
redux分为全局状态和局部状态,上一步我们说的是局部状态,如果有比如说loading
等全局状态的时候要放在app目录下面使得所有组件都能获取到
开发式系统通信互联(OSI)模型定义了一种实现了在层之间实现网络协议,并且控制一层传递到下一层的网络架构。当今它最主要的用途就是作为教学工具。它在概念上将计算机网络划分为七个不同的层次。低层处理电信号、二进制帧以及通过网络路由这些数据。高层涉及到网络的请求和相应、数据表示以及从用户角度网络协议的展示。
第一层,在OSI模型中的物理层是控制数字数据位(bits)从发送设备(源)的物理层通过网络通讯媒介到达接收(目的)设备物理层。物理层的技术包括:以太网电缆( Ethernet cables)和令牌环网络(Token Ring networks)。除此之外,集线器(hub)和其他中继器(repeaters)以及电缆连接器也是工作在网络层的标准网络设备。
在物理层,使用物理介质(电压、无线电频率、红外线或者普通脉冲波)支持的信号类型传输数据。
当从物理层获得到数据的时候,数据链路层检查物理传输错误,然后将位打包位数据帧。数据链路层也管理物理寻址方案,例如以太网的MAC地址,控制不同的网络设备对物理媒介的访问。以为数据链路层是OSI模型中最复杂的层,它也被拆为“媒体访问控制”和“逻辑连接控制”两个子层。
网络层在数据链路层之上增加了路由的概念。当数据到达网络层,包含在帧内的源地址和目的地址将要被检查,为了确定数据是否到达了目的地。如果确认后到达的是目的地,那么网络层将数据格式化为能够传递到传输层数数据包。否则,网络层将要更新目的地址并且将这个数据帧退回下层。
为了支撑路由,网络层保存了设备在网路中的逻辑地址,例如ip Address。网络层也管理逻辑地址和物理地址的映射。在IP 网络中,这个映射是通过Address Resolution Protocol (ARP)完成的。
传输层通过连接的网络传输数据。TCP是传输第四层网络协议network protocol最常见的例子。不同的传输协议可能支持一系列可选的功能,比如:错误回复,流量控制以及重传的支持。
会话层管理启动和拆除网络连接事件的顺序和流程。在第五层,它支持多种类型的连接,这些连接能够被动态的创建和运行在各个网络中
表示层是OSI模型中功能最简单的一层。它处理消息数据的语法格式,例如格式转换以及支持应用层的加密和解密操作。
应用层提供网络服务给端用户的应用。网络服务通常是用户数据一起使用的协议。例如web浏览器应用,应用层协议HTTP打包发送和接受页面所需要的数据。第七层想表示层提供数据(并从中获取数据)。
看完了几篇RESTFUL API
的教程文档之后,发现github restful V3
实践的是相当到位的,把感受到的优势说一下:
teams
中邀请成员的时候,如果该用户不存在在orgs
中,就会返回相应的状态码,相当于报错。但是后者就做到了平滑的升级,当用户不存在在orgs
中的时候就发一封邮件给这个用户,然后把这个用户的状态设置为penging
,当用户同意加入teams
的时候就把该用户的状态设置为actived
http
的Accept
头部字段进行制定。event
这个资源为例子,如果在缓存时间内没有新的event
被触发,那么服务端会返回的是304org
争取但是username
不正确那么返回404,如果org
错误就会重定向到check-public-membership梳理了github主要的资源以及对资源的操作。
上图主要花的是资源之间的"从属"关系图,可能您会疑问为什么要用虚线,因为这里的从属是一种“可以”的关系,不是一种“必须“的关系,比如果user
-> orgs
,要获取orgs
不一定是要通过user
,也可以通过其他方式,比如get-an-organization。
从总览图中我们知道,通过user
我们可以得到这个user
的gists
,orgs
,repos
等信息,下图展示了以user
为中心怎么对应资源进行操作。
参考文章:
kubernetes/pkg/controller/ 下面的都是控制器,这个下面的都是遵循 kube-controller-manager的。
他们都遵循统一的编排模式,即:控制循环,下面的伪代码来解释一下:
for {
实际状态 := 获取集群中对象 X 的实际状态(Actual State)
期望状态 := 获取集群中对象 X 的期望状态(Desired State)
if 实际状态 == 期望状态{
什么都不做
} else {
执行编排动作,将实际状态调整为期望状态
}
}
实际状态的来源可能是:kubelet通过心跳得到容器或节点的状态,监控系统保存的应用监控数据,控制器主动收集
以Deployment
控制器模型的实现举个例子:
这是这个系列博文的第三篇,为的是解决“每个博士生都应该知道的52件事情”这个系列。这个问题的集合已经被写进博士候选人在第一年结束应该知道的事情里面。我们将在明年呈现每个问题的答案,每周一个,我是被分配到第三个问题的学生。
评估相对的计算和存储能力
为了测量设备的计算能力,我们能估计处理器的时钟速度。如果处理器能够实现某种形式的并行性那么可能会被误解--两核以2GHz运行明显比单核以2GHz运行更具有计算能力,所以找到一个直接的量化指标是不实际的希望。对于像通用图卡等特殊设备,通常会报告设备设备能够维持的总FLOPS(每秒浮点运算)(要么是单精度要么是双精度),但这个措施运用到任何问题时都不是可靠的选择。确实,某些服务通过在各种问题实例上通过对不同的设备进行性能基准测试来便于比较--例如,CompuBench(一个性能测试工具)。幸运的是,问题中包含的设备的功能范围足以减少对量化指标的依赖。
可以更加轻松的找到每个设备的存储功能的度量:我们可以简单的比较设备能够在永久存储上的大致信息字节数。
智能卡具有最少计算量的设备:时钟速度会因实现的不同而变得不同,但是人们可能看到达到20MHz核心速度。在存储方面,典型的智能卡可能只有两千字节。
微型控制器是“包含处理器内核,内存和可编程输入/输出外设的单个集成电路上的小型计算机[1]”存储能力和可计算能力会根据微型处理器的确切定义而变得不同,但是以建议的传感器节点为例子,一个典型的微型控制器可能和智能卡有类似的计算能力和稍微多的存储可用,可能是几千字节到几兆的量级。
像手机这种移动计算机有更多存储可计算能力,可用的电量随时间的推移而迅速增加。以苹果的2008版本和2013版的nexus5来说,苹果使用的是412MHz 32位的ARM芯片,nexus5的CPU使用的是2.3GHz的四核处理器。在存储方面,如果我们忽略一些手机使用移动硬盘,在2013年的高端手机可能提供16到32千兆字节的存储空间。
最后,大多数笔记本电脑和台式电脑拥有比手机更强的处理能力:高端的intel“Haswell” i7 4960K处理器包含四个核每个核有4GHz的时钟,AMD "Piledriver" FX-9590 CPU 包含八个核每个核有4.7GHz的处理能力---注意两个处理器之间直接比较不仅仅评估核数量和时钟速度。还有其他的因素影响台式或者笔记本计算机的计算能力--特别的,对于某些问题,添加图形处理单元可以得到性能上的大幅度提升。台式或者笔记本计算机的容量变化很大,但是消费者机器上的典型存储量在数百个千兆和几兆兆--目前最大的单硬盘容量约为8兆兆。
vim /etc/kubernetes/manifests/kube-apiserver.yaml
在master节点中发现apiserver监听的0端口,在etc/kubernetes/manifests/kube-apiserver.yaml
增加了两个字段--insecure-port=
和service-node-port-range=1-65535
重启kubelet:systemctl restart kubelet
然后通过命令netstat -pantl | grep 8989
发现监听的是127.0.0.0又在etc/kubernetes/manifests/kube-apiserver.yaml
增加了--insecure-bind-address
字段为10.0.0.0`就能curl通了
vim /etc/hostname
vim /etc/hosts
curl https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | apt-key add -
cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
deb https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main
EOF
apt-get install -y kubelet=1.12.0-00 kubeadm=1.12.0-00 kubectl=1.12.0-00 kubernetes-cni=0.6.0-00
export KUBERNETES_MASTER=http://MasterIP:8080
openssl x509 -in ca.crt -noout -text
描述 | 现在 | github | suggest | method | 进入 |
---|---|---|---|---|---|
删除分支 | /branch/delete |
/teams/:id |
/branch/:id |
delete |
点击进入 |
更新分支 | /branch/update |
/teams/:id |
/branch/:id |
patch |
点击进入 |
查询分支 | /branch/query |
/teams/:id |
/branch/:id |
get |
点击进入 |
添加分支 | /branch/add |
/orgs/:org/teams |
/branchs/:branch |
post |
点击进入 |
现在 | github | suggest | method | 进入 |
---|---|---|---|---|
/member/delete |
/orgs/:org/members/:username |
branchs/:branch/member/:id |
delete |
点击进入 |
/member/update |
/orgs/:org/memberships/:username |
branchs/:branch/member/:id |
put |
点击进入 |
/member/query |
/orgs/:org/members/:username |
branchs/:branch/member/:id |
get |
点击进入 |
/member/add |
/orgs/:org/memberships/:username |
branchs/:branch/member/:id |
post |
点击进入 |
/member/invitation |
/orgs/:org/invitations |
branchs/:branch/invitations |
post |
从表面上看,你可能对于这两个处理器之间的区别干到很困惑。毕竟你可能更加熟悉并行计算等词,然后会接触到这两种不同的处理器。那么他们之间有什么区别呢?这是‘每个博士生都应该知道的52个问题’这周的问题。在弄清楚它们之间的细节之前,为什么我们不先看看这两种不同处理器之间的概念呢,即并行计算。
在回答这个问题之前,我们首先需要考虑传统的“串型”处理模型。让我们想象我们需要解决的一些问题。传统计算解决此问题的方法是将其视为处理器按照顺序处理的多个步骤。处理器处理每一个指令,知道最后得到答案和这个问题被解决。虽然这是解决问题的一个很好的方式,这却意味着在解决问题的速度上遇到的瓶颈。即,处理器执行各个指令的速度。如果问题不是那么大这是一个好方法,但是当我们必须要解决大问题或者要让计算变得更快会发生什么?有没有办法在没有处理器速度的瓶颈的情况下提高计算速度?
你可能已经猜到答案是肯定的,它以被成为并行计算的形式出现。并行计算对我们试图解决的问题所做的是将其分解为更小的问题,每个问题可以在相同时间独立计算。以这种方式,这个问题分布在不同的处理元件上,这些不同的处理元件执行不同的子问题,这样在计算速度上会有很大的潜力。加速量取决于算法并且可以根据Amdahl定律得到[1]。那么这一切是如何运作的呢?怎么能以这种方式处理事情呢?比较好的两个解决方式是多核处理器和矢量处理器。
多核处理器是单个计算组件,它通过使用多个串行处理器在相同的时间执行不同的操作完成并行计算。前面讨论的大问题的子问题由独立的处理器解决,允许程序并行计算。这就好像有多个人在同一个项目上工作,每个人有不同的任务去做,但是他们都是在为同一个项目做贡献。这可能需要额外的组织去做,但是项目的整体完成速度会变得更快。
一个矢量处理器是能计算单一指令的处理器(像串型处理器),但是能完成一维阵列排列的多个数据集(不像只能操作单一数据集的标准串行处理器)。这里的想法是,如果你对程序中的不同的数据集做了很多次同样的事情,而不是为每个数据块做一条指令,为什么不对所有数据集执行一次指令呢?SIMD(单指令多数据)通常用于表示这种工作方式的指令。
所以这是一个一般的想法,让我们总结一个例子。让我们想象我们想要在路上推四块大石头,并且每分钟只能推一个。串行处理器每次只能推一个所以需要花费四分钟。有两个核的多核处理器相当于有两个人去推石石头所以每次能推两个石头,它需要花费两分钟。矢量处理器就相当于有个长木板,把它放在所有四块石头的后面然后一起去推它们,需要花费一分钟。多核处理器相当于有多个workers,矢量处理器有一种方法可以同时对多个事物在相同的时间内做同一个事情。
先看下面的代码:
function getArea(shape, options) {
let area = 0
switch(shape) {
case 'Triangle':
area = .5 * options.w * options.h
break
/*More case*/
}
return area
}
getArea('Triangle', {w: 200, h: 100})
在上面的程序中,我们能看出Triangle
就是魔术字符串,我们一般为了消除这种魔术字符串,我们会定义下面的常量:
const shapeType = {
TRIANGLE: 'Triangle'
}
然后把上面的代码变为:
const shapeType = {
TRIANGLE: 'Triangle'
}
function getArea(shape, options) {
let area = 0
switch(shape) {
case shapeType.TRIANGLE:
area = .5 * options.w * options.h
break
/*More case*/
}
return area
}
getArea(shapeType.TRIANGLE, {w: 200, h: 100})
上面的这种做法的确消除了强耦合。但是我们可以仔细想想,在这种情况之下其实shapeType.TRIANGLE
等于哪个值没有影响,所有我们可以把定义的常量变为:
const shapeType = {
TRIANGLE: Symbol()
}
先看下面的代码:
const size = Symbol('size')
class Collection {
constructor() {
this[size] = 0
}
add(item) {
this[this[size]] = item
this[size]++
}
static sizeOf(instance) {
return instance[size]
}
}
let x = new Collection()
console.log(Collection.sizeOf(x))
x.add('foo')
x.add('bar')
console.log(Collection.sizeOf(x))
console.log(Object.keys(x)) // ['0', '1']
console.log(Object.getOwnPropertyNames(x)) // ['0', '1']
console.log(Object.getOwnPropertySymbols(x)) // [Symbol(size)]
我们通过key
和getOwnPropertyNames
都无法获得Symbol
的对象,只能通过getOwnPropertySymbols
获得。
这里列出了,k8s中的所有控制器,它们都是遵循的都是控制器模式,控制器模式说白了就是控制循环,如下:
for {
实际状态 := 获取集群中对象 X 的实际状态(Actual State)
期望状态 := 获取集群中对象 X 的期望状态(Desired State)
if 实际状态 == 期望状态{
什么都不做
} else {
执行编排动作,将实际状态调整为期望状态
}
}
下面介绍几种控制器
因为现在在项目中使用的是axios
,通过axios
的displayNotification
会把所有的请求失败的接口统一处理,现在的统一错误处理是只要是错误就出现个弹窗把后端返回的错误显示给前端。但是也是会有特殊情况,在登陆的时候当用户名和密码出现错误的时候,一般在输入框的周围出现红色文字提示,但是如果使用统一的错误处理话还是会出现弹窗,这样对于交互或者体验来说是很不好的,现在的做法:因为这种情况比较特殊,所以就通过判断后端返回回来的错误信息,这个场景就不进行出现弹窗
但是项目中难免会出现一些奇葩或者历史遗留性问题,有的接口默认返回的都是200,那个开发者觉得只要这个请求请求到服务器了就是成功就应该返回200,但是把具体的错误有进一步的包了一下,但是这个请求除了404意外都是200,所有就不会走到上面所说的displayNotification
里面,所以这种情况的做法:就要使用axios
的interceptors
对每个请求的响应进行特殊的判断。
在当前的业务中,不同的业务线是分为不同的repo的,这样做是有很多好处的:
还是有坏处的:
normalize.css
等有全局的东西话就可能会遇到问题。比如现在遇到的问题是:公用模块的样式中有一个* {box-sizing: border-box}
但是有的模块是不依赖这个的,就会出现问题:现在的场景是这样的:
rc-component
是一套开源的组件库,里面封装了常用的基础组件如:rc-select
, rc-tooltip
, rc-
lamma-ui
是基于rc-component
的;apollo-weights
是基于lamma-ui
的;并且内部的lamma-ui
和apollo-weights
已经稳定使用了很长时间了,突然有一天发现了rc-component
中的某一个组件的bug(比如说: rc-select
),并且这个bug是block的会影响产品展示形态。
apollo-weights
中的代码copy一下到本地,然后升级一下里面有bug的组件,升级到最新的没有bug的版本。rc-component
的组件定期升级到最新的版本,然后使用组件的业务进行测试。公司的技术栈是Typescript(下面可能称为ts)
+ React
全家桶,从2018年的7月份入职开始接触Typescript
。ts给我带来的感受是:用三个月和用一年基本看不出啥差别(不深度的去学习的情况下)
因为我接手的项目没有使用状态管理工具,在入职之后的前几个月,一直使用的父子组件传值的方式去处理状态。但是后来发现这种方式很难去复用一些东西,比如:
redux
就dispatch
一个action
就可以了。对此写了一遍文章为什么即将使用react-redux
如果想了解当时的代码结构可以看Typescript配合React实践
如果对于type
和interface
还是云里雾里的话,可以看看这篇翻译:翻译: typescript 2.7中interface和type(Interface vs Type alias in TypeScript 2.7)。这篇文章的最后阐述了作者的观点,作者比较倾向于使用type去声明state/props/context
。
我和好多人讨论这个问题,也看过网上不少的讨论。我觉得对于这个问题来说就属于仁者见仁,智者见智了。
在本文中我还是使用的interface
去声明state/props/context
。
因为七月的第四周参加了个比赛,所以学习的心失去了一大部分,周末没有学习,只是看了一章左右的视频,并把任务延续到八月份。
首先要在十一月份最晚十一月底把工作确认下来。
在工作中学习VUE,利用空余的时间,学习函数式编程争取能在十一月的时间理解函数式编程。
十一月应该会有三个周末,前两个周末(第二个周末可能会有一天去找同学)和工作之余看VUE相关的东西,第三个周末学习函数式编程。
在工作之中继续理解VUE,争取能够在业务中使用函数式编程的**。
感觉就没有必要回家了,元旦再回家。
一共有四个周末:
继续在工作之中理解VUE,继续运用函数式编程。
元旦回家,最后一个周末辞职,应该就还剩下两个周末
两个周末都看《HTTP权威指南》
文章首发:Typescript配合React实践
使用ts写React代码写了将近三个月,从刚开始觉得特别垃圾到现在觉得没有ts不行的一些实践以及思考。
如果按部就班的写React就体会不到使用ts的乐趣,如果多对代码进行优化,进行重构,在业务中实践比较好的一些方案就会体会到ts真正的乐趣,但是ts也在过程中给我带来了痛苦,在本文的最后会具体展开一下。
刚开始觉得ts好垃圾,觉得React的PropType
和defaultProps
几乎能做ts的静态类型检查能做到的事情,甚至做的还能比ts做的多。比如说对于组件间设置默认值,ts对于支持的就是不太好。
后来由于一个需求我改变了一点我的想法,当时的想法就是:“你还别说,这个ts还有点用”。这个场景分为两种情况:
commamd(ctrl) + f
的方式去全局搜索并且修改,但是这样还是如果对于量大话就很不友好(我遇到的就是量大的情况),如果统一替换的话,比如说这个变量叫做user,就有很大概率会包含其他的变量,这样统一替换就会很尴尬。但是ts的静态类型检查就帮你解决了这个问题,对于每一个父组件没有传递的值来说,都会提示错误。而且ts的报错是在编译时,不是在运行时。// 父组件
render(): ReactNode {
const { user, loading, namespaceList, yarnList, workspace } = this.state;
return (
<UserDetail
user={user}
loading={loading}
namespaceList={namespaceList}
yarnList={yarnList}
workspace={workspace}
onLoadWorkspace={(params: IParams) => this.onLoadWorkspace(params)}
onUpdateUser={(field: string, data: IUserBaseInfo) => this.handleUpdateUser(field, data)}
onToCreateAk={(userId: number) => this.handleToCreateAk(userId)}
onDelete={(ids: number[]) => this.handleDelete(ids)}
onUpdateResource={(userId: number, data: IResources) => this.onUpdateResource(userId, data)}
onAkDelete={(ak: IPermanentKey) => this.handleDeleteAk(ak)}
onChangeAkStatus={(ak: IPermanentKey, status: string) => this.onChangeAkStatus(ak, status)}
/>
);
}
只要注意第一个参数就可以了,这个是实际的业务场景,下面是子组件:
export interface IProps {
user: IUser | null;
loading: boolean;
namespaceList: { namespace: string }[];
yarnList: IYarn[];
workspace: {
list: IWorkspace[] | null,
total: number,
} | null;
onLoadWorkspace: (params: IParams) => void;
onUpdateUser: (field: string, data: IUserBaseInfo) => void;
onToCreateAk: (userId: number) => void;
onDelete: (ids: number[]) => void;
onUpdateResource: (userId: number, data: IResources) => void;
onAkDelete: (ak: IPermanentKey) => void;
onChangeAkStatus: (ak: IPermanentKey, status: string) => void;
}
class UserDetail extends Form<IProps, {}> {
看,如果这样写的话,就能覆盖住上面的两种情况了。
当我硬着头皮准备去修改同事上千行的React代码时候,我刚开始犹豫了好长时间,怕赶在上线发版之前搞不完之类的,后来实践的时候发现意淫的有点多了,有了ts不用关心这么多了呀。大致为父组件给子组件传递的值和回调定义好就ok了。这么说可能有点宽泛,好像自己写一个组件也是这样的,哈哈。后面会具体的提到怎么使用ts重构的。这个时候对于ts的心态就是:“这个东西是真的厉害”。
经历了几次重构自己和重构其他人代码的时候,我现在对于ts的心态就是:“我可能以后的前端生涯离不开这玩意儿了”。
因为在网上能搜到的ts+react
的项目还是比较少,真实的实践也是比较少,都是一些从头开始配置项目的。文件的目录结构怎么做比较好还是没有具体的实践方案。当然,这种方案还是要根据具体的业务来分析的。在上一篇文章编写不用redux的React代码中说明我当前遇到的业务场景。
最终决定把所有的interface都放在公用的schemas目录然后在具体的业务中进行具体引用。
具体的common的目录结构如下(schems目录下面就保存着所有的接口信息):
common
├── component
│ ├── delete-workspace-modal
│ │ ├── delete-workspace-modal.less
│ │ ├── delete-workspace-modal.less.d.ts
│ │ └── index.tsx
│ └── step-complete
│ ├── index.tsx
│ ├── step-complete.less
│ └── step-complete.less.d.ts
├── css
│ └── global.less
├── hoc
│ ├── workspace-detail.tsx
│ └── workspace-list.tsx
├── schemas
│ ├── dialog.ts
│ ├── k8s.ts
│ ├── ldap.ts
│ ├── message.ts
│ ├── params.ts
│ ├── password.ts
│ ├── section.ts
│ ├── table.ts
│ ├── user.ts
│ ├── workspace.ts
│ └── yarn.ts
└── util
├── field-value.ts
├── format-datetime.ts
├── genURL.ts
├── getNamespaceList.ts
├── getYarnList.ts
└── validation.ts
在schems目录下面的文件就类似于通用的静态类型,和业务相关但并不是和某个模块进行强绑定,这是因为在每个模块之间难免会遇到一些交叉。下面是某个具体模块的静态类型:
export interface IYarnResource {
id: number;
namespace: string;
user: string;
queue: string;
}
export interface IYarnStatus {
name: string;
error: string;
maxCapacity: number;
state: string;
used: number;
capacity: number;
}
export interface IYarnEntity extends IYarnResource {
status: IYarnStatus;
keytab: string;
}
和模块强耦合的静态类型比如说props和state的静态类型,都会放在绝体的业务文件中,就比如说下面的这个代码(简化后):
import React, { PureComponent, ReactNode, Fragment } from 'react';
import { IComplex } from 'common/schemas/password';
export interface IProps {
onClose(): void;
onOK(data: IComplex): void;
complex: IComplex | null;
}
export interface IState extends IComplex {
}
class PasswordComplex extends PureComponent<IProps, IState> {
state: IState = {
leastLength: 6,
needCapitalLetter: false,
needLowercaseLetter: false,
needNumber: false,
needSpecialCharacter: false,
};
}
所有的业务静态类型一般都是不可复用的,一般是通用静态类型以及某些特殊的静态类型组合而成的。
state的初始化不一定要放在constructor
里面,但是一定要给state指定类型,具体的原因见:Typescript in React: State will not be placed in the constructor will cause an error
如果我们安装了@types/react
,在react目录下的index.d.ts
会有react的所有静态类型定义。
现在比如写一个模块叫用户管理,里面包含查看用户详情,查看用户列表,新建用户等功能。这也就对应这三个路由/users/:id
,/users
,/users/create
。这也就对应着三个有状态组件分别为:user-detail-wrapper
, user-list-wrapper
,user-form-wrappper
。有状态组件里面只是请求或者获取数据之类的。展示是通过component下面的无状态组件。可以看看下面的目录结构:
user
├── component
│ ├── password-complex
│ │ ├── index.tsx
│ │ ├── password-complex.less
│ │ └── password-complex.less.d.ts
│ ├── user-detail
│ │ ├── index.tsx
│ │ ├── user-detail.less
│ │ └── user-detail.less.d.ts
│ ├── user-detail-ak
│ │ ├── index.tsx
│ │ ├── user-detail-ak.less
│ │ └── user-detail-ak.less.d.ts
│ ├── user-detail-base-info
│ │ ├── index.tsx
│ │ ├── user-detail-base-info.less
│ │ └── user-detail-base-info.less.d.ts
│ ├── user-detail-resource
│ │ ├── index.tsx
│ │ ├── user-detail-resource.less
│ │ └── user-detail-resource.less.d.ts
│ ├── user-detail-workspace
│ │ └── index.tsx
│ ├── user-form-dialog
│ │ ├── index.tsx
│ │ ├── user-form-dialog.less
│ │ └── user-form-dialog.less.d.ts
│ └── user-list
│ ├── index.tsx
│ ├── user-list.less
│ └── user-list.less.d.ts
├── user-form-wrapper
│ └── index.tsx
├── user-detail-wrapper
│ └── index.tsx
└── user-list-wrapper
└── index.tsx
看过网上的好多实践,为了防止state
的不可篡改,都会把state
通过下面的方式设置为只是可读的,这种方式虽然好,但是在我的项目中不会出现,这种错误只有React接触的新人或者以前写Vue的人会犯的,我的项目中一共两个人,不会出现在这种问题。
const defaultState = {
name: string;
}
type IState = Readonly<typeof defaultState>
class User extends Component<{}, IState> {
readonly state: IState = defaultState;
}
但是上面这种方式只是适合类型为typescript的基本类型,但是如果有自己定义的复杂类型,比如说下面这种:
interface IUser {
name: string;
id: number:
age: number;
...
}
interface IState {
list: IUser[];
total: number;
}
// default state
const userList: IUser = []
const defaultState = {
list: userList,
total: 0,
}
上面这种就不能通过一个单纯的空数组就推断出list的类型是IUser的数组类型,所以要添加无谓一个userList
定义。
无状态组件也被称为展示组件,如果一个展示组件没有内部的state可以被写为纯函数组件。
如果写的是函数组件,在@types/react
中定义了一个类型type SFC<P = {}> = StatelessComponent<P>;
。我们写函数组件的时候,能指定我们的组件为SFC
或者StatelessComponent
。这个里面已经预定义了children
等,所以我们每次就不用指定类型children的类型了。
下面是一个无状态组件的例子:
import React, { ReactNode, SFC } from 'react';
import style from './step-complete.less';
export interface IProps {
title: string | ReactNode;
description: string | ReactNode;
}
const StepComplete:SFC<IProps> = ({ title, description, children }) => {
return (
<div className={style.complete}>
<div className={style.completeTitle}>
{title}
</div>
<div className={style.completeSubTitle}>
{description}
</div>
<div>
{children}
</div>
</div>
);
};
export default StepComplete;
先看一个组件,这个组件就是展示一个列表。
import React, { Fragment, PureComponent } from 'react';
export interface IProps<T> {
total: number;
list: IYarn[];
title: string;
cols: IColumn[];
}
class YarnList extends PureComponent<IProps> {
}
当我们想通用这个组件的时候,但是就是列表的字段不一样,也就是列表对应的不同的类型。这个时候我们可是使用泛型,把类型传递进来(也可以说是通过typescript的类型推断来推断出来)。来看下面的具体实现:
export interface IProps<T> {
total: number;
list: T[];
title: string;
cols: IColumn[];
}
class ResourceList<T> extends PureComponent<IProps<T>> {
// 我们现在业务的场景会把这个list传递给table,table不同的字段通过外部的父组件传递进来。
tableProps(): ITable<T> {
const columns: IColumn[] = [
...this.props.cols,
{ title: '操作', key: 'operation', render: (record: T) => this.renderOperation(record) },
];
return {
columns,
data: this.props.list,
selectable: false,
enabaleDefaultOperationCol: false,
searchEmptyText: '没有搜索到符合条件的资源',
emptyText: '尚未添加资源',
};
}
}
如果使用的typescript是3.x的版本的话,就不用担心这个问题,就直接在jsx中使用defaultProps
就可以了。
如果使用的是2.x的版本就要如果定义一个类似下面这样一个可选的值:
interface IProps {
name?: string;
}
我们如果在class里面设置defaultProps的话,ts是不认识的。还是要在代码里面进行非空判断。对用这好昂方法可以写一个高阶组件。高阶组件来源
export const withDefaultProps = <
P extends object,
DP extends Partial<P> = Partial<P>
>(
defaultProps: DP,
Cmp: ComponentType<P>,
) => {
// 提取出必须的属性
type RequiredProps = Omit<P, keyof DP>;
// 重新创建我们的属性定义,通过一个相交类型,将所有的原始属性标记成可选的,必选的属性标记成可选的
type Props = Partial<DP> & Required<RequiredProps>;
Cmp.defaultProps = defaultProps;
// 返回重新的定义的属性类型组件,通过将原始组件的类型检查关闭,然后再设置正确的属性类型
return (Cmp as ComponentType<any>) as ComponentType<Props>;
};
就类型定义起来有点费劲,有的时候废了大半天的力气发现都是在整ts类型的问题。
然后。。。应该没有了。
这里就主要介绍在书写组件的时候的个人开发规范:
render
函数中只是给子组件传递信息interface
放在common下面的schemas下面interface
比如说IProps
或者IState
要放在组件的内部infra的作用是在pod中担任各种Namespace的基础(infra容器负责创建Namespace,该POD的其他容器加入Namespace)
对于一个POD里面的容器A和容器B来说:
sidercar容器
POD使用可哪些Namespace
这片文章旨在想您介绍Containerd在docker架构中的集成。
让我们先来定义Docker Daemon然后看它是如何定义进docker架构和Containerd当中的。
像进程有守护进程一样(Like the init has its daemon), cron有crond, dhcp有dhcpd, Docker 也有自己的守护进程dockerd。
你能使用下面的命令列出所有的Linux的守护进程:
ps -U0 -o ‘tty,pid,comm’ | grep ^?
译者注:
# 译者使用的是centos 7,原作者的命令不能用,要用下面的
ps -eo tty,pid,comm | grep ^?
并且在输出中grep出dockerd
ps -U0 -o ‘tty,pid,comm’ | grep ^?|grep -i dockerd
# 译者注:同上,要用:ps -eo tty,pid,comm | grep ^? |grep -i dockerd
? 2779 dockerd
你可可能会看到 docker-containerd-shim
,我们将要这篇文章的后面去讲解。
如果你已经运行了docker,那么当你在命令后输入dockerd
的时候会出现类似下面的信息:
FATA[0000] Error starting daemon: pid file found, ensure docker is not running or delete /var/run/docker.pid
现在让我们来停止docker:
systemctl stop docker
然后在命令行使用dockerd
命令运行守护进程。
使用dockerd
在命令行运行docker的守护进程是一个很好的debug工具,你能在命令行上看到真实的运行轨迹。
INFO[0000] libcontainerd: new containerd process, pid: 19717
WARN[0000] containerd: low RLIMIT_NOFILE changing to max current=1024 max=4096
INFO[0001] [graphdriver] using prior storage driver "aufs"
INFO[0003] Graph migration to content-addressability took 0.63 seconds
WARN[0003] Your kernel does not support swap memory limit.
WARN[0003] mountpoint for pids not found
INFO[0003] Loading containers: start.
INFO[0003] Firewalld running: false
INFO[0004] Removing stale sandbox ingress_sbox (ingress-sbox)
INFO[0004] Default bridge (docker0) is assigned with an IP address 172.17.0.0/16. Daemon option --bip can be used to set a preferred IP address
INFO[0004] Loading containers: done.
INFO[0004] Listening for local connections addr=/var/lib/docker/swarm/control.sock proto=unix
INFO[0004] Listening for connections addr=[::]:2377 proto=tcp
INFO[0004] 61c88d41fce85c57 became follower at term 12
INFO[0004] newRaft 61c88d41fce85c57 [peers: [], term: 12, commit: 290, applied: 0, lastindex: 290, lastterm: 12]
INFO[0004] 61c88d41fce85c57 is starting a new election at term 12
INFO[0004] 61c88d41fce85c57 became candidate at term 13
INFO[0004] 61c88d41fce85c57 received vote from 61c88d41fce85c57 at term 13
INFO[0004] 61c88d41fce85c57 became leader at term 13
INFO[0004] raft.node: 61c88d41fce85c57 elected leader 61c88d41fce85c57 at term 13
INFO[0004] Initializing Libnetwork Agent Listen-Addr=0.0.0.0 Local-addr=192.168.0.47 Adv-addr=192.168.0.47 Remote-addr =
INFO[0004] Daemon has completed initialization
INFO[0004] Initializing Libnetwork Agent Listen-Addr=0.0.0.0 Local-addr=192.168.0.47 Adv-addr=192.168.0.47 Remote-addr =
INFO[0004] Docker daemon commit=7392c3b graphdriver=aufs version=1.12.5
INFO[0004] Gossip cluster hostname eonSpider-3e64aecb2dd5
INFO[0004] API listen on /var/run/docker.sock
INFO[0004] No non-localhost DNS nameservers are left in resolv.conf. Using default external servers : [nameserver 8.8.8.8 nameserver 8.8.4.4]
INFO[0004] IPv6 enabled; Adding default IPv6 external servers : [nameserver 2001:4860:4860::8888 nameserver 2001:4860:4860::8844]
INFO[0000] Firewalld running: false
现在如果你在另一个窗口运行或者删除一个容器,你就能看到docker的守护进程连接了docker的客户端和容器。
它的目的是破坏docker架构中的模块化(its purpose is breaking up more modularity to Docker architecture)和与其他行业参(云提供者以及编排服务)与者相比更加中立。
据Solomon Hykes说,截止到2016年的4月,被包含在Docker 1.11中的Containerd已经被部署在数百万的机器中,宣布扩展Containerd的roadmap得到了许多云提供厂商的一件,这些云厂商包括阿里云,AWS,谷歌,IBM,微软和其他容器生态系统的活跃成员。
更多的docker引擎的功能将要被添加到containerd中,以至于containerd 1.0将要提供在linux或者windows上管理容器的所有原语:
你可能为了构建,传输,运行容器化应用继续使用docker,但是如果你寻找一种标准化组建那么你可以考虑containerd。
Docker Engine 1.11是第一个基于runC(基于OCI技术的runtime)和containerd构建的版本。下图是containerd的集成架构:
Open Container Initiative (OCI)组织成立于2015年的6月,旨在建立容器的通用标准为了避免在容器系统内部可能存在的碎片和分裂。
它包含两个标准:
runtime规范概述了如何在磁盘上解压文件系统包。
如果你导出和提出一个镜像你能看到这个json的文件。下面的这个例子,我们将要使用busybox作为例子:
mkdir my_container
cd my_container
mkdir rootfs
docker export $(docker create busybox) | tar -C rootfs -xvf -
现在我们在rootfs中已经有一个提取出来的busybox的镜像。
tree -d my_container/
my_container/
└── rootfs
├── bin
├── dev
│ ├── pts
│ └── shm
├── etc
├── home
├── proc
├── root
├── sys
├── tmp
├── usr
│ └── sbin
└── var
├── spool
│ └── mail
└── www
我们能够通过下面的命令生成config.json这个文件:
docker-runc spec
生成的配置文件如下:
{
"ociVersion": "1.0.0-rc2-dev",
"platform": {
"os": "linux",
"arch": "amd64"
},
"process": {
"terminal": true,
"consoleSize": {
"height": 0,
"width": 0
},
"user": {
"uid": 0,
"gid": 0
},
"args": [
"sh"
],
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm"
],
"cwd": "/",
"capabilities": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
],
"rlimits": [
{
"type": "RLIMIT_NOFILE",
"hard": 1024,
"soft": 1024
}
],
"noNewPrivileges": true
},
"root": {
"path": "rootfs",
"readonly": true
},
"hostname": "runc",
"mounts": [
{
"destination": "/proc",
"type": "proc",
"source": "proc"
},
{
"destination": "/dev",
"type": "tmpfs",
"source": "tmpfs",
"options": [
"nosuid",
"strictatime",
"mode=755",
"size=65536k"
]
},
{
"destination": "/dev/pts",
"type": "devpts",
"source": "devpts",
"options": [
"nosuid",
"noexec",
"newinstance",
"ptmxmode=0666",
"mode=0620",
"gid=5"
]
},
{
"destination": "/dev/shm",
"type": "tmpfs",
"source": "shm",
"options": [
"nosuid",
"noexec",
"nodev",
"mode=1777",
"size=65536k"
]
},
{
"destination": "/dev/mqueue",
"type": "mqueue",
"source": "mqueue",
"options": [
"nosuid",
"noexec",
"nodev"
]
},
{
"destination": "/sys",
"type": "sysfs",
"source": "sysfs",
"options": [
"nosuid",
"noexec",
"nodev",
"ro"
]
},
{
"destination": "/sys/fs/cgroup",
"type": "cgroup",
"source": "cgroup",
"options": [
"nosuid",
"noexec",
"nodev",
"relatime",
"ro"
]
}
],
"hooks": {},
"linux": {
"resources": {
"devices": [
{
"allow": false,
"access": "rwm"
}
]
},
"namespaces": [
{
"type": "pid"
},
{
"type": "network"
},
{
"type": "ipc"
},
{
"type": "uts"
},
{
"type": "mount"
}
],
"maskedPaths": [
"/proc/kcore",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/sys/firmware"
],
"readonlyPaths": [
"/proc/asound",
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
]
}
}
现在你就可以编辑这个配置文件里面的内容,然后不通过docker启动一个容器,通过runc:
runc run container-name
首先在你第一次用它的时候要先安装它:
sudo apt install runc
你也能通过源文件安装它:
mkdir -p ~/golang/src/github.com/opencontainers/
cd ~/golang/src/github.com/opencontainers/
git clone https://github.com/opencontainers/runc
cd ./runc
make
sudo make install
runC是一个完全符合规范的容器运行时,它允许你扩展(spin)一个容器,能与它们进行交互并且管理它们的声明周期,这要是为什么一个引擎(比如docker)构建的容器,能够在另一个容器中运行的原因。
容器作为runC的一个子进程,也能不通过守护进程(docker的守护进程)切入一些变量到系统之中。
runC构建在libcontainer上,libcontainer也是为docker引擎安装提供相同的容器库。在docker1.11之前,docker引擎用来管理存储,网络,容器以及镜像等等,现在docker的架构被拆为了四个部分:
为了运行一个容器,docker engine创建了一镜像,然后把它传递给containerd,然后containerd去调用containerd-shm ,containerd-shm 再去调用runC去运行这个容器。containerd-shim 允许runC在创建容器之后退出。这样我们就可以运行无守护进程的容器,因为我们没必要去为容器运行长时间的runtime进程。
目前,容器通过runC(经由containerd)创建的,但是可能通过另一种非runC的二进制暴露出相同的docker命令行以及接收OCI包。
你能在你的机器上看到不同的runtime通过下面的命令:
docker info|grep -i runtime
如果用的默认的会得到下面的输出:
Runtimes: runc
Default Runtime: runc
通过下面的命令你可以增加一个runtime:
docker daemon --add-runtime "<runtime-name>=<runtime-path>"
# docker daemon --add-runtime "oci=/usr/local/sbin/runc"
按流程它们只是一个containerd-shim,它管理标准的IO的先入先出队列,和保证在容器挂掉的时候能启动容器。
它也负责发送容器退出的状态给像docker这样的上层。
Containerd实现了容器runtime,声明周期的支持和执行(create, start, stop, pause, resume, exec, signal & delete)这些特性。其他的特性(比如存储,日志等)被其他的组件实现。下图是Containerd Github 的一个图,展示了不同的特性并告诉这个特性是是否在范围Containerd之内。
我们运行容器:
docker run --name mariadb -e MYSQL_ROOT_PASSWORD=password -v /data/lists:/var/lib/mysql -d mariadb
Unable to find image 'mariadb:latest' locally
latest: Pulling from library/mariadb
75a822cd7888: Pull complete
b8d5846e536a: Pull complete
b75e9152a170: Pull complete
832e6b030496: Pull complete
034e06b5514d: Pull complete
374292b6cca5: Pull complete
d2a2cf5c3400: Pull complete
f75e0958527b: Pull complete
1826247c7258: Pull complete
68b5724d9fdd: Pull complete
d56c5e7c652e: Pull complete
b5d709749ac4: Pull complete
Digest: sha256:0ce9f13b5c5d235397252570acd0286a0a03472a22b7f0384fce09e65c680d13
Status: Downloaded newer image for mariadb:latest
db5218c494190c11a2fcc9627ea1371935d7021e86b5f652221bdac1cf182843
如果你运行ps aux
命令,你能注意到docker-containerd-shim和容器运行通过下面的一些参数关联:
这是具有正确格式的完整行:
docker-containerd-shim <container_id>
/var/run/docker/libcontainerd/<container_id> docker-runc
db5218c494190c11a2fcc9627ea1371935d7021e86b5f652221bdac1cf182843是你创建容器之后能看到的id。
ls -l /var/run/docker/libcontainerd/db5218c494190c11a2fcc9627ea1371935d7021e86b5f652221bdac1cf182843
total 4
-rw-r--r-- 1 root root 3653 Dec 27 22:21 config.json
prwx------ 1 root root 0 Dec 27 22:21 init-stderr
prwx------ 1 root root 0 Dec 27 22:21 init-stdin
prwx------ 1 root root 0 Dec 27 22:21 init-stdout
Docker的每个版本都在变化和发展,但有一些重大变化,比如Containerd的集成。在这篇文章中,我们已经看到了在这次集成(Containerd的集成)之后Docker发生了什么变化,以及OCI(在Docker的帮助下)如何通过引入使用独立运行时的可能性来改变容器世界。
(这里说一下去年的时候还应该写一个2017的总结,但是没写)
今天不知道怎么突然想写一篇今年的总结。可能是最近经历的太多了吧。几个月前的七月份结束了自己十几年的学生生涯,完成了从学生到社会人的转变,可能这种转变并不是彻底的,因为毕竟还没完全脱去身上的书气。
今年的这种转变堪比2014的从高中到大学的转变,那个时候还挺惆怅,离开生活了将近二十年的故乡,来到了毛主席的故乡长沙。刚来到时候感觉还不是很大,还和室友同学聊聊天。但是人在异乡,夜深人静的时候难免会思考人生,不断的回想高中美好时光:和朋友上课看电影,晚上下课去撸串之类。也曾哭过几次,但这个都不属于2018年发生的时候,应该在大学的流水账里面去说。
## 时间顺序
下面就按照时间顺序来说说今年发生的几件事:
### 在校招公司实习三个月
### 在学校的完成学生的收尾工作
### 上班
### 她结婚了
还是按照另一个维度,从学习,工作,生活,感情四个方面来对今年做一个总结:
现在没有了上学时候的热情(待完善)
做的都是小feature,没有难度(待完善)
有没有生活???
说下今年在学校的生活和上班生活后的对比(待完善)
她结婚了,55555(待完善)
呼应一下开头,说一下现在的感受。
现在和上学的时候或者刚参加工作两个月的时候发的朋友圈的数量明显变少,或者说不知道发朋友圈给谁看了。现在感觉上学的时候发的那么多朋友圈真的都好傻,有的甚至都想删掉,幸亏微信出了一个把这条朋友圈对所有人不可见的功能。感觉当时就是由于虚荣心,去个地方旅游或者和朋友逛个街吃个饭看个电影啥的就希望所有人都能看到。
这个其实我想到电视剧(好像是武林外传)里面的对话。大致是这个意思:当一个在一方面很有造诣的时候,别人说你在这个方面不行你可能会不屑一顾;如果你在一个领域是半吊子,并且光和别人说你在这个领域的造诣的时候,别人说你在这个领域不怎么怎么着的时候你就会表现特别异常。
这个我在现在的工作中也遇到过这样的人,我也挺佩服她能说出来的:我上班来的时候就觉得旁边的QA妹子怎么光想着开RD的代码呢,还光想着从RD的代码中自己找出bug在哪,我当时真的想和她说先把本职工作做好,不要刷存在感。后来我在她旁边吃饭的时候,听到她和另一个同事说:“人就是约没有什么,越想让别人觉得自己有,就像我一样,我每次写一点代码就恨不得让所有人都知道,但是事实上我是不会写的”。自从这个之后我对这个妹子的就没有任何偏见了。
没有自信这个我感觉体现在各个方面
自信真的很重要,我在三个公司实习过,发现面试官的水平,准确的说一面面试官的水平并不会比自己高出来多少,有时候他们问的问题其实都差不多会,但是就是觉得他们是面试我们的人肯定比我们牛逼所以不敢发表我们的观点。
到了工作的时候我的感受也是这样,对于一个leader总是觉得他们高高在上,肯定是不会和他们成为朋友的,和他们肯定就是上下级的关系,觉得自己低人一等。其实我一直想克服这种问题,但是还是很难去从根上克服。
拿羽毛球和篮球来说吧。
(待完善)
最近把同事的项目接了一下,准备按照自己的想法重构一遍的时候发现自己的写或者重构React代码的时候还是没有代码标杆。有的时候按照想法A写了一下,不知不觉的有改到了另一种格式。
业务属于不算复杂的TO B业务的后台管理系统。如下几个图:
点击租户绑定
中的修改的时候,弹出一个modal框进行进行修改资源,如下图:
所有的管理界面基本都是大同小异,先进列表页(列表页中包含新建等相关操作),然后详情页面,详情页面能对某个东西进行修改。
因为在项目中虽然引用了Redux但是从来没有使用过,所以现在的技术栈为Webpack+React+Typescript+React-router(+Redux)
为什么在这里给redux加上个括号,是因为现在有给项目加上Redux的打算。
脚手架是同事搭建的,针对公司业务的。自己有一套脚手架的好处就是有技术或者组件沉淀的时候就可以打包在脚手架里面。
组件库是基于rc-component
封装了一套业务组件。有的业务线使用的antd
。
这里用技术架构可能有点高大上,但是实在想不出什么朴实的词汇。
虽然现在没有使用redux,但是还是借鉴了Redux中有状态组件和无状态组件的**。
有状态组件负责:
无状态组件负责:
项目目录如下:
├── Makefile
├── README.md
├── mock
│ └── hello
├── package-lock.json
├── package.json
├── pre-commit
├── project.config.js
├── src
│ ├── app
│ │ ├── action.ts
│ │ ├── app.less
│ │ ├── app.tsx
│ │ ├── dao
│ │ │ ├── user.ts
│ │ │ └── workspace.ts
│ │ ├── reducer
│ │ │ └── index.ts
│ │ ├── root-reducer.ts
│ │ ├── root-router.tsx
│ │ ├── type
│ │ │ └── index.ts
│ │ └── type.ts
│ ├── common
│ │ ├── component
│ │ │ ├── form
│ │ │ │ ├── custom-validator.ts
│ │ │ │ └── index.tsx
│ │ │ └── user-form-dialog
│ │ │ ├── index.tsx
│ │ │ ├── user-form-dialog.less
│ │ │ └── user-form-dialog.less.d.ts
│ │ ├── css
│ │ │ └── global.less
│ │ ├── schemas
│ │ │ └── user.ts
│ │ ├── conf
│ │ │ └── validation
│ │ │ ├── user-list.ts
│ │ └── util
│ │ ├── field-value.ts
│ ├── index.html
│ ├── types
│ │ ├── classnames.d.ts
│ │ ├── lodash.chunk.d.ts
│ │ ├── rc-select.d.ts
│ │ └── validator.d.ts
│ ├── user
│ │ ├── create-ldap
│ │ │ ├── component
│ │ │ │ └── ldap-form-dialg
│ │ │ │ ├── index.tsx
│ │ │ │ ├── ldap-form-dialog.less
│ │ │ │ └── ldap-form-dialog.less.d.ts
│ │ │ ├── create-ldap.less
│ │ │ ├── create-ldap.less.d.ts
│ │ │ └── index.tsx
│ │ ├── create-user
│ │ │ ├── create-user.less
│ │ │ ├── create-user.less.d.ts
│ │ │ └── index.tsx
│ │ ├── user-detail
│ │ │ ├── index.tsx
│ │ │ ├── user-detail.less
│ │ │ └── user-detail.less.d.ts
│ │ ├── user-ldap
│ │ │ ├── component
│ │ │ │ └── ladp-detail
│ │ │ │ ├── index.tsx
│ │ │ │ ├── ldap-detail.less
│ │ │ │ └── ldap-detail.less.d.ts
│ │ │ ├── index.tsx
│ │ │ ├── user-ldap.less
│ │ │ └── user-ldap.less.d.ts
│ │ └── user-list
│ │ ├── index.tsx
│ │ ├── user.less
│ │ └── user.less.d.ts
├── static.d.ts
├── tsconfig.json
├── tslint.js
└── typings
├── globals
│ └── index.d.ts
└── index.d.ts
介绍一下几个非业务的目录:
这个目录的定位是工程的入口文件。主要有合并reducer,定义redux中action type以及配置路由。
这么目录是存放一些通用的东西,包括但是不限于:
component
: 通用组件css
: 全局的cssconf
validation
: 填写表单时候的字段校验utils
:常用通用方法schemas
: 定义typescript的要用到的通用类型针对于typescript来讲的如果没有@type对应的组件库,就要自己实现一下。但是公司里这个文件一般是这个样子的:
declare module 'rc-select' {
const Select: any;
export default Select;
}
对,就是这么简单粗暴的声明。
其他的就都是业务目录的。
公司的业务分为不同的产品,每个产品在不同的repo
下开发,我负责的这个产品算我在内有两个人开发,主要的东西还是我一个人开发。
开发模式采用当下比较流行的Feature Branching的开发方式,即使每个feature或者bugfix分为不同的分支,等开发完成之后向主分支(现在是develop是主分支)提一个pull request。然后develop分支就会触发CICD的流程,部署到开发环境,来看线上的效果。
但是这种开发模式,遇到了一个问题:
A分支是1.0版本的上线分支;
develop是目前的开发分支;
每当A分支上有bug的时候会开一个新的分支去修复bug,然后把这个分支在merge到develop分支上来保证bug也在开发分支上进行了fix,
但是--------------------------
develop分支进行很大的重构,文件目录也进行了很大的变化。
当A分支上又有一个bug的时候,就不能通过pull request的方式,简单merge回develop分支了。
就要一份代码还要在develop上在copy一次。
真的不想叫架构这么高大上的名字,也不能叫文件目录的放置吧。
上文也是说到了分为有状态组件和无状态组件。
按照user这个目录来说:
user
├── component
│ ├── user-detail
│ │ ├── index.tsx
│ │ ├── user-detail.less
│ │ └── user-detail.less.d.ts
│ ├── user-detail-ak
│ │ ├── index.tsx
│ │ ├── user-detail-ak.less
│ │ └── user-detail-ak.less.d.ts
│ ├── user-detail-base-info
│ │ └── index.tsx
│ ├── user-detail-resource
│ │ ├── index.tsx
│ │ ├── user-detail-resource.less
│ │ └── user-detail-resource.less.d.ts
│ ├── user-detail-workspace
│ │ └── index.tsx
│ └── user-form-dialog
│ ├── index.tsx
│ ├── user-form-dialog.less
│ └── user-form-dialog.less.d.ts
├── create-ldap
│ ├── component
│ │ └── ldap-form-dialg
│ │ ├── index.tsx
│ │ ├── ldap-form-dialog.less
│ │ └── ldap-form-dialog.less.d.ts
│ ├── create-ldap.less
│ ├── create-ldap.less.d.ts
│ └── index.tsx
├── create-user
│ ├── create-user.less
│ ├── create-user.less.d.ts
│ └── index.tsx
├── user-detail-wrapper
│ └── index.tsx
├── user-ldap
│ ├── index.tsx
│ ├── user-ldap.less
│ └── user-ldap.less.d.ts
├── user-ldap-detail-wrapper
│ ├── index.tsx
│ ├── user-ldap-detail-wrapper.less
│ └── user-ldap-detail-wrapper.less.d.ts
└── user-list
├── index.tsx
├── user.less
└── user.less.d.ts
并没有redux中containers的目录,因为感觉到并没有使用redux,所以就没有使用containers的概念。
但是也是可以把user中的组件分为两种组件,一种是component的里面的组件,另一种是非component中的组件。^__^
component组件承担是无状态组件的责任,非component组件承担的是有状态组件的责任。
虽然这个模式规范一致在心中,但是实现的时候难免会一些偏差。
遇到的问题就是:心中的这份规范并不是很明确。
第一次重构代码完,一个有状态组件的render
函数如下:
render() {
return (
<Fragment>
<div className={style.header}>用户管理</div>
<AdvancedTable
onLoad={(params: IParams) => this.handleLoad(params)}
listOutOfDate={this.state.listOutOfDate}
toolbar={this.toolbarProps()}
table={this.tableProps()}
pagination={{ total: this.state.total }}
/>
</Fragment>
);
}
后来当我重构另一个功能的时候一个有状态组件的render
函数是下面这样的:
render() {
const { total, list } = this.state;
return (
<ResourceList
total= {total}
onLoad={(params: IParams) => this.onLoad(params)}
onAdd={() => this.onAdd()}
jump={(yarn: IYarnEntity) => this.jump(yarn)}
list={list}
title="Hadoop管理"
cols={this.columns}
/>
);
}
后面这份代码和上面的代码相比来说组件抽象的组件更加完全。
第一个组件虽然做到了有状态组件的功能,但是还是不够,因为Table
组件传递的参数还是在这个有状态组件中传递的;
第一个组件中的<div className={style.header}>用户管理</div>
还是应该在是无状态组件管辖的范围。
现在的结果就造成了,看一个月之前的代码看着就很不舒服,又想着慢慢重构。
本来想着这么做都是为了后面引入redux的做铺垫,但是现在这个感觉铺垫的时间要加长了。
详细计划
昨天晚上高烧,自己一个人在住的卧室里。虽然这不是前所未有的孤独,但是身在异乡发高烧无人照顾也显得有些凄惨,我想了很多事情。
以前可能没有这种感觉,但是现在这种感觉突然越来越浓重,那就是“我为什么要离开360”。
在2017年的六月到2018年的四月分别实习了三个公司,分别是百度,360,还有现在的公司。百度作为一个典型的大厂,虽然在人们中的口碑越来越差,但是老话说的好,瘦死的骆驼比马大呀。但是我感觉在百度并没有“比马大”这种感觉,工作了一年之后回想起在百度的将近半年的时间,我感觉我所在的那个部门是真的挫,完全就是在像搬砖,而且是搬不完的砖。只要是谁到了马上就得上手业务写代码,毫无团队建设,技术沉淀之类的。
大多数人对于项目的架构自己也是完全不理解,有时候问带我的mentor他也是一知半解,说这是上层的事情。甚至还有同事和我说:“你不搬砖怎么领导别人搬砖呢,写代码也一样”。我当时还觉得特别有道理。刚开始去的时候抱有对技术的热情,经常问其他人一些东西,别人分享的东西也参与讨论,后来后来发现他们不屑于和我交流。什么问团队中厉害人物甚至对我是敷衍。我当时觉得幸好这就是实习,但是当时就在我心中扎根了一个想法就是:“大公司都是这样,刚进来的时候都要从搬砖工变为包工头的”。但是现在想想那岂不是把很多人的才华磨灭了一段时间么,有能力的人在大公司一开始肯定也是马上发光发热的。就像我这个公司已经离职的leader,在百度待了两三年就升为T7了。
有的人在code review别人代码的时候完全就是为了code review而去code review。举个例子:写一个react组件传递样式一般有两种,通过classname或者style,我当时实现这个组件考虑了这两种情况,我使用的时候觉得传递className要新加文件等好多东西就直接传递了className。当时把我叫过去说以后不要这样搞了,原因就是不要写行内样式。我当时说出了我的想法,说这么简单的东西完全没必要,而且网上成熟的组件都是这样实现的。却得到傲慢的回答:“我现在给别人code review代码的时候,最怕的就是听到‘别人是这么做的’,‘网上都是这样写的’这类的话,因为他们根本就是错的”。我当时心想:“你可能怕的原因是因为你并不能说出‘网上’和‘别人’为什么错吧”。从那之后我再也没有反驳他的任何话了,下一次反驳就是我发现他连ES6的基本语法不知道把想合的代码打回来。
有一些好的地方就是保持了大公司一贯有的对于项目流程的严谨把握。项目评审,开发流程,上线流程都是非常严谨。当线上遇到bug的时候都是很慌的。。。
现在想想在360企业安全的时候,那时候才是真的实习。不写过多的业务代码,老大通过让我做不同的东西看我的学习能力,看能否达到要求。
刚去的时候第一天发邮件申请开发机,因为格式不对,被老大批评说脑子进水了。
然后后面因为不喜欢和同事一起去吃饭,遇到问题不喜欢和同时讨论经常收到批评。
还被老大说我不合群,不适合团队。
因为写文档的事情,我说话的时候说了一句“这个本来就不能编辑”,他说我做事要深入思考,要从设计者的原理去想,为什么这样。直到这句话还是很有用。
告诉我的mentor不要管我了,让我自生自灭。
让我看github的API的时候,我总结出来的东西不行时候对我批评数次。
我当时觉得真的是很难接受,但是现在想想真的因为那几个月我才有了很多的成长,做事不毛手毛脚了,做事喜欢和别人商量了。但是这些成长都是当时看不见的,现在感觉到了,但是。。。当时作为一个相当于温室里的大学生来讲,很少收到这么多批评,一时间有点受不了。
其实老大这个人是特别棒的,做这些也都是为了团队为了我着想。因为团队也是新成立的部门将近二十个人的样子,没有太多的业务去做,团队正在处于0到1的阶段。每天加班到最晚的工作最多的也就是那两个leader,其实已经作为他那个level的人物完全不用搞基层的代码,他每天基本中午两点都会有会议,将近一点去吃饭,可以说是工作最饱和的人了。然后还要抽出时间去关心团队代码的review,关注项目的架构。或许我永远不会忘记的一句话就是他利用午休的时间和我说,当时也是我提出要走的时候,他说:“义龙,我知道你很慌,大家现在都很慌,有的人慌是因为不知道做什么,有的人慌这件事做了不知道是为了什么。今天下午你会看到我有三个会去开,去和别人沟通我们这种方案,让别人相信我们这种架构。你还乐意跟着我干么?”。这也不是我第一次提离职了,我还以学校毕业生信息采集为借口的原因回家待了几天,在这期间和老大发微信提离职,然后给我回了电话说了一下以前对我严厉的原因。
当老大告诉HR给我发offer的时候,HR告诉老大我没有接offer,老大就又想找我谈谈。我就直接说了我得回学校了,老大阅历这么多,当然也听出来了。老大就欣然同意了,剩下的什么话也没说。这是最短的一次谈话了,剩下的几天我就是整理遇到的辞职遇相关的东西。
因为我已经决定来现在这个公司了,原因很简单就是因为这个公司给得钱多。我当时想的就是赶快挣老婆本,然后回家种田,也不想成为什么技术大牛什么的。
从360离职之后在家待了几天就来这个公司了,因为360和百度给是实习工资比较低,所以欠下了一些钱。这个公司实习每天给四百块,所以着急还债就又来这个公司实习了。每天的工作量也不算大,但是一心想着回学校快活也没想着那么多,每天就想着赶快干完活然后去学毕业设计相关的东西。带我的mentor对我也是很不错的,也是挺厉害的一个大牛。基本上没有什么吐槽的地方。但是这个公司的部门分配和其他的公司是不一样的,其他公司都是前端是一个部门,后端是一个部门,然后又项目的时候分配人去搞。这个公司的部门的前端后端产品甚至测试一起的。所以前端的leader是个后端工程师,甚至懂的前端不是很多。实习的时候最大的感触就是这里的前端不是很重要。
现在已经毕业入职一个月了,我发现我想的不一样,并不是赶快挣老婆本的钱就够了,我对技术还是又追求的。因为以前实习过的原因,mentor看我主动性挺好,就把我分到另一个组这个组就十个人左右,就我一个前端。我真的感觉到了在技术上的孤独,甚至连个讨论的人都没有了。我当时来决定来这个公司的时候已经想到了同事的前端水平不是很高的问题,但是没有想到竟然会变得就我一个人了。我其实也不能说过多的话,因为毕竟是人家给我钱,我是来打工的。说句不好听的,搬砖难道还想着穿什么衣服么。每天就是做着后端传过来什么,进行显示或者简单的逻辑就能解决的问题。甚至他们讨论什么我都插不上话。现在以前的mentor成为了其他组的leader了,经常开会也变得忙了起来。我的问题我都不到万不得已就不去找他。
现在开始说为什么离开360,是真的单单纯纯的因为钱。我做这个决定的时候也和当时还联系的晓兵做了一个比喻,说我就好像一个婊子一样,因为钱放弃了自己喜欢的东西。很长一段时间我也在安慰自己,说可能在这个公司也会又很多学到的东西。
当你面试这个公司的时候,我和二面面试官也说为什么离开百度,我想在不算小的创业公司去更早的接触项目的顶层的一些东西。我感觉我说这个真的是笑话,本来在360的时候就能经历项目从零到一的过程,而且是顶级大佬带队,对技术架构的理解更好不过了。在这个公司其实也能接触到项目从零到一的过程,但是那时候我回学校了。
现在想来真的有点对不起老大的,我上班的时候因为老大经常批我,我觉得留不下了,就还出去面试了几个公司。老大还尽力的劝我留下来。
我现在所有的感受我感觉是是我最有应得的,得到一些东西的时候往往会放弃一些东西。
其实这个应该早就写的了,但是一直是懒没有写,可能以前写的感受应该也不是很在时机,但是现在能冷静的分析一下了。
在百度学到的就是严谨的项目流程,以及到点必须把活干好。和公司只是劳务关系,只要你在职一天就得看一天的活儿。
360学到的就是,要懂得团队协作,你不会的东西可能别人已经搞的很成熟了。遇到一个问题要先直到目标,然后再说实施方法。我现在做事情也懂得先和别人交流了。在公司并不是干活儿去的,是为了看看我是不是适合这个团队,从下面几个方面:
比如我们用c语言写了一个不断接受标准输入然后打印到标准输出的程序。
当这个程序不执行的时候,躺在硬盘就是一个文件,当我们把它运行起来的时候,他就是操作系统里面中的一个进程。
所以对于进程来说,它的静态的表现形式就是硬盘中的一个文件,当它运行起来的时候,就是计算机里面状态和数据的集合,也就说进程的动态表现。
容器也是如此,容器本身也是一个tar包,当我们不运行它的时候,它也是一个文件,当我们把它运行起来的时候,他才是宿主机里面的一个进程。只不错Linux通过一些技术,让这个“进程“有了边界。
也就是我们经常会听到的:Namespace技术改变进程视图,CGroups对进程的资源进行限制
一个Docker容器就是宿主机中的一个进程
Docker使用linux内核的namespace技术来实现容器的隔离。
Linux中的Namespace有七个(namespace list)对应的功能如下表:
Namespave | 描述 | 系统调用 |
---|---|---|
UTS | 隔离hostname/NIS domain name | CLONE_NEWUSER |
IPC | 隔离进程间通信 | CLONE_NEWIPC |
PID | 隔离PID | CLONE_NEWPID |
Mount | 隔离文件系统 | CLONE_NEWNS |
Cgroups | 隔离cgroups | CLONE_NEWCGROUP |
User | 隔离用户 | CLONE_NEWUSER |
Network | 隔离网络 | CLONE_NEWNET |
Docker默认开启的Namespace有:
还有一个User Namespace默认不是开启的。
以PID Namespace举例。
当我们理解了,Docker是怎么做到隔离,然后我们来看看官网上展示的图是不是有些问题呢。
我们知道了Docker是怎么做隔离的,既然一个Docker容器在宿主机就是一个进程(可以通过docker inspect --format '{{ .State.Pid }}' containerId
查看容器在宿主机的进程),那么我们就不能让这个进程吃光了我们宿主机的资源,所以就要对使用资源进行限制。
Linux Cgroups 的全称是 Linux Control Group。它最主要的作用就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。
Cgroups暴露给用户的操作是文件系统,是通过文件或者目录的方式存在于宿主机操作系统的/sys/fs/cgroup
目录下,我们可以通过ll /sys/fs/cgroup
将他们展示出来,也可以通过mount命令:
$ mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
......
我们可以看到很多能被限制的资源,比如说cup和memery,他们都是以目录的方式存在的,目录下面的文件是对该资源限制的描述,我们可以看一下cpu下面的文件:
ls /sys/fs/cgroup/cpu
cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release
cgroup.procs cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks
这里比较重要的是cpu.cfs_period_us
和cpu.cfs_quota_us
,表示在cfs_period
这段时间内,该进程最多占用cfs_quota/cfs_period
的CPU(-1表示没有限制)。比如我们设置cfs_period=100
和cfs_quota=50
,那么这个进程最多占用宿主机50%的CPU资源。
当我们在/sys/fs/cgroup/cpu
这个目录下面新建一个目录container,container这个目录就被称为一个控制组,操作系统会给这个子系统生成默认的资源控制文件。
$ sys/fs/cgroup/cpu$ mkdir container
$ /sys/fs/cgroup/cpu$ ls container/
cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release
cgroup.procs cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks
我们可以先看一下现在操作系统对container的cpu限制:
cat /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us
-1
$ cat /sys/fs/cgroup/cpu/container/cpu.cfs_period_us
100000
结论是:没有限制
cfs_period
和cfs_quota
的单位是us,所以上述cfs_period表示的是100000us/100ms。
然后我们执行一个死循环:
while : ; do : ; done &
[1] 7512
可以看到机器上的一个cpu已经被打满了。
但是我们的预期是这个死循环进程最多占用一个CPU的百分之二十,那我们应该怎么操作呢?
cfs_quota/cfs_period
,所以我们需要修改cfs_quota
echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us
containr/tasks
里面:echo 7512 > /sys/fs/cgroup/cpu/container/tasks
我们就能看到现在这个进程的CPU资源利用率的上线就是百分之二十了。
除了CPU子系统之外,Cgroup对每一个子系统都有其独有的资源限制能力:
我们可以举个在限制docker容器的cpu只能占用20%的例子:
docker run -it --cpu-period=100000 --cpu-quota=20000 centos /bin/bash
查看容器中的:
# cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us
20000
# cat /sys/fs/cgroup/cpu/cpu.cfs_period_us
100000
众所周知的是容器之间还是共享的宿主机的内核。/proc
中存储的是当前内核运行状态的一系列的特殊文件,用户可以通过访问这些文件,查看系统以及当前正在运行的进程信息。比如CPU的使用情况,内存的占用率等,这些文件是查看系统的信息指令(比如top)的主要信息来源。
造成这个问题的原因就是:/proc文件系统并不知道用户通过Cgroup对这个容器做了什么样子的限制
怎么修复呢? lxfs
在本系列文章的第四部分将要使用官方client介绍K8S API的可编程性。本篇文章使用client-go去实践一个简单的PVC的watch程序,这个程序在前面的文章里面已经使用python和java实现过了。
在进入代码之前,理解k8s的go client项目是对我们又帮助的。它是k8s client中最古老的一个,因此具有很多特性。 Client-go 没有使用Swagger生成器,就像前面我们介绍的openAPI一样。它使用的是源于k8s项目中的源代码生成工具,这个工具的目的是要生成k8s风格的对象和序列化程序。
该项目是一组包的集合,该包能够满足从REST风格的原语到复杂client的不同的编程需求。
RESTClient是一个基础包,它使用api-machinery
库中的类型作为一组REST原语提供对API的访问。作为对RESTClient
之上的抽象,clientset将是你创建k8s client工具的起点。它暴露了公开化的API资源及其对应的序列化。
注意:
在 client-go中还包含了如discovery, dynamic, 和 scale这样的包,虽然本次不介绍这些包,但是了解它们的能力还是很重要的。
让我们再次回顾我们将要构建的工具,来说明go client的用法。pvcwatch是一个简单的命令行工具,它可以监听集群中声明的PVC容量。当总数到达一个阈值的时候,他会采取一个action(在这个例子中是在屏幕上通知显示)
你能在github上找到完整的例子
这个例子是为了展示k8s的go client的以下几个方面:
client-go支持Godep和dep作为vendor的管理程序,我觉得dep便于使用所以继续使用dep。例如,以下是client-go v6.0和k8s API v1.9所需最低限度的Gopkg.toml
。
[[constraint]]
name = "k8s.io/api"
version = "kubernetes-1.9.0"
[[constraint]]
name = "k8s.io/apimachinery"
version = "kubernetes-1.9.0"
[[constraint]]
name = "k8s.io/client-go"
version = "6.0.0"
运行dep ensure
确保剩下的工作。
我们Go client的第一步就是建立一个于API Server的连接。为了做到这一点,我们要使用实体包中的clientcmd
,如下代码所示:
import (
...
"k8s.io/client-go/tools/clientcmd"
)
func main() {
kubeconfig := filepath.Join(
os.Getenv("HOME"), ".kube", "config",
)
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
log.Fatal(err)
}
...
}
Client-go通过提供实体功能来从不同的上下文中获取你的配置,从而使之成为一个不重要的任务。
正如上面的例子所做的那样,你能从kubeconfig文件启动配置来连接API server。当你的代码运行在集群之外的时候这是一个理想的方案。
clientcmd.BuildConfigFromFlags("", configFile)
当你的代码运行在这个集群中的时候,你可以用上面的函数并且不使用任何参数,这个函数就会通过集群的信息去连接api server。
clientcmd.BuildConfigFromFlags("", "")
或者我们可以通过rest包来创建一个使用集群中的信息去配置启动的(译者注:k8s里所有的Pod都会以Volume的方式自动挂载k8s里面默认的ServiceAccount,所以会实用默认的ServiceAccount的授权信息),如下:
import "k8s.io/client-go/rest"
...
rest.InClusterConfig()
我们需要创建一个序列化的client为了让我们获取API对象。在kubernetes
包中的Clientset类型定义,提供了去访问公开的API对象的序列化client,如下:
type Clientset struct {
*authenticationv1beta1.AuthenticationV1beta1Client
*authorizationv1.AuthorizationV1Client
...
*corev1.CoreV1Client
}
一旦我们有正确的配置连接,我们就能使用这个配置去初始化一个clientset,如下:
func main() {
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
...
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
}
对于我们的例子,我们使用的是v1
的API对象。下一步,我们要使用clientset通过CoreV1()
去访问核心api资源,如下:
func main() {
...
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
api := clientset.CoreV1()
}
你能在这里看到可以获得clientsets。
我们对clientset执行的最基本操作之一获取存储的API对象的列表。在我们的例子中,我们将要拿到一个namespace下面的pvc列表,如下:
import (
...
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func main() {
var ns, label, field string
flag.StringVar(&ns, "namespace", "", "namespace")
flag.StringVar(&label, "l", "", "Label selector")
flag.StringVar(&field, "f", "", "Field selector")
...
api := clientset.CoreV1()
// setup list options
listOptions := metav1.ListOptions{
LabelSelector: label,
FieldSelector: field,
}
pvcs, err := api.PersistentVolumeClaims(ns).List(listOptions)
if err != nil {
log.Fatal(err)
}
printPVCs(pvcs)
...
}
在上面的代码中,我们使用ListOptions
指定 label 和 field selectors (还有namespace)来缩小pvc列表的范围,这个结果的返回类型是v1.PeristentVolumeClaimList
。下面的这个代码展示了我们如何去遍历和打印从api server中获取的pvc列表。
func printPVCs(pvcs *v1.PersistentVolumeClaimList) {
template := "%-32s%-8s%-8s\n"
fmt.Printf(template, "NAME", "STATUS", "CAPACITY")
for _, pvc := range pvcs.Items {
quant := pvc.Spec.Resources.Requests[v1.ResourceStorage]
fmt.Printf(
template,
pvc.Name,
string(pvc.Status.Phase),
quant.String())
}
}
k8s的Go client框架支持为指定的API对象在其生命周期事件中监听集群的能力,包括创建,更新,删除一个指定对象时候触发的CREATED
,MODIFIED
,DELETED
事件。对于我们的命令行工具,我们将要监听在集群中已经声明的PVC的总量。
对于某一个namespace,当pvc的容量到达了某一个阈值(比如说200Gi),我们将会采取某个动作。为了简单起见,我们将要在屏幕上打印个通知。但是在更复杂的实现中,可以使用相同的办法触发一个自动操作。
现在让我们为PersistentVolumeClaim
这个资源通过Watch
去创建一个监听器。然后这个监听器通过ResultChan
从go的channel中访问事件通知。
func main() {
...
api := clientset.CoreV1()
listOptions := metav1.ListOptions{
LabelSelector: label,
FieldSelector: field,
}
watcher, err :=api.PersistentVolumeClaims(ns).
Watch(listOptions)
if err != nil {
log.Fatal(err)
}
ch := watcher.ResultChan()
...
}
接下来我们将要处理资源事件。但是在我们处理事件之前,我们先声明resource.Quantity
类型的的两个变量为maxClaimsQuant
和totalClaimQuant
来分别表示我们的申请资源阈值(译者注:代表某个ns下集群中运行的PVC申请的上限)和运行总数。
import(
"k8s.io/apimachinery/pkg/api/resource"
...
)
func main() {
var maxClaims string
flag.StringVar(&maxClaims, "max-claims", "200Gi",
"Maximum total claims to watch")
var totalClaimedQuant resource.Quantity
maxClaimedQuant := resource.MustParse(maxClaims)
...
ch := watcher.ResultChan()
for event := range ch {
pvc, ok := event.Object.(*v1.PersistentVolumeClaim)
if !ok {
log.Fatal("unexpected type")
}
...
}
}
在上面的for-range
循环中,watcher的channel用于处理来自服务器传入的通知。每个事件赋值给变量event,并且event.Object
的类型被声明为PersistentVolumeClaim
类型,所以我们能从中提取出来。
当一个新的PVC创建的时候,event.Type
的值被设置为watch.Added
。然后我们用下面的代码去获取新增的声明的容量(quant
),将其添加到正在运行的总容量中(totalClaimedQuant
)。最后我们去检查是否当前的容量总值大于当初设定的最大值(maxClaimedQuant
),如果大于的话我们就触发一个事件。
import(
"k8s.io/apimachinery/pkg/watch"
...
)
func main() {
...
for event := range ch {
pvc, ok := event.Object.(*v1.PersistentVolumeClaim)
if !ok {
log.Fatal("unexpected type")
}
quant := pvc.Spec.Resources.Requests[v1.ResourceStorage]
switch event.Type {
case watch.Added:
totalClaimedQuant.Add(quant)
log.Printf("PVC %s added, claim size %s\n",
pvc.Name, quant.String())
if totalClaimedQuant.Cmp(maxClaimedQuant) == 1 {
log.Printf(
"\nClaim overage reached: max %s at %s",
maxClaimedQuant.String(),
totalClaimedQuant.String())
// trigger action
log.Println("*** Taking action ***")
}
}
...
}
}
}
代码也会在PVC被删除的时候做出反应,它执行相反的逻辑以及把被删除的这个PVC申请的容量在正在运行的容量的总值里面减去。
func main() {
...
for event := range ch {
...
switch event.Type {
case watch.Deleted:
quant := pvc.Spec.Resources.Requests[v1.ResourceStorage]
totalClaimedQuant.Sub(quant)
log.Printf("PVC %s removed, size %s\n",
pvc.Name, quant.String())
if totalClaimedQuant.Cmp(maxClaimedQuant) <= 0 {
log.Printf("Claim usage normal: max %s at %s",
maxClaimedQuant.String(),
totalClaimedQuant.String(),
)
// trigger action
log.Println("*** Taking action ***")
}
}
...
}
}
当程序在一个运行中的集群被执行的时候,首先会列出PVC的列表。然后开始监听集群中新的PersistentVolumeClaim
事件。
$> ./pvcwatch
Using kubeconfig: /Users/vladimir/.kube/config
--- PVCs ----
NAME STATUS CAPACITY
my-redis-redis Bound 50Gi
my-redis2-redis Bound 100Gi
-----------------------------
Total capacity claimed: 150Gi
-----------------------------
--- PVC Watch (max claims 200Gi) ----
2018/02/13 21:55:03 PVC my-redis2-redis added, claim size 100Gi
2018/02/13 21:55:03
At 50.0% claim capcity (100Gi/200Gi)
2018/02/13 21:55:03 PVC my-redis-redis added, claim size 50Gi
2018/02/13 21:55:03
At 75.0% claim capcity (150Gi/200Gi)
下面让我们部署一个应用到集群中,这个应用会申请75Gi
容量的存储。(例如,让我们通过helm去部署一个实例influxdb)。
helm install --name my-influx \
--set persistence.enabled=true,persistence.size=75Gi stable/influxdb
正如下面你看到的,我们的工具立刻反应出来有个新的声明以及一个警告因为当前的运行的声明总量已经大于我们设定的阈值。
--- PVC Watch (max claims 200Gi) ----
...
2018/02/13 21:55:03
At 75.0% claim capcity (150Gi/200Gi)
2018/02/13 22:01:29 PVC my-influx-influxdb added, claim size 75Gi
2018/02/13 22:01:29
Claim overage reached: max 200Gi at 225Gi
2018/02/13 22:01:29 *** Taking action ***
2018/02/13 22:01:29
At 112.5% claim capcity (225Gi/200Gi)
相反,从集群中删除一个PVC的时候,该工具会相应展示提示信息。
...
At 112.5% claim capcity (225Gi/200Gi)
2018/02/14 11:30:36 PVC my-redis2-redis removed, size 100Gi
2018/02/14 11:30:36 Claim usage normal: max 200Gi at 125Gi
2018/02/14 11:30:36 *** Taking action ***
这篇文章是进行的系列的一部分,使用Go语言的官方k8s客户端与API server进行交互。和以前一样,这个代码会逐步的去实现一个命令行工具去监听指定namespace下面的PVC的大小。这个代码实现了一个简单的监听列表去触发从服务器返回的资源事件。
从这一点开始,这系列文章将要继续使用client-go这个框架。在接下来的写作中,将要用控制器模式使用client-go 创建更加更加强大的client。
总结一下公司内部的状态/数据管理工具ER和Redux的区别
ER和Redux都是遵从的单向数据流的**,单向数据流也是比较适用于React的。工作中主要使用的ER以前是用的Redux,下面就对这两种框架进行总结一下
Redux
中定义了一个个的Action
用它来表达有事情发生,但是怎么发生,转变为下一个state
要通过reducer
去表达,所以reducer
的作用就是表达了要做什么事情,然后变为的状态会存储在唯一的store
中,然后把store
注入到App中,然后App中又能通过触发action
转变为下一个状态。用图示就是下面这样子:
ER是内部使用的工具,大致**如下:数据状态的管理都存放在Model
里面,然后通过props
把Model
里面的数据注入JSX
当中,然后再jsx中能够通过dispatch
去触发改变Model里面的数据,图示如下:
lsblk -o name,kname,fstype,mountpoint,model,size,rand
NAME | 物理盘以及分区 |
---|---|
KNAME | 设备的名称 |
FSTYPE | 文件系统类型 |
MOUNTPOINT | 系统分区挂载点 |
MODEL | 设备的型号信息 |
SIZE | 分区大小 |
RAND | 0代表SSD,1代表非SSD |
mkfs.ext4 /dev/sdb
mount /dev/sdb /mnt/disk1
当下无论大厂小厂的前后端开发模式都是前后端分离。以前遇到通过jsonp解决跨域的方式也渐渐的淡出的工程中(不了解jsonp的可以看JSONP跨域请求+简答实现百度搜索)。当前端请求一个接口的时候就会引起跨域,但是当下的前端构建工具都有相应的解决方案,比如webpack中web-dev-server这个插件,就能很简单的启一个本地的服务,然后发请求的时候通过启的本地服务去发送请求,这样就解决跨域问题的一部分了。这种情况下客户端代码和正常非跨域请求一摸一样,不用做任何改变。
上面说的方法只是解决了一部分,还有一部分我想大家可能或多或少的会遇到过,就是在登录场景的时候。服务端的response里面有set-cookie这个字段,在客户端中设置cookie,cookie里面可能包含着的seesionID表示的当前登录的用户/当前的登录状态(对这方面不理解的可以看通过cookie和session让http协议变得有状态)。当用户已经登录并且访问其他页面的时候,服务端会通过cookie中的信息去校验用户登录状态,如果请求中没有携带身份信息或者身份信息过期(服务端返回401/403)就会跳转到登录界面。这种情况如果在前后端联调的时候比较麻烦,因为上面方法解决的跨域是不会携带cookie的。目前有两种方法去解决这个:
综上: CORS的主要任务都落在服务端,但是如果为了联调服务端的开发代码和生产代码有区别,他们肯定是会不搞的。
今天组里的实习生在使用Axios去验证登录的时候遇到了跨域的问题(前端是vue, 后端是Spring boot)。
一般的请求通过前端设置代理,服务端设置Access-Control-Allow-origin:\*
就可以了,但是登录时候响应头里面要去set cookie就遇到了问题,结果由于对CORS跨域理解的不是很深刻,对预检请求不是很了解,就在axios的issues里面去搜,通过Axios doesn't send cookies with POST and data定位到了问题,原来他后端写的拦截器里面自动把OPTIONS这个请求给过滤掉了,没有让走到后面的流程。
CORS的出现是为了解决由于浏览器的同源策略带来的请求跨域问题。
“跨资源共享(Cross-Origin Resource Sharing(CORS))是通过HTTP Response header来告诉浏览器,让运行在一个origin(domain)上的web应用被允许访问来自不同源服务器上指定的资源的一种机制。”
简单来说: CORS就是通过设置请求的响应头(能通过开发人员控制的基本都是服务端的响应头,客户端的也会有对应的请求头,但一般不会是开发人员去控制的,后面会仔细说)去控制是否允许某个origin的某个/些请求跨域。
CORS标准新增了一组HTTP首部字段,允许服务端声明哪些源站通过浏览器有权访问哪些资源。对于能对服务器产生副作用的HTTP非简单请求(non-simple request)(特别是除了GET请求以外的请求),浏览器必须首先发送一个方法为OPTIONS的一个预检请求(preflight request)来获取服务器是否允许该请求跨域。服务器得到确认之后,才发起真正的HTTP请求。在预检请求中,服务端也可以通知客户端是否要携带Credentials
.
某些请求是不会触发预检查请求的,这些请求被成为简单请求(simple request)。
如果一个请求满足下列的所有条件就可以被称为简单请求:
application/x-www-form-urlencoded
multipart/form-data
text/plain
不满足上面定义的简单请求,都会发送预检请求。
比如浏览器要发送一个POST请求,content-Type为application/json,新增一个request header为X-TEST
预检请求的步骤:
对于跨域(发生CORS)的请求默认是不会带上凭证信息(credentials)的,如果要发送凭证信息(credentials)就需要设置对应的标识位。
请求:
响应:
HTTP规范规定Access-Control-Allow-Origin
不能是通配符*并且只能是单一的origin
。这是因为如果能设置多个的话,证明该服务器就能接受多个域名下面的cookie,这是很危险的。
Access-Control-Allow-Origin: <origin> | *
origin参数的值制定了允许访问服务器资源的外域URI。对于不需要携带身份凭证的请求,服务器可以指定这个字段的值为通配符*,表示允许来自所有域的请求。
该头信息服务器把允许浏览器访问的头放入白名单,例如:
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
在跨域访问的时候,XHR对象的getResponseHeader()只能拿到一些最基本的响应头。
指定了预检请求(preflight)请求的结果能被缓存多久(秒为单位)。
当浏览器的credentials设置为true时,是否允许浏览器读取response的内容
作为预检请求的响应头,指明了实际请求所允许的HTTP方法。
用于预检请求的响应。其指明了实际请求中允许携带的首部字段。
以逗号分割。
这些字段一般无需手动设置。
预检请求或实际请求的源站。
不包含任何路径,只是服务器的名称。(不管是否为跨域,这个字段都被发送。)
用于预检请求。其作用是,将实际请求所使用的 HTTP 方法告诉服务器。
泛型的目的有两个:提供有意义的约束和加强类型安全。
我们现在这一小结谈一谈提供有意义的约束,第二个问题我们在最后在说。
先上一段js的代码:
const arr1 = []
arr1.push(true)
arr.push(3)
for (let item of arr1) {
console.log(item)
console.log(item.toFixed(2))
}// Uncaught TypeError: item.toFixed is not a function
已经为boolean是没有toFixed
方法的,所以就会报错的。
当我们使用TS指定类型的时候,代码会是下面这个样子:
const arr1: number[] = []
arr1.push(true) // Argument of type 'true' is not assignable to parameter of type 'number'.
arr.push(3)
这样在编译阶段就会报错,并不会等到运行时。
再来一段代码:让我们手动实现一下数组的reverse
方法(这个方法在数组的原型上有):
function reverseArr(arr: number[]): number[] {
const ret: number[] = [];
for (let i = arr.length - 1; i >= 0; i--)
ret.push(arr[i])
return ret
}
const arr = reverseArr([1, 4, 6])
arr[0] = true // Type 'true' is not assignable to type 'number'.
同刚才说的,数组中的每个元素也都是只能是number类型的。那如果有一天我们想要字符串数组的反转,还得把number改为string?(这里得场景也可以用重载实现,因为这里主要介绍泛型,就不多说了),使用泛型得方式如下:
function reverseArr<T>(arr: T[]): T[] {
const ret: T[] = [];
for (let i = arr.length - 1; i >= 0; i--)
ret.push(arr[i])
return ret
}
const arr1: number[] = reverseArr([1, 4, 6])
const arr2 = reverseArr<string>(['1', '4', '6']) // arr2: string[]
const arr3 = reverseArr(['1', '4', '6']) // arr3: string[]
const arr4 = reverseArr([1, '4', '6']) // arr4: (string | number)[]
arr3.push(4) // Argument of type '4' is not assignable to parameter of type 'string'.
arr4.push(1)
arr4.push('d')
这就体现出泛型的好处了,能指定类型。
arr1
没有指定类型,TS是能推断(关于类型推断的东西,在后面会说)出来的;
arr2
是通过<string>
指定了类型;
arr3
同arr1
也是通过推断出来的;
arr4
因为传递的参数number
和string
都有,所以就推断为string | number)
在上一小节我们介绍的时候简单介绍了泛型变量(声明function reverseArr<T>(arr: T[]): T[]
的时候)。
那如果我们要把这个函数赋值给一个变量的时候,这个变量怎么指定类型呢?请看下面:
const tmp: <T>(parms: T[]) => T[] = reverseArr;
这样可能略显不清晰,当然也可以把<T>(parms: T[]) => T[]
这部分声明为alias。我们也可以通过interface去声明,如下:
interface IReverse {
<T>(parms: T[]): T[]
}
const tmp: IReverse = reverseArr;
当然我们还可以提前把reverseArr
需要的类型传递了,如下:
interface IReverse<T> {
(parms: T[]): T[]
}
const tmp: IReverse<string> = reverseArr;
上面的就属于泛型接口了。
对于泛型类也是同样的道理,直接如下:
class Animals<T, U> {
name: T;
getMouseAndLegTotal: (m: U, l: U) => U;
}
const ani = new Animals<string, number>(); // 也可以把这里的number改为string,在实现的对应修改即可
ani.name = 'Dog'
ani.getMouseAndLegTotal = function (m: number, l: number) {
return m + l
}
我们还能给泛系赋默认值,如下:
class Animall<T> {
name: T;
}
interface IProps<T> {
age: T
}
class Dogg<T = number> extends Animall<IProps<T>> {
sex: T;
}
const d = new Dogg;
d.sex = 'sd'// Type '"sd"' is not assignable to type 'number'.
d.sex = 32
虽然泛型解决了我们代码复用以及对类型的约束,但是我们有时候会觉得泛型有点设计过度了呢,比如我们下面这个函数:
function gao<T>(param: T): T {
console.log(param.name); // Property 'name' does not exist on type 'T'
return param
}
gao({name: 'helios'})
gao({name: 'helios2', age: 23})
我们已经保证了,每次传入的参数肯定含有name这个属性,但是我们在gao这个函数中缺不能用。所以我们能进一步的进行约束,先看代码:
interface IHaveName {
name: string
}
function gao<T extends IHaveName>(param: T): T {
param.name = 'helios'
return param
}
gao({name: 'helios'})
gao({name: 'helios2', age: 23})
gao({ age: 23 })
//Argument of type '{ age: number; }' is not assignable to parameter of type 'IHaveName'.
// Object literal may only specify known properties, and 'age' does not exist in type 'IHaveName'.
<T extends IHaveName>
这个的含义是: IHaveName
必须是传入的类型的子集,也就是说T中必须包含 IHaveName
中的所有类型,如果不包含就会报上面代码块中gao({ age: 23 })
的错误。
在泛型中还能使用类类型,这里官网上的例子就很好了,我就直接粘过来了:
class BeeKeeper {
hasMask: boolean;
}
class ZooKeeper {
nametag: string;
}
class Animal {
numLegs: number;
}
class Bee extends Animal {
keeper: BeeKeeper;
}
class Lion extends Animal {
keeper: ZooKeeper;
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag; // typechecks!
createInstance(Bee).keeper.hasMask; // typechecks!
Typescript能够根据自己规则去推断(没有声明类型的)变量的类型。
基本的类型推断是很容易理解的:
比如:
const fa = 8; // number
const str = ''// string
function add(a: number, b: number) {
// function add(a: number, b: number): number
return a + b
}
const adder: (a: number, b: number) => number = function (a, b) {
// function (a, b) a,b就不用写类型了
return a + b
}
以上都是符合正常的逻辑,这里还有提一下在tsconfig
中的noImplicitAny
参数,如果这个参数不指定的话(默认值为false)如果遇到推断不出来的类型会设置为any;如果指定这个参数的话,如果ts推断不出来会报错,如下代码:
function add2(a, b) { // 压根推断不出来
return a + b
}
条件表达式和编程语言中三目运算符很像,形如:T extends U ? X : Y
。
T extends U
表示如果U是T的子集那么返回类型X如果不是返回类型Y。
看下面几个例子:
interface IName {
name: string
}
type typeName1<T> = T extends string ? string : object;
type typeName2<T> = T extends IName ? IName : object;
type typeNameA = typeName1<string> // string
type typeNameB = typeName1<number>// object
type typeNameC = typeName2<number>// object
type typeNameD = typeName2<{name: '32', age: 3}> // IName
条件类型的还能处理联合类型,有条件类型会分别处理联合类型中的每个类型,如下:
type typeName1<T> = T extends string ? string : object;
type typeNameE = typeName1<string | boolean> // string | object
我们有时候想知道一个函数定义(一个实际函数)的参数类型或者返回值,如下:
// 函数
function getP(param1: string, param2: boolean) {
return true
}
// 函数定义(别名)
type Func = (param: IParams) => boolean
如果我们想知道他们函数参数的类型怎么办呢?现在infer就可以登场了。
infer表示一个在conditional type**待推断的类型(可以是extends后面,也可以在条件类型为true的分支中),可能语言比较枯燥,直接上代码吧:
type GetReturnParam<T> = T extends (...param: infer U) => any ? U : never
type ret1 = GetReturnParam<Func> // [IParams]
type ret2 = GetReturnParam<typeof getP> // [string, boolean]
infer U
就代表去推断参数param的类型。如果是在true去返回这个类型。
那如果我们想要得到函数(函数定义的返回值)怎么办呢,上代码:
type GetReturnType<T> = T extends (...param: any) => infer U ? U : never
type ret3 = GetReturnType<Func> // boolean
type ret4 = GetReturnType<typeof getP> // boolean
只要把infer
的位置换一下就可以了。
反正记住了infer U放在哪就代表去推断哪里的类型
在ts2.8中已经把*ReturnType<T>
获取函数返回值类型。*集成进来了。
这也不是Typescript提出的新概念了,在其他的高级语言如java/scala中都有类似的概念。在What are covariance and contravariance?(中文版: 协变与逆变)说的挺好的了。
抗变和协变的主要目的是为了保证类型的安全,比如有下面几个类:
class Animal {
}
class Dog extends Animal {
}
class HuaDog extends Dog {
}
然后我们定义一个函数类型:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.