Electron、メモリリークの対策?
Electronのアプリをレンダラ側はVueで書いています。
ログイン画面→トップ画面みたいな2画面構成。
ログイン、ログアウトを繰り返すとやたらとメモリリークしていました。 何かが破棄しきれていないんだろうけど、スナップショットから見つけるのも結構苦労しそうでした。
VUE_DEV_TOOLだったり、global:appみたいなのだったり・・・。
画面をリロード
画面をリロードしてやれば、メモリリークは一時しのぎでもなんとかなりそうです。
{ remote } from 'electron' router from '@/router' export default { // ... handleClickLogout() { // Vuexストアのリセットとか... remote.getCurrentWindow().reload() router.push('/login') } }
こんな感じにしておけば、リロードされてメモリは開放されます。
ひとつ問題が。
これだとタイミング次第で、元のページがリロード→ログイン画面にいく場合と、ログイン画面にいく→リロード→ログイン画面が再描画。
みたいなことになりました。
前者はそこまで違和感はないですが、後者はユーザ的には良くない動きでしょう。
気になる人はめっちゃ気になるはず。
画面遷移の違和感をなくす
この違和感を取り去りましょう。
空divだけのコンポーネントを間に挟んでみます。
<template> <div></div> </template> <script> import router from '@/router' export default { name: 'Empty', mounted() { setTimeout(() => router.push('/login'), 1000) } } </script>
handleClickLogout() { // ... remote.getCurrentWindow().reload() router.push('/empty') }
routerに/empty
は追加したとして、こんな感じで。
これだと、一旦空のページを挟むので、自然と遷移しているように見えるハズ。
無理矢理ではあるけど、こういうのもテクニック(?)
リークの原因はちゃんと調べないと、そのうちやばい手に負えない感じになってしまいそう・・・。
非同期を同期的に
今更ながら非同期でハマったのでメモ。
非同期処理を同期的に書くなら、async/await
で良いのですが、
これがディレクトリの中身を読むやつみたいな場合...
const entries = [] if (entry.isDirectory) { const reader = entry.createReader() reader.readEntries(entry => { entries.push(entry) }) } // entriesを使った処理
このままだと、readEntriesが非同期なので、後半の処理は思い通りにはいきません。
const _entries = new Promise(resolve => reader.readEntries(entry => resolve(entry))) entries.push(..._entries)
async/await
とPromise
を上手く組み合わせると良いです。
それぞれ単体で使うことは知ってても、組み合わせはハマって初めて知る気がします・・・。
追記
↑のコードだと、ディレクトリ下に100件以上あると全て取得できない。
ドキュメントにも書いてあった。
Note that to read all files in a directory, readEntries needs to be called repeatedly until it returns an empty array. In Chromium-based browsers, the following example will only return a max of 100 entries.
if (entry.isDirectory) { const reader = entry.createReader() const readDir = async _entries => { await new Promise(resolve => { reader.readEntries(async entry => { if (entry.length !== 0) { _entries.push(...entry) await readDir(_entries) } return resolve(_entries) }) }) return Promise.resolve(_entries) } entries.push(...(await readDir([]))) } else { entries.push(entry) }
こんな感じでどうですかね。。。
もっとスマートなやり方ありますか・・・。
さらに加えると、これ、いきなりfor (let item of e.dataTransfer.items)
すると、
複数ドラッグしてるとき、最初以外が消えます。
なので、最初にitem.webkitGetAsEntry()
するなりして、配列に落としておいた方が無難かも。
Vue Programmaticで出したモーダルのprops変更
最近コンポーネントライブラリにBuefyをちょいちょい触っている中で、
モーダルでちょっとハマったのでメモ。
Programmaticにモーダルを出す
Buefyのドキュメント読むと分かりますが、
vm.$modal.open({ parent: true, component: Component, props: { // props }, events: { // emit event } })
要はこんな感じで書けば、プログラムからモーダルが呼び出せます。
よくあるやつですね。
だいたいこの手のやつは
<b-modal :visible.sync="dialogVisible"> </b-modal>
みたいな感じにして、stateのtrue/falseで出したり消したりができます。
自分はこのテンプレートに書くやり方があまり好きではなくて、どちらかというとvim.$modal.open
とかで呼びたい派。
まぁ、全てが全てではないです。
propsが更新されない
ここで問題が発生。
モーダルの中で何かを切り替えると、API叩き直して一覧更新するようなUIのとき、
そのデータをpropsで渡していた場合に更新されずハマりました。
vm.$modal.open({ props: { categoryList: this.categoryList, itemList: this.itemList }, events: { 'change-category': categoryId => { // categoryIdを元にitemListを更新するような処理 } } })
中でchangeがemitされると、itemListが更新されるので、モーダルに渡してるitemListも更新されるかと思いきや・・・。
うんともすんとも言いません。
解決
issueに同じようなことが書いてあって、そこの通りなのですが。
const $modal = vm.$modal.open({ props: { categoryList: this.categoryList, itemList: this.itemList }, events: { 'change-category': categoryId => // categoryIdを元にitemListを更新するような処理 // propsを時前で更新 $modal.props.itemList = this.itemList $modal.$forceUpdate() } } })
自前でprops入れて、モーダルのインスタンスを$forceUpdateでいけました。
templateに書いておくのと比べると、programmaticにやると、vm.$elをappendChildなりしてるからですかね・・・。
ちょっとつらい感じですけど、一応これでいけました。。。
Vueで自前のUtil関数をテンプレートで使う
Vueやってると出てくる、Util関数をテンプレートで呼びたい問題。
何が問題かというと・・・順番に見ていきましょう。
問題
まずはこんな関数を定義。
日付表記をYYYY/MM/DDにするだけの関数です。
import dayjs from 'dayjs' export function formatDate(date) { return dayjs(date).format('YYYY/MM/DD') }
さて、これを他のファイルで使いたい。。。
Vueのスクリプト部分だと単純にimportして使えば良いんですが、テンプレートで使いたいとなると、
一旦メソッド作るしかなくて、「あれ、Utilの意味・・・」となりがちです。
<table> <thead> <th>名前</th> <th>作成日</th> </thead> <tbody> <tr v-for="data in dataList"> <td>{{ data.name }}</td> <td>{{ formatDate(data.insert_date) }}</td> </tr> </dbody> </teble>
import { formatDate } from 'util' export default { name: 'Foo', methods: { formatDate(date) { return formatDate(date) } } }
意味ないやんこれ。。。
解決
UtilをプラグインとしてVueに登録してやりましょう。
import dayjs from 'dayjs' export function formatDate(date) { return dayjs(date).format('YYYY/MM/DD') } export default { formatDate }
import Util from 'util' const myUtil = { install(Vue, options) { Vue.prototype.$myUtil = Util } } Vue.use(myUtil) new Vue({ // ... })
これで、this.$myUtil.dateFormat
で使えます。
さっきのコードはこれだけで良く、script部分からはimportもわざわざmethod作る必要もないです。
<table> <thead> <th>名前</th> <th>作成日</th> </thead> <tbody> <tr v-for="data in dataList"> <td>{{ data.name }}</td> <td>{{ $myUtil.formatDate(data.insert_date) }}</td> </tr> </dbody> </teble>
もっと良い解決方法あればいいけど・・・。
prototypeにぶち込むので、できるだけ被らない名前にしないとなーとぐらいは思いました。
Vueで動的にコンポーネントを追加する
ちょっとしたダイアログを出したいとか、ポップアップみたいなの出したいとか・・・。
そういうことありませんか?
コンポーネントライブラリ使ってれば、大体網羅できますが、要件に合わないとか、思ってるのとは違うなーみたいな状況とか。
方法
Vue.component
でコンポーネント作って、その場で$mount
までしてあげます。
マウントしたやつをappendChild
なりで入れたいとこに追加。
import PopupComponent from '@/components/popup' export default { handleClick(e) { const Popup = Vue.component('popup', PopupComponent) const popup = new Popup().$mount() document.body.appendChild(popup.$el) } }
props渡したいときは、new Popup({ propData: { } })
で渡せます。
破棄もしっかりと
上で追加はできますが、閉じたときとかに消す必要がありますね。
例えば、画面上のどこかしらクリックされたら消す場合、上でいうとPopup側をこんな感じにしてやるといけます。
export default { created() { document.addEventListener('click', this.handleClickRemove) }, beforeDestroy() { document.removeEventListener('click', this.handleClickRemove) }, methods: { handleClickRemove() { this.$destroy() this.$el.parentNode.removeChild(this.$el) } } }
created
でイベントつけてあげて、beforeDestroy
で消してあげましょう。
あとはthis.$destroy()
で自らをdestroyして、追加されたものもremoveChild
で消してしまうと良いです。
ng-kyoto Angular Meetup #9に行ってきた
ng-kyotoに行ってきたので簡単に感想書いておきます。
先週の金曜日の話だけど・・・。
Canvasでスクロールを扱う際の座標計算と苦労
いきなりAngularの話ではなく、ReactHooksの話とか。
座標計算との戦い的なお話がありましたが、本当に大変そうでした。
音符のD&Dはmousemove
で頑張るとか、そういったところは参考になりました。
単純な要素の並び替えならdragStart
とかでいいんですけど、物自体を動かすとなるとそうしないといけないんですね。
AWS AppSync + ApolloではじめるGraphQL
GraphQLは去年の1月にちょっと触って会社で発表して、あのときはgraphql-php使って書いてかなり辛かったのを覚えています。
1年経って、2019年の始めにまたGraphQLを触って、Apolloを触ってみたばかりだったので、かなりタイムリーな内容でした。
「GraphQL Server書くのって結構辛い」
そのとおりで、楽したいんですよね・・・。
AWS AppSyncとgraphql generator使うと良いらしい。
graphql generatorに関して言えば、頑張って書いてたのがコマンドライン一発でできてしまう・・・。
試してみたいと思います。
以下余談。
Apolloの既存のREST APIをそのままData Sourceにできるのあるけど、あれってどうなんですかね。
イマイチGraphQLのベストな感じが分からないというか。
初心者だからこそのAngular
Angularこういう風に使ってるよ。とかAngularのここがいいよ。みたいなお話。
「Angularは未来への投資」みたいなニュアンスが良いと思いました。
雰囲気でやっている人向けのRedux再入門
わかりやすいRedux講座。
Redux自体はとてもシンプルなんだけど、なぜか躓く。
とてもわかり易く説明していただきました。
自分もReduxは意味があまり分からずやっていて、Vuexでなんとか理解して、Vuex→ngrx→Reduxとかいう順番。
結局、この辺りの状態管理ライブラリは、どれかひとつそれらしく理解したら、他も分かると思います。
Reduxで個人的に分からなかったのは、どこにデータ加工ロジック置くんだろう?といったところ。
話聞く限りMiddlewareになるのかな。
感想
Angularの話は・・・?と思いつつ、フロントまわりのいろいろな話が聞けて満足。
D&Dの話とか、結構面倒なUI求められることが多いので、参考になりました。
最近Angular触ってる!ということで参加を決めたんですけど。
いろいろあってAngular触ることがまたなくなってしまった。Angular欲高まってたところでコレなので結構堪える。
それでも、また次回も参加できるならすると思います。
Observableの結合のこと
今年はブログ書くって言うたので書いてみます。
ネタは別のとこに書いたやつではあるけど。
前の記事でも触れていますが、昨年の秋ごろからAngularを触っています。 Angularでngrx使いつつです。
ことのなりゆき
(本題だけなら飛ばしてね)
年末にふとng-japanのSlackに以下の質問。
- ngrxのeffects内でstoreのデータを使いたい場面に出くわした。
withLatestFrom(this.store$.select(fromRoot.getSelectedId))
みたいにして繋げてしまうのはありなのか?
背景的には、選択中のカテゴリに属する記事を一覧表示。
その一覧から記事を削除したとき、表示中の一覧を更新したい。
更新用のAPI呼びたいけど、選択中のカテゴリIDがほしい。
みたいな、結構あるあるな状況ではないでしょうか?
もらった回答が、
withLatestFrom
でも良いけど、このケースならActionのpayloadにID含めて渡すのはどうか?
言われてみれば、確かに。。。
いざ実装してみると、payloadを次に渡してあげないといけなくて、どうするんだろう?
foo$: this.actions$.pipe( ofType(ActionName), map(action => action.payload), mergeMap(payload => { return this.api.fetchList().pipe( map(...) catchError(...) ) }), mergeMap( /*ここにpayloadがほしい */ ...
forkJoin
を使ってまとめたらとやりたいことはできたのですが、テストが通らない。。。
なんで・・・?
理由は、結合方法にも何パターンかあって、それぞれの動きが異なるから。
この辺りちゃんと理解せずやったのでハマりました。
結合方法
Observableを結合するには、何パターンか方法があります。
- zip
- combineLatest
- forkJoin
- withLatestFrom
ざっくりこの4つ。
こんなObservableがあるとして・・・
const observable1 = Rx.Observable.interval(1000).map(x => x).take(5) const observable2 = Rx.Observable.interval(2000).map(y => y + 1).take(5)
それぞれsubscribe
すると、
observable1.subscribe(x => console.log(x)) // 0 - 1秒間隔で // 1 // 2 // 3 // 4 observable2.subscribe(y => console.log(y)) // 1 - 2秒間隔で // 2 // 3 // 4 // 5
こんな感じでobservable1
は1秒間隔で0〜4
、observable2
は1〜5
が出力されます。
これを使ってそれぞれの違いを見ていきます。
zip
まずはzip
。
Observableのそれぞれの値から順に値が計算されるObservableを返す。
訳はChromeの翻訳。
意味を汲み取りましょう。
Rx.Observable.zip(observable1, observable2).subscribe(value => console.log('zip', value)) // 出力間隔を見るためのカウンター const counter = 0 const interval = setInterval(() => { console.log(counter++) if (counter === 10) clearInterval(interval) })
コンソールの出力は以下。[x, y]
の形になっています。
0 1 zip [0, 1] 2 3 zip [1, 2] 4 5 zip [2, 3] 6 7 zip [3, 4] 8 9 zip [4, 5]
普通にsubscribe
したやつが、配列になっただけのような感じです。
間隔に注目してみると、observable1
も2秒間隔になっています。
zip
は、observable2
を待って、1ペアずつ出力されている!
combineLatest
次、combineLatest
。
Observableのそれぞれの最新値から計算されるObservableを返す。
Rx.Observable.combineLatest(observable1, observable2).subscribe(value => console.log('combineLatest', value))
これを同じように出力してみます。
0 combineLatest [1, 1] 1 combineLatest [2, 1] 2 combineLatest [3, 1] combineLatest [3, 2] 3 combineLatest [4, 2] 4 combineLatest [4, 3] 5 6 combineLatest [4, 4] 7 8 combineLatest [4, 5] 9
zip
よりも出力回数が増えています。
zip
はobservable1
とobservable2
が揃って出力されていたのに対して、combineLatest
はお構いなしに、その時の最新の値を出してきます。
ところどころ2連続で出力があるところはobservable2
の間隔。
observable1
が先に終わるので、最後の出力回数が減っているのも分かりますね。
forkJoin
次、forkJoin
。
全てのObservableが完了するのを待って、最後の値を返す。
Rx.Observable.forkJoin(observable1, observable2).subscripbe(value => console.log('forkJoin', value))
同じく出力します。
0 1 2 3 4 5 6 7 8 9 forkJoin [4, 5]
0〜9の間の出力がなくなりました。
observable1
とobservable2
の両方が完了したタイミングで出力されています。
withLatestFrom
withLatestFrom
も見ておきましょう。
ソースObservableが値を発行するたびに、その値と他の入力Observablesからの最新の値を使用して式を計算し、次にその式の出力を発行します。
これは前の3つとは少し違っています。
今回はobservable2
にobservable1
を入れて使ってみます。
observable2.withLatestFrom(observable1, (y, x) => y * x)
.subscribe(value => console.log('withLatestFrom', value))
出力は
0 1 withLatestFrom 1 -- 1 * 1 2 3 withLatestFrom 6 -- 2 * 3 4 5 withLatestFrom 12 -- 3 * 4 6 7 withLatestFrom 16 -- 4 * 4 8 9 withLatestFrom 20 -- 5 * 4
2秒間隔で、observable2 * observable1
が行われていますね。
まとめ
ユニットテストでforkJoin
が上手くいかなかったのは、おそらく完了を待ち続けていたのかな?
値が揃えばOKなら、zip
。
常に最新値でやってほしいならcombineLatest
。
1つのObservable
に別のObservable
を急に入れたいみたいな場合はwithLatestFrom
。
今回のも似てるようで全然違う挙動をするので、使い分けできるようにならないとなーと思いました。
mergeMap
、switchMap
あたりも怪しい・・・。