📄

仕事中にメモ帳とモダンブラウザだけでいろいろ作っている話

2024/12/31に公開

TL;DR

  • メールサポートみたいな部署で、メモ帳とブラウザでいろいろツールを作ってるよ
  • エンジニアはいったん辞めたつもりで就職したのに、楽をしたくなってつい手が動いてしまったよ
  • ブラウザがモダンでHTMLとJavaScriptさえ書ければ結構いろいろできるよ

ちなみにこの取り組みは私個人の「内職」から始まりましたが、現在は上長より業務として認知していただいています。

ちょっと自己紹介

ハンドルネーム「naary」です。発音は「カービィ」じゃなくて「AC」と同じです。

コロナ禍にRailsやAWSを独学したあと、これまでに合計で3年ほど、PHP/Laravelをメインに使ってる受託の会社やNext.js/TypeScriptを使ってるスタートアップなどを業務委託で転々としました。

それが2023年の秋頃までで、2024年1月からは「いったんちょっと開発を休もう」という気持ちになり、まったくの異業種である現在の仕事を始めました。

現職について

詳しいことはほとんどお伝えできませんが、特性としては以下の要素があります。

  • マニュアルに沿ってシステムを操作し、作業を行う
  • 作業は顧客(ユーザー)からの依頼で発生する
  • 顧客とはメールでのみやり取りを行う
    • やってることは違うけど、メールサポートやコールセンターみたいな感じ
    • 件数を早く・多く・正確にこなすのが評価になるという性質も似ている
  • アプリやソフトウェアの追加インストールは不可
    • 外部サイトを閲覧するだけならOK

規則的な単純作業をこなすタイプの仕事で、覚えることもけっこう多かったです。

私としては、新しいことを一から学べるのをむしろ楽しく思っていました。

やったこと

順を追って説明します。

研修期間中、メモ帳にたくさんメモを取った

「メモ帳」は紙ではなく、もちろんWindowsのあのアプリです。

いまの職場では業務や教育の資料をどう作るか・管理するかに元より課題があったらしく、ドキュメントの古さや粗さが目立ちました。

そのため「資料を読んでなお分からなかったとしてもある程度しょうがない」という印象で、未熟な資料の隙間を埋めるような口頭の説明が多く、その説明を個々人がメモ帳に書き溜めていくしかやりようがないという状況でした。

メモをHTMLで清書した

既存の資料とメモの併用は効率が悪かったため、上記のような既存+αの内容にまとめた自分のメモを、見やすいようにHTMLへ書き起こしました。

メモ帳にHTMLを書いて、拡張子を.htmlにして保存すればHTMLファイルを作成できます。

職場ではクラウドストレージにOneDriveを使っているのですが、作成したHTMLをクラウドに保存し、それを開くことで簡単にWebページを配信できることがわかりました。

エクスプローラーのOneDriveからHTMLファイルをダブルクリックするか、SharePointから「アプリで開く」を実行すればブラウザで開けます。
そうやって開いてしまった後はブックマークに保存すればOK。

普通のサーバーではないストレージへHTMLをホストするのは初めてだったので、CSSをきちんと読み込めるか、HTMLのページ同士を正常にリンクできるかなど一つ一つ検証していきました。

学び:

  • OneDriveでもHTMLファイルからCSSファイルを読み込める
  • フォルダを跨いでもOK
  • フォルダ内外のHTMLをaタグでリンクできる

Web Componentsを導入した

外部ファイルにCSSを書いて読み込むことはできました。しかし今さらBEM記法などのCSS管理はやりたくありません。

VueとReactを知る身からするとやはり共通のUIはコンポーネント化したくなるもの。そこで、以前から存在のみは知っていたWeb Componentsを使うことにしました。

まったく未経験なので調査しながらの導入です。昼休み中に実験したり、仕事中にこっそりやったりしていました。
(👆コールセンターであまり電話が鳴らない状況があるみたいに、暇になることがあるんです)

学び:

  • カスタム要素の作り方
  • スロットで小要素を流し込める
  • データ属性でPropsみたいに値を渡せる
  • ライフサイクルメソッドがある

いろいろ作れそうな可能性に気がついた

いまの業務では、いろいろな種類の「コメント的な文言」を書き込むタイミングというのがあって、それぞれに定型文が用意されています。それらの定型文は、コメントの種類ごとにメモ帳やExcelで管理されています。

さらに、先述のように顧客とはメールでやり取りしています。このメール文にもテンプレートがあり、それらもExcelで管理されていたりします。

私はExcelが嫌いですし、メモ帳でマウスをドラッグアンドロップしてコピーするのさえも面倒に感じていました。メモ帳はテキストを折り返さないので、長いコメントだとことさらに面倒なのです。

そこで、手始めに「クリックしたらテキストをクリップボードにコピーしてくれるボタン」をWeb Componentsで作ることにしました。技術ブログのサンプルコードが記載されているところでよく見るやつです。

定型文とコピーボタンを左右に並べたページを完成させたとき、「この実装をベースにもっと機能を追加すれば、あんなツールやこんなツールを作れるのではないか」という可能性を感じました。何せこの業務は「定型文のコピー&ペースト+穴埋め」の作業をそこそこの割合で含んでいるのです。

以後、暇な時間を使ったり忙しい合間を縫ったりしつつ、少しずついろんなものを作っていきました。

作ったものの例

テキストでの説明となり、わかりづらいかもしれませんがご海容ください。

コメント一覧ページ

  • コメントの定型文が縦に並んでいる。
  • 左にコピーボタン、右に文言がある。
  • コピーボタンをクリックすると、右の文言がクリップボードへコピーされる。
  • 穴埋めをして使うものは、変数の箇所が<input type="form" />になっている。
  • 入力してコピーボタンを押すと、穴埋めされたものがコピーされる。

メール文作成ツール

  • 定型文のパターン一覧が並んでいる。
  • 穴埋めの変数は<textarea><select>になる。
  • それぞれ入力、選択をしてボタンを押す。
  • 定型文に入力値が反映される。
  • 完成したメール文がクリップボードに出力される。

チェックシート生成ツール

  • 選択肢と入力欄が並んでいる。
  • それぞれ選択、入力してボタンを押す。
  • 内容に応じたTODOリストが下部に表示される。
  • 「やること」と<input type="checkbox">が左右に並んでいる。
  • 最初に入力した内容をテンプレートへ反映したコメントも出力される。
  • コメントはクリックするとクリックボードへコピーされる。

テキスト抽出ツール

  • とあるWebサイトのページで使う。
  • Ctrl+ACtrl+Cで全文をコピーしたものを<textarea>へペーストする。
  • 中に含まれている必要な文言だけを一覧表示する。
  • それぞれの文言はクリックするとクリックボードへコピーされる。

実装のイメージ

サンプルコードは業務PCからのコピペや写経などではなく、記憶のみで大体のニュアンスを再現したものです。

配列とコンポーネントの組み合わせ

定型文に穴埋めするタイプのツールの例です。

なんとなくこんな感じの配列があって👇

const PATTERNS = [
  {
    name: "〇〇のとき",
    positions: [
      { position: 0, select: true, items: [/* 選択肢の文言 */] }, // select: true なら <select>
      { position: 1, placeholder: "ここに**を入力" } // <textarea>がデフォルト
    ],
    template: [
      "XXXXXXXXXX",
      [
        {
          position: 0,
          select: [ // 選択肢ごとに表示する文言のパターン
            { selected: "XXXXXX", value: "XXXXX", },
            { selected: "YYYYYY", value: "YYYYY", }
          ]
        }
      ],
      "XXXXXXXXXXXXXXXXXXX",
      [
        "XXXXXXX",
        { position: 1 }, // position: 1 の textarea から値が入る
        "XXXXXX"
      ]
    ]
  },
  // ~~~~~~~~~~~~~~~~~~~~~
]

これをjson.js拡張子で保存し、<head>の中で<script>タグから読み込み。

<body>要素の下で読み込んでいるコンポーネントでPATTERNSを参照して、mapで回してHTML文字列の配列にしたものを.join("")で一行にし、<template>タグで読み込んでshadowRootappendChildしています。

class ComponentName extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });

    const elements = PATTERNS.map((_) => {
      if (_.select) {
        const options = _.items.map(item => `${<option>{item}</option>}`)
        return [`<select data-position="${_.position}">`, ...options, "</select>"];
      }
      return `<textarea placeholder="${_.placeholder}" data-position="${_.position}"></textarea>`;
    }).join("");

    const templateContent = `
      <style>
      </style>
      <div>
        ${elements}
      </div>
    `;

    const template = document.createElement('template');
    template.innerHTML = templateContent;

    this.shadowRoot.appendChild(template.content.cloneNode(true));
  }
}

customElements.define('component-name', ComponentName);

👆実際にはもっといろいろなコンポーネントへ分割されていますが、やっていることは要するにこんな感じのことです。

  • data-position属性を持つ要素をコンポーネント内で生成する。
  • 各要素のdata-position属性から、入力値とテンプレートの穴埋め箇所を紐づける。

設計はなんちゃってMVC

各ツールはビューに相当するindex.htmlと、コンポーネントやクラスに分割したJavaScriptから構成しています。

ただこれは最近になってからのことで、そもそもあまり凝ってないものなどもあります。

📁ツール名
  |- /components
  |  ∟ カスタム要素たち
  |- /controllers
  |  ∟ Controllerクラス
  |- /data
  |  ∟ json.js
  |- /services
  |  ∟ Serviceクラス
  |- /view
     ∟ index.html
  • componentsはイベントハンドラで入力値などをcontrollerへ渡す。
  • controllerは入力値やカスタム要素のdata-*などを参照してserviceへ渡す。
  • serviceは入力値の加工や/data層へのアクセスを行う。

data層のjson.jsは、いわば配列の定数です。DBのようにデータの更新は行いません。

というより全てはブラウザ上で行われますから、localStorageindexedDBを使わない限り永続化自体が不可能です。それらに関しても外部サイトへ自由にアクセスできる以上、ブラウザに業務知識に関わることを記憶させるのは危ない気がするので、使用する予定もありません。

自動テスト

志半ばですけど、テストコードも書いてます。

きっかけとなったのは、テキストからslicematchを駆使して文言を抽出するツールを作っていたときでした。抽出する対象の文言があまりにも多岐に渡り、とにかく条件分岐の多い実装になっていったため、手動で検証するのは全く現実的ではなかったのです。

このときからServiceクラスへ主要なビジネスロジックを切り出す上記の設計を導入し、そのServiceクラスに対してテストコードを書くという方針を取ることにしました。

const assert = ({ result, expected }) => {
  /* なんかいろいろ比較する */
  return [
    // 結果を配列で返す
  ]
}
const dataProvider = [
  {
    case: "XXXXの場合",
    input: "XXXX YYYY ZZZZZZZZZZ AAAA BB CCCCC",
    expected: "AAAA"
  }
];
const test = () => {
  const class = new ServiceClass();
  const result = dataProvider.map((pattern) => {
    console.info(pattern.case);
    return assert({
      result: class.Method(pattern.input),
      expected: pattern.expected
    });
  });

  console.table(result) // コンソールにこういう感じの表が出る👇
                          //   ケース名, ✅, 期待値,  実際値
                          //   ケース名, ❌, 期待値,  実際値
}

Laravelでテストを書いていたときのPHPUnitでの書き方を意識した設計になっています。

こんな感じのテストコードと対象のServiceクラスをtest.htmlで読み込んで、<button>のクリックイベントで実行しながら実装やリファクタリングなどを進めました。

つらみ

メモ帳でのプログラミングはその全てにつらみがあります。

...そんなことを書いてもしょうがないので、もうちょっと違う点について。

JSファイルをモジュールとして読み込めない

HTMLやJavaScriptのファイルは、OneDriveかエクスプローラーから直接開いています。この場合importexport文や<script type="module">などでの読み込みを行うとCORSエラーが発生します。

これを解決するにはサーバーへデプロイして https で通信するか、ローカルでlocalhostを立ち上げるしかありません。
いずれも不可能なので、使うファイルは全てHTMLで読み込んで、あとはコンポーネントやファイルの呼び出し順のルール付けを考える程度の対策しかできませんでした。

もちろん普段の業務もあるので常にプログラミングができるわけではなく、たまに浦島太郎状態になってしまうこともあります。設計は今後も見直す必要がありそうです。

最後に

以上、今年業務で行ったメモ帳プログラミングの簡単なご紹介でした。

元は業務メモの清書から始まったコーディングでしたが、そこから発展してゴリゴリにJavaScriptを書くようになってしまいました。バイオハザードをナイフ以外の武器を縛って攻略するのはこういう気分なのでしょうか。プレステのゲームはほぼ全くやったことがないのでイメージですが…

なお、プライベートでは最近購入したiPadとGithub Codespacesを使って、昨年Next.jsで作ったホームページをAstroへリプレイスする作業を進めたりしています。

やっぱりエディタはIDE、フロントエンドはTypeScriptで開発するのが最高ですね。

これを書いているいまは年末の休暇中で、Codespacesで使える今月の無料枠を29日に使い切ってしまい暇になっているところです。

来年は仕事もがんばりつつ、ブログをもっと書いていこうと思います。

Discussion