kahirokunn
twitter: https://twitter.com/kahirokunn
qiita: https://qiita.com/kahirokunn
iCareでフロントの技術顧問させて頂いてる
composition apiにpull requestがマージされた🎉
最近あった嬉しいこと
Vue3.0
Vue3.0のtopics
- 1. Typescript対応の強化
- 2. jsx, tsxを標準サポート
- 3. composition api
Vueを使ってるならVue3の把握をして、いち早く追従しないとですね
ざっくりな紹介になりますので、何か気になることがあれば後ほど質問して頂けたらと思います
Typescript
- jsに型を付けた物
- テストの代わりに導入してもよし
- js docでバリデーションする代わりに書くもよし
Typescript
- jsに型を付けた物
- テストの代わりに導入してもよし
- js docでバリデーションする代わりに書くもよし
メリット
タイポ、存在しない値へのアクセスなどつまらないバグを無くせる
tsの解析ツールの恩恵を最大限に活かせる
jsx, tsx
Reactで良く見る奴。
jsファイル内でhtmlライクな記述ができる。
export default {
functional: true,
render(h) {
return (
<b-tag
class="carely-tag"
ellipsis
closable
rounded
aria-close-label="Remove file"
onClose={() => console.log('hello world')}
>こんにちは</b-tag>
)
}
}
import { FunctionalComponentOptions } from 'vue'
export default {
name: 'SomeName',
functional: true,
props: {
name: {
type: String,
required: true,
},
},
render(h, { listeners, props: { name } }) {
return (
<b-tag
class="classA classB"
ellipsis
closable
rounded
aria-close-label="Remove file"
onClose={() => listeners['onClose']}
>{name}</b-tag>
)
}
} as FunctionalComponentOptions
import { FunctionalComponentOptions } from 'vue'
export default {
name: 'SomeName',
functional: true,
props: {
name: {
type: String,
required: true,
},
},
render(h, { listeners, props: { name } }) {
return (
<b-tag
class="classA classB"
ellipsis
closable
rounded
aria-close-label="Remove file"
onClose={listeners['onClose']}
>{name}</b-tag>
)
}
} as FunctionalComponentOptions
v-on pattern1
pattern2
import { FunctionalComponentOptions } from 'vue'
import { emitGenerator } from 'functionalComponentHelper'
export default {
name: 'SomeName',
functional: true,
props: {
file: {
type: File,
required: true,
},
index: {
type: Number,
required: true,
},
},
render(h, { listeners }) {
const emit = emitGenerator(listeners)
return (
<b-tag
class="carely-tag"
ellipsis
closable
rounded
aria-close-label="Remove file"
onClose={(e: Event) => emit('close', e)}
>{name}</b-tag>
)
}
} as FunctionalComponentOptions
概要
- Vue3から組み込まれる
- Vue2の現時点でも公式ライブラリを使えば使える
- rfc段階
- Vue.jsが抱える型問題の大部分を解決するapi
資料
useCounter
import { computed, reactive } from "@vue/composition-api";
export function useCounter(initial = 0) {
const state = reactive({ count: 0 });
return {
count: computed(() => state.count),
set: (count: number) => {
state.count = count
},
inc: () => {
state.count++;
},
dec: () => {
state.count--;
},
reset: () => {
state.count = initial
}
} as const;
}
import { useState } from 'react'
const add = value => prev => prev + value
const useCounter = (initial = 0) => {
const [count, set] = useState(initial)
return {
count,
set,
inc: (number = 1) => set(add(number)),
dec: (number = 1) => set(add(-number)),
reset: () => set(initial),
}
}
export default useCounter
Sample Repo
Vue composition apiを触った際に作成したシンプルなサンプルです
mountされたらイベントをアタッチして、マウント解除されるタイミングで解除してます。
x, yはsourceがマウスカーソルなので代入禁止の意味を込めてcomputedにしています
import {
reactive,
onMounted,
onUnmounted,
computed
} from "@vue/composition-api";
export function useMouse() {
const pos = reactive({ x: 0, y: 0 });
const update = (e: MouseEvent) => {
pos.x = e.pageX;
pos.y = e.pageY;
};
onMounted(() => {
window.addEventListener("mousemove", update);
});
onUnmounted(() => {
window.removeEventListener("mousemove", update);
});
return {
x: computed(() => pos.x),
y: computed(() => pos.y)
} as const;
}
Component Code
useMouseを利用したコンポーネントコードはこんな感じです。
コードが凄く読みやすいと思います。
<template>
<div>
<h2>mouse pointer</h2>
<div>x: {{ x }}</div>
<div>y: {{ y }}</div>
<button @click="setDummyPos">setDummyPos</button>
</div>
</template>
<script lang="ts">
import { createComponent } from "@vue/composition-api";
import { useMouse } from "@/hooks/useMouse";
export default createComponent({
setup() {
const { x, y } = useMouse();
return {
x,
y,
};
}
});
</script>
再利用性
debug時にこんなhooksがあると便利だと思います。
こんな感じにも使える
import {
onBeforeMount,
onMounted,
onBeforeUnmount,
onUnmounted,
onActivated,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onUpdated
} from "@vue/composition-api";
export function useLifeCycleLog() {
onBeforeMount(() => console.log("onBeforeMount"));
onMounted(() => console.log("onMounted"));
onBeforeUnmount(() => console.log("onBeforeUnmount"));
onUnmounted(() => console.log("onUnmounted"));
onActivated(() => console.log("onActivated"));
onBeforeUpdate(() => console.log("onBeforeUpdate"));
onDeactivated(() => console.log("onDeactivated"));
onErrorCaptured(() => console.log("onErrorCaptured"));
onUpdated(() => console.log("onUpdated"));
}
vs mixin
Vuetifyのコードはmixinでコードの共通化と再利用を上手くやっています。
しかし、これは宣言的ではない為、使い方が直感的ではなく、読み解くのに体力が必要です。
composition apiの利点は独立したネームスペースと宣言的なプログラミングができるため、使う側のコードも読みやすく、ロジックがバラけない3点かと思います。
// Styles
import './VAlert.sass'
// Extensions
import VSheet from '../VSheet'
// Components
import VBtn from '../VBtn'
import VIcon from '../VIcon'
// Mixins
import Toggleable from '../../mixins/toggleable'
import Themeable from '../../mixins/themeable'
import Transitionable from '../../mixins/transitionable'
// Utilities
import mixins from '../../util/mixins'
import { breaking } from '../../util/console'
// Types
import { VNodeData } from 'vue'
import { VNode } from 'vue/types'
/* @vue/component */
export default mixins(
VSheet,
Toggleable,
Transitionable
).extend({
name: 'v-alert',
props: {
border: {
type: String,
validator (val: string) {
return [
'top',
'right',
'bottom',
'left',
].includes(val)
},
},
closeLabel: {
type: String,
default: '$vuetify.close',
},
coloredBorder: Boolean,
dense: Boolean,
dismissible: Boolean,
icon: {
default: '',
type: [Boolean, String],
validator (val: boolean | string) {
return typeof val === 'string' || val === false
},
},
outlined: Boolean,
prominent: Boolean,
text: Boolean,
type: {
type: String,
validator (val: string) {
return [
'info',
'error',
'success',
'warning',
].includes(val)
},
},
value: {
type: Boolean,
default: true,
},
},
computed: {
__cachedBorder (): VNode | null {
if (!this.border) return null
let data: VNodeData = {
staticClass: 'v-alert__border',
class: {
[`v-alert__border--${this.border}`]: true,
},
}
if (this.coloredBorder) {
data = this.setBackgroundColor(this.computedColor, data)
data.class['v-alert__border--has-color'] = true
}
return this.$createElement('div', data)
},
__cachedDismissible (): VNode | null {
if (!this.dismissible) return null
const color = this.iconColor
return this.$createElement(VBtn, {
staticClass: 'v-alert__dismissible',
props: {
color,
icon: true,
small: true,
},
attrs: {
'aria-label': this.$vuetify.lang.t(this.closeLabel),
},
on: {
click: () => (this.isActive = false),
},
}, [
this.$createElement(VIcon, {
props: { color },
}, '$cancel'),
])
},
__cachedIcon (): VNode | null {
if (!this.computedIcon) return null
return this.$createElement(VIcon, {
staticClass: 'v-alert__icon',
props: { color: this.iconColor },
}, this.computedIcon)
},
classes (): object {
const classes: Record<string, boolean> = {
...VSheet.options.computed.classes.call(this),
'v-alert--border': Boolean(this.border),
'v-alert--dense': this.dense,
'v-alert--outlined': this.outlined,
'v-alert--prominent': this.prominent,
'v-alert--text': this.text,
}
if (this.border) {
classes[`v-alert--border-${this.border}`] = true
}
return classes
},
computedColor (): string {
return this.color || this.type
},
computedIcon (): string | boolean {
if (this.icon === false) return false
if (typeof this.icon === 'string' && this.icon) return this.icon
if (!['error', 'info', 'success', 'warning'].includes(this.type)) return false
return `$${this.type}`
},
hasColoredIcon (): boolean {
return (
this.hasText ||
(Boolean(this.border) && this.coloredBorder)
)
},
hasText (): boolean {
return this.text || this.outlined
},
iconColor (): string | undefined {
return this.hasColoredIcon ? this.computedColor : undefined
},
isDark (): boolean {
if (
this.type &&
!this.coloredBorder &&
!this.outlined
) return true
return Themeable.options.computed.isDark.call(this)
},
},
created () {
/* istanbul ignore next */
if (this.$attrs.hasOwnProperty('outline')) {
breaking('outline', 'outlined', this)
}
},
methods: {
genWrapper (): VNode {
const children = [
this.$slots.prepend || this.__cachedIcon,
this.genContent(),
this.__cachedBorder,
this.$slots.append,
this.$scopedSlots.close
? this.$scopedSlots.close({ toggle: this.toggle })
: this.__cachedDismissible,
]
const data: VNodeData = {
staticClass: 'v-alert__wrapper',
}
return this.$createElement('div', data, children)
},
genContent (): VNode {
return this.$createElement('div', {
staticClass: 'v-alert__content',
}, this.$slots.default)
},
genAlert (): VNode {
let data: VNodeData = {
staticClass: 'v-alert',
attrs: {
role: 'alert',
},
class: this.classes,
style: this.styles,
directives: [{
name: 'show',
value: this.isActive,
}],
}
if (!this.coloredBorder) {
const setColor = this.hasText ? this.setTextColor : this.setBackgroundColor
data = setColor(this.computedColor, data)
}
return this.$createElement('div', data, [this.genWrapper()])
},
/** @public */
toggle () {
this.isActive = !this.isActive
},
},
render (h): VNode {
const render = this.genAlert()
if (!this.transition) return render
return h('transition', {
props: {
name: this.transition,
origin: this.origin,
mode: this.mode,
},
}, [render])
},
})
vs class
classを使うとDecoratorを使う
その際の問題を1つサンプルに
Vuexの標準的なapiであるmapStateでmapしたものをclassの中等で参照すると型エラーがでる
Decoratorはあくまでも暗黙的に拡張してくれるだけで、標準的な型システムでは拡張されていることを認識できない
@Component({
computed: {
mapState(['hello', 'world'])
},
})
export default class HelloWorld extends Vue {
mounted() {
this.hello // ERROR!!!!!!
}
}
Vue3へのスムーズな移行の為の準備
composition apiが登場する。
typescript, jsx, tsxが標準でサポートされる。
どうしたらいい?
iCareでの備え
- mixinの可読性に課題
- composition apiを活用してmixin以上の再利用性を獲得
- 且つ宣言的なプログラミングを導入
- Typescript
- Typescriptの型検査でテスト工数削減
- その他恩恵を獲得
- jsx, tsx
- jsx, tsxにより、template内がuntypesafeな課題を解決
-
functional componentでmethodの定義もできない課題
- jsxだとロジックが書けるので、jsxで課題解決
ご清聴ありがとうございました
Vue3.0
By kahirokunn
Vue3.0
- 1,549