基于Vite+Vue3 从0搭建一个可复用的轻量脚手架
2023-12-30 11:38:28
环境
项目依赖
- Vite
- Vue3
- TypeScript
- Vue Router 【Vue官方路由】
- Axios 【请求库】
- Pinia 【全局状态管理工具】
- vue-i18n 【国际化插件】
- VueUse 【Vue工具库】
- Ant Design Vue 【Vue3 UI组件库】
推荐VSCode插件
- Vue Language Features (Volar) 【Vue生态插件】
- TypeScript Vue Plugin (Volar) 【Vue-TS生态插件】
- Ant Design Vue helper 【Antdv官方插件】
- i18n Ally 【国际化插件】
搭建
构建项目
- 初始化项目
yarn create vite
?
-
初始化Git仓库
git init
创建仓库即为了管理代码,也方便在创建项目过程中记录、回滚操作步骤。
添加项目依赖
配置 ESLint
- 安装
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
- 配置
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
- 安装
prettire
# 安装 prettier
yarn add prettier --dev
- 安装插件解决
ESLint
和Prettire
的冲突
# 安装插件 eslint-config-prettier
yarn add eslint-config-prettier --dev
解决
ESLint
中的样式规范和prettier
中样式规范的冲突
,以prettier
的样式规范为准
,使 ESLint 中的样式规范自动失效
- 配置
Prettier
在项目根目录下新建 .prettierrc.json
{
"semi": true,
"eslintIntegration": true,
"singleQuote": true,
"endOfLine": "lf",
"tabWidth": 2,
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid"
}
在项目根目录下新建 .prettierignore
,配置忽略格式化的文件
# 忽略格式化文件 (根据项目需要自行添加)
node_modules
dist
配置ESlint
和Prettier
指令
在package.json
的script
内容中添加:
{
"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'),
},
},
})
配置路由
- 安装
vue-router
# 安装路由
yarn add vue-router@4
- 配置路由
在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
- 启用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
- 安装
axios
# 安装 axios
yarn add axios
# 安装 nprogress 用于请求 loading
# 也可以根据项目需求自定义其它 loading
yarn add nprogress
# 类型声明,或者添加一个包含 `declare module 'nprogress'
yarn add @types/nprogress --dev
- 封装统一操作工具
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
- 配置本地开发跨域代理
定义本地环境变量类型 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
- 安装
Pinia
yarn add pinia
- 启用
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')
- 配置
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')
- 示例代码
// 默认配置
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
- 安装
less
yarn add less -D
- 在
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
}
}
}
}
})
- 示例代码
// example less code
@example-color: #A1B2C3;
<!-- in use -->
<style scope lang='less'>
.example {
color: @example-color;
}
</style>
安装工具库 vue-use
- 安装
VueUse
yarn add @vueuse/core
- 示例代码
<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
- 安装
mrm
npm i mrm -D --registry=https://registry.npm.taobao.org
- 通过
mrm
安装husky
和lint-staged
npx mrm lint-staged
yarn add husky -D
- 修改
package.json
中的lint-staged
脚本
{
"lint-staged": {
"*.{ts,tsx,vue,js,jsx}": [
"yarn lint",
"yarn prettier"
]
}
}
配置国际化
- 安装依赖
vue-i18n
yarn add vue-i18n
- 配置
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
- 在
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')
- 使用示例代码
<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
- 安装
Antdv
组件
yarn add ant-design-vue@4.x
- 配置
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
}),
],
}),
],
});
- 配置
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>
// ......
文章来源:https://blog.csdn.net/m0_55712478/article/details/135190366
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!