vue3 + vant + ts + vuex + vite

vue3 + vant + ts + vuex + vite

1 项目搭建

1.1 基础

vue版本:Vue3

打包工具:Vite,需要 Node.js 版本 18+ 或 20+

UI:Vant

编程语言:TypeScript

全局状态管理工具:Vuex

CSS预处理语言:Less

开发工具推荐:vscode

vscode推荐拓展:Vue - Official、Vue 3 Snippets 等


1.2 搭建

通过命令行有两种创建方式,可参考 Vite,此处采用 vue-ts 模板。

npm create vite@latest my-vant-vuex-vite-pro -- --template vue-ts
npm init vite@latest my-vant-vuex-vite-pro -- --template vue-ts

到此,一个项目已经初步搭建完成,安装相关依赖 (如果一步步下来的,记得切换到项目根目录再安装)

npm install --registry=https://registry.npmmirror.***

 通过命令行已经可以启动项目了

npm run dev

Tips:在不同的项目中对于 node 可能用的版本不一样,可以安装 node管理工具:nvm

        此处不再具体讲解。


2 安装依赖

UI组件:vant

npm i vant

按需引入组件所需依赖:unplugin-vue-***ponents、unplugin-auto-import

npm i @vant/auto-import-resolver unplugin-vue-***ponents unplugin-auto-import -D

基于组合API封装好用的工具函数 :@vueuse/core

npm i @vueuse/core

为TypeScript项目提供Node.js的API的类型定义,从而在开发过程中提供代码补全和类型检查的支持:@types/node

npm i @types/node -D

路由管理工具:vue-router

npm i vue-router

CSS预处理语言:Less

npm install -D less

Tips:如遇到安装依赖不成功的可以通过添加淘宝域名的方式安装,不建议使用 ***pm

在安装命令行后边添加:--registry=https://registry.npmmirror.*** 如:

npm install vuex@next --registry=https://registry.npmmirror.***

3 项目配置

3.1 配置按需引入

在 vite.config.ts 中,我们引入以下依赖

import AutoImport from 'unplugin-auto-import/vite'
import ***ponents from 'unplugin-vue-***ponents/vite'
import { VantResolver } from '@vant/auto-import-resolver'

在 defineConfig 中 的 plugins 下,配置相关的自动导入

plugins: [
  vue(),
  AutoImport({
    // 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
    imports: ['vue', 'vue-router', '@vueuse/core'],
    // 自动导入 vant 相关函数 API,如:showToast
    resolvers: [VantResolver()],
    vueTemplate: true,
    // 配置文件生成位置(false:关闭自动生成)
    dts: false,
  }),
  ***ponents({
    // 自动导入 vant 组件
    resolvers: [VantResolver()],
    // 指定自定义组件位置(默认:src/***ponents)
    dirs: ['src/***ponents', 'src/**/***ponents'],
    // 配置文件位置 (false:关闭自动生成)
    dts: false,
  }),
],

完成以上两步,就可以直接在模板中使用 Vant 组件了,unplugin-vue-***ponents 会解析模板并自动注册对应的组件, @vant/auto-import-resolver 会自动引入对应的组件样式。 

接下来, 在 src/App.vue中,加入一个 vant 的组件,启动项目,按需引入配置完成。

Tips1:已经配置了按需引入,就不需要再 main.ts中在全局引入了,同时使用可能会导致代码重复、样式错乱等问题。


3.2 配置路由

src 文件夹下面,创建 router 文件夹用来配置路由,并在其下面创建 index.ts

src 文件夹下面,创建 views 文件夹存放页面,接着在 views文件夹下面创建个 dashboard 文件夹以及 dashboard 下面的 index.vue 作为 测试页面

重新进入到 router/index.ts 中,安装以下进行配置:

import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
// 静态路由
export const constantRoutes: RouteRecordRaw[] = [
  {
    path: '/',
    redirect: '/dashboard',
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    ***ponent: () => import('../views/dashboard/index.vue')
  },
]
/**
 * 创建路由
 */
const router = createRouter({
  history: createWebHashHistory(),
  routes: constantRoutes as RouteRecordRaw[],
  // 刷新时,滚动条位置还原
  scrollBehavior: () => ({ left: 0, top: 0 }),
})

export default router

此处配置的是 hash路由,url 带#号。如需配置 history路由,可使用  createWebHistory,需要服务端配合设置。

改写 main.ts

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
// 引入路由
import router from './router'

const app = createApp(App)
app.use(router)
app.mount('#app')

运行项目,发现页面地址变了,但是内容怎么没变,我们忘了重要的一点:没有 router-view 怎么能渲染路由呢。

在 App.vue 中,去除多余的内容,HTML 部分改写成

<!-- App.vue -->
<template>
  <router-view></router-view>
</template>

同样的方法,在 views 下再创建一个 home 页面,作为页面切换使用,同时在 router/index.ts 配置该页面的路由,接下来要实现 dashboard 和 home 页面的跳转与返回

<!-- dashboard.vue -->
<template>
  <div>
    <van-button type="primary" block @click="toHome">Dashboard</van-button>
  </div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
defineOptions({
  name: 'Dashboard',
  inheritAttrs: false
})
const router = useRouter()
// 跳转到 home 页面
function toHome () {
  router.push('/home')
}
</script>

同样的方法,在 home 页面 添加 back 方法,通过 router.back() 返回到 dashboard 页面。

Tips1:使用的是 setup 语法糖,不太熟悉的可移步 <script setup>

Tips2:注意 useRouter 和 useRoute 的区别,useRouter 得到的是动态路由,可通过 push、back等实现页面的跳转,useRoute 得到的是静态路由,可以获取路由的一些静态属性。

Tips3:有的可能已经注意到了在 router/indes.ts 中使用的是相对路径引入的页面模块,在 vue2 中已经习惯了使用 @/ 方式引入,在这直接使用,发现会报错,那么下一章节来介绍下使用 @/ 引入


 3.3 配置 @/ 方式引入

在 vite.config.ts 中 设置解析路径,无法 从 path中引入的,请查看下相关依赖是否已安装

@types/node 提供了相关的类型定义,参考 目录2 进行安装

设置 @ 引用的解析路径为 src,通过 @/* 引用 src 文件下的资源 

import { resolve } from 'path'

const pathSrc = resolve(__dirname, 'src')

// defineConfig 中添加 解析模块,与 plugins 同级

resolve: {
  alias: {
    '@': pathSrc,
  },
},

配置已经完成,去 router/index.ts里把路径改成 @ 形式,发现修改已经生效,但是 ts 依然报了类型检查的错误。

tsconfig.app.json 中 在 ***pilerOptions 下,加入以下代码解决报红问题

"baseUrl": ".",
"paths": {
  "@/*": ["src/*"]
},
"allowJs": true,

Tips:此处 加入 allowJs,是允许编译 js 文件,同样也可以加入 checkJs 来检查 js 文件


4 组件样式 

4.1 全局Less变量

在 src 下创建 variables.less 作为 Less变量的定义文件,如果在其他地方使用,就得引入这个文件,如何才能全局去使用,而不必每次都引入呢。

打开 vite.config.ts 文件下, defineConfig 下配置 CSS预处理器

css: {
  // CSS 预处理器
  preprocessorOptions: {
    // 定义全局 less 变量
    less: {
      javascriptEnabled: true,
      // 写法一
      additionalData: `
        @import '${resolve(__dirname, 'src/styles/variables.module.less')}';
      `,
      // 写法二
      // additionalData: `
      //   @import 'src/styles/variables.less';
      // `,
    },
  },
},

这样全局就可以使用 variables.module.less 下面定义的变量了,这里在文件扩展名前加上 .module 来结合使用 CSS modules 和预处理器可以让我们在 js 中直接获取定义的样式变量

以 variables.module.less,下面贴上定义好的样式

/* 定义样式变量 */
:root {
  --theme-color: #42a26a;
  --text-color: var(--van-white);
  --bg-color: #f2f2f2;
  --bg-padding: 10px;
}
:root[theme="dark"] {
  --theme-color: var(--van-black);
  --text-color: var(--van-white);
  --bg-color: var(--van-gray-8);
  --bg-padding: 10px;
}
/* 由于 vant 中的主题变量也是在 :root 下声明的,所以在有些情况下会由于优先级的问题无法成功覆盖。通过 :root:root 可以显式地让你所写内容的优先级更高一些,从而确保主题变量的成功覆盖。 */
:root:root {
  --van-nav-bar-background: var(--theme-color);
  --van-nav-bar-title-text-color: var(--text-color);
  --van-nav-bar-icon-color: var(--text-color);
  --van-nav-bar-height: 46px;
}
@themeColor: var(--theme-color);
@bgColor: var(--bg-color);
@bgPadding: var(--bg-padding);
:export {
  name: 'less';
  themeColor: #42a26a;
}

:export 可以导出我们要使用的变量

 Tips1:导入以 .module.css 为后缀名的 CSS 文件,则获取的是全部的定义的类名,注意这获取到的是样式名,css-loader 默认哈希算法会带有哈希值。 参考:CSS Modules

Tips2:在 variables.module.less 文件中,通过设置 :root[theme="dark"] 、:root[theme="light"] 等不同的样式,可作为主体切换方案:document.documentElement.setAttribute('theme', theme)


4.2 优化页面(仅做为参考)

目标:定义一个父级的页面总结构组件,页面统一使用该组件进行页面布局,可自定义标题、内容、和底部内容

因父级组件和页面自定义标题都会用到标题组件,抽离标题组件,好了,直接上代码

4.2.1 页面组件
<!--
 * ***ponents/Layout/index.vue
 * @Description: 页面结构组件
-->
<script setup lang="ts">
defineOptions({
  name: 'Layout',
  inheritAttrs: false
})
const props = defineProps({
  // body区域样式
  bodyStyle: {
    type: Object,
    default: {}
  }
})
</script>
<template>
  <div class="app-page">
    <slot name="header">
      <Header v-bind="$attrs"/>
    </slot>
    <div class="app-page-body" :style="props.bodyStyle">
      <slot></slot>
    </div>
    <slot name="footer"></slot>
  </div>
</template>
<style lang="less" scoped>
.app-page {
  position: relative;
  width: 100vw;
  height: 100vh;
  .app-page-body {
    position: absolute;
    left: 0;
    right: 0;
    top: var(--van-nav-bar-height);
    bottom: 0;
    overflow-y: scroll;
    padding: @bgPadding;
  }
}
</style>

Tips: 灵活使用 v-bind="$attrs" 可以把父组件所有未声明的 props 传递给子组件,注意只能传递属性,不能传递事件和指令。

4.2.2 标题组件

标题组件是以 vant 中的标题组件为基础,进行的二次封装

<!--
 * ***ponents/Header/index.vue
 * @Description: 标题栏
-->
<template >
  <van-nav-bar v-bind="props" @click-left="back"/>
</template>
<script setup lang="ts">
defineOptions({
  name: 'Header',
  inheritAttrs: false
})
const router = useRouter()
const props = defineProps({
  // 标题
  title: {
    type: String,
    default: ''
  },
  // 左侧文案
  leftText: {
    type: String,
    default: ''
  },
  // 右侧文案
  rightText: {
    type: String,
    default: ''
  },
  // v4.6.8, 是否禁用左侧按钮,禁用时透明度降低,且无法点击
  leftDisabled: {
    type: Boolean,
    default: false
  },
  // v4.6.8, 是否禁用右侧按钮,禁用时透明度降低,且无法点击
  rightDisabled: {
    type: Boolean,
    default: false
  },
  // 是否显示左侧箭头
  leftArrow: {
    type: Boolean,
    default: true
  },
  // 是否显示下边框
  border: {
    type: Boolean,
    default: false
  },
  // 是否固定在顶部
  fixed: {
    type: Boolean,
    default: true
  },
  // 固定在顶部时,是否在标签位置生成一个等高的占位元素
  placeholder: {
    type: Boolean,
    default: false
  },
  // 导航栏 z-index
  zIndex: {
    type: [Number, String],
    default: 1
  },
  // 是否开启顶部安全区适配
  safeAreaInsetTop: {
    type: Boolean,
    default: true
  },
  // 是否开启两侧按钮的点击反馈
  clickable: {
    type: Boolean,
    default: false
  },
})
function back () {
  router.back()
}
</script>
<style lang="less" scoped>
</style>

Tips:注意设置子组件的 inheritAttrs: false,避免父作用域的不被认作props的特性绑定应用在子组件的根元素上

4.3.3 全局引用组件

在 3.1 章节中已经配置了按需引入,在 src 下面的 ***ponents 下的组件会自动按需引入,如果设置了自动生成配置文件,会在根目录生成一个 ***ponents.d.ts,如果没有此文件,可手动添加一个

/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-***ponents
// Read more: https://github.***/vuejs/core/pull/3399
export {}

/* prettier-ignore */
declare module 'vue' {
  export interface Global***ponents {
    Header: typeof import('@/src/***ponents/Header/index.vue')['default']
    Layout: typeof import('@/src/***ponents/Layout/index.vue')['default']
  }
}
 4.3.4 组件使用
<template>
  <!-- 没有返回按键 -->
  <Layout title="首页" :left-arrow="false">
   <!-- body 内容 -->
  </Layout>
</template>
<template>
  <!-- 默认 -->
  <Layout title="个人中心" :left-arrow="false">
   <!-- body 内容 -->
  </Layout>
</template>
<template>
  <!-- 自定义标题 -->
  <Layout>
    <Header slot="header" title="自定义标题" @click="back"/>
  </Layout>
</template>

Tips:自定义标题,在需要改写返回按键、添加右侧按钮等特殊场景中经常用到,同理也可以通过 slot 自定义 footer 区域

4.3.5 页面过滤动画 

本章节的最后在附上一份我经常使用的页面切换,App.vue 中

<script setup lang="ts">
import type { RouteRecordNameGeneric } from 'vue-router'
import { useStore } from '@/store'
const router = useRouter()
const store = useStore()
// 本地存储key
const viewPageListName = ref('viewPageList')
// 本地存储
const viewPageList = reactive<RouteRecordNameGeneric[]>([])
// 切换动画
const slideTransition = ref('slide-left')
// 页面缓存
const cacheView = ***puted(() => store.state.view.cacheView)
// 监测路由
watch(
  () => router.currentRoute.value,
  (to, from) => {
    if (to.name === from?.name) return
    if (!viewPageList || viewPageList.length === 0) {
      let viewPageList = sessionStorage.getItem(viewPageListName.value)
      if (viewPageList) {
        viewPageList = JSON.parse(viewPageList)
      }
    }
    if (viewPageList.indexOf(to.name) === -1) {
      viewPageList.push(to.name)
    }
    if (viewPageList.indexOf(to.name) > viewPageList.indexOf(from?.name)) {
      slideTransition.value = 'slide-left'
    } else {
      slideTransition.value = 'slide-right'
    }
    if (viewPageList.indexOf(from?.name) === viewPageList.length - 1) {
      viewPageList.splice(viewPageList.indexOf(from?.name), 1)
    }
    sessionStorage.setItem(viewPageListName.value, JSON.stringify(viewPageList))
  },
  { immediate: true }
)
</script>

<template>
  <!-- <router-view> can no longer be used directly inside <transition> or <keep-alive>. -->
  <router-view v-slot="{ ***ponent }">
    <transition :name="slideTransition" mode="in-out">
      <keep-alive :include="cacheView">
        <***ponent :is="***ponent" />
      </keep-alive>
    </transition>
  </router-view>
</template>

参考:Transition | Vue.js
原理:把页面 name存放缓存中主要是为了区分页面的前进后退关系,好设置页面是前进还是后退动画,mode 设置为 in-out 模式,页面先进后出,配合 Header 的fixed定位,可以实现页面标题不动,只内容切换的效果。

Tips1:vue3中 router-view 标签不能再嵌套在 transition 和 keep-alive 内部了,需注意

Tips2:上文中涉及到了 cacheView 变量来做页面缓存的,需要搭配 vuex 使用,下一章节讲解,这部分报错可暂时去除。


5 全局状态管理工具 - vuex 

参考:TypeScript 支持 | Vuex

网上的例子很多,官方也给出了很明确的文档,以官方示例为基础,拆分出 modules 以便于模块化管理,以做页面缓存功能为例:

5.1 目录结构

src 目录下新建 store 目录,其目录结构如下

store/index.ts 作为主入口
store/modules/index.ts 作为模块化管理的主入口,统一往外输出

store/modules/view/index.ts 是本次要添加的页面缓存模块

以后如要加其他模块,则配置在 store/modules 下面

5.2 构建store

5.2.1 store/index.ts
// store/index.ts
import type { InjectionKey, App } from 'vue'
import { createStore, useStore as baseUseStore, Store } from 'vuex'
import modules from './modules'
// 此处声明中 count 是非必须的,如有需要,也可以设置其他的参数
export interface State {
  count: number,
  [x: string]: any // 解决使用 store.state.view.x 等报红线的问题
}

// InjectionKey 将store安装到Vue应用程序时提供类型,将类型传递InjectionKey给useStore方法
const key: InjectionKey<Store<State>> = Symbol()

const store = createStore<State>({
  modules
})

/**
 * 对外导出的方法,页面中使用 useStore 不在从 vuex 中引入,而是从 store 目录中,要是怕引起混 
 * 淆,也可以改用其他名字
 * @returns
 */
export function useStore() {
  return baseUseStore(key)
}
// 全局注册 store
export function setupStore(app: App) {
  app.use(store, key)
}
export default store
5.2.2 store/modules/index.ts 
import view from "./view"
// 对外统一的输出
export default { view }
5.2.3 store/modules/view/index.ts
/*
 * @Description: 处理页面缓存
 */
import type { RouteRecordNameGeneric } from 'vue-router'
import type { GetterTree, MutationTree, ActionTree } from 'vuex'
import type { State } from '@/store'

export interface ViewState {
  cacheView: RouteRecordNameGeneric[], // 存储的页面缓存变量,存的是路由中定义的页面的name
}

const state: ViewState = {
  cacheView: []
}

const getters: GetterTree<ViewState, State> = {}

const mutations: MutationTree<ViewState> = {
  ADD_CACHE_VIEW: (state: ViewState, view: RouteRecordNameGeneric) => {
    if (!state.cacheView.includes(view)) {
      state.cacheView.push(view)
    }
  },
  DELETE_CACHE_VIEW: (state: ViewState, view: RouteRecordNameGeneric) => {
    const index = state.cacheView.indexOf(view)
    if (index > -1) {
      state.cacheView.splice(index, 1)
    }
  }
}
const actions: ActionTree<ViewState, State> = {
  addCacheView ({ ***mit } , view: RouteRecordNameGeneric) {
    ***mit('ADD_CACHE_VIEW', view)
  },
  deleteCacheView ({ ***mit }, view: RouteRecordNameGeneric) {
    ***mit('DELETE_CACHE_VIEW', view)
  }
}
export default {
  namespace: true,
  state,
  getters,
  mutations,
  actions
}

跟 vue2 中一样可以定义 state、mutations、actions,只不过涉及到了 ts 对于类型的的检查比较严格,既然使用了 ts 就避免把所有的类型都设为 any,要不然还不如直接使用 js 版本的。

5.2.4 main.ts

在 store/indes.ts中 往外输出了一个构建 store的方法,在 main.ts 中引入并全局注册

import { createApp } from 'vue'
import './styles/index.less'
import App from './App.vue'
import router from './router'
import { setupStore } from './store'

const app = createApp(App)
app.use(router)
setupStore(app)
app.mount('#app')
5.2.5 使用

以 4.3.5 为例,通过与 ***puted 计算属性结合使用来获取到 store中存的值

import { useStore } from '@/store'
const store = useStore()
// 缓存的页面
const cacheView = ***puted(() => store.state.view.cacheView)

// 添加页面缓存
store.dispatch('addCacheView', 'Dashboard')

// 删除页面缓存
store.dispatch('deleteCacheView', 'Dashboard')

// 通过 ***mit 操作,此处仅做为示例
store.***mit('ADD_CACHE_VIEW', 'Dashboard')
store.***mit('DELETE_CACHE_VIEW', 'Dashboard')

Tips:特别注意此处引用的  useStore 是从 store中导出的那个,而不是 vuex 自带的,要是怕混淆,可以改成其他名字。


6 总结 

至此,一个简单的项目已经搭建完毕,另涉及到的 接口请求、本地代理等,后续有时间了会补充上,也可根据开发环境自行搭配。


2024.11.14 

源码已上传到GitHub,有需要的可以 点这里查看

转载请说明出处内容投诉
CSS教程网 » vue3 + vant + ts + vuex + vite

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买