Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FormData Support #164

Open
andycall opened this issue Nov 29, 2022 · 25 comments
Open

Add FormData Support #164

andycall opened this issue Nov 29, 2022 · 25 comments
Labels
enhancement New feature or request

Comments

@andycall
Copy link
Member

Feature type

DOM API

The spec link.

https://kapeli.com/dash_share?docset_file=JavaScript&docset_name=JavaScript&path=developer.mozilla.org/en-US/docs/Web/API/FormData.html&platform=javascript&repo=Main&source=developer.mozilla.org/en-US/docs/Web/API/FormData

How much important for you

No response

@andycall andycall added the enhancement New feature or request label Nov 29, 2022
@linsmod
Copy link

linsmod commented Aug 6, 2024

Hi there, I'm porting an vue app to webf however http errors stop me. Seems like FormData is not implemented currently, do we have a plan
to implement it or if any beta version supports FormData?

E:authClient 'FormData' is not defined     at <anonymous> (<input>:177:27)
    at Promise (native)
    at http (<input>:218:12)
    at call (native)
    at process (<input>:355:27)
    at authClient (<input>:135:17)
    at connect (<input>:112:25)
    at $httpGuard (<input>:422:24)
    at process (<input>:355:72)
    at listFiles (<input>:468:17)
    at refresh (<input>:166:25)
    at mounted (<input>:85:10)
    at <anonymous> (<input>:2716:94)
    at callWithErrorHandling (<input>:353:5)
    at callWithAsyncErrorHandling (<input>:356:17)
    at <anonymous> (<input>:2698:19)
    at flushPostFlushCbs (<input>:514:7)
    at flushJobs (<input>:550:5)
    at flushJobs (<input>:554:7)
 177 27

@andycall
Copy link
Member Author

andycall commented Aug 6, 2024

We do not have a plan to support this from now on

Being a sponsor for this project can enhance your desired features compared to other projects we are currently working on.

@linsmod
Copy link

linsmod commented Aug 7, 2024

Fine, thank you for you reply. I decide to implement formData in webF by my side. Due to the poor code skill of mine on Dart ( the first meet between Dart and I ), I made it hard with much help of AI suggestion. I wish I could issue a pull request to webF if I can get some advice/help from you webF authors about how to follow your archetectures or rules.

arraybuffer.dart

import 'dart:typed_data';

class ArrayBufferData {
  Uint8List buffer;

  ArrayBufferData(this.buffer);

  factory ArrayBufferData.fromBytes(Uint8List bytes) {
    return ArrayBufferData(bytes);
  }

  int get length => buffer.length;
}

blob.dart

import 'dart:typed_data';
import 'dart:convert';

import 'arraybuffer.dart';

/// A simplified version of the Blob class for demonstration purposes.
class Blob {
  Blob(this._data, [this.type = 'application/octet-stream']);

  final Uint8List _data;
  final String type;

  int get size => _data.length;

  Uint8List get bytes => _data;

  String StringResult() {
    return String.fromCharCodes(_data);
  }

  String Base64Result() {
    return 'data:$type;base64,${base64Encode(_data)}';
  }

  ArrayBufferData ArrayBufferResult() {
    return ArrayBufferData(_data.buffer.asUint8List());
  }

  Blob slice(int start, int end, [String contentType = '']) {
    if (start < 0) start = 0;
    if (end < 0) end = 0;
    if (end > _data.length) end = _data.length;
    if (start > end) start = end;
    final slicedData = _data.sublist(start, end);
    return Blob(slicedData, contentType);
  }
}

formdata.dart

import 'dart:convert';

import 'package:webf/webf.dart';

import 'blob.dart';

/// A simple implementation of the FormData interface.
class FormData extends BaseModule {
  /// Creates a new FormData instance.
  FormData(ModuleManager? moduleManager) : super(moduleManager);

  /// The list that holds the data.
  final List<List<dynamic>> _list = [];

  /// Adds a key-value pair to the FormData.
  void append(String name, value) {
    if (value is Blob) {
      // Handle Blob type.
      _list.add([name, value]);
    } else {
      // Handle other types.
      _list.add([name, value]);
    }
  }

  /// Returns the first value associated with the given key.
  dynamic getFirst(String name) {
    for (var entry in _list) {
      if (entry[0] == name) {
        return entry[1];
      }
    }
    return null;
  }

  /// Returns all values associated with the given key.
  List<dynamic> getAll(String name) {
    return _list.where((entry) => entry[0] == name).map((entry) => entry[1]).toList();
  }

  /// Serializes the FormData into a string.
  @override
  String toString() {
    var entries = _list.map((entry) {
      if (entry[1] is String) {
        return '${Uri.encodeComponent(entry[0])}=${Uri.encodeComponent(entry[1] as String)}';
      } else if (entry[1] is Blob) {
        // Handle Blob serialization.
        return '${Uri.encodeComponent(entry[0])}=${Uri.encodeComponent(entry[1].Base64Result())}';
      } else {
        return '${Uri.encodeComponent(entry[0])}=${Uri.encodeComponent(jsonEncode(entry[1]))}';
      }
    }).join('&');

    return entries;
  }

  @override
  String invoke(String method, params, InvokeModuleCallback callback) {
    switch (method) {
      case 'append':
        append(params[0], params[1]);
        break;
      case 'getFirst':
        return jsonEncode(getFirst(params[0]));
      case 'getAll':
        return jsonEncode(getAll(params[0]));
      case 'toString':
        return toString();
      default:
        print('Failed to execute \'$method\' on \'fromData\': NoSuchMethod ');
    }
    return EMPTY_STRING;
  }

  @override
  void dispose() {}

  @override
  String get name => 'FormData';
}

module_manager.dart: one line change.

void _defineModuleCreator() {
  if (_isDefined) return;
  _isDefined = true;
  [...]
  _defineModule((ModuleManager? moduleManager) => FormData(moduleManager));
}

@linsmod
Copy link

linsmod commented Aug 7, 2024

After add code listed above, still say 'FormData' is not defined. I'm no idea about how a js object registered in webF.

@andycall
Copy link
Member Author

andycall commented Aug 7, 2024

Adding a FormData API in JavaScript must be done with C++ bindings.

The Blob JavaScript API is a similar case to the FormData API.

You need to store the underlying byte data of FormData in C++ and send it to Dart via Dart FFI.

Then, append this byte data into the networking request, as described in the GitHub repository.

@linsmod
Copy link

linsmod commented Aug 12, 2024

我尝试添加了formData和arrayBuffer,并且编译通过。
(formData还没有添加到请求的内容里面, 因为我那个vue程序好像可以new FormData了,暂时就没有管它了)

现在在测试arrayBuffer,但是用js测试new出来的对象似乎类型不对。

ts这样写的:

import {webf} from './webf';

export class ArrayBuffer{
    private id:string;
    private byteLength?:number=0;
    constructor(byteLength?:number){
        this.byteLength=byteLength;
        this.id = webf.invokeModule('ArrayBufferData', 'init',[byteLength]);
    }
    public slice(start:number,end:number):void{
        webf.invokeModule('ArrayBufferData','slice',[this.id,start,end]);
    }
    public toString():string{
        return webf.invokeModule('ArrayBufferData','toString',[this.id]);
    }
}

测试代码这样写的:

 const buffer = new ArrayBuffer(16);
 const view = new DataView(buffer);

错误是这样的:

TypeError: ArrayBuffer object expected
    at DataView (native)
    at testBlobArrayBufferDataView (assets:assets/bundle.js:34:18)

我将本地的提交生成了一个patch文件

0001-WIP-Implement-fromdata-and-arraybuffer.zip

测试的代码附加在原有的example里面。
image

请指导一下。

@andycall
Copy link
Member Author

arrayBuffer 是 ecma 标准支持的对象,quickjs 也支持了,不需要单独支持

@andycall
Copy link
Member Author

而且通过 C++ 实现的话,是不需要在 polyfill 里新增任何代码

@linsmod
Copy link

linsmod commented Aug 12, 2024

确实没有很清晰使用了polyfill,C++,又混合dart的时候,对象实现是如何注册到qjs里面。

@linsmod
Copy link

linsmod commented Aug 12, 2024

那如果实现FormData,必须要实现一个qjs_form_data是吗?

@andycall
Copy link
Member Author

写一个 TypeScript Typings,比如 blob.d.ts,生成器会生成与 QuickJS 交互的胶水代码,然后实现 form_data.h 和 form_data.cc,需要继承 BindingObject,这样才可以创建一个用于在 JavaScript 环境内使用的 C++ 对象,还能在 Dart 那边同步访问,最后别忘了在这里注册:binding_initializer.cc

搞定上面一切后,JS 的全局环境就会出现你添加的类,直接在 JS 中调用即可。然后在 JS 里调用了 FormData 返回的 JS 对象可以传递到 JS 写的 Fetch API,并体现在 invokeModule 的参数内,这时候在 C++ 那边提取参数时,就能找到 FormData 创建的 JS 对象。由于它是 BindingObject,可以直接转成 Pointer 发送到 Dart 那边。

然后你需要写一个 Dart 版本的 FormData 对象,记得继承 DynamicBindingObject。

其他的一些参考,比如 CanvasGradient 对象,以及 [Dart 那边的对应实]现](https://github.com/openwebf/webf/blob/main/webf/lib/src/html/canvas/canvas_context.dart#L138)

那如果实现FormData,必须要实现一个qjs_form_data是吗?

这是生成器生成的,不需要手动写

@andycall
Copy link
Member Author

生成器是借鉴了 Chrome 的 WebIDL 思路,使用 TypeScript Typing 定义和 TypeScript API 定制的,代码在这里:

https://github.com/openwebf/webf/tree/main/bridge/scripts/code_generator

@linsmod
Copy link

linsmod commented Aug 12, 2024

好的。Fetch API和xmlHttpRequest,在webf中是共享的实现代码吗?好像没有看到xmlHttpRequest。

@andycall
Copy link
Member Author

@linsmod
Copy link

linsmod commented Aug 12, 2024

好的。那如果添加了FormData, 估计xhr这里也要适配一下。

@linsmod
Copy link

linsmod commented Aug 12, 2024

我尝试在formData中使用BlobParts,

// type FormDataEntryValue = File | string;
export interface FormData {
    new():FormData;
    append(name: string, value: BlobPart, fileName?: string): void;
    del(name: string): void;
    get(name: string): BlobPart
    getAll(name: string): BlobPart[];
    has(name: string): boolean;
    set(name: string, value: BlobPart, fileName?: string): void;
    forEach(callbackfn: (value: BlobPart, key: string, parent: FormData) => void, thisArg?: any): void;
}

但是由于它的ImplType不是BlobPart*,而是一个std_shared_ptr,

class BlobPart {
 public:
  using ImplType = std::shared_ptr<BlobPart>;

那么默认的ConverterImpl在绑定getAll的时候就转换不过去

template <>
struct Converter<BlobPart> : public ConverterBase<BlobPart> {
  using ImplType = BlobPart::ImplType;
  static ImplType FromValue(JSContext* ctx, JSValue value, ExceptionState& exception_state) {
    assert(!JS_IsException(value));
    return BlobPart::Create(ctx, value, exception_state);
  }

  static JSValue ToValue(JSContext* ctx, BlobPart* data) {
    if (data == nullptr)
      return JS_NULL;

    return data->ToQuickJS(ctx);
  }
};

那遇到这样的情况,我认为可以修改converter来支持,也可以创建一个类似blobPart的类型且它的ImplType是其本身指针来解决,不知道这样理解是否正确,有更好的方案吗?

image

@andycall
Copy link
Member Author

template <>
struct Converter<BlobPart> : public ConverterBase<BlobPart> {
  using ImplType = BlobPart::ImplType;
  static ImplType FromValue(JSContext* ctx, JSValue value, ExceptionState& exception_state) {
    assert(!JS_IsException(value));
    return BlobPart::Create(ctx, value, exception_state);
  }

  static JSValue ToValue(JSContext* ctx, BlobPart* data) {
    if (data == nullptr)
      return JS_NULL;

    return data->ToQuickJS(ctx);
  }
};

你的想法没错,这里的实现有问题,应该把 ToValue 的参数改成:

static JSValue ToValue(JSContext* ctx, std::shared_ptr<BlobPart> data) {
    if (data == nullptr)
      return JS_NULL;

    return data->ToQuickJS(ctx);
  }

@linsmod
Copy link

linsmod commented Aug 13, 2024

回调函数在d.ts里面用什么表示,NativeTypeFunction?

@andycall
Copy link
Member Author

declare const setInterval: (callback: Function, timeout?: double) => int64;

@linsmod
Copy link

linsmod commented Aug 13, 2024

FormData已经实现,现在需要执行单元测试。

我这边尝试了npm run test但是测试框架好像配置有问题,简单看了一下不知道如何解决,方便看一下吗

#642 npm run test出错。libwebf.so: undefined symbol: initTestFramework

@andycall

@andycall
Copy link
Member Author

测试需要在 macOS 平台运行。

可以加到这里 https://github.com/openwebf/webf/tree/main/integration_tests/specs/xhr

@linsmod
Copy link

linsmod commented Aug 13, 2024

目前只是测试FormData的api,FFI我还不会,不知道怎么弄到fetchModule里面。

我想是否能在FormData上面添加一个getBytes(), 在xhr.ts里面调用一下,data就自动变成一个dart里面的Uint8List?

@andycall
Copy link
Member Author

看了下,底层的 FFI 通信现在还只支持从 Dart 发送 Uint8List 到 C++,反过来还没实现,你先把 PR 提交一下,我在你的分支上补充这块

@linsmod
Copy link

linsmod commented Aug 13, 2024

你先把 PR 提交一下,我在你的分支上补充这块

Pull request created: #645

@andycall
Copy link
Member Author

@linsmod done,看下 PR,通信的例子已经补充上了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants