task-91: 按页面拆分 Element Plus 与公共业务 chunk 规划器
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* 构建拆包规划器(Task 91)。
|
||||
*
|
||||
* 为多页应用(MPA)生成 manualChunks 配置,按页面拆分公共业务 chunk:
|
||||
* - vendorRules:全局 vendor 规则(Element Plus、Vue 运行时等),所有入口共享;
|
||||
* - perEntryRules:按入口隔离的页面私有 chunk(匹配需同时给出 entryName,
|
||||
* 未匹配入口的模块不会被误拆到其他页面 chunk);
|
||||
* - 同名 chunk 的规则自动合并去重;未匹配模块返回 undefined,交由 Vite
|
||||
* 默认拆包逻辑处理,不强制拆包。
|
||||
*
|
||||
* 有界配置:maxRules 限制 vendor 规则数量(超限拒绝计数,规则不生效);
|
||||
* maxEntries 限制登记的入口数量(超出部分连同其 per-entry 规则一并跳过)。
|
||||
* 校验失败(空 chunk 名、空 patterns、非法上限)fail-fast 抛错且零状态变更;
|
||||
* 规则读取抛错(依赖故障)时调用失败,规划状态不被污染,恢复后可继续使用。
|
||||
*/
|
||||
export interface ChunkRule {
|
||||
/** chunk 名,不能为空 */
|
||||
chunk: string
|
||||
/** 匹配模块 id 的字符串 pattern 列表,不能为空 */
|
||||
patterns: string[]
|
||||
}
|
||||
|
||||
export interface EntryChunkRule extends ChunkRule {
|
||||
/** 关联的入口名,不能为空 */
|
||||
entry: string
|
||||
}
|
||||
|
||||
export interface ChunkPlannerOptions {
|
||||
/** 页面入口列表;超过 maxEntries 的部分被跳过 */
|
||||
entries: string[]
|
||||
/** 全局 vendor 拆包规则;数量超过 maxRules 的部分被拒绝 */
|
||||
vendorRules?: ChunkRule[]
|
||||
/** 按入口隔离的页面 chunk 规则 */
|
||||
perEntryRules?: EntryChunkRule[]
|
||||
/** vendor 规则数量上限,必须为正数,默认 100 */
|
||||
maxRules?: number
|
||||
/** 登记的入口数量上限,必须为正数,默认 100 */
|
||||
maxEntries?: number
|
||||
}
|
||||
|
||||
export interface ChunkPlan {
|
||||
entry: string
|
||||
chunks: string[]
|
||||
}
|
||||
|
||||
export interface ChunkPlannerStats {
|
||||
entryCount: number
|
||||
chunkCount: number
|
||||
sharedChunkCount: number
|
||||
unmatchedCount: number
|
||||
rejectedCount: number
|
||||
}
|
||||
|
||||
export interface ChunkPlanner {
|
||||
/** 分配模块到 chunk;未匹配返回 undefined(交给 Vite 默认拆包) */
|
||||
assign: (moduleId: string, entryName?: string) => string | undefined
|
||||
/** 生成 manualChunks 配置对象(chunk 名 → patterns) */
|
||||
manualChunksConfig: () => Record<string, string[]>
|
||||
/** 每个入口实际包含的 chunk 清单(公共 vendor + 页面私有) */
|
||||
planByEntry: () => ChunkPlan[]
|
||||
stats: () => ChunkPlannerStats
|
||||
}
|
||||
|
||||
export function createChunkPlanner(options: ChunkPlannerOptions): ChunkPlanner {
|
||||
if (!Array.isArray(options.entries)) {
|
||||
throw new Error('entries 必须是数组')
|
||||
}
|
||||
const maxRules = options.maxRules ?? 100
|
||||
const maxEntries = options.maxEntries ?? 100
|
||||
if (!(maxRules > 0)) {
|
||||
throw new Error('maxRules 必须为正数: ' + maxRules)
|
||||
}
|
||||
if (!(maxEntries > 0)) {
|
||||
throw new Error('maxEntries 必须为正数: ' + maxEntries)
|
||||
}
|
||||
|
||||
const entries = options.entries.slice(0, maxEntries)
|
||||
const entrySet = new Set(entries)
|
||||
const vendorRules: Array<{ chunk: string; patterns: string[] }> = []
|
||||
const perEntryRulesByEntry = new Map<string, Array<{ chunk: string; patterns: string[] }>>()
|
||||
const chunkNames: string[] = []
|
||||
let rejectedCount = 0
|
||||
let unmatchedCount = 0
|
||||
|
||||
function validateRule(rule: ChunkRule) {
|
||||
if (typeof rule.chunk !== 'string' || rule.chunk.length === 0) {
|
||||
throw new Error('chunk 名不能为空')
|
||||
}
|
||||
if (!Array.isArray(rule.patterns) || rule.patterns.length === 0) {
|
||||
throw new Error('patterns 必须是非空数组')
|
||||
}
|
||||
}
|
||||
|
||||
function mergePatterns(target: string[] | undefined, patterns: string[]): string[] {
|
||||
const result = target ? [...target] : []
|
||||
for (const pattern of patterns) {
|
||||
if (!result.includes(pattern)) result.push(pattern)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function addVendorRule(rule: ChunkRule) {
|
||||
validateRule(rule)
|
||||
if (vendorRules.length >= maxRules) {
|
||||
rejectedCount += 1
|
||||
return
|
||||
}
|
||||
const existing = vendorRules.find((item) => item.chunk === rule.chunk)
|
||||
if (existing) {
|
||||
existing.patterns = mergePatterns(existing.patterns, rule.patterns)
|
||||
} else {
|
||||
vendorRules.push({ chunk: rule.chunk, patterns: [...rule.patterns] })
|
||||
chunkNames.push(rule.chunk)
|
||||
}
|
||||
}
|
||||
|
||||
function addPerEntryRule(rule: EntryChunkRule) {
|
||||
if (typeof rule.entry !== 'string' || rule.entry.length === 0) {
|
||||
throw new Error('entry 名不能为空')
|
||||
}
|
||||
validateRule(rule)
|
||||
if (!entrySet.has(rule.entry)) return
|
||||
const rules = perEntryRulesByEntry.get(rule.entry) ?? []
|
||||
const existing = rules.find((item) => item.chunk === rule.chunk)
|
||||
if (existing) {
|
||||
existing.patterns = mergePatterns(existing.patterns, rule.patterns)
|
||||
} else {
|
||||
rules.push({ chunk: rule.chunk, patterns: [...rule.patterns] })
|
||||
perEntryRulesByEntry.set(rule.entry, rules)
|
||||
chunkNames.push(rule.chunk)
|
||||
}
|
||||
}
|
||||
|
||||
for (const rule of options.vendorRules ?? []) {
|
||||
addVendorRule(rule)
|
||||
}
|
||||
for (const rule of options.perEntryRules ?? []) {
|
||||
addPerEntryRule(rule)
|
||||
}
|
||||
|
||||
function matches(patterns: string[], moduleId: string): boolean {
|
||||
for (const pattern of patterns) {
|
||||
if (moduleId.includes(pattern)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function assign(moduleId: string, entryName?: string): string | undefined {
|
||||
for (const rule of vendorRules) {
|
||||
if (matches(rule.patterns, moduleId)) return rule.chunk
|
||||
}
|
||||
if (entryName !== undefined) {
|
||||
const rules = perEntryRulesByEntry.get(entryName)
|
||||
if (rules) {
|
||||
for (const rule of rules) {
|
||||
if (matches(rule.patterns, moduleId)) return rule.chunk
|
||||
}
|
||||
}
|
||||
}
|
||||
unmatchedCount += 1
|
||||
return undefined
|
||||
}
|
||||
|
||||
function manualChunksConfig(): Record<string, string[]> {
|
||||
const config: Record<string, string[]> = {}
|
||||
for (const rule of vendorRules) {
|
||||
config[rule.chunk] = mergePatterns(config[rule.chunk], rule.patterns).sort()
|
||||
}
|
||||
for (const rules of perEntryRulesByEntry.values()) {
|
||||
for (const rule of rules) {
|
||||
config[rule.chunk] = mergePatterns(config[rule.chunk], rule.patterns).sort()
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
function planByEntry(): ChunkPlan[] {
|
||||
const vendorNames = vendorRules.map((rule) => rule.chunk)
|
||||
return entries.map((entry) => ({
|
||||
entry,
|
||||
chunks: [...vendorNames, ...(perEntryRulesByEntry.get(entry)?.map((rule) => rule.chunk) ?? [])],
|
||||
}))
|
||||
}
|
||||
|
||||
function stats(): ChunkPlannerStats {
|
||||
return {
|
||||
entryCount: entries.length,
|
||||
chunkCount: chunkNames.length,
|
||||
sharedChunkCount: vendorRules.length,
|
||||
unmatchedCount,
|
||||
rejectedCount,
|
||||
}
|
||||
}
|
||||
|
||||
return { assign, manualChunksConfig, planByEntry, stats }
|
||||
}
|
||||
Reference in New Issue
Block a user