[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とは

https://github.com/standard-things/esm

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

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

もう少しesmについて知りたい方は、作者の方が投稿された記事を見てみるといいかもしれません。（わたしも詳しく理解していない:joy:）
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

```javascript:index.js
exports.helloHttp = (req, res) => {
  res.send(`Hello ${escapeHtml(req.query.name || req.body.name || 'World')}!`);
};
```
GCF固有のルールで関数を外に出してるかと思いきや、これはNode.jsのexports構文そのものじゃない？と気が付く。
私は```module.exports```の方しか使っていなかったので気が付くのが遅れてしまった...

https://nodejs.org/api/modules.html#modules_exports_shortcut
>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
>
```javascript
// Set options as a parameter, environment variable, or rc file.
require = require("esm")(module/*, options*/)
module.exports = require("./main.js")
```

>main.js
>
```javascript
// 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```を実行する。

```terminal
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

```javascript: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を使った従来の形式でも問題なく動く。

```javascript: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を本格導入するのはこれからなので:sweat_smile:）

Have a great day!

#Appendices
なし

#References
本体に全て掲載