在 flutter web 中使用 js 库

Use js library in flutter web

我需要 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')";
           });

我怎样才能让它发挥作用?

由于BpmnJScontainer参数接受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 库调用方法可能会出现某些复杂情况。在这种情况下,我们可以尝试两种选择:

  1. 在 dart 端存储 IFrame 上下文,然后使用 callMethod 保存的上下文。
  2. 使用postMessage方法与IFrame
  3. 通信
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'),
      ),
    );
  }
}