关于 lit-and-flutter
Lit and Flutter 是一篇教程,介绍如何将 Lit Web 组件嵌入 Flutter Widget 树,面向希望结合 Web 界面与 Flutter 设备 API 并构建 iOS、Android 应用的开发者。
主要功能
- 01在 Flutter 中嵌入 Lit 组件
- 02将 Web 事件连接到 Flutter API
- 03构建 iOS 和 Android 应用
- 04加载更新后的 Web 组件代码
- 05首次下载后支持离线使用
适用场景
- 01在 Flutter 中封装现有 Web 应用
- 02从 Web 事件调用应用内购买 API
- 03为 Web 界面添加原生设备功能
- 04构建混合移动应用
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rodydavis/ skills --skill lit-and-flutter安装后在 Claude Code 中输入/lit-and-flutter确认已加载。
详细介绍
Lit 和 Flutter
本文将介绍如何设置 Lit Web 组件,并在 Flutter widget 树中内嵌使用。
简而言之,你可以在此处找到最终源码。
进行这项集成的原因是,你可以将现有 Web 应用,或其中的某个部分,嵌入 widget 树中。
将其封装在 Flutter 中后,你可以从 Web 组件的事件监听器调用设备 API。
例如,你可能有一个处理购买的应用,现在可以调用应用内购买 API 或其他 Web 上不支持的设备专属功能。
你还可以构建跨平台应用,并将其发布到 Google Play 和 App Store。
每次更新网站时,Web 组件都会获取新代码,因此每次 Web 组件发生变化时,你都无需发布更新。
前提条件
- Flutter SDK
- Xcode 和 Command Line Tools
- Android SDK
- Vscode
- Node
- Typescript
开始使用
首先,我们可以创建一个空目录,并用任意名称为其命名,名称采用 snake_case 格式。
mkdir flutter_lit_example
cd flutter_lit_example
Web 设置
现在我们位于 flutter_lit_example 目录中,可以设置 Flutter 和 Lit。先从 Node 开始。
npm init -y
npm i lit
npm i -D typescript vite @types/node
这将为 Node 项目设置基础环境,并安装所需的软件包。接下来添加一些配置文件。
touch tsconfig.json
touch vite.config.ts
这将创建 2 个文件。现在打开 tsconfig.json 并粘贴以下内容:
{
"compilerOptions": {
"module": "esnext",
"lib": [
"es2017",
"dom",
"dom.iterable"
],
"types": [
"vite/client"
],
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "./types",
"rootDir": "./src",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*.ts"
],
"exclude": []
}
这是一个基础的 TypeScript 配置。现在打开 vite.config.ts 并粘贴以下内容:
import { defineConfig } from "vite";
import { resolve } from "path";
// https://vitejs.dev/config/
export default defineConfig({
base: "/flutter_lit_example/", // TODO: Name of your github repo
build: {
outDir: "build/web",
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`,
},
input: {
main: resolve(__dirname, "index.html"),
// TODO: Create a new module for each component you want to embed
},
},
},
});
现在需要创建 Web 组件:
mkdir src
cd src
touch my-app.ts
cd ..
打开 my-app.ts 并粘贴以下内容:
import { html, css, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
@customElement("my-app")
export class MyApp extends LitElement {
static styles = css`
p {
color: blue;
}
`;
@property()
name = "Somebody";
render() {
return html`<div>
<p>Hello, ${this.name}!</p>
<slot></slot>
</div>`;
}
}
我们需要为 Web 应用创建一个 index.html。
touch index.html
打开 index.html 并粘贴以下内容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Example</title>
<script type="module" src="/src/my-app.ts"></script>
<style>
body {
padding: 0;
margin: 0;
}
my-app {
width: 100%;
height: 100vh;
}
</style>
</head>
<body>
<my-app></my-app>
</body>
</html>
Flutter 设置
现在 Web 的基础设置已经完成,可以继续设置 Flutter。使用以下命令创建项目:
flutter create --platforms=ios,android .
flutter packages get
打开 pubspec.yaml 并使用以下内容更新它:
name: flutter_lit_example
description: A hybrid Flutter app.
publish_to: "none"
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
flutter_inappwebview: ^5.3.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
请确保重新获取软件包:
flutter packages get
现在需要创建一个用于封装 Web 组件的文件。
cd lib
touch web_component.dart
cd ..
打开 web_component.dart 并粘贴以下内容:
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
class WebComponent extends StatefulWidget {
const WebComponent({
Key key,
@required this.name,
@required this.bundle,
this.attributes = const {},
this.slot = '',
this.events = const [],
}) : super(key: key);
final String name, bundle;
final Map<String, String> attributes;
final String slot;
final List<EventCallback> events;
@override
_WebComponentState createState() => _WebComponentState();
}
class _WebComponentState extends State<WebComponent> {
InAppWebViewController controller;
final Map<String, List<EventCallback>> _events = {};
String get source {
return '''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
padding: 0;
margin: 0;
}
${widget.name} {
width: 100%;
height: 100vh;
}
</style>
<script type="module" crossorigin src="${widget.bundle}"></script>
</head>
<body>
<${widget.name} ${widget.attributes.entries.map((e) => '${e.key}="${e.value}"').join(' ')}>
${widget.slot}
</${widget.name}>
<script>
window.addEventListener("flutterInAppWebViewPlatformReady", (event) => {
${widget.events.join('\n')}
});
</script>
</body>
</html>
''';
}
void _setup(InAppWebViewController controller) {
this.controller = controller;
this._setupEvents();
}
void _setupEvents() {
for (final event in _events.keys) {
controller.removeJavaScriptHandler(handlerName: event);
}
for (final event in widget.events) {
_addEvent(event);
}
}
void _addEvent(EventCallback event) {
controller.addJavaScriptHandler(
handlerName: event.query,
callback: event.onPressed,
);
_events[event.event] ??= [];
_events[event.event].add(event);
}
@override
void didUpdateWidget(covariant WebComponent oldWidget) {
if (oldWidget.events != widget.events) {
_setupEvents();
}
if (oldWidget.slot != widget.slot ||
oldWidget.bundle != widget.bundle ||
oldWidget.name != widget.name) {
controller.loadData(data: source);
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return InAppWebView(
initialData: InAppWebViewInitialData(data: source),
onWebViewCreated: _setup,
);
}
}
class EventCallback {
EventCallback({
@required this.onPressed,
@required this.event,
this.query,
});
final String query, event;
final dynamic Function(List<dynamic> args) onPressed;
@override
String toString() => _source;
String get _prefix => query != null && query.isNotEmpty
? 'document.querySelector("$query")'
: 'document.body';
String get _source => [
'$_prefix.addEventListener("$event", (e) => {',
' window.flutter_inappwebview.callHandler("$query", e);',
'}, false);',
].join('\n');
}
打开 main.dart 并粘贴以下内容:
import 'package:flutter/material.dart';
import 'web_component.dart';
const WEBSITE_URL = 'https://rodydavis.github.io/flutter_lit_example/';
const BUNDLE_PATH = 'assets/main.js';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final title = 'Flutter Hybrid App';
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: title,
theme: ThemeData(primarySwatch: Colors.blue),
home: MyHomePage(title: title),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Builder(
builder: (context) => WebComponent(
name: 'my-app',
bundle: '$WEBSITE_URL/$BUNDLE_PATH',
attributes: {
'name': widget.title,
},
slot: '<button id="my-button">Talk back!</button>',
events: [
EventCallback(
event: 'click',
query: '#my-button',
onPressed: (_) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Clicked!')));
},
),
],
),
),
);
}
}
你需要将 WEBSITE_URL 更新为要部署网站的 URL,并将 BUNDLE_URL 更新为 JS bundle 的相对路径。
这样可以确保在新版本发布时自动更新,并清除过期缓存。首次下载后,这也能提供离线支持。
运行
现在可以运行应用了,但需要先完成几个步骤。
要在本地测试和构建 Web 应用,我们将使用 vite 并渲染 index.html。
npm i
npm run dev
你应该会看到以下内容:
vite v2.2.3 dev server running at:
Local: http://localhost:3000/flutter_lit_example/
Network: http://192.168.1.143:3000/flutter_lit_example/
ready in 311ms.
打开链接 http://localhost:3000/flutter_lit_example/,即可查看正在运行的 Web 应用,并体验 my-app.ts 的热重载更新。
如果想进一步了解 Lit,可以阅读此处的文档。
确认效果符合预期后,就可以继续使用 Flutter 将其封装成原生应用。这样,如果需要使用应用内购买 API 或推送通知,我们就能访问原生代码。
关闭终端,然后运行以下命令:
flutter packages get
flutter build ios
flutter build appbundle
flutter run
这应该会选择一个正在运行的设备,或提示你选择设备。应用在设备上运行后,你会发现 Flutter 应用与 Web 组件之间可以双向通信。
总结
如果想查看源代码,可以前往此处;感谢阅读!如有任何问题,欢迎告诉我!