Material-UIのDrawerをこれ以上なくシンプルに使ってみる

サークルサイトを更新した際に、ReactのUIフレームワークであるMaterial-UIを使ってモバイル表示用にサイドバーをドロワー化して開閉できるようにしました。

公式サンプルは機能を十全にアピールするためにかなり複雑だったので、最低限のコードで済むようにシンプルな実装にしたものを備忘録として公開しておきます。

主な実装ファイルはこちら

github.com

React Hooksを利用して開閉状態を保持する

まずいきなりReact Hooksの知識が必要となります(Hooksについてはググってください)

const Sidebar: React.FC<HeaderProps> = ({ title }) => {
  const [drawerState, setDrawerState] = React.useState(false)

  const toggleDrawer = () => {
    setDrawerState(!drawerState)
  }

//後略

公式サンプルでは4つのドロワーを一度に表示しているのでドロワーの状態を4つ配列にしていますが、今回は1つでいいのでdrawerStateを開閉状態として定義します。初期値(アクセス時)は閉じていてほしいので初期値はfalseです。

toggleDrawer関数はメニューボタンクリック時やドロワーを閉じる際に起動する関数で、drawerStateを切り替えるだけのものです。

ドロワー本体を実装する

       <TitleGrid item sm={12} xs={12}>
          {/* モバイルドロワー部分開始 */}
          <Hidden smUp>
            <MenuButtonBox>
              <IconButton color="inherit" style={{ padding: 0 }}>
                <MenuOpen
                  fontSize="large"
                  aria-label="open drawer"
                  onClick={toggleDrawer}
                />
              </IconButton>
            </MenuButtonBox>
            <Drawer
              anchor="left"
              elevation={16}
              open={drawerState}
              onClose={toggleDrawer}
            >
              <DrawerGrid
                container
                onClick={toggleDrawer}
                onKeyDown={toggleDrawer}
              >
                <DrawerInnerGrid item alignItems="center" xs={12}>
                  <Externals />
                </DrawerInnerGrid>
                <DrawerInnerGrid item alignItems="center" xs={12}>
                  <Bio />
                </DrawerInnerGrid>
              </DrawerGrid>
            </Drawer>
          </Hidden>
          {/* モバイルドロワーここまで */}

モバイルアクセス時のみドロワーを使うので、ドロワー部分は<hidden>で囲みサイズに応じた表示属性を付与しておきます。

メニューボタンのonClick属性にtoggleDrawer関数を起動するよう設定して、<Drawer>opendrawerStateを指定することでdrawerStateによってドロワーの開閉が制御されるようになります。

あとはサンプルと同様にonClose,onKeyDown属性に状態変更の関数を指定しておくと、ドロワーのリンク以外の場所をクリックすればtoggleDrawerが起動して状態を変更、ドロワーが閉じるようになります。

ドロワーそのもののコンポーネントは自由に指定できます。

Gatsby.jsの翻訳を手伝ってみたら(肩書だけ)OSSのメンテナーになった話

お世話になっているGatsbyのドキュメントを和訳するプロジェクトが進行していたので、一部を手伝うことにしました。 こちらのリポジトリをフォークしてIssueで宣言した上で翻訳・マージすることになっています

github.com

準備までの手順

  1. Issueで翻訳する場所を宣言する、反映を確認
  2. リポジトリをフォークする(Github上でボタンポチ)
  3. git clone [my-folked-repository]
  4. cd gatsby-ja
  5. git switch -c docs/accessibility-statement(docs/accessibility-statement.mdを翻訳する場合)
  6. yarn install

環境構築でハマったとこ

gitのバージョンが古くてswitchが使えなかったのでアップデート

sudo add-apt-repository ppa:git-core/ppa
sudo apt install git`

yarn使わずnpmで生きてきたのでyarnを新規インストール

公式の通り

classic.yarnpkg.com

curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt update && sudo apt install yarn

翻訳

  1. ブランチを切る
  2. スタイルガイドに従って翻訳する
  3. pull requestを出してレビュー対応する

Githubを用いたチーム開発している人には慣れたものだと思いますが、自分は初めてだったのでかなり緊張しました。

報酬

github.com

いくつか翻訳したページの一つが無事Mergeされました!
PRのマージに連動して、メンテナーのGatsby.jsのOrganizationに追加され、公式グッズのバウチャーも頂けることになりました。

maintainerってグループに入れられたからメンテナー名乗ります ドキュメント翻訳のContributerってプロフィールに書いといた。

まさかこんな形でOSSメンテナーの肩書(ただし翻訳のみ)を手に入れられるとは…5つPRが採用されると更にグッズが貰えるようなので、意欲が刺激されますね。

Gatsby.jsでGraphQLのスキーマから型を生成して利用する

TypeScript導入時はコンポーネント内に自分でPropsの型を書いていましたが、全てのコンポーネントでこれを行うと非常に面倒かつ、GraphQLのスキーマとオリジナルの型で二重に型を生成することになるので、自動生成するプラグインを導入してコンポーネントから利用します。

gatsby-plugin-graphql-codegenの導入

npmでインストールした後、生成するtypesファイルの宛先と監視するディレクトリを指定します。 これらはGraphQLのクエリを生成する部分が指定してあればよいので、特に目立った構成変更をしていない自分の場合はREADMEの指定をそのまま利用しました。

{
      resolve: "gatsby-plugin-graphql-codegen",
      options: {
        fileName: "types/graphql-types.ts",
        documentPaths: [
          "./src/**/*.{ts,tsx}",
          "./node_modules/gatsby-*/**/*.js"
        ],
        codegenDelay: 200
      }
    },

出力先fileNameは監視するディレクトdocumentPathsと同じ場所にしてしまうと自分を参照して無限ループになるので注意。

これで、ローカルサーバーを起動した後にクエリに変更を行うとプロジェクト内で使える型データがtypes/graphql-types.tsに吐き出されます。

コンポーネント内で型を利用する

前回までは自分で型を書いていました。

interface StaticQueryProps {
  allMarkdownRemark: {
    edges: Edge[]
  }
}

interface Edge {
  node: {
    frontmatter: {
      date: string
      title: string
    }
    id: string
  }
}

const recentPost: React.FC = () => (
  <StaticQuery
    query={graphql`
      query RecentPostQuery {
        allMarkdownRemark(limit: 4, sort: { fields: frontmatter___date, order: DESC }) {
          edges {
            node {
              frontmatter {
                date(formatString: "YYYY/MM/DD")
                title
              }
              id
            }
          }
        }
      }
    `}
 // render

自動生成した型を使うために、import文を1行書いて型情報を読み込みます。 この場合、GraphQLのクエリに名称をつけておくと[Query名]Queryという名称で型情報が出力されます。

~Queryという名前をクエリにつけていたので、~QueryQueryという型にならないよう全て書き直しました…

//...import
import { RecentPostQuery } from "../../types/graphql-types"

const sportsPost: React.FC = () => {
  const data: RecentPostQuery = useStaticQuery(
    graphql`
      query RecentPost {
        allMarkdownRemark(
          sort: { fields: frontmatter___date, order: DESC }
          limit: 4
        ) {
          edges {
            node {
              frontmatter {
                date(formatString: "YYYY/MM/DD")
                title
                cover {
                  childImageSharp {
                    fluid {
                      src
                    }
                  }
                }
              }
              id
              fields {
                slug
              }
            }
          }
        }
      }
    `
  )
//render

f:id:gensobunya:20200204173243p:plain
vscode capture

VScodeの補完と参照もバッチリ効くので、もうPropsの中身を見る度にあっちこっちのコンポーネントを見る必要なし!

欠点

各Functionの返り値を明確に意識する必要が出てきます。本来なら当たり前ですが、Vanilla JSだとよしなに書いても問題なく動いてしまうので、最初は苦労しました…

Gatsby.jsでTypescriptのコンポーネントを作ってみた

サークルサイトの再構築をするにあたり、Typescriptを導入してみました。

「最近の記事」コンポーネント

GraphQLで取得したデータに型を指定する

interface StaticQueryProps {
  allMarkdownRemark: {
    edges: Edge[]
  }
}

interface Edge {
  node: {
    frontmatter: {
      date: string
      title: string
    }
    id: string
  }
}

const recentPost: React.FC = () => (
  <StaticQuery
    query={graphql`
      query RecentPostQuery {
        allMarkdownRemark(limit: 4, sort: { fields: frontmatter___date, order: DESC }) {
          edges {
            node {
              frontmatter {
                date(formatString: "YYYY/MM/DD")
                title
              }
              id
            }
          }
        }
      }
    `}
    render={(data: StaticQueryProps) =>
      data.allMarkdownRemark.edges.map(({ node }) => (
        <div key={node.id}>{node.frontmatter.title}</div>
        //  component
      ))
    }
  />
)

メリット

  • Propsとして下位コンポーネントに引数を渡す際、「あれ?このProps何入ってたっけ」といちいち上位コンポーネントのコード読む必要がなくなる(このコンポーネントだとそうなってない)(PropsTypesでもいい)
  • MarkdownRemarkあたりのデータ構造、適当にえいやで動くように書いていたものの構造をしっかり理解してコードを書けるので手戻りが少ない

なぜJAMstackを採用するのか?

はじめに

この記事はJAMstack Advent Calender20日目の記事です。

qiita.com

フレームワーク類の実装や特定のCMSが~~といった内容はこの記事には皆無です。
個人開発、企業のWEBサイト・ブログでそれぞれJAMstackを採用するメリット・デメリットについて考察します。

なお、筆者のJAMstack経験は趣味の自転車に関するブログ及び、同人サークルサイトをそれぞれGatsby.js, Hugoで運用しており、所属企業のコーポレートサイトのアーキテクチャ刷新に検討している程度の経験です。

個人開発(自分)編

当初、個人ブログはBloggerを使っていたり、GCE上にWordpressを立てて運用していたりしましたが、どうしてもデザイン自由度や広告問題、アクセス性能問題がついてまわるため、Hugoに行き着き現在ではGatsby.jsで運用されています。

運用(費用・人的)コスト

WEBサイト運営で重視したい点は(個人的に)下記三つです。

非常に残念なことに、これらをブログサービスや、ペライチさんのようなSaaSで満たそうとすると大抵有料になります。当然ですね。

しかし、Gatsby.jsやHugoで作ると上記の要件を満たすために必要なサービスは以下の2つです。

  • Github無料
  • Netlify… 無料

なんと!完全無料で思いのままにWEBサイトが作れる!

しかも、WordPressなどのCMSをセルフホストして使った場合はアップデートを随時行っていないとあっという間に悪い大人の餌食になってしまう一方、JAMstackは静的配信なので脆弱性対応は最小限で済みます。

ただし、本当に労力を割ききれない場合は(もしくは力を入れたいプロダクト・仕事が別にある場合)、はてなブログやnoteのような完全なマネージドサービスを使ったほうがよいでしょう。
自分も、フルタイムで別の仕事をしつつ、運用するサイトは2つが限界だと判断したため、この技術ブログははてなブログを使っています。

コンテンツ移植性

WordPress <=> Blogger など、サービス間のマイグレーションも、基本的にMarkdown形式で記事が残っているため、遥かに簡単に別のフレームワークに変更できます。
自分も、今度サークルサイトをVue.jsのGatsby.jsことGridsomeに変更してみる予定です。コンテンツは特に手に入れなくとも、ビルド環境とテーマを組み直せば完了です。

移行ツールなどをわざわざ用意してデータフォーマットを変更する必要はありません。

エンタープライズ

さて、企業においてJAMstackのメリットを活かせるユースケースを考えてみます。

CMS上でコンテンツを編集し、IaaS+LB+CDNで公開する王道な構成と、Gitリポジトリ+CI+静的ホスティングのJAMStack構成を比較してみます。

セキュリティ

前述の通り、静的ホスティングサービスを利用することで意識しなければならない点がだいぶ減ります。最終的にReactアプリを配信する場合などはアプリレイヤーのみ、企業側でコントロールすればよくなります。

定期的にパブリッククラウドの監査、ミドルウェアのバージョン確認、セキュリティパッチの適用作業などをこなしていく必要はなくなります。

コンテンツベンダーのいる業務運用

Qiita的にはあまり聞かない話題ですね

そこそこ大きい企業では部門ごとにコンテンツの発注先が違うことはよくあることかと思います。
CMS上でコンテンツ作成するタイプのCMSを使っている場合、コンテンツの作成お作法や運用ルールなどを新規ベンダー追加の度に伝える必要があり、情シスは死にます。

さらに、プロモーションページなど継続的に取引が発生しないコンテンツに関して1回だけのためにCMS運用教育を行うことは最高に非効率です。

代替案として、特定ディレクトリ下で機能することを前提にした静的なHTML+CSS+JSを納品してもらい、最も取引の大きいコンテンツベンダーに依頼してCMSで使えるコンテンツ形式に変換してもらうことも考えますが、責任の所在が曖昧になって…よくないことになります。

JAMStackのフレームワークの場合、納品された静的ファイルを/publicに配置してCIを回すだけで新しいコンテンツを追加できます。

プルリクエスト単位でステージングサイトを作ることができ、Gitリポジトリでコンフリクトを管理できることも大きいです。特定企業の中でしか通用しないCMSの運用ルールを覚えてもらうより、Gitの使い方に沿ってもらったほうが生産的です。

部門間

CIのHTTP hookさえキックできるのであれば、部門ごとにHeadless CMSを用意して完全にコンテンツ作成を分離しつつ、最終的に一つのWEBサイトとして公開することも可能になると妄想中です。

製品ページはContentful、プロモーションページは静的ファイルで作成、コーポレート関連の情報は今までの試算を活かすためにDrupalをHeadless化してビルド時にコンテンツ取得…なんてこともできます。

まとめ

ステージングサイトのアクセス制限をどうする?結局Gatsbyなどのフレームワークでテーマを作る部門に要望が集中するのでは?などの課題もありますが、大規模にも使える可能性を秘めているアーキテクチャだと考えています。

もっと事例が増えて、一般的に使われる技術に成長してほしいですね!

Google Tag Managerでパスごとに読み込むscriptを変更してパフォーマンス劣化を防ぐ+Adsenseタグの配信方法

eBayのスマートタグを記事ページのみ配信してみる

eBayパートナーネットワークのスマートリンク機能は、Scriptタグを配置することでページ内のebayリンクを全てアフィリンクに置き換えてくれるスグレモノ。ただ、Lighthouseのスコアを悪化させるので不要なページには置きたくない。

そこでタグマネージャーを使って記事ページのみにこのスクリプトを配信する。

f:id:gensobunya:20191109112329p:plain
trigger

トリガー設定でPage Pathを/postに指定することで、トップページやタグ記事一覧のページを対象外にして配信できる。 これまでなら面倒なJavaScriptを書かなければならなかったことを考えると大変にかんたん。素晴らしい。

2019/10からのAdsenseタグと相性悪くてクソ

<head>タグ内に配置することが必須なタグはGTMで配信しづらい。

代表例はアドセンスの自動広告タグ(2019/10最新版)。こいつは<head>内配置指定されている。

<script data-ad-client="/*your adsense id*/" async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>

回避策は古いタグを使って「HTMLタグ」を使って配信すること。機能に変化はない。

<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
  (adsbygoogle = window.adsbygoogle || []).push({
    google_ad_client: "/*your adsense id*/",
    enable_page_level_ads: true
  });
</script>

自転車ブログをHugoからGatsby.jsで書き直した

Gatsbyに変更した理由

Hugoは元々サイトビルドが爆速という理由で採用しました。事実、ローカルサーバを作る場合でも300ページ近いブログも一瞬(0.2秒程度)でビルドが終わりlocalhostで接続できるのでこの点については全く不満なし。

ただし、主に実際にWEBサイトとして運用するにあたって下記の点が課題になっていました。

  • レスポンシブな画像配信がテーマ任せで、デバイスごとに最適な配信がしづらい
  • 上記の影響で結局shortcodeというHugo特有の記法をMarkdownに書かねばならず、可搬性が落ちている
  • 画像サイズが最適でない影響でSEO悪化
  • 初期のバージョンで構築したので、git submoduleを使ったテーマのアップデートができなかった(そもそもテーマも結構いじってしまったので単純アップデートができない)
  • テーマをいじるのにGolangでフロントエンドを書くという苦行をしなければならない

特にきついのは画像配信周りとテーマの弄り難さでした。

そんなわけもあってReact.jsでフロント周りを自分で1から書きながらGatsby

採用したフレームワークなど

ほぼ自分でデザインを書くと決めていたので、採用するフレームワークも選び放題なのでなるべくモダンで、かつ最小で済むようにしました。
Gatsby-starter-blogをベースに使わないと思われるプラグインを削除して、下記のサービスやフレームワークに必要なものをインストール。
今はgatsby-starter-blog-mdxで始めるのがいいと思います

www.gatsbyjs.org

npmの文脈で全てが完結するので、新たに勉強しなければならないことが少なく割と楽ちんでした。
プログラミングではないけど、Google Tag Managerはかなり便利だったので別途記事書きます。

特に工夫という工夫はなく、各プラグインフレームワークの記載に従って実装しました。

というよりも、3ヶ月ほどかかってそれなりにハマったはずなのに、逐一メモらなかったために終わったら綺麗サッパリ忘れてしまった。どこかでハマってたどり着いた人はリポジトリ参照してください。

github.com

強いて言えば、Hugo時代の遺産であるdraftフラグを使うためにGraphQLに1行追加しました

....
    allMarkdownRemark(
      sort: { fields: [frontmatter___date], order: DESC }
      skip: $skip
      limit: $limit
      filter: {frontmatter: {draft: {eq: false}}}
...
...

悲しい作業

一番不毛だと感じたのがHugo用に書いたShortcodeと、Markdownのfrontmaterを正規表現と手動Grepで置き換える作業。
Markdownでコンテンツ管理する以上は、標準的な(最低でもGithub flavorな)Markdown記法だけに利用をとどめましょう…

Markdown以外から画像を利用するのはGatsby.jsにおけるクソ実装レベルがかなり高い行為なのでなるべくMarkdownからページ生成したほうがいいということはわかった。

移行結果

f:id:gensobunya:20191109104458p:plain
lighthouse

出来上がったものがこちら

blog.gensobunya.net

記事書いといてLighthouseスコア100じゃないのかよとツッコミたくなりますが、以前のブログは画像サイズのせいで70台などと全体的に低迷していたのでかなり改善されました。Performanceは毎回数字が変わるので飾りです。AccessibilityはもうちょいUIの実装力や色の知識を上げれば90後半になりそう。

テストに関して

ホスティングに使っているNetlifyのDeploy-preview機能を利用しました。

リンクしているGithubリポジトリのプルリクから自動的にステージングサイトを生成してくれるので、CI/CDをほぼ書かなくても実装できます。
今回、本番サイトはhugoでビルドしているため、ビルドコマンドが変更となりますがNetlifyはこんな要件にも対応しています。

プロジェクトrootのnetlify.tomlに下記のように環境毎のビルドコマンドと公開ディレクトリを指定できるので本番と違うコマンドで自動ビルド可能です。

[context.production]
  command = "npm run build"
  publish = "/public"

[context.deploy-preview]
  command = "npm run build"
  publish = "/public"

[context.branch-deploy]
  command = "npm run build"
  publish = "/public"

JavaScript同士ならnpm run buildの中身を書き換えておけばいいんですが言語ごと変わるとそれもできないのでありがたい。 管理画面でBranch deployをONにすると特定のブランチが更新されるたびにステージングサイトを作ることもできます。Netlifyマジですごい、神。