在 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 库调用方法可能会出现某些复杂情况。在这种情况下,我们可以尝试两种选择:
- 在 dart 端存储 IFrame 上下文,然后使用
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'),
),
);
}
}
我需要 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 库调用方法可能会出现某些复杂情况。在这种情况下,我们可以尝试两种选择:
- 在 dart 端存储 IFrame 上下文,然后使用
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'),
),
);
}
}