unsweetsを3にした

サイト更新した。

自分のウェブサイトを更新した。一応バージョン的には3扱いだけど細かいバージョニングは面倒なのでバージョン番号については深い意味はない。

<unsweets/> source code

今更だけどそもそもこのサイト何?と聞かれるとサイトに書いてある通り自分の作ったアプリケーションの公開場所として利用している。本当はもっとゴテゴテさせたい気もするけど、ごてごてさせるのがコンセプトと合わず取り入れられない・メンテナンスが面倒くさいなどといった理由でココ最近はペライチのままだ。

サイト名に関しては響きだけで決めたもの。甘いものが好きなのでsweetsを入れたかったが、あまりにググラビリティが低いと思ったのでなんとなくunを入れた。甘い物でなくなってしまっているように見えるが。

技術的な話

ペライチにもかかわらずnext.jsを利用した。Typescript併用。最近業務でもちょいちょい触っているのでより慣れておきたかったのと、近頃はずっとタイプセーフでありたいことを意識しているので、Vue(Nuxt)は使う気が起きなかった。Vueはtemplate周りで補完が効かないのが相当しんどい。3に期待したい。Veturは頑張ってくれているが、かなり独自での実装っぽいしイマイチ安定性に欠けているように見える(Experimentalだししょうがない)。
余談だが、Vue3はComposition APIをちらっと見たところ、これがかなりReact hooksに似ている。JSXも使えば補完もかなり改善されそうだが、そこまでしてVueを使うモチベーションがわからなくなってしまった。いや、Vue自体はかなり好きなんですが…。
ひとまず今はReactをやり、Vue3の使い心地があればまた使うことがあるかもしれない。そんな感じ。

ちなみにCSSに関してはstyled-components, CSS modules, styled-jsxのあたりが選択肢に上がったが、styled-jsxを使ってデザインすることにした。
styled componentはクラス割当とは違った形式あんまり受け付けなかった。
css modulesは最初いい感じだと思っていたが、コンポーネントというそこそこ細分化されているはずのファイルでわざわざcssファイルを別にしてimportするのが途中で面倒くさくなった。そこまでtype safeでもないし({ [key: string]: string }なコードとしてimportされるだけなので)。巷ではcssを食わせてd.tsを吐くようなツールを作ってくれてる人もいたが、わざわざ監視させたり生成させたりするのも面倒なので今回は見送った。この辺改善されれば使用していないクラスの検出とかも便利そうだ。そういう意味ではstyled-componentsはJSだけで完結しているので、tree shakingみたいなのは上手く動きそうで良さそうだ。
で結局採用したのはstyled-jsxだけれど、これは自分がVueをやっていたからというのが大きいと思う。一つのコンポーネントにデザインとロジックが完結してて見通しが良い。まさにVueのSFCを書いている感じがしたからだ。デザインの定義はあくまでもデザインの定義、それを適用するクラスをマッピングしていく。そのスタイルが好きだ。styled-componentsの受け付けない部分は文書構造になりうる要素の宣言とデザインの宣言が一緒になっていて、密結合になっているところがあんまり受け入れられないのだろう、と書いていて気付かされた。
styeld-jsx自体はかなり良いが、エディタ(VSCode)周りの挙動はちょいちょい微妙に感じた。syntax highlightはvscode-styled-jsxで上手く動くが、vscode-styled-jsx-languageserverに関しては謎のエラーが時々吐かれる謎の挙動をする。実際にはsassを使っているのでstyled-jsx Language Server (scss)を使っていての話なのでvscode-styled-jsx-languageserverでは発生しないのかもしれないが。今回はlanguage serverを利用しなくてもなんとかなる規模だったのでインストール後結局Disableにした。

デザインに関しては、Neumorphismというものを取り入れた。少し前に2020年のトレンドになるか?みたいな感じで一瞬話題になったけど一瞬で忘れ去られてそう。デザイン自体は目新しく好みだけれど、フラットデザインに比べてちょこちょこ癖があるので少なくとも今はあんまり流行りそうにない。手っ取り早くやりたかったので
Neumorphism/Soft UI CSS shadow generatorを使ってテーマカラーと大体の見た目を決めて適当に生成した。便利。
ヘッダーとフッターの波はGet Waves – Create SVG waves for your next design. SVGで吐き出されるのが便利。
アイコンはFeather – Simply beautiful open source iconsをReactでラップしたfeathericons/react-feather: React component for Feather icons。アイコンがSVGで記述されていると色やらストロークの太さなどを自在に変更できるのが魅力的だ。細いラインのアイコンが好きな身としては嬉しい。

next.jsを入れるのに合わせて、eslintなどのconfig周りも雑に更新した。Prettierに関してはデフォルト使用で、ダブルクォート、セミコロン有りでのコーディングにしてみた。個人的にはシングルクォート・セミコロン無しが好みというPrettierのデフォルトとは真逆を行く好みだったが、この際個人プロジェクトでこれがないと死んでしまうわけではないので思い切ってデフォルト設定を活用してみた。実際のところセミコロン無しだとJSでは配列宣言から始まる文や即時実行する関数などで解釈がおかしくなるため、その部分だけセミコロンを挿入しなければいけない状態で統一感がなくなりもやもやしていた。セミコロンに関しては常に入れるようにしたほうがバグらせないので(それはそう)、そもそもセミコロン無しというのはやめたほうがいいのだろう。

技術的なTips

いつもの局所的断片Tips。

next.jsでstyled-jsxをSSRしたい

勝手にしてくれると思ったがそんなことはなく、標準だと初回ロード時にスタイルが当てられない画面が一瞬表示される(Nuxt的にはCSS Flashというらしいが、ざっとググった感じこれをそう言っているのはNuxtだけっぽい…?)。pages/_document.tsxに以下を記述したファイルを配置するとSSR(この場合は静的生成)時にheadに追加してくれる。

import React from "react";
import Document, { Html, Head, Main, NextScript } from "next/document";
import flush from "styled-jsx/server";

class MyDocument extends Document {
  render() {
    const styles = process.env.NODE_ENV === "production" ? flush() : null;
    return (
      <Html>
        <Head>{styles}</Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

わりと深い検証をしていない雑実装なので間違っていたらごめん。自分が確認した限りではちゃんと動いた。

box-shadowをアニメーションする

生成したNeumorphismは主にbox-shadowを使って表現されているが、雑に transition: all .2s ease;とやっても上手くアニメーションしてくれない。どうやら同じサイズの影が宣言されていないとうまくtransitionされないようで(体感的にはわかる)、今回は通常時とactive時のbox-shadowの宣言を変化する箇所以外(色)合わせ、必要のない影はtransparentで隠すようにした。

.foo {
  box-shadow: 5px 5px 10px var(--shadow-color),
    -5px -5px 10px var(--light-color),
    inset 5px 5px 10px transparent,
    inset -5px -5px 10px transparent;
}
.foo:active {
  box-shadow: 5px 5px 10px transparent, -5px -5px 10px transparent,
    inset 5px 5px 10px var(--shadow-color),
    inset -5px -5px 10px var(--light-color);
}

CSS variablesでdark mode対応

unsweets.logでも同じようなことは書いたけど、今回はCSS FWは使っていないので対応は楽。本当はもっと白背景時の色を元にdark mode時の色を計算したかったけれど、細かい調整をしまくってたら文字色くらいしかSass関数を活かせなかった。

$text-color: #666;
$text-color-invert: lighten($text-color, 50%);
$bg-color: #f6f6fc;

:root {
  --bg-color: #{$bg-color};
  --text-color: $text-color;
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg-color: #333;
    --text-color: #{$text-color-invert};
  }
}

ご覧の通り--bg-colorは最初宣言しているが、dark mode時は普通にSass変数を通さずダイレクトに宣言していたりで雑。綺麗にしようと考えたがどうせ自分しか書かないので怠慢を決めた。
このとき注意したいのが、Sass変数をcustom propertiesの値として利用したいときは直接$text-color-invertと書いてはならず、 #{$text-color-invert}と書く必要があること。通常のcolorプロパティなどは color: $text-color-invertとかけるが、これはどうやらSassの仕様らしい、が仕様を見つけることができなかった…(Issueは見つかった)。試しに#{}なしでコンパイルすると --text-color: $text-color-invertとSass変数が置換されないまま出力されてしまう。
ネイティブなCSS変数はダークモード対応時にかなり威力を発揮するので積極的に使用していきたい。IEなんてブラウザは知らん。

Smooth scroll

いまどきのブラウザは html { scroll-behavior: smooth; }とやることでページ内のidを降った箇所に飛べるアンカーリンクに飛んだときいい感じにアニメーションしながらスクロールしてくれる。が、アンカーリンク時にURLにハッシュをつけたくなかったため、JSで以下のように追加で制御するようにした(アンカーリンク時にURLをつけないという一般とは異なる挙動がUX的にどうなのかかなり迷ったが、アドレスバーからのURLコピー時にcanonical URLではないのが気になったため)。

const handleAnchor = (e: React.MouseEvent<HTMLAnchorElement>) => {
  e.preventDefault();
  const target = e.currentTarget.hash;
  const el = document.querySelector(target);
  if (!(el instanceof HTMLElement)) return;
  el.scrollIntoView({ behavior: "smooth" });
};

知らなかったが今どきはscrollIntoViewというメソッドがあるらしくこれを使うことで簡単に実装することができた。CSSの宣言とは別らしく、明示的にbehaviorオプションを指定する必要がある。細かい位置調整をしたいのなら Element.scrollTo()を使うことができるが、どちらもdurationやeasingなどの細かい指定はできないので、それらも制御したいのならサードパーティ製のライブラリを使う必要がありそうだ。
ちなみに、これらsafariが対応していない。polyfillがあるので雑に読み込む。 iamdustan/smoothscroll: Scroll Behavior polyfill
next.js内ではpages/_app.tsxで読み込むようにした(_document.tsxでも良かったかも?)

import smoothscroll from "smoothscroll-polyfill";

if (process.browser) {
  smoothscroll.polyfill();
}

現代のIEポジションはsafariだと思う。特にiOSに関してはブラウザベンダーが独自のブラウザを出していようとレンダリングエンジンがWebkitに縛られるのが厳しい(いやAndroidもよくよく考えれば基本的にはBlinkのはずだが、これは優秀なので困ることがない)。まあiOSのUI/UXで独自エンジン搭載したブラウザが出されたところで、それを利用する人なんてたかがしれているだろうが。Webkitの相手をするのほんとつらい。シェアだけはやたらあるのが余計に。


ざっと知見っぽいやつはこのくらいだろうか。思ったより長くなってしまった…。本当はこういう記事は技術ブログとしている unsweets.log の方に載せたいのだけれど、書いていることが特定的すぎるように感じたのでこんなことになってしまった。どちらに書くか悩むのも面倒だし、ワンチャン統一かなあ、とか思ってみたり。