ローカルでホットリロードできる便利なブログテーマ開発環境づくり

ブログのテーマを変えたいなと思っていてずっと変えてなかった. はてなブログ Pro にするとトッフページが全文から一覧表示にできたりと ナイスな機能があって最近切り替えたものの, デザインをあててなくて崩壊させたままにしていた.

なので,この機会にデザインをつくりなおそうということで, リポジトリで管理するようにしたり 自分ごのみのローカル開発環境をつくったりしていた.

f:id:mangano-ito:20210603135356g:plain
こういうかんじ

こんな感じで Hot Module Replacement ですぐ変わってくれるようにした. 複数のブラウザですぐ確認できて楽しくなる:

複数ブラウザで確認

ページ遷移しても維持されてうれしい:

維持されている

最初: 素朴プラン

CSS をそのまま書くのはナウくないな〜ということで 適当な CSS プリプロセッサを入れたりする環境をつくることにした. 最初はいつものように Sass を入れて webpack でビルドしますかな,と思っていた.

ところがどっこい,もう PostCSS だけで十分な気分になった.

postcss-preset-env とか postcss-cssnext があれば次世代 CSS が使えてネストは書けて, mixin も postcss-mixins とか入れればできるので 自分の使いたい機能的には大体代替 OK になっている.

(しかしながら,@color-mod@apply など次世代機能の CSS 提案の状況はオススメされなくなったりして,今の時点で将来を見据えて先行して使っておいても意味はないかもしれない).

どのみち autoprefixer とか cssnano を入れるとなると PostCSS を入れるわけなので これだけでよいのはまとまりがいい気分になるし, モジュラーな感じがミニ四駆をカスタマイズする感じで無意味に楽しくなる.

f:id:mangano-ito:20210604043509p:plain
シンプル

このときは webpack すら不要で,素朴に PostCSS CLI を叩いて出力していた.

第二に: おなじみ Webpack プラン

CSS のビルドはこれで面白おかしくできるのだけれど, 確認の手間を減らしたいなという気分になった.

毎回管理画面にビルドした結果をエイヤと貼って確認すると 想像違っていたときのダメージが大きい. (Developer Tools で編集して確認したりするのだけれど めんどい!)

ので,ナイーブにブログのトップや記事ページの HTML を保存して, おなじみ html-webpack-plugin + webpack-dev-server で確認することにした.

f:id:mangano-ito:20210604043405p:plain
雑イメージ図
(描き忘れたけど開発時は MiniCssExtractPlugin.loader の代わりに
style-loader を使っている)

図で見るとわけが分からなくていいとこなしに見えるけど, この方法のメリットは Webpack の HMR (Hot Module Replacement) が使えることで, スタイルを変更したら即差し替えされて反映される. かなり体験がよくて色を少しずつ変えるとかも面倒でなくなる. browsersync でもいいわけだけど,アグレッシブでおもしろな感じ.

ところで

さらにリモートからページを取ってくる仕組みがあれば, わざわざダウンロードしてこなくてもいいのではということで, DL してくれる素朴なスクリプトを書けば 面白い確認環境がつくれるなと思った.

ので,超素朴に fetch してくる JavaScript を差し込むことにした. こうすればリアルな本番ページにローカルで開発中のスタイルで反映できて便利となる. git stash にそのときのぐっちゃぐちゃのスクリプトが残っていた:

@@ -0,0 +1,61 @@
export class Viewer {
    constructor(
        private origin: string
    ) {}

    async onEnter(url: string = this.origin) {
        console.log('Viewer#onEnter');
        await this.load(url);
        this.removeUserStyle();
        this.injectStyle();
        this.anchorOverride();
    }

    private removeUserStyle() {
        const link = document.querySelector<HTMLLinkElement>('link[rel="stylesheet"][href^="https://usercss.blog.st-hatena.com/blog_style/"]');
        link?.remove();
    }

    private removeScripts(document: Document) {
        document.querySelectorAll<HTMLScriptElement>('script').forEach(script => script.remove());
    }

    private injectStyle() {
        const style = document.createElement('link');
        style.href = window.location.origin + '/main.css';
        style.rel = 'stylesheet';
        document.head.appendChild(style);
    }

    private injectScript() {
        const script = document.createElement('script');
        script.src = window.location.origin + '/main.js';
        document.head.append(script);
    }

    private anchorOverride() {
        const anchors = document.querySelectorAll<HTMLAnchorElement>(`a[href^="${this.origin}"]`);
        anchors.forEach(anchor => {
            const href = anchor.href;
            anchor.addEventListener('click', () => this.onEnter(href));
            anchor.href = '#';
        });
    }

    private async load(url: string) {
        const content = await(fetch(url).then((response) => response.text()));
        document.open();
        document.write(content);
        document.close();
        this.removeScripts(document);
        await new Promise<void>((resolve) => document.addEventListener('DOMContentLoaded', () => resolve()));
    }
}

が,これが絶妙に使い勝手が悪い. ページ遷移するとスタイルがなくなってしまうので, ふたたび無理やり差し替えたりするわけだけど, document.write してたりするのでうまく行かなくて試行錯誤していた.

そんなとき

そもそもとして,html-webpack-plugin で指定している HTML を ローカルのではなくてリモートの HTML を取ってきてくれれば トリッキーにダウンロードして差し替える技は不要だなと思ったりした.

少し調べると,webpack-dev-server には proxy の機能があって, 任意のパスにアクセスしたらリモートの URL をもってきてくれる機能がある. なので,これを使うと狙い通り本番ブログが表示されるようになった.

{
    devServer: {
        contentBase: path.join(__dirname, 'dist'),
        compress: true,
        hot: true,
        port: 8080,
        open: true,
        inline: true,
        proxy: {
            '/': {
                target: 'https://mangano-ito.hatenablog.com',
                changeOrigin: true,
                autoRewrite: true,
            },
        },
    }
}

(webpack.config.ts)

もちろんところがどっこいこれだけではうまくいかなくて, 本当に単純に proxy するだけなので, webpack が emit したスクリプトを入れるようなこともしてくれない. 永遠に単純に本番ブログを謎の土管を通して見ているだけで ローカルのスタイルは適用できない.

完成形: Proxy して差し込む Webpack プラン

proxy でも webpack の生成物を注入してくれる便利プラグインないかな〜 なかったらつくろうかな〜と軽くググってみたら, 世の中あったらいいなはあるもので webpack-dev-server-inject-scripts という ほしかったものズバリが作られていた.

これを適用したらひとたび,スクリプトが注入されるので, HMR もゴキゲンに適用されて最高の開発環境が出来上がりはじめた. (とはいえ,初回時はうまく行かなくてリロードする必要があったりするのだけれど…)

こうなると,自前で fetch したりするスクリプトは完全に不要になって,

ただ 本番の CSS<link> 要素を削除したり,

ブログ内の遷移リンクを localhost に差し替えたりすれば,

いい感じのローカル開発環境ができあがるようになった.

求めていた体験

あとのもろもろ

あとはデプロイをいい感じにできたらよいですね,と思った. npm run deploy で反映できればよいと思った. だけど,API はないから,puppeteer で 管理画面に貼り付けて反映くんみたいなスクリプトを書くことになるだろうかと思い, それはちょっといろんな意味でよくないなと思った.

で,どうしたかというと,単純に @import ルールで, 外部に生成した CSS を読み込ませることにした. こうすれば任意の方法で publish したら反映できるようになる.

f:id:mangano-ito:20210604050324p:plain
インポートすればよい

今回は GitHub Pages に publish することにした. というのも,git push したら反映されると一番ラクでよいと思ったから. (本来の用途とちょっと違う感じはする)

なのでビルドして生成した CSS を公開ディレクトリにコピーするような GitHub Actions ワークフローを書くと push したら配置してくれていい感じになった.

f:id:mangano-ito:20210604125106p:plain
GitHub Actions でビルド

f:id:mangano-ito:20210604125222p:plain
生成物を公開ディレクトリにコミットしてくれる

このときポイントなのが .nojekyll をルートに置いておくことで, こうしないと Jekyll として GitHub Pages を公開するので, 縦横無尽に配置した CSS ファイルにはアクセスできなくなる. Jekyll を使わなければ単純に Git の構造のまま 静的ファイルを配信できるので CSS が配信できるようになる.

github.com

最終的にリポジトリはこういう感じになった.

感想

こういう紆余曲折があって ブログのスタイルは変わった. デザインは何も考えずガチャガチャやって, 後はいかにおもしろ開発環境を作るかに時間を費やしていた. 目的と手段が逆転した好例となっている.

この記事を書くためにはてなブログのヘルプを呼んでいたら, 既にボイラープレートのプロジェクトがあって, そこでは @import url("http://localhost:3000/boilerplate.css"); をデザイン画面で指定して ローカルの CSS を読み込ませる技が紹介されていて 目からウロコになっていた. 最初にコレを見ていたらやっていなかったかもしれない.