手把手搭建 Vite 工程化模版

创建基础模版

全局安装创建脚本

yarn global add create-vite

初始化项目

vite-create <product-name>

添加 git

git init

添加代码格式校验

添加 ESLint

包安装

npm i eslint eslint-plugin-vue @vue/eslint-config-prettier @vue/eslint-config-typescript -D

eslint 配置

.eslintrc

Copy
{
"root": true,
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"plugin:vue/vue3-recommended",
"eslint:recommended",
"@vue/typescript/recommended",
"@vue/prettier"
],
"plugins": ["vue"],
"settings": {
"import/resolver": {
"node": { "extensions": [".js", ".mjs", ".ts", ".d.ts", ".tsx"] }
}
},
"overrides": [
{
"files": ["*.json", "*.json5", "*.jsonc"],
"parser": "jsonc-eslint-parser"
},
{
"files": ["*.ts", "*.vue"],
"rules": {
"no-undef": "off"
}
},
{
"files": ["**/__tests__/**"],
"rules": {
"no-console": "off",
"vue/one-component-per-file": "off"
}
},
{
"files": ["*.d.ts"],
"rules": {
"import/no-duplicates": "off"
}
},
{
"files": ["*.js"],
"rules": {
"@typescript-eslint/no-var-requires": "off"
}
},
{
"files": ["*.vue"],
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser",
"extraFileExtensions": [".vue"],
"ecmaVersion": "latest",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"no-undef": "off"
}
}
],
"rules": {
"no-console": ["warn", { "allow": ["error"] }],
"no-debugger": "warn"
}
}

安装 Prettier

包安装

npm i prettier -D
正常的话，prettier 只需要安装这一个，但是 prettier@3.0.0 会与 @vue/eslint-config-prettier 中的 eslint-plugin-prettier 不兼容，所以我们还需要安装低版本的eslint-plugin-prettier：npm i eslint-plugin-prettier@5.0.0 -D。

你也可以试着把 prettier 版本降下来，但是不推荐。在安装 stylelint 的时候，它会影响 stylelint-prettier，当然你可以安装低版本 stylelint-prettier，但是会产生和prettier 格式化的冲突问题，这时你会想引入stylelint-config-prettier来解决，但是它要求stylelint版本低于15.0.0，这时你又需要降低stylelint的版本，以此造成依赖的逐渐兼容问题。

prettier 配置

.prettierrc

Copy
{
"singleQuote": true,
"semi": false,
"vueIndentScriptAndStyle": true
}

添加 StyleLint

包安装

npm i stylelint stylelint-config-standard stylelint-config-standard-scss stylelint-config-standard-vue stylelint-order stylelint-prettier -D

stylelint 配置

.stylelintrc

Copy
{
"extends": [
"stylelint-config-standard",
"stylelint-config-standard-scss",
"stylelint-config-standard-vue/scss",
"stylelint-prettier/recommended"
],
"plugins": ["stylelint-order"],
"customSyntax": "postcss-scss",
"rules": {
"order/order": ["custom-properties", "declarations"],
"order/properties-order": [
"position",
"top",
"right",
"bottom",
"left",
"z-index",
"display",
"justify-content",
"align-items",
"float",
"clear",
"overflow",
"overflow-x",
"overflow-y",
"margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
"border",
"border-style",
"border-width",
"border-color",
"border-top",
"border-top-style",
"border-top-width",
"border-top-color",
"border-right",
"border-right-style",
"border-right-width",
"border-right-color",
"border-bottom",
"border-bottom-style",
"border-bottom-width",
"border-bottom-color",
"border-left",
"border-left-style",
"border-left-width",
"border-left-color",
"border-radius",
"padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
"width",
"min-width",
"max-width",
"height",
"min-height",
"max-height",
"font-size",
"font-family",
"font-weight",
"text-align",
"text-justify",
"text-indent",
"text-overflow",
"text-decoration",
"white-space",
"color",
"background",
"background-position",
"background-repeat",
"background-size",
"background-color",
"background-clip",
"opacity",
"filter",
"list-style",
"outline",
"visibility",
"box-shadow",
"text-shadow",
"resize",
"transition"
]
},
"ignoreFiles": ["node_modules/**", "dist/**", "build/**", ".git/**", "*.js"],
"overrides": [
{
"files": ["*.(html|vue)", "**/*.(html|vue)"],
"rules": {
"unit-allowed-list": ["em", "rem", "px", "%", "s", "vw", "vh", "ms"]
},
"customSyntax": "postcss-html"
}
]
}

VSCode 配置

插件安装

项目配置完成之后，只是代表你可以使用 终端命令来执行格式化操作，但是 IDE 的格式化操作还需要你安装对应的插件。在 VSCode 中，你需要安装 ESlint、Prettier、StyleLint插件。

项目配置

.vscode/settings.json

Copy
{
"css.validate": true,
"scss.validate": false,
"less.validate": false,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.fixAll.eslint": true
},
"eslint.probe": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"json",
"jsonc"
],
"editor.defaultFormatter": "esbenp.prettier-vscode",
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"json",
"jsonc"
],
"eslint.quiet": true, // 不显示警告信息
"stylelint.validate": ["css", "less", "postcss", "scss", "sass", "vue"],
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "stylelint.vscode-stylelint"
},
"[less]": {
"editor.defaultFormatter": "stylelint.vscode-stylelint"
},
"[scss]": {
"editor.defaultFormatter": "stylelint.vscode-stylelint"
},
"[sass]": {
"editor.defaultFormatter": "stylelint.vscode-stylelint"
}
}

添加 editorconfig

保证不同操作系统的编辑器格式化一致，我们一般会添加EditorConfig for VS Code 插件

配置

.editorconfig

Copy
# http://editorconfig.org
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

提交校验

包安装

npm i commitizen @commitlint/cli @commitlint/config-conventional @commitlint/cz-commitlint -D

提交配置

.commitlintrc.json

Copy
{
"extends": ["@commitlint/config-conventional"],
"rules": {
"type-empty": [2, "never"],
"scope-enum": [2, "always", ["core", "style", "docs"]]
},
"prompt": {
"settings": {
"enableMultipleScopes": true,
"scopeEnumSeparator": ","
},
"messages": {
"skip": ":skip",
"max": "upper %d chars",
"min": "%d chars at least",
"emptyWarning": "can not be empty",
"upperLimitWarning": "over limit",
"lowerLimitWarning": "below limit"
},
"questions": {
"type": {
"description": "Select the type of change that you're committing:",
"enum": {
"feat": {
"description": "A new feature",
"title": "Features",
"emoji": "✨"
},
"fix": {
"description": "A bug fix",
"title": "Bug Fixes",
"emoji": "🐛"
},
"docs": {
"description": "Documentation only changes",
"title": "Documentation",
"emoji": "📚"
},
"style": {
"description": "Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)",
"title": "Styles",
"emoji": "💎"
},
"refactor": {
"description": "A code change that neither fixes a bug nor adds a feature",
"title": "Code Refactoring",
"emoji": "📦"
},
"perf": {
"description": "A code change that improves performance",
"title": "Performance Improvements",
"emoji": "🚀"
},
"test": {
"description": "Adding missing tests or correcting existing tests",
"title": "Tests",
"emoji": "🚨"
},
"build": {
"description": "Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)",
"title": "Builds",
"emoji": "🛠"
},
"ci": {
"description": "Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)",
"title": "Continuous Integrations",
"emoji": "⚙️"
},
"chore": {
"description": "Other changes that don't modify src or test files",
"title": "Chores",
"emoji": "♻️"
},
"revert": {
"description": "Reverts a previous commit",
"title": "Reverts",
"emoji": "🗑"
}
}
},
"scope": {
"description": "scope"
},
"subject": {
"description": "Write a short, imperative tense description of the change"
},
"body": {
"description": "Provide a longer description of the change"
},
"isBreaking": {
"description": "Are there any breaking changes?"
},
"breakingBody": {
"description": "A BREAKING CHANGE commit requires a body. Please enter a longer description of the commit itself"
},
"breaking": {
"description": "Describe the breaking changes"
},
"isIssueAffected": {
"description": "Does this change affect any open issues?"
},
"issuesBody": {
"description": "If issues are closed, the commit requires a body. Please enter a longer description of the commit itself"
},
"issues": {
"description": "Add issue references (e.g. \"fix #123\", \"re #123\".)"
}
}
}
}

在 package.json 中添加以下配置：

Copy
"config": {
"commitizen": {
"path": "@commitlint/cz-commitlint"
}
}

在scripts添加命令"commit": "git-cz"

流程自动化

包安装

npm i husky lint-staged -D

添加命令

在 package.json的scripts中添加以下命令：

Copy
"prepare": "husky install",
"lint": "eslint --ext .ts,vue,tsx --ignore-path .gitignore .",
"style": "stylelint src/**/*.{css,scss,vue}",
"tslint": "vue-tsc --noEmit"

使用 husky 添加 git hook：

Copy
# commit 前执行代码格式化
npx husky add .husky/pre-commit "npx lint-staged"
# commit msg 校验
npx husky add .husky/commit-msg "npx commitlint --edit $1"
# commit 后，自动推送到远程分支
npx husky add .husky/post-commit "branch=$(git symbolic-ref --short -q HEAD)
git pull origin $branch
if [ "$?" == "0" ]; then
git push origin $branch
if [ "$?" == "0" ]; then
echo "✨✨✨ 推送成功！"
else
echo '推送代码失败！'
exit 1
fi
else
echo '拉取代码失败！'
exit 1
fi"

TypeScript 配置

包安装

npm i typescript vue-tsc -D

typescript 配置

tsconfig.json

Copy
{
"compilerOptions": {
"baseUrl": ".",
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"paths": {
"@/*": ["src/*"]
},
"types": [
"unplugin-vue-define-options",
"vitest/importMeta",
"unplugin-icons/types/vue",
"element-plus/global"
],

/* Bundler mode */
"moduleResolution": "node",
"allowImportingTsExtensions": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",

/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"auto-imports.d.ts",
"components.d.ts",
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
],
"references": [{ "path": "./tsconfig.node.json" }]
}

tsconfig.node.json

Copy
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

types/env.d.ts

Copy
/// <reference types="vite/client" />
import Http from '@/utils/Http'
declare module '*.vue' {
import type { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}

declare module '@vue/runtime-core' {
export interface ComponentCustomProperties {
$http: typeof Http
}
}

添加自动按需导入组件、API、图标

包安装

npm i unplugin-auto-import unplugin-vue-components unplugin-icons unplugin-vue-define-options -D

配置

vite.config.ts

Copy
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import DefineOptions from 'unplugin-vue-define-options/vite'
import Unocss from 'unocss/vite'
import Inspect from 'vite-plugin-inspect'
import VueJsx from '@vitejs/plugin-vue-jsx'
import Components from 'unplugin-vue-components/vite'
import AutoImport from 'unplugin-auto-import/vite'
import Icons from 'unplugin-icons/vite'
import IconsResolver from 'unplugin-icons/resolver'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { FileSystemIconLoader } from 'unplugin-icons/loaders'

// https://vitejs.dev/config/
export default defineConfig({
assetsInclude: ['**/*.JPG'],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@use '@/style/variables.scss' as *;`,
},
},
},
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
},
plugins: [
vue(),
VueJsx(),
DefineOptions(),
Components({
dirs: ['src/components', 'main.ts'],
extensions: ['vue'],
deep: true,
dts: true,
resolvers: [
ElementPlusResolver(),
IconsResolver({
alias: {
park: 'icon-park',
},
customCollections: ['custom'],
}),
],
}),
AutoImport({
resolvers: [ElementPlusResolver()],
imports: ['vue', 'vue-router', '@vueuse/core', 'pinia', 'vitest'],
include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/, /\.md$/],
dirs: ['./src/composables'],
eslintrc: {
enabled: true,
globalsPropValue: true,
},
}),
Unocss(),
Icons({
autoInstall: true,
compiler: 'vue3',
customCollections: {
custom: FileSystemIconLoader('./src/icon/svg', (svg) =>
svg
.replace(/(width|height)=['"](\w+)['"]/g, '')
.replace(/^<svg /, '<svg fill="currentColor"'),
),
},
iconCustomizer(collection, icon, props) {
props.width = '1.2em'
props.height = '1.2em'
},
}),
Inspect(),
]
})

添加测试

包安装

npm i vitest @vitest/ui @vitest/coverage-c8 jsdom -D
在 VSCode 中，添加 Vitest 插件

配置

添加命令

Copy
"test": "vitest dev",
"ui": "vitest --ui",
"coverage": "vitest run --coverage"

vitest 配置

vitest.config.ts

Copy
import { defineConfig, mergeConfig } from 'vitest/config'
import viteConfig from './vite.config'
export default mergeConfig(
viteConfig,
defineConfig({
define: {
'import.meta.vitest': 'undefined',
},
test: {
globals: true,
clearMocks: true,
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
transformMode: {
web: [/\.[jt]sx$/],
},
includeSource: ['src/**/*.{js,ts}'],
coverage: {
provider: 'c8',
reporter: ['text', 'json', 'html'],
},
},
}),
)

vitest.setup.ts

Copy
import { config } from '@vue/test-utils'

config.global.stubs = {}

完整代码

经过上面配置流程，一个工程化的模块就搭建好了，完整代码可见 template-vite