typeof Diary

VimとかJSとか。やったことのメモ。自分のため。

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/awaitPromiseを上手く組み合わせると良いです。 それぞれ単体で使うことは知ってても、組み合わせはハマって初めて知る気がします・・・。

追記

↑のコードだと、ディレクトリ下に100件以上あると全て取得できない。

developer.mozilla.org

ドキュメントにも書いてあった。

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なりしてるからですかね・・・。
ちょっとつらい感じですけど、一応これでいけました。。。

Component modal props won't update

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に行ってきたので簡単に感想書いておきます。
先週の金曜日の話だけど・・・。

ng-kyoto.connpass.com

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を結合するには、何パターンか方法があります。

  1. zip
  2. combineLatest
  3. forkJoin
  4. 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〜4observable21〜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よりも出力回数が増えています。
zipobservable1observable2が揃って出力されていたのに対して、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の間の出力がなくなりました。
observable1observable2の両方が完了したタイミングで出力されています。

withLatestFrom

withLatestFromも見ておきましょう。

ソースObservableが値を発行するたびに、その値と他の入力Observablesからの最新の値を使用して式を計算し、次にその式の出力を発行します。

これは前の3つとは少し違っています。
今回はobservable2observable1を入れて使ってみます。

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

今回のも似てるようで全然違う挙動をするので、使い分けできるようにならないとなーと思いました。

mergeMapswitchMapあたりも怪しい・・・。