ビルドシステムを使わずにフロントエンドのJavaScriptライブラリを読み込む
原文は Julia Evans により に公開されました。 このブログを購読する
私はビルドシステムを使わずにJavaScriptを書くのが好きなのですが、昨日、またしても——もう何度目かわからないくらいですが——ビルドシステムなしでJavaScriptライブラリを読み込む方法を調べなければならない場面に遭遇しました。そして、そのライブラリのセットアップ手順はビルドシステムを使っていることを前提に書かれているため、読み込み方を理解するのにとてつもなく時間がかかってしまいました。
幸い、今ではこうした状況への対処法をだいたい学び、ライブラリをなんとか使いこなすか、難しすぎると判断して別のライブラリに乗り換えるかができるようになりました。そこで、数年前にあったらよかったと思う、JavaScriptライブラリの読み込み方ガイドをここにまとめます。
この記事では、フロントエンドでJavaScriptライブラリを使う方法だけを扱います。しかも、ビルドシステムを使わない構成で使う方法に絞ります。
この記事では、以下のことについて話します:
- ライブラリが提供する可能性のある3つの主要なJavaScriptファイルの種類(ES Modules、グローバル変数を定義する「クラシック」なタイプ、CommonJS)
- JavaScriptライブラリのビルドにどの種類のファイルが含まれているかを見分ける方法
- それぞれの種類のファイルをコードで読み込む方法
3種類のJavaScriptファイル
ライブラリが提供しうるJavaScriptファイルには、基本的に3つの種類があります:
- グローバル変数を定義する「クラシック」なタイプのファイル。単に
<script src>で読み込めばそのまま動く種類のものです。手に入れば最高ですが、常にあるわけではありません - ESモジュール(他のファイルに依存している場合も、していない場合もあります。これについては後で触れます)
- 「CommonJS」モジュール。これはNode用のもので、ビルドシステムなしではブラウザでまったく使えません。
「クラシック」なタイプにもっと良い呼び名があるのかどうかはわかりませんが、ここでは単に「クラシック」と呼ぶことにします。また、「AMD」という種類もありますが、2024年時点でどれだけ関係があるのかはよくわかりません。
3つのファイルの種類がわかったところで、ライブラリが実際にどれを提供しているかを見分ける方法について話しましょう!
ファイルはどこにある? NPMのビルド
すべてのJavaScriptライブラリには、NPMにアップロードされるビルドがあります。こう思うかもしれません(私も最初はそう思いました)——「ジュリア!要点はNodeでライブラリをビルドしないことなのに!なぜNPMの話をしているの?」
でも、https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.jsのようなCDNのリンクを使っている場合でも、実はNPMのビルドを使っているのです!CDN上にあるファイルは、元をたどればすべてNPMから来ています。
だから私は、たとえNodeでライブラリをビルドするつもりがまったくなくても、ときどきあえてnpm installでライブラリをインストールすることがあります。適当な一時フォルダを作ってそこでnpm installし、終わったら削除するだけです。ファイルシステム上でNPMビルドの中身をあれこれ覗けるのが気に入っています。そうすれば、ライブラリがビルドで公開しているものをすべて見ていると100%確信できますし、CDNが何かを隠しているということもありません。
というわけで、いくつかのライブラリをnpm installして、それぞれのビルドでどんな種類のJavaScriptファイルが提供されているか見てみましょう!
ライブラリ例1: chart.js
まずはグラフ描画ライブラリであるChart.jsの中身を見てみましょう。
$ cd /tmp/whatever
$ npm install chart.js
$ cd node_modules/chart.js/dist
$ ls *.*js
chart.cjs chart.js chart.umd.js helpers.cjs helpers.js
このライブラリには、基本的に3つの選択肢があるようです:
選択肢1: chart.cjs。.cjsという拡張子から、これがNodeで使うためのCommonJSファイルだとわかります。つまり、何らかのビルドステップなしではブラウザで直接使うことは不可能です。
選択肢2:chart.js。.jsという拡張子だけではどんな種類のファイルかわかりませんが、中を開いてみるとimport '@kurkle/color';と書かれており、これがESモジュールであることの明確な証拠です——import ...という構文はESモジュールの構文です。
選択肢3: chart.umd.js。「UMD」は「Universal Module Definition」の略で、たしかこのファイルは基本的な<script src>でも、CommonJSでも、あるいはAMDというよくわからない第三の方式でも使える、という意味だと思います。
UMDファイルの使い方
Chart.jsを使うとき、私は選択肢3を選びました。コードにこれを追加するだけで済みました:
<script src="./chart.umd.js"> </script>
すると、グローバルなChart変数でライブラリを使えるようになりました。これ以上ないくらい簡単です。NPMを使ったりCDNが落ちたりすることを気にしなくて済むように、chart.umd.jsをそのままGitリポジトリにコピーしました。
ビルドファイルは必ずしもdistディレクトリにあるわけではない
多くのライブラリはビルド成果物をdistディレクトリに置きますが、必ずしもそうとは限りません!ビルドファイルの場所は、ライブラリのpackage.jsonで指定されています。
たとえば、Chart.jsのpackage.jsonからの抜粋がこちらです。
"jsdelivr": "./dist/chart.umd.js",
"unpkg": "./dist/chart.umd.js",
"main": "./dist/chart.cjs",
"module": "./dist/chart.js",
これは、ESモジュール(module)を使いたいならdist/chart.jsを使うべきで、jsDelivrやunpkgのCDNは./dist/chart.umd.jsを使うべきだ、と言っているのだと思います。mainはNode用なのでしょう。
chart.jsのpackage.jsonには"type": "module"とも書かれています。これはこのドキュメントによると、NodeにファイルをデフォルトでESモジュールとして扱うよう指示するものです。どのファイルがESモジュールでどれがそうでないかを具体的に教えてくれるわけではないと思いますが、少なくともその中に何かしらESモジュールがあることはわかります。
ライブラリ例2: @atcute/oauth-browser-client
@atcute/oauth-browser-clientは、ブラウザでOAuthを使ってBlueskyにログインするためのライブラリです。
このビルドではどんな種類のJavaScriptファイルが提供されているか見てみましょう!
$ npm install @atcute/oauth-browser-client
$ cd node_modules/@atcute/oauth-browser-client/dist
$ ls *js
constants.js dpop.js environment.js errors.js index.js resolvers.js
ここで唯一あり得そうなエントリーポイントはindex.jsで、中身はこんな感じです:
export { configureOAuth } from './environment.js';
export * from './errors.js';
export * from './resolvers.js';
このexport構文は、これがESモジュールであることを意味します。つまり、ビルドステップなしでブラウザで使えるということです!その方法を見てみましょう。
import mapを使ってESモジュールを使う方法
ESモジュールを使うのは、単に<script src="whatever.js">を追加するほど簡単ではありません。代わりに、ESモジュールが依存関係を持っている場合(@atcute/oauth-browser-clientのように)、手順は次のようになります:
- HTMLでimport mapを設定する
- JSコード内に
import { configureOAuth } from '@atcute/oauth-browser-client';のようなimport文を書く - HTMLでJSコードを
<script type="module" src="YOURSCRIPT.js"></script>のように読み込む
import { BrowserOAuthClient } from "./oauth-client-browser.js"のようにするだけではなくimport mapが必要な理由は、モジュールの内部でimport {something} from @atcute/clientのようなimport文がさらに使われており、ブラウザに対して@atcute/clientやその他の依存関係のコードをどこから取得するかを教える必要があるからです。
@atcute/oauth-browser-clientで私が使ったimport mapはこんな感じです:
<script type="importmap">
{
"imports": {
"nanoid": "./node_modules/nanoid/bin/dist/index.js",
"nanoid/non-secure": "./node_modules/nanoid/non-secure/index.js",
"nanoid/url-alphabet": "./node_modules/nanoid/url-alphabet/dist/index.js",
"@atcute/oauth-browser-client": "./node_modules/@atcute/oauth-browser-client/dist/index.js",
"@atcute/client": "./node_modules/@atcute/client/dist/index.js",
"@atcute/client/utils/did": "./node_modules/@atcute/client/dist/utils/did.js"
}
}
</script>
このimport mapを動くようにするのはかなり面倒です。自動で生成してくれるツールが絶対あるはずだと思うのですが、まだ見つけられていません。esbuildのmetafileを使ってimport mapを自動生成するスクリプトを書くことは確実に可能ですが、私はまだやっていませんし、もっと良い方法があるかもしれません。
昨日、github.com/jvns/bsky-oauth-exampleを動かすためにimport mapを設定することにしたので、そのリポジトリにサンプルコードがあります。
また、誰かがSimon Willisonのdownload-esmを教えてくれました。これはESモジュールをダウンロードして、importがJSファイルを直接指すように書き換えてくれるので、import mapが不要になります。まだ試していませんが、素晴らしいアイデアだと思います。
import mapの問題点: ファイルが多すぎる
ただ、ブラウザでimport mapを使う際にいくつか問題に遭遇しました——サイトを読み込むのに何十ものJavaScriptファイルをダウンロードする必要があり、なぜか開発中のウェブサーバーがそれに追いつけなかったのです。ファイルの読み込みがランダムに失敗するのを何度も目にし、そのたびにページを再読み込みして今度はうまくいくことを祈るしかありませんでした。
サイトを本番環境にデプロイしたらもう問題は起きなかったので、おそらくローカルの開発環境の問題だったのだと思います。
また、ESモジュール全般について少し面倒なのは、使うためにウェブサーバーを立ち上げる必要があることです。きっとちゃんとした理由があるのでしょうが、ウェブサーバーを起動せずにindex.htmlを直接開くだけで済む方が楽なのは確かです。
「ファイルが多すぎる」という問題があるため、この方法でimport mapと一緒にESモジュールを使うのは正直あまり魅力的に感じませんが、可能だということを知っておくのは良いことです。
import mapなしでESモジュールを使う方法
もしESモジュールが依存関係を持っていなければ、さらに簡単です——import mapは必要ありません!次のようにするだけです:
- HTMLに
<script type="module" src="YOURCODE.js"></script>を書く。type="module"が重要です。 YOURCODE.jsの中にimport {whatever} from "https://example.com/whatever.js"と書く
代替案: esbuildを使う
import mapを使いたくない場合は、esbuildのようなビルドシステムを使うこともできます。その方法についてはSome notes on using esbuildで触れましたが、このブログ記事はビルドシステムを完全に避ける方法についてなので、ここではその選択肢については触れません。とはいえ私は今でもesbuildが好きですし、このケースでも良い選択肢だと思います。
import mapのブラウザ対応状況は?
CanIUseによると、import mapは「Baseline 2023: 主要ブラウザで新たに利用可能」となっているので、私の感覚では2024年時点ではまだ少し新しすぎるかもしれません。自分とあと12人くらいだけが使うような楽しい実験的なコードならimport mapを使うと思いますが、もっと広く使われるコードにしたいなら、代わりにesbuildを使うでしょう。
ライブラリ例3: @atproto/oauth-client-browser
最後にもう一つ、別のライブラリ例を見てみましょう!これは@atcute/oauth-browser-clientとは異なるBluesky認証用ライブラリです。
$ npm install @atproto/oauth-client-browser
$ cd node_modules/@atproto/oauth-client-browser/dist
$ ls *js
browser-oauth-client.js browser-oauth-database.js browser-runtime-implementation.js errors.js index.js indexed-db-store.js util.js
またしても、唯一の有力な候補ファイルはindex.jsのようです。でも今回は前の例のライブラリとは状況が違います!index.jsの中身を見てみましょう:
index.jsの中には、こんなコードがたくさんあります:
__exportStar(require("@atproto/oauth-client"), exports);
__exportStar(require("./browser-oauth-client.js"), exports);
__exportStar(require("./errors.js"), exports);
var util_js_1 = require("./util.js");
このrequire()という構文はCommonJSの構文で、つまりこのファイルはブラウザではまったく使えず、何らかのビルドステップが必要だということです。esbuildでも動きません。
また、このライブラリのpackage.jsonには"type": "commonjs"と書かれており、これもCommonJSであることを示す別の手がかりです。
esm.shを使ってCommonJSモジュールを使う方法
当初、私はビルドシステムを学ばずにCommonJSモジュールを使うのは不可能だと思っていました。でもBlueskyで誰かがesm.shについて教えてくれたのです!これはあらゆるものをESモジュールに変換してくれるCDNです。skypack.devも似たようなことをしており、違いが何なのかはよくわかりませんが、片方が動かないときはもう片方を試すという人もいました。
@atproto/oauth-client-browserでそれを使うのはかなりシンプルなようです。HTMLにこれを書くだけです:
<script type="module" src="script.js"> </script>
そしてscript.jsにこれを書きます。
import { BrowserOAuthClient } from "https://esm.sh/@atproto/[email protected]"
なんだかそのまま動くようで、すごいです!もちろん、これはある意味まだビルドシステムを使っていることになります——自分ではなくesm.shがビルドを実行してくれているだけです。このアプローチについて私が主に懸念しているのは次の点です:
- CDNがずっと動き続けるとはあまり信用していません——たいていは、将来何らかの理由で消えてしまわないように、依存関係をリポジトリにコピーしておくのが好きです。
- CDNがセキュリティ侵害を受けたという話を聞いたことがあり、それが怖いです。
- esm.shが実際に何をやっているのか、よくわかっていません。
esbuildでもCommonJSモジュールをESモジュールに変換できる
esbuildを使ってCommonJSモジュールをESモジュールに変換することもできると知りました。ただし、いくつか制限があります——import { BrowserOAuthClient } fromという構文は動きません。こちらにGitHub issueがあります。
esbuildのアプローチの方が、すでに自分のコンピュータにあるツールなのでより信頼でき、esm.shのアプローチよりも魅力的に感じます。ただ、まだあまり試してはいません。
3種類のファイルのまとめ
ここで、遭遇するかもしれない3種類のJSファイルと、それぞれの使い方の選択肢、そして見分け方をまとめます。
親切とは言えませんが、.jsや.min.jsという拡張子のファイルはこの3つのどれである可能性もあるので、ファイルがsomething.jsだった場合は、自分が何を扱っているのか突き止めるためにもう少し探偵のような作業が必要です。
- 「クラシック」なJSファイル
- 使い方:
<script src="whatever.js"></script> - 見分け方:
- ウェブサイトのセットアップ手順に「CDNで使ってください!」のような大きくて親切なバナーが表示されている
.umd.jsという拡張子- とりあえず
<script src=...タグに入れてみて、動くかどうか試してみる
- 使い方:
- ESモジュール
- 使い方:
- 依存関係がなければ、コード内で単に
import {whatever} from "./my-module.js"とする - 依存関係がある場合は、import mapを作成して
import {whatever} from "my-module"とする- またはdownload-esmを使ってimport mapの必要性をなくす
- esbuildやその他のESモジュールバンドラーを使う
- 依存関係がなければ、コード内で単に
- 見分け方:
importやexport文を探す(module.exports = ...はCommonJSなので注意).mjsという拡張子- たぶん
package.json内の"type": "module"(ただし、これが正確にどのファイルを指すのかは私にはよくわかりません)
- 使い方:
- CommonJSモジュール
- 使い方:
- https://esm.shを使ってESモジュールに変換する(例:
https://esm.sh/@atproto/[email protected]) - 何らかの方法でビルドする(??)
- https://esm.shを使ってESモジュールに変換する(例:
- 見分け方:
- コード内で
require()やmodule.exports = ...を探す .cjsという拡張子- たぶん
package.json内の"type": "commonjs"(ただし、これが正確にどのファイルを指すのかは私にはよくわかりません)
- コード内で
- 使い方:
ESモジュールが標準化されているのは本当に素晴らしい
私の視点から見たCommonJSモジュールとESモジュールの主な違いは、ESモジュールが実際に標準であるということです。そのおかげで、使うときにとても安心できます。なぜなら、ブラウザはウェブ標準の後方互換性を永遠に保つことを約束しているからです——今日ESモジュールを使ってコードを書けば、15年後も同じように動き続けると確信できます。
また、esbuildのようなツールを使うことについても安心感があります。たとえesbuildプロジェクト自体がなくなったとしても、それが標準を実装しているものなので、将来それに置き換えられる似たようなツールがきっと現れるだろうと思えるからです。
JSコミュニティは本当にクールなツールをたくさん作ってきた
こういう話をすると、よく「JavaScript大嫌い!!!最悪!!!」のような反応が返ってきます。でも私の経験では、JavaScriptには素晴らしいツールがたくさんあります(昨日知ったばかりのhttps://esm.shも素晴らしそうです!esbuildも大好きです!)。そして、時間をかけて仕組みを学べば、そうしたツールの一部を活用して、生活をもっと楽にできると思っています。
なので、この記事の目的は決してJavaScriptについて文句を言うことではなく、状況を理解して、自分にとって心地よい形でツールを使えるようになることです。
まだ残っている疑問
まだ残っている疑問をここに挙げておきます。答えがわかったら、この記事に追記します。
- ローカルにセットアップしたESモジュールに対して、自動でimport mapを生成してくれるツールはある?(どうやらあるようです: jspm)
- https://esm.shがやっているように、自分のコンピュータ上でCommonJSモジュールをESモジュールに変換するにはどうすればいい?(どうやらesbuildでなんとなくできるようですが、名前付きexportは動きません)
- 人々が通常CommonJSモジュールを普通のJSコードにビルドするとき、実際に何のコードがそれをやっているの?もちろんwebpack、rollup、esbuildなどのツールはありますが、それらのツールはそれぞれ独自のJSパーサー/静的解析を実装しているの?世の中にはJSパーサーがいくつあるの?
- ESモジュールを単一のファイル(例えば
atcute-client.js)にバンドルしつつ、ブラウザではそのファイルから複数の異なるパス(例えば@atcute/client/lexiconsと@atcute/clientの両方)をimportできるようにする方法はある?
登場したツールまとめ
この記事で触れたすべてのツールのリストがこちらです:
- Simon Willisonのdownload-esm —— ESモジュールをダウンロードして、importがJSファイルを直接指すように変換してくれるのでimport mapが不要になります
- https://esm.sh/とskypack.dev
- esbuild
- JSPMはimport mapを生成できます
この記事を書いて、普段はプロジェクトを更新するたびに実行するようなビルドは持ちたくないと思っているけれど、プロジェクトのセットアップ時に一度だけ実行し、依存関係のバージョンを更新するとき以外は二度と実行しないようなビルドステップ(download-esmなどを使ったもの)なら、あってもいいかもしれないと思うようになりました。
以上です!
この記事の多くのことを教えてくれたMarco Rogersに感謝します。この記事にはおそらく間違いがあると思いますが、ぜひ教えてください——BlueskyやMastodonで教えてもらえると嬉しいです!
記事をランダムに読む
コメント
ログインしてコメントする