Vue3到来記念。Vueのアレって結局なんだっけシリーズ。〜render関数 編〜
はじめに
こちらは『DMMグループ '20卒内定者 Advent Calendar 2019』8日目の記事になります。
初アドカレ&初個人ブログということで気楽に発信していきたいと思います。
実装のみを見たい方は以下「render関数でIconコンポーネントを作る(準備編)」からご覧ください。
ご挨拶
日頃よりお世話になっております。tettyです。
今回は私の自己紹介+αですが、大体の自己紹介はもう書いてしまったんですよね...。
というわけで、今回は自己紹介のうち 技術・業務的なこと にフォーカスすることにしましょう。
自己紹介
はい。てってぃと申します。 自己紹介にはあまり興味がなく、早く主題に移りたいので簡単に。
1. Web Developerとして活動
- Web Developerを目指してかれこれ3年ほど頑張っている。
- メイン1社、お手伝い2社に技術者として関わる。
2. 立ち上げ開発・プロダクト検証がお好き
- 好みof好み。テックを経て世界を知る=自己満足感。
- 主にフロントエンドの技術選定から構築・実装まで行う。
- サーバサイドを担当することはもちろんある。
3. UXエンジニアもまた目標
- これもまた目指して頑張っている。
- 20代半ばくらいにUXうるさいおじさんになりたい。
- 日々自分の作ったプロダクトにクレームを入れている。
4. 最近の活動
- Next.js + TypeScript + Rails5 + Graphqlでプロダクト開発
- 直近の目標はExpress + GraphqlでBFFを作ること。難しい。
- 楽しい👏
- Nuxt.js + TypeScript + PWA(SW)+ FireStoreで卒業制作を開発
- KVSとJS→TSへの移行がとてもつらみ。
- ネイティブアプリのUIやアニメーションの実装にチャレンジしている。難しい。
- 楽しい👏
- Rails5 + S3 + Lambdaでスタートアップのプロダクト開発
- PM業と開発、ドメイン知識のインプット、接待を並行でやることがとてもつらみ。
- 楽しい👏
- DMM.comへの内定承諾について
- 一瞬すぎたのでお話しすることがあまりなく。
- 別記事でまとめたいと思います。
本題:render関数ってなんだっけ?
Vue.jsも少しばかり時間が経ってきたのかな?と思うこの頃ですが、ご存知の方よりも知らない(使ったことない)という方が多いであろう render関数 についてお話します。
1. そもそも
まずrender関数が目に入るのは main.jsですが、それがこちら。
// main.js new Vue({ render: h => h(App) }).$mount("#app");
@vue/cliでVueプロジェクトをインストールすると作成されるものになりますが、この中に記述されている render: h => h(App) って何?他で使うタイミングあるの?
2. render関数
私がVueを使い始めたのは1年と少し前。「は〜。renderはAppコンポーネントを描画する?関数なんだな〜。h?なにこれ???HOCのh???」という第一印象になりました。
こちら、render関数を追っていくことで答えを見つけることができます。
// render関数 render?(this: undefined, createElement: CreateElement, context: RenderContext<Props>): VNode | VNode[]; // hのやつ export interface CreateElement { (tag?: string | Component<any, any, any, any> | AsyncComponent<any, any, any, any> | (() => Component), children?: VNodeChildren): VNode; (tag?: string | Component<any, any, any, any> | AsyncComponent<any, any, any, any> | (() => Component), data?: VNodeData, children?: VNodeChildren): VNode; }
型の通りですが「h」なる存在はcreateElementのこと。
htmlタグもしくはVueコンポーネントなどを渡すと仮装DOMを返却してくれる関数だとわかります。(ちなみにhはhyperscriptの略だそう。)
3. 過去の過ち
render関数についてまとめようと思った動機ですが、自分の過去リポジトリを漁っていてとんでもないものを見つけてしまったことにあります。それがこちら。
<template> <div> <IconAdd v-if="name === 'add'" /> <IconAlert v-if="name === 'alert'" /> <IconBlock v-if="name === 'block'" /> <IconFace v-if="name === 'face'" /> <IconFile v-if="name === 'file'" /> <IconLock v-if="name === 'lock'" /> <IconPerson v-if="name === 'person'" /> <IconTime v-if="name === 'time'" /> ...
「う゛!!!!!!!!!!!!!!!!!!!」
何がとんでもないのかよくお分かりかと思います。 これでは、選ばれなかったアイコン達がHTML自体にコメントとしてレンダリングされてしまいますね。パフォーマンスもよくありません。
さて、この暗黒物質を綺麗にしていきましょう。
render関数でIconコンポーネントを作る(準備編)
0.環境
1. ファイル構成
Iconコンポーネントを作成する際の構成は以下でいきます。
components ├── Example.vue ├── atoms │ └── IconTextField.vue └── icon ├── Icon.js ├── icons │ ├── IconCheckbox.vue │ ├── IconDoneOutline.vue │ ├── IconFace.vue │ ├── IconLock.vue │ ├── IconMoreHoriz.vue │ └── index.js └── types.js
2. アイコンに使いたいSVGをMaterial Designから拝借してきます。
3. SVGをVueコンポーネント化します。
// icon/icons/IconLock.vue <template> <svg xmlns="http://www.w3.org/2000/svg" :width="size" :height="size" :viewBox="viewBox" > <path d="M0 0h24v24H0z" fill="none" /> <path :style="style" d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z" /> </svg> </template> <script> import { iconColors, typeColor } from "../types"; export default { name: "IconLock", props: { size: { type: Number, default: 24 }, color: { type: String, default: "primary", validator: v => typeColor.includes(v) } }, methods: { getIconColor() { return iconColors[this.color] || iconColors.primary; } }, computed: { style() { return { fill: `${this.getIconColor(this.color)}` }; }, viewBox() { return `0 0 ${this.size} ${this.size}`; } } }; </script>
4. 作成したアイコン達をまとめます。
// icon/icons/index.js import IconFace from "./IconFace"; import IconDoneOutline from "./IconDoneOutline"; import IconMoreHoriz from "./IconMoreHoriz"; import IconCheckbox from "./IconCheckbox"; import IconLock from "./IconLock"; export default { checkbox: IconCheckbox, doneOutline: IconDoneOutline, face: IconFace, moreHoriz: IconMoreHoriz, lock: IconLock };
5. Iconコンポーネントに必要な属性値やカラーを用意します。
// icon/types.js export const iconColors = { primary: "#2196F3", secondary: "#607D8B", danger: "#f44336", warning: "#FF9800" }; export const typeColor = ["primary", "secondary", "danger", "warning"]; export const typeIcon = ["face", "checkbox", "doneOutline", "moreHoriz", "lock"];
render関数でIconコンポーネントを作る(実装編)
突然ですが、完成形です。(効果音)
// icon/Icon.js import { typeColor, typeIcon } from "./types"; import icon from "./icons"; export default { name: "Icon", props: { color: { type: String, default: "primary", validator: v => typeColor.includes(v) }, name: { type: String, default: "face", validator: v => typeIcon.includes(v) }, size: { type: Number, default: 24 } }, methods: { getIcon() { return icon[this.name] || icon.checkbox; } }, render(h) { const IconComponent = this.getIcon(); return h( "i", { class: "Icon" }, [ h(IconComponent, { attrs: { size: this.size, color: this.color } }) ] ); } };
templateもstyleも綺麗さっぱり無くなりました。
そして、例の暗黒物質も目に入ることが無くなりましたね。嬉しい!!!
解説
コンポーネント定義
h( "i", { class: "Icon" }, [ h(IconComponent, { attrs: { size: this.size, color: this.color } }) ] );
ついにhがでてきました。ここでは、実際にレンダリングされるコンポーネントとその属性やdirectionなどを定義していきます。
使用できるパラメータに関してはこちらを参照してください。
ちなみにtemplateを使った場合の比較は以下の通りです。
<template> <i class="Icon"> <IconComponent :size="size" :color="color" /> </i> </template>
Iconコンポーネントの取得
getIcon() { return icon[this.name] || icon.checkbox; }
コードの通りなのですが、主に暗黒物質を綺麗にした箇所がこちらになります。 アイコンリストを辞書化しているので、相応のアイコン名をkeyにとして各アイコンを取得することができます。
render関数では使用するcomponentsを定義する必要がありませんので、こういったシンプルな書き方ができるのです。
完成形
おまけ
せっかくIconコンポーネントを作成したので、もっと実用化してみましょう。
以下はIconコンポーネントを用いたTextFieldコンポーネントですが、 エラー時はアイコンごとカラーが変化するようになっています。みたいな。
// atoms/IconTextField.vue <template> <div class="IconTextField" :style="styles.root"> <Icon :color="attr.icon" :size="20" name="lock" class="IconTextField__icon" /> <input :id="`${name}-TextField`" :class="classes.input" :name="name" :value="value" :placeholder="placeholder" type="text" @input="onInput" /> </div> </template> <script> import Icon from "../icon/Icon"; export default { name: "IconTextField", components: { Icon }, props: { name: { type: String, default: "input" }, value: { type: String, default: "" }, placeholder: { type: String, default: "placeholder" }, fullWidth: { type: Boolean, default: false }, error: { type: Boolean, default: false } }, methods: { onInput(e) { if (e) { this.$emit("input", e.target.value); } } }, // Optional Chaining使いてぇ〜〜〜。 computed: { styles() { return { root: { width: this.fullWidth ? "100%" : null } }; }, classes() { return { input: this.error ? "IconTextField__input--error" : "IconTextField__input" }; }, attr() { return { icon: this.error ? "danger" : "secondary" }; } } }; </script>
完成形
おわり
render関数、使ってみたくなりましたか?
もしそう思っていただけると記事にした甲斐があるので嬉しいです。
フロントエンドの開発経験としてはまだまだ若いですが、これから定期的に発信させていただければと思います。最後までご覧いただきありがとうございました!
ご意見や改善点などありましたら、ぜひいただけると嬉しさの極みです。