an odd fellow

仕事のメモ

create-react-appはなぜmanifest.jsonを作るの?

スマートデバイスで「ホームに追加」を出すためだった。

manifest.json拡張機能作るときに書くけど、なんでSPA作るcreate-react-appがmanifest.jsonを作るのか気になった。

reactjs - What is public/manifest.json file in create-react-app? - Stack Overflow

このSOのアンサーで参照されているウェブアプリマニフェスト | MDNにこうある。

ウェブアプリマニフェストは、 JSON 形式のファイルでアプリケーションについての情報 (名前、作者、アイコン、説明など) を提供するものです。マニフェストは端末のホーム画面にインストールされたウェブサイトの詳細を通知し、ユーザーによりすばやいアクセスと、より豊かな使い勝手を提供します

create-react-appはService Workerもサポートしているから、その一環でmanifest.jsonが作られるのだな、と解釈した。

kustomizeのTimeoutについて

注意: kustomize1系の話。2系を使う場合以下は参考にならない。

背景

k8syamlを書くのにkustomizeを使っている。kustomizeの secretGenerator.commands で時間のかかる処理(GCSに置いてある秘匿情報を gsutil cat で取得するなど)をしたところ以下のエラーメッセージを吐いて終了してしまった。

Error: NewResMapFromSecretArgs: NewResMapFromSecretArgs: commands map[password:gsutil cat gs://myproject/mysql/password username:gsutil cat gs://myproject/mysql/username]: signal: killed

secretGenerator.commands に書かれた処理はデフォルトでは5秒で終了する

わからないことはググる。「kustomize "timeout"」でググった。timeoutをダブルクオーテーションでくくらないと有用な情報に当たらなかった。timeoutに完全一致させて検索すると以下のPRが一番上に来た。

Hard-wired timeout of 5s for secretGenerator commands · Issue #252 · kubernetes-sigs/kustomize · GitHub

secretGenerator.commands に書かれた処理は5秒でタイムアウトするらしい。このPRでは kustomize build--command-timeout というオプションを付けることを提案している。しかし、「 kustomize build にはオプション作りたくないんだ」と言われて却下されている。

TimeoutSeconds オプションができる

同じ課題を抱えていることはわかったので誰かが何とかしてくれているはずである。PRの検索欄にtimeoutと入れて探すと以下のPRが見つかる。

Allow setting shell and timeout in generatorOptions by Liujingfang1 · Pull Request #497 · kubernetes-sigs/kustomize · GitHub

TimeoutSeconds オプションができている。

結論

TimeoutSeconds オプションを付けるだけ。

secretGenerator:
  - name: db-secrets
    commands:
      username: "gsutil cat gs://myproject/mysql/username"
      password: "gsutil cat gs://myproject/mysql/password"
    TimeoutSeconds: 30

NginxでCORSの設定を入れるときはOPTIONSの処理を忘れない

server {
    listen 80;
...
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD";
    add_header Access-Control-Allow-Headers *;
    add_header Access-Control-Expose-Headers x-total-count;
    if ($request_method = "OPTIONS") {
        return 204;
    }
...
}

OPTIONSにNGINXが返答しない場合、裏側のアプリケーションサーバーまで通信が行くが、そこで処理できない場合適当なエラーが帰る。

おまけ: Railsの場合

rack-corsがOPTIONSリクエストに対して200番を返してくれている。

Rack CORSコードリーディング

Reduxを使ってるときのAuth0の使い方

背景

ログイン処理にAuth0を使っている。Auth0はサンプルが豊富でReactを使ったサンプルも在るが、そのサンプルではReduxは使わずPure React構成のため、Reduxを使っている場合の使い方がイマイチ分からなかったので、考えて実装した結果をまとめておく。

どこで悩むか

基本的には Auth0 React SDK Quickstarts: Login を見てまず実装した上でRedux化していくのが良い。その上で悩みポイントは2つ。

  • Authオブジェクトで直接historyAPIを叩いているが、react-router-reduxを使っている場合それはできない
  • Authオブジェクトのインスタンス化のタイミング

AuthオブジェクトからhistoryAPIを叩く方法

react-router-reduxを使う場合historyAPIもActionを発行しReducerに処理させなければならない。そうなると store.dispatch をAuthオブジェクトは使わざるを得ない。

つまり公式のサンプルでは hitsoty.replace('/home') とやっているところを、 dispatch(push('/home')) とすれば良い。コードは以下のようになる。

Auth.js

import auth0 from 'auth0-js'
import { push } from 'react-router-redux'

export default class Auth {
  auth0 = new auth0.WebAuth({
    domain: AUTH0_DOMAIN
    clientID: AUTH0_CLIENT_ID,
    redirectUri: AUTH0_REDIRECT_URI
    responseType: 'token id_token',
    scope: 'openid'
  })

  constructor (dispatch) {
    this.login = this.login.bind(this)
    this.handleAuthentication = this.handleAuthentication.bind(this)
    this.isAuthenticated = this.isAuthenticated.bind(this)
    this.logout = this.logout.bind(this)
    this.dispatch = dispatch  // constructorの引数でdispatchをもらってthisに束縛する
  }

  login () {
    this.auth0.authorize()
  }

  handleAuthentication () {
    this.auth0.parseHash((err, authResult) => {
      if (authResult && authResult.accessToken && authResult.idToken) {
        this.setSession(authResult)
        this.dispatch(push('/home'))  // ここが公式のサンプルでは history.replace('/home') になってる
      } else if (err) {
        console.log(err)
        this.dispatch(push('/hoe'))
      }
    })
  }

  setSession (authResult) {
    let expiresAt = JSON.stringify(
      authResult.expiresIn * 1000 + new Date().getTime()
    )
    localStorage.setItem('access_token', authResult.accessToken)
    localStorage.setItem('id_token', authResult.idToken)
    localStorage.setItem('expires_at', expiresAt)
    this.dispatch(push('/home'))
  }

  isAuthenticated () {
    let expiresAt = JSON.parse(localStorage.getItem('expires_at'))
    return new Date().getTime() < expiresAt
  }

  logout () {
    // Clear Access Token and ID Token from local storage
    localStorage.removeItem('access_token')
    localStorage.removeItem('id_token')
    localStorage.removeItem('expires_at')
    this.dispatch(push('/home'))
  }
}

ディレクトリ構成

Authにstore.dispatchを渡さなければならないため、Authオブジェクトのインスタンス化とStoreのインスタンス化のタイミングは同時のタイミングのほうが都合が良い。よってディレクトリの構成は以下のようにした。

公式のサンプルだと routes.js というのがあって、ルーティングの設定を切り出しているが、 App.js で全てやってしまった。コードは以下のようになる。

index.js

import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'react-router-redux'
import createBrowserHistory from 'history/createBrowserHistory'
import App from './App'
import Auth from './auth'
import { createStore } from './store'

const history = createBrowserHistory()
const store = createStore(history)
const auth = new Auth(store.dispatch) // authのdispatchを渡す

ReactDOM.render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <App auth={auth} />
    </ConnectedRouter>
  </Provider>,
  document.getElementById('root')
)

App.js

handleAuthenticationの引数にauthを追加しているのに注意。公式のサンプルだとauthがスコープにいたが、この構成だと明示的に渡してやる必要がある。

import React from 'react'
import { Route, Switch, Redirect } from 'react-router-dom'

const handleAuthentication = (nextState, replace, auth) => {
  if (/access_token|id_token|error/.test(nextState.location.hash)) {
    auth.handleAuthentication()
  }
}

export default ({ auth }) => (
  <Switch>
    <Route exact path='/login' render={() => <LoginPage auth={auth} />} />
    <Route
      exact
      path='/callback'
      render={props => {
        handleAuthentication(props, null, auth)
        return <p>ログイン中</p>
      }}
    />
  </Switch>
)

ログイン処理はこうなる

  1. handleAuthentication() の処理の完了を待たず "ログイン中" がレンダリングされる
  2. handleAuthentication() でログイン処理が終了すると push('/') がstoreにdispatchされ、react-router-reduxのRouterReducerがページ遷移を実行する

React入門 React・Reduxの導入からサーバサイドレンダリングによるUXの向上まで (NEXT ONE)

React入門 React・Reduxの導入からサーバサイドレンダリングによるUXの向上まで (NEXT ONE)

Prettierとstandardを併用する

背景

今のプロダクトにlinterを入れたかった。今までは雑にググってヒットしたPrettierのみ使っていた。

Linterも入れたほうがいいらしい

PrettierはFormatterであってLinterではない。だからPrettierはコードの「この部分が良くないよ!」とは教えてくれず問答無用で整形だけしてくれる。「この部分が良くないよ!」を教えてもらうにはLinterが必要。JavascriptならESLintがデファクトスタンダード

eslint --fix で良くない?

おれもそう思ったが、2つの理由でPrettierも一緒に使ってもよいかなと思う。

まず、WebStormがPrettierに公式で対応していて使うのが簡単だから。Prettierがインストールされていれば何の設定も無しに Alt-Shift-Cmd-P を押せば整形してくれる。これは以前記事にも書いた。

prettierがいい感じ - an odd fellow

ふたつめが、横長過ぎるコードをESLintの --fix では整形できないが、Prettierはこれができる。なぜかというと、Prettierは書かれているコードをパースしてASTにして整形するから、らしい(What is Prettier? · Prettier)。だから横長過ぎるコードも妥当な位置で改行したりインデントを入れることができるようだ。

というわけでESLintとPrettierを併用することにした。

standard

ESLintには、Javascriptのコーディング規約に則っているかチェックしてもらいたい。Javascriptのコーディング規約は世の中にいくつかあるが、Javascript Standard Styleに従うことにした。

JavaScript Standard Style

Standard Style用にESLintの設定を書くのは骨が折れれるが、Standard StyleはGitHub - standard/standard: 🌟 JavaScript Style Guide, with linter & automatic code fixerというツールを提供している。これはESLintの設定をまるっとやって standard コマンドにラップしている。だからインストールして $ standard --fix と叩けばStandard Styleに則ったコードに整形される。素敵。

prettier-standardでPrettierとstandardを併用する

standardを使うと、今度困るのがPrettierで、PrettierのコードフォーマットもJavascript Standard Styleに従ってもらう必要がある。しかし、Prettierの設定でStandard Styleになるよう書くのもやはり骨が折れる。そこで調べているとprettier-standardというのがあった。

GitHub - sheerun/prettier-standard: (✿◠‿◠) Prettier and standard brought together!

これは $ prettier-standard ''/path/to/file' でstandardとPrettierを両方適用してくれる。素敵!!!

WebStormからprettier-standard呼べるの?

残念ながらWebStormから呼べるのはPrettier単体のみ。ただし、あとで説明するようにgit commit時にprettier-standardが走るように設定しておけば、書いている最中はどんな風に整形しても問題はない。だからPrettier単体で呼び出して使っています。ただし、セミコロンの設定だけは気になったから .prettierrc を作って ; は補完されないようにしている。

{
  "semi": false
}

git commit時にprettier-standardを走らせるようにする

commit hookはcloneしてから各自で設定する必要があり微妙に使いづらいが、huskyというライブラリを使うと、インストール時にcommit hookの設定を書き込んでくれる。git commit時に具体的にどのようなコマンドを実行するかは package.json に書けるようになるという素晴らしいライブラリ。これとlint-stagedというライブラリを併用すると、git commit時にコードフォーマットを当ててからgit commitするということが可能になる。この記事が詳しい。

コミット前に Lint を強制するなら lint-staged が便利

結局どうなったの

package.jsonに以下のように書き足すことでgit commit時にprettier-standardが走って全JSファイルをフォーマットしてくれる。

{
  "scripts": {
    "format": "prettier-standard 'src/**/*.js'"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.js": [
      "yarn format",
      "git add"
    ]
  },
}

関数の引数にスプレッド構文を使った場合

背景

React書いていると以下のような...propsのような記述がたまに見られるが、どういう振る舞いになるのか少し混乱したから整理する。

const DeleteButton = ({className, ...props}) => (
  <span className={[style.root, className].join(' ')} {...props}>
    <TrashCanIcon />
    <Baloon>削除する</Baloon>
  </span>
);

スプレッド構文

ドットを3つつなげたこれはスプレッド構文と呼ばれている。Iterableなオブジェクトの中身を展開してくれるようだ。

スプレッド構文 - JavaScript | MDN

これらは以下の3つの場面で使うことができる。 1. 関数呼び出し 2. Arrayリテラル 3. Objectリテラル

2,3は想像が付くんだが関数呼び出しのときの振る舞いがうまく想像できない。

実験

とりあえず使ってみる。

const hage = ({...props}) => {console.log(props)};
hage({a:1});
// => {a: 1}

const hage=(props) => {console.log(props)};と変わらないっぽい? propsの中身を一度展開し、再度Objectにしたものをpropsという名前で束縛しているのか? そもそも({a, b}) => {}って引数に渡されたObjectからabのプロパティだけを抽出する構文だよね?なんかおかしくね?

const hige = ({...props}) => {console.log(a)};
hige({a: 1});
// => ReferenceError: a is not defined

これでa1が束縛されるんじゃないかと思ったんだけどな…。

propsの中身を一度展開し、再度Objectにしたものをpropsという名前で束縛している?

これが正解っぽい

const huge = ({a, ...props}) => {console.log(a, props)};
huge({a: 1, b: 2, c: 3});
/// => (1, {b: 2, c: 3})

なるほど?...を付けたら問答無用で残りは引き渡ってくれるのかー。

結論

というわけで最初の例はなんでもごちゃごちゃに渡されてくるpropsからclassNameだけをチュ出し、残りの要素は再度propsに束縛するという感じか。納得はしてないが振る舞いはわかった。

再掲

const DeleteButton = ({className, ...props}) => (
  <span className={[style.root, className].join(' ')} {...props}>
    <TrashCanIcon />
    <Baloon>削除する</Baloon>
  </span>
);

storybookに階層構造を追加する

背景

デフォルトでは1階層しかパスが切れない。

例えばAtomsのButton、MoleculesのFormがあったとして、理想的には以下のように整理したい。

  • Atoms
    • Button
      • with text
      • with some emoji
  • Molecules
    • Form
      • ContactForm
      • MailAddressForm

しかしデフォルトでは以下のようにするしかない。

  • Button
    • with text
    • with some emoji
  • Form
    • ContactForm
    • MailAddressForm

@storybook/addon-chaptersを使うと実現できるらしい。おれは以下の本を読んでこのアドオンの存在を知った。

Atomic Design ~堅牢で使いやすいUIを効率良く設計する | 五藤 佑典 | コンピュータ・IT | Kindleストア | Amazon

公式のaddon galleryでも紹介されている。

https://storybook.js.org/addons/addon-gallery/#chapters

階層構造と読んでいたがChapterと言うようだ。

@storybook/addon-chapters

install

npmを見る。

@storybook/addon-chapters - npm

storiesOf('Atoms', module)
  .addChapter('Button', chapter=>chapter
    .add('with text', () => <Button>with text</Button>)
    .add('with som emoji', () => <Button>with some emoji</Button>)
  );
storiesOf('Molecules', module)
  .addChapter('Form', chapter=>chapter
    .add(
    ...

というような感じで階層構造を追加出来た。

storybook-directory-chapters

components以下のdirectory構造をそのままstorybookにできるnpmパッケージもあったので試してみたが動かないようだ。 storybook-directory-chapters - npm

設定で module.exports を使っているがimport文も使っておりEJSとCJSの仕様がごちゃまぜになってる。module.exportsexports defaultに書き換えたが今度はcontext(...) is not a functionというエラーが出てしまい解消できなかった。