Step-by-Step Setup Guide for Vite + Vue 3 + TypeScript + Pinia + Vant Project
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:
- Vue - Official: Official Vue language tool (replaces the legacy Volar plugin)
- ESLint: Code quality linting
- 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:
- Open
File > Preferences > Settings - 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:
- Empty the
assets,components,stores, andviewsdirectories - Create new directories:
composables,icons,services,styles,types,utils - Update core files:
router/index.ts,main.ts, andApp.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:
- Install dependencies:
pnpm add @vant/auto-import-resolver unplugin-vue-components --save-dev
- 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()]
})
]
}
- 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:
- Install the package:
pnpm add postcss-px-to-viewport --save-dev
- Create a
postcss.config.jsconfiguration file:
module.exports = {
plugins: {
'postcss-px-to-viewport': {
viewportWidth: 375
}
}
}
Note: If you encounter console warnings, consider using
postcss-px-to-viewport-8-pluginas 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:rootensures 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:
- Define user type (
types/user.ts):
export interface User {
token: string
id: string
account: string
mobile: string
avatar: string
}
- 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
}
})
- 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:
- Install the package:
pnpm add pinia-plugin-persistedstate
- 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')
- 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:
- 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'
- Update
main.tsto 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')
- 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>