@ufjs/webview 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # @ufjs/webview
2
+
3
+ ## 0.1.3
4
+
5
+ - First release. `<web-view>` is one tag: a platform WebView on the app
6
+ (`webview_flutter`) and an iframe on the web, with the same `src`, navigation
7
+ and message props on both. Version 0.1.3 to line up with the rest of the fjs
8
+ packages; nothing before it was published.
9
+ - A `<web-view>` nested inside a `<scroll-view>` keeps its own gestures: the
10
+ page scrolls under the finger instead of the outer scroller stealing the
11
+ drag.
12
+ - `src` accepts an app-owned page from the project's `html/` directory
13
+ (`/html/guide.html`), alongside http URLs and the `asset://` files a module
14
+ ships. Requires `@ufjs/cli` >= 0.1.3, which serves and bundles that
15
+ directory.
16
+ - The module no longer writes a second copy of its files into the app's
17
+ `public/fjs-modules/`. `.fjs/modules/<name>/` is the only copy; the
18
+ `/fjs-modules/<name>/<file>` URL contract is unchanged.
19
+ - The Flutter tests actually compile now — a missing
20
+ `package:flutter/gestures.dart` import meant `flutter test` reported the file
21
+ as a failed "loading …" and the suite had never run.
22
+
23
+ ## Requirements
24
+
25
+ - `@ufjs/cli` >= 0.1.3 and `flutter_fjs` >= 0.1.3.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 flutter-js contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # @ufjs/webview
2
+
3
+ 把网页嵌进 fjs 页面:App 使用 Flutter WebView,Web 使用 iframe。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pnpm add @ufjs/webview
9
+ ```
10
+
11
+ 模块会被 fjs 自动发现和注册,不需要手动 import 组件或修改 Flutter host。
12
+
13
+ ## 用法
14
+
15
+ ```vue
16
+ <script setup lang="ts">
17
+ import { ref } from 'vue';
18
+
19
+ const lastMessage = ref('');
20
+
21
+ function onMessage(payload: string) {
22
+ lastMessage.value = payload;
23
+ }
24
+ </script>
25
+
26
+ <template>
27
+ <view class="page">
28
+ <web-view
29
+ class="frame"
30
+ src="https://example.com/terms"
31
+ @load="(payload) => console.log('loaded', payload)"
32
+ @error="(payload) => console.error('failed', payload)"
33
+ @message="onMessage"
34
+ />
35
+ <text>{{ lastMessage }}</text>
36
+ </view>
37
+ </template>
38
+
39
+ <style scoped>
40
+ .page { flex-grow: 1; }
41
+ .frame { flex-grow: 1; }
42
+ </style>
43
+ ```
44
+
45
+ `web-view` 是普通盒子,不会自动铺满页面,也不会覆盖兄弟节点。网页没有自然高度,
46
+ 请给它明确的 `height`、`flex-grow` 或其它有界父容器。
47
+
48
+ ## Props 和事件
49
+
50
+ | 名称 | 说明 |
51
+ |------|------|
52
+ | `src` | `http://` / `https://` 外部页面,或 `asset://<path>` 模块自带页面 |
53
+ | `@load` | 页面加载完成,载荷为 `{"src":"..."}` |
54
+ | `@error` | 主文档加载失败,载荷为 `{"src":"...","errMsg":"web-view load failed"}` |
55
+ | `@message` | 网页调用 `fjs.postMessage(string)`,载荷为 `{"data":"..."}` |
56
+
57
+ 事件载荷都是字符串。需要传对象时,在网页侧先使用 `JSON.stringify`。
58
+ `src` 变化会开始新一轮加载,旧页面的事件不会回派。
59
+
60
+ ## 模块自带页面
61
+
62
+ 模块的 `public/` 文件会自动打包。页面使用 `asset://`,同一个地址会按目标解析:
63
+
64
+ | 目标 | 实际位置 |
65
+ |------|----------|
66
+ | App dev | `http://<devHost>/modules/webview/<path>` |
67
+ | App release | Flutter asset `assets/fjs/modules/webview/<path>` |
68
+ | Web | `/fjs-modules/webview/<path>` |
69
+
70
+ 例如:
71
+
72
+ ```vue
73
+ <web-view src="asset://demo.html?q=hello#top" style="height: 320px" />
74
+ ```
75
+
76
+ release 会用不含 `?` / `#` 的路径查找 Flutter asset,再把 query 和 fragment 还原到
77
+ 页面 URL。因此页面初始脚本可以正常读取 `location.search` 和 `location.hash`,相对
78
+ CSS、JS、图片也仍以该 HTML 的目录为基准。页面最终触发的 `@load` 仍使用完整的
79
+ `src` 语义。
80
+
81
+ ## 网页与宿主通信
82
+
83
+ App 中,WebView 会提供:
84
+
85
+ ```js
86
+ fjs.postMessage('hello');
87
+ ```
88
+
89
+ Web 的跨源 iframe 不能由宿主注入 `fjs`,网页需要自带一个 shim:
90
+
91
+ ```js
92
+ window.fjs = window.fjs || {
93
+ postMessage: (data) => parent.postMessage({ __fjs: String(data) }, '*'),
94
+ };
95
+ ```
96
+
97
+ 网页里的 JavaScript 世界与 fjs 页面互不相通;网页不能 import fjs natives,只能通过
98
+ `fjs.postMessage` 发送字符串。
@@ -0,0 +1,134 @@
1
+ <script setup lang="ts">
2
+ // Browser stand-in for the Flutter widget <web-view />. The build registers
3
+ // it under that tag, so one template line is a WKWebView on a device and
4
+ // this iframe in a browser.
5
+ //
6
+ // Two things are NOT the same as on the app, and both are the browser's
7
+ // doing rather than something left undone (docs/web.md):
8
+ //
9
+ // * a cross-origin iframe cannot be injected, so the loaded page brings
10
+ // its own `fjs.postMessage` shim (see public/demo.html) and talks to us
11
+ // through window.postMessage;
12
+ // * `error` on an iframe almost never fires. An HTTP 404 or 500 is a
13
+ // successful load of an error page, and network-level failures are
14
+ // silent. A page that needs to know it loaded should say so itself with
15
+ // fjs.postMessage('ready').
16
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
17
+ import {
18
+ errorPayload,
19
+ LoadCycle,
20
+ loadPayload,
21
+ messagePayload,
22
+ resolveSrc,
23
+ classifySrc,
24
+ unsupportedSrcMessage,
25
+ } from '../index';
26
+ import type { FjsHtmlSrc } from '@ufjs/runtime';
27
+
28
+ // FjsHtmlSrc, not string: the tag's type comes from this component's props
29
+ // (the toolchain reads them through FjsWidgetProps), so this one line is
30
+ // what makes <web-view src> complete the project's html/ pages on both
31
+ // targets. It still accepts any string — http URLs and asset:// included.
32
+ const props = defineProps<{ src?: FjsHtmlSrc }>();
33
+
34
+ const emit = defineEmits<{
35
+ (e: 'load', payload: string): void;
36
+ (e: 'error', payload: string): void;
37
+ (e: 'message', payload: string): void;
38
+ (e: 'tap'): void;
39
+ }>();
40
+
41
+ const frame = ref<HTMLIFrameElement | null>(null);
42
+ const cycle = new LoadCycle();
43
+ /** The generation the currently rendered iframe belongs to. */
44
+ const generation = ref(cycle.begin());
45
+
46
+ const warned = new Set<string>();
47
+ function warnOnce(key: string, message: string): void {
48
+ if (warned.has(key)) return;
49
+ warned.add(key);
50
+ console.warn(`[fjs] ${message}`);
51
+ }
52
+
53
+ /** The URL this iframe should carry, or undefined for "load nothing". */
54
+ const url = computed(() => {
55
+ const raw = props.src ?? '';
56
+ const kind = classifySrc(raw);
57
+ if (kind === 'unsupported') {
58
+ warnOnce(`web-view-src:${raw}`, unsupportedSrcMessage(raw));
59
+ return undefined;
60
+ }
61
+ const resolved = resolveSrc(raw, { target: 'web' });
62
+ return resolved.kind === 'url' ? resolved.url : undefined;
63
+ });
64
+
65
+ // A new src is a new load: bump the generation so the old page's load,
66
+ // error and messages are dropped instead of reported against the new URL.
67
+ watch(
68
+ () => props.src,
69
+ () => {
70
+ generation.value = cycle.begin();
71
+ },
72
+ );
73
+
74
+ /** The element a result came from has to be the one on screen now.
75
+ *
76
+ * A src change replaces the iframe (it is keyed), but the OLD element stays
77
+ * alive long enough to finish loading, and its listener is this same
78
+ * closure. Without this check that stale finish would be reported — against
79
+ * the new URL, which is the one thing it certainly is not. */
80
+ const isCurrent = (event: Event) => event.target === frame.value;
81
+
82
+ const onLoad = (event: Event) => {
83
+ // An iframe with no src fires `load` for about:blank; that is not a page
84
+ // anybody asked for.
85
+ if (!url.value || !isCurrent(event)) return;
86
+ if (cycle.finish(generation.value)) emit('load', loadPayload(url.value));
87
+ };
88
+
89
+ const onError = (event: Event) => {
90
+ if (!url.value || !isCurrent(event)) return;
91
+ if (cycle.finish(generation.value)) emit('error', errorPayload(url.value));
92
+ };
93
+
94
+ /** Double filter. The window's message event is a party line: anything on
95
+ * the page can post to it, and other iframes get their own. A message is
96
+ * this web-view's only when it came from THIS frame's window and carries the
97
+ * shape the shim sends. Everything else is somebody else's traffic, so it is
98
+ * ignored silently — it is not an error. */
99
+ function onWindowMessage(event: MessageEvent): void {
100
+ const element = frame.value;
101
+ if (!element || event.source !== element.contentWindow) return;
102
+ const data = event.data as { __fjs?: unknown } | null;
103
+ if (!data || typeof data !== 'object' || typeof data.__fjs !== 'string') return;
104
+ if (!cycle.accepts(generation.value)) return;
105
+ emit('message', messagePayload(data.__fjs));
106
+ }
107
+
108
+ onMounted(() => window.addEventListener('message', onWindowMessage));
109
+ onBeforeUnmount(() => window.removeEventListener('message', onWindowMessage));
110
+ </script>
111
+
112
+ <template>
113
+ <iframe
114
+ ref="frame"
115
+ class="fjs-web-view"
116
+ :src="url"
117
+ :key="generation"
118
+ @load="onLoad"
119
+ @error="onError"
120
+ @click="emit('tap')"
121
+ />
122
+ </template>
123
+
124
+ <style>
125
+ /* The box is the page's; the content is the loaded page's. Same as the app
126
+ side, which draws no chrome of its own either. */
127
+ .fjs-web-view {
128
+ border: 0;
129
+ display: block;
130
+ width: 100%;
131
+ height: 100%;
132
+ background: transparent;
133
+ }
134
+ </style>
@@ -0,0 +1,132 @@
1
+ //
2
+ // Generated file. Do not edit.
3
+ // This file is generated from template in file `flutter_tools/lib/src/flutter_plugins.dart`.
4
+ //
5
+
6
+ // @dart = 3.5
7
+
8
+ import 'dart:io'; // flutter_ignore: dart_io_import.
9
+ import 'package:path_provider_android/path_provider_android.dart';
10
+ import 'package:sqflite_android/sqflite_android.dart';
11
+ import 'package:webview_flutter_android/webview_flutter_android.dart';
12
+ import 'package:path_provider_foundation/path_provider_foundation.dart';
13
+ import 'package:sqflite_darwin/sqflite_darwin.dart';
14
+ import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
15
+ import 'package:path_provider_linux/path_provider_linux.dart';
16
+ import 'package:path_provider_foundation/path_provider_foundation.dart';
17
+ import 'package:sqflite_darwin/sqflite_darwin.dart';
18
+ import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
19
+ import 'package:path_provider_windows/path_provider_windows.dart';
20
+
21
+ @pragma('vm:entry-point')
22
+ class _PluginRegistrant {
23
+
24
+ @pragma('vm:entry-point')
25
+ static void register() {
26
+ if (Platform.isAndroid) {
27
+ try {
28
+ PathProviderAndroid.registerWith();
29
+ } catch (err) {
30
+ print(
31
+ '`path_provider_android` threw an error: $err. '
32
+ 'The app may not function as expected until you remove this plugin from pubspec.yaml'
33
+ );
34
+ }
35
+
36
+ try {
37
+ SqfliteAndroid.registerWith();
38
+ } catch (err) {
39
+ print(
40
+ '`sqflite_android` threw an error: $err. '
41
+ 'The app may not function as expected until you remove this plugin from pubspec.yaml'
42
+ );
43
+ }
44
+
45
+ try {
46
+ AndroidWebViewPlatform.registerWith();
47
+ } catch (err) {
48
+ print(
49
+ '`webview_flutter_android` threw an error: $err. '
50
+ 'The app may not function as expected until you remove this plugin from pubspec.yaml'
51
+ );
52
+ }
53
+
54
+ } else if (Platform.isIOS) {
55
+ try {
56
+ PathProviderFoundation.registerWith();
57
+ } catch (err) {
58
+ print(
59
+ '`path_provider_foundation` threw an error: $err. '
60
+ 'The app may not function as expected until you remove this plugin from pubspec.yaml'
61
+ );
62
+ }
63
+
64
+ try {
65
+ SqfliteDarwin.registerWith();
66
+ } catch (err) {
67
+ print(
68
+ '`sqflite_darwin` threw an error: $err. '
69
+ 'The app may not function as expected until you remove this plugin from pubspec.yaml'
70
+ );
71
+ }
72
+
73
+ try {
74
+ WebKitWebViewPlatform.registerWith();
75
+ } catch (err) {
76
+ print(
77
+ '`webview_flutter_wkwebview` threw an error: $err. '
78
+ 'The app may not function as expected until you remove this plugin from pubspec.yaml'
79
+ );
80
+ }
81
+
82
+ } else if (Platform.isLinux) {
83
+ try {
84
+ PathProviderLinux.registerWith();
85
+ } catch (err) {
86
+ print(
87
+ '`path_provider_linux` threw an error: $err. '
88
+ 'The app may not function as expected until you remove this plugin from pubspec.yaml'
89
+ );
90
+ }
91
+
92
+ } else if (Platform.isMacOS) {
93
+ try {
94
+ PathProviderFoundation.registerWith();
95
+ } catch (err) {
96
+ print(
97
+ '`path_provider_foundation` threw an error: $err. '
98
+ 'The app may not function as expected until you remove this plugin from pubspec.yaml'
99
+ );
100
+ }
101
+
102
+ try {
103
+ SqfliteDarwin.registerWith();
104
+ } catch (err) {
105
+ print(
106
+ '`sqflite_darwin` threw an error: $err. '
107
+ 'The app may not function as expected until you remove this plugin from pubspec.yaml'
108
+ );
109
+ }
110
+
111
+ try {
112
+ WebKitWebViewPlatform.registerWith();
113
+ } catch (err) {
114
+ print(
115
+ '`webview_flutter_wkwebview` threw an error: $err. '
116
+ 'The app may not function as expected until you remove this plugin from pubspec.yaml'
117
+ );
118
+ }
119
+
120
+ } else if (Platform.isWindows) {
121
+ try {
122
+ PathProviderWindows.registerWith();
123
+ } catch (err) {
124
+ print(
125
+ '`path_provider_windows` threw an error: $err. '
126
+ 'The app may not function as expected until you remove this plugin from pubspec.yaml'
127
+ );
128
+ }
129
+
130
+ }
131
+ }
132
+ }