Vue3+ts+pinia+Vant+pnpm项目起步

pnpm安装

npm i pnpm -g

项目创建

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

  1. 执行创建命令
    pnpm create vue
    # or
    npm init vue@latest
    # or
    yarn create vue
    
  2. 选择项目依赖内容
    ✔ Project name: … patients-h5-100
    ✔ Add TypeScript? … No / `Yes`
    ✔ Add JSX Support? … `No` / Yes
    ✔ Add Vue Router for Single Page Application development? … No / `Yes`
    ✔ Add Pinia for state management? … No / `Yes`
    ✔ Add Vitest for Unit Testing? … `No` / Yes
    ✔ Add Cypress for both Unit and End-to-End testing? … `No` / Yes
    ✔ Add 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
    

 vscode插件安装

必装:

  • Vue Language Features (Volar) vue3语法支持
  • TypeScript Vue Plugin (Volar) vue3中更好的ts提示
  • Eslint 代码风格校验

可选:

  • gitLens 代码git提交记录提示
  • json2ts json自动转ts类型
  • Error Lens 行内错误提示

 eslint 预制配置  .eslintrc.cjs

  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'],
    // 💡 添加未定义变量错误提示,create-vue@3.6.3 关闭,这里加上是为了支持下一个章节演示。
    'no-undef': 'error'
  }
  • 格式:单引号,没有分号,行宽度80字符,没有对象数组最后一个逗号,换行字符串自动(系统不一样换行符号不一样)
  • vue 组件需要大驼峰命名,除去 index 之外,App 是默认支持的
  • 允许对 props 进行解构,我们会开启解构保持响应式的语法糖

执行:

# 修复格式
pnpm lint

 vscode 开启 eslint 自动修复:

    "editor.codeActionsOnSave": {
        "source.fixAll": true,
    },

代码检查工作流

husky 配置
  • 初始化与安装
    pnpm dlx husky-init && pnpm install
    
  • 修改 .husky/pre-commit 文件
    pnpm lint
    
lint-staged 配置 
  • 安装
    pnpm i lint-staged -D
    
  • 配置 package.json
    {
      // ... 省略 ...
      "lint-staged": {
        "*.{js,ts,vue}": [
          "eslint --fix"
        ]
      }
    }
    
    {
      "scripts": {
        // ... 省略 ...
        "lint-staged": "lint-staged"
      }
    }
    
  • 修改 .husky/pre-commit 文件
    pnpm lint-staged
    

项目结构调整

./src
├── assets        `静态资源,图片...`
├── components    `通用组件`
├── composable    `组合功能通用函数`
├── icons         `svg图标`
├── router        `路由`
│   └── index.ts
├── services      `接口服务API`
├── stores        `状态仓库`
├── styles        `样式`
│   └── main.scss
├── types         `TS类型`
├── utils         `工具函数`
├── views         `页面`
├── main.ts       `入口文件`
└──App.vue       `根组件`

项目使用sass预处理器,安装sass,即可支持scss语法:

pnpm add sass -D

路由代码解析

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组件库

安装:

# Vue 3 项目,安装最新版 Vant
npm i vant
# 通过 yarn 安装
yarn add vant
# 通过 pnpm 安装
pnpm add vant

样式:main.ts

import { createApp } from 'vue'
import App from './App.vue'
import pinia from './stores'
import router from './router'
// 样式全局使用
import 'vant/lib/index.css'
import './styles/main.scss'

const app = createApp(App)

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

组件按需使用:App.vue

<script setup lang="ts">
import { Button as VanButton } from 'vant'
</script>

<template>
  <van-button>按钮</van-button>
</template>

<style scoped></style>

移动端适配

安装:

npm install postcss-px-to-viewport -D
# or
yarn add -D postcss-px-to-viewport
# or
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,
    },
  },
};

有一个控制台警告可忽略,或者使用 postcss-px-to-viewport-8-plugin 代替当前插件

css变量主题定制

  • 如何定义 css 变量使用 css 变量
    :root {
      --main: #999;
    }
    a {
      color: var(--main)
    }
    
  • 定义项目的颜色风格,覆盖vant的主题色 官方文档   styles/main.scss 
    :root {
      --main-primary: #16C2A3;
      --main-plain: #EAF8F6;
      --main-orange: #FCA21C;
      --main-text1: #121826;
      --main-text2: #3C3E42;
      --main-text3: #6F6F6F;
      --main-tag: #848484;
      --main-dark: #979797;
      --main-tip: #C3C3C5;
      --main-disable: #D9DBDE;
      --main-line: #EDEDED;
      --main-bg: #F6F7F9;
      --main-price: #EB5757;
      // 覆盖vant主体色
      --van-primary-color: var(--main-primary);
    }
    
    App.vue
    <script setup lang="ts"></script>
    
    <template>
      <!-- 验证vant颜色被覆盖 -->
      <van-button type="primary">按钮</van-button>
      <a href="#">123</a>
    </template>
    
    <style scoped lang="scss">
    // 使用 css 变量
    a {
      color: var(--main-primary);
    }
    </style>
    

用户状态仓库

  • 请求工具需要携带token,访问权限控制需要token,所以用户信息仓库先完成

需求:

  • 用户信息仓库创建
  • 提供用户信息
  • 修改用信息的方法
  • 删除用信息的方法

代码:

types/user.d.ts

// 用户信息
export type User = {
  /** token令牌 */
  token: string
  /** 用户ID */
  id: string
  /** 用户名称 */
  account: string
  /** 手机号 */
  mobile: string
  /** 头像 */
  avatar: string
}

 stores/user.ts

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

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

 数据持久化

使用 pinia-plugin-persistedstate 实现pinia仓库状态持久化 参考文档

  • 安装
    pnpm i pinia-plugin-persistedstate
    # or
    npm i pinia-plugin-persistedstate
    # or
    yarn add pinia-plugin-persistedstate
    
  • 使用 main.ts
    import persist from 'pinia-plugin-persistedstate'
    const app = createApp(App)
    
    app.use(createPinia().use(persist))
    
  • 配置 stores/user.ts
    import type { User } from '@/types/user'
    import { defineStore } from 'pinia'
    import { ref } from 'vue'
    
    export const useUserStore = defineStore(
      'cp-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>
    

stores统一导出 

仓库的导出统一从 ./stores 代码简洁,职能单一,入口唯一

即一个模块下的所有资源通过index导出

  • 抽取pinia实例代码,职能单一   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
    
    main.ts 
    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'
    

请求工具函数

拦截器逻辑 

token请求头携带,错误响应处理,401错误处理

utils/request.ts

import { useUserStore } from '@/stores'
import router from '@/router'
import axios from 'axios'
import { showToast } from 'vant'

// 1. 新axios实例,基础配置
const instance = axios.create({
  baseURL: 'https://xxx.com/',
  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) {
      showToast(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)
  }
)

export { baseURL, instance }

 工具函数封装

导出一个通用的请求工具函数,支持设置响应数据类型

  • 导出一个通用的请求工具函数
    import axios, { AxiosError, type Method } from 'axios'
    
    // 4. 请求工具函数
    const request = (url: string, method: Method = 'GET', submitData?: object) => {
      return instance.request({
        url,
        method,
        [method.toUpperCase() === 'GET' ? 'params' : 'data']: submitData
      })
    }
    
  • 支持不同接口设不同的响应数据的类型,加上泛型
    // 这个需要替换axsio.request默认的响应成功后的结果类型
    // 之前是:传 { name: string } 然后res是   res = { data: { name: string } }
    // 但现在:在响应拦截器中返回了 res.data  也就是将来响应成功后的结果,和上面的类型一致吗?
    // 所以要:request<数据类型,数据类型>() 这样才指定了 res.data 的类型
    // 但是呢:后台返回的数据结构相同,所以可以抽取相同的类型
    type Data<T> = {
      code: number
      message: string
      data: T
    }
    // 4. 请求工具函数
    const request = <T>(url: string, method: Method = 'get', submitData?: object) => {
      return instance.request<T, Data<T>>({
        url,
        method,
        [method.toLowerCase() === 'get' ? 'params' : 'data']: submitData
      })
    }
    

自动按需加载 

实现自动按需加载,和自动导入  官方文档

手动按需使用组件比较麻烦,需要先导入。配置函数自动按需导入后直接使用即可

  • 安装:
    # 通过 npm 安装
    npm i unplugin-vue-components -D
    # 通过 yarn 安装
    yarn add unplugin-vue-components -D
    # 通过 pnpm 安装
    pnpm add unplugin-vue-components -D
    
  • 配置:
    import { fileURLToPath, URL } from 'node:url'
    
    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import Components from 'unplugin-vue-components/vite'
    import { VantResolver } from 'unplugin-vue-components/resolvers'
    
    // https://vitejs.dev/config/
    export default defineConfig({
      plugins: [
        // 解析单文件组件的插件
        vue(),
        // 自动导入的插件,解析器可以是 vant element and-vue 
        Components({
          dts: false,
          // 原因:Toast Confirm 这类组件的样式还是需要单独引入,样式全局引入了,关闭自动引入
          resolvers: [VantResolver({ importStyle: false })]
        })
      ],
      resolve: {
        alias: {
          '@': fileURLToPath(new URL('./src', import.meta.url))
        }
      }
    })
    
    

图标组件-打包svg地图

根据 icons 文件svg图片打包到项目中,通过组件使用图标  参考文档

  • 安装插件
    yarn add vite-plugin-svg-icons -D
    # or
    npm i vite-plugin-svg-icons -D
    # or
    pnpm install vite-plugin-svg-icons -D
    
  • 使用插件  vite.config.ts
    import { VantResolver } from 'unplugin-vue-components/resolvers'
    +import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
    +import path from 'path'
    
    // https://vitejs.dev/config/
    export default defineConfig({
      plugins: [
        vue(),
        Components({
          dts: false,
          resolvers: [VantResolver({ importStyle: false })]
        }),
    +    createSvgIconsPlugin({
    +      // 指定图标文件夹,绝对路径(NODE代码)
    +      iconDirs: [path.resolve(process.cwd(), 'src/icons')]
    +    })
      ],
    
  • 导入到main
    import router from './router'
    +import 'virtual:svg-icons-register'
    
    import 'vant/lib/index.css'
    
  • 使用svg精灵地图
        <svg aria-hidden="true">
          <!-- #icon-文件夹名称-图片名称 -->
          <use href="#icon-login-eye-off" />
        </svg>
    

图标组件-封装svg组件 

  • 组件 components/SvgIcon.vue
    <script setup lang="ts">
    // 提供name属性即可
    defineProps<{
      name: string
    }>()
    </script>
    
    <template>
      <svg aria-hidden="true" class="svg-icon">
        <use :href="`#icon-${name}`" />
      </svg>
    </template>
    
    <style lang="scss" scoped>
    .svg-icon {
      // 和字体一样大
      width: 1em;
      height: 1em;
    }
    </style>
    
  • 类型 types/components.d.ts
    import SvgIcon from '@/components/SvgIcon.vue'
    
    declare module 'vue' {
      interface GlobalComponents {
        SvgIcon: typeof SvgIcon
      }
    }
    

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值