こんにちはウーオでエンジニア兼PdMをしている三京(t3qyo)です。
UUUOではモバイルアプリの開発言語にFlutterを採用しています。
最近そのモバイルアプリを既存のリソースを活かしながら同じリポジトリを使って(モノレポで)Webにデプロイしたので、デプロイの際の注意点や運用の感想を書きます。
デプロイしたのは以下の出品者用の管理画面です。
Firebase Hostingにデプロイする
1.Flutter Web Buildを開始する
公式ページ をご参照ください。
10秒ほどでlocalhostでの立ち上げが完了します。
Firebase Hostingへのデプロイは こちらの記事が参考になります。
2. Firebase CLIをインストール
npm install -g firebase-tools
3. firebase init hosting
する
以下の設定でfirebaseの初期設定と、GitHub Actionsでのデプロイの初期設定ができます。
--- === Project Setup First, let's associate this project directory with a Firebase project. You can create multiple project aliases by running firebase use --add, but for now we'll just set up a default project. ? Please select an option: Use an existing project ? Select a default Firebase project for this directory: uuuo (uuuo) i Using project uuuo (uuuo) === Hosting Setup ? Which Firebase features do you want to set up for this directory? Press Space to select features, then Enter to confirm your choices. Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys === Project Setup First, let's associate this project directory with a Firebase project. You can create multiple project aliases by running firebase use --add, but for now we'll just set up a default project. i Using project uuuo (uuuo) === Hosting Setup Your public directory is the folder (relative to your project directory) that will contain Hosting assets to be uploaded with firebase deploy. If you have a build process for your assets, use your build's output directory. ### 重要、どのディレクトリを公開するか ? What do you want to use as your public directory? build/web ? Configure as a single-page app (rewrite all urls to /index.html)? Yes ? Set up automatic builds and deploys with GitHub? Yes ? File build/web/index.html already exists. Overwrite? Yes ✔ Wrote build/web/index.html i Detected a .git folder at /Users/t.sankyo/works/hobby/sample_web i Authorizing with GitHub to upload your service account to a GitHub repository's secrets store. Waiting for authentication... ✔ Success! Logged into GitHub as t-sankyo ? For which GitHub repository would you like to set up a GitHub workflow? (format: user/repository) t-sankyo/flutter_web_sample ✔ Created service account github-action-617732841 with Firebase Hosting admin permissions. ✔ Uploaded service account JSON to GitHub as secret FIREBASE_SERVICE_ACCOUNT_UUUO. i You can manage your secrets at https://github.com/t-sankyo/flutter_web_sample/settings/secrets. ? Set up the workflow to run a build script before every deploy? Yes ? What script should be run before every deploy? flutter build web ✔ Created workflow file /Users/t.sankyo/works/hobby/sample_web/.github/workflows/firebase-hosting-pull-request.yml ? Set up automatic deployment to your site's live channel when a PR is merged? No i Action required: Visit this URL to revoke authorization for the Firebase CLI GitHub OAuth App: https://github.com/settings/connections/applications/89cf50f02ac6aaed3484 i Action required: Push any new workflow file(s) to your repo i Writing configuration info to firebase.json... i Writing project information to .firebaserc... ✔ Firebase initialization complete!
What do you want to use as your public directory?
(どのディレクトリを公開しますか?)
Flutterの場合、デフォルトで
build/web
に生成されるので、build/web
を指定してください
以下のファイルが自動生成されます。
変更をpushするとGitHubのActionsタブでActionが作成されています。
また、デプロイに必要なGCPのサービスアカウントもSecretsに設定されています。(楽ですね!)
4. GitHub ActionsのEnvironmentを設定する
Firebase Hostingには Preview Channelという機能があり、ランダムなURLでデフォルト7日間だけ有効な環境を作ることができます。(便利)
UUUOではPreview Channelを含め、3環境運用しています。
環境 | 用途 |
---|---|
Preview環境 | Pull Requestレビュー用 |
Staging環境 | QA用 |
Production環境 | 本番リリース用 |
それぞれの環境に対して、GitHubの SettingsからEnvironment
を設定しました。
key | secret/variable | 値 |
---|---|---|
FIREBASE_SERVICE_ACCOUNT | secret | 該当環境へのデプロイ権限を持つサービスアカウントキーを指定します。複数環境必要ない場合は、自動生成された FIREBASE_SERVICE_ACCOUNT_... を使うで良いと思います。GCPのコンソールから取得できます。 |
CHANNEL_ID | variable | Staging, Productioの場合は live を、Previewチャンネルの場合は pr-${{ github.ref_name }} を設定します。 |
PROJECT_ID | variable | デプロイ先のプロジェクトIDを指定します |
5. GitHub ActionsのymlでEnvironmentを使うよう設定/ 手動デプロイに変更
4
. で作成したVariablesとSecretsを使うように設定していきます。
また、firebase init
で生成された firebase-hosting-pull-request.yml
ではPRを立ち上げた時に自動でデプロイされてしまう設定となっています。
同じリポジトリ内で管理しているモバイルアプリ側の変更の際にWebにデプロイされたくはないので、デプロイを手動にするなどの対応をしていきます。
'on': pull_request の部分をEnvironmentで分岐できるよう、以下のように設定します。
on: workflow_dispatch: inputs: environment: type: environment default: 'Preview_web' description: 'Either Preview_web or Staging_web or Production_web' required: true
また、jobsを以下のように設定します。
jobs: build_and_preview: runs-on: ubuntu-latest environment: ${{ inputs.environment }} steps: - name: Checkout uses: actions/checkout@v3 - name: Install flutter # Flutterのインストール uses: subosito/flutter-action@v2 with: channel: "stable" cache: true - name: Download Flutter packages # Flutterのパッケージダウンロード run: flutter pub get - name: flutter build web # ビルド run: flutter build web -t lib/main.dart --release --web-renderer html - uses: FirebaseExtended/action-hosting-deploy@v0 # デプロイ with: repoToken: "${{ secrets.GITHUB_TOKEN }}" firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_UUUO_TOOLS }}" projectId: ${{ vars.PROJECT_ID }} channelId: ${{ vars.CHANNEL_ID }}
Environmentsのsecretsとvariablesを使うことでかなりスッキリとかけたのではないでしょうか。
また、デフォルトのビルドでは日本語のフォントが正しく表示されなかったのでbuild時のオプションに、--web-renderer html
を設定しました。
デフォルトではPCブラウザでは canvaskit
、モバイルブラウザでは html
のrendererが設定されるようです。
https://docs.flutter.dev/development/platform-integration/web/renderers
上記の対応を行うと、GitHub Actionsからリリースすることができるようになります。
ミニマムのソースをこちら においておりますので、よければご確認ください。
Flutter Webを運用してみての気づきなど
UUUOでFlutter on the Webを運用してみての気づきを書きます。
1. ブラウザの検索が効かない 😢
これは事前に知っておきたかったです。
2023年3月時点、Ctrl + F
での検索は完全に対応されていません。(issue)
具体的には、htmlでレンダリングすると、画面の表示されている範囲内のDOMの要素は検索対象となるようですが、範囲外の要素を検索したいときに、検索対象になりません。
対応するにはクライアント側でキー操作をオーバーライドするなどのハックが必要になるようです。
2. DataTableを大量にレンダリングする処理は厳しかった。😢
下にスクロールでページングしていくような処理を DataTable
を用いて実装しようとしましたが、
大量のWidgetのレンダリング処理が発生し、画面がカクカクするようになってしまいました。
ページングする際は、下スクロールではなく、以下のようなスタンダードなページングUIを作ってタップに画面が切り替わるようにしたほうが良さそうです。
3. 全角入力されることを考慮する 👆
Webでは TextFormFieldの keyboardType: TextInputType.number,
を指定しても、 全角数字の入力が可能であるため、
以下の処理でアンフォーカス時に半角変換処理を入れました。
/// 全角数字を半角数字に変換する String alphanumericToHalfLength() { // 文字コードから65248を引くと全角を半角に変換できる const fullLengthCode = 65248; final regex = RegExp(r'^[0-9]+$'); final string = runes.map<String>((rune) { final char = String.fromCharCode(rune); return regex.hasMatch(char) ? String.fromCharCode(rune - fullLengthCode) : char; }); return string.join(); }
4. URL指定での直接遷移を考慮する必要がある 👆
モバイルアプリでは、URLを指定して直接アクセスすることはできませんが、 Webではリソースに対してIDを指定してアクセスをすることがあります。
モバイルでは前の画面から値を受け取ることができましたが、Webではそれができません。 今回は GoRouter を使うことでIDの取得部分を対応しました。
以下のイメージです
static final router = GoRouter( routes: [ GoRoute( name: WebPurchaseDetailPage.routeName, path: '${WebPurchaseDetailPage.routeName}/:id', pageBuilder: (context, state) { // IDを取得して画面に渡す final id = int.parse(state.params['id']) ?? 0; return pageBuilder( context, WebPurchaseDetailPage( id: id, ), state, ); }, ), ...
5. モバイルアプリとWebのページは基本的に分離するのが良い 👆
以前別の対応でモノレポでのWebとアプリの対応を行った際、随所に kIsWeb
の分岐を書く必要があり、可読性と保守性が悪くなっていました。
その反省を活かし、今回はWebとアプリでPage, Controllerを分離し、ドメインロジックのみ共有する方針をとりました。
まだモバイルアプリでの方の開発の方が多いため開発時にWebのことを気にしながら開発する必要がなくなり、割と良かったです。
以下のイメージで分けています。
├── domain ### 共通で使うドメインモデル │ ├── entity │ │ ├── announcement.dart │ │ ├── announcement.freezed.dart │ │ ├── announcement.g.dart ├── ui ## モバイルの画面、controller, state │ ├── announcement │ │ ├── announcement_controller.dart │ │ ├── announcement_page.dart │ │ ├── announcement_state.dart │ │ └── announcement_state.freezed.dart └── web ## webの画面、controller, state ├── announcement │ ├── web_announcement_controller.dart │ ├── web_announcement_page.dart │ ├── web_announcement_state.dart │ └── web_announcement_state.freezed.dart
6. (そのうちやりたい)モバイルアプリのコミットか、Webのコミットかをラベル、コミットメッセージなどでわかるようにしたい。👆
モバイルアプリのリリースタイミングとWebのリリースタイミングが異なるので、Pull RequestのタイトルやコミットメッセージなどでこれはWebのコミットなのかモバイルアプリのコミットなのか、
それとも両方かをわかりやすくしたほうが良いと思われます。
7. キャッシュの期限の設定 👆
firebase deployでデプロイした場合のデフォルトのキャッシュ設定は割と長く、ユーザーが古い画面を見続けてしまうことがあったため、以下の header
を設定しています。
{ "hosting": { ... "rewrites": [...], /// 以下を追加 "headers": [ { "source": "**/*.@(eot|otf|ttf|ttc|woff|font.css)", "headers": [ { "key": "Access-Control-Allow-Origin", "value": "*" }, { "key": "Cache-Control", "value": "public, max-age=7200" } ] }, { "source": "**/*.@(jpg|jpeg|gif|png)", "headers": [ { "key": "Access-Control-Allow-Origin", "value": "*" }, { "key": "Cache-Control", "value": "public, max-age=7200" } ] }, { "source": "**/*.@(js|css)", "headers": [ { "key": "Access-Control-Allow-Origin", "value": "*" }, { "key": "Cache-Control", "value": "public, max-age=7200" } ] } ] } }
8. (今の所)各ブラウザ別の特別な対応は必要ない 😄
一昔前はIE対応などが必要なことが多かったように思いますが、最近はそんなことがないのでしょうか。
9. パフォーマンス(そこまで?)悪くない? 😄
当初はレンダリングの問題などで、パフォーマンス、操作性を危惧していましたが、FirebaseのHostingで自動的にキャッシュも効いているということもあり、
ある程度問題なさそうということがわかりました!
ただ、ListViewを多数レンダリングするなどすると著しくパフォーマンスが下がり、ページ描画、スクロールにに時間がかかるということがありました。
10. Railsのhamlを使って書くよりは画面構成、ステート管理がだいぶ楽 😄
Flutterの恩恵によるところが大きいですが、
モバイル開発で慣れているステート管理方法や、Widget構築でWeb画面を作成できるのは、
モバイル開発者の多いチームにとっては技術の再習得も必要なく、得意分野を活かす形で対応できて良かったです。
同じように管理画面をRailsのhamlを使って書いているのですが、それと比べると動的なステート管理もしやすくかなり便利でした。
以上です。
これからもFlutter Webの開発をやっていくので、また何かTipsがあれば共有します!
では!
ウーオでは水産流通を革新するため、プロダクトを通じてあらゆるアプローチをしています。ウーオの事業やプロダクト開発にご興味がある方は、以下をぜひご覧ください 👇
各ポジションの募集要項はこちら
ソフトウェアエンジニア(Mobile Application/Flutter) / 株式会社ウーオ