@ledsun blog

無味の味は佳境に入らざればすなわち知れず

JavaScriptを書くのがダルく感じた

TextAEの修正をしようとしたときに、ふと「JavaScriptで書くのダルいな、Rubyなら…」という思いを抱きました。

具体的にどの文法という話ではないし、難しい修正でもなくて、機能変更前の機械的リファクタリングです。 TextAEもJavaScriptも長いこと触っているので、手癖で書けます。しばらく「ダルい」みたいな感情を持ったことがありませんでした。

我ながらこんな思考が浮かんで驚きました。これがRubyの魅力?謎です。

たぶん、脳のどこかにkakikataのコードを書いてたときの回路が残っていたのだと思います。 実際、手を動かし始めたらサクサク直せました。

JS.falsy?

ruby.wasmように値がJavaScriptのFalsyな値か判定する関数を実装しました。

https://github.com/ledsun/ruby.wasm/blob/19b8f245af9806ceb67601f851300d2df6ac5674/packages/gems/js/lib/js.rb#L98-L109

def self.falsy?(value)
  value == JS::Null || value == JS::Undefined ||
    # Use the strictly_eql? method to compare values using JavaScript's `===` operator.
    # This is because JavaScript's `==` operator is loose in its judgment.
    # For example, in JavaScript, `[] == ""` is true, but `[] === ""` is false.
    value.strictly_eql?(JS::EmptyString) || value.strictly_eql?(JS::False) ||
    value.strictly_eql?(JS::Zero) || value.strictly_eql?(JS::NinusZero) ||
    value.strictly_eql?(JS::BingIntZero) ||
    # `Number.isNaN`` is used to compare whether a value is `NaN`` or not.
    # This is because `NaN == NaN` is false in JavaScript, but `Number.isNaN(NaN)` is true.
    JS.global[:Number].isNaN?(value)
end

もっとシンプルになるかと思ってました。 思ったより複雑でした。

というのは、たとえば次の比較は真になります。

JS.eval('return []') == JS.eval("return ''")

なぜかというとJavaScript==がなかなかマイルドだからです。

> []==''
true
> !([])
false
> !('')
true
> []===''
false

そういえば、昔「JavaScript: The Good Parts」を読んだときにこの説明がありました。

その教えを守り、JavaScriptでプログラム書くときは===しか使いません。 それもESLintでAuto Fixしています。 そして、今ではこの仕様のことをすっかり忘れていました。

実装してから気がついたのですが、これは僕が欲しいものでは無かったです。 名前も悪くないし、実装自体は、なかなか気にいっています。 ただ、使い所が思いつかないので、プルリクエストにするのは、一旦やめておきます

いつか使い所を思いついたときのために、自分のリポジトリのブランチとしては残しておきます。

相対URLの解決

URL: URL() コンストラクター - Web API | MDN を使うと、基準になるURLからの相対パスを解決したURLが得られます。 例えば、次のように使います。

// ベース URL:
let baseUrl = "https://developer.mozilla.org";

new URL("ja/docs", baseUrl);
// => 'https://developer.mozilla.org/ja/docs'

このコンストラクターの挙動を試しているときに、次の例を考えました。

new URL('a.rb', 'http://exapmle.com/lib').toString()
// => 'http://exapmle.com/a.rb'

このとき http://exapmle.com/lib/a.rb となって、libディレクトリの中を参照して欲しいのではないでしょうか?

これはJavaScript特有の動作なのでしょうか?

Rubyでも試してみました。 RubyではURLの結合には URI.join を使います。

require 'uri'

URI.join(URI.parse('http://exapmle.com/lib'), 'a.rb')
# => #<URI::HTTP http://exapmle.com/a.rb>

やはり lib が消えます。 統一された動作です。 もしかしてこれはどこかで決まっているのでしょうか?

るりまに以下の説明がありました。

[RFC2396] の Section 5.2 の仕様に従って連結します。

というわけでRFCを見てます。 https://datatracker.ietf.org/doc/html/rfc2396#autoid-33

6) If this step is reached, then we are resolving a relative-path reference. The relative path needs to be merged with the base URI's path. Although there are many ways to do this, we will describe a simple method using a separate string buffer.

 a) All but the last segment of the base URI's path component is
    copied to the buffer.  In other words, any characters after the
    last (right-most) slash character, if any, are excluded.

base URIの最後のスラッシュ以降に何かあれば、その部分はバッファ(解決後のURLを結合するための場所)に入れないそうです。 なるほど、JavaScriptRubyもこの動きをしていそうです。

この記事を書いている途中で気がつきました。 URLは文字列で、ファイルシステムではありません。 http://exapmle.com/lib がファイルかディレクトリかという区別は、文字列から読み取るしかありません。 すると

  • /で終わるのが、ディレクト
  • /の後ろに文字列が続いていたらファイル

みたいな、単純な方法で区別するしかない気がしてきました。 libディレクトリに見えるのは、背景知識があるから人間に判別できているっぽいです。

RubyJavaScript、ファイルとURLの間を行ったり来たりしていると、自分がどこにいるのか、よく見失います。

Playwrightでリダイレクト後のHTTPリクエストをMockできない

ruby.wasmのテストコードを書いていました。

PlaywrightでHTTPリクエストをMockしているスクリーンショット

cdn.jsdriver.netへのリクエストをMockして、レスポンスの内容をローカルファイルに置き換えています。

リダイレクトしたあとはMockできていないスクリーンショット

前述のスクリーンショットと、同じURLに対するリクエストですが、302レスポンスでリダイレクトしたあとはMockできません。

調べてたら、次のコメントを発見しました。

tests for request event and interception with redirects by tjenkinson · Pull Request #3994 · microsoft/playwright · GitHub

The network interception in Playwright is implemented on the Browser -> Network stack boundary. Once the request is in the network stack, it is going to handle the redirects and report them, but not allow intercepting them.

Playwrightが割り込んでいるのは、ブラウザとネットワークスタックの間だそうです。

fetchメソッドはデフォルトで、リダイレクトレスポンスを自動的に追いかけます。 なるほど!この動きはネットワークスタックに含まれていそうです。

PlaywrightのNewwork mockingをつかってみる

Playwrightをつかってみる - @ledsun blog でPlaywrightが最低限動く環境ができました。 リダイレクトレスポンスを扱うテストを書こうと思います。

Playwrightには Netwrok mockingという機能があります。 実行するテストからのHTTPリクエストをモックして書き換えることができます。

サンプルのコードは次のように動きます。

sequenceDiagram
    participant Client
    participant Server

    Client->>Server: https://playwright.dev/
    Server->>Client: 200

これにモックを挟んでみます。

sequenceDiagram
    participant Client
    participant Server

    Client->>Server: Request http://exapmle.com/redirect
    Server->>Client: 301
    Client->>Server: https://playwright.dev/
    Server->>Client: 200 OK

最初の宛先を http://exapmle.com/redirect に変更します。 このリクエストをモックして301 レスポンスを返します。 https://playwright.dev/ にリダイレクトします。

test.beforeEach(async ({ context }) => {
  await context.route(/redirect/, route => route.fulfill({
    status: 301,
    headers: {
      location: 'https://playwright.dev/'
    }
  }));
});

リクエストの宛先に redirect を含むときにモックします。 route.fulfill を使ってレスポンスを書き換えます。

テストも次のように書き換えます。

test('has title', async ({ page }) => {
  await page.goto('http://exapmle.com/redirect');

  // Expect a title "to contain" a substring.
  await expect(page).toHaveTitle(/Playwright/);
});

開くURLを http://exapmle.com/redirect に変更します。 テストを実行してみましょう。 npx playwright test --project=chromium --uiでテストを起動します。

PlaywrightのUIを起動した直後のスクリーンショット

次のように選択してテストを実行します。

  • テスト: has title
  • Networkタブ

テスト実行完了後のスクリーンショット

Networkタブをみます。

  1. /redirect にリクエストして 301が返る
  2. / にリクエストする

と、期待通りに動いたことが確認できました。

よく見ると https://playwright.dev/js/redirection.js へのリクエストもリダイレクトされています。

Playwrightをつかってみる

Installation | Playwrightの手順に従って進めます。

npm init playwright@latest

いまはnpm initにプロジェクトテンプレートをつくる機能が組み込まれているようです。

npm-init | npm Docsによると

initializer in this case is an npm package named create-, which will be installed by npm-exec, and then have its main bin executed -- presumably creating or updating package.json and running any other initialization-related operations.

create-<initializer> というパッケージが実行されるそうです。 つまり、create-playwright - npmインストーラーのようです。

これでプロジェクトが作成されます。 次のコマンドで、サンプルのテストコードが実行出来ます。

npx playwright test

僕の環境ではエラーが出ます。

  4 failed
    [firefox] › example.spec.ts:3:5 › has title ────────────────────────────────────────────────────
    [firefox] › example.spec.ts:10:5 › get started link ────────────────────────────────────────────
    [webkit] › example.spec.ts:3:5 › has title ─────────────────────────────────────────────────────
    [webkit] › example.spec.ts:10:5 › get started link ─────────────────────────────────────────────
  2 passed (2.0s)

どうやらブラウザを実行するためのライブラリーが足りないようです。 Chromiumに限定して実行してみましょう。

npx playwright test --project=chromium

成功しました。

Running 2 tests using 2 workers
  2 passed (1.5s)

To open last HTML report run:

  npx playwright show-report

サンプルのテストケースは2つあるようです。 npx playwright show-report コマンドでテスト結果をHTML形式で見れるようです。 成功時はあんまり必要無いですね。 ちなみに、失敗時はデフォルトでHTML形式で表示されます。

複数プログラミング言語をいっぺんに実行するスクリプトをMakefileで書く

C#JavaScriptRubyのプログラミングを比較するためにGitHub - ledsun/enumerable: C#とJavaScriptとRubyのコレクション操作を比較するためのリポジトリです。を作りました。 動作確認のために、全部のプログラムをバーンと実行するスクリプトが欲しいです。 いずれかのプログラミング言語に流儀を合わせると角が立ちます。 そこでMakefileを使ってみました。

all: csharp javascript ruby

csharp: csharp/bin/Debug/net6.0/csharp.dll
  dotnet csharp/bin/Debug/net6.0/csharp.dll   

csharp/bin/Debug/net6.0/csharp.dll: csharp/Program.cs
  dotnet build csharp

clean:
  dotnet clean csharp

.PHONY: javascript ruby

javascript:
  node javascript/index.js

ruby:
  ruby ruby/main.rb

実行すると次のようなになります。

実行結果

シェルスクリプトでもよかったでしょうか? C#のビルドが入るとMakefileが便利でした。 C#のソースファイル数が増えるとMakefileでは手に負えなくなりそうに思いました。

参考

C#とJavaScriptとRubyのコレクション操作

大抵のプログラミング言語でコレクション(列挙できる何か)をメソッドチェーンで操作できます。 プログラミング言語ごとに、ちょっとずつちがうので整理してみようと思います。 僕の馴染みのあるC#JavaScriptRubyでそれぞれ書いてみます。

お題

お題は「入力された数値の配列からヌルを取り除き10倍して、3で割ったあまりで分類する」です。 実用的な例だとは思っていません。

C#

C#ではEnumerable クラス (System.Linq) | Microsoft Docs を使います。

var source = new int?[] { 1, null, 2, 3, null, 4 };

var emptyHash = new Dictionary<int, List<int>> {
  { 0, new List<int>() },
  { 1, new List<int>() },
  { 2, new List<int>() }
};
var result = source.OfType<int>()
                   .Select(x => x * 10)
                   .Aggregate(emptyHash, (acc, e) =>
                   {
                       acc[e % 3].Add(e);
                       return acc;
                   });

foreach (var (key, value) in result)
{
    Console.WriteLine($"{key}: {String.Join(",", value)}");
}
  • 結果を保持するためのDictionaryの初期化
  • ヌル許容型からヌル非許容型への変換
  • 結果の出力

が面倒でした。 型付けが強いプログラミング言語は、こういう練習用の短いプログラミングを書くにはまだるっこしいです。 C#はリハビリ中です。 もっと簡単な書き方があるかもしれません。

JavaScript

JavaScriptではArray - JavaScript | MDNを使います。

const source = [1, null, 2, 3, null, 4];

const result = source
  .filter((e) => e)
  .map((e) => e * 10)
  .reduce(
    (acc, e) => {
      acc[e % 3].push(e);
      return acc;
    },
    { 0: [], 1: [], 2: [] }
  );

console.log(result);

僕が、もっとも慣れているプログラミング言語です。 シュッと書けました。

Ruby

Rubyではmodule Enumerable (Ruby 3.1 リファレンスマニュアル)を使います。

source = [1, nil, 2, 3, nil, 4]

result = source
  .find_all { _1 }
  .collect { _1 * 10 }
  .inject({ 0 => [], 1 => [], 2 => [] }) do
    _1[_2 % 3] << _2
    _1
  end

p result

Rubyは同じ関数にいろいろな名前がついていて面白いです。

参考

追記

JavaScriptfilter((e) => e)だと0もはじいちゃいますね。 filter((e) => e !== null)の方が良いです。

あるリファクタリング

次のような関数がありました。 どうしたもんかなあ?と悩みました。

import validateAnnotation from './validateAnnotation'
import convertBeginAndEndToInteger from './convertBeginAndEndToInteger'

export default function (
  spanContainer,
  entityContainer,
  attributeContainer,
  relationContainer,
  text,
  spans,
  rowData,
  trackNumber = ''
) {
  const { accept, reject } = validateAnnotation(text, spans, rowData)

  const { typeSetting } = accept
  const typesettings = typeSetting.map((src) => ({
    ...src,
    span: convertBeginAndEndToInteger(src.span)
  }))
  spanContainer.addSource(typesettings, 'typesetting')

  const { denotation } = accept
  const denotations = denotation.map((src) => ({
    ...src,
    id: src.id ? trackNumber + src.id : null,
    span: convertBeginAndEndToInteger(src.span)
  }))
  spanContainer.addSource(denotations, 'denotation')
  entityContainer.addSource(denotations, 'denotation')

  const { block } = accept
  const blocks = block.map((src) => ({
    ...src,
    id: src.id ? trackNumber + src.id : null,
    span: convertBeginAndEndToInteger(src.span)
  }))
  spanContainer.addSource(blocks, 'block')
  entityContainer.addSource(blocks, 'block')

  const { relation } = accept
  const relations = relation.map((src) => ({
    ...src,
    id: src.id ? trackNumber + src.id : null,
    subj: trackNumber + src.subj,
    obj: trackNumber + src.obj
  }))
  relationContainer.addSource(relations)

  const { attribute } = accept
  const attributes = attribute.map((src) => ({
    ...src,
    id: src.id ? trackNumber + src.id : null,
    subj: trackNumber + src.subj,
    obj: src.obj
  }))
  attributeContainer.addSource(attributes)

  return reject
}

2時間くらいいじくり回して、こうなったっす。

import IDConflictResolver from './IDConflictResolver'
import convertBeginAndEndOfSpanToInteger from './convertBeginAndEndOfSpanToInteger'

export default function (
  spanContainer,
  entityContainer,
  attributeContainer,
  relationContainer,
  accept,
  trackNumber = ''
) {
  const [typeSettings, denotation, block] = convertBeginAndEndOfSpanToInteger(
    accept.typeSetting,
    accept.denotation,
    accept.block
  )
  const { relation, attribute } = accept
  const { denotations, blocks, relations, attributes } = new IDConflictResolver(
    trackNumber
  ).addTrackNumberAsIDPrefix(denotation, block, relation, attribute)

  spanContainer.addSource(typeSettings, 'typesetting')
  spanContainer.addSource(denotations, 'denotation')
  spanContainer.addSource(blocks, 'block')
  entityContainer.addSource(denotations, 'denotation')
  entityContainer.addSource(blocks, 'block')
  relationContainer.addSource(relations)
  attributeContainer.addSource(attributes)
}

当初「データ取り込みの前処理部分なので、綺麗に書きようがないのかなあ?」と思いましたが、やればそれなりに読める形になりました。 やはり、単に脳みそがサボりたくて、賢そうなことを言っていただけのようです。

あとから見るとリファクタリングのコンセプトは、データ種別単位で

  1. 前処理1
  2. 前処理2
  3. 取り込み

とやっていたのを、処理単位にまとめ直したのでした。 それで処理に意図がわかる名前 IDConflictResolver がつけられるようになったのが良かったです。

convertBeginAndEndOfSpanToInteger も、もっとHowでなくWhatを表す名前にしても良いかもしれません。

Romeは本当に速かった

RomeプロジェクトのJavaScriptフォーマッターがリリースされました。

rome.tools

ちょうどPrettier を使っているプロジェクトがあったので比較してみました。

prettierを速く動かしたくてparallel-prettierに.prettierignoreを読む修正を加えて動かして速くなったか - Qiita でNode.jsのまま並列化したparallel-prettierを試したときは

やったね1.25倍速くなりました!

でした。 それと比べてるとRomeは10倍以上速くなっています。 驚異的です。 JavaScriptパーサーやASTを扱うライブラリーなどの資産がないrustで書き直すのは、大変な労力に思えます。 それに見合う性能向上がはかれるようです。

今後の成長が期待大なプロジェクトです。

rails ujsを学ぶ

一昔前のRailsではJavaScriptを簡単に使う機能としてrails ujsがありました。 Rails ガイドにも説明があります。

railsguides.jp

「UJS: Unobtrusive(控えめな)JavaScript

主な用途は

www.inodev.jp

  • JSの処理で確認ダイアログとして「マジでログアウトすんのか?」という表示をする
  • JSの処理でログアウトのために「deleteメソッドとして」HTTPのPOSTリクエストをする

なんだかんだ言って、削除ボタンとか確認ダイアログとか簡単に実装できて便利です。

JavaScriptとして動く部分の実装は https://github.com/rails/rails/tree/cfa728478935dc48d9d6e2463dc500ffafee802b/actionview/app/assets/javascripts/rails-ujs/features にあります。 たかだか300行のCoffeeScriptです。 読もうと思えば読めそうです。 今回は読みません。

https://github.com/rails/rails/tree/cfa728478935dc48d9d6e2463dc500ffafee802b/actionview/app/assets/javascripts がプロジェクトのルートです。

npmパッケージを作っているのは、もう少し上の https://github.com/rails/rails/tree/cfa728478935dc48d9d6e2463dc500ffafee802b/actionview です。依存しているGemの定義がactionview.gemspecに書いてあるからだと思います。

ビルドするにはnpm run buildを実行します。 次のように環境を設定します。

git clone git@github.com:rails/rails.git
cd rails
bundle install --without db
cd actionview/

次のようにnpm run buildを実行するとビルドできます。

ledsun@MSI:~/r/actionview[1]►npm run build

> @rails/ujs@7.1.0-alpha build
> bundle exec blade build

Building assets…
[created] lib/assets/compiled/rails-ujs.js
npm notice
npm notice New minor version of npm available! 8.3.0 -> 8.6.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v8.6.0
npm notice Run npm install -g npm@8.6.0 to update!
npm notice

CoffeeScriptのビルドには GitHub - javan/blade を使っています。

https://github.com/javan/blade/blob/2d6e0ae9ebe4e2b405550d998b9f1bb2d2b4300a/lib/blade/assets/builder.rb#L22

     manifest.compile(logical_paths)

https://github.com/javan/blade/blob/2d6e0ae9ebe4e2b405550d998b9f1bb2d2b4300a/lib/blade/assets/builder.rb#L35-L37

    def manifest
      @manifest ||= Sprockets::Manifest.new(environment.index, compile_path)
    end

結局Sprocketsを呼び出しています。 https://github.com/rails/sprockets/blob/3aa96f7499eb3043eb686d918a308152f658bb53/lib/sprockets/manifest.rb#L161 の辺りですが、Sprocketsは訳わからないっす。

https://github.com/javan/blade/blob/main/blade.gemspec#L27

  spec.add_dependency "coffee-script"

bladeの依存関係にcoffee-script gemが入っています。 なんらかの魔法の力によって、これが呼び出されているのだと思います。 どうせ最終的に GitHub - jashkenas/coffeescript: Unfancy JavaScript が呼ばれると思うので、npm scriptから直接こいつを呼び出しては?みたいな気分になります。

$( document ).ready()

コールバック関数が実行されるタイミング

$( document ).ready()JavaScriptでDOMを操作するに当たって、DOMの読込完了を待つためのjQueryの便利関数です。 ブラウザにloadイベントしか実装されていなかった時代がありました。 loadイベントは次の2つを待ちます。

  1. DOMの構築
  2. 画像、CSSの読込

$( document ).ready()は2の前に発火します。 画像などの(当時として)重たいアセットの読込を待たなくてよくなるため、JavaScriptの開始を高速化出来ました。 次のように使いました。

$(document).ready(function(){
/// 好きな処理
})

代替え案

scriptタグのdefer属性

scriptタグにdefer属性を設定すると

スクリプトを文書の解析完了後かつ DOMContentLoaded が発生する前に実行する

ので、同様の効果が得られます。ただし、インラインスクリプトには効果がありません。 src属性を使って読み込む外部スクリプトには、defer属性を使うのが良いと思います。

<html>
<head>
  <script defer src="outer.js"></script>
</head>
<body>
本文
</body>

outer.jsでは、$( document ).ready()を使わずに、いきなり実行したいJavaScriptが書けます。

/// 好きな処理

scriptタグの位置

bodyタグの最後にscriptタグを書いても大体同じ効果が得られます。 確かheadタグにで読み込むCSSを読み込んだ後で、画像を読み込む前だったような・・・うろ覚えです。 O'Reilly Japan - ハイパフォーマンスJavaScript に詳しい説明が載っています。

インラインスクリプトの場合はこちらの方が良いでしょう。

<html>
<head></head>
<body>
  本文
  <script>
  /// 好きな処理
  </script>
</body>

DOMContentLoaded

他人が使うJavaScriptファイルを提供する場合など、scriptタグの書き方を指定を変更出来ない時、JavaScriptだけで実行タイミングを制御したいことがあります。その場合はDOMContentLoadedイベントが使えます。

document.addEventListener('DOMContentLoaded', () => {
/// 好きな処理
})

DOMContentLoadedは一度しか発火しないため、後から追加するJavaScriptでは発火しません。 $( document ).ready()は次のようにjQuery.Deferredを使って実装されています。

https://github.com/jquery/jquery/blob/5d5ea015114092c157311c4948f7cc3d8c8e7f8a/src/core/ready.js#L7-L13

// The deferred used on DOM ready
var readyList = jQuery.Deferred();

jQuery.fn.ready = function( fn ) {

    readyList
        .then( fn )

DOM構築後に$( document ).ready()を読ぶと即座に実行されます。 同じように動作させるには次のようにreadyStateの値を確認します。

function doSomething() {
/// 好きな処理
}

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', doSomething);
} else {
  doSomething();
}

厳密には$( document ).ready()は次のイベントループ(?)で非同期に実行します。 doSomethingの実行に長い時間が掛かる場合は、requestAnimationFrameを使うなどの工夫が必要かもしれません。

関連ページ

HTMLのscriptタグのdefer属性 - @ledsun blog

marmaid.jsを使ってシーケンス図を書くためのHTMLファイルのテンプレート

SPAのような、あるページを表示するときにブラウザからサーバーへ複数回のリクエストを送るWebサイトがあります。 シーケンスそのものは複雑ではありませんが、複数種類のリクエストを送信していることを強調したいことがあります。 そんなときにサッとシーケンス図を描いて図示できると便利です。

簡単な図なのでわざわざ作図ツールを使うほどではありません。 なるべく手軽にシーケンス図を書きたいです。 そこで思いつきました。 marmaid.jsを組み込んだHTMLファイルを用意します。 例えば次のHTMLファイルです。

これをブラウザで表示すればシーケンス図が得られます。

f:id:ledsun:20220318200816p:plain
ブラウザでひらいくとシーケンス図が得られます。

お好みのテキストエディタで編集して、好きなシーケンス図が得られます。

marmaid.jsにはライブエディタもあります。 よく考えたら、要りませんでした。

ブラウザからJavaScriptモジュールをjQueryとともに使う

ブラウザでJavaScriptモジュールが使えるようになりました。 <script type="module">って書くやつです。 これを使うとimport/exportを使ってモジュール感の依存関係が解決できます。 これまでは人間がscriptタグの順番を決めるか、Webpackのようなモジュールバンドラーで事前処理する必要がありました。 さらにそれ以前は、jQueryのようなグローバル変数を使って、依存関係を解決していました。

jQuery時代の依存関係解決からJavaScriptモジュールに一気にジャンプ出来るのでしょうか? それとも一度Webpackのようなモジュールバンドラーを経由する必要があるのでしょうか? ということを試してみます。 次のindex.htmlではJavaScriptモジュールとして、index.jsを読み込みます。

<head>
  <script src="https://code.jquery.com/jquery-3.6.0.js" integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk="
    crossorigin="anonymous"></script>
  <script type="module" defer>
    import init from './index.js'
    init()
  </script>
</head>

<body>
  <div id="hoge"></div>
</body>

index.jsではすでに読み込まれているはずのjQueryを参照します。

export default function () {
  $("#hoge").text("hello");
}

これが動くのかというと、動きます。 次のようにHTTPサーバーで起動します。

python3 -m http.server
open htttp://localhost:8000

f:id:ledsun:20220316085625p:plain
helloが表示されます。

HTTPサーバーを起動しないと、index.jsをローカルファイルから読もうとします。 これはエラーになります。

f:id:ledsun:20220316085920p:plain
ローカルのスクリプトファイル読み込みエラー

モジュールバンドラーを使っていなくても、<script type="module">を使ってWeb標準の依存関係解決をやっていけることがわかりました。

マクロスのミサイルみたいな矢印

こんな感じでX座標が同じ目標にむかう矢印は狭い範囲に集中してごちゃごちゃします。

f:id:ledsun:20220311100009p:plain
同じ座標に向かっている矢印

そこでマクロスのミサイルみたいに各矢印に個性を与えます。

f:id:ledsun:20220311100034p:plain
マクロスのミサイルみたいな矢印

正確に言うと逆です。 マクロスのミサイルは目標地点は1つで、経路に個性があります。 今回与えた個性は、目標地点をずらしています。 目標地点が画面外に出ていて見えないことを良いことに、矢印が向かうX座標をランダムで変更しています。

ネットワーク、パフォーマンス、メモリを指しているように見えるかもしれませんが、偶然です。

f:id:ledsun:20220311100723p:plain
順番に並んだ矢印

もう少し調整して、矢印が決まった順番にならぶようにしました。 矢印の個性は減ります。 矢印の交差が減るので関係性は見やすくなります。

ちなみにこの画像はブラウザのキャプチャで、矢印はSVGで描いています。