Frontend Weekly 2023-03-31
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 hatchesやYou 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.jsonmessage = (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 相当であり、順次実行されます。
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 は重要な変更が含まれていることを意味しません。
実際、allowImportingTsExtensions
やmoduleResolution: "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@registerexport class Foo {}export class C { constructor(@inject(Foo) private x: any) {}}
// New decorators proposalexport@registerclass 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' hereconst names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });