[Node.js]Google Cloud Functionsでimport/exrpot構文を使う


Overview

フロントエンドはReact、バックエンドはNode.jsで作っていると、どちらでも利用する処理はソースコードを共有したくなります。
別の記事でesmというパッケージを知り、Google Cloud Functions(以降、GCF)でもimport/export構文を使いたくなりました。
GCFのフレームワーク内で動くから厳しいかな?と思っていたのですが、至って簡単だったので残しておきます。

Target reader

  • JavaScriptとNode.jsでソースコードを共有したい方。

Prerequisite

  • バックエンドはGCFを利用する、つまりNodeの起動オプションを指定するようなことはできない。
  • Node.jsのバージョンはGoogle Cloud Function(GCF)に依存し、現時点ではV10系とする。

Body

どうしてimport/export構文にするの?

私のPJの場合、バックエンドで作成した機能をフロントエンドで実行するかも?という機能がいくつかあります。
そうなるとどちらも同じJSだし、フロントとバックを意識する必要性をできたらなくしたいよねという欲求にかられます。
フロントからバックへ移動、バックからフロントへ移動、もちろんパッケージとして切り出すことも可能とソース変更なしに自由に扱えます。
(もちろんブラウザ、もしくはNode.js固有のAPIを使うとそこに対しての手当ては必要になります)
そのような理由からimport/exportに寄せることにしました。

パッケージで共有する場合については以下の記事を参考にしてください。
https://qiita.com/qrusadorz/items/cc2e76afa1d34f1a980c

esmとは

Node.jsではV12系まで起動時にフラグを付けないとimport/exrpot構文が使用できないようです。
しかし、esmパッケージを利用するとV10系でもimport/exrpot構文が利用可能になります。

更に、npm initコマンドに組み込まれているほどのものなので安心できるでしょう。
esmを使用しないnpm initlegacy initといっているのが象徴的です。
https://docs.npmjs.com/cli/init

もう少しesmについて知りたい方は、作者の方が投稿された記事を見てみるといいかもしれません。(わたしも詳しく理解していない
https://medium.com/web-on-the-edge/tomorrows-es-modules-today-c53d29ac448c

GCFのソースコードを眺める

https://cloud.google.com/functions/docs/first-nodejs?hl=ja
https://firebase.google.com/docs/functions/http-events

index.js
exports.helloHttp = (req, res) => {
  res.send(`Hello ${escapeHtml(req.query.name || req.body.name || 'World')}!`);
};

GCF固有のルールで関数を外に出してるかと思いきや、これはNode.jsのexports構文そのものじゃない?と気が付く。
私はmodule.exportsの方しか使っていなかったので気が付くのが遅れてしまった...

It allows a shortcut, so that module.exports.f = ... can be written more succinctly as exports.f = ....

Google翻訳先生曰く

module.exports.f = ...をexports.f = ...としてより簡潔に記述できるように、ショートカットを許可します。

つまり、module.exports使っているだけで特別なことは何もないことが判明。

esmを導入する方法

これはGitHubにしっかり書かれている。
https://github.com/standard-things/esm

Enable esm for local runs:

node -r esm main.js

一つ目は起動時にnodeコマンドにesmをプリロードする方法。
-rは後続のモジュールをプリロードするオプション。
https://nodejs.org/dist/latest/docs/api/cli.html#cli_r_require_module

この方法はGCFではnodeコマンドに介入できないことから使用できない。

二つ目はesmを介してrequireする方法。

Use esm to load the main ES module and export it as CommonJS.
index.js

// Set options as a parameter, environment variable, or rc file.
require = require("esm")(module/*, options*/)
module.exports = require("./main.js")

main.js

// ESM syntax is supported.
export {}

npm init esmを実行すると、エントリーポイントのindex.jsにesmのラッパーが入り、main.jsに自身のコードを記述するようになる。
前項のGCFが標準のmodule.exportsを用いていることを考えると、通常のnpm init esmと同じように作ればFunctionsで動きそうという予測ができる。

npm init esmでGCFの関数を作る

新規のフォルダでnpm init esmを実行する。

npm init esm

適当にEnterキーなり文字入力してpackage.jsonが作成される。
この時すでにindex.jsにesmの仕込みができているのが確認できる。

公式ドキュメントではindex.jsに関数をつくるが、ここではmain.jsにimport/export構文で記述する。
処理については、n-gramというパッケージを利用し、hogeという文字列を2文字ずつに切り出す処理に変更する。
https://cloud.google.com/functions/docs/first-nodejs?hl=ja#creating_a_function

main.js
import nGram from 'n-gram';

const helloHttp = (req, res) => {
    const text = "hoge";
    const bigram = nGram.bigram(text) || [];

    res.send(`Hello!` + bigram);
};

// ESM syntax is supported.
export {
    helloHttp
}

デプロイ前にパッケージをインストール。

npm i n-gram

公式ドキュメントに沿ってデプロイする。

gcloud functions deploy helloHttp --runtime nodejs10 --trigger-http

コンソールから実行すると、n-gramをインポートして、hogeという文字列を2文字ごとに分割できているのが確認できる。

$ Hello!ho,og,ge

ちなみに以下のようにrequireとmodule.exportsを使った従来の形式でも問題なく動く。

main.js
const nGram = require('n-gram');

const helloHttp = (req, res) => {
    const text = "hoge";
    const bigram = nGram.bigram(text) || [];

    res.send(`Hello!` + bigram);
};

exports.helloHttp = helloHttp;
// or
// module.exports = {
//  helloHttp
// }

esmを導入に関する注意点

nodeコマンドで実行する場合、GCFで実行する場合のどちらでも問題ないことを示した。
これでもはやrequireを利用する必要がなくなったと思っていたが盲点が一つ。

jestではesmを意識しないで利用できるような方法がまだない。

テストフレームワークであるjestでスマートにサポートされていないのが痛い。
ただし、テストコードの中でesmを介したrequireを使えばいい模様。
https://github.com/standard-things/esm/issues/97#issuecomment-492435481

また、esmもjestもこれについては対応するために動き出しているので、時間がいずれ解決するものと思われる。
https://github.com/standard-things/esm/issues/706
https://github.com/facebook/jest/issues/9430

よって、現時点ではesmをテストコードで意識する必要があるが、これを理由に導入を見送るほどの理由にはなりえない。

Conclusion

今回はGCFで利用しましたが、exportsを利用していることからAWSのLambdaやAzure Functionsでも同様に利用できると思われます。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-prog-model-handler.html
https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-reference-node

esmもjestの件があるため完璧ではないですが、import/export構文で統一できる状況が整いつつあるといった印象です。
import/exportで統一する未来を先取りして動きたい方にはesmは最良のパッケージだと思います。
もちろん導入後にそれまでのテストコードを通すことは必須です。(私もesmを本格導入するのはこれからなので

Have a great day!

Appendices

なし

References

本体に全て掲載