Vue3官网-工具(十九)TypeScript 支持(Vue CLI)、生产环境部署

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.jsontypes property 里。

确认声明文件是一个 TypeScript 模块

为了利用好模块扩充,你需要确认你的文件中至少有一个顶级的 importexport,哪怕只是一个 export {}

在 TypeScript 中,任何包含一个顶级 importexport 的文件都被视为一个“模块”。如果类型声明在模块之外,该声明会覆盖而不是扩充原本的类型。

关于 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 中的设计限制,当它涉及到为了对函数表达式进行类型推理,你必须注意对象和数组的 validatordefault 值:

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

使用 @rollup/plugin-replace:

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。

参考各自的构建工具文档,看看它是如何做的:

跟踪运行时错误

如果在组件渲染期间发生运行时错误,它将被传递到全局的 app.config.errorHandler 配置函数,如果它已经被设置。将这个钩子与错误跟踪服务如 Sentry 一起使用可能是一个好主意,它为 Vue 提供了一个官方集成

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ChrisP3616

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值