MarkdownファイルをブラウザでLive Reloadしながら編集する(Nodemon + BrowserSync)

完全に遅刻投稿ですがこっそり投稿します…


会議の議事録、タスク管理、仕様の細かい備考など、メモというものは仕事に欠かせないものです。

メモの取り方は人それぞれですが、私は好んでMarkdown形式でメモを取ります。
Marddown形式であれば、Vimでシンタックスハイライトが効きますし、後々何かしらのブログサービスに記事として残す場合にそのまま貼り付ければ大概フォーマットに沿った形式となります。もしくはMarkdownはプレーンなテキストであり、それを何らかの形式に変換することが可能な場合があります。何らか、というのは全く想定していないのですが、何らかの形に変換したいときが来たときにスムーズに対応できる、という理由からとても心が安らぎます。話は逸れますが、同様に音楽ファイルなんかも可逆圧縮形式が好きです。今のところ実際に「何らかの形」に変換したことはないんですが。

しかし、Markdownテキストは通常のエディタで見ると、一定のフォーマットに則ったただのテキストです。

# 見出し1
## 2021/3/7
本文

↑こんな感じです

ある程度の俯瞰しやすさはありますが、更にもう少し、分かりやすく見たいというときがあるのです。つまり、MarkdownテキストをHTMLで展開し閲覧したい。そんなときのために、MarkdownファイルをWebブラウザで確認しながら編集できる環境を作りました。

できあがったもの

image

機能構造

仕組みとしては、

  1. 任意のHTMLファイルをローカルサーバーで監視し、Webブラウザで閲覧する
  2. 任意のMarkdownファイルが保存 / 編集されると、それをもとにしたHTMLファイルを生成する
  3. HTMLが生成されたことを検知し、ブラウザをリロードさせる

という単純な構造です。

Express(Node.jsのフレームワーク)を使えばその機能だけで作れてしまうらしいのですが、それだと利用するライブラリに無駄が多く勉強にもならないため、少し細かいスコープで機能を実装しました。といっても全てライブラリの機能で実現してしまったので勉強も何もないのですが。

以下、各種機能の実装方法です。

1. 任意のHTMLファイルをローカルサーバーで監視し、Webブラウザで閲覧する

これはBrowserSyncというライブラリで実現しました。

Node単体でも実現できるのですが、BrowserSyncは任意のファイルをserveし、ファイルの変更を検知するとブラウザをLive Reload(自動更新)することができます。

ちなみに私はブラウザの自動更新をHot Reloadと呼んでいたのですが、Hot ReloadはLive Reloadとは異なり、アプリケーションのstateを失わずに自動更新する機能のことのようです。

https://github.com/BrowserSync/browser-sync
https://stackoverflow.com/questions/41428954/what-is-the-difference-between-hot-reloading-and-live-reloading-in-react-native

2. 任意のMarkdownファイルが保存 / 編集されると、それをもとにしたHTMLファイルを生成する

これはNodemonとmarkedというライブラリで実現しています。

Nodemonは任意のJSファイルを立ち上げ、任意のファイルの変更を検知するとJSファイルを再実行するライブラリです。

Nodemonで実行するJSファイルでは任意のMarkdownファイルを読み取ってHTMLファイルを生成する、という処理を行い、Markdownファイル編集されるたびにNodemonでJSファイルを再実行され、最新のMarkdownファイルからHTMLが再生成されるという流れになります。

(初めはこのNodemonでLive Reloadができるかと勘違いしており、無駄に時間を取ってしまいました)

markedはmarkdownテキストをHTMLに変換します。

markedはサニタイズをしないとXSSの脆弱性を持つためリポジトリ上でオススメされているDOMPurify(もしくは他のライブラリ)でサニタイズを行う必要があります。

(今回のように自分で作ったファイルを自分で開く分には気にしなくて良いと思います)

https://github.com/remy/nodemon
https://github.com/markedjs/marked

3. HTMLが生成されたことを検知し、ブラウザをリロードさせる

これも先に述べたBrowserSyncというライブラリの機能を使います。

どのファイルを検知するかは予め設定ファイルを書いておく必要があります(もしくはCLIでの実行時に引数で指定する必要があります)。

以下セクションにて、ファイル全体の細かいコードを書いていきます。

ファイルの全体像

- dist/
- node_modules/
- public/
  - index.html
  - index.css
- app.mjs 
- bs-config.js // BrowserSyncの設定ファイル
- nodemon.json // Nodemonの設定ファイル
- package.json
- yarn.lock

.mjs という、人によっては奇妙な拡張子がありますが、今回利用したのはこれを使うことでどういう考慮をする必要があるか(もしくは考慮をする必要がなくなるか)を確認するためです。様々なデメリットを考慮すると .js の方が良いと結論づけます。以下に .mjs にまつわる背景が記述されていますが、正直今の私にとっては「そうですか」という感想しか湧きませんでした。つまるところ、よく分からなかったです、すみません。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Modules

app.mjs

import marked from 'marked';
import fs from 'fs';

const headHTML = `
  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Markdown Browser</title>
    <link rel="stylesheet" href="index.css" type="text/css">
  </head>
  <body>
`

const footHTML = `
  </body>
  </html>
`

// 再実行されるたびにファイル作成処理が走るので、差分がない場合は無視したい
fs.readFile('public/index.md', 'utf-8', (errRead, data) => {
  if (errRead) { throw errRead; }
  const bodyHTML = marked(data);
  const allHTML = headHTML + bodyHTML + footHTML

  fs.writeFile('dist/index.html', allHTML, function (errWrite) {
    if (errWrite) { throw errWrite; }
    console.log('build dist/index.html from markdown');
  });
});

// アクセスするたびにファイル作成処理が走るので、差分がない場合は無視したい
fs.copyFile('public/index.css','dist/index.css', (err) => {
  if (err) { throw err; }
  console.log('copy public/index.css to dist/index.css');
});

やっていることは単純で、実行された際に、public/index.mdを読み込み、それをもとにdist/index.htmlを生成します。

ついでにpublic/index.cssファイルもdist/index.cssとしてcopyしています。

nodemon.json

{
  "ignore": [
    ".git",
    "node_modules/**/node_modules"
  ],
  "watch": [
    "public/index.md",
    "public/index.css"
  ]
}

Nodemonの設定ファイルです。

これも非常に単純で必要ないファイルを無視し、public/index.mdもしくはpublic/index.cssに変更があった場合にapp.mjsを再実行する仕組みとしています。

bs-config.js

module.exports = {
    "watch": ["dist/index.html"],
    "server": {
        baseDir: "dist",
        index: "index.html",
    },
    port: 5000,
};

BrowserSyncの設定ファイルです。

これも単(以下略)

package.json

{
  ...
  "scripts": {
    "start": "concurrently 'yarn:start-*'",
    "start-nodemon": "yarn nodemon app.mjs",
    "start-sync": "yarn browser-sync start --config bs-config.js"
  },
  ...
}

npm-scriptsでNodemon用のサーバーとBrowserSync用のサーバーを並行で起動させています。

concurrentlyはnpm-scriptを簡単に記述するためのライブラリで上記のように start-* と書くことでprefixが start- のコマンドを簡単に指定することができます。

(おそらくもっとトリッキーな使い方ができそうです)

https://github.com/kimmobrunfeldt/concurrently

この状態で

$ yarn start

とすると、自動的にブラウザのページが開きます。

index.cssはお好みで設定してください。

表示の見た目が自分好みになれば、メモがもっと楽しくなるはずです。

多分ね。

振り返り

BrowserSyncのProxyモードで簡単にできるかと思ったのですが、ProxyモードだとProxyしている場合Live Reloadはされないようでした。こちらの設定ミスかも?また、BrowserSyncではきちんとしたHTMLではないとLive Reloadされず、index.htmlにプレーンなテキストを書いても上手く反映されなかったためそこでかなり時間を取られました。本当はLive Reloadを自分で実装したかったのですが、力量が足りず断念。単純に基礎知識が足りないのでnpmパッケージのコードリーディングが全くできませんでした。WebSocketを使えば可能だとは思うので、まずはWebSocketの基本から学ぶことにします。拡張子ごとのシンタックスハイライトも入れたいですが、さすがにライブラリで良いかな……。メモの作成や一覧表示なんかもCLI上でできるようにしたいので、そこができればツールとして公開したいですね。みなさんもオレオレメモ環境を作ってどんどんメモ & 知見を増やしていきましょう。

備考

書いて気づいたんですが、これ提供してないのでサービスでもなんでもないですね。さらにいうとWebという点でも怪しいですが、もう少し最低限の機能追加をしたらパッケージとして公開します!という意気込みを以てご容赦ください。


defu@Todo:早寝早起き

Web系フルリモート受託er。 PHP/Ruby/React/個人開発/https://t.co/6lVYG57vdf

コメント