YSNHatenaBlog

主にアプリやWebサービス開発について

materialize-cssがNext.jsと相性悪そうなのでmaterial-uiに乗り換える

materializecssのSelectを入れたい。 https://materializecss.com/select.html

selectは初期化が必要らしいのだが、next.jsからだと呼べない。 https://materializecss.com/select.html#initialization

materialize-cssっていうnpmがあるので入れてみたけど、importで怒られる。

ReferenceError: window is not defined

This error happened while generating the page. Any console logs will be displayed in the terminal window.

browser-default使うと表示されるけどlabelがなぜか避けてくれない。

<div className="input-field col s12">
    <select id="surface-hard" className="browser-default">
      <option value="" disabled selected>
        Choose your option
      </option>
      <option value="1">Option 1</option>
      <option value="2">Option 2</option>
      <option value="3">Option 3</option>
    </select>
    <label htmlFor="surface-hard">Materialize Select</label>
</div>

materialize-css自体はあまりメンテされてないんだな。2年前...。 https://www.npmjs.com/package/materialize-css

react-materializeっていうのもある。これはメンテされてそう。 https://github.com/react-materialize/react-materialize

Material-UIというのがあって、こっちはNext.js対応してそう。ただしSSR用の設定がいろいろ必要っぽい。 https://material-ui.com/ https://github.com/mui-org/material-ui/tree/master/examples/nextjs https://tech.playground.style/javascript/next-material-ui/

移し変えるかぁ...面倒だが。

初期設定 https://material-ui.com/getting-started/installation/

Reactコンポーネントで指定できるの思ったより良い感じだな。

Running command: npm run build-functions
npm WARN lifecycle The node binary used for scripts is /var/folders/vl/dj884t816mj6ch0bvn80hlsc0000gn/T/yarn--1611412015683-0.5227895432393217/node but npm is using /Users/yosuke/.anyenv/envs/ndenv/versions/v10.16.0/bin/node itself. Use the `--scripts-prepend-node-path` option to include the path for the node binary npm was executed with.

これしてみた

npm config set scripts-prepend-node-path true
Duplicate identifier 'LibraryManagedAttributes'
node_modules/@types/react/index.d.ts:2982:14 - error TS2300: Duplicate identifier 'LibraryManagedAttributes'.

2982         type LibraryManagedAttributes<C, P> = C extends React.MemoExoticComponent<infer T> | React.LazyExoticComponent<infer T>
                  ~~~~~~~~~~~~~~~~~~~~~~~~

  node_modules/@types/react-transition-group/node_modules/@types/react/index.d.ts:2981:14
    2981         type LibraryManagedAttributes<C, P> = C extends React.MemoExoticComponent<infer T> | React.LazyExoticComponent<infer T>
                      ~~~~~~~~~~~~~~~~~~~~~~~~
    'LibraryManagedAttributes' was also declared here.

定義がダブっている模様。package.jsonに次を書き足して再度yarn。

"resolutions": {
    "@types/react": "^16.13.1"
},

デプロイ

(node:32695) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 17)
Warning: viewport meta tags should not be used in _document.js's <Head>. https://err.sh/next.js/no-document-viewport-meta

こういうのも出たのでviewportを移動。

まだ半端だけどとりあえず導入はできた。

Cloud Functionsのonly, exceptオプションについて調査したこと

Cloud Functionsの数が増えてくるとこんな感じのエラーが返ってきてデプロイに失敗する。

FirebaseError: HTTP Error: 429, Quota exceeded for quota group 'Write' and limit 'Write requests per 100 seconds' of service 'cloudfunctions.googleapis.com' for consumer 'project_number:XXXXXXXXXXXX'. To see usage, go to https://console.cloud.google.com/iam-admin/quotas?quota_group=Write&limit=CLIENT_PROJECT-100s&qs_error_code=INSUFFICIENT_TOKENS&project=my-project&service=cloudfunctions.googleapis.com. To request a quota increase, see https://cloud.google.com/functions/quotas.

100秒間に80回の書き込みの制限にひっかかっている。毎回onlyつける手もあるが、全Functionをデプロイする機会も結局発生すると思う。制限を上げる依頼をすることも可能そうだが、まずは制限を越えぬようにデプロイできないかを考えてみる。

※ 制限を上げる依頼はできなそう(https://cloud.google.com/functions/quotas#footnote

1 書き込みの割り当てを引き上げることはできません。割り当て不足は通常、次のいずれかが原因で発生します。 高頻度で多数の関数を同時または順次にデプロイする CI / CD システムを使用している。 Firebase CLI を使用して複数の関数を同時にデプロイしている。

すぐ思いつくのはonlyのオプションを使ってちょっとずつ関数をデプロイしていく方法だが、Functionをindex.jsから削除した場合に、全Functionデプロイ時に削除されるかが気になったので調査。

onlyオプションでグループを指定した場合は削除される

グループを指定した場合は、同一グループ内で減ったFunctionがある場合は、デプロイ時にその関数が削除される。( --force を入れなければ確認される)

グループ化してないFunctionは削除できなそう

それではグループ化していないFunctionを消す場合はどうしたらいいかを考えた場合、 firebase deploy --only functions --except functions:<全グループ> みたいに書けばグループ化してないFunctionだけ残るのではないかと思ったのだが、--except ではFunctionsのグループを指定することはできない模様。基本的にはサービス名を指定して使うものに見える(--except firestore とか、 --except functions とか)。

というわけでグループ化してないFunctionは手動で消すしか無い。

結論

Functionを作る際は、何でもいいのでグループで囲った方がよさそう。

Firebase FirestoreのデータをBigQueryにエクスポートする

FirestoreのデータをBigQueryにデータを取り込む手段について。

まだサービスローンチ前ならFirebase Extension

直接試せてないがFirebase Extensionがあるのでこれを使いたい。

firebase.google.com

リアルタイムでデータが同期される(と思う)。 すでにローンチ済みのサービスの場合は、既存のデータを移す必要がある。

サービスローンチ済みならgcloud

gcloudコマンド(+ 一緒についてくるbqコマンド)でやる。

サービスアカウントの作成

ここを参考にロールを設定する。

コマンド例

実行毎にBigQueryが最新のFirestoreのデータに更新される。

# サービスアカウントのキーファイルをセット
gcloud auth activate-service-account --key-file key.json

# プロジェクトのセット
gcloud config set project my-firebase-project

# collection groupごとにFirestore→Storageへデータをエクスポート
gcloud firestore export gs://firestore-export-for-bq/2020-06-29T10:17:23.011+09:00 --collection-ids=user,shop,product

# collection groupごとにBigQueryのテーブルに入れる
bq load --source_format=DATASTORE_BACKUP --replace=true --projection_fields=name firestore.user gs://firestore-export-for-bq/2020-06-29T10:17:23.011+09:00/all_namespaces/kind_user/all_namespaces_kind_user.export_metadata
bq load --source_format=DATASTORE_BACKUP --replace=true --projection_fields=name,owners firestore.shop gs://firestore-export-for-bq/2020-06-29T10:17:23.011+09:00/all_namespaces/kind_shop/all_namespaces_kind_shop.export_metadata
bq load --source_format=DATASTORE_BACKUP --replace=true --projection_fields=name,price firestore.product gs://firestore-export-for-bq/2020-06-29T10:17:23.011+09:00/all_namespaces/kind_product/all_namespaces_kind_product.export_metadata

projection_fields オプションがあるおかげで、個人情報など取り込みたくない情報はフィルタ可能。

Docker Hubのgoogle公式イメージを使って定期実行する

gcloudを使うための公式イメージがDocker Hubで配布されているのでこれを使えばよい。

google/cloud-sdk - Docker Hub

Cloud Runとか使っていけそうな気がする。

Firebase AuthのデータをGoogleスプレッドシートに出力する

タイトルの通りですが、Firebase AuthのユーザレコードをGoogleスプレッドシートに直接書き出すnodeプロジェクトを作ってみました。

github.com

直接実行するもよし、Cloud Functionsを含めコンテナを起動させて実行するもよし。特に後者の場合はローカルにダウンロードすることなく、アクセス制限のかかっているGoogle Drive上にアップロードすることができます。スプレッドシートなのでその後でフィルタしたり検索したりも簡単。

Next.jsのgetServerSidePropsで遅くなったので対策を考えてみたが...

Cloud FunctionsでNext.jsを動かしてWebアプリを運用してみている。

クライアントサイドで非同期にパラメータ取得した方が画面遷移が早くて爽快だったのだけど、OGPとかやりたいときに困るので getServerSideProps を入れてみた。 その結果、これを入れたページへの画面遷移が極端に遅くなった。

調べてみたけど、画面遷移で来たか初期ロードかはこんな感じで判定できそう。

if ((context.req as functions.https.Request).originalUrl.endsWith('.json')) {
  return {}
}
// 重い処理
return { /* 処理結果 */ }

ただこの「重い処理」が画面遷移にかかる時間の大半を締めていればいいのだが、自分の場合、クライアントとCloud Functionsとの通信にかかる時間の方がほとんどだった。hostingの制限上、Cloud Functionsはus-centralだし...。

処理も複雑になるし、断念。

追記

getInitialProps を使えばいいのかもしれないけど、パスによる場合分けが必要そうなのと、自動最適化が効かなくなるみたいなのでん〜という感じ。

メタデータを使ってFirebase Storageのデータを特定のユーザ間でシェアする方法

ここのグループ非公開のファイル メタデータを使用する方法。

ユーザーデータを保護する  |  Firebase

1対1で相手に画像を送信したい場合

送信者と受信者のAuth uidがクライアント側で分かる前提。

iOSの場合

let metadata = StorageMetadata()
metadata.contentType = "image/jpeg"
metadata.customMetadata = [
    "sender": <送信者のuid>,
    "receiver": <受信者のuid>
]
let storage = Storage.storage()
let ref = storage.reference().child("share/photo.jpg")
ref.putData(data, metadata: metadata)

Storageのルールはこんな感じ。

service firebase.storage {
  match /b/{bucket}/o {
    match /share {
        match /{allPaths=**} {
        allow write: if request.auth.uid != null && request.resource.metadata["sender"] == request.auth.uid
        allow read: if request.auth.uid != null && (resource.metadata["sender"] == request.auth.uid || resource.metadata["receiver"] == request.auth.uid)
      }
    }
  }
}

Firestoreで管理しているグループ内で共有したい場合

Storageのオブジェクト作成時にCloud Functionsを実行してメタデータを付けてあげる。 例えば送信者の属するグループがFirestoreで管理されており、そのグループのメンバーのみで画像を共有したいとする。

iOSアプリ側のコード。

let metadata = StorageMetadata()
metadata.contentType = "image/jpeg"
metadata.customMetadata = [
    "sender": <送信者のuid>
]
let storage = Storage.storage()
let ref = storage.reference().child("share/photo.jpg")
ref.putData(data, metadata: metadata)

Cloud Functionsのコード。

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';

admin.initializeApp();

export const storagePermission = functions.storage.bucket().object().onFinalize(async (object, context) => {

    // <必要に応じてパスのチェックなどをする>

    const sender = object.metadata && object.metadata['sender'];

    if (sender && object.name) {

        const uidsInGroup = ... // Firestoreから共有したいユーザのuid配列を取得

        const file = admin.storage().bucket(object.bucket).file(object.name);
        await file.setMetadata({
            metadata: {
                uids: uidsInGroup.join(',')
            }
        }) // カスタムメタデータをセットする際setMetadataの引数オブジェクトにmetadataのキーが必要。
    }
    return
}

Storageのルール。

service firebase.storage {
  match /b/{bucket}/o {
    match /share {
        match /{allPaths=**} {
        allow write: if request.auth.uid != null && request.resource.metadata["sender"] == request.auth.uid && resource.metadata["uids"] == null
        allow read: if request.auth.uid != null && resource.metadata["uids"] != null && request.auth.uid in resource.metadata["uids"].split(",")
      }
    }
  }
}

メタデータにひたすら <uid>: "true" をセットする方法もありそうだが散らかりそう。

追記

このルールだと他のユーザが上げたデータ上書きできちゃうからStorageのパスは考えた方がいいかも。

SwiftのOptionalのオーバーヘッド

気になることがあってOptionalのunwrapのオーバーヘッドを調べてみました。

先に結論

  • 参照型のunwrapはパフォーマンスが低下する
  • 値型のunwrapはあまりパフォーマンスは低下しない

方法

Counterという型のcountを10,000,000回インクリメントするのを10回平均するコードを書いて、 アプリの起動時に計測させるようにしました。

環境: iPhone 7, デバッグ実行

Optionalな参照型の場合

class Counter {
    
    var count: Int = 0
}

let n = 10
var sum: TimeInterval = 0

for _ in 0..<n {

    let now = Date()
    let counter: Counter? = Counter()

    for _ in 0..<10000000 {
        
        counter?.count += 1
    }
    
    sum += -now.timeIntervalSinceNow
}

let ave = sum / Double(n)
print(ave)

結果: 0.248754590749741[秒]

counterのOptionalを外してみました。 該当箇所だけ書きます。

    let counter: Counter = Counter()

    for _ in 0..<10000000 {
        
        counter.count += 1
    }

結果: 0.0492702841758728[秒]

かなり違います。

ちなみにforce unwrapでも遅いです。

let counter: Counter! = Counter()

for _ in 0..<10000000 {
    
    counter.count += 1
}

結果: 0.234673696756363[秒]

Optionalな値型の場合

ほぼ同じコードで、Counterstructにしてみます。

struct Counter {
    
    var count: Int = 0
}

let n = 10
var sum: TimeInterval = 0

for _ in 0..<n {

    let now = Date()
    var counter: Counter? = Counter()

    for _ in 0..<10000000 {
        
        counter?.count += 1
    }
    
    sum += -now.timeIntervalSinceNow
}

let ave = sum / Double(n)
print(ave)

結果: 0.0521551012992859[s]

速いです。

Optionalを外してみます。

var counter: Counter = Counter()

for _ in 0..<10000000 {
    
    counter.count += 1
}

結果: 0.0496872007846832[s]

あまり変わらないです。