🔳

Flutter エレメントエンべディング・アプリを ウェブサイト内に入れられるの力!AngularやReactまでもできる!

2024/04/29に公開

「Flutterforward 2023」のイベント祭に初めて「エレメントエンべディング」を発表された、そういう機能ができて、驚きました。

早速色んな使い方を思い浮かびました、例えばホームページ上でユーザーがアプリを直接試すことができます。もう一つのアイデアとしてドキュメンテーションのページで実際の例が見られます。


Flutter エレメントエンべディングのメリットは:

  • 実際のアプリを直接使用することができるんです
  • アプリとウェブサイト間で情報を共有することが出来ます

image.png
この例はこちらで見られます

実装方法

実は実装方法は簡単です。まず、普通のHTMLウェブサイトの例で説明します。全部のコードを見たいなら、こちらでチェックしてください

https://github.com/lucas-goldner/Element-Embedding-Presentation/tree/Flutterconnection-2024/example_website/red_counter_homepage

でも、最初から始めましょう

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Embedded Flutter Website</title>
    <link rel="stylesheet" href="./styles.css" />
  </head>
  <body>
    <header>
      <h1>See the Flutter app below!</h1>
    </header>
    <div>
      <main>
        <section>
          <div id="flutter_host" style="height: 812px; width: 375px">
            Loading...
          </div>
        </section>
      </main>
    </div>
  </body>
</html>

image.png

  1. アプリをウェブバージョンビルドをしないといけません
$ flutter build web
  1. 次のステップはウェブサイトにスクリプトをロードします。まず、flutterのエンジンスクリプトとロードを追加します。flutter.jsはflutterのエンジンの開始コードが入っています
<script src="./あなたのflutterアプリ/flutter.js" defer></script>

エンジンの「ロード」イベント時に別のスクリプトで自分のアプリをロード出来ます。これを使って、ウェブサイト内にアプリを入れます。エンジンは色んな設定がありますので、こちらで確認してください

<script>
      window.addEventListener("load", function (ev) {
        const basePath = "./あなたのflutterアプリ/";

        _flutter.loader.loadEntrypoint({
          entrypointUrl: basePath + "main.dart.js",
          onEntrypointLoaded: async function (engineInitializer) {
            let appRunner = await engineInitializer.initializeEngine({
              assetBase: basePath,
              hostElement: document.querySelector("#flutter_host"),
            });
            await appRunner.runApp();
          },
        });
      });
    </script>

ウェブサイトコードに追加したら、この結果をもらえます

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Embedded Flutter Website</title>
    <link rel="stylesheet" href="./styles.css" />
    <script src="./flutter_app/flutter.js" defer></script>
    <script>
      window.addEventListener("load", function (ev) {
        const basePath = "./flutter_app/";

        _flutter.loader.loadEntrypoint({
          entrypointUrl: basePath + "main.dart.js",
          onEntrypointLoaded: async function (engineInitializer) {
            let appRunner = await engineInitializer.initializeEngine({
              assetBase: basePath,
              hostElement: document.querySelector("#flutter_host"),
            });
            await appRunner.runApp();
          },
        });
      });
    </script>
  </head>
  <body>
    <header>
      <h1>See the Flutter app below!</h1>
    </header>
    <div>
      <main>
        <section>
          <div id="flutter_host" style="height: 812px; width: 375px">
            Loading...
          </div>
        </section>
      </main>
    </div>
  </body>
</html>

これで全部です!その方法とおりに、実装したら、これが見られます:

image.png

Reactを使ったらどうする

前の述べたらようにReactウェブサイトで実装したら、問題を見つけました。このガイドのおかげで、結局にエンべディングができました。

1: まずこのコマンドでビルドを書かないといけません

$ flutter build web --profile --dart-define=Dart2jsOptimization=O0

縮小を点けませんでしたので、このアウトプットは普通の比べて、サイズより大きい。その理由で依頼人は大きいバンドルをダウンロドをしないと

2: webファイルは/build/web/からreactのpublic ファイルにコピーしてください。このプロジェクトではwebファイルはflutterに変えましたので、全部はあなたのホスト/flutter/からアクセスことができます.次にpublic/flutter/main.dart.jsを開けて、t1は自分のpathで変えないといけません

// これを探して
    getAssetUrl$1(asset) {
      var t1, fallbackBaseUrl, t2;
      if (A.Uri_parse(asset).get$hasScheme())
        return A._Uri__uriEncode(B.List_5Q7, asset, B.C_Utf8Codec, false);
      t1 = this._assetBase; <----
    }

// こちらに変更しないと
    getAssetUrl$1(asset) {
      var t1;
      if (A.Uri_parse(asset, 0, null).get$hasScheme())
        return A._Uri__uriEncode(B.List_5Q7, asset, B.C_Utf8Codec, false);
      t1 = "/flutter/"; <----
    }

3: 同じ方法でflutter.jsに変更して

// これを探して
function getBaseURI() {
  const base = document.querySelector("base");
  return (base && base.getAttribute("href")) || "";
}

// 同じpathでこちらに変更しないと
function getBaseURI() {
  return "/flutter/";
}

4: publicファイルで新しいflutter_init.jsを作って、このコードをペストをして

window._stateSet = function () {};
window.addEventListener("load", function (ev) {
  let target = document.querySelector("#flutter_target");
  _flutter.loader.loadEntrypoint({
    onEntrypointLoaded: async function (engineInitializer) {
      let appRunner = await engineInitializer.initializeEngine({
        hostElement: target,
      });
      await appRunner.runApp();
    },
  });
});

5: 今reactのプロジェクトでreact-helmet-asyncを追加して

$ yarn add react-helmet-async

6: 準備が終わりました!ReactのアプリはHelmetProviderで包んで

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { HelmetProvider } from "react-helmet-async"
import './index.css'

const helmetContext = {};

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
    <React.StrictMode>
        <HelmetProvider context={helmetContext}>
            <App />
        </HelmetProvider>
    </React.StrictMode>
)

7: 最後にHelmetのコンポーネントを追加して、flutter_init.jsflutter.jsロドしたら、flutterのアプリが見られます

import { Helmet } from "react-helmet-async";

function App() {
  return (
    <>
      <Helmet>
        <script src="/flutter/flutter.js" defer></script>
        <script src="/flutter_init.js" defer></script>
      </Helmet>
      <div
        style={{ aspectRatio: 9 / 19.5 }}
        id="flutter_target"
        className="h-full"
      ></div>
    </>
  );
}

この手続きは結構大変だし、新しい変化するたびに、もう一度あのステップたちをしないとから、このスックリプトを書きました。CIに使ってください!

#!/bin/bash

# Paths relative to the script's location
SOURCE_DIR="../fluttershow_app/"
BUILD_DIR="../fluttershow_app/build/web/"
DESTINATION_DIR="./public/flutter"
MAIN_DART_FILE="$DESTINATION_DIR/main.dart.js"
FLUTTER_FILE="$DESTINATION_DIR/flutter.js"

# Navigate to the SOURCE_DIR and run steps to build the flutter web app
cd "$SOURCE_DIR"
flutter build web --profile --dart-define=Dart2jsOptimization=O0

# Check for any errors during the flutter build
if [ $? -ne 0 ]; then
    echo "Error: Failed to build flutter web app in $SOURCE_DIR"
    exit 1
fi

# Change directory back to the script's location
cd - 

# Check if the build directory exists
if [ ! -d "$BUILD_DIR" ]; then
    echo "Error: Source directory $BUILD_DIR does not exist."
    exit 1
fi

# Remove the old flutter directory if it exists
if [ -d "$DESTINATION_DIR" ]; then
    echo "Removing old flutter directory..."
    rm -rf "$DESTINATION_DIR"
fi

# Copy the source to the destination
echo "Copying $BUILD_DIR to $DESTINATION_DIR..."
cp -r "$BUILD_DIR" "$DESTINATION_DIR"

# Check for any errors during the copy
if [ $? -ne 0 ]; then
    echo "Error: Failed to copy $BUILD_DIR to $DESTINATION_DIR"
    exit 1
fi

# Modify the flutter/main.dart.js file
if [ -f "$MAIN_DART_FILE" ]; then
    echo "Modifying $JS_FILE..."
    sed -i '' 's|t1 = this._assetBase;|t1 = "/flutter/";|g' "$MAIN_DART_FILE"
else
    echo "Warning: $MAIN_DART_FILE does not exist. Skipping modification."
fi

if [ -f "$FLUTTER_FILE" ]; then
    echo "Modifying $FLUTTER_FILE..."

    # Remove the specific line
    sed -i '' '/const base = document.querySelector("base");/d' "$FLUTTER_FILE"

    # # Replace the specific line
    sed -i '' 's|return (base && base.getAttribute("href")) \|\| "";|return "/flutter/";|g' "$FLUTTER_FILE"

else
    echo "Warning: $FLUTTER_FILE does not exist. Skipping modification."
fi

echo "Updated ✨"

ウェブサイトからアプリの状態を変更してみましょう

この例では、ウェブサイトにボタンとテキストフィルドを使いしました

image.png

ウェブサイトからアプリの状態を変更するために、「JS Interop」を使います。まず、Dartコードをウェブサイトからアクセスできるように設定します。

1:「js」パッケージを追加します

$ flutter build web

1:「js」パッケージを追加します

$ flutter build web

2: dartとjsのコミュニケーション

アプリと仕様によって、このステップは異なっています。ですから、このリポジトリでは例が見られます。Decoratorパターンを使ってアプリの状態と関数をエクスポートします。

.JSExport()
class _MyHomePageState extends State<MyHomePage> {
  final _streamController = StreamController<void>.broadcast();
  int _counterScreenCount = 0;

  .JSExport()
  void addHandler(void Function() handler) {
    _streamController.stream.listen((event) {
      handler();
    });
  }

  .JSExport()
  int get count => _counterScreenCount;

  ...
}

「initState」関数を見てみましょう。ここで、2つのオブジェクトをエクスポートします。まず、exportを使って現在の状態をエクスポートし、「_appState」にアサインをします。次に、「_stateSet」関数をウェブサイトのコードで使用できるようにします。

 
  void initState() {
    super.initState();
    final export = js_util.createDartExport(this);
    js_util.setProperty(js_util.globalThis, '_appState', export);
    js_util.callMethod<void>(js_util.globalThis, '_stateSet', []);
  }

普通にカウンターのアプリにstreamのコントロラは必要がありませんけど、この場合は、カウンタを変わるたびに、ウェブサイトのテキストフィルドを変更したいです

  1. dartのコードはjsにアサインをします

ウェブサイトのコードを見てみましょう。まず、「_stateSet」関数を設定します。この例では、プリント機能のために使っています。次に、ブラウザのグローバル「window」オブジェクトに「_appState」を追加します。残りのコードでは、入力フィールドとDartの関数を組み合わせます

// Sets up a channel to JS-interop with Flutter
(function () {
  "use strict";
  // This function will be called from Flutter when it prepares the JS-interop.
  window._stateSet = function () {
    window._stateSet = function () {
      console.log("Calls _stateSet once!");
    };

    // The state of the flutter app, see `class _MyAppState` in lib/main.dart.
    let appState = window._appState;

    let valueField = document.querySelector("#value");
    let updateState = function () {
      valueField.value = appState.count;
    };

    // Register a callback to update the HTML field from Flutter.
    appState.addHandler(updateState);

    // Render the first value (0).
    updateState();

    let incrementButton = document.querySelector("#increase-btn");
    incrementButton.addEventListener("click", (event) => {
      appState.increment();
    });
  };
})();

このコンテンツで新しいの`main.js作りました

// Sets up a channel to JS-interop with Flutter
(function () {
  "use strict";
  // This function will be called from Flutter when it prepares the JS-interop.
  window._stateSet = function () {
    window._stateSet = function () {
      console.log("Calls _stateSet once!");
    };

    // The state of the flutter app, see `class _MyAppState` in lib/main.dart.
    let appState = window._appState;

    let valueField = document.querySelector("#value");
    let updateState = function () {
      valueField.value = appState.count;
    };

    // Register a callback to update the HTML field from Flutter.
    appState.addHandler(updateState);

    // Render the first value (0).
    updateState();

    let incrementButton = document.querySelector("#increase-btn");
    incrementButton.addEventListener("click", (event) => {
      appState.increment();
    });
  };
})();

そのスクリプトをロドします

<script src="./main.js" defer></script>

最後の結果はこれになります

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Interactive Red Counter - Revolutionary Counting App</title>
    <link rel="stylesheet" href="./styles.css" />
    <script src="./flutter_app/flutter.js" defer></script>
    <script src="./main.js" defer></script>

    <script>
      window.addEventListener("load", function (ev) {
        const basePath = "./flutter_app/";

        _flutter.loader.loadEntrypoint({
          entrypointUrl: basePath + "main.dart.js",
          onEntrypointLoaded: async function (engineInitializer) {
            let appRunner = await engineInitializer.initializeEngine({
              assetBase: basePath,
              hostElement: document.querySelector("#flutter_host"),
            });
            await appRunner.runApp();
          },
        });
      });
    </script>
  </head>
  <body>
    <header>
      <h1>See the Flutter app below!</h1>
    </header>
    <div class="main-content">
      <main>
        <section class="interactive-iphone-display">
          <div id="flutter_host" style="height: 812px; width: 375px">
            Loading...
          </div>

          <div class="counter-controls">
            <button id="increase-btn">Increase</button>
            <div class="current-number">
              <label for="current-number-field">Current Number:</label>
              <input
                id="value"
                type="text"
                id="current-number-field"
                value="0"
              />
            </div>
          </div>
        </section>

        <section class="features">
          <h2>Features</h2>
          <ul>
            <li>Stunning Red Color Scheme</li>
            <li>Intuitive and Simple Counter Interface</li>
            <li>Revolutionary One-Tap Counting Technology</li>
          </ul>
        </section>

        <section class="download">
          <h2>Download Now</h2>
          <p>Join the counting revolution today. Available on all platforms.</p>
        </section>
      </main>
      <footer>
        <p>&copy; 2023 Red Counter. All Rights Reserved.</p>
      </footer>
    </div>
  </body>
</html>

試しましょう🔥!

1_J4ZMGFlpJh1S0vUHOl-l2g.gif

別のフレームワークとは?

質問があれば、Xで是非聞いてください!

Discussion