本章将介绍Vue.js开发中的最佳实践、进阶技巧和团队协作规范,帮助你构建高质量的Vue.js应用。
11.1 代码规范与团队协作
ESLint配置
// .eslintrc.js
module.exports = {
root: true,
env: {
node: true,
browser: true,
es2022: true
},
extends: [
'plugin:vue/vue3-essential',
'plugin:vue/vue3-strongly-recommended',
'plugin:vue/vue3-recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier'
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
rules: {
// Vue相关规则
'vue/multi-word-component-names': 'error',
'vue/component-definition-name-casing': ['error', 'PascalCase'],
'vue/component-name-in-template-casing': ['error', 'PascalCase'],
'vue/custom-event-name-casing': ['error', 'camelCase'],
'vue/define-emits-declaration': 'error',
'vue/define-props-declaration': 'error',
'vue/no-unused-vars': 'error',
'vue/no-unused-components': 'error',
'vue/no-unused-refs': 'error',
'vue/prefer-import-from-vue': 'error',
'vue/prefer-separate-static-class': 'error',
'vue/require-macro-variable-name': 'error',
'vue/block-order': ['error', {
order: ['template', 'script', 'style']
}],
'vue/component-tags-order': ['error', {
order: ['template', 'script', 'style']
}],
'vue/padding-line-between-blocks': 'error',
// JavaScript/TypeScript规则
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-unused-vars': 'error',
'prefer-const': 'error',
'no-var': 'error',
'object-shorthand': 'error',
'prefer-template': 'error',
'template-curly-spacing': 'error',
'arrow-spacing': 'error',
'comma-dangle': ['error', 'never'],
'quotes': ['error', 'single'],
'semi': ['error', 'never'],
'indent': ['error', 2],
'max-len': ['error', { code: 120 }],
'no-multiple-empty-lines': ['error', { max: 1 }],
'eol-last': 'error'
},
overrides: [
{
files: ['*.vue'],
rules: {
'max-len': 'off' // Vue模板中可能有长行
}
},
{
files: ['**/__tests__/**/*', '**/*.{test,spec}.*'],
env: {
jest: true
},
rules: {
'no-unused-expressions': 'off'
}
}
]
}
Prettier配置
// .prettierrc
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"printWidth": 120,
"endOfLine": "lf",
"arrowParens": "avoid",
"bracketSpacing": true,
"bracketSameLine": false,
"vueIndentScriptAndStyle": false
}
Git Hooks配置
// package.json
{
"scripts": {
"prepare": "husky install",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"format": "prettier --write .",
"type-check": "vue-tsc --noEmit",
"test": "vitest",
"test:run": "vitest run",
"build": "vue-tsc --noEmit && vite build"
},
"lint-staged": {
"*.{vue,js,ts}": [
"eslint --fix",
"prettier --write"
],
"*.{css,scss,less}": [
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
},
"devDependencies": {
"husky": "^8.0.3",
"lint-staged": "^13.2.3"
}
}
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
# .husky/commit-msg
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx commitlint --edit $1
提交信息规范
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat', // 新功能
'fix', // 修复bug
'docs', // 文档更新
'style', // 代码格式化
'refactor', // 重构
'perf', // 性能优化
'test', // 测试
'chore', // 构建过程或辅助工具的变动
'revert', // 回滚
'build', // 构建系统或外部依赖项的更改
'ci' // CI配置文件和脚本的更改
]
],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'scope-case': [2, 'always', 'lower-case'],
'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'header-max-length': [2, 'always', 72],
'body-leading-blank': [1, 'always'],
'body-max-line-length': [2, 'always', 100],
'footer-leading-blank': [1, 'always'],
'footer-max-line-length': [2, 'always', 100]
}
}
11.2 组件设计模式
高阶组件(HOC)
// src/hoc/withLoading.js
import { defineComponent, h } from 'vue'
export function withLoading(WrappedComponent, loadingComponent) {
return defineComponent({
name: `WithLoading(${WrappedComponent.name})`,
props: {
loading: {
type: Boolean,
default: false
},
...WrappedComponent.props
},
setup(props, { slots, attrs }) {
return () => {
if (props.loading) {
return h(loadingComponent || 'div', { class: 'loading' }, 'Loading...')
}
return h(WrappedComponent, {
...attrs,
...props
}, slots)
}
}
})
}
// 使用示例
import UserList from './UserList.vue'
import LoadingSpinner from './LoadingSpinner.vue'
const UserListWithLoading = withLoading(UserList, LoadingSpinner)
渲染函数组件
// src/components/DynamicTable.js
import { defineComponent, h } from 'vue'
export default defineComponent({
name: 'DynamicTable',
props: {
columns: {
type: Array,
required: true
},
data: {
type: Array,
required: true
},
rowKey: {
type: String,
default: 'id'
}
},
emits: ['row-click', 'cell-click'],
setup(props, { emit }) {
function renderCell(row, column, rowIndex, columnIndex) {
const value = row[column.key]
// 自定义渲染函数
if (column.render) {
return column.render(value, row, rowIndex)
}
// 插槽渲染
if (column.slot) {
return h('slot', {
name: column.slot,
value,
row,
rowIndex,
column,
columnIndex
})
}
// 默认渲染
return value
}
function renderRow(row, rowIndex) {
return h('tr', {
key: row[props.rowKey],
class: {
'table-row': true,
'table-row-even': rowIndex % 2 === 0,
'table-row-odd': rowIndex % 2 === 1
},
onClick: () => emit('row-click', row, rowIndex)
}, props.columns.map((column, columnIndex) =>
h('td', {
key: column.key,
class: {
'table-cell': true,
[`table-cell-${column.align || 'left'}`]: true
},
onClick: (event) => {
event.stopPropagation()
emit('cell-click', row[column.key], row, rowIndex, column, columnIndex)
}
}, renderCell(row, column, rowIndex, columnIndex))
))
}
function renderHeader() {
return h('thead', {
class: 'table-header'
}, [
h('tr', props.columns.map(column =>
h('th', {
key: column.key,
class: {
'table-header-cell': true,
[`table-header-cell-${column.align || 'left'}`]: true,
'sortable': column.sortable
},
style: {
width: column.width
}
}, column.title)
))
])
}
function renderBody() {
return h('tbody', {
class: 'table-body'
}, props.data.map((row, index) => renderRow(row, index)))
}
return () => {
return h('table', {
class: 'dynamic-table'
}, [
renderHeader(),
renderBody()
])
}
}
})
组合式组件
<!-- src/components/ComposableForm.vue -->
<template>
<form @submit.prevent="handleSubmit" class="composable-form">
<div
v-for="field in fields"
:key="field.name"
class="form-field"
>
<label :for="field.name" class="form-label">
{{ field.label }}
<span v-if="field.required" class="required">*</span>
</label>
<component
:is="getFieldComponent(field.type)"
:id="field.name"
v-model="formData[field.name]"
v-bind="field.props"
:class="{
'form-control': true,
'error': errors[field.name]
}"
@blur="validateField(field.name)"
/>
<div v-if="errors[field.name]" class="error-message">
{{ errors[field.name] }}
</div>
</div>
<div class="form-actions">
<button type="submit" :disabled="!isValid" class="btn btn-primary">
{{ submitText }}
</button>
<button type="button" @click="reset" class="btn btn-secondary">
重置
</button>
</div>
</form>
</template>
<script setup>
import { computed } from 'vue'
import { useForm } from '@/composables/useForm'
const props = defineProps({
fields: {
type: Array,
required: true
},
initialData: {
type: Object,
default: () => ({})
},
submitText: {
type: String,
default: '提交'
}
})
const emit = defineEmits(['submit'])
const {
formData,
errors,
isValid,
validateField,
validateAll,
reset
} = useForm(props.fields, props.initialData)
const fieldComponents = {
text: 'input',
email: 'input',
password: 'input',
number: 'input',
textarea: 'textarea',
select: 'select',
checkbox: 'input',
radio: 'input',
date: 'input',
file: 'input'
}
function getFieldComponent(type) {
return fieldComponents[type] || 'input'
}
async function handleSubmit() {
const isFormValid = await validateAll()
if (isFormValid) {
emit('submit', { ...formData.value })
}
}
</script>
<style scoped>
.composable-form {
max-width: 600px;
margin: 0 auto;
}
.form-field {
margin-bottom: 1rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
.required {
color: #e74c3c;
}
.form-control {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
transition: border-color 0.3s;
}
.form-control:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.form-control.error {
border-color: #e74c3c;
}
.error-message {
color: #e74c3c;
font-size: 0.875rem;
margin-top: 0.25rem;
}
.form-actions {
display: flex;
gap: 1rem;
margin-top: 2rem;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: #0056b3;
}
.btn-primary:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #545b62;
}
</style>
11.3 性能优化进阶
组件懒加载策略
// src/utils/lazyLoad.js
import { defineAsyncComponent, h } from 'vue'
// 创建懒加载组件的工厂函数
export function createLazyComponent(loader, options = {}) {
const {
delay = 200,
timeout = 3000,
errorComponent = null,
loadingComponent = null,
retryCount = 3
} = options
return defineAsyncComponent({
loader: () => {
let retries = 0
const load = async () => {
try {
const component = await loader()
return component
} catch (error) {
if (retries < retryCount) {
retries++
console.warn(`Component load failed, retrying... (${retries}/${retryCount})`)
await new Promise(resolve => setTimeout(resolve, 1000 * retries))
return load()
}
throw error
}
}
return load()
},
loadingComponent: loadingComponent || {
template: '<div class="loading-placeholder">Loading...</div>'
},
errorComponent: errorComponent || {
template: '<div class="error-placeholder">Failed to load component</div>'
},
delay,
timeout,
suspensible: false
})
}
// 路由级别的懒加载
export function createLazyRoute(loader, options = {}) {
return {
component: createLazyComponent(loader, options),
meta: {
...options.meta,
lazy: true
}
}
}
// 预加载策略
export class ComponentPreloader {
constructor() {
this.cache = new Map()
this.loading = new Set()
}
async preload(loader, key) {
if (this.cache.has(key) || this.loading.has(key)) {
return this.cache.get(key)
}
this.loading.add(key)
try {
const component = await loader()
this.cache.set(key, component)
return component
} catch (error) {
console.error(`Failed to preload component ${key}:`, error)
throw error
} finally {
this.loading.delete(key)
}
}
// 预加载路由组件
preloadRoute(route) {
if (route.component && typeof route.component === 'function') {
return this.preload(route.component, route.name || route.path)
}
}
// 批量预加载
async preloadBatch(loaders) {
const promises = Object.entries(loaders).map(([key, loader]) =>
this.preload(loader, key).catch(error => ({ key, error }))
)
const results = await Promise.allSettled(promises)
const errors = results
.filter(result => result.status === 'rejected' || result.value?.error)
.map(result => result.reason || result.value.error)
if (errors.length > 0) {
console.warn('Some components failed to preload:', errors)
}
return results
}
}
export const preloader = new ComponentPreloader()
内存泄漏防护
// src/composables/useMemoryLeak.js
import { onUnmounted, ref } from 'vue'
// 防止内存泄漏的工具函数
export function useMemoryLeak() {
const timers = ref(new Set())
const intervals = ref(new Set())
const listeners = ref(new Map())
const observers = ref(new Set())
// 安全的setTimeout
function safeSetTimeout(callback, delay) {
const timer = setTimeout(() => {
timers.value.delete(timer)
callback()
}, delay)
timers.value.add(timer)
return timer
}
// 安全的setInterval
function safeSetInterval(callback, delay) {
const interval = setInterval(callback, delay)
intervals.value.add(interval)
return interval
}
// 安全的事件监听
function safeAddEventListener(target, event, handler, options) {
target.addEventListener(event, handler, options)
const key = `${target.constructor.name}-${event}`
if (!listeners.value.has(key)) {
listeners.value.set(key, [])
}
listeners.value.get(key).push({ target, event, handler, options })
}
// 安全的Observer
function safeObserver(ObserverClass, callback, options) {
const observer = new ObserverClass(callback, options)
observers.value.add(observer)
return observer
}
// 清理函数
function cleanup() {
// 清理定时器
timers.value.forEach(timer => clearTimeout(timer))
timers.value.clear()
// 清理间隔器
intervals.value.forEach(interval => clearInterval(interval))
intervals.value.clear()
// 清理事件监听器
listeners.value.forEach(listenerList => {
listenerList.forEach(({ target, event, handler, options }) => {
target.removeEventListener(event, handler, options)
})
})
listeners.value.clear()
// 清理观察者
observers.value.forEach(observer => {
if (observer.disconnect) {
observer.disconnect()
} else if (observer.unobserve) {
observer.unobserve()
}
})
observers.value.clear()
}
// 组件卸载时自动清理
onUnmounted(cleanup)
return {
safeSetTimeout,
safeSetInterval,
safeAddEventListener,
safeObserver,
cleanup
}
}
// 使用示例
export function usePolling(callback, interval = 1000) {
const { safeSetInterval } = useMemoryLeak()
const isPolling = ref(false)
let intervalId = null
function start() {
if (!isPolling.value) {
isPolling.value = true
intervalId = safeSetInterval(callback, interval)
}
}
function stop() {
if (isPolling.value && intervalId) {
clearInterval(intervalId)
isPolling.value = false
intervalId = null
}
}
return {
isPolling,
start,
stop
}
}
大数据处理优化
// src/composables/useBigData.js
import { ref, computed, nextTick } from 'vue'
export function useBigData(data, options = {}) {
const {
pageSize = 100,
searchFields = [],
sortField = null,
sortOrder = 'asc'
} = options
const currentPage = ref(1)
const searchQuery = ref('')
const sortBy = ref(sortField)
const sortDirection = ref(sortOrder)
const loading = ref(false)
// 搜索过滤
const filteredData = computed(() => {
if (!searchQuery.value || searchFields.length === 0) {
return data.value
}
const query = searchQuery.value.toLowerCase()
return data.value.filter(item =>
searchFields.some(field => {
const value = getNestedValue(item, field)
return value && value.toString().toLowerCase().includes(query)
})
)
})
// 排序
const sortedData = computed(() => {
if (!sortBy.value) {
return filteredData.value
}
return [...filteredData.value].sort((a, b) => {
const aValue = getNestedValue(a, sortBy.value)
const bValue = getNestedValue(b, sortBy.value)
let result = 0
if (aValue < bValue) result = -1
else if (aValue > bValue) result = 1
return sortDirection.value === 'desc' ? -result : result
})
})
// 分页数据
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize
const end = start + pageSize
return sortedData.value.slice(start, end)
})
// 总页数
const totalPages = computed(() =>
Math.ceil(sortedData.value.length / pageSize)
)
// 获取嵌套属性值
function getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => current?.[key], obj)
}
// 搜索
function search(query) {
searchQuery.value = query
currentPage.value = 1
}
// 排序
function sort(field, direction = 'asc') {
sortBy.value = field
sortDirection.value = direction
currentPage.value = 1
}
// 切换排序方向
function toggleSort(field) {
if (sortBy.value === field) {
sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc'
} else {
sortBy.value = field
sortDirection.value = 'asc'
}
currentPage.value = 1
}
// 跳转页面
function goToPage(page) {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
// 重置
function reset() {
searchQuery.value = ''
sortBy.value = sortField
sortDirection.value = sortOrder
currentPage.value = 1
}
// 异步加载数据
async function loadData(loader) {
loading.value = true
try {
const result = await loader({
page: currentPage.value,
size: pageSize,
search: searchQuery.value,
sortBy: sortBy.value,
sortDirection: sortDirection.value
})
return result
} catch (error) {
console.error('Failed to load data:', error)
throw error
} finally {
loading.value = false
}
}
return {
// 状态
currentPage,
searchQuery,
sortBy,
sortDirection,
loading,
// 计算属性
filteredData,
sortedData,
paginatedData,
totalPages,
// 方法
search,
sort,
toggleSort,
goToPage,
reset,
loadData
}
}
11.4 微前端架构
主应用配置
// main-app/src/main.js
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { registerMicroApps, start } from 'qiankun'
import App from './App.vue'
const app = createApp(App)
// 路由配置
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'Home',
component: () => import('./views/Home.vue')
},
{
path: '/about',
name: 'About',
component: () => import('./views/About.vue')
}
]
})
app.use(router)
// 注册微应用
registerMicroApps([
{
name: 'user-management',
entry: '//localhost:8081',
container: '#micro-app-container',
activeRule: '/user-management'
},
{
name: 'order-system',
entry: '//localhost:8082',
container: '#micro-app-container',
activeRule: '/order-system'
},
{
name: 'analytics-dashboard',
entry: '//localhost:8083',
container: '#micro-app-container',
activeRule: '/analytics'
}
], {
beforeLoad: (app) => {
console.log('Before load:', app.name)
return Promise.resolve()
},
beforeMount: (app) => {
console.log('Before mount:', app.name)
return Promise.resolve()
},
afterMount: (app) => {
console.log('After mount:', app.name)
return Promise.resolve()
},
beforeUnmount: (app) => {
console.log('Before unmount:', app.name)
return Promise.resolve()
},
afterUnmount: (app) => {
console.log('After unmount:', app.name)
return Promise.resolve()
}
})
// 设置默认进入的子应用
setDefaultMountApp('/user-management')
// 启动qiankun
start({
prefetch: true, // 预加载
sandbox: {
strictStyleIsolation: true, // 样式隔离
experimentalStyleIsolation: true
},
singular: false // 是否为单实例场景
})
app.mount('#app')
子应用配置
// micro-app/src/main.js
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'
import routes from './router'
let instance = null
let router = null
// 渲染函数
function render(props = {}) {
const { container } = props
router = createRouter({
history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/user-management' : '/'),
routes
})
instance = createApp(App)
instance.use(router)
const containerElement = container
? container.querySelector('#app')
: document.querySelector('#app')
instance.mount(containerElement)
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
// 导出qiankun生命周期函数
export async function bootstrap() {
console.log('User management app bootstraped')
}
export async function mount(props) {
console.log('User management app mount', props)
render(props)
}
export async function unmount() {
console.log('User management app unmount')
instance?.unmount()
instance = null
router = null
}
应用间通信
// shared/eventBus.js
class EventBus {
constructor() {
this.events = new Map()
}
// 订阅事件
on(event, callback) {
if (!this.events.has(event)) {
this.events.set(event, [])
}
this.events.get(event).push(callback)
// 返回取消订阅函数
return () => {
const callbacks = this.events.get(event)
if (callbacks) {
const index = callbacks.indexOf(callback)
if (index > -1) {
callbacks.splice(index, 1)
}
}
}
}
// 发布事件
emit(event, ...args) {
const callbacks = this.events.get(event)
if (callbacks) {
callbacks.forEach(callback => {
try {
callback(...args)
} catch (error) {
console.error(`Error in event callback for ${event}:`, error)
}
})
}
}
// 一次性订阅
once(event, callback) {
const unsubscribe = this.on(event, (...args) => {
unsubscribe()
callback(...args)
})
return unsubscribe
}
// 清除所有事件
clear() {
this.events.clear()
}
// 清除特定事件
off(event) {
this.events.delete(event)
}
}
// 全局事件总线
window.__GLOBAL_EVENT_BUS__ = window.__GLOBAL_EVENT_BUS__ || new EventBus()
export default window.__GLOBAL_EVENT_BUS__
// shared/store.js
import { reactive } from 'vue'
// 全局状态管理
class GlobalStore {
constructor() {
this.state = reactive({
user: null,
theme: 'light',
language: 'zh-CN',
permissions: []
})
this.subscribers = new Set()
}
// 获取状态
getState() {
return this.state
}
// 更新状态
setState(updates) {
Object.assign(this.state, updates)
this.notifySubscribers()
}
// 订阅状态变化
subscribe(callback) {
this.subscribers.add(callback)
return () => {
this.subscribers.delete(callback)
}
}
// 通知订阅者
notifySubscribers() {
this.subscribers.forEach(callback => {
try {
callback(this.state)
} catch (error) {
console.error('Error in state subscriber:', error)
}
})
}
}
// 全局状态实例
window.__GLOBAL_STORE__ = window.__GLOBAL_STORE__ || new GlobalStore()
export default window.__GLOBAL_STORE__
本章小结
本章我们学习了Vue.js开发的最佳实践和进阶技巧:
- 代码规范:ESLint、Prettier、Git Hooks等工具配置
- 组件设计模式:高阶组件、渲染函数、组合式组件
- 性能优化进阶:懒加载策略、内存泄漏防护、大数据处理
- 微前端架构:qiankun框架的使用和应用间通信
下一章预告
下一章我们将学习Vue.js的未来发展和总结,包括: - Vue.js生态系统展望 - 学习路径建议 - 实际项目经验分享 - 持续学习资源
练习题
基础练习
代码规范:
- 配置完整的ESLint和Prettier规则
- 设置Git Hooks自动化检查
- 实现提交信息规范化
组件设计:
- 创建高阶组件实现权限控制
- 使用渲染函数构建动态表格
- 设计可复用的表单组件
进阶练习
性能优化:
- 实现组件预加载策略
- 添加内存泄漏检测
- 优化大数据列表渲染
微前端实践:
- 搭建微前端主应用
- 创建独立的子应用
- 实现应用间数据共享
提示:最佳实践需要在实际项目中不断总结和完善,要根据团队和项目特点选择合适的方案。