优医问诊-项目起步
项目介绍
知道:整体项目概况,并且知道课程中会实现哪些功能
手机端演示:
能学到什么
了解:在项目中会使用到哪些技术方案和特色业务
技术方案:
- 基于 vue3+typescript 中大型项目开发解决方案
- 基于 vant 组件库快速构建H5界面解决方案
- 基于 vue-router 的前端路由解决方案
- 基于 vite 的 create-vue 构建vue3项目解决方案
- 基于 pinia 的状态管理解决方案
- 基于 pinia-plugin-persistedstate 状态持久化解决方案
- 基于 @vuecore/use 的组合式API工具库解决方案
- 身份证信息校验解决方案
- 基于 postcss-px-to-viewport 移动端适配解决方案
- 基于 vite-plugin-svg-icons 的svg图标组件解决方案
- 基于 vite-plugin-html 自定义html模板解决方案
- 基于 unplugin-vue-components 组件自动注册解决方案
- 基于 socket.io 的即时通讯问诊室解决方案
- 第三方登录解决方案
- 第三方支付解决方案
- 第三方地图解决方案
- pnpm 包管理方案
- css 变量主题定制方案
- 自定义 hook 解决方案
- axios 二次封装解决方案
- services API接口分层解决方案
- 基于 vant 的通用组件封装解决方案
- mock 本地数据模拟解决方案
- 基于 eruda 的移动端调试解决方案
- 生产环境配置方案
- CI/CD 持续集成自动部署方案
特色业务:
- 医生与文章推荐业务
- 快速问诊业务
- 问诊费用支付宝支付业务
- 问诊室业务
- 药品订单支付宝支付业务
- 实时物流高德地图业务
- QQ登录业务
pnpm介绍&安装
掌握:pnpm 的安装和使用
本质上他是一个包管理工具,和npm/yarn没有区别,主要优势在于
- 包安装速度极快
- 磁盘空间利用效率高
安装:
sh
npm i pnpm -g
1
使用:
npm命令 | pnpm等效 |
---|---|
npm install | pnpm install |
npm i axios | pnpm add axios |
npm i webpack -D | pnpm add webpack -D |
npm run dev | pnpm dev |
小结:
- pnpm 是一个高效的包管理工具,使用和npm和yarn基本相同
项目创建
使用 create-vue 脚手架创建项目
create-vue参考地址:https://github.com/vuejs/create-vue
步骤:
- 执行创建命令
bash
pnpm create vue
# or
npm init vue@latest
# or
yarn create vue
1
2
3
4
5
2
3
4
5
- 选择项目依赖内容
bash
✔ 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vscode插件安装
安装:项目开发需要的一些插件
必装:
Vue Language Features (Volar)
vue3语法支持TypeScript Vue Plugin (Volar)
vue3中更好的ts提示Eslint
代码风格校验
注意
- vscode 安装了
Prettier
插件的可以先禁用
,或者关闭保存自动格式化功能,避免和项目的Eslint
风格冲突。
可选:
gitLens
代码git提交记录提示json2ts
json自动转ts类型Error Lens
行内错误提示
提示:
- 大中型项目建议开启 TS托管模式 , 更好更快的类型提示。
eslint 预制配置
使用:eslint的预制配置,且了解配置作用
ts
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'
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 格式:单引号,没有分号,行宽度80字符,没有对象数组最后一个逗号,换行字符串自动(系统不一样换行符号不一样)
- vue 组件需要大驼峰命名,除去 index 之外,App 是默认支持的
- 允许对 props 进行解构,我们会开启解构保持响应式的语法糖
执行:
bash
# 修复格式
pnpm lint
1
2
2
vscode 开启 eslint 自动修复
json
"editor.codeActionsOnSave": {
"source.fixAll": true,
},
1
2
3
2
3
小结:
- 如果公司中会有自己的代码风格规则,大家只需遵守即可
- https://prettier.io/docs/en/options.html 常见规则
代码检查工作流
husky 配置
- 初始化与安装
bash
pnpm dlx husky-init && pnpm install
1
- 修改 .husky/pre-commit 文件
bash
pnpm lint
1
lint-staged 配置
- 安装
bash
pnpm i lint-staged -D
1
- 配置
package.json
json
{
// ... 省略 ...
"lint-staged": {
"*.{js,ts,vue}": [
"eslint --fix"
]
}
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
json
{
"scripts": {
// ... 省略 ...
"lint-staged": "lint-staged"
}
}
1
2
3
4
5
6
2
3
4
5
6
- 修改 .husky/pre-commit 文件
bash
pnpm lint-staged
1
项目结构调整
了解:每一个目录结构的作用
bash
./src
├── assets `静态资源,图片...`
├── components `通用组件`
├── composable `组合功能通用函数`
├── icons `svg图标`
├── router `路由`
│ └── index.ts
├── services `接口服务API`
├── stores `状态仓库`
├── styles `样式`
│ └── main.scss
├── types `TS类型`
├── utils `工具函数`
├── views `页面`
├── main.ts `入口文件`
└──App.vue `根组件`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
项目使用sass预处理器,安装sass,即可支持scss语法:
bash
pnpm add sass -D
1
路由代码解析
知道:默认生成的路由代码的含义
ts
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
小结:
如何创建实例的方式?
createRouter()
如何设置路由模式?
createWebHistory()
或者createWebHashHistory()
i
值来自哪里?mport.meta.env.BASE_URL vite.config.ts
的base
属性的值
base
作用是什么?- 项目的基础路径前缀,默认是
/
- 项目的基础路径前缀,默认是
vant组件库
实现:完整使用vant组件库
安装:
bash
# Vue 3 项目,安装最新版 Vant
npm i vant
# 通过 yarn 安装
yarn add vant
# 通过 pnpm 安装
pnpm add vant
1
2
3
4
5
6
2
3
4
5
6
样式:main.ts
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')
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
组件按需使用:App.vue
vue
<script setup lang="ts">
import { Button as VanButton } from 'vant'
</script>
<template>
<van-button>按钮</van-button>
</template>
<style scoped></style>
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
提问:为什么不全局使用?
- 全局使用是全量加载,是项目体积变大,加载慢
移动端适配
实现:使用 vw 完成移动端适配
安装:
bash
npm install postcss-px-to-viewport -D
# or
yarn add -D postcss-px-to-viewport
# or
pnpm add -D postcss-px-to-viewport
1
2
3
4
5
2
3
4
5
配置: postcss.config.js
js
// eslint-disable-next-line no-undef
module.exports = {
plugins: {
'postcss-px-to-viewport': {
// 设备宽度375计算vw的值
viewportWidth: 375,
},
},
};
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
测试:
- 有一个控制台警告可忽略,或者使用
postcss-px-to-viewport-8-plugin
代替当前插件
css变量主题定制
实现:使用css变量定制项目主题,和修改vant主题
- 如果定义 css 变量使用 css 变量
css
:root {
--main: #999;
}
a {
color: var(--main)
}
1
2
3
4
5
6
2
3
4
5
6
- 定义项目的颜色风格,覆盖vant的主题色 官方文档
styles/main.scss
scss
:root {
// 问诊患者:色板
--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;
// 覆盖vant主体色
--van-primary-color: var(--cp-primary);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
App.vue
vue
<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(--cp-primary);
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
用户状态仓库
完成:用户信息仓库创建,提供用户信息,修改用信息,删除用户信息的方法
- 请求工具需要携带token,访问权限控制需要token,所以用户信息仓库先完成
需求:
- 用户信息仓库创建
- 提供用户信息
- 修改用信息的方法
- 删除用信息的方法
代码:
types/user.d.ts
ts
// 用户信息
export type User = {
/** token令牌 */
token: string
/** 用户ID */
id: string
/** 用户名称 */
account: string
/** 手机号 */
mobile: string
/** 头像 */
avatar: string
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
stores/user.ts
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 }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
小结:
pinia存储这个数据的意义?
- 数据共享,提供给项目中任何位置使用
如果存储了数据,刷新页面后数据还在吗?
- 不在,现在仅仅是js内存中,需要进行本地存储(持久化)
数据持久化
掌握:使用
pinia-plugin-persistedstate
实现pinia仓库状态持久化,且完成测试
- 安装
bash
pnpm i pinia-plugin-persistedstate
# or
npm i pinia-plugin-persistedstate
# or
yarn add pinia-plugin-persistedstate
1
2
3
4
5
2
3
4
5
- 使用
main.ts
ts
import persist from 'pinia-plugin-persistedstate'
const app = createApp(App)
app.use(createPinia().use(persist))
1
2
3
4
2
3
4
- 配置
stores/user.ts
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
}
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- 测试
App.vue
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>
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
stores统一导出
实现:仓库的导出统一从
./stores
代码简洁,职能单一,入口唯一
- 抽取pinia实例代码,职能单一
stores/index
ts
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'
// 创建pinia实例
const pinia = createPinia()
// 使用pinia插件
pinia.use(persist)
// 导出pinia实例,给main使用
export default pinia
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
main.ts
ts
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')
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
- 统一导出,代码简洁,入口唯一
stores/index
ts
export * from './modules/user'
1
App.vue
diff
-import { useUserStore } from './stores/user'
+import { useUserStore } from './stores'
1
2
2
小结:
- 统一导出是什么意思?
- 一个模块下的所有资源通过index导出
请求工具函数
拦截器逻辑
实现:token请求头携带,错误响应处理,401错误处理
utils/request.ts
模板代码:
ts
import axios from 'axios'
const instance = axios.create({
// TODO 1. 基础地址,超时时间
})
instance.interceptors.request.use(
(config) => {
// TODO 2. 携带token
return config
},
(err) => Promise.reject(err)
)
instance.interceptors.response.use(
(res) => {
// TODO 3. 处理业务失败
// TODO 4. 摘取核心响应数据
return res
},
(err) => {
// TODO 5. 处理401错误
return Promise.reject(err)
}
)
export default instance
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
代码实现:
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://consult-api.itheima.net/',
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 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
提问:
baseURL 导出的目的是啥?
- 其他模块可能需要使用
为什么使用函数
useXxxStore
函数,建议在拦截器使用?- 模块中的话,store可能还没初始化
业务成功是什么意思?
- 响应成功,且后台业务操作完毕
工具函数封装
实现:导出一个通用的请求工具函数,支持设置响应数据类型
- 导出一个通用的请求工具函数
ts
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
})
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
- 支持不同接口设不同的响应数据的类型
加上泛型
ts
// 这个需要替换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
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
测试请求工具
测试:封装好的请求工具函数
App.vue
vue
<script setup lang="ts">
import { request } from '@/utils/request'
import type { User } from './types/user'
import { Button as VanButton } from 'vant'
import { useUserStore } from './stores'
// 测试,请求拦截器,是否携带token,响应拦截器401拦截到登录地址
const getUserInfo = async () => {
const res = await request('/patient/myUser')
console.log(res)
}
// 测试,响应拦截器,出现非10000的情况,和返回剥离后的数据
const store = useUserStore()
const login = async () => {
const res = await request<User>('/login/password', 'POST', {
mobile: '13211112222',
// 密码 abc123456 测试:出现非10000的情况
password: 'abc12345'
})
store.setUser(res.data)
}
</script>
<template>
<van-button type="primary" @click="getUserInfo">获取个人信息</van-button>
<van-button type="primary" @click="login">登录</van-button>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
测试:
- 登录的时候把密码改错,是测试?
- 业务逻辑失败
- 登录成功,看 res 打印,是测试?
- 剥离一层数据
- 获取用户信息成功,是测试?
- 是否携带token
- 把 token 删除或修改,获取用户信息失败,是测试?
- 401 token 失效跳转 login 页面