Skip to content
当前页大纲

搭建一个开箱即用的基于 Vite + Pinia + Vant + TailwindCSS + TypeScript 的工程

UI框架以 Vant 为例

本工程的Github地址

编写此笔记时所使用的Vite版本为4.4.9

相关文档

初始化项目

sh
pnpm create vue

初始化

按照提示操作即可,这样一个基础项目就创建好了

💡

通过上述交互式命令的选项,我们创建了一个带有vue-routerpiniaESLintPrettier的基于 Vite 脚手架的 Vue 项目

🥧一步到胃

如果你不想尝试一次手动搭建基础模板的过程,那么也可以直接食用Nuxt团队的Anthony Fu大佬的模板

配置EditorConfig

新建.editorconfig,设置编辑器和 IDE 规范,内容根据自己的喜好或者团队规范

ini
# https://editorconfig.org
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

配置ESLint和Prettier

脚手架预设的ESLint还不够完善,这里直接使用Nuxt团队的Anthony Fu大佬的eslint-config进行完善

sh
pnpm dlx @antfu/eslint-config@latest

编辑eslint.config.js

js
import antfu from '@antfu/eslint-config'

export default antfu({
  ignores: ['node_modules', '**/node_modules/**', 'dist', '**/dist/**'],
  formatters: true,
  typescript: true,
  vue: true,
})

编辑package.json,添加如下内容

json
{
  // ...
  "scripts": {
    // ...
    "lint": "eslint .", 
    "lint:fix": "eslint . --fix" 
  }
}

由于 Anthony Fu 大佬的这套eslint-config默认禁用prettier,如果你想配合prettier一起用的话就安装它(不用的话就跳过),然后在根目录新建.prettierrc,填入自己喜欢的配置

sh
pnpm add -D prettier
json
{
  "$schema": "https://json.schemastore.org/prettierrc",
  "semi": false,
  "tabWidth": 2,
  "printWidth": 120,
  "singleQuote": true,
  "trailingComma": "es5"
}

接着编辑.vscode/settings.json,把prettier启用即可

json
{
  "prettier.enable": true 
  // ...
}

安装TailwindCSS

安装依赖

sh
pnpm add -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

编辑tailwind.config.js

js
/** @type {import('tailwindcss').Config} */
export default {
  content: ['./index.html', './src/**/*.{vue,jsx,tsx}'], 
  corePlugins: {
    preflight: true, 
  },
  plugins: [],
}

编辑src/assets/main.css,增加如下内容

css
@tailwind base;
@tailwind components;
@tailwind utilities;

环境变量

关于 Vite 的环境变量详细文档看这里

新建.env文件,填入项目所需的环境变量。注意,环境变量名必须以VITE_开头,否则不会被识别,例如

ini
VITE_APP_NAME=ts-vant-starter
VITE_APP_HOST=localhost
VITE_APP_PORT=5173
API_HOST=http://localhost
API_PORT=80
VITE_BASE_API=$API_HOST:$API_PORT
VITE_API_SECRET=secret_string

编辑env.d.ts,给自定义的环境变量添加类型

ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_NAME: string
  readonly VITE_APP_HOST: string
  readonly VITE_APP_PORT: string
  readonly VITE_BASE_API: string
  readonly VITE_API_SECRET: string
  // 更多环境变量...
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

使用

vite 脚手架规定了src目录下的文件属于浏览器环境,而vite.config.ts文件属于 Node 环境,所以在使用上有点区别

  • src目录下的文件中,通过import.meta.env读取环境变量
  • vite.config.ts文件中,通过loadEnv方法读取环境变量
ts
// ...
import { defineConfig, loadEnv } from 'vite' 
export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd()) 
  // ...
})

自动导入

sh
pnpm add -D unplugin-auto-import

编辑vite.config.ts,注册插件

ts
import AutoImport from 'unplugin-auto-import/vite' 
export default defineConfig(({ mode }) => {
  return {
    plugins: [
      // ... 
      AutoImport({
        include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/, /\.md$/],
        imports: ['vue', 'pinia', 'vue-router'],
        eslintrc: {
          enabled: true,
        },
        dts: true,
      }),
    ],
  }
})

编辑tsconfig.app.json,将插件生成的auto-imports.d.ts添加进include字段

json
{
  "include": [
    // ...
    "auto-imports.d.ts" 
  ]
}

编辑.eslintrc.js,将插件生成的.eslintrc-auto-import.json添加进extends字段

js
module.exports = {
  extends: [
    // ...
    './.eslintrc-auto-import.json', 
  ],
}

助手函数

新建src/libs/utils.ts,封装一些辅助函数,具体代码参考我的助手函数封装

请求模块

sh
pnpm add axios

新建src/api/core/http.tssrc/api/core/config.ts,之后的封装逻辑参考我的Axios封装

Mock

安装2.9.8的版本,3的版本目前有bug

sh
pnpm add -D vite-plugin-mock@2.9.8 mockjs @types/mockjs

编辑vite.config.ts,注册插件

ts
import { viteMockServe } from 'vite-plugin-mock' 
export default defineConfig(({ mode }) => {
  return {
    plugins: [
      //...
      viteMockServe(), 
    ],
  }
})

根目录新建mock/index.ts,示例如下,根据自己的情况添加添加接口

ts
import type { MockMethod } from 'vite-plugin-mock'
export default [
  {
    url: '/api/login',
    method: 'post',
    response: () => {
      return {
        code: '200',
        message: 'ok',
        data: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MjMyODU2LCJzZXNzaW9uIjoiOTRlZTZjOThmMmY4NzgzMWUzNzRmZTBiMzJkYTIwMGMifQ.z5Llnhe4muNsanXQSV-p1DJ-89SADVE-zIkHpM0uoQs',
        success: true,
      }
    },
  },
] as MockMethod[]
  • 使用
ts
import { request } from './api'
request('/api/login', { method: 'POST' })

注意,vite-plugin-mock默认是以当前开发服务器的hostpost作为baseURL

状态持久化

sh
pnpm add pinia-plugin-persistedstate

编辑src/main.ts,注册插件

ts
// ...
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' 
const app = createApp(App)
app.use(createPinia().use(piniaPluginPersistedstate)).use(router).mount('#app') 

新建src/libs/storage.tssrc/stores/user.ts

ts
enum StorageSceneKey {
  USER = 'storage-user',
}
function getItem<T = any>(key: string): T {
  const value = localStorage.getItem(key)
  return value ? JSON.parse(value) ?? null : null
}
function setItem(key: string, value: any) {
  localStorage.setItem(key, JSON.stringify(value))
}
function removeItem(key: string) {
  localStorage.removeItem(key)
}
export { getItem, setItem, removeItem, StorageSceneKey }
ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { StorageSceneKey } from '../libs'
export const useUserStore = defineStore(
  'user',
  () => {
    const token = ref('')
    const isLogged = ref(false)
    const setToken = (value: string) => {
      token.value = value
      isLogged.value = true
    }
    const removeToken = () => {
      token.value = ''
      isLogged.value = false
    }
    return { token, isLogged, setToken, removeToken }
  },
  {
    persist: {
      //! 注意这里的key是当前这个Pinia模块进行缓存时的唯一key, 每个需要缓存的Pinia模块都必须分配一个唯一key
      key: StorageSceneKey.USER,
    },
  }
)

UI框架

使用Vant

sh
pnpm add vant

按需引入

sh
pnpm add -D @vant/auto-import-resolver unplugin-vue-components

编辑vite.config.js,在plugins中增加Components({ resolvers: [VantResolver()] })

js
// ...
import Components from 'unplugin-vue-components/vite' 
import { VantResolver } from '@vant/auto-import-resolver' 
export default defineConfig(({ mode }) => {
  return {
    plugins: [
      //...
      Components({ resolvers: [VantResolver()] }), 
    ],
  }
})

这样就完成了 Vant 的按需引入,就可以直接在模板中使用 Vant 组件了,unplugin-vue-components会解析模板并自动注册对应的组件,@vant/auto-import-resolver会自动引入对应的组件样式

移动端适配

此插件的参数配置文档看这里

sh
pnpm add -D postcss-px-to-viewport-8-plugin

由于Vant使用的设计稿宽度是375,而通常情况下,设计师使用的设计稿宽度更多是750,那么Vant组件在750设计稿下会出现样式缩小的问题

解决方案: 当读取的node_modules文件是vant时,那么就将设计稿宽度变为375,读取的文件不是vant时,就将设计稿宽度变为750

  • 方式一:编辑postcss.config.js,增加如下postcss-px-to-viewport-8-plugin配置项
js
import path from 'path' 
export default {
  plugins: {
    
    'postcss-px-to-viewport-8-plugin': {
      viewportWidth: (file) => {
        return path.resolve(file).includes(path.join('node_modules', 'vant')) ? 375 : 750
      },
      unitPrecision: 6,
      landscapeWidth: 1024,
    },
  },
}
  • 方式二:编辑vite.config.ts,增加如下css配置项
ts
// ...
import path from 'path' 
import postcsspxtoviewport8plugin from 'postcss-px-to-viewport-8-plugin' 
export default defineConfig(({ mode }) => {
  return {
    
    css: {
      postcss: {
        plugins: [
          postcsspxtoviewport8plugin({
            viewportWidth: (file) => {
              return path.resolve(file).includes(path.join('node_modules', 'vant')) ? 375 : 750
            },
            unitPrecision: 6,
            landscapeWidth: 1024,
          }),
        ],
      },
    },
  }
})

🎉

到这里,基于 Vite 的基础项目模板就搭建完成了

搭配React

sh
pnpm create vite

💡

通过上述交互式命令的选项,我们创建了一个带有 ESLint 的基于 Vite 脚手架的 React 项目

EditorConfig 参考上面的配置

ESLint和Prettier

sh
pnpm dlx @antfu/eslint-config@latest

编辑eslint.config.js

js
import antfu from '@antfu/eslint-config'

export default antfu({
  ignores: ['node_modules', '**/node_modules/**', 'dist', '**/dist/**'],
  formatters: true,
  typescript: true,
  react: true,
})

编辑package.json,添加如下内容

json
{
  // ...
  "scripts": {
    // ...
    "lint": "eslint .", 
    "lint:fix": "eslint . --fix" 
  }
}

由于 Anthony Fu 大佬的这套eslint-config默认禁用prettier,如果你想配合prettier一起用的话就安装它(不用的话就跳过),然后在根目录新建.prettierrc,填入自己喜欢的配置

sh
pnpm add -D prettier
json
{
  "$schema": "https://json.schemastore.org/prettierrc",
  "semi": false,
  "tabWidth": 2,
  "printWidth": 120,
  "singleQuote": true,
  "trailingComma": "es5"
}

接着编辑.vscode/settings.json,把prettier启用即可

json
{
  "prettier.enable": true 
  // ...
}

TailwindCSS

参考上面的配置

只是 CSS 的引入变成了src/index.css

css
@tailwind base;
@tailwind components;
@tailwind utilities;

环境变量也是参考上面的配置

引入react-vant

sh
pnpm add react-vant @react-vant/icons

状态管理

sh
pnpm add zustand immer

定义

新建src/models/counter.tssrc/models/selectors.ts

ts
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
import createSelectors from './selectors'
interface State {
  count: number
}
interface Action {
  inc: () => void
  dec: () => void
}
const initialState: State = {
  count: 0,
}
const counterStore = create<State & Action>()(
  immer((set, get) => ({
    count: 0,
    inc: () => set((state) => ({ count: state.count + 1 })),
    dec: () => set((state) => ({ count: state.count - 1 })),
  }))
)
export const useCounterStore = createSelectors(counterStore)
export function useCounterReset() {
  counterStore.setState(initialState)
}
ts
import { StoreApi, UseBoundStore } from 'zustand'
type WithSelectors<S> = S extends { getState: () => infer T }
  ? S & { use: { [K in keyof T]: () => T[K] } }
  : never
const createSelectors = <S extends UseBoundStore<StoreApi<{}>>>(_store: S) => {
  let store = _store as WithSelectors<typeof _store>
  store.use = {}
  for (let k of Object.keys(store.getState())) {
    ;(store.use as any)[k] = () => store((s) => s[k as keyof typeof s])
  }
  return store
}
export default createSelectors

示例

tsx
// ...
import { useCounterStore, useCounterReset } from './models'
function App() {
  const count = useCounterStore.use.count()
  const inc = useCounterStore.use.inc()
  return (
    <>
      <Button
        icon={<Like />}
        round
        color="linear-gradient(to right, #ff6034, #ee0a24)"
        size="small"
        onClick={inc}
      >
        Like {count}
      </Button>
      <div className="card">
        <button onClick={useCounterReset}>Reset</button>
      </div>
    </>
  )
}

持久化

新建src/libs/storage.tssrc/models/user.ts

ts
enum StorageSceneKey {
  USER = 'storage-user',
}
function getItem<T = any>(key: string): T {
  const value = localStorage.getItem(key)
  return value ? JSON.parse(value) ?? null : null
}
function setItem(key: string, value: any) {
  localStorage.setItem(key, JSON.stringify(value))
}
function removeItem(key: string) {
  localStorage.removeItem(key)
}
export { getItem, setItem, removeItem, StorageSceneKey }
ts
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
import { createJSONStorage, persist, StateStorage } from 'zustand/middleware'
import createSelectors from './selectors'
import { StorageSceneKey } from '../libs'
interface State {
  token: string
  isLogged: boolean
}
interface Action {
  setToken: (token: string) => void
  removeToken: () => void
}
const userStorage: StateStorage = {
  getItem: (key) => {
    const value = localStorage.getItem(key)
    return value ?? null
  },
  setItem: (key, value) => {
    localStorage.setItem(key, value)
  },
  removeItem: (key) => {
    localStorage.removeItem(key)
  },
}
const initialState: State = {
  token: '',
  isLogged: false,
}
const userStore = create<State & Action>()(
  immer(
    persist(
      (set, get) => ({
        token: '',
        isLogged: false,
        setToken: (token) => set({ token, isLogged: true }),
        removeToken: () => set({ token: '', isLogged: false }),
      }),
      {
        //! 注意这里的name是当前这个Zustand模块进行缓存时的唯一key, 每个需要缓存的Zustand模块都必须分配一个唯一key
        name: StorageSceneKey.USER,
        storage: createJSONStorage(() => userStorage),
      }
    )
  )
)
export const useUserStore = createSelectors(userStore)
export function useUserReset() {
  userStore.setState(initialState)
}

MIT License.