全局API的改动

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-05-31

非兼容的全局API的改动

在 Vue2 中没有“app”的概念,我们定义的应用只是通过 new Vue() 创建的根 Vue 实例。从同一个 Vue 构造函数创建的每个根实例共享相同的全局配置,因此:

  • 在测试期间,全局配置很容易意外地污染其他测试用例。用户需要仔细存储原始全局配置,并在每次测试后恢复 (例如重置 Vue.config.errorHandler)。有些 API 像 Vue.use 以及 Vue.mixin 甚至连恢复效果的方法都没有,这使得涉及插件的测试特别棘手。实际上,vue-test-utils 必须实现一个特殊的 API createLocalVue 来处理此问题。

  • 全局配置使得在同一页面上的多个“app”之间共享同一个 Vue 副本非常困难,但全局配置不同。

// 这会影响两个根实例
Vue.mixin({
  /* ... */
})

const app1 = new Vue({ el: '#app-1' })
const app2 = new Vue({ el: '#app-2' })

createApp

Vue3 引入了一个新的 API createApp 来解决上面的问题。

import { createApp } from 'vue'

const app = createApp(options); // 返回一个应用实例

并使用实例API来代替全局API。

  • Vue.config => app.config

  • Vue.config.productionTip => 被移除

  • Vue.config.ignoredElements => app.config.isCustomElement

  • Vue.component => app.config

  • Vue.directive => app.directive

  • Vue.mixin => app.mixin

  • Vue.use => app.use

  • Vue.prototype => app.config.globalPropertie

所有其他未改变的全局API在Vue3中被命名为 exports,见全局API Treeshaking

app.config.isCustomElement

Vue2 中 Vue.config.ignoredElements 的作用是,使 Vue 忽略在 Vue 之外的自定义元素 (e.g. 使用了 Web Components APIs)。否则,它会假设你忘记注册全局组件或者拼错了组件名称,从而抛出一个关于 Unknown custom element 的警告。

// before
Vue.config.ignoredElements = ['my-el', /^ion-/]

// after
const app = Vue.createApp({})
app.config.isCustomElement = tag => tag.startsWith('ion-')

// 重命名可以更好地传达它的功能,新选项还需要一个比旧的 string/RegExp 方法提供更多灵活性的函数

app.config.globalProperties

在 Vue2 中,通常在 Vue.prototype 上增加属性以便所有的组件都可以访问到,在 Vue3中被替换为 app.config.globalProperties,这些属性将被复制在应用程序中实例化一个组件的一部分。

// before
Vue.prototype.$http = () => {}

// after 
const app = Vue.createApp({})
app.config.globalProperties.$http = () => {}

在编写插件时,推荐使用 Provide/Inject 来替代 globalProperties

app.use

插件开发者通常使用 Vue.use,但是在 Vue3中此方法将停止工作,所以开发者必须在应用程序实例上显式指定使用此插件:

import VueRouter from 'vue-router';

const app = createApp(MyApp)
// 不再是 Vue.use(VueRouter)
app.use(VueRouter)

app.component / app.directive / app.mount

const app = createApp(MyApp)

app.component('button-counter', {
  data: () => ({
    count: 0
  }),
  template: '<button @click="count++">Clicked {{ count }} times.</button>'
})

app.directive('focus', {
  mounted: el => el.focus()
})

// 现在所有应用实例都挂载了,与其组件树一起,将具有相同的 “button-counter” 组件 和 “focus” 指令不污染全局环境
app.mount('#app')

app.provide

与在 2.x 根实例中使用 provide 选项类似,Vue 3 应用实例还可以提供可由应用内的任何组件注入的依赖项。

// 在入口
app.provide('guide', 'Vue 3 Guide')

// 在子组件
export default {
  inject: {
    book: {
      from: 'guide',
      default: '',
    }
  },
  template: `<div>{{ book }}</div>`, // book => Vue 3 Guide
}

如果是单文件组件中使用:

<template>
  {{ book }}
</template>

<script lang="ts">
import { defineComponent, inject } from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    const book = inject('guide', 'default value'); // name, defaultValue

    return {
      book
    }
  }
})
</script>

父组件提供 provide 的写法:

<template>
  <MyMarker />
</template>

<script>
import { provide } from 'vue'
import MyMarker from './MyMarker.vue

export default {
  components: {
    MyMarker
  },
  setup() {
    provide('location', 'North Pole'); // name, value 
    provide('geolocation', {
      longitude: 90,
      latitude: 135
    })
  }
}
</script>

需要注意的是在使用 provide 提供一些全局公用插件实例时,使用 inject 选项 去接受的时候会导致 Typescript 无法识别。

// main.ts
const app = createApp(App)
  .provide("store", store)
  .use(router); // 支持链式调用

// component
export default defineComponent({
  name: "Home",
  components: {
    // ...
  },
  inject: ["store"], // 使用选项接收
  computed: {
    email() {
      return this.store.state.user.email;
      // Warning: Property 'store' does not exist on type 'ComponentPublicInstance ...'
    },
  },
});

这个时候可以这样来避免上述问题:

// main.ts
const app = createApp(App).use(router);

app.config.globalProperties.store= store;

// shims.d.ts
declare module '@vue/runtime-core' {
  interface ComponentCustomProperties  {
    store: any // replace it with the right type
  }
}

// component 与上面一样无需改动 就可以直接使用 this.store

全局API Treeshaking

在 Vue 3 中,全局和内部 API 都经过了重构,并考虑到了 tree-shaking 的支持。因此,全局 API 现在只能作为 ES 模块构建的命名导出进行访问。

这样,一些全局的API,在你从不使用的时候是不会出现Vue上的,不像Vue2.0+,不管你有没有使用Vue.nextTick,这一部分的代码都会存在Vue上。

// 在Vue3需要这样引入,便于静态分析的tree-shaking
import { nextTick } from 'vue'

nextTick(() => {
  // 一些和DOM有关的东西
})

受影响的全局API有:

  • Vue.nextTick
  • Vue.observable (用 Vue.reactive 替换)
  • Vue.version
  • Vue.compile (仅全构建)
  • Vue.set (仅兼容构建)
  • Vue.delete (仅兼容构建)

同时,除了公共 api,许多内部组件/帮助器现在也被导出为命名导出,只有当编译器的输出是这些特性时,才允许编译器导入这些特性。

<transition>
  <div v-show="ok">hello</div>
</transition>

会被编译为

import { h, Transition, withDirectives, vShow } from 'vue'

export function render() {
  return h(Transition, [withDirectives(h('div', 'hello'), [[vShow, this.ok]])])
}

这样的好处是,如果你不使用 Transition 组件,那么你的最终打包代码中将不会出现此功能的代码。

插件中的用法

在Vue3中,如果你需要使用全局API,必须显示导入

import { nextTick } from 'vue'

const plugin = {
  install: app => {
    nextTick(() => {
      // ...
    })
  }
}

这样,你的插件打包的时候可能因为模块捆绑包的影响,引入了整个Vue的代码,这并不是你所期望的。防止这种情况发生的一种常见做法是配置模块绑定器以将 Vue 从最终捆绑中排除。

// 在webpack中
module.exports = {
  /*...*/
  externals: {
    vue: 'Vue' // 这将告诉 webpack 将 Vue 模块视为一个外部库,而不是捆绑它。
  }
}

// 如果你使用的是rollup 你基本上可以免费获得相同的效果
// 因为默认情况下,Rollup 会将绝对模块 id (在我们的例子中为 'vue') 作为外部依赖项,而不会将它们包含在最终的 bundle 中。
// 但是在绑定期间,它可能会发出一个“将 vue 作为外部依赖” 警告,可使用 external 选项抑制该警告:

// rollup.config.js
export default {
  /*...*/
  external: ['vue']
}