一、Dart与JavaScript互操作的核心前提
很多做跨端开发的朋友都有过这样的经历:写了个用Dart开发的Flutter应用,突然需要调用一个已经用JavaScript写好的成熟功能,比如第三方地图SDK、复杂的加密工具,或者想把Dart写的逻辑暴露给网页端用。这时候就绕不开Dart和JavaScript的互操作——简单说就是让两种语言的代码能互相调用、互相传数据。
要让两种语言能“对话”,最基础的就是得解决类型和数据的问题:Dart里的字符串、数字、列表、对象,传到JavaScript那边得是什么样?反过来JavaScript的东西到Dart这边又该怎么认?要是类型不对应、数据转错了,轻则拿不到结果,重则直接报错崩溃。所以类型映射和数据转换是互操作的核心,也是很多人踩坑最多的地方。
1.1 先搞懂两种语言的类型差异
在说具体映射之前,得先明白Dart和JavaScript的类型体系有本质区别:Dart是强类型语言,写代码时就得明确变量的类型,不能随便改;而JavaScript是弱类型语言,一个变量一会儿存数字一会儿存字符串都没问题。另外Dart有很多独有的类型,比如BigInt(处理超大数字)、DateTime(日期时间)、Uint8List(二进制字节数组),这些在JavaScript里要么没有原生对应,要么得用特殊方式处理。
举个最直观的例子:Dart里的int类型,是可以存任意大小的整数的,比如1、1000000000000000000000都能存;但JavaScript里的数字是用双精度浮点数存的,超过一定范围的整数会丢失精度,比如10000000000000000这个数,传到JavaScript里可能就变成10000000000000001了。这种差异就是我们要做类型映射和数据转换的根本原因。
二、基础类型的映射与转换技巧
基础类型是互操作时最常用的,也是最容易出问题的,我们先从最基础的数字、字符串、布尔值开始说,再扩展到列表、对象这些复杂类型。
2.1 数字类型的映射与转换
数字是两种语言里最常用的类型,但也是坑最多的。Dart里的数字分int(整数)和double(浮点数),而JavaScript里只有一种number类型。
2.1.1 常规数字的映射
大部分情况下,Dart的int(在JavaScript能表示的安全整数范围内)和double传到JavaScript里都会变成number类型,反过来JavaScript的number传到Dart里,会根据实际值自动转成int或double。这里说的“安全整数范围”是指-(2^53 - 1)到2^53 - 1之间的整数,超过这个范围的整数就会丢失精度。
2.1.2 超大数字的处理
如果要传超过安全范围的整数,Dart里可以用BigInt类型,这时候传到JavaScript里,直接传number会丢失精度,得转成字符串传过去,或者用JavaScript的BigInt类型来接。
这里给大家看一个完整的示例,我们用Flutter的js_interop库(Dart官方推荐的互操作库,从Dart 3.0开始替代了之前的dart:js)来实现,技术栈统一用Dart 3.0+、Flutter、js_interop。
技术栈:Dart 3.0+, Flutter, js_interop
import 'package:js_interop/js_interop.dart';
// 定义JavaScript函数的类型声明,告诉Dart这个函数的参数和返回值类型
@JS('processBigInt') // 对应JavaScript里的processBigInt函数
external String processBigInt(String bigIntStr); // 传入字符串,返回字符串
// Dart里调用JavaScript的函数
void main() {
// Dart里的BigInt,值是1000000000000000000000(超过安全范围)
BigInt dartBigInt = BigInt.parse('1000000000000000000000');
// 转成字符串传给JavaScript,避免精度丢失
String jsResult = processBigInt(dartBigInt.toString());
// 把JavaScript返回的字符串转成Dart的BigInt
BigInt resultBigInt = BigInt.parse(jsResult);
print(resultBigInt); // 输出1000000000000000000000,没有丢失精度
}
对应的JavaScript代码(一般是网页里的script标签,或者打包后的JS文件):
// 接收字符串,转成JavaScript的BigInt处理,再返回字符串
function processBigInt(bigIntStr) {
// 转成JS的BigInt
const jsBigInt = BigInt(bigIntStr);
// 做一些处理,比如加1
const newBigInt = jsBigInt + 1n;
// 转成字符串返回给Dart
return newBigInt.toString();
}
这个示例的核心技巧就是:超大数字传字符串,两边用各自的BigInt类型处理,避免精度丢失。
2.2 字符串与布尔值的映射
字符串和布尔值是两种语言里最“友好”的类型,映射规则很简单:
- Dart的
String传到JavaScript里直接变成JS的string,JS的string传到Dart里直接变成Dart的String,不会有任何问题。 - Dart的
bool(true/false)传到JavaScript里直接变成JS的boolean,反过来也一样。
给大家看一个简单的示例: 技术栈:Dart 3.0+, Flutter, js_interop
import 'package:js_interop/js_interop.dart';
// 声明JS函数:接收字符串和布尔值,返回字符串
@JS('processStringAndBool')
external String processStringAndBool(String str, bool isUpper);
void main() {
String dartStr = 'hello dart';
bool dartIsUpper = true;
// 传参给JS
String jsResult = processStringAndBool(dartStr, dartIsUpper);
print(jsResult); // 输出HELLO DART
}
对应的JS代码:
function processStringAndBool(str, isUpper) {
if (isUpper) {
return str.toUpperCase();
}
return str.toLowerCase();
}
2.3 空值的映射
空值也是互操作时容易踩坑的地方,Dart里的空值分两种:null(明确为空)和undefined(未定义),而JavaScript里只有null和undefined。映射规则是:
- Dart的
null传到JavaScript里变成JS的null。 - Dart的
undefined(一般是可选参数没传值的情况)传到JavaScript里变成JS的undefined。 - 反过来JS的
null传到Dart里变成Dart的null,JS的undefined传到Dart里也变成Dart的null(因为Dart没有原生的undefined类型)。
这里要注意:如果Dart的变量是非空类型(比如String,不是String?),传参给JS的时候不能传null,否则会报错。
三、复杂类型的映射与转换技巧
除了基础类型,我们还会经常传列表、对象、日期时间、二进制数据这些复杂类型,这些类型的映射规则更复杂,也需要更多的转换技巧。
3.1 列表(数组)的映射与转换
Dart里的列表分两种:List(动态长度的列表)和Uint8List(固定长度的字节数组),JavaScript里对应的是数组。
3.1.1 普通List的映射
Dart的普通List(比如List<int>、List<String>)传到JavaScript里会直接变成JS的数组,反过来JS的数组传到Dart里会变成Dart的List。但这里要注意:Dart的List是强类型的,如果JS数组里的元素类型不匹配Dart的列表类型,会报错。
比如Dart里定义的是List<int>,JS数组里有一个字符串,传到Dart里就会报错。所以传列表的时候,一定要保证两边的元素类型一致。
3.1.2 Uint8List的映射
Uint8List是Dart里用来存二进制数据的类型,比如图片的字节、加密后的密文等,这个类型传到JavaScript里会变成JS的Uint8Array,反过来JS的Uint8Array传到Dart里会变成Dart的Uint8List。
给大家看一个处理图片字节的示例: 技术栈:Dart 3.0+, Flutter, js_interop
import 'dart:convert';
import 'package:js_interop/js_interop.dart';
// 声明JS函数:接收Uint8Array,返回图片的base64字符串
@JS('processImageBytes')
external String processImageBytes(Uint8List bytes);
void main() async {
// 模拟Dart里的图片字节(这里用一个简单的base64转成Uint8List)
const base64Image = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==';
Uint8List dartImageBytes = base64Decode(base64Image);
// 传Uint8List给JS,JS会转成Uint8Array处理
String jsBase64 = processImageBytes(dartImageBytes);
print(jsBase64); // 输出JS处理后的base64字符串
}
对应的JS代码:
function processImageBytes(imageBytes) {
// JS的Uint8Array转成base64字符串
const base64 = btoa(String.fromCharCode(...imageBytes));
// 给base64加个前缀(模拟处理)
return `data:image/png;base64,${base64}`;
}
3.2 对象的映射与转换
对象是两种语言里最复杂的类型,Dart的对象是基于类的,有明确的结构和类型,而JavaScript的对象是键值对的集合,结构灵活。
3.2.1 普通对象的映射
如果Dart的对象是一个简单的键值对结构(比如没有方法、没有复杂的嵌套),可以用Dart的Map来表示,Map传到JavaScript里会变成JS的普通对象,反过来JS的普通对象传到Dart里会变成Dart的Map。
但如果Dart的对象是一个类的实例(比如class User { String name; int age; }),传到JavaScript里不会自动变成JS的对象,需要手动转成Map,或者用js_interop的@JSExport注解把Dart类暴露给JS。
这里给大家看一个用类的示例: 技术栈:Dart 3.0+, Flutter, js_interop
import 'package:js_interop/js_interop.dart';
// 用@JSExport注解把Dart类暴露给JS,告诉JS这个类的结构
@JSExport()
class User {
final String name;
final int age;
User(this.name, this.age);
// 暴露给JS的方法
@JSExport()
String getInfo() {
return 'Name: $name, Age: $age';
}
}
// 声明JS函数:接收User对象,调用它的getInfo方法
@JS('processUser')
external String processUser(User user);
void main() {
User dartUser = User('张三', 25);
// 把Dart的User对象传给JS,JS可以直接调用getInfo方法
String jsResult = processUser(dartUser);
print(jsResult); // 输出Name: 张三, Age: 25
}
对应的JS代码:
function processUser(user) {
// JS可以直接调用Dart对象的方法
return user.getInfo();
}
这个示例的核心技巧是:用@JSExport注解把Dart类暴露给JS,让JS能直接访问Dart对象的属性和方法,不需要手动转成Map。
3.2.2 嵌套对象的映射
如果对象是嵌套的(比如Dart的class User { String name; List<Order> orders; },Order是另一个类),映射的时候要注意:嵌套的对象也需要用@JSExport注解,或者转成Map。
比如上面的示例,要是User里有List<Order>,Order类也需要加@JSExport注解,否则JS访问user.orders的时候会有问题。
3.3 日期时间的映射与转换
Dart里的日期时间用DateTime类型,JavaScript里的日期时间用Date类型,这两个类型的映射规则是:Dart的DateTime传到JavaScript里会变成JS的Date,反过来JS的Date传到Dart里会变成Dart的DateTime。
但这里要注意时区的问题:Dart的DateTime默认是本地时区,而JavaScript的Date默认是UTC时区,所以传日期时间的时候,最好统一转成UTC时间,或者转成时间戳(数字)传过去,避免时区不一致导致的错误。
给大家看一个示例: 技术栈:Dart 3.0+, Flutter, js_interop
import 'package:js_interop/js_interop.dart';
// 声明JS函数:接收Date对象,返回UTC时间的字符串
@JS('processDate')
external String processDate(DateTime date);
void main() {
// Dart的本地时间
DateTime dartLocalDate = DateTime.now();
print('Dart本地时间:$dartLocalDate'); // 比如输出2024-05-20 14:30:00
// 转成UTC时间传给JS
String jsResult = processDate(dartLocalDate.toUtc());
print('JS返回的UTC时间:$jsResult'); // 比如输出2024-05-20T06:30:00.000Z
}
对应的JS代码:
function processDate(date) {
// JS的Date转成UTC字符串
return date.toISOString();
}
四、应用场景、优缺点与注意事项
4.1 应用场景
Dart与JavaScript互操作的应用场景主要有以下几种:
- 复用已有的JavaScript代码:比如已经写好的复杂业务逻辑、第三方SDK(比如地图、支付、统计)、加密算法等,不用再用Dart重写,节省开发时间。
- 混合开发:比如用Flutter开发的应用,需要嵌入网页内容,网页里的JS代码需要和Flutter的Dart代码通信,比如点击网页里的按钮触发Flutter的页面跳转。
- 跨端能力扩展:比如Flutter的某些能力(比如调用浏览器的API)只有JS能实现,需要通过互操作来调用。
- 数据共享:比如网页端和Flutter端共享同一份数据,通过互操作来传递。
4.2 技术优缺点
优点
- 代码复用:不用重写已有的JS代码,节省开发成本和时间。
- 能力扩展:可以利用JS的丰富生态和浏览器API,弥补Dart的不足。
- 灵活开发:可以根据业务需求选择最合适的语言开发,比如复杂的UI用Flutter(Dart),复杂的逻辑用JS。
缺点
- 性能损耗:互操作需要做类型转换和数据拷贝,会有一定的性能损耗,尤其是传大对象、大数组的时候,性能损耗更明显。
- 类型安全问题:因为两种语言的类型体系不同,互操作时容易出现类型不匹配的问题,调试难度大。
- 调试难度大:跨语言的代码调试比单一语言的调试难很多,比如Dart代码调用JS代码出错,很难定位是Dart的问题还是JS的问题。
- 版本兼容问题:不同版本的Dart和JS的互操作规则可能会有变化,升级版本的时候容易出现兼容问题。
4.3 注意事项
- 尽量传基础类型:传数据的时候,尽量传数字、字符串、布尔值这些基础类型,减少类型转换的损耗和出错的概率。
- 大对象转字符串:如果要传大对象,尽量转成JSON字符串传过去,避免对象拷贝的性能损耗。
- 统一时区:传日期时间的时候,统一用UTC时间或者时间戳,避免时区不一致导致的错误。
- 严格检查类型:传列表、对象的时候,一定要保证两边的类型一致,避免类型不匹配的错误。
- 处理空值:传参的时候要注意空值的处理,非空类型不能传null。
- 测试边界情况:测试的时候要注意边界情况,比如超大数字、空对象、空列表等。
五、文章总结
Dart与JavaScript互操作时的类型映射和数据转换,核心是理解两种语言的类型体系差异,掌握基础类型、复杂类型的映射规则和转换技巧。基础类型里要注意超大数字的精度问题,复杂类型里要注意列表、对象、日期时间的映射规则,尤其是对象的嵌套和暴露问题。
互操作的应用场景很广,能帮助我们复用代码、扩展能力,但也有性能损耗、类型安全、调试难度大等缺点,所以在使用的时候要根据业务需求合理选择,尽量减少互操作的使用,或者优化互操作的方式,减少性能损耗和出错的概率。
最后,大家在实际开发的时候,一定要多测试,尤其是边界情况,避免因为类型映射和数据转换的问题导致程序出错。
评论
围绕“Dart与JavaScript互操作时的类型映射与数据转换技巧”参与讨论