在 Flutter Web 中使用 js 库

Posted

技术标签:

【中文标题】在 Flutter Web 中使用 js 库【英文标题】:Use js library in flutter web 【发布时间】:2020-06-02 04:41:15 【问题描述】:

我需要带有 bpmn.js 视图的小部件:https://github.com/bpmn-io/bpmn-js

使用 htmlElementView:

    // ignore: undefined_prefixed_name
    ui.platformViewRegistry
        .registerViewFactory('bpmn_view', (int viewId) => element);

    return Column(
      children: <Widget>[
        Expanded(
            child: HtmlElementView(key: UniqueKey(), viewType: "bpmn_view")),
      ],
    );

带js:

    const html = '''
    <div id="canvas">canvas</div>
    <script>
      (function () 
        window.addEventListener('view_bpmn', function (e) 
           var bpmnJS = new BpmnJS(
               container: "#canvas"
           );

           bpmnJS.importXML(e.details);
         , false);
      ());
    </script>
    ''';

    element.setInnerHtml(html,
        validator: NodeValidatorBuilder.common()..allowElement('script'));

但执行时出现错误:

VM4761 bpmn-viewer.development.js:18864 Uncaught TypeError: Cannot read property 'appendChild' of null
    at Viewer.BaseViewer.attachTo (VM4761 bpmn-viewer.development.js:18864)
    at Viewer.BaseViewer._init (VM4761 bpmn-viewer.development.js:18911)
    at Viewer.BaseViewer (VM4761 bpmn-viewer.development.js:18454)
    at new Viewer (VM4761 bpmn-viewer.development.js:19082)
    at <anonymous>:3:25
    at main.dart:185
    at future.dart:316
    at internalCallback (isolate_helper.dart:50)

而且我无法为 BpmnJS 设置选择器,例如:

 var bpmnJS = new BpmnJS(
               container: "document.querySelector('flt-platform-view').shadowRoot.querySelector('#canvas')";
           );

我怎样才能让它工作?

【问题讨论】:

您为什么尝试使用 viewType 访问。由于在您的影子 dom 内,画布 id 是 canvas 使用类似这样的东西。document.querySelector('flt-platform-view').shadowRoot.querySelector('#canvas'); 啊,好的。谢谢。我对此进行更改和编辑。 我想我只是使用 iframe 元素,但我想知道如何在 Flutter Web 中正确使用它。 以我的拙见,Iframe 不是一个好的解决方案。尤其是当颤振重建 HTMLElementView 小部件时,它甚至可能完全重新渲染 iframe 例如当调整大小时。主应用程序和 Iframe 之间的通信也可能是另一个绘制点。我将在今天晚些时候尝试这个问题。 【参考方案1】:

由于 BpmnJS container 参数接受 DOMElement 类型值,我们可以直接传递 querySelector 的结果:

    _element = html.DivElement()
      ..id = 'canvas'
      ..append(html.ScriptElement()
        ..text = """
        const canvas = document.querySelector("flt-platform-view").shadowRoot.querySelector("#canvas");
        const viewer = new BpmnJS( container: canvas );
        """);

    // ignore: undefined_prefixed_name
    ui.platformViewRegistry
        .registerViewFactory('bpmn-view', (int viewId) => _element);

BpmnJS 模块应该附加到 index.html 文件(在您项目的*** web 文件夹中):

<!DOCTYPE html>
<head>
  <title>BpmnJS Demo</title>
  <script defer src="main.dart.js" type="application/javascript"></script>
  <script src="https://unpkg.com/bpmn-js@6.4.2/dist/bpmn-navigated-viewer.development.js"></script>
</head>
...

这里是完整的代码:

import 'dart:ui' as ui;
import 'package:universal_html/html.dart' as html;
import 'package:flutter/material.dart';

class BpmnDemo extends StatefulWidget 
  @override
  _BpmnDemoState createState() => _BpmnDemoState();


class _BpmnDemoState extends State<BpmnDemo> 
  html.DivElement _element;

  @override
  void initState() 
    super.initState();

    _element = html.DivElement()
      ..id = 'canvas'
      ..append(html.ScriptElement()
        ..text = """
        const canvas = document.querySelector("flt-platform-view").shadowRoot.querySelector("#canvas");
        const viewer = new BpmnJS( container: canvas );
        const uri = "https://cdn.staticaly.com/gh/bpmn-io/bpmn-js-examples/dfceecba/url-viewer/resources/pizza-collaboration.bpmn";
        fetch(uri).then(res => res.text().then(xml => viewer.importXML(xml)));
        """);

    // ignore: undefined_prefixed_name
    ui.platformViewRegistry
        .registerViewFactory('bpmn-view', (int viewId) => _element);
  

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(),
      body: Center(
          child: HtmlElementView(key: UniqueKey(), viewType: "bpmn-view")),
    );
  


更新:

此示例显示如何从 dart 代码加载图表并使用 dart:js 库:

import 'dart:ui' as ui;
import 'dart:js' as js;
import 'package:universal_html/html.dart' as html;
import 'package:flutter/material.dart';

class BpmnDemo extends StatefulWidget 
  @override
  _BpmnDemoState createState() => _BpmnDemoState();


class _BpmnDemoState extends State<BpmnDemo> 
  html.DivElement _element;
  js.JsObject _viewer;

  @override
  void initState() 
    super.initState();
    _element = html.DivElement();
    _viewer = js.JsObject(
      js.context['BpmnJS'],
      [
        js.JsObject.jsify('container': _element)
      ],
    );
    // ignore: undefined_prefixed_name
    ui.platformViewRegistry.registerViewFactory('bpmn-view', (int viewId) => _element);
    loadDiagram('assets/pizza-collaboration.bpmn');
  

  loadDiagram(String src) async 
    final bundle = DefaultAssetBundle.of(context);
    final xml = await bundle.loadString(src);
    _viewer.callMethod('importXML', [xml]);
  

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(),
      body: Center(child: HtmlElementView(key: UniqueKey(), viewType: "bpmn-view")),
    );
  


更新 2:

HtmlElementView 使用IFrame 元素时,可能会出现从 js 库调用方法的某些复杂情况。在这种情况下,我们可以尝试两种选择:

    将 IFrame 上下文存储在 dart 端,然后将 callMethod 与保存的上下文一起使用。 使用postMessage方法与IFrame通信
import 'dart:ui' as ui;
import 'dart:js' as js;
import 'dart:html' as html;
import 'package:flutter/material.dart';

class IFrameDemoPage extends StatefulWidget 
  @override
  _IFrameDemoPageState createState() => _IFrameDemoPageState();


class _IFrameDemoPageState extends State<IFrameDemoPage> 
  html.IFrameElement _element;
  js.JsObject _connector;

  @override
  void initState() 
    super.initState();

    js.context["connect_content_to_flutter"] = (content) 
      _connector = content;
    ;

    _element = html.IFrameElement()
      ..style.border = 'none'
      ..srcdoc = """
        <!DOCTYPE html>
          <head>
            <script>
              // variant 1
              parent.connect_content_to_flutter && parent.connect_content_to_flutter(window)
              function hello(msg) 
                alert(msg)
              

              // variant 2
              window.addEventListener("message", (message) => 
                if (message.data.id === "test") 
                  alert(message.data.msg)
                
              )
            </script>
          </head>
          <body>
            <h2>I'm IFrame</h2>
          </body>
        </html>
        """;

    // ignore:undefined_prefixed_name
    ui.platformViewRegistry.registerViewFactory(
      'example',
      (int viewId) => _element,
    );
  

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(
        actions: [
          IconButton(
            icon: Icon(Icons.filter_1),
            tooltip: 'Test with connector',
            onPressed: () 
              _connector.callMethod('hello', ['Hello from first variant']);
            ,
          ),
          IconButton(
            icon: Icon(Icons.filter_2),
            tooltip: 'Test with postMessage',
            onPressed: () 
              _element.contentWindow.postMessage(
                'id': 'test',
                'msg': 'Hello from second variant',
              , "*");
            ,
          )
        ],
      ),
      body: Container(
        child: HtmlElementView(viewType: 'example'),
      ),
    );
  


【讨论】:

谢谢!我可以在没有fetch 操作的情况下将 xml 传递给查看器吗? 我加了一个例子。 优秀的答案!有没有办法从 JS 向 Dart 端发回消息? @nbloqs here 是 PayPal 反馈抖动的示例,但要使用大量方法,您必须使用 package:js(不是 dart:js 库)创建包装器。 @nbloqs 我在回答中添加了一些建议。

以上是关于在 Flutter Web 中使用 js 库的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Flutter web 上显示资产图像?

Flutter Web - 如何在桌面上的 Chrome 中测试 Flutter Web 应用程序打开?

如何使用 Flutter 与 ERC721 智能合约交互?

Flutter学习--第三方库&学习资料

Flutter学习--第三方库&学习资料

Flutter学习--第三方库&学习资料