Skip to content

Global API Change #30

@yyx990803

Description

@yyx990803
  • Start Date: (fill me in with today's date, YYYY-MM-DD)
  • Target Major Version: (2.x / 3.x)
  • Reference Issues: (fill in existing related issues, if any)
  • Implementation PR: (leave this empty)

Summary

Re-design app bootstrapping and global configuration API.

Basic example

Before

import Vue from 'vue'
import App from './App.vue'

Vue.config.ignoredElements = [/^app-/]
Vue.use(/* ... */)
Vue.mixin(/* ... */)
Vue.component(/* ... */)
Vue.directive(/* ... */)

new Vue({
  render: h => h(App)
}).$mount('#app')

After

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp()

app.config.ignoredElements = [/^app-/]
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)

app.mount(App, '#app')

Motivation

Vue's current global API and configurations permanently mutate global state. This leads to a few problems:

  • Global configuration makes it easy to accidentally pollute other test cases during testing. Users need to carefully store original global configuration and restore it after each test (e.g. resetting Vue.config.errorHandler). Some APIs (e.g. Vue.use, Vue.mixin) don't even have a way to revert their effects. This makes tests involving plugins particularly tricky.

    • vue-test-utils has to implement a special API createLocalVue to deal with this
  • This also makes it difficult to share the same copy of Vue between multiple "apps" on the same page, but with different global configurations:

    // this affects both root instances
    Vue.mixin({ /* ... */ })
    
    const app1 = new Vue({ el: '#app-1' })
    const app2 = new Vue({ el: '#app-2' })

Detailed design

Technically, Vue 2 doesn't have the concept of an "app". What we define as an app is simply a root Vue instance created via new Vue(). Every root instance created from the same Vue constructor shares the same global configuration.

In this proposal we introduce a new global API, createApp:

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

Calling createApp with a root component returns an app instance. An app instance provides an app context. The entire component tree formed by the root instance and its descendent components share the same app context, which provides the configurations that were previously "global" in Vue 2.x.

Global API Mapping

An app instance exposes a subset of the current global APIs. The rule of thumb is any APIs that globally mutate Vue's behavior are now moved to the app instance. These include:

  • Global configuration
    • Vue.config -> app.config
      • with the exception of Vue.config.productionTip
  • Asset registration APIs
    • Vue.component -> app.component
    • Vue.directive -> app.directive
    • Vue.filter -> app.filter
  • Behavior Extension APIs
    • Vue.mixin -> app.mixin
    • Vue.use -> app.use

Global APIs that are idempotent (i.e. do not globally mutate behavior) are now named exports as proposed in Global API Treeshaking.

Mounting App Instance

The app instance can be mounted with the mount method. It works the same as the existing vm.$mount() component instance method and returns the mounted root component instance:

const rootInstance = app.mount('#app')

rootInstance instanceof Vue // true

Provide / Inject

An app instance can also provide dependencies that can be injected by any component inside the app:

// in the entry
app.provide({
  [ThemeSymbol]: theme
})

// in a child component
export default {
  inject: {
    theme: {
      from: ThemeSymbol
    }
  },
  template: `<div :style="{ color: theme.textColor }" />`
}

This is similar to using the provide option in a 2.x root instance.

Drawbacks

  • Global APIs are now split between app instance methods and global named imports, instead of a single namespace. However the split makes sense because:

    • App instance methods are configuration APIs that globally mutate an app's behavior. They are also almost always used together only in the entry file of a project.

    • Global named imports are idempotent helper methods that are typically imported and used across the entire codebase.

Alternatives

N/A

Adoption strategy

  • The transformation is straightforward (as seen in the basic example).
  • A codemod can also be provided.

Unresolved questions

  • Vue.config.productionTip is left out because it is indeed "global". Maybe it should be moved to a global method?

    import { suppressProductionTip } from 'vue'
    
    suppressProductionTip()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions