優雅的在vue中使用TypeScript

前端森林發表於2020-10-19

引言

近幾年前端對 TypeScript 的呼聲越來越高,Typescript 也成為了前端必備的技能。TypeScript 是 JS 型別的超集,並支援了泛型、型別、名稱空間、列舉等特性,彌補了 JS 在大型應用開發中的不足。

在單獨學習 TypeScript 時,你會感覺很多概念還是比較好理解的,但是和一些框架結合使用的話坑還是比較多的,例如使用 React、Vue 這些框架的時候與 TypeScript 的結合會成為一大障礙,需要去檢視框架提供的.d.ts 的宣告檔案中一些複雜型別的定義、元件的書寫方式等都要做出不小的調整。

本篇文章主要是結合我的經驗和大家聊一下如何在Vue中平滑的從js過渡到ts,閱讀本文建議對 TypeScript 有一定了解,因為文中對於一些 TypeScript 的基礎的知識不會有太過於詳細的講解。(具體可以參考官方文件https://www.w3cschool.cn/typescript/typescript-tutorial.html,官方文件就是最好的入門手冊)

構建

通過官方腳手架構建安裝

# 1. 如果沒有安裝 Vue CLI 就先安裝
npm install --global @vue/cli

最新的Vue CLI工具允許開發者 使用 TypeScript 整合環境 建立新專案。

只需執行vue create my-app

然後,命令列會要求選擇預設。使用箭頭鍵選擇 Manually select features。

接下來,只需確保選擇了 TypeScript 和 Babel 選項,如下圖:

然後配置其餘設定,如下圖:


設定完成 vue cli 就會開始安裝依賴並設定專案。

目錄解析

安裝完成開啟專案,你會發現整合 ts 後的專案目錄結構是這樣子的:

|-- ts-vue
    |-- .browserslistrc     # browserslistrc 配置檔案 (用於支援 Autoprefixer)
    |-- .eslintrc.js        # eslint 配置
    |-- .gitignore
    |-- babel.config.js     # babel-loader 配置
    |-- package-lock.json
    |-- package.json        # package.json 依賴
    |-- postcss.config.js   # postcss 配置
    |-- README.md
    |-- tsconfig.json       # typescript 配置
    |-- vue.config.js       # vue-cli 配置
    |-- public              # 靜態資源 (會被直接複製)
    |   |-- favicon.ico     # favicon圖示
    |   |-- index.html      # html模板
    |-- src
    |   |-- App.vue         # 入口頁面
    |   |-- main.ts         # 入口檔案 載入元件 初始化等
    |   |-- shims-tsx.d.ts
    |   |-- shims-vue.d.ts
    |   |-- assets          # 主題 字型等靜態資源 (由 webpack 處理載入)
    |   |-- components      # 全域性元件
    |   |-- router          # 路由
    |   |-- store           # 全域性 vuex store
    |   |-- styles          # 全域性樣式
    |   |-- views           # 所有頁面
    |-- tests               # 測試

其實大致看下來,與之前用js構建的專案目錄沒有什麼太大的不同,區別主要是之前 js 字尾的現在改為了 ts 字尾,還多了tsconfig.jsonshims-tsx.d.tsshims-vue.d.ts這幾個檔案,那這幾個檔案是幹嘛的呢:

  • tsconfig.json: typescript 配置檔案,主要用於指定待編譯的檔案和定義編譯選項
  • shims-tsx.d.ts: 允許.tsx 結尾的檔案,在 Vue 專案中編寫 jsx 程式碼
  • shims-vue.d.ts: 主要用於 TypeScript 識別.vue 檔案,Ts 預設並不支援匯入 vue 檔案

使用

開始前我們先來了解一下在 vue 中使用 typescript 非常好用的幾個庫

  • vue-class-component: vue-class-component是一個 Class Decorator,也就是類的裝飾器
  • vue-property-decorator: vue-property-decorator是基於 vue 組織裡 vue-class-component 所做的擴充

    import { Vue, Component, Inject, Provide, Prop, Model, Watch, Emit, Mixins } from 'vue-property-decorator'
  • vuex-module-decorators: 用 typescript 寫 vuex 很好用的一個庫

    import { Module, VuexModule, Mutation, Action, MutationAction, getModule } from 'vuex-module-decorators'

元件宣告

建立元件的方式變成如下

import { Component, Prop, Vue, Watch } from "vue-property-decorator";

@Component
export default class Test extends Vue {}

data 物件

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

@Component
export default class Test extends Vue {
  private name: string;
}

Prop 宣告

@Prop({ default: false }) private isCollapse!: boolean;
@Prop({ default: true }) private isFirstLevel!: boolean;
@Prop({ default: "" }) private basePath!: string;
  • !: 表示一定存在,?: 表示可能不存在。這兩種在語法上叫賦值斷言
  • @Prop(options: (PropOptions | Constructor[] | Constructor) = {})

    • PropOptions,可以使用以下選項:type,default,required,validator
    • Constructor[],指定 prop 的可選型別
    • Constructor,例如 String,Number,Boolean 等,指定 prop 的型別

method

js 下是需要在 method 物件中宣告方法,現變成如下

public clickFunc(): void {
  console.log(this.name)
  console.log(this.msg)
}

Watch 監聽屬性

@Watch("$route", { immediate: true })
private onRouteChange(route: Route) {
  const query = route.query as Dictionary<string>;
  if (query) {
  this.redirect = query.redirect;
  this.otherQuery = this.getOtherQuery(query);
  }
}
  • @Watch(path: string, options: WatchOptions = {})

    • options 包含兩個屬性 immediate?:boolean 偵聽開始之後是否立即呼叫該回撥函式 / deep?:boolean 被偵聽的物件的屬性被改變時,是否呼叫該回撥函式
  • @Watch('arr', { immediate: true, deep: true })
    onArrChanged(newValue: number[], oldValue: number[]) {}

computed 計算屬性

public get allname() {
  return 'computed ' + this.name;
}

allname 是計算後的值,name 是被監聽的值

生命週期函式

public created(): void {
  console.log('created');
}

public mounted():void{
  console.log('mounted')
}

emit 事件

import { Vue, Component, Emit } from "vue-property-decorator";
@Component
export default class MyComponent extends Vue {
  count = 0;
  @Emit()
  addToCount(n: number) {
    this.count += n;
  }
  @Emit("reset")
  resetCount() {
    this.count = 0;
  }
  @Emit()
  returnValue() {
    return 10;
  }
  @Emit()
  onInputChange(e) {
    return e.target.value;
  }
  @Emit()
  promise() {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(20);
      }, 0);
    });
  }
}

使用 js 寫法

export default {
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    addToCount(n) {
      this.count += n;
      this.$emit("add-to-count", n);
    },
    resetCount() {
      this.count = 0;
      this.$emit("reset");
    },
    returnValue() {
      this.$emit("return-value", 10);
    },
    onInputChange(e) {
      this.$emit("on-input-change", e.target.value, e);
    },
    promise() {
      const promise = new Promise((resolve) => {
        setTimeout(() => {
          resolve(20);
        }, 0);
      });
      promise.then((value) => {
        this.$emit("promise", value);
      });
    },
  },
};
  • @Emit(event?: string)
  • @Emit 裝飾器接收一個可選引數,該引數是&dollar;Emit 的第一個引數,充當事件名。如果沒有提供這個引數,&dollar;Emit 會將回撥函式名的 camelCase 轉為 kebab-case,並將其作為事件名
  • @Emit 會將回撥函式的返回值作為第二個引數,如果返回值是一個 Promise 物件,&dollar;emit 會在 Promise 物件被標記為 resolved 之後觸發
  • @Emit 的回撥函式的引數,會放在其返回值之後,一起被&dollar;emit 當做引數使用

vuex

在使用 store 裝飾器之前,先過一下傳統的 store 用法吧

export default {
  namespaced: true,
  state: {
    foo: "",
  },
  getters: {
    getFoo(state) {
      return state.foo;
    },
  },
  mutations: {
    setFooSync(state, payload) {
      state.foo = payload;
    },
  },
  actions: {
    setFoo({ commit }, payload) {
      commot("getFoo", payload);
    },
  },
};

然後開始使用vuex-module-decorators

import {
  VuexModule,
  Mutation,
  Action,
  getModule,
  Module,
} from "vuex-module-decorators";
  • VuexModule 用於基本屬性

    export default class TestModule extends VuexModule {}

    VuexModule 提供了一些基本屬性,包括 namespaced,state,getters,modules,mutations,actions,context

  • @Module 標記當前為 module

    @Module({ dynamic: true, store, name: "settings" })
    class Settings extends VuexModule implements ISettingsState {}

    module 本身有幾種可以配置的屬性:

    • namespaced:boolean 啟/停用 分模組
    • stateFactory:boolean 狀態工廠
    • dynamic:boolean 在 store 建立之後,再新增到 store 中。開啟 dynamic 之後必須提供下面的屬性
    • name:string 指定模組名稱
    • store:Vuex.Store 實體 提供初始的 store
  • @Mutation 標註為 mutation

    @Mutation
    private SET_NAME(name: string) {
    // 設定使用者名稱
    this.name = name;
    }
  • @Action 標註為 action

    @Action
    public async Login(userInfo: { username: string; password: string }) {
      // 登入介面,拿到token
      let { username, password } = userInfo;
      username = username.trim();
      const { data } = await login({ username, password });
      setToken(data.accessToken);
      this.SET_TOKEN(data.accessToken);
    }
  • getModule 得到一個型別安全的 store,module 必須提供 name 屬性

    export const UserModule = getModule(User);

示例

我之前基於 ts+vue+element 構建了一個簡單的中後臺通用模板。


涵蓋的功能如下:

- 登入 / 登出

- 許可權驗證
  - 頁面許可權
  - 許可權配置

- 多環境釋出
  - Dev / Stage / Prod

- 全域性功能
  - 動態換膚
  - 動態側邊欄(支援多級路由巢狀)
  - Svg 圖示
  - 全屏
  - 設定
  - Mock 資料 / Mock 伺服器

- 元件
  - ECharts 圖表

- 表格
  - 複雜表格

- 控制檯
- 引導頁
- 錯誤頁面
  - 404

裡面對於在 vue 中使用 typescript 的各種場景都有很好的實踐,大家感興趣的可以參考一下,https://github.com/easy-wheel/ts-vue,當然不要吝惜你的 star!!!

最後

你可以關注我的同名公眾號【前端森林】,這裡我會定期發一些大前端相關的前沿文章和日常開發過程中的實戰總結。當然,我也是開源社群的積極貢獻者,github 地址https://github.com/Cosen95,歡迎 star!!!

image

相關文章