介绍
一个使用Vite2 + Vue3 + pinia + TypeScript + ElementPlus 完整技术路线开发的项目。
特性
-
✨脚手架工具:高效、快速的 Vite
-
🔥前端框架:眼下最时髦的 Vue3
-
🍍状态管理器:
vue3新秀 Pinia,犹如react zustand般的体验,友好的api和异步处理 -
🏆开发语言:政治正确 TypeScript
-
🎉UI组件:
ElementUI开发者无障碍过渡使用 ElementPlus,熟悉的配方熟悉的味道 -
🎨css样式:scss 、
postcss -
📖代码规范:Eslint、Commitlint
-
🔒权限管理:页面级、菜单级、按钮级、接口级
-
✊依赖按需加载:unplugin-auto-import,可自动导入使用到的
vue、vue-router等依赖 -
💪组件按需导入:unplugin-vue-components,无论是第三方UI组件还是自定义组件都可实现自动按需导入以及
TS语法提示
使用Vite快速创建脚手架
yarn create vite vue-data-report --template vue-ts
约束代码风格
vscode中settings.json中配置
// .vscode/settings.json
{
// 保存时 prettier 自动格式化
"editor.formatOnSave": true,
// 保存时自动启用 eslint --fix 自动修复
"editor.codeActionsOnSave": {
"source.fixAll": true
}
}
安装Eslint依赖
yarn add eslint eslint-plugin-vue @typescript-eslint/eslint-plugin @typescript-eslint/parser -D
安装prettier
yarn add eslint-config-prettier eslint-plugin-prettier prettier prettier-eslint -D
创建.prettierrc.js
module.exports = {
semi: false,
singleQuote: true,
trailingComma: 'none',
endOfLine: 'lf'
}
项目创建.eslintrc.js
npx eslint --init
修改.eslintrc.js配置
module.exports = {
root: true,
env: {
browser: true,
node: true,
es2021: true,
},
parser: 'vue-eslint-parser',
extends: [
'eslint:recommended',
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended'
],
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
// eslint-plugin-vue @typescript-eslint/eslint-plugin eslint-plugin-prettier的缩写
plugins: ['vue', '@typescript-eslint'],
rules: {
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'no-var': 'error',
'prettier/prettier': 'error',
// 禁止出现console
'no-console': 'warn',
// 禁用debugger
'no-debugger': 'warn',
// 禁止出现重复的 case 标签
'no-duplicate-case': 'warn',
// 禁止出现空语句块
'no-empty': 'warn',
// 禁止不必要的括号
'no-extra-parens': 'off',
// 禁止对 function 声明重新赋值
'no-func-assign': 'warn',
// 禁止在 return、throw、continue 和 break 语句之后出现不可达代码
'no-unreachable': 'warn',
// 强制所有控制语句使用一致的括号风格
curly: 'warn',
// 要求 switch 语句中有 default 分支
'default-case': 'warn',
// 强制尽可能地使用点号
'dot-notation': 'warn',
// 要求使用 === 和 !==
eqeqeq: 'warn',
// 禁止 if 语句中 return 语句之后有 else 块
'no-else-return': 'warn',
// 禁止出现空函数
'no-empty-function': 'warn',
// 禁用不必要的嵌套块
'no-lone-blocks': 'warn',
// 禁止使用多个空格
'no-multi-spaces': 'warn',
// 禁止多次声明同一变量
'no-redeclare': 'warn',
// 禁止在 return 语句中使用赋值语句
'no-return-assign': 'warn',
// 禁用不必要的 return await
'no-return-await': 'warn',
// 禁止自我赋值
'no-self-assign': 'warn',
// 禁止自身比较
'no-self-compare': 'warn',
// 禁止不必要的 catch 子句
'no-useless-catch': 'warn',
// 禁止多余的 return 语句
'no-useless-return': 'warn',
// 禁止变量声明与外层作用域的变量同名
'no-shadow': 'off',
// 允许delete变量
'no-delete-var': 'off',
// 强制数组方括号中使用一致的空格
'array-bracket-spacing': 'warn',
// 强制在代码块中使用一致的大括号风格
'brace-style': 'warn',
// 强制使用骆驼拼写法命名约定
camelcase: 'warn',
// 强制使用一致的缩进
indent: 'off',
// 强制在 JSX 属性中一致地使用双引号或单引号
// 'jsx-quotes': 'warn',
// 强制可嵌套的块的最大深度4
'max-depth': 'warn',
// 强制最大行数 300
// "max-lines": ["warn", { "max": 1200 }],
// 强制函数最大代码行数 50
// 'max-lines-per-function': ['warn', { max: 70 }],
// 强制函数块最多允许的的语句数量20
'max-statements': ['warn', 100],
// 强制回调函数最大嵌套深度
'max-nested-callbacks': ['warn', 3],
// 强制函数定义中最多允许的参数数量
'max-params': ['warn', 3],
// 强制每一行中所允许的最大语句数量
'max-statements-per-line': ['warn', { max: 1 }],
// 要求方法链中每个调用都有一个换行符
'newline-per-chained-call': ['warn', { ignoreChainWithDepth: 3 }],
// 禁止 if 作为唯一的语句出现在 else 语句中
'no-lonely-if': 'warn',
// 禁止空格和 tab 的混合缩进
'no-mixed-spaces-and-tabs': 'warn',
// 禁止出现多行空行
'no-multiple-empty-lines': 'warn',
// 禁止出现;
semi: ['warn', 'never'],
// 强制在块之前使用一致的空格
'space-before-blocks': 'warn',
// 强制在 function的左括号之前使用一致的空格
// 'space-before-function-paren': ['warn', 'never'],
// 强制在圆括号内使用一致的空格
'space-in-parens': 'warn',
// 要求操作符周围有空格
'space-infix-ops': 'warn',
// 强制在一元操作符前后使用一致的空格
'space-unary-ops': 'warn',
// 强制在注释中 // 或 /* 使用一致的空格
// "spaced-comment": "warn",
// 强制在 switch 的冒号左右有空格
'switch-colon-spacing': 'warn',
// 强制箭头函数的箭头前后使用一致的空格
'arrow-spacing': 'warn',
'prefer-const': 'warn',
'prefer-rest-params': 'warn',
'no-useless-escape': 'warn',
'no-irregular-whitespace': 'warn',
'no-prototype-builtins': 'warn',
'no-fallthrough': 'warn',
'no-extra-boolean-cast': 'warn',
'no-case-declarations': 'warn',
'no-async-promise-executor': 'warn',
},
globals: {
defineProps: 'readonly',
defineEmits: 'readonly',
defineExpose: 'readonly',
withDefaults: 'readonly',
},
}
项目新建.eslintignore
# eslint 忽略检查 (根据项目需要自行添加)
node_modules
dist
有的同学可能在项目中同时使用了eslint和pretttier,结果这两个工具相互冲突。
冲突原因:
vscode的eslint和prettier插件,都开启了自动格式化和自动修复代码的功能。
解决格式化冲突的办法
配置 husky + lint-staged
使用
husky+lint-staged助力团队编码规范, husky&lint-staged 安装推荐使用mrm, 它将根据package.json依赖项中的代码质量工具来安装和配置 husky 和 lint-staged,因此请确保在此之前安装并配置所有代码质量工具,如Prettier 和 ESlint
安装husky + lint-staged
yarn add lint-staged husky -D
创建.husky目录并指定该目录为git hooks所在目录
在package.json中添加prepare脚本
{
"scripts": {
"prepare": "husky install",
"lint": "eslint src --fix --ext .ts,.tsx,.vue,.js,.jsx"
}
}
通过yarn生成.husky目录
yarn
注意:如果没有生成.husky目录,并报如下错误:
fatal: not a git repository (or any of the parent directories): .git Done in 0.50s.那是因为没有生成.git文件,可以通过git init生成.git文件
添加git hooks
创建一条pre-commit hook
npx husky add .husky/pre-commit "npm run lint"
执行该命令后,会看到.husky目录下下新增了一个名为pre-commit的shell脚本。
这样,在之后执行git commit命令时会先触发pre-commit这个脚本。
pre-commit脚本内容如下:
#!/bin/sh . "$(dirname "$0")/_/husky.sh" npm run lint
注意:npm run lint命令根据你自己项目中script脚本而定,eslint --ext .js,.vue src在lint脚本中
commitlint安装与配置
yarn add @commitlint/cli @commitlint/config-conventional -D
规范commit message信息
类似的,我们也可以添加commit-msg钩子,来规范我们的commit message信息。
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'
commitlint.config.js
module.exports = {
extends: [ '@commitlint/config-conventional' ],
rules: {
'type-enum': [2, 'always', [ 'build', 'ci', 'perf', 'feat', 'fix', 'refactor', 'docs', 'chore', 'style', 'revert', 'test' ]],
'type-case': [0], 'type-empty': [0], 'scope-empty': [0], 'scope-case': [0], 'subject-full-stop': [0], 'subject-case': [0, 'never'], 'header-max-length': [0, 'always', 72]
}
}
配置文件引用别名alias
直接修改
vite.config.ts文件配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import {resolve} from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
}
})
注意: 如果项目报
找不到模块“vite”或其相应的类型声明。ts(2307),我也不知道怎么解决,哪位大佬指点一下呢?
Vue能力的支持
模板语法配合JSX语法,使用起来非常方便、灵活
一些必须的插件
{
"@vitejs/plugin-legacy": "^1.6.2", // 低版本浏览器兼容
"@vitejs/plugin-vue": "^1.9.3", // vue 支持
"@vitejs/plugin-vue-jsx": "^1.2.0", // jsx 支持
}
引入jsx
yarn add @vitejs/plugin-vue-jsx -D
// vite.config.js
import vueJsx from '@vitejs/plugin-vue-jsx'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vueJsx({
// options are passed on to @vue/babel-plugin-jsx
})
]
})
如果我们想在.vue文件中写jsx语法,我们可以通过修改script中的lang属性为jsx或者tsx
<script lang="tsx">
import {defineComponent} from 'vue'
export default defineComponent({
render() {
return <div>Hello</div>
}
})
</script>
<style lang="scss" scoped>
// 书写样式
</style>
为打包后的文件提供传统浏览器兼容性支持
yarn add @vitejs/plugin-legacy -D
// vite.config.js
import legacy from '@vitejs/plugin-legacy'
export default {
plugins: [
legacy({
targets: ['defaults', 'not IE 11']
})
]
}
引入ElementPlus
安装依赖
yarn add element-plus
首先你需要安装unplugin-vue-components 和 unplugin-auto-import这两款插件。
yarn add unplugin-vue-components unplugin-auto-import -D
修改配置
// vite.config.ts
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default {
plugins: [
// ...
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
}
引入tailwindcss
yarn add -D tailwindcss@latest postcss@latest autoprefixer@latest @fullhuman/postcss-purgecss
配置postcss.config.js
const purgecss = require('@fullhuman/postcss-purgecss')({
content: ['./src/**/*.html', './src/**/*.vue', './src/**/*.jsx'],
defaultExtractor: (content) => content.match(/[\w-/:]+(?<!:)/g) || []
})
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
...(process.env.NODE_ENV === 'production' ? [purgecss] : [])
]
}
配置tailwind.config.js
module.exports = {
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
content: [],
theme: {
extend: {}
},
plugins: []
}
引入tailwindcss
新建一个tailwind.css
@tailwind base;
@tailwind components;
@tailwind utilities;
在main.ts中引入:
import './style/tailwind.css'
引入scss
yarn add sass -D
如何在Vue组件中使用tailwindcss
<script setup lang="ts">
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
</script>
<template>
<div class="app">
<div class="h-12 w-screen bg-black"></div>
<div class="h-12 w-screen bg-white flex items-center px-4">
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }"
>统一登录平台</el-breadcrumb-item
>
<el-breadcrumb-item><a href="/">商户管理平台</a></el-breadcrumb-item>
</el-breadcrumb>
</div>
<!-- 项目情况, 数据指标 -->
<div class="container mx-auto mt-2.5 flex">
<div class="h-48 w-5/12 bg-white"></div>
<div class="h-48 w-7/12 ml-2.5 bg-white"></div>
</div>
</div>
</template>
<style lang="scss" scoped>
.app {
// 通过apply引入tailwindcss
@apply bg-gray-100;
@apply h-screen;
}
</style>
引入SvgIcon
安装依赖vite-plugin-svg-icons
yarn add vite-plugin-svg-icons -D
配置插件
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'
export default () => {
return {
plugins: [
createSvgIconsPlugin({
// 指定需要缓存的图标文件夹
iconDirs: [path.resolve(process.cwd(), 'src/icons')],
// 指定symbolId格式
symbolId: 'icon-[dir]-[name]',
/**
* 自定义插入位置
* @default: body-last
'body-last' | 'body-first'
*/
inject: 'body-last',
/**
* custom dom id
* @default: __svg__icons__dom__
*/
customDomId: '__svg__icons__dom__',
}),
],
}
}
在src/main.ts内引入注册脚本。
import 'virtual:svg-icons-register'
创建SvgIcon组件
<template>
<svg aria-hidden="true" :width="size" :height="size">
<use :xlink:href="symbolId" :fill="color" />
</svg>
</template>
<script>
import { defineComponent, computed } from 'vue'
export default defineComponent({
name: 'SvgIcon',
props: {
prefix: {
type: String,
default: 'icon',
},
name: {
type: String,
required: true,
},
color: {
type: String,
default: '#333',
},
size: {
type: [String, Number],
default: 16
}
},
setup(props) {
const symbolId = computed(() => `#${props.prefix}-${props.name}`)
return { symbolId }
},
})
</script>
TypeScript支持
// tsconfig.json
{
"compilerOptions": {
"types": ["vite-plugin-svg-icons/client"]
}
}
配置css预处理器scss
安装
yarn add sass -D
配置全局scss样式文件
在src下新增style文件夹,用于存放全局样式文件。在style中新增一个variable.scss文件,设置一个用于测试的颜色变量:
$test-color: red;
如何将这个全局样式文件全局注入到项目中呢?配置 Vite 即可:
css:{
preprocessorOptions:{
scss:{
additionalData:'@import "@/assets/style/variable.scss";'
}
}
},
组件中使用
不需要任何引入可以直接使用全局
scss定义的变量
.test{
color: $test-color;
}
路由
安装
# 安装路由
yarn add vue-router@4
在src文件下新增router文件夹 => router.ts文件,内容如下:
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Login',
component: () => import('@/pages/login/Login.vue'), // 注意这里要带上 文件后缀.vue
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
export default router
统一请求封装
安装依赖
# 安装 axios
yarn add axios
# 安装 nprogress 用于请求 loading
# 也可以根据项目需求自定义其它 loading
yarn add nprogress
# 类型声明,或者添加一个包含 `declare module 'nprogress'
yarn add @types/nprogress --dev
新增utils文件夹,在utils下新增request.ts文件用于axios封装。
// 统一请求封装
import axios, { AxiosRequestConfig, AxiosInstance } from 'axios'
import NProgress from 'nprogress'
// 获取axios请求实例
const instance: AxiosInstance = axios.create({
baseURL: '/api',
timeout: 10000
})
// 请求拦截器
instance.interceptors.request.use(
(config): AxiosRequestConfig<any> => {
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
instance.interceptors.response.use(
(response) => {
NProgress.done()
const { data } = response
return data
},
(error) => {
NProgress.done()
return Promise.reject(error)
}
)
const request = Object.create(instance)
// 对get, post, put, delete等常用方法作了封装
request.get = function (url: string, data = {}, config = {}) {
NProgress.start()
return instance.get(url, { params: data, ...config })
}
request.post = function (url: string, data = {}, config = {}) {
NProgress.start()
return instance.post(url, data, config)
}
request.put = function (url: string, data = {}, config = {}) {
NProgress.start()
return instance.put(url, data, config)
}
request.delete = function (url: string, data = {}, config = {}) {
NProgress.start()
return instance.delete(url, { params: data, ...config })
}
export default request
状态管理pinia
安装pinia
# 安装
yarn add pinia@next
在main.ts中增加
// 引入
import { createPinia } from "pinia"
// 创建根存储库并将其传递给应用程序
app.use(createPinia())
创建store/main.ts
import { defineStore } from 'pinia'
export const useMainStore = defineStore({
id: 'main',
state: () =>({
name: '超级管理员'
})
})
组件中获取store
<template>
<div>{{mainStore.name}}</div>
</template>
<script setup lang="ts">
import { useMainStore } from "@/store/main"
const mainStore = useMainStore()
</script>
getters用法
Pinia中的getter与Vuex中的getter,组件中的计算属性具有相同的功能
import { defineStore } from 'pinia'
export const useMainStore = defineStore({
id: 'mian',
state: () => ({
name: '超级管理员',
}),
// getters
getters: {
nameLength: (state) => state.name.length,
}
})
在组件中的的使用:
<template>
<div>用户名:{{ mainStore.name }}<br />长度:{{ mainStore.nameLength }}</div>
<hr/>
<button @click="updateName">修改store中的name</button>
</template>
<script setup lang="ts">
import { useMainStore } from '@/store/mian'
const mainStore = useMainStore()
const updateName = ()=>{
// $patch 修改 store 中的数据
mainStore.$patch({
name: '名称被修改了,nameLength也随之改变了'
})
}
</script>
actions
这里与Vuex有极大的不同,Pinia仅提供了一种方法来定义如何更改状态的规则,放弃mutations只依靠Actions,这是一项重大的改变。
Pinia 让 Actions 更加的灵活:
- 可以通过组件或其他
action调用 - 可以从其他
store的action中调用 - 直接在
store实例上调用 - 支持
同步或异步 - 有任意数量的参数
- 可以包含有关如何更改状态的逻辑(也就是 vuex 的 mutations 的作用)
- 可以
$patch方法直接更改状态属性
import { defineStore } from 'pinia'
export const useMainStore = defineStore({
id: 'mian',
state: () => ({
name: '超级管理员',
}),
getters: {
nameLength: (state) => state.name.length,
},
actions: {
async insertPost(data:string){
// 可以做异步
// await doAjaxRequest(data);
this.name = data;
}
},
})
环境变量配置
Vite提供了两种模式:具有开发服务器的开发模式和生产模式
项目目录新建:.env.development
NODE_ENV=development
VITE_APP_WEB_URL= 'YOUR WEB URL'
项目根目录新建:.env.production :
NODE_ENV=production
VITE_APP_WEB_URL= 'YOUR WEB URL'
组件中使用环境变量:
console.log(import.meta.env.VITE_APP_WEB_URL)
配置package.json:
"build:dev": "vite build --mode development",
"build:pro": "vite build --mode production",
Vite常用基础配置
代理服务
server: {
host: '0.0.0.0',
port: 3000,
open: true,
https: false,
proxy: {}
},
生产环境去除console debuger
build:{
...
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}
生产环境开启gzip压缩
开启
gzip可以极大的压缩静态资源,对页面加载的速度起到了显著的作用。 使用vite-plugin-compression可以gzip或brotli的方式来压缩资源,这一步需要服务器端的配合,vite只能帮你打包出.gz文件。此插件使用简单,你甚至无需配置参数,引入即可。
# 安装
yarn add --dev vite-plugin-compression
复制代码
plugins 中添加:
// vite.config.ts
import viteCompression from 'vite-plugin-compression'
// gzip压缩 生产环境生成 .gz 文件
viteCompression({
verbose: true,
disable: false,
threshold: 10240,
algorithm: 'gzip',
ext: '.gz',
}),
最终vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import legacy from '@vitejs/plugin-legacy'
import vueJsx from '@vitejs/plugin-vue-jsx'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import viteCompression from 'vite-plugin-compression'
import { resolve } from 'path'
// https://vitejs.dev/config/
export default defineConfig({
base: './', //打包路径
plugins: [
AutoImport({
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
}),
vue(),
vueJsx({
// options are passed on to @vue/babel-plugin-jsx
}),
legacy({
targets: ['defaults', 'not IE 11']
}),
createSvgIconsPlugin({
// 指定需要缓存的图标文件夹
iconDirs: [resolve(process.cwd(), 'src/icons')],
// 指定symbolId格式
symbolId: 'icon-[dir]-[name]',
/**
* 自定义插入位置
* @default: body-last
* 'body-last' | 'body-first'
*/
inject: 'body-last',
/**
* custom dom id
* @default: __svg__icons__dom__
*/
customDomId: '__svg__icons__dom__'
}),
// gzip压缩 生产环境生成 .gz 文件
viteCompression({
verbose: true,
disable: false,
threshold: 10240,
algorithm: 'gzip',
ext: '.gz'
})
],
// 配置别名
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
// 配置全局scss样式文件
css: {
preprocessorOptions: {
scss: {
additionalData: '@import "@/style/variable.scss";'
}
}
},
//启动服务配置
server: {
host: '0.0.0.0',
port: 8000,
open: true,
https: false,
proxy: {}
},
// 生产环境打包配置
//去除 console debugger
build: {
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}
})