前端 技术随笔

Vue3.0 前的 TypeScript 最佳入门实践

本文为本地镜像内容,原文出处:https://juejin.cn/post/6844903865255477261

VUE3.0 前的 TYPESCRIPT 最佳入门实践

前端劝退师 2019-06-14 138,219 阅读5分钟

前言

其实Vue官方从2.6.X版本开始就部分使用Ts重写了。

我个人对更严格类型限制没有积极的看法,毕竟各类转类型的骚写法写习惯了。

然鹅最近的一个项目中,是TypeScript+ Vue,毛计喇,学之...…真香!

注意此篇标题的“前”,本文旨在讲Ts混入框架的使用,不讲Class API

  1. 使用官方脚手架构建

npm install -g @vue/cli

OR

yarn global add @vue/cli

新的Vue CLI工具允许开发者 使用 TypeScript 集成环境 创建新项目。

只需运行vue create my-app。

然后,命令行会要求选择预设。使用箭头键选择Manually select features。

接下来,只需确保选择了TypeScript和Babel选项,如下图:

完成此操作后,它会询问你是否要使用class-style component syntax。

然后配置其余设置,使其看起来如下图所示。

Vue CLI工具现在将安装所有依赖项并设置项目。

接下来就跑项目喇。

总之,先跑起来再说。

  1. 项目目录解析

通过tree指令查看目录结构后可发现其结构和正常构建的大有不同。

这里主要关注shims-tsx.d.ts和 shims-vue.d.ts两个文件

两句话概括:

  • shims-tsx.d.ts,允许你以.tsx结尾的文件,在Vue项目中编写jsx代码
  • shims-vue.d.ts 主要用于 TypeScript 识别.vue 文件,Ts默认并不支持导入 vue 文件,这个文件告诉ts 导入.vue 文件都按VueConstructor处理。

此时我们打开亲切的src/components/HelloWorld.vue,将会发现写法已大有不同

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <!-- 省略 -->
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';

@Component
export default class HelloWorld extends Vue {
  @Prop() private msg!: string;
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>

至此,准备开启新的篇章 TypeScript极速入门 和 vue-property-decorator

  1. TYPESCRIPT极速入门

3.1 基本类型和扩展类型

Typescript与Javascript共享相同的基本类型,但有一些额外的类型。

  • 元组 Tuple
  • 枚举 enum
  • Any 与Void

  • 基本类型合集

// 数字,二、八、十六进制都支持
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;

// 字符串,单双引都行
let name: string = "bob";
let sentence: string = `Hello, my name is ${ name }.`;

// 数组,第二种方式是使用数组泛型,Array<元素类型>:
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];

let u: undefined = undefined;
let n: null = null;
  1. 特殊类型

  2. 元组 TUPLE

想象 元组 作为有组织的数组,你需要以正确的顺序预定义数据类型。

const messyArray = [' something', 2, true, undefined, null];
const tuple: [number, string, string] = [24, "Indrek" , "Lasn"]

如果不遵循 为元组 预设排序的索引规则,那么Typescript会警告。

  1. 枚举 ENUM

enum类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。

// 默认情况从0开始为元素编号,也可手动为1开始
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

let colorName: string = Color[2];
console.log(colorName);  // 输出'Green'因为上面代码里它的值是2

另一个很好的例子是使用枚举来存储应用程序状态。

  1. VOID

在Typescript中,你必须在函数中定义返回类型。像这样:

若没有返回值,则会报错:

我们可以将其返回值定义为void:

此时将无法 return

  1. ANY

Emmm...就是什么类型都行,当你无法确认在处理什么类型时可以用这个。

但要慎重使用,用多了就失去使用Ts的意义。

let person: any = "前端劝退师"
person = 25
person = true

主要应用场景有:

  1. 接入第三方库
  2. Ts菜逼前期都用

  3. NEVER

用很粗浅的话来描述就是:"Never是你永远得不到的爸爸。"

具体的行为是:

  • throw new Error(message)
  • return error("Something failed")
  • while (true) {} // 存在无法达到的终点

  • 类型断言

简略的定义是:可以用来手动指定一个值的类型。

有两种写法,尖括号和as:

let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;
let strLength: number = (someValue as string).length;

使用例子有:

当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:

function getLength(something: string | number): number {
    return something.length;
}
// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.

如果你访问长度将会报错,而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型的属性或方法,此时需要断言才不会报错:

function getLength(something: string | number): number {
    if ((<string>something).length) {
        return (<string>something).length;
    } else {
        return something.toString().length;
    }
}

3.2 泛型:GENERICS

软件工程的一个主要部分就是构建组件,构建的组件不仅需要具有明确的定义和统一的接口,同时也需要组件可复用。支持现有的数据类型和将来添加的数据类型的组件为大型软件系统的开发过程提供很好的灵活性。

  1. 泛型方法

在TypeScript里,声明泛型方法有以下两种方式:

function gen_func1<T>(arg: T): T {
    return arg;
}
// 或者
let gen_func2: <T>(arg: T) => T = function (arg) {
    return arg;
}

调用方式也有两种:

gen_func1<string>('Hello world');
gen_func2('Hello world');
  1. 泛型与ANY
// 方法一:带有any参数的方法
function any_func(arg: any): any {
    console.log(arg.length);
    return arg;
}

// 方法二:Array泛型方法
function array_func<T>(arg: Array<T>): Array<T> {
  console.log(arg.length);
    return arg;
}
  1. 泛型类型

泛型接口:

interface Generics_interface<T> {
    (arg: T): T;
}

function func_demo<T>(arg: T): T {
    return arg;
}

let func1: Generics_interface<number> = func_demo;
func1(123);     // 正确类型的实际参数
func1('123');   // 错误类型的实际参数

3.3 自定义类型:INTERFACE VS TYPE ALIAS

Interface,国内翻译成接口。 Type alias,类型别名。

  1. 相同点

都可以用来描述一个对象或函数:

interface User {
  name: string
  age: number
}

type User = {
  name: string
  age: number
};

interface SetUser {
  (name: string, age: number): void;
}
type SetUser = (name: string, age: number): void;

都允许拓展(extends):

interface extends interface
interface Name {
  name: string;
}
interface User extends Name {
  age: number;
}

type extends type
type Name = {
  name: string;
}
type User = Name & { age: number  };

interface extends type
type Name = {
  name: string;
}
interface User extends Name {
  age: number;
}

type extends interface
interface Name {
  name: string;
}
type User = Name & {
  age: number;
}
  1. 不同点

type 可以而 interface 不行

  • type 可以声明基本类型别名,联合类型,元组等类型
// 基本类型别名
type Name = string

// 联合类型
interface Dog {
    wong();
}
interface Cat {
    miao();
}

type Pet = Dog | Cat

// 具体定义数组每个位置的类型
type PetList = [Dog, Pet]
  • type 语句中还可以使用 typeof获取实例的 类型进行赋值
let div = document.createElement('div');
type B = typeof div
  • 其他骚操作
type StringOrNumber = string | number;
type Text = string | { text: string };
type NameLookup = Dictionary<string, Person>;
type Callback<T> = (data: T) => void;
type Pair<T> = [T, T];
type Coordinates = Pair<number>;
type Tree<T> = T | { left: Tree<T>, right: Tree<T> };

interface可以而 type不行

interface 能够声明合并

interface User {
  name: string
  age: number
}

interface User {
  sex: string
}

interface 有可选属性和只读属性

interface Person {
  name: string;
  age?: number;
  gender?: number;
}

interface User {
    readonly loginName: string;
    password: string;
}

3.4 实现与继承:IMPLEMENTS VS EXTENDS

implement基本用法:

interface IDeveloper {
   name: string;
   age?: number;
}
class dev implements IDeveloper {
    name = 'Alex';
    age = 20;
}
class dev2 implements IDeveloper {
    name = 'Alex';
}
class dev3 implements IDeveloper {
    name = 'Alex';
    age = '9';
}

而extends是继承父类,两者其实可以混着用:

class A extends B implements C,D,E

3.5 声明文件与命名空间:DECLARE 和 NAMESPACE

shims-tsx.d.ts 初始内容:

import Vue, { VNode } from 'vue';

declare global {
  namespace JSX {
    interface Element extends VNode {}
    interface ElementClass extends Vue {}
    interface IntrinsicElements {
      [elem: string]: any;
    }
  }
}

shims-vue.d.ts 初始内容:

declare module '*.vue' {
  import Vue from 'vue';
  export default Vue;
}

declare:当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。

常用:declare var 声明全局变量 / declare function 声明全局方法 / declare class 声明全局类 / declare enum 声明全局枚举类型 / declare global 扩展全局变量 / declare module 扩展模块。

namespace:“内部模块”现在称做“命名空间”。

3.6 访问修饰符:PRIVATE、PUBLIC、PROTECTED

  1. 默认为public
  2. 当成员被标记为private时,它就不能在声明它的类的外部访问
  3. protected和private类似,但是,protected成员在派生类中可以访问
class Animal {
    protected name: string;
    constructor(theName: string) {
        this.name = theName;
    }
}
class Rhino extends Animal {
    constructor() {
        super('Rhino');
   }
   getName() {
       console.log(this.name)
   }
}

3.7 可选参数 ( ?: )和非空断言操作符(!.)

function buildName(firstName: string, lastName?: string) {
    return firstName + ' ' + lastName
}
buildName("firstName")
buildName("firstName", "lastName")

非空断言操作符:能确定变量值一定不为空时使用。

let s = e!.name;  // 断言e是非空并访问name属性
  1. VUE组件的TS写法

从 vue2.5 之后,vue 对 ts 有更好的支持。有两种书写方式:

Vue.extend

import Vue from 'vue'

const Component = Vue.extend({
  // type inference enabled
})

vue-class-component

import { Component, Vue, Prop } from 'vue-property-decorator'

@Component
export default class Test extends Vue {
  @Prop({ type: Object })
  private test: { value: string }
}

4.1 VUE-CLASS-COMPONENT

vue-property-decorator的装饰器:

  • @Prop
  • @PropSync
  • @Provide
  • @Model
  • @Watch
  • @Inject
  • @Emit
  • @Component
  • Mixins

vuex-class的装饰器:

  • @State
  • @Getter
  • @Action
  • @Mutation

原始Vue组件模版替换成修饰符写法:

import { Component, Vue, Prop } from 'vue-property-decorator';
import { State, Getter } from 'vuex-class';
import { count, name } from '@/person'
import { componentA, componentB } from '@/components';

@Component({
    components:{ componentA, componentB},
})
export default class HelloWorld extends Vue{
    @Prop(Number) readonly propA!: number | undefined
    @Prop({ default: 'default value' }) readonly propB!: string
    @Prop([String, Boolean]) readonly propC!: string | boolean | undefined

  // 原data
  message = 'Hello'

  // 计算属性
    private get reversedMessage (): string[] {
        return this.message.split('').reverse().join('')
    }
  // Vuex 数据
  @State((state: IRootState) => state . booking. currentStep) step!: number
    @Getter( 'person/name') name!: name

  // method
  public changeMessage (): void {
    this.message = 'Good bye'
  }
  public getName(): string {
    let storeName = name
    return storeName
  }
    // 生命周期
  private created ():void { },
  private mounted ():void { },
  private updated ():void { },
  private destroyed ():void { }
}

4.2 添加全局工具

引入全局模块,需要改main.ts:

import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';

Vue.config.productionTip = false;

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

使用 i18n:

import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import i18n from './i18n';

Vue.config.productionTip = false;

new Vue({
    router,
    store,
    i18n,
    render: (h) => h(App),
}).$mount('#app');

需要动 src/vue-shim.d.ts:

declare module 'vue/types/vue' {
  interface Vue {
        readonly $i18n: VueI18Next;
        $t: TranslationFunction;
    }
}

4.3 AXIOS 使用与封装

如果只是想简单在Ts里体验使用Axios,可以安装vue-axios:

$ npm i axios vue-axios

main.ts添加:

import Vue from 'vue'
import axios from 'axios'
import VueAxios from 'vue-axios'

Vue.use(VueAxios, axios)

组件内使用:

Vue.axios.get(api).then((response) => {
  console.log(response.data)
})
this.axios.get(api).then((response) => {
  console.log(response.data)
})
this.$http.get(api).then((response) => {
  console.log(response.data)
})
  1. 新建文件 request.ts

文件目录:

-api - main.ts // 实际调用 -utils - request.ts // 接口封装

  1. request.ts文件解析
import * as axios from 'axios';
import store from '@/store';
import { Toast } from 'vant';
import { AxiosResponse, AxiosRequestConfig } from 'axios';

const baseURL = process.env.VUE_APP_URL;

const service = axios.default.create({
    baseURL,
    timeout: 0,
    maxContentLength: 4000,
});

service.interceptors.request.use((config: AxiosRequestConfig) => {
    return config;
}, (error: any) => {
    Promise.reject(error);
});

service.interceptors.response.use(
    (response: AxiosResponse) => {
        if (response.status !== 200) {
            Toast.fail('请求错误!');
        } else {
            return response.data;
        }
    },
    (error: any) => {
        return Promise.reject(error);
    });
    export default service;

为了方便,还需要定义一套固定的 axios 返回的格式,新建 ajax.ts:

export interface AjaxResponse {
    code: number;
    data: any;
    message: string;
}
  1. main.ts接口调用:
// api/main.ts
import request from '../utils/request';

// get
export function getSomeThings(params:any) {
    return request({
        url: '/api/getSomethings',
    });
}

// post
export function postSomeThings(params:any) {
    return request({
        url: '/api/postSomethings',
        methods: 'post',
        data: params
    });
}
  1. 编写一个组件

替换掉src/components/HelloWorld.vue,做一个博客帖子组件:

<template>
    <div class="blogpost">
        <h2>{{ post.title }}</h2>
        <p>{{ post.body }}</p>
        <p class="meta">Written by {{ post.author }} on {{ date }}</p>
    </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';

export interface Post {
    title: string;
    body: string;
    author: string;
    datePosted: Date;
}

@Component
export default class HelloWorld extends Vue {
    @Prop() private post!: Post;

    get date() {
        return `${this.post.datePosted.getDate()}/${this.post.datePosted.getMonth()}/${this.post.datePosted.getFullYear()}`;
    }
}
</script>

<style scoped>
h2 {
  text-decoration: underline;
}
p.meta {
  font-style: italic;
}
</style>

在Home.vue中使用:

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
    <HelloWorld v-for="blogPost in blogPosts" :post="blogPost" :key="blogPost.title" />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HelloWorld, { Post } from '@/components/HelloWorld.vue';

@Component({
  components: {
    HelloWorld,
  },
})
export default class Home extends Vue {
    private blogPosts: Post[] = [
        {
          title: 'My first blogpost ever!',
          body: 'Lorem ipsum dolor sit amet.',
          author: 'Elke',
          datePosted: new Date(2019, 1, 18),
        },
        {
          title: 'Look I am blogging!',
          body: 'Hurray for me, this is my second post!',
          author: 'Elke',
          datePosted: new Date(2019, 1, 19),
        },
        {
          title: 'Another one?!',
          body: 'Another one!',
          author: 'Elke',
          datePosted: new Date(2019, 1, 20),
        },
      ];
}
</script>
  1. 参考文章

TypeScript — JavaScript with superpowers — Part II VUE WITH TYPESCRIPT TypeScript + 大型项目实战 Python修饰符 (一)—— 函数修饰符 “@” Typescript 中的 interface 和 type到底有什么区别

  1. 总结

而关于Class API撤销,其实还是挺舒服的。 用class 来编写 Vue组件确实太奇怪了。 (所以我这篇Ts入门压根没写Class API)

标签
前端vuetypescript