解決したいこと
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に警告メッセージが表示される。
方法
- flutter\bin\cache の flutter_tools.stamp を消す(flutter_sdk.stamp、flutter_tools.snapshotではない)
- flutter\packages\flutter_tools\lib\src\web の chrome.dart を変更する
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