an odd fellow

仕事のメモ

ReactでChrome Extensionを作る

エロゲー批評空間の推薦システムを作っている。推薦アイテムをFirestoreから取ってきて推薦枠を表示するChrome Extensionになる予定。この土日にChrome Extensionだけ出来上がったので、調べたことをまとめておく。

Chrome Extensionの名前はdreamyfishにした。素晴らしき日々という作品の中で主人公がエリック・サティの夢見る魚を引いていたのを思い出したから。

GitHub - roronya/dreamyfish-fe

Chrome Extensionの種類

大きく分けて2つある。

  • Brouser Action
    • 開いているページに関わらず動かすExtension
  • Page Action
    • 開いているページに特化したExtension

Chrome Extensionのアーキテクチャ

公式のOverviewのアーキテクチャの箇所が分かりやすかった。

ref: Overview - Google Chrome

ExtensionはJavascriptが動く箇所が3つあり、それぞれで「この場所はこういうことができますよ」というのが決まっている。そして、これらの3つのJSはMessageを送り合うことでデータを受け渡しできる。

それぞれの特性は以下のようになっている。

  • popup
    • URLの右側にアイコンが表示されていてクリックすると動くHTMLとそこで動くJS
    • Extension独自のUIを作るときに使う
  • content script
    • ブラウザで表示しているページに埋め込まれて動くJS
    • 今表示しているページが持っているContextを引き継いで動く
      • どういうことかというと、例えばcontent scriptでAjax通信をすると、そのページのドメインがOriginとして使われる(サーバーでallow originしてなければ通信できないということ)
      • どこかのAPIを叩いてそのページに埋め込む、みたいなExtensionを作るときはbackground
      • chrome APIが一部たたけないなど制約が多い
  • background
    • UIを持たずに裏側で動かすJS
    • content scriptのように、ページのContextは持たないので自由な処理ができる
    • chrome APIがフルで叩ける
    • どこかのAPIを叩いてcontent scriptやpopupにデータを渡すのに使うことが多いかな?と思った

公式のOverviewにあるように、backgroundでAPIを叩き、Messageでデータは受け渡し、content scriptやpopupはUIに専念する、という責務分割になっているようだ。

create-react-appでChrome Extensionは作れる?

popupであれば簡単に作れる。デフォルトで生成されるmanifest.jsonを書き換えればすぐにできるようだ。

ref: create-react-app で TypeScript × React での Chrome Extension 開発を始める - Qiita

ref: create-react-appはなぜmanifest.jsonを作るの? - an odd fellow

しかし、content scriptやbackgroundで動かしたい場合はejectしてwebpackの設定を弄る必要があったので諦めた。create-react-appは npm run build すると index.html をエントリーポイントに build ディレクトリ配下にファイルを吐くので、popupの場合は都合が良い。しかし、content scriptやbackgroundはHTMLは持たない生のJSとして出力される必要があり、そういう設定はwebpackの設定を直接いじらなければならない。

webpackでChrome Extensionを作る

create-react-appに頼りきっていたのでwebpackから逃げていたが、良い機会だったので本腰を入れて勉強した。詳細は省く。結局webpackを理解せずコピペで進もうとしたら遠回りになることが多かった。

Rails本やJSの本など多数執筆している山田祥寛さんの速習シリーズのwebpackがかなり為になった。2018年の販売だが、十分新しい内容で、これで勉強すれば基礎がわかるのでwebpack4やbabel v7.4なども差分が分かってついていけるようになった。

速習webpack 速習シリーズ

速習webpack 速習シリーズ

webpackのconfigはこのようになった。content scriptとbackgroundを作るのでentryをふたつ用意し、loaderでJSにする必要のないmanifest.jsonをCopyPluginで直接buildディレクトリにコピーしている。

dreamyfish-fe/webpack.config.js at master · roronya/dreamyfish-fe · GitHub

content scriptでFirestoreを叩けなかった

content scriptでFirestoreを叩いたら以下のようなエラーメッセージで動かなかった。

Could not reach Cloud Firestore backend. Connection failed 1 times. Most recent error: FirebaseError: [code=unavailable]: The operation could not be completed This typically indicates that your device does not have a healthy Internet connection at the moment. The client will operate in offline mode until it is able to successfully connect to the backend.

詳細な原因は分からなかった。content scriptはページのContextを引き継ぐので、Content Security Policy周りで引っかかったのだろうか。以下のようなページもあり、Content Security Policyの設定も試したがだめだった。

ref: Google Developers Japan: Chrome 拡張機能で Firebase を使う方法

データ取得はbackgroundでやれということなのだな、と理解した。

backgroundとcontent script間のメッセージのやりとりはLong-lived connectionsで行う

content scriptからbackgroundにメッセージを送り、backgroundはメッセージに即したデータをFirestoreから取得して、content scriptに返答する、という流れをやろうとした。

ref: Message Passing#Simple one-time requests - Google Chrome

この公式にあるSimple one-time requestsで行ったところ、Firestoreのデータ取得を待たずにチャンネルが閉じてしまい通信できなかった。メッセージを返答するまで長時間要する場合はLong-lived connectionsを使えとあったので、使ったらうまくいった。

ref: Message Passing#Long-lived connections - Google Chrome

Reactを埋め込む

埋め込みたい箇所にdivをぶちこんでReactDOM.renderするだけでおk。こんなかんじ。

dreamyfish-fe/content.js at master · roronya/dreamyfish-fe · GitHub

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)

JavascriptのPromiseとasync/awaitについて調べる

背景

ReactでSPAを作っている。APIからデータを取得するときredux thunkとaxiosを使っているが、このときの処理がexampleを見て見よう見まねでやっていてよくわかっていない。

例えば投稿を取得するソースコードはこんな感じになっているが、return async とか axios.get のあとの then とかがわかっていない。

export const fetchPosts = () => {
  return async (dispatch, getState) => {
    const posts = getState().posts
    dispatch(actions.startRequest(posts))
    axios
      .get("http://example.com/posts")
      .then(response => {
        dispatch(
          actions.receiveData(
            posts,
            null,
            normalize(response.data, [schemas.post])
          )
        )
      })
      .catch(error => {
        dispatch(actions.receiveData(posts, error))
      })
      .finally(() => {
        dispatch(actions.finishRequest(posts))
      })
  }
}

これを理解するためにいくつかドキュメントにあたったり実験した結果をまとめる。

そもそも同期/非同期とは

この記事がわかりやすかった。

JavaScriptの同期、非同期、コールバック、プロミス辺りを整理してみる

console.log(1);
setTimeout(function(){console.log(2)}, 0);
console.log(3);

待ち時間がゼロなので、console.log(1)、console.log(2)、console.log(3)の順に実行されると勘違いしやすいです。 しかしあくまでlog(1)、setTimeout(〜略〜)、log(3)が順に、つまり同期的にキューに登録された後、setTimeout(〜略〜)が実行されたタイミングで、つまり非同期でlog(2)がキューに登録される

だから、出力は以下のようになる。

1
3
2

Promiseやasync/awaitを使うと

1
2
3

のように書いた順番通りに実行できるような仕組みのようだ。これを同期的と呼んでいる。

async/awaitの前にPromiseを理解するべきっぽい

適当にググっていると以下の記事を見つけた。そこにはこうある。

Promiseの使い方、それに代わるasync/awaitの使い方

async/awaitを理解するには、Promiseも知る必要がある。

なのでまずPromiseから調べる。

Promiseの基本的な使い方

コマンドライン引数がaかどうかでresolve、rejectを切り替える。

promise-basic.js

const promise = new Promise((resolve, reject) => {
  if (process.argv[2] === 'a') {
    resolve("GOOD")
  } else {
    reject("BAD")
  }
})

promise.then(value=>console.log(value)).catch(value=>console.log(value))

実行結果

$ node promise-basic.js a
GOOD
$ node promise-basic.js b
BAD

Promise.resolveでPromiseのインスタンスを作れる。

promise-basic2.js

promise = Promise.resolve("GOOD")

promise.then(value=>console.log(value)).catch(value=>console.log(value))

実行結果

$ node promise-basic2.js
GOOD

resolveとかrejectの結果を変数に束縛できるか?

こんなような感じで書いて変数hogeに"GOOD"とか"BAD"とか入って欲しい。

promise-bind.js

const promise = new Promise((resolve, reject) => {
  if (process.argv[2] === 'a') {
    resolve("GOOD")
  } else {
    reject("BAD")
  }
})

const hoge = promise.then(value=>value).catch(value=>value)
console.log(hoge)

実行結果

$ node promise-bind.js a
Promise { <pending> }

thencatch の中でごちゃごちゃと書くしか無いのかなあ…。

thenの中でreturnすれば外に出せるのでは説

thenreturn したら外に出せるのでは?と思ったけどダメだった。 promise-bind2.js

const promise = new Promise((resolve, reject) => {
  if (process.argv[2] === 'a') {
    resolve("GOOD")
  } else {
    reject("BAD")
  }
})

const hoge = (
  () => {promise.then(value=>return value).catch(value=> return value)}
  )()
console.log(hoge)

実行結果

$ node promise-bind2.js a   
/Users/roronya/src/github.com/roronya/async-await/promise-bind2.js:10
  () => {promise.then(value=>return value).catch(value=> return value)}
                             ^^^^^^

SyntaxError: Unexpected token return
    at new Script (vm.js:51:7)
    at createScript (vm.js:136:10)
    at Object.runInThisContext (vm.js:197:10)
    at Module._compile (internal/modules/cjs/loader.js:618:28)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:665:10)
    at Module.load (internal/modules/cjs/loader.js:566:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:506:12)
    at Function.Module._load (internal/modules/cjs/loader.js:498:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:695:10)
    at startup (internal/bootstrap/node.js:201:19)

resolveとかrejectとか書かなかったらどうなるのか?

何も起こらなかった。

promise-not-resolve.js

const promise = new Promise((resolve, reject) => "GOOD")

promise.then(value=>console.log(value)).catch(value=>console.log(value))

実行結果

$ node promise-not-resolve.js
# 何も出ない

Promiseを使った感想

  • Promiseの内部で使った値を呼び出し側に出せないのが使いづらい
    • もし値を使いたいなら関数を呼んでその引数にするしか無さそう
  • Promise.resolveは使い所無い気がする
  • Promiseが返ってくる関数は、resolveやrejectに何が渡されるのか実装によって調べる必要があるっぽい

async/awaitの使い方

asyncだけで使う

関数にasyncと付けると Promise.resolve(関数の返り値) と同じ結果になるっぽい。 async-basic.js

const f = async () => "GOOD"

console.log(f)
f().then(value => console.log(value))

実行結果

$ node async-basic.js
[AsyncFunction: f]
GOOD

asyncの中でだけawaitが使える

await-basic.js

const f = async () => "GOOD"
const g = async () => {
  const y = await f()
  console.log('inner', y)
  return y
}

g().then(value => console.log('outer', value))

実行結果

$ node await-basic.js
inner GOOD
outer GOOD

classメソッドでもasync使える?

使える

async-class.js

class C {
  async f() {
    return "GOOD"
  }
}

const c = new C()
console.log(c.f)
c.f().then(v => console.log(v))

実行結果

[AsyncFunction: f]
GOOD

async/await使った感想

  • 受け取る側でPromiseを処理すればasyncは気軽に付けてよい
  • await付けるとPromiseの中の値を外に持ってこれる
    • が、結局asyncによってPromiseに包まれるから、もともとの呼び出し側はPromise.then.catchは書かないとダメっぽい

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"
    ]
  },
}