@jspoetry
1. Новые фичи во Vue 3
2. Паттерны переиспользования функциональности во Vue 2
4. Миграция на Composition API (Vue 3)
5. Миграция на @vue/composition-api (Vue 2)
3. Можно ли мигрировать на Vue 3
<template>
<button @click="isVisible = true">
Open modal
</button>
<teleport to="body">
<Modal v-if="isVisible" @close="isVisible = false">
</teleport>
</template>
Fragments
<template>
<div>
<label>{{ label }}</label>
<input v-model="value" type="text">
</div>
</template>
<template>
<label>{{ label }}</label>
<input v-model="value" type="text">
</template>
State-driven CSS Variables
<template>
<button class="button" :style="buttonStyles">
<slot></slot>
</button>
</template>
<script>
export default {
props: ['disabled'],
computed: {
buttonStyles() {
return {
'--button-background': this.backgroundColor
}
},
backgroundColor() {
return this.disabled ? 'gray' : 'violet'
},
}
}
</script>
<style>
.button {
background-color: var(--button-background);
}
</style>
<template>
<button class="button">
<slot></slot>
</button>
</template>
<script>
export default {
props: ['disabled'],
data() {
return {
colorMap: {
error: 'red',
warning: 'yellow'
}
}
},
computed: {
backgroundColor() {
return this.disabled ? 'gray' : 'violet'
},
}
}
</script>
<style>
.button {
background-color: v-bind(backgroundColor);
color: v-bind('colorMap.error')
}
</style>
Поддержка Typescript
<script lang="ts">
import {
Component,
Vue,
Prop,
Watch
} from 'vue-property-decorator';
@Component()
export default class MyComponent extends Vue {
@Prop() name!: string;
description = 'Some description';
@Watch('name')
onChangeName(name: string) {
this.description = `Some description about ${name}`
}
onClick(event: MouseEvent): void {
this.$emit('update-description', this.description)
}
}
</script>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
name: {
type: String,
default: ''
},
},
data() {
return {
description: 'Some description',
}
},
watch: {
name: function(name: string) {
this.description = `Some description about ${name}`
}
},
methods: {
onClick(event: MouseEvent): void {
this.$emit('update-description', this.description)
}
}
})
</script>
<template>
<div>{{ foo }}</div>
</template>
<script>
import FooMixin from '@/mixins/Foo'
export default {
name: 'SomeComponent',
mixins: [FooMixin]
}
</script>
export default {
data() {
return {
foo: 'bar'
}
}
}
<template>
<div>{{ foo }}</div>
</template>
<script>
import UsefulMixin from '@/mixins/Useful'
export default {
name: 'SomeComponent',
mixins: [UsefulMixin],
methods: {
update() {
// ...
}
}
}
</script>
export default {
methods: {
update() {
//...
}
}
}
<template>
<div>
<h1>{{ title }}</h1>
<div v-for="item of filteredItems" @click="onClick">
{{ item }}
</div>
</div>
</template>
<script>
import HeyMixin from '@/mixins/Hey'
import HelloMixin from '@/mixins/Hello'
import HiMixin from '@/mixins/Hi'
import GoodDayMixin from '@/mixins/GoodDay'
import GoodNightMixin from '@/mixins/GoodNight'
export default {
name: 'FancyList',
mixins: [HeyMixin, HelloMixin, HiMixin, GoodDayMixin, GoodNightMixin],
mounted() {
this.sayGreetings()
}
}
</script>
?
??
???
????
export default function withUsers(Inner: Component) {
return {
props: {
...Inner.props, // spread prop declarations
},
data() {
return {
users: [],
};
},
async mounted() {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
const users = await response.json();
this.users = users;
},
render(h: CreateElement) {
return h(Inner, {
props: {
...this.$props, // pass all props
items: this.users,
},
on: {
...this.$listeners, // pass all listeners
},
});
},
};
}
<template>
<ul>
<li v-for="item of items" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
<script>
export default {
name: 'FancyList',
props: {
items: {
type: Array,
default: () => []
}
}
}
</script>
<template>
<h1>
Users
</h1>
<UserList />
</template>
<script>
import FancyList from '@/components/FancyList.vue'
import withUsers from '@/components/withUsers'
const FancyListWithUsers = withUsers(FancyList)
export default {
name: 'UsersPage',
components: {
UserList: FancyListWithUsers
}
}
</script>
const FullStackComponent = withFeatureA(withFeatureB(withFeatureC(withFeatureD(MyComponent))))
<template>
<EditorMenuBar v-slot="{ commands }">
<button @click="commands.bold">
Bold
</button>
<button @click="commands.italic">
Italic
</button>
<button @click="commands.underline">
Underline
</button>
</EditorMenuBar>
</template>
<template>
<slot v-bind="{ bold, italic, stroke }" />
</template>
<template>
<FeatureA v-slot="{ a }">
<FeatureB v-slot="{ b: foo }">
<FeatuerC v-slot="{ c }">
<FeatureD v-slot="{ d }">
<MyComponent
:a="a"
:b="foo"
:c="c"
:d="d"
/>
</FeatureD>
</FeatuerC>
</FeatureB>
</FeatureA>
</template>
data
computed
watch
methods
life-cycle hooks
setup
<script>
import { defineComponent } from 'vue'
export default defineComponent({
setup() {
const { featureA } = useFeatureA()
const { featureB } = useFeatureB()
const { featureC } = useFeatureC()
const { featureD } = useFeatureD()
return {
featureA,
featureB,
featureC,
featureD
}
}
})
function useFeatureA() {}
function useFeatureB() {}
function useFeatureC() {}
function useFeatureD() {}
</script>
1. Зависимости, которые не мигрировали на Vue 3
2. В поддержке IE11
@vue/composition-api
1. createApp
3. emits
2. defineComponent
4. setup
5. reactive
6. ref
8. unwrap ref'ов
7. ref или reactive
9. toRefs
10. template ref
11. readonly
12. shallowReadonly
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
createApp(App).use(store).use(router).mount("#app");
createApp вместо new Vue()
У каждого приложения свой контекст
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import App2 from "./App2.vue";
import router2 from "./router2";
import store2 from "./store2";
createApp(App).use(store).use(router).mount("#app");
createApp(App2).use(store2).use(router2).mount('#app2')
export default defineComponent({
name: 'MyComponent',
components: {},
props: {},
data() { return {} },
methods: {}
})
export default defineComponent({
emits: ['update', 'change'],
setup(props, { emit }) {
emit('wrong-event') // warn
}
})
export default defineComponent({
emits: {
// validate event payload
update: (data) => Boolean(data.username),
// don't validate
change: null
},
})
beforeCreated
setup
created
<template>
<button :type="type" :disabled="isDisabled" @click="onClick">
{{ foo }}
</button>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
props: ['foo', 'type'],
setup(props, context) {
const foobar = props.foo + 'bar'
const { attrs, slots, emit } = context
const isDisabled = false
const onClick = () => {
emit('click')
}
return {
foo: foobar,
isDisabled,
onClick
}
}
})
</script>
import { defineComponent, reactive, isProxy } from 'vue'
export default defineComponent({
setup() {
const state = reactive({
foo: 1,
bar: 'hey'
})
console.log(isProxy(state)) // true
}
})
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const count = ref(0)
console.log(count.value) // 0
const increaseCount = () => {
count.value += 1
}
const items = ref({}) // reactive({})
}
})
import { defineComponent, reactive } from 'vue'
export default defineComponent({
setup() {
const objectOne = { name: 'David' }
const objectTwo = reactive({
name: 'Jhon'
})
objectOne.name // reactive or plain?
objectTwo.name // reactive or plain?
}
})
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const objectOne = { name: 'David' }
const objectTwo = ref({
name: 'Jhon'
})
objectOne.name // plain
objectTwo.value.name // reactive
}
})
Явность
import { defineComponent, reactive, ref } from 'vue'
export default defineComponent({
setup() {
const state = reactive({ category: 'T-Shirt' })
const productCount = ref(50)
state // .value?
productCount // .value?
}
})
import { defineComponent, ref, computed } from 'vue'
export default defineComponent({
setup() {
const productsState = ref({
category: 'T-Shirt',
products: []
})
const isEmpty = computed(() =>
!Boolean(ref.value.products.length)
)
productState // .value
isEmpty // .value
}
})
Консистентность
<template>
<div>{{ objectWithNestedRefs.value.nested.value.deepNested.value.myDeepProp.value }}</div>
</template>
<script>
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const objectWithNestedRefs = ref({
nested: ref({
deepNested: ref({
myDeepProp: ref('deep inside')
})
})
})
return {
objectWithNestedRefs
}
}
})
</script>
<template>
<div>{{ objectWithNestedRefs.nested.deepNested.myDeepProp }}</div>
</template>
<script>
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const objectWithNestedRefs = ref({
nested: ref({
deepNested: ref({
myDeepProp: ref('deep inside')
})
})
})
return {
objectWithNestedRefs
}
}
})
</script>
<template>
<div>{{ objectWithNestedRefs.nested.deepNested.myDeepProp }}</div>
</template>
<script>
import { defineComponent, ref, reactive } from 'vue'
export default defineComponent({
setup() {
const objectWithNestedRefs = reactive({
nested: ref({
deepNested: ref({
myDeepProp: ref('deep inside')
})
})
})
objectWithNestedRefs.nested.deepNested.myDeepProp // deep inside
return {
objectWithNestedRefs
}
}
})
</script>
<template>
<div>{{ arrayWithRefs[0].value }}</div>
<div>{{ arrayWithObjectRefs[0] }}</div>
</template>
<script>
import { defineComponent, ref, reactive } from 'vue'
export default defineComponent({
setup() {
const arrayWithRefs = reactive([ref(0)])
arrayWithRefs[0].value // 0
const arrayWithObjectRefs = reactive([{id: ref(0)}])
arrayWithObjectRefs[0] // 0
return {
arrayWithRefs,
arrayWithObjectRefs
}
}
})
</script>
import { defineComponent, reactive, computed } from 'vue'
export default defineComponent({
props: ['foo'],
setup(props) {
const state = reactive({ name: 'Peter' })
const { name } = state // not reactive
const { foo } = props // not reactive
const hasFoo = computed(() => Boolean(foo)) // wouldn't update
setTimeout(() => {
name = 'John' // wouldn't update
}, 2000)
return {
name,
hasFoo
}
}
})
import { defineComponent, reactive, computed, toRefs } from 'vue'
export default defineComponent({
props: ['foo'],
setup(props) {
const state = reactive({ name: 'Peter' })
const { name } = toRefs(state)
const { foo } = toRefs(props)
const hasFoo = computed(() => Boolean(foo.value))
setTimeout(() => {
name.value = 'John'
}, 2000)
return {
name,
hasFoo
}
}
})
<template>
<div ref="container">
<!-- elements -->
</div>
</template>
<script>
import { defineComponent, ref, onMounted } from 'vue'
export default defineComponent({
setup() {
const container = ref(null)
onMounted(() => {
console.log(container.value) // HTMLDivElement
})
return {
container
}
}
})
</script>
import { defineComponent, readonly, isProxy } from 'vue'
export default defineComponent({
setup() {
const state = {
status: 'started',
user: {
name: 'Jhon',
lastname: 'Doe'
}
}
const immutableObject = readonly(state)
console.log(isProxy(immutableObject)) // true
immutableObject.status = 'failed' // error
immutableObject.user.name = 'Peter' // error
}
})
import { defineComponent, readonly, isProxy, reactive } from 'vue'
import useOnline from '@/composable/useOnline'
export default defineComponent({
setup() {
const state = reactive({
status: 'started',
user: {
name: 'Jhon',
lastname: 'Doe'
}
})
const immutableObject = readonly(state)
console.log(isProxy(immutableObject)) // true
immutableObject.status = 'failed' // error
immutableObject.user.name = 'Peter' // error
}
})
import { defineComponent } from 'vue'
import useOnline from '@/composable/useOnline'
export default defineComponent({
setup() {
const isOnline = useOnline()
isOnline.value = false // error
}
})
import {
defineComponent,
readonly,
onMounted,
onBeforeUnmount
} from 'vue'
export default function useOnline() {
const isOnline = ref(true)
const onOffline = () => isOnline.value = false
const onOnline = () => isOnline.value = true
onMounted(() => {
window.addEventListener('offline', onOffline)
window.addEventListener('online', onOnline)
})
onBeforeUnmount(() => {
window.removeEventListener('offline', onOffline)
window.removeEventListener('online', onOnline)
})
return readonly(isOnline)
}
import { defineComponent, shallowReadonly, isProxy } from 'vue'
import useOnline from '@/composable/useOnline'
export default defineComponent({
setup() {
const state = {
status: 'started',
user: {
name: 'Jhon',
lastname: 'Doe'
}
}
const immutableObject = shallowReadonly(state)
console.log(isProxy(immutableObject)) // true
immutableObject.status = 'failed' // error
immutableObject.user.name = 'Peter' // ok
}
})
import router from '@/router'
export default function useRouter() {
return router
}
getCurrentInstance
type getCurrentInstance = () => ComponentInternalInstance | null;
import {
defineComponent,
getCurrentInstance,
onMounted
} from 'vue'
export default defineComponent({
setup() {
console.log(getCurrentInstance()) // ComponentInternalInstance
onMounted(() => {
console.log(getCurrentInstance()) // ComponentInternalInstance
})
const onClick = () => {
console.log(getCurrentInstance()) // null
}
}
})
type getCurrentInstance = () => ComponentInternalInstance | null;
import {
defineComponent,
getCurrentInstance,
onMounted
} from 'vue'
export default defineComponent({
setup() {
const instance = getCurrentInstance() // ComponentInternalInstance
onMounted(() => {
console.log(getCurrentInstance()) // ComponentInternalInstance
})
const onClick = () => {
console.log(instance) // ComponentInternalInstance
}
}
})
type getCurrentInstance = () => ComponentInternalInstance | null;
interface ComponentInternalInstance {
uid: number;
type: ConcreteComponent;
parent: ComponentInternalInstance | null;
root: ComponentInternalInstance;
appContext: AppContext;
vnode: VNode;
subTree: VNode;
update: ReactiveEffect;
proxy: ComponentPublicInstance | null;
exposed: Record<string, any> | null;
data: Data;
props: Data;
attrs: Data;
slots: InternalSlots;
refs: Data;
emit: EmitFn;
isMounted: boolean;
isUnmounted: boolean;
isDeactivated: boolean;
}
import { getCurrentInstance } from 'vue'
export default function useRouter() {
return getCurrentInstance().appContext.config.globalProperties.$router
}
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
createApp(App).use(store).use(router).mount("#app");
createApp вместо new Vue()
У каждого приложения свой контекст
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import App2 from "./App2.vue";
import router2 from "./router2";
import store2 from "./store2";
createApp(App).use(store).use(router).mount("#app");
createApp(App2).use(store2).use(router2).mount('#app2')
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
createApp(App).use(store).use(router).mount("#app");
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import App2 from "./App2.vue";
import router2 from "./router2";
import store2 from "./store2";
createApp(App).use(store).use(router).mount("#app");
createApp(App2).use(store2).use(router2).mount('#app2')
createApp вместо new Vue()
Во Vue 2 только глобальный контекст
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
createApp(App).use(store).use(router).mount("#app");
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import App2 from "./App2.vue";
import router2 from "./router2";
import store2 from "./store2";
createApp(App).use(store).use(router).mount("#app");
createApp(App2).use(store2).use(router2).mount('#app2')
createApp === new Vue()
Во Vue 2 только глобальный контекст
createApp === new Vue()
import Vue from 'vue'
import CompositionAPI, { createApp } from "@vue/composition-api";
import Vuex from 'vuex';
import VueRouter from 'vue-router'
import App from "./App.vue";
import router from "./router";
import store from "./store";
Vue.use(CompositionAPI)
const app = createApp({
store,
router,
render: (h) => h(App),
})
app.use(VueRouter).use(Vuex)
app.mount("#app");
import { defineComponent } from 'vue'
export default defineComponent({
setup(props, context) {
const { attrs, slots, emit } = context
}
})
import { defineComponent } from 'vue'
export default defineComponent({
setup(props, context) {
const { parent, root, listeners, refs, ...vue3Context } = context
}
})
import { defineComponent, reactive, set, del } from '@vue/composition-api'
export default defineComponent({
setup() {
const reactiveObj = reactive({
name: 'ObiWan',
type: 'jedi'
})
set(reactiveObj, 'lasersaber', 'blue') // this.$set
del(reactiveObj, 'type') // this.$delete
}
})
import { defineComponent, reactive } from '@vue/composition-api'
export default defineComponent({
setup() {
const state = {
name: 'Dooku',
}
const reactiveState = reactive(state) // Object.defineProperty
console.log(state === reactiveState) // true
// isProxy(reactiveState) -> false
}
})
import { defineComponent, reactive, ref } from '@vue/composition-api'
export default defineComponent({
setup() {
const reactiveObject = reactive({
clones: [{ weapon: ref('blaster') }]
})
reactiveObject.clones[0].weapon.value // wound't unwrap
}
})
import { defineComponent, reactive, ref } from '@vue/composition-api'
export default defineComponent({
setup() {
const reactiveObject = reactive({
clones: [reactive({ weapon: ref('blaster') })]
})
reactiveObject.clones[0].weapon
}
})
import { defineComponent, toRefs, isRef } from '@vue/composition-api'
export default defineComponent({
props: {
jedi: {
type: Object,
default: () => ({ laserSaber: 'blue' })
}
},
setup(props) {
const { laserSaber } = toRefs(props.jedi) // warn, not reactive
}
})
<template>
<Spaceship :jedi="{ laserSaber: 'green' }" />
</template>
<template>
<Spaceship :jedi="staticJedi" />
</template>
<script>
import { defineComponent } from '@vue/compositio-api'
defineComponent({
setup() {
const staticJedi = { laserSaber: 'green' }
return {
staticJedi
}
}
})
</script>
<template>
<Spaceship :jedi="jedi" />
</template>
<script>
export default {
data() {
return {
jedi: { laserSaber: 'green' }
}
}
}
</script>
<template>
<Spaceship :jedi="jedi" />
</template>
<script>
import {
defineComponent,
reactive
} from '@vue/compositio-api'
export default defineComponent({
setup() {
const jedi = reactive({ laserSaber: 'green' })
return {
jedi
}
}
})
</script>
<template>
<div
v-for="(item, i) in list"
:ref="el => { if (el) divs[i] = el }"
>
{{ item }}
</div>
</template>
<script>
export default {
setup() {
const list = reactive([1, 2, 3])
const divs = ref([])
// make sure to reset the refs before each update
onBeforeUpdate(() => {
divs.value = []
})
return {
list,
divs
}
}
}
</script>
import { readonly } from '@vue/composition-api'
export default {
setup() {
const plainObject = {
status: 'waiting'
}
// Readonly<Record<string, string>>
const readonlyObject = readonly(plainObject)
readonlyObject.status = 'failed' // allowed
}
}
import { defineComponent, shallowReadonly, set } from '@vue/composition-api'
export default defineComponent({
setup() {
const sourceObject = reactive({
side: 'dark',
skill: 'lightning strike',
visitedPlanets: {
Tatooine: true,
Naboo: false
}
})
// { get dark() {}, get skill() {}, get visitedPlanets() {} }
const shallowReadonlyObject = shallowReadonly(sourceObject)
shallowReadonlyObject.side = 'light' // warn
shallowReadonlyObject.visitedPlanets.Naboo = true
set(sourceObject, 'level', 5) // wouldn't update shallowReadonlyObject
shallowReadonlyObject.level // undefined
shallowReadonlyObject.illegalProp = 'absolutely illegal' // ok
}
})
import { getCurrentInstance } from '@vue/composition-api'
export default function useRouter() {
return getCurrentInstance().appContext.config.globalProperties.$router
}
interface ComponentInternalInstance {
uid: number;
type: ConcreteComponent;
parent: ComponentInternalInstance | null;
root: ComponentInternalInstance;
appContext: AppContext;
vnode: VNode;
subTree: VNode;
update: ReactiveEffect;
proxy: ComponentPublicInstance | null;
exposed: Record<string, any> | null;
data: Data;
props: Data;
attrs: Data;
slots: InternalSlots;
refs: Data;
emit: EmitFn;
isMounted: boolean;
isUnmounted: boolean;
isDeactivated: boolean;
}
import { getCurrentInstance } from '@vue/composition-api'
export default function useRouter() {
return getCurrentInstance().proxy.$router
}
import { getCurrentInstance, computed } from '@vue/composition-api'
export default function useRoute() {
return computed(() => getCurrentInstance().proxy.$route)
}
import { defineComponent } from '@vue/composition-api'
import useRouter from '@/composable/useRouter'
export default defineComponent({
setup() {
const router = useRouter()
router.push('/')
}
})
import { defineComponent } from '@vue/composition-api'
import { useRouter } from 'vue-router'
export default defineComponent({
setup() {
const router = useRouter()
router.push('/')
}
})
@jspoetry