スクロールを連動させる
Qiitaなんかである入力エリアとプレビューエリアのスクロールが連動するやつ。
要件は違ったんですが、スクロール同期という点では同じだったのでやってみたので書いておきます。
単純に連動する
まずは単純に同じサイズの要素を連動させてみます。
単純な連動デモ
const areaA = document.getElementById('area-a') const areaB = document.getElementById('area-b') areaA.addEventListener('scroll', e => { const scrollTop = e.target.scrollTop areaB.scrollTo(0, scrollTop) })
同じサイズのA→Bと連動させるなら、AのスクロールイベントでscrollTop
をとって、BのscrollTo()
に放り込んであげるだけです。
サイズが違う要素を連動する
次にQiitaみたいな要素の大きさが異なる場合の連動を見ていきます。
サイズ違いの連動デモ
const areaA = document.getElementById('area-a') const areaB = document.getElementById('area-b') areaA.addEventListener('scroll', e => { const scrollMaxA = e.target.scrollHeight - e.target.clientHeight const scrollMaxB = areaB.scrollHeight - areaB.clientHeight const percent = e.target.scrollTop / scrollMaxA areaB.scrollTo(0, scrollMaxB * percent) })
さっきと異なるのがサイズが異なるので、何%スクロールしたのかを求めることが必要です。
まずはscrollHeight - clientHeight
で、どれだけスクロールできるのかをそれぞれ求めます。
次にAのscrollTop / scrollMaxA
で%を計算。
最後にBのscrollTo
にscrollMaxB * percent)
でスクロール量を放り込んであげて完成です!
相互連動させる
最後に相互に連動させる必要がある場合です。
相互連動デモ
const areaA = document.getElementById('area-a') const areaB = document.getElementById('area-b') function scrollA(e) { const scrollTop = e.target.scrollTop areaB.scrollTo(0, scrollTop) } function scrollB(e) { const scrollTop = e.target.scrollTop areaA.scrollTo(0, scrollTop) } areaA.addEventListener('scroll', scrollA) areaB.addEventListener('scroll', scrollB) areaA.addEventListener('mouseenter', e => { areaB.removeEventListener('scroll', scrollB) }) areaB.addEventListener('mouseenter', e => { areaA.removeEventListener('scroll', scrollA) }) areaA.addEventListener('mouseleave', e => { areaB.addEventListener('scroll', scrollB) }) areaB.addEventListener('mouseleave', e => { areaA.addEventListener('scroll', scrollA) })
ちょっと面倒な書き方してるかもしれないですが。。。
相互連動するとA→Bをしたときに、Bのスクロールイベントがトリガーされます。
デモぐらい単純な中身なら気にはなりませんが、たまーにズレたりします。
そこで、AをスクロールさせたときはBのイベントをトリガーしない。
BをスクロールしたときはAのイベントをトリガーしない。みたいなことが必要になります。
方法は
- どこからスクロールされているのかを明確にする
- そもそもイベント自体を消してしまう
どっちかだと思いますが、今回は後者です。
各要素にmouseenter
したら別要素のイベントをremoveEventListener
で削除。
mouseleave
したら別要素にイベントをaddEventListener
で付与しています。
Electronアプリを自動アップデートする
Electronのアプリの自動アップデートについて調べる機会があったので書いておきます。
いくつか方法があって
- Electronに元々入っている
autoUpdater
- electron-builder + electron-updater
- update-electron-app
今回は、electron-builder + electron-updaterを試してみました。
electron-builder + electron-updater
npm install electron-updater
Githubでやる場合、package.jsonはこんな感じで。
"build": { "publish": { "provider": "github", "owner": "owner", "repo": "reponame" } }
トークン求められるので、適宜Githubのトークン発行できるところからトークン発行しましょう。
.env
にGH_TOKEN=xxxxxx
の形で置いておけばOKです。
GH_TOKENがあるだけで、勝手にGitHub使われるっぽいですが・・・。
あとはビルドのオプションに--publish always
とかしとくと、ビルドする度にGithubのReleaseにビルドされたファイル上げてくれます。
それが嫌な場合はnever
とか、他はタグつけたときだけとか指定できます。
その他、自前のサーバとかに置く場合はこんな感じに。
"build": { "publish": { "provider: "generic", "url": "http://xxx" } }
(HTTPSじゃないとだめ、みたいな記事も見たけど、そうでもない?)
あとはurlで指定しているとこに、ビルドしてできたファイル群とlatest.yml、latest-mac.ymlを配置しておきます。
versionに-alpha、-betaあたりをつけると、alpha.yml、beta.ymlが作られます。
Vue CLI3
参考までにVue CLI3でプロジェクト作ってて、vue add electron-builder
してる場合はこんな感じです。
(この構成でやってます)
vue.config.jsに
pluginOptions: { electronBuilder: { builderOptions: { publish: { provider: 'generic', url: 'http://xxxxx' } } } }
そんなに変わらないですね。
コードの方
メインプロセスの方に各イベントごとに処理を記述していきます。
一旦↓ここを参考にしつつ、update-downloaded
のイベントだけ足してあげます。
electron-updater-example
よくあるアップデートがあったら再起動するかを聞いてくるやつにしたいので、
autoUpdater.on('update-downloaded', ({ version, files, path, sha512, releaseName, releaseNotes, releaseDate }) => { const detail = `${app.getName()} ${version} ${releaseDate}` dialog.showMessageBox( win, // new BrowserWindow { type: 'question', buttons: ['再起動', 'あとで'], defaultId: 0, cancelId: 999, message: '新しいバージョンをダウンロードしました。再起動しますか?', detail }, res => { if (res === 0) { autoUpdater.quitAndInstall() } } ) ) const min = 10 app.on('ready', () => { setInterval(() => autoUpdater.checkForUpdatesAndNotify(), 1000 * 60 * min) })
10分に一回ペースで見に行って、あれば聞いてくれます。
update-downloaded
にくるのはlatest.ymlに書いてあるもの。
何があるか分かりやすいようにしてます。
やってみて
思っていたよりも簡単にできました。
自前でアップデートがあるかを確認するAPIが必要?という情報で知識が止まっていたので...。
ng-japan2019に参加した
今年は初のカンファレンスはnj-japanになりました。
お仕事の方でAngularを触る機会があり、その辺から行くことは決めてました。
その後いろいろあって、やってたPJはなくなってしまって、同時にAngular触る機会はなくなったのですが・・・
会場はGoogleのTokyo Office。 こういうカンファレンスに参加すると、他社の中に入れる機会があるので楽しいですよね!
六本木ヒルズなんか普段入れませんから・・・。
今日だけヒルズぞく!!!
印象的だったのは、メトロノームの発表。
Angularとかに関わらず、自分で好きなように機能実装してみて、機能追加していって。。。
チュートリアル終わってから何したらいいか分からない人、実際めっちゃ多いと思いますし、
そういう人たちの参考になったことでしょう。
少なくとも、そうだよこれだよ!って僕は思いました。
本当ならスライドのまとめなんかもしたかったけど、誰かがやっている・・・?
ここ数年、カンファレンスには参加するようにしていて思うのが、「英語リスニングできない」問題。
正直、聞いていても内容入ってこなくて、感覚でしか聞けてないのが辛いです。
言語の壁が高い。
とはいえ、ツイッターの#ng-japan2019には助けられました。
タグ追ってれば誰かしら反応してるのでw
英語に関しては、どうにかこうにかしたいと思うけど、実際に行動に移すこともできず。
英語セッション、なんとなくでも意味理解しながら聞けたら楽しいんだろうなーと。
というような感じなので、セッションひとつひとつ細かくレポートはできないです。。。
みんなどうやって英語できるようになったの・・・?
本当に知りたい。
なんやかんや、ng-japanは参加してよかったです。
Angular個人的に触りたい欲は終わってからもあったので、その刺激になりました。
それと、TypeScript書いていこうと強く思いました。
社内でTypeScript書いてる人間はいないので、次はここで抜けていかないと。
NestJSも触ってみようかな?
キッカケは与えてもらってると思うので、少しずつやっていきます。
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にぶち込むので、できるだけ被らない名前にしないとなーとぐらいは思いました。