時代はサーバレスへ

以前、PHPでLINE NotifyのAPIを叩くプログラムを書きました。

関連記事: GASをclasp(CLIツール)+ TypeScriptでローカルで開発する

この時はPHPファイルをレンタルサーバーに置き、cronで定期実行させていました。

しかし、時代はサーバレスです。APIを叩くだけならGoogle Apps ScriptやAWS LambdaといったFaaSで実現できます。

今回はTypescriptを学びたかったので、claspを使ってGoogle Apps Scriptにデプロイすることにしました。claspを使うと、webpackなどの設定が不要になり、気軽にTypescriptのコードを書くことができます。

claspの主なコマンド

開発中に使う主なclaspコマンドは以下です。

$ clasp login  // script.google.comにログインする
$ clasp create // プロジェクトを作成する
$ clasp push   // tsファイルのコンパイルとGASにアップロード
$ clasp open   // 該当のGASのページをブラウザで開く
$ clasp deploy [Version] [Description] // バージョンを管理できる

claspについては下記の記事が詳しいです。

GAS のGoogle謹製CLIツール clasp

コードを書く

const properties = PropertiesService.getScriptProperties()
// APIトークンは下記ページから発行する。通知を投稿するチャンネルを選択すればトークンが発行される。
// https://notify-bot.line.me/ja/
const TOKEN = properties.getProperty('LINE_NOTIFY_TOKEN')
const FROM = new Date(properties.getProperty('FROM'))
const ENDPOINT = 'https://notify-api.line.me/api/notify'
const MILLISECONDS_OF_DAY = 86400000
const DAYS_OF_YEAR = 365
// スタンプのIDは下記アドレスに記載
// https://devdocs.line.me/files/sticker_list.pdf
const STAMPS = [
    608, // プレゼントボックス
    301, // カクテル
    269, // ハート
    268, // 虹
]

function getHeaders(): { [key: string]: string } {
    return {
        "Content-Type": "application/x-www-form-urlencoded",
        "Authorization": `Bearer ${TOKEN}`
    }
}

function getStampNumber(): number {
    const length = STAMPS.length
    const random = getRandomInt(length)
    return STAMPS[random]
}

function getRandomInt(max: number): number {
    return Math.floor(Math.random() * Math.floor(max));
}

function getPayload(): string {
    const params = {
        'message': getMessage(),
        'stickerPackageId': 4,
        'stickerId': getStampNumber()
    }

    let body = [];
    Object.keys(params).map(key => {
        body.push(key + '=' + encodeURI(params[key]));
    })
    return body.join("&")
}

function getMessage(): string {
    const now = new Date()
    const diff = getDayDiff(now)
    const [years, days] = getYearsAndDays(diff)
    return createMessage(diff, years, days)
}

function getDayDiff(now: Date): number {
    const diff = (now - FROM) / MILLISECONDS_OF_DAY
    return Math.floor(diff)
}

function getYearsAndDays(diff: number): Array<number> {
    const years = Math.floor(diff / DAYS_OF_YEAR)
    const days = diff - DAYS_OF_YEAR * years
    return [years, days]
}

function createMessage(diff: number, years: number, days: number): string {
    return `
🎉おめでとう🎉
二人が付き合ってから
${diff}日が経ちました😍
今日で${years}年と${days}日です💕
これからもよろしくね😘`
}

function send(): void {
    const options = {
        "method": "POST",
        "headers": getHeaders(),
        "payload": getPayload(),
        "muteHttpExceptions": true
    }
    Logger.log(options);
    const res = UrlFetchApp.fetch(ENDPOINT, options);
    Logger.log(res.getContentText());
}

GASのメリット

APIのトークンを「プロジェクトのプロパティ」に記述できるため、誤ってトークンが流出することを避けられる。

GASのプロパティ設定画面

PropertiesService.getScriptProperties().getProperty('KEY名')で値を取得する。FROMは2019-04-01のようにYYYY-MM-DDで定義する。

claspのメリット

  • ES6の構文が使える
  • テンプレートリテラルが使え、文字列の連結を多用せずに済む
  • Typescriptを学ぶことができる
  • GitHubでのコードの管理が楽
    • もしclasp pushがなければ、ローカルで書いたコードをGASにコピペするため煩雑
  • 今回は不使用だが、Class構文も使える

結果

PHPからGAS+Typescriptへ載せ替えができました!

GASのプロパティ設定画面

Google Apps ScriptLINEclaspTypeScript
プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer

本ブログ(Gatsby製)をAMP対応しました

GatsbyをAMP化するPluginはまだ存在しない

AMPとはウェブサイトを高速で表示するためのHTML/CSS/JavaScriptの書き方です。AMPは「Accelerated Mobile Pages」の略で、Googleはニュースサイトに対して何ができるかという問いから始まったプロジェクトです。

**その来歴から、AMPのユースケースはブログのような静的サイトにピッタリです。**しかも、AMP対応のページはGoogleがキャッシュしてくれたり、検索結果で上位に表示されたり、chromeを立ち上げた時のオススメwebページの紹介の中に掲載される可能性があったりといいこと尽くしです。

一方、**GatsbyJS(以下Gatsby)はReactでコンポーネントを記述し、SSRでHTMLを生成します。**また、Plugiプラグインを導入してカスタマイズを柔軟に行えるという特徴がああります。そうであれば、AMP化するPlugiプラグインを使って、SSR時にAMP用のHTMLを吐き出せば誰でもAMP対応のGatsby製のサイトが作れそうに思います。

実際、GitHubのissueが2019年4月に立ち40件以上のコメントが付いていおり、gatsby-plugin-ampgatsby-plugin-html2ampといったプラグインに言及されています。しかし、どちらのPluginもうまくいかないケースがあるようで、GatsbyをAMP化する完全なプラグインは未だに実現されていません。

gatsby-amp-starter-blogというレポジトリを参考に実装する

それでもAMP化を諦めずに調べていると、gatsby-amp-starter-blogというレポジトリに行き当たりました。

このレポジトリの方法でGatsby v2.23.3の当ブログでもAMP対応ができましたので手順をご紹介します。

GatsbyをAMP対応する手順を紹介します

gatsby-amp-starter-blogは最終更新が3年前であり、そのまま使うことはできないため、AMP化に必要なところだけをPickupしていきます。

**GatsbyのAMP化とは、つまりGatsbyで吐き出したHTMLをAMPに対応するように書き換えるというものです。**具体的には、以下の手順になります。

  • $ gatsby buildでHTMLを生成する
  • ampify.jsを作成し、$ node ampify.jsでHTMLをAMPように書き換える

ampify.jsgatsby-amp-starter-blogから拝借し、自分のサイトに合うように修正します。

ampifyjsなどのモジュールをインストールする

まず、ampify.jsをルートディレクトリにコピーします。

ざっくり処理の流れを追えるように省略したりコメントを書きました。

ampify.js
const recursive = require('recursive-readdir');
const fs = require('fs');
const ampify = require('ampifyjs');
const sass = require('node-sass');

const GA_TRACKING_ID = 'UA-SOME_ANALYTICS_ID-1';
const inputDir = 'public/amp';
const filesToConvert = [];

recursive(inputDir, [], (err, files) => {
  // public/ampにある拡張子がhtmlであるファイル名を取得
  for (const file of files) {
    if (file.endsWith('.html')) {
      filesToConvert.push(file);
    }
  }

  for (const fileToConvert of filesToConvert) {
    const urlPath = fileToConvert.replace(inputDir, '');
    const contents = fs.readFileSync(fileToConvert, 'utf8');
    // htmlファイルを読み込んで、ampifyjsでAMP化する
    fs.writeFileSync(fileToConvert, ampify(contents, urlPath, ($) => {
      // 自分のHTMLを書き換える
    }), 'utf8');
  }
  console.log('The site is now AMP ready');
});

次に、ampify.jsを動かせるようにしましょう。まずはnpmモジュールをインストールします。

$ npm install -S recursive-readdir ampifyjs node-sass

package.jsonにampifyamp:buildコマンドを追加します。

{
  "scripts": {
    // ...
    "ampify": "cp -r public amp && rm amp/*.js amp/*.map amp/*.css amp/*.xml amp/*.json amp/*.txt amp/*.webmanifest && mv amp public/amp && node ampify.js",
    "amp:build": "gatsby build && npm run amp:prepare && node ampify.js",
  }
}

そして、amp:buildを実行してみましょう。amp対応ファイルがpublic/amp以下に吐き出されます。$ gatsby serveでローカルサーバーを立ち上げ、http://localhost:9000/amp/posts/[slug](slugはブログ記事のslug)にアクセスします。

開発者ツールでhtmlを確認すると、imgタグはamp-imgなどamp対応のタグに書き換わっていることがわかります。

ampify.jsを書き換える

まずは、GoogleのAMP対応確認ツールにAMP化したHTMLコードを貼り付けましょう。

ツールで検出されたエラーを1つずつ潰していきます。以下では、私のサイトで検出されたエラーと解消方法を紹介します。

amp-imタグにwidthとheightを設定する

amp-imタグはwidthとheightが指定されていないとAMP対応のページと見なされません。このため、ampify.js内でamp-imgタグに画像サイズを指定していきます。

fs.writeFileSync(fileToConvert, ampify(contents, urlPath, ($) => {
  // ...
  $('amp-img').attr('layout', 'responsive');
  $('amp-img').attr('width', '450');
  $('amp-img').attr('height', '270');
  // ...
}))

また、AMPはimgタグをLazy-Loadingに対応させるための属性loadingに対応していないため、こちらを削除します。

$('amp-img').removeAttr('loading');

pictureタグをdivタグに書き換える

**pictureタグはCSSやJSを使わずHTMLだけで画面の幅に応じて画像を出し分けるタグです。**HTML5で登場しました。

Gatsbyのプラグインgatsby-remark-imagesgatsby-remark-relative-imagesは、マークダウンの![alt](/src)をimgタグを囲んだpictureタグに書き換えてくれます。

<a class="gatsby-resp-image-link" href="/static/foo.png">
  <span class="gatsby-resp-image-background-image"></span>
  <picture>
    <source srcset="/static/foo.webp 240w,
        /static/foo.webp 480w,
        /static/foo.webp 960w,
        /static/foo.webp 1244w"
        sizes="(max-width: 960px) 100vw, 960px"
        type="image/webp"
    >
    <source srcset="/static/foo.png 240w,
        /static/foo.png 480w,
        /static/foo.png 960w,
        /static/foo.png 1244w"
        sizes="(max-width: 960px) 100vw, 960px"
        type="image/png"
    >
    <img class="gatsby-resp-image-image" src="/static/foo.png"
     alt="GASの画面" title="GASの画面" loading="lazy" />
  </picture>
</a>

ビルド時にpngとwebpという拡張子で画像を作成し、それぞれ4種類のサイズを用意しています。画面幅に応じて、最適な画像サイズで配信されます。

しかし、AMPではaタグの中でpictureタグは使えません。そこで、pictureタグの中のsourceタグを削除し、imgタグだけを残すようにします。

const pictures = $('picture');

if (pictures.length > 0) {
// pictureタグ内のsourceを削除
pictures.children('source').remove();
// pictureタグをamp-imgタグに変更
pictures.each((_, element) => {
  const ampImg = $(element).html().trim();
  $(element).replaceWith(ampImg);
});
}

これで、タグを書き換えられました。

<a class="gatsby-resp-image-link" href="/static/6acb661c2b99fc28ddc98c515e3b5d5b/5b6ee/2020_06_22__0.png" target="_blank" rel="noopener">
  <span class="gatsby-resp-image-background-image"></span>
  <amp-img class="gatsby-resp-image-image i-amphtml-element i-amphtml-layout-responsive i-amphtml-layout-size-defined i-amphtml-layout"
    src="/static/foo.png"
    alt="GASの画面" title="GASの画面" layout="responsive"
    height="270" width="450" i-amphtml-layout="responsive"
  >
    <i-amphtml-sizer slot="i-amphtml-svc" style="padding-top: 60%;"></i-amphtml-sizer>
    <img decoding="async"
      alt="GASの画面" src="/static/foo.png" title="GASの画面"
      class="i-amphtml-fill-content i-amphtml-replaced-content"
    >
  </amp-img>
</a>

CSSの読み込み方法を変更する

GatsbyはCSSをmoduleとしてコンポーネント内でimportすると、CSSをJavaScriptオブジェクトとして扱います。

import React from "react"
import style from "./meta.module.css"

export default function Meta({ children }) {
  return (
    <h1 className={style.title}>{children}</section>
  )
}

JSのビルド後のクラス名は以下のように変換されます。

Meta-module--title--29eD7

これは[コンポーネント名]-[モジュール名]--[クラス名]--[hash]に対応しています。目的は、クラス名を重複させず、コンポーネントに当てるCSSのスタイルを一意にするためです。

今回のAMP対応ではhashの扱いが鬼門です。

上記のような処理をしていない場合は、単純にCSSファイルを文字列化してビルド後に生成されるHTMLに埋め込めば、AMPページでもCSSを当てることができます。

しかし、hashはビルドごとに変化します。つまり、ビルド後にhashが決められたCSSファイルを読み込んで、AMP化するHTMLに埋め込む必要があるのです。ただラッキーなことに、Gatsbyはビルド時にCSSを単一のファイルにまとめてくれています。

そこで、webpackの生成物webpack.stats.jsonからバンドルされたCSSファイルを取得します。

webpack.stats.json
{
  "namedChunkGroups": {
    "app": {
      "assets": [
        "webpack-runtime-60beaf2db0953a9c3aa4.js",
        "webpack-runtime-60beaf2db0953a9c3aa4.js.map",
        "styles.1aa66f171d481e9f3c38.css", // ビルドされたCSS
        "styles-0dd9b16d06f2e4f550cc.js",
        "styles-0dd9b16d06f2e4f550cc.js.map",
        "framework-7656862d80676d58607a.js",
        "framework-7656862d80676d58607a.js.map",
        "532a2f07-7b955a90846d24fe3005.js",
        "532a2f07-7b955a90846d24fe3005.js.map",
        "app-2898dff8cdf9028680fe.js",
        "app-2898dff8cdf9028680fe.js.map"
      ],
    },
    // ...
  }
}

今回ビルドされたCSS名はstyles.1aa66f171d481e9f3c38.cssです。このCSSを読み込み、文字列に変換します。

const webpackStats = JSON.parse(
  fs.readFileSync('public/webpack.stats.json')
);

// cssのファイル名を取得する
const files = webpackStats.namedChunkGroups.app.assets
  .filter((file) => file.endsWith('.css'));

// node-sassでCSSを文字列にする
let css = files.map((file) => sass.renderSync({
  file: `public/${file}`,
  outputStyle: 'compressed'
}).css.toString()).join('');

// SVGに当てるCSSも読み込みます
css += sass.renderSync({
  file: 'src/assets/amp/svg.scss',
  outputStyle: 'compressed',
}).css.toString();

これでAMPページに通常ページと同じCSSを適用できました。

Netlifyにデプロイする

netlify.tomlを書き換えて、ビルドコマンドを変更します。

[build]
  publish = "public"
- command = "npm run build"
+ command = "npm run amp:build"
[build.environment]
  NPM_VERSION = "6.12.0"

Search ConsoleでAMP対応を確認する

Search ConsoleのURL検査の項目から、サイトがAMPに対応しているかチェックできます。https://panda-program.com/amp/posts/clasp-typescript/を検査してみましょう。

Search Console

AMPに対応していることが確認できました。

これから対応すること

amp-imgタグはwidthとheightを指定する必要があります。私のサイトの画像は縦横比が様々なので、縦横を固定の数値で指定すると画像が歪んでしまいます。

アスペクト比で記述することもできるのですが、どちらにしろアップロードしている画像自体を全て同じサイズで作成しなければいけないなと思っています。

まとめ

GatsbyでAMP対応のページを作成できました。導入しているプラグインによっては、さらに別の対応が必要になるかもしれません。

当サイトで実行しているampify.jsはGitHubで公開していますので、AMP化の際には参考にしてみてください。正式な対応には、Gatsbyの公式からAMPに対応する方法が公開されるのを待ちましょう。

ちなみに、Next.jsはAMP Optimizerでサイトを手軽にAMP化できます。最初からAMP対応のサイトを作るなら、Next.jsの方がいい選択肢だと思っています。ご参考まで。

私はGatsbyもNext.jsもどちらも好きです 😊

(2020/7/11追記)AMPページと元のページでアクセスが分かれてしまうため、SEOを考慮してAMPページを削除しました。AMPページ1本に絞ればURLが分かれる問題は解決できますが、今回は見送ります。

参考

GatsbyJSAMP
プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer

LaravelでBladeファイルのホットリロードを実現する

Laravelでフロントの画面を開発する際に、ホットリロード(hot reload)を使えると便利です。

ホットリロードは指定したファイルを監視し、ファイルの内容に変更があると、わざわざブラウザをリロードをしなくても変更が自動で反映される機能です。

関連記事: LaravelにCircle CIを導入して実行結果をSlackに通知する

BladeファイルでDOMやCSSを変えた時に小まめに画面をリロードするのって煩雑ですよね。

実は、LaravelにはJavaScriptやCSSファイルの変更を検知するWebpackのホットリロードの機能が既に組み込まれています。

$ npm run hotというコマンドでホットリロードを利用できます。

一方、Blade(PHPのテンプレートエンジン)ファイルはwebpackのファイル監視の対象外です。

このため、Bladeを使ってフロント画面を作成するにあたり、ホットリロードを実現するためにはBrowsersyncというnpmモジュールを導入します。

Browsersyncを使うことで、bladeファイルを変更して保存をしただけで、ブラウザに変更が即時反映されます。

解説パンダくん
解説パンダくん

ホットリロードのおかげでいちいちブラウザを更新する手間が省けるんだね。

Browsersyncを導入する方法

Laravelのプロジェクトルートで$ npm install -S browser-sync browser-sync-webpack-pluginを実行します。

$ npm install -S browser-sync browser-sync-webpack-plugin
+ browser-sync-webpack-plugin@2.2.2
+ browser-sync@2.26.7
added 146 packages from 143 contributors and audited 20798 packages in 14.919s
found 0 vulnerabilities

インストールに成功しました。

webpack.mix.jsに設定を追加する

webpack.mix.jsにBrowsersyncの設定を追加します。

mix.browserSync({
    proxy: {
        target: "http://127.0.0.1:8000",
    },
    files: [
        'resources/views/**/*.blade.php',
    ],
});

Browsersyncのプロキシモードを利用して、PHPが動作しているローカルサーバーを監視対象にしています。

この他にも設定のoptionは存在しますが、最低限これだけあれば動作します。

以下でオブジェクトの各フィールドの解説をします。

proxyにローカルサーバーのアドレスを記述する

proxyのtargetフィールドには、開発中のローカルサーバーのアドレスを記述しましょう。

Laravelはartisan serveコマンドを使ってローカルサーバーを立ち上げることができるので、このアドレスを記述すると良いでしょう。

$ php artisan serve
Laravel development server started: http://127.0.0.1:8000

DockerやHomesteadを使っている場合は、プロキシのtargetのアドレスを書き換えてくださいね。

filesに変更を監視するファイルパスを記述する

filesには配列で監視対象のファイルパスを記述します。

今回はresources/views配下の全てのbladeファイルを監視対象にしています。

ホットリロードの実行方法

ここまでくると、あとは下記のコマンドを実行するだけです。

  1. $ php artisan serveを実行する(もしくは、ローカルサーバーを立ち上げる)
  2. $ npm run watchを実行する
$ npm run watch

       Asset     Size   Chunks             Chunk Names
/css/app.css  814 KiB  /js/app  [emitted]  /js/app
  /js/app.js  591 KiB  /js/app  [emitted]  /js/app
[Browsersync] Proxying: http://127.0.0.1:8000
[Browsersync] Access URLs:
 ----------------------------------------
       Local: http://localhost:3000
    External: http://192.168.100.128:3000
 ----------------------------------------
          UI: http://localhost:3001
 UI External: http://localhost:3001
 ----------------------------------------
[Browsersync] Watching files...

開発画面が立ち上がれば成功です。

パソコンとスマホのサイズでの画面同時確認でレスポンシブ対応を加速させる

パソコンの画面幅のブラウザとスマホの画面幅のブラウザを同時に立ち上げることにより、レスポンシブデザインでのサイト構築がとても便利になります。

画面幅が変わることによるデザイン崩れを早期に発見できるからです。

同じwebページをパソコンとスマホの画面サイズで確認している

しかも、Browsersyncはデフォルトで複数のブラウザの動作を同期しています。

スマホの画面幅で表示しているページを操作するだけで、もう一方の画面も自動で同様の動作をするため、ブラウザ操作の二度手間が省けるんです。

Browsersyncの設定画面

Browsersyncの設定はhttp://localhost:3001で確認・変更できます。

手元のスマホで開発画面を確認する

また、Externalの行に表示されているアドレス(上記ではhttp://192.168.100.128:3000)には、同一ネットワーク上の端末ならどれでもアクセスできます。

例えば、パソコンも自分のスマホも家のwi-fiに接続している場合、スマホでこのアドレスにアクセスすると、Laravelで開発中の画面を表示できます。

デプロイ不要でスマホから開発画面を確認できるのはとても便利ですね。

Laravel
プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer

CSSが苦手なエンジニアがTailwind CSSを使ってサイトを作ってみた

Tailwind CSSはユーティリティファーストのCSSフレームワークです。 Tailwind CSSの特徴は、「1つのクラス名は1つのstyleに対応する」です。例えばmx-autoは「マージンの横方向をautoにする」というものです。

そして、Bootstrapとの違いは「button」のようなコンポーネントが存在しないことです。Tailwind CSSは、ユーティリティとして提供されるクラスを組み合わせてコンポーネントを作り上げるのです。

関連記事

Tailwind CSSと他のCSSフレームワークを分ける最大の特徴は、Tailwind CSSで作られたサイトにはいわゆる「bootstrap臭」を感じないところです。CSSの組み合わせ方は人によって異なるため、同じようなデザインにはならないからです。

Webサイトを手軽に作りたい。けれども、CSSを書くのは煩雑で苦手だと感じる方は少なくないと思います。

解説パンダくん
解説パンダくん

本記事は最低限のCSSの知識でウェブサイトをオシャレに作りたいサーバーサイドエンジニアにもオススメです。

本記事ではTailwind CSSの特徴を紹介した後、私が製作したサイトのパーツをサンプルとして、Tailwind CSSを使えばCSSを書くことなくデザインを実装できることを解説していきたいと思います。

Tailwind CSSの特徴

Tailwind CSSの特徴をいくつか紹介します。

・ CSSを当てるためのクラス名が揃っている
・ 値の追加や上書き、削除などカスタマイズが可能
・ 共通箇所と、差異のある箇所を適切に切り出せる
・ 何も考えずにレスポンシブ対応が可能

1つずつ詳しく見ていきましょう。

CSSを当てるためのクラス名が揃っている

多くのCSSフレームワークは、クラス名を当てることでCSSを記述せずにスタイルを整えることができます。Tailwind CSSも同様の機能を持っています。

例えば、個人開発で作ったCreepy Nutsのファンサイトのボタンは下記のようなクラス名を当てています。

黒くて丸いトップに戻るボタン

<a class="bg-black text-white font-bold py-5 px-10 rounded-full shadow-xl hover:bg-gray-dark hover:text-white">
  <span class="text-lg font-light">トップに戻る</span>
</a>

クラス名を読むだけでもパーツの特徴が伝わってきますね。aタグのクラス名とCSSプロパティと値は以下のように対応しています。

.bg-black { background-color: #1b1c1d; }
.text-white { color: #fff; }
.font-bold { font-weight: 700; }
.py-5 {
  padding-top: 1.25rem;
  padding-bottom: 1.25rem;
}
.px-10 {
  padding-right: 2.5rem;
  padding-left: 2.5rem;
}
.rounded-full { border-radius: 9999px; }
.shadow-xl { box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); }

クラス名はプロパティと値を短縮した名前なので、単にCSSを書くよりも記述量を削減できます。

また、それぞれのクラス名と対応する値は公式ドキュメントに掲載されています。興味のある方は、一例としてpaddingを見てみてください。

値の追加や上書き、削除などカスタマイズが可能

Tailwind CSSはカスタマイズが柔軟に行えるので、元々設定されていない値を追加することもできます。例えば、paddingならp-2padding: 0.5remp-3padding: 0.75remというように、あらかじめ値を設定してくれています。

しかし、paddingのクラス名はp-64が最大です。つまり、padding: 16remが組み込みのpaddingの上限です。一方、実務ではより大きなpaddingを必要とする場面があるでしょう。

そのような時には、tailwind.config.jsをカスタマイズしてクラス名を自由に追加できます。

tailwind.config.js
module.exports = {
  theme: {
    extend: {
      spacing: {
        '14': '3.5rem',
        '72': '18rem',
        '84': '21rem',
        '96': '23rem',
      },
    },
  },
}

これで、p-14, p-72, p-84, p-96が使えるようになりました。 Tailwind CSSはPostCSSで書かれており、JavaScriptで設定を記述できるので、このように柔軟なカスタマイズが可能になります。 これがTailwind CSSの大きな特徴といえます。

また、値の上書きも可能です。 先ほどのボタンを作るに当たり、私はデフォルトの黒色を変更しています。

tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        black: {
          default: '#1b1c1d',
        },
      },
      spacing: {
      // ...
      },
    },
  },
}

これにより、bg-blacktext-blackなどのクラス名で当たる色が#1b1c1dに上書きできます。

元々bg-blackの色の指定は#000000です。但し、この色をそのまま使うとキツい印象を与えてしまうため、深い青である#1b1c1dを使ってキレのある黒を表現しています。

黒くて丸いトップに戻るボタン

クラス名と色の対応はTailwind CSSのカラーパレットに掲載されています。ぜひ一度ご覧になることをお勧めします。どの色も美しく、眺めているだけでも色に対する感覚が養われること間違いなしです。

コンポーネント単位のクラス名はない

Tailwind CSSにはbtncardといったクラス名は存在しません。作者Adam Wathan氏は自身のブログでその理由を詳細に説明しています。

HTMLとCSSの関係について実践的でロジカルな彼の探求の成果をぜひ一読して頂きたいのですが、時間のない読者の方のために端的に説明すると記事の主張は「CSSでも継承よりコンポジションを好む」ことだと私は読み取りました。

つまり、「.btnを継承する.btn—primaryや.btn—secondaryを作るのではなく、クラス名を1つ1つ組み合わせてコンポーネントを作る」方が複雑なコンポーネントを作るときもシンプルに管理できることです。

これが、Tailwind CSSがユーティリティファースト(utility-first)であると標榜する理由なのです。ユーティリティファーストという言葉は、公式ドキュメントのトップページで使われており、Tailwind CSSを特徴付ける考え方と言えるでしょう。

A utility-first CSS framework for rapidly building custom designs.

しかし、自分で一からボタンやカードといった基本的なコンポーネントを作る必要はありません。ドキュメント内にボタンやカード、フォームといったWebサイトでよく使われるコンポーネントのサンプルがあるからです。このコンポーネント群を基礎にして、自分なりのアレンジを加えていきましょう。

(ちなみに、コンポーネントのみならずページ単位のテンプレートが利用できるTailwind UIというサービスもあります。こちらは現在有料で利用者を募集しています)

小さいものを組み合わせるという考え方は、UNIXの「Small is beautiful」という設計思想にも通じるところがあります。

小さなプログラムは、単独では大したことはできない。ほん1つか二つの機能を実行するだけだ。しかし、それらを様々に組み合わせることで、真のパワーを発揮する。部分の総和は全体よりも大きくなり、大きくて複雑な作業も簡単に処理できる。 UNIXという考え方―その設計思想と哲学

「プログラム」を「クラス」に、「作業」を「パーツ(コンポーネント)」と読み替えると、まるでTailwind CSSについて語っているように思えますね。

何も考えずにレスポンシブ対応が可能

レスポンシブ対応に、いつも頭を悩まされる人は多いと思います。ブレイクポイントをいくつ設定するのか、それは何ピクセルなのか。Media Queryを書くのも面倒ですよね。

Tailwind CSSはこの煩雑さを解決してくれます。下記のブレイクポイントを設定してくれているからです。

/* Small (sm) */
@media (min-width: 640px) { /* ... */ }

/* Medium (md) */
@media (min-width: 768px) { /* ... */ }

/* Large (lg) */
@media (min-width: 1024px) { /* ... */ }

/* Extra Large (xl) */
@media (min-width: 1280px) { /* ... */ }

私は個人開発でサイトを作る際は、ブレイクポイントをmdの一点にし、スマホ・タブレットとPCで分けています。

ヘッダーは下記のように書いています。

<div className="h-16 flex items-center md:max-w-screen-md md:mx-auto">...</div>

これはmax-w-screen-md mx-autoはmd以上の幅でスタイルが当たるということを示しています。

サイトのヘッダー

レスポンシブ対応はサイトのレイアウトやパーツのデザインと密接に関連しているので、そもそもデザインがまずい場合はまずデザインを見直す必要があります(Googleに「レスポンシブ ウェブデザイン パターン」という記事があり、レスポンシブデザインを考えるのにお勧めです)。少なくともエンジニアがレスポンシブ対応のためにCSSを学ばなければならないコストは格段に下がります。

(ちなみに、サイトのロゴの箇所に栗とナッツを配置しているのは、この絵文字がCreepy Nutsの愛称だからです。彼らのダサカッコ良さを表現するのに最適だと考えて配置しているのですが、一般の方からすると単にダサく思えますよね。ファンからは好評なのですが…。)

コメントとカードのパーツをご紹介します

では、実際にイケてるパーツを簡単に作れるということを、引き続き私が個人開発で製作したサイト(Creepy Nutsファンサイト)を例に説明していきます。

コードはReactですが、HTMLさえ知っていればReactの知識がなくても読み進められます。

カードのコンポーネント

CDを紹介するカードコンポーネント

VerticalCard.tsx
const VerticalCard: React.FC<Props> = ({ title, englishTitle, imageUrl, releasedAt }) => (
  <Link href={DISCOGRAPHY_ID_PATH} as={`${DISCOGRAPHY_PATH}/${englishTitle}`}>
    <a className="vertical-card">
      <img src={imageUrl} alt={title} width={150} height={150} />
      <div className="p-4 text-center">
        <p className="mb-2 text-gray-darkest">{title}</p>
        <p className="text-gray-dark">
          <span>{releasedAt}</span>
        </p>
      </div>
    </a>
  </Link>
)

上記、クラス名を解説していきます。aタグにvertical-cardというクラス名がありますね。先ほど「btnやcard」というクラス名はTailwind CSSに存在しないと書きましたが、どういうことでしょう。一旦スキップして後ほど解説します。

divタグはp-4text-centerを当てています。これでdiv内のテキストが中央揃えになります。

pタグではmargin-bottomで余白を確保したり、text-gray-darktext-gray-darkestで色の濃さを指定しています。

さて、vertical-cardです。これは自分で設定したクラス名です。scssを見てみましょう。

.vertical-card {
  max-width: 150px;
  @apply bg-white rounded-lg overflow-hidden shadow-xl mx-auto my-6;
}

@applyディレクティブを使うことで、Tailwind CSSのクラス名を自分が設定したクラスの中で利用できます。max-widthをインラインで書くことを避けたかったため、vertical-cardという名前をつけて切り出したのです。

ちなみに、Creepy Nutsファンサイトの中でこのようにCSSを書いている箇所は他に3箇所しかありません。

私は仕事でCSSを書かないエンジニアです。しかし、この開発体験の良さからTailwind CSSの汎用性の高さを思い知り、同じ境遇のエンジニアの武器になると確信しています。

コメントのコンポーネント

次に、コメントのコンポーネントを紹介します。

動画に対するコメント

Comment.jsx
const Comment: React.FC<Props> = ({ comment, formattedCreatedAt, liked, handleClick }) => (
  <div className="py-4" key={comment.id}>
    <p className="flex justify-between py-4 px-6">
      <span className="text-gray-dark">{formattedCreatedAt}</span>
      <span className="text-gray-dark">{comment.username}</span>
    </p>
    <div className="mx-4 relative rounded-lg shadow-lg">
      <span className="comment-left-top-icon text-4xl">{comment.emoji}</span>
      <p className="px-10 pt-10 py-6 text-gray-darkest leading-relaxed">{comment.body}</p>
      <p className="pb-4 flex justify-end">
        <button className="focus:outline-none" type="button" onClick={handleClick}>
          <span className="pr-2 text-xl">
            {liked ? <Icon className="text-orange" name="thumbs up" /> : <Icon color="grey" name="thumbs up" />}
          </span>
        </button>
        <span className="pr-10 text-lg text-gray">{comment.likeCount}</span>
      </p>
    </div>
  </div>
)

Like機能のbuttonタグにfocus:outline-noneというクラス名があります。これはその名の通り、focusが発生すれば、擬似クラス(focus:)に付与したプロパティを有効にするということです。ここでは、outlineがなくなるということですね。

公式ドキュメントには他にもhoverやactiveといった擬似クラスが紹介されています。

なお、コメントの左上で栗やナッツを表示している.comment-left-top-iconは自分でCSSを書いています。

/* コメントの左上の栗かピーナッツのアイコン */
.comment-left-top-icon {
  position: absolute;
  left: -12px;
  top: 4px;
  text-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
}

Tailwind CSSのメリットとデメリット

私が小〜中規模のサイトを作った手応えを紹介していきます。これは非デザイナーである個人開発者の感想なので、絶対的なメリット・デメリットではありません。

メリット

・ 誰でもLook and Feelが良いサイトが作れる
・ デザインの統一感が出せる
・ 「Tailwind CSS」を使っていることをユーザーが意識しない
・ CSSサイズ肥大化によるパフォーマンス低下を避けられる

「誰でも見た目がいいサイトが作れる」というところは、私が一番気に入っているDJ松永のプロフィールページを紹介させてください。一度ページをご覧いただきたいのですが、このページは友人にもカッコいいと好評でした。

DJ松永のプロフィールページ

Tailwind CSSを使っていて、誰でも感じのいいサイトを作れる威力を感じたポイントでした。

また、Tailwind CSSのUtilityの中からクラス名を選んでCSSを当てていくので、デザインが統一されているという印象を与えることができます。

この特徴は公式ドキュメントの「Tailwind CSSはデザインシステムを作るためのエンジンである」という言葉にも表れています。

Tailwind is more than a CSS framework, it’s an engine for creating design systems. (Designed to be customized

そして、bootstrapを使っているサイトは「bootstrap臭がする」と揶揄されたり、Material UI製のサイトは「Googleのツールのようだ」と言われるのとは異なり、クラス名を覗かない限りTailwind CSSではTailwindを使っていることがわかってしまうということもないでしょう。コンポーネントはUtilityの組み合わせであるからです。

最後に、Tailwind CSSにはPurgeCSSが組み込まれています(version 1.4から。それまではpostcss.config.jsに正規表現の記述をする必要があった)。Productionでのビルド時にはViewファイルで使われているクラス名だけを抽出するため、不要なスタイルをCSSにコンパイルしません。

このため、CSSのサイズを無闇に肥大化させず、CSSの読み込み速度の低下を免れるでしょう。PurgeCSSの恩恵はhtmlでもvueでもJSXでも簡単に受けることができます。

(なお、上記サイトはNext.jsで作っているので、私のtailwind.config.cssのpurgeの箇所は下記のように記載しています)

tailwind.config.css
module.exports = {
  purge: ['./src/Components/**/*.tsx', './pages/**/*.tsx'],
  // ...
}

これらのメリットを考慮するとTailwind CSSは、他のCSSフレームワークと比べてかなり筋がいいと考えています。

そして、何より公式が動画教材を用意してくれていいます。その英語が聞き取りやすく、またTailwind CSSの機能を一通りカバーしていることもメリット1つです。

デメリット

・ Semantic UIやMaterial UIと組み合わせが悪い

デメリットとしては、Semantic UIやMaterial UIのようなコンポーネントが構築済みのツールとの相性がよくないことです。実際にSemantic UIのコンポーネントにTailwind CSSのクラス名を当ててみたところ、DOMの階層の関係でスタイルが当たらなかったことがありました。

滅多にないケースだと思いますが、以前「Tailwind CSSとMaterial UIを組み合わせてシステムを作りたい」という相談を受けました。食い合わせが悪いことを説明して納得してもらうことができたのですが、そのように考える人も少なくないのだろうと思い、デメリットとして挙げておきます。

想定されるユースケース

Tailwind CSSのユースケースとして最適なのは、個人開発やデザイナーがいないスタートアップや新規プロジェクトでしょう。

CSSが苦手なエンジニアでもいいデザインが作れるため、デザイナーのリソースがない場合に開発の追い風となってくれます(Tailwindは追い風の意味)。

CSSを管理する手間が省けることは、エンジニアにとってはとても楽です。BEMでは、自分でBlock, Element, Modifierの名前を考える必要がありますが、命名における脳のリソース消費量は侮れません。

Tailwind CSSでは基本的に命名をしないので、瑣末になりがちなModifierの命名に頭を悩ませることもなく、レビューを受けてクラス名を変更する手間もありません。 エンジニアが思考停止でスタイルを当てられる恩恵は少なくないでしょう。我々エンジニアは脳のリソースをサーバーサイドやフロントエンドのプログラムのクリーンな設計に費やしましょう。

新規開発で導入することがベストですが、もし既存のプロジェクトにTailwind CSSを追加するなら、既存のクラスのプロパティを分解して、tailwind.config.jsに1つずつプロパティを追加していくことから始めることをお勧めします。「小さいプログラムを組み合わせる」UNIXの発想ですね。これがTailwind CSSのベストプラクティスです。

まとめ

いかがでしたでしょうか。Tailwind CSSの利点、思想、使い方を紹介してきました。

開発者のAdam氏のブログ記事では 「継承よりコンポジション」「関心の分離」 というフレーズが何度も登場します。彼はフルスタックのエンジニアであり、プログラミングの原理原則をCSSに適用する方法を考え抜いているという印象を受けました。

また、「共通のクラス」を抽出するタイミングは、繰り返しが生じたときだと記事中で説明されています。これは 「早すぎる抽象化を避ける」 という原則にも通じます。Adam氏が一流のプログラマであることの証左ですね。

ちなみに、彼はLaravelの開発者のTaylor氏とも友人で、Tailwind CSSでLaravel Vaporの美しいサイトを作成しています。「会社で導入を説得するためにプロダクションで使われている事例が欲しい」のであれば、このサイトを紹介してみましょう。

Laravel Vapor トップページ

さて、Tailwind CSSはCSSを当てるための1つの手段であり、あくまで素晴らしいデザインをシンプルに実現し、スタイルの管理を追求するためのツールです。その成果はサイトをデザインするデザイナー、そして実装するエンジニアの手に掛かっています。

Tailwind CSSという追い風を受けて、スタイル管理の沼に足を取られず進んでいきましょう。それでは、Bon Voyage!

Tailwind CSS
プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer

GatsbyJSをNetlifyにデプロイするときの最適なキャッシュの設定を紹介します

**Netlifyとは、静的サイトのためのホスティングサービスです。**Netlifyへのデプロイの手順は簡単な上にパフォーマンスが高くスケーラブルであるため、ReactやVue.jsといったフロントエンドのJavaScriptフレームワークで作られたサイトと相性が良いです。なお、JamstackはNetlifyによって提唱された技術スタックです。

GatsbyJSとは、React.js製の静的サイトジェネレータです。SSRをすることでビルド時に最適化された静的ファイルを生成するため、サイトの表示速度が爆速になります。

GatsbyJSの勉強には「GatsbyJSで実現する、高速&実用的なサイト構築」という本がオススメです。GatsbyJSの作りやGraphQLの使い方などが解説されています。

この記事では、GatsbyJSのファイルごとのキャッシュの違いを記載した後、GatsbyJS製のサイトの表示速度をさらに爆速にするためにNetlifyでの最適なキャッシュ設定を紹介します。

関連記事: Vercel + GatsbyJSの最適なキャッシュ設定を紹介します

GatsbyJSのファイルごとのキャッシュの設定を知る

「GatsbyJS公式推奨のキャッシュ設定を理解する」という記事で、GatsbyJSが生成するファイルごとにキャッシュ戦略が異なることを紹介しました。

GatsbyJSのキャッシュ戦略とは、ファイル名が変わらないものはキャッシュせず、ビルドのたびにファイル名が変わるものはキャッシュするというものです。

ビルドごとに名前が変わるファイルは、cache-controlヘッダーにmax-age=31536000を付与して1年間キャッシュします。これはビルドのたびにファイル名に一意なハッシュが与えられ、サイトにアクセスするたびに読み込むファイルが変わるため、ブラウザは古いファイルを読み込むことがないからです。

解説パンダくん
解説パンダくん

例えばapp.jsは出力時app-c2875e4f24d448537cff.jsのようになるんだよ。ハッシュ値がつくからファイル名はビルドのたびに変わるんだね。

反対にpage-data.jsonなどはビルドするとファイルの内容が変わるものの、ファイル名は変わらないため、キャッシュしないのがGatsbyJS公式の推奨設定です。

gatsby-plugin-netlifyでキャッシュヘッダーを設定をする

Netlifyのレスポンスに任意のヘッダーを付与するためには、_headersというファイルを作成します。

GatsbyJSにはNetlify用のプラグインgatsby-plugin-netlifyがあるため、このプラグインを利用する方が自分で_headersに設定を書くより簡単です。gatsby-plugin-netlifyはビルド時に_headersを出力してくれるプラグインだからです。

ますはインストールをしましょう。

$ npm install gatsby-plugin-netlify

次に、gatsby-config.jsのpluginsにgatsby-plugin-netlifyを追加します。

そして、「GatsbyJS公式推奨のキャッシュ設定を理解する」という記事に記載の通り、コンテンツごとにキャッシュを設定します。

gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: 'gatsby-plugin-netlify',
      options: {
        headers: {
          '/*.html': [
            'cache-control: public, max-age=0, must-revalidate'
          ],
          '/page-data/app-data.json': [
            'cache-control: public, max-age=0, must-revalidate'
          ],
          '/page-data/*': [
            'cache-control: public, max-age=0, must-revalidate'
          ],
          '/static/*': [
            'cache-control: public, max-age=31536000, immutable'
          ],
          '/icons/*': [
            'cache-control: public, max-age=31536000, immutable'
          ],
          '/media/*': [
            'cache-control: public, max-age=31536000, immutable'
          ],
          '/sw.js': [
            'cache-control: public, max-age=0, must-revalidate'
          ],
          '/**/*.js': [
            'cache-control: public, max-age=31536000, immutable'
          ],
          '/**/*.css': [
            'cache-control: public, max-age=31536000, immutable'
          ],
        }
      }
    }
  ]
}

上記の記述ができたら、ビルドしてみましょう。

$ npm run build

すると、public配下に_headersというファイルが生成されました。ファイルの中身は以下の通りです。

/public/_headers
## Created with gatsby-plugin-netlify

/*.html
  cache-control: public, max-age=0, must-revalidate
/page-data/app-data.json
  cache-control: public, max-age=0, must-revalidate
/page-data/*
  cache-control: public, max-age=0, must-revalidate
/static/*
  cache-control: public, max-age=31536000, immutable
/icons/*
  cache-control: public, max-age=31536000, immutable
/media/*
  cache-control: public, max-age=31536000, immutable
/sw.js
  cache-control: public, max-age=0, must-revalidate
/**/*.js
  cache-control: public, max-age=31536000, immutable
/**/*.css
  cache-control: public, max-age=31536000, immutable

これでGatsbyJSが生成するファイルをNetlifyで配信するときの最適なキャッシュの設定ができました!

GatsbyJSNetlify
プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer