Vue3+TS+Pinia+Vant-项目起步

433 阅读4分钟

创建项目

使用 create-vue 脚手架创建项目

pnpm create vue

选择步骤

Project name: … patients-h5
✔ Add TypeScript? … No / `Yes`Add JSX Support? … `No` / YesAdd Vue Router for Single Page Application development? … No / `Yes`Add Pinia for state management? … No / `Yes`Add Vitest for Unit Testing? … `No` / YesAdd Cypress for both Unit and End-to-End testing? … `No` / YesAdd ESLint for code quality? … No / `Yes`Add Prettier for code formatting? … No / `Yes`

Scaffolding project in /Users/zhousg/Desktop/patient-h5-100...

Done. Now run:

  cd patient-h5-100
  pnpm install
  pnpm lint
  pnpm dev

eslint配置

/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')

module.exports = {
  root: true,
  extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/eslint-config-typescript',
    '@vue/eslint-config-prettier/skip-formatting'
  ],
  parserOptions: {
    ecmaVersion: 'latest'
  },
  rules: {
    'prettier/prettier': [
      'warn',
      {
        singleQuote: true,
        semi: false,
        printWidth: 80,
        trailingComma: 'none',
        endOfLine: 'auto'
      }
    ],
    'vue/multi-word-component-names': [
      'warn',
      {
        ignores: ['index']
      }
    ],
    'vue/no-setup-props-destructure': ['off']
  }
}

创建路由

import { createRouter, createWebHistory } from 'vue-router'

// createRouter 创建路由实例,===> new VueRouter()
// history 是路由模式,hash模式,history模式
// createWebHistory() 是开启history模块   http://xxx/user
// createWebHashHistory() 是开启hash模式    http://xxx/#/user

// vite 的配置 import.meta.env.BASE_URL 是路由的基准地址,默认是 ’/‘
// https://vitejs.dev/guide/build.html#public-base-path
// 如果将来你部署的域名路径是:http://xxx/my-path/user
// vite.config.ts  添加配置  base: my-path,路由这就会加上 my-path 前缀了

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: []
})

export default router

安装vant组件库

pnpm add vant

按需引入组件样式

pnpm add unplugin-vue-components -D

在 vite.config.js 文件中配置插件:

import vue from '@vitejs/plugin-vue';
import Components from 'unplugin-vue-components/vite';
import { VantResolver } from 'unplugin-vue-components/resolvers';

export default {
  plugins: [
    vue(),
    Components({
      resolvers: [VantResolver()],
    }),
  ],
};

移动端适配

postcss-px-to-viewport 是一款 PostCSS 插件,用于将 px 单位转化为 vw/vh 单位。

pnpm add -D postcss-px-to-viewport

配置 postcss.config.js

// eslint-disable-next-line no-undef
module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      // 设备宽度375计算vw的值
      viewportWidth: 375,
    },
  },
};

css变量主题定制

styles/main.scss

// :root:可以在一个CSS文件中定义全局变量,供多个属性引用
// var:读取变量
:root {
  --cp-primary: #16C2A3;
  --cp-plain: #EAF8F6;
  --cp-orange: #FCA21C;
  --cp-text1: #121826;
  --cp-text2: #3C3E42;
  --cp-text3: #6F6F6F;
  --cp-tag: #848484;
  --cp-dark: #979797;
  --cp-tip: #C3C3C5;
  --cp-disable: #D9DBDE;
  --cp-line: #EDEDED;
  --cp-bg: #F6F7F9;
  --cp-price: #EB5757;
  // 覆盖vant的变量
  --van-primary-color: var(--cp-primary);
  // 单元格上下间距
  --van-cell-vertical-padding: 14px;
  // 复选框大小
  --van-checkbox-size: 14px;
  // 默认按钮文字大小
  --van-button-normal-font-size: 16px;
}

// 全局样式
body {
  font-size: 14px;
  color: var(--cp-text1);
}
a {
  color: var(--cp-text2);
}
h1,h2,h3,h4,h5,h6,p,ul,ol {
  margin: 0;
  padding: 0;
}

#nprogress .bar {
  background-color: var(--cp-primary) !important;
}

// 全局覆盖van-tab样式
.van-tabs {
  .van-tabs__nav {
    padding: 0 0 15px 0;
  }
  .van-tabs__line {
    width: 20px;
    background-color: var(--cp-primary);
  }
  .van-tab {
    padding: 0 15px;
  }
}

main.ts文件引入

// 全局样式
import '@/styles/main.scss'

安装pinia

// pinia-plugin-persistedstate插件可以实现pinia仓库状态持久化
pnpm add pinia pinia-plugin-persistedstate

main.ts

import persist from 'pinia-plugin-persistedstate'
const app = createApp(App)

app.use(createPinia().use(persist))

需要在defineStore的第三个参数添加persist:true

import type { User } from '@/types/user'
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useUserStore = defineStore(
  'user',
  () => {
    // 用户信息
    const user = ref<User>()
    // 设置用户,登录后使用
    const setUser = (u: User) => {
      user.value = u
    }
    // 清空用户,退出后使用
    const delUser = () => {
      user.value = undefined
    }
    return { user, setUser, delUser }
  },
  {
    persist: true
  }
)

测试 App.vue

<script setup lang="ts">
import { useUserStore } from './stores/user'

const store = useUserStore()
</script>

<template>
  <p>{{ store.user }}</p>
  <button @click="store.setUser({ id: '1', mobile: '1', account: '1', avatar: '1', token: '1' })">
    登录
  </button>
  <button @click="store.delUser()">退出</button>
</template>

image.png

stores统一导出

stores/index

import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'

// 创建pinia实例
const pinia = createPinia()
// 使用pinia插件
pinia.use(persist)
// 导出pinia实例,给main使用
export default pinia
import { createApp } from 'vue'
import App from './App.vue'
import pinia from './stores'
import router from './router'
import './styles/main.scss'

const app = createApp(App)

app.use(pinia)
app.use(router)
app.mount('#app')

stores/index

// 统一导出,代码简洁,入口唯一
export * from './modules/user'

App.vue

- import { useUserStore } from './stores/user'
+ import { useUserStore } from './stores'

安装axios

pnpm add axios

封装axios

import { useUserStore } from '@/stores'
import router from '@/router'
import axios, { type Method } from 'axios'
import { showFailToast } from 'vant'

type Data<T> = {
  code: number
  message: string
  data: T
}

const baseURL = 'https://consult-api.itheima.net/'
// 1. 新axios实例,基础配置
const instance = axios.create({
  baseURL,
  timeout: 10000
})

// 2. 请求拦截器,携带token
instance.interceptors.request.use(
  (config) => {
    const store = useUserStore()
    if (store.user?.token && config.headers) {
      config.headers['Authorization'] = `Bearer ${store.user?.token}`
    }
    return config
  },
  (err) => Promise.reject(err)
)

// 3. 响应拦截器,剥离无效数据,401拦截
instance.interceptors.response.use(
  (res) => {
    // 后台约定,响应成功,但是code不是10000,是业务逻辑失败
    if (res.data?.code !== 10000) {
      showFailToast(res.data?.message || '业务失败')
      return Promise.reject(res.data)
    }
    // 业务逻辑成功,返回响应数据,作为axios成功的结果
    return res.data
  },
  (err) => {
    if (err.response.status === 401) {
      // 删除用户信息
      const store = useUserStore()
      store.delUser()
      // 跳转登录,带上接口失效所在页面的地址,登录完成后回跳使用
      router.push({
        path: '/login',
        query: { returnUrl: router.currentRoute.value.fullPath }
      })
    }
    return Promise.reject(err)
  }
)

// 这个需要替换axsio.request默认的响应成功后的结果类型
// 之前是:传 { name: string } 然后res是 res = { data: { name: string } }
// 但现在:在响应拦截器中返回了 res.data 也就是将来响应成功后的结果,和上面的类型一致吗?
// 所以要:request<数据类型,数据类型>() 这样才指定了 res.data 的类型 
const request = <T>(
  url: string,
  method: Method = 'get',
  submitData?: object
) => {
  return instance.request<T, Data<T>>({
    url,
    method,
    [method.toLowerCase() === 'get' ? 'params' : 'data']: submitData
  })
}

export { baseURL, request }