esaをより便利に使うための拡張機能「Refined esa」をつくった

2020.05.23 thumbnail

@dala00さんが企画している、1週間でテーマに沿ったWebサービスを作る #web1weekという企画に参加しました。今回は第二回の開催で「Like」というお題でした。 自分は「自分の好きなWebサービスをより好きになるサービス」というコンセプトをもとにesaをより便利に使うための「Refined esa」というChrome拡張機能を作成しました。

Chromeストア GitHub

できること

履歴機能

自分がひとりでesaを使っている感じではたいてい同じ記事をひたすら編集・見返す運用になっています。そういった運用に合わせて、閲覧履歴が簡単に見れると嬉しいので履歴機能を作りました。

実装としては、次の2ステップでした。

  1. **.esa.io/posts/\d+$にマッチするURLであれば、DOMの中を見てtitleやcategoryなどの情報をストレージに保存。
  2. 履歴が開かれたタイミングでストレージにある情報を表示。

本当はショートカットキーの実装や、インクリメンタルサーチを作ろうと思いましたが時間的な制約から今回はスコープ外としました。

文字数カウント

自分はesa.ioをブログエディタとして使っています。ブログを書く際に文字数を基準にするタイミングがあるのですが、毎回コピペして文字数をカウントしていました。 そういったことから「テキストエディタに文字数カウントがあればいいのに」と思っていたので作りました。 コードとしては、Markdownのプレビュー内の変更を監視して、変更があれば文字数カウントを行い表示するだけなので簡単に実現できました。

できなかったこと

本当はエディタの文章を解析して、Lintによる文章修正や正規表現による置換を行いたかったのですが、実現ができませんでした。

Chrome拡張機能では「Content Scripts」という機能を使ってページを操作するのですが、同じDOMを操作することは出来るのですが、もともとページにあるScriptで定義された変数へのアクセスはできません。 参考 昔のesaエディタ(いわゆるv1のエディタ)であれば、標準のTextareaなのでvalueをいじれば触れたかもしれませんが、現状のesaエディタはCodeMirrorというエディタを採用して簡単にいじることができません。

window空間の変数にアクセスできればいろいろな操作が可能でした。例えば次のようなコードでは、JavaScriptを使って値の挿入が可能です。実際にページを開いてChrome Dev Toolsから動作を確認できます。

document.querySelector('.CodeMirror').CodeMirror.setValue('Hello CodeMirror')

これができれば機能の幅が広がったのですが、期間内に解決できそうになかったのでスルーしました。

エンジニアリング的な話

TypeScript対応

chrome-extension-cliを使ってコードを生成しましたが、初期状態ではTypeScriptが使えません。 tsc派とbabel派がいると思いますが、自分はbabel派なのでbabel-loader経由で変換を行いました。

加えて、chrome拡張機能ではchrome側のAPIを叩けるのですが、その型定義ファイルが@types/chromeで配布されているので利用しました。

yarn add -D @types/chrome

dom-chef

ReactやVueなどを使わずビューを生成するには通常次のようなコードを書くでしょう。

const div = document.createElement('div')
div.classList.add('hoge')
div.textContent('Hello World')

console.log(div) // => <div class='hoge'>Hello World</div>

ただ、ReactやVueに慣れてしまったフロントエンドの人からすると冗長な感は否めません。 しかし、拡張機能にReactやVue(あるいはjQuery)を入れると、拡張機能のためだけにこれらのライブラリを読み込むことになります。

そこで導入したのがdom-chefというライブラリです。dom-chefでは上記のコードと同じことが次のコードで実現できます。

import React from 'dom-chef'
const div = <div className='hoge'>Hello World</div>

見て分かる通りJSXです。ただし、実行されるのはReactではなくdom-chefになっています。深堀りするために、このJSXが変換された後のコードを見てみましょう。

import React from 'dom-chef'
const div = React.createElement('div', { className: 'hoge' }, 'Hello World')

import Reactとしていますが、実際はdom-chefのcreateElementメソッドが実行されます。 dom-chefはReactのAPIを模して作られており、dom-chefのcreateElementはざっくり次のような処理を行っています。

const createElement = (type, attributes, children) => {
  const element = document.createElement('div')

  // textの場合はtextNodeを生成しくっつける。
  // 実際は他にArrayやNodeだった場合の処理もある
  element.appendChild(document.createTextNode(children))

  // attributes周りの処理を行っている。以下はclassの例
  const existingClassname = element.getAttribute('class') ?? '';
  setAttribute(element, 'class', (existingClassname + ' ' + String(value)).trim());
}

先程のコードと同じですね。dom-chefはJSX記法を用いて直接DOMを生成できるライブラリであることがわかると思います。

crx-hotreload

Chrome拡張機能ではコードを更新しただけでは変更が反映されず明示的に拡張機能をリロードする必要があります。しかし、crx-hotreloadというライブラリを入れるとディレクトリ内のファイルの変更を監視し、変更が検出されると、拡張機能をリロードしてくれます。

参考にしたもの

Chrome拡張機能「Refined GitHub」をかなり参考にしています。