阅读 23313
Vue3+TS,写一个逼格满满的项目

Vue3+TS,写一个逼格满满的项目

Vue3和TS的概念学了那么多,有没有心动手痒想实践一下呢?

本文将介绍如何使用Vue3+TS写一个基础项目,有了主要框架概念之后,后续对于应用的开发能更加地得心应手。

1.版本升级

大家之前在做Vue开发时想必都是2.x版本,在这个项目之前要先检查Vue处于哪个版本,本次示例需要4.5.3版本及以上:

vue --version
//@vue/cli 4.5.3
复制代码

版本升级之后即可创建项目,使用的命令还同之前一样:

vue create project-name
复制代码

下面展示的是创建示例项目过程中各个步骤的选择内容:

Vue CLI v4.5.8
? Please pick a preset: (Use arrow keys)
  Default ([Vue 2] babel, eslint)		//默认,vue2版本
  Default (Vue 3 Preview) ([Vue 3] babel, eslint)	//默认,vue3版本
> Manually select features  		//手动选择配置
复制代码
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)
>(*) Choose Vue version		//选择vue版本
 (*) Babel
 (*) TypeScript
 ( ) Progressive Web App (PWA) Support
 (*) Router
 (*) Vuex
 (*) CSS Pre-processors
 ( ) Linter / Formatter
 ( ) Unit Testing
 ( ) E2E Testing    
复制代码
? Choose a version of Vue.js that you want to start the project with (Use arrow keys)
  2.x
> 3.x (Preview)  
复制代码
? Use class-style component syntax? (y/N)  N
//是否使用类样式组件
复制代码
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? (Y/n) N
//不需要babel去配合TS
复制代码
? Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n) Y 
//使用路由的历史模式
复制代码
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): (Use arrow keys)
> Sass/SCSS (with dart-sass)
  Sass/SCSS (with node-sass)
  Less
  Stylus 
//选择CSS预处理器
复制代码
? Where do you prefer placing config for Babel, ESLint, etc.?
> In dedicated config files   生成独立的配置文件
  In package.json 
复制代码
? Save this as a preset for future projects? (y/N) N 
//保存这个配置以备将来使用
复制代码

项目创建完成之后,为了配合TS以及异步请求的使用,还需要自己增加几个文件,相应的目录结构展示如下:

─public
├─server
└─src
    ├─api
    │  └─index.ts
    │  └─home.ts
    ├─assets
    ├─components
    ├─router
    │  └─index.ts
    ├─store
    │  └─modules
    │  	  └─home.ts
    │  └─action-types.ts
    │  └─index.ts
    ├─typings
    │  └─home.ts
    │  └─index.ts
    └─views
        ├─cart
        ├─home
        │  └─index.vue
        │  └─homeHeader.vue
        │  └─homeSwiper.vue
        └─mine
复制代码

在src目录下有个shims-vue.d.ts文件,它是一个垫片文件,用于声明**.vue文件**是这样的一个组件:

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}
复制代码

1.1引入Vant

import Vant from 'vant'
import 'vant/lib/index.css'

createApp(App).use(store).use(router).use(Vant).mount('#app')
复制代码

3.x版本开始使用函数式编程,因此可以使用链式调用。

1.2引入axios

对请求进行简单封装,axios/index.ts:

import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'

axios.defaults.baseURL = 'http://localhost:3001/'

axios.interceptors.request.use((config:AxiosRequestConfig) => {
    return config;
})

axios.interceptors.response.use((response:AxiosResponse) => {
    if(response.data.err == 1){
        return Promise.reject(response.data.data);
    }
    return response.data.data;
},err => {
    return Promise.reject(err);
})

export default axios
复制代码

2.定义路由

router/index.ts中:

const routes: Array<RouteRecordRaw> = [];
复制代码

规定了数组元素类型是RouteRecordRaw,它可以在定义路由时进行友善地提示。

其他路由的处理,与之前没有太大差异:

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../views/home/index.vue')
  },
  {
    path: '/cart',
    name: 'Cart',
    component: () => import('../views/cart/index.vue')
  },{
    path: '/mine',
    name: 'Mine',
    component: () => import('../views/mine/index.vue')
  }
]
const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})
export default router
复制代码

定义完路由后,就可以在App.vue中配置底部导航:

<van-tabbar route>
      <van-tabbar-item to="/" icon="home-o">首页</van-tabbar-item>
      <van-tabbar-item to="/cart" icon="shopping-cart-o">购物车</van-tabbar-item>
      <van-tabbar-item to="/mine" icon="friends-o">我的</van-tabbar-item>
</van-tabbar>
复制代码

3.定义数据结构

基本路由定义好后,下面开始写数据结构;一般的开发思路是先定义好所需的数据结构之后,使用时才会更方便,思路也会清晰。

3.1.声明类别

在typings/home.ts中添加如下:

CATEGORY_TYPES有5个值:全部,鞋子,袜子,衬衫,裤子

export enum CATEGORY_TYPES {
    ALL,
    SHOES,
    SOCKES,
    SHIRT,
    PANTS
}
复制代码

枚举类型的值默认从0开始,自动升序;也可手动指定第一个枚举类型的值,而后在此基础上自动加一。

然后声明home文件的接口IHomeState,规定包含当前分类属性,并且必须是CATEGORY_TYPES类型:

export interface IHomeState {
    currentCategory: CATEGORY_TYPES
}
复制代码

在store/moudles/home.ts中声明home的状态:

const state:IHomeState = {
    currentCategory: CATEGORY_TYPES.ALL,
};
复制代码
//action-types.ts中添加状态名称
export const SET_CATEGORY = 'SET_CATEGORY'
复制代码
const home:Module<IHomeState,IGlobalState> = {
    namespaced: true,
    state,
    mutations: {
        [Types.SET_CATEGORY](state,payload:CATEGORY_TYPES){
            state.currentCategory = payload;
        }
    },
    actions: {}
}

export default home;
复制代码

这里定义的home属于Vuex中的Module类型,需要传递两个泛型S和R,分别是当前模块的状态和根状态。当前状态即IHomeState,根状态则需要在index.ts中声明全局类型接口:

export interface IGlobalState{
  home: IHomeState,
      //这里后续增加其他状态模块
}
复制代码

后续将在这里管理所有状态,既用于使用时的代码提示,也用于全局状态管理。

3.2状态操作

状态添加完毕后,到home/index.vue中进行状态的操作:

<template>
<HomeHeader :category="category" @setCurrentCategory="setCurrentCategory"></HomeHeader>
</template>
<script lang="ts">
function useCategory(store: Store < IGlobalState > ) {
    let category = computed(() => {
        return store.state.home.currentCategory
    })   
    function setCurrentCategory(category: CATEGORY_TYPES) {
        store.commit(`home/${Types.SET_CATEGORY}`, category)
    }
    return {
        category,
        setCurrentCategory
    }
}
export default defineComponent({
    components: {
        HomeHeader,
        HomeSwiper,
    },
    setup() {
        let store = useStore < IGlobalState > ();
        let {
            category,
            setCurrentCategory
        } = useCategory(store);
        return {
          category,
          setCurrentCategory
        }
    }
})    
</script>    
复制代码

为了获得传递给 setup() 参数的类型推断,需要使用 defineComponent。

这里定义了一个useCategory方法,专门用来处理切换状态。方法中使用computed获取currentCategory值;如果不用computed,那么这里就是一个死值,只是取到值放在这里;只有使用计算属性才能保证状态变了,计算的新值也变了,并响应到视图中去。

computed具有缓存属性,只有当依赖的值发生变化时,才会重新计算一次wathcer.value

setCurrentCategory作为子组件的发射事件,用于调用状态管理修改currentCategory,这里再回到homeHeader组件中:

<template>
<div class="header">
    //....
    <van-dropdown-menu class="menu">
        <van-dropdown-item :modelValue="category" :options="option" @change="change"/>
    </van-dropdown-menu>
</div>
</template>
<script lang="ts">
export default defineComponent({
    props: {
        category: {
            type: Number as PropType<CATEGORY_TYPES>
        }
    },
    emits: ['setCurrentCategory'],   
    setup(props,context){
        let state = reactive({
            option: [
                {text: '全部',value: CATEGORY_TYPES.ALL},
                {text: '鞋子',value: CATEGORY_TYPES.SHOES},
                {text: '袜子',value: CATEGORY_TYPES.SOCKES},
                {text: '衬衫',value: CATEGORY_TYPES.SHIRT},
                {text: '裤子',value: CATEGORY_TYPES.PANTS},
            ]
        })
        function change(value: CATEGORY_TYPES){
            context.emit('setCurrentCategory',value) 
        }
        return {
            //ref用来处理简单类型
            ...toRefs(state),
            change
        }
    }
})    
</script>    
复制代码

setup方法此时接收了两个参数:props和context,其中props对象是响应式的,注意不要结构props对象,这样会令它失去响应性;context是一个上下文对象,类似2.x中的this属性,并选择性地暴露了一些property。

首先仍是props接收父组件的属性传值,但是这里需要注意的是在type声明时,使用as断言PropType为CATEGORY_TYPES类型;

其次,使用reactive将数据处理为响应式对象,使用toRefs将一个响应式对象转换成普通对象,在 setup 中返回的 ref 在模板中会自动解开,不需要写 .value

emits用于注册数组发射事件的方法名,在真正要调用的时候可以使用代码提示的便捷形式提交;

modelValue目前属于一个内部编译的绑定属性方式,这里用于value的值绑定,具体编译可看参考文档[2]。

到这一步,就可以实现父子组件通信,并且通过选择当前种类,改变页面状态值。

3.3异步数据获取

接下来趁热打铁,实现动态获取轮播数据:

首页注册homeSwiper组件,这里不再表述,直接看组件内部逻辑

<template>
    <van-swipe v-if="sliderList.length" class="my-swipe" :autoplay="3000" indicator-color="white">
        <van-swipe-item v-for="l in sliderList" :key="l.url">
            <img class="banner" :src="l.url" alt="">
        </van-swipe-item>
    </van-swipe>
</template>
<script lang="ts">
export default defineComponent({
    async setup(){
        let store = useStore<IGlobalState>();
        let sliderList = computed(() => store.state.home.sliders);
        if(sliderList.value.length == 0){     
            await store.dispatch(`home/${Types.SET_SLIDER_LIST}`);  
        }   
        return {
            sliderList
        }
    }
})
</script>
复制代码

这里不建议将async写到setup处,但是为了演示先放在这里。

useStore直接使用vuex暴露出来的方法,相较于之前都绑定在this上更加方便。

同上一个组件,这里也利用computed获取要轮播的图片数据,当数据为空时则发起异步dispatch。

首先定义数据类型typings/home.ts:

export interface ISlider{
    url:string
}
export interface IHomeState {
    currentCategory: CATEGORY_TYPES,
    sliders: ISlider[]
}
复制代码

增加状态操作名称action-types.ts:

export const SET_SLIDER_LIST = 'SET_SLIDER_LIST'
复制代码

在原基础上增加slider类型,定义home状态中的sliders为ISlider类型数组。

然后到modules/home.ts中增加状态操作:

const state:IHomeState = {
    currentCategory: CATEGORY_TYPES.ALL,
    sliders: []
}
const home:Module<IHomeState,IGlobalState> = {
    namespaced: true,
    state,
    mutations: {
        [Types.SET_CATEGORY](state,payload:CATEGORY_TYPES){
            state.currentCategory = payload;
        },
        [Types.SET_SLIDER_LIST](state,payload:ISlider[]){
            state.sliders = payload;
        }
    },
    actions: {
        async [Types.SET_SLIDER_LIST]({commit}){
            let sliders = await getSliders<ISlider>();
            commit(Types.SET_SLIDER_LIST,sliders)
        }
    }
}
复制代码

在action中异步获取sliders数据,然后提交到Types.SET_SLIDER_LIST中请求接口获取数据,然后通过提交数据状态,使依赖的数据发生改变,通过computed获取的值将重新获取,这时轮播图将显示在页面中。

3.4 小节

在以上过程中,都没有带入import的内容代码,如果你正确使用了TS这时就能发现,通过代码提示的功能,import都能自动导入所需内容,这也是将来TS的一大特点。不仅具备类型校验,更能提升开发过程体验。

4.组件插槽

<Suspense>
	<template #default>
		<HomeSwiper></HomeSwiper>
	</template>
	<template #fallback>
		<div>loading...</div>
	</template>
</Suspense>
复制代码

这里展示的是在Vue3中提供了一种组件异步渲染插槽的使用方式:在Suspense组件中,当获取数据后默认执行default部分的内容,为获取数据时执行fallback部分的内容,在异步处理上更加简单。

但是,细心的你将会发现控制台输出了这么一段话:

is an experimental feature and its API will likely change.

这是一个实验性语法,将来的API是否能继续保留也未可知,这也为Vue3的发布增加了更多的期待。

5.参考文档

1.Vue 组合式 API

2.Vue 3 Template Exploer

文章分类
前端
文章标签