Node.jsのバージョン管理にVoltaとfnmを試してみた

Node.jsのバージョン管理ツール多すぎ問題

Ruby なら rbenv、Python なら pyenv で決まり、みたいなバージョン管理ツールだけど、Node.js ではやたらと乱立していてどれを使うのがいいのかよくわからない。この問題自体はいろんなところで書かれていて、今回ググった中では↓このページがよくまとまっていた。

このページに挙げられているツールは以下の通り:

  • nvm
  • n
  • volta
  • asdf
  • nodenv
  • fnm
  • nvs
  • nodebrew

nvm、n、nodenv は前に試したことがある。ほかは聞いたことだけあったり、初めて知ったりだ。

いずれにせよ、JavaScript (Node.js)は Ruby や Python ほど使わないこともあって今までテキトーにしてたんだけど、静的サイトジェネレータの 11ty を試したこと(しかも Windows で)がきっかけで、少しまじめに考えてみようという気になった。なので Linux でも Windows でも使えるのが最低条件だ。

で、どれがいいかというと、上のページでは volta を薦めている。ほかには「Windows なら fnm がいい」というページがいくつか見つかった。そういうわけでこの2つを試してみることにする。どちらも Rust で書かれている。

Volta

公式サイトから Windows 用のインストーラをダウンロードする。

最新バージョンは1.0.8だ。ダウンロードしたら普通のアプリ同様にインストールすればいい。

takatoh@sofa: Documents > volta --version
1.0.8

ヘルプを見るとこんな感じ。

takatoh@sofa: Documents > volta --help
Volta 1.0.8
The JavaScript Launcher ⚡

    To install a tool in your toolchain, use `volta install`.
    To pin your project's runtime or package manager, use `volta pin`.

USAGE:
    volta.exe [FLAGS] [SUBCOMMAND]

FLAGS:
        --verbose
            Enables verbose diagnostics

        --quiet
            Prevents unnecessary output

    -v, --version
            Prints the current version of Volta

    -h, --help
            Prints help information


SUBCOMMANDS:
    fetch          Fetches a tool to the local machine
    install        Installs a tool in your toolchain
    uninstall      Uninstalls a tool from your toolchain
    pin            Pins your project's runtime or package manager
    list           Displays the current toolchain
    completions    Generates Volta completions
    which          Locates the actual binary that will be called by Volta
    setup          Enables Volta for the current user / shell
    run            Run a command with custom Node, npm, and/or Yarn versions
    help           Prints this message or the help of the given subcommand(s)

Node.js をインストールするには volta install

takatoh@sofa: Documents > volta install node@14
success: installed and set [email protected] (with [email protected]) as default

node@14 という書き方をしてるのは、Volta が Node.js だけでなく npm や yarn のバージョン管理にも対応してるから。ともかく Node 14 系の最新版がインストールされた。

takatoh@sofa: Documents > node -v
v14.20.0

Node 16 系もインストールしてみる。

takatoh@sofa: Documents > volta install node@16
success: installed and set [email protected] (with [email protected]) as default
takatoh@sofa: Documents > node -v
v16.17.0

volta list all でインストールされている Node.js のリストが表示される。

takatoh@sofa: Documents > volta list node
⚡️ Node runtimes in your toolchain:

    v14.20.0
    v16.17.0 (default)

プロジェクトごとにバージョンを分けることもできる。Volta では package.json に利用するバージョンが記載される。サンプルを作って試してみよう。

takatoh@sofa: Documents > cd node-sample
takatoh@sofa: node-sample > npm init

この状態で package.json はこんなふうになっている。

{
  "name": "sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

このプロジェクトに特定の Node.js のバージョンを指定するには volta pin コマンド。

takatoh@sofa: node-sample > volta pin node@14
success: pinned [email protected] (with [email protected]) in package.json

バージョンの指定は package.json に書き込まれ、次のようになる。

{
  "name": "sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "volta": {
    "node": "14.20.0"
  }
}

node -v で確認。

takatoh@sofa: node-sample > node -v
v14.20.0

プロジェクトのディレクトリを抜けると、デフォルトのバージョンに戻る。

takatoh@sofa: node-sample > cd ..
takatoh@sofa: Documents > node -v
v16.17.0

fnm

fnm は Fast Node Manager の略だそうだ。公式サイトは見当たらないんだけど、GitHub で公開されている。

Windows にインストールするには Chocolatey を使う。PowerShell を「管理者として実行」して、次のコマンドでインストール。

takatoh@sofa: Documents > choco install fnm

PowerShell で使うため、設定ファイルに次の行を書き足す。

fnm env --use-on-cd | Out-String | Invoke-Expression

PowerShell の設定ファイルは $profile と打ち込めばパスを表示してくれる。

takatoh@sofa: Documents > $profile
C:\Users\takatoh\Documents\PowerShell\Microsoft.PowerShell_profile.ps1

これで準備は完了。

Node.js をインストールするには fnm install コマンドを使う。16系をインストールしてみよう。

takatoh@sofa: Documents > fnm install 16
Installing Node v16.17.0 (x64)

でも、これだけだとまだ使えない。

takatoh@sofa: Documents > node -v
node: The term 'node' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

PowerShell を起動しなおしたら使えるようになった。

takatoh@sofa: Documents > node -v
v16.17.0

14系もインストールしてみよう。

takatoh@sofa: Documents > fnm install 14
Installing Node v14.20.0 (x64)

fnm list コマンドでインストール済みのバージョンを確認できる。v14.20.0 とv16.17.0 がインストールされてるけど、この時点では v16.17.0 がデフォルトになってる。

takatoh@sofa: Documents > fnm list
* v14.20.0
* v16.17.0 default
* system
takatoh@sofa: Documents > node -v
v16.17.0

切り替えるには fnm use コマンド。

takatoh@sofa: Documents > fnm use 14
Using Node v14.20.0
takatoh@sofa: Documents > node -v
v14.20.0

ところで system ってのは何だろう?

takatoh@sofa: Documents > fnm use system
Bypassing fnm: using system node
takatoh@sofa: Documents > node -v
node: The term 'node' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

たぶん、fnm の管理下にない、もとからインストールされてるバージョンのことだな。今回の場合はそんなものはないのでエラーが出てる、と。fnm use コマンドでインストール済みのバージョンを指定してやれば直る。

takatoh@sofa: Documents > fnm use 16
Using Node v16.17.0
takatoh@sofa: Documents > node -v
v16.17.0

プロジェクトごとにバージョンを使い分けるには次のようにする。

takatoh@sofa: Documents > mkdir node-sample2
takatoh@sofa: Documents > cd node-sample2
takatoh@sofa: node-sample2 > npm init
takatoh@sofa: node-sample2 > fnm use 14
takatoh@sofa: node-sample2 > node -v > .node-version

fnm では Volta と違って .node-version ファイルに保存される。中身は次のようになってる。

takatoh@sofa: node-sample2 > cat .node-version
v14.20.0

.node-version ファイルがあるディレクトリに移動すると、自動的にバージョンを切り替えてくれる。いったん上のディレクトリに移動し、v16.17.0 に変更した後、もう一度プロジェクトのディレクトリに戻ると、v14.20.0 に切り替わる。

takatoh@sofa: node-sample2 > node -v
v14.20.0
takatoh@sofa: node-sample2 > cd ..
takatoh@sofa: Documents > fnm use 16.17.0
Using Node v16.17.0
takatoh@sofa: Documents > node -v
v16.17.0
takatoh@sofa: Documents > cd node-sample2
Using Node v14.20.0
takatoh@sofa: node-sample2 > node -v
v14.20.0

が、上のディレクトリに戻っても v16.17.0 には切り替わってくれない。どうも .node-version ファイルがないディレクトリに移動したときには切り替えが発生しないみたいだ。どうしようかと思ったら fnm default コマンドがあった。

takatoh@sofa: Documents > fnm default v16.17.0
takatoh@sofa: Documents > node -v
v14.20.0
takatoh@sofa: Documents > fnm use 16
Using Node v16.17.0
takatoh@sofa: Documents > node -v
v16.17.0
takatoh@sofa: Documents > cd node-sample2
Using Node v14.20.0
takatoh@sofa: node-sample2 > node -v
v14.20.0
takatoh@sofa: node-sample2 > cd ..
takatoh@sofa: Documents > node -v
v14.20.0

なのに期待通り動作しないじゃない。よくよくみると上のほうで fnm list コマンドを実行したときの出力に v16.17.0 が default ってなってる。

何か忘れてる?それともバグ?

JavaScript:連番の配列を作る – あるいはJavaScriptの奇妙な仕様

JavaScript で連番の整数を要素に持つ配列を作る方法はいくつも考えられるだろうけど、こんなのを見つけた(見つけたコードそのままじゃなくて、少し整理してある)。

const initArray = (n) => [...Array(n)].map((_, i) => i);

いささかトリッキーだと思うけど、まっさきに思い浮かぶであろう for ループを使うよりも簡潔にかけている。もちろん、期待通りに動く。

> initArray(5);
[ 0, 1, 2, 3, 4 ]

自分のメモのために説明しておくと、Array(n) で要素 n 個の配列をつくり、スプレッド構文を使って別の配列リテラルの中に埋め込む。で、map メソッドを呼び出して、各要素をそのインデックス値に置き換える。結果的に 0 からはじまり n 個の連番をもつ配列が出来上がる。

さて、トリッキーだと思うのは、コンストラクタでつくった配列 Array(n) をわざわざスプレッド構文を使って別の配列リテラルに埋め込むところ [...Array(n)] だ。だけどこれがないと期待通りに動かない。

> Array(5).map((_, i) => i);
[ <5 empty items> ]

<5 empty items> ってなんだ。そりゃ初期化してないから空の配列なんだろうけど、長さ(要素数)は 5 なんでしょ?

> Array(5).length;
5

いままで使ったことがなかったので知らなかったけど、配列をコンストラクタ Array() でつくると、長さはあるけど何の値も入ってない、まさに空の配列が返ってくる。

> Array(5);
[ <5 empty items> ]

この「空(から)」は undefined でも null でもない。この配列にインデックスを使ってアクセスすると undefined が返ってくる。

> const a = Array(5);
undefined
> a[3];
undefined
> a[3] == undefined;
true

なら undefined が5個入った配列と等しいのかというとそれも違う。

> a == [undefined, undefined, undefined, undefined, undefined];
false

さらにいえば、リテラルでつくったから配列とも違う。

> a == [];
false

わけがわからないよ。

JavaScript:関数の名前を取得する

Function オブジェクトの name プロパティで関数名を文字列として取得できることを知った。

Node.js の REPL で試してみるよ。

> function func1() {}
undefined
> func1.name
'func1'

ほんとだ。

アロー関数ではどうだろう?

> const func2 = () => {}
undefined
> func2.name
'func2'

ほうほう。代入先の変数名が返ってくるとな。

なら、これを別の変数に代入したら?

> func3 = func2
[Function: func2]
> func3.name
'func2'

えー??

ところで、関数の名前を文字列で取得して何に使えるのかと思ったら eval() があるのね。

でも「使うな」って書いてある。

JavaScript: アロー関数で再帰

あけおめ、ことよろ。

正月早々、奇妙なものを見つけた。元記事は去年(というか昨日)のもので、タイトルの通りなんだけど、こんなのだった。

> const fibonacci = ((fb = n => n > 1 ? n * fb(n - 1) : 1) => fb)();

どう見ても階乗を求める関数なのに名前が fibonacci っておかしいだろ、というのは置いといて。確かに期待通りに動作する。

> fibonacci(5);
120 

元記事の説明によると:

  • アロー関数 n => n > 1 ? n * fb(n - 1) : 1 を変数 fb に代入し
  • その代入した関数を即時関数によって関数 fb として返すように
  • 代入(fibonacci)する

となってて、まあその通りではある。でも、その即時関数っていうのが単に引数をそのまま返してるだけなんだから、それ、いらねんじゃね?

というわけで即時関数なしでやってみると、やっぱり、ちゃんと期待通りに動作するじゃん。

> const fact = n => n > 1 ? n * fact(n - 1) : 1;
undefined
> fact(5);
120 

と、ここまで書いて気がついた。上では即時関数が云々と書いたけど、要するにその即時関数の引数であるアロー関数を変数 fb に代入してそれを呼び出してるだけだ。即時関数は何の関係もない。元記事の筆者がどこでネタを仕入れたのか知らないけど、不必要にトリッキーなだけだ。それとも以前のバージョンの JavaScript ではこう書かなきゃできなかったのかな。

ちなみに確認したのは Node v14.15.3。

takatoh@montana: takatoh > node -v
v14.15.3

というわけで、元記事から得たものといえば、関数呼び出しの引数部分に代入式が書けることか。使うかな、これ。

あ、あと、ブログのネタにはなった。

ReactのuseState、useEffectにハマる

……いや、React というより Fetch API のせいなのかもしれない。

ここのところで React で web アプリを作ってる。例によって自分で使うためだけのもの。

JSON を返す API を2つバックエンドに置いて、両方から取ってきたデータを合わせて表示する Collections コンポーネントを書いてたんだけど、いい書き方がわからずにハマった。

Collections コンポーネントは次のように動作する(のを期待している)。

  • 一方のAPIからコレクションのリストを取得する
  • リスト中のコレクションそれぞれについて、もう一方のAPIから詳細を取得する
  • 2つのAPIから取得したデータを合わせて、コレクションのリストとしてテーブル表示する

コレクションというのは、プログラミングにおけるデータ構造のことじゃなくて買い集めたコレクション(CDとかDVDみたいな)のこと。renderCollections 関数でレンダリングしているテーブルの行のうち、collection.id と collection.buy_date が一方のAPIから取得したデータ、collection.title と collection.brand がもう一方のAPIから取得したデータだ。

import React, { useState, useEffect } from 'react';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableContainer from '@material-ui/core/TableContainer';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import Paper from '@material-ui/core/Paper';


const Collections = (props) => {
  const perPage = 10;
  const [collections, setCollections] = useState([]);
  const [currentPage, setPage] = useState(0);

  const page = props.query.page ? parseInt(props.query.page) : 1;
  useEffect(
    () => {
      setPage(page);
      const offset = perPage * (page - 1);
      let colls = [];
      const fetchProduct = async (coll) => {
        const result = await fetch(`${props.api2Endpoint}/products/${coll.product_id}`)
          .then(response => response.json())
          .then(data => data["products"][0]);
        colls = [...colls, {...coll, title: result.title, brand: result.brand.name}];
        setCollections(colls);
      };
      const fetchData = async () => {
        const result = await fetch(`${props.api1Endpoint}/collections?limit=${perPage}&offset=${offset}`, { mode: "cors" })
          .then(response => response.json())
          .then(data => data["collections"]);
        for (const c of result) {
          fetchProduct(c);
        }
      };
      fetchData();
    },
    []
  );

  return (
    <div>
      <h2>Collections</h2>
      <TableContainer component={ Paper }>
        <Table aria-label="collection table">
          <TableHead>
            <TableRow>
              <TableCell>ID</TableCell>
              <TableCell>Title</TableCell>
              <TableCell>Brand</TableCell>
              <TableCell>Buy date</TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            { renderCollections(collections) }
          </TableBody>
        </Table>
      </TableContainer>
    </div>
  );
}

const renderCollections = (collections) => {
  return (
    collections.map(collection => (
      <TableRow key={ collection.id }>
        <TableCell>{ collection.id }</TableCell>
        <TableCell>{ collection.title }</TableCell>
        <TableCell>{ collection.brand }</TableCell>
        <TableCell>{ collection.buy_date }</TableCell>
      </TableRow>
    ))
  );
}


export default Collections;

いろいろググりながらなんとか書いた上のコードで一応動くようにはなった。けど、表示されるコレクションの順番が不定になってしまう。APIからリストを取得した段階では ID 順に並んでいるのは確認したので、たぶん、個々の詳細データを取得する fetchProduct 関数内でリスト(変数 collections)を更新するタイミングのせいだ。つまり、ループの中で使ってる fetch 関数が非同期なせいだ(たぶん)。

あぁ、JavaScript ってこういうところがなんかやりづらいんだよなぁ。

なんかいい書き方はないものか。あとからソートすればいいのはわかるんだけど。

Ubuntu 20.04にnodenvをインストール

GitHub のリポジトリにあるドキュメントを見ながら。

GitHub – nodenv/nodenv

まずは GitHub から clone

takatoh@apostrophe:~$ git clone https://github.com/nodenv/nodenv.git ~/.nodenv

オプション扱いだけど dynamic bash extension とかいうのをビルド。nodenv がスピードアップするらしい。

takatoh@apostrophe:~$ cd ~/.nodenv && src/configure && make -C src

環境変数 PATH に ~/.nodenv を追加してから、セットアップ。

takatoh@apostrophe:~$ ~/.nodenv/bin/nodenv init
Load nodenv automatically by appending
the following to ~/.bashrc:

eval "$(nodenv init -)"

メッセージにあるとおり、.bashrc に記述を追加。終わったらシェルを起動し直す。

nodenv-doctor スクリプトで、ちゃんとインストールできてるか確認。

takatoh@apostrophe:~$ curl -fsSL https://github.com/nodenv/nodenv-installer/raw/master/bin/nodenv-doctor | bash

OK って出たから大丈夫なんだろう。

つぎに、これもオプションみたいだけど、node-build をインストール。

takatoh@apostrophe:~$ mkdir -p "$(nodenv root)"/plugins
takatoh@apostrophe:~$ git clone https://github.com/nodenv/node-build.git "$(nodenv root)"/plugins/node-build

これで完了。

nodenv install -l でインストール可能な Node.js のバージョン一覧が見られる。公式サイトによると 12.18.1 が最新の推奨版なので、それをインストールする。

takatoh@apostrophe:~$ nodenv install 12.18.1

nodenv global で利用するバージョンを指定。

takatoh@apostrophe:~$ nodenv global 12.18.1

これで最新推奨版の Node.js が使えるようになる。

takatoh@apostrophe:~$ node --version
v10.19.0

えっ??

takatoh@apostrophe:~$ nodenv versions
  system
* 12.18.1 (set by /home/takatoh/.nodenv/version)

ちゃんと 12.18.1 になってるじゃないか。どういうこと?

ちょっとびっくりしたけど、なんのことはない、シェルを立ち上げ直したらできた。

takatoh@apostrophe:~$ node -v
v12.18.1

おしまい。

UbuntuのNode.jsを最新にする

ここのところ JavaScript を書く機会があって、最近の書き方なんかを調べたりしてるんだけど、そういえば Ubuntu にインストールした Node.js はどうなってるんだっけかな、と思って確かめてみたら:

takatoh@apostrophe $ node -v
v4.2.6

もはや、いつインストールしたのかもわからない古いバージョンだった。公式サイトによると、最新版が13.6.0、推奨版が12.14.1 LTS だ。

で、さて、バージョンアップはどうやったらいいのか。ググってみたら、n package というのを使うといいらしい。

 cf. Ubuntuに最新のNode.jsを難なくインストールする – Qiita

さっそくこの通りにやってみる。まずは npm で n package をインストール。

takatoh@apostrophe $ sudo npm install n -g

その n package (nコマンド)を使って Node.js をインストール。

takatoh@apostrophe $ sudo n stable

もとから入っていた古い Node.js を削除。

takatoh@apostrophe $ sudo apt purge -y nodejs npm

そしてシェルを読みなおす。

takatoh@apostrophe $ exec $SHELL -l

これで完了。最新になっているか、確かめてみよう。

takatoh@apostrophe $ node -v
v12.14.1
takatoh@apostrophe $ npm -v
6.13.4

Qiita の記事によると、複数バージョンの管理なんかもできるらしい。

画像をフェードアウト/フェードインで自動で切り替える

うわー!もう今月終わりそう!

というわけで、Scala の学習のほうはここひと月ほど滞っているんだけど、代わりに(?)今日は、HTML と JavaScript でタイトルに書いたようなことをやってみる。

jQuery を使ってスライドショーを実現するライブラリはいろいろあるんだけど、今日はそういうのは使わないでやる。さらには、jQuery 自体も使わないでやる。やるって言ったらやる。

さて、まずは jQuery 利用バージョン。いくつかの Web ページを参考に書いた(というかほぼコピペ)のがこれ。

<!DOCTYPE html>
<html>
  <head>
    <title>Fade images</title>
    <link rel="stylesheet" href="style.css" />
    <script src="../lib/jquery-3.4.1.min.js"></script>
    <script type="text/javascript">
      $(function() {
        const $width = 960;
        const $height = 540;
        const $interval = 3000;
        const $fade_speed = 1000;
        $("#slide ul li").css({"position": "absolute", "overflow": "hidden", "width": $width, "height": $height});
        $("#slide ul li").hide();
        $("#slide ul li:first").addClass("active").show();
        setInterval(function() {
          let $active = $("#slide ul li.active");
          let $next = $active.next("li").length ? $active.next("li") : $("#slide ul li:first");
          $active.fadeOut($fade_speed).removeClass("active");
          $next.fadeIn($fade_speed).addClass("active");
        }, $interval);
      });
    </script>
  </head>
  <body>
    <div id="slide">
      <ul>
        <li><a href=""><img src="img/img1.jpg" width="960" heght="540"/></a></li>
        <li><a href=""><img src="img/img2.jpg" width="960" heght="540"/></a></li>
        <li><a href=""><img src="img/img3.jpg" width="960" heght="540"/></a></li>
        <li><a href=""><img src="img/img4.jpg" width="960" heght="540"/></a></li>
      </ul>
    </div>
  </body>
</html>

これで、3秒ごとにフェードアウト/フェードインしながら画像が切り替わる。画像は4つ用意したけど、最後まで行ったら元に戻る。

つぎ。jQuery なしバージョン。さらにいくつかのページを参考に、試行錯誤した結果がこれ。

<!DOCTYPE html>
<html>
  <head>
    <title>Fade images</title>
    <link rel="stylesheet" href="style.css" />
    <script type="text/javascript">
      function fadeIn(el, speed) {
        el.style.transition = speed;
        el.style.opacity = "1";
      }

      function fadeOut(el, speed) {
        el.style.transition = speed;
        el.style.opacity = "0";
      }

      function slide() {
        const width = "960px";
        const height = "540px";
        const interval = 3000;  // miliseconds
        const fade_speed = "1.0s";
        const slide = document.querySelectorAll("#slide ul li");
        for (let i =0; i < slide.length; i++) {
          slide[i].style.position = "absolute";
          slide[i].style.overflow = "hidden";
          slide[i].style.width = width;
          slide[i].style.height = height;
          slide[i].style.display = "block";
          slide[i].style.opacity = "0";
        }
        const first = slide[0];
        first.classList.add("active");
        first.style.opacity = "1";
        setInterval(function() {
          let active = document.querySelector("#slide ul li.active");
          let next = active.nextElementSibling || first;
          fadeOut(active, fade_speed)
          active.classList.remove("active");
          fadeIn(next, fade_speed)
          next.classList.add("active");
        }, interval);
      }

      document.addEventListener("DOMContentLoaded", slide);
    </script>
  </head>
  <body>
    <div id="slide">
      <ul>
        <li><a href=""><img src="img/img1.jpg" width="960" heght="540"/></a></li>
        <li><a href=""><img src="img/img2.jpg" width="960" heght="540"/></a></li>
        <li><a href=""><img src="img/img3.jpg" width="960" heght="540"/></a></li>
        <li><a href=""><img src="img/img4.jpg" width="960" heght="540"/></a></li>
      </ul>
    </div>
  </body>
</html>

動作は同じ。

最近の流れとしては jQuery 使わないっていうのがあるみたいだけど、やっぱ使ったほうが楽ではあるよなあ。

HTMLとJavaScriptで画像をリサイズ

HTML + JavaScript ツール第3弾。
HTML5 の canvas 要素で画像のリサイズができるというので、やってみた。

 cf. 画像のリサイズ

Size を指定しておいて、リサイズしたい画像を選ぶとリサイズされた画像が画面に並ぶようになっている。画像をクリックすればダウンロードもできる。

参考にしたページはいくつかあるけど、↓ここを挙げておこう。

 cf. クライアント側でcanvasを使って画像をリサイズする – Qiita

以下、ソース。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Resize image</title>
    <script type="text/javascript">
      function resizeImage(base64image, callback) {
        const MAX_SIZE = Number(document.getElementById("size").value);
        let canvas = document.createElement("canvas");
        let ctx = canvas.getContext("2d");
        let image = new Image();
        image.crossOrigin = "Anonymous";
        image.onload = function(evt) {
          let dstWidth, dstHeight;
          if (this.width > this.height) {
            dstWidth = MAX_SIZE;
            dstHeight = this.height * MAX_SIZE / this.width;
          } else {
            dstHeight = MAX_SIZE;
            dstWidth = this.width * MAX_SIZE / this.height;
          }
          canvas.width = dstWidth;
          canvas.height = dstHeight;
          ctx.drawImage(this, 0, 0, this.width, this.height, 0, 0, dstWidth, dstHeight);
          callback(canvas.toDataURL("image/png"));
        };
        image.src = base64image;
      }

      function buildFilename(origname) {
        let re = /(.*)(?:\.([^.]+$))/;
        let basename = origname.match(re)[1];
        return "resized_" + basename + ".png";
      }

      function handleFileSelect(evt) {
        let files = evt.target.files;
        for (let i = 0, f; f = files[i]; i++) {
          let reader = new FileReader();
          reader.onload = (function(theFile) {
            return function(e) {
            resizeImage(e.target.result, function(imgUrl) {
              let dest = document.getElementById("list");
              let fig = document.createElement("figure");
              fig.style = "float: left;";
              let a = document.createElement("a");
              a.href = imgUrl;
              a.download = buildFilename(theFile.name);
              let image = document.createElement("img");
              image.src = imgUrl;
              let caption = document.createElement("figcaption")
              caption.innerHTML = theFile.name + " resized.";
              a.appendChild(image);
              fig.appendChild(a);
              fig.appendChild(caption);
              dest.appendChild(fig);
            });
          }})(f);
          reader.readAsDataURL(f);
        }
      }
 
      document.addEventListener("DOMContentLoaded", function() {
      document.getElementById("files").addEventListener("change", handleFileSelect, false);
      }, false);
    </script>
  </head>
  <body>
    <h1>Resize image</h1>
    <p>下のSizeで指定したサイズの正方形におさまるようにリサイズします。</p>
    <p>画像をクリックすればダウンロードできます。</p>
    Size: <input id="size" type="text" value="400"> pixel<br />
    <input id="files" multiple="multiple" name="files[]" type="file"><br />
    <div id="list"></div>
  </body>
</html>

HTMLとJavaScriptでMD5ハッシュ値の計算

先週、華氏度と摂氏度の換算をする WEB ページを作ってみて、HTML と JavaScript だけでページを作れば、サーバなしで簡単なツールを作れることが分かった。
そこで、いくつか同じように WEB ページで動くツールを作ってみている。今日は、ローカルファイルの MD5 ハッシュ値を計算するツール(ページ)を紹介しよう。

 cf. MD5ハッシュ値の計算

ページを作るにあたっては、次のページとそこからリンクされているページを参考にした。

 cf. ローカルバイナリファイルからMD5ハッシュ値を計算する

特にローカルファイルの扱い方が参考になった。また、MD5 ハッシュ値の計算は、次のページで公開されている JavaScript のライブラリをそのまま使わせてもらった。

 cf. 高度な JavaScript 技集

最後に HTML のソースを載せておく。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>MD5 calculator</title>
    <script type="text/javascript" src="js/md5.js"></script>
    <script type="text/javascript">
      const handleFileSelect = (evt) => {
        const files = evt.target.files;
        const output = document.getElementById("list");
        output.innerHTML = "<ul>";
        for (let i = 0, f; f = files[i]; i++) {
          const reader = new FileReader();
          reader.onload = ((theFile) => {
            return (e) => {
              const hexdigest = MD5_hexhash(e.target.result);
              output.innerHTML += "<li>" + hexdigest + "  " + theFile.name + "</li>";
            }
          })(f);
          reader.readAsBinaryString(f);
        }
        output.innerHTML += "</ul>"
      }
      document.addEventListener("DOMContentLoaded", () => {
        document.getElementById("files").addEventListener("change", handleFileSelect, false);
      }, false);
    </script>
  </head>
  <body>
    <h1>MD5 calculator</h1>
    <input type="file" id="files" name="files[]" multiple /><br />
    <output id="list"></output>
  </body>
</html>