前言
本文并不会罗列所有 Dart 语言的细节,本文是从一个前端工程师的角度来总结 Dart 语言相对于 JavaScript 和 TypeScript 的一些特性或者不同点,如果没讲的,那基本上就是和 TS(JS) 特性相同,默认读者是有 JavaScript 和 TypeScript 基础的。如果没有这两门语言基础,建议去看其他角度讲解 Dart 语言的文档或者官方文档。
基础用法上的区别点
main 方法
程序的地点必须是一个 main 方法。
void main() {
// ...
}
打印与分号
使用 print 函数打印内容到控制台。Dart 中每语句结束都需要加分号。
print("xxxxxx");
数据类型及 is 关键字
var可以定义任意类型的变量,类型可变bool布尔类型int整数类型double浮点数类型String字符串类型List列表类型,类似数组Map对象类型,类似JS里面的MapSet集合类型,类似 JS 里面的SetRecords记录类型
void main() {
var a = 'a';
bool b = true;
int c = 1;
double d = 1.0;
String e = "e";
List f = <int>[1, 2, 3]; // int 也可以不写
List<String> h = ['1'];
List<int> i = [1];
var j = List<String>.filled(2, 'j'); // ["j", "j"]
var k = {"key": "value"};
print(k["key"]); // "value"
var l = new Map();
l["key"] = 123;
var m = Set();
m.addAll({'a', 'b', 'b', 'c'});
print(m); // {a, b, c}
var record = ('first', a: 2, b: true, 'last');
print(record.$1); // first
print(record.$2); // last
print(record.a); // 2
print(record.b); // true
}
不常用类型:
RunesSymbol
void main() {
const string = 'Dart';
final runes = string.runes.toList();
print(runes); // [68, 97, 114, 116]
const emojiHeart = '♥';
print(emojiHeart.runes); // (9829)
Symbol obj = Symbol('name');
print(obj); // Symbol("name")
}
我们可以使用 is 关键字来判断数据类型,也可以用于判断是不是某个类。
void main() {
var a = null;
if (a is String) {
print('String');
} else {
print('null'); // null
}
}
类型别名
typedef IntList = List<int>;
IntList il = [1, 2, 3];
类型转换
字符串转数字
void main() {
String a = '100';
String b = '100.123';
print(int.parse(a)); // 100
print(double.parse(b)); // 100.123
}
void main() {
String a = '';
String b = '';
print(int.parse(a)); // 报错
print(double.parse(b)); // 报错
}
上述错误可以用 try...catch 语法解决。
数字转字符串
void main() {
var a = 123;
var b = a.toString();
print(b); // 123
}
字符串判断是否为空
void main() {
var a = '';
var b = 'b';
print(a.isEmpty); // true
print(b.isEmpty); // false
}
字符串插值、拼接
可以直接使用 + 拼接,也可以使用字符串插值,类似 JS/TS 的模板字符串。
void main() {
var a = 'a';
var b = 'b';
print('$a - $b');
print("$a - $b");
print('${a} - ${b}');
}
注意上面的 {} 是非必须的,但是在某些情况下必须加,比如在类里面使用某个成员变量 this.xxxx 就必须用 {} 阔起来。
运算符
算术运算符——取整
void main() {
var a = 100;
var b = 7;
print(a / b); // 14.285714285714286
print(a ~/ b); // 14
}
关系运算符
==、!=,不存在三等于的情况。
赋值运算符
??= 前面为空的时候,使用后面的值给前面赋值
void main() {
var a = 100;
a ??= 10;
var b;
b ??= 10;
print(a); // 100
print(b); // 10
}
~/= 取整后赋值
void main() {
var a = 100;
var b = 6;
print(a ~/= b); // 16
}
循环语言
do while
void main() {
int i = 1;
int sum = 0;
do {
sum += i;
i++;
} while (i <= 100);
print(sum); // 5050
}
函数
函数的参数类型及返回值类型
函数的参数类型,可以指定,也可以不指定,返回值也是,可以指定可以不指定。
myfunc(param) {
return param;
}
int myfunc(int param) {
return param;
}
函数的可选参数和参数默认值
将可选参数使用中括号括起来,不传的时候是个空值所有 int 后面需要加一个 ?,这样数据类型才正确。
int myfunc(String param1, [int? param2]) {
if (param2 == null) {
return 20;
}
return param2;
}
void main() {
print(myfunc('')); // 20
print(myfunc('', 22)); // 22
}
参数默认值,一般放在可选参数前,也要放在中括号里面。
int myfunc(String param1, [String param2 = 'param2', int? param3]) {
print(param2);
if (param3 == null) {
return 20;
}
return param3;
}
void main() {
print(myfunc('')); // param2 20
print(myfunc('', 'xxxx', 22)); // xxxx 22
}
函数的命名参数
调用函数传参时需要知道参数的名称,一并传入,才能完成赋值。
// required 代表该参数是必须要要传的,如果写 int? 就代表是可选参数
int myfunc({required int param1, required int param2}) {
print(param2);
return param1;
}
void main() {
print(myfunc(param2: 123, param1: 234)); // 123 234
}
匿名函数
var printNum = (int n) {
if (n > 10) {
print(n);
} else {
print(n + 10);
}
};
void main() {
printNum(5); // 15
printNum(50); // 50
}
箭头函数
比如,我们现在要打印一个列表的数据。
常规写法(匿名函数):
void main() {
List list = ['Apple', 'Banana', 'Peach'];
list.forEach((element) { // 这里和JS写法比,少了一个function
print(element);
});
}
箭头函数写法:箭头函数之后只能写一个语句,也不能写分号。
void main() {
List list = ['Apple', 'Banana', 'Peach'];
list.forEach((element) => print(element)); // print 这里也能用 {} 括起来
}
自执行函数
void main() {
(() {
print("我是自执行函数");
})();
((int n) {
print(n); // 123
})(123);
}
类
Dart 所有的东西都是对象,所有的对象都继承自 Object 类。
Dart 是一门使用类和单继承的面向对象语言,所有的对象都是类的实例,并且所有的类都是 Object 的子类。
最基本的类定义和实例化
类定义不能定义在 main 函数中。一般来说一个文件写一个类,要用的时候再使用 import 导入即可。
可以使用 new 关键字实例化类,也可以将其省略,都是一样的效果。
class Person {
int age = 18;
String name = 'YY';
getInfo() {
print("${this.name}---${this.age}");
}
}
void main() {
var p1 = new Person();
p1.getInfo(); // YY---18
var p2 = Person();
p2.getInfo(); // YY---18
}
默认构造函数
默认构造函数,名称与类名一样,且默认在实例化的时候执行。
class Person {
// late 表示定义的时候不用初始化,创建实例的时候再去初始化
late int age;
late String name;
Person(String name, int age) {
this.name = name;
this.age = age;
}
getInfo() {
print("${this.name}---${this.age}");
}
}
void main() {
var p1 = new Person('ABC', 20);
p1.getInfo(); // ABC---20
var p2 = Person('CBA', 22);
p2.getInfo(); // CBA---22
}
对默认构造函数进行简写
class Person {
int age;
String name;
Person(this.name, this.age);
getInfo() {
print("${this.name}---${this.age}");
}
}
命名构造函数
构造函数可以写多个,我们可以选择性的使用构造函数,比如下面我就创建了一个名称为 now 的构造函数,并用他实例化了一个类。
class Person {
late int age;
late String name;
Person(this.name, this.age);
Person.now() {
print("我是命名构造函数");
}
getInfo() {
print("${this.name}---${this.age}");
}
}
void main() {
var p2 = Person.now(); // 我是命名构造函数
}
factory 构造函数
在创建类实例的时候,如果不想每次 new 都创建一个新的实例 ,那么 使用 factory 关键字修饰构造函数。
class Logger {
final String name;
bool mute = false;
// _cache is library-private, thanks to
// the _ in front of its name.
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);
}
}
void main() {
var logger1 = Logger('UI');
var logger2 = Logger('UI2');
var logger3 = Logger('UI');
// identical 用于判断两个对象是否相等
print(identical(logger1, logger2)); // false
print(identical(logger1, logger3)); // true
}
访问修饰符
Dart 里面没有 public、private、protect 这样的访问修饰符。
Dart 里面使用 _ 开头来命名确定成员是否为私有成员。
不过有一点值得注意的是,如果类和实例化放在一个文件里面,那么你的私有成员的私有化是不生效的,下面看个例子,这个例子里面的私有成员是可以被调用和打印出来的,而且不会报错。
class Person {
late int age;
late String name;
int _height = 170;
Person(this.name, this.age);
Person.now() {
print("我是命名构造函数");
}
getInfo() {
print("${this.name}---${this.age}");
}
}
void main() {
var p2 = Person.now(); // 我是命名构造函数
print(p2._height); // 170
}
下面我们把 Person 类分离到 Person.dart 文件中,再用 import 引入进来:
import 'Person.dart';
void main() {
var p2 = Person.now(); // 我是命名构造函数
print(p2._height); // 报错
}
getter、setter
语法与 JS/TS 有细微的差异。
class Rect {
int height;
int width;
Rect(this.height, this.width);
set areaHeight(value) {
this.height = value * 2;
}
get area {
return this.height * this.width;
}
}
void main() {
var r = Rect(10, 10);
print(r.area); // 100
r.areaHeight = 15;
print(r.area); // 300
}
构造函数运行之前初始化实例变量
class Rect {
int height;
int width;
Rect()
: height = 10,
width = 10 {}
}
void main() {
var r = Rect(); // 不能再传参了
print(r.height); // 10
print(r.width); // 10
}
静态成员的定义和访问
class Person {
late int age;
static String name = 'YY';
Person(this.age);
// 非静态方法可以访问静态成员及非静态成员
void show() {
// 类中访问静态成员不能加 this
print(name);
// 类中访问非静态成员 this 可加可不加
print(age);
}
// 静态方法可以访问静态成员,但是不能访问非静态成员
static void showName() {
// 类中访问静态成员不能加 this
print(name);
}
}
void main() {
var p1 = Person(20);
p1.show(); // YY 20
Person.showName(); // YY
}
级联操作符 ..
class Person {
int age = 18;
String name = 'YY';
Person(this.name, this.age);
void show() {
print(name);
print(age);
}
}
void main() {
var p1 = Person('YY', 20);
p1..age = 25
..name = 'KK'
..show(); // KK 25
}
继承
子类可以继承父类的可见属性和方法,但是构造函数不会被继承,如果构造函数需要传参就要用到 super 关键字。
class Person {
int age = 18;
String name = 'YY';
Person(this.name, this.age);
Person.xxx(this.name, this.age);
void show() {
print(name);
print(age);
}
}
class Superman extends Person {
int power = 10;
Superman(super.name, super.age, this.power);
// 另一种写法
// Superman(String name, int age, this.power): super(name, age);
Superman.xxx(String name, int age, this.power): super.xxx(name, age);
}
void main() {
var s1 = Superman('YY', 20, 10000);
s1..age = 25
..name = 'Superman'
..show(); // Superman 25
}
重写父类方法
class Person {
int age = 18;
String name = 'YY';
Person(this.name, this.age);
void show() {
print(name);
print(age);
}
}
class Superman extends Person {
int power = 10;
Superman(String name, int age, this.power): super(name, age);
// override 可以不写,但是建议写上
@override
show() {
super.show();
print(power);
}
}
void main() {
var s1 = Superman('YY', 20, 10000);
s1..age = 25
..name = 'Superman'
..show(); // Superman 25 10000
}
抽象类
abstract class Animal {
void eat(); // 不需要加 abstract
}
class Dog extends Animal {
@override
eat() {
print('小狗吃东西');
}
}
class Cat extends Animal {
@override
eat() {
print('小猫吃东西');
}
}
void main() {
var dog = new Dog();
dog.eat(); // 小狗吃东西
var cat = new Cat();
cat.eat(); // 小猫吃东西
}
多态
通常意义上的多态是指父类定义一个方法,子类各有各的实现,每个子类有不同的表现。
上面的例子其实就是我们通常意义上的多态了。
但是 Dart 里面,允许将子类类型的指针赋值给父类类型的指针,同一个函数调用会有不同的执行效果。子类的实例赋值给父类的引用。
abstract class Animal {
void eat(); // 不需要加 abstract
}
class Dog extends Animal {
@override
eat() {
print('小狗吃东西');
}
run() {
print('小狗跑');
}
}
class Cat extends Animal {
@override
eat() {
print('小猫吃东西');
}
run() {
print('小猫跑');
}
}
void main() {
Animal dog = new Dog();
dog.eat(); // 小狗吃东西
dog.run(); // 报错
Animal cat = new Cat();
cat.eat(); // 小猫吃东西
cat.run(); // 报错
}
接口
Dart 语言并没有像 JS/TS 里面的 interface 关键字,靠 implements 类或者抽象类来当接口,可以同时实现多个接口。
abstract class Animal {
void eat();
printInfo() {
print("我是父类1方法");
}
}
class Dog implements Animal {
@override
eat() {
print('小狗吃东西');
}
run() {
print('小狗跑');
}
@override
printInfo() {
print("我必须把父类方法全都实现一遍,不能像继承一样拿过来直接用");
}
}
void main() {
var dog = new Dog();
dog.eat(); // 小狗吃东西
dog.run(); // 小狗跑
dog.printInfo(); // 我必须把父类方法全都实现一遍,不能像继承一样拿过来直接用
}
使用 mixins 实现“多继承”
其实 Dart 并不支持多继承,使用 mixins 特性去实现的也只是一个类似多继承的功能。
class A {
late String name;
printInfoA() {
print("A");
}
}
class B {
late String address;
printInfoB() {
print("B");
}
}
abstract class C {
late int age;
printInfoC() {
print("C");
}
}
class D extends A with B, C {}
void main() {
var d = D();
d.printInfoA(); // A
d.printInfoB(); // B
d.printInfoC(); // C
print(d is A); // true
print(d is B); // true
print(d is C); // true
}
上面的例子中,使用 with 去 mixins B类 和 C类都不能有构造函数,否则会报错。如果混入的类方法重名,那么谁后混入就最终会使用哪个类的方法。
枚举
基础用法
enum Color { red, green, blue }
void main() {
var c = Color.blue;
print(c); // Color.blue
List<Color> colors = Color.values;
print(colors[2] == Color.blue); // true
print(Color.red.index == 0); // true
print(Color.green.index == 1); // true
print(Color.blue.index == 2); // true
print(Color.blue.name); // 'blue'
}
声明增强的枚举
Dart 还允许枚举声明,声明具有字段、方法和常量构造函数的类,这些类仅限于固定数量的已知常量实例。
要声明增强的枚举,请遵循与普通类类似的语法,但有一些额外要求:
- 实例变量必须是最终变量,包括由mixin添加的实例变量。
- 所有生成构造函数都必须是常量。
- 工厂构造函数只能返回一个固定的已知枚举实例。
- 没有其他类可以扩展,因为Enum是自动扩展的。
- 索引、hashCode和相等运算符
==不能有重写。 - 不能在枚举中声明名为value的成员,因为它会与自动生成的静态值getter冲突。
- 枚举的所有实例都必须在声明的开头声明,并且必须至少声明一个实例。
增强枚举中的实例方法可以使用此引用当前枚举值。
下面是一个示例,它声明了一个具有多个实例、实例变量、getter和一个已实现接口的增强枚举:
enum Vehicle implements Comparable<Vehicle> {
car(tires: 4, passengers: 5, carbonPerKilometer: 400),
bus(tires: 6, passengers: 50, carbonPerKilometer: 800),
bicycle(tires: 2, passengers: 1, carbonPerKilometer: 0);
const Vehicle({
required this.tires,
required this.passengers,
required this.carbonPerKilometer,
});
final int tires;
final int passengers;
final int carbonPerKilometer;
int get carbonFootprint => (carbonPerKilometer / passengers).round();
bool get isTwoWheeled => this == Vehicle.bicycle;
@override
int compareTo(Vehicle other) => carbonFootprint - other.carbonFootprint;
}
泛型
泛型方法
// 第一个T是返回值类型
T getData<T>(T value) {
return value;
}
void main() {
getData<String>('text');
getData<int>(123);
}
泛型类
List 就是一个典型的泛型类。
void main() {
List list = <int>[1, 2, 3];
}
自定义泛型类
class MyList<T> {
List list = <T>[];
}
void main() {
MyList list = MyList<String>();
}
泛型接口
abstract class ObjectCache {
getByKey(String key);
void setByKey(String key, Object value);
}
abstract class StringCache {
getByKey(String key);
void setByKey(String key, Object value);
}
// 泛型写法就可以代替上面两个接口
abstract class Cache<T> {
getByKey(String key);
void setByKey(String key, T value);
}
class MemoryCache<T> implements Cache<T> {
@override
getByKey(String key) {
return key;
}
@override
void setByKey(String key, T value) {
print("我是内存缓存${key} value=${value}");
}
}
void main() {
var m = MemoryCache<String>();
m.setByKey("key", "0xff"); // 我是内存缓存key value=0xff
}
const、final、identical
const 与 final 的区别
const 与 final 都可以用于定义常量,区别在于初始化的时机不一样,final 是运行时常量,在运行时进行赋值,且只能赋值一次,而 const 是在编译时就要确定值。
// 这种用法 const 与 final 并无什么区别
void main() {
const PI = 3.14;
final PIE = 3.14;
}
// 这种用法就体现出区别了
void main() {
const PI = 3.14;
final TIME = new DateTime.now(); // 这里如果用 const 会报错
}
使用 identical 判断对象是否相同
identical 是 Dart 语言 core 中的函数,他可以用于判断两个引用是否指向同一个对象(同一个存储空间)。
void main() {
var obj1 = Object();
var obj2 = Object();
print(identical(obj1, obj2)); // false
print(identical(obj1, obj1)); // true
}
void main() {
var obj1 = 1;
var obj2 = 1;
var obj3 = 2;
var obj4 = [1, 2, 3];
var obj5 = [1, 2, 3];
print(identical(obj1, obj2)); // true
print(identical(obj1, obj3)); // false
print(identical(obj4, obj5)); // false
}
使用 const 创建相同对象
多个地方在实例化相同对象的时候,如果在前面增加 const 关键字,那么内存中只会保留一个对象,类似单例。
void main() {
var obj1 = const Object();
var obj2 = const Object();
print(identical(obj1, obj2)); // true
print(identical(obj1, obj1)); // true
}
// 对象的值一样的情况下才会只保留一个对象,否则还是会不一样的
void main() {
var obj1 = const [1, 2, 3];
var obj2 = const [1, 2, 3];
var obj3 = const [4, 5, 6];
var obj4 = const [4, 5, 6];
print(identical(obj1, obj2)); // true
print(identical(obj1, obj1)); // true
print(identical(obj2, obj3)); // false
print(identical(obj3, obj4)); // true
}
常量构造函数
常量构造函数特点:
- 其构造函数使用
const关键字修饰 - 成员变量必须以
final关键字修饰 - 实例化的时候仍然需要加
const关键字,否则实例化的对象也不是常量实例(非常量构造函数,用const关键字实例化会报错) - 实例化的时候,多个地方创建这个对象,如果传入的参数相同,只会创建一个对象
flutter中大量使用了const关键字,这是一个比较重要的特性,可以大大减少组件构建时的内存开销,在改变组件状态时不会重新构建const声明的组件,减少不必要的重渲染
class Container {
final int width;
final int height;
const Container({required this.width, required this.height});
}
void main() {
var obj1 = const Container(width: 100, height: 100);
var obj2 = const Container(width: 100, height: 100);
var obj3 = const Container(width: 200, height: 200);
var obj4 = Container(width: 100, height: 100);
var obj5 = Container(width: 100, height: 100);
print(identical(obj1, obj2)); // true
print(identical(obj2, obj3)); // false
print(identical(obj2, obj3)); // false
print(identical(obj4, obj5)); // false
print(identical(obj1, obj4)); // false
}
Dart 的各种库及包管理系统
自定义库
这个比较简单,就是你自己写的一些 dart 文件,抽离出去,然后在你的代码中 import 进来即可。
import 'lib/xxx.dart'
dart 中,模块不需要 export。
系统内置库
dart 内置了很多库,可以直接引入使用。
import 'dart:math';
void main() {
print(max(10, 32)); // 32
}
Pub 包管理系统中的库
通过 pub 包管理系统,可以引入第三方模块。
官网地址如下:
pub-web.flutter-io.cn/packages
要使用这个库,我们需要先定义一个 pubspec.yaml,文件大致内容如下:
name: flutterdemo
description: "A new Flutter project."
environment:
sdk: '>=3.3.1 <4.0.0'
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.6
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter:
uses-material-design: true
name是项目名称description是项目描述environment是我们使用的dart的版本dependencies是运行时的依赖dev_dependencies是开发依赖
如果想要安装一个第三方模块,可以去官方网站找到该模块,然后获得其依赖引入的关键字和版本号,如:
dependencies:
http: ^1.2.1
然后执行如下命令进行获取:
pub get
使用:
import 'package:http/http.dart' as http;
// 或
import 'package:http/http.dart';
部分导入
// 只导出某一个函数
import 'package:lib1/lib1.dart' show foo;
// 只不导出某一个函数
import 'package:lib1/lib1.dart' hide foo;
延迟加载
也称为懒加载,可以在需要的时候再进行加载,减少 APP 的启动时间。
import 'package:deferred/hello.dart' deferred as hello;
greet() async {
// 需要用loadLibrary方法来加载
await hello.loadLibrary();
hello.printGreeting();
}
Null safety
空安全,Dart2.12.0 之后提供的特性。主要是为了在编译期防止意外访问 null 变量的错误产生。
例如,如果一个方法需要整型结果,却接收到了 null,你的应用会出现运行时错误。这类空引用错误在以前调试是非常困难的。
空安全的原则
Dart 的空安全支持基于以下两条核心原则:
- 默认不可空。除非你将变量显式声明为可空,否则它一定是非空的类型。我们在研究后发现,非空是目前的 API 中最常见的选择,所以选择了非空作为默认值。
- 完全可靠。Dart 的空安全是非常可靠的,意味着编译期间包含了很多优化。如果类型系统推断出某个变量不为空,那么它 永远 不为空。当你将整个项目和其依赖完全迁移至空安全后,你会享有健全性带来的所有优势—— 更少的 BUG、更小的二进制文件以及更快的执行速度。
可空类型
pubspec.yaml 配置 2.12.0 之前的版本,下面这种写法是不报错的,但是配置版本 大于等于 2.12.0 则会有报错。
void main() {
String name = '';
name = null;
}
name 的类型是 字符串类型,并不能包含 null 类型,所以就会报错。如果要让 name 可以为 null 那么我们就需要显示声明,告诉我们的编译器,这个变量可以为空,写法如下:
void main() {
String? name = '';
name = null;
}
? 表示可空类型。
String? getData(apiUrl) {
if (apiUrl != null) {
return 'data';
}
return null;
}
非空断言
! 表示非空断言
void main() {
String? str = "data";
str = null;
print(str!.length);
}
延迟初始化
late 关键字延迟初始化。
class Person {
// late 表示定义的时候不用初始化,创建实例的时候再去初始化
late int age;
late String name;
Person(String name, int age) {
this.name = name;
this.age = age;
}
getInfo() {
print("${this.name}---${this.age}");
}
}
required
required 关键字主要用于允许根据需要标记任何命名参数(函数或类),使得他们不为空。因为可选参数中必须有个 requeired。
String printUserInfo(String name, { required int age, required String sex, }) {
if (age != 0) {
return "姓名: $name---性别: $sex---年龄: $age";
}
return "姓名: $name---性别: $sex---年龄保密";
}
void main() {
print(printUserInfo("YY", age: 25, sex: 'Male')); // 姓名: YY---性别: Male---年龄: 25
}
class Person {
int? age;
String name;
Person({required this.name, this.age}); // 表示 name 必须传入 age 可以不传
getInfo() {
print("${this.name}---${this.age}");
}
}
void main() {
var p = Person(age: 23, name: 'YY');
p.getInfo(); // YY---23
}
异步
async、await 套装,Future 类似 Promise。
import 'dart:convert';
import 'dart:io';
const String filename = 'with_keys.json';
void main() async {
// Read some data.
final fileData = await _readFileAsync();
final jsonData = jsonDecode(fileData);
// Use that data.
print('Number of JSON keys: ${jsonData.length}'); // Number of JSON keys: 1
}
Future<String> _readFileAsync() async {
final file = File(filename);
final contents = await file.readAsString();
return contents.trim();
}
with_keys.json
{
"abc": "123"
}