Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

其實Vue官方從2.6.X版本開始就部分使用Ts重寫了。注意此篇標題的“前”,本文旨在講Ts混入框架的使用,不講Class API。本文參考了前端勸退師的掘金文章,鏈接在文末

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

1. 使用官方腳手架構建

npm install -g @vue/cli
# OR
yarn global add @vue/cli

新的Vue CLI工具允許開發者 使用 TypeScript 集成環境 創建新項目。只需運行

vue create my-app。

然後,命令行會要求選擇預設。使用箭頭鍵選擇Manually select features。接下來,只需確保選擇了TypeScript和Babel選項,如下圖:

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

完成此操作後,它會詢問你是否要使用class-style component syntax。然後配置其餘設置,使其看起來如下圖所示。

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

Vue CLI工具現在將安裝所有依賴項並設置項目。

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

接下來就跑項目喇。

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

總之,先跑起來再說。

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

2. 項目目錄解析

通過tree指令查看目錄結構後可發現其結構和正常構建的大有不同。

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

這裡主要關注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,將會發現寫法已大有不同

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

至此,準備開啟新的篇章 TypeScript極速入門 和 vue-property-decorator

3. TypeScript極速入門

3.1 基本類型和擴展類型

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

Typescript與Javascript共享相同的基本類型,但有一些額外的類型。

  • 元組 Tuple
  • 枚舉 enum
  • Any 與Void

基本類型合集

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

特殊類型

1. 元組 Tuple

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

想象 元組 作為有組織的數組,你需要以正確的順序預定義數據類型。

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

如果不遵循 為元組 預設排序的索引規則,那麼Typescript會警告。

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

(tuple第一項應為number類型)

2. 枚舉 enum

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

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

另一個很好的例子是使用枚舉來存儲應用程序狀態。

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

3. Void

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

在Typescript中,你必須在函數中定義返回類型。像這樣:

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

若沒有返回值,則會報錯,我們可以將其返回值定義為void:

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

此時將無法 return

4. Any

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

Emmm...就是什麼類型都行,當你無法確認在處理什麼類型時可以用這個。

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

let person: any = "前端攻城獅"
person = 25

person = true

主要應用場景有:

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

5. Never

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

用很粗淺的話來描述就是:"Never是你永遠得不到的爸爸。"具體的行為是:

  • throw new Error(message)
  • return error("Something failed")
  • while (true) {} // 存在無法達到的終點
Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

3. 類型斷言

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

簡略的定義是:可以用來手動指定一個值的類型。有兩種寫法,尖括號和as:

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
let strLength: number = (someValue as string).length;
/<string>

使用例子有:

當 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'.
// Property 'length' does not exist on type 'number'.

如果你訪問長度將會報錯,而有時候,我們確實需要在還不確定類型的時候就訪問其中一個類型的屬性或方法,此時需要斷言才不會報錯:

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

3.2 泛型:Generics

軟件工程的一個主要部分就是構建組件,構建的組件不僅需要具有明確的定義和統一的接口,同時也需要組件可複用。支持現有的數據類型和將來添加的數據類型的組件為大型軟件系統的開發過程提供很好的靈活性。

在C#和Java中,可以使用"泛型"來創建可複用的組件,並且組件可支持多種數據類型。這樣便可以讓用戶根據自己的數據類型來使用組件。

1. 泛型方法

在TypeScript裡,聲明泛型方法有以下兩種方式:

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

調用方式也有兩種:

gen_func1<string>('Hello world');
gen_func2('Hello world');
// 第二種調用方式可省略類型參數,因為編譯器會根據傳入參數來自動識別對應的類型。

/<string>

2. 泛型與Any

Ts 的特殊類型 Any 在具體使用時,可以代替任意類型,咋一看兩者好像沒啥區別,其實不然:

// 方法一:帶有any參數的方法
function any_func(arg: any): any {
console.log(arg.length);
\t\treturn arg;
}
// 方法二:Array泛型方法
function array_func(arg: Array): Array {
\t console.log(arg.length);
\t\treturn arg;
}
  • 方法一,打印了arg參數的length屬性。因為any可以代替任意類型,所以該方法在傳入參數不是數組或者帶有length屬性對象時,會拋出異常。
  • 方法二,定義了參數類型是Array的泛型類型,肯定會有length屬性,所以不會拋出異常。

3. 泛型類型

泛型接口:

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

function func_demo(arg: T): T {
return arg;
}
let func1: Generics_interface<number> = func_demo;
func1(123); // 正確類型的實際參數
func1('123'); // 錯誤類型的實際參數
/<number>

3.3 自定義類型:Interface vs Type alias

Interface,國內翻譯成接口。

Type alias,類型別名。

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

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 和 type 都可以拓展,並且兩者並不是相互獨立的,也就是說interface可以 extends type, type 也可以 extends interface 。 雖然效果差不多,但是兩者語法不同

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;
}

2. 不同點

type 可以而 interface 不行

  • type 可以聲明基本類型別名,聯合類型,元組等類型
// 基本類型別名
type Name = string

// 聯合類型
interface Dog {
wong();
}
interface Cat {
miao();
}
type Pet = Dog | Cat
// 具體定義數組每個位置的類型
type PetList = [Dog, Pet]
  • type 語句中還可以使用 typeof獲取實例的類型進行賦值
// 當你想獲取一個變量的類型時,使用 typeof
let div = document.createElement('div');
type B = typeof div
  • 其他騷操作
type StringOrNumber = string | number; 
type Text = string | { text: string };
type NameLookup = Dictionary<string>;
type Callback = (data: T) => void;
type Pair = [T, T];
type Coordinates = Pair<number>;
type Tree = T | { left: Tree, right: Tree };
/<number>
/<string>

interface可以而 type不行

interface User {
name: string
age: number
}
interface User {
sex: string
}
/*
User 接口為 {
name: string
age: number
sex: string
}
*/

interface 有可選屬性和只讀屬性

  • 可選屬性
  • 接口裡的屬性不全都是必需的。 有些是隻在某些條件下存在,或者根本不存在。 例如給函數傳入的參數對象中只有部分屬性賦值了。帶有可選屬性的接口與普通的接口定義差不多,只是在可選屬性名字定義的後面加一個?符號。如下所示
interface Person {
name: string;
age?: number;
gender?: number;
}
  • 只讀屬性
  • 顧名思義就是這個屬性是不可寫的,對象屬性只能在對象剛剛創建的時候修改其值。 你可以在屬性名前用 readonly來指定只讀屬性,如下所示:
interface User {
readonly loginName: string;
password: string;
}

上面的例子說明,當完成User對象的初始化後loginName就不可以修改了。

3.4 實現與繼承:implements vs extends

extends很明顯就是ES6裡面的類繼承,那麼implement又是做什麼的呢?它和extends有什麼不同?

implement,實現。與C#或Java裡接口的基本作用一樣,TypeScript也能夠用它來明確的強制一個類去符合某種契約

implement基本用法

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

而extends是繼承父類,兩者其實可以混著用:

 class A extends B implements C,D,E

搭配 interface和type的用法有:

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

3.5 聲明文件與命名空間:declare 和 namespace

前面我們講到Vue項目中的shims-tsx.d.ts和shims-vue.d.ts,其初始內容是這樣的:

// shims-tsx.d.ts
import Vue, { VNode } from 'vue';
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
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:“內部模塊”現在稱做“命名空間”。module X { 相當於現在推薦的寫法 namespace X {}

跟其他 JS 庫協同

類似模塊,同樣也可以通過為其他 JS 庫使用了命名空間的庫創建 .d.ts 文件的聲明文件,如為 D3 JS 庫,可以創建這樣的聲明文件:

declare namespace D3{
export interface Selectors { ... }
}
declare var d3: D3.Base;

所以上述兩個文件:

  • shims-tsx.d.ts, 在全局變量 global中批量命名了數個內部模塊。
  • shims-vue.d.ts,意思是告訴 TypeScript *.vue 後綴的文件可以交給 vue 模塊來處理。

3.6 訪問修飾符:private、public、protected

其實很好理解:

  1. 默認為public
  2. 當成員被標記為private時,它就不能在聲明它的類的外部訪問,比如:
class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;
}
}
let a = new Animal('Cat').name; //錯誤,‘name’是私有的
  1. 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) //此處的name就是Animal類中的name
}
}

3.7 可選參數 ( ?: )和非空斷言操作符(!.)

可選參數

function buildName(firstName: string, lastName?: string) {
return firstName + ' ' + lastName
}
// 錯誤演示
buildName("firstName", "lastName", "lastName")
// 正確演示
buildName("firstName")
// 正確演示
buildName("firstName", "lastName")

非空斷言操作符:

能確定變量值一定不為空時使用。與可選參數 不同的是,非空斷言操作符不會防止出現 null 或 undefined。

let s = e!.name; // 斷言e是非空並訪問name屬性 

4. Vue組件的Ts寫法

從 vue2.5 之後,vue 對 ts 有更好的支持。根據官方文檔,vue 結合 typescript ,有兩種書寫方式:

**Vue.extend **

 import Vue from 'vue'
const Component = Vue.extend({
\t// 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 }
}

理想情況下,Vue.extend 的書寫方式,是學習成本最低的。在現有寫法的基礎上,幾乎 0 成本的遷移。但是Vue.extend模式,需要與mixins 結合使用。在 mixin 中定義的方法,不會被 typescript 識別到,這就意味著會出現丟失代碼提示、類型檢查、編譯報錯等問題。

菜鳥才做選擇,大佬都挑最好的。直接講第二種吧:

4.1 vue-class-component

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

我們回到src/components/HelloWorld.vue

<template>

{{ msg }}




/<template>

<style>

有寫過python的同學應該會發現似曾相識:

  • vue-property-decorator這個官方支持的庫裡,提供了函數 **裝飾器(修飾符)**語法

1. 函數修飾符 @

“@”,與其說是修飾函數倒不如說是引用、調用它修飾的函數。或者用句大白話描述:@: "下面的被我包圍了。"舉個栗子,下面的一段代碼,裡面兩個函數,沒有被調用,也會有輸出結果:可以查看

test(f){
console.log("before ...");
f()
\t\tconsole.log("after ...");
}
@test
func(){
\tconsole.log("func was called");
}

直接運行,輸出結果:

before ...
func was called
after ...

上面代碼可以看出來:

  • 只定義了兩個函數:test和func,沒有調用它們。
  • 如果沒有“@test”,運行應該是沒有任何輸出的。

但是,解釋器讀到函數修飾符“@”的時候,後面步驟會是這樣:

  1. 去調用test函數,test函數的入口參數就是那個叫“func”的函數;
  2. test函數被執行,入口參數的(也就是func函數)會被調用(執行);

換言之,修飾符帶的那個函數的入口參數,就是下面的那個整個的函數。有點兒類似JavaScript裡面的 function a (function () { ... });

2. vue-property-decorator和vuex-class提供的裝飾器

vue-property-decorator的裝飾器:

  • @Prop
  • @PropSync
  • @Provide
  • @Model
  • @Watch
  • @Inject
  • @Provide
  • @Emit
  • @Component (provided by
    vue-class-component)
  • Mixins (the helper function named mixins provided by vue-class-component)

vuex-class的裝飾器:

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

我們拿原始Vue組件模版來看:

import {componentA,componentB} from '@/components';
export default {
\tcomponents: { componentA, componentB},
\tprops: {
propA: { type: Number },
propB: { default: 'default value' },
propC: { type: [String, Boolean] },
}
// 組件數據
data () {
return {
message: 'Hello'
}
},
// 計算屬性
computed: {
reversedMessage () {
return this.message.split('').reverse().join('')
}
// Vuex數據
step() {
\treturn this.$store.state.count
}
},
methods: {
changeMessage () {
this.message = "Good bye"

},
getName() {
\tlet name = this.$store.getters['person/name']
\treturn name
}
},
// 生命週期
created () { },
mounted () { },
updated () { },
destroyed () { }
}

以上模版替換成修飾符寫法則是:

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{
\t@Prop(Number) readonly propA!: number | undefined
@Prop({ default: 'default value' }) readonly propB!: string
@Prop([String, Boolean]) readonly propC!: string | boolean | undefined

// 原data
message = 'Hello'

// 計算屬性
\tprivate get reversedMessage (): string[] {
\treturn this.message.split('').reverse().join('')
}
// Vuex 數據
@State((state: IRootState) => state . booking. currentStep) step!: number
\t@Getter( 'person/name') name!: name

// method
public changeMessage (): void {
this.message = 'Good bye'
},
public getName(): string {
let storeName = name
return storeName
}
\t// 生命週期

private created ():void { },
private mounted ():void { },
private updated ():void { },
private destroyed ():void { }
}

正如你所看到的,我們在生命週期 列表那都添加private XXXX方法,因為這不應該公開給其他組件。

而不對method做私有約束的原因是,可能會用到@Emit來向父組件傳遞信息。

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');

npm i VueI18n

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;
}
}

之後使用this.$i18n()的話就不會報錯了。

4.3 Axios 使用與封裝

Axios的封裝千人千面,如果只是想簡單在Ts裡體驗使用Axios,可以安裝vue-axios 簡單使用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 // 接口封裝

2. request.ts文件解析

import * as axios from 'axios';
import store from '@/store';
// 這裡可根據具體使用的UI組件庫進行替換
import { Toast } from 'vant';
import { AxiosResponse, AxiosRequestConfig } from 'axios';

/* baseURL 按實際項目來定義 */
const baseURL = process.env.VUE_APP_URL;
/* 創建axios實例 */
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;
}

3. 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
});
}

5. 編寫一個組件

這個是我後面將要推出的一個系列,Typescript + Vue實現一個完整的組件庫,在此之前,我將會逐漸給大家介紹一些項目需要用到的知識點,只有大家都掌握了這些基礎的東西,在接下來的專題項目中才不會出現閱讀困難的問題,也能對我的分享提出自己的一些建議

Vue新搭檔TypeScript 最佳入門實踐,為打造UI組件庫鋪路

6.補充

而關於Class API撤銷,其實還是挺舒服的。 用class 來編寫 Vue組件確實太奇怪了。 (所以我這篇Ts入門壓根沒寫Class API)

鏈接文章

http://www.typescriptlang.org/docs/home.html

https://juejin.im/post/5d0259f2518825405d15ae62


分享到:


相關文章: