Electron と ESModules
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 0
◆ Electron だとローカルファイルが file スキームになって ESModules でロードできない
◆ 独自スキームを用意してそれを使ってロード必要
◆ import がパッケージ名だけでロードできないので バンドルなしで node_modules を使えない
◆ パッケージ内の import で相対か絶対パスじゃないものがあればエラー
◆ 依存解決してくれる CDN を使えばできるけど外部通信が必要になる
◆ 独自スキームを用意してそれを使ってロード必要
◆ import がパッケージ名だけでロードできないので バンドルなしで node_modules を使えない
◆ パッケージ内の import で相対か絶対パスじゃないものがあればエラー
◆ 依存解決してくれる CDN を使えばできるけど外部通信が必要になる
Electron をつかっても Chrome なら ESModules に対応してるしと いつもどおり ESModules を使ってたのですが Electron ならではの問題がありました
Electron アプリ内のファイルは普通にロードしようとした場合 file スキームになります
file スキームでは module のロードは禁止されてるのでエラーになります
対処方法はカスタム scheme を作って file スキーム以外にすることです
カスタム scheme を定義して そのスキームへのアクセスのハンドラでファイルのバッファと text/javascript の mime type を返せばロードできます
のようにカスタムスキームを使ってモジュールを指定します
require を使えばパッケージ名だけで node_modules を参照してくれるので node_modules にインストールしたパッケージをロードすることができますが import では相対パスか絶対パスで指定した URL が必要です
node_modules 内のパッケージを相対パスで import した場合も パッケージ名で require した場合も ロードするモジュールが中でパッケージ名を import 構文でロードしてればそこでエラーになります
ESModules ではモジュールの場所は相対パスか絶対パスを書く必要があります
Node.js の require みたいにパッケージ名だけ書けば既定の場所からロードしてくれはしません
しかし あるモジュールが外部パッケージを使うとき それがどこにあるかを事前に URL で示すのは無理です
ある CDN かもしれなければ別の CDN かもしれませんし 場合によってはローカルにあるかもしれません
外部パッケージとなるならパッケージ名だけを伝えて その解決は実行される環境次第で変わるのがベストだと思います
現実には ESModules では動かないものの パッケージ名を直接 import 構文の from に書いているものが多くあります
そのため node_modules のパッケージを ESModules でロードしても動きません
具体例だと lit-element では次のような import 文があります
同じパッケージ内のモジュールは相対パスですが 外部パッケージの lit-html は直接名前の指定です
lit-element を ./node_modules/lit-element/index.js と相対パスで指定してロードしても ライブラリ内で lit-html をロードするところでエラーが出てロードできません
unpkg では ?module をつけると CDN 内で依存解決されて自動で import 構文が unpkg 内の URL になるように変換されます
CDN は楽に使えますが 外部サービス依存になるので CDN 側のトラブルの影響を受けたり ネットが使えないと使えないとかがありますし Electron ならアプリ内に配置して Webサーバならそのサーバで配信するのが一番使えなくなる時間が小さくできます
あとモジュールの場合って大量にファイルをロードするからか unpkg 使うとときどき数秒待たされるくらいの遅さなんですよね
なので CDN ではなくローカルに保存したものを使いたいことも多いです
ローカルでやろうとすれば 自分で各モジュールのソースの import 構文を変更することになります
今のところ そういうツールも聞かないですし 結局 Webpack とかを使ってバンドルするということになると思います
バンドルなしで ESModules が直接使えるようになったと言ってもこういう問題がある以上は ESModules を直接使えず 今後もあまり使われないような気がします
import.searchPath でパッケージ名を指定したときのデフォルトロード先を指定できるとか パッケージ名だけでもロードできる機能を用意してくれれば解決すると思うのですけどねー
こういう問題があるので 前に作った wysiwyg エディタは CDN を使っていて本来ローカルだけで完結するものなのにネットワーク通信が必要です
Electron アプリ内のファイルは普通にロードしようとした場合 file スキームになります
file スキームでは module のロードは禁止されてるのでエラーになります
対処方法はカスタム scheme を作って file スキーム以外にすることです
protocol.registerSchemesAsPrivileged([{ scheme: "esm" }])
protocol.registerBufferProtocol("esm", async (req, cb) => {
const relpath = req.url.replace("esm://", "")
const filepath = path.resolve(__dirname, relpath)
const data = await fs.promises.readFile(filepath)
cb({ mineType: "text/javascript", data })
})
カスタム scheme を定義して そのスキームへのアクセスのハンドラでファイルのバッファと text/javascript の mime type を返せばロードできます
<script type="module" src="esm://index.js"></script>
のようにカスタムスキームを使ってモジュールを指定します
Electron での require と import
Electron で ESModules を使うと 自分で作ったモジュール中ではモジュールロードに import 構文を使って fs や path などの Node.js 機能は require というふうに使い分けることになりますrequire を使えばパッケージ名だけで node_modules を参照してくれるので node_modules にインストールしたパッケージをロードすることができますが import では相対パスか絶対パスで指定した URL が必要です
node_modules 内のパッケージを相対パスで import した場合も パッケージ名で require した場合も ロードするモジュールが中でパッケージ名を import 構文でロードしてればそこでエラーになります
ESModules 自体の問題
この問題は Electron というより ESModules 自体のもので普通の Chrome でもありえるものですESModules ではモジュールの場所は相対パスか絶対パスを書く必要があります
Node.js の require みたいにパッケージ名だけ書けば既定の場所からロードしてくれはしません
しかし あるモジュールが外部パッケージを使うとき それがどこにあるかを事前に URL で示すのは無理です
ある CDN かもしれなければ別の CDN かもしれませんし 場合によってはローカルにあるかもしれません
外部パッケージとなるならパッケージ名だけを伝えて その解決は実行される環境次第で変わるのがベストだと思います
現実には ESModules では動かないものの パッケージ名を直接 import 構文の from に書いているものが多くあります
そのため node_modules のパッケージを ESModules でロードしても動きません
具体例だと lit-element では次のような import 文があります
import { TemplateResult } from 'lit-html';
import { render } from 'lit-html/lib/shady-render.js';
import { UpdatingElement } from './lib/updating-element.js';
同じパッケージ内のモジュールは相対パスですが 外部パッケージの lit-html は直接名前の指定です
lit-element を ./node_modules/lit-element/index.js と相対パスで指定してロードしても ライブラリ内で lit-html をロードするところでエラーが出てロードできません
unpkg では ?module をつけると CDN 内で依存解決されて自動で import 構文が unpkg 内の URL になるように変換されます
import { TemplateResult } from "https://unpkg.com/lit-html@^1.0.0?module";
import { render } from "https://unpkg.com/lit-html@^1.0.0/lib/shady-render.js?module";
import { UpdatingElement } from "./lib/updating-element.js?module";
CDN は楽に使えますが 外部サービス依存になるので CDN 側のトラブルの影響を受けたり ネットが使えないと使えないとかがありますし Electron ならアプリ内に配置して Webサーバならそのサーバで配信するのが一番使えなくなる時間が小さくできます
あとモジュールの場合って大量にファイルをロードするからか unpkg 使うとときどき数秒待たされるくらいの遅さなんですよね
なので CDN ではなくローカルに保存したものを使いたいことも多いです
ローカルでやろうとすれば 自分で各モジュールのソースの import 構文を変更することになります
今のところ そういうツールも聞かないですし 結局 Webpack とかを使ってバンドルするということになると思います
バンドルなしで ESModules が直接使えるようになったと言ってもこういう問題がある以上は ESModules を直接使えず 今後もあまり使われないような気がします
import.searchPath でパッケージ名を指定したときのデフォルトロード先を指定できるとか パッケージ名だけでもロードできる機能を用意してくれれば解決すると思うのですけどねー
こういう問題があるので 前に作った wysiwyg エディタは CDN を使っていて本来ローカルだけで完結するものなのにネットワーク通信が必要です