icon

Blog

electron-vueでメモ帳アプリを作ってみた

2017/12/10

このエントリーは #kosen10s Advent Calendar 2017 11日目のエントリーです。前日の担当はNKMR6194 くんで、なぜpthread_mutexは機能するのか?というエントリーでした。

みんなkosen10sに全く関係無い記事を普通に書いているので自分も普通に今日勉強したことを書く。 もはやelectronとかvueのアドベントカレンダー向けのエントリになっている。

Overview

  • Electron で簡単なメモ帳アプリを作った話(未完成)
  • electron-vue を使ったよ
  • Vue回りの解説はあんまりしないけど雰囲気で読み取れると思う
  • Electron覚えるとWebのFrontend技術で簡単にデスクトップアプリが作れて雑用力が上がるのでおすすめ

Electronとは

Webサイトを作成する感覚でデスクトップアプリを開発できます。Electron は JavaScript, HTML, CSS といったWeb技術を利用してネイティブアプリケーションを作成するためのフレームワークです。開発者はアプリの重要な部分の実装に集中して、面倒な部分はElectronにお任せください。 [https://electronjs.org/] より

だそうです。技術的にはchromiumを内包したアプリができます。中でNode.jsが動いています。
SlackやVisual Studio Codeを始め、がんがん採用実績が増えているフレームワークです。

作るもの

  • メモを新規作成できる
  • ファイル保存できる
  • テキストファイルを読み込める
  • macのNotes.appみたいなのを目指す

electron-vueの導入から起動まで

npm install -g vue-cli
vue init simulatedgreg/electron-vue electron-memo
# 質問されるのでこんな感じで選択した
> ? Application Name electron-memo
> ? Project description An electron-vue project
> ? Select which Vue plugins to install axios, vue-electron, vue-router, vuex
> ? Use linting with ESLint? Yes
> ? Which eslint config would you like to use? Standard
> ? Setup unit testing with Karma + Mocha? Yes
> ? Setup end-to-end testing with Spectron + Mocha? Yes
> ? What build tool would you like to use? packager

cd electron-memo
npm install
npm run dev

ESLintのstandard設定を選択したので生成される.eslintrc.jsを変更しないかぎりはそれに従わないとエラーが発生する。
ESLintに従ったコーディングをおすすめするが慣れない人は削った方がいい。

起動するとこんなWelcome画面。 Chromiumが動いていて、WebPackDevServerがサーバになっている。ちなみに http://localhost:9080/ にアクセスすると真っ白な画面になった。うまいこと隠してくれているみたい。
src/main/index.js を見たところProdビルドではlocalhostURLではなくファイル参照になるようなので安心。

最終的にできたもの

コード解説等

src/renderer/components/Index.vue を作成

<template>
  <div class="grid-container">
    <div class="sidebar">
      <div class="memos-title">Memos</div>
      <div class="memos">
        <div v-for="(memo, memoIndex) in memos" :key="memoIndex" @click="changeIndex(memoIndex)"
              :class="['memo', index === memoIndex ? 'active' : '']">
          <!-- 保存していればファイル名、保存してなければテキストの1行目、テキストがなければEmptyを表示しておく -->
          {{ memo.filePath && memo.filePath.split('/').reverse()[0] || memo.text.split('\n')[0] || "Empty" }}
        </div>
      </div>
    </div>
    <div class="action">
      <div class="action-title">Action</div>
      <div @click="addNew" class="action-item">Add New</div>
      <div @click="importMemo" class="action-item">Import</div>
    </div>
    <div class="editor">
      <!-- keydown.meta.83でcommand+S, ctrl+83でctrl+sを使う。他にもっとうまいやりかたがありそう。 -->
      <textarea ref="editor" v-model="memos[index].text" placeholder="Input free memo." class="textarea"
         @keydown.meta.83="save" @keyup.ctrl.83="save" autofocus></textarea>
    </div>
  </div>
</template>

<script>
  // remoteを通じてメインプロセスのAPIを呼び出す
  const {dialog} = require('electron').remote
  const fs = require('fs')

  const emptyMemo = {'text': '', filePath: ''}

  export default {
    data: () => {
      return {
        memos: [{'text': 'sample memo.', filePath: ''}],
        index: 0
      }
    },
    methods: {
      addNew () {
        this.memos.push(Object.assign({}, emptyMemo))
        this.changeIndex(this.memos.length - 1)
      },
      changeIndex (index) {
        this.index = index
        this.$refs.editor.focus()
      },
      importMemo () {
        // dialog.showOpenDialogでOSのファイル選択を呼び出す
        // 複数ファイルの選択も想定したAPIなので返り値はファイルパスの配列がかえる
        const filePath = dialog.showOpenDialog({
          properties: ['openFile', 'createDirectory']
        })[0]
        // ファイル読み込みはNode.jsのfsを使って普通に行うことができる
        fs.readFile(filePath, 'utf8', (error, text) => {
          if (error) {
            console.error('error: ', error)
          } else {
            this.memos.push({
              text: text,
              filePath: filePath
            })
            this.changeIndex(this.memos.length - 1)
          }
        })
      },
      save () {
        const memo = this.memos[this.index]
        if (!memo.filePath) {
          // showSaveDialogでは保存するファイルのパスを選択できる
          const filePath = dialog.showSaveDialog({title: 'Save as'})
          memo.filePath = filePath
        }
        fs.writeFileSync(memo.filePath, memo.text)
      }
    }
  }
</script>

<style scoped>
  /* style scopedと書くことでこのファイル内のみで有効になる */
  .grid-container {
    /* ブラウザはChromium限定なので新しいの使ってみようと思い
       初めてgrid layoutを使って見たが非常に楽だった
       もうfloatは回り込みとかしたい時だけでよさそう */
    display: grid;
    grid-template-columns: 220px 1fr;
    grid-template-rows: 1fr 100px;
    grid-template-areas: "sidebar editor"
                         "action editor";
  }

  .sidebar {
    grid-area: sidebar;
    background-color: #4863ad;
    color: #FFFFFF;
    max-height: calc(100vh - 100px);
    overflow: auto;
  }

  .action {
    grid-area: action;
    background-color: #4863ad;
    color: #FFFFFF;
  }

  .editor {
    grid-area: editor;
    height: 100vh;
  }
  // 長いので省略
</style>

自分的に学びのあった部分はコメントを入れた。 <template>にHTMLを、<script>にJSを、<style>にCSSを書いていく。
さらに<script>でexportしているdataにリアクティブにするデータを、methodsにクリックイベント等で呼ぶ関数を書いていく。
書くべき場所があるって素晴らしい。

src/renderer/router/index.jsを編集

  routes: [
    {
      path: '/',
      name: 'index',
      component: require('@/components/Index').default
    },
    {
      path: '*',
      redirect: '/'
    }
  ]

src/renderer/App.vue<router-view>にパスに対応したコンポーネントが表示される

ビルド

ビルドもelectron-vueを使っているおかげでnpmコマンド1つで導入時に選択したelectron-packagerを使ってビルドしてくれているようになっている

npm run build
...
ls build/
electron-memo-darwin-x64	electron-memo-linux-x64		electron-memo-mas-x64		icons

mac, linux用のバイナリが出力された
インストーラは出来なかったのでそのうちまた調査する
windowsはエラーが出てビルド出来なかったがwindows用のビルドにはwineが必要らしい
注意!!!ビルドは恐ろしくメモリを食い尽くすようでChromeが落ちる!ビルド前にはブログの途中書きとかがないか確認しよう!!!

感想とか

  • Web技術でデスクトップアプリが作れるようになってしまったので雑用力が向上した
  • このメモ帳はCSSとかで行き詰まったところを除けば3,4時間で作れたので、簡単な社内で使うユーティリティツールなら1日かからず作れてしまいそう
  • 今回は状態の保存すらできず、Vuexを使わなかったがstoreにデータを入れて、それをJSONで保存して、起動時にロードすれば状態の保存がかなり楽になりそうなのでやりたい
  • フロントエンド書いてるのにブラウザがChromium限定なので新しい記法や仕様を思う存分使えそう。ブラウザ別の対応を考えなくていい
  • ピュアelectronを触らずに最初からelectron-vueを使い初めてしまったのでelectronとelectron-vueの恩恵の境界がわかりづらくなってしまった
  • 少なくともelectron-vueを使うことで元からVue.jsを使っていたチームなら爆速でデスクトップアプリ対応出来そう

明日の担当はtsudukamiで「教育のお話詰め合わせセット」です。