このブログのカテゴリページをもう少し見やすくすべく、「Force Dragging III - bl.ocks.org」のコードをベースにカテゴリをd3で視覚化してみることにした。
試した結果はいまいちだったが、そのコードをESMで書き換えてみようと思った。併せてフォールバックも実装して理解を深めておこうかなと。
まずはWebサーバーのMIME Type 'applicattion/javascript'に.mjs
を追加した。
# mime.types
application/javascript js mjs;
そしてd3.js
を<script>
タグで読み込むのをやめimport
で読み込むようにする。
// index.mjs
import * as d3 from '//d3js.org/d3.v4.min.js';
.
.
.
が、このコードはimport
するところで落ちた。ブラウザのimport
で読めるのはESMだけだからである。d3.v4.min.js
はUMDモジュールだから読み込めないのであった。
nodeだとCJSを読み込むことができるので同じようにできるかな?と思ってたけどそうじゃなかった。
d3.jsのESM化
解決のためd3
をESM化することにした。d3はrollupでバンドルされているので、出力フォーマットをumd
から、es
に変更してバンドルしなおした。
// d3のrollup.config.js
import ascii from "rollup-plugin-ascii";
import node from "rollup-plugin-node-resolve";
export default {
input: "index",
extend: true,
plugins: [node(), ascii()],
output: {
//file: "build/d3.js",
file: "build/d3.mjs", // <-- 拡張子を.mjsに変更
//format: "umd",
format: "es",// <-- esに変更
name: "d3"
}
};
上記の変更でできたd3.mjs
をimport
すると読み込むことができた。
// index.mjs
import * as d3 from './d3.mjs';
rollupでformat
をes
にすると、トップレベルのモジュールだけexport
のまま残し、トップレベルが依存するモジュールをすべてバンドルするようだ。
フォールバックの実装
フォールバックの実装はindex.mjs
をrollupに食わせて、iife
フォーマットで出力する。
rollup index.mjs --o index.js --f iife
そうすると、d3.jsをバンドルしたindex.jsファイルができる。
HTMLには<script nomodule src="xxx">
を追加する。
<!-- ESM -->
<script type="module" src="./index.mjs"></script>
<!-- フォールバック -->
<script nomodule src="./index.js"></script>
これで、ESM対応・非対応のブラウザ両方に対応することができる。
試してみてあらためて認識させられた点
nodeのimport
が使用できるのはfile:
のみ
http://
,https://
は使用できない。ただ今のところっぽいが。。
I:\pj\www\html\contents\dev\force-directed>node --experimental-modules ./index.mjs
(node:18136) ExperimentalWarning: The ESM module loader is experimental.
{ Error [ERR_INVALID_PROTOCOL]: Protocol "https:" not supported. Expected "file:"
at resolveRequestUrl (internal/loader/resolveRequestUrl.js:84:11)
at Loader.getModuleJob (internal/loader/Loader.js:50:27)
at ModuleWrap.module.link (internal/loader/ModuleJob.js:30:25)
at linked (internal/loader/ModuleJob.js:28:21)
at <anonymous> [Symbol(code)]: 'ERR_INVALID_PROTOCOL' }
nodeのimport
でUMDを読み込むことは可能
nodeでは下記のインポートが可能である。
import * as d3 from 'd3.v4.js';// <-- UMD
ブラウザのESMは拡張子を省略できない
import * sa d3 from './d3';// <-- ブラウザではNot Found. nodeではOK.
importを含むモジュールがブラウザ/nodeで共通化できる条件は?
- モジュールの相対パスが同じ
- 拡張子が省略されていない
- モジュールはESMのみ
import
している
場合に、import
を含むモジュールはブラウザ/nodeで共通化できる。
バンドル・ユーティリティは残るな..
d3
は現在はESMでソースコードをモジュール化している。ということはバンドルしなくてもESMでインポートできるが、巨大なライブラリなので大量のインポートが発生する。それは問題だろうからある程度の塊にバンドルされるのがトラフィックの効率化の観点からすれば望ましいよね。とするとすべてのモジュールがESM化されたとしても、rollupのようなバンドル・ユーティリティは残るということだよね。
動作サンプル
ソースコード・リソース
/dev/force/index-thumbnail.jpg
<!DOCTYPE html>
<html>
<header>
<meta charset="utf-8">
</header>
<body>
<canvas></canvas>
<script type="module" src="./index.mjs"></script>
<script nomodule src="./index.js"></script>
</body>
</html>
"use strict";
import * as d3 from './d3.mjs';
window.addEventListener('load', () => {
const width = 2048, height = 2048;
const canvas = document.querySelector("canvas");
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d");
window.scroll((width - window.innerWidth) / 2, (height - window.innerHeight) / 2);
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) { return d.id; }).distance(() => 200).strength(() => 0.001))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
d3.json("keywords.json", function (error, graph) {
if (error) throw error;
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
d3.select(canvas)
.call(d3.drag()
.container(canvas)
.subject(dragsubject)
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
function ticked() {
context.clearRect(0, 0, width, height);
context.beginPath();
graph.links.forEach(drawLink);
context.strokeStyle = "#aaa";
context.stroke();
context.beginPath();
graph.nodes.forEach(drawNode);
context.fill();
context.strokeStyle = "#fff";
context.stroke();
}
function dragsubject() {
return simulation.find(d3.event.x, d3.event.y);
}
});
function dragstarted() {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d3.event.subject.fx = d3.event.subject.x;
d3.event.subject.fy = d3.event.subject.y;
}
function dragged() {
d3.event.subject.fx = d3.event.x;
d3.event.subject.fy = d3.event.y;
}
function dragended() {
if (!d3.event.active) simulation.alphaTarget(0);
d3.event.subject.fx = null;
d3.event.subject.fy = null;
}
function drawLink(d) {
context.moveTo(d.source.x, d.source.y);
context.lineTo(d.target.x, d.target.y);
}
function drawNode(d) {
context.moveTo(d.x + 3, d.y);
context.arc(d.x, d.y, 3, 0, 2 * Math.PI);
context.fillText(d.id, d.x, d.y);
}
});