🏄
Matsuri-tech Frontend Weekly 2023-02

hrdtbs

hrdtbs

2023年2月22日
MFW

社内向けに、フロントエンド関連のニュースや業務で発生したQ&A、利用しているライブラリなどの情報を定期的に書いています。

# Q&A

普段の業務において、質問を受けた際の回答で面白そうなものをまとめました。

# Q. MediaRecorderを用いた録画機能がSafariで動作しない

指定されていたmineTypeがSafariでサポートされていなかったのが原因でした。 WebKitが、動画の場合はvideo/mp4のみをサポートしていることは実装から明らかです。 一方、Chromiumでは現在video/webmのみをサポートしています。

UserAgentは一般に見たくないと思うので、次のような分岐を追加することを推奨しました。

MediaRecorder.isTypeSupported("video/webm") ? "video/webm" : "video/mp4"

# Q. Next.jsのapiが404を返す。エラーが出ていないのに...

エラーが出ていないため、そもそもpages/apiディレクトリ以下のファイルが何らかの理由で認識されていない可能性が考えられます。

実際、next.config.jspageExtensionsの設定が原因でした。

pageExtensionsという名前は、ページコンポーネントを記述したファイルの拡張子を指定しているように見えますが、実際はpagesディレクトリ以下で特別な役割を持っている_app.js_document.jsなどに加えて、apiディレクトリ以下のファイルにも適用されます。 今回のケースでは、page.tsxなどが設定されていたため、apiディレクトリ以下のファイルが認識されていませんでした。

このことは以下のPRで注意書きが追加されています。

# Safari 16.4 beta

2023年2月にリリースされたiOS 16.4 beta 1及びSafari 16.4 betaについて、いくつか紹介します。

# Web Pushのサポート

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

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

# Many ECMAScript features supported

Safari 16.4 betaは、非常に巨大なリリースです。Web PushだけでなくECMAScriptの多くの機能が新たにサポートされました。

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

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を返します。

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

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を指定する手段です。

<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>

# AV1のサポート

AV1コーデックがSafari 16.4 betaでサポートされました。 しかし、AppleのM1/M2はAV1用のハードウェアアクセラレータを搭載していないため、利用できる環境はほぼありません。

# Chrome 110

2023年2月にリリースされたChrome 110について、いくつか紹介します。

# 古いWindows OSのサポートが終了

Chrome 110で、Windows 7/8/8.1などのOSのサポートが終了しました。 なお2023年1月に、MicrosoftはWindows 7/8/8.1のほとんどのバージョンのサポートを終了しています。

また最後のサポートバージョンであるChrome 109へのセキュリティ修正も2023年10月までとなっています。

R.I.P.

# PIP擬似クラスのサポート

PIP(Picture in Picture)とは、再生している動画を画面の隅に別ウィンドウで表示する機能です。 PIPを利用しているvideo要素に対してスタイルを適用できる:picture-in-picture疑似クラスがChrome 110でサポートされました。

:picture-in-picture:が1つだけであり、::after::first-letterのような擬似要素ではありません。 PIPで表示される別ウィンドウを表すものではなく、PIPを利用しているvideo要素を表す点に注意してください。

Chrome Developersの記事では、PIPモードであるvideo要素のデフォルトの見た目を上書きするデモが紹介されています。

video:picture-in-picture {
  opacity: 0;
}
.video-container {
  background: #000;
  position: relative;
}
.video-container:has(video:picture-in-picture)::before{
  content: 'Video is now playing in a Picture-in-Picture window';
  position: absolute;
  right: 36px;
  bottom: 36px;
  color: #ddd;
}

現状、PIPを利用する機会はあまりないかもしれませんが、Chrome 111ではPIPが任意の要素に対して利用できるdocumentPictureInPictureのサポートが予定されており、 今後は利用する機会が増えるかもしれません。

# TypeScript 5.0 Beta

2023年1月にTypeScript 5.0 Betaがリリースされました。正式版は2023年3月が予定されています。 なお、TypeScriptはセマンティックバージョニングに準拠していないため、5.0は重要な変更が含まれていることを意味しません。

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

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

# 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"] });

# Next.js v13.2

弊社のいくつかのサービスとこの技術ブログは現在Next.jsで構築されており、特にこの技術ブログでは、未だ実験的な機能であるApp directoryも利用しています。

以下では、2023年2月にリリースされたNext.js v13.2から、お世話になりそうな機能をいくつか紹介します。

# Metadata APIによるビルトインSEOサポート

今までのhead.jsが非推奨になり、代わりにMetadata APIが利用できるようになりました。

メタデータが静的な場合は、metadataをlayout.jsやpage.jsからexportし、動的な場合は、 generateMetadataメソッドをexportします。

import type { Metadata } from "next";

// 静的な場合
export const metadata = {
  viewport: "width=device-width, initial-scale=1",
  title: "My App",
  description: "My App Description",
  openGraph: {
    type: "website",
    // ...
  },
  twitter: {
    card: "summary",
    //...
  },
} satisfies Metadata;

// 動的な場合、記事ページなど
export async function generateMetadata({ params, searchParams }): Promise<Metadata> {
  const data = await getData(params.id)
  const metadata = {
    title: data.title,
    // ...
  } satisfies Metadata;
  return metadata;
}

Metadata APIでは、設定がルートからマージされていきます。head.jsではページ毎に設定する必要がありました。 次の例では、app/page.jsで設定したtwitterの設定が、app/about/page.jsの設定に引き継がれます。

// app/page.js
export const metadata = {
  title: "My App",
  description: "My App Description",
  twitter: {
    card: "summary",
    site: "@site",
    creator: "@creator",
    images: "https://example.com/image.png"
  },
} satisfies Metadata;

// app/about/page.js
export const metadata = {
  title: "About",
  description: "About Description",
  // twitterの設定が引き継がれる
} satisfies Metadata;

また、どのページでも後ろ側にブランド名を入れたいといったケースに簡単に対応できる機能もあります。 次の例では、これを指定したページ以下の階層でtitleAboutを指定すると、About|My Appのようなタイトルになります。

export const metadata = {
  title: {
    default: "My App",
    template: `%s | My App`,
  },
}

# Custom Route Handlers

pages/apiのApp directoryバージョンです。

今までApp directoryであっても、APIを実装したい場合はpages/apiを追加する必要がありました。 ですが今後はpage.jsの代わりにroute.jsを追加するだけで、APIを実装できるようになります。

# Statically Typed Links (Beta)

Linkコンポーネントのhrefに型が付くようになり、存在しないページへのリンクを防ぐことができます。 この機能はappディレクトリだけでなく、pagesディレクトリでも利用できます。 ただし、ベータ版であるため、next.config.jsexperimental.typedRoutesを追加する必要があります。

// next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    typedRoutes: true,
  },
};

module.exports = nextConfig;

# ES2023

ECMAScript 2023は、2023年にリリースされるECMAScriptの仕様です。 なおECMAScriptは、前年にStage 4となった機能が次期ECMAScriptに含まれるという仕組みであり、 Stage 4の条件として2つ以上の実装が必要であるため、次期ECMAScriptがほとんどのブラウザで既に実装されているということがよくあります。

以下では、ECMAScript 2023に追加されたものを紹介します。Symbols as WeakMap keysは紹介しません。

# Array find from last

全てのモダンブラウザで実装済みです。

const array = [ 1, 2, 3, 4];
array.find((item) => item % 2 === 1) // 1
array.findLast((item) => item % 2 === 1); // 3

array.findIndex((item) => item % 2 === 1) // 0
array.findLastIndex((item) => item % 2 === 1); // 2

# Hashbang Grammar

Node.jsなどと同様に、ファイルの先頭行にある#!をコメントとして扱います。 これにより、例えばサーバーサイドとクライアントサイドで同じコードを使いまわすことができます。

なお、全てのモダンブラウザで実装済みです。

#!/usr/bin/env node
// in the Script Goal
'use strict';
console.log(1);

# Change Array by copy

非破壊的な配列の操作をするメソッド群です。 これらが追加されると、Array.prototype.slice()などを利用したハックをしなくても済みます。 ただし、ほとんどのブラウザで未実装です。

const array = [2, 6, 3];
array.toReversed() // [3, 6, 2]
array.toSorted() // [2, 3, 6]
array.with(1, 2) // [2, 2, 3]

# Lighthouse 10

2023年2月にLighthouse 10がリリースされました。

# TTIの削除

操作可能になるまでの時間を表す指標であるTime To Interactive(TTI)が削除されました。 TTIが占めていたスコアウェイトは、CLSに移行されました。

削除された理由としては、ランキングで利用されるCore Web Vitalsの指標ではない点、Core Web Vitalsの指標であるインタラクティブ性(FID)を測定するにはTTIよりもTotal Blocking Time(TBT)の方がより良い指標である点などが考えられます。 FIDやTTI、TBTの関係性については次の図を参考にしてください。

なお、よく誤解されますがLighthouseとCore Web Vitalsには直接的な関連はありません。つまり、この変更はSEOに影響がありません。

# Squoosh

# Squoosh CLI及びlibsquooshが非推奨に

Squooshは、Googleが開発した画像圧縮ウェブアプリです。また、Squoosh CLIは、Squooshの圧縮機能をコマンドラインから利用できるようにしたものです。

ですが、2023年1月に経済状況などから取り組む時間や人がいないというコメントと共に、SquooshのCLI及びlibsquooshのコードがリポジトリから削除されました。

なお、数日後にGoogleでは1万2000人(全従業員の6%)の解雇を発表しています。