✨
搭建一个开箱即用的基于 Vite + Pinia + Vant + TailwindCSS + TypeScript 的工程
UI框架以 Vant 为例
编写此笔记时所使用的Vite
版本为4.4.9
相关文档
初始化项目
pnpm create vue
按照提示操作即可,这样一个基础项目就创建好了
💡
通过上述交互式命令的选项,我们创建了一个带有vue-router
、pinia
、ESLint
和Prettier
的基于 Vite 脚手架的 Vue 项目
🥧一步到胃
如果你不想尝试一次手动搭建基础模板的过程,那么也可以直接食用Nuxt团队的Anthony Fu大佬的模板
配置EditorConfig
新建.editorconfig
,设置编辑器和 IDE 规范,内容根据自己的喜好或者团队规范
# 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进行完善
pnpm dlx @antfu/eslint-config@latest
编辑eslint.config.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
,添加如下内容
{
// ...
"scripts": {
// ...
"lint": "eslint .",
"lint:fix": "eslint . --fix"
}
}
由于 Anthony Fu 大佬的这套eslint-config
默认禁用prettier
,如果你想配合prettier
一起用的话就安装它(不用的话就跳过),然后在根目录新建.prettierrc
,填入自己喜欢的配置
pnpm add -D prettier
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"printWidth": 120,
"singleQuote": true,
"trailingComma": "es5"
}
接着编辑.vscode/settings.json
,把prettier
启用即可
{
"prettier.enable": true
// ...
}
安装TailwindCSS
安装依赖
pnpm add -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
编辑tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{vue,jsx,tsx}'],
corePlugins: {
preflight: true,
},
plugins: [],
}
编辑src/assets/main.css
,增加如下内容
@tailwind base;
@tailwind components;
@tailwind utilities;
环境变量
关于 Vite 的环境变量详细文档看这里
新建.env
文件,填入项目所需的环境变量。注意,环境变量名必须以VITE_
开头,否则不会被识别,例如
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
,给自定义的环境变量添加类型
/// <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
方法读取环境变量
// ...
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
// ...
})
自动导入
pnpm add -D unplugin-auto-import
编辑vite.config.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
字段
{
"include": [
// ...
"auto-imports.d.ts"
]
}
编辑.eslintrc.js
,将插件生成的.eslintrc-auto-import.json
添加进extends
字段
module.exports = {
extends: [
// ...
'./.eslintrc-auto-import.json',
],
}
助手函数
新建src/libs/utils.ts
,封装一些辅助函数,具体代码参考我的助手函数封装
请求模块
pnpm add axios
新建src/api/core/http.ts
和src/api/core/config.ts
,之后的封装逻辑参考我的Axios封装
Mock
安装2.9.8
的版本,3
的版本目前有bug
pnpm add -D vite-plugin-mock@2.9.8 mockjs @types/mockjs
编辑vite.config.ts
,注册插件
import { viteMockServe } from 'vite-plugin-mock'
export default defineConfig(({ mode }) => {
return {
plugins: [
//...
viteMockServe(),
],
}
})
根目录新建mock/index.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[]
- 使用
import { request } from './api'
request('/api/login', { method: 'POST' })
注意,vite-plugin-mock
默认是以当前开发服务器的host
和post
作为baseURL
状态持久化
pnpm add pinia-plugin-persistedstate
编辑src/main.ts
,注册插件
// ...
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const app = createApp(App)
app.use(createPinia().use(piniaPluginPersistedstate)).use(router).mount('#app')
新建src/libs/storage.ts
和src/stores/user.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 }
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
pnpm add vant
按需引入
pnpm add -D @vant/auto-import-resolver unplugin-vue-components
编辑vite.config.js
,在plugins
中增加Components({ resolvers: [VantResolver()] })
// ...
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
会自动引入对应的组件样式
移动端适配
此插件的参数配置文档看这里
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
配置项
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
配置项
// ...
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
pnpm create vite
💡
通过上述交互式命令的选项,我们创建了一个带有 ESLint 的基于 Vite 脚手架的 React 项目
EditorConfig 参考上面的配置
ESLint和Prettier
pnpm dlx @antfu/eslint-config@latest
编辑eslint.config.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
,添加如下内容
{
// ...
"scripts": {
// ...
"lint": "eslint .",
"lint:fix": "eslint . --fix"
}
}
由于 Anthony Fu 大佬的这套eslint-config
默认禁用prettier
,如果你想配合prettier
一起用的话就安装它(不用的话就跳过),然后在根目录新建.prettierrc
,填入自己喜欢的配置
pnpm add -D prettier
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"printWidth": 120,
"singleQuote": true,
"trailingComma": "es5"
}
接着编辑.vscode/settings.json
,把prettier
启用即可
{
"prettier.enable": true
// ...
}
TailwindCSS
只是 CSS 的引入变成了src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
环境变量也是参考上面的配置
引入react-vant
pnpm add react-vant @react-vant/icons
状态管理
pnpm add zustand immer
定义
新建src/models/counter.ts
和src/models/selectors.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)
}
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
示例
// ...
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.ts
和src/models/user.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 }
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)
}