【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是如何处理的,数据是如何交互的。

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

相关推荐
GY-93几秒前
Flutter项目和鸿蒙平台的通信
flutter·harmonyos
呵呵哒( ̄▽ ̄)"32 分钟前
React-Cropper (#^.^#) 特制裁剪图片组件^_^+腾讯云
前端·react.js·前端框架
小彭努力中40 分钟前
13.在 Vue 3 中使用 ECharts 实现桑基图
前端·javascript·vue.js·echarts
2401_897605651 小时前
OpenAI进军实体机器人:GPT赋能的智能未来
前端·gpt·机器人
樊南1 小时前
【esp32&小程序】小程序篇02——连接git
javascript·git·小程序·typescript·gitee
风茫1 小时前
【useCallback Hook】在多次渲染中缓存组件中的函数,避免重复创建函数
前端·javascript·react.js·usecallback
ネф̶-イω8 小时前
登录、注册、忘记密码、首页HTML模板
前端·css·html
小殷要努力刷题!9 小时前
JavaWeb项目——如何处理管理员登录和退出——笔记
java·javascript·笔记·学习·servlet·javaweb·寒假
小李老笨了10 小时前
React的渲染流程
javascript·react.js·ecmascript
忘不了情10 小时前
react中,使用antd的Upload组件上传zip压缩包文件
前端·javascript·react.js