经济模型项目: 分析 web-ele 项目的框架和技术栈

分析 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/core
    • vue-hooks-plus
    • dayjs
    • lodash.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-periodNumberglobal-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 架构,模块化设计。"

技术亮点(重点)

  1. 公式计算引擎
    • "实现了类似 Excel 的公式计算系统,支持变量引用、依赖分析、拓扑排序,能处理复杂的多层级依赖关系,并具备循环依赖检测机制。"
  2. 性能优化
    • "使用 Web Worker 处理大量数据的排序和 Excel 导出,避免阻塞主线程。通过依赖图分析和增量计算,只计算变更影响的单元格,提升性能。"
  3. 架构设计
    • "采用 Monorepo 架构,将功能拆分为多个内部包(@vben/*),便于复用和维护。使用组合式 API 和自定义 Hooks,代码复用性高。"
  4. 复杂业务处理
    • "支持多种时间维度(年/季/月)的数据计算,实现了周期累计、往期累计、年度累计等复杂业务逻辑,并支持 Lua 脚本扩展。"

遇到的挑战与解决

  1. 公式依赖关系复杂
    • "通过构建依赖图,使用拓扑排序确定计算顺序,并实现循环检测避免死循环。"
  2. 大数据量性能
    • "使用 Web Worker 异步处理,分页加载数据,增量更新计算,优化渲染性能。"
  3. 公式解析与执行
    • "使用正则表达式解析公式中的变量引用,结合 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;

支持的变量类型:

  1. 普通指标:${metricCode-date}

    • 例如:${C10001A0433-2025}
  2. 全局变量(不产生依赖):

    • global-arrayAllDate - 所有日期数组
    • global-periodNumber - 总期数
    • global-targetIndustry - 目标行业
    • var-everyPeriod - 当前期数
  3. 特殊表达式(产生多依赖):

    • 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. 性能优化

  1. 索引指针队列:用索引替代 shift(),避免 O(n)
  2. 排序稳定性:对队列和邻接表排序,保证结果稳定
  3. 二分插入:使用二分查找插入新节点,保持队列有序
// 二分插入优化
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 的公式计算引擎。核心包括:

  1. 公式解析:使用正则表达式 /\$\{([^${}]+)\}/g 提取变量引用,支持普通指标、全局变量和特殊表达式(如 prevPeriodAddtotal 等)。

  2. 依赖图构建:遍历所有公式,解析每个单元格的依赖关系,构建有向图。特殊表达式会生成多个依赖(如 total-code 依赖所有期间的数据)。

  3. 循环检测:使用 DFS + 递归栈检测循环依赖,发现环后移除环节点,构建无环 DAG。

  4. 拓扑排序:使用 Kahn 算法对依赖图进行拓扑排序,确定计算顺序。优化包括使用索引指针队列、二分插入等。

  5. 按序计算:按拓扑排序结果依次计算,解析变量、替换值、执行表达式、更新结果。

性能优化:使用 Web Worker 处理拓扑排序,避免阻塞主线程;增量计算只计算变更影响的单元格。"

2. 拓扑排序算法

回答框架:

"使用 Kahn 算法实现拓扑排序:

  1. 计算入度:统计每个节点被依赖的次数。

  2. 初始化队列:将所有入度为 0 的节点加入队列。

  3. BFS 遍历:从队列取出节点,将其加入结果;遍历依赖该节点的所有节点,减少它们的入度;如果入度变为 0,加入队列。

  4. 环检测:如果结果数量不等于节点总数,说明存在环。

优化点:

  • 使用索引指针替代 shift(),避免 O(n) 操作
  • 对队列和邻接表排序,保证结果稳定性
  • 使用二分查找插入新节点,保持队列有序"

3. 性能优化策略

回答框架:

"针对大数据量场景,做了以下优化:

  1. Web Worker:将拓扑排序和 Excel 处理放到 Worker 线程,避免阻塞主线程。

  2. 增量计算:构建反向依赖图,当某个单元格变更时,只重新计算受影响的单元格。

  3. 依赖图缓存:依赖图构建后缓存,公式不变时复用。

  4. 分页加载:表格数据分页加载,减少初始渲染压力。

  5. 算法优化:Kahn 算法使用索引指针、二分插入等优化,提升排序性能。"

4. 遇到的挑战与解决

回答框架:

"主要挑战:

  1. 循环依赖:公式可能存在循环引用。解决:实现 DFS 环检测算法,检测到环后移除环节点,构建无环 DAG,环节点单独处理。

  2. 特殊表达式依赖:如 total-code 需要依赖所有期间。解决:为每种特殊表达式实现处理器,返回依赖数组。

  3. 性能问题:大量公式计算导致页面卡顿。解决:使用 Web Worker 处理拓扑排序,增量计算只更新变更影响的单元格。

  4. 计算精度:使用 decimal.js 保证数值精度,避免浮点数误差。"


六、技术亮点总结

  1. 算法应用:Kahn 拓扑排序、DFS 环检测
  2. 性能优化:Web Worker、增量计算、算法优化
  3. 架构设计:模块化、依赖图、计算引擎
  4. 业务理解:复杂公式解析、特殊表达式处理
  5. 工程实践:错误处理、日志记录、调试工具

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容