#Overview
フロントエンドはReact、バックエンドはNode.jsで作っていると、どちらでも利用したい処理が出てきます。
両方とも同一言語なので、バックエンドをNode.jsにしたメリットを享受できます
…そう思っていたのですが、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の設定を変更できない縛りがあります。
自身の環境でこの辺の縛りがなければ、ここでは採用できなかった方法を採用するのもありかもしれません。
##問題点
フロントエンドとバックエンドでは以下のように微妙に異なる。
export const display = (text) => console.log("display1:" + text);
export default { display }
const display = (text) => console.log("display1:" + text);
module.exports = { display };
フロントエンドはエクスポートする場合export
キーワードが必須、バックエンドはそれが不要でmodule.exports
に代入する形と少しだけ異なる。
プログラムの条件分岐で何とかできないかと試行錯誤したが、関数の前に入れるexport
がどうにもできず断念。
ちなみにmodule.exports
の方式でReactを実行すると実行時エラーになる。
TypeError: Cannot assign to read only property 'exports' of object '#<Object>'
そうなると何とかしてどちらかの記述に寄せる方法を探る必要がある。
それを調べていくことにした。
<2020/2/1追記>
export default
ではなくexport
なら以下のようになり、単純にexport
かmodule.exports =
の違いにできました。
簡単にはできないとモジュール化まで記事を書いたわけですが、真実は機械的にフォルダ配下のファイル一括置換で行けてしまいます
const display = (text) => console.log("display1:" + text);
export { display }
ただし、defaultの指定がないため、インポート側は下記のようにインポートするものを明示的に指定する必要があります。
// 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を使用する場合、エントリーポイントへの介入は厳しいと考えていたため、再度こちらになびくかも?
###JavaScript側でrequire/exportsのサポート状況はどうなっている?
今回のエラーはWebPackによるものだと判明している。
https://github.com/webpack/webpack/issues/4039
WebPackの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のドキュメントを見たが、なんか大変そうだなぁという印象
何よりプライベートパッケージは有料プランであるのがきつい。
https://docs.npmjs.com/about-private-packages
個人向け、もしくは企業の一人当たりの料金は7ドル/月。
https://www.npmjs.com/products
フロントエンドとバックエンドでソースを共有したいだけなのに7ドルにアカウント管理とか面倒
かなりネガティブな印象でドキュメントを漁っていたら…いいのありました!
###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を作成する。
npm init
もしimport/export
の形式を使いたい場合、コピペの時にも出てきたesm
を使って以下のコマンドで作成できる。
詳しくは公式ドキュメントを参考にしてほしい。
https://docs.npmjs.com/cli-commands/init.html
npm init esm
基本的にEnterキーで入力なしで進めればいいが、私が入力したのは以下の項目。
変更したいならpackage.jsonを直接修正すればいいので特にここで入力しないといけないというものではない。
- description
- パッケージの概要を記入。
- author
- 自身のユーザーネームを記入
- 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
の形式で記述する。
const display = (text) => console.log("display1:" + text);
module.exports = { display };
これをgit commit
してプッシュすれば、GitHubに反映されるのでパッケージの準備は完了。
#####プロジェクトにインストールする
プロジェクトにパッケージをインストールする。
例では、ユーザーがqrusadorzでレポジトリがLearn-moduleでブランチがdevelopの例となっている。
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の例となっている。
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を使ったものを掘り下げていないため、後日機会があれば更新するかもしれません。
まとめの段階で、この程度の差ならテキストの置換でいけるよなと思ったり <2020/2/1> ``` module.exports =``` => export
の置換でOK
基本的にコピペは保守性の低さから嫌われるため、コピペのメリットが大きい場合にのみ使用してください。(チームプレイでバグ修正時にコピペみるとしんどい)
パッケージ化の方は実用性が高いため、十分に掘り下げました。
GitHubにより手軽な共有ができることで、受け入れやすいものになったと思います。
FaaSではトークンを隠せなそうなので完全解決とはなりませんでしたが、機密情報を一切含んでいないならトークンの使い捨てでしのげると思います。
パッケージにしてしまうといちいち更新してやらないといけないのかと思いましたが、@103ma2さんが記事にされているようnpm link
を使ってシンボリックリンクを張るとこれを回避できそうです。
https://qiita.com/103ma2/items/284b3f00948121f23ee4
#Appendices
パッケージ化で作成したパブリックレポジトリ
https://github.com/qrusadorz/Learn-module
#References
本体に全て掲載