基于Vite+Vue3 从0搭建一个可复用的轻量脚手架

环境

项目依赖

推荐VSCode插件

搭建

构建项目

  1. 初始化项目
yarn create vite

通过vite创建vue3项目

  1. 初始化Git仓库

    git init
    

    创建仓库即为了管理代码,也方便在创建项目过程中记录、回滚操作步骤。

添加项目依赖

配置 ESLint
  1. 安装 ESLint
# eslint 安装
yarn add eslint --dev
# eslint 插件安装
yarn add eslint-plugin-vue --dev
yarn add @typescript-eslint/eslint-plugin --dev
yarn add eslint-plugin-prettier --dev
# typescript parser
yarn add @typescript-eslint/parser --dev
  1. 配置ESLint

在项目根目录下创建.eslintrc.js

module.exports = {
  root: true,
  env: {
    browser: true,
    node: true,
    es2021: true
  },
  parser: 'vue-eslint-parser',
  extends: [
    'eslint:recommended',
    'plugin:vue/vue3-essential',
    'plugin:vue/vue3-recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended', // eslint-config-prettier 的缩写
    'prettier'
  ],
  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', 'prettier'],
  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',
    // 'no-var': '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',
    'vue/multi-word-component-names': [
      'error',
      {
        ignores: ['index', 'Index', '403', '404', '500'] //需要忽略的组件名
      }
    ]
  },
  globals: {
    defineProps: 'readonly',
    defineEmits: 'readonly',
    defineExpose: 'readonly',
    withDefaults: 'readonly'
  }
}

在项目下添加 .eslintignore,忽略不需要进行代码检查的文件

# eslint 忽略检查 (根据项目需要自行添加)
node_modules
dist
配置 Prettier
  1. 安装prettire
# 安装 prettier
yarn add prettier --dev
  1. 安装插件解决 ESLintPrettire的冲突
# 安装插件 eslint-config-prettier
yarn add eslint-config-prettier --dev

解决 ESLint 中的样式规范和 prettier 中样式规范的冲突,以 prettier 的样式规范为准,使 ESLint 中的样式规范自动失效

  1. 配置Prettier

在项目根目录下新建 .prettierrc.json

{
  "semi": true,
  "eslintIntegration": true,
  "singleQuote": true,
  "endOfLine": "lf",
  "tabWidth": 2,
  "trailingComma": "none",
  "bracketSpacing": true,
  "arrowParens": "avoid"
}

在项目根目录下新建 .prettierignore,配置忽略格式化的文件

# 忽略格式化文件 (根据项目需要自行添加)
node_modules
dist
配置ESlintPrettier指令

package.jsonscript内容中添加:

{
  "script": {
    "lint": "eslint src --fix --ext .ts,.tsx,.vue,.js,.jsx",
    "prettier": "prettier --write ."
  }
}
tsconfig.json配置
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    },
    "target": "esnext",
    "useDefineForClassFields": true,
    "module": "esnext",
    "moduleResolution": "node",
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "lib": ["esnext", "dom"]
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
配置别名引用

出现找不到 path,执行 npm i @types/node --save-dev安装依赖。

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
  },
})
配置路由
  1. 安装vue-router
# 安装路由
yarn add vue-router@4
  1. 配置路由

src目录下创建 router文件夹,并创建router.ts文件。

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'HelloWorld',
    component: () => import('@/pages/HelloWorld.vue'),
  },
]

const router = createRouter({
  history: createWebHistory(),
  routes,
})

export default router
  1. 启用router

修改main.ts

import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'

const app = createApp(App)

app.use(router)

app.mount('#app')
配置 axios
  1. 安装axios
# 安装 axios
yarn add axios
# 安装 nprogress 用于请求 loading
# 也可以根据项目需求自定义其它 loading
yarn add nprogress
# 类型声明,或者添加一个包含 `declare module 'nprogress'
yarn add @types/nprogress --dev
  1. 封装统一操作工具 Request.ts

定义请求类型

/**
 * @description: 全局类型定义
 */

/**
 * @description: 基础请求返回类型
 */
declare interface ResType<T> {
  code: number
  data?: T
  message?: string
  errorCode?: string
}

/**
 * @description: 基础分页请求返回类型
 * 用法:ex.get<PageRes<T>>(...)
 */
declare interface PageRes<T> {
  current: number
  pageSize: number
  total?: number
  records: T[]
}

/**
 * @description: 基础分页请求参数类型
 */
declare enum sortEnum {
  ASC = 'ASC',
  DESC = 'DESC'
}

/**
 * @description 分页请求参数
 */
declare interface PageReq {
  current: number
  pageSize: number
  sortField?: string
  sortOrder?: sortEnum.ASC | sortEnum.DESC
}

定义工具类

import axios, { InternalAxiosRequestConfig } from 'axios'
import NProgress from 'nprogress'

// 创建 axios 实例
const service = axios.create({
  baseURL: import.meta.env.VITE_APP_BASE_API,
  timeout: import.meta.env.TIMEOUT,
  headers: { 'Content-Type': 'application/json;charset=utf-8' }
})

// 请求拦截器
service.interceptors.request.use(
  (config): InternalAxiosRequestConfig<any> => {
    // 执行请求前的操作,如添加Header等
    return config
  },
  (error: any) => {
    return Promise.reject(error)
  }
)

// 响应拦截
service.interceptors.response.use(res => {
  // 拦截接口响应信息,根据响应信息定义不同的操作
  return res
})

interface Request {
  get<T>(url: string, params?: unknown): Promise<ResType<T>>

  post<T>(url: string, params?: unknown): Promise<ResType<T>>

  put<T>(url: string, params?: unknown): Promise<ResType<T>>

  delete<T>(url: string, params?: unknown): Promise<ResType<T>>

  upload<T>(url: string, params: unknown): Promise<ResType<T>>

  download(url: string): void
}

const request: Request = {
  get(url, params) {
    return new Promise((resolve, reject) => {
      NProgress.start()
      service
        .get(url, { params })
        .then(res => {
          NProgress.done()
          resolve(res.data)
        })
        .catch(err => {
          NProgress.done()
          reject(err.data)
        })
    })
  },
  post(url, params) {
    return new Promise((resolve, reject) => {
      NProgress.start()
      service
        .post(url, JSON.stringify(params))
        .then(res => {
          NProgress.done()
          resolve(res.data)
        })
        .catch(err => {
          NProgress.done()
          reject(err.data)
        })
    })
  },
  put(url, params) {
    return new Promise((resolve, reject) => {
      NProgress.start()
      service
        .put(url, JSON.stringify(params))
        .then(res => {
          NProgress.done()
          resolve(res.data)
        })
        .catch(err => {
          NProgress.done()
          reject(err.data)
        })
    })
  },
  delete(url, params) {
    return new Promise((resolve, reject) => {
      NProgress.start()
      service
        .delete(url, { params })
        .then(res => {
          NProgress.done()
          resolve(res.data)
        })
        .catch(err => {
          NProgress.done()
          reject(err.data)
        })
    })
  },
  upload(url, file) {
    return new Promise((resolve, reject) => {
      NProgress.start()
      service
        .post(url, file, {
          headers: { 'Content-Type': 'multipart/form-data' }
        })
        .then(res => {
          NProgress.done()
          resolve(res.data)
        })
        .catch(err => {
          NProgress.done()
          reject(err.data)
        })
    })
  },
  download(url) {
    const iframe = document.createElement('iframe')
    iframe.style.display = 'none'
    iframe.src = url
    iframe.onload = function () {
      document.body.removeChild(iframe)
    }
    document.body.appendChild(iframe)
  }
}

export default request
  1. 配置本地开发跨域代理

定义本地环境变量类型 env.d.ts

// src/types/env.d.ts
interface ImportMetaEnv {
  /**
   * 应用标题
   */
  VITE_APP_TITLE: string
  /**
   * 应用端口
   */
  VITE_APP_PORT: number
  /**
   * API基础路径(反向代理)
   */
  VITE_APP_BASE_API: string
  /**
   * 后端服务地址
   */
  VITE_APP_API_URL: string
  /**
   * 请求超时时间
   */
  TIMEOUT: number
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

定义环境配置:在项目根目录创建文件 .env,.env.development

# .env
VITE_APP_TITLE="Vite - Vue"
VITE_APP_PORT=3000

# .env.development
VITE_APP_BASE_API='/dev-api'
VITE_APP_API_URL='http://localhost:8080/api'
TIMEOUT=10000

修改 vite.config.ts

import vue from '@vitejs/plugin-vue'
import path from 'path'
import { ConfigEnv, defineConfig, loadEnv, UserConfig } from 'vite'

// https://vitejs.dev/config/
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
  const env = loadEnv(mode, process.cwd())
  return {
    plugins: [vue()],
    resolve: {
      // 别名配置
      alias: {
        '@': path.resolve(__dirname, 'src')
      }
    },
    //启动服务配置
    server: {
      host: '0.0.0.0',
      port: Number(env.VITE_APP_PORT),
      hmr: true,
      open: false,
      proxy: {
        //反向代理解决跨域
        [env.VITE_APP_BASE_API]: {
          target: env.VITE_APP_API_URL,
          changeOrigin: true,
          // eg: https://www.xxx.com/dev-api/user => https://www.xxx.com/user
          rewrite: path =>
            path.replace(new RegExp(`^${env.VITE_APP_BASE_API}`), '')
        }
      }
    },
    // 生产环境打包配置
    build: {
      terserOptions: {
        compress: {
          drop_console: true,
          drop_debugger: true
        }
      }
    }
  }
})
配置 Pinia
  1. 安装Pinia
yarn add pinia
  1. 启用 Pinia,修改main.ts
import 'default-passive-events'
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router/router'
import './style.css'

const app = createApp(App)
// 启用router
app.use(router)
// 启用Pinia
const pinia = createPinia()
app.use(pinia)
// 挂载
app.mount('#app')
  1. 配置 Pinia持久化

默认持久化在 local storage

下载插件

yarn add pinia-plugin-persistedstate

mian.ts引入插件

import 'default-passive-events'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router/router'
import './style.css'

const app = createApp(App)
// 启用router
app.use(router)
// 启用Pinia
const pinia = createPinia()
// 启用Pinia 持久化
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
// 挂载
app.mount('#app')
  1. 示例代码
// 默认配置
import { defineStore } from 'pinia'

/**
 * Pinia 使用示例
 */
export const useExampleStore = defineStore('example', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++
    }
  }
})
// 开启持久化
import { defineStore } from 'pinia'

/**
 * Pinia 使用示例
 */
export const useExampleStore = defineStore('example', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++
    }
  },
  // 默认持久化在local storage
  persist: true
})
配置 less
  1. 安装less
yarn add less -D
  1. vite.config.ts中配置全局less 【需要创建全局less文件】
import vue from '@vitejs/plugin-vue'
import path from 'path'
import { ConfigEnv, defineConfig, loadEnv, UserConfig } from 'vite'

// https://vitejs.dev/config/
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
  const env = loadEnv(mode, process.cwd())
  return {
    plugins: [vue()],
    resolve: {
      // 别名配置
      alias: {
        '@': path.resolve(__dirname, 'src')
      }
    },
    css: {
      preprocessorOptions: {
        // 引入全局less样式
        less: {
          modifyVars: {
            hack: `true; @import "./src/assets/less/base.less";`
          },
          javascriptEnabled: true
        }
      }
    },
    //启动服务配置
    server: {
      host: '0.0.0.0',
      port: Number(env.VITE_APP_PORT),
      hmr: true,
      open: false,
      proxy: {
        //反向代理解决跨域
        [env.VITE_APP_BASE_API]: {
          target: env.VITE_APP_API_URL,
          changeOrigin: true,
          // eg: https://www.xxx.com/dev-api/user => https://www.xxx.com/user
          rewrite: path =>
            path.replace(new RegExp(`^${env.VITE_APP_BASE_API}`), '')
        }
      }
    },
    // 生产环境打包配置
    build: {
      terserOptions: {
        compress: {
          drop_console: true,
          drop_debugger: true
        }
      }
    }
  }
})
  1. 示例代码
// example less code
@example-color: #A1B2C3;
<!-- in use -->
<style scope lang='less'>
    .example {
        color: @example-color;
    }
</style>
安装工具库 vue-use
  1. 安装VueUse
yarn add @vueuse/core
  1. 示例代码
<script setup lang='ts'>
import { useMouse } from '@vueuse/core'

const { x, y } = useMouse()
</script>

<template>
	<div class="card">
        Here's the VueUse-based tooling implementation of mouse coordinate tracking
        <p>X : {{ x }} - Y : {{ y }}</p>
    </div>
</template>
配置 husky+lint-staged
  1. 安装mrm
npm i mrm -D --registry=https://registry.npm.taobao.org
  1. 通过mrm安装huskylint-staged
npx mrm lint-staged
yarn add husky -D
  1. 修改package.json中的lint-staged脚本
{
    "lint-staged": {
        "*.{ts,tsx,vue,js,jsx}": [
          "yarn lint",
          "yarn prettier"
        ]
      }
}
配置国际化
  1. 安装依赖 vue-i18n
yarn add vue-i18n
  1. 配置 i18n

src下创建目录 locales,并创建index.ts

import { createI18n } from 'vue-i18n'
import en_US from './lang/en_US'
import zh_CN from './lang/zh_CN'

// 获取浏览器默认语言
const navLang = navigator.language
// 获取本地缓存语言
const localLang = localStorage.getItem('locale')

const i18n = createI18n({
  legacy: false,
  // 指定语言
  locale: localLang || navLang || 'zh_CN',
  // 备选语言
  fallbackLocale: 'zh_CN',
  // 开启全局 $t
  globalInjection: true,
  messages: {
    zh_CN,
    en_US
  }
})

export default i18n
  1. main.ts中启用 i18n
import 'default-passive-events'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { createApp } from 'vue'
import App from './App.vue'
import i18n from './locales/index'
import router from './router/router'
import './style.css'

const app = createApp(App)
// 启用router
app.use(router)
// 启用国际化
app.use(i18n)
// 启用Pinia
const pinia = createPinia()
// 启用Pinia 持久化
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
// 挂载
app.mount('#app')
  1. 使用示例代码
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
const { locale } = useI18n()

/**
 * 切换语言
 */
const changeLocale = () => {
  const changeTo = locale.value === 'zh_CN' ? 'en_US' : 'zh_CN'
  locale.value = changeTo
}
</script>

<template>
  <div class="card">
    <button @click="changeLocale()">切换语言</button>
    <p>{{ $t('page.title') }}</p>
  </div>
</template>

集成UI组件库

Ant Design Vue
  1. 安装Antdv组件
yarn add ant-design-vue@4.x
  1. 配置Antdv按需导入

安装按需导入依赖

yarn add unplugin-vue-components -D

编辑vite.config.ts,添加如下内容

// vite.config.js
import { defineConfig } from 'vite';
import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
export default defineConfig({
  plugins: [
    // ...
    Components({
      resolvers: [
        AntDesignVueResolver({
          importStyle: false, // css in js
        }),
      ],
    }),
  ],
});
  1. 配置Antdv国际化

store目录下创建app.ts状态管理类,用来管理应用的配置信息

import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
import { defineStore } from 'pinia'

export const useAppStore = defineStore('app', {
  state: () => ({
    locale: 'zh-CN'
  }),
  actions: {
    changeLocale(locale: string) {
      this.locale = locale
      dayjs.locale(locale.toLowerCase())
    }
  },
  persist: true
})

编辑App.vue,添加Antdv国际化组件

<script setup lang="ts">
import enUS from 'ant-design-vue/es/locale/en_US'
import zhCN from 'ant-design-vue/es/locale/zh_CN'

import { useAppStore } from '@/store/app'
const appStore = useAppStore()
</script>

<template>
  <a-config-provider :locale="appStore.locale === 'zh-CN' ? zhCN : enUS">
    <div id="app">
      <router-view />
    </div>
  </a-config-provider>
</template>

<style lang="less"></style>

在切换语言处添加执行方法

// HelloWorld.vue
<script setup lang='ts'>
// ...
import { useAppStore } from '@/store/app'
const appStore = useAppStore()

/**
 * 切换语言
 */
const changeLocale = () => {
  const changeTo = locale.value === 'zh-CN' ? 'en-US' : 'zh-CN'
  localStorage.setItem('locale', changeTo)
  locale.value = changeTo
  appStore.changeLocale(changeTo)
}
// ..
</script>
// ......
  • 11
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值