美文网首页
Dart 学习笔记

Dart 学习笔记

作者: aJIEw | 来源:发表于2021-04-05 21:54 被阅读0次
    dart-logo

    阅读 language-tour 时做的一些笔记。

    Important concepts

    • Dart 中一切皆对象,除了 null 之外,所有对象都继承自 Object
    • Dart 是强类型语言,但是类型标注是可选的,因为 Dart 中存在类型推断的机制,使用 var 关键字后编译器可以推断出具体的类型
    • 当开启了空安全(dart ≧ v2.12)之后,对于可为空的值必须明确指出(用 ?),可为空的值转换成不为空的值时使用 !
    • 当需要接收任意对象时使用 Object?
    • Dart 支持泛型,比如 List<int>
    • Dart 支持顶层函数和局部函数,同样也支持顶层变量
    • 标识符可以以字母或者 _ 开头,Dart 中 _ 开头的方法表示私有方法
    • Dart 中也分为表达式 expressions 和声明 statements,表达式有返回值而声明无返回值
    • Dart 工具提供了两种类型的问题:警告和错误。警告表示代码可能无法正确执行,错误分为编译期错误和运行时错误,编译期错误会导致程序无法执行,而运行时错误是代码执行过程中抛出的异常。

    Variables

    var name = 'Bob';
    

    变量保存了引用,上面的例子中,name 变量包含了一个引用,该引用指向了一个值为 Bob 的 String 对象。

    当使用 var 关键字时,变量的类型可以自动被推断出来,当然你也可以显式指出对象的类型。除此之外,如果你不想限定变量的类型,则可以使用 Obejct 或者 dynamic

    默认值

    如果没有开启空安全 (dart ≧ v2.12),则所有未被初始化的变量都会被初始化为 null,哪怕是数字型的变量,因为 Dart 中一切都是对象。但是,如果开启了空安全,则所有不可为空的变量会被要求先初始化才能使用。

    Late 变量

    Dart 2.12 之后添加了 late 修饰符,主要有两种用途:

    • 声明不可为空的顶层变量或者成员变量而不直接初始化值
    • 延迟初始化变量(使用到时才初始化)

    第一种情况很好理解,因为如果开启了空安全,则顶层变量和成员变量必须要在声明的同时进行初始化,否则编译期无法保证空安全,所以引进 late 之后表明我们不想立马初始化该变量,但是它会在稍后被初始化。

    第二种情况则适用于,一个变量对于程序来说是非必须的,或者性能消耗比较大,则可以用 late 修饰,这样,只有在使用到该变量时程序才会初始化它。比如:

    // 如果该变量未被使用,则 _readThermometer() 不会被调用
    late String temperature = _readThermometer(); // 延迟初始化
    

    final 和 const

    final 修饰的变量只能被赋值一次,并且 final 修饰的顶层变量会在第一次被使用时初始化。

    final name = 'Bob'; // final 修饰的变量可以没有类型标注
    final String nickname = 'Bobby';
    
    name = 'Alice'; // Error: a final variable can only be set once.
    

    const 变量是编译期常量(同时也是 final 的)。如果 const 修饰的是成员变量,则需要用 static const

    const bar = 1000000; // Unit of pressure (dynes/cm2)
    const double atm = 1.01325 * bar; // Standard atmosphere
    

    finalconst 的不同之处在于,final 修饰的对象不能被修改,但是其属性可以被修改;而 const 修饰的对象和属性都不能被修改,它们是不可变的

    Build-in types

    Numbers

    • int, 不大于 64 位
    • double, 64 位浮点数

    int 和 double 都是 num 的子类,num 类型的数据包含了基本的操作符,比如加减乘以及 abs(), ceil(), floor(), 位运算等。

    Strings

    • Dart 中使用 String 可以用单引号或者双引号。不过,在单引号中的 String 对象中需要对 ' 进行转义。
    • 我们可以使用 ${expression} 在 String 中引用变量或者表达式。
    • 我们可以用 == 比较两个 String 的值是否一致。
    • 我们可以使用三个单引号或者双引号来创建多行 String。
    • 我们可以在 String 值之前加 r 来表示原始类型的 String。

    Booleans

    Dart 中使用 bool 表示布尔类型的值。

    Lists

    Dart 中数组是用 List 表示的。可以用如下的方式创建数组:

    var list = [1, 2, 3];
    var list = [
      'Car',
      'Boat',
      'Plane', // Dart 中允许添加 trailing comma,这样可以防止复制粘贴出错
    ];
    

    Dart 2.3 之后添加了扩展运算符 (...) 和空值敏感的扩展运算符 (...?):

    var list = [1, 2, 3];
    var list2 = [0, ...list];
    assert(list2.length == 4);
    

    Dart 中还有提供了集合 if集合 for 的操作:

    var nav = [
      'Home',
      'Mall',
      'Mine',
      if (promoActive) 'Outlet'
    ];
    
    var listOfInts = [1, 2, 3];
    var listOfStrings = [
      '#0',
      for (var i in listOfInts) '#$i'
    ];
    

    关于其它常见的 API 见 collections

    Sets

    Dart 中同样用 Set 类型表示无序且唯一的集合。

    var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};
    

    Set 和 List 一样,支持扩展运算符、集合 if 和集合 for 的操作。

    Maps

    Map 是包含了 key 和 value 集合,其中 key 必须唯一。Dar 中的 Map 同样使用花括号 {} 表示,而且优先级比集合 Set 更高:

    var gifts = {
      // Key:    Value
      'first': 'partridge',
      'second': 'turtledoves',
      'fifth': 'golden rings'
    }; // 自动推断出 map 的类型为 Map<String, String>
    
    // 也可以通过这种方式创建
    var gifts = Map<String, String>();
    gifts['first'] = 'partridge';
    gifts['second'] = 'turtledoves';
    gifts['fifth'] = 'golden rings';
    
    var names = {}; // 这种方式创建的是 map 而不是 set
    
    // 访问 map 的语法 [],其实类似调用方法,具体获取到的值是不确定的
    // 报错:Const variables must be initialized with a constant value
    const lightRed = Colors.red[200];
    

    Runes and grapheme clusters

    Dart 中,使用 Runes 表示字符串的 Unicode 字符集。我们可以使用 characters 包下的类来操作字符。

    import 'package:characters/characters.dart';
    ...
    var hi = 'Hi ✡️';
    print(hi);
    print('The end of the string: ${hi.substring(hi.length - 1)}');
    print('The last character: ${hi.characters.last}\n'); // 使用 characters 才能正常打印 emoji
    

    Dart 中的字符串使用 UTF-16 编码,如果要用字符串表示 Unicode 字符则需要使用 \uXXXX 的语法。

    print('\u{1f606}'); // 😆
    

    Symbols

    Dart 中使用 Symbol 对象表示操作符或者标识符,用 # + 修饰符表示。

    Functions

    Dart 中一切都是对象,函数的类型用 Function 表示,而且函数也可以作为变量或者作为方法的参数进行传递。

    void say(String content) {
      print(content);
    }
    
    // Dart 中只有单个表达式的函数可以使用缩写,用 => 代替 { }
    bool isEvenNumber(int num) => num > 0 && num % 2 == 0;
    

    参数

    Dart 中的函数除了可以有普通的参数,还可以是具名参数或者可选位置参数。如果使用了 Sound null safety (sdk ≧ 2.12),且没有指明默认值,则这两种类型参数都必须是可为空的,即参数后跟 ?

    具名参数

    具名参数 (Named parameters) 是可选的参数。

    // 定义函数
    void enableFlags({bool? bold, int? index}) {}
    
    // 也可以给具名函数的参数添加默认值
    void enableFlags({bool? bold = true}) {}
    
    // 用 required 标记具名参数中必须有的参数
    void enableFlags({required bool? bold}) {}
    
    // 调用函数,无参数
    enableFlags()
    
    // 调用时如果有参数名则必须写参数名称
    enableFlags(
      bold: true,
      hidden: false
    );
    
    可选位置参数

    可选位置参数 (Optional positional parameters) 和普通的参数唯一的不同之处是它是可选的😅。用 [] 圈起来的参数即可选位置参数。比如:

    // title 是必须的位置参数,而 subtitle 是可选的
    void say(String title, [String? subtitle]) {
      print(title);
      if (subtitle != null) print(subtitle);
    }
    
    // 可选位置参数也可以有默认值
    void sing(String song, [String? instrument = 'Piano']) { }
    

    main() 函数

    每个应用都有一个 main() 函数作为应用的入口。main() 函数通常返回空,并且可以有数组作为参数。

    void main(List<String> arguments) {
      print(arguments);
    }
    

    匿名函数

    Dart 中的匿名函数和其它语言中类似,可以有多个参数或者没有参数,后跟方法体,形式如下:

    ([[Type] param1[, …]]) {
      codeBlock;
    };
    

    如果只有单个的表达式或者只有返回值,可以使用箭头表达式:

    ([[Type] param1[, …]]) => expression; // 如果是赋值表达式,则该值就是返回值
    

    Lexical scope

    Dart 同样是具有词法作用域或静态作用域的语言,即只要还在代码作用域内的值,都能访问到。

    bool topLevel = true;
    
    void main() {
      var insideMain = true;
    
      void myFunction() {
        var insideFunction = true;
    
        void nestedFunction() {
          var insideNestedFunction = true;
    
          assert(topLevel);
          assert(insideMain);
          assert(insideFunction);
          assert(insideNestedFunction);
        }
      }
    }
    

    Lexical closures

    词法闭包也叫函数闭包,和 JS 中的闭包概念类似,即定义在函数中的函数,同时使得该函数能够访问其它函数作用域中的变量。

    Function makeAdder(int addBy) {
      return (int i) => addBy + i;
    }
    
    void main() {
      var add2 = makeAdder(2);
      print(add2(3)); // 这里 add2 访问了 makeAdder 中定义的值,也即把 2 保存了下来
      print(makeAdder(10)(3));
    }
    

    Operators

    Dart 中支持的操作符表,见 Operators

    Cascade notation

    // cascade notation,使用级联表达式使得属性赋值变得更简单
    var paint = Paint()
      ..color = 'Black'
      ..strokeCap = 'Round'
      ..strokeWidth = 5.0;
    print(paint);
    

    Control flow statements

    switch and case

    // Dart 中的 case 语法有 fall through 机制
    // 但是如果定义了 case 内容而没有 break 则会报错
    var command = 'CLOSED';
    switch (command) {
      case 'EARLY_CLOSE': // fall through to 'CLOSED'
      case 'CLOSED':
        var youCantAccess = 1; // Dart 中的 case 自带作用域
        print('bye bye');
        continue afterClosed; // 可以使用 label 绕过无 break 的限制
    
      afterClosed:
      case 'CLOSED_AFTER':
        // print(youCantAccess); // undefined
    
        print('see you tomorrow');
        break;
    }
    

    Exceptions

    Dart 中所有异常都是 unchecked exceptions,意味着 Dart 不会强制你去捕捉异常。

    Catch

    Dart 中的 try catch 语法:

    try {
      breedMoreLlamas();
    } on OutOfLlamasException {
      // 特定类型的异常
      buyMoreLlamas();
    } on Exception catch (e, s) { // 第二个参数为错误栈
      // 其它类型的异常
      print('Unknown exception: $e');
    } catch (e) {
      // 所有其它未指定的错误
      print('Something really unknown: $e');
    }
    

    Classes

    Dart 中一切皆对象,除了 null 之外,所有对象都是某个类的实例。除了可以继承类之外,Dart 还提供了一种基于 mixin 的继承,也就是说可以通过 with 关键字继承某个没有构造器的类来扩展功能,还可以使用扩展函数

    Using class members

    所有对象都由成员变量和方法构成,使用 . 的语法访问对象的属性或者方法,使用 ?. 访问可为空对象的属性和方法。

    Using constructors

    Dart 中创建构造器除了可以使用类名,也可以使用 类名.构造器名() 的形式。

    class DummyClass {
      late int i;
      
      // 默认构造函数
      DummyClass(this.i);
      
      // 我们可以为构造函数定义名称,这种构造器被称为「具名构造器」
      DummyClass.create(int i, String message) {
        print(message);
        this.i = i;
      }
      
      // 在「具名构造器」后为成员变量赋值
      DummyClass.ten() : i = 10 { // :后的部分称为 initializer list
        print('Ten ${this.i}'); // print: Ten 10
      }
    }
    

    Getting an object’s type

    我们可以使用 runtimeType 获得对象的类型 Type

    var dummy = DummyClass.create(1);
    print('The type of a is ${dummy.runtimeType}');
    

    Instance variables

    • 所有未初始化可为空的实例变量都会被初始化为 null。
    • 所有实例变量都会默认自带 getter 函数,非 final 的实例变量和不带初始化器的 late final 实例变量会自动生成 setter 函数。
    • 非延迟初始化的实例变量的值会在对象创建之后,构造器和初始化器列表执行之前,就被赋值。可为空的赋值为 null,不可为空的赋值为初始值。
    • 实例变量可以是 final 的,但是必须在构造器或者初始化列表中进行赋值。

    Constructors

    Dart 中类的构造器和其它语言类似:

    class Point {
      double x = 0;
      double y = 0;
    
      Point(double x, double y) {
        this.x = x;
        this.y = y;
      }
    }
    

    不过,Dart 提供了一种语法糖的写法:

    class Point {
      double x = 0;
      double y = 0;
    
      Point(this.x, this.y);
    }
    
    具名构造器

    我们可以通过具名构造器 (Named constructors) 为一个类实现多个构造器。不过具名构造器不能被继承,如果想在子类中使用和父类相同的构造器,只能为子类单独实现。

    class Weather {
      var humidity = '';
    
      Weather.display(this.humidity);
    }
    
    class Shower extends Weather{
      Shower.display(String humidity) : super.display(humidity);
    }
    
    构造器初始化列表

    除了调用父类构造器之外,我们还可以在构造器方法体执行之前初始化成员变量,称为 Initializer list

    Point.fromJson(Map<String, double> json)
        : x = json['x']!,
          y = json['y']! {
      print('In Point.fromJson(): ($x, $y)');
    }
    
    构造器初始化顺序

    默认情况下,子类会先调用父类中的默认构造函数(未定义的话会生成一个无参构造器),如果存在构造器初始化列表 (Initializer list) 则会先调用它们。所以,当使用默认构造函数创建对象时,构造器的初始化顺序为:

    1. 构造器初始化列表
    2. 父类无参构造器
    3. 子类无参构造器

    另外,如果父类未定义默认构造器,则子类实现构造器时必须先调用父类的某个具名构造器。

    重定向构造器
    class Point {
      double x, y;
    
      Point(this.x, this.y);
    
      // 使用 this 关键字重定向到默认构造器
      Point.alongXAxis(double x) : this(x, 0);
    }
    
    常量构造器

    当你想要创建的对象是编译期常量时,可以使用 const 修饰构造器(比如用于创建注解):

    class ImmutablePoint {
      static const ImmutablePoint origin = ImmutablePoint(0, 0);
    
      final double x, y; // 记得所有的变量都要是 final 的
    
      const ImmutablePoint(this.x, this.y);
    }
    

    使用常量构造器 (Constant Constructors) 初始化对象时,使用相同的值的对象只会在第一次创建时被初始化,也就是说相同值的对象只会初始化一次

    工厂构造器

    使用工厂模式创建对象。

    class Logger {
      final String name;
      bool mute = false;
    
      static final Map<String, Logger> _cache = <String, Logger>{};
    
      factory Logger(String name) {
        return _cache.putIfAbsent(name, () => Logger._internal(name));
      }
    
      factory Logger.fromJson(Map<String, Object> json) {
        return Logger(json['name'].toString());
      }
    
      Logger._internal(this.name);
    
      void log(String msg) {
        if (!mute) print(msg);
      }
    }
    

    Methods

    Getters and setters
    class Rectangle {
      double left, top, width, height;
    
      Rectangle(this.left, this.top, this.width, this.height);
    
      // Define two calculated properties: right and bottom.
      double get right => left + width;
      set right(double value) => left = value - width;
      
      double get bottom => top + height;
      set bottom(double value) => top = value - height;
    }
    

    Abstract classes

    Dart 中的抽象类同样无法被直接实例化,如果需要实例化可以通过工厂构造器

    abstract class AbstractContainer {
      // Define constructors, fields, methods...
    
      void updateChildren(); // Abstract method.
    }
    

    Implicit interfaces

    Dart 中同样使用 implement 关键字实现一个或多个接口。

    class Point implements Comparable, Location {...}
    

    Extending a class

    使用 extends 继承父类,使用 super 引用父类。

    Extension methods

    和 Kotlin 类似,在 Dart 中我们同样可以通过扩展函数扩充函数库。

    Enumerated types

    Dart 中的枚举类和 Java 中类似。

    enum Color { red, green, blue }
    

    枚举类有以下限制:

    • 无法继承、实现或者混用 (mixin) 枚举类
    • 无法直接实例化

    Adding features to a class: mixins

    Dart 中还提供了另外一种复用代码的方式 Mixins。我们可以把可复用的代码放到 mixin 类中。子类使用 with 关键字进行关联。

    mixin 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');
        }
      }
    }
    
    class Musician with Musical {
      // ···
    }
    
    class Maestro extends Person with Musical {
      Maestro(String maestroName) : super(maestroName) {
        canPlayPiano = true;
      }
    }
    

    除此之外,我们还可以限制 mixin 的使用范围 (on),以及像接口一样使用逗号 (,) 分割继承多个 mixin:

    class Musician {
      // ...
    }
    mixin MusicalPerformer on Musician {
      // ...
    }
    class SingerDancer extends Musician with MusicalPerformer, DancerPerformer {
      // ...
    }
    

    Class variables and methods

    Dart 中的类同样可以使用静态变量和常量,以及静态方法。

    class DummyClass {
      static const a = 1;
      static var b = 2;
      static void f() { }
    }
    

    Generics

    Dart 中的泛型(参数化类型)和 Java 以及 Kotlin 等语言非常相似,使用 <> 表示泛型。

    为什么使用泛型

    使用泛型一般有两个目的:

    • 指明泛型类型之后,减少代码出错。比如在集合中加入不该加入的值。
    • 使用泛型减少复制粘贴的代码。比如持有某个对象的类,当需要改变持有的对象而其它部分不发生变化时,如果使用了泛型就不用重新复制一个新的类。

    Using collection literals

    即在使用集合时定义集合类型,形式如 <type>[] or <type>{} or <keyType, valueType>{}

    var names = <String>['Seth', 'Kathy', 'Lars'];
    var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};
    

    泛型集合及其类型

    Java 中,由于泛型存在类型擦除,所以是无法确定集合的确切类型的。但是,Dart 中的泛型是 reified 的,也就是说在运行时也能得到集合的类型信息。

    var names = <String>[];
    names.addAll(['Seth', 'Kathy', 'Lars']);
    print(names is List<String>); // true
    

    泛型参数类型的限制

    Dart 中限制泛型参数边界同样使用 extends 关键字。

    class Foo<T extends SomeBaseClass> {
      // Implementation goes here...
      String toString() => "Instance of 'Foo<$T>'";
    }
    
    class Extender extends SomeBaseClass {...}
    

    Libraries and visibility

    Dart 中,我们可以通过 importlibrary 指示符创建模块化的、可共享的代码库。另外,即使不使用 library 指示符,每个 dart app 都是一个 library。

    导入一个 library 的语法:

    import 'dart:html'; // html 是 dart 内建的库,所以使用 dart 作为命名空间
    

    其它库可以使用文件路径或者 package: 命名空间:

    import 'package:test/test.dart'; // 导入一些三方库,比如通过 pub 包管理工具发布的三方库
    

    指定库的前缀

    如果两个库有相同的名称,可以通过 as 指定前缀解决冲突。

    import 'package:lib1/lib1.dart';
    import 'package:lib2/lib2.dart' as lib2;
    
    // Uses Element from lib1.
    Element element1 = Element();
    
    // Uses Element from lib2.
    lib2.Element element2 = lib2.Element();
    

    部分导入

    如果只使用到库的一部分,可以使用部分导入的语法,通过 showhide 做到。

    // 只导入 foo
    import 'package:lib1/lib1.dart' show foo;
    
    // 导入除了 foo 意外的部分
    import 'package:lib2/lib2.dart' hide foo;
    

    实现 libraries

    Create Library Packages

    Asynchrony support

    Dart 中有很多返回 FutureStream 对象的函数,我们可以使用它们编写出异步的代码。除此之外,Dart 还提供了 asyncawait 关键字,让你可以像 JS 一样,更方便地写出一些更具易读性的异步代码。

    常见的用法如下:

    Future<String> lookUpVersion() async => '1.0.0';
    
    // 在方法末尾添加 async 关键字,说明方法执行耗时操作,返回值通常是 Future
    Future checkVersion() async {
      var version = '';
      try {
        version = await lookUpVersion();
      } catch (e) {
        // React to inability to look up the version
      }
    }
    

    如果 async 方法无有意义的返回值,可以返回 Future<void>

    Generators

    Dart 中的 Generator 函数和 ES6 中相似,如果你想要延迟生成一系列值,可以考虑使用生成器。Dart 中的 Generator 函数分为两种:

    • 同步生成器:返回 Iterable 对象
    • 异步生成器:返回 Stream 对象
    // 同步生成器
    Iterable<int> naturalsTo(int n) sync* {
      int k = 0;
      while (k < n) yield k++;
    }
    
    // 异步生成器
    Stream<int> asynchronousNaturalsTo(int n) async* {
      int k = 0;
      while (k < n) yield k++;
    }
    
    // 递归的生成器,使用 yield* 提高性能
    Iterable<int> naturalsDownFrom(int n) sync* {
      if (n > 0) {
        yield n;
        yield* naturalsDownFrom(n - 1);
      }
    }
    

    Callable classes

    我们可以通过给某个类实现 call() 方法,使得该类可以像方法一样被调用。

    class WannabeFunction {
      String call(String a, String b, String c) {
        print('WannabeFunction.call()');
        return '$a $b $c!';
      }
    }
    
    var wf = WannabeFunction(); // 初始化该类对象,然后可以像方法一样调用它们
    wf('Hi', 'there,', 'gang') // 实际调用的是 call() 方法
    

    Isolates

    与其它语言的并发机制不同,Dart 中并没有采用共享状态的并发机制 (shared-state concurrency),而是使用了 isolates。所有的代码都运行在自己的 isolate 中,每个 isolate 都有自己的内存堆,保证了相互独立性。

    更多资料见:Isolates

    Typedefs

    Dart 中,一切皆是对象,函数也是对象,但是函数对象的信息在运行时往往会被丢失,比如下面这个例子:

    class SortedCollection {
      Function compare;
    
      SortedCollection(int f(Object a, Object b)) : compare = f;
    }
    
    int sort(Object a, Object b) => 0;
    
    SortedCollection sc = SortedCollection(sort);
    
    // 只知道 compare 是 Function,但是其具体类型被丢失了
    assert(sc.compare is Function);
    

    typedef 就是为了解决上面这个问题而出现的,我们可以给方法类型一个别名,这样,当方法被赋值到一个变量上时就能保留其类型信息:

    // 函数的信息被保存在 Compare 中
    typedef Compare = int Function(Object a, Object b);
    
    class SortedCollection {
      Compare compare; // 用于接收我们定义的函数
    
      SortedCollection(this.compare);
    }
    
    int sort(Object a, Object b) => 0;
    
    SortedCollection sc = SortedCollection(sort);
    assert(sc.compare is Compare); // 现在可以确定函数的具体类型了
    

    typedef 也可以使用泛型:

    typedef Compare<T> = int Function(T a, T b);
    
    int sort(int a, int b) => a - b;
    
    assert(sort is Compare<int>); // True!
    

    Metadata

    通过元信息注解,我们可以为类和方法提供额外的信息。Dart 中自带的注解有 @deprecated@override,除此之外,你也可以自定义注解,只要在类的构造器上使用 const 关键字就可以了:

    class Todo {
      final String who;
      final String what;
    
      const Todo(this.who, this.what);
    }
    

    Comments

    Dart 支持三种类型的注释:单行注释、多行注释和文档注释。

    /// 文档注释,使用 [] 给成员属性或者方法添加链接,比如查看 [main] 方法
    /// 虽然 Dart 中也能用 /** */ 作为文档注释,但是编辑器会提醒你用 /// 代替
    void main() {
      // 单行注释
      print('Howdy!');
      /*
       * 多行注释
       */
       print('Hola!');
    }
    

    相关文章

      网友评论

          本文标题:Dart 学习笔记

          本文链接:https://www.haomeiwen.com/subject/fmrjkltx.html