前言
由于Flutter在双端的开发体验几乎接近的情况下,尝试将Flutter代码转Web端进行部署和发布,在其中遇到的所有问题,我都会通过这种方式分享出来。那么第一个要解决的就是上传资源到TOS上,在双端中都是通过插件的方式在各端通过插件使用不同的SDK去解决上传问题,那么为了改动最小化
,web端同样希望使用插件的方式去解决上传问题。
官方链接
tos使用Browser.js SDK去进行上传
https://www.volcengine.com/docs/6349/127737
这次使用的是普通上传的方式
https://www.volcengine.com/docs/6349/127739
分析
将过程进行拆解
流程分析
总流程
- Flutter端通过请求获取临时密钥
- 将密钥交付给web插件,web插件完成初始化工作
- 用户触发文件选择,将文件交付给web插件
- web插件获取到文件以及完成初始化工作之后,使用tos sdk发出请求
- 将结果反馈给Flutter端,完成流程
问题分析
文件上传问题
在双端,可以通过文件选择器file_picker
去触发系统的文件管理,这种方式可以获取到临时文件路径,当然也可以通过写入应用沙箱环境获取应用沙箱文件路径,将路径传递给插件处理即可,但是,在web端,本质是处于浏览器环境,浏览器环境是不允许直接访问本地文件系统的,也就无法直接获取到任何路径。,我们使用原生html的input上传文件也可以进行验证,能够得到的是一个File对象,再者,web端不仅仅包括H5,还包括各家小程序,不同小程序提供的web环境,文件系统的管理是受各自小程序的管控的,因此这项工作应该交给web插件去处理,判断具体的环境情况,是获取File对象还是通过不同小程序提供的开放能力获取临时文件路径。
Dart和JS互相调用
Dart是强语言,JS是弱语言,没有类型的概念,那么我写了JS插件之后,如何在Dart层提前感知到相应的插件。
Flutter SDK在3.3.0之后,推出两个标准库去处理这样的问题。
JavaScript interop
:基于扩展类型的新 JS 互操作机制
,当针对 JavaScript 和 Wasm 时,可以在 Dart 代码、浏览器 API 和 JS 库之间进行简洁、类型安全的调用,Dart 开发人员可以访问类型化 API 来与 JavaScript 交互
,API 通过静态强制明确定义了两种语言之间的边界,在编译之前消除了许多问题。
package:web
:能够直接操作dom相关。
我们需要使用这两个库去解决问题。
编写插件
创建web插件模版
使用命令
flutter create --template=plugin --platforms=web .
这个时候可能会得到一个错误
Ambiguous organization in existing files: {com.example}. The --org command line argument must be specified to recreate project.
这个时候是因为当前的库有了其他端的测试用例,重新生成example文件夹即可。
这个时候会在lib目录下面生成一个文件。
根据个人习惯,我也阅读了其他插件,都会把这份文件放在lib的src目录下面。
配置pubspec.yaml文件
在dependencies下面加入:
同时web的依赖也要添加,便于我们后续操作dom
dependencies:
、、、
web: ^1.1.0
flutter_web_plugins:
sdk: flutter
、、、
运行测试
我们在example
文件夹下面点击lib的main.dart
选择chrome进行运行即可。
选择文件
我们实际的逻辑就在生成的文件下进行编写。
我们首先解决选择文件的问题。
这里仅提供逻辑参考,假如在h5端,本质就是创建一个input元素,模拟点击即可。在小程序就使用其他端的sdk去选择文件,得到的是File对象,比如在小程序,就是wx.chooseMedia的方式直接获取到临时路径
// wx.chooseMedia({
// count: 9,
// mediaType: ['image','video'],
// sourceType: ['album', 'camera'],
// maxDuration: 30,
// camera: 'back',
// success(res) {
// console.log(res.tempFiles[0].tempFilePath)
// console.log(res.tempFiles[0].size)
// }
// })
Future<dynamic> pickWebFile() async {
// 判断是否在微信小程序环境
if (isWeChatMiniProgram()) {
// 微信小程序逻辑处理
return await pickFileInWeChatMiniProgram();
} else {
// 非微信小程序环境下,使用浏览器的文件选择器
final input = web.document.createElement('input') as web.HTMLInputElement;
input.type = 'file'; // 设置为文件选择类型
input.accept = '*/*'; // 可选:限制选择的文件类型,例如 'image/*' 或 '.txt'
input.click();
// 等待文件选择完成
await input.onChange.first;
if (input.files != null) {
// 返回选中的第一个文件
return input.files!;
}
}
// 如果未选择文件或处理失败,返回 null
return null;
}
上传文件
JS插件
这里我是根据官方文档去编写的js代码
这里我创建了一个类
- 提供一个初始化方法init函数
- 提供获取当前web环境的函数
- 提供一个上传文件的方法uploadFiles函数
class TosPluginJx {
static instance; // 定义静态属性来保存单例实例
constructor(client, dir, host) {
this._client = client;
this._dir = dir;
this._host = host;
}
get host() {
return this._host;
}
set host(host) {
this._host = host;
}
get client() {
return this._client;
}
set client(client) {
this._client = client;
}
get dir() {
return this._dir;
}
set dir(dir) {
this._dir = dir;
}
static getInstance() {
if (!TosPluginJx.instance) {
TosPluginJx.instance = new TosPluginJx();
}
return TosPluginJx.instance;
}
checkOut(data) {
if (!data.accessKeyId) {
return false
}
if (!data.secretAccessKey) {
return false
}
if (!data.sessionToken) {
return false
}
if (!data.region) {
return false
}
if (!data.bucket) {
return false
}
if (!data.dir) {
return false
}
if (!data.host) {
return false
}
return true
}
init(data) {
if (!this.checkOut(data)) {
console.log("tos_sdk_初始化失败", data)
return false
}
this.dir = data.dir
this.host = data.host
this.client = new TOS({
// 从 STS 服务获取的临时访问密钥 AccessKeyId
accessKeyId: data.accessKeyId,
// 从 STS 服务获取的临时访问密钥 AccessKeySecret
accessKeySecret: data.secretAccessKey,
// 从 STS 服务获取的安全令牌 SessionToken
stsToken: data.sessionToken,
// 填写 Bucket 所在地域。以华北2(北京)为例,Region 填写为cn-beijing
region: data.region,
// 填写 Bucket 名称
bucket: data.bucket,
});
console.log("tos_sdk_初始化完成", this.client, data)
return true
}
//获取当前web环境
getEnv() {
if (window.WeixinJSBridge) {
return 'wxMiniProgram'
}
return 'h5'
}
/// 上传文件
//返回一个数组回去
async uploadFiles(data) {
try {
console.log("开始上传", data)
console.log("上传路径为", this.dir)
if (data.length == 0) {
console.log("当前文件为空")
return [{
fileStr: '',
uuid: data[0].uuid,
downloadUrl: '',
isCompleted: true,
msg: '当前文件为空',
code: 0
}];
}
if (data[0].fileStr) {
console.log("当前为文件路径", data[0].fileStr)
const result = await this.client.putObject({
key: this.dir + data[0].fileStr,
body: data[0].fileStr ? data[0].fileStr : data[0].fileBlob,
// headers,
});
console.log("上传结果", result)
if (result.statusCode == 200) {
return [{
fileStr: '',
uuid: data[0].uuid,
downloadUrl: this.host + this.dir + data[0].fileStr,
isCompleted: true,
msg: '上传成功',
code: 1
}];
}
} else if (data[0].fileBlob) {
console.log("当前为文件对象", data[0].fileBlob)
const result = await this.client.putObject({
key: this.dir + data[0].fileBlob.name,
body: data[0].fileBlob,
});
console.log("上传结果", result)
if (result.statusCode == 200) {
return [{
fileStr: '',
uuid: data[0].uuid,
downloadUrl: this.host + this.dir + data[0].fileBlob.name,
isCompleted: true,
msg: '上传成功',
code: 1
}];
}
}
return [{
fileStr: '',
uuid: data[0].uuid,
downloadUrl: '',
isCompleted: true,
msg: '上传失败',
code: 0
}];
} catch (e) {
console.log(e)
return [{
fileStr: '',
uuid: data[0].uuid,
isCompleted: true,
downloadUrl: '',
msg: '上传失败,' + e,
code: 0
}];
}
}
}
globalThis.TosPluginJx = TosPluginJx.getInstance(); //对外暴露单例,这里非常重要
Flutter端
在web编写插件,不像ios或者安卓,有channal管道的概念。
但是我们需要提前声明我们要使用的Js类以及函数以及我们需要使用的数据类型
这里我声明了我要使用的类TosPluginJx,里面有三个函数,分别接收什么类型的参数,返回什么类型的数据
我们使用的就是JavaScript interop
提供的能力。
还记得我们上面暴露到全局的TosPluginJx
吗,在这里,通过注解@JS
去进行标注,告诉Flutter层在web环境中有这样的类。
在Js中类的本质就是一个object,将所有类型定义出来。
//声明tos要获取的数据类型
extension type TosInfoType._(JSObject _) implements JSObject {
external JSString accessKeyId;
external JSString secretAccessKey;
external JSString sessionToken;
external JSString host;
external JSString region;
external JSString endpoint;
external JSString bucket;
external JSString dir;
//转换层
factory TosInfoType.fromMap(Map param) {
final obj = JSObject();
return TosInfoType._(obj)
..accessKeyId = param['AccessKeyId']
..bucket = param['Bucket']
..dir = param['_dir_']
..endpoint = param['Endpoint']
..host = param['Host']
..region = param['Region']
..secretAccessKey = param['SecretAccessKey']
..sessionToken = param['SessionToken'];
}
}
//声明要获取的文件类型
extension type FileInfoType._(JSObject _) implements JSObject {
external JSString uuid;
external JSString? fileStr;
external JSAny? fileBlob;
external JSAny? ext;
external JSString? downloadUrl;
external JSBoolean? isCompleted;
external JSString? msg;
external JSNumber? code;
//转换层
factory FileInfoType.fromMap(Map param) {
final obj = JSObject();
return FileInfoType._(obj)
..uuid = param['uuid']
..fileStr = param['fileStr']
..fileBlob = param['fileBlob']
..ext = param['ext']
..downloadUrl = param['downloadUrl']
..isCompleted = param['isCompleted']
..msg = param['msg']
..code = param['code'];
}
//转成map
Map toMap() {
return {
'uuid': uuid,
'fileStr': fileStr,
'fileBlob': fileBlob,
'ext': ext,
'downloadUrl': downloadUrl,
'isCompleted': isCompleted,
'msg': msg,
'code': code
};
}
}
//声明要操作的类
extension type TosPluginJx._(JSObject _) implements JSObject {
external TosPluginJx();
external JSBoolean init(TosInfoType tosInfo);
external JSPromise<JSArray<FileInfoType>> uploadFiles(
JSArray<FileInfoType> fileInfo);
external JSString getEnv();
}
@JS('window.TosPluginJx') //标识全局对象
external TosPluginJx get tosPluginJx;
调用js sdk
在上面我们已经定义好了TosPluginJx
类所涉及的所有需要使用的类型、方法、返回值。
现在我们可以直接进行使用。
需要注意一点,从js层拿到的是js类型,需要通过toDart
进行转换
反之通过toJS
//初始化
tosPluginJx.init(info);
//获取环境
String current = tosPluginJx.getEnv().toDart;
//上传文件
//将客户端传过来的参数转成js
final jsFileArray =
files.map((file) => FileInfoType.fromMap(file)).toList().toJS;
print(jsFileArray);
//传递给js插件层
JSArray<FileInfoType> result =
await tosPluginJx.uploadFiles(jsFileArray).toDart;
//拿到结果再转换dart层
List finl = result.toDart.map((e) => e.toMap()).toList();
打包测试
由于上传可能有tos的跨域问题,所以我们打包上传之后再进行传输。
执行命令
flutter build web
注意:生成的index.html要把base的href进行修改,否则找不到资源
<base href="">
结论
至此,我们完成Flutter_web插件的编写,Flutter提供了两个库去操作dom和js层,但我们的场景是需要使用第三方的js,因此又涉及到了dart层和js层互相调用的地方,两者在类型不同的情况下,如何做到类型兼容,下一篇将解决Flutter转web之后,webview是如何处理的,数据是如何交互的。
如果有更好的想法,欢迎提出。