【Flutter_Web】Flutter编译Web第一篇(插件篇):Flutter_web实现上传TOS上传资源,编写web插件

前言

由于Flutter在双端的开发体验几乎接近的情况下,尝试将Flutter代码转Web端进行部署和发布,在其中遇到的所有问题,我都会通过这种方式分享出来。那么第一个要解决的就是上传资源到TOS上,在双端中都是通过插件的方式在各端通过插件使用不同的SDK去解决上传问题,那么为了改动最小化,web端同样希望使用插件的方式去解决上传问题。

官方链接

tos使用Browser.js SDK去进行上传

bash 复制代码
https://www.volcengine.com/docs/6349/127737

这次使用的是普通上传的方式

bash 复制代码
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插件模版

使用命令

bash 复制代码
flutter create --template=plugin --platforms=web .

这个时候可能会得到一个错误

bash 复制代码
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

bash 复制代码
dependencies:
	、、、
  web: ^1.1.0
  flutter_web_plugins:
    sdk: flutter
   、、、

运行测试

我们在example文件夹下面点击lib的main.dart选择chrome进行运行即可。

选择文件

我们实际的逻辑就在生成的文件下进行编写。

我们首先解决选择文件的问题。

这里仅提供逻辑参考,假如在h5端,本质就是创建一个input元素,模拟点击即可。在小程序就使用其他端的sdk去选择文件,得到的是File对象,比如在小程序,就是wx.chooseMedia的方式直接获取到临时路径

bash 复制代码
//  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)
//   }
// })
bash 复制代码
  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函数
bash 复制代码
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,将所有类型定义出来。

bash 复制代码
//声明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

bash 复制代码
//初始化
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的跨域问题,所以我们打包上传之后再进行传输。

执行命令

bash 复制代码
flutter build  web

注意:生成的index.html要把base的href进行修改,否则找不到资源

bash 复制代码
    <base href="">

结论

至此,我们完成Flutter_web插件的编写,Flutter提供了两个库去操作dom和js层,但我们的场景是需要使用第三方的js,因此又涉及到了dart层和js层互相调用的地方,两者在类型不同的情况下,如何做到类型兼容,下一篇将解决Flutter转web之后,webview是如何处理的,数据是如何交互的。

如果有更好的想法,欢迎提出。

相关推荐
一只大侠的侠1 小时前
Flutter开源鸿蒙跨平台训练营 Day 10特惠推荐数据的获取与渲染
flutter·开源·harmonyos
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
renke33644 小时前
Flutter for OpenHarmony:色彩捕手——基于HSL色轮与感知色差的交互式色觉训练系统
flutter
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端