立って仕事をする
スタンディングデスクを使い始める。FlexiSpot EG8というのをもらった。それまでレンタル家具サービスで借りたちっぽけな机を使っていたのだがずいぶん立派になった。ついでにXiserのステッパーも買って一緒に使っている。
リモートワークになり通勤時間は無くなったけれど、生活の中でオフラインになる貴重な時間でもあった。日記を付けたり本を読んだりと活用していたので同様の時間をどう作るかなあと困っていたんだが、スタンディングデスクとステッパーで解消した。
最近は朝起きるととりあえずステッパーに乗る。ステッパーを踏みながらスマホで日記を付けたり本を読んだりする。30分もすると目が冴えてくるのでそのまま立って仕事を始めるルーティングができてきた。
よく肩こりとか腰痛がマシになったと聞くが、肩こりはあまり変わっていない。こないだたまたま知り合った鍼灸師に話聞いたら、そもそも僕は正しい立ち方が出来てないらしい。それが肩こりにつながってるよと教えてもらった。巻き肩と猫背を直さないとどうしようもないらしい。
立ったまま仕事をしていると部活動的な感覚になる(伝われ)。じっくり座ってゆっくり考えるというよりは、立って歩いて体がずっと起動している感じがあって、それに連動して頭も動く感じ。座っていると没頭できるが、立っていると自然と頭が動くという差を感じる。タスクの状況によって使い分けている。
FlexiSpot EG8は電動で高さを調節でき、その高さも記録できるので、座るとき、立つとき、ステッパーで歩くときの3シチュエーションを記録してある。ボタン1つで高さを変えられるのは便利だ。障害検知機能も優秀で、高さを変えるときになにかに突っかかると止まってくれる。
重さが十分あるので、キーボードをガシガシたたいても全く揺れることなく安定していて良い。安いニトリの机を使っていた時代があるのだが、書き物すると揺れるので壁際に寄せて揺れを抑えていたんだが、FlexiSpot EG8はそういう不便もない。ただし、40kgもあるので組み立ては命がけだった。
届いたときの写真。メゾネットで2階がリビングなので、玄関でばらして階段を持ち上げなければならず死ぬかと思った。
快適になって嬉しいが、こんなに家で仕事するような時代が来るとはなあ。
ReactでChrome Extensionを作る
- Chrome Extensionの種類
- Chrome Extensionのアーキテクチャ
- create-react-appでChrome Extensionは作れる?
- webpackでChrome Extensionを作る
- content scriptでFirestoreを叩けなかった
- backgroundとcontent script間のメッセージのやりとりはLong-lived connectionsで行う
- Reactを埋め込む
エロゲー批評空間の推薦システムを作っている。推薦アイテムをFirestoreから取ってきて推薦枠を表示するChrome Extensionになる予定。この土日にChrome Extensionだけ出来上がったので、調べたことをまとめておく。
Chrome Extensionの名前はdreamyfishにした。素晴らしき日々という作品の中で主人公がエリック・サティの夢見る魚を引いていたのを思い出したから。
GitHub - roronya/dreamyfish-fe
Chrome Extensionの種類
大きく分けて2つある。
- Brouser Action
- 開いているページに関わらず動かすExtension
- Page Action
- 開いているページに特化したExtension
Chrome Extensionのアーキテクチャ
公式のOverviewのアーキテクチャの箇所が分かりやすかった。
ExtensionはJavascriptが動く箇所が3つあり、それぞれで「この場所はこういうことができますよ」というのが決まっている。そして、これらの3つのJSはMessageを送り合うことでデータを受け渡しできる。
それぞれの特性は以下のようになっている。
- popup
- URLの右側にアイコンが表示されていてクリックすると動くHTMLとそこで動くJS
- Extension独自のUIを作るときに使う
- content script
- background
公式の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なども差分が分かってついていけるようになった。
- 作者: 山田祥寛
- 出版社/メーカー: WINGSプロジェクト
- 発売日: 2018/04/27
- メディア: Kindle版
- この商品を含むブログを見る
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系を使う場合以下は参考にならない。
背景
k8sのyamlを書くのに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が一番上に来た。
secretGenerator.commands
に書かれた処理は5秒でタイムアウトするらしい。このPRでは kustomize build
に --command-timeout
というオプションを付けることを提案している。しかし、「 kustomize build
にはオプション作りたくないんだ」と言われて却下されている。
TimeoutSeconds
オプションができる
同じ課題を抱えていることはわかったので誰かが何とかしてくれているはずである。PRの検索欄にtimeoutと入れて探すと以下のPRが見つかる。
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番を返してくれている。
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のインスタンス化のタイミングは同時のタイミングのほうが都合が良い。よってディレクトリの構成は以下のようにした。
- App.js
- ページ全体のテンプレート 且つ ルーティング設定
- index.js
公式のサンプルだと 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> )
ログイン処理はこうなる
handleAuthentication()
の処理の完了を待たず "ログイン中" がレンダリングされるhandleAuthentication()
でログイン処理が終了すると push('/') がstoreにdispatchされ、react-router-reduxのRouterReducerがページ遷移を実行する
改訂新版JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用まで
- 作者: 山田祥寛
- 出版社/メーカー: 技術評論社
- 発売日: 2016/09/30
- メディア: 大型本
- この商品を含むブログを見る
React入門 React・Reduxの導入からサーバサイドレンダリングによるUXの向上まで (NEXT ONE)
- 作者: 穴井宏幸,石井直矢,柴田和祈,三宮肇
- 出版社/メーカー: 翔泳社
- 発売日: 2018/02/19
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
「Auth0」で作る!認証付きシングルページアプリケーション (技術書典シリーズ(NextPublishing))
- 作者: 土屋貴裕
- 出版社/メーカー: インプレスR&D
- 発売日: 2018/08/31
- メディア: Kindle版
- この商品を含むブログを見る
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> }
then
や catch
の中でごちゃごちゃと書くしか無いのかなあ…。
thenの中でreturnすれば外に出せるのでは説
then
で return
したら外に出せるのでは?と思ったけどダメだった。
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は書かないとダメっぽい
改訂新版JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用まで
- 作者: 山田祥寛
- 出版社/メーカー: 技術評論社
- 発売日: 2016/09/30
- メディア: 大型本
- この商品を含むブログを見る