dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

TypeScript: 配列の値をString Literal Typesとして使う

メモ。
こんな感じで、ある配列として定義されている値に対し、「その値のいずれか」を意図した String Literal Types を作りたい。

const size = ['small', 'medium', 'large'];

type Size = 'small' | 'medium' | 'large';

でも、配列と型定義とで値を二度書きたくない。


こうする

TypeScript 3.4 で導入された const assertion を使うと、次のように書ける。

const size = ['small', 'medium', 'large'] as const; // readonly ["small", "medium", "large"]

type Size = typeof size[number]; // 'small' | 'medium' | 'large'

(Playground)

こちらの Stack Overflow を参考にした。


リファレンス

TypeScript: ReactNode型とReactElement型とReactChild型

メモ。

  • ReactNode
  • ReactElement
  • ReactChild

の関係性、何回か調べている気がするので整理しておく。

f:id:dackdive:20190807012948p:plain

@types/react の型定義
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts
を参照した。

図で、ReactNodeArrayArray<ReactNode> 以外の線は Union Types を表している。
たとえば

type ReactChild = ReactElement | ReactText

である。


メモ

  • JSX が受け付けるすべてのものをまとめた type として ReactNode があり、そこから string や null などを除いた純粋な React コンポーネントを意味するのが ReactElement
  • ほとんどが type alias だが、ReactNodeArrayReactPortal だけは interface だった
    • ReactNodeArrayArray<ReactNode> を継承してるだけ(コード)

npm installしたパッケージをVSCodeでデバッグする

npmでインストールしたパッケージが期待通りに動かず内部の動作を確認したい、となったときの話。

これまでは該当のスクリプトconsole.log() とか直接書いてデバッグしてたんですが、あまりにも効率が悪いのでもうちょっと良いやり方を見つけたい。
VSCodeでbreakpoint置きつつ変数の中身を見たりステップ実行したい。
と思ったので調べました。

ここではデバッグするパッケージの例として Storybook を取り上げてみます。
Storybookはたいていpackage.json

"scripts": {
  "storybook": "start-storybook -p 6006 -s ./assets"
}

のようなスクリプトを書いて実行することが多いと思います。
このとき、start-storybookというコマンド内部で何が行われているかを調査します。


手順

1. start-storybookコマンドで実行されるファイルを特定する

まずは、start-storybookコマンドによって実行されるファイルを見つけます。
npm scriptにコマンドで書けるということは node_modules/.bin 以下に該当のコマンドがあるはずなので探すと、

$ ls -l node_modules/.bin | grep start-storybook
lrwxr-xr-x  1 yamazaki  staff  32  6 20 22:37 start-storybook@ -> ../@storybook/react/bin/index.js

ということで、今回の場合 node_modules/@storybook/react/bin/index.js を見ればいいことがわかります。

また、VSCodeの場合シンボリックリンクはマウスオーバーでリンク先がわかります。

f:id:dackdive:20190620230331g:plain

(もうちょっと良い表示方法ないのかな)

別の方法として、対象のnpmパッケージのpackage.jsonbinを見るという方法もあります。

見つけたファイルの冒頭にbreakpointを置いておきます。
今回の @storybook/react/bin/index.js は @storybook/react/dist/server/index.js を読み込んでいるだけだったので、そちらの先頭にbreakpointをつけました。

f:id:dackdive:20190620230941p:plain


2. デバッグ用の設定ファイル(launch.json)を作る

次は、VSCode上でデバッグを行うために必要な設定ファイルを作ります。 コマンドパレット(Macならcmd+shift+P)で

Debug: Open launch.json

を実行します。または、直接.vscode/ディレクトリ下にlaunch.jsonというファイルを作成してもいいです。

コマンドパレットから作成した場合は

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "program": "${workspaceFolder}/index.js"
    }
  ]
}

という雛形が作成されているかと思います。


3. デバッグしたいコマンドをprogramに記載する

作成したlaunch.jsonprogramという値が、デバッグ実行されるコマンドです。
なので、ここにnpm scriptとして書かれていたコマンドをコピーしてきます。

注意事項としては

  • npm scriptにstart-storybookと書いていた場合、programには${workspaceFolder}/node_modules/.bin/start-storybookと書く
    • ${workspaceFolder}は必須っぽいです
  • 引数がある場合、programではなくargsという属性に配列で記載する

という点があります。

なので、今回の場合

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "debug start-storybook",
      "program": "${workspaceFolder}/node_modules/.bin/start-storybook",
      "args": ["-p 6006", "-s ./assets"]
    }
  ]
}

となります。

なおnameは識別しやすい好きな名前に変えていいです。


4. デバッグ実行する

ここまでで準備はできたので、コマンドを実行します。
左側のDebugアイコンか、Macだとcmd+shift+Dデバッグ用のパネルが表示されます。

f:id:dackdive:20190620232929p:plain

パネル上部にnameで指定したコマンドが表示されます。目的のコマンドを選んで▶をクリックします。

f:id:dackdive:20190620233142p:plain

無事、最初にbreakpointを置いた箇所で止まってくれました。
左側のパネルのVARIABLES、WATCH、CALL STACK、BREAKPOINTSなどはだいたいブラウザのDeveloper Toolsと同じ機能です。

というわけで、無事やりたいことが実現できました。


よくわかっていないこと

公式ドキュメントによると、launch.jsonconfigurationsの書き方は何種類かあるようです。

そのうちの1つに Launch via NPM というのがあるので、npm scriptはこっちを使えばいいのかと思ってましたがうまく起動できませんでした。
(エラーにはならないが何も起きない)


リファレンス

VSCodeでのデバッグ方法については、公式ドキュメントにかなり多くの情報が載っています。

自分もまだ完全には理解できてません。

Google I/O'19の「Speed at Scale: Web Performance Tips and Tricks from the Trenches」を観たメモ

途中飛ばしてしまった箇所もありますが、一通り観たのでメモを。

長いので最初にまとめ

動画の最後のスライドにキーワードがまとまってます。

f:id:dackdive:20190604035502p:plain

  • 速くする(Get fast)ための13個のTips
  • 速さを維持する(Stay Fast)ためのTipsとしてPerformance BudgetsおよびLightWallet

が紹介されています。
また、このセッションではパフォーマンス改善の事例が多数紹介されています。

f:id:dackdive:20190604035551p:plain


Performance Budgets

  • パフォーマンスを維持するためのしくみ
    • 予算を管理するように守るべきパフォーマンス指標を設定できる
  • Performance Budgetsでできること
    • Time (TTI ◯秒以下)
    • Resources (JSは◯KB以下)
    • Lighthouse (スコア◯点以上)

f:id:dackdive:20190604035618p:plain

  • Budgetsすなわち予算を設定することで、shipする前にパフォーマンスの問題に気づける
  • 事例: Walmart Grocely
    • PR時にチェックし、バンドルサイズが1%以上増加するとPRは失敗、Issue登録されPerformance Engineerにエスカレーションされる


LightWallet

  • Webサイトのパフォーマンス測定ツールであるLighthouseでPerformance Budgetsをサポート
  • CLI版のLighthouseで使える
    • Lighthouse自体はCLI版とChrome拡張版がある
$ npm i -g lighthouse
$ lighthouse https://example.com --budgetPath budgets.json --chrome-flags="--window-size=1280,660" --view

🤔時間系のBudgetsは指定できない?


画像

Lazy Loading
  • JSのライブラリを使った例だと、こんな書き方をすることが多い
    • LazySizesやReact Lazy Loadなど
<img data-src="images/unicorn.jpg" class="lazyload">
  • 事例: chrome.com サイト
    • 20% Faster page load times (mobile)
    • 26% Faster page laod times (desktop, Windows)
    • 画像サイズに関してはページ初回ロード(initial page)で46%の削減に成功
  • 事例: Netflix

    • たくさんの動画がタイル状に並んだページ
    • 初期描画では最初に見える数行分の画像のみロードし、残りはlazy load
    • ページ全体で4.4MBの画像があるのを、初期描画では1.2MBに抑えることができた
    • メモリだと45MB(Full Page Load) -> 8MB(Initial Page State)
  • ブラウザのAPIで画像のlazy loadをサポート(comming to Chrome this summer.)

    • iframeも
<img loading=eager>
<img loading=lazy>


Responsive Images
<!-- By width -->
<img src="cat.jpg"
  srcset="cat-240.jpg 240w,
          cat-480.jpg 480w,
          cat-960.jpg 960w>


Images CDNs
  • image optimization as a service
    • Cloudinary, Imgix, Thumborなど

f:id:dackdive:20190604035824p:plain

  • 事例: Trivago
    • Image CDNとしてCloudinaryを採用
    • Image CDNを利用することにより、自社のエンジニアはコアビジネスに集中できた
    • 全体の画像サイズを80%削減
  • 日本の事例もいくつか(一休、ANA)

f:id:dackdive:20190604035910p:plain

JavaScript

Defer Third-Party JavaScript
  • third-partyのコードはJS実行時間の57%を占める
  • 事例:Telegraph
    • JSをすべて(third-partyだけでなく自社のものも含め)deferして描画を3s高速化した
  • ref. asyncdeferの違いについては \<script> タグに async / defer を付けた場合のタイミング - Qiita
    • deferはJSの実行順序を保証してくれる
  • 事例: chrome.com
    • トップページに埋め込んでいたYouTube動画をlazy loadしたところTTIで10sの高速化


Remove Expensive Libraries
  • moment.js, jQuery, Bootstrapのようなサイズの大きいライブラリをリプレイスする
    • 例: lodash -> lodash-es, momentjs -> date-fns
    • サイズが小さいことに加え、Tree-shakeableなライブラリを選ぶのも重要
  • 事例: Tokopedia
    • ランディングページをSvelteでリライトした
    • 新バージョンはabove-the-fold contentの描画に必要なJSは37KB
    • Reactで作成した現行バージョンは320KB
    • ランディングページ以外はReactのまま。Service Workerを使ってprecache
  • Update dependencies
    • 事例: Zalando (ヨーロッパのfashion retailer)
      • Reactを15.6.1 -> 16.2.0に上げただけでload timeが100ms向上した
  • Code-splitting


Critical CSS

  • Critical CSS: above-the-fold contentの描画に必要なCSS
  • ドキュメントにインラインで埋め込むべき。14KB以下
  • 事例: TUI (ヨーロッパの旅行サイト)
    • First Contentful Paintの描画を2.4s -> 1.2sに短縮
  • 事例: 日経
    • Critical CSSが300KBもあった
    • シチュエーション(ユーザーがログイン状態か、など)に応じた適切なCritical CSSを返すサーバーを構築
    • ESI: Edge Side Inclusion

Lightning Web Componentsの一部がオープンソース化された

現在開催中の TrailheaDX'19 で発表があったようですね。

記事によると、LWCの機能は以下の3つのレイヤーから成るとし、

  • The Lightning Web Components framework: the framework’s engine.
  • The Base Lightning Components: a set of over 70 UI components all built as custom elements.
  • Salesforce Bindings, a set of specialized services that provide declarative and imperative access to Salesforce data and metadata, data caching, and data synchronization.

今回オープンソースとして公開されたのは、一番下の基本的なレイヤーである framework 部分とのこと。

f:id:dackdive:20190530125155p:plain

(画像は該当のブログ記事より引用)

Salesforceプラットフォーム固有の要素を含まないので、Heroku などの任意のプラットフォームで利用できます。

発表と同時に lwc.dev という充実したドキュメントを用意したり、
create-react-app ライクな lwc-create-app というコマンドで始められたり
ドキュメント内に playground があったりというあたりはさすがですね。

なお、ブログ記事中にはGitHubリポジトリは紹介されてませんでしたが、こちらです。

TypeScript: ReactのContextに型をつける(useContextと16.3以前のLegacy Contextも含む)

TypeScript + Reactでコンポーネントを書くとき、Context を使っているコンポーネントに対してどう型を書くのが正解か迷ったので、調べたことをメモしておきます。

調べるきっかけとなったコンポーネントは React 16.3 以前の Legacy Context を使った書き方になっていたのですが、ついでに新しい Context での書き方と、最新の Hooks の useContext() を使った場合も調べます。

なお、Context および useContext() についての説明は公式ドキュメントに譲ります。

目次


Contextを使う前のサンプルコード

import React from "react";
import "./App.css";

const ThemedButton: React.FC<{ theme: string }> = props => (
  <button className={props.theme}>Click me</button>
);

const Toolbar: React.FC<{ theme: string }> = props => (
  <div>
    Hello, TypeScript & React. <ThemedButton theme={props.theme} />
  </div>
);

const App: React.FC = () => {
  return (
    <div className="app">
      <header className="app-header">
        <Toolbar theme="dark" />
      </header>
    </div>
  );
};

export default App;

ThemedButtontheme を渡すために Toolbar にも props を渡しています。


1. React 16.3 以降の Context を使う場合

import React from "react";
import "./App.css";

const ThemeContext = React.createContext("light"); // (1)

class ThemedButton extends React.Component {
  static contextType = ThemeContext; // (1)
  context!: React.ContextType<typeof ThemeContext>; // (2)

  render() {
    return (
      <button className={this.context}>Click me</button>
    );
  }
}

const ThemedButtonFC: React.FC<{ theme: string }> = props => (
  <button className={`app-toolbar-button ${props.theme}`}>Click me</button>
);

const Toolbar: React.FC = props => (
  <div>
    Hello, TypeScript & React. <ThemedButton />
  </div>
);

const App: React.FC = () => {
  return (
    <ThemeContext.Provider value="dark"> // (1-2)
      <div className="app">
        <header className="app-header">
          <Toolbar />
        </header>
      </div>
      <div>
        <div>
          <ThemeContext.Consumer>
            {value => <ThemedButtonFC theme={value} />} // (1-3)
          </ThemeContext.Consumer>
        </div>
      </div>
    </ThemeContext.Provider>
  );
};

export default App;

ThemedButtonFC および <ThemeContext.Consumer>...</ThemeContext.Consumer> については、Consumerを使った箇所も確認したかったのでオマケです。

新しいContextでは、React.createContext(defaultValue) を使ってContextオブジェクトを作ります。
コメントで(1) と書いたところは、通常のContextのお作法通りに書いた箇所です。
これだけで、(1-2)Providervalue(1-3)Consumer 内の関数の value は正しく型チェックされるようになります。

ポイントは (2) のところで、どうやらこれがないとコンポーネント内のthis.contextが型チェックされないようです。
この書き方はReactの型定義ファイルを参考にしました。

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts#L421-L434

 /**
  * If using the new style context, re-declare this in your class to be the
  * `React.ContextType` of your `static contextType`.
  *
  * ```ts
  * static contextType = MyContext
  * context!: React.ContextType<typeof MyContext>
  * ```
  *
  * @deprecated if used without a type annotation, or without static contextType
  * @see https://reactjs.org/docs/legacy-context.html
  */


2. React 16.3 以前の Legacy Context を使う場合

Legacy Context の場合はこのようになります。

import React from "react";
import PropTypes from "prop-types";
import "./App.css";

// (1)
type ThemeContext = {
  theme: string;
};

class ThemedButton extends React.Component {
  static contextTypes = {
    theme: PropTypes.string
  };
  context!: ThemeContext; // (1)

  render() {
    return (
      <button className={this.context.theme}>
        Click me
      </button>
    );
  }
}

const Toolbar: React.FC = props => (
  <div>
    Hello, TypeScript & React. <ThemedButton />
  </div>
);

class App extends React.Component {
  static childContextTypes = {
    theme: PropTypes.string
  };

  getChildContext(): ThemeContext { // (1)
    return { theme: "light" };
  }

  render() {
    return (
      <div className="app">
        <header className="app-header">
          <Toolbar />
        </header>
      </div>
    );
  }
}

export default App;

公式ドキュメント相当の信頼できそうな情報は見つけられなかったのですが、こちらのブログを参考にしました。

ポイントとしては、Legacy Contextの場合、コンテキストはPropTypesで記述するためランタイムでのチェックができません。
そのため、(1) にあるように同じ構造のTypeScriptの型を定義した後、親のgetChildContext()、子のthis.contextに型を付けています。

ブログの記事との違いとして、ブログでは

context: ThemeContext;

としてますが、これだと--strictPropertyInitializationオプションがONのときに以下のエラーが出ます。

Property 'context' has no initializer and is not definitely assigned in the constructor. ts(2564)


3. React Hooksの useContext() を使った場合

最後に、Hooksを使った場合。

import React, { useContext } from "react";
import "./App.css";

const ThemeContext = React.createContext("light");

const ThemedButton: React.FC = props => {
  const theme = useContext(ThemeContext);

  return <button className={theme}>Click me</button>;
};

const Toolbar: React.FC = props => (
  <div>
    Hello, TypeScript & React. <ThemedButton />
  </div>
);

const App: React.FC = () => {
  return (
    <ThemeContext.Provider value="dark">
      <div className="app">
        <header className="app-header">
          <Toolbar />
        </header>
      </div>
    </ThemeContext.Provider>
  );
};

export default App;

コンポーネントが Function Component で書けるようになるということ以外は 1 と変わりません。

Amazonの商品ページの情報をkintoneに登録するChrome拡張

を作りました。

https://chrome.google.com/webstore/detail/amazon-to-kintone/leipfhjipgnfbdjkbinlmlmfdhgcakki からインストールできます。

アイコン未登録だったりコードがぐちゃぐちゃだったりしますが、とりあえず自分が使うための要件は満たせたので一区切り。


キャプチャ

f:id:dackdive:20190520192441p:plain

商品のタイトル、商品ページのURL、画像URLをkintoneの任意のアプリにレコード登録するだけのChrome拡張です。


モチベーション

kintone の勉強がてら、読みたい/読んだ本の記録を kintone でやっています
気になった本はたいていAmazonで検索してkintoneに登録してたんですが、それを1クリックでできるようにしたかった、というのがきっかけ。

また、技術的にもこのへんを試したかったというのもあります。

  • TypeScript で Chrome 拡張を書く
  • React Hooks
  • (未) styled-components

スタイルについてはせっかくなので kintone-ui-component 使って kintone ライクなUIにしようと考えていたけど
もろもろ断念して SalesforceLightning Design System になった。


技術的なメモ

TypeScript

chrome.*** 系のAPIに対する TypeScript の型定義は @types/chrome が利用できる。
Chrome拡張書くときのボイラープレート的なものは今回でだいぶ学べた。

また、 chrome.*** APIを Promise で書けるようにしつつブラウザ間の差異を吸収した webextension-polyfill というのもある。
今回は使わなかったけど興味あるのでリファクタしたい。


Chrome 拡張 ( chrome.*** API まわり)

すごい単純なChrome拡張だったのでPopupのみでいけるかなーと思ってたんだけど、PopupからはページのDOMを触れない。
content scirptを追加してあげる必要がある。

また、content scriptとPopup間のやり取りは chrome.tabs.sendMessage() および chrome.runtime.onMessage.addListener() を使ったメッセージング機能を利用することになる。

// メッセージ送信側
chrome.tabs.sendMessage(props.tabId, { /* パラメータ */ }, response => {
  // 受信側からのレスポンスが返ってきた後の処理
});
// メッセージ受信側
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  // もろもろ処理
  sendResponse(/* レスポンス */);
});

sendRequest() は deprecated
https://developer.chrome.com/extensions/tabs#method-sendRequest


React Hooks

今回は useState, useEffect しか使ってない。
慣れるまで少し時間がかかったけど、関心ごとに処理をまとめられるようになるのでたしかに読みやすいかも、と思った。

使わなかったものだとカスタムフック は興味あるのと useContext あたりは便利だという話を聞くので、ドキュメント読みたい。


リファレンス