概要
Excelの資料作成でちょっとでも楽がしたかったので、「Doci」というツールをつくりました。
本記事では、新たに技術を学ぶことではなく、培った知識・経験で日常の課題をいかに解決していくかにフォーカスしております。ですので、コードそのものよりも、「〜〜したいをどうコードに落とし込むか」を中心に書いています。
以下に簡単な動作デモを載せます。
動作環境
とにかく挫折しないことを目指して、自分が使いやすい形でつくったので、以下のようにゆるい感じになっています。
- PCで使うことのみを想定しているので、レスポンシブ対応は考慮対象からはずしました
- クリップボードにアクセスする機能はブラウザによって挙動がさまざまだったので、Chromeのみに対応する形としました
各種リンク
利用した技術
- JavaScript(ES2015)
- Sass
解決したかった課題
システムがインテグレーションする感じのお仕事をしていると、業務の10割大部分でExcelを使用し、資料やテスト証跡などをつくることになります。
Excelは表計算ソフトなので、資料をつくっていると、「これはExcelだとちょっと面倒だな...」と思うことが多々あります。
スクリーンショットを撮影したら、自動でExcelに貼り付けてくれる素敵ツールなどもあるようですが、資料やテスト証跡では、おもてなしの精神
を以ってスクリーンショットの大事な部分を枠で囲ったり、吹き出しでコメントを入れる作業が別で必要になります。個人的にはこちらの方がスクショぺたぺたよりも、しんどい部分だと感じています。
これをいい感じに解決してくれるツールは見当たらなかったので、無いならつくれば良いじゃないの精神で、1ヶ月少々かけてざっくりとつくってみました。
以下では、課題の詳細等について記述していきます。
現状
アプリを作る上で、あれも欲しい、これも欲しいと最初から構想を膨らませていき、幾度となく散ってきたので、まずは、「これは外せない」と思う機能をリストアップします。箇条書きでボリュームを目に見えるよう整理することで、最低限実装すべきものに注力できるようにします。
絶対欲しい機能
- マウスでカチカチ吹き出しやら枠やらを切り替えるのが面倒なので、さくっと切り替えられるキーボードショートカットが欲しい
- ぺたぺた貼り付けた画像にコメントを入れていると、スクロールするのが手間だから、まとめて編集したい
- アプリからExcelへ貼り付ける作業が面倒になったら元も子もないので、結果をすぐにExcelに貼り付けられるようにしたい
これらの「願望」から、実装に落とし込むため、「要件」として少し整理してみます。
願望は、いくらでも素敵に実現できる可能性を秘めていますが、思いのおもむくままに進めると、大体大きな落とし穴に叩き落とされてしまいます。これを防ぐため、既存のライブラリ・API、そして、自身の技術力と向き合いながら、「これならまあ、なんとかできるかな」というレベルまで落とし込んでいきます。事前調査は大事です。
ざっくりと整理し、以下の方針で進めることとしました。
- インストールなどの手間はかけたくないので、Webアプリとして公開
- Webアプリで画像を扱うのは、情報が充実しているCanvasを利用
- ペイントツールのような形で描画モードを表示し、キーボードで切り替え可能とする
- キャンバスを複数個利用し、キーボードで切り替えることで、複数の描画領域をスクロールなしで実現
- 結果を一気にExcelとして出力できるのが理想だが、実装コストが大きくなりそうなのと、画像の配置の調整はExcelで行った方が良さそうなので、編集結果をクリップボードにコピーすることをゴールとする
このとき、抱えている課題を解決するのはもちろんのことなのですが、趣味の開発なので、「こういうスキルを習得したいな」といった感じの技術的な目標もふんわりと意識しておきます。業務では冒険が難しいことも多々あるかと思いますが、自分一人で好き勝手できるのであれば、スキルアップも目指していきたいです。
ということで、開発を通じて、こんなことを学べたらいいなーという感じの目標をざっくりとたてます。
- ユーザの入力をもとにキャンバスを動的に描画する機能の実装方法
- クリップボードを操作するAPIの基本
- JavaScriptのES2015の基本機能
目標というと、ややハードルが高く見えてしまいますが、狙いとしては、開発を終えたら、上に書いたことができるようになっていたらいいなー、ぐらいのゆるっとした感じです。昨日の自分より強くなりたい。
要件が固まったら、あとはひたすらコードを書いていきます。
12月の土日と年末年始休みがけっこう溶けてなくなりました。
ひたすらコードを書いていったのですが、いきなりアプリがどん、と出来上がったわけではなく、色々と試行錯誤の果てに少しずつ形になっていきました。
普段はフレームワークのお作法に沿って土台となる枠組みから徐々に組み立てていくのですが、今回は、素のJSのみのすごくシンプルなものなので、自分なりに「こう組んでいったらメンテとかしやすくなるんじゃないかな」とあれこれ試しながら進めていきました。
完成形のソースコードにも、もちろんそういった試行錯誤の形跡は含まれているのですが、どういう思考過程でコードが組まれたか
を「結果」だけから読み取るのは中々難しいです。
なるべく設計書を書いたり、コメントを多めに書くようにして、「なぜ」を明確にするようには努めました。しかし、せっかくゴリゴリ組んでみたので、ざっくりとどんな風に考えながらコードを組んでいったか
を備忘録がてら書いておこうと思います。
※以下の記述が正解というわけではなく、あくまでコードを組むときの思考パターンの一例ぐらいに考えて頂ければと思います。
Step.1 とりあえず組んでみる時期
最初は「超」がつくほどシンプルに、キャンバスに図形を描くことから始めました。
この段階では特にクラス設計とかを意識することもなく、ただひたすらコードを書いて画面で結果を見て...を繰り返していました。いきなりばしっと適切な設計ができれば、試行錯誤の時間も減らせるのですが、まだまだ経験が足りないので、できる部分から取り組んでいきます。
Step.2 「図形」でまとめてみる時期
よくあるオブジェクト指向の解説では、「大きさ」や「色」を持った「図形」クラスを定義し、具体的な図形はそれを継承して使う、といったことが書かれているかと思います。ですが、目に見えるものをそのままプログラムの世界に落とし込む
と、管理が難しくなってしまいます。
具象に頼ると直感的に分かりやすくはなりますが、抽象度が下がることで、拡張性・再利用性を失うことになります。
そもそも一かたまりの「状態」と「振る舞い」をまとめて扱えるようにできるのが、クラスのうれしい機能なので、ここでは、図形は、「大きさ」などの状態と、「描画」機能を持ったクラスとして定義します。
例として、「赤枠」を表す図形のクラスを抜粋して記します。
/**
* 赤枠の四角を表すクラス
* 位置・大きさ・色を状態として持ち、伸縮を可能とする
*
* @property {string} color 枠の色
*/
export default class PointRectangle extends Shape{
constructor(context, startX, startY) {
super(context, startX, startY)
this.defineAttribute()
this._color = '#FF0000'
}
/**
* 四角の枠をキャンバス上に描画
*/
draw() {
this._context.canvasContext.strokeStyle = this._color
this._context.canvasContext.strokeRect(this.x, this.y, this.width, this.height)
}
}
JavaScriptの標準機能ではインタフェースがまだ用意されていないので、擬似的ではありますが、上記のように、各図形にdrawメソッドを用意しておけば、呼び出す側は、図形のdrawメソッドを呼ぶだけで、後は各々がよろしくやってくれる、というようになってくれます。
アプリの中では、配列で図形を管理し、各々を描画するときは、ループでそれぞれのdrawメソッドを呼ぶ形で利用しています。
Step.3 インスタンス管理をいい感じにしたい時期
ある程度機能が充実してくると、インスタンスがもりもり増えてきます。ペイントツールだと特に、「マウスが押されている間」とか「クリックしたとき」など似通った処理をさまざまなインスタンスについて記述する必要が出てくるため、コードの重複がたくさん発生してしまいます。
これを解決するための手法の一例として、まずはコードを見て頂くとイメージが掴みやすいかと思います。
// アプリで実行され得るイベントの種類
const occurEvents = ['mousedown', 'mousemove', 'mouseup', 'click']
// アプリで描画されるオブジェクト
const drawingList = [
new MetaDrawing(this._context), new CellDrawing(this._context),
new RectangleDrawing(this._context), new TextDrawing(this._context), new ImageDrawing(this._context),
new MoveDrawing(this._context), new DeleteDrawing(this._context), new CursorDrawing(this._context)
]
occurEvents.forEach((event) => {
const targetEvents = []
// 各描画機能で扱うイベントを取得
drawingList.forEach((drawing, index) => {
if (typeof drawing[`${event}Event`] === 'function') {
targetEvents.push(index)
}
})
// イベントリスナーで発火させるべきイベントを設定
this._context.canvas.addEventListener(event, (eventArg) => {
targetEvents.forEach((targetIndex) => {
// インターセプターで前処理を実行した後、イベント処理を発火
drawingList[targetIndex]['setupEvent'].call(drawingList[targetIndex],event, eventArg)
})
})
})
処理されるインスタンスは、キャンバス上でマウス操作をするとなんらかのイベントを発火させるもの、という点で共通しており、似ているものはまとめて処理できるようにしたいです。
そこで、キャンバスで発生し得るイベントを、各々のインスタンスについて、処理を行うかを最初に確認します。処理を行うのであれば、イベントリスナーに登録させます。
そして、イベントが発火したとき、各イベントは、前処理を実現できると便利です。たとえば、削除モードで画面をクリックしたけど、削除対象がなかった場合は、削除処理を実行しない、といったように、イベント後の処理を行うかどうかを判断するための処理などが考えられます。
アプリでは、イベントが発火したタイミングで、直接イベント処理を呼び出すのではなく、前処理として「setupEventメソッド」を呼び出すようにしています。
やや抽象度は上がりましたが、具体的なものとして扱っていると重複していたグループから、「共通化してもグループの本質を崩さない要素」を抽出し、まとめていくことで、機能が増えてもコードをコピーすることなく、一定のルールにもとづいたメソッドを実装するだけで対応できるようになりました。
この他にもクラスが大きくなり過ぎないようレイヤーを分けたりなどなどありましたが、その辺りはまた別の機会に...。
ということで、すごくざっくりとではありますが、実装の中でどういうことを考えながらコードを組んだのかを書き出してみました。長くなってしまいましたが、要点は以下になるかと思います。
- クラスが扱いにくい or 長くなってきたら、役割単位で統合/切り出しできないか考える
- 共通化することで可読性・メンテナンス性が損なわれないのであれば、似たものをまとめて扱えないか考える
可読性については、たとえば、二回同じような処理を書いたという理由だけで安直に共通化してしまった場合を考えてみます。
実際はたまたま似ているだけで、別物の処理を一か所に集約させてしまうと、コード量は減ったのに、かえって読みづらくなってしまった、ということが起こってしまいます。
ある程度プログラムを書くことに慣れてくると、なんでも共通化したくなりますが、共通化した結果、読みにくくなっていないかは、常に意識しておくとよいかと思います。
いろいろとエラそうなことを書いてしまいましたが、まだまだ私も修行中の身なので、「私はこういう風に書きました」程度に捉えて頂ければと思います。
また、もっといい書き方あるよ!!等ございましたら、やさしく教えて頂けるとうれしいです。
今後の課題
まずは完成させることを目標としていたので、最初に掲げた「絶対はずせない機能」の実装に注力しました。ですので、当然現状ではまだまだ不十分な点が多々あります。
ここでは、より完成度を高めるために必要な機能を今後の課題として書いていきます。もっと技術力が身についたらリトライしたい...。
- テキストのフォント・サイズ・色を自由に指定可能に
- キャンバスを画像ではなく、再操作可能なオブジェクトとして保存し、ブラウザを閉じても再編集可能に
- アンドゥ・リドゥ機能
- より精巧なExcelへの擬態
まとめ
思ったよりも色んなところで詰まり、何度か心が折れそうにもなりましたが、なんとか、「こんな課題を解決したいな」というぼやっとした願望から、実際に使えないこともない形にすることができました。内部資料や一回きりの備忘録程度のテスト証跡なんかでは、楽をしていきたいなーと思います。
技術力を身につけるために新しい技術を学ぶのも大事なことではありますが、日常の課題を勉強して培った知識・経験で試行錯誤しながらつくってみるのも、やりがい・達成感があっていいなと思いました。
プログラムを組むための土台も少しずつできてきたかなーと思いたいので、これからも少しずつアプリをつくっていきたいです。頑張りたい。