[JavaScript][Node.js]フロントエンドとバックエンドの処理を共通化する

#Overview

フロントエンドはReact、バックエンドはNode.jsで作っていると、どちらでも利用したい処理が出てきます。
両方とも同一言語なので、バックエンドをNode.jsにしたメリットを享受できます:relaxed:
…そう思っていたのですが、write at onceかと思いきやモジュールの入出力の違いでそのまま共有することができません。
最終的にはGitHubを介して共有することになりましたが、結果に至るまでの過程を残しておこうと思います。

#Target reader

- JavaScriptとNode.jsを扱っている方。

#Prerequisite

- バックエンドはGoogle Cloud Function(以降、GCF)を利用する、つまりNodeの起動オプションを指定するようなことはできない。
- Node.jsのバージョンはGoogle Cloud Function(GCF)に依存し、現時点ではV10系とする。
- フロントエンドはCreate React App（以降、CRA）をビルドに利用し、イジェクトしてWebpackの設定は変更しない。

ここで一貫していることは、自身の管理範囲を最小化すること。


#Body

##前置き

前提条件に書いていますが、バックエンドはGoogle Cloud Functionを利用します。
GCFは関数を定義するだけなので、Nodeの起動オプションは指定できず、更にバージョンもV10系の縛りがあります。
（Cloud Runを利用すればNode.jsの最新バージョンが利用できそうですが、可能な限りクラウドベンダーに乗っかりたいのでやりません）
フロントエンドについてもCRAを利用するため、BabelやWebPackの設定を変更できない縛りがあります。
自身の環境でこの辺の縛りがなければ、ここでは採用できなかった方法を採用するのもありかもしれません。


##問題点

フロントエンドとバックエンドでは以下のように微妙に異なる。

```javascript:frontend.js

export const display = (text) => console.log("display1:" + text);

export default { display }

```

```javascript:backend.js

const display = (text) => console.log("display1:" + text);

module.exports = { display };

```

フロントエンドはエクスポートする場合```export```キーワードが必須、バックエンドはそれが不要で```module.exports```に代入する形と少しだけ異なる。
プログラムの条件分岐で何とかできないかと試行錯誤したが、関数の前に入れる```export```がどうにもできず断念。


ちなみに```module.exports```の方式でReactを実行すると実行時エラーになる。

```terminal
TypeError: Cannot assign to read only property 'exports' of object '#<Object>'
```

そうなると何とかしてどちらかの記述に寄せる方法を探る必要がある。
それを調べていくことにした。

**<2020/2/1追記>**

```export default```ではなく```export```なら以下のようになり、単純に```export```か```module.exports =```の違いにできました。
簡単にはできないとモジュール化まで記事を書いたわけですが、真実は機械的にフォルダ配下のファイル一括置換で行けてしまいます:joy:

```javascript:frontend.js

const display = (text) => console.log("display1:" + text);

export { display }

```

ただし、defaultの指定がないため、インポート側は下記のようにインポートするものを明示的に指定する必要があります。

```javascript:caller.js
// OK
import { display } from './frontend';
display("hoge");

// NG: compile error
import frontend from './frontend';
frontend.display("hoge");

```



##コピペコース

まずはフロントエンドとバックエンドのソースをコピペによって共有する方法を模索する。
本来コピペは選択するべきではないが、スピード重視であることとどの部分を共有するか明確に決まっていないこともあり一時的な手段として選択する。
チームプレイにおいて属人化は許されないため、原則として後述のパッケージ化を検討するべき。

###Node.js側のimport/exportのサポート状況はどうなっている？

現時点のLTSであるV12系では実験的段階(Experimental)のため```--experimental-modules```フラグが必要です。
GCFではこのフラグを設定できないため、これは断念しました。（V13系では不要になったようですが、GCFではV14(2年程？)まで待たないといけない）
https://nodejs.org/docs/latest-v12.x/api/esm.html

```--experimental-modules``` フラグを使った場合については@azawakhさんが書かれています。
https://qiita.com/azawakh/items/8b4a7d3061bddd3b340e


###Node.js側でパッケージを利用してimport/exportをサポートできないか？

@issyxissyさんが記事にされていますが、ESMのパッケージを利用すると```import/export```の記述が可能になります。
https://qiita.com/issyxissy/items/f999c06a2b6834c643d3

最初エントリーポイントに```require('esm')```が必須と認識していたため試していませんでした。
しかし、記事を見るとそうでもなさそう。
GCFを使用する場合、エントリーポイントへの介入は厳しいと考えていたため、再度こちらになびくかも？:sweat_smile:

###JavaScript側でrequire/exportsのサポート状況はどうなっている？

今回のエラーはWebPackによるものだと判明している。
https://github.com/webpack/webpack/issues/4039

WebPackの[Tree Shaking](https://webpack.js.org/guides/tree-shaking/)には```import/export```が必須のため、どちらの記述に寄せるかの回答としては```import/export```が無難の模様。
https://github.com/webpack/webpack/issues/4039#issuecomment-419284940

>You've actually using import and module.exports in the same file, which is not allowed by Webpack. You can sidestep this by setting "modules": "commonjs", which will make Babel compile the import to a require. This breaks tree shaking, as mentioned above though, so fixing your code would be a better idea.

Google翻訳先生の解釈
>実際には同じファイルでimportとmodule.exportsを使用していますが、これはWebpackでは許可されていません。 これを回避するには、"modules"："commonjs"を設定します。これにより、Babelはimportをrequireにコンパイルします。 ただし、上記のようにTree Shakingを壊すため、コードを修正することをお勧めします。

私の場合、そもそもCRAのイジェクトは実施しない方針のため、Babelの設定自体を公式な方法で変更することができない。


##パッケージ化コース

コピペコースを一通り見て、近道しようとしたら遠回りになりそうだったので、パッケージ化に方針を変更する。
いつも気軽に使用しているパッケージだが、自分で作るとなるとどうするの？というのがあったので調べた。

###npmは敷居が高そう

npmのドキュメントを見たが、なんか大変そうだなぁという印象:sweat_smile:
何よりプライベートパッケージは有料プランであるのがきつい。
https://docs.npmjs.com/about-private-packages
個人向け、もしくは企業の一人当たりの料金は7ドル/月。
https://www.npmjs.com/products

フロントエンドとバックエンドでソースを共有したいだけなのに7ドルにアカウント管理とか面倒:pensive:
かなりネガティブな印象でドキュメントを漁っていたら…いいのありました！

###GitHubを使うという公式裏技？

npmに登録しないとnpmを介したパッケージ共有はできないかと思ったら、GitHubを利用した共有ができる。
https://docs.npmjs.com/configuring-npm/package-json.html#github-urls

npmに登録済みのパッケージとは少し扱いにくい部分もあるが、手軽に共有できるためこれを掘り下げていく。


####GitHubを使ってパブリックレポジトリを共有する
#####GitHubにパッケージを作成

まずはGitHubにレポジトリを作成する。
https://qiita.com/qrusadorz/items/9916644e1af1453fe30b

準備が整ったら早速```npm init```を実行してpackage.jsonを作成する。

```terminal
npm init
```

もし```import/export```の形式を使いたい場合、コピペの時にも出てきた```esm```を使って以下のコマンドで作成できる。
詳しくは公式ドキュメントを参考にしてほしい。
https://docs.npmjs.com/cli-commands/init.html

```terminal
npm init esm
```

基本的にEnterキーで入力なしで進めればいいが、私が入力したのは以下の項目。
変更したいならpackage.jsonを直接修正すればいいので特にここで入力しないといけないというものではない。

- description
    - パッケージの概要を記入。
- author
    - 自身のユーザーネームを記入:sweat_smile:
- license
    - みんな大好きMITで。

これでpackage.jsonが作成される。
npmに公開する意思がないことを明確にしておくため、```"private": true```の1行を追加するのをお勧めしておく。
https://docs.npmjs.com/files/package.json#private

esmを使った場合、index.jsとmain.jsが作られるが、esmを使っていない場合は自身でindex.jsを作ってしまおう。
esmを使った場合は、main.jsに```export```の形式で記述する。

```javascript:index.js
const display = (text) => console.log("display1:" + text);

module.exports = { display };
```

これを```git commit```してプッシュすれば、GitHubに反映されるのでパッケージの準備は完了。

#####プロジェクトにインストールする

プロジェクトにパッケージをインストールする。
例では、ユーザーがqrusadorzでレポジトリがLearn-moduleでブランチがdevelopの例となっている。

```terminal
npm i qrusadorz/Learn-module#develop
```
公式ドキュメントの書式は以下の通り。
https://docs.npmjs.com/cli-commands/install.html
>```npm install <githubname>/<githubrepo>[#<commit-ish>]：```

上記は簡略系で、バージョン指定や後述のプライベートレポジトリ指定の場合にはurlの形式を利用する。

>```<protocol>://[<user>[:<password>]@]<hostname>[:<port>][:][/]<path>[#<commit-ish> | #semver:<semver>]```

ちなみに指定のブランチに```package.json```がないとエラーになる。
エラー内容から判断するのは難しかった記憶があるため、必ず指定先に有効な```package.json```があることを確認したい。

#####プロジェクトからアンインストールする

```
npm uninstall learn-module
```

アンインストールではGitHubのUrl指定ではなく、```package.json```に記述されているパッケージ名であることに注意。


####GitHubを使ってプライベートレポジトリを共有する

プライベートレポジトリの場合、当然ですがURLを知っていても参照することはできません。
そのため、```npm i```ではパブリックレポジトリとは書式が異なってきます。

プライベートレポジトリを参照するため、GitHubのアクセストークンを生成します。
https://help.github.com/ja/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line

権限は```read:packages```と```repo```が必要になります。
https://help.github.com/en/github/managing-packages-with-github-packages/about-github-packages#about-tokens

生成したトークンをどうやって```npm i```に指定するかは、古いですがGitHubのブログに載っています。
https://github.blog/2012-09-21-easier-builds-and-deployments-using-git-over-https-and-oauth/

>```git clone https://<token>@github.com/owner/repo.git```
または
```git clone https://<token>:x-oauth-basic@github.com/owner/repo.git```

これを踏まえて、```npm i```ではこのように指定できます。
例では、tokenがxxxxxxxxx、ユーザーがqrusadorz、レポジトリがlearn-private-module、ブランチがdevelopの例となっている。

```terminal
npm i https://xxxxxxxxx@github.com/qrusadorz/learn-private-module#develop
```

これでプライベートレポジトリでもGCFにインストールできるようになります。
注意点として、残念なことにGitHubのトークンはレポジトリ単位ではなくユーザー単位で有効のため、package.jsonに直接記述して漏洩した場合、ユーザーの他のレポジトリが見られるリスクが伴います。
私は試せませんが、```git config```を利用した回避方法があるようなので、必要に応じて採用を検討してください。
https://stackoverflow.com/questions/23210437/npm-install-private-github-repositories-by-dependency-in-package-json

#Conclusion

JavaScriptとNode.jsでソース共有の手段として、コピペとパッケージ化で調査してみました。

コピペの方はesmを使ったものを掘り下げていないため、後日機会があれば更新するかもしれません。
まとめの段階で、この程度の差ならテキストの置換でいけるよなと思ったり:joy: **<2020/2/1> ``` module.exports =``` => ```export``` の置換でOK**
基本的にコピペは保守性の低さから嫌われるため、コピペのメリットが大きい場合にのみ使用してください。（チームプレイでバグ修正時にコピペみるとしんどい:cry:）

パッケージ化の方は実用性が高いため、十分に掘り下げました。
GitHubにより手軽な共有ができることで、受け入れやすいものになったと思います。
FaaSではトークンを隠せなそうなので完全解決とはなりませんでしたが、機密情報を一切含んでいないならトークンの使い捨てでしのげると思います。
パッケージにしてしまうといちいち更新してやらないといけないのかと思いましたが、@103ma2さんが記事にされているよう```npm link```を使ってシンボリックリンクを張るとこれを回避できそうです。
https://qiita.com/103ma2/items/284b3f00948121f23ee4


#Appendices
パッケージ化で作成したパブリックレポジトリ
https://github.com/qrusadorz/Learn-module

#References
本体に全て掲載