Vue3官网-工具(十九)TypeScript 支持(Vue CLI)、生产环境部署
文章目录
总结:
- Vue CLI
- https://cli.vuejs.org/zh/guide/
1. TypeScript 支持
Vue CLI 提供内置的 TypeScript 工具支持。
NPM 包中的官方声明
随着应用的增长,静态类型系统可以帮助防止许多潜在的运行时错误,这就是为什么 Vue 3 是用 TypeScript 编写的。这意味着在 Vue 中使用 TypeScript 不需要任何其他工具——它具有一等公民支持。
推荐配置
// tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
// 这样就可以对 `this` 上的数据属性进行更严格的推断
"strict": true,
"jsx": "preserve",
"moduleResolution": "node"
}
}
请注意,必须包含 strict: true
(或至少包含 noImplicitThis: true
,它是 strict
标志的一部分) 才能在组件方法中利用 this
的类型检查,否则它总是被视为 any
类型。
参见 TypeScript 编译选项文档查看更多细节。
Webpack 配置
如果你使用自定义 Webpack 配置,需要配置 ’ ts-loader ’ 来解析 vue 文件里的 <script lang="ts">
代码块:
// webpack.config.js
module.exports = {
...
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/],
},
exclude: /node_modules/,
},
{
test: /\.vue$/,
loader: 'vue-loader',
}
...
开发工具
项目创建
Vue CLI 可以生成使用 TypeScript 的新项目,开始:
# 1. Install Vue CLI, 如果尚未安装
npm install --global @vue/cli@next
# 2. 创建一个新项目, 选择 "Manually select features" 选项
vue create my-project-name
# 3. 如果已经有一个不存在TypeScript的 Vue CLI项目,请添加适当的 Vue CLI插件:
vue add typescript
请确保组件的 script
部分已将语言设置为 TypeScript:
<script lang="ts">
...
</script>
或者,如果你想将 TypeScript 与 JSX render
函数结合起来:
<script lang="tsx">
...
</script>
编辑器支持
对于使用 TypeScript 开发 Vue 应用程序,我们强烈建议使用 Visual Studio Code,它为 TypeScript 提供了很好的开箱即用支持。如果你使用的是单文件组件 (SFCs),那么可以使用很棒的 Volar extension,它在 SFCs 中提供了 TypeScript 推理和许多其他优秀的特性。
WebStorm 还为 TypeScript 和 Vue 提供现成的支持。
定义 Vue 组件
要让 TypeScript 正确推断 Vue 组件选项中的类型,需要使用 defineComponent
全局方法定义组件:
import { defineComponent } from 'vue'
const Component = defineComponent({
// 已启用类型推断
})
如果你使用的是单文件组件,则通常会被写成:
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
// 已启用类型推断
})
</script>
与 Options API 一起使用
TypeScript 应该能够在不显式定义类型的情况下推断大多数类型。例如,对于拥有一个数字类型的 count
property 的组件来说,如果你试图对其调用字符串独有的方法,会出现错误:
const Component = defineComponent({
data() {
return {
count: 0
}
},
mounted() {
const result = this.count.split('') // => Property 'split' does not exist on type 'number'
}
})
如果你有一个复杂的类型或接口,你可以使用 type assertion 对其进行指明:
interface Book {
title: string
author: string
year: number
}
const Component = defineComponent({
data() {
return {
book: {
title: 'Vue 3 Guide',
author: 'Vue Team',
year: 2020
} as Book
}
}
})
为 globalProperties
扩充类型
Vue 3 提供了一个 globalProperties
对象,用来添加可以被任意组件实例访问的全局 property。例如一个插件想要注入一个共享全局对象或函数。
// 用户定义
import axios from 'axios'
const app = Vue.createApp({})
app.config.globalProperties.$http = axios
// 验证数据的插件
export default {
install(app, options) {
app.config.globalProperties.$validate = (data: object, rule: object) => {
// 检查对象是否合规
}
}
}
为了告诉 TypeScript 这些新 property,我们可以使用模块扩充 (module augmentation)。
在上述示例中,我们可以添加以下类型声明:
import axios from 'axios'
declare module '@vue/runtime-core' {
export interface ComponentCustomProperties {
$http: typeof axios
$validate: (data: object, rule: object) => boolean
}
}
我们可以把这些类型声明放在同一个文件里,或一个项目级别的 *.d.ts
文件 (例如在 TypeScript 会自动加载的 src/typings
文件夹中)。对于库/插件作者来说,这个文件应该被定义在 package.json
的 types
property 里。
确认声明文件是一个 TypeScript 模块
为了利用好模块扩充,你需要确认你的文件中至少有一个顶级的 import
或 export
,哪怕只是一个 export {}
。
在 TypeScript 中,任何包含一个顶级 import
或 export
的文件都被视为一个“模块”。如果类型声明在模块之外,该声明会覆盖而不是扩充原本的类型。
关于 ComponentCustomProperties
类型的更多信息,请参阅其在 @vue/runtime-core
中的定义及其 TypeScript 测试用例学习更多。
注解返回类型
由于 Vue 声明文件的循环特性,TypeScript 可能难以推断 computed 的类型。因此,你可能需要注解计算属性的返回类型。
import { defineComponent } from 'vue'
const Component = defineComponent({
data() {
return {
message: 'Hello!'
}
},
computed: {
// 需要注解
greeting(): string {
return this.message + '!'
},
// 在使用 setter 进行计算时,需要对 getter 进行注解
greetingUppercased: {
get(): string {
return this.greeting.toUpperCase()
},
set(newValue: string) {
this.message = newValue.toUpperCase()
}
}
}
})
注解 Props
Vue 对定义了 type
的 prop 执行运行时验证。要将这些类型提供给 TypeScript,我们需要使用 PropType
指明构造函数:
import { defineComponent, PropType } from 'vue'
interface Book {
title: string
author: string
year: number
}
const Component = defineComponent({
props: {
name: String,
id: [Number, String],
success: { type: String },
callback: {
type: Function as PropType<() => void>
},
book: {
type: Object as PropType<Book>,
required: true
},
metadata: {
type: null // metadata 的类型是 any
}
}
})
WARNING
由于 TypeScript 中的设计限制,当它涉及到为了对函数表达式进行类型推理,你必须注意对象和数组的
validator
和default
值:
import { defineComponent, PropType } from 'vue'
interface Book {
title: string
year?: number
}
const Component = defineComponent({
props: {
bookA: {
type: Object as PropType<Book>,
// 请务必使用箭头函数
default: () => ({
title: 'Arrow Function Expression'
}),
validator: (book: Book) => !!book.title
},
bookB: {
type: Object as PropType<Book>,
// 或者提供一个明确的 this 参数
default(this: void) {
return {
title: 'Function Expression'
}
},
validator(this: void, book: Book) {
return !!book.title
}
}
}
})
注解 emit
我们可以为触发的事件注解一个有效载荷。另外,所有未声明的触发事件在调用时都会抛出一个类型错误。
const Component = defineComponent({
emits: {
addBook(payload: { bookName: string }) {
// perform runtime 验证
return payload.bookName.length > 0
}
},
methods: {
onSubmit() {
this.$emit('addBook', {
bookName: 123 // 类型错误!
})
this.$emit('non-declared-event') // 类型错误!
}
}
})
与组合式 API 一起使用
在 setup()
函数中,不需要将类型传递给 props
参数,因为它将从 props
组件选项推断类型。
import { defineComponent } from 'vue'
const Component = defineComponent({
props: {
message: {
type: String,
required: true
}
},
setup(props) {
const result = props.message.split('') // 正确, 'message' 被声明为字符串
const filtered = props.message.filter(p => p.value) // 将引发错误: Property 'filter' does not exist on type 'string'
}
})
类型声明 refs
Refs 根据初始值推断类型:
import { defineComponent, ref } from 'vue'
const Component = defineComponent({
setup() {
const year = ref(2020)
const result = year.value.split('') // => Property 'split' does not exist on type 'number'
}
})
有时我们可能需要为 ref 的内部值指定复杂类型。我们可以在调用 ref 重写默认推理时简单地传递一个泛型参数:
const year = ref<string | number>('2020') // year's type: Ref<string | number>
year.value = 2020 // ok!
TIP
如果泛型的类型未知,建议将
ref
转换为Ref<T>
。
为模板引用定义类型
有时你可能需要为一个子组件标注一个模板引用,以调用其公共方法。例如我们有一个 MyModal
子组件,它有一个打开模态的方法:
import { defineComponent, ref } from 'vue'
const MyModal = defineComponent({
setup() {
const isContentShown = ref(false)
const open = () => (isContentShown.value = true)
return {
isContentShown,
open
}
}
})
我们希望从其父组件的一个模板引用调用这个方法:
import { defineComponent, ref } from 'vue'
const MyModal = defineComponent({
setup() {
const isContentShown = ref(false)
const open = () => (isContentShown.value = true)
return {
isContentShown,
open
}
}
})
const app = defineComponent({
components: {
MyModal
},
template: `
<button @click="openModal">Open from parent</button>
<my-modal ref="modal" />
`,
setup() {
const modal = ref()
const openModal = () => {
modal.value.open()
}
return { modal, openModal }
}
})
它可以工作,但是没有关于 MyModal
及其可用方法的类型信息。为了解决这个问题,你应该在创建引用时使用 InstanceType
:
setup() {
const modal = ref<InstanceType<typeof MyModal>>()
const openModal = () => {
modal.value?.open()
}
return { modal, openModal }
}
请注意你还需要使用可选链操作符或其它方式来确认 modal.value
不是 undefined。
类型声明 reactive
当声明类型 reactive
property,我们可以使用接口:
import { defineComponent, reactive } from 'vue'
interface Book {
title: string
year?: number
}
export default defineComponent({
name: 'HelloWorld',
setup() {
const book = reactive<Book>({ title: 'Vue 3 Guide' })
// or
const book: Book = reactive({ title: 'Vue 3 Guide' })
// or
const book = reactive({ title: 'Vue 3 Guide' }) as Book
}
})
类型声明 computed
计算值将根据返回值自动推断类型
import { defineComponent, ref, computed } from 'vue'
export default defineComponent({
name: 'CounterButton',
setup() {
let count = ref(0)
// 只读
const doubleCount = computed(() => count.value * 2)
const result = doubleCount.value.split('') // => Property 'split' does not exist on type 'number'
}
})
为事件处理器添加类型
在处理原生 DOM 事件的时候,正确地为处理函数的参数添加类型或许会是有用的。让我们看这个例子:
<template>
<input type="text" @change="handleChange" />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
setup() {
// `evt` 将会是 `any` 类型
const handleChange = evt => {
console.log(evt.target.value) // 此处 TS 将抛出异常
}
return { handleChange }
}
})
</script>
如你所见,在没有为 evt
参数正确地声明类型的情况下,当我们尝试获取 <input>
元素的值时,TypeScript 将抛出异常。解决方案是将事件的目标转换为正确的类型:
const handleChange = (evt: Event) => {
console.log((evt.target as HTMLInputElement).value)
}
2. 生产环境部署
INFO
如果你使用 Vue CLI,下面的大多数提示都是默认启用的。此部分仅当你使用自定义构建设置时才相关。
开启生产环境模式
在开发期间,Vue 提供了许多警告,以帮助你处理常见的错误和隐患。但是,这些警告字符串在生产环境中会变得无意义,并且会增大应用程序的负担。此外,有一些警告检查还会产生些许的运行时开销,在生产模式下可以避免这些开销。
不使用构建工具
如果你正在使用完整的构建版本,即直接通过脚本标签引入 Vue,而不使用构建工具,那么请确保在生产环境中使用压缩版。这可以在安装指南中找到。
使用构建工具
当使用 Webpack 或 Browserify 这样的构建工具时,生产环境模式将由 Vue 的源代码中的 process.env.NODE_ENV
决定,默认为开发模式。这两种构建工具都提供了重写这个变量以启用 Vue 的生产模式的方法,并且在构建过程中警告将被压缩工具删除。Vue CLI 已经为你预先配置了这个,不过了解它的工作原理会更好:
Webpack
在 Webpack 4+,你可以使用 mode
选项:
module.exports = {
mode: 'production'
}
Browserify
-
将当前的环境变量
NODE_ENV
设置为"production"
作为运行的打包命令。它告诉vueify
避免引入热重载和开发相关的代码。 -
将一个全局的 envify 转换应用到你的包中。这使得压缩工具可以删除包裹在环境变量条件块中的Vue源代码中的所有警告。例如:
NODE_ENV=production browserify -g envify -e main.js | uglifyjs -c -m > build.js
1
-
或者,利用 Gulp 使用 envify:
// Use the envify custom module to specify environment variables const envify = require('envify/custom') browserify(browserifyOptions) .transform(vueify) .transform( // Required in order to process node_modules files { global: true }, envify({ NODE_ENV: 'production' }) ) .bundle()
-
或者,利用 Grunt 和 grunt-browserify 使用 envify:
// Use the envify custom module to specify environment variables const envify = require('envify/custom') browserify: { dist: { options: { // Function to deviate from grunt-browserify's default order configure: (b) => b .transform('vueify') .transform( // Required in order to process node_modules files { global: true }, envify({ NODE_ENV: 'production' }) ) .bundle() } } }
Rollup
const replace = require('@rollup/plugin-replace')
rollup({
// ...
plugins: [
replace({
'process.env.NODE_ENV': JSON.stringify( 'production' )
})
]
}).then(...)
预编译模板
当使用 DOM 内模板或 JavaScript 内模板字符串时,将动态地执行从模板到渲染函数的编译。在大多数情况下,这已经足够快了,但是如果应用程序对性能敏感,最好避免这样做。
预编译模板最简单的方法是使用单文件组件——相关的构建设置自动为你执行预编译,所以构建代码包含已经编译的渲染函数,而不是原始的模板字符串。
如果你正在使用 Webpack,并且更喜欢将 JavaScript 和模板文件分开,你可以使用 vue-template-loader,它还可以在构建步骤中将模板文件转换为 JavaScript 渲染函数。
提取组件CSS
当使用单文件组件时,组件内部的 CSS 会通过 JavaScript 以 <style>
标签的形式被动态注入。这有一个小的运行时成本,如果你使用服务器端渲染,它将导致“无样式内容的闪现” 。将所有组件的 CSS 提取到同一个文件中可以避免这些问题,还可以更好地压缩和缓存 CSS。
参考各自的构建工具文档,看看它是如何做的:
- Webpack + vue-loader (
vue-cli
webpack 模板已经预先配置了这个) - Browserify + vueify
- Rollup + rollup-plugin-vue
跟踪运行时错误
如果在组件渲染期间发生运行时错误,它将被传递到全局的 app.config.errorHandler
配置函数,如果它已经被设置。将这个钩子与错误跟踪服务如 Sentry 一起使用可能是一个好主意,它为 Vue 提供了一个官方集成。