vue3 + vite + ts + eslint + prettier + husky
1. 创建项目
常用的创建项目命令:
- pnpm create vue@latest(可以选择需要安装的依赖,推荐)
- pnpm create vite (没有其它依赖,需要一个一个手动安装)
1.1 pnpm create vue@latest
2. 创建vscode相关配置文件
.vscode文件夹是用来存放共享项目配置
2.1 extensions.json
推荐插件配置
js
{
"recommendations": [
"Vue.volar", // vue3
"Vue.vscode-typescript-vue-plugin", // vue3 ts
"formulahendry.auto-rename-tag", // 自动跟随修改标签名
"dbaeumer.vscode-eslint", // eslint
"esbenp.prettier-vscode", // prettier
"bradlc.vscode-tailwindcss", // tailwindcss 提示
"cpylua.language-postcss" // postcss高亮插件
]
}
使用: 点击扩展,在已安装下面有一栏推荐栏,会展示我们上面配置的插件
2.2 settings.json
常见问题:为啥在我的vscode eslint不生效?由于每个人的eslint的配置可能不一样导致有问题,最好统一配置和版本
js
{
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.addMissingImports": true, // 自动导入
},
// 和codeActionsOnSave一起开启会导致格式化两次, 推荐使用codeActionsOnSave
"editor.formatOnSave": false,
// 格式化JSON文件,使用vscode自带的格式化功能就可以了,使用eslint需要安装额外的插件
"[json]": {
// eslint不会格式化json文件,可以在单独文件配置下开启formatOnSave
"editor.formatOnSave": true,
// editor.formatOnSave开启情况下才会生效
"editor.defaultFormatter": "vscode.json-language-features"
},
// 带注释的json
"[jsonc]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "vscode.json-language-features"
},
// 格式化html文件
"[html]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "vscode.html-language-features"
},
}
2.3 配置自动生成模板代码
Code -> 首选项 -> 配置用户代码片段 -> vue.json
js
{
"Print to console": {
"prefix": "vue3",
"body": [
"<script lang=\"ts\" setup>",
"console.log('new')",
"</script>",
"",
"<template>",
" <div class=\"bg-white\">main</div>",
"</template>",
"",
"<style lang=\"postcss\" scoped></style>",
"",
],
"description": "vue3 template"
}
}
使用: 新建.vue文件,输入vue3可以看到提示
3. 配置Eslint和prettierrc规则
3.1 Eslint
规则按需配
css
rules: {
'prettier/prettier': ['error'],
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'vue/multi-word-component-names': [
'error',
{
ignores: ['index']
}
],
'vue/html-self-closing': [
'error',
{
html: {
void: 'always',
normal: 'always',
component: 'always'
},
svg: 'always',
math: 'always'
}
],
'vue/component-name-in-template-casing': [
'error',
'kebab-case',
{
registeredComponentsOnly: false
}
]
}
3.1.1 require('@rushstack/eslint-patch/modern-module-resolution')的作用
默认生成的eslint文件会有一行这个代码,当你在一个成熟的前端团队时,一般会有封装好的eslint统一配置,其中使用了很多eslint相关的库,这时候我们需要一个一个下载,这个补丁库的作用就是让我们不用手动安装,不用关心到底使用了哪些库,而且这些库不会在package.json中出现
3.2 prettierrc
默认生成.prettierrc.json文件,无法写注释,即使把格式更改为vscode的json with comments,运行时还是会报错,修改为.prettierrc.cjs, 修改prettierrc配置需要重新打开vscode才能生效
js
/* eslint-env node */
module.exports = {
semi: false, //句末分号
singleQuote: true, // 单引号
endOfLine: 'lf', // 换行符, 建议使用lf, 防止在linux下出现问题
tabs: 2, // 缩进长度
printWidth: 120, // 单行代码长度
trailingComma: 'none', // 换行符
bracketSameLine: true // 对象前后添加空格
}
最好看一下vscode右下角有一个行尾序列,需要改为lf,如果不改,可能会出现
Delete ␍
eslintprettier/prettier的错误
4. 配置自动导入功能
3.1 安装
1. 下载依赖
js
pnpm add -D unplugin-auto-import
pnpm add -D unplugin-vue-components
2. vite.config.js添加配置
js
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
export default defineConfig({
plugins: [
vue(),
AutoImport({
imports: ['vue', 'vue-router', 'vue-i18n', 'pinia'],
dts: 'src/auto-import.d.ts',
eslintrc: {
enabled: true
}
}),
Components({
extensions: ['vue'],
include: [/\.vue$/, /\.vue\?vue/],
dirs: ['src/'],
allowOverrides: true,
deep: true,
dts: 'src/components.d.ts'
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
3. 导入声明
重新运行项目,在src目录下就会生成auto-import.d.ts和components.d.ts声明文件,现在可以把main.js的createApp和createPinia的import删掉了,如果不行可以重新打开下项目,
3.3 配置组件库ant-design-vue自动导入
组件库推荐ant-design-vue
js
pnpm add ant-design-vue@4.x
// vite.config.ts
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
// 还需要配置一下
Components({
......
resolvers: [
AntDesignVueResolver({
importStyle: false
})
],
})
importStyle必须要设为false,ant-design-vue4采用了css in js的方式加载css,不然会报错找不到css文件,随便加个button,保存,会发现自动导入生效, 不生效就reload一下vscode
5. 配置tailwindcss
官方文档:v2.tailwindcss.com/docs/guides...
5.1 安装依赖
js
pnpm add tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init -p
5.2 导入css
在assets/main.css添加代码
js
@tailwind base;
@tailwind components;
@tailwind utilities;
5.3 修改tailwind.config.js文件
改为tailwind.config.cjs和添加下面的eslint-env node, 不然eslint会报错
js
/* eslint-env node */
module.exports = {
content: ['./src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
}
添加css属性看一下是否生效
<a-button class="h-[40px] w-[80px]" />
6. 规范前端git操作
记得先git init生成git相关文件
6.1 husky-配置git hook
js
// 安装 + 初始化配置
pnpm dlx husky-init && pnpm install
6.2 commitlint-规范提交信息
js
pnpm add -D husky @commitlint/cli @commitlint/config-conventional
新建commitlint.config.cjs
js
/* eslint-env node */
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test', 'record']
],
'type-empty': [0],
'type-case': [0],
'scope-empty': [0],
'header-max-length': [0, 'always', 72]
}
}
添加校验命令
husky官网的这个添加语句有问题,$1是传入的参数,却变成了一个空字符串
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
js
正确的操作:
1. 创建文件,删除生成的undefined
npx husky add .husky/commit-msg
2. 复制下面的代码到文件里
npx --no-install commitlint --edit $1
执行命令检查规则是否生效 git commit -m 'xxxx'
6.3 lint-staged-格式化代码
js
pnpm add lint-staged -D
npx husky add .husky/pre-commit "npx --no-install lint-staged"
package.json添加
js
"lint-staged": {
"*.{js,jsx,vue,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{scss,less,css,html,md}": [
"prettier --write"
],
"package.json": [
"prettier --write"
],
"{!(package)*.json,.!(browserslist)*rc}": [
"prettier --write--parser json"
]
}
执行日志
7. 注入打包时间
在构建后的页面添加打包时间, 方便查看线上版本是否更新
js
// 安装依赖
pnpm add vite-plugin-html -D
// index.html
<meta name="build-time" content="<%= buildTime %>" />
// vite.config.ts
import { createHtmlPlugin } from 'vite-plugin-html'
plugins: [
......
createHtmlPlugin({
// 需要注入 index.html ejs 模版的数据
inject: {
data: {
buildTime: new Date()
}
}
})
],
7. 配置环境变量
坑:测试环境的命名最好不要叫test,有些第三方库会读取这个字段,执行了测试环境的代码,导致异常
js
NODE_ENV=development
VITE_BASE_URL=https://www.fastmock.site/mock/ef8bbf0e03d4cb0e575b719d6fd6e372
只有VITE_开头的变量才i可以使用mport.meta.env.xxx的方式访问
8. 一些规范
views目录
很多项目的目录结构管理是一团糟,从目录结构和名称根本无法辨别功能和所属组件,子组件多的时候使用下面这种结构可以比较直观, 不要全部堆积在一个目录下面,当然这样可能会导致层级目录过深,不过我们配置了自动导入,可以规避这个问题
js
views
--home
----components
------home-one
--------index.vue
----hook
------index.ts
----index.vue
其它规范
- 文件名包含多个单词时,单词之间建议使用半角的连词线 ( - ) 分隔, 驼峰法存在大小写,linux系统对大小写敏感,防止可能出现的问题
- 工具类目录可以在根节点配置index.ts作为导出的出口文件, 统一出口
- 推荐具名导出,防止改名,使用export default, 可能会导致方法被重命名等问题
- 就近原则,所有依赖的类型声明或工具方法就近放置,当需要复用时可以放在公共文件夹
9. 常用工具封装
axios封装
js
pnpm add axios
pnpm add lodash-es
pnpm add -D @types/lodash-es
pnpm add class-transformer
js
export class CommonConfig {
url = `${import.meta.env.VITE_APP_BASE_URL}/api`
}
js
import { HttpMethod } from '@/service'
import { CommonConfig } from '../common-config'
export class ApiGetUserinfoReq {
declare userId: string
}
export class ApiGetUserinfoRes {
declare userId: string
declare userName: string
declare userRole: string | null
}
class BaseRequest {
declare params: ApiGetUserinfoReq
}
class BaseResponse {
declare data: {
code: number
data: ApiGetUserinfoRes[]
msg: string
}
}
/**
* 通用-获取用户信息
*/
export class ApiGetUserInfo extends CommonConfig {
url = 'xxxxxxxx'
method = HttpMethod.Get
reqType = BaseRequest
resType = BaseResponse
}
axios.ts
js
import axios from 'axios'
import { cloneDeep } from 'lodash-es'
import type { ClassConstructor } from 'class-transformer'
import type { AxiosRequestConfig } from 'axios'
import { notification } from 'ant-design-vue'
export enum HttpMethod {
Get = 'get',
Post = 'post',
Delete = 'delete',
Put = 'put'
}
axios.defaults.withCredentials = true
interface TypedHttpConfig {
// 是否显示加载状态
showLoading?: boolean
// 是否使用mock。只在开发环境有效
useMock?: boolean
// 加载文字
loadingText?: string
}
const defaultConfig: TypedHttpConfig = {
showLoading: false,
useMock: false
}
const instance = axios.create({
baseURL: '',
// 请求超时时间
timeout: 10000,
// 跨域携带cookie
withCredentials: true,
// 请求头默认配置
headers: { wework: true, 'Content-Type': 'application/json' }
})
instance.interceptors.request.use(
(config) => {
return config
},
(error) => Promise.reject(error)
)
instance.interceptors.response.use(
(response) => {
// 基于业务进行拦截处理
if (response?.data.success === false) {
notification.error({
message: '',
description: response.data.message
})
}
return response
},
(error) => {
console.log('response:error', error)
if (error.response) {
const { status } = error.response
if (status === 401) {
location.reload()
} else {
console.error(error)
}
}
return Promise.reject(error)
}
)
export class ApiDto {
declare reqType: ClassConstructor<any> | null
declare resType: ClassConstructor<any> | null
declare url: string
declare method: HttpMethod
}
type PickInstancePropertyType<T extends ClassConstructor<any>, K extends keyof InstanceType<T>> = InstanceType<
InstanceType<T>[K]
>
type PickInstancePropertyTypeDouble<
T extends ClassConstructor<any>,
K extends keyof InstanceType<T>,
M extends keyof InstanceType<ClassConstructor<any>>
> = InstanceType<InstanceType<T>[K]>[M]
const request = <T>(config: AxiosRequestConfig) => {
return new Promise((resolve, reject) => {
instance
.request<T>(config)
.then((res) => {
resolve(res.data)
})
.catch((err) => {
reject(err)
})
.finally(() => {
// 关闭loading
})
}) as Promise<T>
}
export const sendHttpRequest = <T extends ApiDto>(
ApiClass: ClassConstructor<T>,
data?: PickInstancePropertyType<ClassConstructor<T>, 'reqType'>,
config: TypedHttpConfig = {}
): Promise<PickInstancePropertyTypeDouble<ClassConstructor<T>, 'resType', 'data'>> => {
const cloneConfig = cloneDeep({
...defaultConfig,
...config
})
if (cloneConfig.showLoading) {
// loading框
}
let url = ''
let Parent = ApiClass
while (Parent.prototype) {
const parent = new Parent()
url = parent.url + url
Parent = Object.getPrototypeOf(Parent)
}
const api = new ApiClass()
const all = {
url: url,
method: api.method,
...data
}
return request(all)
}
使用
js
const { data } = await sendHttpRequest(ApiGetUserInfo, { params })
enum封装
开发中,使用枚举类型时,一般使用一个对象作为映射,但遇到一些复杂情况代码就写的一团糟,可以参考下面的封装 参考文章:
blog.csdn.net/qq_41694291... blog.csdn.net/mouday/arti...
ts
/**
* 增强枚举对象
* @param enums 枚举值
*/
type CommonEnumType = { [x: string]: any }[]
export const createEnumObject = (enums: CommonEnumType) => {
const valueKey = 'value'
const labelKey = 'label'
return {
// 根据value支获取完整项
getItem<T>(value: T, key = '') {
for (const item of enums) {
if (item[key || valueKey] == value) {
return item
}
}
},
// 根据key值获取所有取值
getColums(key: string) {
return enums.map((item) => item[key])
},
getColum<T>(column: string, key = '', value: T) {
const item = this.getItem(value, key)
if (item) {
return item[column]
}
},
getLabels() {
return this.getColums(labelKey)
},
getValues() {
return this.getColums(valueKey)
},
getLabel<T>(value: T, key = '') {
return this.getColum(labelKey, key || valueKey, value)
},
getValue<T>(value: T, key = '') {
return this.getColum(valueKey, key || labelKey, value)
}
}
}
使用实例:
js
const sexEnum = [
{
name: '男',
value: 1,
color: 'blue'
},
{
name: '女',
value: 2,
color: 'pink'
}
]
const sexEnumObj = createEnumObject(sexEnum)
sexEnumObj.getLabel(1)
sexEnumObj.getValue('男')
sexEnumObj.getItem(1)
使用echarts
js
<script lang="ts" setup>
import * as echarts from 'echarts'
const props = withDefaults(
defineProps<{
option: any
onClick?: (params: any) => void
}>(),
{
onClick: () => {}
}
)
const myChart = shallowRef()
const instance = ref()
const initChart = () => {
myChart.value = echarts.init(instance.value)
if (props.onClick) {
myChart.value.on('click', props.onClick)
}
myChart.value.setOption(props.option)
}
onMounted(() => {
initChart()
})
watch(
props.option,
() => {
myChart.value.setOption(props.option)
},
{
deep: true
}
)
const resize = () => {
if (myChart.value) {
myChart.value.resize()
}
}
const resizeObserver = new ResizeObserver(() => {
resize()
})
const initResizeObserver = () => {
// 从微前端进入时不会触发resize事件, 导致自适应宽度和高度有问题,直接监听元素本身
if (instance.value) {
resizeObserver.observe(instance.value)
}
}
onMounted(() => {
initResizeObserver()
})
onUnmounted(() => {
if (instance.value) {
resizeObserver.unobserve(instance.value)
}
})
</script>
<template>
<div>
<div ref="instance" class="w-full h-full" />
</div>
</template>
10. 问题记录:
1. element-plus在shadow root下会出现css变量丢失的问题
js
// 需要全量导入
import 'element-plus/dist/index.css'
2. inject类型缺失的问题,可以使用InjectionKey声明
ts
export const TEST: InjectionKey<string> = Symbol()