diff --git a/blog/en-us/dart/syntax.md b/blog/en-us/dart/syntax.md new file mode 100644 index 0000000..c480f08 --- /dev/null +++ b/blog/en-us/dart/syntax.md @@ -0,0 +1,1624 @@ +# Dart语法学习 + +## 目录 +* 参考资料 +* 语言特性 +* 关键字 +* 变量与常量 +* 数据类型 +* 运算符 operators +* 控制流程语句 +* 异常 Exceptions +* 函数 Function +* 类 Class +* 类-方法 +* 类-抽象类 +* 类-隐式接口 +* 类-扩展一个类(重写) +* 库和可见性 +* 异步支持 + +## 参考资料 + +* [【官方文档】](https://www.dartlang.org/guides/language/language-tour ) +* [【极客学院】](http://wiki.jikexueyuan.com/project/dart-language-tour/) +* [【author:AWeiLoveAndroid】](https://www.jianshu.com/p/3d927a7bf020) +* [【author:soojade】](https://www.jianshu.com/p/a7cc623132b0) +* [【author:优腾爱乐】](https://www.jianshu.com/p/8a62b1a2fd75) + +## 语言特性 + +* Dart所有的东西都是对象, 即使是数字numbers、函数function、null也都是对象,所有的对象都继承自Object类。 + +* Dart动态类型语言, 尽量给变量定义一个类型,会更安全,没有显示定义类型的变量在 debug 模式下会类型会是 dynamic(动态的)。 + +* Dart 在 running 之前解析你的所有代码,指定数据类型和编译时的常量,可以提高运行速度。 + +* Dart中的类和接口是统一的,类即接口,你可以继承一个类,也可以实现一个类(接口),自然也包含了良好的面向对象和并发编程的支持。 + +* Dart 提供了顶级函数(如:main())。 + +* Dart 没有 public、private、protected 这些关键字,变量名以"_"开头意味着对它的 lib 是私有的。 + +* 没有初始化的变量都会被赋予默认值 null。 + +* final的值只能被设定一次。const 是一个编译时的常量,可以通过 const 来创建常量值,var c=const[];,这里 c 还是一个变量,只是被赋值了一个常量值,它还是可以赋其它值。实例变量可以是 final,但不能是 const。 + +* 编程语言并不是孤立存在的,Dart也是这样,他由语言规范、虚拟机、类库和工具等组成: + + * SDK:SDK 包含 Dart VM、dart2js、Pub、库和工具。 + * Dartium:内嵌 Dart VM 的 Chromium ,可以在浏览器中直接执行 dart 代码。 + * Dart2js:将 Dart 代码编译为 JavaScript 的工具。 + * Dart Editor:基于 Eclipse 的全功能 IDE,并包含以上所有工具。支持代码补全、代码导航、快速修正、重构、调试等功能。 + +## 关键字(56个) + +关键字|-|-|- +-|-|-|- +abstract | do | import | super +as | dynamic | in | switch +assert | else | interface | sync +enum | implements | is | this +async | export | library | throw +await | external | mixin | true +break | extends | new | try +case | factory | null | typedef +catch | false | operator | var +class | final | part | void +const | finally | rethrow | while +continue | for | return | with +covariant | get | set | yield +default | if | static | deferred + +## 变量与常量 + +1. 变量声明与初始化 + +* 调用的变量name包含对String值为“张三” 的对象的引用,name推断变量的类型是String,但可以通过指定它来更改该类型,如果对象不限于单一类型(没有明确的类型),请使用Object或dynamic关键字。 +```dart + // 没有明确类型,编译的时候根据值明确类型 + var name = ‘Bob’; + Object name = '张三'; + dynamic name = '李四'; + + // 显示声明将被推断类型, 可以使用String显示声明字符串类型 + String name = 'Bob' ; +``` + +2. 默认值 + +* 未初始化的变量的初始值为null(包括数字),因此数字、字符串都可以调用各种方法 + +```dart + //测试 数字类型的初始值是什么? + int lineCount; + // 为false的时候抛出异常 + assert(lineCount == null); + print(lineCount); //打印结果为null,证明数字类型初始化值是null +``` + +3. final and const + * 如果您从未打算更改一个变量,那么使用 final 或 const,不是var,也不是一个类型。 + 一个 final 变量只能被初始化一次; const变量是一个编译时常量,(Const变量是隐式的final) + final的顶级或类变量在第一次使用时被初始化。 + + * 被final修饰的顶级变量或类变量在第一次声明的时候就需要初始化。 + + ``` dart + // The final variable 'outSideFinalName' must be initialized. + final String outSideFinalName + + ``` + + * 被final或者const修饰的变量,变量类型可以省略,建议指定数据类型。 + + ``` dart + //可以省略String这个类型声明 + final name = "Bob"; + final String name1 = "张三"; + + const name2 = "alex"; + const String name3 = "李四"; + ``` + + * 被 final 或 const 修饰的变量无法再去修改其值。 + + ``` dart + final String outSideFinalName = "Alex"; + // outSideFinalName', a final variable, can only be set once + // 一个final变量,只能被设置一次。 + outSideFinalName = "Bill"; + + const String outSideName = 'Bill'; + // 这样写,编译器提示:Constant variables can't be assigned a value + // const常量不能赋值 + // outSideName = "小白"; + ``` + + * flnal 或者 const 不能和 var 同时使用 + + ``` dart + // Members can't be declared to be both 'const' and 'var' + const var String outSideName = 'Bill'; + + // Members can't be declared to be both 'final' and 'var' + final var String name = 'Lili'; + ``` + + * 常量如果是类级别的,请使用 static const + + ``` dart + // 常量如果是类级别的,请使用 static const + static const String name3 = 'Tom'; + + // 这样写保存 + // Only static fields can be declared as const + // 只有静态字段可以声明为const + //const String name3 = 'Tom'; + ``` + + * 常量的运算 + + ``` dart + const speed = 100; //速度(km/h) + const double distance = 2.5 * speed; // 距离 = 时间 * 速度 + + final speed2 = 100; //速度(km/h) + final double distance2 = 2.5 * speed2; // 距离 = 时间 * 速度 + + ``` + + * const关键字不只是声明常数变量,您也可以使用它来创建常量值,以及声明创建常量值的构造函数,任何变量都可以有一个常量值。 + + ``` dart + // 注意: [] 创建的是一个空的list集合 + // const []创建一个空的、不可变的列表(EIL)。 + var varList = const []; // varList 当前是一个EIL + final finalList = const []; // finalList一直是EIL + const constList = const []; // constList 是一个编译时常量的EIL + + // 可以更改非final,非const变量的值 + // 即使它曾经具有const值 + varList = ["haha"]; + + // 不能更改final变量或const变量的值 + // 这样写,编译器提示:a final variable, can only be set once + // finalList = ["haha"]; + // 这样写,编译器提示:Constant variables can't be assigned a value + // constList = ["haha"]; + + ``` + + * 在常量表达式中,该运算符的操作数必须为'bool'、'num'、'String'或'null', const常量必须用conat类型的值初始化。 + + ``` dart + const String outSideName = 'Bill'; + final String outSideFinalName = 'Alex'; + const String outSideName2 = 'Tom'; + + const aConstList = const ['1', '2', '3']; + + // In constant expressions, operands of this operator must be of type 'bool', 'num', 'String' or 'null' + // 在常量表达式中,该运算符的操作数必须为'bool'、'num'、'String'或'null'。 + const validConstString = '$outSideName $outSideName2 $aConstList'; + + // Const variables must be initialized with a constant value + // const常量必须用conat类型的值初始化 + const validConstString = '$outSideName $outSideName2 $outSideFinalName'; + + var outSideVarName='Cathy'; + // Const variables must be initialized with a constant value. + // const常量必须用conat类型的值初始化 + const validConstString = '$outSideName $outSideName2 $outSideVarName'; + + // 正确写法 + const String outSideConstName = 'Joy'; + const validConstString = '$outSideName $outSideName2 $outSideConstName'; + + ``` + +## 数据类型 + +1. num + + * num 是数字类型的父类,有两个子类 int 和 double。 + + * int 根据平台的不同,整数值不大于64位。在Dart VM上,值可以从-263到263 - 1,编译成JavaScript的Dart使用JavaScript代码,允许值从-253到253 - 1。 + + * double 64位(双精度)浮点数,如IEEE 754标准所规定。 + + ``` dart + int a = 1; + print(a); + + double b = 1.12; + print(b); + + // String -> int + int one = int.parse('1'); + // 输出3 + print(one + 2); + + // String -> double + var onePointOne = double.parse('1.1'); + // 输出3.1 + print(onePointOne + 2); + + // int -> String + String oneAsString = 1.toString(); + // The argument type 'int' can't be assigned to the parameter type 'String' + //print(oneAsString + 2); + // 输出 1 + 2 + print('$oneAsString + 2'); + // 输出 1 2 + print('$oneAsString 2'); + + // double -> String 注意括号中要有小数点位数,否则报错 + String piAsString = 3.14159.toStringAsFixed(2); + // 截取两位小数, 输出3.14 + print(piAsString); + + String aString = 1.12618.toStringAsFixed(2); + // 检查是否四舍五入,输出1.13,发现会做四舍五入 + print(aString); + ``` + +2. String + + * Dart里面的String是一系列 UTF-16 代码单元。 + + * Dart里面的String是一系列 UTF-16 代码单元。 + + * 单引号或者双引号里面嵌套使用引号。 + + * 用 或{} 来计算字符串中变量的值,需要注意的是如果是表达式需要${表达式} + + ``` dart + String singleString = 'abcdddd'; + String doubleString = "abcsdfafd"; + + String sdString = '$singleString a "bcsd" ${singleString}'; + String dsString = "abc 'aaa' $sdString"; + print(sdString); + print(dsString); + + + String singleString = 'aaa'; + String doubleString = "bbb"; + // 单引号嵌套双引号 + String sdString = '$singleString a "bbb" ${doubleString}'; + // 输出 aaa a "bbb" bbb + print(sdString); + + // 双引号嵌套单引号 + String dsString = "${singleString.toUpperCase()} abc 'aaa' $doubleString.toUpperCase()"; + // 输出 AAA abc 'aaa' bbb.toUpperCase(), + 可以看出 ”$doubleString.toUpperCase()“ 没有加“{}“,导致输出结果是”bbb.toUpperCase()“ + print(dsString); + ``` + +3. bool + + * Dart 是强 bool 类型检查,只有bool 类型的值是true 才被认为是true。 + * 只有两个对象具有bool类型:true和false,它们都是编译时常量。 + * Dart的类型安全意味着您不能使用 if(nonbooleanValue) 或 assert(nonbooleanValue) 等代码, 相反Dart使用的是显式的检查值。 + * assert 是语言内置的断言函数,仅在检查模式下有效在开发过程中, 除非条件为真,否则会引发异常。(断言失败则程序立刻终止)。 + + ``` dart + // 检查是否为空字符串 + var fullName = ''; + assert(fullName.isEmpty); + + // 检查0 + var hitPoints = 0; + assert(hitPoints <= 0); + + // 检查是否为null + var unicorn; + assert(unicorn == null); + + // 检查是否为NaN + var iMeantToDoThis = 0 / 0; + assert(iMeantToDoThis.isNaN); + + ``` + +4. List集合 + + * 在Dart中,数组是List对象,因此大多数人只是将它们称为List。Dart list文字看起来像JavaScript数组文字 + + ``` dart + //创建一个int类型的list + List list = [10, 7, 23]; + // 输出[10, 7, 23] + print(list); + + // 使用List的构造函数,也可以添加int参数,表示List固定长度,不能进行添加 删除操作 + var fruits = new List(); + + // 添加元素 + fruits.add('apples'); + + // 添加多个元素 + fruits.addAll(['oranges', 'bananas']); + + List subFruits = ['apples', 'oranges', 'banans']; + // 添加多个元素 + fruits.addAll(subFruits); + + // 输出: [apples, oranges, bananas, apples, oranges, banans] + print(fruits); + + // 获取List的长度 + print(fruits.length); + + // 获取第一个元素 + print(fruits.first); + + // 获取元素最后一个元素 + print(fruits.last); + + // 利用索引获取元素 + print(fruits[0]); + + // 查找某个元素的索引号 + print(fruits.indexOf('apples')); + + // 删除指定位置的元素,返回删除的元素 + print(fruits.removeAt(0)); + + // 删除指定元素,成功返回true,失败返回false + // 如果集合里面有多个“apples”, 只会删除集合中第一个改元素 + fruits.remove('apples'); + + // 删除最后一个元素,返回删除的元素 + fruits.removeLast(); + + // 删除指定范围(索引)元素,含头不含尾 + fruits.removeRange(start,end); + + // 删除指定条件的元素(这里是元素长度大于6) + fruits.removeWhere((item) => item.length >6); + + // 删除所有的元素 + fruits.clear(); + ``` + * 注意事项: + + 1. 可以直接打印list包括list的元素,list也是一个对象。但是java必须遍历才能打印list,直接打印是地址值。 + 2. 和java一样list里面的元素必须保持类型一致,不一致就会报错。 + 3. 和java一样list的角标从0开始。 + 4. 如果集合里面有多个相同的元素“X”, 只会删除集合中第一个改元素 + +5. Map集合 + + * 一般来说,map是将键和值相关联的对象。键和值都可以是任何类型的对象。每个键只出现一次,但您可以多次使用相同的值。Dart支持map由map文字和map类型提供。 + * 初始化Map方式一: 直接声明,用{}表示,里面写key和value,每组键值对中间用逗号隔开。 + + ``` dart + // Two keys in a map literal can't be equal. + // Map companys = {'Alibaba': '阿里巴巴', 'Tencent': '腾讯', 'baidu': '百度', 'Alibaba': '钉钉', 'Tenect': 'qq-music'}; + + Map companys = {'Alibaba': '阿里巴巴', 'Tencent': '腾讯', 'baidu': '百度'}; + // 输出:{Alibaba: 阿里巴巴, Tencent: 腾讯, baidu: 百度} + print(companys); + + ``` + + * 创建Map方式二:先声明,再去赋值。 + + ``` dart + Map schoolsMap = new Map(); + schoolsMap['first'] = '清华'; + schoolsMap['second'] = '北大'; + schoolsMap['third'] = '复旦'; + // 打印结果 {first: 清华, second: 北大, third: 复旦} + print(schoolsMap); + + var fruits = new Map(); + fruits["first"] = "apple"; + fruits["second"] = "banana"; + fruits["fifth"] = "orange"; + //换成双引号,换成var 打印结果 {first: apple, second: banana, fifth: orange} + print(fruits); + ``` + + * Map API + + ``` dart + // 指定键值对的参数类型 + var aMap = new Map(); + + // Map的赋值,中括号中是Key,这里可不是数组 + aMap[1] = '小米'; + + //Map中的键值对是唯一的 + //同Set不同,第二次输入的Key如果存在,Value会覆盖之前的数据 + aMap[1] = 'alibaba'; + + // map里面的value可以相同 + aMap[2] = 'alibaba'; + + // map里面value可以为空字符串 + aMap[3] = ''; + + // map里面的value可以为null + aMap[4] = null; + + print(aMap); + + // 检索Map是否含有某Key + assert(aMap.containsKey(1)); + + //删除某个键值对 + aMap.remove(1); + + print(aMap); + ``` + + * 注意事项 + 1. map的key类型不一致也不会报错。 + 2. 添加元素的时候,会按照你添加元素的顺序逐个加入到map里面,哪怕你的key,比如分别是 1,2,4,看起来有间隔,事实上添加到map的时候是{1:value,2:value,4:value} 这种形式。 + 3. map里面的key不能相同。但是value可以相同,value可以为空字符串或者为null。 +## 运算符 + +描述 | 操作符 +-|- +一元后置操作符 | expr++ expr-- () [] . ?. +一元前置操作符 | expr !expr ~expr ++expr --expr +乘除 | * / % ~/ +加减 | + - +位移 | << >> +按位与 | & +按位或 | +按位异或 | ^ +逻辑与 | && +逻辑或 | +关系和类型判断 | >= > <= < as is is! +等 | == != +如果为空 | ?? +条件表达式 | expr1 ? expr2 : expr3 +赋值 | = *= /= ~/= %= += -= <<= >>= &= ^= = ??= +级联 | .. + +## 流程控制语句(Control flow statements) + +* if...else +* for +* while do-whild +* break continue +* break continue +* assert(仅在checked模式有效) + +## 异常(Exceptions) + +1. throw + * 抛出固定类型的异常 + ``` dart + throw new FormatException('Expected at least 1 section'); + ``` + + * 抛出任意类型的异常 + + ``` dart + throw 'Out of llamas!'; + ``` + * 因为抛出异常属于表达式,可以将throw语句放在=>语句中,或者其它可以出现表达式的地方 + + ``` dart + distanceTo(Point other) => + throw new UnimplementedError(); + ``` +2. catch + + * 将可能出现异常的代码放置到try语句中,可以通过 on语句来指定需要捕获的异常类型,使用catch来处理异常。 + ``` dart + try { + breedMoreLlamas(); + } on OutOfLlamasException { + // A specific exception + buyMoreLlamas(); + } on Exception catch (e) { + // Anything else that is an exception + print('Unknown exception: $e'); + } catch (e, s) { + print('Exception details:\n $e'); + print('Stack trace:\n $s'); + } + ``` + +3. rethrow + + * rethrow语句用来处理一个异常,同时希望这个异常能够被其它调用的部分使用。 + + ``` dart + final foo = ''; + + void misbehave() { + try { + foo = "1"; + } catch (e) { + print('2'); + rethrow;// 如果不重新抛出异常,main函数中的catch语句执行不到 + } + } + + void main() { + try { + misbehave(); + } catch (e) { + print('3'); + } + } + ``` +4. finally + + * Dart的finally用来执行那些无论异常是否发生都执行的操作。 + + ``` dart + + final foo = ''; + + void misbehave() { + try { + foo = "1"; + } catch (e) { + print('2'); + } + } + + void main() { + try { + misbehave(); + } catch (e) { + print('3'); + } finally { + print('4'); // 即使没有rethrow最终都会执行到 + } + } + + ``` + +## 函数 Function +* 以下是一个实现函数的例子: + +``` dart + bool isNoble(int atomicNumber) { + return _nobleGases[atomicNumber] != null; + } +``` + +1. main()函数 + * 每个应用程序都必须有一个顶层main()函数,它可以作为应用程序的入口点。该main()函数返回void并具有List参数的可选参数。 + ``` dart + void main() { + querySelector('#sample_text_id') + ..text = 'Click me!' + ..onClick.listen(reverseText); + } + + ``` + + * 级联符号..允许您在同一个对象上进行一系列操作。除了函数调用之外,还可以访问同一对象上的字段。这通常会为您节省创建临时变量的步骤,并允许您编写更流畅的代码。 + + ``` dart + querySelector('#confirm') // Get an object. + ..text = 'Confirm' // Use its members. + ..classes.add('important') + ..onClick.listen((e) => window.alert('Confirmed!')); + + ``` + + * 上述例子相对于: + + ``` dart + var button = querySelector('#confirm'); + button.text = 'Confirm'; + button.classes.add('important'); + button.onClick.listen((e) => window.alert('Confirmed!')); + + ``` + + * 级联符号也可以嵌套使用。 例如: + + ``` dart + final addressBook = (AddressBookBuilder() + ..name = 'jenny' + ..email = 'jenny@example.com' + ..phone = (PhoneNumberBuilder() + ..number = '415-555-0100' + ..label = 'home') + .build()) + .build(); + + ``` + + * 当返回值是void时不能构建级联。 例如,以下代码失败: + + ``` dart + var sb = StringBuffer(); + sb.write('foo') // 返回void + ..write('bar'); // 这里会报错 + + ``` + * 注意: 严格地说,级联的..符号不是操作符。它只是Dart语法的一部分。 + +2. 可选参数 + + * 可选的命名参数, 定义函数时,使用{param1, param2, …},用于指定命名参数。例如: + + ``` dart + //设置[bold]和[hidden]标志 + void enableFlags({bool bold, bool hidden}) { + // ... + } + + enableFlags(bold: true, hidden: false); + ``` + + * 可选的位置参数,用[]它们标记为可选的位置参数: + + ``` dart + String say(String from, String msg, [String device]) { + var result = '$from says $msg'; + if (device != null) { + result = '$result with a $device'; + } + return result; + } + ``` + * 下面是一个不带可选参数调用这个函数的例子: + + ``` dart + say('Bob', 'Howdy'); //结果是: Bob says Howdy + ``` + + * 下面是用第三个参数调用这个函数的例子: + + ``` dart + say('Bob', 'Howdy', 'smoke signal'); //结果是:Bob says Howdy with a smoke signal + ``` + +3. 默认参数 + + * 函数可以使用=为命名参数和位置参数定义默认值。默认值必须是编译时常量。如果没有提供默认值,则默认值为null。 + * 下面是为命名参数设置默认值的示例: + ``` dart + // 设置 bold 和 hidden 标记的默认值都为false + void enableFlags2({bool bold = false, bool hidden = false}) { + // ... + } + + // 调用的时候:bold will be true; hidden will be false. + enableFlags2(bold: true); + ``` + * 下一个示例显示如何为位置参数设置默认值: + ``` dart + String say(String from, String msg, + [String device = 'carrier pigeon', String mood]) { + var result = '$from says $msg'; + if (device != null) { + result = '$result with a $device'; + } + if (mood != null) { + result = '$result (in a $mood mood)'; + } + return result; + } + + //调用方式: + say('Bob', 'Howdy'); //结果为:Bob says Howdy with a carrier pigeon; + ``` + + * 您还可以将list或map作为默认值传递。下面的示例定义一个函数doStuff(),该函数指定列表参数的默认list和gifts参数的默认map。 + + ``` dart + // 使用list 或者map设置默认值 + void doStuff( + {List list = const [1, 2, 3], + Map gifts = const {'first': 'paper', + 'second': 'cotton', 'third': 'leather' + }}) { + print('list: $list'); + print('gifts: $gifts'); + } + ``` + +4. 作为一个类对象的功能 + + * 您可以将一个函数作为参数传递给另一个函数。 + + ``` dart + void printElement(int element) { + print(element); + } + + var list = [1, 2, 3]; + + // 把 printElement函数作为一个参数传递进来 + list.forEach(printElement); + ``` + + * 您也可以将一个函数分配给一个变量。 + + ``` dart + var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!'; + assert(loudify('hello') == '!!! HELLO !!!'); + ``` + +5. 匿名函数 + + * 大多数函数都能被命名为匿名函数,如 main() 或 printElement()。您还可以创建一个名为匿名函数的无名函数,有时也可以创建lambda或闭包。您可以为变量分配一个匿名函数,例如,您可以从集合中添加或删除它。 + * 一个匿名函数看起来类似于一个命名函数 - 0或更多的参数,在括号之间用逗号和可选类型标注分隔。 + * 下面的代码块包含函数的主体: + ``` dart + ([[Type] param1[, …]]) { + codeBlock; + }; + ``` + * 下面的示例定义了一个具有无类型参数的匿名函数item,该函数被list中的每个item调用,输出一个字符串,该字符串包含指定索引处的值。 + + ``` dart + var list = ['apples', 'bananas', 'oranges']; + list.forEach((item) { + print('${list.indexOf(item)}: $item'); + }); + ``` + * 如果函数只包含一条语句,可以使用箭头符号=>来缩短它, 比如上面的例2可以简写成: + + ``` dart + list.forEach((item) => print('${list.indexOf(item)}: $item')); + ``` +6. 返回值 + * 所有函数都返回一个值,如果没有指定返回值,则语句return null,隐式地附加到函数体。 + ``` dart + foo() {} + assert(foo() == null); + ``` +## 类(Classes) + +1. 对象 + + * Dart 是一种面向对象的语言,并且支持基于mixin的继承方式。 + * Dart 语言中所有的对象都是某一个类的实例,所有的类有同一个基类--Object。 + * 基于mixin的继承方式具体是指:一个类可以继承自多个父类。 + * 使用new语句来构造一个类,构造函数的名字可能是ClassName,也可以是ClassName.identifier, 例如: + ``` dart + var jsonData = JSON.decode('{"x":1, "y":2}'); + + // Create a Point using Point(). + var p1 = new Point(2, 2); + + // Create a Point using Point.fromJson(). + var p2 = new Point.fromJson(jsonData); + + ``` + * 使用.(dot)来调用实例的变量或者方法。 + ``` dart + var p = new Point(2, 2); + + // Set the value of the instance variable y. + p.y = 3; + + // Get the value of y. + assert(p.y == 3); + + // Invoke distanceTo() on p. + num distance = p.distanceTo(new Point(4, 4)); + ``` + * 使用?.来确认前操作数不为空, 常用来替代. , 避免左边操作数为null引发异常。 + + ``` dart + // If p is non-null, set its y value to 4. + p?.y = 4; + ``` + * 使用const替代new来创建编译时的常量构造函数。 + ``` dart + var p = const ImmutablePoint(2, 2); + ``` + * 使用runtimeType方法,在运行中获取对象的类型。该方法将返回Type 类型的变量。 + ``` dart + print('The type of a is ${a.runtimeType}'); + ``` + +2. 实例化变量(Instance variables) + * 在类定义中,所有没有初始化的变量都会被初始化为null。 + ``` dart + class Point { + num x; // Declare instance variable x, initially null. + num y; // Declare y, initially null. + num z = 0; // Declare z, initially 0. + } + ``` + + * 类定义中所有的变量, Dart语言都会隐式的定义 setter 方法,针对非空的变量会额外增加 getter 方法。 + + ``` dart + class Point { + num x; + num y; + } + + main() { + var point = new Point(); + point.x = 4; // Use the setter method for x. + assert(point.x == 4); // Use the getter method for x. + assert(point.y == null); // Values default to null. + } + ``` +3. 构造函数(Constructors) + * 声明一个和类名相同的函数,来作为类的构造函数。 + ``` dart + class Point { + num x; + num y; + + Point(num x, num y) { + // There's a better way to do this, stay tuned. + this.x = x; + this.y = y; + } + } + ``` + * this关键字指向了当前类的实例, 上面的代码可以简化为: + ``` dart + class Point { + num x; + num y; + + // Syntactic sugar for setting x and y + // before the constructor body runs. + Point(this.x, this.y); + } + ``` +4. 构造函数不能继承(Constructors aren’t inherited) + + * Dart 语言中,子类不会继承父类的命名构造函数。如果不显式提供子类的构造函数,系统就提供默认的构造函数。 + +5. 命名的构造函数(Named constructors) + * 使用命名构造函数从另一类或现有的数据中快速实现构造函数。 + ``` dart + class Point { + num x; + num y; + + Point(this.x, this.y); + + // 命名构造函数Named constructor + Point.fromJson(Map json) { + x = json['x']; + y = json['y']; + } + } + ``` + * 构造函数不能被继承,父类中的命名构造函数不能被子类继承。如果想要子类也拥有一个父类一样名字的构造函数,必须在子类是实现这个构造函数。 + + 6. 调用父类的非默认构造函数 + * 默认情况下,子类只能调用父类的无名,无参数的构造函数; 父类的无名构造函数会在子类的构造函数前调用; 如果initializer list 也同时定义了,则会先执行initializer list 中的内容,然后在执行父类的无名无参数构造函数,最后调用子类自己的无名无参数构造函数。即下面的顺序: + + 1. initializer list(初始化列表) + 2. super class’s no-arg constructor(父类无参数构造函数) + 3. main class’s no-arg constructor (主类无参数构造函数) + * 如果父类不显示提供无名无参数构造函数的构造函数,在子类中必须手打调用父类的一个构造函数。这种情况下,调用父类的构造函数的代码放在子类构造函数名后,子类构造函数体前,中间使用:(colon) 分割。 + ``` dart + class Person { + String firstName; + + Person.fromJson(Map data) { + print('in Person'); + } + } + + class Employee extends Person { + // 父类没有无参数的非命名构造函数,必须手动调用一个构造函数 + super.fromJson(data) + Employee.fromJson(Map data) : super.fromJson(data) { + print('in Employee'); + } + } + + main() { + var emp = new Employee.fromJson({}); + + // Prints: + // in Person + // in Employee + if (emp is Person) { + // Type check + emp.firstName = 'Bob'; + } + (emp as Person).firstName = 'Bob'; + } + + ``` + 7. 初始化列表 + * 除了调用父类的构造函数,也可以通过初始化列表在子类的构造函数体前(大括号前)来初始化实例的变量值,使用逗号,分隔。如下所示: + ``` dart + class Point { + num x; + num y; + + Point(this.x, this.y); + + // 初始化列表在构造函数运行前设置实例变量。 + Point.fromJson(Map jsonMap) + : x = jsonMap['x'], + y = jsonMap['y'] { + print('In Point.fromJson(): ($x, $y)'); + } + } + ``` + **注意:上述代码,初始化程序无法访问 this 关键字。** +8. 静态构造函数 + * 如果你的类产生的对象永远不会改变,你可以让这些对象成为编译时常量。为此,需要定义一个 const 构造函数并确保所有的实例变量都是 final 的。 + + ``` dart + class ImmutablePoint { + final num x; + final num y; + const ImmutablePoint(this.x, this.y); + static final ImmutablePoint origin = const ImmutablePoint(0, 0); + } + + ``` +9. 重定向构造函数 + + * 有时候构造函数的目的只是重定向到该类的另一个构造函数。重定向构造函数没有函数体,使用冒号:分隔。 + ``` dart + class Point { + num x; + num y; + + // 主构造函数 + Point(this.x, this.y) { + print("Point($x, $y)"); + } + + // 重定向构造函数,指向主构造函数,函数体为空 + Point.alongXAxis(num x) : this(x, 0); + } + + void main() { + var p1 = new Point(1, 2); + var p2 = new Point.alongXAxis(4); + } + ``` +10. 常量构造函数 + + * 如果类的对象不会发生变化,可以构造一个编译时的常量构造函数。定义格式如下: + * 定义所有的实例变量是final。 + * 使用const声明构造函数。 + ``` dart + class ImmutablePoint { + final num x; + final num y; + const ImmutablePoint(this.x, this.y); + static final ImmutablePoint origin = const ImmutablePoint(0, 0); + } + + ``` +11. 工厂构造函数 + + * 当实现一个使用 factory 关键词修饰的构造函数时,这个构造函数不必创建类的新实例。例如,工厂构造函数可能从缓存返回实例,或者它可能返回子类型的实例。 下面的示例演示一个工厂构造函数从缓存返回的对象: + + ``` dart + class Logger { + final String name; + bool mute = false; + + // _cache 是一个私有库,幸好名字前有个 _ 。 + static final Map _cache = {}; + + factory Logger(String name) { + if (_cache.containsKey(name)) { + return _cache[name]; + } else { + final logger = new Logger._internal(name); + _cache[name] = logger; + return logger; + } + } + + Logger._internal(this.name); + + void log(String msg) { + if (!mute) { + print(msg); + } + } + + } + ``` + **注意:工厂构造函数不能用 this。** +## 方法 +* 方法就是为对象提供行为的函数。 +1. 实例方法 + + * 对象的实例方法可以访问实例变量和 this 。以下示例中的 distanceTo() 方法是实例方法的一个例子: + ``` dart + import 'dart:math'; + + class Point { + num x; + num y; + Point(this.x, this.y); + + num distanceTo(Point other) { + var dx = x - other.x; + var dy = y - other.y; + return sqrt(dx * dx + dy * dy); + } + } + ``` +2. setters 和 Getters + + * 是一种提供对方法属性读和写的特殊方法。每个实例变量都有一个隐式的 getter 方法,合适的话可能还会有 setter 方法。你可以通过实现 getters 和 setters 来创建附加属性,也就是直接使用 get 和 set 关键词: + ``` dart + class Rectangle { + num left; + num top; + num width; + num height; + + Rectangle(this.left, this.top, this.width, this.height); + + // 定义两个计算属性: right and bottom. + num get right => left + width; + set right(num value) => left = value - width; + num get bottom => top + height; + set bottom(num value) => top = value - height; + } + + main() { + var rect = new Rectangle(3, 4, 20, 15); + assert(rect.left == 3); + rect.right = 12; + assert(rect.left == -8); + } + ``` + * 借助于 getter 和 setter ,你可以直接使用实例变量,并且在不改变客户代码的情况下把他们包装成方法。 + * 注: 不论是否显式地定义了一个 getter,类似增量(++)的操作符,都能以预期的方式工作。为了避免产生任何向着不期望的方向的影响,操作符一旦调用 getter ,就会把他的值存在临时变量里。 +3. 抽象方法 + * Instance , getter 和 setter 方法可以是抽象的,也就是定义一个接口,但是把实现交给其他的类。要创建一个抽象方法,使用分号(;)代替方法体: + ``` dart + abstract class Doer { + // ...定义实例变量和方法... + void doSomething(); // 定义一个抽象方法。 + } + + class EffectiveDoer extends Doer { + void doSomething() { + // ...提供一个实现,所以这里的方法不是抽象的... + } + } + + ``` + + 4. 枚举类型 + + * 枚举类型,通常被称为 enumerations 或 enums ,是一种用来代表一个固定数量的常量的特殊类。 + * 声明一个枚举类型需要使用关键字 enum : + ``` dart + enum Color { + red, + green, + blue + } + + ``` + * 在枚举中每个值都有一个 index getter 方法,它返回一个在枚举声明中从 0 开始的位置。例如,第一个值索引值为 0 ,第二个值索引值为 1 。 + + ``` dart + assert(Color.red.index == 0); + assert(Color.green.index == 1); + assert(Color.blue.index == 2); + ``` + * 要得到枚举列表的所有值,可使用枚举的 values 常量。 + + ``` dart + List colors = Color.values; + assert(colors[2] == Color.blue); + + ``` + +* 你可以在 switch 语句 中使用枚举。如果 e 在 switch (e) 是显式类型的枚举,那么如果你不处理所有的枚举值将会弹出警告: + + ``` dart + ***枚举类型有以下限制*** + * 你不能在子类中混合或实现一个枚举。 + * 你不能显式实例化一个枚举。 + enum Color { + red, + green, + blue + } + // ... + Color aColor = Color.blue; + switch (aColor) { + case Color.red: + print('Red as roses!'); + break; + + case Color.green: + print('Green as grass!'); + break; + + default: // Without this, you see a WARNING. + print(aColor); // 'Color.blue' + } + + ``` +5. 为类添加特征:mixins + + * mixins 是一种多类层次结构的类的代码重用。 + * 要使用 mixins ,在 with 关键字后面跟一个或多个 mixin 的名字。下面的例子显示了两个使用mixins的类: + ``` dart + class Musician extends Performer with Musical { + // ... + } + + class Maestro extends Person with Musical, + Aggressive, Demented { + + Maestro(String maestroName) { + name = maestroName; + canConduct = true; + } + } + ``` + * 要实现 mixin ,就创建一个继承 Object 类的子类,不声明任何构造函数,不调用 super 。例如: + ``` dart + abstract class Musical { + bool canPlayPiano = false; + bool canCompose = false; + bool canConduct = false; + + void entertainMe() { + if (canPlayPiano) { + print('Playing piano'); + } else if (canConduct) { + print('Waving hands'); + } else { + print('Humming to self'); + } + } + } + ``` +6. 类的变量和方法 + * 使用 static 关键字来实现类变量和类方法。 + * 只有当静态变量被使用时才被初始化。 + * 静态变量, 静态变量(类变量)对于类状态和常数是有用的: + ``` dart + class Color { + static const red = const Color('red'); // 一个恒定的静态变量 + final String name; // 一个实例变量。 + const Color(this.name); // 一个恒定的构造函数。 + } + + main() { + assert(Color.red.name == 'red'); + } + ``` + * 静态方法, 静态方法(类方法)不在一个实例上进行操作,因而不必访问 this 。例如: + + ``` dart + import 'dart:math'; + + class Point { + num x; + num y; + Point(this.x, this.y); + + static num distanceBetween(Point a, Point b) { + var dx = a.x - b.x; + var dy = a.y - b.y; + return sqrt(dx * dx + dy * dy); + } + } + + main() { + var a = new Point(2, 2); + var b = new Point(4, 4); + var distance = Point.distanceBetween(a, b); + assert(distance < 2.9 && distance > 2.8); + } + + ``` + * 注:考虑到使用高阶层的方法而不是静态方法,是为了常用或者广泛使用的工具和功能。 + * 你可以将静态方法作为编译时常量。例如,你可以把静态方法作为一个参数传递给静态构造函数。 + ## 抽象类 + + * 使用 abstract 修饰符来定义一个抽象类,该类不能被实例化。抽象类在定义接口的时候非常有用,实际上抽象中也包含一些实现。如果你想让你的抽象类被实例化,请定义一个 工厂构造函数 。 + * 抽象类通常包含 抽象方法。下面是声明一个含有抽象方法的抽象类的例子: + ``` dart + // 这个类是抽象类,因此不能被实例化。 + abstract class AbstractContainer { + // ...定义构造函数,域,方法... + + void updateChildren(); // 抽象方法。 + } + ``` + * 下面的类不是抽象类,因此它可以被实例化,即使定义了一个抽象方法: + ``` dart + class SpecializedContainer extends AbstractContainer { + // ...定义更多构造函数,域,方法... + + void updateChildren() { + // ...实现 updateChildren()... + } + + // 抽象方法造成一个警告,但是不会阻止实例化。 + void doSomething(); + } + ``` + + ## 类-隐式接口 + + * 每个类隐式的定义了一个接口,含有类的所有实例和它实现的所有接口。如果你想创建一个支持类 B 的 API 的类 A,但又不想继承类 B ,那么,类 A 应该实现类 B 的接口。 + + * 一个类实现一个或更多接口通过用 implements 子句声明,然后提供 API 接口要求。例如: + + ``` dart + + // 一个 person ,包含 greet() 的隐式接口。 + class Person { + // 在这个接口中,只有库中可见。 + final _name; + + // 不在接口中,因为这是个构造函数。 + Person(this._name); + + // 在这个接口中。 + String greet(who) => 'Hello, $who. I am $_name.'; + } + + // Person 接口的一个实现。 + class Imposter implements Person { + // 我们不得不定义它,但不用它。 + final _name = ""; + + String greet(who) => 'Hi $who. Do you know who I am?'; + } + + greetBob(Person person) => person.greet('bob'); + + main() { + print(greetBob(new Person('kathy'))); + print(greetBob(new Imposter())); + } + + ``` + * 这里是具体说明一个类实现多个接口的例子: + + ``` dart + class Point implements Comparable, Location { + // ... + } + ``` +## 类-扩展一个类 + +* 使用 extends 创建一个子类,同时 supper 将指向父类: + +``` dart + class Television { + void turnOn() { + _illuminateDisplay(); + _activateIrSensor(); + } + // ... + } + + class SmartTelevision extends Television { + + void turnOn() { + super.turnOn(); + _bootNetworkInterface(); + _initializeMemory(); + _upgradeApps(); + } + // ... + } +``` +* 子类可以重载实例方法, getters 方法, setters 方法。下面是个关于重写 Object 类的方法 noSuchMethod() 的例子,当代码企图用不存在的方法或实例变量时,这个方法会被调用。 +``` dart + class A { + // 如果你不重写 noSuchMethod 方法, 就用一个不存在的成员,会导致NoSuchMethodError 错误。 + void noSuchMethod(Invocation mirror) { + print('You tried to use a non-existent member:' + + '${mirror.memberName}'); + } + } + +``` +* 你可以使用 @override 注释来表明你重写了一个成员。 +``` dart + class A { + @override + void noSuchMethod(Invocation mirror) { + // ... + } + } + +``` + +* 如果你用 noSuchMethod() 实现每一个可能的 getter 方法,setter 方法和类的方法,那么你可以使用 @proxy 标注来避免警告。 + +``` dart +@proxy + class A { + void noSuchMethod(Invocation mirror) { + // ... + } + } +``` + +## 库和可见性 + +1. import,part,library指令可以帮助创建一个模块化的,可共享的代码库。库不仅提供了API,还提供隐私单元:以下划线(_)开头的标识符只对内部库可见。每个Dartapp就是一个库,即使它不使用库指令。 + +2. 库可以分布式使用包。见 Pub Package and Asset Manager 中有关pub(SDK中的一个包管理器)。 + +3. 使用库 + * 使用 import 来指定如何从一个库命名空间用于其他库的范围。 + * 使用 import 来指定如何从一个库命名空间用于其他库的范围。 + ``` dart + import 'dart:html'; + ``` + * 唯一需要 import 的参数是一个指向库的 URI。对于内置库,URI中具有特殊dart:scheme。对于其他库,你可以使用文件系统路径或package:scheme。包 package:scheme specifies libraries ,如pub工具提供的软件包管理器库。例如: + ``` dart + import 'dart:io'; + import 'package:mylib/mylib.dart'; + import 'package:utils/utils.dart'; + ``` +4. 指定库前缀 + + * 如果导入两个库是有冲突的标识符,那么你可以指定一个或两个库的前缀。例如,如果 library1 和 library2 都有一个元素类,那么你可能有这样的代码: + + ``` dart + import 'package:lib1/lib1.dart'; + import 'package:lib2/lib2.dart' as lib2; + // ... + var element1 = new Element(); // 使用lib1里的元素 + var element2 = + new lib2.Element(); // 使用lib2里的元素 + ``` +5. 导入部分库 + + * 如果想使用的库一部分,你可以选择性导入库。例如: + ``` dart + // 只导入foo库 + import 'package:lib1/lib1.dart' show foo; + + //导入所有除了foo + import 'package:lib2/lib2.dart' hide foo; + + ``` + + 6. 延迟加载库 + * 延迟(deferred)加载(也称为延迟(lazy)加载)允许应用程序按需加载库。下面是当你可能会使用延迟加载某些情况: + + * 为了减少应用程序的初始启动时间; + * 执行A / B测试-尝试的算法的替代实施方式中; + * 加载很少使用的功能,例如可选的屏幕和对话框。 + + + + * 为了延迟加载一个库,你必须使用 deferred as 先导入它。 + + ``` dart + import 'package:deferred/hello.dart' deferred as hello; + ``` + + * 当需要库时,使用该库的调用标识符调用 LoadLibrary()。 + ``` dart + greet() async { + await hello.loadLibrary(); + hello.printGreeting(); + } + ``` + * 在前面的代码,在库加载好之前,await关键字都是暂停执行的。有关 async 和 await 见 asynchrony support 的更多信息。 + 您可以在一个库调用 LoadLibrary() 多次都没有问题。该库也只被加载一次。 + + * 当您使用延迟加载,请记住以下内容: + + * 延迟库的常量在其作为导入文件时不是常量。记住,这些常量不存在,直到迟库被加载完成。 + * 你不能在导入文件中使用延迟库常量的类型。相反,考虑将接口类型移到同时由延迟库和导入文件导入的库。 + * Dart隐含调用LoadLibrary()插入到定义deferred as namespace。在调用LoadLibrary()函数返回一个Future。 + +7. 库的实现 + + * 用 library 来来命名库,用part来指定库中的其他文件。 注意:不必在应用程序中(具有顶级main()函数的文件)使用library,但这样做可以让你在多个文件中执行应用程序。 + + + + + +8. 声明库 + * 利用library identifier(库标识符)指定当前库的名称: + ``` dart + // 声明库,名ballgame + library ballgame; + + // 导入html库 + import 'dart:html'; + + // ...代码从这里开始... + ``` +9. 关联文件与库 + + * 添加实现文件,把part fileUri放在有库的文件,其中fileURI是实现文件的路径。然后在实现文件中,添加部分标识符(part of identifier),其中标识符是库的名称。下面的示例使用的一部分,在三个文件来实现部分库。 + + * 第一个文件,ballgame.dart,声明球赛库,导入其他需要的库,并指定ball.dart和util.dart是此库的部分: + ``` dart + library ballgame; + + import 'dart:html'; + // ...其他导入在这里... + + part 'ball.dart'; + part 'util.dart'; + + // ...代码从这里开始... + ``` + + * 第二个文件ball.dart,实现了球赛库的一部分: + ``` dart + part of ballgame; + + // ...代码从这里开始... + ``` + + * 第三个文件,util.dart,实现了球赛库的其余部分: + ``` dart + part of ballgame; + + // ...Code goes here... + ``` + +10. 重新导出库(Re-exporting libraries) + + *可以通过重新导出部分库或者全部库来组合或重新打包库。例如,你可能有实现为一组较小的库集成为一个较大库。或者你可以创建一个库,提供了从另一个库方法的子集。 + + ``` dart + // In french.dart: + library french; + + hello() => print('Bonjour!'); + goodbye() => print('Au Revoir!'); + + // In togo.dart: + library togo; + + import 'french.dart'; + export 'french.dart' show hello; + + // In another .dart file: + import 'togo.dart'; + + void main() { + hello(); //print bonjour + goodbye(); //FAIL + } + + ``` +## 异步的支持 + +1. Dart 添加了一些新的语言特性用于支持异步编程。最通常使用的特性是 async 方法和 await 表达式。Dart 库大多方法返回 Future 和 Stream 对象。这些方法是异步的:它们在设置一个可能的耗时操作(比如 I/O 操作)之后返回,而无需等待操作完成 + +2. 当你需要使用 Future 来表示一个值时,你有两个选择。 + + * 使用 async 和 await + * 使用 Future API + + + +3. 同样的,当你需要从 Stream 获取值的时候,你有两个选择。 + +使用 async 和一个异步的 for 循环 (await for) +使用 Stream API + + +4. 使用 async 和 await 的代码是异步的,不过它看起来很像同步的代码。比如这里有一段使用 await 等待一个异步函数结果的代码: +``` dart +await lookUpVersion() +``` + +5. 要使用 await,代码必须用 await 标记 +``` dart + checkVersion() async { + var version = await lookUpVersion(); + if (version == expectedVersion) { + // Do something. + } else { + // Do something else. + } + } +``` + + + +6. 你可以使用 try, catch, 和 finally 来处理错误并精简使用了 await 的代码。 +``` dart + try { + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 4044); + } catch (e) { + // React to inability to bind to the port... + } +``` + + +7. 声明异步函数 + * 一个异步函数是一个由 async 修饰符标记的函数。虽然一个异步函数可能在操作上比较耗时,但是它可以立即返回-在任何方法体执行之前。 + ``` dart + checkVersion() async { + // ... + } + + lookUpVersion() async => /* ... */; + ``` + * 在函数中添加关键字 async 使得它返回一个 Future,比如,考虑一下这个同步函数,它将返回一个字符串。 + * String lookUpVersionSync() => '1.0.0'; + * 如果你想更改它成为异步方法-因为在以后的实现中将会非常耗时-它的返回值是一个 Future 。 + * Future lookUpVersion() async => '1.0.0'; + * 请注意函数体不需要使用 Future API,如果必要的话 Dart 将会自己创建 Future 对象 + + +8. 使用带 future 的 await 表达式 + + * 一个 await表达式具有以下形式 + ``` dart + await expression + ``` + + * 在异步方法中你可以使用 await 多次。比如,下列代码为了得到函数的结果一共等待了三次。 + ``` dart + var entrypoint = await findEntrypoint(); + var exitCode = await runExecutable(entrypoint, args); + await flushThenExit(exitCode); + ``` + + * 在 await 表达式中, 表达式 的值通常是一个 Future 对象;如果不是,那么这个值会自动转为 Future。这个 Future 对象表明了表达式应该返回一个对象。await 表达式 的值就是返回的一个对象。在对象可用之前,await 表达式将会一直处于暂停状态。 + + * 如果 await 没有起作用,请确认它是一个异步方法。比如,在你的 main() 函数里面使用await,main() 的函数体必须被 async 标记: + + ``` dart + main() async { + checkVersion(); + print('In main: version is ${await lookUpVersion()}'); + } + + ``` + + + +9. 结合 streams 使用异步循环 + + * 一个异步循环具有以下形式: + ``` dart + await for (variable declaration in expression) { + // Executes each time the stream emits a value. + } + ``` + * 表达式 的值必须有Stream 类型(流类型)。执行过程如下: + + * 在 stream 发出一个值之前等待 + * 执行 for 循环的主体,把变量设置为发出的值。 + * 重复 1 和 2,直到 Stream 关闭 + + + * 如果要停止监听 stream ,你可以使用 break 或者 return 语句,跳出循环并取消来自 stream 的订阅 。 + * 如果一个异步 for 循环没有正常运行,请确认它是一个异步方法。 比如,在应用的 main() 方法中使用异步的 for 循环时,main() 的方法体必须被 async 标记。 + ``` dart + main() async { + ... + + await for (var request in requestServer) { + handleRequest(request); + } + + ... + } + ``` + + +更多关于异步编程的信息,请看 dart:async 库部分的介绍。你也可以看文章 [Dart Language Asynchrony Support: Phase 1 ](https://www.dartlang.org/articles/await-async/) diff --git a/blog/en-us/docker/docker-compose.md b/blog/en-us/docker/docker-compose.md new file mode 100644 index 0000000..9b2daf5 --- /dev/null +++ b/blog/en-us/docker/docker-compose.md @@ -0,0 +1,501 @@ +# docker和docker-compose 配置 mysql mssql mongodb redis nginx jenkins 环境 + +## 磁盘挂载 + +``` shell +fdisk -l #查看磁盘列表 +mkfs.ext4 /dev/vdb #格式化磁盘 +mount /dev/vdb /data #挂载磁盘在/data +echo '/dev/vdb /data ext4 defaults,nofail 0 1'>> /etc/fstab # 启动服务器自动挂载 +mount -a #校验自动挂载脚本 +df -h #查看磁盘挂载后信息 +``` + +## docker + +### 安装 docker + +``` shell +yum update #更新系统包 +yum install -y yum-utils device-mapper-persistent-data lvm2 #安装yum-utils +yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo #为yum源添加docker仓库位置 +yum install docker-ce #安装docker +systemctl enable docker #设置开机自动启动 +systemctl start docker #启动docker +systemctl stop docker #暂停docker +mv /var/lib/docker /data/docker # 修改Docker镜像的存放位置 +ln -s /data/docker /var/lib/docker #建立软连接 +echo '{ + "registry-mirrors": [ + "https://dockerhub.azk8s.cn", + "https://hub-mirror.c.163.com", + "https://registry.docker-cn.com" + ] +} +'>> /etc/docker/daemon.json # 镜像下载代理 +``` + +### 拉取 Java 镜像 + +``` shell +docker pull java +``` + +### 拉取SqlServer镜像 + +``` shell +docker pull microsoft/mssql-server-linux # 拉取SqlServer镜像 +docker run -p 1433:1433 --name mssql \ # run 运行容器 -p 将容器的1433端口映射到主机的1433端口 --name 容器运行的名字 +--restart=always \ # 挂断自动重新启动 +-v /data/sqlserver:/var/opt/mssql \ # 挂载mssql文件夹到主机 +-e ACCEPT_EULA=Y \ # 同意协议 +-e MSSQL_SA_PASSWORD=mssql-MSSQL \ # 初始化sa密码 +-u root \ # 指定容器为root运行 +-d microsoft/mssql-server-linux # -d 后台运行 +``` + +### 拉取 MySql 镜像 + +``` shell +docker pull mysql #拉取 MySql +docker run -p 3306:3306 --name mysql \ # run 运行容器 -p 将容器的3306端口映射到主机的3306端口 --name 容器运行的名字 +--restart=always \ # 挂断自动重新启动 +-v /etc/localtime:/etc/localtime \ # 将主机本地时间夹挂在到容器 +-v /data/mysql/log:/var/log/mysql \ # 将日志文件夹挂载到主机 +-v /data/mysql/data:/var/lib/mysql \ # 将数据文件夹挂载到主机 +-v /data/mysql/mysql-files:/var/lib/mysql-files \ # 将数据文件夹挂载到主机 +-v /data/mysql/conf:/etc/mysql \ # 将配置文件夹挂在到主机 +-e MYSQL_ROOT_PASSWORD=xiujingmysql. \ # 初始化root用户的密码 +-d mysql # -d 后台运行 +docker exec -it mysql /bin/bash # 进入Docker容器内部的bash +``` + +### 拉取 Mongodb 镜像 + +``` shell +docker pull mongo #拉取 mongodb +docker run -p 27017:27017 --name mongo \ # run 运行容器 -p 将容器的27017端口映射到主机的27017端口 --name 容器运行的名字 +--restart=always \ # 挂断自动重新启动 +-v /etc/localtime:/etc/localtime \ # 将主机本地时间夹挂在到容器 +-v /data/mongodb/db:/data/db \ # 将数据文件夹挂载到主机 +-v /data/mongodb/configdb:/data/configdb \ # 将数据库配置文件挂载到主机 +-v /data/mongodb/initdb:/docker-entrypoint-initdb.d # 通过/docker-entrypoint-initdb.d/将更复杂的用户设置显式留给用户 当容器首次启动时它会执行/docker-entrypoint-initdb.d 目录下的sh 和js脚本 。 以脚本字母顺序执行 +-e MONGO_INITDB_ROOT_USERNAME=admin \ # 设置admin数据库账户名称 如果使用了此项,则不需要 --auth 参数 +-e MONGO_INITDB_ROOT_PASSWORD=admin \ # 设置admin数据库账户密码 如果使用了此项,则不需要 --auth 参数 +-d mongo \ # -d 后台运行 +--auth # --auth 需要密码才能访问容器服务 + +docker exec -it mongo mongo admin # 进入mongo +db.createUser({ user:'admin',pwd:'123456',roles:[ { role:'userAdminAnyDatabase', db: 'admin'}]}); #创建一个名为 admin,密码为 123456 的用户。 +db.auth('admin', '123456') # 尝试使用上面创建的用户信息进行连接。 +``` + +### 拉取 Redis 镜像 + +``` shell +docker pull redis #拉取 redis +docker run -p 6379:6379 --name redis \ # run 运行容器 -p 将容器的6379端口映射到主机的6379端口 --name 容器运行的名字 +--restart=always \ # 挂断自动重新启动 +-v /etc/localtime:/etc/localtime \ # 将主机本地时间夹挂在到容器 +-v /data/redis:/data \ # 将数据文件夹挂载到主机 +-d redis \ # -d 后台运行 +redis-server --appendonly yes \ # 在容器执行redis-server启动命令,并打开redis持久化配置 +--requirepass "123456" # 设置密码123456 +``` + +### 拉取 Nginx 镜像 + +``` shell +docker pull nginx #拉取 nginx +docker run -p 80:80 -p 443:443 --name nginx -d nginx # 运行容器 +docker container cp nginx:/etc/nginx /data/nginx/ #拷贝容器配置 +docker rm -f nginx # 删除容器 +``` + +nginx 配置文件 + +``` conf + +user nginx; +worker_processes 1; + +error_log /var/log/nginx/error_log.log warn; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on; + + gzip on; #开启gzip + gzip_disable "msie6"; #IE6不使用gzip + gzip_vary on; #设置为on会在Header里增加 "Vary: Accept-Encoding" + gzip_proxied any; #代理结果数据的压缩 + gzip_comp_level 6; #gzip压缩比(1~9),越小压缩效果越差,但是越大处理越慢,所以一般取中间值 + gzip_buffers 16 8k; #获取多少内存用于缓存压缩结果 + gzip_http_version 1.1; #识别http协议的版本 + gzip_min_length 1k; #设置允许压缩的页面最小字节数,超过1k的文件会被压缩 + gzip_types application/javascript text/css; #对特定的MIME类型生效,js和css文件会被压缩 + + include /etc/nginx/conf.d/*.conf; + + server { + #nginx同时开启http和https + listen 80 default backlog=2048; + listen 443 ssl; + server_name ysf.djtlpay.com; + + ssl_certificate /ssl/1_ysf.djtlpay.com_bundle.crt; + ssl_certificate_key /ssl/2_ysf.djtlpay.com.key; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + } +} + +``` +运行 nginx +``` shell +docker run -p 80:80 -p 443:443 --name nginx \ # run 运行容器 -p 将容器的80,443端口映射到主机的80,443端口 --name 容器运行的名字 +--restart=always \ # 挂断自动重新启动 +-v /etc/localtime:/etc/localtime \ # 将主机本地时间夹挂在到容器 +-v /data/nginx/html:/usr/share/nginx/html \ # nginx 静态资源 +-v /data/nginx/logs:/var/log/nginx \ # 将日志文件夹挂载到主机 +-v /data/nginx/conf:/etc/nginx \ # 将配置文件夹挂在到主机 +-v /data/nginx/conf/ssl:/ssl \ # 将证书文件夹挂在到主机 +-d nginx # +``` + +### 拉取Jenkins镜像: +```shell +docker pull jenkins/jenkins:lts # 拉取 jenkins +docker run -p 8080:8080 -p 50000:50000 --name jenkins \ # run 运行容器 -p 将容器的8080,50000端口映射到主机的8080,50000端口 --name 容器运行的名字 +--restart=always \ # 挂断自动重新启动 +-u root \ # 运行的用户为root +-v /etc/localtime:/etc/localtime \ # 将主机本地时间夹挂在到容器 +-v /data/jenkins_home:/var/jenkins_home \ # 将jenkins_home文件夹挂在到主机 +-e JAVA_OPTS=-Duser.timezone=Asia/Shanghai \ #设置jenkins运行环境时区 +-d jenkins/jenkins:lts # -d 后台运行 +``` + +### 拉取MinIO镜像 +```shell +docker pull minio/minio # 拉取MinIO镜像 +docker run -p 9000:9000 --name minio \ # run 运行容器 -p 将容器的9000,9000端口映射到主机的9000,9000端口 --name 容器运行的名 +--restart=always \ # 挂断自动重新启动 +-v /etc/localtime:/etc/localtime \ # 将主机本地时间夹挂在到容器 +-v /data/minio/data:/data \ # 将data文件夹挂在到主机 +-v /data/minio/config:/root/.minio \ # 将配置文件夹挂在到主机 +-e "MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE" \ # 设置MINIO_ACCESS_KEY的值 +-e "MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" \ # 设置MINIO_SECRET_KEY值 +-d minio/minio server /data # -d 后台运行 server /data 导出/data目录 + +``` + +### 拉取Portainer镜像 + +``` shell +docker pull portainer/portainer # 拉取MinIO镜像 +docker run -p 8001:8000 -p 9001:9000 --name portainer \ # run 运行容器 -p 将容器的8000,9000端口映射到主机的8000,9000端口 --name 容器运行的名 +--restart=always \ # 挂断自动重新启动 +-v /etc/localtime:/etc/localtime \ # 将主机本地时间夹挂在到容器 +-v /var/run/docker.sock:/var/run/docker.sock \ # 将docker.sock文件夹挂在到主机 +-v /data/portainer/data:/data \ # 将配置文件夹挂在到主机 +-d portainer/portainer portainer # -d 后台运行 +``` + +### Docker 开启远程API + +* 用vi编辑器修改docker.service文件 +``` shell +vi /usr/lib/systemd/system/docker.service +#需要修改的部分: +ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock +#修改后的部分: +ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock +``` + +### Docker 常用命令 + +``` shell +systemctl start docker #启动docker +systemctl enable docker #将docker服务设为开机启动 +systemctl stop docker #停止容器 +systemctl restart docker #重启docker服务 +docker images # 列出镜像 +docker rmi --name # 删除镜像 -f 强制删除 +docker ps # 列出容器 -a 所有 +docker start --name # 启动容器 +docker stop --name # 停止容器 +docker restart --name # 重启docker容器 +docker rm --name # 删除容器 -f 强制删除 +docker stats -a # 查看所有容器情况 +docker system df # 查看Docker磁盘使用情况 +docker exec -it --name /bin/bash #进入Docker容器内部的bash +docker cp 主机文件 容器名称:容器路径 #复制文件到docker容器中 +docker logs --name #查看docker镜像日志 +docker rm $(docker ps -a -q) # 删除所有容器 -f 强制删除 +docker rmi $(docker images -a -q) # 删除所有镜像 -f 强制删除 +docker rm -f `docker ps -a | grep -vE 'mysql|nginx|redis|jenkins' | awk '{print $1}'` # 删除mysql|nginx|redis|jenkins非容器 -f 强制删除 +docker rmi -f `docker images | grep none | awk '{print $3}'` # 删除镜像none镜像 -f 强制删除 +``` + +## docker-compose + +### 安装 docker-compose +```shell +# 下载Docker Compose +curl -L https://get.daocloud.io/docker/compose/releases/download/1.24.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose +# 修改该文件的权限为可执行 +chmod +x /usr/local/bin/docker-compose +# 查看是否已经安装成功 +docker-compose --version +``` +### 使用Docker Compose的步骤 +* 使用Dockerfile定义应用程序环境,一般需要修改初始镜像行为时才需要使用; +* 使用docker-compose.yml定义需要部署的应用程序服务,以便执行脚本一次性部署; +* 使用docker-compose up命令将所有应用服务一次性部署起来。 + +### docker-compose.yml常用命令 +``` shell +# 指定运行的镜像名称 +image: name:version +# 配置容器名称 +container_name: name +# 指定宿主机和容器的端口映射 +ports: + - 3306:3306 +# 将宿主机的文件或目录挂载到容器中 +volumes: + - /etc/localtime:/etc/localtime + - /data/mysql/log:/var/log/mysql + - /data/mysql/data:/var/lib/mysql + - /data/mysql/conf:/etc/mysql + - /data/mysql/mysql-files:/var/lib/mysql-files +# 配置环境变量 +environment: + - MYSQL_ROOT_PASSWORD=xiujingmysql. +# 连接其他容器的服务 +links: + - db:database #可以以database为域名访问服务名称为db的容器 +# 挂断自动重新启动 +restart: always +# 指定容器执行命令 +command: redis-server --requirepass xiujingredis. +``` + +### Docker Compose常用命令 + +```shell +# 构建、创建、启动相关容器 +docker-compose up -d # -d表示在后台运行 +# 停止所有相关容器 +docker-compose stop +# 删除容器文件 +docker-compose rm -f # -f 强制删除 +# 重启容器 +docker-compose restart +# 列出所有容器信息 +docker-compose ps +# 查看容器日志 +docker-compose logs +``` + +### 使用Docker Compose 部署应用 + +编写docker-compose.yml文件 +``` yml +version: '3' +services: + # 指定服务名称 + nginx: + # 指定服务使用的镜像 + image: nginx + # 指定容器名称 + container_name: nginx + # 指定服务运行的端口 + ports: + - 80:80 + - 443:443 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + # 挂断自动重新启动 + restart: always + # 指定容器的环境变量 + environment: + - TZ=Asia/Shanghai # 设置容器时区与宿主机保持一致 + # 指定服务名称 + sqlserver: + # 指定服务使用的镜像 + image: mcr.microsoft.com/mssql/server + # 指定容器名称 + container_name: sqlserver + # 指定服务运行的端口 + ports: + - "1433" + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + - /data/sqlserver:/var/opt/mssql + # 挂断自动重新启动 + restart: always + environment: + - TZ=Asia/Shanghai + - SA_PASSWORD=mssql-MSSQL + - ACCEPT_EULA=Y + # 指定容器运行的用户为root + user: + root + # 指定服务名称 + mysql: + # 指定服务使用的镜像 + image: mysql + # 指定容器名称 + container_name: mysql + # 指定服务运行的端口 + ports: + - 3306:3306 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + - /data/mysql/log:/var/log/mysql + - /data/mysql/data:/var/lib/mysql + - /data/mysql/mysql-files:/var/lib/mysql-files + - /data/mysql/conf:/etc/mysql + # 挂断自动重新启动 + restart: always + # 指定容器的环境变量 + environment: + - TZ=Asia/Shanghai # 设置容器时区与宿主机保持一致 + - MYSQL_ROOT_PASSWORD=xiujingmysql. # 设置root密码 + # 指定容器运行的用户为root + user: + root + # 指定服务名称 + redis: + # 指定服务使用的镜像 + image: redis + # 指定容器名称 + container_name: redis + # 指定服务运行的端口 + ports: + - 6379:6379 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + - /data/redis:/data + - /data/redis/redis.conf:/etc/redis.conf + # 挂断自动重新启动 + restart: always + # 指定容器执行命令 + command: redis-server /etc/redis.conf --requirepass xiujingredis. --appendonly yes + # 指定容器的环境变量 + environment: + - TZ=Asia/Shanghai # 设置容器时区与宿主机保持一致 + # 指定服务名称 + mongo: + # 指定服务使用的镜像 + image: mongo + # 指定容器名称 + container_name: mongo + # 指定服务运行的端口 + ports: + - 27017:27017 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + - /data/mongodb/db:/data/db + - /data/mongodb/configdb:/data/configdb + - /data/mongodb/initdb:/docker-entrypoint-initdb.d + # 挂断自动重新启动 + restart: always + # 指定容器的环境变量 + environment: + - TZ=Asia/Shanghai # 设置容器时区与宿主机保持一致 + - AUTH=yes + - MONGO_INITDB_ROOT_USERNAME=admin + - MONGO_INITDB_ROOT_PASSWORD=admin + # 指定服务名称 + jenkins: + # 指定服务使用的镜像 + image: jenkins + # 指定容器名称 + container_name: jenkins + # 指定服务运行的端口 + ports: + - 8080:8080 + - 50000:50000 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + - /data/jenkins_home:/var/jenkins_home + # 挂断自动重新启动 + restart: always + # 指定容器的环境变量 + environment: + - TZ=Asia/Shanghai # 设置容器时区与宿主机保持一致 + - JAVA_OPTS=-Duser.timezone=Asia/Shanghai + # 指定容器运行的用户为root + user: + root + # 指定服务名称 + minio: + # 指定服务使用的镜像 + image: minio + # 指定容器名称 + container_name: minio + # 指定服务运行的端口 + ports: + - 9000:9000 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + - /data/minio/data:/data + - /data/minio/config:/root/.minio + # 挂断自动重新启动 + restart: always + # 指定容器执行命令 + command: server /data + # 指定服务名称 + portainer : + # 指定服务使用的镜像 + image: portainer + # 指定容器名称 + container_name: portainer + # 指定服务运行的端口 + ports: + - 8001:8000 + - 9001:9000 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + - /var/run/docker.sock:/var/run/docker.sock + - /data/portainer/data:/data + # 挂断自动重新启动 + restart: always +``` +运行Docker Compose命令启动所有服务 +``` shell +docker-compose up -d +``` diff --git a/blog/en-us/docker/docker-jenkins.md b/blog/en-us/docker/docker-jenkins.md new file mode 100644 index 0000000..44525d3 --- /dev/null +++ b/blog/en-us/docker/docker-jenkins.md @@ -0,0 +1,92 @@ +# Docker部署Jenkins + +## Jenkins简介 +Jenkins是开源CI&CD软件领导者,提供超过1000个插件来支持构建、部署、自动化,满足任何项目的需要。我们可以用Jenkins来构建和部署我们的项目,比如说从我们的代码仓库获取代码,然后将我们的代码打包成可执行的文件,之后通过远程的ssh工具执行脚本来运行我们的项目。 + +## Jenkins的安装及配置 + +### Docker环境下的安装 + +* 下载Jenkins的Docker镜像: +```shell +docker pull jenkins/jenkins:lts +``` +* 在Docker容器中运行Jenkins: +``` shell +docker run -p 8080:8080 -p 50000:50000 --name jenkins \ +-u root \ +-v /etc/localtime:/etc/localtime \ +-v /data/jenkins_home:/var/jenkins_home \ +-e JAVA_OPTS=-Duser.timezone=Asia/Shanghai \ +-d jenkins/jenkins:lts +``` +* echo 'Asia/Shanghai' >/etc/timezone + +### Jenkins的配置 +* 运行成功后访问该地址登录Jenkins,第一次登录需要输入管理员密码:http://localhost:8080/ + +![jenkins](./../img/jenkins-1.png) + +* 使用管理员密码进行登录,可以使用以下命令从容器启动日志中获取管理密码: + +``` shell +docker logs jenkins +``` + +![jenkins](./../img/jenkins-2.png) + +* 选择安装插件方式,这里我们直接安装推荐的插件: + +![jenkins](./../img/jenkins-3.png) + +* 进入插件安装界面,联网等待插件安装: + +![jenkins](./../img/jenkins-4.png) + +* 安装完成后,创建管理员账号: + +![jenkins](./../img/jenkins-5.png) + +* 进行实例配置,配置Jenkins的URL: + +![jenkins](./../img/jenkins-6.png) + +* 点击系统管理->插件管理,进行一些自定义的插件安装: + +![jenkins](./../img/jenkins-7.png) + +* 确保以下插件被正确安装: + 1. 根据角色管理权限的插件:Role-based Authorization Strategy + 2. 远程使用ssh的插件:SSH plugin + +* 通过系统管理->全局工具配置来进行全局工具的配置,比如maven的配置: + +![jenkins](./../img/jenkins-8.png) + +* 新增maven的安装配置: + +![jenkins](./../img/jenkins-9.png) + +* 在系统管理->系统配置中添加全局ssh的配置,这样Jenkins使用ssh就可以执行远程的linux脚本了: + +![jenkins](./../img/jenkins-10.png) + +## 角色权限管理 + +我们可以使用Jenkins的角色管理插件来管理Jenkins的用户,比如我们可以给管理员赋予所有权限,运维人员赋予执行任务的相关权限,其他人员只赋予查看权限。 + +* 在系统管理->全局安全配置中启用基于角色的权限管理: + +![jenkins](./../img/jenkins-11.png) + +* 进入系统管理->Manage and Assign Roles界面: + +![jenkins](./../img/jenkins-12.png) + +* 添加角色与权限的关系: + +![jenkins](./../img/jenkins-13.png) + +* 给用户分配角色: + +![jenkins](./../img/jenkins-14.png) diff --git a/blog/en-us/exp/cto.md b/blog/en-us/exp/cto.md new file mode 100644 index 0000000..6b3b2b4 --- /dev/null +++ b/blog/en-us/exp/cto.md @@ -0,0 +1,140 @@ +# CTO 技能图谱 + +### 岗位职责 +* 建立技术团队文化 +* 规划技术发展路线 +* 落地产品研发成果 +* 宣传公司技术品牌 +* 吸引优秀技术人才 + +### 基本素质 +* 正直诚实的道德修养 +* 谦虚谨慎的工作态度 +* 随机应变的处事风格 +* 统领全局的战略思维 + +### 硬技能 + +#### 技术能力 +* 具备一定的技术深度 +* 具备较强的技术广度 +* 追求技术选型合理性 +* 对技术发展嗅觉敏锐 + +#### 业务能力 +* 能深度理解业务本质 +* 能用技术来帮助业务 +* 让技术驱动业务发展 + +#### 架构能力 +* 能站在业务上设计架构 +* 架构规范合理且易落地 +* 能为架构设计提出意见 + +#### 产品能力 +* 具备一定的产品价值观 +* 能准确地抓住用户刚需 +* 能为产品设计提出意见 + +#### 管理能力 +- 团队管理 + * 招人:知道如何吸引所需的人? + * 识人:知道如何识别已有队员? + * 育人:知道如何让队员们成长? + * 开人:知道如何请人愉快离开? + * 留人:知道如何留住该留的人? + * 挖人:知道如何挖到需要的人? +- 项目管理 + * 估算:知道项目成本如何估算? + * 分工:知道工作任务如何分配? + * 排期:知道项目计划如何制定? + * 实施:知道项目实施如何开展? + * 发布:知道项目发布如何执行? + * 回顾:知道项目回顾如何进行? +- 绩效管理 + * 制定:知道如何制定考核标准? + * 执行:知道如何执行绩效考核? + * 优化:知道如何提升团队绩效? +- 时间管理 + * 制定:知道如何制定团队计划? + * 管理:知道如何管理团队任务? + * 权衡:知道如何权衡优先级别? +- 情绪管理 + * 控制:知道如何控制自我情绪? + * 转化:知道如何化悲观为乐观? + * 使用:知道如何善用自我情绪? + +### 软技能 + +#### 领导能力 +- 决策能力 + * 不要害怕做决定 + * 做出正确的决定 + * 敢于为决定负责 +- 影响能力 + * 不要改变别人而是激发别人 + * 用自己的行为和态度去激发 + * 持续不断提高自己的影响力 +- 沟通能力 + + 向上沟通(与公司创始人) + * 领会老板真实意图 + * 站在老板角度思考 + * 不要强迫改变老板 + + 向下沟通(与本部门同事) + * 是沟通而不是命令 + * 站在下属立场沟通 + * 不要吝啬夸赞队员 +- 横向沟通(与跨部门同事) + * 突出对方的重要性 + * 先要共识才能共赢 + * 懂得圆满大于完美 + +#### 执行能力 +* 理解执行目标 +* 提高执行效率 +* 确保执行效果 + +#### 学习能力 +* 对新知识充满好奇心 +* 能够快速学习新技能 +* 拥有触类旁通的能力 + +#### 组织能力 +* 积极主动并有活力 +* 做事情敢于放得开 +* 能够调用所需资源 + +#### 洞察能力 +* 善于抓住事物本质 +* 善于观察人性心理 +* 善于预见未来变化 + +#### 抗压能力 +* 学会释放压力 +* 化压力为动力 +* 化消极为积极 + +#### 自省能力 +* 能够不断自我反省 +* 能够从失败中总结 +* 能够在总结中提高 + +#### 战略能力 +* 能够深刻理解公司战略 +* 能够站在更高层次思考 +* 结合战略形成技术壁垒 + +#### 社交能力 +* 善于表达自己 +* 善于结交朋友 +* 善于公众演讲 + +#### 谈判能力 +* 能够通过谈判得到对方认同 +* 能够从谈判中找到共赢方式 +* 能够在谈判场合中保持镇定 + +#### 政治能力 +* 能够对政治持有敏感度 +* 能够处理办公室小政治 +* 能够主动避免政治风险 diff --git a/blog/en-us/img/Javavm11.png b/blog/en-us/img/Javavm11.png new file mode 100644 index 0000000..2e325fb Binary files /dev/null and b/blog/en-us/img/Javavm11.png differ diff --git a/blog/en-us/img/c_sqlserver_nginx1.png b/blog/en-us/img/c_sqlserver_nginx1.png new file mode 100644 index 0000000..848396c Binary files /dev/null and b/blog/en-us/img/c_sqlserver_nginx1.png differ diff --git a/blog/en-us/img/c_sqlserver_nginx2.png b/blog/en-us/img/c_sqlserver_nginx2.png new file mode 100644 index 0000000..cb68b10 Binary files /dev/null and b/blog/en-us/img/c_sqlserver_nginx2.png differ diff --git a/blog/en-us/img/c_sqlserver_nginx4.png b/blog/en-us/img/c_sqlserver_nginx4.png new file mode 100644 index 0000000..3ecb310 Binary files /dev/null and b/blog/en-us/img/c_sqlserver_nginx4.png differ diff --git a/blog/en-us/img/c_sqlserver_nginx5.png b/blog/en-us/img/c_sqlserver_nginx5.png new file mode 100644 index 0000000..a9e0e2f Binary files /dev/null and b/blog/en-us/img/c_sqlserver_nginx5.png differ diff --git a/blog/en-us/img/c_sqlserver_nginx6.png b/blog/en-us/img/c_sqlserver_nginx6.png new file mode 100644 index 0000000..97f0a5d Binary files /dev/null and b/blog/en-us/img/c_sqlserver_nginx6.png differ diff --git a/blog/en-us/img/c_sqlserver_nginx7.png b/blog/en-us/img/c_sqlserver_nginx7.png new file mode 100644 index 0000000..ce5f281 Binary files /dev/null and b/blog/en-us/img/c_sqlserver_nginx7.png differ diff --git a/blog/en-us/img/c_sqlserver_nginx8.png b/blog/en-us/img/c_sqlserver_nginx8.png new file mode 100644 index 0000000..76d2932 Binary files /dev/null and b/blog/en-us/img/c_sqlserver_nginx8.png differ diff --git a/blog/en-us/img/favicon.jpg b/blog/en-us/img/favicon.jpg new file mode 100644 index 0000000..4d647bb Binary files /dev/null and b/blog/en-us/img/favicon.jpg differ diff --git a/blog/en-us/img/javavm1.png b/blog/en-us/img/javavm1.png new file mode 100644 index 0000000..c380974 Binary files /dev/null and b/blog/en-us/img/javavm1.png differ diff --git a/blog/en-us/img/javavm10.png b/blog/en-us/img/javavm10.png new file mode 100644 index 0000000..cfb1607 Binary files /dev/null and b/blog/en-us/img/javavm10.png differ diff --git a/blog/en-us/img/javavm12.png b/blog/en-us/img/javavm12.png new file mode 100644 index 0000000..cfb1607 Binary files /dev/null and b/blog/en-us/img/javavm12.png differ diff --git a/blog/en-us/img/javavm13.png b/blog/en-us/img/javavm13.png new file mode 100644 index 0000000..385784b Binary files /dev/null and b/blog/en-us/img/javavm13.png differ diff --git a/blog/en-us/img/javavm14.png b/blog/en-us/img/javavm14.png new file mode 100644 index 0000000..be377e3 Binary files /dev/null and b/blog/en-us/img/javavm14.png differ diff --git a/blog/en-us/img/javavm15.png b/blog/en-us/img/javavm15.png new file mode 100644 index 0000000..f4050ff Binary files /dev/null and b/blog/en-us/img/javavm15.png differ diff --git a/blog/en-us/img/javavm16.png b/blog/en-us/img/javavm16.png new file mode 100644 index 0000000..137c4c6 Binary files /dev/null and b/blog/en-us/img/javavm16.png differ diff --git a/blog/en-us/img/javavm17.png b/blog/en-us/img/javavm17.png new file mode 100644 index 0000000..b326f1e Binary files /dev/null and b/blog/en-us/img/javavm17.png differ diff --git a/blog/en-us/img/javavm18.png b/blog/en-us/img/javavm18.png new file mode 100644 index 0000000..9664d11 Binary files /dev/null and b/blog/en-us/img/javavm18.png differ diff --git a/blog/en-us/img/javavm19.png b/blog/en-us/img/javavm19.png new file mode 100644 index 0000000..811edfe Binary files /dev/null and b/blog/en-us/img/javavm19.png differ diff --git a/blog/en-us/img/javavm2.png b/blog/en-us/img/javavm2.png new file mode 100644 index 0000000..b1934d6 Binary files /dev/null and b/blog/en-us/img/javavm2.png differ diff --git a/blog/en-us/img/javavm20.png b/blog/en-us/img/javavm20.png new file mode 100644 index 0000000..486d332 Binary files /dev/null and b/blog/en-us/img/javavm20.png differ diff --git a/blog/en-us/img/javavm3.png b/blog/en-us/img/javavm3.png new file mode 100644 index 0000000..290a8a9 Binary files /dev/null and b/blog/en-us/img/javavm3.png differ diff --git a/blog/en-us/img/javavm4.png b/blog/en-us/img/javavm4.png new file mode 100644 index 0000000..5393daa Binary files /dev/null and b/blog/en-us/img/javavm4.png differ diff --git a/blog/en-us/img/javavm5.png b/blog/en-us/img/javavm5.png new file mode 100644 index 0000000..45f3748 Binary files /dev/null and b/blog/en-us/img/javavm5.png differ diff --git a/blog/en-us/img/javavm6.png b/blog/en-us/img/javavm6.png new file mode 100644 index 0000000..a8c0dac Binary files /dev/null and b/blog/en-us/img/javavm6.png differ diff --git a/blog/en-us/img/javavm7.png b/blog/en-us/img/javavm7.png new file mode 100644 index 0000000..dfd2d26 Binary files /dev/null and b/blog/en-us/img/javavm7.png differ diff --git a/blog/en-us/img/javavm8.png b/blog/en-us/img/javavm8.png new file mode 100644 index 0000000..4d18652 Binary files /dev/null and b/blog/en-us/img/javavm8.png differ diff --git a/blog/en-us/img/javavm9.png b/blog/en-us/img/javavm9.png new file mode 100644 index 0000000..d463018 Binary files /dev/null and b/blog/en-us/img/javavm9.png differ diff --git a/blog/en-us/img/jenkins-1.png b/blog/en-us/img/jenkins-1.png new file mode 100644 index 0000000..3d8794b Binary files /dev/null and b/blog/en-us/img/jenkins-1.png differ diff --git a/blog/en-us/img/jenkins-10.png b/blog/en-us/img/jenkins-10.png new file mode 100644 index 0000000..5812612 Binary files /dev/null and b/blog/en-us/img/jenkins-10.png differ diff --git a/blog/en-us/img/jenkins-11.png b/blog/en-us/img/jenkins-11.png new file mode 100644 index 0000000..f5bbe24 Binary files /dev/null and b/blog/en-us/img/jenkins-11.png differ diff --git a/blog/en-us/img/jenkins-12.png b/blog/en-us/img/jenkins-12.png new file mode 100644 index 0000000..4e66687 Binary files /dev/null and b/blog/en-us/img/jenkins-12.png differ diff --git a/blog/en-us/img/jenkins-13.png b/blog/en-us/img/jenkins-13.png new file mode 100644 index 0000000..00bcf53 Binary files /dev/null and b/blog/en-us/img/jenkins-13.png differ diff --git a/blog/en-us/img/jenkins-14.png b/blog/en-us/img/jenkins-14.png new file mode 100644 index 0000000..765aeeb Binary files /dev/null and b/blog/en-us/img/jenkins-14.png differ diff --git a/blog/en-us/img/jenkins-2.png b/blog/en-us/img/jenkins-2.png new file mode 100644 index 0000000..afb3d50 Binary files /dev/null and b/blog/en-us/img/jenkins-2.png differ diff --git a/blog/en-us/img/jenkins-3.png b/blog/en-us/img/jenkins-3.png new file mode 100644 index 0000000..ed75c2c Binary files /dev/null and b/blog/en-us/img/jenkins-3.png differ diff --git a/blog/en-us/img/jenkins-4.png b/blog/en-us/img/jenkins-4.png new file mode 100644 index 0000000..8dbe6a5 Binary files /dev/null and b/blog/en-us/img/jenkins-4.png differ diff --git a/blog/en-us/img/jenkins-5.png b/blog/en-us/img/jenkins-5.png new file mode 100644 index 0000000..a809092 Binary files /dev/null and b/blog/en-us/img/jenkins-5.png differ diff --git a/blog/en-us/img/jenkins-6.png b/blog/en-us/img/jenkins-6.png new file mode 100644 index 0000000..0035c90 Binary files /dev/null and b/blog/en-us/img/jenkins-6.png differ diff --git a/blog/en-us/img/jenkins-7.png b/blog/en-us/img/jenkins-7.png new file mode 100644 index 0000000..a98c173 Binary files /dev/null and b/blog/en-us/img/jenkins-7.png differ diff --git a/blog/en-us/img/jenkins-8.png b/blog/en-us/img/jenkins-8.png new file mode 100644 index 0000000..27c573c Binary files /dev/null and b/blog/en-us/img/jenkins-8.png differ diff --git a/blog/en-us/img/jenkins-9.png b/blog/en-us/img/jenkins-9.png new file mode 100644 index 0000000..0e17a19 Binary files /dev/null and b/blog/en-us/img/jenkins-9.png differ diff --git a/blog/en-us/img/minio1.png b/blog/en-us/img/minio1.png new file mode 100644 index 0000000..69fbf38 Binary files /dev/null and b/blog/en-us/img/minio1.png differ diff --git a/blog/en-us/img/minio2.png b/blog/en-us/img/minio2.png new file mode 100644 index 0000000..d65f693 Binary files /dev/null and b/blog/en-us/img/minio2.png differ diff --git a/blog/en-us/img/minio3.png b/blog/en-us/img/minio3.png new file mode 100644 index 0000000..fa9432e Binary files /dev/null and b/blog/en-us/img/minio3.png differ diff --git a/blog/en-us/img/minio4.png b/blog/en-us/img/minio4.png new file mode 100644 index 0000000..518e71f Binary files /dev/null and b/blog/en-us/img/minio4.png differ diff --git a/blog/en-us/img/minio5.png b/blog/en-us/img/minio5.png new file mode 100644 index 0000000..19eed92 Binary files /dev/null and b/blog/en-us/img/minio5.png differ diff --git a/blog/en-us/img/minio6.png b/blog/en-us/img/minio6.png new file mode 100644 index 0000000..eae1ef3 Binary files /dev/null and b/blog/en-us/img/minio6.png differ diff --git a/blog/en-us/img/minio7.png b/blog/en-us/img/minio7.png new file mode 100644 index 0000000..d121ca6 Binary files /dev/null and b/blog/en-us/img/minio7.png differ diff --git a/blog/en-us/img/react-top.png b/blog/en-us/img/react-top.png new file mode 100644 index 0000000..3d9cf13 Binary files /dev/null and b/blog/en-us/img/react-top.png differ diff --git a/blog/en-us/img/ui-dom-1.png b/blog/en-us/img/ui-dom-1.png new file mode 100644 index 0000000..e95f74a Binary files /dev/null and b/blog/en-us/img/ui-dom-1.png differ diff --git a/blog/en-us/img/ui-dom-2.png b/blog/en-us/img/ui-dom-2.png new file mode 100644 index 0000000..194d5d6 Binary files /dev/null and b/blog/en-us/img/ui-dom-2.png differ diff --git a/blog/en-us/img/ui-dom-3.png b/blog/en-us/img/ui-dom-3.png new file mode 100644 index 0000000..4a02e9f Binary files /dev/null and b/blog/en-us/img/ui-dom-3.png differ diff --git a/blog/en-us/img/ui-dom-4.png b/blog/en-us/img/ui-dom-4.png new file mode 100644 index 0000000..0064f11 Binary files /dev/null and b/blog/en-us/img/ui-dom-4.png differ diff --git a/blog/en-us/img/ui-dom-5.png b/blog/en-us/img/ui-dom-5.png new file mode 100644 index 0000000..cee65ef Binary files /dev/null and b/blog/en-us/img/ui-dom-5.png differ diff --git a/blog/en-us/img/vue-react-1.png b/blog/en-us/img/vue-react-1.png new file mode 100644 index 0000000..b8a8d14 Binary files /dev/null and b/blog/en-us/img/vue-react-1.png differ diff --git a/blog/en-us/img/vue-react-2.png b/blog/en-us/img/vue-react-2.png new file mode 100644 index 0000000..455368b Binary files /dev/null and b/blog/en-us/img/vue-react-2.png differ diff --git a/blog/en-us/img/vue-react-3.png b/blog/en-us/img/vue-react-3.png new file mode 100644 index 0000000..7bb5ccb Binary files /dev/null and b/blog/en-us/img/vue-react-3.png differ diff --git a/blog/en-us/img/vue-react-4.png b/blog/en-us/img/vue-react-4.png new file mode 100644 index 0000000..4710440 Binary files /dev/null and b/blog/en-us/img/vue-react-4.png differ diff --git a/blog/en-us/java/javavm.md b/blog/en-us/java/javavm.md new file mode 100644 index 0000000..926c794 --- /dev/null +++ b/blog/en-us/java/javavm.md @@ -0,0 +1,613 @@ +# Java 虚拟机 + + +## 一、基本概念 + +### 1.1 OpenJDK + +自 1996 年 `JDK 1.0` 发布以来,Sun 公司在大版本上发行了 `JDK 1.1`、`JDK 1.2`、`JDK 1.3`、`JDK 1.4`、`JDK 5`,`JDK 6` ,这些版本的 JDK 都可以统称为 SunJDK 。之后在 2006 年的 JavaOne 大会上,Sun 公司宣布将 Java 开源,在随后的一年多里,它陆续将 JDK 的各个部分在 GPL v2(GNU General Public License,version 2)协议下开源,并建立了 OpenJDK 组织来对这些代码进行独立的管理,这就是 OpenJDK 的来源,此时的 OpenJDK 拥有当时 sunJDK 7 的几乎全部代码。 + +### 1.2 OracleJDK + +在 JDK 7 的开发期间,由于各种原因的影响 Sun 公司市值一路下跌,已无力推进 JDK 7 的开发,JDK 7 的发布一直被推迟。之后在 2009 年 Sun 公司被 Oracle 公司所收购,为解决 JDK 7 长期跳票的问题,Oracle 将 JDK 7 中大部分未能完成的项目推迟到 JDK 8 ,并于 2011 年发布了JDK 7,在这之后由 Oracle 公司正常发行的 JDK 版本就由 SunJDK 改称为 Oracle JDK。 + +在 2017 年 JDK 9 发布后,Oracle 公司宣布从此以后 JDK 将会在每年的 3 月和 9 月各发布一个大版本,即半年发行一个大版本,目的是为了避免众多功能被捆绑到一个 JDK 版本上而引发的无法交付的风险。 + + 在 JDK 11 发布后,Oracle 同步调整了 JDK 的商业授权,宣布从 JDK 11 起将以前的商业特性全部开源给 OpenJDK ,这样 OpenJDK 11 和 OracleJDK 11 的代码和功能,在本质上就完全相同了。同时还宣布以后都会发行两个版本的 JDK : + ++ 一个是在 GPLv2 + CE 协议下由 Oracle 开源的 OpenJDK; ++ 一个是在 OTN 协议下正常发行的 OracleJDK。 + +两者共享大部分源码,在功能上几乎一致。唯一的区别是 Oracle OpenJDK 可以在开发、测试或者生产环境中使用,但只有半年的更新支持;而 OracleJDK 对个人免费,但在生产环境中商用收费,可以有三年时间的更新支持。 + +### 1.3 HotSpot VM + +它是 Sun/Oracle JDK 和 OpenJDK 中默认的虚拟机,也是目前使用最为广泛的虚拟机。最初由 Longview Technologies 公司设计发明,该公司在 1997 年被 Sun 公司收购,随后 Sun 公司在 2006 年开源 SunJDK 时也将 HotSpot 虚拟机一并进行了开源。之后 Oracle 收购 Sun 以后,建立了 HotRockit 项目,用于将其收购的另外一家公司(BEA)的 JRockit 虚拟机中的优秀特性集成到 HotSpot 中。HotSpot 在这个过程里面移除掉永久代,并吸收了 JRockit 的 Java Mission Control 监控工具等功能。到 JDK 8 发行时,采用的就是集两者之长的 HotSpot VM 。 + +我们可以在自己的电脑上使用 `java -version` 来获得 JDK 的信息: + +```shell +C:\Users> java -version +java version "1.8.0_171" # 如果是openJDK, 则这里会显示:openjdk version +Java(TM) SE Runtime Environment (build 1.8.0_171-b11) +Java HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode) # 使用的是HotSpot虚拟机,默认为服务端模式 +``` + + + +## 二、Java 内存区域 + +
+ + +### 2.1 程序计数器 + +程序计数器(Program Counter Register)是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。字节码解释器通过改变程序计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要该计数器来完成。每条线程都拥有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。 + +### 2.2 Java虚拟机栈 + +Java 虚拟机栈(Java Virtual Machine Stack)也为线程私有,它描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法出口等信息。方法从调用到结束就对应着一个栈帧从入栈到出栈的过程。在《Java 虚拟机规范》中,对该内存区域规定了两类异常: + ++ 如果线程请求的栈深度大于虚拟机所允许的栈深度,将抛出 `StackOverflowError` 异常; ++ 如果 Java 虚拟机栈的容量允许动态扩展,当栈扩展时如果无法申请到足够的内存会抛出 `OutOfMemoryError` 异常。 + +### 2.3 本地方法栈 + +本地方法栈(Native Method Stacks)与虚拟机栈类似,其区别在于:Java 虚拟机栈是为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。 + +### 2.4 Java堆 + +Java 堆(Java Heap)是虚拟机所管理的最大一块的内存空间,它被所有线程所共享,用于存放对象实例。Java 堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为是连续的。Java 堆可以被实现成固定大小的,也可以是可扩展的,当前大多数主流的虚拟机都是按照可扩展来实现的,即可以通过最大值参数 `-Xmx` 和最小值参数 `-Xms` 进行设定。如果 Java 堆中没有足够的内存来完成实例分配,并且堆也无法再扩展时,Java 虚拟机将会抛出 `OutOfMemoryError` 异常。 + +### 2.5 方法区 + +方法区(Method Area)也是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。方法区也被称为 “非堆”,目的是与 Java 堆进行区分。《Java 虚拟机规范》规定,如果方法区无法满足新的内存分配需求时,将会抛出 `OutOfMemoryError` 异常。 + +运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存放常量池表(Constant Pool Table),常量池表中存放了编译期生成的各种符号字面量和符号引用。 + +## 三、对象 + +### 3.1 对象的创建 + +当我们在代码中使用 `new` 关键字创建一个对象时,其在虚拟机中需要经过以下步骤: + +**1. 类加载过程** + +当虚拟机遇到一条字节码 `new` 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就必须先执行相应的类加载过程。 + +**2. 分配内存** + +在类加载检查通过后,虚拟机需要新生对象分配内存空间。根据 Java 堆是否规整,可以有以下两种分配方案: + ++ **指针碰撞**:假设 Java 堆中内存是绝对规整的,所有使用的内存放在一边,所有未被使用的内存放在另外一边,中间以指针作为分界点指示器。此时内存分配只是将指针向空闲方向偏移出对象大小的空间即可,这种方式被称为指针碰撞。 + +
+ + ++ **空闲列表**:如果 Java 堆不是规整的,此时虚拟机需要维护一个列表,记录哪些内存块是可用的,哪些是不可用的。在进行内存分配时,只需要从该列表中选取出一块足够的内存空间划分给对象实例即可。 + +> 注:Java 堆是否规整取决于其采用的垃圾收集器是否带有空间压缩整理能力,后文将会介绍。 + +除了分配方式外,由于对象创建在虚拟机中是一个非常频繁的行为,此时需要保证在并发环境下的线程安全:如果一个线程给对象 A 分配了内存空间,但指针还没来得及修改,此时就可能出现另外一个线程使用原来的指针来给对象 B 分配内存空间的情况。想要解决这个问题有两个方案: + ++ **方式一**:采用同步锁定,或采用 CAS 配上失败重试的方式来保证更新操作的原子性。 ++ **方式二**:为每个线程在 Java 堆中预先分配一块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。线程在进行内存分配时优先使用本地缓冲,当本地缓冲使用完成后,再向 Java 堆申请分配,此时 Java 堆采用同步锁定的方式来保证分配行为的线程安全。 + +**3. 对象头设置** + +将对象有关的元数据信息、对象的哈希码、分代年龄等信息存储到对象头中。 + +**4. 对象初始化** + +调用对象的构造函数,即 Class 文件中的 `()` 来初始化对象,为相关字段赋值。 + +### 3.2 对象的内存布局 + +在 HotSpot 虚拟机中,对象在堆内存中的存储布局可以划分为以下三个部分: + +**1. 对象头 (Header)** + +对象头包括两部分信息: + ++ **Mark Word**:对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,官方统称为 Mark Word 。 ++ **类型指针**:对象指向它类型元数据的指针,Java 虚拟机通过这个指针来确定该对象是哪个类的示例。需要说明的是并非所有的虚拟机都必须要在对象数据上保留类型指针,这取决于对象的访问定位方式(详见下文)。 + +**2. 实例数据 (Instance Data)** + +即我们在程序代码中定义的各种类型的字段的内容,无论是从父类继承而来,还是子类中定义的都需要记录。 + +**3. 对其填充 (Padding)** + +主要起占位符的作用。HotSpot 虚拟机要求对象起始地址必须是 8 字节的整倍数,即间接要求了任何对象的大小都必须是 8 字节的整倍数。对象头部分在设计上就是 8 字节的整倍数,如果对象的实例数据不是 8 字节的整倍数,则由对齐填充进行补全。 + +### 3.3 对象的访问定位 + +对象创建后,Java 程序就可以通过栈上的 `reference` 来操作堆上的具体对象。《Java 虚拟机规范》规定 `reference` 是一个指向对象的引用,但并未规定其具体实现方式。主流的方式方式有以下两种: + ++ **句柄访问**:Java 堆将划分出一块内存来作为句柄池, `reference` 中存储的是对象的句柄地址,而句柄则包含了对象实例数据和类型数据的地址信息。 ++ **指针访问**:`reference` 中存储的直接就是对象地址,而对象的类型数据则由上文介绍的对象头中的类型指针来指定。 + +通过句柄访问对象: + +
+ + +通过直接指针访问对象: + +
+ + +句柄访问的优点在于对象移动时(垃圾收集时移动对象是非常普遍的行为)只需要改变句柄中实例数据的指针,而 `reference` 本生并不需要修改;指针访问则反之,由于其 `reference` 中存储的直接就是对象地址,所以当对象移动时, `reference` 需要被修改。但针对只需要访问对象本身的场景,指针访问则可以减少一次定位开销。由于对象访问是一项非常频繁的操作,所以这类减少的效果会非常显著,基于这个原因,HotSpot 主要使用的是指针访问的方式。 + +## 四、垃圾收集算法 + +在 Java 虚拟机内存模型中,程序计数器、虚拟机栈、本地方法栈这 3 个区域都是线程私有的,会随着线程的结束而销毁,因此在这 3 个区域当中,无需过多考虑垃圾回收问题。垃圾回收问题主要发生在 Java 堆和方法区上。 + +### 4.1 Java 堆回收 + +在 Java 堆上,垃圾回收的主要内容是死亡对象(不可能再被任何途径使用的对象)。而判断对象是否死亡有以下两种方法: + +#### 1. 引用计数法 + +在对象中添加一个引用计数器,对象每次被引用时,该计数器加一;当引用失效时,计数器的值减一;只要计数器的值为零,则代表对应的对象不可能再被使用。该方法的缺点在于无法避免相互循环引用的问题: + +```java +objA.instance = objB +objB.instance = objA +objA = null; +objB = null; +System.gc(); +``` + +如上所示,此时两个对象已经不能再被访问,但其互相持有对对方的引用,如果采用引用计数法,则两个对象都无法被回收。 + +#### 2. 可达性分析 + +上面的代码在大多数虚拟机中都能被正确的回收,因为大多数主流的虚拟机都是采用的可达性分析方法来判断对象是否死亡。可达性分析是通过一系列被称为 `GC Roots` 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径被称为引用链(Reference Chain),如果某个对象到 `GC Roots` 间没有任何引用链相连,这代表 `GC Roots` 到该对象不可达, 此时证明此该对象不可能再被使用。 + +
+ + +在 Java 语言中,固定可作为 `GC Roots` 的对象包括以下几种: + ++ 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等; ++ 在方法区中类静态属性引用的对象,譬如 Java 类中引用类型的静态变量; ++ 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用; ++ 在本地方法栈中的 JNI(即 Native 方法)引用的对象; ++ Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象(如 NullPointException,OutOfMemoryError 等)及系统类加载器; ++ 所有被同步锁(synchronized 关键字)持有的对象; ++ 反应 Java 虚拟机内部情况的 JMXBean,JVMTI 中注册的回调,本地代码缓存等。 + +除了这些固定的 `GC Roots` 集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域的不同,还可能会有其他对象 “临时性” 地加入,共同构成完整的 `GC Roots` 集合。 + +#### 3. 对象引用 + +可达性分析是基于引用链进行判断的,在 JDK 1.2 之后,Java 将引用关系分为以下四类: + ++ **强引用 (Strongly Reference)** :最传统的引用,如 `Object obj = new Object()` 。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。 ++ **软引用 (Soft Reference)** :用于描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常之前,会被列入回收范围内进行第二次回收,如果这次回收后还没有足够的内存,才会抛出内存溢出异常。 ++ **弱引用 (Weak Reference)** :用于描述那些非必须的对象,强度比软引用弱。被弱引用关联对象只能生存到下一次垃圾收集发生时,无论当前内存是否足够,弱引用对象都会被回收。 ++ **虚引用 (Phantom Reference)** :最弱的引用关系。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被回收时收到一个系统通知。 + +#### 4. 对象真正死亡 + +要真正宣告一个对象死亡,需要经过至少两次标记过程: + ++ 如果对象在进行可达性分析后发现 `GC Roots` 不可达,将会进行第一次标记; ++ 随后进行一次筛选,筛选的条件是此对象是否有必要执行 `finalized()` 方法。如果对象没有覆盖 `finalized()` 方法,或者 `finalized()` 已经被虚拟机调用过,这两种情况都会视为没有必要执行。如果判定结果是有必要执行,此时对象会被放入名为 `F-Queue` 的队列,等待 Finalizer 线程执行其 `finalized()` 方法。在这个过程中,收集器会进行第二次小规模的标记,如果对象在 `finalized()` 方法中重新将自己与引用链上的任何一个对象进行了关联,如将自己(this 关键字)赋值给某个类变量或者对象的成员变量,此时它就实现了自我拯救,则第二次标记会将其移除 “即将回收” 的集合,否则该对象就将被真正回收,走向死亡。 + +### 4.2 方法区回收 + +在 Java 堆上进行对象回收的性价比通常比较高,因为大多数对象都是朝生夕灭的。而方法区由于回收条件比较苛刻,对应的回收性价比通常比较低,主要回收两部分内容:废弃的常量和不再使用的类型。 + +### 4.3 垃圾收集算法 + +#### 1. 分代收集理论 + +当前大多数虚拟机都遵循 “分代收集” 的理论进行设计,它建立在强弱两个分代假说下: + ++ **弱分代假说 (Weak Generational Hypothesis)**:绝大多数对象都是朝生夕灭的。 ++ **强分代假说 (Strong Generational Hypothesis)**:熬过越多次垃圾收集过程的对象就越难以消亡。 ++ **跨带引用假说 (Intergenerational Reference Hypothesis)**:基于上面两条假说还可以得出的一条隐含推论:存在相互引用关系的两个对象,应该倾向于同时生存或者同时消亡。 + +强弱分代假说奠定了垃圾收集器的设计原则:收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据其年龄(年龄就是对象经历垃圾收集的次数)分配到不同的区域中进行存储。之后如果一个区域中的对象都是朝生夕灭的,那么收集器只需要关注少量对象的存活而不是去标记那些大量将要被回收的对象,此时就能以较小的代价获取较大的空间。最后再将难以消亡的对象集中到一块,根据强分代假说,它们是很难消亡的,因此虚拟机可以使用较低的频率进行回收,这就兼顾了时间和内存空间的开销。 + +#### 2. 回收类型 + +根据分代收集理论,收集范围可以分为以下几种类型: + ++ **部分收集 (Partial GC)**:具体分为: + + 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集; + + 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集; + + 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。 ++ **整堆收集 (Full GC)**:收集整个 Java 堆和方法区。 + +#### 3. 标记-清除算法 + +它是最基础的垃圾收集算法,收集过程分为两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象;也可以反过来,标记存活对象,统一回收所有未被标记的对象。 + +
+ + +它主要有以下两个缺点: + ++ 执行效率不稳定:如果 Java 堆上包含大量需要回收的对象,则需要进行大量标记和清除动作; ++ 内存空间碎片化:标记清除后会产生大量不连续的空间,从而可能导致无法为大对象分配足够的连续内存。 + +#### 4. 标记-复制算法 + +标记-复制算法基于 ”半区复制“ 算法:它将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块的内存使用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的那块内存空间一次性清理掉。其优点在于避免了内存空间碎片化的问题,其缺点如下: + ++ 如果内存中多数对象都是存活的,这种算法将产生大量的复制开销; ++ 浪费内存空间,内存空间变为了原有的一半。 + +
+ + +基于新生代 “朝生夕灭” 的特点,大多数虚拟机都不会按照 1:1 的比例来进行内存划分,例如 HotSpot 虚拟机会将内存空间划分为一块较大的 `Eden` 和 两块较小的 `Survivor` 空间,它们之间的比例是 8:1:1 。 每次分配时只会使用 `Eden` 和其中的一块 `Survivor` ,发生垃圾回收时,只需要将存活的对象一次性复制到另外一块 `Survivor` 上,这样只有 10% 的内存空间会被浪费掉。当 `Survivor` 空间不足以容纳一次 `Minor GC` 时,此时由其他内存区域(通常是老年代)来进行分配担保。 + +#### 5. 标记-整理算法 + +标记-整理算法是在标记完成后,让所有存活对象都向内存的一端移动,然后直接清理掉边界以外的内存。其优点在于可以避免内存空间碎片化的问题,也可以充分利用内存空间;其缺点在于根据所使用的收集器的不同,在移动存活对象时可能要全程暂停用户程序: + +
+ + +## 五、经典垃圾收集器 + +并行与并发是并发编程中的专有名词,在谈论垃圾收集器的上下文语境中,它们的含义如下: + ++ **并行 (Parallel)**:并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,此时通常默认用户线程是处于等待状态。 + ++ **并发 (Concurrent)**:并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。但由于垃圾收集器线程会占用一部分系统资源,所以程序的吞吐量依然会受到一定影响。 + +HotSpot 虚拟机中一共存在七款经典的垃圾收集器: + +
+ +> 注:收集器之间存在连线,则代表它们可以搭配使用。 + +### 5.1 Serial 收集器 + +Serial 收集器是最基础、历史最悠久的收集器,它是一个单线程收集器,在进行垃圾回收时,必须暂停其他所有的工作线程,直到收集结束,这是其主要缺点。它的优点在于单线程避免了多线程复杂的上下文切换,因此在单线程环境下收集效率非常高,由于这个优点,迄今为止,其仍然是 HotSpot 虚拟机在客户端模式下默认的新生代收集器: + +
+ + +### 5.2 ParNew 收集器 + +他是 Serial 收集器的多线程版本,可以使用多条线程进行垃圾回收: + +
+ + +### 5.3 Parallel Scavenge 收集器 + +Parallel Scavenge 也是新生代收集器,基于 标记-复制 算法进行实现,它的目标是达到一个可控的吞吐量。这里的吞吐量指的是处理器运行用户代码的时间与处理器总消耗时间的比值: +$$ +吞吐量 = \frac{运行用户代码时间}{运行用户代码时间+运行垃圾收集时间} +$$ +Parallel Scavenge 收集器提供两个参数用于精确控制吞吐量: + ++ **-XX:MaxGCPauseMillis**:控制最大垃圾收集时间,假设需要回收的垃圾总量不变,那么降低垃圾收集的时间就会导致收集频率变高,所以需要将其设置为合适的值,不能一味减小。 ++ **-XX:MaxGCTimeRatio**:直接用于设置吞吐量大小,它是一个大于 0 小于 100 的整数。假设把它设置为 19,表示此时允许的最大垃圾收集时间占总时间的 5%(即 1/(1+19) );默认值为 99 ,即允许最大 1%( 1/(1+99) )的垃圾收集时间。 + +### 5.4 Serial Old 收集器 + +从名字也可以看出来,它是 Serial 收集器的老年代版本,同样是一个单线程收集器,采用 标记-整理 算法,主要用于给客户端模式下的 HotSpot 虚拟机使用: + +
+ + +### 5.5 Paralled Old 收集器 + +Paralled Old 是 Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,采用 标记-整理 算法实现: + +
+ + +### 5.6 CMS 收集器 + +CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于 标记-清除 算法实现,整个收集过程分为以下四个阶段: + +1. **初始标记 (inital mark)**:标记 `GC Roots` 能直接关联到的对象,耗时短但需要暂停用户线程; +2. **并发标记 (concurrent mark)**:从 `GC Roots` 能直接关联到的对象开始遍历整个对象图,耗时长但不需要暂停用户线程; +3. **重新标记 (remark)**:采用增量更新算法,对并发标记阶段因为用户线程运行而产生变动的那部分对象进行重新标记,耗时比初始标记稍长且需要暂停用户线程; +4. **并发清除 (inital sweep)**:并发清除掉已经死亡的对象,耗时长但不需要暂停用户线程。 + +
+ + +其优点在于耗时长的 并发标记 和 并发清除 阶段都不需要暂停用户线程,因此其停顿时间较短,其主要缺点如下: + ++ 由于涉及并发操作,因此对处理器资源比较敏感。 ++ 由于是基于 标记-清除 算法实现的,因此会产生大量空间碎片。 ++ 无法处理浮动垃圾(Floating Garbage):由于并发清除时用户线程还是在继续,所以此时仍然会产生垃圾,这些垃圾就被称为浮动垃圾,只能等到下一次垃圾收集时再进行清理。 + +### 5.7 Garbage First 收集器 + +Garbage First(简称 G1)是一款面向服务端的垃圾收集器,也是 JDK 9 服务端模式下默认的垃圾收集器,它的诞生具有里程碑式的意义。G1 虽然也遵循分代收集理论,但不再以固定大小和固定数量来划分分代区域,而是把连续的 Java 堆划分为多个大小相等的独立区域(Region)。每一个 Region 都可以根据不同的需求来扮演新生代的 `Eden` 空间、`Survivor` 空间或者老年代空间,收集器会根据其扮演角色的不同而采用不同的收集策略。 + +
+ + +上面还有一些 Region 使用 H 进行标注,它代表 Humongous,表示这些 Region 用于存储大对象(humongous object,H-obj),即大小大于等于 region 一半的对象。G1 收集器的运行大致可以分为以下四个步骤: + +1. **初始标记 (Inital Marking)**:标记 `GC Roots` 能直接关联到的对象,并且修改 TAMS(Top at Mark Start)指针的值,让下一阶段用户线程并发运行时,能够正确的在 Reigin 中分配新对象。G1 为每一个 Reigin 都设计了两个名为 TAMS 的指针,新分配的对象必须位于这两个指针位置以上,位于这两个指针位置以上的对象默认被隐式标记为存活的,不会纳入回收范围; +2. **并发标记 (Concurrent Marking)**:从 `GC Roots` 能直接关联到的对象开始遍历整个对象图。遍历完成后,还需要处理 SATB 记录中变动的对象。SATB(snapshot-at-the-beginning,开始阶段快照)能够有效的解决并发标记阶段因为用户线程运行而导致的对象变动,其效率比 CMS 重新标记阶段所使用的增量更新算法效率更高; +3. **最终标记 (Final Marking)**:对用户线程做一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的少量的 STAB 记录。虽然并发标记阶段会处理 SATB 记录,但由于处理时用户线程依然是运行中的,因此依然会有少量的变动,所以需要最终标记来处理; +4. **筛选回收 (Live Data Counting and Evacuation)**:负责更新 Regin 统计数据,按照各个 Regin 的回收价值和成本进行排序,在根据用户期望的停顿时间进行来指定回收计划,可以选择任意多个 Regin 构成回收集。然后将回收集中 Regin 的存活对象复制到空的 Regin 中,再清理掉整个旧的 Regin 。此时因为涉及到存活对象的移动,所以需要暂停用户线程,并由多个收集线程并行执行。 + +
+ + +### 5.8 内存分配原则 + +#### 1. 对象优先在 Eden 分配 + +大多数情况下,对象在新生代的 `Eden` 区中进行分配,当 `Eden` 区没有足够空间时,虚拟机将进行一次 Minor GC。 + +#### 2. 大对象直接进入老年代 + +大对象就是指需要大量连续内存空间的 Java 对象,最典型的就是超长的字符串或者元素数量很多的数组,它们将直接进入老年代。主要是因为如果在新生代分配,因为其需要大量连续的内存空间,可能会导致提前触发垃圾回收;并且由于新生代的垃圾回收本身就很频繁,此时复制大对象也需要额外的性能开销。 + +#### 3. 长期存活的对象将进入老年代 + +虚拟机会给每个对象在其对象头中定义一个年龄计数器。对象通常在 `Eden` 区中诞生,如果经历第一次 Minor GC 后仍然存活,并且能够被 Survivor 容纳的话,该对象就会被移动到 Survivor 中,并将其年龄加 1。对象在 Survivor 中每经过一次 Minor GC,年龄就加 1,当年龄达到一定程度后(由 `-XX:MaxTenuringThreshold` 设置,默认值为 15)就会进入老年代中。 + +#### 4. 动态年龄判断 + +如果在 Survivor 空间中相同年龄的所有对象大小的总和大于 Survivor 空间的一半,那么年龄大于或等于该年龄的对象就可以直接进入老年代,而无需等待年龄到达 `-XX:MaxTenuringThreshold` 设置的值。 + +#### 5. 空间担保分配 + +在发生 Minor GC 之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果条件成立,那么这一次的 Minor GC 可以确认是安全的。如果不成立,虚拟机会查看 `-XX:HandlePromotionFailure` 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于或者 `-XX:HandlePromotionFailure` 的值设置不允许冒险,那么就要改为进行一次 Full GC 。 + +## 六、虚拟机类加载机制 + +Java 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程被称为虚拟机的类加载机制。 + +### 6.1 类加载时机 + +一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载、验证、准备、卸载、解析、初始化、使用、卸载七个阶段,其中验证、准备、解析三个部分统称为连接: + +
+ + +《Java 虚拟机规范》严格规定了有且只有六种情况必须立即对类进行初始化: + +1. 遇到 `new`、 `getstatic`、 `putstatic`、 `invokestatic` 这四条字节码指令,如果类型进行过初始化,则需要先触发其进行初始化,能够生成这四条指令码的典型 Java 代码场景有: + + 使用 `new` 关键字实例化对象时; + + 读取或设置一个类型的静态字段时(被 final 修饰,已在编译期把结果放入常量池的静态字段除外); + + 调用一个类型的静态方法时。 +2. 使用 `java.lang.reflect` 包的方法对类型进行反射调用时,如果类型没有进行过初始化、则需要触发其初始化; +3. 当初始化类时,如发现其父类还没有进行过初始化、则需要触发其父类进行初始化; +4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类; +5. 当使用 JDK 7 新加入的动态语言支持时,如果一个 `java.lang.invoke.MethodHandle` 实例最后解析的结果为 `REF_getStatic` , `REF_putStatic` , `REF_invokeStatic` , `REF_newInvokeSpecial` 四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化; +6. 当一个接口中定义了 JDK 8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那么该接口要在其之前被初始化。 + +### 6.2 类加载过程 + +#### 1. 加载 + +在加载阶段,虚拟机需要完成以下三件事: + ++ 通过一个类的全限定名来获取定义此类的二进制字节流 ; ++ 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构; ++ 在内存中生成一个代表这个类的 `java.lang.Class` 对象,作为方法区这个类的各种数据的访问入口。 + +《Java 虚拟机规范》并没有限制从何处获取二进制流,因此可以从 JAR 包、WAR 包获取,也可以从 JSP 生成的 Class 文件等处获取。 + +#### 2. 验证 + +这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,从而保证这些信息被当做代码运行后不会危害虚拟机自身的安全。验证阶段大致会完成下面四项验证: + ++ **文件格式验证**:验证字节流是否符合 Class 文件格式的规范; ++ **元数据验证**:对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java 语言规范》的要求(如除了 `java.lang.Object` 外,所有的类都应该有父类); ++ **字节码验证**:通过数据流分析和控制流分析,确定程序语义是合法的,符合逻辑的(如允许把子类对象赋值给父类数据类型,但不能把父类对象赋值给子类数据类型); ++ **符号引用验证**:验证类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。如果无法验证通过,则会抛出一个`java.lang.IncompatibleClassChangeError` 的子类异常,如 `java.lang.NoSuchFieldError` 、 `java.lang.NoSuchMethodError` 等。 + +#### 3. 准备 + +准备阶段是正式为类中定义的变量(即静态变量,被 static 修饰的变量)分配内存并设置类变量初始值的阶段。 + +#### 4. 解析 + +解析是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程: + ++ **符号引用**:符号引用用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。 ++ **直接引用**:直接引用是指可以直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。 + +整个解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这 7 类符号引用进行解析。 + +#### 5. 初始化 + +初始化阶段就是执行类构造器的 `()` 方法的过程,该方法具有以下特点: + ++ `()` 方法由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生,编译器收集顺序由语句在源文件中出现的顺序决定。 ++ `()` 方法与类的构造器函数(即在虚拟机视角中的实例构造器 `()`方法)不同,它不需要显示的调用父类的构造器,Java 虚拟机会保证在子类的 `()` 方法执行前,父类的 `()` 方法已经执行完毕。 ++ 由于父类的 `()` 方法先执行,也就意味着父类中定义的静态语句块要优先于子类变量的赋值操作。 ++ `()` 方法对于类或者接口不是必须的,如果一个类中没有静态语句块,也没有对变量进行赋值操作,那么编译器可以不为这个类生成 `()` 方法。 ++ 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成 `()` 方法。 ++ Java 虚拟机必须保证一个类的 `()` 方法在多线程环境中被正确的加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的 `()` 方法,其他线程都需要阻塞等待。 + +### 6.3 类加载器 + +能够通过一个类的全限定名来获取描述该类的二进制字节流的工具称为类加载器。每一个类加载器都拥有一个独立的类名空间,因此对于任意一个类,都必须由加载它的类加载器和这个类本身来共同确立其在 Java 虚拟机中的唯一性。这意味着要想比较两个类是否相等,必须在同一类加载器加载的前提下;如果两个类的类加载器不同,则它们一定不相等。 + +### 6.4 双亲委派模型 + +从 Java 虚拟机角度而言,类加载器可以分为以下两类: + ++ **启动类加载器**:启动类加载器(Bootstrap ClassLoader)由 C++ 语言实现(以 HotSpot 为例),它是虚拟机自身的一部分; ++ **其他所有类的类加载器**:由 Java 语言实现,独立存在于虚拟机外部,并且全部继承自 `java.lang.ClassLoader` 。 + +从开发人员角度而言,类加载器可以分为以下三类: + ++ **启动类加载器 (Boostrap Class Loader)**:负责把存放在 `\lib` 目录中,或被 `-Xbootclasspath` 参数所指定的路径中存放的能被 Java 虚拟机识别的类库加载到虚拟机的内存中; ++ **扩展类加载器 (Extension Class Loader)**:负责加载 `\lib\ext` 目录中,或被 `java.ext.dirs` 系统变量所指定的路径中的所有类库。 ++ **应用程序类加载器 (Application Class Loader)**:负责加载用户类路径(ClassPath)上的所有的类库。 + +JDK 9 之前的 Java 应用都是由这三种类加载器相互配合来完成加载: + +
+ + +上图所示的各种类加载器之间的层次关系被称为类加载器的 “双亲委派模型”,“双亲委派模型” 要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,需要注意的是这里的加载器之间的父子关系一般不是以继承关系来实现的,而是使用组合关系来复用父类加载器的代码。 + +双亲委派模型的工作过程如下:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。基于双亲委派模型可以保证程序中的类在各种类加载器环境中都是同一个类,否则就有可能出现一个程序中存在两个不同的 `java.lang.Object` 的情况。 + +### 6.5 模块化下的类加载器 + +JDK 9 之后为了适应模块化的发展,类加载器做了如下变化: + ++ 仍维持三层类加载器和双亲委派的架构,但扩展类加载器被平台类加载器所取代; ++ 当平台及应用程序类加载器收到类加载请求时,要首先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载; ++ 启动类加载器、平台类加载器、应用程序类加载器全部继承自 `java.internal.loader.BuiltinClassLoader` ,BuiltinClassLoader 中实现了新的模块化架构下类如何从模块中加载的逻辑,以及模块中资源可访问性的处理。 + +
+ + +## 七、程序编译 + +### 7.1 编译器分类 + ++ **前端编译器**:把 `*.java` 文件转变成 `.class` 文件的过程;如 JDK 的 Javac,Eclipse JDT 中的增量式编译器。 ++ **即使编译器**:常称为 JIT 编译器(Just In Time Complier),在运行期把字节码转变成本地机器码的过程;如 HotSpot 虚拟机中的 C1、C2 编译器,Graal 编译器。 ++ **提前编译器**:直接把程序编译成目标机器指令集相关的二进制代码的过程。如 JDK 的 jaotc,GUN Compiler for the Java(GCJ),Excelsior JET 。 + +### 7.2 解释器与编译器 + +在 HotSpot 虚拟机中,Java 程序最初都是通过解释器(Interpreter)进行解释执行的,其优点在于可以省去编译时间,让程序快速启动。当程序启动后,如果虚拟机发现某个方法或代码块的运行特别频繁,就会使用编译器将其编译为本地机器码,并使用各种手段进行优化,从而提高执行效率,这就是即时编译器。HotSpot 内置了两个(或三个)即时编译器: + ++ **客户端编译器 (Client Complier)**:简称 C1; ++ **服务端编译器 (Servier Complier)**:简称 C2,在有的资料和 JDK 源码中也称为 Opto 编译器; ++ **Graal 编译器**:在 JDK 10 时才出现,长期目标是替代 C2。 + +在分层编译的工作模式出现前,采用客户端编译器还是服务端编译器完全取决于虚拟机是运行在客户端模式还是服务端模式下,可以在启动时通过 `-client` 或 `-server` 参数进行指定,也可以让虚拟机根据自身版本和宿主机性能来自主选择。 + +### 7.3 分层编译 + +要编译出优化程度越高的代码通常都需要越长的编译时间,为了在程序启动速度与运行效率之间达到最佳平衡,HotSpot 虚拟机在编译子系统中加入了分层编译(Tiered Compilation): + ++ **第 0 层**:程序纯解释执行,并且解释器不开启性能监控功能; ++ **第 1 层**:使用客户端编译器将字节码编译为本地代码来运行,进行简单可靠的稳定优化,不开启性能监控功能; ++ **第 2 层**:仍然使用客户端编译执行,仅开启方法及回边次数统计等有限的性能监控; ++ **第 3 层**:仍然使用客户端编译执行,开启全部性能监控; ++ **第 4 层**:使用服务端编译器将字节码编译为本地代码,其耗时更长,并且会根据性能监控信息进行一些不可靠的激进优化。 + +以上层次并不是固定不变的,根据不同的运行参数和版本,虚拟机可以调整分层的数量。各层次编译之间的交互转换关系如下图所示: + +
+ + +实施分层编译后,解释器、客户端编译器和服务端编译器就会同时工作,可以用客户端编译器获取更高的编译速度、用服务端编译器来获取更好的编译质量。 + +### 7.4 热点探测 + +即时编译器编译的目标是 “热点代码”,它主要分为以下两类: + ++ 被多次调用的方法。 ++ 被多次执行循环体。这里指的是一个方法只被少量调用过,但方法体内部存在循环次数较多的循环体,此时也认为是热点代码。但编译器编译的仍然是循环体所在的方法,而不会单独编译循环体。 + +判断某段代码是否是热点代码的行为称为 “热点探测” (Hot Spot Code Detection),主流的热点探测方法有以下两种: + ++ **基于采样的热点探测 (Sample Based Hot Spot Code Detection)**:采用这种方法的虚拟机会周期性地检查各个线程的调用栈顶,如果发现某个(或某些)方法经常出现在栈顶,那么就认为它是 “热点方法”。 ++ **基于计数的热点探测 (Counter Based Hot Spot Code Detection)**:采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是 “热点方法”。 + +## 八、代码优化 + +即时编译器除了将字节码编译为本地机器码外,还会对代码进行一定程度的优化,它包含多达几十种优化技术,这里选取其中代表性的四种进行介绍: + +### 8.1 方法内联 + +最重要的优化手段,它会将目标方法中的代码原封不动地 “复制” 到发起调用的方法之中,避免发生真实的方法调用,并采用名为类型继承关系分析(Class Hierarchy Analysis,CHA)的技术来解决虚方法(Java 语言中默认的实例方法都是虚方法)的内联问题。 + +### 8.2 逃逸分析 + +逃逸行为主要分为以下两类: + ++ **方法逃逸**:当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,此时称为方法逃逸; ++ **线程逃逸**:当一个对象在方法里面被定义后,它可能被外部线程所访问,例如赋值给可以在其他线程中访问的实例变量,此时称为线程,其逃逸程度高于方法逃逸。 + +```java +public static StringBuilder concat(String... strings) { + StringBuilder sb = new StringBuilder(); + for (String string : strings) { + sb.append(string); + } + return sb; // 发生了方法逃逸 +} + +public static String concat(String... strings) { + StringBuilder sb = new StringBuilder(); + for (String string : strings) { + sb.append(string); + } + return sb.toString(); // 没有发生方法逃逸 +} +``` + +如果能证明一个对象不会逃逸到方法或线程之外,或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则可以为这个对象实例采取不同程序的优化: + ++ **栈上分配 (Stack Allocations)**:如果一个对象不会逃逸到线程外,那么将会在栈上分配内存来创建这个对象,而不是 Java 堆上,此时对象所占用的内存空间就会随着栈帧的出栈而销毁,从而可以减轻垃圾回收的压力。 ++ **标量替换 (Scalar Replacement)**:如果一个数据已经无法再分解成为更小的数据类型,那么这些数据就称为标量(如 int、long 等数值类型及 reference 类型等);反之,如果一个数据可以继续分解,那它就被称为聚合量(如对象)。如果一个对象不会逃逸外方法外,那么就可以将其改为直接创建若干个被这个方法使用的成员变量来替代,从而减少内存占用。 ++ **同步消除 (Synchronization Elimination)**:如果一个变量不会逃逸出线程,那么对这个变量实施的同步措施就可以消除掉。 + +### 8.3 公共子表达式消除 + +如果一个表达式 E 之前已经被计算过了,并且从先前的计算到现在 E 中所有变量的值都没有发生过变化,那么 E 这次的出现就称为公共子表达式。对于这种表达式,无需再重新进行计算,只需要直接使用前面的计算结果即可。 + +### 8.4 数组边界检查消除 + +对于虚拟机执行子系统来说,每次数组元素的读写都带有一次隐含的上下文检查以避免访问越界。如果数组的访问发生在循环之中,并且使用循环变量来访问数据,即循环变量的取值永远在 [0,list.length) 之间,那么此时就可以消除整个循环的数据边界检查,从而避免多次无用的判断。 + + + +## 参考资料 + ++ 主要参考自:周志明 . 深入理解Java虚拟机(第3版). 机械工业出版社 , 2019-12 ,想要深入了解虚拟机的话,推荐阅读原书。 ++ [美团技术团队 —— Java Hotspot G1 GC的一些关键技术](https://tech.meituan.com/2016/09/23/g1.html) + + + + + diff --git a/blog/en-us/java/springAnnotation.md b/blog/en-us/java/springAnnotation.md new file mode 100644 index 0000000..7a10098 --- /dev/null +++ b/blog/en-us/java/springAnnotation.md @@ -0,0 +1,232 @@ +# Spring 中的 18 个注解 + +## 1 @Controller + +标识一个该类是Spring MVC controller处理器,用来创建处理http请求的对象. +```java +@Controller +public class TestController{ + + public String test(Map map){ + return "hello"; + } +} +``` + +## 2 @RestController + +Spring4之后加入的注解,原来在@Controller中返回json需要@ResponseBody来配合,如果直接用@RestController替代@Controller就不需要再配置@ResponseBody,默认返回json格式。 + +## 3 @Service + +用于标注业务层组件,说白了就是加入你有一个用注解的方式把这个类注入到spring配置中 + +## 4 @Autowired + +用来装配bean,都可以写在字段上,或者方法上。 + +默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false,例如: + +```java +@Autowired(required=false) +``` +## 5 @RequestMapping + +类定义处: 提供初步的请求映射信息,相对于 WEB 应用的根目录。 + +方法处: 提供进一步的细分映射信息,相对于类定义处的 URL。 + +## 6 @RequestParam + +用于将请求参数区数据映射到功能处理方法的参数上 + +例如 + +``` java +public Resp test(@RequestParam Integer id){ + return Resp.success(customerInfoService.fetch(id)); +} +``` +这个id就是要接收从接口传递过来的参数id的值的,如果接口传递过来的参数名和你接收的不一致,也可以如下 + +``` java +public Resp test (@RequestParam(value="course_id") Integer id){ + return Resp.success(customerInfoService.fetch(id)); +} +``` + +其中course_id就是接口传递的参数,id就是映射course_id的参数名 + +## 7 @ModelAttribute + +使用地方如下: + +1. 标记在方法上 + +标记在方法上,会在每一个@RequestMapping标注的方法前执行,如果有返回值,则自动将该返回值加入到ModelMap中。 + +(1). 在有返回的方法上:当ModelAttribute设置了value,方法返回的值会以这个value为key,以参数接受到的值作为value,存入到Model中,如下面的方法执行之后,最终相当于 + +model.addAttribute("user_name", name);假如 @ModelAttribute没有自定义value,则相当于 + +model.addAttribute("name", name); +``` java +@ModelAttribute(value="user_name") +public String before(@RequestParam(required = false) String Name,Model model){ + System.out.println("name is "+name); +} +``` +(2) 在没返回的方法上: + +需要手动model.add方法 + +``` java +@ModelAttribute +public void before(@RequestParam(required = false) Integer age,Model model) { + model.addAttribute("age",age); + System.out.println("age is "+age); +} +``` +2. 标记在方法的参数上 + +标记在方法的参数上,会将客户端传递过来的参数按名称注入到指定对象中,并且会将这个对象自动加入ModelMap中,便于View层使用.我们在上面的类中加入一个方法如下 + +``` java + +public Resp model(@ModelAttribute("user_name") String user_name, + @ModelAttribute("name") String name, + @ModelAttribute("age") Integer age,Model model){ + System.out.println("user_name="+user_name+" name="+name+" age="+age); + System.out.println("model="+model); + } +``` +用在方法参数中的@ModelAttribute注解,实际上是一种接受参数并且自动放入Model对象中,便于使用。 + +## 8 @Cacheable + +用来标记缓存查询。可用用于方法或者类中,当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。 + +参数列表 + +参数|解释|例子 +-|-|-|- +value|名称|@Cacheable(value={"c1","c2"}) +key|key|@Cacheable(value="c1",key="#id") +condition|条件|@Cacheable(value="c1",condition="#id=1") + +比如@Cacheable(value="UserCache") 标识的是当调用了标记了这个注解的方法时,逻辑默认加上从缓存中获取结果的逻辑,如果缓存中没有数据,则执行用户编写查询逻辑,查询成功之后,同时将结果放入缓存中。 + +但凡说到缓存,都是key-value的形式的,因此key就是方法中的参数(id),value就是查询的结果,而命名空间UserCache是在spring*.xml中定义。 + +```java +@Cacheable(value="UserCache") +public int getUserAge(int id){ + int age=getAgeById(id); + return age; +} +``` + +## 9 @CacheEvict + +@CacheEvict用来标记要清空缓存的方法,当这个方法被调用后,即会清空缓存。 + +@CacheEvict(value=”UserCache”) + +参数列表 + +参数|解释|例子 +-|-|-|- +value|名称|@CacheEvict(value={"c1","c2"}) +key|key|@CacheEvict(value="c1",key="#id") +condition|缓存得条件可为空| +allEntries|是否清空所有内容|@CacheEvict(value="c1",allEntries=true) +beforeInvocation|是否在方法执行前清空|@CacheEvict(value="c1",beforeInvocation=true) + +## 10 @Resource + +@Resource的作用相当于@Autowired + +只不过@Autowired按byType自动注入, + +而@Resource默认按 byName自动注入罢了。 + +@Resource有两个属性是比较重要的,分是name和type,Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不指定name也不指定type属性,这时将通过反射机制使用byName自动注入策略。 + +@Resource装配顺序: + +1、如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常 + +2、如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常 + +3、如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常 + +4、如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配; + +## 11 @PostConstruct + +@PostConstruct用来标记是在项目启动的时候执行这个方法。用来修饰一个非静态的void()方法 + +也就是spring容器启动时就执行,多用于一些全局配置、数据字典之类的加载 + +被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。PostConstruct在构造函数之后执行,init()方法之前执行。PreDestroy()方法在destroy()方法执行之后执行 + +## 12 @PreDestory + +被@PreDestroy修饰的方法会在服务器卸载Servlet的时候运行,并且只会被服务器调用一次,类似于Servlet的destroy()方法。被@PreDestroy修饰的方法会在destroy()方法之后运行,在Servlet被彻底卸载之前 + +## 13 @Repository + +@Repository用于标注数据访问组件,即DAO组件 + +## 14 @Component + +@Component泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注 + +## 15 @Scope + +@Scope用来配置 spring bean 的作用域,它标识 bean 的作用域。 + +默认值是单例 + +1、singleton:单例模式,全局有且仅有一个实例 + +2、prototype:原型模式,每次获取Bean的时候会有一个新的实例 + +3、request:request表示该针对每一次HTTP请求都会产生一个新的bean,同时该bean仅在当前HTTP request内有效 + +4、session:session作用域表示该针对每一次HTTP请求都会产生一个新的bean,同时该bean仅在当前HTTP session内有效 + +5、global session:只在portal应用中有用,给每一个 global http session 新建一个Bean实例。 + +## 16 @SessionAttributes + +默认情况下Spring MVC将模型中的数据存储到request域中。当一个请求结束后,数据就失效了。如果要跨页面使用。那么需要使用到session。而@SessionAttributes注解就可以使得模型中的数据存储一份到session域中 + +参数: + +1、names:这是一个字符串数组。里面应写需要存储到session中数据的名称。 + +2、types:根据指定参数的类型,将模型中对应类型的参数存储到session中 + +3、value:和names是一样的。 + +```java +@Controller +@SessionAttributes(value={"names"},types={Integer.class}) +public class ScopeService{ + @RequestMapping("/testSession") + public String test(Map map){ + map.put("names",Arrays.asList("a","b","c")); + map.put("age",12); + return "hello"; + } +} +``` + +## 17 @Required + +适用于bean属性setter方法,并表示受影响的bean属性必须在XML配置文件在配置时进行填充。否则,容器会抛出一个BeanInitializationException异常。 + +## 18 @Qualifier + +@Qualifier当你创建多个具有相同类型的 bean 时,并且想要用一个属性只为它们其中的一个进行装配,在这种情况下,你可以使用 @Qualifier 注释和 @Autowired 注释通过指定哪一个真正的 bean 将会被装配来消除混乱。 diff --git a/blog/en-us/js/baidu-tongji.js b/blog/en-us/js/baidu-tongji.js new file mode 100644 index 0000000..1a35847 --- /dev/null +++ b/blog/en-us/js/baidu-tongji.js @@ -0,0 +1,35 @@ + +var _hmt = _hmt || []; +(function() { + let hm = document.createElement("script"); + // rosemarys.gitee.io + hm.src = "https://hm.baidu.com/hm.js?08563f1498cfbdc12f55e3fc8b133b4b"; + let s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + // burningmyself.cn + hm = document.createElement("script"); + hm.src = "https://hm.baidu.com/hm.js?ea33cbaa80e12b2127ea3d44494c0870"; + s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + // burningmyself.gitee.io + hm = document.createElement("script"); + hm.src = "https://hm.baidu.com/hm.js?10b96d1600d054d9d9519e5512e0af19"; + s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + // burningmyself.github.io + hm = document.createElement("script"); + hm.src = "https://hm.baidu.com/hm.js?14446117c56cf1fdab44c58081833af8"; + s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + // yangfubing.gitee.io + hm = document.createElement("script"); + hm.src = "https://hm.baidu.com/hm.js?66401a22d7cba55cbbdd060204e0f29d"; + s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + // yangfubing.github.io + hm = document.createElement("script"); + hm.src = "https://hm.baidu.com/hm.js?dd0db5e74ea516ec6bee7d759094aaaa"; + s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); +})(); + diff --git a/blog/en-us/js/extra.js b/blog/en-us/js/extra.js new file mode 100644 index 0000000..7b3c571 --- /dev/null +++ b/blog/en-us/js/extra.js @@ -0,0 +1,13 @@ + +(function () { + let generator = document.getElementsByTagName("meta").generator; + generator.name="keywords"; + generator.content="net,java,php,python,docker,web"; + let meta = document.createElement("meta"); + let s = document.getElementsByTagName("meta")[0]; + meta.name="google-site-verification"; + meta.content="le9TAKnSKhLDEEGnDu2ofXi3taLVIxmKNT0bEIsetNE"; + s.parentNode.insertBefore(meta, s); + let copyright = document.getElementsByClassName("md-footer-copyright") + copyright[0].outerHTML=document.getElementsByClassName("md-footer-copyright__highlight")[0].outerHTML +})(); diff --git a/blog/en-us/js/google.js b/blog/en-us/js/google.js new file mode 100644 index 0000000..7fe8064 --- /dev/null +++ b/blog/en-us/js/google.js @@ -0,0 +1,14 @@ +// burningmyself.gitee.io +// Global site tag (gtag.js) - Google Analytics +/* */ + window.dataLayer = window.dataLayer || []; + function gtag(){dataLayer.push(arguments);} + gtag('js', new Date()); + gtag('config', 'UA-155084439-1'); + +//yangfubing.gitee.io +/* */ + window.dataLayer = window.dataLayer || []; + function gtag(){dataLayer.push(arguments);} + gtag('js', new Date()); + gtag('config', 'UA-155132293-1'); diff --git a/blog/en-us/net/c_sqlserver_nginx.md b/blog/en-us/net/c_sqlserver_nginx.md new file mode 100644 index 0000000..ef5ef2e --- /dev/null +++ b/blog/en-us/net/c_sqlserver_nginx.md @@ -0,0 +1,392 @@ +# ASP.NET Core 实战:使用 Docker 容器化部署 ASP.NET Core + sqlserver + Nginx + +### 一、前言 + +  在之前的文章(ASP.NET Core 实战:Linux 小白的 .NET Core 部署之路)中,我介绍了如何在 Linux 环境中安装 .NET Core SDK / .NET Core Runtime、Nginx、sqlserver,以及如何将我们的 ASP.NET Core MVC 程序部署到 Linux 上,同时,使用 supervisor 守护程序守护我们的 .NET Core 程序。如果,你有看过那篇文章,并且和我一样是个 Linux 小白用户的话,可能第一感觉就是,把 .NET Core 项目部署在 IIS 上也挺好。 + +  将 .NET Core 项目部署到 Linux 上如此复杂,就没有简单的部署方式吗? + +  你好,有的,Docker 了解一下~~~ + +  PS:这里的示例代码还是采用之前的毕业设计项目,在这篇文章发布的时候,我已经在程序的仓库中添加了对于 Docker 的支持,你可以下载下来,自己尝试一下,毕竟,实践出真知。 + +   代码仓储:https://github.com/burningmyself/micro + + ### 二、Step by Step + +#### 1、安装 Docker & Docker Compose + +  在代码交付的过程中,偶尔会遇到这样的问题,在本地测试是好的,但是部署到测试环境、生产环境时就出这样那样的问题,同时,因为本地与测试环境、生产环境之间存在差异,我们可能无法在本地复现这些问题,那么,有没有一种工具可以很好的解决这一问题呢?随着历史的车轮不断前行,容器技术诞生了。 + +  Docker,作为最近几年兴起的一种虚拟化容器技术,他可以将我们的运行程序与操作系统做一个隔离,例如这里我们需要运行 .NET Core 程序,我们不再需要关心底层的操作系统是什么,不需要在每台需要需要运行程序的机器上安装程序运行的各种依赖,我们可以通过程序打包成镜像的方式,将应用程序和该程序的依赖全部置于一个镜像文件中,这时,只要别的机器上有安装 Docker,就可以通过我们打包的这个镜像来运行这个程序。 + +#### 1.1、卸载 Docker + +  在安装 Docker 之前,我们应该确定当前的机器上是否已经安装好了 Docker,为了防止与现在安装的 Docker CE 发生冲突,这里我们先卸载掉以前版本的 Docker,如果你确定你的机器上并没有安装 Docker 的话此步可以跳过。 + +  在 Linux 中可以使用 \ 加 Enter 在输入很长很长的语句时进行换行,这里和后面的命令都是采用这样的方式。 + +sudo yum remove docker \ + docker-client \ + docker-client-latest \ + docker-common \ + docker-latest \ + docker-latest-logrotate \ + docker-logrotate \ + docker-engine +#### 1.2、添加 yum 源 + +  在安装 Docker CE 的方式上,我是采用将 Docker CE 的源添加到 yum 源中,之后我们就可以直接使用 yum install 安装 Docker CE,整个的安装过程如下。 + +安装工具包从而可以让我们在 yum 中添加别的仓储源 +``` +sudo yum install -y yum-utils \ + device-mapper-persistent-data \ + lvm2 +``` +设置 docker ce 的稳定库地址 +``` +sudo yum-config-manager \ + --add-repo \ + https://download.docker.com/linux/centos/docker-ce.repo +``` +安装 docker ce +``` +sudo yum install docker-ce docker-ce-cli containerd.io +``` +当我们安装好 Docker 之后,我们就可以使用 docker 命令验证我们是否在机器上成功安装了 Docker,同时,也可以使用 docker --version 命令查看我们安装的 Docker CE 版本。 + +![cmn](./../img/c_sqlserver_nginx1.png) + +#### 1.3、设置开机自启 + +  当 Docker 已经在我们的机器上安装完成后,我们就可以将 Docker 设置成机器的自启服务,这样,如果出现服务器重启的情况下,我们的 Docker 也可以随服务器的重启自动启动 Docker 服务。 + +启动 Docker 服务并允许开机自启 +``` +sudo systemctl start docker +``` +查看当前 dokcer 的运行情况 +``` +sudo systemctl status docker +``` +#### 1.4、Hello World + +  就像我们在学习一门新的语言时,运行的第一句代码,几乎都是打印出 Hello World,而在 Docker Hub 中,也有这么一个镜像,在无数的 Docker 教程中,安装完 Docker 后,第一件事就是拉取这个镜像文件,“告诉” Docker,我来了。 + +  Docker Hub 是存放镜像的仓库,里面包含了许多的镜像文件,因为服务器在国外的原因,下载的速度可能不理想,像国内的阿里云、腾讯云也有提供对于 Docker 镜像的加速器服务,你可以按需使用,当然,你也可以创建属于你的私有镜像仓库。 + +  docker run 命令,它会在我们的本地镜像库中先寻找这个镜像,然后运行。如果在本地没有找到的话,则会自动使用 docker pull 从 Docker Hub 中寻找,能找到的话,则会自动下载到本地,然后运行,找不到的话,这条命令也就运行失败了。 + +![cmn](./../img/c_sqlserver_nginx2.png) + +#### 1.5、安装 Docker Compose + +  在实际的项目开发中,我们可能会有多个应用镜像,例如在本篇文章的示例中,为了在 Docker 中运行我们的程序,我们需要三个镜像:应用程序自身镜像、sqlserver Server 镜像、以及 Nginx 镜像,为了将我们的程序启动起来,我们需要手敲各个容器的启动参数,环境变量,容器命名,指定不同容器的链接参数等等一系列的操作,又多又烦,可能某一步操作失败后程序就无法正常运行。而当我们使用了 Docker Compose 之后,我们就可以把这些命令一次性写在 docker-compose.yml 配置文件中,以后每次启动我们的应用程序时,只需要通过 docker compose 命令就可以自动帮我们完成这些操作。 + +从 github 下载 docker compose 二进制文件 +``` +sudo curl -L "https://github.com/docker/compose/releases/download/1.23.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +``` +对下载的二进制文件应用可执行权限 +``` +sudo chmod +x /usr/local/bin/docker-compose +``` +查看 docker compose 版本 +``` +docker-compose --version +``` + +![cmn](./../img/c_sqlserver_nginx3.png) + +#### 2、构建程序镜像 + +  当我们在服务器上安装好 docker 和 docker compose 之后,就可以开始构建我们的程序镜像了。首先我们需要对我们的运行程序添加对于 Docker 的支持。你可以自己手动在 MVC 项目中添加 Dockerfile 文件,或是通过右键添加 Docker 支持。 + +![cmn](./../img/c_sqlserver_nginx4.png) + +Dockerfile 就像一个执行的清单,它告诉 Docker,我们这个镜像在构建和运行时需要按照什么样的命令运行。打开 VS 为我们自动创建的 Dockerfile,可以看到清晰的分成了四块的内容。 + +![cmn](./../img/c_sqlserver_nginx5.png) + +我们知道,.NET Core 程序的运行需要依赖于 .NET Core Runtime(CoreCLR),因此,为了使我们的程序可以运行起来,我们需要从 hub 中拉取 runtime ,并在 此基础上构建我们的应用镜像。同时,为了避免因为基础的环境的不同造成对程序的影响,这里的 Runtime 需要同程序开发时的 .NET Core SDK 版本保持一致,所以这里我使用的是 .NET Core 3.0 Runtime。 + +  一个镜像中包含了应用程序及其所有的依赖,与虚拟机不同的是,容器中的每个镜像最终是共享了宿主机的操作系统资源,容器作为用户空间中的独立进程运行在主机操作系统上。 +![cmn](./../img/c_sqlserver_nginx6.png) + +PS:图片版权归属于微软的技术文档,如有侵权,请联系我删除,源文件地址:什么是 Docker? + +  镜像可以看成一个个小型的“虚拟主机”,这里我们在镜像中创建了一个 /app 路径作为我们程序在镜像中的工作目录,同时,将 80,443 端口暴露给 Docker,从而可以使我们在镜像外面通过端口访问到当前镜像中的运行的程序。 +``` +FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-buster-slim AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 +``` +  因为我们的应用是一个微服架构的应用,最终的项目依赖于解决方案中的各个类库以及我们从 Nuget 中下载的各种第三方组件,在部署时,需要将这些组件打包成 dll 引用。所以,这里我们需要使用 .NET Core SDK 中包含的 .NET Core CLI 进行还原和构建。 + +  就像在下面的代码中,我们在镜像的内部创建了一个 /src 的路径,将当前解决方案下的类库都复制到这个目录下,之后通过 dotnet restore 命令还原我们的主程序所依赖的各个组件。当我们还原好依赖的组件后,就可以使用 dotnet build 命令生成 Release版本的 dll 文件,同时输出到之前创建的 /app 路径下。 + +``` +FROM mcr.microsoft.com/dotnet/core/sdk:3.0-buster AS build +WORKDIR /src +COPY . . +WORKDIR /src/templates/service/host/Base.IdentityServer +RUN dotnet restore -nowarn:msb3202,nu1503 +RUN dotnet build --no-restore -c Release -o /app + +``` + +上面一步可以看成我们在使用 VS 生成 Release 版本的解决方案,当生成没有出错之后,我们就可以进行程序的发布。 +``` +FROM build AS publish +RUN dotnet publish --no-restore -c Release -o /app +``` +  当已经生成发布文件之后,按照我们平时部署在 Windows 上的过程,这时就可以通过 IIS 部署运行了,因此,构建我们应用镜像的最后一步就是通过 dotnet 命令执行我们的程序。 +``` +FROM base AS final +WORKDIR /app +COPY --from=publish /app . +ENTRYPOINT ["dotnet", "Base.IdentityServer.dll"] +``` +  似乎到这一步构建程序镜像就结束了,按照这样流程做的话,就需要我们将整个的解决方案上传到服务器上了,可是,很多时候,我们仅仅是把我们在本地发布好的项目上传到服务器上,这与我们现在的构建流程具有很大的不同,所以这里我们来修改 Dockerfile 文件,从而符合我们的发布流程。 + +  从上面分析 Dockerfile 的过程中不难看出,在服务器上构建镜像的第二步、第三步就是我们现在在开发环境中手动完成的部分,所以这里,我们只需要对这部分进行删除即可,修改后的 Dockerfile 如下。 +``` +FROM mcr.microsoft.com/dotnet/core/sdk:3.0-buster +WORKDIR /app +COPY . /app +EXPOSE 80 +EXPOSE 443 +ENTRYPOINT ["dotnet", "Base.IdentityServer.dll"] +``` +  在修改后的 Dockerfile 中,可以看到,我们删去了 build 和 release 的过程,选择直接将我们 Dockerfile 路径下的文件拷贝到镜像中的 /app 路径下,然后直接执行 dotnet 命令,运行我们的程序。 + +  为了确保 Dockerfile 与发布后的文件处于同一路径下,这里我们需要使用 VS 修改 Dockerfile 的属性值,确保会复制到输出的目录下,这里选择如果较新则复制即可。 + +![cmn](./../img/c_sqlserver_nginx7.png) + +#### 3、编写 docker-compose.yml + +  当我们构建好应用的镜像,对于 Nginx 和 sqlserver 我们完全可以从 hub 中拉取下来,再执行一些配置即可。所以,我们现在就可以编写 docker compose 文件,来定义我们的应用镜像运行时需要包含的依赖以及每个镜像的启动顺序。 + +  右键选中 MVC 项目,添加一个 docker-compose.yml 文件,同样的,需要修改该文件的属性,以便于该文件可以复制到输出目录下。注意,这里的文件名和上文的 Dockerfile 都是特定的,你不能做任何的修改。如果你的电脑上已经安装了 Docker for Windows,你也可以使用 VS,右键添加,选中容器业务流程协调程序支持自动对 docker compose 进行配置。 + +![cmn](./../img/c_sqlserver_nginx8.png) + +在 yml 文件中,我定义了三个镜像:AdminApiGateway.Host、Base.IdentityServer、Base.HttpApi.Host。三个镜像的定义中有许多相同的地方,都设置了自动重启(restart),以及都处于同一个桥接网络下(psu-net)从而达到镜像间的通信。 + + +sqlserver 是 SqlServer 的镜像,我们通过环境变量 SA_PASSWORD 设置了 SqlServer 的数据库连接密码,并通过挂载卷的方式将镜像中的数据库文件持久化到我们的服务器本地路径中。同时,将镜像的 1433 端口映射到服务器的 1433 端口上。 + +  AdminApiGateway.Host 则是我们的程序后台网关镜像,采用位于 /data/dotnet/AdminApiGateway/ 路径下的 Dockerfile 文件进行构建的,因为主程序的运行需要依赖于数据库,所以这里采用 depends_on 属性,使我们的应用镜像依赖于 sqlserver 镜像,即,在 sqlserver 启动后才会启动应用镜像。 + +  nginx 则是我们的 nginx 镜像,这里将镜像中的 80 端口和 443 端口都映射到服务器 IP 上,因为我们需要配置 Nginx 从而监听我们的程序,所以通过挂载卷的方式,将本地的 nginx.conf 配置文件用配置映射到镜像中。同时,因为我们在构建应用镜像的 Dockerfile 文件时,对外暴露了 80,443 端口,所以这里就可以通过 links 属性进行监听(如果构建时未暴露端口,你可以在 docker compose 文件中通过 Expose 属性暴露镜像中的端口)。 + +  Nginx 的配置文件如下,这里特别需要注意文件的格式,缩进,一点小错误都可能导致镜像无法正常运行。如果你和我一样将 nginx.conf 放到程序运行路径下的,别忘了修改文件的属性。 +``` +user nginx; +worker_processes 1; + +error_log /var/log/nginx/error_log.log warn; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on; + + gzip on; #开启gzip + gzip_disable "msie6"; #IE6不使用gzip + gzip_vary on; #设置为on会在Header里增加 "Vary: Accept-Encoding" + gzip_proxied any; #代理结果数据的压缩 + gzip_comp_level 6; #gzip压缩比(1~9),越小压缩效果越差,但是越大处理越慢,所以一般取中间值 + gzip_buffers 16 8k; #获取多少内存用于缓存压缩结果 + gzip_http_version 1.1; #识别http协议的版本 + gzip_min_length 1k; #设置允许压缩的页面最小字节数,超过1k的文件会被压缩 + gzip_types application/javascript text/css; #对特定的MIME类型生效,js和css文件会被压缩 + + include /etc/nginx/conf.d/*.conf; + + server { + #nginx同时开启http和https + listen 80 default backlog=2048; + listen 443 ssl; + server_name ysf.djtlpay.com; + + ssl_certificate /ssl/1_ysf.djtlpay.com_bundle.crt; + ssl_certificate_key /ssl/2_ysf.djtlpay.com.key; + + location / { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $http_host; + proxy_cache_bypass $http_upgrade; + root /usr/share/nginx/html; + index index.html index.htm; + } + } +} +``` +一个完整的 docker compose 文件如下,包含了三个镜像以及一个桥接网络。 +``` +version: '3.0' + +services: + # 指定服务名称 + sqlserver: + # 指定服务使用的镜像 + image: mcr.microsoft.com/mssql/server + # 指定容器名称 + container_name: sqlserver + # 指定服务运行的端口 + ports: + - "1433" + # 指定容器中需要挂载的文件 + volumes: + - /data/sqlserver:/var/opt/mssql + # 挂断自动重新启动 + restart: always + environment: + - TZ=Asia/Shanghai + - SA_PASSWORD=mssql-MSSQL + - ACCEPT_EULA=Y + # 指定容器运行的用户为root + user: + root + # 指定服务名称 + redis: + # 指定服务使用的镜像 + image: redis + # 指定容器名称 + container_name: redis + # 指定服务运行的端口 + ports: + - 6379:6379 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + - /data/redis:/data + - /data/redis/redis.conf:/etc/redis.conf + # 挂断自动重新启动 + restart: always + # 指定容器执行命令 + command: redis-server /etc/redis.conf --requirepass xiujingredis. --appendonly yes + # 指定容器的环境变量 + environment: + - TZ=Asia/Shanghai # 设置容器时区与宿主机保持一致 + # 指定服务名称 + mongo: + # 指定服务使用的镜像 + image: mongo + # 指定容器名称 + container_name: mongo + # 指定服务运行的端口 + ports: + - 27017:27017 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + - /data/mongodb/db:/data/db + - /data/mongodb/configdb:/data/configdb + - /data/mongodb/initdb:/docker-entrypoint-initdb.d + # 挂断自动重新启动 + restart: always + # 指定容器的环境变量 + environment: + - TZ=Asia/Shanghai # 设置容器时区与宿主机保持一致 + - AUTH=yes + - MONGO_INITDB_ROOT_USERNAME=admin + - MONGO_INITDB_ROOT_PASSWORD=admin + nginx: + # 指定服务使用的镜像 + image: nginx + # 指定容器名称 + container_name: nginx + # 指定服务运行的端口 + ports: + - 80:80 + - 443:443 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + # 挂断自动重新启动 + restart: always + + AdminApiGateway.Host: + image: 'volosoft/microservice-demo-public-website-gateway:${TAG:-latest}' + build: + context: ../ + dockerfile: micro/gateways/AdminApiGateway.Host/Dockerfile + depends_on: + - sqlserver + - redis + - mongo + + Base.IdentityServer: + image: 'Base.IdentityServer:${TAG:-latest}' + build: + context: ../ + dockerfile: micro/modules/base/host/Base.IdentityServer/Dockerfile + depends_on: + - sqlserver + - redis + - mongo + - AdminApiGateway.Host + + Base.HttpApi.Host: + image: 'Base.HttpApi.Host:${TAG:-latest}' + build: + context: ../ + dockerfile: micro/modules/base/host/Base.HttpApi.Host/Dockerfile + depends_on: + - sqlserver + - redis + - mongo + - AdminApiGateway.Host + - Base.IdentityServer +``` + +这里需要注意,所有有用到镜像间的通信的地方,我们都需要使用镜像名进行指代,例如上面的 nginx 的配置文件中,我们需要将监听的地址改为镜像名称,以及,我们需要修改程序的数据库访问字符串的服务器地址 + +#### 4、发布部署程序 + +  当我们构建好 docker compose 文件后就可以把整个文件上传到服务器上进行构建 docker 镜像了。这里我将所有的部署文件放在服务器的 /data/wwwroot/micro/ 路径下,这时我们就可以通过 docker compose 命令进行镜像构建。 + +  定位到部署文件在的位置,我们可以直接使用下面的命令进行镜像的(重新)构建,启动,并链接一个服务相关的容器,整个过程都会在后台运行,如果你希望看到整个过程的话,你可以去掉 -d 参数。 + +执行镜像构建,启动 +``` +docker-compose up -d +``` +  当 up 命令执行完成后,我们就可以通过 ps 命令查看正在运行的容器,若有的容器并没有运行起来,则可以使用 logs 查看容器的运行日志从而进行排错。 + + 查看所有正在运行的容器 +``` +docker-compose ps +``` +显示容器运行日志 +``` +docker-compose logs +``` +### 三、总结 + +    本章主要是介绍了如何通过 docker 容器,完整的部署一个可实际使用的 .NET Core 的单体应用,相比于之前通过 Linux 部署 .NET Core 应用,可以看到整个步骤少了很多,也简单很多。文中涉及到了一些 docker 的命令,如果你之前并没有接触过 docker 的话,可能需要你进一步的了解。当我们将程序打包成一个镜像之后,你完全可以将镜像上传到私有镜像仓库中,或是直接打包成镜像的压缩文件,这样,当需要切换部署环境时,只需要获取到这个镜像之后即可快速完成部署,相比之前,极大的方便了我们的工作。 diff --git a/blog/en-us/sql/mybatis.md b/blog/en-us/sql/mybatis.md new file mode 100644 index 0000000..e0f3f56 --- /dev/null +++ b/blog/en-us/sql/mybatis.md @@ -0,0 +1,392 @@ +# Mybatis使用心德 + +## 什么是Mybatis? + +1. Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。程序员直接编写原生态sql,可以严格控制sql执行性能,灵活度高。 + +2. MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。 + +3. 通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和 statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)。 + +## Mybaits的优点: + +1. 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。 + +2. 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接; + +3. 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。 + +4. 能够与Spring很好的集成; + +5. 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。 + +## MyBatis框架的缺点: + +1. SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。 + +2. SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。 + +## MyBatis框架适用场合: + +1. MyBatis专注于SQL本身,是一个足够灵活的DAO层解决方案。 + +2. 对性能的要求很高,或者需求变化较多的项目,如互联网项目,MyBatis将是不错的选择。 + +## MyBatis与Hibernate有哪些不同? + +1. Mybatis和hibernate不同,它不完全是一个ORM框架,因为MyBatis需要程序员自己编写Sql语句。 + +2. Mybatis直接编写原生态sql,可以严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频繁,一但需求变化要求迅速输出成果。但是灵活的前提是mybatis无法做到数据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套sql映射文件,工作量大。 + +3. Hibernate对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件,如果用hibernate开发可以节省很多代码,提高效率。 + +## #{}和${}的区别是什么? + +1. #{}是预编译处理,${}是字符串替换。 + +2. Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值; + +3. Mybatis在处理${}时,就是把${}替换成变量的值。 + +4. 使用#{}可以有效的防止SQL注入,提高系统安全性。 + +## 当实体类中的属性名和表中的字段名不一样 ,怎么办 ? + +第1种:通过在查询的sql语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。 + +``` xml + +``` + +第2种:通过 来映射字段名和实体类属性名的一一对应的关系。 + +``` xml + + + + + + + + +``` + +## 模糊查询like语句该怎么写? +第1种:在Java代码中添加sql通配符。 +``` xml +string wildcardname = “%smi%”; +list names = mapper.selectlike(wildcardname); + +``` +第2种:在sql语句中拼接通配符,会引起sql注入 +``` xml +string wildcardname = “smi”; +list names = mapper.selectlike(wildcardname); + +``` + +## 通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗? + +Dao接口即Mapper接口。接口的全限名,就是映射文件中的namespace的值;接口的方法名,就是映射文件中Mapper的Statement的id值;接口方法内的参数,就是传递给sql的参数。 + +Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MapperStatement。在Mybatis中,每一个 ``` + select * fromuser_user_t whereuser_name = #{0} anduser_area=#{1} + +``` +2. 第二种:使用 @param 注解: + +``` java + +public interface usermapper { + user selectuser(@param(“username”) string username,@param(“hashedpassword”) string hashedpassword); +} +``` +然后,就可以在xml像下面这样使用(推荐封装为一个map,作为单个参数传递给mapper): + +``` xml + +``` + +3. 第三种:多个参数封装成map + +``` java +try { + //映射文件的命名空间.SQL片段的ID,就可以调用对应的映射文件中的SQL + //由于我们的参数超过了两个,而方法中只有一个Object参数收集,因此我们使用Map集合来装载我们的参数 + Map < String, Object > map = new HashMap(); + map.put("start", start); + map.put("end", end); + return sqlSession.selectList("StudentID.pagination", map); +} catch (Exception e) { + e.printStackTrace(); + sqlSession.rollback(); + throw e; +} finally { + MybatisUtil.closeSqlSession(); +``` + +## Mybatis动态sql有什么用?执行原理?有哪些动态sql? + +Mybatis动态sql可以在Xml映射文件内,以标签的形式编写动态sql,执行原理是根据表达式的值 完成逻辑判断并动态拼接sql的功能。 + +Mybatis提供了9种动态sql标签: + +trim|where|set|foreach|if|choose|when|otherwise|bind。 + +## Xml映射文件中,除了常见的select|insert|updae|delete标签之外,还有哪些标签? + +``````,加上动态sql的9个标签,其中 ``````为sql片段标签,通过 ``````标签引入sql片段, ``````为不支持自增的主键生成策略标签。 + +## Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复? + +不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复; + +原因就是namespace+id是作为Map 的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。 + +## 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里? + +Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 + +## 一对一、一对多的关联查询 ? + +``` xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +## MyBatis实现一对一有几种方式?具体怎么操作的? + +有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成; + +嵌套查询是先查一个表,根据这个表里面的结果的 外键id,去再另外一个表里面查询数据,也是通过association配置,但另外一个表的查询通过select属性配置。 + +## MyBatis实现一对多有几种方式,怎么操作的? + +有联合查询和嵌套查询。联合查询是几个表联合查询,只查询一次,通过在resultMap里面的collection节点配置一对多的类就可以完成;嵌套查询是先查一个表,根据这个表里面的 结果的外键id,去再另外一个表里面查询数据,也是通过配置collection,但另外一个表的查询通过select节点配置。 + +## Mybatis是否支持延迟加载?如果支持,它的实现原理是什么? + +Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 + +它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。 + +当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。 + +## Mybatis的一级、二级缓存: + +1)一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。 + +2)二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ; + +3)对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。 + +## 什么是MyBatis的接口绑定?有哪些实现方式? + +接口绑定,就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置。 + +接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加上 @Select、@Update等注解,里面包含Sql语句来绑定;另外一种就是通过xml里面写SQL来绑定, 在这种情况下,要指定xml映射文件里面的namespace必须为接口的全路径名。当Sql语句比较简单时候,用注解绑定, 当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多。 + +## 使用MyBatis的mapper接口调用时有哪些要求? + +1、Mapper接口方法名和mapper.xml中定义的每个sql的id相同; +2、Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同; +3、Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同; +4、Mapper.xml文件中的namespace即是mapper接口的类路径。 + +## Mapper编写有哪几种方式? + +接口实现类继承SqlSessionDaoSupport:使用此种方法需要编写mapper接口,mapper接口实现类、mapper.xml文件。 + +1、在sqlMapConfig.xml中配置mapper.xml的位置 + +``` xml + + + + +``` + +2、定义mapper接口 + +3、实现类集成SqlSessionDaoSupportmapper方法中可以this.getSqlSession()进行数据增删改查。 + +4、spring 配置 + +``` xml + + + +``` + +第二种:使用 org.mybatis.spring.mapper.MapperFactoryBean: +1、在sqlMapConfig.xml中配置mapper.xml的位置,如果mapper.xml和mappre接口的名称相同且在同一个目录,这里可以不用配置 +``` xml + + + + +``` + +2、定义mapper接口: + + mapper.xml中的namespace为mapper接口的地址 + mapper接口中的方法名和mapper.xml中的定义的statement的id保持一致 + Spring中定义 + +``` xml + + + + +``` + +第三种:使用mapper扫描器: + +1、mapper.xml文件编写: + +mapper.xml中的namespace为mapper接口的地址;mapper接口中的方法名和mapper.xml中的定义的statement的id保持一致;如果将mapper.xml和mapper接口的名称保持一致则不用在sqlMapConfig.xml中进行配置。 + +2、定义mapper接口: + +注意mapper.xml的文件名和mapper的接口名称保持一致,且放在同一个目录 + +3、配置mapper扫描器: + +``` xml + + + + +``` +4、使用扫描器后从spring容器中获取mapper的实现对象。 + +## 简述Mybatis的插件运行原理,以及如何编写一个插件。 + +Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。 + +编写插件:实现Mybatis的Interceptor接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。 diff --git a/blog/en-us/sql/mysql_backups.md b/blog/en-us/sql/mysql_backups.md new file mode 100644 index 0000000..4f40692 --- /dev/null +++ b/blog/en-us/sql/mysql_backups.md @@ -0,0 +1,382 @@ +# 数据备份与恢复 + + + +## 一、备份简介 + +### 2.1 备份分类 + +按照不同的思考维度,通常将数据库的备份分为以下几类: + +**物理备份 与 逻辑备份** + ++ 物理备份:备份的是完整的数据库目录和数据文件。采用该模式会进行大量的 IO 操作,但不含任何逻辑转换,因此备份和恢复速度通常都比较快。 ++ 逻辑备份:通过数据库结构和内容信息来进行备份。因为要执行逻辑转换,因此其速度较慢,并且在以文本格式保存时,其输出文件的大小大于物理备份。逻辑备份的还原的粒度可以从服务器级别(所有数据库)精确到具体表,但备份不会包括日志文件、配置文件等与数据库无关的内容。 + +**全量备份 与 增量备份** + ++ 全量备份:备份服务器在给定时间点上的所有数据。 ++ 增量备份:备份在给定时间跨度内(从一个时间点到另一个时间点)对数据所做的更改。 + +**在线备份 与 离线备份** + ++ 在线备份:数据库服务在运行状态下进行备份。此时其他客户端依旧可以连接到数据库,但为了保证数据的一致性,在备份期间可能会对数据进行加锁,此时客户端的访问依然会受限。 ++ 离线备份:在数据库服务停机状态下进行备份。此备份过程简单,但由于无法提供对外服务,通常会对业务造成比较大的影响。 + +### 2.2 备份工具 + +MySQL 支持的备份工具有很多种,这里列出常用的三种: + ++ **mysqldump**:这是 MySQL 自带的备份工具,其采用的备份方式是逻辑备份,支持全库备份、单库备份、单表备份。由于其采用的是逻辑备份,所以生成的备份文件比物理备份的大,且所需恢复时间也比较长。 ++ **mysqlpump**:这是 MySQL 5.7 之后新增的备份工具,在 mysqldump 的基础上进行了功能的扩展,支持多线程备份,支持对备份文件进行压缩,能够提高备份的速度和降低备份文件所需的储存空间。 ++ **Xtrabackup**:这是 Percona 公司开发的实时热备工具,能够在不停机的情况下进行快速可靠的热备份,并且备份期间不会间断数据库事务的处理。它支持数据的全备和增备,并且由于其采用的是物理备份的方式,所以恢复速度比较快。 + +## 二、mysqldump + +### 2.1 常用参数 + +mysqldump 的基本语法如下: + +```shell +# 备份数据库或数据库中的指定表 +mysqldump [options] db_name [tbl_name ...] +# 备份多个指定的数据库 +mysqldump [options] --databases db_name ... +# 备份当前数据库实例中的所有表 +mysqldump [options] --all-databases +``` + +options 代表可选操作,常用的可选参数如下: + ++ **--host=host_name, -h host_name** + + 指定服务器地址。 + ++ **--user=user_name, -u user_name** + + 指定用户名。 + ++ **--password[=password], -p[password]** + + 指定密码。通常无需在命令行中明文指定,按照提示输入即可。 + ++ **--default-character-set=charset_name** + + 导出文本使用的字符集,默认为 utf8。 + ++ **--events, -E** + + 备份包含数据库中的事件。 + ++ **--ignore-table=db_name.tbl_name** + + 不需要进行备份的表,必须使用数据库和表名来共同指定。也可以作用于视图。 + ++ **--routines, -R** + + 备份包含数据库中的存储过程和自定义函数。 + ++ **--triggers** + + 备份包含数据库中的触发器。 + ++ **--where='where_condition', -w 'where_condition'** + + 在对单表进行导出时候,可以指定过滤条件,例如指定用户名 `--where="user='jimf'"` 或用户范围 `-w"userid>1"` 。 + ++ **--lock-all-tables, -x** + + 锁定所有数据库中的所有表,从而保证备份数据的一致性。此选项自动关闭 `--single-transaction` 和 `--lock-tables`。 + ++ **--lock-tables, -l** + + 锁定当前数据库中所有表,能够保证当前数据库中表的一致性,但不能保证全局的一致性。 + ++ **--single-transaction** + + 此选项会将事务隔离模式设置为 REPEATABLE READ 并开启一个事务,从而保证备份数据的一致性。主要用于事务表,如 InnoDB 表。 但是此时仍然不能在备份表上执行 ALTER TABLE, CREATE TABLE, DROP TABLE, RENAME TABLE, TRUNCATE TABLE 等操作,因为 REPEATABLE READ 并不能隔离这些操作。 + + 另外需要注意的是 `--single-transaction` 选项与 `--lock-tables` 选项是互斥的,因为 LOCK TABLES 会导致任何正在挂起的事务被隐式提交。转储大表时,可以将 `--single-transaction` 选项与 `--quick` 选项组合使用 。 + ++ **--quick, -q** + + 主要用于备份大表。它强制 mysqldump 一次只从服务器检索一行数据,避免一次检索所有行而导致缓存溢出。 + ++ **--flush-logs, -F** + + 在开始备份前刷新 MySQL 的日志文件。此选项需要 RELOAD 权限。如果此选项与 `--all-databases` 配合使用,则会在每个数据库开始备份前都刷新一次日志。如果配合 `--lock-all-tables`,`--master-data` 或 `--single-transaction` 使用,则只会在锁定所有表或者开启事务时刷新一次。 + ++ **--master-data[=*value*]** + + 可以通过配置此参数来控制生成的备份文件是否包含 CHANGE MASTER 语句,该语句中包含了当前时间点二进制日志的信息。该选项有两个可选值:1 和 2 ,设置为 1 时 CHANGE MASTER 语句正常生成,设置为 2 时以注释的方式生成。`--master-data` 选项还会自动关闭 `--lock-tables` 选项,而且如果你没有指定 `--single-transaction` 选项,那么它还会启用 `--lock-all-tables` 选项,在这种情况下,会在备份开始时短暂内获取全局读锁。 + +### 2.2 全量备份 + +mysqldump 的全量备份与恢复的操作比较简单,示例如下: + +```shell +# 备份雇员库 +mysqldump -uroot -p --databases employees > employees_bak.sql + +# 恢复雇员库 +mysql -uroot -p < employees_bak.sql +``` + +单表备份: + +```shell +# 备份雇员库中的职位表 +mysqldump -uroot -p --single-transaction employees titles > titles_bak.sql + +# 恢复雇员库中的职位表 +mysql> use employees; +mysql> source /root/mysqldata/titles_bak.sql; +``` + +### 2.3 增量备份 + +mysqldump 本身并不能直接进行增量备份,需要通过分析二进制日志的方式来完成。具体示例如下: + +#### 1. 基础全备 + +1.先执行一次全备作为基础,这里以单表备份为例,需要用到上文提到的 `--master-data` 参数,语句如下: + +```shell +mysqldump -uroot -p --master-data=2 --flush-logs employees titles > titles_bak.sql +``` + +使用 more 命令查看备份文件,此时可以在文件开头看到 CHANGE MASTER 语句,语句中包含了二进制日志的名称和偏移量信息,具体如下: + +```sql +-- CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000004', MASTER_LOG_POS=155; +``` + +#### 2. 增量恢复 + +对表内容进行任意修改,然后通过分析二进制日志文件来生成增量备份的脚本文件,示例如下: + +```shell +mysqlbinlog --start-position=155 \ +--database=employees ${MYSQL_HOME}/data/mysql-bin.000004 > titles_inr_bak_01.sql +``` + +需要注意的是,在实际生产环境中,可能在全量备份后与增量备份前的时间间隔里生成了多份二进制文件,此时需要对每一个二进制文件都执行相同的命令: + +```shell +mysqlbinlog --database=employees ${MYSQL_HOME}/data/mysql-bin.000005 > titles_inr_bak_02.sql +mysqlbinlog --database=employees ${MYSQL_HOME}/data/mysql-bin.000006 > titles_inr_bak_03.sql +..... +``` + +之后将全备脚本 ( titles_bak.sql ),以及所有的增备脚本 ( inr_01.sql,inr_02.sql .... ) 通过 source 命令导入即可,这样就完成了全量 + 增量的恢复。 + +## 三、mysqlpump + +### 3.1 功能优势 + +mysqlpump 在 mysqldump 的基础上进行了扩展增强,其主要的优点如下: + +- 能够并行处理数据库及其中的对象,从而可以加快备份进程; + +- 能够更好地控制数据库及数据库对象(表,存储过程,用户帐户等); + +- 能够直接对备份文件进行压缩; + +- 备份时能够显示进度指标(估计值); +- 备份用户时生成的是 CREATE USER 与 GRANT 语句,而不是像 mysqldump 一样备份成数据,可以方便用户按需恢复。 + +### 3.2 常用参数 + +mysqlpump 的使用和 mysqldump 基本一致,这里不再进行赘述。以下主要介绍部分新增的可选项,具体如下: + ++ **--default-parallelism=N** + + 每个并行处理队列的默认线程数。默认值为 2。 + ++ **--parallel-schemas=[N:]db_list** + + 用于并行备份多个数据库:db_list 是一个或多个以逗号分隔的数据库名称列表;N 为使用的线程数,如果没有设置,则使用 `--default-parallelism` 参数的值。 + ++ **--users** + + 将用户信息备份为 CREATE USER 语句和 GRANT 语句 。如果想要只备份用户信息,则可以使用下面的命令: + + ```shell + mysqlpump --exclude-databases=% --users + ``` + ++ **--compress-output=algorithm** + + 默认情况下,mysqlpump 不对备份文件进行压缩。可以使用该选项指定压缩格式,当前支持 LZ4 和 ZLIB 两种格式。需要注意的是压缩后的文件可以占用更少的存储空间,但是却不能直接用于备份恢复,需要先进行解压,具体如下: + + ```shell + # 采用lz4算法进行压缩 + mysqlpump --compress-output=LZ4 > dump.lz4 + # 恢复前需要先进行解压 + lz4_decompress input_file output_file + + # 采用ZLIB算法进行压缩 + mysqlpump --compress-output=ZLIB > dump.zlib + zlib_decompress input_file output_file + ``` + + MySQL 发行版自带了上面两个压缩工具,不需要进行额外安装。以上就是 mysqlpump 新增的部分常用参数,完整参数可以参考官方文档:[mysqlpump — A Database Backup Program](https://dev.mysql.com/doc/refman/8.0/en/mysqlpump.html#option_mysqlpump_compress-output) + +## 四、Xtrabackup + +### 4.1 在线安装 + +Xtrabackup 可以直接使用 yum 命令进行安装,这里我的 MySQL 为 8.0 ,对应安装的 Xtrabackup 也为 8.0,命令如下: + +```shell +# 安装Percona yum 源 +yum install https://repo.percona.com/yum/percona-release-latest.noarch.rpm + +# 安装 +yum install percona-xtrabackup-80 +``` + +### 4.2 全量备份 + +全量备份的具体步骤如下: + +#### 1. 创建备份 + +Xtrabackup 全量备份的基本语句如下,可以使用 target-dir 指明备份文件的存储位置,parallel 则是指明操作的并行度: + +```shell +xtrabackup --backup --user=root --password --parallel=3 --target-dir=/data/backups/ +``` + +以上进行的是整个数据库实例的备份,如果需要备份指定数据库,则可以使用 --databases 进行指定。 + +另外一个容易出现的异常是:Xtrabackup 在进行备份时,默认会去 `/var/lib/mysql/mysql.sock` 文件里获取数据库的 socket 信息,如果你修改了数据库的 socket 配置,则需要使用 --socket 参数进行重新指定,否则会抛出找不到连接的异常。备份完整后需要立即执行的另外一个操作是 prepare (准备备份)。 + +#### 2. 准备备份 + +由于备份是将所有物理库表等文件复制到备份目录,而整个过程需要持续一段时间,此时备份的数据中就可能会包含尚未提交的事务或已经提交但尚未同步至数据文件中的事务,最终导致备份结果处于不一致状态。此时需要进行 prepare 操作来回滚未提交的事务及同步已经提交的事务至数据文件,从而使得整体达到一致性状态。命令如下: + +```shell +xtrabackup --prepare --target-dir=/data/backups/ +``` + +需要特别注意的在该阶段不要随意中断 xtrabackup 进程,因为这可能会导致数据文件损坏,备份将无法使用。 + +#### 3. 恢复备份 + +由于 xtrabackup 执行的是物理备份,所以想要进行恢复,必须先要停止 MySQL 服务。同时这里我们可以删除 MySQL 的数据目录来模拟数据丢失的情况,之后使用以下命令将备份文件拷贝到 MySQL 的数据目录下: + +```shell +# 模拟数据异常丢失 +rm -rf /usr/app/mysql-8.0.17/data/* + +# 将备份文件拷贝到 data 目录下 +xtrabackup --copy-back --target-dir=/data/backups/ +``` + +copy-back 命令只需要指定备份文件的位置,不需要指定 MySQL 数据目录的位置,因为 Xtrabackup 会自动从 `/etc/my.cnf` 上获取 MySQL 的相关信息,包括数据目录的位置。如果不需要保留备份文件,可以直接使用 `--move-back` 命令,代表直接将备份文件移动到数据目录下。此时数据目录的所有者通常为执行命令的用户,需要更改为 mysql 用户,命令如下: + +```shell +chown -R mysql:mysql /usr/app/mysql-8.0.18/data +``` + +再次启动即可完成备份恢复。 + +### 4.3 增量备份 + +使用 Xtrabackup 进行增量备份时,每一次增量备份都需要以上一次的备份为基础,之后再将增量备份运用到第一次全备之上,从而完成备份。具体操作如下: + +#### 1. 创建备份 + +这里首先创建一个全备作为基础: + +```shell +xtrabackup --backup --user=root --password=xiujingmysql. --host=172.17.0.4 --port=13306 --datadir=/data/mysql/data --parallel-3 --target-dir=/data/backups +``` + +之后修改库中任意数据,然后进行第一次增量备份,此时需要使用 `incremental-basedir` 指定基础目录为全备目录: + +```shell +xtrabackup --user=root --password --backup --target-dir=/data/backups/inc1 \ +--incremental-basedir=/data/backups/base +``` + +再修改库中任意数据,然后进行第二次增量备份,此时需要使用 `incremental-basedir` 指定基础目录为上一次增备目录: + +```shell +xtrabackup --user=root --password --backup --target-dir=/data/backups/inc2 \ +--incremental-basedir=/data/backups/inc1 +``` + +#### 2. 准备备份 + +准备基础备份: + +```shell +xtrabackup --prepare --apply-log-only --target-dir=/data/backups/base +``` + +将第一次备份作用于全备数据: + +```shell +xtrabackup --prepare --apply-log-only --target-dir=/data/backups/base \ +--incremental-dir=/data/backups/inc1 +``` + +将第二次备份作用于全备数据: + +```shell +xtrabackup --prepare --target-dir=/data/backups/base \ +--incremental-dir=/data/backups/inc2 +``` + +在准备备份时候,除了最后一次增备外,其余的准备命令都需要加上 `--apply-log-only` 选项来阻止事务的回滚,因为备份时未提交的事务可能正在进行,并可能在下一次增量备份中提交,如果不进行阻止,那么增量备份将没有任何意义。 + +#### 3. 恢复备份 + +恢复备份和全量备份时相同,只需要最终准备好的全备数据复制到 MySQL 的数据目录下即可: + +```shell +xtrabackup --copy-back --target-dir=/data/backups/base +# 必须修改文件权限,否则无法启动 +chown -R mysql:mysql /usr/app/mysql-8.0.17/data +``` + +此时增量备份就已经完成。需要说明的是:按照上面的情况,如果第二次备份之后发生了宕机,那么第二次备份后到宕机前的数据依然没法通过 Xtrabackup 进行恢复,此时就只能采用上面介绍的分析二进制日志的恢复方法。由此可以看出,无论是采用何种备份方式,二进制日志都是非常重要的,因此最好对其进行实时备份。 + +## 五、二进制日志的备份 + +想要备份二进制日志文件,可以通过定时执行 cp 或 scp 等命令来实现,也可以通过 mysqlbinlog 自带的功能来实现远程备份,将远程服务器上的二进制日志文件复制到本机,命令如下: + +```shell +mysqlbinlog --read-from-remote-server --raw --stop-never \ +--host=主机名 --port=3306 \ +--user=用户名 --password=密码 初始复制时的日志文件名 +``` + +需要注意的是这里的用户必须具有 replication slave 权限,因为上述命令本质上是模拟主从复制架构下,从节点通过 IO 线程不断去获取主节点的二进制日志,从而达到备份的目的。 + + + +## 参考资料 + ++ [Chapter 7 Backup and Recovery](https://dev.mysql.com/doc/refman/8.0/en/backup-and-recovery.html) ++ [mysqldump — A Database Backup Program](https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html) ++ [mysqlpump — A Database Backup Program](https://dev.mysql.com/doc/refman/8.0/en/mysqlpump.html) ++ [Percona XtraBackup - Documentation](https://www.percona.com/doc/percona-xtrabackup/LATEST/index.html) diff --git a/blog/en-us/tool/minio.md b/blog/en-us/tool/minio.md new file mode 100644 index 0000000..a1db8db --- /dev/null +++ b/blog/en-us/tool/minio.md @@ -0,0 +1,156 @@ +# MinIO 搭建使用 + +## MinIO简介 + +MinIO 是一款基于Go语言的高性能对象存储服务,在Github上已有19K+Star。它采用了Apache License v2.0开源协议,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等。 本文将使用 MinIO 来自建一个对象存储服务用于存储图片。 + +## 安装及部署 + +> MinIO的安装方式有很多,这里我们使用它在Docker环境下的安装方式。 + +* 下载MinIO的Docker镜像: + +``` shell +docker pull minio/minio +``` + +* 在Docker容器中运行MinIO,这里我们将MiniIO的数据和配置文件夹挂在到宿主机上: + +``` shell +docker run -p 9000:9000 --name minio \ + --restart=always \ + -v /etc/localtime:/etc/localtime \ + -v /data/minio/data:/data \ + -v /data/minio/config:/root/.minio \ + -d minio/minio server /data +``` + +* 运行成功后,访问该地址来登录并使用MinIO,默认Access Key和Secret都是minioadmin:http://localhost:9000 + +![minio](./../img/minio1.png) + +## 上传文件及使用 + +>通过使用MinIO的网页端即可完成文件的上传下载功能,下面我们以图片上传下载为例来演示下该功能。 + +* 在存储文件之前,我们需要新建一个存储桶: + +![minio](./../img/minio2.png) + +* 存储桶创建完成后,通过上传按钮可以上传文件,这里我们上传一张图片: + +![minio](./../img/minio3.png) + +* 图片上传完成后,我们可以通过拷贝链接按钮来获取图片访问路径,但是这只是个临时的访问路径: + +![minio](./../img/minio4.png) + +* 要想获取一个永久的访问路径,需要修改存储桶的访问策略,我们可以点击存储桶右上角的编辑策略按钮来修改访问策略; + +![minio](./../img/minio5.png) + +* 这里有三种访问策略可以选择,一种只读、一种只写、一种可读可写,这里我们选择只读即可,但是需要注意的是,访问前缀需要设置为*.*,否则会无法访问; + +![minio](./../img/minio6.png) + +* 设置完成后,我们只需要通过拷贝链接中的前一串路径即可永久访问该文件; + +## MinIO客户端的使用 + +>虽然MinIO的网页端管理已经很方便了,但是官网还是给我们提供了基于命令行的客户端MinIO Client(简称mc),下面我们来讲讲它的使用方法。 + +### 常用命令 + +> 下面我们先来熟悉下mc的命令,这些命令和Linux中的命令有很多相似之处。 + +| 命令 | 作用 | +| ------- | --------------------------------------------- | +| ls | 列出文件和文件夹 | +| mb | 创建一个存储桶或一个文件夹 | +| cat | 显示文件和对象内容 | +| pipe | 将一个STDIN重定向到一个对象或者文件或者STDOUT | +| share | 生成用于共享的URL | +| cp | 拷贝文件和对象 | +| mirror | 给存储桶和文件夹做镜像 | +| find | 基于参数查找文件 | +| diff | 对两个文件夹或者存储桶比较差异 | +| rm | 删除文件和对象 | +| events | 管理对象通知 | +| watch | 监听文件和对象的事件 | +| policy | 管理访问策略 | +| session | 为cp命令管理保存的会话 | +| config | 管理mc配置文件 | +| update | 检查软件更新 | +| version | 输出版本信息 | + +### 安装及配置 + +> 由于MinIO服务端中并没有自带客户端,所以我们需要安装配置完客户端后才能使用,这里以Docker环境下的安装为例。 + +- 下载MinIO Client 的Docker镜像: + +```bash +docker pull minio/mc +``` + +- 在Docker容器中运行mc: + +```bash +docker run -it --entrypoint=/bin/sh minio/mc +``` + +- 运行完成后我们需要进行配置,将我们自己的MinIO服务配置到客户端上去,配置的格式如下: + +```bash +mc config host add +``` + +- 对于我们的MinIO服务可以这样配置: + +```bash +mc config host add minio http://localhost:9000 minioadmin minioadmin S3v4 +``` + +### 常用操作 + +- 查看存储桶和查看存储桶中存在的文件: + +```bash +# 查看存储桶 +mc ls minio +# 查看存储桶中存在的文件 +mc ls minio/blog +``` + +![minio](./../img/minio7.png) + +- 创建一个名为`test`的存储桶: + +```bash +mc mb minio/test +``` + +- 共享`avatar.png`文件的下载路径: + +```bash +mc share download minio/blog/avatar.png +``` + +- 查找`blog`存储桶中的png文件: + +```bash +mc find minio/blog --name "*.png" +``` + +- 设置`test`存储桶的访问权限为`只读`: + +```bash +# 目前可以设置这四种权限:none, download, upload, public +mc policy set download minio/test/ +# 查看存储桶当前权限 +mc policy list minio/test/ +``` + +## 参考资料 + +详细了解MinIO可以参考官方文档:https://docs.min.io/cn/minio-quickstart-guide.html diff --git a/blog/en-us/web/javascript.md b/blog/en-us/web/javascript.md new file mode 100644 index 0000000..89be9f1 --- /dev/null +++ b/blog/en-us/web/javascript.md @@ -0,0 +1,841 @@ +# JavaScript 基础 + + + +## 一、概念简介 + +JavaScript 是一种专为与网页交互而设计的脚本语言,由以下三个部分组成: + +- **ECMAScript**:由 ECMA-262 定义,提供核心语言功能; +- **文档对象模型 (DOM)**:提供访问和操作网页内容的方法和接口; +- **浏览器对象模型 (BOM)**:提供与浏览器交互的方法和接口。 + +ECMAScript 提供了语言的核心功能,它定义了以下七种数据类型: + +- **六种基本数据类型**:`Undefined`,`Null`,`Boolean`,`Number`,`String`,`Symbol` ( ES 6新增 ); +- **一种引用数据类型**:统称为 Object 类型;具体又细分为 `Object`,`Array`,`Date`,`RegExp`,`Function` 等类型。另外和 Java 语言类似,对于布尔,数值,字符串等基本类型,分别存在其对应的包装类型 Boolean,Number,String,但通常我们并不会使用到这些包装类型,只需要使用其基本类型即可。 + +## 二、基本类型 + +### 2.1 数值类型 + +**1. 进制数值** + +ECMAScript 中的 Number 支持以下三种常用进制: + ++ **十进制**:正常数值就是十进制; ++ **八进制**:八进制字面值的第一位必须是零(0),然后是八进制数字序列(0~7); ++ **十六进制**:十六进制字面值的前两位必须是 0x,后跟任意的十六进制数字(0~9 及 A~F)。 + +```javascript +console.log(56); // 56 +console.log(070); // 56 +console.log(0x38); // 56 +``` + +**2. 浮点数值** + +ECMAScript 的数值类型同样支持浮点数,但是由于保存浮点数值需要的内存空间是保存整数值的两倍,因此 ECMAScript 会尽量将浮点数值转换为整数值存储: + +```javascript +var a = 10.0; +console.log(a); // 10 +``` + +和其他语言类似,浮点数中的数值也是不精准的,示例如下: + +```javascript +var a = 0.1; var b = 0.2; + +a + b ; // 0.30000000000000004 +a+b === 0.3 ; // false +``` + +如果想要对浮点数进行精确计算,可以使用 [decimal.js](https://github.com/MikeMcl/decimal.js) ,[ math.js](https://github.com/josdejong/mathjs) 等第三方库。 + +**3. 科学计数法** + +ECMAScript 支持使用科学计数法来表达数值: + +```javascript +8e-2 // 0.08 +8e2 // 800 +``` + +**4. parseInt() \ parseFloat()** + +parseInt 可以用于解析字符串并返回整数,parseFloat 用于解析字符串并返回浮点数: + +```javascript +parseInt("56"); // 56 +parseInt("0x38", 16); // 56 支持使用第二个参数来表示转换的进制 +parseInt("56.6"); // 56 + +parseFloat("12.2"); // 12.2 + +parseInt("blue"); // NaN NaN用于表示一个本来要返回数值的操作却未返回数值的情况 +``` + +**5. toFixed()** + + toFixed 用于保留指定位数的小数,但需要注意的是其四舍五入的行为是不确定的: + +```javascript +1.35.toFixed(1) // 1.4 正确 +1.335.toFixed(2) // 1.33 错误 +1.3335.toFixed(3) // 1.333 错误 +1.33335.toFixed(4) // 1.3334 正确 +1.333335.toFixed(5) // 1.33333 错误 +1.3333335.toFixed(6) // 1.333333 错误 +``` + +想要解决这个问题,需要重写 toFixed 方法并通过判断最后一位是否大于或等于5来决定是否需要进位,具体代码如下: + +```javascript +// toFixed兼容方法 +Number.prototype.toFixed = function(len){ + if(len>20 || len<0){ + throw new RangeError('toFixed() digits argument must be between 0 and 20'); + } + // .123转为0.123 + var number = Number(this); + if (isNaN(number) || number >= Math.pow(10, 21)) { + return number.toString(); + } + if (typeof (len) == 'undefined' || len == 0) { + return (Math.round(number)).toString(); + } + var result = number.toString(), + numberArr = result.split('.'); + + if(numberArr.length<2){ + //整数的情况 + return padNum(result); + } + var intNum = numberArr[0], //整数部分 + deciNum = numberArr[1],//小数部分 + lastNum = deciNum.substr(len, 1);//最后一个数字 + + if(deciNum.length == len){ + //需要截取的长度等于当前长度 + return result; + } + if(deciNum.length < len){ + //需要截取的长度大于当前长度 1.3.toFixed(2) + return padNum(result) + } + //需要截取的长度小于当前长度,需要判断最后一位数字 + result = intNum + '.' + deciNum.substr(0, len); + if(parseInt(lastNum, 10)>=5){ + //最后一位数字大于5,要进位 + var times = Math.pow(10, len); //需要放大的倍数 + var changedInt = Number(result.replace('.',''));//截取后转为整数 + changedInt++;//整数进位 + changedInt /= times;//整数转为小数,注:有可能还是整数 + result = padNum(changedInt+''); + } + return result; + //对数字末尾加0 + function padNum(num){ + var dotPos = num.indexOf('.'); + if(dotPos === -1){ + //整数的情况 + num += '.'; + for(var i = 0;i 参考自:[*js中小数四舍五入和浮点数的研究*](http://caibaojian.com/js-tofixed.html) + +### 2.2 字符类型 + +**1. 字符串表示** + +ECMAScript 支持使用双引号 ` " ` 或单引号 ` ' ` 来表示字符串,并且 ECMAScript 中的字符串是不可变的,也就是说,字符串一旦创建,它们的值就不能改变。要改变某个变量保存的字符串,首先要销毁原来的字符串,然后再用另一个包含新值的字符串填充该变量,示例如下: + +```javascript +var lang = "Java"; +/*程序会创建一个能容纳 10 个字符的新字符串用于填充"Java"和"Script",之后再销毁原有的字符串"Java"和"Script"*/ +lang = lang + "Script"; +``` + +**2. 转换为字符串** + +要把一个值转换为一个字符串有两种方式: + ++ 使用对象方法 **toString()** :大多数对象都具有这个方法,但需要注意的是 null 和 undefined 没有; ++ 使用转型函数 **String()** :使用该转型函数时,如果传入的值有 toString() 方法,则调用该方法并返回相应的结果;如果传入的值是 null,则返回 "null" ;如果传入值是 undefined,则返回 "undefined" 。 示例如下: + +```javascript +var a = null; +a.toString() // Uncaught TypeError: Cannot read property 'toString' of null +String(a) // "null" +``` + +**3. 常用的字符串操作** + ++ **concat()** :用于拼接一个或多个字符串; ++ **slice()** :用于截取字符串,接收两个参数,分别代表截取的开始位置和结束位置; ++ **substring()**:用于截取字符串,接收两个参数,分别代表截取的开始位置和结束位置; ++ **substr()** :用于截取字符串,接收两个参数,分别代表截取的开始位置和截取的长度; ++ **indexOf() \ lastIndexOf()** :均接收两个参数,分别代表待查找的字符串和查找的开始位置; ++ **trim()** :用于去除字符串前后的空格。 + +slice,substring,substr 等方法在传入正数参数时,其行为比较好预期,但传递参数是负数时,则具体的行为表现如下: + ++ **slice()** :会将传入的负值与字符串的长度相加; ++ **substring()** :方法会把所有负值参数都转换为 0 ; ++ **substr()** :会将第一个负值参数加上字符串的长度,如果传递了第二个参数且为负数时候,会将其转换为 0 。 + +```javascript +var stringValue = "hello world"; + +// 只接收一个参数时 +alert(stringValue.slice(3)); // "lo world" +alert(stringValue.substring(3)); // "lo world" +alert(stringValue.substr(3)); // "lo world" + +// 接收两个参数时候 +alert(stringValue.slice(3, 7)); // "lo w" +alert(stringValue.substring(3,7)); // "lo w" +alert(stringValue.substr(3, 7)); // "lo worl" + +// 当第一个参数为负值时 +alert(stringValue.slice(-3)); // "rld" 按照规则等价于: slice(8) +alert(stringValue.substring(-3)); // "hello world" 按照规则等价于: substring(0) +alert(stringValue.substr(-3)); // "rld" 按照规则等价于: substr(8) + +// 当第二个参数为负值时 +alert(stringValue.slice(3, -4)); // "lo w" 按照规则等价于: slice(3,7) +alert(stringValue.substring(3, -4)); // "hel" 按照规则等价于: substring(3,0) +alert(stringValue.substr(3, -4)); // ""(空字符串) 按照规则等价于: substr(3,0) +``` + +### 2.3 基本类型检测 + +JavaScript 是一种弱类型的语言,在声明变量时候可以不必指明其具体类型,而是由程序进行推断。如果想要知道变量具体属于哪一个基础类型,可以使用 **typeof** 关键字,它的返回情况如下: + +- **undefined**:如果对应的值未定义; +- **boolean**:如果对应的值是布尔值; +- **string**:如果对应的值是字符串; +- **number**:如果对应的值是数值; +- **object**:如果对应的值是对象或 null; +- **function**:如果对应的值是函数则返回 function。 函数在本质上也是对象,但是由于其一等公民的特殊地位,所以将其和其他普通对象进行区分是很有必要的,因此 typeof 对其检测时会返回 function ,而不是 object 。 + + +## 三、引用类型 + +### 3.1 Object 类型 + +创建 Object 实例有以下两种方式: + ++ 使用 new 操作符后跟着 Object 构造函数; ++ 使用对象字面量的方式。 + +```javascript +// 1. 使用new操作符 +var user = new Object(); +user.name = "heibaiying"; +user.age = 30; + +// 2. 使用对象字面量 +var user = { + name: "heibaiying", + age: 30 +}; +``` + +### 3.2 Array 类型 + +创建数组也有两种方式,基于构造函数的方式和基于对象字面量的方式: + +```javascript +// 1.基于构造函数的方式 +var colors = new Array(); +var colors = new Array(20); +var colors = new Array("red", "blue", "green"); + +// 2.基于对象字面量的方式 +var names = []; +var colors = ["red", "blue", "green"]; +``` + +数组的长度保存在其 length 属性中,和其他语言中的 length 属性不同,这个值是不是只读的,可以用其进行数组的截断操作或添加新的数据项,示例如下: + +```javascript +var colors = ["red", "blue", "green"]; + +colors.length = 2; // ["red", "blue"] +colors[colors.length] = "green"; // ["red", "blue", "green"] +colors[10] = "black"; // ["red", "blue", "green", empty × 7, "black"] +``` + +数组的其他常用方法如下: + +**1. 检测数组** + +```javascript +colors instanceof Array +Array.isArray(colors) +``` + +**2. 转换方法** + +```java +var colors = ["red", "blue", "green"]; + +colors.valueOf(); // [ 'red', 'blue', 'green' ] +colors; // [ 'red', 'blue', 'green' ] +colors.toString(); // red,blue,green +colors.join("|"); // red|blue|green +``` + +**3. 栈方法** + +ECMAScript 的数组提供了类似栈的特性,能够实现后进先出: + +```javascript +var colors = ["red", "blue", "green"]; + +colors.push("black"); // ["red", "blue", "green", "black"] +colors.pop() // "black" +colors // ["red", "blue", "green"] +``` + +**4. 队列方法** + +ECMAScript 的数组提供了类似栈的特性,能够实现先进先出: + +```javascript +colors.push("black","yellow"); // ["red", "blue", "green", "black", "yellow"] +colors.shift() // "red" +colors // ["blue", "green", "black", "yellow"] +``` + +**5. 重排序方法** + +```javascript +var values = [1, 2, 3, 4, 5]; +values.reverse(); +values // [5, 4, 3, 2, 1] + +// 支持传入排序函数进行自定义排序 +function compare(value1, value2) { + if (value1 < value2) { + return -1; + } else if (value1 > value2) { + return 1; + } else { + return 0; + } +} +values.sort(compare) +values // [1, 2, 3, 4, 5] +``` + +**6. 操作方法** + +**concat()** 用于拼接并返回新的数组: + +```javascript +var colors = ["red", "green", "blue"]; +var colors2 = colors.concat("yellow", ["black", "brown"]); + +colors // ["red", "green", "blue"] +colors2 // ["red", "green", "blue", "yellow", "black", "brown"] +``` + +**slice()** 用于截取数组并返回新的数组,它接收两个参数,分别代表截取的开始位置和结束位置,它是一个前开后闭的区间: + +```javascript +var colors = ["red", "green", "blue", "yellow", "purple"]; + +var colors2 = colors.slice(1); // ["green", "blue", "yellow", "purple"] +var colors3 = colors.slice(0,2); // ["red", "green"] +``` + +**splice()** 用于删除并在删除位置新增数据项,它接收任意个参数,其中第一个参数为删除的开始位置,第二个参数为删除多少个数据项,之后可以接任意个参数,用于表示待插入的数据项: + +```javascript +var colors = ["red", "green", "blue", "yellow"]; + +colors.splice(1,2) // 返回删除的数据项:["green", "blue"] +colors // ["red", "yellow"] + +colors.splice(1,0,"black","green") // [] +colors // ["red", "black", "green", "yellow"] +``` + +**7. 位置方法** + +**indexOf()** 和 **lastIndexOf()** 用于查找指定元素的 Index ,它们都接收两个参数:待查找项和查找的起点位置: + +```shell +var colors = ["red", "green", "blue", "yellow", "green", "blue"]; + +colors.indexOf("green"); // 1 +colors.indexOf("green", 3); // 4 +colors.lastIndexOf("green"); // 4 +colors.lastIndexOf("green", 3); // 1 +``` + +**8. 迭代方法** + +ECMAScript 5 提供了五个迭代方法: + +- **every()**:判断数组中的每个元素是否满足指定条件,如果全部满足则返回 true,否则返回 flase; +- **some()**:判断数组中的每个元素是否满足指定条件,只要有一个满足则返回 true,否则返回 flase; +- **filter()**:过滤并返回符合条件的元素组成的数组。 +- **forEach()**:对数组中的每一项运行给定函数。 +- **map()**:对数组中的每一项运行给定函数,并返回每次函数调用结果所组成的数组。 + +```javascript +var numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1]; + +numbers.every(function (value, index, array) { + return value > 3; +}); +// false + +numbers.some(function (value, index, array) { + return value > 3; +}); +// true + +numbers.filter(function (value, index, array) { + return value > 3; +}); +// [4, 5, 4] + +numbers.forEach(function (value, index, array) { + console.log(value); +}); + +numbers.map(function (value, index, array) { + return value * 10; +}); +// [10, 20, 30, 40, 50, 40, 30, 20, 10] +``` + +**9. 归并方法** + +ECMAScript 5 提供了两个归并数组的方法: **reduce()** 和 **reduceRight()** 。 它们都接收四个参数:前一个值、当前值、当前项的索引 和 数组本身,使用示例如下: + +```javascript +var values = [1, 2, 3, 4, 5]; +var sum01 = values.reduce(function (prev, cur, index, array) { + return prev + cur; +}); // 15 + +var sum02 = values.reduceRight(function (prev, cur, index, array) { + return prev + cur; +}); // 15 +``` + +### 3.3 Date 类型 + +创建一个日期对象,可以使用 new 操作符和 Date 构造函数: + +```java +var now = new Date(); +now.toLocaleString() // 2019-9-14 9:53:59 AM + +var date = new Date(2018, 7, 8, 8, 30, 20); +date.toLocaleString(); // 2018-8-8 8:30:20 AM +``` + +如果你只想知道当前时间的毫秒数,可以直接使用 Date 对象的静态方法: + +```javascript +Date.now() +1568426130593 +``` + +**1. 格式转换** + +- **toLocaleString()** :按照浏览器所在时区返回相应的日期格式; +- **toString()** :返回日期时间数据和的时区数据; +- **valueOf()** :返回日期的时间戳格式。 + +```javascript +var date = new Date(2018, 7, 8, 8, 30, 20); + +console.log(date.toLocaleString()); // 2018-8-8 8:30:20 AM +console.log(date.toString()); // Wed Aug 08 2018 08:30:20 GMT+0800 (GMT+08:00) +console.log(date.valueOf()); // 1533688220000 +``` + +由于 **valueOf()** 返回的是日期的时间戳格式,所以对于 date 对象,可以直接使用比较运算符来比较其大小: + +```javascript +var date01 = new Date(2018, 7, 8, 8, 30, 20); +var date02 = new Date(2016, 7, 8, 8, 30, 20); + +console.log(date01 > date02); // true +console.log(date01 < date02); // flase +``` + +**2. 常用方法** + +- **getTime() \ setTime(毫秒)** :返回和设置整个日期的所代表的毫秒数;与 valueOf() 方法返回的值相同; +- **getFullYear() \ setFullYear(年)** :返回和设置4位数的年份; +- **getMonth() \ setMonth(月)** :返回和设置月份,其中 0 表示一月, 11 表示十二月; +- **getDate() \ setDate(日)** :返回和设置月份中的天数(1到31); +- **getDay()** :返回和设置星期几 ( 其中0表示星期日, 6表示星期六); +- **getHours() \ setHours(时)** :返回和设置小时数(0到23); +- **getMinutes() \ setMinutes(分)** :返回和设置日期中的分钟数(0到59); +- **getSeconds() \ setSeconds(秒)** :返回和设置日期中的秒数(0到59); +- **getMilliseconds() \ setMilliseconds(毫秒)** :返回和设置日期中的毫秒数。 + +### 3.4 Funcation 类型 + +**1. 函数参数** + +ECMAScript 使用 function 关键字来声明函数,但和其他语言不同的是,ECMAScript 中函数对于参数的限制是非常宽松的,例如你在定义函数时定义了两个参数,但在调用时可以只传递一个参数、也可以传三个参数,甚至不传递,示例如下: + +```java +function test(first, second) { + console.log("first:" + first + ",second:" + second); +} +test(1) // first:1,second:undefined +test(1,2) // first:1,second:2 +test(1,2,3) // first:1,second:2 +``` + +之所以能实现这样的效果,是因为 ECMAScript 在函数内部使用了一个数组 arguments 来维护所有参数,函数接收到的始终都是这个数组,而在实际使用时指向的也是这个数组中的具体元素,所以以上的函数等价于下面的函数: + +```javascript +function test(first, second) { + console.log("first:" + arguments[0] + ",second:" + arguments[1]); +} +``` + +**2. 改变函数作用域** + +在 ECMAScript 5 中,每个函数都包含两个非继承而来的方法:**apply()** 和 **call()** ,它们都用于在特定的作用域中调用函数。简单来说,可以用这两个方法来改变函数的实际调用对象,从而改变 this 的值,因为 this 总是指向当前函数的实际调用对象: + +```javascript +window.color = "red"; +var o = { color: "blue" }; +function sayColor(){ + console.log(this.color); +} + +sayColor(); // red +sayColor.call(this); // red +sayColor.call(window); // red +sayColor.call(o); // blue 此时this指向的是函数调用对象,即 o +``` + +**apply()** 和 **call()** 的第一个参数都是指代函数的调用对象,它们的区别主要在于第二个参数:**apply()** 支持使用数组或 arguments 给调用函数传值,而 **call()** 给调用函数传值时,必须逐个列举: + +```javascript +function sum(num1, num2) { + return num1 + num2; +} + +function callSum1(num1, num2) { + return sum.apply(this, arguments); +} + +function callSum2(num1, num2) { + return sum.apply(this, [num1, num2]); +} + +function callSum3(num1, num2) { + return sum.call(this, num1, num2); +} + +callSum1(10, 10); +callSum2(10, 10); +callSum3(10, 10); +``` + +**3. 绑定函数作用域** + +如果想要将函数绑定在某个特定的作用域上,可以使用 **bind()** 函数: + +```javascript +window.color = "red"; +var o = { color: "blue" }; +function sayColor(){ + console.log(this.color); +} + +var objectSayColor = sayColor.bind(o); +objectSayColor(); // blue 此时即便是在全局作用域中调用这个函数,其作用域仍然被永远绑定在 o 对象上 +``` + +### 3.5 引用类型检测 + +想要检测某个对象是否属于某个引用类型,可以使用 **instanceof** 关键字: + +```shell +var date = new Date(); +date instanceof Date // true +``` + + + +## 四、内置对象 + +### 4.1 Global 对象 + +ECMAScript 中内置了一个全局对象 Global ,任何不属于任何其他对象的属性和方法,最终都是它的属性和方法。 ES 通过该内置对象,提供了一些可以直接调用的全局方法,常用的如下: + ++ **isNaN()**:用于确定一个值是否为 NaN; ++ **isFinite()**:用于判断被传入的参数值是否为一个有限数值; ++ **parseInt() \ parseFloat()**:解析并返回一个整数 \ 浮点数; ++ **encodeURI()**:对 URI 进行编码,但不会对本身属于 URI 的特殊字符进行编码,例如冒号、正斜杠、问号和井号; ++ **encodeURIComponent()**:对 URI 进行编码,会对任何非标准字符进行编码。 + +### 4.2 window 对象 + +ECMAScript 并没有提供任何直接访问 Global 对象的方法,但是浏览器却基于 Global 扩展实现了 window 对象,可以直接在浏览器环境下使用: + +```javascript +window.isFinite(12) // true +a = 12; +window.a // 12 +``` + + + +## 五、作用域与闭包 + +### 5.1 作用域 + +在 ECMAScript 6 之前,只存在两种作用域,即:全局作用域 和 函数作用域,不存在块级作用域。这意味着在除了函数外的任何代码块中使用 var 关键字声明的变量都会被提升为全局变量,示例如下: + +```javascript +function test() { + var age =12; +} +age // age is not defined + +if (true) { + var name = "heibaiying"; +} +name // heibaiying +``` + +这种情况同样适用与 for 循环代码块: + +```javascript +for (var i = 0; i < 10; i++) {} +console.log(i); // 10 +``` + +### 5.2 作用域链 + +由于函数作用域的存在,函数内的变量不能被外部访问,但是函数内的变量可以被其内部的函数访问,并且函数也可以访问其父级作用域上的变量,从而形成一条从其自身作用域到全局作用域的链条,示例如下: + +```javascript +var global = "global"; +var outer = "outer global"; + +(function outer() { + var outer = "outer"; + + function inner() { + console.log(global, outer); + } + + inner() +})(); + +// 输出:global outer +``` + +### 5.3 闭包 + +由于函数作用域的存在,函数内的变量不能被外部访问,这可以保证变量的私有性。但如果你想允许外部对内部变量进行特定操作,可以通过闭包来实现。闭包是指有权访问另一个函数作用域中的变量的函数。示例如下: + +```java +var contain = function () { + + var arr = []; + + return { + push: function () { + arr.push(...arguments); + }, + + get: function () { + return arr; + } + } +}; + +var ctn = contain(); +ctn.push(1, 2, 3, 4); +ctn.get(); // [ 1, 2, 3, 4 ] +``` + + + +## 六、对象设计 + +ECMAScript 中的对象都有两种基本属性:数据属性和访问器属性。 + +### 6.1 数据属性 + + +数据属性有以下 4 个描述其行为的特性: + ++ **Enumerable**:表示该属性能否通过 for-in 循环返回;对于直接在对象上定义的属性, 该值默认为 true。 ++ **Writable**:表示能否修改属性的值;对于直接在对象上定义的属性, 该值默认为 true。 ++ **Value**:对应属性的数据值。默认值为 undefined。 ++ **Configurable**:表示能否对属性进行删除,修改等配置操作,对于直接在对象上定义的属性, 该值默认为 true。需要注意的是一旦将该属性的值设置为 false,就不能再将其设置为 true 。即一旦设置为不可配置,就不能再修改为可配置。因为你已经修改为不可配置,此时任何配置操作都无效了,自然修改 Configurable 属性的操作也无效。 + +```javascript +var person = {age: 12}; +Object.defineProperty(person, "name", { + Enumerable: false, + writable: false, + value: "heibai" +}); + +console.log(person.name); // heibai +person.name = "ying"; +console.log(person.name); // ying + +for (var key in person) { + console.log("key:" + key + ",value:" + person[key]) /// key:age,value:12 +} +``` + +### 6.2 访问器属性 + +访问器属性也有以下 4 个描述其行为的特性: + ++ **Configurable**:表示能否对属性进行删除,修改等配置操作;对于直接在对象上定义的属性, 该值默认为 true。 ++ **Enumerable**:表示该属性能否通过 for-in 循环返回;对于直接在对象上定义的属性, 该值默认为 true。 ++ **Get**:在读取属性时调用的函数。默认值为 undefined。 ++ **Set**:在写入属性时调用的函数。默认值为 undefined。 + +```javascript +var student = { + _age: null, + birthday: new Date(2012,7,2) +}; +Object.defineProperty(student, "age", { + + get: function () { + if (this._age == null) { + // 如果年龄不存在就根据出生日期计算 + return new Date().getFullYear() - this.birthday.getFullYear() + }else { + return this._age; + } + }, + set: function (newValue) { + if (newValue < 0) { + console.log("年龄不能设置为负数"); + } else { + this._age = newValue; + } + } +}); + +student.age = -1; // 输出:年龄不能设置为负数 +console.log(student.age); // 输出:7 +student.age = 12; +console.log(student.age); // 输出 12 +``` + + +### 6.3 读取属性 + +想要获取一个对象的数据属性和访问器属性,可以使用 **Object.getOwnPropertyDescriptor()** 方法,类似于其他语言中的反射机制。这个方法接收两个参数:属性所在的对象和要读取属性名称。沿用上面的例子,示例如下: + +```javascript +var descriptor = Object.getOwnPropertyDescriptor(student, "age"); +console.log(descriptor.get); // 输出 [Function: get] +console.log(descriptor.enumerable); // 输出 false +``` + +### 6.4 创建对象 + +在 ECMAScript 中,对象就是一种特殊的函数,想要声明一个对象,可以结合使用构造器模式和原型模式:基本属性可以通过构造器传入;但方法声明需要定义在原型属性上,如果直接定义在构造器上,每个对象实例都会创建该方法函数,即每个对象实例调用的都是自己重复声明的方法函数,示例如下: + +```javascript +function Person(name, age, job) { + this.name = name; + this.age = age; + this.job = job; + this.friends = ["hei", "bai"]; + // 方法应该声明在原型属性上,而不是这里 + this.sayAge = function () { + console.log(this.age) + } +} + +Person.prototype = { + constructor: Person, + // 方法应该声明在这 + sayName: function () { + alert(this.name); + } +} + + +var person1 = new Person("user01", 29, "Software Engineer"); +var person2 = new Person("user02", 27, "Doctor"); + +person1.friends.push("ying"); +console.log(person1.friends); // [ 'hei', 'bai', 'ying' ] +console.log(person2.friends); // [ 'hei', 'bai'] +console.log(person1 instanceof Person); // true +console.log(person1.constructor === Person); // true +console.log(person1.sayName === person2.sayName); // true +console.log(person1.sayAge===person2.sayAge); // false +``` + + + + + +## 参考资料 + +1. 尼古拉斯·泽卡斯 . JavaScript高级程序设计(第3版). 人民邮电出版社 . 2012-3-29 +2. [JS中小数四舍五入和浮点数的研究](http://caibaojian.com/js-tofixed.html) + + + diff --git a/blog/en-us/web/js_tool_method.md b/blog/en-us/web/js_tool_method.md new file mode 100644 index 0000000..d81e5df --- /dev/null +++ b/blog/en-us/web/js_tool_method.md @@ -0,0 +1,1006 @@ +# JavaScript 工具函数大全 + +## 数组 + +1. all:布尔全等判断 + +``` js +const all = (arr, fn = Boolean) => arr.every(fn); + +all([4, 2, 3], x => x > 1); // true +all([1, 2, 3]); // true + +``` + +2. allEqual:检查数组各项相等 + +``` js +const allEqual = arr => arr.every(val => val === arr[0]); + +allEqual([1, 2, 3, 4, 5, 6]); // false +allEqual([1, 1, 1, 1]); // true + +``` + +3. approximatelyEqual:约等于 + +``` js +const approximatelyEqual = (v1, v2, epsilon = 0.001) => Math.abs(v1 - v2) < epsilon; + +approximatelyEqual(Math.PI / 2.0, 1.5708); // true + +``` + +4. arrayToCSV:数组转CSV格式(带空格的字符串) + +``` js + +const arrayToCSV = (arr, delimiter = ',') => + arr.map(v => v.map(x => `"${x}"`).join(delimiter)).join('\n'); + +arrayToCSV([['a', 'b'], ['c', 'd']]); // '"a","b"\n"c","d"' +arrayToCSV([['a', 'b'], ['c', 'd']], ';'); // '"a";"b"\n"c";"d"' + +``` + +5. arrayToHtmlList:数组转li列表 + +``` js +const arrayToHtmlList = (arr, listID) => + (el => ( + (el = document.querySelector('#' + listID)), + (el.innerHTML += arr.map(item => `
  • ${item}
  • `).join('')) + ))(); + +arrayToHtmlList(['item 1', 'item 2'], 'myListID'); + +``` + +6. average:平均数 + +``` js +const average = (...nums) => nums.reduce((acc, val) => acc + val, 0) / nums.length; +average(...[1, 2, 3]); // 2 +average(1, 2, 3); // 2 + +``` + +7. averageBy:数组对象属性平均数 + +此代码段将获取数组对象属性的平均值 + +``` js +const averageBy = (arr, fn) => + arr.map(typeof fn === 'function' ? fn : val => val[fn]).reduce((acc, val) => acc + val, 0) / + arr.length; + +averageBy([{ n: 4 }, { n: 2 }, { n: 8 }, { n: 6 }], o => o.n); // 5 +averageBy([{ n: 4 }, { n: 2 }, { n: 8 }, { n: 6 }], 'n'); // 5 + +``` + +8. bifurcate:拆分断言后的数组 + +可以根据每个元素返回的值,使用reduce()和push() 将元素添加到第二次参数fn中 。 + +``` js +const bifurcate = (arr, filter) => + arr.reduce((acc, val, i) => (acc[filter[i] ? 0 : 1].push(val), acc), [[], []]); +bifurcate(['beep', 'boop', 'foo', 'bar'], [true, true, false, true]); +// [ ['beep', 'boop', 'bar'], ['foo'] ] + +``` + +9. castArray:其它类型转数组 + +``` js +const castArray = val => (Array.isArray(val) ? val : [val]); + +castArray('foo'); // ['foo'] +castArray([1]); // [1] +castArray(1); // [1] + +``` + +10. compact:去除数组中的无效/无用值 + +``` js +const compact = arr => arr.filter(Boolean); + +compact([0, 1, false, 2, '', 3, 'a', 'e' * 23, NaN, 's', 34]); +// [ 1, 2, 3, 'a', 's', 34 ] + +``` + +11. countOccurrences:检测数值出现次数 + +``` js +const countOccurrences = (arr, val) => arr.reduce((a, v) => (v === val ? a + 1 : a), 0); +countOccurrences([1, 1, 2, 1, 2, 3], 1); // 3 + +``` + +12. deepFlatten:递归扁平化数组 + +``` js +const deepFlatten = arr => [].concat(...arr.map(v => (Array.isArray(v) ? deepFlatten(v) : v))); + +deepFlatten([1, [2], [[3], 4], 5]); // [1,2,3,4,5] + +``` + +13. difference:寻找差异(并返回第一个数组独有的) + +此代码段查找两个数组之间的差异,并返回第一个数组独有的。 + +``` js + +const difference = (a, b) => { + const s = new Set(b); + return a.filter(x => !s.has(x)); +}; + +difference([1, 2, 3], [1, 2, 4]); // [3] + +``` + +14. differenceBy:先执行再寻找差异 + +在将给定函数应用于两个列表的每个元素之后,此方法返回两个数组之间的差异。 + +``` js +const differenceBy = (a, b, fn) => { + const s = new Set(b.map(fn)); + return a.filter(x => !s.has(fn(x))); +}; + +differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor); // [1.2] +differenceBy([{ x: 2 }, { x: 1 }], [{ x: 1 }], v => v.x); // [ { x: 2 } ] + +``` + +15. dropWhile:删除不符合条件的值 + +此代码段从数组顶部开始删除元素,直到传递的函数返回为true。 + +``` js +const dropWhile = (arr, func) => { + while (arr.length > 0 && !func(arr[0])) arr = arr.slice(1); + return arr; +}; + +dropWhile([1, 2, 3, 4], n => n >= 3); // [3,4] + +``` + +16. flatten:指定深度扁平化数组 + +此代码段第二参数可指定深度。 + +``` js +const flatten = (arr, depth = 1) => + arr.reduce((a, v) => a.concat(depth > 1 && Array.isArray(v) ? flatten(v, depth - 1) : v), []); + +flatten([1, [2], 3, 4]); // [1, 2, 3, 4] +flatten([1, [2, [3, [4, 5], 6], 7], 8], 2); // [1, 2, 3, [4, 5], 6, 7, 8] + +``` + +17. indexOfAll:返回数组中某值的所有索引 + +此代码段可用于获取数组中某个值的所有索引,如果此值中未包含该值,则返回一个空数组。 + +``` js +const indexOfAll = (arr, val) => arr.reduce((acc, el, i) => (el === val ? [...acc, i] : acc), []); + +indexOfAll([1, 2, 3, 1, 2, 3], 1); // [0,3] +indexOfAll([1, 2, 3], 4); // [] + +``` + +18. intersection:两数组的交集 + +``` js + +const intersection = (a, b) => { + const s = new Set(b); + return a.filter(x => s.has(x)); +}; + +intersection([1, 2, 3], [4, 3, 2]); // [2, 3] + +``` + +19. intersectionWith:两数组都符合条件的交集 + +此片段可用于在对两个数组的每个元素执行了函数之后,返回两个数组中存在的元素列表。 + +``` js + +const intersectionBy = (a, b, fn) => { + const s = new Set(b.map(fn)); + return a.filter(x => s.has(fn(x))); +}; + +intersectionBy([2.1, 1.2], [2.3, 3.4], Math.floor); // [2.1] + +``` + +20. intersectionWith:先比较后返回交集 + +``` js +const intersectionWith = (a, b, comp) => a.filter(x => b.findIndex(y => comp(x, y)) !== -1); + +intersectionWith([1, 1.2, 1.5, 3, 0], [1.9, 3, 0, 3.9], (a, b) => Math.round(a) === Math.round(b)); // [1.5, 3, 0] + +``` + +21. minN:返回指定长度的升序数组 + +``` js +const minN = (arr, n = 1) => [...arr].sort((a, b) => a - b).slice(0, n); + +minN([1, 2, 3]); // [1] +minN([1, 2, 3], 2); // [1,2] + +``` + +22. negate:根据条件反向筛选 + +``` js + +const negate = func => (...args) => !func(...args); + +[1, 2, 3, 4, 5, 6].filter(negate(n => n % 2 === 0)); // [ 1, 3, 5 ] + +``` + +23. randomIntArrayInRange:生成两数之间指定长度的随机数组 + +``` js +const randomIntArrayInRange = (min, max, n = 1) => + Array.from({ length: n }, () => Math.floor(Math.random() * (max - min + 1)) + min); + +randomIntArrayInRange(12, 35, 10); // [ 34, 14, 27, 17, 30, 27, 20, 26, 21, 14 ] + +``` + +24. sample:在指定数组中获取随机数 + +``` js +const sample = arr => arr[Math.floor(Math.random() * arr.length)]; + +sample([3, 7, 9, 11]); // 9 + +``` + +25. sampleSize:在指定数组中获取指定长度的随机数 + +此代码段可用于从数组中获取指定长度的随机数,直至穷尽数组。 使用Fisher-Yates算法对数组中的元素进行随机选择。 + +``` js +const sampleSize = ([...arr], n = 1) => { + let m = arr.length; + while (m) { + const i = Math.floor(Math.random() * m--); + [arr[m], arr[i]] = [arr[i], arr[m]]; + } + return arr.slice(0, n); +}; + +sampleSize([1, 2, 3], 2); // [3,1] +sampleSize([1, 2, 3], 4); // [2,3,1] + +``` +26. shuffle:“洗牌” 数组 + +此代码段使用Fisher-Yates算法随机排序数组的元素。 + +``` js + +const shuffle = ([...arr]) => { + let m = arr.length; + while (m) { + const i = Math.floor(Math.random() * m--); + [arr[m], arr[i]] = [arr[i], arr[m]]; + } + return arr; +}; + +const foo = [1, 2, 3]; +shuffle(foo); // [2, 3, 1], foo = [1, 2, 3] + +``` +27. nest:根据parent_id生成树结构(阿里一面真题) + +根据每项的parent_id,生成具体树形结构的对象。 + +``` js +const nest = (items, id = null, link = 'parent_id') => + items + .filter(item => item[link] === id) + .map(item => ({ ...item, children: nest(items, item.id) })); + +``` + +用法: + +``` js + const comments = [ + { id: 1, parent_id: null }, + { id: 2, parent_id: 1 }, + { id: 3, parent_id: 1 }, + { id: 4, parent_id: 2 }, + { id: 5, parent_id: 4 } +]; +const nestedComments = nest(comments); // [{ id: 1, parent_id: null, children: [...] }] + +``` +## 函数 + +1. attempt:捕获函数运行异常 + +该代码段执行一个函数,返回结果或捕获的错误对象。 + +``` js +onst attempt = (fn, ...args) => { + try { + return fn(...args); + } catch (e) { + return e instanceof Error ? e : new Error(e); + } +}; +var elements = attempt(function(selector) { + return document.querySelectorAll(selector); +}, '>_>'); +if (elements instanceof Error) elements = []; // elements = [] + +``` + +2. defer:推迟执行 + +``` js +const defer = (fn, ...args) => setTimeout(fn, 1, ...args); + +defer(console.log, 'a'), console.log('b'); // logs 'b' then 'a' + +``` +3. runPromisesInSeries:运行多个Promises + +``` js +const runPromisesInSeries = ps => ps.reduce((p, next) => p.then(next), Promise.resolve()); +const delay = d => new Promise(r => setTimeout(r, d)); + +runPromisesInSeries([() => delay(1000), () => delay(2000)]); +//依次执行每个Promises ,总共需要3秒钟才能完成 + +``` +4. timeTaken:计算函数执行时间 + +``` js + +const timeTaken = callback => { + console.time('timeTaken'); + const r = callback(); + console.timeEnd('timeTaken'); + return r; +}; + +timeTaken(() => Math.pow(2, 10)); // 1024, (logged): timeTaken: 0.02099609375ms + +``` +5. createEventHub:简单的发布/订阅模式 + +创建一个发布/订阅(发布-订阅)事件集线,有emit,on和off方法。 + +* 使用Object.create(null)创建一个空的hub对象。 +* emit,根据event参数解析处理程序数组,然后.forEach()通过传入数据作为参数来运行每个处理程序。 +* on,为事件创建一个数组(若不存在则为空数组),然后.push()将处理程序添加到该数组。 +* off,用.findIndex()在事件数组中查找处理程序的索引,并使用.splice()删除。 + +``` js +const createEventHub = () => ({ + hub: Object.create(null), + emit(event, data) { + (this.hub[event] || []).forEach(handler => handler(data)); + }, + on(event, handler) { + if (!this.hub[event]) this.hub[event] = []; + this.hub[event].push(handler); + }, + off(event, handler) { + const i = (this.hub[event] || []).findIndex(h => h === handler); + if (i > -1) this.hub[event].splice(i, 1); + if (this.hub[event].length === 0) delete this.hub[event]; + } +}); + +``` +用法: + +``` js +const handler = data => console.log(data); +const hub = createEventHub(); +let increment = 0; + +// 订阅,监听不同事件 +hub.on('message', handler); +hub.on('message', () => console.log('Message event fired')); +hub.on('increment', () => increment++); + +// 发布:发出事件以调用所有订阅给它们的处理程序,并将数据作为参数传递给它们 +hub.emit('message', 'hello world'); // 打印 'hello world' 和 'Message event fired' +hub.emit('message', { hello: 'world' }); // 打印 对象 和 'Message event fired' +hub.emit('increment'); // increment = 1 + +// 停止订阅 +hub.off('message', handler); + +``` +6. memoize:缓存函数 + +通过实例化一个Map对象来创建一个空的缓存。 + +通过检查输入值的函数输出是否已缓存,返回存储一个参数的函数,该参数将被提供给已记忆的函数;如果没有,则存储并返回它。 + +``` js +const memoize = fn => { + const cache = new Map(); + const cached = function(val) { + return cache.has(val) ? cache.get(val) : cache.set(val, fn.call(this, val)) && cache.get(val); + }; + cached.cache = cache; + return cached; +}; + + +``` +Ps: 这个版本可能不是很清晰,还有Vue源码版的: + +``` js +/** + * Create a cached version of a pure function. + */ +export function cached (fn: F): F { + const cache = Object.create(null) + return (function cachedFn (str: string) { + const hit = cache[str] + return hit || (cache[str] = fn(str)) + }: any) +} + +``` +7. once:只调用一次的函数 + +``` js +const once = fn => { + let called = false + return function () { + if (!called) { + called = true + fn.apply(this, arguments) + } + } +}; + +``` +8. flattenObject:以键的路径扁平化对象 + +使用递归。 + +* 利用Object.keys(obj)联合Array.prototype.reduce(),以每片叶子节点转换为扁平的路径节点。 +* 如果键的值是一个对象,则函数使用调用适当的自身prefix以创建路径Object.assign()。 +* 否则,它将适当的前缀键值对添加到累加器对象。 +* prefix除非您希望每个键都有一个前缀,否则应始终省略第二个参数。 + +``` js +const flattenObject = (obj, prefix = '') => + Object.keys(obj).reduce((acc, k) => { + const pre = prefix.length ? prefix + '.' : ''; + if (typeof obj[k] === 'object') Object.assign(acc, flattenObject(obj[k], pre + k)); + else acc[pre + k] = obj[k]; + return acc; + }, {}); + +flattenObject({ a: { b: { c: 1 } }, d: 1 }); // { 'a.b.c': 1, d: 1 } + +``` +9. unflattenObject:以键的路径展开对象 + +与上面的相反,展开对象。 + +``` js +const unflattenObject = obj => + Object.keys(obj).reduce((acc, k) => { + if (k.indexOf('.') !== -1) { + const keys = k.split('.'); + Object.assign( + acc, + JSON.parse( + '{' + + keys.map((v, i) => (i !== keys.length - 1 ? `"${v}":{` : `"${v}":`)).join('') + + obj[k] + + '}'.repeat(keys.length) + ) + ); + } else acc[k] = obj[k]; + return acc; + }, {}); + +unflattenObject({ 'a.b.c': 1, d: 1 }); // { a: { b: { c: 1 } }, d: 1 } + +``` +这个的用途,在做Tree组件或复杂表单时取值非常舒服。 + + +## 字符串 + +1. byteSize:返回字符串的字节长度 + +``` js +const byteSize = str => new Blob([str]).size; + +byteSize('😀'); // 4 +byteSize('Hello World'); // 11 + +``` +2. capitalize:首字母大写 + +``` js +const capitalize = ([first, ...rest]) => + first.toUpperCase() + rest.join(''); + +capitalize('fooBar'); // 'FooBar' +capitalize('fooBar', true); // 'Foobar' + +``` +3. capitalizeEveryWord:每个单词首字母大写 +``` js +const capitalizeEveryWord = str => str.replace(/\b[a-z]/g, char => char.toUpperCase()); + +capitalizeEveryWord('hello world!'); // 'Hello World!' + +``` +4. decapitalize:首字母小写 +``` js +const decapitalize = ([first, ...rest]) => + first.toLowerCase() + rest.join('') + +decapitalize('FooBar'); // 'fooBar' +decapitalize('FooBar'); // 'fooBar' + +``` +5. luhnCheck:银行卡号码校验(luhn算法) + +Luhn算法的实现,用于验证各种标识号,例如信用卡号,IMEI号,国家提供商标识号等。 + +与String.prototype.split('')结合使用,以获取数字数组。获得最后一个数字。实施luhn算法。如果被整除,则返回,否则返回。 + +``` js +const luhnCheck = num => { + let arr = (num + '') + .split('') + .reverse() + .map(x => parseInt(x)); + let lastDigit = arr.splice(0, 1)[0]; + let sum = arr.reduce((acc, val, i) => (i % 2 !== 0 ? acc + val : acc + ((val * 2) % 9) || 9), 0); + sum += lastDigit; + return sum % 10 === 0; +}; + +``` +用例: +``` js +luhnCheck('4485275742308327'); // true +luhnCheck(6011329933655299); // false +luhnCheck(123456789); // false +``` +补充:银行卡号码的校验规则: + +银行卡号码的校验采用Luhn算法,校验过程大致如下: + +* 从右到左给卡号字符串编号,最右边第一位是1,最右边第二位是2,最右边第三位是3…. +* 从右向左遍历,对每一位字符t执行第三个步骤,并将每一位的计算结果相加得到一个数s。 +* 对每一位的计算规则:如果这一位是奇数位,则返回t本身,如果是偶数位,则先将t乘以2得到一个数n,如果n是一位数(小于10),直接返回n,否则将n的个位数和十位数相加返回。 +* 如果s能够整除10,则此号码有效,否则号码无效。 + +因为最终的结果会对10取余来判断是否能够整除10,所以又叫做模10算法。 +当然,还是库比较香: bankcardinfo + +6. splitLines:将多行字符串拆分为行数组。 + +使用String.prototype.split()和正则表达式匹配换行符并创建一个数组。 + +``` js +const splitLines = str => str.split(/\r?\n/); + +splitLines('This\nis a\nmultiline\nstring.\n'); // ['This', 'is a', 'multiline', 'string.' , ''] + +``` +7. stripHTMLTags:删除字符串中的HTMl标签 + +从字符串中删除HTML / XML标签。 + +使用正则表达式从字符串中删除HTML / XML 标记。 + +``` js +const stripHTMLTags = str => str.replace(/<[^>]*>/g, ''); + +stripHTMLTags('

    lorem ipsum

    '); // 'lorem ipsum' + +``` +## 对象 + +1. dayOfYear:当前日期天数 + +``` js +const dayOfYear = date => + Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24); + +dayOfYear(new Date()); // 285 + +``` +2. forOwn:迭代属性并执行回调 + +``` js +const forOwn = (obj, fn) => Object.keys(obj).forEach(key => fn(obj[key], key, obj)); +forOwn({ foo: 'bar', a: 1 }, v => console.log(v)); // 'bar', 1 + +``` +3. Get Time From Date:返回当前24小时制时间的字符串 + +``` js +const getColonTimeFromDate = date => date.toTimeString().slice(0, 8); + +getColonTimeFromDate(new Date()); // "08:38:00" + +``` +4. Get Days Between Dates:返回日期间的天数 +``` js +const getDaysDiffBetweenDates = (dateInitial, dateFinal) => + (dateFinal - dateInitial) / (1000 * 3600 * 24); + +getDaysDiffBetweenDates(new Date('2019-01-01'), new Date('2019-10-14')); // 286 + +``` +5. is:检查值是否为特定类型。 +``` js +const is = (type, val) => ![, null].includes(val) && val.constructor === type; + +is(Array, [1]); // true +is(ArrayBuffer, new ArrayBuffer()); // true +is(Map, new Map()); // true +is(RegExp, /./g); // true +is(Set, new Set()); // true +is(WeakMap, new WeakMap()); // true +is(WeakSet, new WeakSet()); // true +is(String, ''); // true +is(String, new String('')); // true +is(Number, 1); // true +is(Number, new Number(1)); // true +is(Boolean, true); // true +is(Boolean, new Boolean(true)); // true + +``` +6. isAfterDate:检查是否在某日期后 +``` js +const isAfterDate = (dateA, dateB) => dateA > dateB; + +isAfterDate(new Date(2010, 10, 21), new Date(2010, 10, 20)); // true + +``` +7. isBeforeDate:检查是否在某日期前 +``` js +const isBeforeDate = (dateA, dateB) => dateA < dateB; + +isBeforeDate(new Date(2010, 10, 20), new Date(2010, 10, 21)); // true + +``` +8. tomorrow:获取明天的字符串格式时间 + +``` js + +const tomorrow = () => { + let t = new Date(); + t.setDate(t.getDate() + 1); + return t.toISOString().split('T')[0]; +}; + +tomorrow(); // 2019-10-15 (如果明天是2019-10-15) + +``` +9. equals:全等判断 + +在两个变量之间进行深度比较以确定它们是否全等。 + +此代码段精简的核心在于Array.prototype.every()的使用。 + +``` js +const equals = (a, b) => { + if (a === b) return true; + if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime(); + if (!a || !b || (typeof a !== 'object' && typeof b !== 'object')) return a === b; + if (a.prototype !== b.prototype) return false; + let keys = Object.keys(a); + if (keys.length !== Object.keys(b).length) return false; + return keys.every(k => equals(a[k], b[k])); +}; + +``` +用法: + +``` js +equals({ a: [2, { e: 3 }], b: [4], c: 'foo' }, { a: [2, { e: 3 }], b: [4], c: 'foo' }); // true + +``` + +## 数字 + +1. randomIntegerInRange:生成指定范围的随机整数 + +``` js +const randomIntegerInRange = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; + +randomIntegerInRange(0, 5); // 3 + +``` +2. randomNumberInRange:生成指定范围的随机小数 +``` js +const randomNumberInRange = (min, max) => Math.random() * (max - min) + min; + +randomNumberInRange(2, 10); // 6.0211363285087005 + +``` +3. round:四舍五入到指定位数 +``` js +const round = (n, decimals = 0) => Number(`${Math.round(`${n}e${decimals}`)}e-${decimals}`); + +round(1.005, 2); // 1.01 + +``` +4. sum:计算数组或多个数字的总和 +``` js + +const sum = (...arr) => [...arr].reduce((acc, val) => acc + val, 0); + +sum(1, 2, 3, 4); // 10 +sum(...[1, 2, 3, 4]); // 10 + +``` +5. toCurrency:简单的货币单位转换 +``` js +const toCurrency = (n, curr, LanguageFormat = undefined) => + Intl.NumberFormat(LanguageFormat, { style: 'currency', currency: curr }).format(n); + +toCurrency(123456.789, 'EUR'); // €123,456.79 +toCurrency(123456.789, 'USD', 'en-us'); // $123,456.79 +toCurrency(123456.789, 'USD', 'fa'); // ۱۲۳٬۴۵۶٫۷۹ +toCurrency(322342436423.2435, 'JPY'); // ¥322,342,436,423 + +``` +## 浏览器操作及其它 + +1. bottomVisible:检查页面底部是否可见 +``` js +const bottomVisible = () => + document.documentElement.clientHeight + window.scrollY >= + (document.documentElement.scrollHeight || document.documentElement.clientHeight); + +bottomVisible(); // true + +``` +2. Create Directory:检查创建目录 + +此代码段调用fs模块的existsSync()检查目录是否存在,如果不存在,则mkdirSync()创建该目录。 + +``` js +const fs = require('fs'); +const createDirIfNotExists = dir => (!fs.existsSync(dir) ? fs.mkdirSync(dir) : undefined); +createDirIfNotExists('test'); + +``` +3. currentURL:返回当前链接url + +``` js +const currentURL = () => window.location.href; + +currentURL(); // 'https://juejin.im' + +``` +4. distance:返回两点间的距离 + +该代码段通过计算欧几里得距离来返回两点之间的距离。 +``` js +const distance = (x0, y0, x1, y1) => Math.hypot(x1 - x0, y1 - y0); + +distance(1, 1, 2, 3); // 2.23606797749979 + +``` +5. elementContains:检查是否包含子元素 +此代码段检查父元素是否包含子元素。 +``` js +const elementContains = (parent, child) => parent !== child && parent.contains(child); + +elementContains(document.querySelector('head'), document.querySelector('title')); // true +elementContains(document.querySelector('body'), document.querySelector('body')); // false + +``` +6. getStyle:返回指定元素的生效样式 +``` js +const getStyle = (el, ruleName) => getComputedStyle(el)[ruleName]; + +getStyle(document.querySelector('p'), 'font-size'); // '16px' + +``` +7. getType:返回值或变量的类型名 +``` js +const getType = v => + v === undefined ? 'undefined' : v === null ? 'null' : v.constructor.name.toLowerCase(); + +getType(new Set([1, 2, 3])); // 'set' +getType([1, 2, 3]); // 'array' + +``` +8. hasClass:校验指定元素的类名 +``` js +const hasClass = (el, className) => el.classList.contains(className); +hasClass(document.querySelector('p.special'), 'special'); // true + +``` +9. hide:隐藏所有的指定标签 +``` js +const hide = (...el) => [...el].forEach(e => (e.style.display = 'none')); + +hide(document.querySelectorAll('img')); // 隐藏所有标签 + +``` +10. httpsRedirect:HTTP 跳转 HTTPS +``` js +const httpsRedirect = () => { + if (location.protocol !== 'https:') location.replace('https://' + location.href.split('//')[1]); +}; + +httpsRedirect(); // 若在`http://www.baidu.com`, 则跳转到`https://www.baidu.com` + +``` +11. insertAfter:在指定元素之后插入新元素 +``` js +const insertAfter = (el, htmlString) => el.insertAdjacentHTML('afterend', htmlString); + +//
    ...

    after

    +insertAfter(document.getElementById('myId'), '

    after

    '); + +``` +12. insertBefore:在指定元素之前插入新元素 +``` js +const insertBefore = (el, htmlString) => el.insertAdjacentHTML('beforebegin', htmlString); + +insertBefore(document.getElementById('myId'), '

    before

    '); //

    before

    ...
    + +``` +13. isBrowser:检查是否为浏览器环境 + +此代码段可用于确定当前运行时环境是否为浏览器。这有助于避免在服务器(节点)上运行前端模块时出错。 + +``` js +const isBrowser = () => ![typeof window, typeof document].includes('undefined'); + +isBrowser(); // true (browser) +isBrowser(); // false (Node) + +``` +14. isBrowserTab:检查当前标签页是否活动 +``` js +const isBrowserTabFocused = () => !document.hidden; + +isBrowserTabFocused(); // true + +``` +15. nodeListToArray:转换nodeList为数组 +``` js +const nodeListToArray = nodeList => [...nodeList]; + +nodeListToArray(document.childNodes); // [ , html ] + +``` +16. Random Hexadecimal Color Code:随机十六进制颜色 +``` js + +const randomHexColorCode = () => { + let n = (Math.random() * 0xfffff * 1000000).toString(16); + return '#' + n.slice(0, 6); +}; + +randomHexColorCode(); // "#e34155" + +``` +17. scrollToTop:平滑滚动至顶部 +``` js +const scrollToTop = () => { + const c = document.documentElement.scrollTop || document.body.scrollTop; + if (c > 0) { + window.requestAnimationFrame(scrollToTop); + window.scrollTo(0, c - c / 8); + } +}; + +scrollToTop(); + +``` +18. smoothScroll:滚动到指定元素区域 + +该代码段可将指定元素平滑滚动到浏览器窗口的可见区域。 +``` js +const smoothScroll = element => + document.querySelector(element).scrollIntoView({ + behavior: 'smooth' + }); + +smoothScroll('#fooBar'); +smoothScroll('.fooBar'); + +``` +19. detectDeviceType:检测移动/PC设备 +``` js +const detectDeviceType = () => + /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) + ? 'Mobile' + : 'Desktop'; + +``` +20. getScrollPosition:返回当前的滚动位置 + +默认参数为window ,pageXOffset(pageYOffset)为第一选择,没有则用scrollLeft(scrollTop) + +``` js +const getScrollPosition = (el = window) => ({ + x: el.pageXOffset !== undefined ? el.pageXOffset : el.scrollLeft, + y: el.pageYOffset !== undefined ? el.pageYOffset : el.scrollTop +}); + +getScrollPosition(); // {x: 0, y: 200} + +``` +21. size:获取不同类型变量的字节长度 + +这个的实现非常巧妙,利用Blob类文件对象的特性,获取对象的长度。 + +另外,多重三元运算符,是真香。 + +``` js +const size = val => + Array.isArray(val) + ? val.length + : val && typeof val === 'object' + ? val.size || val.length || Object.keys(val).length + : typeof val === 'string' + ? new Blob([val]).size + : 0; + +size([1, 2, 3, 4, 5]); // 5 +size('size'); // 4 +size({ one: 1, two: 2, three: 3 }); // 3 + + +``` +22. escapeHTML:转义HTML + +当然是用来防XSS攻击啦。 + +``` js +const escapeHTML = str => + str.replace( + /[&<>'"]/g, + tag => + ({ + '&': '&', + '<': '<', + '>': '>', + "'": ''', + '"': '"' + }[tag] || tag) + ); + +escapeHTML('Me & you'); // '<a href="#">Me & you</a>' + +``` diff --git a/blog/en-us/web/react_interview.md b/blog/en-us/web/react_interview.md new file mode 100644 index 0000000..06991c7 --- /dev/null +++ b/blog/en-us/web/react_interview.md @@ -0,0 +1,624 @@ +# React 面试问题 +>如果你是一位有理想的前端开发人员,并且正在准备面试,那么这篇文章就是为你准备的。本文收集了 React 面试中最常见的 50 大问题,这是一份理想的指南,让你为 React 相关的面试做好充分的准备工作。首先我们快速了解一下 React 在市场上的需求和现状,然后再开始讨论 React 面试问题。 + +JavaScript 工具的市场地位正在缓慢而稳定地上升当中,而对 React 认证的需求正在飞速增长。选择正确的技术来开发应用程序或网站变得愈加艰难。React 被认为是 Javascript 语言中增长最快的框架。 + +虚拟 DOM 和可复用部件等独特特性吸引了前端开发人员的注意。尽管成熟的框架(如 Angular、Meteor 和 Vue 等)在 MVC(模型 - 视图 - 控制器)中只是一个“视图”库,但它们都有很强的竞争力。下图显示了常见 JS 框架的趋势: + +![react-top](./../img/react-top.png) + +以下是面试官最有可能提出的 50 个面试问题和答案。 + +## React 面试问题——常规知识 + +1. 真实 DOM 和虚拟 DOM 的区别 + + 真实 DOM|虚拟 DOM + --|:--| + 1.更新较慢| 1.更新较快 + 2.可以直接更新 HTML|2.不能直接更新 HTML + 3.元素更新时创建一个新 DOM|3.元素更新时更新 JSX + 4.DOM 操作开销较大|4.DOM 操作非常容易 + 5.内存浪费严重|5.没有内存浪费 + +2. 什么是 React? + + * React 是 2011 年由 Facebook 开发的前端 JavaScript 库。 + * 它遵循基于组件的方法,这种方法可以用来构建可复用的 UI 组件。 + * 它用于复杂的交互式 Web 端和移动端用户界面开发。 + * 尽管它在 2015 年才开源,但得到了一家巨头的支持。 + +3. React 的特点是什么? + + * 轻量级 DOM,以获得更好的性能。 + * 在 React 中,一切都被视为组件。 + * React 使用 JSX(JavaScript eXtension),使我们可以编写类似于 HTML 的 JavaScript。 + * React 不是直接运行在浏览器的文档对象模型(DOM)上,而是运行在虚拟 DOM 上。 + * ReactJS 遵循单向数据流或单向数据绑定。 + +4. 列出 React 的一些主要优势。 + + * 可以提高应用程序的性能。 + * 可以方便地用在客户端和服务端。 + * 由于有了 JSX,代码的可读性提高了。 + * 使用 React 后,编写 UI 测试用例变得非常容易。 + +5. React 有哪些局限? + + * React 只是一个库,而不是一个成熟的框架。 + * 它的库很大,需要花费一些时间来理解。 + * 新手程序员可能很难入门。 + * 由于它使用了内联模板和 JSX,编码也比较复杂。 + +6. 什么是 JSX? + +JSX 是 JavaScript XML 的简写。这是 React 使用的一种文件类型,具备 JavaScript 的表现力,并使用 HTML 作为模板语法。这样一来 HTML 文件理解起来就非常简单。这种文件可以创造稳健的应用程序并提高其效率。下面是一个 JSX 实例: + +``` jsx +render(){ + return( +
    +

    Hello World from Codersera!!

    +
    + ); + } +``` +7. 你对虚拟 DOM 有什么了解?解释其工作机制。 + +虚拟 DOM 是轻量级的 JavaScript 对象,一开始只是真实 DOM 的一个副本。它是一个节点树,将组件列为对象及其属性和内容的列表。React 的渲染功能从 React 的各个部分生成一个节点树。然后,它会根据由不同用户或系统行为引起的信息模型突变来更新此树。 +虚拟 DOM 的工作机制只有简单的三步组成。 + + 1. 每当任何基础信息更改时,整个 UI 就会以虚拟 DOM 的表示形式重新渲染。 + + ![ui-dom](./../img/ui-dom-1.png) + + 2. 然后计算先前的 DOM 表示和新的 DOM 表示之间的区别。 + + ![ui-dom](./../img/ui-dom-2.png) + + 3. 计算完成后,只有实际更改的内容才会更新到真实 DOM。 + + ![ui-dom](./../img/ui-dom-3.png) + +8. 为什么浏览器无法读取 JSX? + +React 使用 JSX(JavaScript eXtension),我们可以用它编写类似于 HTML 的 JavaScript。但由于 JSX 不是合法的 JavaScript,因此浏览器无法直接读取它。如果 JavaScript 文件包含 JSX,则必须将其转换。你需要一个转换器将 JSX 转换为浏览器可以理解的常规 Javascript。目前最常用的转换器是 Babel。 + +9. 与 ES5 相比,React 的 ES6 语法有何不同? + +ES5 和 ES6 的语法区别如下: + +```javascript +// ES5 +var React = require('react'); +// ES6 +import React from 'react'; + +******** export vs exports ********* + +// ES5 +module.exports = Component; +// ES6 +export default Component; + +****** function ***** +// ES5 + +var MyComponent = React.createClass({ + render: function() { + return

    Hello CoderSera!

    + }, +}); + +// ES6 +class MyComponent extends React.Component { + render() { + return

    Hello CoderSera!

    + } +} + + ******* props ****** + +// ES5 +var App = React.createClass({ + propTypes: { name: React.PropTypes.string }, + render: function() { + return

    Hello, { this.props.name }! < /h3> + }, +}); + +// ES6 +class App extends React.Component { + render() { + return

    Hello, { this.props.name }!

    + } +} + + ****** state ***** + +// ES5 +var App = React.createClass({ + getInitialState: function() { + return { name: 'world' }; + } + + render: function() { + return

    Hello, { this.state.name }! < /h3>; + }, +}); + +// ES6 +class App extends React.Component { + constructor() { + super(); + this.state = { name: 'world' }; + } + + render() { + return

    Hello, { this.state.name }! < /h3> + } + render() { + return; +

    Hello, { this.state.name }! < /h3> + } + +``` + +10. React 和 Angular 有何不同? + +React 对比 Angular|React|Angular +--|:--|:--| +架构|使用虚拟 DOM|使用真实 DOM +渲染|服务端渲染|客户端渲染 +DOM|使用虚拟 DOM|使用真实 DOM +数据绑定|单向数据绑定|双向数据绑定 +调试|编译时调试|运行时调试 +开发者|Facebook|谷歌 + +11. 如何理解“在 React 中,一切都是组件”。 + +React 应用程序的 UI 构建块都是组件。这些部分将整个 UI 划分为许多可自治和可复用的微小部分。然后独立的某个部分发生变化就不会影响 UI 的其余部分。 + +12. 解释 React 中 render() 的目的。 + +它被视为普通函数,但 render() 函数必须返回某些值,无论值是否为空。调用组件文件时默认会调用 render() 方法,因为组件需要显示 HTML 标记,或者我们可以说 JSX 语法。每个 React 组件必须有一个 render() 函数,它返回单个 React 元素,该元素代表原生 DOM 组件。如果需要渲染多个 HTML 元素,则必须将它们分组在一个封闭的标签内,如form、group和div等。此函数必须保持纯净,就是说它在每次调用时必须返回相同的结果。 + +``` jsx +import React, { Component } from 'react'; + +class App extends Component { + render() { + return (

    hello CoderSera

    ) + } +} + +export default App; + +``` + +13. 什么是 Hooks? + +Hooks 是一项新功能,使你无需编写类即可使用状态等 React 功能。来看一个 useState hook 示例。 + +```jsx +import {useState} from 'react';

    function Example() {
    // Declare a new +``` + +14. 什么是 props? + +* Props 用于将数据从父级传递到子级或由组件本身传递。它们是不可变的,因此不会更改。 + +* Props 是不可变的。因为它们是基于纯函数的概念开发的。在纯函数中,我们无法更改参数数据。因此在 ReactJS 中也无法更改 props 的数据。 + +15. React 中的状态是什么,如何使用? + +组件可以通过状态来跟踪其执行的任何渲染之间的信息。 + +状态用于可变数据或将要更改的数据。这对于用户输入尤其方便。以搜索栏为例,用户输入数据时他们看到的内容也会更新。 + +16. 状态和 props 的区别。 + +条件|状态|Props +--|:--|:--| +从父组件接收初始值|是|是 +父组件可以更改值|否|是 +在组件内设置默认值|是|是 +在组件内更改|是|否 +为子组件设置初始值|是|是 +在子组件内更改|否|是 + +17. 如何更新组件的状态? + +可以使用 this.setState() 更新组件的状态。 + +``` jsx +class MyComponent extends React.Component { + constructor() { + super(); + this.state = { + name: 'Maxx', + id: '101' + } + } + render() + { + setTimeout(()=>{this.setState({name:'Jaeha', id:'222'})},2000) + return ( +
    +

    Hello {this.state.name}

    +

    Your Id is {this.state.id}

    +
    + ); + } + } + ReactDOM.render( + , document.getElementById('content') +); + +``` + +18.React 中的箭头函数是什么?如何使用? + +**粗箭头**=> 用于定义匿名函数,这通常是将参数传递给回调函数的最简单方法。但是你需要在使用它时优化性能。注意:每次渲染组件时,在 render 方法中使用箭头函数都会创建一个新函数,这可能会影响性能。 + +```jsx +//General way +render() { + return( + + ); +} +//With Arrow Function +render() { + return( + this.handleOnChange(e) } /> + ); +} + +``` + +19. 有状态和无状态组件的区别。 + +有状态组件|无状态组件 +--|:--| +在内存中存储组件状态更改的信息|计算组件的内部状态 +有权更改状态|无权更改状态 +包含过去、现在和可能的未来状态更改的信息|没有包含关于状态更改的信息 +无状态组件通知它们关于状态更改的需求,然后它们将 props 传递给前者|它们从有状态组件接收 props,将其视为回调函数 + +20. React 组件的生命周期有哪些阶段? + +React 组件的生命周期分为三个不同阶段: + +* **初始化**:在初始化阶段,我们为 this.props 和 this.state 定义默认值和初始值。 + +* **挂载**:挂载是将组件插入 DOM 时发生的过程 +* **更新**:这个阶段中,每当状态更改或组件收到新的 prop 时,组件都会更新 +* **卸载**:这是从 DOM 卸载组件的阶段。 + +21. 详细解释 React 组件的生命周期技术。 + +一些最重要的生命周期方法包括: + +* componentWillMount()——在初始渲染发生之前,即在 React 将组件插入 DOM 之前立即调用一次。请务必注意,在此方法中调用 this.setState() 不会触发重新渲染。 + +* componentDidMount()——在渲染函数之后触发此方法。现在可以访问更新的 DOM,这意味着该方法是初始化其他需要访问 DOM 的 Javascript 库以及数据提取操作的最佳选择。 + +* componentWillReceiveProps()——componentWillReceiveProps() 在组件接收新 props 时调用。在调用 render() 方法之前,我们可以用这个方法对 prop 过渡做出反应。在此函数中调用 this.setState() 不会触发额外的重新渲染,我们可以通过 this.props 访问旧的 props。 + +* shouldComponentUpdate()——我们可以用它来决定下一个组件的状态是否应触发重新渲染。此方法返回一个布尔值,默认为 true。但是我们可以返回 false,并且不会调用以下方法: + +* componentWillUpdate()——当接收到新的 props 或状态时,在渲染(更新)之前立即调用此方法。我们可以用它在更新之前做准备,但是不允许使用 this.setState()。 + +* componentDidUpdate()——React 更新 DOM 后立即调用此方法。我们可以使用此方法与更新后的 DOM 交互,或执行任何渲染后操作。 + +* componentWillUnmount()——从 DOM 卸载组件之前立即调用此方法。我们可以用它执行可能需要的任何清理工作。 + +22. React 中的事件是什么? + +在 React 中,事件是对特定动作(如鼠标悬停、鼠标单击和按键等)触发的反应。处理这些事件类似于处理 DOM 元素上的事件。但是在语法上存在一些差异,例如: + +* 事件命名使用驼峰式大小写,而不是仅使用小写字母。 + +* 事件作为函数而不是字符串传递。 + +事件参数包含一组特定于事件的属性。每个事件类型都包含它自己的属性和行为,这些属性和行为只能通过它的事件处理程序访问。 + +23. 如何在 React 中创建事件? + +```jsx +class Display extends React.Component({ + show(evt) { + // code + }, + render() { + // Render the div with an onClick prop (value is a function) + return ( +
    Click Me!
    + ); + } +}); + + +``` + +24. 什么是 React 中的合成事件? + +合成事件是围绕浏览器原生事件充当跨浏览器包装器的对象。它们将不同的浏览器行为合并为一个 API。这样做是为了确保在各个浏览器的事件中显示一致的特征。 + +25. 如何理解 React 中的引用? + +Ref 是 React 引用的简写。它是一个属性,帮助存储对特定元素或组件的引用,由组件渲染的配置函数返回。它用于返回对渲染返回的特定元素或组件的引用。当我们需要 DOM 测量或向组件添加方法时,它们会派上用场。 + +``` jsx +class ReferenceDemo extends React.Component{ + display() { + const name = this.inputDemo.value; + document.getElementById('disp').innerHTML = name; + } + render() { + return( +
    + Name: this.inputDemo = input} /> + + +

    Hello !!!

    +
    + ); + } + } + +``` + +26. 列出一些应该使用引用的情况? + +以下是应使用 ref 的情况: + +* 当你需要管理焦点、选择文本或媒体播放时。 + +* 触发命令式动画。 + +* 与第三方 DOM 库集成。 + +27. 如何模块化 React 代码? + +可以使用 export 和 import 属性来模块化软件。它们有助于在不同的文档中单独编写组件。 + +```jsx +//ChildComponent.jsx +export default class ChildComponent extends React.Component { + render() { + return(
    +

    This is a child component

    +
    ) + } +} + +//ParentComponent.jsx +import ChildComponent from './childcomponent.js'; +class ParentComponent extends React.Component { + render() { + return( +
    + +
    + ); + } +} + +``` + +28. 在 React 中如何创建表单? + +React 提供了一种有状态的,响应式的方法来构建表单。与其他 DOM 元素不同,HTML 表单元素在 React 中的工作机制有所不同。例如,表单数据通常由组件而不是 DOM 处理,并且通常使用受控组件来实现。 +区别在于可以使用回调函数来处理表单事件,然后使用容器的状态存储表单数据。这使你的组件可以更好地控制表单控制元素和表单数据。 +回调函数是在发生事件(包括更改表单控制值或表单提交)时触发的。 + +``` jsx +handleSubmit(event) { + alert('A name was submitted: ' + this.state.value); + event.preventDefault(); +} + +render() { + return ( + + + +
    + + +
    + + + + ); +} + +``` + +29. 如何理解受控和非受控组件? + +受控组件|非受控组件 +--|:--| +它们不维护自己的状态|它们维护自己的状态 +数据由父组件控制|数据由 DOM 控制 +它们通过 props 获得当前值,然后通过回调通知更改|使用引用来获得它们的当前值 + +30. 高阶组件(HOC)是什么意思? + +React 中的高阶组件是一种在组件之间共享通用功能而无需重复代码的模式。 +高阶组件实际上不是组件,它是一个接受组件并返回新组件的函数。它将一个组件转换为另一个组件,并添加其他数据或功能 + +31. HOC 可以做什么? + +HOC 可用于许多任务,例如: + +* 代码复用,逻辑和引导抽象。 + +* 渲染劫持。 +* 状态抽象和控制。 +* Props 操控。 + +32. 什么是纯组件? + +React 并没有在我们的组件中编写 shouldComponent 方法,而是引入了一个带有内置 shouldComponentUpdate 实现的新组件,它是 React.PureComponent 组件。 +React.PureComponent 通过浅层 prop 和状态比较来实现它。在某些情况下,你可以使用 React.PureComponent 来提高性能。 + +33. React 中键有什么用途? + +键可帮助 React 识别哪些项目已更改、添加或删除。应该为数组内的元素提供键,以赋予元素稳定的身份。键必须是唯一的。 +当使用动态创建的组件或用户更改列表时,React 键非常有用。设置键值后,更改后的组件就能保持唯一标识。 + +34. MVC 框架的主要问题有哪些? + +以下是 MVC 框架的一些主要问题: + +* MVC 不能解决代码复杂性问题。它也不能解决代码复用或灵活性问题。 +* 它不保证解耦代码。 + +35. 你对 Flux 有什么了解? + +Flux 是 Facebook 内部与 React 搭配使用的架构。它不是框架或库。只是一种新型的体系结构,是对 React 和单向数据流概念的补充: + +Flux 的各个组成部分如下: + +* 动作(Actions)——帮助数据传递到调度器的辅助方法。 +* 调度器(Dispatcher)——接收动作并将负载广播到已注册的回调。 +* 存储(Stores)——具有已注册到调度器的回调的应用程序状态和逻辑的容器。 +* 控制器视图(Controller Views)——从组件中获取状态并通过 props 传递给子组件的 React 组件。 + +!['ui-dom-4'](./../img/ui-dom-4.png) + +36. 什么是 Redux? + +Redux 是一种状态管理工具。尽管它主要与 React 搭配使用,但也可以与其他任何 JavaScript 框架或库搭配。 + +Redux 允许你在一个称为存储(Store)的对象中管理整个应用程序状态。 + +对存储的更新将触发与存储的已更新部分连接的组件的重新渲染。当我们想要更新某些东西时,我们称之为动作(Action)。我们还创建函数来处理这些动作并返回更新的存储。这些函数称为 Reducer。 + +37. Redux 遵循的三大原则是什么? + +* 单一可信来源:整个应用程序的状态存储在单个存储区中的对象 / 状态树中。单一状态树使我们能更容易地跟踪历史更改,更方便地调试或检查应用程序。 + +* 状态是只读的:更改状态的唯一方法是触发动作。动作是描述更改的普通 JS 对象。就像状态是数据的最小表示一样,动作是数据更改的最小表示。 + +* 使用纯函数更改:为了确认动作是如何转换状态树的,你需要纯函数。纯函数是返回值仅取决于其参数值的函数。 + +38. 如何理解“单一可信源”? + +单一可信源(SSOT)是构造信息模型和相关数据模式的实践,其中每个数据元素都只能在一个地方掌握(或编辑) +Redux 使用“存储”将应用程序的整个状态存储在一个位置。因此,组件的所有状态都存储在存储中,并且存储本身会接收更新。单一状态树使我们能更容易地跟踪历史更改,更方便地调试或检查应用程序。 + +39. 列出 Redux 的组件。 + +Redux 由以下组件组成: + +* 动作——这是一个描述发生了什么的对象。 +* Reducer——确定状态如何变化的地方。 +* 存储——整个应用程序的状态 / 对象树保存在存储中。 +* 视图——仅显示存储提供的数据。 + +40. 数据在 Redux 中是如何流动的? + +!['ui-dom-5'](./../img/ui-dom-5.png) + +41. 在 Redux 中如何定义动作? + +React 中的动作必须具有 type 属性,该属性指示正在执行的 ACTION 的类型。必须将它们定义为字符串常量,你也可以为其添加更多属性。在 Redux 中使用称为“动作创建者”的函数来创建动作。以下是动作和动作创建者的示例: + +``` jsx +function addTodo(text) { + return { + type: ADD_TODO, + text + } +} + +``` + +42. 说明 Reducer 的作用。 + +Reducer 是用于指示 ACTION 反应中应用程序状态变化的简单功能。它接收先前的状态和动作,然后返回新的状态。它根据动作类型确定需要哪种更新,然后返回新值。如果没有要完成的工作,它将按原样返回先前状态。 + +43. 在 Redux 中存储的用途是什么? + +存储是一个 JavaScript 对象,可以保存应用程序的状态,并提供一些辅助方法来访问状态、调度动作并记录侦听器。应用程序的整个状态 / 对象树存储在单个存储中。因此 Redux 非常容易理解且可预测。我们可以将中间件转移到存储,以管理数据处理任务,并维护更改存储状态的各种活动的日志。通过 Reducer,所有活动都返回新的状态。 + +44. Redux 与 Flux 有何不同? + +Flux|Redux +--|:--| +存储包括状态和更改逻辑|存储和更改逻辑是分离的 +有多个存储|只有一个存储 +所有存储不互通,是平行的|带有分层 Reducer 的单个存储 +有单个调度器|没有调度器的概念 +React 组件订阅到存储|容器组件是有联系的 +状态是可变的|状态是不可变的 + +45. Redux 有哪些优势? + +Redux 的优点如下: + +* 结果的可预测性——由于总是有单一可信源,比如存储,因此当前状态与动作及应用程序的其他部分同步时不会出现混乱。 +* 可维护性——代码易于维护,具有可预测的结果和严格的结构。 +* 服务端渲染——你只需将在服务器上创建的存储传递给客户端即可。这对于初始渲染非常有用,并优化了应用程序性能,提供了更好的用户体验。 +* 开发人员工具——从动作到状态更改,开发人员可以利用这些工具实时跟踪应用程序中发生的所有事情。 +* 社区和生态系统——Redux 背后拥有巨大的社区,用起来更加便利。大批优秀的开发者为库的发展做出了贡献,并开发了很多应用程序。 +* 易于测试——Redux 的代码主要是较小的、纯净的和孤立的函数。这使代码可测试且独立。 +* 组织——Redux 精确地规定了代码的组织方式,这使得团队合作时代码更加一致,更容易理解。 + +46. 什么是 React Router? + +React Router 是建立在 React 之上的功能强大的路由库。它使 URL 与网页上显示的数据保持同步。它保持标准化的结构和行为,可用于开发单页 Web 应用程序。React Router 有一个简单的 API。React Router 提供了一种方法,**只会显示你的应用中路由匹配你的定义的那些组件**。 + +47. 为什么在 React Router v4 中使用 switch 关键字? + +在 Switch 组件内,**Route**和**Redirect**组件嵌套在内部。从 Switch 顶部的 Route/Redirect 组件开始到底部的 Route/Redirect,根据浏览器中当前的 URL 是否与 Route/Redirect 组件的 prop/ 路径匹配,将每个组件评估为 true 或 false。 +Switch 只会渲染第一个匹配的子级。当我们嵌套了下面这样的路由时真的很方便: + +```jsx + + + + + +``` + +48. 为什么我们在 React 中需要一个路由器? + +路由器用于定义多个路由,并且当用户键入特定的 URL 时,如果该 URL 与路由器内部定义的任何“路由”的路径匹配,则该用户将被重定向到该路由。因此我们需要在应用程序中添加一个路由器库,以允许创建多个路由,每个路由都为我们指向一个独特的视图。 + +从 React Router 包导入的组件有两个属性,一个是将用户引导到指定路径的 path,另一个是用于定义所述路径中内容的 component。 + +```jsx + + + + + + +``` + +49. 列出 React Router 的优点。 + +几个优点是: + +1. 就像 React 基于组件的理念一样,在 React Router v4 中 API 是“完全组件化的”。路由器可以可视化为单个根组件(),其中包含特定的子路由()。 +2. 无需手动设置历史值:在 React Router v4 中,我们要做的就是将路由包装在组件中。 +3. 包是拆分的:三个包分别用于 Web、Native 和 Core。这使我们的应用更加紧凑。它们的编码样式类似,所以很容易来回切换。 + +50. React Router 与传统路由有何不同? + + +~|传统路由|React 路由 +--|:--|:--| +参与的页面|每个视图对应一个新页面|只涉及单个 HTML 页面 +URL 更改|向服务器发送一个 HTTP 请求并接收对应的 HTML 页面|只有历史属性被更改 +体验|用户其实是在每个视图的不同页面间切换|用户以为自己正在不同的页面间切换 + +[原文链接:](https://codersera.com/blog/top-50-react-questions-you-need-to-prepare-for-the-interview-in-2019/ )https://codersera.com/blog/top-50-react-questions-you-need-to-prepare-for-the-interview-in-2019/ diff --git a/blog/en-us/web/vue_cp_react.md b/blog/en-us/web/vue_cp_react.md new file mode 100644 index 0000000..16eaf25 --- /dev/null +++ b/blog/en-us/web/vue_cp_react.md @@ -0,0 +1,293 @@ +# 前端框架用vue还是react?清晰对比两者差异 + +## 前言 + +近两年前端技术层出不穷,目前市面上已经有了很多供前端人员使用的开发框架,转眼19年已过大半,前端框架领域日趋成熟,实现了三足鼎立的局面,截止到10月22日,Angular,react和vue数据统计如下图所示: + +![vue-react-1](./../img/vue-react-1.png) + +最近在学习使用框架的时候,分别使用vue和react开发了两个移动端产品,对这两个框架的学习曲线有了一些感悟,这两个都是现在比较热门的js框架,它俩在使用方式上和学习复杂度上还是有很大区别的,这里简单总结下两者的差异。 +主要从以下几个方面入手方面展开: + +* 框架的诞生 +* 设计思想 +* 编写语法 +* 脚手架构建工具 +* 数据绑定 +* 虚拟DOM +* 指令 +* 性能优化 +* 原生渲染native +* ssr服务端渲染 +* 生命周期函数 +* 销毁组件 +* 状态集管理工具 + +## 诞生 + +### vue + +vue由尤雨溪开发,由独立团队维护,现在大部分的子项目都交给团队成员打理,Vue核心库依然主要由尤雨溪亲自维护。vue近几年来特别的受关注,三年前的时候angularJS霸占前端JS框架市场很长时间,接着react框架横空出世,因为它有一个特性是虚拟DOM,从性能上碾轧angularJS,这个时候,vue1.0悄悄的问世了,它的优雅,轻便也吸引了一部分用户,开始受到关注,16年中旬,VUE2.0问世,不管从性能上,还是从成本上都隐隐超过了react,火的一塌糊涂,这个时候,angular开发团队也开发了angular2.0版本,并且更名为angular,吸收了react、vue的优点,加上angular本身的特点,也吸引到很多用户,目前已经迭代到8.0了。友情提示注意下vue的诞生时间,如果正好有小伙伴在面试,被问到你是从什么时候开始接触并且使用vue的,你要是回答用了5、6年了那场面就十分尴尬了。 + +### react + +起初facebook在建设instagram(图片分享)的时候,因为牵扯到一个东西叫数据流,那为了处理数据流并且还要考虑好性能方面的问题,Facebook开始对市场上的各种前端MVC框架去进行一个研究,然而并没有看上眼的,于是Facebook觉得,还是自己开发一个才是最棒的,那么他们决定抛开很多所谓的“最佳实践”,重新思考前端界面的构建方式,他们就自己开发了一套,果然大牛创造力还是很强大的。 +React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设Instagram 的网站。做出来以后,发现这套东西很好用,就在2013年5月开源了。 + +## 设计思想 + +### vue + +vue的官网中说它是一款渐进式框架,采用自底向上增量开发的设计。这里我们需要明确一个概念,什么是渐进式框架。在声明式渲染(视图模板引擎)的基础上,我们可以通过添加组件系统(components)、客户端路由(vue-router)、大规模状态管理(vuex)来构建一个完整的框架。Vue从设计角度来讲,虽然能够涵盖所有这些内容,但是你并不需要一上手就把所有东西全用上,因为没有必要。无论从学习角度,还是实际情况,这都是可选的。声明式渲染和组建系统是Vue的核心库所包含内容,而客户端路由、状态管理、构建工具都有专门解决方案。这些解决方案相互独立,你可以在核心的基础上任意选用其他的部件,不一定要全部整合在一起。可以看到,所说的“渐进式”,其实就是Vue的使用方式,同时也体现了Vue的设计的理念。 + +### react + +react主张函数式编程,所以推崇纯组件,数据不可变,单向数据流,当然需要双向的地方也可以手动实现,比如借助 onChange 和 setState 来实现一个双向的数据流。而vue是基于可变数据的,支持双向绑定,它提供了v-model这样的指令来实现文本框的数据流双向绑定。 + +## 编写语法 + +### vue +vue推荐的做法是webpack+vue-loader的单文件组件格式,vue保留了html、css、js分离的写法,使得现有的前端开发者在开发的时候能保持原有的习惯,更接近常用的web开发方式,模板就是普通的html,数据绑定使用mustache风格,样式直接使用css。其中**style**标签还提供了一个可选的scoped属性,它会为组件内 CSS 指定作用域,用它来控制仅对当前组件有效还是全局生效。 +模板和JSX是各有利弊的东西。模板更贴近我们的HTML,可以让我们更直观地思考语义结构,更好地结合CSS的书写。 +同时vue也支持JSX语法,因为是真正的JavaScript,拥有这个语言本身的所有的能力,可以进行复杂的逻辑判断,进行选择性的返回最终要返回的DOM结构,能够实现一些在模板的语法限制下,很难做到的一些事情。 + +### react + +用过react的开发者可能知道,react是没有模板的,直接就是一个渲染函数,它中间返回的就是一个虚拟DOM树,React推荐的做法是 JSX + inline style, 也就是把HTML和CSS全都写进JavaScript了,即'all in js'。JSX实际就是一套使用XML语法,用于让我们更简单地去描述树状结构的语法糖。在react中,所有的组件的渲染功能都依靠JSX。你可以在render()中编写类似XML的语法,它最终会被编译成原生JavaScript。不仅仅是 HTML 可以用 JSX 来表达,现在的潮流也越来越多地将 CSS 也纳入到 JavaScript 中来处理。JSX是基于 JS 之上的一套额外语法,学习使用起来有一定的成本。 + +## 构建工具 + +### vue + +vue提供了CLI 脚手架,可以帮助你非常容易地构建项目。全局安装之后,我们就可以用 vue create命令创建一个新的项目,vue 的 CLI 跟其他 CLI不同之处在于,有多个可选模板,有简单的也有复杂的,可以让用户自定义选择需要安装的模块,还可以将你的选择保存成模板,便于后续使用。 +极简的配置,更快的安装,可以更快的上手。它也有一个更完整的模板,包括单元测试在内的各种内容都涵盖,但是,它的复杂度也更高,这又涉及到根据用例来选择恰当复杂度的问题。 + +### react + +React 在这方面也提供了 create-react-app,但是现在还存在一些局限性: + +* 它不允许在项目生成时进行任何配置,而 Vue CLI 运行于可升级的运行时依赖之上,该运行时可以通过插件进行扩展。 +* 它只提供一个构建单页面应用的默认选项,而 Vue 提供了各种用途的模板。 +* 它不能用用户自建的预设配置构建项目,这对企业环境下预先建立约定是特别有用的。 + +而要注意的是这些限制是故意设计的,这有它的优势。例如,如果你的项目需求非常简单,你就不需要自定义生成过程。你能把它作为一个依赖来更新。 + +## 数据绑定 +### vue +vue是实现了双向数据绑定的mvvm框架,当视图改变更新模型层,当模型层改变更新视图层。在vue中,使用了双向绑定技术,就是View的变化能实时让Model发生变化,而Model的变化也能实时更新到View。 +Vue采用数据劫持&发布-订阅模式的方式,vue在创建vm的时候,会将数据配置在实例当中,然后通过Object.defineProperty对数据进行操作,为数据动态添加了getter与setter方法,当获取数据的时候会触发对应的getter方法,当设置数据的时候会触发对应的setter方法,从而进一步触发vm的watcher方法,然后数据更改,vm则会进一步触发视图更新操作。 +### react +react是单向数据流,react中属性是不允许更改的,状态是允许更改的。react中组件不允许通过this.state这种方式直接更改组件的状态。自身设置的状态,可以通过setState来进行更改。在setState中,传入一个对象,就会将组件的状态中键值对的部分更改,还可以传入一个函数,这个回调函数必须向上面方式一样的一个对象函数可以接受prevState和props。通过调用this.setState去更新this.state,不能直接操作this.state,请把它当成不可变的。 +调用setState更新this.state,它不是马上就会生效的,它是异步的。所以不要认为调用完setState后可以立马获取到最新的值。多个顺序执行的setState不是同步的一个接着一个的执行,会加入一个异步队列,然后最后一起执行,即批处理。 +setState是异步的,导致获取dom可能拿的还是之前的内容,所以我们需要在setState第二个参数(回调函数)中获取更新后的新的内容。 + +## diff算法 +### vue +vue中diff算法实现流程 + +在内存中构建虚拟dom树 +将内存中虚拟dom树渲染成真实dom结构 +数据改变的时候,将之前的虚拟dom树结合新的数据生成新的虚拟dom树 +将此次生成好的虚拟dom树和上一次的虚拟dom树进行一次比对(diff算法进行比对),来更新只需要被替换的DOM,而不是全部重绘。在Diff算法中,只平层的比较前后两棵DOM树的节点,没有进行深度的遍历。 +会将对比出来的差异进行重新渲染 + +### react +react中diff算法实现流程 + +DOM结构发生改变-----直接卸载并重新create +DOM结构一样-----不会卸载,但是会update变化的内容 +所有同一层级的子节点.他们都可以通过key来区分-----同时遵循1.2两点 +(其实这个key的存在与否只会影响diff算法的复杂度,换言之,你不加key的情况下,diff算法就会以暴力的方式去根据一二的策略更新,但是你加了key,diff算法会引入一些另外的操作) + +React会逐个对节点进行更新,转换到目标节点。而最后插入新的节点,涉及到的DOM操作非常多。diff总共就是移动、删除、增加三个操作,而如果给每个节点唯一的标识(key),那么React优先采用移动的方式,能够找到正确的位置去插入新的节点。 +vue会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。而对于React而言,每当应用的状态被改变时,全部组件都会重新渲染,所以react中会需要shouldComponentUpdate这个生命周期函数方法来进行控制。 + +## 指令 + +指令 (Directives) 是带有 +v- 前缀的特殊特性,指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。 +### vue +vue中提供很多内部指令供我们使用,它可以让我们进行一些模板的操作,例如有时候,我们的data中的存放的数据不是个简单的数字或者字符串,而是数组Array类型,这个时候,我们要把数组的元素展示在视图上,就需要用到vue提供的 v-for 指令,来实现列表的渲染。 +### react +因为react中没有v-for指令,所以循环渲染的时候需要用到map()方法来渲染视图,并且将符合条件的元素放入一个新数组返回。 + +## 性能优化 +### vue +vue中的每个组件内部自动实现了 +shouldComponentUpdate的优化,在vue里面由于依赖追踪系统的存在,当任意数据变动的时,Vue的每一个组件都精确地知道自己是否需要重绘,所以并不需要手动优化。用vue渲染这些组件的时候,数据变了,对应的组件基本上去除了手动优化的必要性。而在react中我们需要手动去优化其性能,但是当数据特别多的时候vue中的watcher也会特别多,从而造成页面卡顿,所以一般数据比较多的大型项目会倾向于使用react。在react官网中,官方也建议我们使用React来构建快速响应的大型 Web 应用程序。 +### react +当props或state发生改变的时候会触发 +shouldComponentUpdate生命周期函数,它是用来控制组件是否被重新渲染的,如果它返回true,则执行render函数,更新组件;如果它返回false,则不会触发重新渲染的过程。 +有的时候我们希望它在更新之前,和之前的状态进行一个对比,这个时候我们就需要重写 +shouldComponentUpdate来避免不必要的dom操作,对比当前的props或state和更新之后的nextProps或nextState,返回true时 ,组件更新;返回false,则不会更新,节省性能。 +``` js +shouldComponentUpdate(nextProps, nextState) { + if (this.props.a !== nextProps.a) { + return true; + } + if (this.state.b !== nextState.b) { + return true; + } + return false; +} +``` + +我们也可以创建一个继承React.PureComponent的React组件,它自带 +shouldComponentUpdate,可以对props进行浅比较,发现更新之后的props与当前的props一样,就不会进行render了。 +classTestextendsReact.PureComponent{constructor(props){super(props);}render(){return
    hello...{this.props.a}
    }} +由于React.PureComponent进行的是浅比较,也就是说它只会对比原对象的值是否相同,当我们的props或state为数组或者对象这种引用类型的时候,我们修改它的数值,由于数据引用指针没有发生改变,所以组件也是不会重新渲染的。这个时候我们就需要进行深拷贝,创建一个新的对象或数组,将原对象的各项属性的"值"(数组的所有元素)拷贝过来,是"值"而不仅仅是"引用地址"。我们可以使用slice()方法: +``` js +ew_state.todos = new_state.todos.slice(); +``` +或者引入immutable库来实现数据不可变。 + +## 原生渲染native +native指的是使用原生API来开发App,比如ios使用OC语言,android使用java。 +### vue +vue和Weex进行官方合作,weex是阿里巴巴发起的跨平台用户界面开发框架,它的思想是多个平台,只写一套代码,weex允许你使用 vue 语法开发不仅仅可以运行在浏览器端,还能被用于开发 iOS 和 Android 上的原生应用的组件。即只需要编写一份代码,即可运行在Web、iOS、Android上。 +weex相对来说上手比较简单,安装vue-cli之后就可以使用,学习门槛低,但是它的社区目前还处于成长期,react native的社区非常成熟活跃,有非常丰富的组件可供扩展。 +### react +react native是Facebook在2015年3月在F8开发者大会上开源的跨平台UI框架,需针对iOS、Android不同编写2份代码,使用react native需要按照文档安装配置很多依赖的工具,相对比较麻烦。weex的思想是多个平台,只写一套代码,而react-native的思想是多个平台可以写多套代码,但其使用的是同一套语言框架。 +weex的目标在于抹平各个平台的差异性,从而简化应用开发。而react-native承认了各个平台之间的差异,退而求其次,在语言和框架层面对平台进行抽象,从方法论的角度去解决多平台开发的问题。 + +## ssr服务端渲染 +服务端渲染核心在于方便seo优化,后端先调用数据库,获得数据之后,将数据和页面元素进行拼装,组合成完整的html页面,再直接返回给浏览器,以便用户浏览。 +### vue +2016 年 10 月 25 日,zeit.co背后的团队对外发布了 Next.js,一个 React 的服务端渲染应用框架。几小时后,与 Next.js 异曲同工,一个基于 Vue.js 的服务端渲染应用框架应运而生,我们称之为:Nuxt.js。 +服务端渲染支持流式渲染,因为HTTP请求也是流式,Vue 的服务端渲染结果可以直接 pipe 到返回的请求里面。这样一来,就可以更早地在浏览器中呈现给用户内容,通过合理的缓存策略,可以有效地提升服务端渲染的性能。 + +* 基于 Vue.js +* 自动代码分层 +* 服务端渲染 +* 强大的路由功能,支持异步数据 +* 静态文件服务 +* ES2015+ 语法支持 +* 打包和压缩 JS 和 CSS +* HTML 头部标签管理 +* 本地开发支持热加载 +* 集成 ESLint +* 支持各种样式预处理器: SASS、LESS、 Stylus 等等 +* 支持 HTTP/2 推送 + +### react +Next是一个React框架,允许使用React构建SSR和静态web应用 + +* 服务器渲染,获取数据非常简单 +* 无需学习新框架,支持静态导出。 +* 支持CSS-in-JS库 +* 自动代码拆分,加快页面加载速度,不加载不必要的代码 +* 基于Webpack的开发环境,支持模块热更新(HMR) +* 支持Babel和Webpack自定义配置服务器、路由和next插件。 +* 能够部署在任何能运行node的平台 +* 内置页面搜索引擎优化(SEO)处理 +* 在生产环境下,打包文件体积更小,运行速度更快 + +## 生命周期 +### vue +【初始化阶段(4个)】 +(1)beforeCreate +此钩子函数不能获取到数据,dom元素也没有渲染出来,此钩子函数不会用来做什么事情。 +(2)created +此钩子函数,数据已经挂载了,但是dom节点还是没有渲染出来,在这个钩子函数里面,如果同步更改数据的话,不会影响运行中钩子函数的执行。可以用来发送ajax请求,也可以做一些初始化事件的相关操作。 +(3)beforeMount +代表dom节点马上要被渲染出来了,但是还没有真正的渲染出来,此钩子函数跟created钩子函数基本一样,也可以做一些初始化数据的配置。 +(4)mounted +是生命周期初始化阶段的最后一个钩子函数,数据已经挂载完毕了,真实dom也可以获取到了。 +【运行中阶段(2个)】 +(5)beforeUpdate +运行中钩子函数beforeUpdate默认是不会执行的,当数据更改的时候,才会执行。数据更新的时候,先调用beforeUpdate,然后数据更新引发视图渲染完成之后,再会执行updated。运行时beforeUpdate这个钩子函数获取的数据还是更新之前的数据(获取的是更新前的dom内容),在这个钩子函数里面,千万不能对数据进行更改,会造成死循环。 +(6)updated +这个钩子函数获取的数据是更新后的数据,生成新的虚拟dom,跟上一次的虚拟dom结构进行比较,比较出来差异(diff算法)后再渲染真实dom,当数据引发dom重新渲染的时候,在updated钩子函数里面就可以获取最新的真实dom了。 +【销毁阶段(2个)】 +(7)beforeDestroy +切换路由的时候,组件就会被销毁了,销毁之前执行beforeDestroy。在这个钩子函数里面,我们可以做一些善后的操作,例如可以清空一下全局的定时器(created钩子函数绑定的初始化阶段的事件)、清除事件绑定。 +(8)destoryed +组件销毁后执行destroyed,销毁后组件的双向数据绑定、事件监听watcher相关的都被移除掉了,但是组件的真实dom结构还是存在在页面中的。 +添加keep-alive标签后会增加active和deactive这两个生命周期函数,初始化操作放在actived里面,一旦切换组件,因为组件没有被销毁,所以它不会执行销毁阶段的钩子函数,所以移除操作需要放在deactived里面,在里面进行一些善后操作,这个时候created钩子函数只会执行一次,销毁的钩子函数一直没有执行。 + +![vue-react-2](./../img/vue-react-2.png) + +### react +【初始化阶段(5个)】: +(1)getDefaultProps:实例化组件之后,组件的getDefaultProps钩子函数会执行 +这个钩子函数的目的是为组件的实例挂载默认的属性 +这个钩子函数只会执行一次,也就是说,只在第一次实例化的时候执行,创建出所有实例共享的默认属性,后面再实例化的时候,不会执行getDefaultProps,直接使用已有的共享的默认属性 +理论上来说,写成函数返回对象的方式,是为了防止实例共享,但是react专门为了让实例共享,只能让这个函数只执行一次 +组件间共享默认属性会减少内存空间的浪费,而且也不需要担心某一个实例更改属性后其他的实例也会更改的问题,因为组件不能自己更改属性,而且默认属性的优先级低。 +(2)getInitialState:为实例挂载初始状态,且每次实例化都会执行,也就是说,每一个组件实例都拥有自己独立的状态。 +(3)componentWillMount:执行componentWillMount,相当于Vue里的created+beforeMount,这里是在渲染之前最后一次更改数据的机会,在这里更改的话是不会触发render的重新执行。 +(4)render:渲染dom +render()方法必须是一个纯函数,他不应该改变 +state,也不能直接和浏览器进行交互,应该将事件放在其他生命周期函数中。 如果 +shouldComponentUpdate()返回 +false, +render()不会被调用。 +(5)componentDidMount:相当于Vue里的mounted,多用于操作真实dom +【运行中阶段(5个)】 +当组件mount到页面中之后,就进入了运行中阶段,在这里有5个钩子函数,但是这5个函数只有在数据(属性、状态)发送改变的时候才会执行 +(1)componentWillReceiveProps(nextProps,nextState) +当父组件给子组件传入的属性改变的时候,子组件的这个函数才会执行。初始化props时候不会主动执行 +当执行的时候,函数接收的参数是子组件接收到的新参数,这个时候,新参数还没有同步到this.props上,多用于判断新属性和原有属性的变化后更改组件的状态。 +(2)接下来就会执行shouldComponentUpdate(nextProps,nextState),这个函数的作用:当属性或状态发生改变后控制组件是否要更新,提高性能,返回true就更新,否则不更新,默认返回true。 +接收nextProp、nextState,根据根据新属性状态和原属性状态作出对比、判断后控制是否更新 +如果 +shouldComponentUpdate()返回 +false, +componentWillUpdate, +render和 +componentDidUpdate不会被调用。 +(3)componentWillUpdate,在这里,组件马上就要重新render了,多做一些准备工作,千万千万,不要在这里修改状态,否则会死循环 相当于Vue中的beforeUpdate +(4)render,重新渲染dom +(5)componentDidUpdate,在这里,新的dom结构已经诞生了,相当于Vue里的updated +【销毁阶段】 +当组件被销毁之前的一刹那,会触发componentWillUnmount,临死前的挣扎 +相当于Vue里的beforeDestroy,所以说一般会做一些善后的事情,例如使定时器无效,取消网络请求或清理在 +componentDidMount中创建的任何监听。 + +![vue-react-3](./../img/vue-react-3.png) + +## 销毁组件 + +### vue +vue在调用$destroy方法的时候就会执行beforeDestroy生命周期函数,然后组件被销毁,这个时候组件的dom结构还存在于页面结构中,也就说如果想要对残留的dom结构进行处理必须在destroyed生命周期函数中处理。 +### react +react执行完componentWillUnmount之后把事件、数据、dom都全部处理掉了,也就是说当父组件从渲染这个子组件变成不渲染这个子组件的时候,子组件相当于被销毁,所以根本不需要其他的钩子函数了。react销毁组件的时候,会将组件的dom结构也移除,vue则不然,在调用destory方法销毁组件的时候,组件的dom结构还是存在于页面中的,this.$destory组件结构还是存在的,只是移除了事件监听,所以这就是为什么vue中有destroyed,而react却没有componentDidUnmount。 + +## 状态集管理工具 +### vue +vuex是一个专门为vue构建的状态集管理工具,vue和react都是基于组件化开发的,项目中包含很多的组件,组件都会有组件嵌套,想让组件中的数据被其他组件也可以访问到就需要使用到Vuex。 +vuex的流程 + +将需要共享的状态挂载到state上:this.$store.state来调用 + +创建store,将状态挂载到state上,在根实例里面配置store,之后我们在组件中就可以通过this.$store.state来使用state中管理的数据,但是这样使用时,当state的数据更改的时候,vue组件并不会重新渲染,所以我们要通过计算属性computed来使用,但是当我们使用多个数据的时候这种写法比较麻烦,vuex提供了mapState辅助函数,帮助我们在组件中获取并使用vuex的store中保存的状态。 + +我们通过getters来创建状态:通过this.$store.getters来调用 + +可以根据某一个状态派生出一个新状态,vuex也提供了mapGetters辅助函数来帮助我们在组件中使用getters里的状态。 + +使用mutations来更改state:通过this.$store.commit来调用 + +我们不能直接在组件中更改state,而是需要使用mutations来更改,mutations也是一个纯对象,里面包含很多更改state的方法,这些方法的形参接收到state,在函数体里更改,这时,组件用到的数据也会更改,实现响应式。vuex提供了mapMutations方法来帮助我们在组件中调用mutations 的方法。 + +使用actions来处理异步操作:this.$store.dispatch来调用 + +Actions类似于mutations,不同在于:Actions提交的是mutations,而不是直接变更状态。Actions可以包含任意异步操作。也就是说,如果有这样的需求:在一个异步操作处理之后,更改状态,我们在组件中应该先调用actions,来进行异步动作,然后由actions调用mutations来更改数据。在组件中通过this.$store.dispatch方法调用actions的方法,当然也可以使用mapMutations来辅助使用。 +### react +2015年Redux出现,将 Flux 与函数式编程结合一起,很短时间内就成为了最热门的前端架构。它的出现主要是为解决react中组件之间的通信问题。建议把数据放入到redux中管理,目的就是方便数据统一,好管理。项目一旦出现问题,可以直接定位问题点。组件扩展的时候,后续涉及到传递的问题。本来的话,组件使用自己的数据,但是后来公用组件,还需要考虑如何值传递,在redux中可以存储至少5G以上的数据。 +redux的流程 + +![vue-react-4](./../img/vue-react-4.png) + +1. 创建store: 从redux工具中取出createStore去生成一个store。 +2. 创建一个reducer,然后将其传入到createStore中辅助store的创建。 reducer是一个纯函数,接收当前状态和action,返回一个状态,返回什么,store的状态就3是什么,需要注意的是,不能直接操作当前状态,而是需要返回一个新的状态。 想要给store创建默认状态其实就是给reducer一个参数创建默认值。 +4. 组件通过调用store.getState方法来使用store中的state,挂载在了自己的状态上。 +5. 组件产生用户操作,调用actionCreator的方法创建一个action,利用store.dispatch方法传递给reducer +6. reducer对action上的标示性信息做出判断后对新状态进行处理,然后返回新状态,这个时候store的数据就会发生改变, reducer返回什么状态,store.getState就可以获取什么状态。 +7. 我们可以在组件中,利用store.subscribe方法去订阅数据的变化,也就是可以传入一个函数,当数据变化的时候,传入的函数会执行,在这个函数中让组件去获取最新的状态。 + +## 小结 + +vue和react的核心都是专注于轻量级的视图层,虽然只是解决一个很小的问题,但是它们庞大的生态圈提供了丰富的配套工具,一开始它并不会给你提供全套的配置方案,将所有的功能都一次性给你打包好,它只会给你提供一些简单的核心功能,当你需要做一个更复杂的应用时,再增添相应的工具。例如做一个单页应用的时候才需要用路由;做一个相当庞大的应用,涉及到多组件状态共享以及多个开发者共同协作时,才可能需要大规模状态管理方案。 +框架的存在就是为了帮助我们应对不同的项目复杂度,当我们面对一个大型、复杂的开发项目时,使用太简陋的工具会极大的降低开发人员的生产力,影响工作效率,框架的诞生就是在这些工程中提取一些重复的并且已经受过验证的模式,抽象到一个已经帮你设计好的API封装当中,帮助我们去应对不同复杂度的问题。所以在开发的过程中,选择一个合适的框架就会事半功倍。但是,框架本身也有复杂度,有些框架会让人一时不知如何上手。当你接到一个并不复杂的需求,却使用了很复杂的框架,那么就相当于杀鸡用牛刀,会遇到工具复杂度所带来的副作用,不仅会失去工具本身所带来优势,还会增加各种问题,例如学习成本、上手成本,以及实际开发效率等。 +所以并不是说做得少的框架就不如做的做的框架,每个框架都有各自的优势和劣势,并不能找到完全符合需求的框架,最重要的适合当前项目,目前两大框架的生态圈一片繁荣,react社区是当前最活跃的,最快的时候三天更新一个版本,一个问题可能存在几十种不同的解决方案,这就需要我们前端人员去在不同的功能之间做取舍,以后前端框架的发展方向应该是小而精、灵活以及开放的,核心功能+生态附加库可以帮我们更加灵活的构建项目,为了跟上前进的脚步,就需要不停的吸收最新的内容,这也是从事前端开发领域的一大乐趣,希望大家都能在学习中获得长足的进步。 diff --git a/blog/zh-cn/dart/syntax.md b/blog/zh-cn/dart/syntax.md new file mode 100644 index 0000000..c480f08 --- /dev/null +++ b/blog/zh-cn/dart/syntax.md @@ -0,0 +1,1624 @@ +# Dart语法学习 + +## 目录 +* 参考资料 +* 语言特性 +* 关键字 +* 变量与常量 +* 数据类型 +* 运算符 operators +* 控制流程语句 +* 异常 Exceptions +* 函数 Function +* 类 Class +* 类-方法 +* 类-抽象类 +* 类-隐式接口 +* 类-扩展一个类(重写) +* 库和可见性 +* 异步支持 + +## 参考资料 + +* [【官方文档】](https://www.dartlang.org/guides/language/language-tour ) +* [【极客学院】](http://wiki.jikexueyuan.com/project/dart-language-tour/) +* [【author:AWeiLoveAndroid】](https://www.jianshu.com/p/3d927a7bf020) +* [【author:soojade】](https://www.jianshu.com/p/a7cc623132b0) +* [【author:优腾爱乐】](https://www.jianshu.com/p/8a62b1a2fd75) + +## 语言特性 + +* Dart所有的东西都是对象, 即使是数字numbers、函数function、null也都是对象,所有的对象都继承自Object类。 + +* Dart动态类型语言, 尽量给变量定义一个类型,会更安全,没有显示定义类型的变量在 debug 模式下会类型会是 dynamic(动态的)。 + +* Dart 在 running 之前解析你的所有代码,指定数据类型和编译时的常量,可以提高运行速度。 + +* Dart中的类和接口是统一的,类即接口,你可以继承一个类,也可以实现一个类(接口),自然也包含了良好的面向对象和并发编程的支持。 + +* Dart 提供了顶级函数(如:main())。 + +* Dart 没有 public、private、protected 这些关键字,变量名以"_"开头意味着对它的 lib 是私有的。 + +* 没有初始化的变量都会被赋予默认值 null。 + +* final的值只能被设定一次。const 是一个编译时的常量,可以通过 const 来创建常量值,var c=const[];,这里 c 还是一个变量,只是被赋值了一个常量值,它还是可以赋其它值。实例变量可以是 final,但不能是 const。 + +* 编程语言并不是孤立存在的,Dart也是这样,他由语言规范、虚拟机、类库和工具等组成: + + * SDK:SDK 包含 Dart VM、dart2js、Pub、库和工具。 + * Dartium:内嵌 Dart VM 的 Chromium ,可以在浏览器中直接执行 dart 代码。 + * Dart2js:将 Dart 代码编译为 JavaScript 的工具。 + * Dart Editor:基于 Eclipse 的全功能 IDE,并包含以上所有工具。支持代码补全、代码导航、快速修正、重构、调试等功能。 + +## 关键字(56个) + +关键字|-|-|- +-|-|-|- +abstract | do | import | super +as | dynamic | in | switch +assert | else | interface | sync +enum | implements | is | this +async | export | library | throw +await | external | mixin | true +break | extends | new | try +case | factory | null | typedef +catch | false | operator | var +class | final | part | void +const | finally | rethrow | while +continue | for | return | with +covariant | get | set | yield +default | if | static | deferred + +## 变量与常量 + +1. 变量声明与初始化 + +* 调用的变量name包含对String值为“张三” 的对象的引用,name推断变量的类型是String,但可以通过指定它来更改该类型,如果对象不限于单一类型(没有明确的类型),请使用Object或dynamic关键字。 +```dart + // 没有明确类型,编译的时候根据值明确类型 + var name = ‘Bob’; + Object name = '张三'; + dynamic name = '李四'; + + // 显示声明将被推断类型, 可以使用String显示声明字符串类型 + String name = 'Bob' ; +``` + +2. 默认值 + +* 未初始化的变量的初始值为null(包括数字),因此数字、字符串都可以调用各种方法 + +```dart + //测试 数字类型的初始值是什么? + int lineCount; + // 为false的时候抛出异常 + assert(lineCount == null); + print(lineCount); //打印结果为null,证明数字类型初始化值是null +``` + +3. final and const + * 如果您从未打算更改一个变量,那么使用 final 或 const,不是var,也不是一个类型。 + 一个 final 变量只能被初始化一次; const变量是一个编译时常量,(Const变量是隐式的final) + final的顶级或类变量在第一次使用时被初始化。 + + * 被final修饰的顶级变量或类变量在第一次声明的时候就需要初始化。 + + ``` dart + // The final variable 'outSideFinalName' must be initialized. + final String outSideFinalName + + ``` + + * 被final或者const修饰的变量,变量类型可以省略,建议指定数据类型。 + + ``` dart + //可以省略String这个类型声明 + final name = "Bob"; + final String name1 = "张三"; + + const name2 = "alex"; + const String name3 = "李四"; + ``` + + * 被 final 或 const 修饰的变量无法再去修改其值。 + + ``` dart + final String outSideFinalName = "Alex"; + // outSideFinalName', a final variable, can only be set once + // 一个final变量,只能被设置一次。 + outSideFinalName = "Bill"; + + const String outSideName = 'Bill'; + // 这样写,编译器提示:Constant variables can't be assigned a value + // const常量不能赋值 + // outSideName = "小白"; + ``` + + * flnal 或者 const 不能和 var 同时使用 + + ``` dart + // Members can't be declared to be both 'const' and 'var' + const var String outSideName = 'Bill'; + + // Members can't be declared to be both 'final' and 'var' + final var String name = 'Lili'; + ``` + + * 常量如果是类级别的,请使用 static const + + ``` dart + // 常量如果是类级别的,请使用 static const + static const String name3 = 'Tom'; + + // 这样写保存 + // Only static fields can be declared as const + // 只有静态字段可以声明为const + //const String name3 = 'Tom'; + ``` + + * 常量的运算 + + ``` dart + const speed = 100; //速度(km/h) + const double distance = 2.5 * speed; // 距离 = 时间 * 速度 + + final speed2 = 100; //速度(km/h) + final double distance2 = 2.5 * speed2; // 距离 = 时间 * 速度 + + ``` + + * const关键字不只是声明常数变量,您也可以使用它来创建常量值,以及声明创建常量值的构造函数,任何变量都可以有一个常量值。 + + ``` dart + // 注意: [] 创建的是一个空的list集合 + // const []创建一个空的、不可变的列表(EIL)。 + var varList = const []; // varList 当前是一个EIL + final finalList = const []; // finalList一直是EIL + const constList = const []; // constList 是一个编译时常量的EIL + + // 可以更改非final,非const变量的值 + // 即使它曾经具有const值 + varList = ["haha"]; + + // 不能更改final变量或const变量的值 + // 这样写,编译器提示:a final variable, can only be set once + // finalList = ["haha"]; + // 这样写,编译器提示:Constant variables can't be assigned a value + // constList = ["haha"]; + + ``` + + * 在常量表达式中,该运算符的操作数必须为'bool'、'num'、'String'或'null', const常量必须用conat类型的值初始化。 + + ``` dart + const String outSideName = 'Bill'; + final String outSideFinalName = 'Alex'; + const String outSideName2 = 'Tom'; + + const aConstList = const ['1', '2', '3']; + + // In constant expressions, operands of this operator must be of type 'bool', 'num', 'String' or 'null' + // 在常量表达式中,该运算符的操作数必须为'bool'、'num'、'String'或'null'。 + const validConstString = '$outSideName $outSideName2 $aConstList'; + + // Const variables must be initialized with a constant value + // const常量必须用conat类型的值初始化 + const validConstString = '$outSideName $outSideName2 $outSideFinalName'; + + var outSideVarName='Cathy'; + // Const variables must be initialized with a constant value. + // const常量必须用conat类型的值初始化 + const validConstString = '$outSideName $outSideName2 $outSideVarName'; + + // 正确写法 + const String outSideConstName = 'Joy'; + const validConstString = '$outSideName $outSideName2 $outSideConstName'; + + ``` + +## 数据类型 + +1. num + + * num 是数字类型的父类,有两个子类 int 和 double。 + + * int 根据平台的不同,整数值不大于64位。在Dart VM上,值可以从-263到263 - 1,编译成JavaScript的Dart使用JavaScript代码,允许值从-253到253 - 1。 + + * double 64位(双精度)浮点数,如IEEE 754标准所规定。 + + ``` dart + int a = 1; + print(a); + + double b = 1.12; + print(b); + + // String -> int + int one = int.parse('1'); + // 输出3 + print(one + 2); + + // String -> double + var onePointOne = double.parse('1.1'); + // 输出3.1 + print(onePointOne + 2); + + // int -> String + String oneAsString = 1.toString(); + // The argument type 'int' can't be assigned to the parameter type 'String' + //print(oneAsString + 2); + // 输出 1 + 2 + print('$oneAsString + 2'); + // 输出 1 2 + print('$oneAsString 2'); + + // double -> String 注意括号中要有小数点位数,否则报错 + String piAsString = 3.14159.toStringAsFixed(2); + // 截取两位小数, 输出3.14 + print(piAsString); + + String aString = 1.12618.toStringAsFixed(2); + // 检查是否四舍五入,输出1.13,发现会做四舍五入 + print(aString); + ``` + +2. String + + * Dart里面的String是一系列 UTF-16 代码单元。 + + * Dart里面的String是一系列 UTF-16 代码单元。 + + * 单引号或者双引号里面嵌套使用引号。 + + * 用 或{} 来计算字符串中变量的值,需要注意的是如果是表达式需要${表达式} + + ``` dart + String singleString = 'abcdddd'; + String doubleString = "abcsdfafd"; + + String sdString = '$singleString a "bcsd" ${singleString}'; + String dsString = "abc 'aaa' $sdString"; + print(sdString); + print(dsString); + + + String singleString = 'aaa'; + String doubleString = "bbb"; + // 单引号嵌套双引号 + String sdString = '$singleString a "bbb" ${doubleString}'; + // 输出 aaa a "bbb" bbb + print(sdString); + + // 双引号嵌套单引号 + String dsString = "${singleString.toUpperCase()} abc 'aaa' $doubleString.toUpperCase()"; + // 输出 AAA abc 'aaa' bbb.toUpperCase(), + 可以看出 ”$doubleString.toUpperCase()“ 没有加“{}“,导致输出结果是”bbb.toUpperCase()“ + print(dsString); + ``` + +3. bool + + * Dart 是强 bool 类型检查,只有bool 类型的值是true 才被认为是true。 + * 只有两个对象具有bool类型:true和false,它们都是编译时常量。 + * Dart的类型安全意味着您不能使用 if(nonbooleanValue) 或 assert(nonbooleanValue) 等代码, 相反Dart使用的是显式的检查值。 + * assert 是语言内置的断言函数,仅在检查模式下有效在开发过程中, 除非条件为真,否则会引发异常。(断言失败则程序立刻终止)。 + + ``` dart + // 检查是否为空字符串 + var fullName = ''; + assert(fullName.isEmpty); + + // 检查0 + var hitPoints = 0; + assert(hitPoints <= 0); + + // 检查是否为null + var unicorn; + assert(unicorn == null); + + // 检查是否为NaN + var iMeantToDoThis = 0 / 0; + assert(iMeantToDoThis.isNaN); + + ``` + +4. List集合 + + * 在Dart中,数组是List对象,因此大多数人只是将它们称为List。Dart list文字看起来像JavaScript数组文字 + + ``` dart + //创建一个int类型的list + List list = [10, 7, 23]; + // 输出[10, 7, 23] + print(list); + + // 使用List的构造函数,也可以添加int参数,表示List固定长度,不能进行添加 删除操作 + var fruits = new List(); + + // 添加元素 + fruits.add('apples'); + + // 添加多个元素 + fruits.addAll(['oranges', 'bananas']); + + List subFruits = ['apples', 'oranges', 'banans']; + // 添加多个元素 + fruits.addAll(subFruits); + + // 输出: [apples, oranges, bananas, apples, oranges, banans] + print(fruits); + + // 获取List的长度 + print(fruits.length); + + // 获取第一个元素 + print(fruits.first); + + // 获取元素最后一个元素 + print(fruits.last); + + // 利用索引获取元素 + print(fruits[0]); + + // 查找某个元素的索引号 + print(fruits.indexOf('apples')); + + // 删除指定位置的元素,返回删除的元素 + print(fruits.removeAt(0)); + + // 删除指定元素,成功返回true,失败返回false + // 如果集合里面有多个“apples”, 只会删除集合中第一个改元素 + fruits.remove('apples'); + + // 删除最后一个元素,返回删除的元素 + fruits.removeLast(); + + // 删除指定范围(索引)元素,含头不含尾 + fruits.removeRange(start,end); + + // 删除指定条件的元素(这里是元素长度大于6) + fruits.removeWhere((item) => item.length >6); + + // 删除所有的元素 + fruits.clear(); + ``` + * 注意事项: + + 1. 可以直接打印list包括list的元素,list也是一个对象。但是java必须遍历才能打印list,直接打印是地址值。 + 2. 和java一样list里面的元素必须保持类型一致,不一致就会报错。 + 3. 和java一样list的角标从0开始。 + 4. 如果集合里面有多个相同的元素“X”, 只会删除集合中第一个改元素 + +5. Map集合 + + * 一般来说,map是将键和值相关联的对象。键和值都可以是任何类型的对象。每个键只出现一次,但您可以多次使用相同的值。Dart支持map由map文字和map类型提供。 + * 初始化Map方式一: 直接声明,用{}表示,里面写key和value,每组键值对中间用逗号隔开。 + + ``` dart + // Two keys in a map literal can't be equal. + // Map companys = {'Alibaba': '阿里巴巴', 'Tencent': '腾讯', 'baidu': '百度', 'Alibaba': '钉钉', 'Tenect': 'qq-music'}; + + Map companys = {'Alibaba': '阿里巴巴', 'Tencent': '腾讯', 'baidu': '百度'}; + // 输出:{Alibaba: 阿里巴巴, Tencent: 腾讯, baidu: 百度} + print(companys); + + ``` + + * 创建Map方式二:先声明,再去赋值。 + + ``` dart + Map schoolsMap = new Map(); + schoolsMap['first'] = '清华'; + schoolsMap['second'] = '北大'; + schoolsMap['third'] = '复旦'; + // 打印结果 {first: 清华, second: 北大, third: 复旦} + print(schoolsMap); + + var fruits = new Map(); + fruits["first"] = "apple"; + fruits["second"] = "banana"; + fruits["fifth"] = "orange"; + //换成双引号,换成var 打印结果 {first: apple, second: banana, fifth: orange} + print(fruits); + ``` + + * Map API + + ``` dart + // 指定键值对的参数类型 + var aMap = new Map(); + + // Map的赋值,中括号中是Key,这里可不是数组 + aMap[1] = '小米'; + + //Map中的键值对是唯一的 + //同Set不同,第二次输入的Key如果存在,Value会覆盖之前的数据 + aMap[1] = 'alibaba'; + + // map里面的value可以相同 + aMap[2] = 'alibaba'; + + // map里面value可以为空字符串 + aMap[3] = ''; + + // map里面的value可以为null + aMap[4] = null; + + print(aMap); + + // 检索Map是否含有某Key + assert(aMap.containsKey(1)); + + //删除某个键值对 + aMap.remove(1); + + print(aMap); + ``` + + * 注意事项 + 1. map的key类型不一致也不会报错。 + 2. 添加元素的时候,会按照你添加元素的顺序逐个加入到map里面,哪怕你的key,比如分别是 1,2,4,看起来有间隔,事实上添加到map的时候是{1:value,2:value,4:value} 这种形式。 + 3. map里面的key不能相同。但是value可以相同,value可以为空字符串或者为null。 +## 运算符 + +描述 | 操作符 +-|- +一元后置操作符 | expr++ expr-- () [] . ?. +一元前置操作符 | expr !expr ~expr ++expr --expr +乘除 | * / % ~/ +加减 | + - +位移 | << >> +按位与 | & +按位或 | +按位异或 | ^ +逻辑与 | && +逻辑或 | +关系和类型判断 | >= > <= < as is is! +等 | == != +如果为空 | ?? +条件表达式 | expr1 ? expr2 : expr3 +赋值 | = *= /= ~/= %= += -= <<= >>= &= ^= = ??= +级联 | .. + +## 流程控制语句(Control flow statements) + +* if...else +* for +* while do-whild +* break continue +* break continue +* assert(仅在checked模式有效) + +## 异常(Exceptions) + +1. throw + * 抛出固定类型的异常 + ``` dart + throw new FormatException('Expected at least 1 section'); + ``` + + * 抛出任意类型的异常 + + ``` dart + throw 'Out of llamas!'; + ``` + * 因为抛出异常属于表达式,可以将throw语句放在=>语句中,或者其它可以出现表达式的地方 + + ``` dart + distanceTo(Point other) => + throw new UnimplementedError(); + ``` +2. catch + + * 将可能出现异常的代码放置到try语句中,可以通过 on语句来指定需要捕获的异常类型,使用catch来处理异常。 + ``` dart + try { + breedMoreLlamas(); + } on OutOfLlamasException { + // A specific exception + buyMoreLlamas(); + } on Exception catch (e) { + // Anything else that is an exception + print('Unknown exception: $e'); + } catch (e, s) { + print('Exception details:\n $e'); + print('Stack trace:\n $s'); + } + ``` + +3. rethrow + + * rethrow语句用来处理一个异常,同时希望这个异常能够被其它调用的部分使用。 + + ``` dart + final foo = ''; + + void misbehave() { + try { + foo = "1"; + } catch (e) { + print('2'); + rethrow;// 如果不重新抛出异常,main函数中的catch语句执行不到 + } + } + + void main() { + try { + misbehave(); + } catch (e) { + print('3'); + } + } + ``` +4. finally + + * Dart的finally用来执行那些无论异常是否发生都执行的操作。 + + ``` dart + + final foo = ''; + + void misbehave() { + try { + foo = "1"; + } catch (e) { + print('2'); + } + } + + void main() { + try { + misbehave(); + } catch (e) { + print('3'); + } finally { + print('4'); // 即使没有rethrow最终都会执行到 + } + } + + ``` + +## 函数 Function +* 以下是一个实现函数的例子: + +``` dart + bool isNoble(int atomicNumber) { + return _nobleGases[atomicNumber] != null; + } +``` + +1. main()函数 + * 每个应用程序都必须有一个顶层main()函数,它可以作为应用程序的入口点。该main()函数返回void并具有List参数的可选参数。 + ``` dart + void main() { + querySelector('#sample_text_id') + ..text = 'Click me!' + ..onClick.listen(reverseText); + } + + ``` + + * 级联符号..允许您在同一个对象上进行一系列操作。除了函数调用之外,还可以访问同一对象上的字段。这通常会为您节省创建临时变量的步骤,并允许您编写更流畅的代码。 + + ``` dart + querySelector('#confirm') // Get an object. + ..text = 'Confirm' // Use its members. + ..classes.add('important') + ..onClick.listen((e) => window.alert('Confirmed!')); + + ``` + + * 上述例子相对于: + + ``` dart + var button = querySelector('#confirm'); + button.text = 'Confirm'; + button.classes.add('important'); + button.onClick.listen((e) => window.alert('Confirmed!')); + + ``` + + * 级联符号也可以嵌套使用。 例如: + + ``` dart + final addressBook = (AddressBookBuilder() + ..name = 'jenny' + ..email = 'jenny@example.com' + ..phone = (PhoneNumberBuilder() + ..number = '415-555-0100' + ..label = 'home') + .build()) + .build(); + + ``` + + * 当返回值是void时不能构建级联。 例如,以下代码失败: + + ``` dart + var sb = StringBuffer(); + sb.write('foo') // 返回void + ..write('bar'); // 这里会报错 + + ``` + * 注意: 严格地说,级联的..符号不是操作符。它只是Dart语法的一部分。 + +2. 可选参数 + + * 可选的命名参数, 定义函数时,使用{param1, param2, …},用于指定命名参数。例如: + + ``` dart + //设置[bold]和[hidden]标志 + void enableFlags({bool bold, bool hidden}) { + // ... + } + + enableFlags(bold: true, hidden: false); + ``` + + * 可选的位置参数,用[]它们标记为可选的位置参数: + + ``` dart + String say(String from, String msg, [String device]) { + var result = '$from says $msg'; + if (device != null) { + result = '$result with a $device'; + } + return result; + } + ``` + * 下面是一个不带可选参数调用这个函数的例子: + + ``` dart + say('Bob', 'Howdy'); //结果是: Bob says Howdy + ``` + + * 下面是用第三个参数调用这个函数的例子: + + ``` dart + say('Bob', 'Howdy', 'smoke signal'); //结果是:Bob says Howdy with a smoke signal + ``` + +3. 默认参数 + + * 函数可以使用=为命名参数和位置参数定义默认值。默认值必须是编译时常量。如果没有提供默认值,则默认值为null。 + * 下面是为命名参数设置默认值的示例: + ``` dart + // 设置 bold 和 hidden 标记的默认值都为false + void enableFlags2({bool bold = false, bool hidden = false}) { + // ... + } + + // 调用的时候:bold will be true; hidden will be false. + enableFlags2(bold: true); + ``` + * 下一个示例显示如何为位置参数设置默认值: + ``` dart + String say(String from, String msg, + [String device = 'carrier pigeon', String mood]) { + var result = '$from says $msg'; + if (device != null) { + result = '$result with a $device'; + } + if (mood != null) { + result = '$result (in a $mood mood)'; + } + return result; + } + + //调用方式: + say('Bob', 'Howdy'); //结果为:Bob says Howdy with a carrier pigeon; + ``` + + * 您还可以将list或map作为默认值传递。下面的示例定义一个函数doStuff(),该函数指定列表参数的默认list和gifts参数的默认map。 + + ``` dart + // 使用list 或者map设置默认值 + void doStuff( + {List list = const [1, 2, 3], + Map gifts = const {'first': 'paper', + 'second': 'cotton', 'third': 'leather' + }}) { + print('list: $list'); + print('gifts: $gifts'); + } + ``` + +4. 作为一个类对象的功能 + + * 您可以将一个函数作为参数传递给另一个函数。 + + ``` dart + void printElement(int element) { + print(element); + } + + var list = [1, 2, 3]; + + // 把 printElement函数作为一个参数传递进来 + list.forEach(printElement); + ``` + + * 您也可以将一个函数分配给一个变量。 + + ``` dart + var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!'; + assert(loudify('hello') == '!!! HELLO !!!'); + ``` + +5. 匿名函数 + + * 大多数函数都能被命名为匿名函数,如 main() 或 printElement()。您还可以创建一个名为匿名函数的无名函数,有时也可以创建lambda或闭包。您可以为变量分配一个匿名函数,例如,您可以从集合中添加或删除它。 + * 一个匿名函数看起来类似于一个命名函数 - 0或更多的参数,在括号之间用逗号和可选类型标注分隔。 + * 下面的代码块包含函数的主体: + ``` dart + ([[Type] param1[, …]]) { + codeBlock; + }; + ``` + * 下面的示例定义了一个具有无类型参数的匿名函数item,该函数被list中的每个item调用,输出一个字符串,该字符串包含指定索引处的值。 + + ``` dart + var list = ['apples', 'bananas', 'oranges']; + list.forEach((item) { + print('${list.indexOf(item)}: $item'); + }); + ``` + * 如果函数只包含一条语句,可以使用箭头符号=>来缩短它, 比如上面的例2可以简写成: + + ``` dart + list.forEach((item) => print('${list.indexOf(item)}: $item')); + ``` +6. 返回值 + * 所有函数都返回一个值,如果没有指定返回值,则语句return null,隐式地附加到函数体。 + ``` dart + foo() {} + assert(foo() == null); + ``` +## 类(Classes) + +1. 对象 + + * Dart 是一种面向对象的语言,并且支持基于mixin的继承方式。 + * Dart 语言中所有的对象都是某一个类的实例,所有的类有同一个基类--Object。 + * 基于mixin的继承方式具体是指:一个类可以继承自多个父类。 + * 使用new语句来构造一个类,构造函数的名字可能是ClassName,也可以是ClassName.identifier, 例如: + ``` dart + var jsonData = JSON.decode('{"x":1, "y":2}'); + + // Create a Point using Point(). + var p1 = new Point(2, 2); + + // Create a Point using Point.fromJson(). + var p2 = new Point.fromJson(jsonData); + + ``` + * 使用.(dot)来调用实例的变量或者方法。 + ``` dart + var p = new Point(2, 2); + + // Set the value of the instance variable y. + p.y = 3; + + // Get the value of y. + assert(p.y == 3); + + // Invoke distanceTo() on p. + num distance = p.distanceTo(new Point(4, 4)); + ``` + * 使用?.来确认前操作数不为空, 常用来替代. , 避免左边操作数为null引发异常。 + + ``` dart + // If p is non-null, set its y value to 4. + p?.y = 4; + ``` + * 使用const替代new来创建编译时的常量构造函数。 + ``` dart + var p = const ImmutablePoint(2, 2); + ``` + * 使用runtimeType方法,在运行中获取对象的类型。该方法将返回Type 类型的变量。 + ``` dart + print('The type of a is ${a.runtimeType}'); + ``` + +2. 实例化变量(Instance variables) + * 在类定义中,所有没有初始化的变量都会被初始化为null。 + ``` dart + class Point { + num x; // Declare instance variable x, initially null. + num y; // Declare y, initially null. + num z = 0; // Declare z, initially 0. + } + ``` + + * 类定义中所有的变量, Dart语言都会隐式的定义 setter 方法,针对非空的变量会额外增加 getter 方法。 + + ``` dart + class Point { + num x; + num y; + } + + main() { + var point = new Point(); + point.x = 4; // Use the setter method for x. + assert(point.x == 4); // Use the getter method for x. + assert(point.y == null); // Values default to null. + } + ``` +3. 构造函数(Constructors) + * 声明一个和类名相同的函数,来作为类的构造函数。 + ``` dart + class Point { + num x; + num y; + + Point(num x, num y) { + // There's a better way to do this, stay tuned. + this.x = x; + this.y = y; + } + } + ``` + * this关键字指向了当前类的实例, 上面的代码可以简化为: + ``` dart + class Point { + num x; + num y; + + // Syntactic sugar for setting x and y + // before the constructor body runs. + Point(this.x, this.y); + } + ``` +4. 构造函数不能继承(Constructors aren’t inherited) + + * Dart 语言中,子类不会继承父类的命名构造函数。如果不显式提供子类的构造函数,系统就提供默认的构造函数。 + +5. 命名的构造函数(Named constructors) + * 使用命名构造函数从另一类或现有的数据中快速实现构造函数。 + ``` dart + class Point { + num x; + num y; + + Point(this.x, this.y); + + // 命名构造函数Named constructor + Point.fromJson(Map json) { + x = json['x']; + y = json['y']; + } + } + ``` + * 构造函数不能被继承,父类中的命名构造函数不能被子类继承。如果想要子类也拥有一个父类一样名字的构造函数,必须在子类是实现这个构造函数。 + + 6. 调用父类的非默认构造函数 + * 默认情况下,子类只能调用父类的无名,无参数的构造函数; 父类的无名构造函数会在子类的构造函数前调用; 如果initializer list 也同时定义了,则会先执行initializer list 中的内容,然后在执行父类的无名无参数构造函数,最后调用子类自己的无名无参数构造函数。即下面的顺序: + + 1. initializer list(初始化列表) + 2. super class’s no-arg constructor(父类无参数构造函数) + 3. main class’s no-arg constructor (主类无参数构造函数) + * 如果父类不显示提供无名无参数构造函数的构造函数,在子类中必须手打调用父类的一个构造函数。这种情况下,调用父类的构造函数的代码放在子类构造函数名后,子类构造函数体前,中间使用:(colon) 分割。 + ``` dart + class Person { + String firstName; + + Person.fromJson(Map data) { + print('in Person'); + } + } + + class Employee extends Person { + // 父类没有无参数的非命名构造函数,必须手动调用一个构造函数 + super.fromJson(data) + Employee.fromJson(Map data) : super.fromJson(data) { + print('in Employee'); + } + } + + main() { + var emp = new Employee.fromJson({}); + + // Prints: + // in Person + // in Employee + if (emp is Person) { + // Type check + emp.firstName = 'Bob'; + } + (emp as Person).firstName = 'Bob'; + } + + ``` + 7. 初始化列表 + * 除了调用父类的构造函数,也可以通过初始化列表在子类的构造函数体前(大括号前)来初始化实例的变量值,使用逗号,分隔。如下所示: + ``` dart + class Point { + num x; + num y; + + Point(this.x, this.y); + + // 初始化列表在构造函数运行前设置实例变量。 + Point.fromJson(Map jsonMap) + : x = jsonMap['x'], + y = jsonMap['y'] { + print('In Point.fromJson(): ($x, $y)'); + } + } + ``` + **注意:上述代码,初始化程序无法访问 this 关键字。** +8. 静态构造函数 + * 如果你的类产生的对象永远不会改变,你可以让这些对象成为编译时常量。为此,需要定义一个 const 构造函数并确保所有的实例变量都是 final 的。 + + ``` dart + class ImmutablePoint { + final num x; + final num y; + const ImmutablePoint(this.x, this.y); + static final ImmutablePoint origin = const ImmutablePoint(0, 0); + } + + ``` +9. 重定向构造函数 + + * 有时候构造函数的目的只是重定向到该类的另一个构造函数。重定向构造函数没有函数体,使用冒号:分隔。 + ``` dart + class Point { + num x; + num y; + + // 主构造函数 + Point(this.x, this.y) { + print("Point($x, $y)"); + } + + // 重定向构造函数,指向主构造函数,函数体为空 + Point.alongXAxis(num x) : this(x, 0); + } + + void main() { + var p1 = new Point(1, 2); + var p2 = new Point.alongXAxis(4); + } + ``` +10. 常量构造函数 + + * 如果类的对象不会发生变化,可以构造一个编译时的常量构造函数。定义格式如下: + * 定义所有的实例变量是final。 + * 使用const声明构造函数。 + ``` dart + class ImmutablePoint { + final num x; + final num y; + const ImmutablePoint(this.x, this.y); + static final ImmutablePoint origin = const ImmutablePoint(0, 0); + } + + ``` +11. 工厂构造函数 + + * 当实现一个使用 factory 关键词修饰的构造函数时,这个构造函数不必创建类的新实例。例如,工厂构造函数可能从缓存返回实例,或者它可能返回子类型的实例。 下面的示例演示一个工厂构造函数从缓存返回的对象: + + ``` dart + class Logger { + final String name; + bool mute = false; + + // _cache 是一个私有库,幸好名字前有个 _ 。 + static final Map _cache = {}; + + factory Logger(String name) { + if (_cache.containsKey(name)) { + return _cache[name]; + } else { + final logger = new Logger._internal(name); + _cache[name] = logger; + return logger; + } + } + + Logger._internal(this.name); + + void log(String msg) { + if (!mute) { + print(msg); + } + } + + } + ``` + **注意:工厂构造函数不能用 this。** +## 方法 +* 方法就是为对象提供行为的函数。 +1. 实例方法 + + * 对象的实例方法可以访问实例变量和 this 。以下示例中的 distanceTo() 方法是实例方法的一个例子: + ``` dart + import 'dart:math'; + + class Point { + num x; + num y; + Point(this.x, this.y); + + num distanceTo(Point other) { + var dx = x - other.x; + var dy = y - other.y; + return sqrt(dx * dx + dy * dy); + } + } + ``` +2. setters 和 Getters + + * 是一种提供对方法属性读和写的特殊方法。每个实例变量都有一个隐式的 getter 方法,合适的话可能还会有 setter 方法。你可以通过实现 getters 和 setters 来创建附加属性,也就是直接使用 get 和 set 关键词: + ``` dart + class Rectangle { + num left; + num top; + num width; + num height; + + Rectangle(this.left, this.top, this.width, this.height); + + // 定义两个计算属性: right and bottom. + num get right => left + width; + set right(num value) => left = value - width; + num get bottom => top + height; + set bottom(num value) => top = value - height; + } + + main() { + var rect = new Rectangle(3, 4, 20, 15); + assert(rect.left == 3); + rect.right = 12; + assert(rect.left == -8); + } + ``` + * 借助于 getter 和 setter ,你可以直接使用实例变量,并且在不改变客户代码的情况下把他们包装成方法。 + * 注: 不论是否显式地定义了一个 getter,类似增量(++)的操作符,都能以预期的方式工作。为了避免产生任何向着不期望的方向的影响,操作符一旦调用 getter ,就会把他的值存在临时变量里。 +3. 抽象方法 + * Instance , getter 和 setter 方法可以是抽象的,也就是定义一个接口,但是把实现交给其他的类。要创建一个抽象方法,使用分号(;)代替方法体: + ``` dart + abstract class Doer { + // ...定义实例变量和方法... + void doSomething(); // 定义一个抽象方法。 + } + + class EffectiveDoer extends Doer { + void doSomething() { + // ...提供一个实现,所以这里的方法不是抽象的... + } + } + + ``` + + 4. 枚举类型 + + * 枚举类型,通常被称为 enumerations 或 enums ,是一种用来代表一个固定数量的常量的特殊类。 + * 声明一个枚举类型需要使用关键字 enum : + ``` dart + enum Color { + red, + green, + blue + } + + ``` + * 在枚举中每个值都有一个 index getter 方法,它返回一个在枚举声明中从 0 开始的位置。例如,第一个值索引值为 0 ,第二个值索引值为 1 。 + + ``` dart + assert(Color.red.index == 0); + assert(Color.green.index == 1); + assert(Color.blue.index == 2); + ``` + * 要得到枚举列表的所有值,可使用枚举的 values 常量。 + + ``` dart + List colors = Color.values; + assert(colors[2] == Color.blue); + + ``` + +* 你可以在 switch 语句 中使用枚举。如果 e 在 switch (e) 是显式类型的枚举,那么如果你不处理所有的枚举值将会弹出警告: + + ``` dart + ***枚举类型有以下限制*** + * 你不能在子类中混合或实现一个枚举。 + * 你不能显式实例化一个枚举。 + enum Color { + red, + green, + blue + } + // ... + Color aColor = Color.blue; + switch (aColor) { + case Color.red: + print('Red as roses!'); + break; + + case Color.green: + print('Green as grass!'); + break; + + default: // Without this, you see a WARNING. + print(aColor); // 'Color.blue' + } + + ``` +5. 为类添加特征:mixins + + * mixins 是一种多类层次结构的类的代码重用。 + * 要使用 mixins ,在 with 关键字后面跟一个或多个 mixin 的名字。下面的例子显示了两个使用mixins的类: + ``` dart + class Musician extends Performer with Musical { + // ... + } + + class Maestro extends Person with Musical, + Aggressive, Demented { + + Maestro(String maestroName) { + name = maestroName; + canConduct = true; + } + } + ``` + * 要实现 mixin ,就创建一个继承 Object 类的子类,不声明任何构造函数,不调用 super 。例如: + ``` dart + abstract class Musical { + bool canPlayPiano = false; + bool canCompose = false; + bool canConduct = false; + + void entertainMe() { + if (canPlayPiano) { + print('Playing piano'); + } else if (canConduct) { + print('Waving hands'); + } else { + print('Humming to self'); + } + } + } + ``` +6. 类的变量和方法 + * 使用 static 关键字来实现类变量和类方法。 + * 只有当静态变量被使用时才被初始化。 + * 静态变量, 静态变量(类变量)对于类状态和常数是有用的: + ``` dart + class Color { + static const red = const Color('red'); // 一个恒定的静态变量 + final String name; // 一个实例变量。 + const Color(this.name); // 一个恒定的构造函数。 + } + + main() { + assert(Color.red.name == 'red'); + } + ``` + * 静态方法, 静态方法(类方法)不在一个实例上进行操作,因而不必访问 this 。例如: + + ``` dart + import 'dart:math'; + + class Point { + num x; + num y; + Point(this.x, this.y); + + static num distanceBetween(Point a, Point b) { + var dx = a.x - b.x; + var dy = a.y - b.y; + return sqrt(dx * dx + dy * dy); + } + } + + main() { + var a = new Point(2, 2); + var b = new Point(4, 4); + var distance = Point.distanceBetween(a, b); + assert(distance < 2.9 && distance > 2.8); + } + + ``` + * 注:考虑到使用高阶层的方法而不是静态方法,是为了常用或者广泛使用的工具和功能。 + * 你可以将静态方法作为编译时常量。例如,你可以把静态方法作为一个参数传递给静态构造函数。 + ## 抽象类 + + * 使用 abstract 修饰符来定义一个抽象类,该类不能被实例化。抽象类在定义接口的时候非常有用,实际上抽象中也包含一些实现。如果你想让你的抽象类被实例化,请定义一个 工厂构造函数 。 + * 抽象类通常包含 抽象方法。下面是声明一个含有抽象方法的抽象类的例子: + ``` dart + // 这个类是抽象类,因此不能被实例化。 + abstract class AbstractContainer { + // ...定义构造函数,域,方法... + + void updateChildren(); // 抽象方法。 + } + ``` + * 下面的类不是抽象类,因此它可以被实例化,即使定义了一个抽象方法: + ``` dart + class SpecializedContainer extends AbstractContainer { + // ...定义更多构造函数,域,方法... + + void updateChildren() { + // ...实现 updateChildren()... + } + + // 抽象方法造成一个警告,但是不会阻止实例化。 + void doSomething(); + } + ``` + + ## 类-隐式接口 + + * 每个类隐式的定义了一个接口,含有类的所有实例和它实现的所有接口。如果你想创建一个支持类 B 的 API 的类 A,但又不想继承类 B ,那么,类 A 应该实现类 B 的接口。 + + * 一个类实现一个或更多接口通过用 implements 子句声明,然后提供 API 接口要求。例如: + + ``` dart + + // 一个 person ,包含 greet() 的隐式接口。 + class Person { + // 在这个接口中,只有库中可见。 + final _name; + + // 不在接口中,因为这是个构造函数。 + Person(this._name); + + // 在这个接口中。 + String greet(who) => 'Hello, $who. I am $_name.'; + } + + // Person 接口的一个实现。 + class Imposter implements Person { + // 我们不得不定义它,但不用它。 + final _name = ""; + + String greet(who) => 'Hi $who. Do you know who I am?'; + } + + greetBob(Person person) => person.greet('bob'); + + main() { + print(greetBob(new Person('kathy'))); + print(greetBob(new Imposter())); + } + + ``` + * 这里是具体说明一个类实现多个接口的例子: + + ``` dart + class Point implements Comparable, Location { + // ... + } + ``` +## 类-扩展一个类 + +* 使用 extends 创建一个子类,同时 supper 将指向父类: + +``` dart + class Television { + void turnOn() { + _illuminateDisplay(); + _activateIrSensor(); + } + // ... + } + + class SmartTelevision extends Television { + + void turnOn() { + super.turnOn(); + _bootNetworkInterface(); + _initializeMemory(); + _upgradeApps(); + } + // ... + } +``` +* 子类可以重载实例方法, getters 方法, setters 方法。下面是个关于重写 Object 类的方法 noSuchMethod() 的例子,当代码企图用不存在的方法或实例变量时,这个方法会被调用。 +``` dart + class A { + // 如果你不重写 noSuchMethod 方法, 就用一个不存在的成员,会导致NoSuchMethodError 错误。 + void noSuchMethod(Invocation mirror) { + print('You tried to use a non-existent member:' + + '${mirror.memberName}'); + } + } + +``` +* 你可以使用 @override 注释来表明你重写了一个成员。 +``` dart + class A { + @override + void noSuchMethod(Invocation mirror) { + // ... + } + } + +``` + +* 如果你用 noSuchMethod() 实现每一个可能的 getter 方法,setter 方法和类的方法,那么你可以使用 @proxy 标注来避免警告。 + +``` dart +@proxy + class A { + void noSuchMethod(Invocation mirror) { + // ... + } + } +``` + +## 库和可见性 + +1. import,part,library指令可以帮助创建一个模块化的,可共享的代码库。库不仅提供了API,还提供隐私单元:以下划线(_)开头的标识符只对内部库可见。每个Dartapp就是一个库,即使它不使用库指令。 + +2. 库可以分布式使用包。见 Pub Package and Asset Manager 中有关pub(SDK中的一个包管理器)。 + +3. 使用库 + * 使用 import 来指定如何从一个库命名空间用于其他库的范围。 + * 使用 import 来指定如何从一个库命名空间用于其他库的范围。 + ``` dart + import 'dart:html'; + ``` + * 唯一需要 import 的参数是一个指向库的 URI。对于内置库,URI中具有特殊dart:scheme。对于其他库,你可以使用文件系统路径或package:scheme。包 package:scheme specifies libraries ,如pub工具提供的软件包管理器库。例如: + ``` dart + import 'dart:io'; + import 'package:mylib/mylib.dart'; + import 'package:utils/utils.dart'; + ``` +4. 指定库前缀 + + * 如果导入两个库是有冲突的标识符,那么你可以指定一个或两个库的前缀。例如,如果 library1 和 library2 都有一个元素类,那么你可能有这样的代码: + + ``` dart + import 'package:lib1/lib1.dart'; + import 'package:lib2/lib2.dart' as lib2; + // ... + var element1 = new Element(); // 使用lib1里的元素 + var element2 = + new lib2.Element(); // 使用lib2里的元素 + ``` +5. 导入部分库 + + * 如果想使用的库一部分,你可以选择性导入库。例如: + ``` dart + // 只导入foo库 + import 'package:lib1/lib1.dart' show foo; + + //导入所有除了foo + import 'package:lib2/lib2.dart' hide foo; + + ``` + + 6. 延迟加载库 + * 延迟(deferred)加载(也称为延迟(lazy)加载)允许应用程序按需加载库。下面是当你可能会使用延迟加载某些情况: + + * 为了减少应用程序的初始启动时间; + * 执行A / B测试-尝试的算法的替代实施方式中; + * 加载很少使用的功能,例如可选的屏幕和对话框。 + + + + * 为了延迟加载一个库,你必须使用 deferred as 先导入它。 + + ``` dart + import 'package:deferred/hello.dart' deferred as hello; + ``` + + * 当需要库时,使用该库的调用标识符调用 LoadLibrary()。 + ``` dart + greet() async { + await hello.loadLibrary(); + hello.printGreeting(); + } + ``` + * 在前面的代码,在库加载好之前,await关键字都是暂停执行的。有关 async 和 await 见 asynchrony support 的更多信息。 + 您可以在一个库调用 LoadLibrary() 多次都没有问题。该库也只被加载一次。 + + * 当您使用延迟加载,请记住以下内容: + + * 延迟库的常量在其作为导入文件时不是常量。记住,这些常量不存在,直到迟库被加载完成。 + * 你不能在导入文件中使用延迟库常量的类型。相反,考虑将接口类型移到同时由延迟库和导入文件导入的库。 + * Dart隐含调用LoadLibrary()插入到定义deferred as namespace。在调用LoadLibrary()函数返回一个Future。 + +7. 库的实现 + + * 用 library 来来命名库,用part来指定库中的其他文件。 注意:不必在应用程序中(具有顶级main()函数的文件)使用library,但这样做可以让你在多个文件中执行应用程序。 + + + + + +8. 声明库 + * 利用library identifier(库标识符)指定当前库的名称: + ``` dart + // 声明库,名ballgame + library ballgame; + + // 导入html库 + import 'dart:html'; + + // ...代码从这里开始... + ``` +9. 关联文件与库 + + * 添加实现文件,把part fileUri放在有库的文件,其中fileURI是实现文件的路径。然后在实现文件中,添加部分标识符(part of identifier),其中标识符是库的名称。下面的示例使用的一部分,在三个文件来实现部分库。 + + * 第一个文件,ballgame.dart,声明球赛库,导入其他需要的库,并指定ball.dart和util.dart是此库的部分: + ``` dart + library ballgame; + + import 'dart:html'; + // ...其他导入在这里... + + part 'ball.dart'; + part 'util.dart'; + + // ...代码从这里开始... + ``` + + * 第二个文件ball.dart,实现了球赛库的一部分: + ``` dart + part of ballgame; + + // ...代码从这里开始... + ``` + + * 第三个文件,util.dart,实现了球赛库的其余部分: + ``` dart + part of ballgame; + + // ...Code goes here... + ``` + +10. 重新导出库(Re-exporting libraries) + + *可以通过重新导出部分库或者全部库来组合或重新打包库。例如,你可能有实现为一组较小的库集成为一个较大库。或者你可以创建一个库,提供了从另一个库方法的子集。 + + ``` dart + // In french.dart: + library french; + + hello() => print('Bonjour!'); + goodbye() => print('Au Revoir!'); + + // In togo.dart: + library togo; + + import 'french.dart'; + export 'french.dart' show hello; + + // In another .dart file: + import 'togo.dart'; + + void main() { + hello(); //print bonjour + goodbye(); //FAIL + } + + ``` +## 异步的支持 + +1. Dart 添加了一些新的语言特性用于支持异步编程。最通常使用的特性是 async 方法和 await 表达式。Dart 库大多方法返回 Future 和 Stream 对象。这些方法是异步的:它们在设置一个可能的耗时操作(比如 I/O 操作)之后返回,而无需等待操作完成 + +2. 当你需要使用 Future 来表示一个值时,你有两个选择。 + + * 使用 async 和 await + * 使用 Future API + + + +3. 同样的,当你需要从 Stream 获取值的时候,你有两个选择。 + +使用 async 和一个异步的 for 循环 (await for) +使用 Stream API + + +4. 使用 async 和 await 的代码是异步的,不过它看起来很像同步的代码。比如这里有一段使用 await 等待一个异步函数结果的代码: +``` dart +await lookUpVersion() +``` + +5. 要使用 await,代码必须用 await 标记 +``` dart + checkVersion() async { + var version = await lookUpVersion(); + if (version == expectedVersion) { + // Do something. + } else { + // Do something else. + } + } +``` + + + +6. 你可以使用 try, catch, 和 finally 来处理错误并精简使用了 await 的代码。 +``` dart + try { + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 4044); + } catch (e) { + // React to inability to bind to the port... + } +``` + + +7. 声明异步函数 + * 一个异步函数是一个由 async 修饰符标记的函数。虽然一个异步函数可能在操作上比较耗时,但是它可以立即返回-在任何方法体执行之前。 + ``` dart + checkVersion() async { + // ... + } + + lookUpVersion() async => /* ... */; + ``` + * 在函数中添加关键字 async 使得它返回一个 Future,比如,考虑一下这个同步函数,它将返回一个字符串。 + * String lookUpVersionSync() => '1.0.0'; + * 如果你想更改它成为异步方法-因为在以后的实现中将会非常耗时-它的返回值是一个 Future 。 + * Future lookUpVersion() async => '1.0.0'; + * 请注意函数体不需要使用 Future API,如果必要的话 Dart 将会自己创建 Future 对象 + + +8. 使用带 future 的 await 表达式 + + * 一个 await表达式具有以下形式 + ``` dart + await expression + ``` + + * 在异步方法中你可以使用 await 多次。比如,下列代码为了得到函数的结果一共等待了三次。 + ``` dart + var entrypoint = await findEntrypoint(); + var exitCode = await runExecutable(entrypoint, args); + await flushThenExit(exitCode); + ``` + + * 在 await 表达式中, 表达式 的值通常是一个 Future 对象;如果不是,那么这个值会自动转为 Future。这个 Future 对象表明了表达式应该返回一个对象。await 表达式 的值就是返回的一个对象。在对象可用之前,await 表达式将会一直处于暂停状态。 + + * 如果 await 没有起作用,请确认它是一个异步方法。比如,在你的 main() 函数里面使用await,main() 的函数体必须被 async 标记: + + ``` dart + main() async { + checkVersion(); + print('In main: version is ${await lookUpVersion()}'); + } + + ``` + + + +9. 结合 streams 使用异步循环 + + * 一个异步循环具有以下形式: + ``` dart + await for (variable declaration in expression) { + // Executes each time the stream emits a value. + } + ``` + * 表达式 的值必须有Stream 类型(流类型)。执行过程如下: + + * 在 stream 发出一个值之前等待 + * 执行 for 循环的主体,把变量设置为发出的值。 + * 重复 1 和 2,直到 Stream 关闭 + + + * 如果要停止监听 stream ,你可以使用 break 或者 return 语句,跳出循环并取消来自 stream 的订阅 。 + * 如果一个异步 for 循环没有正常运行,请确认它是一个异步方法。 比如,在应用的 main() 方法中使用异步的 for 循环时,main() 的方法体必须被 async 标记。 + ``` dart + main() async { + ... + + await for (var request in requestServer) { + handleRequest(request); + } + + ... + } + ``` + + +更多关于异步编程的信息,请看 dart:async 库部分的介绍。你也可以看文章 [Dart Language Asynchrony Support: Phase 1 ](https://www.dartlang.org/articles/await-async/) diff --git a/blog/zh-cn/docker/docker-compose.md b/blog/zh-cn/docker/docker-compose.md new file mode 100644 index 0000000..9b2daf5 --- /dev/null +++ b/blog/zh-cn/docker/docker-compose.md @@ -0,0 +1,501 @@ +# docker和docker-compose 配置 mysql mssql mongodb redis nginx jenkins 环境 + +## 磁盘挂载 + +``` shell +fdisk -l #查看磁盘列表 +mkfs.ext4 /dev/vdb #格式化磁盘 +mount /dev/vdb /data #挂载磁盘在/data +echo '/dev/vdb /data ext4 defaults,nofail 0 1'>> /etc/fstab # 启动服务器自动挂载 +mount -a #校验自动挂载脚本 +df -h #查看磁盘挂载后信息 +``` + +## docker + +### 安装 docker + +``` shell +yum update #更新系统包 +yum install -y yum-utils device-mapper-persistent-data lvm2 #安装yum-utils +yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo #为yum源添加docker仓库位置 +yum install docker-ce #安装docker +systemctl enable docker #设置开机自动启动 +systemctl start docker #启动docker +systemctl stop docker #暂停docker +mv /var/lib/docker /data/docker # 修改Docker镜像的存放位置 +ln -s /data/docker /var/lib/docker #建立软连接 +echo '{ + "registry-mirrors": [ + "https://dockerhub.azk8s.cn", + "https://hub-mirror.c.163.com", + "https://registry.docker-cn.com" + ] +} +'>> /etc/docker/daemon.json # 镜像下载代理 +``` + +### 拉取 Java 镜像 + +``` shell +docker pull java +``` + +### 拉取SqlServer镜像 + +``` shell +docker pull microsoft/mssql-server-linux # 拉取SqlServer镜像 +docker run -p 1433:1433 --name mssql \ # run 运行容器 -p 将容器的1433端口映射到主机的1433端口 --name 容器运行的名字 +--restart=always \ # 挂断自动重新启动 +-v /data/sqlserver:/var/opt/mssql \ # 挂载mssql文件夹到主机 +-e ACCEPT_EULA=Y \ # 同意协议 +-e MSSQL_SA_PASSWORD=mssql-MSSQL \ # 初始化sa密码 +-u root \ # 指定容器为root运行 +-d microsoft/mssql-server-linux # -d 后台运行 +``` + +### 拉取 MySql 镜像 + +``` shell +docker pull mysql #拉取 MySql +docker run -p 3306:3306 --name mysql \ # run 运行容器 -p 将容器的3306端口映射到主机的3306端口 --name 容器运行的名字 +--restart=always \ # 挂断自动重新启动 +-v /etc/localtime:/etc/localtime \ # 将主机本地时间夹挂在到容器 +-v /data/mysql/log:/var/log/mysql \ # 将日志文件夹挂载到主机 +-v /data/mysql/data:/var/lib/mysql \ # 将数据文件夹挂载到主机 +-v /data/mysql/mysql-files:/var/lib/mysql-files \ # 将数据文件夹挂载到主机 +-v /data/mysql/conf:/etc/mysql \ # 将配置文件夹挂在到主机 +-e MYSQL_ROOT_PASSWORD=xiujingmysql. \ # 初始化root用户的密码 +-d mysql # -d 后台运行 +docker exec -it mysql /bin/bash # 进入Docker容器内部的bash +``` + +### 拉取 Mongodb 镜像 + +``` shell +docker pull mongo #拉取 mongodb +docker run -p 27017:27017 --name mongo \ # run 运行容器 -p 将容器的27017端口映射到主机的27017端口 --name 容器运行的名字 +--restart=always \ # 挂断自动重新启动 +-v /etc/localtime:/etc/localtime \ # 将主机本地时间夹挂在到容器 +-v /data/mongodb/db:/data/db \ # 将数据文件夹挂载到主机 +-v /data/mongodb/configdb:/data/configdb \ # 将数据库配置文件挂载到主机 +-v /data/mongodb/initdb:/docker-entrypoint-initdb.d # 通过/docker-entrypoint-initdb.d/将更复杂的用户设置显式留给用户 当容器首次启动时它会执行/docker-entrypoint-initdb.d 目录下的sh 和js脚本 。 以脚本字母顺序执行 +-e MONGO_INITDB_ROOT_USERNAME=admin \ # 设置admin数据库账户名称 如果使用了此项,则不需要 --auth 参数 +-e MONGO_INITDB_ROOT_PASSWORD=admin \ # 设置admin数据库账户密码 如果使用了此项,则不需要 --auth 参数 +-d mongo \ # -d 后台运行 +--auth # --auth 需要密码才能访问容器服务 + +docker exec -it mongo mongo admin # 进入mongo +db.createUser({ user:'admin',pwd:'123456',roles:[ { role:'userAdminAnyDatabase', db: 'admin'}]}); #创建一个名为 admin,密码为 123456 的用户。 +db.auth('admin', '123456') # 尝试使用上面创建的用户信息进行连接。 +``` + +### 拉取 Redis 镜像 + +``` shell +docker pull redis #拉取 redis +docker run -p 6379:6379 --name redis \ # run 运行容器 -p 将容器的6379端口映射到主机的6379端口 --name 容器运行的名字 +--restart=always \ # 挂断自动重新启动 +-v /etc/localtime:/etc/localtime \ # 将主机本地时间夹挂在到容器 +-v /data/redis:/data \ # 将数据文件夹挂载到主机 +-d redis \ # -d 后台运行 +redis-server --appendonly yes \ # 在容器执行redis-server启动命令,并打开redis持久化配置 +--requirepass "123456" # 设置密码123456 +``` + +### 拉取 Nginx 镜像 + +``` shell +docker pull nginx #拉取 nginx +docker run -p 80:80 -p 443:443 --name nginx -d nginx # 运行容器 +docker container cp nginx:/etc/nginx /data/nginx/ #拷贝容器配置 +docker rm -f nginx # 删除容器 +``` + +nginx 配置文件 + +``` conf + +user nginx; +worker_processes 1; + +error_log /var/log/nginx/error_log.log warn; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on; + + gzip on; #开启gzip + gzip_disable "msie6"; #IE6不使用gzip + gzip_vary on; #设置为on会在Header里增加 "Vary: Accept-Encoding" + gzip_proxied any; #代理结果数据的压缩 + gzip_comp_level 6; #gzip压缩比(1~9),越小压缩效果越差,但是越大处理越慢,所以一般取中间值 + gzip_buffers 16 8k; #获取多少内存用于缓存压缩结果 + gzip_http_version 1.1; #识别http协议的版本 + gzip_min_length 1k; #设置允许压缩的页面最小字节数,超过1k的文件会被压缩 + gzip_types application/javascript text/css; #对特定的MIME类型生效,js和css文件会被压缩 + + include /etc/nginx/conf.d/*.conf; + + server { + #nginx同时开启http和https + listen 80 default backlog=2048; + listen 443 ssl; + server_name ysf.djtlpay.com; + + ssl_certificate /ssl/1_ysf.djtlpay.com_bundle.crt; + ssl_certificate_key /ssl/2_ysf.djtlpay.com.key; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + } +} + +``` +运行 nginx +``` shell +docker run -p 80:80 -p 443:443 --name nginx \ # run 运行容器 -p 将容器的80,443端口映射到主机的80,443端口 --name 容器运行的名字 +--restart=always \ # 挂断自动重新启动 +-v /etc/localtime:/etc/localtime \ # 将主机本地时间夹挂在到容器 +-v /data/nginx/html:/usr/share/nginx/html \ # nginx 静态资源 +-v /data/nginx/logs:/var/log/nginx \ # 将日志文件夹挂载到主机 +-v /data/nginx/conf:/etc/nginx \ # 将配置文件夹挂在到主机 +-v /data/nginx/conf/ssl:/ssl \ # 将证书文件夹挂在到主机 +-d nginx # +``` + +### 拉取Jenkins镜像: +```shell +docker pull jenkins/jenkins:lts # 拉取 jenkins +docker run -p 8080:8080 -p 50000:50000 --name jenkins \ # run 运行容器 -p 将容器的8080,50000端口映射到主机的8080,50000端口 --name 容器运行的名字 +--restart=always \ # 挂断自动重新启动 +-u root \ # 运行的用户为root +-v /etc/localtime:/etc/localtime \ # 将主机本地时间夹挂在到容器 +-v /data/jenkins_home:/var/jenkins_home \ # 将jenkins_home文件夹挂在到主机 +-e JAVA_OPTS=-Duser.timezone=Asia/Shanghai \ #设置jenkins运行环境时区 +-d jenkins/jenkins:lts # -d 后台运行 +``` + +### 拉取MinIO镜像 +```shell +docker pull minio/minio # 拉取MinIO镜像 +docker run -p 9000:9000 --name minio \ # run 运行容器 -p 将容器的9000,9000端口映射到主机的9000,9000端口 --name 容器运行的名 +--restart=always \ # 挂断自动重新启动 +-v /etc/localtime:/etc/localtime \ # 将主机本地时间夹挂在到容器 +-v /data/minio/data:/data \ # 将data文件夹挂在到主机 +-v /data/minio/config:/root/.minio \ # 将配置文件夹挂在到主机 +-e "MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE" \ # 设置MINIO_ACCESS_KEY的值 +-e "MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" \ # 设置MINIO_SECRET_KEY值 +-d minio/minio server /data # -d 后台运行 server /data 导出/data目录 + +``` + +### 拉取Portainer镜像 + +``` shell +docker pull portainer/portainer # 拉取MinIO镜像 +docker run -p 8001:8000 -p 9001:9000 --name portainer \ # run 运行容器 -p 将容器的8000,9000端口映射到主机的8000,9000端口 --name 容器运行的名 +--restart=always \ # 挂断自动重新启动 +-v /etc/localtime:/etc/localtime \ # 将主机本地时间夹挂在到容器 +-v /var/run/docker.sock:/var/run/docker.sock \ # 将docker.sock文件夹挂在到主机 +-v /data/portainer/data:/data \ # 将配置文件夹挂在到主机 +-d portainer/portainer portainer # -d 后台运行 +``` + +### Docker 开启远程API + +* 用vi编辑器修改docker.service文件 +``` shell +vi /usr/lib/systemd/system/docker.service +#需要修改的部分: +ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock +#修改后的部分: +ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock +``` + +### Docker 常用命令 + +``` shell +systemctl start docker #启动docker +systemctl enable docker #将docker服务设为开机启动 +systemctl stop docker #停止容器 +systemctl restart docker #重启docker服务 +docker images # 列出镜像 +docker rmi --name # 删除镜像 -f 强制删除 +docker ps # 列出容器 -a 所有 +docker start --name # 启动容器 +docker stop --name # 停止容器 +docker restart --name # 重启docker容器 +docker rm --name # 删除容器 -f 强制删除 +docker stats -a # 查看所有容器情况 +docker system df # 查看Docker磁盘使用情况 +docker exec -it --name /bin/bash #进入Docker容器内部的bash +docker cp 主机文件 容器名称:容器路径 #复制文件到docker容器中 +docker logs --name #查看docker镜像日志 +docker rm $(docker ps -a -q) # 删除所有容器 -f 强制删除 +docker rmi $(docker images -a -q) # 删除所有镜像 -f 强制删除 +docker rm -f `docker ps -a | grep -vE 'mysql|nginx|redis|jenkins' | awk '{print $1}'` # 删除mysql|nginx|redis|jenkins非容器 -f 强制删除 +docker rmi -f `docker images | grep none | awk '{print $3}'` # 删除镜像none镜像 -f 强制删除 +``` + +## docker-compose + +### 安装 docker-compose +```shell +# 下载Docker Compose +curl -L https://get.daocloud.io/docker/compose/releases/download/1.24.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose +# 修改该文件的权限为可执行 +chmod +x /usr/local/bin/docker-compose +# 查看是否已经安装成功 +docker-compose --version +``` +### 使用Docker Compose的步骤 +* 使用Dockerfile定义应用程序环境,一般需要修改初始镜像行为时才需要使用; +* 使用docker-compose.yml定义需要部署的应用程序服务,以便执行脚本一次性部署; +* 使用docker-compose up命令将所有应用服务一次性部署起来。 + +### docker-compose.yml常用命令 +``` shell +# 指定运行的镜像名称 +image: name:version +# 配置容器名称 +container_name: name +# 指定宿主机和容器的端口映射 +ports: + - 3306:3306 +# 将宿主机的文件或目录挂载到容器中 +volumes: + - /etc/localtime:/etc/localtime + - /data/mysql/log:/var/log/mysql + - /data/mysql/data:/var/lib/mysql + - /data/mysql/conf:/etc/mysql + - /data/mysql/mysql-files:/var/lib/mysql-files +# 配置环境变量 +environment: + - MYSQL_ROOT_PASSWORD=xiujingmysql. +# 连接其他容器的服务 +links: + - db:database #可以以database为域名访问服务名称为db的容器 +# 挂断自动重新启动 +restart: always +# 指定容器执行命令 +command: redis-server --requirepass xiujingredis. +``` + +### Docker Compose常用命令 + +```shell +# 构建、创建、启动相关容器 +docker-compose up -d # -d表示在后台运行 +# 停止所有相关容器 +docker-compose stop +# 删除容器文件 +docker-compose rm -f # -f 强制删除 +# 重启容器 +docker-compose restart +# 列出所有容器信息 +docker-compose ps +# 查看容器日志 +docker-compose logs +``` + +### 使用Docker Compose 部署应用 + +编写docker-compose.yml文件 +``` yml +version: '3' +services: + # 指定服务名称 + nginx: + # 指定服务使用的镜像 + image: nginx + # 指定容器名称 + container_name: nginx + # 指定服务运行的端口 + ports: + - 80:80 + - 443:443 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + # 挂断自动重新启动 + restart: always + # 指定容器的环境变量 + environment: + - TZ=Asia/Shanghai # 设置容器时区与宿主机保持一致 + # 指定服务名称 + sqlserver: + # 指定服务使用的镜像 + image: mcr.microsoft.com/mssql/server + # 指定容器名称 + container_name: sqlserver + # 指定服务运行的端口 + ports: + - "1433" + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + - /data/sqlserver:/var/opt/mssql + # 挂断自动重新启动 + restart: always + environment: + - TZ=Asia/Shanghai + - SA_PASSWORD=mssql-MSSQL + - ACCEPT_EULA=Y + # 指定容器运行的用户为root + user: + root + # 指定服务名称 + mysql: + # 指定服务使用的镜像 + image: mysql + # 指定容器名称 + container_name: mysql + # 指定服务运行的端口 + ports: + - 3306:3306 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + - /data/mysql/log:/var/log/mysql + - /data/mysql/data:/var/lib/mysql + - /data/mysql/mysql-files:/var/lib/mysql-files + - /data/mysql/conf:/etc/mysql + # 挂断自动重新启动 + restart: always + # 指定容器的环境变量 + environment: + - TZ=Asia/Shanghai # 设置容器时区与宿主机保持一致 + - MYSQL_ROOT_PASSWORD=xiujingmysql. # 设置root密码 + # 指定容器运行的用户为root + user: + root + # 指定服务名称 + redis: + # 指定服务使用的镜像 + image: redis + # 指定容器名称 + container_name: redis + # 指定服务运行的端口 + ports: + - 6379:6379 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + - /data/redis:/data + - /data/redis/redis.conf:/etc/redis.conf + # 挂断自动重新启动 + restart: always + # 指定容器执行命令 + command: redis-server /etc/redis.conf --requirepass xiujingredis. --appendonly yes + # 指定容器的环境变量 + environment: + - TZ=Asia/Shanghai # 设置容器时区与宿主机保持一致 + # 指定服务名称 + mongo: + # 指定服务使用的镜像 + image: mongo + # 指定容器名称 + container_name: mongo + # 指定服务运行的端口 + ports: + - 27017:27017 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + - /data/mongodb/db:/data/db + - /data/mongodb/configdb:/data/configdb + - /data/mongodb/initdb:/docker-entrypoint-initdb.d + # 挂断自动重新启动 + restart: always + # 指定容器的环境变量 + environment: + - TZ=Asia/Shanghai # 设置容器时区与宿主机保持一致 + - AUTH=yes + - MONGO_INITDB_ROOT_USERNAME=admin + - MONGO_INITDB_ROOT_PASSWORD=admin + # 指定服务名称 + jenkins: + # 指定服务使用的镜像 + image: jenkins + # 指定容器名称 + container_name: jenkins + # 指定服务运行的端口 + ports: + - 8080:8080 + - 50000:50000 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + - /data/jenkins_home:/var/jenkins_home + # 挂断自动重新启动 + restart: always + # 指定容器的环境变量 + environment: + - TZ=Asia/Shanghai # 设置容器时区与宿主机保持一致 + - JAVA_OPTS=-Duser.timezone=Asia/Shanghai + # 指定容器运行的用户为root + user: + root + # 指定服务名称 + minio: + # 指定服务使用的镜像 + image: minio + # 指定容器名称 + container_name: minio + # 指定服务运行的端口 + ports: + - 9000:9000 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + - /data/minio/data:/data + - /data/minio/config:/root/.minio + # 挂断自动重新启动 + restart: always + # 指定容器执行命令 + command: server /data + # 指定服务名称 + portainer : + # 指定服务使用的镜像 + image: portainer + # 指定容器名称 + container_name: portainer + # 指定服务运行的端口 + ports: + - 8001:8000 + - 9001:9000 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + - /var/run/docker.sock:/var/run/docker.sock + - /data/portainer/data:/data + # 挂断自动重新启动 + restart: always +``` +运行Docker Compose命令启动所有服务 +``` shell +docker-compose up -d +``` diff --git a/blog/zh-cn/docker/docker-jenkins.md b/blog/zh-cn/docker/docker-jenkins.md new file mode 100644 index 0000000..44525d3 --- /dev/null +++ b/blog/zh-cn/docker/docker-jenkins.md @@ -0,0 +1,92 @@ +# Docker部署Jenkins + +## Jenkins简介 +Jenkins是开源CI&CD软件领导者,提供超过1000个插件来支持构建、部署、自动化,满足任何项目的需要。我们可以用Jenkins来构建和部署我们的项目,比如说从我们的代码仓库获取代码,然后将我们的代码打包成可执行的文件,之后通过远程的ssh工具执行脚本来运行我们的项目。 + +## Jenkins的安装及配置 + +### Docker环境下的安装 + +* 下载Jenkins的Docker镜像: +```shell +docker pull jenkins/jenkins:lts +``` +* 在Docker容器中运行Jenkins: +``` shell +docker run -p 8080:8080 -p 50000:50000 --name jenkins \ +-u root \ +-v /etc/localtime:/etc/localtime \ +-v /data/jenkins_home:/var/jenkins_home \ +-e JAVA_OPTS=-Duser.timezone=Asia/Shanghai \ +-d jenkins/jenkins:lts +``` +* echo 'Asia/Shanghai' >/etc/timezone + +### Jenkins的配置 +* 运行成功后访问该地址登录Jenkins,第一次登录需要输入管理员密码:http://localhost:8080/ + +![jenkins](./../img/jenkins-1.png) + +* 使用管理员密码进行登录,可以使用以下命令从容器启动日志中获取管理密码: + +``` shell +docker logs jenkins +``` + +![jenkins](./../img/jenkins-2.png) + +* 选择安装插件方式,这里我们直接安装推荐的插件: + +![jenkins](./../img/jenkins-3.png) + +* 进入插件安装界面,联网等待插件安装: + +![jenkins](./../img/jenkins-4.png) + +* 安装完成后,创建管理员账号: + +![jenkins](./../img/jenkins-5.png) + +* 进行实例配置,配置Jenkins的URL: + +![jenkins](./../img/jenkins-6.png) + +* 点击系统管理->插件管理,进行一些自定义的插件安装: + +![jenkins](./../img/jenkins-7.png) + +* 确保以下插件被正确安装: + 1. 根据角色管理权限的插件:Role-based Authorization Strategy + 2. 远程使用ssh的插件:SSH plugin + +* 通过系统管理->全局工具配置来进行全局工具的配置,比如maven的配置: + +![jenkins](./../img/jenkins-8.png) + +* 新增maven的安装配置: + +![jenkins](./../img/jenkins-9.png) + +* 在系统管理->系统配置中添加全局ssh的配置,这样Jenkins使用ssh就可以执行远程的linux脚本了: + +![jenkins](./../img/jenkins-10.png) + +## 角色权限管理 + +我们可以使用Jenkins的角色管理插件来管理Jenkins的用户,比如我们可以给管理员赋予所有权限,运维人员赋予执行任务的相关权限,其他人员只赋予查看权限。 + +* 在系统管理->全局安全配置中启用基于角色的权限管理: + +![jenkins](./../img/jenkins-11.png) + +* 进入系统管理->Manage and Assign Roles界面: + +![jenkins](./../img/jenkins-12.png) + +* 添加角色与权限的关系: + +![jenkins](./../img/jenkins-13.png) + +* 给用户分配角色: + +![jenkins](./../img/jenkins-14.png) diff --git a/blog/zh-cn/exp/cto.md b/blog/zh-cn/exp/cto.md new file mode 100644 index 0000000..6b3b2b4 --- /dev/null +++ b/blog/zh-cn/exp/cto.md @@ -0,0 +1,140 @@ +# CTO 技能图谱 + +### 岗位职责 +* 建立技术团队文化 +* 规划技术发展路线 +* 落地产品研发成果 +* 宣传公司技术品牌 +* 吸引优秀技术人才 + +### 基本素质 +* 正直诚实的道德修养 +* 谦虚谨慎的工作态度 +* 随机应变的处事风格 +* 统领全局的战略思维 + +### 硬技能 + +#### 技术能力 +* 具备一定的技术深度 +* 具备较强的技术广度 +* 追求技术选型合理性 +* 对技术发展嗅觉敏锐 + +#### 业务能力 +* 能深度理解业务本质 +* 能用技术来帮助业务 +* 让技术驱动业务发展 + +#### 架构能力 +* 能站在业务上设计架构 +* 架构规范合理且易落地 +* 能为架构设计提出意见 + +#### 产品能力 +* 具备一定的产品价值观 +* 能准确地抓住用户刚需 +* 能为产品设计提出意见 + +#### 管理能力 +- 团队管理 + * 招人:知道如何吸引所需的人? + * 识人:知道如何识别已有队员? + * 育人:知道如何让队员们成长? + * 开人:知道如何请人愉快离开? + * 留人:知道如何留住该留的人? + * 挖人:知道如何挖到需要的人? +- 项目管理 + * 估算:知道项目成本如何估算? + * 分工:知道工作任务如何分配? + * 排期:知道项目计划如何制定? + * 实施:知道项目实施如何开展? + * 发布:知道项目发布如何执行? + * 回顾:知道项目回顾如何进行? +- 绩效管理 + * 制定:知道如何制定考核标准? + * 执行:知道如何执行绩效考核? + * 优化:知道如何提升团队绩效? +- 时间管理 + * 制定:知道如何制定团队计划? + * 管理:知道如何管理团队任务? + * 权衡:知道如何权衡优先级别? +- 情绪管理 + * 控制:知道如何控制自我情绪? + * 转化:知道如何化悲观为乐观? + * 使用:知道如何善用自我情绪? + +### 软技能 + +#### 领导能力 +- 决策能力 + * 不要害怕做决定 + * 做出正确的决定 + * 敢于为决定负责 +- 影响能力 + * 不要改变别人而是激发别人 + * 用自己的行为和态度去激发 + * 持续不断提高自己的影响力 +- 沟通能力 + + 向上沟通(与公司创始人) + * 领会老板真实意图 + * 站在老板角度思考 + * 不要强迫改变老板 + + 向下沟通(与本部门同事) + * 是沟通而不是命令 + * 站在下属立场沟通 + * 不要吝啬夸赞队员 +- 横向沟通(与跨部门同事) + * 突出对方的重要性 + * 先要共识才能共赢 + * 懂得圆满大于完美 + +#### 执行能力 +* 理解执行目标 +* 提高执行效率 +* 确保执行效果 + +#### 学习能力 +* 对新知识充满好奇心 +* 能够快速学习新技能 +* 拥有触类旁通的能力 + +#### 组织能力 +* 积极主动并有活力 +* 做事情敢于放得开 +* 能够调用所需资源 + +#### 洞察能力 +* 善于抓住事物本质 +* 善于观察人性心理 +* 善于预见未来变化 + +#### 抗压能力 +* 学会释放压力 +* 化压力为动力 +* 化消极为积极 + +#### 自省能力 +* 能够不断自我反省 +* 能够从失败中总结 +* 能够在总结中提高 + +#### 战略能力 +* 能够深刻理解公司战略 +* 能够站在更高层次思考 +* 结合战略形成技术壁垒 + +#### 社交能力 +* 善于表达自己 +* 善于结交朋友 +* 善于公众演讲 + +#### 谈判能力 +* 能够通过谈判得到对方认同 +* 能够从谈判中找到共赢方式 +* 能够在谈判场合中保持镇定 + +#### 政治能力 +* 能够对政治持有敏感度 +* 能够处理办公室小政治 +* 能够主动避免政治风险 diff --git a/blog/zh-cn/img/Javavm11.png b/blog/zh-cn/img/Javavm11.png new file mode 100644 index 0000000..2e325fb Binary files /dev/null and b/blog/zh-cn/img/Javavm11.png differ diff --git a/blog/zh-cn/img/c_sqlserver_nginx1.png b/blog/zh-cn/img/c_sqlserver_nginx1.png new file mode 100644 index 0000000..848396c Binary files /dev/null and b/blog/zh-cn/img/c_sqlserver_nginx1.png differ diff --git a/blog/zh-cn/img/c_sqlserver_nginx2.png b/blog/zh-cn/img/c_sqlserver_nginx2.png new file mode 100644 index 0000000..cb68b10 Binary files /dev/null and b/blog/zh-cn/img/c_sqlserver_nginx2.png differ diff --git a/blog/zh-cn/img/c_sqlserver_nginx4.png b/blog/zh-cn/img/c_sqlserver_nginx4.png new file mode 100644 index 0000000..3ecb310 Binary files /dev/null and b/blog/zh-cn/img/c_sqlserver_nginx4.png differ diff --git a/blog/zh-cn/img/c_sqlserver_nginx5.png b/blog/zh-cn/img/c_sqlserver_nginx5.png new file mode 100644 index 0000000..a9e0e2f Binary files /dev/null and b/blog/zh-cn/img/c_sqlserver_nginx5.png differ diff --git a/blog/zh-cn/img/c_sqlserver_nginx6.png b/blog/zh-cn/img/c_sqlserver_nginx6.png new file mode 100644 index 0000000..97f0a5d Binary files /dev/null and b/blog/zh-cn/img/c_sqlserver_nginx6.png differ diff --git a/blog/zh-cn/img/c_sqlserver_nginx7.png b/blog/zh-cn/img/c_sqlserver_nginx7.png new file mode 100644 index 0000000..ce5f281 Binary files /dev/null and b/blog/zh-cn/img/c_sqlserver_nginx7.png differ diff --git a/blog/zh-cn/img/c_sqlserver_nginx8.png b/blog/zh-cn/img/c_sqlserver_nginx8.png new file mode 100644 index 0000000..76d2932 Binary files /dev/null and b/blog/zh-cn/img/c_sqlserver_nginx8.png differ diff --git a/blog/zh-cn/img/favicon.jpg b/blog/zh-cn/img/favicon.jpg new file mode 100644 index 0000000..4d647bb Binary files /dev/null and b/blog/zh-cn/img/favicon.jpg differ diff --git a/blog/zh-cn/img/javavm1.png b/blog/zh-cn/img/javavm1.png new file mode 100644 index 0000000..c380974 Binary files /dev/null and b/blog/zh-cn/img/javavm1.png differ diff --git a/blog/zh-cn/img/javavm10.png b/blog/zh-cn/img/javavm10.png new file mode 100644 index 0000000..cfb1607 Binary files /dev/null and b/blog/zh-cn/img/javavm10.png differ diff --git a/blog/zh-cn/img/javavm12.png b/blog/zh-cn/img/javavm12.png new file mode 100644 index 0000000..cfb1607 Binary files /dev/null and b/blog/zh-cn/img/javavm12.png differ diff --git a/blog/zh-cn/img/javavm13.png b/blog/zh-cn/img/javavm13.png new file mode 100644 index 0000000..385784b Binary files /dev/null and b/blog/zh-cn/img/javavm13.png differ diff --git a/blog/zh-cn/img/javavm14.png b/blog/zh-cn/img/javavm14.png new file mode 100644 index 0000000..be377e3 Binary files /dev/null and b/blog/zh-cn/img/javavm14.png differ diff --git a/blog/zh-cn/img/javavm15.png b/blog/zh-cn/img/javavm15.png new file mode 100644 index 0000000..f4050ff Binary files /dev/null and b/blog/zh-cn/img/javavm15.png differ diff --git a/blog/zh-cn/img/javavm16.png b/blog/zh-cn/img/javavm16.png new file mode 100644 index 0000000..137c4c6 Binary files /dev/null and b/blog/zh-cn/img/javavm16.png differ diff --git a/blog/zh-cn/img/javavm17.png b/blog/zh-cn/img/javavm17.png new file mode 100644 index 0000000..b326f1e Binary files /dev/null and b/blog/zh-cn/img/javavm17.png differ diff --git a/blog/zh-cn/img/javavm18.png b/blog/zh-cn/img/javavm18.png new file mode 100644 index 0000000..9664d11 Binary files /dev/null and b/blog/zh-cn/img/javavm18.png differ diff --git a/blog/zh-cn/img/javavm19.png b/blog/zh-cn/img/javavm19.png new file mode 100644 index 0000000..811edfe Binary files /dev/null and b/blog/zh-cn/img/javavm19.png differ diff --git a/blog/zh-cn/img/javavm2.png b/blog/zh-cn/img/javavm2.png new file mode 100644 index 0000000..b1934d6 Binary files /dev/null and b/blog/zh-cn/img/javavm2.png differ diff --git a/blog/zh-cn/img/javavm20.png b/blog/zh-cn/img/javavm20.png new file mode 100644 index 0000000..486d332 Binary files /dev/null and b/blog/zh-cn/img/javavm20.png differ diff --git a/blog/zh-cn/img/javavm3.png b/blog/zh-cn/img/javavm3.png new file mode 100644 index 0000000..290a8a9 Binary files /dev/null and b/blog/zh-cn/img/javavm3.png differ diff --git a/blog/zh-cn/img/javavm4.png b/blog/zh-cn/img/javavm4.png new file mode 100644 index 0000000..5393daa Binary files /dev/null and b/blog/zh-cn/img/javavm4.png differ diff --git a/blog/zh-cn/img/javavm5.png b/blog/zh-cn/img/javavm5.png new file mode 100644 index 0000000..45f3748 Binary files /dev/null and b/blog/zh-cn/img/javavm5.png differ diff --git a/blog/zh-cn/img/javavm6.png b/blog/zh-cn/img/javavm6.png new file mode 100644 index 0000000..a8c0dac Binary files /dev/null and b/blog/zh-cn/img/javavm6.png differ diff --git a/blog/zh-cn/img/javavm7.png b/blog/zh-cn/img/javavm7.png new file mode 100644 index 0000000..dfd2d26 Binary files /dev/null and b/blog/zh-cn/img/javavm7.png differ diff --git a/blog/zh-cn/img/javavm8.png b/blog/zh-cn/img/javavm8.png new file mode 100644 index 0000000..4d18652 Binary files /dev/null and b/blog/zh-cn/img/javavm8.png differ diff --git a/blog/zh-cn/img/javavm9.png b/blog/zh-cn/img/javavm9.png new file mode 100644 index 0000000..d463018 Binary files /dev/null and b/blog/zh-cn/img/javavm9.png differ diff --git a/blog/zh-cn/img/jenkins-1.png b/blog/zh-cn/img/jenkins-1.png new file mode 100644 index 0000000..3d8794b Binary files /dev/null and b/blog/zh-cn/img/jenkins-1.png differ diff --git a/blog/zh-cn/img/jenkins-10.png b/blog/zh-cn/img/jenkins-10.png new file mode 100644 index 0000000..5812612 Binary files /dev/null and b/blog/zh-cn/img/jenkins-10.png differ diff --git a/blog/zh-cn/img/jenkins-11.png b/blog/zh-cn/img/jenkins-11.png new file mode 100644 index 0000000..f5bbe24 Binary files /dev/null and b/blog/zh-cn/img/jenkins-11.png differ diff --git a/blog/zh-cn/img/jenkins-12.png b/blog/zh-cn/img/jenkins-12.png new file mode 100644 index 0000000..4e66687 Binary files /dev/null and b/blog/zh-cn/img/jenkins-12.png differ diff --git a/blog/zh-cn/img/jenkins-13.png b/blog/zh-cn/img/jenkins-13.png new file mode 100644 index 0000000..00bcf53 Binary files /dev/null and b/blog/zh-cn/img/jenkins-13.png differ diff --git a/blog/zh-cn/img/jenkins-14.png b/blog/zh-cn/img/jenkins-14.png new file mode 100644 index 0000000..765aeeb Binary files /dev/null and b/blog/zh-cn/img/jenkins-14.png differ diff --git a/blog/zh-cn/img/jenkins-2.png b/blog/zh-cn/img/jenkins-2.png new file mode 100644 index 0000000..afb3d50 Binary files /dev/null and b/blog/zh-cn/img/jenkins-2.png differ diff --git a/blog/zh-cn/img/jenkins-3.png b/blog/zh-cn/img/jenkins-3.png new file mode 100644 index 0000000..ed75c2c Binary files /dev/null and b/blog/zh-cn/img/jenkins-3.png differ diff --git a/blog/zh-cn/img/jenkins-4.png b/blog/zh-cn/img/jenkins-4.png new file mode 100644 index 0000000..8dbe6a5 Binary files /dev/null and b/blog/zh-cn/img/jenkins-4.png differ diff --git a/blog/zh-cn/img/jenkins-5.png b/blog/zh-cn/img/jenkins-5.png new file mode 100644 index 0000000..a809092 Binary files /dev/null and b/blog/zh-cn/img/jenkins-5.png differ diff --git a/blog/zh-cn/img/jenkins-6.png b/blog/zh-cn/img/jenkins-6.png new file mode 100644 index 0000000..0035c90 Binary files /dev/null and b/blog/zh-cn/img/jenkins-6.png differ diff --git a/blog/zh-cn/img/jenkins-7.png b/blog/zh-cn/img/jenkins-7.png new file mode 100644 index 0000000..a98c173 Binary files /dev/null and b/blog/zh-cn/img/jenkins-7.png differ diff --git a/blog/zh-cn/img/jenkins-8.png b/blog/zh-cn/img/jenkins-8.png new file mode 100644 index 0000000..27c573c Binary files /dev/null and b/blog/zh-cn/img/jenkins-8.png differ diff --git a/blog/zh-cn/img/jenkins-9.png b/blog/zh-cn/img/jenkins-9.png new file mode 100644 index 0000000..0e17a19 Binary files /dev/null and b/blog/zh-cn/img/jenkins-9.png differ diff --git a/blog/zh-cn/img/minio1.png b/blog/zh-cn/img/minio1.png new file mode 100644 index 0000000..69fbf38 Binary files /dev/null and b/blog/zh-cn/img/minio1.png differ diff --git a/blog/zh-cn/img/minio2.png b/blog/zh-cn/img/minio2.png new file mode 100644 index 0000000..d65f693 Binary files /dev/null and b/blog/zh-cn/img/minio2.png differ diff --git a/blog/zh-cn/img/minio3.png b/blog/zh-cn/img/minio3.png new file mode 100644 index 0000000..fa9432e Binary files /dev/null and b/blog/zh-cn/img/minio3.png differ diff --git a/blog/zh-cn/img/minio4.png b/blog/zh-cn/img/minio4.png new file mode 100644 index 0000000..518e71f Binary files /dev/null and b/blog/zh-cn/img/minio4.png differ diff --git a/blog/zh-cn/img/minio5.png b/blog/zh-cn/img/minio5.png new file mode 100644 index 0000000..19eed92 Binary files /dev/null and b/blog/zh-cn/img/minio5.png differ diff --git a/blog/zh-cn/img/minio6.png b/blog/zh-cn/img/minio6.png new file mode 100644 index 0000000..eae1ef3 Binary files /dev/null and b/blog/zh-cn/img/minio6.png differ diff --git a/blog/zh-cn/img/minio7.png b/blog/zh-cn/img/minio7.png new file mode 100644 index 0000000..d121ca6 Binary files /dev/null and b/blog/zh-cn/img/minio7.png differ diff --git a/blog/zh-cn/img/react-top.png b/blog/zh-cn/img/react-top.png new file mode 100644 index 0000000..3d9cf13 Binary files /dev/null and b/blog/zh-cn/img/react-top.png differ diff --git a/blog/zh-cn/img/ui-dom-1.png b/blog/zh-cn/img/ui-dom-1.png new file mode 100644 index 0000000..e95f74a Binary files /dev/null and b/blog/zh-cn/img/ui-dom-1.png differ diff --git a/blog/zh-cn/img/ui-dom-2.png b/blog/zh-cn/img/ui-dom-2.png new file mode 100644 index 0000000..194d5d6 Binary files /dev/null and b/blog/zh-cn/img/ui-dom-2.png differ diff --git a/blog/zh-cn/img/ui-dom-3.png b/blog/zh-cn/img/ui-dom-3.png new file mode 100644 index 0000000..4a02e9f Binary files /dev/null and b/blog/zh-cn/img/ui-dom-3.png differ diff --git a/blog/zh-cn/img/ui-dom-4.png b/blog/zh-cn/img/ui-dom-4.png new file mode 100644 index 0000000..0064f11 Binary files /dev/null and b/blog/zh-cn/img/ui-dom-4.png differ diff --git a/blog/zh-cn/img/ui-dom-5.png b/blog/zh-cn/img/ui-dom-5.png new file mode 100644 index 0000000..cee65ef Binary files /dev/null and b/blog/zh-cn/img/ui-dom-5.png differ diff --git a/blog/zh-cn/img/vue-react-1.png b/blog/zh-cn/img/vue-react-1.png new file mode 100644 index 0000000..b8a8d14 Binary files /dev/null and b/blog/zh-cn/img/vue-react-1.png differ diff --git a/blog/zh-cn/img/vue-react-2.png b/blog/zh-cn/img/vue-react-2.png new file mode 100644 index 0000000..455368b Binary files /dev/null and b/blog/zh-cn/img/vue-react-2.png differ diff --git a/blog/zh-cn/img/vue-react-3.png b/blog/zh-cn/img/vue-react-3.png new file mode 100644 index 0000000..7bb5ccb Binary files /dev/null and b/blog/zh-cn/img/vue-react-3.png differ diff --git a/blog/zh-cn/img/vue-react-4.png b/blog/zh-cn/img/vue-react-4.png new file mode 100644 index 0000000..4710440 Binary files /dev/null and b/blog/zh-cn/img/vue-react-4.png differ diff --git a/blog/zh-cn/java/javavm.md b/blog/zh-cn/java/javavm.md new file mode 100644 index 0000000..926c794 --- /dev/null +++ b/blog/zh-cn/java/javavm.md @@ -0,0 +1,613 @@ +# Java 虚拟机 + + +## 一、基本概念 + +### 1.1 OpenJDK + +自 1996 年 `JDK 1.0` 发布以来,Sun 公司在大版本上发行了 `JDK 1.1`、`JDK 1.2`、`JDK 1.3`、`JDK 1.4`、`JDK 5`,`JDK 6` ,这些版本的 JDK 都可以统称为 SunJDK 。之后在 2006 年的 JavaOne 大会上,Sun 公司宣布将 Java 开源,在随后的一年多里,它陆续将 JDK 的各个部分在 GPL v2(GNU General Public License,version 2)协议下开源,并建立了 OpenJDK 组织来对这些代码进行独立的管理,这就是 OpenJDK 的来源,此时的 OpenJDK 拥有当时 sunJDK 7 的几乎全部代码。 + +### 1.2 OracleJDK + +在 JDK 7 的开发期间,由于各种原因的影响 Sun 公司市值一路下跌,已无力推进 JDK 7 的开发,JDK 7 的发布一直被推迟。之后在 2009 年 Sun 公司被 Oracle 公司所收购,为解决 JDK 7 长期跳票的问题,Oracle 将 JDK 7 中大部分未能完成的项目推迟到 JDK 8 ,并于 2011 年发布了JDK 7,在这之后由 Oracle 公司正常发行的 JDK 版本就由 SunJDK 改称为 Oracle JDK。 + +在 2017 年 JDK 9 发布后,Oracle 公司宣布从此以后 JDK 将会在每年的 3 月和 9 月各发布一个大版本,即半年发行一个大版本,目的是为了避免众多功能被捆绑到一个 JDK 版本上而引发的无法交付的风险。 + + 在 JDK 11 发布后,Oracle 同步调整了 JDK 的商业授权,宣布从 JDK 11 起将以前的商业特性全部开源给 OpenJDK ,这样 OpenJDK 11 和 OracleJDK 11 的代码和功能,在本质上就完全相同了。同时还宣布以后都会发行两个版本的 JDK : + ++ 一个是在 GPLv2 + CE 协议下由 Oracle 开源的 OpenJDK; ++ 一个是在 OTN 协议下正常发行的 OracleJDK。 + +两者共享大部分源码,在功能上几乎一致。唯一的区别是 Oracle OpenJDK 可以在开发、测试或者生产环境中使用,但只有半年的更新支持;而 OracleJDK 对个人免费,但在生产环境中商用收费,可以有三年时间的更新支持。 + +### 1.3 HotSpot VM + +它是 Sun/Oracle JDK 和 OpenJDK 中默认的虚拟机,也是目前使用最为广泛的虚拟机。最初由 Longview Technologies 公司设计发明,该公司在 1997 年被 Sun 公司收购,随后 Sun 公司在 2006 年开源 SunJDK 时也将 HotSpot 虚拟机一并进行了开源。之后 Oracle 收购 Sun 以后,建立了 HotRockit 项目,用于将其收购的另外一家公司(BEA)的 JRockit 虚拟机中的优秀特性集成到 HotSpot 中。HotSpot 在这个过程里面移除掉永久代,并吸收了 JRockit 的 Java Mission Control 监控工具等功能。到 JDK 8 发行时,采用的就是集两者之长的 HotSpot VM 。 + +我们可以在自己的电脑上使用 `java -version` 来获得 JDK 的信息: + +```shell +C:\Users> java -version +java version "1.8.0_171" # 如果是openJDK, 则这里会显示:openjdk version +Java(TM) SE Runtime Environment (build 1.8.0_171-b11) +Java HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode) # 使用的是HotSpot虚拟机,默认为服务端模式 +``` + + + +## 二、Java 内存区域 + +
    + + +### 2.1 程序计数器 + +程序计数器(Program Counter Register)是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。字节码解释器通过改变程序计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要该计数器来完成。每条线程都拥有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。 + +### 2.2 Java虚拟机栈 + +Java 虚拟机栈(Java Virtual Machine Stack)也为线程私有,它描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法出口等信息。方法从调用到结束就对应着一个栈帧从入栈到出栈的过程。在《Java 虚拟机规范》中,对该内存区域规定了两类异常: + ++ 如果线程请求的栈深度大于虚拟机所允许的栈深度,将抛出 `StackOverflowError` 异常; ++ 如果 Java 虚拟机栈的容量允许动态扩展,当栈扩展时如果无法申请到足够的内存会抛出 `OutOfMemoryError` 异常。 + +### 2.3 本地方法栈 + +本地方法栈(Native Method Stacks)与虚拟机栈类似,其区别在于:Java 虚拟机栈是为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。 + +### 2.4 Java堆 + +Java 堆(Java Heap)是虚拟机所管理的最大一块的内存空间,它被所有线程所共享,用于存放对象实例。Java 堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为是连续的。Java 堆可以被实现成固定大小的,也可以是可扩展的,当前大多数主流的虚拟机都是按照可扩展来实现的,即可以通过最大值参数 `-Xmx` 和最小值参数 `-Xms` 进行设定。如果 Java 堆中没有足够的内存来完成实例分配,并且堆也无法再扩展时,Java 虚拟机将会抛出 `OutOfMemoryError` 异常。 + +### 2.5 方法区 + +方法区(Method Area)也是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。方法区也被称为 “非堆”,目的是与 Java 堆进行区分。《Java 虚拟机规范》规定,如果方法区无法满足新的内存分配需求时,将会抛出 `OutOfMemoryError` 异常。 + +运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存放常量池表(Constant Pool Table),常量池表中存放了编译期生成的各种符号字面量和符号引用。 + +## 三、对象 + +### 3.1 对象的创建 + +当我们在代码中使用 `new` 关键字创建一个对象时,其在虚拟机中需要经过以下步骤: + +**1. 类加载过程** + +当虚拟机遇到一条字节码 `new` 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就必须先执行相应的类加载过程。 + +**2. 分配内存** + +在类加载检查通过后,虚拟机需要新生对象分配内存空间。根据 Java 堆是否规整,可以有以下两种分配方案: + ++ **指针碰撞**:假设 Java 堆中内存是绝对规整的,所有使用的内存放在一边,所有未被使用的内存放在另外一边,中间以指针作为分界点指示器。此时内存分配只是将指针向空闲方向偏移出对象大小的空间即可,这种方式被称为指针碰撞。 + +
    + + ++ **空闲列表**:如果 Java 堆不是规整的,此时虚拟机需要维护一个列表,记录哪些内存块是可用的,哪些是不可用的。在进行内存分配时,只需要从该列表中选取出一块足够的内存空间划分给对象实例即可。 + +> 注:Java 堆是否规整取决于其采用的垃圾收集器是否带有空间压缩整理能力,后文将会介绍。 + +除了分配方式外,由于对象创建在虚拟机中是一个非常频繁的行为,此时需要保证在并发环境下的线程安全:如果一个线程给对象 A 分配了内存空间,但指针还没来得及修改,此时就可能出现另外一个线程使用原来的指针来给对象 B 分配内存空间的情况。想要解决这个问题有两个方案: + ++ **方式一**:采用同步锁定,或采用 CAS 配上失败重试的方式来保证更新操作的原子性。 ++ **方式二**:为每个线程在 Java 堆中预先分配一块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。线程在进行内存分配时优先使用本地缓冲,当本地缓冲使用完成后,再向 Java 堆申请分配,此时 Java 堆采用同步锁定的方式来保证分配行为的线程安全。 + +**3. 对象头设置** + +将对象有关的元数据信息、对象的哈希码、分代年龄等信息存储到对象头中。 + +**4. 对象初始化** + +调用对象的构造函数,即 Class 文件中的 `()` 来初始化对象,为相关字段赋值。 + +### 3.2 对象的内存布局 + +在 HotSpot 虚拟机中,对象在堆内存中的存储布局可以划分为以下三个部分: + +**1. 对象头 (Header)** + +对象头包括两部分信息: + ++ **Mark Word**:对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,官方统称为 Mark Word 。 ++ **类型指针**:对象指向它类型元数据的指针,Java 虚拟机通过这个指针来确定该对象是哪个类的示例。需要说明的是并非所有的虚拟机都必须要在对象数据上保留类型指针,这取决于对象的访问定位方式(详见下文)。 + +**2. 实例数据 (Instance Data)** + +即我们在程序代码中定义的各种类型的字段的内容,无论是从父类继承而来,还是子类中定义的都需要记录。 + +**3. 对其填充 (Padding)** + +主要起占位符的作用。HotSpot 虚拟机要求对象起始地址必须是 8 字节的整倍数,即间接要求了任何对象的大小都必须是 8 字节的整倍数。对象头部分在设计上就是 8 字节的整倍数,如果对象的实例数据不是 8 字节的整倍数,则由对齐填充进行补全。 + +### 3.3 对象的访问定位 + +对象创建后,Java 程序就可以通过栈上的 `reference` 来操作堆上的具体对象。《Java 虚拟机规范》规定 `reference` 是一个指向对象的引用,但并未规定其具体实现方式。主流的方式方式有以下两种: + ++ **句柄访问**:Java 堆将划分出一块内存来作为句柄池, `reference` 中存储的是对象的句柄地址,而句柄则包含了对象实例数据和类型数据的地址信息。 ++ **指针访问**:`reference` 中存储的直接就是对象地址,而对象的类型数据则由上文介绍的对象头中的类型指针来指定。 + +通过句柄访问对象: + +
    + + +通过直接指针访问对象: + +
    + + +句柄访问的优点在于对象移动时(垃圾收集时移动对象是非常普遍的行为)只需要改变句柄中实例数据的指针,而 `reference` 本生并不需要修改;指针访问则反之,由于其 `reference` 中存储的直接就是对象地址,所以当对象移动时, `reference` 需要被修改。但针对只需要访问对象本身的场景,指针访问则可以减少一次定位开销。由于对象访问是一项非常频繁的操作,所以这类减少的效果会非常显著,基于这个原因,HotSpot 主要使用的是指针访问的方式。 + +## 四、垃圾收集算法 + +在 Java 虚拟机内存模型中,程序计数器、虚拟机栈、本地方法栈这 3 个区域都是线程私有的,会随着线程的结束而销毁,因此在这 3 个区域当中,无需过多考虑垃圾回收问题。垃圾回收问题主要发生在 Java 堆和方法区上。 + +### 4.1 Java 堆回收 + +在 Java 堆上,垃圾回收的主要内容是死亡对象(不可能再被任何途径使用的对象)。而判断对象是否死亡有以下两种方法: + +#### 1. 引用计数法 + +在对象中添加一个引用计数器,对象每次被引用时,该计数器加一;当引用失效时,计数器的值减一;只要计数器的值为零,则代表对应的对象不可能再被使用。该方法的缺点在于无法避免相互循环引用的问题: + +```java +objA.instance = objB +objB.instance = objA +objA = null; +objB = null; +System.gc(); +``` + +如上所示,此时两个对象已经不能再被访问,但其互相持有对对方的引用,如果采用引用计数法,则两个对象都无法被回收。 + +#### 2. 可达性分析 + +上面的代码在大多数虚拟机中都能被正确的回收,因为大多数主流的虚拟机都是采用的可达性分析方法来判断对象是否死亡。可达性分析是通过一系列被称为 `GC Roots` 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径被称为引用链(Reference Chain),如果某个对象到 `GC Roots` 间没有任何引用链相连,这代表 `GC Roots` 到该对象不可达, 此时证明此该对象不可能再被使用。 + +
    + + +在 Java 语言中,固定可作为 `GC Roots` 的对象包括以下几种: + ++ 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等; ++ 在方法区中类静态属性引用的对象,譬如 Java 类中引用类型的静态变量; ++ 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用; ++ 在本地方法栈中的 JNI(即 Native 方法)引用的对象; ++ Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象(如 NullPointException,OutOfMemoryError 等)及系统类加载器; ++ 所有被同步锁(synchronized 关键字)持有的对象; ++ 反应 Java 虚拟机内部情况的 JMXBean,JVMTI 中注册的回调,本地代码缓存等。 + +除了这些固定的 `GC Roots` 集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域的不同,还可能会有其他对象 “临时性” 地加入,共同构成完整的 `GC Roots` 集合。 + +#### 3. 对象引用 + +可达性分析是基于引用链进行判断的,在 JDK 1.2 之后,Java 将引用关系分为以下四类: + ++ **强引用 (Strongly Reference)** :最传统的引用,如 `Object obj = new Object()` 。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。 ++ **软引用 (Soft Reference)** :用于描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常之前,会被列入回收范围内进行第二次回收,如果这次回收后还没有足够的内存,才会抛出内存溢出异常。 ++ **弱引用 (Weak Reference)** :用于描述那些非必须的对象,强度比软引用弱。被弱引用关联对象只能生存到下一次垃圾收集发生时,无论当前内存是否足够,弱引用对象都会被回收。 ++ **虚引用 (Phantom Reference)** :最弱的引用关系。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被回收时收到一个系统通知。 + +#### 4. 对象真正死亡 + +要真正宣告一个对象死亡,需要经过至少两次标记过程: + ++ 如果对象在进行可达性分析后发现 `GC Roots` 不可达,将会进行第一次标记; ++ 随后进行一次筛选,筛选的条件是此对象是否有必要执行 `finalized()` 方法。如果对象没有覆盖 `finalized()` 方法,或者 `finalized()` 已经被虚拟机调用过,这两种情况都会视为没有必要执行。如果判定结果是有必要执行,此时对象会被放入名为 `F-Queue` 的队列,等待 Finalizer 线程执行其 `finalized()` 方法。在这个过程中,收集器会进行第二次小规模的标记,如果对象在 `finalized()` 方法中重新将自己与引用链上的任何一个对象进行了关联,如将自己(this 关键字)赋值给某个类变量或者对象的成员变量,此时它就实现了自我拯救,则第二次标记会将其移除 “即将回收” 的集合,否则该对象就将被真正回收,走向死亡。 + +### 4.2 方法区回收 + +在 Java 堆上进行对象回收的性价比通常比较高,因为大多数对象都是朝生夕灭的。而方法区由于回收条件比较苛刻,对应的回收性价比通常比较低,主要回收两部分内容:废弃的常量和不再使用的类型。 + +### 4.3 垃圾收集算法 + +#### 1. 分代收集理论 + +当前大多数虚拟机都遵循 “分代收集” 的理论进行设计,它建立在强弱两个分代假说下: + ++ **弱分代假说 (Weak Generational Hypothesis)**:绝大多数对象都是朝生夕灭的。 ++ **强分代假说 (Strong Generational Hypothesis)**:熬过越多次垃圾收集过程的对象就越难以消亡。 ++ **跨带引用假说 (Intergenerational Reference Hypothesis)**:基于上面两条假说还可以得出的一条隐含推论:存在相互引用关系的两个对象,应该倾向于同时生存或者同时消亡。 + +强弱分代假说奠定了垃圾收集器的设计原则:收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据其年龄(年龄就是对象经历垃圾收集的次数)分配到不同的区域中进行存储。之后如果一个区域中的对象都是朝生夕灭的,那么收集器只需要关注少量对象的存活而不是去标记那些大量将要被回收的对象,此时就能以较小的代价获取较大的空间。最后再将难以消亡的对象集中到一块,根据强分代假说,它们是很难消亡的,因此虚拟机可以使用较低的频率进行回收,这就兼顾了时间和内存空间的开销。 + +#### 2. 回收类型 + +根据分代收集理论,收集范围可以分为以下几种类型: + ++ **部分收集 (Partial GC)**:具体分为: + + 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集; + + 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集; + + 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。 ++ **整堆收集 (Full GC)**:收集整个 Java 堆和方法区。 + +#### 3. 标记-清除算法 + +它是最基础的垃圾收集算法,收集过程分为两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象;也可以反过来,标记存活对象,统一回收所有未被标记的对象。 + +
    + + +它主要有以下两个缺点: + ++ 执行效率不稳定:如果 Java 堆上包含大量需要回收的对象,则需要进行大量标记和清除动作; ++ 内存空间碎片化:标记清除后会产生大量不连续的空间,从而可能导致无法为大对象分配足够的连续内存。 + +#### 4. 标记-复制算法 + +标记-复制算法基于 ”半区复制“ 算法:它将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块的内存使用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的那块内存空间一次性清理掉。其优点在于避免了内存空间碎片化的问题,其缺点如下: + ++ 如果内存中多数对象都是存活的,这种算法将产生大量的复制开销; ++ 浪费内存空间,内存空间变为了原有的一半。 + +
    + + +基于新生代 “朝生夕灭” 的特点,大多数虚拟机都不会按照 1:1 的比例来进行内存划分,例如 HotSpot 虚拟机会将内存空间划分为一块较大的 `Eden` 和 两块较小的 `Survivor` 空间,它们之间的比例是 8:1:1 。 每次分配时只会使用 `Eden` 和其中的一块 `Survivor` ,发生垃圾回收时,只需要将存活的对象一次性复制到另外一块 `Survivor` 上,这样只有 10% 的内存空间会被浪费掉。当 `Survivor` 空间不足以容纳一次 `Minor GC` 时,此时由其他内存区域(通常是老年代)来进行分配担保。 + +#### 5. 标记-整理算法 + +标记-整理算法是在标记完成后,让所有存活对象都向内存的一端移动,然后直接清理掉边界以外的内存。其优点在于可以避免内存空间碎片化的问题,也可以充分利用内存空间;其缺点在于根据所使用的收集器的不同,在移动存活对象时可能要全程暂停用户程序: + +
    + + +## 五、经典垃圾收集器 + +并行与并发是并发编程中的专有名词,在谈论垃圾收集器的上下文语境中,它们的含义如下: + ++ **并行 (Parallel)**:并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,此时通常默认用户线程是处于等待状态。 + ++ **并发 (Concurrent)**:并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。但由于垃圾收集器线程会占用一部分系统资源,所以程序的吞吐量依然会受到一定影响。 + +HotSpot 虚拟机中一共存在七款经典的垃圾收集器: + +
    + +> 注:收集器之间存在连线,则代表它们可以搭配使用。 + +### 5.1 Serial 收集器 + +Serial 收集器是最基础、历史最悠久的收集器,它是一个单线程收集器,在进行垃圾回收时,必须暂停其他所有的工作线程,直到收集结束,这是其主要缺点。它的优点在于单线程避免了多线程复杂的上下文切换,因此在单线程环境下收集效率非常高,由于这个优点,迄今为止,其仍然是 HotSpot 虚拟机在客户端模式下默认的新生代收集器: + +
    + + +### 5.2 ParNew 收集器 + +他是 Serial 收集器的多线程版本,可以使用多条线程进行垃圾回收: + +
    + + +### 5.3 Parallel Scavenge 收集器 + +Parallel Scavenge 也是新生代收集器,基于 标记-复制 算法进行实现,它的目标是达到一个可控的吞吐量。这里的吞吐量指的是处理器运行用户代码的时间与处理器总消耗时间的比值: +$$ +吞吐量 = \frac{运行用户代码时间}{运行用户代码时间+运行垃圾收集时间} +$$ +Parallel Scavenge 收集器提供两个参数用于精确控制吞吐量: + ++ **-XX:MaxGCPauseMillis**:控制最大垃圾收集时间,假设需要回收的垃圾总量不变,那么降低垃圾收集的时间就会导致收集频率变高,所以需要将其设置为合适的值,不能一味减小。 ++ **-XX:MaxGCTimeRatio**:直接用于设置吞吐量大小,它是一个大于 0 小于 100 的整数。假设把它设置为 19,表示此时允许的最大垃圾收集时间占总时间的 5%(即 1/(1+19) );默认值为 99 ,即允许最大 1%( 1/(1+99) )的垃圾收集时间。 + +### 5.4 Serial Old 收集器 + +从名字也可以看出来,它是 Serial 收集器的老年代版本,同样是一个单线程收集器,采用 标记-整理 算法,主要用于给客户端模式下的 HotSpot 虚拟机使用: + +
    + + +### 5.5 Paralled Old 收集器 + +Paralled Old 是 Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,采用 标记-整理 算法实现: + +
    + + +### 5.6 CMS 收集器 + +CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于 标记-清除 算法实现,整个收集过程分为以下四个阶段: + +1. **初始标记 (inital mark)**:标记 `GC Roots` 能直接关联到的对象,耗时短但需要暂停用户线程; +2. **并发标记 (concurrent mark)**:从 `GC Roots` 能直接关联到的对象开始遍历整个对象图,耗时长但不需要暂停用户线程; +3. **重新标记 (remark)**:采用增量更新算法,对并发标记阶段因为用户线程运行而产生变动的那部分对象进行重新标记,耗时比初始标记稍长且需要暂停用户线程; +4. **并发清除 (inital sweep)**:并发清除掉已经死亡的对象,耗时长但不需要暂停用户线程。 + +
    + + +其优点在于耗时长的 并发标记 和 并发清除 阶段都不需要暂停用户线程,因此其停顿时间较短,其主要缺点如下: + ++ 由于涉及并发操作,因此对处理器资源比较敏感。 ++ 由于是基于 标记-清除 算法实现的,因此会产生大量空间碎片。 ++ 无法处理浮动垃圾(Floating Garbage):由于并发清除时用户线程还是在继续,所以此时仍然会产生垃圾,这些垃圾就被称为浮动垃圾,只能等到下一次垃圾收集时再进行清理。 + +### 5.7 Garbage First 收集器 + +Garbage First(简称 G1)是一款面向服务端的垃圾收集器,也是 JDK 9 服务端模式下默认的垃圾收集器,它的诞生具有里程碑式的意义。G1 虽然也遵循分代收集理论,但不再以固定大小和固定数量来划分分代区域,而是把连续的 Java 堆划分为多个大小相等的独立区域(Region)。每一个 Region 都可以根据不同的需求来扮演新生代的 `Eden` 空间、`Survivor` 空间或者老年代空间,收集器会根据其扮演角色的不同而采用不同的收集策略。 + +
    + + +上面还有一些 Region 使用 H 进行标注,它代表 Humongous,表示这些 Region 用于存储大对象(humongous object,H-obj),即大小大于等于 region 一半的对象。G1 收集器的运行大致可以分为以下四个步骤: + +1. **初始标记 (Inital Marking)**:标记 `GC Roots` 能直接关联到的对象,并且修改 TAMS(Top at Mark Start)指针的值,让下一阶段用户线程并发运行时,能够正确的在 Reigin 中分配新对象。G1 为每一个 Reigin 都设计了两个名为 TAMS 的指针,新分配的对象必须位于这两个指针位置以上,位于这两个指针位置以上的对象默认被隐式标记为存活的,不会纳入回收范围; +2. **并发标记 (Concurrent Marking)**:从 `GC Roots` 能直接关联到的对象开始遍历整个对象图。遍历完成后,还需要处理 SATB 记录中变动的对象。SATB(snapshot-at-the-beginning,开始阶段快照)能够有效的解决并发标记阶段因为用户线程运行而导致的对象变动,其效率比 CMS 重新标记阶段所使用的增量更新算法效率更高; +3. **最终标记 (Final Marking)**:对用户线程做一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的少量的 STAB 记录。虽然并发标记阶段会处理 SATB 记录,但由于处理时用户线程依然是运行中的,因此依然会有少量的变动,所以需要最终标记来处理; +4. **筛选回收 (Live Data Counting and Evacuation)**:负责更新 Regin 统计数据,按照各个 Regin 的回收价值和成本进行排序,在根据用户期望的停顿时间进行来指定回收计划,可以选择任意多个 Regin 构成回收集。然后将回收集中 Regin 的存活对象复制到空的 Regin 中,再清理掉整个旧的 Regin 。此时因为涉及到存活对象的移动,所以需要暂停用户线程,并由多个收集线程并行执行。 + +
    + + +### 5.8 内存分配原则 + +#### 1. 对象优先在 Eden 分配 + +大多数情况下,对象在新生代的 `Eden` 区中进行分配,当 `Eden` 区没有足够空间时,虚拟机将进行一次 Minor GC。 + +#### 2. 大对象直接进入老年代 + +大对象就是指需要大量连续内存空间的 Java 对象,最典型的就是超长的字符串或者元素数量很多的数组,它们将直接进入老年代。主要是因为如果在新生代分配,因为其需要大量连续的内存空间,可能会导致提前触发垃圾回收;并且由于新生代的垃圾回收本身就很频繁,此时复制大对象也需要额外的性能开销。 + +#### 3. 长期存活的对象将进入老年代 + +虚拟机会给每个对象在其对象头中定义一个年龄计数器。对象通常在 `Eden` 区中诞生,如果经历第一次 Minor GC 后仍然存活,并且能够被 Survivor 容纳的话,该对象就会被移动到 Survivor 中,并将其年龄加 1。对象在 Survivor 中每经过一次 Minor GC,年龄就加 1,当年龄达到一定程度后(由 `-XX:MaxTenuringThreshold` 设置,默认值为 15)就会进入老年代中。 + +#### 4. 动态年龄判断 + +如果在 Survivor 空间中相同年龄的所有对象大小的总和大于 Survivor 空间的一半,那么年龄大于或等于该年龄的对象就可以直接进入老年代,而无需等待年龄到达 `-XX:MaxTenuringThreshold` 设置的值。 + +#### 5. 空间担保分配 + +在发生 Minor GC 之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果条件成立,那么这一次的 Minor GC 可以确认是安全的。如果不成立,虚拟机会查看 `-XX:HandlePromotionFailure` 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于或者 `-XX:HandlePromotionFailure` 的值设置不允许冒险,那么就要改为进行一次 Full GC 。 + +## 六、虚拟机类加载机制 + +Java 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程被称为虚拟机的类加载机制。 + +### 6.1 类加载时机 + +一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载、验证、准备、卸载、解析、初始化、使用、卸载七个阶段,其中验证、准备、解析三个部分统称为连接: + +
    + + +《Java 虚拟机规范》严格规定了有且只有六种情况必须立即对类进行初始化: + +1. 遇到 `new`、 `getstatic`、 `putstatic`、 `invokestatic` 这四条字节码指令,如果类型进行过初始化,则需要先触发其进行初始化,能够生成这四条指令码的典型 Java 代码场景有: + + 使用 `new` 关键字实例化对象时; + + 读取或设置一个类型的静态字段时(被 final 修饰,已在编译期把结果放入常量池的静态字段除外); + + 调用一个类型的静态方法时。 +2. 使用 `java.lang.reflect` 包的方法对类型进行反射调用时,如果类型没有进行过初始化、则需要触发其初始化; +3. 当初始化类时,如发现其父类还没有进行过初始化、则需要触发其父类进行初始化; +4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类; +5. 当使用 JDK 7 新加入的动态语言支持时,如果一个 `java.lang.invoke.MethodHandle` 实例最后解析的结果为 `REF_getStatic` , `REF_putStatic` , `REF_invokeStatic` , `REF_newInvokeSpecial` 四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化; +6. 当一个接口中定义了 JDK 8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那么该接口要在其之前被初始化。 + +### 6.2 类加载过程 + +#### 1. 加载 + +在加载阶段,虚拟机需要完成以下三件事: + ++ 通过一个类的全限定名来获取定义此类的二进制字节流 ; ++ 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构; ++ 在内存中生成一个代表这个类的 `java.lang.Class` 对象,作为方法区这个类的各种数据的访问入口。 + +《Java 虚拟机规范》并没有限制从何处获取二进制流,因此可以从 JAR 包、WAR 包获取,也可以从 JSP 生成的 Class 文件等处获取。 + +#### 2. 验证 + +这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,从而保证这些信息被当做代码运行后不会危害虚拟机自身的安全。验证阶段大致会完成下面四项验证: + ++ **文件格式验证**:验证字节流是否符合 Class 文件格式的规范; ++ **元数据验证**:对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java 语言规范》的要求(如除了 `java.lang.Object` 外,所有的类都应该有父类); ++ **字节码验证**:通过数据流分析和控制流分析,确定程序语义是合法的,符合逻辑的(如允许把子类对象赋值给父类数据类型,但不能把父类对象赋值给子类数据类型); ++ **符号引用验证**:验证类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。如果无法验证通过,则会抛出一个`java.lang.IncompatibleClassChangeError` 的子类异常,如 `java.lang.NoSuchFieldError` 、 `java.lang.NoSuchMethodError` 等。 + +#### 3. 准备 + +准备阶段是正式为类中定义的变量(即静态变量,被 static 修饰的变量)分配内存并设置类变量初始值的阶段。 + +#### 4. 解析 + +解析是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程: + ++ **符号引用**:符号引用用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。 ++ **直接引用**:直接引用是指可以直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。 + +整个解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这 7 类符号引用进行解析。 + +#### 5. 初始化 + +初始化阶段就是执行类构造器的 `()` 方法的过程,该方法具有以下特点: + ++ `()` 方法由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生,编译器收集顺序由语句在源文件中出现的顺序决定。 ++ `()` 方法与类的构造器函数(即在虚拟机视角中的实例构造器 `()`方法)不同,它不需要显示的调用父类的构造器,Java 虚拟机会保证在子类的 `()` 方法执行前,父类的 `()` 方法已经执行完毕。 ++ 由于父类的 `()` 方法先执行,也就意味着父类中定义的静态语句块要优先于子类变量的赋值操作。 ++ `()` 方法对于类或者接口不是必须的,如果一个类中没有静态语句块,也没有对变量进行赋值操作,那么编译器可以不为这个类生成 `()` 方法。 ++ 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成 `()` 方法。 ++ Java 虚拟机必须保证一个类的 `()` 方法在多线程环境中被正确的加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的 `()` 方法,其他线程都需要阻塞等待。 + +### 6.3 类加载器 + +能够通过一个类的全限定名来获取描述该类的二进制字节流的工具称为类加载器。每一个类加载器都拥有一个独立的类名空间,因此对于任意一个类,都必须由加载它的类加载器和这个类本身来共同确立其在 Java 虚拟机中的唯一性。这意味着要想比较两个类是否相等,必须在同一类加载器加载的前提下;如果两个类的类加载器不同,则它们一定不相等。 + +### 6.4 双亲委派模型 + +从 Java 虚拟机角度而言,类加载器可以分为以下两类: + ++ **启动类加载器**:启动类加载器(Bootstrap ClassLoader)由 C++ 语言实现(以 HotSpot 为例),它是虚拟机自身的一部分; ++ **其他所有类的类加载器**:由 Java 语言实现,独立存在于虚拟机外部,并且全部继承自 `java.lang.ClassLoader` 。 + +从开发人员角度而言,类加载器可以分为以下三类: + ++ **启动类加载器 (Boostrap Class Loader)**:负责把存放在 `\lib` 目录中,或被 `-Xbootclasspath` 参数所指定的路径中存放的能被 Java 虚拟机识别的类库加载到虚拟机的内存中; ++ **扩展类加载器 (Extension Class Loader)**:负责加载 `\lib\ext` 目录中,或被 `java.ext.dirs` 系统变量所指定的路径中的所有类库。 ++ **应用程序类加载器 (Application Class Loader)**:负责加载用户类路径(ClassPath)上的所有的类库。 + +JDK 9 之前的 Java 应用都是由这三种类加载器相互配合来完成加载: + +
    + + +上图所示的各种类加载器之间的层次关系被称为类加载器的 “双亲委派模型”,“双亲委派模型” 要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,需要注意的是这里的加载器之间的父子关系一般不是以继承关系来实现的,而是使用组合关系来复用父类加载器的代码。 + +双亲委派模型的工作过程如下:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。基于双亲委派模型可以保证程序中的类在各种类加载器环境中都是同一个类,否则就有可能出现一个程序中存在两个不同的 `java.lang.Object` 的情况。 + +### 6.5 模块化下的类加载器 + +JDK 9 之后为了适应模块化的发展,类加载器做了如下变化: + ++ 仍维持三层类加载器和双亲委派的架构,但扩展类加载器被平台类加载器所取代; ++ 当平台及应用程序类加载器收到类加载请求时,要首先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载; ++ 启动类加载器、平台类加载器、应用程序类加载器全部继承自 `java.internal.loader.BuiltinClassLoader` ,BuiltinClassLoader 中实现了新的模块化架构下类如何从模块中加载的逻辑,以及模块中资源可访问性的处理。 + +
    + + +## 七、程序编译 + +### 7.1 编译器分类 + ++ **前端编译器**:把 `*.java` 文件转变成 `.class` 文件的过程;如 JDK 的 Javac,Eclipse JDT 中的增量式编译器。 ++ **即使编译器**:常称为 JIT 编译器(Just In Time Complier),在运行期把字节码转变成本地机器码的过程;如 HotSpot 虚拟机中的 C1、C2 编译器,Graal 编译器。 ++ **提前编译器**:直接把程序编译成目标机器指令集相关的二进制代码的过程。如 JDK 的 jaotc,GUN Compiler for the Java(GCJ),Excelsior JET 。 + +### 7.2 解释器与编译器 + +在 HotSpot 虚拟机中,Java 程序最初都是通过解释器(Interpreter)进行解释执行的,其优点在于可以省去编译时间,让程序快速启动。当程序启动后,如果虚拟机发现某个方法或代码块的运行特别频繁,就会使用编译器将其编译为本地机器码,并使用各种手段进行优化,从而提高执行效率,这就是即时编译器。HotSpot 内置了两个(或三个)即时编译器: + ++ **客户端编译器 (Client Complier)**:简称 C1; ++ **服务端编译器 (Servier Complier)**:简称 C2,在有的资料和 JDK 源码中也称为 Opto 编译器; ++ **Graal 编译器**:在 JDK 10 时才出现,长期目标是替代 C2。 + +在分层编译的工作模式出现前,采用客户端编译器还是服务端编译器完全取决于虚拟机是运行在客户端模式还是服务端模式下,可以在启动时通过 `-client` 或 `-server` 参数进行指定,也可以让虚拟机根据自身版本和宿主机性能来自主选择。 + +### 7.3 分层编译 + +要编译出优化程度越高的代码通常都需要越长的编译时间,为了在程序启动速度与运行效率之间达到最佳平衡,HotSpot 虚拟机在编译子系统中加入了分层编译(Tiered Compilation): + ++ **第 0 层**:程序纯解释执行,并且解释器不开启性能监控功能; ++ **第 1 层**:使用客户端编译器将字节码编译为本地代码来运行,进行简单可靠的稳定优化,不开启性能监控功能; ++ **第 2 层**:仍然使用客户端编译执行,仅开启方法及回边次数统计等有限的性能监控; ++ **第 3 层**:仍然使用客户端编译执行,开启全部性能监控; ++ **第 4 层**:使用服务端编译器将字节码编译为本地代码,其耗时更长,并且会根据性能监控信息进行一些不可靠的激进优化。 + +以上层次并不是固定不变的,根据不同的运行参数和版本,虚拟机可以调整分层的数量。各层次编译之间的交互转换关系如下图所示: + +
    + + +实施分层编译后,解释器、客户端编译器和服务端编译器就会同时工作,可以用客户端编译器获取更高的编译速度、用服务端编译器来获取更好的编译质量。 + +### 7.4 热点探测 + +即时编译器编译的目标是 “热点代码”,它主要分为以下两类: + ++ 被多次调用的方法。 ++ 被多次执行循环体。这里指的是一个方法只被少量调用过,但方法体内部存在循环次数较多的循环体,此时也认为是热点代码。但编译器编译的仍然是循环体所在的方法,而不会单独编译循环体。 + +判断某段代码是否是热点代码的行为称为 “热点探测” (Hot Spot Code Detection),主流的热点探测方法有以下两种: + ++ **基于采样的热点探测 (Sample Based Hot Spot Code Detection)**:采用这种方法的虚拟机会周期性地检查各个线程的调用栈顶,如果发现某个(或某些)方法经常出现在栈顶,那么就认为它是 “热点方法”。 ++ **基于计数的热点探测 (Counter Based Hot Spot Code Detection)**:采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是 “热点方法”。 + +## 八、代码优化 + +即时编译器除了将字节码编译为本地机器码外,还会对代码进行一定程度的优化,它包含多达几十种优化技术,这里选取其中代表性的四种进行介绍: + +### 8.1 方法内联 + +最重要的优化手段,它会将目标方法中的代码原封不动地 “复制” 到发起调用的方法之中,避免发生真实的方法调用,并采用名为类型继承关系分析(Class Hierarchy Analysis,CHA)的技术来解决虚方法(Java 语言中默认的实例方法都是虚方法)的内联问题。 + +### 8.2 逃逸分析 + +逃逸行为主要分为以下两类: + ++ **方法逃逸**:当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,此时称为方法逃逸; ++ **线程逃逸**:当一个对象在方法里面被定义后,它可能被外部线程所访问,例如赋值给可以在其他线程中访问的实例变量,此时称为线程,其逃逸程度高于方法逃逸。 + +```java +public static StringBuilder concat(String... strings) { + StringBuilder sb = new StringBuilder(); + for (String string : strings) { + sb.append(string); + } + return sb; // 发生了方法逃逸 +} + +public static String concat(String... strings) { + StringBuilder sb = new StringBuilder(); + for (String string : strings) { + sb.append(string); + } + return sb.toString(); // 没有发生方法逃逸 +} +``` + +如果能证明一个对象不会逃逸到方法或线程之外,或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则可以为这个对象实例采取不同程序的优化: + ++ **栈上分配 (Stack Allocations)**:如果一个对象不会逃逸到线程外,那么将会在栈上分配内存来创建这个对象,而不是 Java 堆上,此时对象所占用的内存空间就会随着栈帧的出栈而销毁,从而可以减轻垃圾回收的压力。 ++ **标量替换 (Scalar Replacement)**:如果一个数据已经无法再分解成为更小的数据类型,那么这些数据就称为标量(如 int、long 等数值类型及 reference 类型等);反之,如果一个数据可以继续分解,那它就被称为聚合量(如对象)。如果一个对象不会逃逸外方法外,那么就可以将其改为直接创建若干个被这个方法使用的成员变量来替代,从而减少内存占用。 ++ **同步消除 (Synchronization Elimination)**:如果一个变量不会逃逸出线程,那么对这个变量实施的同步措施就可以消除掉。 + +### 8.3 公共子表达式消除 + +如果一个表达式 E 之前已经被计算过了,并且从先前的计算到现在 E 中所有变量的值都没有发生过变化,那么 E 这次的出现就称为公共子表达式。对于这种表达式,无需再重新进行计算,只需要直接使用前面的计算结果即可。 + +### 8.4 数组边界检查消除 + +对于虚拟机执行子系统来说,每次数组元素的读写都带有一次隐含的上下文检查以避免访问越界。如果数组的访问发生在循环之中,并且使用循环变量来访问数据,即循环变量的取值永远在 [0,list.length) 之间,那么此时就可以消除整个循环的数据边界检查,从而避免多次无用的判断。 + + + +## 参考资料 + ++ 主要参考自:周志明 . 深入理解Java虚拟机(第3版). 机械工业出版社 , 2019-12 ,想要深入了解虚拟机的话,推荐阅读原书。 ++ [美团技术团队 —— Java Hotspot G1 GC的一些关键技术](https://tech.meituan.com/2016/09/23/g1.html) + + + + + diff --git a/blog/zh-cn/java/springAnnotation.md b/blog/zh-cn/java/springAnnotation.md new file mode 100644 index 0000000..7a10098 --- /dev/null +++ b/blog/zh-cn/java/springAnnotation.md @@ -0,0 +1,232 @@ +# Spring 中的 18 个注解 + +## 1 @Controller + +标识一个该类是Spring MVC controller处理器,用来创建处理http请求的对象. +```java +@Controller +public class TestController{ + + public String test(Map map){ + return "hello"; + } +} +``` + +## 2 @RestController + +Spring4之后加入的注解,原来在@Controller中返回json需要@ResponseBody来配合,如果直接用@RestController替代@Controller就不需要再配置@ResponseBody,默认返回json格式。 + +## 3 @Service + +用于标注业务层组件,说白了就是加入你有一个用注解的方式把这个类注入到spring配置中 + +## 4 @Autowired + +用来装配bean,都可以写在字段上,或者方法上。 + +默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false,例如: + +```java +@Autowired(required=false) +``` +## 5 @RequestMapping + +类定义处: 提供初步的请求映射信息,相对于 WEB 应用的根目录。 + +方法处: 提供进一步的细分映射信息,相对于类定义处的 URL。 + +## 6 @RequestParam + +用于将请求参数区数据映射到功能处理方法的参数上 + +例如 + +``` java +public Resp test(@RequestParam Integer id){ + return Resp.success(customerInfoService.fetch(id)); +} +``` +这个id就是要接收从接口传递过来的参数id的值的,如果接口传递过来的参数名和你接收的不一致,也可以如下 + +``` java +public Resp test (@RequestParam(value="course_id") Integer id){ + return Resp.success(customerInfoService.fetch(id)); +} +``` + +其中course_id就是接口传递的参数,id就是映射course_id的参数名 + +## 7 @ModelAttribute + +使用地方如下: + +1. 标记在方法上 + +标记在方法上,会在每一个@RequestMapping标注的方法前执行,如果有返回值,则自动将该返回值加入到ModelMap中。 + +(1). 在有返回的方法上:当ModelAttribute设置了value,方法返回的值会以这个value为key,以参数接受到的值作为value,存入到Model中,如下面的方法执行之后,最终相当于 + +model.addAttribute("user_name", name);假如 @ModelAttribute没有自定义value,则相当于 + +model.addAttribute("name", name); +``` java +@ModelAttribute(value="user_name") +public String before(@RequestParam(required = false) String Name,Model model){ + System.out.println("name is "+name); +} +``` +(2) 在没返回的方法上: + +需要手动model.add方法 + +``` java +@ModelAttribute +public void before(@RequestParam(required = false) Integer age,Model model) { + model.addAttribute("age",age); + System.out.println("age is "+age); +} +``` +2. 标记在方法的参数上 + +标记在方法的参数上,会将客户端传递过来的参数按名称注入到指定对象中,并且会将这个对象自动加入ModelMap中,便于View层使用.我们在上面的类中加入一个方法如下 + +``` java + +public Resp model(@ModelAttribute("user_name") String user_name, + @ModelAttribute("name") String name, + @ModelAttribute("age") Integer age,Model model){ + System.out.println("user_name="+user_name+" name="+name+" age="+age); + System.out.println("model="+model); + } +``` +用在方法参数中的@ModelAttribute注解,实际上是一种接受参数并且自动放入Model对象中,便于使用。 + +## 8 @Cacheable + +用来标记缓存查询。可用用于方法或者类中,当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。 + +参数列表 + +参数|解释|例子 +-|-|-|- +value|名称|@Cacheable(value={"c1","c2"}) +key|key|@Cacheable(value="c1",key="#id") +condition|条件|@Cacheable(value="c1",condition="#id=1") + +比如@Cacheable(value="UserCache") 标识的是当调用了标记了这个注解的方法时,逻辑默认加上从缓存中获取结果的逻辑,如果缓存中没有数据,则执行用户编写查询逻辑,查询成功之后,同时将结果放入缓存中。 + +但凡说到缓存,都是key-value的形式的,因此key就是方法中的参数(id),value就是查询的结果,而命名空间UserCache是在spring*.xml中定义。 + +```java +@Cacheable(value="UserCache") +public int getUserAge(int id){ + int age=getAgeById(id); + return age; +} +``` + +## 9 @CacheEvict + +@CacheEvict用来标记要清空缓存的方法,当这个方法被调用后,即会清空缓存。 + +@CacheEvict(value=”UserCache”) + +参数列表 + +参数|解释|例子 +-|-|-|- +value|名称|@CacheEvict(value={"c1","c2"}) +key|key|@CacheEvict(value="c1",key="#id") +condition|缓存得条件可为空| +allEntries|是否清空所有内容|@CacheEvict(value="c1",allEntries=true) +beforeInvocation|是否在方法执行前清空|@CacheEvict(value="c1",beforeInvocation=true) + +## 10 @Resource + +@Resource的作用相当于@Autowired + +只不过@Autowired按byType自动注入, + +而@Resource默认按 byName自动注入罢了。 + +@Resource有两个属性是比较重要的,分是name和type,Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不指定name也不指定type属性,这时将通过反射机制使用byName自动注入策略。 + +@Resource装配顺序: + +1、如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常 + +2、如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常 + +3、如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常 + +4、如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配; + +## 11 @PostConstruct + +@PostConstruct用来标记是在项目启动的时候执行这个方法。用来修饰一个非静态的void()方法 + +也就是spring容器启动时就执行,多用于一些全局配置、数据字典之类的加载 + +被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。PostConstruct在构造函数之后执行,init()方法之前执行。PreDestroy()方法在destroy()方法执行之后执行 + +## 12 @PreDestory + +被@PreDestroy修饰的方法会在服务器卸载Servlet的时候运行,并且只会被服务器调用一次,类似于Servlet的destroy()方法。被@PreDestroy修饰的方法会在destroy()方法之后运行,在Servlet被彻底卸载之前 + +## 13 @Repository + +@Repository用于标注数据访问组件,即DAO组件 + +## 14 @Component + +@Component泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注 + +## 15 @Scope + +@Scope用来配置 spring bean 的作用域,它标识 bean 的作用域。 + +默认值是单例 + +1、singleton:单例模式,全局有且仅有一个实例 + +2、prototype:原型模式,每次获取Bean的时候会有一个新的实例 + +3、request:request表示该针对每一次HTTP请求都会产生一个新的bean,同时该bean仅在当前HTTP request内有效 + +4、session:session作用域表示该针对每一次HTTP请求都会产生一个新的bean,同时该bean仅在当前HTTP session内有效 + +5、global session:只在portal应用中有用,给每一个 global http session 新建一个Bean实例。 + +## 16 @SessionAttributes + +默认情况下Spring MVC将模型中的数据存储到request域中。当一个请求结束后,数据就失效了。如果要跨页面使用。那么需要使用到session。而@SessionAttributes注解就可以使得模型中的数据存储一份到session域中 + +参数: + +1、names:这是一个字符串数组。里面应写需要存储到session中数据的名称。 + +2、types:根据指定参数的类型,将模型中对应类型的参数存储到session中 + +3、value:和names是一样的。 + +```java +@Controller +@SessionAttributes(value={"names"},types={Integer.class}) +public class ScopeService{ + @RequestMapping("/testSession") + public String test(Map map){ + map.put("names",Arrays.asList("a","b","c")); + map.put("age",12); + return "hello"; + } +} +``` + +## 17 @Required + +适用于bean属性setter方法,并表示受影响的bean属性必须在XML配置文件在配置时进行填充。否则,容器会抛出一个BeanInitializationException异常。 + +## 18 @Qualifier + +@Qualifier当你创建多个具有相同类型的 bean 时,并且想要用一个属性只为它们其中的一个进行装配,在这种情况下,你可以使用 @Qualifier 注释和 @Autowired 注释通过指定哪一个真正的 bean 将会被装配来消除混乱。 diff --git a/blog/zh-cn/js/baidu-tongji.js b/blog/zh-cn/js/baidu-tongji.js new file mode 100644 index 0000000..1a35847 --- /dev/null +++ b/blog/zh-cn/js/baidu-tongji.js @@ -0,0 +1,35 @@ + +var _hmt = _hmt || []; +(function() { + let hm = document.createElement("script"); + // rosemarys.gitee.io + hm.src = "https://hm.baidu.com/hm.js?08563f1498cfbdc12f55e3fc8b133b4b"; + let s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + // burningmyself.cn + hm = document.createElement("script"); + hm.src = "https://hm.baidu.com/hm.js?ea33cbaa80e12b2127ea3d44494c0870"; + s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + // burningmyself.gitee.io + hm = document.createElement("script"); + hm.src = "https://hm.baidu.com/hm.js?10b96d1600d054d9d9519e5512e0af19"; + s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + // burningmyself.github.io + hm = document.createElement("script"); + hm.src = "https://hm.baidu.com/hm.js?14446117c56cf1fdab44c58081833af8"; + s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + // yangfubing.gitee.io + hm = document.createElement("script"); + hm.src = "https://hm.baidu.com/hm.js?66401a22d7cba55cbbdd060204e0f29d"; + s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + // yangfubing.github.io + hm = document.createElement("script"); + hm.src = "https://hm.baidu.com/hm.js?dd0db5e74ea516ec6bee7d759094aaaa"; + s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); +})(); + diff --git a/blog/zh-cn/js/extra.js b/blog/zh-cn/js/extra.js new file mode 100644 index 0000000..7b3c571 --- /dev/null +++ b/blog/zh-cn/js/extra.js @@ -0,0 +1,13 @@ + +(function () { + let generator = document.getElementsByTagName("meta").generator; + generator.name="keywords"; + generator.content="net,java,php,python,docker,web"; + let meta = document.createElement("meta"); + let s = document.getElementsByTagName("meta")[0]; + meta.name="google-site-verification"; + meta.content="le9TAKnSKhLDEEGnDu2ofXi3taLVIxmKNT0bEIsetNE"; + s.parentNode.insertBefore(meta, s); + let copyright = document.getElementsByClassName("md-footer-copyright") + copyright[0].outerHTML=document.getElementsByClassName("md-footer-copyright__highlight")[0].outerHTML +})(); diff --git a/blog/zh-cn/js/google.js b/blog/zh-cn/js/google.js new file mode 100644 index 0000000..7fe8064 --- /dev/null +++ b/blog/zh-cn/js/google.js @@ -0,0 +1,14 @@ +// burningmyself.gitee.io +// Global site tag (gtag.js) - Google Analytics +/* */ + window.dataLayer = window.dataLayer || []; + function gtag(){dataLayer.push(arguments);} + gtag('js', new Date()); + gtag('config', 'UA-155084439-1'); + +//yangfubing.gitee.io +/* */ + window.dataLayer = window.dataLayer || []; + function gtag(){dataLayer.push(arguments);} + gtag('js', new Date()); + gtag('config', 'UA-155132293-1'); diff --git a/blog/zh-cn/net/c_sqlserver_nginx.md b/blog/zh-cn/net/c_sqlserver_nginx.md new file mode 100644 index 0000000..ef5ef2e --- /dev/null +++ b/blog/zh-cn/net/c_sqlserver_nginx.md @@ -0,0 +1,392 @@ +# ASP.NET Core 实战:使用 Docker 容器化部署 ASP.NET Core + sqlserver + Nginx + +### 一、前言 + +  在之前的文章(ASP.NET Core 实战:Linux 小白的 .NET Core 部署之路)中,我介绍了如何在 Linux 环境中安装 .NET Core SDK / .NET Core Runtime、Nginx、sqlserver,以及如何将我们的 ASP.NET Core MVC 程序部署到 Linux 上,同时,使用 supervisor 守护程序守护我们的 .NET Core 程序。如果,你有看过那篇文章,并且和我一样是个 Linux 小白用户的话,可能第一感觉就是,把 .NET Core 项目部署在 IIS 上也挺好。 + +  将 .NET Core 项目部署到 Linux 上如此复杂,就没有简单的部署方式吗? + +  你好,有的,Docker 了解一下~~~ + +  PS:这里的示例代码还是采用之前的毕业设计项目,在这篇文章发布的时候,我已经在程序的仓库中添加了对于 Docker 的支持,你可以下载下来,自己尝试一下,毕竟,实践出真知。 + +   代码仓储:https://github.com/burningmyself/micro + + ### 二、Step by Step + +#### 1、安装 Docker & Docker Compose + +  在代码交付的过程中,偶尔会遇到这样的问题,在本地测试是好的,但是部署到测试环境、生产环境时就出这样那样的问题,同时,因为本地与测试环境、生产环境之间存在差异,我们可能无法在本地复现这些问题,那么,有没有一种工具可以很好的解决这一问题呢?随着历史的车轮不断前行,容器技术诞生了。 + +  Docker,作为最近几年兴起的一种虚拟化容器技术,他可以将我们的运行程序与操作系统做一个隔离,例如这里我们需要运行 .NET Core 程序,我们不再需要关心底层的操作系统是什么,不需要在每台需要需要运行程序的机器上安装程序运行的各种依赖,我们可以通过程序打包成镜像的方式,将应用程序和该程序的依赖全部置于一个镜像文件中,这时,只要别的机器上有安装 Docker,就可以通过我们打包的这个镜像来运行这个程序。 + +#### 1.1、卸载 Docker + +  在安装 Docker 之前,我们应该确定当前的机器上是否已经安装好了 Docker,为了防止与现在安装的 Docker CE 发生冲突,这里我们先卸载掉以前版本的 Docker,如果你确定你的机器上并没有安装 Docker 的话此步可以跳过。 + +  在 Linux 中可以使用 \ 加 Enter 在输入很长很长的语句时进行换行,这里和后面的命令都是采用这样的方式。 + +sudo yum remove docker \ + docker-client \ + docker-client-latest \ + docker-common \ + docker-latest \ + docker-latest-logrotate \ + docker-logrotate \ + docker-engine +#### 1.2、添加 yum 源 + +  在安装 Docker CE 的方式上,我是采用将 Docker CE 的源添加到 yum 源中,之后我们就可以直接使用 yum install 安装 Docker CE,整个的安装过程如下。 + +安装工具包从而可以让我们在 yum 中添加别的仓储源 +``` +sudo yum install -y yum-utils \ + device-mapper-persistent-data \ + lvm2 +``` +设置 docker ce 的稳定库地址 +``` +sudo yum-config-manager \ + --add-repo \ + https://download.docker.com/linux/centos/docker-ce.repo +``` +安装 docker ce +``` +sudo yum install docker-ce docker-ce-cli containerd.io +``` +当我们安装好 Docker 之后,我们就可以使用 docker 命令验证我们是否在机器上成功安装了 Docker,同时,也可以使用 docker --version 命令查看我们安装的 Docker CE 版本。 + +![cmn](./../img/c_sqlserver_nginx1.png) + +#### 1.3、设置开机自启 + +  当 Docker 已经在我们的机器上安装完成后,我们就可以将 Docker 设置成机器的自启服务,这样,如果出现服务器重启的情况下,我们的 Docker 也可以随服务器的重启自动启动 Docker 服务。 + +启动 Docker 服务并允许开机自启 +``` +sudo systemctl start docker +``` +查看当前 dokcer 的运行情况 +``` +sudo systemctl status docker +``` +#### 1.4、Hello World + +  就像我们在学习一门新的语言时,运行的第一句代码,几乎都是打印出 Hello World,而在 Docker Hub 中,也有这么一个镜像,在无数的 Docker 教程中,安装完 Docker 后,第一件事就是拉取这个镜像文件,“告诉” Docker,我来了。 + +  Docker Hub 是存放镜像的仓库,里面包含了许多的镜像文件,因为服务器在国外的原因,下载的速度可能不理想,像国内的阿里云、腾讯云也有提供对于 Docker 镜像的加速器服务,你可以按需使用,当然,你也可以创建属于你的私有镜像仓库。 + +  docker run 命令,它会在我们的本地镜像库中先寻找这个镜像,然后运行。如果在本地没有找到的话,则会自动使用 docker pull 从 Docker Hub 中寻找,能找到的话,则会自动下载到本地,然后运行,找不到的话,这条命令也就运行失败了。 + +![cmn](./../img/c_sqlserver_nginx2.png) + +#### 1.5、安装 Docker Compose + +  在实际的项目开发中,我们可能会有多个应用镜像,例如在本篇文章的示例中,为了在 Docker 中运行我们的程序,我们需要三个镜像:应用程序自身镜像、sqlserver Server 镜像、以及 Nginx 镜像,为了将我们的程序启动起来,我们需要手敲各个容器的启动参数,环境变量,容器命名,指定不同容器的链接参数等等一系列的操作,又多又烦,可能某一步操作失败后程序就无法正常运行。而当我们使用了 Docker Compose 之后,我们就可以把这些命令一次性写在 docker-compose.yml 配置文件中,以后每次启动我们的应用程序时,只需要通过 docker compose 命令就可以自动帮我们完成这些操作。 + +从 github 下载 docker compose 二进制文件 +``` +sudo curl -L "https://github.com/docker/compose/releases/download/1.23.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +``` +对下载的二进制文件应用可执行权限 +``` +sudo chmod +x /usr/local/bin/docker-compose +``` +查看 docker compose 版本 +``` +docker-compose --version +``` + +![cmn](./../img/c_sqlserver_nginx3.png) + +#### 2、构建程序镜像 + +  当我们在服务器上安装好 docker 和 docker compose 之后,就可以开始构建我们的程序镜像了。首先我们需要对我们的运行程序添加对于 Docker 的支持。你可以自己手动在 MVC 项目中添加 Dockerfile 文件,或是通过右键添加 Docker 支持。 + +![cmn](./../img/c_sqlserver_nginx4.png) + +Dockerfile 就像一个执行的清单,它告诉 Docker,我们这个镜像在构建和运行时需要按照什么样的命令运行。打开 VS 为我们自动创建的 Dockerfile,可以看到清晰的分成了四块的内容。 + +![cmn](./../img/c_sqlserver_nginx5.png) + +我们知道,.NET Core 程序的运行需要依赖于 .NET Core Runtime(CoreCLR),因此,为了使我们的程序可以运行起来,我们需要从 hub 中拉取 runtime ,并在 此基础上构建我们的应用镜像。同时,为了避免因为基础的环境的不同造成对程序的影响,这里的 Runtime 需要同程序开发时的 .NET Core SDK 版本保持一致,所以这里我使用的是 .NET Core 3.0 Runtime。 + +  一个镜像中包含了应用程序及其所有的依赖,与虚拟机不同的是,容器中的每个镜像最终是共享了宿主机的操作系统资源,容器作为用户空间中的独立进程运行在主机操作系统上。 +![cmn](./../img/c_sqlserver_nginx6.png) + +PS:图片版权归属于微软的技术文档,如有侵权,请联系我删除,源文件地址:什么是 Docker? + +  镜像可以看成一个个小型的“虚拟主机”,这里我们在镜像中创建了一个 /app 路径作为我们程序在镜像中的工作目录,同时,将 80,443 端口暴露给 Docker,从而可以使我们在镜像外面通过端口访问到当前镜像中的运行的程序。 +``` +FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-buster-slim AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 +``` +  因为我们的应用是一个微服架构的应用,最终的项目依赖于解决方案中的各个类库以及我们从 Nuget 中下载的各种第三方组件,在部署时,需要将这些组件打包成 dll 引用。所以,这里我们需要使用 .NET Core SDK 中包含的 .NET Core CLI 进行还原和构建。 + +  就像在下面的代码中,我们在镜像的内部创建了一个 /src 的路径,将当前解决方案下的类库都复制到这个目录下,之后通过 dotnet restore 命令还原我们的主程序所依赖的各个组件。当我们还原好依赖的组件后,就可以使用 dotnet build 命令生成 Release版本的 dll 文件,同时输出到之前创建的 /app 路径下。 + +``` +FROM mcr.microsoft.com/dotnet/core/sdk:3.0-buster AS build +WORKDIR /src +COPY . . +WORKDIR /src/templates/service/host/Base.IdentityServer +RUN dotnet restore -nowarn:msb3202,nu1503 +RUN dotnet build --no-restore -c Release -o /app + +``` + +上面一步可以看成我们在使用 VS 生成 Release 版本的解决方案,当生成没有出错之后,我们就可以进行程序的发布。 +``` +FROM build AS publish +RUN dotnet publish --no-restore -c Release -o /app +``` +  当已经生成发布文件之后,按照我们平时部署在 Windows 上的过程,这时就可以通过 IIS 部署运行了,因此,构建我们应用镜像的最后一步就是通过 dotnet 命令执行我们的程序。 +``` +FROM base AS final +WORKDIR /app +COPY --from=publish /app . +ENTRYPOINT ["dotnet", "Base.IdentityServer.dll"] +``` +  似乎到这一步构建程序镜像就结束了,按照这样流程做的话,就需要我们将整个的解决方案上传到服务器上了,可是,很多时候,我们仅仅是把我们在本地发布好的项目上传到服务器上,这与我们现在的构建流程具有很大的不同,所以这里我们来修改 Dockerfile 文件,从而符合我们的发布流程。 + +  从上面分析 Dockerfile 的过程中不难看出,在服务器上构建镜像的第二步、第三步就是我们现在在开发环境中手动完成的部分,所以这里,我们只需要对这部分进行删除即可,修改后的 Dockerfile 如下。 +``` +FROM mcr.microsoft.com/dotnet/core/sdk:3.0-buster +WORKDIR /app +COPY . /app +EXPOSE 80 +EXPOSE 443 +ENTRYPOINT ["dotnet", "Base.IdentityServer.dll"] +``` +  在修改后的 Dockerfile 中,可以看到,我们删去了 build 和 release 的过程,选择直接将我们 Dockerfile 路径下的文件拷贝到镜像中的 /app 路径下,然后直接执行 dotnet 命令,运行我们的程序。 + +  为了确保 Dockerfile 与发布后的文件处于同一路径下,这里我们需要使用 VS 修改 Dockerfile 的属性值,确保会复制到输出的目录下,这里选择如果较新则复制即可。 + +![cmn](./../img/c_sqlserver_nginx7.png) + +#### 3、编写 docker-compose.yml + +  当我们构建好应用的镜像,对于 Nginx 和 sqlserver 我们完全可以从 hub 中拉取下来,再执行一些配置即可。所以,我们现在就可以编写 docker compose 文件,来定义我们的应用镜像运行时需要包含的依赖以及每个镜像的启动顺序。 + +  右键选中 MVC 项目,添加一个 docker-compose.yml 文件,同样的,需要修改该文件的属性,以便于该文件可以复制到输出目录下。注意,这里的文件名和上文的 Dockerfile 都是特定的,你不能做任何的修改。如果你的电脑上已经安装了 Docker for Windows,你也可以使用 VS,右键添加,选中容器业务流程协调程序支持自动对 docker compose 进行配置。 + +![cmn](./../img/c_sqlserver_nginx8.png) + +在 yml 文件中,我定义了三个镜像:AdminApiGateway.Host、Base.IdentityServer、Base.HttpApi.Host。三个镜像的定义中有许多相同的地方,都设置了自动重启(restart),以及都处于同一个桥接网络下(psu-net)从而达到镜像间的通信。 + + +sqlserver 是 SqlServer 的镜像,我们通过环境变量 SA_PASSWORD 设置了 SqlServer 的数据库连接密码,并通过挂载卷的方式将镜像中的数据库文件持久化到我们的服务器本地路径中。同时,将镜像的 1433 端口映射到服务器的 1433 端口上。 + +  AdminApiGateway.Host 则是我们的程序后台网关镜像,采用位于 /data/dotnet/AdminApiGateway/ 路径下的 Dockerfile 文件进行构建的,因为主程序的运行需要依赖于数据库,所以这里采用 depends_on 属性,使我们的应用镜像依赖于 sqlserver 镜像,即,在 sqlserver 启动后才会启动应用镜像。 + +  nginx 则是我们的 nginx 镜像,这里将镜像中的 80 端口和 443 端口都映射到服务器 IP 上,因为我们需要配置 Nginx 从而监听我们的程序,所以通过挂载卷的方式,将本地的 nginx.conf 配置文件用配置映射到镜像中。同时,因为我们在构建应用镜像的 Dockerfile 文件时,对外暴露了 80,443 端口,所以这里就可以通过 links 属性进行监听(如果构建时未暴露端口,你可以在 docker compose 文件中通过 Expose 属性暴露镜像中的端口)。 + +  Nginx 的配置文件如下,这里特别需要注意文件的格式,缩进,一点小错误都可能导致镜像无法正常运行。如果你和我一样将 nginx.conf 放到程序运行路径下的,别忘了修改文件的属性。 +``` +user nginx; +worker_processes 1; + +error_log /var/log/nginx/error_log.log warn; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on; + + gzip on; #开启gzip + gzip_disable "msie6"; #IE6不使用gzip + gzip_vary on; #设置为on会在Header里增加 "Vary: Accept-Encoding" + gzip_proxied any; #代理结果数据的压缩 + gzip_comp_level 6; #gzip压缩比(1~9),越小压缩效果越差,但是越大处理越慢,所以一般取中间值 + gzip_buffers 16 8k; #获取多少内存用于缓存压缩结果 + gzip_http_version 1.1; #识别http协议的版本 + gzip_min_length 1k; #设置允许压缩的页面最小字节数,超过1k的文件会被压缩 + gzip_types application/javascript text/css; #对特定的MIME类型生效,js和css文件会被压缩 + + include /etc/nginx/conf.d/*.conf; + + server { + #nginx同时开启http和https + listen 80 default backlog=2048; + listen 443 ssl; + server_name ysf.djtlpay.com; + + ssl_certificate /ssl/1_ysf.djtlpay.com_bundle.crt; + ssl_certificate_key /ssl/2_ysf.djtlpay.com.key; + + location / { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $http_host; + proxy_cache_bypass $http_upgrade; + root /usr/share/nginx/html; + index index.html index.htm; + } + } +} +``` +一个完整的 docker compose 文件如下,包含了三个镜像以及一个桥接网络。 +``` +version: '3.0' + +services: + # 指定服务名称 + sqlserver: + # 指定服务使用的镜像 + image: mcr.microsoft.com/mssql/server + # 指定容器名称 + container_name: sqlserver + # 指定服务运行的端口 + ports: + - "1433" + # 指定容器中需要挂载的文件 + volumes: + - /data/sqlserver:/var/opt/mssql + # 挂断自动重新启动 + restart: always + environment: + - TZ=Asia/Shanghai + - SA_PASSWORD=mssql-MSSQL + - ACCEPT_EULA=Y + # 指定容器运行的用户为root + user: + root + # 指定服务名称 + redis: + # 指定服务使用的镜像 + image: redis + # 指定容器名称 + container_name: redis + # 指定服务运行的端口 + ports: + - 6379:6379 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + - /data/redis:/data + - /data/redis/redis.conf:/etc/redis.conf + # 挂断自动重新启动 + restart: always + # 指定容器执行命令 + command: redis-server /etc/redis.conf --requirepass xiujingredis. --appendonly yes + # 指定容器的环境变量 + environment: + - TZ=Asia/Shanghai # 设置容器时区与宿主机保持一致 + # 指定服务名称 + mongo: + # 指定服务使用的镜像 + image: mongo + # 指定容器名称 + container_name: mongo + # 指定服务运行的端口 + ports: + - 27017:27017 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + - /data/mongodb/db:/data/db + - /data/mongodb/configdb:/data/configdb + - /data/mongodb/initdb:/docker-entrypoint-initdb.d + # 挂断自动重新启动 + restart: always + # 指定容器的环境变量 + environment: + - TZ=Asia/Shanghai # 设置容器时区与宿主机保持一致 + - AUTH=yes + - MONGO_INITDB_ROOT_USERNAME=admin + - MONGO_INITDB_ROOT_PASSWORD=admin + nginx: + # 指定服务使用的镜像 + image: nginx + # 指定容器名称 + container_name: nginx + # 指定服务运行的端口 + ports: + - 80:80 + - 443:443 + # 指定容器中需要挂载的文件 + volumes: + - /etc/localtime:/etc/localtime + # 挂断自动重新启动 + restart: always + + AdminApiGateway.Host: + image: 'volosoft/microservice-demo-public-website-gateway:${TAG:-latest}' + build: + context: ../ + dockerfile: micro/gateways/AdminApiGateway.Host/Dockerfile + depends_on: + - sqlserver + - redis + - mongo + + Base.IdentityServer: + image: 'Base.IdentityServer:${TAG:-latest}' + build: + context: ../ + dockerfile: micro/modules/base/host/Base.IdentityServer/Dockerfile + depends_on: + - sqlserver + - redis + - mongo + - AdminApiGateway.Host + + Base.HttpApi.Host: + image: 'Base.HttpApi.Host:${TAG:-latest}' + build: + context: ../ + dockerfile: micro/modules/base/host/Base.HttpApi.Host/Dockerfile + depends_on: + - sqlserver + - redis + - mongo + - AdminApiGateway.Host + - Base.IdentityServer +``` + +这里需要注意,所有有用到镜像间的通信的地方,我们都需要使用镜像名进行指代,例如上面的 nginx 的配置文件中,我们需要将监听的地址改为镜像名称,以及,我们需要修改程序的数据库访问字符串的服务器地址 + +#### 4、发布部署程序 + +  当我们构建好 docker compose 文件后就可以把整个文件上传到服务器上进行构建 docker 镜像了。这里我将所有的部署文件放在服务器的 /data/wwwroot/micro/ 路径下,这时我们就可以通过 docker compose 命令进行镜像构建。 + +  定位到部署文件在的位置,我们可以直接使用下面的命令进行镜像的(重新)构建,启动,并链接一个服务相关的容器,整个过程都会在后台运行,如果你希望看到整个过程的话,你可以去掉 -d 参数。 + +执行镜像构建,启动 +``` +docker-compose up -d +``` +  当 up 命令执行完成后,我们就可以通过 ps 命令查看正在运行的容器,若有的容器并没有运行起来,则可以使用 logs 查看容器的运行日志从而进行排错。 + + 查看所有正在运行的容器 +``` +docker-compose ps +``` +显示容器运行日志 +``` +docker-compose logs +``` +### 三、总结 + +    本章主要是介绍了如何通过 docker 容器,完整的部署一个可实际使用的 .NET Core 的单体应用,相比于之前通过 Linux 部署 .NET Core 应用,可以看到整个步骤少了很多,也简单很多。文中涉及到了一些 docker 的命令,如果你之前并没有接触过 docker 的话,可能需要你进一步的了解。当我们将程序打包成一个镜像之后,你完全可以将镜像上传到私有镜像仓库中,或是直接打包成镜像的压缩文件,这样,当需要切换部署环境时,只需要获取到这个镜像之后即可快速完成部署,相比之前,极大的方便了我们的工作。 diff --git a/blog/zh-cn/sql/mybatis.md b/blog/zh-cn/sql/mybatis.md new file mode 100644 index 0000000..e0f3f56 --- /dev/null +++ b/blog/zh-cn/sql/mybatis.md @@ -0,0 +1,392 @@ +# Mybatis使用心德 + +## 什么是Mybatis? + +1. Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。程序员直接编写原生态sql,可以严格控制sql执行性能,灵活度高。 + +2. MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。 + +3. 通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和 statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)。 + +## Mybaits的优点: + +1. 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。 + +2. 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接; + +3. 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。 + +4. 能够与Spring很好的集成; + +5. 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。 + +## MyBatis框架的缺点: + +1. SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。 + +2. SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。 + +## MyBatis框架适用场合: + +1. MyBatis专注于SQL本身,是一个足够灵活的DAO层解决方案。 + +2. 对性能的要求很高,或者需求变化较多的项目,如互联网项目,MyBatis将是不错的选择。 + +## MyBatis与Hibernate有哪些不同? + +1. Mybatis和hibernate不同,它不完全是一个ORM框架,因为MyBatis需要程序员自己编写Sql语句。 + +2. Mybatis直接编写原生态sql,可以严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频繁,一但需求变化要求迅速输出成果。但是灵活的前提是mybatis无法做到数据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套sql映射文件,工作量大。 + +3. Hibernate对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件,如果用hibernate开发可以节省很多代码,提高效率。 + +## #{}和${}的区别是什么? + +1. #{}是预编译处理,${}是字符串替换。 + +2. Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值; + +3. Mybatis在处理${}时,就是把${}替换成变量的值。 + +4. 使用#{}可以有效的防止SQL注入,提高系统安全性。 + +## 当实体类中的属性名和表中的字段名不一样 ,怎么办 ? + +第1种:通过在查询的sql语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。 + +``` xml + +``` + +第2种:通过 来映射字段名和实体类属性名的一一对应的关系。 + +``` xml + + + + + + + + +``` + +## 模糊查询like语句该怎么写? +第1种:在Java代码中添加sql通配符。 +``` xml +string wildcardname = “%smi%”; +list names = mapper.selectlike(wildcardname); + +``` +第2种:在sql语句中拼接通配符,会引起sql注入 +``` xml +string wildcardname = “smi”; +list names = mapper.selectlike(wildcardname); + +``` + +## 通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗? + +Dao接口即Mapper接口。接口的全限名,就是映射文件中的namespace的值;接口的方法名,就是映射文件中Mapper的Statement的id值;接口方法内的参数,就是传递给sql的参数。 + +Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MapperStatement。在Mybatis中,每一个 ``` + select * fromuser_user_t whereuser_name = #{0} anduser_area=#{1} + +``` +2. 第二种:使用 @param 注解: + +``` java + +public interface usermapper { + user selectuser(@param(“username”) string username,@param(“hashedpassword”) string hashedpassword); +} +``` +然后,就可以在xml像下面这样使用(推荐封装为一个map,作为单个参数传递给mapper): + +``` xml + +``` + +3. 第三种:多个参数封装成map + +``` java +try { + //映射文件的命名空间.SQL片段的ID,就可以调用对应的映射文件中的SQL + //由于我们的参数超过了两个,而方法中只有一个Object参数收集,因此我们使用Map集合来装载我们的参数 + Map < String, Object > map = new HashMap(); + map.put("start", start); + map.put("end", end); + return sqlSession.selectList("StudentID.pagination", map); +} catch (Exception e) { + e.printStackTrace(); + sqlSession.rollback(); + throw e; +} finally { + MybatisUtil.closeSqlSession(); +``` + +## Mybatis动态sql有什么用?执行原理?有哪些动态sql? + +Mybatis动态sql可以在Xml映射文件内,以标签的形式编写动态sql,执行原理是根据表达式的值 完成逻辑判断并动态拼接sql的功能。 + +Mybatis提供了9种动态sql标签: + +trim|where|set|foreach|if|choose|when|otherwise|bind。 + +## Xml映射文件中,除了常见的select|insert|updae|delete标签之外,还有哪些标签? + +``````,加上动态sql的9个标签,其中 ``````为sql片段标签,通过 ``````标签引入sql片段, ``````为不支持自增的主键生成策略标签。 + +## Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复? + +不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复; + +原因就是namespace+id是作为Map 的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。 + +## 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里? + +Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 + +## 一对一、一对多的关联查询 ? + +``` xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +## MyBatis实现一对一有几种方式?具体怎么操作的? + +有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成; + +嵌套查询是先查一个表,根据这个表里面的结果的 外键id,去再另外一个表里面查询数据,也是通过association配置,但另外一个表的查询通过select属性配置。 + +## MyBatis实现一对多有几种方式,怎么操作的? + +有联合查询和嵌套查询。联合查询是几个表联合查询,只查询一次,通过在resultMap里面的collection节点配置一对多的类就可以完成;嵌套查询是先查一个表,根据这个表里面的 结果的外键id,去再另外一个表里面查询数据,也是通过配置collection,但另外一个表的查询通过select节点配置。 + +## Mybatis是否支持延迟加载?如果支持,它的实现原理是什么? + +Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 + +它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。 + +当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。 + +## Mybatis的一级、二级缓存: + +1)一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。 + +2)二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ; + +3)对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。 + +## 什么是MyBatis的接口绑定?有哪些实现方式? + +接口绑定,就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置。 + +接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加上 @Select、@Update等注解,里面包含Sql语句来绑定;另外一种就是通过xml里面写SQL来绑定, 在这种情况下,要指定xml映射文件里面的namespace必须为接口的全路径名。当Sql语句比较简单时候,用注解绑定, 当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多。 + +## 使用MyBatis的mapper接口调用时有哪些要求? + +1、Mapper接口方法名和mapper.xml中定义的每个sql的id相同; +2、Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同; +3、Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同; +4、Mapper.xml文件中的namespace即是mapper接口的类路径。 + +## Mapper编写有哪几种方式? + +接口实现类继承SqlSessionDaoSupport:使用此种方法需要编写mapper接口,mapper接口实现类、mapper.xml文件。 + +1、在sqlMapConfig.xml中配置mapper.xml的位置 + +``` xml + + + + +``` + +2、定义mapper接口 + +3、实现类集成SqlSessionDaoSupportmapper方法中可以this.getSqlSession()进行数据增删改查。 + +4、spring 配置 + +``` xml + + + +``` + +第二种:使用 org.mybatis.spring.mapper.MapperFactoryBean: +1、在sqlMapConfig.xml中配置mapper.xml的位置,如果mapper.xml和mappre接口的名称相同且在同一个目录,这里可以不用配置 +``` xml + + + + +``` + +2、定义mapper接口: + + mapper.xml中的namespace为mapper接口的地址 + mapper接口中的方法名和mapper.xml中的定义的statement的id保持一致 + Spring中定义 + +``` xml + + + + +``` + +第三种:使用mapper扫描器: + +1、mapper.xml文件编写: + +mapper.xml中的namespace为mapper接口的地址;mapper接口中的方法名和mapper.xml中的定义的statement的id保持一致;如果将mapper.xml和mapper接口的名称保持一致则不用在sqlMapConfig.xml中进行配置。 + +2、定义mapper接口: + +注意mapper.xml的文件名和mapper的接口名称保持一致,且放在同一个目录 + +3、配置mapper扫描器: + +``` xml + + + + +``` +4、使用扫描器后从spring容器中获取mapper的实现对象。 + +## 简述Mybatis的插件运行原理,以及如何编写一个插件。 + +Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。 + +编写插件:实现Mybatis的Interceptor接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。 diff --git a/blog/zh-cn/sql/mysql_backups.md b/blog/zh-cn/sql/mysql_backups.md new file mode 100644 index 0000000..4f40692 --- /dev/null +++ b/blog/zh-cn/sql/mysql_backups.md @@ -0,0 +1,382 @@ +# 数据备份与恢复 + + + +## 一、备份简介 + +### 2.1 备份分类 + +按照不同的思考维度,通常将数据库的备份分为以下几类: + +**物理备份 与 逻辑备份** + ++ 物理备份:备份的是完整的数据库目录和数据文件。采用该模式会进行大量的 IO 操作,但不含任何逻辑转换,因此备份和恢复速度通常都比较快。 ++ 逻辑备份:通过数据库结构和内容信息来进行备份。因为要执行逻辑转换,因此其速度较慢,并且在以文本格式保存时,其输出文件的大小大于物理备份。逻辑备份的还原的粒度可以从服务器级别(所有数据库)精确到具体表,但备份不会包括日志文件、配置文件等与数据库无关的内容。 + +**全量备份 与 增量备份** + ++ 全量备份:备份服务器在给定时间点上的所有数据。 ++ 增量备份:备份在给定时间跨度内(从一个时间点到另一个时间点)对数据所做的更改。 + +**在线备份 与 离线备份** + ++ 在线备份:数据库服务在运行状态下进行备份。此时其他客户端依旧可以连接到数据库,但为了保证数据的一致性,在备份期间可能会对数据进行加锁,此时客户端的访问依然会受限。 ++ 离线备份:在数据库服务停机状态下进行备份。此备份过程简单,但由于无法提供对外服务,通常会对业务造成比较大的影响。 + +### 2.2 备份工具 + +MySQL 支持的备份工具有很多种,这里列出常用的三种: + ++ **mysqldump**:这是 MySQL 自带的备份工具,其采用的备份方式是逻辑备份,支持全库备份、单库备份、单表备份。由于其采用的是逻辑备份,所以生成的备份文件比物理备份的大,且所需恢复时间也比较长。 ++ **mysqlpump**:这是 MySQL 5.7 之后新增的备份工具,在 mysqldump 的基础上进行了功能的扩展,支持多线程备份,支持对备份文件进行压缩,能够提高备份的速度和降低备份文件所需的储存空间。 ++ **Xtrabackup**:这是 Percona 公司开发的实时热备工具,能够在不停机的情况下进行快速可靠的热备份,并且备份期间不会间断数据库事务的处理。它支持数据的全备和增备,并且由于其采用的是物理备份的方式,所以恢复速度比较快。 + +## 二、mysqldump + +### 2.1 常用参数 + +mysqldump 的基本语法如下: + +```shell +# 备份数据库或数据库中的指定表 +mysqldump [options] db_name [tbl_name ...] +# 备份多个指定的数据库 +mysqldump [options] --databases db_name ... +# 备份当前数据库实例中的所有表 +mysqldump [options] --all-databases +``` + +options 代表可选操作,常用的可选参数如下: + ++ **--host=host_name, -h host_name** + + 指定服务器地址。 + ++ **--user=user_name, -u user_name** + + 指定用户名。 + ++ **--password[=password], -p[password]** + + 指定密码。通常无需在命令行中明文指定,按照提示输入即可。 + ++ **--default-character-set=charset_name** + + 导出文本使用的字符集,默认为 utf8。 + ++ **--events, -E** + + 备份包含数据库中的事件。 + ++ **--ignore-table=db_name.tbl_name** + + 不需要进行备份的表,必须使用数据库和表名来共同指定。也可以作用于视图。 + ++ **--routines, -R** + + 备份包含数据库中的存储过程和自定义函数。 + ++ **--triggers** + + 备份包含数据库中的触发器。 + ++ **--where='where_condition', -w 'where_condition'** + + 在对单表进行导出时候,可以指定过滤条件,例如指定用户名 `--where="user='jimf'"` 或用户范围 `-w"userid>1"` 。 + ++ **--lock-all-tables, -x** + + 锁定所有数据库中的所有表,从而保证备份数据的一致性。此选项自动关闭 `--single-transaction` 和 `--lock-tables`。 + ++ **--lock-tables, -l** + + 锁定当前数据库中所有表,能够保证当前数据库中表的一致性,但不能保证全局的一致性。 + ++ **--single-transaction** + + 此选项会将事务隔离模式设置为 REPEATABLE READ 并开启一个事务,从而保证备份数据的一致性。主要用于事务表,如 InnoDB 表。 但是此时仍然不能在备份表上执行 ALTER TABLE, CREATE TABLE, DROP TABLE, RENAME TABLE, TRUNCATE TABLE 等操作,因为 REPEATABLE READ 并不能隔离这些操作。 + + 另外需要注意的是 `--single-transaction` 选项与 `--lock-tables` 选项是互斥的,因为 LOCK TABLES 会导致任何正在挂起的事务被隐式提交。转储大表时,可以将 `--single-transaction` 选项与 `--quick` 选项组合使用 。 + ++ **--quick, -q** + + 主要用于备份大表。它强制 mysqldump 一次只从服务器检索一行数据,避免一次检索所有行而导致缓存溢出。 + ++ **--flush-logs, -F** + + 在开始备份前刷新 MySQL 的日志文件。此选项需要 RELOAD 权限。如果此选项与 `--all-databases` 配合使用,则会在每个数据库开始备份前都刷新一次日志。如果配合 `--lock-all-tables`,`--master-data` 或 `--single-transaction` 使用,则只会在锁定所有表或者开启事务时刷新一次。 + ++ **--master-data[=*value*]** + + 可以通过配置此参数来控制生成的备份文件是否包含 CHANGE MASTER 语句,该语句中包含了当前时间点二进制日志的信息。该选项有两个可选值:1 和 2 ,设置为 1 时 CHANGE MASTER 语句正常生成,设置为 2 时以注释的方式生成。`--master-data` 选项还会自动关闭 `--lock-tables` 选项,而且如果你没有指定 `--single-transaction` 选项,那么它还会启用 `--lock-all-tables` 选项,在这种情况下,会在备份开始时短暂内获取全局读锁。 + +### 2.2 全量备份 + +mysqldump 的全量备份与恢复的操作比较简单,示例如下: + +```shell +# 备份雇员库 +mysqldump -uroot -p --databases employees > employees_bak.sql + +# 恢复雇员库 +mysql -uroot -p < employees_bak.sql +``` + +单表备份: + +```shell +# 备份雇员库中的职位表 +mysqldump -uroot -p --single-transaction employees titles > titles_bak.sql + +# 恢复雇员库中的职位表 +mysql> use employees; +mysql> source /root/mysqldata/titles_bak.sql; +``` + +### 2.3 增量备份 + +mysqldump 本身并不能直接进行增量备份,需要通过分析二进制日志的方式来完成。具体示例如下: + +#### 1. 基础全备 + +1.先执行一次全备作为基础,这里以单表备份为例,需要用到上文提到的 `--master-data` 参数,语句如下: + +```shell +mysqldump -uroot -p --master-data=2 --flush-logs employees titles > titles_bak.sql +``` + +使用 more 命令查看备份文件,此时可以在文件开头看到 CHANGE MASTER 语句,语句中包含了二进制日志的名称和偏移量信息,具体如下: + +```sql +-- CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000004', MASTER_LOG_POS=155; +``` + +#### 2. 增量恢复 + +对表内容进行任意修改,然后通过分析二进制日志文件来生成增量备份的脚本文件,示例如下: + +```shell +mysqlbinlog --start-position=155 \ +--database=employees ${MYSQL_HOME}/data/mysql-bin.000004 > titles_inr_bak_01.sql +``` + +需要注意的是,在实际生产环境中,可能在全量备份后与增量备份前的时间间隔里生成了多份二进制文件,此时需要对每一个二进制文件都执行相同的命令: + +```shell +mysqlbinlog --database=employees ${MYSQL_HOME}/data/mysql-bin.000005 > titles_inr_bak_02.sql +mysqlbinlog --database=employees ${MYSQL_HOME}/data/mysql-bin.000006 > titles_inr_bak_03.sql +..... +``` + +之后将全备脚本 ( titles_bak.sql ),以及所有的增备脚本 ( inr_01.sql,inr_02.sql .... ) 通过 source 命令导入即可,这样就完成了全量 + 增量的恢复。 + +## 三、mysqlpump + +### 3.1 功能优势 + +mysqlpump 在 mysqldump 的基础上进行了扩展增强,其主要的优点如下: + +- 能够并行处理数据库及其中的对象,从而可以加快备份进程; + +- 能够更好地控制数据库及数据库对象(表,存储过程,用户帐户等); + +- 能够直接对备份文件进行压缩; + +- 备份时能够显示进度指标(估计值); +- 备份用户时生成的是 CREATE USER 与 GRANT 语句,而不是像 mysqldump 一样备份成数据,可以方便用户按需恢复。 + +### 3.2 常用参数 + +mysqlpump 的使用和 mysqldump 基本一致,这里不再进行赘述。以下主要介绍部分新增的可选项,具体如下: + ++ **--default-parallelism=N** + + 每个并行处理队列的默认线程数。默认值为 2。 + ++ **--parallel-schemas=[N:]db_list** + + 用于并行备份多个数据库:db_list 是一个或多个以逗号分隔的数据库名称列表;N 为使用的线程数,如果没有设置,则使用 `--default-parallelism` 参数的值。 + ++ **--users** + + 将用户信息备份为 CREATE USER 语句和 GRANT 语句 。如果想要只备份用户信息,则可以使用下面的命令: + + ```shell + mysqlpump --exclude-databases=% --users + ``` + ++ **--compress-output=algorithm** + + 默认情况下,mysqlpump 不对备份文件进行压缩。可以使用该选项指定压缩格式,当前支持 LZ4 和 ZLIB 两种格式。需要注意的是压缩后的文件可以占用更少的存储空间,但是却不能直接用于备份恢复,需要先进行解压,具体如下: + + ```shell + # 采用lz4算法进行压缩 + mysqlpump --compress-output=LZ4 > dump.lz4 + # 恢复前需要先进行解压 + lz4_decompress input_file output_file + + # 采用ZLIB算法进行压缩 + mysqlpump --compress-output=ZLIB > dump.zlib + zlib_decompress input_file output_file + ``` + + MySQL 发行版自带了上面两个压缩工具,不需要进行额外安装。以上就是 mysqlpump 新增的部分常用参数,完整参数可以参考官方文档:[mysqlpump — A Database Backup Program](https://dev.mysql.com/doc/refman/8.0/en/mysqlpump.html#option_mysqlpump_compress-output) + +## 四、Xtrabackup + +### 4.1 在线安装 + +Xtrabackup 可以直接使用 yum 命令进行安装,这里我的 MySQL 为 8.0 ,对应安装的 Xtrabackup 也为 8.0,命令如下: + +```shell +# 安装Percona yum 源 +yum install https://repo.percona.com/yum/percona-release-latest.noarch.rpm + +# 安装 +yum install percona-xtrabackup-80 +``` + +### 4.2 全量备份 + +全量备份的具体步骤如下: + +#### 1. 创建备份 + +Xtrabackup 全量备份的基本语句如下,可以使用 target-dir 指明备份文件的存储位置,parallel 则是指明操作的并行度: + +```shell +xtrabackup --backup --user=root --password --parallel=3 --target-dir=/data/backups/ +``` + +以上进行的是整个数据库实例的备份,如果需要备份指定数据库,则可以使用 --databases 进行指定。 + +另外一个容易出现的异常是:Xtrabackup 在进行备份时,默认会去 `/var/lib/mysql/mysql.sock` 文件里获取数据库的 socket 信息,如果你修改了数据库的 socket 配置,则需要使用 --socket 参数进行重新指定,否则会抛出找不到连接的异常。备份完整后需要立即执行的另外一个操作是 prepare (准备备份)。 + +#### 2. 准备备份 + +由于备份是将所有物理库表等文件复制到备份目录,而整个过程需要持续一段时间,此时备份的数据中就可能会包含尚未提交的事务或已经提交但尚未同步至数据文件中的事务,最终导致备份结果处于不一致状态。此时需要进行 prepare 操作来回滚未提交的事务及同步已经提交的事务至数据文件,从而使得整体达到一致性状态。命令如下: + +```shell +xtrabackup --prepare --target-dir=/data/backups/ +``` + +需要特别注意的在该阶段不要随意中断 xtrabackup 进程,因为这可能会导致数据文件损坏,备份将无法使用。 + +#### 3. 恢复备份 + +由于 xtrabackup 执行的是物理备份,所以想要进行恢复,必须先要停止 MySQL 服务。同时这里我们可以删除 MySQL 的数据目录来模拟数据丢失的情况,之后使用以下命令将备份文件拷贝到 MySQL 的数据目录下: + +```shell +# 模拟数据异常丢失 +rm -rf /usr/app/mysql-8.0.17/data/* + +# 将备份文件拷贝到 data 目录下 +xtrabackup --copy-back --target-dir=/data/backups/ +``` + +copy-back 命令只需要指定备份文件的位置,不需要指定 MySQL 数据目录的位置,因为 Xtrabackup 会自动从 `/etc/my.cnf` 上获取 MySQL 的相关信息,包括数据目录的位置。如果不需要保留备份文件,可以直接使用 `--move-back` 命令,代表直接将备份文件移动到数据目录下。此时数据目录的所有者通常为执行命令的用户,需要更改为 mysql 用户,命令如下: + +```shell +chown -R mysql:mysql /usr/app/mysql-8.0.18/data +``` + +再次启动即可完成备份恢复。 + +### 4.3 增量备份 + +使用 Xtrabackup 进行增量备份时,每一次增量备份都需要以上一次的备份为基础,之后再将增量备份运用到第一次全备之上,从而完成备份。具体操作如下: + +#### 1. 创建备份 + +这里首先创建一个全备作为基础: + +```shell +xtrabackup --backup --user=root --password=xiujingmysql. --host=172.17.0.4 --port=13306 --datadir=/data/mysql/data --parallel-3 --target-dir=/data/backups +``` + +之后修改库中任意数据,然后进行第一次增量备份,此时需要使用 `incremental-basedir` 指定基础目录为全备目录: + +```shell +xtrabackup --user=root --password --backup --target-dir=/data/backups/inc1 \ +--incremental-basedir=/data/backups/base +``` + +再修改库中任意数据,然后进行第二次增量备份,此时需要使用 `incremental-basedir` 指定基础目录为上一次增备目录: + +```shell +xtrabackup --user=root --password --backup --target-dir=/data/backups/inc2 \ +--incremental-basedir=/data/backups/inc1 +``` + +#### 2. 准备备份 + +准备基础备份: + +```shell +xtrabackup --prepare --apply-log-only --target-dir=/data/backups/base +``` + +将第一次备份作用于全备数据: + +```shell +xtrabackup --prepare --apply-log-only --target-dir=/data/backups/base \ +--incremental-dir=/data/backups/inc1 +``` + +将第二次备份作用于全备数据: + +```shell +xtrabackup --prepare --target-dir=/data/backups/base \ +--incremental-dir=/data/backups/inc2 +``` + +在准备备份时候,除了最后一次增备外,其余的准备命令都需要加上 `--apply-log-only` 选项来阻止事务的回滚,因为备份时未提交的事务可能正在进行,并可能在下一次增量备份中提交,如果不进行阻止,那么增量备份将没有任何意义。 + +#### 3. 恢复备份 + +恢复备份和全量备份时相同,只需要最终准备好的全备数据复制到 MySQL 的数据目录下即可: + +```shell +xtrabackup --copy-back --target-dir=/data/backups/base +# 必须修改文件权限,否则无法启动 +chown -R mysql:mysql /usr/app/mysql-8.0.17/data +``` + +此时增量备份就已经完成。需要说明的是:按照上面的情况,如果第二次备份之后发生了宕机,那么第二次备份后到宕机前的数据依然没法通过 Xtrabackup 进行恢复,此时就只能采用上面介绍的分析二进制日志的恢复方法。由此可以看出,无论是采用何种备份方式,二进制日志都是非常重要的,因此最好对其进行实时备份。 + +## 五、二进制日志的备份 + +想要备份二进制日志文件,可以通过定时执行 cp 或 scp 等命令来实现,也可以通过 mysqlbinlog 自带的功能来实现远程备份,将远程服务器上的二进制日志文件复制到本机,命令如下: + +```shell +mysqlbinlog --read-from-remote-server --raw --stop-never \ +--host=主机名 --port=3306 \ +--user=用户名 --password=密码 初始复制时的日志文件名 +``` + +需要注意的是这里的用户必须具有 replication slave 权限,因为上述命令本质上是模拟主从复制架构下,从节点通过 IO 线程不断去获取主节点的二进制日志,从而达到备份的目的。 + + + +## 参考资料 + ++ [Chapter 7 Backup and Recovery](https://dev.mysql.com/doc/refman/8.0/en/backup-and-recovery.html) ++ [mysqldump — A Database Backup Program](https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html) ++ [mysqlpump — A Database Backup Program](https://dev.mysql.com/doc/refman/8.0/en/mysqlpump.html) ++ [Percona XtraBackup - Documentation](https://www.percona.com/doc/percona-xtrabackup/LATEST/index.html) diff --git a/blog/zh-cn/tool/minio.md b/blog/zh-cn/tool/minio.md new file mode 100644 index 0000000..a1db8db --- /dev/null +++ b/blog/zh-cn/tool/minio.md @@ -0,0 +1,156 @@ +# MinIO 搭建使用 + +## MinIO简介 + +MinIO 是一款基于Go语言的高性能对象存储服务,在Github上已有19K+Star。它采用了Apache License v2.0开源协议,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等。 本文将使用 MinIO 来自建一个对象存储服务用于存储图片。 + +## 安装及部署 + +> MinIO的安装方式有很多,这里我们使用它在Docker环境下的安装方式。 + +* 下载MinIO的Docker镜像: + +``` shell +docker pull minio/minio +``` + +* 在Docker容器中运行MinIO,这里我们将MiniIO的数据和配置文件夹挂在到宿主机上: + +``` shell +docker run -p 9000:9000 --name minio \ + --restart=always \ + -v /etc/localtime:/etc/localtime \ + -v /data/minio/data:/data \ + -v /data/minio/config:/root/.minio \ + -d minio/minio server /data +``` + +* 运行成功后,访问该地址来登录并使用MinIO,默认Access Key和Secret都是minioadmin:http://localhost:9000 + +![minio](./../img/minio1.png) + +## 上传文件及使用 + +>通过使用MinIO的网页端即可完成文件的上传下载功能,下面我们以图片上传下载为例来演示下该功能。 + +* 在存储文件之前,我们需要新建一个存储桶: + +![minio](./../img/minio2.png) + +* 存储桶创建完成后,通过上传按钮可以上传文件,这里我们上传一张图片: + +![minio](./../img/minio3.png) + +* 图片上传完成后,我们可以通过拷贝链接按钮来获取图片访问路径,但是这只是个临时的访问路径: + +![minio](./../img/minio4.png) + +* 要想获取一个永久的访问路径,需要修改存储桶的访问策略,我们可以点击存储桶右上角的编辑策略按钮来修改访问策略; + +![minio](./../img/minio5.png) + +* 这里有三种访问策略可以选择,一种只读、一种只写、一种可读可写,这里我们选择只读即可,但是需要注意的是,访问前缀需要设置为*.*,否则会无法访问; + +![minio](./../img/minio6.png) + +* 设置完成后,我们只需要通过拷贝链接中的前一串路径即可永久访问该文件; + +## MinIO客户端的使用 + +>虽然MinIO的网页端管理已经很方便了,但是官网还是给我们提供了基于命令行的客户端MinIO Client(简称mc),下面我们来讲讲它的使用方法。 + +### 常用命令 + +> 下面我们先来熟悉下mc的命令,这些命令和Linux中的命令有很多相似之处。 + +| 命令 | 作用 | +| ------- | --------------------------------------------- | +| ls | 列出文件和文件夹 | +| mb | 创建一个存储桶或一个文件夹 | +| cat | 显示文件和对象内容 | +| pipe | 将一个STDIN重定向到一个对象或者文件或者STDOUT | +| share | 生成用于共享的URL | +| cp | 拷贝文件和对象 | +| mirror | 给存储桶和文件夹做镜像 | +| find | 基于参数查找文件 | +| diff | 对两个文件夹或者存储桶比较差异 | +| rm | 删除文件和对象 | +| events | 管理对象通知 | +| watch | 监听文件和对象的事件 | +| policy | 管理访问策略 | +| session | 为cp命令管理保存的会话 | +| config | 管理mc配置文件 | +| update | 检查软件更新 | +| version | 输出版本信息 | + +### 安装及配置 + +> 由于MinIO服务端中并没有自带客户端,所以我们需要安装配置完客户端后才能使用,这里以Docker环境下的安装为例。 + +- 下载MinIO Client 的Docker镜像: + +```bash +docker pull minio/mc +``` + +- 在Docker容器中运行mc: + +```bash +docker run -it --entrypoint=/bin/sh minio/mc +``` + +- 运行完成后我们需要进行配置,将我们自己的MinIO服务配置到客户端上去,配置的格式如下: + +```bash +mc config host add +``` + +- 对于我们的MinIO服务可以这样配置: + +```bash +mc config host add minio http://localhost:9000 minioadmin minioadmin S3v4 +``` + +### 常用操作 + +- 查看存储桶和查看存储桶中存在的文件: + +```bash +# 查看存储桶 +mc ls minio +# 查看存储桶中存在的文件 +mc ls minio/blog +``` + +![minio](./../img/minio7.png) + +- 创建一个名为`test`的存储桶: + +```bash +mc mb minio/test +``` + +- 共享`avatar.png`文件的下载路径: + +```bash +mc share download minio/blog/avatar.png +``` + +- 查找`blog`存储桶中的png文件: + +```bash +mc find minio/blog --name "*.png" +``` + +- 设置`test`存储桶的访问权限为`只读`: + +```bash +# 目前可以设置这四种权限:none, download, upload, public +mc policy set download minio/test/ +# 查看存储桶当前权限 +mc policy list minio/test/ +``` + +## 参考资料 + +详细了解MinIO可以参考官方文档:https://docs.min.io/cn/minio-quickstart-guide.html diff --git a/blog/zh-cn/web/javascript.md b/blog/zh-cn/web/javascript.md new file mode 100644 index 0000000..89be9f1 --- /dev/null +++ b/blog/zh-cn/web/javascript.md @@ -0,0 +1,841 @@ +# JavaScript 基础 + + + +## 一、概念简介 + +JavaScript 是一种专为与网页交互而设计的脚本语言,由以下三个部分组成: + +- **ECMAScript**:由 ECMA-262 定义,提供核心语言功能; +- **文档对象模型 (DOM)**:提供访问和操作网页内容的方法和接口; +- **浏览器对象模型 (BOM)**:提供与浏览器交互的方法和接口。 + +ECMAScript 提供了语言的核心功能,它定义了以下七种数据类型: + +- **六种基本数据类型**:`Undefined`,`Null`,`Boolean`,`Number`,`String`,`Symbol` ( ES 6新增 ); +- **一种引用数据类型**:统称为 Object 类型;具体又细分为 `Object`,`Array`,`Date`,`RegExp`,`Function` 等类型。另外和 Java 语言类似,对于布尔,数值,字符串等基本类型,分别存在其对应的包装类型 Boolean,Number,String,但通常我们并不会使用到这些包装类型,只需要使用其基本类型即可。 + +## 二、基本类型 + +### 2.1 数值类型 + +**1. 进制数值** + +ECMAScript 中的 Number 支持以下三种常用进制: + ++ **十进制**:正常数值就是十进制; ++ **八进制**:八进制字面值的第一位必须是零(0),然后是八进制数字序列(0~7); ++ **十六进制**:十六进制字面值的前两位必须是 0x,后跟任意的十六进制数字(0~9 及 A~F)。 + +```javascript +console.log(56); // 56 +console.log(070); // 56 +console.log(0x38); // 56 +``` + +**2. 浮点数值** + +ECMAScript 的数值类型同样支持浮点数,但是由于保存浮点数值需要的内存空间是保存整数值的两倍,因此 ECMAScript 会尽量将浮点数值转换为整数值存储: + +```javascript +var a = 10.0; +console.log(a); // 10 +``` + +和其他语言类似,浮点数中的数值也是不精准的,示例如下: + +```javascript +var a = 0.1; var b = 0.2; + +a + b ; // 0.30000000000000004 +a+b === 0.3 ; // false +``` + +如果想要对浮点数进行精确计算,可以使用 [decimal.js](https://github.com/MikeMcl/decimal.js) ,[ math.js](https://github.com/josdejong/mathjs) 等第三方库。 + +**3. 科学计数法** + +ECMAScript 支持使用科学计数法来表达数值: + +```javascript +8e-2 // 0.08 +8e2 // 800 +``` + +**4. parseInt() \ parseFloat()** + +parseInt 可以用于解析字符串并返回整数,parseFloat 用于解析字符串并返回浮点数: + +```javascript +parseInt("56"); // 56 +parseInt("0x38", 16); // 56 支持使用第二个参数来表示转换的进制 +parseInt("56.6"); // 56 + +parseFloat("12.2"); // 12.2 + +parseInt("blue"); // NaN NaN用于表示一个本来要返回数值的操作却未返回数值的情况 +``` + +**5. toFixed()** + + toFixed 用于保留指定位数的小数,但需要注意的是其四舍五入的行为是不确定的: + +```javascript +1.35.toFixed(1) // 1.4 正确 +1.335.toFixed(2) // 1.33 错误 +1.3335.toFixed(3) // 1.333 错误 +1.33335.toFixed(4) // 1.3334 正确 +1.333335.toFixed(5) // 1.33333 错误 +1.3333335.toFixed(6) // 1.333333 错误 +``` + +想要解决这个问题,需要重写 toFixed 方法并通过判断最后一位是否大于或等于5来决定是否需要进位,具体代码如下: + +```javascript +// toFixed兼容方法 +Number.prototype.toFixed = function(len){ + if(len>20 || len<0){ + throw new RangeError('toFixed() digits argument must be between 0 and 20'); + } + // .123转为0.123 + var number = Number(this); + if (isNaN(number) || number >= Math.pow(10, 21)) { + return number.toString(); + } + if (typeof (len) == 'undefined' || len == 0) { + return (Math.round(number)).toString(); + } + var result = number.toString(), + numberArr = result.split('.'); + + if(numberArr.length<2){ + //整数的情况 + return padNum(result); + } + var intNum = numberArr[0], //整数部分 + deciNum = numberArr[1],//小数部分 + lastNum = deciNum.substr(len, 1);//最后一个数字 + + if(deciNum.length == len){ + //需要截取的长度等于当前长度 + return result; + } + if(deciNum.length < len){ + //需要截取的长度大于当前长度 1.3.toFixed(2) + return padNum(result) + } + //需要截取的长度小于当前长度,需要判断最后一位数字 + result = intNum + '.' + deciNum.substr(0, len); + if(parseInt(lastNum, 10)>=5){ + //最后一位数字大于5,要进位 + var times = Math.pow(10, len); //需要放大的倍数 + var changedInt = Number(result.replace('.',''));//截取后转为整数 + changedInt++;//整数进位 + changedInt /= times;//整数转为小数,注:有可能还是整数 + result = padNum(changedInt+''); + } + return result; + //对数字末尾加0 + function padNum(num){ + var dotPos = num.indexOf('.'); + if(dotPos === -1){ + //整数的情况 + num += '.'; + for(var i = 0;i 参考自:[*js中小数四舍五入和浮点数的研究*](http://caibaojian.com/js-tofixed.html) + +### 2.2 字符类型 + +**1. 字符串表示** + +ECMAScript 支持使用双引号 ` " ` 或单引号 ` ' ` 来表示字符串,并且 ECMAScript 中的字符串是不可变的,也就是说,字符串一旦创建,它们的值就不能改变。要改变某个变量保存的字符串,首先要销毁原来的字符串,然后再用另一个包含新值的字符串填充该变量,示例如下: + +```javascript +var lang = "Java"; +/*程序会创建一个能容纳 10 个字符的新字符串用于填充"Java"和"Script",之后再销毁原有的字符串"Java"和"Script"*/ +lang = lang + "Script"; +``` + +**2. 转换为字符串** + +要把一个值转换为一个字符串有两种方式: + ++ 使用对象方法 **toString()** :大多数对象都具有这个方法,但需要注意的是 null 和 undefined 没有; ++ 使用转型函数 **String()** :使用该转型函数时,如果传入的值有 toString() 方法,则调用该方法并返回相应的结果;如果传入的值是 null,则返回 "null" ;如果传入值是 undefined,则返回 "undefined" 。 示例如下: + +```javascript +var a = null; +a.toString() // Uncaught TypeError: Cannot read property 'toString' of null +String(a) // "null" +``` + +**3. 常用的字符串操作** + ++ **concat()** :用于拼接一个或多个字符串; ++ **slice()** :用于截取字符串,接收两个参数,分别代表截取的开始位置和结束位置; ++ **substring()**:用于截取字符串,接收两个参数,分别代表截取的开始位置和结束位置; ++ **substr()** :用于截取字符串,接收两个参数,分别代表截取的开始位置和截取的长度; ++ **indexOf() \ lastIndexOf()** :均接收两个参数,分别代表待查找的字符串和查找的开始位置; ++ **trim()** :用于去除字符串前后的空格。 + +slice,substring,substr 等方法在传入正数参数时,其行为比较好预期,但传递参数是负数时,则具体的行为表现如下: + ++ **slice()** :会将传入的负值与字符串的长度相加; ++ **substring()** :方法会把所有负值参数都转换为 0 ; ++ **substr()** :会将第一个负值参数加上字符串的长度,如果传递了第二个参数且为负数时候,会将其转换为 0 。 + +```javascript +var stringValue = "hello world"; + +// 只接收一个参数时 +alert(stringValue.slice(3)); // "lo world" +alert(stringValue.substring(3)); // "lo world" +alert(stringValue.substr(3)); // "lo world" + +// 接收两个参数时候 +alert(stringValue.slice(3, 7)); // "lo w" +alert(stringValue.substring(3,7)); // "lo w" +alert(stringValue.substr(3, 7)); // "lo worl" + +// 当第一个参数为负值时 +alert(stringValue.slice(-3)); // "rld" 按照规则等价于: slice(8) +alert(stringValue.substring(-3)); // "hello world" 按照规则等价于: substring(0) +alert(stringValue.substr(-3)); // "rld" 按照规则等价于: substr(8) + +// 当第二个参数为负值时 +alert(stringValue.slice(3, -4)); // "lo w" 按照规则等价于: slice(3,7) +alert(stringValue.substring(3, -4)); // "hel" 按照规则等价于: substring(3,0) +alert(stringValue.substr(3, -4)); // ""(空字符串) 按照规则等价于: substr(3,0) +``` + +### 2.3 基本类型检测 + +JavaScript 是一种弱类型的语言,在声明变量时候可以不必指明其具体类型,而是由程序进行推断。如果想要知道变量具体属于哪一个基础类型,可以使用 **typeof** 关键字,它的返回情况如下: + +- **undefined**:如果对应的值未定义; +- **boolean**:如果对应的值是布尔值; +- **string**:如果对应的值是字符串; +- **number**:如果对应的值是数值; +- **object**:如果对应的值是对象或 null; +- **function**:如果对应的值是函数则返回 function。 函数在本质上也是对象,但是由于其一等公民的特殊地位,所以将其和其他普通对象进行区分是很有必要的,因此 typeof 对其检测时会返回 function ,而不是 object 。 + + +## 三、引用类型 + +### 3.1 Object 类型 + +创建 Object 实例有以下两种方式: + ++ 使用 new 操作符后跟着 Object 构造函数; ++ 使用对象字面量的方式。 + +```javascript +// 1. 使用new操作符 +var user = new Object(); +user.name = "heibaiying"; +user.age = 30; + +// 2. 使用对象字面量 +var user = { + name: "heibaiying", + age: 30 +}; +``` + +### 3.2 Array 类型 + +创建数组也有两种方式,基于构造函数的方式和基于对象字面量的方式: + +```javascript +// 1.基于构造函数的方式 +var colors = new Array(); +var colors = new Array(20); +var colors = new Array("red", "blue", "green"); + +// 2.基于对象字面量的方式 +var names = []; +var colors = ["red", "blue", "green"]; +``` + +数组的长度保存在其 length 属性中,和其他语言中的 length 属性不同,这个值是不是只读的,可以用其进行数组的截断操作或添加新的数据项,示例如下: + +```javascript +var colors = ["red", "blue", "green"]; + +colors.length = 2; // ["red", "blue"] +colors[colors.length] = "green"; // ["red", "blue", "green"] +colors[10] = "black"; // ["red", "blue", "green", empty × 7, "black"] +``` + +数组的其他常用方法如下: + +**1. 检测数组** + +```javascript +colors instanceof Array +Array.isArray(colors) +``` + +**2. 转换方法** + +```java +var colors = ["red", "blue", "green"]; + +colors.valueOf(); // [ 'red', 'blue', 'green' ] +colors; // [ 'red', 'blue', 'green' ] +colors.toString(); // red,blue,green +colors.join("|"); // red|blue|green +``` + +**3. 栈方法** + +ECMAScript 的数组提供了类似栈的特性,能够实现后进先出: + +```javascript +var colors = ["red", "blue", "green"]; + +colors.push("black"); // ["red", "blue", "green", "black"] +colors.pop() // "black" +colors // ["red", "blue", "green"] +``` + +**4. 队列方法** + +ECMAScript 的数组提供了类似栈的特性,能够实现先进先出: + +```javascript +colors.push("black","yellow"); // ["red", "blue", "green", "black", "yellow"] +colors.shift() // "red" +colors // ["blue", "green", "black", "yellow"] +``` + +**5. 重排序方法** + +```javascript +var values = [1, 2, 3, 4, 5]; +values.reverse(); +values // [5, 4, 3, 2, 1] + +// 支持传入排序函数进行自定义排序 +function compare(value1, value2) { + if (value1 < value2) { + return -1; + } else if (value1 > value2) { + return 1; + } else { + return 0; + } +} +values.sort(compare) +values // [1, 2, 3, 4, 5] +``` + +**6. 操作方法** + +**concat()** 用于拼接并返回新的数组: + +```javascript +var colors = ["red", "green", "blue"]; +var colors2 = colors.concat("yellow", ["black", "brown"]); + +colors // ["red", "green", "blue"] +colors2 // ["red", "green", "blue", "yellow", "black", "brown"] +``` + +**slice()** 用于截取数组并返回新的数组,它接收两个参数,分别代表截取的开始位置和结束位置,它是一个前开后闭的区间: + +```javascript +var colors = ["red", "green", "blue", "yellow", "purple"]; + +var colors2 = colors.slice(1); // ["green", "blue", "yellow", "purple"] +var colors3 = colors.slice(0,2); // ["red", "green"] +``` + +**splice()** 用于删除并在删除位置新增数据项,它接收任意个参数,其中第一个参数为删除的开始位置,第二个参数为删除多少个数据项,之后可以接任意个参数,用于表示待插入的数据项: + +```javascript +var colors = ["red", "green", "blue", "yellow"]; + +colors.splice(1,2) // 返回删除的数据项:["green", "blue"] +colors // ["red", "yellow"] + +colors.splice(1,0,"black","green") // [] +colors // ["red", "black", "green", "yellow"] +``` + +**7. 位置方法** + +**indexOf()** 和 **lastIndexOf()** 用于查找指定元素的 Index ,它们都接收两个参数:待查找项和查找的起点位置: + +```shell +var colors = ["red", "green", "blue", "yellow", "green", "blue"]; + +colors.indexOf("green"); // 1 +colors.indexOf("green", 3); // 4 +colors.lastIndexOf("green"); // 4 +colors.lastIndexOf("green", 3); // 1 +``` + +**8. 迭代方法** + +ECMAScript 5 提供了五个迭代方法: + +- **every()**:判断数组中的每个元素是否满足指定条件,如果全部满足则返回 true,否则返回 flase; +- **some()**:判断数组中的每个元素是否满足指定条件,只要有一个满足则返回 true,否则返回 flase; +- **filter()**:过滤并返回符合条件的元素组成的数组。 +- **forEach()**:对数组中的每一项运行给定函数。 +- **map()**:对数组中的每一项运行给定函数,并返回每次函数调用结果所组成的数组。 + +```javascript +var numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1]; + +numbers.every(function (value, index, array) { + return value > 3; +}); +// false + +numbers.some(function (value, index, array) { + return value > 3; +}); +// true + +numbers.filter(function (value, index, array) { + return value > 3; +}); +// [4, 5, 4] + +numbers.forEach(function (value, index, array) { + console.log(value); +}); + +numbers.map(function (value, index, array) { + return value * 10; +}); +// [10, 20, 30, 40, 50, 40, 30, 20, 10] +``` + +**9. 归并方法** + +ECMAScript 5 提供了两个归并数组的方法: **reduce()** 和 **reduceRight()** 。 它们都接收四个参数:前一个值、当前值、当前项的索引 和 数组本身,使用示例如下: + +```javascript +var values = [1, 2, 3, 4, 5]; +var sum01 = values.reduce(function (prev, cur, index, array) { + return prev + cur; +}); // 15 + +var sum02 = values.reduceRight(function (prev, cur, index, array) { + return prev + cur; +}); // 15 +``` + +### 3.3 Date 类型 + +创建一个日期对象,可以使用 new 操作符和 Date 构造函数: + +```java +var now = new Date(); +now.toLocaleString() // 2019-9-14 9:53:59 AM + +var date = new Date(2018, 7, 8, 8, 30, 20); +date.toLocaleString(); // 2018-8-8 8:30:20 AM +``` + +如果你只想知道当前时间的毫秒数,可以直接使用 Date 对象的静态方法: + +```javascript +Date.now() +1568426130593 +``` + +**1. 格式转换** + +- **toLocaleString()** :按照浏览器所在时区返回相应的日期格式; +- **toString()** :返回日期时间数据和的时区数据; +- **valueOf()** :返回日期的时间戳格式。 + +```javascript +var date = new Date(2018, 7, 8, 8, 30, 20); + +console.log(date.toLocaleString()); // 2018-8-8 8:30:20 AM +console.log(date.toString()); // Wed Aug 08 2018 08:30:20 GMT+0800 (GMT+08:00) +console.log(date.valueOf()); // 1533688220000 +``` + +由于 **valueOf()** 返回的是日期的时间戳格式,所以对于 date 对象,可以直接使用比较运算符来比较其大小: + +```javascript +var date01 = new Date(2018, 7, 8, 8, 30, 20); +var date02 = new Date(2016, 7, 8, 8, 30, 20); + +console.log(date01 > date02); // true +console.log(date01 < date02); // flase +``` + +**2. 常用方法** + +- **getTime() \ setTime(毫秒)** :返回和设置整个日期的所代表的毫秒数;与 valueOf() 方法返回的值相同; +- **getFullYear() \ setFullYear(年)** :返回和设置4位数的年份; +- **getMonth() \ setMonth(月)** :返回和设置月份,其中 0 表示一月, 11 表示十二月; +- **getDate() \ setDate(日)** :返回和设置月份中的天数(1到31); +- **getDay()** :返回和设置星期几 ( 其中0表示星期日, 6表示星期六); +- **getHours() \ setHours(时)** :返回和设置小时数(0到23); +- **getMinutes() \ setMinutes(分)** :返回和设置日期中的分钟数(0到59); +- **getSeconds() \ setSeconds(秒)** :返回和设置日期中的秒数(0到59); +- **getMilliseconds() \ setMilliseconds(毫秒)** :返回和设置日期中的毫秒数。 + +### 3.4 Funcation 类型 + +**1. 函数参数** + +ECMAScript 使用 function 关键字来声明函数,但和其他语言不同的是,ECMAScript 中函数对于参数的限制是非常宽松的,例如你在定义函数时定义了两个参数,但在调用时可以只传递一个参数、也可以传三个参数,甚至不传递,示例如下: + +```java +function test(first, second) { + console.log("first:" + first + ",second:" + second); +} +test(1) // first:1,second:undefined +test(1,2) // first:1,second:2 +test(1,2,3) // first:1,second:2 +``` + +之所以能实现这样的效果,是因为 ECMAScript 在函数内部使用了一个数组 arguments 来维护所有参数,函数接收到的始终都是这个数组,而在实际使用时指向的也是这个数组中的具体元素,所以以上的函数等价于下面的函数: + +```javascript +function test(first, second) { + console.log("first:" + arguments[0] + ",second:" + arguments[1]); +} +``` + +**2. 改变函数作用域** + +在 ECMAScript 5 中,每个函数都包含两个非继承而来的方法:**apply()** 和 **call()** ,它们都用于在特定的作用域中调用函数。简单来说,可以用这两个方法来改变函数的实际调用对象,从而改变 this 的值,因为 this 总是指向当前函数的实际调用对象: + +```javascript +window.color = "red"; +var o = { color: "blue" }; +function sayColor(){ + console.log(this.color); +} + +sayColor(); // red +sayColor.call(this); // red +sayColor.call(window); // red +sayColor.call(o); // blue 此时this指向的是函数调用对象,即 o +``` + +**apply()** 和 **call()** 的第一个参数都是指代函数的调用对象,它们的区别主要在于第二个参数:**apply()** 支持使用数组或 arguments 给调用函数传值,而 **call()** 给调用函数传值时,必须逐个列举: + +```javascript +function sum(num1, num2) { + return num1 + num2; +} + +function callSum1(num1, num2) { + return sum.apply(this, arguments); +} + +function callSum2(num1, num2) { + return sum.apply(this, [num1, num2]); +} + +function callSum3(num1, num2) { + return sum.call(this, num1, num2); +} + +callSum1(10, 10); +callSum2(10, 10); +callSum3(10, 10); +``` + +**3. 绑定函数作用域** + +如果想要将函数绑定在某个特定的作用域上,可以使用 **bind()** 函数: + +```javascript +window.color = "red"; +var o = { color: "blue" }; +function sayColor(){ + console.log(this.color); +} + +var objectSayColor = sayColor.bind(o); +objectSayColor(); // blue 此时即便是在全局作用域中调用这个函数,其作用域仍然被永远绑定在 o 对象上 +``` + +### 3.5 引用类型检测 + +想要检测某个对象是否属于某个引用类型,可以使用 **instanceof** 关键字: + +```shell +var date = new Date(); +date instanceof Date // true +``` + + + +## 四、内置对象 + +### 4.1 Global 对象 + +ECMAScript 中内置了一个全局对象 Global ,任何不属于任何其他对象的属性和方法,最终都是它的属性和方法。 ES 通过该内置对象,提供了一些可以直接调用的全局方法,常用的如下: + ++ **isNaN()**:用于确定一个值是否为 NaN; ++ **isFinite()**:用于判断被传入的参数值是否为一个有限数值; ++ **parseInt() \ parseFloat()**:解析并返回一个整数 \ 浮点数; ++ **encodeURI()**:对 URI 进行编码,但不会对本身属于 URI 的特殊字符进行编码,例如冒号、正斜杠、问号和井号; ++ **encodeURIComponent()**:对 URI 进行编码,会对任何非标准字符进行编码。 + +### 4.2 window 对象 + +ECMAScript 并没有提供任何直接访问 Global 对象的方法,但是浏览器却基于 Global 扩展实现了 window 对象,可以直接在浏览器环境下使用: + +```javascript +window.isFinite(12) // true +a = 12; +window.a // 12 +``` + + + +## 五、作用域与闭包 + +### 5.1 作用域 + +在 ECMAScript 6 之前,只存在两种作用域,即:全局作用域 和 函数作用域,不存在块级作用域。这意味着在除了函数外的任何代码块中使用 var 关键字声明的变量都会被提升为全局变量,示例如下: + +```javascript +function test() { + var age =12; +} +age // age is not defined + +if (true) { + var name = "heibaiying"; +} +name // heibaiying +``` + +这种情况同样适用与 for 循环代码块: + +```javascript +for (var i = 0; i < 10; i++) {} +console.log(i); // 10 +``` + +### 5.2 作用域链 + +由于函数作用域的存在,函数内的变量不能被外部访问,但是函数内的变量可以被其内部的函数访问,并且函数也可以访问其父级作用域上的变量,从而形成一条从其自身作用域到全局作用域的链条,示例如下: + +```javascript +var global = "global"; +var outer = "outer global"; + +(function outer() { + var outer = "outer"; + + function inner() { + console.log(global, outer); + } + + inner() +})(); + +// 输出:global outer +``` + +### 5.3 闭包 + +由于函数作用域的存在,函数内的变量不能被外部访问,这可以保证变量的私有性。但如果你想允许外部对内部变量进行特定操作,可以通过闭包来实现。闭包是指有权访问另一个函数作用域中的变量的函数。示例如下: + +```java +var contain = function () { + + var arr = []; + + return { + push: function () { + arr.push(...arguments); + }, + + get: function () { + return arr; + } + } +}; + +var ctn = contain(); +ctn.push(1, 2, 3, 4); +ctn.get(); // [ 1, 2, 3, 4 ] +``` + + + +## 六、对象设计 + +ECMAScript 中的对象都有两种基本属性:数据属性和访问器属性。 + +### 6.1 数据属性 + + +数据属性有以下 4 个描述其行为的特性: + ++ **Enumerable**:表示该属性能否通过 for-in 循环返回;对于直接在对象上定义的属性, 该值默认为 true。 ++ **Writable**:表示能否修改属性的值;对于直接在对象上定义的属性, 该值默认为 true。 ++ **Value**:对应属性的数据值。默认值为 undefined。 ++ **Configurable**:表示能否对属性进行删除,修改等配置操作,对于直接在对象上定义的属性, 该值默认为 true。需要注意的是一旦将该属性的值设置为 false,就不能再将其设置为 true 。即一旦设置为不可配置,就不能再修改为可配置。因为你已经修改为不可配置,此时任何配置操作都无效了,自然修改 Configurable 属性的操作也无效。 + +```javascript +var person = {age: 12}; +Object.defineProperty(person, "name", { + Enumerable: false, + writable: false, + value: "heibai" +}); + +console.log(person.name); // heibai +person.name = "ying"; +console.log(person.name); // ying + +for (var key in person) { + console.log("key:" + key + ",value:" + person[key]) /// key:age,value:12 +} +``` + +### 6.2 访问器属性 + +访问器属性也有以下 4 个描述其行为的特性: + ++ **Configurable**:表示能否对属性进行删除,修改等配置操作;对于直接在对象上定义的属性, 该值默认为 true。 ++ **Enumerable**:表示该属性能否通过 for-in 循环返回;对于直接在对象上定义的属性, 该值默认为 true。 ++ **Get**:在读取属性时调用的函数。默认值为 undefined。 ++ **Set**:在写入属性时调用的函数。默认值为 undefined。 + +```javascript +var student = { + _age: null, + birthday: new Date(2012,7,2) +}; +Object.defineProperty(student, "age", { + + get: function () { + if (this._age == null) { + // 如果年龄不存在就根据出生日期计算 + return new Date().getFullYear() - this.birthday.getFullYear() + }else { + return this._age; + } + }, + set: function (newValue) { + if (newValue < 0) { + console.log("年龄不能设置为负数"); + } else { + this._age = newValue; + } + } +}); + +student.age = -1; // 输出:年龄不能设置为负数 +console.log(student.age); // 输出:7 +student.age = 12; +console.log(student.age); // 输出 12 +``` + + +### 6.3 读取属性 + +想要获取一个对象的数据属性和访问器属性,可以使用 **Object.getOwnPropertyDescriptor()** 方法,类似于其他语言中的反射机制。这个方法接收两个参数:属性所在的对象和要读取属性名称。沿用上面的例子,示例如下: + +```javascript +var descriptor = Object.getOwnPropertyDescriptor(student, "age"); +console.log(descriptor.get); // 输出 [Function: get] +console.log(descriptor.enumerable); // 输出 false +``` + +### 6.4 创建对象 + +在 ECMAScript 中,对象就是一种特殊的函数,想要声明一个对象,可以结合使用构造器模式和原型模式:基本属性可以通过构造器传入;但方法声明需要定义在原型属性上,如果直接定义在构造器上,每个对象实例都会创建该方法函数,即每个对象实例调用的都是自己重复声明的方法函数,示例如下: + +```javascript +function Person(name, age, job) { + this.name = name; + this.age = age; + this.job = job; + this.friends = ["hei", "bai"]; + // 方法应该声明在原型属性上,而不是这里 + this.sayAge = function () { + console.log(this.age) + } +} + +Person.prototype = { + constructor: Person, + // 方法应该声明在这 + sayName: function () { + alert(this.name); + } +} + + +var person1 = new Person("user01", 29, "Software Engineer"); +var person2 = new Person("user02", 27, "Doctor"); + +person1.friends.push("ying"); +console.log(person1.friends); // [ 'hei', 'bai', 'ying' ] +console.log(person2.friends); // [ 'hei', 'bai'] +console.log(person1 instanceof Person); // true +console.log(person1.constructor === Person); // true +console.log(person1.sayName === person2.sayName); // true +console.log(person1.sayAge===person2.sayAge); // false +``` + + + + + +## 参考资料 + +1. 尼古拉斯·泽卡斯 . JavaScript高级程序设计(第3版). 人民邮电出版社 . 2012-3-29 +2. [JS中小数四舍五入和浮点数的研究](http://caibaojian.com/js-tofixed.html) + + + diff --git a/blog/zh-cn/web/js_tool_method.md b/blog/zh-cn/web/js_tool_method.md new file mode 100644 index 0000000..d81e5df --- /dev/null +++ b/blog/zh-cn/web/js_tool_method.md @@ -0,0 +1,1006 @@ +# JavaScript 工具函数大全 + +## 数组 + +1. all:布尔全等判断 + +``` js +const all = (arr, fn = Boolean) => arr.every(fn); + +all([4, 2, 3], x => x > 1); // true +all([1, 2, 3]); // true + +``` + +2. allEqual:检查数组各项相等 + +``` js +const allEqual = arr => arr.every(val => val === arr[0]); + +allEqual([1, 2, 3, 4, 5, 6]); // false +allEqual([1, 1, 1, 1]); // true + +``` + +3. approximatelyEqual:约等于 + +``` js +const approximatelyEqual = (v1, v2, epsilon = 0.001) => Math.abs(v1 - v2) < epsilon; + +approximatelyEqual(Math.PI / 2.0, 1.5708); // true + +``` + +4. arrayToCSV:数组转CSV格式(带空格的字符串) + +``` js + +const arrayToCSV = (arr, delimiter = ',') => + arr.map(v => v.map(x => `"${x}"`).join(delimiter)).join('\n'); + +arrayToCSV([['a', 'b'], ['c', 'd']]); // '"a","b"\n"c","d"' +arrayToCSV([['a', 'b'], ['c', 'd']], ';'); // '"a";"b"\n"c";"d"' + +``` + +5. arrayToHtmlList:数组转li列表 + +``` js +const arrayToHtmlList = (arr, listID) => + (el => ( + (el = document.querySelector('#' + listID)), + (el.innerHTML += arr.map(item => `
  • ${item}
  • `).join('')) + ))(); + +arrayToHtmlList(['item 1', 'item 2'], 'myListID'); + +``` + +6. average:平均数 + +``` js +const average = (...nums) => nums.reduce((acc, val) => acc + val, 0) / nums.length; +average(...[1, 2, 3]); // 2 +average(1, 2, 3); // 2 + +``` + +7. averageBy:数组对象属性平均数 + +此代码段将获取数组对象属性的平均值 + +``` js +const averageBy = (arr, fn) => + arr.map(typeof fn === 'function' ? fn : val => val[fn]).reduce((acc, val) => acc + val, 0) / + arr.length; + +averageBy([{ n: 4 }, { n: 2 }, { n: 8 }, { n: 6 }], o => o.n); // 5 +averageBy([{ n: 4 }, { n: 2 }, { n: 8 }, { n: 6 }], 'n'); // 5 + +``` + +8. bifurcate:拆分断言后的数组 + +可以根据每个元素返回的值,使用reduce()和push() 将元素添加到第二次参数fn中 。 + +``` js +const bifurcate = (arr, filter) => + arr.reduce((acc, val, i) => (acc[filter[i] ? 0 : 1].push(val), acc), [[], []]); +bifurcate(['beep', 'boop', 'foo', 'bar'], [true, true, false, true]); +// [ ['beep', 'boop', 'bar'], ['foo'] ] + +``` + +9. castArray:其它类型转数组 + +``` js +const castArray = val => (Array.isArray(val) ? val : [val]); + +castArray('foo'); // ['foo'] +castArray([1]); // [1] +castArray(1); // [1] + +``` + +10. compact:去除数组中的无效/无用值 + +``` js +const compact = arr => arr.filter(Boolean); + +compact([0, 1, false, 2, '', 3, 'a', 'e' * 23, NaN, 's', 34]); +// [ 1, 2, 3, 'a', 's', 34 ] + +``` + +11. countOccurrences:检测数值出现次数 + +``` js +const countOccurrences = (arr, val) => arr.reduce((a, v) => (v === val ? a + 1 : a), 0); +countOccurrences([1, 1, 2, 1, 2, 3], 1); // 3 + +``` + +12. deepFlatten:递归扁平化数组 + +``` js +const deepFlatten = arr => [].concat(...arr.map(v => (Array.isArray(v) ? deepFlatten(v) : v))); + +deepFlatten([1, [2], [[3], 4], 5]); // [1,2,3,4,5] + +``` + +13. difference:寻找差异(并返回第一个数组独有的) + +此代码段查找两个数组之间的差异,并返回第一个数组独有的。 + +``` js + +const difference = (a, b) => { + const s = new Set(b); + return a.filter(x => !s.has(x)); +}; + +difference([1, 2, 3], [1, 2, 4]); // [3] + +``` + +14. differenceBy:先执行再寻找差异 + +在将给定函数应用于两个列表的每个元素之后,此方法返回两个数组之间的差异。 + +``` js +const differenceBy = (a, b, fn) => { + const s = new Set(b.map(fn)); + return a.filter(x => !s.has(fn(x))); +}; + +differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor); // [1.2] +differenceBy([{ x: 2 }, { x: 1 }], [{ x: 1 }], v => v.x); // [ { x: 2 } ] + +``` + +15. dropWhile:删除不符合条件的值 + +此代码段从数组顶部开始删除元素,直到传递的函数返回为true。 + +``` js +const dropWhile = (arr, func) => { + while (arr.length > 0 && !func(arr[0])) arr = arr.slice(1); + return arr; +}; + +dropWhile([1, 2, 3, 4], n => n >= 3); // [3,4] + +``` + +16. flatten:指定深度扁平化数组 + +此代码段第二参数可指定深度。 + +``` js +const flatten = (arr, depth = 1) => + arr.reduce((a, v) => a.concat(depth > 1 && Array.isArray(v) ? flatten(v, depth - 1) : v), []); + +flatten([1, [2], 3, 4]); // [1, 2, 3, 4] +flatten([1, [2, [3, [4, 5], 6], 7], 8], 2); // [1, 2, 3, [4, 5], 6, 7, 8] + +``` + +17. indexOfAll:返回数组中某值的所有索引 + +此代码段可用于获取数组中某个值的所有索引,如果此值中未包含该值,则返回一个空数组。 + +``` js +const indexOfAll = (arr, val) => arr.reduce((acc, el, i) => (el === val ? [...acc, i] : acc), []); + +indexOfAll([1, 2, 3, 1, 2, 3], 1); // [0,3] +indexOfAll([1, 2, 3], 4); // [] + +``` + +18. intersection:两数组的交集 + +``` js + +const intersection = (a, b) => { + const s = new Set(b); + return a.filter(x => s.has(x)); +}; + +intersection([1, 2, 3], [4, 3, 2]); // [2, 3] + +``` + +19. intersectionWith:两数组都符合条件的交集 + +此片段可用于在对两个数组的每个元素执行了函数之后,返回两个数组中存在的元素列表。 + +``` js + +const intersectionBy = (a, b, fn) => { + const s = new Set(b.map(fn)); + return a.filter(x => s.has(fn(x))); +}; + +intersectionBy([2.1, 1.2], [2.3, 3.4], Math.floor); // [2.1] + +``` + +20. intersectionWith:先比较后返回交集 + +``` js +const intersectionWith = (a, b, comp) => a.filter(x => b.findIndex(y => comp(x, y)) !== -1); + +intersectionWith([1, 1.2, 1.5, 3, 0], [1.9, 3, 0, 3.9], (a, b) => Math.round(a) === Math.round(b)); // [1.5, 3, 0] + +``` + +21. minN:返回指定长度的升序数组 + +``` js +const minN = (arr, n = 1) => [...arr].sort((a, b) => a - b).slice(0, n); + +minN([1, 2, 3]); // [1] +minN([1, 2, 3], 2); // [1,2] + +``` + +22. negate:根据条件反向筛选 + +``` js + +const negate = func => (...args) => !func(...args); + +[1, 2, 3, 4, 5, 6].filter(negate(n => n % 2 === 0)); // [ 1, 3, 5 ] + +``` + +23. randomIntArrayInRange:生成两数之间指定长度的随机数组 + +``` js +const randomIntArrayInRange = (min, max, n = 1) => + Array.from({ length: n }, () => Math.floor(Math.random() * (max - min + 1)) + min); + +randomIntArrayInRange(12, 35, 10); // [ 34, 14, 27, 17, 30, 27, 20, 26, 21, 14 ] + +``` + +24. sample:在指定数组中获取随机数 + +``` js +const sample = arr => arr[Math.floor(Math.random() * arr.length)]; + +sample([3, 7, 9, 11]); // 9 + +``` + +25. sampleSize:在指定数组中获取指定长度的随机数 + +此代码段可用于从数组中获取指定长度的随机数,直至穷尽数组。 使用Fisher-Yates算法对数组中的元素进行随机选择。 + +``` js +const sampleSize = ([...arr], n = 1) => { + let m = arr.length; + while (m) { + const i = Math.floor(Math.random() * m--); + [arr[m], arr[i]] = [arr[i], arr[m]]; + } + return arr.slice(0, n); +}; + +sampleSize([1, 2, 3], 2); // [3,1] +sampleSize([1, 2, 3], 4); // [2,3,1] + +``` +26. shuffle:“洗牌” 数组 + +此代码段使用Fisher-Yates算法随机排序数组的元素。 + +``` js + +const shuffle = ([...arr]) => { + let m = arr.length; + while (m) { + const i = Math.floor(Math.random() * m--); + [arr[m], arr[i]] = [arr[i], arr[m]]; + } + return arr; +}; + +const foo = [1, 2, 3]; +shuffle(foo); // [2, 3, 1], foo = [1, 2, 3] + +``` +27. nest:根据parent_id生成树结构(阿里一面真题) + +根据每项的parent_id,生成具体树形结构的对象。 + +``` js +const nest = (items, id = null, link = 'parent_id') => + items + .filter(item => item[link] === id) + .map(item => ({ ...item, children: nest(items, item.id) })); + +``` + +用法: + +``` js + const comments = [ + { id: 1, parent_id: null }, + { id: 2, parent_id: 1 }, + { id: 3, parent_id: 1 }, + { id: 4, parent_id: 2 }, + { id: 5, parent_id: 4 } +]; +const nestedComments = nest(comments); // [{ id: 1, parent_id: null, children: [...] }] + +``` +## 函数 + +1. attempt:捕获函数运行异常 + +该代码段执行一个函数,返回结果或捕获的错误对象。 + +``` js +onst attempt = (fn, ...args) => { + try { + return fn(...args); + } catch (e) { + return e instanceof Error ? e : new Error(e); + } +}; +var elements = attempt(function(selector) { + return document.querySelectorAll(selector); +}, '>_>'); +if (elements instanceof Error) elements = []; // elements = [] + +``` + +2. defer:推迟执行 + +``` js +const defer = (fn, ...args) => setTimeout(fn, 1, ...args); + +defer(console.log, 'a'), console.log('b'); // logs 'b' then 'a' + +``` +3. runPromisesInSeries:运行多个Promises + +``` js +const runPromisesInSeries = ps => ps.reduce((p, next) => p.then(next), Promise.resolve()); +const delay = d => new Promise(r => setTimeout(r, d)); + +runPromisesInSeries([() => delay(1000), () => delay(2000)]); +//依次执行每个Promises ,总共需要3秒钟才能完成 + +``` +4. timeTaken:计算函数执行时间 + +``` js + +const timeTaken = callback => { + console.time('timeTaken'); + const r = callback(); + console.timeEnd('timeTaken'); + return r; +}; + +timeTaken(() => Math.pow(2, 10)); // 1024, (logged): timeTaken: 0.02099609375ms + +``` +5. createEventHub:简单的发布/订阅模式 + +创建一个发布/订阅(发布-订阅)事件集线,有emit,on和off方法。 + +* 使用Object.create(null)创建一个空的hub对象。 +* emit,根据event参数解析处理程序数组,然后.forEach()通过传入数据作为参数来运行每个处理程序。 +* on,为事件创建一个数组(若不存在则为空数组),然后.push()将处理程序添加到该数组。 +* off,用.findIndex()在事件数组中查找处理程序的索引,并使用.splice()删除。 + +``` js +const createEventHub = () => ({ + hub: Object.create(null), + emit(event, data) { + (this.hub[event] || []).forEach(handler => handler(data)); + }, + on(event, handler) { + if (!this.hub[event]) this.hub[event] = []; + this.hub[event].push(handler); + }, + off(event, handler) { + const i = (this.hub[event] || []).findIndex(h => h === handler); + if (i > -1) this.hub[event].splice(i, 1); + if (this.hub[event].length === 0) delete this.hub[event]; + } +}); + +``` +用法: + +``` js +const handler = data => console.log(data); +const hub = createEventHub(); +let increment = 0; + +// 订阅,监听不同事件 +hub.on('message', handler); +hub.on('message', () => console.log('Message event fired')); +hub.on('increment', () => increment++); + +// 发布:发出事件以调用所有订阅给它们的处理程序,并将数据作为参数传递给它们 +hub.emit('message', 'hello world'); // 打印 'hello world' 和 'Message event fired' +hub.emit('message', { hello: 'world' }); // 打印 对象 和 'Message event fired' +hub.emit('increment'); // increment = 1 + +// 停止订阅 +hub.off('message', handler); + +``` +6. memoize:缓存函数 + +通过实例化一个Map对象来创建一个空的缓存。 + +通过检查输入值的函数输出是否已缓存,返回存储一个参数的函数,该参数将被提供给已记忆的函数;如果没有,则存储并返回它。 + +``` js +const memoize = fn => { + const cache = new Map(); + const cached = function(val) { + return cache.has(val) ? cache.get(val) : cache.set(val, fn.call(this, val)) && cache.get(val); + }; + cached.cache = cache; + return cached; +}; + + +``` +Ps: 这个版本可能不是很清晰,还有Vue源码版的: + +``` js +/** + * Create a cached version of a pure function. + */ +export function cached (fn: F): F { + const cache = Object.create(null) + return (function cachedFn (str: string) { + const hit = cache[str] + return hit || (cache[str] = fn(str)) + }: any) +} + +``` +7. once:只调用一次的函数 + +``` js +const once = fn => { + let called = false + return function () { + if (!called) { + called = true + fn.apply(this, arguments) + } + } +}; + +``` +8. flattenObject:以键的路径扁平化对象 + +使用递归。 + +* 利用Object.keys(obj)联合Array.prototype.reduce(),以每片叶子节点转换为扁平的路径节点。 +* 如果键的值是一个对象,则函数使用调用适当的自身prefix以创建路径Object.assign()。 +* 否则,它将适当的前缀键值对添加到累加器对象。 +* prefix除非您希望每个键都有一个前缀,否则应始终省略第二个参数。 + +``` js +const flattenObject = (obj, prefix = '') => + Object.keys(obj).reduce((acc, k) => { + const pre = prefix.length ? prefix + '.' : ''; + if (typeof obj[k] === 'object') Object.assign(acc, flattenObject(obj[k], pre + k)); + else acc[pre + k] = obj[k]; + return acc; + }, {}); + +flattenObject({ a: { b: { c: 1 } }, d: 1 }); // { 'a.b.c': 1, d: 1 } + +``` +9. unflattenObject:以键的路径展开对象 + +与上面的相反,展开对象。 + +``` js +const unflattenObject = obj => + Object.keys(obj).reduce((acc, k) => { + if (k.indexOf('.') !== -1) { + const keys = k.split('.'); + Object.assign( + acc, + JSON.parse( + '{' + + keys.map((v, i) => (i !== keys.length - 1 ? `"${v}":{` : `"${v}":`)).join('') + + obj[k] + + '}'.repeat(keys.length) + ) + ); + } else acc[k] = obj[k]; + return acc; + }, {}); + +unflattenObject({ 'a.b.c': 1, d: 1 }); // { a: { b: { c: 1 } }, d: 1 } + +``` +这个的用途,在做Tree组件或复杂表单时取值非常舒服。 + + +## 字符串 + +1. byteSize:返回字符串的字节长度 + +``` js +const byteSize = str => new Blob([str]).size; + +byteSize('😀'); // 4 +byteSize('Hello World'); // 11 + +``` +2. capitalize:首字母大写 + +``` js +const capitalize = ([first, ...rest]) => + first.toUpperCase() + rest.join(''); + +capitalize('fooBar'); // 'FooBar' +capitalize('fooBar', true); // 'Foobar' + +``` +3. capitalizeEveryWord:每个单词首字母大写 +``` js +const capitalizeEveryWord = str => str.replace(/\b[a-z]/g, char => char.toUpperCase()); + +capitalizeEveryWord('hello world!'); // 'Hello World!' + +``` +4. decapitalize:首字母小写 +``` js +const decapitalize = ([first, ...rest]) => + first.toLowerCase() + rest.join('') + +decapitalize('FooBar'); // 'fooBar' +decapitalize('FooBar'); // 'fooBar' + +``` +5. luhnCheck:银行卡号码校验(luhn算法) + +Luhn算法的实现,用于验证各种标识号,例如信用卡号,IMEI号,国家提供商标识号等。 + +与String.prototype.split('')结合使用,以获取数字数组。获得最后一个数字。实施luhn算法。如果被整除,则返回,否则返回。 + +``` js +const luhnCheck = num => { + let arr = (num + '') + .split('') + .reverse() + .map(x => parseInt(x)); + let lastDigit = arr.splice(0, 1)[0]; + let sum = arr.reduce((acc, val, i) => (i % 2 !== 0 ? acc + val : acc + ((val * 2) % 9) || 9), 0); + sum += lastDigit; + return sum % 10 === 0; +}; + +``` +用例: +``` js +luhnCheck('4485275742308327'); // true +luhnCheck(6011329933655299); // false +luhnCheck(123456789); // false +``` +补充:银行卡号码的校验规则: + +银行卡号码的校验采用Luhn算法,校验过程大致如下: + +* 从右到左给卡号字符串编号,最右边第一位是1,最右边第二位是2,最右边第三位是3…. +* 从右向左遍历,对每一位字符t执行第三个步骤,并将每一位的计算结果相加得到一个数s。 +* 对每一位的计算规则:如果这一位是奇数位,则返回t本身,如果是偶数位,则先将t乘以2得到一个数n,如果n是一位数(小于10),直接返回n,否则将n的个位数和十位数相加返回。 +* 如果s能够整除10,则此号码有效,否则号码无效。 + +因为最终的结果会对10取余来判断是否能够整除10,所以又叫做模10算法。 +当然,还是库比较香: bankcardinfo + +6. splitLines:将多行字符串拆分为行数组。 + +使用String.prototype.split()和正则表达式匹配换行符并创建一个数组。 + +``` js +const splitLines = str => str.split(/\r?\n/); + +splitLines('This\nis a\nmultiline\nstring.\n'); // ['This', 'is a', 'multiline', 'string.' , ''] + +``` +7. stripHTMLTags:删除字符串中的HTMl标签 + +从字符串中删除HTML / XML标签。 + +使用正则表达式从字符串中删除HTML / XML 标记。 + +``` js +const stripHTMLTags = str => str.replace(/<[^>]*>/g, ''); + +stripHTMLTags('

    lorem ipsum

    '); // 'lorem ipsum' + +``` +## 对象 + +1. dayOfYear:当前日期天数 + +``` js +const dayOfYear = date => + Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24); + +dayOfYear(new Date()); // 285 + +``` +2. forOwn:迭代属性并执行回调 + +``` js +const forOwn = (obj, fn) => Object.keys(obj).forEach(key => fn(obj[key], key, obj)); +forOwn({ foo: 'bar', a: 1 }, v => console.log(v)); // 'bar', 1 + +``` +3. Get Time From Date:返回当前24小时制时间的字符串 + +``` js +const getColonTimeFromDate = date => date.toTimeString().slice(0, 8); + +getColonTimeFromDate(new Date()); // "08:38:00" + +``` +4. Get Days Between Dates:返回日期间的天数 +``` js +const getDaysDiffBetweenDates = (dateInitial, dateFinal) => + (dateFinal - dateInitial) / (1000 * 3600 * 24); + +getDaysDiffBetweenDates(new Date('2019-01-01'), new Date('2019-10-14')); // 286 + +``` +5. is:检查值是否为特定类型。 +``` js +const is = (type, val) => ![, null].includes(val) && val.constructor === type; + +is(Array, [1]); // true +is(ArrayBuffer, new ArrayBuffer()); // true +is(Map, new Map()); // true +is(RegExp, /./g); // true +is(Set, new Set()); // true +is(WeakMap, new WeakMap()); // true +is(WeakSet, new WeakSet()); // true +is(String, ''); // true +is(String, new String('')); // true +is(Number, 1); // true +is(Number, new Number(1)); // true +is(Boolean, true); // true +is(Boolean, new Boolean(true)); // true + +``` +6. isAfterDate:检查是否在某日期后 +``` js +const isAfterDate = (dateA, dateB) => dateA > dateB; + +isAfterDate(new Date(2010, 10, 21), new Date(2010, 10, 20)); // true + +``` +7. isBeforeDate:检查是否在某日期前 +``` js +const isBeforeDate = (dateA, dateB) => dateA < dateB; + +isBeforeDate(new Date(2010, 10, 20), new Date(2010, 10, 21)); // true + +``` +8. tomorrow:获取明天的字符串格式时间 + +``` js + +const tomorrow = () => { + let t = new Date(); + t.setDate(t.getDate() + 1); + return t.toISOString().split('T')[0]; +}; + +tomorrow(); // 2019-10-15 (如果明天是2019-10-15) + +``` +9. equals:全等判断 + +在两个变量之间进行深度比较以确定它们是否全等。 + +此代码段精简的核心在于Array.prototype.every()的使用。 + +``` js +const equals = (a, b) => { + if (a === b) return true; + if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime(); + if (!a || !b || (typeof a !== 'object' && typeof b !== 'object')) return a === b; + if (a.prototype !== b.prototype) return false; + let keys = Object.keys(a); + if (keys.length !== Object.keys(b).length) return false; + return keys.every(k => equals(a[k], b[k])); +}; + +``` +用法: + +``` js +equals({ a: [2, { e: 3 }], b: [4], c: 'foo' }, { a: [2, { e: 3 }], b: [4], c: 'foo' }); // true + +``` + +## 数字 + +1. randomIntegerInRange:生成指定范围的随机整数 + +``` js +const randomIntegerInRange = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; + +randomIntegerInRange(0, 5); // 3 + +``` +2. randomNumberInRange:生成指定范围的随机小数 +``` js +const randomNumberInRange = (min, max) => Math.random() * (max - min) + min; + +randomNumberInRange(2, 10); // 6.0211363285087005 + +``` +3. round:四舍五入到指定位数 +``` js +const round = (n, decimals = 0) => Number(`${Math.round(`${n}e${decimals}`)}e-${decimals}`); + +round(1.005, 2); // 1.01 + +``` +4. sum:计算数组或多个数字的总和 +``` js + +const sum = (...arr) => [...arr].reduce((acc, val) => acc + val, 0); + +sum(1, 2, 3, 4); // 10 +sum(...[1, 2, 3, 4]); // 10 + +``` +5. toCurrency:简单的货币单位转换 +``` js +const toCurrency = (n, curr, LanguageFormat = undefined) => + Intl.NumberFormat(LanguageFormat, { style: 'currency', currency: curr }).format(n); + +toCurrency(123456.789, 'EUR'); // €123,456.79 +toCurrency(123456.789, 'USD', 'en-us'); // $123,456.79 +toCurrency(123456.789, 'USD', 'fa'); // ۱۲۳٬۴۵۶٫۷۹ +toCurrency(322342436423.2435, 'JPY'); // ¥322,342,436,423 + +``` +## 浏览器操作及其它 + +1. bottomVisible:检查页面底部是否可见 +``` js +const bottomVisible = () => + document.documentElement.clientHeight + window.scrollY >= + (document.documentElement.scrollHeight || document.documentElement.clientHeight); + +bottomVisible(); // true + +``` +2. Create Directory:检查创建目录 + +此代码段调用fs模块的existsSync()检查目录是否存在,如果不存在,则mkdirSync()创建该目录。 + +``` js +const fs = require('fs'); +const createDirIfNotExists = dir => (!fs.existsSync(dir) ? fs.mkdirSync(dir) : undefined); +createDirIfNotExists('test'); + +``` +3. currentURL:返回当前链接url + +``` js +const currentURL = () => window.location.href; + +currentURL(); // 'https://juejin.im' + +``` +4. distance:返回两点间的距离 + +该代码段通过计算欧几里得距离来返回两点之间的距离。 +``` js +const distance = (x0, y0, x1, y1) => Math.hypot(x1 - x0, y1 - y0); + +distance(1, 1, 2, 3); // 2.23606797749979 + +``` +5. elementContains:检查是否包含子元素 +此代码段检查父元素是否包含子元素。 +``` js +const elementContains = (parent, child) => parent !== child && parent.contains(child); + +elementContains(document.querySelector('head'), document.querySelector('title')); // true +elementContains(document.querySelector('body'), document.querySelector('body')); // false + +``` +6. getStyle:返回指定元素的生效样式 +``` js +const getStyle = (el, ruleName) => getComputedStyle(el)[ruleName]; + +getStyle(document.querySelector('p'), 'font-size'); // '16px' + +``` +7. getType:返回值或变量的类型名 +``` js +const getType = v => + v === undefined ? 'undefined' : v === null ? 'null' : v.constructor.name.toLowerCase(); + +getType(new Set([1, 2, 3])); // 'set' +getType([1, 2, 3]); // 'array' + +``` +8. hasClass:校验指定元素的类名 +``` js +const hasClass = (el, className) => el.classList.contains(className); +hasClass(document.querySelector('p.special'), 'special'); // true + +``` +9. hide:隐藏所有的指定标签 +``` js +const hide = (...el) => [...el].forEach(e => (e.style.display = 'none')); + +hide(document.querySelectorAll('img')); // 隐藏所有标签 + +``` +10. httpsRedirect:HTTP 跳转 HTTPS +``` js +const httpsRedirect = () => { + if (location.protocol !== 'https:') location.replace('https://' + location.href.split('//')[1]); +}; + +httpsRedirect(); // 若在`http://www.baidu.com`, 则跳转到`https://www.baidu.com` + +``` +11. insertAfter:在指定元素之后插入新元素 +``` js +const insertAfter = (el, htmlString) => el.insertAdjacentHTML('afterend', htmlString); + +//
    ...

    after

    +insertAfter(document.getElementById('myId'), '

    after

    '); + +``` +12. insertBefore:在指定元素之前插入新元素 +``` js +const insertBefore = (el, htmlString) => el.insertAdjacentHTML('beforebegin', htmlString); + +insertBefore(document.getElementById('myId'), '

    before

    '); //

    before

    ...
    + +``` +13. isBrowser:检查是否为浏览器环境 + +此代码段可用于确定当前运行时环境是否为浏览器。这有助于避免在服务器(节点)上运行前端模块时出错。 + +``` js +const isBrowser = () => ![typeof window, typeof document].includes('undefined'); + +isBrowser(); // true (browser) +isBrowser(); // false (Node) + +``` +14. isBrowserTab:检查当前标签页是否活动 +``` js +const isBrowserTabFocused = () => !document.hidden; + +isBrowserTabFocused(); // true + +``` +15. nodeListToArray:转换nodeList为数组 +``` js +const nodeListToArray = nodeList => [...nodeList]; + +nodeListToArray(document.childNodes); // [ , html ] + +``` +16. Random Hexadecimal Color Code:随机十六进制颜色 +``` js + +const randomHexColorCode = () => { + let n = (Math.random() * 0xfffff * 1000000).toString(16); + return '#' + n.slice(0, 6); +}; + +randomHexColorCode(); // "#e34155" + +``` +17. scrollToTop:平滑滚动至顶部 +``` js +const scrollToTop = () => { + const c = document.documentElement.scrollTop || document.body.scrollTop; + if (c > 0) { + window.requestAnimationFrame(scrollToTop); + window.scrollTo(0, c - c / 8); + } +}; + +scrollToTop(); + +``` +18. smoothScroll:滚动到指定元素区域 + +该代码段可将指定元素平滑滚动到浏览器窗口的可见区域。 +``` js +const smoothScroll = element => + document.querySelector(element).scrollIntoView({ + behavior: 'smooth' + }); + +smoothScroll('#fooBar'); +smoothScroll('.fooBar'); + +``` +19. detectDeviceType:检测移动/PC设备 +``` js +const detectDeviceType = () => + /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) + ? 'Mobile' + : 'Desktop'; + +``` +20. getScrollPosition:返回当前的滚动位置 + +默认参数为window ,pageXOffset(pageYOffset)为第一选择,没有则用scrollLeft(scrollTop) + +``` js +const getScrollPosition = (el = window) => ({ + x: el.pageXOffset !== undefined ? el.pageXOffset : el.scrollLeft, + y: el.pageYOffset !== undefined ? el.pageYOffset : el.scrollTop +}); + +getScrollPosition(); // {x: 0, y: 200} + +``` +21. size:获取不同类型变量的字节长度 + +这个的实现非常巧妙,利用Blob类文件对象的特性,获取对象的长度。 + +另外,多重三元运算符,是真香。 + +``` js +const size = val => + Array.isArray(val) + ? val.length + : val && typeof val === 'object' + ? val.size || val.length || Object.keys(val).length + : typeof val === 'string' + ? new Blob([val]).size + : 0; + +size([1, 2, 3, 4, 5]); // 5 +size('size'); // 4 +size({ one: 1, two: 2, three: 3 }); // 3 + + +``` +22. escapeHTML:转义HTML + +当然是用来防XSS攻击啦。 + +``` js +const escapeHTML = str => + str.replace( + /[&<>'"]/g, + tag => + ({ + '&': '&', + '<': '<', + '>': '>', + "'": ''', + '"': '"' + }[tag] || tag) + ); + +escapeHTML('Me & you'); // '<a href="#">Me & you</a>' + +``` diff --git a/blog/zh-cn/web/react_interview.md b/blog/zh-cn/web/react_interview.md new file mode 100644 index 0000000..06991c7 --- /dev/null +++ b/blog/zh-cn/web/react_interview.md @@ -0,0 +1,624 @@ +# React 面试问题 +>如果你是一位有理想的前端开发人员,并且正在准备面试,那么这篇文章就是为你准备的。本文收集了 React 面试中最常见的 50 大问题,这是一份理想的指南,让你为 React 相关的面试做好充分的准备工作。首先我们快速了解一下 React 在市场上的需求和现状,然后再开始讨论 React 面试问题。 + +JavaScript 工具的市场地位正在缓慢而稳定地上升当中,而对 React 认证的需求正在飞速增长。选择正确的技术来开发应用程序或网站变得愈加艰难。React 被认为是 Javascript 语言中增长最快的框架。 + +虚拟 DOM 和可复用部件等独特特性吸引了前端开发人员的注意。尽管成熟的框架(如 Angular、Meteor 和 Vue 等)在 MVC(模型 - 视图 - 控制器)中只是一个“视图”库,但它们都有很强的竞争力。下图显示了常见 JS 框架的趋势: + +![react-top](./../img/react-top.png) + +以下是面试官最有可能提出的 50 个面试问题和答案。 + +## React 面试问题——常规知识 + +1. 真实 DOM 和虚拟 DOM 的区别 + + 真实 DOM|虚拟 DOM + --|:--| + 1.更新较慢| 1.更新较快 + 2.可以直接更新 HTML|2.不能直接更新 HTML + 3.元素更新时创建一个新 DOM|3.元素更新时更新 JSX + 4.DOM 操作开销较大|4.DOM 操作非常容易 + 5.内存浪费严重|5.没有内存浪费 + +2. 什么是 React? + + * React 是 2011 年由 Facebook 开发的前端 JavaScript 库。 + * 它遵循基于组件的方法,这种方法可以用来构建可复用的 UI 组件。 + * 它用于复杂的交互式 Web 端和移动端用户界面开发。 + * 尽管它在 2015 年才开源,但得到了一家巨头的支持。 + +3. React 的特点是什么? + + * 轻量级 DOM,以获得更好的性能。 + * 在 React 中,一切都被视为组件。 + * React 使用 JSX(JavaScript eXtension),使我们可以编写类似于 HTML 的 JavaScript。 + * React 不是直接运行在浏览器的文档对象模型(DOM)上,而是运行在虚拟 DOM 上。 + * ReactJS 遵循单向数据流或单向数据绑定。 + +4. 列出 React 的一些主要优势。 + + * 可以提高应用程序的性能。 + * 可以方便地用在客户端和服务端。 + * 由于有了 JSX,代码的可读性提高了。 + * 使用 React 后,编写 UI 测试用例变得非常容易。 + +5. React 有哪些局限? + + * React 只是一个库,而不是一个成熟的框架。 + * 它的库很大,需要花费一些时间来理解。 + * 新手程序员可能很难入门。 + * 由于它使用了内联模板和 JSX,编码也比较复杂。 + +6. 什么是 JSX? + +JSX 是 JavaScript XML 的简写。这是 React 使用的一种文件类型,具备 JavaScript 的表现力,并使用 HTML 作为模板语法。这样一来 HTML 文件理解起来就非常简单。这种文件可以创造稳健的应用程序并提高其效率。下面是一个 JSX 实例: + +``` jsx +render(){ + return( +
    +

    Hello World from Codersera!!

    +
    + ); + } +``` +7. 你对虚拟 DOM 有什么了解?解释其工作机制。 + +虚拟 DOM 是轻量级的 JavaScript 对象,一开始只是真实 DOM 的一个副本。它是一个节点树,将组件列为对象及其属性和内容的列表。React 的渲染功能从 React 的各个部分生成一个节点树。然后,它会根据由不同用户或系统行为引起的信息模型突变来更新此树。 +虚拟 DOM 的工作机制只有简单的三步组成。 + + 1. 每当任何基础信息更改时,整个 UI 就会以虚拟 DOM 的表示形式重新渲染。 + + ![ui-dom](./../img/ui-dom-1.png) + + 2. 然后计算先前的 DOM 表示和新的 DOM 表示之间的区别。 + + ![ui-dom](./../img/ui-dom-2.png) + + 3. 计算完成后,只有实际更改的内容才会更新到真实 DOM。 + + ![ui-dom](./../img/ui-dom-3.png) + +8. 为什么浏览器无法读取 JSX? + +React 使用 JSX(JavaScript eXtension),我们可以用它编写类似于 HTML 的 JavaScript。但由于 JSX 不是合法的 JavaScript,因此浏览器无法直接读取它。如果 JavaScript 文件包含 JSX,则必须将其转换。你需要一个转换器将 JSX 转换为浏览器可以理解的常规 Javascript。目前最常用的转换器是 Babel。 + +9. 与 ES5 相比,React 的 ES6 语法有何不同? + +ES5 和 ES6 的语法区别如下: + +```javascript +// ES5 +var React = require('react'); +// ES6 +import React from 'react'; + +******** export vs exports ********* + +// ES5 +module.exports = Component; +// ES6 +export default Component; + +****** function ***** +// ES5 + +var MyComponent = React.createClass({ + render: function() { + return

    Hello CoderSera!

    + }, +}); + +// ES6 +class MyComponent extends React.Component { + render() { + return

    Hello CoderSera!

    + } +} + + ******* props ****** + +// ES5 +var App = React.createClass({ + propTypes: { name: React.PropTypes.string }, + render: function() { + return

    Hello, { this.props.name }! < /h3> + }, +}); + +// ES6 +class App extends React.Component { + render() { + return

    Hello, { this.props.name }!

    + } +} + + ****** state ***** + +// ES5 +var App = React.createClass({ + getInitialState: function() { + return { name: 'world' }; + } + + render: function() { + return

    Hello, { this.state.name }! < /h3>; + }, +}); + +// ES6 +class App extends React.Component { + constructor() { + super(); + this.state = { name: 'world' }; + } + + render() { + return

    Hello, { this.state.name }! < /h3> + } + render() { + return; +

    Hello, { this.state.name }! < /h3> + } + +``` + +10. React 和 Angular 有何不同? + +React 对比 Angular|React|Angular +--|:--|:--| +架构|使用虚拟 DOM|使用真实 DOM +渲染|服务端渲染|客户端渲染 +DOM|使用虚拟 DOM|使用真实 DOM +数据绑定|单向数据绑定|双向数据绑定 +调试|编译时调试|运行时调试 +开发者|Facebook|谷歌 + +11. 如何理解“在 React 中,一切都是组件”。 + +React 应用程序的 UI 构建块都是组件。这些部分将整个 UI 划分为许多可自治和可复用的微小部分。然后独立的某个部分发生变化就不会影响 UI 的其余部分。 + +12. 解释 React 中 render() 的目的。 + +它被视为普通函数,但 render() 函数必须返回某些值,无论值是否为空。调用组件文件时默认会调用 render() 方法,因为组件需要显示 HTML 标记,或者我们可以说 JSX 语法。每个 React 组件必须有一个 render() 函数,它返回单个 React 元素,该元素代表原生 DOM 组件。如果需要渲染多个 HTML 元素,则必须将它们分组在一个封闭的标签内,如form、group和div等。此函数必须保持纯净,就是说它在每次调用时必须返回相同的结果。 + +``` jsx +import React, { Component } from 'react'; + +class App extends Component { + render() { + return (

    hello CoderSera

    ) + } +} + +export default App; + +``` + +13. 什么是 Hooks? + +Hooks 是一项新功能,使你无需编写类即可使用状态等 React 功能。来看一个 useState hook 示例。 + +```jsx +import {useState} from 'react';

    function Example() {
    // Declare a new +``` + +14. 什么是 props? + +* Props 用于将数据从父级传递到子级或由组件本身传递。它们是不可变的,因此不会更改。 + +* Props 是不可变的。因为它们是基于纯函数的概念开发的。在纯函数中,我们无法更改参数数据。因此在 ReactJS 中也无法更改 props 的数据。 + +15. React 中的状态是什么,如何使用? + +组件可以通过状态来跟踪其执行的任何渲染之间的信息。 + +状态用于可变数据或将要更改的数据。这对于用户输入尤其方便。以搜索栏为例,用户输入数据时他们看到的内容也会更新。 + +16. 状态和 props 的区别。 + +条件|状态|Props +--|:--|:--| +从父组件接收初始值|是|是 +父组件可以更改值|否|是 +在组件内设置默认值|是|是 +在组件内更改|是|否 +为子组件设置初始值|是|是 +在子组件内更改|否|是 + +17. 如何更新组件的状态? + +可以使用 this.setState() 更新组件的状态。 + +``` jsx +class MyComponent extends React.Component { + constructor() { + super(); + this.state = { + name: 'Maxx', + id: '101' + } + } + render() + { + setTimeout(()=>{this.setState({name:'Jaeha', id:'222'})},2000) + return ( +
    +

    Hello {this.state.name}

    +

    Your Id is {this.state.id}

    +
    + ); + } + } + ReactDOM.render( + , document.getElementById('content') +); + +``` + +18.React 中的箭头函数是什么?如何使用? + +**粗箭头**=> 用于定义匿名函数,这通常是将参数传递给回调函数的最简单方法。但是你需要在使用它时优化性能。注意:每次渲染组件时,在 render 方法中使用箭头函数都会创建一个新函数,这可能会影响性能。 + +```jsx +//General way +render() { + return( + + ); +} +//With Arrow Function +render() { + return( + this.handleOnChange(e) } /> + ); +} + +``` + +19. 有状态和无状态组件的区别。 + +有状态组件|无状态组件 +--|:--| +在内存中存储组件状态更改的信息|计算组件的内部状态 +有权更改状态|无权更改状态 +包含过去、现在和可能的未来状态更改的信息|没有包含关于状态更改的信息 +无状态组件通知它们关于状态更改的需求,然后它们将 props 传递给前者|它们从有状态组件接收 props,将其视为回调函数 + +20. React 组件的生命周期有哪些阶段? + +React 组件的生命周期分为三个不同阶段: + +* **初始化**:在初始化阶段,我们为 this.props 和 this.state 定义默认值和初始值。 + +* **挂载**:挂载是将组件插入 DOM 时发生的过程 +* **更新**:这个阶段中,每当状态更改或组件收到新的 prop 时,组件都会更新 +* **卸载**:这是从 DOM 卸载组件的阶段。 + +21. 详细解释 React 组件的生命周期技术。 + +一些最重要的生命周期方法包括: + +* componentWillMount()——在初始渲染发生之前,即在 React 将组件插入 DOM 之前立即调用一次。请务必注意,在此方法中调用 this.setState() 不会触发重新渲染。 + +* componentDidMount()——在渲染函数之后触发此方法。现在可以访问更新的 DOM,这意味着该方法是初始化其他需要访问 DOM 的 Javascript 库以及数据提取操作的最佳选择。 + +* componentWillReceiveProps()——componentWillReceiveProps() 在组件接收新 props 时调用。在调用 render() 方法之前,我们可以用这个方法对 prop 过渡做出反应。在此函数中调用 this.setState() 不会触发额外的重新渲染,我们可以通过 this.props 访问旧的 props。 + +* shouldComponentUpdate()——我们可以用它来决定下一个组件的状态是否应触发重新渲染。此方法返回一个布尔值,默认为 true。但是我们可以返回 false,并且不会调用以下方法: + +* componentWillUpdate()——当接收到新的 props 或状态时,在渲染(更新)之前立即调用此方法。我们可以用它在更新之前做准备,但是不允许使用 this.setState()。 + +* componentDidUpdate()——React 更新 DOM 后立即调用此方法。我们可以使用此方法与更新后的 DOM 交互,或执行任何渲染后操作。 + +* componentWillUnmount()——从 DOM 卸载组件之前立即调用此方法。我们可以用它执行可能需要的任何清理工作。 + +22. React 中的事件是什么? + +在 React 中,事件是对特定动作(如鼠标悬停、鼠标单击和按键等)触发的反应。处理这些事件类似于处理 DOM 元素上的事件。但是在语法上存在一些差异,例如: + +* 事件命名使用驼峰式大小写,而不是仅使用小写字母。 + +* 事件作为函数而不是字符串传递。 + +事件参数包含一组特定于事件的属性。每个事件类型都包含它自己的属性和行为,这些属性和行为只能通过它的事件处理程序访问。 + +23. 如何在 React 中创建事件? + +```jsx +class Display extends React.Component({ + show(evt) { + // code + }, + render() { + // Render the div with an onClick prop (value is a function) + return ( +
    Click Me!
    + ); + } +}); + + +``` + +24. 什么是 React 中的合成事件? + +合成事件是围绕浏览器原生事件充当跨浏览器包装器的对象。它们将不同的浏览器行为合并为一个 API。这样做是为了确保在各个浏览器的事件中显示一致的特征。 + +25. 如何理解 React 中的引用? + +Ref 是 React 引用的简写。它是一个属性,帮助存储对特定元素或组件的引用,由组件渲染的配置函数返回。它用于返回对渲染返回的特定元素或组件的引用。当我们需要 DOM 测量或向组件添加方法时,它们会派上用场。 + +``` jsx +class ReferenceDemo extends React.Component{ + display() { + const name = this.inputDemo.value; + document.getElementById('disp').innerHTML = name; + } + render() { + return( +
    + Name: this.inputDemo = input} /> + + +

    Hello !!!

    +
    + ); + } + } + +``` + +26. 列出一些应该使用引用的情况? + +以下是应使用 ref 的情况: + +* 当你需要管理焦点、选择文本或媒体播放时。 + +* 触发命令式动画。 + +* 与第三方 DOM 库集成。 + +27. 如何模块化 React 代码? + +可以使用 export 和 import 属性来模块化软件。它们有助于在不同的文档中单独编写组件。 + +```jsx +//ChildComponent.jsx +export default class ChildComponent extends React.Component { + render() { + return(
    +

    This is a child component

    +
    ) + } +} + +//ParentComponent.jsx +import ChildComponent from './childcomponent.js'; +class ParentComponent extends React.Component { + render() { + return( +
    + +
    + ); + } +} + +``` + +28. 在 React 中如何创建表单? + +React 提供了一种有状态的,响应式的方法来构建表单。与其他 DOM 元素不同,HTML 表单元素在 React 中的工作机制有所不同。例如,表单数据通常由组件而不是 DOM 处理,并且通常使用受控组件来实现。 +区别在于可以使用回调函数来处理表单事件,然后使用容器的状态存储表单数据。这使你的组件可以更好地控制表单控制元素和表单数据。 +回调函数是在发生事件(包括更改表单控制值或表单提交)时触发的。 + +``` jsx +handleSubmit(event) { + alert('A name was submitted: ' + this.state.value); + event.preventDefault(); +} + +render() { + return ( + + + +
    + + +
    + + + + ); +} + +``` + +29. 如何理解受控和非受控组件? + +受控组件|非受控组件 +--|:--| +它们不维护自己的状态|它们维护自己的状态 +数据由父组件控制|数据由 DOM 控制 +它们通过 props 获得当前值,然后通过回调通知更改|使用引用来获得它们的当前值 + +30. 高阶组件(HOC)是什么意思? + +React 中的高阶组件是一种在组件之间共享通用功能而无需重复代码的模式。 +高阶组件实际上不是组件,它是一个接受组件并返回新组件的函数。它将一个组件转换为另一个组件,并添加其他数据或功能 + +31. HOC 可以做什么? + +HOC 可用于许多任务,例如: + +* 代码复用,逻辑和引导抽象。 + +* 渲染劫持。 +* 状态抽象和控制。 +* Props 操控。 + +32. 什么是纯组件? + +React 并没有在我们的组件中编写 shouldComponent 方法,而是引入了一个带有内置 shouldComponentUpdate 实现的新组件,它是 React.PureComponent 组件。 +React.PureComponent 通过浅层 prop 和状态比较来实现它。在某些情况下,你可以使用 React.PureComponent 来提高性能。 + +33. React 中键有什么用途? + +键可帮助 React 识别哪些项目已更改、添加或删除。应该为数组内的元素提供键,以赋予元素稳定的身份。键必须是唯一的。 +当使用动态创建的组件或用户更改列表时,React 键非常有用。设置键值后,更改后的组件就能保持唯一标识。 + +34. MVC 框架的主要问题有哪些? + +以下是 MVC 框架的一些主要问题: + +* MVC 不能解决代码复杂性问题。它也不能解决代码复用或灵活性问题。 +* 它不保证解耦代码。 + +35. 你对 Flux 有什么了解? + +Flux 是 Facebook 内部与 React 搭配使用的架构。它不是框架或库。只是一种新型的体系结构,是对 React 和单向数据流概念的补充: + +Flux 的各个组成部分如下: + +* 动作(Actions)——帮助数据传递到调度器的辅助方法。 +* 调度器(Dispatcher)——接收动作并将负载广播到已注册的回调。 +* 存储(Stores)——具有已注册到调度器的回调的应用程序状态和逻辑的容器。 +* 控制器视图(Controller Views)——从组件中获取状态并通过 props 传递给子组件的 React 组件。 + +!['ui-dom-4'](./../img/ui-dom-4.png) + +36. 什么是 Redux? + +Redux 是一种状态管理工具。尽管它主要与 React 搭配使用,但也可以与其他任何 JavaScript 框架或库搭配。 + +Redux 允许你在一个称为存储(Store)的对象中管理整个应用程序状态。 + +对存储的更新将触发与存储的已更新部分连接的组件的重新渲染。当我们想要更新某些东西时,我们称之为动作(Action)。我们还创建函数来处理这些动作并返回更新的存储。这些函数称为 Reducer。 + +37. Redux 遵循的三大原则是什么? + +* 单一可信来源:整个应用程序的状态存储在单个存储区中的对象 / 状态树中。单一状态树使我们能更容易地跟踪历史更改,更方便地调试或检查应用程序。 + +* 状态是只读的:更改状态的唯一方法是触发动作。动作是描述更改的普通 JS 对象。就像状态是数据的最小表示一样,动作是数据更改的最小表示。 + +* 使用纯函数更改:为了确认动作是如何转换状态树的,你需要纯函数。纯函数是返回值仅取决于其参数值的函数。 + +38. 如何理解“单一可信源”? + +单一可信源(SSOT)是构造信息模型和相关数据模式的实践,其中每个数据元素都只能在一个地方掌握(或编辑) +Redux 使用“存储”将应用程序的整个状态存储在一个位置。因此,组件的所有状态都存储在存储中,并且存储本身会接收更新。单一状态树使我们能更容易地跟踪历史更改,更方便地调试或检查应用程序。 + +39. 列出 Redux 的组件。 + +Redux 由以下组件组成: + +* 动作——这是一个描述发生了什么的对象。 +* Reducer——确定状态如何变化的地方。 +* 存储——整个应用程序的状态 / 对象树保存在存储中。 +* 视图——仅显示存储提供的数据。 + +40. 数据在 Redux 中是如何流动的? + +!['ui-dom-5'](./../img/ui-dom-5.png) + +41. 在 Redux 中如何定义动作? + +React 中的动作必须具有 type 属性,该属性指示正在执行的 ACTION 的类型。必须将它们定义为字符串常量,你也可以为其添加更多属性。在 Redux 中使用称为“动作创建者”的函数来创建动作。以下是动作和动作创建者的示例: + +``` jsx +function addTodo(text) { + return { + type: ADD_TODO, + text + } +} + +``` + +42. 说明 Reducer 的作用。 + +Reducer 是用于指示 ACTION 反应中应用程序状态变化的简单功能。它接收先前的状态和动作,然后返回新的状态。它根据动作类型确定需要哪种更新,然后返回新值。如果没有要完成的工作,它将按原样返回先前状态。 + +43. 在 Redux 中存储的用途是什么? + +存储是一个 JavaScript 对象,可以保存应用程序的状态,并提供一些辅助方法来访问状态、调度动作并记录侦听器。应用程序的整个状态 / 对象树存储在单个存储中。因此 Redux 非常容易理解且可预测。我们可以将中间件转移到存储,以管理数据处理任务,并维护更改存储状态的各种活动的日志。通过 Reducer,所有活动都返回新的状态。 + +44. Redux 与 Flux 有何不同? + +Flux|Redux +--|:--| +存储包括状态和更改逻辑|存储和更改逻辑是分离的 +有多个存储|只有一个存储 +所有存储不互通,是平行的|带有分层 Reducer 的单个存储 +有单个调度器|没有调度器的概念 +React 组件订阅到存储|容器组件是有联系的 +状态是可变的|状态是不可变的 + +45. Redux 有哪些优势? + +Redux 的优点如下: + +* 结果的可预测性——由于总是有单一可信源,比如存储,因此当前状态与动作及应用程序的其他部分同步时不会出现混乱。 +* 可维护性——代码易于维护,具有可预测的结果和严格的结构。 +* 服务端渲染——你只需将在服务器上创建的存储传递给客户端即可。这对于初始渲染非常有用,并优化了应用程序性能,提供了更好的用户体验。 +* 开发人员工具——从动作到状态更改,开发人员可以利用这些工具实时跟踪应用程序中发生的所有事情。 +* 社区和生态系统——Redux 背后拥有巨大的社区,用起来更加便利。大批优秀的开发者为库的发展做出了贡献,并开发了很多应用程序。 +* 易于测试——Redux 的代码主要是较小的、纯净的和孤立的函数。这使代码可测试且独立。 +* 组织——Redux 精确地规定了代码的组织方式,这使得团队合作时代码更加一致,更容易理解。 + +46. 什么是 React Router? + +React Router 是建立在 React 之上的功能强大的路由库。它使 URL 与网页上显示的数据保持同步。它保持标准化的结构和行为,可用于开发单页 Web 应用程序。React Router 有一个简单的 API。React Router 提供了一种方法,**只会显示你的应用中路由匹配你的定义的那些组件**。 + +47. 为什么在 React Router v4 中使用 switch 关键字? + +在 Switch 组件内,**Route**和**Redirect**组件嵌套在内部。从 Switch 顶部的 Route/Redirect 组件开始到底部的 Route/Redirect,根据浏览器中当前的 URL 是否与 Route/Redirect 组件的 prop/ 路径匹配,将每个组件评估为 true 或 false。 +Switch 只会渲染第一个匹配的子级。当我们嵌套了下面这样的路由时真的很方便: + +```jsx + + + + + +``` + +48. 为什么我们在 React 中需要一个路由器? + +路由器用于定义多个路由,并且当用户键入特定的 URL 时,如果该 URL 与路由器内部定义的任何“路由”的路径匹配,则该用户将被重定向到该路由。因此我们需要在应用程序中添加一个路由器库,以允许创建多个路由,每个路由都为我们指向一个独特的视图。 + +从 React Router 包导入的组件有两个属性,一个是将用户引导到指定路径的 path,另一个是用于定义所述路径中内容的 component。 + +```jsx + + + + + + +``` + +49. 列出 React Router 的优点。 + +几个优点是: + +1. 就像 React 基于组件的理念一样,在 React Router v4 中 API 是“完全组件化的”。路由器可以可视化为单个根组件(),其中包含特定的子路由()。 +2. 无需手动设置历史值:在 React Router v4 中,我们要做的就是将路由包装在组件中。 +3. 包是拆分的:三个包分别用于 Web、Native 和 Core。这使我们的应用更加紧凑。它们的编码样式类似,所以很容易来回切换。 + +50. React Router 与传统路由有何不同? + + +~|传统路由|React 路由 +--|:--|:--| +参与的页面|每个视图对应一个新页面|只涉及单个 HTML 页面 +URL 更改|向服务器发送一个 HTTP 请求并接收对应的 HTML 页面|只有历史属性被更改 +体验|用户其实是在每个视图的不同页面间切换|用户以为自己正在不同的页面间切换 + +[原文链接:](https://codersera.com/blog/top-50-react-questions-you-need-to-prepare-for-the-interview-in-2019/ )https://codersera.com/blog/top-50-react-questions-you-need-to-prepare-for-the-interview-in-2019/ diff --git a/blog/zh-cn/web/vue_cp_react.md b/blog/zh-cn/web/vue_cp_react.md new file mode 100644 index 0000000..16eaf25 --- /dev/null +++ b/blog/zh-cn/web/vue_cp_react.md @@ -0,0 +1,293 @@ +# 前端框架用vue还是react?清晰对比两者差异 + +## 前言 + +近两年前端技术层出不穷,目前市面上已经有了很多供前端人员使用的开发框架,转眼19年已过大半,前端框架领域日趋成熟,实现了三足鼎立的局面,截止到10月22日,Angular,react和vue数据统计如下图所示: + +![vue-react-1](./../img/vue-react-1.png) + +最近在学习使用框架的时候,分别使用vue和react开发了两个移动端产品,对这两个框架的学习曲线有了一些感悟,这两个都是现在比较热门的js框架,它俩在使用方式上和学习复杂度上还是有很大区别的,这里简单总结下两者的差异。 +主要从以下几个方面入手方面展开: + +* 框架的诞生 +* 设计思想 +* 编写语法 +* 脚手架构建工具 +* 数据绑定 +* 虚拟DOM +* 指令 +* 性能优化 +* 原生渲染native +* ssr服务端渲染 +* 生命周期函数 +* 销毁组件 +* 状态集管理工具 + +## 诞生 + +### vue + +vue由尤雨溪开发,由独立团队维护,现在大部分的子项目都交给团队成员打理,Vue核心库依然主要由尤雨溪亲自维护。vue近几年来特别的受关注,三年前的时候angularJS霸占前端JS框架市场很长时间,接着react框架横空出世,因为它有一个特性是虚拟DOM,从性能上碾轧angularJS,这个时候,vue1.0悄悄的问世了,它的优雅,轻便也吸引了一部分用户,开始受到关注,16年中旬,VUE2.0问世,不管从性能上,还是从成本上都隐隐超过了react,火的一塌糊涂,这个时候,angular开发团队也开发了angular2.0版本,并且更名为angular,吸收了react、vue的优点,加上angular本身的特点,也吸引到很多用户,目前已经迭代到8.0了。友情提示注意下vue的诞生时间,如果正好有小伙伴在面试,被问到你是从什么时候开始接触并且使用vue的,你要是回答用了5、6年了那场面就十分尴尬了。 + +### react + +起初facebook在建设instagram(图片分享)的时候,因为牵扯到一个东西叫数据流,那为了处理数据流并且还要考虑好性能方面的问题,Facebook开始对市场上的各种前端MVC框架去进行一个研究,然而并没有看上眼的,于是Facebook觉得,还是自己开发一个才是最棒的,那么他们决定抛开很多所谓的“最佳实践”,重新思考前端界面的构建方式,他们就自己开发了一套,果然大牛创造力还是很强大的。 +React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设Instagram 的网站。做出来以后,发现这套东西很好用,就在2013年5月开源了。 + +## 设计思想 + +### vue + +vue的官网中说它是一款渐进式框架,采用自底向上增量开发的设计。这里我们需要明确一个概念,什么是渐进式框架。在声明式渲染(视图模板引擎)的基础上,我们可以通过添加组件系统(components)、客户端路由(vue-router)、大规模状态管理(vuex)来构建一个完整的框架。Vue从设计角度来讲,虽然能够涵盖所有这些内容,但是你并不需要一上手就把所有东西全用上,因为没有必要。无论从学习角度,还是实际情况,这都是可选的。声明式渲染和组建系统是Vue的核心库所包含内容,而客户端路由、状态管理、构建工具都有专门解决方案。这些解决方案相互独立,你可以在核心的基础上任意选用其他的部件,不一定要全部整合在一起。可以看到,所说的“渐进式”,其实就是Vue的使用方式,同时也体现了Vue的设计的理念。 + +### react + +react主张函数式编程,所以推崇纯组件,数据不可变,单向数据流,当然需要双向的地方也可以手动实现,比如借助 onChange 和 setState 来实现一个双向的数据流。而vue是基于可变数据的,支持双向绑定,它提供了v-model这样的指令来实现文本框的数据流双向绑定。 + +## 编写语法 + +### vue +vue推荐的做法是webpack+vue-loader的单文件组件格式,vue保留了html、css、js分离的写法,使得现有的前端开发者在开发的时候能保持原有的习惯,更接近常用的web开发方式,模板就是普通的html,数据绑定使用mustache风格,样式直接使用css。其中**style**标签还提供了一个可选的scoped属性,它会为组件内 CSS 指定作用域,用它来控制仅对当前组件有效还是全局生效。 +模板和JSX是各有利弊的东西。模板更贴近我们的HTML,可以让我们更直观地思考语义结构,更好地结合CSS的书写。 +同时vue也支持JSX语法,因为是真正的JavaScript,拥有这个语言本身的所有的能力,可以进行复杂的逻辑判断,进行选择性的返回最终要返回的DOM结构,能够实现一些在模板的语法限制下,很难做到的一些事情。 + +### react + +用过react的开发者可能知道,react是没有模板的,直接就是一个渲染函数,它中间返回的就是一个虚拟DOM树,React推荐的做法是 JSX + inline style, 也就是把HTML和CSS全都写进JavaScript了,即'all in js'。JSX实际就是一套使用XML语法,用于让我们更简单地去描述树状结构的语法糖。在react中,所有的组件的渲染功能都依靠JSX。你可以在render()中编写类似XML的语法,它最终会被编译成原生JavaScript。不仅仅是 HTML 可以用 JSX 来表达,现在的潮流也越来越多地将 CSS 也纳入到 JavaScript 中来处理。JSX是基于 JS 之上的一套额外语法,学习使用起来有一定的成本。 + +## 构建工具 + +### vue + +vue提供了CLI 脚手架,可以帮助你非常容易地构建项目。全局安装之后,我们就可以用 vue create命令创建一个新的项目,vue 的 CLI 跟其他 CLI不同之处在于,有多个可选模板,有简单的也有复杂的,可以让用户自定义选择需要安装的模块,还可以将你的选择保存成模板,便于后续使用。 +极简的配置,更快的安装,可以更快的上手。它也有一个更完整的模板,包括单元测试在内的各种内容都涵盖,但是,它的复杂度也更高,这又涉及到根据用例来选择恰当复杂度的问题。 + +### react + +React 在这方面也提供了 create-react-app,但是现在还存在一些局限性: + +* 它不允许在项目生成时进行任何配置,而 Vue CLI 运行于可升级的运行时依赖之上,该运行时可以通过插件进行扩展。 +* 它只提供一个构建单页面应用的默认选项,而 Vue 提供了各种用途的模板。 +* 它不能用用户自建的预设配置构建项目,这对企业环境下预先建立约定是特别有用的。 + +而要注意的是这些限制是故意设计的,这有它的优势。例如,如果你的项目需求非常简单,你就不需要自定义生成过程。你能把它作为一个依赖来更新。 + +## 数据绑定 +### vue +vue是实现了双向数据绑定的mvvm框架,当视图改变更新模型层,当模型层改变更新视图层。在vue中,使用了双向绑定技术,就是View的变化能实时让Model发生变化,而Model的变化也能实时更新到View。 +Vue采用数据劫持&发布-订阅模式的方式,vue在创建vm的时候,会将数据配置在实例当中,然后通过Object.defineProperty对数据进行操作,为数据动态添加了getter与setter方法,当获取数据的时候会触发对应的getter方法,当设置数据的时候会触发对应的setter方法,从而进一步触发vm的watcher方法,然后数据更改,vm则会进一步触发视图更新操作。 +### react +react是单向数据流,react中属性是不允许更改的,状态是允许更改的。react中组件不允许通过this.state这种方式直接更改组件的状态。自身设置的状态,可以通过setState来进行更改。在setState中,传入一个对象,就会将组件的状态中键值对的部分更改,还可以传入一个函数,这个回调函数必须向上面方式一样的一个对象函数可以接受prevState和props。通过调用this.setState去更新this.state,不能直接操作this.state,请把它当成不可变的。 +调用setState更新this.state,它不是马上就会生效的,它是异步的。所以不要认为调用完setState后可以立马获取到最新的值。多个顺序执行的setState不是同步的一个接着一个的执行,会加入一个异步队列,然后最后一起执行,即批处理。 +setState是异步的,导致获取dom可能拿的还是之前的内容,所以我们需要在setState第二个参数(回调函数)中获取更新后的新的内容。 + +## diff算法 +### vue +vue中diff算法实现流程 + +在内存中构建虚拟dom树 +将内存中虚拟dom树渲染成真实dom结构 +数据改变的时候,将之前的虚拟dom树结合新的数据生成新的虚拟dom树 +将此次生成好的虚拟dom树和上一次的虚拟dom树进行一次比对(diff算法进行比对),来更新只需要被替换的DOM,而不是全部重绘。在Diff算法中,只平层的比较前后两棵DOM树的节点,没有进行深度的遍历。 +会将对比出来的差异进行重新渲染 + +### react +react中diff算法实现流程 + +DOM结构发生改变-----直接卸载并重新create +DOM结构一样-----不会卸载,但是会update变化的内容 +所有同一层级的子节点.他们都可以通过key来区分-----同时遵循1.2两点 +(其实这个key的存在与否只会影响diff算法的复杂度,换言之,你不加key的情况下,diff算法就会以暴力的方式去根据一二的策略更新,但是你加了key,diff算法会引入一些另外的操作) + +React会逐个对节点进行更新,转换到目标节点。而最后插入新的节点,涉及到的DOM操作非常多。diff总共就是移动、删除、增加三个操作,而如果给每个节点唯一的标识(key),那么React优先采用移动的方式,能够找到正确的位置去插入新的节点。 +vue会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。而对于React而言,每当应用的状态被改变时,全部组件都会重新渲染,所以react中会需要shouldComponentUpdate这个生命周期函数方法来进行控制。 + +## 指令 + +指令 (Directives) 是带有 +v- 前缀的特殊特性,指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。 +### vue +vue中提供很多内部指令供我们使用,它可以让我们进行一些模板的操作,例如有时候,我们的data中的存放的数据不是个简单的数字或者字符串,而是数组Array类型,这个时候,我们要把数组的元素展示在视图上,就需要用到vue提供的 v-for 指令,来实现列表的渲染。 +### react +因为react中没有v-for指令,所以循环渲染的时候需要用到map()方法来渲染视图,并且将符合条件的元素放入一个新数组返回。 + +## 性能优化 +### vue +vue中的每个组件内部自动实现了 +shouldComponentUpdate的优化,在vue里面由于依赖追踪系统的存在,当任意数据变动的时,Vue的每一个组件都精确地知道自己是否需要重绘,所以并不需要手动优化。用vue渲染这些组件的时候,数据变了,对应的组件基本上去除了手动优化的必要性。而在react中我们需要手动去优化其性能,但是当数据特别多的时候vue中的watcher也会特别多,从而造成页面卡顿,所以一般数据比较多的大型项目会倾向于使用react。在react官网中,官方也建议我们使用React来构建快速响应的大型 Web 应用程序。 +### react +当props或state发生改变的时候会触发 +shouldComponentUpdate生命周期函数,它是用来控制组件是否被重新渲染的,如果它返回true,则执行render函数,更新组件;如果它返回false,则不会触发重新渲染的过程。 +有的时候我们希望它在更新之前,和之前的状态进行一个对比,这个时候我们就需要重写 +shouldComponentUpdate来避免不必要的dom操作,对比当前的props或state和更新之后的nextProps或nextState,返回true时 ,组件更新;返回false,则不会更新,节省性能。 +``` js +shouldComponentUpdate(nextProps, nextState) { + if (this.props.a !== nextProps.a) { + return true; + } + if (this.state.b !== nextState.b) { + return true; + } + return false; +} +``` + +我们也可以创建一个继承React.PureComponent的React组件,它自带 +shouldComponentUpdate,可以对props进行浅比较,发现更新之后的props与当前的props一样,就不会进行render了。 +classTestextendsReact.PureComponent{constructor(props){super(props);}render(){return
    hello...{this.props.a}
    }} +由于React.PureComponent进行的是浅比较,也就是说它只会对比原对象的值是否相同,当我们的props或state为数组或者对象这种引用类型的时候,我们修改它的数值,由于数据引用指针没有发生改变,所以组件也是不会重新渲染的。这个时候我们就需要进行深拷贝,创建一个新的对象或数组,将原对象的各项属性的"值"(数组的所有元素)拷贝过来,是"值"而不仅仅是"引用地址"。我们可以使用slice()方法: +``` js +ew_state.todos = new_state.todos.slice(); +``` +或者引入immutable库来实现数据不可变。 + +## 原生渲染native +native指的是使用原生API来开发App,比如ios使用OC语言,android使用java。 +### vue +vue和Weex进行官方合作,weex是阿里巴巴发起的跨平台用户界面开发框架,它的思想是多个平台,只写一套代码,weex允许你使用 vue 语法开发不仅仅可以运行在浏览器端,还能被用于开发 iOS 和 Android 上的原生应用的组件。即只需要编写一份代码,即可运行在Web、iOS、Android上。 +weex相对来说上手比较简单,安装vue-cli之后就可以使用,学习门槛低,但是它的社区目前还处于成长期,react native的社区非常成熟活跃,有非常丰富的组件可供扩展。 +### react +react native是Facebook在2015年3月在F8开发者大会上开源的跨平台UI框架,需针对iOS、Android不同编写2份代码,使用react native需要按照文档安装配置很多依赖的工具,相对比较麻烦。weex的思想是多个平台,只写一套代码,而react-native的思想是多个平台可以写多套代码,但其使用的是同一套语言框架。 +weex的目标在于抹平各个平台的差异性,从而简化应用开发。而react-native承认了各个平台之间的差异,退而求其次,在语言和框架层面对平台进行抽象,从方法论的角度去解决多平台开发的问题。 + +## ssr服务端渲染 +服务端渲染核心在于方便seo优化,后端先调用数据库,获得数据之后,将数据和页面元素进行拼装,组合成完整的html页面,再直接返回给浏览器,以便用户浏览。 +### vue +2016 年 10 月 25 日,zeit.co背后的团队对外发布了 Next.js,一个 React 的服务端渲染应用框架。几小时后,与 Next.js 异曲同工,一个基于 Vue.js 的服务端渲染应用框架应运而生,我们称之为:Nuxt.js。 +服务端渲染支持流式渲染,因为HTTP请求也是流式,Vue 的服务端渲染结果可以直接 pipe 到返回的请求里面。这样一来,就可以更早地在浏览器中呈现给用户内容,通过合理的缓存策略,可以有效地提升服务端渲染的性能。 + +* 基于 Vue.js +* 自动代码分层 +* 服务端渲染 +* 强大的路由功能,支持异步数据 +* 静态文件服务 +* ES2015+ 语法支持 +* 打包和压缩 JS 和 CSS +* HTML 头部标签管理 +* 本地开发支持热加载 +* 集成 ESLint +* 支持各种样式预处理器: SASS、LESS、 Stylus 等等 +* 支持 HTTP/2 推送 + +### react +Next是一个React框架,允许使用React构建SSR和静态web应用 + +* 服务器渲染,获取数据非常简单 +* 无需学习新框架,支持静态导出。 +* 支持CSS-in-JS库 +* 自动代码拆分,加快页面加载速度,不加载不必要的代码 +* 基于Webpack的开发环境,支持模块热更新(HMR) +* 支持Babel和Webpack自定义配置服务器、路由和next插件。 +* 能够部署在任何能运行node的平台 +* 内置页面搜索引擎优化(SEO)处理 +* 在生产环境下,打包文件体积更小,运行速度更快 + +## 生命周期 +### vue +【初始化阶段(4个)】 +(1)beforeCreate +此钩子函数不能获取到数据,dom元素也没有渲染出来,此钩子函数不会用来做什么事情。 +(2)created +此钩子函数,数据已经挂载了,但是dom节点还是没有渲染出来,在这个钩子函数里面,如果同步更改数据的话,不会影响运行中钩子函数的执行。可以用来发送ajax请求,也可以做一些初始化事件的相关操作。 +(3)beforeMount +代表dom节点马上要被渲染出来了,但是还没有真正的渲染出来,此钩子函数跟created钩子函数基本一样,也可以做一些初始化数据的配置。 +(4)mounted +是生命周期初始化阶段的最后一个钩子函数,数据已经挂载完毕了,真实dom也可以获取到了。 +【运行中阶段(2个)】 +(5)beforeUpdate +运行中钩子函数beforeUpdate默认是不会执行的,当数据更改的时候,才会执行。数据更新的时候,先调用beforeUpdate,然后数据更新引发视图渲染完成之后,再会执行updated。运行时beforeUpdate这个钩子函数获取的数据还是更新之前的数据(获取的是更新前的dom内容),在这个钩子函数里面,千万不能对数据进行更改,会造成死循环。 +(6)updated +这个钩子函数获取的数据是更新后的数据,生成新的虚拟dom,跟上一次的虚拟dom结构进行比较,比较出来差异(diff算法)后再渲染真实dom,当数据引发dom重新渲染的时候,在updated钩子函数里面就可以获取最新的真实dom了。 +【销毁阶段(2个)】 +(7)beforeDestroy +切换路由的时候,组件就会被销毁了,销毁之前执行beforeDestroy。在这个钩子函数里面,我们可以做一些善后的操作,例如可以清空一下全局的定时器(created钩子函数绑定的初始化阶段的事件)、清除事件绑定。 +(8)destoryed +组件销毁后执行destroyed,销毁后组件的双向数据绑定、事件监听watcher相关的都被移除掉了,但是组件的真实dom结构还是存在在页面中的。 +添加keep-alive标签后会增加active和deactive这两个生命周期函数,初始化操作放在actived里面,一旦切换组件,因为组件没有被销毁,所以它不会执行销毁阶段的钩子函数,所以移除操作需要放在deactived里面,在里面进行一些善后操作,这个时候created钩子函数只会执行一次,销毁的钩子函数一直没有执行。 + +![vue-react-2](./../img/vue-react-2.png) + +### react +【初始化阶段(5个)】: +(1)getDefaultProps:实例化组件之后,组件的getDefaultProps钩子函数会执行 +这个钩子函数的目的是为组件的实例挂载默认的属性 +这个钩子函数只会执行一次,也就是说,只在第一次实例化的时候执行,创建出所有实例共享的默认属性,后面再实例化的时候,不会执行getDefaultProps,直接使用已有的共享的默认属性 +理论上来说,写成函数返回对象的方式,是为了防止实例共享,但是react专门为了让实例共享,只能让这个函数只执行一次 +组件间共享默认属性会减少内存空间的浪费,而且也不需要担心某一个实例更改属性后其他的实例也会更改的问题,因为组件不能自己更改属性,而且默认属性的优先级低。 +(2)getInitialState:为实例挂载初始状态,且每次实例化都会执行,也就是说,每一个组件实例都拥有自己独立的状态。 +(3)componentWillMount:执行componentWillMount,相当于Vue里的created+beforeMount,这里是在渲染之前最后一次更改数据的机会,在这里更改的话是不会触发render的重新执行。 +(4)render:渲染dom +render()方法必须是一个纯函数,他不应该改变 +state,也不能直接和浏览器进行交互,应该将事件放在其他生命周期函数中。 如果 +shouldComponentUpdate()返回 +false, +render()不会被调用。 +(5)componentDidMount:相当于Vue里的mounted,多用于操作真实dom +【运行中阶段(5个)】 +当组件mount到页面中之后,就进入了运行中阶段,在这里有5个钩子函数,但是这5个函数只有在数据(属性、状态)发送改变的时候才会执行 +(1)componentWillReceiveProps(nextProps,nextState) +当父组件给子组件传入的属性改变的时候,子组件的这个函数才会执行。初始化props时候不会主动执行 +当执行的时候,函数接收的参数是子组件接收到的新参数,这个时候,新参数还没有同步到this.props上,多用于判断新属性和原有属性的变化后更改组件的状态。 +(2)接下来就会执行shouldComponentUpdate(nextProps,nextState),这个函数的作用:当属性或状态发生改变后控制组件是否要更新,提高性能,返回true就更新,否则不更新,默认返回true。 +接收nextProp、nextState,根据根据新属性状态和原属性状态作出对比、判断后控制是否更新 +如果 +shouldComponentUpdate()返回 +false, +componentWillUpdate, +render和 +componentDidUpdate不会被调用。 +(3)componentWillUpdate,在这里,组件马上就要重新render了,多做一些准备工作,千万千万,不要在这里修改状态,否则会死循环 相当于Vue中的beforeUpdate +(4)render,重新渲染dom +(5)componentDidUpdate,在这里,新的dom结构已经诞生了,相当于Vue里的updated +【销毁阶段】 +当组件被销毁之前的一刹那,会触发componentWillUnmount,临死前的挣扎 +相当于Vue里的beforeDestroy,所以说一般会做一些善后的事情,例如使定时器无效,取消网络请求或清理在 +componentDidMount中创建的任何监听。 + +![vue-react-3](./../img/vue-react-3.png) + +## 销毁组件 + +### vue +vue在调用$destroy方法的时候就会执行beforeDestroy生命周期函数,然后组件被销毁,这个时候组件的dom结构还存在于页面结构中,也就说如果想要对残留的dom结构进行处理必须在destroyed生命周期函数中处理。 +### react +react执行完componentWillUnmount之后把事件、数据、dom都全部处理掉了,也就是说当父组件从渲染这个子组件变成不渲染这个子组件的时候,子组件相当于被销毁,所以根本不需要其他的钩子函数了。react销毁组件的时候,会将组件的dom结构也移除,vue则不然,在调用destory方法销毁组件的时候,组件的dom结构还是存在于页面中的,this.$destory组件结构还是存在的,只是移除了事件监听,所以这就是为什么vue中有destroyed,而react却没有componentDidUnmount。 + +## 状态集管理工具 +### vue +vuex是一个专门为vue构建的状态集管理工具,vue和react都是基于组件化开发的,项目中包含很多的组件,组件都会有组件嵌套,想让组件中的数据被其他组件也可以访问到就需要使用到Vuex。 +vuex的流程 + +将需要共享的状态挂载到state上:this.$store.state来调用 + +创建store,将状态挂载到state上,在根实例里面配置store,之后我们在组件中就可以通过this.$store.state来使用state中管理的数据,但是这样使用时,当state的数据更改的时候,vue组件并不会重新渲染,所以我们要通过计算属性computed来使用,但是当我们使用多个数据的时候这种写法比较麻烦,vuex提供了mapState辅助函数,帮助我们在组件中获取并使用vuex的store中保存的状态。 + +我们通过getters来创建状态:通过this.$store.getters来调用 + +可以根据某一个状态派生出一个新状态,vuex也提供了mapGetters辅助函数来帮助我们在组件中使用getters里的状态。 + +使用mutations来更改state:通过this.$store.commit来调用 + +我们不能直接在组件中更改state,而是需要使用mutations来更改,mutations也是一个纯对象,里面包含很多更改state的方法,这些方法的形参接收到state,在函数体里更改,这时,组件用到的数据也会更改,实现响应式。vuex提供了mapMutations方法来帮助我们在组件中调用mutations 的方法。 + +使用actions来处理异步操作:this.$store.dispatch来调用 + +Actions类似于mutations,不同在于:Actions提交的是mutations,而不是直接变更状态。Actions可以包含任意异步操作。也就是说,如果有这样的需求:在一个异步操作处理之后,更改状态,我们在组件中应该先调用actions,来进行异步动作,然后由actions调用mutations来更改数据。在组件中通过this.$store.dispatch方法调用actions的方法,当然也可以使用mapMutations来辅助使用。 +### react +2015年Redux出现,将 Flux 与函数式编程结合一起,很短时间内就成为了最热门的前端架构。它的出现主要是为解决react中组件之间的通信问题。建议把数据放入到redux中管理,目的就是方便数据统一,好管理。项目一旦出现问题,可以直接定位问题点。组件扩展的时候,后续涉及到传递的问题。本来的话,组件使用自己的数据,但是后来公用组件,还需要考虑如何值传递,在redux中可以存储至少5G以上的数据。 +redux的流程 + +![vue-react-4](./../img/vue-react-4.png) + +1. 创建store: 从redux工具中取出createStore去生成一个store。 +2. 创建一个reducer,然后将其传入到createStore中辅助store的创建。 reducer是一个纯函数,接收当前状态和action,返回一个状态,返回什么,store的状态就3是什么,需要注意的是,不能直接操作当前状态,而是需要返回一个新的状态。 想要给store创建默认状态其实就是给reducer一个参数创建默认值。 +4. 组件通过调用store.getState方法来使用store中的state,挂载在了自己的状态上。 +5. 组件产生用户操作,调用actionCreator的方法创建一个action,利用store.dispatch方法传递给reducer +6. reducer对action上的标示性信息做出判断后对新状态进行处理,然后返回新状态,这个时候store的数据就会发生改变, reducer返回什么状态,store.getState就可以获取什么状态。 +7. 我们可以在组件中,利用store.subscribe方法去订阅数据的变化,也就是可以传入一个函数,当数据变化的时候,传入的函数会执行,在这个函数中让组件去获取最新的状态。 + +## 小结 + +vue和react的核心都是专注于轻量级的视图层,虽然只是解决一个很小的问题,但是它们庞大的生态圈提供了丰富的配套工具,一开始它并不会给你提供全套的配置方案,将所有的功能都一次性给你打包好,它只会给你提供一些简单的核心功能,当你需要做一个更复杂的应用时,再增添相应的工具。例如做一个单页应用的时候才需要用路由;做一个相当庞大的应用,涉及到多组件状态共享以及多个开发者共同协作时,才可能需要大规模状态管理方案。 +框架的存在就是为了帮助我们应对不同的项目复杂度,当我们面对一个大型、复杂的开发项目时,使用太简陋的工具会极大的降低开发人员的生产力,影响工作效率,框架的诞生就是在这些工程中提取一些重复的并且已经受过验证的模式,抽象到一个已经帮你设计好的API封装当中,帮助我们去应对不同复杂度的问题。所以在开发的过程中,选择一个合适的框架就会事半功倍。但是,框架本身也有复杂度,有些框架会让人一时不知如何上手。当你接到一个并不复杂的需求,却使用了很复杂的框架,那么就相当于杀鸡用牛刀,会遇到工具复杂度所带来的副作用,不仅会失去工具本身所带来优势,还会增加各种问题,例如学习成本、上手成本,以及实际开发效率等。 +所以并不是说做得少的框架就不如做的做的框架,每个框架都有各自的优势和劣势,并不能找到完全符合需求的框架,最重要的适合当前项目,目前两大框架的生态圈一片繁荣,react社区是当前最活跃的,最快的时候三天更新一个版本,一个问题可能存在几十种不同的解决方案,这就需要我们前端人员去在不同的功能之间做取舍,以后前端框架的发展方向应该是小而精、灵活以及开放的,核心功能+生态附加库可以帮我们更加灵活的构建项目,为了跟上前进的脚步,就需要不停的吸收最新的内容,这也是从事前端开发领域的一大乐趣,希望大家都能在学习中获得长足的进步。