LoginSignup
0
0

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

Posted at

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

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


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

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

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

実装方法

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

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

<!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パターンを使ってアプリの状態と関数をエクスポートします。

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

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

  @js.JSExport()
  int get count => _counterScreenCount;

  ...
}

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

 @override
  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で是非聞いてください!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0