GCP Cloud FunctionsをTypeScriptで書く

Google Cloud Platform(GCP)のCloud FunctionsもTypeScriptで書きたい。そしてデプロイしたい。で、ググるとFirebaseのCloud Functionsの説明ばかり出てくるんですよね・・・FirebaseはFunction作成時にTypeScriptを選択できるみたいだけど、GCPは無理なようです。

ではどうするかというと、

  • TypeScriptのソースファイルをトランスパイルしてJavaScriptにする
  • webpackでimport,requireしているソースファイルを芋づる式に一つのJavaScriptファイルにバンドルする(まとめる)
  • ただしnode_modulesはバンドルの対象外とする

ようにします。

デプロイするファイルは、以下の通り

node_modulesの内容はバンドルしない代わりに必要なパッケージがdependenciesに書いてあるpackage.jsonをデプロイします。そうするとCloud Functionsサーバー側で必要なパッケージをダウンロードしてくれるようです。

tsconfig.jsonはこんな感じです。

{
  "compilerOptions": {
    "target": "ES2018", 
    "module": "commonjs",
    "outDir": "./dist/",    
    "strict": true,   
    "esModuleInterop": true,    
    "skipLibCheck": true, 
    "forceConsistentCasingInFileNames": true
  }
}

webpack.config.jsonはこんな感じ。

// node_modulesをバンドル対象から外すために必要
const nodeExternals = require('webpack-node-externals');
module.exports = {
   mode: "production",
   target: "node",
   externals: [nodeExternals()],
   devtool: "hidden-source-map",
   resolve: {
       modules: ["node_modules"],
       extensions: ['.ts', '.js']
   },
   module: {
       rules: [
           {
               test: /\.ts$/,
               loader: 'ts-loader'
           }
       ]
   },
   output: {
       path: __dirname + "/dist",
       filename: "index.js",
       libraryTarget: "commonjs"
   }
}

いらない設定、した方がいい設定もありそうですが・・・

Clooud Function実行の起点となる関数をmainとします。これは任意の名前で良いですがデプロイ時のオプションもその名前に合わせる必要があります。そしてmodule.exportsでこの関数を提供しておく必要があります。

function main(res, res) {
   // 諸々処理
}

module.exports = {
  main: main
}

webpackを実行するとソースコードがバンドルされたindex.jsが./dist/にできます。書いたとおりnode_modulesにあるものはバンドルしません。

$ npx webpack --entry=${起点となるts OR jsファイル}

./dist/にpackage.jsonをコピーします。これで./dist/以下にindex.jsとpackage.jsonがある状態になります。

$ cp -f ./package.json ./dist

package.jsonのmainはindex.jsにしてください。index.jsが実行の起点となるからです。

Cloud Functionsへデプロイします。

$ gcloud functions deploy ${Cloud Functionの名前} \
 --source=./dist \
 --entry-point=main \
 --runtime=nodejs12

--entry-pointで指定するのはindex.jsから最初に呼び出す関数名です。ここでは先ほど書いたとおりmainにしています。--entry-pointを省略すると呼び出される関数名はCloud Functionとしてつけた名前と同じになるはず。詳しくはdelopyのオプションを参考に。

gcloud functions deploy  |  Cloud SDK のドキュメント  |  Google Cloud

MulterのエラーをJSONで返したい時

Node.jsでmutipart/form-dataを受信する場合、Multerを使っている人も多いと思います。Multerに設定したファイルサイズ上限を超えた時など、デフォルトではMulterがHTMLでレスポンスを返してしまいます。Node.jsでAPIサーバーを実装している時など任意のJSONでレスポンスを返したいですよね?任意のHTMLを返したい時もあるでしょう。

Multerで上限ファイルサイズなどを指定する場合、以下のようにしていると思います。

const myMulter = multer({
   storage: multer.memoryStorage(),
   limits: {
       fileSize: 5 * 1024 * 1024   // 5MB
   }
})
app.post(path, myMulter.single("file"), async (req, res) => {
   // POST受信した時の処理
})

これを以下のように書き換えます。

const myMulter = multer({
   storage: multer.memoryStorage(),
   limits: {
       fileSize: 5 * 1024 * 1024   // 5MB
   }
})
app.post(path, async (req, res) => {
   const multerHandler = myMulter.single("file")
   multerHandler(req, res, async (multerError) => {
       if (multerError) {
           // multerのエラーを検出して、JSONレスポンスで返す
           res.status(500).json({
               error: multerError.message
           })
           return
       }
       // アプリケーションのPOS受信処理
   })
})

app.postの第二引数に渡すのではなく、明示的にmulterHandlerを呼び出して、そこでエラーを検出するようにします🤌 そして、res.jsonやres.sendなどで任意のレスポンスを返すことができます。

PHPickerResult#assetIdentifierがnilになる

iOS 14から使えるようになったPHPickerViewControllerを使うと撮影した写真や動画を選択するUIを提供してくれて便利です!・・・なのですがPHPickerResult#assetIdentifierがnilになるせいで、PHAsset#fetchAssetsで詳細情報を取れずハマりました😫

理由はPHPickerConfigurationにphotoLibraryを指定しなかったことが原因でした。

let photoLibrary = PHPhotoLibrary.shared()
var config = PHPickerConfiguration(photoLibrary: photoLibrary)
// これが駄目!photoLibraryを渡さないと駄目!
// var config = PHPickerConfiguration()
config.filter = .videos
config.selectionLimit = 1
config.preferredAssetRepresentationMode = .current
phPicker = PHPickerViewController(configuration: config)

こうすると、PHPickerViewControllerDelegateのdidFinishPickingで受け取るPHPickerResultにassetIdentifierが入ってきます。これをPHAsset#fetchAssetsに渡すと画像のサイズや動画の再生時間を取得できます。ただし、ユーザーに読み取り許可を取っていない場合はPHAsset#fetchAssets呼び出し時にアラートが表示されます。この時fetchAssetsはユーザーのアラートの操作を待たずして戻り値を返し、それのfirstObjectはnilになるようなので注意です。

func picker(_ picker: PHPickerViewController, 
            didFinishPicking results: [PHPickerResult]) {
    guard let result = results.first else { return }
    if let assetId = result.assetIdentifier,
       let asset = PHAsset.fetchAssets(
              withLocalIdentifiers: [assetId], options: nil).firstObject {
        print("size \(asset.pixelWidth)x\(asset.pixelHeight)")
        print("duration \(asset.duration)")           
    }

IntelliJで意図しないファイルタイプで開くのを解決する

WebStormなどIntelliJ IDEA系ファイルを作った時、たまに拡張子を無視してプレーンテキストとして開かれるようになることがあります。ファイルを削除して作り直しても解決しない・・・この問題は設定で解決できます。

  • メニューからPreferencesを選択
  • サイドバーのFile Typesを選択

f:id:pgtips:20220106110403p:plain

Recognized File Typesにファイルタイプが並んでいますが、おそらくはTextに問題のファイル名があると思います。これを削除すると拡張子でファイルタイプを判別して開くようになります。割とやらかすミスのわりにわかりづらいところにありますね・・・

UIImageでAssetsのフォルダ指定できるようにする

画像のAssetsはフォルダ分けできますが、そのままだと

UIImage(named: "dir1/icon1")

のようにフォルダを指定するとnilが返ってきます。

Assetsに作ったフォルダを選択してProvides Namespaceにチェックを入れるとフォルダを指定してUIImageオブジェクトを取得できるようになります。

f:id:pgtips:20220106110154p:plain

Provides Namespaceにチェックを入れるとこのようにフォルダが黄色から青色に変わります。

f:id:pgtips:20220106110206p:plain

AirDropで音はするけど何も表示されない

iPhoneからMacAirDropでファイルを送ろうとしたら、Mac側で音はするけど受け入れ確認のアラートが表示されないことがちょくちょくあります。Finderを再起動すると直りました。ターミナルで以下のコマンドを打ちます。

$ killall Finder

Wi-Fiオフだと位置取得精度が悪くなる

CLLocationManagerのdesiredAccuracyの話です。Wi-Fiオフにすると位置情報精度が非常に悪くなる現象に遭遇し、よくみたらドキュメントに書いてありました。

Apple Developer Documentation

if you set the desired accuracy to kCLLocationAccuracyKilometer, the system might disable GPS and use only the Wi-Fi hardware, which would save power and still give you a greater accuracy than you requested.

desiredAccuracyがkCLLocationAccuracyKilometerの時、GPSを使わずWi-Fiだけを使うかもしれないということですね。system might disable GPSとあるので絶対にGPSを使わないということではないようです。実際、Wi-Fiオフでも位置情報は取得します。取得間隔にムラができるなど自分のアプリでは実質使えない精度でしたが・・・

というわけで、desiredAccuracyにkCLLocationAccuracyKilometerをセットする時は、ユーザーにWi-Fiをオンを推奨するアラートを出したほうがいいですね。もしくはより精度の良いkCLLocationAccuracyHundredMetersなどを使うか、ユーザーに精度を選べるようにするかですね。

ドキュメントにはkCLLocationAccuracyKilometerについてだけ書いてますが、kCLLocationAccuracyThreeKilometersも同じかと思います。