『ちいさくはじめるデザインシステム』
先日こちらの本が出版されたので早速買いました。
SmartHRが公開しているSmartHR Design Systemを立ち上げた背景やその構築・運用までどのように考えて作られたのかがまとめられている1冊です。
現在在籍しているチームでもUIコンポーネントをまとめたリポジトリを作成・運用しているので参考になることが多そうだと思い買いました。
内容についてまとめてみたいと思います。
デザインシステムとは
まずデザインシステムは一般的に
「デザインの再現性を高め、一貫した製品体験を効率よく表現すること」を目的に導入される「ドキュメントやリソース群のこと」
大塚亜周他 著『ちいさくはじめるデザインシステム』 p.4
とされています。
デザインシステムの構成要素としては以下の通り大別できます。
- ブランドガイドライン
- ロゴ、色、企業理念、ビジョン、行動規範
- UIガイドライン
- コンテンツガイドライン
- ボイスアンドトーン、表記ルール、用字用語、ライティングパターン
- 文章を書くすべての人を対象としたガイドライン
- 運用ガイドライン
- デザインシステムを運用し続けるためのガイドライン
- 何が含まれるかは組織が抱えている課題による
デザインシステムは社会インフラ等と同じく、様々なものが組み合わさって仕組み化されています。
そこには目的に応じて必要な仕組みも変える柔軟な対応が必要です。「これがあれば正解」というものはありません。
なのでデザインシステムには「何らかの目的」が必要です。目的があって初めてデザインシステムの構想が始まります。
またデザインシステムは組織の問題を解決したり目的を果たすためのものであるのでデザイナーやデザイン組織だけのものではありません。
組織全体で取り組んでいくものであり、職能の垣根を超えて巻き込んで多様な観点を取り入れることが大事です。
デザインシステムを作ることだけでなく、「使うこと」も身近な貢献なのでまずは使ってくれる人を増やして小さな貢献がたくさん生まれる状況を作ることを目指すのが良いと本書には書かれています。
デザインシステムの構築のステップ
第2章では実際にデザインシステムを構築する上でのステップが解説されています。その内容を見ていきましょう。
デザインシステムをはじめる3つのステップ
便利なコンテンツから作る
- いきなり多様なコンテンツを作るよりまずは便利なものを1つ作る
- まずは使われている状態を目指す
- SmartHRではコンポーネントライブラリから始めた
課題発見と課題解決のサイクルを繰り返す
- プロダクト開発やブランドコミュニケーションの現場にあるリスクや課題を書き留める
- 優先度の高いものから着手
- 課題とそれに対する解決を残し続けていくプロセスそのものがデザインシステムである
原則を言語化する
- 「誰が判断しても結論に大きなズレが生まれない」原則は必要
- 時間をかけて言語化
「まずは便利なものを1つ作る」はタイトルにある通り「ちいさくはじめる」を体現している思想かなと思います。
最初から何でも揃った完璧なものを目指すより、まずは目の前の課題を解決できるものを作り、フィードバックを元に改善のサイクルを回すことに重きを置く。
それを繰り返す中で共通して見えてくる原則を言語化する。
作って終わり、ではないのがデザインシステムです。
デザインをみんなのものにする3つのステップ
最初は地道な草の根活動
- 社内MTG等でお知らせの機会を作る
- Slack上でコミュニケーション
- デザインシステムを解説する会を企画
- 社外にも広報
課題発見に小さく協力してもらう
- 使っている現場に直接聞きに行く
- 課題を解決できたらそれをお知らせに行く⇒その後も積極的にフィードバックをくれるように
コンテンツの作者になってもらう
- 小さく解決策が作れそうなものをお願いしてみる
- 作者になると実際に使われているか、改善できるところはないかと気になる
特に「コンテンツの作者になってもらう」はとても大事だなと思います。
デザインシステムに限らず、色んな人を巻き込んで当事者になってもらうのはプロダクト開発において必要なスキルの1つじゃないかと思います。
自分が作ったものは愛着も湧くし、もっと多くの人に使ってもらうために改善したくなるはず。
デザインチームだけのものではなく、組織全体で考える上でもこうしたステップは必要かなと思います。
デザインシステムの運用
第4章では構築したデザインシステムを運用していく上でのコツが記されています。
先程も述べた通り、デザインシステムは一度作ったら終わりではありません。
そのため、各プロセスの自動化は必要です。テストやコード規約等、作り始める段階から何が必要なのか見越しておくことが必要です。
また外部サービスや各種ツールも活用することを検討すべきです。
Figma等のデザインツールやStorybook等のコンポーネントライブラリ等、使えるものを有効に使うことでコストや工数を削減できます。
ただし、外部サービスのデメリットとして柔軟性に欠けるという点が挙げられます。
複雑な実装が必要な場合等はフルスクラッチで構築することも選択肢の一つです。
また、デザインシステムの前提として書かれていたのはデザインとエンジニアリングは不可分であるということです。
- コミュニケーション手段の延長として皆が使える「道具」を作る
- 非デザイナーでもデザインの意思決定を素早くできるように「パターン」を用意する
先程も書いた通りデザインシステムはデザイナーやデザイン組織だけのものではありません。
職種や職能の枠を超えてプロダクト開発にデザインの面から誰もが貢献できるようなシステム、それがデザインシステムなのではと思います。
レビューについて
第3章では様々な実例が示されているのですが、その中でもデザインレビューの項が非常に良かったです。
デザインレビューだけでなく、コードレビュー等にも通じる部分が多々あったのでぜひ読んでみてください。
エンジニア文化祭
先日開催されたエンジニア文化祭に参加しました。
と言っても当日は普通に平日だったのでアーカイブでの視聴となりました。
まだ全部見たわけではないのですが、大変勉強になる講演がたくさんあったので箇条書きですがざっくり書いてみます。
視聴した講演とそのまとめ
フロントエンドの潮流から見る需要と進化 〜これまでとこれからのフロントエンドの話〜
- モダンフロントエンドの課題と背景について
- 「今」のフロントエンドを作る技術とその需要
- Node.jsを中心にツールが発達 (Webpack, Babel etc...)
- スマホの普及とともにUI・フロントエンドの重要性・複雑性が高まってきた
- 2015~2016年頃が過渡期、「フロントエンドの進化速すぎ、疲弊」
- 宣言的なUIがReactのキャッチーなところ
- 『「表示」と「更新」を同じ書き味で書きたい』という思想からできたのがReact
- Reactコミュニティだといろんな機能全部盛りみたいなToo muchなライブラリが多いがそれはなぜ?
- 各コンポーネントが状態を持つとアプリケーション全体からは各コンポーネントの状態が見れない
- それを集約したのがFlux, Redux
- グローバル変数であっても使うところと更新するところを集約すればいいんじゃないの的発想
- Hooks以降のReactだと各コンポーネントごとに状態を持たせる方に流れていってる
- 全部Reduxに集約するのではなく、例えばAPIのキャッシュはそのレイヤーで管理する方が合理的、Too muchなものからの揺り戻し
- SPAがモダン⇒でも初期表示遅い⇒SSR⇒じゃあ最初から生成すればOK⇒SSG
- いつまで「今」は続くのか
- 「フロントエンド」の概念が劇的に変わる時
- 大きな変化からの揺り戻しの繰り返し
- 作り方がよりシンプルになった時
- copilot, chatGPT等の技術的革新
- 現状のフロントエンドの考え方は今後も必要
ウェブセキュリティの知識は普及しているか、徳丸本の著者が憂慮していること
- chatGPTの書くコードはセキュアなの?
- 検索やAIによる情報収集の問題
- SSRFについてBing Chat AIに聞いてみる
- そのソースを見てみると徳丸さんや他の方が書いた記事から転載・改変したもので内容は不正確
- 既存記事を組み合わせたキメラのような記事
- もはやググって上位の記事を参照する方法では質の高い記事を見つけることはできない
- 今後はSEO目的の記事をAIを使って量産⇒さらに加速
- 検索やAIを使う場合でもセキュリティの基礎的な知識が無いと見分けられない
- CapitalOne事件に学ぶDevSecOpsの落とし穴
- DevSecOpsとは
- ソフトウェア開発ライフサイクルの全てのフェーズでセキュリティを自動化し、統合する手法のこと
- SSRF攻撃の被害事例についてBing Chat AIに聞いてみる
- WAFの設定ミスによる個人情報漏洩事件
- Apacheでリバースプロキシを設定する例
- ProxyRequests On はNG
- IMDS: Instant MetaData Service EC2の仮想エンドポイントで設定情報がJSONで返ってくる
- 推測される本事件の原因
- WAFがオープンプロキシになっていた
- WAFに多数の不要なクレデンシャルが付与されていた
- S3に1億件超の個人情報を保管
- IMDSが無効化されていなかった
- CapitalOneはDevSecOpsの先駆的企業だったがWAFに対する脆弱性対策ができていなかった
- SDLC
- 開発プロセスの全行程にセキュリティ関連の活動を組み込むこと
- シフトレフト
- 開発プロセスのできる限り早期の段階にセキュリティ施策を実施すること
- 上流工程で「実際に何をするのか」はあまり語られていない
- AIやツールを導入するだけではダメ、セキュリティの知識を身につけ、実践することが必要
- DevSecOpsとは
サバンナ便り〜自動テストに関する連載で得られた知見のまとめ〜
- 学習用テスト
- 学びを自動テストとして書く
- 即時性と再現性
- 動かして初めて分かることは多い
- 自分が作ったものじゃないものに対するテストを書いてみる(ライブラリ等)
- 疑問をテストにする
- 驚きをテストにする
- コンストラクタをもう一度呼べてしまう(!)⇒破壊的変更ができてしまう
- 偽陽性と偽陰性
- テストサイズ
- テストダブル
- テストピラミッド
分岐を低減するinterface設計と発想の転換
- 設計なんもわからん
- interfaceを使うと分岐が劇的に低減してコードがシンプルになる⇒上手く使えてる?
- 架空のホームセキュリティシステムの事例
- いろんな仕組みに対応して欲しいという要望
- 個別に対応するのは無理
- 要望ごとに分岐が増えて複雑化
- interfaceを上手く使うには大幅な発想の転換が必要
- interface設計の要点
- 目的単位で抽象化
- システムには必ず目的がある
- 問題領域に対し、それを解決する様々な解決領域が世の中にはある
- 目的の具体化(ツリー構造化)
- 目的が決まると課題が浮き彫りに
- 課題に対処するための解決手段も決まる
- 目的が違うと課題も解決手段も違ってくる
- 目的/課題と解決手段はツリー構造
- 目的達成手段を抽象化
- 同一目的に対する達成のバリエーション
- 「作る」と「使う」を分ける
- 目的単位で抽象化
- 目的のバリエーションと機能性
- 同一目的に対して手段にバリエーションがある
- 達成手段それぞれで性能が異なる
- 状況に応じて最適な手段は異なる
- interface設計可能な箇所は同一目的でもより顧客にリーチする機能に置き換えられる可能性を秘めている
- ただしinterfaceは銀の弾丸ではない
- 手段のバリエーションという観点での設計を推奨
まとめ
以上、ざっくりしたまとめでした。
単純に技術力をつけることだけじゃなく、その背景だったり思想を学ぶことも必要だなと改めて感じた機会でした。
また講演の中で紹介されていた書籍も気になるものがたくさんあったので読んでみたいと思います。
最近欲しいもの
久しぶりにプライベートな話題の記事です。
他にも書きたい内容はあるんですが、まだちょっと整理できていないのでそちらは次回以降に。
今回はタイトル通り最近欲しいものをただただ羅列していくだけの記事です。
物欲が止まらない今日この頃、欲望が尽きることは無いですね。
M2 Proチップ搭載16inch MacBook ProまたはM2 Proチップ搭載Mac mini
プライベートで使っているPCは2019年モデルの13inch MacBook Proなんですが、今年発表されたM2 Proチップ搭載モデルが欲しいです。
もちろんまだ現役で使えるんですが、買い替えのペース的にもそろそろなんじゃないかと。下取り価格も高いうちに買い替えたいですし。
あと、仕事用のMacBook Proが16inchでそちらに慣れてしまったのもあり、13inchだと小さく感じるんですよね。。
ただ、問題はその値段です。
一番安いモデルでも348,800円...
おいそれと買える値段じゃないです。
そこで候補に上がるのがMac miniです。
同じチームのメンバーの方が教えてくれてこっちだとかなり安く買えそう。
カメラが無いとか持ち運びできないとかはあるんですが、カメラは今も外付けのものを使ってるし、持ち運びする用途も無さそう。
なので買うとしたらこちらかなぁと思ってます。
Alpha Recording System Model 9100
続いてこちら。
何をするための機械なのかわからない方がほとんどだと思います。
こちらDJ用のロータリーミキサーと呼ばれる機材です。
学生時代から趣味でDJをやったりしておりまして、その時に使う機材ですね。
音楽が途切れないように2つのターンテーブルを使って曲を繋ぎ続ける、のがDJなんですが2台のターンテーブルを繋ぐのがミキサーという機材です。
ミキサーを操作することでどっちのターンテーブルからの音を出力するかをコントロールするわけです。
で、そのミキサーは大きく2種類に分けられて1つはフェーダーミキサーと呼ばれるものです。
それぞれのチャンネルのボリュームを縦のフェーダーを上下することで調整したり、どちらのチャンネルの音を流すかを横のフェーダーでバランス調整したりします。
多分DJと聞くと皆さんイメージするのはスクラッチだと思うんですが、スクラッチする場合は基本フェーダーミキサーです。
一方もう1つはロータリーミキサーと言います。今回欲しいやつです。
先程のような縦フェーダー/横フェーダーは無く、全部ロータリーと呼ばれるツマミを回すことで操作します。
何が良いかと言うと曲をつなぐ時の微調整がしやすいのと良い感じに音を混ぜてくれるのでめちゃくちゃ触っていて楽しいです。
基本的にロータリミキサーは職人のハンドメイドによるものが多く、入力信号をデジタル変換しないので音が良いです。
またハンドメイドゆえにメーカーごとに出音の特徴も異なっており、そこも一般的なミキサーには無い楽しみかと思います。
詳しくはこの辺の記事を見てみてください。
で、肝心のお値段ですが先程のMacBook Proより高いです。物によっては50万円以上します。
これは先ほど述べた通りハンドメイドであるがゆえに生産量が少ないこと、そしてパーツにもこだわっていることが挙げられます。
なのでこちらもパッと買える代物では無いんですが、一度知ってしまったので一生モノだと思って買いたいなと。
特に見出しにリンクを貼ったAlpha Recording Systemは数少ない日本のメーカーなので買うならココにしたいなと思ってます。
買える目処が立って、なおかつその時在庫があれば、という感じです。
ウイスキー
先の2つはある程度実用的と言いますか、長く使えるものですがこちらはただただ消費するだけのやつです。
ウイスキーを好んで飲むんですが、物価高騰著しい昨今、ウイスキーも同様にめちゃくちゃ高くなってます。
例えば僕の好きな銘柄で言うとこの辺りです。
Glenmorangieの方は今調べてみると意外と上がってないなという印象ですがLagavulin16年は高い。
1年前に買ったときは確か8〜9千円ぐらいだったような気がします。
とは言ってもやはり美味しいので少々高くてもそのうち買っちゃいそうです。MacBook Proやミキサーに比べると全然安い
まとめ
というわけで趣味全開な記事でした。
今年洗濯機が壊れて買い替えたばかりなのでこれらを買うのはまだしばらく先になりそうです。
地道に貯金します。。
『マイクロフロントエンド』
先日こんな本を買いました。
オライリーから出版された「マイクロフロントエンド」という本です。
今日はこちらの本について書いていきたいと思います。
マイクロフロントエンドとは
マイクロフロントエンドという言葉自体、耳馴染みが無い方も多いと思います。僕もそうです。
マイクロフロントエンドとは、自律的、独立してデプロイ可能、かつ単一のチームが所有するビジネスドメインのことです。
Luca Mezzalira 著、嶋田 健志 訳 『マイクロフロントエンド』p.27
まだちょっと抽象的です。具体的に見ていきます。
通常、新しくプロジェクトを立ち上げプロダクト開発を進めていく際にはそのビジネスが成功するかどうかの検証からスタートします。
コストを抑え、必要最低限の機能に絞り、最小限のプロダクト(MVP: Minimun Viable Product)を作り、ユーザーからの情報を集めます。
その際、多くの場合はモノリシックなアプリケーションとして設計されます。
サービスがスケールするにつれてモノリスからマイクロサービスへと移行していく、というのがよく見られるケースです。
ドメインごとに独立したAPIやDBを持ち、各チームは担当のドメインに対して責任を負います。
しかしAPIやDBはデザインパターンやベストプラクティスがあればスケールできるのに対し、フロントエンドについてはどうでしょうか?
昔からフロントエンドをスケールさせる選択肢はあまり存在しませんでした。
というのも、ビジネスロジックを持つファットサーバと処理結果を表示するシンクライアントが標準的であったため、その必要性が無かったからです。
しかし、ユーザーの行動の変化に伴ってUXの水準が向上し、質の高いコンテンツやサービスを維持しながらユーザーの希望を実現するためのスムーズな道筋を提供する必要性が高まってきた。
言い換えればサービスにおけるフロントエンドの重要性が高まってきたということです。
そうした背景から新機能の追加を迅速に、かつアプリケーションの一部を単体でリリースできる手法が必要となりました。
これがマイクロフロントエンドです。
ここまでの説明を振り返るとマイクロサービスそのものといった感じですが、著者はマイクロサービスの原則はマイクロフロントエンドにも適用できるとしています。
- ビジネスドメインのモデル化
- 自動化の文化
- 実装の詳細を隠す
- ガバナンスの分散
- 独立デプロイ可能性
- 障害の分離
- 高い観測性
また巻末に収録されているマイクロフロントエンドを実践している開発者へのインタビューではこんな一文もありました。
――マイクロフロントエンドを3語で表現すると?
Microservices for frontends. (フロントエンドの/ための/マイクロサービス)
Luca Mezzalira 著、嶋田 健志 訳 『マイクロフロントエンド』p.346
というわけでマイクロサービスアーキテクチャをフロントエンドで実践するには、というのが本書のテーマです。
ただ、すべてのプロダクトをマイクロフロントエンドにすべきというわけではありません。
デメリットがメリットを上回る場合は採用しないよう著者も呼びかけています。
マイクロフロントエンドは、その性質上、また技術的・組織的なレベルで複雑さを増す可能性があるため、すべてのアプリケーションに適しているわけではありません。
Luca Mezzalira 著、嶋田 健志 訳 『マイクロフロントエンド』p.24
マイクロフロントエンドの実践
意思決定フレームワーク
まずはどのようなアーキテクチャにするか、その意思決定のフレームワークを見ていきます。
水平分割 / 垂直分割
- 水平分割
水平分割は複数のビューにまたがるパーツごとにフロントエンドを分割する方法です。
つまり1つのビューの中に複数のマイクロフロントエンドが含まれることになります。
柔軟性の高い手法ですが、何百ものマイクロフロントエンドが存在することにならないよう規律やガバナンスが必要となります。
- 垂直分割
垂直分割は各チームが1つのビジネスロジックに関わるフロントエンドを担当する方法です。
これはドメイン駆動開発に近い考え方と言えます。
マイクロフロントエンドにおいてもコンテキストの境界を定義づけるにはビジネスの仕組みをよく理解することが必要であると書かれています。
構成
マイクロフロントエンドの構成は大きく分けると3種類あります。
- クライアントサイドコンポジション
- CDNまたはオリジンサーバからロードしてクライアントでビューを構成する
- エッジサイドコンポジション
- オリジンサーバからロードしてCDNレベルでビューを構成する
- サーバサイドコンポジション
- オリジンサーバレベルでビューを構成し、CDNにキャッシュさせる
ルーティング
通常は構成と同じレベルでルーティングを行うことが多いです。
マイクロフロントエンド同士でのコミュニケーション
マイクロフロントエンドは単体で完結しているため、通常は相互にやり取りする必要はありません。
しかし、水平分割の場合等同じビューに複数のマイクロフロントエンドが存在している場合はビュー全体として整合性が取れている必要があります。
その為、フロントエンド同士のイベント通知の方法についても考慮する必要があります。例えば以下のような手法です。
- イベントバスを通じてすべてのマイクロフロントエンドに通知する
- カスタムイベントを使う
- Webストレージ / cookieを使う
- クエリパラメータを使う
これらの観点を元にチーム内で選定し、実装を進めていきます。
ただし、すべてを満たす完璧なアーキテクチャは存在しないと著者は述べています。
本書の他の箇所でも指摘したように、私は完璧なアーキテクチャは存在せず、常にトレードオフの関係にあると固く信じています。トレードオフは技術的なものだけでなく、ビジネス要件や組織構造にも基づいています。現代のアーキテクチャは、技術的な側面だけでなく、最終的な結果に影響する他の力を考慮します。「完璧なアーキテクチャ」を探したり(これは存在しない)、他のコンテキストからアーキテクチャを借りてきて、それが我々のコンテキストに適切かどうかを調査する代わりに、社会技術的側面を認識し、我々が活動するコンテキストに対して最適化しなければなりません。
Luca Mezzalira 著、嶋田 健志 訳 『マイクロフロントエンド』p.53
つまり、アーキテクチャの選定よりも前に自分たちが活動するコンテキスト、チームの構造、チーム間のコミュニケーションフローを時間をかけて理解することが大切であるということです。
他の企業やチームで上手くいった事例をそのまま取り上げるだけでは全くフィットしない可能性もあります。
自分たちのチームの特性や事例にあった他社の特性との違い、背景、解決しようとしている課題等、総合的に判断する必要があります。
自動化
マイクロフロントエンドには自動化は不可欠です。
先程このように書きました。
新機能の追加を迅速に、かつアプリケーションの一部を単体でリリースできる手法
CI/CDといった自動化パイプラインが整備されていなければこれらの実現が難しくなります。
パイプラインが整備されていればリリース時の負担は少なく済みますし、また何かトラブルがあっても速やかに対応できます。自動テストによってバグを未然に防ぐこともできます。
マイクロフロントエンドを取り入れる理由の一つに小さな単位で迅速に改善を重ねることが挙げられると思うのでCI/CDを始めとした自動化は常に整備しておく必要があります。
また、フロントエンド全体に対してCIを行うよりもビルド時間やテストの実行時間なども短くなるというメリットもあります。
フィードバックループを高速に保ち、最適化する手法を理解する必要があります。
テスト戦略
自動化に関連してテスト戦略も考慮が必要です。
例えば垂直分割のマイクロフロントエンドのE2Eテストを行う場合、担当するドメインのテストだけでなく異なるドメインとの接点となるテストも作成する必要があります。
また水平分割の場合はどのチームがどのテストを作成するのか、チーム間での調整が必要となります。
デプロイ戦略
最後に継続的デプロイ(CD)戦略についてです。
マイクロフロントエンドは単体で独立してデプロイできることが前提です。
コードベース全体や他のマイクロフロントエンドに影響を及ぼすことなく、アプリケーションの一部のみをリリースできます。
単体でリリースできるということはアプリケーション全体を破損させるリスクやユーザー体験を損なう可能性を回避することもできます。
つまり、すべてのユーザーに対して新しいバージョンをリリースするのではなく、ブルーグリーンデプロイやカナリアリリースを採用することでトラフィックの一部に対してのみリリースし、本番環境でのテストに合格した後に切り替える、ということです。
マイクロサービスで有効な手段はマイクロフロントエンドでも同様に採用することができます。
まとめ
以上、簡単ではありますが「マイクロフロントエンド」の内容をまとめてみました。
本記事では割愛しましたが、事例やインタビューも豊富に載っていて内容の濃い1冊でしたので気になった方はぜひ。
取り入れるのは結構難しそう、というのが率直な感想です。
垂直分割だとドメイン単位で開発できるというメリットはありますが、ドメイン間での統一感であったり重複したコンポーネントも沢山出てきそうだなと。
逆に水平分割だとUI上の統一感はクリアできても大元のアプリケーションの管理はどうするのかや、後テストが結構大変そうだなと思いました。
いずれにしても著者も書いている通り、チームや組織について理解すること、議論することがかなり重要であると改めて感じました。
フロントエンドの設計についてはまだまだ知識も実践も不足しているので引き続き勉強していきたいと思います。
Next.jsでmarkdownを使う
引き続きNext.jsをちまちまいじっています。
このブログを移行することを取り急ぎの目標にして作成中です。
ブログなのでmarkdown使えるようにしたら色々楽だろうということで今週その辺りを作っていました。
markdown関連のライブラリ
markdownをHTMLに変換するライブラリは数多くあります。
代表的なところで言うとmarked, markdown-it, remark辺りでしょうか。
ちなみに最近ではmarkdownとJSXを組み合わせたMDXというものもあるとチームのメンバーから教えてもらいました。
これを使えばmarkdown中でReactのコンポーネントを使うことができるようです。
公式ドキュメントに載ってた例ではこんな感じ。
import {Chart} from './snowfall.js' export const year = 2018 # Last year’s snowfall In {year}, the snowfall was above average. It was followed by a warm spring which caused flood conditions in many of the nearby rivers. <Chart year={year} color="#fcb32c" />
確かにかなり便利そう。
ただ、markdownでブログを書く際にはあまりコンポーネントを意識したくないなというところです。
「ここでこれを表示したいからこのコンポーネントを使って、そのpropsはこれとこれで...」というのを考えながら書くのがちょっと面倒な気がします。
なので今回は別の方法を採ってみることにしました。
react-markdown
今回はreact-markdownを使うことにしました。
基本的な使い方は以下の通り。
import React from 'react' import ReactMarkdown from 'react-markdown' import ReactDom from 'react-dom' ReactDom.render(<ReactMarkdown># Hello, *world*!</ReactMarkdown>, document.body)
<ReactMarkdown>
コンポーネントでラップするだけ。簡単です。
で、先程markdown中でコンポーネントを使う話をしましたが、components
プロパティで設定できます。
Use custom components (syntax highlight)
This example shows how you can overwrite the normal handling of an element by passing a component. In this case, we apply syntax highlighting with the seriously super amazing
react-syntax-highlighter
by @conorhastings:
import React from 'react' import ReactDom from 'react-dom' import ReactMarkdown from 'react-markdown' import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter' import {dark} from 'react-syntax-highlighter/dist/esm/styles/prism' // Did you know you can use tildes instead of backticks for code in markdown? ✨ const markdown = `Here is some JavaScript code: ~~~js console.log('It works!') ~~~ ` ReactDom.render( <ReactMarkdown children={markdown} components={{ code({node, inline, className, children, ...props}) { const match = /language-(\w+)/.exec(className || '') return !inline && match ? ( <SyntaxHighlighter children={String(children).replace(/\n$/, '')} style={dark} language={match[1]} PreTag="div" {...props} /> ) : ( <code className={className} {...props}> {children} </code> ) } }} />, document.body )
上記の例ではコードブロックにシンタックスハイライトを適用するコンポーネントを使っています。
こんな感じでHTMLタグごとに出力を変えることができます。
なのでリンクカードを表示するLinkCard
コンポーネントを作ってこんな感じにしてみました。
import ReactMarkdown from 'react-markdown' import SyntaxHighlighter from 'react-syntax-highlighter' import { atelierHeathDark } from 'react-syntax-highlighter/dist/cjs/styles/hljs' import LinkCard from './LinkCard' import { isValidElement } from 'react' import remarkGfm from 'remark-gfm' import { Body as BodyType } from '../../types/atoms/Body' import { Href } from '../../types/atoms/Href' const Body = (props: { body: BodyType }): JSX.Element => { return ( <div id='content'> <ReactMarkdown children={props.body.toString()} remarkPlugins={[remarkGfm]} components={{ p({ children }) { if ( Array.isArray(children) && children[0] && isValidElement(children[0]) && children[0].props.node?.tagName === 'a' ) { return ( <div> {children} </div> ) } else { return ( <p> {children} </p> ) } }, a({ href }) { if (!href) { return ( <p></p> ) } return ( <LinkCard href={new Href(href)} /> ) }, code({ node, inline, className, children, style, ...props }) { const match = /language-(\w+)/.exec(className || '') return !inline && match ? ( <SyntaxHighlighter children={String(children).replace(/\n$/, '')} language={match[1]} style={atelierHeathDark} PreTag="div" {...props} /> ) : ( <code className={className} {...props}> {children} </code> ) }, }} /> </div> ) } export default Body
<a>
タグを変更している箇所は以下です。
a({ href }) { if (!href) { return ( <p></p> ) } return ( <LinkCard href={new Href(href)} /> ) },
a(props)
のprops
にはhref
の他、target
等<a>
タグのattributesで設定できるものが入ります。
ただundefined
が入る可能性もあるのでその場合は空の<p>
タグを返します。
href
に値が入っている場合は<LinkCard>
コンポーネントを返します。
new Href(href)
はclass型です。URL形式になっていない場合はError
を投げるようになっています。
ちなみに<LinkCard>
はこんな感じです。まだ作り込みが甘いですが。。
import { useEffect, useState } from 'react' import { useClient } from '../../hooks/useClient' import styles from '../../styles/atoms/LinkCard.module.scss' import { Href } from '../../types/atoms/Href' const LinkCard = (props: { href: Href }): JSX.Element => { const [ogps, setOgps] = useState<Array<{ prop: string, content: string }>>([]) const isClient = useClient() useEffect(() => { const getOgps = async () => { if (!props.href) { return } const source = await fetch(props.href.toString()) const text = await source.text() const html = new DOMParser().parseFromString(text, 'text/html') const headEls = html.head.children setOgps(Array.from(headEls).map(headEl => { const property = headEl.getAttribute('property') if (!property) { return { prop: '', content: '', } } return { prop: property.replace('og:', ''), content: headEl.getAttribute('content') ?? '', } }).filter(ogp => { return ogp.prop !== '' })) } getOgps() }, [props.href]) const image = ogps.find(ogp => { return ogp.prop === 'image' }) const title = ogps.find(ogp => { return ogp.prop === 'title' }) const description = ogps.find(ogp => { return ogp.prop === 'description' }) return ( <> { isClient && <a href={props.href.toString()} target="_blank" rel="noreferrer" > <div className={styles.link_card_wrapper}> <img className={styles.link_card_image} src={image?.content ?? ''} alt={title?.content ?? ''} /> <h3>{title?.content}</h3> <p className={styles.link_card_description}>{description?.content}</p> </div> </a> } </> ) } export default LinkCard
useEffect()
でリンク先のOGPを取得しています。
useClient()
はカスタムHookでこんな感じ。
import { useEffect, useState } from "react"; export const useClient = () => { const [isClient, setIsClient] = useState(false) useEffect(() => { if (typeof window !== 'undefined') { setIsClient(true) } }) return isClient }
クライアントサイドかサーバーサイドかを判定するHookです。
なぜこれを追加したかと言うと、Hydration Error
になってしまうからです。
Hydration Error
とはざっくり言うとサーバーサイドで生成したDOMとクライアントサイドでレンダリングされたDOMが違っていることによるものです。
While rendering your application, there was a difference between the React tree that was pre-rendered (SSR/SSG) and the React tree that rendered during the first render in the Browser. The first render is called Hydration which is a feature of React.
This can cause the React tree to be out of sync with the DOM and result in unexpected content/attributes being present.
どうしたものかと思いましたがこちらを参考にしてカスタムHookを作ることで対応しました。
【React】Hydrate時のワーニングを放置するとパフォーマンスが悪くなる!?
これでHydration Error
は回避できましたが、まだコンソールにWarningが出ます。
Warning: validateDOMNesting(...): <h3> cannot appear as a descendant of <p>.
markdownからHTMLに変換する際、地の文は一文ごとに<p>
タグに入ります。
そうすると<a>
タグを<LinkCard>
コンポーネントに置き換えているので<p>
タグの中に<h3>
タグが入る形になってしまい、Warningが出てしまっているわけです。
これを回避しているのが以下の部分です。
p({ children }) { if ( Array.isArray(children) && children[0] && isValidElement(children[0]) && children[0].props.node?.tagName === 'a' ) { return ( <div> {children} </div> ) } else { return ( <p> {children} </p> ) } },
<p>
タグのchildren
属性が配列で、かつ空でなく、ReactElement
で、それが<a>
タグの場合(=<LinkCard>
コンポーネントを使う場合)は<div>
タグに変換しています。
それ以外の場合は通常通り出力しています。
こうすることでWarningが出なくなりました。
ここまで対応した状態でmarkdownに通常通りリンク設定してあげると、
[https://numnam.net](https://numnam.net)
こんな感じで表示されます。
まとめ
以上、markdown中でReactコンポーネントを使う方法でした。
ただこれだとリンクだけ独立した段落に書かないと表示がおかしくなりそうなのでまだ改善の余地は大きそうです。
とりあえずは当初の狙い通りリンクカードを出すことができるようになったので今のところはOKとします。
Nuxt3 (Vue3)の変更点をまとめてみた(Plugins, Composables, Composition API)
ここのところはReactの記事を書いていましたが、最近業務でNuxt3へのアップデートを行っているのでその備忘録も兼ねてまとめていきたいと思います。
大きく分けてPlugins, Composables, Composition APIについて。
Plugins
Nuxtでは自作の関数や外部パッケージ等をプラグインとして使うことで拡張することができます。
Nuxt2ではこんな感じでした。
// /plugins/hoge.js export default (context, inject) => { inject('hoge', () => { console.log('hoge') }) }
このようなファイルを作ってnuxt.config.js
に
plugins: ['~/plugins/hoge.js']
と書くとコンポーネントでthis.$hoge()
のように使うことができます。
第1引数のcontext
はNuxt Contextで、第2引数にはinject
という関数が入ります。
このinject
を使ってcontext
にプラグインを注入するわけです。
なのでcontext
を直接参照できる箇所、例えばasyncData
では以下のようにも使えます。
async asyncData({ $hoge }) { $hoge(); }
また、Vueのプラグインを使う場合は、
// /plugins/fuga.js import Vue from 'vue'; import Fuga from 'fuga'; Vue.use(Fuga);
として、
plugins: ['~/plugins/fuga.js']
とすることでVueのプラグインを追加することもできました。
Nuxt3ではプラグインを使うにはdefineNuxtPlugin()
を使います。
最初の例をNuxt3で書くと以下のようになります。
import { defineNuxtPlugin } from '#app'; export default defineNuxtPlugin(() => { return { provide: { hoge: () => { console.log('hoge'); } } } });
こうすることでNuxtのインスタンスにhoge()
が注入されます。
上述した通りNuxt2ではcontext
がありましたが、Nuxt3ではuseNuxtApp()
を使うことでNuxtのインスタンスを呼び出せます。
const nuxtApp = useNuxtApp(); nuxtApp.$hoge(); // 分割代入も可能 const { $hoge } = useNuxtApp(); $hoge();
Vueプラグインを使う場合も同様にdefineNuxtPlugin()
を使います。
defineNuxtPlugin()
の引数に入る関数はNuxtのインスタンスを引数に取ります。
import { defineNuxtPlugin } from '#app'; import Fuga from 'fuga'; export default defineNuxtPlugin((nuxtApp) => { nuxtApp.vueApp.use(Fuga); });
ここでのnuxtApp
は先程のuseNuxtApp()
の返り値と同じNuxtインスタンスです。
NuxtインスタンスからVueインスタンスを取り出し、Nuxt2と同じくuse()
で登録します。
もう1つ大きな違いとしてはNuxt3ではnuxt.config.js
にplugins
として記載する必要がありません。/plugins
にファイルを追加するだけで使えます。
/plugins
に追加したプラグインはデフォルトではサーバーサイド・クライアントサイドどちらでも実行されます。
サーバーサイドのみ、またはクライアントサイドのみで実行したいプラグインはそれぞれファイル名をhoge.server.js
またはhoge.client.js
のようにします。
Composables
Vue2ではグローバル関数を使うためのものとしてmixin
という仕組みがありましたが、こちらは現在非推奨になっています(一応後方互換性のために現在も使えるようにはなっています)。
代わりにVue3ではComposablesという新しい仕組みが追加されました。
Nuxt3では/composables
ディレクトリに追加したファイルは自動で読み込まれます。
なのでグローバルに使いたい関数等はここに追加しておくと便利です。
後で説明するComposition APIも同一の文脈であり、共通のロジックを切り出すための仕組みになります。
こうすることでロジックだけを単体テストすることも容易になります。
慣習としてuseXXXX
というReactのパクリ命名にする、とのことですがそこは別にわかりやすい名前で良いと思います。
Composablesの一番の使い道としてはやはりStoreでしょうか。
Nuxt3ではVuexがバンドルされなくなったのでuseState
を使った方法かPiniaが推奨されています。
// /composables/hogeStore.js import { useState } from '#app'; export const hogeStore = () => { const counter = useState('hoge', () => { return 0; }); const increment = () => { counter.value++; } const decrement = () => { counter.value--; } return { counter, increment, decrement }; };
こうすることでコンポーネント側で以下のように呼び出すことができます。
<script setup> import hogeStore from '~/composables/hogeStore'; const { counter, increment, decrement } = hogeStore(); </script> <template> <div> <span>Count: {{ counter }}</span> <button @click="increment">+</button> <button @click="decrement">-</button> </div> </template>
Composition API
Vueコンポーネントも従来のOptions APIからComposition APIが標準となりました。
特にVue3.2から登場した<script setup>
構文はこれまでよりシンプルに書くことができます。
例えばOptions APIで以下のようなコンポーネントがあったとします。
<template> <div> <button @click="$emit('click')">click</button> </template> <script> export default { name: 'HogeComponent', props: { hoge: { type: String }, fuga: { type: Number, default: 0 } }, data() { return { title: 'hogehoge', description: 'fugafuga' } }, computed: { piyo() { return this.fuga * 2; } }, methods: { poyo() { // Do something. } } } </script>
これを<script setup>
構文で書くとこんな感じ。
<template> <div> <button @click="emit('click')">click</button> </template> <script setup> const props = defineProps({ hoge: { type: String }, fuga: { type: Number, default: 0 } }); const title = ref('hogehoge'); const description = ref('fugafuga'); const piyo = computed(() => { return props.fuga * 2; }); const poyo = () => { // Do something. } const emit = defineEmits(['click']); </script>
順番に見ていきます。
props
props
を定義するにはdefineProps()
を使います。
渡す値は従来のprops
と同じでOKです。
TypeScriptを使う場合、型定義を渡してあげるだけでもOKです。
<script setup lang="ts"> interface Props { hoge: string, fuga?: number } const props = defineProps<Props>(); </script>
ただこれだとデフォルト値が設定できないのでそのような場合にはwithDefaults()
を使います。
<script setup lang="ts"> interface Props { hoge: string, fuga?: number } const props = withDefaults(defineProps<Props>(), { hoge: 'hoge', fuga: 0 }); </script>
ref / reactive
従来data()
だったもの、つまりコンポーネント内でリアクティブな値を保持する変数はref()
またはreactive()
で定義します。
ref()
とreactive()
の違いは以下の通りです。
ref()
- 全ての型が使える
- 代入した値にアクセスするには
.value
というプロパティを使う
const hoge = ref({a: 0, b: 1}); console.log(hoge.value.a); // 0
reactive()
- オブジェクトのみ使える
- 代入した値にアクセスするには代入したオブジェクトのプロパティを直接参照する
const hoge = reactive({a: 0, b: 1}); console.log(hoge.a); // 0
computed
Options APIではcomputed
プロパティにまとめて定義していましたが、Composition APIではcomputed()
という関数を使います。
ただ書き方は従来と同じです。getter / setter両方定義することももちろん可能です。
const hoge = ref(0); const fuga = computed({ get: () => { return hoge.value; }, set: (value) => { hoge.value = value - 1; } }); ## methods `methods`に定義していた関数は`<script>`直下にそのまま定義できます。 ## emit Vue3から[親コンポーネントに`emit`するイベントをコンポーネント内で定義しなければならなくなりました](https://v3-migration.vuejs.org/breaking-changes/emits-option.html)。 `emit`を定義するには`defineEmits()`を使います。 そのコンポーネントから`emit`するイベントを配列で渡してやるだけです。 `defineEmits()`の返り値はそのまま関数となり、テンプレート内の`@click`等で使用できます。 ちなみに`<script setup>`の中で定義した変数や関数はそのままテンプレート内で使用することができます。別途returnする必要はありません。 また、↑で出てきた`defineProps()`、`ref()`、`reactive()`、`computed()`、`defineEmits()`はNuxtが自動的にインポートしてくれるので明示的にインポートする必要がありません。 # まとめ 以上、Nuxt3及びVue3についてまとめてみました。 以前軽く記事にしましたが、やはり実際にアップデートを進めていく中でより理解が深まっている気がします。 Composition APIへの置き換えが一番大変そうですが、引き続き対応していきます。
「はじめての設計をやり抜くための本」
先日こんな本を買いました。
はじめての設計をやり抜くための本 第2版 概念モデリングからアプリケーション、データベース、アーキテクチャ設計、アジャイル開発まで | 吉原 庄三郎 |本 | 通販 | Amazon
まだまだ設計についての知識は足りてないなと感じる部分が多いので改めて概論から学ぼうと思った次第です。
表紙にも「概念モデリングからアプリケーション、データベース、アーキテクチャ設計、アジャイル開発まで」とあるので気になるところは網羅できそうな感じです。
で、読んでみたんですが正直イマイチでした。
なので今回はこの本について書いてみたいと思います。
概要
本書は導入編・設計編・アーキテクチャ編の3部構成になっています。
そもそもなんですがこの本は自社開発企業のWebエンジニアではなく、SIerを想定して書かれたものであることに読み始めてから気づきました。
なのでアプリケーションの設計手法というよりも業務パッケージ開発の上流工程のための本といったところです。
表紙だけじゃなくちょっとだけでも中身を見てから買うべきでした。
とはいえ参考になる部分が無かったわけではありません。
3章の概念モデリングやデータベース論理設計の話は実践でも活かせそうでした。
特にデータベース設計については以前このブログでも書いた正規化の話がより詳細に解説されていました。
気になった点
読んでみて気になった点をいくつか挙げたいと思います。
設計以外の話に逸れることが多い
上でも述べたように本書はアプリケーションの設計手法の本ではありません。
タイトルからして設計にフォーカスした本だと思ったのですが、そうではなくSIerの振る舞い的な話の割合もそこそこ多いです。
例えばベンダーとしてクライアントとどう向き合うかみたいな話がよく出てきます。
なので求めていたものと違うなぁと感じました。
一つ一つの内容が薄い
↑と関連しますが、文量が多い割に話があちこちに逸れるので内容はそこまで濃くありません。
「はじめての」とあるので初心者向けの概説と思えば仕方ない部分かなとも思います。
用語の使い方が間違っている?箇所が散見
一番気になったのはこれです。
例えば6章「アーキテクチャ設計のアプローチ」の「依存性の注入(DI)」の項で、
つまりDIを使うことで、クラスが持っていた責務を外部に委譲し、依存関係を減らすことができるのですが、その分、クラスの内部構造を少し外部に露呈することになります。
「はじめての設計をやり抜くための本 第2版 概念モデリングからアプリケーション、データベース、アーキテクチャ設計、アジャイル開発まで」吉原 庄三郎著 p.294
とありました。
前後を読み返してみても意図するところはよくわかりませんでしたが、これは違うでしょうと思います。
他にも、
TDD (Test Driven Development) は、テスト駆動開発とも呼ばれるもので、システム開発におけるテスト手法の1つです。
「はじめての設計をやり抜くための本 第2版 概念モデリングからアプリケーション、データベース、アーキテクチャ設計、アジャイル開発まで」吉原 庄三郎著 p.237
という文もあり、これも誤りです。テスト駆動開発はテスト手法ではなく「動作するきれいなコード」を書くための開発手法です。
このように、所々で用語の使い方が誤っていると見られる箇所があり、主にその意味でイマイチでした。
まとめ
技術本買う時はやはり中身を見てから買ったほうがいいなと改めて反省しました。
他にもまだ読んでいない本があるのでまた記事にしたいと思います。