vue3+ts+vant4+pinia+axios+scss创建项目基本流程
自我记录
本质上他是一个包管理工具,和npm/yarn没有区别,主要优势在于
- 包安装速度极快
- 磁盘空间利用效率高
- 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 add webpack -D |
create-vue
脚手架创建项目create-vue参考地址:https://github.com/vuejs/create-vue
pnpm create vue
# or
npm init vue@latest
# or
yarn create vue
✔ Project name: … czbk-h5-test
✔ 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 an End-to-End Testing Solution? › `No`
✔ Add ESLint for code quality? … No / `Yes`
✔ Add Prettier for code formatting? … No / `Yes`
Scaffolding project in /Users/sougewang/test/test/czbk-h5-test...
Done. Now run:
cd czbk-h5-test
pnpm install
pnpm format
pnpm dev
1.项目名称
2.是否支持TS是
3.是否支持JSX否
4.是否用一个 路由 单文件包?是
5.是否使用 Pinia 进行状态管理是
6.是否需要单元测试否
7.是否需要 端对端 的测试否
8.是否需要 ESLint 的验证校验是
9.是否需要 Prettier 格式化工具是
如下图
正常一共是 5173
因为我已经打开过一个窗口了所以是5174
这个可以忽略 后期也可以配置
说明:后续以czbk-h5
为例5174
是czbk-h5-test
都是一样的上面我是新走了一下创建流程用来截图
extensions.json
里面的必装Ps
: 如果vscode没有安装过,项目打开的时候右下角会有提示安装
注意
:安装过 Prettier
要禁用掉避免和项目里面的 eslint
冲突。
了解
:大中型项目建议开启 TS托管模式 , 更好更快的类型提示。
使用:eslint的预制配置,且了解配置作用(每个公司都不一样看个人)
常见规则 https://prettier.io/docs/en/options.html
在.eslintrc.cjs
添加
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
'prettier/prettier': [
// 格式:单引号,没有分号,行宽度100字符,没有对象数组最后一个逗号,换行字符串自动(系统不一样换行符号不一样)
'warn',
{
singleQuote: true,
semi: false,
printWidth: 80,
trailingComma: 'none',
endOfLine: 'auto'
}
],
'vue/multi-word-component-names': [
//vue 组件需要大驼峰命名,除去 index 之外,App 是默认支持的
'warn',
{
ignores: ['index']
}
],
'vue/no-setup-props-destructure': ['off'] //允许对 props 进行解构,我们会开启解构保持响应式的语法糖
}
}
项目文件会有很多报错目前不用管
执行命令所有文件修复格式或者每个文件自己去保存一下也可以
pnpm format
or
pnpm lint
./src
├── assets `静态资源,图片...`
├── components `通用组件`
├── composable `组合功能通用函数`
├── icons `svg图标`
├── router `路由`
│ └── index.ts
├── services `接口服务API`
├── stores `状态仓库`
├── styles `样式`
│ └── main.scss
├── types `TS类型`
├── utils `工具函数`
├── views `页面`
├── main.ts `入口文件`
└── App.vue `根组件`
import { createRouter, createWebHistory } from 'vue-router'
// vue2的路由
// 1.import VueRouter from 'vue-router'
// 2.const router = new VueRouter({ routes: [ // 路由规则 ] })
// 3.选择路由模式 hash /#/user history /user
// 现在vue3的路由
// 1.创建路由实例 createRouter({ //配置对象 })
// 2.配置选项中 routes: [ // 路由规则 ]
// 3.createWebHistory 使用路由history 模式
// 4.createWebHashHistory(), //使用路由hash模式
// 5.import.meta.env.BASE_URL 路由的基准路由 create-vue 脚手架提供的环境变量 ps:在vite.config.ts 文件可以配置base: '/',
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: []
})
export default router
NProgress
请看 另一篇文章 主要就是加载页面的时候实现进度条功能
路由前置首位 后置首位 以及修改 标题
import { useUserStore } from '@/stores'
import { createRouter, createWebHistory } from 'vue-router'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
component: () => import('@/views/Login/index.vue'),
meta: { title: '登录' }
},
{
path: '/',
redirect: '/home',
component: () => import('@/views/Layout/index.vue'),
children: [
{
path: '/home',
component: () => import('@/views/Home/index.vue'),
meta: { title: '首页' }
},
{
path: '/user',
component: () => import('@/views/User/index.vue'),
meta: { title: '我的' }
}
]
}
]
})
// 修改进度条插件的配置
NProgress.configure({
showSpinner: false
})
// 前置首位 访问权限控制
router.beforeEach((to) => {
// 开启页面进度条
NProgress.start()
// 用户仓库
const store = useUserStore()
// 用户白名单
const wihteList = ['/login']
// 没有token 并且 不再白名单 则跳转登录页
if (!store.user?.token && !wihteList.includes(to.path)) return '/login'
// 放行 return true 可以不用写
})
// 后置守卫
router.afterEach((to) => {
// 设置页面标题
document.title = to.meta.title || '奔跑的代码!'
NProgress.done()
})
export default router
详情请见另一篇文章:Vue3+Pinia+数据持久化 20分钟快速上手
项目使用sass预处理器,安装sass,即可支持scss语法:
pnpm add sass -D
文件使用:
# Vue 3 项目,安装最新版 Vant
npm i vant
# 通过 yarn 安装
yarn add vant
# 通过 pnpm 安装
pnpm add vant
src/main.ts
注意全局样式文件要在下方,这样方便后期覆盖vant组件样式
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
// 引入 pinia
import pinia from './stores'
// 样式全局使用
import 'vant/lib/index.css'
import './styles/main.scss'
const app = createApp(App)
// vue使用pinia插件,use(pinia的插件)
app.use(pinia)
app.use(router)
app.mount('#app')
组件按需手动引入测试是否可用
<script setup lang="ts">
// 组件按需手动引入测试是否可用
import { Button as VanButton } from 'vant'
</script>
<template>
<van-button>按钮</van-button>
</template>
<style scoped lang="scss"></style>
postcss-px-to-viewport
npm install postcss-px-to-viewport -D
# or
yarn add -D postcss-px-to-viewport
# or
pnpm add -D postcss-px-to-viewport
报错:[vite] Internal server error: Failed to load PostCSS config
解决方案:将postcss.config.js
文件名改为 postcss.config.cjs
也就是更改.js
文件后缀为.cjs
解决方案:可忽略,或者使用 postcss-px-to-viewport-8-plugin
代替当前插件
手动按需使用组件比较麻烦,需要先导入。配置函数自动按需导入后直接使用即可。 官方文档
# 通过 npm 安装
npm i unplugin-vue-components -D
# 通过 yarn 安装
yarn add unplugin-vue-components -D
# 通过 pnpm 安装
pnpm add unplugin-vue-components -D
vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// 引入按需导入插件
import Components from 'unplugin-vue-components/vite'
// 各种组件库的解析器 //里面还有 ElementUiResolver,ElementPlusResolverOptions,AntDesignVueResolver等常见的组件库
import { VantResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig({
// 默认'/' 是基准地址
base: '/',
plugins: [
// 解析单文件组件的插件
vue(),
//使用按需导入插件 默认自动加载 components 下的组件,通用级别组件.
Components({
// 默认是true 开启自动生成组件的类型声明文件,vant的组件已经有类型声明文件,只要导入了就会使用类型声明.
dts: false,
// 在main.ts 已经引入了所有的组件样式,不需要自动导入样式,只需要自动导入组件即可
resolvers: [VantResolver({ importStyle: false })]
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
使用的时候 就不需要在引入了
<script setup lang="ts"></script>
<template>
<van-button>按钮</van-button>
</template>
<style scoped lang="scss"></style>
src/App.vue
默认样式
使用css变量定制项目主题,和修改vant主题 官方文档
src/styles/main.scss
// 项目共用的样式文件
:root {
--cz-primary: #16C2A3;
--cz-text1: red;
--cz-text2: pink;
// 覆盖vant主体色
--van-primary-color: var(--cz-primary);
}
src/App.vue
使用
<script setup lang="ts">
import { Button as VanButton } from 'vant'
</script>
<template>
<!-- 验证vant颜色被覆盖 -->
<van-button type="primary">按钮</van-button>
<a href="#">测试</a>
</template>
<style scoped lang="scss">
// 使用css全局变量
a {
color: var(--cz-text1);
}
</style>
src/components/CpNavBar.vue
<script setup lang="ts">
import { useRouter } from 'vue-router'
// 拿路由实例
const router = useRouter()
const props = defineProps<{
title?: string
rightText?: string
back?: () => void
}>()
const emit = defineEmits<{ (e: 'click-right'): void }>()
const onClickLeft = () => {
// 扩展 back 属性,如果有就执行 back 对应的函数。
// 给全屏弹出层使用
if (props.back) {
return props.back()
}
// TODO 点击左侧返回按钮
// 实现返回 如果有当前网站的上一次历史记录 就执行back返回
// 没有历史记录则跳转到 首页
if (history.state?.back) {
router.back()
} else {
router.push('/home')// 为了测试功能时否正常 后续需要换成 '/'
}
}
const onClickRight = () => {
// TODO 点击右侧文字按钮
emit('click-right')
}
</script>
<template>
<van-nav-bar
left-arrow
fixed
:title="title"
:right-text="rightText"
@click-left="onClickLeft"
@click-right="onClickRight"
/>
</template>
<style lang="scss" scoped>
:deep() {
.van-nav-bar {
&__arrow {
font-size: 18px;
color: var(--cz-text1);
}
&__text {
font-size: 15px;
}
}
}
</style
使用
src/views/Login/index.vue
之所以不用引入组件 就可以使用是因为
4. 自动按需加载组件
里面的配置项 可以回去看看 有注释
<script setup lang="ts">
const fn = () => {
console.log('点击了右侧')
}
</script>
<template>
<div class="login-page">
<cp-nav-bar title="登录" right-text="注册" @click-right="fn" />
</div>
</template>
<style lang="scss" scoped>
.login {
&-page {
padding-top: 46px;
}
}
</style>
给组件添加类型,让写属性和事件可以有提示(TS可以去识别) 这里也是模仿Vant组件的源码写法
src/types/components.d.ts
// 给components 下的全局组件设置类型
// 1. 导入组件实例
import CpNavBar from '@/components/CpNavBar.vue'
// 2. 声明 vue 类型模块
declare module 'vue' {
// 3. 给 vue 添加全局组件类型,interface 和之前的合并
interface GlobalComponents {
// 指定组件类型,typeof(从一个JS对象中得到他对应的TS类型) 从组件对象得到类型,设置给全局组件 CpNavBar
CpNavBar: typeof CpNavBar
}
}
1.中文文档
2.github文档
npm install axios
or
yarn add axios
or
pnpm add axios
src/utils/rquest.ts
// 二次封装axios
import router from '@/router'
import { useUserStore } from '@/stores'
import axios, { type Method } from 'axios'
import { showToast } from 'vant'
// 1. axios的配置
// 1.1 创建一个新的axios实例,配置基准地址,配置响应超时时间
// 1.2 添加请求拦截器,在请求头携带token
// 1.3 添加响应拦截器,判断业务是否成功,剥离无效的数据,401错误拦截去登录页面(删除当前用户信息),
const baseURL = 'xxxxxxxxx'
const instance = axios.create({
baseURL,
timeout: 10000
})
// 请求拦截器
instance.interceptors.request.use(
(config) => {
// 修改config,比如:修改请求头
// 获取token===>就是获取user
const store = useUserStore()
if (store.user?.token && config.headers) {
config.headers.Authorization = `Bearer ${store.user.token}`
}
return config
},
(err) => Promise.reject(err)
)
// 响应拦截器
// 将来 axios.get()
// .then(res=>{ // res 就是后台的数据,之前的res.data })
// .catch(e=>{ // 200+10001这种情况,e就是res.data , 如果是状态吗的错误 401 403 404 e 就错误对象 })
instance.interceptors.response.use(
(res) => {
// status 是200是响应成功的,res.data.code 是10000业务成功
// 如果不是 10000 呢,使用 vant 的轻提示,报错阻断程序
if (res.data.code !== 10000) {
showToast(res.data.message || '网络异常')
return Promise.reject(res.data)
}
// 剥离无效数据
return res.data
},
// 401处理
(err) => {
// 请求报错,响应出错
// 遇见401跳转登录
// 1. 现在在 /user/patient 页面下,发起一个获取用户信息的请求,但是此时token失效
// 2. 跳转登录页面,登录成功之后,需要跳转回 /user/patient 页面 (默认跳转 /user 首页)
// vue2 $router 路由实例,提供路由相关函数操作 $route 路由相关信息,query params path 。。。
if (err.response.status === 401) {
// 需要删除用户信息
const store = useUserStore()
store.delUser()
// /user/patient?id=1000
// path /user/patient 不带查询参数
// fullPath /user/patient?id=1000 完整路径
// currentRoute 是一个 ref 创建的数据,需要.value
router.push(`/login?returnUrl=${router.currentRoute.value.fullPath}`)
}
return Promise.reject(err)
}
)
// obj = { name: 'jack', age: 100 } obj['name'] ===> const name = 'name' obj[name]
// 2. 请求工具函数
// 2.1 参数:url method submitData
// 2.2 返回:instance 调用接口的promise对象
// const request = (url: string, method: string, submitData?: object) => {
// return instance.request({
// url,
// method,
// // 区分get和其他请求post
// // get 提交数据,选项:params
// // 其他请求post 提交数据,选项:data
// [method.toLowerCase() === 'get' ? 'params' : 'data']: submitData
// })
// }
// res响应数据类型
type Data<T> = {
code: string
message: string
data: T
}
const request = <T>(
url: string,
method: Method = 'get', // 默认是get
submitData?: object // 可以不传
) => {
// 泛型的第二个参数,可以自定义响应数据类型
return instance.request<T, Data<T>>({
url,
method,
// 区分get和其他请求post
// get 提交数据,选项:params
// 其他请求post 提交数据,选项:data // toLowerCase 将字符串转换为小写字母
[method.toLowerCase() === 'get' ? 'params' : 'data']: submitData
})
}
// baseURL 基准地址
// instance 是配置好的axios
// request 用来调用接口
export { baseURL, instance, request }
简单使用测试src/App.vue
<script setup lang="ts">
import { useUserStore } from './stores'
import type { User } from './types/user'
import { request } from './utils/rquest'
const store = useUserStore()
const login = () => {
// 接口地址(去除基准路径之后的地址), 请求方式 , 请求参数
request<User>('login/password', 'POST', {
mobile: '12345678900',
password: '111111'
})
.then((res) => {
// 存储到本地
store.setUser(res.data)
})
.catch((e) => {
// 异常捕获
console.log(e)
})
}
</script>
<template>
<van-button type="primary" @click="login">登录</van-button>
</template>
实现:根据 icons 文件svg图片打包到项目中,通过组件使用图标
参考文档:https://github.com/vbenjs/vite-plugin-svg-icons
icons文件打包的产物?会生成一个 svg 结构(js创建的)包含所有图标,理解为 精灵图
安装
yarn add vite-plugin-svg-icons -D
# or
npm i vite-plugin-svg-icons -D
# or
pnpm install vite-plugin-svg-icons -D
配置 打包svg图标 vite.congfig.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// 引入按需导入插件
import Components from 'unplugin-vue-components/vite'
// 各种组件库的解析器 //里面还有 ElementUiResolver,ElementPlusResolverOptions,AntDesignVueResolver 等常见的组件库
import { VantResolver } from 'unplugin-vue-components/resolvers'
// 配置svg精灵图插件
+import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
+import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
// 默认'/' 是基准地址
base: '/',
plugins: [
// 解析单文件组件的插件
vue(),
//使用按需导入插件 默认自动加载 components 下的组件,通用级别组件.
Components({
// 默认是true 开启自动生成组件的类型声明文件,vant的组件已经有类型声明文件,只要导入了就会使用类型声明.
dts: false,
// 在main.ts 已经引入了所有的组件样式,不需要自动导入样式,只需要自动导入组件即可
resolvers: [VantResolver({ importStyle: false })]
}),
+ // 打包svg图标目录
+ createSvgIconsPlugin({
+ // 指定需要缓存的图标文件夹 (打包指定svg图标的目录) //路径转换为绝对路径
+ iconDirs: [path.resolve(process.cwd(), 'src/icons')]
+ })
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
在 src/main.ts
引用
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
// 引入 pinia
import pinia from './stores'
// 使用svg精灵图
+import 'virtual:svg-icons-register'
// 样式全局使用
import 'vant/lib/index.css'
import './styles/main.scss'
const app = createApp(App)
// vue使用pinia插件,use(pinia的插件)
app.use(pinia)
app.use(router)
app.mount('#app')
在src/views/Login/index.vue
例如: src/icons/user/add.svg
'icon-[dir]-[name]'
#
是固定的icon-
固定开头dir
图标所在目录name
图标的名称
<script setup lang="ts">
const fn = () => {
console.log('点击了右侧')
}
</script>
<template>
<div class="login-page">
<cp-nav-bar title="登录" right-text="注册" @click-right="fn" />
<!-- 测试svg -->
<svg aria-hidden="true">
<!-- src/icons/user/add.svg -->
<!-- 'icon-[dir]-[name]' # 是固定的 icon- 固定开头 dir 图标所在目录 name 图标的名称 -->
<use href="#icon-user-add" />
</svg>
</div>
</template>
<style lang="scss" scoped>
.login {
&-page {
padding-top: 46px;
}
}
</style>
创建 src/components/CpIcon.vue
<script setup lang="ts">
// 提供一个props 属性 确定使用哪个图标 使用的规则: 文件夹名称+图片名称 user-add
defineProps<{ name: string }>()
</script>
<template>
<svg aria-hidden="true" class="cp-icon">
<!-- src/icons/user/add.svg -->
<!-- 'icon-[dir]-[name]' # 是固定的 icon- 固定开头 dir 图标所在目录 name 图标的名称 #icon-user-add -->
<use :href="`#icon-${name}`" />
</svg>
</template>
<style lang="scss" scoped>
.cp-icon {
// 通过font-size 控制图片的大小(和字体一样大)
width: 1em;
height: 1em;
}
</style>
定义类型 src/types/components.d.ts
// 给components 下的全局组件设置类型
// 1. 导入组件实例
import CpNavBar from '@/components/CpNavBar.vue'
+import CpIcon from '@/components/CpIcon.vue'
// 2. 声明 vue 类型模块
declare module 'vue' {
// 3. 给 vue 添加全局组件类型,interface 和之前的合并
interface GlobalComponents {
// 指定组件类型,typeof(从一个JS对象中得到他对应的TS类型) 从组件对象得到类型,设置给全局组件 CpNavBar
CpNavBar: typeof CpNavBar
+ CpIcon: typeof CpIcon
}
}
使用 src/views/Login/index.vue
效果
有些图标可以根据 style 中 color 的值来设置颜色,图标是否有这个功能取决于 UI 做图片时否开启。
后续持续更新!