全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2024-09-24_跟杰哥一起学Flutter | 实战进阶-Json序反序列化的最佳实践

您的位置:首页 >> 新闻 >> 行业资讯

跟杰哥一起学Flutter | 实战进阶-Json序反序列化的最佳实践 点击关注公众号,“技术干货”及时达!1. 引言 ?? 之前写的 《六、项目实战-非UI部分???♂?》一文中关于 Json序/反序列化 和 网络请求 部分写得有些简陋,在实际开发中发现并不太好用,本节先来讲讲关于 Flutter中的Json序/反序列化 本人归纳出的最佳实践。?? 当然,有更好的实践方式也欢迎在评论区指出,谢谢~ QuickType + json_serializable库,复制后端返回的Json字符串到QuickType生成Model类,直接复制粘贴到项目中。可以使用 泛型 减少重复Model的编写,利用 build.yaml 减少重复属性的设置,根据实际情况按需 自定义字段的序列化/反序列化。 本节内容如下: 2. 常识 2.1. Json → 序列化 VS 反序列化?? 温故知新,先区分下 Json 的 序列化 和 反序列化: 序列化:对象或数据结构 → Json字符串 → 发起接口请求时用到,Dart对象 → Json字符串。反序列化:Json字符串 → 对象或数据结构 → 解析接口响应时用到,Json字符串 → Dart对象。2.2. Flutter对Json序/反序列化的支持Flutter中提供了 dart:convert 库来编码和解码各种数据格式,其中就包含了Json的 解码(反序列化) 和 编码(序列化) ,对应方法:decode() 和 encode() 。简单调用代码示例如下: import 'dart:convert'; void main() { String jsonString = '{"name": "Jay", "age": 30}'; // 解码 (反序列化),Json字符串 → Dart对象 MapString, dynamic userMap = jsonDecode(jsonString); print(userMap); // 输出:{name: John, age: 30} // 编码 (序列化),Dart对象 → Json字符串 MapString, dynamic userMap = {'name': 'Jay', 'age': 30}; String jsonString = jsonEncode(userMap); print(jsonString); // 输出:{"name":"John","age":30}}当然,实际开发中很少这样直接解析Json,通常会根据字段写一个Model类,然后定义两个方法来 序/反序列化,通常命名为 fromJson() 和 toJson() 。注意!只是 约定俗成 的命名,非强制,你硬要定义成decodeFromJson() 和 encodeToJson() 也是可以的。但还是建议这样命名,特别是 toJson() : Dart中json.encode()会 自动查找并调用对象的toJson() 方法来获取对象的Json表示,这是通过 dart:convert库中的 JsonEncoder 的约定来实现的。 来一段Json样本: { "desc": "我们支持订阅啦~", "id": 30, "imagePath": "https://www.wanandroid.com/blogimgs/42da12d8-de56-4439-b40c-eab66c227a4b.png", "isVisible": 1, "order": 2, "title": "我们支持订阅啦~", "type": 0, "url": "https://www.wanandroid.com/blog/show/3352"}序列化和反序列化的代码示例如下: import 'dart:convert'; class Banner { final String desc; final int id; final String imagePath; final int isVisible; final int order; final String title; final int type; final String url; Banner({ required this.desc, required this.id, required this.imagePath, required this.isVisible, required this.order, required this.title, required this.type, required this.url, }); // 反序列化 factory Banner.fromJson(MapString, dynamic json) { return Banner( desc: json['desc'], id: json['id'], imagePath: json['imagePath'], isVisible: json['isVisible'], order: json['order'], title: json['title'], type: json['type'], url: json['url'], ); } // 序列化 MapString, dynamic toJson() { return { 'desc': desc, 'id': id, 'imagePath': imagePath, 'isVisible': isVisible, 'order': order, 'title': title, 'type': type, 'url': url, }; }} // 调用方法void main() { String jsonString = "上面的json字符串"; // 反序列化 MapString, dynamic jsonMap = json.decode(jsonString); Banner banner = Banner.fromJson(jsonMap); print(banner.title); // 输出: 我们支持订阅啦~ // 序列化 (不需要手动调用toJson()) String jsonString = json.encode(banner); print(jsonString);}2.3. 为啥Flutter中处理Json这么麻烦??? 每次序列化和反序列化都自己抠字段,重复劳动 之余还容易引入 人为错误 (如字段拼写错误),肯定是得向南发 自动生成 代码的。在Java中,一般通过 Gson/Jackson库 来实现自动自动序列化,原理是 运行时反射。但在Flutter中,你却找不到这样的库。Dart本身是支持反射的!!!只是 Flutter中禁用了反射, 因为它会干扰Dart的 Tree Shaking(摇树) : 摇树是Dart编译过程的一个术语, 它会 移除 应用程序编译后的 未被使用的代码,以缩减应用体积。而反射需要在运行时动态查询或调用对象的属性或方法,为此,编译器必须保留应用中所有可能会被反射机制调用的代码,即便这些代码在实际工作流程中可能永远不会被执行。这直接干扰到摇树,编译器无法确定那些代码是 "多余" 的。因此,Flutter直接禁掉了运行时反射(具体表现:不能使用 dart:mirrors库),鼓励开发者使用 编译时代码生成 的方式来代替反射。 ?? 官方推荐使用 json_serializable 这个 Flutter编译时工具 来生成Json序/反序列化代码,过下它的详细用法~ 3. json_serializable 库 ?? 用库前,得之前这个库是用来解决什么问题的~ 答:简化Json数据与Dart对象的转换,提供 Json序列化和反序列化代码的自动生成。 3.1. 添加依赖它由三个部分组成: json_annotation → 定义注解。json_serializable → 使用这些注解来生成代码。build_runner → 执行生成代码的任务。添加库依赖的方式二选一: # 方式一:终端直接键入下述命令安装flutter pub add json_annotation dev:build_runner dev:json_serializable # 方式2:打开 build.yaml文件手动添加依赖dependencies:flutter: sdk: flutterjson_annotation: ^4.8.1 dev_dependencies:flutter_test: sdk: flutterbuild_runner: ^2.4.7json_serializable: ^6.7.13.2. 基本使用定义Model类添加属性,给类加上 @JsonSerializable 注解,并添加 fromJson() 和 toJson() 方法: import 'package:json_annotation/json_annotation.dart'; part 'banner.g.dart'; // 1.指定生成的文件,一般是当前文件.g.dart @JsonSerializable() // 2.添加注解,告知此类是要生成Model类的class Banner { @JsonKey(name: 'id') // 3.可选,添加注解,告知此属性对应的json key final int bid; final String desc; final String imagePath; final int isVisible; final int order; final String title; final int type; final String url; Banner({ required this.bid, required this.desc, required this.imagePath, required this.isVisible, required this.order, required this.title, required this.type, required this.url, }); // 4、反序列化,固定写法:_${类名}FromJson(json) factory Banner.fromJson(MapString, dynamic json) = _$BannerFromJson(json); // 5、序列化,固定写法:_${类名}ToJson(this) MapString, dynamic toJson() = _$BannerToJson(this);}编写完上述代码,编译器会报_BannerFromJson和_BannerToJson找不到,没关系,只要确定没拼写错误就行,直接执行下述命令生成对应的序列化代码: flutter pub run build_runner build --delete-conflicting-outputs # 后面的 --delete-conflicting-outputs 是可选的,作用是:# 自动删除任何现存的,与即将生成的输出文件冲突的文件,然后继续构建过程。# 这样可以清理由于老版本或不同构建配置造成的遗留文件命令执行完,原先的报错就消失了,而且会在 同级目录 生成一个 xxx.g.dart 的文件: 然后跟前面一样调用fromJson()就行了,这里用到了这两个 注解,详细讲讲,加?的属性是比较常用的~ 3.2.1. @JsonSerializable用于:指示生成器如何 为类生成序/反序列化代码,可选属性如下: ? explicitToJson → 默认为false,涉及Model类嵌套时,赋值一个 引用类型,而不是 显式调用嵌套类的toJson() !涉及对象嵌套时,建议设置为true。? ignoreUnannotated → 默认为false,如果设置为true时,生成器只序列化和反序列化用 @JsonKey标记 的字段。? includeIfNull → 序列化时是否包含值为null的字段,默认为true,即忽略为null的字段。? genericArgumentFactories → 用于 泛型类的序列化和反序列化,默认为false,如果设置为true,生成的fromJson()和toJson()将 需要额外的类型参数的工厂函数,以保证泛型类的正确序列化和反序列化。anyMap → 默认false,如果设置为true,生成的 fromJson() 方法将接受如何类型为Map的对象,而不仅仅是MapString, dynamic。checked → 默认为false,如果设置为true,生成的代码会包含对每个字段的类型检查,确保在反序列化期间的类型匹配,如果类型不匹配,会抛出一个有用的错误信息。constructor → 指定用于生成 fromJson() 工厂构造函数的名称,默认为空字符串,即使用无名构造函数。createFieldMap → 默认为true,生成器将为 Map类型的字段 生成额外的序列化逻辑。createFactory → 默认为true,当你需要自定义反序列化逻辑时,可以设置为false,生成器不会生成fromJson()。createToJson → 默认为true,需要自定义序列化逻辑时,可以设置为false,生成器不会生成 toJson()。disallowUnrecognizedKeys → 默认为false,设置为true时,如果输入的Json中包含Model中未定义的Key,fromJson() 将抛出一个异常。fieldRename → 控制如何将类字段的名称更改为Json键名称,枚举类FieldRename,可选值有:none(默认,不更改)、kebab(短横线命名a-b)、snak(蛇形命名a_b)、pascal(帕斯卡命名AxxBxx)。converters → 允许自定义转换器,这些转换器可以在序列化和反序列期间使用。createPerFieldToJson → 默认为false,是否为每个字段创建一个单独的 _$[FieldName]ToJson() 函数,当你需要对某些字段进行特殊处理时,如:自定义类型需要特殊的序列化逻辑、想根据字段的值改变输出的Json结构等,即复杂的自定义序列化,再考虑是否将这个属性设置为true,毕竟会增加代码的复杂性!3.2.2. @JsonKey用于:定制 单个字段的序/反序列化行为,可选属性如下: ? disallowNullValue → 如果设为true,序列化时字段为null,会抛出一个异常,通常用于确保某些字段在序列化时不为null的场景。? ignore → 如果设置为true,序列化和反序列化时会忽略这个字段。? includeIfNull → 如果设置为true,即便字段的值为null,仍然会被包含在序列化的Json中。? name → 用于对Json中Key指定一个不同于Dart字段名的名称。defaultValue → json中缺少这个字段或值为null时,反序列化过程中使用的默认值。fromJson → 允许为字段提供一个自定义的反序列化函数。required → 如果设置为 true,反序列化时,如果Json中缺少这个字段会抛出一个异常。toJson → 允许为字段提供一个自定义的序列化函数。unknownEnumValue → Json中的值无法映射到Dart枚举类型中的任何值时,可以指定一个枚举默认值。3.3. 进阶玩法3.3.1. 泛型?? 在实际开发中,接口返回的Json数据大都是有固定格式的,比如玩Android的接口就由这三部分组成:data + errorCode + errorMsg。变化的结构只有 data 部分,这次是Banner,得写个BannerResponse的类,下次是Article,得写个ArticleResponse的类... 完全可以使用 泛型 来减少这种 重复劳动~ ?? 这里提一嘴 Dart泛型 和 Java泛型 的区别: Java泛型 是"假泛型",通过 类型擦除 来实现,泛型类型信息 只在 编译时存在,一旦代码被编译了就会被擦除,转换为它们的 边界类型 (如果指定了边界) 或 Object类型,这样做是为了 向后兼容早期的Java版本。而Dart泛型 的类型是 具象化(reified) 的,即:在运行时保留了泛型的类型信息,因此,你可以在运行时进行类型检查,比如使用 is 关键字判断对象是否为特定的泛型类型。除此之外,还可以使用 Type对象 和 runtimeType属性 来获取泛型的类型信息。 运行时获取泛型参数类型 的简单代码示例如下: void main() { Listint numbers1 = [1,2,3]; print(numbers1 is Listint // 输出:true Listint? numbers2 = []; print("${numbers1.runtimeType} == ${numbers2.runtimeType} → ${numbers1.runtimeType == numbers2.runtimeType}"); // 输出:Listint == Listint? → false // 定义变量赋值 Type type = numbers1.runtimeType; // 格式化输出 print("${numbers1.runtimeType} == ${type} → ${numbers1.runtimeType == type}");// Listint == Listint → true // 验证相同类型泛型参数不同是否相等 ListString stringList = ['a', 'b', 'c']; print("${stringList.runtimeType} == ${type} → ${stringList.runtimeType == type}"); // 输出:ListString == Listint → false // 运行时类型判定 if(numbers1.runtimeType == Listint) print("true");// 输出:true // 验证泛型嵌套是否能返回完整的泛型类型信息 ListListListString list = []; print("${list.runtimeType}"); // 输出:ListListListString}除此之外,还可以通过 显式传递类型信息 来实现 运行时获取泛型的类型信息,简单代码示例: void main() { var intBox = Boxint(type: int); var stringBox = BoxString(type: String); checkType(intBox); checkType(stringBox);} void checkType(Box box) { if (box.type == int) { print('Box contains int'); } else if (box.type == String) { print('Box contains String'); } else { print('Box contains unknown type'); }} class Box { final Type type; // 显式传递类型信息 Box({required this.type});}???♂? 扯得有点远了,收一下。data字段 返回的数据类型可能是 对象 或 列表,需要定义两个Model类,让他们支持泛型只需两步: @JsonSerializable 设置 genericArgumentFactories 为 true。fromJson() 和 toJson() 中需要传递 额外的函数参数 指明如何将T类型的数据转换为Json,以及如何将Json转换为T。具体代码示例如下: import 'package:json_annotation/json_annotation.dart'; part 'base_response.g.dart'; // Data是对象@JsonSerializable(genericArgumentFactories: true)class DataResponse { final T? data; final int errorCode; final String errorMsg; DataResponse({required this.data, required this.errorCode, required this.errorMsg}); factory DataResponse.fromJson(MapString, dynamic json, T Function(dynamic json) fromJsonT) = _$DataResponseFromJson(json, fromJsonT); MapString, dynamic toJson(dynamic Function(T value) toJsonT) = _$DataResponseToJson(this, toJsonT);} // Data是列表@JsonSerializable(genericArgumentFactories: true)class ListResponse { final List? data; final int errorCode; final String errorMsg; ListResponse({required this.data, required this.errorCode, required this.errorMsg}); factory ListResponse.fromJson(MapString, dynamic json, T Function(dynamic json) fromJsonT) = _$ListResponseFromJson(json, fromJsonT); MapString, dynamic toJson(dynamic Function(T value) toJsonT) = _$ListResponseToJson(this, toJsonT);}执行 flutter pub run build_runner build 生成完.g.dart文件后,写下简单的测试代码: // 泛型的具体类型class User { final int id; final String name; User({required this.id, required this.name}); factory User.fromJson(MapString, dynamic json) = User(id: json['id'], name: json['name']); MapString, dynamic toJson() = {'id': id, 'name': name}; @override toString() = "User{id: $id, name: $name}";} // 测试代码void main() { var dataResponse = DataResponseUser.fromJson( { "data": {"id": 1, "name": "张三"}, "errorCode": 200, "errorMsg": "成功", }, (json) = User.fromJson(json), ); print("${dataResponse.runtimeType} → ${dataResponse.data}"); var listResponse = ListResponseUser.fromJson( { "data": [ {"id": 1, "name": "张三"}, {"id": 2, "name": "李四"}, ], "errorCode": 200, "errorMsg": "成功", }, (json) = User.fromJson(json), ); print("${listResponse.runtimeType} → ${listResponse.data}");}运行输出结果如下: 非常简单,聪明的你可能发现了:两个泛型类的代码,除了data的类型不一样,其它代码都是一样的。能不能合并成一个呢??? 当然可以,就是调用时的传参稍微麻烦点 (需要和泛型类型保持一致),具体代码如下: @JsonSerializable(genericArgumentFactories: true, explicitToJson: true)class WanResponse { final T? data; final int errorCode; final String errorMsg; WanResponse({required this.data, required this.errorCode, required this.errorMsg}); factory WanResponse.fromJson(MapString, dynamic json, T Function(dynamic json) fromJsonT) = _$WanResponseFromJson(json, fromJsonT); MapString, dynamic toJson(dynamic Function(T value) toJsonT) = _$WanResponseToJson(this, toJsonT);} // 测试代码void main() { var dataResponse = WanResponseUser?.fromJson( { "data": {"id": 1, "name": "张三"}, "errorCode": 200, "errorMsg": "成功", }, (json) = User.fromJson(json), ); print("${dataResponse.runtimeType} → ${dataResponse.data?.name}"); var listResponse = WanResponseListUser.fromJson({ "data": [ {"id": 1, "name": "张三"}, {"id": 2, "name": "李四"}, ], "errorCode": 200, "errorMsg": "成功", }, (json) = (json as List).map((e) = User.fromJson(e)).toList()); // !!!返回类型需要与泛型一致 print("${listResponse.runtimeType} → ${listResponse.data?[0].name}");}运行输出结果如下: ?? 每次解析List类型的data 都要写上一串 (json) = (json as List).map((e) = User.fromJson(e)).toList()) 略显繁琐,抽取成一个 全局方法: List parseList(dynamic json, T Function(MapString, dynamic) fromJson) = (json as List).map((e) = fromJson(e)).toList(); // 调用处(json) = parseList(json, User.fromJson));3.3.2. 用 build.yaml 减少重复属性设置实例场景:后端要求值为null的字段不要提交。如何解决:在每个Model类的 @JsonSerializable 注解中都设置属性 includeIfNull: false 生成器生成toJson()方法时忽略掉值为null的字段。又比如:涉及Model类嵌套,需要设置 explicitToJson: true,让其显式调用嵌套类的toJson(),而不是复制一个引用类型。发现问题:需要手动对所有的Model进行 重复的属性设置。更好解法:使用 build.yaml 文件进行 全局配置,该文件会被 build_runner 和 项目中使用的构建插件 (如 json_serializable、source_gen 等) 所使用。在项目中右键新建一个 build.yaml 文件,按需配置下就可: targets: $default: builders: # 全局配置json_serializable库省去重复设置属性 json_serializable: options: explicit_to_json: true # toJson()时将嵌套的对象也转换为Map类型而非引用 include_if_null: false # toJson()时忽略值为null的字段更多可选配置可见: json_serializable build configuration 3.3.3. 自定义字段序/反序列化逻辑有时我们需要对某些字段的序/反序列化做下 特殊处理,可以通过为 @JsonKey 注解设置 toJson 和 fromJson 属性来进行自定义。比如:提交(序列化)的时候是时间戳字符串,解析(反序列化)的时候是Datetime 的代码示例: @JsonSerializable()class User { String name; @JsonKey(fromJson: _timeStampFromJson, toJson: _timeStampToJson) DateTime timest User({required this.name, required this.timestamp}); factory User.fromJson(MapString, dynamic json) = _$UserFromJson(json); MapString, dynamic toJson() = _$UserToJson(this); // 自定义fromJson和toJson逻辑 static DateTime _timeStampFromJson(String timestamp) = DateTime.fromMillisecondsSinceEpoch(int.parse(timestamp)); static String _timeStampToJson(DateTime date) = date.millisecondsSinceEpoch.toString();} // 测试代码void main() { var user = User(name: 'Jay', timestamp: DateTime.now()); print(user.toJson()); print(User.fromJson(user.toJson()).timestamp);}运行输出结果如下: 3.3.4. 自定义转换器 (JsonConverter)除了上面这种 设置属性 的方式来 自定义fromJson和toJson逻辑 外,还可以 自定义转换器(JsonConverter) 来处理特定类型的转换。一个把Json中星期几 (如Monday、Tuesday等) 转换为枚举类型的代码示例: // 枚举类enum DayOfWeek { monday, tuesday, wednesday, thursday, friday, saturday, sunday } // 自定义转换器class DayOfWeekConverter implements JsonConverterDayOfWeek, String { const DayOfWeekConverter(); @override DayOfWeek fromJson(String json) { switch (json.toLowerCase()) { case 'monday': return DayOfWeek.monday; case 'tuesday': return DayOfWeek.tuesday; case 'wednesday': return DayOfWeek.wednesday; case 'thursday': return DayOfWeek.thursday; case 'friday': return DayOfWeek.friday; case 'saturday': return DayOfWeek.saturday; case 'sunday': return DayOfWeek.sunday; default: throw ArgumentError('Invalid day of the week: $json'); } } @override String toJson(DayOfWeek object) = object.toString().split('.').last; // DayOfWeek.monday,只取后面的monday} @JsonSerializable()class MyModel { // 使用自定义转换器 @DayOfWeekConverter() DayOfWeek dayOfWeek; MyModel(this.dayOfWeek); factory MyModel.fromJson(MapString, dynamic json) = _$MyModelFromJson(json); MapString, dynamic toJson() = _$MyModelToJson(this);} // 测试代码void main() { MyModel model = MyModel(DayOfWeek.monday); print(model.toJson()); print(MyModel.fromJson(model.toJson()).dayOfWeek);}运行输出结果如下: 3.4. 常见问题3.4.1. .g.dart 是否需要提交到Git?答:没有绝对的标准答案,可以提交,也可以不提交,只要 开发团队统一 就行了??。个人倾向于不提交,首先这个文件是 自动生成 的,你项目能跑起来它肯定是得生成的,它经常会发生变化 (改下字段就得重新生成),这可能导致 Git合并冲突,而这完全是可以避免的。 3.4.2. 执行生成序/反序列化代码的命令真烦~!如题,每次新建或修改Model类,都需要执行 flutter pub run build_runner 来生成序/反序列化代码,觉得麻烦可以执行下述命令启动一个 watcher,当带有 json_serializable 注解的类发生改变时,它会自动更新对应的.g.dart文件: flutter packages pub run build_runner watch如果你使用的IDE是 VSCode 的话,可以在 tasks.json 文件中配置下这个命令,方便快速执行: { "version": "2.0.0", "tasks": [ { "label": "Flutter Build Runner Watch", "type": "shell", "command": "flutter packages pub run build_runner watch", "isBackground": true, "presentation": { "reveal": "always", "panel": "shared" }, "problemMatcher": [] } ]}然后就可以在命令行面板找到这个任务啦,你还可以为其设置一个快捷键,打开命令面板,输入 Preferences: Open Keyboard Shortcuts (JSON) 并选中,在打开的 keybindings.json 文件中新增一个绑定,如: { "key": "ctrl+shift+b", // 快捷键 "command": "workbench.action.tasks.runTask", "args": "Flutter Build Runner Watch" // 任务名称}保存后,使用你设置的快捷键就能快速执行上述任务啦~ 4. Json 生成 Model类 的一些工具 json_serializable库 解决的是 自动生成Json序/反序列化代码,Model类的 属性 还是得 手动抠Json字段,可以通过一些工具来自动生成,喜欢哪个用哪个哈~ 4.1. 网页4.1.1. QuickType (?推荐)QuickType,直接复制Json到左侧,右侧会自动生成Model类的代码,复制粘贴到项目里就好了: 默认生成的代码是 不包含json_serializable注解相关,直接生成对应的toJson() 和 fromJson() 方法。需要点击 Options-Other 勾选下左图的选项。点击 Language 可以进行 编程语言的细节设置 (如这里选中Dart): 4.1.2. json2dartjson2dart,也是左边复制json右侧生成代码的玩法: 4.2. 插件 & 其它AS的插件商店搜下 JsonToDart 下载安装: 安装后,选定要存放Model类的目录,右键,依次选中:New → Json To Dart: 接着粘贴接口Json,写个类名,点击Generate生成就完事了~ ?? 不支持生成json_serializable注解,类似的插件还有 FlutterJsonBeanFactory,用法大同小异。其它工具: fluttercandies/JsonToDart:号称功能最全面的 Json 转换 Dart 的工具,支持 Windows,Mac,Web;flutterchina/json_model:一行命令,将Json文件转为Dart model类;?? 个人还是更倾向于 QuickType 或 json2dart,简单易用,通过工具生成绝大部分代码后,还需要进行微调,比如类名修改,添加属性注释等。?? 也可以根据自己的实际情况 编写自动生成脚本/插件,比如杰哥之前就写过一个py脚本,输入接口文档的URL: 自动提取出json里的字段并自动添加字段注释~ 点击关注公众号,“技术干货”及时达! 阅读原文

上一篇:2021-05-18_IBM开源了5亿行代码数据集,里面最多的编程语言却不是Python 下一篇:2021-10-30_「转」为什么你拍摄的画面总是很平?你需要着重考虑这三点!

TAG标签:

18
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价