atomエディタのソースコードを再利用してアプリのカスタマイズ性を高めよう

本記事はElectron Advent Calendar 2016 5日目の記事です。

InkdropというElectron製ノートアプリを作っています。
このアプリにはプラグイン機構による拡張性を備えているのですが、これはatomからコードを拝借して実装しました。
atomはMITライセンスによるオープンソースのテキストエディタです。
その際に得た知見を共有したいと思います。
これをきっかけにあなたのElectronアプリ改善のお役に立てれば幸いです。
atomのパッケージを作ったことのある方ならすんなり理解できると思います。

event-kit

全体を通してイベント駆動の処理はevent-kitと呼ばれるモジュールが使われています。
一見車輪の再発明っぽいですが、良いところがあります。

イベント購読APIの基本的な実装方法は以下の通りです:

import { Emitter } from 'event-kit'

class User {
  constructor () {
    this.emitter = new Emitter()
  }
  onDidChangeName (callback) {
    this.emitter.on('did-change-name', callback)
  }
  setName (name) {
    if (name !== this.name) {
      this.name = name
      this.emitter.emit('did-change-name', name)
    }
    return this.name
  }
  destroy () {
    this.emitter.dispose()
  }
}

イベント購読の利用側は以下のように実装します:

const subscription = user.onDidChangeName(name => console.log(`My name is ${name}`))
// 購読解除
subscription.dispose()

event-kitでは、複数の購読をまとめてキャンセルできます。これが結構便利。

import { CompositeDisposable } from 'event-kit'

const subscriptions = new CompositeDisposable()
subscriptions.add(user1.onDidChangeName(name => console.log(`User 1: ${name}`)))
subscriptions.add(user2.onDidChangeName(name => console.log(`User 2: ${name}`)))

// 購読解除
subscriptions.dispose()

node標準のEventEmitterだとremoveListenerにコールバック関数を渡さないといけなくて面倒ですが、これならいちいち関数を保存しておく必要がありません。

CommandRegistry

ソースコード: https://github.com/atom/atom/blob/master/src/command-registry.coffee

atomはそれ自体が多くのパッケージで出来ています。
そのため、モジュール同士の疎結合化がかなり考えぬかれて設計されています。
その中核をなすのがCommandRegistryです。
これはコマンド名と対応する処理のペアを管理するモジュールです。
たとえば、application:quitcore:copyといったものがあります。
モジュール間でやりとりされるメッセージみたいなものと考えてもいいかもしれません。
具体的には、主にショートカットの入力やメニューの実行に使われています。
エディタ内での方向キーすらcore:move-upといったコマンドになっています。
この仕組みによって高いカスタマイズ性をも実現しています。

もちろん、コマンドのイベント検知には前述のevent-kitが使われています。

MenuManager

ソースコード: https://github.com/atom/atom/blob/master/src/menu-manager.coffee

Electronアプリはマルチプラットフォームで動作しますが、プラットフォームに合わせた挙動は自分で実装しなければなりません。
その一つにメニューが挙げられます。

atomでは以下のようにCSON形式でメニューの項目とそれに対応するコマンドが列挙されています:

'menu': [
  {
    label: 'File'
    submenu: [
      { label: 'New Window', command: 'application:new-window' }
      { label: 'New File', command: 'application:new-file' }
      { label: 'Open…', command: 'application:open' }
      { label: 'Add Project Folder…', command: 'application:add-project-folder' }
      { label: 'Reopen Last Item', command: 'pane:reopen-closed-item' }
      { type: 'separator' }
      { label: 'Save', command: 'core:save' }
      { label: 'Save As…', command: 'core:save-as' }
      { label: 'Save All', command: 'window:save-all' }
      { type: 'separator' }
      { label: 'Close Tab', command: 'core:close' }
      { label: 'Close Pane', command: 'pane:close' }
      { label: 'Close Window', command: 'window:close' }
    ]
  }
...

https://github.com/atom/atom/tree/master/menus
上記にmacOS、linux、windowsの3種類のメニュー定義データがあります。

Electronにもテンプレートからメニューを定義するAPIがあります。
MenuManagerは単なるテンプレートにとどまらず、コマンドのハンドラの有無で自動で項目を無効/有効を切り替えたりしてくれます。
また、パッケージによって新たにメニューを部分的に追加したり削除しやすいように作られています。

atom-keymap

ここで、キーボードショートカットの定義が無いことに気づいたかもしれません。
atomではメニューとショートカットさえも疎結合にできています。
各キー入力とコマンドのペアをkeymapと呼び、別途定義されています。
この仕組みはatom-keymapというモジュールによって実現しています。

atomのkeymapはプラットフォームごとにこちらに定義されています: https://github.com/atom/atom/tree/master/keymaps

下記のように、キーの組み合わせとコマンドの対で定義されています:

'atom-text-editor':
  # Platform Bindings
  'home': 'editor:move-to-first-character-of-line'
  'end': 'editor:move-to-end-of-screen-line'
  'shift-home': 'editor:select-to-first-character-of-line'
  'shift-end': 'editor:select-to-end-of-line'

atom内部では、window.documentkeyupkeydownイベントを受け取って対応するコマンドをdispatchしています。
このkeymap機構によって、プラットフォームに合わせたキー操作、新しいコマンドの定義、ショートカットの変更などがとても容易になります。

再利用しやすかった

紹介したモジュールはatomの中核でありそれぞれ依存関係にあるので、一部だけ導入するというのは難しいでしょう。
しかし中核とそれ以外で完全に疎結合なので、比較的アプリのコードと馴染ませやすいと思います。
実際に再利用するにあたって、あまりモジュール自身のコードをいじる必要はありませんでした。
もしこれからElectronのアプリを作るという方は、一度見てみて損はないと思います。

パッケージ機構そのものはさすがに複雑で説明が大変なので、また機会があれば説明したいと思います。


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です