从零到一,搭建V3+ts脚手架,封装axios与pinia持久化储存(学习自用)

174 阅读4分钟

目标

使用 create-vue 脚手架创建项目,搭建V3+ts脚手架,封装axios进行请求函数自适应,保证pinia本地持久化存储。

实现如下:

自行选择执行创建命令(本人使用的是pnpm)

pnpm create vue
# or
npm init vue@latest
# or
yarn create vue
​

选择项目依赖内容 这里可以自行选择,本项目主要是V3+TS+pinia

✔ Project name: … 项目名
✔ 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 项目名
  pnpm install
  pnpm lint
  pnpm dev
``

husky配置(不需要可以跳过)

官网cv初始化与安装:typicode.github.io/husky/getti…

pnpm dlx husky-init
pnpm install

修改 .husky/pre-commit 文件

#!/usr/bin/env sh
pnpm lint

用于合并代码时调整代码格式,会自动运行pnpm lint。

项目目录解构(了解)

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

路由相关代码解析(了解)

import {createRouter,createWebHistory} from 'vue-router'// createRouter 创建路由实例
// createWebHistory() 是开启history模块
// createWebHashHistory() 是开启hash模式// 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 route

设置用户状态(.d.ts)

准备用户信息的类型,types/user.d.ts

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

数据的持久化(Pinia)

使用 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(user.ts新增)

import type { User } from '@/types/user'
import { defineStore } from 'pinia'
import { ref } from 'vue'export default 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
    }
)

抽取 Pinia 代码(Pinia)

stores/index(自建)

import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'export * from './user'
// 创建 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 router from './router'
import pinia from './stores'const app = createApp(App)
​
app.use(pinia)
app.use(router)
app.mount('#app')

stores 统一导出(优雅)

// 写法 1
// import { useUserStore } from './user'
// export { useUserStore }// 写法 2
// export { useUserStore } from './user'// 写法 3
export * from './user'

App.vue中使用方法不变

import { useUserStore } from './stores'

移动端的适配(vw)

使用 vw 完成移动端适配

1.先安装

npm install postcss-px-to-viewport -D
# or
yarn add -D postcss-px-to-viewport
# or
pnpm add -D postcss-px-to-viewport

2.配置 postcss.config.js

// eslint-disable-next-line no-undef
module.exports = {
    plugins: {
        'postcss-px-to-viewport': {
            // 以设备宽度 375 为基准计算 vw 的值
            // 假如 100px 的 div
            // 375 宽度下,最终转换出的 vw 应该是:x / 100vw = 100px / 375px,x 等于 26.66vw
            // 而转换出来的 26.66vw 自然在不同的设备宽度下所表示的 div 宽度会不一样,例如 414 设备下
            // 26.66vw / 100vw = div 宽度 / 414px
            viewportWidth: 375,
        },
    },
};

请求实例封装

1.基本准备,utils/request.ts

import axios from 'axios'const instance = axios.create({
    // TODO 1. 基础地址,超时时间
})
​
instance.interceptors.request.use(
    (config) => {
        // TODO 2. 携带 token
        return config
    },
    (err) => Promise.reject(err)
)
​
instance.interceptors.response.use(
    (res) => {
        // TODO 3. 处理业务失败
        // TODO 4. 摘取核心响应数据
        return res
    },
    (err) => {
        // TODO 5. 处理 401 错误
        return Promise.reject(err)
    }
)
​
export default instance

2.基本封装

import axios from 'axios'
import { useUserStore } from '../stores'
import router from '../router'
import { showToast } from 'vant'const baseURL = '基地址'
const instance = axios.create({
  // TODO 1. 基础地址,超时时间
  baseURL,
  timeout: 10000
})
​
instance.interceptors.request.use(
  (config) => {
    // TODO 2. 携带 token
    const store = useUserStore()
    if (store.user?.token && config.headers) {
      config.headers['Authorization'] = `Bearer ${store.user?.token}`
    }
    return config
  },
  (err) => Promise.reject(err)
)
​
instance.interceptors.response.use(
  (res) => {
    // TODO 3. 处理业务失败
    if (res.data?.code !== 10000) {
      showToast(res.data?.message || '网络异常')
      return Promise.reject(res.data)
    }
    // TODO 4. 摘取核心响应数据
    return res.data
  },
  (err) => {
    // TODO 5. 处理 401 错误
    if (err.response.status === 401) {
      // 删除用户信息
      const store = useUserStore()
      store.delUser()
      // 跳转登录,带上接口失效所在页面的地址,登录完成后回跳使用
      router.push(`/login?returnUrl=${router.currentRoute.value.fullPath}`)
    }
    return Promise.reject(err)
  }
)
​
export { baseURL, instance }

请求函数封装

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

import axios, { type Method } from 'axios'const request = <T>(
  url: string,
  method: Method = 'get',
  submitData?: object
) => {
  return instance<T, { code: number; data: T; message: string }>({
    url,
    method,
    // data, params
    [method.toUpperCase() === 'GET' ? 'params' : 'data']: submitData
  })
}
​
export { baseURL, instance, request }