分析 web-ele 项目的框架和技术栈
项目概述
这是一个经济模型测算系统(Economic Model System),用于创建、管理和计算经济预测模型,支持多维度数据录入、复杂公式计算和 Excel 导入导出。
技术栈
核心技术
- 前端框架: Vue 3 + TypeScript
- 构建工具: Vite
- 状态管理: Pinia
- 路由: Vue Router(支持 Hash/History)
-
UI 组件库:
- Element Plus(主要 UI)
- VXE Table(表格)
- VXE PC UI(企业级组件)
核心依赖
-
公式计算:
-
fengari/fengari-web(Lua 引擎) -
lua-in-js(Lua 脚本支持) -
decimal.js(高精度计算)
-
-
表格处理:
-
handsontable/@handsontable/vue3(类 Excel 编辑) -
exceljs(Excel 处理) -
xlsx(Excel 解析)
-
-
工具库:
@vueuse/corevue-hooks-plusdayjslodash.clonedeep
工程化
- Monorepo: pnpm workspace
- 代码规范: ESLint + Prettier + Stylelint
- 测试: Vitest + Testing Library
- 类型检查: vue-tsc
核心功能模块
1. 模型管理
- 创建/编辑经济模型
- 模型版本管理
- 模型配置(时间维度:年/季/月)
2. 指标配置
- 指标 CRUD
- 指标分类与层级
- 指标与模型关联
3. 公式计算引擎(核心)
- 支持复杂公式表达式(类似 Excel)
- 依赖关系分析与拓扑排序
- 循环依赖检测
- 特殊函数支持:
-
total-code(总计) -
periodAdd-code(周期累计) -
prev-code(上期数据) -
prevPeriodAdd-code(往期累计) -
futurePeriodAdd-code(后期累计) -
totalYear-code(年度总和) -
arrayAllValue-code(数组数据)
-
- 全局变量支持(如
global-periodNumber、global-targetIndustry) - Lua 脚本支持(复杂计算逻辑)
4. 数据编辑
- 类 Excel 表格编辑(基于 Handsontable)
- 实时计算与更新
- 数据验证
- 自动保存
- 分页加载(大数据量)
5. 实例管理
- 模型实例创建与编辑
- 实例数据录入
- 数据版本管理
- 数据导入导出(Excel)
6. 性能优化
- Web Worker 处理复杂计算(排序、Excel 处理)
- 拓扑排序优化计算顺序
- 依赖图缓存
- 增量更新
项目架构特点
1. 模块化设计
- 使用
@vben/*内部包:-
@vben/access(权限) -
@vben/layouts(布局) -
@vben/request(请求) -
@vben/stores(状态) -
@vben/utils(工具) -
@vben/hooks(组合式函数)
-
2. 代码组织
src/
├── api/ # API 接口层
├── components/ # 公共组件
├── views/ # 页面视图
│ ├── dashboard/ # 仪表盘
│ ├── model/ # 模型管理
│ ├── instance/ # 实例管理(核心)
│ └── table/ # 数据表管理
├── router/ # 路由配置
├── store/ # 状态管理
├── formula/ # 公式相关
├── hooks/ # 组合式函数
└── utils/ # 工具函数
3. 核心算法
- 拓扑排序:计算依赖顺序
- 循环检测:避免死循环
- 依赖图构建:解析公式依赖关系
- 增量计算:仅计算变更影响的单元格
面试回答建议
项目介绍(1-2分钟)
"这是一个经济模型测算系统,主要用于企业进行经济预测和财务分析。核心功能包括:模型创建与管理、指标配置、复杂公式计算引擎、类 Excel 的数据编辑界面,以及 Excel 导入导出功能。项目采用 Vue 3 + TypeScript + Vite 技术栈,使用 Monorepo 架构,模块化设计。"
技术亮点(重点)
- 公式计算引擎
- "实现了类似 Excel 的公式计算系统,支持变量引用、依赖分析、拓扑排序,能处理复杂的多层级依赖关系,并具备循环依赖检测机制。"
- 性能优化
- "使用 Web Worker 处理大量数据的排序和 Excel 导出,避免阻塞主线程。通过依赖图分析和增量计算,只计算变更影响的单元格,提升性能。"
- 架构设计
- "采用 Monorepo 架构,将功能拆分为多个内部包(@vben/*),便于复用和维护。使用组合式 API 和自定义 Hooks,代码复用性高。"
- 复杂业务处理
- "支持多种时间维度(年/季/月)的数据计算,实现了周期累计、往期累计、年度累计等复杂业务逻辑,并支持 Lua 脚本扩展。"
遇到的挑战与解决
- 公式依赖关系复杂
- "通过构建依赖图,使用拓扑排序确定计算顺序,并实现循环检测避免死循环。"
- 大数据量性能
- "使用 Web Worker 异步处理,分页加载数据,增量更新计算,优化渲染性能。"
- 公式解析与执行
- "使用正则表达式解析公式中的变量引用,结合 Lua 引擎处理复杂计算逻辑,使用 decimal.js 保证数值精度。"
项目规模
- 代码量:约 136 个文件(Vue + TS)
- 核心模块:模型管理、实例编辑、公式计算、数据管理
- 技术深度:涉及算法(拓扑排序)、性能优化、复杂业务逻辑
可以强调的技能点
- Vue 3 组合式 API 与 TypeScript
- 复杂业务逻辑设计与实现
- 性能优化(Web Worker、增量计算)
- 算法应用(拓扑排序、依赖分析)
- 工程化(Monorepo、模块化)
- 前端架构设计能力
需要我深入某个具体模块或技术点吗?
深入分析核心模块和技术点。先查看关键实现文件:
[7 tools called]
[3 tools called]
查看拓扑排序和函数计算核心的实现:
[5 tools called]
以下是核心模块与技术点的深入分析,便于面试时讲解:
核心模块与技术点深入分析
一、公式计算引擎(核心模块)
1. 架构设计
核心流程:
公式表达式 → 依赖解析 → 依赖图构建 → 拓扑排序 → 按序计算 → 结果更新
关键文件:
-
utils/calculate.ts- 计算执行器 -
utils/dependencies.ts- 依赖图生成 -
math/index.js- 函数执行核心
2. 公式解析机制
变量引用格式:
// 公式示例:(${C10001A0433-2025} + ${C10001A0433-2026}) * 0.1
const VARIABLE_REGEX = /\$\{([^${}]+)\}/g;
支持的变量类型:
-
普通指标:
${metricCode-date}- 例如:
${C10001A0433-2025}
- 例如:
-
全局变量(不产生依赖):
-
global-arrayAllDate- 所有日期数组 -
global-periodNumber- 总期数 -
global-targetIndustry- 目标行业 -
var-everyPeriod- 当前期数
-
-
特殊表达式(产生多依赖):
-
prevPeriodAdd-code- 往期累计(不含当期) -
periodAdd-code- 周期累计(含当期) -
total-code- 所有期间总和 -
totalYear-code- 年度总和 -
prev-code- 上期数据
-
3. 依赖图构建算法
实现逻辑(dependencies.ts):
// 1. 解析公式中的所有变量引用
const matches = expression.match(VARIABLE_REGEX);
// 2. 对每个变量进行分类处理
for (const match of matches) {
const content = match.slice(2, -1); // 提取 ${...} 中的内容
if (content in globalHandlers) {
// 全局变量,不产生依赖
continue;
} else if (content.includes('-')) {
// 特殊表达式,如 prevPeriodAdd-code
const [prefix, code] = content.split('-');
const handler = expressionHandlers[prefix];
const deps = handler(code, date, context); // 返回依赖数组
dependencies.push(...deps);
} else {
// 普通指标
dependencies.push(`${content}-${date}`);
}
}
// 3. 构建依赖图:{ "cell-id": ["dep1", "dep2", ...] }
result[`${code}-${date}`] = dependencies;
特殊表达式处理示例:
// prevPeriodAdd: 往期累计(不包含当前周期)
prevPeriodAdd: (code, date, context) => {
const { dateFields, dateIndexMap } = context;
const index = dateIndexMap.get(date);
return dateFields
.slice(0, index) // 只取当前日期之前的所有日期
.map(field => `${code}-${field}`);
}
// 结果:如果 date = "2026",返回 ["code-2025"]
4. 循环依赖检测
算法:DFS + 递归栈
function detectCycles(dag: DAG): string[][] {
const visited = new Set<string>();
const recursionStack = new Set<string>(); // 当前递归路径
const cycles: string[][] = [];
const checkCycle = (node: string, currentPath: string[]) => {
if (recursionStack.has(node)) {
// 发现环:从环起点到当前节点
const cycleStartIndex = currentPath.indexOf(node);
const cyclePath = currentPath.slice(cycleStartIndex);
cyclePath.push(node); // 闭合环
cycles.push(cyclePath);
return;
}
if (visited.has(node)) return;
visited.add(node);
recursionStack.add(node);
currentPath.push(node);
// 递归检查所有邻接节点
const neighbors = dag[node] || [];
for (const neighbor of neighbors) {
checkCycle(neighbor, currentPath);
}
// 回溯
recursionStack.delete(node);
currentPath.pop();
};
// 遍历所有节点
for (const node of Object.keys(dag)) {
if (!visited.has(node)) {
checkCycle(node, []);
}
}
return cycles;
}
处理策略:
- 检测到环后,移除环节点,构建无环 DAG
- 环节点单独处理或标记为错误
二、拓扑排序算法(Kahn 算法)
1. 算法原理
Kahn 算法(BFS 实现):
function kahnTopologicalSort(graph: Record<string, string[]>) {
// 1. 计算入度(被依赖次数)
const inDegree: Record<string, number> = {};
const adj: Record<string, string[]> = {}; // 反向邻接表
// 初始化所有节点入度为 0
Object.keys(graph).forEach(key => {
inDegree[key] = 0;
});
// 构建反向邻接表并计算入度
Object.entries(graph).forEach(([key, deps]) => {
deps.forEach(dep => {
if (!adj[dep]) adj[dep] = [];
adj[dep].push(key); // dep -> key(反向)
inDegree[key]++; // key 的入度 +1
});
});
// 2. 初始化队列:入度为 0 的节点
const queue = Object.keys(inDegree).filter(n => inDegree[n] === 0);
const order: string[] = [];
// 3. Kahn 算法主循环
let queueIndex = 0; // 优化:使用索引指针,避免 shift() 的 O(n) 操作
while (queueIndex < queue.length) {
const node = queue[queueIndex++];
order.push(node);
// 处理依赖当前节点的所有节点
const neighbors = adj[node] || [];
for (const nbr of neighbors) {
inDegree[nbr]--;
if (inDegree[nbr] === 0) {
queue.push(nbr);
}
}
}
// 4. 检测环
const hasCycle = order.length !== Object.keys(inDegree).length;
return { order, hasCycle };
}
2. 性能优化
- 索引指针队列:用索引替代
shift(),避免 O(n) - 排序稳定性:对队列和邻接表排序,保证结果稳定
- 二分插入:使用二分查找插入新节点,保持队列有序
// 二分插入优化
const insertSorted = (arr: string[], item: string) => {
let high = arr.length;
let low = 0;
while (low < high) {
const mid = (low + high) >>> 1; // 位运算优化
if (arr[mid].localeCompare(item) < 0) {
low = mid + 1;
} else {
high = mid;
}
}
arr.splice(low, 0, item);
};
三、计算执行引擎
1. 计算流程
export default async function calculate(instance, graph: string[]) {
// 1. 预计算常量
const precomputed = {
periodMonths: timeType === 'year' ? 12 : 3,
periodNumber: dateFields.length,
arrayAllDate: JSON.stringify(dateFields),
// ...
};
// 2. 全局变量处理器
const globalHandlers = {
'global-periodNumber': precomputed.periodNumber,
'var-everyPeriod': (date) => dateFields.indexOf(date) + 1,
// ...
};
// 3. 特殊表达式处理器
const expressionHandlers = {
prevPeriodAdd: (code, date) => { /* 往期累计 */ },
periodAdd: (code, date) => { /* 周期累计 */ },
// ...
};
// 4. 按拓扑排序顺序计算每个单元格
graph.forEach(cellId => {
const [metricCode, date] = cellId.split('-');
const expression = getFormula(metricCode);
// 解析变量并替换为实际值
const values = {};
const matches = expression.match(VARIABLE_REGEX);
for (const match of matches) {
const content = match.slice(2, -1);
if (content in globalHandlers) {
values[match] = globalHandlers[content];
} else if (content.includes('-')) {
const [prefix, code] = content.split('-');
values[match] = expressionHandlers[prefix]?.(code, date);
} else {
values[match] = getData(content, date) || 0;
}
}
// 替换变量为值
const newExpr = expression
.replaceAll(VARIABLE_REGEX, m => values[m] ?? m)
.replaceAll('--', '+'); // 处理连续负号
// 执行计算
const result = core.executeFunction(newExpr);
// 更新数据
updateData(metricCode, date, result);
});
}
2. 函数执行核心
FunctionCore 实现:
export default class FunctionCore {
createSafeContext() {
const context = {};
// 添加自定义数学函数
const mathFunctions = {
...Q10000A0044, // IRR 计算
...Q10000A0045, // 其他自定义函数
...xirr, // XIRR
...xnpv, // XNPV
};
Object.assign(context, mathFunctions);
return context;
}
executeFunction(str) {
try {
const context = this.createSafeContext();
// 使用 Function 构造器,安全执行
const fn = new Function(...Object.keys(context), `return ${str};`);
return fn(...Object.values(context));
} catch (error) {
console.error('Function execution error:', error);
return '#ERROR';
}
}
}
安全机制:
- 使用
Function构造器而非eval - 限制可用函数(白名单)
- 错误捕获,返回
#ERROR
四、Web Worker 性能优化
1. 拓扑排序 Worker
主线程:
const sortWorker = new Worker(
new URL('./workers/sort/index.ts', import.meta.url),
{ type: 'module' }
);
sortWorker.postMessage({
formula,
instance: { dateFields, forecastTimeType }
});
sortWorker.onmessage = (e) => {
if (e.data.success) {
const order = e.data.order; // 拓扑排序结果
calculate(instance, order);
}
};
Worker 线程:
// workers/sort/index.ts
globalThis.addEventListener('message', async (e) => {
const { formula, instance } = JSON.parse(e.data);
// 生成依赖关系图
const relation = generateRelation(instance, formula);
// 执行拓扑排序
const order = await getAllIds(true, instance, relation);
// 返回结果
globalThis.postMessage({ success: true, order });
});
2. Excel 处理 Worker
用于大数据量 Excel 导入导出,避免阻塞主线程。
五、面试回答要点
1. 公式计算引擎设计
回答框架:
"我设计了一个类似 Excel 的公式计算引擎。核心包括:
公式解析:使用正则表达式
/\$\{([^${}]+)\}/g提取变量引用,支持普通指标、全局变量和特殊表达式(如prevPeriodAdd、total等)。依赖图构建:遍历所有公式,解析每个单元格的依赖关系,构建有向图。特殊表达式会生成多个依赖(如
total-code依赖所有期间的数据)。循环检测:使用 DFS + 递归栈检测循环依赖,发现环后移除环节点,构建无环 DAG。
拓扑排序:使用 Kahn 算法对依赖图进行拓扑排序,确定计算顺序。优化包括使用索引指针队列、二分插入等。
按序计算:按拓扑排序结果依次计算,解析变量、替换值、执行表达式、更新结果。
性能优化:使用 Web Worker 处理拓扑排序,避免阻塞主线程;增量计算只计算变更影响的单元格。"
2. 拓扑排序算法
回答框架:
"使用 Kahn 算法实现拓扑排序:
计算入度:统计每个节点被依赖的次数。
初始化队列:将所有入度为 0 的节点加入队列。
BFS 遍历:从队列取出节点,将其加入结果;遍历依赖该节点的所有节点,减少它们的入度;如果入度变为 0,加入队列。
环检测:如果结果数量不等于节点总数,说明存在环。
优化点:
- 使用索引指针替代
shift(),避免 O(n) 操作- 对队列和邻接表排序,保证结果稳定性
- 使用二分查找插入新节点,保持队列有序"
3. 性能优化策略
回答框架:
"针对大数据量场景,做了以下优化:
Web Worker:将拓扑排序和 Excel 处理放到 Worker 线程,避免阻塞主线程。
增量计算:构建反向依赖图,当某个单元格变更时,只重新计算受影响的单元格。
依赖图缓存:依赖图构建后缓存,公式不变时复用。
分页加载:表格数据分页加载,减少初始渲染压力。
算法优化:Kahn 算法使用索引指针、二分插入等优化,提升排序性能。"
4. 遇到的挑战与解决
回答框架:
"主要挑战:
循环依赖:公式可能存在循环引用。解决:实现 DFS 环检测算法,检测到环后移除环节点,构建无环 DAG,环节点单独处理。
特殊表达式依赖:如
total-code需要依赖所有期间。解决:为每种特殊表达式实现处理器,返回依赖数组。性能问题:大量公式计算导致页面卡顿。解决:使用 Web Worker 处理拓扑排序,增量计算只更新变更影响的单元格。
计算精度:使用
decimal.js保证数值精度,避免浮点数误差。"
六、技术亮点总结
- 算法应用:Kahn 拓扑排序、DFS 环检测
- 性能优化:Web Worker、增量计算、算法优化
- 架构设计:模块化、依赖图、计算引擎
- 业务理解:复杂公式解析、特殊表达式处理
- 工程实践:错误处理、日志记录、调试工具