font-size の指定位置によって要素の高さが変わる問題
こんにちは、たじまです。ブログの名前をデフォルトから変えてみました。
さて、今回は先日 CSS と戯れていた際に発見した謎とその正体を備忘録の意味も込めてブログに綴ります。わかってから見直すと何故わからなかったのかがわからない現象に陥りますが過去の自分を助けるべく綴ってみようと思います。
また、もし見当違いなことを言っている場合はそっと教えていただけると飛んで喜びます。
問題: font-size の位置によって要素の高さが変わる
ピュアな HTML+CSS で実装された UI を JSX で再実装していました。 すると旧 UI と新 UI で下の図のような要素の高さが違うという現象が起きました。
JSX ではテキスト部分を実装する際に <Typogrphy />
という font に関わるスタイルを指定するコンポーネントを使いました。
そのため元の実装と比べると font-size
を指定する箇所が移動し、結果見た目が変わってしまいました。
( ※ 見比べしやすいように図では横に並べていますが display: flex;
などはかけていません。)
codepen でも再現してみたので触ってみてください。
See the Pen why font-size by Tajima Sachiko (@schktjm) on CodePen.
原因: 要素の高さ判定
何故今回のような違いがでてしまったのかというと、要素の高さの算出方法に原因がありました。
inline box コンテンツの場合、行ボックスの高さ計算は line-height
x font-size
で算出されます。
( 正確には影響しているスタイルなどによりもっと複雑です。正確な情報は公式ドキュメント参考にしてください*1 )
つまり、 wrapper の要素で font-size
をしている要素の高さは次のように算出されます。
font-size x line-height = 12px x 1.5 = 18px
しかし Text を囲んでる要素で font-size
を指定している場合は、要素の font-size
は親から継承されている値を使うため次のように算出されます。
font-size x line-height = 16px x 1.5 = 24px
その結果 font-size
の指定箇所によって見た目に違いが発生していました。
問題解決方法
簡単にですが解決方法を挙げます。
height
をつける
要素に高さが指定されていなかった結果、中身のコンテンツによって自動計算されていました。 そのため高さを指定すれば解決します。
フレックスコンテンツにする
高さを指定するものと基本は同じですが、フレックスボックスレベルに従って要素がレイアウトされるとフレックスコンテナーの高さに要素が引き伸ばされるため有効な手段です。今回の場合では wrapper
指定している CSS に display: inline-flex;
を付け足すことで同じ高さになります。
そういうものだと理解する
もちろん理解すれば問題も発生しません。
おわりに
既存 UI の再実装により HTML+CSS の仕様についてひとつ詳しくなりました。複雑なのでブログには載せませんでしたが、 W3C のドキュメントにもっと詳しい inline box の高さの算出方法が載っていたため気になる方はそちらもご覧ください。
CSS Inline Layout Module Level 3
また文中に発生する単語は MDN や W3C の単語をそのまま引用しているだけなのでごちゃ混ぜになっている可能性があるため意味わかんねえよという時はそっと教えてください。
感想
実装した当時は何が起こっているのかさっぱりわかりませんでした。助けてくださったみんなには感謝の気持ちしかないです。 正しいソースを探すために MDN を飛び越えて W3C を初めて読み込んだのが楽しかったです。
2021年ふりかえり
はじめに
こんにちは、たじまです。今年度が始まったころに出した 2021年の目標でお話ししたように、年度末ではなく年末に目標を振り返ってみようと思います。
一年区切りでやらないのかの理由としては年末に年納めの雰囲気を感じたいからです。小さい頃は冬休みがあり、家族でスキーに行き、大晦日に頑張って夜更かしをし、近くの小さい神社に除夜の鐘を突きに行っていましたが、大人になるにつれそういった季節を感じる瞬間が少なくなってきたのが少し寂しくかんじますね。
こちら、目標記事です。
私は人を見て良いと思ったものを真似することをよくやってしまうのですが、この目標を書いたときいろいろな記事を参考にしすぎて構成がそっくりになってしまい、当人からツッコミをもらってしまったので、 自戒の念を込めこの記事を書き終えるまでは TL に流れてくるふりかえりブログを読まない ようにしています。
丁度、この記事を書きながら聞いていたゆる言語学ラジオの忘年会ライブでは、水野さんも堀本さんも構成を作る能力に関しては双方負けていない自負があると仰っていました。話の構成を作る能力は魅せる能力に比べて努力で伸ばせるものであるから負けたくないとの事です。私も優れた人の真似ばかりするのではなく自分の能力を上げていかないとなぁと考えたラジオでした。
ゆる言語学ラジオ忘年会ライブ【流行語大賞決定&ひたすらエモい話】 - YouTube
長くなってしまいましたがそれでは本編参ります。
OKR 評価
O: Web フロントエンドの知識と経験を得る
Key Results | 達成率 |
---|---|
KR-1 | 60% |
KR-2 | 0% |
--- | --- |
総合 | 30% |
この項目に関しての反省点は、学生から社会人へとライフステージが変わっていく中で学習リソースの変化を予測できなかったこと、目標立てた時点で具体的なものが自分で分かっていなかったことが原因だと考えられます。SNS を見ていると強い人は OSS コントリビュートしてるし、 bio に書いてあると強そうに見えるしといった理由でこの KR を設定しました。しかし常日頃から GitHub を見たりする習慣もなければ、一番難易度が低いといわれる英語翻訳も自信がなく積極的に参加することができませんでした。
また、社会人になりプロダクション開発に参加すると、趣味開発をしていた時とはインプットする内容が変わり、最先端の技術よりもリリースされてからいろんな会社で先行導入例があり散々擦られている技術だったり、関わっているプロダクションに導入可能かどうかといったものを多く調べるようになりました。そのためこの一年企業さんのテックブログの読んだ本数は学生の時と比べ倍以上になってると思います。
来年は今年得た反省を踏まえ、実情に合って現実的でかつ現在の自分より少し高い目標を立てたいと思います。
O: 自分の技術力を可視化する
Key Results | 達成率 |
---|---|
KR-1 | 0 |
KR-2 | 0.22% |
--- | --- |
総合 | 0.11% |
ひどいですね、自己最低評価です。 KR-1 のポートフォリオサイトは完全に忘れていました。今年の前半は少なくともやるぞと Figma をこねくり回していたんですが社会人が楽しくすっかり忘れこの記事を作る際に目標記事で思い出しました。反省です。
KR-2 は社会人になる意識もありあえて高めに設定していましたが去年同様、アドベントカレンダーの時期しか書かないという結果になりました。アドベントカレンダーを書くことが経験になり技術ブログに向けた心理的ハードルは低くはなっているのにこの時期以外にかけないのはモチベーションの問題だと思うので時折目標を見直して頑張ろうと思います。
O: 心身を健やかに保つ
Key Results | 達成率 |
---|---|
KR-1 | 51% |
KR-2 | 100%(?) |
KR-3 | 100% |
--- | --- |
総合 | 83% |
達成 👏
KR-1 の内訳を挙げると、リングフィット7回、ランニング 22 回、筋トレをカウントしていないのでランニングと同じ 22 回として 51 回です。しかしこれは運動にはまってた4~7月がほぼすべてで夏暑くなってランニングの習慣をやめて以降、一切やっていないのが反省です。
KR-2, KR-3 は主に心の健康に関しての目標です。KR-2は複数という曖昧な設定にしてしまいましたが精神的な依存先を増やすという目標をもって生活でき、所属コミュニティの分散に成功できたので 100% にしました。
旅行に関しては当時のコロナ生活で我慢していた心の叫びをうまく発散し楽しむことができました。
以上が今年(4 ~ 12月)の目標達成度合いと振り返りでした。
その他
反省はここまでにして記憶に残った面白かったものをダダ流しします。
今年のおすすめ読んだ漫画
(一つ一つ語ろうと筆を執ったのですが書き終えるよりも前に年が越しそうだったので断念しました。)
連載中
- 葬送のフリーレン
- 【推しの子】
- 左利きのエレン
- 怪獣8号
- ヴァンピアーズ
- よふかしのうた
- ブラックラグーン
- BEASTARS
- 明日、私は誰かのカノジョ
- ツンデレ悪役令嬢リーゼロッテと実況の遠藤くんと解説の小林さん
- かげきしょうじょ!!
- 薬屋のひとりごと
- ヒマチの嬢王
完結しているもの
読み切り
- 地獄楽
- 皇の器
- クローゼットガール
- 赤錆森
ゲーム
今年は AmongUs にはまりました。幸いなことに週末になると AmongUs が開催されるサーバーを友人が紹介してくれ他おかげで楽しくぷれいすることができました。
また人狼ゲームを見ることにはまりました。戦法など覚えられたらオンラインゲームで参加したいなと思っています。
お酒
今年は取り寄せでクラフトビールをいろいろ飲みました。実は会津にいるときは少し外に出ればおいしい日本酒が飲めたので、ビールを飲むことがあまりなかったです。 東京に来てからはオンラインで帰る日本酒があまりないためクラフトビールを飲んでみました。その他東京で飲めるおすすめなお酒あれば紹介してください。
おわりに
この前社会人になったと思ったらあっという間に年末になってしまいました。 (卒論に取り組む前の)学生の頃と比べると自由に使える時間が少なく感じるし、業務中にプログラミング意欲が満たされてしまうとわざわざ土日にやらなくてもいいかという気持ちになりがちでした。
来年は目標を社会人向けかつ自分ができそうな目標に改め頑張っていこうと思います。
最後までお読みいただきありがとうございました。
Figma の SVG アイコンと GitHub リポジトリを自動同期する
この記事は ZOZO Advent Calendar 2021 の 20 日目の記事です。
はじめに
こんにちは、田嶋です。この記事は ZOZO のアドカレの記事2本目です。最近はポケモンと自作PCのセットアップに勤しんでいます。フロントエンドエンジニア全然できてません。
前提
今回は Figma と GitHub リポジトリ の SVG アイコンを自動で同期させ、アイコン管理を楽にしちゃおうという記事です。
タイトルでは自動同期と言ってますが、Figma が変更されたかどうかの検知ではなく cron での定期実行でリポジトリを Figma の最新に合わせるすることで、自動同期を実現しています。
こちらの記事が大変参考になりました。GitHub か GitLab かの違いなのでもうほぼ二番煎じです。
この記事で述べられている Github Action の figma-action ですが、現在はメンテされておらずアーカイブされていました。一応自分でも使ってみましたが上手く動かないようでした。
方法
実現方法について簡単に説明します。
1. Figma API を叩けるようにする
Figma Developers で API を確認することができます。
Figma のファイルは node
と呼ばれる木構造で構成されています。 node は node.type
で RECTANGLE
や COMPONENT
など判別することができます。今回使用する Figma ファイルは下図のレイアウトです。今回は Page1
の node に 背景に使っている Rectangle 1
と Icon のコンポーネントが複数あるという構成で、フレームにしていないため Icon は Rectangle の子ではなく兄弟です。
2. Node.js で SVG をダウンロードしてくる CLI を作成する
Figma の API では子要素以下のファイルを直接ダウンロードする方法がないため、以下の階構成にしました。
- Icon を構成している node の
id
name
を取得 id
を使用し画像取得用 API を叩き画像URL を取得fs
を使用しローカルに画像をダウンロード
以下がコードです。
require("dotenv").config(); const fetch = require("node-fetch"); const fs = require("fs"); const baseUrl = "https://api.figma.com/v1/"; const figmaToken = process.env.FIGMA_TOKEN; const fileId = "YhHWg9mZ6GdKkseHAAS1gw"; // Figma ファイルの API から component の ID を取得 const getComponentIds = async () => { try { const res = await fetch(`${baseUrl}files/${fileId}`, { headers: { "X-Figma-Token": figmaToken, }, }); const data = await res.json(); const { components } = data; return components; } catch (error) { throw error; } }; // Id から画像の URL を取得 const getImageLinks = async (component) => { try { const componentKeys = Object.keys(component).join(); const res = await fetch( `${baseUrl}images/${fileId}?ids=${componentKeys}&format=svg`, { headers: { "X-Figma-Token": figmaToken, }, } ); const data = await res.json(); const images = Object.keys(component).map((key) => { const { name } = component[key]; const link = data.images[key]; return { name, link }; }); return images; } catch (error) { throw error; } }; // ローカルに画像を保存 const downloadImages = async ({ link, name }) => { const res = await fetch(link); const data = await res.buffer(); fs.writeFileSync(`../src/${name}.svg`, data); }; const main = async () => { const component = await getComponentIds(); const images = await getImageLinks(component); images.forEach(async (image) => { await downloadImages(image); }); }; main();
3. Github Actions で定期実行する
あとは Github Action で定期的に上記のコードを実行するだけです。猿でもできますね。(私は二時間くらいここで躓きました。)
説明できることが少ないためコードを載せておきます。
name: Export SVG from Figma on: schedule: cron: "0 0 * * *" jobs: actions: runs-on: ubuntu-latest env: FIGMA_TOKEN: ${{ secrets.FIGMA_TOKEN }} steps: - name: checkout uses: actions/checkout@v2 - name: node setup uses: actions/setup-node@v2 with: node-version: "12" - run: cd script; npm install - run: cd script; npm run start - name: Commit run: | git config --local user.email "github_actions@example.com" git config --local user.name "GitHub Actions" git add . git commit -m "downloaded from Figma" - name: Push changes uses: ad-m/github-push-action@master with: github_token: ${{ secrets.TOKEN }}` branch: master
GitHub Action の schedule は UTC なためこちらじゃ JST の 9:00 に実行されます。あえて 0:00 (UTC) にしたのは起きている時間のほうがバグった時に対応できるからと思たという言い訳です。
まとめ
作成したコードはこちらになります。
今回は簡単な Figma ファイルで試したためもっと複雑な要求に対応するためにはもう少し改良を加える必要があります。例えば Solid
Outline
のアイコンはそれぞれ別に管理したいなど。しかし適切な API とドキュメントが提供されているため改良は容易にできそうです。
また curl 芸人ではないため、今回は Node で画像をダウンロードするスクリプトを書きましたが、 Github Actions で完結する方法もあります。
最後に
アドカレ2本目を書くことにあたって実は他に以下の二つのネタを用意していました。
しかし 1 はあまりに複雑で簡単にできなさそうだなと諦めたのと、 2 は Beta 版なため Figma の Organization プランでしかつ買うことができず、個人では契約することができなかったので諦めて前から気になっていたこちらをやってみることにしました。
私の所属している部署ではデザインツールは XD が使用されていますがこうしてアドカレで Figma の利便性を訴え布教をしていきたいです。
参考にした資料
no such file or directory, open の理由を知る - Qiita
https://www.figma.com/developers/api
平凡なフロントエンドエンジニアの Flutter 入門
この記事は ZOZO Advent Calender 2021 の6日目の記事です。ちゃんと事前に記事を用意してなくてすみません!!
はじめに
こんにちは、たじまです。 今回は前から興味を持っていた Flutter に入門してみました。普段業務ではフロントエンドエンジニアをしていて、他の職種について触れる機会がなかったため「 Flutter 触れば iOS と Android エンジニアの気持ちがわかるようになるやろ!」とアドカレを機に体験してみました。(これは両アプリエンジニアに怒られそうですね。) 内容的に「これをやりました!楽しかったです!」で終わりそうなのでできるだけフロントエンジニア目線で感想を述べてみたいと思います。
やったこと
Webと比べたときのアプリの大きなメリットというのはやはり、アプリ内要素指でタッチしったり指でスライドしたときのぬるっとしたものを掴んでいる UX(UI?) です。 そこで、あの体験を再現すべく Hello World から始めてみました。
勉強で参考にしたもの
kazutxt さんのこちらの本が大変参考になりました! 初級編しか読めていないですが大変詳しく段階的に書かれていてわかりやすかったです。
できたもの
carousel_slider といいうライブラリを使って作ってみました。 面倒臭い印象があるカルーセルを一瞬でつくれました。
感じたこと
Hello World しながら感じたことを時系列順に述べてみます。
開発環境まわり
- Xcode のインストールが無限すぎる。
- Android や iOS のエミュレータを立ち上げようとしたが、マシンスペックが足りずに Mac が落ちた。フロント開発だと Chrome と Node.js さえ起動できればいいので MacBookAir で耐えていたが限界。実機で試してみる。
- iOS にビルドする際に開発チームや iPhone 側で開発元を許可しないといけないということが新鮮。
- Android は大昔の Xperia 端末しか持ってないので断念。ただ web と ios は同じアプリが動いたので感動。
開発体験
- 軽く触っただけだが
Container
のような element が CSS のプロパティを型として持っているため、文字を打つと補完ができてわかりやすい。また<bold> や <h1>
みたいな element としての意味とスタイルを持ってしまったりしているような古の tag がないため綺麗に設計できそう。 - 見た目の構造は DOM Tree を直接書いているような感じで分かりやすかった。class にはあまり馴染みがないが名前などが CSS のプロパティに似ているため直感的にわかる。
Center
で左右だけじゃなく上下の中央揃えになる感覚が新しい。- google だから? Material Design の色々がデフォで入っていて馴染みの感覚ができた。
- icon の自動生成だったり VSCode での開発から Android/iOS/Web 全てにビルドできるのが開発障壁が下がっている気がする。
- ビルドが重い。私の MBA は常に悲鳴を上げている。
終わりに
思ってたよりも Dart という言語が分かりやすくとっつきやすい言語でした。 カルーセルの実装で終わってしまいましたが本当は上からカードが降りてきて左右に分け捨てるような UX を作りたいのでもうすこし色々試してみようと思います。それが終わったら PWA でも作って比較したいです。
自分のマシンが常に火を吹きそうな状態なため、ディスプレイの数を減らし並行で動いているアプリも終了したため開発環境がつらかったです。常にいいマシンが欲しいですね。
ありがとうございました!
2021年の目標
はじめに
こんにちはたじまです。もうすでに年が明けてから3ヶ月が過ぎ去ろうとする今日この頃、皆さんいかがお過ごしでしょうか。私はやっと重い腰を上げ、卒論で忙しいのを理由に決めていなかった目標を考えることにしました。
卒論終わったら2021年の抱負を書こうとしたのを思い出した。
— たじまん 🌕🏠 (@schktjm) 2021年2月22日
最近気がついたら一週間経っているを何度も繰り返しもう少し計画立てて人生を謳歌したいなと思い、初めてですが目標をまとめてみることにしました。
目標の決め方
目標を考える際、いろんな方のブログを参考にさせていただきました。特にこの二つの記事の目標設定フレームワークを参考にさせていただきました。
SMARTとOKRで比べた時、具体的で達成可能な目標を掲げるよりも若干気後れするくらい高いレベルを設定し7割の達成を目指すというOKRのほうが、夢みがちな自分に向いていると思ったのでこちらを採用します。
また、ストレッチゴールとなる目標の立て方に悩んだので、とりあえず達成したい目標や願望や欲求をたくさん書き出してからカテゴリー分けをし、願望を取り除きました。そしてカテゴリーごとに残ったものからそもそも何故その欲求が生じたのか目的を考え、それらを包括する目標を設定するボトムアップ式で考えていきました。
OKR
初めに宣言すると、このOKRの開始は4月になってしまいましたが 締め日は年末に設定したい と思います。学生の間では年度が変わるごとに学年が変わることで心機一転することができましたが、社会人ではそのような人生のイベントは起こらないだろうと予想してのことです。
O: Web フロントエンドの知識と経験を得る
KR: 技術本を5冊読む
KR: OSS に3回コミットする
周りの人に比べ自分の技術力のなさ感じているので文字に起こして気を引き締めました。 「Webフロントエンドが好き」という言葉を感情だけではなく具体的な知識や経験を伴わせることが目標 です。学生時代に比べ社会人では自分の時間を確保しづらいと思うので成果指標は低めに設定してます(それでも2020年よりは多いですが)。
O: 自分の技術力を可視化する
KR: ポートフォリオサイト作る
KR: 技術ブログを9本書く
2つ目の目標も技術のことですがこれはつまりアウトプットです。自分って結局何ができるんだっけと説いた時にポートフォリオさえないのにWeb フロントエンジニア名乗っていいのかと自問自答しながらずっとやるやる詐欺をしていました。頑張ります。
O: 心身を健やかに保つ
KR: リングフィット or ランニングを100回する
KR: 複数のコミュニティに所属する
KR: 旅行に行く
心と身体を健康に保つのは楽しく生きる上で必須ですよね。最近リングフィット用のヨガマットとランニング用の高いシューズを買って体を動かすことにハマっています。この100回という数字は残りの9ヶ月の約4割を目標に設定しました。成果指標は七割達成を目指すのでだいぶ弱気ですがおそらく8時間労働に慣れないうちは就労後に運動できないと見積もっています。また複数のコミュニティというのは心の健康です。一つのコミュニティに依存してしまうと逃げ場を失いメンヘラになりやすいと思ったので入れました。
その他願望
OKRに含めるには願望がすぎるかなと思いつつでも書きたかったので番外編です。
- 物事に関心を持ち自分の意見を持つ
- TOEIC 700点取る
- 漢字力つける
- すぷらガチマ全部A帯
一つ目はこのこのツイートを見て以来思っていたことです。
今が何の時代かは知らないけど、「SNSで他人の投稿見て一喜一憂してるうちに1日が終わる人」の時代は来ない。
— 澤山モッツァレラ@編集者 (@diceK_sawayama) 2021年3月7日
一方、ネットは使いこなせば信じられない量のインプットを得られるので、うまく使いたいですね。
自分の意見を大事にするよりも専門家の意見を聞いた上で立場を決めたいと思い過ごしていたらいつのまにか何にも興味がない人間になってしまいました。積極的に意見していこうということではなく、他人のコピーでは無い自分を見つけたいなと思っています。
英語力はずっと思っているんですがいつも願望で終わります。
おわりに
具体的な目標を立てるのは初めてなので多いのか少ないのか分かりませんが達成できるようにしていきたいです。まずやることはちゃんと記憶して忘れないことですね。
卒論終わったらやりたいことはもっとたくさんあったのに気付いたら春休みが終わってました。麻雀楽しい。とりあえず明日から始まる社会人生活が不安でどきどきしています。
プロフィール画像を刷新しました!
こんばんは、ついに東京都民になったたじまです。 いつもアドカレの時期にしかブログを書か無いので文頭に何を述べればいいか少し悩んでしまいました。
このブログではつい先ほどツイートした、新しいプロフィール画像について作るきっかけと作る工程など語っていきたいと思います。
実は公開するのがめちゃめちゃ恥ずかしくて完成してるのに Twitter のプロフィール画像変えるまでに1日かかりました。 まだちょっと恥ずかしくて変なツイートしちゃいそうなのと完成するまでの工程を晒したいな〜と思ったのでメモついでにブログに書きます。
長らく使ってたプロフィール画像を変えました🙌
— たじまん 🌓🏠 (@schktjm) 2021年3月22日
もともとの北海道行った時に撮ったソフトクリームの画像と顔を押し付けて深呼吸したいくらい好きな猫を組み合わせてアイデンティティを出してみました! pic.twitter.com/IMV3q2M9Lr
きっかけ
ずっとプロフィール画像をアイコン化したいな〜とは思ってたんですが口に出したのはこのツイートが初めてのようです。
自分もなんかsvgにしやすいアイコンが欲しくなってきた
— たじまん 🌓🏠 (@schktjm) 2020年12月23日
ちなみに私は複数のワークスペースのプロフィール画像管理に Gravatar を使っているのですが過去の履歴はこんな感じでした。懐かしいです。
下書き
とりあえず猫が好きなので猫の落書きをたくさんしました。私は落書きに iPad の Procreate というアプリを使っています。
最初右下の寝てる猫とか可愛いなと思いました。しかし猫をプロフィール画像にしている方を普段から Twitter で無限に観測しておりオリジナル性がないなということでボツに、ソフトクリームと猫を組み合わせたものに決定しました。
トレース
ここから Figma を使いトレースを行いました。 画像の SVG 化です。
Procreate で手振れなどは抑えてますがそれでも手書きなので幾許かの線の震えがあります。 それを考慮し下書きを手本にしながら綺麗なベジェ曲線を描いていきました。
配色
なんとなく最近桃色が好きなのでストロベリーアイスっぽくしようと心に決めていました。
Google で アイス イラスト
と調べて、美味しそうなストロベリーアイスの色をキャプチャしてきました。
食べたら美味しそうなアイス にしようと思い派手なピンクよりリアルでありそうなピンクに近づけました。
そして白と黒の上に置き、霞んで見え無いかどうか調べたり、
グレースケール化できるサービスを使い色の彩度を調節しました。
こちらのサービスを使いました。
他にもカラーユニバーサルデザインチェックも行ってみて、実際微妙でしたが自分の目標である 美味しそうな アイスの色にならなそうだったので諦めました。
ついに完成👏
配色を決定してから一晩寝かせてやはりよかったので完成しました。 しかしやはり自分のプロフィール画像にするということは今後自分の顔になるということなので少しどころじゃなく恥ずかしかったです。
おわりに
これで卒論終わったらやりたかったことの一つが終わりました。間に合ってよかったです。 あとは自分のPFを作る(進捗なし)、2021年の抱負を書く(30%くらい) を頑張って社会人になるまでに完成させようと思います。
ここまでご覧いただきありがとうございました。
Composition API + Typescript で Vuex をリアクティブに安全に使いたい
こんにちは、毎年アドカレの時期しかブログを更新しないたじまです。(今年は少し頑張ったかな?)
この記事は、 Aizu Advent Calender 2020 の14日目の記事です。 13日目は虚無さん、15日目は spookydokey さんです。
さて、 Composition API といえば今年の 9/18 に正式リリースされたVue.js v3.0 が記憶に新しいかと思います。
この記事では、 Vue.js 2系 で先取り Composition API と、Vuex を使っているプロジェクトでなんとか型安全に Vuex を使えないかと足掻き苦しんだ末に私なりに出した解決方法をまとめてみました。私がこのプロジェクトを受け継いだ当時はまだ Composition API 周りのライブラリが発展しておらず、ベストプラクティスから程遠いかもしれないので、こうしたほうがいいよという助言をいただけるととても嬉しいです。
Composition API とは
簡単に紹介します。
Introducing the Composition API: a set of additive, function-based APIs that allow flexible composition of component logic. 1
Vue 2系 では宣言的な Vue インスタンスと HTML like なテンプレートにより、デザイナや経験の浅い開発者も簡単に使いこなすことができ、小規模から中規模なプロジェクトを簡単に構築することができます。 しかしながら大規模なプロジェクトでは、複雑なコンポーネントの推論の難しさや複数のコンポーネント間でロジックを抽出し再利用する方法の難しさ、TypeScript との相性の悪さから敬遠されがちです。
Composition API を含め Vue 3系ではコンポーネントを作る際の柔軟性と型推論を念頭に置いた設計により、複雑なコードもよりスマートに書くことができるようになりました。 RFC 詳しく書いてあるので気になる方はそちらを読んでください。
Composition API で Vuex を使う上での困った点
Composition API の強みであるロジック部分のモジュール分けなどはこのブログが参考になりました。
このブログでは Vuex 関わる部分だけ簡単に書いていきます。
Vuex + TypeScript を使うときは、 vuex-module-decorators などのライブラリを使うことが多いと思いますが、 Vue Class Module の書き方に依存していて Composition API での使い方がわからなかったり、そもそも自分に経験がなかったりなどの理由で導入を見送りました。
下記のコードは、ボタンをクリックしたら Vuex の action 経由で text を fetch するというコードの一部です。
onClick
で getText
を dispatch しています。Vuex でいつもお世話になっている mapState などのヘルパー2 は this に依存するためつかえません。
そのため、action を下記のように文字列で指定する必要がありました。しかしそうすると少しの誤字でエラーになったりコードジャンプができないなど開発する上で不便に感じる点が多々ありました。
setup(props: Props, ctx: SetupContext){ const text = computed(() => ctx.root.$store.state.text); const onClick = () => { ctx.root.$store.dispatch("getText"); } return { text, onClick } }
Composition APIで書いた Vue のコード全体
<template> <div> <p>{{ text }}</p> <button type="button">Push!</button> </div> </template> <script lang="ts"> import {computed, defineComponent, SetupContext} from "@vue/composition-api"; type Props = { } export default defineComponent({ setup(props: Props, ctx: SetupContext){ const text = computed(() => ctx.root.$store.state.text); const onClick = () => { ctx.root.$store.dispatch("getText"); } return { text, onClick } } }) </script>
改善前の vuex のコード
import Vue from 'vue'; import Vuex, { ActionTree, MutationTree, StoreOptions } from 'vuex'; import { RootState } from './type'; import API from './../api'; Vue.use(Vuex); const mutations: MutationTree<RootState> = { setText(state, payload: { text: string }) { state.text = payload.text; } } const actions: ActionTree<RootState, RootState> = { async getText({ commit }) { try { const { text } = API.getText(); commit('setText', { text }); } catch (e) { console.error(e); } }, } const store: StoreOptions<RootState> = { state: { text: '' }, mutations, actions } export default new Vuex.Store<RootState>(store);
足掻き苦しんだ末の解決方法(仮)
このコード群をなんとかして読みやすくしたい(せめて補完がほしい)と悩んだ末に、参考にしたのがこの記事と Vuex+TS を使っていたプロジェクトのコードでした。
1. Vuex の state をリアクティブに使いたい
computed
で逐一参照すれば問題ない話ではあるのですが、ひっぱってきた Vuex の state を reactive でひとまとめにしたい気持ちや階層化された値の監視に対応したいなどなどあったのでカスタムフックスにして切り分けました。(カスタムフックスは参考にした記事と全く同じです。)
ロジック部分のみに切り分けたモジュール
import { reactive, SetupContext } from "@vue/composition-api"; import { useVuexRef } from "./useVuex"; export default (ctx: SetupContext) => { const state = reactive({ text: useVuexRef<string>(ctx, "text"), }) const getText = async () => { await ctx.root.$store.dispatch("getText"); }; return { state, getText, };
Vuex の state をリアクティブにつかうためのカスタムフックス
import { onUnmounted, ref, Ref, SetupContext } from "@vue/composition-api"; export function useVuexRef<T>(context: SetupContext, key: string): Ref<T> { // 最後に as Ref<T> をしておかないと data.value の更新の部分で型エラーが起きる const data = ref<T>(context.root.$store.state[key] as T) as Ref<T>; const unwatch = context.root.$store.watch<T>( (vuexState: any) => { // 階層化された値の監視には対応してない return vuexState[key] as T; }, (newVal: T) => { // refのvalueを更新すればコンポーネントが反応する data.value = newVal; } ); onUnmounted(() => { unwatch(); }); // Refを返す return data; }
さて、こうしてカスタムフックスに分けて書くことができましたが dispatch の部分や state の呼び出し部分が文字列で指定していてまだ不安があります。まだこの時点ではいいかもしれないのですが state の階層が深くなったりしていくと闇が見えるのでなんとかしたいです。
2. dispatch や state の呼び出しに補完を効かせたい
ほんとは TypeScript で方安全にしたいのはやまやまなのですが、知識不足&経験不足により方法が皆目見当がつかなかったため見送りました。
これを一言で表すと Vuex の action や mutation を定数として使うためのヘルパー関数です。以前 Vuex + TS でやっていたプロジェクトのコードを参考にしました。 (作成者は @uzimaru 君です。公開しよっかな言ってましたがまだっぽいです。)
type.ts
export type Action<A extends string, S, R = S> = { [key in A]: Vuex.Action<S, R>; }; export type Mutation<M extends string, S> = { [key in M]: Vuex.Mutation<S> }; export const typesEnumeration = <T extends string>( obj: { [key in T]: any }, prefix?: string ) => Object.keys(obj).reduce( (acc, x) => ({ ...acc, [x]: prefix ? `${prefix}/${x}` : x }), {} as { [key in T]: string } );
Action
と Mutation
は、Vuex.Action
と Vuex.Mutation
型のラッパーです。
Action や Mutation の関数一覧を union 型で宣言し、 Action
に渡してあげることで関数の宣言忘れを防いだり誤字を防ぐことができます。
その上で、宣言した Action らを typesEnumeration
を使い、関数名: それを文字列化したもの
のオブジェクトに変換します。
mutation.ts
type MutationTypes = 'setText' | 'clearText'; const mutations: Mutation<MutationTypes, RootState> = { setText(state, payload: { text: string }) { state.text = payload.text; }, clearText(state) { state.text = ''; } }; export const MutationTypes = typesEnumeration(mutations); export default mutations;
actions.ts
type ActionTypes = 'getText' | 'clearText'; const actions: Action<ActionTypes, RootState> = { async getText({ commit }) { try { const { text } = API.getText(); commit(MutationTypes.setText, { text }); } catch (e) { console.error(e); } }, clearText({ commit }) { commit(MutationTypes.clearText); } } export const ActionTypes = typesEnumeration(actions); export default actions;
こうして、action.ts
のアクションの中で mutation を MutationTypes.setText
のように指定してコミットできます。
Composition API によって切り出したモジュールの中でも、 ActionTypes
や StateTypes
を使うことで補完を効かせて Action や State を指定することができます。
import { reactive, SetupContext } from "@vue/composition-api"; import { useVuexRef } from "./useVuex"; import { ActionTypes } from "./../store/action.ts"; import { StoreTypes } from "./../store/index.ts"; export default (ctx: SetupContext) => { const state = reactive({ text: useVuexRef<string>(ctx, StoreTypes.text), }) const getText = async () => { await ctx.root.$store.dispatch(ActionTypes.getText); }; return { state, getText, };
こんな具合で、開発をする上で最低限の環境を整えることに成功しました。
終わりに
Compotision API + Vuex のプロジェクト開発は現在私1人で行っていて、引き継いだ当初はあまり知見もなく不安だったのですが沢山の先駆者のブログや RFC の助けを借りなんとか1人で歩いて行けるコードになりました。 もし、 Vuex をもっと型安全に使えるプラクティスや助言などあったら是非コメントいただけるとうれしいです。
参考
【Vue3に備える】実務で使うComposition APIについて考える | by slont | Finatext | Medium