第40天:Element Plus 工程化配置与构建优化
学习目标
- 深入理解 Element Plus 的工程化配置
- 掌握构建优化技术和策略
- 学习模块化和代码分割
- 实践生产环境部署优化
学习内容
1. Element Plus 工程化概述
1.1 现代前端工程化体系
typescript
// 工程化配置架构
interface EngineeringConfig {
// 构建工具配置
build: {
bundler: 'vite' | 'webpack' | 'rollup'
target: string[]
minify: boolean
sourcemap: boolean
splitting: boolean
}
// 开发环境配置
development: {
devServer: {
port: number
host: string
https: boolean
proxy: Record<string, any>
}
hmr: boolean
mock: boolean
}
// 生产环境配置
production: {
optimization: {
treeshaking: boolean
compression: boolean
bundleAnalysis: boolean
cdn: boolean
}
deployment: {
staticAssets: string
caching: Record<string, number>
preload: string[]
}
}
// 质量保证
quality: {
linting: boolean
testing: boolean
typeChecking: boolean
coverage: number
}
}
1.2 Vite 配置优化
typescript
// vite.config.ts - 优化的 Vite 配置
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import Components from 'unplugin-vue-components/vite'
import AutoImport from 'unplugin-auto-import/vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import { visualizer } from 'rollup-plugin-visualizer'
import { compression } from 'vite-plugin-compression2'
import { createHtmlPlugin } from 'vite-plugin-html'
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd(), '')
const isProduction = command === 'build'
return {
// 基础配置
base: env.VITE_PUBLIC_PATH || '/',
// 路径解析
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@utils': resolve(__dirname, 'src/utils'),
'@assets': resolve(__dirname, 'src/assets'),
'@api': resolve(__dirname, 'src/api'),
'@stores': resolve(__dirname, 'src/stores'),
'@views': resolve(__dirname, 'src/views')
},
extensions: ['.js', '.ts', '.jsx', '.tsx', '.vue', '.json']
},
// 插件配置
plugins: [
vue({
script: {
defineModel: true,
propsDestructure: true
}
}),
// 自动导入
AutoImport({
imports: [
'vue',
'vue-router',
'pinia',
{
'element-plus': [
'ElMessage',
'ElMessageBox',
'ElNotification',
'ElLoading'
]
}
],
resolvers: [ElementPlusResolver()],
dts: true,
eslintrc: {
enabled: true
}
}),
// 组件自动导入
Components({
resolvers: [
ElementPlusResolver({
importStyle: 'sass',
directives: true,
version: '2.4.0'
})
],
dts: true,
include: [/\.vue$/, /\.vue\?vue/, /\.tsx$/]
}),
// SVG 图标
createSvgIconsPlugin({
iconDirs: [resolve(process.cwd(), 'src/assets/icons')],
symbolId: 'icon-[dir]-[name]',
inject: 'body-last',
customDomId: '__svg__icons__dom__'
}),
// HTML 模板处理
createHtmlPlugin({
minify: isProduction,
inject: {
data: {
title: env.VITE_APP_TITLE || 'Element Plus App',
description: env.VITE_APP_DESCRIPTION || '',
keywords: env.VITE_APP_KEYWORDS || ''
}
}
}),
// 生产环境插件
...(isProduction ? [
// 包分析
visualizer({
filename: 'dist/stats.html',
open: false,
gzipSize: true,
brotliSize: true
}),
// Gzip 压缩
compression({
algorithm: 'gzip',
exclude: [/\.(br)$/, /\.(gz)$/]
}),
// Brotli 压缩
compression({
algorithm: 'brotliCompress',
exclude: [/\.(br)$/, /\.(gz)$/]
})
] : [])
],
// 开发服务器配置
server: {
host: '0.0.0.0',
port: Number(env.VITE_PORT) || 3000,
open: true,
cors: true,
proxy: {
'/api': {
target: env.VITE_API_BASE_URL,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
// 构建配置
build: {
target: 'es2015',
outDir: 'dist',
assetsDir: 'assets',
sourcemap: !isProduction,
minify: isProduction ? 'terser' : false,
// Terser 配置
terserOptions: {
compress: {
drop_console: isProduction,
drop_debugger: isProduction,
pure_funcs: isProduction ? ['console.log'] : []
}
},
// Rollup 配置
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html')
},
output: {
// 分包策略
manualChunks: {
// Vue 生态
vue: ['vue', 'vue-router', 'pinia'],
// Element Plus
'element-plus': ['element-plus'],
// 工具库
utils: ['lodash-es', 'dayjs', 'axios'],
// 图表库
charts: ['echarts', '@antv/g2']
},
// 文件命名
chunkFileNames: (chunkInfo) => {
const facadeModuleId = chunkInfo.facadeModuleId
? chunkInfo.facadeModuleId.split('/').pop().replace(/\.[^.]*$/, '')
: 'chunk'
return `js/${facadeModuleId}-[hash].js`
},
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.')
const ext = info[info.length - 1]
if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) {
return `media/[name]-[hash].${ext}`
}
if (/\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i.test(assetInfo.name)) {
return `images/[name]-[hash].${ext}`
}
if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) {
return `fonts/[name]-[hash].${ext}`
}
return `assets/[name]-[hash].${ext}`
}
}
},
// 包大小警告阈值
chunkSizeWarningLimit: 1000
},
// CSS 配置
css: {
preprocessorOptions: {
scss: {
additionalData: `
@use "@/styles/variables.scss" as *;
@use "@/styles/mixins.scss" as *;
`
}
},
postcss: {
plugins: [
require('autoprefixer'),
require('cssnano')({
preset: 'default'
})
]
}
},
// 优化配置
optimizeDeps: {
include: [
'vue',
'vue-router',
'pinia',
'element-plus/es',
'element-plus/es/components/button/style/index',
'element-plus/es/components/input/style/index',
'element-plus/es/components/form/style/index',
'element-plus/es/components/table/style/index',
'lodash-es',
'dayjs'
],
exclude: ['@iconify/json']
},
// 环境变量
define: {
__APP_VERSION__: JSON.stringify(process.env.npm_package_version),
__BUILD_TIME__: JSON.stringify(new Date().toISOString())
}
}
})
2. 按需加载优化
2.1 Element Plus 按需导入
typescript
// elementPlus.ts - Element Plus 按需导入配置
import type { App } from 'vue'
// 按需导入的组件列表
const components = [
'ElButton',
'ElInput',
'ElForm',
'ElFormItem',
'ElTable',
'ElTableColumn',
'ElPagination',
'ElDialog',
'ElDrawer',
'ElCard',
'ElTabs',
'ElTabPane',
'ElSelect',
'ElOption',
'ElDatePicker',
'ElTimePicker',
'ElUpload',
'ElImage',
'ElCarousel',
'ElCarouselItem'
] as const
// 按需导入的指令列表
const directives = [
'ElLoading',
'ElInfiniteScroll',
'ElPopover'
] as const
// 动态导入函数
const importComponent = async (name: string) => {
const component = await import(`element-plus/es/components/${name.toLowerCase().replace('el', '')}/index.mjs`)
return component.default || component
}
const importDirective = async (name: string) => {
const directive = await import(`element-plus/es/directives/${name.toLowerCase().replace('el', '')}/index.mjs`)
return directive.default || directive
}
// 按需注册插件
export const setupElementPlus = async (app: App) => {
// 注册组件
const componentPromises = components.map(async (name) => {
try {
const component = await importComponent(name)
app.component(name, component)
} catch (error) {
console.warn(`Failed to load component ${name}:`, error)
}
})
// 注册指令
const directivePromises = directives.map(async (name) => {
try {
const directive = await importDirective(name)
app.directive(name.replace('El', '').toLowerCase(), directive)
} catch (error) {
console.warn(`Failed to load directive ${name}:`, error)
}
})
await Promise.all([...componentPromises, ...directivePromises])
}
// 样式按需导入
export const loadElementPlusStyles = () => {
const styles = [
'element-plus/theme-chalk/base.css',
'element-plus/theme-chalk/el-button.css',
'element-plus/theme-chalk/el-input.css',
'element-plus/theme-chalk/el-form.css',
'element-plus/theme-chalk/el-table.css',
'element-plus/theme-chalk/el-pagination.css',
'element-plus/theme-chalk/el-dialog.css',
'element-plus/theme-chalk/el-card.css'
]
styles.forEach(style => {
import(style).catch(error => {
console.warn(`Failed to load style ${style}:`, error)
})
})
}
2.2 路由懒加载优化
typescript
// router/index.ts - 路由懒加载配置
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { defineAsyncComponent } from 'vue'
// 懒加载组件工厂
const createAsyncComponent = (loader: () => Promise<any>) => {
return defineAsyncComponent({
loader,
loadingComponent: () => import('@/components/Loading.vue'),
errorComponent: () => import('@/components/ErrorComponent.vue'),
delay: 200,
timeout: 3000
})
}
// 路由配置
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Layout',
component: () => import('@/layout/index.vue'),
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: createAsyncComponent(() => import('@/views/dashboard/index.vue')),
meta: {
title: '仪表盘',
icon: 'dashboard',
preload: true // 标记为预加载
}
},
{
path: 'users',
name: 'Users',
component: createAsyncComponent(() => import('@/views/users/index.vue')),
meta: {
title: '用户管理',
icon: 'users'
}
},
{
path: 'orders',
name: 'Orders',
component: createAsyncComponent(() => import('@/views/orders/index.vue')),
meta: {
title: '订单管理',
icon: 'orders'
}
},
{
path: 'analytics',
name: 'Analytics',
component: createAsyncComponent(() => import('@/views/analytics/index.vue')),
meta: {
title: '数据分析',
icon: 'analytics',
heavy: true // 标记为重型组件
}
}
]
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
meta: {
title: '登录',
layout: false
}
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/error/404.vue')
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { top: 0 }
}
}
})
// 路由预加载
class RoutePreloader {
private preloadedRoutes = new Set<string>()
private preloadPromises = new Map<string, Promise<any>>()
// 预加载路由组件
async preloadRoute(routeName: string) {
if (this.preloadedRoutes.has(routeName)) {
return this.preloadPromises.get(routeName)
}
const route = routes.find(r => r.name === routeName ||
r.children?.some(child => child.name === routeName))
if (!route) return
let targetRoute = route
if (route.children) {
targetRoute = route.children.find(child => child.name === routeName) || route
}
if (typeof targetRoute.component === 'function') {
const promise = (targetRoute.component as Function)()
this.preloadPromises.set(routeName, promise)
this.preloadedRoutes.add(routeName)
return promise
}
}
// 预加载相关路由
async preloadRelatedRoutes(currentRoute: string) {
const relatedRoutes = this.getRelatedRoutes(currentRoute)
const promises = relatedRoutes.map(route => this.preloadRoute(route))
await Promise.allSettled(promises)
}
// 获取相关路由
private getRelatedRoutes(currentRoute: string): string[] {
const routeRelations: Record<string, string[]> = {
'Dashboard': ['Users', 'Orders'],
'Users': ['Dashboard', 'Orders'],
'Orders': ['Users', 'Analytics']
}
return routeRelations[currentRoute] || []
}
}
const routePreloader = new RoutePreloader()
// 路由守卫
router.beforeEach(async (to, from, next) => {
// 预加载相关路由
if (to.name && typeof to.name === 'string') {
routePreloader.preloadRelatedRoutes(to.name)
}
next()
})
export default router
export { routePreloader }
3. 代码分割与模块化
3.1 智能代码分割
typescript
// bundleOptimizer.ts - 智能代码分割
import type { ManualChunksOption } from 'rollup'
// 分包策略配置
interface ChunkConfig {
name: string
test: (id: string) => boolean
priority: number
minSize: number
maxSize: number
}
const chunkConfigs: ChunkConfig[] = [
{
name: 'vue-vendor',
test: (id) => /[\/]node_modules[\/](vue|@vue|vue-router|pinia)[\/]/.test(id),
priority: 10,
minSize: 0,
maxSize: 500000
},
{
name: 'element-plus',
test: (id) => /[\/]node_modules[\/]element-plus[\/]/.test(id),
priority: 9,
minSize: 0,
maxSize: 800000
},
{
name: 'ui-vendor',
test: (id) => /[\/]node_modules[\/](@element-plus|@vueuse)[\/]/.test(id),
priority: 8,
minSize: 0,
maxSize: 300000
},
{
name: 'utils-vendor',
test: (id) => /[\/]node_modules[\/](lodash|dayjs|axios|qs)[\/]/.test(id),
priority: 7,
minSize: 0,
maxSize: 200000
},
{
name: 'charts-vendor',
test: (id) => /[\/]node_modules[\/](echarts|@antv|d3)[\/]/.test(id),
priority: 6,
minSize: 0,
maxSize: 1000000
},
{
name: 'vendor',
test: (id) => /[\/]node_modules[\/]/.test(id),
priority: 5,
minSize: 0,
maxSize: 500000
}
]
// 智能分包函数
export const createManualChunks: ManualChunksOption = (id) => {
// 排序配置按优先级
const sortedConfigs = chunkConfigs.sort((a, b) => b.priority - a.priority)
for (const config of sortedConfigs) {
if (config.test(id)) {
return config.name
}
}
// 业务代码分割
if (id.includes('/src/views/')) {
const match = id.match(/\/src\/views\/([^\/]+)/)
if (match) {
return `page-${match[1]}`
}
}
if (id.includes('/src/components/')) {
return 'components'
}
if (id.includes('/src/utils/')) {
return 'utils'
}
return undefined
}
// 动态导入优化
class DynamicImportOptimizer {
private importCache = new Map<string, Promise<any>>()
private importStats = new Map<string, { count: number, lastUsed: number }>()
// 优化的动态导入
async optimizedImport<T>(importFn: () => Promise<T>, key: string): Promise<T> {
// 更新统计信息
const stats = this.importStats.get(key) || { count: 0, lastUsed: 0 }
stats.count++
stats.lastUsed = Date.now()
this.importStats.set(key, stats)
// 检查缓存
if (this.importCache.has(key)) {
return this.importCache.get(key)!
}
// 执行导入
const promise = importFn()
this.importCache.set(key, promise)
try {
const result = await promise
return result
} catch (error) {
// 导入失败时清除缓存
this.importCache.delete(key)
throw error
}
}
// 预加载热门模块
async preloadPopularModules() {
const popularModules = Array.from(this.importStats.entries())
.filter(([_, stats]) => stats.count > 3)
.sort((a, b) => b[1].count - a[1].count)
.slice(0, 5)
.map(([key]) => key)
console.log('Preloading popular modules:', popularModules)
// 这里需要根据实际的模块映射来预加载
// 示例实现
for (const key of popularModules) {
if (!this.importCache.has(key)) {
// 根据 key 动态构建导入函数
const importFn = this.createImportFunction(key)
if (importFn) {
this.optimizedImport(importFn, key).catch(console.error)
}
}
}
}
// 创建导入函数
private createImportFunction(key: string): (() => Promise<any>) | null {
// 根据 key 的模式创建相应的导入函数
if (key.startsWith('page-')) {
const pageName = key.replace('page-', '')
return () => import(`@/views/${pageName}/index.vue`)
}
if (key.startsWith('component-')) {
const componentName = key.replace('component-', '')
return () => import(`@/components/${componentName}.vue`)
}
return null
}
// 清理过期缓存
cleanupCache() {
const now = Date.now()
const expireTime = 30 * 60 * 1000 // 30分钟
for (const [key, stats] of this.importStats.entries()) {
if (now - stats.lastUsed > expireTime) {
this.importCache.delete(key)
this.importStats.delete(key)
}
}
}
// 获取导入统计
getImportStats() {
return {
cacheSize: this.importCache.size,
statsSize: this.importStats.size,
popularModules: Array.from(this.importStats.entries())
.sort((a, b) => b[1].count - a[1].count)
.slice(0, 10)
}
}
}
const dynamicImportOptimizer = new DynamicImportOptimizer()
export { dynamicImportOptimizer }
3.2 模块联邦配置
typescript
// moduleFederation.ts - 模块联邦配置
import { defineConfig } from 'vite'
import federation from '@originjs/vite-plugin-federation'
// 模块联邦配置
export const federationConfig = {
name: 'element-plus-app',
filename: 'remoteEntry.js',
// 暴露的模块
exposes: {
'./Button': './src/components/Button.vue',
'./Table': './src/components/Table.vue',
'./Form': './src/components/Form.vue',
'./utils': './src/utils/index.ts'
},
// 共享依赖
shared: {
vue: {
singleton: true,
requiredVersion: '^3.3.0'
},
'vue-router': {
singleton: true,
requiredVersion: '^4.2.0'
},
'element-plus': {
singleton: true,
requiredVersion: '^2.4.0'
},
pinia: {
singleton: true,
requiredVersion: '^2.1.0'
}
},
// 远程模块
remotes: {
'shared-components': 'http://localhost:3001/assets/remoteEntry.js',
'micro-app': 'http://localhost:3002/assets/remoteEntry.js'
}
}
// 微前端应用管理器
class MicroAppManager {
private apps = new Map<string, any>()
private loadingStates = new Map<string, boolean>()
// 注册微应用
registerApp(name: string, config: any) {
this.apps.set(name, config)
}
// 加载微应用
async loadApp(name: string): Promise<any> {
if (this.loadingStates.get(name)) {
return new Promise((resolve) => {
const checkLoading = () => {
if (!this.loadingStates.get(name)) {
resolve(this.apps.get(name))
} else {
setTimeout(checkLoading, 100)
}
}
checkLoading()
})
}
const config = this.apps.get(name)
if (!config) {
throw new Error(`Micro app ${name} not found`)
}
this.loadingStates.set(name, true)
try {
// 动态导入远程模块
const module = await import(/* @vite-ignore */ config.entry)
this.apps.set(name, module)
return module
} finally {
this.loadingStates.set(name, false)
}
}
// 预加载微应用
async preloadApps(names: string[]) {
const promises = names.map(name =>
this.loadApp(name).catch(error => {
console.warn(`Failed to preload app ${name}:`, error)
})
)
await Promise.allSettled(promises)
}
// 卸载微应用
unloadApp(name: string) {
this.apps.delete(name)
this.loadingStates.delete(name)
}
// 获取应用状态
getAppStatus() {
return {
registered: Array.from(this.apps.keys()),
loading: Array.from(this.loadingStates.entries())
.filter(([_, loading]) => loading)
.map(([name]) => name)
}
}
}
const microAppManager = new MicroAppManager()
export { microAppManager }
4. 构建性能优化
4.1 构建缓存策略
typescript
// buildCache.ts - 构建缓存优化
import { createHash } from 'crypto'
import { existsSync, readFileSync, writeFileSync } from 'fs'
import { resolve } from 'path'
interface CacheEntry {
hash: string
timestamp: number
size: number
dependencies: string[]
}
class BuildCache {
private cacheDir = resolve(process.cwd(), 'node_modules/.cache/vite-build')
private cacheIndex = new Map<string, CacheEntry>()
private indexFile = resolve(this.cacheDir, 'index.json')
constructor() {
this.loadCacheIndex()
}
// 加载缓存索引
private loadCacheIndex() {
try {
if (existsSync(this.indexFile)) {
const data = readFileSync(this.indexFile, 'utf-8')
const entries = JSON.parse(data)
this.cacheIndex = new Map(Object.entries(entries))
}
} catch (error) {
console.warn('Failed to load cache index:', error)
}
}
// 保存缓存索引
private saveCacheIndex() {
try {
const entries = Object.fromEntries(this.cacheIndex)
writeFileSync(this.indexFile, JSON.stringify(entries, null, 2))
} catch (error) {
console.warn('Failed to save cache index:', error)
}
}
// 计算文件哈希
private calculateHash(content: string): string {
return createHash('md5').update(content).digest('hex')
}
// 检查缓存是否有效
isCacheValid(key: string, content: string, dependencies: string[] = []): boolean {
const entry = this.cacheIndex.get(key)
if (!entry) return false
const currentHash = this.calculateHash(content)
if (entry.hash !== currentHash) return false
// 检查依赖是否变化
for (const dep of dependencies) {
if (!existsSync(dep)) return false
const depContent = readFileSync(dep, 'utf-8')
const depHash = this.calculateHash(depContent)
const depEntry = this.cacheIndex.get(dep)
if (!depEntry || depEntry.hash !== depHash) return false
}
return true
}
// 设置缓存
setCache(key: string, content: string, dependencies: string[] = []) {
const hash = this.calculateHash(content)
const entry: CacheEntry = {
hash,
timestamp: Date.now(),
size: content.length,
dependencies
}
this.cacheIndex.set(key, entry)
// 缓存依赖文件
dependencies.forEach(dep => {
if (existsSync(dep)) {
const depContent = readFileSync(dep, 'utf-8')
const depHash = this.calculateHash(depContent)
this.cacheIndex.set(dep, {
hash: depHash,
timestamp: Date.now(),
size: depContent.length,
dependencies: []
})
}
})
this.saveCacheIndex()
}
// 清理过期缓存
cleanupCache(maxAge = 7 * 24 * 60 * 60 * 1000) { // 7天
const now = Date.now()
let cleaned = 0
for (const [key, entry] of this.cacheIndex.entries()) {
if (now - entry.timestamp > maxAge) {
this.cacheIndex.delete(key)
cleaned++
}
}
if (cleaned > 0) {
console.log(`Cleaned up ${cleaned} cache entries`)
this.saveCacheIndex()
}
}
// 获取缓存统计
getCacheStats() {
const entries = Array.from(this.cacheIndex.values())
const totalSize = entries.reduce((sum, entry) => sum + entry.size, 0)
return {
entryCount: entries.length,
totalSize: `${(totalSize / 1024 / 1024).toFixed(2)} MB`,
oldestEntry: Math.min(...entries.map(e => e.timestamp)),
newestEntry: Math.max(...entries.map(e => e.timestamp))
}
}
}
const buildCache = new BuildCache()
export { buildCache }
4.2 并行构建优化
typescript
// parallelBuild.ts - 并行构建优化
import { Worker } from 'worker_threads'
import { cpus } from 'os'
import { resolve } from 'path'
interface BuildTask {
id: string
type: 'component' | 'page' | 'chunk'
input: string
output: string
options: any
}
class ParallelBuilder {
private workers: Worker[] = []
private taskQueue: BuildTask[] = []
private activeTasks = new Map<string, BuildTask>()
private results = new Map<string, any>()
private maxWorkers = Math.min(cpus().length, 8)
constructor() {
this.initWorkers()
}
// 初始化工作线程
private initWorkers() {
for (let i = 0; i < this.maxWorkers; i++) {
const worker = new Worker(resolve(__dirname, 'buildWorker.js'))
worker.on('message', (result) => {
this.handleWorkerResult(result)
})
worker.on('error', (error) => {
console.error(`Worker ${i} error:`, error)
})
this.workers.push(worker)
}
}
// 添加构建任务
addTask(task: BuildTask) {
this.taskQueue.push(task)
this.processQueue()
}
// 批量添加任务
addTasks(tasks: BuildTask[]) {
this.taskQueue.push(...tasks)
this.processQueue()
}
// 处理任务队列
private processQueue() {
while (this.taskQueue.length > 0 && this.activeTasks.size < this.maxWorkers) {
const task = this.taskQueue.shift()!
const availableWorker = this.workers.find(worker =>
!Array.from(this.activeTasks.values()).some(activeTask =>
activeTask.workerId === worker.threadId
)
)
if (availableWorker) {
this.activeTasks.set(task.id, { ...task, workerId: availableWorker.threadId })
availableWorker.postMessage(task)
} else {
// 没有可用的工作线程,重新放回队列
this.taskQueue.unshift(task)
break
}
}
}
// 处理工作线程结果
private handleWorkerResult(result: any) {
const { taskId, success, data, error } = result
const task = this.activeTasks.get(taskId)
if (task) {
this.activeTasks.delete(taskId)
if (success) {
this.results.set(taskId, data)
console.log(`Task ${taskId} completed successfully`)
} else {
console.error(`Task ${taskId} failed:`, error)
}
// 继续处理队列
this.processQueue()
}
}
// 等待所有任务完成
async waitForCompletion(): Promise<Map<string, any>> {
return new Promise((resolve) => {
const checkCompletion = () => {
if (this.taskQueue.length === 0 && this.activeTasks.size === 0) {
resolve(this.results)
} else {
setTimeout(checkCompletion, 100)
}
}
checkCompletion()
})
}
// 获取构建进度
getProgress() {
const total = this.results.size + this.activeTasks.size + this.taskQueue.length
const completed = this.results.size
return {
total,
completed,
active: this.activeTasks.size,
pending: this.taskQueue.length,
progress: total > 0 ? (completed / total) * 100 : 0
}
}
// 清理资源
cleanup() {
this.workers.forEach(worker => worker.terminate())
this.workers = []
this.taskQueue = []
this.activeTasks.clear()
this.results.clear()
}
}
// 构建工作线程代码
const buildWorkerCode = `
const { parentPort } = require('worker_threads')
const { build } = require('vite')
parentPort.on('message', async (task) => {
try {
const result = await build({
configFile: false,
build: {
lib: {
entry: task.input,
name: task.id,
fileName: (format) => \`\${task.id}.\${format}.js\`
},
outDir: task.output,
...task.options
}
})
parentPort.postMessage({
taskId: task.id,
success: true,
data: result
})
} catch (error) {
parentPort.postMessage({
taskId: task.id,
success: false,
error: error.message
})
}
})
`
// 创建工作线程文件
import { writeFileSync } from 'fs'
const workerFile = resolve(__dirname, 'buildWorker.js')
writeFileSync(workerFile, buildWorkerCode)
const parallelBuilder = new ParallelBuilder()
export { parallelBuilder }
5. 生产环境优化
5.1 资源优化策略
typescript
// productionOptimizer.ts - 生产环境优化
import { defineConfig } from 'vite'
import { resolve } from 'path'
// 生产环境优化配置
export const productionConfig = defineConfig({
build: {
// 目标环境
target: ['es2015', 'chrome58', 'firefox57', 'safari11'],
// 输出配置
outDir: 'dist',
assetsDir: 'static',
// 压缩配置
minify: 'terser',
terserOptions: {
compress: {
// 移除 console
drop_console: true,
drop_debugger: true,
// 移除无用代码
dead_code: true,
// 优化条件表达式
conditionals: true,
// 优化布尔值
booleans: true,
// 优化循环
loops: true,
// 移除未使用的函数参数
unused: true,
// 内联函数
inline: 2,
// 移除无用的变量
pure_funcs: ['console.log', 'console.info', 'console.debug']
},
mangle: {
// 混淆变量名
toplevel: true,
// 保留类名
keep_classnames: true,
// 保留函数名
keep_fnames: false
},
format: {
// 移除注释
comments: false
}
},
// CSS 代码分割
cssCodeSplit: true,
// 生成 sourcemap
sourcemap: false,
// Rollup 配置
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html')
},
output: {
// 分包配置
manualChunks: (id) => {
// 第三方库
if (id.includes('node_modules')) {
if (id.includes('vue')) {
return 'vue-vendor'
}
if (id.includes('element-plus')) {
return 'element-vendor'
}
if (id.includes('lodash') || id.includes('dayjs')) {
return 'utils-vendor'
}
if (id.includes('echarts') || id.includes('@antv')) {
return 'charts-vendor'
}
return 'vendor'
}
// 业务代码
if (id.includes('/src/views/')) {
const match = id.match(/\/src\/views\/([^\/]+)/)
if (match) {
return `page-${match[1]}`
}
}
if (id.includes('/src/components/')) {
return 'components'
}
},
// 文件命名策略
entryFileNames: 'js/[name]-[hash].js',
chunkFileNames: (chunkInfo) => {
const name = chunkInfo.name || 'chunk'
return `js/${name}-[hash].js`
},
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.')
const ext = info[info.length - 1]
if (/\.(png|jpe?g|gif|svg|webp|avif)$/i.test(assetInfo.name)) {
return `images/[name]-[hash].${ext}`
}
if (/\.(woff2?|eot|ttf|otf)$/i.test(assetInfo.name)) {
return `fonts/[name]-[hash].${ext}`
}
if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)$/i.test(assetInfo.name)) {
return `media/[name]-[hash].${ext}`
}
return `assets/[name]-[hash].${ext}`
}
},
// 外部依赖
external: (id) => {
// CDN 依赖
const cdnDeps = ['vue', 'vue-router', 'element-plus']
return cdnDeps.some(dep => id === dep || id.startsWith(`${dep}/`))
}
},
// 包大小分析
reportCompressedSize: true,
chunkSizeWarningLimit: 1000
},
// 实验性功能
experimental: {
// 构建优化
renderBuiltUrl: (filename, { hostType }) => {
if (hostType === 'js') {
return { js: `window.__assetsPath(${JSON.stringify(filename)})` }
} else {
return { relative: true }
}
}
}
})
// 资源优化器
class AssetOptimizer {
private optimizations = new Map<string, any>()
// 图片优化
optimizeImages(config: any) {
return {
...config,
plugins: [
...config.plugins,
// 图片压缩
{
name: 'image-optimization',
generateBundle(options, bundle) {
Object.keys(bundle).forEach(fileName => {
const file = bundle[fileName]
if (file.type === 'asset' && /\.(png|jpe?g|gif|svg)$/i.test(fileName)) {
// 这里可以集成图片压缩库
console.log(`Optimizing image: ${fileName}`)
}
})
}
}
]
}
}
// CSS 优化
optimizeCSS(config: any) {
return {
...config,
css: {
...config.css,
postcss: {
plugins: [
require('autoprefixer'),
require('cssnano')({
preset: ['default', {
discardComments: { removeAll: true },
normalizeWhitespace: true,
colormin: true,
convertValues: true,
discardDuplicates: true,
discardEmpty: true,
mergeRules: true,
minifyFontValues: true,
minifySelectors: true
}]
})
]
}
}
}
}
// 字体优化
optimizeFonts(config: any) {
return {
...config,
plugins: [
...config.plugins,
{
name: 'font-optimization',
generateBundle(options, bundle) {
Object.keys(bundle).forEach(fileName => {
const file = bundle[fileName]
if (file.type === 'asset' && /\.(woff2?|eot|ttf|otf)$/i.test(fileName)) {
console.log(`Processing font: ${fileName}`)
// 可以在这里添加字体子集化逻辑
}
})
}
}
]
}
}
// 生成资源清单
generateManifest(config: any) {
return {
...config,
plugins: [
...config.plugins,
{
name: 'asset-manifest',
generateBundle(options, bundle) {
const manifest = {}
Object.keys(bundle).forEach(fileName => {
const file = bundle[fileName]
if (file.type === 'chunk' && file.isEntry) {
manifest[file.name] = fileName
}
})
this.emitFile({
type: 'asset',
fileName: 'manifest.json',
source: JSON.stringify(manifest, null, 2)
})
}
}
]
}
}
// 应用所有优化
applyOptimizations(config: any) {
let optimizedConfig = config
optimizedConfig = this.optimizeImages(optimizedConfig)
optimizedConfig = this.optimizeCSS(optimizedConfig)
optimizedConfig = this.optimizeFonts(optimizedConfig)
optimizedConfig = this.generateManifest(optimizedConfig)
return optimizedConfig
}
}
const assetOptimizer = new AssetOptimizer()
export { assetOptimizer }
5.2 CDN 和缓存策略
typescript
// cdnOptimizer.ts - CDN 和缓存优化
interface CDNConfig {
baseUrl: string
version: string
dependencies: Record<string, string>
fallback: boolean
}
class CDNOptimizer {
private config: CDNConfig
private fallbackMap = new Map<string, string>()
constructor(config: CDNConfig) {
this.config = config
this.setupFallbacks()
}
// 设置回退机制
private setupFallbacks() {
Object.entries(this.config.dependencies).forEach(([name, version]) => {
const localPath = `node_modules/${name}/dist/index.js`
this.fallbackMap.set(name, localPath)
})
}
// 生成 CDN 链接
generateCDNUrl(name: string, version: string, file = 'index.js'): string {
return `${this.config.baseUrl}/${name}@${version}/${file}`
}
// 生成 HTML 模板
generateHTMLTemplate(): string {
const cdnLinks = Object.entries(this.config.dependencies)
.map(([name, version]) => {
const url = this.generateCDNUrl(name, version)
const fallback = this.fallbackMap.get(name)
return `
<script>
(function() {
var script = document.createElement('script');
script.src = '${url}';
script.onerror = function() {
console.warn('CDN failed for ${name}, loading fallback');
var fallbackScript = document.createElement('script');
fallbackScript.src = '${fallback}';
document.head.appendChild(fallbackScript);
};
document.head.appendChild(script);
})();
</script>`
})
.join('')
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Element Plus App</title>
<!-- Preload critical resources -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/css/main.css" as="style">
<!-- DNS prefetch for CDN -->
<link rel="dns-prefetch" href="${this.config.baseUrl}">
<!-- CDN dependencies -->${cdnLinks}
<!-- Critical CSS -->
<style>
/* Critical above-the-fold styles */
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
.loading { display: flex; justify-content: center; align-items: center; height: 100vh; }
</style>
</head>
<body>
<div id="app">
<div class="loading">Loading...</div>
</div>
<!-- App scripts will be injected here -->
</body>
</html>`
}
// 生成缓存配置
generateCacheConfig() {
return {
// 静态资源缓存策略
staticAssets: {
// 图片缓存 1 年
images: {
pattern: /\.(png|jpe?g|gif|svg|webp|avif)$/,
maxAge: 365 * 24 * 60 * 60,
immutable: true
},
// 字体缓存 1 年
fonts: {
pattern: /\.(woff2?|eot|ttf|otf)$/,
maxAge: 365 * 24 * 60 * 60,
immutable: true
},
// JS/CSS 缓存 1 年(带 hash)
scripts: {
pattern: /\.(js|css)$/,
maxAge: 365 * 24 * 60 * 60,
immutable: true
},
// HTML 缓存 1 小时
html: {
pattern: /\.html$/,
maxAge: 60 * 60,
mustRevalidate: true
}
},
// Service Worker 配置
serviceWorker: {
enabled: true,
strategies: {
// 应用外壳缓存优先
appShell: 'CacheFirst',
// API 网络优先
api: 'NetworkFirst',
// 静态资源缓存优先
static: 'CacheFirst',
// 图片缓存优先,网络回退
images: 'CacheFirst'
}
}
}
}
// 生成 Service Worker
generateServiceWorker(): string {
return `
const CACHE_NAME = 'element-plus-app-v${this.config.version}';
const STATIC_CACHE = 'static-v${this.config.version}';
const API_CACHE = 'api-v${this.config.version}';
// 需要缓存的静态资源
const STATIC_ASSETS = [
'/',
'/manifest.json',
'/css/main.css',
'/js/main.js'
];
// 安装事件
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then((cache) => cache.addAll(STATIC_ASSETS))
.then(() => self.skipWaiting())
);
});
// 激活事件
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys()
.then((cacheNames) => {
return Promise.all(
cacheNames
.filter((cacheName) => {
return cacheName.startsWith('element-plus-app-') &&
cacheName !== CACHE_NAME &&
cacheName !== STATIC_CACHE &&
cacheName !== API_CACHE;
})
.map((cacheName) => caches.delete(cacheName))
);
})
.then(() => self.clients.claim())
);
});
// 请求拦截
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// API 请求 - 网络优先
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(request)
.then((response) => {
if (response.ok) {
const responseClone = response.clone();
caches.open(API_CACHE)
.then((cache) => cache.put(request, responseClone));
}
return response;
})
.catch(() => {
return caches.match(request);
})
);
return;
}
// 静态资源 - 缓存优先
if (request.destination === 'script' ||
request.destination === 'style' ||
request.destination === 'image' ||
request.destination === 'font') {
event.respondWith(
caches.match(request)
.then((response) => {
if (response) {
return response;
}
return fetch(request)
.then((response) => {
if (response.ok) {
const responseClone = response.clone();
caches.open(STATIC_CACHE)
.then((cache) => cache.put(request, responseClone));
}
return response;
});
})
);
return;
}
// HTML 文档 - 网络优先,缓存回退
if (request.destination === 'document') {
event.respondWith(
fetch(request)
.then((response) => {
if (response.ok) {
const responseClone = response.clone();
caches.open(CACHE_NAME)
.then((cache) => cache.put(request, responseClone));
}
return response;
})
.catch(() => {
return caches.match(request)
.then((response) => {
return response || caches.match('/');
});
})
);
}
});
// 消息处理
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
`
}
}
const cdnOptimizer = new CDNOptimizer({
baseUrl: 'https://unpkg.com',
version: '1.0.0',
dependencies: {
'vue': '^3.3.0',
'vue-router': '^4.2.0',
'element-plus': '^2.4.0'
},
fallback: true
})
export { cdnOptimizer }
实践练习
练习1:Vite 配置优化
typescript
// 创建优化的 Vite 配置
// 1. 配置自动导入
// 2. 设置代码分割策略
// 3. 优化构建性能
// 4. 配置开发服务器
练习2:按需加载实现
typescript
// 实现 Element Plus 按需加载
// 1. 配置组件按需导入
// 2. 实现样式按需加载
// 3. 优化路由懒加载
// 4. 实现预加载策略
练习3:构建性能优化
typescript
// 优化构建性能
// 1. 实现构建缓存
// 2. 配置并行构建
// 3. 优化包分析
// 4. 实现增量构建
练习4:生产环境部署
typescript
// 配置生产环境优化
// 1. 配置资源压缩
// 2. 实现 CDN 部署
// 3. 配置缓存策略
// 4. 实现 Service Worker
学习资源
官方文档
工具和插件
性能优化
作业
- 配置优化:为项目配置完整的 Vite 构建优化
- 按需加载:实现 Element Plus 组件和样式的按需加载
- 性能分析:使用工具分析构建产物并优化
- 部署策略:设计完整的生产环境部署方案
- 缓存策略:实现多层级的缓存优化策略
下一步学习
明天我们将学习「Element Plus 主题系统深入定制」,包括:
- CSS 变量系统
- SCSS 变量定制
- 主题切换实现
- 动态主题生成