Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Global API Change #30

Closed
yyx990803 opened this issue Mar 22, 2019 · 11 comments
Closed

Global API Change #30

yyx990803 opened this issue Mar 22, 2019 · 11 comments

Comments

@yyx990803
Copy link
Member

yyx990803 commented Mar 22, 2019

  • 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()
@LinusBorg
Copy link
Member

Concerning Adoption strategies: The compatibility build won't be able to provide complete compatibility in this regard, since an "old" app would still try and use the global constructor to do mixins etc., right?

@LinusBorg
Copy link
Member

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

Will we do a separate RFC for provide/inject as a whole or are we planning to keep the provide API in components intact?

@Jinjiang
Copy link
Member

Jinjiang commented Apr 5, 2019

Will it break the auto-installation feature like vuex or vue-i18n?

<script src="/path/to/vue.js"></script>
<script src="/path/to/vuex.js"></script>

If I don't misunderstand we couldn't "install" or "use" a feature before an app created. Does it mean this kind of global installation would be banned?

Thanks.

@Jinjiang
Copy link
Member

Jinjiang commented Apr 5, 2019

And currently Vue.extend({}) seems able to solve the same problem as an app instance does. As I remember it works well in an earlier version of Weex. So I think the current design is also fine if we want to keep auto-installation supported.

@yyx990803
Copy link
Member Author

@Jinjiang yes, this is a breaking change and it will break auto installation. If we still allow implicit global installation it would make this change pointless.

@LinusBorg
Copy link
Member

Hm.

Would the Plugin API (install function receiving the/a Vue constructor) be the same though?

I don't want to break every plugin in the whole ecosystem :(

@yyx990803
Copy link
Member Author

@LinusBorg most plugins should be able to remain unchanged, with some exceptions:

  • implicit global installations won't work anymore

  • if a plugin relies on tree-shakable global APIs (e.g. nextTick), these won't be available on the passed app instance anymore and must be separately imported (e.g. import { nextTick } from 'vue').

@LinusBorg
Copy link
Member

LinusBorg commented Apr 8, 2019

Couldn't we try and find some middle ground for 3.0 and just deprecate it and then drop this in 4.0?

Maybe something like this:

  1. Vue.use() adds plugins it recieves to some cache
  2. A deprecation warning is logged during development.
  3. When running createApp(), these plugins are applied to the app instance in the background.

This would ensure backwards compatibility while giving people the possibility of using the new api in their apps if they want to keep the plugins local to their app instance.

@posva
Copy link
Member

posva commented Apr 8, 2019

but what is the deal with global plugin install? it only worked while prototyping

@LinusBorg
Copy link
Member

it only worked while prototyping

Not sure what you mean.

I'm sure there are a ton of production apps out there that use Vue in server-rendered pages and include it from a CDN, along with plugins:

<script src="http://unpkg.com/vue/dist/vue.min.js"></script>
<script src="http://unpkg.com/some-plugin.min.js"></script>

some-plugin would try and call Vue.use(SomePlugin), which won't work anymore in Vue 3.

Sure, people have to upgrade their dependency and some code anyways, but the point is that this requires the plugin author to upgrade their plugin's code to work with Vue 2 and 3 differenty, and add documentation about this etc.

In short: It's another thing plugin authors have to do to ensure Vue 3 compatiblity, and it's not done by just compiling a new bundle with the new template compiler and correcting a few little things in the code, now they need to update docs as well etc.

I think this is one of those things that we should deprecate and not just take away from people, as I generally have a bad feeling about potentially underestimating the amount of churn pain we will be inflicting on people with all of the things we are changing.

@yyx990803
Copy link
Member Author

Published: https://github.com/vuejs/rfcs/blob/global-api-change/active-rfcs/0000-global-api-change.md

@LinusBorg I think a middle ground (as included in the public RFC) is to provide stubs that emit warnings for these moved methods. For example if a plugin calls Vue.use(), it will be a noop and emit a warning that tells the user to explicit call app.use() for related plugins. This way plugin authors won't have to do anything regarding migration.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants