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

2023年 10月 7日 150.8k 0

vue3 + vite + ts + eslint + prettier + husky

1. 创建项目

常用的创建项目命令:

  • pnpm create vue@latest(可以选择需要安装的依赖,推荐)
  • pnpm create vite (没有其它依赖,需要一个一个手动安装)
  • 1.1 pnpm create vue@latest

    image.png

    image.png

    2. 创建vscode相关配置文件

    .vscode文件夹是用来存放共享项目配置

    2.1 extensions.json

    推荐插件配置

    {
      "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的配置可能不一样导致有问题,最好统一配置和版本

    {
        "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

    {
    	"Print to console": {
    		"prefix": "vue3",
    		"body": [
    			"",
    			"console.log('new')",
    			"",
    			"",
    			"",
    			"  main",
    			"",
    			"",
    			"",
    			"",
    		],
    		"description": "vue3 template"
    	}
    }
    

    使用: 新建.vue文件,输入vue3可以看到提示

    3. 配置Eslint和prettierrc规则

    3.1 Eslint

    规则按需配

      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才能生效

    /* 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. 下载依赖

    pnpm add -D unplugin-auto-import
    pnpm add -D unplugin-vue-components
    

    2. vite.config.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

    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 安装依赖

    pnpm add tailwindcss@latest postcss@latest autoprefixer@latest
    
    npx tailwindcss init -p
    

    5.2 导入css

    在assets/main.css添加代码

    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    

    5.3 修改tailwind.config.js文件

    改为tailwind.config.cjs和添加下面的eslint-env node, 不然eslint会报错

    /* eslint-env node */
    
    module.exports = {
      content: ['./src/**/*.{vue,js,ts,jsx,tsx}'],
      theme: {
        extend: {},
      },
      plugins: [],
    }
    

    添加css属性看一下是否生效

    6. 规范前端git操作

    记得先git init生成git相关文件

    6.1 husky-配置git hook

    // 安装 + 初始化配置
    pnpm dlx husky-init && pnpm install
    

    6.2 commitlint-规范提交信息

    pnpm add -D husky @commitlint/cli @commitlint/config-conventional
    

    新建commitlint.config.cjs

    /* 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"'

    正确的操作:
    1. 创建文件,删除生成的undefined
    npx husky add .husky/commit-msg
    2. 复制下面的代码到文件里
    npx --no-install commitlint --edit $1
    

    执行命令检查规则是否生效
    git commit -m 'xxxx'

    image.png

    6.3 lint-staged-格式化代码

    pnpm add lint-staged -D
    npx husky add .husky/pre-commit "npx --no-install lint-staged"
    

    package.json添加

      "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"
        ]
      }
    

    执行日志

    image.png

    7. 注入打包时间

    在构建后的页面添加打包时间, 方便查看线上版本是否更新

    // 安装依赖
    pnpm add vite-plugin-html -D
    
    // index.html
    
    
    // vite.config.ts  
    import { createHtmlPlugin } from 'vite-plugin-html'
    
      plugins: [
        ......
        createHtmlPlugin({
          // 需要注入 index.html ejs 模版的数据
          inject: {
            data: {
              buildTime: new Date()
            }
          }
        })
      ],
    

    7. 配置环境变量

    坑:测试环境的命名最好不要叫test,有些第三方库会读取这个字段,执行了测试环境的代码,导致异常

    image.png

    image.png

    NODE_ENV=development
    VITE_BASE_URL=https://www.fastmock.site/mock/ef8bbf0e03d4cb0e575b719d6fd6e372
    

    只有VITE_开头的变量才i​可以使用mport.meta.env.xxx的方式访问

    8. 一些规范

    views目录

    很多项目的目录结构管理是一团糟,从目录结构和名称根本无法辨别功能和所属组件,子组件多的时候使用下面这种结构可以比较直观, 不要全部堆积在一个目录下面,当然这样可能会导致层级目录过深,不过我们配置了自动导入,可以规避这个问题

    views
    --home
    ----components
    ------home-one
    --------index.vue
    ----hook
    ------index.ts
    ----index.vue
    

    其它规范

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

    axios封装

    pnpm add axios
    pnpm add lodash-es
    pnpm add -D  @types/lodash-es
    pnpm add class-transformer
    
    export class CommonConfig {
      url = `${import.meta.env.VITE_APP_BASE_URL}/api`
    }
    
    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

    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 | null
      declare resType: ClassConstructor | null
      declare url: string
      declare method: HttpMethod
    }
    
    type PickInstancePropertyType = InstanceType
    
    type PickInstancePropertyTypeDouble = InstanceType[M]
    
    const request = (config: AxiosRequestConfig) => {
      return new Promise((resolve, reject) => {
        instance
          .request(config)
          .then((res) => {
            resolve(res.data)
          })
          .catch((err) => {
            reject(err)
          })
          .finally(() => {
            // 关闭loading
          })
      }) as Promise
    }
    
    export const sendHttpRequest = (
      ApiClass: ClassConstructor,
      data?: PickInstancePropertyType,
      config: TypedHttpConfig = {}
    ): Promise => {
      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)
    }
    
    

    使用

    const { data } = await sendHttpRequest(ApiGetUserInfo, { params })
    

    enum封装

    开发中,使用枚举类型时,一般使用一个对象作为映射,但遇到一些复杂情况代码就写的一团糟,可以参考下面的封装
    参考文章:
    blog.csdn.net/qq_41694291…
    blog.csdn.net/mouday/arti…

    /**
     * 增强枚举对象
     * @param enums 枚举值
     */
    
    type CommonEnumType = { [x: string]: any }[]
    
    export const createEnumObject = (enums: CommonEnumType) => {
      const valueKey = 'value'
      const labelKey = 'label'
    
      return {
        // 根据value支获取完整项
        getItem(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(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(value: T, key = '') {
          return this.getColum(labelKey, key || valueKey, value)
        },
    
        getValue(value: T, key = '') {
          return this.getColum(valueKey, key || labelKey, value)
        }
      }
    }
    
    

    使用实例:

    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

    
    import * as echarts from 'echarts'
    
    const props = withDefaults(
      defineProps 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)
      }
    })
    
    
    
      
        
      
    
    
    

    10. 问题记录:

    1. element-plus在shadow root下会出现css变量丢失的问题

    // 需要全量导入
    import 'element-plus/dist/index.css'
    

    2. inject类型缺失的问题,可以使用InjectionKey声明

    export const TEST: InjectionKey = Symbol()
    

    相关文章

    服务器端口转发,带你了解服务器端口转发
    服务器开放端口,服务器开放端口的步骤
    产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
    如何使用 WinGet 下载 Microsoft Store 应用
    百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
    百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

    发布评论