LoginSignup
9
11

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-27

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の設定を変更できない縛りがあります。
自身の環境でこの辺の縛りがなければ、ここでは採用できなかった方法を採用するのもありかもしれません。

問題点

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

frontend.js

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

export default { display }

backend.js

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なら以下のようになり、単純にexportmodule.exports =の違いにできました。
簡単にはできないとモジュール化まで記事を書いたわけですが、真実は機械的にフォルダ配下のファイル一括置換で行けてしまいます:joy:

frontend.js

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

export { display }

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

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には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を作成する。

npm init

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

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の形式で記述する。

index.js
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:packagesrepoが必要になります。
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を使ったものを掘り下げていないため、後日機会があれば更新するかもしれません。
まとめの段階で、この程度の差ならテキストの置換でいけるよなと思ったり: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

本体に全て掲載

9
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
11