pnpm安装
npm i pnpm -g
项目创建
使用 create-vue 脚手架创建项目
- 执行创建命令
pnpm create vue # or npm init vue@latest # or yarn create vue
- 选择项目依赖内容
✔ Project name: … patients-h5-100 ✔ 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 patient-h5-100 pnpm install pnpm lint pnpm dev
vscode插件安装
必装:
Vue Language Features (Volar)
vue3语法支持TypeScript Vue Plugin (Volar)
vue3中更好的ts提示Eslint
代码风格校验
可选:
gitLens
代码git提交记录提示json2ts
json自动转ts类型Error Lens
行内错误提示
eslint 预制配置 .eslintrc.cjs
rules: {
'prettier/prettier': [
'warn',
{
singleQuote: true,
semi: false,
printWidth: 80,
trailingComma: 'none',
endOfLine: 'auto'
}
],
'vue/multi-word-component-names': [
'warn',
{
ignores: ['index']
}
],
'vue/no-setup-props-destructure': ['off'],
// 💡 添加未定义变量错误提示,create-vue@3.6.3 关闭,这里加上是为了支持下一个章节演示。
'no-undef': 'error'
}
- 格式:单引号,没有分号,行宽度80字符,没有对象数组最后一个逗号,换行字符串自动(系统不一样换行符号不一样)
- vue 组件需要大驼峰命名,除去 index 之外,App 是默认支持的
- 允许对 props 进行解构,我们会开启解构保持响应式的语法糖
执行:
# 修复格式
pnpm lint
vscode 开启 eslint 自动修复:
"editor.codeActionsOnSave": {
"source.fixAll": true,
},
代码检查工作流
husky 配置
- 初始化与安装
pnpm dlx husky-init && pnpm install
- 修改 .husky/pre-commit 文件
pnpm lint
lint-staged 配置
- 安装
pnpm i lint-staged -D
- 配置
package.json
{ // ... 省略 ... "lint-staged": { "*.{js,ts,vue}": [ "eslint --fix" ] } }
{ "scripts": { // ... 省略 ... "lint-staged": "lint-staged" } }
- 修改 .husky/pre-commit 文件
pnpm lint-staged
项目结构调整
./src
├── assets `静态资源,图片...`
├── components `通用组件`
├── composable `组合功能通用函数`
├── icons `svg图标`
├── router `路由`
│ └── index.ts
├── services `接口服务API`
├── stores `状态仓库`
├── styles `样式`
│ └── main.scss
├── types `TS类型`
├── utils `工具函数`
├── views `页面`
├── main.ts `入口文件`
└──App.vue `根组件`
项目使用sass预处理器,安装sass,即可支持scss语法:
pnpm add sass -D
路由代码解析
import { createRouter, createWebHistory } from 'vue-router'
// createRouter 创建路由实例,===> new VueRouter()
// history 是路由模式,hash模式,history模式
// createWebHistory() 是开启history模块 http://xxx/user
// createWebHashHistory() 是开启hash模式 http://xxx/#/user
// 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 router
vant组件库
安装:
# Vue 3 项目,安装最新版 Vant
npm i vant
# 通过 yarn 安装
yarn add vant
# 通过 pnpm 安装
pnpm add vant
样式:main.ts
import { createApp } from 'vue'
import App from './App.vue'
import pinia from './stores'
import router from './router'
// 样式全局使用
import 'vant/lib/index.css'
import './styles/main.scss'
const app = createApp(App)
app.use(pinia)
app.use(router)
app.mount('#app')
组件按需使用:App.vue
<script setup lang="ts">
import { Button as VanButton } from 'vant'
</script>
<template>
<van-button>按钮</van-button>
</template>
<style scoped></style>
移动端适配
安装:
npm install postcss-px-to-viewport -D
# or
yarn add -D postcss-px-to-viewport
# or
pnpm add -D postcss-px-to-viewport
配置: postcss.config.js
// eslint-disable-next-line no-undef
module.exports = {
plugins: {
'postcss-px-to-viewport': {
// 设备宽度375计算vw的值
viewportWidth: 375,
},
},
};
有一个控制台警告可忽略,或者使用 postcss-px-to-viewport-8-plugin
代替当前插件
css变量主题定制
- 如何定义 css 变量使用 css 变量
:root { --main: #999; } a { color: var(--main) }
- 定义项目的颜色风格,覆盖vant的主题色 官方文档 styles/main.scss
App.vue:root { --main-primary: #16C2A3; --main-plain: #EAF8F6; --main-orange: #FCA21C; --main-text1: #121826; --main-text2: #3C3E42; --main-text3: #6F6F6F; --main-tag: #848484; --main-dark: #979797; --main-tip: #C3C3C5; --main-disable: #D9DBDE; --main-line: #EDEDED; --main-bg: #F6F7F9; --main-price: #EB5757; // 覆盖vant主体色 --van-primary-color: var(--main-primary); }
<script setup lang="ts"></script> <template> <!-- 验证vant颜色被覆盖 --> <van-button type="primary">按钮</van-button> <a href="#">123</a> </template> <style scoped lang="scss"> // 使用 css 变量 a { color: var(--main-primary); } </style>
用户状态仓库
- 请求工具需要携带token,访问权限控制需要token,所以用户信息仓库先完成
需求:
- 用户信息仓库创建
- 提供用户信息
- 修改用信息的方法
- 删除用信息的方法
代码:
types/user.d.ts
// 用户信息
export type User = {
/** token令牌 */
token: string
/** 用户ID */
id: string
/** 用户名称 */
account: string
/** 手机号 */
mobile: string
/** 头像 */
avatar: string
}
stores/user.ts
import type { User } from '@/types/user'
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useUserStore = defineStore('cp-user', () => {
// 用户信息
const user = ref<User>()
// 设置用户,登录后使用
const setUser = (u: User) => {
user.value = u
}
// 清空用户,退出后使用
const delUser = () => {
user.value = undefined
}
return { user, setUser, delUser }
})
数据持久化
使用
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
import type { User } from '@/types/user' import { defineStore } from 'pinia' import { ref } from 'vue' export const useUserStore = 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 } )
- 测试
App.vue
<script setup lang="ts"> import { useUserStore } from './stores/user' const store = useUserStore() </script> <template> <p>{{ store.user }}</p> <button @click="store.setUser({ id: '1', mobile: '1', account: '1', avatar: '1', token: '1' })"> 登录 </button> <button @click="store.delUser()">退出</button> </template>
stores统一导出
仓库的导出统一从
./stores
代码简洁,职能单一,入口唯一即一个模块下的所有资源通过index导出
- 抽取pinia实例代码,职能单一 stores/index
main.tsimport { createPinia } from 'pinia' import persist from 'pinia-plugin-persistedstate' // 创建pinia实例 const pinia = createPinia() // 使用pinia插件 pinia.use(persist) // 导出pinia实例,给main使用 export default pinia
import { createApp } from 'vue' import App from './App.vue' import pinia from './stores' import router from './router' import './styles/main.scss' const app = createApp(App) app.use(pinia) app.use(router) app.mount('#app')
- 统一导出,代码简洁,入口唯一 stores/index
App.vueexport * from './modules/user'
-import { useUserStore } from './stores/user' +import { useUserStore } from './stores'
请求工具函数
拦截器逻辑
token请求头携带,错误响应处理,401错误处理
utils/request.ts
import { useUserStore } from '@/stores'
import router from '@/router'
import axios from 'axios'
import { showToast } from 'vant'
// 1. 新axios实例,基础配置
const instance = axios.create({
baseURL: 'https://xxx.com/',
timeout: 10000
})
// 2. 请求拦截器,携带token
instance.interceptors.request.use(
(config) => {
const store = useUserStore()
if (store.user?.token && config.headers) {
config.headers['Authorization'] = `Bearer ${store.user?.token}`
}
return config
},
(err) => Promise.reject(err)
)
// 3. 响应拦截器,剥离无效数据,401拦截
instance.interceptors.response.use(
(res) => {
// 后台约定,响应成功,但是code不是10000,是业务逻辑失败
if (res.data?.code !== 10000) {
showToast(res.data?.message || '业务失败')
return Promise.reject(res.data)
}
// 业务逻辑成功,返回响应数据,作为axios成功的结果
return res.data
},
(err) => {
if (err.response.status === 401) {
// 删除用户信息
const store = useUserStore()
store.delUser()
// 跳转登录,带上接口失效所在页面的地址,登录完成后回跳使用
router.push({
path: '/login',
query: { returnUrl: router.currentRoute.value.fullPath }
})
}
return Promise.reject(err)
}
)
export { baseURL, instance }
工具函数封装
导出一个通用的请求工具函数,支持设置响应数据类型
- 导出一个通用的请求工具函数
import axios, { AxiosError, type Method } from 'axios' // 4. 请求工具函数 const request = (url: string, method: Method = 'GET', submitData?: object) => { return instance.request({ url, method, [method.toUpperCase() === 'GET' ? 'params' : 'data']: submitData }) }
- 支持不同接口设不同的响应数据的类型,加上泛型
// 这个需要替换axsio.request默认的响应成功后的结果类型 // 之前是:传 { name: string } 然后res是 res = { data: { name: string } } // 但现在:在响应拦截器中返回了 res.data 也就是将来响应成功后的结果,和上面的类型一致吗? // 所以要:request<数据类型,数据类型>() 这样才指定了 res.data 的类型 // 但是呢:后台返回的数据结构相同,所以可以抽取相同的类型 type Data<T> = { code: number message: string data: T } // 4. 请求工具函数 const request = <T>(url: string, method: Method = 'get', submitData?: object) => { return instance.request<T, Data<T>>({ url, method, [method.toLowerCase() === 'get' ? 'params' : 'data']: submitData }) }
自动按需加载
实现自动按需加载,和自动导入 官方文档
手动按需使用组件比较麻烦,需要先导入。配置函数自动按需导入后直接使用即可
- 安装:
# 通过 npm 安装 npm i unplugin-vue-components -D # 通过 yarn 安装 yarn add unplugin-vue-components -D # 通过 pnpm 安装 pnpm add unplugin-vue-components -D
- 配置:
import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import Components from 'unplugin-vue-components/vite' import { VantResolver } from 'unplugin-vue-components/resolvers' // https://vitejs.dev/config/ export default defineConfig({ plugins: [ // 解析单文件组件的插件 vue(), // 自动导入的插件,解析器可以是 vant element and-vue Components({ dts: false, // 原因:Toast Confirm 这类组件的样式还是需要单独引入,样式全局引入了,关闭自动引入 resolvers: [VantResolver({ importStyle: false })] }) ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } })
图标组件-打包svg地图
根据 icons 文件svg图片打包到项目中,通过组件使用图标 参考文档
- 安装插件
yarn add vite-plugin-svg-icons -D # or npm i vite-plugin-svg-icons -D # or pnpm install vite-plugin-svg-icons -D
- 使用插件
vite.config.ts
import { VantResolver } from 'unplugin-vue-components/resolvers' +import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' +import path from 'path' // https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue(), Components({ dts: false, resolvers: [VantResolver({ importStyle: false })] }), + createSvgIconsPlugin({ + // 指定图标文件夹,绝对路径(NODE代码) + iconDirs: [path.resolve(process.cwd(), 'src/icons')] + }) ],
- 导入到main
import router from './router' +import 'virtual:svg-icons-register' import 'vant/lib/index.css'
- 使用svg精灵地图
<svg aria-hidden="true"> <!-- #icon-文件夹名称-图片名称 --> <use href="#icon-login-eye-off" /> </svg>
图标组件-封装svg组件
- 组件
components/SvgIcon.vue
<script setup lang="ts"> // 提供name属性即可 defineProps<{ name: string }>() </script> <template> <svg aria-hidden="true" class="svg-icon"> <use :href="`#icon-${name}`" /> </svg> </template> <style lang="scss" scoped> .svg-icon { // 和字体一样大 width: 1em; height: 1em; } </style>
- 类型
types/components.d.ts
import SvgIcon from '@/components/SvgIcon.vue' declare module 'vue' { interface GlobalComponents { SvgIcon: typeof SvgIcon } }