O hirunewani blog

Frontend Weekly 2023-03-31

Created at

React.dev、Safari 16.4、Web API、TypeScript 5.0などについて紹介する。

Q & A

普段の業務において、質問を受けた際の回答などで有益そうなものをピックアップしています。

div要素にa要素を付けるとwidthが100%になり、button要素に付けるとwidthが最小幅のように振る舞うのは何故か

これはa要素が非置換要素であるのに対して、button要素が置換要素のように振る舞うからです。

まずHTML要素において、imgやiframe、audio、いくつかのフォーム要素などは置換要素に分類され、それ以外のほとんどの要素は非置換要素に分類されます。置換要素では、まず外的要因を排除した内在サイズと呼ばれるもので大きさが決定されますが、この内在サイズはmin-contentやmax-contentを考慮しており、高さや幅がautoとして動作するブロック/インラインレベルの置換要素ではmax-contentが使用されます。max-contentでは基本的にコンテンツの幅、コンテンツのオーバーフローを避けつつ最小の幅に等しくなります。

加えて、置換要素がブラウザによってコンテンツが取得され内容のサイズが決定した後に要素のサイズが計算されるのに対して、非置換要素はブラウザがその要素のサイズを計算する前に子要素のサイズが決定されている必要があります。従って、基本的に置換要素のサイズ計算は、非置換要素のサイズ計算より先に行われます。ただし、再度レイアウトが計算されるケースや、リソースの読み込みを待ってサイズを計算をするケースがあるため、絶対ではありません。

今回のケースでは、非置換要素であるa要素の場合、子要素のdiv要素によってwidthが100%に拡張されたのに対して、button要素の場合、まずbutton要素のサイズが決定され、その後div要素が適用されるため、button要素の横幅が100%にならないわけです。また、置換要素ではインラインレベルであってもwidthやheightを持てるため、これはbutton要素のdisplayに依存しません。


ここまでbutton要素は置換要素であると書きましたが、実際は置換要素ではありません。本来、非置換要素であり、長い間ブラウザにおいて置換要素のように扱われてきたため、中途半端、半置換要素と揶揄されるようなことになっていました。

現在では、WhatWGのレンダリングでWidgetに分類され、button要素のレイアウトについては個別に章立てされています。

ここまでの置換要素及びサイズの計算についての話は恐らく不正確な点がいくつかあるので、詳しくはWhatWGの置換要素の章などを見てください。

React

react.dev

2023年3月17日に、Reactの新しいドキュメントサイトであるreact.devが正式にリリースされました。

React Hooksがデフォルトとなり、学習とAPIドキュメントを兼ねており、豊富なサンプルに加え、より踏み込んだ説明が行われています。特にEscape hatchesYou Might Not Need an Effectなどは、業務でReactをなんとなくで触ってきた全ての人におすすめです。

Safari 16.4

2023年3月27日にSafari 16.4がリリースされました。2月にBeta版を扱っていますが、個人的にSafari 16.4はもっと話題になるべきだと思うので再度取り上げます。

Web API

多くのWeb APIがサポートされるようになりました。モダンブラウザの中でSafariのみがサポートしていなかったものが多く、ブラウザ互換性を見て失望することや、冗長な記述を強いられること、ポリフィルを含める必要がなくなるのは大変ありがたいです。数が多いので、使う機会があるかもしれないと思うものを適当にピックアップして軽く紹介します。

Fullscreen API

特定の要素を全画面モードで表示したり、それを解除したりできるAPIです。ブラウザゲームや、賃貸情報サイトの間取り図などの拡大などで有用かもしれません。ただし、これは離脱用のUIを作らない限り、ESCキーなどで離脱する必要があるため、ユーザーのことをよく考え利用した方が良いでしょう。

document.addEventListener(
  "keydown",
  e => {
    if (e.key === "Enter") {
      toggleFullScreen();
    }
  },
  false
);

function toggleFullScreen() {
  if (!document.fullscreenElement) {
    document.documentElement.requestFullscreen();
  } else if (document.exitFullscreen) {
    document.exitFullscreen();
  }
}

Screen Orientation API

画面の向きを取得できるAPIです。

const orientation = screen.orientation;
orientation.addEventListerner("change", () => {
  console.log(orientation.type, orientation.angle);
});

rotateBtn.addEventListener("click", async () => {
  const oppositeOrientation = screen.orientation.type.startsWith("portrait")
    ? "landscape"
    : "portrait";
  scrren.orientation
    .lock(oppositeOrientation)
    .then(() => {
      console.log(`Locked to ${oppositeOrientation}`);
    })
    .catch(error => {
      console.error(error);
    });
});

unlockBtn.addEventListener("click", () => {
  screen.otientation.unlock();
});

Screen Wake Lock API

端末が自動で画面を暗くしたり、ロックしたりするのを防ぐAPIです。アプリケーションの継続が必要なケースや、QRコードを表示しておく必要があるアプリなどで有用です。

const wakeLock = await navigator.wakeLock.request("screen");
wakeLock.addEventListener("release", () => {
  console.log("Happy release wakeLock!!");
});

await heavyTask();

await wakeLock.release();

ページのタブを切り替えるなどでドキュメントの可視性が変化すると解除されてしまう点には注意が必要です。必要に応じて、visibilityChangeイベントを監視し、ロックを掛け直しましょう。

document.addEventListener("visibilitychange", async () => {
  if (wakeLock !== null && document.visibilityState === "visible") {
    wakeLock = await navigator.wakeLock.request("screen");
  }
});

OffscreenCanvas

OffscreenCanvasは、端的に言えばDOMとCanvas APIの分離です。

従来のCanvasを利用した高度なアニメーションの描画はメインスレッドを圧迫するため、ユーザーの阻害するという問題がありました。OffscreenCanvasでは、アニメーションの描画をWeb WorkerのWorkerスレッドで行うことで、この問題が解消されます。

今までも、DOMの操作は出来ないものの重いデータ処理をWeb Workerなどで行うことで、負荷を軽減することは出来ましたが、OffscreenCanvasはTransferableオブジェクトであり、実質オフスクリーンでレンダリングできるDOMから分離されたCanvasであるため、描画までをWeb Workerで担えます。

const offscreenCanvas = canvas.transferControllToOffscreen();
const worker = new Worker("worker.js");
worker.postMessage({ canvas: offscreenCanvas }, [offscreenCanvas]);

// worker.js
onmessage = event => {
  const offscreenCanvas = event.data.canvas;
  const ctx = offscreenCanvas.getContext("webgl");
  // ...
};

Web Push API

今まではAndroidなどでしかサポートされていなかったWeb Pushが、iOS 16.4でサポートされました。 ホーム画面に追加されたWebアプリから、Push API、Notifications API、Service Workerなどを駆使して、ユーザーにプッシュ通知を送ることが可能になります。

またホーム画面に追加されたWebアプリでカウントを表示できるBadging APIなどもサポートされ、今後PWAの利用が広がると期待されます。

https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/

ECMAScript features

Safari 16.4は、非常に巨大なリリースです。JavaScritpの多くの機能も新たにサポートされました。

https://developer.apple.com/documentation/safari-release-notes/safari-16_4-release-notes

以下に、サポートされた機能のいくつかを軽く紹介します。

Array.formAsync

Array.fromがfor相当なのに対して、Array.fromAsyncはfor awaitと見れば理解しやすいと思います。 Async iterableを処理する方法として、Promise.allなどがありますが、Promise.allが並列実行であるのに対して、 Array.fromAsyncはfor await相当であり、順次実行されます。

https://github.com/tc39/proposal-array-from-async

Array#group, Array#groupToMap

Array#groupは、配列を指定した関数の戻り値でグルーピングしたオブジェクトを返します。 一方、Array#groupToMapは、配列を指定した関数の戻り値でグルーピングしたMapを返します。

https://github.com/tc39/proposal-array-grouping

コードを見た方が理解しやすいと思います。

const animals = [
  { name: "たま", type: "猫" },
  { name: "みけ", type: "猫" },
  { name: "ぽち", type: "犬" },
];
console.log(animals.group(animal => animal.type));
/* result
{
  猫: [
    { name: 'たま', type: '猫' },
    { name: 'みけ',  type: '猫' },
  ],
  犬: [
    { name: 'ぽち', type: '犬', },
  ],
}
*/
console.log(animals.groupToMap(animal => animal.type).get("猫"));
/* result
[
  { name: 'たま', type: '猫' },
  { name: 'みけ',  type: '猫' },
]
*/

Import Maps

ブラウザで実行されるimportによって読み込まれるパッケージのURLを指定する手段です。

https://github.com/WICG/import-maps#multiple-import-map-support

<head>
  <script type="importmap">
    {
      "imports": {
        "react": "https://unpkg.com/react@18/umd/react.development.js",
        "react-dom": "https://unpkg.com/react-dom@18/umd/react-dom.development.js"
      }
    }
  </script>
</head>
<body>
  <script type="module">
    import { createRoot } from "react-dom/client";
    import React from "react";
  </script>
</body>

TypeScript 5.0

2023年3月16日にTypeScript 5.0がリリースされました。Beta版でも取り上げましたが、ネタがあまりないので再度取り上げます。なお、TypeScriptはセマンティックバージョニングに準拠していないため、5.0は重要な変更が含まれていることを意味しません。

実際、allowImportingTsExtensionsmoduleResolution: "bundler"verbatimModuleSyntaxなどのフラグや、extendsに複数の設定ファイルを記述できるようになるなど コンパイラ周りの変更には興味深いものが多くあるものの、tsconfig.jsonの変更を滅多にしない多くのライトユーザーにとっては、すぐさま影響のあるバージョンではないと思われます。

以下では、今後誰でも使う機会がありそうな新機能について、いくつか紹介します。

https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-beta/

Decorators 

TypeScript 5.0では、TS39のStage3相当のデコレータの実装がされました。 今後、デコレータを利用したライブラリが多く登場すると予想されます。

なお、今までコンパイラフラグの--experimentalDecoratorsで利用できたデコレータ(Legacy Decoratorsと呼称される)は、古いプロポーザルを元にしたものであり、互換性がないことには注意が必要です。 ただしexperimentalDecoratorsが削除される予定はなく、TypeScript 5.0には、experimentalDecoratorsを有効にした際の挙動改善も含まれています。

// Legacy Decorators
@register
export class Foo {}
export class C {
  constructor(@inject(Foo) private x: any) {}
}

// New decorators proposal
export
@register
class Foo {}
export class C {
  // Cannot use decorators as arguments.
  constructor(@inject(Foo) private x: any) {}
}

const Type Parameters

今まで、あるオブジェクトのプロパティを取得する関数を実装した際に、具体的な型が欲しければ、呼び出す度に毎回引数へas constを付ける必要がありました。

TypeScript 5.0では、型パラメータにconst修飾子を付けることで、引数側でas constを付ける必要がなくなります。

/* 
TypeScript 4.9
*/
type HasNames = { readonly names: string[] };
function getNamesExactly<T extends HasNames>(arg: T): T["names"] {
  return arg.names;
}

// Inferred type: string[]
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });

// Inferred type: readonly ["Alice", "Bob", "Eve"]
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] as const });

/*
TypeScript 5.0
*/
type HasNames = { names: readonly string[] };
function getNamesExactly<const T extends HasNames>(arg: T): T["names"] {
  return arg.names;
}

// Inferred type: readonly ["Alice", "Bob", "Eve"]
// Note: Didn't need to write 'as const' here
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });