从0开始搭建一个完备的vue3项目

vue3 + vite + ts + eslint + prettier + husky

1. 创建项目

常用的创建项目命令:

  1. pnpm create vue@latest(可以选择需要安装的依赖,推荐)
  2. 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

其它规范

  1. 文件名包含多个单词时,单词之间建议使用半角的连词线 ( - ) 分隔, 驼峰法存在大小写,linux系统对大小写敏感,防止可能出现的问题
  2. 工具类目录可以在根节点配置index.ts作为导出的出口文件, 统一出口
  3. 推荐具名导出,防止改名,使用export default, 可能会导致方法被重命名等问题
  4. 就近原则,所有依赖的类型声明或工具方法就近放置,当需要复用时可以放在公共文件夹

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()
相关推荐
程序媛小果4 分钟前
基于java+SpringBoot+Vue的桂林旅游景点导游平台设计与实现
java·vue.js·spring boot
喵叔哟23 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
man20171 小时前
【2024最新】基于springboot+vue的闲一品交易平台lw+ppt
vue.js·spring boot·后端
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web