一、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互操作的应用场景主要有以下几种:

  1. 复用已有的JavaScript代码:比如已经写好的复杂业务逻辑、第三方SDK(比如地图、支付、统计)、加密算法等,不用再用Dart重写,节省开发时间。
  2. 混合开发:比如用Flutter开发的应用,需要嵌入网页内容,网页里的JS代码需要和Flutter的Dart代码通信,比如点击网页里的按钮触发Flutter的页面跳转。
  3. 跨端能力扩展:比如Flutter的某些能力(比如调用浏览器的API)只有JS能实现,需要通过互操作来调用。
  4. 数据共享:比如网页端和Flutter端共享同一份数据,通过互操作来传递。

4.2 技术优缺点

优点

  1. 代码复用:不用重写已有的JS代码,节省开发成本和时间。
  2. 能力扩展:可以利用JS的丰富生态和浏览器API,弥补Dart的不足。
  3. 灵活开发:可以根据业务需求选择最合适的语言开发,比如复杂的UI用Flutter(Dart),复杂的逻辑用JS。

缺点

  1. 性能损耗:互操作需要做类型转换和数据拷贝,会有一定的性能损耗,尤其是传大对象、大数组的时候,性能损耗更明显。
  2. 类型安全问题:因为两种语言的类型体系不同,互操作时容易出现类型不匹配的问题,调试难度大。
  3. 调试难度大:跨语言的代码调试比单一语言的调试难很多,比如Dart代码调用JS代码出错,很难定位是Dart的问题还是JS的问题。
  4. 版本兼容问题:不同版本的Dart和JS的互操作规则可能会有变化,升级版本的时候容易出现兼容问题。

4.3 注意事项

  1. 尽量传基础类型:传数据的时候,尽量传数字、字符串、布尔值这些基础类型,减少类型转换的损耗和出错的概率。
  2. 大对象转字符串:如果要传大对象,尽量转成JSON字符串传过去,避免对象拷贝的性能损耗。
  3. 统一时区:传日期时间的时候,统一用UTC时间或者时间戳,避免时区不一致导致的错误。
  4. 严格检查类型:传列表、对象的时候,一定要保证两边的类型一致,避免类型不匹配的错误。
  5. 处理空值:传参的时候要注意空值的处理,非空类型不能传null。
  6. 测试边界情况:测试的时候要注意边界情况,比如超大数字、空对象、空列表等。

五、文章总结

Dart与JavaScript互操作时的类型映射和数据转换,核心是理解两种语言的类型体系差异,掌握基础类型、复杂类型的映射规则和转换技巧。基础类型里要注意超大数字的精度问题,复杂类型里要注意列表、对象、日期时间的映射规则,尤其是对象的嵌套和暴露问题。

互操作的应用场景很广,能帮助我们复用代码、扩展能力,但也有性能损耗、类型安全、调试难度大等缺点,所以在使用的时候要根据业务需求合理选择,尽量减少互操作的使用,或者优化互操作的方式,减少性能损耗和出错的概率。

最后,大家在实际开发的时候,一定要多测试,尤其是边界情况,避免因为类型映射和数据转换的问题导致程序出错。