LoginSignup
5

More than 3 years have passed since last update.

Vue3到来記念。Vueのアレって結局なんだっけシリーズ。〜render関数 編〜

Last updated at Posted at 2019-12-08

はじめに

こちらは『DMMグループ '20卒内定者 Advent Calendar 2019』8日目の記事になります。

実装のみを見たい方は「render関数でIconコンポーネントを作る(準備編)」からご覧ください。

本題:render関数ってなんだっけ?

Vue.jsも少しばかり時間が経ってきたのかな?と思うこの頃ですが、ご存知の方よりも知らない(使ったことない)という方が多いであろう render関数 についてお話します。

1. そもそも

まずVueの環境をインストールしますが、生成されたうちのmain.jsにてrender関数を見かけるかと思います。

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.環境

  • node: v11.10.0
  • @vue/cli: v4.1.1
  • IDE: WebStorm 2019.2
  • PC: macOS Mojave v10.14.4
  • その他詳しくはこちら

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を定義する必要がありませんので、こういったシンプルな書き方ができるのです。

完成形

スクリーンショット 2019-12-08 2.39.33.png

おまけ

せっかく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>

完成形

スクリーンショット 2019-12-08 3.29.24.png

おわり

render関数、使ってみたくなりましたか?

もしそう思っていただけると記事にした甲斐があるので嬉しいです。

フロントエンドの開発経験としてはまだまだ若いですが、これから定期的に発信させていただければと思います。最後までご覧いただきありがとうございました。

ご意見や改善点、質問などありましたら、ぜひツイッターまでよろしくお願いします!

今回のサンプルリポジトリ

自己紹介などもある+αの記事はこちら

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5