d3をESM対応してみた話

公開:2017-09-18 02:10
更新:2017-11-15 21:58
カテゴリ:es6 modules,.mjs,ESM,d3,rollup.js

このブログのカテゴリページをもう少し見やすくすべく、「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.mjsimportすると読み込むことができた。

// index.mjs
import * as d3 from './d3.mjs';

rollupformatesにすると、トップレベルのモジュールだけ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で共通化できる条件は?

場合に、importを含むモジュールはブラウザ/nodeで共通化できる。

バンドル・ユーティリティは残るな..

d3は現在はESMでソースコードをモジュール化している。ということはバンドルしなくてもESMでインポートできるが、巨大なライブラリなので大量のインポートが発生する。それは問題だろうからある程度の塊にバンドルされるのがトラフィックの効率化の観点からすれば望ましいよね。とするとすべてのモジュールがESM化されたとしても、rollupのようなバンドル・ユーティリティは残るということだよね。

動作サンプル

新しいウィンドウで開く

ソースコード・リソース

/dev/force/d3.mjs

/dev/force/index-thumbnail.jpg

/dev/force/index.html

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

/dev/force/index.js

/dev/force/index.mjs

"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);

  }

});

/dev/force/keywords.json