背景
ブラウザの標準機能(type=module)でモジュールを扱いたいと思いました。近い将来古いブラウザに縛られる必要性が薄れ、ネイティブなJavaScriptで十分なエコシステムを築けるのではないでしょうか。ブラウザの基本機能でいけるのが最も簡単なので、そうなってほしいと思っています。
ブラウザの標準機能でモジュールを使うとしても、モジュールを管理する方法は必要です。モジュール管理といえばやはりnpmかなぁと思います。node.js用のモジュール管理ツールですが、これからのブラウザのモジュール管理にも使えるのではないかと思いました。
Verdaccioを使えば、自前のnpmサーバもできるので、モジュール管理も簡単です。
しかし、モジュール開発後、npmでモジュールを管理し、ブラウザのimportでモジュールを呼び出すと問題が起こりました。
それはブラウザのimportが絶対もしくは相対のURLしか使えないために、2重の依存関係があるときにモジュールをうまく呼び出せなくなるということです。対処法に悩みましたので、ここに書き残しておきます。
要約
開発時に./node_modules/moduleA/index.jsとかで参照していたモジュールは、モジュール配布時には並列のディレクトリ構造化におかれるので参照できなくなる。
/scripts/node_modules/moduleB/node_modules/moduleA/index.js
が見つからない等といわれてしまう。
考えた解決方法は次の2つです。
(方法1)
シンボリックリンクでnode_modulesの中のモジュールにリンクを張るとともに、importのfromの先をシンボリックリンクの場所にする
export const funcA = (a)=> 2*a
import {funcA} from "../moduleA/index.js"
const a = funcA(1)
console.log(a) // -> 2
const funcB = (b)=>3*b
export {funcB}
import {funcB} from "../moduleB/index.js"
const b = funcB(1)
console.log(b) // -> 3
(方法2)
絶対参照で参照しやすいURLにすべてのモジュールをおく。
export const funcA = (a)=> 2*a
import {funcA} from "/modules/node_modules/moduleA/index.js"
const a = funcA(1)
console.log(a) // -> 2
const funcB = (b)=>3*b
export {funcB}
import {funcB} from "/modules/node_modules/moduleB/index.js"
const b = funcB(1)
console.log(b) // -> 3
#内容
依存が1階層の場合は問題ない
moduleBがmoduleAに依存しているケース
moduleB → moduleA
moduleAディレクトリを用意して、その中で
npm init
を実行し、モジュール管理をする準備をした後、index.jsを作成し、保存します。
verdaccioでローカルのリポジトリをしているとして、次のコマンドでリポジトリにpushします。
npm publish
続いて、moduleBディレクトリを作り、moduleAと同様の操作を行ったのち、
npm install moduleA
でmoduleAをローカルにインストールします。
これで下記のようなディレクトリ構成となります。
scripts
|- moduleA
| |- index.js
| |- package.json
scripts
|- moduleB
| |-index.js
| |-package.json
| |-package-lock.json
| |-node_modules
| |- moduleA
| |- index.js
| |- package.json
export const funcA = (a)=> 2*a
import {funcA} from "./node_modules/moduleA/index.js"
const a = funcA(1)
console.log(a) // -> 2
const funcB = (b)=>b*3
export {funcB}
moduleBの中からmoduleAを呼ぶには、"./node_modules/index.js"を参照すればいけます。
依存が深い場合に問題
moduleCがmoduleBに依存し、moduleBがmoduleAに依存しているケース
moduleC → moduleB → moduleA
上記の依存が浅い場合のmoduleBをnpm publishでレポジトリに登録して、moduleCのディレクトリ下でnpm install moduleBを実行すると下記のようなディレクトリ構成になります。moduleBの依存関係をnpmが解釈して、moduleAもインストールしてくれますが、ディレクトリ構成が並列になるので、上記のようにmoduleBでmoduleAをimportしていると、パスが存在せずエラーになります。
scripts
|- moduleC
| |-index.js
| |-package.json
| |-package-lock.json
| |-node_modules
| |- moduleA
| | |- index.js
| | |- package.json
| |- moduleB
| | |- index.js
| | |- package.json
import {funcA} from "./node_modules/moduleB/index.js"
const c = funcA(1)
console.log(c) // -> 2
/scripts/node_modules/moduleB/node_modules/moduleA/index.js
が見つからないとブラウザでエラーが出る。入れ子の構造でimporしようとするけれど、npmのインストールは依存関係にあるモジュールをすべてnode_modulesの中に並列に置くので、開発時に想定してたURL構造がモジュール配布時に壊れてしまいます。
方法
方法1: シンボリックリンクを張る
モジュールの開発時にシンボリックリンクを使い、その場所を参照するようにすることで、モジュール配布時と同じパスにする方法です。
moduleAに依存したmoduleBを開発するとき、下記のようにmoduleBと並列にmoduleAへのシンボリックリンクを用意することで、moduleBの中でのmoduleAの呼び出しをモジュール配布時と同じにできます。node_modulesと書かなくてよいので、見た目もいい感じになります。
scripts
|- moduleAへのシンボリックリンク
|- moduleB
| |-index.js
| |-package.json
| |-package-lock.json
| |-node_modules
| |- moduleA
| |- index.js
| |- package.json
import {funcA} from "../moduleA/index.js"
const b = funcA(1)
console.log(b) // -> 2
export {funcA}
このmoduleBにさらに依存するmoduleCを開発する時も同じです。
scripts
|- moduleAへのリンク
|- moduleBへのリンク
|- moduleC
| |-index.js
| |-package.json
| |-package-lock.json
| |-node_modules
| |- moduleA
| | |- index.js
| | |- package.json
| |- moduleB
| | |- index.js
| | |- package.json
import {funcB} from "../moduleB/index.js"
const b = funcB(1)
console.log(b) // -> 3
ここで、注意しないといけないのが、moduleCから直接moduleAを呼んでいなくても、moduleAへのシンボリックリンクを張っておかないといけないということです。moduleBからmoduleAを呼ぶに行くとき、moduleBのindex.jsはmoduleBのシンボリックリンクの下の階層にいることになっているからです(ブラウザからするとnode_modulesの下にいることにはなっていません)。
方法2: 絶対パスで参照できるわかりやすい場所にインストールする
モジュールを開発した後は、開発者がアクセスできる分かりやすい場所にモジュールをまとめておけば、絶対パスで参照しに行くだけなので、簡単です。しかし、この方法を使うと他の環境に持っていくと全然動かない可能性があります。また、node.jsで使えなくなってしまって、ユニバーサル性を保てなくなるように思います。また、依存関係を書かなくても使えてしまうため、依存関係を書き忘れないか心配です。
modules
|- package.json
|- node_modules
| |- moduleA
| | |- index.js
| | |- package.json
| |- moduleB
| | |- index.js
| | |- package.json
import {funcA} from "/modules/node_modules/moduleA/index.js"
const a = funcA(1)
console.log(a) // -> 2
const funcB = (b)=>3*b
export {funcB}
moduelCを開発するときはmoduleBを絶対参照で呼べば大丈夫です。
moduleCを開発し終わった後はmoduleBと同じ階層に入れておけば、他からmoduleCを呼ぶこともできます。
import {funcB} from "/modules/node_modules/moduleB/index.js"
const b = funcB(1)
console.log(b) // -> 3
感想
ブラウザ標準機能のimport, export周りをどうやってかくべきなのか、日々悩んでいます。今回、モジュールの入れ子依存問題に数時間頭を悩ませました。最初は方法2で行こうと思ったのですが、絶対パス書くのは汎用性に欠けるのではないかと思ったのと、node.jsで動かなさそうと思ったのでしばらく方法1で行ってみようかと思います。