[Flutter Web]CORS policyを解決する3つの方法

解決したいこと

Flutter WebでレンダラーをCanvaskitに選択すると、外部サーバにある画像を読み込もうとすると読み込めず、以下のようなCORSエラーが発生する。Flutterのアセットにして自分のサーバに配置できるといいのだが、全ての画像を持ってくることはできない。外部サーバの持ち主に頼んで、読み込めるよう設定してもらうこともできない。そこで外部サーバの画像を直接読めるようにしたい。

Access to XMLHttpRequest at 'https://www.otherdomain.com/img/image.jpg' from origin 'http://localhost:63022' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

左のFlutterWebでのブラウザ表示では、左上の画像の外部サーバの画像が表示されていない。左下はCORS対策したので表示できている。右はアプリで、両方とも外部サーバの画像が表示されている。

3つの解決方法

私が調査した中で解決方法は以下の3つ。

  • chrome.dartを変更する
  • 開発環境としては良い

  • HTMLレンダリングにする
  • お手軽。

  • 外部画像用のWidgetを作成する
  • CanvasKitのレンダリングができる。

上記3つに加えて、PHPでプロキシサーバを作る方法もあるようですが、実施してません。

chrome.dartを変更する

メリット・デメリット

開発中の全てのプロジェクトに適応できる。
開発環境でのみ有効と思われる。実行時にChromeに警告メッセージが表示される。

方法

  1. flutter\bin\cache の flutter_tools.stamp を消す(flutter_sdk.stamp、flutter_tools.snapshotではない)
  2. flutter\packages\flutter_tools\lib\src\web の chrome.dart を変更する
  3. disable-extensionsの下に、disable-web-securityを追加する。

    '--disable-extensions',
    '--disable-web-security', // 追加
    

HTMLレンダリングにする

公式ページによると、CanvasKitの場合ピクセルで画像にアクセスするから駄目だとのこと。そこでレンダラーをHTMLに変更すると、表示されるようになる。方法としては3つある。

実行時に指定

flutter run -d chrome --web-renderer html

ビルド時に指定

flutter build web --web-renderer html

ソースに指定

web/index.html の最後に、WebRndererをhtmlを指定するコードを追加する。

    </script>
  <script type="text/javascript">window.flutterWebRenderer = "html";</script>
</body>
</html>

外部画像用のWidgetを作成する

外部画像を読み込むときはhtmlで表示させるWidgetを作成します。そのWidgetをFlutterWebの時のみ有効にします。アプリの時は通常の画像用のWidgetを使用します。

抽象クラスと条件付きインポート

ベースとなる抽象クラスを作成します。また、ここで条件付きインポートを使用して、アプリ時とWEB時で異なるファイルをインポートする設定を記載します。条件付きインポートについての詳細は、以下をご覧下さい。

[Dart/Flutter] 専用ライブラリを作って、Conditional Importing(条件付インポート)でアプリとWebのソースを共存させる

import 'package:flutter/material.dart';
import 'outer_server_image_stub.dart'
// ignore: uri_does_not_exist
    if (dart.library.io) 'outer_server_image_app.dart'
// ignore: uri_does_not_exist
    if (dart.library.html) 'outer_server_image_web.dart';

abstract class OuterServerImage extends StatelessWidget {
  factory OuterServerImage(String urlImage, {double? width, double? height}) =>
      getOuterServerImage(urlImage, width: width, height: height);
}

スタブクラス

import 'outer_server_image.dart';

OuterServerImage getOuterServerImage(String urlImage,
        {double? width, double? height}) =>
    throw UnsupportedError('error');

アプリ時に読み込むクラス

抽象クラスをインプリメントしつつ、StatelessWidgetも継承します。抽象クラスでStatelessWidgetをextendsしてますけど、その子クラスでextendsする、ってできるんですね。(extends OuterServerImageとすると、親クラスでfactoryメソッドを使っているためだと思うが、コンパイルエラーになる)

サンプルのため非常に簡単な実装になっていますが、実際の要件に基づいて細かく実装してください。

import 'package:flutter/material.dart';
import 'outer_server_image.dart';

class OuterServerImageApp extends StatelessWidget implements OuterServerImage {
  const OuterServerImageApp(
    this.imageUrl, {
    this.width,
    this.height,
    Key? key,
  }) : super(key: key);

  final String imageUrl;
  final double? width;
  final double? height;

  @override
  Widget build(BuildContext context) {
    return Image.network(imageUrl, width: width, height: height);
  }
}

OuterServerImage getOuterServerImage(String urlImage,
        {double? width, double? height}) =>
    OuterServerImageApp(
      urlImage,
      width: width,
      height: height,
    );

FlutterWeb実行時のクラス

正直、まだ理解できていません。ここの場合、以下のようなことをしています。

  • imageUrlというIDでImageElementの要素を作成している
  • このWidgetのbuildしたWidgetとして、imageUrlのIDが振り分けられたHtmlElementを使用するようにする
import 'dart:html';
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import 'outer_server_image.dart';

class OuterServerImageWeb extends StatelessWidget implements OuterServerImage {
  const OuterServerImageWeb(this.imageUrl);

  final String imageUrl;

  @override
  Widget build(BuildContext context) {
    // ignore: undefined_prefixed_name
    ui.platformViewRegistry.registerViewFactory(
      imageUrl,
      (int _) => ImageElement()
        ..src = imageUrl
        ..style.width = '100%'
        ..style.height = '100%',
    );
    return HtmlElementView(
      viewType: imageUrl,
    );
  }
}

OuterServerImage getOuterServerImage(String urlImage,
        {double? width, double? height}) =>
    OuterServerImageWeb(
      urlImage,
    );

メリット・デメリット

全体のレンダラーをHTMLにせず基本はCanvasKit、HTMLレンダラーは必要最低限にする、というのであれば、この方法になります。ただ、画像を表示する方法を統一する必要があるので、色々と不便になるかも知れません。今後使用する予定ですので、感想等も今後記載しようと思います。

まとめ

以上でFlutterWebのCROSを解決する手段を3つ紹介しました。

参考

[公式] Displaying images on the web
How to solve flutter web api cors error only with dart code?
ソース ブランチ:conditional_importing