おじんブログ

Webアプリに関する知見とか雑記です

Inputタグにアップされた画像ファイルのプレビューを オブジェクト URLと データ URL でやる方法で書き比べてみた

動機

アップロードした画像を使って画像を加工するなどの処理の場合、どのように取り出して取り扱うか、というのが全然整理できていなかったのでその整理を込めてサンプルコードを書いた。

File オブジェクトや addEventListener もあんまり書いたことがなかったので、ライブラリなしの Vanilla JS で書いてみた。

やりたいこと

  • input[type="file"] に入力された画像はファイル名しか表示されないので、画像がプレビューされてほしい
  • fileを取り出した後に <img> タグに表示するためにURLに変換する必要があるが、アプローチとして URL.createObjectURL() を使う、 FileReaderreadAsDataURL() を使うアプローチがあった

TL;DR

Codesandbox で試せる環境を作った。勉強も兼ねてVanilla + TypeScriptで型をつけながら書いています。

  • 動作的にはどっちでプレビューしても殆ど変わりはない
  • URL.createObjectURL() してオブジェクトURLを生成すると、開放しない限りはメモリ上に残り続ける
  • なのでオブジェクトURLが不要になったタイミング、今回の場合だと画像の読み込みが終わったタイミングで URL.revokeObjectURL() を実行する必要がある
  • 開放するとそのオブジェクトURLを使って画像を表示、というのができなくなるので、複数箇所で同じ画像を表示するなら URL.revokeObjectURL() を実行するタイミングを考える必要がある
  • データURLはURL自体にデータを持っているようなものなのでオブジェクトURLよりは比較的取り回しが楽に感じた

解説

入力したファイルの取り出し

const fileForm = document.getElementById("userFile");

function handleFiles(event: Event) {
  const target = event.target as HTMLInputElement;
  const files = target.files;
  // ...
}

fileForm?.addEventListener("change", handleFiles, false);

input[type="file"] に対して document.getElementById("userFile").files という形で取り出せますが、フォームへの入力が変化したタイミングで処理を行いたいので EventListener 経由でアクセスして取り出しています。

取り出したファイルデータは複数の File データを持つ FileList オブジェクトとして取り出されます。

フォームから受け取りデータが単一の File オブジェクトではなく FileListオブジェクトなのは、フォームにて複数のファイルが入力する場合があるためです。

file からオブジェクトURLの変換

Array.from(files).forEach((file) => {
  // if you would like to watch uploaded file info, remove comment
  // console.log(file)
  const image = new Image(300);
  image.addEventListener("load", (event: Event) => {
    const target = event.target as HTMLImageElement;
    URL.revokeObjectURL(target.src);
  });
  const imageUrl = URL.createObjectURL(file);
  image.src = imageUrl;
  image.title = file.name;
  previewPoint?.appendChild(image);
});

まず new Image() にてプレビュー画像を表示するための <img> タグを生成しています。width=300 としたいのでコンストラクターに引数として渡しています。

オブジェクトURLは不要になったら開放する必要があるため、 <img> タグにて画像の読み込みが終わった時点で URL.revokeObjectURL() するように EventListener で登録します。

取得したFileを URL.createObjectURL() を通してオブジェクトURLを発行し、それを <img> タグの src に入力し、画像の表示を行います。

最後にプレビューするポイントにプレビュー画像のタグを付与する。

file からデータURLの変換

Array.from(files).forEach((file) => {
  const reader = new FileReader();
  reader.addEventListener(
    "load",
    (event: Event) => {
      const image = new Image(300);
      image.title = file.name;
      image.src = reader.result as string;
      previewPoint2?.appendChild(image);
    },
    false
  );
  reader.readAsDataURL(file);
});

FileReader.readAsDataURL() でファイルデータを読み込むとデータURLとして変換され、 FileReader.result にて取り出すことが出来る。

FileReaderがFileを読み込んだタイミングで FileReader.result にデータURLが入るため EventListener にて Event を登録する。

画像のタグを付与するあたりはオブジェクトURLと一緒。

実はこの箇所はMDNのサンプルコードのままではある。

最後に

フォームで入力した画像を取り出して取り扱う方法を学習することができた。

動機としては React Image Crop というライブラリを使おうと思った際に、当然のようにオブジェクトURLとbase64 data が登場したが、曖昧な理解だと今後も詰まりそうだと思ったのでWeb APIの標準を通じて学習した。

https://github.com/DominicTobias/react-image-crop

上記のオブジェクトURLを使ったサンプルコードを書いた後に、MDNのサンプルに自分がやりたいのとほぼ同じ物があったのでぐぬぬ…となったが、TypeScriptで型付けるという形で発展させた。

Web アプリケーションからのファイルの使用

Next.js で SG したサイトがなぜあんなに早いのか、 Chrome DevTools とコードリーディングで調べる

はじめに

静的コンテンツをCDNに乗せておくとアクセス時間が早い、という話ではなく、 Next.js を SG してブラウザで閲覧した時の挙動に絞っています。

TL;DR

  • SGした際に、 getStaticProps() 経由で渡したコンテンツは _next/data/ ディレクトリ配下に JSON 形式として書き出される
  • 該当するローティングに達すると、 JSON を読み込み、クライアントサイドでページを書き換える
  • すべてのコンテンツを一気に読み込むのではなく、ページへのリンクが表示されたら、そのリンク先のコンテンツの JSON を先読みしている
  • つまり、クリックする前に既にコンテンツは読み込まれているので、クライアントサイドの書き換えしか時間がかからない

Chrome DevTools で見てみる

Next.js の公式サイトは SG で構築されています。これを例として見ていきます。 もし初めて見て場合は各リンクをクリックしてみてください。 どのページも一瞬で遷移するのを感じると思います。

https://nextjs.org/

Chrome の DevTools を開き、 Network タブを開き、フィルター欄に json を入力し、一旦更新します。 するとJSONファイルが読み込まれているのがわかります。 f:id:mr_ozin:20211108224946p:plain

Headers タブを開いて Request URL 見ると、サイトのルートの /_next/data/ ディレクトリ配下にこのJSONファイルが格納されているのがわかります。 f:id:mr_ozin:20211108225015p:plain

Preview タブに切り替えてJSONの中身を見るとページの情報が格納されています。 f:id:mr_ozin:20211108225045p:plain

実際に該当のページに遷移すると、 JSON に格納したデータと一致するのを確認できます。

つまりページ遷移 = 読み込んだ JSON を JS でページの内容を書き換えるクライアントサイドレンダリングをしていると推測できます。

コードベースで言えば下記でビルド時にファイルパスを指定したマニフェストを作成し、ページロード時のコンテンツのパスを指定していると思われます。

https://github.com/vercel/next.js/blob/ad981783abbb347b6510c4dee5ce969e87ab0e24/packages/next/build/index.ts#L1725-L1734

https://github.com/vercel/next.js/blob/ad981783abbb347b6510c4dee5ce969e87ab0e24/packages/next/client/page-loader.ts#L141-L143

自作のサイトでも確認してみる

JSON の中の pageProps の構造について確証が持てなかったので自作のサイトでも確認してみることにしました。

https://nextjs-demo-spacex-crew.vercel.app/

自分のソースコードと比較すると、 pageProps 中の構造は Next.js の getStaticProps() で渡している構造と一致します。

つまり、ビルド時に渡したコンテンツのデータはJSONとして保存されていることが推測できます。 f:id:mr_ozin:20211108225109p:plain

https://github.com/ryokryok/nextjs-demo-spacex-crew/blob/b8812902315d0b111040f4980b1a70906faddcd0/pages/crews/%5Bid%5D.tsx#L30-L37

基本的にページコンテンツ必要になったら読み込む

ではブログの一覧画面のようなページが有り、リンク先が大量にあるようなケースでは読み込まれるJSONファイルの数が増えて、初期表示が遅れるのでは?と思いますが、その心配はありません。

Network タブを開いたまま、ページを上下にスクロールすると読み込まれる JSON ファイルが増えているのを確認できます。

ゆっくりスクロールするとわかりますが、ページのリンク が表示されたらその 遷移先のコンテンツのJSON が読み込まれます。

ユーザーが遷移先のリンクを閲覧する必要性が出てくるタイミングとして 「リンクが表示された段階」 を想定しており、そこで初めてJSONを読み込むように設計していることが推測されます。

リンクが表示された段階で既にコンテンツは読み込まれており、リンクをクリックするとクライアントサイドで書き換え、ページ内の画像やアセットの読み込みが実行されます。

これによって一気に読み込まれるJSONファイルの数を減らし、低速回線でも快適に閲覧できます。

コードベースで言えば next/link のコードを見ると Intersection Observer API を使って監視をしているのが確認できます。

(useIntersection という Hooks 経由で使用しています)

https://github.com/vercel/next.js/blob/4cdb585962b4f17be55848740d6e34106d730217/packages/next/client/link.tsx#L249

https://github.com/vercel/next.js/blob/4cdb585962b4f17be55848740d6e34106d730217/packages/next/client/use-intersection.tsx#L18

※ちょっと脱線しますが、 next/imageuseIntersection を使って監視しています。

https://github.com/vercel/next.js/blob/4cdb585962b4f17be55848740d6e34106d730217/packages/next/client/image.tsx#L438

最後に

Next.js は基本的にマニュアルどおりに作成すれば適宜最適化され、表示速度の早い Web サイトを簡単に作成できます。

素の軽い HTML ページに比べて、初回の表示は微妙に劣ることはありえますが、2ページ目以降は先読みした JSON を元にクライアントサイドレンダリングするのが基本のため、ページ切り替えが早いです。

コードリーディングした箇所についてはあまり自身がないので、間違っていたら指摘お願います。

Next.js で SG したサイトがなぜあんなに早いのか、 Chrome DevTools とコードリーディングで調べる

はじめに

静的コンテンツをCDNに乗せておくとアクセス時間が早い、という話ではなく、 Next.js を SG してブラウザで閲覧した時の挙動に絞っています。

TL;DR

  • SGした際に、 getStaticProps() 経由で渡したコンテンツは _next/data/ ディレクトリ配下に JSON 形式として書き出される
  • 該当するローティングに達すると、 JSON を読み込み、クライアントサイドでページを書き換える
  • すべてのコンテンツを一気に読み込むのではなく、ページへのリンクが表示されたら、そのリンク先のコンテンツの JSON を先読みしている
  • つまり、クリックする前に既にコンテンツは読み込まれているので、クライアントサイドの書き換えしか時間がかからない

Chrome DevTools で見てみる

Next.js の公式サイトは SG で構築されています。これを例として見ていきます。 もし初めて見て場合は各リンクをクリックしてみてください。 どのページも一瞬で遷移するのを感じると思います。

https://nextjs.org/

Chrome の DevTools を開き、 Network タブを開き、フィルター欄に json を入力し、一旦更新します。 するとJSONファイルが読み込まれているのがわかります。 f:id:mr_ozin:20211108224946p:plain

Headers タブを開いて Request URL 見ると、サイトのルートの /_next/data/ ディレクトリ配下にこのJSONファイルが格納されているのがわかります。 f:id:mr_ozin:20211108225015p:plain

Preview タブに切り替えてJSONの中身を見るとページの情報が格納されています。 f:id:mr_ozin:20211108225045p:plain

実際に該当のページに遷移すると、 JSON に格納したデータと一致するのを確認できます。

つまりページ遷移 = 読み込んだ JSON を JS でページの内容を書き換えるクライアントサイドレンダリングをしていると推測できます。

コードベースで言えば下記でビルド時にファイルパスを指定したマニフェストを作成し、ページロード時のコンテンツのパスを指定していると思われます。

https://github.com/vercel/next.js/blob/ad981783abbb347b6510c4dee5ce969e87ab0e24/packages/next/build/index.ts#L1725-L1734

https://github.com/vercel/next.js/blob/ad981783abbb347b6510c4dee5ce969e87ab0e24/packages/next/client/page-loader.ts#L141-L143

自作のサイトでも確認してみる

JSON の中の pageProps の構造について確証が持てなかったので自作のサイトでも確認してみることにしました。

https://nextjs-demo-spacex-crew.vercel.app/

自分のソースコードと比較すると、 pageProps 中の構造は Next.js の getStaticProps() で渡している構造と一致します。

つまり、ビルド時に渡したコンテンツのデータはJSONとして保存されていることが推測できます。 f:id:mr_ozin:20211108225109p:plain

https://github.com/ryokryok/nextjs-demo-spacex-crew/blob/b8812902315d0b111040f4980b1a70906faddcd0/pages/crews/%5Bid%5D.tsx#L30-L37

基本的にページコンテンツ必要になったら読み込む

ではブログの一覧画面のようなページが有り、リンク先が大量にあるようなケースでは読み込まれるJSONファイルの数が増えて、初期表示が遅れるのでは?と思いますが、その心配はありません。

Network タブを開いたまま、ページを上下にスクロールすると読み込まれる JSON ファイルが増えているのを確認できます。

ゆっくりスクロールするとわかりますが、ページのリンク が表示されたらその 遷移先のコンテンツのJSON が読み込まれます。

ユーザーが遷移先のリンクを閲覧する必要性が出てくるタイミングとして 「リンクが表示された段階」 を想定しており、そこで初めてJSONを読み込むように設計していることが推測されます。

リンクが表示された段階で既にコンテンツは読み込まれており、リンクをクリックするとクライアントサイドで書き換え、ページ内の画像やアセットの読み込みが実行されます。

これによって一気に読み込まれるJSONファイルの数を減らし、低速回線でも快適に閲覧できます。

コードベースで言えば next/link のコードを見ると Intersection Observer API を使って監視をしているのが確認できます。

(useIntersection という Hooks 経由で使用しています)

https://github.com/vercel/next.js/blob/4cdb585962b4f17be55848740d6e34106d730217/packages/next/client/link.tsx#L249

https://github.com/vercel/next.js/blob/4cdb585962b4f17be55848740d6e34106d730217/packages/next/client/use-intersection.tsx#L18

※ちょっと脱線しますが、 next/imageuseIntersection を使って監視しています。

https://github.com/vercel/next.js/blob/4cdb585962b4f17be55848740d6e34106d730217/packages/next/client/image.tsx#L438

最後に

Next.js は基本的にマニュアルどおりに作成すれば適宜最適化され、表示速度の早い Web サイトを簡単に作成できます。

素の軽い HTML ページに比べて、初回の表示は微妙に劣ることはありえますが、2ページ目以降は先読みした JSON を元にクライアントサイドレンダリングするのが基本のため、ページ切り替えが早いです。

コードリーディングした箇所についてはあまり自身がないので、間違っていたら指摘お願います。

Next.js と外部 API で作成した静的サイトをデプロイしようとしたら、画像最適化を考えて結局 Vercel にデプロイした

TL;DR

  • ローカルのリポジトリに入っていない、外部の API から取得した画像の最適化をどうするか
  • 結局、 Vercel なら外部 URL の画像も最適化して高速で配信できるので Vercel を選択した

経由

Next.jsの getStaticPropsgetStaticPaths を使ったStatic Generationの素振りをするために作成しました。

悩んだ末に結局 Vercel デプロイ

最初は Vercel にロックインはしたくないと思い、 next export で静的に書き出して firebaseNetlify にアップしようとしたしたのですが、 next export を使用する時は next/image が基本的に使えません。

なので、<img> タグで置き換えようかと思ったのですが、結局下記の理由で Vercel にロックインしてもいいので Vercel にデプロイすることにしました。

  • 今回のように外部 API を利用する場合はビルド時に画像を最適化するのは難しい
  • せっかく Next.js で高速な静的サイトを作成するのに、画像の表示がネックになるのは意味がないのでは?
  • Vercel のドキュメントを読んだら、外部 URL の画像も最適化して配信されることが判明
  • Vercel にデプロイするなら、 next export しないので next/image がそのまま使えて楽

Next.js は Vercel が作っているから親和性がいいのだろうと思っていましたが、真面目に考えると悩ましい画像の最適化・配信も担ってくれるので本当にありがたいです。

外部の API に含まれている画像 URL を next/image で使用する時の注意点

  • 表示する URL のドメインを調べて next.config.js に追記しておく必要があります。
  • もし画像のアップロード先のドメインが複数ある場合はそれらを列挙する必要があります。
  • もしドメインがない場合はエラーが出るので next.config.js に追記して dev server を再起動してください。

公式ドキュメント

https://nextjs.org/docs/api-reference/next/image#domains

今回、自分の場合は下記のように書きました。

/** @type {import('next').NextConfig} */
module.exports = {
  reactStrictMode: true,
  images: {
    domains: ['imgur.com', 'i.imgur.com', 'images2.imgbox.com'],
  },
}

JavaScript で小数点以下を桁数指定して四捨五入するなら、parseFloat() と toFixed() を組み合わせればよい

TL;DR

第一引数に四捨五入する値を、第二引数に小数点以下の桁数を指定する関数を定義する。

/**
 * Rounding number with digit
 * Example:
 * round(3.14159265359, 2) = 3.14
 * @param {number} value
 * @param {number} digits default : 0
 * @returns {number} rounded number
 */
function round(value, digits = 0){
  return parseFloat(value.toFixed(digits))
}

// Math.E = 3.141592653589793
round(Math.PI, 3)
// expected output: 3.142

round(Math.PI)
// expected output: 3

Math.round(Math.PI)
// expected output: 3

解説

JavaScriptで四捨五入するための関数として Math.round() があった。 ただしこれは、引数として与えられた数字を四捨五入して整数部分しか表示できない。

四捨五入後に、整数部分だけでなく、小数点以下の桁数取り扱う場合は下記の関数を組み合わせる必要がある。

まず toFixed() で桁数を指定して整形する。このとき、戻り値が文字列なので、 parseFloat() によって小数点を含んだ文字列に変換する。

3.141592653589793.toFixed(3)
// expected output: "3.142"
parseFloat("3.142")
// expected output: 3.142

厳密にやる場合はparseFloat() の値に対して Number.isNaN() で判定すれば良いと思われる。

余談

自分がこの方法を調べたとき、元の数字に100を掛けて Math.round() した後に再び100で割る、という方法がかなり出てきて直感的に「なんかスマートじゃない」と思った。

なので、調べてみたところ、Math.jsが提供している round() 関数では自分がやりたいことに近かったので、該当する実装箇所 を参考にした。

他にも参考になりそうな実装があるので数値処理をしたい方は Math.js のソースを読めば良いと思います。

「Reactを自作しよう」をやってみた & Babelの設定の復習

動機

以前から React を自作しよう を見かけてやりたいと思っていたが GW でやっとまとまった時間が取れたのでじっくり取り込んだ。

React をよく個人的に使うので、その設計と実装を経験したほうが React への理解が深まるのでは?と思った。

TL;DR

作業結果は下記のリポジトリにアップした。

https://github.com/ryokryok/practice-diy-react

感想

記事の中で実装したのは下記の React の関数を簡素化したものだった。

  • React.createElement
  • React.useState
  • ReactDOM.render

実際手を動かした見た所感は下記。

  • 本当に 1 からの実装して数百行のコードで React の関数を置き換えるので面白かった
  • /** @jsx Didact.createElement * とコメントを書くだけで JSX の変換関数が置き換わるのを知った
  • Fiber という作業単位によって効率よくレンダリングするというのが理解できた
  • JSX が取りうる構造を想定しながらそれを実装に落とし込む方法が理解できた
    • HTML の要素が親子関係、または兄弟関係を想定するなど
  • どのように差分検出するのか、というのを理解できた
    • 更新か、追加か、削除かなどを判定するためにどのような実装
  • 型がない JavaScript での実装なので、作業中にどのような Object の実装を目指しているのかちょっとわかりにくかった

環境構築

翻訳版にも元記事にも環境構築はなく、悩んだが Babel で JSX を JS に変換したときの挙動を確認する手順があったため、開発用にBabel + Webpack でセットアップして、変換確認用に script を設定した。

Babel のプラグインに関してはただ単純に変換した結果を見たいため、@babel/preset-react だけ入れた。

通常なら "@babel/preset-env" も入れるが今回はパス。

{
  "presets": ["@babel/preset-react"]
}

package.json にて scripts を追加して yarn transform すればJSX を JS の関数に変換した結果が temp ディレクトリーに展開される。

  "scripts": {
    "transform": "babel src -d temp",
    "dev": "webpack serve",
    "build:dev": "webpack --mode=development",
    "build": "webpack --mode=production"
  },

記事中にもあったがコメントで @jsx Didact.createElement と書くと JSX の変換用関数を別のものに指定できる。

(標準では React.createElement が指定される)

公式では下記に記載がある。例として Preact の変換用関数の記載がある。

@babel/plugin-transform-react-jsx

後は Webpack を入れて、 babel-loader で変換するようにするだけ。Webpack の設定は素朴なので詳細はリポジトリを見てください。


自分以外にやってみた人の記事を見ると create-react-app したのを eject していた。

https://blog.shibayu36.org/entry/2021/04/15/173000

参考

プライムデーで買ってよかったもの

プライムデーから2週間ぐらい経過したけど振り返る。 なんだかんだで合計3万円ぐらい買った。基本的に必要なものを買えたので満足。

Amazon Basicの10mm厚のヨガマット

自宅トレーニングの環境を改善するために買った。 ケトルベルを振ったりスクワットするときに足裏の負荷がかなり減って助かる。 今まで4mm厚のヨガマットを使っていたけど、全く別物。 ゴム臭いというレビューがあったが、プランクぐらいの姿勢なら感じない。鼻を近づけて感じるレベル。

www.amazon.co.jp

8畳向け LEDシーリングライト

3年ぐらい前に買ったLED電球の天井照明が暗かったので買った。 6畳の部屋だけど、8畳タイプでも値段が変わらなかったのでこちらを購入。 LEDシーリングライトって初めてなんだけど、最大光量にするとキンキンに明るくて目が覚めるレベル。 基本的には最低光量から1段上げて、1段温色寄りにしている。

www.amazon.co.jp

ソケットレンチ

バイクの整備に使用するので買った。 まだ使っていないけど、パッケージがそのまま収納ホルダーなので気楽に運搬できるの良い。 ケースみたいにひっくり返してソケットが吹っ飛ぶというのもないのは楽。 在庫切れになっているけど売れると思うので再入荷してほしい。

www.amazon.co.jp

Anker 8-in-1 ハブ

Macbookに接続するのに使用するため買った。 正直オーバースペックだが、LANケーブルも挿せるのでWi-Fiの調子が悪くてもそっちで解決できる。 2つのUSB3.0の端子は片方はオーディオインターフェース、片方はHHKBとマウスをつないでいる。

www.amazon.co.jp

Echo Show 5

アラームと作業中の音楽流しのために買った。 かなり低音が太いため、Alexaが話すたびにドスの利いた機械音声が机を震わせる。 半分ですらかなりの音量で、アラーム効果は強い。 毎朝おはようを言うと今日はなんの日か教えてくれる。和む。 Apple Musicを標準の音楽サービスとして登録できるの快適。 ちなみにBluetooth経由でスマホの音楽流すと低音がないスカスカな音になるのでアレクサ~を流してって言う必要あり。

www.amazon.co.jp

SwitchBot Hub Mini

Echo Showと抱合せで買うと安かったので買った。 が、正直Nature Remoと比較するとリモコンのメーカー検出の精度が微妙だったりリモコンUIが実際のものと剥離した感じがあり、Alexaと連携前提にしたほうがいいかも。

www.amazon.co.jp

総括

プライムデーは直前で値上げがあるとか買うものないとか言われるけど、大体は20%OFFでここ1年で最安値だったりするし、欲しい物が安かったら買うの感覚なら何買っても得する。

余談だけど最近は骨伝導イヤホンが気になっている。 音楽流しつつ周りの音を聞けるので職場での作業だったり、インターホン鳴らされたときに反応できるし、物理的に耳に突っ込まないので汚れる心配が減るのがいいなあと思っている。 なぜプライムデーが終わったときにしばらく何も買いたくないと思っていたのになんで物欲が湧くんだ…

www.amazon.co.jp