解鎖 Vue3 全家桶 - TS 的正確姿勢

創建項目

基礎語法

定義 data

<script lang="ts">
import { defineComponent, reactive, ref, toRefs } from 'vue';

type Todo = {
  id: number,
  name: string,
  completed: boolean
}

export default defineComponent({
  const data = reactive({
    todoList: [] as Todo[]
  })
  const count = ref(0);
  console.log(count.value)
  return {
    ...toRefs(data)
  }
})
</script>
複製代碼

定義 props

props需要使用PropType泛型來約束。

<script lang="ts">
import { defineComponent, PropType} from 'vue';

interface UserInfo = {
  id: number,
  name: string,
  age: number
}

export default defineComponent({
  props: {
    userInfo: {
      type: Object as PropType<UserInfo>, // 泛型類型
      required: true
    }
  },
})
</script>
複製代碼

定義 methods

<script lang="ts">
import { defineComponent, reactive, ref, toRefs } from 'vue';

type Todo = {
  id: number,
  name: string,
  completed: boolean
}

export default defineComponent({
  const data = reactive({
    todoList: [] as Todo[]
  })
  // 約束輸入和輸出類型
  const newTodo = (name: string):Todo  ={
    return {
      id: this.items.length + 1,
      name,
      completed: false
    };
  }
  const addTodo = (todo: Todo)void ={
    data.todoList.push(todo)
  }
  return {
    ...toRefs(data),
    newTodo,
    addTodo
  }
})
</script>
複製代碼

vue-router

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import Home from '../views/Home.vue';
const routes: Array< RouteRecordRaw > = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
});

export default router;
複製代碼

擴展路由額外屬性

在實際項目開發中,常常會遇到這麼一個場景,某一個路由是不需要渲染到側邊欄導航上的,此時我們可以給該路由添加一個 hidden 屬性來實現。

在 ts 的強類型約束下,添加額外屬性就會報錯,那麼我們就需要擴展RouteRecordRaw類型。

// 聯合類型
type RouteConfig = RouteRecordRaw & {hidden?: boolean}; //hidden 是可選屬性
const routes: Array<RouteConfig> = [
  {
    path: '/',
    name: 'Home',
    component: Home,
    hidden: true,
    meta: {
      permission: true,
      icon: ''
    }
  }
];
複製代碼

在 setup 中使用

需要導入useRouter創建一個router實例。

<script lang="ts">
import { useRouter } from 'vue-router';
import { defineComponent } from 'vue';
export default defineComponent({
  setup () {
    const router = useRouter();
    goRoute(path) {
       router.push({path})
    }
  }
})
</script>
複製代碼

vuex

使用 this.$store

import { createStore } from 'vuex';
export type State = {
  count: number
}

export default createStore({
  state: {
    count: 0
  }
});
複製代碼

需要創建一個聲明文件vuex.d.ts

// vuex.d.ts
import {ComponentCustomProperties} from 'vue';
import {Store} from 'vuex';
import {State} from './store'
declare module '@vue/runtime-core' {
    interface ComponentCustomProperties {
        $store: Store<State>
    }
}
複製代碼

在 setup 中使用

  1. 定義InjecktionKey
  2. 在安裝插件時傳入key
  3. 在使用useStore時傳入
import { InjectionKey } from 'vue';
import { createStore, Store } from 'vuex';

export type State = {
  count: number
}
// 創建一個injectionKey
export const key: InjectionKey<Store<State>> = Symbol('key');
複製代碼
// main.ts
import store, { key } from './store';
app.use(store, key);
複製代碼
<script lang="ts">
import { useStore } from 'vuex';
import { key } from '@/store';
export default defineComponent({
  setup () {
    const store = useStore(key);
    const count = computed(() => store.state.count);
    return {
      count
    }
  }
})
</script>
複製代碼

模塊

新增一個todo模塊。導入的模塊,需要是一個vuex中的 interface Module的對象, 接收兩個泛型約束,第一個是該模塊類型,第二個是根模塊類型

// modules/todo.ts
import { Module } from 'vuex';
import { State } from '../index.ts';

type Todo = {
  id: number,
  name: string,
  completed: boolean
}

const initialState = {
  todos: [] as Todo[]
};

export type TodoState = typeof initialState;

export default {
  namespaced: true,
  state: initialState,
  mutations: {
    addTodo (state, payload: Todo) {
      state.todos.push(payload);
    }
  }
} as Module<TodoState, State>; //Module<S, R> S 該模塊類型 R根模塊類型
複製代碼
// index.ts
export type State = {
  count: number,
  todo?: TodoState // 這裏必須是可選,不然state會報錯
}

export default createStore({
  state: {
    count: 0
  }
  modules: {
    todo
  }
});
複製代碼

使用:

setup () {
  console.log(store.state.todo?.todos);
}
複製代碼

elementPlus

yarn add element-plus
複製代碼

完整引入

import { createApp } from 'vue'
import ElementPlus from 'element-plus';import 'element-plus/lib/theme-chalk/index.css';import App from './App.vue';
import 'dayjs/locale/zh-cn'
import locale from 'element-plus/lib/locale/lang/zh-cn'
const app = createApp(App)
app.use(ElementPlus, { size: 'small', zIndex: 3000, locale })
app.mount('#app')
複製代碼

按需加載

需要安裝babel-plugin-component插件:

yarn add babel-plugin-component -D

// babel.config.js
plugins: [
    [
      'component',
      {
        libraryName: 'element-plus',
        styleLibraryName: 'theme-chalk'
      }
    ]
]

複製代碼
import 'element-plus/lib/theme-chalk/index.css';
import 'dayjs/locale/zh-cn';
import locale from 'element-plus/lib/locale';
import lang from 'element-plus/lib/locale/lang/zh-cn';
import {
  ElAside,
  ElButton,
  ElButtonGroup,
} from 'element-plus';

const components: any[] = [
  ElAside,
  ElButton,
  ElButtonGroup,
];

const plugins:any[] = [
  ElLoading,
  ElMessage,
  ElMessageBox,
  ElNotification
];

const element = (app: any):any ={
  // 國際化
  locale.use(lang);
  // 全局配置
  app.config.globalProperties.$ELEMENT = { size: 'small' };
  
  components.forEach(component ={
    app.component(component.name, component);
  });

  plugins.forEach(plugin ={
    app.use(plugin);
  });
};

export default element;
複製代碼
// main.ts
import element from './plugin/elemment'

const app = createApp(App);
element(app);
複製代碼

axios

axios 的安裝使用和 vue2 上沒有什麼大的區別,如果需要做一些擴展屬性,還是需要聲明一個新的類型。

type Config = AxiosRequestConfig & {successNotice? : boolean, errorNotice? : boolean}
複製代碼
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios';
import { ElMessage } from 'element-plus';
const instance = axios.create({
  baseURL: process.env.VUE_APP_API_BASE_URL || '',
  timeout: 120 * 1000,
  withCredentials: true
});

// 錯誤處理
const err = (error) ={
  if (error.message.includes('timeout')) {
    ElMessage({
      message: '請求超時,請刷新網頁重試',
      type: 'error'
    });
  }
  if (error.response) {
    const data = error.response.data;
    if (error.response.status === 403) {
      ElMessage({
        message: 'Forbidden',
        type: 'error'
      });
    }
    if (error.response.status === 401) {
      ElMessage({
        message: 'Unauthorized',
        type: 'error'
      });
    }
  }
  return Promise.reject(error);
};

type Config = AxiosRequestConfig & {successNotice? : boolean, errorNotice? : boolean}

// 請求攔截
instance.interceptors.request.use((config: Config) ={
  config.headers['Access-Token'] = localStorage.getItem('token') || '';
  return config;
}, err);

// 響應攔截
instance.interceptors.response.use((response: AxiosResponse) ={
  const config: Config = response.config;

  const code = Number(response.data.status);
  if (code === 200) {
    if (config && config.successNotice) {
      ElMessage({
        message: response.data.msg,
        type: 'success'
      });
    }
    return response.data;
  } else {
    let errCode = [402, 403];
    if (errCode.includes(response.data.code)) {
      ElMessage({
        message: response.data.msg,
        type: 'warning'
      });
    }
  }
}, err);

export default instance;
複製代碼

setup script

官方提供了一個實驗性的寫法,直接在script裏面寫setup的內容,即:setup script

之前我們寫組件是這樣的:

<template>
  <div>
    {{count}}
    <ImgReview></ImgReview >
  </div>
</template>
<script lang="ts">
import { ref, defineComponent } from "vue";
import ImgReview from "./components/ImgReview.vue";

export default defineComponent({
  components: {
    ImgReview,
  },
  setup() {
    const count = ref(0);
    return { count };
  }
});
</script>
複製代碼

啓用setup script後:在script上加上setup

<template>
  <div>
    {{count}}
    <ImgReview></ImgReview>
  </div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import ImgReview from "./components/ImgReview.vue";
const count = ref(0);
</script>
複製代碼

是不是看起來簡潔了很多,組件直接導入就行了,不用註冊組件,數據定義了就可以用。其實我們可以簡單的理解爲script包括的內容就是setup中的,並做了return

導出方法

<script lang="ts" setup>
const handleClick = (type: string) ={
  console.log(type);
}
</script>
複製代碼

定義 props

使用props需要用到defineProps來定義,具體用法跟之前的props寫法類似:

基礎用法

<script lang="ts" setup>
import { defineProps } from "vue";
const props = defineProps(['userInfo''gameId']);
</script>
複製代碼

構造函數進行檢查 給 props 定義類型:

const props = defineProps({
  gameId: Number,
  userInfo: {
      type: Object,
      required: true
  }
});
複製代碼

使用類型註解進行檢查

defineProps<{
  name: string
  phoneNumber: number
  userInfo: object
  tags: string[]
}>()
複製代碼

可以先定義好類型:

interface UserInfo {
  id: number,
  name: string,
  age: number
}

defineProps<{
  name: string
  userInfo: UserInfo
}>()
複製代碼

defineEmit

<script lang="ts" setup>
import { defineEmit } from 'vue';

// expects emits options
const emit = defineEmit(['kk''up']);
const handleClick = () ={
  emit('kk''點了我');
};
</script>
複製代碼
<Comp @kk="handleClick"/>

<script lang="ts" setup>
const handleClick = (data) ={
  console.log(data)
}
</script>
複製代碼

獲取上下文

在標準組件寫法裏,setup 函數默認支持兩個入參:

dBGU0P

在 setup script 中使用 useContext 獲取上下文:

<script lang="ts" setup>
 import { useContext } from 'vue'
 const { slots, attrs } = useContext();
</script>
複製代碼

獲取到的slots,attrssetup裏面的是一樣的。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/rv1lXw4VwBAk9B8UUC34wA