pnpm介绍&安装
本质上他是一个包管理工具,和npm/yarn没有区别,主要优势在于
- 包安装速度极快
- 磁盘空间利用效率高
安装:
npm i pnpm -g
使用:
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基本相同
1、项目创建
使用 create-vue 脚手架创建项目
create-vue,即官方的项目脚手架工具,提供了搭建基于 Vite且 TypeScript 就绪的 Vue 项目的选项。
步骤:
- 执行创建命令
pnpm create vue@latest
- 选择项目依赖内容
✔ 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
2、项目准备工作
vscode插件安装
安装:项目开发需要的一些插件
必装:
-
Vue Language Features (Volar)
语法高亮,代码提示, volar支持setup语法糖,官方推荐vue3使用 -
TypeScript Vue Plugin (Volar)
vvue3中更好的ts提示, 让Ts服务知道.vue文件
近日,Vue 官方语言工具发布了 2.0 版本,该版本已经将 VS Code 插件名称由Volar Language Features (Volar) 改为了 Vue - Official,TypeScript Vue Plugin扩展也已经被弃用
Eslint
代码风格校验
注意
- vscode 安装了
Prettier
插件的可以先禁用
,或者关闭保存自动格式化功能,避免和项目的Eslint
风格冲突。- (Volar为vue3开发)(vetur主要是vue2)Volar和vetur不能共存
可选:
gitLens
代码git提交记录提示json2ts
json自动转ts类型Error Lens
行内错误提示 💡
提示:
大中型项目建议开启 TS托管模式 , 更好更快的类型提示。
更高效的开启TS托管模式 (Take Over Mode(托管模式),TS服务性能更好)
什么是托管模式,vscode内置了一套ts服务,volar也提供了一套ts服务,所以需要开启托管模式,让ts性能提示更加高效,只需要下面两步即可,1.关闭vscode内置的TS服务,2.使用Volar提供的TS服务
vscode 插件中搜索 @builtin ty,在当前工作区禁用ts服务后,重启vscode即可。
查看ts已托管成功,重启后vscode工具栏下方有这个提示即可
这里就相当于告诉vscode,你不需要提供ts服务了,我找个人(volar插件)来替你服务,即所谓的托管。
eslint 预制配置
.eslintrc.cjs文件
加入如下配置,覆盖原有的eslint配置
rules: {
'prettier/prettier': [
'warn',
{
singleQuote: true,//单引号
semi: false,//没有分号
printWidth: 80,//行宽度80字符
trailingComma: 'none',//没有对象数组最后一个逗号
endOfLine: 'auto'//换行字符串自动(系统不一样换行符号不一样)
}
],
//新手在组件命名的时候不够规范,根据官方风格指南,除了根组件(App.vue)外,
// 自定义组件名称应该由多单词组成,防止和html标签冲突。
'vue/multi-word-component-names': [
'warn',
{
ignores: ['index']
}
],
// 允许对 props 进行解构
'vue/no-setup-props-destructure': ['off'],
// 添加未定义变量错误提示
'no-undef': 'error'
}
- 格式:单引号,没有分号,行宽度80字符,没有对象数组最后一个逗号,换行字符串自动(系统不一样换行符号不一样)
- vue 组件需要大驼峰命名,除去 index 之外,App 是默认支持的
- 允许对 props 进行解构,我们会开启解构保持响应式的语法糖
执行:
# 修复格式
pnpm lint
vscode 开启 eslint 自动修复
"editor.codeActionsOnSave": {
"source.fixAll": true,
},
提示:安装Eslint且配置保存修复,不要开启默认的自动保存格式化
代码检查工作流
提交代码前做代码检查 -使用husky这个git hooks工具(husky相当于是git的钩子)
husky [ˈhʌski] 哈士奇
eslint只能修复代码的风格,不能修复代码的错误,此时提交代码比较危险
怎么做?安装husky工具(这个工具是在commit提交文件之前检查代码是否有问题的和git一起用)
-
初始化git仓库,执行git init即可
-
初始化husky工具配置,执行
pnpm dlx husky-init && pnpm install
-
修改 .husky/pre-commit 文件,添加pnpm lint 这个命令
pnpm lint是全量检查,突然添加就会有耗时问题和检查历史遗留问题,所以最好只检查修改的文件,在暂存区做lint校验(git add .的时候把文件提交到了暂存区),在commit命令之前去执行
如何只检查暂存区代码?
-
安装
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 `通用组件`
├── composables `组合功能通用函数`
├── icons `svg图标`
├── router `路由`
│ └── index.ts
├── services `接口服务API`
├── stores `状态仓库`
├── styles `样式`
│ └── main.scss
├── types `TS类型`
├── utils `工具函数`
├── views `页面`
├── main.ts `入口文件`
└──App.vue `根组件`
assets, components, stores, views 清空文件夹
composables, icons, services, styles/main.scss, types, utils 新建文件夹
router/index.ts, main.ts, App.vue 修改
router/index.ts
import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [] }) export default router
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')
App.vue
<script setup lang="ts"></script> <template> <div>App</div> </template> <style scoped></style>
项目使用sass预处理器,安装sass,即可支持scss语法:
pnpm add sass -D
导入 main.ts 作为全局样式
import './styles/main.scss'
路由代码解析
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
- 创建路由实例由 createRouter 实现
- 路由模式
- history 模式使用 createWebHistory()
- hash 模式使用 createWebHashHistory()
- 参数是基础路径,默认/
扩展:import.meta.env.BASE_URL
import.meta
是 JavaScript 模块暴露的描述模块的信息对象
env.BASE_URL 是Vite 环境变量
3、移动端项目基础架构要做的工作
4、构建界面
vant组件库
安装:
# Vue 3 项目,安装最新版 Vant
npm i vant
# 通过 yarn 安装
yarn add vant
# 通过 pnpm 安装
pnpm add vant
引入组件 - vant 自动引入组件,并按需引入组件的样式:
在基于
vite
、webpack
或vue-cli
的项目中使用 Vant 时,可以使用 unplugin-vue-components 插件,它可以自动引入组件。Vant 官方基于
unplugin-vue-components
提供了自动导入样式的解析器 @vant/auto-import-resolver,两者可以配合使用。
-
安装插件
# 通过 npm 安装 npm i @vant/auto-import-resolver unplugin-vue-components -D # 通过 yarn 安装 yarn add @vant/auto-import-resolver unplugin-vue-components -D # 通过 pnpm 安装 pnpm add @vant/auto-import-resolver unplugin-vue-components -D # 通过 bun 安装 bun add @vant/auto-import-resolver unplugin-vue-components -D
-
配置插件
如果是基于
vite
的项目,在vite.config.js
文件中配置插件: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()], }), ], };
-
使用组件
<template> <van-button type="primary" /> </template>
-
引入函数组件的样式
Vant 中有个别组件是以函数的形式提供的,包括
Toast
,Dialog
,Notify
和ImagePreview
组件。在使用函数组件时,unplugin-vue-components
无法解析自动注册组件,导致@vant/auto-import-resolver
无法解析样式,因此需要手动引入样式。// 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';
移动端适配
实现:使用 vw 完成移动端适配
安装:
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变量定制项目主题,和修改vant主题
-
全局变量:使用场景:项目主题
:root { /* 全局css变量 */ --main: #999; --cp-primary: #16C2A3; /* 覆盖vant主体色 */ --van-primary-color: var(--cp-primary); } a { /* 使用变量 */ color: var(--main) }
-
局部变量:使用场景:组件变量
.footer { /* 局部变量 */ --footer-color: green; } .footer { /* 只能在footer下使用 */ color: var(--footer-color); }
-
如何覆盖vant的主题色:找到主题色的变量名,覆盖即可
styles/main.scss
注意:为什么要写两个重复的 :root?
由于 vant 中的主题变量也是在 :root 下声明的,所以在有些情况下会由于优先级的问题无法成功覆盖。通过 :root:root 可以显式地让你所写内容的优先级更高一些,从而确保主题变量的成功覆盖。:root: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); }
App.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、css变量
有的网站都会有自己的主题色,例如饿了么:
所以很多元素都会用到这些颜色,例如某些字体颜色,弹框提示颜色等等。
如果每次用到的时候,都使用十六进制的颜色表示,那么效率十分低,并且如果万一有一天需要更换主题颜色,那么一个一个更改,是十分繁琐的。
为了解决以上问题,css引入了变量。
var()
函数用于插入 CSS 变量的值。全局变量可以在整个文档进行访问使用,局部变量只能在声明它的选择器内部使用
。例如:
/* 全局变量 */ :root { --main-color: red; } /* 局部变量 */ .footer { --footer-color: green; }
:root
声明的是全局变量
,如果是一个自定义属性
用--
作为前缀,使用时比如: var(–main-color)就和red相等。好处:
- 可维护性
如果没有CSS变量需要手动改变大量的属性值,使用批量处理查找和替换,可能会影响其他样式规则。使用CSS变量只用改变定义时的值。- 提高CSS可读性
可以通过变量名判断属性内容注意事项:使用CSS变量要注意大小写敏感,不要把长度的变量用于颜色属性等。
2、实例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> /* 全局变量可以在整个文档进行访问使用 */ :root { --main-color: red; } /* 局部变量只能在声明它的选择器内部使用 */ .footer { --footer-color: green; } .main { color: var(--main-color); /* √ 生效 */ background-color: var(--footer-color); /* × 不生效 */ } .footer { color: var(--main-color); /* √ 生效 */ background-color: var(--footer-color); /* √ 生效 */ } </style> </head> <body> <div class="main"> main </div> <br /> <div class="footer"> footer </div> </body> </html>
5、状态管理
用户状态仓库
完成:用户信息仓库创建,提供用户信息,修改用信息,删除用户信息的方法
- 请求工具需要携带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 { ref } from 'vue'
import { defineStore } from 'pinia'
import type { User } from '@/types/user'
export const useUserStore = defineStore('cp-user', () => {
// 1.用户信息状态
const user = ref<User>()
// 2.设置用户信息函数
const setUser = (u: User) => {
user.value = u
}
// 3.删除用户信息的函数
const delUser = () => {
user.value = undefined
}
return {
user,
setUser,
delUser
}
})
测试App.vue
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
import type { User } from './types/user'
const store = useUserStore()
const login = () => {
const user: User = {
token: 'xxx',
id: '1',
account: 'xxx',
mobile: 'xxx',
avatar: 'xxx'
}
store.setUser(user)
}
const logout = () => {
store.delUser()
}
</script>
<template>
<div>
{{ store.user }}
<van-button type="primary" @click="login">登录</van-button>
<van-button type="primary" @click="logout">退出</van-button>
</div>
</template>
<style scoped lang="scss"></style>
小结:
- pinia存储这个数据的意义?
- 数据共享,提供给项目中任何位置使用
- 如果存储了数据,刷新页面后数据还在吗?
- 不在,现在仅仅是js内存中,需要进行本地存储(持久化)
数据持久化
掌握:使用
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 { ref } from 'vue' import { defineStore } from 'pinia' import type { User } from '@/types/user' export const useUserStore = defineStore( 'cp-user', () => { // 1.用户信息状态 const user = ref<User>() // 2.设置用户信息函数 const setUser = (u: User) => { user.value = u } // 3.删除用户信息的函数 const delUser = () => { user.value = undefined } return { user, setUser, delUser } }, { // 开启本地持久化 persist: true } )
-
测试
App.vue
<script setup lang="ts"> import { useUserStore } from '@/stores/user' import type { User } from './types/user' const store = useUserStore() const login = () => { const user: User = { token: 'xxx', id: '1', account: 'xxx', mobile: 'xxx', avatar: 'xxx' } store.setUser(user) } const logout = () => { store.delUser() } </script> <template> <div> {{ store.user }} <van-button type="primary" @click="login">登录</van-button> <van-button type="primary" @click="logout">退出</van-button> </div> </template> <style scoped lang="scss"></style>
stores统一导出
如何统一管理?
-
pinia 独立维护
-
现在:初始化代码在 main.ts 中,仓库代码在 stores中,代码分散职能不单一
-
优化:由 stores 统一维护,
在 stores/index.ts 中完成 pinia 初始化,交付 main.ts 使用
-
-
仓库 统一导出
-
现在:使用一个仓库 import { useUserStore } from ./stores/user.ts 不同仓库路径不一致
-
优化:由
stores/index.ts 统一导出,导入路径统一 ./stores
,而且仓库维护在 stores/modules 中
-
抽取pinia实例代码,职能单一
stores/index
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'
// 创建pinia实例
const pinia = createPinia()
// 使用pinia插件
pinia.use(persist)
// 导出pinia实例,给main使用
export default pinia
main.ts
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')
统一导出,代码简洁,入口唯一
stores/index
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'
// 创建pinia实例
const pinia = createPinia()
// 使用pinia插件
pinia.use(persist)
// 导出pinia实例,给main使用
export default pinia
// import { useUserStore } from './modules/user'
// export { useUserStore }
export * from './modules/user'
App.vue
-import { useUserStore } from './stores/user'
+import { useUserStore } from './stores'
6、数据交互
请求工具
axios配置
创建axios实例
基准地址,超时时间
请求拦截器
携带 token
响应拦截器
业务失败处理,摘取核心响应数据,401处理
业务失败处理&摘取核心响应数据
业务失败处理:
- 如何判断业务失败?code 不是 10000
- 失败后需要做什么?弹出轻提示,此时返回一个失败的 promise,传递code给catch
摘取核心响应数据:
- 现在结果 ===> { data: 响应数据 }
- 期望结果 ===> 响应数据
响应失败-token无效401处理
代码
实现:token请求头携带,错误响应处理,摘取核心响应数据, 401错误处理
utils/request.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
代码实现:
import { useUserStore } from '@/stores'
import axios, { AxiosError } from 'axios'
import { useRouter } from 'vue-router'
import { showToast } from 'vant'
import 'vant/es/toast/style'
const instance = axios.create({
// 1. 基础地址,超时时间
baseURL: 'https://consult-api.itheima.net/',
timeout: 10000
})
instance.interceptors.request.use(
(config) => {
// 2. 携带token
const store = useUserStore()
if (store.user?.token && config.headers) {
config.headers.Authorization = `Bearer ${store.user.token}`
}
return config
},
(err) => Promise.reject(err)
)
instance.interceptors.response.use(
(res) => {
// 3. 处理业务失败
// 后台约定,响应成功,但是code不是10000,是业务逻辑失败
if (res.data.code !== 10000) {
// 错误提示
showToast(res.data.message || '业务失败')
// 返回错误的promise
// 传入code将来catch的时候可以使用(res.data包含code)
return Promise.reject(res.data)
}
// 4. 摘取核心响应数据
return res.data
},
(err: AxiosError) => {
// 5. 处理401错误
if (err.response?.status === 401) {
// 删除用户信息
const store = useUserStore()
store.delUser()
// 跳转登录,带上接口失效所在页面的地址,登录完成后回跳使用
const router = useRouter()
router.push({
path: '/login',
query: { returnUrl: router.currentRoute.value.fullPath }
})
}
return Promise.reject(err)
}
)
export default instance
请求函数
函数封装
简化传参逻辑
添加类型
设置响应数据类型
扩展:为什么使用axios.request()泛型第二个参数?
代码
实现:导出一个通用的请求工具函数,支持设置响应数据类型
utils/request.ts
import { useUserStore } from '@/stores'
import axios, { AxiosError, type Method } from 'axios'
import { showToast } from 'vant'
import 'vant/es/toast/style'
import { useRouter } from 'vue-router'
const instance = axios.create({
// TODO 1. 基础地址,超时时间
baseURL: 'https://consult-api.itheima.net/',
timeout: 10000
})
instance.interceptors.request.use(
(config) => {
// TODO 2. 携带token
const userStore = useUserStore()
if (userStore.user?.token) {
config.headers.Authorization = `Bearer ${userStore.user.token}`
}
return config
},
(err) => Promise.reject(err)
)
instance.interceptors.response.use(
(res) => {
// TODO 3. 处理业务失败
// 后台约定,响应成功,但是code不是10000,是业务逻辑失败
if (res.data.code !== 10000) {
// 错误提示
showToast(res.data.message || '业务失败')
// 返回错误的promise
// 传入code将来catch的时候可以使用(res.data包含code)
return Promise.reject(res.data)
}
// TODO 4. 摘取核心响应数据
return res.data
},
(err: AxiosError) => {
// TODO 5. 处理401错误
// 5. 处理401错误
if (err.response?.status === 401) {
// 删除用户信息
const store = useUserStore()
store.delUser()
// 跳转登录,带上接口失效所在页面的地址,登录完成后回跳使用
const router = useRouter()
router.push({
path: '/login',
query: { returnUrl: router.currentRoute.value.fullPath }
})
}
return Promise.reject(err)
}
)
export default instance
// 请求工具函数
type Data<T> = {
code: number
message: string
data: T
}
export const request = <T>(
url: string,
method: Method = 'GET',
submitData?: object
) => {
// 参数:地址,请求方式,提交的数据
// 返回:promise
return instance.request<any, Data<T>>({
url,
method,
[method.toUpperCase() === 'GET' ? 'params' : 'data']: submitData
})
}
测试:
<script setup lang="ts">
import { request } from '@/utils/request'
import type { User } from './types/user'
import { useUserStore } from './stores'
const store = useUserStore()
const login = async () => {
const res = await request<User>('/login/password', 'POST', {
mobile: '13211112222',
password: 'abc12345'
})
store.setUser(res.data)
}
</script>
<template>
<van-button type="primary" @click="login">登录</van-button>
</template>