来吧,解锁 Vue 3 全家桶 + TS 的正确姿势

百家 作者:程序员的那些事 2021-10-29 16:15:49

推荐关注↓

创建项目

基础语法

定义data

  • script标签上lang="ts"
  • 定义一个类型type或者接口interface来约束data
  • 可以使用ref或者toRefs来定义响应式数据
  • 使用refsetup读取的时候需要获取xxx.value,但在template中不需要
  • 使用reactive时,可以用toRefs解构导出,在template就可以直接使用了
<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

  • createRouter创建router实例
  • router的模式分为:
  • createWebHistory -- history模式
  • createWebHashHistory -- hash模式
  • routes的约束类型是RouteRecordRaw
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 函数默认支持两个入参:

参数类型含义
propsobject由父组件传递下来的数据
contextobject组件的执行上下文

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

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

获取到的slots,attrssetup里面的是一样的。


作者:FinGet

https://juejin.cn/post/6980267119933931551

- EOF -

推荐阅读??点击标题可跳转

1、整理的一些 Vue3 知识点

2、令人眼前一亮的 Vue 实战技巧

3、从 Event Loop 角度解读 Vue NextTick 源码


关注「程序员的那些事」加星标,不错过圈内事

点赞和在看就是最大的支持?

关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

图库
公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接