На полпути к Vue 3

Николай Пугачев

Frontend developer at

@jspoetry

1. Новые фичи во Vue 3

Структура доклада

2. Паттерны переиспользования функциональности во Vue 2

4. Миграция на Composition API (Vue 3)

5. Миграция на @vue/composition-api (Vue 2)

3. Можно ли мигрировать на Vue 3

Vue 3

Vue 3

  • Teleport

Teleport

<template>
  <button @click="isVisible = true">
    Open modal
  </button>

  <teleport to="body">
    <Modal v-if="isVisible" @close="isVisible = false">
  </teleport>
</template>

Vue 3

  • Teleport
  • Fragments

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>

Vue 3

  • Teleport
  • Fragments
  • State-driven CSS Variables

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>

Vue 3

  • Teleport
  • Fragments
  • State-driven CSS Variables
  • Поддержка Typescript

Поддержка 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>

Vue 3

  • Teleport
  • Fragments
  • State-driven CSS Variables
  • Поддержка Typescript
  • Composition API

Недостатки паттернов переиспользования функциональности во Vue 2

Недостатки паттернов переиспользования функциональности во Vue 2

Mixin

HOC

Renderless component

Mixin

<template>
  <div>{{ foo }}</div>
</template>

<script>
  import FooMixin from '@/mixins/Foo'
  export default {
    name: 'SomeComponent',
    mixins: [FooMixin]
  }
</script>
export default {
  data() {
    return {
      foo: 'bar'
    }
  }
}

Mixin

<template>
  <div>{{ foo }}</div>
</template>

<script>
  import UsefulMixin from '@/mixins/Useful'
  export default {
    name: 'SomeComponent',
    mixins: [UsefulMixin],
    methods: {
      update() {
        // ...
      }
    }
  }
</script>
export default {
  methods: {
    update() { 
      //... 
    }
  }
}

Mixin

<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>

?

??

???

????

Недостатки паттернов переиспользования функциональности во Vue 2

Mixin

HOC

Renderless component

  • Конфликты неймспейса
  • Неочевидно, что внедряется в компонент

High Order Component (HOC)

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>

High Order Component (HOC)

<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>

High Order Component (HOC)

const FullStackComponent = withFeatureA(withFeatureB(withFeatureC(withFeatureD(MyComponent))))

Недостатки паттернов переиспользования функциональности во Vue 2

Mixin

HOC

  • Неочевидно, что попадает в props
  • Инициализация инстанса

Renderless component

  • Конфликты неймспейса
  • Неочевидно, что внедряется в компонент

Renderless Component

<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>

tiptap — WYSIWYG-редактор

<template>
  <slot v-bind="{ bold, italic, stroke }" />
</template>

Renderless Component

<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>

Недостатки паттернов переиспользования функциональности во Vue 2

Mixin

HOC

  • Неочевидно, что попадает в  props
  • Инициализация инстанса

Renderless component

  • Увеличивается темплейт
  • Инициализация инстанса
  • Конфликты неймспейса
  • Неочевидно, что внедряется в компонент

Главный недостаток паттернов — масштабирование

Composition API

Options API vs Composition API

data

computed

watch

methods

life-cycle hooks

setup

Composition API

<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>

Уже можно мигрировать на Vue 3?

Уже можно мигрировать на Vue 3?

Нельзя, если есть:

1. Зависимости, которые не мигрировали на Vue 3

2. В поддержке IE11

@vue/composition-api

Зачем использовать @vue/compisition-api?

Миграция проекта на Composition API

Миграция проекта на 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

createApp

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')

defineComponent

export default defineComponent({
  name: 'MyComponent',
  components: {},
  props: {},
  data() { return {} },
  methods: {}
})
  • Возвращает объект как есть
  • Предоставляет типы

emits

export default defineComponent({
  emits: ['update', 'change'],
  setup(props, { emit }) {
    emit('wrong-event') // warn
  }
})
  • Ошибка в Typescript и warn в консоли при эмите события, которое не было указано
  • В объектном синтаксисе можно указать валидатор
export default defineComponent({
  emits: {
    // validate event payload
    update: (data) => Boolean(data.username),
    // don't validate
    change: null
  },
})

setup

beforeCreated
setup
created

setup

<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>

Система реактивности

  • Proxy + WeakMap
  • Опциональная
  • Решены проблемы реактивности Vue 2

reactive

import { defineComponent, reactive, isProxy } from 'vue'

export default defineComponent({
  setup() {
    const state = reactive({
      foo: 1,
      bar: 'hey'
    })
    
    console.log(isProxy(state)) // true
  }
})

ref

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({})
  }
})

reactive vs ref для объектов

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
  }
})

Явность

reactive vs ref для объектов

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
  }
})

Консистентность

unwrap

<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>

unwrap

<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>

unwrap

<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>

unwrap

<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>

toRefs

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
    }
  }
})

toRefs

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 ref

<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>

readonly

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
  } 
})

readonly

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
  } 
})

readonly

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)
}

shallowReadonly

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
  } 
})

Глобальные свойства в Composition API

import router from '@/router'

export default function useRouter() {
  return router
}

getCurrentInstance

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
    }
  }
})

getCurrentInstance

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
    }
  }
})

getCurrentInstance

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;
}

Глобальные свойства в Composition API

import { getCurrentInstance } from 'vue'

export default function useRouter() {
  return getCurrentInstance().appContext.config.globalProperties.$router
}

Миграция проекта на @vue/composition-api

createApp

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')

createApp

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

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

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");

emits

setup


import { defineComponent } from 'vue'

export default defineComponent({
  setup(props, context) {
    const { attrs, slots, emit } = context
  }
})

setup


import { defineComponent } from 'vue'

export default defineComponent({
  setup(props, context) {
    const { parent, root, listeners, refs, ...vue3Context } = context
  }
})

Система реактивности

  • Object.defineProperty + WeakMap
  • Те же проблемы реактивности Vue 2
  • Опциональная

set и del

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
  }
})

reactive

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
  }
})

unwrap

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
  }
})

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
  }
})

toRefs

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
  }
})

toRefs

<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>

toRefs

<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 ref

<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>

readonly

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
  }
}

shallowReadonly

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
  }
})

Глобальные свойства в

@vue/composition-api

import { getCurrentInstance } from '@vue/composition-api'

export default function useRouter() {
  return getCurrentInstance().appContext.config.globalProperties.$router
}

getCurrentInstance

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;
}

Глобальные свойства в

@vue/composition-api

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)
}

Глобальные свойства в

@vue/composition-api

import { defineComponent } from '@vue/composition-api'
import useRouter from '@/composable/useRouter'

export default defineComponent({
  setup() {
    const router = useRouter()
    router.push('/')
  }
})

Глобальные свойства в

@vue/composition-api

import { defineComponent } from '@vue/composition-api'
import { useRouter } from 'vue-router'

export default defineComponent({
  setup() {
    const router = useRouter()
    router.push('/')
  }
})

Ссылки

Николай Пугачев

Frontend developer at

Спасибо за внимание

@jspoetry

На полпути к Vue 3

By Nikolai Pugachev

На полпути к Vue 3

  • 261