3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Grimoire.jsAdvent Calendar 2016

Day 21

フロントエンド用の複数ファイルからなるフレームワークでのnpmとscriptタグ両方で扱える仕組みの話をする

Last updated at Posted at 2016-12-23

拙作のWebGLフレームワーク**Grimoire.js**は、Typescriptで組まれていて、一方でユーザーはJavascriptからも扱うことができる。
これが案外難しいところがいくつもあってこれを解決した。実現のために色々とビルド環境を何回も作り変えた結果最終的にいい形に5回目の作り直しで落ち着いて纏まったのでこれについて書いていく。

また、誤解を招かぬよう先に注釈しておくが、Grimoire.jsのプラグイン作者は以下のことをほとんどきにする必要はない。grimoirejs-cauldronというスキャフォールダーが存在する。 しかし、具体的に中身のビルド周りやプラグイン連携の仕組みを俯瞰的に見るための文章であることに留意していただきたい。あるいは、同じような仕組みを持つライブラリを作りたい人の方が対象読者かもしれない。

問題点その1(ファイルを分割してrequireしたい)

このフレームワークは、複数のファイルから成り立っていて、それぞれのファイルはユーザーから参照され利用されうる可能性がある。また、browserifyなどのバンドリング環境が常に利用可能かもわからない。

npmからアクセスされる場合

例えば、以下のファイルA.ts,B.tsがあったとしよう。

src/A.ts
export default class A{}
src/B.ts
export default class B{}

npmから扱う場合は、requireあるいはimportをする場合は通常、package.jsonmain属性に、var lib = require("ライブラリ名")あるいはimport lib from "ライブラリ名"とすれば、mainに指定したファイルをユーザーは参照することができる。

しかし、あるライブラリが複数個のファイルを別々のファイルパスでrequireするときはどうすればいいのだろうか。

実は、この解決法はある。例えば、src/**/*.tsをそれぞれlib/**/*.jsにコンパイル結果として生成するなら、これらは以下のようにrequireすることができる。(package.jsonのpublish内容の設定にlibフォルダが含まれるよう設定する必要がある)

こうすれば、ユーザーはvar A = require("ライブラリ名/lib/A")あるいは、import A from "ライブラリ名/lib/A"とすればそれぞれのファイルを別々にrequireして参照することができる。

一般的にnpmに公開されている場合、単一のファイルをrequireして使う場合が多いが、このようにすれば、複数個のファイルを最終生成物にする場合でも用いれるし、ユーザーが自前でバンドリングするとしても、自分で使ったファイルだけをバンドリング対象に含めることができる。

scriptタグからアクセスする場合

僕のライブラリは、スクリプトタグから読み込まれた場合、window.grにメインとなるオブジェクトのデータを代入する。スクリプトタグからの参照の場合でも、requireのようなことをしたい。

となれば、何かしらのオブジェクトに代入する以外の手段はない。そこで、以下のように記述したら、src/A.tsのデフォルトエクスポートを取れるようにした。

index.js
var A = gr.src.A;

やり方は単純だ。webpackShellCommandPluginを用いて、npmのコマンドを呼べるので、npm scriptに記述したスクリプトを呼びうる。

実際には以下のように記述してnpmのスクリプトを呼び出している。

build.js
const shell = require("webpack-shell-plugin");
// ...returnでオブジェクトを作る部分
      plugins: [new shell({
      onBuildStart: "npm run generate-expose"
    })]

この、npm scriptで、srcの中身を読み取って**こんなtsファイル**を作成すれば、目的を達成できる。

気をつけなくてはならないのは、tsのdefault exportのpublic要素の中に、exportされていない型の参照が使われていると、エラーになる。そこで、多くの場合インターフェースも出力する必要がある。

問題点その2(バンドリングしても、中身を含めないようにしたい場合がある)

僕のライブラリは、プラグインによる拡張にかなり重きが置かれている。
ある人が簡単に作ったプラグインは、簡単にnpmにでき、かつscriptタグで簡単に利用できなければならない。

この際問題になるのが、ライブラリ同士の依存関係だ。grimoirejs-A,grimoirejs-Bというパッケージがあったとして、BがAを依存関係として参照してたとしよう。

この場合、もしgrimoirejs-Bをバンドリングしたものをスクリプトタグで読み込もうものなら、通常ならgrimoirejs-Aも読み込まれる。なぜなら、grimoirejs-Bのバンドリング結果にはgrimoirejs-Aも含まれるべきだからだ。

これだけならば問題は無い。しかし、ここでgrimoirejs-Cgrimoirejs-Aに依存していたとしよう。
もしもユーザーがnpmを用いてgrimoirejs-Bgrimoirejs-Cをrequireした場合、BとCの依存関係のバージョンがsemvar的に共存しても問題ないなら、通常通りgrimoirejs-Aを含めることができる。

しかし、scriptタグでは意味がない。grimoirejs-Bから読み込んでいるgrimoirejs-Agrimoirejs-Cから読み込んでいるgrimoirejs-Cから読み込んだgrimoirejs-Aは全くの別物だ。
Aの中でシングルトンであることを保証したかったとすれば、その実装はすぐに崩壊するだろう。

refフォルダの生成

そこで、実際のコードはは参照しないが、ブラウザ上からwindow.GrimoireJS利用して参照だけ取るようなコードを書いてみればいいことに気がつく。

refフォルダの中に、以下のようなA.jsとB.jsを生成してやる。

ref/A.js
Object.defineProperty(exports, "__esModule", {
   value: true
});
exports.default=window.gr.A;
ref/B.js
Object.defineProperty(exports, "__esModule", {
   value: true
});
exports.default=window.gr.B;

こうしてやれば、実際に別のスクリプトタグで読み込まれた対象しか参照しないのでスクリプトタグでうまく動作するし、バンドリングしたとしても、依存ライブラリが存在すれば動作をする。

refフォルダのd.ts

もし、Typescriptを利用するなら、refフォルダの対象を参照した時の内容が厄介であると気がつくだろう。
しかし、tsconfigでdeclarationの設定を変えて、このrefフォルダに生成されるようにすれば、Typescriptからは実態にアクセスしているように見えるため、型的には問題がなくなる。

この仕組みが結構素晴らしく、ユーザーがTypescriptを使う場合でも問題なく同じ仕組みが適用できることがわかる。

register/index.jsの作成

一方で、バンドリングして用いるなら、参照ではなく実際に実体を含めたい場合があるだろう。
そういう際に、require("ライブラリ名/register")とした時点で、バンドリングした内容に含まれるようにすればいい。

やり方は簡単で、register/index.jsとして、**あのtsファイル**からコンパイルされ、バンドリングされた結果を出力すればいいのだ。

最後に

webpackなどに慣れていない人からしたらものすごく複雑だし、こんなユースケースはあまりないのかもしれないが、ハリボテのように見えて案外うまく動作するウマいシステムではある。
今回は抽象化した説明しかしていないが、実際にGrimoire.jsでは、今回の説明を少し拡張した**ある仕様**を満たせばプラグインとして読み込める。もちろん、npmもスクリプトタグも問題なく相互に連携して運用が可能だ。

もし、興味があったら是非実際のビルド環境も**レポジトリ**から見ていただきたい。

3
3
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
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?