Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Step-by-Step Setup Guide for Vite + Vue 3 + TypeScript + Pinia + Vant Project

Tech 1

pnpm Overview & Installation

pnpm is a fast, efficient package manager similar to npm and Yarn, with key advantages:

  • Blazing-fast package installation
  • Optimized disk space utilization

Install via npm:

npm install -g pnpm

Common command equivalences between npm and pnpm:

npm Command pnpm Equivalent
npm install pnpm install
npm i axios pnpm add axios
npm i webpack --save-dev pnpm add webpack --save-dev
npm run dev pnpm dev

1. Project Creation

Use the official create-vue scaffold to bootstrap a Vite-powered Vue 3 project with TypeScript support.

Run the creation command:

pnpm create vue@latest

Follow the interactive prompts:

? Project name: … patient-h5-app
? 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

After setup completes, run these commands to start:

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

2. Project Preparation

Required VS Code Extensions

Install these extensions for optimal development:

  1. Vue - Official: Official Vue language tool (replaces the legacy Volar plugin)
  2. ESLint: Code quality linting
  3. Prettier: Opinionated code formatter

Optional Extensions:

  • gitLens: Git commit history insights
  • json2ts: Auto-generate TypeScript types from JSON
  • Error Lens: Inline error and warning displays

Configure ESLint & Prettier

For ESLint 9, we'll use the default configuration for now (more details to come).

Prettier Setup

Create a .prettierrc.json config file in your project root:

{
  "printWidth": 100,
  "tabWidth": 2,
  "useTabs": false,
  "semi": true,
  "singleQuote": false,
  "trailingComma": "all"
}

Create a .prettierignore file to exclude files from formatting:

node_modules
dist
public
*.min.js

Add a formatting script to package.json:

{
  "scripts": {
    "lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,css,scss,vue,html,md}\""
  }
}

Enable auto-format-on-save in VS Code:

  1. Open File > Preferences > Settings
  2. Select Open Settings (JSON) and add these configurations:
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode"
}

Project Structure Restructuring

Standardize your project's source code structure:

./src
├── assets        # Static assets like images
├── components    # Reusable global components
├── composables   # Shared Vue composition functions
├── icons         # SVG icon assets
├── router        # Routing configuration
│   └── index.ts
├── services      # API request functions
├── stores        # Pinia state management stores
├── styles        # Global styles
│   └── main.scss
├── types         # TypeScript type definitions
├── utils         # Shared utility functions
├── views         # Page-level components
├── main.ts       # Application entry point
└── App.vue       # Root application component

Clean up default generated files:

  1. Empty the assets, components, stores, and views directories
  2. Create new directories: composables, icons, services, styles, types, utils
  3. Update core files: router/index.ts, main.ts, and App.vue

Updated Router Configuration (router/index.ts)

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

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

export default router

Updated Application Entry (main.ts)

import { createApp } from 'vue'
import { createPinia } from 'pinia'

import App from './App.vue'
import router from './router'
import './styles/main.scss'

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.mount('#app')

Updated Root Component (App.vue)

<script setup lang="ts"></script>

<template>
  <div>App</div>
</template>

<style scoped></style>

Install Sass for SCSS syntax support:

pnpm add sass --save-dev

Import the global stylesheet in main.ts:

import './styles/main.scss'

3. Mobile Project Foundation

(Content to be continued)


4. UI Building with Vant

Install Vant

For Vue 3 projects, install the latest Vant version:

# Using pnpm
pnpm add vant

Auto-import Components

Use unplugin-vue-components and Vant's official resolver to automatically import components and their styles:

  1. Install dependencies:
pnpm add @vant/auto-import-resolver unplugin-vue-components --save-dev
  1. Configure Vite (vite.config.ts):
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { VantResolver } from '@vant/auto-import-resolver'

export default {
  plugins: [
    vue(),
    Components({
      resolvers: [VantResolver()]
    })
  ]
}
  1. Use components in your templates:
<template>
  <van-button type="primary" />
</template>

Import Function Component Styles

Some Vant components are used as functions (Toast, Dialog, Notify, ImagePreview). You'll need to manually import their styles:

// Toast
import { showToast } from 'vant'
import 'vant/es/toast/style'

// Dialog
import { showDialog } from 'vant'
import 'vant/es/dialog/style'

// Notify
import { showNotify } from 'vant'
import 'vant/es/notify/style'

// ImagePreview
import { showImagePreview } from 'vant'
import 'vant/es/image-preview/style'

Mobile Viewport Adaptation

Use postcss-px-to-viewport to convert px units to viewport units for responsive mobile design:

  1. Install the package:
pnpm add postcss-px-to-viewport --save-dev
  1. Create a postcss.config.js configuration file:
module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      viewportWidth: 375
    }
  }
}

Note: If you encounter console warnings, consider using postcss-px-to-viewport-8-plugin as an alternative.

CSS Variable Theming

Customize your project's theme and override Vant's default styles using CSS variables:

Global Theme Variables

Define global CSS variables in styles/main.scss:

:root:root {
  /* Custom project colors */
  --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;

  /* Override Vant's primary color */
  --van-primary-color: var(--cp-primary);
}

Using :root:root ensures your custom variables have higher priority than Vant's default styles.

Use variables in your code:

<template>
  <van-button type="primary">Custom Button</van-button>
  <a href="#" class="custom-link">Custom Link</a>
</template>

<style scoped lang="scss">
.custom-link {
  color: var(--cp-primary);
}
</style>

5. State Management with Pinia

User Authentication Store

Create a Pinia store to manage user authentication state:

  1. Define user type (types/user.ts):
export interface User {
  token: string
  id: string
  account: string
  mobile: string
  avatar: string
}
  1. Create the user store (stores/modules/user.ts):
import { ref } from 'vue'
import { defineStore } from 'pinia'
import type { User } from '@/types/user'

export const useAuthStore = defineStore('cp-auth', () => {
  const currentUser = ref<User>()

  const setAuthUser = (user: User) => {
    currentUser.value = user
  }

  const clearAuthUser = () => {
    currentUser.value = undefined
  }

  return {
    currentUser,
    setAuthUser,
    clearAuthUser
  }
})
  1. Test the store in App.vue:
<script setup lang="ts">
import { useAuthStore } from '@/stores'
import type { User } from '@/types/user'

const authStore = useAuthStore()

const handleLogin = () => {
  const testUser: User = {
    token: 'test-token-123',
    id: '1',
    account: 'test-user',
    mobile: '13211112222',
    avatar: 'https://example.com/avatar.jpg'
  }
  authStore.setAuthUser(testUser)
}

const handleLogout = () => {
  authStore.clearAuthUser()
}
</script>

<template>
  <div>
    {{ authStore.currentUser }}
    <van-button type="primary" @click="handleLogin">Login</van-button>
    <van-button type="primary" @click="handleLogout">Logout</van-button>
  </div>
</template>

Persist Pinia State

Use pinia-plugin-persistedstate to persist store data across page refreshes:

  1. Install the package:
pnpm add pinia-plugin-persistedstate
  1. Update the application entry (main.ts):
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'

import App from './App.vue'
import router from './router'
import './styles/main.scss'

const pinia = createPinia().use(persist)
const app = createApp(App)

app.use(pinia)
app.use(router)
app.mount('#app')
  1. Enable persistence in the user store:
import { ref } from 'vue'
import { defineStore } from 'pinia'
import type { User } from '@/types/user'

export const useAuthStore = defineStore('cp-auth', () => {
  const currentUser = ref<User>()

  const setAuthUser = (user: User) => {
    currentUser.value = user
  }

  const clearAuthUser = () => {
    currentUser.value = undefined
  }

  return {
    currentUser,
    setAuthUser,
    clearAuthUser
  }
}, {
  persist: true
})

Unified Pinia Export

Optimize your store imports by creating a unified export file:

  1. Create stores/index.ts:
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(persist)

export default pinia
export * from './modules/user'
  1. Update main.ts to use the unified export:
import { createApp } from 'vue'
import pinia from './stores'
import App from './App.vue'
import router from './router'
import './styles/main.scss'

const app = createApp(App)

app.use(pinia)
app.use(router)
app.mount('#app')
  1. Simplify imports in components:
// Before
import { useAuthStore } from './stores/modules/user'

// After
import { useAuthStore } from './stores'

6. Data Interaction

Axios Request Utility

Configure Axios Instance

Create a reusable axios instance with interceptors for authentication and error handling:

import { useAuthStore } from '@/stores'
import axios, type AxiosError, type Method } from 'axios'
import { showToast } from 'vant'
import 'vant/es/toast/style'
import router from '@/router'

const requestInstance = axios.create({
  baseURL: 'https://consult-api.itheima.net/',
  timeout: 10000
})

// Request interceptor: Attach auth token
requestInstance.interceptors.request.use(
  (config) => {
    const authStore = useAuthStore()
    if (authStore.currentUser?.token && config.headers) {
      config.headers.Authorization = `Bearer ${authStore.currentUser.token}`
    }
    return config
  },
  (error) => Promise.reject(error)
)

// Response interceptor: Handle business errors and 401 unauthorized
requestInstance.interceptors.response.use(
  (response) => {
    const { code, message, data } = response.data
    // Check for business failure
    if (code !== 10000) {
      showToast(message || 'Business request failed')
      return Promise.reject(response.data)
    }
    // Return only the response data
    return data
  },
  (error: AxiosError) => {
    // Handle 401 unauthorized error
    if (error.response?.status === 401) {
      const authStore = useAuthStore()
      authStore.clearAuthUser()
      // Redirect to login page with return URL
      router.push({
        path: '/login',
        query: { returnUrl: router.currentRoute.value.fullPath }
      })
    }
    showToast(error.message || 'Network error')
    return Promise.reject(error)
  }
)

// Generic request function with TypeScript support
type ApiResponse<T> = {
  code: number
  message: string
  data: T
}

export const request = <T>(
  url: string,
  method: Method = 'GET',
  submitData?: object
) => {
  return requestInstance.request<any, ApiResponse<T>>({
    url,
    method,
    [method.toUpperCase() === 'GET' ? 'params' : 'data']: submitData
  })
}

Test the Request Utility

<script setup lang="ts">
import { request } from '@/utils/request'
import type { User } from '@/types/user'
import { useAuthStore } from '@/stores'

const authStore = useAuthStore()

const handleLogin = async () => {
  try {
    const res = await request<User>('/login/password', 'POST', {
      mobile: '13211112222',
      password: 'abc12345'
    })
    authStore.setAuthUser(res.data)
  } catch (err) {
    console.error('Login failed', err)
  }
}
</script>

<template>
  <van-button type="primary" @click="handleLogin">Login</van-button>
</template>

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.