webf icon indicating copy to clipboard operation
webf copied to clipboard

Add FormData Support

Open andycall opened this issue 2 years ago • 25 comments

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 avatar Nov 29 '22 07:11 andycall

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

linsmod avatar Aug 06 '24 01:08 linsmod

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.

andycall avatar Aug 06 '24 14:08 andycall

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 avatar Aug 07 '24 11:08 linsmod

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

linsmod avatar Aug 07 '24 23:08 linsmod

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.

andycall avatar Aug 07 '24 23:08 andycall

我尝试添加了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

请指导一下。

linsmod avatar Aug 12 '24 07:08 linsmod

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

andycall avatar Aug 12 '24 07:08 andycall

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

andycall avatar Aug 12 '24 07:08 andycall

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

linsmod avatar Aug 12 '24 08:08 linsmod

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

linsmod avatar Aug 12 '24 08:08 linsmod

写一个 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 avatar Aug 12 '24 08:08 andycall

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

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

andycall avatar Aug 12 '24 08:08 andycall

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

linsmod avatar Aug 12 '24 08:08 linsmod

https://github.com/openwebf/webf/blob/main/bridge/polyfill/src/xhr.ts

andycall avatar Aug 12 '24 08:08 andycall

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

linsmod avatar Aug 12 '24 08:08 linsmod

我尝试在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<BlobPart>,

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

linsmod avatar Aug 12 '24 13:08 linsmod

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);
  }

andycall avatar Aug 12 '24 15:08 andycall

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

linsmod avatar Aug 13 '24 01:08 linsmod

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

andycall avatar Aug 13 '24 01:08 andycall

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

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

https://github.com/openwebf/webf/issues/642 npm run test出错。libwebf.so: undefined symbol: initTestFramework

@andycall

linsmod avatar Aug 13 '24 09:08 linsmod

测试需要在 macOS 平台运行。

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

andycall avatar Aug 13 '24 10:08 andycall

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

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

linsmod avatar Aug 13 '24 10:08 linsmod

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

andycall avatar Aug 13 '24 10:08 andycall

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

Pull request created: https://github.com/openwebf/webf/pull/645

linsmod avatar Aug 13 '24 14:08 linsmod

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

andycall avatar Aug 14 '24 08:08 andycall