Vue3 工程化配置
Package.json
Details
json
{
"name": "vue-ts",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=20.14.0",
"pnpm": ">=9.0.0"
},
"scripts": {
"dev": "vite",
"dev:dev": "vite --mode dev",
"build:check": "vite build --mode check",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"lint/stylelint": "stylelint '**/*.{css,scss,vue,html}' --fix",
"format": "prettier --write src/",
"prepare": "husky",
"lint:lint-staged": "lint-staged",
"commit": "git-cz"
},
"dependencies": {
"element-plus": "^2.7.6",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"vue": "^3.4.29",
"vue-router": "^4.3.3"
},
"devDependencies": {
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@iconify-json/ep": "^1.1.15",
"@rushstack/eslint-patch": "^1.8.0",
"@tsconfig/node20": "^20.1.4",
"@types/node": "^20.14.5",
"@unocss/preset-rem-to-px": "^0.61.0",
"@unocss/transformer-directives": "^0.61.0",
"@vitejs/plugin-vue": "^5.0.5",
"@vitejs/plugin-vue-jsx": "^4.0.0",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.19",
"commitizen": "^4.3.0",
"consola": "^3.2.3",
"cz-git": "^1.9.3",
"eslint": "^8.57.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-vue": "^9.23.0",
"husky": "^9.0.11",
"lint-staged": "^15.2.7",
"npm-run-all2": "^6.2.0",
"postcss": "^8.4.39",
"postcss-html": "^1.7.0",
"postcss-scss": "^4.0.9",
"prettier": "^3.2.5",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.77.6",
"stylelint": "^16.6.1",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recess-order": "^5.0.1",
"stylelint-config-recommended-scss": "^14.0.0",
"stylelint-config-recommended-vue": "^1.5.0",
"stylelint-config-standard": "^36.0.1",
"typescript": "~5.4.0",
"unocss": "^0.61.0",
"unplugin-auto-import": "^0.17.6",
"unplugin-vue-components": "^0.27.2",
"vite": "^5.3.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-restart": "^0.4.1",
"vite-plugin-style-import": "^2.0.0",
"vite-plugin-vue-devtools": "^7.3.1",
"vue-tsc": "^2.0.21"
},
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
}
}yaml
VITE_APP_TITLE = 环境变量设置title
VITE_APP_BASE_API = /api
VITE_APP_PORT= 3600
VITE_APP_BASE_URL = 变量设置baseUrl
VITE_APP_BASE_URL_PROD = 变量设置baseUrlProd
VITE_APP_GZIP = true
VITE_APP_VISUALIZER = falseyaml
VITE_APP_GZIP = false
VITE_APP_VISUALIZER = trueyaml
VITE_APP_TITLE = dev环境
VITE_APP_BASE_API = /api
VITE_APP_BASE_URL = 变量设置baseUrl
VITE_APP_BASE_URL_PROD = 变量设置baseUrlProdyaml
VITE_APP_TITLE = prod环境
VITE_APP_BASE_API = /api
VITE_APP_BASE_URL = 变量设置baseUrl
VITE_APP_BASE_URL_PROD = 变量设置baseUrlProdyaml
VITE_APP_TITLE = test环境
VITE_APP_GZIP = false
VITE_APP_VISUALIZER = trueVite
Details
ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import { useBuild } from './build/build'
import { usePlugins } from './build/plugins'
import { useServer } from './build/server'
import { wrapperEnv } from './build/utils'
// https://vitejs.dev/config/
export default defineConfig(({ command, mode }) => {
const isBuild = command === 'build'
const root = process.cwd()
const env = loadEnv(mode, root)
const viteEnv = wrapperEnv(env)
return {
plugins: usePlugins(isBuild, viteEnv),
build: useBuild(viteEnv),
server: useServer(viteEnv),
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
esbuild: {
pure: viteEnv.VITE_DROP_CONSOLE ? ['console.log', 'debugger'] : []
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "./src/styles/mixin.scss" as *; @use "./src/styles/var.scss" as *;`
}
}
}
}
})ts
import type { UserConfig } from 'vite'
// eslint-disable-next-line no-unused-vars
export const useBuild = (viteEnv: ImportMetaEnv) => {
const config: UserConfig['build'] = {
// 10kb以下,转Base64
assetsInlineLimit: 1024 * 10,
// chunksizewarningLimit:1500,//配置文件大小提醒限制,默认500
rollupOptions: {
output: {
// 每个node_modules模块分成一个js文件
manualChunks(id: string) {
if (id.includes('node_modules')) {
// 把第三方依赖抽离出来 or第三方依赖合并在一起
return viteEnv.VITE_APP_VISUALIZER
? id.toString().split('node_modules/.pnpm/')[1].split('/')[0].toString()
: 'vendor'
}
return undefined
}, // 用于从入口点创建的块的打包输出格式[name]表示文件名,[hash]表示该文件内容hash值
entryFileNames: 'assets/js/[name].[hash].js', // 用于命名代码拆分时创建的共享块的输出命名
chunkFileNames: 'assets/js/[name].[hash].js', // 用于输出静态资源的命名,[ext]表示文件扩展名
assetFileNames: 'assets/[ext]/[name].[hash].[ext]'
}
}
}
return config
}ts
import type { UserConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { autoImportAndComponents } from './autoImportAndComponents'
import { useCompression } from './compression'
import { useRestart } from './restart'
import { useUnocss } from './unocss'
import { useVisualizer } from './visualizer'
import { useVueDevTools } from './vueDevTools'
export const usePlugins = (isBuild: boolean, viteEnv: ImportMetaEnv) => {
const plugins: UserConfig['plugins'] = [vue(), vueJsx()]
plugins.push(...autoImportAndComponents())
plugins.push(useUnocss())
// 开发环境
if (!isBuild) {
plugins.push(useRestart())
}
if (isBuild) {
viteEnv.VITE_APP_GZIP && plugins.push(useCompression())
viteEnv.VITE_APP_VISUALIZER && plugins.push(useVisualizer())
}
return plugins
}ts
import type { UserConfig } from 'vite'
export const useServer = (viteEnv: ImportMetaEnv) => {
const config: UserConfig['server'] = {
// 监听所有公共ip
host: '0.0.0.0',
cors: true,
port: viteEnv.VITE_APP_PORT,
proxy: {
// 前缀
'/api': {
target: 'http://www.example.com',
changeOrigin: true,
// 前缀重写
rewrite: (path: string) => path.replace(/^\/api/, '/api')
}
}
}
return config
}ts
/* eslint-disable */
/**
* 封装环境配置信息。
*
* 该函数用于处理和转换传入的环境配置对象,将其转换为适合于进程环境变量的形式。
* 同时,也将处理一些特殊配置项的转换,如端口号的数字转换、代理配置的JSON解析等。
*
* @param envConf 环境配置对象,包含需要设置的环境变量及其值。
* @returns 返回处理后的环境配置对象。
*/
export function wrapperEnv(envConf: any) {
// 初始化返回的对象,用于存储处理后的环境配置。
const ret: any = {}
// 遍历传入的环境配置对象,处理每个环境变量。
for (const envName of Object.keys(envConf)) {
// 替换环境变量值中的换行符,并处理布尔值字符串。
let realName = envConf[envName].replace(/\\n/g, '\n')
realName = realName === 'true' ? true : realName === 'false' ? false : realName
// 如果环境变量名为VITE_PORT,将其值转换为数字。
if (envName === 'VITE_PORT') {
realName = Number(realName)
}
// 如果环境变量名为VITE_PROXY且其值不为空,尝试将其值解析为JSON对象。
if (envName === 'VITE_PROXY' && realName) {
try {
realName = JSON.parse(realName.replace(/'/g, '"'))
} catch (e) {
realName = ''
}
}
// 将处理后的环境变量及其值存入返回对象。
ret[envName] = realName
// 根据环境变量的值的类型,设置进程环境变量。
if (typeof realName === 'string') {
process.env[envName] = realName
} else if (typeof realName === 'object') {
process.env[envName] = JSON.stringify(realName)
}
}
// 返回处理后的环境配置对象。
return ret
}plugins
ts
import AutoImport from 'unplugin-auto-import/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import Components from 'unplugin-vue-components/vite'
import { createStyleImportPlugin, ElementPlusResolve } from 'vite-plugin-style-import'
export const autoImportAndComponents = () => {
return [
AutoImport({
resolvers: [ElementPlusResolver()],
imports: ['vue', 'vue-router', 'pinia'],
dts: './types/auto-imports.d.ts',
eslintrc: {
enabled: true,
filepath: './.eslintrc-auto-import.json'
}
}),
Components({
resolvers: [ElementPlusResolver()],
dts: './types/components.d.ts'
}),
createStyleImportPlugin({
resolves: [ElementPlusResolve()],
libs: [
{
libraryName: 'element-plus',
esModule: true,
resolveStyle: (name: string) => {
return `element-plus/theme-chalk/${name}.css`
}
}
]
})
]
}ts
import vitepluginCompression from 'vite-plugin-compression'
/**
* @description 压缩
*/
export const useCompression = () =>
vitepluginCompression({
verbose: true, // 默认即可
disable: false, // 开启压缩(不禁用),默认即可
deleteOriginFile: false, // 删除源文件
threshold: 10240, // 压缩前最小文件大小
algorithm: 'gzip', // 压缩算法
ext: '.gz' // 文件类型
})ts
import viteRestart from 'vite-plugin-restart'
export const useRestart = () =>
viteRestart({
restart: ['*.config.[jt]s', '**/config/*.[jt]s', '*.config.cjs', './.eslintrc.cjs']
})ts
import Unocss from 'unocss/vite'
export const useUnocss = () => Unocss()ts
import { visualizer } from 'rollup-plugin-visualizer'
export const useVisualizer = () =>
visualizer({
open: true,
filename: 'stats.html',
title: '打包分析',
template: 'treemap', // sunburst
gzipSize: true,
brotliSize: true
})ts
import vueDevTools from 'vite-plugin-vue-devtools'
export const useVueDevTools = () => vueDevTools()Eslint
Eslint
js
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'airbnb-base',
'@vue/eslint-config-prettier/skip-formatting',
'./.eslintrc-auto-import.json'
],
parserOptions: {
ecmaVersion: 'latest'
},
overrides: [
{
files: ['*.ts', '*.tsx', '*.vue'],
rules: {
'no-undef': 'off',
'no-unused-vars': 'off',
'no-bitwise': 'off',
'no-param-reassign': 'off'
}
}
],
settings: {
'import/resolver': {
typescript: {
// eslint-import-resolver-typescript 插件解决@别名问题
project: './tsconfig.*.json'
}
}
},
rules: {
'import/no-extraneous-dependencies': 'off',
'import/prefer-default-export': 'off',
'no-console': 'off',
'no-unused-expressions': 'off',
// 对后缀的检测
'import/extensions': [
'error',
'ignorePackages',
{ js: 'never', jsx: 'never', ts: 'never', tsx: 'never' }
],
'import/order': [
'error',
{
/**
* builtin :内置模块,如 path,fs等 Node.js内置模块。
* external :外部模块,如 lodash ,react 等第三方库。
* internal :内部模块,如相对路径的模块、包名前缀为 @ 的模块。
* unknown :未知模块,如模块名没有指定扩展名或模块路径缺失扩展名。
* parent :父级目录的模块。
* sibling :同级目录的模块。
* index :当前目录的 index 文件。
* object :使用ES6 导入的模块。
* type :使用 import type 导入的模块。
* 默认值 ["builtin", "external", "parent", "sibling", "index"]。
*/
groups: [
'type',
['builtin', 'external'],
'internal',
['parent', 'sibling'],
'index',
'object',
'unknown'
],
pathGroups: [
{
pattern: '../**',
group: 'parent',
position: 'after'
},
{
pattern: './*.scss',
group: 'sibling',
position: 'after'
}
],
// 不同组之间是否换行。
'newlines-between': 'always',
// 根据字母顺序对每组内的引用进行排序。
alphabetize: {
order: 'asc',
caseInsensitive: true
}
}
]
}
}dist
node_modules
public
.husky
.vscode
.idea
*.sh
*.md
.gitignore
index.html
.eslintrc.cjs
.prettierrc.json
.stylelintrc.cjs
commitlint.config.cjs
pnpm-lock.yamljson
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"EffectScope": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"PropType": true,
"Ref": true,
"VNode": true,
"WritableComputedRef": true,
"acceptHMRUpdate": true,
"computed": true,
"createApp": true,
"createPinia": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"defineStore": true,
"effectScope": true,
"getActivePinia": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"mapActions": true,
"mapGetters": true,
"mapState": true,
"mapStores": true,
"mapWritableState": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeRouteLeave": true,
"onBeforeRouteUpdate": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onUnmounted": true,
"onUpdated": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"setActivePinia": true,
"setMapStoreSuffix": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"storeToRefs": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useLink": true,
"useRoute": true,
"useRouter": true,
"useSlots": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true
}
}Prettier
Prettier
json
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}/dist/*
.local
/node_modules/**
**/*.svg
**/*.sh
/public/*Stylelint
Stylelint
js
module.exports = {
extends: [
'stylelint-config-standard',
'stylelint-config-recommended-scss',
'stylelint-config-recommended-vue/scss',
'stylelint-config-html/vue',
'stylelint-config-recess-order'
],
//指定不同文件对应的解析器
overrides: [
{ files: ['**/*.{vue,html}'], customSyntax: 'postcss-html' },
{ files: ['**/*.{css,scss}'], customSyntax: 'postcss-scss' }
],
// 自定义规则
rules: {
//允许 global、export 、v-deep等伪类
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['global', 'export', 'v-deep', 'deep']
}
],
'selector-class-pattern': null,
//'selector-no-vendor-prefix': null.
//'value-no-vendor-prefix': null,
//'alpha-value-notation': null,
'color-function-notation': null,
//'rule-empty-line-before': null
'no-descending-specificity': null,
//'number-leading-zero': null,
//'declaration-block-no-redundant-longhand-properties': null,
'font-family-no-missing-generic-family-keyword': null
}
}/dist/*
/public/*Commitlint
commitlint.config.cjs
js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'subject-case': [0], // subject大小写不做校验
// 类型枚举,git提交type必须是以下类型
'type-enum': [
// 当前验证的错误级别
2,
// 在什么情况下进行验证,always表示一直进行验证
'always',
[
'feat', // 新增功能
'fix', // 修复缺陷
'docs', // 文档变更
'style', // 代码格式(不影响功能,例如空格、分号等格式修正)
'refactor', // 代码重构(不包括 bug 修复、功能新增)
'perf', // 性能优化
'test', // 添加疏漏测试或已有测试改动
'build', // 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)
'ci', // 修改 CI 配置、脚本
'revert', // 回滚 commit
'chore' // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)
]
]
},
prompt: {
useEmoji: true,
messages: {
type: '选择你要提交的类型 :',
scope: '选择一个提交范围(可选):',
customScope: '请输入自定义的提交范围 :',
subject: '填写简短精炼的变更描述 :\n',
body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
footerPrefixesSelect: '选择关联issue前缀(可选):',
customFooterPrefix: '输入自定义issue前缀 :',
footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
confirmCommit: '是否提交或修改commit ?'
},
types: [
{ value: 'feat', name: 'feat: 新增功能 | A new feature', emoji: '✨' },
{ value: 'fix', name: 'fix: 修复缺陷 | A bug fix', emoji: '🐛' },
{ value: 'docs', name: 'docs: 文档更新 | Documentation only changes', emoji: '📝' },
{
value: 'style',
name: 'style: 代码格式 | Changes that do not affect the meaning of the code',
emoji: '💄'
},
{
value: 'refactor',
name: 'refactor: 代码重构 | A code change that neither fixes a bug nor adds a feature',
emoji: '♻️'
},
{
value: 'perf',
name: 'perf: 性能提升 | A code change that improves performance',
emoji: '⚡️'
},
{
value: 'test',
name: 'test: 测试相关 | Adding missing tests or correcting existing tests',
emoji: '✅'
},
{
value: 'build',
name: 'build: 构建相关 | Changes that affect the build system or external dependencies',
emoji: '📦️'
},
{
value: 'ci',
name: 'ci: 持续集成 | Changes to our CI configuration files and scripts',
emoji: '🎡'
},
{ value: 'revert', name: 'revert: 回退代码 | Revert to a commit', emoji: '🔨' },
{
value: 'chore',
name: 'chore: 其他修改 | Other changes that do not modify src or test files',
emoji: '⏪️'
}
]
}
}Lint-staged
lint-staged.config.cjs
js
module.exports = {
'*.{js,ts,jsx,tsx}': ['eslint --fix', 'prettier --write'],
'*.{cjs,json}': ['prettier --write'],
'!(package)*.json': ['prettier --write--parser json'],
'package.json': ['prettier --write'],
'*.{vue,html}': ['eslint --fix', 'prettier --write', 'stylelint --fix --allow-empty-input'],
'*.{scss,css}': ['stylelint --fix --allow-empty-input', 'prettier --write']
}Postcss
postcss.config.cjs
js
module.exports = {
plugins: {
autoprefixer: {}
}
}Tsconfig
tsconfig
json
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}
```json [tsconfig.app.json]
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["types", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"types": ["element-plus/global"]
}
}json
{
"extends": "@tsconfig/node20/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"build",
"types"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}UnoCSS
unocss.config.ts
ts
// 预设rem转px
import presetRemToPx from '@unocss/preset-rem-to-px'
// transformerDirectives 可以使用@apply @screen theme函数
import transformerDirective from '@unocss/transformer-directives'
import {
defineConfig,
presetAttributify,
presetUno,
transformerVariantGroup,
presetIcons
} from 'unocss'
export default defineConfig({
presets: [
presetUno(),
presetAttributify(),
presetRemToPx({
baseFontSize: 4
}),
presetIcons({
scale: 1.2,
warn: true,
extraProperties: {
display: 'inline-block',
'vertical-align': 'middle'
}
})
],
transformers: [transformerVariantGroup(), transformerDirective()],
shortcuts: [
{
'flex-center': 'flex justify-center items-center',
'flex-end': 'flex justify-end items-end',
'flex-between': 'flex justify-between items-center',
'flex-around': 'flex justify-around items-center',
'flex-column': 'flex flex-col',
'flex-column-center': 'flex flex-col justify-center items-center',
'flex-column-between': 'flex flex-col justify-between items-center',
'flex-column-around': 'flex flex-col justify-around items-center',
'flex-column-end': 'flex flex-col justify-end items-end'
},
{
'text-ellipsis': 'overflow-hidden text-ellipsis whitespace-nowrap',
'text-ellipsis-2': 'overflow-hidden text-ellipsis whitespace-nowrap text-2',
'text-ellipsis-3': 'overflow-hidden text-ellipsis whitespace-nowrap text-3'
}
]
})Husky
bash
npx husky add .husky/commit-msg 'npx commitlint --edit $1'.husky
js
npm run lint:lint-staged --allow-emptyjs
npx --no -- commitlint --edit $1